forked from go/golangs_learn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
168 lines
4.2 KiB
168 lines
4.2 KiB
// 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 := o.Get(markdown.MetadataURL); 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")
|
|
ui.Window.Option(app.Title(o.Get(markdown.MetadataURL)))
|
|
}
|
|
}
|
|
}
|
|
for _, event := range ui.Editor.Events() {
|
|
if _, ok := event.(widget.ChangeEvent); ok {
|
|
var err error
|
|
ui.Theme.cache, err = ui.Renderer.Render(ui.Theme.Base, []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.Px(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}
|
|
},
|
|
)
|
|
}
|
|
|