mirror of
https://github.com/anotherhadi/ilovetui.git
synced 2026-08-21 03:55:48 +02:00
aa6496901f
Signed-off-by: Hadi <hadi@example.fr>
156 lines
4.7 KiB
Go
156 lines
4.7 KiB
Go
package drawer
|
|
|
|
import (
|
|
"image/color"
|
|
"strings"
|
|
|
|
"charm.land/lipgloss/v2"
|
|
"github.com/charmbracelet/x/ansi"
|
|
|
|
"github.com/anotherhadi/ilovetui/style"
|
|
)
|
|
|
|
// Render draws every open drawer (see Model.Update/Show) on top of
|
|
// background (already rendered, e.g. layout.Model.View() or any other
|
|
// component's View()) and returns the result. Each drawer in the stack
|
|
// first flattens whatever came before it - background plus any earlier
|
|
// drawer - to a single flat DimColor (see dim), then draws its own
|
|
// full-height box flush against its Side on top, so nesting a second
|
|
// drawer on top of a first dims the first one too. background is returned
|
|
// unchanged whenever there's nothing to draw.
|
|
func (m Model) Render(background string) string {
|
|
if len(m.drawers) == 0 {
|
|
return background
|
|
}
|
|
|
|
result := background
|
|
for _, d := range m.drawers {
|
|
w, h := lipgloss.Width(result), lipgloss.Height(result)
|
|
if w <= 0 || h <= 0 {
|
|
return result
|
|
}
|
|
result = m.renderOne(d, result, w, h)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// View is a convenience for a pane whose sole purpose is showing drawers
|
|
// (e.g. a dedicated layout.Leaf): it draws the stack over a blank
|
|
// width x height area instead of an existing background.
|
|
func (m Model) View(width, height int) string {
|
|
return m.Render(blank(width, height))
|
|
}
|
|
|
|
// renderOne dims background flat and draws d's box, spanning its full
|
|
// height, flush against d.Side.
|
|
func (m Model) renderOne(d Drawer, background string, w, h int) string {
|
|
s := m.styles
|
|
if d.Style != nil {
|
|
s = *d.Style
|
|
}
|
|
|
|
box := m.renderBox(d, s, w, h)
|
|
bw := lipgloss.Width(box)
|
|
x := 0
|
|
if d.Side == Right {
|
|
x = max(w-bw, 0)
|
|
}
|
|
|
|
compositor := lipgloss.NewCompositor(
|
|
lipgloss.NewLayer(dim(background, s.DimColor)),
|
|
lipgloss.NewLayer(box).X(x).Y(0).Z(1),
|
|
)
|
|
return compositor.Render()
|
|
}
|
|
|
|
// dim flattens s to a single flat color: every existing style (colors,
|
|
// bold, underline...) is stripped, then every character - including
|
|
// whitespace, so highlighted/selected backgrounds vanish too - is
|
|
// repainted in c. Applying a Foreground style to a multi-line string styles
|
|
// each line independently (see lipgloss.Style.Render), so this keeps s's
|
|
// line structure intact.
|
|
func dim(s string, c color.Color) string {
|
|
return lipgloss.NewStyle().Foreground(c).Render(ansi.Strip(s))
|
|
}
|
|
|
|
// renderBox draws d as a bordered, title-embedded box (style.RenderWithTitle)
|
|
// spanning the background's full height, capped by the Model's configured
|
|
// max width and by whatever actually fits inside a bgW-wide background.
|
|
func (m Model) renderBox(d Drawer, s Styles, bgW, bgH int) string {
|
|
widthCap := m.maxWidth
|
|
if d.Width > 0 {
|
|
widthCap = d.Width
|
|
}
|
|
maxW := effectiveMax(widthCap, bgW)
|
|
|
|
body := contentView(d)
|
|
inner := contentWidth(d, body, maxW)
|
|
content := s.Content.Width(inner).Render(body)
|
|
|
|
boxWidth := inner + 4 // border (2) + Padding(0, 1) (2)
|
|
|
|
return style.RenderWithTitle(s.Border, s.Title.Render(d.Title), content, boxWidth, bgH)
|
|
}
|
|
|
|
// contentView is the drawer body's rendered string, or "" for a drawer
|
|
// without content. The box shrinks to fit whatever the content model draws,
|
|
// so a content that wants a specific size sets it on itself - the drawer only
|
|
// ever sees the result.
|
|
func contentView(d Drawer) string {
|
|
if d.Content == nil {
|
|
return ""
|
|
}
|
|
return d.Content.View().Content
|
|
}
|
|
|
|
// contentWidth is the drawer's inner (border/padding excluded) width, given
|
|
// the resolved total-width cap maxWidth (see renderBox): maxWidth-4 if
|
|
// d.Width is set (a fixed total width), otherwise its natural size (long
|
|
// enough for the widest line of title/content), capped at maxWidth-4.
|
|
func contentWidth(d Drawer, body string, maxWidth int) int {
|
|
capped := max(maxWidth-4, 1)
|
|
if d.Width > 0 {
|
|
return capped
|
|
}
|
|
natural := max(naturalWidth(body), lipgloss.Width(d.Title), 1)
|
|
return min(natural, capped)
|
|
}
|
|
|
|
// naturalWidth is the width of content's widest line.
|
|
func naturalWidth(content string) int {
|
|
w := 0
|
|
for _, line := range strings.Split(content, "\n") {
|
|
if lw := lipgloss.Width(line); lw > w {
|
|
w = lw
|
|
}
|
|
}
|
|
return w
|
|
}
|
|
|
|
// effectiveMax resolves the cap actually used along the width axis:
|
|
// configured (0 = unlimited) narrowed down to fits, whatever actually fits
|
|
// the background - a drawer can never overflow past the edge of the
|
|
// background, or the terminal, when the background is a full-screen View(),
|
|
// regardless of how WithMaxWidth/WithWidth was set.
|
|
func effectiveMax(configured, fits int) int {
|
|
if fits < 1 {
|
|
fits = 1
|
|
}
|
|
if configured > 0 && configured < fits {
|
|
return configured
|
|
}
|
|
return fits
|
|
}
|
|
|
|
func blank(width, height int) string {
|
|
if width <= 0 || height <= 0 {
|
|
return ""
|
|
}
|
|
line := strings.Repeat(" ", width)
|
|
lines := make([]string, height)
|
|
for i := range lines {
|
|
lines[i] = line
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|