259 lines
6.8 KiB
Go
259 lines
6.8 KiB
Go
package form
|
||
|
||
import (
|
||
"fmt"
|
||
|
||
"github.com/gdamore/tcell/v2"
|
||
"github.com/rivo/tview"
|
||
"github.com/sjzar/chatlog/internal/ui/style"
|
||
)
|
||
|
||
const (
|
||
// DialogPadding dialog inner padding.
|
||
DialogPadding = 3
|
||
|
||
// DialogHelpHeight dialog help text height.
|
||
DialogHelpHeight = 1
|
||
|
||
// DialogMinWidth dialog min width.
|
||
DialogMinWidth = 40
|
||
|
||
// FormHeightOffset form height offset for border.
|
||
FormHeightOffset = 3
|
||
|
||
// 额外的宽度补偿,类似于 submenu 的 cmdWidthOffset
|
||
formWidthOffset = 10
|
||
)
|
||
|
||
// Form is a modal form component with a title, form fields, and help text.
|
||
type Form struct {
|
||
*tview.Box
|
||
title string
|
||
layout *tview.Flex
|
||
form *tview.Form
|
||
helpText *tview.TextView
|
||
width int
|
||
height int
|
||
cancelHandler func()
|
||
fields []formField // 存储字段信息以便重新计算宽度
|
||
}
|
||
|
||
// formField 存储表单字段的信息
|
||
type formField struct {
|
||
label string
|
||
value string
|
||
fieldWidth int
|
||
}
|
||
|
||
// NewForm creates a new form with the given title.
|
||
func NewForm(title string) *Form {
|
||
f := &Form{
|
||
Box: tview.NewBox(),
|
||
title: title,
|
||
layout: tview.NewFlex().SetDirection(tview.FlexRow),
|
||
form: tview.NewForm(),
|
||
fields: make([]formField, 0),
|
||
}
|
||
|
||
// 设置表单样式
|
||
f.form.SetBorderPadding(1, 1, 1, 1)
|
||
f.form.SetBackgroundColor(style.DialogBgColor)
|
||
f.form.SetFieldBackgroundColor(style.BgColor)
|
||
f.form.SetFieldTextColor(style.FgColor)
|
||
f.form.SetButtonBackgroundColor(style.ButtonBgColor)
|
||
f.form.SetButtonTextColor(style.FgColor)
|
||
f.form.SetLabelColor(style.DialogFgColor)
|
||
f.form.SetButtonsAlign(tview.AlignCenter)
|
||
|
||
// 创建帮助文本
|
||
f.helpText = tview.NewTextView()
|
||
f.helpText.SetDynamicColors(true)
|
||
f.helpText.SetTextAlign(tview.AlignCenter)
|
||
f.helpText.SetTextColor(style.DialogFgColor)
|
||
f.helpText.SetBackgroundColor(style.DialogBgColor)
|
||
fmt.Fprintf(f.helpText,
|
||
"[%s::b]Tab[%s::b]: 导航 [%s::b]Enter[%s::b]: 选择 [%s::b]ESC[%s::b]: 返回",
|
||
style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
|
||
style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
|
||
style.GetColorHex(style.MenuBgColor), style.GetColorHex(style.PageHeaderFgColor),
|
||
)
|
||
|
||
// 创建布局
|
||
formLayout := tview.NewFlex().SetDirection(tview.FlexColumn)
|
||
formLayout.AddItem(EmptyBoxSpace(style.DialogBgColor), 1, 0, false)
|
||
formLayout.AddItem(f.form, 0, 1, true)
|
||
formLayout.AddItem(EmptyBoxSpace(style.DialogBgColor), 1, 0, false)
|
||
|
||
// 设置主布局
|
||
f.layout.SetTitle(fmt.Sprintf("[::b]%s", f.title))
|
||
f.layout.SetTitleColor(style.DialogFgColor)
|
||
f.layout.SetTitleAlign(tview.AlignCenter)
|
||
f.layout.SetBorder(true)
|
||
f.layout.SetBorderColor(style.DialogBorderColor)
|
||
f.layout.SetBackgroundColor(style.DialogBgColor)
|
||
|
||
// 添加表单区域
|
||
f.layout.AddItem(formLayout, 0, 1, true)
|
||
|
||
// 添加帮助文本区域
|
||
f.layout.AddItem(f.helpText, DialogHelpHeight, 0, false)
|
||
|
||
return f
|
||
}
|
||
|
||
// AddInputField adds an input field to the form.
|
||
func (f *Form) AddInputField(label, value string, fieldWidth int, accept func(textToCheck string, lastChar rune) bool, changed func(text string)) *Form {
|
||
// 存储字段信息
|
||
f.fields = append(f.fields, formField{
|
||
label: label,
|
||
value: value,
|
||
fieldWidth: fieldWidth,
|
||
})
|
||
|
||
// 添加输入字段到表单
|
||
f.form.AddInputField(label, value, fieldWidth, accept, changed)
|
||
|
||
// 更新表单尺寸
|
||
f.recalculateSize()
|
||
|
||
return f
|
||
}
|
||
|
||
// AddButton adds a button to the form.
|
||
func (f *Form) AddButton(label string, selected func()) *Form {
|
||
f.form.AddButton(label, selected)
|
||
// 更新表单尺寸
|
||
f.recalculateSize()
|
||
return f
|
||
}
|
||
|
||
// AddCheckbox adds a checkbox to the form.
|
||
func (f *Form) AddCheckbox(label string, checked bool, changed func(checked bool)) *Form {
|
||
f.form.AddCheckbox(label, checked, changed)
|
||
// 更新表单尺寸
|
||
f.recalculateSize()
|
||
return f
|
||
}
|
||
|
||
// SetCancelFunc sets the function to be called when the form is cancelled.
|
||
func (f *Form) SetCancelFunc(handler func()) *Form {
|
||
f.cancelHandler = handler
|
||
return f
|
||
}
|
||
|
||
// recalculateSize 重新计算表单尺寸
|
||
func (f *Form) recalculateSize() {
|
||
// 计算表单项数量
|
||
itemCount := f.form.GetFormItemCount()
|
||
|
||
// 计算高度 - 每个表单项占2行,按钮区域至少占2行,再加上边框和帮助文本
|
||
f.height = (itemCount * 2) + 2 + FormHeightOffset + DialogHelpHeight
|
||
|
||
// 计算宽度 - 类似于 submenu 的实现
|
||
maxLabelWidth := 0
|
||
maxValueWidth := 0
|
||
|
||
// 遍历所有字段,找出最长的标签和值
|
||
for _, field := range f.fields {
|
||
if len(field.label) > maxLabelWidth {
|
||
maxLabelWidth = len(field.label)
|
||
}
|
||
|
||
// 对于值,使用字段宽度和实际值长度中的较大者
|
||
valueWidth := field.fieldWidth
|
||
if len(field.value) > valueWidth {
|
||
valueWidth = len(field.value)
|
||
}
|
||
|
||
if valueWidth > maxValueWidth {
|
||
maxValueWidth = valueWidth
|
||
}
|
||
}
|
||
|
||
// 计算总宽度,类似于 submenu 的计算方式
|
||
f.width = maxLabelWidth + maxValueWidth + formWidthOffset
|
||
|
||
// 确保宽度不小于最小值
|
||
if f.width < DialogMinWidth {
|
||
f.width = DialogMinWidth
|
||
}
|
||
}
|
||
|
||
// Draw draws the form on the screen.
|
||
func (f *Form) Draw(screen tcell.Screen) {
|
||
// 在绘制前重新计算尺寸,确保尺寸是最新的
|
||
f.recalculateSize()
|
||
|
||
// 绘制
|
||
f.Box.DrawForSubclass(screen, f)
|
||
f.layout.Draw(screen)
|
||
}
|
||
|
||
// SetRect sets the position and size of the form.
|
||
func (f *Form) SetRect(x, y, width, height int) {
|
||
// 确保尺寸是最新的
|
||
f.recalculateSize()
|
||
|
||
// 类似于 submenu 的实现
|
||
ws := (width - f.width) / 2
|
||
hs := (height - f.height) / 2
|
||
|
||
// 确保不会超出屏幕
|
||
if f.width > width {
|
||
ws = 0
|
||
f.width = width - 1
|
||
}
|
||
|
||
if f.height > height {
|
||
hs = 0
|
||
f.height = height - 1
|
||
}
|
||
|
||
// 设置表单位置
|
||
f.Box.SetRect(x+ws, y+hs, f.width, f.height)
|
||
|
||
// 获取内部矩形并设置布局
|
||
x, y, width, height = f.Box.GetInnerRect()
|
||
f.layout.SetRect(x, y, width, height)
|
||
}
|
||
|
||
// Focus is called when this primitive receives focus.
|
||
func (f *Form) Focus(delegate func(p tview.Primitive)) {
|
||
// 确保表单获得焦点
|
||
if f.form != nil {
|
||
delegate(f.form)
|
||
} else {
|
||
// 如果表单为空,则让Box获得焦点
|
||
delegate(f.Box)
|
||
}
|
||
}
|
||
|
||
// HasFocus returns whether or not this primitive has focus.
|
||
func (f *Form) HasFocus() bool {
|
||
return f.form.HasFocus()
|
||
}
|
||
|
||
// InputHandler returns the handler for this primitive.
|
||
func (f *Form) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||
return f.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||
// ESC键处理
|
||
if event.Key() == tcell.KeyEscape && f.cancelHandler != nil {
|
||
f.cancelHandler()
|
||
return
|
||
}
|
||
|
||
// 将事件传递给表单
|
||
if handler := f.form.InputHandler(); handler != nil {
|
||
handler(event, setFocus)
|
||
}
|
||
})
|
||
}
|
||
|
||
// EmptyBoxSpace creates an empty box with the specified background color.
|
||
func EmptyBoxSpace(bgColor tcell.Color) *tview.Box {
|
||
box := tview.NewBox()
|
||
box.SetBackgroundColor(bgColor)
|
||
box.SetBorder(false)
|
||
return box
|
||
}
|