~eliasnaur/gio-example

gio-example/markdown/main.go -rw-r--r-- 4.2 KiB
a9116b22Chris Waldon go.*: update to latest gio{,/x} and prune deps 2 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// SPDX-License-Identifier: Unlicense OR MIT

package main

// A simple Gio program. See https://gioui.org for more information.
//
// This program showcases markdown rendering.
// The left pane contains a text editor for inputing raw text.
// The right pane renders the resulting markdown document using richtext.
//
// Richtext is fully interactive, links can be clicked, hovered, and longpressed.

import (
	"image"
	"image/color"
	"log"
	"os"

	"gioui.org/app"
	"gioui.org/io/system"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/text"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
	"gioui.org/x/component"
	"gioui.org/x/markdown"
	"gioui.org/x/richtext"

	"gioui.org/font/gofont"
	"github.com/inkeliz/giohyperlink"
)

func main() {
	ui := UI{
		Window:   app.NewWindow(),
		Renderer: markdown.NewRenderer(),
		Shaper:   text.NewCache(gofont.Collection()),
		Theme:    NewTheme(gofont.Collection()),
		Resize:   component.Resize{Ratio: 0.5},
	}
	go func() {
		if err := ui.Loop(); err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	}()
	app.Main()
}

type (
	C = layout.Context
	D = layout.Dimensions
)

// UI specifies the user interface.
type UI struct {
	// External systems.
	// Window provides access to the OS window.
	Window *app.Window
	// Theme contains semantic style data. Extends `material.Theme`.
	Theme *Theme
	// Shaper cache of registered fonts.
	Shaper *text.Cache
	// Renderer tranforms raw text containing markdown into richtext.
	Renderer *markdown.Renderer

	// Core state.
	// Editor retains raw text in an edit buffer.
	Editor widget.Editor
	// TextState retains rich text interactions: clicks, hovers and longpresses.
	TextState richtext.InteractiveText
	// Resize state retains the split between the editor and the rendered text.
	component.Resize
}

// Theme contains semantic style data.
type Theme struct {
	// Base theme to extend.
	Base *material.Theme
	// cache of processed markdown.
	cache []richtext.SpanStyle
}

// NewTheme instantiates a theme, extending material theme.
func NewTheme(font []text.FontFace) *Theme {
	return &Theme{
		Base: material.NewTheme(font),
	}
}

// Loop drives the UI until the window is destroyed.
func (ui UI) Loop() error {
	var ops op.Ops
	for {
		e := <-ui.Window.Events()
		giohyperlink.ListenEvents(e)
		switch e := e.(type) {
		case system.DestroyEvent:
			return e.Err
		case system.FrameEvent:
			gtx := layout.NewContext(&ops, e)
			ui.Layout(gtx)
			e.Frame(gtx.Ops)
		}
	}
}

// Update processes events from the previous frame, updating state accordingly.
func (ui *UI) Update(gtx C) {
	for o, events := ui.TextState.Events(); o != nil; o, events = ui.TextState.Events() {
		for _, e := range events {
			switch e.Type {
			case richtext.Click:
				if url, ok := o.Get(markdown.MetadataURL).(string); ok && url != "" {
					if err := giohyperlink.Open(url); err != nil {
						// TODO(jfm): display UI element explaining the error to the user.
						log.Printf("error: opening hyperlink: %v", err)
					}
				}
			case richtext.Hover:
			case richtext.LongPress:
				log.Println("longpress")
				if url, ok := o.Get(markdown.MetadataURL).(string); ok && url != "" {
					ui.Window.Option(app.Title(url))
				}
			}
		}
	}
	for _, event := range ui.Editor.Events() {
		if _, ok := event.(widget.ChangeEvent); ok {
			var err error
			ui.Theme.cache, err = ui.Renderer.Render([]byte(ui.Editor.Text()))
			if err != nil {
				// TODO(jfm): display UI element explaining the error to the user.
				log.Printf("error: rendering markdown: %v", err)
			}
		}
	}
}

// Layout renders the current frame.
func (ui *UI) Layout(gtx C) D {
	ui.Update(gtx)
	return ui.Resize.Layout(gtx,
		func(gtx C) D {
			return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx C) D {
				return material.Editor(ui.Theme.Base, &ui.Editor, "markdown").Layout(gtx)
			})
		},
		func(gtx C) D {
			return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx C) D {
				return richtext.Text(&ui.TextState, ui.Shaper, ui.Theme.cache...).Layout(gtx)
			})
		},
		func(gtx C) D {
			rect := image.Rectangle{
				Max: image.Point{
					X: (gtx.Dp(unit.Dp(4))),
					Y: (gtx.Constraints.Max.Y),
				},
			}
			paint.FillShape(gtx.Ops, color.NRGBA{A: 200}, clip.Rect(rect).Op())
			return D{Size: rect.Max}
		},
	)
}