Skip to content

refactor(ui): implement granular map repaint reasons - #1015

Merged
karolak6612 merged 5 commits into
masterfrom
Animator-fix
Apr 19, 2026
Merged

refactor(ui): implement granular map repaint reasons#1015
karolak6612 merged 5 commits into
masterfrom
Animator-fix

Conversation

@karolak6612

@karolak6612 karolak6612 commented Apr 19, 2026

Copy link
Copy Markdown
Collaborator

Replaced generic canvas refreshes with a flag-based repaint system to optimize rendering performance. Introduced granular reasons for updates, including viewport shifts, interaction overlays, and throttled animation ticks.

  • source/game/animation_timer.cpp — switched to RequestAnimationRepaint() for throttled animation updates
  • source/rendering/ui/drawing_controller.cpp — utilized InteractionOverlayChanged flag for drag-related refreshes
  • source/rendering/ui/keyboard_handler.cpp — mapped keyboard actions to specific hover, interaction, or viewport repaint reasons
  • source/rendering/ui/map_display.cpp — implemented RequestRepaint logic with animation throttling and overlay state validation
  • source/rendering/ui/map_display.h — added RepaintReason enum and members for tracking repaint flags and animation timing
  • source/rendering/ui/navigation_controller.cpp — replaced generic refreshes with ViewportChanged repaint requests
  • source/rendering/ui/selection_controller.cpp — categorized selection dragging under InteractionOverlayChanged repaint reason
  • source/rendering/ui/zoom_controller.cpp — transitioned zoom operations to use ViewportChanged flag
  • source/ui/gui.cpp — updated RefreshView to skip redundant refreshes for unrelated map tabs
  • source/ui/map_window.cpp — updated scroll event handlers to request ViewportChanged repaints

Summary by CodeRabbit

  • Performance
    • Optimized canvas refresh system with targeted repaint requests instead of full-window refreshes.
    • Improved animation preview handling with rate-limited repaints based on zoom level.
    • Enhanced hover-preview state management for better responsiveness during interactive operations.
    • Streamlined refresh behavior during keyboard input, mouse interactions, and scrolling.

Replaced generic canvas refreshes with a flag-based repaint system to optimize rendering performance. Introduced granular reasons for updates, including viewport shifts, interaction overlays, and throttled animation ticks.
- `source/game/animation_timer.cpp` — switched to `RequestAnimationRepaint()` for throttled animation updates
- `source/rendering/ui/drawing_controller.cpp` — utilized `InteractionOverlayChanged` flag for drag-related refreshes
- `source/rendering/ui/keyboard_handler.cpp` — mapped keyboard actions to specific hover, interaction, or viewport repaint reasons
- `source/rendering/ui/map_display.cpp` — implemented `RequestRepaint` logic with animation throttling and overlay state validation
- `source/rendering/ui/map_display.h` — added `RepaintReason` enum and members for tracking repaint flags and animation timing
- `source/rendering/ui/navigation_controller.cpp` — replaced generic refreshes with `ViewportChanged` repaint requests
- `source/rendering/ui/selection_controller.cpp` — categorized selection dragging under `InteractionOverlayChanged` repaint reason
- `source/rendering/ui/zoom_controller.cpp` — transitioned zoom operations to use `ViewportChanged` flag
- `source/ui/gui.cpp` — updated `RefreshView` to skip redundant refreshes for unrelated map tabs
- `source/ui/map_window.cpp` — updated scroll event handlers to request `ViewportChanged` repaints
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a granular repaint mechanism for the map UI to optimize rendering performance. By categorizing update triggers into specific reasons, the system can now avoid redundant full-canvas refreshes, particularly during animations and interaction events. This change significantly improves the responsiveness of the UI by ensuring that only necessary components are re-rendered based on the context of the user's actions.

Highlights

  • Granular Repaint System: Implemented a flag-based RepaintReason system to replace generic canvas refreshes, allowing for more efficient rendering updates.
  • Performance Optimization: Introduced animation throttling and conditional repaint logic to reduce unnecessary rendering cycles.
  • UI Refinement: Updated various UI controllers to request specific repaint types (Viewport, Interaction, Hover) instead of triggering full view refreshes.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions

Copy link
Copy Markdown

🤖 Hi @karolak6612, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@karolak6612 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 40 minutes and 37 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 40 minutes and 37 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b25d0d57-123d-49e4-a4d0-b31422ae0528

📥 Commits

Reviewing files that changed from the base of the PR and between 2c8d186 and 90c271d.

📒 Files selected for processing (14)
  • source/brushes/managers/brush_manager.cpp
  • source/editor/managers/editor_manager.cpp
  • source/rendering/ui/drawing_controller.cpp
  • source/rendering/ui/keyboard_handler.cpp
  • source/rendering/ui/map_display.cpp
  • source/rendering/ui/map_display.h
  • source/rendering/ui/navigation_controller.cpp
  • source/rendering/ui/screenshot_controller.cpp
  • source/rendering/ui/selection_controller.cpp
  • source/rendering/ui/zoom_controller.cpp
  • source/ui/gui.cpp
  • source/ui/gui.h
  • source/ui/gui_autoborder_ext.cpp
  • source/ui/map_window.cpp
📝 Walkthrough

Walkthrough

This PR replaces direct/unconditional canvas refreshes with a reasoned repaint-request system: view invalidation state, reason-tagged requests, centralized evaluation/throttling (including animation-rate logic), and targeted flush/queueing APIs used across controllers and UI entry points.

Changes

