package ui import ( "image" "image/color" "gioui.org/f32" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" "gioui.org/op/paint" "gioui.org/text" "gioui.org/unit" "gioui.org/widget/material" "golang.org/x/image/math/fixed" ) // 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() th := material.NewTheme() th.Shaper = r.shp size := l.FontSize if size == 0 { size = r.theme.FontSize } // Position label at region origin gtx.Constraints.Min.X = int(reg.X) gtx.Constraints.Min.Y = int(reg.Y) material.Label(th, size, l.Text).Layout(gtx) } func (r *Renderer) drawListView(gtx layout.Context, lv ListView) { reg := lv.Region() th := material.NewTheme() th.Shaper = r.shp // Position list view at region origin gtx.Constraints.Min.X = int(reg.X) gtx.Constraints.Min.Y = int(reg.Y) for _, item := range lv.Items { material.Label(th, r.theme.FontSize, item.Text).Layout(gtx) } } func (r *Renderer) drawStatusBar(gtx layout.Context, sb StatusBar) { reg := sb.Region() th := material.NewTheme() th.Shaper = r.shp // Clip to status bar 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) // Line 1: Filename (truncated with ellipsis if needed) filenameLineY := reg.Y filenameLineH := unit.Dp(24) // Truncate filename only if it doesn't fit displayFilename := sb.Filename availableWidth := int(reg.W - unit.Dp(8) - unit.Dp(40)) // left margin + right icons in DP // Layout ellipsis once and get its width th.Shaper.LayoutString(text.Parameters{PxPerEm: fixed.I(gtx.Sp(r.theme.FontSize))}, "...") var ellipsisWidths []int for { g, ok := th.Shaper.NextGlyph() if !ok { break } if len(ellipsisWidths) == 0 { ellipsisWidths = append(ellipsisWidths, int(g.Advance>>6)) } else { ellipsisWidths = append(ellipsisWidths, ellipsisWidths[len(ellipsisWidths)-1]+int(g.Advance>>6)) } } ellipsisWidth := ellipsisWidths[len(ellipsisWidths)-1] // Measure how wide "..." is, then find how many chars fit in availableWidth - ellipsisWidth spaceForText := availableWidth - ellipsisWidth // Layout filename, measure widths, and draw in a single pass r.drawTruncatedText(gtx, th.Shaper, displayFilename, r.theme.FontSize, reg.X+unit.Dp(8), filenameLineY, availableWidth, spaceForText, color.NRGBA{R: 0, G: 0, B: 0, A: 255}) // Line 2: Icons iconsLineY := filenameLineY + filenameLineH // Left side: Cut, Copy, Paste icons leftX := reg.X + unit.Dp(8) iconY := iconsLineY if sb.CutCopy { r.drawText(gtx, th.Shaper, "✂", r.theme.FontSize, leftX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255}) leftX += unit.Dp(36) } if sb.Copy { r.drawText(gtx, th.Shaper, "⎘", r.theme.FontSize, leftX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255}) leftX += unit.Dp(36) } if sb.Paste { r.drawText(gtx, th.Shaper, "⎘", r.theme.FontSize, leftX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255}) 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.drawText(gtx, th.Shaper, "🔍", r.theme.FontSize, rightX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255}) } if sb.ConflictIcon { rightX -= unit.Dp(36) r.drawText(gtx, th.Shaper, "⚠", r.theme.FontSize, rightX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255}) } c.Pop() } func (r *Renderer) drawBottomBar(gtx layout.Context, bb BottomBar) { reg := bb.Region() th := material.NewTheme() th.Shaper = r.shp // Left: Cursor position cursorX := reg.X + unit.Dp(8) call1 := op.Offset(image.Point{X: int(cursorX), Y: int(reg.Y)}).Push(gtx.Ops) material.Label(th, r.theme.FontSize, bb.CursorPos).Layout(gtx) call1.Pop() // Center: Byte position byteX := reg.X + reg.W/2 - unit.Dp(60) // approximate center call2 := op.Offset(image.Point{X: int(byteX), Y: int(reg.Y)}).Push(gtx.Ops) material.Label(th, r.theme.FontSize, bb.BytePos).Layout(gtx) call2.Pop() // Right: Word wrap button wordWrapX := reg.X + reg.W - unit.Dp(80) call3 := op.Offset(image.Point{X: int(wordWrapX), Y: int(reg.Y)}).Push(gtx.Ops) material.Label(th, r.theme.FontSize, "W:"+boolToString(bb.WordWrap)).Layout(gtx) call3.Pop() } func (r *Renderer) drawButton(gtx layout.Context, btn Button) { reg := btn.Region() th := material.NewTheme() th.Shaper = r.shp // Position button at region origin gtx.Constraints.Min.X = int(reg.X) gtx.Constraints.Min.Y = int(reg.Y) 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) { // Position icon button at (x, y) gtx.Constraints.Min.X = int(x) gtx.Constraints.Min.Y = int(y) material.Label(th, r.theme.FontSize, icon).Layout(gtx) } func boolToString(b bool) string { if b { return "On" } return "Off" } // drawTruncatedText layouts text, measures widths, truncates if needed, and draws in a single pass. func (r *Renderer) drawTruncatedText(gtx layout.Context, shp *text.Shaper, str string, size unit.Sp, x, y unit.Dp, availableWidth, spaceForText int, col color.NRGBA) { // Layout text shp.LayoutString(text.Parameters{PxPerEm: fixed.I(gtx.Sp(size))}, str) // Measure widths and find truncation point var widths []int var cumWidth fixed.Int26_6 fullStr := str for { g, ok := shp.NextGlyph() if !ok { break } cumWidth += g.Advance w := int(cumWidth >> 6) widths = append(widths, w) } // Truncate if needed if len(widths) > 0 && widths[len(widths)-1] > availableWidth { // Find truncation point (from longest to shortest) for i := len(widths)-1; i >= 0; i-- { if widths[i] <= spaceForText { fullStr = str[:i] + "..." break } } } // Re-layout truncated text and draw glyphs directly shp.LayoutString(text.Parameters{PxPerEm: fixed.I(gtx.Sp(size))}, fullStr) r.drawLineText(gtx, shp, x, y, col) } // drawText draws text using the same approach as Gio's textView: // layout text, iterate glyphs, buffer into lines, and draw using shaper.Shape(). func (r *Renderer) drawText(gtx layout.Context, shp *text.Shaper, str string, size unit.Sp, x, y unit.Dp, col color.NRGBA) { // Layout text shp.LayoutString(text.Parameters{PxPerEm: fixed.I(gtx.Sp(size))}, str) r.drawLineText(gtx, shp, x, y, col) } // drawLineText draws already-laid-out glyphs using shaper.Shape() and shaper.Bitmaps(). func (r *Renderer) drawLineText(gtx layout.Context, shp *text.Shaper, x, y unit.Dp, col color.NRGBA) { var glyphs [32]text.Glyph line := glyphs[:0] for g, ok := shp.NextGlyph(); ok; g, ok = shp.NextGlyph() { line = append(line, g) if g.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 { r.drawLine(gtx, shp, line, x, y, col) line = line[:0] } } if len(line) > 0 { r.drawLine(gtx, shp, line, x, y, col) } } // drawLine draws a line of glyphs using shaper.Shape() and shaper.Bitmaps(). func (r *Renderer) drawLine(gtx layout.Context, shp *text.Shaper, line []text.Glyph, x, y unit.Dp, col color.NRGBA) { // Apply offset transform off := f32.Point{X: float32(x), Y: float32(y)} t := op.Affine(f32.Affine2D{}.Offset(off)).Push(gtx.Ops) // Draw vector glyphs path := shp.Shape(line) outline := clip.Outline{Path: path}.Op().Push(gtx.Ops) paint.ColorOp{Color: col}.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) outline.Pop() // Draw bitmap glyphs (emoji, etc.) if call := shp.Bitmaps(line); call != (op.CallOp{}) { call.Add(gtx.Ops) } t.Pop() } // charWidths returns the cumulative width after each rune in str. func charWidths(gtx layout.Context, shp *text.Shaper, str string, size unit.Sp) []int { // Match Gio's textView: PxPerEm = font size in device pixels shp.LayoutString(text.Parameters{PxPerEm: fixed.I(gtx.Sp(size))}, str) var widths []int var cumWidth fixed.Int26_6 = 0 for { g, ok := shp.NextGlyph() if !ok { break } cumWidth += g.Advance widths = append(widths, int(cumWidth>>6)) } return widths }