Files
MujicaUI/code/minimap.go
T

136 lines
4.0 KiB
Go

package code
import (
"fmt"
"log/slog"
"strings"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
)
// MinimapView is the editor's first line and line count.
type MinimapView struct{ Top, Visible int }
// MinimapOptions configure a Minimap.
type MinimapOptions struct {
// Language colors the strokes as CodeViewer does.
Language string
// Width is the map's width in DIPs; zero is 96.
Width float32
Disabled bool
}
// Line height and character width on the map.
const (
minimapLine float32 = 2
minimapChar float32 = 1
)
// Minimap draws code small and moves the visible frame.
func Minimap(c *ui.Context, lines []string, view *MinimapView, opts MinimapOptions) core.ChoiceResult {
if view == nil {
panic("mujica: Minimap needs a view")
}
if opts.Width < 0 || view.Visible < 0 || view.Top < 0 {
panic("mujica: Minimap width, Top and Visible must not be negative")
}
k := core.Tokens(c)
w := opts.Width
if w == 0 {
w = 96
}
n := len(lines)
var res core.ChoiceResult
move := func(top int) {
top = max(0, min(top, n-view.Visible))
if top != view.Top {
view.Top = top
res = core.NewChoiceResult(res.Element, true)
slog.Debug("mujica: minimap move", "top", top)
}
}
e := ui.Box(c).Width(w).Shrink(0).MinHeight(theme.MinHitSize*2).Focusable().Clip().Cursor(ui.CursorPointer).
Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius).Disabled(opts.Disabled).
Label(core.Msg(c, "minimap.label")).Description(fmt.Sprintf(core.Msg(c, "minimap.range"), min(view.Top+1, n), min(view.Top+view.Visible, n), n))
res.Element = e
h := e.Bounds().H
offset := minimapOffset(n, *view, h)
if !e.IsDisabled() {
if _, _, held := e.Dragged(); held {
_, y, _ := e.PointerPosition()
move(int((y+offset)/minimapLine) - view.Visible/2)
}
switch {
case e.Shortcut(0, ui.KeyDown):
move(view.Top + 1)
case e.Shortcut(0, ui.KeyUp):
move(view.Top - 1)
case e.Shortcut(0, ui.KeyPageDown):
move(view.Top + max(view.Visible, 1))
case e.Shortcut(0, ui.KeyPageUp):
move(view.Top - max(view.Visible, 1))
case e.Shortcut(0, ui.KeyHome):
move(0)
case e.Shortcut(0, ui.KeyEnd):
move(n)
}
}
internal.Ring(e, k.Focus, theme.ControlRadius)
v := *view
e.Draw(func(p *ui.Painter, r ui.Rect) {
if r.H != h {
c.Invalidate()
}
p.Clip(r, theme.ControlRadius, func() {
off := minimapOffset(n, v, r.H)
first := int(off / minimapLine)
for i := first; i < n && float32(i-first)*minimapLine < r.H+minimapLine; i++ {
y := internal.Snap(p, r.Y+float32(i)*minimapLine-off)
minimapStrokes(p, k, opts.Language, lines[i], r.X+6, y, r.X+r.W-6)
}
fy := internal.Snap(p, r.Y+float32(v.Top)*minimapLine-off)
frame := ui.Rect{X: r.X + 1, Y: fy, W: r.W - 2, H: max(internal.Snap(p, float32(v.Visible)*minimapLine), 4)}
p.Fill(frame, k.Selection.Alpha(0.55), 0)
p.Stroke(frame, k.AccentText, 0, 1)
})
})
return res
}
// minimapOffset scrolls a tall map with the view.
func minimapOffset(n int, v MinimapView, h float32) float32 {
total := float32(n) * minimapLine
if total <= h || n <= v.Visible {
return 0
}
return float32(v.Top) / float32(n-v.Visible) * (total - h)
}
// minimapStrokes draws a line's tokens as short bars, skipping blanks.
func minimapStrokes(p *ui.Painter, k theme.Tokens, lang, line string, x0, y, x1 float32) {
line = strings.ReplaceAll(line, "\t", " ")
toks, _ := internal.Highlight(lang, line, false)
col := 0
for _, t := range toks {
runes := []rune(t.Text)
start := -1
for i, r := range append(runes, ' ') {
if r != ' ' && start < 0 {
start = i
}
if r == ' ' && start >= 0 {
x := x0 + float32(col+start)*minimapChar
if x < x1 {
p.Fill(ui.Rect{X: x, Y: y, W: min(float32(i-start)*minimapChar, x1-x), H: minimapLine * 0.7}, CodeTokenColor(k, t.Kind).Alpha(0.75), 0)
}
start = -1
}
}
col += len(runes)
}
}