From 9d7e424453b47ad6fd99d58013f9698d12725201 Mon Sep 17 00:00:00 2001 From: igaboo Date: Sat, 25 Apr 2026 16:27:40 -0700 Subject: [PATCH] Fix #221: restrict view-mode click activation to centered content column Clicks in the wide horizontal margins around the centered view-mode column previously fell through to whichever block they shared a row with, causing accidental checklist toggles and embed opens when the user was just clicking around to focus the terminal or select text. Adds viewContentXRange() as the single source of truth for the [leftPad, leftPad+contentWidth) bounds and uses it in both the click and hover handlers so visual feedback matches activation. Hover is narrowed too, so the highlight no longer suggests clickability in margins where clicks won't fire. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/editor/editor.go | 42 +++++++++++++++++++++++++--------- internal/editor/editor_test.go | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/internal/editor/editor.go b/internal/editor/editor.go index 0732eda..a5f3c00 100644 --- a/internal/editor/editor.go +++ b/internal/editor/editor.go @@ -1709,10 +1709,16 @@ func (m Model) update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } if m.viewMode { - // Track hover state for checklist/embed visual feedback. - hoverY := m.viewport.YOffset() + msg.Y - idx := m.blockIndexAtLine(hoverY) + // Track hover state for checklist/embed visual feedback. Drop + // hover when the cursor is in the centered-column margin so + // the highlight matches click activation bounds. + leftPad, contentWidth := m.viewContentXRange() oldHover := m.hoverBlock + idx := -1 + if msg.X >= leftPad && msg.X < leftPad+contentWidth { + hoverY := m.viewport.YOffset() + msg.Y + idx = m.blockIndexAtLine(hoverY) + } m.hoverBlock = idx // Re-render if hovering over a different interactive block. @@ -1765,6 +1771,14 @@ func (m Model) update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } if m.viewMode { + // Reject clicks that land in the centered-column margin so + // stray clicks outside the rendered block content don't fire + // checklist toggles or embed opens. + leftPad, contentWidth := m.viewContentXRange() + if msg.X < leftPad || msg.X >= leftPad+contentWidth { + m.hoverBlock = -1 + return m, nil + } // Track hover state for checklist visual feedback. hoverY := m.viewport.YOffset() + msg.Y idx := m.blockIndexAtLine(hoverY) @@ -3164,22 +3178,28 @@ func (m *Model) renderAllBlocks() string { return strings.Join(parts, "\n") } -// renderViewContent builds the full view-mode content: centered, max-width, -// with generous spacing for a clean reading experience. -func (m Model) renderViewContent() string { - contentWidth := viewMaxWidth +// viewContentXRange returns the [leftPad, leftPad+contentWidth) X range of +// the centered view-mode content column. Click and hover handlers use this +// to reject coordinates that fall in the surrounding margin. +func (m Model) viewContentXRange() (leftPad, contentWidth int) { + contentWidth = viewMaxWidth if m.width < contentWidth { - contentWidth = m.width - 4 // leave some margin even on small terms + contentWidth = m.width - 4 if contentWidth < 20 { contentWidth = 20 } } - - // Horizontal padding to center the content column. - leftPad := (m.width - contentWidth) / 2 + leftPad = (m.width - contentWidth) / 2 if leftPad < 0 { leftPad = 0 } + return leftPad, contentWidth +} + +// renderViewContent builds the full view-mode content: centered, max-width, +// with generous spacing for a clean reading experience. +func (m Model) renderViewContent() string { + leftPad, contentWidth := m.viewContentXRange() padStr := strings.Repeat(" ", leftPad) var parts []string diff --git a/internal/editor/editor_test.go b/internal/editor/editor_test.go index 58607d1..4604815 100644 --- a/internal/editor/editor_test.go +++ b/internal/editor/editor_test.go @@ -2030,5 +2030,46 @@ func TestBlockIndexAtLineReturnsCorrectBlock(t *testing.T) { } } +func TestMouseClickInLeftMarginDoesNotToggle(t *testing.T) { + content := "- [ ] buy milk" + m := New(Config{Title: "test", Content: content}) + + updated, _ := m.Update(tea.WindowSizeMsg{Width: 80, Height: 24}) + m = updated.(Model) + updated, _ = m.Update(tea.KeyPressMsg{Code: 'r', Mod: tea.ModCtrl}) + m = updated.(Model) + if !m.viewMode { + t.Fatal("expected view mode") + } + + // width=80, contentWidth=72, leftPad=4 — X=1 falls in the left margin. + clickY := m.blockLineOffsets[0] - m.viewport.YOffset() + updated, _ = m.Update(tea.MouseClickMsg{X: 1, Y: clickY, Button: tea.MouseLeft}) + m = updated.(Model) + + if m.blocks[0].Checked { + t.Fatal("click in left margin should not toggle checklist") + } +} + +func TestMouseClickInRightMarginDoesNotToggle(t *testing.T) { + content := "- [ ] buy milk" + m := New(Config{Title: "test", Content: content}) + + updated, _ := m.Update(tea.WindowSizeMsg{Width: 80, Height: 24}) + m = updated.(Model) + updated, _ = m.Update(tea.KeyPressMsg{Code: 'r', Mod: tea.ModCtrl}) + m = updated.(Model) + + // leftPad=4, contentWidth=72 → content range [4,76). X=78 is right margin. + clickY := m.blockLineOffsets[0] - m.viewport.YOffset() + updated, _ = m.Update(tea.MouseClickMsg{X: 78, Y: clickY, Button: tea.MouseLeft}) + m = updated.(Model) + + if m.blocks[0].Checked { + t.Fatal("click in right margin should not toggle checklist") + } +} + // Verify strings import is used. var _ = strings.Contains