Split into category packages; module github.com/ZacharyZhang-NY/MujicaUI
This commit is contained in:
1822 files changed
+24568
-21314
No files matched your search
@@ -0,0 +1,309 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"math"
|
||||
"runtime"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/core"
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/internal"
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/theme"
|
||||
)
|
||||
|
||||
// TerminalOptions configure a Terminal.
|
||||
type TerminalOptions struct {
|
||||
// Input receives what the user types, encoded for the PTY.
|
||||
Input func(data string)
|
||||
// Resized reports a new grid size for the PTY.
|
||||
Resized func(cols, rows int)
|
||||
// Scrollback caps lines kept above; zero keeps 1000.
|
||||
Scrollback int
|
||||
Label string
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
// terminalPad is the space around the grid in DIPs.
|
||||
const terminalPad float32 = 6
|
||||
|
||||
// terminalCtrl maps letter keys to their Ctrl codes.
|
||||
var terminalCtrl = map[ui.Key]byte{ui.KeyA: 1, ui.KeyB: 2, ui.KeyC: 3, ui.KeyD: 4, ui.KeyE: 5, ui.KeyF: 6, ui.KeyG: 7,
|
||||
ui.KeyH: 8, ui.KeyI: 9, ui.KeyJ: 10, ui.KeyK: 11, ui.KeyL: 12, ui.KeyM: 13, ui.KeyN: 14, ui.KeyO: 15, ui.KeyP: 16,
|
||||
ui.KeyQ: 17, ui.KeyR: 18, ui.KeyS: 19, ui.KeyT: 20, ui.KeyU: 21, ui.KeyV: 22, ui.KeyW: 23, ui.KeyX: 24, ui.KeyY: 25, ui.KeyZ: 26}
|
||||
|
||||
// terminalKeys are the sequences of keys that type no text.
|
||||
var terminalKeys = map[ui.Key]string{ui.KeyEnter: "\r", ui.KeyBackspace: "\x7f", ui.KeyTab: "\t", ui.KeyEscape: "\x1b",
|
||||
ui.KeyUp: "\x1b[A", ui.KeyDown: "\x1b[B", ui.KeyRight: "\x1b[C", ui.KeyLeft: "\x1b[D", ui.KeyHome: "\x1b[H", ui.KeyEnd: "\x1b[F",
|
||||
ui.KeyDelete: "\x1b[3~", ui.KeyPageUp: "\x1b[5~", ui.KeyPageDown: "\x1b[6~"}
|
||||
|
||||
// terminalCell size and baseline for the code font.
|
||||
func terminalMetrics(f ui.Font) (cw, ch, base float32) {
|
||||
m := f.Metrics()
|
||||
g := ui.Shape("M", f)
|
||||
cw = f.Size * 0.6
|
||||
if len(g) > 0 {
|
||||
cw = g[0].Advance
|
||||
}
|
||||
ch = float32(math.Ceil(float64((m.Ascent + m.Descent + m.LineGap) * 1.15)))
|
||||
return cw, ch, (ch-m.Ascent-m.Descent)/2 + m.Ascent
|
||||
}
|
||||
|
||||
// Terminal draws a TerminalState and sends input to the PTY.
|
||||
func Terminal(c *ui.Context, s *TerminalState, opts TerminalOptions) *ui.Element {
|
||||
if s == nil {
|
||||
panic("mujica: Terminal needs a TerminalState")
|
||||
}
|
||||
if opts.Scrollback < 0 {
|
||||
panic("mujica: Terminal Scrollback is negative")
|
||||
}
|
||||
s.init()
|
||||
if opts.Scrollback > 0 {
|
||||
s.limit = opts.Scrollback
|
||||
}
|
||||
k := core.Tokens(c)
|
||||
font := codeFont(c)
|
||||
cw, ch, base := terminalMetrics(font)
|
||||
label := opts.Label
|
||||
if label == "" {
|
||||
label = core.Msg(c, "terminal.label")
|
||||
}
|
||||
e := ui.Box(c).Focusable().Cursor(ui.CursorText).Clip().Label(label).Background(k.Surface).
|
||||
Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius).Disabled(opts.Disabled).MinHeight(ch * 4)
|
||||
b := e.Bounds()
|
||||
if b.W > 0 {
|
||||
cols := max(int((b.W-2*terminalPad)/cw), 1)
|
||||
rows := max(int((b.H-2*terminalPad)/ch), 1)
|
||||
if cols != s.cols || rows != s.rows {
|
||||
s.Resize(cols, rows)
|
||||
slog.Debug("mujica: terminal resize", "cols", cols, "rows", rows)
|
||||
if opts.Resized != nil {
|
||||
opts.Resized(cols, rows)
|
||||
}
|
||||
}
|
||||
}
|
||||
send := func(data string) {
|
||||
s.offset, s.selOn, s.preedit = 0, false, ""
|
||||
if opts.Input != nil {
|
||||
opts.Input(data)
|
||||
}
|
||||
}
|
||||
cell := func(x, y float32) TerminalPos {
|
||||
col := max(0, min(int((x-terminalPad)/cw), s.cols-1))
|
||||
row := max(0, min(int((y-terminalPad)/ch), s.rows-1))
|
||||
return TerminalPos{Line: len(s.back) - s.offset + row, Col: col}
|
||||
}
|
||||
anchor := ui.Local(e, "terminal.anchor", func() TerminalPos { return TerminalPos{} })
|
||||
e.HandleInput(func(ev ui.InputEvent) bool {
|
||||
if e.IsDisabled() {
|
||||
return false
|
||||
}
|
||||
switch ev.Kind {
|
||||
case ui.InputKeyDown:
|
||||
return terminalKey(c, s, ev, send)
|
||||
case ui.InputText:
|
||||
send(ev.Text)
|
||||
return true
|
||||
case ui.InputCompose:
|
||||
s.preedit = ev.Text
|
||||
return true
|
||||
case ui.InputScroll:
|
||||
s.scrolled -= ev.DY / ch
|
||||
n := int(s.scrolled)
|
||||
s.scrolled -= float32(n)
|
||||
s.ScrollBack(n)
|
||||
return true
|
||||
case ui.InputPointerDown:
|
||||
if ev.Button != 0 {
|
||||
return false
|
||||
}
|
||||
e.Focus()
|
||||
*anchor = cell(ev.X, ev.Y)
|
||||
s.selOn = false
|
||||
return true
|
||||
case ui.InputPointerMove:
|
||||
if e.Pressed() {
|
||||
s.Select(*anchor, cell(ev.X, ev.Y))
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
cy := float32(s.cy+s.offset) * ch
|
||||
e.TextCaret(ui.Rect{X: terminalPad + float32(s.cx)*cw, Y: terminalPad + cy, W: cw, H: ch})
|
||||
focused := e.Focused()
|
||||
internal.Ring(e, k.Focus, theme.ControlRadius)
|
||||
e.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
if r.W != b.W || r.H != b.H {
|
||||
// Laid out at another size: rebuild to fit the grid.
|
||||
c.Invalidate()
|
||||
}
|
||||
terminalPaint(p, r, s, k, font, cw, ch, base, focused)
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// terminalKey encodes a key, handling copy, paste and scrollback.
|
||||
func terminalKey(c *ui.Context, s *TerminalState, ev ui.InputEvent, send func(string)) bool {
|
||||
clip := ui.Super
|
||||
if runtime.GOOS != "darwin" {
|
||||
clip = ui.Ctrl | ui.Shift
|
||||
}
|
||||
switch {
|
||||
case ev.Mods == clip && ev.Key == ui.KeyC:
|
||||
if t := s.Selection(); t != "" {
|
||||
c.WriteClipboard(t)
|
||||
slog.Debug("mujica: terminal copy", "runes", len([]rune(t)))
|
||||
}
|
||||
return true
|
||||
case ev.Mods == clip && ev.Key == ui.KeyV:
|
||||
if t := c.ReadClipboard(); t != "" {
|
||||
send(t)
|
||||
}
|
||||
return true
|
||||
case ev.Mods == ui.Shift && (ev.Key == ui.KeyPageUp || ev.Key == ui.KeyPageDown):
|
||||
n := s.rows - 1
|
||||
if ev.Key == ui.KeyPageDown {
|
||||
n = -n
|
||||
}
|
||||
s.ScrollBack(n)
|
||||
return true
|
||||
case ev.Mods == ui.Ctrl && terminalCtrl[ev.Key] != 0:
|
||||
send(string([]byte{terminalCtrl[ev.Key]}))
|
||||
return true
|
||||
}
|
||||
if seq, ok := terminalKeys[ev.Key]; ok && ev.Mods&^ui.Shift == 0 {
|
||||
if ev.Key == ui.KeyTab && ev.Mods == ui.Shift {
|
||||
seq = "\x1b[Z"
|
||||
}
|
||||
send(seq)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// terminalPaint draws the visible rows, marks and cursor.
|
||||
func terminalPaint(p *ui.Painter, r ui.Rect, s *TerminalState, k theme.Tokens, font ui.Font, cw, ch, base float32, focused bool) {
|
||||
ox, oy := r.X+terminalPad, r.Y+terminalPad
|
||||
top := len(s.back) - s.offset
|
||||
for row := range s.rows {
|
||||
li := top + row
|
||||
cells := s.line(li)
|
||||
y := oy + float32(row)*ch
|
||||
for col := 0; col < len(cells); {
|
||||
st := cells[col].st
|
||||
end := col + 1
|
||||
for end < len(cells) && cells[end].st == st {
|
||||
end++
|
||||
}
|
||||
fg, bg := codeAnsiColor(k, st.FG), codeAnsiColor(k, st.BG)
|
||||
if fg.A == 0 {
|
||||
fg = k.Text
|
||||
}
|
||||
if st.Inverse {
|
||||
if bg.A == 0 {
|
||||
bg = k.Surface
|
||||
}
|
||||
fg, bg = bg, fg
|
||||
}
|
||||
if st.Dim {
|
||||
fg = fg.Alpha(0.6)
|
||||
}
|
||||
x0 := ox + float32(col)*cw
|
||||
if bg.A > 0 {
|
||||
p.Fill(ui.Rect{X: x0, Y: y, W: float32(end-col) * cw, H: ch}, bg, 0)
|
||||
}
|
||||
for i := col; i < end; i++ {
|
||||
if s.selected(TerminalPos{li, i}) {
|
||||
p.Fill(ui.Rect{X: ox + float32(i)*cw, Y: y, W: cw, H: ch}, k.Selection, 0)
|
||||
}
|
||||
}
|
||||
terminalRun(p, s, cells[col:end], st, font, x0, y+base, cw, fg)
|
||||
if st.Underline || st.Strike {
|
||||
ly := y + base + 2
|
||||
if st.Strike {
|
||||
ly = y + ch/2
|
||||
}
|
||||
internal.Rule(p, x0, x0+float32(end-col)*cw, ly, fg)
|
||||
}
|
||||
col = end
|
||||
}
|
||||
for mi, m := range s.matches {
|
||||
if m.pos.Line != li {
|
||||
continue
|
||||
}
|
||||
mr := ui.Rect{X: ox + float32(m.pos.Col)*cw, Y: y, W: float32(m.n) * cw, H: ch}
|
||||
internal.Rule(p, mr.X, mr.X+mr.W, y+ch-1, k.AccentText)
|
||||
if mi == s.current {
|
||||
p.Stroke(mr, k.AccentText, 1, 1.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.offset > 0 || s.cy >= s.rows {
|
||||
return
|
||||
}
|
||||
cr := ui.Rect{X: ox + float32(s.cx)*cw, Y: oy + float32(s.cy)*ch, W: cw, H: ch}
|
||||
if s.cx >= s.cols {
|
||||
cr.X = ox + float32(s.cols-1)*cw
|
||||
}
|
||||
if s.preedit != "" {
|
||||
g := terminalShape(s, s.preedit, font)
|
||||
p.Glyphs(g, cr.X, cr.Y+base, k.AccentText)
|
||||
internal.Rule(p, cr.X, cr.X+float32(len([]rune(s.preedit)))*cw, cr.Y+ch-1, k.AccentText)
|
||||
return
|
||||
}
|
||||
if !focused {
|
||||
p.Stroke(cr, k.AccentText, 0, 1)
|
||||
return
|
||||
}
|
||||
p.Fill(cr, k.AccentText, 0)
|
||||
if c := min(s.cx, s.cols-1); s.screen[s.cy][c].r > ' ' {
|
||||
p.Glyphs(terminalShape(s, string(s.screen[s.cy][c].r), font), cr.X, cr.Y+base, k.Surface)
|
||||
}
|
||||
}
|
||||
|
||||
// terminalRun draws same-style cells, one glyph per cell.
|
||||
func terminalRun(p *ui.Painter, s *TerminalState, cells []terminalCell, st internal.AnsiStyle, font ui.Font, x, y, cw float32, fg ui.Color) {
|
||||
runes := make([]rune, len(cells))
|
||||
blank := true
|
||||
for i, cell := range cells {
|
||||
runes[i] = cell.r
|
||||
if cell.r == 0 {
|
||||
runes[i] = ' '
|
||||
} else if cell.r != ' ' {
|
||||
blank = false
|
||||
}
|
||||
}
|
||||
if blank {
|
||||
return
|
||||
}
|
||||
if st.Bold {
|
||||
font.Weight = 700
|
||||
}
|
||||
font.Italic = st.Italic
|
||||
p.Glyphs(terminalShape(s, string(runes), font), x, y, fg)
|
||||
}
|
||||
|
||||
// terminalShape shapes text into cells, cached across frames.
|
||||
func terminalShape(s *TerminalState, text string, f ui.Font) []ui.Glyph {
|
||||
key := string(rune(f.Weight)) + string(rune(int(f.Size*4))) + text
|
||||
if f.Italic {
|
||||
key = "i" + key
|
||||
}
|
||||
if g, ok := s.shaped[key]; ok {
|
||||
return g
|
||||
}
|
||||
if s.shaped == nil || len(s.shaped) > 4096 {
|
||||
s.shaped = map[string][]ui.Glyph{}
|
||||
}
|
||||
g := ui.Shape(text, f)
|
||||
cw := f.Size * 0.6
|
||||
if m := ui.Shape("M", ui.Font{Family: f.Family, Size: f.Size}); len(m) > 0 {
|
||||
cw = m[0].Advance
|
||||
}
|
||||
for i := range g {
|
||||
g[i].X = float32(g[i].Cluster) * cw
|
||||
}
|
||||
s.shaped[key] = g
|
||||
return g
|
||||
}
|
||||
Reference in new issue
Block a user