~eliasnaur/gio-patches

gio-x: Add richtext v1 PROPOSED

ghost08: 1
 Add richtext

 3 files changed, 210 insertions(+), 11 deletions(-)
Thanks for merging :)
I think, that the only approach to build a parser is by
Rob Pike (https://www.youtube.com/watch?v=HxaD_trXwRE).

And as for the markdown part. I would make a intermediate
data structure from which it will be converted to richtext.


Great, maybe before you begin, I was thinking about
adding some new parameters to the TextObjects. 

Like BackgroundColor, BorderWidth, BorderColor, maybe
Padding. That way it would be easier to print markdown's
quotes, code blocks, inline code ...

What do you think?

Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~eliasnaur/gio-patches/patches/22963/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH gio-x] Add richtext Export this patch

Signed-off-by: ghost08 <magyarv@protonmail.com>
---
 README.md            |  23 ++---
 richtext/README.md   |   3 +
 richtext/richtext.go | 195 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 210 insertions(+), 11 deletions(-)
 create mode 100644 richtext/README.md
 create mode 100644 richtext/richtext.go

diff --git a/README.md b/README.md
index 8b00352..3b14d20 100644
--- a/README.md
+++ b/README.md
@@ -6,17 +6,18 @@ This repository hosts `gioui.org/x`. Two kinds of package exist in this namespac

This table describes the current status of each package in `gioui.org/x`:

| Name        | Purpose                                | Intended for core? | Non-core dependencies? | API Stability |
| ---         | ---                                    | ---                | ---                    | ---           |
| colorpicker | Widgets for choosing colors            | uncertain          | no                     | unstable      |
| component   | Material.io components                 | uncertain          | no                     | unstable      |
| eventx      | Event management tools                 | yes                | no                     | unstable      |
| haptic      | Haptic feedback for mobile devices     | no                 | yes                    | unstable      |
| notify      | Background notifications               | no                 | yes                    | unstable      |
| outlay      | Extra layouts                          | yes                | no                     | unstable      |
| pref        | Query user/device preferences          | no                 | yes                    | unstable      |
| profiling   | Gio render performance recording tools | uncertain          | no                     | unstable      |
| scroll      | Scrollbar widget for Gio               | yes                | no                     | unstable      |
| Name        | Purpose                                     | Intended for core? | Non-core dependencies? | API Stability |
| ----------- | ------------------------------------------- | ------------------ | ---------------------- | ------------- |
| colorpicker | Widgets for choosing colors                 | uncertain          | no                     | unstable      |
| component   | Material.io components                      | uncertain          | no                     | unstable      |
| eventx      | Event management tools                      | yes                | no                     | unstable      |
| haptic      | Haptic feedback for mobile devices          | no                 | yes                    | unstable      |
| notify      | Background notifications                    | no                 | yes                    | unstable      |
| outlay      | Extra layouts                               | yes                | no                     | unstable      |
| pref        | Query user/device preferences               | no                 | yes                    | unstable      |
| profiling   | Gio render performance recording tools      | uncertain          | no                     | unstable      |
| scroll      | Scrollbar widget for Gio                    | yes                | no                     | unstable      |
| richtext    | Printing text objects with different styles | uncertain          | no                     | unstable      |

## Contributing

diff --git a/richtext/README.md b/richtext/README.md
new file mode 100644
index 0000000..0f2248b
--- /dev/null
+++ b/richtext/README.md
@@ -0,0 +1,3 @@
# richtext

Provides a widget that renders text in different styles.
diff --git a/richtext/richtext.go b/richtext/richtext.go
new file mode 100644
index 0000000..e8fff84
--- /dev/null
+++ b/richtext/richtext.go
@@ -0,0 +1,195 @@
package richtext

import (
	"image"
	"image/color"

	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/paint"
	"gioui.org/text"
	"gioui.org/unit"
	"golang.org/x/image/math/fixed"
)

//TextObjects represents the whole richtext widget
type TextObjects []*TextObject

//TextObject is one of the objects in the richtext widget
type TextObject struct {
	Font      text.Font
	Size      unit.Value
	Color     color.NRGBA
	Content   string
	Clickable bool
	metadata  map[string]string

	origin     *TextObject
	textLayout text.Layout
	xOffset    int
	size       image.Point
	clicked    int
}

//Layout prints out the text at specified offset
func (to *TextObject) Layout(gtx layout.Context, s text.Shaper, off image.Point) layout.Dimensions {
	stack := op.Save(gtx.Ops)
	paint.ColorOp{Color: to.Color}.Add(gtx.Ops)
	op.Offset(layout.FPt(off)).Add(gtx.Ops)
	s.Shape(to.Font, fixed.I(gtx.Px(to.Size)), to.textLayout).Add(gtx.Ops)
	paint.PaintOp{}.Add(gtx.Ops)
	stack.Load()
	return layout.Dimensions{Size: to.size}
}

