From f324477dd06c15022670a4486bf83be3abbf48b5 Mon Sep 17 00:00:00 2001 From: whitequark Date: Thu, 12 Jul 2018 19:29:44 +0000 Subject: [PATCH] Implement a platform abstraction for windows. This commit removes a large amount of code partially duplicated between the text and the graphics windows, and opens the path to having more than one model window on screen at any given time, as well as simplifies platform work. This commit also adds complete support for High-DPI device pixel ratio. It adds support for font scale factor (a fractional factor on top of integral device pixel ratio) on the platform side, but not on the application side. This commit also adds error checking to all Windows API calls (within the abstracted code) and fixes a significant number of misuses and non-future-proof uses of Windows API. This commit also makes uses of Windows API idiomatic, e.g. using the built-in vertical scroll bar, native tooltips, control subclassing instead of hooks in the global dispatch loop, and so on. It reinstates tooltip support and removes menu-related hacks. --- CHANGELOG.md | 1 + res/CMakeLists.txt | 2 +- res/win32/manifest.xml | 13 +- src/CMakeLists.txt | 2 +- src/clipboard.cpp | 2 +- src/confscreen.cpp | 47 +- src/constraint.cpp | 2 +- src/draw.cpp | 43 +- src/export.cpp | 6 +- src/expr.cpp | 26 +- src/expr.h | 4 +- src/generate.cpp | 2 +- src/graphicswin.cpp | 130 ++- src/mouse.cpp | 119 ++- src/platform/climain.cpp | 32 +- src/platform/cocoamain.mm | 662 +------------ src/platform/gtkmain.cpp | 682 +------------ src/platform/gui.h | 112 ++- src/platform/guigtk.cpp | 594 ++++++++++- src/platform/guimac.mm | 689 ++++++++++++- src/platform/guinone.cpp | 170 +--- src/platform/guiwin.cpp | 933 +++++++++++++++++- src/platform/w32main.cpp | 892 +---------------- src/render/render.cpp | 2 +- src/render/render.h | 32 +- src/render/rendercairo.cpp | 54 +- src/render/rendergl1.cpp | 4 +- src/render/rendergl3.cpp | 7 +- src/resource.cpp | 14 +- src/resource.h | 2 + src/solvespace.cpp | 64 +- src/solvespace.h | 28 +- src/style.cpp | 17 +- src/textscreens.cpp | 85 +- src/textwin.cpp | 170 ++-- src/toolbar.cpp | 59 +- src/ui.h | 39 +- src/view.cpp | 8 +- test/constraint/arc_line_tangent/normal.png | Bin 5097 -> 5101 bytes .../curve_curve_tangent/arc_arc.png | Bin 5001 -> 5017 bytes .../curve_curve_tangent/arc_cubic.png | Bin 5217 -> 5219 bytes test/constraint/diameter/normal.png | Bin 5026 -> 5023 bytes test/constraint/diameter/reference.png | Bin 5108 -> 5100 bytes test/constraint/equal_line_arc_len/normal.png | Bin 4547 -> 4554 bytes test/constraint/equal_line_arc_len/pi.png | Bin 4651 -> 4672 bytes test/constraint/equal_line_arc_len/tau.png | Bin 4970 -> 4966 bytes test/constraint/equal_radius/normal.png | Bin 5255 -> 5249 bytes test/constraint/pt_on_circle/normal.png | Bin 4884 -> 4879 bytes test/constraint/pt_on_face/normal.png | Bin 4663 -> 4661 bytes test/group/translate_asy/test.cpp | 1 + test/harness.cpp | 40 +- test/request/arc_of_circle/normal.png | Bin 4552 -> 4569 bytes test/request/circle/free_in_3d.png | Bin 4806 -> 4803 bytes test/request/circle/free_in_3d_dof.png | Bin 7197 -> 7197 bytes test/request/circle/normal.png | Bin 4806 -> 4803 bytes test/request/circle/normal_dof.png | Bin 4820 -> 4836 bytes test/request/cubic/normal.png | Bin 4976 -> 4947 bytes test/request/cubic_periodic/normal.png | Bin 5216 -> 5223 bytes test/request/ttf_text/normal.png | Bin 6158 -> 6162 bytes 59 files changed, 3077 insertions(+), 2714 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f97a4c90..c8c6f0ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ New measurement/analysis features: Other new features: * New command-line interface, for batch exporting and more. + * The graphical interface now supports HiDPI screens on every OS. * New link to match the on-screen size of the sketch with its actual size, "view → set to full scale". * When zooming to fit, constraints are also considered. diff --git a/res/CMakeLists.txt b/res/CMakeLists.txt index d35b5043..1a9e73d2 100644 --- a/res/CMakeLists.txt +++ b/res/CMakeLists.txt @@ -118,7 +118,7 @@ endfunction() # Second, register all resources. if(WIN32) add_resource(win32/icon.ico 4000 ICON) - add_resource(win32/manifest.xml 2 RT_MANIFEST) + add_resource(win32/manifest.xml 1 RT_MANIFEST) elseif(APPLE) add_iconset (cocoa/AppIcon.iconset) add_xib (cocoa/MainMenu.xib) diff --git a/res/win32/manifest.xml b/res/win32/manifest.xml index 82834364..7c49a2e3 100644 --- a/res/win32/manifest.xml +++ b/res/win32/manifest.xml @@ -1,17 +1,18 @@ - + - - - true - - Parametric 3d CAD tool. + + + PerMonitorV2, PerMonitor + true + + Use Perspective Projection.")); } - InvalidateGraphics(); + SS.GW.Invalidate(); break; } case Edit::GRID_SPACING: { SS.gridSpacing = (float)min(1e4, max(1e-3, SS.StringToMm(s))); - InvalidateGraphics(); + SS.GW.Invalidate(); break; } case Edit::DIGITS_AFTER_DECIMAL: { - int v = atoi(s); + int v = atoi(s.c_str()); if(v < 0 || v > 8) { Error(_("Specify between 0 and 8 digits after the decimal.")); } else { SS.SetUnitDigitsAfterDecimal(v); } - InvalidateGraphics(); + SS.GW.Invalidate(); break; } case Edit::EXPORT_SCALE: { @@ -455,8 +454,8 @@ bool TextWindow::EditControlDoneForConfiguration(const char *s) { break; } case Edit::AUTOSAVE_INTERVAL: { - int interval; - if(sscanf(s, "%d", &interval)==1) { + int interval = atoi(s.c_str()); + if(interval) { if(interval >= 1) { SS.autosaveInterval = interval; SS.ScheduleAutosave(); diff --git a/src/constraint.cpp b/src/constraint.cpp index 516b8b01..a48ef10e 100644 --- a/src/constraint.cpp +++ b/src/constraint.cpp @@ -744,7 +744,7 @@ void Constraint::MenuConstrain(Command id) { } SS.GW.ClearSelection(); - InvalidateGraphics(); + SS.GW.Invalidate(); } #endif /* ! LIBRARY */ diff --git a/src/draw.cpp b/src/draw.cpp index d79c9bfd..86ea59ed 100644 --- a/src/draw.cpp +++ b/src/draw.cpp @@ -80,7 +80,7 @@ void GraphicsWindow::Selection::Draw(bool isHovered, Canvas *canvas) { void GraphicsWindow::ClearSelection() { selection.Clear(); SS.ScheduleShowTW(); - InvalidateGraphics(); + Invalidate(); } void GraphicsWindow::ClearNonexistentSelectionItems() { @@ -98,7 +98,7 @@ void GraphicsWindow::ClearNonexistentSelectionItems() { } } selection.RemoveTagged(); - if(change) InvalidateGraphics(); + if(change) Invalidate(); } //----------------------------------------------------------------------------- @@ -304,14 +304,14 @@ void GraphicsWindow::GroupSelection() { Camera GraphicsWindow::GetCamera() const { Camera camera = {}; - camera.width = (int)width; - camera.height = (int)height; - camera.offset = offset; - camera.projUp = projUp; - camera.projRight = projRight; - camera.scale = scale; - camera.tangent = SS.CameraTangent(); - camera.hasPixels = true; + window->GetContentSize(&camera.width, &camera.height); + camera.pixelRatio = window->GetDevicePixelRatio(); + camera.gridFit = (window->GetDevicePixelRatio() == 1); + camera.offset = offset; + camera.projUp = projUp; + camera.projRight = projRight; + camera.scale = scale; + camera.tangent = SS.CameraTangent(); return camera; } @@ -457,7 +457,7 @@ void GraphicsWindow::HitTestMakeSelection(Point2d mp) { if(!sel.Equals(&hover)) { hover = sel; - InvalidateGraphics(); + Invalidate(); } } @@ -545,6 +545,10 @@ void GraphicsWindow::NormalizeProjectionVectors() { void GraphicsWindow::DrawSnapGrid(Canvas *canvas) { if(!LockedInWorkplane()) return; + const Camera &camera = canvas->GetCamera(); + double width = camera.width, + height = camera.height; + hEntity he = ActiveWorkplane(); EntityBase *wrkpl = SK.GetEntity(he), *norm = wrkpl->Normal(); @@ -816,15 +820,11 @@ void GraphicsWindow::Draw(Canvas *canvas) { } void GraphicsWindow::Paint() { - if(!canvas) return; + ssassert(window != NULL && canvas != NULL, + "Cannot paint without window and canvas"); havePainted = true; - int w, h; - GetGraphicsWindowSize(&w, &h); - width = w; - height = h; - Camera camera = GetCamera(); Lighting lighting = GetLighting(); @@ -902,3 +902,12 @@ void GraphicsWindow::Paint() { canvas->FlushFrame(); canvas->Clear(); } + +void GraphicsWindow::Invalidate(bool clearPersistent) { + if(window) { + if(clearPersistent) { + persistentDirty = true; + } + window->Invalidate(); + } +} diff --git a/src/export.cpp b/src/export.cpp index bafd6852..d68dce05 100644 --- a/src/export.cpp +++ b/src/export.cpp @@ -263,7 +263,7 @@ void SolveSpaceUI::ExportViewOrWireframeTo(const Platform::Path &filename, bool } SS.justExportedInfo.draw = true; - InvalidateGraphics(); + GW.Invalidate(); } edges.Clear(); @@ -839,7 +839,7 @@ void SolveSpaceUI::ExportMeshTo(const Platform::Path &filename) { SS.justExportedInfo.showOrigin = false; SS.justExportedInfo.draw = true; - InvalidateGraphics(); + GW.Invalidate(); } //----------------------------------------------------------------------------- @@ -1108,5 +1108,5 @@ void SolveSpaceUI::ExportMeshAsThreeJsTo(FILE *f, const Platform::Path &filename void SolveSpaceUI::ExportAsPngTo(const Platform::Path &filename) { screenshotFile = filename; // The rest of the work is done in the next redraw. - InvalidateGraphics(); + GW.Invalidate(); } diff --git a/src/expr.cpp b/src/expr.cpp index cbff0385..700d6b69 100644 --- a/src/expr.cpp +++ b/src/expr.cpp @@ -605,8 +605,7 @@ public: bool IsError() const { return type == TokenType::ERROR; } }; - const char *input; - unsigned inputPos; + std::string::const_iterator it, end; std::vector stack; char ReadChar(); @@ -624,7 +623,7 @@ public: bool Reduce(std::string *error); bool Parse(std::string *error, size_t reduceUntil = 0); - static Expr *Parse(const char *input, std::string *error); + static Expr *Parse(const std::string &input, std::string *error); }; ExprParser::Token ExprParser::Token::From(TokenType type, Expr *expr) { @@ -643,11 +642,15 @@ ExprParser::Token ExprParser::Token::From(TokenType type, Expr::Op op) { } char ExprParser::ReadChar() { - return input[inputPos++]; + return *it++; } char ExprParser::PeekChar() { - return input[inputPos]; + if(it == end) { + return '\0'; + } else { + return *it; + } } std::string ExprParser::ReadWord() { @@ -889,10 +892,10 @@ bool ExprParser::Parse(std::string *error, size_t reduceUntil) { return true; } -Expr *ExprParser::Parse(const char *input, std::string *error) { +Expr *ExprParser::Parse(const std::string &input, std::string *error) { ExprParser parser; - parser.input = input; - parser.inputPos = 0; + parser.it = input.cbegin(); + parser.end = input.cend(); if(!parser.Parse(error)) return NULL; Token r = parser.PopOperand(error); @@ -900,17 +903,18 @@ Expr *ExprParser::Parse(const char *input, std::string *error) { return r.expr; } -Expr *Expr::Parse(const char *input, std::string *error) { +Expr *Expr::Parse(const std::string &input, std::string *error) { return ExprParser::Parse(input, error); } -Expr *Expr::From(const char *input, bool popUpError) { +Expr *Expr::From(const std::string &input, bool popUpError) { std::string error; Expr *e = ExprParser::Parse(input, &error); if(!e) { dbp("Parse/lex error: %s", error.c_str()); if(popUpError) { - Error("Not a valid number or expression: '%s'.\n%s.", input, error.c_str()); + Error("Not a valid number or expression: '%s'.\n%s.", + input.c_str(), error.c_str()); } } return e; diff --git a/src/expr.h b/src/expr.h index 83ba23b3..7383d993 100644 --- a/src/expr.h +++ b/src/expr.h @@ -96,8 +96,8 @@ public: Expr *DeepCopyWithParamsAsPointers(IdList *firstTry, IdList *thenTry) const; - static Expr *Parse(const char *input, std::string *error); - static Expr *From(const char *in, bool popUpError); + static Expr *Parse(const std::string &input, std::string *error); + static Expr *From(const std::string &input, bool popUpError); }; class ExprVector { diff --git a/src/generate.cpp b/src/generate.cpp index 62f35fa2..ae37ba5d 100644 --- a/src/generate.cpp +++ b/src/generate.cpp @@ -313,7 +313,7 @@ void SolveSpaceUI::GenerateAll(Generate type, bool andFindFree, bool genForBBox) } prev.Clear(); - InvalidateGraphics(); + GW.Invalidate(); // Remove nonexistent selection items, for same reason we waited till // the end to put up a dialog box. diff --git a/src/graphicswin.cpp b/src/graphicswin.cpp index 77a1f773..6e67415d 100644 --- a/src/graphicswin.cpp +++ b/src/graphicswin.cpp @@ -255,7 +255,7 @@ bool GraphicsWindow::KeyboardEvent(Platform::KeyboardEvent event) { void GraphicsWindow::PopulateMainMenu() { bool unique = false; - mainMenu = Platform::GetOrCreateMainMenu(&unique); + Platform::MenuBarRef mainMenu = Platform::GetOrCreateMainMenu(&unique); if(unique) mainMenu->Clear(); Platform::MenuRef currentSubMenu; @@ -285,8 +285,9 @@ void GraphicsWindow::PopulateMainMenu() { SetLocale(locale.Culture()); CnfFreezeString(locale.Culture(), "Locale"); - SS.UpdateWindowTitle(); + SS.UpdateWindowTitles(); PopulateMainMenu(); + EnsureValidActives(); }); } } else if(Menu[i].fn == NULL) { @@ -331,7 +332,7 @@ void GraphicsWindow::PopulateMainMenu() { PopulateRecentFiles(); SS.UndoEnableMenus(); - SetMainMenu(mainMenu); + window->SetMenuBar(mainMenu); } static void PopulateMenuWithPathnames(Platform::MenuRef menu, @@ -361,15 +362,7 @@ void GraphicsWindow::PopulateRecentFiles() { } void GraphicsWindow::Init() { - PopulateMainMenu(); - - canvas = CreateRenderer(); - if(canvas) { - persistentCanvas = canvas->CreateBatch(); - persistentDirty = true; - } - - scale = 5; + scale = 5; offset = Vector::From(0, 0, 0); projRight = Vector::From(1, 0, 0); projUp = Vector::From(0, 1, 0); @@ -394,11 +387,30 @@ void GraphicsWindow::Init() { drawOccludedAs = DrawOccludedAs::INVISIBLE; showTextWindow = true; - ShowTextWindow(showTextWindow); showSnapGrid = false; context.active = false; + if(!window) { + window = Platform::CreateWindow(); + if(window) { + canvas = CreateRenderer(); + if(canvas) { + persistentCanvas = canvas->CreateBatch(); + persistentDirty = true; + } + + using namespace std::placeholders; + window->onClose = std::bind(&SolveSpaceUI::MenuFile, Command::EXIT); + window->onRender = std::bind(&GraphicsWindow::Paint, this); + window->onKeyboardEvent = std::bind(&GraphicsWindow::KeyboardEvent, this, _1); + window->onMouseEvent = std::bind(&GraphicsWindow::MouseEvent, this, _1); + window->onEditingDone = std::bind(&GraphicsWindow::EditControlDone, this, _1); + window->SetMinContentSize(720, 670); + PopulateMainMenu(); + } + } + // Do this last, so that all the menus get updated correctly. ClearSuper(); } @@ -444,7 +456,7 @@ void GraphicsWindow::AnimateOnto(Quaternion quatf, Vector offsetf) { projRight = quat.RotationU(); projUp = quat.RotationV(); - PaintGraphics(); + window->Redraw(); tn = GetMilliseconds(); s = (tn - t0)/((double)dt); @@ -453,16 +465,17 @@ void GraphicsWindow::AnimateOnto(Quaternion quatf, Vector offsetf) { projRight = quatf.RotationU(); projUp = quatf.RotationV(); offset = offsetf; - InvalidateGraphics(); + Invalidate(); // If the view screen is open, then we need to refresh it. SS.ScheduleShowTW(); } void GraphicsWindow::HandlePointForZoomToFit(Vector p, Point2d *pmax, Point2d *pmin, - double *wmin, bool usePerspective) + double *wmin, bool usePerspective, + const Camera &camera) { double w; - Vector pp = ProjectPoint4(p, &w); + Vector pp = camera.ProjectPoint4(p, &w); // If usePerspective is true, then we calculate a perspective projection of the point. // If not, then we do a parallel projection regardless of the current // scale factor. @@ -480,11 +493,12 @@ void GraphicsWindow::LoopOverPoints(const std::vector &entities, const std::vector &constraints, const std::vector &faces, Point2d *pmax, Point2d *pmin, double *wmin, - bool usePerspective, bool includeMesh) { + bool usePerspective, bool includeMesh, + const Camera &camera) { for(Entity *e : entities) { if(e->IsPoint()) { - HandlePointForZoomToFit(e->PointGetNum(), pmax, pmin, wmin, usePerspective); + HandlePointForZoomToFit(e->PointGetNum(), pmax, pmin, wmin, usePerspective, camera); } else if(e->type == Entity::Type::CIRCLE) { // Lots of entities can extend outside the bbox of their points, // but circles are particularly bad. We want to get things halfway @@ -498,23 +512,23 @@ void GraphicsWindow::LoopOverPoints(const std::vector &entities, (j == 1) ? (c.Plus(q.RotationU().ScaledBy(-r))) : (j == 2) ? (c.Plus(q.RotationV().ScaledBy( r))) : (c.Plus(q.RotationV().ScaledBy(-r))); - HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective); + HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera); } } else { // We have to iterate children points, because we can select entities without points for(int i = 0; i < MAX_POINTS_IN_ENTITY; i++) { if(e->point[i].v == 0) break; Vector p = SK.GetEntity(e->point[i])->PointGetNum(); - HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective); + HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera); } } } for(Constraint *c : constraints) { std::vector refs; - c->GetReferencePoints(GetCamera(), &refs); + c->GetReferencePoints(camera, &refs); for(Vector p : refs) { - HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective); + HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera); } } @@ -533,19 +547,25 @@ void GraphicsWindow::LoopOverPoints(const std::vector &entities, } if(!found) continue; } - HandlePointForZoomToFit(tr->a, pmax, pmin, wmin, usePerspective); - HandlePointForZoomToFit(tr->b, pmax, pmin, wmin, usePerspective); - HandlePointForZoomToFit(tr->c, pmax, pmin, wmin, usePerspective); + HandlePointForZoomToFit(tr->a, pmax, pmin, wmin, usePerspective, camera); + HandlePointForZoomToFit(tr->b, pmax, pmin, wmin, usePerspective, camera); + HandlePointForZoomToFit(tr->c, pmax, pmin, wmin, usePerspective, camera); } if(!includeMesh) return; for(int i = 0; i < g->polyLoops.l.n; i++) { SContour *sc = &(g->polyLoops.l.elem[i]); for(int j = 0; j < sc->l.n; j++) { - HandlePointForZoomToFit(sc->l.elem[j].p, pmax, pmin, wmin, usePerspective); + HandlePointForZoomToFit(sc->l.elem[j].p, pmax, pmin, wmin, usePerspective, camera); } } } void GraphicsWindow::ZoomToFit(bool includingInvisibles, bool useSelection) { + if(!window) return; + + scale = ZoomToFit(GetCamera(), includingInvisibles, useSelection); +} +double GraphicsWindow::ZoomToFit(const Camera &camera, + bool includingInvisibles, bool useSelection) { std::vector entities; std::vector constraints; std::vector faces; @@ -588,7 +608,8 @@ void GraphicsWindow::ZoomToFit(bool includingInvisibles, bool useSelection) { Point2d pmax = { -1e12, -1e12 }, pmin = { 1e12, 1e12 }; double wmin = 1; LoopOverPoints(entities, constraints, faces, &pmax, &pmin, &wmin, - /*usePerspective=*/false, /*includeMesh=*/!selectionUsed); + /*usePerspective=*/false, /*includeMesh=*/!selectionUsed, + camera); double xm = (pmax.x + pmin.x)/2, ym = (pmax.y + pmin.y)/2; double dx = pmax.x - pmin.x, dy = pmax.y - pmin.y; @@ -597,12 +618,13 @@ void GraphicsWindow::ZoomToFit(bool includingInvisibles, bool useSelection) { projUp. ScaledBy(-ym)); // And based on this, we calculate the scale and offset + double scale; if(EXACT(dx == 0 && dy == 0)) { scale = 5; } else { double scalex = 1e12, scaley = 1e12; - if(EXACT(dx != 0)) scalex = 0.9*width /dx; - if(EXACT(dy != 0)) scaley = 0.9*height/dy; + if(EXACT(dx != 0)) scalex = 0.9*camera.width /dx; + if(EXACT(dy != 0)) scaley = 0.9*camera.height/dy; scale = min(scalex, scaley); scale = min(300.0, scale); @@ -614,17 +636,20 @@ void GraphicsWindow::ZoomToFit(bool includingInvisibles, bool useSelection) { pmin.x = 1e12; pmin.y = 1e12; wmin = 1; LoopOverPoints(entities, constraints, faces, &pmax, &pmin, &wmin, - /*usePerspective=*/true, /*includeMesh=*/!selectionUsed); + /*usePerspective=*/true, /*includeMesh=*/!selectionUsed, + camera); // Adjust the scale so that no points are behind the camera if(wmin < 0.1) { - double k = SS.CameraTangent(); + double k = camera.tangent; // w = 1+k*scale*z double zmin = (wmin - 1)/(k*scale); // 0.1 = 1 + k*scale*zmin // (0.1 - 1)/(k*zmin) = scale scale = min(scale, (0.1 - 1)/(k*zmin)); } + + return scale; } void GraphicsWindow::MenuView(Command id) { @@ -650,7 +675,7 @@ void GraphicsWindow::MenuView(Command id) { Message(_("No workplane is active, so the grid will not appear.")); } SS.GW.EnsureValidActives(); - InvalidateGraphics(); + SS.GW.Invalidate(); break; case Command::PERSPECTIVE_PROJ: @@ -663,7 +688,7 @@ void GraphicsWindow::MenuView(Command id) { "is typical.")); } SS.GW.EnsureValidActives(); - InvalidateGraphics(); + SS.GW.Invalidate(); break; case Command::ONTO_WORKPLANE: @@ -745,7 +770,7 @@ void GraphicsWindow::MenuView(Command id) { case Command::SHOW_TOOLBAR: SS.showToolbar = !SS.showToolbar; SS.GW.EnsureValidActives(); - InvalidateGraphics(); + SS.GW.Invalidate(); break; case Command::SHOW_TEXT_WND: @@ -772,13 +797,13 @@ void GraphicsWindow::MenuView(Command id) { break; case Command::FULL_SCREEN: - ToggleFullScreen(); + SS.GW.window->SetFullScreen(!SS.GW.window->IsFullScreen()); SS.GW.EnsureValidActives(); break; default: ssassert(false, "Unexpected menu ID"); } - InvalidateGraphics(); + SS.GW.Invalidate(); } void GraphicsWindow::EnsureValidActives() { @@ -827,6 +852,8 @@ void GraphicsWindow::EnsureValidActives() { } } + if(!window) return; + // And update the checked state for various menus bool locked = LockedInWorkplane(); in3dMenuItem->SetActive(!locked); @@ -847,13 +874,13 @@ void GraphicsWindow::EnsureValidActives() { unitsMetersMenuItem->SetActive(SS.viewUnits == Unit::METERS); unitsInchesMenuItem->SetActive(SS.viewUnits == Unit::INCHES); - ShowTextWindow(SS.GW.showTextWindow); + if(SS.TW.window) SS.TW.window->SetVisible(SS.GW.showTextWindow); showTextWndMenuItem->SetActive(SS.GW.showTextWindow); showGridMenuItem->SetActive(SS.GW.showSnapGrid); perspectiveProjMenuItem->SetActive(SS.usePerspectiveProj); showToolbarMenuItem->SetActive(SS.showToolbar); - fullScreenMenuItem->SetActive(FullScreenIsActive()); + fullScreenMenuItem->SetActive(SS.GW.window->IsFullScreen()); if(change) SS.ScheduleShowTW(); } @@ -877,7 +904,7 @@ void GraphicsWindow::ForceTextWindowShown() { if(!showTextWindow) { showTextWindow = true; showTextWndMenuItem->SetActive(true); - ShowTextWindow(true); + SS.TW.window->SetVisible(true); } } @@ -894,7 +921,7 @@ void GraphicsWindow::DeleteTaggedRequests() { // An edit might be in progress for the just-deleted item. So // now it's not. - HideGraphicsEditControl(); + window->HideEditor(); SS.TW.HideEditControl(); // And clear out the selection, which could contain that item. ClearSuper(); @@ -934,8 +961,8 @@ void GraphicsWindow::MenuEdit(Command id) { SS.GW.gs.constraints == 0 && SS.GW.pending.operation == Pending::NONE) { - if(!(TextEditControlIsVisible() || - GraphicsEditControlIsVisible())) + if(!(SS.TW.window->IsEditorVisible() || + SS.GW.window->IsEditorVisible())) { if(SS.TW.shown.screen == TextWindow::Screen::STYLE_INFO) { SS.TW.GoToScreen(TextWindow::Screen::LIST_OF_STYLES); @@ -971,7 +998,7 @@ void GraphicsWindow::MenuEdit(Command id) { SS.GW.MakeSelected(e->h); } - InvalidateGraphics(); + SS.GW.Invalidate(); SS.ScheduleShowTW(); break; } @@ -1020,7 +1047,7 @@ void GraphicsWindow::MenuEdit(Command id) { if(newlySelected == 0) { Error(_("No additional entities share endpoints with the selected entities.")); } - InvalidateGraphics(); + SS.GW.Invalidate(); SS.ScheduleShowTW(); break; } @@ -1099,7 +1126,7 @@ void GraphicsWindow::MenuEdit(Command id) { SS.GW.ClearPending(); SS.GW.ClearSelection(); - InvalidateGraphics(); + SS.GW.Invalidate(); break; } @@ -1156,7 +1183,7 @@ void GraphicsWindow::MenuRequest(Command id) { SS.GW.SetWorkplaneFreeIn3d(); SS.GW.EnsureValidActives(); SS.ScheduleShowTW(); - InvalidateGraphics(); + SS.GW.Invalidate(); break; case Command::TANGENT_ARC: @@ -1171,7 +1198,7 @@ void GraphicsWindow::MenuRequest(Command id) { SS.TW.GoToScreen(TextWindow::Screen::TANGENT_ARC); SS.GW.ForceTextWindowShown(); SS.ScheduleShowTW(); - InvalidateGraphics(); // repaint toolbar + SS.GW.Invalidate(); // repaint toolbar } break; @@ -1196,7 +1223,7 @@ c: SS.GW.pending.command = id; SS.GW.pending.description = s; SS.ScheduleShowTW(); - InvalidateGraphics(); // repaint toolbar + SS.GW.Invalidate(); // repaint toolbar break; case Command::CONSTRUCTION: { @@ -1227,7 +1254,7 @@ c: } void GraphicsWindow::ClearSuper() { - HideGraphicsEditControl(); + if(window) window->HideEditor(); ClearPending(); ClearSelection(); hover.Clear(); @@ -1248,8 +1275,7 @@ void GraphicsWindow::ToggleBool(bool *v) { SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE); } - SS.GW.persistentDirty = true; - InvalidateGraphics(); + Invalidate(/*clearPersistent=*/true); SS.ScheduleShowTW(); } diff --git a/src/mouse.cpp b/src/mouse.cpp index 0ffc4ac0..f823be85 100644 --- a/src/mouse.cpp +++ b/src/mouse.cpp @@ -84,7 +84,7 @@ void GraphicsWindow::StartDraggingBySelection() { void GraphicsWindow::MouseMoved(double x, double y, bool leftDown, bool middleDown, bool rightDown, bool shiftDown, bool ctrlDown) { - if(GraphicsEditControlIsVisible()) return; + if(window->IsEditorVisible()) return; if(context.active) return; SS.extraLine.draw = false; @@ -112,7 +112,7 @@ void GraphicsWindow::MouseMoved(double x, double y, bool leftDown, pending.operation == Pending::DRAGGING_MARQUEE)) { ClearPending(); - InvalidateGraphics(); + Invalidate(); } Point2d mp = Point2d::From(x, y); @@ -168,7 +168,7 @@ void GraphicsWindow::MouseMoved(double x, double y, bool leftDown, SS.ScheduleShowTW(); } } - InvalidateGraphics(); + Invalidate(); havePainted = false; return; } @@ -282,7 +282,7 @@ void GraphicsWindow::MouseMoved(double x, double y, bool leftDown, Constraint *c = SK.constraint.FindById(pending.constraint); UpdateDraggedNum(&(c->disp.offset), x, y); orig.mouse = mp; - InvalidateGraphics(); + Invalidate(); return; } @@ -298,7 +298,7 @@ void GraphicsWindow::MouseMoved(double x, double y, bool leftDown, HitTestMakeSelection(mp); SS.MarkGroupDirtyByEntity(pending.point); orig.mouse = mp; - InvalidateGraphics(); + Invalidate(); break; case Pending::DRAGGING_POINTS: @@ -459,7 +459,7 @@ void GraphicsWindow::MouseMoved(double x, double y, bool leftDown, case Pending::DRAGGING_MARQUEE: orig.mouse = mp; - InvalidateGraphics(); + Invalidate(); return; case Pending::NONE: @@ -497,7 +497,7 @@ void GraphicsWindow::ReplacePending(hRequest before, hRequest after) { } void GraphicsWindow::MouseMiddleOrRightDown(double x, double y) { - if(GraphicsEditControlIsVisible()) return; + if(window->IsEditorVisible()) return; orig.offset = offset; orig.projUp = projUp; @@ -509,7 +509,7 @@ void GraphicsWindow::MouseMiddleOrRightDown(double x, double y) { void GraphicsWindow::MouseRightUp(double x, double y) { SS.extraLine.draw = false; - InvalidateGraphics(); + Invalidate(); // Don't show a context menu if the user is right-clicking the toolbar, // or if they are finishing a pan. @@ -866,13 +866,67 @@ bool GraphicsWindow::ConstrainPointByHovered(hEntity pt, const Point2d *projecte return false; } +bool GraphicsWindow::MouseEvent(Platform::MouseEvent event) { + using Platform::MouseEvent; + + double width, height; + window->GetContentSize(&width, &height); + + event.x = event.x - width / 2; + event.y = height / 2 - event.y; + + switch(event.type) { + case MouseEvent::Type::MOTION: + this->MouseMoved(event.x, event.y, + event.button == MouseEvent::Button::LEFT, + event.button == MouseEvent::Button::MIDDLE, + event.button == MouseEvent::Button::RIGHT, + event.shiftDown, + event.controlDown); + break; + + case MouseEvent::Type::PRESS: + if(event.button == MouseEvent::Button::LEFT) { + this->MouseLeftDown(event.x, event.y); + } else if(event.button == MouseEvent::Button::MIDDLE || + event.button == MouseEvent::Button::RIGHT) { + this->MouseMiddleOrRightDown(event.x, event.y); + } + break; + + case MouseEvent::Type::DBL_PRESS: + if(event.button == MouseEvent::Button::LEFT) { + this->MouseLeftDoubleClick(event.x, event.y); + } + break; + + case MouseEvent::Type::RELEASE: + if(event.button == MouseEvent::Button::LEFT) { + this->MouseLeftUp(event.x, event.y); + } else if(event.button == MouseEvent::Button::RIGHT) { + this->MouseRightUp(event.x, event.y); + } + break; + + case MouseEvent::Type::SCROLL_VERT: + this->MouseScroll(event.x, event.y, event.scrollDelta); + break; + + case MouseEvent::Type::LEAVE: + this->MouseLeave(); + break; + } + + return true; +} + void GraphicsWindow::MouseLeftDown(double mx, double my) { orig.mouseDown = true; - if(GraphicsEditControlIsVisible()) { + if(window->IsEditorVisible()) { orig.mouse = Point2d::From(mx, my); orig.mouseOnButtonDown = orig.mouse; - HideGraphicsEditControl(); + window->HideEditor(); return; } SS.TW.HideEditControl(); @@ -1234,7 +1288,7 @@ void GraphicsWindow::MouseLeftDown(double mx, double my) { } SS.ScheduleShowTW(); - InvalidateGraphics(); + Invalidate(); } void GraphicsWindow::MouseLeftUp(double mx, double my) { @@ -1249,13 +1303,13 @@ void GraphicsWindow::MouseLeftUp(double mx, double my) { case Pending::DRAGGING_NORMAL: case Pending::DRAGGING_RADIUS: ClearPending(); - InvalidateGraphics(); + Invalidate(); break; case Pending::DRAGGING_MARQUEE: SelectByMarquee(); ClearPending(); - InvalidateGraphics(); + Invalidate(); break; case Pending::NONE: @@ -1270,7 +1324,7 @@ void GraphicsWindow::MouseLeftUp(double mx, double my) { } void GraphicsWindow::MouseLeftDoubleClick(double mx, double my) { - if(GraphicsEditControlIsVisible()) return; + if(window->IsEditorVisible()) return; SS.TW.HideEditControl(); if(hover.constraint.v) { @@ -1291,17 +1345,17 @@ void GraphicsWindow::MouseLeftDoubleClick(double mx, double my) { Point2d p2 = ProjectPoint(p3); std::string editValue; - int editMinWidthChar; + std::string editPlaceholder; switch(c->type) { case Constraint::Type::COMMENT: editValue = c->comment; - editMinWidthChar = 30; + editPlaceholder = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; break; case Constraint::Type::ANGLE: case Constraint::Type::LENGTH_RATIO: editValue = ssprintf("%.3f", c->valA); - editMinWidthChar = 5; + editPlaceholder = "0.000"; break; default: { @@ -1327,20 +1381,28 @@ void GraphicsWindow::MouseLeftDoubleClick(double mx, double my) { if(fabs(std::stod(editValue) - v) < eps) break; } } - editMinWidthChar = 5; + editPlaceholder = "0.00000"; break; } } + + double width, height; + window->GetContentSize(&width, &height); hStyle hs = c->disp.style; if(hs.v == 0) hs.v = Style::CONSTRAINT; - ShowGraphicsEditControl((int)p2.x, (int)p2.y, - (int)(VectorFont::Builtin()->GetHeight(Style::TextHeight(hs))), - editMinWidthChar, editValue); + double capHeight = Style::TextHeight(hs); + double fontHeight = VectorFont::Builtin()->GetHeight(capHeight); + double editMinWidth = VectorFont::Builtin()->GetWidth(capHeight, editPlaceholder); + window->ShowEditor(p2.x + width / 2, height / 2 - p2.y, + fontHeight, editMinWidth, + /*isMonospace=*/false, editValue); } } -void GraphicsWindow::EditControlDone(const char *s) { - HideGraphicsEditControl(); +void GraphicsWindow::EditControlDone(const std::string &s) { + window->HideEditor(); + window->Invalidate(); + Constraint *c = SK.GetConstraint(constraintBeingEdited); if(c->type == Constraint::Type::COMMENT) { @@ -1349,8 +1411,7 @@ void GraphicsWindow::EditControlDone(const char *s) { return; } - Expr *e = Expr::From(s, true); - if(e) { + if(Expr *e = Expr::From(s, true)) { SS.UndoRemember(); switch(c->type) { @@ -1420,7 +1481,7 @@ void GraphicsWindow::MouseScroll(double x, double y, int delta) { } } havePainted = false; - InvalidateGraphics(); + Invalidate(); } void GraphicsWindow::MouseLeave() { @@ -1429,7 +1490,7 @@ void GraphicsWindow::MouseLeave() { if(!context.active) { hover.Clear(); toolbarHovered = Command::NONE; - PaintGraphics(); + Invalidate(); } SS.extraLine.draw = false; } @@ -1496,11 +1557,11 @@ void GraphicsWindow::SpaceNavigatorMoved(double tx, double ty, double tz, } havePainted = false; - InvalidateGraphics(); + Invalidate(); } void GraphicsWindow::SpaceNavigatorButtonUp() { ZoomToFit(/*includingInvisibles=*/false, /*useSelection=*/true); - InvalidateGraphics(); + Invalidate(); } diff --git a/src/platform/climain.cpp b/src/platform/climain.cpp index 34d3fe36..5074de28 100644 --- a/src/platform/climain.cpp +++ b/src/platform/climain.cpp @@ -191,16 +191,32 @@ static bool RunCommand(const std::vector args) { } runner = [&](const Platform::Path &output) { - SS.GW.width = width; - SS.GW.height = height; - SS.GW.projRight = projRight; - SS.GW.projUp = projUp; - SS.chordTol = chordTol; + Camera camera = {}; + camera.pixelRatio = 1; + camera.gridFit = true; + camera.width = width; + camera.height = height; + camera.projUp = SS.GW.projUp; + camera.projRight = SS.GW.projRight; - SS.GW.ZoomToFit(/*includingInvisibles=*/false); + SS.GW.projUp = projUp; + SS.GW.projRight = projRight; + SS.GW.scale = SS.GW.ZoomToFit(camera); + camera.scale = SS.GW.scale; SS.GenerateAll(); - PaintGraphics(); - framebuffer->WritePng(output, /*flip=*/true); + + CairoPixmapRenderer *pixmapCanvas = (CairoPixmapRenderer *)SS.GW.canvas.get(); + pixmapCanvas->antialias = true; + pixmapCanvas->SetLighting(SS.GW.GetLighting()); + pixmapCanvas->SetCamera(camera); + pixmapCanvas->Init(); + + SS.GW.canvas->NewFrame(); + SS.GW.Draw(SS.GW.canvas.get()); + SS.GW.canvas->FlushFrame(); + SS.GW.canvas->ReadFrame()->WritePng(output, /*flip=*/true); + + pixmapCanvas->Clear(); }; } else if(args[1] == "export-view") { for(size_t argn = 2; argn < args.size(); argn++) { diff --git a/src/platform/cocoamain.mm b/src/platform/cocoamain.mm index 23bd12d8..77ef7da5 100644 --- a/src/platform/cocoamain.mm +++ b/src/platform/cocoamain.mm @@ -62,419 +62,6 @@ std::string CnfThawString(const std::string &val, const std::string &key) { } }; -/* OpenGL view */ - -@interface GLViewWithEditor : NSView -- (void)drawGL; - -@property BOOL wantsBackingStoreScaling; - -@property(readonly, getter=isEditing) BOOL editing; -- (void)startEditing:(NSString*)text at:(NSPoint)origin withHeight:(double)fontHeight - usingMonospace:(BOOL)isMonospace; -- (void)stopEditing; -- (void)didEdit:(NSString*)text; -@end - -@implementation GLViewWithEditor -{ - SolveSpace::GlOffscreen offscreen; - NSOpenGLContext *glContext; -@protected - NSTextField *editor; -} - -- (id)initWithFrame:(NSRect)frameRect { - self = [super initWithFrame:frameRect]; - [self setWantsLayer:YES]; - - NSOpenGLPixelFormatAttribute attrs[] = { - NSOpenGLPFAColorSize, 24, - NSOpenGLPFADepthSize, 24, - 0 - }; - NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; - glContext = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:NULL]; - - editor = [[NSTextField alloc] init]; - [editor setEditable:YES]; - [[editor cell] setWraps:NO]; - [[editor cell] setScrollable:YES]; - [editor setBezeled:NO]; - [editor setTarget:self]; - [editor setAction:@selector(editorAction:)]; - - return self; -} - -- (void)dealloc { - offscreen.Clear(); -} - -#define CONVERT1(name, to_from) \ - - (NS##name)convert##name##to_from##Backing:(NS##name)input { \ - return _wantsBackingStoreScaling ? [super convert##name##to_from##Backing:input] : input; } -#define CONVERT(name) CONVERT1(name, To) CONVERT1(name, From) -CONVERT(Size) -CONVERT(Rect) -#undef CONVERT -#undef CONVERT1 - -- (NSPoint)convertPointToBacking:(NSPoint)input { - if(_wantsBackingStoreScaling) return [super convertPointToBacking:input]; - else { - input.y *= -1; - return input; - } -} - -- (NSPoint)convertPointFromBacking:(NSPoint)input { - if(_wantsBackingStoreScaling) return [super convertPointFromBacking:input]; - else { - input.y *= -1; - return input; - } -} - -- (void)drawRect:(NSRect)aRect { - [glContext makeCurrentContext]; - - NSSize size = [self convertSizeToBacking:[self bounds].size]; - int width = (int)size.width, - height = (int)size.height; - offscreen.Render(width, height, [&] { [self drawGL]; }); - - CGDataProviderRef provider = CGDataProviderCreateWithData( - NULL, &offscreen.data[0], width * height * 4, NULL); - CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); - CGImageRef image = CGImageCreate(width, height, 8, 32, - width * 4, colorspace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst, - provider, NULL, true, kCGRenderingIntentDefault); - - CGContextDrawImage((CGContextRef) [[NSGraphicsContext currentContext] graphicsPort], - [self bounds], image); - - CGImageRelease(image); - CGDataProviderRelease(provider); -} - -- (void)drawGL { -} - -@synthesize editing; - -- (void)startEditing:(NSString*)text at:(NSPoint)origin withHeight:(double)fontHeight - usingMonospace:(BOOL)isMonospace { - if(!self->editing) { - [self addSubview:editor]; - self->editing = YES; - } - - NSFont *font; - if(isMonospace) - font = [NSFont fontWithName:@"Monaco" size:fontHeight]; - else - font = [NSFont controlContentFontOfSize:fontHeight]; - [editor setFont:font]; - - origin.x -= 3; /* left padding; no way to get it from NSTextField */ - origin.y -= [editor intrinsicContentSize].height; - origin.y += [editor baselineOffsetFromBottom]; - - [editor setFrameOrigin:origin]; - [editor setStringValue:text]; - [[self window] makeFirstResponder:editor]; -} - -- (void)stopEditing { - if(self->editing) { - [editor removeFromSuperview]; - self->editing = NO; - } -} - -- (void)editorAction:(id)sender { - [self didEdit:[editor stringValue]]; - [self stopEditing]; -} - -- (void)didEdit:(NSString*)text { -} -@end - -/* Graphics window */ - -@interface GraphicsWindowView : GLViewWithEditor -{ - NSTrackingArea *trackingArea; -} - -@property(readonly) NSEvent *lastContextMenuEvent; -@end - -@implementation GraphicsWindowView -- (BOOL)isFlipped { - return YES; -} - -- (void)drawGL { - SolveSpace::SS.GW.Paint(); -} - -- (BOOL)acceptsFirstResponder { - return YES; -} - -- (void) createTrackingArea { - trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] - options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | - NSTrackingActiveInKeyWindow) - owner:self userInfo:nil]; - [self addTrackingArea:trackingArea]; -} - -- (void) updateTrackingAreas -{ - [self removeTrackingArea:trackingArea]; - [self createTrackingArea]; - [super updateTrackingAreas]; -} - -- (void)mouseMoved:(NSEvent*)event { - NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; - NSUInteger flags = [event modifierFlags]; - NSUInteger buttons = [NSEvent pressedMouseButtons]; - SolveSpace::SS.GW.MouseMoved(point.x, point.y, - buttons & (1 << 0), - buttons & (1 << 2), - buttons & (1 << 1), - flags & NSShiftKeyMask, - flags & NSCommandKeyMask); -} - -- (void)mouseDragged:(NSEvent*)event { - [self mouseMoved:event]; -} - -- (void)rightMouseDragged:(NSEvent*)event { - [self mouseMoved:event]; -} - -- (void)otherMouseDragged:(NSEvent*)event { - [self mouseMoved:event]; -} - -- (void)mouseDown:(NSEvent*)event { - NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; - if([event clickCount] == 1) - SolveSpace::SS.GW.MouseLeftDown(point.x, point.y); - else if([event clickCount] == 2) - SolveSpace::SS.GW.MouseLeftDoubleClick(point.x, point.y); -} - -- (void)rightMouseDown:(NSEvent*)event { - NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; - SolveSpace::SS.GW.MouseMiddleOrRightDown(point.x, point.y); -} - -- (void)otherMouseDown:(NSEvent*)event { - [self rightMouseDown:event]; -} - -- (void)mouseUp:(NSEvent*)event { - NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; - SolveSpace::SS.GW.MouseLeftUp(point.x, point.y); -} - -- (void)rightMouseUp:(NSEvent*)event { - NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; - self->_lastContextMenuEvent = event; - SolveSpace::SS.GW.MouseRightUp(point.x, point.y); -} - -- (void)scrollWheel:(NSEvent*)event { - NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; - SolveSpace::SS.GW.MouseScroll(point.x, point.y, (int)-[event deltaY]); -} - -- (void)mouseExited:(NSEvent*)event { - SolveSpace::SS.GW.MouseLeave(); -} - -- (void)keyDown:(NSEvent*)nsEvent { - using SolveSpace::Platform::KeyboardEvent; - - KeyboardEvent event = {}; - event.type = KeyboardEvent::Type::PRESS; - - NSUInteger flags = [nsEvent modifierFlags]; - if(flags & NSShiftKeyMask) - event.shiftDown = true; - if(flags & NSCommandKeyMask) - event.controlDown = true; - if(flags & ~(NSShiftKeyMask|NSCommandKeyMask)) { - [super keyDown:nsEvent]; - return; - } - - unichar chr = 0; - if(NSString *nsChr = [nsEvent charactersIgnoringModifiers]) - chr = [nsChr characterAtIndex:0]; - - if(chr >= NSF1FunctionKey && chr <= NSF12FunctionKey) { - event.key = KeyboardEvent::Key::FUNCTION; - event.num = chr - NSF1FunctionKey + 1; - } else { - event.key = KeyboardEvent::Key::CHARACTER; - event.chr = chr; - } - - if(SolveSpace::SS.GW.KeyboardEvent(event)) - return; - - [super keyDown:nsEvent]; -} - -- (void)startEditing:(NSString*)text at:(NSPoint)xy withHeight:(double)fontHeight - withMinWidthInChars:(int)minWidthChars { - // Convert to ij (vs. xy) style coordinates - NSSize size = [self convertSizeToBacking:[self bounds].size]; - NSPoint point = { - .x = xy.x + size.width / 2, - .y = xy.y - size.height / 2 - }; - [[self window] makeKeyWindow]; - [super startEditing:text at:[self convertPointFromBacking:point] - withHeight:fontHeight usingMonospace:FALSE]; - [self prepareEditorWithMinWidthInChars:minWidthChars]; -} - -- (void)prepareEditorWithMinWidthInChars:(int)minWidthChars { - NSFont *font = [editor font]; - NSGlyph glyphA = [font glyphWithName:@"a"]; - ssassert(glyphA != (NSGlyph)-1, "Expected font to have U+0061"); - CGFloat glyphAWidth = [font advancementForGlyph:glyphA].width; - - [editor sizeToFit]; - - NSSize frameSize = [editor frame].size; - frameSize.width = std::max(frameSize.width, glyphAWidth * minWidthChars); - [editor setFrameSize:frameSize]; -} - -- (void)didEdit:(NSString*)text { - SolveSpace::SS.GW.EditControlDone([text UTF8String]); - [self setNeedsDisplay:YES]; -} - -- (void)cancelOperation:(id)sender { - [self stopEditing]; -} - -- (NSPoint)ij_to_xy:(NSPoint)ij { - // Convert to xy (vs. ij) style coordinates, - // with (0, 0) at center - NSSize size = [self bounds].size; - return [self convertPointToBacking:(NSPoint){ - .x = ij.x - size.width / 2, .y = ij.y - size.height / 2 }]; -} -@end - -@interface GraphicsWindowDelegate : NSObject -- (BOOL)windowShouldClose:(id)sender; - -@property(readonly, getter=isFullscreen) BOOL fullscreen; -- (void)windowDidEnterFullScreen:(NSNotification *)notification; -- (void)windowDidExitFullScreen:(NSNotification *)notification; -@end - -@implementation GraphicsWindowDelegate -- (BOOL)windowShouldClose:(id)sender { - [NSApp terminate:sender]; - return FALSE; /* in case NSApp changes its mind */ -} - -@synthesize fullscreen; -- (void)windowDidEnterFullScreen:(NSNotification *)notification { - fullscreen = true; - /* Update the menus */ - SolveSpace::SS.GW.EnsureValidActives(); -} -- (void)windowDidExitFullScreen:(NSNotification *)notification { - fullscreen = false; - /* Update the menus */ - SolveSpace::SS.GW.EnsureValidActives(); -} -@end - -static NSWindow *GW; -static GraphicsWindowView *GWView; -static GraphicsWindowDelegate *GWDelegate; - -namespace SolveSpace { -void InitGraphicsWindow() { - GW = [[NSWindow alloc] init]; - GWDelegate = [[GraphicsWindowDelegate alloc] init]; - [GW setDelegate:GWDelegate]; - [GW setStyleMask:(NSTitledWindowMask | NSClosableWindowMask | - NSMiniaturizableWindowMask | NSResizableWindowMask)]; - [GW setFrameAutosaveName:@"GraphicsWindow"]; - [GW setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; - if(![GW setFrameUsingName:[GW frameAutosaveName]]) - [GW setContentSize:(NSSize){ .width = 600, .height = 600 }]; - GWView = [[GraphicsWindowView alloc] init]; - [GW setContentView:GWView]; -} - -void GetGraphicsWindowSize(int *w, int *h) { - NSSize size = [GWView convertSizeToBacking:[GWView frame].size]; - *w = (int)size.width; - *h = (int)size.height; -} - -void InvalidateGraphics() { - [GWView setNeedsDisplay:YES]; -} - -void PaintGraphics() { - [GWView setNeedsDisplay:YES]; - CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, YES); -} - -void SetCurrentFilename(const Platform::Path &filename) { - if(!filename.IsEmpty()) { - [GW setTitleWithRepresentedFilename:Wrap(filename.raw)]; - } else { - [GW setTitle:Wrap(C_("title", "(new sketch)"))]; - [GW setRepresentedFilename:@""]; - } -} - -void ToggleFullScreen() { - [GW toggleFullScreen:nil]; -} - -bool FullScreenIsActive() { - return [GWDelegate isFullscreen]; -} - -void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, - const std::string &str) { - [GWView startEditing:Wrap(str) - at:(NSPoint){(CGFloat)x, (CGFloat)y} - withHeight:fontHeight - withMinWidthInChars:minWidthChars]; -} - -void HideGraphicsEditControl() { - [GWView stopEditing]; -} - -bool GraphicsEditControlIsVisible() { - return [GWView isEditing]; -} -} - /* Save/load */ bool SolveSpace::GetOpenFile(Platform::Path *filename, const std::string &defExtension, @@ -652,207 +239,6 @@ SolveSpace::DialogChoice SolveSpace::LocateImportedFileYesNoCancel( } } -/* Text window */ - -@interface TextWindowView : GLViewWithEditor -{ - NSTrackingArea *trackingArea; -} - -@property (nonatomic, getter=isCursorHand) BOOL cursorHand; -@end - -@implementation TextWindowView -- (BOOL)isFlipped { - return YES; -} - -- (void)drawGL { - SolveSpace::SS.TW.Paint(); -} - -- (BOOL)acceptsFirstMouse:(NSEvent*)event { - return YES; -} - -- (void) createTrackingArea { - trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] - options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | - NSTrackingActiveAlways) - owner:self userInfo:nil]; - [self addTrackingArea:trackingArea]; -} - -- (void) updateTrackingAreas -{ - [self removeTrackingArea:trackingArea]; - [self createTrackingArea]; - [super updateTrackingAreas]; -} - -- (void)mouseMoved:(NSEvent*)event { - NSPoint point = [self convertPointToBacking: - [self convertPoint:[event locationInWindow] fromView:nil]]; - SolveSpace::SS.TW.MouseEvent(/*leftClick*/ false, /*leftDown*/ false, - point.x, -point.y); -} - -- (void)mouseDown:(NSEvent*)event { - NSPoint point = [self convertPointToBacking: - [self convertPoint:[event locationInWindow] fromView:nil]]; - SolveSpace::SS.TW.MouseEvent(/*leftClick*/ true, /*leftDown*/ true, - point.x, -point.y); -} - -- (void)mouseDragged:(NSEvent*)event { - NSPoint point = [self convertPointToBacking: - [self convertPoint:[event locationInWindow] fromView:nil]]; - SolveSpace::SS.TW.MouseEvent(/*leftClick*/ false, /*leftDown*/ true, - point.x, -point.y); -} - -- (void)setCursorHand:(BOOL)cursorHand { - if(_cursorHand != cursorHand) { - if(cursorHand) - [[NSCursor pointingHandCursor] push]; - else - [NSCursor pop]; - } - _cursorHand = cursorHand; -} - -- (void)mouseExited:(NSEvent*)event { - [self setCursorHand:FALSE]; - SolveSpace::SS.TW.MouseLeave(); -} - -- (void)startEditing:(NSString*)text at:(NSPoint)point { - point = [self convertPointFromBacking:point]; - point.y = -point.y + 2; - [[self window] makeKeyWindow]; - [super startEditing:text at:point withHeight:15.0 usingMonospace:TRUE]; - [editor setFrameSize:(NSSize){ - .width = [self bounds].size.width - [editor frame].origin.x, - .height = [editor intrinsicContentSize].height }]; -} - -- (void)stopEditing { - [super stopEditing]; - [GW makeKeyWindow]; -} - -- (void)didEdit:(NSString*)text { - SolveSpace::SS.TW.EditControlDone([text UTF8String]); -} - -- (void)cancelOperation:(id)sender { - [self stopEditing]; -} -@end - -@interface TextWindowDelegate : NSObject -- (BOOL)windowShouldClose:(id)sender; -- (void)windowDidResize:(NSNotification *)notification; -@end - -@implementation TextWindowDelegate -- (BOOL)windowShouldClose:(id)sender { - SolveSpace::GraphicsWindow::MenuView(SolveSpace::Command::SHOW_TEXT_WND); - return NO; -} - -- (void)windowDidResize:(NSNotification *)notification { - NSClipView *view = [[[notification object] contentView] contentView]; - NSView *document = [view documentView]; - NSSize size = [document frame].size; - size.width = [view frame].size.width; - [document setFrameSize:size]; -} -@end - -static NSPanel *TW; -static TextWindowView *TWView; -static TextWindowDelegate *TWDelegate; - -namespace SolveSpace { -void InitTextWindow() { - TW = [[NSPanel alloc] init]; - TWDelegate = [[TextWindowDelegate alloc] init]; - [TW setStyleMask:(NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask | - NSUtilityWindowMask)]; - [[TW standardWindowButton:NSWindowMiniaturizeButton] setHidden:YES]; - [[TW standardWindowButton:NSWindowZoomButton] setHidden:YES]; - [TW setFrameAutosaveName:@"TextWindow"]; - [TW setFloatingPanel:YES]; - [TW setBecomesKeyOnlyIfNeeded:YES]; - - NSScrollView *scrollView = [[NSScrollView alloc] init]; - [TW setContentView:scrollView]; - [scrollView setBackgroundColor:[NSColor blackColor]]; - [scrollView setHasVerticalScroller:YES]; - [scrollView setScrollerKnobStyle:NSScrollerKnobStyleLight]; - [[scrollView contentView] setCopiesOnScroll:YES]; - - TWView = [[TextWindowView alloc] init]; - [scrollView setDocumentView:TWView]; - - [TW setDelegate:TWDelegate]; - if(![TW setFrameUsingName:[TW frameAutosaveName]]) - [TW setContentSize:(NSSize){ .width = 420, .height = 300 }]; - [TWView setFrame:[[scrollView contentView] frame]]; -} - -void ShowTextWindow(bool visible) { - if(visible) - [TW orderFront:nil]; - else - [TW close]; -} - -void GetTextWindowSize(int *w, int *h) { - NSSize size = [TWView convertSizeToBacking:[TWView frame].size]; - *w = (int)size.width; - *h = (int)size.height; -} - -double GetScreenDpi() { - NSScreen *screen = [NSScreen mainScreen]; - NSDictionary *description = [screen deviceDescription]; - NSSize displayPixelSize = [[description objectForKey:NSDeviceSize] sizeValue]; - CGSize displayPhysicalSize = CGDisplayScreenSize( - [[description objectForKey:@"NSScreenNumber"] unsignedIntValue]); - return (displayPixelSize.width / displayPhysicalSize.width) * 25.4f; -} - -void InvalidateText() { - NSSize size = [TWView convertSizeToBacking:[TWView frame].size]; - size.height = (SS.TW.top[SS.TW.rows - 1] + 1) * TextWindow::LINE_HEIGHT / 2; - [TWView setFrameSize:[TWView convertSizeFromBacking:size]]; - [TWView setNeedsDisplay:YES]; -} - -void MoveTextScrollbarTo(int pos, int maxPos, int page) { - /* unused; we draw the entire text window and scroll in Cocoa */ -} - -void SetMousePointerToHand(bool is_hand) { - [TWView setCursorHand:is_hand]; -} - -void ShowTextEditControl(int x, int y, const std::string &str) { - return [TWView startEditing:Wrap(str) - at:(NSPoint){(CGFloat)x, (CGFloat)y}]; -} - -void HideTextEditControl() { - return [TWView stopEditing]; -} - -bool TextEditControlIsVisible() { - return [TWView isEditing]; -} -}; - /* Miscellanea */ void SolveSpace::DoMessageBox(const char *str, int rows, int cols, bool error) { @@ -904,27 +290,15 @@ std::vector SolveSpace::GetFontFiles() { /* Application lifecycle */ @interface ApplicationDelegate : NSObject -- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication; -- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender; -- (void)applicationWillTerminate:(NSNotification *)aNotification; -- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename; - (IBAction)preferences:(id)sender; +- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename; +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender; @end @implementation ApplicationDelegate -- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication { - return YES; -} - -- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { - if(SolveSpace::SS.OkayToStartNewFile()) - return NSTerminateNow; - else - return NSTerminateCancel; -} - -- (void)applicationWillTerminate:(NSNotification *)aNotification { - SolveSpace::SS.Exit(); +- (IBAction)preferences:(id)sender { + SolveSpace::SS.TW.GoToScreen(SolveSpace::TextWindow::Screen::CONFIGURATION); + SolveSpace::SS.ScheduleShowTW(); } - (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename { @@ -932,21 +306,16 @@ std::vector SolveSpace::GetFontFiles() { return SolveSpace::SS.Load(path.Expand(/*fromCurrentDirectory=*/true)); } -- (IBAction)preferences:(id)sender { - SolveSpace::SS.TW.GoToScreen(SolveSpace::TextWindow::Screen::CONFIGURATION); - SolveSpace::SS.ScheduleShowTW(); +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { + [[[NSApp mainWindow] delegate] windowShouldClose:nil]; + return NSTerminateCancel; +} + +- (void)applicationTerminatePrompt { + SolveSpace::SS.MenuFile(SolveSpace::Command::EXIT); } @end -void SolveSpace::SetMainMenu(Platform::MenuBarRef menuBar) { - SS.UpdateWindowTitle(); - [TW setTitle:Wrap(C_("title", "Property Browser"))]; -} - -void SolveSpace::ExitNow() { - [NSApp stop:nil]; -} - /* * Normally we would just link to the 3DconnexionClient framework. * We don't want to (are not allowed to) distribute the official @@ -1097,17 +466,13 @@ static void connexionClose() { } int main(int argc, const char *argv[]) { - [NSApplication sharedApplication]; ApplicationDelegate *delegate = [[ApplicationDelegate alloc] init]; - [NSApp setDelegate:delegate]; + [[NSApplication sharedApplication] setDelegate:delegate]; - SolveSpace::InitGraphicsWindow(); - SolveSpace::InitTextWindow(); [[NSBundle mainBundle] loadNibNamed:@"MainMenu" owner:nil topLevelObjects:nil]; NSArray *languages = [NSLocale preferredLanguages]; for(NSString *language in languages) { - dbp("%s", ([language UTF8String])); if(SolveSpace::SetLocale([language UTF8String])) break; } if([languages count] == 0) { @@ -1117,7 +482,6 @@ int main(int argc, const char *argv[]) { connexionInit(); SolveSpace::SS.Init(); - [GW makeKeyAndOrderFront:nil]; [NSApp run]; connexionClose(); diff --git a/src/platform/gtkmain.cpp b/src/platform/gtkmain.cpp index 62d821b3..2ab8e93d 100644 --- a/src/platform/gtkmain.cpp +++ b/src/platform/gtkmain.cpp @@ -162,439 +162,6 @@ std::string CnfThawString(const std::string &val, const std::string &key) { return val; } -static void CnfFreezeWindowPos(Gtk::Window *win, const std::string &key) { - int x, y, w, h; - win->get_position(x, y); - win->get_size(w, h); - - CnfFreezeInt(x, key + "_left"); - CnfFreezeInt(y, key + "_top"); - CnfFreezeInt(w, key + "_width"); - CnfFreezeInt(h, key + "_height"); -} - -static void CnfThawWindowPos(Gtk::Window *win, const std::string &key) { - int x, y, w, h; - win->get_position(x, y); - win->get_size(w, h); - - x = CnfThawInt(x, key + "_left"); - y = CnfThawInt(y, key + "_top"); - w = CnfThawInt(w, key + "_width"); - h = CnfThawInt(h, key + "_height"); - - win->move(x, y); - win->resize(w, h); -} - -/* Editor overlay */ - -class EditorOverlay : public Gtk::Fixed { -public: - EditorOverlay(Gtk::Widget &underlay) : _underlay(underlay) { - set_size_request(0, 0); - - add(_underlay); - - _entry.set_no_show_all(true); - _entry.set_has_frame(false); - add(_entry); - - _entry.signal_activate(). - connect(sigc::mem_fun(this, &EditorOverlay::on_activate)); - } - - void start_editing(int x, int y, int font_height, bool is_monospace, int minWidthChars, - const std::string &val) { - x /= get_scale_factor(); - y /= get_scale_factor(); - font_height /= get_scale_factor(); - - Pango::FontDescription font_desc; - font_desc.set_family(is_monospace ? "monospace" : "normal"); - font_desc.set_absolute_size(font_height * Pango::SCALE); - _entry.override_font(font_desc); - - /* y coordinate denotes baseline */ - Pango::FontMetrics font_metrics = get_pango_context()->get_metrics(font_desc); - y -= font_metrics.get_ascent() / Pango::SCALE; - - Glib::RefPtr layout = Pango::Layout::create(get_pango_context()); - layout->set_font_description(font_desc); - layout->set_text(val + " "); /* avoid scrolling */ - int width = layout->get_logical_extents().get_width(); - - Gtk::Border margin = _entry.get_style_context()->get_margin(); - Gtk::Border border = _entry.get_style_context()->get_border(); - Gtk::Border padding = _entry.get_style_context()->get_padding(); - move(_entry, - x - margin.get_left() - border.get_left() - padding.get_left(), - y - margin.get_top() - border.get_top() - padding.get_top()); - _entry.set_width_chars(minWidthChars); - _entry.set_size_request( - width / Pango::SCALE + padding.get_left() + padding.get_right(), - -1); - - _entry.set_text(val); - if(!_entry.is_visible()) { - _entry.show(); - _entry.grab_focus(); - add_modal_grab(); - } - } - - void stop_editing() { - if(_entry.is_visible()) { - remove_modal_grab(); - } - _entry.hide(); - } - - bool is_editing() const { - return _entry.is_visible(); - } - - sigc::signal signal_editing_done() { - return _signal_editing_done; - } - - Gtk::Entry &get_entry() { - return _entry; - } - -protected: - bool on_key_press_event(GdkEventKey *event) override { - if(is_editing()) { - if(event->keyval == GDK_KEY_Escape) { - stop_editing(); - } else { - _entry.event((GdkEvent *)event); - } - return true; - } else { - return Gtk::Fixed::on_key_press_event(event); - } - } - - bool on_key_release_event(GdkEventKey *event) override { - if(is_editing()) { - _entry.event((GdkEvent *)event); - return true; - } else { - return Gtk::Fixed::on_key_release_event(event); - } - } - - void on_size_allocate(Gtk::Allocation& allocation) override { - Gtk::Fixed::on_size_allocate(allocation); - - _underlay.size_allocate(allocation); - } - - void on_activate() { - _signal_editing_done(_entry.get_text()); - } - -private: - Gtk::Widget &_underlay; - Gtk::Entry _entry; - sigc::signal _signal_editing_done; -}; - -/* Graphics window */ - -double DeltaYOfScrollEvent(GdkEventScroll *event) { - double delta_y = event->delta_y; - if(delta_y == 0) { - switch(event->direction) { - case GDK_SCROLL_UP: - delta_y = -1; - break; - - case GDK_SCROLL_DOWN: - delta_y = 1; - break; - - default: - /* do nothing */ - return false; - } - } - - return delta_y; -} - -class GraphicsWidget : public Gtk::GLArea { -public: - GraphicsWidget() { - set_events(Gdk::POINTER_MOTION_MASK | - Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::BUTTON_MOTION_MASK | - Gdk::SCROLL_MASK | - Gdk::LEAVE_NOTIFY_MASK); - set_has_depth_buffer(true); - } - -protected: - // Work around a bug fixed in GTKMM 3.22: - // https://mail.gnome.org/archives/gtkmm-list/2016-April/msg00020.html - Glib::RefPtr on_create_context() override { - return get_window()->create_gl_context(); - } - - void on_resize(int width, int height) override { - _w = width; - _h = height; - } - - bool on_render(const Glib::RefPtr &context) override { - SS.GW.Paint(); - return true; - } - - bool on_motion_notify_event(GdkEventMotion *event) override { - int x, y; - ij_to_xy(event->x, event->y, x, y); - - SS.GW.MouseMoved(x, y, - event->state & GDK_BUTTON1_MASK, - event->state & GDK_BUTTON2_MASK, - event->state & GDK_BUTTON3_MASK, - event->state & GDK_SHIFT_MASK, - event->state & GDK_CONTROL_MASK); - - return true; - } - - bool on_button_press_event(GdkEventButton *event) override { - int x, y; - ij_to_xy(event->x, event->y, x, y); - - switch(event->button) { - case 1: - if(event->type == GDK_BUTTON_PRESS) - SS.GW.MouseLeftDown(x, y); - else if(event->type == GDK_2BUTTON_PRESS) - SS.GW.MouseLeftDoubleClick(x, y); - break; - - case 2: - case 3: - SS.GW.MouseMiddleOrRightDown(x, y); - break; - } - - return true; - } - - bool on_button_release_event(GdkEventButton *event) override { - int x, y; - ij_to_xy(event->x, event->y, x, y); - - switch(event->button) { - case 1: - SS.GW.MouseLeftUp(x, y); - break; - - case 3: - SS.GW.MouseRightUp(x, y); - break; - } - - return true; - } - - bool on_scroll_event(GdkEventScroll *event) override { - int x, y; - ij_to_xy(event->x, event->y, x, y); - - SS.GW.MouseScroll(x, y, (int)-DeltaYOfScrollEvent(event)); - - return true; - } - - bool on_leave_notify_event (GdkEventCrossing *) override { - SS.GW.MouseLeave(); - - return true; - } - -private: - int _w, _h; - void ij_to_xy(double i, double j, int &x, int &y) { - // Convert to xy (vs. ij) style coordinates, - // with (0, 0) at center - x = (int)(i * get_scale_factor()) - _w / 2; - y = _h / 2 - (int)(j * get_scale_factor()); - } -}; - -class GraphicsWindowGtk : public Gtk::Window { -public: - GraphicsWindowGtk() : _overlay(_widget), _is_fullscreen(false) { - set_default_size(900, 600); - - _box.pack_end(_overlay, true, true); - - add(_box); - - _overlay.signal_editing_done(). - connect(sigc::mem_fun(this, &GraphicsWindowGtk::on_editing_done)); - } - - GraphicsWidget &get_widget() { - return _widget; - } - - EditorOverlay &get_overlay() { - return _overlay; - } - - void set_menubar(Gtk::MenuBar *menubar) { - if(_menubar) - _box.remove(*_menubar); - _menubar = menubar; - if(_menubar) { - _menubar->show_all(); - _box.pack_start(*_menubar, false, false); - } - } - - Gtk::MenuBar *get_menubar() { - return _menubar; - } - - bool is_fullscreen() const { - return _is_fullscreen; - } - -protected: - void on_show() override { - Gtk::Window::on_show(); - - CnfThawWindowPos(this, "GraphicsWindow"); - } - - void on_hide() override { - CnfFreezeWindowPos(this, "GraphicsWindow"); - - Gtk::Window::on_hide(); - } - - bool on_delete_event(GdkEventAny *) override { - if(!SS.OkayToStartNewFile()) return true; - SS.Exit(); - - return true; - } - - bool on_window_state_event(GdkEventWindowState *event) override { - _is_fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN; - - /* The event arrives too late for the caller of ToggleFullScreen - to notice state change; and it's possible that the WM will - refuse our request, so we can't just toggle the saved state */ - SS.GW.EnsureValidActives(); - - return Gtk::Window::on_window_state_event(event); - } - - bool on_key_press_event(GdkEventKey *gdk_event) override { - Platform::KeyboardEvent event = {}; - event.type = Platform::KeyboardEvent::Type::PRESS; - - if(gdk_event->state & ~(GDK_SHIFT_MASK|GDK_CONTROL_MASK)) { - return Gtk::Window::on_key_press_event(gdk_event); - } - - event.shiftDown = (gdk_event->state & GDK_SHIFT_MASK) != 0; - event.controlDown = (gdk_event->state & GDK_CONTROL_MASK) != 0; - - char32_t chr = gdk_keyval_to_unicode(gdk_keyval_to_lower(gdk_event->keyval)); - if(chr != 0) { - event.key = Platform::KeyboardEvent::Key::CHARACTER; - event.chr = chr; - } else if(gdk_event->keyval >= GDK_KEY_F1 && - gdk_event->keyval <= GDK_KEY_F12) { - event.key = Platform::KeyboardEvent::Key::FUNCTION; - event.num = gdk_event->keyval - GDK_KEY_F1 + 1; - } else { - return Gtk::Window::on_key_press_event(gdk_event); - } - - if(SS.GW.KeyboardEvent(event)) { - return true; - } - - return Gtk::Window::on_key_press_event(gdk_event); - } - - void on_editing_done(Glib::ustring value) { - SS.GW.EditControlDone(value.c_str()); - } - -private: - GraphicsWidget _widget; - EditorOverlay _overlay; - Gtk::MenuBar *_menubar; - Gtk::VBox _box; - - bool _is_fullscreen; -}; - -std::unique_ptr GW; - -void GetGraphicsWindowSize(int *w, int *h) { - Gdk::Rectangle allocation = GW->get_widget().get_allocation(); - *w = allocation.get_width() * GW->get_scale_factor(); - *h = allocation.get_height() * GW->get_scale_factor(); -} - -void InvalidateGraphics(void) { - GW->get_widget().queue_draw(); -} - -void PaintGraphics(void) { - GW->get_widget().queue_draw(); - /* Process animation */ - Glib::MainContext::get_default()->iteration(false); -} - -void SetCurrentFilename(const Platform::Path &filename) { - GW->set_title(Title(filename.IsEmpty() ? C_("title", "(new sketch)") : filename.raw.c_str())); -} - -void ToggleFullScreen(void) { - if(GW->is_fullscreen()) - GW->unfullscreen(); - else - GW->fullscreen(); -} - -bool FullScreenIsActive(void) { - return GW->is_fullscreen(); -} - -void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, - const std::string &val) { - Gdk::Rectangle rect = GW->get_widget().get_allocation(); - - // Convert to ij (vs. xy) style coordinates, - // and compensate for the input widget height due to inverse coord - int i, j; - i = x + rect.get_width() / 2 * GW->get_widget().get_scale_factor(); - j = -y + rect.get_height() / 2 * GW->get_widget().get_scale_factor(); - - GW->get_overlay().start_editing(i, j, fontHeight, /*is_monospace=*/false, minWidthChars, val); -} - -void HideGraphicsEditControl(void) { - GW->get_overlay().stop_editing(); -} - -bool GraphicsEditControlIsVisible(void) { - return GW->get_overlay().is_editing(); -} - /* Save/load */ static std::string ConvertFilters(std::string active, const FileFilter ssFilters[], @@ -630,7 +197,8 @@ static std::string ConvertFilters(std::string active, const FileFilter ssFilters bool GetOpenFile(Platform::Path *filename, const std::string &activeOrEmpty, const FileFilter filters[]) { - Gtk::FileChooserDialog chooser(*GW, Title(C_("title", "Open File"))); + Gtk::FileChooserDialog chooser(*(Gtk::Window *)SS.GW.window->NativePtr(), + Title(C_("title", "Open File"))); chooser.set_filename(filename->raw); chooser.add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); chooser.add_button(_("_Open"), Gtk::RESPONSE_OK); @@ -672,7 +240,8 @@ static void ChooserFilterChanged(Gtk::FileChooserDialog *chooser) bool GetSaveFile(Platform::Path *filename, const std::string &defExtension, const FileFilter filters[]) { - Gtk::FileChooserDialog chooser(*GW, Title(C_("title", "Save File")), + Gtk::FileChooserDialog chooser(*(Gtk::Window *)SS.GW.window->NativePtr(), + Title(C_("title", "Save File")), Gtk::FILE_CHOOSER_ACTION_SAVE); chooser.set_do_overwrite_confirmation(true); chooser.add_button(C_("button", "_Cancel"), Gtk::RESPONSE_CANCEL); @@ -706,7 +275,8 @@ DialogChoice SaveFileYesNoCancel(void) { Glib::ustring message = _("The file has changed since it was last saved.\n\n" "Do you want to save the changes?"); - Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, + Gtk::MessageDialog dialog(*(Gtk::Window *)SS.GW.window->NativePtr(), + message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_NONE, /*is_modal*/ true); dialog.set_title(Title(C_("title", "Modified File"))); dialog.add_button(C_("button", "_Save"), Gtk::RESPONSE_YES); @@ -730,7 +300,8 @@ DialogChoice LoadAutosaveYesNo(void) { Glib::ustring message = _("An autosave file is available for this project.\n\n" "Do you want to load the autosave file instead?"); - Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, + Gtk::MessageDialog dialog(*(Gtk::Window *)SS.GW.window->NativePtr(), + message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_NONE, /*is_modal*/ true); dialog.set_title(Title(C_("title", "Autosave Available"))); dialog.add_button(C_("button", "_Load autosave"), Gtk::RESPONSE_YES); @@ -753,7 +324,8 @@ DialogChoice LocateImportedFileYesNoCancel(const Platform::Path &filename, "Do you want to locate it manually?\n\n" "If you select \"No\", any geometry that depends on " "the missing file will be removed."; - Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, + Gtk::MessageDialog dialog(*(Gtk::Window *)SS.GW.window->NativePtr(), + message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_NONE, /*is_modal*/ true); dialog.set_title(Title(C_("title", "Missing File"))); dialog.add_button(C_("button", "_Yes"), Gtk::RESPONSE_YES); @@ -774,197 +346,11 @@ DialogChoice LocateImportedFileYesNoCancel(const Platform::Path &filename, } } -/* Text window */ - -class TextWidget : public Gtk::GLArea { -public: - TextWidget(Glib::RefPtr adjustment) : _adjustment(adjustment) { - set_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK | - Gdk::LEAVE_NOTIFY_MASK); - set_has_depth_buffer(true); - } - - void set_cursor_hand(bool is_hand) { - Glib::RefPtr gdkwin = get_window(); - if(gdkwin) { // returns NULL if not realized - Gdk::CursorType type = is_hand ? Gdk::HAND1 : Gdk::ARROW; - gdkwin->set_cursor(Gdk::Cursor::create(type)); - } - } - -protected: - // See GraphicsWidget::on_create_context. - Glib::RefPtr on_create_context() override { - return get_window()->create_gl_context(); - } - - bool on_render(const Glib::RefPtr &context) override { - SS.TW.Paint(); - return true; - } - - bool on_motion_notify_event(GdkEventMotion *event) override { - SS.TW.MouseEvent(/*leftClick*/ false, - /*leftDown*/ event->state & GDK_BUTTON1_MASK, - event->x * get_scale_factor(), - event->y * get_scale_factor()); - - return true; - } - - bool on_button_press_event(GdkEventButton *event) override { - SS.TW.MouseEvent(/*leftClick*/ event->type == GDK_BUTTON_PRESS, - /*leftDown*/ event->state & GDK_BUTTON1_MASK, - event->x * get_scale_factor(), - event->y * get_scale_factor()); - - return true; - } - - bool on_scroll_event(GdkEventScroll *event) override { - _adjustment->set_value(_adjustment->get_value() + - DeltaYOfScrollEvent(event) * _adjustment->get_page_increment()); - - return true; - } - - bool on_leave_notify_event (GdkEventCrossing *) override { - SS.TW.MouseLeave(); - - return true; - } - -private: - Glib::RefPtr _adjustment; -}; - -class TextWindowGtk : public Gtk::Window { -public: - TextWindowGtk() : _scrollbar(), _widget(_scrollbar.get_adjustment()), - _overlay(_widget), _box() { - set_type_hint(Gdk::WINDOW_TYPE_HINT_UTILITY); - set_skip_taskbar_hint(true); - set_skip_pager_hint(true); - set_default_size(420, 300); - - _box.pack_start(_overlay, true, true); - _box.pack_start(_scrollbar, false, true); - add(_box); - - _scrollbar.get_adjustment()->signal_value_changed(). - connect(sigc::mem_fun(this, &TextWindowGtk::on_scrollbar_value_changed)); - - _overlay.signal_editing_done(). - connect(sigc::mem_fun(this, &TextWindowGtk::on_editing_done)); - - _overlay.get_entry().signal_motion_notify_event(). - connect(sigc::mem_fun(this, &TextWindowGtk::on_editor_motion_notify_event)); - _overlay.get_entry().signal_button_press_event(). - connect(sigc::mem_fun(this, &TextWindowGtk::on_editor_button_press_event)); - } - - Gtk::VScrollbar &get_scrollbar() { - return _scrollbar; - } - - TextWidget &get_widget() { - return _widget; - } - - EditorOverlay &get_overlay() { - return _overlay; - } - -protected: - void on_show() override { - Gtk::Window::on_show(); - - CnfThawWindowPos(this, "TextWindow"); - } - - void on_hide() override { - CnfFreezeWindowPos(this, "TextWindow"); - - Gtk::Window::on_hide(); - } - - bool on_delete_event(GdkEventAny *) override { - /* trigger the action and ignore the request */ - GraphicsWindow::MenuView(Command::SHOW_TEXT_WND); - - return false; - } - - void on_scrollbar_value_changed() { - SS.TW.ScrollbarEvent((int)_scrollbar.get_adjustment()->get_value()); - } - - void on_editing_done(Glib::ustring value) { - SS.TW.EditControlDone(value.c_str()); - } - - bool on_editor_motion_notify_event(GdkEventMotion *event) { - return _widget.event((GdkEvent*) event); - } - - bool on_editor_button_press_event(GdkEventButton *event) { - return _widget.event((GdkEvent*) event); - } - -private: - Gtk::VScrollbar _scrollbar; - TextWidget _widget; - EditorOverlay _overlay; - Gtk::HBox _box; -}; - -std::unique_ptr TW; - -void ShowTextWindow(bool visible) { - if(visible) - TW->show(); - else - TW->hide(); -} - -void GetTextWindowSize(int *w, int *h) { - Gdk::Rectangle allocation = TW->get_widget().get_allocation(); - *w = allocation.get_width() * TW->get_scale_factor(); - *h = allocation.get_height() * TW->get_scale_factor(); -} - -double GetScreenDpi() { - return Gdk::Screen::get_default()->get_resolution(); -} - -void InvalidateText(void) { - TW->get_widget().queue_draw(); -} - -void MoveTextScrollbarTo(int pos, int maxPos, int page) { - TW->get_scrollbar().get_adjustment()->configure(pos, 0, maxPos, 1, 10, page); -} - -void SetMousePointerToHand(bool is_hand) { - TW->get_widget().set_cursor_hand(is_hand); -} - -void ShowTextEditControl(int x, int y, const std::string &val) { - TW->get_overlay().start_editing(x, y, TextWindow::CHAR_HEIGHT, /*is_monospace=*/true, 30, val); -} - -void HideTextEditControl(void) { - TW->get_overlay().stop_editing(); -} - -bool TextEditControlIsVisible(void) { - return TW->get_overlay().is_editing(); -} - /* Miscellanea */ void DoMessageBox(const char *message, int rows, int cols, bool error) { - Gtk::MessageDialog dialog(*GW, message, /*use_markup*/ true, + Gtk::MessageDialog dialog(*(Gtk::Window *)SS.GW.window->NativePtr(), + message, /*use_markup*/ true, error ? Gtk::MESSAGE_ERROR : Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK, /*is_modal*/ true); dialog.set_title(error ? @@ -1031,23 +417,6 @@ static GdkFilterReturn GdkSpnavFilter(GdkXEvent *gxevent, GdkEvent *, gpointer) } #endif -/* Application lifecycle */ - -void SetMainMenu(Platform::MenuBarRef menuBar) { - static Platform::MenuBarRef _menuBar; - GW->set_menubar((Gtk::MenuBar*)menuBar->NativePtr()); - GW->get_menubar()->accelerate(*GW); - GW->get_menubar()->accelerate(*TW); - _menuBar = menuBar; - - SS.UpdateWindowTitle(); - TW->set_title(Title(C_("title", "Property Browser"))); -} - -void ExitNow() { - GW->hide(); - TW->hide(); -} }; int main(int argc, char** argv) { @@ -1091,21 +460,6 @@ int main(int argc, char** argv) { CnfLoad(); - auto icon = LoadPng("freedesktop/solvespace-48x48.png"); - auto icon_gdk = - Gdk::Pixbuf::create_from_data(&icon->data[0], Gdk::COLORSPACE_RGB, - icon->format == SolveSpace::Pixmap::Format::RGBA, 8, - icon->width, icon->height, icon->stride); - - TW.reset(new TextWindowGtk); - GW.reset(new GraphicsWindowGtk); - TW->set_transient_for(*GW); - GW->set_icon(icon_gdk); - TW->set_icon(icon_gdk); - - TW->show_all(); - GW->show_all(); - const char* const* langNames = g_get_language_names(); while(*langNames) { if(SetLocale(*langNames++)) break; @@ -1114,16 +468,17 @@ int main(int argc, char** argv) { SetLocale("en_US"); } + SS.Init(); + #if defined(HAVE_SPACEWARE) && defined(GDK_WINDOWING_X11) if(GDK_IS_X11_DISPLAY(Gdk::Display::get_default()->gobj())) { // We don't care if it can't be opened; just continue without. spnav_x11_open(gdk_x11_get_default_xdisplay(), - gdk_x11_window_get_xid(GW->get_window()->gobj())); + gdk_x11_window_get_xid(((Gtk::Window *)SS.GW.window->NativePtr()) + ->get_window()->gobj())); } #endif - SS.Init(); - if(argc >= 2) { if(argc > 2) { dbp("Only the first file passed on command line will be opened."); @@ -1134,10 +489,7 @@ int main(int argc, char** argv) { SS.Load(Platform::Path::From(arg).Expand(/*fromCurrentDirectory=*/true)); } - main.run(*GW); - - TW.reset(); - GW.reset(); + main.run(); SK.Clear(); SS.Clear(); diff --git a/src/platform/gui.h b/src/platform/gui.h index 1faded98..81a6306e 100644 --- a/src/platform/gui.h +++ b/src/platform/gui.h @@ -13,6 +13,36 @@ namespace Platform { // Events //----------------------------------------------------------------------------- +// A mouse input event. +class MouseEvent { +public: + enum class Type { + MOTION, + PRESS, + DBL_PRESS, + RELEASE, + SCROLL_VERT, + LEAVE, + }; + + enum class Button { + NONE, + LEFT, + MIDDLE, + RIGHT, + }; + + Type type; + double x; + double y; + bool shiftDown; + bool controlDown; + union { + Button button; // for Type::{MOTION,PRESS,DBL_PRESS,RELEASE} + double scrollDelta; // for Type::SCROLL_VERT + }; +}; + // A keyboard input event. struct KeyboardEvent { enum class Type { @@ -106,7 +136,6 @@ public: virtual std::shared_ptr AddSubMenu(const std::string &label) = 0; virtual void Clear() = 0; - virtual void *NativePtr() = 0; }; typedef std::shared_ptr MenuBarRef; @@ -114,6 +143,87 @@ typedef std::shared_ptr MenuBarRef; MenuRef CreateMenu(); MenuBarRef GetOrCreateMainMenu(bool *unique); +// A native top-level window, with an OpenGL context, and an editor overlay. +class Window { +public: + enum class Kind { + TOPLEVEL, + TOOL, + }; + + enum class Cursor { + POINTER, + HAND + }; + + std::function onClose; + std::function onFullScreen; + std::function onMouseEvent; + std::function onKeyboardEvent; + std::function onEditingDone; + std::function onScrollbarAdjusted; + std::function onRender; + + virtual ~Window() {} + + // Returns physical display DPI. + virtual double GetPixelDensity() = 0; + // Returns raster graphics and coordinate scale (already applied on the platform side), + // i.e. size of logical pixel in physical pixels, or device pixel ratio. + virtual int GetDevicePixelRatio() = 0; + // Returns (fractional) font scale, to be applied on top of (integral) device pixel ratio. + virtual double GetDeviceFontScale() { + return GetPixelDensity() / GetDevicePixelRatio() / 96.0; + } + + virtual bool IsVisible() = 0; + virtual void SetVisible(bool visible) = 0; + virtual void Focus() = 0; + + virtual bool IsFullScreen() = 0; + virtual void SetFullScreen(bool fullScreen) = 0; + + virtual void SetTitle(const std::string &title) = 0; + virtual bool SetTitleForFilename(const Path &filename) { return false; } + + virtual void SetMenuBar(MenuBarRef menuBar) = 0; + + virtual void GetContentSize(double *width, double *height) = 0; + virtual void SetMinContentSize(double width, double height) = 0; + + virtual void FreezePosition(const std::string &key) = 0; + virtual void ThawPosition(const std::string &key) = 0; + + virtual void SetCursor(Cursor cursor) = 0; + virtual void SetTooltip(const std::string &text) = 0; + + virtual bool IsEditorVisible() = 0; + virtual void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string &text) = 0; + virtual void HideEditor() = 0; + + virtual void SetScrollbarVisible(bool visible) = 0; + virtual void ConfigureScrollbar(double min, double max, double pageSize) = 0; + virtual double GetScrollbarPosition() = 0; + virtual void SetScrollbarPosition(double pos) = 0; + + virtual void Invalidate() = 0; + virtual void Redraw() = 0; + + virtual void *NativePtr() = 0; +}; + +typedef std::shared_ptr WindowRef; + +WindowRef CreateWindow(Window::Kind kind = Window::Kind::TOPLEVEL, + WindowRef parentWindow = NULL); + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +void Exit(); + } #endif diff --git a/src/platform/guigtk.cpp b/src/platform/guigtk.cpp index 8b13885a..cbd3bd12 100644 --- a/src/platform/guigtk.cpp +++ b/src/platform/guigtk.cpp @@ -3,12 +3,19 @@ // // Copyright 2018 whitequark //----------------------------------------------------------------------------- +#include "solvespace.h" #include +#include #include -#include +#include +#include +#include +#include #include #include -#include "solvespace.h" +#include +#include +#include namespace SolveSpace { namespace Platform { @@ -248,10 +255,6 @@ public: gtkMenuBar.foreach([&](Gtk::Widget &w) { gtkMenuBar.remove(w); }); subMenus.clear(); } - - void *NativePtr() override { - return >kMenuBar; - } }; MenuBarRef GetOrCreateMainMenu(bool *unique) { @@ -259,5 +262,584 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) { return std::make_shared(); } +//----------------------------------------------------------------------------- +// GTK GL and window extensions +//----------------------------------------------------------------------------- + +class GtkGLWidget : public Gtk::GLArea { + Window *_receiver; + +public: + GtkGLWidget(Platform::Window *receiver) : _receiver(receiver) { + set_has_depth_buffer(true); + set_can_focus(true); + set_events(Gdk::POINTER_MOTION_MASK | + Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::BUTTON_MOTION_MASK | + Gdk::SCROLL_MASK | + Gdk::LEAVE_NOTIFY_MASK | + Gdk::KEY_PRESS_MASK | + Gdk::KEY_RELEASE_MASK); + } + +protected: + // Work around a bug fixed in GTKMM 3.22: + // https://mail.gnome.org/archives/gtkmm-list/2016-April/msg00020.html + Glib::RefPtr on_create_context() override { + return get_window()->create_gl_context(); + } + + bool on_render(const Glib::RefPtr &context) override { + if(_receiver->onRender) { + _receiver->onRender(); + } + return true; + } + + bool process_pointer_event(MouseEvent::Type type, double x, double y, + guint state, guint button = 0, int scroll_delta = 0) { + MouseEvent event = {}; + event.type = type; + event.x = x; + event.y = y; + if(button == 1 || (state & GDK_BUTTON1_MASK) != 0) { + event.button = MouseEvent::Button::LEFT; + } else if(button == 2 || (state & GDK_BUTTON2_MASK) != 0) { + event.button = MouseEvent::Button::MIDDLE; + } else if(button == 3 || (state & GDK_BUTTON3_MASK) != 0) { + event.button = MouseEvent::Button::RIGHT; + } + if((state & GDK_SHIFT_MASK) != 0) { + event.shiftDown = true; + } + if((state & GDK_CONTROL_MASK) != 0) { + event.controlDown = true; + } + if(scroll_delta != 0) { + event.scrollDelta = scroll_delta; + } + + if(_receiver->onMouseEvent) { + return _receiver->onMouseEvent(event); + } + + return false; + } + + bool on_motion_notify_event(GdkEventMotion *gdk_event) override { + if(process_pointer_event(MouseEvent::Type::MOTION, + gdk_event->x, gdk_event->y, gdk_event->state)) + return true; + + return Gtk::GLArea::on_motion_notify_event(gdk_event); + } + + bool on_button_press_event(GdkEventButton *gdk_event) override { + MouseEvent::Type type; + if(gdk_event->type == GDK_BUTTON_PRESS) { + type = MouseEvent::Type::PRESS; + } else if(gdk_event->type == GDK_2BUTTON_PRESS) { + type = MouseEvent::Type::DBL_PRESS; + } else { + return Gtk::GLArea::on_button_press_event(gdk_event); + } + + if(process_pointer_event(type, gdk_event->x, gdk_event->y, + gdk_event->state, gdk_event->button)) + return true; + + return Gtk::GLArea::on_button_press_event(gdk_event); + } + + bool on_button_release_event(GdkEventButton *gdk_event) override { + if(process_pointer_event(MouseEvent::Type::RELEASE, + gdk_event->x, gdk_event->y, + gdk_event->state, gdk_event->button)) + return true; + + return Gtk::GLArea::on_button_release_event(gdk_event); + } + + bool on_scroll_event(GdkEventScroll *gdk_event) override { + int delta; + if(gdk_event->delta_y < 0 || gdk_event->direction == GDK_SCROLL_UP) { + delta = 1; + } else if(gdk_event->delta_y > 0 || gdk_event->direction == GDK_SCROLL_DOWN) { + delta = -1; + } else { + return false; + } + + if(process_pointer_event(MouseEvent::Type::SCROLL_VERT, + gdk_event->x, gdk_event->y, + gdk_event->state, 0, delta)) + return true; + + return Gtk::GLArea::on_scroll_event(gdk_event); + } + + bool on_leave_notify_event(GdkEventCrossing *gdk_event) override { + if(process_pointer_event(MouseEvent::Type::LEAVE, + gdk_event->x, gdk_event->y, gdk_event->state)) + return true; + + return Gtk::GLArea::on_leave_notify_event(gdk_event); + } + + bool process_key_event(KeyboardEvent::Type type, GdkEventKey *gdk_event) { + KeyboardEvent event = {}; + event.type = type; + + if(gdk_event->state & ~(GDK_SHIFT_MASK|GDK_CONTROL_MASK)) { + return false; + } + + event.shiftDown = (gdk_event->state & GDK_SHIFT_MASK) != 0; + event.controlDown = (gdk_event->state & GDK_CONTROL_MASK) != 0; + + char32_t chr = gdk_keyval_to_unicode(gdk_keyval_to_lower(gdk_event->keyval)); + if(chr != 0) { + event.key = KeyboardEvent::Key::CHARACTER; + event.chr = chr; + } else if(gdk_event->keyval >= GDK_KEY_F1 && + gdk_event->keyval <= GDK_KEY_F12) { + event.key = KeyboardEvent::Key::FUNCTION; + event.num = gdk_event->keyval - GDK_KEY_F1 + 1; + } else { + return false; + } + + if(SS.GW.KeyboardEvent(event)) { + return true; + } + + return false; + } + + bool on_key_press_event(GdkEventKey *gdk_event) override { + if(process_key_event(KeyboardEvent::Type::PRESS, gdk_event)) + return true; + + return Gtk::GLArea::on_key_press_event(gdk_event); + } + + bool on_key_release_event(GdkEventKey *gdk_event) override { + if(process_key_event(KeyboardEvent::Type::RELEASE, gdk_event)) + return true; + + return Gtk::GLArea::on_key_release_event(gdk_event); + } +}; + +class GtkEditorOverlay : public Gtk::Fixed { + Window *_receiver; + GtkGLWidget _gl_widget; + Gtk::Entry _entry; + +public: + GtkEditorOverlay(Platform::Window *receiver) : _receiver(receiver), _gl_widget(receiver) { + add(_gl_widget); + + _entry.set_no_show_all(true); + _entry.set_has_frame(false); + add(_entry); + + _entry.signal_activate(). + connect(sigc::mem_fun(this, &GtkEditorOverlay::on_activate)); + } + + bool is_editing() const { + return _entry.is_visible(); + } + + void start_editing(int x, int y, int font_height, int min_width, bool is_monospace, + const std::string &val) { + Pango::FontDescription font_desc; + font_desc.set_family(is_monospace ? "monospace" : "normal"); + font_desc.set_absolute_size(font_height * Pango::SCALE); + _entry.override_font(font_desc); + + // The y coordinate denotes baseline. + Pango::FontMetrics font_metrics = get_pango_context()->get_metrics(font_desc); + y -= font_metrics.get_ascent() / Pango::SCALE; + + Glib::RefPtr layout = Pango::Layout::create(get_pango_context()); + layout->set_font_description(font_desc); + // Add one extra char width to avoid scrolling. + layout->set_text(val + " "); + int width = layout->get_logical_extents().get_width(); + + Gtk::Border margin = _entry.get_style_context()->get_margin(); + Gtk::Border border = _entry.get_style_context()->get_border(); + Gtk::Border padding = _entry.get_style_context()->get_padding(); + move(_entry, + x - margin.get_left() - border.get_left() - padding.get_left(), + y - margin.get_top() - border.get_top() - padding.get_top()); + + int fitWidth = width / Pango::SCALE + padding.get_left() + padding.get_right(); + _entry.set_size_request(max(fitWidth, min_width), -1); + queue_resize(); + + _entry.set_text(val); + + if(!_entry.is_visible()) { + _entry.show(); + _entry.grab_focus(); + + // We grab the input for ourselves and not the entry to still have + // the pointer events go through the underlay. + add_modal_grab(); + } + } + + void stop_editing() { + if(_entry.is_visible()) { + remove_modal_grab(); + _entry.hide(); + _gl_widget.grab_focus(); + } + } + + GtkGLWidget &get_gl_widget() { + return _gl_widget; + } + +protected: + bool on_key_press_event(GdkEventKey *gdk_event) override { + if(is_editing()) { + if(gdk_event->keyval == GDK_KEY_Escape) { + stop_editing(); + } else { + _entry.event((GdkEvent *)gdk_event); + } + return true; + } + + return Gtk::Fixed::on_key_press_event(gdk_event); + } + + bool on_key_release_event(GdkEventKey *gdk_event) override { + if(is_editing()) { + _entry.event((GdkEvent *)gdk_event); + return true; + } + + return Gtk::Fixed::on_key_release_event(gdk_event); + } + + void on_size_allocate(Gtk::Allocation& allocation) override { + Gtk::Fixed::on_size_allocate(allocation); + + _gl_widget.size_allocate(allocation); + + int width, height, min_height, natural_height; + _entry.get_size_request(width, height); + _entry.get_preferred_height(min_height, natural_height); + + Gtk::Allocation entry_rect = _entry.get_allocation(); + entry_rect.set_width(width); + entry_rect.set_height(natural_height); + _entry.size_allocate(entry_rect); + } + + void on_activate() { + if(_receiver->onEditingDone) { + _receiver->onEditingDone(_entry.get_text()); + } + } +}; + +class GtkWindow : public Gtk::Window { + Platform::Window *_receiver; + Gtk::VBox _vbox; + Gtk::MenuBar *_menu_bar; + Gtk::HBox _hbox; + GtkEditorOverlay _editor_overlay; + Gtk::VScrollbar _scrollbar; + bool _is_fullscreen; + +public: + GtkWindow(Platform::Window *receiver) : + _receiver(receiver), _menu_bar(NULL), _editor_overlay(receiver) { + _hbox.pack_start(_editor_overlay, /*expand=*/true, /*fill=*/true); + _hbox.pack_end(_scrollbar, /*expand=*/false, /*fill=*/false); + _vbox.pack_end(_hbox, /*expand=*/true, /*fill=*/true); + add(_vbox); + + _vbox.show(); + _hbox.show(); + _editor_overlay.show(); + get_gl_widget().show(); + + _scrollbar.get_adjustment()->signal_value_changed(). + connect(sigc::mem_fun(this, &GtkWindow::on_scrollbar_value_changed)); + } + + bool is_full_screen() const { + return _is_fullscreen; + } + + Gtk::MenuBar *get_menu_bar() const { + return _menu_bar; + } + + void set_menu_bar(Gtk::MenuBar *menu_bar) { + if(_menu_bar) { + _vbox.remove(*_menu_bar); + } + _menu_bar = menu_bar; + if(_menu_bar) { + _menu_bar->show_all(); + _vbox.pack_start(*_menu_bar, /*expand=*/false, /*fill=*/false); + } + } + + GtkEditorOverlay &get_editor_overlay() { + return _editor_overlay; + } + + GtkGLWidget &get_gl_widget() { + return _editor_overlay.get_gl_widget(); + } + + Gtk::VScrollbar &get_scrollbar() { + return _scrollbar; + } + +protected: + bool on_delete_event(GdkEventAny* gdk_event) { + if(_receiver->onClose) { + _receiver->onClose(); + return true; + } + + return false; + } + + bool on_window_state_event(GdkEventWindowState *gdk_event) override { + _is_fullscreen = gdk_event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN; + if(_receiver->onFullScreen) { + _receiver->onFullScreen(_is_fullscreen); + } + + return Gtk::Window::on_window_state_event(gdk_event); + } + + void on_scrollbar_value_changed() { + if(_receiver->onScrollbarAdjusted) { + _receiver->onScrollbarAdjusted(_scrollbar.get_adjustment()->get_value()); + } + } +}; + +//----------------------------------------------------------------------------- +// Windows +//----------------------------------------------------------------------------- + +class WindowImplGtk : public Window { +public: + GtkWindow gtkWindow; + MenuBarRef menuBar; + + WindowImplGtk(Window::Kind kind) : gtkWindow(this) { + switch(kind) { + case Kind::TOPLEVEL: + break; + + case Kind::TOOL: + gtkWindow.set_type_hint(Gdk::WINDOW_TYPE_HINT_UTILITY); + gtkWindow.set_skip_taskbar_hint(true); + gtkWindow.set_skip_pager_hint(true); + break; + } + + auto icon = LoadPng("freedesktop/solvespace-48x48.png"); + auto gdkIcon = + Gdk::Pixbuf::create_from_data(&icon->data[0], Gdk::COLORSPACE_RGB, + icon->format == Pixmap::Format::RGBA, 8, + icon->width, icon->height, icon->stride); + gtkWindow.set_icon(gdkIcon->copy()); + } + + double GetPixelDensity() override { + return gtkWindow.get_screen()->get_resolution(); + } + + int GetDevicePixelRatio() override { + return gtkWindow.get_scale_factor(); + } + + bool IsVisible() override { + return gtkWindow.is_visible(); + } + + void SetVisible(bool visible) override { + if(visible) { + gtkWindow.show(); + } else { + gtkWindow.hide(); + } + } + + void Focus() override { + gtkWindow.present(); + } + + bool IsFullScreen() override { + return gtkWindow.is_full_screen(); + } + + void SetFullScreen(bool fullScreen) override { + if(fullScreen) { + gtkWindow.fullscreen(); + } else { + gtkWindow.unfullscreen(); + } + } + + void SetTitle(const std::string &title) override { + gtkWindow.set_title(title + " — SolveSpace"); + } + + void SetMenuBar(MenuBarRef newMenuBar) override { + if(newMenuBar) { + Gtk::MenuBar *gtkMenuBar = &((MenuBarImplGtk*)&*newMenuBar)->gtkMenuBar; + gtkWindow.set_menu_bar(gtkMenuBar); + } else { + gtkWindow.set_menu_bar(NULL); + } + menuBar = newMenuBar; + } + + void GetContentSize(double *width, double *height) override { + *width = gtkWindow.get_gl_widget().get_allocated_width(); + *height = gtkWindow.get_gl_widget().get_allocated_height(); + } + + void SetMinContentSize(double width, double height) override { + gtkWindow.get_gl_widget().set_size_request(width, height); + } + + void FreezePosition(const std::string &key) override { + if(!gtkWindow.is_visible()) return; + + int left, top, width, height; + gtkWindow.get_position(left, top); + gtkWindow.get_size(width, height); + bool isMaximized = gtkWindow.is_maximized(); + + CnfFreezeInt(left, key + "_left"); + CnfFreezeInt(top, key + "_top"); + CnfFreezeInt(width, key + "_width"); + CnfFreezeInt(height, key + "_height"); + CnfFreezeInt(isMaximized, key + "_maximized"); + } + + void ThawPosition(const std::string &key) override { + int left, top, width, height; + gtkWindow.get_position(left, top); + gtkWindow.get_size(width, height); + + left = CnfThawInt(left, key + "_left"); + top = CnfThawInt(top, key + "_top"); + width = CnfThawInt(width, key + "_width"); + height = CnfThawInt(height, key + "_height"); + + gtkWindow.move(left, top); + gtkWindow.resize(width, height); + + if(CnfThawInt(false, key + "_maximized")) { + gtkWindow.maximize(); + } + } + + void SetCursor(Cursor cursor) override { + Gdk::CursorType gdkCursorType; + switch(cursor) { + case Cursor::POINTER: gdkCursorType = Gdk::ARROW; break; + case Cursor::HAND: gdkCursorType = Gdk::HAND1; break; + } + + auto gdkWindow = gtkWindow.get_gl_widget().get_window(); + if(gdkWindow) { + gdkWindow->set_cursor(Gdk::Cursor::create(gdkCursorType)); + } + } + + void SetTooltip(const std::string &text) override { + if(text.empty()) { + gtkWindow.get_gl_widget().set_has_tooltip(false); + } else { + gtkWindow.get_gl_widget().set_tooltip_text(text); + } + } + + bool IsEditorVisible() override { + return gtkWindow.get_editor_overlay().is_editing(); + } + + void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string &text) override { + gtkWindow.get_editor_overlay().start_editing( + x, y, fontHeight, minWidth, isMonospace, text); + } + + void HideEditor() override { + gtkWindow.get_editor_overlay().stop_editing(); + } + + void SetScrollbarVisible(bool visible) override { + if(visible) { + gtkWindow.get_scrollbar().show(); + } else { + gtkWindow.get_scrollbar().hide(); + } + } + + void ConfigureScrollbar(double min, double max, double pageSize) override { + auto adjustment = gtkWindow.get_scrollbar().get_adjustment(); + adjustment->configure(adjustment->get_value(), min, max, 1, 4, pageSize); + } + + double GetScrollbarPosition() override { + return gtkWindow.get_scrollbar().get_adjustment()->get_value(); + } + + void SetScrollbarPosition(double pos) override { + return gtkWindow.get_scrollbar().get_adjustment()->set_value(pos); + } + + void Invalidate() override { + gtkWindow.get_gl_widget().queue_render(); + } + + void Redraw() override { + Invalidate(); + Gtk::Main::iteration(/*blocking=*/false); + } + + void *NativePtr() override { + return >kWindow; + } +}; + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + auto window = std::make_shared(kind); + if(parentWindow) { + window->gtkWindow.set_transient_for( + std::static_pointer_cast(parentWindow)->gtkWindow); + } + return window; +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +void Exit() { + Gtk::Main::quit(); +} + } } diff --git a/src/platform/guimac.mm b/src/platform/guimac.mm index 3c346b17..7b04d68b 100644 --- a/src/platform/guimac.mm +++ b/src/platform/guimac.mm @@ -6,6 +6,19 @@ #import #include "solvespace.h" +using namespace SolveSpace; + +//----------------------------------------------------------------------------- +// Internal AppKit classes +//----------------------------------------------------------------------------- + +@interface NSToolTipManager : NSObject ++ (NSToolTipManager *)sharedToolTipManager; +- (void)setInitialToolTipDelay:(double)delay; +- (void)orderOutToolTip; +- (void)_displayTemporaryToolTipForView:(id)arg1 withString:(id)arg2; +@end + //----------------------------------------------------------------------------- // Objective-C bridging //----------------------------------------------------------------------------- @@ -215,10 +228,6 @@ public: } subMenus.clear(); } - - void *NativePtr() override { - return NULL; - } }; MenuBarRef GetOrCreateMainMenu(bool *unique) { @@ -230,7 +239,677 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) { return mainMenu; } -void SetMainMenu(MenuBarRef menuBar) { +} +} + +//----------------------------------------------------------------------------- +// Cocoa NSView and NSWindow extensions +//----------------------------------------------------------------------------- + +@interface SSView : NSView +@property Platform::Window *receiver; + +@property BOOL acceptsFirstResponder; + +@property(readonly, getter=isEditing) BOOL editing; +- (void)startEditing:(NSString *)text at:(NSPoint)origin + withHeight:(double)fontHeight minWidth:(double)minWidth + usingMonospace:(BOOL)isMonospace; +- (void)stopEditing; +- (void)didEdit:(NSString *)text; + +@property double scrollerMin; +@property double scrollerMax; +@end + +@implementation SSView +{ + GlOffscreen offscreen; + NSOpenGLContext *glContext; + NSTrackingArea *trackingArea; + NSTextField *editor; +} + +- (id)initWithFrame:(NSRect)frameRect { + if(self = [super initWithFrame:frameRect]) { + self.wantsLayer = YES; + + NSOpenGLPixelFormatAttribute attrs[] = { + NSOpenGLPFAColorSize, 24, + NSOpenGLPFADepthSize, 24, + 0 + }; + NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; + glContext = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:NULL]; + + editor = [[NSTextField alloc] init]; + editor.editable = YES; + [[editor cell] setWraps:NO]; + [[editor cell] setScrollable:YES]; + editor.bezeled = NO; + editor.target = self; + editor.action = @selector(didEdit:); + } + return self; +} + +- (void)dealloc { + offscreen.Clear(); +} + +- (BOOL)isFlipped { + return YES; +} + +@synthesize receiver; + +- (void)drawRect:(NSRect)aRect { + [glContext makeCurrentContext]; + + NSSize size = [self convertSizeToBacking:self.bounds.size]; + int width = (int)size.width, + height = (int)size.height; + offscreen.Render(width, height, [&] { + if(receiver->onRender) { + receiver->onRender(); + } + }); + + CGDataProviderRef provider = CGDataProviderCreateWithData( + NULL, &offscreen.data[0], width * height * 4, NULL); + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); + CGImageRef image = CGImageCreate(width, height, 8, 32, + width * 4, colorspace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst, + provider, NULL, true, kCGRenderingIntentDefault); + + CGContextDrawImage((CGContextRef) [[NSGraphicsContext currentContext] graphicsPort], + [self bounds], image); + + CGImageRelease(image); + CGDataProviderRelease(provider); +} + +- (BOOL)acceptsFirstMouse:(NSEvent *)event { + return YES; +} + +- (void)updateTrackingAreas { + [self removeTrackingArea:trackingArea]; + trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] + options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | + ([self acceptsFirstResponder] + ? NSTrackingActiveInKeyWindow + : NSTrackingActiveAlways)) + owner:self userInfo:nil]; + [self addTrackingArea:trackingArea]; + [super updateTrackingAreas]; +} + +- (Platform::MouseEvent)convertMouseEvent:(NSEvent *)nsEvent { + Platform::MouseEvent event = {}; + + NSPoint nsPoint = [self convertPoint:nsEvent.locationInWindow fromView:self]; + event.x = nsPoint.x; + event.y = self.bounds.size.height - nsPoint.y; + + NSUInteger nsFlags = [nsEvent modifierFlags]; + if(nsFlags & NSShiftKeyMask) event.shiftDown = true; + if(nsFlags & NSCommandKeyMask) event.controlDown = true; + + return event; +} + +- (void)mouseMotionEvent:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + MouseEvent event = [self convertMouseEvent:nsEvent]; + event.type = MouseEvent::Type::MOTION; + event.button = MouseEvent::Button::NONE; + + NSUInteger nsButtons = [NSEvent pressedMouseButtons]; + if(nsButtons & (1 << 0)) { + event.button = MouseEvent::Button::LEFT; + } else if(nsButtons & (1 << 1)) { + event.button = MouseEvent::Button::RIGHT; + } else if(nsButtons & (1 << 2)) { + event.button = MouseEvent::Button::MIDDLE; + } + + if(receiver->onMouseEvent) { + receiver->onMouseEvent(event); + } +} + +- (void)mouseMoved:(NSEvent *)nsEvent { + [self mouseMotionEvent:nsEvent]; + [super mouseMoved:nsEvent]; +} + +- (void)mouseDragged:(NSEvent *)nsEvent { + [self mouseMotionEvent:nsEvent]; +} + +- (void)otherMouseDragged:(NSEvent *)nsEvent { + [self mouseMotionEvent:nsEvent]; +} + +- (void)rightMouseDragged:(NSEvent *)nsEvent { + [self mouseMotionEvent:nsEvent]; +} + +- (void)mouseButtonEvent:(NSEvent *)nsEvent withType:(Platform::MouseEvent::Type)type { + using Platform::MouseEvent; + + MouseEvent event = [self convertMouseEvent:nsEvent]; + event.type = type; + if([nsEvent buttonNumber] == 0) { + event.button = MouseEvent::Button::LEFT; + } else if([nsEvent buttonNumber] == 1) { + event.button = MouseEvent::Button::RIGHT; + } else if([nsEvent buttonNumber] == 2) { + event.button = MouseEvent::Button::MIDDLE; + } else return; + + if(receiver->onMouseEvent) { + receiver->onMouseEvent(event); + } +} + +- (void)mouseDownEvent:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + MouseEvent::Type type; + if([nsEvent clickCount] == 1) { + type = MouseEvent::Type::PRESS; + } else { + type = MouseEvent::Type::DBL_PRESS; + } + [self mouseButtonEvent:nsEvent withType:type]; +} + +- (void)mouseUpEvent:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + [self mouseButtonEvent:nsEvent withType:(MouseEvent::Type::RELEASE)]; +} + +- (void)mouseDown:(NSEvent *)nsEvent { + [self mouseDownEvent:nsEvent]; +} + +- (void)otherMouseDown:(NSEvent *)nsEvent { + [self mouseDownEvent:nsEvent]; +} + +- (void)rightMouseDown:(NSEvent *)nsEvent { + [self mouseDownEvent:nsEvent]; + [super rightMouseDown:nsEvent]; +} + +- (void)mouseUp:(NSEvent *)nsEvent { + [self mouseUpEvent:nsEvent]; +} + +- (void)otherMouseUp:(NSEvent *)nsEvent { + [self mouseUpEvent:nsEvent]; +} + +- (void)rightMouseUp:(NSEvent *)nsEvent { + [self mouseUpEvent:nsEvent]; +} + +- (void)scrollWheel:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + MouseEvent event = [self convertMouseEvent:nsEvent]; + event.type = MouseEvent::Type::SCROLL_VERT; + event.scrollDelta = [nsEvent deltaY]; + + if(receiver->onMouseEvent) { + receiver->onMouseEvent(event); + } +} + +- (void)mouseExited:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + MouseEvent event = [self convertMouseEvent:nsEvent]; + event.type = MouseEvent::Type::LEAVE; + + if(receiver->onMouseEvent) { + receiver->onMouseEvent(event); + } +} + +- (Platform::KeyboardEvent)convertKeyboardEvent:(NSEvent *)nsEvent { + using Platform::KeyboardEvent; + + KeyboardEvent event = {}; + + NSUInteger nsFlags = [nsEvent modifierFlags]; + if(nsFlags & NSShiftKeyMask) + event.shiftDown = true; + if(nsFlags & NSCommandKeyMask) + event.controlDown = true; + + unichar chr = 0; + if(NSString *nsChr = [[nsEvent charactersIgnoringModifiers] lowercaseString]) { + chr = [nsChr characterAtIndex:0]; + } + if(chr >= NSF1FunctionKey && chr <= NSF12FunctionKey) { + event.key = KeyboardEvent::Key::FUNCTION; + event.num = chr - NSF1FunctionKey + 1; + } else { + event.key = KeyboardEvent::Key::CHARACTER; + event.chr = chr; + } + + return event; +} + +- (void)keyDown:(NSEvent *)nsEvent { + using Platform::KeyboardEvent; + + if([NSEvent modifierFlags] & ~(NSShiftKeyMask|NSCommandKeyMask)) { + [super keyDown:nsEvent]; + return; + } + + KeyboardEvent event = [self convertKeyboardEvent:nsEvent]; + event.type = KeyboardEvent::Type::PRESS; + + if(receiver->onKeyboardEvent) { + receiver->onKeyboardEvent(event); + return; + } + + [super keyDown:nsEvent]; +} + +- (void)keyUp:(NSEvent *)nsEvent { + using Platform::KeyboardEvent; + + if([NSEvent modifierFlags] & ~(NSShiftKeyMask|NSCommandKeyMask)) { + [super keyUp:nsEvent]; + return; + } + + KeyboardEvent event = [self convertKeyboardEvent:nsEvent]; + event.type = KeyboardEvent::Type::RELEASE; + + if(receiver->onKeyboardEvent) { + receiver->onKeyboardEvent(event); + return; + } + + [super keyUp:nsEvent]; +} + +@synthesize editing; + +- (void)startEditing:(NSString *)text at:(NSPoint)origin withHeight:(double)fontHeight + minWidth:(double)minWidth usingMonospace:(BOOL)isMonospace { + if(!editing) { + [self addSubview:editor]; + editing = YES; + } + + if(isMonospace) { + editor.font = [NSFont fontWithName:@"Monaco" size:fontHeight]; + } else { + editor.font = [NSFont controlContentFontOfSize:fontHeight]; + } + + origin.x -= 3; /* left padding; no way to get it from NSTextField */ + origin.y -= [editor intrinsicContentSize].height; + origin.y += [editor baselineOffsetFromBottom]; + + [editor setFrameOrigin:origin]; + [editor setStringValue:text]; + [editor sizeToFit]; + + NSSize frameSize = [editor frame].size; + frameSize.width = std::max(frameSize.width, minWidth); + [editor setFrameSize:frameSize]; + + [[self window] makeFirstResponder:editor]; + [[self window] makeKeyWindow]; +} + +- (void)stopEditing { + if(editing) { + [editor removeFromSuperview]; + [[self window] makeFirstResponder:self]; + editing = NO; + } +} + +- (void)didEdit:(id)sender { + if(receiver->onEditingDone) { + receiver->onEditingDone([[editor stringValue] UTF8String]); + } +} + +- (void)cancelOperation:(id)sender { + using Platform::KeyboardEvent; + + if(receiver->onKeyboardEvent) { + KeyboardEvent event = {}; + event.key = KeyboardEvent::Key::CHARACTER; + event.chr = '\e'; + event.type = KeyboardEvent::Type::PRESS; + receiver->onKeyboardEvent(event); + event.type = KeyboardEvent::Type::RELEASE; + receiver->onKeyboardEvent(event); + } +} + +@synthesize scrollerMin; +@synthesize scrollerMax; + +- (void)didScroll:(NSScroller *)sender { + if(receiver->onScrollbarAdjusted) { + double pos = scrollerMin + [sender doubleValue] * (scrollerMax - scrollerMin); + receiver->onScrollbarAdjusted(pos); + } +} +@end + +@interface SSWindowDelegate : NSObject +@property Platform::Window *receiver; + +- (BOOL)windowShouldClose:(id)sender; + +@property(readonly, getter=isFullScreen) BOOL fullScreen; +- (void)windowDidEnterFullScreen:(NSNotification *)notification; +- (void)windowDidExitFullScreen:(NSNotification *)notification; +@end + +@implementation SSWindowDelegate +@synthesize receiver; + +- (BOOL)windowShouldClose:(id)sender { + if(receiver->onClose) { + receiver->onClose(); + } + return NO; +} + +@synthesize fullScreen; + +- (void)windowDidEnterFullScreen:(NSNotification *)notification { + fullScreen = true; + if(receiver->onFullScreen) { + receiver->onFullScreen(fullScreen); + } +} + +- (void)windowDidExitFullScreen:(NSNotification *)notification { + fullScreen = false; + if(receiver->onFullScreen) { + receiver->onFullScreen(fullScreen); + } +} +@end + +namespace SolveSpace { +namespace Platform { + +//----------------------------------------------------------------------------- +// Windows +//----------------------------------------------------------------------------- + +class WindowImplCocoa : public Window { +public: + NSWindow *nsWindow; + SSWindowDelegate *ssWindowDelegate; + SSView *ssView; + NSScroller *nsScroller; + NSView *nsContainer; + + NSArray *nsConstraintsWithScrollbar; + NSArray *nsConstraintsWithoutScrollbar; + + double minWidth = 100.0; + double minHeight = 100.0; + + NSString *nsToolTip; + + WindowImplCocoa(Window::Kind kind, std::shared_ptr parentWindow) { + ssView = [[SSView alloc] init]; + ssView.translatesAutoresizingMaskIntoConstraints = NO; + ssView.receiver = this; + + nsScroller = [[NSScroller alloc] initWithFrame:NSMakeRect(0, 0, 0, 100)]; + nsScroller.translatesAutoresizingMaskIntoConstraints = NO; + nsScroller.enabled = YES; + nsScroller.scrollerStyle = NSScrollerStyleOverlay; + nsScroller.knobStyle = NSScrollerKnobStyleLight; + nsScroller.action = @selector(didScroll:); + nsScroller.target = ssView; + nsScroller.continuous = YES; + + nsContainer = [[NSView alloc] init]; + [nsContainer addSubview:ssView]; + [nsContainer addSubview:nsScroller]; + + NSDictionary *views = NSDictionaryOfVariableBindings(ssView, nsScroller); + nsConstraintsWithoutScrollbar = [NSLayoutConstraint + constraintsWithVisualFormat:@"H:|[ssView]|" + options:0 metrics:nil views:views]; + [nsContainer addConstraints:nsConstraintsWithoutScrollbar]; + nsConstraintsWithScrollbar = [NSLayoutConstraint + constraintsWithVisualFormat:@"H:|[ssView]-0-[nsScroller(11)]|" + options:0 metrics:nil views:views]; + [nsContainer addConstraints:[NSLayoutConstraint + constraintsWithVisualFormat:@"V:|[ssView]|" + options:0 metrics:nil views:views]]; + [nsContainer addConstraints:[NSLayoutConstraint + constraintsWithVisualFormat:@"V:|[nsScroller]|" + options:0 metrics:nil views:views]]; + + switch(kind) { + case Window::Kind::TOPLEVEL: + nsWindow = [[NSWindow alloc] init]; + nsWindow.styleMask = NSTitledWindowMask | NSResizableWindowMask | + NSClosableWindowMask | NSMiniaturizableWindowMask; + nsWindow.collectionBehavior = NSWindowCollectionBehaviorFullScreenPrimary; + ssView.acceptsFirstResponder = YES; + break; + + case Window::Kind::TOOL: + NSPanel *nsPanel = [[NSPanel alloc] init]; + nsPanel.styleMask = NSTitledWindowMask | NSResizableWindowMask | + NSClosableWindowMask | NSUtilityWindowMask; + [nsPanel standardWindowButton:NSWindowMiniaturizeButton].hidden = YES; + [nsPanel standardWindowButton:NSWindowZoomButton].hidden = YES; + nsPanel.floatingPanel = YES; + nsPanel.becomesKeyOnlyIfNeeded = YES; + nsWindow = nsPanel; + break; + } + + ssWindowDelegate = [[SSWindowDelegate alloc] init]; + ssWindowDelegate.receiver = this; + nsWindow.delegate = ssWindowDelegate; + + nsWindow.backgroundColor = [NSColor blackColor]; + nsWindow.contentView = nsContainer; + } + + double GetPixelDensity() override { + NSDictionary *description = nsWindow.screen.deviceDescription; + NSSize displayPixelSize = [[description objectForKey:NSDeviceSize] sizeValue]; + CGSize displayPhysicalSize = CGDisplayScreenSize( + [[description objectForKey:@"NSScreenNumber"] unsignedIntValue]); + return (displayPixelSize.width / displayPhysicalSize.width) * 25.4f; + } + + int GetDevicePixelRatio() override { + NSSize unitSize = { 1.0f, 0.0f }; + unitSize = [ssView convertSizeToBacking:unitSize]; + return (int)unitSize.width; + } + + bool IsVisible() override { + return ![nsWindow isVisible]; + } + + void SetVisible(bool visible) override { + if(visible) { + [nsWindow orderFront:nil]; + } else { + [nsWindow close]; + } + } + + void Focus() override { + [nsWindow makeKeyAndOrderFront:nil]; + } + + bool IsFullScreen() override { + return ssWindowDelegate.fullScreen; + } + + void SetFullScreen(bool fullScreen) override { + if(fullScreen != IsFullScreen()) { + [nsWindow toggleFullScreen:nil]; + } + } + + void SetTitle(const std::string &title) override { + nsWindow.representedFilename = @""; + nsWindow.title = Wrap(title); + } + + bool SetTitleForFilename(const Path &filename) override { + [nsWindow setTitleWithRepresentedFilename:Wrap(filename.raw)]; + return true; + } + + void SetMenuBar(MenuBarRef newMenuBar) override { + // Doesn't do anything, since we have an unique global menu bar. + } + + void GetContentSize(double *width, double *height) override { + NSSize nsSize = [ssView frame].size; + *width = nsSize.width; + *height = nsSize.height; + } + + void SetMinContentSize(double width, double height) { + NSSize nsMinSize; + nsMinSize.width = width; + nsMinSize.height = height; + [nsWindow setContentMinSize:nsMinSize]; + [nsWindow setContentSize:nsMinSize]; + } + + void FreezePosition(const std::string &key) override { + [nsWindow saveFrameUsingName:Wrap(key)]; + } + + void ThawPosition(const std::string &key) override { + [nsWindow setFrameUsingName:Wrap(key)]; + } + + void SetCursor(Cursor cursor) override { + NSCursor *nsNewCursor; + switch(cursor) { + case Cursor::POINTER: nsNewCursor = [NSCursor arrowCursor]; break; + case Cursor::HAND: nsNewCursor = [NSCursor pointingHandCursor]; break; + } + + if([NSCursor currentCursor] != nsNewCursor) { + [nsNewCursor set]; + } + } + + void SetTooltip(const std::string &newText) override { + NSString *nsNewText = Wrap(newText); + if(![[ssView toolTip] isEqualToString:nsNewText]) { + [ssView setToolTip:nsNewText]; + + NSToolTipManager *nsToolTipManager = [NSToolTipManager sharedToolTipManager]; + if(newText.empty()) { + [nsToolTipManager orderOutToolTip]; + } else { + [nsToolTipManager _displayTemporaryToolTipForView:ssView withString:Wrap(newText)]; + } + } + } + + bool IsEditorVisible() override { + return [ssView isEditing]; + } + + void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string &text) override { + [ssView startEditing:Wrap(text) at:(NSPoint){(CGFloat)x, (CGFloat)y} + withHeight:fontHeight minWidth:minWidth usingMonospace:isMonospace]; + } + + void HideEditor() override { + [ssView stopEditing]; + } + + void SetScrollbarVisible(bool visible) override { + if(visible) { + [nsContainer removeConstraints:nsConstraintsWithoutScrollbar]; + [nsContainer addConstraints:nsConstraintsWithScrollbar]; + } else { + [nsContainer removeConstraints:nsConstraintsWithScrollbar]; + [nsContainer addConstraints:nsConstraintsWithoutScrollbar]; + } + } + + void ConfigureScrollbar(double min, double max, double pageSize) override { + ssView.scrollerMin = min; + ssView.scrollerMax = max - pageSize; + [nsScroller setKnobProportion:(pageSize / (ssView.scrollerMax - ssView.scrollerMin))]; + } + + double GetScrollbarPosition() override { + return ssView.scrollerMin + + [nsScroller doubleValue] * (ssView.scrollerMax - ssView.scrollerMin); + } + + void SetScrollbarPosition(double pos) override { + if(pos > ssView.scrollerMax) { + pos = ssView.scrollerMax; + } + [nsScroller setDoubleValue:(pos / (ssView.scrollerMax - ssView.scrollerMin))]; + if(onScrollbarAdjusted) { + onScrollbarAdjusted(pos); + } + } + + void Invalidate() override { + ssView.needsDisplay = YES; + } + + void Redraw() override { + Invalidate(); + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, YES); + } + + void *NativePtr() override { + return (__bridge void *)ssView; + } +}; + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + return std::make_shared(kind, + std::static_pointer_cast(parentWindow)); +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +void Exit() { + [NSApp setDelegate:nil]; + [NSApp terminate:nil]; } } diff --git a/src/platform/guinone.cpp b/src/platform/guinone.cpp index 416c86bf..71385f4e 100644 --- a/src/platform/guinone.cpp +++ b/src/platform/guinone.cpp @@ -8,6 +8,14 @@ namespace SolveSpace { +//----------------------------------------------------------------------------- +// Rendering +//----------------------------------------------------------------------------- + +std::shared_ptr CreateRenderer() { + return std::make_shared(); +} + namespace Platform { //----------------------------------------------------------------------------- @@ -27,50 +35,31 @@ TimerRef CreateTimer() { // Menus //----------------------------------------------------------------------------- -class MenuItemImplDummy : public MenuItem { -public: - void SetAccelerator(KeyboardEvent accel) override {} - void SetIndicator(Indicator type) override {} - void SetActive(bool active) override {} - void SetEnabled(bool enabled) override {} -}; - -class MenuImplDummy : public Menu { -public: - MenuItemRef AddItem(const std::string &label, - std::function onTrigger = NULL) override { - return std::make_shared(); - } - MenuRef AddSubMenu(const std::string &label) override { - return std::make_shared(); - } - - void AddSeparator() override {} - void PopUp() override {} - void Clear() override {} -}; - MenuRef CreateMenu() { - return std::make_shared(); + return std::shared_ptr(); } -class MenuBarImplDummy : public MenuBar { -public: - MenuRef AddSubMenu(const std::string &label) override { - return std::make_shared(); - } - void Clear() override {} - void *NativePtr() override { return NULL; } -}; - MenuBarRef GetOrCreateMainMenu(bool *unique) { *unique = false; - return std::make_shared(); + return std::shared_ptr(); } +//----------------------------------------------------------------------------- +// Windows +//----------------------------------------------------------------------------- + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + return std::shared_ptr(); +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +void Exit() { + exit(0); } -void SetMainMenu(Platform::MenuBarRef menuBar) { } //----------------------------------------------------------------------------- @@ -143,106 +132,6 @@ std::string CnfThawString(const std::string &val, const std::string &key) { return ret; } -//----------------------------------------------------------------------------- -// Rendering -//----------------------------------------------------------------------------- - -std::shared_ptr CreateRenderer() { - return NULL; -} - -//----------------------------------------------------------------------------- -// Graphics window -//----------------------------------------------------------------------------- - -void GetGraphicsWindowSize(int *w, int *h) { - *w = *h = 600; -} -double GetScreenDpi() { - return 72; -} - -void InvalidateGraphics() { -} - -std::shared_ptr framebuffer; -bool antialias = true; -void PaintGraphics() { - const Camera &camera = SS.GW.GetCamera(); - - std::shared_ptr pixmap = std::make_shared(); - pixmap->format = Pixmap::Format::BGRA; - pixmap->width = camera.width; - pixmap->height = camera.height; - pixmap->stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, (int)camera.width); - pixmap->data = std::vector(pixmap->stride * pixmap->height); - cairo_surface_t *surface = - cairo_image_surface_create_for_data(&pixmap->data[0], CAIRO_FORMAT_RGB24, - (int)pixmap->width, (int)pixmap->height, - (int)pixmap->stride); - cairo_t *context = cairo_create(surface); - - CairoRenderer canvas; - canvas.camera = camera; - canvas.lighting = SS.GW.GetLighting(); - canvas.chordTolerance = SS.chordTol; - canvas.context = context; - canvas.antialias = antialias; - - SS.GW.Draw(&canvas); - canvas.CullOccludedStrokes(); - canvas.OutputInPaintOrder(); - - pixmap->ConvertTo(Pixmap::Format::RGBA); - framebuffer = pixmap; - - canvas.Clear(); - - cairo_surface_destroy(surface); - cairo_destroy(context); -} - -void SetCurrentFilename(const Platform::Path &filename) { -} -void ToggleFullScreen() { -} -bool FullScreenIsActive() { - return false; -} -void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, - const std::string &val) { - ssassert(false, "Not implemented"); -} -void HideGraphicsEditControl() { -} -bool GraphicsEditControlIsVisible() { - return false; -} - -//----------------------------------------------------------------------------- -// Text window -//----------------------------------------------------------------------------- - -void ShowTextWindow(bool visible) { -} -void GetTextWindowSize(int *w, int *h) { - *w = *h = 100; -} -void InvalidateText() { -} -void MoveTextScrollbarTo(int pos, int maxPos, int page) { -} -void SetMousePointerToHand(bool is_hand) { -} -void ShowTextEditControl(int x, int y, const std::string &val) { - ssassert(false, "Not implemented"); -} -void HideTextEditControl() { -} -bool TextEditControlIsVisible() { - return false; -} - //----------------------------------------------------------------------------- // Dialogs //----------------------------------------------------------------------------- @@ -282,15 +171,4 @@ std::vector GetFontFiles() { return fontFiles; } -//----------------------------------------------------------------------------- -// Application lifecycle -//----------------------------------------------------------------------------- - -void RefreshLocale() { -} - -void ExitNow() { - ssassert(false, "Not implemented"); -} - } diff --git a/src/platform/guiwin.cpp b/src/platform/guiwin.cpp index 85afafaa..19974505 100644 --- a/src/platform/guiwin.cpp +++ b/src/platform/guiwin.cpp @@ -3,9 +3,24 @@ // // Copyright 2018 whitequark //----------------------------------------------------------------------------- +#include "config.h" #include "solvespace.h" // Include after solvespace.h to avoid identifier clashes. #include +#include +#include + +#ifndef WM_DPICHANGED +#define WM_DPICHANGED 0x02E0 +#endif + +// We have our own CreateWindow. +#undef CreateWindow + +#if HAVE_OPENGL == 3 +#define EGLAPI /*static linkage*/ +#include +#endif namespace SolveSpace { namespace Platform { @@ -35,6 +50,58 @@ void CheckLastError(const char *file, int line, const char *function, const char } } +typedef UINT (WINAPI *LPFNGETDPIFORWINDOW)(HWND); + +UINT ssGetDpiForWindow(HWND hwnd) { + static bool checked; + static LPFNGETDPIFORWINDOW lpfnGetDpiForWindow; + if(!checked) { + checked = true; + lpfnGetDpiForWindow = (LPFNGETDPIFORWINDOW) + GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"); + } + if(lpfnGetDpiForWindow) { + return lpfnGetDpiForWindow(hwnd); + } else { + HDC hDc; + sscheck(hDc = GetDC(HWND_DESKTOP)); + UINT dpi; + sscheck(dpi = GetDeviceCaps(hDc, LOGPIXELSX)); + sscheck(ReleaseDC(HWND_DESKTOP, hDc)); + return dpi; + } +} + +typedef BOOL (WINAPI *LPFNADJUSTWINDOWRECTEXFORDPI)(LPRECT, DWORD, BOOL, DWORD, UINT); + +BOOL ssAdjustWindowRectExForDpi(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, + DWORD dwExStyle, UINT dpi) { + static bool checked; + static LPFNADJUSTWINDOWRECTEXFORDPI lpfnAdjustWindowRectExForDpi; + if(!checked) { + checked = true; + lpfnAdjustWindowRectExForDpi = (LPFNADJUSTWINDOWRECTEXFORDPI) + GetProcAddress(GetModuleHandleW(L"user32.dll"), "AdjustWindowRectExForDpi"); + } + if(lpfnAdjustWindowRectExForDpi) { + return lpfnAdjustWindowRectExForDpi(lpRect, dwStyle, bMenu, dwExStyle, dpi); + } else { + return AdjustWindowRectEx(lpRect, dwStyle, bMenu, dwExStyle); + } +} + +//----------------------------------------------------------------------------- +// Utility functions +//----------------------------------------------------------------------------- + +std::wstring Title(const std::string &s) { + return Widen("SolveSpace - " + s); +} + +static int Clamp(int x, int a, int b) { + return max(a, min(x, b)); +} + //----------------------------------------------------------------------------- // Timers //----------------------------------------------------------------------------- @@ -153,14 +220,7 @@ public: } }; -void TriggerMenu(int id) { - MenuItemImplWin32 *menuItem = (MenuItemImplWin32 *)id; - if(menuItem->onTrigger) { - menuItem->onTrigger(); - } -} - -int64_t contextMenuCancelTime = 0; +int64_t contextMenuPopTime = 0; class MenuImplWin32 : public Menu { public: @@ -172,12 +232,6 @@ public: MenuImplWin32() { sscheck(hMenu = CreatePopupMenu()); - - MENUINFO mi = {}; - mi.cbSize = sizeof(mi); - mi.fMask = MIM_STYLE; - mi.dwStyle = MNS_NOTIFYBYPOS; - sscheck(SetMenuInfo(hMenu, &mi)); } MenuItemRef AddItem(const std::string &label, @@ -187,7 +241,15 @@ public: menuItem->onTrigger = onTrigger; menuItems.push_back(menuItem); - sscheck(AppendMenuW(hMenu, MF_STRING, (UINT_PTR)&*menuItem, Widen(label).c_str())); + sscheck(AppendMenuW(hMenu, MF_STRING, (UINT_PTR)menuItem.get(), Widen(label).c_str())); + + // uID is just an UINT, which isn't large enough to hold a pointer on 64-bit Windows, + // so we use dwItemData, which is. + MENUITEMINFOW mii = {}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_DATA; + mii.dwItemData = (LONG_PTR)menuItem.get(); + sscheck(SetMenuItemInfoW(hMenu, (UINT_PTR)menuItem.get(), FALSE, &mii)); return menuItem; } @@ -208,15 +270,17 @@ public: } void PopUp() override { + MENUINFO mi = {}; + mi.cbSize = sizeof(mi); + mi.fMask = MIM_APPLYTOSUBMENUS|MIM_STYLE; + mi.dwStyle = MNS_NOTIFYBYPOS; + sscheck(SetMenuInfo(hMenu, &mi)); + POINT pt; sscheck(GetCursorPos(&pt)); - int id = TrackPopupMenu(hMenu, TPM_TOPALIGN|TPM_RIGHTBUTTON|TPM_RETURNCMD, - pt.x, pt.y, 0, GetActiveWindow(), NULL); - if(id == 0) { - contextMenuCancelTime = GetMilliseconds(); - } else { - TriggerMenu(id); - } + + sscheck(TrackPopupMenu(hMenu, TPM_TOPALIGN, pt.x, pt.y, 0, GetActiveWindow(), NULL)); + contextMenuPopTime = GetMilliseconds(); } void Clear() override { @@ -276,10 +340,6 @@ public: Clear(); sscheck(DestroyMenu(hMenuBar)); } - - void *NativePtr() override { - return hMenuBar; - } }; MenuBarRef GetOrCreateMainMenu(bool *unique) { @@ -287,5 +347,826 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) { return std::make_shared(); } +//----------------------------------------------------------------------------- +// Windows +//----------------------------------------------------------------------------- + +#define SCROLLBAR_UNIT 65536 + +class WindowImplWin32 : public Window { +public: + HWND hWindow = NULL; + HWND hTooltip = NULL; + HWND hEditor = NULL; + WNDPROC editorWndProc = NULL; + +#if HAVE_OPENGL == 1 + HGLRC hGlRc = NULL; +#elif HAVE_OPENGL == 3 + EGLDisplay eglDisplay = NULL; + EGLSurface eglSurface = NULL; + EGLContext eglContext = NULL; +#endif + + WINDOWPLACEMENT placement = {}; + int minWidth = 0, minHeight = 0; + + std::shared_ptr menuBar; + std::string tooltipText; + bool scrollbarVisible = false; + + static void RegisterWindowClass() { + static bool registered; + if(registered) return; + + WNDCLASSEX wc = {}; + wc.cbSize = sizeof(wc); + wc.style = CS_BYTEALIGNCLIENT|CS_BYTEALIGNWINDOW|CS_OWNDC|CS_DBLCLKS; + wc.lpfnWndProc = WndProc; + wc.cbWndExtra = sizeof(WindowImplWin32 *); + wc.hIcon = (HICON)LoadImage(GetModuleHandle(NULL), MAKEINTRESOURCE(4000), + IMAGE_ICON, 32, 32, 0); + wc.hIconSm = (HICON)LoadImage(GetModuleHandle(NULL), MAKEINTRESOURCE(4000), + IMAGE_ICON, 16, 16, 0); + wc.hCursor = LoadCursorW(NULL, IDC_ARROW); + wc.lpszClassName = L"SolveSpace"; + sscheck(RegisterClassEx(&wc)); + registered = true; + } + + WindowImplWin32(Window::Kind kind, std::shared_ptr parentWindow) { + placement.length = sizeof(placement); + + RegisterWindowClass(); + + HWND hParentWindow = NULL; + if(parentWindow) { + hParentWindow = parentWindow->hWindow; + } + + DWORD style = WS_SIZEBOX|WS_CLIPCHILDREN; + switch(kind) { + case Window::Kind::TOPLEVEL: + style |= WS_OVERLAPPEDWINDOW|WS_CLIPSIBLINGS; + break; + + case Window::Kind::TOOL: + style |= WS_POPUPWINDOW|WS_CAPTION; + break; + } + sscheck(hWindow = CreateWindowExW(0, L"SolveSpace", L"", style, + CW_USEDEFAULT, CW_USEDEFAULT, + CW_USEDEFAULT, CW_USEDEFAULT, + hParentWindow, NULL, NULL, NULL)); + sscheck(SetWindowLongPtr(hWindow, 0, (LONG_PTR)this)); + if(hParentWindow != NULL) { + sscheck(SetWindowPos(hWindow, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE)); + } + + sscheck(hTooltip = CreateWindowExW(0, TOOLTIPS_CLASS, NULL, + WS_POPUP|TTS_NOPREFIX|TTS_ALWAYSTIP, + CW_USEDEFAULT, CW_USEDEFAULT, + CW_USEDEFAULT, CW_USEDEFAULT, + hWindow, NULL, NULL, NULL)); + sscheck(SetWindowPos(hTooltip, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE)); + + TOOLINFOW ti = {}; + ti.cbSize = sizeof(ti); + ti.uFlags = TTF_IDISHWND|TTF_SUBCLASS; + ti.hwnd = hWindow; + ti.uId = (UINT_PTR)hWindow; + ti.lpszText = (LPWSTR)L""; + sscheck(SendMessageW(hTooltip, TTM_ADDTOOLW, 0, (LPARAM)&ti)); + sscheck(SendMessageW(hTooltip, TTM_ACTIVATE, FALSE, 0)); + + DWORD editorStyle = WS_CLIPSIBLINGS|WS_CHILD|WS_TABSTOP|ES_AUTOHSCROLL; + sscheck(hEditor = CreateWindowExW(WS_EX_CLIENTEDGE, WC_EDIT, L"", editorStyle, + 0, 0, 0, 0, hWindow, NULL, NULL, NULL)); + sscheck(editorWndProc = + (WNDPROC)SetWindowLongPtr(hEditor, GWLP_WNDPROC, (LONG_PTR)EditorWndProc)); + + HDC hDc; + sscheck(hDc = GetDC(hWindow)); + +#if HAVE_OPENGL == 1 + PIXELFORMATDESCRIPTOR pfd = {}; + pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR); + pfd.nVersion = 1; + pfd.dwFlags = PFD_DRAW_TO_WINDOW|PFD_SUPPORT_OPENGL|PFD_DOUBLEBUFFER; + pfd.dwLayerMask = PFD_MAIN_PLANE; + pfd.iPixelType = PFD_TYPE_RGBA; + pfd.cColorBits = 32; + pfd.cDepthBits = 24; + pfd.cAccumBits = 0; + pfd.cStencilBits = 0; + int pixelFormat; + sscheck(pixelFormat = ChoosePixelFormat(hDc, &pfd)); + sscheck(SetPixelFormat(hDc, pixelFormat, &pfd)); + + sscheck(hGlRc = wglCreateContext(hDc)); +#elif HAVE_OPENGL == 3 + ssassert(eglBindAPI(EGL_OPENGL_ES_API), "Cannot bind EGL API"); + + eglDisplay = eglGetDisplay(hDc); + ssassert(eglInitialize(eglDisplay, NULL, NULL), "Cannot initialize EGL"); + + EGLint configAttributes[] = { + EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_DEPTH_SIZE, 24, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_NONE + }; + EGLint numConfigs; + EGLConfig windowConfig; + ssassert(eglChooseConfig(eglDisplay, configAttributes, &windowConfig, 1, &numConfigs), + "Cannot choose EGL configuration"); + + EGLint surfaceAttributes[] = { + EGL_NONE + }; + eglSurface = eglCreateWindowSurface(eglDisplay, windowConfig, hWindow, surfaceAttributes); + ssassert(eglSurface != EGL_NO_SURFACE, "Cannot create EGL window surface"); + + EGLint contextAttributes[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + eglContext = eglCreateContext(eglDisplay, windowConfig, NULL, contextAttributes); + ssassert(eglContext != EGL_NO_CONTEXT, "Cannot create EGL context"); +#endif + + sscheck(ReleaseDC(hWindow, hDc)); + } + + static LRESULT CALLBACK WndProc(HWND h, UINT msg, WPARAM wParam, LPARAM lParam) { + if(handlingFatalError) return TRUE; + + WindowImplWin32 *window; + sscheck(window = (WindowImplWin32 *)GetWindowLongPtr(h, 0)); + + // The wndproc may be called from within CreateWindowEx, and before we've associated + // the window with the WindowImplWin32. In that case, just defer to the default wndproc. + if(window == NULL) { + return DefWindowProc(h, msg, wParam, lParam); + } + + switch (msg) { + case WM_ERASEBKGND: + break; + + case WM_PAINT: { + PAINTSTRUCT ps; + HDC hDc = BeginPaint(window->hWindow, &ps); + if(window->onRender) { +#if HAVE_OPENGL == 1 + wglMakeCurrent(hDc, window->hGlRc); +#elif HAVE_OPENGL == 3 + eglMakeCurrent(window->eglDisplay, window->eglSurface, + window->eglSurface, window->eglContext); +#endif + window->onRender(); +#if HAVE_OPENGL == 1 + SwapBuffers(hDc); +#elif HAVE_OPENGL == 3 + eglSwapBuffers(window->eglDisplay, window->eglSurface); + (void)hDc; +#endif + } + EndPaint(window->hWindow, &ps); + break; + } + + case WM_CLOSE: + case WM_DESTROY: + if(window->onClose) { + window->onClose(); + } + break; + + case WM_SIZE: + window->Invalidate(); + break; + + case WM_SIZING: { + int pixelRatio = window->GetDevicePixelRatio(); + + RECT rcw, rcc; + sscheck(GetWindowRect(window->hWindow, &rcw)); + sscheck(GetClientRect(window->hWindow, &rcc)); + int nonClientWidth = (rcw.right - rcw.left) - (rcc.right - rcc.left); + int nonClientHeight = (rcw.bottom - rcw.top) - (rcc.bottom - rcc.top); + + RECT *rc = (RECT *)lParam; + int adjWidth = rc->right - rc->left; + int adjHeight = rc->bottom - rc->top; + + adjWidth -= nonClientWidth; + adjWidth = max(window->minWidth * pixelRatio, adjWidth); + adjWidth += nonClientWidth; + adjHeight -= nonClientHeight; + adjHeight = max(window->minHeight * pixelRatio, adjHeight); + adjHeight += nonClientHeight; + switch(wParam) { + case WMSZ_RIGHT: + case WMSZ_BOTTOMRIGHT: + case WMSZ_TOPRIGHT: + rc->right = rc->left + adjWidth; + break; + + case WMSZ_LEFT: + case WMSZ_BOTTOMLEFT: + case WMSZ_TOPLEFT: + rc->left = rc->right - adjWidth; + break; + } + switch(wParam) { + case WMSZ_BOTTOM: + case WMSZ_BOTTOMLEFT: + case WMSZ_BOTTOMRIGHT: + rc->bottom = rc->top + adjHeight; + break; + + case WMSZ_TOP: + case WMSZ_TOPLEFT: + case WMSZ_TOPRIGHT: + rc->top = rc->bottom - adjHeight; + break; + } + break; + } + + case WM_DPICHANGED: { + RECT rc = *(RECT *)lParam; + sscheck(SendMessage(window->hWindow, WM_SIZING, WMSZ_BOTTOMRIGHT, (LPARAM)&rc)); + sscheck(SetWindowPos(window->hWindow, NULL, rc.left, rc.top, + rc.right - rc.left, rc.bottom - rc.top, + SWP_NOZORDER|SWP_NOACTIVATE)); + window->Invalidate(); + break; + } + + case WM_LBUTTONDOWN: + case WM_MBUTTONDOWN: + case WM_RBUTTONDOWN: + case WM_LBUTTONDBLCLK: + case WM_MBUTTONDBLCLK: + case WM_RBUTTONDBLCLK: + case WM_LBUTTONUP: + case WM_MBUTTONUP: + case WM_RBUTTONUP: + if(GetMilliseconds() - Platform::contextMenuPopTime < 100) { + // Ignore the mouse click that dismisses a context menu, to avoid + // (e.g.) clearing a selection. + return 0; + } + // fallthrough + case WM_MOUSEMOVE: + case WM_MOUSEWHEEL: + case WM_MOUSELEAVE: { + int pixelRatio = window->GetDevicePixelRatio(); + + MouseEvent event = {}; + event.x = GET_X_LPARAM(lParam) / pixelRatio; + event.y = GET_Y_LPARAM(lParam) / pixelRatio; + event.button = MouseEvent::Button::NONE; + + event.shiftDown = (wParam & MK_SHIFT) != 0; + event.controlDown = (wParam & MK_CONTROL) != 0; + + switch(msg) { + case WM_LBUTTONDOWN: + event.button = MouseEvent::Button::LEFT; + event.type = MouseEvent::Type::PRESS; + break; + case WM_MBUTTONDOWN: + event.button = MouseEvent::Button::MIDDLE; + event.type = MouseEvent::Type::PRESS; + break; + case WM_RBUTTONDOWN: + event.button = MouseEvent::Button::RIGHT; + event.type = MouseEvent::Type::PRESS; + break; + + case WM_LBUTTONDBLCLK: + event.button = MouseEvent::Button::LEFT; + event.type = MouseEvent::Type::DBL_PRESS; + break; + case WM_MBUTTONDBLCLK: + event.button = MouseEvent::Button::MIDDLE; + event.type = MouseEvent::Type::DBL_PRESS; + break; + case WM_RBUTTONDBLCLK: + event.button = MouseEvent::Button::RIGHT; + event.type = MouseEvent::Type::DBL_PRESS; + break; + + case WM_LBUTTONUP: + event.button = MouseEvent::Button::LEFT; + event.type = MouseEvent::Type::RELEASE; + break; + case WM_MBUTTONUP: + event.button = MouseEvent::Button::MIDDLE; + event.type = MouseEvent::Type::RELEASE; + break; + case WM_RBUTTONUP: + event.button = MouseEvent::Button::RIGHT; + event.type = MouseEvent::Type::RELEASE; + break; + + case WM_MOUSEWHEEL: + // Make the mousewheel work according to which window the mouse is + // over, not according to which window is active. + POINT pt; + pt.x = LOWORD(lParam); + pt.y = HIWORD(lParam); + HWND hWindowUnderMouse; + sscheck(hWindowUnderMouse = WindowFromPoint(pt)); + if(hWindowUnderMouse && hWindowUnderMouse != h) { + SendMessageW(hWindowUnderMouse, msg, wParam, lParam); + break; + } + + event.type = MouseEvent::Type::SCROLL_VERT; + event.scrollDelta = GET_WHEEL_DELTA_WPARAM(wParam) > 0 ? 1 : -1; + break; + + case WM_MOUSELEAVE: + event.type = MouseEvent::Type::LEAVE; + break; + case WM_MOUSEMOVE: { + event.type = MouseEvent::Type::MOTION; + + if(wParam & MK_LBUTTON) { + event.button = MouseEvent::Button::LEFT; + } else if(wParam & MK_MBUTTON) { + event.button = MouseEvent::Button::MIDDLE; + } else if(wParam & MK_RBUTTON) { + event.button = MouseEvent::Button::RIGHT; + } + + // We need this in order to get the WM_MOUSELEAVE + TRACKMOUSEEVENT tme = {}; + tme.cbSize = sizeof(tme); + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = window->hWindow; + sscheck(TrackMouseEvent(&tme)); + break; + } + } + + if(window->onMouseEvent) { + window->onMouseEvent(event); + } + break; + } + + case WM_KEYDOWN: + case WM_KEYUP: { + Platform::KeyboardEvent event = {}; + if(msg == WM_KEYDOWN) { + event.type = Platform::KeyboardEvent::Type::PRESS; + } else if(msg == WM_KEYUP) { + event.type = Platform::KeyboardEvent::Type::RELEASE; + } + + if(GetKeyState(VK_SHIFT) & 0x8000) + event.shiftDown = true; + if(GetKeyState(VK_CONTROL) & 0x8000) + event.controlDown = true; + + if(wParam >= VK_F1 && wParam <= VK_F12) { + event.key = Platform::KeyboardEvent::Key::FUNCTION; + event.num = wParam - VK_F1 + 1; + } else { + event.key = Platform::KeyboardEvent::Key::CHARACTER; + event.chr = tolower(MapVirtualKeyW(wParam, MAPVK_VK_TO_CHAR)); + if(event.chr == 0) { + if(wParam == VK_DELETE) { + event.chr = '\x7f'; + } else { + // Non-mappable key. + break; + } + } else if(event.chr == '.' && event.shiftDown) { + event.chr = '>'; + event.shiftDown = false;; + } + } + + if(window->onKeyboardEvent) { + window->onKeyboardEvent(event); + } else { + HWND hParent; + sscheck(hParent = GetParent(h)); + if(hParent != NULL) { + sscheck(SetForegroundWindow(hParent)); + sscheck(SendMessageW(hParent, msg, wParam, lParam)); + } + } + break; + } + + case WM_SYSKEYDOWN: { + HWND hParent; + sscheck(hParent = GetParent(h)); + if(hParent != NULL) { + // If the user presses the Alt key when a tool window has focus, + // then that should probably go to the main window instead. + sscheck(SetForegroundWindow(hParent)); + break; + } else { + return DefWindowProc(h, msg, wParam, lParam); + } + } + + case WM_VSCROLL: { + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_POS|SIF_TRACKPOS|SIF_RANGE|SIF_PAGE; + sscheck(GetScrollInfo(window->hWindow, SB_VERT, &si)); + + switch(LOWORD(wParam)) { + case SB_LINEUP: si.nPos -= SCROLLBAR_UNIT; break; + case SB_PAGEUP: si.nPos -= si.nPage; break; + case SB_LINEDOWN: si.nPos += SCROLLBAR_UNIT; break; + case SB_PAGEDOWN: si.nPos += si.nPage; break; + case SB_TOP: si.nPos = si.nMin; break; + case SB_BOTTOM: si.nPos = si.nMax; break; + case SB_THUMBTRACK: + case SB_THUMBPOSITION: si.nPos = si.nTrackPos; break; + } + + si.nPos = min((UINT)si.nPos, (UINT)(si.nMax - si.nPage)); + + if(window->onScrollbarAdjusted) { + window->onScrollbarAdjusted((double)si.nPos / SCROLLBAR_UNIT); + } + break; + } + + case WM_MENUCOMMAND: { + MENUITEMINFOW mii = {}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_DATA; + sscheck(GetMenuItemInfoW((HMENU)lParam, wParam, TRUE, &mii)); + + MenuItemImplWin32 *menuItem = (MenuItemImplWin32 *)mii.dwItemData; + if(menuItem->onTrigger) { + menuItem->onTrigger(); + } + break; + } + + default: + return DefWindowProc(h, msg, wParam, lParam); + } + + return 0; + } + + static LRESULT CALLBACK EditorWndProc(HWND h, UINT msg, WPARAM wParam, LPARAM lParam) { + if(handlingFatalError) return 0; + + HWND hWindow; + sscheck(hWindow = GetParent(h)); + + WindowImplWin32 *window; + sscheck(window = (WindowImplWin32 *)GetWindowLongPtr(hWindow, 0)); + + switch(msg) { + case WM_KEYDOWN: + if(wParam == VK_RETURN) { + if(window->onEditingDone) { + int length; + sscheck(length = GetWindowTextLength(h)); + + std::wstring resultW; + resultW.resize(length); + sscheck(GetWindowTextW(h, &resultW[0], resultW.length() + 1)); + + window->onEditingDone(Narrow(resultW)); + return 0; + } + } else if(wParam == VK_ESCAPE) { + sscheck(SendMessageW(hWindow, msg, wParam, lParam)); + return 0; + } + } + + return CallWindowProc(window->editorWndProc, h, msg, wParam, lParam); + } + + double GetPixelDensity() override { + UINT dpi; + sscheck(dpi = ssGetDpiForWindow(hWindow)); + return (double)dpi; + } + + int GetDevicePixelRatio() override { + UINT dpi; + sscheck(dpi = ssGetDpiForWindow(hWindow)); + return dpi / USER_DEFAULT_SCREEN_DPI; + } + + bool IsVisible() override { + BOOL isVisible; + sscheck(isVisible = IsWindowVisible(hWindow)); + return isVisible == TRUE; + } + + void SetVisible(bool visible) override { + sscheck(ShowWindow(hWindow, visible ? SW_SHOW : SW_HIDE)); + } + + void Focus() override { + sscheck(SetActiveWindow(hWindow)); + } + + bool IsFullScreen() override { + DWORD style; + sscheck(style = GetWindowLongPtr(hWindow, GWL_STYLE)); + return !(style & WS_OVERLAPPEDWINDOW); + } + + void SetFullScreen(bool fullScreen) override { + DWORD style; + sscheck(style = GetWindowLongPtr(hWindow, GWL_STYLE)); + if(fullScreen) { + sscheck(GetWindowPlacement(hWindow, &placement)); + + MONITORINFO mi; + mi.cbSize = sizeof(mi); + sscheck(GetMonitorInfo(MonitorFromWindow(hWindow, MONITOR_DEFAULTTONEAREST), &mi)); + + sscheck(SetWindowLong(hWindow, GWL_STYLE, style & ~WS_OVERLAPPEDWINDOW)); + sscheck(SetWindowPos(hWindow, HWND_TOP, + mi.rcMonitor.left, mi.rcMonitor.top, + mi.rcMonitor.right - mi.rcMonitor.left, + mi.rcMonitor.bottom - mi.rcMonitor.top, + SWP_NOOWNERZORDER|SWP_FRAMECHANGED)); + } else { + sscheck(SetWindowLong(hWindow, GWL_STYLE, style | WS_OVERLAPPEDWINDOW)); + sscheck(SetWindowPlacement(hWindow, &placement)); + sscheck(SetWindowPos(hWindow, NULL, 0, 0, 0, 0, + SWP_NOMOVE|SWP_NOSIZE|SWP_NOZORDER| + SWP_NOOWNERZORDER|SWP_FRAMECHANGED)); + } + } + + void SetTitle(const std::string &title) override { + sscheck(SetWindowTextW(hWindow, Title(title).c_str())); + } + + void SetMenuBar(MenuBarRef newMenuBar) override { + menuBar = std::static_pointer_cast(newMenuBar); + + MENUINFO mi = {}; + mi.cbSize = sizeof(mi); + mi.fMask = MIM_APPLYTOSUBMENUS|MIM_STYLE; + mi.dwStyle = MNS_NOTIFYBYPOS; + sscheck(SetMenuInfo(menuBar->hMenuBar, &mi)); + + sscheck(SetMenu(hWindow, menuBar->hMenuBar)); + } + + void GetContentSize(double *width, double *height) override { + int pixelRatio = GetDevicePixelRatio(); + + RECT rc; + sscheck(GetClientRect(hWindow, &rc)); + *width = (rc.right - rc.left) / pixelRatio; + *height = (rc.bottom - rc.top) / pixelRatio; + } + + void SetMinContentSize(double width, double height) { + minWidth = (int)width; + minHeight = (int)height; + + int pixelRatio = GetDevicePixelRatio(); + + RECT rc; + sscheck(GetClientRect(hWindow, &rc)); + if(rc.right - rc.left < minWidth * pixelRatio) { + rc.right = rc.left + minWidth * pixelRatio; + } + if(rc.bottom - rc.top < minHeight * pixelRatio) { + rc.bottom = rc.top + minHeight * pixelRatio; + } + } + + void FreezePosition(const std::string &key) override { + sscheck(GetWindowPlacement(hWindow, &placement)); + + BOOL isMaximized; + sscheck(isMaximized = IsZoomed(hWindow)); + + RECT rc = placement.rcNormalPosition; + CnfFreezeInt(rc.left, key + "_left"); + CnfFreezeInt(rc.right, key + "_right"); + CnfFreezeInt(rc.top, key + "_top"); + CnfFreezeInt(rc.bottom, key + "_bottom"); + CnfFreezeInt(isMaximized, key + "_maximized"); + } + + void ThawPosition(const std::string &key) override { + sscheck(GetWindowPlacement(hWindow, &placement)); + + RECT rc = placement.rcNormalPosition; + rc.left = CnfThawInt(rc.left, key + "_left"); + rc.right = CnfThawInt(rc.right, key + "_right"); + rc.top = CnfThawInt(rc.top, key + "_top"); + rc.bottom = CnfThawInt(rc.bottom, key + "_bottom"); + + MONITORINFO mi; + mi.cbSize = sizeof(mi); + sscheck(GetMonitorInfo(MonitorFromRect(&rc, MONITOR_DEFAULTTONEAREST), &mi)); + + // If it somehow ended up off-screen, then put it back. + RECT mrc = mi.rcMonitor; + rc.left = Clamp(rc.left, mrc.left, mrc.right); + rc.right = Clamp(rc.right, mrc.left, mrc.right); + rc.top = Clamp(rc.top, mrc.top, mrc.bottom); + rc.bottom = Clamp(rc.bottom, mrc.top, mrc.bottom); + + // And make sure the minimum size is respected. (We can freeze a size smaller + // than minimum size if the DPI changed between runs.) + sscheck(SendMessageW(hWindow, WM_SIZING, WMSZ_BOTTOMRIGHT, (LPARAM)&rc)); + + placement.flags = 0; + if(CnfThawInt(false, key + "_maximized")) { + placement.showCmd = SW_SHOWMAXIMIZED; + } else { + placement.showCmd = SW_SHOW; + } + placement.rcNormalPosition = rc; + sscheck(SetWindowPlacement(hWindow, &placement)); + } + + void SetCursor(Cursor cursor) override { + LPWSTR cursorName; + switch(cursor) { + case Cursor::POINTER: cursorName = IDC_ARROW; break; + case Cursor::HAND: cursorName = IDC_HAND; break; + } + + HCURSOR hCursor; + sscheck(hCursor = LoadCursorW(NULL, cursorName)); + sscheck(::SetCursor(hCursor)); + } + + void SetTooltip(const std::string &newText) override { + // The following SendMessage calls sometimes fail with ERROR_ACCESS_DENIED for + // no discernible reason, but only on wine. + if(newText.empty()) { + SendMessageW(hTooltip, TTM_ACTIVATE, FALSE, 0); + SendMessageW(hTooltip, TTM_POP, 0, 0); + } else if(newText != tooltipText) { + tooltipText = newText; + + std::wstring newTextW = Widen(newText); + TOOLINFOW ti = {}; + ti.cbSize = sizeof(ti); + ti.uFlags = TTF_IDISHWND; + ti.hwnd = hWindow; + ti.uId = (UINT_PTR)hWindow; + ti.lpszText = &newTextW[0]; + SendMessageW(hTooltip, TTM_UPDATETIPTEXTW, 0, (LPARAM)&ti); + + SendMessageW(hTooltip, TTM_ACTIVATE, TRUE, 0); + SendMessageW(hTooltip, TTM_POPUP, 0, 0); + } + } + + bool IsEditorVisible() override { + BOOL visible; + sscheck(visible = IsWindowVisible(hEditor)); + return visible == TRUE; + } + + void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string &text) override { + if(IsEditorVisible()) return; + + int pixelRatio = GetDevicePixelRatio(); + + HFONT hFont = CreateFontW(-(LONG)fontHeight * GetDevicePixelRatio(), 0, 0, 0, + FW_REGULAR, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, + DEFAULT_QUALITY, FF_DONTCARE, isMonospace ? L"Lucida Console" : L"Arial"); + if(hFont == NULL) { + sscheck(hFont = (HFONT)GetStockObject(SYSTEM_FONT)); + } + sscheck(SendMessageW(hEditor, WM_SETFONT, (WPARAM)hFont, FALSE)); + sscheck(SendMessageW(hEditor, EM_SETMARGINS, EC_LEFTMARGIN|EC_RIGHTMARGIN, 0)); + + std::wstring textW = Widen(text); + + HDC hDc; + TEXTMETRICW tm; + SIZE ts; + sscheck(hDc = GetDC(hEditor)); + sscheck(SelectObject(hDc, hFont)); + sscheck(GetTextMetricsW(hDc, &tm)); + sscheck(GetTextExtentPoint32W(hDc, textW.c_str(), textW.length(), &ts)); + sscheck(ReleaseDC(hEditor, hDc)); + + RECT rc; + rc.left = (LONG)x * pixelRatio; + rc.top = (LONG)y * pixelRatio - tm.tmAscent; + // Add one extra char width to avoid scrolling. + rc.right = (LONG)x * pixelRatio + + std::max((LONG)minWidth * pixelRatio, ts.cx + tm.tmAveCharWidth); + rc.bottom = (LONG)y * pixelRatio + tm.tmDescent; + sscheck(ssAdjustWindowRectExForDpi(&rc, 0, /*bMenu=*/FALSE, WS_EX_CLIENTEDGE, + ssGetDpiForWindow(hWindow))); + + sscheck(MoveWindow(hEditor, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, + /*bRepaint=*/true)); + sscheck(ShowWindow(hEditor, SW_SHOW)); + if(!textW.empty()) { + sscheck(SendMessageW(hEditor, WM_SETTEXT, 0, (LPARAM)textW.c_str())); + sscheck(SendMessageW(hEditor, EM_SETSEL, 0, textW.length())); + sscheck(SetFocus(hEditor)); + } + } + + void HideEditor() override { + if(!IsEditorVisible()) return; + + sscheck(ShowWindow(hEditor, SW_HIDE)); + } + + void SetScrollbarVisible(bool visible) override { + scrollbarVisible = visible; + sscheck(ShowScrollBar(hWindow, SB_VERT, visible)); + } + + void ConfigureScrollbar(double min, double max, double pageSize) override { + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_RANGE|SIF_PAGE; + si.nMin = (UINT)(min * SCROLLBAR_UNIT); + si.nMax = (UINT)(max * SCROLLBAR_UNIT); + si.nPage = (UINT)(pageSize * SCROLLBAR_UNIT); + sscheck(SetScrollInfo(hWindow, SB_VERT, &si, /*redraw=*/TRUE)); + } + + double GetScrollbarPosition() override { + if(!scrollbarVisible) return 0.0; + + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_POS; + sscheck(GetScrollInfo(hWindow, SB_VERT, &si)); + return (double)si.nPos / SCROLLBAR_UNIT; + } + + void SetScrollbarPosition(double pos) override { + if(!scrollbarVisible) return; + + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_POS; + si.nPos = (UINT)(pos * SCROLLBAR_UNIT); + sscheck(SetScrollInfo(hWindow, SB_VERT, &si, /*redraw=*/TRUE)); + + // Windows won't synthesize a WM_VSCROLL for us here. + if(onScrollbarAdjusted) { + onScrollbarAdjusted((double)si.nPos / SCROLLBAR_UNIT); + } + } + + void Invalidate() override { + sscheck(InvalidateRect(hWindow, NULL, /*bErase=*/FALSE)); + } + + void Redraw() override { + Invalidate(); + sscheck(UpdateWindow(hWindow)); + } + + void *NativePtr() override { + return hWindow; + } +}; + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + return std::make_shared(kind, + std::static_pointer_cast(parentWindow)); +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +void Exit() { + PostQuitMessage(0); +} + } } diff --git a/src/platform/w32main.cpp b/src/platform/w32main.cpp index d18f4b0d..da7058a7 100644 --- a/src/platform/w32main.cpp +++ b/src/platform/w32main.cpp @@ -16,53 +16,15 @@ #include #include -#ifdef MenuHelp -// This is defined to IsolationAwareMenuHelp on Windows 6.0 and later. -#undef MenuHelp -#endif - #ifdef HAVE_SPACEWARE # include # include # undef uint32_t // thanks but no thanks #endif -#if HAVE_OPENGL == 3 -#define EGLAPI /*static linkage*/ -#include -#endif - using Platform::Narrow; using Platform::Widen; -HINSTANCE Instance; - -HWND TextWnd; -HWND TextWndScrollBar; -HWND TextEditControl; -#if HAVE_OPENGL == 3 -EGLDisplay TextGlDisplay; -EGLSurface TextGlSurface; -EGLContext TextGlContext; -#else -HGLRC TextGl; -#endif - -HWND GraphicsWnd; -HWND GraphicsEditControl; -#if HAVE_OPENGL == 3 -EGLDisplay GraphicsGlDisplay; -EGLSurface GraphicsGlSurface; -EGLContext GraphicsGlContext; -#else -HGLRC GraphicsGl; -#endif -static struct { - int x, y; -} LastMousePos; - -int ClientIsSmallerBy; - HFONT FixedFont; #ifdef HAVE_SPACEWARE @@ -143,42 +105,42 @@ HWND CreateWindowClient(DWORD exStyle, const wchar_t *className, const wchar_t * void SolveSpace::DoMessageBox(const char *str, int rows, int cols, bool error) { - EnableWindow(GraphicsWnd, false); - EnableWindow(TextWnd, false); + EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); + EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); // Register the window class for our dialog. WNDCLASSEX wc = {}; wc.cbSize = sizeof(wc); wc.style = CS_BYTEALIGNCLIENT | CS_BYTEALIGNWINDOW | CS_OWNDC; wc.lpfnWndProc = (WNDPROC)MessageProc; - wc.hInstance = Instance; + wc.hInstance = NULL; wc.hbrBackground = (HBRUSH)COLOR_BTNSHADOW; wc.lpszClassName = L"MessageWnd"; wc.lpszMenuName = NULL; wc.hCursor = LoadCursor(NULL, IDC_ARROW); - wc.hIcon = (HICON)LoadImage(Instance, MAKEINTRESOURCE(4000), + wc.hIcon = (HICON)LoadImage(NULL, MAKEINTRESOURCE(4000), IMAGE_ICON, 32, 32, 0); - wc.hIconSm = (HICON)LoadImage(Instance, MAKEINTRESOURCE(4000), + wc.hIconSm = (HICON)LoadImage(NULL, MAKEINTRESOURCE(4000), IMAGE_ICON, 16, 16, 0); RegisterClassEx(&wc); // Create the window. MessageString = str; RECT r; - GetWindowRect(GraphicsWnd, &r); + GetWindowRect((HWND)SS.GW.window->NativePtr(), &r); int width = cols*SS.TW.CHAR_WIDTH_ + 20, height = rows*SS.TW.LINE_HEIGHT + 60; MessageWidth = width; MessageHeight = height; MessageWnd = CreateWindowClient(0, L"MessageWnd", - (error ? Title(C_("title", "Error")) : Title(C_("title", "Message"))).c_str(), + Title(error ? C_("title", "Error") : C_("title", "Message")).c_str(), WS_OVERLAPPED | WS_SYSMENU, - r.left + 100, r.top + 100, width, height, NULL, NULL, Instance, NULL); + r.left + 100, r.top + 100, width, height, NULL, NULL, NULL, NULL); OkButton = CreateWindowExW(0, WC_BUTTON, Widen(C_("button", "OK")).c_str(), WS_CHILD | WS_TABSTOP | WS_CLIPSIBLINGS | WS_VISIBLE | BS_DEFPUSHBUTTON, (width - 70)/2, rows*SS.TW.LINE_HEIGHT + 20, - 70, 25, MessageWnd, NULL, Instance, NULL); + 70, 25, MessageWnd, NULL, NULL, NULL); SendMessage(OkButton, WM_SETFONT, (WPARAM)FixedFont, true); ShowWindow(MessageWnd, true); @@ -203,53 +165,22 @@ void SolveSpace::DoMessageBox(const char *str, int rows, int cols, bool error) } MessageString = NULL; - EnableWindow(TextWnd, true); - EnableWindow(GraphicsWnd, true); - SetForegroundWindow(GraphicsWnd); DestroyWindow(MessageWnd); -} -static void GetWindowSize(HWND hwnd, int *w, int *h) -{ - RECT r; - GetClientRect(hwnd, &r); - *w = r.right - r.left; - *h = r.bottom - r.top; -} -void SolveSpace::GetGraphicsWindowSize(int *w, int *h) -{ - GetWindowSize(GraphicsWnd, w, h); -} -void SolveSpace::GetTextWindowSize(int *w, int *h) -{ - GetWindowSize(TextWnd, w, h); -} - -double SolveSpace::GetScreenDpi() { - HDC hdc = GetDC(NULL); - double dpi = GetDeviceCaps(hdc, LOGPIXELSX); - ReleaseDC(NULL, hdc); - return dpi; + EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); + EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); + SetForegroundWindow((HWND)SS.GW.window->NativePtr()); } void SolveSpace::OpenWebsite(const char *url) { - ShellExecuteW(GraphicsWnd, L"open", Widen(url).c_str(), NULL, NULL, SW_SHOWNORMAL); -} - -void SolveSpace::ExitNow() { - PostQuitMessage(0); + ShellExecuteW((HWND)SS.GW.window->NativePtr(), + L"open", Widen(url).c_str(), NULL, NULL, SW_SHOWNORMAL); } //----------------------------------------------------------------------------- // Helpers so that we can read/write registry keys from the platform- // independent code. //----------------------------------------------------------------------------- -inline int CLAMP(int v, int a, int b) { - // Clamp it to the range [a, b] - if(v <= a) return a; - if(v >= b) return b; - return v; -} static HKEY GetRegistryKey() { @@ -292,17 +223,6 @@ void SolveSpace::CnfFreezeString(const std::string &str, const std::string &name REG_SZ, (const BYTE*) &strW[0], (strW.length() + 1) * 2); RegCloseKey(SolveSpace); } -static void FreezeWindowPos(HWND hwnd, const std::string &name) -{ - RECT r; - GetWindowRect(hwnd, &r); - CnfFreezeInt(r.left, name + "_left"); - CnfFreezeInt(r.right, name + "_right"); - CnfFreezeInt(r.top, name + "_top"); - CnfFreezeInt(r.bottom, name + "_bottom"); - - CnfFreezeInt(IsZoomed(hwnd), name + "_maximized"); -} uint32_t SolveSpace::CnfThawInt(uint32_t val, const std::string &name) { @@ -352,613 +272,6 @@ std::string SolveSpace::CnfThawString(const std::string &val, const std::string RegCloseKey(SolveSpace); return Narrow(newval); } -static void ThawWindowPos(HWND hwnd, const std::string &name) -{ - RECT r; - GetWindowRect(hwnd, &r); - r.left = CnfThawInt(r.left, name + "_left"); - r.right = CnfThawInt(r.right, name + "_right"); - r.top = CnfThawInt(r.top, name + "_top"); - r.bottom = CnfThawInt(r.bottom, name + "_bottom"); - - HMONITOR hMonitor = MonitorFromRect(&r, MONITOR_DEFAULTTONEAREST);; - MONITORINFO mi; - mi.cbSize = sizeof(mi); - GetMonitorInfo(hMonitor, &mi); - - // If it somehow ended up off-screen, then put it back. - RECT dr = mi.rcMonitor; - r.left = CLAMP(r.left, dr.left, dr.right); - r.right = CLAMP(r.right, dr.left, dr.right); - r.top = CLAMP(r.top, dr.top, dr.bottom); - r.bottom = CLAMP(r.bottom, dr.top, dr.bottom); - MoveWindow(hwnd, r.left, r.top, r.right - r.left, r.bottom - r.top, TRUE); - - if(CnfThawInt(FALSE, name + "_maximized")) - ShowWindow(hwnd, SW_MAXIMIZE); -} - -void SolveSpace::SetCurrentFilename(const Platform::Path &filename) { - SetWindowTextW(GraphicsWnd, - Title(filename.IsEmpty() ? C_("title", "(new sketch)") : filename.raw).c_str()); -} - -void SolveSpace::SetMousePointerToHand(bool yes) { - SetCursor(LoadCursor(NULL, yes ? IDC_HAND : IDC_ARROW)); -} - -static void PaintTextWnd() -{ -#if HAVE_OPENGL == 3 - eglMakeCurrent(TextGlDisplay, TextGlSurface, TextGlSurface, TextGlContext); - - SS.TW.Paint(); - eglSwapBuffers(TextGlDisplay, TextGlSurface); - - // Leave the graphics window context active, except when we're painting - // this text window. - eglMakeCurrent(GraphicsGlDisplay, GraphicsGlSurface, GraphicsGlSurface, GraphicsGlContext); -#else - wglMakeCurrent(GetDC(TextWnd), TextGl); - - SS.TW.Paint(); - SwapBuffers(GetDC(TextWnd)); - - // Leave the graphics window context active, except when we're painting - // this text window. - wglMakeCurrent(GetDC(GraphicsWnd), GraphicsGl); -#endif -} - -void SolveSpace::MoveTextScrollbarTo(int pos, int maxPos, int page) -{ - SCROLLINFO si = {}; - si.cbSize = sizeof(si); - si.fMask = SIF_DISABLENOSCROLL | SIF_ALL; - si.nMin = 0; - si.nMax = maxPos; - si.nPos = pos; - si.nPage = page; - SetScrollInfo(TextWndScrollBar, SB_CTL, &si, true); -} - -void HandleTextWindowScrollBar(WPARAM wParam, LPARAM lParam) -{ - int maxPos, minPos, pos; - GetScrollRange(TextWndScrollBar, SB_CTL, &minPos, &maxPos); - pos = GetScrollPos(TextWndScrollBar, SB_CTL); - - switch(LOWORD(wParam)) { - case SB_LINEUP: pos--; break; - case SB_PAGEUP: pos -= 4; break; - - case SB_LINEDOWN: pos++; break; - case SB_PAGEDOWN: pos += 4; break; - - case SB_TOP: pos = 0; break; - - case SB_BOTTOM: pos = maxPos; break; - - case SB_THUMBTRACK: - case SB_THUMBPOSITION: pos = HIWORD(wParam); break; - } - - SS.TW.ScrollbarEvent(pos); -} - -static void MouseWheel(int thisDelta) { - static int DeltaAccum; - int delta = 0; - // Handle mouse deltas of less than 120 (like from an un-detented mouse - // wheel) correctly, even though no one ever uses those. - DeltaAccum += thisDelta; - while(DeltaAccum >= 120) { - DeltaAccum -= 120; - delta += 120; - } - while(DeltaAccum <= -120) { - DeltaAccum += 120; - delta -= 120; - } - if(delta == 0) return; - - POINT pt; - GetCursorPos(&pt); - HWND hw = WindowFromPoint(pt); - - // Make the mousewheel work according to which window the mouse is - // over, not according to which window is active. - bool inTextWindow; - if(hw == TextWnd) { - inTextWindow = true; - } else if(hw == GraphicsWnd) { - inTextWindow = false; - } else if(GetForegroundWindow() == TextWnd) { - inTextWindow = true; - } else { - inTextWindow = false; - } - - if(inTextWindow) { - int i; - for(i = 0; i < abs(delta/40); i++) { - HandleTextWindowScrollBar(delta > 0 ? SB_LINEUP : SB_LINEDOWN, 0); - } - } else { - SS.GW.MouseScroll(LastMousePos.x, LastMousePos.y, delta); - } -} - -LRESULT CALLBACK TextWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) -{ - if(Platform::handlingFatalError) return 1; - - switch (msg) { - case WM_ERASEBKGND: - break; - - case WM_CLOSE: - case WM_DESTROY: - SolveSpaceUI::MenuFile(Command::EXIT); - break; - - case WM_PAINT: { - // Actually paint the text window, with gl. - PaintTextWnd(); - // And then just make Windows happy. - PAINTSTRUCT ps; - HDC hdc = BeginPaint(hwnd, &ps); - EndPaint(hwnd, &ps); - break; - } - - case WM_SIZING: { - RECT *r = (RECT *)lParam; - int hc = (r->bottom - r->top) - ClientIsSmallerBy; - int extra = hc % (SS.TW.LINE_HEIGHT/2); - switch(wParam) { - case WMSZ_BOTTOM: - case WMSZ_BOTTOMLEFT: - case WMSZ_BOTTOMRIGHT: - r->bottom -= extra; - break; - - case WMSZ_TOP: - case WMSZ_TOPLEFT: - case WMSZ_TOPRIGHT: - r->top += extra; - break; - } - int tooNarrow = (SS.TW.MIN_COLS*SS.TW.CHAR_WIDTH_) - - (r->right - r->left); - if(tooNarrow >= 0) { - switch(wParam) { - case WMSZ_RIGHT: - case WMSZ_BOTTOMRIGHT: - case WMSZ_TOPRIGHT: - r->right += tooNarrow; - break; - - case WMSZ_LEFT: - case WMSZ_BOTTOMLEFT: - case WMSZ_TOPLEFT: - r->left -= tooNarrow; - break; - } - } - break; - } - - case WM_MOUSELEAVE: - SS.TW.MouseLeave(); - break; - - case WM_LBUTTONDOWN: - case WM_MOUSEMOVE: { - // We need this in order to get the WM_MOUSELEAVE - TRACKMOUSEEVENT tme = {}; - tme.cbSize = sizeof(tme); - tme.dwFlags = TME_LEAVE; - tme.hwndTrack = TextWnd; - TrackMouseEvent(&tme); - - // And process the actual message - int x = LOWORD(lParam); - int y = HIWORD(lParam); - SS.TW.MouseEvent(msg == WM_LBUTTONDOWN, wParam & MK_LBUTTON, x, y); - break; - } - - case WM_SIZE: { - RECT r; - GetWindowRect(TextWndScrollBar, &r); - int sw = r.right - r.left; - GetClientRect(hwnd, &r); - MoveWindow(TextWndScrollBar, r.right - sw, r.top, sw, - (r.bottom - r.top), true); - // If the window is growing, then the scrollbar position may - // be moving, so it's as if we're dragging the scrollbar. - HandleTextWindowScrollBar((WPARAM)-1, -1); - InvalidateRect(TextWnd, NULL, false); - break; - } - - case WM_MOUSEWHEEL: - MouseWheel(GET_WHEEL_DELTA_WPARAM(wParam)); - break; - - case WM_VSCROLL: - HandleTextWindowScrollBar(wParam, lParam); - break; - - default: - return DefWindowProc(hwnd, msg, wParam, lParam); - } - - return 1; -} - -static std::string EditControlText(HWND hwnd) -{ - std::wstring result; - result.resize(GetWindowTextLength(hwnd)); - GetWindowTextW(hwnd, &result[0], result.length() + 1); - return Narrow(result); -} - -static bool ProcessKeyDown(WPARAM wParam) -{ - if(GraphicsEditControlIsVisible() && wParam != VK_ESCAPE) { - if(wParam == VK_RETURN) { - SS.GW.EditControlDone(EditControlText(GraphicsEditControl).c_str()); - return true; - } else { - return false; - } - } - if(TextEditControlIsVisible() && wParam != VK_ESCAPE) { - if(wParam == VK_RETURN) { - SS.TW.EditControlDone(EditControlText(TextEditControl).c_str()); - } else { - return false; - } - } - - Platform::KeyboardEvent event = {}; - event.type = Platform::KeyboardEvent::Type::PRESS; - - if(GetAsyncKeyState(VK_SHIFT) & 0x8000) - event.shiftDown = true; - if(GetAsyncKeyState(VK_CONTROL) & 0x8000) - event.controlDown = true; - - if(wParam >= VK_F1 && wParam <= VK_F12) { - event.key = Platform::KeyboardEvent::Key::FUNCTION; - event.num = wParam - VK_F1 + 1; - } else { - event.key = Platform::KeyboardEvent::Key::CHARACTER; - event.chr = tolower(MapVirtualKeyW(wParam, MAPVK_VK_TO_CHAR)); - if(event.chr == 0) { - if(wParam == VK_DELETE) { - event.chr = '\x7f'; - } else { - // Non-mappable key. - return false; - } - } else if(event.chr == '.' && event.shiftDown) { - event.chr = '>'; - event.shiftDown = false;; - } - } - - if(SS.GW.KeyboardEvent(event)) return true; - - return false; -} - -void SolveSpace::ShowTextWindow(bool visible) -{ - ShowWindow(TextWnd, visible ? SW_SHOWNOACTIVATE : SW_HIDE); -} - -#if HAVE_OPENGL == 3 -static void CreateGlContext(HWND hwnd, EGLDisplay *eglDisplay, EGLSurface *eglSurface, - EGLContext *eglContext) { - ssassert(eglBindAPI(EGL_OPENGL_ES_API), "Cannot bind EGL API"); - - *eglDisplay = eglGetDisplay(GetDC(hwnd)); - ssassert(eglInitialize(*eglDisplay, NULL, NULL), "Cannot initialize EGL"); - - EGLint configAttributes[] = { - EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_DEPTH_SIZE, 24, - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL_SURFACE_TYPE, EGL_WINDOW_BIT, - EGL_NONE - }; - EGLint numConfigs; - EGLConfig windowConfig; - ssassert(eglChooseConfig(*eglDisplay, configAttributes, &windowConfig, 1, &numConfigs), - "Cannot choose EGL configuration"); - - EGLint surfaceAttributes[] = { - EGL_NONE - }; - *eglSurface = eglCreateWindowSurface(*eglDisplay, windowConfig, hwnd, surfaceAttributes); - ssassert(eglSurface != EGL_NO_SURFACE, "Cannot create EGL window surface"); - - EGLint contextAttributes[] = { - EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_NONE - }; - *eglContext = eglCreateContext(*eglDisplay, windowConfig, NULL, contextAttributes); - ssassert(eglContext != EGL_NO_CONTEXT, "Cannot create EGL context"); - - eglMakeCurrent(*eglDisplay, *eglSurface, *eglSurface, *eglContext); -} -#else -static void CreateGlContext(HWND hwnd, HGLRC *glrc) -{ - HDC hdc = GetDC(hwnd); - - PIXELFORMATDESCRIPTOR pfd = {}; - int pixelFormat; - - pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR); - pfd.nVersion = 1; - pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | - PFD_DOUBLEBUFFER; - pfd.dwLayerMask = PFD_MAIN_PLANE; - pfd.iPixelType = PFD_TYPE_RGBA; - pfd.cColorBits = 32; - pfd.cDepthBits = 24; - pfd.cAccumBits = 0; - pfd.cStencilBits = 0; - - pixelFormat = ChoosePixelFormat(hdc, &pfd); - ssassert(pixelFormat != 0, "Expected a valid pixel format to be chosen"); - - ssassert(SetPixelFormat(hdc, pixelFormat, &pfd), "Cannot set pixel format"); - - *glrc = wglCreateContext(hdc); - wglMakeCurrent(hdc, *glrc); -} -#endif - -void SolveSpace::PaintGraphics() -{ - SS.GW.Paint(); -#if HAVE_OPENGL == 3 - eglSwapBuffers(GraphicsGlDisplay, GraphicsGlSurface); -#else - SwapBuffers(GetDC(GraphicsWnd)); -#endif -} -void SolveSpace::InvalidateGraphics() -{ - InvalidateRect(GraphicsWnd, NULL, false); -} - -void SolveSpace::ToggleFullScreen() -{ - static WINDOWPLACEMENT wp; - wp.length = sizeof(wp); - - DWORD dwStyle = GetWindowLong(GraphicsWnd, GWL_STYLE); - if(dwStyle & WS_OVERLAPPEDWINDOW) { - MONITORINFO mi; - mi.cbSize = sizeof(mi); - - if(GetWindowPlacement(GraphicsWnd, &wp) && - GetMonitorInfo(MonitorFromWindow(GraphicsWnd, MONITOR_DEFAULTTOPRIMARY), &mi)) { - SetWindowLong(GraphicsWnd, GWL_STYLE, dwStyle & ~WS_OVERLAPPEDWINDOW); - SetWindowPos(GraphicsWnd, HWND_TOP, - mi.rcMonitor.left, mi.rcMonitor.top, - mi.rcMonitor.right - mi.rcMonitor.left, - mi.rcMonitor.bottom - mi.rcMonitor.top, - SWP_NOOWNERZORDER | SWP_FRAMECHANGED); - } - } else { - SetWindowLong(GraphicsWnd, GWL_STYLE, dwStyle | WS_OVERLAPPEDWINDOW); - SetWindowPlacement(GraphicsWnd, &wp); - SetWindowPos(GraphicsWnd, NULL, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | - SWP_NOOWNERZORDER | SWP_FRAMECHANGED); - } -} -bool SolveSpace::FullScreenIsActive() -{ - return (GetWindowLong(GraphicsWnd, GWL_STYLE) & WS_OVERLAPPEDWINDOW) != 0; -} - -void SolveSpace::InvalidateText() -{ - InvalidateRect(TextWnd, NULL, false); -} - -static void ShowEditControl(HWND h, int x, int y, int fontHeight, int minWidthChars, - bool isMonospace, const std::wstring &s) { - static HFONT hf; - if(hf) DeleteObject(hf); - hf = CreateFontW(-fontHeight, 0, 0, 0, - FW_REGULAR, false, false, false, ANSI_CHARSET, - OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, - DEFAULT_QUALITY, FF_DONTCARE, isMonospace ? L"Lucida Console" : L"Arial"); - if(hf) SendMessage(h, WM_SETFONT, (WPARAM)hf, false); - else SendMessage(h, WM_SETFONT, (WPARAM)(HFONT)GetStockObject(SYSTEM_FONT), false); - SendMessage(h, EM_SETMARGINS, EC_LEFTMARGIN|EC_RIGHTMARGIN, 0); - - HDC hdc = GetDC(h); - TEXTMETRICW tm; - SIZE ts; - SelectObject(hdc, hf); - GetTextMetrics(hdc, &tm); - GetTextExtentPoint32W(hdc, s.c_str(), s.length(), &ts); - ReleaseDC(h, hdc); - - RECT rc; - rc.left = x; - rc.top = y - tm.tmAscent; - // Add one extra char width to avoid scrolling. - rc.right = x + std::max(tm.tmAveCharWidth * minWidthChars, - ts.cx + tm.tmAveCharWidth); - rc.bottom = y + tm.tmDescent; - - AdjustWindowRectEx(&rc, 0, false, WS_EX_CLIENTEDGE); - MoveWindow(h, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, true); - ShowWindow(h, SW_SHOW); - if(!s.empty()) { - SendMessage(h, WM_SETTEXT, 0, (LPARAM)s.c_str()); - SendMessage(h, EM_SETSEL, 0, s.length()); - SetFocus(h); - } -} -void SolveSpace::ShowTextEditControl(int x, int y, const std::string &str) -{ - if(GraphicsEditControlIsVisible()) return; - - ShowEditControl(TextEditControl, x, y, TextWindow::CHAR_HEIGHT, 30, - /*isMonospace=*/true, Widen(str)); -} -void SolveSpace::HideTextEditControl() -{ - ShowWindow(TextEditControl, SW_HIDE); -} -bool SolveSpace::TextEditControlIsVisible() -{ - return IsWindowVisible(TextEditControl) ? true : false; -} -void SolveSpace::ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, - const std::string &str) -{ - if(GraphicsEditControlIsVisible()) return; - - RECT r; - GetClientRect(GraphicsWnd, &r); - x = x + (r.right - r.left)/2; - y = (r.bottom - r.top)/2 - y; - - ShowEditControl(GraphicsEditControl, x, y, fontHeight, minWidthChars, - /*isMonospace=*/false, Widen(str)); -} -void SolveSpace::HideGraphicsEditControl() -{ - ShowWindow(GraphicsEditControl, SW_HIDE); -} -bool SolveSpace::GraphicsEditControlIsVisible() -{ - return IsWindowVisible(GraphicsEditControl) ? true : false; -} - -namespace SolveSpace { -namespace Platform { -void TriggerMenu(int id); -extern int64_t contextMenuCancelTime; -} -} - -LRESULT CALLBACK GraphicsWndProc(HWND hwnd, UINT msg, WPARAM wParam, - LPARAM lParam) -{ - if(Platform::handlingFatalError) return 1; - - switch (msg) { - case WM_ERASEBKGND: - break; - - case WM_SIZE: - InvalidateRect(GraphicsWnd, NULL, false); - break; - - case WM_PAINT: { - // Actually paint the window, with gl. - PaintGraphics(); - // And make Windows happy. - PAINTSTRUCT ps; - HDC hdc = BeginPaint(hwnd, &ps); - EndPaint(hwnd, &ps); - break; - } - - case WM_MOUSELEAVE: - SS.GW.MouseLeave(); - break; - - case WM_MOUSEMOVE: - case WM_LBUTTONDOWN: - case WM_LBUTTONUP: - case WM_LBUTTONDBLCLK: - case WM_RBUTTONDOWN: - case WM_RBUTTONUP: - case WM_MBUTTONDOWN: { - if(GetMilliseconds() - Platform::contextMenuCancelTime < 100) { - // Ignore the mouse click that dismisses a context menu, to avoid - // (e.g.) clearing a selection. - return 1; - } - - int x = LOWORD(lParam); - int y = HIWORD(lParam); - - // We need this in order to get the WM_MOUSELEAVE - TRACKMOUSEEVENT tme = {}; - tme.cbSize = sizeof(tme); - tme.dwFlags = TME_LEAVE; - tme.hwndTrack = GraphicsWnd; - TrackMouseEvent(&tme); - - // Convert to xy (vs. ij) style coordinates, with (0, 0) at center - RECT r; - GetClientRect(GraphicsWnd, &r); - x = x - (r.right - r.left)/2; - y = (r.bottom - r.top)/2 - y; - - LastMousePos.x = x; - LastMousePos.y = y; - - if(msg == WM_LBUTTONDOWN) { - SS.GW.MouseLeftDown(x, y); - } else if(msg == WM_LBUTTONUP) { - SS.GW.MouseLeftUp(x, y); - } else if(msg == WM_LBUTTONDBLCLK) { - SS.GW.MouseLeftDoubleClick(x, y); - } else if(msg == WM_MBUTTONDOWN || msg == WM_RBUTTONDOWN) { - SS.GW.MouseMiddleOrRightDown(x, y); - } else if(msg == WM_RBUTTONUP) { - SS.GW.MouseRightUp(x, y); - } else if(msg == WM_MOUSEMOVE) { - SS.GW.MouseMoved(x, y, - !!(wParam & MK_LBUTTON), - !!(wParam & MK_MBUTTON), - !!(wParam & MK_RBUTTON), - !!(wParam & MK_SHIFT), - !!(wParam & MK_CONTROL)); - } - break; - } - case WM_MOUSEWHEEL: - MouseWheel(GET_WHEEL_DELTA_WPARAM(wParam)); - break; - - case WM_MENUCOMMAND: { - SolveSpace::Platform::TriggerMenu(GetMenuItemID((HMENU)lParam, wParam)); - break; - } - - case WM_CLOSE: - case WM_DESTROY: - SolveSpaceUI::MenuFile(Command::EXIT); - return 1; - - default: - return DefWindowProc(hwnd, msg, wParam, lParam); - } - - return 1; -} //----------------------------------------------------------------------------- // Common dialog routines, to open or save a file. @@ -1011,16 +324,16 @@ static bool OpenSaveFile(bool isOpen, Platform::Path *filename, const std::strin OPENFILENAME ofn = {}; ofn.lStructSize = sizeof(ofn); - ofn.hInstance = Instance; - ofn.hwndOwner = GraphicsWnd; + ofn.hInstance = NULL; + ofn.hwndOwner = (HWND)SS.GW.window->NativePtr(); ofn.lpstrFilter = selPatternW.c_str(); ofn.lpstrDefExt = defExtensionW.c_str(); ofn.lpstrFile = filenameC; ofn.nMaxFile = len; ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; - EnableWindow(GraphicsWnd, false); - EnableWindow(TextWnd, false); + EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); + EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); BOOL r; if(isOpen) { @@ -1029,9 +342,9 @@ static bool OpenSaveFile(bool isOpen, Platform::Path *filename, const std::strin r = GetSaveFileNameW(&ofn); } - EnableWindow(TextWnd, true); - EnableWindow(GraphicsWnd, true); - SetForegroundWindow(GraphicsWnd); + EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); + EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); + SetForegroundWindow((HWND)SS.GW.window->NativePtr()); if(r) *filename = Platform::Path::From(Narrow(filenameC)); return r ? true : false; @@ -1051,18 +364,18 @@ bool SolveSpace::GetSaveFile(Platform::Path *filename, const std::string &defExt DialogChoice SolveSpace::SaveFileYesNoCancel() { - EnableWindow(GraphicsWnd, false); - EnableWindow(TextWnd, false); + EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); + EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); - int r = MessageBoxW(GraphicsWnd, + int r = MessageBoxW((HWND)SS.GW.window->NativePtr(), Widen(_("The file has changed since it was last saved.\n\n" "Do you want to save the changes?")).c_str(), Title(C_("title", "Modified File")).c_str(), MB_YESNOCANCEL | MB_ICONWARNING); - EnableWindow(TextWnd, true); - EnableWindow(GraphicsWnd, true); - SetForegroundWindow(GraphicsWnd); + EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); + EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); + SetForegroundWindow((HWND)SS.GW.window->NativePtr()); switch(r) { case IDYES: @@ -1077,18 +390,18 @@ DialogChoice SolveSpace::SaveFileYesNoCancel() DialogChoice SolveSpace::LoadAutosaveYesNo() { - EnableWindow(GraphicsWnd, false); - EnableWindow(TextWnd, false); + EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); + EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); - int r = MessageBoxW(GraphicsWnd, + int r = MessageBoxW((HWND)SS.GW.window->NativePtr(), Widen(_("An autosave file is available for this project.\n\n" "Do you want to load the autosave file instead?")).c_str(), Title(C_("title", "Autosave Available")).c_str(), MB_YESNO | MB_ICONWARNING); - EnableWindow(TextWnd, true); - EnableWindow(GraphicsWnd, true); - SetForegroundWindow(GraphicsWnd); + EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); + EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); + SetForegroundWindow((HWND)SS.GW.window->NativePtr()); switch (r) { case IDYES: @@ -1101,8 +414,8 @@ DialogChoice SolveSpace::LoadAutosaveYesNo() DialogChoice SolveSpace::LocateImportedFileYesNoCancel(const Platform::Path &filename, bool canCancel) { - EnableWindow(GraphicsWnd, false); - EnableWindow(TextWnd, false); + EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); + EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); std::string message = "The linked file " + filename.raw + " is not present.\n\n" @@ -1110,13 +423,14 @@ DialogChoice SolveSpace::LocateImportedFileYesNoCancel(const Platform::Path &fil "If you select \"No\", any geometry that depends on " "the missing file will be removed."; - int r = MessageBoxW(GraphicsWnd, Widen(message).c_str(), + int r = MessageBoxW((HWND)SS.GW.window->NativePtr(), + Widen(message).c_str(), Title(C_("title", "Missing File")).c_str(), (canCancel ? MB_YESNOCANCEL : MB_YESNO) | MB_ICONWARNING); - EnableWindow(TextWnd, true); - EnableWindow(GraphicsWnd, true); - SetForegroundWindow(GraphicsWnd); + EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); + EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); + SetForegroundWindow((HWND)SS.GW.window->NativePtr()); switch(r) { case IDYES: @@ -1147,88 +461,6 @@ std::vector SolveSpace::GetFontFiles() { return fonts; } -static void CreateMainWindows() -{ - WNDCLASSEX wc = {}; - - wc.cbSize = sizeof(wc); - - // The graphics window, where the sketch is drawn and shown. - wc.style = CS_BYTEALIGNCLIENT | CS_BYTEALIGNWINDOW | CS_OWNDC | - CS_DBLCLKS; - wc.lpfnWndProc = (WNDPROC)GraphicsWndProc; - wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); - wc.lpszClassName = L"GraphicsWnd"; - wc.lpszMenuName = NULL; - wc.hCursor = LoadCursor(NULL, IDC_ARROW); - wc.hIcon = (HICON)LoadImage(Instance, MAKEINTRESOURCE(4000), - IMAGE_ICON, 32, 32, 0); - wc.hIconSm = (HICON)LoadImage(Instance, MAKEINTRESOURCE(4000), - IMAGE_ICON, 16, 16, 0); - ssassert(RegisterClassEx(&wc), "Cannot register window class"); - - GraphicsWnd = CreateWindowExW(0, L"GraphicsWnd", - Title(C_("title", "(new sketch)")).c_str(), - WS_OVERLAPPED | WS_THICKFRAME | WS_CLIPCHILDREN | WS_MAXIMIZEBOX | - WS_MINIMIZEBOX | WS_SYSMENU | WS_SIZEBOX | WS_CLIPSIBLINGS, - 50, 50, 900, 600, NULL, NULL, Instance, NULL); - ssassert(GraphicsWnd != NULL, "Cannot create window"); - - GraphicsEditControl = CreateWindowExW(WS_EX_CLIENTEDGE, WC_EDIT, L"", - WS_CHILD | ES_AUTOHSCROLL | WS_TABSTOP | WS_CLIPSIBLINGS, - 50, 50, 100, 21, GraphicsWnd, NULL, Instance, NULL); - - // The text window, with a command line and some textual information - // about the sketch. - wc.style &= ~CS_DBLCLKS; - wc.lpfnWndProc = (WNDPROC)TextWndProc; - wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); - wc.lpszClassName = L"TextWnd"; - wc.hCursor = NULL; - ssassert(RegisterClassEx(&wc), "Cannot register window class"); - - // We get the desired Alt+Tab behaviour by specifying that the text - // window is a child of the graphics window. - TextWnd = CreateWindowExW(0, L"TextWnd", - Title(C_("title", "Property Browser")).c_str(), - WS_THICKFRAME | WS_CLIPCHILDREN, - 650, 500, 420, 300, GraphicsWnd, (HMENU)NULL, Instance, NULL); - ssassert(TextWnd != NULL, "Cannot create window"); - - TextWndScrollBar = CreateWindowExW(0, WC_SCROLLBAR, L"", WS_CHILD | - SBS_VERT | SBS_LEFTALIGN | WS_VISIBLE | WS_CLIPSIBLINGS, - 200, 100, 100, 100, TextWnd, NULL, Instance, NULL); - // Force the scrollbar to get resized to the window, - TextWndProc(TextWnd, WM_SIZE, 0, 0); - - TextEditControl = CreateWindowExW(WS_EX_CLIENTEDGE, WC_EDIT, L"", - WS_CHILD | ES_AUTOHSCROLL | WS_TABSTOP | WS_CLIPSIBLINGS, - 50, 50, 100, 21, TextWnd, NULL, Instance, NULL); - -#if HAVE_OPENGL == 3 - // Now that all our windows exist, set up gl contexts. - CreateGlContext(TextWnd, &TextGlDisplay, &TextGlSurface, &TextGlContext); - CreateGlContext(GraphicsWnd, &GraphicsGlDisplay, &GraphicsGlSurface, &GraphicsGlContext); -#else - CreateGlContext(TextWnd, &TextGl); - CreateGlContext(GraphicsWnd, &GraphicsGl); -#endif - - RECT r, rc; - GetWindowRect(TextWnd, &r); - GetClientRect(TextWnd, &rc); - ClientIsSmallerBy = (r.bottom - r.top) - (rc.bottom - rc.top); -} - -void SolveSpace::SetMainMenu(Platform::MenuBarRef menuBar) { - static Platform::MenuBarRef _menuBar; - SetMenu(GraphicsWnd, (HMENU)menuBar->NativePtr()); - _menuBar = menuBar; - - SS.UpdateWindowTitle(); - SetWindowTextW(TextWnd, Title(C_("title", "Property Browser")).c_str()); -} - #ifdef HAVE_SPACEWARE //----------------------------------------------------------------------------- // Test if a message comes from the SpaceNavigator device. If yes, dispatch @@ -1273,9 +505,10 @@ static bool ProcessSpaceNavigatorMsg(MSG *msg) { int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, INT nCmdShow) { - Instance = hInstance; - - InitCommonControls(); + INITCOMMONCONTROLSEX icc; + icc.dwSize = sizeof(icc); + icc.dwICC = ICC_STANDARD_CLASSES|ICC_BAR_CLASSES; + InitCommonControlsEx(&icc); // A monospaced font FixedFont = CreateFontW(SS.TW.CHAR_HEIGHT, SS.TW.CHAR_WIDTH_, 0, 0, @@ -1285,16 +518,6 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, if(!FixedFont) FixedFont = (HFONT)GetStockObject(SYSTEM_FONT); - // Create the root windows: one for control, with text, and one for - // the graphics - CreateMainWindows(); - - ThawWindowPos(TextWnd, "TextWnd"); - ThawWindowPos(GraphicsWnd, "GraphicsWnd"); - - ShowWindow(TextWnd, SW_SHOWNOACTIVATE); - ShowWindow(GraphicsWnd, SW_SHOW); - std::vector args = InitPlatform(0, NULL); #ifdef HAVE_SPACEWARE @@ -1304,9 +527,8 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, if(swdc != NULL) { SiOpenData sod; SiInitialize(); - SiOpenWinInit(&sod, GraphicsWnd); - SpaceNavigator = - SiOpen("GraphicsWnd", SI_ANY_DEVICE, SI_NO_MASK, SI_EVENT, &sod); + SiOpenWinInit(&sod, (HWND)SS.GW.window->NativePtr()); + SpaceNavigator = SiOpen("GraphicsWnd", SI_ANY_DEVICE, SI_NO_MASK, SI_EVENT, &sod); SiSetUiMode(SpaceNavigator, SI_UI_NO_CONTROLS); } #endif @@ -1319,16 +541,11 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, // Call in to the platform-independent code, and let them do their init SS.Init(); - // A filename may have been specified on the command line; if so, then - // strip any quotation marks, and make it absolute. + // A filename may have been specified on the command line. if(args.size() >= 2) { SS.Load(Platform::Path::From(args[1]).Expand(/*fromCurrentDirectory=*/true)); } - // Repaint one more time, after we've set everything up. - PaintGraphics(); - PaintTextWnd(); - // And now it's the message loop. All calls in to the rest of the code // will be from the wndprocs. MSG msg; @@ -1339,17 +556,6 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, if(ProcessSpaceNavigatorMsg(&msg)) continue; #endif - // A message from the keyboard, which should be processed as a keyboard - // accelerator? - if(msg.message == WM_KEYDOWN) { - if(ProcessKeyDown(msg.wParam)) continue; - } - if(msg.message == WM_SYSKEYDOWN && msg.hwnd == TextWnd) { - // If the user presses the Alt key when the text window has focus, - // then that should probably go to the graphics window instead. - SetForegroundWindow(GraphicsWnd); - } - // None of the above; so just a normal message to process. TranslateMessage(&msg); DispatchMessage(&msg); @@ -1362,10 +568,6 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, } #endif - // Write everything back to the registry - FreezeWindowPos(TextWnd, "TextWnd"); - FreezeWindowPos(GraphicsWnd, "GraphicsWnd"); - // Free the memory we've used; anything that remains is a leak. SK.Clear(); SS.Clear(); diff --git a/src/render/render.cpp b/src/render/render.cpp index 16a2cc81..3ef7171b 100644 --- a/src/render/render.cpp +++ b/src/render/render.cpp @@ -70,7 +70,7 @@ Vector Camera::VectorFromProjs(Vector rightUpForward) const { } Vector Camera::AlignToPixelGrid(Vector v) const { - if(!hasPixels) return v; + if(!gridFit) return v; v = ProjectPoint3(v); v.x = floor(v.x) + 0.5; diff --git a/src/render/render.h b/src/render/render.h index ac072840..714180d2 100644 --- a/src/render/render.h +++ b/src/render/render.h @@ -17,13 +17,15 @@ enum class StipplePattern : uint32_t; // an axonometric projection. class Camera { public: - size_t width, height; + double width; + double height; + double pixelRatio; + bool gridFit; Vector offset; Vector projRight; Vector projUp; double scale; double tangent; - bool hasPixels; bool IsPerspective() const { return tangent != 0.0; } @@ -255,7 +257,7 @@ public: // A canvas that renders onto a 2d surface, performing z-index sorting, occlusion testing, etc, // on the CPU. -class SurfaceRenderer : public Canvas { +class SurfaceRenderer : public ViewportCanvas { public: Camera camera = {}; Lighting lighting = {}; @@ -273,6 +275,10 @@ public: // Canvas interface. const Camera &GetCamera() const override { return camera; } + // ViewportCanvas interface. + void SetCamera(const Camera &camera) override { this->camera = camera; }; + void SetLighting(const Lighting &lighting) override { this->lighting = lighting; } + void DrawLine(const Vector &a, const Vector &b, hStroke hcs) override; void DrawEdges(const SEdgeList &el, hStroke hcs) override; bool DrawBeziers(const SBezierList &bl, hStroke hcs) override; @@ -326,6 +332,14 @@ public: hStroke hcs; } current = {}; + void Clear() override; + + void NewFrame() override {} + void FlushFrame() override; + std::shared_ptr ReadFrame() override; + + void GetIdent(const char **vendor, const char **renderer, const char **version) override; + void SelectStroke(hStroke hcs); void MoveTo(Vector p); void FinishPath(); @@ -339,6 +353,18 @@ public: void OutputEnd() override; }; +class CairoPixmapRenderer : public CairoRenderer { +public: + std::shared_ptr pixmap; + + cairo_surface_t *surface = NULL; + + void Init(); + void Clear() override; + + std::shared_ptr ReadFrame() override; +}; + //----------------------------------------------------------------------------- // 3d renderers //----------------------------------------------------------------------------- diff --git a/src/render/rendercairo.cpp b/src/render/rendercairo.cpp index 0aeb946b..3091b792 100644 --- a/src/render/rendercairo.cpp +++ b/src/render/rendercairo.cpp @@ -8,6 +8,30 @@ namespace SolveSpace { +void CairoRenderer::Clear() { + SurfaceRenderer::Clear(); + + if(context != NULL) cairo_destroy(context); + context = NULL; +} + +void CairoRenderer::GetIdent(const char **vendor, const char **renderer, const char **version) { + *vendor = "Cairo"; + *renderer = "Cairo"; + *version = cairo_version_string(); +} + +void CairoRenderer::FlushFrame() { + CullOccludedStrokes(); + OutputInPaintOrder(); + + cairo_surface_flush(cairo_get_target(context)); +} + +std::shared_ptr CairoRenderer::ReadFrame() { + ssassert(false, "generic Cairo renderer does not support pixmap readout"); +} + void CairoRenderer::OutputStart() { cairo_save(context); @@ -31,7 +55,6 @@ void CairoRenderer::OutputEnd() { FinishPath(); cairo_restore(context); - cairo_surface_flush(cairo_get_target(context)); } void CairoRenderer::SelectStroke(hStroke hcs) { @@ -118,4 +141,33 @@ void CairoRenderer::OutputTriangle(const STriangle &tr) { cairo_fill(context); } +void CairoPixmapRenderer::Init() { + Clear(); + + pixmap = std::make_shared(); + pixmap->format = Pixmap::Format::BGRA; + pixmap->width = (size_t)camera.width; + pixmap->height = (size_t)camera.height; + pixmap->stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, (int)camera.width); + pixmap->data = std::vector(pixmap->stride * pixmap->height); + surface = + cairo_image_surface_create_for_data(&pixmap->data[0], CAIRO_FORMAT_RGB24, + pixmap->width, pixmap->height, + pixmap->stride); + context = cairo_create(surface); +} + +void CairoPixmapRenderer::Clear() { + CairoRenderer::Clear(); + + if(surface != NULL) cairo_surface_destroy(surface); + surface = NULL; +} + +std::shared_ptr CairoPixmapRenderer::ReadFrame() { + std::shared_ptr result = pixmap->Copy(); + result->ConvertTo(Pixmap::Format::RGBA); + return result; +} + } diff --git a/src/render/rendergl1.cpp b/src/render/rendergl1.cpp index edc21347..d06a7391 100644 --- a/src/render/rendergl1.cpp +++ b/src/render/rendergl1.cpp @@ -705,7 +705,9 @@ void OpenGl1Renderer::InvalidatePixmap(std::shared_ptr pm) { void OpenGl1Renderer::UpdateProjection() { UnSelectPrimitive(); - glViewport(0, 0, camera.width, camera.height); + glViewport(0, 0, + camera.width * camera.pixelRatio, + camera.height * camera.pixelRatio); glMatrixMode(GL_PROJECTION); glLoadIdentity(); diff --git a/src/render/rendergl3.cpp b/src/render/rendergl3.cpp index 3256691c..3f409f7d 100644 --- a/src/render/rendergl3.cpp +++ b/src/render/rendergl3.cpp @@ -541,7 +541,9 @@ void OpenGl2Renderer::DrawPixmap(std::shared_ptr pm, } void OpenGl2Renderer::UpdateProjection() { - glViewport(0, 0, camera.width, camera.height); + glViewport(0, 0, + (int)(camera.width * camera.pixelRatio), + (int)(camera.height * camera.pixelRatio)); double mat1[16]; double mat2[16]; @@ -670,7 +672,8 @@ void OpenGl2Renderer::Clear() { std::shared_ptr OpenGl2Renderer::ReadFrame() { std::shared_ptr pixmap = Pixmap::Create(Pixmap::Format::RGB, (size_t)camera.width, (size_t)camera.height); - glReadPixels(0, 0, camera.width, camera.height, GL_RGB, GL_UNSIGNED_BYTE, &pixmap->data[0]); + glReadPixels(0, 0, (int)camera.width, (int)camera.height, + GL_RGB, GL_UNSIGNED_BYTE, &pixmap->data[0]); return pixmap; } diff --git a/src/resource.cpp b/src/resource.cpp index 0f051f88..af0ac3ec 100644 --- a/src/resource.cpp +++ b/src/resource.cpp @@ -354,6 +354,16 @@ std::shared_ptr Pixmap::Create(Format format, size_t width, size_t heigh return pixmap; } +std::shared_ptr Pixmap::Copy() { + std::shared_ptr pixmap = std::make_shared(); + pixmap->format = format; + pixmap->width = width; + pixmap->height = height; + pixmap->stride = stride; + pixmap->data = data; + return pixmap; +} + //----------------------------------------------------------------------------- // ASCII sequence parsing //----------------------------------------------------------------------------- @@ -938,8 +948,8 @@ void VectorFont::Trace(double forCapHeight, Vector o, Vector u, Vector v, const ssassert(!IsEmpty(), "Expected a loaded font"); // Perform grid-fitting only when the text is parallel to the view plane. - if(camera.hasPixels && !(u.WithMagnitude(1).Equals(camera.projRight) && - v.WithMagnitude(1).Equals(camera.projUp))) { + if(camera.gridFit && !(u.WithMagnitude(1).Equals(camera.projRight) && + v.WithMagnitude(1).Equals(camera.projUp))) { return Trace(forCapHeight, o, u, v, str, traceEdge); } diff --git a/src/resource.h b/src/resource.h index 20bf6bd4..6c14dad8 100644 --- a/src/resource.h +++ b/src/resource.h @@ -40,6 +40,8 @@ public: void ConvertTo(Format newFormat); void SetPixel(size_t x, size_t y, RgbaColor color); + + std::shared_ptr Copy(); }; class BitmapFont { diff --git a/src/solvespace.cpp b/src/solvespace.cpp index 3c11d011..e36a32e6 100644 --- a/src/solvespace.cpp +++ b/src/solvespace.cpp @@ -127,6 +127,14 @@ void SolveSpaceUI::Init() { NewFile(); AfterNewFile(); + + if(TW.window && GW.window) { + TW.window->ThawPosition("TextWindow"); + TW.window->SetVisible(true); + GW.window->ThawPosition("GraphicsWindow"); + GW.window->SetVisible(true); + GW.window->Focus(); + } } bool SolveSpaceUI::LoadAutosaveFor(const Platform::Path &filename) { @@ -161,6 +169,9 @@ bool SolveSpaceUI::Load(const Platform::Path &filename) { } void SolveSpaceUI::Exit() { + GW.window->FreezePosition("GraphicsWindow"); + TW.window->FreezePosition("TextWindow"); + // Recent files for(size_t i = 0; i < MAX_RECENT; i++) { std::string rawPath; @@ -242,7 +253,7 @@ void SolveSpaceUI::Exit() { // And the default styles, colors and line widths and such. Style::FreezeDefaultStyles(); - ExitNow(); + Platform::Exit(); } void SolveSpaceUI::ScheduleGenerateAll() { @@ -339,23 +350,18 @@ void SolveSpaceUI::AfterNewFile() { GenerateAll(Generate::ALL); - TW.Init(); GW.Init(); + TW.Init(); unsaved = false; - int w, h; - GetGraphicsWindowSize(&w, &h); - GW.width = w; - GW.height = h; - - GW.ZoomToFit(/*includingInvisibles=*/false); + GW.ZoomToFit(); // Create all the default styles; they'll get created on the fly anyways, // but can't hurt to do it now. Style::CreateAllDefaultStyles(); - UpdateWindowTitle(); + UpdateWindowTitles(); } void SolveSpaceUI::AddToRecentList(const Platform::Path &filename) { @@ -423,8 +429,18 @@ bool SolveSpaceUI::OkayToStartNewFile() { ssassert(false, "Unexpected dialog choice"); } -void SolveSpaceUI::UpdateWindowTitle() { - SetCurrentFilename(saveFile); +void SolveSpaceUI::UpdateWindowTitles() { + if(!GW.window || !TW.window) return; + + if(saveFile.IsEmpty()) { + GW.window->SetTitle(C_("title", "(new sketch)")); + } else { + if(!GW.window->SetTitleForFilename(saveFile)) { + GW.window->SetTitle(saveFile.raw); + } + } + + TW.window->SetTitle(C_("title", "Property Browser")); } void SolveSpaceUI::MenuFile(Command id) { @@ -553,7 +569,7 @@ void SolveSpaceUI::MenuFile(Command id) { default: ssassert(false, "Unexpected menu ID"); } - SS.UpdateWindowTitle(); + SS.UpdateWindowTitles(); } void SolveSpaceUI::MenuAnalyze(Command id) { @@ -603,7 +619,7 @@ void SolveSpaceUI::MenuAnalyze(Command id) { root->MakeCertainEdgesInto(&(SS.nakedEdges), EdgeKind::SELF_INTER, /*coplanarIsInter=*/false, &inters, &leaks); - InvalidateGraphics(); + SS.GW.Invalidate(); if(inters) { Error("%d edges interfere with other triangles, bad.", @@ -617,7 +633,7 @@ void SolveSpaceUI::MenuAnalyze(Command id) { case Command::CENTER_OF_MASS: { SS.UpdateCenterOfMass(); SS.centerOfMass.draw = true; - InvalidateGraphics(); + SS.GW.Invalidate(); break; } @@ -777,7 +793,7 @@ void SolveSpaceUI::MenuAnalyze(Command id) { // Clear the trace, and stop tracing SS.traced.point = Entity::NO_ENTITY; SS.traced.path.l.Clear(); - InvalidateGraphics(); + SS.GW.Invalidate(); break; } @@ -798,7 +814,7 @@ void SolveSpaceUI::ShowNakedEdges(bool reportOnlyWhenNotOkay) { if(reportOnlyWhenNotOkay && !inters && !leaks && SS.nakedEdges.l.n == 0) { return; } - InvalidateGraphics(); + SS.GW.Invalidate(); const char *intersMsg = inters ? "The mesh is self-intersecting (NOT okay, invalid)." : @@ -852,6 +868,22 @@ void SolveSpaceUI::Clear() { if(i < undo.cnt) undo.d[i].Clear(); if(i < redo.cnt) redo.d[i].Clear(); } + TW.window = NULL; + GW.openRecentMenu = NULL; + GW.linkRecentMenu = NULL; + GW.showGridMenuItem = NULL; + GW.perspectiveProjMenuItem = NULL; + GW.showToolbarMenuItem = NULL; + GW.showTextWndMenuItem = NULL; + GW.fullScreenMenuItem = NULL; + GW.unitsMmMenuItem = NULL; + GW.unitsMetersMenuItem = NULL; + GW.unitsInchesMenuItem = NULL; + GW.inWorkplaneMenuItem = NULL; + GW.in3dMenuItem = NULL; + GW.undoMenuItem = NULL; + GW.redoMenuItem = NULL; + GW.window = NULL; } void Sketch::Clear() { diff --git a/src/solvespace.h b/src/solvespace.h index a0dcffa7..7eb44332 100644 --- a/src/solvespace.h +++ b/src/solvespace.h @@ -38,6 +38,7 @@ struct FT_LibraryRec_; struct FT_FaceRec_; typedef struct _cairo cairo_t; +typedef struct _cairo_surface cairo_surface_t; // The few floating-point equality comparisons in SolveSpace have been // carefully considered, so we disable the -Wfloat-equal warning for them @@ -164,37 +165,13 @@ std::vector GetFontFiles(); void OpenWebsite(const char *url); -void SetMainMenu(Platform::MenuBarRef menuBar); - -void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, - const std::string &str); -void HideGraphicsEditControl(); -bool GraphicsEditControlIsVisible(); -void ShowTextEditControl(int x, int y, const std::string &str); -void HideTextEditControl(); -bool TextEditControlIsVisible(); -void MoveTextScrollbarTo(int pos, int maxPos, int page); - -void ShowTextWindow(bool visible); -void InvalidateText(); -void InvalidateGraphics(); -void PaintGraphics(); -void ToggleFullScreen(); -bool FullScreenIsActive(); -void GetGraphicsWindowSize(int *w, int *h); -void GetTextWindowSize(int *w, int *h); -double GetScreenDpi(); -int64_t GetMilliseconds(); - void dbp(const char *str, ...); #define DBPTRI(tri) \ dbp("tri: (%.3f %.3f %.3f) (%.3f %.3f %.3f) (%.3f %.3f %.3f)", \ CO((tri).a), CO((tri).b), CO((tri).c)) -void SetCurrentFilename(const Platform::Path &filename); void SetMousePointerToHand(bool yes); void DoMessageBox(const char *str, int rows, int cols, bool error); -void ExitNow(); void CnfFreezeInt(uint32_t val, const std::string &name); void CnfFreezeFloat(float val, const std::string &name); @@ -283,6 +260,7 @@ void MakeMatrix(double *mat, double a11, double a12, double a13, double a14, double a41, double a42, double a43, double a44); void MultMatrix(double *mata, double *matb, double *matr); +int64_t GetMilliseconds(); void Message(const char *str, ...); void Error(const char *str, ...); void CnfFreezeBool(bool v, const std::string &name); @@ -730,7 +708,7 @@ public: bool GetFilenameAndSave(bool saveAs); bool OkayToStartNewFile(); hGroup CreateDefaultDrawingGroup(); - void UpdateWindowTitle(); + void UpdateWindowTitles(); void ClearExisting(); void NewFile(); bool SaveToFile(const Platform::Path &filename); diff --git a/src/style.cpp b/src/style.cpp index 9729b3e4..78090c5b 100644 --- a/src/style.cpp +++ b/src/style.cpp @@ -162,7 +162,7 @@ void Style::AssignSelectionToStyle(uint32_t v) { } SS.GW.ClearSelection(); - InvalidateGraphics(); + SS.GW.Invalidate(); // And show that style's info screen in the text window. SS.TW.GoToScreen(TextWindow::Screen::STYLE_INFO); @@ -439,7 +439,7 @@ void TextWindow::ScreenDeleteStyle(int link, uint32_t v) { // the style, so no need to do anything else. } SS.TW.GoToScreen(Screen::LIST_OF_STYLES); - InvalidateGraphics(); + SS.GW.Invalidate(); } void TextWindow::ScreenChangeStylePatternType(int link, uint32_t v) { @@ -595,11 +595,10 @@ void TextWindow::ScreenChangeStyleYesNo(int link, uint32_t v) { s->textOrigin = (Style::TextOrigin)((uint32_t)s->textOrigin | (uint32_t)Style::TextOrigin::TOP); break; } - SS.GW.persistentDirty = true; - InvalidateGraphics(); + SS.GW.Invalidate(/*clearPersistent=*/true); } -bool TextWindow::EditControlDoneForStyles(const char *str) { +bool TextWindow::EditControlDoneForStyles(const std::string &str) { Style *s; switch(edit.meaning) { case Edit::STYLE_STIPPLE_PERIOD: @@ -614,7 +613,7 @@ bool TextWindow::EditControlDoneForStyles(const char *str) { if(units == Style::UnitsAs::MM) { v = SS.StringToMm(str); } else { - v = atof(str); + v = atof(str.c_str()); } v = max(0.0, v); if(edit.meaning == Edit::STYLE_TEXT_HEIGHT) { @@ -629,14 +628,14 @@ bool TextWindow::EditControlDoneForStyles(const char *str) { case Edit::STYLE_TEXT_ANGLE: SS.UndoRemember(); s = Style::Get(edit.style); - s->textAngle = WRAP_SYMMETRIC(atof(str), 360); + s->textAngle = WRAP_SYMMETRIC(atof(str.c_str()), 360); break; case Edit::BACKGROUND_COLOR: case Edit::STYLE_FILL_COLOR: case Edit::STYLE_COLOR: { Vector rgb; - if(sscanf(str, "%lf, %lf, %lf", &rgb.x, &rgb.y, &rgb.z)==3) { + if(sscanf(str.c_str(), "%lf, %lf, %lf", &rgb.x, &rgb.y, &rgb.z)==3) { rgb = rgb.ClampWithin(0, 1); if(edit.meaning == Edit::STYLE_COLOR) { SS.UndoRemember(); @@ -655,7 +654,7 @@ bool TextWindow::EditControlDoneForStyles(const char *str) { break; } case Edit::STYLE_NAME: - if(!*str) { + if(str.empty()) { Error(_("Style name cannot be empty")); } else { SS.UndoRemember(); diff --git a/src/textscreens.cpp b/src/textscreens.cpp index bcd6bf1e..c42a4851 100644 --- a/src/textscreens.cpp +++ b/src/textscreens.cpp @@ -577,10 +577,10 @@ void TextWindow::ScreenStepDimGo(int link, uint32_t v) { // Failed to solve, so quit break; } - PaintGraphics(); + SS.GW.window->Redraw(); } } - InvalidateGraphics(); + SS.GW.Invalidate(); SS.TW.GoToScreen(Screen::LIST_OF_GROUPS); } void TextWindow::ShowStepDimension() { @@ -657,13 +657,12 @@ void TextWindow::ShowTangentArc() { //----------------------------------------------------------------------------- // The edit control is visible, and the user just pressed enter. //----------------------------------------------------------------------------- -void TextWindow::EditControlDone(const char *s) { +void TextWindow::EditControlDone(std::string s) { edit.showAgain = false; switch(edit.meaning) { - case Edit::TIMES_REPEATED: { - Expr *e = Expr::From(s, /*popUpError=*/true); - if(e) { + case Edit::TIMES_REPEATED: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { SS.UndoRemember(); double ev = e->Eval(); @@ -698,9 +697,9 @@ void TextWindow::EditControlDone(const char *s) { SS.MarkGroupDirty(g->h); } break; - } - case Edit::GROUP_NAME: { - if(!*s) { + + case Edit::GROUP_NAME: + if(s.empty()) { Error(_("Group name cannot be empty")); } else { SS.UndoRemember(); @@ -709,10 +708,9 @@ void TextWindow::EditControlDone(const char *s) { g->name = s; } break; - } - case Edit::GROUP_SCALE: { - Expr *e = Expr::From(s, /*popUpError=*/true); - if(e) { + + case Edit::GROUP_SCALE: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { double ev = e->Eval(); if(fabs(ev) < 1e-6) { Error(_("Scale cannot be zero.")); @@ -723,10 +721,10 @@ void TextWindow::EditControlDone(const char *s) { } } break; - } + case Edit::GROUP_COLOR: { Vector rgb; - if(sscanf(s, "%lf, %lf, %lf", &rgb.x, &rgb.y, &rgb.z)==3) { + if(sscanf(s.c_str(), "%lf, %lf, %lf", &rgb.x, &rgb.y, &rgb.z)==3) { rgb = rgb.ClampWithin(0, 1); Group *g = SK.group.FindByIdNoOops(SS.TW.shown.group); @@ -740,9 +738,8 @@ void TextWindow::EditControlDone(const char *s) { } break; } - case Edit::GROUP_OPACITY: { - Expr *e = Expr::From(s, /*popUpError=*/true); - if(e) { + case Edit::GROUP_OPACITY: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { double alpha = e->Eval(); if(alpha < 0 || alpha > 1) { Error(_("Opacity must be between zero and one.")); @@ -754,42 +751,38 @@ void TextWindow::EditControlDone(const char *s) { } } break; - } - case Edit::TTF_TEXT: { + + case Edit::TTF_TEXT: SS.UndoRemember(); - Request *r = SK.request.FindByIdNoOops(edit.request); - if(r) { + if(Request *r = SK.request.FindByIdNoOops(edit.request)) { r->str = s; SS.MarkGroupDirty(r->group); } break; - } - case Edit::STEP_DIM_FINISH: { - Expr *e = Expr::From(s, /*popUpError=*/true); - if(!e) { - break; + + case Edit::STEP_DIM_FINISH: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { + if(shown.dimIsDistance) { + shown.dimFinish = SS.ExprToMm(e); + } else { + shown.dimFinish = e->Eval(); + } } - if(shown.dimIsDistance) { - shown.dimFinish = SS.ExprToMm(e); - } else { - shown.dimFinish = e->Eval(); - } - break; - } - case Edit::STEP_DIM_STEPS: - shown.dimSteps = min(300, max(1, atoi(s))); break; - case Edit::TANGENT_ARC_RADIUS: { - Expr *e = Expr::From(s, /*popUpError=*/true); - if(!e) break; - if(e->Eval() < LENGTH_EPS) { - Error(_("Radius cannot be zero or negative.")); - break; - } - SS.tangentArcRadius = SS.ExprToMm(e); + case Edit::STEP_DIM_STEPS: + shown.dimSteps = min(300, max(1, atoi(s.c_str()))); + break; + + case Edit::TANGENT_ARC_RADIUS: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { + if(e->Eval() < LENGTH_EPS) { + Error(_("Radius cannot be zero or negative.")); + break; + } + SS.tangentArcRadius = SS.ExprToMm(e); + } break; - } default: { int cnt = 0; @@ -801,7 +794,7 @@ void TextWindow::EditControlDone(const char *s) { break; } } - InvalidateGraphics(); + SS.GW.Invalidate(); SS.ScheduleShowTW(); if(!edit.showAgain) { diff --git a/src/textwin.cpp b/src/textwin.cpp index 8524be3f..9f10caf9 100644 --- a/src/textwin.cpp +++ b/src/textwin.cpp @@ -148,7 +148,7 @@ public: } SS.GenerateAll(); - InvalidateGraphics(); + SS.GW.Invalidate(); SS.ScheduleShowTW(); } }; @@ -225,15 +225,49 @@ void TextWindow::MakeColorTable(const Color *in, float *out) { } void TextWindow::Init() { - canvas = CreateRenderer(); + if(!window) { + window = Platform::CreateWindow(Platform::Window::Kind::TOOL, SS.GW.window); + if(window) { + canvas = CreateRenderer(); + + using namespace std::placeholders; + window->onClose = []() { + SS.GW.showTextWindow = false; + SS.GW.EnsureValidActives(); + }; + window->onMouseEvent = [this](Platform::MouseEvent event) { + using Platform::MouseEvent; + + if(event.type == MouseEvent::Type::PRESS || + event.type == MouseEvent::Type::DBL_PRESS || + event.type == MouseEvent::Type::MOTION) { + bool isClick = (event.type != MouseEvent::Type::MOTION); + bool leftDown = (event.button == MouseEvent::Button::LEFT); + this->MouseEvent(isClick, leftDown, event.x, event.y); + return true; + } else if(event.type == MouseEvent::Type::LEAVE) { + MouseLeave(); + return true; + } else if(event.type == MouseEvent::Type::SCROLL_VERT) { + window->SetScrollbarPosition(window->GetScrollbarPosition() - + LINE_HEIGHT / 2 * event.scrollDelta); + } + return false; + }; + window->onKeyboardEvent = SS.GW.window->onKeyboardEvent; + window->onRender = std::bind(&TextWindow::Paint, this); + window->onEditingDone = std::bind(&TextWindow::EditControlDone, this, _1); + window->onScrollbarAdjusted = std::bind(&TextWindow::ScrollbarEvent, this, _1); + window->SetMinContentSize(370, 370); + } + } ClearSuper(); } void TextWindow::ClearSuper() { - HideEditControl(); - // Ugly hack, but not so ugly as the next line + Platform::WindowRef oldWindow = std::move(window); std::shared_ptr oldCanvas = canvas; // Cannot use *this = {} here because TextWindow instances @@ -242,8 +276,11 @@ void TextWindow::ClearSuper() { memset(this, 0, sizeof(*this)); // Return old canvas + window = std::move(oldWindow); canvas = oldCanvas; + HideEditControl(); + MakeColorTable(fgColors, fgColorTable); MakeColorTable(bgColors, bgColorTable); @@ -253,7 +290,10 @@ void TextWindow::ClearSuper() { void TextWindow::HideEditControl() { editControl.colorPicker.show = false; - HideTextEditControl(); + if(window) { + window->HideEditor(); + window->Invalidate(); + } } void TextWindow::ShowEditControl(int col, const std::string &str, int halfRow) { @@ -264,11 +304,13 @@ void TextWindow::ShowEditControl(int col, const std::string &str, int halfRow) { int x = LEFT_MARGIN + CHAR_WIDTH_*col; int y = (halfRow - SS.TW.scrollPos)*(LINE_HEIGHT/2); - ShowTextEditControl(x, y + 18, str); + double width, height; + window->GetContentSize(&width, &height); + window->ShowEditor(x, y + LINE_HEIGHT - 2, LINE_HEIGHT - 4, + width - x, /*isMonospace=*/true, str); } -void TextWindow::ShowEditControlWithColorPicker(int col, RgbaColor rgb) -{ +void TextWindow::ShowEditControlWithColorPicker(int col, RgbaColor rgb) { SS.ScheduleShowTW(); editControl.colorPicker.show = true; @@ -518,21 +560,35 @@ void TextWindow::Show() { } } - InvalidateText(); + if(window) { + double width, height; + window->GetContentSize(&width, &height); + + halfRows = (int)height / (LINE_HEIGHT/2); + + int bottom = top[rows-1] + 2; + scrollPos = min(scrollPos, bottom - halfRows); + scrollPos = max(scrollPos, 0); + + window->ConfigureScrollbar(0, top[rows - 1] + 1, halfRows); + window->SetScrollbarPosition(scrollPos); + window->SetScrollbarVisible(top[rows - 1] + 1 > halfRows); + window->Invalidate(); + } } void TextWindow::DrawOrHitTestIcons(UiCanvas *uiCanvas, TextWindow::DrawOrHitHow how, double mx, double my) { - int width, height; - GetTextWindowSize(&width, &height); + double width, height; + window->GetContentSize(&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, + uiCanvas->DrawRect(0, (int)width, top, bot, /*fillColor=*/{ 30, 30, 30, 255 }, /*outlineColor=*/{}); } @@ -546,9 +602,6 @@ void TextWindow::DrawOrHitTestIcons(UiCanvas *uiCanvas, TextWindow::DrawOrHitHow button->Draw(uiCanvas, x, y, (button == hoveredButton)); } else if(mx > x - 2 && mx < x + 26 && my < y + 2 && my > y - 26) { - if(button != oldHovered) { - // FIXME(platorm/gui): implement native tooltips here - } hoveredButton = button; if(how == CLICK) { button->Click(); @@ -559,7 +612,12 @@ void TextWindow::DrawOrHitTestIcons(UiCanvas *uiCanvas, TextWindow::DrawOrHitHow } if(how != PAINT && hoveredButton != oldHovered) { - InvalidateText(); + if(hoveredButton == NULL) { + window->SetTooltip(""); + } else { + window->SetTooltip(hoveredButton->Tooltip()); + } + window->Invalidate(); } } @@ -629,12 +687,14 @@ std::shared_ptr TextWindow::HsvPattern1d(double hue, double sat, int w, void TextWindow::ColorPickerDone() { RgbaColor rgb = editControl.colorPicker.rgb; - EditControlDone(ssprintf("%.2f, %.2f, %.3f", rgb.redF(), rgb.greenF(), rgb.blueF()).c_str()); + EditControlDone(ssprintf("%.2f, %.2f, %.3f", rgb.redF(), rgb.greenF(), rgb.blueF())); } bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how, bool leftDown, double x, double y) { + using Platform::Window; + bool mousePointerAsHand = false; if(how == HOVER && !leftDown) { @@ -643,7 +703,7 @@ bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how, } if(!editControl.colorPicker.show) return false; - if(how == CLICK || (how == HOVER && leftDown)) InvalidateText(); + if(how == CLICK || (how == HOVER && leftDown)) window->Invalidate(); static const RgbaColor BaseColor[12] = { RGBi(255, 0, 0), @@ -662,8 +722,8 @@ bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how, RGBi( 0, 127, 255), }; - int width, height; - GetTextWindowSize(&width, &height); + double width, height; + window->GetContentSize(&width, &height); int px = LEFT_MARGIN + CHAR_WIDTH_*editControl.col; int py = (editControl.halfRow - SS.TW.scrollPos)*(LINE_HEIGHT/2); @@ -673,7 +733,7 @@ bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how, static const int WIDTH = 16, HEIGHT = 12; static const int PITCH = 18, SIZE = 15; - px = min(px, width - (WIDTH*PITCH + 40)); + px = min(px, (int)width - (WIDTH*PITCH + 40)); int pxm = px + WIDTH*PITCH + 11, pym = py + HEIGHT*PITCH + 7; @@ -818,22 +878,26 @@ bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how, } } - SetMousePointerToHand(mousePointerAsHand); + window->SetCursor(mousePointerAsHand ? + Window::Cursor::HAND : + Window::Cursor::POINTER); return true; } void TextWindow::Paint() { if (!canvas) return; - int width, height; - GetTextWindowSize(&width, &height); + double width, height; + window->GetContentSize(&width, &height); Camera camera = {}; - camera.width = width; - camera.height = height; + camera.width = width; + camera.height = height; + camera.pixelRatio = window->GetDevicePixelRatio(); + camera.gridFit = (window->GetDevicePixelRatio() == 1); camera.LoadIdentity(); - camera.offset.x = -(double)camera.width / 2.0; - camera.offset.y = -(double)camera.height / 2.0; + camera.offset.x = -camera.width / 2.0; + camera.offset.y = -camera.height / 2.0; Lighting lighting = {}; lighting.backgroundColor = RGBi(0, 0, 0); @@ -846,16 +910,6 @@ void TextWindow::Paint() { 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++) { @@ -863,7 +917,7 @@ void TextWindow::Paint() { if(ltop < (scrollPos-1)) continue; if(ltop > scrollPos+halfRows) break; - for(c = 0; c < min((width/CHAR_WIDTH_)+1, (int) MAX_COLS); c++) { + for(c = 0; c < min(((int)width/CHAR_WIDTH_)+1, (int) MAX_COLS); c++) { int x = LEFT_MARGIN + c*CHAR_WIDTH_; int y = (ltop-scrollPos)*(LINE_HEIGHT/2) + 4; @@ -971,17 +1025,18 @@ void TextWindow::Paint() { } void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) { - if(TextEditControlIsVisible() || GraphicsEditControlIsVisible()) { - if(DrawOrHitTestColorPicker(NULL, leftClick ? CLICK : HOVER, leftDown, x, y)) - { + using Platform::Window; + + if(SS.TW.window->IsEditorVisible() || SS.GW.window->IsEditorVisible()) { + if(DrawOrHitTestColorPicker(NULL, leftClick ? CLICK : HOVER, leftDown, x, y)) { return; } if(leftClick) { HideEditControl(); - HideGraphicsEditControl(); + SS.GW.window->HideEditor(); } else { - SetMousePointerToHand(false); + window->SetCursor(Window::Cursor::POINTER); } return; } @@ -1007,7 +1062,7 @@ void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) { } } if(r >= 0 && c >= 0 && r < rows && c < MAX_COLS) { - SetMousePointerToHand(false); + window->SetCursor(Window::Cursor::POINTER); hoveredRow = r; hoveredCol = c; @@ -1017,16 +1072,16 @@ void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) { if(item.link && item.f) { (item.f)(item.link, item.data); Show(); - InvalidateGraphics(); + SS.GW.Invalidate(); } } else { if(item.link) { - SetMousePointerToHand(true); + window->SetCursor(Window::Cursor::HAND); if(item.h) { (item.h)(item.link, item.data); } } else { - SetMousePointerToHand(false); + window->SetCursor(Window::Cursor::POINTER); } } } @@ -1035,8 +1090,8 @@ void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) { prevHoveredRow != hoveredRow || prevHoveredCol != hoveredCol) { - InvalidateGraphics(); - InvalidateText(); + SS.GW.Invalidate(); + window->Invalidate(); } } @@ -1044,21 +1099,20 @@ void TextWindow::MouseLeave() { hoveredButton = NULL; hoveredRow = 0; hoveredCol = 0; - InvalidateText(); + window->Invalidate(); } -void TextWindow::ScrollbarEvent(int newPos) { - if(TextEditControlIsVisible()) +void TextWindow::ScrollbarEvent(double newPos) { + if(window->IsEditorVisible()) return; int bottom = top[rows-1] + 2; - newPos = min(newPos, bottom - halfRows); - newPos = max(newPos, 0); + newPos = min((int)newPos, bottom - halfRows); + newPos = max((int)newPos, 0); if(newPos != scrollPos) { - scrollPos = newPos; - MoveTextScrollbarTo(scrollPos, top[rows - 1] + 1, halfRows); - InvalidateText(); + scrollPos = (int)newPos; + window->Invalidate(); } } diff --git a/src/toolbar.cpp b/src/toolbar.cpp index fafc39cb..756cba07 100644 --- a/src/toolbar.cpp +++ b/src/toolbar.cpp @@ -91,34 +91,61 @@ void GraphicsWindow::ToolbarDraw(UiCanvas *canvas) { } bool GraphicsWindow::ToolbarMouseMoved(int x, int y) { - x += ((int)width/2); - y += ((int)height/2); + double width, height; + window->GetContentSize(&width, &height); - Command nh = Command::NONE; - bool withinToolbar = ToolbarDrawOrHitTest(x, y, NULL, &nh); - if(!withinToolbar) nh = Command::NONE; - - if(nh != toolbarHovered) { - toolbarHovered = nh; - // FIXME(platorm/gui): implement native tooltips here - PaintGraphics(); - } - return withinToolbar; -} - -bool GraphicsWindow::ToolbarMouseDown(int x, int y) { x += ((int)width/2); y += ((int)height/2); Command hit; bool withinToolbar = ToolbarDrawOrHitTest(x, y, NULL, &hit); - SS.GW.ActivateCommand(hit); + + if(hit != toolbarHovered) { + toolbarHovered = hit; + Invalidate(); + } + + if(toolbarHovered != Command::NONE) { + std::string tooltip; + for(ToolIcon &icon : Toolbar) { + if(toolbarHovered == icon.command) { + tooltip = Translate(icon.tooltip); + } + } + + Platform::KeyboardEvent accel = SS.GW.AcceleratorForCommand(toolbarHovered); + std::string accelDesc = Platform::AcceleratorDescription(accel); + if(!accelDesc.empty()) { + tooltip += ssprintf(" (%s)", accelDesc.c_str()); + } + + window->SetTooltip(tooltip); + } + + return withinToolbar; +} + +bool GraphicsWindow::ToolbarMouseDown(int x, int y) { + double width, height; + window->GetContentSize(&width, &height); + + x += ((int)width/2); + y += ((int)height/2); + + Command hit; + bool withinToolbar = ToolbarDrawOrHitTest(x, y, NULL, &hit); + if(hit != Command::NONE) { + SS.GW.ActivateCommand(hit); + } return withinToolbar; } bool GraphicsWindow::ToolbarDrawOrHitTest(int mx, int my, UiCanvas *canvas, Command *menuHit) { + double width, height; + window->GetContentSize(&width, &height); + int x = 17, y = (int)(height - 52); int fudge = 8; diff --git a/src/ui.h b/src/ui.h index 339f6cd9..f5c002c8 100644 --- a/src/ui.h +++ b/src/ui.h @@ -276,16 +276,15 @@ public: int top[MAX_ROWS]; // in half-line units, or -1 for unused int rows; + Platform::WindowRef window; std::shared_ptr canvas; void Draw(Canvas *canvas); - // These are called by the platform-specific code. void Paint(); void MouseEvent(bool isClick, bool leftDown, double x, double y); - void MouseScroll(double x, double y, int delta); void MouseLeave(); - void ScrollbarEvent(int newPos); + void ScrollbarEvent(double newPos); enum DrawOrHitHow : uint32_t { PAINT = 0, @@ -532,18 +531,19 @@ public: static void ScreenChangeViewOrigin(int link, uint32_t v); static void ScreenChangeViewProjection(int link, uint32_t v); - bool EditControlDoneForStyles(const char *s); - bool EditControlDoneForConfiguration(const char *s); - bool EditControlDoneForPaste(const char *s); - bool EditControlDoneForView(const char *s); - void EditControlDone(const char *s); + bool EditControlDoneForStyles(const std::string &s); + bool EditControlDoneForConfiguration(const std::string &s); + bool EditControlDoneForPaste(const std::string &s); + bool EditControlDoneForView(const std::string &s); + void EditControlDone(std::string s); }; class GraphicsWindow { public: void Init(); - Platform::MenuBarRef mainMenu; + Platform::WindowRef window; + void PopulateMainMenu(); void PopulateRecentFiles(); @@ -581,8 +581,6 @@ public: std::shared_ptr persistentCanvas; bool persistentDirty; - // The width and height (in pixels) of the window. - double width, height; // These parameters define the map from 2d screen coordinates to the // coordinates of the 3d sketch points. We will use an axonometric // projection. @@ -635,13 +633,17 @@ public: void AnimateOntoWorkplane(); Vector VectorFromProjs(Vector rightUpForward); void HandlePointForZoomToFit(Vector p, Point2d *pmax, Point2d *pmin, - double *wmin, bool usePerspective); + double *wmin, bool usePerspective, + const Camera &camera); void LoopOverPoints(const std::vector &entities, const std::vector &constraints, const std::vector &faces, Point2d *pmax, Point2d *pmin, - double *wmin, bool usePerspective, bool includeMesh); - void ZoomToFit(bool includingInvisibles, bool useSelection = false); + double *wmin, bool usePerspective, bool includeMesh, + const Camera &camera); + void ZoomToFit(bool includingInvisibles = false, bool useSelection = false); + double ZoomToFit(const Camera &camera, + bool includingInvisibles = false, bool useSelection = false); hGroup activeGroup; void EnsureValidActives(); @@ -831,14 +833,15 @@ public: void UpdateDraggedNum(Vector *pos, double mx, double my); void UpdateDraggedPoint(hEntity hp, double mx, double my); + void Invalidate(bool clearPersistent = false); void DrawEntities(Canvas *canvas, bool persistent); void DrawPersistent(Canvas *canvas); void Draw(Canvas *canvas); - - // These are called by the platform-specific code. void Paint(); + + bool MouseEvent(Platform::MouseEvent event); void MouseMoved(double x, double y, bool leftDown, bool middleDown, - bool rightDown, bool shiftDown, bool ctrlDown); + bool rightDown, bool shiftDown, bool ctrlDown); void MouseLeftDown(double x, double y); void MouseLeftUp(double x, double y); void MouseLeftDoubleClick(double x, double y); @@ -847,7 +850,7 @@ public: void MouseScroll(double x, double y, int delta); void MouseLeave(); bool KeyboardEvent(Platform::KeyboardEvent event); - void EditControlDone(const char *s); + void EditControlDone(const std::string &s); int64_t lastSpaceNavigatorTime; hGroup lastSpaceNavigatorGroup; diff --git a/src/view.cpp b/src/view.cpp index a51f080c..14d09ba3 100644 --- a/src/view.cpp +++ b/src/view.cpp @@ -45,7 +45,7 @@ void TextWindow::ScreenChangeViewScale(int link, uint32_t v) { } void TextWindow::ScreenChangeViewToFullScale(int link, uint32_t v) { - SS.GW.scale = GetScreenDpi() / 25.4; + SS.GW.scale = SS.GW.window->GetPixelDensity() / 25.4; } void TextWindow::ScreenChangeViewOrigin(int link, uint32_t v) { @@ -66,7 +66,7 @@ void TextWindow::ScreenChangeViewProjection(int link, uint32_t v) { SS.TW.ShowEditControl(10, edit_value); } -bool TextWindow::EditControlDoneForView(const char *s) { +bool TextWindow::EditControlDoneForView(const std::string &s) { switch(edit.meaning) { case Edit::VIEW_SCALE: { Expr *e = Expr::From(s, /*popUpError=*/true); @@ -83,7 +83,7 @@ bool TextWindow::EditControlDoneForView(const char *s) { case Edit::VIEW_ORIGIN: { Vector pt; - if(sscanf(s, "%lf, %lf, %lf", &pt.x, &pt.y, &pt.z) == 3) { + if(sscanf(s.c_str(), "%lf, %lf, %lf", &pt.x, &pt.y, &pt.z) == 3) { pt = pt.ScaledBy(SS.MmPerUnit()); SS.GW.offset = pt.ScaledBy(-1); } else { @@ -95,7 +95,7 @@ bool TextWindow::EditControlDoneForView(const char *s) { case Edit::VIEW_PROJ_RIGHT: case Edit::VIEW_PROJ_UP: { Vector pt; - if(sscanf(s, "%lf, %lf, %lf", &pt.x, &pt.y, &pt.z) != 3) { + if(sscanf(s.c_str(), "%lf, %lf, %lf", &pt.x, &pt.y, &pt.z) != 3) { Error(_("Bad format: specify x, y, z")); break; } diff --git a/test/constraint/arc_line_tangent/normal.png b/test/constraint/arc_line_tangent/normal.png index 67fc0e3fc34abdc59bf309e995c58efb002bed2e..b885995f86ab05f5afd6d0a1841a6aaa6282d999 100644 GIT binary patch literal 5101 zcmcf_dpMNo{+*9un1p1ggs9n#DN1dkTq-(kN{-S*7=&t45wb(fR9k5(=_1k+igFz? zsEj^os1&JCuCq!+sMa*OpZD`k+VkFYK!y50x-_&g_)U7>^Lt^7uff_$AjCh~x- zqV|nq7MqN~ypouT3UP2kA_>DPWQ*Nn|zulj@mx{%6iIzaQB0%om>Md*yQ)`;Ox z*SxnJ-;AJTb$q^G=-`fZxT%T*%;MY#?@jT?qCKgb!4(qD^>M!^J5g-g@+y@KhJu?KK^78A}*K*m42$?O2Wh|KEPec=Nk9nFwy6@KE zN~U_Uh^PsPc3G#}S%9!JNVHTBh`E8W<=sLTc6CJB<$w?>uAcBcJW!qTy`;Wf3E5sg zh{wRBN+_a%RD{`31Oq%0gnQweYg8UPe3y$@k=-&ClXv9;0U{zv<n{P5g zra;_kkAp&$d+K1d9&VOq$VIdrznsj(E=QxtU~YD;At*YICc9vm^Oz7BWLiqNHUr8Z z9fzxT%L^WAj|V@JzBYt+`>mwkprTer(hp?p!z%p4C#c^Nn|dXVFbR6* zi9YTDQyNr{#m6nY4^j{;ENwdtC0>rF0=EpRcX>Z<3;*vS=@XFW2)({b9c;B>_+tv6 zSb;ICCB`^d=ES-YjHlohwpj{E2Rlw!_$oA>CR7*8vU89sVF&(Or+IedhFT?;6^3laDXpfnJ7E{-7ou&H7nn5t;+SigrvFq;$TAM2)_9)MW z(P3xDE9&fTe4Bl3@U13{n#dw~ZgV80Ax-w%3n6z~7w`PiP;JJV#h&mlJZ)F9Z50KDqk{Zm(4@v}Lh7+Mb~*ARM|Y?C9&=MjY>hL@;Jt z+UC)w2*RnnqWB(bU}}%M9=|$=SHy+Lki_4GS=;Ucd4gyeNBDS`GJ;lolh$b4{N6jr zWwMRA55kQv8dC)Ge^(RzO(gCFv;q&bww=58%CozrS&bFRqR1IdbNPNSm|2d2%_?7O zslU3VH3`(aI5SK31=Or%+>|GS`6i{`7F_?u81W#S_}8TPaw&-8{ahJL>T(n#>|er$ zL|%!B1&ZvsFZE%y)s*ZC=aVd@<0xA9cHqNs~3YOza-Bz!Sk^wZbmafz%pMEN;tfVBpK-h`WQqt=a zFn&+_O_~BhJ%3-XHv$$?kS(o_4iO_AIXA{;0-v@t($en!#-S(|`smPylke^- zvR^9Ds#r4}(puMRq@?pdRiSa4 zfZ{PT&Oo>vOQ*B`rfJU}EaJM4ymTIN7LeGLc05xWbQWt4&lGE2EGo}* z5q6&a6~7ES>rdc24`S3*=~@XGm+R!rpn2wt4=fZSmo=#Lz|yH)?cpr~ATpMvxGoU3 z-7hK7iJp@$vKnti&BnrHdG|)kB;upBjGCE_G#29`ai1DJP&k#jqSZ}MH6rF57#Xs( zp*&j6sF~xKxn?BePNGG)!2A1rhBm_OjrFY2e@2U~eCnJrjkt`pd90v4D&lSIsfgHD zckFmE5o4hjUP(qr0}l)rR=5?`6?9igI_9~r%+0AUJ}N2ktq%yRNNoR1tUuC#c6EB* z|8Qyjch54X6_?cSbcv9y=T_UhKVIpUy4l@wn5LV4ZHdM5krOJj^E!oz>*%G>@y*UL z!pFTgwhYCy;+?H?lbaWEY$SHMauU`I-VGPBT^7R);bzBq`maiLVpl9OmW>uV95cMk zf0(FuShw-87H6Kd=x5ZEG&Z)C7EMG6FZ8yNV6f_|(kBT8bfCpSJvE{Gwcs672sf%h zJaj+w;iLx;{)ljYh3?*vHJ?|-Nn8|b{&CcaXtjz3^iIRmK1JqBE#CHJ;J>X1C|FHR6Zp5f{S{xph8z55hw?$ zEla_oEKip53?_%JRG+%-gR*4)C zvKvenU28_^G)Q(?8!Ypp?J<38%}2s__B6&RhldXafX>qjluNl`g_}s|h_uFz&+KSX zLDuVhypF+9bE9RH!fMHL?TUY1N2TS&5 zOf=#Dz8aMC$Ibiui}SMhj$%EyXn5iUf1;^;>1z{GV|5$Osm}Fzjxylg18GkhF&>cH zz3H27`j?x@A51Y$^eR6%-Axvt|FHYo)BcqG__E`qLGN}B-U-;trS0Of4z(4{I-Wh_ z8&UsaG19CrIDg3rA48;eFBqRN0nm(8_&d8+7s7O{T(1Op->KnCLx0(qH26#Hzb
WHDb}CxB1eaL=*BW50y%-v}mXc*fR7rvPWX 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 Nc3A8-&)Q6n{3pJcugL%a diff --git a/test/constraint/curve_curve_tangent/arc_arc.png b/test/constraint/curve_curve_tangent/arc_arc.png index 2ee4ea9de7c0a73f846de165b13322426612a73a..c6ceb2abf9a5a3f1c13fb0cdd9dadd22d6e98fba 100644 GIT binary patch literal 5017 zcmeHLX;c$g7JjKD3L>&eMA-xyx|O&9?m$2U(H3#X5K#(bQ6d7eqmYEQp#>CChfM{A zQ3L{3gUHrG*?`a@n@Rv#8^qA;J3>g9ieY*NJw4O&V}8sza}J!Bs`uWl`|5t*ckit$ z2QAGdm#kU>0Fd0f$Mi4&5LNguwg|gZKnbmvjOI|Ho)9xaVIA$9ne8+jgn zP%M0OPsxK#2ba9Sy(;~c+%gp!l3!GdfV#T&M{MzebDeWB zYSUW`n9_0!&Q^3Z5lxx(;0^^f_mcq#vJd`rYG%+Y&xw`5;K7jQ&x)3F2K>?CD`(?f zEEj=ochvBG@o#<;bQKX4!1cqqrg@!@7&D$OZ@d_VUOVmvH387(5i8Dla8WKjGOvJS zt#uNh8i9*B-$vT!MLZg--7z<0F9CecC!9DDT_x4*3%kCza=)?;bgW`cgp9??o&cyO zYho|9ChsdGc(5$G7GicuQ?>IW*4-z9@?SG9Y;hO#ud8W;h&W3E ziU_<398@?VP5n*|+hfS`2I>;vsV1__!~uu;5IC$K$e=L<-JyUj?vtY~*2)5~GStVc zZZDKB$RgJ}gYkzw1!~B$mxt8=+Z)`Vr-$g>sxa;vkr!Nk$D%~NWdW$whJR- z&<^LsV{5)*mCa$cE2KZcJv=ei9*1(p$w9y7iZ^AR@9zf#jxBmQJrXe!Y8NLBf}DcQA{53Q#Dx02vmMUe zLF6fpAIt641Abw`gYpgsLW!g0cZ+$h+1VW>-3?u{HCu~RQsxR7k2&M2HE#lL4@og~ zz2(Ts0=xYTOV^pP{EhJfJ^quE0yj{rvF~zeAdi27H_6NGeFK=ff$L-hx)ZcaMvHFl zWMydEM`}`aNpJH|`@3QR|MAb++ZEbXL2*3y2HnP%RxADBCdH06_$rf3U+$PQmw^3J)yhW1@wm~d2aSWCb7VwYTkC4IcuaR#~hY`_7BFH3_mh0YML)sAc zk6_DDZ+T?vgXAQUTpt0flE`dJ(W1>A6+`!9xVnNm=W~ zn`ZCEObLVV)OAOI?4XX^nY#k$zc3~2BRGPZ?xlhTOp&^mzsFgloKPQCm%5o4x;|ZkDZhvdQHiJF<9$JIH z*0+8~)b~_5K3D~H=n+UZ(ll@>8Vy#wOaeo))L~{V)N$P1|?%jISV1nWT(J{?UR z1yeNuQ5}39th}Sxw{Mh^DHSv2soW{ZoLet?(M08Z`BRbhOPQ{}v*Ux7ufkuT$X+D9?!>nErNXE5OlfGwg<(devg5!zhS_m_c*LALNlB>)U zG%&)*+f-P@@h(+9e{y8{#{JshE-ddUZmbGE9Y>?5+|Xc5W(pjC8Jer3473Xd7<|6% z;Kft46AAil4aX^=_U(!OlI?+23667@bu8V~pt6YtZEQ(mi<%O#C zhwpPiHM=p>am{x2f|z1(xtn$X0`OOVkNdx@gwrM1{_FKO0W{u1q{+2XH{Cjp3AN@= zR?QJ~>u;B7i#E@DjV1e(-kSg@ToL~>2kC|A36;wv0irdJ?D3NKihjrHymx(;b^ox0 z@1jNDC|UAZ{(hG_^A!NQ=L_LK-216VSb)+`y#s&a^rBP8yjn#C*1zv3(7fme?~(a} zK-733sX0Dr6mh_eiv2SO`9B;)UE2Q!9QyrO-f0vZfW68k*c>e7A!(-Xjrmsf7DUs(cew$i{KImG+ zVY*36g8mW@eEMdrr(<_Evg)yZ3~Mi}=;{*S2U)5rh6QPLq*~t4QWYpv7D0U%g1zWT zUT&|)`zZ;0UXEtu++fjR?!jrA6+;=<@d{gEof|!t$#XY&>g85E0F>;y5VAu<0eDBs|68jcY?qXMg&usD<(&rYGUqt4Gf(xtIe*w#NGL{ z`*nx@A>V;nl>zRo=f!cINWoy!*iT;mZMo6s+M9+7dH$2$7oWI!EJMRB3!6Sn3liP< zdNr3?w0SmtWB%hhQXD|$H(gt+`p#!JooWrs zROGLhx%Q#$uY(tK9Rjk}JvIUx(j?d#E&E3LOZG($(*VjE50v6eJJ>yrwAmKPKEd@B zY;QTNY?pg&uBqWt2A}edQ6B`hT@EC0GWNWqZ+h0NdvEZ2PDV}sj5oFKBR%L;&1@E$ zoo34b1+Rw;sSShSO#v^5dq-xkFJkWM*@~QR(#=vaYt`qDzC6IWBh_%mj_X@A6I(*<{CfQ<5@iEoi|bNZ0*Pl{$TPjE$|Wd@W)F#T7G)^ zG_tB#V$1D}o31};{=8QHjUIqwTw=sjiN83%2&gHoUd210Jf-)e)7+%ds6ejaYLu-i zFPFh%xE0$S;pw~#=CA7ZPzFk$zSVe2{h74)g;4yCsYL@(BB2Dz1Y zzhwv2V#*nQP%Y*v41z!PEcTKMn83@F=~zOZU|)#t?k?;;d?0kNl81X4a!J;A#sgO@x3Pw zOrM$L*aAS^hL!{>)>IU@geL;#78VvrYa#$V#t?vM0v8Q%vd{&$|Bw-~*^9*P=g0lo zTFH3*vDbAsd#J<^heD zZKtg{biP3m05j`l8=O5zN2iYVHl)tIFDVA#!)3f^>oZ$oOR*lH!`SSPY7{7XJSY&-4 zFgY`;@~$aW`zzVEx(|+e0A@;(aMOYafdSIhqxU!KQotl(__vl?tpq<5XvO8FU1-{r zjs-QU$c}(@&SsJz+M`zM{>by`Gi8PzlHiGV*u;0GYV{8A(>?4^&*;-pO$yi&m(6h; zds#m4$n}C}5*B!DebkPcI*#I!G5l!q4h(ZEE-z*4b2M|l<>kfdjl(?nl|U8T_A&

H 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 delta 3700 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 L!tB{WO89>P6Vr1m diff --git a/test/constraint/diameter/normal.png b/test/constraint/diameter/normal.png index a53fdaf810f38537f42e985c57f9a6e817106dbf..c58025bd0ef2ccf623037dc3c533a104402a0a4e 100644 GIT binary patch literal 5023 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 diff --git a/test/constraint/diameter/reference.png b/test/constraint/diameter/reference.png index 42cd96f128e70bf6f969092ef0525cb947a60535..b923aeab5e603d83c3ee3b71e409dcdabef09a94 100644 GIT binary patch literal 5100 zcmeI0dpOkj9>>2kjC¹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 mUqU{?y3fnSfA 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 HKym*9ydRy= diff --git a/test/constraint/equal_line_arc_len/normal.png b/test/constraint/equal_line_arc_len/normal.png index 3031ca662e72287fa03f4bf49e26f3891df34efa..bb0e2181d2d072cefd33f7dbe78a003f677895ef 100644 GIT binary patch literal 4554 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 diff --git a/test/constraint/equal_line_arc_len/pi.png b/test/constraint/equal_line_arc_len/pi.png index e7bccc7ada376f53fdc0182ba1e8fb09d7e77d0c..9207fcaa1730559938cb49e3986d5b548344f966 100644 GIT binary patch literal 4672 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 literal 4651 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 literal 4970 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 P3=QlcdAO8*MP>dcn#))P diff --git a/test/constraint/equal_radius/normal.png b/test/constraint/equal_radius/normal.png index 22144dfae358b944163d5b1f6b5d12d8d9813edd..c7f8e797d10020ffc2aa995e8f5cb7a59ef78cd5 100644 GIT binary patch literal 5249 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 literal 5255 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 NxMhcZ;U+@D{{rMk9j^cY diff --git a/test/constraint/pt_on_circle/normal.png b/test/constraint/pt_on_circle/normal.png index ef105be82c4e2987bda30ec5cda4f8210706f0e1..fe0734c2bbfb8bb60b5797d4541fc21092dcaef4 100644 GIT binary patch delta 2951 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 diff --git a/test/constraint/pt_on_face/normal.png b/test/constraint/pt_on_face/normal.png index 62080a07651cc77d4d7dfc6672d0cb4587a35618..1f7a7e895a9b2d4c3d5a34466ee6b6fcb7877ad8 100644 GIT binary patch literal 4661 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 literal 4663 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 GN%}WxDu(I+ diff --git a/test/group/translate_asy/test.cpp b/test/group/translate_asy/test.cpp index f1d22278..957dccef 100644 --- a/test/group/translate_asy/test.cpp +++ b/test/group/translate_asy/test.cpp @@ -15,6 +15,7 @@ TEST_CASE(normal_inters) { CHECK_LOAD("normal.slvs"); Group *g = SK.GetGroup(SS.GW.activeGroup); + g->GenerateDisplayItems(); SMesh *m = &g->displayMesh; SEdgeList el = {}; diff --git a/test/harness.cpp b/test/harness.cpp index 70f340c6..09635dee 100644 --- a/test/harness.cpp +++ b/test/harness.cpp @@ -17,8 +17,6 @@ namespace SolveSpace { // These are defined in headless.cpp, and aren't exposed in solvespace.h. extern std::vector fontFiles; - extern bool antialias; - extern std::shared_ptr framebuffer; } // The paths in __FILE__ are from the build system, but defined(WIN32) returns @@ -237,24 +235,45 @@ bool Test::Helper::CheckSave(const char *file, int line, const char *reference) } bool Test::Helper::CheckRender(const char *file, int line, const char *reference) { - PaintGraphics(); + // First, render to a framebuffer. + Camera camera = {}; + camera.pixelRatio = 1; + camera.gridFit = true; + camera.width = 600; + camera.height = 600; + camera.projUp = SS.GW.projUp; + camera.projRight = SS.GW.projRight; + camera.scale = SS.GW.scale; + CairoPixmapRenderer pixmapCanvas; + pixmapCanvas.SetLighting(SS.GW.GetLighting()); + pixmapCanvas.SetCamera(camera); + pixmapCanvas.Init(); + + pixmapCanvas.NewFrame(); + SS.GW.Draw(&pixmapCanvas); + pixmapCanvas.FlushFrame(); + std::shared_ptr frame = pixmapCanvas.ReadFrame(); + + pixmapCanvas.Clear(); + + // Now, diff framebuffer against reference render. Platform::Path refPath = GetAssetPath(file, reference), outPath = GetAssetPath(file, reference, "out"), diffPath = GetAssetPath(file, reference, "diff"); std::shared_ptr refPixmap = Pixmap::ReadPng(refPath, /*flip=*/true); - if(!RecordCheck(refPixmap && refPixmap->Equals(*framebuffer))) { - framebuffer->WritePng(outPath, /*flip=*/true); + if(!RecordCheck(refPixmap && refPixmap->Equals(*frame))) { + frame->WritePng(outPath, /*flip=*/true); if(!refPixmap) { PrintFailure(file, line, "reference render not present"); return false; } - ssassert(refPixmap->format == framebuffer->format, "Expected buffer formats to match"); - if(refPixmap->width != framebuffer->width || - refPixmap->height != framebuffer->height) { + ssassert(refPixmap->format == frame->format, "Expected buffer formats to match"); + if(refPixmap->width != frame->width || + refPixmap->height != frame->height) { PrintFailure(file, line, "render doesn't match reference; dimensions differ"); } else { std::shared_ptr diffPixmap = @@ -263,7 +282,7 @@ bool Test::Helper::CheckRender(const char *file, int line, const char *reference int diffPixelCount = 0; for(size_t j = 0; j < refPixmap->height; j++) { for(size_t i = 0; i < refPixmap->width; i++) { - if(!refPixmap->GetPixel(i, j).Equals(framebuffer->GetPixel(i, j))) { + if(!refPixmap->GetPixel(i, j).Equals(frame->GetPixel(i, j))) { diffPixelCount++; diffPixmap->SetPixel(i, j, RgbaColor::From(255, 0, 0, 255)); } @@ -321,9 +340,6 @@ int main(int argc, char **argv) { fontFiles.push_back(HostRoot().Join("Gentium-R.ttf")); - // Different Cairo versions have different antialiasing algorithms. - antialias = false; - // Wreck order dependencies between tests! std::random_shuffle(testCasesPtr->begin(), testCasesPtr->end()); diff --git a/test/request/arc_of_circle/normal.png b/test/request/arc_of_circle/normal.png index 766cb15e57dd1d60c4b36615c2427fa9b2efa87c..e006a1d84bd0ad4492fea73f2396098fddcd796d 100644 GIT binary patch literal 4569 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 aT8Jpu{VK3Rp&9u?1B?yK^)tUD2mb*eLSQ`r diff --git a/test/request/circle/free_in_3d.png b/test/request/circle/free_in_3d.png index c735d860f5b99e73b06ee72763c0cf927cb7eaf2..86257e073098c089a4b0acaeff1e7d2d6d60a86e 100644 GIT binary patch literal 4803 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 IXbVUG8>sr@0ssI2 literal 4806 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 Y;0?!2ukJTS=2KvkgX_i;dveOZ0F?gcXaE2J diff --git a/test/request/circle/free_in_3d_dof.png b/test/request/circle/free_in_3d_dof.png index 010296d21b404a589db045806d1f754a4182e85b..0a4422b0e2ad5020f922bc3613a65f385db408f2 100644 GIT binary patch literal 7197 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 literal 7197 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 IXbVUG8>sr@0ssI2 literal 4806 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 Y;0?!2ukJTS=2KvkgX_i;dveOZ0F?gcXaE2J diff --git a/test/request/circle/normal_dof.png b/test/request/circle/normal_dof.png index baf11cfb298917f606831c358748c657c82f93ff..a032f1aa3105641d6c7ac4b6dde80b94461c1a01 100644 GIT binary patch literal 4836 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 x#go1*;D8Pd%c#{2KrO literal 4820 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 diff --git a/test/request/cubic/normal.png b/test/request/cubic/normal.png index 20d23502caa38111867ff3a972600e8473949dc0..c4fc05047440a458dcb706f9e18230ab8ea34eb8 100644 GIT binary patch literal 4947 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 literal 4976 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 delta 3514 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 diff --git a/test/request/ttf_text/normal.png b/test/request/ttf_text/normal.png index 39f789ef420e6155e2a3aae630f2056abafb44fc..9aa4d185373e07540cd3ebb2985cc867c8f8072e 100644 GIT binary patch delta 1729 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