diff --git a/examples/prevent-quit/main.go b/examples/prevent-quit/main.go index d61ac0d7ca..618223ec36 100644 --- a/examples/prevent-quit/main.go +++ b/examples/prevent-quit/main.go @@ -20,14 +20,14 @@ var ( ) func main() { - p := tea.NewProgram(initialModel(), tea.WithFilter(filter)) + p := tea.NewProgram(initialModel(), tea.WithFilters(preventUnsavedFilter)) if _, err := p.Run(); err != nil { log.Fatal(err) } } -func filter(teaModel tea.Model, msg tea.Msg) tea.Msg { +func preventUnsavedFilter(teaModel tea.Model, msg tea.Msg) tea.Msg { if _, ok := msg.(tea.QuitMsg); !ok { return msg } diff --git a/options.go b/options.go index dfd8cda4f8..19dd60ff2b 100644 --- a/options.go +++ b/options.go @@ -4,6 +4,7 @@ import ( "context" "io" "sync/atomic" + "time" "github.com/charmbracelet/colorprofile" ) @@ -101,17 +102,23 @@ func WithoutRenderer() ProgramOption { } } -// WithFilter supplies an event filter that will be invoked before Bubble Tea -// processes a tea.Msg. The event filter can return any tea.Msg which will then -// get handled by Bubble Tea instead of the original event. If the event filter -// returns nil, the event will be ignored and Bubble Tea will not process it. +// MsgFilter is a function that can be used to filter messages before they are +// processed by Bubble Tea. If the provided function returns nil, the message will +// be ignored and Bubble Tea will not process it. +type MsgFilter func(Model, Msg) Msg + +// WithFilters supplies one or more message filters that will be invoked before +// Bubble Tea processes a [Msg]. The message filter can return any [Msg] which +// will then get handled by Bubble Tea instead of the original message. If the +// filter returns nil for a specific message, the message will be ignored and +// Bubble Tea will not process it, and not continue to the next filter. // // As an example, this could be used to prevent a program from shutting down if -// there are unsaved changes. +// there are unsaved changes, or used to throttle/drop high-frequency messages. // -// Example: +// Example -- preventing a program from shutting down if there are unsaved changes: // -// func filter(m tea.Model, msg tea.Msg) tea.Msg { +// func preventUnsavedFilter(m tea.Model, msg tea.Msg) tea.Msg { // if _, ok := msg.(tea.QuitMsg); !ok { // return msg // } @@ -124,15 +131,54 @@ func WithoutRenderer() ProgramOption { // return msg // } // -// p := tea.NewProgram(Model{}, tea.WithFilter(filter)); +// p := tea.NewProgram(Model{}, tea.WithFilters(preventUnsavedFilter)); // // if _,err := p.Run(); err != nil { // fmt.Println("Error running program:", err) // os.Exit(1) // } -func WithFilter(filter func(Model, Msg) Msg) ProgramOption { +func WithFilters(filters ...MsgFilter) ProgramOption { return func(p *Program) { - p.filter = filter + if len(filters) == 0 { + p.filter = nil + return + } + p.filter = func(m Model, msg Msg) Msg { + for _, filter := range filters { + msg = filter(m, msg) + if msg == nil { + return nil + } + } + return msg + } + } +} + +// MouseThrottleFilter is a message filter that throttles [MouseWheelMsg] and +// [MouseMotionMsg] messages. This is particularly useful when enabling +// [MouseModeCellMotion] or [MouseModeAllMotion] mouse modes, which can often +// send excessive messages when the user is moving the mouse very fast, causing +// high-resource usage and sluggish re-rendering. +// +// If the provided throttle duration is 0, the default value of 15ms will be used. +func MouseThrottleFilter(throttle time.Duration) MsgFilter { + if throttle <= 0 { + throttle = 15 * time.Millisecond + } + + var lastMouseMsg, now time.Time + + return func(_ Model, msg Msg) Msg { + switch msg.(type) { + case MouseWheelMsg, MouseMotionMsg: + now = time.Now() + if now.Sub(lastMouseMsg) < throttle { + return nil + } + lastMouseMsg = now + } + return msg } } diff --git a/options_test.go b/options_test.go index 170de1ba2e..d1ae136202 100644 --- a/options_test.go +++ b/options_test.go @@ -32,7 +32,7 @@ func TestOptions(t *testing.T) { }) t.Run("filter", func(t *testing.T) { - p := NewProgram(nil, WithFilter(func(_ Model, msg Msg) Msg { return msg })) + p := NewProgram(nil, WithFilters(func(_ Model, msg Msg) Msg { return msg })) if p.filter == nil { t.Errorf("expected filter to be set") } diff --git a/tea.go b/tea.go index a30685b19d..ce81c84108 100644 --- a/tea.go +++ b/tea.go @@ -440,38 +440,9 @@ type Program struct { // cleanup on exit. disableCatchPanics bool - // filter supplies an event filter that will be invoked before Bubble Tea - // processes a tea.Msg. The event filter can return any tea.Msg which will - // then get handled by Bubble Tea instead of the original event. If the - // event filter returns nil, the event will be ignored and Bubble Tea will - // not process it. - // - // As an example, this could be used to prevent a program from shutting - // down if there are unsaved changes. - // - // Example: - // - // func filter(m tea.Model, msg tea.Msg) tea.Msg { - // if _, ok := msg.(tea.QuitMsg); !ok { - // return msg - // } - // - // model := m.(myModel) - // if model.hasChanges { - // return nil - // } - // - // return msg - // } - // - // p := tea.NewProgram(Model{}); - // p.filter = filter - // - // if _,err := p.Run(context.Background()); err != nil { - // fmt.Println("Error running program:", err) - // os.Exit(1) - // } - filter func(Model, Msg) Msg + // filter provides a way of filtering messages before they are processed by + // Bubble Tea. See [WithFilters] for more information. + filter MsgFilter // fps sets a custom maximum fps at which the renderer should run. If less // than 1, the default value of 60 will be used. If over 120, the fps will