mirror of
https://github.com/zoriya/bubbles.git
synced 2026-08-05 04:36:07 +00:00
chore(textarea,cursor): use textarea to manage virtual cursor styling
This commit is contained in:
+2
-19
@@ -1,8 +1,5 @@
|
|||||||
// Package cursor provides a virtual cursor to support the textinput and
|
// Package cursor provides a virtual cursor to support the textinput and
|
||||||
// textarea elements.
|
// textarea elements.
|
||||||
//
|
|
||||||
// Both the textinput and textarea elements also use this package to style an
|
|
||||||
// optional real cursor.
|
|
||||||
package cursor
|
package cursor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -57,28 +54,14 @@ func (c Mode) String() string {
|
|||||||
// Model is the Bubble Tea model for this cursor element.
|
// Model is the Bubble Tea model for this cursor element.
|
||||||
type Model struct {
|
type Model struct {
|
||||||
// Style styles 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
|
Style 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
|
// BlinkedStyle is the style used for the cursor when it is blinking
|
||||||
// (hidden), i.e. displaying normal text. This has no effect on real
|
// (hidden), i.e. displaying normal text.
|
||||||
// cursors.
|
|
||||||
BlinkedStyle lipgloss.Style
|
BlinkedStyle lipgloss.Style
|
||||||
|
|
||||||
// BlinkSpeed is the speed at which the cursor blinks. This has no effect
|
// 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
|
// unless [CursorMode] is not set to [CursorBlink].
|
||||||
// [CursorBlink].
|
|
||||||
BlinkSpeed time.Duration
|
BlinkSpeed time.Duration
|
||||||
|
|
||||||
// Blink is the cursor blink state.
|
// Blink is the cursor blink state.
|
||||||
|
|||||||
+118
-46
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/atotto/clipboard"
|
"github.com/atotto/clipboard"
|
||||||
@@ -128,6 +129,39 @@ type LineInfo struct {
|
|||||||
CharOffset int
|
CharOffset int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CursorStyle is the style for real and virtual cursors.
|
||||||
|
type CursorStyle struct {
|
||||||
|
// Style styles the cursor block.
|
||||||
|
//
|
||||||
|
// For real cursors, the foreground color set here will be used as the
|
||||||
|
// cursor color.
|
||||||
|
Style lipgloss.Style
|
||||||
|
|
||||||
|
// BlinkedStyle is the style used for the cursor when it is blinking
|
||||||
|
// (hidden), i.e. displaying normal text. This is only used for virtual
|
||||||
|
// cursors.
|
||||||
|
BlinkedStyle 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
|
||||||
|
|
||||||
|
// CursorBlink determines whether or not the cursor should blink.
|
||||||
|
Blink bool
|
||||||
|
|
||||||
|
// BlinkSpeed is the speed at which the virtualcursor blinks. This has no
|
||||||
|
// effect on real cursors as well as no effect if the cursor is set not
|
||||||
|
// to [CursorBlink].
|
||||||
|
//
|
||||||
|
// By default, the blink speed is set to about 500ms.
|
||||||
|
BlinkSpeed time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
// Styles are the styles for the textarea, separated into focused and blurred
|
// Styles are the styles for the textarea, separated into focused and blurred
|
||||||
// states. The appropriate styles will be chosen based on the focus state of
|
// states. The appropriate styles will be chosen based on the focus state of
|
||||||
// the textarea.
|
// the textarea.
|
||||||
@@ -152,6 +186,7 @@ type StyleState struct {
|
|||||||
Placeholder lipgloss.Style
|
Placeholder lipgloss.Style
|
||||||
Prompt lipgloss.Style
|
Prompt lipgloss.Style
|
||||||
Text lipgloss.Style
|
Text lipgloss.Style
|
||||||
|
Cursor CursorStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s StyleState) computedCursorLine() lipgloss.Style {
|
func (s StyleState) computedCursorLine() lipgloss.Style {
|
||||||
@@ -229,7 +264,7 @@ type Model struct {
|
|||||||
|
|
||||||
// Styling. FocusedStyle and BlurredStyle are used to style the textarea in
|
// Styling. FocusedStyle and BlurredStyle are used to style the textarea in
|
||||||
// focused and blurred states.
|
// focused and blurred states.
|
||||||
Styles Styles
|
styles Styles
|
||||||
|
|
||||||
// activeStyle is the current styling to use.
|
// activeStyle is the current styling to use.
|
||||||
// It is used to abstract the differences in focus state when styling the
|
// It is used to abstract the differences in focus state when styling the
|
||||||
@@ -237,27 +272,12 @@ type Model struct {
|
|||||||
// when switching focus states.
|
// when switching focus states.
|
||||||
activeStyle *StyleState
|
activeStyle *StyleState
|
||||||
|
|
||||||
// Cursor manages the virtual cursor and contains styling settings for
|
// virtualCursor manages the virtual cursor.
|
||||||
// both a real and virtual cursor.
|
virtualCursor cursor.Model
|
||||||
Cursor cursor.Model
|
|
||||||
|
|
||||||
// UseRealCursor determines whether or not to use the real cursor. By
|
// VirtualCursor determines whether or not to use the virtual cursor. If
|
||||||
// default, a virtual cursor is used.
|
// set to false, use [Model.Cursor] to return a real cursor for rendering.
|
||||||
//
|
VirtualCursor bool
|
||||||
// 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
|
// CharLimit is the maximum number of characters this input element will
|
||||||
// accept. If 0 or less, there's no limit.
|
// accept. If 0 or less, there's no limit.
|
||||||
@@ -325,12 +345,13 @@ func New() Model {
|
|||||||
MaxHeight: defaultMaxHeight,
|
MaxHeight: defaultMaxHeight,
|
||||||
MaxWidth: defaultMaxWidth,
|
MaxWidth: defaultMaxWidth,
|
||||||
Prompt: lipgloss.ThickBorder().Left + " ",
|
Prompt: lipgloss.ThickBorder().Left + " ",
|
||||||
Styles: styles,
|
styles: styles,
|
||||||
activeStyle: &styles.Blurred,
|
activeStyle: &styles.Blurred,
|
||||||
cache: memoization.NewMemoCache[line, [][]rune](maxLines),
|
cache: memoization.NewMemoCache[line, [][]rune](maxLines),
|
||||||
EndOfBufferCharacter: ' ',
|
EndOfBufferCharacter: ' ',
|
||||||
ShowLineNumbers: true,
|
ShowLineNumbers: true,
|
||||||
Cursor: cur,
|
VirtualCursor: true,
|
||||||
|
virtualCursor: cur,
|
||||||
KeyMap: DefaultKeyMap(),
|
KeyMap: DefaultKeyMap(),
|
||||||
|
|
||||||
value: make([][]rune, minHeight, maxLines),
|
value: make([][]rune, minHeight, maxLines),
|
||||||
@@ -362,6 +383,9 @@ func DefaultStyles(isDark bool) Styles {
|
|||||||
Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
|
Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
|
||||||
Prompt: lipgloss.NewStyle().Foreground(lipgloss.Color("7")),
|
Prompt: lipgloss.NewStyle().Foreground(lipgloss.Color("7")),
|
||||||
Text: lipgloss.NewStyle(),
|
Text: lipgloss.NewStyle(),
|
||||||
|
Cursor: CursorStyle{
|
||||||
|
Style: lipgloss.NewStyle().Foreground(lipgloss.Color("7")),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
s.Blurred = StyleState{
|
s.Blurred = StyleState{
|
||||||
Base: lipgloss.NewStyle(),
|
Base: lipgloss.NewStyle(),
|
||||||
@@ -386,6 +410,40 @@ func DefaultDarkStyles() Styles {
|
|||||||
return DefaultStyles(true)
|
return DefaultStyles(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetStyles sets the styles for the textarea.
|
||||||
|
func (m *Model) SetStyles(s Styles) {
|
||||||
|
m.styles = s
|
||||||
|
if m.focus {
|
||||||
|
m.activeStyle = &m.styles.Focused
|
||||||
|
} else {
|
||||||
|
m.activeStyle = &m.styles.Blurred
|
||||||
|
}
|
||||||
|
|
||||||
|
m.virtualCursor.Style = s.Focused.Cursor.Style
|
||||||
|
m.virtualCursor.BlinkedStyle = s.Focused.Cursor.BlinkedStyle
|
||||||
|
|
||||||
|
// By default, the blink speed of the cursor is set to a deafault
|
||||||
|
// internally.
|
||||||
|
if s.Focused.Cursor.BlinkSpeed > 0 {
|
||||||
|
m.virtualCursor.BlinkSpeed = m.activeStyle.Cursor.BlinkSpeed
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.VirtualCursor {
|
||||||
|
m.virtualCursor.SetMode(cursor.CursorHide)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.Focused.Cursor.Blink {
|
||||||
|
m.virtualCursor.SetMode(cursor.CursorBlink)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.virtualCursor.SetMode(cursor.CursorStatic)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles returns the styles for the textarea.
|
||||||
|
func (m Model) Styles() Styles {
|
||||||
|
return m.styles
|
||||||
|
}
|
||||||
|
|
||||||
// SetValue sets the value of the text input.
|
// SetValue sets the value of the text input.
|
||||||
func (m *Model) SetValue(s string) {
|
func (m *Model) SetValue(s string) {
|
||||||
m.Reset()
|
m.Reset()
|
||||||
@@ -624,16 +682,16 @@ func (m Model) Focused() bool {
|
|||||||
// receive keyboard input and the cursor will be hidden.
|
// receive keyboard input and the cursor will be hidden.
|
||||||
func (m *Model) Focus() tea.Cmd {
|
func (m *Model) Focus() tea.Cmd {
|
||||||
m.focus = true
|
m.focus = true
|
||||||
m.activeStyle = &m.Styles.Focused
|
m.activeStyle = &m.styles.Focused
|
||||||
return m.Cursor.Focus()
|
return m.virtualCursor.Focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blur removes the focus state on the model. When the model is blurred it can
|
// 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.
|
// 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.activeStyle = &m.Styles.Blurred
|
m.activeStyle = &m.styles.Blurred
|
||||||
m.Cursor.Blur()
|
m.virtualCursor.Blur()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset sets the input to its default state with no input.
|
// Reset sets the input to its default state with no input.
|
||||||
@@ -1001,7 +1059,7 @@ func (m *Model) SetHeight(h int) {
|
|||||||
// Update is the Bubble Tea update loop.
|
// Update is the Bubble Tea update loop.
|
||||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||||
if !m.focus {
|
if !m.focus {
|
||||||
m.Cursor.Blur()
|
m.virtualCursor.Blur()
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1123,10 +1181,10 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
newRow, newCol := m.cursorLineNumber(), m.col
|
newRow, newCol := m.cursorLineNumber(), m.col
|
||||||
m.Cursor, cmd = m.Cursor.Update(msg)
|
m.virtualCursor, cmd = m.virtualCursor.Update(msg)
|
||||||
if (newRow != oldRow || newCol != oldCol) && m.Cursor.Mode() == cursor.CursorBlink {
|
if (newRow != oldRow || newCol != oldCol) && m.virtualCursor.Mode() == cursor.CursorBlink {
|
||||||
m.Cursor.Blink = false
|
m.virtualCursor.Blink = false
|
||||||
cmd = m.Cursor.BlinkCmd()
|
cmd = m.virtualCursor.BlinkCmd()
|
||||||
}
|
}
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
@@ -1140,7 +1198,7 @@ func (m Model) View() string {
|
|||||||
if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" {
|
if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" {
|
||||||
return m.placeholderView()
|
return m.placeholderView()
|
||||||
}
|
}
|
||||||
m.Cursor.BlinkedStyle = m.activeStyle.computedCursorLine()
|
m.virtualCursor.BlinkedStyle = m.activeStyle.computedCursorLine()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
s strings.Builder
|
s strings.Builder
|
||||||
@@ -1209,11 +1267,11 @@ func (m Model) View() string {
|
|||||||
if m.row == l && lineInfo.RowOffset == wl {
|
if m.row == l && lineInfo.RowOffset == wl {
|
||||||
s.WriteString(style.Render(string(wrappedLine[:lineInfo.ColumnOffset])))
|
s.WriteString(style.Render(string(wrappedLine[:lineInfo.ColumnOffset])))
|
||||||
if m.col >= len(line) && lineInfo.CharOffset >= m.width {
|
if m.col >= len(line) && lineInfo.CharOffset >= m.width {
|
||||||
m.Cursor.SetChar(" ")
|
m.virtualCursor.SetChar(" ")
|
||||||
s.WriteString(m.Cursor.View())
|
s.WriteString(m.virtualCursor.View())
|
||||||
} else {
|
} else {
|
||||||
m.Cursor.SetChar(string(wrappedLine[lineInfo.ColumnOffset]))
|
m.virtualCursor.SetChar(string(wrappedLine[lineInfo.ColumnOffset]))
|
||||||
s.WriteString(style.Render(m.Cursor.View()))
|
s.WriteString(style.Render(m.virtualCursor.View()))
|
||||||
s.WriteString(style.Render(string(wrappedLine[lineInfo.ColumnOffset+1:])))
|
s.WriteString(style.Render(string(wrappedLine[lineInfo.ColumnOffset+1:])))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1316,9 +1374,9 @@ func (m Model) placeholderView() string {
|
|||||||
// first line
|
// first line
|
||||||
case i == 0:
|
case i == 0:
|
||||||
// first character of first line as cursor with character
|
// first character of first line as cursor with character
|
||||||
m.Cursor.BlinkedStyle = m.activeStyle.computedPlaceholder()
|
m.virtualCursor.BlinkedStyle = m.activeStyle.computedPlaceholder()
|
||||||
m.Cursor.SetChar(string(plines[0][0]))
|
m.virtualCursor.SetChar(string(plines[0][0]))
|
||||||
s.WriteString(lineStyle.Render(m.Cursor.View()))
|
s.WriteString(lineStyle.Render(m.virtualCursor.View()))
|
||||||
|
|
||||||
// the rest of the first line
|
// 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]))))))
|
s.WriteString(lineStyle.Render(style.Render(plines[0][1:] + strings.Repeat(" ", max(0, m.width-uniseg.StringWidth(plines[0]))))))
|
||||||
@@ -1347,10 +1405,24 @@ func Blink() tea.Msg {
|
|||||||
return cursor.Blink()
|
return cursor.Blink()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RealCursor returns a [tea.Cursor] for rendering a real cursor in a Bubble
|
// Cursor returns a [tea.Cursor] for rendering a real cursor in a Bubble Tea
|
||||||
// Tea program.
|
// program.
|
||||||
func (m Model) RealCursor() *tea.Cursor {
|
//
|
||||||
if m.Cursor.Mode() == cursor.CursorHide {
|
// 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
|
||||||
|
//
|
||||||
|
// Note that you will almost certainly also need to adjust the offset
|
||||||
|
// position of the textarea to properly set the cursor position.
|
||||||
|
//
|
||||||
|
// If you're using a real cursor, you should also set [Model.VirtualCursor] to
|
||||||
|
// false.
|
||||||
|
func (m Model) Cursor() *tea.Cursor {
|
||||||
|
if m.VirtualCursor || !m.Focused() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1358,8 +1430,8 @@ func (m Model) RealCursor() *tea.Cursor {
|
|||||||
x, y := lineInfo.CharOffset, m.cursorLineNumber()-m.viewport.YOffset
|
x, y := lineInfo.CharOffset, m.cursorLineNumber()-m.viewport.YOffset
|
||||||
|
|
||||||
c := tea.NewCursor(x, y)
|
c := tea.NewCursor(x, y)
|
||||||
c.Blink = m.Cursor.Mode() == cursor.CursorBlink
|
c.Blink = m.styles.Focused.Cursor.Blink
|
||||||
c.Color = m.Cursor.Style.GetForeground()
|
c.Color = m.styles.Focused.Cursor.Style.GetForeground()
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user