mirror of
https://github.com/charmbracelet/bubbles.git
synced 2026-07-22 11:39:28 -06:00
chore(textarea,cursor): flesh out real cursor API
This commit is contained in:
parent
a0d9fe396d
commit
9d5d140b3c
@ -1,3 +1,8 @@
|
||||
// Package cursor provides a virtual cursor to support the textinput and
|
||||
// textarea elements.
|
||||
//
|
||||
// Both the textinput and textarea elements also use this package to style an
|
||||
// optional real cursor.
|
||||
package cursor
|
||||
|
||||
import (
|
||||
@ -51,25 +56,49 @@ func (c Mode) String() string {
|
||||
|
||||
// Model is the Bubble Tea model for this cursor element.
|
||||
type Model struct {
|
||||
BlinkSpeed time.Duration
|
||||
// Style for styling the cursor block.
|
||||
// Style styles the cursor block.
|
||||
//
|
||||
// For real cursors, the foreground color set here will be used as the
|
||||
// cursor color.
|
||||
Style lipgloss.Style
|
||||
// TextStyle is the style used for the cursor when it is hidden (when blinking).
|
||||
// I.e. displaying normal text.
|
||||
TextStyle lipgloss.Style
|
||||
|
||||
// Shape is the cursor shape. The following shapes are available:
|
||||
//
|
||||
// - tea.CursorBlock
|
||||
// - tea.CursorUnderline
|
||||
// - tea.CursorBar
|
||||
//
|
||||
// This is only used for real cursors.
|
||||
Shape tea.CursorShape
|
||||
|
||||
// BlinkedStyle is the style used for the cursor when it is blinking
|
||||
// (hidden), i.e. displaying normal text. This has no effect on real
|
||||
// cursors.
|
||||
BlinkedStyle lipgloss.Style
|
||||
|
||||
// BlinkSpeed is the speed at which the cursor blinks. This has no effect
|
||||
// on real cursors as well as no effect if the [CursorMode] is not set to
|
||||
// [CursorBlink].
|
||||
BlinkSpeed time.Duration
|
||||
|
||||
// Blink is the cursor blink state.
|
||||
Blink bool
|
||||
|
||||
// char is the character under the cursor
|
||||
char string
|
||||
|
||||
// The ID of this Model as it relates to other cursors
|
||||
id int
|
||||
|
||||
// focus indicates whether the containing input is focused
|
||||
focus bool
|
||||
// Cursor Blink state.
|
||||
Blink bool
|
||||
|
||||
// Used to manage cursor blink
|
||||
blinkCtx *blinkCtx
|
||||
|
||||
// The ID of the blink message we're expecting to receive.
|
||||
blinkTag int
|
||||
|
||||
// mode determines the behavior of the cursor
|
||||
mode Mode
|
||||
}
|
||||
@ -212,7 +241,7 @@ func (m *Model) SetChar(char string) {
|
||||
// View displays the cursor.
|
||||
func (m Model) View() string {
|
||||
if m.Blink {
|
||||
return m.TextStyle.Inline(true).Render(m.char)
|
||||
return m.BlinkedStyle.Inline(true).Render(m.char)
|
||||
}
|
||||
return m.Style.Inline(true).Reverse(true).Render(m.char)
|
||||
}
|
||||
|
||||
@ -102,19 +102,25 @@ func DefaultKeyMap() KeyMap {
|
||||
type LineInfo struct {
|
||||
// Width is the number of columns in the line.
|
||||
Width int
|
||||
|
||||
// CharWidth is the number of characters in the line to account for
|
||||
// double-width runes.
|
||||
CharWidth int
|
||||
|
||||
// Height is the number of rows in the line.
|
||||
Height int
|
||||
|
||||
// StartColumn is the index of the first column of the line.
|
||||
StartColumn int
|
||||
|
||||
// ColumnOffset is the number of columns that the cursor is offset from the
|
||||
// start of the line.
|
||||
ColumnOffset int
|
||||
|
||||
// RowOffset is the number of rows that the cursor is offset from the start
|
||||
// of the line.
|
||||
RowOffset int
|
||||
|
||||
// CharOffset is the number of characters that the cursor is offset
|
||||
// from the start of the line. This will generally be equivalent to
|
||||
// ColumnOffset, but will be different there are double-width runes before
|
||||
@ -231,8 +237,27 @@ type Model struct {
|
||||
// when switching focus states.
|
||||
activeStyle *StyleState
|
||||
|
||||
// VirtualCursor is the text area cursor.
|
||||
VirtualCursor cursor.Model
|
||||
// Cursor manages the virtual cursor and contains styling settings for
|
||||
// both a real and virtual cursor.
|
||||
Cursor cursor.Model
|
||||
|
||||
// UseRealCursor determines whether or not to use the real cursor. By
|
||||
// default, a virtual cursor is used.
|
||||
//
|
||||
// When [UseRealCursor] is enabled, the virual cursor is hidden and you
|
||||
// must use [Model.RealCursor] to produce a real cursor for a [tea.Frame].
|
||||
//
|
||||
// Note that you will almost certainly also need to adjust the offset
|
||||
// postion of the textarea to properly set the cursor position.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // In your top-level View function:
|
||||
// f := tea.NewFrame(m.textarea.View())
|
||||
// f.Cursor = m.textarea.RealCursor()
|
||||
// f.Cursor.Position.X += offsetX
|
||||
// f.Cursor.Position.Y += offsetY
|
||||
UseRealCursor bool
|
||||
|
||||
// CharLimit is the maximum number of characters this input element will
|
||||
// accept. If 0 or less, there's no limit.
|
||||
@ -305,7 +330,7 @@ func New() Model {
|
||||
cache: memoization.NewMemoCache[line, [][]rune](maxLines),
|
||||
EndOfBufferCharacter: ' ',
|
||||
ShowLineNumbers: true,
|
||||
VirtualCursor: cur,
|
||||
Cursor: cur,
|
||||
KeyMap: DefaultKeyMap(),
|
||||
|
||||
value: make([][]rune, minHeight, maxLines),
|
||||
@ -600,7 +625,7 @@ func (m Model) Focused() bool {
|
||||
func (m *Model) Focus() tea.Cmd {
|
||||
m.focus = true
|
||||
m.activeStyle = &m.Styles.Focused
|
||||
return m.VirtualCursor.Focus()
|
||||
return m.Cursor.Focus()
|
||||
}
|
||||
|
||||
// Blur removes the focus state on the model. When the model is blurred it can
|
||||
@ -608,7 +633,7 @@ func (m *Model) Focus() tea.Cmd {
|
||||
func (m *Model) Blur() {
|
||||
m.focus = false
|
||||
m.activeStyle = &m.Styles.Blurred
|
||||
m.VirtualCursor.Blur()
|
||||
m.Cursor.Blur()
|
||||
}
|
||||
|
||||
// Reset sets the input to its default state with no input.
|
||||
@ -976,7 +1001,7 @@ func (m *Model) SetHeight(h int) {
|
||||
// Update is the Bubble Tea update loop.
|
||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
if !m.focus {
|
||||
m.VirtualCursor.Blur()
|
||||
m.Cursor.Blur()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@ -1098,10 +1123,10 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
newRow, newCol := m.cursorLineNumber(), m.col
|
||||
m.VirtualCursor, cmd = m.VirtualCursor.Update(msg)
|
||||
if (newRow != oldRow || newCol != oldCol) && m.VirtualCursor.Mode() == cursor.CursorBlink {
|
||||
m.VirtualCursor.Blink = false
|
||||
cmd = m.VirtualCursor.BlinkCmd()
|
||||
m.Cursor, cmd = m.Cursor.Update(msg)
|
||||
if (newRow != oldRow || newCol != oldCol) && m.Cursor.Mode() == cursor.CursorBlink {
|
||||
m.Cursor.Blink = false
|
||||
cmd = m.Cursor.BlinkCmd()
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
@ -1115,7 +1140,7 @@ func (m Model) View() string {
|
||||
if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" {
|
||||
return m.placeholderView()
|
||||
}
|
||||
m.VirtualCursor.TextStyle = m.activeStyle.computedCursorLine()
|
||||
m.Cursor.BlinkedStyle = m.activeStyle.computedCursorLine()
|
||||
|
||||
var (
|
||||
s strings.Builder
|
||||
@ -1184,11 +1209,11 @@ func (m Model) View() string {
|
||||
if m.row == l && lineInfo.RowOffset == wl {
|
||||
s.WriteString(style.Render(string(wrappedLine[:lineInfo.ColumnOffset])))
|
||||
if m.col >= len(line) && lineInfo.CharOffset >= m.width {
|
||||
m.VirtualCursor.SetChar(" ")
|
||||
s.WriteString(m.VirtualCursor.View())
|
||||
m.Cursor.SetChar(" ")
|
||||
s.WriteString(m.Cursor.View())
|
||||
} else {
|
||||
m.VirtualCursor.SetChar(string(wrappedLine[lineInfo.ColumnOffset]))
|
||||
s.WriteString(style.Render(m.VirtualCursor.View()))
|
||||
m.Cursor.SetChar(string(wrappedLine[lineInfo.ColumnOffset]))
|
||||
s.WriteString(style.Render(m.Cursor.View()))
|
||||
s.WriteString(style.Render(string(wrappedLine[lineInfo.ColumnOffset+1:])))
|
||||
}
|
||||
} else {
|
||||
@ -1291,9 +1316,9 @@ func (m Model) placeholderView() string {
|
||||
// first line
|
||||
case i == 0:
|
||||
// first character of first line as cursor with character
|
||||
m.VirtualCursor.TextStyle = m.activeStyle.computedPlaceholder()
|
||||
m.VirtualCursor.SetChar(string(plines[0][0]))
|
||||
s.WriteString(lineStyle.Render(m.VirtualCursor.View()))
|
||||
m.Cursor.BlinkedStyle = m.activeStyle.computedPlaceholder()
|
||||
m.Cursor.SetChar(string(plines[0][0]))
|
||||
s.WriteString(lineStyle.Render(m.Cursor.View()))
|
||||
|
||||
// the rest of the first line
|
||||
s.WriteString(lineStyle.Render(style.Render(plines[0][1:] + strings.Repeat(" ", max(0, m.width-uniseg.StringWidth(plines[0]))))))
|
||||
@ -1317,21 +1342,24 @@ func (m Model) placeholderView() string {
|
||||
return m.activeStyle.Base.Render(m.viewport.View())
|
||||
}
|
||||
|
||||
// Blink returns the blink command for the cursor.
|
||||
// Blink returns the blink command for the virtual cursor.
|
||||
func Blink() tea.Msg {
|
||||
return cursor.Blink()
|
||||
}
|
||||
|
||||
// Cursor returns the current cursor position accounting any
|
||||
// soft-wrapped lines.
|
||||
func (m Model) Cursor() *tea.Cursor {
|
||||
// RealCursor returns a [tea.Cursor] for rendering a real cursor in a Bubble
|
||||
// Tea program.
|
||||
func (m Model) RealCursor() *tea.Cursor {
|
||||
if m.Cursor.Mode() == cursor.CursorHide {
|
||||
return nil
|
||||
}
|
||||
|
||||
lineInfo := m.LineInfo()
|
||||
x, y := lineInfo.CharOffset, m.cursorLineNumber()-m.viewport.YOffset
|
||||
|
||||
// TODO: sort out where these properties live.
|
||||
c := tea.NewCursor(x, y)
|
||||
c.Blink = true
|
||||
c.Color = m.VirtualCursor.Style.GetForeground()
|
||||
c.Blink = m.Cursor.Mode() == cursor.CursorBlink
|
||||
c.Color = m.Cursor.Style.GetForeground()
|
||||
return c
|
||||
}
|
||||
|
||||
|
||||
@ -671,7 +671,7 @@ func (m Model) View() string {
|
||||
if m.canAcceptSuggestion() {
|
||||
suggestion := m.matchedSuggestions[m.currentSuggestionIndex]
|
||||
if len(value) < len(suggestion) {
|
||||
m.Cursor.TextStyle = m.CompletionStyle
|
||||
m.Cursor.BlinkedStyle = m.CompletionStyle
|
||||
m.Cursor.SetChar(m.echoTransform(string(suggestion[pos])))
|
||||
v += m.Cursor.View()
|
||||
v += m.completionView(1)
|
||||
@ -709,7 +709,7 @@ func (m Model) placeholderView() string {
|
||||
p := make([]rune, m.Width()+1)
|
||||
copy(p, []rune(m.Placeholder))
|
||||
|
||||
m.Cursor.TextStyle = m.PlaceholderStyle
|
||||
m.Cursor.BlinkedStyle = m.PlaceholderStyle
|
||||
m.Cursor.SetChar(string(p[:1]))
|
||||
v += m.Cursor.View()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user