mirror of
https://github.com/charmbracelet/crush.git
synced 2025-08-02 05:20:46 +03:00
chore: add mouse support
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user