- Fix render.go: replace broken material.Label with shaper-based text rendering (LayoutString + Shape + glyph iteration) per Gio's paintGlyph - Fix render.go: only apply clip rects at container boundaries; leaf elements have zero-size regions that were clipping all text out - Fix render.go: call r.drawElement recursively for container children so nested containers work and clips are applied correctly - Fix render.go: add drawLineText/drawLine helpers matching Gio's paintGlyph offset calculation (x + first.X, y + first.Y) - Fix element.go: Button.Draw now uses r.drawText instead of duplicate broken material.Label code; Label defaults to black when color is unset - Fix state.go: container children use relative coordinates instead of mixed screen-space/container-relative positions - Fix main.go: ConfigEvent carries raw pixel dimensions only; ScaleEvent carries scale only; layout is computed once per frame using both, eliminating the infinite Invalidate() loop
386 lines
9.0 KiB
Go
386 lines
9.0 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"gioui.org/layout"
|
|
"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
|
|
}
|
|
|
|
// 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 {
|
|
fmt.Printf("[DEBUG NewContainer] region={X=%.0f Y=%.0f W=%.0f H=%.0f} children=%d\n", region.X, region.Y, region.W, region.H, len(children))
|
|
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
|
|
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) Draw(gtx layout.Context, r *Renderer) {
|
|
fmt.Printf("[DEBUG Label.Draw] text=%q region={X=%.0f Y=%.0f}\n", l.Text, l.region.X, l.region.Y)
|
|
col := l.Color
|
|
if col == (Color{}) {
|
|
col = Color{R: 0, G: 0, B: 0, A: 255}
|
|
}
|
|
r.drawText(gtx, l.Text, l.FontSize, l.region, col)
|
|
}
|
|
func (l Label) ID() string { return l.id }
|
|
|
|
// NewLabel creates a visible Label element.
|
|
func NewLabel(text string, fontSize unit.Sp, region Region) Label {
|
|
return Label{
|
|
region: region,
|
|
visible: true,
|
|
Text: text,
|
|
FontSize: fontSize,
|
|
}
|
|
}
|
|
|
|
// Icon displays a named icon image.
|
|
type Icon struct {
|
|
id string
|
|
region Region
|
|
visible bool
|
|
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) Draw(gtx layout.Context, r *Renderer) {
|
|
fmt.Printf("[DEBUG Icon.Draw] name=%q icon=%v region={X=%.0f Y=%.0f}\n", i.Name, r.icon(i.Name) != nil, i.region.X, i.region.Y)
|
|
img := r.icon(i.Name)
|
|
if img == nil {
|
|
return
|
|
}
|
|
size := i.Size
|
|
if size == 0 {
|
|
size = Dp(16)
|
|
}
|
|
r.drawPng(gtx, img, i.region, size, i.Size)
|
|
}
|
|
func (i Icon) ID() string { return i.id }
|
|
|
|
// NewIcon creates a visible Icon element.
|
|
func NewIcon(name string, region Region, size Dp) Icon {
|
|
return Icon{
|
|
region: region,
|
|
visible: true,
|
|
Name: name,
|
|
Size: size,
|
|
}
|
|
}
|
|
|
|
// TextField accepts text input or displays multiline text.
|
|
type TextField struct {
|
|
id string
|
|
region Region
|
|
visible bool
|
|
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 }
|
|
|
|
// 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
|
|
Items []ListItem
|
|
ScrollOffset int
|
|
Selected int
|
|
}
|
|
|
|
func (lv ListView) Region() Region { return lv.region }
|
|
func (lv ListView) Visible() bool { return lv.visible }
|
|
func (lv ListView) ID() string { return lv.id }
|
|
|
|
// 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
|
|
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 }
|
|
|
|
// 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
|
|
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) 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, col)
|
|
}
|
|
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
|
|
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 }
|
|
|
|
// 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
|
|
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 }
|
|
|
|
// 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
|
|
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 }
|
|
|
|
// 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
|
|
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 }
|
|
|
|
// 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
|
|
}
|
|
|
|
func (s Spacer) Region() Region { return s.region }
|
|
func (s Spacer) Visible() bool { return s.visible }
|
|
func (s Spacer) ID() string { return s.id }
|
|
|
|
// NewSpacer creates a visible Spacer element.
|
|
func NewSpacer(height Dp) Spacer {
|
|
return Spacer{
|
|
region: Region{H: height},
|
|
visible: true,
|
|
}
|
|
}
|
|
|
|
// --- Utility types ---
|
|
|
|
// 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
|
|
)
|
|
|
|
// InputEvent represents a user input event routed to an element.
|
|
type InputEvent struct {
|
|
ElementID string
|
|
Type InputType
|
|
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
|
|
}
|