func (to *TextObject) Clicked() bool {
	if to.clicked <= 0 {
		return false
	}
	to.clicked--
	return true
}

func (to *TextObject) updateClicks(gtx layout.Context) {
	if !to.Clickable {
		return
	}
	for _, e := range gtx.Events(origin(to)) {
		switch p := e.(type) {
		case pointer.Event:
			switch p.Type {
			case pointer.Release:
				to.clicked++
			}
		}
	}
}

func (to *TextObject) SetMetadata(key, value string) {
	if to.metadata == nil {
		to.metadata = make(map[string]string)
	}
	to.metadata[key] = value
}

func (to *TextObject) GetMetadata(key string) string {
	if to.metadata == nil {
		return ""
	}
	return to.metadata[key]
}

//Layout prints out the rich text widget
func (tos TextObjects) Layout(gtx layout.Context, s text.Shaper) layout.Dimensions {
	var tosDims layout.Dimensions
	var lineDims image.Point
	oi := &objectIterator{objs: tos}
	//TextObjects (or it's parts) on a single line
	var lineObjects []*TextObject

	for to := oi.Next(); to != nil; to = oi.Next() {
		maxWidth := gtx.Constraints.Max.X - lineDims.X
		//layout the string using the maxWidthe remaining for the line
		lines := s.LayoutString(to.Font, fixed.I(gtx.Px(to.Size)), maxWidth, to.Content)
		//using just the first line
		first := lines[0]
		lineWidth := first.Width.Ceil()
		lineHeight := (first.Ascent + first.Descent).Ceil()

		//getting the used text in the line and calculating the dimensions
		to.textLayout = first.Layout
		to.xOffset = lineDims.X
		to.size = image.Point{X: lineWidth, Y: lineHeight}
		lineObjects = append(lineObjects, to)

		//calculating the X and the max of the Y of the line
		lineDims.X += lineWidth
		if lineDims.Y < lineHeight {
			lineDims.Y = lineHeight
		}
		//the width of the whole richtext object
		if tosDims.Size.X < lineDims.X {
			tosDims.Size.X = lineDims.X
		}

		//if we are going to break the line, or we are at the end of the richtext
		if len(lines) > 1 || oi.Empty() {
			//we print the line
			for _, obj := range lineObjects {
				off := image.Point{
					X: obj.xOffset,
					Y: tosDims.Size.Y + lineDims.Y,
				}
				obj.Layout(gtx, s, off)
				if !obj.Clickable {
					continue
				}
				to.updateClicks(gtx)
				stack := op.Save(gtx.Ops)
				clickableOffset := image.Point{X: obj.xOffset, Y: tosDims.Size.Y}
				op.Offset(layout.FPt(clickableOffset)).Add(gtx.Ops)
				pointer.Rect(image.Rectangle{Max: obj.size}).Add(gtx.Ops)
				pointer.InputOp{Tag: origin(obj), Types: pointer.Release}.Add(gtx.Ops)
				pointer.CursorNameOp{Name: pointer.CursorPointer}.Add(gtx.Ops)
				stack.Load()
			}
			//we printed these objectss, so we get rid of them
			lineObjects = nil
			tosDims.Size.Y += lineDims.Y
		}
		if len(lines) > 1 {
			//add the rest of the TextObject to be printed on a new line
			//this puts it to the front of the objectIterator
			oi.Add(&TextObject{
				origin:    origin(to),
				Font:      to.Font,
				Size:      to.Size,
				Color:     to.Color,
				Clickable: to.Clickable,
				Content:   to.Content[len(first.Layout.Text):],
			})
			lineDims.X = 0
			lineDims.Y = 0
		}
	}

	return tosDims
}

type objectIterator struct {
	objs []*TextObject
}

func (oi *objectIterator) Add(to *TextObject) {
	oi.objs = append([]*TextObject{to}, oi.objs...)
}

func (oi *objectIterator) Next() *TextObject {
	if len(oi.objs) == 0 {
		return nil
	}
	next := oi.objs[0]
	oi.objs = oi.objs[1:]
	return next
}

func (oi *objectIterator) Empty() bool {
	return len(oi.objs) == 0
}

func (tos TextObjects) Clicked() *TextObject {
	for _, to := range tos {
		if to.Clicked() {
			return to
		}
	}
	return nil
}

func origin(to *TextObject) *TextObject {
	if to.origin == nil {
		return to
	}
	return to.origin
}
-- 
2.31.1
Merged! I tweaked the commit message to match the Gio style, but that's it.

I'm thinking on how to create a markdown parser now. I'll post
anything I come up with.

Cheers,
Chris

On Tue, May 25, 2021 at 12:07 PM ghost08 <magyarv@protonmail.com> wrote: