summaryrefslogtreecommitdiffhomepage
path: root/internal/tui/layout
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tui/layout')
-rw-r--r--internal/tui/layout/bento.go20
-rw-r--r--internal/tui/layout/border.go7
-rw-r--r--internal/tui/layout/overlay.go205
-rw-r--r--internal/tui/layout/single.go23
4 files changed, 243 insertions, 12 deletions
diff --git a/internal/tui/layout/bento.go b/internal/tui/layout/bento.go
index 8d1f1d10f..5225db192 100644
--- a/internal/tui/layout/bento.go
+++ b/internal/tui/layout/bento.go
@@ -73,7 +73,14 @@ func (b *bentoLayout) GetSize() (int, int) {
}
func (b *bentoLayout) Init() tea.Cmd {
- return nil
+ var cmds []tea.Cmd
+ for _, pane := range b.panes {
+ cmd := pane.Init()
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ }
+ return tea.Batch(cmds...)
}
func (b *bentoLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -98,12 +105,15 @@ func (b *bentoLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
- if pane, ok := b.panes[b.currentPane]; ok {
+ var cmds []tea.Cmd
+ for id, pane := range b.panes {
u, cmd := pane.Update(msg)
- b.panes[b.currentPane] = u.(SinglePaneLayout)
- return b, cmd
+ b.panes[id] = u.(SinglePaneLayout)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
}
- return b, nil
+ return b, tea.Batch(cmds...)
}
func (b *bentoLayout) View() string {
diff --git a/internal/tui/layout/border.go b/internal/tui/layout/border.go
index d1b334b32..a3c80396f 100644
--- a/internal/tui/layout/border.go
+++ b/internal/tui/layout/border.go
@@ -24,17 +24,20 @@ var (
InactivePreviewBorder = styles.Grey
)
-func Borderize(content string, active bool, embeddedText map[BorderPosition]string) string {
+func Borderize(content string, active bool, embeddedText map[BorderPosition]string, activeColor lipgloss.TerminalColor) string {
if embeddedText == nil {
embeddedText = make(map[BorderPosition]string)
}
+ if activeColor == nil {
+ activeColor = ActiveBorder
+ }
var (
thickness = map[bool]lipgloss.Border{
true: lipgloss.Border(lipgloss.ThickBorder()),
false: lipgloss.Border(lipgloss.NormalBorder()),
}
color = map[bool]lipgloss.TerminalColor{
- true: ActiveBorder,
+ true: activeColor,
false: InactivePreviewBorder,
}
border = thickness[active]
diff --git a/internal/tui/layout/overlay.go b/internal/tui/layout/overlay.go
new file mode 100644
index 000000000..87cc80e68
--- /dev/null
+++ b/internal/tui/layout/overlay.go
@@ -0,0 +1,205 @@
+package layout
+
+import (
+ "bytes"
+ "strings"
+
+ "github.com/charmbracelet/lipgloss"
+ "github.com/kujtimiihoxha/termai/internal/tui/util"
+ "github.com/mattn/go-runewidth"
+ "github.com/muesli/ansi"
+ "github.com/muesli/reflow/truncate"
+ "github.com/muesli/termenv"
+)
+
+// Most of this code is borrowed from
+// https://github.com/charmbracelet/lipgloss/pull/102
+// as well as the lipgloss library, with some modification for what I needed.
+
+// Split a string into lines, additionally returning the size of the widest
+// line.
+func getLines(s string) (lines []string, widest int) {
+ lines = strings.Split(s, "\n")
+
+ for _, l := range lines {
+ w := ansi.PrintableRuneWidth(l)
+ if widest < w {
+ widest = w
+ }
+ }
+
+ return lines, widest
+}
+
+// PlaceOverlay places fg on top of bg.
+func PlaceOverlay(
+ x, y int,
+ fg, bg string,
+ shadow bool, opts ...WhitespaceOption,
+) string {
+ fgLines, fgWidth := getLines(fg)
+ bgLines, bgWidth := getLines(bg)
+ bgHeight := len(bgLines)
+ fgHeight := len(fgLines)
+
+ if shadow {
+ var shadowbg string = ""
+ shadowchar := lipgloss.NewStyle().
+ Foreground(lipgloss.Color("#333333")).
+ Render("░")
+ for i := 0; i <= fgHeight; i++ {
+ if i == 0 {
+ shadowbg += " " + strings.Repeat(" ", fgWidth) + "\n"
+ } else {
+ shadowbg += " " + strings.Repeat(shadowchar, fgWidth) + "\n"
+ }
+ }
+
+ fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...)
+ fgLines, fgWidth = getLines(fg)
+ fgHeight = len(fgLines)
+ }
+
+ if fgWidth >= bgWidth && fgHeight >= bgHeight {
+ // FIXME: return fg or bg?
+ return fg
+ }
+ // TODO: allow placement outside of the bg box?
+ x = util.Clamp(x, 0, bgWidth-fgWidth)
+ y = util.Clamp(y, 0, bgHeight-fgHeight)
+
+ ws := &whitespace{}
+ for _, opt := range opts {
+ opt(ws)
+ }
+
+ var b strings.Builder
+ for i, bgLine := range bgLines {
+ if i > 0 {
+ b.WriteByte('\n')
+ }
+ if i < y || i >= y+fgHeight {
+ b.WriteString(bgLine)
+ continue
+ }
+
+ pos := 0
+ if x > 0 {
+ left := truncate.String(bgLine, uint(x))
+ pos = ansi.PrintableRuneWidth(left)
+ b.WriteString(left)
+ if pos < x {
+ b.WriteString(ws.render(x - pos))
+ pos = x
+ }
+ }
+
+ fgLine := fgLines[i-y]
+ b.WriteString(fgLine)
+ pos += ansi.PrintableRuneWidth(fgLine)
+
+ right := cutLeft(bgLine, pos)
+ bgWidth := ansi.PrintableRuneWidth(bgLine)
+ rightWidth := ansi.PrintableRuneWidth(right)
+ if rightWidth <= bgWidth-pos {
+ b.WriteString(ws.render(bgWidth - rightWidth - pos))
+ }
+
+ b.WriteString(right)
+ }
+
+ return b.String()
+}
+
+// cutLeft cuts printable characters from the left.
+// This function is heavily based on muesli's ansi and truncate packages.
+func cutLeft(s string, cutWidth int) string {
+ var (
+ pos int
+ isAnsi bool
+ ab bytes.Buffer
+ b bytes.Buffer
+ )
+ for _, c := range s {
+ var w int
+ if c == ansi.Marker || isAnsi {
+ isAnsi = true
+ ab.WriteRune(c)
+ if ansi.IsTerminator(c) {
+ isAnsi = false
+ if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) {
+ ab.Reset()
+ }
+ }
+ } else {
+ w = runewidth.RuneWidth(c)
+ }
+
+ if pos >= cutWidth {
+ if b.Len() == 0 {
+ if ab.Len() > 0 {
+ b.Write(ab.Bytes())
+ }
+ if pos-cutWidth > 1 {
+ b.WriteByte(' ')
+ continue
+ }
+ }
+ b.WriteRune(c)
+ }
+ pos += w
+ }
+ return b.String()
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+
+type whitespace struct {
+ style termenv.Style
+ chars string
+}
+
+// Render whitespaces.
+func (w whitespace) render(width int) string {
+ if w.chars == "" {
+ w.chars = " "
+ }
+
+ r := []rune(w.chars)
+ j := 0
+ b := strings.Builder{}
+
+ // Cycle through runes and print them into the whitespace.
+ for i := 0; i < width; {
+ b.WriteRune(r[j])
+ j++
+ if j >= len(r) {
+ j = 0
+ }
+ i += ansi.PrintableRuneWidth(string(r[j]))
+ }
+
+ // Fill any extra gaps white spaces. This might be necessary if any runes
+ // are more than one cell wide, which could leave a one-rune gap.
+ short := width - ansi.PrintableRuneWidth(b.String())
+ if short > 0 {
+ b.WriteString(strings.Repeat(" ", short))
+ }
+
+ return w.style.Styled(b.String())
+}
+
+// WhitespaceOption sets a styling rule for rendering whitespace.
+type WhitespaceOption func(*whitespace)
diff --git a/internal/tui/layout/single.go b/internal/tui/layout/single.go
index 95513cc62..1e4d0881c 100644
--- a/internal/tui/layout/single.go
+++ b/internal/tui/layout/single.go
@@ -11,6 +11,7 @@ type SinglePaneLayout interface {
Focusable
Sizeable
Bindings
+ Pane() tea.Model
}
type singlePaneLayout struct {
@@ -26,6 +27,8 @@ type singlePaneLayout struct {
content tea.Model
padding []int
+
+ activeColor lipgloss.TerminalColor
}
type SinglePaneOption func(*singlePaneLayout)
@@ -48,7 +51,7 @@ func (s *singlePaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (s *singlePaneLayout) View() string {
style := lipgloss.NewStyle().Width(s.width).Height(s.height)
if s.bordered {
- style = style.Width(s.width).Height(s.height)
+ style = style.Width(s.width - 2).Height(s.height - 2)
}
if s.padding != nil {
style = style.Padding(s.padding...)
@@ -61,7 +64,7 @@ func (s *singlePaneLayout) View() string {
if bordered, ok := s.content.(Bordered); ok {
s.borderText = bordered.BorderText()
}
- return Borderize(content, s.focused, s.borderText)
+ return Borderize(content, s.focused, s.borderText, s.activeColor)
}
return content
}
@@ -89,11 +92,11 @@ func (s *singlePaneLayout) Focus() tea.Cmd {
func (s *singlePaneLayout) SetSize(width, height int) {
s.width = width
s.height = height
+ childWidth, childHeight := s.width, s.height
if s.bordered {
- s.width -= 2
- s.height -= 2
+ childWidth -= 2
+ childHeight -= 2
}
- childWidth, childHeight := s.width, s.height
if s.padding != nil {
if len(s.padding) == 1 {
childWidth -= s.padding[0] * 2
@@ -131,6 +134,10 @@ func (s *singlePaneLayout) BindingKeys() []key.Binding {
return []key.Binding{}
}
+func (s *singlePaneLayout) Pane() tea.Model {
+ return s.content
+}
+
func NewSinglePane(content tea.Model, opts ...SinglePaneOption) SinglePaneLayout {
layout := &singlePaneLayout{
content: content,
@@ -171,3 +178,9 @@ func WithSinglePanePadding(padding ...int) SinglePaneOption {
opts.padding = padding
}
}
+
+func WithSinglePaneActiveColor(color lipgloss.TerminalColor) SinglePaneOption {
+ return func(opts *singlePaneLayout) {
+ opts.activeColor = color
+ }
+}