From 500424a4b259650ad120e50c2e2807375631fe4e Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 13:53:28 +0100 Subject: [PATCH 1/8] Correctly enable/disable zoom items --- src/gui/gameframe.cc | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/gui/gameframe.cc b/src/gui/gameframe.cc index dbd4d61da..fa97b99e4 100644 --- a/src/gui/gameframe.cc +++ b/src/gui/gameframe.cc @@ -342,8 +342,16 @@ void GameFrame::OnUpdate() } menuBar->Check(GBT_MENU_VIEW_PROFILES, m_splitter->IsSplit()); GetToolBar()->ToggleTool(GBT_MENU_VIEW_PROFILES, m_splitter->IsSplit()); - menuBar->Enable(GBT_MENU_VIEW_ZOOMIN, m_efgPanel && m_efgPanel->IsShown()); - menuBar->Enable(GBT_MENU_VIEW_ZOOMOUT, m_efgPanel && m_efgPanel->IsShown()); + + const bool canZoomTree = m_efgPanel && m_efgPanel->IsShown(); + menuBar->Enable(GBT_MENU_VIEW_ZOOMIN, canZoomTree); + menuBar->Enable(GBT_MENU_VIEW_ZOOMOUT, canZoomTree); + menuBar->Enable(GBT_MENU_VIEW_ZOOMFIT, canZoomTree); + menuBar->Enable(GBT_MENU_VIEW_ZOOM100, canZoomTree); + + GetToolBar()->EnableTool(GBT_MENU_VIEW_ZOOMIN, canZoomTree); + GetToolBar()->EnableTool(GBT_MENU_VIEW_ZOOMOUT, canZoomTree); + GetToolBar()->EnableTool(GBT_MENU_VIEW_ZOOMFIT, canZoomTree); } //-------------------------------------------------------------------- @@ -1112,15 +1120,19 @@ void GameFrame::OnViewStrategic(wxCommandEvent &p_event) m_efgPanel->SetFocus(); } + const bool canZoomTree = m_efgPanel && m_efgPanel->IsShown(); + GetMenuBar()->Check(GBT_MENU_VIEW_STRATEGIC, m_nfgPanel->IsShown()); - GetMenuBar()->Enable(GBT_MENU_VIEW_ZOOMIN, !p_event.IsChecked()); - GetMenuBar()->Enable(GBT_MENU_VIEW_ZOOMOUT, !p_event.IsChecked()); + GetMenuBar()->Enable(GBT_MENU_VIEW_ZOOMIN, canZoomTree); + GetMenuBar()->Enable(GBT_MENU_VIEW_ZOOMOUT, canZoomTree); + GetMenuBar()->Enable(GBT_MENU_VIEW_ZOOMFIT, canZoomTree); + GetMenuBar()->Enable(GBT_MENU_VIEW_ZOOM100, canZoomTree); GetMenuBar()->Enable(GBT_MENU_TOOLS_DOMINANCE, m_nfgPanel->IsShown()); GetToolBar()->ToggleTool(GBT_MENU_VIEW_STRATEGIC, p_event.IsChecked()); - GetToolBar()->EnableTool(GBT_MENU_VIEW_ZOOMIN, !p_event.IsChecked()); - GetToolBar()->EnableTool(GBT_MENU_VIEW_ZOOMOUT, !p_event.IsChecked()); - GetToolBar()->EnableTool(GBT_MENU_VIEW_ZOOMFIT, !p_event.IsChecked()); + GetToolBar()->EnableTool(GBT_MENU_VIEW_ZOOMIN, canZoomTree); + GetToolBar()->EnableTool(GBT_MENU_VIEW_ZOOMOUT, canZoomTree); + GetToolBar()->EnableTool(GBT_MENU_VIEW_ZOOMFIT, canZoomTree); } //---------------------------------------------------------------------- From 6d9c07c0a81f4a89cca5b02f99ac9f53f38cb04b Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 13:55:20 +0100 Subject: [PATCH 2/8] Make zoom handler defensive, eliminate code duplication --- src/gui/gameframe.cc | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/gui/gameframe.cc b/src/gui/gameframe.cc index fa97b99e4..8b41af65d 100644 --- a/src/gui/gameframe.cc +++ b/src/gui/gameframe.cc @@ -1066,7 +1066,9 @@ void GameFrame::OnViewProfiles(wxCommandEvent &p_event) void GameFrame::OnViewZoom(wxCommandEvent &p_event) { // All zoom events get passed along to the panel - wxPostEvent(m_efgPanel, p_event); + if (m_efgPanel && m_efgPanel->IsShown()) { + wxPostEvent(m_efgPanel, p_event); + } } void GameFrame::OnViewStrategic(wxCommandEvent &p_event) @@ -1123,16 +1125,11 @@ void GameFrame::OnViewStrategic(wxCommandEvent &p_event) const bool canZoomTree = m_efgPanel && m_efgPanel->IsShown(); GetMenuBar()->Check(GBT_MENU_VIEW_STRATEGIC, m_nfgPanel->IsShown()); - GetMenuBar()->Enable(GBT_MENU_VIEW_ZOOMIN, canZoomTree); - GetMenuBar()->Enable(GBT_MENU_VIEW_ZOOMOUT, canZoomTree); - GetMenuBar()->Enable(GBT_MENU_VIEW_ZOOMFIT, canZoomTree); - GetMenuBar()->Enable(GBT_MENU_VIEW_ZOOM100, canZoomTree); GetMenuBar()->Enable(GBT_MENU_TOOLS_DOMINANCE, m_nfgPanel->IsShown()); GetToolBar()->ToggleTool(GBT_MENU_VIEW_STRATEGIC, p_event.IsChecked()); - GetToolBar()->EnableTool(GBT_MENU_VIEW_ZOOMIN, canZoomTree); - GetToolBar()->EnableTool(GBT_MENU_VIEW_ZOOMOUT, canZoomTree); - GetToolBar()->EnableTool(GBT_MENU_VIEW_ZOOMFIT, canZoomTree); + + OnUpdate(); } //---------------------------------------------------------------------- From 5e819bd9356864301abc77b89cac8d51593e94fd Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 13:59:55 +0100 Subject: [PATCH 3/8] Show accelerators in menu --- src/gui/gameframe.cc | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/gui/gameframe.cc b/src/gui/gameframe.cc index 8b41af65d..526f4a0da 100644 --- a/src/gui/gameframe.cc +++ b/src/gui/gameframe.cc @@ -249,7 +249,7 @@ GameFrame::GameFrame(wxWindow *p_parent, GameDocument *p_doc) MakeMenus(); MakeToolbar(); - wxAcceleratorEntry entries[8]; + wxAcceleratorEntry entries[10]; entries[0].Set(wxACCEL_CTRL, 'o', wxID_OPEN); entries[1].Set(wxACCEL_CTRL, 's', wxID_SAVE); entries[2].Set(wxACCEL_CTRL | wxACCEL_SHIFT, 's', wxID_SAVEAS); @@ -257,8 +257,10 @@ GameFrame::GameFrame(wxWindow *p_parent, GameDocument *p_doc) entries[4].Set(wxACCEL_CTRL, 'w', wxID_CLOSE); entries[5].Set(wxACCEL_CTRL, 'q', wxID_EXIT); entries[6].Set(wxACCEL_CTRL, '+', GBT_MENU_VIEW_ZOOMIN); - entries[7].Set(wxACCEL_CTRL, '-', GBT_MENU_VIEW_ZOOMOUT); - const wxAcceleratorTable accel(8, entries); + entries[7].Set(wxACCEL_CTRL, '=', GBT_MENU_VIEW_ZOOMIN); + entries[8].Set(wxACCEL_CTRL, '-', GBT_MENU_VIEW_ZOOMOUT); + entries[9].Set(wxACCEL_CTRL, '0', GBT_MENU_VIEW_ZOOM100); + const wxAcceleratorTable accel(10, entries); wxWindowBase::SetAcceleratorTable(accel); m_splitter = new wxSplitterWindow(this, wxID_ANY); @@ -480,12 +482,12 @@ void GameFrame::MakeMenus() viewMenu->Check(GBT_MENU_VIEW_PROFILES, false); viewMenu->AppendSeparator(); - AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOMIN, _("Zoom &in"), + AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOMIN, _("Zoom &in\tCtrl-+"), _("Increase display magnification"), wxBitmap(zoomin_xpm)); - AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOMOUT, _("Zoom &out"), + AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOMOUT, _("Zoom &out\tCtrl--"), _("Decrease display magnification"), wxBitmap(zoomout_xpm)); - AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOM100, _("&Zoom 1:1"), _("Set magnification to 1:1"), - wxBitmap(zoom1_xpm)); + AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOM100, _("&Zoom 1:1\tCtrl-0"), + _("Set magnification to 1:1"), wxBitmap(zoom1_xpm)); AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOMFIT, _("&Fit tree to window"), _("Rescale to show entire tree in window"), wxBitmap(zoomfit_xpm)); From a840e39b7001cc8a8eae6c7356fe0e33d5961d0e Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 14:04:13 +0100 Subject: [PATCH 4/8] Toolbar consistency --- src/gui/gameframe.cc | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/gui/gameframe.cc b/src/gui/gameframe.cc index 526f4a0da..260030245 100644 --- a/src/gui/gameframe.cc +++ b/src/gui/gameframe.cc @@ -482,13 +482,13 @@ void GameFrame::MakeMenus() viewMenu->Check(GBT_MENU_VIEW_PROFILES, false); viewMenu->AppendSeparator(); - AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOMIN, _("Zoom &in\tCtrl-+"), + AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOMIN, _("Zoom &In\tCtrl-+"), _("Increase display magnification"), wxBitmap(zoomin_xpm)); - AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOMOUT, _("Zoom &out\tCtrl--"), + AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOMOUT, _("Zoom &Out\tCtrl--"), _("Decrease display magnification"), wxBitmap(zoomout_xpm)); - AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOM100, _("&Zoom 1:1\tCtrl-0"), + AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOM100, _("&Actual Size\tCtrl-0"), _("Set magnification to 1:1"), wxBitmap(zoom1_xpm)); - AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOMFIT, _("&Fit tree to window"), + AppendBitmapItem(viewMenu, GBT_MENU_VIEW_ZOOMFIT, _("Zoom to &Fit"), _("Rescale to show entire tree in window"), wxBitmap(zoomfit_xpm)); viewMenu->AppendSeparator(); @@ -517,7 +517,7 @@ void GameFrame::MakeMenus() AppendBitmapItem(toolsMenu, GBT_MENU_TOOLS_EQUILIBRIUM, _("&Equilibrium"), _("Compute Nash equilibria and refinements"), wxBitmap(calc_xpm)); - toolsMenu->Append(GBT_MENU_TOOLS_QRE, _("&Qre"), _("Compute quantal response equilibria")); + toolsMenu->Append(GBT_MENU_TOOLS_QRE, _("&QRE"), _("Compute quantal response equilibria")); auto *helpMenu = new wxMenu; AppendBitmapItem(helpMenu, wxID_ABOUT, _("&About Gambit"), _("About Gambit"), @@ -573,9 +573,10 @@ void GameFrame::MakeToolbar() wxITEM_NORMAL, _("Zoom in"), _("Increase magnification"), nullptr); toolBar->AddTool(GBT_MENU_VIEW_ZOOMOUT, wxEmptyString, wxBitmap(zoomout_xpm), wxNullBitmap, wxITEM_NORMAL, _("Zoom out"), _("Decrease magnification"), nullptr); + toolBar->AddTool(GBT_MENU_VIEW_ZOOM100, wxEmptyString, wxBitmap(zoom1_xpm), wxNullBitmap, + wxITEM_NORMAL, _("Actual size"), _("Set magnification to 1:1"), nullptr); toolBar->AddTool(GBT_MENU_VIEW_ZOOMFIT, wxEmptyString, wxBitmap(zoomfit_xpm), wxNullBitmap, - wxITEM_NORMAL, _("Fit to window"), _("Set magnification to see entrie tree"), - nullptr); + wxITEM_NORMAL, _("Zoom to fit"), _("Fit the tree in the window"), nullptr); } toolBar->AddSeparator(); @@ -595,7 +596,7 @@ void GameFrame::MakeToolbar() _("Display the reduced strategic representation of the game"), nullptr); } toolBar->AddTool(GBT_MENU_VIEW_PROFILES, wxEmptyString, wxBitmap(profiles_xpm), wxNullBitmap, - wxITEM_NORMAL, _("View the list of computed strategy profiles"), + wxITEM_CHECK, _("View the list of computed strategy profiles"), _("Show or hide the list of computed strategy profiles"), nullptr); toolBar->AddTool(GBT_MENU_TOOLS_EQUILIBRIUM, wxEmptyString, wxBitmap(calc_xpm), wxNullBitmap, wxITEM_NORMAL, _("Compute Nash equilibria of this game"), From bc17cd98bbfb9144ce60835a01314fb253659a3f Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 14:09:04 +0100 Subject: [PATCH 5/8] Correct clamping of zoom --- src/gui/efgpanel.cc | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/gui/efgpanel.cc b/src/gui/efgpanel.cc index c9e1631e6..fb600410a 100644 --- a/src/gui/efgpanel.cc +++ b/src/gui/efgpanel.cc @@ -20,6 +20,8 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // +#include + #include #ifndef WX_PRECOMP #include @@ -500,22 +502,24 @@ EfgPanel::EfgPanel(wxWindow *p_parent, GameDocument *p_doc) wxWindowBase::Layout(); } +namespace { + +constexpr int kMinZoom = 10; +constexpr int kMaxZoom = 150; +constexpr int kZoomStep = 10; + +int ClampZoom(int p_zoom) { return std::clamp(p_zoom, kMinZoom, kMaxZoom); } + +} // namespace + void EfgPanel::OnViewZoomIn(wxCommandEvent &) { - int zoom = m_treeWindow->GetZoom(); - if (zoom < 150) { - zoom += 10; - } - m_treeWindow->SetZoom(zoom); + m_treeWindow->SetZoom(ClampZoom(m_treeWindow->GetZoom() + kZoomStep)); } void EfgPanel::OnViewZoomOut(wxCommandEvent &) { - int zoom = m_treeWindow->GetZoom(); - if (zoom > 10) { - zoom -= 10; - } - m_treeWindow->SetZoom(zoom); + m_treeWindow->SetZoom(ClampZoom(m_treeWindow->GetZoom() - kZoomStep)); } void EfgPanel::OnViewZoom100(wxCommandEvent &) { m_treeWindow->SetZoom(100); } From c562b594750c5199d2469c91fca2d9800f085948 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 14:22:06 +0100 Subject: [PATCH 6/8] Add support for magnify event. --- src/gui/efgdisplay.cc | 20 +++++++++++++++++++- src/gui/efgdisplay.h | 1 + 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/gui/efgdisplay.cc b/src/gui/efgdisplay.cc index 27d498834..8f8947641 100644 --- a/src/gui/efgdisplay.cc +++ b/src/gui/efgdisplay.cc @@ -313,6 +313,7 @@ BEGIN_EVENT_TABLE(EfgDisplay, wxScrolledWindow) EVT_MOTION(EfgDisplay::OnMouseMotion) EVT_LEFT_DOWN(EfgDisplay::OnLeftClick) EVT_LEFT_DCLICK(EfgDisplay::OnLeftDoubleClick) +EVT_MAGNIFY(EfgDisplay::OnMagnify) EVT_RIGHT_DOWN(EfgDisplay::OnRightClick) EVT_KEY_DOWN(EfgDisplay::OnKeyEvent) EVT_SIZE(EfgDisplay::OnSize) @@ -640,9 +641,19 @@ void EfgDisplay::FitZoom() Refresh(); } +namespace { + +constexpr int kMinZoom = 10; +constexpr int kMaxZoom = 150; +constexpr int kZoomStep = 10; + +int ClampZoom(int p_zoom) { return std::clamp(p_zoom, kMinZoom, kMaxZoom); } + +} // namespace + void EfgDisplay::SetZoom(int p_zoom) { - m_zoom = p_zoom; + m_zoom = ClampZoom(p_zoom); AdjustScrollbarSteps(); EnsureNodeVisible(m_doc->GetSelectNode()); Refresh(); @@ -873,6 +884,13 @@ void EfgDisplay::OnLeftDoubleClick(wxMouseEvent &p_event) } } +void EfgDisplay::OnMagnify(wxMouseEvent &p_event) +{ + if (const double factor = 1.0 + p_event.GetMagnification(); factor > 0.0) { + SetZoom(static_cast(std::lround(GetZoom() * factor))); + } +} + #include "bitmaps/tree.xpm" #include "bitmaps/move.xpm" diff --git a/src/gui/efgdisplay.h b/src/gui/efgdisplay.h index f9042c4e0..2f7223bea 100644 --- a/src/gui/efgdisplay.h +++ b/src/gui/efgdisplay.h @@ -70,6 +70,7 @@ class EfgDisplay final : public wxScrolledWindow, public GameView { void OnLeftClick(wxMouseEvent &); void OnRightClick(wxMouseEvent &); void OnLeftDoubleClick(wxMouseEvent &); + void OnMagnify(wxMouseEvent &); void OnKeyEvent(wxKeyEvent &); /// Payoff editor changes accepted with enter void OnAcceptPayoffEdit(wxCommandEvent &); From ffe7baf61f8a3b396cbe7256bb3e2016fd979933 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 14:25:01 +0100 Subject: [PATCH 7/8] Remove stray debug output --- src/gui/efgpanel.cc | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/gui/efgpanel.cc b/src/gui/efgpanel.cc index fb600410a..4767fb47b 100644 --- a/src/gui/efgpanel.cc +++ b/src/gui/efgpanel.cc @@ -609,8 +609,6 @@ void EfgPanel::RenderGame(wxDC &p_dc, int p_marginX, int p_marginY) auto posY = ((h - (maxY * scale)) / 2.0); p_dc.SetDeviceOrigin(static_cast(posX), static_cast(posY)); - printf("Drawing with scale %f\n", scale); - // Draw! m_treeWindow->OnDraw(p_dc, scale); } From 1dd360269bba46fab1f010f4c9173897ac932a5b Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 14:40:27 +0100 Subject: [PATCH 8/8] Make magnify event centred on position --- ChangeLog | 1 + src/gui/efgdisplay.cc | 95 +++++++++++++++++++++++++++++++++++++++---- src/gui/efgdisplay.h | 5 ++- 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/ChangeLog b/ChangeLog index 3d0aad983..836c13e68 100644 --- a/ChangeLog +++ b/ChangeLog @@ -5,6 +5,7 @@ ### Added - Implement `GameSubgameRep` (C++) and `Subgame` (Python), a first-class object representing a subgame. (#585) - Games can be materialised directly from OpenSpiel games if `pyspiel` is installed. (#917) +- Magnify events are now supported in GUI for zooming in/out on trees. ### Fixed - Corrected resizing of row and column index labels in strategic form so pivoting works correctly. (#844) diff --git a/src/gui/efgdisplay.cc b/src/gui/efgdisplay.cc index 8f8947641..b62c27cb9 100644 --- a/src/gui/efgdisplay.cc +++ b/src/gui/efgdisplay.cc @@ -613,16 +613,34 @@ void EfgDisplay::RefreshTree() Refresh(); } +constexpr int kScrollPixelsPerUnit = 1; + void EfgDisplay::AdjustScrollbarSteps() { - int width, height; - GetClientSize(&width, &height); + int oldPixelsPerUnitX, oldPixelsPerUnitY; + GetScrollPixelsPerUnit(&oldPixelsPerUnitX, &oldPixelsPerUnitY); int scrollX, scrollY; GetViewStart(&scrollX, &scrollY); - SetScrollbars(50, 50, LayoutToDevice(m_layout.MaxX()) / 50 + 1, - LayoutToDevice(m_layout.MaxY()) / 50 + 1, scrollX, scrollY); + const int currentPixelX = scrollX * oldPixelsPerUnitX; + const int currentPixelY = scrollY * oldPixelsPerUnitY; + + int clientWidth, clientHeight; + GetClientSize(&clientWidth, &clientHeight); + + const int virtualWidth = LayoutToDevice(m_layout.MaxX()); + const int virtualHeight = LayoutToDevice(m_layout.MaxY()); + + const int maxPixelX = std::max(0, virtualWidth - clientWidth); + const int maxPixelY = std::max(0, virtualHeight - clientHeight); + + const int clampedPixelX = std::clamp(currentPixelX, 0, maxPixelX); + const int clampedPixelY = std::clamp(currentPixelY, 0, maxPixelY); + + SetScrollbars(kScrollPixelsPerUnit, kScrollPixelsPerUnit, + virtualWidth / kScrollPixelsPerUnit + 1, virtualHeight / kScrollPixelsPerUnit + 1, + clampedPixelX / kScrollPixelsPerUnit, clampedPixelY / kScrollPixelsPerUnit); } void EfgDisplay::FitZoom() @@ -646,19 +664,80 @@ namespace { constexpr int kMinZoom = 10; constexpr int kMaxZoom = 150; constexpr int kZoomStep = 10; +constexpr int kScrollPixelsPerUnit = 1; int ClampZoom(int p_zoom) { return std::clamp(p_zoom, kMinZoom, kMaxZoom); } } // namespace -void EfgDisplay::SetZoom(int p_zoom) +void EfgDisplay::SetZoom(int p_zoom, bool p_keepSelectionVisible) { - m_zoom = ClampZoom(p_zoom); + const int zoom = ClampZoom(p_zoom); + if (zoom == m_zoom) { + return; + } + + m_zoom = zoom; AdjustScrollbarSteps(); - EnsureNodeVisible(m_doc->GetSelectNode()); + + if (p_keepSelectionVisible) { + EnsureNodeVisible(m_doc->GetSelectNode()); + } + Refresh(); } +void EfgDisplay::ZoomByFactor(double p_factor, const wxPoint &p_clientPoint) +{ + if (p_factor <= 0.0) { + return; + } + + const int oldZoom = GetZoom(); + const int newZoom = ClampZoom(static_cast(std::lround(oldZoom * p_factor))); + + if (newZoom == oldZoom) { + return; + } + + int unscrolledX, unscrolledY; + CalcUnscrolledPosition(p_clientPoint.x, p_clientPoint.y, &unscrolledX, &unscrolledY); + + const double oldScale = GetZoom() / 100.0; + const double layoutX = unscrolledX / oldScale; + const double layoutY = unscrolledY / oldScale; + + SetZoom(newZoom, false); + + const double newScale = GetZoom() / 100.0; + const int targetUnscrolledX = static_cast(std::lround(layoutX * newScale)); + const int targetUnscrolledY = static_cast(std::lround(layoutY * newScale)); + + int pixelsPerUnitX, pixelsPerUnitY; + GetScrollPixelsPerUnit(&pixelsPerUnitX, &pixelsPerUnitY); + + if (pixelsPerUnitX <= 0 || pixelsPerUnitY <= 0) { + return; + } + + const int targetScrollX = targetUnscrolledX - p_clientPoint.x; + const int targetScrollY = targetUnscrolledY - p_clientPoint.y; + + int clientWidth, clientHeight; + GetClientSize(&clientWidth, &clientHeight); + + int virtualWidth, virtualHeight; + GetVirtualSize(&virtualWidth, &virtualHeight); + + const int maxPixelX = std::max(0, virtualWidth - clientWidth); + const int maxPixelY = std::max(0, virtualHeight - clientHeight); + + const int clampedScrollX = std::clamp(targetScrollX, 0, maxPixelX); + const int clampedScrollY = std::clamp(targetScrollY, 0, maxPixelY); + + Scroll(clampedScrollX / pixelsPerUnitX, clampedScrollY / pixelsPerUnitY); +} + void EfgDisplay::OnDraw(wxDC &p_dc) { p_dc.SetUserScale(GetScale(), GetScale()); @@ -887,7 +966,7 @@ void EfgDisplay::OnLeftDoubleClick(wxMouseEvent &p_event) void EfgDisplay::OnMagnify(wxMouseEvent &p_event) { if (const double factor = 1.0 + p_event.GetMagnification(); factor > 0.0) { - SetZoom(static_cast(std::lround(GetZoom() * factor))); + ZoomByFactor(factor, p_event.GetPosition()); } } diff --git a/src/gui/efgdisplay.h b/src/gui/efgdisplay.h index 2f7223bea..3f10e8d69 100644 --- a/src/gui/efgdisplay.h +++ b/src/gui/efgdisplay.h @@ -59,7 +59,6 @@ class EfgDisplay final : public wxScrolledWindow, public GameView { TreePayoffEditor *m_payoffEditor; bool m_pendingInitialZoom{true}; - // Private Functions void MakeMenus(); void AdjustScrollbarSteps(); @@ -87,6 +86,8 @@ class EfgDisplay final : public wxScrolledWindow, public GameView { /// @brief Scroll the viewport such that the node is at the specified fraction of the viewport void FocusNode(const GameNode &p_node, double p_xFrac = 0.5, double p_yFrac = 0.5); + void ZoomByFactor(double p_factor, const wxPoint &p_clientPoint); + public: EfgDisplay(wxWindow *p_parent, GameDocument *p_doc); @@ -94,7 +95,7 @@ class EfgDisplay final : public wxScrolledWindow, public GameView { void OnDraw(wxDC &, double); int GetZoom() const { return m_zoom; } - void SetZoom(int p_zoom); + void SetZoom(int p_zoom, bool p_keepSelectionVisible = true); void FitZoom(); double GetScale() const { return 0.01 * m_zoom; }