Cohort / File(s) Summary
Core Repaint System
source/rendering/ui/map_display.h, source/rendering/ui/map_display.cpp, source/rendering/ui/view_invalidation_state.h, source/rendering/ui/repaint_policy.h, source/rendering/ui/repaint_policy.cpp, source/CMakeLists.txt
Introduced view invalidation state, RepaintReason/RefreshScope types, repaint decision/evaluation and animation-interval logic, and MapCanvas APIs: MarkInvalid, RequestLocalRefresh, RequestSharedMapRefresh, RequestAnimationRepaint, FlushRepaintRequest, SetHoverPreviewActive, plus native refresh queueing and invalidation bookkeeping.
Controller & Input Callsites
source/rendering/ui/navigation_controller.cpp, source/rendering/ui/zoom_controller.cpp, source/rendering/ui/selection_controller.cpp, source/rendering/ui/drawing_controller.cpp, source/rendering/ui/keyboard_handler.cpp, source/rendering/ui/map_display.cpp
Replaced direct Refresh()/g_gui.RefreshView() calls with targeted invalidation calls (MarkInvalid(...)) and flushes (FlushRepaintRequest()), using specific RepaintReason values per interaction (viewport, interaction overlay, hover, map content, animation).
Window / UI Integration
source/ui/gui.cpp, source/ui/gui.h, source/ui/gui_autoborder_ext.cpp, source/ui/map_window.cpp
Updated GUI refresh API to accept a RepaintReason + immediate flag, added SyncCurrentMapCanvasPreviewState(), and changed tab/canvas refresh logic to use the new Request/MarkInvalid/Flush pipeline for scrolls, mode switches, autoborder and preview state changes.
Animation Timer
source/game/animation_timer.cpp
AnimationTimer::Notify() now calls map_canvas->RequestAnimationRepaint() unconditionally (delegating rate control to MapCanvas/repaint policy) instead of conditionally calling map_canvas->Refresh() based on zoom.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Controller
    participant GUI as GUI
    participant Canvas as MapCanvas
    participant Policy as RepaintPolicy
    participant Native as Native Renderer

    User->>GUI: input event (drag/scroll/keypress/animation tick)
    GUI->>Canvas: MarkInvalid(reason) / RequestLocalRefresh(reason, immediate)
    Canvas->>Canvas: accumulate pending_reasons, update invalidation_state
    Canvas->>Policy: EvaluateRepaintRequest(state, pending_reasons, immediate, now)
    Policy-->>Canvas: RepaintDecision (should_refresh, allowed_reasons, interval)
    alt should_refresh == true
        Canvas->>Native: QueueNativeRefresh(immediate?)
        Native->>Canvas: OnPaint()
        Canvas->>Native: Render using allowed_reasons
        Canvas->>Canvas: clear pending_reasons, update last_animation_refresh
    else should_refresh == false
        Canvas-->>User: throttle / defer repaint
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I thumped my foot and nudged a tick,
The canvas listened, quiet and quick.
Reasons gathered, flags in line,
Throttle hums, refreshes fine.
A hop, a repaint — pixels sing! 🎨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately describes the main change: refactoring the UI layer to implement granular repaint reasons instead of generic canvas refreshes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Animator-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

## 📋 Review Summary

This PR introduces a granular repaint system for the map canvas to optimize rendering performance by categorizing update reasons and throttling animation ticks. While the architectural direction is excellent, there is a critical issue where hover-dependent previews (like brushes) may stop updating, and a logic flaw in the throttling mechanism that could delay unrelated repaint requests.

🔍 General Feedback

  • Animation Throttling: The implementation of throttled animation updates is a great addition for performance, especially when zoomed out.
  • Tab-Aware Refreshing: The optimization in GUI::RefreshView correctly avoids repainting unrelated map tabs, which is a significant improvement for multi-tab workflows.
  • Refinement Needed: The RequestRepaint logic needs to be more robust when handling multiple flags simultaneously to ensure no critical updates are skipped due to throttling of a single reason.


bool MapCanvas::IsAnimationEnabled() const {
return g_settings.getBoolean(Config::SHOW_PREVIEW);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 HasHoverDependentPreview() is too restrictive. It should also return true if an interactive brush is selected, as the brush preview overlay needs to be repainted when the mouse moves. Without this, the brush preview will not update its position during hovering.

Suggested change
}
bool MapCanvas::HasHoverDependentPreview() const {
if (isPasting() || g_gui.GetSelectedBrush()) {
return true;
}
if (auto* map_tab = dynamic_cast<MapTab*>(GetMapWindow())) {
return map_tab->GetSession()->secondary_map != nullptr;
}
return false;
}

Comment thread source/rendering/ui/map_display.cpp Outdated

