package ui import ( "fmt" "image" "image/color" "strings" "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 } // String returns a string representation of the Region. func (r Region) String() string { return fmt.Sprintf("Region{x=%g y=%g w=%g h=%g}", r.X, r.Y, r.W, r.H) } // 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) String() string } // 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) } } // String returns a string representation of the Container and all children. func (c Container) String() string { var sb strings.Builder sb.WriteString(fmt.Sprintf("Container[%s] region=%+v bg=%#v children=%d", c.id, c.region, c.background, len(c.children))) for i, child := range c.children { if str, ok := any(child).(fmt.Stringer); ok { sb.WriteString(fmt.Sprintf("\n [%d] %s", i, str.String())) } else { sb.WriteString(fmt.Sprintf("\n [%d] %T region=%+v", i, child, child.Region())) } } return sb.String() } // 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 } // String returns a string representation of the Label. func (l Label) String() string { return fmt.Sprintf("Label[%s] text=%q region=%+v align=%d fontSize=%g", l.id, l.Text, l.region, l.Align, l.FontSize) } // 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 } // String returns a string representation of the Icon. func (i Icon) String() string { return fmt.Sprintf("Icon[%s] name=%q region=%+v size=%g", i.id, i.Name, i.region, i.Size) } // 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 } // String returns a string representation of the TextField. func (tf TextField) String() string { return fmt.Sprintf("TextField[%s] region=%+v len=%d multiline=%v", tf.id, tf.region, len(tf.Value), tf.Multiline) } // 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 Dp // pixel-level scroll offset Selected int RowTapHandler func(any) // handler for row taps, receives index as any } 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 } // String returns a string representation of the ListView. func (lv ListView) String() string { var sb strings.Builder sb.WriteString(fmt.Sprintf("ListView[%s] region=%+v items=%d selected=%d", lv.id, lv.region, len(lv.Items), lv.Selected)) for i, item := range lv.Items { sb.WriteString(fmt.Sprintf("\n [%d] %s", i, item.String())) } return sb.String() } // 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) firstVisibleRow := int(lv.ScrollOffset / rowHeight) for i, item := range lv.Items { // Account for scroll offset: row Y is shifted up by scroll amount rowGlobalIndex := firstVisibleRow + i y := lv.region.Y + Dp(rowGlobalIndex)*rowHeight - lv.ScrollOffset // Only draw visible rows (virtualized) 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: y, W: lv.region.W, H: rowHeight, }, Color{R: 200, G: 220, B: 255, A: 255}) } // Register click area for this row rowID := fmt.Sprintf("list_row_%d", rowGlobalIndex) r.RegisterClick(gtx, rowID, Region{ X: lv.region.X, Y: y, W: lv.region.W, H: rowHeight, }, func(data any) { if lv.RowTapHandler != nil { lv.RowTapHandler(rowGlobalIndex) } else { OpenFile(item.Text) } }) // Draw main text textRegion := Region{ X: lv.region.X + Dp(8), Y: y + 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: y + 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 Dp, selected int, interactions []Interaction, rowTapHandler func(any)) ListView { return ListView{ id: id, region: region, visible: true, interactions: interactions, Items: items, ScrollOffset: scrollOffset, Selected: selected, RowTapHandler: rowTapHandler, } } // ListItem is a single entry in a ListView. type ListItem struct { Text string Subtext string Selected bool } // String returns a string representation of the ListItem. func (li ListItem) String() string { selected := "" if li.Selected { selected = " *" } return fmt.Sprintf("ListItem text=%q subtext=%q%s", li.Text, li.Subtext, selected) } // 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 } // String returns a string representation of the AlphaIndex. func (ai AlphaIndex) String() string { return fmt.Sprintf("AlphaIndex[%s] region=%+v letters=%v active=%q", ai.id, ai.region, ai.Letters, ai.ActiveLetter) } // 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 } // String returns a string representation of the Button. func (b Button) String() string { return fmt.Sprintf("Button[%s] text=%q region=%+v enabled=%v", b.id, b.Text, b.region, b.Enabled) } // 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 } // String returns a string representation of the SearchBar. func (sb SearchBar) String() string { return fmt.Sprintf("SearchBar[%s] region=%+v query=%q match=%d/%d", sb.id, sb.region, sb.Query, sb.Match, sb.Total) } // 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 } // String returns a string representation of the Cursor. func (c Cursor) String() string { return fmt.Sprintf("Cursor[%s] region=%+v line=%d col=%d", c.id, c.region, c.Line, c.Column) } // 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 } // String returns a string representation of the MergeHunk. func (mh MergeHunk) String() string { return fmt.Sprintf("MergeHunk[%s] region=%+v hunk=%d/%d lineRange=%s resolution=%d", mh.id, mh.region, mh.HunkNumber, mh.TotalHunks, mh.LineRange, mh.Resolution) } // 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 } // String returns a string representation of the Toast. func (t Toast) String() string { return fmt.Sprintf("Toast[%s] text=%q region=%+v timeout=%d", t.id, t.Text, t.region, t.Timeout) } // 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 } // String returns a string representation of the Spacer. func (s Spacer) String() string { return fmt.Sprintf("Spacer[%s] region=%+v", s.id, s.region) } // 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 } // String returns a string representation of the GioEditor. func (ge GioEditor) String() string { return fmt.Sprintf("GioEditor[%s] region=%+v", ge.id, ge.region) } // 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 } // String returns a string representation of the Color. func (c Color) String() string { return fmt.Sprintf("Color{R:%d G:%d B:%d A:%d}", c.R, c.G, c.B, c.A) } // 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 }