diff --git a/draw_buffer.go b/draw_buffer.go index adb890c..29a168e 100644 --- a/draw_buffer.go +++ b/draw_buffer.go @@ -1,13 +1,12 @@ package tui import ( - "git.tordarus.net/tordarus/buf2d" "github.com/gdamore/tcell" ) -func drawBuffer(scr tcell.Screen, buf *buf2d.Buffer) { - buf.Draw(func(x, y int, cn rune) { - scr.SetContent(x, y, cn, nil, tcell.StyleDefault) +func drawBuffer(scr tcell.Screen, buf *ViewBuffer) { + buf.ForEach(func(x, y int, rn Rune) { + scr.SetContent(x, y, rn.Rn, nil, rn.Style) }) scr.Show() } diff --git a/events.go b/events.go index 38588f3..fea5151 100644 --- a/events.go +++ b/events.go @@ -1,7 +1,5 @@ package tui -import "github.com/gdamore/tcell" - type Events interface { - OnKeyPressed(key tcell.Key) + OnKeyPressed(event *KeyEvent) (consumed bool) } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a5519c9 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module git.tordarus.net/Tordarus/tui + +go 1.18 + +require ( + git.tordarus.net/Tordarus/buf2d v1.1.0 + github.com/gdamore/tcell v1.4.0 +) + +require ( + github.com/gdamore/encoding v1.0.0 // indirect + github.com/lucasb-eyer/go-colorful v1.0.3 // indirect + github.com/mattn/go-runewidth v0.0.7 // indirect + golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 // indirect + golang.org/x/text v0.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..01c1580 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +git.tordarus.net/Tordarus/buf2d v1.1.0 h1:rIZjD7yeX5XK2D1h75ET5Og0u/NQF3eVonnC5aaqVkQ= +git.tordarus.net/Tordarus/buf2d v1.1.0/go.mod h1:XXPpS8nQK0gUI0ki7ftV/qlprsGCRWFVSD4ybvDuUL8= +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 v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= +github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= +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.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/rune.go b/rune.go new file mode 100644 index 0000000..f842058 --- /dev/null +++ b/rune.go @@ -0,0 +1,60 @@ +package tui + +import "regexp" + +type Rune struct { + Rn rune + Style Style +} + +var DefaultRune = Rune{' ', StyleDefault} + +// Text represents a string with rune-based styling +type Text struct { + str string + style []Style +} + +// Txt returns a Text containing the given str +// using the default style +func Txt(str string) *Text { + styles := make([]Style, 0, len(str)) + for range str { + styles = append(styles, StyleDefault) + } + return &Text{str: str, style: styles} +} + +// Len returns the amount of runes in t +func (t *Text) Len() int { + return len(t.style) +} + +// AppendString appends str with default styling to t +func (t *Text) AppendString(str string) { + t.AppendText(Txt(str)) +} + +// PrependString prepends str with default styling to t +func (t *Text) PrependString(str string) { + newTxt := Txt(str) + newTxt.AppendText(t) + *t = *newTxt +} + +// AppendText appends txt to t +func (t *Text) AppendText(txt *Text) { + t.str += txt.str + t.style = append(t.style, txt.style...) +} + +// Style applies the given style to the part of t between startIndex (inclusive) and endIndex (exclusive) +func (t *Text) Style(style Style, startIndex, endIndex int) { + for i := startIndex; i < endIndex; i++ { + t.style[i] = style + } +} + +func (t *Text) StyleRegex(style Style, pattern *regexp.Regexp) { + // TODO +} diff --git a/screen.go b/screen.go index d4f68a2..b0a5e67 100644 --- a/screen.go +++ b/screen.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - "git.tordarus.net/tordarus/buf2d" + "git.tordarus.net/Tordarus/buf2d" "github.com/gdamore/tcell" ) @@ -38,7 +38,7 @@ func (s *Screen) eventloop() { go s.Redraw() case *tcell.EventKey: go func() { - s.Root.OnKeyPressed(event.Key()) + s.Root.OnKeyPressed(event) s.Redraw() }() default: @@ -53,6 +53,8 @@ func (s *Screen) Start() error { if err != nil { return err } + defer s.scr.Fini() + s.Redraw() return <-s.stopCh } @@ -67,7 +69,7 @@ func (s *Screen) StopWithError(err error) { func (s *Screen) Redraw() { w, h := s.scr.Size() - buf := buf2d.NewBuffer(w, h) + buf := buf2d.NewBuffer(w, h, DefaultRune) s.Root.Draw(buf) drawBuffer(s.scr, buf) } diff --git a/tests/screen_test.go b/tests/screen_test.go index aa1b092..5c0bd0b 100644 --- a/tests/screen_test.go +++ b/tests/screen_test.go @@ -4,22 +4,28 @@ import ( "errors" "fmt" "testing" - "tui" - "tui/views" + "git.tordarus.net/Tordarus/tui" + "git.tordarus.net/Tordarus/tui/views" "github.com/gdamore/tcell" ) func TestScreen(t *testing.T) { - eventView := views.NewEventView() + textView := views.NewTextView("hello world") + eventView := views.NewEventView(textView) screen, err := tui.NewScreen(eventView) if err != nil { t.Error(err) return } - eventView.KeyPressed = func(key tcell.Key) { - screen.StopWithError(errors.New(fmt.Sprintf("%#v", key))) + eventView.KeyPressed = func(event *tui.KeyEvent) (consumed bool) { + if event.Key() == tcell.KeyCtrlC { + screen.StopWithError(errors.New(fmt.Sprintf("key: %#v | rune: %s", event.Key(), string(event.Rune())))) + } + + //textView.Text = event.When().String() + return true } err = screen.Start() diff --git a/types.go b/types.go new file mode 100644 index 0000000..cf30060 --- /dev/null +++ b/types.go @@ -0,0 +1,13 @@ +package tui + +import ( + "git.tordarus.net/Tordarus/buf2d" + "github.com/gdamore/tcell" +) + +type ViewBuffer = buf2d.Buffer[Rune] +type KeyEvent = tcell.EventKey +type Style = tcell.Style +type Color = tcell.Color + +var StyleDefault Style = tcell.StyleDefault diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..9d2d87f --- /dev/null +++ b/utils.go @@ -0,0 +1,31 @@ +package tui + +import ( + "strings" +) + +// WriteString writes a whole string to the buffer at position (x,y) +// no word wrap is applied at all. If the string does not fit, it will be truncated +func WriteString(b *ViewBuffer, str string, style Style, x, y int) { + dx := x + for _, r := range str { + if dx >= b.Width() { + return + } + b.Set(dx, y, Rune{r, style}) + dx++ + } +} + +// WriteMultiLineString writes a multi-line string to the buffer at position (x,y) +// no word wrap is applied at all. If a line does not fit horizontally, it will be truncated +// All lines which do not fit vertically will be truncated as well +func WriteMultiLineString(b *ViewBuffer, str string, style Style, x, y int) { + lines := strings.Split(str, "\n") + for dy, line := range lines { + if dy >= b.Height() { + return + } + WriteString(b, line, style, x, y+dy) + } +} diff --git a/view.go b/view.go index fb14a68..7912478 100644 --- a/view.go +++ b/view.go @@ -1,18 +1,40 @@ package tui -import ( - "git.tordarus.net/tordarus/buf2d" - "github.com/gdamore/tcell" -) - +// View defines the behavior of any element displayable on screen +// To define custom Views, it is recommended to add ViewTmpl +// as the promoted anonymous field for your custom View struct. +// It implements the View interface with useful default behavior type View interface { Events - SetForeground(color tcell.Color) - Foreground() tcell.Color + SetForeground(color Color) + Foreground() Color - SetBackground(color tcell.Color) - Background() tcell.Color + SetBackground(color Color) + Background() Color - Draw(*buf2d.Buffer) + Style() Style + + Draw(*ViewBuffer) +} + +// Group defines the behavior of a View which can hold multiple sub views +// To define custom Groups, it is recommended to add GroupTmpl +// as the promoted anonymous field for your custom Wrapper struct. +// It implements the Group interface with useful default behavior +type Group interface { + View + + Children() []View +} + +// Wrapper defines the behavior of a GroupView which can hold exactly one sub view +// To define custom Wrappers, it is recommended to add WrapperTmpl +// as the promoted anonymous field for your custom Wrapper struct. +// It implements the Wrapper interface with useful default behavior +type Wrapper interface { + Group + + SetView(View) + View() View } diff --git a/viewgroup.go b/viewgroup.go index 6637a12..b37474c 100644 --- a/viewgroup.go +++ b/viewgroup.go @@ -1,6 +1,3 @@ package tui -type ViewGroup interface { - View - Children() []*View -} +// TODO GroupTmpl diff --git a/views/eventview.go b/views/eventview.go index aa95bad..5f5d3a9 100644 --- a/views/eventview.go +++ b/views/eventview.go @@ -1,22 +1,27 @@ package views import ( - "tui" - - "github.com/gdamore/tcell" + "git.tordarus.net/Tordarus/tui" ) type EventView struct { - tui.ViewTmpl - KeyPressed func(key tcell.Key) + tui.WrapperTmpl + + View tui.View + KeyPressed func(event *tui.KeyEvent) (consumed bool) } -func (v *EventView) OnKeyPressed(key tcell.Key) { +func NewEventView(view tui.View) *EventView { + return &EventView{View: view} +} + +func (v *EventView) OnKeyPressed(event *tui.KeyEvent) (consumed bool) { if v.KeyPressed != nil { - v.KeyPressed(key) + return v.KeyPressed(event) } + return v.ViewTmpl.OnKeyPressed(event) } -func NewEventView() *EventView { - return &EventView{} +func (v *EventView) Draw(buf *tui.ViewBuffer) { + v.View.Draw(buf) } diff --git a/views/textview.go b/views/textview.go index 2af50f4..1b16b14 100644 --- a/views/textview.go +++ b/views/textview.go @@ -1,9 +1,7 @@ package views import ( - "tui" - - "git.tordarus.net/tordarus/buf2d" + "git.tordarus.net/Tordarus/tui" ) type TextView struct { @@ -13,8 +11,8 @@ type TextView struct { var _ tui.View = &TextView{} -func (v *TextView) Draw(buf *buf2d.Buffer) { - buf.WriteMultiLineString(v.Text, 0, 0) +func (v *TextView) Draw(buf *tui.ViewBuffer) { + tui.WriteMultiLineString(buf, v.Text, v.Style(), 0, 0) } func NewTextView(text string) *TextView { diff --git a/viewtmpl.go b/viewtmpl.go index 878b835..fdfc120 100644 --- a/viewtmpl.go +++ b/viewtmpl.go @@ -1,45 +1,44 @@ package tui -import ( - "git.tordarus.net/tordarus/buf2d" - "github.com/gdamore/tcell" -) +import "github.com/gdamore/tcell" type ViewTmpl struct { - view View - - foreground tcell.Color - background tcell.Color + foreground *Color + background *Color } var _ View = &ViewTmpl{} -func NewViewTmpl(v View) *ViewTmpl { - return &ViewTmpl{ - view: v, +func (v *ViewTmpl) Draw(buf *ViewBuffer) { + buf.Fill(DefaultRune) +} + +func (v *ViewTmpl) OnKeyPressed(event *KeyEvent) (consumed bool) { + return false +} + +func (v *ViewTmpl) Style() Style { + return StyleDefault.Background(v.Background()).Foreground(v.Foreground()) +} + +func (v *ViewTmpl) Foreground() Color { + if v.foreground == nil { + return tcell.ColorDefault } + return *v.foreground } -func (v *ViewTmpl) Draw(buf *buf2d.Buffer) { - buf.Fill(' ') +func (v *ViewTmpl) SetForeground(color Color) { + v.foreground = &color } -func (v *ViewTmpl) OnKeyPressed(key tcell.Key) { - +func (v *ViewTmpl) Background() Color { + if v.background == nil { + return tcell.ColorDefault + } + return *v.background } -func (v *ViewTmpl) Foreground() tcell.Color { - return v.foreground -} - -func (v *ViewTmpl) SetForeground(color tcell.Color) { - v.foreground = color -} - -func (v *ViewTmpl) Background() tcell.Color { - return v.background -} - -func (v *ViewTmpl) SetBackground(color tcell.Color) { - v.background = color +func (v *ViewTmpl) SetBackground(color Color) { + v.background = &color } diff --git a/wrappertmpl.go b/wrappertmpl.go new file mode 100644 index 0000000..3786b1f --- /dev/null +++ b/wrappertmpl.go @@ -0,0 +1,74 @@ +package tui + +import "github.com/gdamore/tcell" + +type WrapperTmpl struct { + ViewTmpl + view View +} + +var _ Wrapper = &WrapperTmpl{} + +func (v *WrapperTmpl) Draw(buf *ViewBuffer) { + if v.view != nil { + v.view.Draw(buf) + } else { + v.ViewTmpl.Draw(buf) + } +} + +func (v *WrapperTmpl) OnKeyPressed(event *KeyEvent) (consumed bool) { + if v.view != nil { + return v.view.OnKeyPressed(event) + } + return v.ViewTmpl.OnKeyPressed(event) +} + +func (v *WrapperTmpl) Style() Style { + if v.view != nil { + return v.view.Style() + } + return v.ViewTmpl.Style() +} + +func (v *WrapperTmpl) Foreground() Color { + if v.view != nil { + return v.view.Foreground() + } + return v.ViewTmpl.Foreground() +} + +func (v *WrapperTmpl) SetForeground(color Color) { + if v.view != nil { + v.view.SetForeground(color) + } else { + v.ViewTmpl.SetForeground(color) + } +} + +func (v *WrapperTmpl) Background() Color { + if v.background == nil { + return tcell.ColorDefault + } + return *v.background +} + +func (v *WrapperTmpl) SetBackground(color Color) { + if v.view != nil { + v.view.SetBackground(color) + } else { + v.ViewTmpl.SetBackground(color) + } +} + +func (v *WrapperTmpl) Children() []View { + return []View{v.view} +} + +func (v *WrapperTmpl) View() View { + return v.view +} + +func (v *WrapperTmpl) SetView(view View) { + v.view = view +}