From b9f798ab66f5c1fc1b656794dc735f8ae46d9819 Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Sun, 19 Jul 2026 15:37:10 +0000 Subject: [PATCH 35/71] fix(macos): mouse selection crash + drag, and wheel scrolling - Selection crash (std::bad_function_call): Terminal::setWordDelimiters() is what assigns the selection helper's `wordDelimited` std::function; the session never called it, so the first selection invoked an empty function and threw. Seed the standard word/extended-word delimiters right after constructing the Terminal. - Selection also never extended because only mouse press/release were sent, not the drags between them. Wire -mouseDragged: -> bridge_mouse_move -> Terminal::sendMouseMoveEvent (with a pixel->cell mapping helper on the session). - Wheel scrolling: wire -scrollWheel: -> bridge_scroll -> viewport().scrollUp/scrollDown. Remaining reported issues are separate: sixel post-image ANSI echo glitch, and drag & drop (not yet implemented). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AeG2jwYMX5gdvvtUnx3PJa --- src/contour_macos/SessionBridge.cpp | 19 +++++++++++++++++++ src/contour_macos/SessionBridge.h | 5 +++++ src/contour_macos/TerminalSession.cpp | 16 ++++++++++++++++ src/contour_macos/TerminalSession.h | 3 +++ src/contour_macos/TerminalView.mm | 26 ++++++++++++++++++++++++++ 5 files changed, 69 insertions(+) diff --git a/src/contour_macos/SessionBridge.cpp b/src/contour_macos/SessionBridge.cpp index af229ed0..559a2ddd 100644 --- a/src/contour_macos/SessionBridge.cpp +++ b/src/contour_macos/SessionBridge.cpp @@ -180,6 +180,16 @@ void bridge_mouse_press(TerminalSession* session, int x, int y, uint32_t modifie false); } +void bridge_mouse_move(TerminalSession* session, int x, int y, uint32_t modifiers) +{ + // The engine derives the cell from the pixel position; pass a best-effort cell too. + auto const cell = session->pixelToCell(x, y); + session->terminal().sendMouseMoveEvent(toKeyboardModifiers(modifiers).chord, + cell, + vtbackend::PixelCoordinate { { x }, { y } }, + false); +} + void bridge_mouse_release(TerminalSession* session, int x, int y, uint32_t modifiers) { session->terminal().sendMouseReleaseEvent(toKeyboardModifiers(modifiers).chord, @@ -193,6 +203,15 @@ void bridge_focus_in(TerminalSession* session) session->terminal().sendFocusInEvent(); } +bool bridge_scroll(TerminalSession* session, int lines) +{ + if (lines == 0) + return false; + auto& vp = session->terminal().viewport(); + return lines > 0 ? vp.scrollUp(vtbackend::LineCount(lines)) + : vp.scrollDown(vtbackend::LineCount(-lines)); +} + char* bridge_copy_selection(TerminalSession* session) { std::string const text = session->terminal().extractSelectionText(); diff --git a/src/contour_macos/SessionBridge.h b/src/contour_macos/SessionBridge.h index c937e0d7..329b7d2e 100644 --- a/src/contour_macos/SessionBridge.h +++ b/src/contour_macos/SessionBridge.h @@ -121,9 +121,14 @@ void bridge_send_key(TerminalSession* session, int bridgeKey, uint32_t modifiers void bridge_send_char(TerminalSession* session, uint32_t codepoint, uint32_t modifiers); void bridge_mouse_press(TerminalSession* session, int x, int y, uint32_t modifiers); +void bridge_mouse_move(TerminalSession* session, int x, int y, uint32_t modifiers); void bridge_mouse_release(TerminalSession* session, int x, int y, uint32_t modifiers); void bridge_focus_in(TerminalSession* session); +/// Scrolls the viewport by the given number of lines (positive = toward older/scrollback, +/// negative = toward newer/bottom). Returns true if the viewport moved. +bool bridge_scroll(TerminalSession* session, int lines); + // --- edit operations (for the Edit menu) --- /// Returns the currently selected text as a freshly malloc()'d UTF-8 C string (caller frees), diff --git a/src/contour_macos/TerminalSession.cpp b/src/contour_macos/TerminalSession.cpp index 1f87a85e..5e464f98 100644 --- a/src/contour_macos/TerminalSession.cpp +++ b/src/contour_macos/TerminalSession.cpp @@ -76,6 +76,12 @@ TerminalSession::TerminalSession(vtbackend::PageSize pageSize, _terminal = std::make_unique( *this, makePty(_pageSize), makeSettings(_pageSize), steady_clock::now()); + // setWordDelimiters() is what actually assigns the selection helper's `wordDelimited` + // std::function; without calling it, mouse selection invokes an empty std::function and + // throws std::bad_function_call. Seed with the usual terminal word-boundary set. + _terminal->setWordDelimiters(" /\\()\"'-.,:;<>~!@#$%^&*|+=[]{}~?│"); + _terminal->setExtendedWordDelimiters(" /\\()\"'-.,:;<>~!@#$%^&*|+=[]{}~?"); + _renderTarget.setRenderSize(surfaceSize); _renderer.applyResize( surfaceSize, _pageSize, vtrasterizer::PageMargin { PageMarginPx, PageMarginPx, PageMarginPx }); @@ -100,6 +106,16 @@ vtbackend::PageSize TerminalSession::derivePageSize(ImageSize surfaceSize) const vtbackend::ColumnCount(static_cast(columns)) }; } +vtbackend::CellLocation TerminalSession::pixelToCell(int x, int y) const +{ + auto const cell = _renderer.gridMetrics().cellSize; + auto const cw = std::max(1, static_cast(unbox(cell.width))); + auto const ch = std::max(1, static_cast(unbox(cell.height))); + auto const col = std::max(0, (x - PageMarginPx) / cw); + auto const line = std::max(0, (y - PageMarginPx) / ch); + return vtbackend::CellLocation { vtbackend::LineOffset(line), vtbackend::ColumnOffset(col) }; +} + void TerminalSession::start() { if (_started) diff --git a/src/contour_macos/TerminalSession.h b/src/contour_macos/TerminalSession.h index de306073..3087ab5c 100644 --- a/src/contour_macos/TerminalSession.h +++ b/src/contour_macos/TerminalSession.h @@ -56,6 +56,9 @@ class TerminalSession: public vtbackend::Terminal::Events [[nodiscard]] vtrasterizer::Renderer& renderer() noexcept { return _renderer; } [[nodiscard]] SoftwareRenderTarget& renderTarget() noexcept { return _renderTarget; } + /// Maps a view pixel coordinate (top-left origin) to a grid cell location. + [[nodiscard]] vtbackend::CellLocation pixelToCell(int x, int y) const; + /// Renders one frame into the render target's output buffer. Call on the main thread /// (e.g. from -drawRect:). Clears, ticks, renders. void renderFrame(); diff --git a/src/contour_macos/TerminalView.mm b/src/contour_macos/TerminalView.mm index c863422a..39e36208 100644 --- a/src/contour_macos/TerminalView.mm +++ b/src/contour_macos/TerminalView.mm @@ -313,6 +313,18 @@ void cbOnClosed(void* userData) [self setNeedsDisplay:YES]; } +- (void)mouseDragged:(NSEvent*)event +{ + // A drag is a move with the button held — this is what extends a mouse selection. + TerminalSession* session = [self session]; + if (!session) + return; + NSPoint p = [self convertPoint:[event locationInWindow] fromView:nil]; + contour_macos::bridge_mouse_move( + session, (int) p.x, (int) p.y, bridgeModifiers([event modifierFlags])); + [self setNeedsDisplay:YES]; +} + - (void)mouseUp:(NSEvent*)event { TerminalSession* session = [self session]; @@ -324,6 +336,20 @@ void cbOnClosed(void* userData) [self setNeedsDisplay:YES]; } +- (void)scrollWheel:(NSEvent*)event +{ + TerminalSession* session = [self session]; + if (!session) + return; + // deltaY > 0 is a scroll toward the top (show older lines); map to scrollback lines. + CGFloat const dy = [event deltaY]; + int lines = (int) dy; + if (lines == 0) + lines = dy > 0 ? 1 : (dy < 0 ? -1 : 0); + if (lines != 0 && contour_macos::bridge_scroll(session, lines)) + [self setNeedsDisplay:YES]; +} + - (BOOL)becomeFirstResponder { TerminalSession* session = [self session];