@@ -67,6 +67,7 @@ func New(app *app.App) MessageListCmp {
list.WithDirectionBackward(),
list.WithFocus(false),
list.WithKeyMap(defaultListKeyMap),
+ list.WithEnableMouse(),
)
return &messageListCmp{
app: app,
@@ -97,6 +98,11 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case pubsub.Event[message.Message]:
cmd := m.handleMessageEvent(msg)
return m, cmd
+
+ case tea.MouseWheelMsg:
+ u, cmd := m.listCmp.Update(msg)
+ m.listCmp = u.(list.List[list.Item])
+ return m, cmd
default:
var cmds []tea.Cmd
u, cmd := m.listCmp.Update(msg)
@@ -23,7 +23,6 @@ type HasAnim interface {
Item
Spinning() bool
}
-type renderedMsg struct{}
type List[T Item] interface {
util.Model
@@ -77,6 +76,7 @@ type confOptions struct {
selectedItem string
focused bool
resize bool
+ enableMouse bool
}
type list[T Item] struct {
@@ -156,6 +156,12 @@ func WithResizeByList() ListOption {
}
}
+func WithEnableMouse() ListOption {
+ return func(l *confOptions) {
+ l.enableMouse = true
+ }
+}
+
func New[T Item](items []T, opts ...ListOption) List[T] {
list := &list[T]{
confOptions: &confOptions{
@@ -188,6 +194,11 @@ func (l *list[T]) Init() tea.Cmd {
// Update implements List.
func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
+ case tea.MouseWheelMsg:
+ if l.enableMouse {
+ return l.handleMouseWheel(msg)
+ }
+ return l, nil
case anim.StepMsg:
var cmds []tea.Cmd
for _, item := range l.items.Slice() {
@@ -229,6 +240,17 @@ func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return l, nil
}
+func (l *list[T]) handleMouseWheel(msg tea.MouseWheelMsg) (tea.Model, tea.Cmd) {
+ var cmd tea.Cmd
+ switch msg.Button {
+ case tea.MouseWheelDown:
+ cmd = l.MoveDown(ViewportDefaultScrollSize)
+ case tea.MouseWheelUp:
+ cmd = l.MoveUp(ViewportDefaultScrollSize)
+ }
+ return l, cmd
+}
+
// View implements List.
func (l *list[T]) View() string {
if l.height <= 0 || l.width <= 0 {
@@ -292,9 +314,8 @@ func (l *list[T]) render() tea.Cmd {
}
// we are not rendering the first time
if l.rendered != "" {
- l.rendered = ""
// rerender everything will mostly hit cache
- _ = l.renderIterator(0, false)
+ l.rendered, _ = l.renderIterator(0, false, "")
if l.direction == DirectionBackward {
l.recalculateItemPositions()
}
@@ -304,14 +325,17 @@ func (l *list[T]) render() tea.Cmd {
}
return focusChangeCmd
}
- finishIndex := l.renderIterator(0, true)
+ rendered, finishIndex := l.renderIterator(0, true, "")
+ l.rendered = rendered
+
// recalculate for the initial items
if l.direction == DirectionBackward {
l.recalculateItemPositions()
}
renderCmd := func() tea.Msg {
+ l.offset = 0
// render the rest
- _ = l.renderIterator(finishIndex, false)
+ l.rendered, _ = l.renderIterator(finishIndex, false, l.rendered)
// needed for backwards
if l.direction == DirectionBackward {
l.recalculateItemPositions()
@@ -321,7 +345,7 @@ func (l *list[T]) render() tea.Cmd {
l.scrollToSelection()
}
- return renderedMsg{}
+ return nil
}
return tea.Batch(focusChangeCmd, renderCmd)
}
@@ -568,13 +592,14 @@ func (l *list[T]) blurSelectedItem() tea.Cmd {
}
// render iterator renders items starting from the specific index and limits hight if limitHeight != -1
-// returns the last index
-func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
- currentContentHeight := lipgloss.Height(l.rendered) - 1
+// returns the last index and the rendered content so far
+// we pass the rendered content around and don't use l.rendered to prevent jumping of the content
+func (l *list[T]) renderIterator(startInx int, limitHeight bool, rendered string) (string, int) {
+ currentContentHeight := lipgloss.Height(rendered) - 1
itemsLen := l.items.Len()
for i := startInx; i < itemsLen; i++ {
if currentContentHeight >= l.height && limitHeight {
- return i
+ return rendered, i
}
// cool way to go through the list in both directions
inx := i
@@ -602,13 +627,13 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
}
if l.direction == DirectionForward {
- l.rendered += rItem.view + strings.Repeat("\n", gap)
+ rendered += rItem.view + strings.Repeat("\n", gap)
} else {
- l.rendered = rItem.view + strings.Repeat("\n", gap) + l.rendered
+ rendered = rItem.view + strings.Repeat("\n", gap) + rendered
}
currentContentHeight = rItem.end + 1 + l.gap
}
- return itemsLen
+ return rendered, itemsLen
}
func (l *list[T]) renderItem(item Item) renderedItem {
@@ -164,6 +164,13 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyboardEnhancementsMsg:
p.keyboardEnhancements = msg
return p, nil
+ case tea.MouseWheelMsg:
+ if p.isMouseOverChat(msg.Mouse().X, msg.Mouse().Y) {
+ u, cmd := p.chat.Update(msg)
+ p.chat = u.(chat.MessageListCmp)
+ return p, cmd
+ }
+ return p, nil
case tea.WindowSizeMsg:
return p, p.SetSize(msg.Width, msg.Height)
case CancelTimerExpiredMsg:
@@ -906,3 +913,31 @@ func (p *chatPage) Help() help.KeyMap {
func (p *chatPage) IsChatFocused() bool {
return p.focusedPane == PanelTypeChat
}
+
+// isMouseOverChat checks if the given mouse coordinates are within the chat area bounds.
+// Returns true if the mouse is over the chat area, false otherwise.
+func (p *chatPage) isMouseOverChat(x, y int) bool {
+ // No session means no chat area
+ if p.session.ID == "" {
+ return false
+ }
+
+ var chatX, chatY, chatWidth, chatHeight int
+
+ if p.compact {
+ // In compact mode: chat area starts after header and spans full width
+ chatX = 0
+ chatY = HeaderHeight
+ chatWidth = p.width
+ chatHeight = p.height - EditorHeight - HeaderHeight
+ } else {
+ // In non-compact mode: chat area spans from left edge to sidebar
+ chatX = 0
+ chatY = 0
+ chatWidth = p.width - SideBarWidth
+ chatHeight = p.height - EditorHeight
+ }
+
+ // Check if mouse coordinates are within chat bounds
+ return x >= chatX && x < chatX+chatWidth && y >= chatY && y < chatY+chatHeight
+}
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strings"
+ "time"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
@@ -33,26 +34,18 @@ import (
"github.com/charmbracelet/lipgloss/v2"
)
-// MouseEventFilter filters mouse events based on the current focus state
-// This is used with tea.WithFilter to prevent mouse scroll events from
-// interfering with typing performance in the editor
+var lastMouseEvent time.Time
+
func MouseEventFilter(m tea.Model, msg tea.Msg) tea.Msg {
- // Only filter mouse events
switch msg.(type) {
case tea.MouseWheelMsg, tea.MouseMotionMsg:
- // Check if we have an appModel and if editor is focused
- if appModel, ok := m.(*appModel); ok {
- if appModel.currentPage == chat.ChatPageID {
- if chatPage, ok := appModel.pages[appModel.currentPage].(chat.ChatPage); ok {
- // If editor is focused (not chatFocused), filter out mouse wheel/motion events
- if !chatPage.IsChatFocused() {
- return nil // Filter out the event
- }
- }
- }
+ now := time.Now()
+ // trackpad is sending too many requests
+ if now.Sub(lastMouseEvent) < 5*time.Millisecond {
+ return nil
}
+ lastMouseEvent = now
}
- // Allow all other events to pass through
return msg
}