Skip to content

Commit 47282e0

Browse files
committed
Refine mouse resize and scrolling
1 parent b1baabd commit 47282e0

File tree

7 files changed

+79
-70
lines changed

7 files changed

+79
-70
lines changed

docs/src/content/docs/getting-started/keybindings/preview.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,7 @@ Press <kbd>[</kbd> to move to the next tab in the preview sidebar, if one exists
2929
## `]` - Previous Preview Tab
3030

3131
Press <kbd>]</kbd> to move to the previous tab in the preview sidebar, if one exists.
32+
33+
## `P` - Reset Preview Width
34+
35+
Press <kbd>Shift</kbd>+<kbd>p</kbd> to reset the preview pane width back to the configured default.

internal/tui/components/carousel/carousel.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -445,22 +445,24 @@ func (m *Model) renderItem(itemID int, maxWidth int) string {
445445
item := m.renderItemContent(itemID, maxWidth)
446446

447447
// Wrap the item in a zone for click detection
448-
zoneID := fmt.Sprintf("%s%s%d", TabZonePrefix, m.zonePrefix, itemID)
449-
return zone.Mark(zoneID, item)
448+
return zone.Mark(m.tabZoneID(itemID), item)
450449
}
451450

452451
// HandleClick checks if a mouse click event is on a tab and returns the tab index if so
453452
// Returns -1 if no tab was clicked
454453
func (m *Model) HandleClick(msg tea.MouseMsg) int {
455454
for i := range m.items {
456-
zoneID := fmt.Sprintf("%s%s%d", TabZonePrefix, m.zonePrefix, i)
457-
if zone.Get(zoneID).InBounds(msg) {
455+
if zone.Get(m.tabZoneID(i)).InBounds(msg) {
458456
return i
459457
}
460458
}
461459
return -1
462460
}
463461

462+
func (m *Model) tabZoneID(itemID int) string {
463+
return fmt.Sprintf("%s%s%d", TabZonePrefix, m.zonePrefix, itemID)
464+
}
465+
464466
func max(a, b int) int {
465467
if a > b {
466468
return a

internal/tui/components/carousel/carousel_test.go

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package carousel
22

33
import (
44
"fmt"
5+
"strings"
56
"testing"
67

78
tea "github.com/charmbracelet/bubbletea"
@@ -90,13 +91,13 @@ func TestCarousel(t *testing.T) {
9091
// The view should contain the items (after zone.Scan)
9192
scanned := zone.Scan(view)
9293
for _, item := range items {
93-
if !contains(scanned, item) {
94+
if !strings.Contains(scanned, item) {
9495
t.Errorf("Expected view to contain %q", item)
9596
}
9697
}
9798
})
9899

99-
t.Run("HandleClick should check zones for each tab", func(t *testing.T) {
100+
t.Run("HandleClick should return index for clicked tab", func(t *testing.T) {
100101
items := []string{"Tab 1", "Tab 2", "Tab 3"}
101102
c := New(WithItems(items), WithWidth(200))
102103
c.UpdateSize()
@@ -105,14 +106,39 @@ func TestCarousel(t *testing.T) {
105106
view := c.View()
106107
_ = zone.Scan(view)
107108

108-
// Verify that zone IDs are being used in the rendered output
109-
for i := range items {
110-
zoneID := fmt.Sprintf("%s%d", TabZonePrefix, i)
111-
// The zone should be marked in the view
112-
if !contains(view, zoneID) {
113-
// This is expected - zones are encoded differently
114-
// The important thing is that HandleClick uses the correct zone IDs
115-
}
109+
// Click inside the first tab's zone
110+
zoneID := fmt.Sprintf("%s%d", TabZonePrefix, 0)
111+
z := zone.Get(zoneID)
112+
if z.IsZero() {
113+
t.Fatal("Expected zone to be registered")
114+
}
115+
msg := tea.MouseMsg{
116+
X: z.StartX,
117+
Y: z.StartY,
118+
Button: tea.MouseButtonLeft,
119+
Action: tea.MouseActionRelease,
120+
}
121+
122+
result := c.HandleClick(msg)
123+
if result != 0 {
124+
t.Errorf("Expected tab index 0, got %d", result)
125+
}
126+
})
127+
128+
t.Run("HandleClick should return -1 for empty items", func(t *testing.T) {
129+
c := New(WithItems([]string{}), WithWidth(100))
130+
c.UpdateSize()
131+
132+
msg := tea.MouseMsg{
133+
X: 1,
134+
Y: 1,
135+
Button: tea.MouseButtonLeft,
136+
Action: tea.MouseActionRelease,
137+
}
138+
139+
result := c.HandleClick(msg)
140+
if result != -1 {
141+
t.Errorf("Expected -1 for empty items, got %d", result)
116142
}
117143
})
118144

@@ -135,16 +161,3 @@ func TestCarousel(t *testing.T) {
135161
}
136162
})
137163
}
138-
139-
func contains(s, substr string) bool {
140-
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
141-
}
142-
143-
func containsHelper(s, substr string) bool {
144-
for i := 0; i <= len(s)-len(substr); i++ {
145-
if s[i:i+len(substr)] == substr {
146-
return true
147-
}
148-
}
149-
return false
150-
}

internal/tui/components/listviewport/listviewport.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,13 @@ func (m *Model) getNumPrsPerPage() int {
7474
}
7575

7676
func (m *Model) ResetCurrItem() {
77+
m.resetCurrItem()
78+
}
79+
80+
func (m *Model) resetCurrItem() {
7781
m.currId = 0
82+
m.topBoundId = 0
83+
m.bottomBoundId = 0
7884
m.viewport.GotoTop()
7985
}
8086

@@ -122,21 +128,16 @@ func (m *Model) LastItem() int {
122128

123129
func (m *Model) SetCurrItem(index int) {
124130
if m.NumCurrentItems == 0 {
125-
m.currId = 0
126-
m.topBoundId = 0
127-
m.bottomBoundId = 0
128-
m.viewport.GotoTop()
131+
m.resetCurrItem()
129132
return
130133
}
131134

132135
index = utils.Max(0, utils.Min(index, m.NumCurrentItems-1))
133136
itemsPerPage := m.getNumPrsPerPage()
134137

135138
if itemsPerPage <= 0 {
139+
m.resetCurrItem()
136140
m.currId = index
137-
m.topBoundId = 0
138-
m.bottomBoundId = 0
139-
m.viewport.GotoTop()
140141
return
141142
}
142143

@@ -153,6 +154,11 @@ func (m *Model) SetCurrItem(index int) {
153154
}
154155

155156
m.currId = index
157+
if m.currId == 0 && m.topBoundId > 0 {
158+
m.topBoundId = 0
159+
m.bottomBoundId = utils.Min(itemsPerPage-1, m.NumCurrentItems-1)
160+
m.viewport.GotoTop()
161+
}
156162
}
157163

158164
func (m *Model) SetDimensions(dimensions constants.Dimensions) {

internal/tui/components/sidebar/sidebar.go

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package sidebar
22

33
import (
4-
"fmt"
5-
64
"github.com/charmbracelet/bubbles/key"
75
"github.com/charmbracelet/bubbles/viewport"
86
tea "github.com/charmbracelet/bubbletea"
@@ -21,7 +19,7 @@ const (
2119
ResizeHandleChar = "│"
2220
ScrollbarWidth = 1
2321
ScrollThumbChar = "┃"
24-
ScrollTrackChar = ""
22+
ScrollTrackChar = ""
2523
)
2624

2725
// ResizeMsg is sent when the sidebar is resized via mouse drag
@@ -115,17 +113,11 @@ func (m Model) handleMouseMsg(msg tea.MouseMsg) (Model, tea.Cmd) {
115113
// Mouse X is relative to the terminal, sidebar is on the right
116114
// New width = ScreenWidth - MouseX
117115
newWidth := m.ctx.ScreenWidth - msg.X
118-
if newWidth < MinPreviewWidth {
119-
newWidth = MinPreviewWidth
120-
}
121-
if newWidth > MaxPreviewWidth {
122-
newWidth = MaxPreviewWidth
123-
}
116+
newWidth = max(newWidth, MinPreviewWidth)
117+
newWidth = min(newWidth, MaxPreviewWidth)
124118
// Don't let the sidebar take more than 70% of the screen
125119
maxWidth := int(float64(m.ctx.ScreenWidth) * 0.7)
126-
if newWidth > maxWidth {
127-
newWidth = maxWidth
128-
}
120+
newWidth = min(newWidth, maxWidth)
129121
return m, func() tea.Msg { return ResizeMsg{NewWidth: newWidth} }
130122

131123
case tea.MouseActionRelease:
@@ -157,38 +149,30 @@ func (m Model) View() string {
157149
if width <= 0 {
158150
width = m.ctx.Config.Defaults.Preview.Width
159151
}
152+
handleWidth := lipgloss.Width(ResizeHandleChar)
160153

161154
var content string
162155
if m.data == "" {
163156
// Content style without the left border and scrollbar
164157
contentStyle := lipgloss.NewStyle().
165158
Height(height).
166-
Width(width - 1) // Subtract 1 for the resize handle
159+
Width(width - handleWidth)
167160
content = contentStyle.Align(lipgloss.Center).Render(
168161
lipgloss.PlaceVertical(height, lipgloss.Center, m.emptyState),
169162
)
170163
} else {
171164
// Render scrollbar
172-
scrollbar := m.renderScrollbar(height - m.ctx.Styles.Sidebar.PagerHeight)
173-
174-
// Content style - subtract space for resize handle and scrollbar
175-
contentWidth := width - 1 - ScrollbarWidth
165+
scrollbar := m.renderScrollbar(height)
176166

177167
// Note: Avoid using MaxWidth() on content that may contain zone markers
178168
// as it can truncate them and cause visual artifacts.
179169
viewportContent := m.viewport.View()
180170

181-
pager := lipgloss.NewStyle().
182-
Width(contentWidth).
183-
Render(m.ctx.Styles.Sidebar.PagerStyle.
184-
Render(fmt.Sprintf("%d%%", int(m.viewport.ScrollPercent()*100))))
185-
186-
mainContent := lipgloss.JoinVertical(lipgloss.Top, viewportContent, pager)
187-
188171
// Join content and scrollbar
189-
content = lipgloss.JoinHorizontal(lipgloss.Top, mainContent, scrollbar)
172+
content = lipgloss.JoinHorizontal(lipgloss.Top, viewportContent, scrollbar)
190173
}
191174

175+
// Normalize heights so the resize handle and content align without truncation.
192176
contentHeight := lipgloss.Height(content)
193177
if height > contentHeight {
194178
content = lipgloss.PlaceVertical(height, lipgloss.Top, content)
@@ -230,8 +214,8 @@ func (m Model) renderScrollbar(height int) string {
230214

231215
// Build scrollbar string
232216
scrollbar := ""
233-
trackStyle := lipgloss.NewStyle().Foreground(m.ctx.Theme.FaintBorder)
234-
thumbStyle := lipgloss.NewStyle().Foreground(m.ctx.Theme.PrimaryBorder)
217+
trackStyle := lipgloss.NewStyle().Foreground(m.ctx.Theme.SecondaryBorder).Bold(true)
218+
thumbStyle := lipgloss.NewStyle().Foreground(m.ctx.Theme.PrimaryText).Bold(true)
235219

236220
for i := 0; i < height; i++ {
237221
if i >= thumbPos && i < thumbPos+thumbSize {
@@ -298,6 +282,6 @@ func (m *Model) UpdateProgramContext(ctx *context.ProgramContext) {
298282
return
299283
}
300284
m.ctx = ctx
301-
m.viewport.Height = m.ctx.MainContentHeight - m.ctx.Styles.Sidebar.PagerHeight
285+
m.viewport.Height = m.ctx.MainContentHeight
302286
m.viewport.Width = m.GetSidebarContentWidth() - 1 // Account for resize handle
303287
}

internal/tui/components/table/table.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,11 @@ func (m *Model) renderRow(rowId int, headerColumns []string) string {
329329
MaxWidth(m.dimensions.Width).
330330
Render(lipgloss.JoinHorizontal(lipgloss.Top, renderedColumns...))
331331

332-
zoneID := fmt.Sprintf("%s%d-%d", RowZonePrefix, m.sectionId, rowId)
333-
return zone.Mark(zoneID, row)
332+
return zone.Mark(m.rowZoneID(rowId), row)
333+
}
334+
335+
func (m *Model) rowZoneID(rowID int) string {
336+
return fmt.Sprintf("%s%d-%d", RowZonePrefix, m.sectionId, rowID)
334337
}
335338

336339
func (m *Model) UpdateProgramContext(ctx *context.ProgramContext) {
@@ -365,8 +368,7 @@ func (m *Model) SetSectionId(id int) {
365368
// HandleClick checks if a row was clicked and returns the row index, or -1 if no row was clicked
366369
func (m *Model) HandleClick(msg tea.MouseMsg) int {
367370
for i := range m.Rows {
368-
zoneID := fmt.Sprintf("%s%d-%d", RowZonePrefix, m.sectionId, i)
369-
if zone.Get(zoneID).InBounds(msg) {
371+
if zone.Get(m.rowZoneID(i)).InBounds(msg) {
370372
return i
371373
}
372374
}

internal/tui/ui.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -631,14 +631,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
631631
if m.sidebar.IsOpen {
632632
var sidebarCmd tea.Cmd
633633
m.sidebar, sidebarCmd = m.sidebar.Update(msg)
634-
if sidebarCmd != nil {
635-
cmds = append(cmds, sidebarCmd)
636-
}
634+
cmds = append(cmds, sidebarCmd)
637635
// If resizing is in progress, don't process other mouse events
638636
if m.sidebar.IsResizing() {
639637
return m, tea.Batch(cmds...)
640638
}
641-
// If sidebar handled a scroll event, return early
639+
// If sidebar handled a scroll event, return early to avoid also scrolling the list.
642640
if msg.Button == tea.MouseButtonWheelUp || msg.Button == tea.MouseButtonWheelDown {
643641
// Check if mouse is in sidebar area
644642
sidebarStartX := m.ctx.ScreenWidth - m.ctx.PreviewWidth

0 commit comments

Comments
 (0)