Pad/internal/ui/render.go
Greg Pomerantz 869f0b13ac Implement initial editor mockup with StatusBar, BottomBar, window resize
Elements implemented:
- StatusBar (top bar with filename, action icons, conflict/search icons)
- BottomBar (bottom bar with cursor position, byte position, word wrap button)
- Button (interactive button element)
- AlphaIndex, SearchBar, Cursor, MergeHunk, Toast, Spacer (stubs)

Layout:
- EditorLayout function computes regions for StatusBar and BottomBar
- Filename truncation with ellipsis (mocked filename for testing)
- Mocked data: "very_long_filename...", "Ln 47, Col 12", "1024 / 50000"

Renderer:
- drawStatusBar: draws filename line + icon line
- drawBottomBar: draws cursor pos, byte pos, word wrap button
- drawButton: draws button text
- drawIconButton: draws icon buttons

Window resize:
- configChan setup in main.go
- ConfigEvent handling in event loop
- Layout recomputation on resize

Builds and runs successfully.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-08 16:22:46 -04:00

174 lines
4.2 KiB
Go

package ui
import (
"image"
"gioui.org/layout"
"gioui.org/op/clip"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget/material"
)
// Renderer consumes a slice of elements and draws them.
type Renderer struct {
theme Theme
shp *text.Shaper
}
// New creates a new Renderer.
func New(th Theme, shp *text.Shaper) *Renderer {
return &Renderer{theme: th, shp: shp}
}
// Draw iterates elements and draws each in slice order (back-to-front).
func (r *Renderer) Draw(gtx layout.Context, elems []Element) {
for _, e := range elems {
if !e.Visible() {
continue
}
switch v := e.(type) {
case Label:
r.drawLabel(gtx, v)
case ListView:
r.drawListView(gtx, v)
case StatusBar:
r.drawStatusBar(gtx, v)
case BottomBar:
r.drawBottomBar(gtx, v)
case Button:
r.drawButton(gtx, v)
default:
// unknown element type, skip
}
}
}
func (r *Renderer) drawLabel(gtx layout.Context, l Label) {
reg := l.Region()
c := clip.Rect{
Min: image.Point{X: int(reg.X), Y: int(reg.Y)},
Max: image.Point{X: int(reg.X + reg.W), Y: int(reg.Y + reg.H)},
}.Push(gtx.Ops)
th := material.NewTheme()
th.Shaper = r.shp
size := l.FontSize
if size == 0 {
size = r.theme.FontSize
}
material.Label(th, size, l.Text).Layout(gtx)
c.Pop()
}
func (r *Renderer) drawListView(gtx layout.Context, lv ListView) {
reg := lv.Region()
c := clip.Rect{
Min: image.Point{X: int(reg.X), Y: int(reg.Y)},
Max: image.Point{X: int(reg.X + reg.W), Y: int(reg.Y + reg.H)},
}.Push(gtx.Ops)
th := material.NewTheme()
th.Shaper = r.shp
for _, item := range lv.Items {
material.Label(th, r.theme.FontSize, item.Text).Layout(gtx)
}
c.Pop()
}
func (r *Renderer) drawStatusBar(gtx layout.Context, sb StatusBar) {
reg := sb.Region()
th := material.NewTheme()
th.Shaper = r.shp
// Line 1: Filename (truncated with ellipsis if needed)
filenameLineY := reg.Y
filenameLineH := unit.Dp(24)
filenameW := reg.W - unit.Dp(16) // 8 DP padding on each side
// Truncate filename if needed
displayFilename := sb.Filename
// Simple truncation: if filename is too long, truncate it
if len(displayFilename) > 30 {
displayFilename = displayFilename[:27] + "..."
}
clipFilename := clip.Rect{
Min: image.Point{X: int(reg.X + unit.Dp(8)), Y: int(filenameLineY)},
Max: image.Point{X: int(reg.X + filenameW), Y: int(filenameLineY + filenameLineH)},
}.Push(gtx.Ops)
material.Label(th, r.theme.FontSize, displayFilename).Layout(gtx)
clipFilename.Pop()
// Line 2: Icons
iconsLineY := filenameLineY + filenameLineH
// Left side: Cut, Copy, Paste icons
leftX := reg.X + unit.Dp(8)
iconY := iconsLineY
if sb.CutCopy {
r.drawIconButton(gtx, th, "✂", leftX, iconY)
leftX += unit.Dp(36)
}
if sb.Copy {
r.drawIconButton(gtx, th, "⎘", leftX, iconY)
leftX += unit.Dp(36)
}
if sb.Paste {
r.drawIconButton(gtx, th, "⎘", leftX, iconY)
leftX += unit.Dp(36)
}
// Right side: Conflict, Search icons
rightX := reg.X + reg.W - unit.Dp(8)
if sb.Search {
rightX -= unit.Dp(36)
r.drawIconButton(gtx, th, "🔍", rightX, iconY)
}
if sb.ConflictIcon {
rightX -= unit.Dp(36)
r.drawIconButton(gtx, th, "⚠", rightX, iconY)
}
}
func (r *Renderer) drawBottomBar(gtx layout.Context, bb BottomBar) {
th := material.NewTheme()
th.Shaper = r.shp
// Left: Cursor position
material.Label(th, r.theme.FontSize, bb.CursorPos).Layout(gtx)
// Center: Byte position
material.Label(th, r.theme.FontSize, bb.BytePos).Layout(gtx)
// Right: Word wrap button
material.Label(th, r.theme.FontSize, "[Word Wrap: "+boolToString(bb.WordWrap)+"]").Layout(gtx)
}
func (r *Renderer) drawButton(gtx layout.Context, btn Button) {
th := material.NewTheme()
th.Shaper = r.shp
// Draw button text
material.Label(th, r.theme.FontSize, btn.Text).Layout(gtx)
}
func (r *Renderer) drawIconButton(gtx layout.Context, th *material.Theme, icon string, x, y unit.Dp) {
// Draw icon button
iconRegion := Region{X: x, Y: y, W: unit.Dp(24), H: unit.Dp(24)}
c := clip.Rect{
Min: image.Point{X: int(iconRegion.X), Y: int(iconRegion.Y)},
Max: image.Point{X: int(iconRegion.X + iconRegion.W), Y: int(iconRegion.Y + iconRegion.H)},
}.Push(gtx.Ops)
material.Label(th, r.theme.FontSize, icon).Layout(gtx)
c.Pop()
}
func boolToString(b bool) string {
if b {
return "On"
}
return "Off"
}