void MapCanvas::QueueNativeRefresh(bool immediate) {
(void)immediate;
if (refresh_watch.Time() > g_settings.getInteger(Config::HARD_REFRESH_RATE)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 The immediate parameter is currently unused. It should likely be used to bypass the HARD_REFRESH_RATE throttling or to decide whether to call wxGLCanvas::Update() (immediate) vs wxGLCanvas::Refresh() (queued).

Suggested change
if (refresh_watch.Time() > g_settings.getInteger(Config::HARD_REFRESH_RATE)) {
void MapCanvas::QueueNativeRefresh(bool immediate) {
if (immediate || refresh_watch.Time() > g_settings.getInteger(Config::HARD_REFRESH_RATE)) {
refresh_watch.Start();
wxGLCanvas::Update();
}
wxGLCanvas::Refresh();
}

Comment thread source/rendering/ui/map_display.cpp Outdated
wxGLCanvas::Refresh();
}

void MapCanvas::RequestRepaint(RepaintReason reason, bool immediate) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Returning early when an AnimationTick is throttled can cause other flags in the same reason bitmask to be ignored, as QueueNativeRefresh is skipped. The check should be adjusted to only return if AnimationTick is the only reason or if all reasons are throttled.

Suggested change
void MapCanvas::RequestRepaint(RepaintReason reason, bool immediate) {
void MapCanvas::RequestRepaint(RepaintReason reason, bool immediate) {
repaint_flags_ |= static_cast<uint32_t>(reason);
bool should_refresh = !HasAnyFlag(reason, RepaintReason::AnimationTick);
if (!should_refresh) {
if (!IsAnimationEnabled()) {
repaint_flags_ &= ~static_cast<uint32_t>(RepaintReason::AnimationTick);
} else {
const long long now = wxGetUTCTimeMillis().GetValue();
if (now - last_animation_refresh_ms_ >= GetAnimationRefreshIntervalMs()) {
last_animation_refresh_ms_ = now;
should_refresh = true;
}
}
}
if (should_refresh || (repaint_flags_ & ~static_cast<uint32_t>(RepaintReason::AnimationTick)) != 0) {
QueueNativeRefresh(immediate);
}
}

Comment thread source/rendering/ui/map_display.h Outdated
class MapMenuHandler;

class MapCanvas : public wxGLCanvas {
public:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Using an enum class for bitmask flags leads to repetitive static_cast calls. Consider adding bitwise operator overloads for RepaintReason to improve readability and maintainability.

Suggested change
public:
enum class RepaintReason : uint32_t {
None = 0,
ViewportChanged = 1u << 0,
MapContentChanged = 1u << 1,
HoverOverlayChanged = 1u << 2,
InteractionOverlayChanged = 1u << 3,
AnimationTick = 1u << 4,
};
friend constexpr RepaintReason operator|(RepaintReason a, RepaintReason b) {
return static_cast<RepaintReason>(static_cast<uint32_t>(a) | static_cast<uint32_t>(b));
}
friend constexpr RepaintReason operator&(RepaintReason a, RepaintReason b) {
return static_cast<RepaintReason>(static_cast<uint32_t>(a) & static_cast<uint32_t>(b));
}

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a granular repaint system for MapCanvas using RepaintReason flags to optimize rendering updates. Key changes include the implementation of RequestRepaint, which allows for specific updates like viewport changes or animation ticks, and the addition of logic to throttle animation refreshes based on zoom levels. Feedback includes a correction for a dynamic_cast that would otherwise fail, a suggestion to implement the ignored immediate parameter in QueueNativeRefresh, and recommendations to use C++23's std::to_underlying for enum operations and std::chrono::steady_clock for timing to align with the project's mandatory modern C++ standards.

Comment thread source/rendering/ui/map_display.cpp Outdated
return true;
}

if (auto* map_tab = dynamic_cast<MapTab*>(GetMapWindow())) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The dynamic_cast here will always fail because MapWindow is a wxPanel (as defined in map_window.cpp), not a MapTab. MapTab is the parent of the MapWindow. You should cast the parent of the window instead.

	if (auto* map_tab = dynamic_cast<MapTab*>(GetMapWindow()->GetParent())) {

Comment on lines +214 to 221
void MapCanvas::QueueNativeRefresh(bool immediate) {
(void)immediate;
if (refresh_watch.Time() > g_settings.getInteger(Config::HARD_REFRESH_RATE)) {
refresh_watch.Start();
wxGLCanvas::Update();
}
wxGLCanvas::Refresh();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The immediate parameter is currently ignored. If immediate is true, the canvas should call wxGLCanvas::Update() to force an immediate repaint and bypass the HARD_REFRESH_RATE throttle.

void MapCanvas::QueueNativeRefresh(bool immediate) {
	if (immediate || refresh_watch.Time() > g_settings.getInteger(Config::HARD_REFRESH_RATE)) {
		refresh_watch.Start();
		wxGLCanvas::Update();
	}
	wxGLCanvas::Refresh();
}

Comment thread source/rendering/ui/map_display.cpp Outdated
Comment on lines +178 to +180
bool MapCanvas::HasAnyFlag(RepaintReason value, RepaintReason flag) const {
return (static_cast<uint32_t>(value) & static_cast<uint32_t>(flag)) != 0;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

To adhere to the C++20/23 mandatory standard (Rule 7 & 80), use std::to_underlying from <utility> instead of manual static_cast for enum class bitwise operations.

bool MapCanvas::HasAnyFlag(RepaintReason value, RepaintReason flag) const {
	return (std::to_underlying(value) & std::to_underlying(flag)) != 0;
}
References
  1. Every line of code you write or modify MUST use C++20/23 features. Pre-C++17 patterns are technical debt. (link)

Comment thread source/rendering/ui/map_display.cpp Outdated
return;
}

const long long now = wxGetUTCTimeMillis().GetValue();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

Prefer using std::chrono::steady_clock for interval timing instead of wxGetUTCTimeMillis(). Steady clocks are monotonic and unaffected by system time adjustments, making them safer for throttling logic. Also, use auto for the type as it is obvious from the assignment (Rule 308).

		const auto now = std::chrono::steady_clock::now().time_since_epoch();
		const auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
		if (now_ms - last_animation_refresh_ms_ < GetAnimationRefreshIntervalMs()) {
			return;
		}

		last_animation_refresh_ms_ = now_ms;
References
  1. Use auto where type is obvious. Eliminate pre-C++17 patterns (like legacy time helpers) on contact. (link)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
source/rendering/ui/navigation_controller.cpp (1)

95-108: Optional: only request a viewport repaint when the floor actually changes.

RequestRepaint(ViewportChanged) at line 107 runs unconditionally, even when old_floor == new_floor (e.g., mouse-wheel floor change at the bottom/top, or repeated calls from wheel accumulation). Moving it inside the existing guard avoids a redundant full-viewport repaint on no-op calls.

♻️ Proposed refactor
 	if (old_floor != new_floor) {
 		if (auto* map_window = dynamic_cast<MapWindow*>(canvas->GetParent())) {
 			map_window->ResumeMinimapTrackingToCurrentView();
 		}
 		canvas->UpdatePositionStatus();
 		g_gui.root->UpdateFloorMenu();
 		g_gui.UpdateMinimap(true);
+		canvas->RequestRepaint(MapCanvas::RepaintReason::ViewportChanged);
 	}
-	canvas->RequestRepaint(MapCanvas::RepaintReason::ViewportChanged);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@source/rendering/ui/navigation_controller.cpp` around lines 95 - 108, The
RequestRepaint call is made unconditionally in NavigationController::ChangeFloor
causing unnecessary full viewport repaints even when old_floor == new_floor;
move the call to
canvas->RequestRepaint(MapCanvas::RepaintReason::ViewportChanged) inside the
existing if (old_floor != new_floor) block (alongside UpdatePositionStatus(),
g_gui.root->UpdateFloorMenu(), and g_gui.UpdateMinimap(true)) so a viewport
repaint only occurs when the floor actually changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@source/rendering/ui/map_display.cpp`:
- Around line 186-196: MapCanvas::HasHoverDependentPreview currently only
returns true for pasting and a secondary_map in MapTab, causing normal
brush/autoborder previews to stop following the cursor; update
HasHoverDependentPreview to also return true when the canvas is in a
drawing/brush/autoborder preview state by querying the canvas/tool state (e.g.
check the current tool or a method like IsDrawingMode/HasBrushPreview or similar
on MapCanvas or its tool manager) so OnMouseMove() and SyncCursorHoverState()
will repaint while brush/autoborder previews are active; keep existing
isPasting() and MapTab/GetSession()->secondary_map checks and add the
drawing-preview check before returning false.
- Around line 223-240: MapCanvas::RequestRepaint currently bails out early when
RepaintReason contains AnimationTick, which drops any other simultaneous flags;
instead, only remove the AnimationTick bit when animation is disabled or
throttled and continue to call QueueNativeRefresh for the remaining reasons.
Concretely: in MapCanvas::RequestRepaint, replace the early returns inside the
IsAnimationEnabled() check and the throttling check so that you clear
repaint_flags_ &= ~RepaintReason::AnimationTick when animation is disabled or
when now - last_animation_refresh_ms_ < GetAnimationRefreshIntervalMs(), and
only return early when you intend to suppress all reasons (which you don’t
here); if the tick is allowed, update last_animation_refresh_ms_ as now; always
fall through to QueueNativeRefresh(immediate) so non-animation repaint reasons
still trigger a refresh.
- Around line 214-221: MapCanvas::QueueNativeRefresh currently ignores the
immediate parameter and calls Update() before Refresh(), which violates
wxWidgets contract; change the logic to honor immediate and ensure Refresh() is
called before any Update(): always call wxGLCanvas::Refresh(), then if
immediate==true or refresh_watch.Time() >
g_settings.getInteger(Config::HARD_REFRESH_RATE) call refresh_watch.Start() and
wxGLCanvas::Update(); remove the (void)immediate cast and reference
MapCanvas::QueueNativeRefresh and refresh_watch so the fix is applied in that
function.

---

Nitpick comments:
In `@source/rendering/ui/navigation_controller.cpp`:
- Around line 95-108: The RequestRepaint call is made unconditionally in
NavigationController::ChangeFloor causing unnecessary full viewport repaints
even when old_floor == new_floor; move the call to
canvas->RequestRepaint(MapCanvas::RepaintReason::ViewportChanged) inside the
existing if (old_floor != new_floor) block (alongside UpdatePositionStatus(),
g_gui.root->UpdateFloorMenu(), and g_gui.UpdateMinimap(true)) so a viewport
repaint only occurs when the floor actually changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d82e5daa-91cf-40e1-8a50-c2bd822d359b

📥 Commits

Reviewing files that changed from the base of the PR and between 4c3369e and 845009c.

📒 Files selected for processing (10)
  • source/game/animation_timer.cpp
  • source/rendering/ui/drawing_controller.cpp
  • source/rendering/ui/keyboard_handler.cpp
  • source/rendering/ui/map_display.cpp
  • source/rendering/ui/map_display.h
  • source/rendering/ui/navigation_controller.cpp
  • source/rendering/ui/selection_controller.cpp
  • source/rendering/ui/zoom_controller.cpp
  • source/ui/gui.cpp
  • source/ui/map_window.cpp

Comment thread source/rendering/ui/map_display.cpp Outdated
Comment on lines +186 to +196
bool MapCanvas::HasHoverDependentPreview() const {
if (isPasting()) {
return true;
}

if (auto* map_tab = dynamic_cast<MapTab*>(GetMapWindow())) {
return map_tab->GetSession()->secondary_map != nullptr;
}

return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep normal brush previews hover-dependent.

OnMouseMove() and SyncCursorHoverState() now repaint cursor movement only when this returns true. This currently covers paste and secondary-map previews, but not ordinary drawing-mode brush/autoborder previews, so the preview can stop following the cursor when neither condition is true.

🐛 Proposed fix
 bool MapCanvas::HasHoverDependentPreview() const {
 	if (isPasting()) {
 		return true;
 	}
 
+	if (g_gui.IsDrawingMode() && g_gui.GetCurrentBrush() && g_settings.getBoolean(Config::SHOW_PREVIEW)) {
+		return true;
+	}
+
 	if (auto* map_tab = dynamic_cast<MapTab*>(GetMapWindow())) {
 		return map_tab->GetSession()->secondary_map != nullptr;
 	}
 
 	return false;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bool MapCanvas::HasHoverDependentPreview() const {
if (isPasting()) {
return true;
}
if (auto* map_tab = dynamic_cast<MapTab*>(GetMapWindow())) {
return map_tab->GetSession()->secondary_map != nullptr;
}
return false;
}
bool MapCanvas::HasHoverDependentPreview() const {
if (isPasting()) {
return true;
}
if (g_gui.IsDrawingMode() && g_gui.GetCurrentBrush() && g_settings.getBoolean(Config::SHOW_PREVIEW)) {
return true;
}
if (auto* map_tab = dynamic_cast<MapTab*>(GetMapWindow())) {
return map_tab->GetSession()->secondary_map != nullptr;
}
return false;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@source/rendering/ui/map_display.cpp` around lines 186 - 196,
MapCanvas::HasHoverDependentPreview currently only returns true for pasting and
a secondary_map in MapTab, causing normal brush/autoborder previews to stop
following the cursor; update HasHoverDependentPreview to also return true when
the canvas is in a drawing/brush/autoborder preview state by querying the
canvas/tool state (e.g. check the current tool or a method like
IsDrawingMode/HasBrushPreview or similar on MapCanvas or its tool manager) so
OnMouseMove() and SyncCursorHoverState() will repaint while brush/autoborder
previews are active; keep existing isPasting() and
MapTab/GetSession()->secondary_map checks and add the drawing-preview check
before returning false.

Comment thread source/rendering/ui/map_display.cpp
Comment thread source/rendering/ui/map_display.cpp Outdated
Refactored map canvas invalidation into a policy-based system to handle throttled animation ticks and state-dependent refreshes. Replaced direct refresh calls with a mark-and-flush pattern that evaluates invalidation reasons against current view state.
- `source/CMakeLists.txt` — registered new repaint policy and invalidation state files in the build system
- `source/rendering/ui/drawing_controller.cpp` — switched interaction overlay updates to `MarkInvalid` and `FlushRepaintRequest` pattern
- `source/rendering/ui/keyboard_handler.cpp` — updated keyboard-driven view updates to use explicit invalidation marking and shared map refresh reasons
- `source/rendering/ui/map_display.cpp` — implemented `invalidation_state_` tracking, switched to `steady_clock` timing, and integrated `EvaluateRepaintRequest` logic
- `source/rendering/ui/map_display.h` — moved `RepaintReason` to dedicated header, modernized refresh API, and added `ViewInvalidationState` member
- `source/rendering/ui/navigation_controller.cpp` — migrated viewport navigation and floor changes to use granular invalidation marking
- `source/rendering/ui/repaint_policy.cpp` — implemented `EvaluateRepaintRequest` with zoom-dependent animation intervals and refresh throttling logic
- `source/rendering/ui/repaint_policy.h` — defined the `RepaintDecision` interface for evaluating invalidation requests against view state
- `source/rendering/ui/selection_controller.cpp` — converted selection drag triggers to use the mark-and-flush invalidation pattern
- `source/rendering/ui/view_invalidation_state.h` — defined `RepaintReason` enum, `ViewInvalidationState` container, and bitwise operator overloads for flag management
- `source/rendering/ui/zoom_controller.cpp` — updated zoom handlers to mark viewport changes and flush repaint requests sequentially
- `source/ui/gui.cpp` — introduced `SyncCurrentMapCanvasPreviewState` to manage global hover status and updated multi-tab refresh propagation
- `source/ui/gui.h` — added state synchronization declarations and enhanced `RefreshView` parameter signatures
- `source/ui/gui_autoborder_ext.cpp` — integrated canvas preview state synchronization with autoborder buffer lifecycle
- `source/ui/map_window.cpp` — migrated scroll event handlers to the new invalidation marking and flushing system
@github-actions

Copy link
Copy Markdown

🤖 Hi @karolak6612, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@karolak6612

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@github-actions

Copy link
Copy Markdown

🤖 I'm sorry @karolak6612, but I was unable to process your request. Please see the logs for more details.

@karolak6612

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
source/ui/gui_autoborder_ext.cpp (1)

20-34: ⚠️ Potential issue | 🟠 Major

Ensure secondary_map mutations are consistently synchronized.

The autoborder and brush-change paths in gui_autoborder_ext.cpp properly call SyncCurrentMapCanvasPreviewState() after mutations, but doodad preview updates in brush_manager.cpp (lines 118, 197, 394) and pasting operations in editor_manager.cpp (lines 421, 430) do not. While the GUI wrapper layer (GUI::FillDoodadPreviewBuffer() and GUI::StartPasting()/GUI::EndPasting()) currently provides synchronization, this relies on an implicit contract rather than explicit pairing at the mutation site. Centralize secondary_map updates through a setter method that handles synchronization, or add sync calls consistently at each mutation point to prevent stale preview state if the code is refactored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@source/ui/gui_autoborder_ext.cpp` around lines 20 - 34, The code mutates
session->secondary_map in several places (e.g., the autoborder path uses
mapTab->GetSession()->secondary_map = g_autoborder_preview.GetBufferMap()) but
not every mutation (brush_manager.cpp lines ~118/197/394 and editor_manager.cpp
lines ~421/430) calls SyncCurrentMapCanvasPreviewState(), risking stale previews
after refactors; fix by centralizing secondary_map updates into a single setter
(e.g., Session::SetSecondaryMap or MapTab::SetSecondaryMap) that assigns
secondary_map and always calls SyncCurrentMapCanvasPreviewState(), and then
replace direct assignments to session->secondary_map across
g_autoborder_preview.Update, brush_manager, and editor_manager with calls to
that setter (or, if you prefer not to add a setter, ensure every direct
assignment to session->secondary_map is immediately followed by
SyncCurrentMapCanvasPreviewState()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@source/rendering/ui/map_display.cpp`:
- Around line 238-240: SetHoverPreviewActive currently only flips
invalidation_state_.hover_preview_active and doesn't request a redraw, leaving
previews stale; update MapCanvas::SetHoverPreviewActive(bool active) to also
trigger a repaint after changing the flag by calling the canvas
redraw/invalidate entry (e.g., call Invalidate() or the existing MapCanvas
redraw/schedule method) so the hover/paste preview is repainted immediately when
hover_preview_active changes.

---

Outside diff comments:
In `@source/ui/gui_autoborder_ext.cpp`:
- Around line 20-34: The code mutates session->secondary_map in several places
(e.g., the autoborder path uses mapTab->GetSession()->secondary_map =
g_autoborder_preview.GetBufferMap()) but not every mutation (brush_manager.cpp
lines ~118/197/394 and editor_manager.cpp lines ~421/430) calls
SyncCurrentMapCanvasPreviewState(), risking stale previews after refactors; fix
by centralizing secondary_map updates into a single setter (e.g.,
Session::SetSecondaryMap or MapTab::SetSecondaryMap) that assigns secondary_map
and always calls SyncCurrentMapCanvasPreviewState(), and then replace direct
assignments to session->secondary_map across g_autoborder_preview.Update,
brush_manager, and editor_manager with calls to that setter (or, if you prefer
not to add a setter, ensure every direct assignment to session->secondary_map is
immediately followed by SyncCurrentMapCanvasPreviewState()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7e82b015-2dee-42db-a447-fd6838eeb30a

📥 Commits

Reviewing files that changed from the base of the PR and between 4c3369e and 2c8d186.

📒 Files selected for processing (16)
  • source/CMakeLists.txt
  • source/game/animation_timer.cpp
  • source/rendering/ui/drawing_controller.cpp
  • source/rendering/ui/keyboard_handler.cpp
  • source/rendering/ui/map_display.cpp
  • source/rendering/ui/map_display.h
  • source/rendering/ui/navigation_controller.cpp
  • source/rendering/ui/repaint_policy.cpp
  • source/rendering/ui/repaint_policy.h
  • source/rendering/ui/selection_controller.cpp
  • source/rendering/ui/view_invalidation_state.h
  • source/rendering/ui/zoom_controller.cpp
  • source/ui/gui.cpp
  • source/ui/gui.h
  • source/ui/gui_autoborder_ext.cpp
  • source/ui/map_window.cpp

Comment thread source/rendering/ui/map_display.cpp
@karolak6612

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a granular view invalidation system for the map canvas, replacing generic refresh calls with specific repaint reasons such as ViewportChanged and MapContentChanged. It also implements a RepaintPolicy to throttle animation updates based on zoom levels and interaction states, improving rendering efficiency. The review feedback suggests minor refinements to align with modern C++20/23 idioms, including using default-constructed time_point comparisons, removing redundant scope resolution operators for consistency, and pruning an unused helper function in the invalidation state header.

Comment thread source/rendering/ui/map_display.cpp Outdated
return;
}

if (::HasFlag(decision.allowed_reasons, RepaintReason::AnimationTick)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The global scope resolution operator :: is redundant here as there is no name conflict. Removing it would be more consistent with other parts of the codebase, such as repaint_policy.cpp.

	if (HasFlag(decision.allowed_reasons, RepaintReason::AnimationTick)) {

Comment thread source/rendering/ui/repaint_policy.cpp Outdated
}

const auto refresh_interval = std::chrono::milliseconds(decision.animation_interval_ms);
if (state.last_animation_refresh.time_since_epoch().count() != 0 && now - state.last_animation_refresh < refresh_interval) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

A more idiomatic way to check if a time_point has been initialized (i.e., is not at the epoch) is to compare it directly against a default-constructed time_point instead of checking the underlying tick count.

Suggested change
if (state.last_animation_refresh.time_since_epoch().count() != 0 && now - state.last_animation_refresh < refresh_interval) {
if (state.last_animation_refresh != std::chrono::steady_clock::time_point{} && now - state.last_animation_refresh < refresh_interval) {

Comment on lines +56 to +58
[[nodiscard]] constexpr bool AnyNonAnimationFlags(RepaintReason value) {
return (std::to_underlying(value) & ~std::to_underlying(RepaintReason::AnimationTick)) != 0;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The helper function AnyNonAnimationFlags appears to be unused in the current implementation. Consider removing it to keep the header clean, or mark it with [[maybe_unused]] if it is intended for future use in other modules.

Centralized secondary map (preview) assignment into a single GUI method to ensure consistent state synchronization. Fixed hover preview invalidation and refined animation throttling logic.
- `source/brushes/managers/brush_manager.cpp` — replaced direct session access with `SetCurrentMapSecondaryMap()` calls
- `source/editor/managers/editor_manager.cpp` — migrated pasting preview map assignment to centralized GUI setter
- `source/rendering/ui/map_display.cpp` — added state guard and invalidation triggers to `SetHoverPreviewActive()`
- `source/rendering/ui/repaint_policy.cpp` — switched to idiomatic `time_point` comparison for animation throttling [minor]
- `source/rendering/ui/view_invalidation_state.h` — removed unused `AnyNonAnimationFlags` helper [minor]
- `source/ui/gui.cpp` — implemented `SetCurrentMapSecondaryMap` to handle session updates and canvas sync
- `source/ui/gui.h` — added `SetCurrentMapSecondaryMap` to public interface
- `source/ui/gui_autoborder_ext.cpp` — updated autoborder preview logic to use centralized GUI map setter
@github-actions

Copy link
Copy Markdown

🤖 Hi @karolak6612, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@github-actions

Copy link
Copy Markdown

🤖 I'm sorry @karolak6612, but I was unable to process your request. Please see the logs for more details.

Simplified the view invalidation and repaint logic by removing the complex `RepaintReason` system and `RepaintPolicy`. Replaced manual invalidation tracking with direct refresh requests to streamline canvas updates.
- `source/CMakeLists.txt` — removed deleted repaint policy and invalidation state files from build system
- `source/rendering/ui/drawing_controller.cpp` — replaced explicit invalidation and flush calls with `RequestLocalRefresh()`
- `source/rendering/ui/keyboard_handler.cpp` — migrated to direct refresh calls and simplified map refresh request signature
- `source/rendering/ui/map_display.cpp` — implemented direct refresh logic, moved animation throttling to class, and removed `ViewInvalidationState` dependency
- `source/rendering/ui/map_display.h` — simplified public interface by removing `MarkInvalid` and `FlushRepaintRequest`, replaced state struct with local members
- `source/rendering/ui/navigation_controller.cpp` — switched to `RequestLocalRefresh()` for viewport updates
- `source/rendering/ui/repaint_policy.cpp` — removed complex repaint decision logic and animation interval calculations
- `source/rendering/ui/repaint_policy.h` — removed repaint policy interface and `RepaintDecision` struct
- `source/rendering/ui/selection_controller.cpp` — updated overlay updates to use direct refresh calls
- `source/rendering/ui/view_invalidation_state.h` — removed `RepaintReason` enum, `RefreshScope` enum, and `ViewInvalidationState` struct
- `source/rendering/ui/zoom_controller.cpp` — updated zoom-triggered repaints to use direct refresh
- `source/ui/gui.cpp` — updated `RefreshView` implementation to use simplified canvas refresh signature
- `source/ui/gui.h` — removed `RepaintReason` parameter from `RefreshView` signature
- `source/ui/map_window.cpp` — updated scroll-triggered repaints to use direct refresh calls
@github-actions

Copy link
Copy Markdown

🤖 Hi @karolak6612, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

## 📋 Review Summary

The Pull Request successfully implements a more granular and optimized repaint system for the map canvas, replacing many generic full-view refreshes with local refreshes and throttled animations. This should lead to noticeable performance improvements, especially during mouse movement and animations.

🔍 General Feedback

  • Optimization: The introduction of the hover_preview_active_ flag is a great optimization to avoid redundant repaints on every mouse move when no preview (brush, paste, or autoborder) is active.
  • Throttling: The animation throttling system is a good addition to manage CPU/GPU usage, although the current zoom-based logic seems to have its naming or priority inverted.
  • Consistency: Ensure that global state changes (like brush selection) are synchronized across all open map tabs, as the current implementation only updates the active tab.
  • Description Mismatch: The PR description mentions specific enum-based repaint reasons that are not yet implemented in the provided code; consider updating the description to reflect the current state of the refactor.

Comment thread source/ui/gui.cpp Outdated
const bool hover_preview_active = IsPasting()
|| (mapTab->GetMode() == DRAWING_MODE && GetCurrentBrush() != nullptr)
|| mapTab->GetSession()->secondary_map != nullptr;
mapTab->GetCanvas()->SetHoverPreviewActive(hover_preview_active);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 This method only updates the current tab. Since the brush selection is global, switching to another tab that was previously in Drawing mode will result in a stale hover_preview_active_ state (e.g., if the brush was changed or cleared while the other tab was backgrounded). It should iterate through all map tabs to ensure consistency.

Suggested change
mapTab->GetCanvas()->SetHoverPreviewActive(hover_preview_active);
void GUI::SyncCurrentMapCanvasPreviewState() {
for (int i = 0; i < tabbook->GetTabCount(); ++i) {
auto* map_tab = dynamic_cast<MapTab*>(tabbook->GetTab(i));
if (!map_tab) {
continue;
}
const bool hover_preview_active = IsPasting()
|| (map_tab->GetMode() == DRAWING_MODE && GetCurrentBrush() != nullptr)
|| map_tab->GetSession()->secondary_map != nullptr;
map_tab->GetCanvas()->SetHoverPreviewActive(hover_preview_active);
}
}

Comment thread source/rendering/ui/map_display.cpp Outdated

if (map_update) {
Refresh();
if (map_update && hover_preview_active_) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 In Selection mode, hover_preview_active_ will be false, which skips the refresh in SyncCursorHoverState. This will disable the hover highlight (the blue box indicating the tile under the mouse) in Selection mode, which might be a regression in UX. Consider enabling it for Selection mode if a hover highlight is desired.

Suggested change
if (map_update && hover_preview_active_) {
if (map_update && (hover_preview_active_ || g_gui.IsSelectionMode())) {

constexpr int near_zoom_refresh_interval_ms = 1000 / 60;
constexpr int far_zoom_refresh_interval_ms = 1000 / 20;
return zoom <= far_zoom_threshold ? near_zoom_refresh_interval_ms : far_zoom_refresh_interval_ms;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 The logic for animation throttling appears to have swapped variable names or logic. If zoom <= 2.0 is considered "zoomed out" (far), it is currently receiving the near_zoom_refresh_interval_ms (60 FPS), while zoom > 2.0 (zoomed in/near) receives far_zoom_refresh_interval_ms (20 FPS). Usually, users expect smoother animations when looking closely (zoomed in).

Suggested change
}
int MapCanvas::GetAnimationRefreshIntervalMs() const {
constexpr double near_zoom_threshold = 2.0;
constexpr int near_zoom_refresh_interval_ms = 1000 / 60;
constexpr int far_zoom_refresh_interval_ms = 1000 / 20;
return zoom >= near_zoom_threshold ? near_zoom_refresh_interval_ms : far_zoom_refresh_interval_ms;
}

class SelectionController;
class DrawingController;
class ScreenshotController;
class MapMenuHandler;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The PR description mentions the introduction of a RepaintReason enum and a flag-based repaint system (e.g., InteractionOverlayChanged, ViewportChanged). However, these are not present in the current diff. It seems the implementation was simplified to generic local/shared refreshes, or some changes were omitted from this commit.

@karolak6612

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the rendering and refresh logic by introducing specialized methods like RequestLocalRefresh and RequestSharedMapRefresh, alongside a state-driven hover preview system. While these changes aim to optimize performance, the current implementation of the hover_preview_active_ flag causes a regression by disabling tile highlights in Selection Mode. Furthermore, the Refresh method in MapCanvas hides the base wxWindow::Refresh due to a signature mismatch, which could lead to unexpected behavior. The refactor also pushes map_display.cpp beyond the 500-line limit specified in the style guide, necessitating a plan for file decomposition. Other minor improvements regarding modern C++ attributes like [[nodiscard]] and noexcept were also noted.

Comment thread source/rendering/ui/map_display.cpp Outdated
Comment on lines 441 to 443
if (map_update && hover_preview_active_) {
RequestLocalRefresh();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This optimization introduces a regression in visual feedback. In Selection Mode, the editor typically displays a hover highlight (border) around the tile under the cursor. By checking hover_preview_active_ (which is false in selection mode per SyncCurrentMapCanvasPreviewState), you are preventing the canvas from repainting when the mouse moves between tiles. This means the hover highlight will not move until a click or other event occurs.

Since map_update already ensures we only refresh when the cursor actually moves to a different tile, the hover_preview_active_ check is likely unnecessary here and should be removed to maintain basic UI responsiveness.

	if (map_update) {
		RequestLocalRefresh();
	}

Comment thread source/rendering/ui/map_display.cpp Outdated
Comment on lines +471 to +473
if (hover_preview_active_) {
RequestLocalRefresh();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the issue in SyncCursorHoverState, this check prevents the hover highlight from updating in Selection Mode. The map_update flag already provides sufficient throttling by ensuring we only repaint when the mouse crosses tile boundaries. Restricting it further to only when a "preview" is active breaks standard hover feedback.

		if (map_update) {
			RequestLocalRefresh();
		}

Comment thread source/rendering/ui/map_display.h Outdated
@@ -80,6 +79,10 @@ class MapCanvas : public wxGLCanvas {
void OnMousePropertiesRelease(wxMouseEvent& event);

virtual void Refresh();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The signature of this Refresh method does not match the base wxWindow::Refresh(bool eraseBackground = true, const wxRect* rect = nullptr). Since wxWindow::Refresh is not virtual, this creates a name-hiding situation rather than an override. If a caller uses a wxWindow* pointer to this canvas and calls Refresh(), your optimized logic will be bypassed.

To adhere to the repository's principle of using modern C++ and avoiding confusion, you should either match the signature and mark it override (if you intend to hide it consistently) or, preferably, rename this to something like RequestRefresh and use RequestLocalRefresh consistently throughout the codebase.

References
  1. Upgrade legacy patterns and ensure clear function responsibilities (SRP). (link)

Comment thread source/ui/gui.cpp Outdated
Comment on lines +229 to +231
const bool hover_preview_active = IsPasting()
|| (mapTab->GetMode() == DRAWING_MODE && GetCurrentBrush() != nullptr)
|| mapTab->GetSession()->secondary_map != nullptr;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic here for hover_preview_active specifically excludes Selection Mode (unless pasting). As noted in map_display.cpp, this causes the hover highlight to stop updating during normal mouse movement. You should include a check for whether the hover highlight is enabled in settings, or simply allow it to be active if the mode is not NONE.

	const bool hover_preview_active = IsPasting()
		|| (mapTab->GetMode() == DRAWING_MODE && GetCurrentBrush() != nullptr)
		|| mapTab->GetMode() == SELECTION_MODE
		|| mapTab->GetSession()->secondary_map != nullptr;

Comment thread source/rendering/ui/map_display.cpp Outdated
return g_settings.getBoolean(Config::SHOW_PREVIEW);
}

int MapCanvas::GetAnimationRefreshIntervalMs() const {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

This function should be marked with [[nodiscard]] to ensure the return value is not ignored, and noexcept as it only performs simple arithmetic and a comparison.

int MapCanvas::GetAnimationRefreshIntervalMs() const noexcept {

@@ -171,11 +172,58 @@ MapCanvas::~MapCanvas() {
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The file map_display.cpp is currently 755 lines long, which violates the repository style guide limit of 500 lines. While this PR is a refactor, consider planning a split of this file (e.g., moving event handlers to a separate controller) to adhere to the SRP and file size limits.

References
  1. File > 500 lines → split. (link)

Removed redundant Refresh override in MapCanvas in favor of RequestLocalRefresh and refactored GUI to synchronize hover preview state across all map tabs.
- `source/rendering/ui/map_display.cpp` — removed `Refresh()` override, added `noexcept` to refresh interval getter, and simplified refresh triggers
- `source/rendering/ui/map_display.h` — removed `Refresh()` declaration and marked `GetAnimationRefreshIntervalMs()` as `noexcept`
- `source/rendering/ui/screenshot_controller.cpp` — replaced `Refresh()` calls with `RequestLocalRefresh()`
- `source/ui/gui.cpp` — updated `SyncCurrentMapCanvasPreviewState()` to iterate through all tabs and update hover state for each MapCanvas
@github-actions

Copy link
Copy Markdown

🤖 Hi @karolak6612, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@github-actions

Copy link
Copy Markdown

🤖 I'm sorry @karolak6612, but I was unable to process your request. Please see the logs for more details.

@karolak6612

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@karolak6612
karolak6612 merged commit 5d558c8 into master Apr 19, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant