| 1365 | } |
| 1366 | |
| 1367 | void InputController::handleScroll([[maybe_unused]] double xoff, double yoff) { |
| 1368 | // Capture mode (input settings panel) consumes scroll first so the user |
| 1369 | // can rebind scroll-only actions like Camera Zoom or chord-style Roll. |
| 1370 | if (bindings_.isCapturing()) { |
| 1371 | std::optional<int> chord_key; |
| 1372 | if (!held_keys_.empty()) { |
| 1373 | chord_key = held_keys_.back(); |
| 1374 | } |
| 1375 | bindings_.captureScroll(getModifierKeys(), chord_key); |
| 1376 | return; |
| 1377 | } |
| 1378 | |
| 1379 | float fx, fy; |
| 1380 | SDL_GetMouseState(&fx, &fy); |
| 1381 | double mouse_x = fx, mouse_y = fy; |
| 1382 | bool over_gui = false; |
| 1383 | bool over_gui_hover = false; |
| 1384 | if (input_router_) { |
| 1385 | const auto targets = input_router_->pointerTargets(mouse_x, mouse_y); |
| 1386 | over_gui = targets.pointer_target == input::InputTarget::Gui; |
| 1387 | over_gui_hover = targets.hover_target == input::InputTarget::Gui; |
| 1388 | } else { |
| 1389 | over_gui = isPointerOverBlockingUi(mouse_x, mouse_y); |
| 1390 | over_gui_hover = isPointerOverUiHover(mouse_x, mouse_y); |
| 1391 | } |
| 1392 | |
| 1393 | // Dispatch to modal operators first - if consumed, don't continue |
| 1394 | if (dispatchScrollToModals(xoff, yoff, mouse_x, mouse_y, getModifierKeys(), over_gui_hover)) { |
| 1395 | return; |
| 1396 | } |
| 1397 | |
| 1398 | const int mods = getModifierKeys(); |
| 1399 | const input::Action scroll_action = bindings_.getActionForScroll(getCurrentToolMode(), mods, held_keys_); |
| 1400 | if (selection_tool_ && selection_tool_->isEnabled()) { |
| 1401 | if (scroll_action == input::Action::DEPTH_ADJUST_FAR && |
| 1402 | selection_tool_->isDepthFilterEnabled()) { |
| 1403 | selection_tool_->adjustDepthFar((yoff > 0) ? 1.1f : 0.9f); |
| 1404 | return; |
| 1405 | } |
| 1406 | } |
| 1407 | |
| 1408 | // Brush radius adjustment for the selection tool. Modal operators |
| 1409 | // for selection strokes pass scroll through, so it's safe to honor |
| 1410 | // BRUSH_RESIZE here even mid-stroke — that's what lets the user grow |
| 1411 | // or shrink the ring while in the middle of an add or subtract drag. |
| 1412 | if (scroll_action == input::Action::BRUSH_RESIZE) { |
| 1413 | if (selection_tool_ && selection_tool_->isEnabled()) { |
| 1414 | const float scale = (yoff > 0) ? 1.1f : 0.9f; |
| 1415 | selection_tool_->setBrushRadius(selection_tool_->getBrushRadius() * scale); |
| 1416 | return; |
| 1417 | } |
| 1418 | } |
| 1419 | |
| 1420 | if (drag_mode_ == DragMode::Gizmo || drag_mode_ == DragMode::Splitter) |
| 1421 | return; |
| 1422 | |
| 1423 | if (!isInViewport(mouse_x, mouse_y) || over_gui) |
| 1424 | return; |