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)
[![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)
[![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.
@@ -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>
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) {
switch msg := msg.(type) {
case TickMsg:
// 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 spinning too fast.
// thus spinning too fast.
if msg.tag > 0 && msg.tag != m.tag {
return m, nil
}

View File

@@ -3,6 +3,7 @@ package textinput
import (
"context"
"strings"
"sync"
"time"
"unicode"
@@ -14,11 +15,35 @@ import (
const defaultBlinkSpeed = time.Millisecond * 530
// blinkMsg and blinkCanceled are used to manage cursor blinking.
type blinkMsg struct{}
// Internal ID management for text inputs. Necessary for blink integrity when
// 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{}
// Messages for clipboard events.
// Internal messages for clipboard operations.
type pasteMsg string
type pasteErrMsg struct{ error }
@@ -46,14 +71,26 @@ type blinkCtx struct {
cancel context.CancelFunc
}
type cursorMode int
// CursorMode describes the behavior of the cursor.
type CursorMode int
// Available cursor modes.
const (
cursorBlink = iota
cursorStatic
cursorHide
CursorBlink CursorMode = iota
CursorStatic
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.
type Model struct {
Err error
@@ -84,11 +121,17 @@ type Model struct {
// viewport. If 0 or less this setting is ignored.
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.
value []rune
// Focus indicates whether user input focus should be on this input
// component. When false, don't blink and ignore keyboard input.
// focus indicates whether user input focus should be on this input
// component. When false, ignore keyboard input and hide the cursor.
focus bool
// Cursor blink state.
@@ -106,7 +149,7 @@ type Model struct {
blinkCtx *blinkCtx
// cursorMode determines the behavior of the cursor
cursorMode cursorMode
cursorMode CursorMode
}
// NewModel creates a new model with default settings.
@@ -118,11 +161,12 @@ func NewModel() Model {
CharLimit: 0,
PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
id: nextID(),
value: nil,
focus: false,
blink: true,
pos: 0,
cursorMode: cursorBlink,
cursorMode: CursorBlink,
blinkCtx: &blinkCtx{
ctx: context.Background(),
@@ -168,10 +212,10 @@ func (m *Model) setCursor(pos int) bool {
m.handleOverflow()
// Show the cursor unless it's been explicitly hidden
m.blink = m.cursorMode == cursorHide
m.blink = m.cursorMode == CursorHide
// Reset cursor blink if necessary
return m.cursorMode == cursorBlink
return m.cursorMode == CursorBlink
}
// CursorStart moves the cursor to the start of the input field.
@@ -190,8 +234,26 @@ func (m *Model) 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
// or not
// the cursor should blink should reset.
func (m *Model) cursorEnd() bool {
return m.setCursor(len(m.value))
}
@@ -201,13 +263,20 @@ func (m Model) Focused() bool {
return m.focus
}
// Focus sets the focus state on the model.
func (m *Model) Focus() {
// Focus sets the focus state on the model. When the model is in focus it can
// receive keyboard input and the cursor will be hidden.
func (m *Model) Focus() tea.Cmd {
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() {
m.focus = false
m.blink = true
@@ -221,7 +290,7 @@ func (m *Model) Reset() bool {
}
// 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 {
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:
// 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
if m.cursorMode == cursorBlink {
if m.cursorMode == CursorBlink {
m.blink = !m.blink
cmd = m.blinkCmd()
}
@@ -653,7 +745,11 @@ func (m Model) cursorView(v string) string {
}
// 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 {
m.blinkCtx.cancel()
}
@@ -661,11 +757,13 @@ func (m Model) blinkCmd() tea.Cmd {
ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)
m.blinkCtx.cancel = cancel
m.blinkTag++
return func() tea.Msg {
defer cancel()
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded {
return blinkMsg{}
return blinkMsg{id: m.id, tag: m.blinkTag}
}
return blinkCanceled{}
}
@@ -673,7 +771,7 @@ func (m Model) blinkCmd() tea.Cmd {
// Blink is a command used to initialize cursor blinking.
func Blink() tea.Msg {
return blinkMsg{}
return initialBlinkMsg{}
}
// Paste is a command for pasting from the clipboard into the text input.