package ui import ( "fmt" "image" "image/color" "strings" "gioui.org/font" "gioui.org/io/key" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" "gioui.org/op/paint" "gioui.org/unit" ) // 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 Type() 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 // Changed to Exported } func (c Container) Type() string { return "container" } 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) Type() string { return "label" } 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) Type() string { return "icon" } 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 CursorPosition int // SelectionStart/SelectionEnd are byte offsets into Value (the visible // window); -1 means no selection. Used for the IME selection push and the // in-app highlight. SelectionStart int SelectionEnd int ScrollOffset Dp VisibleLines []Line WordWrap bool WrapWidth Dp } func (tf TextField) Type() string { return "textfield" } 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 } func (tf TextField) NeedsClip() bool { return true } // 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) { if tf.Focused { // (Re-)gained focus for this field (or a different field than the one the // dedup state currently tracks): force a fresh snippet/selection push so // the IME starts from a known state. if r.lastIMEField != tf.id || !r.lastWasFocused { r.lastSnippet = key.Snippet{} r.lastSelStart = -1 r.lastSelCaret = -1 } r.lastIMEField = tf.id r.lastWasFocused = true gtx.Execute(key.FocusCmd{Tag: tf.id}) gtx.Execute(key.SoftKeyboardCmd{Show: true}) // IME wiring. The visible window (tf.Value) is pushed as the snippet // with Range {0, len}, so the IME treats the window as the document and // reports EditEvent.Range window-relative. This lets swipe/autocorrect // operate on the visible text without shipping the whole file to the IME. // // Item 4: tell the IME this is a text field (enables the text keyboard, // autocorrect, and suggestions). key.InputHintOp{Tag: tf.id, Hint: key.HintText}.Add(gtx.Ops) // Item 2: push the snippet (the visible window) for swipe/autocorrect, // but only when it changed. Re-pushing an unchanged snippet every frame // resets the IME's composition/cursor, which desyncs fast commits (see // widget.Editor's updateSnippet dedup). snippet := key.Snippet{ Range: key.Range{Start: 0, End: runeCount(tf.Value, len(tf.Value))}, Text: tf.Value, } if snippet != r.lastSnippet { r.lastSnippet = snippet gtx.Execute(key.SnippetCmd{Tag: tf.id, Snippet: snippet}) } // Item 1: sync the caret/selection so the IME's selection matches. // Window-relative rune indices (tf.CursorPosition and the selection // bounds are byte offsets into tf.Value). With a selection, push the // full range so the IME highlights it and a commit replaces it (the // logic side unions the commit range with the selection, so the // replacement is deterministic regardless of what the IME reports). // Push only when the (start, end) pair changes, so a static selection // does not reset the IME every frame. var selStart, selEnd int if tf.SelectionStart >= 0 && tf.SelectionEnd > tf.SelectionStart { // Clamp to the window (the element may be built from a window that // does not fully contain the selection). s := tf.SelectionStart if s < 0 { s = 0 } e := tf.SelectionEnd if e > len(tf.Value) { e = len(tf.Value) } selStart = runeCount(tf.Value, s) selEnd = runeCount(tf.Value, e) } else { selStart = -1 selEnd = runeCount(tf.Value, tf.CursorPosition) } if selStart != r.lastSelStart || selEnd != r.lastSelCaret { r.lastSelStart = selStart r.lastSelCaret = selEnd rng := key.Range{Start: selStart, End: selEnd} if selStart < 0 { rng = key.Range{Start: selEnd, End: selEnd} } gtx.Execute(key.SelectionCmd{Tag: tf.id, Range: rng, Caret: key.Caret{}}) } } else if r.lastIMEField == tf.id { // This (previously-focused) field lost focus: forget it so the next focus // pushes a fresh snippet/selection. r.lastIMEField = "" r.lastWasFocused = false r.lastSnippet = key.Snippet{} r.lastSelStart = -1 r.lastSelCaret = -1 } r.drawWrappedText(gtx, tf.Value, tf.region, tf.WrapWidth, tf.ScrollOffset, tf.CursorPosition, tf.SelectionStart, tf.SelectionEnd) } // runeCount returns the number of UTF-8 runes in s[:bytePos] (bytePos is a // byte offset, clamped to len(s)). A rune starts at an ASCII byte (<0x80) or a // multi-byte lead byte (>=0xC0); 0x80-0xBF are continuation bytes. func runeCount(s string, bytePos int) int { if bytePos > len(s) { bytePos = len(s) } n := 0 for i := 0; i < bytePos; i++ { b := s[i] if b < 0x80 || b >= 0xC0 { n++ } } return n } // NewTextField creates a visible multiline TextField. func NewTextField(id string, value string, region Region, wrapWidth Dp, scrollOffset Dp, cursorPos int, selStart, selEnd int, interactions []Interaction) TextField { return TextField{ id: id, region: region, visible: true, interactions: interactions, Value: value, Multiline: true, WordWrap: true, WrapWidth: wrapWidth, ScrollOffset: scrollOffset, CursorPosition: cursorPos, SelectionStart: selStart, SelectionEnd: selEnd, } } // 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) Type() string { return "listview" } 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 (removed as per request) /* 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) { // ListView clicks are simple taps, not coordinate-based. 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) Type() string { return "alphaindex" } 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) } // 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) Type() string { return "button" } 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) Type() string { return "searchbar" } 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 ClipRegion Region // Region to clip drawing visible bool interactions []Interaction Line int Column int Blinking bool Selection *Selection } func (c Cursor) NeedsClip() bool { return true } func (c Cursor) Type() string { return "cursor" } 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) } func (c Cursor) Draw(gtx layout.Context, r *Renderer) { // Clip to the cursor's allocated clip region (e.g., the editor text field) // to prevent drawing over status bars. var stack *clip.Stack if c.ClipRegion.W > 0 && c.ClipRegion.H > 0 { stack = new(clip.Stack) *stack = clip.Rect{ Min: image.Point{X: int(r.toPx(c.ClipRegion.X)), Y: int(r.toPx(c.ClipRegion.Y))}, Max: image.Point{X: int(r.toPx(c.ClipRegion.X + c.ClipRegion.W)), Y: int(r.toPx(c.ClipRegion.Y + c.ClipRegion.H))}, }.Op().Push(gtx.Ops) } // Draw a thin vertical bar (e.g., width 2dp, height 18dp) at the cursor's top-left position // instead of filling the entire region, which covers the text editor. cursorRegion := Region{ X: c.region.X, Y: c.region.Y, W: Dp(2), H: Dp(18), } r.drawBg(gtx, cursorRegion, Color{R: 0, G: 0, B: 0, A: 255}) if stack != nil { stack.Pop() } } func NewCursor(id string, region Region, line, col int, blinking bool) Cursor { return Cursor{ id: id, region: region, visible: true, Line: line, Column: col, Blinking: blinking, } } // 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) Type() string { return "mergehunk" } 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) Type() string { return "toast" } 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) Type() string { return "spacer" } 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 is a reference to a main-owned widget.Editor (see // Renderer.RegisterGioEditor). The element itself is pure data: the mutable // widget state lives in the renderer, which is owned by the main goroutine. type GioEditor struct { id string region Region visible bool interactions []Interaction } func (ge GioEditor) Type() string { return "gioeditor" } 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 registered for this element's ID. func (ge GioEditor) Draw(gtx layout.Context, r *Renderer) { ed, ok := r.GioEditor(ge.id) if !ok { return } ed.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 // NOTE: We pass the tap handler to RegisterClick which uses coordinates. var tapHandler func(any) for _, interaction := range ge.interactions { if interaction.Gesture == Tap { tapHandler = interaction.Handler break } } r.RegisterClick(gtx, ge.id, ge.region, tapHandler) ed.Layout(gtx, r.shp, font.Font{}, r.theme.FontSize, textColor, selectionColor) } // NewGioEditor creates a GioEditor element referencing the widget.Editor // registered with the renderer under id. func NewGioEditor(id string, region Region) GioEditor { return GioEditor{ id: id, region: region, visible: true, } } // --- 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(string) // 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 } // Point defines a 2D coordinate in device-independent pixels (Dp). // KeyEvent is a key press with its modifier state, delivered as the Data of // an InputEvent to a ui.KeyDown handler. It replaces passing a bare key.Name // (which discarded modifier state) so handlers can distinguish e.g. // shift+arrow (extend selection) from plain arrow (move cursor). type KeyEvent struct { Name key.Name Shift bool } type Point struct { X, Y Dp } // 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 }