136 lines
4.0 KiB
Go
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)
|
|
}
|
|
}
|