Initial InputEvent handling. Clickable "Wrap" text label.
This commit is contained in:
parent
50e9ca5b34
commit
1596dab72d
|
|
@ -58,6 +58,9 @@ func run(w *app.Window) error {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
currentElems := elems
|
currentElems := elems
|
||||||
renderer.Draw(gtx, currentElems)
|
renderer.Draw(gtx, currentElems)
|
||||||
|
if events := renderer.CheckGestures(e.Source); len(events) > 0 {
|
||||||
|
logic.InputChan() <- events
|
||||||
|
}
|
||||||
e.Frame(&ops)
|
e.Frame(&ops)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,6 @@ func (e ScaleEvent) apply(s *State) {
|
||||||
s.SetScale(e.Scale)
|
s.SetScale(e.Scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InputEvent represents a user input event routed to an element.
|
|
||||||
type InputEvent struct {
|
|
||||||
ElementID string
|
|
||||||
Type ui.InputType
|
|
||||||
Data any
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResultEvent represents a completed async task result.
|
// ResultEvent represents a completed async task result.
|
||||||
type ResultEvent struct {
|
type ResultEvent struct {
|
||||||
// Future: add result fields here
|
// Future: add result fields here
|
||||||
|
|
@ -50,18 +43,20 @@ type Logic struct {
|
||||||
state *State
|
state *State
|
||||||
configChan chan ConfigUpdate
|
configChan chan ConfigUpdate
|
||||||
frameChan chan []ui.Element
|
frameChan chan []ui.Element
|
||||||
inputChan chan []InputEvent
|
inputChan chan []ui.InputEvent
|
||||||
resultChan chan ResultEvent
|
resultChan chan ResultEvent
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLogic creates a new Logic instance.
|
// NewLogic creates a new Logic instance.
|
||||||
func NewLogic() *Logic {
|
func NewLogic() *Logic {
|
||||||
|
state := NewState()
|
||||||
|
TheState = state
|
||||||
return &Logic{
|
return &Logic{
|
||||||
state: NewState(),
|
state: state,
|
||||||
configChan: make(chan ConfigUpdate),
|
configChan: make(chan ConfigUpdate),
|
||||||
frameChan: make(chan []ui.Element),
|
frameChan: make(chan []ui.Element),
|
||||||
inputChan: make(chan []InputEvent),
|
inputChan: make(chan []ui.InputEvent),
|
||||||
resultChan: make(chan ResultEvent),
|
resultChan: make(chan ResultEvent),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +73,7 @@ func (l *Logic) FrameChan() <-chan []ui.Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
// InputChan returns the input channel for the logic goroutine.
|
// InputChan returns the input channel for the logic goroutine.
|
||||||
func (l *Logic) InputChan() chan<- []InputEvent {
|
func (l *Logic) InputChan() chan<- []ui.InputEvent {
|
||||||
return l.inputChan
|
return l.inputChan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,6 +82,9 @@ func (l *Logic) ResultChan() chan<- ResultEvent {
|
||||||
return l.resultChan
|
return l.resultChan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TheState is the global editor state, set once at startup.
|
||||||
|
var TheState *State
|
||||||
|
|
||||||
// Scale returns the current scale factor (pixels per DP).
|
// Scale returns the current scale factor (pixels per DP).
|
||||||
func (l *Logic) Scale() float32 {
|
func (l *Logic) Scale() float32 {
|
||||||
return l.state.Scale()
|
return l.state.Scale()
|
||||||
|
|
@ -99,7 +97,10 @@ func (l *Logic) Run() {
|
||||||
case update := <-l.configChan:
|
case update := <-l.configChan:
|
||||||
update.apply(l.state)
|
update.apply(l.state)
|
||||||
l.frameChan <- l.state.layout()
|
l.frameChan <- l.state.layout()
|
||||||
case <-l.inputChan:
|
case events := <-l.inputChan:
|
||||||
|
for _, evt := range events {
|
||||||
|
evt.Handler(evt.Data)
|
||||||
|
}
|
||||||
l.frameChan <- l.state.layout()
|
l.frameChan <- l.state.layout()
|
||||||
case <-l.resultChan:
|
case <-l.resultChan:
|
||||||
l.frameChan <- l.state.layout()
|
l.frameChan <- l.state.layout()
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ type State struct {
|
||||||
PixelWidth int // raw pixel width from Gio ConfigEvent
|
PixelWidth int // raw pixel width from Gio ConfigEvent
|
||||||
PixelHeight int // raw pixel height from Gio ConfigEvent
|
PixelHeight int // raw pixel height from Gio ConfigEvent
|
||||||
scale float32
|
scale float32
|
||||||
|
WordWrap bool
|
||||||
Elems []ui.Element
|
Elems []ui.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,12 +30,17 @@ func (s *State) Scale() float32 {
|
||||||
func (s *State) layout() []ui.Element {
|
func (s *State) layout() []ui.Element {
|
||||||
dpW := ui.ToDp(ui.Px(s.PixelWidth), s.scale)
|
dpW := ui.ToDp(ui.Px(s.PixelWidth), s.scale)
|
||||||
dpH := ui.ToDp(ui.Px(s.PixelHeight), s.scale)
|
dpH := ui.ToDp(ui.Px(s.PixelHeight), s.scale)
|
||||||
s.Elems = EditorLayout(dpW, dpH)
|
s.Elems = EditorLayout(dpW, dpH, s.WordWrap)
|
||||||
return s.Elems
|
return s.Elems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToggleWordWrap toggles the word wrap setting.
|
||||||
|
func ToggleWordWrap(data any) {
|
||||||
|
TheState.WordWrap = !TheState.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
// EditorLayout computes the element tree for the editor page.
|
// EditorLayout computes the element tree for the editor page.
|
||||||
func EditorLayout(screenWidth, screenHeight ui.Dp) []ui.Element {
|
func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element {
|
||||||
margin := ui.Dp(10)
|
margin := ui.Dp(10)
|
||||||
|
|
||||||
// --- Top bar: filename on row 1, icons on row 2 ---
|
// --- Top bar: filename on row 1, icons on row 2 ---
|
||||||
|
|
@ -49,7 +55,7 @@ func EditorLayout(screenWidth, screenHeight ui.Dp) []ui.Element {
|
||||||
ui.Color{R: 230, G: 230, B: 230, A: 255},
|
ui.Color{R: 230, G: 230, B: 230, A: 255},
|
||||||
[]ui.Element{
|
[]ui.Element{
|
||||||
// Row 1: filename
|
// Row 1: filename
|
||||||
ui.NewLabel("a very long file name that probably will eventually need to be truncated.txt", 14, ui.Region{X: 0, Y: ui.Dp(2), W: statusBarW, H: ui.Dp(20)}, ui.AlignStart),
|
ui.NewLabel("a very long file name that probably will eventually need to be truncated.txt", 14, ui.Region{X: 0, Y: ui.Dp(2), W: statusBarW, H: ui.Dp(20)}, ui.AlignStart, "", nil),
|
||||||
// Row 2: cut, copy, paste icons
|
// Row 2: cut, copy, paste icons
|
||||||
ui.NewIcon("cut", ui.Region{X: ui.Dp(0), Y: ui.Dp(28), W: ui.IconSize, H: ui.IconSize}, 0),
|
ui.NewIcon("cut", ui.Region{X: ui.Dp(0), Y: ui.Dp(28), W: ui.IconSize, H: ui.IconSize}, 0),
|
||||||
ui.NewIcon("copy", ui.Region{X: ui.Dp(48), Y: ui.Dp(28), W: ui.IconSize, H: ui.IconSize}, 0),
|
ui.NewIcon("copy", ui.Region{X: ui.Dp(48), Y: ui.Dp(28), W: ui.IconSize, H: ui.IconSize}, 0),
|
||||||
|
|
@ -66,13 +72,19 @@ func EditorLayout(screenWidth, screenHeight ui.Dp) []ui.Element {
|
||||||
H: bottomBarHeight,
|
H: bottomBarHeight,
|
||||||
}
|
}
|
||||||
bottomBarW := bottomBarRegion.W
|
bottomBarW := bottomBarRegion.W
|
||||||
|
wrapText := "Wrap: Off"
|
||||||
|
if wordWrap {
|
||||||
|
wrapText = "Wrap: On"
|
||||||
|
}
|
||||||
bottomBar := ui.NewContainer(
|
bottomBar := ui.NewContainer(
|
||||||
bottomBarRegion,
|
bottomBarRegion,
|
||||||
ui.Color{R: 230, G: 230, B: 230, A: 255},
|
ui.Color{R: 230, G: 230, B: 230, A: 255},
|
||||||
[]ui.Element{
|
[]ui.Element{
|
||||||
ui.NewLabel("Ln 47, Col 12", 12, ui.Region{X: 0, Y: ui.Dp(2), W: bottomBarW, H: ui.Dp(20)}, ui.AlignStart),
|
ui.NewLabel("Ln 47, Col 12", 12, ui.Region{X: 0, Y: ui.Dp(2), W: bottomBarW, H: ui.Dp(20)}, ui.AlignStart, "", nil),
|
||||||
ui.NewLabel("1024 / 50000", 12, ui.Region{X: 0, Y: ui.Dp(2), W: bottomBarW, H: ui.Dp(20)}, ui.AlignCenter),
|
ui.NewLabel("1024 / 50000", 12, ui.Region{X: 0, Y: ui.Dp(2), W: bottomBarW, H: ui.Dp(20)}, ui.AlignCenter, "", nil),
|
||||||
ui.NewLabel("Wrap: On", 12, ui.Region{X: 0, Y: ui.Dp(2), W: bottomBarW, H: ui.Dp(20)}, ui.AlignEnd),
|
ui.NewLabel(wrapText, 12, ui.Region{X: 0, Y: ui.Dp(2), W: bottomBarW, H: ui.Dp(20)}, ui.AlignEnd, "wrap", []ui.Interaction{
|
||||||
|
{Gesture: ui.Tap, Handler: ToggleWordWrap},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ type Label struct {
|
||||||
id string
|
id string
|
||||||
region Region
|
region Region
|
||||||
visible bool
|
visible bool
|
||||||
|
interactions []Interaction
|
||||||
Text string
|
Text string
|
||||||
Align TextAlign
|
Align TextAlign
|
||||||
FontSize unit.Sp // 0 = theme default
|
FontSize unit.Sp // 0 = theme default
|
||||||
|
|
@ -65,20 +66,23 @@ type Label struct {
|
||||||
|
|
||||||
func (l Label) Region() Region { return l.region }
|
func (l Label) Region() Region { return l.region }
|
||||||
func (l Label) Visible() bool { return l.visible }
|
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) {
|
func (l Label) Draw(gtx layout.Context, r *Renderer) {
|
||||||
col := l.Color
|
col := l.Color
|
||||||
if col == (Color{}) {
|
if col == (Color{}) {
|
||||||
col = Color{R: 0, G: 0, B: 0, A: 255}
|
col = Color{R: 0, G: 0, B: 0, A: 255}
|
||||||
}
|
}
|
||||||
r.drawText(gtx, l.Text, l.FontSize, l.region, l.Align, col)
|
r.drawText(gtx, l.Text, l.FontSize, l.region, l.Align, col, l.id)
|
||||||
}
|
}
|
||||||
func (l Label) ID() string { return l.id }
|
func (l Label) ID() string { return l.id }
|
||||||
|
|
||||||
// NewLabel creates a visible Label element.
|
// NewLabel creates a visible Label element.
|
||||||
func NewLabel(text string, fontSize unit.Sp, region Region, align TextAlign) Label {
|
func NewLabel(text string, fontSize unit.Sp, region Region, align TextAlign, id string, interactions []Interaction) Label {
|
||||||
return Label{
|
return Label{
|
||||||
|
id: id,
|
||||||
region: region,
|
region: region,
|
||||||
visible: true,
|
visible: true,
|
||||||
|
interactions: interactions,
|
||||||
Text: text,
|
Text: text,
|
||||||
FontSize: fontSize,
|
FontSize: fontSize,
|
||||||
Align: align,
|
Align: align,
|
||||||
|
|
@ -90,12 +94,14 @@ type Icon struct {
|
||||||
id string
|
id string
|
||||||
region Region
|
region Region
|
||||||
visible bool
|
visible bool
|
||||||
|
interactions []Interaction
|
||||||
Name string // icon name, e.g. "cut"
|
Name string // icon name, e.g. "cut"
|
||||||
Size Dp // 0 = default icon size
|
Size Dp // 0 = default icon size
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i Icon) Region() Region { return i.region }
|
func (i Icon) Region() Region { return i.region }
|
||||||
func (i Icon) Visible() bool { return i.visible }
|
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) {
|
func (i Icon) Draw(gtx layout.Context, r *Renderer) {
|
||||||
img := r.icon(i.Name)
|
img := r.icon(i.Name)
|
||||||
if img == nil {
|
if img == nil {
|
||||||
|
|
@ -125,6 +131,7 @@ type TextField struct {
|
||||||
id string
|
id string
|
||||||
region Region
|
region Region
|
||||||
visible bool
|
visible bool
|
||||||
|
interactions []Interaction
|
||||||
Value string
|
Value string
|
||||||
Placeholder string
|
Placeholder string
|
||||||
Focused bool
|
Focused bool
|
||||||
|
|
@ -138,6 +145,7 @@ type TextField struct {
|
||||||
func (tf TextField) Region() Region { return tf.region }
|
func (tf TextField) Region() Region { return tf.region }
|
||||||
func (tf TextField) Visible() bool { return tf.visible }
|
func (tf TextField) Visible() bool { return tf.visible }
|
||||||
func (tf TextField) ID() string { return tf.id }
|
func (tf TextField) ID() string { return tf.id }
|
||||||
|
func (tf TextField) Interactions() []Interaction { return tf.interactions }
|
||||||
|
|
||||||
// Line represents a single line of text in a multiline TextField.
|
// Line represents a single line of text in a multiline TextField.
|
||||||
type Line struct {
|
type Line struct {
|
||||||
|
|
@ -150,6 +158,7 @@ type ListView struct {
|
||||||
id string
|
id string
|
||||||
region Region
|
region Region
|
||||||
visible bool
|
visible bool
|
||||||
|
interactions []Interaction
|
||||||
Items []ListItem
|
Items []ListItem
|
||||||
ScrollOffset int
|
ScrollOffset int
|
||||||
Selected int
|
Selected int
|
||||||
|
|
@ -158,6 +167,7 @@ type ListView struct {
|
||||||
func (lv ListView) Region() Region { return lv.region }
|
func (lv ListView) Region() Region { return lv.region }
|
||||||
func (lv ListView) Visible() bool { return lv.visible }
|
func (lv ListView) Visible() bool { return lv.visible }
|
||||||
func (lv ListView) ID() string { return lv.id }
|
func (lv ListView) ID() string { return lv.id }
|
||||||
|
func (lv ListView) Interactions() []Interaction { return lv.interactions }
|
||||||
|
|
||||||
// ListItem is a single entry in a ListView.
|
// ListItem is a single entry in a ListView.
|
||||||
type ListItem struct {
|
type ListItem struct {
|
||||||
|
|
@ -171,6 +181,7 @@ type AlphaIndex struct {
|
||||||
id string
|
id string
|
||||||
region Region
|
region Region
|
||||||
visible bool
|
visible bool
|
||||||
|
interactions []Interaction
|
||||||
Letters []string
|
Letters []string
|
||||||
ActiveLetter string
|
ActiveLetter string
|
||||||
}
|
}
|
||||||
|
|
@ -178,6 +189,7 @@ type AlphaIndex struct {
|
||||||
func (ai AlphaIndex) Region() Region { return ai.region }
|
func (ai AlphaIndex) Region() Region { return ai.region }
|
||||||
func (ai AlphaIndex) Visible() bool { return ai.visible }
|
func (ai AlphaIndex) Visible() bool { return ai.visible }
|
||||||
func (ai AlphaIndex) ID() string { return ai.id }
|
func (ai AlphaIndex) ID() string { return ai.id }
|
||||||
|
func (ai AlphaIndex) Interactions() []Interaction { return ai.interactions }
|
||||||
|
|
||||||
// NewAlphaIndex creates a visible AlphaIndex element.
|
// NewAlphaIndex creates a visible AlphaIndex element.
|
||||||
func NewAlphaIndex(region Region, letters []string) AlphaIndex {
|
func NewAlphaIndex(region Region, letters []string) AlphaIndex {
|
||||||
|
|
@ -193,6 +205,7 @@ type Button struct {
|
||||||
id string
|
id string
|
||||||
region Region
|
region Region
|
||||||
visible bool
|
visible bool
|
||||||
|
interactions []Interaction
|
||||||
Text string
|
Text string
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Primary bool
|
Primary bool
|
||||||
|
|
@ -200,9 +213,10 @@ type Button struct {
|
||||||
|
|
||||||
func (b Button) Region() Region { return b.region }
|
func (b Button) Region() Region { return b.region }
|
||||||
func (b Button) Visible() bool { return b.visible }
|
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) {
|
func (b Button) Draw(gtx layout.Context, r *Renderer) {
|
||||||
col := Color{R: 0, G: 0, B: 0, A: 255}
|
col := Color{R: 0, G: 0, B: 0, A: 255}
|
||||||
r.drawText(gtx, b.Text, r.theme.FontSize, b.region, AlignStart, col)
|
r.drawText(gtx, b.Text, r.theme.FontSize, b.region, AlignStart, col, b.id)
|
||||||
}
|
}
|
||||||
func (b Button) ID() string { return b.id }
|
func (b Button) ID() string { return b.id }
|
||||||
|
|
||||||
|
|
@ -222,6 +236,7 @@ type SearchBar struct {
|
||||||
id string
|
id string
|
||||||
region Region
|
region Region
|
||||||
visible bool
|
visible bool
|
||||||
|
interactions []Interaction
|
||||||
Query string
|
Query string
|
||||||
Match int
|
Match int
|
||||||
Total int
|
Total int
|
||||||
|
|
@ -231,6 +246,7 @@ type SearchBar struct {
|
||||||
func (sb SearchBar) Region() Region { return sb.region }
|
func (sb SearchBar) Region() Region { return sb.region }
|
||||||
func (sb SearchBar) Visible() bool { return sb.visible }
|
func (sb SearchBar) Visible() bool { return sb.visible }
|
||||||
func (sb SearchBar) ID() string { return sb.id }
|
func (sb SearchBar) ID() string { return sb.id }
|
||||||
|
func (sb SearchBar) Interactions() []Interaction { return sb.interactions }
|
||||||
|
|
||||||
// NewSearchBar creates a visible SearchBar element.
|
// NewSearchBar creates a visible SearchBar element.
|
||||||
func NewSearchBar(region Region, query string, match, total int, forward bool) SearchBar {
|
func NewSearchBar(region Region, query string, match, total int, forward bool) SearchBar {
|
||||||
|
|
@ -249,6 +265,7 @@ type Cursor struct {
|
||||||
id string
|
id string
|
||||||
region Region
|
region Region
|
||||||
visible bool
|
visible bool
|
||||||
|
interactions []Interaction
|
||||||
Line int
|
Line int
|
||||||
Column int
|
Column int
|
||||||
Blinking bool
|
Blinking bool
|
||||||
|
|
@ -258,6 +275,7 @@ type Cursor struct {
|
||||||
func (c Cursor) Region() Region { return c.region }
|
func (c Cursor) Region() Region { return c.region }
|
||||||
func (c Cursor) Visible() bool { return c.visible }
|
func (c Cursor) Visible() bool { return c.visible }
|
||||||
func (c Cursor) ID() string { return c.id }
|
func (c Cursor) ID() string { return c.id }
|
||||||
|
func (c Cursor) Interactions() []Interaction { return c.interactions }
|
||||||
|
|
||||||
// Selection represents a text selection range.
|
// Selection represents a text selection range.
|
||||||
type Selection struct {
|
type Selection struct {
|
||||||
|
|
@ -270,6 +288,7 @@ type MergeHunk struct {
|
||||||
id string
|
id string
|
||||||
region Region
|
region Region
|
||||||
visible bool
|
visible bool
|
||||||
|
interactions []Interaction
|
||||||
HunkNumber int
|
HunkNumber int
|
||||||
TotalHunks int
|
TotalHunks int
|
||||||
LineRange string
|
LineRange string
|
||||||
|
|
@ -282,6 +301,7 @@ type MergeHunk struct {
|
||||||
func (mh MergeHunk) Region() Region { return mh.region }
|
func (mh MergeHunk) Region() Region { return mh.region }
|
||||||
func (mh MergeHunk) Visible() bool { return mh.visible }
|
func (mh MergeHunk) Visible() bool { return mh.visible }
|
||||||
func (mh MergeHunk) ID() string { return mh.id }
|
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.
|
// HunkResolution represents the resolution state of a merge hunk.
|
||||||
type HunkResolution int
|
type HunkResolution int
|
||||||
|
|
@ -298,6 +318,7 @@ type Toast struct {
|
||||||
id string
|
id string
|
||||||
region Region
|
region Region
|
||||||
visible bool
|
visible bool
|
||||||
|
interactions []Interaction
|
||||||
Text string
|
Text string
|
||||||
Timeout int // milliseconds
|
Timeout int // milliseconds
|
||||||
}
|
}
|
||||||
|
|
@ -305,6 +326,7 @@ type Toast struct {
|
||||||
func (t Toast) Region() Region { return t.region }
|
func (t Toast) Region() Region { return t.region }
|
||||||
func (t Toast) Visible() bool { return t.visible }
|
func (t Toast) Visible() bool { return t.visible }
|
||||||
func (t Toast) ID() string { return t.id }
|
func (t Toast) ID() string { return t.id }
|
||||||
|
func (t Toast) Interactions() []Interaction { return t.interactions }
|
||||||
|
|
||||||
// NewToast creates a visible Toast element.
|
// NewToast creates a visible Toast element.
|
||||||
func NewToast(region Region, text string, timeout int) Toast {
|
func NewToast(region Region, text string, timeout int) Toast {
|
||||||
|
|
@ -321,11 +343,13 @@ type Spacer struct {
|
||||||
id string
|
id string
|
||||||
region Region
|
region Region
|
||||||
visible bool
|
visible bool
|
||||||
|
interactions []Interaction
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Spacer) Region() Region { return s.region }
|
func (s Spacer) Region() Region { return s.region }
|
||||||
func (s Spacer) Visible() bool { return s.visible }
|
func (s Spacer) Visible() bool { return s.visible }
|
||||||
func (s Spacer) ID() string { return s.id }
|
func (s Spacer) ID() string { return s.id }
|
||||||
|
func (s Spacer) Interactions() []Interaction { return s.interactions }
|
||||||
|
|
||||||
// NewSpacer creates a visible Spacer element.
|
// NewSpacer creates a visible Spacer element.
|
||||||
func NewSpacer(height Dp) Spacer {
|
func NewSpacer(height Dp) Spacer {
|
||||||
|
|
@ -368,10 +392,21 @@ const (
|
||||||
KeyUp
|
KeyUp
|
||||||
)
|
)
|
||||||
|
|
||||||
// InputEvent represents a user input event routed to an element.
|
// 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 {
|
type InputEvent struct {
|
||||||
ElementID string
|
Handler func(any)
|
||||||
Type InputType
|
|
||||||
Data any
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import (
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
|
"gioui.org/gesture"
|
||||||
|
"gioui.org/io/input"
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
|
|
@ -28,17 +30,30 @@ type ScaleProvider interface {
|
||||||
Scale() float32
|
Scale() float32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clickReg pairs a gesture.Click with its handler.
|
||||||
|
type clickReg struct {
|
||||||
|
click *gesture.Click
|
||||||
|
handler func(any)
|
||||||
|
}
|
||||||
|
|
||||||
// Renderer consumes a slice of elements and draws them.
|
// Renderer consumes a slice of elements and draws them.
|
||||||
type Renderer struct {
|
type Renderer struct {
|
||||||
theme Theme
|
theme Theme
|
||||||
shp *text.Shaper
|
shp *text.Shaper
|
||||||
scale ScaleProvider
|
scale ScaleProvider
|
||||||
icons map[string]image.Image
|
icons map[string]image.Image
|
||||||
|
clicks map[string]clickReg
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Renderer.
|
// New creates a new Renderer.
|
||||||
func New(th Theme, shp *text.Shaper, scale ScaleProvider) *Renderer {
|
func New(th Theme, shp *text.Shaper, scale ScaleProvider) *Renderer {
|
||||||
r := &Renderer{theme: th, shp: shp, scale: scale, icons: make(map[string]image.Image)}
|
r := &Renderer{
|
||||||
|
theme: th,
|
||||||
|
shp: shp,
|
||||||
|
scale: scale,
|
||||||
|
icons: make(map[string]image.Image),
|
||||||
|
clicks: make(map[string]clickReg),
|
||||||
|
}
|
||||||
r.loadIcons()
|
r.loadIcons()
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +108,45 @@ func (r *Renderer) Draw(gtx layout.Context, elems []Element) {
|
||||||
clipRect.Pop()
|
clipRect.Pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// registerInteraction registers a gesture for an element.
|
||||||
|
// Only Tap is supported for now.
|
||||||
|
func (r *Renderer) registerInteraction(id string, interaction Interaction, gtx layout.Context, region Region) {
|
||||||
|
if interaction.Gesture != Tap {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reg, ok := r.clicks[id]
|
||||||
|
if !ok {
|
||||||
|
reg = clickReg{click: &gesture.Click{}}
|
||||||
|
r.clicks[id] = reg
|
||||||
|
}
|
||||||
|
// click.Add() is now called inside drawText, within the text clip
|
||||||
|
reg.handler = interaction.Handler
|
||||||
|
r.clicks[id] = reg
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckGestures checks all registered gestures and returns any events.
|
||||||
|
func (r *Renderer) CheckGestures(q input.Source) []InputEvent {
|
||||||
|
var events []InputEvent
|
||||||
|
for _, reg := range r.clicks {
|
||||||
|
evt, ok := reg.click.Update(q)
|
||||||
|
if ok && evt.Kind == gesture.KindClick {
|
||||||
|
events = append(events, InputEvent{
|
||||||
|
Handler: reg.handler,
|
||||||
|
Data: evt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return events
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Renderer) drawElement(gtx layout.Context, e Element) {
|
func (r *Renderer) drawElement(gtx layout.Context, e Element) {
|
||||||
|
// Register interactions before drawing
|
||||||
|
if interactive, ok := e.(Interactive); ok {
|
||||||
|
for _, interaction := range interactive.Interactions() {
|
||||||
|
r.registerInteraction(interactive.ID(), interaction, gtx, e.Region())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
reg := e.Region()
|
reg := e.Region()
|
||||||
|
|
||||||
if container, ok := e.(Container); ok {
|
if container, ok := e.(Container); ok {
|
||||||
|
|
@ -125,7 +178,7 @@ func (r *Renderer) drawBg(gtx layout.Context, reg Region, col Color) {
|
||||||
bgClip.Pop()
|
bgClip.Pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Renderer) drawText(gtx layout.Context, str string, size unit.Sp, reg Region, align TextAlign, col Color) {
|
func (r *Renderer) drawText(gtx layout.Context, str string, size unit.Sp, reg Region, align TextAlign, col Color, id string) {
|
||||||
if str == "" {
|
if str == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -136,36 +189,40 @@ func (r *Renderer) drawText(gtx layout.Context, str string, size unit.Sp, reg Re
|
||||||
MaxLines: 1,
|
MaxLines: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
if align == AlignStart {
|
// Measure text width via glyph iteration (consumes iterator)
|
||||||
// No measurement needed — draw at reg.X directly
|
|
||||||
r.shp.LayoutString(params, str)
|
|
||||||
r.drawLineText(gtx, reg.X, reg.Y, col)
|
|
||||||
} else {
|
|
||||||
// Phase 1: measure text width via glyph iteration (consumes iterator)
|
|
||||||
r.shp.LayoutString(params, str)
|
r.shp.LayoutString(params, str)
|
||||||
var totalAdvance fixed.Int26_6
|
var totalAdvance fixed.Int26_6
|
||||||
for g, ok := r.shp.NextGlyph(); ok; g, ok = r.shp.NextGlyph() {
|
for g, ok := r.shp.NextGlyph(); ok; g, ok = r.shp.NextGlyph() {
|
||||||
totalAdvance += g.Advance
|
totalAdvance += g.Advance
|
||||||
}
|
}
|
||||||
// totalAdvance>>6 is in device pixels (shaper outputs device pixels).
|
|
||||||
// Divide by scale to convert to Dp so it matches region coordinate space.
|
|
||||||
textW := Dp(float32(totalAdvance>>6) / r.scale.Scale())
|
textW := Dp(float32(totalAdvance>>6) / r.scale.Scale())
|
||||||
|
|
||||||
// Compute aligned X position
|
// Compute aligned X position
|
||||||
var drawX Dp
|
var drawX Dp
|
||||||
switch align {
|
switch align {
|
||||||
|
case AlignStart:
|
||||||
|
drawX = reg.X
|
||||||
case AlignCenter:
|
case AlignCenter:
|
||||||
drawX = reg.X + (reg.W - textW) / 2
|
drawX = reg.X + (reg.W - textW) / 2
|
||||||
case AlignEnd:
|
case AlignEnd:
|
||||||
drawX = reg.X + reg.W - textW
|
drawX = reg.X + reg.W - textW
|
||||||
default:
|
|
||||||
drawX = reg.X
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: layout again (iterator consumed) and draw
|
// Clip to just the text area, register click inside the clip
|
||||||
|
textClip := clip.Rect{
|
||||||
|
Min: image.Point{X: int(r.toPx(drawX)), Y: int(r.toPx(reg.Y))},
|
||||||
|
Max: image.Point{X: int(r.toPx(drawX + textW)), Y: int(r.toPx(reg.Y + reg.H))},
|
||||||
|
}.Push(gtx.Ops)
|
||||||
|
|
||||||
|
if cr, ok := r.clicks[id]; ok && cr.click != nil {
|
||||||
|
cr.click.Add(gtx.Ops)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout again (iterator consumed) and draw
|
||||||
r.shp.LayoutString(params, str)
|
r.shp.LayoutString(params, str)
|
||||||
r.drawLineText(gtx, drawX, reg.Y, col)
|
r.drawLineText(gtx, drawX, reg.Y, col)
|
||||||
}
|
|
||||||
|
textClip.Pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawLineText iterates laid-out glyphs and draws each line using op.Record for clipping safety.
|
// drawLineText iterates laid-out glyphs and draws each line using op.Record for clipping safety.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user