6 Commits

Author SHA1 Message Date
Christian Rocha
d06aa4aa48 Ensure that textinputs can only receive their own blink messages 2021-06-02 10:49:43 -04:00
Christian Rocha
10962af2f2 Textinput's focus method now returns a command to initiate cursor blink 2021-06-02 10:49:43 -04:00
Christian Rocha
5082ae6f31 Add API for blink, static and hidden cursor modes to textinput
Closes #53.
2021-06-02 10:49:43 -04:00
Christian Rocha
4365990396 Fix typo in a spinner comment 2021-06-02 10:49:43 -04:00
Christian Rocha
1405b09ec8 Update footer in README for consistency with newer Charm repos 2021-06-02 10:49:43 -04:00
Christian Muehlhaeuser
f53c742d6c Fix goreportcard badge 2021-05-18 20:56:27 +02:00
3 changed files with 124 additions and 27 deletions

View File

@@ -8,7 +8,7 @@ Bubbles
[![Latest Release](https://img.shields.io/github/release/charmbracelet/bubbles.svg)](https://github.com/charmbracelet/bubbles/releases) [![Latest Release](https://img.shields.io/github/release/charmbracelet/bubbles.svg)](https://github.com/charmbracelet/bubbles/releases)
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/bubbles) [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/bubbles)
[![Build Status](https://github.com/charmbracelet/bubbles/workflows/build/badge.svg)](https://github.com/charmbracelet/bubbles/actions) [![Build Status](https://github.com/charmbracelet/bubbles/workflows/build/badge.svg)](https://github.com/charmbracelet/bubbles/actions)
[![Go ReportCard](http://goreportcard.com/badge/charmbracelet/bubbles)](http://goreportcard.com/report/charmbracelet/bubbles) [![Go ReportCard](https://goreportcard.com/badge/charmbracelet/bubbles)](https://goreportcard.com/report/charmbracelet/bubbles)
Some components for [Bubble Tea](https://github.com/charmbracelet/bubbletea) applications. Some components for [Bubble Tea](https://github.com/charmbracelet/bubbletea) applications.
@@ -91,4 +91,4 @@ Part of [Charm](https://charm.sh).
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge-unrounded.jpg" width="400"></a> <a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge-unrounded.jpg" width="400"></a>
Charm热爱开源! / Charm loves open source! Charm热爱开源 Charm loves open source

View File

@@ -188,10 +188,9 @@ type TickMsg struct {
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case TickMsg: case TickMsg:
// If a tag is set, and it's not the one we expect, reject the message. // If a tag is set, and it's not the one we expect, reject the message.
// This prevents the spinner from receiving too many messages and // This prevents the spinner from receiving too many messages and
// this spinning too fast. // thus spinning too fast.
if msg.tag > 0 && msg.tag != m.tag { if msg.tag > 0 && msg.tag != m.tag {
return m, nil return m, nil
} }

View File

@@ -3,6 +3,7 @@ package textinput
import ( import (
"context" "context"
"strings" "strings"
"sync"
"time" "time"
"unicode" "unicode"
@@ -14,11 +15,35 @@ import (
const defaultBlinkSpeed = time.Millisecond * 530 const defaultBlinkSpeed = time.Millisecond * 530
// blinkMsg and blinkCanceled are used to manage cursor blinking. // Internal ID management for text inputs. Necessary for blink integrity when
type blinkMsg struct{} // multiple text inputs are involved.
var (
lastID int
idMtx sync.Mutex
)
// Return the next ID we should use on the Model.
func nextID() int {
idMtx.Lock()
defer idMtx.Unlock()
lastID++
return lastID
}
// initialBlinkMsg initializes cursor blinking.
type initialBlinkMsg struct{}
// blinkMsg signals that the cursor should blink. It contains metadata that
// allows us to tell if the blink message is the one we're expecting.
type blinkMsg struct {
id int
tag int
}
// blinkCanceled is sent when a blink operation is canceled.
type blinkCanceled struct{} type blinkCanceled struct{}
// Messages for clipboard events. // Internal messages for clipboard operations.
type pasteMsg string type pasteMsg string
type pasteErrMsg struct{ error } type pasteErrMsg struct{ error }
@@ -46,14 +71,26 @@ type blinkCtx struct {
cancel context.CancelFunc cancel context.CancelFunc
} }
type cursorMode int // CursorMode describes the behavior of the cursor.
type CursorMode int
// Available cursor modes.
const ( const (
cursorBlink = iota CursorBlink CursorMode = iota
cursorStatic CursorStatic
cursorHide CursorHide
) )
// String returns a the cursor mode in a human-readable format. This method is
// provisional and for informational purposes only.
func (c CursorMode) String() string {
return [...]string{
"blink",
"static",
"hidden",
}[c]
}
// Model is the Bubble Tea model for this text input element. // Model is the Bubble Tea model for this text input element.
type Model struct { type Model struct {
Err error Err error
@@ -84,11 +121,17 @@ type Model struct {
// viewport. If 0 or less this setting is ignored. // viewport. If 0 or less this setting is ignored.
Width int Width int
// The ID of this Model as it relates to other textinput Models.
id int
// The ID of the blink message we're expecting to receive.
blinkTag int
// Underlying text value. // Underlying text value.
value []rune value []rune
// Focus indicates whether user input focus should be on this input // focus indicates whether user input focus should be on this input
// component. When false, don't blink and ignore keyboard input. // component. When false, ignore keyboard input and hide the cursor.
focus bool focus bool
// Cursor blink state. // Cursor blink state.
@@ -106,7 +149,7 @@ type Model struct {
blinkCtx *blinkCtx blinkCtx *blinkCtx
// cursorMode determines the behavior of the cursor // cursorMode determines the behavior of the cursor
cursorMode cursorMode cursorMode CursorMode
} }
// NewModel creates a new model with default settings. // NewModel creates a new model with default settings.
@@ -118,11 +161,12 @@ func NewModel() Model {
CharLimit: 0, CharLimit: 0,
PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
id: nextID(),
value: nil, value: nil,
focus: false, focus: false,
blink: true, blink: true,
pos: 0, pos: 0,
cursorMode: cursorBlink, cursorMode: CursorBlink,
blinkCtx: &blinkCtx{ blinkCtx: &blinkCtx{
ctx: context.Background(), ctx: context.Background(),
@@ -168,10 +212,10 @@ func (m *Model) setCursor(pos int) bool {
m.handleOverflow() m.handleOverflow()
// Show the cursor unless it's been explicitly hidden // Show the cursor unless it's been explicitly hidden
m.blink = m.cursorMode == cursorHide m.blink = m.cursorMode == CursorHide
// Reset cursor blink if necessary // Reset cursor blink if necessary
return m.cursorMode == cursorBlink return m.cursorMode == CursorBlink
} }
// CursorStart moves the cursor to the start of the input field. // CursorStart moves the cursor to the start of the input field.
@@ -190,8 +234,26 @@ func (m *Model) CursorEnd() {
m.cursorEnd() m.cursorEnd()
} }
// CursorMode returns the model's cursor mode. For available cursor modes, see
// type CursorMode.
func (m Model) CursorMode() CursorMode {
return m.cursorMode
}
// CursorMode sets the model's cursor mode. This method returns a command.
//
// For available cursor modes, see type CursorMode.
func (m *Model) SetCursorMode(mode CursorMode) tea.Cmd {
m.cursorMode = mode
m.blink = m.cursorMode == CursorHide || !m.focus
if mode == CursorBlink {
return Blink
}
return nil
}
// cursorEnd moves the cursor to the end of the input field and returns whether // cursorEnd moves the cursor to the end of the input field and returns whether
// or not // the cursor should blink should reset.
func (m *Model) cursorEnd() bool { func (m *Model) cursorEnd() bool {
return m.setCursor(len(m.value)) return m.setCursor(len(m.value))
} }
@@ -201,13 +263,20 @@ func (m Model) Focused() bool {
return m.focus return m.focus
} }
// Focus sets the focus state on the model. // Focus sets the focus state on the model. When the model is in focus it can
func (m *Model) Focus() { // receive keyboard input and the cursor will be hidden.
func (m *Model) Focus() tea.Cmd {
m.focus = true m.focus = true
m.blink = m.cursorMode == cursorHide // show the cursor unless we've explicitly hidden it m.blink = m.cursorMode == CursorHide // show the cursor unless we've explicitly hidden it
if m.cursorMode == CursorBlink && m.focus {
return m.blinkCmd()
}
return nil
} }
// Blur removes the focus state on the model. // Blur removes the focus state on the model. When the model is blurred it can
// not receive keyboard input and the cursor will be hidden.
func (m *Model) Blur() { func (m *Model) Blur() {
m.focus = false m.focus = false
m.blink = true m.blink = true
@@ -221,7 +290,7 @@ func (m *Model) Reset() bool {
} }
// handle a clipboard paste event, if supported. Returns whether or not the // handle a clipboard paste event, if supported. Returns whether or not the
// cursor blink should be reset. // cursor blink should reset.
func (m *Model) handlePaste(v string) bool { func (m *Model) handlePaste(v string) bool {
paste := []rune(v) paste := []rune(v)
@@ -562,9 +631,32 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
} }
} }
case initialBlinkMsg:
// We accept all initialBlinkMsgs genrated by the Blink command.
if m.cursorMode != CursorBlink || !m.focus {
return m, nil
}
cmd := m.blinkCmd()
return m, cmd
case blinkMsg: case blinkMsg:
// We're choosy about whether to accept blinkMsgs so that our cursor
// only exactly when it should.
// Is this model blinkable?
if m.cursorMode != CursorBlink || !m.focus {
return m, nil
}
// Were we expecting this blink message?
if msg.id != m.id || msg.tag != m.blinkTag {
return m, nil
}
var cmd tea.Cmd var cmd tea.Cmd
if m.cursorMode == cursorBlink { if m.cursorMode == CursorBlink {
m.blink = !m.blink m.blink = !m.blink
cmd = m.blinkCmd() cmd = m.blinkCmd()
} }
@@ -653,7 +745,11 @@ func (m Model) cursorView(v string) string {
} }
// blinkCmd is an internal command used to manage cursor blinking. // blinkCmd is an internal command used to manage cursor blinking.
func (m Model) blinkCmd() tea.Cmd { func (m *Model) blinkCmd() tea.Cmd {
if m.cursorMode != CursorBlink {
return nil
}
if m.blinkCtx != nil && m.blinkCtx.cancel != nil { if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
m.blinkCtx.cancel() m.blinkCtx.cancel()
} }
@@ -661,11 +757,13 @@ func (m Model) blinkCmd() tea.Cmd {
ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed) ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)
m.blinkCtx.cancel = cancel m.blinkCtx.cancel = cancel
m.blinkTag++
return func() tea.Msg { return func() tea.Msg {
defer cancel() defer cancel()
<-ctx.Done() <-ctx.Done()
if ctx.Err() == context.DeadlineExceeded { if ctx.Err() == context.DeadlineExceeded {
return blinkMsg{} return blinkMsg{id: m.id, tag: m.blinkTag}
} }
return blinkCanceled{} return blinkCanceled{}
} }
@@ -673,7 +771,7 @@ func (m Model) blinkCmd() tea.Cmd {
// Blink is a command used to initialize cursor blinking. // Blink is a command used to initialize cursor blinking.
func Blink() tea.Msg { func Blink() tea.Msg {
return blinkMsg{} return initialBlinkMsg{}
} }
// Paste is a command for pasting from the clipboard into the text input. // Paste is a command for pasting from the clipboard into the text input.