package cview import ( "math" "sync" "github.com/gdamore/tcell/v2" ) // Slider is a progress bar which may be modified via keyboard and mouse. type Slider struct { *ProgressBar // The text to be displayed before the slider. label []byte // The screen width of the label area. A value of 0 means use the width of // the label text. labelWidth int // The label color. labelColor tcell.Color // The label color when focused. labelColorFocused tcell.Color // The background color of the input area. fieldBackgroundColor tcell.Color // The background color of the input area when focused. fieldBackgroundColorFocused tcell.Color // The text color of the input area. fieldTextColor tcell.Color // The text color of the input area when focused. fieldTextColorFocused tcell.Color // The amount to increment by when modified via keyboard. increment int // Set to true when mouse dragging is in progress. dragging bool // An optional function which is called when the user changes the value of // this slider. changed func(value int) // An optional function which is called when the user indicated that they // are done entering text. The key which was pressed is provided (tab, // shift-tab, or escape). done func(tcell.Key) // A callback function set by the Form class and called when the user leaves // this form item. finished func(tcell.Key) sync.RWMutex } // NewSlider returns a new slider. func NewSlider() *Slider { s := &Slider{ ProgressBar: NewProgressBar(), increment: 10, labelColor: Styles.SecondaryTextColor, fieldBackgroundColor: Styles.ContrastBackgroundColor, fieldTextColor: Styles.PrimaryTextColor, labelColorFocused: ColorUnset, fieldBackgroundColorFocused: ColorUnset, fieldTextColorFocused: ColorUnset, } return s } // SetLabel sets the text to be displayed before the input area. func (s *Slider) SetLabel(label string) { s.Lock() defer s.Unlock() s.label = []byte(label) } // GetLabel returns the text to be displayed before the input area. func (s *Slider) GetLabel() string { s.RLock() defer s.RUnlock() return string(s.label) } // SetLabelWidth sets the screen width of the label. A value of 0 will cause the // primitive to use the width of the label string. func (s *Slider) SetLabelWidth(width int) { s.Lock() defer s.Unlock() s.labelWidth = width } // SetLabelColor sets the color of the label. func (s *Slider) SetLabelColor(color tcell.Color) { s.Lock() defer s.Unlock() s.labelColor = color } // SetLabelColorFocused sets the color of the label when focused. func (s *Slider) SetLabelColorFocused(color tcell.Color) { s.Lock() defer s.Unlock() s.labelColorFocused = color } // SetFieldBackgroundColor sets the background color of the input area. func (s *Slider) SetFieldBackgroundColor(color tcell.Color) { s.Lock() defer s.Unlock() s.fieldBackgroundColor = color } // SetFieldBackgroundColorFocused sets the background color of the input area when focused. func (s *Slider) SetFieldBackgroundColorFocused(color tcell.Color) { s.Lock() defer s.Unlock() s.fieldBackgroundColorFocused = color } // SetFieldTextColor sets the text color of the input area. func (s *Slider) SetFieldTextColor(color tcell.Color) { s.Lock() defer s.Unlock() s.fieldTextColor = color } // SetFieldTextColorFocused sets the text color of the input area when focused. func (s *Slider) SetFieldTextColorFocused(color tcell.Color) { s.Lock() defer s.Unlock() s.fieldTextColorFocused = color } // GetFieldHeight returns the height of the field. func (s *Slider) GetFieldHeight() int { return 1 } // GetFieldWidth returns this primitive's field width. func (s *Slider) GetFieldWidth() int { return 0 } // SetIncrement sets the amount the slider is incremented by when modified via // keyboard. func (s *Slider) SetIncrement(increment int) { s.Lock() defer s.Unlock() s.increment = increment } // SetChangedFunc sets a handler which is called when the value of this slider // was changed by the user. The handler function receives the new value. func (s *Slider) SetChangedFunc(handler func(value int)) { s.Lock() defer s.Unlock() s.changed = handler } // SetDoneFunc sets a handler which is called when the user is done using the // slider. The callback function is provided with the key that was pressed, // which is one of the following: // // - KeyEscape: Abort text input. // - KeyTab: Move to the next field. // - KeyBacktab: Move to the previous field. func (s *Slider) SetDoneFunc(handler func(key tcell.Key)) { s.Lock() defer s.Unlock() s.done = handler } // SetFinishedFunc sets a callback invoked when the user leaves this form item. func (s *Slider) SetFinishedFunc(handler func(key tcell.Key)) { s.Lock() defer s.Unlock() s.finished = handler } // Draw draws this primitive onto the screen. func (s *Slider) Draw(screen tcell.Screen) { if !s.GetVisible() { return } s.Box.Draw(screen) hasFocus := s.GetFocusable().HasFocus() s.Lock() // Select colors labelColor := s.labelColor fieldBackgroundColor := s.fieldBackgroundColor fieldTextColor := s.fieldTextColor if hasFocus { if s.labelColorFocused != ColorUnset { labelColor = s.labelColorFocused } if s.fieldBackgroundColorFocused != ColorUnset { fieldBackgroundColor = s.fieldBackgroundColorFocused } if s.fieldTextColorFocused != ColorUnset { fieldTextColor = s.fieldTextColorFocused } } // Prepare. x, y, width, height := s.GetInnerRect() rightLimit := x + width if height < 1 || rightLimit <= x { s.Unlock() return } // Draw label. if len(s.label) > 0 { if s.vertical { height-- // TODO draw label on bottom } else { if s.labelWidth > 0 { labelWidth := s.labelWidth if labelWidth > rightLimit-x { labelWidth = rightLimit - x } Print(screen, []byte(s.label), x, y, labelWidth, AlignLeft, labelColor) x += labelWidth + 1 width -= labelWidth + 1 } else { _, drawnWidth := Print(screen, []byte(s.label), x, y, rightLimit-x, AlignLeft, labelColor) x += drawnWidth + 1 width -= drawnWidth + 1 } } } // Draw slider. s.Unlock() s.ProgressBar.SetRect(x, y, width, height) s.ProgressBar.SetEmptyColor(fieldBackgroundColor) s.ProgressBar.SetFilledColor(fieldTextColor) s.ProgressBar.Draw(screen) } // InputHandler returns the handler for this primitive. func (s *Slider) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return s.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { if HitShortcut(event, Keys.Cancel, Keys.MovePreviousField, Keys.MoveNextField) { if s.done != nil { s.done(event.Key()) } if s.finished != nil { s.finished(event.Key()) } return } previous := s.progress if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) { s.SetProgress(0) } else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) { s.SetProgress(s.max) } else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2, Keys.MoveRight, Keys.MoveRight2, Keys.MovePreviousField) { s.AddProgress(s.increment) } else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2, Keys.MoveLeft, Keys.MoveLeft2, Keys.MoveNextField) { s.AddProgress(s.increment * -1) } if s.progress != previous && s.changed != nil { s.changed(s.progress) } }) } // MouseHandler returns the mouse handler for this primitive. func (s *Slider) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return s.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { x, y := event.Position() if !s.InRect(x, y) { s.dragging = false return false, nil } // Process mouse event. if action == MouseLeftClick { setFocus(s) consumed = true } handleMouse := func() { if !s.ProgressBar.InRect(x, y) { s.dragging = false return } bx, by, bw, bh := s.GetInnerRect() var clickPos, clickRange int if s.ProgressBar.vertical { clickPos = (bh - 1) - (y - by) clickRange = bh - 1 } else { clickPos = x - bx clickRange = bw - 1 } setValue := int(math.Floor(float64(s.max) * (float64(clickPos) / float64(clickRange)))) if setValue != s.progress { s.SetProgress(setValue) if s.changed != nil { s.changed(s.progress) } } } // Handle dragging. Clicks are implicitly handled by this logic. switch action { case MouseLeftDown: setFocus(s) consumed = true capture = s s.dragging = true handleMouse() case MouseMove: if s.dragging { consumed = true capture = s handleMouse() } case MouseLeftUp: if s.dragging { consumed = true s.dragging = false handleMouse() } } return }) }