Did a lot of work off-repo, basically init
This commit is contained in:
parent
2e9b2ad2b1
commit
d0bb43cc8f
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Block executable
|
||||
diesel*
|
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module github.com/fivemoreminix/diesel
|
||||
|
||||
go 1.12
|
||||
|
||||
require github.com/gdamore/tcell/v2 v2.2.0
|
16
go.sum
Normal file
16
go.sum
Normal file
@ -0,0 +1,16 @@
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.2.0 h1:vSyEgKwraXPSOkvCk7IwOSyX+Pv3V2cV9CikJMXg4U4=
|
||||
github.com/gdamore/tcell/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
193
main.go
Normal file
193
main.go
Normal file
@ -0,0 +1,193 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"io/fs"
|
||||
|
||||
"github.com/fivemoreminix/diesel/ui"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
var theme = ui.Theme{}
|
||||
|
||||
var focusedComponent ui.Component = nil
|
||||
|
||||
func changeFocus(to ui.Component) {
|
||||
if focusedComponent != nil {
|
||||
focusedComponent.SetFocused(false)
|
||||
}
|
||||
focusedComponent = to
|
||||
to.SetFocused(true)
|
||||
}
|
||||
|
||||
func main() {
|
||||
s, e := tcell.NewScreen()
|
||||
if e != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", e)
|
||||
os.Exit(1)
|
||||
}
|
||||
if e := s.Init(); e != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", e)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer s.Fini() // Useful for handling panics
|
||||
|
||||
// defer func() {
|
||||
// if err := recover(); err != nil {
|
||||
// s.Fini()
|
||||
// fmt.Fprintln(os.Stderr, err)
|
||||
// }
|
||||
// }()
|
||||
|
||||
sizex, sizey := s.Size()
|
||||
|
||||
tabContainer := ui.NewTabContainer(&theme)
|
||||
tabContainer.SetPos(0, 1)
|
||||
tabContainer.SetSize(sizex, sizey-1)
|
||||
|
||||
var fileSelector *ui.FileSelectorDialog // if nil, we don't draw it
|
||||
|
||||
bar := ui.NewMenuBar(nil, &theme)
|
||||
|
||||
barFocused := false
|
||||
|
||||
// TODO: load menus in another function
|
||||
bar.Menus = append(bar.Menus, ui.NewMenu("File", &theme, []ui.Item{&ui.ItemEntry{Name: "New File", Callback: func() {
|
||||
textEdit := ui.NewTextEdit(&s, "", "", &theme) // No file path, no contents
|
||||
tabContainer.AddTab("noname", textEdit)
|
||||
}}, &ui.ItemEntry{Name: "Open...", Callback: func() {
|
||||
callback := func(filePaths []string) {
|
||||
for _, path := range filePaths {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
panic("Could not open file at path " + path)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
bytes, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
panic("Could not read all of file")
|
||||
}
|
||||
|
||||
textEdit := ui.NewTextEdit(&s, path, string(bytes), &theme)
|
||||
tabContainer.AddTab(path, textEdit)
|
||||
}
|
||||
fileSelector = nil // Hide the file selector
|
||||
changeFocus(tabContainer)
|
||||
barFocused = false
|
||||
}
|
||||
fileSelector = ui.NewFileSelectorDialog(
|
||||
&s,
|
||||
"Comma-separated files or a directory",
|
||||
true,
|
||||
&theme,
|
||||
callback,
|
||||
func() { // Dialog is canceled
|
||||
fileSelector = nil
|
||||
changeFocus(bar)
|
||||
barFocused = true
|
||||
},
|
||||
)
|
||||
changeFocus(fileSelector)
|
||||
}}, &ui.ItemEntry{Name: "Save", Callback: func() {
|
||||
if tabContainer.GetTabCount() > 0 {
|
||||
tab := tabContainer.GetTab(tabContainer.Selected)
|
||||
te := tab.Child.(*ui.TextEdit)
|
||||
if len(te.FilePath) > 0 {
|
||||
contents := te.String()
|
||||
|
||||
// Write the contents into the file, creating one if it does
|
||||
// not exist.
|
||||
err := ioutil.WriteFile(te.FilePath, []byte(contents), fs.ModePerm)
|
||||
if err != nil {
|
||||
panic("Could not write file at path " + te.FilePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}}, &ui.ItemEntry{Name: "Save As...", Callback: func() {
|
||||
// TODO: implement a "Save as" dialog system, and show that when trying to save noname files
|
||||
callback := func(filePaths []string) {
|
||||
fileSelector = nil // Hide the file selector
|
||||
}
|
||||
|
||||
fileSelector = ui.NewFileSelectorDialog(
|
||||
&s,
|
||||
"Select a file to overwrite",
|
||||
false,
|
||||
&theme,
|
||||
callback,
|
||||
func() { // Dialog canceled
|
||||
fileSelector = nil
|
||||
changeFocus(bar)
|
||||
},
|
||||
)
|
||||
changeFocus(fileSelector)
|
||||
}}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Exit", Callback: func() {
|
||||
s.Fini()
|
||||
os.Exit(0)
|
||||
}}}))
|
||||
|
||||
bar.Menus = append(bar.Menus, ui.NewMenu("Edit", &theme, []ui.Item{&ui.ItemEntry{Name: "New", Callback: func() {
|
||||
s.Beep()
|
||||
}}}))
|
||||
|
||||
bar.Menus = append(bar.Menus, ui.NewMenu("Search", &theme, []ui.Item{&ui.ItemEntry{Name: "New", Callback: func() {
|
||||
s.Beep()
|
||||
}}}))
|
||||
|
||||
changeFocus(tabContainer) // TabContainer is focused by default
|
||||
|
||||
main_loop:
|
||||
for {
|
||||
s.Clear()
|
||||
|
||||
// Draw background (grey and black checkerboard)
|
||||
ui.DrawRect(s, 0, 0, sizex, sizey, '▚', tcell.Style{}.Foreground(tcell.ColorGrey).Background(tcell.ColorBlack))
|
||||
|
||||
if tabContainer.GetTabCount() > 0 { // Draw the tab container only if a tab is open
|
||||
tabContainer.Draw(s)
|
||||
}
|
||||
bar.Draw(s) // Always draw the menu bar
|
||||
|
||||
if fileSelector != nil {
|
||||
// Update fileSelector dialog pos and size
|
||||
diagMinX, diagMinY := fileSelector.GetMinSize()
|
||||
fileSelector.SetSize(diagMinX, diagMinY)
|
||||
fileSelector.SetPos(sizex/2-diagMinX/2, sizey/2-diagMinY/2) // Center
|
||||
|
||||
fileSelector.Draw(s)
|
||||
}
|
||||
|
||||
s.Show()
|
||||
|
||||
switch ev := s.PollEvent().(type) {
|
||||
case *tcell.EventResize:
|
||||
sizex, sizey = s.Size()
|
||||
|
||||
bar.SetSize(sizex, 1)
|
||||
tabContainer.SetSize(sizex, sizey-1)
|
||||
|
||||
s.Sync() // Redraw everything
|
||||
case *tcell.EventKey:
|
||||
// On Escape, we change focus between editor and the MenuBar.
|
||||
if fileSelector == nil { // While no dialog is present...
|
||||
if ev.Key() == tcell.KeyEscape {
|
||||
barFocused = !barFocused
|
||||
if barFocused {
|
||||
changeFocus(bar)
|
||||
} else {
|
||||
changeFocus(tabContainer)
|
||||
}
|
||||
}
|
||||
// Ctrl + Q is a shortcut to exit
|
||||
if ev.Key() == tcell.KeyCtrlQ { // TODO: replace with shortcut keys in menus
|
||||
break main_loop
|
||||
}
|
||||
}
|
||||
|
||||
focusedComponent.HandleEvent(ev)
|
||||
}
|
||||
}
|
||||
}
|
79
ui/button.go
Normal file
79
ui/button.go
Normal file
@ -0,0 +1,79 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
type Button struct {
|
||||
Text string
|
||||
Callback func()
|
||||
|
||||
x, y int
|
||||
width, height int
|
||||
focused bool
|
||||
|
||||
Theme *Theme
|
||||
}
|
||||
|
||||
func NewButton(text string, theme *Theme, callback func()) *Button {
|
||||
return &Button{
|
||||
Text: text,
|
||||
Callback: callback,
|
||||
Theme: theme,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Button) Draw(s tcell.Screen) {
|
||||
var str string
|
||||
if b.focused {
|
||||
str = fmt.Sprintf("🭬 %s 🭮", b.Text)
|
||||
} else {
|
||||
str = fmt.Sprintf(" %s ", b.Text)
|
||||
}
|
||||
DrawStr(s, b.x, b.y, str, b.Theme.GetOrDefault("Button"))
|
||||
}
|
||||
|
||||
func (b *Button) SetFocused(v bool) {
|
||||
b.focused = v
|
||||
}
|
||||
|
||||
func (b *Button) SetTheme(theme *Theme) {
|
||||
b.Theme = theme
|
||||
}
|
||||
|
||||
func (b *Button) GetPos() (int, int) {
|
||||
return b.x, b.y
|
||||
}
|
||||
|
||||
func (b *Button) SetPos(x, y int) {
|
||||
b.x, b.y = x, y
|
||||
}
|
||||
|
||||
func (b *Button) GetMinSize() (int, int) {
|
||||
return len(b.Text) + 4, 1
|
||||
}
|
||||
|
||||
func (b *Button) GetSize() (int, int) {
|
||||
return b.GetMinSize()
|
||||
}
|
||||
|
||||
func (b *Button) SetSize(width, height int) {}
|
||||
|
||||
func (b *Button) HandleEvent(event tcell.Event) bool {
|
||||
if b.focused {
|
||||
switch ev := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
if ev.Key() == tcell.KeyEnter {
|
||||
if b.Callback != nil {
|
||||
b.Callback()
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
39
ui/component.go
Normal file
39
ui/component.go
Normal file
@ -0,0 +1,39 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
// A Component refers generally to the behavior of a UI "component". Components
|
||||
// include buttons, input fields, and labels. It is expected that after constructing
|
||||
// a component, to call the SetPos() function, and possibly SetSize() as well.
|
||||
//
|
||||
// Many components implement their own `New...()` function. In those constructor
|
||||
// functions, it is good practice for that component to set its size to be its
|
||||
// minimum size. TODO: implement that behavior
|
||||
type Component interface {
|
||||
// A component knows its position and size, which is used to draw itself in
|
||||
// its bounding rectangle.
|
||||
Draw(tcell.Screen)
|
||||
// Components can be focused, which may affect how it handles events. For
|
||||
// example, when a button is focused, the Return key may be pressed to
|
||||
// activate the button.
|
||||
SetFocused(bool)
|
||||
// Applies the theme to the component and all of its children.
|
||||
SetTheme(*Theme)
|
||||
|
||||
// Get position of the Component.
|
||||
GetPos() (int, int)
|
||||
// Set position of the Component.
|
||||
SetPos(int, int)
|
||||
|
||||
// Returns the smallest size the Component can be.
|
||||
GetMinSize() (int, int)
|
||||
// Get size of the Component.
|
||||
GetSize() (int, int)
|
||||
// Set size of the component. If size is smaller than minimum, minimum is
|
||||
// used, instead.
|
||||
SetSize(int, int)
|
||||
|
||||
tcell.EventHandler // A Component can handle events
|
||||
}
|
336
ui/container.go
Normal file
336
ui/container.go
Normal file
@ -0,0 +1,336 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
// A Container has zero or more Components. Containers decide how Components are
|
||||
// laid out in view, and may draw decorations like bounding boxes.
|
||||
type Container interface {
|
||||
Component
|
||||
}
|
||||
|
||||
// A BoxContainer draws an outline using the `Character` with `Style` attributes
|
||||
// around the `Child` Component.
|
||||
type BoxContainer struct {
|
||||
Child Component
|
||||
|
||||
x, y int
|
||||
width, height int
|
||||
ULRune rune // Rune for upper-left
|
||||
URRune rune // Rune for upper-right
|
||||
BLRune rune // Rune for bottom-left
|
||||
BRRune rune // Rune for bottom-right
|
||||
HorRune rune // Rune for horizontals
|
||||
VertRune rune // Rune for verticals
|
||||
|
||||
Style tcell.Style
|
||||
}
|
||||
|
||||
// New constructs a default BoxContainer using the terminal default style.
|
||||
func NewBoxContainer(child Component, style tcell.Style) *BoxContainer {
|
||||
return &BoxContainer{
|
||||
Child: child,
|
||||
ULRune: '╭',
|
||||
URRune: '╮',
|
||||
BLRune: '╰',
|
||||
BRRune: '╯',
|
||||
HorRune: '—',
|
||||
VertRune: '│',
|
||||
Style: style,
|
||||
}
|
||||
}
|
||||
|
||||
// Draw will draws the border of the BoxContainer, then it draws its child component.
|
||||
func (c *BoxContainer) Draw(s tcell.Screen) {
|
||||
DrawRectOutline(s, c.x, c.y, c.width, c.height, c.ULRune, c.URRune, c.BLRune, c.BRRune, c.HorRune, c.VertRune, c.Style)
|
||||
|
||||
if c.Child != nil {
|
||||
c.Child.Draw(s)
|
||||
}
|
||||
}
|
||||
|
||||
// SetFocused calls SetFocused on the child Component.
|
||||
func (c *BoxContainer) SetFocused(v bool) {
|
||||
if c.Child != nil {
|
||||
c.Child.SetFocused(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *BoxContainer) SetTheme(theme *Theme) {}
|
||||
|
||||
// GetPos returns the position of the container.
|
||||
func (c *BoxContainer) GetPos() (int, int) {
|
||||
return c.x, c.y
|
||||
}
|
||||
|
||||
// SetPos sets the position of the container and updates the child Component.
|
||||
func (c *BoxContainer) SetPos(x, y int) {
|
||||
c.x, c.y = x, y
|
||||
if c.Child != nil {
|
||||
c.Child.SetPos(x+1, y+1)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *BoxContainer) GetMinSize() (int, int) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// GetSize gets the size of the container.
|
||||
func (c *BoxContainer) GetSize() (int, int) {
|
||||
return c.width, c.height
|
||||
}
|
||||
|
||||
// SetSize sets the size of the container and updates the size of the child Component.
|
||||
func (c *BoxContainer) SetSize(width, height int) {
|
||||
c.width, c.height = width, height
|
||||
if c.Child != nil {
|
||||
c.Child.SetSize(width-2, height-2)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleEvent forwards the event to the child Component and returns whether it was handled.
|
||||
func (c *BoxContainer) HandleEvent(event tcell.Event) bool {
|
||||
if c.Child != nil {
|
||||
return c.Child.HandleEvent(event)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// A Tab is a child of a TabContainer; has a name and child Component.
|
||||
type Tab struct {
|
||||
Name string
|
||||
Child Component
|
||||
}
|
||||
|
||||
// A TabContainer organizes children by showing only one of them at a time.
|
||||
type TabContainer struct {
|
||||
Selected int
|
||||
|
||||
children []Tab
|
||||
x, y int
|
||||
width, height int
|
||||
focused bool
|
||||
|
||||
Theme *Theme
|
||||
}
|
||||
|
||||
func NewTabContainer(theme *Theme) *TabContainer {
|
||||
return &TabContainer{
|
||||
children: make([]Tab, 0, 4),
|
||||
Theme: theme,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TabContainer) AddTab(name string, child Component) {
|
||||
c.children = append(c.children, Tab{Name: name, Child: child})
|
||||
// Update new child's size and position
|
||||
child.SetPos(c.x+1, c.y+1)
|
||||
child.SetSize(c.width-2, c.height-2)
|
||||
}
|
||||
|
||||
// RemoveTab deletes the tab at `idx`. Returns true if the tab was found,
|
||||
// false otherwise.
|
||||
func (c *TabContainer) RemoveTab(idx int) bool {
|
||||
if idx >= 0 && idx < len(c.children) {
|
||||
copy(c.children[idx:], c.children[idx+1:]) // Shift all items after idx to the left
|
||||
c.children = c.children[:len(c.children)-1] // Shrink slice by one
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *TabContainer) GetTabCount() int {
|
||||
return len(c.children)
|
||||
}
|
||||
|
||||
func (c *TabContainer) GetTab(idx int) *Tab {
|
||||
return &c.children[idx]
|
||||
}
|
||||
|
||||
// Draw will draws the border of the BoxContainer, then it draws its child component.
|
||||
func (c *TabContainer) Draw(s tcell.Screen) {
|
||||
// Draw outline
|
||||
DrawRectOutlineDefault(s, c.x, c.y, c.width, c.height, c.Theme.GetOrDefault("TabContainer"))
|
||||
|
||||
combinedTabLength := 0
|
||||
for _, tab := range c.children {
|
||||
combinedTabLength += len(tab.Name) + 2 // 2 for padding
|
||||
}
|
||||
combinedTabLength += len(c.children)-1 // add for spacing between tabs
|
||||
|
||||
// Draw tabs
|
||||
col := c.x + c.width/2 - combinedTabLength/2 // Starting column
|
||||
for i, tab := range c.children {
|
||||
var sty tcell.Style
|
||||
if c.Selected == i {
|
||||
sty = c.Theme.GetOrDefault("TabSelected")
|
||||
} else {
|
||||
sty = c.Theme.GetOrDefault("Tab")
|
||||
}
|
||||
str := fmt.Sprintf(" %s ", tab.Name)
|
||||
//DrawStr(s, c.x+c.width/2-len(str)/2, c.y, str, sty)
|
||||
DrawStr(s, c.x+col, c.y, str, sty)
|
||||
col += len(str) + 1 // Add one for spacing between tabs
|
||||
}
|
||||
|
||||
// Draw selected child in center
|
||||
if c.Selected < len(c.children) {
|
||||
c.children[c.Selected].Child.Draw(s)
|
||||
}
|
||||
}
|
||||
|
||||
// SetFocused calls SetFocused on the visible child Component.
|
||||
func (c *TabContainer) SetFocused(v bool) {
|
||||
c.focused = v
|
||||
if c.Selected < len(c.children) {
|
||||
c.children[c.Selected].Child.SetFocused(v)
|
||||
}
|
||||
}
|
||||
|
||||
// SetTheme sets the theme.
|
||||
func (c *TabContainer) SetTheme(theme *Theme) {
|
||||
c.Theme = theme
|
||||
for _, tab := range c.children {
|
||||
tab.Child.SetTheme(theme) // Update the theme for all children
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TabContainer) GetMinSize() (int, int) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// GetPos returns the position of the container.
|
||||
func (c *TabContainer) GetPos() (int, int) {
|
||||
return c.x, c.y
|
||||
}
|
||||
|
||||
// SetPos sets the position of the container and updates the child Component.
|
||||
func (c *TabContainer) SetPos(x, y int) {
|
||||
c.x, c.y = x, y
|
||||
if c.Selected < len(c.children) {
|
||||
c.children[c.Selected].Child.SetPos(x+1, y+1)
|
||||
}
|
||||
}
|
||||
|
||||
// GetSize gets the size of the container.
|
||||
func (c *TabContainer) GetSize() (int, int) {
|
||||
return c.width, c.height
|
||||
}
|
||||
|
||||
// SetSize sets the size of the container and updates the size of the child Component.
|
||||
func (c *TabContainer) SetSize(width, height int) {
|
||||
c.width, c.height = width, height
|
||||
if c.Selected < len(c.children) {
|
||||
c.children[c.Selected].Child.SetSize(width-2, height-2)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleEvent forwards the event to the child Component and returns whether it was handled.
|
||||
func (c *TabContainer) HandleEvent(event tcell.Event) bool {
|
||||
switch ev := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
if ev.Key() == tcell.KeyTab { // Ctrl + Tab was pressed
|
||||
if ev.Modifiers() == tcell.ModCtrl {
|
||||
c.Selected++
|
||||
if c.Selected >= len(c.children) {
|
||||
c.Selected = 0
|
||||
}
|
||||
} else if ev.Modifiers() == tcell.ModCtrl & tcell.ModShift { // Ctrl + Shift + Tab was pressed
|
||||
c.Selected--
|
||||
if c.Selected < 0 {
|
||||
c.Selected = len(c.children)-1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.Selected < len(c.children) {
|
||||
return c.children[c.Selected].Child.HandleEvent(event)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: replace window container with draw function
|
||||
// A WindowContainer has a border, a title, and a button to close the window.
|
||||
type WindowContainer struct {
|
||||
Title string
|
||||
Child Component
|
||||
|
||||
x, y int
|
||||
width, height int
|
||||
focused bool
|
||||
|
||||
Theme *Theme
|
||||
}
|
||||
|
||||
// New constructs a default WindowContainer using the terminal default style.
|
||||
func NewWindowContainer(title string, child Component, theme *Theme) *WindowContainer {
|
||||
return &WindowContainer{
|
||||
Title: title,
|
||||
Child: child,
|
||||
Theme: theme,
|
||||
}
|
||||
}
|
||||
|
||||
// Draw will draws the border of the WindowContainer, then it draws its child component.
|
||||
func (w *WindowContainer) Draw(s tcell.Screen) {
|
||||
headerStyle := w.Theme.GetOrDefault("WindowHeader")
|
||||
|
||||
DrawRect(s, w.x, w.y, w.width, 1, ' ', headerStyle) // Draw header
|
||||
DrawStr(s, w.x+w.width/2 - len(w.Title)/2, w.y, w.Title, headerStyle) // Draw title
|
||||
DrawRect(s, w.x, w.y+1, w.width, w.height-1, ' ', w.Theme.GetOrDefault("Window")) // Draw body background
|
||||
|
||||
if w.Child != nil {
|
||||
w.Child.Draw(s)
|
||||
}
|
||||
}
|
||||
|
||||
// SetFocused calls SetFocused on the child Component.
|
||||
func (w *WindowContainer) SetFocused(v bool) {
|
||||
w.focused = v
|
||||
if w.Child != nil {
|
||||
w.Child.SetFocused(v)
|
||||
}
|
||||
}
|
||||
|
||||
// GetPos returns the position of the container.
|
||||
func (w *WindowContainer) GetPos() (int, int) {
|
||||
return w.x, w.y
|
||||
}
|
||||
|
||||
// SetPos sets the position of the container and updates the child Component.
|
||||
func (w *WindowContainer) SetPos(x, y int) {
|
||||
w.x, w.y = x, y
|
||||
if w.Child != nil {
|
||||
w.Child.SetPos(x, y+1)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WindowContainer) GetMinSize() (int, int) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// GetSize gets the size of the container.
|
||||
func (w *WindowContainer) GetSize() (int, int) {
|
||||
return w.width, w.height
|
||||
}
|
||||
|
||||
// SetSize sets the size of the container and updates the size of the child Component.
|
||||
func (w *WindowContainer) SetSize(width, height int) {
|
||||
w.width, w.height = width, height
|
||||
if w.Child != nil {
|
||||
w.Child.SetSize(width, height-2)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleEvent forwards the event to the child Component and returns whether it was handled.
|
||||
func (w *WindowContainer) HandleEvent(event tcell.Event) bool {
|
||||
if w.Child != nil {
|
||||
return w.Child.HandleEvent(event)
|
||||
}
|
||||
return false
|
||||
}
|
51
ui/drawfunctions.go
Normal file
51
ui/drawfunctions.go
Normal file
@ -0,0 +1,51 @@
|
||||
package ui
|
||||
|
||||
import "github.com/gdamore/tcell/v2"
|
||||
|
||||
// DrawRect renders a filled box at `x` and `y`, of size `width` and `height`.
|
||||
// Will not call `Show()`.
|
||||
func DrawRect(s tcell.Screen, x, y, width, height int, char rune, style tcell.Style) {
|
||||
for col := x; col < x+width; col++ {
|
||||
for row := y; row < y+height; row++ {
|
||||
s.SetContent(col, row, char, nil, style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DrawStr will render each character of a string at `x` and `y`.
|
||||
func DrawStr(s tcell.Screen, x, y int, str string, style tcell.Style) {
|
||||
runes := []rune(str)
|
||||
for idx := 0; idx < len(runes); idx++ {
|
||||
s.SetContent(x+idx, y, runes[idx], nil, style)
|
||||
}
|
||||
}
|
||||
|
||||
// DrawRectOutline draws only the outline of a rectangle, using `ul`, `ur`, `bl`, and `br`
|
||||
// for the corner runes, and `hor` and `vert` for the horizontal and vertical runes, respectively.
|
||||
func DrawRectOutline(s tcell.Screen, x, y, _width, _height int, ul, ur, bl, br, hor, vert rune, style tcell.Style) {
|
||||
width := x + _width - 1 // Length across
|
||||
height := y + _height - 1 // Length top-to-bottom
|
||||
|
||||
// Horizontals and verticals
|
||||
for col := x + 1; col < width; col++ {
|
||||
s.SetContent(col, y, hor, nil, style) // Top line
|
||||
s.SetContent(col, height, hor, nil, style) // Bottom line
|
||||
}
|
||||
for row := y + 1; row < height; row++ {
|
||||
s.SetContent(x, row, vert, nil, style) // Left line
|
||||
s.SetContent(width, row, vert, nil, style) // Right line
|
||||
}
|
||||
// Corners
|
||||
s.SetContent(x, y, ul, nil, style)
|
||||
s.SetContent(width, y, ur, nil, style)
|
||||
s.SetContent(x, height, bl, nil, style)
|
||||
s.SetContent(width, height, br, nil, style)
|
||||
}
|
||||
|
||||
// DrawRectOutlineDefault calls DrawRectOutline with the default edge runes.
|
||||
func DrawRectOutlineDefault(s tcell.Screen, x, y, width, height int, style tcell.Style) {
|
||||
DrawRectOutline(s, x, y, width, height, '┌', '┐', '└', '┘', '─', '│', style)
|
||||
}
|
||||
|
||||
// TODO: add DrawShadow(x, y, width, height int)
|
||||
// TODO: add DrawWindow(x, y, width, height int, style tcell.Style)
|
129
ui/fileselectordialog.go
Normal file
129
ui/fileselectordialog.go
Normal file
@ -0,0 +1,129 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
// A FileSelectorDialog is a WindowContainer with an input and buttons for selecting files.
|
||||
// It can be used to open zero or more existing files, or select one non-existant file (for saving).
|
||||
type FileSelectorDialog struct {
|
||||
MustExist bool // Whether the dialog should have a user select an existing file.
|
||||
FilesChosenCallback func([]string) // Returns slice of filenames selected. nil if user canceled.
|
||||
CancelCallback func() // Called when the dialog has been canceled by the user
|
||||
|
||||
container *WindowContainer
|
||||
x, y int
|
||||
width, height int
|
||||
focused bool
|
||||
|
||||
tabOrder []Component
|
||||
tabOrderIdx int
|
||||
|
||||
inputField *InputField
|
||||
confirmButton *Button
|
||||
cancelButton *Button
|
||||
|
||||
Theme *Theme
|
||||
}
|
||||
|
||||
func NewFileSelectorDialog(screen *tcell.Screen, title string, mustExist bool, theme *Theme, filesChosenCallback func([]string), cancelCallback func()) *FileSelectorDialog {
|
||||
dialog := &FileSelectorDialog{
|
||||
MustExist: mustExist,
|
||||
FilesChosenCallback: filesChosenCallback,
|
||||
container: NewWindowContainer(title, nil, theme),
|
||||
Theme: theme,
|
||||
}
|
||||
|
||||
dialog.inputField = NewInputField(screen, "", theme)
|
||||
dialog.confirmButton = NewButton("Confirm", theme, dialog.onConfirm)
|
||||
dialog.cancelButton = NewButton("Cancel", theme, cancelCallback)
|
||||
dialog.tabOrder = []Component{dialog.inputField, dialog.cancelButton, dialog.confirmButton}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
// onConfirm is a callback called by the confirm button.
|
||||
func (d *FileSelectorDialog) onConfirm() {
|
||||
if d.FilesChosenCallback != nil {
|
||||
files := strings.Split(d.inputField.Text, ",") // Split input by commas
|
||||
for i := range files {
|
||||
files[i] = strings.TrimSpace(files[i]) // Trim all strings in slice
|
||||
}
|
||||
d.FilesChosenCallback(files)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *FileSelectorDialog) SetTitle(title string) {
|
||||
d.container.Title = title
|
||||
}
|
||||
|
||||
func (d *FileSelectorDialog) Draw(s tcell.Screen) {
|
||||
d.container.Draw(s)
|
||||
|
||||
// Update positions of child components (dependent on size information that may not be available at SetPos() )
|
||||
btnWidth, _ := d.confirmButton.GetSize()
|
||||
d.confirmButton.SetPos(d.x+d.width-btnWidth-1, d.y+4) // Place "Ok" button on right, bottom
|
||||
|
||||
d.inputField.Draw(s)
|
||||
d.confirmButton.Draw(s)
|
||||
d.cancelButton.Draw(s)
|
||||
}
|
||||
|
||||
func (d *FileSelectorDialog) SetFocused(v bool) {
|
||||
d.focused = v
|
||||
d.tabOrder[d.tabOrderIdx].SetFocused(v)
|
||||
}
|
||||
|
||||
func (d *FileSelectorDialog) SetTheme(theme *Theme) {
|
||||
d.Theme = theme
|
||||
}
|
||||
|
||||
func (d *FileSelectorDialog) GetPos() (int, int) {
|
||||
return d.x, d.y
|
||||
}
|
||||
|
||||
func (d *FileSelectorDialog) SetPos(x, y int) {
|
||||
d.x, d.y = x, y
|
||||
d.container.SetPos(x, y)
|
||||
d.inputField.SetPos(d.x+1, d.y+2) // Center input field
|
||||
d.cancelButton.SetPos(d.x+1, d.y+4) // Place "Cancel" button on left, bottom
|
||||
}
|
||||
|
||||
func (d *FileSelectorDialog) GetMinSize() (int, int) {
|
||||
return len(d.container.Title) + 2, 6
|
||||
}
|
||||
|
||||
func (d *FileSelectorDialog) GetSize() (int, int) {
|
||||
return d.width, d.height
|
||||
}
|
||||
|
||||
func (d *FileSelectorDialog) SetSize(width, height int) {
|
||||
minX, minY := d.GetMinSize()
|
||||
d.width, d.height = Max(width, minX), Max(height, minY)
|
||||
d.container.SetSize(d.width, d.height)
|
||||
|
||||
d.inputField.SetSize(d.width-2, 1)
|
||||
d.cancelButton.SetSize(d.cancelButton.GetMinSize())
|
||||
d.confirmButton.SetSize(d.confirmButton.GetMinSize())
|
||||
}
|
||||
|
||||
func (d *FileSelectorDialog) HandleEvent(event tcell.Event) bool {
|
||||
switch ev := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
if ev.Key() == tcell.KeyTab {
|
||||
d.tabOrder[d.tabOrderIdx].SetFocused(false)
|
||||
|
||||
d.tabOrderIdx++
|
||||
if d.tabOrderIdx >= len(d.tabOrder) {
|
||||
d.tabOrderIdx = 0
|
||||
}
|
||||
|
||||
d.tabOrder[d.tabOrderIdx].SetFocused(true)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
return d.tabOrder[d.tabOrderIdx].HandleEvent(event)
|
||||
}
|
129
ui/inputfield.go
Normal file
129
ui/inputfield.go
Normal file
@ -0,0 +1,129 @@
|
||||
package ui
|
||||
|
||||
import "github.com/gdamore/tcell/v2"
|
||||
|
||||
// An InputField is a single-line input box.
|
||||
type InputField struct {
|
||||
Text string
|
||||
|
||||
cursorPos int
|
||||
scrollPos int
|
||||
x, y int
|
||||
width, height int
|
||||
focused bool
|
||||
screen *tcell.Screen
|
||||
|
||||
Theme *Theme
|
||||
}
|
||||
|
||||
func NewInputField(screen *tcell.Screen, placeholder string, theme *Theme) *InputField {
|
||||
return &InputField{
|
||||
Text: placeholder,
|
||||
screen: screen,
|
||||
Theme: theme,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *InputField) GetCursorPos() int {
|
||||
return f.cursorPos
|
||||
}
|
||||
|
||||
// SetCursorPos sets the cursor position offset. Offset is clamped to possible values.
|
||||
// The InputField is scrolled to show the new cursor position.
|
||||
func (f *InputField) SetCursorPos(offset int) {
|
||||
// Clamping
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
} else if offset > len(f.Text) {
|
||||
offset = len(f.Text)
|
||||
}
|
||||
|
||||
// Scrolling
|
||||
if offset >= f.scrollPos+f.width-2 { // If cursor position is out of view to the right...
|
||||
f.scrollPos = offset - f.width+2 // Scroll just enough to view that column
|
||||
} else if offset < f.scrollPos { // If cursor position is out of view to the left...
|
||||
f.scrollPos = offset
|
||||
}
|
||||
|
||||
f.cursorPos = offset
|
||||
if f.focused {
|
||||
(*f.screen).ShowCursor(f.x+offset-f.scrollPos+1, f.y)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *InputField) Delete(forward bool) {
|
||||
if forward {
|
||||
//if f.cursorPos
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (f *InputField) Draw(s tcell.Screen) {
|
||||
style := f.Theme.GetOrDefault("InputField")
|
||||
|
||||
DrawRect(s, f.x, f.y, f.width, f.height, ' ', style) // Draw background
|
||||
s.SetContent(f.x, f.y, '[', nil, style)
|
||||
s.SetContent(f.x+f.width-1, f.y, ']', nil, style)
|
||||
|
||||
if len(f.Text) > 0 {
|
||||
endPos := f.scrollPos + Min(len(f.Text)-f.scrollPos, f.width-2)
|
||||
DrawStr(s, f.x+1, f.y, f.Text[f.scrollPos:endPos], style) // Draw text
|
||||
}
|
||||
|
||||
// Update cursor
|
||||
f.SetCursorPos(f.cursorPos)
|
||||
}
|
||||
|
||||
func (f *InputField) SetFocused(v bool) {
|
||||
f.focused = v
|
||||
if v {
|
||||
f.SetCursorPos(f.cursorPos)
|
||||
} else {
|
||||
(*f.screen).HideCursor()
|
||||
}
|
||||
}
|
||||
|
||||
func (f *InputField) SetTheme(theme *Theme) {
|
||||
f.Theme = theme
|
||||
}
|
||||
|
||||
func (f *InputField) GetPos() (int, int) {
|
||||
return f.x, f.y
|
||||
}
|
||||
|
||||
func (f *InputField) SetPos(x, y int) {
|
||||
f.x, f.y = x, y
|
||||
}
|
||||
|
||||
func (f *InputField) GetMinSize() (int, int) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func (f *InputField) GetSize() (int, int) {
|
||||
return f.width, f.height
|
||||
}
|
||||
|
||||
func (f *InputField) SetSize(width, height int) {
|
||||
f.width, f.height = width, height
|
||||
}
|
||||
|
||||
func (f *InputField) HandleEvent(event tcell.Event) bool {
|
||||
switch ev := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
switch ev.Key() {
|
||||
case tcell.KeyLeft:
|
||||
f.SetCursorPos(f.cursorPos - 1)
|
||||
case tcell.KeyRight:
|
||||
f.SetCursorPos(f.cursorPos + 1)
|
||||
case tcell.KeyRune:
|
||||
ch := ev.Rune()
|
||||
f.Text += string(ch)
|
||||
f.SetCursorPos(f.cursorPos + 1)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
25
ui/label.go
Normal file
25
ui/label.go
Normal file
@ -0,0 +1,25 @@
|
||||
package ui
|
||||
|
||||
// Align defines the text alignment of a label.
|
||||
type Align uint8
|
||||
|
||||
const (
|
||||
// AlignLeft is the normal text alignment where text is aligned to the left
|
||||
// of its bounding box.
|
||||
AlignLeft Align = iota
|
||||
// AlignRight causes text to be aligned to the right of its bounding box.
|
||||
AlignRight
|
||||
// AlignJustify causes text to be left-aligned, but also spaced so that it
|
||||
// fits the entire box where it is being rendered.
|
||||
AlignJustify
|
||||
)
|
||||
|
||||
// A Label is a component for rendering text. Text can be rendered easily
|
||||
// without a Label, but this component forces the text to fit within its
|
||||
// bounding box and allows for left-align, right-align, and justify.
|
||||
type Label struct {
|
||||
Text string
|
||||
x, y int
|
||||
width, height int
|
||||
Alignment Align
|
||||
}
|
290
ui/menu.go
Normal file
290
ui/menu.go
Normal file
@ -0,0 +1,290 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
// Item is an interface implemented by ItemEntry and ItemMenu to be listed in Menus.
|
||||
type Item interface {
|
||||
isItem()
|
||||
GetName() string
|
||||
}
|
||||
|
||||
// An ItemSeparator is like a blank Item that cannot actually be selected. It is useful
|
||||
// for separating items in a Menu.
|
||||
type ItemSeparator struct{}
|
||||
|
||||
func (i *ItemSeparator) isItem() {}
|
||||
|
||||
// GetName returns an empty string.
|
||||
func (i *ItemSeparator) GetName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ItemEntry is a listing in a Menu with a name and callback.
|
||||
type ItemEntry struct {
|
||||
Name string
|
||||
Callback func()
|
||||
}
|
||||
|
||||
func (i *ItemEntry) isItem() {}
|
||||
|
||||
// GetName returns the name of the ItemEntry.
|
||||
func (i *ItemEntry) GetName() string {
|
||||
return i.Name
|
||||
}
|
||||
|
||||
func (m *Menu) isItem() {}
|
||||
|
||||
// GetName returns the name of the Menu.
|
||||
func (m *Menu) GetName() string {
|
||||
return m.Name
|
||||
}
|
||||
|
||||
// A MenuBar is a horizontal list of menus.
|
||||
type MenuBar struct {
|
||||
Menus []Menu
|
||||
|
||||
x, y int
|
||||
width, height int
|
||||
menuExpanded bool
|
||||
focused bool
|
||||
selected int // Index of selection in MenuBar
|
||||
|
||||
Theme *Theme
|
||||
}
|
||||
|
||||
func NewMenuBar(menus []Menu, theme *Theme) *MenuBar {
|
||||
if menus == nil {
|
||||
menus = make([]Menu, 0, 6)
|
||||
}
|
||||
|
||||
return &MenuBar{
|
||||
Menus: menus,
|
||||
Theme: theme,
|
||||
}
|
||||
}
|
||||
|
||||
// GetMenuXPos returns the X position of the name of Menu at `idx` visually.
|
||||
func (b *MenuBar) GetMenuXPos(idx int) int {
|
||||
x := 1
|
||||
for i := 0; i < idx; i++ {
|
||||
x += len(b.Menus[i].Name) + 2 // two for padding
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// Draw renders the MenuBar and its sub-menus.
|
||||
func (b *MenuBar) Draw(s tcell.Screen) {
|
||||
normalStyle := b.Theme.GetOrDefault("MenuBar")
|
||||
|
||||
// Draw menus based on whether b.focused and which is selected
|
||||
DrawRect(s, b.x, b.y, 200, 1, ' ', normalStyle) // TODO: calculate actual width
|
||||
col := b.x + 1
|
||||
for i, item := range b.Menus {
|
||||
str := fmt.Sprintf(" %s ", item.Name) // Surround the name in spaces
|
||||
var sty tcell.Style
|
||||
if b.selected == i && b.focused { // If we are drawing the selected item ...
|
||||
sty = b.Theme.GetOrDefault("MenuBarSelected") // Use style for selected items
|
||||
} else {
|
||||
sty = normalStyle
|
||||
}
|
||||
DrawStr(s, col, b.y, str, sty)
|
||||
col += len(str)
|
||||
}
|
||||
|
||||
if b.menuExpanded {
|
||||
menu := b.Menus[b.selected]
|
||||
menu.Draw(s) // Draw menu when it is expanded / visible
|
||||
}
|
||||
}
|
||||
|
||||
// SetFocused highlights the MenuBar and focuses any sub-menus.
|
||||
func (b *MenuBar) SetFocused(v bool) {
|
||||
b.focused = v
|
||||
if !v {
|
||||
b.menuExpanded = false
|
||||
}
|
||||
}
|
||||
|
||||
func (b *MenuBar) SetTheme(theme *Theme) {
|
||||
b.Theme = theme
|
||||
}
|
||||
|
||||
// GetPos returns the position of the MenuBar.
|
||||
func (b *MenuBar) GetPos() (int, int) {
|
||||
return b.x, b.y
|
||||
}
|
||||
|
||||
// SetPos sets the position of the MenuBar.
|
||||
func (b *MenuBar) SetPos(x, y int) {
|
||||
b.x, b.y = x, y
|
||||
}
|
||||
|
||||
func (b *MenuBar) GetMinSize() (int, int) {
|
||||
return 0, 1
|
||||
}
|
||||
|
||||
// GetSize returns the size of the MenuBar.
|
||||
func (b *MenuBar) GetSize() (int, int) {
|
||||
return b.width, b.height
|
||||
}
|
||||
|
||||
// SetSize sets the size of the MenuBar.
|
||||
func (b *MenuBar) SetSize(width, height int) {
|
||||
b.width, b.height = width, height
|
||||
}
|
||||
|
||||
// HandleEvent will propogate events to sub-menus and returns true if
|
||||
// any of them handled the event.
|
||||
func (b *MenuBar) HandleEvent(event tcell.Event) bool {
|
||||
switch ev := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
if ev.Key() == tcell.KeyEnter && !b.menuExpanded {
|
||||
menu := &b.Menus[b.selected]
|
||||
menu.SetPos(b.GetMenuXPos(b.selected), b.y+1)
|
||||
menu.SetFocused(true)
|
||||
|
||||
b.menuExpanded = true // Tells Draw() to render the menu
|
||||
} else if ev.Key() == tcell.KeyLeft {
|
||||
if b.selected <= 0 {
|
||||
b.selected = len(b.Menus) - 1 // Wrap to end
|
||||
} else {
|
||||
b.selected--
|
||||
}
|
||||
// Update position of new menu after changing menu selection
|
||||
b.Menus[b.selected].SetPos(b.GetMenuXPos(b.selected), b.y+1)
|
||||
} else if ev.Key() == tcell.KeyRight {
|
||||
if b.selected >= len(b.Menus)-1 {
|
||||
b.selected = 0 // Wrap to beginning
|
||||
} else {
|
||||
b.selected++
|
||||
}
|
||||
// Update position of new menu after changing menu selection
|
||||
b.Menus[b.selected].SetPos(b.GetMenuXPos(b.selected), b.y+1)
|
||||
} else {
|
||||
if b.menuExpanded {
|
||||
return b.Menus[b.selected].HandleEvent(event)
|
||||
} else {
|
||||
return false // Nobody to propogate our event to
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// A Menu contains one or more ItemEntry or ItemMenus.
|
||||
type Menu struct {
|
||||
Name string
|
||||
Items []Item
|
||||
|
||||
x, y int
|
||||
width, height int // Size may not be settable
|
||||
selected int // Index of selected Item
|
||||
|
||||
Theme *Theme
|
||||
}
|
||||
|
||||
// New creates a new Menu. `items` can be `nil`.
|
||||
func NewMenu(name string, theme *Theme, items []Item) Menu {
|
||||
if items == nil {
|
||||
items = make([]Item, 0, 6)
|
||||
}
|
||||
|
||||
return Menu{
|
||||
Name: name,
|
||||
Items: items,
|
||||
Theme: theme,
|
||||
}
|
||||
}
|
||||
|
||||
// Draw renders the Menu at its position.
|
||||
func (m *Menu) Draw(s tcell.Screen) {
|
||||
defaultStyle := m.Theme.GetOrDefault("Menu")
|
||||
|
||||
m.GetSize() // Call this to update internal width and height
|
||||
DrawRect(s, m.x, m.y, m.width, m.height, ' ', defaultStyle) // Fill background
|
||||
DrawRectOutlineDefault(s, m.x, m.y, m.width, m.height, defaultStyle) // Draw outline
|
||||
|
||||
// Draw items based on whether m.focused and which is selected
|
||||
for i, item := range m.Items {
|
||||
var sty tcell.Style
|
||||
if m.selected == i {
|
||||
sty = m.Theme.GetOrDefault("MenuSelected")
|
||||
} else {
|
||||
sty = defaultStyle
|
||||
}
|
||||
DrawStr(s, m.x+1, m.y+1+i, item.GetName(), sty)
|
||||
}
|
||||
}
|
||||
|
||||
// SetFocused does not do anything for a Menu.
|
||||
func (m *Menu) SetFocused(v bool) {
|
||||
// TODO: wat do
|
||||
}
|
||||
|
||||
// GetPos returns the position of the Menu.
|
||||
func (m *Menu) GetPos() (int, int) {
|
||||
return m.x, m.y
|
||||
}
|
||||
|
||||
// SetPos sets the position of the Menu.
|
||||
func (m *Menu) SetPos(x, y int) {
|
||||
m.x, m.y = x, y
|
||||
}
|
||||
|
||||
// GetSize returns the size of the Menu.
|
||||
func (m *Menu) GetSize() (int, int) {
|
||||
// TODO: no, pls don't do this
|
||||
maxLen := 0
|
||||
for _, item := range m.Items {
|
||||
len := len(item.GetName())
|
||||
if len > maxLen {
|
||||
maxLen = len
|
||||
}
|
||||
}
|
||||
m.width = maxLen + 2 // Add two for padding
|
||||
m.height = len(m.Items) + 2 // And another two for the same reason ...
|
||||
return m.width, m.height
|
||||
}
|
||||
|
||||
// SetSize sets the size of the Menu.
|
||||
func (m *Menu) SetSize(width, height int) {
|
||||
// Cannot set the size of a Menu
|
||||
}
|
||||
|
||||
// HandleEvent will handle events for a Menu and may propogate them
|
||||
// to sub-menus. Returns true if the event was handled.
|
||||
func (m *Menu) HandleEvent(event tcell.Event) bool {
|
||||
// TODO: simplify this function
|
||||
switch ev := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
if ev.Key() == tcell.KeyEnter {
|
||||
switch item := m.Items[m.selected].(type) {
|
||||
case *ItemEntry:
|
||||
item.Callback()
|
||||
case *Menu:
|
||||
// TODO: implement sub-menus ...
|
||||
}
|
||||
return true
|
||||
} else if ev.Key() == tcell.KeyUp {
|
||||
if m.selected <= 0 {
|
||||
m.selected = len(m.Items) - 1 // Wrap to end
|
||||
} else {
|
||||
m.selected--
|
||||
}
|
||||
return true
|
||||
} else if ev.Key() == tcell.KeyDown {
|
||||
if m.selected >= len(m.Items)-1 {
|
||||
m.selected = 0 // Wrap to beginning
|
||||
} else {
|
||||
m.selected++
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
414
ui/textedit.go
Normal file
414
ui/textedit.go
Normal file
@ -0,0 +1,414 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
// TextEdit is a field for line-based editing. It features syntax highlighting
|
||||
// tools, is autocomplete ready, and contains the various information about
|
||||
// content being edited.
|
||||
type TextEdit struct {
|
||||
LineNumbers bool // Whether to render line numbers (and therefore the column)
|
||||
Dirty bool // Whether the buffer has been edited
|
||||
UseHardTabs bool // When true, tabs are '\t'
|
||||
TabSize int // How many spaces to indent by
|
||||
IsCRLF bool // Whether the file's line endings are CRLF (\r\n) or LF (\n)
|
||||
FilePath string // Will be empty if the file has not been saved yet
|
||||
|
||||
buffer []string // TODO: replace line-based buffer with gap buffer
|
||||
screen *tcell.Screen // We keep our own reference to the screen for cursor purposes.
|
||||
x, y int
|
||||
width, height int
|
||||
focused bool
|
||||
curx, cury int // Zero-based: cursor points before the character at that position.
|
||||
prevCurCol int // Previous maximum column the cursor was at, when the user pressed left or right
|
||||
scrollx, scrolly int // X and Y offset of view, known as scroll
|
||||
|
||||
Theme *Theme
|
||||
}
|
||||
|
||||
// New will initialize the buffer using the given string `contents`. If the `filePath` or `FilePath` is empty,
|
||||
// it can be assumed that the TextEdit has no file association, or it is unsaved.
|
||||
func NewTextEdit(screen *tcell.Screen, filePath, contents string, theme *Theme) *TextEdit {
|
||||
te := &TextEdit{
|
||||
LineNumbers: true,
|
||||
UseHardTabs: true,
|
||||
TabSize: 4,
|
||||
FilePath: filePath,
|
||||
buffer: nil,
|
||||
screen: screen,
|
||||
Theme: theme,
|
||||
}
|
||||
te.SetContents(contents)
|
||||
return te
|
||||
}
|
||||
|
||||
// SetContents applies the string to the internal buffer of the TextEdit component.
|
||||
// The string is determined to be either CRLF or LF based on line-endings.
|
||||
func (t *TextEdit) SetContents(contents string) {
|
||||
loop:
|
||||
for _, r := range contents {
|
||||
switch r {
|
||||
case '\n':
|
||||
t.IsCRLF = false
|
||||
break loop
|
||||
case '\r':
|
||||
// We could check for a \n after, but what's the point?
|
||||
t.IsCRLF = true
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
delimiter := "\n"
|
||||
if t.IsCRLF {
|
||||
delimiter = "\r\n"
|
||||
}
|
||||
|
||||
t.buffer = strings.Split(contents, delimiter) // Split contents into lines
|
||||
}
|
||||
|
||||
func (t *TextEdit) String() string {
|
||||
delimiter := "\n"
|
||||
if t.IsCRLF {
|
||||
delimiter = "\r\n"
|
||||
}
|
||||
return strings.Join(t.buffer, delimiter)
|
||||
}
|
||||
|
||||
// Changes a file's line delimiters. If `crlf` is true, then line delimiters are replaced
|
||||
// with Windows CRLF (\r\n). If `crlf` is false, then line delimtiers are replaced with Unix
|
||||
// LF (\n). The TextEdit `IsCRLF` variable is updated with the new value.
|
||||
func (t *TextEdit) ChangeLineDelimiters(crlf bool) {
|
||||
t.IsCRLF = crlf
|
||||
// line delimiters are constructed with String() function
|
||||
}
|
||||
|
||||
// Delete with `forwards` false will backspace, destroying the character before the cursor,
|
||||
// while Delete with `forwards` true will delete the character after (or on) the cursor.
|
||||
// In insert mode, forwards is always true.
|
||||
func (t *TextEdit) Delete(forwards bool) {
|
||||
// TODO: deleting through lines
|
||||
if forwards { // Delete the character after the cursor
|
||||
if t.curx < len(t.buffer[t.cury]) { // If the cursor is not at the end of the line...
|
||||
lineRunes := []rune(t.buffer[t.cury])
|
||||
copy(lineRunes[t.curx:], lineRunes[t.curx+1:]) // Shift runes at cursor + 1 left
|
||||
lineRunes = lineRunes[:len(lineRunes)-1] // Shrink line length
|
||||
t.buffer[t.cury] = string(lineRunes) // Reassign line
|
||||
} else { // If the cursor is at the end of the line...
|
||||
if t.cury < len(t.buffer)-1 { // And the cursor is not at the last line...
|
||||
oldLineIdx := t.cury + 1
|
||||
curLineRunes := []rune(t.buffer[t.cury])
|
||||
oldLineRunes := []rune(t.buffer[oldLineIdx])
|
||||
curLineRunes = append(curLineRunes, oldLineRunes...) // Append runes from deleted line to current line
|
||||
t.buffer[t.cury] = string(curLineRunes) // Update the current line with the new runes
|
||||
|
||||
copy(t.buffer[oldLineIdx:], t.buffer[oldLineIdx+1:]) // Shift lines below the old line up
|
||||
t.buffer = t.buffer[:len(t.buffer)-1] // Shrink buffer by one line
|
||||
}
|
||||
}
|
||||
} else { // Delete the character before the cursor
|
||||
if t.curx > 0 { // If the cursor is not at the beginning of the line...
|
||||
lineRunes := []rune(t.buffer[t.cury])
|
||||
copy(lineRunes[t.curx-1:], lineRunes[t.curx:]) // Shift runes at cursor left
|
||||
lineRunes = lineRunes[:len(lineRunes)-1] // Shrink line length
|
||||
t.buffer[t.cury] = string(lineRunes) // Reassign line
|
||||
|
||||
t.SetLineCol(t.cury, t.curx-1) // Shift cursor left
|
||||
} else { // If the cursor is at the beginning of the line...
|
||||
if t.cury > 0 { // And the cursor is not at the first line...
|
||||
oldLineIdx := t.cury
|
||||
t.SetLineCol(t.cury-1, len(t.buffer[t.cury-1])) // Cursor goes to the end of the above line
|
||||
curLineRunes := []rune(t.buffer[t.cury])
|
||||
oldLineRunes := []rune(t.buffer[oldLineIdx])
|
||||
curLineRunes = append(curLineRunes, oldLineRunes...) // Append the old line to the current line
|
||||
t.buffer[t.cury] = string(curLineRunes) // Update the current line to the new runes
|
||||
|
||||
copy(t.buffer[oldLineIdx:], t.buffer[oldLineIdx+1:]) // Shift lines below the old line up
|
||||
t.buffer = t.buffer[:len(t.buffer)-1] // Shrink buffer by one line
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Writes `contents` at the cursor position. Line delimiters and tab character supported.
|
||||
// Any other control characters will be printed.
|
||||
func (t *TextEdit) Insert(contents string) {
|
||||
runes := []rune(contents)
|
||||
for i := 0; i < len(runes); i++ {
|
||||
ch := runes[i]
|
||||
switch ch {
|
||||
case '\r':
|
||||
// If the character after is a \n, then it is a CRLF
|
||||
if i+1 < len(runes) && runes[i+1] == '\n' {
|
||||
i++
|
||||
t.insertNewLine()
|
||||
}
|
||||
case '\n':
|
||||
t.insertNewLine()
|
||||
case '\b':
|
||||
t.Delete(false) // Delete the character before the cursor
|
||||
case '\t':
|
||||
if !t.UseHardTabs { // If this file does not use hard tabs...
|
||||
// Insert spaces
|
||||
spaces := []rune(strings.Repeat(" ", t.TabSize))
|
||||
spacesLen := len(spaces)
|
||||
|
||||
lineRunes := []rune(t.buffer[t.cury])
|
||||
lineRunes = append(lineRunes, spaces...)
|
||||
copy(lineRunes[t.curx+spacesLen:], lineRunes[t.curx:]) // Shift runes at cursor to the right
|
||||
copy(lineRunes[t.curx:], spaces) // Copy spaces into the gap
|
||||
|
||||
t.buffer[t.cury] = string(lineRunes) // Reassign the line
|
||||
|
||||
t.SetLineCol(t.cury, t.curx+spacesLen) // Advance the cursor
|
||||
break
|
||||
}
|
||||
fallthrough // Append the \t character
|
||||
default:
|
||||
// TODO: this operation is not efficient by any means. OPTIMIZE
|
||||
|
||||
// Insert character into line
|
||||
lineRunes := []rune(t.buffer[t.cury])
|
||||
lineRunes = append(lineRunes, ch) // Extend the length of the string
|
||||
copy(lineRunes[t.curx+1:], lineRunes[t.curx:]) // Shift runes at cursor to the right
|
||||
lineRunes[t.curx] = ch
|
||||
|
||||
t.buffer[t.cury] = string(lineRunes) // Reassign the line
|
||||
|
||||
t.SetLineCol(t.cury, t.curx+1) // Advance the cursor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// insertNewLine inserts a line break at the cursor and sets the cursor position to the first
|
||||
// column of that new line. Text before the cursor on the current line remains on that line,
|
||||
// text at or after the cursor on the current line is moved to the new line.
|
||||
func (t *TextEdit) insertNewLine() {
|
||||
lineRunes := []rune(t.buffer[t.cury]) // A slice of runes of the old line
|
||||
movedRunes := lineRunes[t.curx:] // A slice of the old line containing runes to be moved
|
||||
newLineRunes := make([]rune, len(movedRunes))
|
||||
copy(newLineRunes, movedRunes) // Copy old runes to new line
|
||||
t.buffer[t.cury] = string(lineRunes[:t.curx]) // Shrink old line's length
|
||||
|
||||
t.buffer = append(t.buffer, "") // Increment buffer length
|
||||
copy(t.buffer[t.cury+2:], t.buffer[t.cury+1:]) // Shift lines after current line down
|
||||
t.buffer[t.cury+1] = string(newLineRunes) // Assign the new line
|
||||
|
||||
t.SetLineCol(t.cury+1, 0) // Go to start of new line
|
||||
}
|
||||
|
||||
// GetLineCol returns (line, col) of the cursor. Zero is origin for both.
|
||||
func (t *TextEdit) GetLineCol() (int, int) {
|
||||
return t.cury, t.curx
|
||||
}
|
||||
|
||||
// SetLineCol sets the cursor line and column position. Zero is origin for both.
|
||||
// If `line` is out of bounds, `line` will be clamped to the closest available line.
|
||||
// If `col` is out of bounds, `col` will be clamped to the closest column available for the line.
|
||||
// Will scroll the TextEdit just enough to see the line the cursor is at.
|
||||
func (t *TextEdit) SetLineCol(line, col int) {
|
||||
// Clamp the line input
|
||||
if line < 0 {
|
||||
line = 0
|
||||
} else if len := len(t.buffer); line >= len { // If new line is beyond the length of the buffer...
|
||||
line = len - 1 // Change that line to be the end of the buffer, instead
|
||||
}
|
||||
|
||||
// Clamp the column input
|
||||
if col < 0 {
|
||||
col = 0
|
||||
} else if len := len(t.buffer[line]); col > len {
|
||||
col = len
|
||||
}
|
||||
|
||||
// Handle hard tabs
|
||||
tabOffset := 0 // Offset for the current column (temporary; purely visual)
|
||||
if t.UseHardTabs { // If the file is encoded as tabs, not spaces...
|
||||
tabOffset = (t.TabSize - 1) * strings.Count(t.buffer[line][0:col], "\t") // Shift the cursor for tabs (they are rendered as spaces)
|
||||
}
|
||||
|
||||
// Scroll the screen when going to lines out of view
|
||||
if line >= t.scrolly+t.height-1 { // If the new line is below view...
|
||||
t.scrolly = line - t.height + 1 // Scroll just enough to view that line
|
||||
} else if line < t.scrolly { // If the new line is above view
|
||||
t.scrolly = line
|
||||
}
|
||||
|
||||
t.cury, t.curx = line, col
|
||||
if t.focused {
|
||||
columnWidth := t.getColumnWidth()
|
||||
(*t.screen).ShowCursor(t.x+columnWidth+col+tabOffset, t.y+line-t.scrolly)
|
||||
}
|
||||
}
|
||||
|
||||
// CursorUp moves the cursor up a line.
|
||||
func (t *TextEdit) CursorUp() {
|
||||
t.SetLineCol(t.cury-1, t.prevCurCol)
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down a line.
|
||||
func (t *TextEdit) CursorDown() {
|
||||
if t.cury >= len(t.buffer)-1 { // If the cursor is at the last line...
|
||||
t.SetLineCol(t.cury, len(t.buffer[t.cury])) // Go to end of current line
|
||||
} else {
|
||||
t.SetLineCol(t.cury+1, t.prevCurCol) // Go to line below
|
||||
}
|
||||
}
|
||||
|
||||
// CursorLeft moves the cursor left a column.
|
||||
func (t *TextEdit) CursorLeft() {
|
||||
if t.curx <= 0 && t.cury != 0 { // If we are at the beginning of the current line...
|
||||
t.SetLineCol(t.cury-1, math.MaxInt32) // Go to end of line above
|
||||
} else {
|
||||
t.SetLineCol(t.cury, t.curx-1)
|
||||
}
|
||||
t.prevCurCol = t.curx
|
||||
}
|
||||
|
||||
// CursorRight moves the cursor right a column.
|
||||
func (t *TextEdit) CursorRight() {
|
||||
// If we are at the end of the current line,
|
||||
// and not at the last line...
|
||||
if t.curx >= len(t.buffer[t.cury]) && t.cury < len(t.buffer)-1 {
|
||||
t.SetLineCol(t.cury+1, 0) // Go to beginning of line below
|
||||
} else {
|
||||
t.SetLineCol(t.cury, t.curx+1)
|
||||
}
|
||||
t.prevCurCol = t.curx
|
||||
}
|
||||
|
||||
// getColumnWidth returns the width of the line numbers column if it is present.
|
||||
func (t *TextEdit) getColumnWidth() int {
|
||||
columnWidth := 0
|
||||
if t.LineNumbers {
|
||||
// Set columnWidth to max count of line number digits
|
||||
columnWidth = Max(2, len(strconv.Itoa(len(t.buffer)))) // Column has minimum width of 2
|
||||
}
|
||||
return columnWidth
|
||||
}
|
||||
|
||||
// Draw renders the TextEdit component.
|
||||
func (t *TextEdit) Draw(s tcell.Screen) {
|
||||
columnWidth := t.getColumnWidth()
|
||||
bufferLen := len(t.buffer)
|
||||
|
||||
textEditStyle := t.Theme.GetOrDefault("TextEdit")
|
||||
columnStyle := t.Theme.GetOrDefault("TextEditColumn")
|
||||
|
||||
DrawRect(s, t.x, t.y, t.width, t.height, ' ', textEditStyle) // Fill background
|
||||
|
||||
for lineY := t.y; lineY < t.y+t.height; lineY++ { // For each line we can draw...
|
||||
line := lineY + t.scrolly - t.y // The line number being drawn (starts at zero)
|
||||
|
||||
lineNumStr := ""
|
||||
|
||||
if line < bufferLen { // Only index buffer if we are within it...
|
||||
lineNumStr = strconv.Itoa(line + 1) // Line number as a string
|
||||
|
||||
var lineStr string // Line to be drawn
|
||||
if t.UseHardTabs {
|
||||
lineStr = strings.ReplaceAll(t.buffer[line], "\t", strings.Repeat(" ", t.TabSize))
|
||||
} else {
|
||||
lineStr = t.buffer[line]
|
||||
}
|
||||
|
||||
DrawStr(s, t.x+columnWidth, lineY, lineStr, textEditStyle) // Draw line
|
||||
}
|
||||
|
||||
columnStr := fmt.Sprintf("%s%s", strings.Repeat(" ", columnWidth-len(lineNumStr)), lineNumStr) // Right align line number
|
||||
|
||||
DrawStr(s, t.x, lineY, columnStr, columnStyle) // Draw column
|
||||
}
|
||||
|
||||
// Update cursor
|
||||
t.SetLineCol(t.cury, t.curx)
|
||||
}
|
||||
|
||||
// SetFocused sets whether the TextEdit is focused. When focused, the cursor is set visible
|
||||
// and its position is updated on every event.
|
||||
func (t *TextEdit) SetFocused(v bool) {
|
||||
t.focused = v
|
||||
if v {
|
||||
t.SetLineCol(t.curx, t.cury)
|
||||
} else {
|
||||
(*t.screen).HideCursor()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TextEdit) SetTheme(theme *Theme) {
|
||||
t.Theme = theme
|
||||
}
|
||||
|
||||
// GetPos gets the position of the TextEdit.
|
||||
func (t *TextEdit) GetPos() (int, int) {
|
||||
return t.x, t.y
|
||||
}
|
||||
|
||||
// SetPos sets the position of the TextEdit.
|
||||
func (t *TextEdit) SetPos(x, y int) {
|
||||
t.x, t.y = x, y
|
||||
}
|
||||
|
||||
func (t *TextEdit) GetMinSize() (int, int) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// GetSize gets the size of the TextEdit.
|
||||
func (t *TextEdit) GetSize() (int, int) {
|
||||
return t.width, t.height
|
||||
}
|
||||
|
||||
// SetSize sets the size of the TextEdit.
|
||||
func (t *TextEdit) SetSize(width, height int) {
|
||||
t.width, t.height = width, height
|
||||
}
|
||||
|
||||
// HandleEvent allows the TextEdit to handle `event` if it chooses, returns
|
||||
// whether the TextEdit handled the event.
|
||||
func (t *TextEdit) HandleEvent(event tcell.Event) bool {
|
||||
switch ev := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
switch ev.Key() {
|
||||
// Cursor movement
|
||||
case tcell.KeyUp:
|
||||
t.CursorUp()
|
||||
case tcell.KeyDown:
|
||||
t.CursorDown()
|
||||
case tcell.KeyLeft:
|
||||
t.CursorLeft()
|
||||
case tcell.KeyRight:
|
||||
t.CursorRight()
|
||||
case tcell.KeyHome:
|
||||
t.SetLineCol(t.cury, 0)
|
||||
case tcell.KeyEnd:
|
||||
t.SetLineCol(t.cury, len(t.buffer[t.cury]))
|
||||
|
||||
// Deleting
|
||||
case tcell.KeyBackspace:
|
||||
fallthrough
|
||||
case tcell.KeyBackspace2:
|
||||
t.Delete(false)
|
||||
case tcell.KeyDelete:
|
||||
t.Delete(true)
|
||||
|
||||
// Other control
|
||||
case tcell.KeyTab:
|
||||
t.Insert("\t") // (can translate to four spaces)
|
||||
case tcell.KeyEnter:
|
||||
t.insertNewLine()
|
||||
|
||||
// Inserting
|
||||
case tcell.KeyRune:
|
||||
t.Insert(string(ev.Rune())) // Insert rune
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
47
ui/theme.go
Normal file
47
ui/theme.go
Normal file
@ -0,0 +1,47 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
// A Theme is a map of string names to styles. Themes can be passed by reference to components
|
||||
// to set their styles. Some components will depend upon the basic keys, but most components
|
||||
// may use keys specific to their component. If a theme value cannot be found, then the
|
||||
// `DefaultTheme` value will be used, instead. An updated list of theme keys can be found on
|
||||
// the default theme.
|
||||
type Theme map[string]tcell.Style
|
||||
|
||||
func (theme *Theme) GetOrDefault(key string) tcell.Style {
|
||||
if theme != nil {
|
||||
if val, ok := (*theme)[key]; ok {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := DefaultTheme[key]; ok {
|
||||
return val
|
||||
} else {
|
||||
panic(fmt.Sprintf("key \"%v\" not present in default theme", key))
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultTheme uses only the first 16 colors present in most colored terminals.
|
||||
var DefaultTheme = Theme{
|
||||
"Normal": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
||||
"Button": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite),
|
||||
"InputField": tcell.Style{}.Foreground(tcell.ColorWhite).Background(tcell.ColorBlack),
|
||||
"MenuBar": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
||||
"MenuBarSelected": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
||||
"Menu": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
||||
"MenuSelected": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
||||
"QuickChar": tcell.Style{}.Foreground(tcell.ColorYellow).Background(tcell.ColorBlack),
|
||||
"Tab": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
||||
"TabContainer": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
||||
"TabSelected": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
||||
"TextEdit": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
||||
"TextEditColumn": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorGray),
|
||||
"Window": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
||||
"WindowHeader": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite),
|
||||
}
|
23
ui/util.go
Normal file
23
ui/util.go
Normal file
@ -0,0 +1,23 @@
|
||||
package ui
|
||||
|
||||
// Max returns the larger integer.
|
||||
func Max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Min returns the smaller integer.
|
||||
func Min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Clamp keeps `v` within `a` and `b` numerically. `a` must be smaller than `b`.
|
||||
// Returns clamped `v`.
|
||||
func Clamp(v, a, b int) int {
|
||||
return Max(a, Min(v, b))
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user