package ui import ( "fmt" "image" "image/color" "gioui.org/font" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" "gioui.org/op/paint" "gioui.org/unit" "gioui.org/widget" ) // Region defines a screen area in device-independent pixels (Dp). // All element positions and sizes use Dp for device independence. type Region struct { X, Y Dp W, H Dp } // Element is the base interface for all UI elements. // Elements know how to draw themselves when given a Renderer. type Element interface { Region() Region Visible() bool Draw(gtx layout.Context, r *Renderer) } // Container holds child elements and draws them within its bounds. // Children's regions are screen-space; clipping handles containment. type Container struct { id string region Region visible bool background Color children []Element } func (c Container) Region() Region { return c.region } func (c Container) Visible() bool { return c.visible } func (c Container) Draw(gtx layout.Context, r *Renderer) { // Draw background — children are drawn by drawElement, not here if c.background != (Color{}) { r.drawBg(gtx, c.region, c.background) } } // NewContainer creates a Container with the given region, background, and children. func NewContainer(region Region, bg Color, children []Element) Container { return Container{ region: region, visible: true, background: bg, children: children, } } // --- Leaf element types (each implements Element and knows how to Draw itself) --- // Label displays static text. type Label struct { id string region Region visible bool interactions []Interaction Text string Align TextAlign FontSize unit.Sp // 0 = theme default Color Color // 0 = theme default Bold bool } func (l Label) Region() Region { return l.region } func (l Label) Visible() bool { return l.visible } func (l Label) Interactions() []Interaction { return l.interactions } func (l Label) Draw(gtx layout.Context, r *Renderer) { col := l.Color if col == (Color{}) { col = Color{R: 0, G: 0, B: 0, A: 255} } // Click registration is handled by registerInteraction within drawElement, // which correctly applies the container offset. No separate RegisterClick needed. r.drawText(gtx, l.Text, l.FontSize, l.region, l.Align, col, l.id) } func (l Label) ID() string { return l.id } // NewLabel creates a visible Label element. func NewLabel(text string, fontSize unit.Sp, region Region, align TextAlign, id string, interactions []Interaction) Label { return Label{ id: id, region: region, visible: true, interactions: interactions, Text: text, FontSize: fontSize, Align: align, } } // Icon displays a named icon image. type Icon struct { id string region Region visible bool interactions []Interaction Name string // icon name, e.g. "cut" Size Dp // 0 = default icon size } func (i Icon) Region() Region { return i.region } func (i Icon) Visible() bool { return i.visible } func (i Icon) Interactions() []Interaction { return i.interactions } func (i Icon) Draw(gtx layout.Context, r *Renderer) { img := r.icon(i.Name) if img == nil { return } // Click registration is handled by registerInteraction within drawElement, // which correctly applies the container offset. No separate RegisterClick needed. // Auto-scale: if Size is 0, use region dimensions so the icon fills its region w, h := Dp(0), Dp(0) if i.Size == 0 { w, h = i.region.W, i.region.H } r.drawPng(gtx, img, i.region, w, h) } func (i Icon) ID() string { return i.id } // NewIcon creates a visible Icon element. func NewIcon(name string, region Region, size Dp, interactions []Interaction) Icon { return Icon{ id: name, region: region, visible: true, interactions: interactions, Name: name, Size: size, } } // TextField accepts text input or displays multiline text. type TextField struct { id string region Region visible bool interactions []Interaction Value string Placeholder string Focused bool Multiline bool ScrollOffset Dp VisibleLines []Line WordWrap bool WrapWidth Dp } func (tf TextField) Region() Region { return tf.region } func (tf TextField) Visible() bool { return tf.visible } func (tf TextField) ID() string { return tf.id } func (tf TextField) Interactions() []Interaction { return tf.interactions } // Draw renders the TextField. For multiline text, it shapes with word wrap // and draws display lines inline — one LayoutString call, no double-shaping. func (tf TextField) Draw(gtx layout.Context, r *Renderer) { r.drawWrappedText(gtx, tf.Value, tf.region, tf.WrapWidth, tf.ScrollOffset) } // NewTextField creates a visible multiline TextField. func NewTextField(id string, value string, region Region, wrapWidth Dp, scrollOffset Dp, interactions []Interaction) TextField { return TextField{ id: id, region: region, visible: true, interactions: interactions, Value: value, Multiline: true, WordWrap: true, WrapWidth: wrapWidth, ScrollOffset: scrollOffset, } } // Line represents a single line of text in a multiline TextField. type Line struct { Text string LineNumber int // 1-indexed, for display } // ListView displays a scrollable list of items. type ListView struct { id string region Region visible bool interactions []Interaction Items []ListItem ScrollOffset int Selected int RowFilenames []string // filenames for click navigation } func (lv ListView) Region() Region { return lv.region } func (lv ListView) Visible() bool { return lv.visible } func (lv ListView) ID() string { return lv.id } func (lv ListView) NeedsClip() bool { return true } func (lv ListView) Interactions() []Interaction { // ListView registers its scroll in Draw, not via registerInteraction. // Filter out Scroll so registerInteraction only handles Tap. var filtered []Interaction for _, interaction := range lv.interactions { if interaction.Gesture != Scroll { filtered = append(filtered, interaction) } } return filtered } // Draw renders each list item as a row of text. // The scroll gesture is registered via the normal interaction path so it // is clipped to the list region by the element's clip context. func (lv ListView) Draw(gtx layout.Context, r *Renderer) { // Register scroll gesture - clip is already active from drawElement if len(lv.interactions) > 0 { var scrollHandler func(any) for _, interaction := range lv.interactions { if interaction.Gesture == Scroll { scrollHandler = interaction.Handler break } } if scrollHandler != nil { r.RegisterScroll(gtx, lv.id, lv.region, scrollHandler) } } rowHeight := Dp(48) for i, item := range lv.Items { // Only draw visible rows (virtualized) y := lv.region.Y + Dp(i)*rowHeight if y+rowHeight < lv.region.Y || y > lv.region.Y+lv.region.H { continue } // Draw background for selected item if item.Selected || (lv.Selected == i) { r.drawBg(gtx, Region{ X: lv.region.X, Y: lv.region.Y + Dp(i)*rowHeight, W: lv.region.W, H: rowHeight, }, Color{R: 200, G: 220, B: 255, A: 255}) } // Register click area for this row rowGlobalIndex := lv.ScrollOffset + i rowID := fmt.Sprintf("list_row_%d", rowGlobalIndex) var filename string if rowGlobalIndex < len(lv.RowFilenames) { filename = lv.RowFilenames[rowGlobalIndex] } r.RegisterClick(gtx, rowID, Region{ X: lv.region.X, Y: y, W: lv.region.W, H: rowHeight, }, func(data any) { OpenFile(filename) }) // Draw main text textRegion := Region{ X: lv.region.X + Dp(8), Y: lv.region.Y + Dp(i)*rowHeight + Dp(4), W: lv.region.W - Dp(32), H: rowHeight - Dp(8), } r.drawText(gtx, item.Text, 14, textRegion, AlignStart, Color{R: 0, G: 0, B: 0, A: 255}, "") // Draw subtext if item.Subtext != "" { subRegion := Region{ X: lv.region.X + Dp(8), Y: lv.region.Y + Dp(i)*rowHeight + rowHeight - Dp(22), W: lv.region.W - Dp(32), H: Dp(16), } r.drawText(gtx, item.Subtext, 12, subRegion, AlignStart, Color{R: 128, G: 128, B: 128, A: 255}, "") } } } // NewListView creates a visible ListView element. func NewListView(id string, items []ListItem, region Region, scrollOffset, selected int, interactions []Interaction) ListView { return ListView{ id: id, region: region, visible: true, interactions: interactions, Items: items, ScrollOffset: scrollOffset, Selected: selected, } } // ListItem is a single entry in a ListView. type ListItem struct { Text string Subtext string Selected bool } // AlphaIndex displays an alphabetical index for quick navigation. type AlphaIndex struct { id string region Region visible bool interactions []Interaction Letters []string ActiveLetter string } func (ai AlphaIndex) Region() Region { return ai.region } func (ai AlphaIndex) Visible() bool { return ai.visible } func (ai AlphaIndex) ID() string { return ai.id } func (ai AlphaIndex) Interactions() []Interaction { return ai.interactions } // Draw renders letters vertically along the right edge. func (ai AlphaIndex) Draw(gtx layout.Context, r *Renderer) { letterHeight := ai.region.H / Dp(len(ai.Letters)) for i, letter := range ai.Letters { region := Region{ X: ai.region.X, Y: ai.region.Y + Dp(i)*letterHeight, W: ai.region.W, H: letterHeight, } col := Color{R: 100, G: 100, B: 100, A: 255} if letter == ai.ActiveLetter { col = Color{R: 0, G: 100, B: 200, A: 255} } r.drawText(gtx, letter, 10, region, AlignCenter, col, "") } } // NewAlphaIndex creates a visible AlphaIndex element. func NewAlphaIndex(region Region, letters []string) AlphaIndex { return AlphaIndex{ region: region, visible: true, Letters: letters, } } // Button is an interactive button element. type Button struct { id string region Region visible bool interactions []Interaction Text string Enabled bool Primary bool } func (b Button) Region() Region { return b.region } func (b Button) Visible() bool { return b.visible } func (b Button) Interactions() []Interaction { return b.interactions } func (b Button) Draw(gtx layout.Context, r *Renderer) { col := Color{R: 0, G: 0, B: 0, A: 255} r.drawText(gtx, b.Text, r.theme.FontSize, b.region, AlignStart, col, b.id) } func (b Button) ID() string { return b.id } // NewButton creates a visible Button element. func NewButton(text string, enabled bool, primary bool, region Region) Button { return Button{ region: region, visible: true, Text: text, Enabled: enabled, Primary: primary, } } // SearchBar displays an in-editor search interface. type SearchBar struct { id string region Region visible bool interactions []Interaction Query string Match int Total int Forward bool } func (sb SearchBar) Region() Region { return sb.region } func (sb SearchBar) Visible() bool { return sb.visible } func (sb SearchBar) ID() string { return sb.id } func (sb SearchBar) Interactions() []Interaction { return sb.interactions } // NewSearchBar creates a visible SearchBar element. func NewSearchBar(region Region, query string, match, total int, forward bool) SearchBar { return SearchBar{ region: region, visible: true, Query: query, Match: match, Total: total, Forward: forward, } } // Cursor displays the text cursor and optional selection highlight. type Cursor struct { id string region Region visible bool interactions []Interaction Line int Column int Blinking bool Selection *Selection } func (c Cursor) Region() Region { return c.region } func (c Cursor) Visible() bool { return c.visible } func (c Cursor) ID() string { return c.id } func (c Cursor) Interactions() []Interaction { return c.interactions } // Selection represents a text selection range. type Selection struct { StartLine, StartCol int EndLine, EndCol int } // MergeHunk displays a conflict resolution hunk. type MergeHunk struct { id string region Region visible bool interactions []Interaction HunkNumber int TotalHunks int LineRange string ContextLines []string OurLines []string TheirLines []string Resolution HunkResolution } func (mh MergeHunk) Region() Region { return mh.region } func (mh MergeHunk) Visible() bool { return mh.visible } func (mh MergeHunk) ID() string { return mh.id } func (mh MergeHunk) Interactions() []Interaction { return mh.interactions } // HunkResolution represents the resolution state of a merge hunk. type HunkResolution int const ( Unresolved HunkResolution = iota KeepOurs KeepTheirs MergeBoth ) // Toast displays a temporary notification. type Toast struct { id string region Region visible bool interactions []Interaction Text string Timeout int // milliseconds } func (t Toast) Region() Region { return t.region } func (t Toast) Visible() bool { return t.visible } func (t Toast) ID() string { return t.id } func (t Toast) Interactions() []Interaction { return t.interactions } // NewToast creates a visible Toast element. func NewToast(region Region, text string, timeout int) Toast { return Toast{ region: region, visible: true, Text: text, Timeout: timeout, } } // Spacer adds vertical or horizontal space. type Spacer struct { id string region Region visible bool interactions []Interaction } func (s Spacer) Region() Region { return s.region } func (s Spacer) Visible() bool { return s.visible } func (s Spacer) ID() string { return s.id } func (s Spacer) Interactions() []Interaction { return s.interactions } // NewSpacer creates a visible Spacer element. func NewSpacer(height Dp) Spacer { return Spacer{ region: Region{H: height}, visible: true, } } // GioEditor wraps a Gio widget.Editor for single-line or multiline text input. type GioEditor struct { id string region Region visible bool interactions []Interaction Editor *widget.Editor } func (ge GioEditor) Region() Region { return ge.region } func (ge GioEditor) Visible() bool { return ge.visible } func (ge GioEditor) ID() string { return ge.id } func (ge GioEditor) Interactions() []Interaction { return ge.interactions } // Draw renders the Gio Editor widget. func (ge GioEditor) Draw(gtx layout.Context, r *Renderer) { ge.Editor.SingleLine = true // Position and clip to the editor's region stack := op.Offset(image.Pt(int(r.toPx(ge.region.X)), int(r.toPx(ge.region.Y)))).Push(gtx.Ops) defer stack.Pop() // Draw a simple background for the search bar rect := image.Rectangle{Max: image.Pt(int(r.toPx(ge.region.W)), int(r.toPx(ge.region.H)))} paint.FillShape(gtx.Ops, color.NRGBA{R: 245, G: 245, B: 245, A: 255}, clip.Rect(rect).Op()) // Create paint color macros for text and selection colors textMacro := op.Record(gtx.Ops) paint.ColorOp{Color: color.NRGBA{R: 0, G: 0, B: 0, A: 255}}.Add(gtx.Ops) textColor := textMacro.Stop() selectionMacro := op.Record(gtx.Ops) paint.ColorOp{Color: color.NRGBA{R: 200, G: 220, B: 255, A: 255}}.Add(gtx.Ops) selectionColor := selectionMacro.Stop() // Set constraints so the editor knows its size for hit-testing gtx.Constraints = layout.Exact(rect.Size()) // Use our RegisterClick to ensure the full region is clickable r.RegisterClick(gtx, ge.id, ge.region, nil) ge.Editor.Layout(gtx, r.shp, font.Font{}, r.theme.FontSize, textColor, selectionColor) } // NewGioEditor creates a GioEditor element wrapping a widget.Editor. func NewGioEditor(id string, region Region, editor *widget.Editor) GioEditor { return GioEditor{ id: id, region: region, visible: true, Editor: editor, } } // --- Utility types --- // OpenFile is called by the browser list when a file row is tapped. // Set by the editor package after initialization. var OpenFile func(any) // Color is an RGBA color. type Color struct { R, G, B, A uint8 } // Theme holds styling defaults for the UI. type Theme struct { FontSize unit.Sp // Gio's shaper requires unit.Sp for font sizes } // TextAlign specifies horizontal text alignment. type TextAlign int const ( AlignStart TextAlign = iota AlignCenter AlignEnd ) // InputType specifies the type of user input event. type InputType int const ( Tap InputType = iota DoubleTap LongPress Scroll KeyDown KeyUp ) // Interaction pairs a gesture type with a handler function. type Interaction struct { Gesture InputType Handler func(any) } // Interactive is implemented by elements that respond to input events. type Interactive interface { ID() string Interactions() []Interaction } // InputEvent represents a user input event with its handler. type InputEvent struct { Handler func(any) Data any } // ConfigEvent represents a window configuration change (resize, orientation). // Width and Height are in device-independent pixels (Dp). type ConfigEvent struct { Width Dp Height Dp }