package cview import ( "sync" "github.com/gdamore/tcell/v2" ) // Modal is a centered message window used to inform the user or prompt them // for an immediate decision. It needs to have at least one button (added via // AddButtons) or it will never disappear. You may change the title and // appearance of the window by modifying the Frame returned by GetFrame. You // may include additional elements within the window by modifying the Form // returned by GetForm. type Modal struct { *Box // The Frame embedded in the Modal. frame *Frame // The Form embedded in the Modal's Frame. form *Form // The message text (original, not word-wrapped). text string // The text color. textColor tcell.Color // The optional callback for when the user clicked one of the buttons. It // receives the index of the clicked button and the button's label. done func(buttonIndex int, buttonLabel string) sync.RWMutex } // NewModal returns a new centered message window. func NewModal() *Modal { m := &Modal{ Box: NewBox(), textColor: Styles.PrimaryTextColor, } m.form = NewForm() m.form.SetButtonsAlign(AlignCenter) m.form.SetButtonBackgroundColor(Styles.PrimitiveBackgroundColor) m.form.SetButtonTextColor(Styles.PrimaryTextColor) m.form.SetBackgroundColor(Styles.ContrastBackgroundColor) m.form.SetPadding(0, 0, 0, 0) m.form.SetCancelFunc(func() { if m.done != nil { m.done(-1, "") } }) m.frame = NewFrame(m.form) m.frame.SetBorder(true) m.frame.SetBorders(0, 0, 1, 0, 0, 0) m.frame.SetBackgroundColor(Styles.ContrastBackgroundColor) m.frame.SetPadding(1, 1, 1, 1) m.focus = m return m } // SetBackgroundColor sets the color of the Modal Frame background. func (m *Modal) SetBackgroundColor(color tcell.Color) { m.Lock() defer m.Unlock() m.form.SetBackgroundColor(color) m.frame.SetBackgroundColor(color) } // SetTextColor sets the color of the message text. func (m *Modal) SetTextColor(color tcell.Color) { m.Lock() defer m.Unlock() m.textColor = color } // SetButtonBackgroundColor sets the background color of the buttons. func (m *Modal) SetButtonBackgroundColor(color tcell.Color) { m.Lock() defer m.Unlock() m.form.SetButtonBackgroundColor(color) } // SetButtonTextColor sets the color of the button texts. func (m *Modal) SetButtonTextColor(color tcell.Color) { m.Lock() defer m.Unlock() m.form.SetButtonTextColor(color) } // SetDoneFunc sets a handler which is called when one of the buttons was // pressed. It receives the index of the button as well as its label text. The // handler is also called when the user presses the Escape key. The index will // then be negative and the label text an empty string. func (m *Modal) SetDoneFunc(handler func(buttonIndex int, buttonLabel string)) { m.Lock() defer m.Unlock() m.done = handler } // SetText sets the message text of the window. The text may contain line // breaks. Note that words are wrapped, too, based on the final size of the // window. func (m *Modal) SetText(text string) { m.Lock() defer m.Unlock() m.text = text } // GetForm returns the Form embedded in the window. The returned Form may be // modified to include additional elements (e.g. AddInputField, AddFormItem). func (m *Modal) GetForm() *Form { m.RLock() defer m.RUnlock() return m.form } // GetFrame returns the Frame embedded in the window. func (m *Modal) GetFrame() *Frame { m.RLock() defer m.RUnlock() return m.frame } // AddButtons adds buttons to the window. There must be at least one button and // a "done" handler so the window can be closed again. func (m *Modal) AddButtons(labels []string) { m.Lock() defer m.Unlock() for index, label := range labels { func(i int, l string) { m.form.AddButton(label, func() { if m.done != nil { m.done(i, l) } }) button := m.form.GetButton(m.form.GetButtonCount() - 1) button.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyDown, tcell.KeyRight: return tcell.NewEventKey(tcell.KeyTab, 0, tcell.ModNone) case tcell.KeyUp, tcell.KeyLeft: return tcell.NewEventKey(tcell.KeyBacktab, 0, tcell.ModNone) } return event }) }(index, label) } } // ClearButtons removes all buttons from the window. func (m *Modal) ClearButtons() { m.Lock() defer m.Unlock() m.form.ClearButtons() } // SetFocus shifts the focus to the button with the given index. func (m *Modal) SetFocus(index int) { m.Lock() defer m.Unlock() m.form.SetFocus(index) } // Focus is called when this primitive receives focus. func (m *Modal) Focus(delegate func(p Primitive)) { delegate(m.form) } // HasFocus returns whether or not this primitive has focus. func (m *Modal) HasFocus() bool { return m.GetForm().HasFocus() } // Draw draws this primitive onto the screen. func (m *Modal) Draw(screen tcell.Screen) { if !m.GetVisible() { return } formItemCount := m.form.GetFormItemCount() m.Lock() defer m.Unlock() // Calculate the width of this Modal. buttonsWidth := 0 for _, button := range m.form.buttons { buttonsWidth += TaggedTextWidth(button.label) + 4 + 2 } buttonsWidth -= 2 screenWidth, screenHeight := screen.Size() width := screenWidth / 3 if width < buttonsWidth { width = buttonsWidth } // width is now without the box border. // Reset the text and find out how wide it is. m.frame.Clear() lines := WordWrap(m.text, width) for _, line := range lines { m.frame.AddText(line, true, AlignCenter, m.textColor) } // Set the Modal's position and size. height := len(lines) + (formItemCount * 2) + 6 width += 4 x := (screenWidth - width) / 2 y := (screenHeight - height) / 2 m.SetRect(x, y, width, height) // Draw the frame. m.frame.SetRect(x, y, width, height) m.frame.Draw(screen) } // MouseHandler returns the mouse handler for this primitive. func (m *Modal) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return m.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { // Pass mouse events on to the form. consumed, capture = m.form.MouseHandler()(action, event, setFocus) if !consumed && action == MouseLeftClick && m.InRect(event.Position()) { setFocus(m) consumed = true } return }) }