mirror of
https://github.com/zoriya/bubbles.git
synced 2026-08-05 04:36:07 +00:00
Merge branch 'v2-exp' into v2-table
This commit is contained in:
+25
-16
@@ -4,6 +4,7 @@ package cursor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
@@ -12,6 +13,14 @@ import (
|
||||
|
||||
const defaultBlinkSpeed = time.Millisecond * 530
|
||||
|
||||
// Internal ID management. Used during animating to ensure that frame messages
|
||||
// are received only by spinner components that sent them.
|
||||
var lastID int64
|
||||
|
||||
func nextID() int {
|
||||
return int(atomic.AddInt64(&lastID, 1))
|
||||
}
|
||||
|
||||
// initialBlinkMsg initializes cursor blinking.
|
||||
type initialBlinkMsg struct{}
|
||||
|
||||
@@ -64,8 +73,9 @@ type Model struct {
|
||||
// unless [CursorMode] is not set to [CursorBlink].
|
||||
BlinkSpeed time.Duration
|
||||
|
||||
// Blink is the state of the cursor blink. When true, the cursor is hidden.
|
||||
Blink bool
|
||||
// IsBlinked is the state of the cursor blink. When true, the cursor is
|
||||
// hidden.
|
||||
IsBlinked bool
|
||||
|
||||
// char is the character under the cursor
|
||||
char string
|
||||
@@ -89,10 +99,10 @@ type Model struct {
|
||||
// New creates a new model with default settings.
|
||||
func New() Model {
|
||||
return Model{
|
||||
id: nextID(),
|
||||
BlinkSpeed: defaultBlinkSpeed,
|
||||
|
||||
Blink: true,
|
||||
mode: CursorBlink,
|
||||
IsBlinked: true,
|
||||
mode: CursorBlink,
|
||||
|
||||
blinkCtx: &blinkCtx{
|
||||
ctx: context.Background(),
|
||||
@@ -110,7 +120,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
cmd := m.BlinkCmd()
|
||||
cmd := m.Blink()
|
||||
return m, cmd
|
||||
|
||||
case tea.FocusMsg:
|
||||
@@ -136,8 +146,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
|
||||
var cmd tea.Cmd
|
||||
if m.mode == CursorBlink {
|
||||
m.Blink = !m.Blink
|
||||
cmd = m.BlinkCmd()
|
||||
m.IsBlinked = !m.IsBlinked
|
||||
cmd = m.Blink()
|
||||
}
|
||||
return m, cmd
|
||||
|
||||
@@ -162,15 +172,15 @@ func (m *Model) SetMode(mode Mode) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
m.mode = mode
|
||||
m.Blink = m.mode == CursorHide || !m.focus
|
||||
m.IsBlinked = m.mode == CursorHide || !m.focus
|
||||
if mode == CursorBlink {
|
||||
return Blink
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BlinkCmd is a command used to manage cursor blinking.
|
||||
func (m *Model) BlinkCmd() tea.Cmd {
|
||||
// Blink is a command used to manage cursor blinking.
|
||||
func (m *Model) Blink() tea.Cmd {
|
||||
if m.mode != CursorBlink {
|
||||
return nil
|
||||
}
|
||||
@@ -183,7 +193,6 @@ func (m *Model) BlinkCmd() tea.Cmd {
|
||||
m.blinkCtx.cancel = cancel
|
||||
|
||||
m.blinkTag++
|
||||
|
||||
blinkMsg := BlinkMsg{id: m.id, tag: m.blinkTag}
|
||||
|
||||
return func() tea.Msg {
|
||||
@@ -204,10 +213,10 @@ func Blink() tea.Msg {
|
||||
// Focus focuses the cursor to allow it to blink if desired.
|
||||
func (m *Model) Focus() tea.Cmd {
|
||||
m.focus = true
|
||||
m.Blink = m.mode == CursorHide // show the cursor unless we've explicitly hidden it
|
||||
m.IsBlinked = m.mode == CursorHide // show the cursor unless we've explicitly hidden it
|
||||
|
||||
if m.mode == CursorBlink && m.focus {
|
||||
return m.BlinkCmd()
|
||||
return m.Blink()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -215,7 +224,7 @@ func (m *Model) Focus() tea.Cmd {
|
||||
// Blur blurs the cursor.
|
||||
func (m *Model) Blur() {
|
||||
m.focus = false
|
||||
m.Blink = true
|
||||
m.IsBlinked = true
|
||||
}
|
||||
|
||||
// SetChar sets the character under the cursor.
|
||||
@@ -225,7 +234,7 @@ func (m *Model) SetChar(char string) {
|
||||
|
||||
// View displays the cursor.
|
||||
func (m Model) View() string {
|
||||
if m.Blink {
|
||||
if m.IsBlinked {
|
||||
return m.TextStyle.Inline(true).Render(m.char)
|
||||
}
|
||||
return m.Style.Inline(true).Reverse(true).Render(m.char)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
// TestBlinkCmdDataRace tests for a race on [Cursor.blinkTag].
|
||||
//
|
||||
// The original [Model.BlinkCmd] implementation returned a closure over the pointer receiver:
|
||||
// The original [Model.Blink] implementation returned a closure over the pointer receiver:
|
||||
//
|
||||
// return func() tea.Msg {
|
||||
// defer cancel()
|
||||
@@ -20,12 +20,12 @@ import (
|
||||
// }
|
||||
//
|
||||
// A race on “m.blinkTag” will occur if:
|
||||
// 1. [Model.BlinkCmd] is called e.g. by calling [Model.Focus] from
|
||||
// 1. [Model.Blink] is called e.g. by calling [Model.Focus] from
|
||||
// ["github.com/charmbracelet/bubbletea".Model.Update];
|
||||
// 2. ["github.com/charmbracelet/bubbletea".handleCommands] is kept sufficiently busy that it does not recieve and
|
||||
// execute the [Model.BlinkCmd] e.g. by other long running command or commands;
|
||||
// execute the [Model.Blink] e.g. by other long running command or commands;
|
||||
// 3. at least [Mode.BlinkSpeed] time elapses;
|
||||
// 4. [Model.BlinkCmd] is called again;
|
||||
// 4. [Model.Blink] is called again;
|
||||
// 5. ["github.com/charmbracelet/bubbletea".handleCommands] gets around to receiving and executing the original
|
||||
// closure.
|
||||
//
|
||||
@@ -33,7 +33,7 @@ import (
|
||||
// current value rather than the value at the time the closure was created).
|
||||
func TestBlinkCmdDataRace(t *testing.T) {
|
||||
m := New()
|
||||
cmd := m.BlinkCmd()
|
||||
cmd := m.Blink()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
@@ -44,7 +44,7 @@ func TestBlinkCmdDataRace(t *testing.T) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
time.Sleep(m.BlinkSpeed * 2)
|
||||
m.BlinkCmd()
|
||||
m.Blink()
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
@@ -121,5 +121,5 @@ type HInt int
|
||||
|
||||
// Hash is a method that returns the hash of the integer.
|
||||
func (h HInt) Hash() string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%d", h))))
|
||||
return fmt.Sprintf("%x", sha256.Sum256(fmt.Appendf(nil, "%d", h)))
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -17,8 +18,8 @@ const (
|
||||
type cacheAction struct {
|
||||
actionType actionType
|
||||
key HString
|
||||
value interface{}
|
||||
expectedValue interface{}
|
||||
value any
|
||||
expectedValue any
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
@@ -121,7 +122,7 @@ func TestCache(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cache := NewMemoCache[HString, interface{}](tt.capacity)
|
||||
cache := NewMemoCache[HString, any](tt.capacity)
|
||||
for _, action := range tt.actions {
|
||||
switch action.actionType {
|
||||
case set:
|
||||
@@ -174,7 +175,7 @@ func FuzzCache(f *testing.F) {
|
||||
// If the key is already in accessOrder, we remove it and append it again later
|
||||
for index, accessedKey := range accessOrder {
|
||||
if accessedKey == key {
|
||||
accessOrder = append(accessOrder[:index], accessOrder[index+1:]...)
|
||||
accessOrder = slices.Delete(accessOrder, index, index+1)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -206,7 +207,7 @@ func FuzzCache(f *testing.F) {
|
||||
// If the key was accessed, move it to the end of the accessOrder to represent recent use
|
||||
for index, accessedKey := range accessOrder {
|
||||
if accessedKey == key {
|
||||
accessOrder = append(accessOrder[:index], accessOrder[index+1:]...)
|
||||
accessOrder = slices.Delete(accessOrder, index, index+1)
|
||||
accessOrder = append(accessOrder, key)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func (s *sanitizer) Sanitize(runes []rune) []rune {
|
||||
// is smaller or equal to the input.
|
||||
copied := false
|
||||
|
||||
for src := 0; src < len(runes); src++ {
|
||||
for src := range runes {
|
||||
r := runes[src]
|
||||
switch {
|
||||
case r == utf8.RuneError:
|
||||
|
||||
@@ -185,7 +185,7 @@ func (m Model) View() string {
|
||||
|
||||
func (m Model) dotsView() string {
|
||||
var s string
|
||||
for i := 0; i < m.TotalPages; i++ {
|
||||
for i := range m.TotalPages {
|
||||
if i == m.Page {
|
||||
s += m.ActiveDot
|
||||
continue
|
||||
|
||||
@@ -295,7 +295,7 @@ func (m Model) barView(b *strings.Builder, percent float64, textWidth int) {
|
||||
|
||||
if m.useRamp {
|
||||
// Gradient fill
|
||||
for i := 0; i < fw; i++ {
|
||||
for i := range fw {
|
||||
if fw == 1 {
|
||||
// this is up for debate: in a gradient of width=1, should the
|
||||
// single character rendered be the first color, the last color
|
||||
|
||||
+64
-31
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -260,17 +261,9 @@ type Model struct {
|
||||
// KeyMap encodes the keybindings recognized by the widget.
|
||||
KeyMap KeyMap
|
||||
|
||||
// Styling. FocusedStyle and BlurredStyle are used to style the textarea in
|
||||
// focused and blurred states.
|
||||
Styles Styles
|
||||
|
||||
// virtualCursor manages the virtual cursor.
|
||||
virtualCursor cursor.Model
|
||||
|
||||
// VirtualCursor determines whether or not to use the virtual cursor. If
|
||||
// set to false, use [Model.Cursor] to return a real cursor for rendering.
|
||||
VirtualCursor bool
|
||||
|
||||
// CharLimit is the maximum number of characters this input element will
|
||||
// accept. If 0 or less, there's no limit.
|
||||
CharLimit int
|
||||
@@ -283,6 +276,15 @@ type Model struct {
|
||||
// there's no limit.
|
||||
MaxWidth int
|
||||
|
||||
// Styling. Styles are defined in [Styles]. Use [SetStyles] and [GetStyles]
|
||||
// to work with this value publicly.
|
||||
styles Styles
|
||||
|
||||
// useVirtualCursor determines whether or not to use the virtual cursor.
|
||||
// Use [SetVirtualCursor] and [VirtualCursor] to work with this this
|
||||
// value publicly.
|
||||
useVirtualCursor bool
|
||||
|
||||
// If promptFunc is set, it replaces Prompt as a generator for
|
||||
// prompt strings at the beginning of each line.
|
||||
promptFunc func(line int) string
|
||||
@@ -337,11 +339,11 @@ func New() Model {
|
||||
MaxHeight: defaultMaxHeight,
|
||||
MaxWidth: defaultMaxWidth,
|
||||
Prompt: lipgloss.ThickBorder().Left + " ",
|
||||
Styles: styles,
|
||||
styles: styles,
|
||||
cache: memoization.NewMemoCache[line, [][]rune](maxLines),
|
||||
EndOfBufferCharacter: ' ',
|
||||
ShowLineNumbers: true,
|
||||
VirtualCursor: true,
|
||||
useVirtualCursor: true,
|
||||
virtualCursor: cur,
|
||||
KeyMap: DefaultKeyMap(),
|
||||
|
||||
@@ -403,21 +405,43 @@ func DefaultDarkStyles() Styles {
|
||||
return DefaultStyles(true)
|
||||
}
|
||||
|
||||
// Styles returns the current styles for the textarea.
|
||||
func (m Model) Styles() Styles {
|
||||
return m.styles
|
||||
}
|
||||
|
||||
// SetStyles updates styling for the textarea.
|
||||
func (m *Model) SetStyles(s Styles) {
|
||||
m.styles = s
|
||||
m.updateVirtualCursorStyle()
|
||||
}
|
||||
|
||||
// VirtualCursor returns whether or not the virtual cursor is enabled.
|
||||
func (m Model) VirtualCursor() bool {
|
||||
return m.useVirtualCursor
|
||||
}
|
||||
|
||||
// SetVirtualCursor sets whether or not to use the virtual cursor.
|
||||
func (m *Model) SetVirtualCursor(v bool) {
|
||||
m.useVirtualCursor = v
|
||||
m.updateVirtualCursorStyle()
|
||||
}
|
||||
|
||||
// updateVirtualCursorStyle sets styling on the virtual cursor based on the
|
||||
// textarea's style settings.
|
||||
func (m *Model) updateVirtualCursorStyle() {
|
||||
if !m.VirtualCursor {
|
||||
if !m.useVirtualCursor {
|
||||
m.virtualCursor.SetMode(cursor.CursorHide)
|
||||
return
|
||||
}
|
||||
|
||||
m.virtualCursor.Style = lipgloss.NewStyle().Foreground(m.Styles.Cursor.Color)
|
||||
m.virtualCursor.Style = lipgloss.NewStyle().Foreground(m.styles.Cursor.Color)
|
||||
|
||||
// By default, the blink speed of the cursor is set to a default
|
||||
// internally.
|
||||
if m.Styles.Cursor.Blink {
|
||||
if m.Styles.Cursor.BlinkSpeed > 0 {
|
||||
m.virtualCursor.BlinkSpeed = m.Styles.Cursor.BlinkSpeed
|
||||
if m.styles.Cursor.Blink {
|
||||
if m.styles.Cursor.BlinkSpeed > 0 {
|
||||
m.virtualCursor.BlinkSpeed = m.styles.Cursor.BlinkSpeed
|
||||
}
|
||||
m.virtualCursor.SetMode(cursor.CursorBlink)
|
||||
return
|
||||
@@ -464,7 +488,7 @@ func (m *Model) insertRunesFromUserInput(runes []rune) {
|
||||
// Split the input into lines.
|
||||
var lines [][]rune
|
||||
lstart := 0
|
||||
for i := 0; i < len(runes); i++ {
|
||||
for i := range runes {
|
||||
if runes[i] == '\n' {
|
||||
// Queue a line to become a new row in the text area below.
|
||||
// Beware to clamp the max capacity of the slice, to ensure no
|
||||
@@ -663,9 +687,9 @@ func (m Model) Focused() bool {
|
||||
// whether the textarea is focused or blurred.
|
||||
func (m Model) activeStyle() *StyleState {
|
||||
if m.focus {
|
||||
return &m.Styles.Focused
|
||||
return &m.styles.Focused
|
||||
}
|
||||
return &m.Styles.Blurred
|
||||
return &m.styles.Blurred
|
||||
}
|
||||
|
||||
// Focus sets the focus state on the model. When the model is in focus it can
|
||||
@@ -1102,7 +1126,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
}
|
||||
case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
|
||||
if len(m.value[m.row]) > 0 && m.col < len(m.value[m.row]) {
|
||||
m.value[m.row] = append(m.value[m.row][:m.col], m.value[m.row][m.col+1:]...)
|
||||
m.value[m.row] = slices.Delete(m.value[m.row], m.col, m.col+1)
|
||||
}
|
||||
if m.col >= len(m.value[m.row]) {
|
||||
m.mergeLineBelow(m.row)
|
||||
@@ -1173,13 +1197,18 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m.viewport = &vp
|
||||
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()
|
||||
if m.useVirtualCursor {
|
||||
m.virtualCursor, cmd = m.virtualCursor.Update(msg)
|
||||
|
||||
// If the cursor has moved, reset the blink state. This is a small UX
|
||||
// nuance that makes cursor movement obvious and feel snappy.
|
||||
newRow, newCol := m.cursorLineNumber(), m.col
|
||||
if (newRow != oldRow || newCol != oldCol) && m.virtualCursor.Mode() == cursor.CursorBlink {
|
||||
m.virtualCursor.IsBlinked = false
|
||||
cmd = m.virtualCursor.Blink()
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
m.repositionView()
|
||||
|
||||
@@ -1188,7 +1217,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
|
||||
// View renders the text area in its current state.
|
||||
func (m Model) View() string {
|
||||
m.updateVirtualCursorStyle()
|
||||
if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" {
|
||||
return m.placeholderView()
|
||||
}
|
||||
@@ -1348,7 +1376,7 @@ func (m Model) placeholderView() string {
|
||||
// split string by new lines
|
||||
plines := strings.Split(strings.TrimSpace(pwrap), "\n")
|
||||
|
||||
for i := 0; i < m.height; i++ {
|
||||
for i := range m.height {
|
||||
isLineNumber := len(plines) > i
|
||||
|
||||
lineStyle := styles.computedPlaceholder()
|
||||
@@ -1390,6 +1418,11 @@ func (m Model) placeholderView() string {
|
||||
|
||||
// the rest of the first line
|
||||
s.WriteString(lineStyle.Render(styles.computedPlaceholder().Render(rest)))
|
||||
|
||||
// extend the first line with spaces to fill the width, so that
|
||||
// the entire line is filled when cursorline is enabled.
|
||||
gap := strings.Repeat(" ", max(0, m.width-lipgloss.Width(plines[0])))
|
||||
s.WriteString(lineStyle.Render(gap))
|
||||
// remaining lines
|
||||
case len(plines) > i:
|
||||
// current line placeholder text
|
||||
@@ -1431,7 +1464,7 @@ func Blink() tea.Msg {
|
||||
// f.Cursor.Position.X += offsetX
|
||||
// f.Cursor.Position.Y += offsetY
|
||||
func (m Model) Cursor() *tea.Cursor {
|
||||
if m.VirtualCursor {
|
||||
if m.useVirtualCursor {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1453,9 +1486,9 @@ func (m Model) Cursor() *tea.Cursor {
|
||||
baseStyle.GetBorderTopSize()
|
||||
|
||||
c := tea.NewCursor(xOffset, yOffset)
|
||||
c.Blink = m.Styles.Cursor.Blink
|
||||
c.Color = m.Styles.Cursor.Color
|
||||
c.Shape = m.Styles.Cursor.Shape
|
||||
c.Blink = m.styles.Cursor.Blink
|
||||
c.Color = m.styles.Cursor.Color
|
||||
c.Shape = m.styles.Cursor.Shape
|
||||
return c
|
||||
}
|
||||
|
||||
|
||||
@@ -1032,7 +1032,9 @@ func TestView(t *testing.T) {
|
||||
{
|
||||
name: "set width with style",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
s := m.Styles()
|
||||
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
m.SetStyles(s)
|
||||
m.Focus()
|
||||
|
||||
m.SetWidth(12)
|
||||
@@ -1060,7 +1062,9 @@ func TestView(t *testing.T) {
|
||||
{
|
||||
name: "set width with style max width minus one",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
s := m.Styles()
|
||||
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
m.SetStyles(s)
|
||||
m.Focus()
|
||||
|
||||
m.SetWidth(12)
|
||||
@@ -1088,7 +1092,9 @@ func TestView(t *testing.T) {
|
||||
{
|
||||
name: "set width with style max width",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
s := m.Styles()
|
||||
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
m.SetStyles(s)
|
||||
m.Focus()
|
||||
|
||||
m.SetWidth(12)
|
||||
@@ -1116,7 +1122,9 @@ func TestView(t *testing.T) {
|
||||
{
|
||||
name: "set width with style max width plus one",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
s := m.Styles()
|
||||
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
m.SetStyles(s)
|
||||
m.Focus()
|
||||
|
||||
m.SetWidth(12)
|
||||
@@ -1144,7 +1152,9 @@ func TestView(t *testing.T) {
|
||||
{
|
||||
name: "set width without line numbers with style",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
s := m.Styles()
|
||||
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
m.SetStyles(s)
|
||||
m.Focus()
|
||||
|
||||
m.ShowLineNumbers = false
|
||||
@@ -1173,7 +1183,9 @@ func TestView(t *testing.T) {
|
||||
{
|
||||
name: "set width without line numbers with style max width minus one",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
s := m.Styles()
|
||||
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
m.SetStyles(s)
|
||||
m.Focus()
|
||||
|
||||
m.ShowLineNumbers = false
|
||||
@@ -1202,7 +1214,9 @@ func TestView(t *testing.T) {
|
||||
{
|
||||
name: "set width without line numbers with style max width",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
s := m.Styles()
|
||||
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
m.SetStyles(s)
|
||||
m.Focus()
|
||||
|
||||
m.ShowLineNumbers = false
|
||||
@@ -1231,7 +1245,9 @@ func TestView(t *testing.T) {
|
||||
{
|
||||
name: "set width without line numbers with style max width plus one",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
s := m.Styles()
|
||||
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||
m.SetStyles(s)
|
||||
m.Focus()
|
||||
|
||||
m.ShowLineNumbers = false
|
||||
|
||||
+67
-34
@@ -4,6 +4,7 @@ package textinput
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
@@ -95,19 +96,21 @@ type Model struct {
|
||||
EchoMode EchoMode
|
||||
EchoCharacter rune
|
||||
|
||||
// VirtualCursor determines whether or not to use the virtual cursor. If
|
||||
// useVirtualCursor determines whether or not to use the virtual cursor. If
|
||||
// set to false, use [Model.Cursor] to return a real cursor for rendering.
|
||||
VirtualCursor bool
|
||||
virtualCursor cursor.Model
|
||||
useVirtualCursor bool
|
||||
|
||||
// Styling. FocusedStyle and BlurredStyle are used to style the textarea in
|
||||
// focused and blurred states.
|
||||
Styles Styles
|
||||
// Virtual cursor manager.
|
||||
virtualCursor cursor.Model
|
||||
|
||||
// CharLimit is the maximum amount of characters this input element will
|
||||
// accept. If 0 or less, there's no limit.
|
||||
CharLimit int
|
||||
|
||||
// Styling. FocusedStyle and BlurredStyle are used to style the textarea in
|
||||
// focused and blurred states.
|
||||
styles Styles
|
||||
|
||||
// Width is the maximum number of characters that can be displayed at once.
|
||||
// It essentially treats the text field like a horizontally scrolling
|
||||
// viewport. If 0 or less this setting is ignored.
|
||||
@@ -152,19 +155,45 @@ type Model struct {
|
||||
|
||||
// New creates a new model with default settings.
|
||||
func New() Model {
|
||||
return Model{
|
||||
Prompt: "> ",
|
||||
EchoCharacter: '*',
|
||||
CharLimit: 0,
|
||||
Styles: DefaultDarkStyles(),
|
||||
ShowSuggestions: false,
|
||||
virtualCursor: cursor.New(),
|
||||
KeyMap: DefaultKeyMap(),
|
||||
suggestions: [][]rune{},
|
||||
value: nil,
|
||||
focus: false,
|
||||
pos: 0,
|
||||
m := Model{
|
||||
Prompt: "> ",
|
||||
EchoCharacter: '*',
|
||||
CharLimit: 0,
|
||||
styles: DefaultDarkStyles(),
|
||||
ShowSuggestions: false,
|
||||
useVirtualCursor: true,
|
||||
virtualCursor: cursor.New(),
|
||||
KeyMap: DefaultKeyMap(),
|
||||
suggestions: [][]rune{},
|
||||
value: nil,
|
||||
focus: false,
|
||||
pos: 0,
|
||||
}
|
||||
m.updateVirtualCursorStyle()
|
||||
return m
|
||||
}
|
||||
|
||||
// VirtualCursor returns whether the model is using a virtual cursor.
|
||||
func (m Model) VirtualCursor() bool {
|
||||
return m.useVirtualCursor
|
||||
}
|
||||
|
||||
// SetVirtualCursor sets whether the model should use a virtual cursor. If
|
||||
// disabled, use [Model.Cursor] to return a real cursor for rendering.
|
||||
func (m *Model) SetVirtualCursor(v bool) {
|
||||
m.useVirtualCursor = v
|
||||
m.updateVirtualCursorStyle()
|
||||
}
|
||||
|
||||
// Styles returns the current set of styles.
|
||||
func (m Model) Styles() Styles {
|
||||
return m.styles
|
||||
}
|
||||
|
||||
// SetStyles sets the styles for the text input.
|
||||
func (m *Model) SetStyles(s Styles) {
|
||||
m.styles = s
|
||||
m.updateVirtualCursorStyle()
|
||||
}
|
||||
|
||||
// Width returns the width of the text input.
|
||||
@@ -549,7 +578,6 @@ func (m Model) echoTransform(v string) string {
|
||||
|
||||
// Update is the Bubble Tea update loop.
|
||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m.updateVirtualCursorStyle()
|
||||
if !m.focus {
|
||||
return m, nil
|
||||
}
|
||||
@@ -597,7 +625,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m.CursorStart()
|
||||
case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
|
||||
if len(m.value) > 0 && m.pos < len(m.value) {
|
||||
m.value = append(m.value[:m.pos], m.value[m.pos+1:]...)
|
||||
m.value = slices.Delete(m.value, m.pos, m.pos+1)
|
||||
m.Err = m.validate(m.value)
|
||||
}
|
||||
case key.Matches(msg, m.KeyMap.LineEnd):
|
||||
@@ -636,12 +664,16 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
var cmd tea.Cmd
|
||||
|
||||
m.virtualCursor, cmd = m.virtualCursor.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
if m.useVirtualCursor {
|
||||
m.virtualCursor, cmd = m.virtualCursor.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
if oldPos != m.pos && m.virtualCursor.Mode() == cursor.CursorBlink {
|
||||
m.virtualCursor.Blink = false
|
||||
cmds = append(cmds, m.virtualCursor.BlinkCmd())
|
||||
// If the cursor position changed, reset the blink state. This is a
|
||||
// small UX nuance that makes cursor movement obvious and feel snappy.
|
||||
if oldPos != m.pos && m.virtualCursor.Mode() == cursor.CursorBlink {
|
||||
m.virtualCursor.IsBlinked = false
|
||||
cmds = append(cmds, m.virtualCursor.Blink())
|
||||
}
|
||||
}
|
||||
|
||||
m.handleOverflow()
|
||||
@@ -882,7 +914,7 @@ func (m Model) validate(v []rune) error {
|
||||
// f.Cursor.Position.X += offsetX
|
||||
// f.Cursor.Position.Y += offsetY
|
||||
func (m Model) Cursor() *tea.Cursor {
|
||||
if m.VirtualCursor {
|
||||
if m.useVirtualCursor {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -895,7 +927,7 @@ func (m Model) Cursor() *tea.Cursor {
|
||||
xOffset = min(xOffset, m.width+promptWidth)
|
||||
}
|
||||
|
||||
style := m.Styles.Cursor
|
||||
style := m.styles.Cursor
|
||||
c := tea.NewCursor(xOffset, 0)
|
||||
c.Blink = style.Blink
|
||||
c.Color = style.Color
|
||||
@@ -906,18 +938,19 @@ func (m Model) Cursor() *tea.Cursor {
|
||||
// updateVirtualCursorStyle sets styling on the virtual cursor based on the
|
||||
// textarea's style settings.
|
||||
func (m *Model) updateVirtualCursorStyle() {
|
||||
if !m.VirtualCursor {
|
||||
if !m.useVirtualCursor {
|
||||
// Hide the virtual cursor if we're using a real cursor.
|
||||
m.virtualCursor.SetMode(cursor.CursorHide)
|
||||
return
|
||||
}
|
||||
|
||||
m.virtualCursor.Style = lipgloss.NewStyle().Foreground(m.Styles.Cursor.Color)
|
||||
m.virtualCursor.Style = lipgloss.NewStyle().Foreground(m.styles.Cursor.Color)
|
||||
|
||||
// By default, the blink speed of the cursor is set to a default
|
||||
// internally.
|
||||
if m.Styles.Cursor.Blink {
|
||||
if m.Styles.Cursor.BlinkSpeed > 0 {
|
||||
m.virtualCursor.BlinkSpeed = m.Styles.Cursor.BlinkSpeed
|
||||
if m.styles.Cursor.Blink {
|
||||
if m.styles.Cursor.BlinkSpeed > 0 {
|
||||
m.virtualCursor.BlinkSpeed = m.styles.Cursor.BlinkSpeed
|
||||
}
|
||||
m.virtualCursor.SetMode(cursor.CursorBlink)
|
||||
return
|
||||
@@ -929,7 +962,7 @@ func (m *Model) updateVirtualCursorStyle() {
|
||||
// whether the textarea is focused or blurred.
|
||||
func (m Model) activeStyle() *StyleState {
|
||||
if m.focus {
|
||||
return &m.Styles.Focused
|
||||
return &m.styles.Focused
|
||||
}
|
||||
return &m.Styles.Blurred
|
||||
return &m.styles.Blurred
|
||||
}
|
||||
|
||||
@@ -374,7 +374,7 @@ func TestRightOverscroll(t *testing.T) {
|
||||
m := New(WithHeight(5), WithWidth(len(content)+1))
|
||||
m.SetContent(content)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
for range 10 {
|
||||
m.ScrollRight(m.horizontalStep)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user