Files
wehub-resource-sync c4536f7e05
CI / test (push) Failing after 1s
CI / macOS amd64 (push) Has been cancelled
CI / macOS arm64 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:31 +08:00

173 lines
3.8 KiB
Go

package gui
import (
"image"
"image/color"
"time"
"gioui.org/font"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"loov.dev/lensm/internal/syntax"
)
// SourceLine is a single-line of text.
type SourceLine struct {
TopLeft image.Point
Width int
Text string
Spans []syntax.Span
TextHeight unit.Sp
Italic bool
Bold bool
Color color.NRGBA
}
// codeLineHeightScale leaves room for ascenders and descenders when a
// line is clipped to its width; rows themselves advance by the tighter
// CodeUIStyle.LineHeight.
const codeLineHeightScale = 4.0 / 3.0
func CodeLineHeightPx(gtx layout.Context, textHeight unit.Sp) int {
height := gtx.Metric.Sp(textHeight * codeLineHeightScale)
if height < 1 {
return 1
}
return height
}
// Layout draws the text.
func (line SourceLine) Layout(th *material.Theme, gtx layout.Context) {
gtx.Constraints.Min.X = 0
gtx.Constraints.Max.X = MaxLineWidth
gtx.Constraints.Min.Y = 0
gtx.Constraints.Max.Y = MaxLineWidth
defer op.Offset(line.TopLeft).Push(gtx.Ops).Pop()
if line.Width > 0 {
maxSize := image.Pt(line.Width, CodeLineHeightPx(gtx, line.TextHeight))
defer clip.Rect{Max: maxSize}.Push(gtx.Ops).Pop()
}
spans := line.Spans
if len(spans) == 0 {
spans = []syntax.Span{{
Text: line.Text,
Color: line.Color,
Italic: line.Italic,
Bold: line.Bold,
}}
}
left := 0
for _, span := range spans {
if span.Text == "" {
continue
}
dims := layoutSourceSpan(th, gtx, line, span, left)
left += dims.Size.X
}
}
func layoutSourceSpan(th *material.Theme, gtx layout.Context, line SourceLine, span syntax.Span, left int) layout.Dimensions {
f := font.Font{Typeface: "override-monospace,Go,monospace", Weight: font.Normal}
if line.Italic || span.Italic {
f.Style = font.Italic
}
if line.Bold || span.Bold {
f.Weight = font.Black
}
col := span.Color
if col == (color.NRGBA{}) {
col = line.Color
}
defer op.Offset(image.Pt(left, 0)).Push(gtx.Ops).Pop()
paint.ColorOp{Color: col}.Add(gtx.Ops)
return widget.Label{MaxLines: 1}.Layout(gtx, th.Shaper, f, line.TextHeight, span.Text, op.CallOp{})
}
type VerticalLine struct {
Width unit.Dp
Color color.NRGBA
}
func (line VerticalLine) Layout(gtx layout.Context) layout.Dimensions {
size := image.Point{
X: gtx.Metric.Dp(line.Width),
Y: gtx.Constraints.Min.Y,
}
paint.FillShape(gtx.Ops, line.Color, clip.Rect{Max: size}.Op())
return layout.Dimensions{
Size: size,
}
}
type HorizontalLine struct {
Height unit.Dp
Color color.NRGBA
}
func (line HorizontalLine) Layout(gtx layout.Context) layout.Dimensions {
size := image.Point{
X: gtx.Constraints.Min.X,
Y: gtx.Metric.Dp(line.Height),
}
paint.FillShape(gtx.Ops, line.Color, clip.Rect{Max: size}.Op())
return layout.Dimensions{
Size: size,
}
}
type ScrollAnimation struct {
active bool
from, to float32
duration time.Duration
start time.Time
}
func (anim *ScrollAnimation) Start(gtx layout.Context, from, to float32, duration time.Duration) {
anim.active = true
anim.from = from
anim.to = to
anim.duration = duration
anim.start = gtx.Now
gtx.Execute(op.InvalidateCmd{})
}
func (anim *ScrollAnimation) Stop() { anim.active = false }
func (anim *ScrollAnimation) Update(gtx layout.Context) (float32, bool) {
if !anim.active {
return anim.to, false
}
gtx.Execute(op.InvalidateCmd{})
elapsed := gtx.Now.Sub(anim.start)
if elapsed > anim.duration {
anim.active = false
return anim.to, true
}
progress := float32(elapsed) / float32(anim.duration)
progress = easeInOutCubic(progress)
pos := anim.from + progress*(anim.to-anim.from)
return pos, true
}
func easeInOutCubic(t float32) float32 {
if t < .5 {
return 4 * t * t * t
}
return (t-1)*(2*t-2)*(2*t-2) + 1
}
const MaxLineWidth = 10 * 1024