diff --git a/.gitignore b/.gitignore index 2c29600..072080c 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,11 @@ node_modules/** !*.png !*.ttf !*.sfd +!*.frag +!*.vert +!*.m +!*.gz +!*.java # ...even if they are in subdirectories !*/ diff --git a/gel/app.go b/gel/app.go index 28d732e..3d2338d 100644 --- a/gel/app.go +++ b/gel/app.go @@ -6,9 +6,9 @@ import ( "realy.lol/atomic" "golang.org/x/exp/shiny/materialdesign/icons" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/text" - "realy.lol/gel/gio/unit" + l "realy.lol/gio/layout" + "realy.lol/gio/text" + "realy.lol/gio/unit" ) // App defines an application with a header, sidebar/menu, right side button bar, changeable body page widget and diff --git a/gel/bool.go b/gel/bool.go index d2ff94c..d387775 100644 --- a/gel/bool.go +++ b/gel/bool.go @@ -1,7 +1,7 @@ package gel import ( - l "realy.lol/gel/gio/layout" + l "realy.lol/gio/layout" ) type BoolHook func(b bool) diff --git a/gel/border.go b/gel/border.go index f9874e1..d6cc8b6 100644 --- a/gel/border.go +++ b/gel/border.go @@ -2,12 +2,12 @@ package gel import ( "image/color" - - "realy.lol/gel/gio/f32" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op/clip" - "realy.lol/gel/gio/op/paint" - "realy.lol/gel/gio/unit" + + "realy.lol/gio/f32" + l "realy.lol/gio/layout" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" ) // Border lays out a widget and draws a border inside it. @@ -53,15 +53,15 @@ func (b *Border) Embed(w l.Widget) *Border { func (b *Border) Fn(gtx l.Context) l.Dimensions { dims := b.w(gtx) sz := l.FPt(dims.Size) - + rr := float32(gtx.Px(b.cornerRadius)) width := float32(gtx.Px(b.width)) sz.X -= width sz.Y -= width - + r := f32.Rectangle{Max: sz} r = r.Add(f32.Point{X: width * 0.5, Y: width * 0.5}) - + paint.FillShape(gtx.Ops, b.color, clip.Stroke{ @@ -69,6 +69,6 @@ func (b *Border) Fn(gtx l.Context) l.Dimensions { Style: clip.StrokeStyle{Width: width}, }.Op(), ) - + return dims } diff --git a/gel/button.go b/gel/button.go index b1aad84..60f5678 100644 --- a/gel/button.go +++ b/gel/button.go @@ -5,13 +5,13 @@ import ( "math" "strings" - "realy.lol/gel/gio/f32" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/op/clip" - "realy.lol/gel/gio/op/paint" - "realy.lol/gel/gio/text" - "realy.lol/gel/gio/unit" + "realy.lol/gio/f32" + l "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" "realy.lol/gel/f32color" ) diff --git a/gel/buttonlayout.go b/gel/buttonlayout.go index 6a0c475..feccf3f 100644 --- a/gel/buttonlayout.go +++ b/gel/buttonlayout.go @@ -2,12 +2,12 @@ package gel import ( "image/color" - - "realy.lol/gel/gio/f32" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op/clip" - "realy.lol/gel/gio/unit" - + + "realy.lol/gio/f32" + l "realy.lol/gio/layout" + "realy.lol/gio/op/clip" + "realy.lol/gio/unit" + "realy.lol/gel/f32color" ) diff --git a/gel/card.go b/gel/card.go index 4aded51..b885a14 100644 --- a/gel/card.go +++ b/gel/card.go @@ -1,6 +1,6 @@ package gel -import l "realy.lol/gel/gio/layout" +import l "realy.lol/gio/layout" func (w *Window) Card(background string, embed l.Widget, ) func(gtx l.Context) l.Dimensions { diff --git a/gel/checkable.go b/gel/checkable.go index 8457bc3..0352dbc 100644 --- a/gel/checkable.go +++ b/gel/checkable.go @@ -3,11 +3,11 @@ package gel import ( "image" - "realy.lol/gel/gio/io/pointer" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op/paint" - "realy.lol/gel/gio/text" - "realy.lol/gel/gio/unit" + "realy.lol/gio/io/pointer" + l "realy.lol/gio/layout" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" "golang.org/x/exp/shiny/materialdesign/icons" ) diff --git a/gel/checkbox.go b/gel/checkbox.go index 561e1e1..beb291d 100644 --- a/gel/checkbox.go +++ b/gel/checkbox.go @@ -1,7 +1,7 @@ package gel import ( - l "realy.lol/gel/gio/layout" + l "realy.lol/gio/layout" ) // CheckBox creates a checkbox with a text label diff --git a/gel/clickable.go b/gel/clickable.go index c0a0b20..23bd9c4 100644 --- a/gel/clickable.go +++ b/gel/clickable.go @@ -4,12 +4,12 @@ import ( "image" "time" - "realy.lol/gel/gio/f32" - "realy.lol/gel/gio/gesture" - "realy.lol/gel/gio/io/key" - "realy.lol/gel/gio/io/pointer" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" + "realy.lol/gio/f32" + "realy.lol/gio/gesture" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + l "realy.lol/gio/layout" + "realy.lol/gio/op" ) type clickEvents struct { diff --git a/gel/cmd/clipboard/main.go b/gel/cmd/clipboard/main.go index 32c4303..82613a6 100644 --- a/gel/cmd/clipboard/main.go +++ b/gel/cmd/clipboard/main.go @@ -3,7 +3,7 @@ package main import ( "realy.lol/qu" - l "realy.lol/gel/gio/layout" + l "realy.lol/gio/layout" "realy.lol/gel" "realy.lol/gel/clipboard" diff --git a/gel/cmd/hello/main.go b/gel/cmd/hello/main.go index 36234da..b7e9004 100644 --- a/gel/cmd/hello/main.go +++ b/gel/cmd/hello/main.go @@ -1,7 +1,7 @@ package main import ( - l "realy.lol/gel/gio/layout" + l "realy.lol/gio/layout" "realy.lol/qu" "realy.lol/gel" diff --git a/gel/cmd/iconchooser/main.go b/gel/cmd/iconchooser/main.go index d0e820f..c73fdc8 100644 --- a/gel/cmd/iconchooser/main.go +++ b/gel/cmd/iconchooser/main.go @@ -3,7 +3,7 @@ package main import ( "sort" - l "realy.lol/gel/gio/layout" + l "realy.lol/gio/layout" "github.com/atotto/clipboard" "realy.lol/interrupt" "realy.lol/qu" diff --git a/gel/column.go b/gel/column.go index 85ee16d..3895db5 100644 --- a/gel/column.go +++ b/gel/column.go @@ -1,7 +1,7 @@ package gel import ( - l "realy.lol/gel/gio/layout" + l "realy.lol/gio/layout" ) type ColumnRow struct { @@ -23,7 +23,7 @@ type Column struct { func (w *Window) Column(rows Rows, font string, scale float32, color string, background string) *Column { return &Column{Window: w, rows: rows, font: font, scale: scale, - color: color, + color: color, background: background, list: w.List()} } diff --git a/gel/dialog/_dialog.go b/gel/dialog/_dialog.go index 2ee7997..5bc1894 100644 --- a/gel/dialog/_dialog.go +++ b/gel/dialog/_dialog.go @@ -4,11 +4,11 @@ import ( "image" "image/color" - "realy.lol/gel/gio/io/pointer" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/op/paint" - "realy.lol/gel/gio/unit" + "realy.lol/gio/io/pointer" + l "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" "github.com/p9c/p9/pkg/gui" ) diff --git a/gel/dialog/example/_main.go b/gel/dialog/example/_main.go index f72125e..73e6a38 100644 --- a/gel/dialog/example/_main.go +++ b/gel/dialog/example/_main.go @@ -4,12 +4,12 @@ import ( "log" "os" - "realy.lol/gel/gio/app" - "realy.lol/gel/gio/io/system" - "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/op/paint" - "realy.lol/gel/gio/unit" + "realy.lol/gio/app" + "realy.lol/gio/io/system" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" "github.com/p9c/p9/pkg/gui" "github.com/p9c/p9/pkg/gui/dialog" diff --git a/gel/dimensionlist.go b/gel/dimensionlist.go index 640e7b6..5ea5b87 100644 --- a/gel/dimensionlist.go +++ b/gel/dimensionlist.go @@ -1,8 +1,8 @@ package gel import ( - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" + l "realy.lol/gio/layout" + "realy.lol/gio/op" ) type DimensionList []l.Dimensions @@ -15,7 +15,8 @@ func (d DimensionList) GetTotal(axis l.Axis) (total int) { } // PositionToCoordinate converts a list position to absolute coordinate -func (d DimensionList) PositionToCoordinate(position Position, axis l.Axis) (coordinate int) { +func (d DimensionList) PositionToCoordinate(position Position, + axis l.Axis) (coordinate int) { for i := 0; i < position.First; i++ { coordinate += axisMain(axis, d[i].Size) } @@ -23,7 +24,8 @@ func (d DimensionList) PositionToCoordinate(position Position, axis l.Axis) (coo } // CoordinateToPosition converts an absolute coordinate to a list position -func (d DimensionList) CoordinateToPosition(coordinate int, axis l.Axis) (position Position) { +func (d DimensionList) CoordinateToPosition(coordinate int, + axis l.Axis) (position Position) { cursor := 0 if coordinate < 0 { coordinate = 0 @@ -56,7 +58,8 @@ func (d DimensionList) CoordinateToPosition(coordinate int, axis l.Axis) (positi } // GetDimensionList returns a dimensionlist based on the given listelement -func GetDimensionList(gtx l.Context, length int, listElement ListElement) (dims DimensionList) { +func GetDimensionList(gtx l.Context, length int, + listElement ListElement) (dims DimensionList) { // gather the dimensions of the list elements for i := 0; i < length; i++ { child := op.Record(gtx.Ops) @@ -74,7 +77,8 @@ func GetDimension(gtx l.Context, w l.Widget) (dim l.Dimensions) { return } -func (d DimensionList) GetSizes(position Position, axis l.Axis) (total, before int) { +func (d DimensionList) GetSizes(position Position, + axis l.Axis) (total, before int) { for i := range d { inc := axisMain(axis, d[i].Size) total += inc diff --git a/gel/direction.go b/gel/direction.go index e8027ed..9ff2630 100644 --- a/gel/direction.go +++ b/gel/direction.go @@ -1,6 +1,6 @@ package gel -import l "realy.lol/gel/gio/layout" +import l "realy.lol/gio/layout" type Direction struct { l.Direction diff --git a/gel/editor.go b/gel/editor.go index ae7e6b0..fa8304a 100644 --- a/gel/editor.go +++ b/gel/editor.go @@ -15,19 +15,19 @@ import ( "unicode" "unicode/utf8" - "realy.lol/gel/gio/f32" - "realy.lol/gel/gio/io/clipboard" - "realy.lol/gel/gio/io/event" - "realy.lol/gel/gio/io/key" - "realy.lol/gel/gio/io/pointer" - "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/op/clip" - "realy.lol/gel/gio/op/paint" - "realy.lol/gel/gio/text" - "realy.lol/gel/gio/unit" - - "realy.lol/gel/gio/gesture" + "realy.lol/gio/f32" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" + + "realy.lol/gio/gesture" "golang.org/x/image/math/fixed" @@ -215,7 +215,8 @@ func (e *Editor) processEvents(gtx layout.Context) { // Can't process events without a shaper. return } - oldStart, oldLen := min(e.caret.start.ofs, e.caret.end.ofs), e.SelectionLen() + oldStart, oldLen := min(e.caret.start.ofs, + e.caret.end.ofs), e.SelectionLen() e.processPointer(gtx) e.processKey(gtx) if newStart, newLen := min(e.caret.start.ofs, e.caret.end.ofs), @@ -295,10 +296,11 @@ func (e *Editor) processPointer(gtx layout.Context) { e.moveWord(1, selectionExtend) e.dragging = false } - // process a triple click - select all. This required forking realy.lol/gel/gio/gesture + // process a triple click - select all. This required forking realy.lol/gio/gesture if evt.NumClicks == 3 { e.dragging = false - e.caret.end, e.caret.start = e.offsetToScreenPos2(0, e.Len()) + e.caret.end, e.caret.start = e.offsetToScreenPos2(0, + e.Len()) evt.NumClicks = 0 } } @@ -407,7 +409,8 @@ func (e *Editor) processKey(gtx layout.Context) { } func (e *Editor) moveLines(distance int, selAct selectionAction) { - e.caret.start = e.movePosToLine(e.caret.start, e.caret.start.x+e.caret.start.xoff, + e.caret.start = e.movePosToLine(e.caret.start, + e.caret.start.x+e.caret.start.xoff, e.caret.start.lineCol.Y+distance) e.updateSelection(selAct) } @@ -508,7 +511,8 @@ func (e *Editor) Focused() bool { } // Layout lays out the editor. -func (e *Editor) Layout(gtx layout.Context, sh text.Shaper, font text.Font, size unit.Value) layout.Dimensions { +func (e *Editor) Layout(gtx layout.Context, sh text.Shaper, font text.Font, + size unit.Value) layout.Dimensions { textSize := fixed.I(gtx.Px(size)) if e.font != font || e.textSize != textSize { e.invalidate() @@ -666,7 +670,8 @@ func (e *Editor) PaintCaret(gtx layout.Context) { e.lines[e.caret.start.lineCol.Y].Bounds.Max.Y carRect := image.Rectangle{ Min: image.Point{X: carX.Ceil(), Y: carY - carAsc.Ceil()}, - Max: image.Point{X: carX.Ceil() + carWidth.Ceil(), Y: carY + carDesc.Ceil()}, + Max: image.Point{X: carX.Ceil() + carWidth.Ceil(), + Y: carY + carDesc.Ceil()}, } carRect = carRect.Add(image.Point{ X: -e.scrollOff.X, @@ -824,7 +829,8 @@ func (e *Editor) offsetToScreenPos2(o1, o2 int) (combinedPos, combinedPos) { // This function is written this way to take advantage of previous work done // for offsets after the first. Otherwise you have to start from the top each // time. -func (e *Editor) offsetToScreenPos(offset int) (combinedPos, func(int) combinedPos) { +func (e *Editor) offsetToScreenPos(offset int) (combinedPos, + func(int) combinedPos) { var col, line, idx int var x fixed.Int26_6 @@ -937,11 +943,13 @@ func (e *Editor) movePages(pages int, selAct selectionAction) { y2 += h carLine2++ } - e.caret.start = e.movePosToLine(e.caret.start, e.caret.start.x+e.caret.start.xoff, carLine2) + e.caret.start = e.movePosToLine(e.caret.start, + e.caret.start.x+e.caret.start.xoff, carLine2) e.updateSelection(selAct) } -func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6, line int) combinedPos { +func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6, + line int) combinedPos { e.makeValid(&pos) if line < 0 { line = 0 @@ -1270,7 +1278,8 @@ func (e *Editor) SelectedText() string { return "" } buf := make([]byte, l) - e.editBuffer.Seek(int64(min(e.caret.start.ofs, e.caret.end.ofs)), io.SeekStart) + e.editBuffer.Seek(int64(min(e.caret.start.ofs, e.caret.end.ofs)), + io.SeekStart) _, err := e.editBuffer.Read(buf) if err != nil { // The only error that rr.Read can return is EOF, which just means no diff --git a/gel/enum.go b/gel/enum.go index 5e364c2..63da1ed 100644 --- a/gel/enum.go +++ b/gel/enum.go @@ -3,10 +3,10 @@ package gel import ( "image" - "realy.lol/gel/gio/gesture" - "realy.lol/gel/gio/io/pointer" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" + "realy.lol/gio/gesture" + "realy.lol/gio/io/pointer" + l "realy.lol/gio/layout" + "realy.lol/gio/op" ) type Enum struct { diff --git a/gel/fill.go b/gel/fill.go index 8df1ec7..f4295d4 100644 --- a/gel/fill.go +++ b/gel/fill.go @@ -3,11 +3,11 @@ package gel import ( "image" "image/color" - - "realy.lol/gel/gio/f32" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op/clip" - "realy.lol/gel/gio/op/paint" + + "realy.lol/gio/f32" + l "realy.lol/gio/layout" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" ) // Filler fills the background of a widget with a specified color and corner @@ -30,18 +30,22 @@ const ( // Fill fills underneath a widget you can put over top of it, dxn sets which // direction to place a smaller object, cardinal axes and center -func (w *Window) Fill(col string, dxn l.Direction, radius float32, corners int, embed l.Widget) *Filler { - return &Filler{Window: w, col: col, w: embed, dxn: dxn, cornerRadius: radius, corners: corners} +func (w *Window) Fill(col string, dxn l.Direction, radius float32, corners int, + embed l.Widget) *Filler { + return &Filler{Window: w, col: col, w: embed, dxn: dxn, + cornerRadius: radius, corners: corners} } // Fn renders the fill with the widget inside func (f *Filler) Fn(gtx l.Context) l.Dimensions { gtx1 := CopyContextDimensionsWithMaxAxis(gtx, l.Horizontal) // generate the dimensions for all the list elements - dL := GetDimensionList(gtx1, 1, func(gtx l.Context, index int) l.Dimensions { - return f.w(gtx) - }) - fill(gtx, f.Colors.GetNRGBAFromName(f.col), dL[0].Size, f.cornerRadius, f.corners) + dL := GetDimensionList(gtx1, 1, + func(gtx l.Context, index int) l.Dimensions { + return f.w(gtx) + }) + fill(gtx, f.Colors.GetNRGBAFromName(f.col), dL[0].Size, f.cornerRadius, + f.corners) return f.dxn.Layout(gtx, f.w) } @@ -52,7 +56,8 @@ func ifDir(radius float32, dir int) float32 { return 0 } -func fill(gtx l.Context, col color.NRGBA, bounds image.Point, radius float32, cnrs int) { +func fill(gtx l.Context, col color.NRGBA, bounds image.Point, radius float32, + cnrs int) { rect := f32.Rectangle{ Max: f32.Pt(float32(bounds.X), float32(bounds.Y)), } diff --git a/gel/fit.go b/gel/fit.go index 69cd25e..9b3c192 100644 --- a/gel/fit.go +++ b/gel/fit.go @@ -4,11 +4,11 @@ package gel import ( "image" - - "realy.lol/gel/gio/f32" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/op/clip" + + "realy.lol/gio/f32" + l "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" ) // Fit scales a widget to fit and clip to the constraints. @@ -35,24 +35,25 @@ const ( // scale adds clip and scale operations to fit dims to the constraints. // It positions the widget to the appropriate position. // It returns dimensions modified accordingly. -func (fit Fit) scale(gtx l.Context, pos l.Direction, dims l.Dimensions) l.Dimensions { +func (fit Fit) scale(gtx l.Context, pos l.Direction, + dims l.Dimensions) l.Dimensions { widgetSize := dims.Size - + if fit == Unscaled || dims.Size.X == 0 || dims.Size.Y == 0 { dims.Size = gtx.Constraints.Constrain(dims.Size) clip.Rect{Max: dims.Size}.Add(gtx.Ops) - + offset := pos.Position(widgetSize, dims.Size) op.Offset(l.FPt(offset)).Add(gtx.Ops) dims.Baseline += offset.Y return dims } - + scale := f32.Point{ X: float32(gtx.Constraints.Max.X) / float32(dims.Size.X), Y: float32(gtx.Constraints.Max.Y) / float32(dims.Size.Y), } - + switch fit { case Contain: if scale.Y < scale.X { @@ -72,12 +73,12 @@ func (fit Fit) scale(gtx l.Context, pos l.Direction, dims l.Dimensions) l.Dimens } else { scale.Y = scale.X } - + // The widget would need to be scaled up, no change needed. if scale.X >= 1 { dims.Size = gtx.Constraints.Constrain(dims.Size) clip.Rect{Max: dims.Size}.Add(gtx.Ops) - + offset := pos.Position(widgetSize, dims.Size) op.Offset(l.FPt(offset)).Add(gtx.Ops) dims.Baseline += offset.Y @@ -85,22 +86,22 @@ func (fit Fit) scale(gtx l.Context, pos l.Direction, dims l.Dimensions) l.Dimens } case Stretch: } - + var scaledSize image.Point scaledSize.X = int(float32(widgetSize.X) * scale.X) scaledSize.Y = int(float32(widgetSize.Y) * scale.Y) dims.Size = gtx.Constraints.Constrain(scaledSize) dims.Baseline = int(float32(dims.Baseline) * scale.Y) - + clip.Rect{Max: dims.Size}.Add(gtx.Ops) - + offset := pos.Position(scaledSize, dims.Size) op.Affine(f32.Affine2D{}. Scale(f32.Point{}, scale). Offset(l.FPt(offset)), ).Add(gtx.Ops) - + dims.Baseline += offset.Y - + return dims } diff --git a/gel/flex.go b/gel/flex.go index d4d20b0..516ead4 100644 --- a/gel/flex.go +++ b/gel/flex.go @@ -1,6 +1,6 @@ package gel -import l "realy.lol/gel/gio/layout" +import l "realy.lol/gio/layout" type Flex struct { flex l.Flex diff --git a/gel/float.go b/gel/float.go index 155c23e..6e6cacc 100644 --- a/gel/float.go +++ b/gel/float.go @@ -5,10 +5,10 @@ package gel import ( "image" - "realy.lol/gel/gio/gesture" - "realy.lol/gel/gio/io/pointer" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" + "realy.lol/gio/gesture" + "realy.lol/gio/io/pointer" + l "realy.lol/gio/layout" + "realy.lol/gio/op" ) // Float is for selecting a value in a range. @@ -39,7 +39,8 @@ func (f *Float) SetHook(fn func(fl float32)) *Float { } // Fn processes events. -func (f *Float) Fn(gtx l.Context, pointerMargin int, min, max float32) l.Dimensions { +func (f *Float) Fn(gtx l.Context, pointerMargin int, + min, max float32) l.Dimensions { size := gtx.Constraints.Min f.length = float32(size.X) var de *pointer.Event diff --git a/gel/fonts/p9fonts/fonts.go b/gel/fonts/p9fonts/fonts.go index f41bacf..37d9f98 100644 --- a/gel/fonts/p9fonts/fonts.go +++ b/gel/fonts/p9fonts/fonts.go @@ -4,8 +4,8 @@ import ( "fmt" "sync" - "realy.lol/gel/gio/font/opentype" - "realy.lol/gel/gio/text" + "realy.lol/gio/font/opentype" + "realy.lol/gio/text" "golang.org/x/image/font/gofont/gomono" "golang.org/x/image/font/gofont/gomonobold" "golang.org/x/image/font/gofont/gomonobolditalic" @@ -28,12 +28,15 @@ var ( "bariol regular": {Typeface: "bariol regular"}, "bariol italic": {Typeface: "bariol italic", Style: text.Italic}, "bariol bold": {Typeface: "bariol bold", Weight: text.Bold}, - "bariol bolditalic": {Typeface: "bariol bolditalic", Style: text.Italic, Weight: text.Bold}, + "bariol bolditalic": {Typeface: "bariol bolditalic", + Style: text.Italic, Weight: text.Bold}, "bariol light": {Typeface: "bariol light", Weight: text.Medium}, - "bariol lightitalic": {Typeface: "bariol lightitalic", Weight: text.Medium, Style: text.Italic}, + "bariol lightitalic": {Typeface: "bariol lightitalic", + Weight: text.Medium, Style: text.Italic}, "go regular": {Typeface: "go regular"}, "go bold": {Typeface: "go bold", Weight: text.Bold}, - "go bolditalic": {Typeface: "go bolditalic", Weight: text.Bold, Style: text.Italic}, + "go bolditalic": {Typeface: "go bolditalic", Weight: text.Bold, + Style: text.Italic}, "go italic": {Typeface: "go italic", Style: text.Italic}, } ) diff --git a/gel/helpers.go b/gel/helpers.go index 4dc4a4e..5b11ce8 100644 --- a/gel/helpers.go +++ b/gel/helpers.go @@ -5,12 +5,12 @@ import ( "image" "image/color" "time" - - "realy.lol/gel/gio/f32" - "realy.lol/gel/gio/io/system" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/text" + + "realy.lol/gio/f32" + "realy.lol/gio/io/system" + l "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/text" ) // Defining these as types gives flexibility later to create methods that modify them @@ -27,7 +27,7 @@ func (c Collection) Font(font string) (out text.Font, e error) { } } return out, errors.New("font " + font + " not found") - + } const Inf = 1e6 @@ -59,7 +59,8 @@ func Fill(gtx l.Context, col color.NRGBA) l.Dimensions { // } func argb(c uint32) color.RGBA { - return color.RGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), B: uint8(c)} + return color.RGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), + B: uint8(c)} } // FPt converts an point to a f32.Point. @@ -91,14 +92,14 @@ func axisMain(a l.Axis, sz image.Point) int { if a == l.Horizontal { return sz.X } - return sz.Y + return sz.Y } func axisCross(a l.Axis, sz image.Point) int { if a == l.Horizontal { return sz.Y } - return sz.X + return sz.X } func axisMainConstraint(a l.Axis, cs l.Constraints) (int, int) { @@ -115,11 +116,14 @@ func axisCrossConstraint(a l.Axis, cs l.Constraints) (int, int) { return cs.Min.X, cs.Max.X } -func axisConstraints(a l.Axis, mainMin, mainMax, crossMin, crossMax int) l.Constraints { +func axisConstraints(a l.Axis, + mainMin, mainMax, crossMin, crossMax int) l.Constraints { if a == l.Horizontal { - return l.Constraints{Min: image.Pt(mainMin, crossMin), Max: image.Pt(mainMax, crossMax)} + return l.Constraints{Min: image.Pt(mainMin, crossMin), + Max: image.Pt(mainMax, crossMax)} } - return l.Constraints{Min: image.Pt(crossMin, mainMin), Max: image.Pt(crossMax, mainMax)} + return l.Constraints{Min: image.Pt(crossMin, mainMin), + Max: image.Pt(crossMax, mainMax)} } func EmptySpace(x, y int) func(gtx l.Context) l.Dimensions { @@ -144,21 +148,24 @@ func EmptyFromSize(size image.Point) func(gtx l.Context) l.Dimensions { func EmptyMaxWidth() func(gtx l.Context) l.Dimensions { return func(gtx l.Context) l.Dimensions { return l.Dimensions{ - Size: image.Point{X: gtx.Constraints.Max.X, Y: gtx.Constraints.Min.Y}, + Size: image.Point{X: gtx.Constraints.Max.X, + Y: gtx.Constraints.Min.Y}, Baseline: 0, } } } func EmptyMaxHeight() func(gtx l.Context) l.Dimensions { return func(gtx l.Context) l.Dimensions { - return l.Dimensions{Size: image.Point{X: gtx.Constraints.Min.X, Y: gtx.Constraints.Min.Y}} + return l.Dimensions{Size: image.Point{X: gtx.Constraints.Min.X, + Y: gtx.Constraints.Min.Y}} } } func EmptyMinWidth() func(gtx l.Context) l.Dimensions { return func(gtx l.Context) l.Dimensions { return l.Dimensions{ - Size: image.Point{X: gtx.Constraints.Min.X, Y: gtx.Constraints.Min.Y}, + Size: image.Point{X: gtx.Constraints.Min.X, + Y: gtx.Constraints.Min.Y}, Baseline: 0, } } diff --git a/gel/icon.go b/gel/icon.go index afa7d65..b905c11 100644 --- a/gel/icon.go +++ b/gel/icon.go @@ -5,9 +5,9 @@ import ( "image/color" "image/draw" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op/paint" - "realy.lol/gel/gio/unit" + l "realy.lol/gio/layout" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" "golang.org/x/exp/shiny/iconvg" ) diff --git a/gel/iconbutton.go b/gel/iconbutton.go index fd07135..d306d40 100644 --- a/gel/iconbutton.go +++ b/gel/iconbutton.go @@ -2,14 +2,14 @@ package gel import ( "image" - - "realy.lol/gel/gio/f32" - "realy.lol/gel/gio/io/pointer" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op/clip" - "realy.lol/gel/gio/unit" + + "realy.lol/gio/f32" + "realy.lol/gio/io/pointer" + l "realy.lol/gio/layout" + "realy.lol/gio/op/clip" + "realy.lol/gio/unit" "golang.org/x/exp/shiny/materialdesign/icons" - + "realy.lol/gel/f32color" ) diff --git a/gel/image.go b/gel/image.go index a4e0990..e09dd52 100644 --- a/gel/image.go +++ b/gel/image.go @@ -4,12 +4,12 @@ package gel import ( "image" - - "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/op/clip" - "realy.lol/gel/gio/op/paint" - "realy.lol/gel/gio/unit" + + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" ) // Image is a widget that displays an image. diff --git a/gel/incdec.go b/gel/incdec.go index acd8190..b1d5632 100644 --- a/gel/incdec.go +++ b/gel/incdec.go @@ -2,8 +2,8 @@ package gel import ( "fmt" - - l "realy.lol/gel/gio/layout" + + l "realy.lol/gio/layout" "golang.org/x/exp/shiny/materialdesign/icons" ) diff --git a/gel/indefinite.go b/gel/indefinite.go index 60fa958..55151a2 100644 --- a/gel/indefinite.go +++ b/gel/indefinite.go @@ -5,12 +5,12 @@ import ( "image/color" "math" "time" - - "realy.lol/gel/gio/f32" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/op/clip" - "realy.lol/gel/gio/op/paint" + + "realy.lol/gio/f32" + l "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" ) type Indefinite struct { @@ -74,12 +74,12 @@ func clipLoader(ops *op.Ops, startAngle, endAngle, radius float64) { var ( width = float32(radius * thickness) delta = float32(endAngle - startAngle) - + vy, vx = math.Sincos(startAngle) - + pen = f32.Pt(float32(vx), float32(vy)).Mul(float32(radius)) center = f32.Pt(0, 0).Sub(pen) - + p clip.Path ) p.Begin(ops) diff --git a/gel/input.go b/gel/input.go index 527888f..20f053f 100644 --- a/gel/input.go +++ b/gel/input.go @@ -5,7 +5,7 @@ import ( "golang.org/x/exp/shiny/materialdesign/icons" - l "realy.lol/gel/gio/layout" + l "realy.lol/gio/layout" ) type Input struct { diff --git a/gel/inset.go b/gel/inset.go index d991c42..616a04b 100644 --- a/gel/inset.go +++ b/gel/inset.go @@ -1,13 +1,13 @@ package gel import ( - l "realy.lol/gel/gio/layout" + l "realy.lol/gio/layout" ) type Inset struct { *Window - in l.Inset - w l.Widget + in l.Inset + w l.Widget } // Inset creates a padded empty space around a widget diff --git a/gel/intslider.go b/gel/intslider.go index 975590d..a0a4d4f 100644 --- a/gel/intslider.go +++ b/gel/intslider.go @@ -3,7 +3,7 @@ package gel import ( "fmt" - l "realy.lol/gel/gio/layout" + l "realy.lol/gio/layout" ) type IntSlider struct { diff --git a/gel/label.go b/gel/label.go index e14cbac..ba65758 100644 --- a/gel/label.go +++ b/gel/label.go @@ -6,12 +6,12 @@ import ( "image/color" "unicode/utf8" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/op/clip" - "realy.lol/gel/gio/op/paint" - "realy.lol/gel/gio/text" - "realy.lol/gel/gio/unit" + l "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" "golang.org/x/image/math/fixed" ) diff --git a/gel/list.go b/gel/list.go index 248d68f..bc44d80 100644 --- a/gel/list.go +++ b/gel/list.go @@ -3,12 +3,12 @@ package gel import ( "image" "time" - - "realy.lol/gel/gio/gesture" - "realy.lol/gel/gio/io/pointer" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/op/clip" + + "realy.lol/gio/gesture" + "realy.lol/gio/io/pointer" + l "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" ) type scrollChild struct { @@ -36,7 +36,7 @@ type List struct { maxSize int children []scrollChild dir iterationDir - + // all below are additional fields to implement the scrollbar *Window // we store the constraints here instead of in the `cs` field @@ -158,8 +158,10 @@ func (li *List) Dragging() bool { // update the scrolling func (li *List) update() { - d := li.scroll.Scroll(li.ctx.Metric, li.ctx, li.ctx.Now, gesture.Axis(li.axis)) - d += li.sideScroll.Scroll(li.ctx.Metric, li.ctx, li.ctx.Now, gesture.Axis(li.axis)) + d := li.scroll.Scroll(li.ctx.Metric, li.ctx, li.ctx.Now, + gesture.Axis(li.axis)) + d += li.sideScroll.Scroll(li.ctx.Metric, li.ctx, li.ctx.Now, + gesture.Axis(li.axis)) li.scrollDelta = d li.position.Offset += d } @@ -323,7 +325,7 @@ func (li *List) layout(macro op.MacroOp) l.Dimensions { pointer.Rect(bounds).Add(ops) // li.sideScroll.Add(ops, bounds) // li.scroll.Add(ops, bounds) - + var min, max int if o := li.position.Offset; o > 0 { // Use the size of the invisible part as scroll boundary. @@ -342,12 +344,12 @@ func (li *List) layout(macro op.MacroOp) l.Dimensions { } li.scroll.Add(ops, scrollRange) li.sideScroll.Add(ops, scrollRange) - + call.Add(ops) return l.Dimensions{Size: dims} } -// Everything below is extensions on the original from realy.lol/gel/gio/layout +// Everything below is extensions on the original from realy.lol/gio/layout // Position returns the current position of the scroller func (li *List) Position() Position { @@ -467,7 +469,8 @@ func (li *List) Active(color string) *List { } func (li *List) Slice(gtx l.Context, widgets ...l.Widget) l.Widget { - return li.Length(len(widgets)).Vertical().ListElement(func(gtx l.Context, index int) l.Dimensions { + return li.Length(len(widgets)).Vertical().ListElement(func(gtx l.Context, + index int) l.Dimensions { return widgets[index](gtx) }, ).Fn @@ -531,7 +534,8 @@ func (li *List) Fn(gtx l.Context) l.Dimensions { containerFlex := li.Theme.VFlex() if !li.leftSide { containerFlex.Rigid(li.embedWidget(li.scrollWidth /* + int(li.TextSize.True)/4)*/)) - containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/4, int(li.TextSize.V)/4)) + containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/4, + int(li.TextSize.V)/4)) } containerFlex.Rigid( li.VFlex(). @@ -564,7 +568,8 @@ func (li *List) Fn(gtx l.Context) l.Dimensions { Fn, ) if li.leftSide { - containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/4, int(li.TextSize.V)/4)) + containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/4, + int(li.TextSize.V)/4)) containerFlex.Rigid(li.embedWidget(li.scrollWidth)) // li.scrollWidth)) // + li.scrollBarPad)) } container = containerFlex.Fn @@ -572,7 +577,8 @@ func (li *List) Fn(gtx l.Context) l.Dimensions { containerFlex := li.Theme.Flex() if !li.leftSide { containerFlex.Rigid(li.embedWidget(li.scrollWidth + int(li.TextSize.V)/2)) // + li.scrollBarPad)) - containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/2, int(li.TextSize.V)/2)) + containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/2, + int(li.TextSize.V)/2)) } containerFlex.Rigid( li.Fill(li.background, l.Center, li.TextSize.V/4, 0, li.Flex(). @@ -605,10 +611,12 @@ func (li *List) Fn(gtx l.Context) l.Dimensions { ).Fn, ) if li.leftSide { - containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/2, int(li.TextSize.V)/2)) + containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/2, + int(li.TextSize.V)/2)) containerFlex.Rigid(li.embedWidget(li.scrollWidth + int(li.TextSize.V)/2)) } - container = li.Fill(li.background, l.Center, li.TextSize.V/4, 0, containerFlex.Fn).Fn + container = li.Fill(li.background, l.Center, li.TextSize.V/4, 0, + containerFlex.Fn).Fn } return container(gtx) } @@ -628,7 +636,8 @@ func (li *List) embedWidget(scrollWidth int) func(l.Context) l.Dimensions { } // pageUpDown creates the clickable areas either side of the grabber that trigger a page up/page down action -func (li *List) pageUpDown(dims DimensionList, view, total, x, y int, down bool) func(l.Context) l.Dimensions { +func (li *List) pageUpDown(dims DimensionList, view, total, x, y int, + down bool) func(l.Context) l.Dimensions { button := li.pageUp if down { button = li.pageDown @@ -660,7 +669,8 @@ func (li *List) pageUpDown(dims DimensionList, view, total, x, y int, down bool) li.Flex(). Rigid(EmptySpace(x/4, y)). Rigid( - li.Fill("scrim", l.Center, li.TextSize.V/4, 0, EmptySpace(x/2, y)).Fn, + li.Fill("scrim", l.Center, li.TextSize.V/4, 0, + EmptySpace(x/2, y)).Fn, ). Rigid(EmptySpace(x/4, y)). Fn, @@ -669,7 +679,8 @@ func (li *List) pageUpDown(dims DimensionList, view, total, x, y int, down bool) } // grabber renders the grabber -func (li *List) grabber(dims DimensionList, x, y, viewAxis, viewCross int) func(l.Context) l.Dimensions { +func (li *List) grabber(dims DimensionList, + x, y, viewAxis, viewCross int) func(l.Context) l.Dimensions { return func(gtx l.Context) l.Dimensions { ax := gesture.Vertical if li.axis == l.Horizontal { @@ -697,13 +708,15 @@ func (li *List) grabber(dims DimensionList, x, y, viewAxis, viewCross int) func( deltaX := int(de.Position.X) if deltaX > 8 || deltaX < -8 { d = deltaX * (total / viewAxis) - li.SetPosition(dims.CoordinateToPosition(d, li.axis)) + li.SetPosition(dims.CoordinateToPosition(d, + li.axis)) } } else { deltaY := int(de.Position.Y) if deltaY > 8 || deltaY < -8 { d = deltaY * (total / viewAxis) - li.SetPosition(dims.CoordinateToPosition(d, li.axis)) + li.SetPosition(dims.CoordinateToPosition(d, + li.axis)) } } } diff --git a/gel/multi.go b/gel/multi.go index 4a87739..5e95004 100644 --- a/gel/multi.go +++ b/gel/multi.go @@ -1,7 +1,7 @@ package gel import ( - l "realy.lol/gel/gio/layout" + l "realy.lol/gio/layout" "golang.org/x/exp/shiny/materialdesign/icons" ) diff --git a/gel/password.go b/gel/password.go index ed4da52..6d0a2ad 100644 --- a/gel/password.go +++ b/gel/password.go @@ -4,7 +4,7 @@ import ( "realy.lol/opts/text" icons2 "golang.org/x/exp/shiny/materialdesign/icons" - l "realy.lol/gel/gio/layout" + l "realy.lol/gio/layout" ) type Password struct { diff --git a/gel/progressbar.go b/gel/progressbar.go index 98feb12..5d2102d 100644 --- a/gel/progressbar.go +++ b/gel/progressbar.go @@ -4,12 +4,12 @@ import ( "image" "image/color" - "realy.lol/gel/gio/f32" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op/clip" - "realy.lol/gel/gio/op/paint" - "realy.lol/gel/gio/unit" - + "realy.lol/gio/f32" + l "realy.lol/gio/layout" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" + "realy.lol/gel/f32color" ) @@ -45,34 +45,35 @@ func (p *ProgressBar) Fn(gtx l.Context) l.Dimensions { shader := func(width float32, color color.NRGBA) l.Dimensions { maxHeight := unit.Dp(4) rr := float32(gtx.Px(unit.Dp(2))) - + d := image.Point{X: int(width), Y: gtx.Px(maxHeight)} - + clip.RRect{ - Rect: f32.Rectangle{Max: f32.Point{X: width, Y: float32(gtx.Px(maxHeight))}}, + Rect: f32.Rectangle{Max: f32.Point{X: width, + Y: float32(gtx.Px(maxHeight))}}, NE: rr, NW: rr, SE: rr, SW: rr, }.Add(gtx.Ops) - + paint.ColorOp{Color: color}.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) - + return l.Dimensions{Size: d} } - + progress := p.progress if progress > 100 { progress = 100 } else if progress < 0 { progress = 0 } - + progressBarWidth := float32(gtx.Constraints.Max.X) - + return l.Stack{Alignment: l.W}.Layout(gtx, l.Stacked(func(gtx l.Context) l.Dimensions { // Use a transparent equivalent of progress color. bgCol := f32color.MulAlpha(p.color, 150) - + return shader(progressBarWidth, bgCol) }), l.Stacked(func(gtx l.Context) l.Dimensions { diff --git a/gel/radiobutton.go b/gel/radiobutton.go index 5b875a5..10e06de 100644 --- a/gel/radiobutton.go +++ b/gel/radiobutton.go @@ -5,7 +5,7 @@ package gel import ( "golang.org/x/exp/shiny/materialdesign/icons" - l "realy.lol/gel/gio/layout" + l "realy.lol/gio/layout" ) type RadioButton struct { diff --git a/gel/responsive.go b/gel/responsive.go index 3e8e5d4..5675e18 100644 --- a/gel/responsive.go +++ b/gel/responsive.go @@ -2,8 +2,8 @@ package gel import ( "sort" - - l "realy.lol/gel/gio/layout" + + l "realy.lol/gio/layout" ) // WidgetSize is a widget with a specification of the minimum size to select it for viewing. diff --git a/gel/slider.go b/gel/slider.go index 3fe6e12..50a93e8 100644 --- a/gel/slider.go +++ b/gel/slider.go @@ -3,14 +3,14 @@ package gel import ( "image" "image/color" - - "realy.lol/gel/gio/f32" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/op/clip" - "realy.lol/gel/gio/op/paint" - "realy.lol/gel/gio/unit" - + + "realy.lol/gio/f32" + l "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" + "realy.lol/gel/f32color" ) @@ -60,7 +60,7 @@ func (s *Slider) Fn(gtx l.Context) l.Dimensions { thumbRadius := float32(thumbRadiusInt) halfWidthInt := 2 * thumbRadiusInt halfWidth := float32(halfWidthInt) - + size := gtx.Constraints.Min // Keep a minimum length so that the track is always visible. minLength := halfWidthInt + 3*thumbRadiusInt + halfWidthInt @@ -68,19 +68,19 @@ func (s *Slider) Fn(gtx l.Context) l.Dimensions { size.X = minLength } size.Y = 2 * halfWidthInt - + st := op.Save(gtx.Ops) op.Offset(f32.Pt(halfWidth, 0)).Add(gtx.Ops) gtx.Constraints.Min = image.Pt(size.X-2*halfWidthInt, size.Y) s.float.Fn(gtx, halfWidthInt, s.min, s.max) thumbPos := halfWidth + s.float.Pos() st.Load() - + col := s.color if gtx.Queue == nil { col = f32color.MulAlpha(col, 150) } - + // Draw track before thumb. st = op.Save(gtx.Ops) track := f32.Rectangle{ @@ -97,7 +97,7 @@ func (s *Slider) Fn(gtx l.Context) l.Dimensions { paint.ColorOp{Color: col}.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) st.Load() - + // Draw track after thumb. st = op.Save(gtx.Ops) track.Min.X = thumbPos @@ -106,7 +106,7 @@ func (s *Slider) Fn(gtx l.Context) l.Dimensions { paint.ColorOp{Color: f32color.MulAlpha(col, 96)}.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) st.Load() - + // Draw thumb. st = op.Save(gtx.Ops) thumb := f32.Rectangle{ @@ -127,6 +127,6 @@ func (s *Slider) Fn(gtx l.Context) l.Dimensions { paint.ColorOp{Color: col}.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) st.Load() - + return l.Dimensions{Size: size} } diff --git a/gel/stack.go b/gel/stack.go index a27fdae..ab84eb6 100644 --- a/gel/stack.go +++ b/gel/stack.go @@ -1,6 +1,6 @@ package gel -import l "realy.lol/gel/gio/layout" +import l "realy.lol/gio/layout" type Stack struct { *l.Stack diff --git a/gel/switch.go b/gel/switch.go index 696d6f5..cfb6975 100644 --- a/gel/switch.go +++ b/gel/switch.go @@ -3,15 +3,15 @@ package gel import ( "image" "image/color" - - "realy.lol/gel/gio/f32" - "realy.lol/gel/gio/io/pointer" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/op/clip" - "realy.lol/gel/gio/op/paint" - "realy.lol/gel/gio/unit" - + + "realy.lol/gio/f32" + "realy.lol/gio/io/pointer" + l "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" + "realy.lol/gel/f32color" ) @@ -59,7 +59,7 @@ func (s *Switch) Fn(gtx l.Context) l.Dimensions { trackHeight := gtx.Px(unit.Dp(16)) thumbSize := gtx.Px(unit.Dp(20)) trackOff := float32(thumbSize-trackHeight) * .5 - + // Draw track. stack := op.Save(gtx.Ops) trackCorner := float32(trackHeight) / 2 @@ -78,12 +78,13 @@ func (s *Switch) Fn(gtx l.Context) l.Dimensions { op.Offset(f32.Point{Y: trackOff}).Add(gtx.Ops) clip.RRect{ Rect: trackRect, - NE: trackCorner, NW: trackCorner, SE: trackCorner, SW: trackCorner, + NE: trackCorner, NW: trackCorner, SE: trackCorner, + SW: trackCorner, }.Add(gtx.Ops) paint.ColorOp{Color: trackColor}.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) stack.Load() - + // Draw thumb ink. stack = op.Save(gtx.Ops) inkSize := gtx.Px(unit.Dp(44)) @@ -104,27 +105,28 @@ func (s *Switch) Fn(gtx l.Context) l.Dimensions { drawInk(gtx, p) } stack.Load() - + // Compute thumb offset and color. stack = op.Save(gtx.Ops) if s.swtch.value { off := trackWidth - thumbSize op.Offset(f32.Point{X: float32(off)}).Add(gtx.Ops) } - + // Draw thumb shadow, a translucent disc slightly larger than the // thumb itself. shadowStack := op.Save(gtx.Ops) shadowSize := float32(2) // Center shadow horizontally and slightly adjust its Y. op.Offset(f32.Point{X: -shadowSize / 2, Y: -.75}).Add(gtx.Ops) - drawDisc(gtx.Ops, float32(thumbSize)+shadowSize, color.NRGBA(argb(0x55000000))) + drawDisc(gtx.Ops, float32(thumbSize)+shadowSize, + color.NRGBA(argb(0x55000000))) shadowStack.Load() - + // Draw thumb. drawDisc(gtx.Ops, float32(thumbSize), col) stack.Load() - + // Set up click area. stack = op.Save(gtx.Ops) clickSize := gtx.Px(unit.Dp(40)) @@ -138,7 +140,7 @@ func (s *Switch) Fn(gtx l.Context) l.Dimensions { gtx.Constraints.Min = sz s.swtch.Fn(gtx) stack.Load() - + dims := image.Point{X: trackWidth, Y: thumbSize} return l.Dimensions{Size: dims} }).Fn(gtx) diff --git a/gel/table.go b/gel/table.go index 268d23a..8069097 100644 --- a/gel/table.go +++ b/gel/table.go @@ -3,9 +3,9 @@ package gel import ( "image" "sort" - - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" + + l "realy.lol/gio/layout" + "realy.lol/gio/op" ) type Cell struct { @@ -120,7 +120,7 @@ func (t *Table) Fn(gtx l.Context) l.Dimensions { // if len(t.body) == 0 || len(t.header) == 0 { // return l.Dimensions{} // } - + for i := range t.body { if len(t.header) != len(t.body[i]) { // this should never happen hence panic @@ -140,7 +140,7 @@ func (t *Table) Fn(gtx l.Context) l.Dimensions { } } // D.S(t.body) - + // find the max of each row and column var table CellGrid table = append(table, t.header) @@ -244,12 +244,13 @@ func (t *Table) Fn(gtx l.Context) l.Dimensions { cs.Max.Y = tyi cs.Min.Y = gtx.Constraints.Max.Y // gtx.Constraints.Constrain(image.Point{X: txi, Y: tyi}) - dims := t.Fill(t.headerBackground, l.Center, t.TextSize.V, 0, EmptySpace(txi, tyi)).Fn(gtx) + dims := t.Fill(t.headerBackground, l.Center, t.TextSize.V, 0, + EmptySpace(txi, tyi)).Fn(gtx) oie.Widget(gtx) return dims }) } - + var out CellGrid out = CellGrid{t.header} if t.reverse { @@ -276,24 +277,25 @@ func (t *Table) Fn(gtx l.Context) l.Dimensions { oie := oiee txi := t.X[i] tyi := t.Y[index] - f.Rigid(t.Fill(t.cellBackground, l.Center, t.TextSize.V, 0, func(gtx l.Context) l.Dimensions { - cs := gtx.Constraints - cs.Max.X = txi - cs.Min.X = gtx.Constraints.Max.X - cs.Max.Y = tyi - cs.Min.Y = gtx.Constraints.Max.Y // gtx.Constraints.Constrain(image.Point{ - // X: t.X[i], - // Y: t.Y[index], - // }) - gtx.Constraints.Max.X = txi - // gtx.Constraints.Min.X = gtx.Constraints.Max.X - gtx.Constraints.Max.Y = tyi - // gtx.Constraints.Min.Y = gtx.Constraints.Max.Y - dims := EmptySpace(txi, tyi)(gtx) - // dims - oie.Widget(gtx) - return dims - }).Fn) + f.Rigid(t.Fill(t.cellBackground, l.Center, t.TextSize.V, 0, + func(gtx l.Context) l.Dimensions { + cs := gtx.Constraints + cs.Max.X = txi + cs.Min.X = gtx.Constraints.Max.X + cs.Max.Y = tyi + cs.Min.Y = gtx.Constraints.Max.Y // gtx.Constraints.Constrain(image.Point{ + // X: t.X[i], + // Y: t.Y[index], + // }) + gtx.Constraints.Max.X = txi + // gtx.Constraints.Min.X = gtx.Constraints.Max.X + gtx.Constraints.Max.Y = tyi + // gtx.Constraints.Min.Y = gtx.Constraints.Max.Y + dims := EmptySpace(txi, tyi)(gtx) + // dims + oie.Widget(gtx) + return dims + }).Fn) } } return f.Fn(gtx) @@ -301,16 +303,18 @@ func (t *Table) Fn(gtx l.Context) l.Dimensions { return t.Theme.VFlex(). Rigid(func(gtx l.Context) l.Dimensions { // header is fixed to the top of the widget - return t.Fill(t.headerBackground, l.Center, t.TextSize.V, 0, header.Fn).Fn(gtx) + return t.Fill(t.headerBackground, l.Center, t.TextSize.V, 0, + header.Fn).Fn(gtx) }). Flexed(1, - t.Fill(t.cellBackground, l.Center, t.TextSize.V, 0, func(gtx l.Context) l.Dimensions { - return t.list.Vertical(). - Length(len(out)). - Background(t.cellBackground). - ListElement(le). - Fn(gtx) - }).Fn, + t.Fill(t.cellBackground, l.Center, t.TextSize.V, 0, + func(gtx l.Context) l.Dimensions { + return t.list.Vertical(). + Length(len(out)). + Background(t.cellBackground). + ListElement(le). + Fn(gtx) + }).Fn, ). Fn(gtx) } diff --git a/gel/text.go b/gel/text.go index 18308a3..e405c7d 100644 --- a/gel/text.go +++ b/gel/text.go @@ -4,12 +4,12 @@ import ( "image" "unicode/utf8" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/op/clip" - "realy.lol/gel/gio/op/paint" - "realy.lol/gel/gio/text" - "realy.lol/gel/gio/unit" + l "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" "golang.org/x/image/math/fixed" ) @@ -45,7 +45,7 @@ type lineIterator struct { Alignment text.Alignment Width int Offset image.Point - + y, prevDesc fixed.Int26_6 txtOff int } @@ -126,7 +126,8 @@ func (l *lineIterator) Next() (text.Layout, image.Point, bool) { // } // } -func (t *Text) Fn(gtx l.Context, s text.Shaper, font text.Font, size unit.Value, txt string) l.Dimensions { +func (t *Text) Fn(gtx l.Context, s text.Shaper, font text.Font, size unit.Value, + txt string) l.Dimensions { cs := gtx.Constraints textSize := fixed.I(gtx.Px(size)) lines := s.LayoutString(font, textSize, cs.Max.X, txt) diff --git a/gel/textinput.go b/gel/textinput.go index 8066fbf..2937d70 100644 --- a/gel/textinput.go +++ b/gel/textinput.go @@ -3,11 +3,11 @@ package gel import ( "image/color" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/op/paint" - "realy.lol/gel/gio/text" - "realy.lol/gel/gio/unit" + l "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" "realy.lol/gel/f32color" ) diff --git a/gel/texttable.go b/gel/texttable.go index 9ae633c..9428c66 100644 --- a/gel/texttable.go +++ b/gel/texttable.go @@ -1,6 +1,6 @@ package gel -import l "realy.lol/gel/gio/layout" +import l "realy.lol/gio/layout" type TextTableHeader []string @@ -67,7 +67,7 @@ func (tt *TextTable) Regenerate(fully bool) { // startIndex = len(tt.Table.body) // D.Ln("startIndex", startIndex, len(tt.Body)) // if startIndex < len(tt.Body) { - + // bd := tt.Body // [startIndex:] diff := len(tt.Body) - len(tt.Table.body) // D.Ln(len(tt.Table.body), len(tt.Body), diff) diff --git a/gel/theme.go b/gel/theme.go index 8e31f46..bd4d5a6 100644 --- a/gel/theme.go +++ b/gel/theme.go @@ -6,8 +6,8 @@ import ( "strconv" "strings" - "realy.lol/gel/gio/text" - "realy.lol/gel/gio/unit" + "realy.lol/gio/text" + "realy.lol/gio/unit" "realy.lol/opts/binary" "realy.lol/qu" ) diff --git a/gel/window.go b/gel/window.go index 5ef832c..8a85a70 100644 --- a/gel/window.go +++ b/gel/window.go @@ -11,22 +11,22 @@ import ( "realy.lol/opts/binary" "realy.lol/opts/meta" - clipboard2 "realy.lol/gel/gio/io/clipboard" + clipboard2 "realy.lol/gio/io/clipboard" "realy.lol/gel/clipboard" "realy.lol/gel/fonts/p9fonts" - "realy.lol/gel/gio/io/event" + "realy.lol/gio/io/event" "realy.lol/qu" "realy.lol/atomic" - "realy.lol/gel/gio/app" - "realy.lol/gel/gio/io/system" - l "realy.lol/gel/gio/layout" - "realy.lol/gel/gio/op" - "realy.lol/gel/gio/unit" + "realy.lol/gio/app" + "realy.lol/gio/io/system" + l "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/unit" ) type CallbackQueue chan func() error diff --git a/gel/wraplist.go b/gel/wraplist.go index 4e21127..ace69f0 100644 --- a/gel/wraplist.go +++ b/gel/wraplist.go @@ -1,7 +1,7 @@ package gel import ( - l "realy.lol/gel/gio/layout" + l "realy.lol/gio/layout" "golang.org/x/exp/shiny/text" ) diff --git a/gio/.builds/apple.yml b/gio/.builds/apple.yml new file mode 100644 index 0000000..fde6bcb --- /dev/null +++ b/gio/.builds/apple.yml @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: Unlicense OR MIT +image: debian/testing +packages: + - clang + - cmake + - curl + - autoconf + - libxml2-dev + - libssl-dev + - libz-dev + - llvm-dev # for cctools + - uuid-dev ## for cctools + - libplist-utils # for gogio + - golang +sources: + - https://git.sr.ht/~eliasnaur/applesdks + - https://git.sr.ht/~eliasnaur/gio + - https://git.sr.ht/~eliasnaur/giouiorg + - https://github.com/tpoechtrager/cctools-port.git + - https://github.com/tpoechtrager/apple-libtapi.git + - https://github.com/mackyle/xar.git +environment: + APPLE_TOOLCHAIN_ROOT: /home/build/appletools + PATH: /home/build/go/bin:/usr/bin +tasks: + - prepare_toolchain: | + mkdir -p $APPLE_TOOLCHAIN_ROOT + cd $APPLE_TOOLCHAIN_ROOT + tar xJf /home/build/applesdks/applesdks.tar.xz + mkdir bin tools + cd bin + ln -s ../toolchain/bin/x86_64-apple-darwin19-ld ld + ln -s ../toolchain/bin/x86_64-apple-darwin19-ar ar + ln -s /home/build/cctools-port/cctools/misc/lipo lipo + ln -s ../tools/appletoolchain xcrun + ln -s /usr/bin/plistutil plutil + cd ../tools + ln -s appletoolchain clang-ios + ln -s appletoolchain clang-macos + - install_appletoolchain: | + cd giouiorg + go build -o $APPLE_TOOLCHAIN_ROOT/tools ./cmd/appletoolchain + - build_xar: | + cd xar/xar + ac_cv_lib_crypto_OpenSSL_add_all_ciphers=yes CC=clang ./autogen.sh --prefix=/usr + make + sudo make install + - build_libtapi: | + cd apple-libtapi + INSTALLPREFIX=$APPLE_TOOLCHAIN_ROOT/libtapi ./build.sh + ./install.sh + - build_cctools: | + cd cctools-port/cctools + ./configure --prefix $APPLE_TOOLCHAIN_ROOT/toolchain --with-libtapi=$APPLE_TOOLCHAIN_ROOT/libtapi --target=x86_64-apple-darwin19 + make install + - test_macos: | + cd gio + export PATH=/home/build/appletools/bin:$PATH + CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-macos GOOS=darwin CGO_ENABLED=1 go build ./... + - test_ios: | + cd gio + CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-ios GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -tags ios ./... + - install_gogio: | + cd gio/cmd + go install ./gogio + - test_ios_gogio: | + mkdir tmp + cd tmp + go mod init example.com + go get -d github.com/p9c/p9/pkg/gel/gio/example/kitchen + export PATH=/home/build/appletools/bin:$PATH + gogio -target ios -o app.app github.com/p9c/p9/pkg/gel/gio/example/kitchen diff --git a/gio/.builds/freebsd.yml b/gio/.builds/freebsd.yml new file mode 100644 index 0000000..1816b70 --- /dev/null +++ b/gio/.builds/freebsd.yml @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: Unlicense OR MIT +image: freebsd/11.x +packages: + - libX11 + - libxkbcommon + - libXcursor + - libXfixes + - wayland + - mesa-libs + - xorg-vfbserver +sources: + - https://git.sr.ht/~eliasnaur/gio +environment: + PATH: /home/build/sdk/go/bin:/bin:/usr/local/bin:/usr/bin +tasks: + - install_go1_14: | + mkdir -p /home/build/sdk + curl https://dl.google.com/go/go1.14.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf - + - test_gio: | + export EGL_PLATFORM=surfaceless # for headless tests + cd gio + go test ./... + - test_cmd: | + cd gio/cmd + go test ./... diff --git a/gio/.builds/linux.yml b/gio/.builds/linux.yml new file mode 100644 index 0000000..55cb5a5 --- /dev/null +++ b/gio/.builds/linux.yml @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: Unlicense OR MIT +image: debian/testing +packages: + - curl + - pkg-config + - libwayland-dev + - libx11-dev + - libx11-xcb-dev + - libxkbcommon-dev + - libxkbcommon-x11-dev + - libgles2-mesa-dev + - libegl1-mesa-dev + - libffi-dev + - libxcursor-dev + - libxrandr-dev + - libxinerama-dev + - libxi-dev + - libxxf86vm-dev + - wine + - xvfb + - xdotool + - scrot + - sway + - grim + - wine + - unzip +sources: + - https://git.sr.ht/~eliasnaur/gio +environment: + GOFLAGS: -mod=readonly + PATH: /home/build/sdk/go/bin:/usr/bin:/home/build/go/bin:/home/build/android/tools/bin + ANDROID_SDK_ROOT: /home/build/android + android_sdk_tools_zip: sdk-tools-linux-3859397.zip + android_ndk_zip: android-ndk-r20-linux-x86_64.zip + github_mirror: git@github.com:gioui/gio +secrets: + - 75d8a1eb-5fc5-4074-8a36-db6015d6ed5a +tasks: + - install_go1_14: | + mkdir -p /home/build/sdk + curl -s https://dl.google.com/go/go1.14.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf - + - test_gio: | + cd gio + export EGL_PLATFORM=surfaceless # for headless tests + go test -race ./... + GOOS=windows go test -exec=wine ./... + GOOS=js GOARCH=wasm go build -o /dev/null ./... + - install_chrome: | + curl -s https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' + sudo apt-get -qq update + sudo apt-get -qq install -y google-chrome-stable + - test_cmd: | + cd gio/cmd + go test ./... + go test -race ./... + cd gogio # since we need -modfile to point at the parent directory + GOFLAGS=-modfile=../go.local.mod go test + - install_jdk8: | + curl -so jdk.deb "https://cdn.azul.com/zulu/bin/zulu8.42.0.21-ca-jdk8.0.232-linux_amd64.deb" + sudo apt-get -qq install -y -f ./jdk.deb + - install_android: | + mkdir android + cd android + curl -so sdk-tools.zip https://dl.google.com/android/repository/$android_sdk_tools_zip + unzip -q sdk-tools.zip + rm sdk-tools.zip + curl -so ndk.zip https://dl.google.com/android/repository/$android_ndk_zip + unzip -q ndk.zip + rm ndk.zip + mv android-ndk-* ndk-bundle + yes|sdkmanager --licenses + sdkmanager "platforms;android-29" "build-tools;29.0.2" + - test_android: | + cd gio + CC=$ANDROID_SDK_ROOT/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build ./... + - install_gogio: | + cd gio/cmd + go install ./gogio + - test_android_gogio: | + mkdir tmp + cd tmp + go mod init example.com + go get -d github.com/p9c/p9/pkg/gel/gio/example/kitchen + gogio -target android github.com/p9c/p9/pkg/gel/gio/example/kitchen + - check_gofmt: | + cd gio + test -z "$(gofmt -s -l .)" + - check_sign_off: | + set +x -e + cd gio + for hash in $(git log -n 20 --format="%H"); do + message=$(git log -1 --format=%B $hash) + if [[ ! "$message" =~ "Signed-off-by: " ]]; then + echo "Missing 'Signed-off-by' in commit $hash" + exit 1 + fi + done + - mirror: | + # mirror to github + ssh-keyscan github.com > "$HOME"/.ssh/known_hosts && cd gio && git push --mirror "$github_mirror" || echo "failed mirroring" diff --git a/gio/.builds/openbsd.yml b/gio/.builds/openbsd.yml new file mode 100644 index 0000000..757e80f --- /dev/null +++ b/gio/.builds/openbsd.yml @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Unlicense OR MIT +image: openbsd/latest +packages: + - libxkbcommon + - go +sources: + - https://git.sr.ht/~eliasnaur/gio +environment: + PATH: /home/build/sdk/go/bin:/bin:/usr/local/bin:/usr/bin +tasks: + - install_go1_14: | + mkdir -p /home/build/sdk + curl https://dl.google.com/go/go1.14.src.tar.gz | tar -C /home/build/sdk -xzf - + cd /home/build/sdk/go/src + ./make.bash + - test_gio: | + cd gio + go test ./... + - test_cmd: | + cd gio/cmd + go test ./... diff --git a/gio/LICENSE b/gio/LICENSE new file mode 100644 index 0000000..81f4733 --- /dev/null +++ b/gio/LICENSE @@ -0,0 +1,63 @@ +This project is provided under the terms of the UNLICENSE or +the MIT license denoted by the following SPDX identifier: + +SPDX-License-Identifier: Unlicense OR MIT + +You may use the project under the terms of either license. + +Both licenses are reproduced below. + +---- +The MIT License (MIT) + +Copyright (c) 2019 The Gio authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +--- + + + +--- +The UNLICENSE + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to +--- diff --git a/gio/README.md b/gio/README.md new file mode 100644 index 0000000..634cb42 --- /dev/null +++ b/gio/README.md @@ -0,0 +1,26 @@ +# Gio - https://github.com/p9c/p9/pkg/gel/gio + +Immediate mode GUI programs in Go for Android, iOS, macOS, Linux, +FreeBSD, OpenBSD, Windows, and WebAssembly (experimental). + +# Installation, examples, documentation + +Go to [github.com/p9c/p9/pkg/gel/gio](https://github.com/p9c/p9/pkg/gel/gio). + +[![builds.sr.ht status](https://builds.sr.ht/~eliasnaur/gio.svg)](https://builds.sr.ht/~eliasnaur/gio) + +## Issues + +File bugs and TODOs through the [issue tracker](https://todo.sr.ht/~eliasnaur/gio) or send an email +to [~eliasnaur/gio@todo.sr.ht](mailto:~eliasnaur/gio@todo.sr.ht). For general discussion, use the +mailing list: [~eliasnaur/gio@lists.sr.ht](mailto:~eliasnaur/gio@lists.sr.ht). + +## Contributing + +Post discussion to the [mailing list](https://lists.sr.ht/~eliasnaur/gio) and patches to +[gio-patches](https://lists.sr.ht/~eliasnaur/gio-patches). No Sourcehut +account is required and you can post without being subscribed. + +See the [contribution guide](https://github.com/p9c/p9/pkg/gel/gio/doc/contribute) for more details. + +An [official GitHub mirror](https://github.com/gioui/gio) is available. diff --git a/gio/app/app.go b/gio/app/app.go new file mode 100644 index 0000000..e9fbdf7 --- /dev/null +++ b/gio/app/app.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +import ( + "os" + "strings" + + "realy.lol/gio/app/internal/wm" +) + +// extraArgs contains extra arguments to append to +// os.Args. The arguments are separated with |. +// Useful for running programs on mobiles where the +// command line is not available. +// Set with the go linker flag -X. +var extraArgs string + +func init() { + if extraArgs != "" { + args := strings.Split(extraArgs, "|") + os.Args = append(os.Args, args...) + } +} + +// DataDir returns a path to use for application-specific +// configuration data. +// On desktop systems, DataDir use os.UserConfigDir. +// On iOS NSDocumentDirectory is queried. +// For Android Context.getFilesDir is used. +// +// BUG: DataDir blocks on Android until init functions +// have completed. +func DataDir() (string, error) { + return dataDir() +} + +// Main must be called last from the program main function. +// On most platforms Main blocks forever, for Android and +// iOS it returns immediately to give control of the main +// thread back to the system. +// +// Calling Main is necessary because some operating systems +// require control of the main thread of the program for +// running windows. +func Main() { + wm.Main() +} diff --git a/gio/app/app_android.go b/gio/app/app_android.go new file mode 100644 index 0000000..060544f --- /dev/null +++ b/gio/app/app_android.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +import ( + "realy.lol/gio/app/internal/wm" +) + +type ViewEvent = wm.ViewEvent + +// JavaVM returns the global JNI JavaVM. +func JavaVM() uintptr { + return wm.JavaVM() +} + +// AppContext returns the global Application context as a JNI +// jobject. +func AppContext() uintptr { + return wm.AppContext() +} diff --git a/gio/app/datadir.go b/gio/app/datadir.go new file mode 100644 index 0000000..31e5453 --- /dev/null +++ b/gio/app/datadir.go @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build !android + +package app + +import "os" + +func dataDir() (string, error) { + return os.UserConfigDir() +} diff --git a/gio/app/datadir_android.go b/gio/app/datadir_android.go new file mode 100644 index 0000000..cbbc6c4 --- /dev/null +++ b/gio/app/datadir_android.go @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build android +// +build android + +package app + +import "C" + +import ( + "os" + "path/filepath" + "sync" + + "realy.lol/gio/app/internal/wm" +) + +var ( + dataDirOnce sync.Once + dataPath string +) + +func dataDir() (string, error) { + dataDirOnce.Do(func() { + dataPath = wm.GetDataDir() + // Set XDG_CACHE_HOME to make os.UserCacheDir work. + if _, exists := os.LookupEnv("XDG_CACHE_HOME"); !exists { + cachePath := filepath.Join(dataPath, "cache") + os.Setenv("XDG_CACHE_HOME", cachePath) + } + // Set XDG_CONFIG_HOME to make os.UserConfigDir work. + if _, exists := os.LookupEnv("XDG_CONFIG_HOME"); !exists { + cfgPath := filepath.Join(dataPath, "config") + os.Setenv("XDG_CONFIG_HOME", cfgPath) + } + // Set HOME to make os.UserHomeDir work. + if _, exists := os.LookupEnv("HOME"); !exists { + os.Setenv("HOME", dataPath) + } + }) + return dataPath, nil +} diff --git a/gio/app/doc.go b/gio/app/doc.go new file mode 100644 index 0000000..fb0826a --- /dev/null +++ b/gio/app/doc.go @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package app provides a platform-independent interface to operating system +functionality for running graphical user interfaces. + +See https://realy.lol/gio for instructions to set up and run Gio programs. + +Windows + +Create a new Window by calling NewWindow. On mobile platforms or when Gio +is embedded in another project, NewWindow merely connects with a previously +created window. + +A Window is run by receiving events from its Events channel. The most +important event is FrameEvent that prompts an update of the window +contents and state. + +For example: + + import "realy.lol/gio/unit" + + w := app.NewWindow() + for e := range w.Events() { + if e, ok := e.(system.FrameEvent); ok { + ops.Reset() + // Add operations to ops. + ... + // Completely replace the window contents and state. + e.Frame(ops) + } + } + +A program must keep receiving events from the event channel until +DestroyEvent is received. + +Main + +The Main function must be called from a program's main function, to hand over +control of the main thread to operating systems that need it. + +Because Main is also blocking on some platforms, the event loop of a Window must run in a goroutine. + +For example, to display a blank but otherwise functional window: + + package main + + import "realy.lol/gio/app" + + func main() { + go func() { + w := app.NewWindow() + for range w.Events() { + } + }() + app.Main() + } + + +Event queue + +A FrameEvent's Queue method returns an event.Queue implementation that distributes +incoming events to the event handlers declared in the last frame. +See the realy.lol/gio/io/event package for more information about event handlers. + +Permissions + +The packages under realy.lol/gio/app/permission should be imported +by a Gio program or by one of its dependencies to indicate that specific +operating-system permissions are required. Please see documentation for +package realy.lol/gio/app/permission for more information. +*/ +package app diff --git a/gio/app/internal/log/log.go b/gio/app/internal/log/log.go new file mode 100644 index 0000000..731ae49 --- /dev/null +++ b/gio/app/internal/log/log.go @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package points standard output, standard error and the standard +// library package log to the platform logger. +package log + +var appID = "gio" diff --git a/gio/app/internal/log/log_android.go b/gio/app/internal/log/log_android.go new file mode 100644 index 0000000..1245598 --- /dev/null +++ b/gio/app/internal/log/log_android.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package log + +/* +#cgo LDFLAGS: -llog + +#include +#include +*/ +import "C" + +import ( + "bufio" + "log" + "os" + "runtime" + "syscall" + "unsafe" +) + +// 1024 is the truncation limit from android/log.h, plus a \n. +const logLineLimit = 1024 + +var logTag = C.CString(appID) + +func init() { + // Android's logcat already includes timestamps. + log.SetFlags(log.Flags() &^ log.LstdFlags) + log.SetOutput(new(androidLogWriter)) + + // Redirect stdout and stderr to the Android logger. + logFd(os.Stdout.Fd()) + logFd(os.Stderr.Fd()) +} + +type androidLogWriter struct { + // buf has room for the maximum log line, plus a terminating '\0'. + buf [logLineLimit + 1]byte +} + +func (w *androidLogWriter) Write(data []byte) (int, error) { + n := 0 + for len(data) > 0 { + msg := data + // Truncate the buffer, leaving space for the '\0'. + if max := len(w.buf) - 1; len(msg) > max { + msg = msg[:max] + } + buf := w.buf[:len(msg)+1] + copy(buf, msg) + // Terminating '\0'. + buf[len(msg)] = 0 + C.__android_log_write(C.ANDROID_LOG_INFO, logTag, (*C.char)(unsafe.Pointer(&buf[0]))) + n += len(msg) + data = data[len(msg):] + } + return n, nil +} + +func logFd(fd uintptr) { + r, w, err := os.Pipe() + if err != nil { + panic(err) + } + if err := syscall.Dup3(int(w.Fd()), int(fd), syscall.O_CLOEXEC); err != nil { + panic(err) + } + go func() { + lineBuf := bufio.NewReaderSize(r, logLineLimit) + // The buffer to pass to C, including the terminating '\0'. + buf := make([]byte, lineBuf.Size()+1) + cbuf := (*C.char)(unsafe.Pointer(&buf[0])) + for { + line, _, err := lineBuf.ReadLine() + if err != nil { + break + } + copy(buf, line) + buf[len(line)] = 0 + C.__android_log_write(C.ANDROID_LOG_INFO, logTag, cbuf) + } + // The garbage collector doesn't know that w's fd was dup'ed. + // Avoid finalizing w, and thereby avoid its finalizer closing its fd. + runtime.KeepAlive(w) + }() +} diff --git a/gio/app/internal/log/log_ios.go b/gio/app/internal/log/log_ios.go new file mode 100644 index 0000000..6a041db --- /dev/null +++ b/gio/app/internal/log/log_ios.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build darwin && ios +// +build darwin,ios + +package log + +/* +#cgo CFLAGS: -Werror -fmodules -fobjc-arc -x objective-c + +__attribute__ ((visibility ("hidden"))) void nslog(char *str); +*/ +import "C" + +import ( + "bufio" + "io" + "log" + "unsafe" + + _ "realy.lol/gio/internal/cocoainit" +) + +func init() { + // macOS Console already includes timestamps. + log.SetFlags(log.Flags() &^ log.LstdFlags) + log.SetOutput(newNSLogWriter()) +} + +func newNSLogWriter() io.Writer { + r, w := io.Pipe() + go func() { + // 1024 is an arbitrary truncation limit, taken from Android's + // log buffer size. + lineBuf := bufio.NewReaderSize(r, 1024) + // The buffer to pass to C, including the terminating '\0'. + buf := make([]byte, lineBuf.Size()+1) + cbuf := (*C.char)(unsafe.Pointer(&buf[0])) + for { + line, _, err := lineBuf.ReadLine() + if err != nil { + break + } + copy(buf, line) + buf[len(line)] = 0 + C.nslog(cbuf) + } + }() + return w +} diff --git a/gio/app/internal/log/log_ios.m b/gio/app/internal/log/log_ios.m new file mode 100644 index 0000000..201bc36 --- /dev/null +++ b/gio/app/internal/log/log_ios.m @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,ios + +@import Foundation; + +#include "_cgo_export.h" + +void nslog(char *str) { + NSLog(@"%@", @(str)); +} diff --git a/gio/app/internal/log/log_windows.go b/gio/app/internal/log/log_windows.go new file mode 100644 index 0000000..13c5fe4 --- /dev/null +++ b/gio/app/internal/log/log_windows.go @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package log + +import ( + "log" + "syscall" + "unsafe" +) + +type logger struct{} + +var ( + kernel32 = syscall.NewLazyDLL("kernel32") + outputDebugStringW = kernel32.NewProc("OutputDebugStringW") + debugView *logger +) + +func init() { + // Windows DebugView already includes timestamps. + if syscall.Stderr == 0 { + log.SetFlags(log.Flags() &^ log.LstdFlags) + log.SetOutput(debugView) + } +} + +func (l *logger) Write(buf []byte) (int, error) { + p, err := syscall.UTF16PtrFromString(string(buf)) + if err != nil { + return 0, err + } + outputDebugStringW.Call(uintptr(unsafe.Pointer(p))) + return len(buf), nil +} diff --git a/gio/app/internal/windows/windows.go b/gio/app/internal/windows/windows.go new file mode 100644 index 0000000..8af575e --- /dev/null +++ b/gio/app/internal/windows/windows.go @@ -0,0 +1,673 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build windows + +package windows + +import ( + "fmt" + "runtime" + "time" + "unsafe" + + syscall "golang.org/x/sys/windows" +) + +type Rect struct { + Left, Top, Right, Bottom int32 +} + +type WndClassEx struct { + CbSize uint32 + Style uint32 + LpfnWndProc uintptr + CnClsExtra int32 + CbWndExtra int32 + HInstance syscall.Handle + HIcon syscall.Handle + HCursor syscall.Handle + HbrBackground syscall.Handle + LpszMenuName *uint16 + LpszClassName *uint16 + HIconSm syscall.Handle +} + +type Msg struct { + Hwnd syscall.Handle + Message uint32 + WParam uintptr + LParam uintptr + Time uint32 + Pt Point + LPrivate uint32 +} + +type Point struct { + X, Y int32 +} + +type MinMaxInfo struct { + PtReserved Point + PtMaxSize Point + PtMaxPosition Point + PtMinTrackSize Point + PtMaxTrackSize Point +} + +type WindowPlacement struct { + length uint32 + flags uint32 + showCmd uint32 + ptMinPosition Point + ptMaxPosition Point + rcNormalPosition Rect + rcDevice Rect +} + +type MonitorInfo struct { + cbSize uint32 + Monitor Rect + WorkArea Rect + Flags uint32 +} + +const ( + TRUE = 1 + + CS_HREDRAW = 0x0002 + CS_VREDRAW = 0x0001 + CS_OWNDC = 0x0020 + + CW_USEDEFAULT = -2147483648 + + GWL_STYLE = ^(uint32(16) - 1) // -16 + HWND_TOPMOST = ^(uint32(1) - 1) // -1 + + HTCLIENT = 1 + + IDC_ARROW = 32512 + IDC_IBEAM = 32513 + IDC_HAND = 32649 + IDC_CROSS = 32515 + IDC_SIZENS = 32645 + IDC_SIZEWE = 32644 + IDC_SIZEALL = 32646 + + INFINITE = 0xFFFFFFFF + + LOGPIXELSX = 88 + + MDT_EFFECTIVE_DPI = 0 + + MONITOR_DEFAULTTOPRIMARY = 1 + + SIZE_MAXIMIZED = 2 + SIZE_MINIMIZED = 1 + SIZE_RESTORED = 0 + + SW_SHOWDEFAULT = 10 + + SWP_FRAMECHANGED = 0x0020 + SWP_NOMOVE = 0x0002 + SWP_NOOWNERZORDER = 0x0200 + SWP_NOSIZE = 0x0001 + SWP_NOZORDER = 0x0004 + + USER_TIMER_MINIMUM = 0x0000000A + + VK_CONTROL = 0x11 + VK_LWIN = 0x5B + VK_MENU = 0x12 + VK_RWIN = 0x5C + VK_SHIFT = 0x10 + + VK_BACK = 0x08 + VK_DELETE = 0x2e + VK_DOWN = 0x28 + VK_END = 0x23 + VK_ESCAPE = 0x1b + VK_HOME = 0x24 + VK_LEFT = 0x25 + VK_NEXT = 0x22 + VK_PRIOR = 0x21 + VK_RIGHT = 0x27 + VK_RETURN = 0x0d + VK_SPACE = 0x20 + VK_TAB = 0x09 + VK_UP = 0x26 + + VK_F1 = 0x70 + VK_F2 = 0x71 + VK_F3 = 0x72 + VK_F4 = 0x73 + VK_F5 = 0x74 + VK_F6 = 0x75 + VK_F7 = 0x76 + VK_F8 = 0x77 + VK_F9 = 0x78 + VK_F10 = 0x79 + VK_F11 = 0x7A + VK_F12 = 0x7B + + VK_OEM_1 = 0xba + VK_OEM_PLUS = 0xbb + VK_OEM_COMMA = 0xbc + VK_OEM_MINUS = 0xbd + VK_OEM_PERIOD = 0xbe + VK_OEM_2 = 0xbf + VK_OEM_3 = 0xc0 + VK_OEM_4 = 0xdb + VK_OEM_5 = 0xdc + VK_OEM_6 = 0xdd + VK_OEM_7 = 0xde + VK_OEM_102 = 0xe2 + + UNICODE_NOCHAR = 65535 + + WM_CANCELMODE = 0x001F + WM_CHAR = 0x0102 + WM_CREATE = 0x0001 + WM_DPICHANGED = 0x02E0 + WM_DESTROY = 0x0002 + WM_ERASEBKGND = 0x0014 + WM_KEYDOWN = 0x0100 + WM_KEYUP = 0x0101 + WM_LBUTTONDOWN = 0x0201 + WM_LBUTTONUP = 0x0202 + WM_MBUTTONDOWN = 0x0207 + WM_MBUTTONUP = 0x0208 + WM_MOUSEMOVE = 0x0200 + WM_MOUSEWHEEL = 0x020A + WM_MOUSEHWHEEL = 0x020E + WM_PAINT = 0x000F + WM_CLOSE = 0x0010 + WM_QUIT = 0x0012 + WM_SETCURSOR = 0x0020 + WM_SETFOCUS = 0x0007 + WM_KILLFOCUS = 0x0008 + WM_SHOWWINDOW = 0x0018 + WM_SIZE = 0x0005 + WM_SYSKEYDOWN = 0x0104 + WM_SYSKEYUP = 0x0105 + WM_RBUTTONDOWN = 0x0204 + WM_RBUTTONUP = 0x0205 + WM_TIMER = 0x0113 + WM_UNICHAR = 0x0109 + WM_USER = 0x0400 + WM_GETMINMAXINFO = 0x0024 + + WS_CLIPCHILDREN = 0x00010000 + WS_CLIPSIBLINGS = 0x04000000 + WS_VISIBLE = 0x10000000 + WS_OVERLAPPED = 0x00000000 + WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | + WS_MINIMIZEBOX | WS_MAXIMIZEBOX + WS_CAPTION = 0x00C00000 + WS_SYSMENU = 0x00080000 + WS_THICKFRAME = 0x00040000 + WS_MINIMIZEBOX = 0x00020000 + WS_MAXIMIZEBOX = 0x00010000 + + WS_EX_APPWINDOW = 0x00040000 + WS_EX_WINDOWEDGE = 0x00000100 + + QS_ALLINPUT = 0x04FF + + MWMO_WAITALL = 0x0001 + MWMO_INPUTAVAILABLE = 0x0004 + + WAIT_OBJECT_0 = 0 + + PM_REMOVE = 0x0001 + PM_NOREMOVE = 0x0000 + + GHND = 0x0042 + + CF_UNICODETEXT = 13 + IMAGE_BITMAP = 0 + IMAGE_ICON = 1 + IMAGE_CURSOR = 2 + + LR_CREATEDIBSECTION = 0x00002000 + LR_DEFAULTCOLOR = 0x00000000 + LR_DEFAULTSIZE = 0x00000040 + LR_LOADFROMFILE = 0x00000010 + LR_LOADMAP3DCOLORS = 0x00001000 + LR_LOADTRANSPARENT = 0x00000020 + LR_MONOCHROME = 0x00000001 + LR_SHARED = 0x00008000 + LR_VGACOLOR = 0x00000080 +) + +var ( + kernel32 = syscall.NewLazySystemDLL("kernel32.dll") + _GetModuleHandleW = kernel32.NewProc("GetModuleHandleW") + _GlobalAlloc = kernel32.NewProc("GlobalAlloc") + _GlobalFree = kernel32.NewProc("GlobalFree") + _GlobalLock = kernel32.NewProc("GlobalLock") + _GlobalUnlock = kernel32.NewProc("GlobalUnlock") + + user32 = syscall.NewLazySystemDLL("user32.dll") + _AdjustWindowRectEx = user32.NewProc("AdjustWindowRectEx") + _CallMsgFilter = user32.NewProc("CallMsgFilterW") + _CloseClipboard = user32.NewProc("CloseClipboard") + _CreateWindowEx = user32.NewProc("CreateWindowExW") + _DefWindowProc = user32.NewProc("DefWindowProcW") + _DestroyWindow = user32.NewProc("DestroyWindow") + _DispatchMessage = user32.NewProc("DispatchMessageW") + _EmptyClipboard = user32.NewProc("EmptyClipboard") + _GetClientRect = user32.NewProc("GetClientRect") + _GetClipboardData = user32.NewProc("GetClipboardData") + _GetDC = user32.NewProc("GetDC") + _GetDpiForWindow = user32.NewProc("GetDpiForWindow") + _GetKeyState = user32.NewProc("GetKeyState") + _GetMessage = user32.NewProc("GetMessageW") + _GetMessageTime = user32.NewProc("GetMessageTime") + _GetMonitorInfo = user32.NewProc("GetMonitorInfoW") + _GetWindowLong = user32.NewProc("GetWindowLongPtrW") + _GetWindowPlacement = user32.NewProc("GetWindowPlacement") + _KillTimer = user32.NewProc("KillTimer") + _LoadCursor = user32.NewProc("LoadCursorW") + _LoadImage = user32.NewProc("LoadImageW") + _MonitorFromPoint = user32.NewProc("MonitorFromPoint") + _MonitorFromWindow = user32.NewProc("MonitorFromWindow") + _MoveWindow = user32.NewProc("MoveWindow") + _MsgWaitForMultipleObjectsEx = user32.NewProc("MsgWaitForMultipleObjectsEx") + _OpenClipboard = user32.NewProc("OpenClipboard") + _PeekMessage = user32.NewProc("PeekMessageW") + _PostMessage = user32.NewProc("PostMessageW") + _PostQuitMessage = user32.NewProc("PostQuitMessage") + _ReleaseCapture = user32.NewProc("ReleaseCapture") + _RegisterClassExW = user32.NewProc("RegisterClassExW") + _ReleaseDC = user32.NewProc("ReleaseDC") + _ScreenToClient = user32.NewProc("ScreenToClient") + _ShowWindow = user32.NewProc("ShowWindow") + _SetCapture = user32.NewProc("SetCapture") + _SetCursor = user32.NewProc("SetCursor") + _SetClipboardData = user32.NewProc("SetClipboardData") + _SetForegroundWindow = user32.NewProc("SetForegroundWindow") + _SetFocus = user32.NewProc("SetFocus") + _SetProcessDPIAware = user32.NewProc("SetProcessDPIAware") + _SetTimer = user32.NewProc("SetTimer") + _SetWindowLong = user32.NewProc("SetWindowLongPtrW") + _SetWindowPlacement = user32.NewProc("SetWindowPlacement") + _SetWindowPos = user32.NewProc("SetWindowPos") + _SetWindowText = user32.NewProc("SetWindowTextW") + _TranslateMessage = user32.NewProc("TranslateMessage") + _UnregisterClass = user32.NewProc("UnregisterClassW") + _UpdateWindow = user32.NewProc("UpdateWindow") + + shcore = syscall.NewLazySystemDLL("shcore") + _GetDpiForMonitor = shcore.NewProc("GetDpiForMonitor") + + gdi32 = syscall.NewLazySystemDLL("gdi32") + _GetDeviceCaps = gdi32.NewProc("GetDeviceCaps") +) + +func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) { + _AdjustWindowRectEx.Call(uintptr(unsafe.Pointer(r)), uintptr(dwStyle), uintptr(bMenu), uintptr(dwExStyle)) + issue34474KeepAlive(r) +} + +func CallMsgFilter(m *Msg, nCode uintptr) bool { + r, _, _ := _CallMsgFilter.Call(uintptr(unsafe.Pointer(m)), nCode) + issue34474KeepAlive(m) + return r != 0 +} + +func CloseClipboard() error { + r, _, err := _CloseClipboard.Call() + if r == 0 { + return fmt.Errorf("CloseClipboard: %v", err) + } + return nil +} + +func CreateWindowEx(dwExStyle uint32, lpClassName uint16, lpWindowName string, dwStyle uint32, x, y, w, h int32, hWndParent, hMenu, hInstance syscall.Handle, lpParam uintptr) (syscall.Handle, error) { + wname := syscall.StringToUTF16Ptr(lpWindowName) + hwnd, _, err := _CreateWindowEx.Call( + uintptr(dwExStyle), + uintptr(lpClassName), + uintptr(unsafe.Pointer(wname)), + uintptr(dwStyle), + uintptr(x), uintptr(y), + uintptr(w), uintptr(h), + uintptr(hWndParent), + uintptr(hMenu), + uintptr(hInstance), + uintptr(lpParam)) + issue34474KeepAlive(wname) + if hwnd == 0 { + return 0, fmt.Errorf("CreateWindowEx failed: %v", err) + } + return syscall.Handle(hwnd), nil +} + +func DefWindowProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr { + r, _, _ := _DefWindowProc.Call(uintptr(hwnd), uintptr(msg), wparam, lparam) + return r +} + +func DestroyWindow(hwnd syscall.Handle) { + _DestroyWindow.Call(uintptr(hwnd)) +} + +func DispatchMessage(m *Msg) { + _DispatchMessage.Call(uintptr(unsafe.Pointer(m))) + issue34474KeepAlive(m) +} + +func EmptyClipboard() error { + r, _, err := _EmptyClipboard.Call() + if r == 0 { + return fmt.Errorf("EmptyClipboard: %v", err) + } + return nil +} + +func GetClientRect(hwnd syscall.Handle, r *Rect) { + _GetClientRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(r))) + issue34474KeepAlive(r) +} + +func GetClipboardData(format uint32) (syscall.Handle, error) { + r, _, err := _GetClipboardData.Call(uintptr(format)) + if r == 0 { + return 0, fmt.Errorf("GetClipboardData: %v", err) + } + return syscall.Handle(r), nil +} + +func GetDC(hwnd syscall.Handle) (syscall.Handle, error) { + hdc, _, err := _GetDC.Call(uintptr(hwnd)) + if hdc == 0 { + return 0, fmt.Errorf("GetDC failed: %v", err) + } + return syscall.Handle(hdc), nil +} + +func GetModuleHandle() (syscall.Handle, error) { + h, _, err := _GetModuleHandleW.Call(uintptr(0)) + if h == 0 { + return 0, fmt.Errorf("GetModuleHandleW failed: %v", err) + } + return syscall.Handle(h), nil +} + +func getDeviceCaps(hdc syscall.Handle, index int32) int { + c, _, _ := _GetDeviceCaps.Call(uintptr(hdc), uintptr(index)) + return int(c) +} + +func getDpiForMonitor(hmonitor syscall.Handle, dpiType uint32) int { + var dpiX, dpiY uintptr + _GetDpiForMonitor.Call(uintptr(hmonitor), uintptr(dpiType), uintptr(unsafe.Pointer(&dpiX)), uintptr(unsafe.Pointer(&dpiY))) + return int(dpiX) +} + +// GetSystemDPI returns the effective DPI of the system. +func GetSystemDPI() int { + // Check for GetDpiForMonitor, introduced in Windows 8.1. + if _GetDpiForMonitor.Find() == nil { + hmon := monitorFromPoint(Point{}, MONITOR_DEFAULTTOPRIMARY) + return getDpiForMonitor(hmon, MDT_EFFECTIVE_DPI) + } else { + // Fall back to the physical device DPI. + screenDC, err := GetDC(0) + if err != nil { + return 96 + } + defer ReleaseDC(screenDC) + return getDeviceCaps(screenDC, LOGPIXELSX) + } +} + +func GetKeyState(nVirtKey int32) int16 { + c, _, _ := _GetKeyState.Call(uintptr(nVirtKey)) + return int16(c) +} + +func GetMessage(m *Msg, hwnd syscall.Handle, wMsgFilterMin, wMsgFilterMax uint32) int32 { + r, _, _ := _GetMessage.Call(uintptr(unsafe.Pointer(m)), + uintptr(hwnd), + uintptr(wMsgFilterMin), + uintptr(wMsgFilterMax)) + issue34474KeepAlive(m) + return int32(r) +} + +func GetMessageTime() time.Duration { + r, _, _ := _GetMessageTime.Call() + return time.Duration(r) * time.Millisecond +} + +// GetWindowDPI returns the effective DPI of the window. +func GetWindowDPI(hwnd syscall.Handle) int { + // Check for GetDpiForWindow, introduced in Windows 10. + if _GetDpiForWindow.Find() == nil { + dpi, _, _ := _GetDpiForWindow.Call(uintptr(hwnd)) + return int(dpi) + } else { + return GetSystemDPI() + } +} + +func GetWindowPlacement(hwnd syscall.Handle) *WindowPlacement { + var wp WindowPlacement + wp.length = uint32(unsafe.Sizeof(wp)) + _GetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&wp))) + return &wp +} + +func GetMonitorInfo(hwnd syscall.Handle) MonitorInfo { + var mi MonitorInfo + mi.cbSize = uint32(unsafe.Sizeof(mi)) + v, _, _ := _MonitorFromWindow.Call(uintptr(hwnd), MONITOR_DEFAULTTOPRIMARY) + _GetMonitorInfo.Call(v, uintptr(unsafe.Pointer(&mi))) + return mi +} + +func GetWindowLong(hwnd syscall.Handle) (style uintptr) { + style, _, _ = _GetWindowLong.Call(uintptr(hwnd), uintptr(GWL_STYLE)) + return +} + +func SetWindowLong(hwnd syscall.Handle, idx uint32, style uintptr) { + _SetWindowLong.Call(uintptr(hwnd), uintptr(idx), style) +} + +func SetWindowPlacement(hwnd syscall.Handle, wp *WindowPlacement) { + _SetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wp))) +} + +func SetWindowPos(hwnd syscall.Handle, hwndInsertAfter uint32, x, y, dx, dy int32, style uintptr) { + _SetWindowPos.Call(uintptr(hwnd), uintptr(hwndInsertAfter), + uintptr(x), uintptr(y), + uintptr(dx), uintptr(dy), + style, + ) +} + +func SetWindowText(hwnd syscall.Handle, title string) { + wname := syscall.StringToUTF16Ptr(title) + _SetWindowText.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wname))) +} + +func GlobalAlloc(size int) (syscall.Handle, error) { + r, _, err := _GlobalAlloc.Call(GHND, uintptr(size)) + if r == 0 { + return 0, fmt.Errorf("GlobalAlloc: %v", err) + } + return syscall.Handle(r), nil +} + +func GlobalFree(h syscall.Handle) { + _GlobalFree.Call(uintptr(h)) +} + +func GlobalLock(h syscall.Handle) (uintptr, error) { + r, _, err := _GlobalLock.Call(uintptr(h)) + if r == 0 { + return 0, fmt.Errorf("GlobalLock: %v", err) + } + return r, nil +} + +func GlobalUnlock(h syscall.Handle) { + _GlobalUnlock.Call(uintptr(h)) +} + +func KillTimer(hwnd syscall.Handle, nIDEvent uintptr) error { + r, _, err := _SetTimer.Call(uintptr(hwnd), uintptr(nIDEvent), 0, 0) + if r == 0 { + return fmt.Errorf("KillTimer failed: %v", err) + } + return nil +} + +func LoadCursor(curID uint16) (syscall.Handle, error) { + h, _, err := _LoadCursor.Call(0, uintptr(curID)) + if h == 0 { + return 0, fmt.Errorf("LoadCursorW failed: %v", err) + } + return syscall.Handle(h), nil +} + +func LoadImage(hInst syscall.Handle, res uint32, typ uint32, cx, cy int, fuload uint32) (syscall.Handle, error) { + h, _, err := _LoadImage.Call(uintptr(hInst), uintptr(res), uintptr(typ), uintptr(cx), uintptr(cy), uintptr(fuload)) + if h == 0 { + return 0, fmt.Errorf("LoadImageW failed: %v", err) + } + return syscall.Handle(h), nil +} + +func MoveWindow(hwnd syscall.Handle, x, y, width, height int32, repaint bool) { + var paint uintptr + if repaint { + paint = TRUE + } + _MoveWindow.Call(uintptr(hwnd), uintptr(x), uintptr(y), uintptr(width), uintptr(height), paint) +} + +func monitorFromPoint(pt Point, flags uint32) syscall.Handle { + r, _, _ := _MonitorFromPoint.Call(uintptr(pt.X), uintptr(pt.Y), uintptr(flags)) + return syscall.Handle(r) +} + +func MsgWaitForMultipleObjectsEx(nCount uint32, pHandles uintptr, millis, mask, flags uint32) (uint32, error) { + r, _, err := _MsgWaitForMultipleObjectsEx.Call(uintptr(nCount), pHandles, uintptr(millis), uintptr(mask), uintptr(flags)) + res := uint32(r) + if res == 0xFFFFFFFF { + return 0, fmt.Errorf("MsgWaitForMultipleObjectsEx failed: %v", err) + } + return res, nil +} + +func OpenClipboard(hwnd syscall.Handle) error { + r, _, err := _OpenClipboard.Call(uintptr(hwnd)) + if r == 0 { + return fmt.Errorf("OpenClipboard: %v", err) + } + return nil +} + +func PeekMessage(m *Msg, hwnd syscall.Handle, wMsgFilterMin, wMsgFilterMax, wRemoveMsg uint32) bool { + r, _, _ := _PeekMessage.Call(uintptr(unsafe.Pointer(m)), uintptr(hwnd), uintptr(wMsgFilterMin), uintptr(wMsgFilterMax), uintptr(wRemoveMsg)) + issue34474KeepAlive(m) + return r != 0 +} + +func PostQuitMessage(exitCode uintptr) { + _PostQuitMessage.Call(exitCode) +} + +func PostMessage(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) error { + r, _, err := _PostMessage.Call(uintptr(hwnd), uintptr(msg), wParam, lParam) + if r == 0 { + return fmt.Errorf("PostMessage failed: %v", err) + } + return nil +} + +func ReleaseCapture() bool { + r, _, _ := _ReleaseCapture.Call() + return r != 0 +} + +func RegisterClassEx(cls *WndClassEx) (uint16, error) { + a, _, err := _RegisterClassExW.Call(uintptr(unsafe.Pointer(cls))) + issue34474KeepAlive(cls) + if a == 0 { + return 0, fmt.Errorf("RegisterClassExW failed: %v", err) + } + return uint16(a), nil +} + +func ReleaseDC(hdc syscall.Handle) { + _ReleaseDC.Call(uintptr(hdc)) +} + +func SetForegroundWindow(hwnd syscall.Handle) { + _SetForegroundWindow.Call(uintptr(hwnd)) +} + +func SetFocus(hwnd syscall.Handle) { + _SetFocus.Call(uintptr(hwnd)) +} + +func SetProcessDPIAware() { + _SetProcessDPIAware.Call() +} + +func SetCapture(hwnd syscall.Handle) syscall.Handle { + r, _, _ := _SetCapture.Call(uintptr(hwnd)) + return syscall.Handle(r) +} + +func SetClipboardData(format uint32, mem syscall.Handle) error { + r, _, err := _SetClipboardData.Call(uintptr(format), uintptr(mem)) + if r == 0 { + return fmt.Errorf("SetClipboardData: %v", err) + } + return nil +} + +func SetCursor(h syscall.Handle) { + _SetCursor.Call(uintptr(h)) +} + +func SetTimer(hwnd syscall.Handle, nIDEvent uintptr, uElapse uint32, timerProc uintptr) error { + r, _, err := _SetTimer.Call(uintptr(hwnd), uintptr(nIDEvent), uintptr(uElapse), timerProc) + if r == 0 { + return fmt.Errorf("SetTimer failed: %v", err) + } + return nil +} + +func ScreenToClient(hwnd syscall.Handle, p *Point) { + _ScreenToClient.Call(uintptr(hwnd), uintptr(unsafe.Pointer(p))) + issue34474KeepAlive(p) +} + +func ShowWindow(hwnd syscall.Handle, nCmdShow int32) { + _ShowWindow.Call(uintptr(hwnd), uintptr(nCmdShow)) +} + +func TranslateMessage(m *Msg) { + _TranslateMessage.Call(uintptr(unsafe.Pointer(m))) + issue34474KeepAlive(m) +} + +func UnregisterClass(cls uint16, hInst syscall.Handle) { + _UnregisterClass.Call(uintptr(cls), uintptr(hInst)) +} + +func UpdateWindow(hwnd syscall.Handle) { + _UpdateWindow.Call(uintptr(hwnd)) +} + +// issue34474KeepAlive calls runtime.KeepAlive as a +// workaround for golang.org/issue/34474. +func issue34474KeepAlive(v interface{}) { + runtime.KeepAlive(v) +} diff --git a/gio/app/internal/wm/Gio.java b/gio/app/internal/wm/Gio.java new file mode 100644 index 0000000..33e1a68 --- /dev/null +++ b/gio/app/internal/wm/Gio.java @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package org.gioui; + +import android.content.ClipboardManager; +import android.content.ClipData; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import java.io.UnsupportedEncodingException; + +public final class Gio { + private static final Object initLock = new Object(); + private static boolean jniLoaded; + private static final Handler handler = new Handler(Looper.getMainLooper()); + + /** + * init loads and initializes the Go native library and runs + * the Go main function. + * + * It is exported for use by Android apps that need to run Go code + * outside the lifecycle of the Gio activity. + */ + public static synchronized void init(Context appCtx) { + synchronized (initLock) { + if (jniLoaded) { + return; + } + String dataDir = appCtx.getFilesDir().getAbsolutePath(); + byte[] dataDirUTF8; + try { + dataDirUTF8 = dataDir.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + System.loadLibrary("gio"); + runGoMain(dataDirUTF8, appCtx); + jniLoaded = true; + } + } + + static private native void runGoMain(byte[] dataDir, Context context); + + static void writeClipboard(Context ctx, String s) { + ClipboardManager m = (ClipboardManager)ctx.getSystemService(Context.CLIPBOARD_SERVICE); + m.setPrimaryClip(ClipData.newPlainText(null, s)); + } + + static String readClipboard(Context ctx) { + ClipboardManager m = (ClipboardManager)ctx.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData c = m.getPrimaryClip(); + if (c == null || c.getItemCount() < 1) { + return null; + } + return c.getItemAt(0).coerceToText(ctx).toString(); + } + + static void wakeupMainThread() { + handler.post(new Runnable() { + @Override public void run() { + scheduleMainFuncs(); + } + }); + } + + static private native void scheduleMainFuncs(); +} diff --git a/gio/app/internal/wm/GioActivity.java b/gio/app/internal/wm/GioActivity.java new file mode 100644 index 0000000..260d4b6 --- /dev/null +++ b/gio/app/internal/wm/GioActivity.java @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package org.gioui; + +import android.app.Activity; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +public final class GioActivity extends Activity { + private GioView view; + + @Override public void onCreate(Bundle state) { + super.onCreate(state); + + Window w = getWindow(); + + this.view = new GioView(this); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + this.view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + this.view.setLayoutParams(new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT)); + setContentView(view); + } + + @Override public void onDestroy() { + view.destroy(); + super.onDestroy(); + } + + @Override public void onStart() { + super.onStart(); + view.start(); + } + + @Override public void onStop() { + view.stop(); + super.onStop(); + } + + @Override public void onConfigurationChanged(Configuration c) { + super.onConfigurationChanged(c); + view.configurationChanged(); + } + + @Override public void onLowMemory() { + super.onLowMemory(); + view.lowMemory(); + } + + @Override public void onBackPressed() { + if (!view.backPressed()) + super.onBackPressed(); + } +} diff --git a/gio/app/internal/wm/GioView.java b/gio/app/internal/wm/GioView.java new file mode 100644 index 0000000..7ed9f05 --- /dev/null +++ b/gio/app/internal/wm/GioView.java @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package org.gioui; + +import java.lang.Class; +import java.lang.IllegalAccessException; +import java.lang.InstantiationException; +import java.lang.ExceptionInInitializerError; +import java.lang.SecurityException; +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; +import android.os.Build; +import android.text.Editable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Choreographer; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.PointerIcon; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.WindowInsets; +import android.view.Surface; +import android.view.SurfaceView; +import android.view.SurfaceHolder; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.EditorInfo; + +import java.io.UnsupportedEncodingException; + +public final class GioView extends SurfaceView implements Choreographer.FrameCallback { + private static boolean jniLoaded; + + private final SurfaceHolder.Callback surfCallbacks; + private final View.OnFocusChangeListener focusCallback; + private final InputMethodManager imm; + private final float scrollXScale; + private final float scrollYScale; + + private long nhandle; + + public GioView(Context context) { + this(context, null); + } + + public GioView(Context context, AttributeSet attrs) { + super(context, attrs); + + // Late initialization of the Go runtime to wait for a valid context. + Gio.init(context.getApplicationContext()); + + // Set background color to transparent to avoid a flickering + // issue on ChromeOS. + setBackgroundColor(Color.argb(0, 0, 0, 0)); + + ViewConfiguration conf = ViewConfiguration.get(context); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + scrollXScale = conf.getScaledHorizontalScrollFactor(); + scrollYScale = conf.getScaledVerticalScrollFactor(); + + // The platform focus highlight is not aware of Gio's widgets. + setDefaultFocusHighlightEnabled(false); + } else { + float listItemHeight = 48; // dp + float px = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + listItemHeight, + getResources().getDisplayMetrics() + ); + scrollXScale = px; + scrollYScale = px; + } + + nhandle = onCreateView(this); + imm = (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); + setFocusable(true); + setFocusableInTouchMode(true); + focusCallback = new View.OnFocusChangeListener() { + @Override public void onFocusChange(View v, boolean focus) { + GioView.this.onFocusChange(nhandle, focus); + } + }; + setOnFocusChangeListener(focusCallback); + surfCallbacks = new SurfaceHolder.Callback() { + @Override public void surfaceCreated(SurfaceHolder holder) { + // Ignore; surfaceChanged is guaranteed to be called immediately after this. + } + @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + onSurfaceChanged(nhandle, getHolder().getSurface()); + } + @Override public void surfaceDestroyed(SurfaceHolder holder) { + onSurfaceDestroyed(nhandle); + } + }; + getHolder().addCallback(surfCallbacks); + } + + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { + onKeyEvent(nhandle, keyCode, event.getUnicodeChar(), event.getEventTime()); + return false; + } + + @Override public boolean onGenericMotionEvent(MotionEvent event) { + dispatchMotionEvent(event); + return true; + } + + @Override public boolean onTouchEvent(MotionEvent event) { + // Ask for unbuffered events. Flutter and Chrome do it + // so assume it's good for us as well. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + requestUnbufferedDispatch(event); + } + + dispatchMotionEvent(event); + return true; + } + + private void setCursor(int id) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return; + } + PointerIcon pointerIcon = PointerIcon.getSystemIcon(getContext(), id); + setPointerIcon(pointerIcon); + } + + private void dispatchMotionEvent(MotionEvent event) { + for (int j = 0; j < event.getHistorySize(); j++) { + long time = event.getHistoricalEventTime(j); + for (int i = 0; i < event.getPointerCount(); i++) { + onTouchEvent( + nhandle, + event.ACTION_MOVE, + event.getPointerId(i), + event.getToolType(i), + event.getHistoricalX(i, j), + event.getHistoricalY(i, j), + scrollXScale*event.getHistoricalAxisValue(MotionEvent.AXIS_HSCROLL, i, j), + scrollYScale*event.getHistoricalAxisValue(MotionEvent.AXIS_VSCROLL, i, j), + event.getButtonState(), + time); + } + } + int act = event.getActionMasked(); + int idx = event.getActionIndex(); + for (int i = 0; i < event.getPointerCount(); i++) { + int pact = event.ACTION_MOVE; + if (i == idx) { + pact = act; + } + onTouchEvent( + nhandle, + pact, + event.getPointerId(i), + event.getToolType(i), + event.getX(i), event.getY(i), + scrollXScale*event.getAxisValue(MotionEvent.AXIS_HSCROLL, i), + scrollYScale*event.getAxisValue(MotionEvent.AXIS_VSCROLL, i), + event.getButtonState(), + event.getEventTime()); + } + } + + @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return new InputConnection(this); + } + + void showTextInput() { + GioView.this.requestFocus(); + imm.showSoftInput(GioView.this, 0); + } + + void hideTextInput() { + imm.hideSoftInputFromWindow(getWindowToken(), 0); + } + + @Override protected boolean fitSystemWindows(Rect insets) { + onWindowInsets(nhandle, insets.top, insets.right, insets.bottom, insets.left); + return true; + } + + void postFrameCallback() { + Choreographer.getInstance().removeFrameCallback(this); + Choreographer.getInstance().postFrameCallback(this); + } + + @Override public void doFrame(long nanos) { + onFrameCallback(nhandle, nanos); + } + + int getDensity() { + return getResources().getDisplayMetrics().densityDpi; + } + + float getFontScale() { + return getResources().getConfiguration().fontScale; + } + + void start() { + onStartView(nhandle); + } + + void stop() { + onStopView(nhandle); + } + + void destroy() { + setOnFocusChangeListener(null); + getHolder().removeCallback(surfCallbacks); + onDestroyView(nhandle); + nhandle = 0; + } + + void configurationChanged() { + onConfigurationChanged(nhandle); + } + + void lowMemory() { + onLowMemory(); + } + + boolean backPressed() { + return onBack(nhandle); + } + + static private native long onCreateView(GioView view); + static private native void onDestroyView(long handle); + static private native void onStartView(long handle); + static private native void onStopView(long handle); + static private native void onSurfaceDestroyed(long handle); + static private native void onSurfaceChanged(long handle, Surface surface); + static private native void onConfigurationChanged(long handle); + static private native void onWindowInsets(long handle, int top, int right, int bottom, int left); + static private native void onLowMemory(); + static private native void onTouchEvent(long handle, int action, int pointerID, int tool, float x, float y, float scrollX, float scrollY, int buttons, long time); + static private native void onKeyEvent(long handle, int code, int character, long time); + static private native void onFrameCallback(long handle, long nanos); + static private native boolean onBack(long handle); + static private native void onFocusChange(long handle, boolean focus); + + private static class InputConnection extends BaseInputConnection { + private final Editable editable; + + InputConnection(View view) { + // Passing false enables "dummy mode", where the BaseInputConnection + // attempts to convert IME operations to key events. + super(view, false); + editable = Editable.Factory.getInstance().newEditable(""); + } + + @Override public Editable getEditable() { + return editable; + } + } +} diff --git a/gio/app/internal/wm/d3d11_windows.go b/gio/app/internal/wm/d3d11_windows.go new file mode 100644 index 0000000..8368933 --- /dev/null +++ b/gio/app/internal/wm/d3d11_windows.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +import ( + "fmt" + "unsafe" + + "realy.lol/gio/gpu" + "realy.lol/gio/internal/d3d11" +) + +type d3d11Context struct { + win *window + dev *d3d11.Device + ctx *d3d11.DeviceContext + + swchain *d3d11.IDXGISwapChain + renderTarget *d3d11.RenderTargetView + depthView *d3d11.DepthStencilView + width, height int +} + +const debug = false + +func init() { + drivers = append(drivers, gpuAPI{ + priority: 1, + initializer: func(w *window) (Context, error) { + hwnd, _, _ := w.HWND() + var flags uint32 + if debug { + flags |= d3d11.CREATE_DEVICE_DEBUG + } + dev, ctx, _, err := d3d11.CreateDevice( + d3d11.DRIVER_TYPE_HARDWARE, + flags, + ) + if err != nil { + return nil, fmt.Errorf("NewContext: %v", err) + } + swchain, err := d3d11.CreateSwapChain(dev, hwnd) + if err != nil { + d3d11.IUnknownRelease(unsafe.Pointer(ctx), ctx.Vtbl.Release) + d3d11.IUnknownRelease(unsafe.Pointer(dev), dev.Vtbl.Release) + return nil, err + } + return &d3d11Context{win: w, dev: dev, ctx: ctx, + swchain: swchain}, nil + }, + }) +} + +func (c *d3d11Context) API() gpu.API { + return gpu.Direct3D11{Device: unsafe.Pointer(c.dev)} +} + +func (c *d3d11Context) Present() error { + err := c.swchain.Present(1, 0) + if err == nil { + return nil + } + if err, ok := err.(d3d11.ErrorCode); ok { + switch err.Code { + case d3d11.DXGI_STATUS_OCCLUDED: + // Ignore + return nil + case d3d11.DXGI_ERROR_DEVICE_RESET, d3d11.DXGI_ERROR_DEVICE_REMOVED, d3d11.D3DDDIERR_DEVICEREMOVED: + return ErrDeviceLost + } + } + return err +} + +func (c *d3d11Context) MakeCurrent() error { + _, width, height := c.win.HWND() + if c.renderTarget != nil && width == c.width && height == c.height { + c.ctx.OMSetRenderTargets(c.renderTarget, c.depthView) + return nil + } + c.releaseFBO() + if err := c.swchain.ResizeBuffers(0, 0, 0, d3d11.DXGI_FORMAT_UNKNOWN, + 0); err != nil { + return err + } + c.width = width + c.height = height + + desc, err := c.swchain.GetDesc() + if err != nil { + return err + } + backBuffer, err := c.swchain.GetBuffer(0, &d3d11.IID_Texture2D) + if err != nil { + return err + } + texture := (*d3d11.Resource)(unsafe.Pointer(backBuffer)) + renderTarget, err := c.dev.CreateRenderTargetView(texture) + d3d11.IUnknownRelease(unsafe.Pointer(backBuffer), backBuffer.Vtbl.Release) + if err != nil { + return err + } + depthView, err := d3d11.CreateDepthView(c.dev, int(desc.BufferDesc.Width), + int(desc.BufferDesc.Height), 24) + if err != nil { + d3d11.IUnknownRelease(unsafe.Pointer(renderTarget), + renderTarget.Vtbl.Release) + return err + } + c.renderTarget = renderTarget + c.depthView = depthView + + c.ctx.OMSetRenderTargets(c.renderTarget, c.depthView) + return nil +} + +func (c *d3d11Context) Lock() {} + +func (c *d3d11Context) Unlock() {} + +func (c *d3d11Context) Release() { + c.releaseFBO() + if c.swchain != nil { + d3d11.IUnknownRelease(unsafe.Pointer(c.swchain), c.swchain.Vtbl.Release) + } + if c.ctx != nil { + d3d11.IUnknownRelease(unsafe.Pointer(c.ctx), c.ctx.Vtbl.Release) + } + if c.dev != nil { + d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release) + } + *c = d3d11Context{} +} + +func (c *d3d11Context) releaseFBO() { + if c.depthView != nil { + d3d11.IUnknownRelease(unsafe.Pointer(c.depthView), + c.depthView.Vtbl.Release) + c.depthView = nil + } + if c.renderTarget != nil { + d3d11.IUnknownRelease(unsafe.Pointer(c.renderTarget), + c.renderTarget.Vtbl.Release) + c.renderTarget = nil + } +} diff --git a/gio/app/internal/wm/egl_android.go b/gio/app/internal/wm/egl_android.go new file mode 100644 index 0000000..50e38ad --- /dev/null +++ b/gio/app/internal/wm/egl_android.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +/* +#include +*/ +import "C" + +import ( + "unsafe" + + "realy.lol/gio/internal/egl" +) + +type context struct { + win *window + *egl.Context +} + +func (w *window) NewContext() (Context, error) { + ctx, err := egl.NewContext(nil) + if err != nil { + return nil, err + } + return &context{win: w, Context: ctx}, nil +} + +func (c *context) Release() { + if c.Context != nil { + c.Context.Release() + c.Context = nil + } +} + +func (c *context) MakeCurrent() error { + c.Context.ReleaseSurface() + win, width, height := c.win.nativeWindow(c.Context.VisualID()) + if win == nil { + return nil + } + eglSurf := egl.NativeWindowType(unsafe.Pointer(win)) + if err := c.Context.CreateSurface(eglSurf, width, height); err != nil { + return err + } + if err := c.Context.MakeCurrent(); err != nil { + return err + } + return nil +} + +func (c *context) Lock() {} + +func (c *context) Unlock() {} diff --git a/gio/app/internal/wm/egl_wayland.go b/gio/app/internal/wm/egl_wayland.go new file mode 100644 index 0000000..0cd5b6c --- /dev/null +++ b/gio/app/internal/wm/egl_wayland.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build (linux && !android && !nowayland) || freebsd +// +build linux,!android,!nowayland freebsd + +package wm + +import ( + "errors" + "unsafe" + + "realy.lol/gio/internal/egl" +) + +/* +#cgo linux pkg-config: egl wayland-egl +#cgo freebsd openbsd LDFLAGS: -lwayland-egl +#cgo CFLAGS: -DEGL_NO_X11 + +#include +#include +#include +*/ +import "C" + +type context struct { + win *window + *egl.Context + eglWin *C.struct_wl_egl_window +} + +func (w *window) NewContext() (Context, error) { + disp := egl.NativeDisplayType(unsafe.Pointer(w.display())) + ctx, err := egl.NewContext(disp) + if err != nil { + return nil, err + } + return &context{Context: ctx, win: w}, nil +} + +func (c *context) Release() { + if c.Context != nil { + c.Context.Release() + c.Context = nil + } + if c.eglWin != nil { + C.wl_egl_window_destroy(c.eglWin) + c.eglWin = nil + } +} + +func (c *context) MakeCurrent() error { + c.Context.ReleaseSurface() + if c.eglWin != nil { + C.wl_egl_window_destroy(c.eglWin) + c.eglWin = nil + } + surf, width, height := c.win.surface() + if surf == nil { + return errors.New("wayland: no surface") + } + eglWin := C.wl_egl_window_create(surf, C.int(width), C.int(height)) + if eglWin == nil { + return errors.New("wayland: wl_egl_window_create failed") + } + c.eglWin = eglWin + eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin))) + if err := c.Context.CreateSurface(eglSurf, width, height); err != nil { + return err + } + return c.Context.MakeCurrent() +} + +func (c *context) Lock() {} + +func (c *context) Unlock() {} diff --git a/gio/app/internal/wm/egl_windows.go b/gio/app/internal/wm/egl_windows.go new file mode 100644 index 0000000..ce7645c --- /dev/null +++ b/gio/app/internal/wm/egl_windows.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +import ( + "realy.lol/gio/internal/egl" +) + +type glContext struct { + win *window + *egl.Context +} + +func init() { + drivers = append(drivers, gpuAPI{ + priority: 2, + initializer: func(w *window) (Context, error) { + disp := egl.NativeDisplayType(w.HDC()) + ctx, err := egl.NewContext(disp) + if err != nil { + return nil, err + } + return &glContext{win: w, Context: ctx}, nil + }, + }) +} + +func (c *glContext) Release() { + if c.Context != nil { + c.Context.Release() + c.Context = nil + } +} + +func (c *glContext) MakeCurrent() error { + c.Context.ReleaseSurface() + win, width, height := c.win.HWND() + eglSurf := egl.NativeWindowType(win) + if err := c.Context.CreateSurface(eglSurf, width, height); err != nil { + return err + } + if err := c.Context.MakeCurrent(); err != nil { + return err + } + c.Context.EnableVSync(true) + return nil +} + +func (c *glContext) Lock() {} + +func (c *glContext) Unlock() {} diff --git a/gio/app/internal/wm/egl_x11.go b/gio/app/internal/wm/egl_x11.go new file mode 100644 index 0000000..556cd78 --- /dev/null +++ b/gio/app/internal/wm/egl_x11.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build (linux && !android && !nox11) || freebsd || openbsd +// +build linux,!android,!nox11 freebsd openbsd + +package wm + +import ( + "unsafe" + + "realy.lol/gio/internal/egl" +) + +type x11Context struct { + win *x11Window + *egl.Context +} + +func (w *x11Window) NewContext() (Context, error) { + disp := egl.NativeDisplayType(unsafe.Pointer(w.display())) + ctx, err := egl.NewContext(disp) + if err != nil { + return nil, err + } + return &x11Context{win: w, Context: ctx}, nil +} + +func (c *x11Context) Release() { + if c.Context != nil { + c.Context.Release() + c.Context = nil + } +} + +func (c *x11Context) MakeCurrent() error { + c.Context.ReleaseSurface() + win, width, height := c.win.window() + eglSurf := egl.NativeWindowType(uintptr(win)) + if err := c.Context.CreateSurface(eglSurf, width, height); err != nil { + return err + } + if err := c.Context.MakeCurrent(); err != nil { + return err + } + c.Context.EnableVSync(true) + return nil +} + +func (c *x11Context) Lock() {} + +func (c *x11Context) Unlock() {} diff --git a/gio/app/internal/wm/framework_ios.h b/gio/app/internal/wm/framework_ios.h new file mode 100644 index 0000000..18e5a02 --- /dev/null +++ b/gio/app/internal/wm/framework_ios.h @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +#include + +@interface GioViewController : UIViewController +@end diff --git a/gio/app/internal/wm/gl_ios.go b/gio/app/internal/wm/gl_ios.go new file mode 100644 index 0000000..b3e7a47 --- /dev/null +++ b/gio/app/internal/wm/gl_ios.go @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build darwin && ios +// +build darwin,ios + +package wm + +/* +#include +#include +#include + +__attribute__ ((visibility ("hidden"))) int gio_renderbufferStorage(CFTypeRef ctx, CFTypeRef layer, GLenum buffer); +__attribute__ ((visibility ("hidden"))) int gio_presentRenderbuffer(CFTypeRef ctx, GLenum buffer); +__attribute__ ((visibility ("hidden"))) int gio_makeCurrent(CFTypeRef ctx); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createContext(void); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createGLLayer(void); +*/ +import "C" + +import ( + "errors" + "fmt" + + "realy.lol/gio/gpu" + "realy.lol/gio/internal/gl" +) + +type context struct { + owner *window + c *gl.Functions + ctx C.CFTypeRef + layer C.CFTypeRef + init bool + frameBuffer gl.Framebuffer + colorBuffer, depthBuffer gl.Renderbuffer +} + +func init() { + layerFactory = func() uintptr { + return uintptr(C.gio_createGLLayer()) + } +} + +func newContext(w *window) (*context, error) { + ctx := C.gio_createContext() + if ctx == 0 { + return nil, fmt.Errorf("failed to create EAGLContext") + } + c := &context{ + ctx: ctx, + owner: w, + layer: C.CFTypeRef(w.contextLayer()), + c: new(gl.Functions), + } + return c, nil +} + +func (c *context) API() gpu.API { + return gpu.OpenGL{} +} + +func (c *context) Release() { + if c.ctx == 0 { + return + } + C.gio_renderbufferStorage(c.ctx, 0, C.GLenum(gl.RENDERBUFFER)) + c.c.DeleteFramebuffer(c.frameBuffer) + c.c.DeleteRenderbuffer(c.colorBuffer) + c.c.DeleteRenderbuffer(c.depthBuffer) + C.gio_makeCurrent(0) + C.CFRelease(c.ctx) + c.ctx = 0 +} + +func (c *context) Present() error { + if c.layer == 0 { + panic("context is not active") + } + // Discard depth buffer as recommended in + // https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/WorkingwithEAGLContexts/WorkingwithEAGLContexts.html + c.c.BindFramebuffer(gl.FRAMEBUFFER, c.frameBuffer) + c.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT) + c.c.BindRenderbuffer(gl.RENDERBUFFER, c.colorBuffer) + if C.gio_presentRenderbuffer(c.ctx, C.GLenum(gl.RENDERBUFFER)) == 0 { + return errors.New("presentRenderBuffer failed") + } + return nil +} + +func (c *context) Lock() {} + +func (c *context) Unlock() {} + +func (c *context) MakeCurrent() error { + if C.gio_makeCurrent(c.ctx) == 0 { + C.CFRelease(c.ctx) + c.ctx = 0 + return errors.New("[EAGLContext setCurrentContext] failed") + } + if !c.init { + c.init = true + c.frameBuffer = c.c.CreateFramebuffer() + c.colorBuffer = c.c.CreateRenderbuffer() + c.depthBuffer = c.c.CreateRenderbuffer() + } + if !c.owner.isVisible() { + // Make sure any in-flight GL commands are complete. + c.c.Finish() + return nil + } + currentRB := gl.Renderbuffer{uint(c.c.GetInteger(gl.RENDERBUFFER_BINDING))} + c.c.BindRenderbuffer(gl.RENDERBUFFER, c.colorBuffer) + if C.gio_renderbufferStorage(c.ctx, c.layer, + C.GLenum(gl.RENDERBUFFER)) == 0 { + return errors.New("renderbufferStorage failed") + } + w := c.c.GetRenderbufferParameteri(gl.RENDERBUFFER, gl.RENDERBUFFER_WIDTH) + h := c.c.GetRenderbufferParameteri(gl.RENDERBUFFER, gl.RENDERBUFFER_HEIGHT) + c.c.BindRenderbuffer(gl.RENDERBUFFER, c.depthBuffer) + c.c.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h) + c.c.BindRenderbuffer(gl.RENDERBUFFER, currentRB) + c.c.BindFramebuffer(gl.FRAMEBUFFER, c.frameBuffer) + c.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, + gl.RENDERBUFFER, c.colorBuffer) + c.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, + gl.RENDERBUFFER, c.depthBuffer) + if st := c.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { + return fmt.Errorf("framebuffer incomplete, status: %#x\n", st) + } + return nil +} + +func (w *window) NewContext() (Context, error) { + return newContext(w) +} diff --git a/gio/app/internal/wm/gl_ios.m b/gio/app/internal/wm/gl_ios.m new file mode 100644 index 0000000..065ea97 --- /dev/null +++ b/gio/app/internal/wm/gl_ios.m @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,ios + +@import UIKit; +@import OpenGLES; + +#include "_cgo_export.h" + +int gio_renderbufferStorage(CFTypeRef ctxRef, CFTypeRef layerRef, GLenum buffer) { + EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; + CAEAGLLayer *layer = (__bridge CAEAGLLayer *)layerRef; + return (int)[ctx renderbufferStorage:buffer fromDrawable:layer]; +} + +int gio_presentRenderbuffer(CFTypeRef ctxRef, GLenum buffer) { + EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; + return (int)[ctx presentRenderbuffer:buffer]; +} + +int gio_makeCurrent(CFTypeRef ctxRef) { + EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; + return (int)[EAGLContext setCurrentContext:ctx]; +} + +CFTypeRef gio_createContext(void) { + EAGLContext *ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; + if (ctx == nil) { + return nil; + } + return CFBridgingRetain(ctx); +} + +CFTypeRef gio_createGLLayer(void) { + CAEAGLLayer *layer = [[CAEAGLLayer layer] init]; + if (layer == nil) { + return nil; + } + layer.drawableProperties = @{kEAGLDrawablePropertyColorFormat: kEAGLColorFormatSRGBA8}; + layer.opaque = YES; + layer.anchorPoint = CGPointMake(0, 0); + return CFBridgingRetain(layer); +} diff --git a/gio/app/internal/wm/gl_js.go b/gio/app/internal/wm/gl_js.go new file mode 100644 index 0000000..0a931b6 --- /dev/null +++ b/gio/app/internal/wm/gl_js.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +import ( + "errors" + "syscall/js" + + "realy.lol/gio/gpu" + "realy.lol/gio/internal/gl" + "realy.lol/gio/internal/srgb" +) + +type context struct { + ctx js.Value + cnv js.Value + srgbFBO *srgb.FBO +} + +func newContext(w *window) (*context, error) { + args := map[string]interface{}{ + // Enable low latency rendering. + // See https://developers.google.com/web/updates/2019/05/desynchronized. + "desynchronized": true, + "preserveDrawingBuffer": true, + } + ctx := w.cnv.Call("getContext", "webgl2", args) + if ctx.IsNull() { + ctx = w.cnv.Call("getContext", "webgl", args) + } + if ctx.IsNull() { + return nil, errors.New("app: webgl is not supported") + } + c := &context{ + ctx: ctx, + cnv: w.cnv, + } + return c, nil +} + +func (c *context) API() gpu.API { + return gpu.OpenGL{Context: gl.Context(c.ctx)} +} + +func (c *context) Release() { + if c.srgbFBO != nil { + c.srgbFBO.Release() + c.srgbFBO = nil + } +} + +func (c *context) Present() error { + if c.srgbFBO != nil { + c.srgbFBO.Blit() + } + if c.srgbFBO != nil { + c.srgbFBO.AfterPresent() + } + if c.ctx.Call("isContextLost").Bool() { + return errors.New("context lost") + } + return nil +} + +func (c *context) Lock() {} + +func (c *context) Unlock() {} + +func (c *context) MakeCurrent() error { + if c.srgbFBO == nil { + var err error + c.srgbFBO, err = srgb.New(gl.Context(c.ctx)) + if err != nil { + c.Release() + c.srgbFBO = nil + return err + } + } + w, h := c.cnv.Get("width").Int(), c.cnv.Get("height").Int() + if err := c.srgbFBO.Refresh(w, h); err != nil { + c.Release() + return err + } + return nil +} + +func (w *window) NewContext() (Context, error) { + return newContext(w) +} diff --git a/gio/app/internal/wm/gl_macos.go b/gio/app/internal/wm/gl_macos.go new file mode 100644 index 0000000..a95557b --- /dev/null +++ b/gio/app/internal/wm/gl_macos.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build darwin && !ios +// +build darwin,!ios + +package wm + +import ( + "realy.lol/gio/gpu" + "realy.lol/gio/internal/gl" +) + +/* +#include +#include +#include +#include + +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createGLView(void); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_contextForView(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) void gio_makeCurrentContext(CFTypeRef ctx); +__attribute__ ((visibility ("hidden"))) void gio_flushContextBuffer(CFTypeRef ctx); +__attribute__ ((visibility ("hidden"))) void gio_clearCurrentContext(void); +__attribute__ ((visibility ("hidden"))) void gio_lockContext(CFTypeRef ctxRef); +__attribute__ ((visibility ("hidden"))) void gio_unlockContext(CFTypeRef ctxRef); +*/ +import "C" + +type context struct { + c *gl.Functions + ctx C.CFTypeRef + view C.CFTypeRef +} + +func init() { + viewFactory = func() C.CFTypeRef { + return C.gio_createGLView() + } +} + +func newContext(w *window) (*context, error) { + view := w.contextView() + ctx := C.gio_contextForView(view) + c := &context{ + ctx: ctx, + view: view, + } + return c, nil +} + +func (c *context) API() gpu.API { + return gpu.OpenGL{} +} + +func (c *context) Release() { + c.Lock() + defer c.Unlock() + C.gio_clearCurrentContext() + // We could release the context with [view clearGLContext] + // and rely on [view openGLContext] auto-creating a new context. + // However that second context is not properly set up by + // OpenGLContextView, so we'll stay on the safe side and keep + // the first context around. +} + +func (c *context) Present() error { + // Assume the caller already locked the context. + C.glFlush() + return nil +} + +func (c *context) Lock() { + C.gio_lockContext(c.ctx) +} + +func (c *context) Unlock() { + C.gio_unlockContext(c.ctx) +} + +func (c *context) MakeCurrent() error { + c.Lock() + defer c.Unlock() + C.gio_makeCurrentContext(c.ctx) + return nil +} + +func (w *window) NewContext() (Context, error) { + return newContext(w) +} diff --git a/gio/app/internal/wm/gl_macos.m b/gio/app/internal/wm/gl_macos.m new file mode 100644 index 0000000..576aa40 --- /dev/null +++ b/gio/app/internal/wm/gl_macos.m @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,!ios + +@import AppKit; + +#include +#include +#include +#include "_cgo_export.h" + +static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) { + NSPoint p = [view convertPoint:[event locationInWindow] fromView:nil]; + if (!event.hasPreciseScrollingDeltas) { + // dx and dy are in rows and columns. + dx *= 10; + dy *= 10; + } + gio_onMouse((__bridge CFTypeRef)view, typ, [NSEvent pressedMouseButtons], p.x, p.y, dx, dy, [event timestamp], [event modifierFlags]); +} + +@interface GioView : NSOpenGLView +@end + +@implementation GioView +- (instancetype)initWithFrame:(NSRect)frameRect + pixelFormat:(NSOpenGLPixelFormat *)format { + return [super initWithFrame:frameRect pixelFormat:format]; +} +- (void)prepareOpenGL { + [super prepareOpenGL]; + // Bind a default VBA to emulate OpenGL ES 2. + GLuint defVBA; + glGenVertexArrays(1, &defVBA); + glBindVertexArray(defVBA); + glEnable(GL_FRAMEBUFFER_SRGB); +} +- (BOOL)isFlipped { + return YES; +} +- (void)update { + [super update]; + [self setNeedsDisplay:YES]; +} +- (void)drawRect:(NSRect)r { + gio_onDraw((__bridge CFTypeRef)self); +} +- (void)mouseDown:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_DOWN, 0, 0); +} +- (void)mouseUp:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_UP, 0, 0); +} +- (void)middleMouseDown:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_DOWN, 0, 0); +} +- (void)middletMouseUp:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_UP, 0, 0); +} +- (void)rightMouseDown:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_DOWN, 0, 0); +} +- (void)rightMouseUp:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_UP, 0, 0); +} +- (void)mouseMoved:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_MOVE, 0, 0); +} +- (void)mouseDragged:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_MOVE, 0, 0); +} +- (void)scrollWheel:(NSEvent *)event { + CGFloat dx = -event.scrollingDeltaX; + CGFloat dy = -event.scrollingDeltaY; + handleMouse(self, event, GIO_MOUSE_SCROLL, dx, dy); +} +- (void)keyDown:(NSEvent *)event { + NSString *keys = [event charactersIgnoringModifiers]; + gio_onKeys((__bridge CFTypeRef)self, (char *)[keys UTF8String], [event timestamp], [event modifierFlags], true); + [self interpretKeyEvents:[NSArray arrayWithObject:event]]; +} +- (void)keyUp:(NSEvent *)event { + NSString *keys = [event charactersIgnoringModifiers]; + gio_onKeys((__bridge CFTypeRef)self, (char *)[keys UTF8String], [event timestamp], [event modifierFlags], false); +} +- (void)insertText:(id)string { + const char *utf8 = [string UTF8String]; + gio_onText((__bridge CFTypeRef)self, (char *)utf8); +} +- (void)doCommandBySelector:(SEL)sel { + // Don't pass commands up the responder chain. + // They will end up in a beep. +} +@end + +CFTypeRef gio_createGLView(void) { + @autoreleasepool { + NSOpenGLPixelFormatAttribute attr[] = { + NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, + NSOpenGLPFAColorSize, 24, + NSOpenGLPFADepthSize, 16, + NSOpenGLPFAAccelerated, + // Opt-in to automatic GPU switching. CGL-only property. + kCGLPFASupportsAutomaticGraphicsSwitching, + NSOpenGLPFAAllowOfflineRenderers, + 0 + }; + id pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr]; + + NSRect frame = NSMakeRect(0, 0, 0, 0); + GioView* view = [[GioView alloc] initWithFrame:frame pixelFormat:pixFormat]; + + [view setWantsBestResolutionOpenGLSurface:YES]; + [view setWantsLayer:YES]; // The default in Mojave. + + return CFBridgingRetain(view); + } +} + +void gio_setNeedsDisplay(CFTypeRef viewRef) { + NSOpenGLView *view = (__bridge NSOpenGLView *)viewRef; + [view setNeedsDisplay:YES]; +} + +CFTypeRef gio_contextForView(CFTypeRef viewRef) { + NSOpenGLView *view = (__bridge NSOpenGLView *)viewRef; + return (__bridge CFTypeRef)view.openGLContext; +} + +void gio_clearCurrentContext(void) { + [NSOpenGLContext clearCurrentContext]; +} + +void gio_makeCurrentContext(CFTypeRef ctxRef) { + NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; + [ctx makeCurrentContext]; +} + +void gio_lockContext(CFTypeRef ctxRef) { + NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; + CGLLockContext([ctx CGLContextObj]); +} + +void gio_unlockContext(CFTypeRef ctxRef) { + NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; + CGLUnlockContext([ctx CGLContextObj]); +} diff --git a/gio/app/internal/wm/os_android.c b/gio/app/internal/wm/os_android.c new file mode 100644 index 0000000..8a2c62d --- /dev/null +++ b/gio/app/internal/wm/os_android.c @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +#include +#include "_cgo_export.h" + +jint gio_jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version) { + return (*vm)->GetEnv(vm, (void **)env, version); +} + +jint gio_jni_GetJavaVM(JNIEnv *env, JavaVM **jvm) { + return (*env)->GetJavaVM(env, jvm); +} + +jint gio_jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args) { + return (*vm)->AttachCurrentThread(vm, p_env, thr_args); +} + +jint gio_jni_DetachCurrentThread(JavaVM *vm) { + return (*vm)->DetachCurrentThread(vm); +} + +jobject gio_jni_NewGlobalRef(JNIEnv *env, jobject obj) { + return (*env)->NewGlobalRef(env, obj); +} + +void gio_jni_DeleteGlobalRef(JNIEnv *env, jobject obj) { + (*env)->DeleteGlobalRef(env, obj); +} + +jclass gio_jni_GetObjectClass(JNIEnv *env, jobject obj) { + return (*env)->GetObjectClass(env, obj); +} + +jmethodID gio_jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { + return (*env)->GetMethodID(env, clazz, name, sig); +} + +jmethodID gio_jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { + return (*env)->GetStaticMethodID(env, clazz, name, sig); +} + +jfloat gio_jni_CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID) { + return (*env)->CallFloatMethod(env, obj, methodID); +} + +jint gio_jni_CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID) { + return (*env)->CallIntMethod(env, obj, methodID); +} + +void gio_jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args) { + (*env)->CallStaticVoidMethodA(env, cls, methodID, args); +} + +void gio_jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args) { + (*env)->CallVoidMethodA(env, obj, methodID, args); +} + +jbyte *gio_jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr) { + return (*env)->GetByteArrayElements(env, arr, NULL); +} + +void gio_jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *bytes) { + (*env)->ReleaseByteArrayElements(env, arr, bytes, JNI_ABORT); +} + +jsize gio_jni_GetArrayLength(JNIEnv *env, jbyteArray arr) { + return (*env)->GetArrayLength(env, arr); +} + +jstring gio_jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len) { + return (*env)->NewString(env, unicodeChars, len); +} + +jsize gio_jni_GetStringLength(JNIEnv *env, jstring str) { + return (*env)->GetStringLength(env, str); +} + +const jchar *gio_jni_GetStringChars(JNIEnv *env, jstring str) { + return (*env)->GetStringChars(env, str, NULL); +} + +jthrowable gio_jni_ExceptionOccurred(JNIEnv *env) { + return (*env)->ExceptionOccurred(env); +} + +void gio_jni_ExceptionClear(JNIEnv *env) { + (*env)->ExceptionClear(env); +} + +jobject gio_jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) { + return (*env)->CallObjectMethodA(env, obj, method, args); +} + +jobject gio_jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) { + return (*env)->CallStaticObjectMethodA(env, cls, method, args); +} diff --git a/gio/app/internal/wm/os_android.go b/gio/app/internal/wm/os_android.go new file mode 100644 index 0000000..a690c42 --- /dev/null +++ b/gio/app/internal/wm/os_android.go @@ -0,0 +1,785 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +/* +#cgo CFLAGS: -Werror +#cgo LDFLAGS: -landroid + +#include +#include +#include +#include +#include + +__attribute__ ((visibility ("hidden"))) jint gio_jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version); +__attribute__ ((visibility ("hidden"))) jint gio_jni_GetJavaVM(JNIEnv *env, JavaVM **jvm); +__attribute__ ((visibility ("hidden"))) jint gio_jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args); +__attribute__ ((visibility ("hidden"))) jint gio_jni_DetachCurrentThread(JavaVM *vm); + +__attribute__ ((visibility ("hidden"))) jobject gio_jni_NewGlobalRef(JNIEnv *env, jobject obj); +__attribute__ ((visibility ("hidden"))) void gio_jni_DeleteGlobalRef(JNIEnv *env, jobject obj); +__attribute__ ((visibility ("hidden"))) jclass gio_jni_GetObjectClass(JNIEnv *env, jobject obj); +__attribute__ ((visibility ("hidden"))) jmethodID gio_jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); +__attribute__ ((visibility ("hidden"))) jmethodID gio_jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); +__attribute__ ((visibility ("hidden"))) jfloat gio_jni_CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID); +__attribute__ ((visibility ("hidden"))) jint gio_jni_CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID); +__attribute__ ((visibility ("hidden"))) void gio_jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args); +__attribute__ ((visibility ("hidden"))) void gio_jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args); +__attribute__ ((visibility ("hidden"))) jbyte *gio_jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr); +__attribute__ ((visibility ("hidden"))) void gio_jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *bytes); +__attribute__ ((visibility ("hidden"))) jsize gio_jni_GetArrayLength(JNIEnv *env, jbyteArray arr); +__attribute__ ((visibility ("hidden"))) jstring gio_jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len); +__attribute__ ((visibility ("hidden"))) jsize gio_jni_GetStringLength(JNIEnv *env, jstring str); +__attribute__ ((visibility ("hidden"))) const jchar *gio_jni_GetStringChars(JNIEnv *env, jstring str); +__attribute__ ((visibility ("hidden"))) jthrowable gio_jni_ExceptionOccurred(JNIEnv *env); +__attribute__ ((visibility ("hidden"))) void gio_jni_ExceptionClear(JNIEnv *env); +__attribute__ ((visibility ("hidden"))) jobject gio_jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args); +__attribute__ ((visibility ("hidden"))) jobject gio_jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args); +*/ +import "C" + +import ( + "errors" + "fmt" + "image" + "reflect" + "runtime" + "runtime/debug" + "sync" + "time" + "unicode/utf16" + "unsafe" + + "realy.lol/gio/f32" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/unit" +) + +type window struct { + callbacks Callbacks + + view C.jobject + + dpi int + fontScale float32 + insets system.Insets + + stage system.Stage + started bool + + state, newState windowState + + // mu protects the fields following it. + mu sync.Mutex + win *C.ANativeWindow + animating bool +} + +// windowState tracks the View or Activity specific state lost when Android +// re-creates our Activity. +type windowState struct { + cursor *pointer.CursorName +} + +// gioView hold cached JNI methods for GioView. +var gioView struct { + once sync.Once + getDensity C.jmethodID + getFontScale C.jmethodID + showTextInput C.jmethodID + hideTextInput C.jmethodID + postFrameCallback C.jmethodID + setCursor C.jmethodID +} + +// ViewEvent is sent whenever the Window's underlying Android view +// changes. +type ViewEvent struct { + // View is a JNI global reference to the android.view.View + // instance backing the Window. The reference is valid until + // the next ViewEvent is received. + // A zero View means that there is currently no view attached. + View uintptr +} + +type jvalue uint64 // The largest JNI type fits in 64 bits. + +var dataDirChan = make(chan string, 1) + +var android struct { + // mu protects all fields of this structure. However, once a + // non-nil jvm is returned from javaVM, all the other fields may + // be accessed unlocked. + mu sync.Mutex + jvm *C.JavaVM + + // appCtx is the global Android App context. + appCtx C.jobject + // gioCls is the class of the Gio class. + gioCls C.jclass + + mwriteClipboard C.jmethodID + mreadClipboard C.jmethodID + mwakeupMainThread C.jmethodID +} + +// view maps from GioView JNI refenreces to windows. +var views = make(map[C.jlong]*window) + +// windows maps from Callbacks to windows +var windows = make(map[Callbacks]*window) + +var mainWindow = newWindowRendezvous() + +var mainFuncs = make(chan func(env *C.JNIEnv), 1) + +func getMethodID(env *C.JNIEnv, class C.jclass, + method, sig string) C.jmethodID { + m := C.CString(method) + defer C.free(unsafe.Pointer(m)) + s := C.CString(sig) + defer C.free(unsafe.Pointer(s)) + jm := C.gio_jni_GetMethodID(env, class, m, s) + if err := exception(env); err != nil { + panic(err) + } + return jm +} + +func getStaticMethodID(env *C.JNIEnv, class C.jclass, + method, sig string) C.jmethodID { + m := C.CString(method) + defer C.free(unsafe.Pointer(m)) + s := C.CString(sig) + defer C.free(unsafe.Pointer(s)) + jm := C.gio_jni_GetStaticMethodID(env, class, m, s) + if err := exception(env); err != nil { + panic(err) + } + return jm +} + +//export Java_org_gioui_Gio_runGoMain +func Java_org_gioui_Gio_runGoMain(env *C.JNIEnv, class C.jclass, + jdataDir C.jbyteArray, context C.jobject) { + initJVM(env, class, context) + dirBytes := C.gio_jni_GetByteArrayElements(env, jdataDir) + if dirBytes == nil { + panic("runGoMain: GetByteArrayElements failed") + } + n := C.gio_jni_GetArrayLength(env, jdataDir) + dataDir := C.GoStringN((*C.char)(unsafe.Pointer(dirBytes)), n) + dataDirChan <- dataDir + C.gio_jni_ReleaseByteArrayElements(env, jdataDir, dirBytes) + + runMain() +} + +func initJVM(env *C.JNIEnv, gio C.jclass, ctx C.jobject) { + android.mu.Lock() + defer android.mu.Unlock() + if res := C.gio_jni_GetJavaVM(env, &android.jvm); res != 0 { + panic("gio: GetJavaVM failed") + } + android.appCtx = C.gio_jni_NewGlobalRef(env, ctx) + android.gioCls = C.jclass(C.gio_jni_NewGlobalRef(env, C.jobject(gio))) + android.mwriteClipboard = getStaticMethodID(env, gio, "writeClipboard", + "(Landroid/content/Context;Ljava/lang/String;)V") + android.mreadClipboard = getStaticMethodID(env, gio, "readClipboard", + "(Landroid/content/Context;)Ljava/lang/String;") + android.mwakeupMainThread = getStaticMethodID(env, gio, "wakeupMainThread", + "()V") +} + +func JavaVM() uintptr { + jvm := javaVM() + return uintptr(unsafe.Pointer(jvm)) +} + +func javaVM() *C.JavaVM { + android.mu.Lock() + defer android.mu.Unlock() + return android.jvm +} + +func AppContext() uintptr { + android.mu.Lock() + defer android.mu.Unlock() + return uintptr(android.appCtx) +} + +func GetDataDir() string { + return <-dataDirChan +} + +//export Java_org_gioui_GioView_onCreateView +func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, + view C.jobject) C.jlong { + gioView.once.Do(func() { + m := &gioView + m.getDensity = getMethodID(env, class, "getDensity", "()I") + m.getFontScale = getMethodID(env, class, "getFontScale", "()F") + m.showTextInput = getMethodID(env, class, "showTextInput", "()V") + m.hideTextInput = getMethodID(env, class, "hideTextInput", "()V") + m.postFrameCallback = getMethodID(env, class, "postFrameCallback", + "()V") + m.setCursor = getMethodID(env, class, "setCursor", "(I)V") + }) + view = C.gio_jni_NewGlobalRef(env, view) + wopts := <-mainWindow.out + w, ok := windows[wopts.window] + if !ok { + w = &window{ + callbacks: wopts.window, + } + windows[wopts.window] = w + } + w.callbacks.SetDriver(w) + w.view = view + handle := C.jlong(view) + views[handle] = w + w.loadConfig(env, class) + applyStateDiff(env, view, windowState{}, w.state) + w.setStage(system.StagePaused) + w.callbacks.Event(ViewEvent{View: uintptr(view)}) + return handle +} + +//export Java_org_gioui_GioView_onDestroyView +func Java_org_gioui_GioView_onDestroyView(env *C.JNIEnv, class C.jclass, + handle C.jlong) { + w := views[handle] + w.callbacks.Event(ViewEvent{View: 0}) + w.callbacks.SetDriver(nil) + delete(views, handle) + C.gio_jni_DeleteGlobalRef(env, w.view) + w.view = 0 +} + +//export Java_org_gioui_GioView_onStopView +func Java_org_gioui_GioView_onStopView(env *C.JNIEnv, class C.jclass, + handle C.jlong) { + w := views[handle] + w.started = false + w.setStage(system.StagePaused) +} + +//export Java_org_gioui_GioView_onStartView +func Java_org_gioui_GioView_onStartView(env *C.JNIEnv, class C.jclass, + handle C.jlong) { + w := views[handle] + w.started = true + if w.aNativeWindow() != nil { + w.setVisible() + } +} + +//export Java_org_gioui_GioView_onSurfaceDestroyed +func Java_org_gioui_GioView_onSurfaceDestroyed(env *C.JNIEnv, class C.jclass, + handle C.jlong) { + w := views[handle] + w.mu.Lock() + w.win = nil + w.mu.Unlock() + w.setStage(system.StagePaused) +} + +//export Java_org_gioui_GioView_onSurfaceChanged +func Java_org_gioui_GioView_onSurfaceChanged(env *C.JNIEnv, class C.jclass, + handle C.jlong, surf C.jobject) { + w := views[handle] + w.mu.Lock() + w.win = C.ANativeWindow_fromSurface(env, surf) + w.mu.Unlock() + if w.started { + w.setVisible() + } +} + +//export Java_org_gioui_GioView_onLowMemory +func Java_org_gioui_GioView_onLowMemory() { + runtime.GC() + debug.FreeOSMemory() +} + +//export Java_org_gioui_GioView_onConfigurationChanged +func Java_org_gioui_GioView_onConfigurationChanged(env *C.JNIEnv, + class C.jclass, view C.jlong) { + w := views[view] + w.loadConfig(env, class) + if w.stage >= system.StageRunning { + w.draw(true) + } +} + +//export Java_org_gioui_GioView_onFrameCallback +func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, + view C.jlong, nanos C.jlong) { + w, exist := views[view] + if !exist { + return + } + if w.stage < system.StageRunning { + return + } + w.mu.Lock() + anim := w.animating + w.mu.Unlock() + if anim { + runInJVM(javaVM(), func(env *C.JNIEnv) { + callVoidMethod(env, w.view, gioView.postFrameCallback) + }) + w.draw(false) + } +} + +//export Java_org_gioui_GioView_onBack +func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, + view C.jlong) C.jboolean { + w := views[view] + ev := &system.CommandEvent{Type: system.CommandBack} + w.callbacks.Event(ev) + if ev.Cancel { + return C.JNI_TRUE + } + return C.JNI_FALSE +} + +//export Java_org_gioui_GioView_onFocusChange +func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, + view C.jlong, focus C.jboolean) { + w := views[view] + w.callbacks.Event(key.FocusEvent{Focus: focus == C.JNI_TRUE}) +} + +//export Java_org_gioui_GioView_onWindowInsets +func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, + view C.jlong, top, right, bottom, left C.jint) { + w := views[view] + w.insets = system.Insets{ + Top: unit.Px(float32(top)), + Right: unit.Px(float32(right)), + Bottom: unit.Px(float32(bottom)), + Left: unit.Px(float32(left)), + } + if w.stage >= system.StageRunning { + w.draw(true) + } +} + +func (w *window) setVisible() { + win := w.aNativeWindow() + width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) + if width == 0 || height == 0 { + return + } + w.setStage(system.StageRunning) + w.draw(true) +} + +func (w *window) setStage(stage system.Stage) { + if stage == w.stage { + return + } + w.stage = stage + w.callbacks.Event(system.StageEvent{stage}) +} + +func (w *window) nativeWindow(visID int) (*C.ANativeWindow, int, int) { + win := w.aNativeWindow() + var width, height int + if win != nil { + if C.ANativeWindow_setBuffersGeometry(win, 0, 0, + C.int32_t(visID)) != 0 { + panic(errors.New("ANativeWindow_setBuffersGeometry failed")) + } + w, h := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) + width, height = int(w), int(h) + } + return win, width, height +} + +func (w *window) aNativeWindow() *C.ANativeWindow { + w.mu.Lock() + defer w.mu.Unlock() + return w.win +} + +func (w *window) loadConfig(env *C.JNIEnv, class C.jclass) { + dpi := int(C.gio_jni_CallIntMethod(env, w.view, gioView.getDensity)) + w.fontScale = float32(C.gio_jni_CallFloatMethod(env, w.view, + gioView.getFontScale)) + switch dpi { + case C.ACONFIGURATION_DENSITY_NONE, + C.ACONFIGURATION_DENSITY_DEFAULT, + C.ACONFIGURATION_DENSITY_ANY: + // Assume standard density. + w.dpi = C.ACONFIGURATION_DENSITY_MEDIUM + default: + w.dpi = int(dpi) + } +} + +func (w *window) SetAnimating(anim bool) { + w.mu.Lock() + w.animating = anim + w.mu.Unlock() + if anim { + runOnMain(func(env *C.JNIEnv) { + if w.view == 0 { + // View was destroyed while switching to main thread. + return + } + callVoidMethod(env, w.view, gioView.postFrameCallback) + }) + } +} + +func (w *window) draw(sync bool) { + win := w.aNativeWindow() + width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) + if width == 0 || height == 0 { + return + } + const inchPrDp = 1.0 / 160 + ppdp := float32(w.dpi) * inchPrDp + w.callbacks.Event(FrameEvent{ + FrameEvent: system.FrameEvent{ + Now: time.Now(), + Size: image.Point{ + X: int(width), + Y: int(height), + }, + Insets: w.insets, + Metric: unit.Metric{ + PxPerDp: ppdp, + PxPerSp: w.fontScale * ppdp, + }, + }, + Sync: sync, + }) +} + +type keyMapper func(devId, keyCode C.int32_t) rune + +func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) { + if jvm == nil { + panic("nil JVM") + } + runtime.LockOSThread() + defer runtime.UnlockOSThread() + var env *C.JNIEnv + if res := C.gio_jni_GetEnv(jvm, &env, C.JNI_VERSION_1_6); res != C.JNI_OK { + if res != C.JNI_EDETACHED { + panic(fmt.Errorf("JNI GetEnv failed with error %d", res)) + } + if C.gio_jni_AttachCurrentThread(jvm, &env, nil) != C.JNI_OK { + panic(errors.New("runInJVM: AttachCurrentThread failed")) + } + defer C.gio_jni_DetachCurrentThread(jvm) + } + + f(env) +} + +func convertKeyCode(code C.jint) (string, bool) { + var n string + switch code { + case C.AKEYCODE_DPAD_UP: + n = key.NameUpArrow + case C.AKEYCODE_DPAD_DOWN: + n = key.NameDownArrow + case C.AKEYCODE_DPAD_LEFT: + n = key.NameLeftArrow + case C.AKEYCODE_DPAD_RIGHT: + n = key.NameRightArrow + case C.AKEYCODE_FORWARD_DEL: + n = key.NameDeleteForward + case C.AKEYCODE_DEL: + n = key.NameDeleteBackward + case C.AKEYCODE_NUMPAD_ENTER: + n = key.NameEnter + case C.AKEYCODE_ENTER: + n = key.NameEnter + default: + return "", false + } + return n, true +} + +//export Java_org_gioui_GioView_onKeyEvent +func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, + handle C.jlong, keyCode, r C.jint, t C.jlong) { + w := views[handle] + if n, ok := convertKeyCode(keyCode); ok { + w.callbacks.Event(key.Event{Name: n}) + } + if r != 0 { + w.callbacks.Event(key.EditEvent{Text: string(rune(r))}) + } +} + +//export Java_org_gioui_GioView_onTouchEvent +func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, + handle C.jlong, action, pointerID, tool C.jint, + x, y, scrollX, scrollY C.jfloat, jbtns C.jint, t C.jlong) { + w := views[handle] + var typ pointer.Type + switch action { + case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN: + typ = pointer.Press + case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP: + typ = pointer.Release + case C.AMOTION_EVENT_ACTION_CANCEL: + typ = pointer.Cancel + case C.AMOTION_EVENT_ACTION_MOVE: + typ = pointer.Move + case C.AMOTION_EVENT_ACTION_SCROLL: + typ = pointer.Scroll + default: + return + } + var src pointer.Source + var btns pointer.Buttons + if jbtns&C.AMOTION_EVENT_BUTTON_PRIMARY != 0 { + btns |= pointer.ButtonPrimary + } + if jbtns&C.AMOTION_EVENT_BUTTON_SECONDARY != 0 { + btns |= pointer.ButtonSecondary + } + if jbtns&C.AMOTION_EVENT_BUTTON_TERTIARY != 0 { + btns |= pointer.ButtonTertiary + } + switch tool { + case C.AMOTION_EVENT_TOOL_TYPE_FINGER: + src = pointer.Touch + case C.AMOTION_EVENT_TOOL_TYPE_MOUSE: + src = pointer.Mouse + case C.AMOTION_EVENT_TOOL_TYPE_UNKNOWN: + // For example, triggered via 'adb shell input tap'. + // Instead of discarding it, treat it as a touch event. + src = pointer.Touch + default: + return + } + w.callbacks.Event(pointer.Event{ + Type: typ, + Source: src, + Buttons: btns, + PointerID: pointer.ID(pointerID), + Time: time.Duration(t) * time.Millisecond, + Position: f32.Point{X: float32(x), Y: float32(y)}, + Scroll: f32.Pt(float32(scrollX), float32(scrollY)), + }) +} + +func (w *window) ShowTextInput(show bool) { + runOnMain(func(env *C.JNIEnv) { + if w.view == 0 { + return + } + if show { + callVoidMethod(env, w.view, gioView.showTextInput) + } else { + callVoidMethod(env, w.view, gioView.hideTextInput) + } + }) +} + +func javaString(env *C.JNIEnv, str string) C.jstring { + if str == "" { + return 0 + } + utf16Chars := utf16.Encode([]rune(str)) + return C.gio_jni_NewString(env, (*C.jchar)(unsafe.Pointer(&utf16Chars[0])), + C.int(len(utf16Chars))) +} + +func varArgs(args []jvalue) *C.jvalue { + if len(args) == 0 { + return nil + } + return (*C.jvalue)(unsafe.Pointer(&args[0])) +} + +func callStaticVoidMethod(env *C.JNIEnv, cls C.jclass, method C.jmethodID, + args ...jvalue) error { + C.gio_jni_CallStaticVoidMethodA(env, cls, method, varArgs(args)) + return exception(env) +} + +func callStaticObjectMethod(env *C.JNIEnv, cls C.jclass, method C.jmethodID, + args ...jvalue) (C.jobject, error) { + res := C.gio_jni_CallStaticObjectMethodA(env, cls, method, varArgs(args)) + return res, exception(env) +} + +func callVoidMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID, + args ...jvalue) error { + C.gio_jni_CallVoidMethodA(env, obj, method, varArgs(args)) + return exception(env) +} + +func callObjectMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID, + args ...jvalue) (C.jobject, error) { + res := C.gio_jni_CallObjectMethodA(env, obj, method, varArgs(args)) + return res, exception(env) +} + +// exception returns an error corresponding to the pending +// exception, or nil if no exception is pending. The pending +// exception is cleared. +func exception(env *C.JNIEnv) error { + thr := C.gio_jni_ExceptionOccurred(env) + if thr == 0 { + return nil + } + C.gio_jni_ExceptionClear(env) + cls := getObjectClass(env, C.jobject(thr)) + toString := getMethodID(env, cls, "toString", "()Ljava/lang/String;") + msg, err := callObjectMethod(env, C.jobject(thr), toString) + if err != nil { + return err + } + return errors.New(goString(env, C.jstring(msg))) +} + +func getObjectClass(env *C.JNIEnv, obj C.jobject) C.jclass { + if obj == 0 { + panic("null object") + } + cls := C.gio_jni_GetObjectClass(env, C.jobject(obj)) + if err := exception(env); err != nil { + // GetObjectClass should never fail. + panic(err) + } + return cls +} + +// goString converts the JVM jstring to a Go string. +func goString(env *C.JNIEnv, str C.jstring) string { + if str == 0 { + return "" + } + strlen := C.gio_jni_GetStringLength(env, C.jstring(str)) + chars := C.gio_jni_GetStringChars(env, C.jstring(str)) + var utf16Chars []uint16 + hdr := (*reflect.SliceHeader)(unsafe.Pointer(&utf16Chars)) + hdr.Data = uintptr(unsafe.Pointer(chars)) + hdr.Cap = int(strlen) + hdr.Len = int(strlen) + utf8 := utf16.Decode(utf16Chars) + return string(utf8) +} + +func Main() { +} + +func NewWindow(window Callbacks, opts *Options) error { + mainWindow.in <- windowAndOptions{window, opts} + return <-mainWindow.errs +} + +func (w *window) WriteClipboard(s string) { + runOnMain(func(env *C.JNIEnv) { + jstr := javaString(env, s) + callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard, + jvalue(android.appCtx), jvalue(jstr)) + }) +} + +func (w *window) ReadClipboard() { + runOnMain(func(env *C.JNIEnv) { + c, err := callStaticObjectMethod(env, android.gioCls, + android.mreadClipboard, + jvalue(android.appCtx)) + if err != nil { + return + } + content := goString(env, C.jstring(c)) + w.callbacks.Event(clipboard.Event{Text: content}) + }) +} + +func (w *window) Option(opts *Options) {} + +func (w *window) SetCursor(name pointer.CursorName) { + w.setState(func(state *windowState) { + state.cursor = &name + }) +} + +// setState adjust the window state on the main thread. +func (w *window) setState(f func(state *windowState)) { + runOnMain(func(env *C.JNIEnv) { + f(&w.newState) + if w.view == 0 { + // No View attached. The state will be applied at next onCreateView. + return + } + old := w.state + state := w.newState + applyStateDiff(env, w.view, old, state) + w.state = state + }) +} + +func applyStateDiff(env *C.JNIEnv, view C.jobject, old, state windowState) { + if state.cursor != nil && old.cursor != state.cursor { + setCursor(env, view, *state.cursor) + } +} + +func setCursor(env *C.JNIEnv, view C.jobject, name pointer.CursorName) { + var curID int + switch name { + default: + fallthrough + case pointer.CursorDefault: + curID = 1000 // TYPE_ARROW + case pointer.CursorText: + curID = 1008 // TYPE_TEXT + case pointer.CursorPointer: + curID = 1002 // TYPE_HAND + case pointer.CursorCrossHair: + curID = 1007 // TYPE_CROSSHAIR + case pointer.CursorColResize: + curID = 1014 // TYPE_HORIZONTAL_DOUBLE_ARROW + case pointer.CursorRowResize: + curID = 1015 // TYPE_VERTICAL_DOUBLE_ARROW + case pointer.CursorNone: + curID = 0 // TYPE_NULL + } + callVoidMethod(env, view, gioView.setCursor, jvalue(curID)) +} + +// Close the window. Not implemented for Android. +func (w *window) Close() {} + +// runOnMain runs a function on the Java main thread. +func runOnMain(f func(env *C.JNIEnv)) { + go func() { + mainFuncs <- f + runInJVM(javaVM(), func(env *C.JNIEnv) { + callStaticVoidMethod(env, android.gioCls, android.mwakeupMainThread) + }) + }() +} + +//export Java_org_gioui_Gio_scheduleMainFuncs +func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) { + for { + select { + case f := <-mainFuncs: + f(env) + default: + return + } + } +} + +func (_ ViewEvent) ImplementsEvent() {} diff --git a/gio/app/internal/wm/os_darwin.go b/gio/app/internal/wm/os_darwin.go new file mode 100644 index 0000000..9bd7a17 --- /dev/null +++ b/gio/app/internal/wm/os_darwin.go @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +/* +#include + +__attribute__ ((visibility ("hidden"))) void gio_wakeupMainThread(void); +__attribute__ ((visibility ("hidden"))) NSUInteger gio_nsstringLength(CFTypeRef str); +__attribute__ ((visibility ("hidden"))) void gio_nsstringGetCharacters(CFTypeRef str, unichar *chars, NSUInteger loc, NSUInteger length); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createDisplayLink(void); +__attribute__ ((visibility ("hidden"))) void gio_releaseDisplayLink(CFTypeRef dl); +__attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl); +__attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl); +__attribute__ ((visibility ("hidden"))) void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did); +__attribute__ ((visibility ("hidden"))) void gio_hideCursor(); +__attribute__ ((visibility ("hidden"))) void gio_showCursor(); +__attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID); +__attribute__ ((visibility ("hidden"))) bool gio_isMainThread(); +*/ +import "C" +import ( + "errors" + "sync" + "sync/atomic" + "time" + "unicode/utf16" + "unsafe" + + "realy.lol/gio/io/pointer" +) + +// displayLink is the state for a display link (CVDisplayLinkRef on macOS, +// CADisplayLink on iOS). It runs a state-machine goroutine that keeps the +// display link running for a while after being stopped to avoid the thread +// start/stop overhead and because the CVDisplayLink sometimes fails to +// start, stop and start again within a short duration. +type displayLink struct { + callback func() + // states is for starting or stopping the display link. + states chan bool + // done is closed when the display link is destroyed. + done chan struct{} + // dids receives the display id when the callback owner is moved + // to a different screen. + dids chan uint64 + // running tracks the desired state of the link. running is accessed + // with atomic. + running uint32 +} + +// displayLinks maps CFTypeRefs to *displayLinks. +var displayLinks sync.Map + +var mainFuncs = make(chan func(), 1) + +// runOnMain runs the function on the main thread. +func runOnMain(f func()) { + if C.gio_isMainThread() { + f() + return + } + go func() { + mainFuncs <- f + C.gio_wakeupMainThread() + }() +} + +//export gio_dispatchMainFuncs +func gio_dispatchMainFuncs() { + for { + select { + case f := <-mainFuncs: + f() + default: + return + } + } +} + +// nsstringToString converts a NSString to a Go string, and +// releases the original string. +func nsstringToString(str C.CFTypeRef) string { + if str == 0 { + return "" + } + defer C.CFRelease(str) + n := C.gio_nsstringLength(str) + if n == 0 { + return "" + } + chars := make([]uint16, n) + C.gio_nsstringGetCharacters(str, (*C.unichar)(unsafe.Pointer(&chars[0])), 0, + n) + utf8 := utf16.Decode(chars) + return string(utf8) +} + +func NewDisplayLink(callback func()) (*displayLink, error) { + d := &displayLink{ + callback: callback, + done: make(chan struct{}), + states: make(chan bool), + dids: make(chan uint64), + } + dl := C.gio_createDisplayLink() + if dl == 0 { + return nil, errors.New("app: failed to create display link") + } + go d.run(dl) + return d, nil +} + +func (d *displayLink) run(dl C.CFTypeRef) { + defer C.gio_releaseDisplayLink(dl) + displayLinks.Store(dl, d) + defer displayLinks.Delete(dl) + var stopTimer *time.Timer + var tchan <-chan time.Time + started := false + for { + select { + case <-tchan: + tchan = nil + started = false + C.gio_stopDisplayLink(dl) + case start := <-d.states: + switch { + case !start && tchan == nil: + // stopTimeout is the delay before stopping the display link to + // avoid the overhead of frequently starting and stopping the + // link thread. + const stopTimeout = 500 * time.Millisecond + if stopTimer == nil { + stopTimer = time.NewTimer(stopTimeout) + } else { + // stopTimer is always drained when tchan == nil. + stopTimer.Reset(stopTimeout) + } + tchan = stopTimer.C + atomic.StoreUint32(&d.running, 0) + case start: + if tchan != nil && !stopTimer.Stop() { + <-tchan + } + tchan = nil + atomic.StoreUint32(&d.running, 1) + if !started { + started = true + C.gio_startDisplayLink(dl) + } + } + case did := <-d.dids: + C.gio_setDisplayLinkDisplay(dl, C.uint64_t(did)) + case <-d.done: + return + } + } +} + +func (d *displayLink) Start() { + d.states <- true +} + +func (d *displayLink) Stop() { + d.states <- false +} + +func (d *displayLink) Close() { + close(d.done) +} + +func (d *displayLink) SetDisplayID(did uint64) { + d.dids <- did +} + +//export gio_onFrameCallback +func gio_onFrameCallback(dl C.CFTypeRef) { + if d, exists := displayLinks.Load(dl); exists { + d := d.(*displayLink) + if atomic.LoadUint32(&d.running) != 0 { + d.callback() + } + } +} + +// windowSetCursor updates the cursor from the current one to a new one +// and returns the new one. +func windowSetCursor(from, to pointer.CursorName) pointer.CursorName { + if from == to { + return to + } + var curID int + switch to { + default: + to = pointer.CursorDefault + fallthrough + case pointer.CursorDefault: + curID = 1 + case pointer.CursorText: + curID = 2 + case pointer.CursorPointer: + curID = 3 + case pointer.CursorCrossHair: + curID = 4 + case pointer.CursorColResize: + curID = 5 + case pointer.CursorRowResize: + curID = 6 + case pointer.CursorGrab: + curID = 7 + case pointer.CursorNone: + runOnMain(func() { + C.gio_hideCursor() + }) + return to + } + runOnMain(func() { + if from == pointer.CursorNone { + C.gio_showCursor() + } + C.gio_setCursor(C.NSUInteger(curID)) + }) + return to +} diff --git a/gio/app/internal/wm/os_darwin.m b/gio/app/internal/wm/os_darwin.m new file mode 100644 index 0000000..8d37371 --- /dev/null +++ b/gio/app/internal/wm/os_darwin.m @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +@import Dispatch; +@import Foundation; + +#include "_cgo_export.h" + +void gio_wakeupMainThread(void) { + dispatch_async(dispatch_get_main_queue(), ^{ + gio_dispatchMainFuncs(); + }); +} + +bool gio_isMainThread() { + return [NSThread isMainThread]; +} + +NSUInteger gio_nsstringLength(CFTypeRef cstr) { + NSString *str = (__bridge NSString *)cstr; + return [str length]; +} + +void gio_nsstringGetCharacters(CFTypeRef cstr, unichar *chars, NSUInteger loc, NSUInteger length) { + NSString *str = (__bridge NSString *)cstr; + [str getCharacters:chars range:NSMakeRange(loc, length)]; +} diff --git a/gio/app/internal/wm/os_ios.go b/gio/app/internal/wm/os_ios.go new file mode 100644 index 0000000..62d854f --- /dev/null +++ b/gio/app/internal/wm/os_ios.go @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build darwin && ios +// +build darwin,ios + +package wm + +/* +#cgo CFLAGS: -DGLES_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c + +#include +#include +#include + +struct drawParams { + CGFloat dpi, sdpi; + CGFloat width, height; + CGFloat top, right, bottom, left; +}; + +__attribute__ ((visibility ("hidden"))) void gio_showTextInput(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) void gio_hideTextInput(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) void gio_addLayerToView(CFTypeRef viewRef, CFTypeRef layerRef); +__attribute__ ((visibility ("hidden"))) void gio_updateView(CFTypeRef viewRef, CFTypeRef layerRef); +__attribute__ ((visibility ("hidden"))) void gio_removeLayer(CFTypeRef layerRef); +__attribute__ ((visibility ("hidden"))) struct drawParams gio_viewDrawParams(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_readClipboard(void); +__attribute__ ((visibility ("hidden"))) void gio_writeClipboard(unichar *chars, NSUInteger length); +*/ +import "C" + +import ( + "image" + "runtime" + "runtime/debug" + "sync/atomic" + "time" + "unicode/utf16" + "unsafe" + + "realy.lol/gio/f32" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/unit" +) + +type window struct { + view C.CFTypeRef + w Callbacks + displayLink *displayLink + + layer C.CFTypeRef + visible atomic.Value + cursor pointer.CursorName + + pointerMap []C.CFTypeRef +} + +var mainWindow = newWindowRendezvous() + +var layerFactory func() uintptr + +var views = make(map[C.CFTypeRef]*window) + +func init() { + // Darwin requires UI operations happen on the main thread only. + runtime.LockOSThread() +} + +//export onCreate +func onCreate(view C.CFTypeRef) { + w := &window{ + view: view, + } + dl, err := NewDisplayLink(func() { + w.draw(false) + }) + if err != nil { + panic(err) + } + w.displayLink = dl + wopts := <-mainWindow.out + w.w = wopts.window + w.w.SetDriver(w) + w.visible.Store(false) + w.layer = C.CFTypeRef(layerFactory()) + C.gio_addLayerToView(view, w.layer) + views[view] = w + w.w.Event(system.StageEvent{Stage: system.StagePaused}) +} + +//export gio_onDraw +func gio_onDraw(view C.CFTypeRef) { + w := views[view] + w.draw(true) +} + +func (w *window) draw(sync bool) { + params := C.gio_viewDrawParams(w.view) + if params.width == 0 || params.height == 0 { + return + } + wasVisible := w.isVisible() + w.visible.Store(true) + C.gio_updateView(w.view, w.layer) + if !wasVisible { + w.w.Event(system.StageEvent{Stage: system.StageRunning}) + } + const inchPrDp = 1.0 / 163 + w.w.Event(FrameEvent{ + FrameEvent: system.FrameEvent{ + Now: time.Now(), + Size: image.Point{ + X: int(params.width + .5), + Y: int(params.height + .5), + }, + Insets: system.Insets{ + Top: unit.Px(float32(params.top)), + Right: unit.Px(float32(params.right)), + Bottom: unit.Px(float32(params.bottom)), + Left: unit.Px(float32(params.left)), + }, + Metric: unit.Metric{ + PxPerDp: float32(params.dpi) * inchPrDp, + PxPerSp: float32(params.sdpi) * inchPrDp, + }, + }, + Sync: sync, + }) +} + +//export onStop +func onStop(view C.CFTypeRef) { + w := views[view] + w.visible.Store(false) + w.w.Event(system.StageEvent{Stage: system.StagePaused}) +} + +//export onDestroy +func onDestroy(view C.CFTypeRef) { + w := views[view] + delete(views, view) + w.w.Event(system.DestroyEvent{}) + w.displayLink.Close() + C.gio_removeLayer(w.layer) + C.CFRelease(w.layer) + w.layer = 0 + w.view = 0 +} + +//export onFocus +func onFocus(view C.CFTypeRef, focus int) { + w := views[view] + w.w.Event(key.FocusEvent{Focus: focus != 0}) +} + +//export onLowMemory +func onLowMemory() { + runtime.GC() + debug.FreeOSMemory() +} + +//export onUpArrow +func onUpArrow(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameUpArrow) +} + +//export onDownArrow +func onDownArrow(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameDownArrow) +} + +//export onLeftArrow +func onLeftArrow(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameLeftArrow) +} + +//export onRightArrow +func onRightArrow(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameRightArrow) +} + +//export onDeleteBackward +func onDeleteBackward(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameDeleteBackward) +} + +//export onText +func onText(view C.CFTypeRef, str *C.char) { + w := views[view] + w.w.Event(key.EditEvent{ + Text: C.GoString(str), + }) +} + +//export onTouch +func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, + x, y C.CGFloat, ti C.double) { + var typ pointer.Type + switch phase { + case C.UITouchPhaseBegan: + typ = pointer.Press + case C.UITouchPhaseMoved: + typ = pointer.Move + case C.UITouchPhaseEnded: + typ = pointer.Release + case C.UITouchPhaseCancelled: + typ = pointer.Cancel + default: + return + } + w := views[view] + t := time.Duration(float64(ti) * float64(time.Second)) + p := f32.Point{X: float32(x), Y: float32(y)} + w.w.Event(pointer.Event{ + Type: typ, + Source: pointer.Touch, + PointerID: w.lookupTouch(last != 0, touchRef), + Position: p, + Time: t, + }) +} + +func (w *window) ReadClipboard() { + runOnMain(func() { + content := nsstringToString(C.gio_readClipboard()) + w.w.Event(clipboard.Event{Text: content}) + }) +} + +func (w *window) WriteClipboard(s string) { + u16 := utf16.Encode([]rune(s)) + runOnMain(func() { + var chars *C.unichar + if len(u16) > 0 { + chars = (*C.unichar)(unsafe.Pointer(&u16[0])) + } + C.gio_writeClipboard(chars, C.NSUInteger(len(u16))) + }) +} + +func (w *window) Option(opts *Options) {} + +func (w *window) SetAnimating(anim bool) { + v := w.view + if v == 0 { + return + } + if anim { + w.displayLink.Start() + } else { + w.displayLink.Stop() + } +} + +func (w *window) SetCursor(name pointer.CursorName) { + w.cursor = windowSetCursor(w.cursor, name) +} + +func (w *window) onKeyCommand(name string) { + w.w.Event(key.Event{ + Name: name, + }) +} + +// lookupTouch maps an UITouch pointer value to an index. If +// last is set, the map is cleared. +func (w *window) lookupTouch(last bool, touch C.CFTypeRef) pointer.ID { + id := -1 + for i, ref := range w.pointerMap { + if ref == touch { + id = i + break + } + } + if id == -1 { + id = len(w.pointerMap) + w.pointerMap = append(w.pointerMap, touch) + } + if last { + w.pointerMap = w.pointerMap[:0] + } + return pointer.ID(id) +} + +func (w *window) contextLayer() uintptr { + return uintptr(w.layer) +} + +func (w *window) isVisible() bool { + return w.visible.Load().(bool) +} + +func (w *window) ShowTextInput(show bool) { + v := w.view + if v == 0 { + return + } + C.CFRetain(v) + runOnMain(func() { + defer C.CFRelease(v) + if show { + C.gio_showTextInput(w.view) + } else { + C.gio_hideTextInput(w.view) + } + }) +} + +// Close the window. Not implemented for iOS. +func (w *window) Close() {} + +func NewWindow(win Callbacks, opts *Options) error { + mainWindow.in <- windowAndOptions{win, opts} + return <-mainWindow.errs +} + +func Main() { +} + +//export gio_runMain +func gio_runMain() { + runMain() +} diff --git a/gio/app/internal/wm/os_ios.m b/gio/app/internal/wm/os_ios.m new file mode 100644 index 0000000..f1e556d --- /dev/null +++ b/gio/app/internal/wm/os_ios.m @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,ios + +@import UIKit; + +#include +#include "_cgo_export.h" +#include "framework_ios.h" + +@interface GioView: UIView +@end + +@implementation GioViewController + +CGFloat _keyboardHeight; + +- (void)loadView { + gio_runMain(); + + CGRect zeroFrame = CGRectMake(0, 0, 0, 0); + self.view = [[UIView alloc] initWithFrame:zeroFrame]; + self.view.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0); + UIView *drawView = [[GioView alloc] initWithFrame:zeroFrame]; + [self.view addSubview: drawView]; +#ifndef TARGET_OS_TV + drawView.multipleTouchEnabled = YES; +#endif + drawView.preservesSuperviewLayoutMargins = YES; + drawView.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0); + onCreate((__bridge CFTypeRef)drawView); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillChange:) + name:UIKeyboardWillShowNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillChange:) + name:UIKeyboardWillChangeFrameNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillHide:) + name:UIKeyboardWillHideNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver: self + selector: @selector(applicationDidEnterBackground:) + name: UIApplicationDidEnterBackgroundNotification + object: nil]; + [[NSNotificationCenter defaultCenter] addObserver: self + selector: @selector(applicationWillEnterForeground:) + name: UIApplicationWillEnterForegroundNotification + object: nil]; +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + UIView *drawView = self.view.subviews[0]; + if (drawView != nil) { + gio_onDraw((__bridge CFTypeRef)drawView); + } +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + UIView *drawView = self.view.subviews[0]; + if (drawView != nil) { + onStop((__bridge CFTypeRef)drawView); + } +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + CFTypeRef viewRef = (__bridge CFTypeRef)self.view.subviews[0]; + onDestroy(viewRef); +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + UIView *view = self.view.subviews[0]; + CGRect frame = self.view.bounds; + // Adjust view bounds to make room for the keyboard. + frame.size.height -= _keyboardHeight; + view.frame = frame; + gio_onDraw((__bridge CFTypeRef)view); +} + +- (void)didReceiveMemoryWarning { + onLowMemory(); + [super didReceiveMemoryWarning]; +} + +- (void)keyboardWillChange:(NSNotification *)note { + NSDictionary *userInfo = note.userInfo; + CGRect f = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + _keyboardHeight = f.size.height; + [self.view setNeedsLayout]; +} + +- (void)keyboardWillHide:(NSNotification *)note { + _keyboardHeight = 0.0; + [self.view setNeedsLayout]; +} +@end + +static void handleTouches(int last, UIView *view, NSSet *touches, UIEvent *event) { + CGFloat scale = view.contentScaleFactor; + NSUInteger i = 0; + NSUInteger n = [touches count]; + CFTypeRef viewRef = (__bridge CFTypeRef)view; + for (UITouch *touch in touches) { + CFTypeRef touchRef = (__bridge CFTypeRef)touch; + i++; + NSArray *coalescedTouches = [event coalescedTouchesForTouch:touch]; + NSUInteger j = 0; + NSUInteger m = [coalescedTouches count]; + for (UITouch *coalescedTouch in [event coalescedTouchesForTouch:touch]) { + CGPoint loc = [coalescedTouch locationInView:view]; + j++; + int lastTouch = last && i == n && j == m; + onTouch(lastTouch, viewRef, touchRef, touch.phase, loc.x*scale, loc.y*scale, [coalescedTouch timestamp]); + } + } +} + +@implementation GioView +NSArray *_keyCommands; ++ (void)onFrameCallback:(CADisplayLink *)link { + gio_onFrameCallback((__bridge CFTypeRef)link); +} + +- (void)willMoveToWindow:(UIWindow *)newWindow { + if (self.window != nil) { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIWindowDidBecomeKeyNotification + object:self.window]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIWindowDidResignKeyNotification + object:self.window]; + } + self.contentScaleFactor = newWindow.screen.nativeScale; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(onWindowDidBecomeKey:) + name:UIWindowDidBecomeKeyNotification + object:newWindow]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(onWindowDidResignKey:) + name:UIWindowDidResignKeyNotification + object:newWindow]; +} + +- (void)onWindowDidBecomeKey:(NSNotification *)note { + if (self.isFirstResponder) { + onFocus((__bridge CFTypeRef)self, YES); + } +} + +- (void)onWindowDidResignKey:(NSNotification *)note { + if (self.isFirstResponder) { + onFocus((__bridge CFTypeRef)self, NO); + } +} + +- (void)dealloc { +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + handleTouches(0, self, touches, event); +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + handleTouches(0, self, touches, event); +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + handleTouches(1, self, touches, event); +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + handleTouches(1, self, touches, event); +} + +- (void)insertText:(NSString *)text { + onText((__bridge CFTypeRef)self, (char *)text.UTF8String); +} + +- (BOOL)canBecomeFirstResponder { + return YES; +} + +- (BOOL)hasText { + return YES; +} + +- (void)deleteBackward { + onDeleteBackward((__bridge CFTypeRef)self); +} + +- (void)onUpArrow { + onUpArrow((__bridge CFTypeRef)self); +} + +- (void)onDownArrow { + onDownArrow((__bridge CFTypeRef)self); +} + +- (void)onLeftArrow { + onLeftArrow((__bridge CFTypeRef)self); +} + +- (void)onRightArrow { + onRightArrow((__bridge CFTypeRef)self); +} + +- (NSArray *)keyCommands { + if (_keyCommands == nil) { + _keyCommands = @[ + [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow + modifierFlags:0 + action:@selector(onUpArrow)], + [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow + modifierFlags:0 + action:@selector(onDownArrow)], + [UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow + modifierFlags:0 + action:@selector(onLeftArrow)], + [UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow + modifierFlags:0 + action:@selector(onRightArrow)] + ]; + } + return _keyCommands; +} +@end + +void gio_writeClipboard(unichar *chars, NSUInteger length) { + @autoreleasepool { + NSString *s = [NSString string]; + if (length > 0) { + s = [NSString stringWithCharacters:chars length:length]; + } + UIPasteboard *p = UIPasteboard.generalPasteboard; + p.string = s; + } +} + +CFTypeRef gio_readClipboard(void) { + @autoreleasepool { + UIPasteboard *p = UIPasteboard.generalPasteboard; + return (__bridge_retained CFTypeRef)p.string; + } +} + +void gio_showTextInput(CFTypeRef viewRef) { + UIView *view = (__bridge UIView *)viewRef; + [view becomeFirstResponder]; +} + +void gio_hideTextInput(CFTypeRef viewRef) { + UIView *view = (__bridge UIView *)viewRef; + [view resignFirstResponder]; +} + +void gio_addLayerToView(CFTypeRef viewRef, CFTypeRef layerRef) { + UIView *view = (__bridge UIView *)viewRef; + CALayer *layer = (__bridge CALayer *)layerRef; + [view.layer addSublayer:layer]; +} + +void gio_updateView(CFTypeRef viewRef, CFTypeRef layerRef) { + UIView *view = (__bridge UIView *)viewRef; + CAEAGLLayer *layer = (__bridge CAEAGLLayer *)layerRef; + layer.contentsScale = view.contentScaleFactor; + layer.bounds = view.bounds; +} + +void gio_removeLayer(CFTypeRef layerRef) { + CALayer *layer = (__bridge CALayer *)layerRef; + [layer removeFromSuperlayer]; +} + +struct drawParams gio_viewDrawParams(CFTypeRef viewRef) { + UIView *v = (__bridge UIView *)viewRef; + struct drawParams params; + CGFloat scale = v.layer.contentsScale; + // Use 163 as the standard ppi on iOS. + params.dpi = 163*scale; + params.sdpi = params.dpi; + UIEdgeInsets insets = v.layoutMargins; + if (@available(iOS 11.0, tvOS 11.0, *)) { + UIFontMetrics *metrics = [UIFontMetrics defaultMetrics]; + params.sdpi = [metrics scaledValueForValue:params.sdpi]; + insets = v.safeAreaInsets; + } + params.width = v.bounds.size.width*scale; + params.height = v.bounds.size.height*scale; + params.top = insets.top*scale; + params.right = insets.right*scale; + params.bottom = insets.bottom*scale; + params.left = insets.left*scale; + return params; +} + +CFTypeRef gio_createDisplayLink(void) { + CADisplayLink *dl = [CADisplayLink displayLinkWithTarget:[GioView class] selector:@selector(onFrameCallback:)]; + dl.paused = YES; + NSRunLoop *runLoop = [NSRunLoop mainRunLoop]; + [dl addToRunLoop:runLoop forMode:[runLoop currentMode]]; + return (__bridge_retained CFTypeRef)dl; +} + +int gio_startDisplayLink(CFTypeRef dlref) { + CADisplayLink *dl = (__bridge CADisplayLink *)dlref; + dl.paused = NO; + return 0; +} + +int gio_stopDisplayLink(CFTypeRef dlref) { + CADisplayLink *dl = (__bridge CADisplayLink *)dlref; + dl.paused = YES; + return 0; +} + +void gio_releaseDisplayLink(CFTypeRef dlref) { + CADisplayLink *dl = (__bridge CADisplayLink *)dlref; + [dl invalidate]; + CFRelease(dlref); +} + +void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did) { + // Nothing to do on iOS. +} + +void gio_hideCursor() { + // Not supported. +} + +void gio_showCursor() { + // Not supported. +} + +void gio_setCursor(NSUInteger curID) { + // Not supported. +} diff --git a/gio/app/internal/wm/os_js.go b/gio/app/internal/wm/os_js.go new file mode 100644 index 0000000..f30f7bc --- /dev/null +++ b/gio/app/internal/wm/os_js.go @@ -0,0 +1,656 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +import ( + "image" + "strings" + "sync" + "syscall/js" + "time" + "unicode" + "unicode/utf8" + + "realy.lol/gio/f32" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/unit" +) + +type window struct { + window js.Value + document js.Value + clipboard js.Value + cnv js.Value + tarea js.Value + w Callbacks + redraw js.Func + clipboardCallback js.Func + requestAnimationFrame js.Value + browserHistory js.Value + visualViewport js.Value + cleanfuncs []func() + touches []js.Value + composing bool + requestFocus bool + + chanAnimation chan struct{} + chanRedraw chan struct{} + + mu sync.Mutex + size f32.Point + inset f32.Point + scale float32 + animating bool + // animRequested tracks whether a requestAnimationFrame callback + // is pending. + animRequested bool +} + +func NewWindow(win Callbacks, opts *Options) error { + doc := js.Global().Get("document") + cont := getContainer(doc) + cnv := createCanvas(doc) + cont.Call("appendChild", cnv) + tarea := createTextArea(doc) + cont.Call("appendChild", tarea) + w := &window{ + cnv: cnv, + document: doc, + tarea: tarea, + window: js.Global().Get("window"), + clipboard: js.Global().Get("navigator").Get("clipboard"), + } + w.requestAnimationFrame = w.window.Get("requestAnimationFrame") + w.browserHistory = w.window.Get("history") + w.visualViewport = w.window.Get("visualViewport") + if w.visualViewport.IsUndefined() { + w.visualViewport = w.window + } + w.chanAnimation = make(chan struct{}, 1) + w.chanRedraw = make(chan struct{}, 1) + w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} { + w.chanAnimation <- struct{}{} + return nil + }) + w.clipboardCallback = w.funcOf(func(this js.Value, + args []js.Value) interface{} { + content := args[0].String() + win.Event(clipboard.Event{Text: content}) + return nil + }) + w.addEventListeners() + w.addHistory() + w.Option(opts) + w.w = win + + go func() { + defer w.cleanup() + w.w.SetDriver(w) + w.blur() + w.w.Event(system.StageEvent{Stage: system.StageRunning}) + w.resize() + w.draw(true) + for { + select { + case <-w.chanAnimation: + w.animCallback() + case <-w.chanRedraw: + w.draw(true) + } + } + }() + return nil +} + +func getContainer(doc js.Value) js.Value { + cont := doc.Call("getElementById", "giowindow") + if !cont.IsNull() { + return cont + } + cont = doc.Call("createElement", "DIV") + doc.Get("body").Call("appendChild", cont) + return cont +} + +func createTextArea(doc js.Value) js.Value { + tarea := doc.Call("createElement", "input") + style := tarea.Get("style") + style.Set("width", "1px") + style.Set("height", "1px") + style.Set("opacity", "0") + style.Set("border", "0") + style.Set("padding", "0") + tarea.Set("autocomplete", "off") + tarea.Set("autocorrect", "off") + tarea.Set("autocapitalize", "off") + tarea.Set("spellcheck", false) + return tarea +} + +func createCanvas(doc js.Value) js.Value { + cnv := doc.Call("createElement", "canvas") + style := cnv.Get("style") + style.Set("position", "fixed") + style.Set("width", "100%") + style.Set("height", "100%") + return cnv +} + +func (w *window) cleanup() { + // Cleanup in the opposite order of + // construction. + for i := len(w.cleanfuncs) - 1; i >= 0; i-- { + w.cleanfuncs[i]() + } + w.cleanfuncs = nil +} + +func (w *window) addEventListeners() { + w.addEventListener(w.visualViewport, "resize", + func(this js.Value, args []js.Value) interface{} { + w.resize() + w.chanRedraw <- struct{}{} + return nil + }) + w.addEventListener(w.window, "contextmenu", + func(this js.Value, args []js.Value) interface{} { + args[0].Call("preventDefault") + return nil + }) + w.addEventListener(w.window, "popstate", + func(this js.Value, args []js.Value) interface{} { + ev := &system.CommandEvent{Type: system.CommandBack} + w.w.Event(ev) + if ev.Cancel { + return w.browserHistory.Call("forward") + } + + return w.browserHistory.Call("back") + }) + w.addEventListener(w.document, "visibilitychange", + func(this js.Value, args []js.Value) interface{} { + ev := system.StageEvent{} + switch w.document.Get("visibilityState").String() { + case "hidden", "prerender", "unloaded": + ev.Stage = system.StagePaused + default: + ev.Stage = system.StageRunning + } + w.w.Event(ev) + return nil + }) + w.addEventListener(w.cnv, "mousemove", + func(this js.Value, args []js.Value) interface{} { + w.pointerEvent(pointer.Move, 0, 0, args[0]) + return nil + }) + w.addEventListener(w.cnv, "mousedown", + func(this js.Value, args []js.Value) interface{} { + w.pointerEvent(pointer.Press, 0, 0, args[0]) + if w.requestFocus { + w.focus() + w.requestFocus = false + } + return nil + }) + w.addEventListener(w.cnv, "mouseup", + func(this js.Value, args []js.Value) interface{} { + w.pointerEvent(pointer.Release, 0, 0, args[0]) + return nil + }) + w.addEventListener(w.cnv, "wheel", + func(this js.Value, args []js.Value) interface{} { + e := args[0] + dx, dy := e.Get("deltaX").Float(), e.Get("deltaY").Float() + mode := e.Get("deltaMode").Int() + switch mode { + case 0x01: // DOM_DELTA_LINE + dx *= 10 + dy *= 10 + case 0x02: // DOM_DELTA_PAGE + dx *= 120 + dy *= 120 + } + w.pointerEvent(pointer.Scroll, float32(dx), float32(dy), e) + return nil + }) + w.addEventListener(w.cnv, "touchstart", + func(this js.Value, args []js.Value) interface{} { + w.touchEvent(pointer.Press, args[0]) + if w.requestFocus { + w.focus() // iOS can only focus inside a Touch event. + w.requestFocus = false + } + return nil + }) + w.addEventListener(w.cnv, "touchend", + func(this js.Value, args []js.Value) interface{} { + w.touchEvent(pointer.Release, args[0]) + return nil + }) + w.addEventListener(w.cnv, "touchmove", + func(this js.Value, args []js.Value) interface{} { + w.touchEvent(pointer.Move, args[0]) + return nil + }) + w.addEventListener(w.cnv, "touchcancel", + func(this js.Value, args []js.Value) interface{} { + // Cancel all touches even if only one touch was cancelled. + for i := range w.touches { + w.touches[i] = js.Null() + } + w.touches = w.touches[:0] + w.w.Event(pointer.Event{ + Type: pointer.Cancel, + Source: pointer.Touch, + }) + return nil + }) + w.addEventListener(w.tarea, "focus", + func(this js.Value, args []js.Value) interface{} { + w.w.Event(key.FocusEvent{Focus: true}) + return nil + }) + w.addEventListener(w.tarea, "blur", + func(this js.Value, args []js.Value) interface{} { + w.w.Event(key.FocusEvent{Focus: false}) + w.blur() + return nil + }) + w.addEventListener(w.tarea, "keydown", + func(this js.Value, args []js.Value) interface{} { + w.keyEvent(args[0], key.Press) + return nil + }) + w.addEventListener(w.tarea, "keyup", + func(this js.Value, args []js.Value) interface{} { + w.keyEvent(args[0], key.Release) + return nil + }) + w.addEventListener(w.tarea, "compositionstart", + func(this js.Value, args []js.Value) interface{} { + w.composing = true + return nil + }) + w.addEventListener(w.tarea, "compositionend", + func(this js.Value, args []js.Value) interface{} { + w.composing = false + w.flushInput() + return nil + }) + w.addEventListener(w.tarea, "input", + func(this js.Value, args []js.Value) interface{} { + if w.composing { + return nil + } + w.flushInput() + return nil + }) + w.addEventListener(w.tarea, "paste", + func(this js.Value, args []js.Value) interface{} { + if w.clipboard.IsUndefined() { + return nil + } + // Prevents duplicated-paste, since "paste" is already handled through Clipboard API. + args[0].Call("preventDefault") + return nil + }) +} + +func (w *window) addHistory() { + w.browserHistory.Call("pushState", nil, nil, + w.window.Get("location").Get("href")) +} + +func (w *window) flushInput() { + val := w.tarea.Get("value").String() + w.tarea.Set("value", "") + w.w.Event(key.EditEvent{Text: string(val)}) +} + +func (w *window) blur() { + w.tarea.Call("blur") + w.requestFocus = false +} + +func (w *window) focus() { + w.tarea.Call("focus") + w.requestFocus = true +} + +func (w *window) keyEvent(e js.Value, ks key.State) { + k := e.Get("key").String() + if n, ok := translateKey(k); ok { + cmd := key.Event{ + Name: n, + Modifiers: modifiersFor(e), + State: ks, + } + w.w.Event(cmd) + } +} + +// modifiersFor returns the modifier set for a DOM MouseEvent or +// KeyEvent. +func modifiersFor(e js.Value) key.Modifiers { + var mods key.Modifiers + if e.Get("getModifierState").IsUndefined() { + // Some browsers doesn't support getModifierState. + return mods + } + if e.Call("getModifierState", "Alt").Bool() { + mods |= key.ModAlt + } + if e.Call("getModifierState", "Control").Bool() { + mods |= key.ModCtrl + } + if e.Call("getModifierState", "Shift").Bool() { + mods |= key.ModShift + } + return mods +} + +func (w *window) touchEvent(typ pointer.Type, e js.Value) { + e.Call("preventDefault") + t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond + changedTouches := e.Get("changedTouches") + n := changedTouches.Length() + rect := w.cnv.Call("getBoundingClientRect") + w.mu.Lock() + scale := w.scale + w.mu.Unlock() + var mods key.Modifiers + if e.Get("shiftKey").Bool() { + mods |= key.ModShift + } + if e.Get("altKey").Bool() { + mods |= key.ModAlt + } + if e.Get("ctrlKey").Bool() { + mods |= key.ModCtrl + } + for i := 0; i < n; i++ { + touch := changedTouches.Index(i) + pid := w.touchIDFor(touch) + x, y := touch.Get("clientX").Float(), touch.Get("clientY").Float() + x -= rect.Get("left").Float() + y -= rect.Get("top").Float() + pos := f32.Point{ + X: float32(x) * scale, + Y: float32(y) * scale, + } + w.w.Event(pointer.Event{ + Type: typ, + Source: pointer.Touch, + Position: pos, + PointerID: pid, + Time: t, + Modifiers: mods, + }) + } +} + +func (w *window) touchIDFor(touch js.Value) pointer.ID { + id := touch.Get("identifier") + for i, id2 := range w.touches { + if id2.Equal(id) { + return pointer.ID(i) + } + } + pid := pointer.ID(len(w.touches)) + w.touches = append(w.touches, id) + return pid +} + +func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) { + e.Call("preventDefault") + x, y := e.Get("clientX").Float(), e.Get("clientY").Float() + rect := w.cnv.Call("getBoundingClientRect") + x -= rect.Get("left").Float() + y -= rect.Get("top").Float() + w.mu.Lock() + scale := w.scale + w.mu.Unlock() + pos := f32.Point{ + X: float32(x) * scale, + Y: float32(y) * scale, + } + scroll := f32.Point{ + X: dx * scale, + Y: dy * scale, + } + t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond + jbtns := e.Get("buttons").Int() + var btns pointer.Buttons + if jbtns&1 != 0 { + btns |= pointer.ButtonPrimary + } + if jbtns&2 != 0 { + btns |= pointer.ButtonSecondary + } + if jbtns&4 != 0 { + btns |= pointer.ButtonTertiary + } + w.w.Event(pointer.Event{ + Type: typ, + Source: pointer.Mouse, + Buttons: btns, + Position: pos, + Scroll: scroll, + Time: t, + Modifiers: modifiersFor(e), + }) +} + +func (w *window) addEventListener(this js.Value, event string, + f func(this js.Value, args []js.Value) interface{}) { + jsf := w.funcOf(f) + this.Call("addEventListener", event, jsf) + w.cleanfuncs = append(w.cleanfuncs, func() { + this.Call("removeEventListener", event, jsf) + }) +} + +// funcOf is like js.FuncOf but adds the js.Func to a list of +// functions to be released during cleanup. +func (w *window) funcOf(f func(this js.Value, + args []js.Value) interface{}) js.Func { + jsf := js.FuncOf(f) + w.cleanfuncs = append(w.cleanfuncs, jsf.Release) + return jsf +} + +func (w *window) animCallback() { + w.mu.Lock() + anim := w.animating + w.animRequested = anim + if anim { + w.requestAnimationFrame.Invoke(w.redraw) + } + w.mu.Unlock() + if anim { + w.draw(false) + } +} + +func (w *window) SetAnimating(anim bool) { + w.mu.Lock() + defer w.mu.Unlock() + w.animating = anim + if anim && !w.animRequested { + w.animRequested = true + w.requestAnimationFrame.Invoke(w.redraw) + } +} + +func (w *window) ReadClipboard() { + if w.clipboard.IsUndefined() { + return + } + if w.clipboard.Get("readText").IsUndefined() { + return + } + w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback) +} + +func (w *window) WriteClipboard(s string) { + if w.clipboard.IsUndefined() { + return + } + if w.clipboard.Get("writeText").IsUndefined() { + return + } + w.clipboard.Call("writeText", s) +} + +func (w *window) Option(opts *Options) { + if o := opts.WindowMode; o != nil { + w.windowMode(*o) + } +} + +func (w *window) SetCursor(name pointer.CursorName) { + style := w.cnv.Get("style") + style.Set("cursor", string(name)) +} + +func (w *window) ShowTextInput(show bool) { + // Run in a goroutine to avoid a deadlock if the + // focus change result in an event. + go func() { + if show { + w.focus() + } else { + w.blur() + } + }() +} + +// Close the window. Not implemented for js. +func (w *window) Close() {} + +func (w *window) resize() { + w.mu.Lock() + defer w.mu.Unlock() + + w.scale = float32(w.window.Get("devicePixelRatio").Float()) + + rect := w.cnv.Call("getBoundingClientRect") + w.size.X = float32(rect.Get("width").Float()) * w.scale + w.size.Y = float32(rect.Get("height").Float()) * w.scale + + if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() { + w.inset.X = w.size.X - float32(vx.Float())*w.scale + w.inset.Y = w.size.Y - float32(vy.Float())*w.scale + } + + if w.size.X == 0 || w.size.Y == 0 { + return + } + + w.cnv.Set("width", int(w.size.X+.5)) + w.cnv.Set("height", int(w.size.Y+.5)) +} + +func (w *window) draw(sync bool) { + width, height, insets, metric := w.config() + if metric == (unit.Metric{}) || width == 0 || height == 0 { + return + } + + w.w.Event(FrameEvent{ + FrameEvent: system.FrameEvent{ + Now: time.Now(), + Size: image.Point{ + X: width, + Y: height, + }, + Insets: insets, + Metric: metric, + }, + Sync: sync, + }) +} + +func (w *window) config() (int, int, system.Insets, unit.Metric) { + w.mu.Lock() + defer w.mu.Unlock() + + return int(w.size.X + .5), int(w.size.Y + .5), system.Insets{ + Bottom: unit.Px(w.inset.Y), + Right: unit.Px(w.inset.X), + }, unit.Metric{ + PxPerDp: w.scale, + PxPerSp: w.scale, + } +} + +func (w *window) windowMode(mode WindowMode) { + switch mode { + case Windowed: + if fs := w.document.Get("fullscreenElement"); !fs.Truthy() { + return // Browser is already Windowed. + } + if !w.document.Get("exitFullscreen").Truthy() { + return // Browser doesn't support such feature. + } + w.document.Call("exitFullscreen") + case Fullscreen: + elem := w.document.Get("documentElement") + if !elem.Get("requestFullscreen").Truthy() { + return // Browser doesn't support such feature. + } + elem.Call("requestFullscreen") + } +} + +func Main() { + select {} +} + +func translateKey(k string) (string, bool) { + var n string + switch k { + case "ArrowUp": + n = key.NameUpArrow + case "ArrowDown": + n = key.NameDownArrow + case "ArrowLeft": + n = key.NameLeftArrow + case "ArrowRight": + n = key.NameRightArrow + case "Escape": + n = key.NameEscape + case "Enter": + n = key.NameReturn + case "Backspace": + n = key.NameDeleteBackward + case "Delete": + n = key.NameDeleteForward + case "Home": + n = key.NameHome + case "End": + n = key.NameEnd + case "PageUp": + n = key.NamePageUp + case "PageDown": + n = key.NamePageDown + case "Tab": + n = key.NameTab + case " ": + n = key.NameSpace + case "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12": + n = k + default: + r, s := utf8.DecodeRuneInString(k) + // If there is exactly one printable character, return that. + if s == len(k) && unicode.IsPrint(r) { + return strings.ToUpper(k), true + } + return "", false + } + return n, true +} diff --git a/gio/app/internal/wm/os_macos.go b/gio/app/internal/wm/os_macos.go new file mode 100644 index 0000000..f93a5b8 --- /dev/null +++ b/gio/app/internal/wm/os_macos.go @@ -0,0 +1,516 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build darwin && !ios +// +build darwin,!ios + +package wm + +import ( + "errors" + "image" + "runtime" + "time" + "unicode" + "unicode/utf16" + "unsafe" + + "realy.lol/gio/f32" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/unit" + + _ "realy.lol/gio/internal/cocoainit" +) + +/* +#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c + +#include + +#define GIO_MOUSE_MOVE 1 +#define GIO_MOUSE_UP 2 +#define GIO_MOUSE_DOWN 3 +#define GIO_MOUSE_SCROLL 4 + +__attribute__ ((visibility ("hidden"))) void gio_main(void); +__attribute__ ((visibility ("hidden"))) CGFloat gio_viewWidth(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) CGFloat gio_viewHeight(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) CGFloat gio_getViewBackingScale(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) CGFloat gio_getScreenBackingScale(void); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_readClipboard(void); +__attribute__ ((visibility ("hidden"))) void gio_writeClipboard(unichar *chars, NSUInteger length); +__attribute__ ((visibility ("hidden"))) void gio_setNeedsDisplay(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) void gio_toggleFullScreen(CFTypeRef windowRef); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight); +__attribute__ ((visibility ("hidden"))) void gio_makeKeyAndOrderFront(CFTypeRef windowRef); +__attribute__ ((visibility ("hidden"))) NSPoint gio_cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft); +__attribute__ ((visibility ("hidden"))) void gio_close(CFTypeRef windowRef); +__attribute__ ((visibility ("hidden"))) void gio_setSize(CFTypeRef windowRef, CGFloat width, CGFloat height); +__attribute__ ((visibility ("hidden"))) void gio_setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height); +__attribute__ ((visibility ("hidden"))) void gio_setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height); +__attribute__ ((visibility ("hidden"))) void gio_setTitle(CFTypeRef windowRef, const char *title); +*/ +import "C" + +func init() { + // Darwin requires that UI operations happen on the main thread only. + runtime.LockOSThread() +} + +type window struct { + view C.CFTypeRef + window C.CFTypeRef + w Callbacks + stage system.Stage + displayLink *displayLink + cursor pointer.CursorName + + scale float32 + mode WindowMode +} + +// viewMap is the mapping from Cocoa NSViews to Go windows. +var viewMap = make(map[C.CFTypeRef]*window) + +var viewFactory func() C.CFTypeRef + +// launched is closed when applicationDidFinishLaunching is called. +var launched = make(chan struct{}) + +// nextTopLeft is the offset to use for the next window's call to +// cascadeTopLeftFromPoint. +var nextTopLeft C.NSPoint + +// mustView is like lookupView, except that it panics +// if the view isn't mapped. +func mustView(view C.CFTypeRef) *window { + w, ok := lookupView(view) + if !ok { + panic("no window for view") + } + return w +} + +func lookupView(view C.CFTypeRef) (*window, bool) { + w, exists := viewMap[view] + if !exists { + return nil, false + } + return w, true +} + +func deleteView(view C.CFTypeRef) { + delete(viewMap, view) +} + +func insertView(view C.CFTypeRef, w *window) { + viewMap[view] = w +} + +func (w *window) contextView() C.CFTypeRef { + return w.view +} + +func (w *window) ReadClipboard() { + runOnMain(func() { + content := nsstringToString(C.gio_readClipboard()) + w.w.Event(clipboard.Event{Text: content}) + }) +} + +func (w *window) WriteClipboard(s string) { + u16 := utf16.Encode([]rune(s)) + runOnMain(func() { + var chars *C.unichar + if len(u16) > 0 { + chars = (*C.unichar)(unsafe.Pointer(&u16[0])) + } + C.gio_writeClipboard(chars, C.NSUInteger(len(u16))) + }) +} + +func (w *window) Option(opts *Options) { + w.runOnMain(func() { + screenScale := float32(C.gio_getScreenBackingScale()) + cfg := configFor(screenScale) + val := func(v unit.Value) float32 { + return float32(cfg.Px(v)) / screenScale + } + if o := opts.Size; o != nil { + width := val(o.Width) + height := val(o.Height) + if width > 0 || height > 0 { + C.gio_setSize(w.window, C.CGFloat(width), C.CGFloat(height)) + } + } + if o := opts.MinSize; o != nil { + width := val(o.Width) + height := val(o.Height) + if width > 0 || height > 0 { + C.gio_setMinSize(w.window, C.CGFloat(width), C.CGFloat(height)) + } + } + if o := opts.MaxSize; o != nil { + width := val(o.Width) + height := val(o.Height) + if width > 0 || height > 0 { + C.gio_setMaxSize(w.window, C.CGFloat(width), C.CGFloat(height)) + } + } + if o := opts.Title; o != nil { + title := C.CString(*o) + defer C.free(unsafe.Pointer(title)) + C.gio_setTitle(w.window, title) + } + if o := opts.WindowMode; o != nil { + w.SetWindowMode(*o) + } + }) +} + +func (w *window) SetWindowMode(mode WindowMode) { + switch mode { + case w.mode: + case Windowed, Fullscreen: + C.gio_toggleFullScreen(w.window) + w.mode = mode + } +} + +func (w *window) SetCursor(name pointer.CursorName) { + w.cursor = windowSetCursor(w.cursor, name) +} + +func (w *window) ShowTextInput(show bool) {} + +func (w *window) SetAnimating(anim bool) { + if anim { + w.displayLink.Start() + } else { + w.displayLink.Stop() + } +} + +func (w *window) runOnMain(f func()) { + runOnMain(func() { + // Make sure the view is still valid. The window might've been closed + // during the switch to the main thread. + if w.view != 0 { + f() + } + }) +} + +func (w *window) Close() { + w.runOnMain(func() { + C.gio_close(w.window) + }) +} + +func (w *window) setStage(stage system.Stage) { + if stage == w.stage { + return + } + w.stage = stage + w.w.Event(system.StageEvent{Stage: stage}) +} + +//export gio_onKeys +func gio_onKeys(view C.CFTypeRef, cstr *C.char, ti C.double, mods C.NSUInteger, + keyDown C.bool) { + str := C.GoString(cstr) + kmods := convertMods(mods) + ks := key.Release + if keyDown { + ks = key.Press + } + w := mustView(view) + for _, k := range str { + if n, ok := convertKey(k); ok { + w.w.Event(key.Event{ + Name: n, + Modifiers: kmods, + State: ks, + }) + } + } +} + +//export gio_onText +func gio_onText(view C.CFTypeRef, cstr *C.char) { + str := C.GoString(cstr) + w := mustView(view) + w.w.Event(key.EditEvent{Text: str}) +} + +//export gio_onMouse +func gio_onMouse(view C.CFTypeRef, cdir C.int, cbtns C.NSUInteger, + x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) { + var typ pointer.Type + switch cdir { + case C.GIO_MOUSE_MOVE: + typ = pointer.Move + case C.GIO_MOUSE_UP: + typ = pointer.Release + case C.GIO_MOUSE_DOWN: + typ = pointer.Press + case C.GIO_MOUSE_SCROLL: + typ = pointer.Scroll + default: + panic("invalid direction") + } + var btns pointer.Buttons + if cbtns&(1<<0) != 0 { + btns |= pointer.ButtonPrimary + } + if cbtns&(1<<1) != 0 { + btns |= pointer.ButtonSecondary + } + if cbtns&(1<<2) != 0 { + btns |= pointer.ButtonTertiary + } + t := time.Duration(float64(ti)*float64(time.Second) + .5) + w := mustView(view) + xf, yf := float32(x)*w.scale, float32(y)*w.scale + dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale + w.w.Event(pointer.Event{ + Type: typ, + Source: pointer.Mouse, + Time: t, + Buttons: btns, + Position: f32.Point{X: xf, Y: yf}, + Scroll: f32.Point{X: dxf, Y: dyf}, + Modifiers: convertMods(mods), + }) +} + +//export gio_onDraw +func gio_onDraw(view C.CFTypeRef) { + w := mustView(view) + w.draw() +} + +//export gio_onFocus +func gio_onFocus(view C.CFTypeRef, focus C.int) { + w := mustView(view) + w.w.Event(key.FocusEvent{Focus: focus == 1}) + w.SetCursor(w.cursor) +} + +//export gio_onChangeScreen +func gio_onChangeScreen(view C.CFTypeRef, did uint64) { + w := mustView(view) + w.displayLink.SetDisplayID(did) +} + +func (w *window) draw() { + w.scale = float32(C.gio_getViewBackingScale(w.view)) + wf, hf := float32(C.gio_viewWidth(w.view)), float32(C.gio_viewHeight(w.view)) + if wf == 0 || hf == 0 { + return + } + width := int(wf*w.scale + .5) + height := int(hf*w.scale + .5) + cfg := configFor(w.scale) + w.setStage(system.StageRunning) + w.w.Event(FrameEvent{ + FrameEvent: system.FrameEvent{ + Now: time.Now(), + Size: image.Point{ + X: width, + Y: height, + }, + Metric: cfg, + }, + Sync: true, + }) +} + +func configFor(scale float32) unit.Metric { + return unit.Metric{ + PxPerDp: scale, + PxPerSp: scale, + } +} + +//export gio_onClose +func gio_onClose(view C.CFTypeRef) { + w := mustView(view) + w.displayLink.Close() + deleteView(view) + w.w.Event(system.DestroyEvent{}) + C.CFRelease(w.view) + w.view = 0 + C.CFRelease(w.window) + w.window = 0 +} + +//export gio_onHide +func gio_onHide(view C.CFTypeRef) { + w := mustView(view) + w.setStage(system.StagePaused) +} + +//export gio_onShow +func gio_onShow(view C.CFTypeRef) { + w := mustView(view) + w.setStage(system.StageRunning) +} + +//export gio_onAppHide +func gio_onAppHide() { + for _, w := range viewMap { + w.setStage(system.StagePaused) + } +} + +//export gio_onAppShow +func gio_onAppShow() { + for _, w := range viewMap { + w.setStage(system.StageRunning) + } +} + +//export gio_onFinishLaunching +func gio_onFinishLaunching() { + close(launched) +} + +func NewWindow(win Callbacks, opts *Options) error { + <-launched + errch := make(chan error) + runOnMain(func() { + w, err := newWindow(opts) + if err != nil { + errch <- err + return + } + errch <- nil + win.SetDriver(w) + w.w = win + w.window = C.gio_createWindow(w.view, nil, 0, 0, 0, 0, 0, 0) + w.Option(opts) + if nextTopLeft.x == 0 && nextTopLeft.y == 0 { + // cascadeTopLeftFromPoint treats (0, 0) as a no-op, + // and just returns the offset we need for the first window. + nextTopLeft = C.gio_cascadeTopLeftFromPoint(w.window, nextTopLeft) + } + nextTopLeft = C.gio_cascadeTopLeftFromPoint(w.window, nextTopLeft) + C.gio_makeKeyAndOrderFront(w.window) + }) + return <-errch +} + +func newWindow(opts *Options) (*window, error) { + view := viewFactory() + if view == 0 { + return nil, errors.New("CreateWindow: failed to create view") + } + scale := float32(C.gio_getViewBackingScale(view)) + w := &window{ + view: view, + scale: scale, + } + dl, err := NewDisplayLink(func() { + w.runOnMain(func() { + C.gio_setNeedsDisplay(w.view) + }) + }) + w.displayLink = dl + if err != nil { + C.CFRelease(view) + return nil, err + } + insertView(view, w) + return w, nil +} + +func Main() { + C.gio_main() +} + +func convertKey(k rune) (string, bool) { + var n string + switch k { + case 0x1b: + n = key.NameEscape + case C.NSLeftArrowFunctionKey: + n = key.NameLeftArrow + case C.NSRightArrowFunctionKey: + n = key.NameRightArrow + case C.NSUpArrowFunctionKey: + n = key.NameUpArrow + case C.NSDownArrowFunctionKey: + n = key.NameDownArrow + case 0xd: + n = key.NameReturn + case 0x3: + n = key.NameEnter + case C.NSHomeFunctionKey: + n = key.NameHome + case C.NSEndFunctionKey: + n = key.NameEnd + case 0x7f: + n = key.NameDeleteBackward + case C.NSDeleteFunctionKey: + n = key.NameDeleteForward + case C.NSPageUpFunctionKey: + n = key.NamePageUp + case C.NSPageDownFunctionKey: + n = key.NamePageDown + case C.NSF1FunctionKey: + n = "F1" + case C.NSF2FunctionKey: + n = "F2" + case C.NSF3FunctionKey: + n = "F3" + case C.NSF4FunctionKey: + n = "F4" + case C.NSF5FunctionKey: + n = "F5" + case C.NSF6FunctionKey: + n = "F6" + case C.NSF7FunctionKey: + n = "F7" + case C.NSF8FunctionKey: + n = "F8" + case C.NSF9FunctionKey: + n = "F9" + case C.NSF10FunctionKey: + n = "F10" + case C.NSF11FunctionKey: + n = "F11" + case C.NSF12FunctionKey: + n = "F12" + case 0x09, 0x19: + n = key.NameTab + case 0x20: + n = key.NameSpace + default: + k = unicode.ToUpper(k) + if !unicode.IsPrint(k) { + return "", false + } + n = string(k) + } + return n, true +} + +func convertMods(mods C.NSUInteger) key.Modifiers { + var kmods key.Modifiers + if mods&C.NSAlternateKeyMask != 0 { + kmods |= key.ModAlt + } + if mods&C.NSControlKeyMask != 0 { + kmods |= key.ModCtrl + } + if mods&C.NSCommandKeyMask != 0 { + kmods |= key.ModCommand + } + if mods&C.NSShiftKeyMask != 0 { + kmods |= key.ModShift + } + return kmods +} diff --git a/gio/app/internal/wm/os_macos.m b/gio/app/internal/wm/os_macos.m new file mode 100644 index 0000000..7980d53 --- /dev/null +++ b/gio/app/internal/wm/os_macos.m @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,!ios + +@import AppKit; + +#include "_cgo_export.h" + +@interface GioAppDelegate : NSObject +@end + +@interface GioWindowDelegate : NSObject +@end + +@implementation GioWindowDelegate +- (void)windowWillMiniaturize:(NSNotification *)notification { + NSWindow *window = (NSWindow *)[notification object]; + gio_onHide((__bridge CFTypeRef)window.contentView); +} +- (void)windowDidDeminiaturize:(NSNotification *)notification { + NSWindow *window = (NSWindow *)[notification object]; + gio_onShow((__bridge CFTypeRef)window.contentView); +} +- (void)windowDidChangeScreen:(NSNotification *)notification { + NSWindow *window = (NSWindow *)[notification object]; + CGDirectDisplayID dispID = [[[window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue]; + CFTypeRef view = (__bridge CFTypeRef)window.contentView; + gio_onChangeScreen(view, dispID); +} +- (void)windowDidBecomeKey:(NSNotification *)notification { + NSWindow *window = (NSWindow *)[notification object]; + gio_onFocus((__bridge CFTypeRef)window.contentView, 1); +} +- (void)windowDidResignKey:(NSNotification *)notification { + NSWindow *window = (NSWindow *)[notification object]; + gio_onFocus((__bridge CFTypeRef)window.contentView, 0); +} +- (void)windowWillClose:(NSNotification *)notification { + NSWindow *window = (NSWindow *)[notification object]; + window.delegate = nil; + gio_onClose((__bridge CFTypeRef)window.contentView); +} +@end + +// Delegates are weakly referenced from their peers. Nothing +// else holds a strong reference to our window delegate, so +// keep a single global reference instead. +static GioWindowDelegate *globalWindowDel; + +void gio_writeClipboard(unichar *chars, NSUInteger length) { + @autoreleasepool { + NSString *s = [NSString string]; + if (length > 0) { + s = [NSString stringWithCharacters:chars length:length]; + } + NSPasteboard *p = NSPasteboard.generalPasteboard; + [p declareTypes:@[NSPasteboardTypeString] owner:nil]; + [p setString:s forType:NSPasteboardTypeString]; + } +} + +CFTypeRef gio_readClipboard(void) { + @autoreleasepool { + NSPasteboard *p = NSPasteboard.generalPasteboard; + NSString *content = [p stringForType:NSPasteboardTypeString]; + return (__bridge_retained CFTypeRef)content; + } +} + +CGFloat gio_viewHeight(CFTypeRef viewRef) { + NSView *view = (__bridge NSView *)viewRef; + return [view bounds].size.height; +} + +CGFloat gio_viewWidth(CFTypeRef viewRef) { + NSView *view = (__bridge NSView *)viewRef; + return [view bounds].size.width; +} + +CGFloat gio_getScreenBackingScale(void) { + return [NSScreen.mainScreen backingScaleFactor]; +} + +CGFloat gio_getViewBackingScale(CFTypeRef viewRef) { + NSView *view = (__bridge NSView *)viewRef; + return [view.window backingScaleFactor]; +} + +void gio_hideCursor() { + @autoreleasepool { + [NSCursor hide]; + } +} + +void gio_showCursor() { + @autoreleasepool { + [NSCursor unhide]; + } +} + +void gio_setCursor(NSUInteger curID) { + @autoreleasepool { + switch (curID) { + case 1: + [NSCursor.arrowCursor set]; + break; + case 2: + [NSCursor.IBeamCursor set]; + break; + case 3: + [NSCursor.pointingHandCursor set]; + break; + case 4: + [NSCursor.crosshairCursor set]; + break; + case 5: + [NSCursor.resizeLeftRightCursor set]; + break; + case 6: + [NSCursor.resizeUpDownCursor set]; + break; + case 7: + [NSCursor.openHandCursor set]; + break; + default: + [NSCursor.arrowCursor set]; + break; + } + } +} + +static CVReturn displayLinkCallback(CVDisplayLinkRef dl, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) { + gio_onFrameCallback(dl); + return kCVReturnSuccess; +} + +CFTypeRef gio_createDisplayLink(void) { + CVDisplayLinkRef dl; + CVDisplayLinkCreateWithActiveCGDisplays(&dl); + CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, nil); + return dl; +} + +int gio_startDisplayLink(CFTypeRef dl) { + return CVDisplayLinkStart((CVDisplayLinkRef)dl); +} + +int gio_stopDisplayLink(CFTypeRef dl) { + return CVDisplayLinkStop((CVDisplayLinkRef)dl); +} + +void gio_releaseDisplayLink(CFTypeRef dl) { + CVDisplayLinkRelease((CVDisplayLinkRef)dl); +} + +void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did) { + CVDisplayLinkSetCurrentCGDisplay((CVDisplayLinkRef)dl, (CGDirectDisplayID)did); +} + +NSPoint gio_cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft) { + NSWindow *window = (__bridge NSWindow *)windowRef; + return [window cascadeTopLeftFromPoint:topLeft]; +} + +void gio_makeKeyAndOrderFront(CFTypeRef windowRef) { + NSWindow *window = (__bridge NSWindow *)windowRef; + [window makeKeyAndOrderFront:nil]; +} + +void gio_toggleFullScreen(CFTypeRef windowRef) { + NSWindow *window = (__bridge NSWindow *)windowRef; + [window toggleFullScreen:nil]; +} + +CFTypeRef gio_createWindow(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight) { + @autoreleasepool { + NSRect rect = NSMakeRect(0, 0, width, height); + NSUInteger styleMask = NSTitledWindowMask | + NSResizableWindowMask | + NSMiniaturizableWindowMask | + NSClosableWindowMask; + + NSWindow* window = [[NSWindow alloc] initWithContentRect:rect + styleMask:styleMask + backing:NSBackingStoreBuffered + defer:NO]; + if (minWidth > 0 || minHeight > 0) { + window.contentMinSize = NSMakeSize(minWidth, minHeight); + } + if (maxWidth > 0 || maxHeight > 0) { + window.contentMaxSize = NSMakeSize(maxWidth, maxHeight); + } + [window setAcceptsMouseMovedEvents:YES]; + if (title != nil) { + window.title = [NSString stringWithUTF8String: title]; + } + NSView *view = (__bridge NSView *)viewRef; + [window setContentView:view]; + [window makeFirstResponder:view]; + window.releasedWhenClosed = NO; + window.delegate = globalWindowDel; + return (__bridge_retained CFTypeRef)window; + } +} + +void gio_close(CFTypeRef windowRef) { + NSWindow* window = (__bridge NSWindow *)windowRef; + [window performClose:nil]; +} + +void gio_setSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { + NSWindow* window = (__bridge NSWindow *)windowRef; + NSSize size = NSMakeSize(width, height); + [window setContentSize:size]; +} + +void gio_setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { + NSWindow* window = (__bridge NSWindow *)windowRef; + window.contentMinSize = NSMakeSize(width, height); +} + +void gio_setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { + NSWindow* window = (__bridge NSWindow *)windowRef; + window.contentMaxSize = NSMakeSize(width, height); +} + +void gio_setTitle(CFTypeRef windowRef, const char *title) { + NSWindow* window = (__bridge NSWindow *)windowRef; + window.title = [NSString stringWithUTF8String: title]; +} + +@implementation GioAppDelegate +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; + gio_onFinishLaunching(); +} +- (void)applicationDidHide:(NSNotification *)aNotification { + gio_onAppHide(); +} +- (void)applicationWillUnhide:(NSNotification *)notification { + gio_onAppShow(); +} +@end + +void gio_main() { + @autoreleasepool { + [NSApplication sharedApplication]; + GioAppDelegate *del = [[GioAppDelegate alloc] init]; + [NSApp setDelegate:del]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + + NSMenuItem *mainMenu = [NSMenuItem new]; + + NSMenu *menu = [NSMenu new]; + NSMenuItem *hideMenuItem = [[NSMenuItem alloc] initWithTitle:@"Hide" + action:@selector(hide:) + keyEquivalent:@"h"]; + [menu addItem:hideMenuItem]; + NSMenuItem *quitMenuItem = [[NSMenuItem alloc] initWithTitle:@"Quit" + action:@selector(terminate:) + keyEquivalent:@"q"]; + [menu addItem:quitMenuItem]; + [mainMenu setSubmenu:menu]; + NSMenu *menuBar = [NSMenu new]; + [menuBar addItem:mainMenu]; + [NSApp setMainMenu:menuBar]; + + globalWindowDel = [[GioWindowDelegate alloc] init]; + + [NSApp run]; + } +} diff --git a/gio/app/internal/wm/os_unix.go b/gio/app/internal/wm/os_unix.go new file mode 100644 index 0000000..8143100 --- /dev/null +++ b/gio/app/internal/wm/os_unix.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build linux,!android freebsd openbsd + +package wm + +import ( + "errors" +) + +func Main() { + select {} +} + +type windowDriver func(Callbacks, *Options) error + +// Instead of creating files with build tags for each combination of wayland +/- x11 +// let each driver initialize these variables with their own version of createWindow. +var wlDriver, x11Driver windowDriver + +func NewWindow(window Callbacks, opts *Options) error { + var errFirst error + for _, d := range []windowDriver{x11Driver, wlDriver} { + if d == nil { + continue + } + err := d(window, opts) + if err == nil { + return nil + } + if errFirst == nil { + errFirst = err + } + } + if errFirst != nil { + return errFirst + } + return errors.New("app: no window driver available") +} diff --git a/gio/app/internal/wm/os_wayland.c b/gio/app/internal/wm/os_wayland.c new file mode 100644 index 0000000..5c1c075 --- /dev/null +++ b/gio/app/internal/wm/os_wayland.c @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build linux,!android,!nowayland freebsd + +#include +#include "wayland_xdg_shell.h" +#include "wayland_text_input.h" +#include "_cgo_export.h" + +const struct wl_registry_listener gio_registry_listener = { + // Cast away const parameter. + .global = (void (*)(void *, struct wl_registry *, uint32_t, const char *, uint32_t))gio_onRegistryGlobal, + .global_remove = gio_onRegistryGlobalRemove +}; + +const struct wl_surface_listener gio_surface_listener = { + .enter = gio_onSurfaceEnter, + .leave = gio_onSurfaceLeave, +}; + +const struct xdg_surface_listener gio_xdg_surface_listener = { + .configure = gio_onXdgSurfaceConfigure, +}; + +const struct xdg_toplevel_listener gio_xdg_toplevel_listener = { + .configure = gio_onToplevelConfigure, + .close = gio_onToplevelClose, +}; + +static void xdg_wm_base_handle_ping(void *data, struct xdg_wm_base *wm, uint32_t serial) { + xdg_wm_base_pong(wm, serial); +} + +const struct xdg_wm_base_listener gio_xdg_wm_base_listener = { + .ping = xdg_wm_base_handle_ping, +}; + +const struct wl_callback_listener gio_callback_listener = { + .done = gio_onFrameDone, +}; + +const struct wl_output_listener gio_output_listener = { + // Cast away const parameter. + .geometry = (void (*)(void *, struct wl_output *, int32_t, int32_t, int32_t, int32_t, int32_t, const char *, const char *, int32_t))gio_onOutputGeometry, + .mode = gio_onOutputMode, + .done = gio_onOutputDone, + .scale = gio_onOutputScale, +}; + +const struct wl_seat_listener gio_seat_listener = { + .capabilities = gio_onSeatCapabilities, + // Cast away const parameter. + .name = (void (*)(void *, struct wl_seat *, const char *))gio_onSeatName, +}; + +const struct wl_pointer_listener gio_pointer_listener = { + .enter = gio_onPointerEnter, + .leave = gio_onPointerLeave, + .motion = gio_onPointerMotion, + .button = gio_onPointerButton, + .axis = gio_onPointerAxis, + .frame = gio_onPointerFrame, + .axis_source = gio_onPointerAxisSource, + .axis_stop = gio_onPointerAxisStop, + .axis_discrete = gio_onPointerAxisDiscrete, +}; + +const struct wl_touch_listener gio_touch_listener = { + .down = gio_onTouchDown, + .up = gio_onTouchUp, + .motion = gio_onTouchMotion, + .frame = gio_onTouchFrame, + .cancel = gio_onTouchCancel, +}; + +const struct wl_keyboard_listener gio_keyboard_listener = { + .keymap = gio_onKeyboardKeymap, + .enter = gio_onKeyboardEnter, + .leave = gio_onKeyboardLeave, + .key = gio_onKeyboardKey, + .modifiers = gio_onKeyboardModifiers, + .repeat_info = gio_onKeyboardRepeatInfo +}; + +const struct zwp_text_input_v3_listener gio_zwp_text_input_v3_listener = { + .enter = gio_onTextInputEnter, + .leave = gio_onTextInputLeave, + // Cast away const parameter. + .preedit_string = (void (*)(void *, struct zwp_text_input_v3 *, const char *, int32_t, int32_t))gio_onTextInputPreeditString, + .commit_string = (void (*)(void *, struct zwp_text_input_v3 *, const char *))gio_onTextInputCommitString, + .delete_surrounding_text = gio_onTextInputDeleteSurroundingText, + .done = gio_onTextInputDone +}; + +const struct wl_data_device_listener gio_data_device_listener = { + .data_offer = gio_onDataDeviceOffer, + .enter = gio_onDataDeviceEnter, + .leave = gio_onDataDeviceLeave, + .motion = gio_onDataDeviceMotion, + .drop = gio_onDataDeviceDrop, + .selection = gio_onDataDeviceSelection, +}; + +const struct wl_data_offer_listener gio_data_offer_listener = { + .offer = (void (*)(void *, struct wl_data_offer *, const char *))gio_onDataOfferOffer, + .source_actions = gio_onDataOfferSourceActions, + .action = gio_onDataOfferAction, +}; + +const struct wl_data_source_listener gio_data_source_listener = { + .target = (void (*)(void *, struct wl_data_source *, const char *))gio_onDataSourceTarget, + .send = (void (*)(void *, struct wl_data_source *, const char *, int32_t))gio_onDataSourceSend, + .cancelled = gio_onDataSourceCancelled, + .dnd_drop_performed = gio_onDataSourceDNDDropPerformed, + .dnd_finished = gio_onDataSourceDNDFinished, + .action = gio_onDataSourceAction, +}; diff --git a/gio/app/internal/wm/os_wayland.go b/gio/app/internal/wm/os_wayland.go new file mode 100644 index 0000000..f59bc1e --- /dev/null +++ b/gio/app/internal/wm/os_wayland.go @@ -0,0 +1,1694 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build (linux && !android && !nowayland) || freebsd +// +build linux,!android,!nowayland freebsd + +package wm + +import ( + "bytes" + "errors" + "fmt" + "image" + "io" + "io/ioutil" + "math" + "os" + "os/exec" + "strconv" + "sync" + "time" + "unsafe" + + syscall "golang.org/x/sys/unix" + + "realy.lol/gio/app/internal/xkb" + "realy.lol/gio/f32" + "realy.lol/gio/internal/fling" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/unit" +) + +// Use wayland-scanner to generate glue code for the xdg-shell and xdg-decoration extensions. +//go:generate wayland-scanner client-header /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml wayland_xdg_shell.h +//go:generate wayland-scanner private-code /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml wayland_xdg_shell.c + +//go:generate wayland-scanner client-header /usr/share/wayland-protocols/unstable/text-input/text-input-unstable-v3.xml wayland_text_input.h +//go:generate wayland-scanner private-code /usr/share/wayland-protocols/unstable/text-input/text-input-unstable-v3.xml wayland_text_input.c + +//go:generate wayland-scanner client-header /usr/share/wayland-protocols/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml wayland_xdg_decoration.h +//go:generate wayland-scanner private-code /usr/share/wayland-protocols/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml wayland_xdg_decoration.c + +//go:generate sed -i "1s;^;// +build linux,!android,!nowayland freebsd\\n\\n;" wayland_xdg_shell.c +//go:generate sed -i "1s;^;// +build linux,!android,!nowayland freebsd\\n\\n;" wayland_xdg_decoration.c +//go:generate sed -i "1s;^;// +build linux,!android,!nowayland freebsd\\n\\n;" wayland_text_input.c + +/* +#cgo linux pkg-config: wayland-client wayland-cursor +#cgo freebsd openbsd LDFLAGS: -lwayland-client -lwayland-cursor +#cgo freebsd CFLAGS: -I/usr/local/include +#cgo freebsd LDFLAGS: -L/usr/local/lib + +#include +#include +#include +#include "wayland_text_input.h" +#include "wayland_xdg_shell.h" +#include "wayland_xdg_decoration.h" + +extern const struct wl_registry_listener gio_registry_listener; +extern const struct wl_surface_listener gio_surface_listener; +extern const struct xdg_surface_listener gio_xdg_surface_listener; +extern const struct xdg_toplevel_listener gio_xdg_toplevel_listener; +extern const struct xdg_wm_base_listener gio_xdg_wm_base_listener; +extern const struct wl_callback_listener gio_callback_listener; +extern const struct wl_output_listener gio_output_listener; +extern const struct wl_seat_listener gio_seat_listener; +extern const struct wl_pointer_listener gio_pointer_listener; +extern const struct wl_touch_listener gio_touch_listener; +extern const struct wl_keyboard_listener gio_keyboard_listener; +extern const struct zwp_text_input_v3_listener gio_zwp_text_input_v3_listener; +extern const struct wl_data_device_listener gio_data_device_listener; +extern const struct wl_data_offer_listener gio_data_offer_listener; +extern const struct wl_data_source_listener gio_data_source_listener; +*/ +import "C" + +type wlDisplay struct { + disp *C.struct_wl_display + reg *C.struct_wl_registry + compositor *C.struct_wl_compositor + wm *C.struct_xdg_wm_base + imm *C.struct_zwp_text_input_manager_v3 + shm *C.struct_wl_shm + dataDeviceManager *C.struct_wl_data_device_manager + decor *C.struct_zxdg_decoration_manager_v1 + seat *wlSeat + xkb *xkb.Context + outputMap map[C.uint32_t]*C.struct_wl_output + outputConfig map[*C.struct_wl_output]*wlOutput + + // Notification pipe fds. + notify struct { + read, write int + } + + repeat repeatState +} + +type wlSeat struct { + disp *wlDisplay + seat *C.struct_wl_seat + name C.uint32_t + pointer *C.struct_wl_pointer + touch *C.struct_wl_touch + keyboard *C.struct_wl_keyboard + im *C.struct_zwp_text_input_v3 + + // The most recent input serial. + serial C.uint32_t + + pointerFocus *window + keyboardFocus *window + touchFoci map[C.int32_t]*window + + // Clipboard support. + dataDev *C.struct_wl_data_device + // offers is a map from active wl_data_offers to + // the list of mime types they support. + offers map[*C.struct_wl_data_offer][]string + // clipboard is the wl_data_offer for the clipboard. + clipboard *C.struct_wl_data_offer + // mimeType is the chosen mime type of clipboard. + mimeType string + // source represents the clipboard content of the most recent + // clipboard write, if any. + source *C.struct_wl_data_source + // content is the data belonging to source. + content []byte +} + +type repeatState struct { + rate int + delay time.Duration + + key uint32 + win Callbacks + stopC chan struct{} + + start time.Duration + last time.Duration + mu sync.Mutex + now time.Duration +} + +type window struct { + w Callbacks + disp *wlDisplay + surf *C.struct_wl_surface + wmSurf *C.struct_xdg_surface + topLvl *C.struct_xdg_toplevel + decor *C.struct_zxdg_toplevel_decoration_v1 + ppdp, ppsp float32 + scroll struct { + time time.Duration + steps image.Point + dist f32.Point + } + pointerBtns pointer.Buttons + lastPos f32.Point + lastTouch f32.Point + + cursor struct { + theme *C.struct_wl_cursor_theme + cursor *C.struct_wl_cursor + surf *C.struct_wl_surface + } + + fling struct { + yExtrapolation fling.Extrapolation + xExtrapolation fling.Extrapolation + anim fling.Animation + start bool + dir f32.Point + } + + stage system.Stage + dead bool + lastFrameCallback *C.struct_wl_callback + + mu sync.Mutex + animating bool + opts *Options + needAck bool + // The most recent configure serial waiting to be ack'ed. + serial C.uint32_t + width int + height int + newScale bool + scale int + // readClipboard tracks whether a ClipboardEvent is requested. + readClipboard bool + // writeClipboard is set whenever a clipboard write is requested. + writeClipboard *string +} + +type poller struct { + pollfds [2]syscall.PollFd + // buf is scratch space for draining the notification pipe. + buf [100]byte +} + +type wlOutput struct { + width int + height int + physWidth int + physHeight int + transform C.int32_t + scale int + windows []*window +} + +// callbackMap maps Wayland native handles to corresponding Go +// references. It is necessary because the the Wayland client API +// forces the use of callbacks and storing pointers to Go values +// in C is forbidden. +var callbackMap sync.Map + +// clipboardMimeTypes is a list of supported clipboard mime types, in +// order of preference. +var clipboardMimeTypes = []string{"text/plain;charset=utf8", "UTF8_STRING", + "text/plain", "TEXT", "STRING"} + +func init() { + wlDriver = newWLWindow +} + +func newWLWindow(window Callbacks, opts *Options) error { + d, err := newWLDisplay() + if err != nil { + return err + } + w, err := d.createNativeWindow(opts) + if err != nil { + d.destroy() + return err + } + w.w = window + go func() { + defer d.destroy() + defer w.destroy() + w.w.SetDriver(w) + if err := w.loop(); err != nil { + panic(err) + } + }() + return nil +} + +func (d *wlDisplay) writeClipboard(content []byte) error { + s := d.seat + if s == nil { + return nil + } + // Clear old offer. + if s.source != nil { + C.wl_data_source_destroy(s.source) + s.source = nil + s.content = nil + } + if d.dataDeviceManager == nil || s.dataDev == nil { + return nil + } + s.content = content + s.source = C.wl_data_device_manager_create_data_source(d.dataDeviceManager) + C.wl_data_source_add_listener(s.source, &C.gio_data_source_listener, + unsafe.Pointer(s.seat)) + for _, mime := range clipboardMimeTypes { + C.wl_data_source_offer(s.source, C.CString(mime)) + } + C.wl_data_device_set_selection(s.dataDev, s.source, s.serial) + return nil +} + +func (d *wlDisplay) readClipboard() (io.ReadCloser, error) { + s := d.seat + if s == nil { + return nil, nil + } + if s.clipboard == nil { + return nil, nil + } + r, w, err := os.Pipe() + if err != nil { + return nil, err + } + // wl_data_offer_receive performs and implicit dup(2) of the write end + // of the pipe. Close our version. + defer w.Close() + cmimeType := C.CString(s.mimeType) + defer C.free(unsafe.Pointer(cmimeType)) + C.wl_data_offer_receive(s.clipboard, cmimeType, C.int(w.Fd())) + return r, nil +} + +func (d *wlDisplay) createNativeWindow(opts *Options) (*window, error) { + if d.compositor == nil { + return nil, errors.New("wayland: no compositor available") + } + if d.wm == nil { + return nil, errors.New("wayland: no xdg_wm_base available") + } + if d.shm == nil { + return nil, errors.New("wayland: no wl_shm available") + } + if len(d.outputMap) == 0 { + return nil, errors.New("wayland: no outputs available") + } + var scale int + for _, conf := range d.outputConfig { + if s := conf.scale; s > scale { + scale = s + } + } + ppdp := detectUIScale() + + w := &window{ + disp: d, + scale: scale, + newScale: scale != 1, + ppdp: ppdp, + ppsp: ppdp, + } + w.surf = C.wl_compositor_create_surface(d.compositor) + if w.surf == nil { + w.destroy() + return nil, errors.New("wayland: wl_compositor_create_surface failed") + } + callbackStore(unsafe.Pointer(w.surf), w) + w.wmSurf = C.xdg_wm_base_get_xdg_surface(d.wm, w.surf) + if w.wmSurf == nil { + w.destroy() + return nil, errors.New("wayland: xdg_wm_base_get_xdg_surface failed") + } + w.topLvl = C.xdg_surface_get_toplevel(w.wmSurf) + if w.topLvl == nil { + w.destroy() + return nil, errors.New("wayland: xdg_surface_get_toplevel failed") + } + w.cursor.theme = C.wl_cursor_theme_load(nil, 32, d.shm) + if w.cursor.theme == nil { + w.destroy() + return nil, errors.New("wayland: wl_cursor_theme_load failed") + } + cname := C.CString("left_ptr") + defer C.free(unsafe.Pointer(cname)) + w.cursor.cursor = C.wl_cursor_theme_get_cursor(w.cursor.theme, cname) + if w.cursor.cursor == nil { + w.destroy() + return nil, errors.New("wayland: wl_cursor_theme_get_cursor failed") + } + w.cursor.surf = C.wl_compositor_create_surface(d.compositor) + if w.cursor.surf == nil { + w.destroy() + return nil, errors.New("wayland: wl_compositor_create_surface failed") + } + C.xdg_wm_base_add_listener(d.wm, &C.gio_xdg_wm_base_listener, + unsafe.Pointer(w.surf)) + C.wl_surface_add_listener(w.surf, &C.gio_surface_listener, + unsafe.Pointer(w.surf)) + C.xdg_surface_add_listener(w.wmSurf, &C.gio_xdg_surface_listener, + unsafe.Pointer(w.surf)) + C.xdg_toplevel_add_listener(w.topLvl, &C.gio_xdg_toplevel_listener, + unsafe.Pointer(w.surf)) + + w.setOptions(opts) + + if d.decor != nil { + // Request server side decorations. + w.decor = C.zxdg_decoration_manager_v1_get_toplevel_decoration(d.decor, + w.topLvl) + C.zxdg_toplevel_decoration_v1_set_mode(w.decor, + C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE) + } + w.updateOpaqueRegion() + C.wl_surface_commit(w.surf) + return w, nil +} + +func callbackDelete(k unsafe.Pointer) { + callbackMap.Delete(k) +} + +func callbackStore(k unsafe.Pointer, v interface{}) { + callbackMap.Store(k, v) +} + +func callbackLoad(k unsafe.Pointer) interface{} { + v, exists := callbackMap.Load(k) + if !exists { + panic("missing callback entry") + } + return v +} + +//export gio_onSeatCapabilities +func gio_onSeatCapabilities(data unsafe.Pointer, seat *C.struct_wl_seat, + caps C.uint32_t) { + s := callbackLoad(data).(*wlSeat) + s.updateCaps(caps) +} + +// flushOffers remove all wl_data_offers that isn't the clipboard +// content. +func (s *wlSeat) flushOffers() { + for o := range s.offers { + if o == s.clipboard { + continue + } + // We're only interested in clipboard offers. + delete(s.offers, o) + callbackDelete(unsafe.Pointer(o)) + C.wl_data_offer_destroy(o) + } +} + +func (s *wlSeat) destroy() { + if s.source != nil { + C.wl_data_source_destroy(s.source) + s.source = nil + } + if s.im != nil { + C.zwp_text_input_v3_destroy(s.im) + s.im = nil + } + if s.pointer != nil { + C.wl_pointer_release(s.pointer) + } + if s.touch != nil { + C.wl_touch_release(s.touch) + } + if s.keyboard != nil { + C.wl_keyboard_release(s.keyboard) + } + s.clipboard = nil + s.flushOffers() + if s.dataDev != nil { + C.wl_data_device_release(s.dataDev) + } + if s.seat != nil { + callbackDelete(unsafe.Pointer(s.seat)) + C.wl_seat_release(s.seat) + } +} + +func (s *wlSeat) updateCaps(caps C.uint32_t) { + if s.im == nil && s.disp.imm != nil { + s.im = C.zwp_text_input_manager_v3_get_text_input(s.disp.imm, s.seat) + C.zwp_text_input_v3_add_listener(s.im, + &C.gio_zwp_text_input_v3_listener, unsafe.Pointer(s.seat)) + } + switch { + case s.pointer == nil && caps&C.WL_SEAT_CAPABILITY_POINTER != 0: + s.pointer = C.wl_seat_get_pointer(s.seat) + C.wl_pointer_add_listener(s.pointer, &C.gio_pointer_listener, + unsafe.Pointer(s.seat)) + case s.pointer != nil && caps&C.WL_SEAT_CAPABILITY_POINTER == 0: + C.wl_pointer_release(s.pointer) + s.pointer = nil + } + switch { + case s.touch == nil && caps&C.WL_SEAT_CAPABILITY_TOUCH != 0: + s.touch = C.wl_seat_get_touch(s.seat) + C.wl_touch_add_listener(s.touch, &C.gio_touch_listener, + unsafe.Pointer(s.seat)) + case s.touch != nil && caps&C.WL_SEAT_CAPABILITY_TOUCH == 0: + C.wl_touch_release(s.touch) + s.touch = nil + } + switch { + case s.keyboard == nil && caps&C.WL_SEAT_CAPABILITY_KEYBOARD != 0: + s.keyboard = C.wl_seat_get_keyboard(s.seat) + C.wl_keyboard_add_listener(s.keyboard, &C.gio_keyboard_listener, + unsafe.Pointer(s.seat)) + case s.keyboard != nil && caps&C.WL_SEAT_CAPABILITY_KEYBOARD == 0: + C.wl_keyboard_release(s.keyboard) + s.keyboard = nil + } +} + +//export gio_onSeatName +func gio_onSeatName(data unsafe.Pointer, seat *C.struct_wl_seat, name *C.char) { +} + +//export gio_onXdgSurfaceConfigure +func gio_onXdgSurfaceConfigure(data unsafe.Pointer, + wmSurf *C.struct_xdg_surface, serial C.uint32_t) { + w := callbackLoad(data).(*window) + w.mu.Lock() + w.serial = serial + w.needAck = true + w.mu.Unlock() + w.setStage(system.StageRunning) + w.draw(true) +} + +//export gio_onToplevelClose +func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) { + w := callbackLoad(data).(*window) + w.dead = true +} + +//export gio_onToplevelConfigure +func gio_onToplevelConfigure(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel, + width, height C.int32_t, states *C.struct_wl_array) { + w := callbackLoad(data).(*window) + if width != 0 && height != 0 { + w.mu.Lock() + defer w.mu.Unlock() + w.width = int(width) + w.height = int(height) + w.updateOpaqueRegion() + } +} + +//export gio_onOutputMode +func gio_onOutputMode(data unsafe.Pointer, output *C.struct_wl_output, + flags C.uint32_t, width, height, refresh C.int32_t) { + if flags&C.WL_OUTPUT_MODE_CURRENT == 0 { + return + } + d := callbackLoad(data).(*wlDisplay) + c := d.outputConfig[output] + c.width = int(width) + c.height = int(height) +} + +//export gio_onOutputGeometry +func gio_onOutputGeometry(data unsafe.Pointer, output *C.struct_wl_output, + x, y, physWidth, physHeight, subpixel C.int32_t, make, model *C.char, + transform C.int32_t) { + d := callbackLoad(data).(*wlDisplay) + c := d.outputConfig[output] + c.transform = transform + c.physWidth = int(physWidth) + c.physHeight = int(physHeight) +} + +//export gio_onOutputScale +func gio_onOutputScale(data unsafe.Pointer, output *C.struct_wl_output, + scale C.int32_t) { + d := callbackLoad(data).(*wlDisplay) + c := d.outputConfig[output] + c.scale = int(scale) +} + +//export gio_onOutputDone +func gio_onOutputDone(data unsafe.Pointer, output *C.struct_wl_output) { + d := callbackLoad(data).(*wlDisplay) + conf := d.outputConfig[output] + for _, w := range conf.windows { + w.draw(true) + } +} + +//export gio_onSurfaceEnter +func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, + output *C.struct_wl_output) { + w := callbackLoad(data).(*window) + conf := w.disp.outputConfig[output] + var found bool + for _, w2 := range conf.windows { + if w2 == w { + found = true + break + } + } + if !found { + conf.windows = append(conf.windows, w) + } + w.updateOutputs() +} + +//export gio_onSurfaceLeave +func gio_onSurfaceLeave(data unsafe.Pointer, surf *C.struct_wl_surface, + output *C.struct_wl_output) { + w := callbackLoad(data).(*window) + conf := w.disp.outputConfig[output] + for i, w2 := range conf.windows { + if w2 == w { + conf.windows = append(conf.windows[:i], conf.windows[i+1:]...) + break + } + } + w.updateOutputs() +} + +//export gio_onRegistryGlobal +func gio_onRegistryGlobal(data unsafe.Pointer, reg *C.struct_wl_registry, + name C.uint32_t, cintf *C.char, version C.uint32_t) { + d := callbackLoad(data).(*wlDisplay) + switch C.GoString(cintf) { + case "wl_compositor": + d.compositor = (*C.struct_wl_compositor)(C.wl_registry_bind(reg, name, + &C.wl_compositor_interface, 3)) + case "wl_output": + output := (*C.struct_wl_output)(C.wl_registry_bind(reg, name, + &C.wl_output_interface, 2)) + C.wl_output_add_listener(output, &C.gio_output_listener, + unsafe.Pointer(d.disp)) + d.outputMap[name] = output + d.outputConfig[output] = new(wlOutput) + case "wl_seat": + if d.seat != nil { + break + } + s := (*C.struct_wl_seat)(C.wl_registry_bind(reg, name, + &C.wl_seat_interface, 5)) + if s == nil { + // No support for v5 protocol. + break + } + d.seat = &wlSeat{ + disp: d, + name: name, + seat: s, + offers: make(map[*C.struct_wl_data_offer][]string), + touchFoci: make(map[C.int32_t]*window), + } + callbackStore(unsafe.Pointer(s), d.seat) + C.wl_seat_add_listener(s, &C.gio_seat_listener, unsafe.Pointer(s)) + if d.dataDeviceManager == nil { + break + } + d.seat.dataDev = C.wl_data_device_manager_get_data_device(d.dataDeviceManager, + s) + if d.seat.dataDev == nil { + break + } + callbackStore(unsafe.Pointer(d.seat.dataDev), d.seat) + C.wl_data_device_add_listener(d.seat.dataDev, + &C.gio_data_device_listener, unsafe.Pointer(d.seat.dataDev)) + case "wl_shm": + d.shm = (*C.struct_wl_shm)(C.wl_registry_bind(reg, name, + &C.wl_shm_interface, 1)) + case "xdg_wm_base": + d.wm = (*C.struct_xdg_wm_base)(C.wl_registry_bind(reg, name, + &C.xdg_wm_base_interface, 1)) + case "zxdg_decoration_manager_v1": + d.decor = (*C.struct_zxdg_decoration_manager_v1)(C.wl_registry_bind(reg, + name, &C.zxdg_decoration_manager_v1_interface, 1)) + // TODO: Implement and test text-input support. + /*case "zwp_text_input_manager_v3": + d.imm = (*C.struct_zwp_text_input_manager_v3)(C.wl_registry_bind(reg, name, &C.zwp_text_input_manager_v3_interface, 1))*/ + case "wl_data_device_manager": + d.dataDeviceManager = (*C.struct_wl_data_device_manager)(C.wl_registry_bind(reg, + name, &C.wl_data_device_manager_interface, 3)) + } +} + +//export gio_onDataOfferOffer +func gio_onDataOfferOffer(data unsafe.Pointer, offer *C.struct_wl_data_offer, + mime *C.char) { + s := callbackLoad(data).(*wlSeat) + s.offers[offer] = append(s.offers[offer], C.GoString(mime)) +} + +//export gio_onDataOfferSourceActions +func gio_onDataOfferSourceActions(data unsafe.Pointer, + offer *C.struct_wl_data_offer, acts C.uint32_t) { +} + +//export gio_onDataOfferAction +func gio_onDataOfferAction(data unsafe.Pointer, offer *C.struct_wl_data_offer, + act C.uint32_t) { +} + +//export gio_onDataDeviceOffer +func gio_onDataDeviceOffer(data unsafe.Pointer, + dataDev *C.struct_wl_data_device, id *C.struct_wl_data_offer) { + s := callbackLoad(data).(*wlSeat) + callbackStore(unsafe.Pointer(id), s) + C.wl_data_offer_add_listener(id, &C.gio_data_offer_listener, + unsafe.Pointer(id)) + s.offers[id] = nil +} + +//export gio_onDataDeviceEnter +func gio_onDataDeviceEnter(data unsafe.Pointer, + dataDev *C.struct_wl_data_device, serial C.uint32_t, + surf *C.struct_wl_surface, x, y C.wl_fixed_t, id *C.struct_wl_data_offer) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + s.flushOffers() +} + +//export gio_onDataDeviceLeave +func gio_onDataDeviceLeave(data unsafe.Pointer, + dataDev *C.struct_wl_data_device) { +} + +//export gio_onDataDeviceMotion +func gio_onDataDeviceMotion(data unsafe.Pointer, + dataDev *C.struct_wl_data_device, t C.uint32_t, x, y C.wl_fixed_t) { +} + +//export gio_onDataDeviceDrop +func gio_onDataDeviceDrop(data unsafe.Pointer, + dataDev *C.struct_wl_data_device) { +} + +//export gio_onDataDeviceSelection +func gio_onDataDeviceSelection(data unsafe.Pointer, + dataDev *C.struct_wl_data_device, id *C.struct_wl_data_offer) { + s := callbackLoad(data).(*wlSeat) + defer s.flushOffers() + s.clipboard = nil +loop: + for _, want := range clipboardMimeTypes { + for _, got := range s.offers[id] { + if want != got { + continue + } + s.clipboard = id + s.mimeType = got + break loop + } + } +} + +//export gio_onRegistryGlobalRemove +func gio_onRegistryGlobalRemove(data unsafe.Pointer, reg *C.struct_wl_registry, + name C.uint32_t) { + d := callbackLoad(data).(*wlDisplay) + if s := d.seat; s != nil && name == s.name { + s.destroy() + d.seat = nil + } + if output, exists := d.outputMap[name]; exists { + C.wl_output_destroy(output) + delete(d.outputMap, name) + delete(d.outputConfig, output) + } +} + +//export gio_onTouchDown +func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, + serial, t C.uint32_t, surf *C.struct_wl_surface, id C.int32_t, + x, y C.wl_fixed_t) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + w := callbackLoad(unsafe.Pointer(surf)).(*window) + s.touchFoci[id] = w + w.lastTouch = f32.Point{ + X: fromFixed(x) * float32(w.scale), + Y: fromFixed(y) * float32(w.scale), + } + w.w.Event(pointer.Event{ + Type: pointer.Press, + Source: pointer.Touch, + Position: w.lastTouch, + PointerID: pointer.ID(id), + Time: time.Duration(t) * time.Millisecond, + Modifiers: w.disp.xkb.Modifiers(), + }) +} + +//export gio_onTouchUp +func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, + serial, t C.uint32_t, id C.int32_t) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + w := s.touchFoci[id] + delete(s.touchFoci, id) + w.w.Event(pointer.Event{ + Type: pointer.Release, + Source: pointer.Touch, + Position: w.lastTouch, + PointerID: pointer.ID(id), + Time: time.Duration(t) * time.Millisecond, + Modifiers: w.disp.xkb.Modifiers(), + }) +} + +//export gio_onTouchMotion +func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, + t C.uint32_t, id C.int32_t, x, y C.wl_fixed_t) { + s := callbackLoad(data).(*wlSeat) + w := s.touchFoci[id] + w.lastTouch = f32.Point{ + X: fromFixed(x) * float32(w.scale), + Y: fromFixed(y) * float32(w.scale), + } + w.w.Event(pointer.Event{ + Type: pointer.Move, + Position: w.lastTouch, + Source: pointer.Touch, + PointerID: pointer.ID(id), + Time: time.Duration(t) * time.Millisecond, + Modifiers: w.disp.xkb.Modifiers(), + }) +} + +//export gio_onTouchFrame +func gio_onTouchFrame(data unsafe.Pointer, touch *C.struct_wl_touch) { +} + +//export gio_onTouchCancel +func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) { + s := callbackLoad(data).(*wlSeat) + for id, w := range s.touchFoci { + delete(s.touchFoci, id) + w.w.Event(pointer.Event{ + Type: pointer.Cancel, + Source: pointer.Touch, + }) + } +} + +//export gio_onPointerEnter +func gio_onPointerEnter(data unsafe.Pointer, pointer *C.struct_wl_pointer, + serial C.uint32_t, surf *C.struct_wl_surface, x, y C.wl_fixed_t) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + w := callbackLoad(unsafe.Pointer(surf)).(*window) + s.pointerFocus = w + w.setCursor(pointer, serial) + w.lastPos = f32.Point{X: fromFixed(x), Y: fromFixed(y)} +} + +//export gio_onPointerLeave +func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, + serial C.uint32_t, surface *C.struct_wl_surface) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial +} + +//export gio_onPointerMotion +func gio_onPointerMotion(data unsafe.Pointer, p *C.struct_wl_pointer, + t C.uint32_t, x, y C.wl_fixed_t) { + s := callbackLoad(data).(*wlSeat) + w := s.pointerFocus + w.resetFling() + w.onPointerMotion(x, y, t) +} + +//export gio_onPointerButton +func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, + serial, t, wbtn, state C.uint32_t) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + w := s.pointerFocus + // From linux-event-codes.h. + const ( + BTN_LEFT = 0x110 + BTN_RIGHT = 0x111 + BTN_MIDDLE = 0x112 + ) + var btn pointer.Buttons + switch wbtn { + case BTN_LEFT: + btn = pointer.ButtonPrimary + case BTN_RIGHT: + btn = pointer.ButtonSecondary + case BTN_MIDDLE: + btn = pointer.ButtonTertiary + default: + return + } + var typ pointer.Type + switch state { + case 0: + w.pointerBtns &^= btn + typ = pointer.Release + case 1: + w.pointerBtns |= btn + typ = pointer.Press + } + w.flushScroll() + w.resetFling() + w.w.Event(pointer.Event{ + Type: typ, + Source: pointer.Mouse, + Buttons: w.pointerBtns, + Position: w.lastPos, + Time: time.Duration(t) * time.Millisecond, + Modifiers: w.disp.xkb.Modifiers(), + }) +} + +//export gio_onPointerAxis +func gio_onPointerAxis(data unsafe.Pointer, p *C.struct_wl_pointer, + t, axis C.uint32_t, value C.wl_fixed_t) { + s := callbackLoad(data).(*wlSeat) + w := s.pointerFocus + v := fromFixed(value) + w.resetFling() + if w.scroll.dist == (f32.Point{}) { + w.scroll.time = time.Duration(t) * time.Millisecond + } + switch axis { + case C.WL_POINTER_AXIS_HORIZONTAL_SCROLL: + w.scroll.dist.X += v + case C.WL_POINTER_AXIS_VERTICAL_SCROLL: + w.scroll.dist.Y += v + } +} + +//export gio_onPointerFrame +func gio_onPointerFrame(data unsafe.Pointer, p *C.struct_wl_pointer) { + s := callbackLoad(data).(*wlSeat) + w := s.pointerFocus + w.flushScroll() + w.flushFling() +} + +func (w *window) flushFling() { + if !w.fling.start { + return + } + w.fling.start = false + estx, esty := w.fling.xExtrapolation.Estimate(), w.fling.yExtrapolation.Estimate() + w.fling.xExtrapolation = fling.Extrapolation{} + w.fling.yExtrapolation = fling.Extrapolation{} + vel := float32(math.Sqrt(float64(estx.Velocity*estx.Velocity + esty.Velocity*esty.Velocity))) + _, _, c := w.config() + if !w.fling.anim.Start(c, time.Now(), vel) { + return + } + invDist := 1 / vel + w.fling.dir.X = estx.Velocity * invDist + w.fling.dir.Y = esty.Velocity * invDist + // Wake up the window loop. + w.disp.wakeup() +} + +//export gio_onPointerAxisSource +func gio_onPointerAxisSource(data unsafe.Pointer, pointer *C.struct_wl_pointer, + source C.uint32_t) { +} + +//export gio_onPointerAxisStop +func gio_onPointerAxisStop(data unsafe.Pointer, p *C.struct_wl_pointer, + t, axis C.uint32_t) { + s := callbackLoad(data).(*wlSeat) + w := s.pointerFocus + w.fling.start = true +} + +//export gio_onPointerAxisDiscrete +func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer, + axis C.uint32_t, discrete C.int32_t) { + s := callbackLoad(data).(*wlSeat) + w := s.pointerFocus + w.resetFling() + switch axis { + case C.WL_POINTER_AXIS_HORIZONTAL_SCROLL: + w.scroll.steps.X += int(discrete) + case C.WL_POINTER_AXIS_VERTICAL_SCROLL: + w.scroll.steps.Y += int(discrete) + } +} + +func (w *window) ReadClipboard() { + w.mu.Lock() + w.readClipboard = true + w.mu.Unlock() + w.disp.wakeup() +} + +func (w *window) WriteClipboard(s string) { + w.mu.Lock() + w.writeClipboard = &s + w.mu.Unlock() + w.disp.wakeup() +} + +func (w *window) Option(opts *Options) { + w.mu.Lock() + w.opts = opts + w.mu.Unlock() + w.disp.wakeup() +} + +func (w *window) setOptions(opts *Options) { + _, _, cfg := w.config() + if o := opts.Size; o != nil { + w.width = cfg.Px(o.Width) + w.height = cfg.Px(o.Height) + } + if o := opts.Title; o != nil { + title := C.CString(*o) + C.xdg_toplevel_set_title(w.topLvl, title) + C.free(unsafe.Pointer(title)) + } +} + +func (w *window) SetCursor(name pointer.CursorName) { + if name == pointer.CursorNone { + C.wl_pointer_set_cursor(w.disp.seat.pointer, w.serial, nil, 0, 0) + return + } + switch name { + default: + fallthrough + case pointer.CursorDefault: + name = "left_ptr" + case pointer.CursorText: + name = "xterm" + case pointer.CursorPointer: + name = "hand1" + case pointer.CursorCrossHair: + name = "crosshair" + case pointer.CursorRowResize: + name = "top_side" + case pointer.CursorColResize: + name = "left_side" + case pointer.CursorGrab: + name = "hand1" + } + cname := C.CString(string(name)) + defer C.free(unsafe.Pointer(cname)) + c := C.wl_cursor_theme_get_cursor(w.cursor.theme, cname) + if c == nil { + return + } + w.cursor.cursor = c + w.setCursor(w.disp.seat.pointer, w.serial) +} + +func (w *window) setCursor(pointer *C.struct_wl_pointer, serial C.uint32_t) { + // Get images[0]. + img := *w.cursor.cursor.images + buf := C.wl_cursor_image_get_buffer(img) + if buf == nil { + return + } + C.wl_pointer_set_cursor(pointer, serial, w.cursor.surf, + C.int32_t(img.hotspot_x), C.int32_t(img.hotspot_y)) + C.wl_surface_attach(w.cursor.surf, buf, 0, 0) + C.wl_surface_damage(w.cursor.surf, 0, 0, C.int32_t(img.width), + C.int32_t(img.height)) + C.wl_surface_commit(w.cursor.surf) +} + +func (w *window) resetFling() { + w.fling.start = false + w.fling.anim = fling.Animation{} +} + +//export gio_onKeyboardKeymap +func gio_onKeyboardKeymap(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, + format C.uint32_t, fd C.int32_t, size C.uint32_t) { + defer syscall.Close(int(fd)) + s := callbackLoad(data).(*wlSeat) + s.disp.repeat.Stop(0) + s.disp.xkb.DestroyKeymapState() + if format != C.WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 { + return + } + if err := s.disp.xkb.LoadKeymap(int(format), int(fd), + int(size)); err != nil { + // TODO: Do better. + panic(err) + } +} + +//export gio_onKeyboardEnter +func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, + serial C.uint32_t, surf *C.struct_wl_surface, keys *C.struct_wl_array) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + w := callbackLoad(unsafe.Pointer(surf)).(*window) + s.keyboardFocus = w + s.disp.repeat.Stop(0) + w.w.Event(key.FocusEvent{Focus: true}) +} + +//export gio_onKeyboardLeave +func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, + serial C.uint32_t, surf *C.struct_wl_surface) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + s.disp.repeat.Stop(0) + w := s.keyboardFocus + w.w.Event(key.FocusEvent{Focus: false}) +} + +//export gio_onKeyboardKey +func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, + serial, timestamp, keyCode, state C.uint32_t) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + w := s.keyboardFocus + t := time.Duration(timestamp) * time.Millisecond + s.disp.repeat.Stop(t) + w.resetFling() + kc := mapXKBKeycode(uint32(keyCode)) + ks := mapXKBKeyState(uint32(state)) + for _, e := range w.disp.xkb.DispatchKey(kc, ks) { + w.w.Event(e) + } + if state != C.WL_KEYBOARD_KEY_STATE_PRESSED { + return + } + if w.disp.xkb.IsRepeatKey(kc) { + w.disp.repeat.Start(w, kc, t) + } +} + +func mapXKBKeycode(keyCode uint32) uint32 { + // According to the xkb_v1 spec: "to determine the xkb keycode, clients must add 8 to the key event keycode." + return keyCode + 8 +} + +func mapXKBKeyState(state uint32) key.State { + switch state { + case C.WL_KEYBOARD_KEY_STATE_RELEASED: + return key.Release + default: + return key.Press + } +} + +func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) { + if r.rate <= 0 { + return + } + stopC := make(chan struct{}) + r.start = t + r.last = 0 + r.now = 0 + r.stopC = stopC + r.key = keyCode + r.win = w.w + rate, delay := r.rate, r.delay + go func() { + timer := time.NewTimer(delay) + for { + select { + case <-timer.C: + case <-stopC: + close(stopC) + return + } + r.Advance(delay) + w.disp.wakeup() + delay = time.Second / time.Duration(rate) + timer.Reset(delay) + } + }() +} + +func (r *repeatState) Stop(t time.Duration) { + if r.stopC == nil { + return + } + r.stopC <- struct{}{} + <-r.stopC + r.stopC = nil + t -= r.start + if r.now > t { + r.now = t + } +} + +func (r *repeatState) Advance(dt time.Duration) { + r.mu.Lock() + defer r.mu.Unlock() + r.now += dt +} + +func (r *repeatState) Repeat(d *wlDisplay) { + if r.rate <= 0 { + return + } + r.mu.Lock() + now := r.now + r.mu.Unlock() + for { + var delay time.Duration + if r.last < r.delay { + delay = r.delay + } else { + delay = time.Second / time.Duration(r.rate) + } + if r.last+delay > now { + break + } + for _, e := range d.xkb.DispatchKey(r.key, key.Press) { + r.win.Event(e) + } + r.last += delay + } +} + +//export gio_onFrameDone +func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, + t C.uint32_t) { + C.wl_callback_destroy(callback) + w := callbackLoad(data).(*window) + if w.lastFrameCallback == callback { + w.lastFrameCallback = nil + w.draw(false) + } +} + +func (w *window) loop() error { + var p poller + for { + if err := w.disp.dispatch(&p); err != nil { + return err + } + if w.dead { + w.w.Event(system.DestroyEvent{}) + break + } + w.process() + } + return nil +} + +func (w *window) process() { + w.mu.Lock() + readClipboard := w.readClipboard + writeClipboard := w.writeClipboard + opts := w.opts + w.readClipboard = false + w.writeClipboard = nil + w.opts = nil + w.mu.Unlock() + if readClipboard { + r, err := w.disp.readClipboard() + // Send empty responses on unavailable clipboards or errors. + if r == nil || err != nil { + w.w.Event(clipboard.Event{}) + return + } + // Don't let slow clipboard transfers block event loop. + go func() { + defer r.Close() + data, _ := ioutil.ReadAll(r) + w.w.Event(clipboard.Event{Text: string(data)}) + }() + } + if writeClipboard != nil { + w.disp.writeClipboard([]byte(*writeClipboard)) + } + if opts != nil { + w.setOptions(opts) + } + // pass false to skip unnecessary drawing. + w.draw(false) +} + +func (d *wlDisplay) dispatch(p *poller) error { + dispfd := C.wl_display_get_fd(d.disp) + // Poll for events and notifications. + pollfds := append(p.pollfds[:0], + syscall.PollFd{Fd: int32(dispfd), + Events: syscall.POLLIN | syscall.POLLERR}, + syscall.PollFd{Fd: int32(d.notify.read), + Events: syscall.POLLIN | syscall.POLLERR}, + ) + dispFd := &pollfds[0] + if ret, err := C.wl_display_flush(d.disp); ret < 0 { + if err != syscall.EAGAIN { + return fmt.Errorf("wayland: wl_display_flush failed: %v", err) + } + // EAGAIN means the output buffer was full. Poll for + // POLLOUT to know when we can write again. + dispFd.Events |= syscall.POLLOUT + } + if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR { + return fmt.Errorf("wayland: poll failed: %v", err) + } + // Clear notifications. + for { + _, err := syscall.Read(d.notify.read, p.buf[:]) + if err == syscall.EAGAIN { + break + } + if err != nil { + return fmt.Errorf("wayland: read from notify pipe failed: %v", err) + } + } + // Handle events + switch { + case dispFd.Revents&syscall.POLLIN != 0: + if ret, err := C.wl_display_dispatch(d.disp); ret < 0 { + return fmt.Errorf("wayland: wl_display_dispatch failed: %v", err) + } + case dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0: + return errors.New("wayland: display file descriptor gone") + } + d.repeat.Repeat(d) + return nil +} + +func (w *window) SetAnimating(anim bool) { + w.mu.Lock() + w.animating = anim + w.mu.Unlock() + w.disp.wakeup() +} + +// Wakeup wakes up the event loop through the notification pipe. +func (d *wlDisplay) wakeup() { + oneByte := make([]byte, 1) + if _, err := syscall.Write(d.notify.write, + oneByte); err != nil && err != syscall.EAGAIN { + panic(fmt.Errorf("failed to write to pipe: %v", err)) + } +} + +func (w *window) destroy() { + if w.cursor.surf != nil { + C.wl_surface_destroy(w.cursor.surf) + } + if w.cursor.theme != nil { + C.wl_cursor_theme_destroy(w.cursor.theme) + } + if w.topLvl != nil { + C.xdg_toplevel_destroy(w.topLvl) + } + if w.surf != nil { + C.wl_surface_destroy(w.surf) + } + if w.wmSurf != nil { + C.xdg_surface_destroy(w.wmSurf) + } + if w.decor != nil { + C.zxdg_toplevel_decoration_v1_destroy(w.decor) + } + callbackDelete(unsafe.Pointer(w.surf)) +} + +//export gio_onKeyboardModifiers +func gio_onKeyboardModifiers(data unsafe.Pointer, + keyboard *C.struct_wl_keyboard, + serial, depressed, latched, locked, group C.uint32_t) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + d := s.disp + d.repeat.Stop(0) + if d.xkb == nil { + return + } + d.xkb.UpdateMask(uint32(depressed), uint32(latched), uint32(locked), + uint32(group), uint32(group), uint32(group)) +} + +//export gio_onKeyboardRepeatInfo +func gio_onKeyboardRepeatInfo(data unsafe.Pointer, + keyboard *C.struct_wl_keyboard, rate, delay C.int32_t) { + s := callbackLoad(data).(*wlSeat) + d := s.disp + d.repeat.Stop(0) + d.repeat.rate = int(rate) + d.repeat.delay = time.Duration(delay) * time.Millisecond +} + +//export gio_onTextInputEnter +func gio_onTextInputEnter(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, + surf *C.struct_wl_surface) { +} + +//export gio_onTextInputLeave +func gio_onTextInputLeave(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, + surf *C.struct_wl_surface) { +} + +//export gio_onTextInputPreeditString +func gio_onTextInputPreeditString(data unsafe.Pointer, + im *C.struct_zwp_text_input_v3, ctxt *C.char, begin, end C.int32_t) { +} + +//export gio_onTextInputCommitString +func gio_onTextInputCommitString(data unsafe.Pointer, + im *C.struct_zwp_text_input_v3, ctxt *C.char) { +} + +//export gio_onTextInputDeleteSurroundingText +func gio_onTextInputDeleteSurroundingText(data unsafe.Pointer, + im *C.struct_zwp_text_input_v3, before, after C.uint32_t) { +} + +//export gio_onTextInputDone +func gio_onTextInputDone(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, + serial C.uint32_t) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial +} + +//export gio_onDataSourceTarget +func gio_onDataSourceTarget(data unsafe.Pointer, + source *C.struct_wl_data_source, mime *C.char) { +} + +//export gio_onDataSourceSend +func gio_onDataSourceSend(data unsafe.Pointer, source *C.struct_wl_data_source, + mime *C.char, fd C.int32_t) { + s := callbackLoad(data).(*wlSeat) + content := s.content + go func() { + defer syscall.Close(int(fd)) + syscall.Write(int(fd), content) + }() +} + +//export gio_onDataSourceCancelled +func gio_onDataSourceCancelled(data unsafe.Pointer, + source *C.struct_wl_data_source) { + s := callbackLoad(data).(*wlSeat) + if s.source == source { + s.content = nil + s.source = nil + } + C.wl_data_source_destroy(source) +} + +//export gio_onDataSourceDNDDropPerformed +func gio_onDataSourceDNDDropPerformed(data unsafe.Pointer, + source *C.struct_wl_data_source) { +} + +//export gio_onDataSourceDNDFinished +func gio_onDataSourceDNDFinished(data unsafe.Pointer, + source *C.struct_wl_data_source) { +} + +//export gio_onDataSourceAction +func gio_onDataSourceAction(data unsafe.Pointer, + source *C.struct_wl_data_source, act C.uint32_t) { +} + +func (w *window) flushScroll() { + var fling f32.Point + if w.fling.anim.Active() { + dist := float32(w.fling.anim.Tick(time.Now())) + fling = w.fling.dir.Mul(dist) + } + // The Wayland reported scroll distance for + // discrete scroll axes is only 10 pixels, where + // 100 seems more appropriate. + const discreteScale = 10 + if w.scroll.steps.X != 0 { + w.scroll.dist.X *= discreteScale + } + if w.scroll.steps.Y != 0 { + w.scroll.dist.Y *= discreteScale + } + total := w.scroll.dist.Add(fling) + if total == (f32.Point{}) { + return + } + w.w.Event(pointer.Event{ + Type: pointer.Scroll, + Source: pointer.Mouse, + Buttons: w.pointerBtns, + Position: w.lastPos, + Scroll: total, + Time: w.scroll.time, + Modifiers: w.disp.xkb.Modifiers(), + }) + if w.scroll.steps == (image.Point{}) { + w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X) + w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y) + } + w.scroll.dist = f32.Point{} + w.scroll.steps = image.Point{} +} + +func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) { + w.flushScroll() + w.lastPos = f32.Point{ + X: fromFixed(x) * float32(w.scale), + Y: fromFixed(y) * float32(w.scale), + } + w.w.Event(pointer.Event{ + Type: pointer.Move, + Position: w.lastPos, + Buttons: w.pointerBtns, + Source: pointer.Mouse, + Time: time.Duration(t) * time.Millisecond, + Modifiers: w.disp.xkb.Modifiers(), + }) +} + +func (w *window) updateOpaqueRegion() { + reg := C.wl_compositor_create_region(w.disp.compositor) + C.wl_region_add(reg, 0, 0, C.int32_t(w.width), C.int32_t(w.height)) + C.wl_surface_set_opaque_region(w.surf, reg) + C.wl_region_destroy(reg) +} + +func (w *window) updateOutputs() { + scale := 1 + var found bool + for _, conf := range w.disp.outputConfig { + for _, w2 := range conf.windows { + if w2 == w { + found = true + if conf.scale > scale { + scale = conf.scale + } + } + } + } + w.mu.Lock() + if found && scale != w.scale { + w.scale = scale + w.newScale = true + } + w.mu.Unlock() + if !found { + w.setStage(system.StagePaused) + } else { + w.setStage(system.StageRunning) + w.draw(true) + } +} + +func (w *window) config() (int, int, unit.Metric) { + width, height := w.width*w.scale, w.height*w.scale + return width, height, unit.Metric{ + PxPerDp: w.ppdp * float32(w.scale), + PxPerSp: w.ppsp * float32(w.scale), + } +} + +func (w *window) draw(sync bool) { + w.flushScroll() + w.mu.Lock() + anim := w.animating || w.fling.anim.Active() + dead := w.dead + w.mu.Unlock() + if dead || (!anim && !sync) { + return + } + width, height, cfg := w.config() + if cfg == (unit.Metric{}) { + return + } + if anim && w.lastFrameCallback == nil { + w.lastFrameCallback = C.wl_surface_frame(w.surf) + // Use the surface as listener data for gio_onFrameDone. + C.wl_callback_add_listener(w.lastFrameCallback, + &C.gio_callback_listener, unsafe.Pointer(w.surf)) + } + w.w.Event(FrameEvent{ + FrameEvent: system.FrameEvent{ + Now: time.Now(), + Size: image.Point{ + X: width, + Y: height, + }, + Metric: cfg, + }, + Sync: sync, + }) +} + +func (w *window) setStage(s system.Stage) { + if s == w.stage { + return + } + w.stage = s + w.w.Event(system.StageEvent{Stage: s}) +} + +func (w *window) display() *C.struct_wl_display { + return w.disp.disp +} + +func (w *window) surface() (*C.struct_wl_surface, int, int) { + if w.needAck { + C.xdg_surface_ack_configure(w.wmSurf, w.serial) + w.needAck = false + } + width, height, scale := w.width, w.height, w.scale + if w.newScale { + C.wl_surface_set_buffer_scale(w.surf, C.int32_t(scale)) + w.newScale = false + } + return w.surf, width * scale, height * scale +} + +func (w *window) ShowTextInput(show bool) {} + +// Close the window. Not implemented for Wayland. +func (w *window) Close() {} + +// detectUIScale reports the system UI scale, or 1.0 if it fails. +func detectUIScale() float32 { + // TODO: What about other window environments? + out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", + "text-scaling-factor").Output() + if err != nil { + return 1.0 + } + scale, err := strconv.ParseFloat(string(bytes.TrimSpace(out)), 32) + if err != nil { + return 1.0 + } + return float32(scale) +} + +func newWLDisplay() (*wlDisplay, error) { + d := &wlDisplay{ + outputMap: make(map[C.uint32_t]*C.struct_wl_output), + outputConfig: make(map[*C.struct_wl_output]*wlOutput), + } + pipe := make([]int, 2) + if err := syscall.Pipe2(pipe, + syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil { + return nil, fmt.Errorf("wayland: failed to create pipe: %v", err) + } + d.notify.read = pipe[0] + d.notify.write = pipe[1] + xkb, err := xkb.New() + if err != nil { + d.destroy() + return nil, fmt.Errorf("wayland: %v", err) + } + d.xkb = xkb + d.disp, err = C.wl_display_connect(nil) + if d.disp == nil { + d.destroy() + return nil, fmt.Errorf("wayland: wl_display_connect failed: %v", err) + } + callbackMap.Store(unsafe.Pointer(d.disp), d) + d.reg = C.wl_display_get_registry(d.disp) + if d.reg == nil { + d.destroy() + return nil, errors.New("wayland: wl_display_get_registry failed") + } + C.wl_registry_add_listener(d.reg, &C.gio_registry_listener, + unsafe.Pointer(d.disp)) + // Wait for the server to register all its globals to the + // registry listener (gio_onRegistryGlobal). + C.wl_display_roundtrip(d.disp) + // Configuration listeners are added to outputs by gio_onRegistryGlobal. + // We need another roundtrip to get the initial output configurations + // through the gio_onOutput* callbacks. + C.wl_display_roundtrip(d.disp) + return d, nil +} + +func (d *wlDisplay) destroy() { + if d.notify.write != 0 { + syscall.Close(d.notify.write) + d.notify.write = 0 + } + if d.notify.read != 0 { + syscall.Close(d.notify.read) + d.notify.read = 0 + } + d.repeat.Stop(0) + if d.xkb != nil { + d.xkb.Destroy() + d.xkb = nil + } + if d.seat != nil { + d.seat.destroy() + d.seat = nil + } + if d.imm != nil { + C.zwp_text_input_manager_v3_destroy(d.imm) + } + if d.decor != nil { + C.zxdg_decoration_manager_v1_destroy(d.decor) + } + if d.shm != nil { + C.wl_shm_destroy(d.shm) + } + if d.compositor != nil { + C.wl_compositor_destroy(d.compositor) + } + if d.wm != nil { + C.xdg_wm_base_destroy(d.wm) + } + for _, output := range d.outputMap { + C.wl_output_destroy(output) + } + if d.reg != nil { + C.wl_registry_destroy(d.reg) + } + if d.disp != nil { + C.wl_display_disconnect(d.disp) + callbackDelete(unsafe.Pointer(d.disp)) + } +} + +// fromFixed converts a Wayland wl_fixed_t 23.8 number to float32. +func fromFixed(v C.wl_fixed_t) float32 { + // Convert to float64 to avoid overflow. + // From wayland-util.h. + b := ((1023 + 44) << 52) + (1 << 51) + uint64(v) + f := math.Float64frombits(b) - (3 << 43) + return float32(f) +} diff --git a/gio/app/internal/wm/os_windows.go b/gio/app/internal/wm/os_windows.go new file mode 100644 index 0000000..ba83a87 --- /dev/null +++ b/gio/app/internal/wm/os_windows.go @@ -0,0 +1,805 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +import ( + "errors" + "fmt" + "image" + "reflect" + "runtime" + "sort" + "strings" + "sync" + "time" + "unicode" + "unsafe" + + syscall "golang.org/x/sys/windows" + + "realy.lol/gio/app/internal/windows" + "realy.lol/gio/unit" + gowindows "golang.org/x/sys/windows" + + "realy.lol/gio/f32" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" +) + +type winConstraints struct { + minWidth, minHeight int32 + maxWidth, maxHeight int32 +} + +type winDeltas struct { + width int32 + height int32 +} + +type window struct { + hwnd syscall.Handle + hdc syscall.Handle + w Callbacks + width int + height int + stage system.Stage + pointerBtns pointer.Buttons + + // cursorIn tracks whether the cursor was inside the window according + // to the most recent WM_SETCURSOR. + cursorIn bool + cursor syscall.Handle + + // placement saves the previous window position when in full screen mode. + placement *windows.WindowPlacement + + mu sync.Mutex + animating bool + + minmax winConstraints + deltas winDeltas + opts *Options +} + +const ( + _WM_REDRAW = windows.WM_USER + iota + _WM_CURSOR + _WM_OPTION +) + +type gpuAPI struct { + priority int + initializer func(w *window) (Context, error) +} + +// drivers is the list of potential Context implementations. +var drivers []gpuAPI + +// winMap maps win32 HWNDs to *windows. +var winMap sync.Map + +// iconID is the ID of the icon in the resource file. +const iconID = 1 + +var resources struct { + once sync.Once + // handle is the module handle from GetModuleHandle. + handle syscall.Handle + // class is the Gio window class from RegisterClassEx. + class uint16 + // cursor is the arrow cursor resource. + cursor syscall.Handle +} + +func Main() { + select {} +} + +func NewWindow(window Callbacks, opts *Options) error { + cerr := make(chan error) + go func() { + // GetMessage and PeekMessage can filter on a window HWND, but + // then thread-specific messages such as WM_QUIT are ignored. + // Instead lock the thread so window messages arrive through + // unfiltered GetMessage calls. + runtime.LockOSThread() + w, err := createNativeWindow(opts) + if err != nil { + cerr <- err + return + } + defer w.destroy() + cerr <- nil + winMap.Store(w.hwnd, w) + defer winMap.Delete(w.hwnd) + w.w = window + w.w.SetDriver(w) + defer w.w.Event(system.DestroyEvent{}) + w.Option(opts) + windows.ShowWindow(w.hwnd, windows.SW_SHOWDEFAULT) + windows.SetForegroundWindow(w.hwnd) + windows.SetFocus(w.hwnd) + // Since the window class for the cursor is null, + // set it here to show the cursor. + w.SetCursor(pointer.CursorDefault) + if err := w.loop(); err != nil { + panic(err) + } + }() + return <-cerr +} + +// initResources initializes the resources global. +func initResources() error { + windows.SetProcessDPIAware() + hInst, err := windows.GetModuleHandle() + if err != nil { + return err + } + resources.handle = hInst + c, err := windows.LoadCursor(windows.IDC_ARROW) + if err != nil { + return err + } + resources.cursor = c + icon, _ := windows.LoadImage(hInst, iconID, windows.IMAGE_ICON, 0, 0, + windows.LR_DEFAULTSIZE|windows.LR_SHARED) + wcls := windows.WndClassEx{ + CbSize: uint32(unsafe.Sizeof(windows.WndClassEx{})), + Style: windows.CS_HREDRAW | windows.CS_VREDRAW | windows.CS_OWNDC, + LpfnWndProc: syscall.NewCallback(windowProc), + HInstance: hInst, + HIcon: icon, + LpszClassName: syscall.StringToUTF16Ptr("GioWindow"), + } + cls, err := windows.RegisterClassEx(&wcls) + if err != nil { + return err + } + resources.class = cls + return nil +} + +func getWindowConstraints(cfg unit.Metric, opts *Options) winConstraints { + var minmax winConstraints + if o := opts.MinSize; o != nil { + minmax.minWidth = int32(cfg.Px(o.Width)) + minmax.minHeight = int32(cfg.Px(o.Height)) + } + if o := opts.MaxSize; o != nil { + minmax.maxWidth = int32(cfg.Px(o.Width)) + minmax.maxHeight = int32(cfg.Px(o.Height)) + } + return minmax +} + +func createNativeWindow(opts *Options) (*window, error) { + var resErr error + resources.once.Do(func() { + resErr = initResources() + }) + if resErr != nil { + return nil, resErr + } + dpi := windows.GetSystemDPI() + cfg := configForDPI(dpi) + dwStyle := uint32(windows.WS_OVERLAPPEDWINDOW) + dwExStyle := uint32(windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE) + + hwnd, err := windows.CreateWindowEx(dwExStyle, + resources.class, + "", + dwStyle|windows.WS_CLIPSIBLINGS|windows.WS_CLIPCHILDREN, + windows.CW_USEDEFAULT, windows.CW_USEDEFAULT, + windows.CW_USEDEFAULT, windows.CW_USEDEFAULT, + 0, + 0, + resources.handle, + 0) + if err != nil { + return nil, err + } + w := &window{ + hwnd: hwnd, + minmax: getWindowConstraints(cfg, opts), + opts: opts, + } + w.hdc, err = windows.GetDC(hwnd) + if err != nil { + return nil, err + } + return w, nil +} + +func windowProc(hwnd syscall.Handle, msg uint32, + wParam, lParam uintptr) uintptr { + win, exists := winMap.Load(hwnd) + if !exists { + return windows.DefWindowProc(hwnd, msg, wParam, lParam) + } + + w := win.(*window) + + switch msg { + case windows.WM_UNICHAR: + if wParam == windows.UNICODE_NOCHAR { + // Tell the system that we accept WM_UNICHAR messages. + return windows.TRUE + } + fallthrough + case windows.WM_CHAR: + if r := rune(wParam); unicode.IsPrint(r) { + w.w.Event(key.EditEvent{Text: string(r)}) + } + // The message is processed. + return windows.TRUE + case windows.WM_DPICHANGED: + // Let Windows know we're prepared for runtime DPI changes. + return windows.TRUE + case windows.WM_ERASEBKGND: + // Avoid flickering between GPU content and background color. + return windows.TRUE + case windows.WM_KEYDOWN, windows.WM_KEYUP, windows.WM_SYSKEYDOWN, windows.WM_SYSKEYUP: + if n, ok := convertKeyCode(wParam); ok { + e := key.Event{ + Name: n, + Modifiers: getModifiers(), + State: key.Press, + } + if msg == windows.WM_KEYUP || msg == windows.WM_SYSKEYUP { + e.State = key.Release + } + + w.w.Event(e) + + if (wParam == windows.VK_F10) && (msg == windows.WM_SYSKEYDOWN || msg == windows.WM_SYSKEYUP) { + // Reserve F10 for ourselves, and don't let it open the system menu. Other Windows programs + // such as cmd.exe and graphical debuggers also reserve F10. + return 0 + } + } + case windows.WM_LBUTTONDOWN: + w.pointerButton(pointer.ButtonPrimary, true, lParam, getModifiers()) + case windows.WM_LBUTTONUP: + w.pointerButton(pointer.ButtonPrimary, false, lParam, getModifiers()) + case windows.WM_RBUTTONDOWN: + w.pointerButton(pointer.ButtonSecondary, true, lParam, getModifiers()) + case windows.WM_RBUTTONUP: + w.pointerButton(pointer.ButtonSecondary, false, lParam, getModifiers()) + case windows.WM_MBUTTONDOWN: + w.pointerButton(pointer.ButtonTertiary, true, lParam, getModifiers()) + case windows.WM_MBUTTONUP: + w.pointerButton(pointer.ButtonTertiary, false, lParam, getModifiers()) + case windows.WM_CANCELMODE: + w.w.Event(pointer.Event{ + Type: pointer.Cancel, + }) + case windows.WM_SETFOCUS: + w.w.Event(key.FocusEvent{Focus: true}) + case windows.WM_KILLFOCUS: + w.w.Event(key.FocusEvent{Focus: false}) + case windows.WM_MOUSEMOVE: + x, y := coordsFromlParam(lParam) + p := f32.Point{X: float32(x), Y: float32(y)} + w.w.Event(pointer.Event{ + Type: pointer.Move, + Source: pointer.Mouse, + Position: p, + Buttons: w.pointerBtns, + Time: windows.GetMessageTime(), + }) + case windows.WM_MOUSEWHEEL: + w.scrollEvent(wParam, lParam, false) + case windows.WM_MOUSEHWHEEL: + w.scrollEvent(wParam, lParam, true) + case windows.WM_DESTROY: + windows.PostQuitMessage(0) + case windows.WM_PAINT: + w.draw(true) + case windows.WM_SIZE: + switch wParam { + case windows.SIZE_MINIMIZED: + w.setStage(system.StagePaused) + case windows.SIZE_MAXIMIZED, windows.SIZE_RESTORED: + w.setStage(system.StageRunning) + } + case windows.WM_GETMINMAXINFO: + mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam))) + if w.minmax.minWidth > 0 || w.minmax.minHeight > 0 { + mm.PtMinTrackSize = windows.Point{ + X: w.minmax.minWidth + w.deltas.width, + Y: w.minmax.minHeight + w.deltas.height, + } + } + if w.minmax.maxWidth > 0 || w.minmax.maxHeight > 0 { + mm.PtMaxTrackSize = windows.Point{ + X: w.minmax.maxWidth + w.deltas.width, + Y: w.minmax.maxHeight + w.deltas.height, + } + } + case windows.WM_SETCURSOR: + w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT + fallthrough + case _WM_CURSOR: + if w.cursorIn { + windows.SetCursor(w.cursor) + return windows.TRUE + } + case _WM_OPTION: + w.setOptions() + } + + return windows.DefWindowProc(hwnd, msg, wParam, lParam) +} + +func getModifiers() key.Modifiers { + var kmods key.Modifiers + if windows.GetKeyState(windows.VK_LWIN)&0x1000 != 0 || windows.GetKeyState(windows.VK_RWIN)&0x1000 != 0 { + kmods |= key.ModSuper + } + if windows.GetKeyState(windows.VK_MENU)&0x1000 != 0 { + kmods |= key.ModAlt + } + if windows.GetKeyState(windows.VK_CONTROL)&0x1000 != 0 { + kmods |= key.ModCtrl + } + if windows.GetKeyState(windows.VK_SHIFT)&0x1000 != 0 { + kmods |= key.ModShift + } + return kmods +} + +func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, + kmods key.Modifiers) { + var typ pointer.Type + if press { + typ = pointer.Press + if w.pointerBtns == 0 { + windows.SetCapture(w.hwnd) + } + w.pointerBtns |= btn + } else { + typ = pointer.Release + w.pointerBtns &^= btn + if w.pointerBtns == 0 { + windows.ReleaseCapture() + } + } + x, y := coordsFromlParam(lParam) + p := f32.Point{X: float32(x), Y: float32(y)} + w.w.Event(pointer.Event{ + Type: typ, + Source: pointer.Mouse, + Position: p, + Buttons: w.pointerBtns, + Time: windows.GetMessageTime(), + Modifiers: kmods, + }) +} + +func coordsFromlParam(lParam uintptr) (int, int) { + x := int(int16(lParam & 0xffff)) + y := int(int16((lParam >> 16) & 0xffff)) + return x, y +} + +func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool) { + x, y := coordsFromlParam(lParam) + // The WM_MOUSEWHEEL coordinates are in screen coordinates, in contrast + // to other mouse events. + np := windows.Point{X: int32(x), Y: int32(y)} + windows.ScreenToClient(w.hwnd, &np) + p := f32.Point{X: float32(np.X), Y: float32(np.Y)} + dist := float32(int16(wParam >> 16)) + var sp f32.Point + if horizontal { + sp.X = dist + } else { + sp.Y = -dist + } + w.w.Event(pointer.Event{ + Type: pointer.Scroll, + Source: pointer.Mouse, + Position: p, + Buttons: w.pointerBtns, + Scroll: sp, + Time: windows.GetMessageTime(), + }) +} + +// Adapted from https://blogs.msdn.microsoft.com/oldnewthing/20060126-00/?p=32513/ +func (w *window) loop() error { + msg := new(windows.Msg) +loop: + for { + w.mu.Lock() + anim := w.animating + w.mu.Unlock() + if anim && !windows.PeekMessage(msg, 0, 0, 0, windows.PM_NOREMOVE) { + w.draw(false) + continue + } + switch ret := windows.GetMessage(msg, 0, 0, 0); ret { + case -1: + return errors.New("GetMessage failed") + case 0: + // WM_QUIT received. + break loop + } + windows.TranslateMessage(msg) + windows.DispatchMessage(msg) + } + return nil +} + +func (w *window) SetAnimating(anim bool) { + w.mu.Lock() + w.animating = anim + w.mu.Unlock() + if anim { + w.postRedraw() + } +} + +func (w *window) postRedraw() { + if err := windows.PostMessage(w.hwnd, _WM_REDRAW, 0, 0); err != nil { + panic(err) + } +} + +func (w *window) setStage(s system.Stage) { + w.stage = s + w.w.Event(system.StageEvent{Stage: s}) +} + +func (w *window) draw(sync bool) { + var r windows.Rect + windows.GetClientRect(w.hwnd, &r) + w.width = int(r.Right - r.Left) + w.height = int(r.Bottom - r.Top) + if w.width == 0 || w.height == 0 { + return + } + dpi := windows.GetWindowDPI(w.hwnd) + cfg := configForDPI(dpi) + w.minmax = getWindowConstraints(cfg, w.opts) + w.w.Event(FrameEvent{ + FrameEvent: system.FrameEvent{ + Now: time.Now(), + Size: image.Point{ + X: w.width, + Y: w.height, + }, + Metric: cfg, + }, + Sync: sync, + }) +} + +func (w *window) destroy() { + if w.hdc != 0 { + windows.ReleaseDC(w.hdc) + w.hdc = 0 + } + if w.hwnd != 0 { + windows.DestroyWindow(w.hwnd) + w.hwnd = 0 + } +} + +func (w *window) NewContext() (Context, error) { + sort.Slice(drivers, func(i, j int) bool { + return drivers[i].priority < drivers[j].priority + }) + var errs []string + for _, b := range drivers { + ctx, err := b.initializer(w) + if err == nil { + return ctx, nil + } + errs = append(errs, err.Error()) + } + if len(errs) > 0 { + return nil, fmt.Errorf("NewContext: failed to create a GPU device, tried: %s", + strings.Join(errs, ", ")) + } + return nil, errors.New("NewContext: no available GPU drivers") +} + +func (w *window) ReadClipboard() { + w.readClipboard() +} + +func (w *window) readClipboard() error { + if err := windows.OpenClipboard(w.hwnd); err != nil { + return err + } + defer windows.CloseClipboard() + mem, err := windows.GetClipboardData(windows.CF_UNICODETEXT) + if err != nil { + return err + } + ptr, err := windows.GlobalLock(mem) + if err != nil { + return err + } + defer windows.GlobalUnlock(mem) + content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr))) + go func() { + w.w.Event(clipboard.Event{Text: content}) + }() + return nil +} + +func (w *window) Option(opts *Options) { + w.mu.Lock() + w.opts = opts + w.mu.Unlock() + if err := windows.PostMessage(w.hwnd, _WM_OPTION, 0, 0); err != nil { + panic(err) + } +} + +func (w *window) setOptions() { + w.mu.Lock() + opts := w.opts + w.mu.Unlock() + if o := opts.Size; o != nil { + dpi := windows.GetSystemDPI() + cfg := configForDPI(dpi) + width := int32(cfg.Px(o.Width)) + height := int32(cfg.Px(o.Height)) + + // Include the window decorations. + wr := windows.Rect{ + Right: width, + Bottom: height, + } + dwStyle := uint32(windows.WS_OVERLAPPEDWINDOW) + dwExStyle := uint32(windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE) + windows.AdjustWindowRectEx(&wr, dwStyle, 0, dwExStyle) + + dw, dh := width, height + width = wr.Right - wr.Left + height = wr.Bottom - wr.Top + w.deltas.width = width - dw + w.deltas.height = height - dh + + w.opts.Size = o + windows.MoveWindow(w.hwnd, 0, 0, width, height, true) + } + if o := opts.MinSize; o != nil { + w.opts.MinSize = o + } + if o := opts.MaxSize; o != nil { + w.opts.MaxSize = o + } + if o := opts.Title; o != nil { + windows.SetWindowText(w.hwnd, *opts.Title) + } + if o := opts.WindowMode; o != nil { + w.SetWindowMode(*o) + } +} + +func (w *window) SetWindowMode(mode WindowMode) { + // https://devblogs.microsoft.com/oldnewthing/20100412-00/?p=14353 + switch mode { + case Windowed: + if w.placement == nil { + return + } + windows.SetWindowPlacement(w.hwnd, w.placement) + w.placement = nil + style := windows.GetWindowLong(w.hwnd) + windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, + style|windows.WS_OVERLAPPEDWINDOW) + windows.SetWindowPos(w.hwnd, windows.HWND_TOPMOST, + 0, 0, 0, 0, + windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED, + ) + case Fullscreen: + if w.placement != nil { + return + } + w.placement = windows.GetWindowPlacement(w.hwnd) + style := windows.GetWindowLong(w.hwnd) + windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, + style&^windows.WS_OVERLAPPEDWINDOW) + mi := windows.GetMonitorInfo(w.hwnd) + windows.SetWindowPos(w.hwnd, 0, + mi.Monitor.Left, mi.Monitor.Top, + mi.Monitor.Right-mi.Monitor.Left, + mi.Monitor.Bottom-mi.Monitor.Top, + windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED, + ) + } +} + +func (w *window) WriteClipboard(s string) { + w.writeClipboard(s) +} + +func (w *window) writeClipboard(s string) error { + if err := windows.OpenClipboard(w.hwnd); err != nil { + return err + } + defer windows.CloseClipboard() + if err := windows.EmptyClipboard(); err != nil { + return err + } + u16, err := gowindows.UTF16FromString(s) + if err != nil { + return err + } + n := len(u16) * int(unsafe.Sizeof(u16[0])) + mem, err := windows.GlobalAlloc(n) + if err != nil { + return err + } + ptr, err := windows.GlobalLock(mem) + if err != nil { + windows.GlobalFree(mem) + return err + } + var u16v []uint16 + hdr := (*reflect.SliceHeader)(unsafe.Pointer(&u16v)) + hdr.Data = ptr + hdr.Cap = len(u16) + hdr.Len = len(u16) + copy(u16v, u16) + windows.GlobalUnlock(mem) + if err := windows.SetClipboardData(windows.CF_UNICODETEXT, + mem); err != nil { + windows.GlobalFree(mem) + return err + } + return nil +} + +func (w *window) SetCursor(name pointer.CursorName) { + c, err := loadCursor(name) + if err != nil { + c = resources.cursor + } + w.cursor = c + if err := windows.PostMessage(w.hwnd, _WM_CURSOR, 0, 0); err != nil { + panic(err) + } +} + +func loadCursor(name pointer.CursorName) (syscall.Handle, error) { + var curID uint16 + switch name { + default: + fallthrough + case pointer.CursorDefault: + return resources.cursor, nil + case pointer.CursorText: + curID = windows.IDC_IBEAM + case pointer.CursorPointer: + curID = windows.IDC_HAND + case pointer.CursorCrossHair: + curID = windows.IDC_CROSS + case pointer.CursorColResize: + curID = windows.IDC_SIZEWE + case pointer.CursorRowResize: + curID = windows.IDC_SIZENS + case pointer.CursorGrab: + curID = windows.IDC_SIZEALL + case pointer.CursorNone: + return 0, nil + } + return windows.LoadCursor(curID) +} + +func (w *window) ShowTextInput(show bool) {} + +func (w *window) HDC() syscall.Handle { + return w.hdc +} + +func (w *window) HWND() (syscall.Handle, int, int) { + return w.hwnd, w.width, w.height +} + +func (w *window) Close() { + windows.PostMessage(w.hwnd, windows.WM_CLOSE, 0, 0) +} + +func convertKeyCode(code uintptr) (string, bool) { + if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' { + return string(rune(code)), true + } + var r string + switch code { + case windows.VK_ESCAPE: + r = key.NameEscape + case windows.VK_LEFT: + r = key.NameLeftArrow + case windows.VK_RIGHT: + r = key.NameRightArrow + case windows.VK_RETURN: + r = key.NameReturn + case windows.VK_UP: + r = key.NameUpArrow + case windows.VK_DOWN: + r = key.NameDownArrow + case windows.VK_HOME: + r = key.NameHome + case windows.VK_END: + r = key.NameEnd + case windows.VK_BACK: + r = key.NameDeleteBackward + case windows.VK_DELETE: + r = key.NameDeleteForward + case windows.VK_PRIOR: + r = key.NamePageUp + case windows.VK_NEXT: + r = key.NamePageDown + case windows.VK_F1: + r = "F1" + case windows.VK_F2: + r = "F2" + case windows.VK_F3: + r = "F3" + case windows.VK_F4: + r = "F4" + case windows.VK_F5: + r = "F5" + case windows.VK_F6: + r = "F6" + case windows.VK_F7: + r = "F7" + case windows.VK_F8: + r = "F8" + case windows.VK_F9: + r = "F9" + case windows.VK_F10: + r = "F10" + case windows.VK_F11: + r = "F11" + case windows.VK_F12: + r = "F12" + case windows.VK_TAB: + r = key.NameTab + case windows.VK_SPACE: + r = key.NameSpace + case windows.VK_OEM_1: + r = ";" + case windows.VK_OEM_PLUS: + r = "+" + case windows.VK_OEM_COMMA: + r = "," + case windows.VK_OEM_MINUS: + r = "-" + case windows.VK_OEM_PERIOD: + r = "." + case windows.VK_OEM_2: + r = "/" + case windows.VK_OEM_3: + r = "`" + case windows.VK_OEM_4: + r = "[" + case windows.VK_OEM_5, windows.VK_OEM_102: + r = "\\" + case windows.VK_OEM_6: + r = "]" + case windows.VK_OEM_7: + r = "'" + default: + return "", false + } + return r, true +} + +func configForDPI(dpi int) unit.Metric { + const inchPrDp = 1.0 / 96.0 + ppdp := float32(dpi) * inchPrDp + return unit.Metric{ + PxPerDp: ppdp, + PxPerSp: ppdp, + } +} diff --git a/gio/app/internal/wm/os_x11.go b/gio/app/internal/wm/os_x11.go new file mode 100644 index 0000000..f17f36f --- /dev/null +++ b/gio/app/internal/wm/os_x11.go @@ -0,0 +1,799 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build (linux && !android && !nox11) || freebsd || openbsd +// +build linux,!android,!nox11 freebsd openbsd + +package wm + +/* +#cgo openbsd CFLAGS: -I/usr/X11R6/include -I/usr/local/include +#cgo openbsd LDFLAGS: -L/usr/X11R6/lib -L/usr/local/lib +#cgo freebsd openbsd LDFLAGS: -lX11 -lxkbcommon -lxkbcommon-x11 -lX11-xcb -lXcursor -lXfixes +#cgo linux pkg-config: x11 xkbcommon xkbcommon-x11 x11-xcb xcursor xfixes + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +*/ +import "C" +import ( + "errors" + "fmt" + "image" + "os" + "path/filepath" + "strconv" + "sync" + "time" + "unsafe" + + "realy.lol/gio/f32" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/unit" + + syscall "golang.org/x/sys/unix" + + "realy.lol/gio/app/internal/xkb" +) + +type x11Window struct { + w Callbacks + x *C.Display + xkb *xkb.Context + xkbEventBase C.int + xw C.Window + + atoms struct { + // "UTF8_STRING". + utf8string C.Atom + // "text/plain;charset=utf-8". + plaintext C.Atom + // "TARGETS" + targets C.Atom + // "CLIPBOARD". + clipboard C.Atom + // "CLIPBOARD_CONTENT", the clipboard destination property. + clipboardContent C.Atom + // "WM_DELETE_WINDOW" + evDelWindow C.Atom + // "ATOM" + atom C.Atom + // "GTK_TEXT_BUFFER_CONTENTS" + gtk_text_buffer_contents C.Atom + // "_NET_WM_NAME" + wmName C.Atom + // "_NET_WM_STATE" + wmState C.Atom + // _NET_WM_STATE_FULLSCREEN" + wmStateFullscreen C.Atom + } + stage system.Stage + cfg unit.Metric + width int + height int + notify struct { + read, write int + } + dead bool + + mu sync.Mutex + animating bool + opts *Options + + pointerBtns pointer.Buttons + + clipboard struct { + read bool + write *string + content []byte + } + cursor pointer.CursorName + mode WindowMode +} + +func (w *x11Window) SetAnimating(anim bool) { + w.mu.Lock() + w.animating = anim + w.mu.Unlock() + if anim { + w.wakeup() + } +} + +func (w *x11Window) ReadClipboard() { + w.mu.Lock() + w.clipboard.read = true + w.mu.Unlock() + w.wakeup() +} + +func (w *x11Window) WriteClipboard(s string) { + w.mu.Lock() + w.clipboard.write = &s + w.mu.Unlock() + w.wakeup() +} + +func (w *x11Window) Option(opts *Options) { + w.mu.Lock() + w.opts = opts + w.mu.Unlock() + w.wakeup() +} + +func (w *x11Window) setOptions() { + w.mu.Lock() + opts := w.opts + w.opts = nil + w.mu.Unlock() + if opts == nil { + return + } + var shints C.XSizeHints + if o := opts.MinSize; o != nil { + shints.min_width = C.int(w.cfg.Px(o.Width)) + shints.min_height = C.int(w.cfg.Px(o.Height)) + shints.flags = C.PMinSize + } + if o := opts.MaxSize; o != nil { + shints.max_width = C.int(w.cfg.Px(o.Width)) + shints.max_height = C.int(w.cfg.Px(o.Height)) + shints.flags = shints.flags | C.PMaxSize + } + if shints.flags != 0 { + C.XSetWMNormalHints(w.x, w.xw, &shints) + } + + var title string + if o := opts.Title; o != nil { + title = *o + } + ctitle := C.CString(title) + defer C.free(unsafe.Pointer(ctitle)) + C.XStoreName(w.x, w.xw, ctitle) + // set _NET_WM_NAME as well for UTF-8 support in window title. + C.XSetTextProperty(w.x, w.xw, + &C.XTextProperty{ + value: (*C.uchar)(unsafe.Pointer(ctitle)), + encoding: w.atoms.utf8string, + format: 8, + nitems: C.ulong(len(title)), + }, + w.atoms.wmName) + + if o := opts.WindowMode; o != nil { + w.SetWindowMode(*o) + } +} + +func (w *x11Window) SetCursor(name pointer.CursorName) { + switch name { + case pointer.CursorNone: + w.cursor = name + C.XFixesHideCursor(w.x, w.xw) + return + case pointer.CursorGrab: + name = "hand1" + } + if w.cursor == pointer.CursorNone { + C.XFixesShowCursor(w.x, w.xw) + } + cname := C.CString(string(name)) + defer C.free(unsafe.Pointer(cname)) + c := C.XcursorLibraryLoadCursor(w.x, cname) + if c == 0 { + name = pointer.CursorDefault + } + w.cursor = name + // If c if null (i.e. name was not found), + // XDefineCursor will use the default cursor. + C.XDefineCursor(w.x, w.xw, c) +} + +func (w *x11Window) SetWindowMode(mode WindowMode) { + switch mode { + case w.mode: + return + case Windowed: + C.XDeleteProperty(w.x, w.xw, w.atoms.wmStateFullscreen) + case Fullscreen: + C.XChangeProperty(w.x, w.xw, w.atoms.wmState, C.XA_ATOM, + 32, C.PropModeReplace, + (*C.uchar)(unsafe.Pointer(&w.atoms.wmStateFullscreen)), 1, + ) + default: + return + } + w.mode = mode + // "A Client wishing to change the state of a window MUST send + // a _NET_WM_STATE client message to the root window (see below)." + var xev C.XEvent + ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev)) + *ev = C.XClientMessageEvent{ + _type: C.ClientMessage, + display: w.x, + window: w.xw, + message_type: w.atoms.wmState, + format: 32, + } + arr := (*[5]C.long)(unsafe.Pointer(&ev.data)) + arr[0] = 2 // _NET_WM_STATE_TOGGLE + arr[1] = C.long(w.atoms.wmStateFullscreen) + arr[2] = 0 + arr[3] = 1 // application + arr[4] = 0 + C.XSendEvent( + w.x, + C.XDefaultRootWindow(w.x), // MUST be the root window + C.False, + C.SubstructureNotifyMask|C.SubstructureRedirectMask, + &xev, + ) +} + +func (w *x11Window) ShowTextInput(show bool) {} + +// Close the window. +func (w *x11Window) Close() { + w.mu.Lock() + defer w.mu.Unlock() + + var xev C.XEvent + ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev)) + *ev = C.XClientMessageEvent{ + _type: C.ClientMessage, + display: w.x, + window: w.xw, + message_type: w.atom("WM_PROTOCOLS", true), + format: 32, + } + arr := (*[5]C.long)(unsafe.Pointer(&ev.data)) + arr[0] = C.long(w.atoms.evDelWindow) + arr[1] = C.CurrentTime + C.XSendEvent(w.x, w.xw, C.False, C.NoEventMask, &xev) +} + +var x11OneByte = make([]byte, 1) + +func (w *x11Window) wakeup() { + if _, err := syscall.Write(w.notify.write, + x11OneByte); err != nil && err != syscall.EAGAIN { + panic(fmt.Errorf("failed to write to pipe: %v", err)) + } +} + +func (w *x11Window) display() *C.Display { + return w.x +} + +func (w *x11Window) window() (C.Window, int, int) { + return w.xw, w.width, w.height +} + +func (w *x11Window) setStage(s system.Stage) { + if s == w.stage { + return + } + w.stage = s + w.w.Event(system.StageEvent{Stage: s}) +} + +func (w *x11Window) loop() { + h := x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)} + xfd := C.XConnectionNumber(w.x) + + // Poll for events and notifications. + pollfds := []syscall.PollFd{ + {Fd: int32(xfd), Events: syscall.POLLIN | syscall.POLLERR}, + {Fd: int32(w.notify.read), Events: syscall.POLLIN | syscall.POLLERR}, + } + xEvents := &pollfds[0].Revents + // Plenty of room for a backlog of notifications. + buf := make([]byte, 100) + +loop: + for !w.dead { + var syn, anim bool + // Check for pending draw events before checking animation or blocking. + // This fixes an issue on Xephyr where on startup XPending() > 0 but + // poll will still block. This also prevents no-op calls to poll. + if syn = h.handleEvents(); !syn { + w.mu.Lock() + anim = w.animating + w.mu.Unlock() + if !anim { + // Clear poll events. + *xEvents = 0 + // Wait for X event or gio notification. + if _, err := syscall.Poll(pollfds, + -1); err != nil && err != syscall.EINTR { + panic(fmt.Errorf("x11 loop: poll failed: %w", err)) + } + switch { + case *xEvents&syscall.POLLIN != 0: + syn = h.handleEvents() + if w.dead { + break loop + } + case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0: + break loop + } + } + } + w.setOptions() + // Clear notifications. + for { + _, err := syscall.Read(w.notify.read, buf) + if err == syscall.EAGAIN { + break + } + if err != nil { + panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", + err)) + } + } + + if anim || syn { + w.w.Event(FrameEvent{ + FrameEvent: system.FrameEvent{ + Now: time.Now(), + Size: image.Point{ + X: w.width, + Y: w.height, + }, + Metric: w.cfg, + }, + Sync: syn, + }) + } + w.mu.Lock() + readClipboard := w.clipboard.read + writeClipboard := w.clipboard.write + w.clipboard.read = false + w.clipboard.write = nil + w.mu.Unlock() + if readClipboard { + C.XDeleteProperty(w.x, w.xw, w.atoms.clipboardContent) + C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, + w.atoms.clipboardContent, w.xw, C.CurrentTime) + } + if writeClipboard != nil { + w.clipboard.content = []byte(*writeClipboard) + C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime) + } + } + w.w.Event(system.DestroyEvent{Err: nil}) +} + +func (w *x11Window) destroy() { + if w.notify.write != 0 { + syscall.Close(w.notify.write) + w.notify.write = 0 + } + if w.notify.read != 0 { + syscall.Close(w.notify.read) + w.notify.read = 0 + } + if w.xkb != nil { + w.xkb.Destroy() + w.xkb = nil + } + C.XDestroyWindow(w.x, w.xw) + C.XCloseDisplay(w.x) +} + +// atom is a wrapper around XInternAtom. Callers should cache the result +// in order to limit round-trips to the X server. +// +func (w *x11Window) atom(name string, onlyIfExists bool) C.Atom { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + flag := C.Bool(C.False) + if onlyIfExists { + flag = C.True + } + return C.XInternAtom(w.x, cname, flag) +} + +// x11EventHandler wraps static variables for the main event loop. +// Its sole purpose is to prevent heap allocation and reduce clutter +// in x11window.loop. +// +type x11EventHandler struct { + w *x11Window + text []byte + xev *C.XEvent +} + +// handleEvents returns true if the window needs to be redrawn. +// +func (h *x11EventHandler) handleEvents() bool { + w := h.w + xev := h.xev + redraw := false + for C.XPending(w.x) != 0 { + C.XNextEvent(w.x, xev) + if C.XFilterEvent(xev, C.None) == C.True { + continue + } + switch _type := (*C.XAnyEvent)(unsafe.Pointer(xev))._type; _type { + case h.w.xkbEventBase: + xkbEvent := (*C.XkbAnyEvent)(unsafe.Pointer(xev)) + switch xkbEvent.xkb_type { + case C.XkbNewKeyboardNotify, C.XkbMapNotify: + if err := h.w.updateXkbKeymap(); err != nil { + panic(err) + } + case C.XkbStateNotify: + state := (*C.XkbStateNotifyEvent)(unsafe.Pointer(xev)) + h.w.xkb.UpdateMask(uint32(state.base_mods), + uint32(state.latched_mods), uint32(state.locked_mods), + uint32(state.base_group), uint32(state.latched_group), + uint32(state.locked_group)) + } + case C.KeyPress, C.KeyRelease: + ks := key.Press + if _type == C.KeyRelease { + ks = key.Release + } + kevt := (*C.XKeyPressedEvent)(unsafe.Pointer(xev)) + for _, e := range h.w.xkb.DispatchKey(uint32(kevt.keycode), ks) { + w.w.Event(e) + } + case C.ButtonPress, C.ButtonRelease: + bevt := (*C.XButtonEvent)(unsafe.Pointer(xev)) + ev := pointer.Event{ + Type: pointer.Press, + Source: pointer.Mouse, + Position: f32.Point{ + X: float32(bevt.x), + Y: float32(bevt.y), + }, + Time: time.Duration(bevt.time) * time.Millisecond, + Modifiers: w.xkb.Modifiers(), + } + if bevt._type == C.ButtonRelease { + ev.Type = pointer.Release + } + var btn pointer.Buttons + const scrollScale = 10 + switch bevt.button { + case C.Button1: + btn = pointer.ButtonPrimary + case C.Button2: + btn = pointer.ButtonTertiary + case C.Button3: + btn = pointer.ButtonSecondary + case C.Button4: + // scroll up + ev.Type = pointer.Scroll + ev.Scroll.Y = -scrollScale + case C.Button5: + // scroll down + ev.Type = pointer.Scroll + ev.Scroll.Y = +scrollScale + case 6: + // http://xahlee.info/linux/linux_x11_mouse_button_number.html + // scroll left + ev.Type = pointer.Scroll + ev.Scroll.X = -scrollScale * 2 + case 7: + // scroll right + ev.Type = pointer.Scroll + ev.Scroll.X = +scrollScale * 2 + default: + continue + } + switch _type { + case C.ButtonPress: + w.pointerBtns |= btn + case C.ButtonRelease: + w.pointerBtns |= btn + } + ev.Buttons = w.pointerBtns + w.w.Event(ev) + w.pointerBtns = 0 + case C.MotionNotify: + mevt := (*C.XMotionEvent)(unsafe.Pointer(xev)) + w.w.Event(pointer.Event{ + Type: pointer.Move, + Source: pointer.Mouse, + Buttons: w.pointerBtns, + Position: f32.Point{ + X: float32(mevt.x), + Y: float32(mevt.y), + }, + Time: time.Duration(mevt.time) * time.Millisecond, + Modifiers: w.xkb.Modifiers(), + }) + case C.Expose: // update + // redraw only on the last expose event + redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0 + case C.FocusIn: + w.w.Event(key.FocusEvent{Focus: true}) + case C.FocusOut: + w.w.Event(key.FocusEvent{Focus: false}) + case C.ConfigureNotify: // window configuration change + cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev)) + w.width = int(cevt.width) + w.height = int(cevt.height) + // redraw will be done by a later expose event + case C.SelectionNotify: + cevt := (*C.XSelectionEvent)(unsafe.Pointer(xev)) + prop := w.atoms.clipboardContent + if cevt.property != prop { + break + } + if cevt.selection != w.atoms.clipboard { + break + } + var text C.XTextProperty + if st := C.XGetTextProperty(w.x, w.xw, &text, prop); st == 0 { + // Failed; ignore. + break + } + if text.format != 8 || text.encoding != w.atoms.utf8string { + // Ignore non-utf-8 encoded strings. + break + } + str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), + C.int(text.nitems)) + w.w.Event(clipboard.Event{Text: str}) + case C.SelectionRequest: + cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev)) + if cevt.selection != w.atoms.clipboard || cevt.property == C.None { + // Unsupported clipboard or obsolete requestor. + break + } + notify := func() { + var xev C.XEvent + ev := (*C.XSelectionEvent)(unsafe.Pointer(&xev)) + *ev = C.XSelectionEvent{ + _type: C.SelectionNotify, + display: cevt.display, + requestor: cevt.requestor, + selection: cevt.selection, + target: cevt.target, + property: cevt.property, + time: cevt.time, + } + C.XSendEvent(w.x, cevt.requestor, 0, 0, &xev) + } + switch cevt.target { + case w.atoms.targets: + // The requestor wants the supported clipboard + // formats. First write the targets... + formats := [...]C.long{ + C.long(w.atoms.targets), + C.long(w.atoms.utf8string), + C.long(w.atoms.plaintext), + // GTK clients need this. + C.long(w.atoms.gtk_text_buffer_contents), + } + C.XChangeProperty(w.x, cevt.requestor, cevt.property, + w.atoms.atom, + 32 /* bitwidth of formats */, C.PropModeReplace, + (*C.uchar)(unsafe.Pointer(&formats)), C.int(len(formats)), + ) + // ...then notify the requestor. + notify() + case w.atoms.plaintext, w.atoms.utf8string, w.atoms.gtk_text_buffer_contents: + content := w.clipboard.content + var ptr *C.uchar + if len(content) > 0 { + ptr = (*C.uchar)(unsafe.Pointer(&content[0])) + } + C.XChangeProperty(w.x, cevt.requestor, cevt.property, + cevt.target, + 8 /* bitwidth */, C.PropModeReplace, + ptr, C.int(len(content)), + ) + notify() + } + case C.ClientMessage: // extensions + cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev)) + switch *(*C.long)(unsafe.Pointer(&cevt.data)) { + case C.long(w.atoms.evDelWindow): + w.dead = true + return false + } + } + } + return redraw +} + +var ( + x11Threads sync.Once +) + +func init() { + x11Driver = newX11Window +} + +func newX11Window(gioWin Callbacks, opts *Options) error { + var err error + + pipe := make([]int, 2) + if err := syscall.Pipe2(pipe, + syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil { + return fmt.Errorf("NewX11Window: failed to create pipe: %w", err) + } + + x11Threads.Do(func() { + if C.XInitThreads() == 0 { + err = errors.New("x11: threads init failed") + } + C.XrmInitialize() + }) + if err != nil { + return err + } + dpy := C.XOpenDisplay(nil) + if dpy == nil { + return errors.New("x11: cannot connect to the X server") + } + var major, minor C.int = C.XkbMajorVersion, C.XkbMinorVersion + var xkbEventBase C.int + if C.XkbQueryExtension(dpy, nil, &xkbEventBase, nil, &major, + &minor) != C.True { + C.XCloseDisplay(dpy) + return errors.New("x11: XkbQueryExtension failed") + } + const bits = C.uint(C.XkbNewKeyboardNotifyMask | C.XkbMapNotifyMask | C.XkbStateNotifyMask) + if C.XkbSelectEvents(dpy, C.XkbUseCoreKbd, bits, bits) != C.True { + C.XCloseDisplay(dpy) + return errors.New("x11: XkbSelectEvents failed") + } + xkb, err := xkb.New() + if err != nil { + C.XCloseDisplay(dpy) + return fmt.Errorf("x11: %v", err) + } + + ppsp := x11DetectUIScale(dpy) + cfg := unit.Metric{PxPerDp: ppsp, PxPerSp: ppsp} + swa := C.XSetWindowAttributes{ + event_mask: C.ExposureMask | C.FocusChangeMask | // update + C.KeyPressMask | C.KeyReleaseMask | // keyboard + C.ButtonPressMask | C.ButtonReleaseMask | // mouse clicks + C.PointerMotionMask | // mouse movement + C.StructureNotifyMask, // resize + background_pixmap: C.None, + override_redirect: C.False, + } + var width, height int + if o := opts.Size; o != nil { + width = cfg.Px(o.Width) + height = cfg.Px(o.Height) + } + win := C.XCreateWindow(dpy, C.XDefaultRootWindow(dpy), + 0, 0, C.uint(width), C.uint(height), + 0, C.CopyFromParent, C.InputOutput, nil, + C.CWEventMask|C.CWBackPixmap|C.CWOverrideRedirect, &swa) + + w := &x11Window{ + w: gioWin, x: dpy, xw: win, + width: width, + height: height, + cfg: cfg, + xkb: xkb, + xkbEventBase: xkbEventBase, + } + w.notify.read = pipe[0] + w.notify.write = pipe[1] + + if err := w.updateXkbKeymap(); err != nil { + w.destroy() + return err + } + + var hints C.XWMHints + hints.input = C.True + hints.flags = C.InputHint + C.XSetWMHints(dpy, win, &hints) + + name := C.CString(filepath.Base(os.Args[0])) + defer C.free(unsafe.Pointer(name)) + wmhints := C.XClassHint{name, name} + C.XSetClassHint(dpy, win, &wmhints) + + w.atoms.utf8string = w.atom("UTF8_STRING", false) + w.atoms.plaintext = w.atom("text/plain;charset=utf-8", false) + w.atoms.gtk_text_buffer_contents = w.atom("GTK_TEXT_BUFFER_CONTENTS", false) + w.atoms.evDelWindow = w.atom("WM_DELETE_WINDOW", false) + w.atoms.clipboard = w.atom("CLIPBOARD", false) + w.atoms.clipboardContent = w.atom("CLIPBOARD_CONTENT", false) + w.atoms.atom = w.atom("ATOM", false) + w.atoms.targets = w.atom("TARGETS", false) + w.atoms.wmName = w.atom("_NET_WM_NAME", false) + w.atoms.wmState = w.atom("_NET_WM_STATE", false) + w.atoms.wmStateFullscreen = w.atom("_NET_WM_STATE_FULLSCREEN", false) + + // extensions + C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1) + + w.Option(opts) + + // make the window visible on the screen + C.XMapWindow(dpy, win) + + go func() { + w.w.SetDriver(w) + w.setStage(system.StageRunning) + w.loop() + w.destroy() + }() + return nil +} + +// detectUIScale reports the system UI scale, or 1.0 if it fails. +func x11DetectUIScale(dpy *C.Display) float32 { + // default fixed DPI value used in most desktop UI toolkits + const defaultDesktopDPI = 96 + var scale float32 = 1.0 + + // Get actual DPI from X resource Xft.dpi (set by GTK and Qt). + // This value is entirely based on user preferences and conflates both + // screen (UI) scaling and font scale. + rms := C.XResourceManagerString(dpy) + if rms != nil { + db := C.XrmGetStringDatabase(rms) + if db != nil { + var ( + t *C.char + v C.XrmValue + ) + if C.XrmGetResource(db, + (*C.char)(unsafe.Pointer(&[]byte("Xft.dpi\x00")[0])), + (*C.char)(unsafe.Pointer(&[]byte("Xft.Dpi\x00")[0])), &t, + &v) != C.False { + if t != nil && C.GoString(t) == "String" { + f, err := strconv.ParseFloat(C.GoString(v.addr), 32) + if err == nil { + scale = float32(f) / defaultDesktopDPI + } + } + } + C.XrmDestroyDatabase(db) + } + } + + return scale +} + +func (w *x11Window) updateXkbKeymap() error { + w.xkb.DestroyKeymapState() + ctx := (*C.struct_xkb_context)(unsafe.Pointer(w.xkb.Ctx)) + xcb := C.XGetXCBConnection(w.x) + if xcb == nil { + return errors.New("x11: XGetXCBConnection failed") + } + xkbDevID := C.xkb_x11_get_core_keyboard_device_id(xcb) + if xkbDevID == -1 { + return errors.New("x11: xkb_x11_get_core_keyboard_device_id failed") + } + keymap := C.xkb_x11_keymap_new_from_device(ctx, xcb, xkbDevID, + C.XKB_KEYMAP_COMPILE_NO_FLAGS) + if keymap == nil { + return errors.New("x11: xkb_x11_keymap_new_from_device failed") + } + state := C.xkb_x11_state_new_from_device(keymap, xcb, xkbDevID) + if state == nil { + C.xkb_keymap_unref(keymap) + return errors.New("x11: xkb_x11_keymap_new_from_device failed") + } + w.xkb.SetKeymap(unsafe.Pointer(keymap), unsafe.Pointer(state)) + return nil +} diff --git a/gio/app/internal/wm/runmain.go b/gio/app/internal/wm/runmain.go new file mode 100644 index 0000000..4617217 --- /dev/null +++ b/gio/app/internal/wm/runmain.go @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build android darwin,ios + +package wm + +// Android only supports non-Java programs as c-shared libraries. +// Unfortunately, Go does not run a program's main function in +// library mode. To make Gio programs simpler and uniform, we'll +// link to the main function here and call it from Java. + +import ( + "sync" + _ "unsafe" // for go:linkname +) + +//go:linkname mainMain main.main +func mainMain() + +var runMainOnce sync.Once + +func runMain() { + runMainOnce.Do(func() { + // Indirect call, since the linker does not know the address of main when + // laying down this package. + fn := mainMain + fn() + }) +} diff --git a/gio/app/internal/wm/wayland_text_input.c b/gio/app/internal/wm/wayland_text_input.c new file mode 100644 index 0000000..de01dd5 --- /dev/null +++ b/gio/app/internal/wm/wayland_text_input.c @@ -0,0 +1,98 @@ +// +build linux,!android,!nowayland freebsd + +/* Generated by wayland-scanner 1.21.0 */ + +/* + * Copyright Ā© 2012, 2013 Intel Corporation + * Copyright Ā© 2015, 2016 Jan Arne Petersen + * Copyright Ā© 2017, 2018 Red Hat, Inc. + * Copyright Ā© 2018 Purism SPC + * + * Permission to use, copy, modify, distribute, and sell this + * software and its documentation for any purpose is hereby granted + * without fee, provided that the above copyright notice appear in + * all copies and that both that copyright notice and this permission + * notice appear in supporting documentation, and that the name of + * the copyright holders not be used in advertising or publicity + * pertaining to distribution of the software without specific, + * written prior permission. The copyright holders make no + * representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied + * warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + * THIS SOFTWARE. + */ + +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_seat_interface; +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface zwp_text_input_v3_interface; + +static const struct wl_interface *text_input_unstable_v3_types[] = { + NULL, + NULL, + NULL, + NULL, + &wl_surface_interface, + &wl_surface_interface, + &zwp_text_input_v3_interface, + &wl_seat_interface, +}; + +static const struct wl_message zwp_text_input_v3_requests[] = { + { "destroy", "", text_input_unstable_v3_types + 0 }, + { "enable", "", text_input_unstable_v3_types + 0 }, + { "disable", "", text_input_unstable_v3_types + 0 }, + { "set_surrounding_text", "sii", text_input_unstable_v3_types + 0 }, + { "set_text_change_cause", "u", text_input_unstable_v3_types + 0 }, + { "set_content_type", "uu", text_input_unstable_v3_types + 0 }, + { "set_cursor_rectangle", "iiii", text_input_unstable_v3_types + 0 }, + { "commit", "", text_input_unstable_v3_types + 0 }, +}; + +static const struct wl_message zwp_text_input_v3_events[] = { + { "enter", "o", text_input_unstable_v3_types + 4 }, + { "leave", "o", text_input_unstable_v3_types + 5 }, + { "preedit_string", "?sii", text_input_unstable_v3_types + 0 }, + { "commit_string", "?s", text_input_unstable_v3_types + 0 }, + { "delete_surrounding_text", "uu", text_input_unstable_v3_types + 0 }, + { "done", "u", text_input_unstable_v3_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwp_text_input_v3_interface = { + "zwp_text_input_v3", 1, + 8, zwp_text_input_v3_requests, + 6, zwp_text_input_v3_events, +}; + +static const struct wl_message zwp_text_input_manager_v3_requests[] = { + { "destroy", "", text_input_unstable_v3_types + 0 }, + { "get_text_input", "no", text_input_unstable_v3_types + 6 }, +}; + +WL_PRIVATE const struct wl_interface zwp_text_input_manager_v3_interface = { + "zwp_text_input_manager_v3", 1, + 2, zwp_text_input_manager_v3_requests, + 0, NULL, +}; + diff --git a/gio/app/internal/wm/wayland_text_input.h b/gio/app/internal/wm/wayland_text_input.h new file mode 100644 index 0000000..b1bb886 --- /dev/null +++ b/gio/app/internal/wm/wayland_text_input.h @@ -0,0 +1,838 @@ +/* Generated by wayland-scanner 1.21.0 */ + +#ifndef TEXT_INPUT_UNSTABLE_V3_CLIENT_PROTOCOL_H +#define TEXT_INPUT_UNSTABLE_V3_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_text_input_unstable_v3 The text_input_unstable_v3 protocol + * Protocol for composing text + * + * @section page_desc_text_input_unstable_v3 Description + * + * This protocol allows compositors to act as input methods and to send text + * to applications. A text input object is used to manage state of what are + * typically text entry fields in the application. + * + * This document adheres to the RFC 2119 when using words like "must", + * "should", "may", etc. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + * + * @section page_ifaces_text_input_unstable_v3 Interfaces + * - @subpage page_iface_zwp_text_input_v3 - text input + * - @subpage page_iface_zwp_text_input_manager_v3 - text input manager + * @section page_copyright_text_input_unstable_v3 Copyright + *
+ *
+ * Copyright Ā© 2012, 2013 Intel Corporation
+ * Copyright Ā© 2015, 2016 Jan Arne Petersen
+ * Copyright Ā© 2017, 2018 Red Hat, Inc.
+ * Copyright Ā© 2018       Purism SPC
+ *
+ * Permission to use, copy, modify, distribute, and sell this
+ * software and its documentation for any purpose is hereby granted
+ * without fee, provided that the above copyright notice appear in
+ * all copies and that both that copyright notice and this permission
+ * notice appear in supporting documentation, and that the name of
+ * the copyright holders not be used in advertising or publicity
+ * pertaining to distribution of the software without specific,
+ * written prior permission.  The copyright holders make no
+ * representations about the suitability of this software for any
+ * purpose.  It is provided "as is" without express or implied
+ * warranty.
+ *
+ * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+ * THIS SOFTWARE.
+ * 
+ */ +struct wl_seat; +struct wl_surface; +struct zwp_text_input_manager_v3; +struct zwp_text_input_v3; + +#ifndef ZWP_TEXT_INPUT_V3_INTERFACE +#define ZWP_TEXT_INPUT_V3_INTERFACE +/** + * @page page_iface_zwp_text_input_v3 zwp_text_input_v3 + * @section page_iface_zwp_text_input_v3_desc Description + * + * The zwp_text_input_v3 interface represents text input and input methods + * associated with a seat. It provides enter/leave events to follow the + * text input focus for a seat. + * + * Requests are used to enable/disable the text-input object and set + * state information like surrounding and selected text or the content type. + * The information about the entered text is sent to the text-input object + * via the preedit_string and commit_string events. + * + * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices + * must not point to middle bytes inside a code point: they must either + * point to the first byte of a code point or to the end of the buffer. + * Lengths must be measured between two valid indices. + * + * Focus moving throughout surfaces will result in the emission of + * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused + * surface must commit zwp_text_input_v3.enable and + * zwp_text_input_v3.disable requests as the keyboard focus moves across + * editable and non-editable elements of the UI. Those two requests are not + * expected to be paired with each other, the compositor must be able to + * handle consecutive series of the same request. + * + * State is sent by the state requests (set_surrounding_text, + * set_content_type and set_cursor_rectangle) and a commit request. After an + * enter event or disable request all state information is invalidated and + * needs to be resent by the client. + * @section page_iface_zwp_text_input_v3_api API + * See @ref iface_zwp_text_input_v3. + */ +/** + * @defgroup iface_zwp_text_input_v3 The zwp_text_input_v3 interface + * + * The zwp_text_input_v3 interface represents text input and input methods + * associated with a seat. It provides enter/leave events to follow the + * text input focus for a seat. + * + * Requests are used to enable/disable the text-input object and set + * state information like surrounding and selected text or the content type. + * The information about the entered text is sent to the text-input object + * via the preedit_string and commit_string events. + * + * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices + * must not point to middle bytes inside a code point: they must either + * point to the first byte of a code point or to the end of the buffer. + * Lengths must be measured between two valid indices. + * + * Focus moving throughout surfaces will result in the emission of + * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused + * surface must commit zwp_text_input_v3.enable and + * zwp_text_input_v3.disable requests as the keyboard focus moves across + * editable and non-editable elements of the UI. Those two requests are not + * expected to be paired with each other, the compositor must be able to + * handle consecutive series of the same request. + * + * State is sent by the state requests (set_surrounding_text, + * set_content_type and set_cursor_rectangle) and a commit request. After an + * enter event or disable request all state information is invalidated and + * needs to be resent by the client. + */ +extern const struct wl_interface zwp_text_input_v3_interface; +#endif +#ifndef ZWP_TEXT_INPUT_MANAGER_V3_INTERFACE +#define ZWP_TEXT_INPUT_MANAGER_V3_INTERFACE +/** + * @page page_iface_zwp_text_input_manager_v3 zwp_text_input_manager_v3 + * @section page_iface_zwp_text_input_manager_v3_desc Description + * + * A factory for text-input objects. This object is a global singleton. + * @section page_iface_zwp_text_input_manager_v3_api API + * See @ref iface_zwp_text_input_manager_v3. + */ +/** + * @defgroup iface_zwp_text_input_manager_v3 The zwp_text_input_manager_v3 interface + * + * A factory for text-input objects. This object is a global singleton. + */ +extern const struct wl_interface zwp_text_input_manager_v3_interface; +#endif + +#ifndef ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM +#define ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM +/** + * @ingroup iface_zwp_text_input_v3 + * text change reason + * + * Reason for the change of surrounding text or cursor posision. + */ +enum zwp_text_input_v3_change_cause { + /** + * input method caused the change + */ + ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD = 0, + /** + * something else than the input method caused the change + */ + ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER = 1, +}; +#endif /* ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM */ + +#ifndef ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM +#define ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM +/** + * @ingroup iface_zwp_text_input_v3 + * content hint + * + * Content hint is a bitmask to allow to modify the behavior of the text + * input. + */ +enum zwp_text_input_v3_content_hint { + /** + * no special behavior + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE = 0x0, + /** + * suggest word completions + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION = 0x1, + /** + * suggest word corrections + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK = 0x2, + /** + * switch to uppercase letters at the start of a sentence + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION = 0x4, + /** + * prefer lowercase letters + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_LOWERCASE = 0x8, + /** + * prefer uppercase letters + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_UPPERCASE = 0x10, + /** + * prefer casing for titles and headings (can be language dependent) + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE = 0x20, + /** + * characters should be hidden + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_HIDDEN_TEXT = 0x40, + /** + * typed text should not be stored + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA = 0x80, + /** + * just Latin characters should be entered + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_LATIN = 0x100, + /** + * the text input is multiline + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_MULTILINE = 0x200, +}; +#endif /* ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM */ + +#ifndef ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM +#define ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM +/** + * @ingroup iface_zwp_text_input_v3 + * content purpose + * + * The content purpose allows to specify the primary purpose of a text + * input. + * + * This allows an input method to show special purpose input panels with + * extra characters or to disallow some characters. + */ +enum zwp_text_input_v3_content_purpose { + /** + * default input, allowing all characters + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL = 0, + /** + * allow only alphabetic characters + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA = 1, + /** + * allow only digits + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS = 2, + /** + * input a number (including decimal separator and sign) + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER = 3, + /** + * input a phone number + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE = 4, + /** + * input an URL + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL = 5, + /** + * input an email address + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL = 6, + /** + * input a name of a person + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME = 7, + /** + * input a password (combine with sensitive_data hint) + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD = 8, + /** + * input is a numeric password (combine with sensitive_data hint) + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PIN = 9, + /** + * input a date + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATE = 10, + /** + * input a time + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TIME = 11, + /** + * input a date and time + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATETIME = 12, + /** + * input for a terminal + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL = 13, +}; +#endif /* ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM */ + +/** + * @ingroup iface_zwp_text_input_v3 + * @struct zwp_text_input_v3_listener + */ +struct zwp_text_input_v3_listener { + /** + * enter event + * + * Notification that this seat's text-input focus is on a certain + * surface. + * + * If client has created multiple text input objects, compositor + * must send this event to all of them. + * + * When the seat has the keyboard capability the text-input focus + * follows the keyboard focus. This event sets the current surface + * for the text-input object. + */ + void (*enter)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + struct wl_surface *surface); + /** + * leave event + * + * Notification that this seat's text-input focus is no longer on + * a certain surface. The client should reset any preedit string + * previously set. + * + * The leave notification clears the current surface. It is sent + * before the enter notification for the new focus. After leave + * event, compositor must ignore requests from any text input + * instances until next enter event. + * + * When the seat has the keyboard capability the text-input focus + * follows the keyboard focus. + */ + void (*leave)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + struct wl_surface *surface); + /** + * pre-edit + * + * Notify when a new composing text (pre-edit) should be set at + * the current cursor position. Any previously set composing text + * must be removed. Any previously existing selected text must be + * removed. + * + * The argument text contains the pre-edit string buffer. + * + * The parameters cursor_begin and cursor_end are counted in bytes + * relative to the beginning of the submitted text buffer. Cursor + * should be hidden when both are equal to -1. + * + * They could be represented by the client as a line if both values + * are the same, or as a text highlight otherwise. + * + * Values set with this event are double-buffered. They must be + * applied and reset to initial on the next zwp_text_input_v3.done + * event. + * + * The initial value of text is an empty string, and cursor_begin, + * cursor_end and cursor_hidden are all 0. + */ + void (*preedit_string)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + const char *text, + int32_t cursor_begin, + int32_t cursor_end); + /** + * text commit + * + * Notify when text should be inserted into the editor widget. + * The text to commit could be either just a single character after + * a key press or the result of some composing (pre-edit). + * + * Values set with this event are double-buffered. They must be + * applied and reset to initial on the next zwp_text_input_v3.done + * event. + * + * The initial value of text is an empty string. + */ + void (*commit_string)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + const char *text); + /** + * delete surrounding text + * + * Notify when the text around the current cursor position should + * be deleted. + * + * Before_length and after_length are the number of bytes before + * and after the current cursor index (excluding the selection) to + * delete. + * + * If a preedit text is present, in effect before_length is counted + * from the beginning of it, and after_length from its end (see + * done event sequence). + * + * Values set with this event are double-buffered. They must be + * applied and reset to initial on the next zwp_text_input_v3.done + * event. + * + * The initial values of both before_length and after_length are 0. + * @param before_length length of text before current cursor position + * @param after_length length of text after current cursor position + */ + void (*delete_surrounding_text)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t before_length, + uint32_t after_length); + /** + * apply changes + * + * Instruct the application to apply changes to state requested + * by the preedit_string, commit_string and delete_surrounding_text + * events. The state relating to these events is double-buffered, + * and each one modifies the pending state. This event replaces the + * current state with the pending state. + * + * The application must proceed by evaluating the changes in the + * following order: + * + * 1. Replace existing preedit string with the cursor. 2. Delete + * requested surrounding text. 3. Insert commit string with the + * cursor at its end. 4. Calculate surrounding text to send. 5. + * Insert new preedit text in cursor position. 6. Place cursor + * inside preedit text. + * + * The serial number reflects the last state of the + * zwp_text_input_v3 object known to the compositor. The value of + * the serial argument must be equal to the number of commit + * requests already issued on that object. + * + * When the client receives a done event with a serial different + * than the number of past commit requests, it must proceed with + * evaluating and applying the changes as normal, except it should + * not change the current state of the zwp_text_input_v3 object. + * All pending state requests (set_surrounding_text, + * set_content_type and set_cursor_rectangle) on the + * zwp_text_input_v3 object should be sent and committed after + * receiving a zwp_text_input_v3.done event with a matching serial. + */ + void (*done)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t serial); +}; + +/** + * @ingroup iface_zwp_text_input_v3 + */ +static inline int +zwp_text_input_v3_add_listener(struct zwp_text_input_v3 *zwp_text_input_v3, + const struct zwp_text_input_v3_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zwp_text_input_v3, + (void (**)(void)) listener, data); +} + +#define ZWP_TEXT_INPUT_V3_DESTROY 0 +#define ZWP_TEXT_INPUT_V3_ENABLE 1 +#define ZWP_TEXT_INPUT_V3_DISABLE 2 +#define ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT 3 +#define ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE 4 +#define ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE 5 +#define ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE 6 +#define ZWP_TEXT_INPUT_V3_COMMIT 7 + +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_ENTER_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_LEAVE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_PREEDIT_STRING_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_COMMIT_STRING_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DELETE_SURROUNDING_TEXT_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DONE_SINCE_VERSION 1 + +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_ENABLE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DISABLE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_COMMIT_SINCE_VERSION 1 + +/** @ingroup iface_zwp_text_input_v3 */ +static inline void +zwp_text_input_v3_set_user_data(struct zwp_text_input_v3 *zwp_text_input_v3, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwp_text_input_v3, user_data); +} + +/** @ingroup iface_zwp_text_input_v3 */ +static inline void * +zwp_text_input_v3_get_user_data(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwp_text_input_v3); +} + +static inline uint32_t +zwp_text_input_v3_get_version(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + return wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Destroy the wp_text_input object. Also disables all surfaces enabled + * through this wp_text_input object. + */ +static inline void +zwp_text_input_v3_destroy(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Requests text input on the surface previously obtained from the enter + * event. + * + * This request must be issued every time the active text input changes + * to a new one, including within the current surface. Use + * zwp_text_input_v3.disable when there is no longer any input focus on + * the current surface. + * + * Clients must not enable more than one text input on the single seat + * and should disable the current text input before enabling the new one. + * At most one instance of text input may be in enabled state per instance, + * Requests to enable the another text input when some text input is active + * must be ignored by compositor. + * + * This request resets all state associated with previous enable, disable, + * set_surrounding_text, set_text_change_cause, set_content_type, and + * set_cursor_rectangle requests, as well as the state associated with + * preedit_string, commit_string, and delete_surrounding_text events. + * + * The set_surrounding_text, set_content_type and set_cursor_rectangle + * requests must follow if the text input supports the necessary + * functionality. + * + * State set with this request is double-buffered. It will get applied on + * the next zwp_text_input_v3.commit request, and stay valid until the + * next committed enable or disable request. + * + * The changes must be applied by the compositor after issuing a + * zwp_text_input_v3.commit request. + */ +static inline void +zwp_text_input_v3_enable(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_ENABLE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Explicitly disable text input on the current surface (typically when + * there is no focus on any text entry inside the surface). + * + * State set with this request is double-buffered. It will get applied on + * the next zwp_text_input_v3.commit request. + */ +static inline void +zwp_text_input_v3_disable(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_DISABLE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Sets the surrounding plain text around the input, excluding the preedit + * text. + * + * The client should notify the compositor of any changes in any of the + * values carried with this request, including changes caused by handling + * incoming text-input events as well as changes caused by other + * mechanisms like keyboard typing. + * + * If the client is unaware of the text around the cursor, it should not + * issue this request, to signify lack of support to the compositor. + * + * Text is UTF-8 encoded, and should include the cursor position, the + * complete selection and additional characters before and after them. + * There is a maximum length of wayland messages, so text can not be + * longer than 4000 bytes. + * + * Cursor is the byte offset of the cursor within text buffer. + * + * Anchor is the byte offset of the selection anchor within text buffer. + * If there is no selected text, anchor is the same as cursor. + * + * If any preedit text is present, it is replaced with a cursor for the + * purpose of this event. + * + * Values set with this request are double-buffered. They will get applied + * on the next zwp_text_input_v3.commit request, and stay valid until the + * next committed enable or disable request. + * + * The initial state for affected fields is empty, meaning that the text + * input does not support sending surrounding text. If the empty values + * get applied, subsequent attempts to change them may have no effect. + */ +static inline void +zwp_text_input_v3_set_surrounding_text(struct zwp_text_input_v3 *zwp_text_input_v3, const char *text, int32_t cursor, int32_t anchor) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, text, cursor, anchor); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Tells the compositor why the text surrounding the cursor changed. + * + * Whenever the client detects an external change in text, cursor, or + * anchor posision, it must issue this request to the compositor. This + * request is intended to give the input method a chance to update the + * preedit text in an appropriate way, e.g. by removing it when the user + * starts typing with a keyboard. + * + * cause describes the source of the change. + * + * The value set with this request is double-buffered. It must be applied + * and reset to initial at the next zwp_text_input_v3.commit request. + * + * The initial value of cause is input_method. + */ +static inline void +zwp_text_input_v3_set_text_change_cause(struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t cause) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, cause); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Sets the content purpose and content hint. While the purpose is the + * basic purpose of an input field, the hint flags allow to modify some of + * the behavior. + * + * Values set with this request are double-buffered. They will get applied + * on the next zwp_text_input_v3.commit request. + * Subsequent attempts to update them may have no effect. The values + * remain valid until the next committed enable or disable request. + * + * The initial value for hint is none, and the initial value for purpose + * is normal. + */ +static inline void +zwp_text_input_v3_set_content_type(struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t hint, uint32_t purpose) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, hint, purpose); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Marks an area around the cursor as a x, y, width, height rectangle in + * surface local coordinates. + * + * Allows the compositor to put a window with word suggestions near the + * cursor, without obstructing the text being input. + * + * If the client is unaware of the position of edited text, it should not + * issue this request, to signify lack of support to the compositor. + * + * Values set with this request are double-buffered. They will get applied + * on the next zwp_text_input_v3.commit request, and stay valid until the + * next committed enable or disable request. + * + * The initial values describing a cursor rectangle are empty. That means + * the text input does not support describing the cursor area. If the + * empty values get applied, subsequent attempts to change them may have + * no effect. + */ +static inline void +zwp_text_input_v3_set_cursor_rectangle(struct zwp_text_input_v3 *zwp_text_input_v3, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, x, y, width, height); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Atomically applies state changes recently sent to the compositor. + * + * The commit request establishes and updates the state of the client, and + * must be issued after any changes to apply them. + * + * Text input state (enabled status, content purpose, content hint, + * surrounding text and change cause, cursor rectangle) is conceptually + * double-buffered within the context of a text input, i.e. between a + * committed enable request and the following committed enable or disable + * request. + * + * Protocol requests modify the pending state, as opposed to the current + * state in use by the input method. A commit request atomically applies + * all pending state, replacing the current state. After commit, the new + * pending state is as documented for each related request. + * + * Requests are applied in the order of arrival. + * + * Neither current nor pending state are modified unless noted otherwise. + * + * The compositor must count the number of commit requests coming from + * each zwp_text_input_v3 object and use the count as the serial in done + * events. + */ +static inline void +zwp_text_input_v3_commit(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_COMMIT, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0); +} + +#define ZWP_TEXT_INPUT_MANAGER_V3_DESTROY 0 +#define ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT 1 + + +/** + * @ingroup iface_zwp_text_input_manager_v3 + */ +#define ZWP_TEXT_INPUT_MANAGER_V3_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_manager_v3 + */ +#define ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT_SINCE_VERSION 1 + +/** @ingroup iface_zwp_text_input_manager_v3 */ +static inline void +zwp_text_input_manager_v3_set_user_data(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwp_text_input_manager_v3, user_data); +} + +/** @ingroup iface_zwp_text_input_manager_v3 */ +static inline void * +zwp_text_input_manager_v3_get_user_data(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwp_text_input_manager_v3); +} + +static inline uint32_t +zwp_text_input_manager_v3_get_version(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) +{ + return wl_proxy_get_version((struct wl_proxy *) zwp_text_input_manager_v3); +} + +/** + * @ingroup iface_zwp_text_input_manager_v3 + * + * Destroy the wp_text_input_manager object. + */ +static inline void +zwp_text_input_manager_v3_destroy(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_manager_v3, + ZWP_TEXT_INPUT_MANAGER_V3_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_manager_v3), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_zwp_text_input_manager_v3 + * + * Creates a new text-input object for a given seat. + */ +static inline struct zwp_text_input_v3 * +zwp_text_input_manager_v3_get_text_input(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3, struct wl_seat *seat) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_manager_v3, + ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT, &zwp_text_input_v3_interface, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_manager_v3), 0, NULL, seat); + + return (struct zwp_text_input_v3 *) id; +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/gio/app/internal/wm/wayland_xdg_decoration.c b/gio/app/internal/wm/wayland_xdg_decoration.c new file mode 100644 index 0000000..78f9328 --- /dev/null +++ b/gio/app/internal/wm/wayland_xdg_decoration.c @@ -0,0 +1,77 @@ +// +build linux,!android,!nowayland freebsd + +/* Generated by wayland-scanner 1.21.0 */ + +/* + * Copyright Ā© 2018 Simon Ser + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface xdg_toplevel_interface; +extern const struct wl_interface zxdg_toplevel_decoration_v1_interface; + +static const struct wl_interface *xdg_decoration_unstable_v1_types[] = { + NULL, + &zxdg_toplevel_decoration_v1_interface, + &xdg_toplevel_interface, +}; + +static const struct wl_message zxdg_decoration_manager_v1_requests[] = { + { "destroy", "", xdg_decoration_unstable_v1_types + 0 }, + { "get_toplevel_decoration", "no", xdg_decoration_unstable_v1_types + 1 }, +}; + +WL_PRIVATE const struct wl_interface zxdg_decoration_manager_v1_interface = { + "zxdg_decoration_manager_v1", 1, + 2, zxdg_decoration_manager_v1_requests, + 0, NULL, +}; + +static const struct wl_message zxdg_toplevel_decoration_v1_requests[] = { + { "destroy", "", xdg_decoration_unstable_v1_types + 0 }, + { "set_mode", "u", xdg_decoration_unstable_v1_types + 0 }, + { "unset_mode", "", xdg_decoration_unstable_v1_types + 0 }, +}; + +static const struct wl_message zxdg_toplevel_decoration_v1_events[] = { + { "configure", "u", xdg_decoration_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zxdg_toplevel_decoration_v1_interface = { + "zxdg_toplevel_decoration_v1", 1, + 3, zxdg_toplevel_decoration_v1_requests, + 1, zxdg_toplevel_decoration_v1_events, +}; + diff --git a/gio/app/internal/wm/wayland_xdg_decoration.h b/gio/app/internal/wm/wayland_xdg_decoration.h new file mode 100644 index 0000000..286c236 --- /dev/null +++ b/gio/app/internal/wm/wayland_xdg_decoration.h @@ -0,0 +1,378 @@ +/* Generated by wayland-scanner 1.21.0 */ + +#ifndef XDG_DECORATION_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define XDG_DECORATION_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_xdg_decoration_unstable_v1 The xdg_decoration_unstable_v1 protocol + * @section page_ifaces_xdg_decoration_unstable_v1 Interfaces + * - @subpage page_iface_zxdg_decoration_manager_v1 - window decoration manager + * - @subpage page_iface_zxdg_toplevel_decoration_v1 - decoration object for a toplevel surface + * @section page_copyright_xdg_decoration_unstable_v1 Copyright + *
+ *
+ * Copyright Ā© 2018 Simon Ser
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * 
+ */ +struct xdg_toplevel; +struct zxdg_decoration_manager_v1; +struct zxdg_toplevel_decoration_v1; + +#ifndef ZXDG_DECORATION_MANAGER_V1_INTERFACE +#define ZXDG_DECORATION_MANAGER_V1_INTERFACE +/** + * @page page_iface_zxdg_decoration_manager_v1 zxdg_decoration_manager_v1 + * @section page_iface_zxdg_decoration_manager_v1_desc Description + * + * This interface allows a compositor to announce support for server-side + * decorations. + * + * A window decoration is a set of window controls as deemed appropriate by + * the party managing them, such as user interface components used to move, + * resize and change a window's state. + * + * A client can use this protocol to request being decorated by a supporting + * compositor. + * + * If compositor and client do not negotiate the use of a server-side + * decoration using this protocol, clients continue to self-decorate as they + * see fit. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + * @section page_iface_zxdg_decoration_manager_v1_api API + * See @ref iface_zxdg_decoration_manager_v1. + */ +/** + * @defgroup iface_zxdg_decoration_manager_v1 The zxdg_decoration_manager_v1 interface + * + * This interface allows a compositor to announce support for server-side + * decorations. + * + * A window decoration is a set of window controls as deemed appropriate by + * the party managing them, such as user interface components used to move, + * resize and change a window's state. + * + * A client can use this protocol to request being decorated by a supporting + * compositor. + * + * If compositor and client do not negotiate the use of a server-side + * decoration using this protocol, clients continue to self-decorate as they + * see fit. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + */ +extern const struct wl_interface zxdg_decoration_manager_v1_interface; +#endif +#ifndef ZXDG_TOPLEVEL_DECORATION_V1_INTERFACE +#define ZXDG_TOPLEVEL_DECORATION_V1_INTERFACE +/** + * @page page_iface_zxdg_toplevel_decoration_v1 zxdg_toplevel_decoration_v1 + * @section page_iface_zxdg_toplevel_decoration_v1_desc Description + * + * The decoration object allows the compositor to toggle server-side window + * decorations for a toplevel surface. The client can request to switch to + * another mode. + * + * The xdg_toplevel_decoration object must be destroyed before its + * xdg_toplevel. + * @section page_iface_zxdg_toplevel_decoration_v1_api API + * See @ref iface_zxdg_toplevel_decoration_v1. + */ +/** + * @defgroup iface_zxdg_toplevel_decoration_v1 The zxdg_toplevel_decoration_v1 interface + * + * The decoration object allows the compositor to toggle server-side window + * decorations for a toplevel surface. The client can request to switch to + * another mode. + * + * The xdg_toplevel_decoration object must be destroyed before its + * xdg_toplevel. + */ +extern const struct wl_interface zxdg_toplevel_decoration_v1_interface; +#endif + +#define ZXDG_DECORATION_MANAGER_V1_DESTROY 0 +#define ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION 1 + + +/** + * @ingroup iface_zxdg_decoration_manager_v1 + */ +#define ZXDG_DECORATION_MANAGER_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_decoration_manager_v1 + */ +#define ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION_SINCE_VERSION 1 + +/** @ingroup iface_zxdg_decoration_manager_v1 */ +static inline void +zxdg_decoration_manager_v1_set_user_data(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zxdg_decoration_manager_v1, user_data); +} + +/** @ingroup iface_zxdg_decoration_manager_v1 */ +static inline void * +zxdg_decoration_manager_v1_get_user_data(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zxdg_decoration_manager_v1); +} + +static inline uint32_t +zxdg_decoration_manager_v1_get_version(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zxdg_decoration_manager_v1); +} + +/** + * @ingroup iface_zxdg_decoration_manager_v1 + * + * Destroy the decoration manager. This doesn't destroy objects created + * with the manager. + */ +static inline void +zxdg_decoration_manager_v1_destroy(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zxdg_decoration_manager_v1, + ZXDG_DECORATION_MANAGER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_decoration_manager_v1), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_zxdg_decoration_manager_v1 + * + * Create a new decoration object associated with the given toplevel. + * + * Creating an xdg_toplevel_decoration from an xdg_toplevel which has a + * buffer attached or committed is a client error, and any attempts by a + * client to attach or manipulate a buffer prior to the first + * xdg_toplevel_decoration.configure event must also be treated as + * errors. + */ +static inline struct zxdg_toplevel_decoration_v1 * +zxdg_decoration_manager_v1_get_toplevel_decoration(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1, struct xdg_toplevel *toplevel) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) zxdg_decoration_manager_v1, + ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION, &zxdg_toplevel_decoration_v1_interface, wl_proxy_get_version((struct wl_proxy *) zxdg_decoration_manager_v1), 0, NULL, toplevel); + + return (struct zxdg_toplevel_decoration_v1 *) id; +} + +#ifndef ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM +#define ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM +enum zxdg_toplevel_decoration_v1_error { + /** + * xdg_toplevel has a buffer attached before configure + */ + ZXDG_TOPLEVEL_DECORATION_V1_ERROR_UNCONFIGURED_BUFFER = 0, + /** + * xdg_toplevel already has a decoration object + */ + ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ALREADY_CONSTRUCTED = 1, + /** + * xdg_toplevel destroyed before the decoration object + */ + ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ORPHANED = 2, +}; +#endif /* ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM */ + +#ifndef ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM +#define ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * window decoration modes + * + * These values describe window decoration modes. + */ +enum zxdg_toplevel_decoration_v1_mode { + /** + * no server-side window decoration + */ + ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE = 1, + /** + * server-side window decoration + */ + ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE = 2, +}; +#endif /* ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM */ + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * @struct zxdg_toplevel_decoration_v1_listener + */ +struct zxdg_toplevel_decoration_v1_listener { + /** + * suggest a surface change + * + * The configure event asks the client to change its decoration + * mode. The configured state should not be applied immediately. + * Clients must send an ack_configure in response to this event. + * See xdg_surface.configure and xdg_surface.ack_configure for + * details. + * + * A configure event can be sent at any time. The specified mode + * must be obeyed by the client. + * @param mode the decoration mode + */ + void (*configure)(void *data, + struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, + uint32_t mode); +}; + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +static inline int +zxdg_toplevel_decoration_v1_add_listener(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, + const struct zxdg_toplevel_decoration_v1_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zxdg_toplevel_decoration_v1, + (void (**)(void)) listener, data); +} + +#define ZXDG_TOPLEVEL_DECORATION_V1_DESTROY 0 +#define ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE 1 +#define ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE 2 + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +#define ZXDG_TOPLEVEL_DECORATION_V1_CONFIGURE_SINCE_VERSION 1 + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +#define ZXDG_TOPLEVEL_DECORATION_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +#define ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +#define ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE_SINCE_VERSION 1 + +/** @ingroup iface_zxdg_toplevel_decoration_v1 */ +static inline void +zxdg_toplevel_decoration_v1_set_user_data(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zxdg_toplevel_decoration_v1, user_data); +} + +/** @ingroup iface_zxdg_toplevel_decoration_v1 */ +static inline void * +zxdg_toplevel_decoration_v1_get_user_data(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zxdg_toplevel_decoration_v1); +} + +static inline uint32_t +zxdg_toplevel_decoration_v1_get_version(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zxdg_toplevel_decoration_v1); +} + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * + * Switch back to a mode without any server-side decorations at the next + * commit. + */ +static inline void +zxdg_toplevel_decoration_v1_destroy(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zxdg_toplevel_decoration_v1, + ZXDG_TOPLEVEL_DECORATION_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_toplevel_decoration_v1), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * + * Set the toplevel surface decoration mode. This informs the compositor + * that the client prefers the provided decoration mode. + * + * After requesting a decoration mode, the compositor will respond by + * emitting an xdg_surface.configure event. The client should then update + * its content, drawing it without decorations if the received mode is + * server-side decorations. The client must also acknowledge the configure + * when committing the new content (see xdg_surface.ack_configure). + * + * The compositor can decide not to use the client's mode and enforce a + * different mode instead. + * + * Clients whose decoration mode depend on the xdg_toplevel state may send + * a set_mode request in response to an xdg_surface.configure event and wait + * for the next xdg_surface.configure event to prevent unwanted state. + * Such clients are responsible for preventing configure loops and must + * make sure not to send multiple successive set_mode requests with the + * same decoration mode. + */ +static inline void +zxdg_toplevel_decoration_v1_set_mode(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, uint32_t mode) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zxdg_toplevel_decoration_v1, + ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_toplevel_decoration_v1), 0, mode); +} + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * + * Unset the toplevel surface decoration mode. This informs the compositor + * that the client doesn't prefer a particular decoration mode. + * + * This request has the same semantics as set_mode. + */ +static inline void +zxdg_toplevel_decoration_v1_unset_mode(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zxdg_toplevel_decoration_v1, + ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_toplevel_decoration_v1), 0); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/gio/app/internal/wm/wayland_xdg_shell.c b/gio/app/internal/wm/wayland_xdg_shell.c new file mode 100644 index 0000000..4ed2659 --- /dev/null +++ b/gio/app/internal/wm/wayland_xdg_shell.c @@ -0,0 +1,185 @@ +// +build linux,!android,!nowayland freebsd + +/* Generated by wayland-scanner 1.21.0 */ + +/* + * Copyright Ā© 2008-2013 Kristian HĆøgsberg + * Copyright Ā© 2013 Rafael Antognolli + * Copyright Ā© 2013 Jasper St. Pierre + * Copyright Ā© 2010-2013 Intel Corporation + * Copyright Ā© 2015-2017 Samsung Electronics Co., Ltd + * Copyright Ā© 2015-2017 Red Hat Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_output_interface; +extern const struct wl_interface wl_seat_interface; +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface xdg_popup_interface; +extern const struct wl_interface xdg_positioner_interface; +extern const struct wl_interface xdg_surface_interface; +extern const struct wl_interface xdg_toplevel_interface; + +static const struct wl_interface *xdg_shell_types[] = { + NULL, + NULL, + NULL, + NULL, + &xdg_positioner_interface, + &xdg_surface_interface, + &wl_surface_interface, + &xdg_toplevel_interface, + &xdg_popup_interface, + &xdg_surface_interface, + &xdg_positioner_interface, + &xdg_toplevel_interface, + &wl_seat_interface, + NULL, + NULL, + NULL, + &wl_seat_interface, + NULL, + &wl_seat_interface, + NULL, + NULL, + &wl_output_interface, + &wl_seat_interface, + NULL, + &xdg_positioner_interface, + NULL, +}; + +static const struct wl_message xdg_wm_base_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "create_positioner", "n", xdg_shell_types + 4 }, + { "get_xdg_surface", "no", xdg_shell_types + 5 }, + { "pong", "u", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_wm_base_events[] = { + { "ping", "u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_wm_base_interface = { + "xdg_wm_base", 5, + 4, xdg_wm_base_requests, + 1, xdg_wm_base_events, +}; + +static const struct wl_message xdg_positioner_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "set_size", "ii", xdg_shell_types + 0 }, + { "set_anchor_rect", "iiii", xdg_shell_types + 0 }, + { "set_anchor", "u", xdg_shell_types + 0 }, + { "set_gravity", "u", xdg_shell_types + 0 }, + { "set_constraint_adjustment", "u", xdg_shell_types + 0 }, + { "set_offset", "ii", xdg_shell_types + 0 }, + { "set_reactive", "3", xdg_shell_types + 0 }, + { "set_parent_size", "3ii", xdg_shell_types + 0 }, + { "set_parent_configure", "3u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_positioner_interface = { + "xdg_positioner", 5, + 10, xdg_positioner_requests, + 0, NULL, +}; + +static const struct wl_message xdg_surface_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "get_toplevel", "n", xdg_shell_types + 7 }, + { "get_popup", "n?oo", xdg_shell_types + 8 }, + { "set_window_geometry", "iiii", xdg_shell_types + 0 }, + { "ack_configure", "u", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_surface_events[] = { + { "configure", "u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_surface_interface = { + "xdg_surface", 5, + 5, xdg_surface_requests, + 1, xdg_surface_events, +}; + +static const struct wl_message xdg_toplevel_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "set_parent", "?o", xdg_shell_types + 11 }, + { "set_title", "s", xdg_shell_types + 0 }, + { "set_app_id", "s", xdg_shell_types + 0 }, + { "show_window_menu", "ouii", xdg_shell_types + 12 }, + { "move", "ou", xdg_shell_types + 16 }, + { "resize", "ouu", xdg_shell_types + 18 }, + { "set_max_size", "ii", xdg_shell_types + 0 }, + { "set_min_size", "ii", xdg_shell_types + 0 }, + { "set_maximized", "", xdg_shell_types + 0 }, + { "unset_maximized", "", xdg_shell_types + 0 }, + { "set_fullscreen", "?o", xdg_shell_types + 21 }, + { "unset_fullscreen", "", xdg_shell_types + 0 }, + { "set_minimized", "", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_toplevel_events[] = { + { "configure", "iia", xdg_shell_types + 0 }, + { "close", "", xdg_shell_types + 0 }, + { "configure_bounds", "4ii", xdg_shell_types + 0 }, + { "wm_capabilities", "5a", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_toplevel_interface = { + "xdg_toplevel", 5, + 14, xdg_toplevel_requests, + 4, xdg_toplevel_events, +}; + +static const struct wl_message xdg_popup_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "grab", "ou", xdg_shell_types + 22 }, + { "reposition", "3ou", xdg_shell_types + 24 }, +}; + +static const struct wl_message xdg_popup_events[] = { + { "configure", "iiii", xdg_shell_types + 0 }, + { "popup_done", "", xdg_shell_types + 0 }, + { "repositioned", "3u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_popup_interface = { + "xdg_popup", 5, + 3, xdg_popup_requests, + 3, xdg_popup_events, +}; + diff --git a/gio/app/internal/wm/wayland_xdg_shell.h b/gio/app/internal/wm/wayland_xdg_shell.h new file mode 100644 index 0000000..aa14e2e --- /dev/null +++ b/gio/app/internal/wm/wayland_xdg_shell.h @@ -0,0 +1,2280 @@ +/* Generated by wayland-scanner 1.21.0 */ + +#ifndef XDG_SHELL_CLIENT_PROTOCOL_H +#define XDG_SHELL_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_xdg_shell The xdg_shell protocol + * @section page_ifaces_xdg_shell Interfaces + * - @subpage page_iface_xdg_wm_base - create desktop-style surfaces + * - @subpage page_iface_xdg_positioner - child surface positioner + * - @subpage page_iface_xdg_surface - desktop user interface surface base interface + * - @subpage page_iface_xdg_toplevel - toplevel surface + * - @subpage page_iface_xdg_popup - short-lived, popup surfaces for menus + * @section page_copyright_xdg_shell Copyright + *
+ *
+ * Copyright Ā© 2008-2013 Kristian HĆøgsberg
+ * Copyright Ā© 2013      Rafael Antognolli
+ * Copyright Ā© 2013      Jasper St. Pierre
+ * Copyright Ā© 2010-2013 Intel Corporation
+ * Copyright Ā© 2015-2017 Samsung Electronics Co., Ltd
+ * Copyright Ā© 2015-2017 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * 
+ */ +struct wl_output; +struct wl_seat; +struct wl_surface; +struct xdg_popup; +struct xdg_positioner; +struct xdg_surface; +struct xdg_toplevel; +struct xdg_wm_base; + +#ifndef XDG_WM_BASE_INTERFACE +#define XDG_WM_BASE_INTERFACE +/** + * @page page_iface_xdg_wm_base xdg_wm_base + * @section page_iface_xdg_wm_base_desc Description + * + * The xdg_wm_base interface is exposed as a global object enabling clients + * to turn their wl_surfaces into windows in a desktop environment. It + * defines the basic functionality needed for clients and the compositor to + * create windows that can be dragged, resized, maximized, etc, as well as + * creating transient windows such as popup menus. + * @section page_iface_xdg_wm_base_api API + * See @ref iface_xdg_wm_base. + */ +/** + * @defgroup iface_xdg_wm_base The xdg_wm_base interface + * + * The xdg_wm_base interface is exposed as a global object enabling clients + * to turn their wl_surfaces into windows in a desktop environment. It + * defines the basic functionality needed for clients and the compositor to + * create windows that can be dragged, resized, maximized, etc, as well as + * creating transient windows such as popup menus. + */ +extern const struct wl_interface xdg_wm_base_interface; +#endif +#ifndef XDG_POSITIONER_INTERFACE +#define XDG_POSITIONER_INTERFACE +/** + * @page page_iface_xdg_positioner xdg_positioner + * @section page_iface_xdg_positioner_desc Description + * + * The xdg_positioner provides a collection of rules for the placement of a + * child surface relative to a parent surface. Rules can be defined to ensure + * the child surface remains within the visible area's borders, and to + * specify how the child surface changes its position, such as sliding along + * an axis, or flipping around a rectangle. These positioner-created rules are + * constrained by the requirement that a child surface must intersect with or + * be at least partially adjacent to its parent surface. + * + * See the various requests for details about possible rules. + * + * At the time of the request, the compositor makes a copy of the rules + * specified by the xdg_positioner. Thus, after the request is complete the + * xdg_positioner object can be destroyed or reused; further changes to the + * object will have no effect on previous usages. + * + * For an xdg_positioner object to be considered complete, it must have a + * non-zero size set by set_size, and a non-zero anchor rectangle set by + * set_anchor_rect. Passing an incomplete xdg_positioner object when + * positioning a surface raises an invalid_positioner error. + * @section page_iface_xdg_positioner_api API + * See @ref iface_xdg_positioner. + */ +/** + * @defgroup iface_xdg_positioner The xdg_positioner interface + * + * The xdg_positioner provides a collection of rules for the placement of a + * child surface relative to a parent surface. Rules can be defined to ensure + * the child surface remains within the visible area's borders, and to + * specify how the child surface changes its position, such as sliding along + * an axis, or flipping around a rectangle. These positioner-created rules are + * constrained by the requirement that a child surface must intersect with or + * be at least partially adjacent to its parent surface. + * + * See the various requests for details about possible rules. + * + * At the time of the request, the compositor makes a copy of the rules + * specified by the xdg_positioner. Thus, after the request is complete the + * xdg_positioner object can be destroyed or reused; further changes to the + * object will have no effect on previous usages. + * + * For an xdg_positioner object to be considered complete, it must have a + * non-zero size set by set_size, and a non-zero anchor rectangle set by + * set_anchor_rect. Passing an incomplete xdg_positioner object when + * positioning a surface raises an invalid_positioner error. + */ +extern const struct wl_interface xdg_positioner_interface; +#endif +#ifndef XDG_SURFACE_INTERFACE +#define XDG_SURFACE_INTERFACE +/** + * @page page_iface_xdg_surface xdg_surface + * @section page_iface_xdg_surface_desc Description + * + * An interface that may be implemented by a wl_surface, for + * implementations that provide a desktop-style user interface. + * + * It provides a base set of functionality required to construct user + * interface elements requiring management by the compositor, such as + * toplevel windows, menus, etc. The types of functionality are split into + * xdg_surface roles. + * + * Creating an xdg_surface does not set the role for a wl_surface. In order + * to map an xdg_surface, the client must create a role-specific object + * using, e.g., get_toplevel, get_popup. The wl_surface for any given + * xdg_surface can have at most one role, and may not be assigned any role + * not based on xdg_surface. + * + * A role must be assigned before any other requests are made to the + * xdg_surface object. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_surface state to take effect. + * + * Creating an xdg_surface from a wl_surface which has a buffer attached or + * committed is a client error, and any attempts by a client to attach or + * manipulate a buffer prior to the first xdg_surface.configure call must + * also be treated as errors. + * + * After creating a role-specific object and setting it up, the client must + * perform an initial commit without any buffer attached. The compositor + * will reply with an xdg_surface.configure event. The client must + * acknowledge it and is then allowed to attach a buffer to map the surface. + * + * Mapping an xdg_surface-based role surface is defined as making it + * possible for the surface to be shown by the compositor. Note that + * a mapped surface is not guaranteed to be visible once it is mapped. + * + * For an xdg_surface to be mapped by the compositor, the following + * conditions must be met: + * (1) the client has assigned an xdg_surface-based role to the surface + * (2) the client has set and committed the xdg_surface state and the + * role-dependent state to the surface + * (3) the client has committed a buffer to the surface + * + * A newly-unmapped surface is considered to have met condition (1) out + * of the 3 required conditions for mapping a surface if its role surface + * has not been destroyed, i.e. the client must perform the initial commit + * again before attaching a buffer. + * @section page_iface_xdg_surface_api API + * See @ref iface_xdg_surface. + */ +/** + * @defgroup iface_xdg_surface The xdg_surface interface + * + * An interface that may be implemented by a wl_surface, for + * implementations that provide a desktop-style user interface. + * + * It provides a base set of functionality required to construct user + * interface elements requiring management by the compositor, such as + * toplevel windows, menus, etc. The types of functionality are split into + * xdg_surface roles. + * + * Creating an xdg_surface does not set the role for a wl_surface. In order + * to map an xdg_surface, the client must create a role-specific object + * using, e.g., get_toplevel, get_popup. The wl_surface for any given + * xdg_surface can have at most one role, and may not be assigned any role + * not based on xdg_surface. + * + * A role must be assigned before any other requests are made to the + * xdg_surface object. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_surface state to take effect. + * + * Creating an xdg_surface from a wl_surface which has a buffer attached or + * committed is a client error, and any attempts by a client to attach or + * manipulate a buffer prior to the first xdg_surface.configure call must + * also be treated as errors. + * + * After creating a role-specific object and setting it up, the client must + * perform an initial commit without any buffer attached. The compositor + * will reply with an xdg_surface.configure event. The client must + * acknowledge it and is then allowed to attach a buffer to map the surface. + * + * Mapping an xdg_surface-based role surface is defined as making it + * possible for the surface to be shown by the compositor. Note that + * a mapped surface is not guaranteed to be visible once it is mapped. + * + * For an xdg_surface to be mapped by the compositor, the following + * conditions must be met: + * (1) the client has assigned an xdg_surface-based role to the surface + * (2) the client has set and committed the xdg_surface state and the + * role-dependent state to the surface + * (3) the client has committed a buffer to the surface + * + * A newly-unmapped surface is considered to have met condition (1) out + * of the 3 required conditions for mapping a surface if its role surface + * has not been destroyed, i.e. the client must perform the initial commit + * again before attaching a buffer. + */ +extern const struct wl_interface xdg_surface_interface; +#endif +#ifndef XDG_TOPLEVEL_INTERFACE +#define XDG_TOPLEVEL_INTERFACE +/** + * @page page_iface_xdg_toplevel xdg_toplevel + * @section page_iface_xdg_toplevel_desc Description + * + * This interface defines an xdg_surface role which allows a surface to, + * among other things, set window-like properties such as maximize, + * fullscreen, and minimize, set application-specific metadata like title and + * id, and well as trigger user interactive operations such as interactive + * resize and move. + * + * Unmapping an xdg_toplevel means that the surface cannot be shown + * by the compositor until it is explicitly mapped again. + * All active operations (e.g., move, resize) are canceled and all + * attributes (e.g. title, state, stacking, ...) are discarded for + * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + * the state it had right after xdg_surface.get_toplevel. The client + * can re-map the toplevel by perfoming a commit without any buffer + * attached, waiting for a configure event and handling it as usual (see + * xdg_surface description). + * + * Attaching a null buffer to a toplevel unmaps the surface. + * @section page_iface_xdg_toplevel_api API + * See @ref iface_xdg_toplevel. + */ +/** + * @defgroup iface_xdg_toplevel The xdg_toplevel interface + * + * This interface defines an xdg_surface role which allows a surface to, + * among other things, set window-like properties such as maximize, + * fullscreen, and minimize, set application-specific metadata like title and + * id, and well as trigger user interactive operations such as interactive + * resize and move. + * + * Unmapping an xdg_toplevel means that the surface cannot be shown + * by the compositor until it is explicitly mapped again. + * All active operations (e.g., move, resize) are canceled and all + * attributes (e.g. title, state, stacking, ...) are discarded for + * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + * the state it had right after xdg_surface.get_toplevel. The client + * can re-map the toplevel by perfoming a commit without any buffer + * attached, waiting for a configure event and handling it as usual (see + * xdg_surface description). + * + * Attaching a null buffer to a toplevel unmaps the surface. + */ +extern const struct wl_interface xdg_toplevel_interface; +#endif +#ifndef XDG_POPUP_INTERFACE +#define XDG_POPUP_INTERFACE +/** + * @page page_iface_xdg_popup xdg_popup + * @section page_iface_xdg_popup_desc Description + * + * A popup surface is a short-lived, temporary surface. It can be used to + * implement for example menus, popovers, tooltips and other similar user + * interface concepts. + * + * A popup can be made to take an explicit grab. See xdg_popup.grab for + * details. + * + * When the popup is dismissed, a popup_done event will be sent out, and at + * the same time the surface will be unmapped. See the xdg_popup.popup_done + * event for details. + * + * Explicitly destroying the xdg_popup object will also dismiss the popup and + * unmap the surface. Clients that want to dismiss the popup when another + * surface of their own is clicked should dismiss the popup using the destroy + * request. + * + * A newly created xdg_popup will be stacked on top of all previously created + * xdg_popup surfaces associated with the same xdg_toplevel. + * + * The parent of an xdg_popup must be mapped (see the xdg_surface + * description) before the xdg_popup itself. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_popup state to take effect. + * @section page_iface_xdg_popup_api API + * See @ref iface_xdg_popup. + */ +/** + * @defgroup iface_xdg_popup The xdg_popup interface + * + * A popup surface is a short-lived, temporary surface. It can be used to + * implement for example menus, popovers, tooltips and other similar user + * interface concepts. + * + * A popup can be made to take an explicit grab. See xdg_popup.grab for + * details. + * + * When the popup is dismissed, a popup_done event will be sent out, and at + * the same time the surface will be unmapped. See the xdg_popup.popup_done + * event for details. + * + * Explicitly destroying the xdg_popup object will also dismiss the popup and + * unmap the surface. Clients that want to dismiss the popup when another + * surface of their own is clicked should dismiss the popup using the destroy + * request. + * + * A newly created xdg_popup will be stacked on top of all previously created + * xdg_popup surfaces associated with the same xdg_toplevel. + * + * The parent of an xdg_popup must be mapped (see the xdg_surface + * description) before the xdg_popup itself. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_popup state to take effect. + */ +extern const struct wl_interface xdg_popup_interface; +#endif + +#ifndef XDG_WM_BASE_ERROR_ENUM +#define XDG_WM_BASE_ERROR_ENUM +enum xdg_wm_base_error { + /** + * given wl_surface has another role + */ + XDG_WM_BASE_ERROR_ROLE = 0, + /** + * xdg_wm_base was destroyed before children + */ + XDG_WM_BASE_ERROR_DEFUNCT_SURFACES = 1, + /** + * the client tried to map or destroy a non-topmost popup + */ + XDG_WM_BASE_ERROR_NOT_THE_TOPMOST_POPUP = 2, + /** + * the client specified an invalid popup parent surface + */ + XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT = 3, + /** + * the client provided an invalid surface state + */ + XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE = 4, + /** + * the client provided an invalid positioner + */ + XDG_WM_BASE_ERROR_INVALID_POSITIONER = 5, + /** + * the client didnā€™t respond to a ping event in time + */ + XDG_WM_BASE_ERROR_UNRESPONSIVE = 6, +}; +#endif /* XDG_WM_BASE_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_wm_base + * @struct xdg_wm_base_listener + */ +struct xdg_wm_base_listener { + /** + * check if the client is alive + * + * The ping event asks the client if it's still alive. Pass the + * serial specified in the event back to the compositor by sending + * a "pong" request back with the specified serial. See + * xdg_wm_base.pong. + * + * Compositors can use this to determine if the client is still + * alive. It's unspecified what will happen if the client doesn't + * respond to the ping request, or in what timeframe. Clients + * should try to respond in a reasonable amount of time. The + * ā€œunresponsiveā€ error is provided for compositors that wish + * to disconnect unresponsive clients. + * + * A compositor is free to ping in any way it wants, but a client + * must always respond to any xdg_wm_base object it created. + * @param serial pass this to the pong request + */ + void (*ping)(void *data, + struct xdg_wm_base *xdg_wm_base, + uint32_t serial); +}; + +/** + * @ingroup iface_xdg_wm_base + */ +static inline int +xdg_wm_base_add_listener(struct xdg_wm_base *xdg_wm_base, + const struct xdg_wm_base_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_wm_base, + (void (**)(void)) listener, data); +} + +#define XDG_WM_BASE_DESTROY 0 +#define XDG_WM_BASE_CREATE_POSITIONER 1 +#define XDG_WM_BASE_GET_XDG_SURFACE 2 +#define XDG_WM_BASE_PONG 3 + +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_PING_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_CREATE_POSITIONER_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_PONG_SINCE_VERSION 1 + +/** @ingroup iface_xdg_wm_base */ +static inline void +xdg_wm_base_set_user_data(struct xdg_wm_base *xdg_wm_base, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_wm_base, user_data); +} + +/** @ingroup iface_xdg_wm_base */ +static inline void * +xdg_wm_base_get_user_data(struct xdg_wm_base *xdg_wm_base) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_wm_base); +} + +static inline uint32_t +xdg_wm_base_get_version(struct xdg_wm_base *xdg_wm_base) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_wm_base); +} + +/** + * @ingroup iface_xdg_wm_base + * + * Destroy this xdg_wm_base object. + * + * Destroying a bound xdg_wm_base object while there are surfaces + * still alive created by this xdg_wm_base object instance is illegal + * and will result in a defunct_surfaces error. + */ +static inline void +xdg_wm_base_destroy(struct xdg_wm_base *xdg_wm_base) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_wm_base + * + * Create a positioner object. A positioner object is used to position + * surfaces relative to some parent surface. See the interface description + * and xdg_surface.get_popup for details. + */ +static inline struct xdg_positioner * +xdg_wm_base_create_positioner(struct xdg_wm_base *xdg_wm_base) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_CREATE_POSITIONER, &xdg_positioner_interface, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, NULL); + + return (struct xdg_positioner *) id; +} + +/** + * @ingroup iface_xdg_wm_base + * + * This creates an xdg_surface for the given surface. While xdg_surface + * itself is not a role, the corresponding surface may only be assigned + * a role extending xdg_surface, such as xdg_toplevel or xdg_popup. It is + * illegal to create an xdg_surface for a wl_surface which already has an + * assigned role and this will result in a role error. + * + * This creates an xdg_surface for the given surface. An xdg_surface is + * used as basis to define a role to a given surface, such as xdg_toplevel + * or xdg_popup. It also manages functionality shared between xdg_surface + * based surface roles. + * + * See the documentation of xdg_surface for more details about what an + * xdg_surface is and how it is used. + */ +static inline struct xdg_surface * +xdg_wm_base_get_xdg_surface(struct xdg_wm_base *xdg_wm_base, struct wl_surface *surface) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_GET_XDG_SURFACE, &xdg_surface_interface, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, NULL, surface); + + return (struct xdg_surface *) id; +} + +/** + * @ingroup iface_xdg_wm_base + * + * A client must respond to a ping event with a pong request or + * the client may be deemed unresponsive. See xdg_wm_base.ping + * and xdg_wm_base.error.unresponsive. + */ +static inline void +xdg_wm_base_pong(struct xdg_wm_base *xdg_wm_base, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_PONG, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, serial); +} + +#ifndef XDG_POSITIONER_ERROR_ENUM +#define XDG_POSITIONER_ERROR_ENUM +enum xdg_positioner_error { + /** + * invalid input provided + */ + XDG_POSITIONER_ERROR_INVALID_INPUT = 0, +}; +#endif /* XDG_POSITIONER_ERROR_ENUM */ + +#ifndef XDG_POSITIONER_ANCHOR_ENUM +#define XDG_POSITIONER_ANCHOR_ENUM +enum xdg_positioner_anchor { + XDG_POSITIONER_ANCHOR_NONE = 0, + XDG_POSITIONER_ANCHOR_TOP = 1, + XDG_POSITIONER_ANCHOR_BOTTOM = 2, + XDG_POSITIONER_ANCHOR_LEFT = 3, + XDG_POSITIONER_ANCHOR_RIGHT = 4, + XDG_POSITIONER_ANCHOR_TOP_LEFT = 5, + XDG_POSITIONER_ANCHOR_BOTTOM_LEFT = 6, + XDG_POSITIONER_ANCHOR_TOP_RIGHT = 7, + XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT = 8, +}; +#endif /* XDG_POSITIONER_ANCHOR_ENUM */ + +#ifndef XDG_POSITIONER_GRAVITY_ENUM +#define XDG_POSITIONER_GRAVITY_ENUM +enum xdg_positioner_gravity { + XDG_POSITIONER_GRAVITY_NONE = 0, + XDG_POSITIONER_GRAVITY_TOP = 1, + XDG_POSITIONER_GRAVITY_BOTTOM = 2, + XDG_POSITIONER_GRAVITY_LEFT = 3, + XDG_POSITIONER_GRAVITY_RIGHT = 4, + XDG_POSITIONER_GRAVITY_TOP_LEFT = 5, + XDG_POSITIONER_GRAVITY_BOTTOM_LEFT = 6, + XDG_POSITIONER_GRAVITY_TOP_RIGHT = 7, + XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT = 8, +}; +#endif /* XDG_POSITIONER_GRAVITY_ENUM */ + +#ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM +#define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM +/** + * @ingroup iface_xdg_positioner + * constraint adjustments + * + * The constraint adjustment value define ways the compositor will adjust + * the position of the surface, if the unadjusted position would result + * in the surface being partly constrained. + * + * Whether a surface is considered 'constrained' is left to the compositor + * to determine. For example, the surface may be partly outside the + * compositor's defined 'work area', thus necessitating the child surface's + * position be adjusted until it is entirely inside the work area. + * + * The adjustments can be combined, according to a defined precedence: 1) + * Flip, 2) Slide, 3) Resize. + */ +enum xdg_positioner_constraint_adjustment { + /** + * don't move the child surface when constrained + * + * Don't alter the surface position even if it is constrained on + * some axis, for example partially outside the edge of an output. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE = 0, + /** + * move along the x axis until unconstrained + * + * Slide the surface along the x axis until it is no longer + * constrained. + * + * First try to slide towards the direction of the gravity on the x + * axis until either the edge in the opposite direction of the + * gravity is unconstrained or the edge in the direction of the + * gravity is constrained. + * + * Then try to slide towards the opposite direction of the gravity + * on the x axis until either the edge in the direction of the + * gravity is unconstrained or the edge in the opposite direction + * of the gravity is constrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X = 1, + /** + * move along the y axis until unconstrained + * + * Slide the surface along the y axis until it is no longer + * constrained. + * + * First try to slide towards the direction of the gravity on the y + * axis until either the edge in the opposite direction of the + * gravity is unconstrained or the edge in the direction of the + * gravity is constrained. + * + * Then try to slide towards the opposite direction of the gravity + * on the y axis until either the edge in the direction of the + * gravity is unconstrained or the edge in the opposite direction + * of the gravity is constrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y = 2, + /** + * invert the anchor and gravity on the x axis + * + * Invert the anchor and gravity on the x axis if the surface is + * constrained on the x axis. For example, if the left edge of the + * surface is constrained, the gravity is 'left' and the anchor is + * 'left', change the gravity to 'right' and the anchor to 'right'. + * + * If the adjusted position also ends up being constrained, the + * resulting position of the flip_x adjustment will be the one + * before the adjustment. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X = 4, + /** + * invert the anchor and gravity on the y axis + * + * Invert the anchor and gravity on the y axis if the surface is + * constrained on the y axis. For example, if the bottom edge of + * the surface is constrained, the gravity is 'bottom' and the + * anchor is 'bottom', change the gravity to 'top' and the anchor + * to 'top'. + * + * The adjusted position is calculated given the original anchor + * rectangle and offset, but with the new flipped anchor and + * gravity values. + * + * If the adjusted position also ends up being constrained, the + * resulting position of the flip_y adjustment will be the one + * before the adjustment. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y = 8, + /** + * horizontally resize the surface + * + * Resize the surface horizontally so that it is completely + * unconstrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X = 16, + /** + * vertically resize the surface + * + * Resize the surface vertically so that it is completely + * unconstrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y = 32, +}; +#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM */ + +#define XDG_POSITIONER_DESTROY 0 +#define XDG_POSITIONER_SET_SIZE 1 +#define XDG_POSITIONER_SET_ANCHOR_RECT 2 +#define XDG_POSITIONER_SET_ANCHOR 3 +#define XDG_POSITIONER_SET_GRAVITY 4 +#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT 5 +#define XDG_POSITIONER_SET_OFFSET 6 +#define XDG_POSITIONER_SET_REACTIVE 7 +#define XDG_POSITIONER_SET_PARENT_SIZE 8 +#define XDG_POSITIONER_SET_PARENT_CONFIGURE 9 + + +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_ANCHOR_RECT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_ANCHOR_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_GRAVITY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_OFFSET_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION 3 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_PARENT_SIZE_SINCE_VERSION 3 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_PARENT_CONFIGURE_SINCE_VERSION 3 + +/** @ingroup iface_xdg_positioner */ +static inline void +xdg_positioner_set_user_data(struct xdg_positioner *xdg_positioner, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_positioner, user_data); +} + +/** @ingroup iface_xdg_positioner */ +static inline void * +xdg_positioner_get_user_data(struct xdg_positioner *xdg_positioner) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_positioner); +} + +static inline uint32_t +xdg_positioner_get_version(struct xdg_positioner *xdg_positioner) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_positioner); +} + +/** + * @ingroup iface_xdg_positioner + * + * Notify the compositor that the xdg_positioner will no longer be used. + */ +static inline void +xdg_positioner_destroy(struct xdg_positioner *xdg_positioner) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the size of the surface that is to be positioned with the positioner + * object. The size is in surface-local coordinates and corresponds to the + * window geometry. See xdg_surface.set_window_geometry. + * + * If a zero or negative size is set the invalid_input error is raised. + */ +static inline void +xdg_positioner_set_size(struct xdg_positioner *xdg_positioner, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, width, height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify the anchor rectangle within the parent surface that the child + * surface will be placed relative to. The rectangle is relative to the + * window geometry as defined by xdg_surface.set_window_geometry of the + * parent surface. + * + * When the xdg_positioner object is used to position a child surface, the + * anchor rectangle may not extend outside the window geometry of the + * positioned child's parent surface. + * + * If a negative size is set the invalid_input error is raised. + */ +static inline void +xdg_positioner_set_anchor_rect(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_ANCHOR_RECT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y, width, height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Defines the anchor point for the anchor rectangle. The specified anchor + * is used derive an anchor point that the child surface will be + * positioned relative to. If a corner anchor is set (e.g. 'top_left' or + * 'bottom_right'), the anchor point will be at the specified corner; + * otherwise, the derived anchor point will be centered on the specified + * edge, or in the center of the anchor rectangle if no edge is specified. + */ +static inline void +xdg_positioner_set_anchor(struct xdg_positioner *xdg_positioner, uint32_t anchor) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_ANCHOR, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, anchor); +} + +/** + * @ingroup iface_xdg_positioner + * + * Defines in what direction a surface should be positioned, relative to + * the anchor point of the parent surface. If a corner gravity is + * specified (e.g. 'bottom_right' or 'top_left'), then the child surface + * will be placed towards the specified gravity; otherwise, the child + * surface will be centered over the anchor point on any axis that had no + * gravity specified. If the gravity is not in the ā€˜gravityā€™ enum, an + * invalid_input error is raised. + */ +static inline void +xdg_positioner_set_gravity(struct xdg_positioner *xdg_positioner, uint32_t gravity) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_GRAVITY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, gravity); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify how the window should be positioned if the originally intended + * position caused the surface to be constrained, meaning at least + * partially outside positioning boundaries set by the compositor. The + * adjustment is set by constructing a bitmask describing the adjustment to + * be made when the surface is constrained on that axis. + * + * If no bit for one axis is set, the compositor will assume that the child + * surface should not change its position on that axis when constrained. + * + * If more than one bit for one axis is set, the order of how adjustments + * are applied is specified in the corresponding adjustment descriptions. + * + * The default adjustment is none. + */ +static inline void +xdg_positioner_set_constraint_adjustment(struct xdg_positioner *xdg_positioner, uint32_t constraint_adjustment) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, constraint_adjustment); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify the surface position offset relative to the position of the + * anchor on the anchor rectangle and the anchor on the surface. For + * example if the anchor of the anchor rectangle is at (x, y), the surface + * has the gravity bottom|right, and the offset is (ox, oy), the calculated + * surface position will be (x + ox, y + oy). The offset position of the + * surface is the one used for constraint testing. See + * set_constraint_adjustment. + * + * An example use case is placing a popup menu on top of a user interface + * element, while aligning the user interface element of the parent surface + * with some user interface element placed somewhere in the popup surface. + */ +static inline void +xdg_positioner_set_offset(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_OFFSET, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y); +} + +/** + * @ingroup iface_xdg_positioner + * + * When set reactive, the surface is reconstrained if the conditions used + * for constraining changed, e.g. the parent window moved. + * + * If the conditions changed and the popup was reconstrained, an + * xdg_popup.configure event is sent with updated geometry, followed by an + * xdg_surface.configure event. + */ +static inline void +xdg_positioner_set_reactive(struct xdg_positioner *xdg_positioner) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_REACTIVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the parent window geometry the compositor should use when + * positioning the popup. The compositor may use this information to + * determine the future state the popup should be constrained using. If + * this doesn't match the dimension of the parent the popup is eventually + * positioned against, the behavior is undefined. + * + * The arguments are given in the surface-local coordinate space. + */ +static inline void +xdg_positioner_set_parent_size(struct xdg_positioner *xdg_positioner, int32_t parent_width, int32_t parent_height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_PARENT_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, parent_width, parent_height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the serial of an xdg_surface.configure event this positioner will be + * used in response to. The compositor may use this information together + * with set_parent_size to determine what future state the popup should be + * constrained using. + */ +static inline void +xdg_positioner_set_parent_configure(struct xdg_positioner *xdg_positioner, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_PARENT_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, serial); +} + +#ifndef XDG_SURFACE_ERROR_ENUM +#define XDG_SURFACE_ERROR_ENUM +enum xdg_surface_error { + /** + * Surface was not fully constructed + */ + XDG_SURFACE_ERROR_NOT_CONSTRUCTED = 1, + /** + * Surface was already constructed + */ + XDG_SURFACE_ERROR_ALREADY_CONSTRUCTED = 2, + /** + * Attaching a buffer to an unconfigured surface + */ + XDG_SURFACE_ERROR_UNCONFIGURED_BUFFER = 3, + /** + * Invalid serial number when acking a configure event + */ + XDG_SURFACE_ERROR_INVALID_SERIAL = 4, + /** + * Width or height was zero or negative + */ + XDG_SURFACE_ERROR_INVALID_SIZE = 5, + /** + * Surface was destroyed before its role object + */ + XDG_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 6, +}; +#endif /* XDG_SURFACE_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_surface + * @struct xdg_surface_listener + */ +struct xdg_surface_listener { + /** + * suggest a surface change + * + * The configure event marks the end of a configure sequence. A + * configure sequence is a set of one or more events configuring + * the state of the xdg_surface, including the final + * xdg_surface.configure event. + * + * Where applicable, xdg_surface surface roles will during a + * configure sequence extend this event as a latched state sent as + * events before the xdg_surface.configure event. Such events + * should be considered to make up a set of atomically applied + * configuration states, where the xdg_surface.configure commits + * the accumulated state. + * + * Clients should arrange their surface for the new states, and + * then send an ack_configure request with the serial sent in this + * configure event at some point before committing the new surface. + * + * If the client receives multiple configure events before it can + * respond to one, it is free to discard all but the last event it + * received. + * @param serial serial of the configure event + */ + void (*configure)(void *data, + struct xdg_surface *xdg_surface, + uint32_t serial); +}; + +/** + * @ingroup iface_xdg_surface + */ +static inline int +xdg_surface_add_listener(struct xdg_surface *xdg_surface, + const struct xdg_surface_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_surface, + (void (**)(void)) listener, data); +} + +#define XDG_SURFACE_DESTROY 0 +#define XDG_SURFACE_GET_TOPLEVEL 1 +#define XDG_SURFACE_GET_POPUP 2 +#define XDG_SURFACE_SET_WINDOW_GEOMETRY 3 +#define XDG_SURFACE_ACK_CONFIGURE 4 + +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_CONFIGURE_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_GET_TOPLEVEL_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_GET_POPUP_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_SET_WINDOW_GEOMETRY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_ACK_CONFIGURE_SINCE_VERSION 1 + +/** @ingroup iface_xdg_surface */ +static inline void +xdg_surface_set_user_data(struct xdg_surface *xdg_surface, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_surface, user_data); +} + +/** @ingroup iface_xdg_surface */ +static inline void * +xdg_surface_get_user_data(struct xdg_surface *xdg_surface) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_surface); +} + +static inline uint32_t +xdg_surface_get_version(struct xdg_surface *xdg_surface) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_surface); +} + +/** + * @ingroup iface_xdg_surface + * + * Destroy the xdg_surface object. An xdg_surface must only be destroyed + * after its role object has been destroyed, otherwise + * a defunct_role_object error is raised. + */ +static inline void +xdg_surface_destroy(struct xdg_surface *xdg_surface) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_surface + * + * This creates an xdg_toplevel object for the given xdg_surface and gives + * the associated wl_surface the xdg_toplevel role. + * + * See the documentation of xdg_toplevel for more details about what an + * xdg_toplevel is and how it is used. + */ +static inline struct xdg_toplevel * +xdg_surface_get_toplevel(struct xdg_surface *xdg_surface) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_GET_TOPLEVEL, &xdg_toplevel_interface, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, NULL); + + return (struct xdg_toplevel *) id; +} + +/** + * @ingroup iface_xdg_surface + * + * This creates an xdg_popup object for the given xdg_surface and gives + * the associated wl_surface the xdg_popup role. + * + * If null is passed as a parent, a parent surface must be specified using + * some other protocol, before committing the initial state. + * + * See the documentation of xdg_popup for more details about what an + * xdg_popup is and how it is used. + */ +static inline struct xdg_popup * +xdg_surface_get_popup(struct xdg_surface *xdg_surface, struct xdg_surface *parent, struct xdg_positioner *positioner) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_GET_POPUP, &xdg_popup_interface, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, NULL, parent, positioner); + + return (struct xdg_popup *) id; +} + +/** + * @ingroup iface_xdg_surface + * + * The window geometry of a surface is its "visible bounds" from the + * user's perspective. Client-side decorations often have invisible + * portions like drop-shadows which should be ignored for the + * purposes of aligning, placing and constraining windows. + * + * The window geometry is double buffered, and will be applied at the + * time wl_surface.commit of the corresponding wl_surface is called. + * + * When maintaining a position, the compositor should treat the (x, y) + * coordinate of the window geometry as the top left corner of the window. + * A client changing the (x, y) window geometry coordinate should in + * general not alter the position of the window. + * + * Once the window geometry of the surface is set, it is not possible to + * unset it, and it will remain the same until set_window_geometry is + * called again, even if a new subsurface or buffer is attached. + * + * If never set, the value is the full bounds of the surface, + * including any subsurfaces. This updates dynamically on every + * commit. This unset is meant for extremely simple clients. + * + * The arguments are given in the surface-local coordinate space of + * the wl_surface associated with this xdg_surface. + * + * The width and height must be greater than zero. Setting an invalid size + * will raise an invalid_size error. When applied, the effective window + * geometry will be the set window geometry clamped to the bounding + * rectangle of the combined geometry of the surface of the xdg_surface and + * the associated subsurfaces. + */ +static inline void +xdg_surface_set_window_geometry(struct xdg_surface *xdg_surface, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_SET_WINDOW_GEOMETRY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, x, y, width, height); +} + +/** + * @ingroup iface_xdg_surface + * + * When a configure event is received, if a client commits the + * surface in response to the configure event, then the client + * must make an ack_configure request sometime before the commit + * request, passing along the serial of the configure event. + * + * For instance, for toplevel surfaces the compositor might use this + * information to move a surface to the top left only when the client has + * drawn itself for the maximized or fullscreen state. + * + * If the client receives multiple configure events before it + * can respond to one, it only has to ack the last configure event. + * Acking a configure event that was never sent raises an invalid_serial + * error. + * + * A client is not required to commit immediately after sending + * an ack_configure request - it may even ack_configure several times + * before its next surface commit. + * + * A client may send multiple ack_configure requests before committing, but + * only the last request sent before a commit indicates which configure + * event the client really is responding to. + * + * Sending an ack_configure request consumes the serial number sent with + * the request, as well as serial numbers sent by all configure events + * sent on this xdg_surface prior to the configure event referenced by + * the committed serial. + * + * It is an error to issue multiple ack_configure requests referencing a + * serial from the same configure event, or to issue an ack_configure + * request referencing a serial from a configure event issued before the + * event identified by the last ack_configure request for the same + * xdg_surface. Doing so will raise an invalid_serial error. + */ +static inline void +xdg_surface_ack_configure(struct xdg_surface *xdg_surface, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_ACK_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, serial); +} + +#ifndef XDG_TOPLEVEL_ERROR_ENUM +#define XDG_TOPLEVEL_ERROR_ENUM +enum xdg_toplevel_error { + /** + * provided value is not a valid variant of the resize_edge enum + */ + XDG_TOPLEVEL_ERROR_INVALID_RESIZE_EDGE = 0, + /** + * invalid parent toplevel + */ + XDG_TOPLEVEL_ERROR_INVALID_PARENT = 1, + /** + * client provided an invalid min or max size + */ + XDG_TOPLEVEL_ERROR_INVALID_SIZE = 2, +}; +#endif /* XDG_TOPLEVEL_ERROR_ENUM */ + +#ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM +#define XDG_TOPLEVEL_RESIZE_EDGE_ENUM +/** + * @ingroup iface_xdg_toplevel + * edge values for resizing + * + * These values are used to indicate which edge of a surface + * is being dragged in a resize operation. + */ +enum xdg_toplevel_resize_edge { + XDG_TOPLEVEL_RESIZE_EDGE_NONE = 0, + XDG_TOPLEVEL_RESIZE_EDGE_TOP = 1, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM = 2, + XDG_TOPLEVEL_RESIZE_EDGE_LEFT = 4, + XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT = 5, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT = 6, + XDG_TOPLEVEL_RESIZE_EDGE_RIGHT = 8, + XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT = 9, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT = 10, +}; +#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM */ + +#ifndef XDG_TOPLEVEL_STATE_ENUM +#define XDG_TOPLEVEL_STATE_ENUM +/** + * @ingroup iface_xdg_toplevel + * types of state on the surface + * + * The different state values used on the surface. This is designed for + * state values like maximized, fullscreen. It is paired with the + * configure event to ensure that both the client and the compositor + * setting the state can be synchronized. + * + * States set in this way are double-buffered. They will get applied on + * the next commit. + */ +enum xdg_toplevel_state { + /** + * the surface is maximized + * the surface is maximized + * + * The surface is maximized. The window geometry specified in the + * configure event must be obeyed by the client. + * + * The client should draw without shadow or other decoration + * outside of the window geometry. + */ + XDG_TOPLEVEL_STATE_MAXIMIZED = 1, + /** + * the surface is fullscreen + * the surface is fullscreen + * + * The surface is fullscreen. The window geometry specified in + * the configure event is a maximum; the client cannot resize + * beyond it. For a surface to cover the whole fullscreened area, + * the geometry dimensions must be obeyed by the client. For more + * details, see xdg_toplevel.set_fullscreen. + */ + XDG_TOPLEVEL_STATE_FULLSCREEN = 2, + /** + * the surface is being resized + * the surface is being resized + * + * The surface is being resized. The window geometry specified in + * the configure event is a maximum; the client cannot resize + * beyond it. Clients that have aspect ratio or cell sizing + * configuration can use a smaller size, however. + */ + XDG_TOPLEVEL_STATE_RESIZING = 3, + /** + * the surface is now activated + * the surface is now activated + * + * Client window decorations should be painted as if the window + * is active. Do not assume this means that the window actually has + * keyboard or pointer focus. + */ + XDG_TOPLEVEL_STATE_ACTIVATED = 4, + /** + * the surfaceā€™s left edge is tiled + * + * The window is currently in a tiled layout and the left edge is + * considered to be adjacent to another part of the tiling grid. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_LEFT = 5, + /** + * the surfaceā€™s right edge is tiled + * + * The window is currently in a tiled layout and the right edge + * is considered to be adjacent to another part of the tiling grid. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_RIGHT = 6, + /** + * the surfaceā€™s top edge is tiled + * + * The window is currently in a tiled layout and the top edge is + * considered to be adjacent to another part of the tiling grid. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_TOP = 7, + /** + * the surfaceā€™s bottom edge is tiled + * + * The window is currently in a tiled layout and the bottom edge + * is considered to be adjacent to another part of the tiling grid. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_BOTTOM = 8, +}; +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_RIGHT_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_TOP_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_BOTTOM_SINCE_VERSION 2 +#endif /* XDG_TOPLEVEL_STATE_ENUM */ + +#ifndef XDG_TOPLEVEL_WM_CAPABILITIES_ENUM +#define XDG_TOPLEVEL_WM_CAPABILITIES_ENUM +enum xdg_toplevel_wm_capabilities { + /** + * show_window_menu is available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU = 1, + /** + * set_maximized and unset_maximized are available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE = 2, + /** + * set_fullscreen and unset_fullscreen are available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN = 3, + /** + * set_minimized is available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE = 4, +}; +#endif /* XDG_TOPLEVEL_WM_CAPABILITIES_ENUM */ + +/** + * @ingroup iface_xdg_toplevel + * @struct xdg_toplevel_listener + */ +struct xdg_toplevel_listener { + /** + * suggest a surface change + * + * This configure event asks the client to resize its toplevel + * surface or to change its state. The configured state should not + * be applied immediately. See xdg_surface.configure for details. + * + * The width and height arguments specify a hint to the window + * about how its surface should be resized in window geometry + * coordinates. See set_window_geometry. + * + * If the width or height arguments are zero, it means the client + * should decide its own window dimension. This may happen when the + * compositor needs to configure the state of the surface but + * doesn't have any information about any previous or expected + * dimension. + * + * The states listed in the event specify how the width/height + * arguments should be interpreted, and possibly how it should be + * drawn. + * + * Clients must send an ack_configure in response to this event. + * See xdg_surface.configure and xdg_surface.ack_configure for + * details. + */ + void (*configure)(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, + int32_t height, + struct wl_array *states); + /** + * surface wants to be closed + * + * The close event is sent by the compositor when the user wants + * the surface to be closed. This should be equivalent to the user + * clicking the close button in client-side decorations, if your + * application has any. + * + * This is only a request that the user intends to close the + * window. The client may choose to ignore this request, or show a + * dialog to ask the user to save their data, etc. + */ + void (*close)(void *data, + struct xdg_toplevel *xdg_toplevel); + /** + * recommended window geometry bounds + * + * The configure_bounds event may be sent prior to a + * xdg_toplevel.configure event to communicate the bounds a window + * geometry size is recommended to constrain to. + * + * The passed width and height are in surface coordinate space. If + * width and height are 0, it means bounds is unknown and + * equivalent to as if no configure_bounds event was ever sent for + * this surface. + * + * The bounds can for example correspond to the size of a monitor + * excluding any panels or other shell components, so that a + * surface isn't created in a way that it cannot fit. + * + * The bounds may change at any point, and in such a case, a new + * xdg_toplevel.configure_bounds will be sent, followed by + * xdg_toplevel.configure and xdg_surface.configure. + * @since 4 + */ + void (*configure_bounds)(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, + int32_t height); + /** + * compositor capabilities + * + * This event advertises the capabilities supported by the + * compositor. If a capability isn't supported, clients should hide + * or disable the UI elements that expose this functionality. For + * instance, if the compositor doesn't advertise support for + * minimized toplevels, a button triggering the set_minimized + * request should not be displayed. + * + * The compositor will ignore requests it doesn't support. For + * instance, a compositor which doesn't advertise support for + * minimized will ignore set_minimized requests. + * + * Compositors must send this event once before the first + * xdg_surface.configure event. When the capabilities change, + * compositors must send this event again and then send an + * xdg_surface.configure event. + * + * The configured state should not be applied immediately. See + * xdg_surface.configure for details. + * + * The capabilities are sent as an array of 32-bit unsigned + * integers in native endianness. + * @param capabilities array of 32-bit capabilities + * @since 5 + */ + void (*wm_capabilities)(void *data, + struct xdg_toplevel *xdg_toplevel, + struct wl_array *capabilities); +}; + +/** + * @ingroup iface_xdg_toplevel + */ +static inline int +xdg_toplevel_add_listener(struct xdg_toplevel *xdg_toplevel, + const struct xdg_toplevel_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_toplevel, + (void (**)(void)) listener, data); +} + +#define XDG_TOPLEVEL_DESTROY 0 +#define XDG_TOPLEVEL_SET_PARENT 1 +#define XDG_TOPLEVEL_SET_TITLE 2 +#define XDG_TOPLEVEL_SET_APP_ID 3 +#define XDG_TOPLEVEL_SHOW_WINDOW_MENU 4 +#define XDG_TOPLEVEL_MOVE 5 +#define XDG_TOPLEVEL_RESIZE 6 +#define XDG_TOPLEVEL_SET_MAX_SIZE 7 +#define XDG_TOPLEVEL_SET_MIN_SIZE 8 +#define XDG_TOPLEVEL_SET_MAXIMIZED 9 +#define XDG_TOPLEVEL_UNSET_MAXIMIZED 10 +#define XDG_TOPLEVEL_SET_FULLSCREEN 11 +#define XDG_TOPLEVEL_UNSET_FULLSCREEN 12 +#define XDG_TOPLEVEL_SET_MINIMIZED 13 + +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CLOSE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION 4 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION 5 + +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_PARENT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_TITLE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_APP_ID_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SHOW_WINDOW_MENU_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_MOVE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_RESIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MAX_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MIN_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MAXIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_UNSET_MAXIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_FULLSCREEN_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_UNSET_FULLSCREEN_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MINIMIZED_SINCE_VERSION 1 + +/** @ingroup iface_xdg_toplevel */ +static inline void +xdg_toplevel_set_user_data(struct xdg_toplevel *xdg_toplevel, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_toplevel, user_data); +} + +/** @ingroup iface_xdg_toplevel */ +static inline void * +xdg_toplevel_get_user_data(struct xdg_toplevel *xdg_toplevel) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_toplevel); +} + +static inline uint32_t +xdg_toplevel_get_version(struct xdg_toplevel *xdg_toplevel) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_toplevel); +} + +/** + * @ingroup iface_xdg_toplevel + * + * This request destroys the role surface and unmaps the surface; + * see "Unmapping" behavior in interface section for details. + */ +static inline void +xdg_toplevel_destroy(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set the "parent" of this surface. This surface should be stacked + * above the parent surface and all other ancestor surfaces. + * + * Parent surfaces should be set on dialogs, toolboxes, or other + * "auxiliary" surfaces, so that the parent is raised when the dialog + * is raised. + * + * Setting a null parent for a child surface unsets its parent. Setting + * a null parent for a surface which currently has no parent is a no-op. + * + * Only mapped surfaces can have child surfaces. Setting a parent which + * is not mapped is equivalent to setting a null parent. If a surface + * becomes unmapped, its children's parent is set to the parent of + * the now-unmapped surface. If the now-unmapped surface has no parent, + * its children's parent is unset. If the now-unmapped surface becomes + * mapped again, its parent-child relationship is not restored. + * + * The parent toplevel must not be one of the child toplevel's + * descendants, and the parent must be different from the child toplevel, + * otherwise the invalid_parent protocol error is raised. + */ +static inline void +xdg_toplevel_set_parent(struct xdg_toplevel *xdg_toplevel, struct xdg_toplevel *parent) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_PARENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, parent); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a short title for the surface. + * + * This string may be used to identify the surface in a task bar, + * window list, or other user interface elements provided by the + * compositor. + * + * The string must be encoded in UTF-8. + */ +static inline void +xdg_toplevel_set_title(struct xdg_toplevel *xdg_toplevel, const char *title) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_TITLE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, title); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set an application identifier for the surface. + * + * The app ID identifies the general class of applications to which + * the surface belongs. The compositor can use this to group multiple + * surfaces together, or to determine how to launch a new application. + * + * For D-Bus activatable applications, the app ID is used as the D-Bus + * service name. + * + * The compositor shell will try to group application surfaces together + * by their app ID. As a best practice, it is suggested to select app + * ID's that match the basename of the application's .desktop file. + * For example, "org.freedesktop.FooViewer" where the .desktop file is + * "org.freedesktop.FooViewer.desktop". + * + * Like other properties, a set_app_id request can be sent after the + * xdg_toplevel has been mapped to update the property. + * + * See the desktop-entry specification [0] for more details on + * application identifiers and how they relate to well-known D-Bus + * names and .desktop files. + * + * [0] https://standards.freedesktop.org/desktop-entry-spec/ + */ +static inline void +xdg_toplevel_set_app_id(struct xdg_toplevel *xdg_toplevel, const char *app_id) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_APP_ID, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, app_id); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Clients implementing client-side decorations might want to show + * a context menu when right-clicking on the decorations, giving the + * user a menu that they can use to maximize or minimize the window. + * + * This request asks the compositor to pop up such a window menu at + * the given position, relative to the local surface coordinates of + * the parent surface. There are no guarantees as to what menu items + * the window menu contains, or even if a window menu will be drawn + * at all. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. + */ +static inline void +xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, int32_t x, int32_t y) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SHOW_WINDOW_MENU, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, x, y); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Start an interactive, user-driven move of the surface. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. The passed + * serial is used to determine the type of interactive move (touch, + * pointer, etc). + * + * The server may ignore move requests depending on the state of + * the surface (e.g. fullscreen or maximized), or if the passed serial + * is no longer valid. + * + * If triggered, the surface will lose the focus of the device + * (wl_pointer, wl_touch, etc) used for the move. It is up to the + * compositor to visually indicate that the move is taking place, such as + * updating a pointer cursor, during the move. There is no guarantee + * that the device focus will return when the move is completed. + */ +static inline void +xdg_toplevel_move(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_MOVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Start a user-driven, interactive resize of the surface. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. The passed + * serial is used to determine the type of interactive resize (touch, + * pointer, etc). + * + * The server may ignore resize requests depending on the state of + * the surface (e.g. fullscreen or maximized). + * + * If triggered, the client will receive configure events with the + * "resize" state enum value and the expected sizes. See the "resize" + * enum value for more details about what is required. The client + * must also acknowledge configure events using "ack_configure". After + * the resize is completed, the client will receive another "configure" + * event without the resize state. + * + * If triggered, the surface also will lose the focus of the device + * (wl_pointer, wl_touch, etc) used for the resize. It is up to the + * compositor to visually indicate that the resize is taking place, + * such as updating a pointer cursor, during the resize. There is no + * guarantee that the device focus will return when the resize is + * completed. + * + * The edges parameter specifies how the surface should be resized, and + * is one of the values of the resize_edge enum. Values not matching + * a variant of the enum will cause a protocol error. The compositor + * may use this information to update the surface position for example + * when dragging the top left corner. The compositor may also use + * this information to adapt its behavior, e.g. choose an appropriate + * cursor image. + */ +static inline void +xdg_toplevel_resize(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, uint32_t edges) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_RESIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, edges); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a maximum size for the window. + * + * The client can specify a maximum size so that the compositor does + * not try to configure the window beyond this size. + * + * The width and height arguments are in window geometry coordinates. + * See xdg_surface.set_window_geometry. + * + * Values set in this way are double-buffered. They will get applied + * on the next commit. + * + * The compositor can use this information to allow or disallow + * different states like maximize or fullscreen and draw accurate + * animations. + * + * Similarly, a tiling window manager may use this information to + * place and resize client windows in a more effective way. + * + * The client should not rely on the compositor to obey the maximum + * size. The compositor may decide to ignore the values set by the + * client and request a larger size. + * + * If never set, or a value of zero in the request, means that the + * client has no expected maximum size in the given dimension. + * As a result, a client wishing to reset the maximum size + * to an unspecified state can use zero for width and height in the + * request. + * + * Requesting a maximum size to be smaller than the minimum size of + * a surface is illegal and will result in an invalid_size error. + * + * The width and height must be greater than or equal to zero. Using + * strictly negative values for width or height will result in a + * invalid_size error. + */ +static inline void +xdg_toplevel_set_max_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MAX_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a minimum size for the window. + * + * The client can specify a minimum size so that the compositor does + * not try to configure the window below this size. + * + * The width and height arguments are in window geometry coordinates. + * See xdg_surface.set_window_geometry. + * + * Values set in this way are double-buffered. They will get applied + * on the next commit. + * + * The compositor can use this information to allow or disallow + * different states like maximize or fullscreen and draw accurate + * animations. + * + * Similarly, a tiling window manager may use this information to + * place and resize client windows in a more effective way. + * + * The client should not rely on the compositor to obey the minimum + * size. The compositor may decide to ignore the values set by the + * client and request a smaller size. + * + * If never set, or a value of zero in the request, means that the + * client has no expected minimum size in the given dimension. + * As a result, a client wishing to reset the minimum size + * to an unspecified state can use zero for width and height in the + * request. + * + * Requesting a minimum size to be larger than the maximum size of + * a surface is illegal and will result in an invalid_size error. + * + * The width and height must be greater than or equal to zero. Using + * strictly negative values for width and height will result in a + * invalid_size error. + */ +static inline void +xdg_toplevel_set_min_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MIN_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Maximize the surface. + * + * After requesting that the surface should be maximized, the compositor + * will respond by emitting a configure event. Whether this configure + * actually sets the window maximized is subject to compositor policies. + * The client must then update its content, drawing in the configured + * state. The client must also acknowledge the configure when committing + * the new content (see ack_configure). + * + * It is up to the compositor to decide how and where to maximize the + * surface, for example which output and what region of the screen should + * be used. + * + * If the surface was already maximized, the compositor will still emit + * a configure event with the "maximized" state. + * + * If the surface is in a fullscreen state, this request has no direct + * effect. It may alter the state the surface is returned to when + * unmaximized unless overridden by the compositor. + */ +static inline void +xdg_toplevel_set_maximized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Unmaximize the surface. + * + * After requesting that the surface should be unmaximized, the compositor + * will respond by emitting a configure event. Whether this actually + * un-maximizes the window is subject to compositor policies. + * If available and applicable, the compositor will include the window + * geometry dimensions the window had prior to being maximized in the + * configure event. The client must then update its content, drawing it in + * the configured state. The client must also acknowledge the configure + * when committing the new content (see ack_configure). + * + * It is up to the compositor to position the surface after it was + * unmaximized; usually the position the surface had before maximizing, if + * applicable. + * + * If the surface was already not maximized, the compositor will still + * emit a configure event without the "maximized" state. + * + * If the surface is in a fullscreen state, this request has no direct + * effect. It may alter the state the surface is returned to when + * unmaximized unless overridden by the compositor. + */ +static inline void +xdg_toplevel_unset_maximized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_UNSET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Make the surface fullscreen. + * + * After requesting that the surface should be fullscreened, the + * compositor will respond by emitting a configure event. Whether the + * client is actually put into a fullscreen state is subject to compositor + * policies. The client must also acknowledge the configure when + * committing the new content (see ack_configure). + * + * The output passed by the request indicates the client's preference as + * to which display it should be set fullscreen on. If this value is NULL, + * it's up to the compositor to choose which display will be used to map + * this surface. + * + * If the surface doesn't cover the whole output, the compositor will + * position the surface in the center of the output and compensate with + * with border fill covering the rest of the output. The content of the + * border fill is undefined, but should be assumed to be in some way that + * attempts to blend into the surrounding area (e.g. solid black). + * + * If the fullscreened surface is not opaque, the compositor must make + * sure that other screen content not part of the same surface tree (made + * up of subsurfaces, popups or similarly coupled surfaces) are not + * visible below the fullscreened surface. + */ +static inline void +xdg_toplevel_set_fullscreen(struct xdg_toplevel *xdg_toplevel, struct wl_output *output) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, output); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Make the surface no longer fullscreen. + * + * After requesting that the surface should be unfullscreened, the + * compositor will respond by emitting a configure event. + * Whether this actually removes the fullscreen state of the client is + * subject to compositor policies. + * + * Making a surface unfullscreen sets states for the surface based on the following: + * * the state(s) it may have had before becoming fullscreen + * * any state(s) decided by the compositor + * * any state(s) requested by the client while the surface was fullscreen + * + * The compositor may include the previous window geometry dimensions in + * the configure event, if applicable. + * + * The client must also acknowledge the configure when committing the new + * content (see ack_configure). + */ +static inline void +xdg_toplevel_unset_fullscreen(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_UNSET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Request that the compositor minimize your surface. There is no + * way to know if the surface is currently minimized, nor is there + * any way to unset minimization on this surface. + * + * If you are looking to throttle redrawing when minimized, please + * instead use the wl_surface.frame event for this, as this will + * also work with live previews on windows in Alt-Tab, Expose or + * similar compositor features. + */ +static inline void +xdg_toplevel_set_minimized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MINIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +#ifndef XDG_POPUP_ERROR_ENUM +#define XDG_POPUP_ERROR_ENUM +enum xdg_popup_error { + /** + * tried to grab after being mapped + */ + XDG_POPUP_ERROR_INVALID_GRAB = 0, +}; +#endif /* XDG_POPUP_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_popup + * @struct xdg_popup_listener + */ +struct xdg_popup_listener { + /** + * configure the popup surface + * + * This event asks the popup surface to configure itself given + * the configuration. The configured state should not be applied + * immediately. See xdg_surface.configure for details. + * + * The x and y arguments represent the position the popup was + * placed at given the xdg_positioner rule, relative to the upper + * left corner of the window geometry of the parent surface. + * + * For version 2 or older, the configure event for an xdg_popup is + * only ever sent once for the initial configuration. Starting with + * version 3, it may be sent again if the popup is setup with an + * xdg_positioner with set_reactive requested, or in response to + * xdg_popup.reposition requests. + * @param x x position relative to parent surface window geometry + * @param y y position relative to parent surface window geometry + * @param width window geometry width + * @param height window geometry height + */ + void (*configure)(void *data, + struct xdg_popup *xdg_popup, + int32_t x, + int32_t y, + int32_t width, + int32_t height); + /** + * popup interaction is done + * + * The popup_done event is sent out when a popup is dismissed by + * the compositor. The client should destroy the xdg_popup object + * at this point. + */ + void (*popup_done)(void *data, + struct xdg_popup *xdg_popup); + /** + * signal the completion of a repositioned request + * + * The repositioned event is sent as part of a popup + * configuration sequence, together with xdg_popup.configure and + * lastly xdg_surface.configure to notify the completion of a + * reposition request. + * + * The repositioned event is to notify about the completion of a + * xdg_popup.reposition request. The token argument is the token + * passed in the xdg_popup.reposition request. + * + * Immediately after this event is emitted, xdg_popup.configure and + * xdg_surface.configure will be sent with the updated size and + * position, as well as a new configure serial. + * + * The client should optionally update the content of the popup, + * but must acknowledge the new popup configuration for the new + * position to take effect. See xdg_surface.ack_configure for + * details. + * @param token reposition request token + * @since 3 + */ + void (*repositioned)(void *data, + struct xdg_popup *xdg_popup, + uint32_t token); +}; + +/** + * @ingroup iface_xdg_popup + */ +static inline int +xdg_popup_add_listener(struct xdg_popup *xdg_popup, + const struct xdg_popup_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_popup, + (void (**)(void)) listener, data); +} + +#define XDG_POPUP_DESTROY 0 +#define XDG_POPUP_GRAB 1 +#define XDG_POPUP_REPOSITION 2 + +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_POPUP_DONE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_REPOSITIONED_SINCE_VERSION 3 + +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_GRAB_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_REPOSITION_SINCE_VERSION 3 + +/** @ingroup iface_xdg_popup */ +static inline void +xdg_popup_set_user_data(struct xdg_popup *xdg_popup, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_popup, user_data); +} + +/** @ingroup iface_xdg_popup */ +static inline void * +xdg_popup_get_user_data(struct xdg_popup *xdg_popup) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_popup); +} + +static inline uint32_t +xdg_popup_get_version(struct xdg_popup *xdg_popup) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_popup); +} + +/** + * @ingroup iface_xdg_popup + * + * This destroys the popup. Explicitly destroying the xdg_popup + * object will also dismiss the popup, and unmap the surface. + * + * If this xdg_popup is not the "topmost" popup, a protocol error + * will be sent. + */ +static inline void +xdg_popup_destroy(struct xdg_popup *xdg_popup) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_popup + * + * This request makes the created popup take an explicit grab. An explicit + * grab will be dismissed when the user dismisses the popup, or when the + * client destroys the xdg_popup. This can be done by the user clicking + * outside the surface, using the keyboard, or even locking the screen + * through closing the lid or a timeout. + * + * If the compositor denies the grab, the popup will be immediately + * dismissed. + * + * This request must be used in response to some sort of user action like a + * button press, key press, or touch down event. The serial number of the + * event should be passed as 'serial'. + * + * The parent of a grabbing popup must either be an xdg_toplevel surface or + * another xdg_popup with an explicit grab. If the parent is another + * xdg_popup it means that the popups are nested, with this popup now being + * the topmost popup. + * + * Nested popups must be destroyed in the reverse order they were created + * in, e.g. the only popup you are allowed to destroy at all times is the + * topmost one. + * + * When compositors choose to dismiss a popup, they may dismiss every + * nested grabbing popup as well. When a compositor dismisses popups, it + * will follow the same dismissing order as required from the client. + * + * If the topmost grabbing popup is destroyed, the grab will be returned to + * the parent of the popup, if that parent previously had an explicit grab. + * + * If the parent is a grabbing popup which has already been dismissed, this + * popup will be immediately dismissed. If the parent is a popup that did + * not take an explicit grab, an error will be raised. + * + * During a popup grab, the client owning the grab will receive pointer + * and touch events for all their surfaces as normal (similar to an + * "owner-events" grab in X11 parlance), while the top most grabbing popup + * will always have keyboard focus. + */ +static inline void +xdg_popup_grab(struct xdg_popup *xdg_popup, struct wl_seat *seat, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_GRAB, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, seat, serial); +} + +/** + * @ingroup iface_xdg_popup + * + * Reposition an already-mapped popup. The popup will be placed given the + * details in the passed xdg_positioner object, and a + * xdg_popup.repositioned followed by xdg_popup.configure and + * xdg_surface.configure will be emitted in response. Any parameters set + * by the previous positioner will be discarded. + * + * The passed token will be sent in the corresponding + * xdg_popup.repositioned event. The new popup position will not take + * effect until the corresponding configure event is acknowledged by the + * client. See xdg_popup.repositioned for details. The token itself is + * opaque, and has no other special meaning. + * + * If multiple reposition requests are sent, the compositor may skip all + * but the last one. + * + * If the popup is repositioned in response to a configure event for its + * parent, the client should send an xdg_positioner.set_parent_configure + * and possibly an xdg_positioner.set_parent_size request to allow the + * compositor to properly constrain the popup. + * + * If the popup is repositioned together with a parent that is being + * resized, but not in response to a configure event, the client should + * send an xdg_positioner.set_parent_size request. + */ +static inline void +xdg_popup_reposition(struct xdg_popup *xdg_popup, struct xdg_positioner *positioner, uint32_t token) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_REPOSITION, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, positioner, token); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/gio/app/internal/wm/window.go b/gio/app/internal/wm/window.go new file mode 100644 index 0000000..82e3c38 --- /dev/null +++ b/gio/app/internal/wm/window.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// package wm implements platform specific windows +// and GPU contexts. +package wm + +import ( + "errors" + + "realy.lol/gio/gpu" + "realy.lol/gio/io/event" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/unit" +) + +type Size struct { + Width unit.Value + Height unit.Value +} + +type Options struct { + Size *Size + MinSize *Size + MaxSize *Size + Title *string + WindowMode *WindowMode +} + +type WindowMode uint8 + +const ( + Windowed WindowMode = iota + Fullscreen +) + +type FrameEvent struct { + system.FrameEvent + + Sync bool +} + +type Callbacks interface { + SetDriver(d Driver) + Event(e event.Event) +} + +type Context interface { + API() gpu.API + Present() error + MakeCurrent() error + Release() + Lock() + Unlock() +} + +// ErrDeviceLost is returned from Context.Present when +// the underlying GPU device is gone and should be +// recreated. +var ErrDeviceLost = errors.New("GPU device lost") + +// Driver is the interface for the platform implementation +// of a window. +type Driver interface { + // SetAnimating sets the animation flag. When the window is animating, + // FrameEvents are delivered as fast as the display can handle them. + SetAnimating(anim bool) + // ShowTextInput updates the virtual keyboard state. + ShowTextInput(show bool) + NewContext() (Context, error) + + // ReadClipboard requests the clipboard content. + ReadClipboard() + // WriteClipboard requests a clipboard write. + WriteClipboard(s string) + + // Option processes option changes. + Option(opts *Options) + + // SetCursor updates the current cursor to name. + SetCursor(name pointer.CursorName) + + // Close the window. + Close() +} + +type windowRendezvous struct { + in chan windowAndOptions + out chan windowAndOptions + errs chan error +} + +type windowAndOptions struct { + window Callbacks + opts *Options +} + +func newWindowRendezvous() *windowRendezvous { + wr := &windowRendezvous{ + in: make(chan windowAndOptions), + out: make(chan windowAndOptions), + errs: make(chan error), + } + go func() { + var main windowAndOptions + var out chan windowAndOptions + for { + select { + case w := <-wr.in: + var err error + if main.window != nil { + err = errors.New("multiple windows are not supported") + } + wr.errs <- err + main = w + out = wr.out + case out <- main: + } + } + }() + return wr +} diff --git a/gio/app/internal/xkb/xkb_unix.go b/gio/app/internal/xkb/xkb_unix.go new file mode 100644 index 0000000..be72a58 --- /dev/null +++ b/gio/app/internal/xkb/xkb_unix.go @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build (linux && !android) || freebsd || openbsd +// +build linux,!android freebsd openbsd + +// Package xkb implements a Go interface for the X Keyboard Extension library. +package xkb + +import ( + "errors" + "fmt" + "os" + "syscall" + "unicode" + "unicode/utf8" + "unsafe" + + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" +) + +/* +#cgo linux pkg-config: xkbcommon +#cgo freebsd openbsd CFLAGS: -I/usr/local/include +#cgo freebsd openbsd LDFLAGS: -L/usr/local/lib -lxkbcommon + +#include +#include +#include +*/ +import "C" + +type Context struct { + Ctx *C.struct_xkb_context + keyMap *C.struct_xkb_keymap + state *C.struct_xkb_state + compTable *C.struct_xkb_compose_table + compState *C.struct_xkb_compose_state + utf8Buf []byte +} + +var ( + _XKB_MOD_NAME_CTRL = []byte("Control\x00") + _XKB_MOD_NAME_SHIFT = []byte("Shift\x00") + _XKB_MOD_NAME_ALT = []byte("Mod1\x00") + _XKB_MOD_NAME_LOGO = []byte("Mod4\x00") +) + +func (x *Context) Destroy() { + if x.compState != nil { + C.xkb_compose_state_unref(x.compState) + x.compState = nil + } + if x.compTable != nil { + C.xkb_compose_table_unref(x.compTable) + x.compTable = nil + } + x.DestroyKeymapState() + if x.Ctx != nil { + C.xkb_context_unref(x.Ctx) + x.Ctx = nil + } +} + +func New() (*Context, error) { + ctx := &Context{ + Ctx: C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS), + } + if ctx.Ctx == nil { + return nil, errors.New("newXKB: xkb_context_new failed") + } + locale := os.Getenv("LC_ALL") + if locale == "" { + locale = os.Getenv("LC_CTYPE") + } + if locale == "" { + locale = os.Getenv("LANG") + } + if locale == "" { + locale = "C" + } + cloc := C.CString(locale) + defer C.free(unsafe.Pointer(cloc)) + ctx.compTable = C.xkb_compose_table_new_from_locale(ctx.Ctx, cloc, + C.XKB_COMPOSE_COMPILE_NO_FLAGS) + if ctx.compTable == nil { + ctx.Destroy() + return nil, errors.New("newXKB: xkb_compose_table_new_from_locale failed") + } + ctx.compState = C.xkb_compose_state_new(ctx.compTable, + C.XKB_COMPOSE_STATE_NO_FLAGS) + if ctx.compState == nil { + ctx.Destroy() + return nil, errors.New("newXKB: xkb_compose_state_new failed") + } + return ctx, nil +} + +func (x *Context) DestroyKeymapState() { + if x.state != nil { + C.xkb_state_unref(x.state) + x.state = nil + } + if x.keyMap != nil { + C.xkb_keymap_unref(x.keyMap) + x.keyMap = nil + } +} + +// SetKeymap sets the keymap and state. The context takes ownership of the +// keymap and state and frees them in Destroy. +func (x *Context) SetKeymap(xkbKeyMap, xkbState unsafe.Pointer) { + x.DestroyKeymapState() + x.keyMap = (*C.struct_xkb_keymap)(xkbKeyMap) + x.state = (*C.struct_xkb_state)(xkbState) +} + +func (x *Context) LoadKeymap(format int, fd int, size int) error { + x.DestroyKeymapState() + mapData, err := syscall.Mmap(int(fd), 0, int(size), syscall.PROT_READ, + syscall.MAP_SHARED) + if err != nil { + return fmt.Errorf("newXKB: mmap of keymap failed: %v", err) + } + defer syscall.Munmap(mapData) + keyMap := C.xkb_keymap_new_from_buffer(x.Ctx, + (*C.char)(unsafe.Pointer(&mapData[0])), C.size_t(size-1), + C.XKB_KEYMAP_FORMAT_TEXT_V1, C.XKB_KEYMAP_COMPILE_NO_FLAGS) + if keyMap == nil { + return errors.New("newXKB: xkb_keymap_new_from_buffer failed") + } + state := C.xkb_state_new(keyMap) + if state == nil { + C.xkb_keymap_unref(keyMap) + return errors.New("newXKB: xkb_state_new failed") + } + x.keyMap = keyMap + x.state = state + return nil +} + +func (x *Context) Modifiers() key.Modifiers { + var mods key.Modifiers + if C.xkb_state_mod_name_is_active(x.state, + (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_CTRL[0])), + C.XKB_STATE_MODS_EFFECTIVE) == 1 { + mods |= key.ModCtrl + } + if C.xkb_state_mod_name_is_active(x.state, + (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_SHIFT[0])), + C.XKB_STATE_MODS_EFFECTIVE) == 1 { + mods |= key.ModShift + } + if C.xkb_state_mod_name_is_active(x.state, + (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_ALT[0])), + C.XKB_STATE_MODS_EFFECTIVE) == 1 { + mods |= key.ModAlt + } + if C.xkb_state_mod_name_is_active(x.state, + (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_LOGO[0])), + C.XKB_STATE_MODS_EFFECTIVE) == 1 { + mods |= key.ModSuper + } + return mods +} + +func (x *Context) DispatchKey(keyCode uint32, + state key.State) (events []event.Event) { + if x.state == nil { + return + } + kc := C.xkb_keycode_t(keyCode) + if len(x.utf8Buf) == 0 { + x.utf8Buf = make([]byte, 1) + } + sym := C.xkb_state_key_get_one_sym(x.state, kc) + if name, ok := convertKeysym(sym); ok { + cmd := key.Event{ + Name: name, + Modifiers: x.Modifiers(), + State: state, + } + // Ensure that a physical backtab key is translated to + // Shift-Tab. + if sym == C.XKB_KEY_ISO_Left_Tab { + cmd.Modifiers |= key.ModShift + } + events = append(events, cmd) + } + C.xkb_compose_state_feed(x.compState, sym) + var str []byte + switch C.xkb_compose_state_get_status(x.compState) { + case C.XKB_COMPOSE_CANCELLED, C.XKB_COMPOSE_COMPOSING: + return + case C.XKB_COMPOSE_COMPOSED: + size := C.xkb_compose_state_get_utf8(x.compState, + (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf))) + if int(size) >= len(x.utf8Buf) { + x.utf8Buf = make([]byte, size+1) + size = C.xkb_compose_state_get_utf8(x.compState, + (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), + C.size_t(len(x.utf8Buf))) + } + C.xkb_compose_state_reset(x.compState) + str = x.utf8Buf[:size] + case C.XKB_COMPOSE_NOTHING: + mod := x.Modifiers() + if mod&(key.ModCtrl|key.ModAlt|key.ModSuper) == 0 { + str = x.charsForKeycode(kc) + } + } + // Report only printable runes. + var n int + for n < len(str) { + r, s := utf8.DecodeRune(str) + if unicode.IsPrint(r) { + n += s + } else { + copy(str[n:], str[n+s:]) + str = str[:len(str)-s] + } + } + if state == key.Press && len(str) > 0 { + events = append(events, key.EditEvent{Text: string(str)}) + } + return +} + +func (x *Context) charsForKeycode(keyCode C.xkb_keycode_t) []byte { + size := C.xkb_state_key_get_utf8(x.state, keyCode, + (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf))) + if int(size) >= len(x.utf8Buf) { + x.utf8Buf = make([]byte, size+1) + size = C.xkb_state_key_get_utf8(x.state, keyCode, + (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf))) + } + return x.utf8Buf[:size] +} + +func (x *Context) IsRepeatKey(keyCode uint32) bool { + kc := C.xkb_keycode_t(keyCode) + return C.xkb_keymap_key_repeats(x.keyMap, kc) == 1 +} + +func (x *Context) UpdateMask(depressed, latched, locked, depressedGroup, latchedGroup, lockedGroup uint32) { + if x.state == nil { + return + } + C.xkb_state_update_mask(x.state, C.xkb_mod_mask_t(depressed), + C.xkb_mod_mask_t(latched), C.xkb_mod_mask_t(locked), + C.xkb_layout_index_t(depressedGroup), + C.xkb_layout_index_t(latchedGroup), C.xkb_layout_index_t(lockedGroup)) +} + +func convertKeysym(s C.xkb_keysym_t) (string, bool) { + if 'a' <= s && s <= 'z' { + return string(rune(s - 'a' + 'A')), true + } + if ' ' < s && s <= '~' { + return string(rune(s)), true + } + var n string + switch s { + case C.XKB_KEY_Escape: + n = key.NameEscape + case C.XKB_KEY_Left: + n = key.NameLeftArrow + case C.XKB_KEY_Right: + n = key.NameRightArrow + case C.XKB_KEY_Return: + n = key.NameReturn + case C.XKB_KEY_KP_Enter: + n = key.NameEnter + case C.XKB_KEY_Up: + n = key.NameUpArrow + case C.XKB_KEY_Down: + n = key.NameDownArrow + case C.XKB_KEY_Home: + n = key.NameHome + case C.XKB_KEY_End: + n = key.NameEnd + case C.XKB_KEY_BackSpace: + n = key.NameDeleteBackward + case C.XKB_KEY_Delete: + n = key.NameDeleteForward + case C.XKB_KEY_Page_Up: + n = key.NamePageUp + case C.XKB_KEY_Page_Down: + n = key.NamePageDown + case C.XKB_KEY_F1: + n = "F1" + case C.XKB_KEY_F2: + n = "F2" + case C.XKB_KEY_F3: + n = "F3" + case C.XKB_KEY_F4: + n = "F4" + case C.XKB_KEY_F5: + n = "F5" + case C.XKB_KEY_F6: + n = "F6" + case C.XKB_KEY_F7: + n = "F7" + case C.XKB_KEY_F8: + n = "F8" + case C.XKB_KEY_F9: + n = "F9" + case C.XKB_KEY_F10: + n = "F10" + case C.XKB_KEY_F11: + n = "F11" + case C.XKB_KEY_F12: + n = "F12" + case C.XKB_KEY_Tab, C.XKB_KEY_KP_Tab, C.XKB_KEY_ISO_Left_Tab: + n = key.NameTab + case 0x20, C.XKB_KEY_KP_Space: + n = key.NameSpace + default: + return "", false + } + return n, true +} diff --git a/gio/app/loop.go b/gio/app/loop.go new file mode 100644 index 0000000..6b2a57a --- /dev/null +++ b/gio/app/loop.go @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +import ( + "image" + "image/color" + "runtime" + + "realy.lol/gio/app/internal/wm" + "realy.lol/gio/gpu" + "realy.lol/gio/op" +) + +type renderLoop struct { + summary string + drawing bool + err error + + frames chan frame + results chan frameResult + refresh chan struct{} + refreshErr chan error + ack chan struct{} + stop chan struct{} + stopped chan struct{} +} + +type frame struct { + viewport image.Point + ops *op.Ops +} + +type frameResult struct { + profile string + err error +} + +func newLoop(ctx wm.Context) (*renderLoop, error) { + l := &renderLoop{ + frames: make(chan frame), + results: make(chan frameResult), + refresh: make(chan struct{}), + refreshErr: make(chan error), + // Ack is buffered so GPU commands can be issued after + // ack'ing the frame. + ack: make(chan struct{}, 1), + stop: make(chan struct{}), + stopped: make(chan struct{}), + } + if err := l.renderLoop(ctx); err != nil { + return nil, err + } + return l, nil +} + +func (l *renderLoop) renderLoop(ctx wm.Context) error { + // GL Operations must happen on a single OS thread, so + // pass initialization result through a channel. + initErr := make(chan error) + go func() { + defer close(l.stopped) + runtime.LockOSThread() + // Don't UnlockOSThread to avoid reuse by the Go runtime. + + if err := ctx.MakeCurrent(); err != nil { + initErr <- err + return + } + g, err := gpu.New(ctx.API()) + if err != nil { + initErr <- err + return + } + defer g.Release() + initErr <- nil + loop: + for { + select { + case <-l.refresh: + l.refreshErr <- ctx.MakeCurrent() + case frame := <-l.frames: + ctx.Lock() + if runtime.GOOS == "js" { + // Use transparent black when Gio is embedded, to allow mixing of Gio and + // foreign content below. + g.Clear(color.NRGBA{A: 0x00, R: 0x00, G: 0x00, B: 0x00}) + } else { + g.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}) + } + g.Collect(frame.viewport, frame.ops) + // Signal that we're done with the frame ops. + l.ack <- struct{}{} + var res frameResult + res.err = g.Frame() + if res.err == nil { + res.err = ctx.Present() + } + res.profile = g.Profile() + ctx.Unlock() + l.results <- res + case <-l.stop: + break loop + } + } + }() + return <-initErr +} + +func (l *renderLoop) Release() { + // Flush error. + l.Flush() + close(l.stop) + <-l.stopped + l.stop = nil +} + +func (l *renderLoop) Flush() error { + if l.drawing { + st := <-l.results + l.setErr(st.err) + if st.profile != "" { + l.summary = st.profile + } + l.drawing = false + } + return l.err +} + +func (l *renderLoop) Summary() string { + return l.summary +} + +func (l *renderLoop) Refresh() { + if l.err != nil { + return + } + // Make sure any pending frame is complete. + l.Flush() + l.refresh <- struct{}{} + l.setErr(<-l.refreshErr) +} + +// Draw initiates a draw of a frame. It returns a channel +// than signals when the frame is no longer being accessed. +func (l *renderLoop) Draw(viewport image.Point, + frameOps *op.Ops) <-chan struct{} { + if l.err != nil { + l.ack <- struct{}{} + return l.ack + } + l.Flush() + l.frames <- frame{viewport, frameOps} + l.drawing = true + return l.ack +} + +func (l *renderLoop) setErr(err error) { + if l.err == nil { + l.err = err + } +} diff --git a/gio/app/permission/bluetooth/main.go b/gio/app/permission/bluetooth/main.go new file mode 100644 index 0000000..392bbbe --- /dev/null +++ b/gio/app/permission/bluetooth/main.go @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package bluetooth implements permissions to access Bluetooth and Bluetooth +Low Energy hardware, including the ability to discover and pair devices. + +Android + +The following entries will be added to AndroidManifest.xml: + + + + + + + +Note that ACCESS_FINE_LOCATION is required on Android before the Bluetooth +device may be used. +See https://developer.android.com/guide/topics/connectivity/bluetooth. + +ACCESS_FINE_LOCATION is a "dangerous" permission. See documentation for +package realy.lol/gio/app/permission for more information. +*/ +package bluetooth diff --git a/gio/app/permission/camera/main.go b/gio/app/permission/camera/main.go new file mode 100644 index 0000000..1e89a31 --- /dev/null +++ b/gio/app/permission/camera/main.go @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package camera implements permissions to access camera hardware. + +Android + +The following entries will be added to AndroidManifest.xml: + + + + +CAMERA is a "dangerous" permission. See documentation for package +realy.lol/gio/app/permission for more information. +*/ +package camera diff --git a/gio/app/permission/doc.go b/gio/app/permission/doc.go new file mode 100644 index 0000000..878a5cb --- /dev/null +++ b/gio/app/permission/doc.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package permission includes sub-packages that should be imported +by a Gio program or by one of its dependencies to indicate that specific +operating-system permissions are required. For example, if a Gio +program requires access to a device's Bluetooth interface, it +should import "realy.lol/gio/app/permission/bluetooth" as follows: + + package main + + import ( + "realy.lol/gio/app" + _ "realy.lol/gio/app/permission/bluetooth" + ) + + func main() { + ... + } + +Since there are no exported identifiers in the app/permission/bluetooth +package, the import uses the anonymous identifier (_) as the imported +package name. + +As a special case, the gogio tool detects when a program directly or +indirectly depends on the "net" package from the Go standard library as an +indication that the program requires network access permissions. If a program +requires network permissions but does not directly or indirectly import +"net", it will be necessary to add the following code somewhere in the +program's source code: + + import ( + ... + _ "net" + ) + +Android -- Dangerous Permissions + +Certain permissions on Android are marked with a protection level of +"dangerous". This means that, in addition to including the relevant +Gio permission packages, your app will need to prompt the user +specifically to request access. To access the Android Activity +required for prompting, use app.ViewEvent (only available on Android). +app.ViewEvent exposes the underlying Android View, on which the +getContext method returns the Activity. + +For more information on dangerous permissions, see: +https://developer.android.com/guide/topics/permissions/overview#dangerous_permissions +*/ +package permission diff --git a/gio/app/permission/networkstate/main.go b/gio/app/permission/networkstate/main.go new file mode 100644 index 0000000..c594219 --- /dev/null +++ b/gio/app/permission/networkstate/main.go @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package networkstate implements permissions to access network connectivity information. + +Android + +The following entries will be added to AndroidManifest.xml: + + + +*/ +package networkstate diff --git a/gio/app/permission/storage/main.go b/gio/app/permission/storage/main.go new file mode 100644 index 0000000..623a624 --- /dev/null +++ b/gio/app/permission/storage/main.go @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package storage implements read and write storage permissions +on mobile devices. + +Android + +The following entries will be added to AndroidManifest.xml: + + + + +READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE are "dangerous" permissions. +See documentation for package realy.lol/gio/app/permission for more information. +*/ +package storage diff --git a/gio/app/sigpipe_darwin.go b/gio/app/sigpipe_darwin.go new file mode 100644 index 0000000..aca19b7 --- /dev/null +++ b/gio/app/sigpipe_darwin.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build !go1.14 + +// Work around golang.org/issue/33384, fixed in CL 191785, +// to be released in Go 1.14. + +package app + +import ( + "os" + "os/signal" + "syscall" +) + +func init() { + signal.Notify(make(chan os.Signal), syscall.SIGPIPE) +} diff --git a/gio/app/window.go b/gio/app/window.go new file mode 100644 index 0000000..815e1e6 --- /dev/null +++ b/gio/app/window.go @@ -0,0 +1,531 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +import ( + "errors" + "fmt" + "image" + "time" + + "realy.lol/gio/io/event" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/profile" + "realy.lol/gio/io/router" + "realy.lol/gio/io/system" + "realy.lol/gio/op" + "realy.lol/gio/unit" + + _ "realy.lol/gio/app/internal/log" + "realy.lol/gio/app/internal/wm" +) + +// WindowOption configures a wm. +type Option func(opts *wm.Options) + +// Window represents an operating system wm. +type Window struct { + driver wm.Driver + ctx wm.Context + loop *renderLoop + + // driverFuncs is a channel of functions to run when + // the Window has a valid driver. + driverFuncs chan func() + + out chan event.Event + in chan event.Event + ack chan struct{} + invalidates chan struct{} + frames chan *op.Ops + frameAck chan struct{} + // dead is closed when the window is destroyed. + dead chan struct{} + + stage system.Stage + animating bool + hasNextFrame bool + nextFrame time.Time + delayedDraw *time.Timer + + queue queue + cursor pointer.CursorName + + callbacks callbacks +} + +type callbacks struct { + w *Window +} + +// queue is an event.Queue implementation that distributes system events +// to the input handlers declared in the most recent frame. +type queue struct { + q router.Router +} + +// driverEvent is sent when a new native driver +// is available for the wm. +type driverEvent struct { + driver wm.Driver +} + +// Pre-allocate the ack event to avoid garbage. +var ackEvent event.Event + +// NewWindow creates a new window for a set of window +// options. The options are hints; the platform is free to +// ignore or adjust them. +// +// If the current program is running on iOS and Android, +// NewWindow returns the window previously created by the +// platform. +// +// Calling NewWindow more than once is not supported on +// iOS, Android, WebAssembly. +func NewWindow(options ...Option) *Window { + opts := new(wm.Options) + // Default options. + Size(unit.Px(800), unit.Px(600))(opts) + Title("Gio")(opts) + + for _, o := range options { + o(opts) + } + + w := &Window{ + in: make(chan event.Event), + out: make(chan event.Event), + ack: make(chan struct{}), + invalidates: make(chan struct{}, 1), + frames: make(chan *op.Ops), + frameAck: make(chan struct{}), + driverFuncs: make(chan func()), + dead: make(chan struct{}), + } + w.callbacks.w = w + go w.run(opts) + return w +} + +// Events returns the channel where events are delivered. +func (w *Window) Events() <-chan event.Event { + return w.out +} + +// update updates the wm. Paint operations updates the +// window contents, input operations declare input handlers, +// and so on. The supplied operations list completely replaces +// the window state from previous calls. +func (w *Window) update(frame *op.Ops) { + w.frames <- frame + <-w.frameAck +} + +func (w *Window) validateAndProcess(frameStart time.Time, size image.Point, + sync bool, frame *op.Ops) error { + for { + if w.loop != nil { + if err := w.loop.Flush(); err != nil { + w.destroyGPU() + if err == wm.ErrDeviceLost { + continue + } + return err + } + } + if w.loop == nil { + var err error + w.ctx, err = w.driver.NewContext() + if err != nil { + return err + } + w.loop, err = newLoop(w.ctx) + if err != nil { + w.ctx.Release() + return err + } + } + w.processFrame(frameStart, size, frame) + if sync { + if err := w.loop.Flush(); err != nil { + w.destroyGPU() + if err == wm.ErrDeviceLost { + continue + } + return err + } + } + return nil + } +} + +func (w *Window) processFrame(frameStart time.Time, size image.Point, + frame *op.Ops) { + sync := w.loop.Draw(size, frame) + w.queue.q.Frame(frame) + switch w.queue.q.TextInputState() { + case router.TextInputOpen: + w.driver.ShowTextInput(true) + case router.TextInputClose: + w.driver.ShowTextInput(false) + } + if txt, ok := w.queue.q.WriteClipboard(); ok { + go w.WriteClipboard(txt) + } + if w.queue.q.ReadClipboard() { + go w.ReadClipboard() + } + if w.queue.q.Profiling() { + frameDur := time.Since(frameStart) + frameDur = frameDur.Truncate(100 * time.Microsecond) + q := 100 * time.Microsecond + timings := fmt.Sprintf("tot:%7s %s", frameDur.Round(q), + w.loop.Summary()) + w.queue.q.Queue(profile.Event{Timings: timings}) + } + if t, ok := w.queue.q.WakeupTime(); ok { + w.setNextFrame(t) + } + // Opportunistically check whether Invalidate has been called, to avoid + // stopping and starting animation mode. + select { + case <-w.invalidates: + w.setNextFrame(time.Time{}) + default: + } + w.updateAnimation() + // Wait for the GPU goroutine to finish processing frame. + <-sync +} + +// Invalidate the window such that a FrameEvent will be generated immediately. +// If the window is inactive, the event is sent when the window becomes active. +// +// Note that Invalidate is intended for externally triggered updates, such as a +// response from a network request. InvalidateOp is more efficient for animation +// and similar internal updates. +// +// Invalidate is safe for concurrent use. +func (w *Window) Invalidate() { + select { + case w.invalidates <- struct{}{}: + default: + } +} + +// Option applies the options to the window. +func (w *Window) Option(opts ...Option) { + go w.driverDo(func() { + o := new(wm.Options) + for _, opt := range opts { + opt(o) + } + w.driver.Option(o) + }) +} + +// ReadClipboard initiates a read of the clipboard in the form +// of a clipboard.Event. Multiple reads may be coalesced +// to a single event. +func (w *Window) ReadClipboard() { + go w.driverDo(func() { + w.driver.ReadClipboard() + }) +} + +// WriteClipboard writes a string to the clipboard. +func (w *Window) WriteClipboard(s string) { + go w.driverDo(func() { + w.driver.WriteClipboard(s) + }) +} + +// SetCursorName changes the current window cursor to name. +func (w *Window) SetCursorName(name pointer.CursorName) { + go w.driverDo(func() { + w.driver.SetCursor(name) + }) +} + +// Close the wm. The window's event loop should exit when it receives +// system.DestroyEvent. +// +// Currently, only macOS, Windows and X11 drivers implement this functionality, +// all others are stubbed. +func (w *Window) Close() { + go w.driverDo(func() { + w.driver.Close() + }) +} + +// driverDo waits for the window to have a valid driver attached and calls f. +// It does nothing if the if the window was destroyed while waiting. +func (w *Window) driverDo(f func()) { + select { + case w.driverFuncs <- f: + case <-w.dead: + } +} + +func (w *Window) updateAnimation() { + animate := false + if w.delayedDraw != nil { + w.delayedDraw.Stop() + w.delayedDraw = nil + } + if w.stage >= system.StageRunning && w.hasNextFrame { + if dt := time.Until(w.nextFrame); dt <= 0 { + animate = true + } else { + w.delayedDraw = time.NewTimer(dt) + } + } + if animate != w.animating { + w.animating = animate + w.driver.SetAnimating(animate) + } +} + +func (w *Window) setNextFrame(at time.Time) { + if !w.hasNextFrame || at.Before(w.nextFrame) { + w.hasNextFrame = true + w.nextFrame = at + } +} + +func (c *callbacks) SetDriver(d wm.Driver) { + c.Event(driverEvent{d}) +} + +func (c *callbacks) Event(e event.Event) { + select { + case c.w.in <- e: + <-c.w.ack + case <-c.w.dead: + } +} + +func (w *Window) waitAck() { + // Send a dummy event; when it gets through we + // know the application has processed the previous event. + w.out <- ackEvent +} + +// Prematurely destroy the window and wait for the native window +// destroy event. +func (w *Window) destroy(err error) { + w.destroyGPU() + // Ack the current event. + w.ack <- struct{}{} + w.out <- system.DestroyEvent{Err: err} + close(w.dead) + for e := range w.in { + w.ack <- struct{}{} + if _, ok := e.(system.DestroyEvent); ok { + return + } + } +} + +func (w *Window) destroyGPU() { + if w.loop != nil { + w.loop.Release() + w.loop = nil + } + if w.ctx != nil { + w.ctx.Release() + w.ctx = nil + } +} + +// waitFrame waits for the client to either call FrameEvent.Frame +// or to continue event handling. It returns whether the client +// called Frame or not. +func (w *Window) waitFrame() (*op.Ops, bool) { + select { + case frame := <-w.frames: + // The client called FrameEvent.Frame. + return frame, true + case w.out <- ackEvent: + // The client ignored FrameEvent and continued processing + // events. + return nil, false + } +} + +func (w *Window) run(opts *wm.Options) { + defer close(w.in) + defer close(w.out) + if err := wm.NewWindow(&w.callbacks, opts); err != nil { + w.out <- system.DestroyEvent{Err: err} + return + } + for { + var driverFuncs chan func() + if w.driver != nil { + driverFuncs = w.driverFuncs + } + var timer <-chan time.Time + if w.delayedDraw != nil { + timer = w.delayedDraw.C + } + select { + case <-timer: + w.setNextFrame(time.Time{}) + w.updateAnimation() + case <-w.invalidates: + w.setNextFrame(time.Time{}) + w.updateAnimation() + case f := <-driverFuncs: + f() + case e := <-w.in: + switch e2 := e.(type) { + case system.StageEvent: + if w.loop != nil { + if e2.Stage < system.StageRunning { + w.destroyGPU() + } else { + w.loop.Refresh() + } + } + w.stage = e2.Stage + w.updateAnimation() + w.out <- e + w.waitAck() + case wm.FrameEvent: + if e2.Size == (image.Point{}) { + panic(errors.New("internal error: zero-sized Draw")) + } + if w.stage < system.StageRunning { + // No drawing if not visible. + break + } + frameStart := time.Now() + w.hasNextFrame = false + e2.Frame = w.update + e2.Queue = &w.queue + w.out <- e2.FrameEvent + if w.loop != nil { + if e2.Sync { + w.loop.Refresh() + } + } + frame, gotFrame := w.waitFrame() + err := w.validateAndProcess(frameStart, e2.Size, e2.Sync, frame) + if gotFrame { + // We're done with frame, let the client continue. + w.frameAck <- struct{}{} + } + if err != nil { + w.destroyGPU() + w.destroy(err) + return + } + w.updateCursor() + case *system.CommandEvent: + w.out <- e + w.waitAck() + case driverEvent: + w.driver = e2.driver + case system.DestroyEvent: + w.destroyGPU() + w.out <- e2 + w.ack <- struct{}{} + return + case event.Event: + if w.queue.q.Queue(e2) { + w.setNextFrame(time.Time{}) + w.updateAnimation() + } + w.updateCursor() + w.out <- e + } + w.ack <- struct{}{} + } + } +} + +func (w *Window) updateCursor() { + if c := w.queue.q.Cursor(); c != w.cursor { + w.cursor = c + w.SetCursorName(c) + } +} + +func (q *queue) Events(k event.Tag) []event.Event { + return q.q.Events(k) +} + +const ( + // Windowed is the normal window mode with OS specific window decorations. + Windowed = wm.Windowed + // Fullscreen is the full screen window mode. + Fullscreen = wm.Fullscreen +) + +// WindowMode sets the window mode. +// +// Supported platforms are macOS, X11 and Windows. +func WindowMode(mode wm.WindowMode) Option { + return func(opts *wm.Options) { + opts.WindowMode = &mode + } +} + +// Title sets the title of the wm. +func Title(t string) Option { + return func(opts *wm.Options) { + opts.Title = &t + } +} + +// Size sets the size of the wm. +func Size(w, h unit.Value) Option { + if w.V <= 0 { + panic("width must be larger than or equal to 0") + } + if h.V <= 0 { + panic("height must be larger than or equal to 0") + } + return func(opts *wm.Options) { + opts.Size = &wm.Size{ + Width: w, + Height: h, + } + } +} + +// MaxSize sets the maximum size of the wm. +func MaxSize(w, h unit.Value) Option { + if w.V <= 0 { + panic("width must be larger than or equal to 0") + } + if h.V <= 0 { + panic("height must be larger than or equal to 0") + } + return func(opts *wm.Options) { + opts.MaxSize = &wm.Size{ + Width: w, + Height: h, + } + } +} + +// MinSize sets the minimum size of the wm. +func MinSize(w, h unit.Value) Option { + if w.V <= 0 { + panic("width must be larger than or equal to 0") + } + if h.V <= 0 { + panic("height must be larger than or equal to 0") + } + return func(opts *wm.Options) { + opts.MinSize = &wm.Size{ + Width: w, + Height: h, + } + } +} + +func (driverEvent) ImplementsEvent() {} diff --git a/gio/cmd/go.local.sum b/gio/cmd/go.local.sum new file mode 100644 index 0000000..c197ac2 --- /dev/null +++ b/gio/cmd/go.local.sum @@ -0,0 +1,53 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o= +github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg= +github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g= +github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194= +github.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs= +github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0= +github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197 h1:7+SpRyhoo46QjKkYInQXpcfxx3TYFEYkn131lwGE9/0= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/gio/cmd/go.sum b/gio/cmd/go.sum new file mode 100644 index 0000000..c197ac2 --- /dev/null +++ b/gio/cmd/go.sum @@ -0,0 +1,53 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o= +github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg= +github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g= +github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194= +github.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs= +github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0= +github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197 h1:7+SpRyhoo46QjKkYInQXpcfxx3TYFEYkn131lwGE9/0= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/gio/cmd/gogio/android_test.go b/gio/cmd/gogio/android_test.go new file mode 100644 index 0000000..e73386f --- /dev/null +++ b/gio/cmd/gogio/android_test.go @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "bytes" + "context" + "fmt" + "image" + "image/png" + "os" + "os/exec" + "path/filepath" + "regexp" +) + +type AndroidTestDriver struct { + driverBase + + sdkDir string + adbPath string +} + +var rxAdbDevice = regexp.MustCompile(`(.*)\s+device$`) + +func (d *AndroidTestDriver) Start(path string) { + d.sdkDir = os.Getenv("ANDROID_SDK_ROOT") + if d.sdkDir == "" { + d.Skipf("Android SDK is required; set $ANDROID_SDK_ROOT") + } + d.adbPath = filepath.Join(d.sdkDir, "platform-tools", "adb") + if _, err := os.Stat(d.adbPath); os.IsNotExist(err) { + d.Skipf("adb not found") + } + + devOut := bytes.TrimSpace(d.adb("devices")) + devices := rxAdbDevice.FindAllSubmatch(devOut, -1) + switch len(devices) { + case 0: + d.Skipf("no Android devices attached via adb; skipping") + case 1: + default: + d.Skipf("multiple Android devices attached via adb; skipping") + } + + // If the device is attached but asleep, it's probably just charging. + // Don't use it; the screen needs to be on and unlocked for the test to + // work. + if !bytes.Contains( + d.adb("shell", "dumpsys", "power"), + []byte(" mWakefulness=Awake"), + ) { + d.Skipf("Android device isn't awake; skipping") + } + + // First, build the app. + apk := filepath.Join(d.tempDir("gio-endtoend-android"), "e2e.apk") + d.gogio("-target=android", "-appid="+appid, "-o="+apk, path) + + // Make sure the app isn't installed already, and try to uninstall it + // when we finish. Previous failed test runs might have left the app. + d.tryUninstall() + d.adb("install", apk) + d.Cleanup(d.tryUninstall) + + // Force our e2e app to be fullscreen, so that the android system bar at + // the top doesn't mess with our screenshots. + // TODO(mvdan): is there a way to do this via gio, so that we don't need + // to set up a global Android setting via the shell? + d.adb("shell", "settings", "put", "global", "policy_control", "immersive.full="+appid) + + // Make sure the app isn't already running. + d.adb("shell", "pm", "clear", appid) + + // Start listening for log messages. + { + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, d.adbPath, + "logcat", + "-s", // suppress other logs + "-T1", // don't show previous log messages + appid+":*", // show all logs from our gio app ID + ) + output, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + cmd.Stderr = cmd.Stdout + d.output = output + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + } + + // Start the app. + d.adb("shell", "monkey", "-p", appid, "1") + + // Wait for the gio app to render. + d.waitForFrame() +} + +func (d *AndroidTestDriver) Screenshot() image.Image { + out := d.adb("shell", "screencap", "-p") + img, err := png.Decode(bytes.NewReader(out)) + if err != nil { + d.Fatal(err) + } + return img +} + +func (d *AndroidTestDriver) tryUninstall() { + cmd := exec.Command(d.adbPath, "shell", "pm", "uninstall", appid) + out, err := cmd.CombinedOutput() + if err != nil { + if bytes.Contains(out, []byte("Unknown package")) { + // The package is not installed. Don't log anything. + return + } + d.Logf("could not uninstall: %v\n%s", err, out) + } +} + +func (d *AndroidTestDriver) adb(args ...interface{}) []byte { + strs := []string{} + for _, arg := range args { + strs = append(strs, fmt.Sprint(arg)) + } + cmd := exec.Command(d.adbPath, strs...) + out, err := cmd.CombinedOutput() + if err != nil { + d.Errorf("%s", out) + d.Fatal(err) + } + return out +} + +func (d *AndroidTestDriver) Click(x, y int) { + d.adb("shell", "input", "tap", x, y) + + // Wait for the gio app to render after this click. + d.waitForFrame() +} diff --git a/gio/cmd/gogio/androidbuild.go b/gio/cmd/gogio/androidbuild.go new file mode 100644 index 0000000..4a055b9 --- /dev/null +++ b/gio/cmd/gogio/androidbuild.go @@ -0,0 +1,1032 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "archive/zip" + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "text/template" + + "golang.org/x/sync/errgroup" + "golang.org/x/tools/go/packages" +) + +type androidTools struct { + buildtools string + androidjar string +} + +// zip.Writer with a sticky error. +type zipWriter struct { + err error + w *zip.Writer +} + +// Writer that saves any errors. +type errWriter struct { + w io.Writer + err *error +} + +var exeSuffix string + +type manifestData struct { + AppID string + Version int + MinSDK int + TargetSDK int + Permissions []string + Features []string + IconSnip string + AppName string +} + +const ( + themes = ` + + +` + themesV21 = ` + + +` +) + +func init() { + if runtime.GOOS == "windows" { + exeSuffix = ".exe" + } +} + +func buildAndroid(tmpDir string, bi *buildInfo) error { + sdk := os.Getenv("ANDROID_SDK_ROOT") + if sdk == "" { + return errors.New("please set ANDROID_SDK_ROOT to the Android SDK path") + } + if _, err := os.Stat(sdk); err != nil { + return err + } + platform, err := latestPlatform(sdk) + if err != nil { + return err + } + buildtools, err := latestTools(sdk) + if err != nil { + return err + } + + tools := &androidTools{ + buildtools: buildtools, + androidjar: filepath.Join(platform, "android.jar"), + } + perms := []string{"default"} + const permPref = "realy.lol/gio/app/permission/" + cfg := &packages.Config{ + Mode: packages.NeedName + + packages.NeedFiles + + packages.NeedImports + + packages.NeedDeps, + Env: append( + os.Environ(), + "GOOS=android", + "CGO_ENABLED=1", + ), + } + pkgs, err := packages.Load(cfg, bi.pkgPath) + if err != nil { + return err + } + var extraJars []string + visitedPkgs := make(map[string]bool) + var visitPkg func(*packages.Package) error + visitPkg = func(p *packages.Package) error { + if len(p.GoFiles) == 0 { + return nil + } + dir := filepath.Dir(p.GoFiles[0]) + jars, err := filepath.Glob(filepath.Join(dir, "*.jar")) + if err != nil { + return err + } + extraJars = append(extraJars, jars...) + switch { + case p.PkgPath == "net": + perms = append(perms, "network") + case strings.HasPrefix(p.PkgPath, permPref): + perms = append(perms, p.PkgPath[len(permPref):]) + } + + for _, imp := range p.Imports { + if !visitedPkgs[imp.ID] { + visitPkg(imp) + visitedPkgs[imp.ID] = true + } + } + return nil + } + if err := visitPkg(pkgs[0]); err != nil { + return err + } + + if err := compileAndroid(tmpDir, tools, bi); err != nil { + return err + } + switch *buildMode { + case "archive": + return archiveAndroid(tmpDir, bi, perms) + case "exe": + file := *destPath + if file == "" { + file = fmt.Sprintf("%s.apk", bi.name) + } + + isBundle := false + switch filepath.Ext(file) { + case ".apk": + case ".aab": + isBundle = true + default: + return fmt.Errorf("the specified output %q does not end in '.apk' or '.aab'", + file) + } + + if err := exeAndroid(tmpDir, tools, bi, extraJars, perms, + isBundle); err != nil { + return err + } + if isBundle { + return signAAB(tmpDir, file, tools, bi) + } + return signAPK(tmpDir, file, tools, bi) + default: + panic("unreachable") + } +} + +func compileAndroid(tmpDir string, tools *androidTools, + bi *buildInfo) (err error) { + androidHome := os.Getenv("ANDROID_SDK_ROOT") + if androidHome == "" { + return errors.New("ANDROID_SDK_ROOT is not set. Please point it to the root of the Android SDK") + } + javac, err := findJavaC() + if err != nil { + return fmt.Errorf("could not find javac: %v", err) + } + ndkRoot, err := findNDK(androidHome) + if err != nil { + return err + } + minSDK := 16 + if bi.minsdk > minSDK { + minSDK = bi.minsdk + } + tcRoot := filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", + archNDK()) + var builds errgroup.Group + for _, a := range bi.archs { + arch := allArchs[a] + clang, err := latestCompiler(tcRoot, a, minSDK) + if err != nil { + return fmt.Errorf("%s. Please make sure you have NDK >= r19c installed. Use the command `sdkmanager ndk-bundle` to install it.", + err) + } + if runtime.GOOS == "windows" { + // Because of https://github.com/android-ndk/ndk/issues/920, + // we need NDK r19c, not just r19b. Check for the presence of + // clang++.cmd which is only available in r19c. + clangpp := clang + "++.cmd" + if _, err := os.Stat(clangpp); err != nil { + return fmt.Errorf("NDK version r19b detected, but >= r19c is required. Use the command `sdkmanager ndk-bundle` to install it") + } + } + archDir := filepath.Join(tmpDir, "jni", arch.jniArch) + if err := os.MkdirAll(archDir, 0755); err != nil { + return fmt.Errorf("failed to create %q: %v", archDir, err) + } + libFile := filepath.Join(archDir, "libgio.so") + cmd := exec.Command( + "go", + "build", + "-ldflags=-w -s "+bi.ldflags, + "-buildmode=c-shared", + "-tags", bi.tags, + "-o", libFile, + bi.pkgPath, + ) + cmd.Env = append( + os.Environ(), + "GOOS=android", + "GOARCH="+a, + "GOARM=7", // Avoid softfloat. + "CGO_ENABLED=1", + "CC="+clang, + ) + builds.Go(func() error { + _, err := runCmd(cmd) + return err + }) + } + appDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", + "realy.lol/gio/app/internal/wm")) + if err != nil { + return err + } + javaFiles, err := filepath.Glob(filepath.Join(appDir, "*.java")) + if err != nil { + return err + } + if len(javaFiles) > 0 { + classes := filepath.Join(tmpDir, "classes") + if err := os.MkdirAll(classes, 0755); err != nil { + return err + } + javac := exec.Command( + javac, + "-target", "1.8", + "-source", "1.8", + "-sourcepath", appDir, + "-bootclasspath", tools.androidjar, + "-d", classes, + ) + javac.Args = append(javac.Args, javaFiles...) + builds.Go(func() error { + _, err := runCmd(javac) + return err + }) + } + return builds.Wait() +} + +func archiveAndroid(tmpDir string, bi *buildInfo, perms []string) (err error) { + aarFile := *destPath + if aarFile == "" { + aarFile = fmt.Sprintf("%s.aar", bi.name) + } + if filepath.Ext(aarFile) != ".aar" { + return fmt.Errorf("the specified output %q does not end in '.aar'", + aarFile) + } + aar, err := os.Create(aarFile) + if err != nil { + return err + } + defer func() { + if cerr := aar.Close(); err == nil { + err = cerr + } + }() + aarw := newZipWriter(aar) + defer aarw.Close() + aarw.Create("R.txt") + themesXML := aarw.Create("res/values/themes.xml") + themesXML.Write([]byte(themes)) + themesXML21 := aarw.Create("res/values-v21/themes.xml") + themesXML21.Write([]byte(themesV21)) + permissions, features := getPermissions(perms) + // Disable input emulation on ChromeOS. + manifest := aarw.Create("AndroidManifest.xml") + manifestSrc := manifestData{ + AppID: bi.appID, + MinSDK: bi.minsdk, + Permissions: permissions, + Features: features, + } + tmpl, err := template.New("manifest").Parse( + ` + +{{range .Permissions}} +{{end}}{{range .Features}} +{{end}} +`) + if err != nil { + panic(err) + } + err = tmpl.Execute(manifest, manifestSrc) + proguard := aarw.Create("proguard.txt") + proguard.Write([]byte(`-keep class org.gioui.** { *; }`)) + + for _, a := range bi.archs { + arch := allArchs[a] + libFile := filepath.Join("jni", arch.jniArch, "libgio.so") + aarw.Add(filepath.ToSlash(libFile), filepath.Join(tmpDir, libFile)) + } + classes := filepath.Join(tmpDir, "classes") + if _, err := os.Stat(classes); err == nil { + jarFile := filepath.Join(tmpDir, "classes.jar") + if err := writeJar(jarFile, classes); err != nil { + return err + } + aarw.Add("classes.jar", jarFile) + } + return aarw.Close() +} + +func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, + extraJars, perms []string, isBundle bool) (err error) { + classes := filepath.Join(tmpDir, "classes") + var classFiles []string + err = filepath.Walk(classes, + func(path string, f os.FileInfo, err error) error { + if err != nil { + return err + } + if filepath.Ext(path) == ".class" { + classFiles = append(classFiles, path) + } + return nil + }) + classFiles = append(classFiles, extraJars...) + dexDir := filepath.Join(tmpDir, "apk") + if err := os.MkdirAll(dexDir, 0755); err != nil { + return err + } + if len(classFiles) > 0 { + d8 := exec.Command( + filepath.Join(tools.buildtools, "d8"), + "--classpath", tools.androidjar, + "--output", dexDir, + ) + d8.Args = append(d8.Args, classFiles...) + if _, err := runCmd(d8); err != nil { + return err + } + } + + // Compile resources. + resDir := filepath.Join(tmpDir, "res") + valDir := filepath.Join(resDir, "values") + v21Dir := filepath.Join(resDir, "values-v21") + for _, dir := range []string{valDir, v21Dir} { + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + } + iconSnip := "" + if _, err := os.Stat(bi.iconPath); err == nil { + err := buildIcons(resDir, bi.iconPath, []iconVariant{ + {path: filepath.Join("mipmap-hdpi", "ic_launcher.png"), size: 72}, + {path: filepath.Join("mipmap-xhdpi", "ic_launcher.png"), size: 96}, + {path: filepath.Join("mipmap-xxhdpi", "ic_launcher.png"), + size: 144}, + {path: filepath.Join("mipmap-xxxhdpi", "ic_launcher.png"), + size: 192}, + }) + if err != nil { + return err + } + iconSnip = `android:icon="@mipmap/ic_launcher"` + } + err = ioutil.WriteFile(filepath.Join(valDir, "themes.xml"), []byte(themes), + 0660) + if err != nil { + return err + } + err = ioutil.WriteFile(filepath.Join(v21Dir, "themes.xml"), + []byte(themesV21), 0660) + if err != nil { + return err + } + resZip := filepath.Join(tmpDir, "resources.zip") + aapt2 := filepath.Join(tools.buildtools, "aapt2") + _, err = runCmd(exec.Command( + aapt2, + "compile", + "-o", resZip, + "--dir", resDir)) + if err != nil { + return err + } + + // Link APK. + // Currently, new apps must have a target SDK version of at least 30. + // https://developer.android.com/distribute/best-practices/develop/target-sdk + targetSDK := 30 + if bi.minsdk > targetSDK { + targetSDK = bi.minsdk + } + minSDK := 16 + if bi.minsdk > minSDK { + minSDK = bi.minsdk + } + permissions, features := getPermissions(perms) + appName := strings.Title(bi.name) + manifestSrc := manifestData{ + AppID: bi.appID, + Version: bi.version, + MinSDK: minSDK, + TargetSDK: targetSDK, + Permissions: permissions, + Features: features, + IconSnip: iconSnip, + AppName: appName, + } + tmpl, err := template.New("test").Parse( + ` + + +{{range .Permissions}} +{{end}}{{range .Features}} +{{end}} + + + + + + + +`) + var manifestBuffer bytes.Buffer + if err := tmpl.Execute(&manifestBuffer, manifestSrc); err != nil { + return err + } + manifest := filepath.Join(tmpDir, "AndroidManifest.xml") + if err := ioutil.WriteFile(manifest, manifestBuffer.Bytes(), + 0660); err != nil { + return err + } + + linkAPK := filepath.Join(tmpDir, "link.apk") + + args := []string{ + "link", + "--manifest", manifest, + "-I", tools.androidjar, + "-o", linkAPK, + } + if isBundle { + args = append(args, "--proto-format") + } + args = append(args, resZip) + + if _, err := runCmd(exec.Command(aapt2, args...)); err != nil { + return err + } + + // The Go standard library archive/zip doesn't support appending to zip + // files. Copy files from `link.apk` (generated by aapt2) along with classes.dex and + // the Go libraries to a new `app.zip` file. + + // Load link.apk as zip. + linkAPKZip, err := zip.OpenReader(linkAPK) + if err != nil { + return err + } + defer linkAPKZip.Close() + + // Create new "APK". + unsignedAPK := filepath.Join(tmpDir, "app.zip") + unsignedAPKFile, err := os.Create(unsignedAPK) + if err != nil { + return err + } + defer func() { + if cerr := unsignedAPKFile.Close(); err == nil { + err = cerr + } + }() + unsignedAPKZip := zip.NewWriter(unsignedAPKFile) + defer unsignedAPKZip.Close() + + // Copy files from linkAPK to unsignedAPK. + for _, f := range linkAPKZip.File { + header := zip.FileHeader{ + Name: f.FileHeader.Name, + Method: f.FileHeader.Method, + } + + if isBundle { + // AAB have pre-defined folders. + switch header.Name { + case "AndroidManifest.xml": + header.Name = "manifest/AndroidManifest.xml" + } + } + + w, err := unsignedAPKZip.CreateHeader(&header) + if err != nil { + return err + } + r, err := f.Open() + if err != nil { + return err + } + if _, err := io.Copy(w, r); err != nil { + return err + } + } + + // Append new files (that doesn't exists inside the link.apk). + appendToZip := func(path string, file string) error { + f, err := os.Open(file) + if err != nil { + return err + } + defer f.Close() + w, err := unsignedAPKZip.CreateHeader(&zip.FileHeader{ + Name: filepath.ToSlash(path), + Method: zip.Deflate, + }) + if err != nil { + return err + } + _, err = io.Copy(w, f) + return err + } + + // Append Go binaries (libgio.so). + for _, a := range bi.archs { + arch := allArchs[a] + libFile := filepath.Join(arch.jniArch, "libgio.so") + if err := appendToZip(filepath.Join("lib", libFile), + filepath.Join(tmpDir, "jni", libFile)); err != nil { + return err + } + } + + // Append classes.dex. + classesFolder := "classes.dex" + if isBundle { + classesFolder = "dex/classes.dex" + } + if err := appendToZip(classesFolder, + filepath.Join(dexDir, "classes.dex")); err != nil { + return err + } + + return unsignedAPKZip.Close() +} + +func signAPK(tmpDir string, apkFile string, tools *androidTools, + bi *buildInfo) error { + if err := zipalign(tools, filepath.Join(tmpDir, "app.zip"), + apkFile); err != nil { + return err + } + + if bi.key == "" { + if err := defaultAndroidKeystore(tmpDir, bi); err != nil { + return err + } + } + + _, err := runCmd(exec.Command( + filepath.Join(tools.buildtools, "apksigner"), + "sign", + "--ks-pass", "pass:"+bi.password, + "--ks", bi.key, + apkFile, + )) + + return err +} + +func signAAB(tmpDir string, aabFile string, tools *androidTools, + bi *buildInfo) error { + allBundleTools, err := filepath.Glob(filepath.Join(tools.buildtools, + "bundletool*.jar")) + if err != nil { + return err + } + + bundletool := "" + for _, v := range allBundleTools { + bundletool = v + break + } + + if bundletool == "" { + return fmt.Errorf("bundletool was not found at %s. Download it from https://github.com/google/bundletool/releases and move to the respective folder", + tools.buildtools) + } + + _, err = runCmd(exec.Command( + "java", + "-jar", bundletool, + "build-bundle", + "--modules="+filepath.Join(tmpDir, "app.zip"), + "--output="+filepath.Join(tmpDir, "app.aab"), + )) + if err != nil { + return err + } + + if err := zipalign(tools, filepath.Join(tmpDir, "app.aab"), + aabFile); err != nil { + return err + } + + if bi.key == "" { + if err := defaultAndroidKeystore(tmpDir, bi); err != nil { + return err + } + } + + keytoolList, err := runCmd(exec.Command( + "keytool", + "-keystore", bi.key, + "-list", + "-keypass", bi.password, + "-v", + )) + if err != nil { + return err + } + + var alias string + for _, t := range strings.Split(keytoolList, "\n") { + if i, _ := fmt.Sscanf(t, "Alias name: %s", &alias); i > 0 { + break + } + } + + _, err = runCmd(exec.Command( + filepath.Join("jarsigner"), + "-sigalg", "SHA256withRSA", + "-digestalg", "SHA-256", + "-keystore", bi.key, + "-storepass", bi.password, + aabFile, + strings.TrimSpace(alias), + )) + + return err +} + +func zipalign(tools *androidTools, input, output string) error { + _, err := runCmd(exec.Command( + filepath.Join(tools.buildtools, "zipalign"), + "-f", + "4", // 32-bit alignment. + input, + output, + )) + return err +} + +func defaultAndroidKeystore(tmpDir string, bi *buildInfo) error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + + // Use debug.keystore, if exists. + bi.key = filepath.Join(home, ".android", "debug.keystore") + bi.password = "android" + if _, err := os.Stat(bi.key); err == nil { + return nil + } + + // Generate new key. + bi.key = filepath.Join(tmpDir, "sign.keystore") + keytool, err := findKeytool() + if err != nil { + return err + } + _, err = runCmd(exec.Command( + keytool, + "-genkey", + "-keystore", bi.key, + "-storepass", bi.password, + "-alias", "android", + "-keyalg", "RSA", "-keysize", "2048", + "-validity", "10000", + "-noprompt", + "-dname", "CN=android", + )) + return err +} + +func findNDK(androidHome string) (string, error) { + ndks, err := filepath.Glob(filepath.Join(androidHome, "ndk", "*")) + if err != nil { + return "", err + } + if bestNDK, found := latestVersionPath(ndks); found { + return bestNDK, nil + } + // The old NDK path was $ANDROID_SDK_ROOT/ndk-bundle. + ndkBundle := filepath.Join(androidHome, "ndk-bundle") + if _, err := os.Stat(ndkBundle); err == nil { + return ndkBundle, nil + } + // Certain non-standard NDK isntallations set the $ANDROID_NDK_ROOT + // environment variable + if ndkBundle, ok := os.LookupEnv("ANDROID_NDK_ROOT"); ok { + if _, err := os.Stat(ndkBundle); err == nil { + return ndkBundle, nil + } + } + + return "", fmt.Errorf("no NDK found in $ANDROID_SDK_ROOT (%s). Set $ANDROID_NDK_ROOT or use `sdkmanager ndk-bundle` to install the NDK", + androidHome) +} + +func findKeytool() (string, error) { + keytool, err := exec.LookPath("keytool") + if err == nil { + return keytool, err + } + javaHome := os.Getenv("JAVA_HOME") + if javaHome == "" { + return "", err + } + keytool = filepath.Join(javaHome, "jre", "bin", "keytool"+exeSuffix) + if _, serr := os.Stat(keytool); serr == nil { + return keytool, nil + } + return "", err +} + +func findJavaC() (string, error) { + javac, err := exec.LookPath("javac") + if err == nil { + return javac, err + } + javaHome := os.Getenv("JAVA_HOME") + if javaHome == "" { + return "", err + } + javac = filepath.Join(javaHome, "bin", "javac"+exeSuffix) + if _, serr := os.Stat(javac); serr == nil { + return javac, nil + } + return "", err +} + +func writeJar(jarFile, dir string) (err error) { + jar, err := os.Create(jarFile) + if err != nil { + return err + } + defer func() { + if cerr := jar.Close(); err == nil { + err = cerr + } + }() + jarw := newZipWriter(jar) + const manifestHeader = `Manifest-Version: 1.0 +Created-By: 1.0 (Go) + +` + jarw.Create("META-INF/MANIFEST.MF").Write([]byte(manifestHeader)) + err = filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { + if err != nil { + return err + } + if f.IsDir() { + return nil + } + if filepath.Ext(path) == ".class" { + rel := filepath.ToSlash(path[len(dir)+1:]) + jarw.Add(rel, path) + } + return nil + }) + if err != nil { + return err + } + return jarw.Close() +} + +func archNDK() string { + var arch string + switch runtime.GOARCH { + case "386": + arch = "x86" + case "amd64": + arch = "x86_64" + default: + panic("unsupported GOARCH: " + runtime.GOARCH) + } + return runtime.GOOS + "-" + arch +} + +func getPermissions(ps []string) ([]string, []string) { + var permissions, features []string + seenPermissions := make(map[string]bool) + seenFeatures := make(map[string]bool) + for _, perm := range ps { + for _, x := range AndroidPermissions[perm] { + if !seenPermissions[x] { + permissions = append(permissions, x) + seenPermissions[x] = true + } + } + for _, x := range AndroidFeatures[perm] { + if !seenFeatures[x] { + features = append(features, x) + seenFeatures[x] = true + } + } + } + return permissions, features +} + +func latestPlatform(sdk string) (string, error) { + allPlats, err := filepath.Glob(filepath.Join(sdk, "platforms", "android-*")) + if err != nil { + return "", err + } + var bestVer int + var bestPlat string + for _, platform := range allPlats { + _, name := filepath.Split(platform) + // The glob above guarantees the "android-" prefix. + verStr := name[len("android-"):] + ver, err := strconv.Atoi(verStr) + if err != nil { + continue + } + if ver < bestVer { + continue + } + bestVer = ver + bestPlat = platform + } + if bestPlat == "" { + return "", fmt.Errorf("no platforms found in %q", sdk) + } + return bestPlat, nil +} + +func latestCompiler(tcRoot, a string, minsdk int) (string, error) { + arch := allArchs[a] + allComps, err := filepath.Glob(filepath.Join(tcRoot, "bin", + arch.clangArch+"*-clang")) + if err != nil { + return "", err + } + var bestVer int + var firstVer int + var bestCompiler string + var firstCompiler string + for _, compiler := range allComps { + var ver int + pattern := filepath.Join(tcRoot, "bin", arch.clangArch) + "%d-clang" + if n, err := fmt.Sscanf(compiler, pattern, &ver); n < 1 || err != nil { + continue + } + if firstCompiler == "" || ver < firstVer { + firstVer = ver + firstCompiler = compiler + } + if ver < bestVer { + continue + } + if ver > minsdk { + continue + } + bestVer = ver + bestCompiler = compiler + } + if bestCompiler == "" { + bestCompiler = firstCompiler + } + if bestCompiler == "" { + return "", fmt.Errorf("no NDK compiler found for architecture %s in %s", + a, tcRoot) + } + return bestCompiler, nil +} + +func latestTools(sdk string) (string, error) { + allTools, err := filepath.Glob(filepath.Join(sdk, "build-tools", "*")) + if err != nil { + return "", err + } + tools, found := latestVersionPath(allTools) + if !found { + return "", fmt.Errorf("no build-tools found in %q", sdk) + } + return tools, nil +} + +// latestVersionFile finds the path with the highest version +// among paths on the form +// +// /some/path/major.minor.patch +func latestVersionPath(paths []string) (string, bool) { + var bestVer [3]int + var bestDir string +loop: + for _, path := range paths { + name := filepath.Base(path) + s := strings.SplitN(name, ".", 3) + if len(s) != len(bestVer) { + continue + } + var version [3]int + for i, v := range s { + v, err := strconv.Atoi(v) + if err != nil { + continue loop + } + if v < bestVer[i] { + continue loop + } + if v > bestVer[i] { + break + } + version[i] = v + } + bestVer = version + bestDir = path + } + return bestDir, bestDir != "" +} + +func newZipWriter(w io.Writer) *zipWriter { + return &zipWriter{ + w: zip.NewWriter(w), + } +} + +func (z *zipWriter) Close() error { + err := z.w.Close() + if z.err == nil { + z.err = err + } + return z.err +} + +func (z *zipWriter) Create(name string) io.Writer { + if z.err != nil { + return ioutil.Discard + } + w, err := z.w.Create(name) + if err != nil { + z.err = err + return ioutil.Discard + } + return &errWriter{w: w, err: &z.err} +} + +func (z *zipWriter) Store(name, file string) { + z.add(name, file, false) +} + +func (z *zipWriter) Add(name, file string) { + z.add(name, file, true) +} + +func (z *zipWriter) add(name, file string, compressed bool) { + if z.err != nil { + return + } + f, err := os.Open(file) + if err != nil { + z.err = err + return + } + defer f.Close() + fh := &zip.FileHeader{ + Name: name, + } + if compressed { + fh.Method = zip.Deflate + } + w, err := z.w.CreateHeader(fh) + if err != nil { + z.err = err + return + } + if _, err := io.Copy(w, f); err != nil { + z.err = err + return + } +} + +func (w *errWriter) Write(p []byte) (n int, err error) { + if err := *w.err; err != nil { + return 0, err + } + n, err = w.w.Write(p) + *w.err = err + return +} diff --git a/gio/cmd/gogio/build_info.go b/gio/cmd/gogio/build_info.go new file mode 100644 index 0000000..ecda1f3 --- /dev/null +++ b/gio/cmd/gogio/build_info.go @@ -0,0 +1,160 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" +) + +type buildInfo struct { + appID string + archs []string + ldflags string + minsdk int + name string + pkgDir string + pkgPath string + iconPath string + tags string + target string + version int + key string + password string +} + +func newBuildInfo(pkgPath string) (*buildInfo, error) { + pkgMetadata, err := getPkgMetadata(pkgPath) + if err != nil { + return nil, err + } + appID := getAppID(pkgMetadata) + appIcon := filepath.Join(pkgMetadata.Dir, "appicon.png") + if *iconPath != "" { + appIcon = *iconPath + } + bi := &buildInfo{ + appID: appID, + archs: getArchs(), + ldflags: getLdFlags(appID), + minsdk: *minsdk, + name: getPkgName(pkgMetadata), + pkgDir: pkgMetadata.Dir, + pkgPath: pkgPath, + iconPath: appIcon, + tags: *extraTags, + target: *target, + version: *version, + key: *signKey, + password: *signPass, + } + return bi, nil +} + +func getArchs() []string { + if *archNames != "" { + return strings.Split(*archNames, ",") + } + switch *target { + case "js": + return []string{"wasm"} + case "ios", "tvos": + // Only 64-bit support. + return []string{"arm64", "amd64"} + case "android": + return []string{"arm", "arm64", "386", "amd64"} + case "windows": + goarch := os.Getenv("GOARCH") + if goarch == "" { + goarch = runtime.GOARCH + } + return []string{goarch} + default: + // TODO: Add flag tests. + panic("The target value has already been validated, this will never execute.") + } +} + +func getLdFlags(appID string) string { + var ldflags []string + if extra := *extraLdflags; extra != "" { + ldflags = append(ldflags, strings.Split(extra, " ")...) + } + // Pass appID along, to be used for logging on platforms like Android. + ldflags = append(ldflags, + fmt.Sprintf("-X realy.lol/gio/app/internal/log.appID=%s", appID)) + // Pass along all remaining arguments to the app. + if appArgs := flag.Args()[1:]; len(appArgs) > 0 { + ldflags = append(ldflags, + fmt.Sprintf("-X realy.lol/gio/app.extraArgs=%s", + strings.Join(appArgs, "|"))) + } + if m := *linkMode; m != "" { + ldflags = append(ldflags, "-linkmode="+m) + } + return strings.Join(ldflags, " ") +} + +type packageMetadata struct { + PkgPath string + Dir string +} + +func getPkgMetadata(pkgPath string) (*packageMetadata, error) { + pkgImportPath, err := runCmd(exec.Command("go", "list", "-f", + "{{.ImportPath}}", pkgPath)) + if err != nil { + return nil, err + } + pkgDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", pkgPath)) + if err != nil { + return nil, err + } + return &packageMetadata{ + PkgPath: pkgImportPath, + Dir: pkgDir, + }, nil +} + +func getAppID(pkgMetadata *packageMetadata) string { + if *appID != "" { + return *appID + } + elems := strings.Split(pkgMetadata.PkgPath, "/") + domain := strings.Split(elems[0], ".") + name := "" + if len(elems) > 1 { + name = "." + elems[len(elems)-1] + } + if len(elems) < 2 && len(domain) < 2 { + name = "." + domain[0] + domain[0] = "localhost" + } else { + for i := 0; i < len(domain)/2; i++ { + opp := len(domain) - 1 - i + domain[i], domain[opp] = domain[opp], domain[i] + } + } + + pkgDomain := strings.Join(domain, ".") + appid := []rune(pkgDomain + name) + + // a Java-language-style package name may contain upper- and lower-case + // letters and underscores with individual parts separated by '.'. + // https://developer.android.com/guide/topics/manifest/manifest-element + for i, c := range appid { + if !('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || + c == '_' || c == '.') { + appid[i] = '_' + } + } + return string(appid) +} + +func getPkgName(pkgMetadata *packageMetadata) string { + return path.Base(pkgMetadata.PkgPath) +} diff --git a/gio/cmd/gogio/build_info_test.go b/gio/cmd/gogio/build_info_test.go new file mode 100644 index 0000000..397e2a3 --- /dev/null +++ b/gio/cmd/gogio/build_info_test.go @@ -0,0 +1,32 @@ +package main + +import "testing" + +type expval struct { + in, out string +} + +func TestAppID(t *testing.T) { + t.Parallel() + + tests := []expval{ + {"example", "localhost.example"}, + {"example.com", "com.example"}, + {"www.example.com", "com.example.www"}, + {"examplecom/app", "examplecom.app"}, + {"example.com/app", "com.example.app"}, + {"www.example.com/app", "com.example.www.app"}, + {"www.en.example.com/app", "com.example.en.www.app"}, + {"example.com/dir/app", "com.example.app"}, + {"example.com/dir.ext/app", "com.example.app"}, + {"example.com/dir/app.ext", "com.example.app.ext"}, + {"example-com.net/dir/app", "net.example_com.app"}, + } + + for i, test := range tests { + got := getAppID(&packageMetadata{PkgPath: test.in}) + if exp := test.out; got != exp { + t.Errorf("(%d): expected '%s', got '%s'", i, exp, got) + } + } +} diff --git a/gio/cmd/gogio/doc.go b/gio/cmd/gogio/doc.go new file mode 100644 index 0000000..6b788fd --- /dev/null +++ b/gio/cmd/gogio/doc.go @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +The gogio tool builds and packages Gio programs for Android, iOS/tvOS +and WebAssembly. + +Run gogio with no arguments for instructions, or see the examples at +https://realy.lol/gio. +*/ +package main diff --git a/gio/cmd/gogio/e2e_test.go b/gio/cmd/gogio/e2e_test.go new file mode 100644 index 0000000..893f580 --- /dev/null +++ b/gio/cmd/gogio/e2e_test.go @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "bufio" + "errors" + "flag" + "fmt" + "image" + "image/color" + "io" + "io/ioutil" + "os" + "os/exec" + "strings" + "testing" + "time" +) + +var raceEnabled = false + +var headless = flag.Bool("headless", true, + "run end-to-end tests in headless mode") + +const appid = "localhost.gogio.endtoend" + +// TestDriver is implemented by each of the platforms we can run end-to-end +// tests on. None of its methods return any errors, as the errors are directly +// reported to testing.T via methods like Fatal. +type TestDriver interface { + initBase(t *testing.T, width, height int) + + // Start opens the Gio app found at path. The driver should attempt to + // run the app with the base driver's width and height, and the + // platform's background should be white. + // + // When the function returns, the gio app must be ready to use on the + // platform, with its initial frame fully drawn. + Start(path string) + + // Screenshot takes a screenshot of the Gio app on the platform. + Screenshot() image.Image + + // Click performs a pointer click at the specified coordinates, + // including both press and release. It returns when the next frame is + // fully drawn. + Click(x, y int) +} + +type driverBase struct { + *testing.T + + width, height int + + output io.Reader + frameNotifs chan bool +} + +func (d *driverBase) initBase(t *testing.T, width, height int) { + d.T = t + d.width, d.height = width, height +} + +func TestEndToEnd(t *testing.T) { + if testing.Short() { + t.Skipf("end-to-end tests tend to be slow") + } + + t.Parallel() + + const ( + testdataWithGoImportPkgPath = "realy.lol/gio/cmd/gogio/testdata" + testdataWithRelativePkgPath = "testdata/testdata.go" + ) + // Keep this list local, to not reuse TestDriver objects. + subtests := []struct { + name string + driver TestDriver + pkgPath string + }{ + {"X11 using go import path", &X11TestDriver{}, + testdataWithGoImportPkgPath}, + {"X11", &X11TestDriver{}, testdataWithRelativePkgPath}, + {"Wayland", &WaylandTestDriver{}, testdataWithRelativePkgPath}, + {"JS", &JSTestDriver{}, testdataWithRelativePkgPath}, + {"Android", &AndroidTestDriver{}, testdataWithRelativePkgPath}, + {"Windows", &WineTestDriver{}, testdataWithRelativePkgPath}, + } + + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + subtest := subtest // copy the changing loop variable + t.Parallel() + runEndToEndTest(t, subtest.driver, subtest.pkgPath) + }) + } +} + +func runEndToEndTest(t *testing.T, driver TestDriver, pkgPath string) { + size := image.Point{X: 800, Y: 600} + driver.initBase(t, size.X, size.Y) + + t.Log("starting driver and gio app") + driver.Start(pkgPath) + + beef := color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff} + white := color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} + black := color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff} + gray := color.NRGBA{R: 0xbb, G: 0xbb, B: 0xbb, A: 0xff} + red := color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff} + + // These are the four colors at the beginning. + t.Log("taking initial screenshot") + withRetries(t, 4*time.Second, func() error { + img := driver.Screenshot() + size = img.Bounds().Size() // override the default size + return checkImageCorners(img, beef, white, black, gray) + }) + + // TODO(mvdan): implement this properly in the Wayland driver; swaymsg + // almost works to automate clicks, but the button presses end up in the + // wrong coordinates. + if _, ok := driver.(*WaylandTestDriver); ok { + return + } + + // Click the first and last sections to turn them red. + t.Log("clicking twice and taking another screenshot") + driver.Click(1*(size.X/4), 1*(size.Y/4)) + driver.Click(3*(size.X/4), 3*(size.Y/4)) + withRetries(t, 4*time.Second, func() error { + img := driver.Screenshot() + return checkImageCorners(img, red, white, black, red) + }) +} + +// withRetries keeps retrying fn until it succeeds, or until the timeout is hit. +// It uses a rudimentary kind of backoff, which starts with 100ms delays. As +// such, timeout should generally be in the order of seconds. +func withRetries(t *testing.T, timeout time.Duration, fn func() error) { + t.Helper() + + timeoutTimer := time.NewTimer(timeout) + defer timeoutTimer.Stop() + backoff := 100 * time.Millisecond + + tries := 0 + var lastErr error + for { + if lastErr = fn(); lastErr == nil { + return + } + tries++ + t.Logf("retrying after %s", backoff) + + // Use a timer instead of a sleep, so that the timeout can stop + // the backoff early. Don't reuse this timer, since we're not in + // a hot loop, and we don't want tricky code. + backoffTimer := time.NewTimer(backoff) + defer backoffTimer.Stop() + + select { + case <-timeoutTimer.C: + t.Errorf("last error: %v", lastErr) + t.Fatalf("hit timeout of %s after %d tries", timeout, tries) + case <-backoffTimer.C: + } + + // Keep doubling it until a maximum. With the start at 100ms, + // we'll do: 100ms, 200ms, 400ms, 800ms, 1.6s, and 2s forever. + backoff *= 2 + if max := 2 * time.Second; backoff > max { + backoff = max + } + } +} + +type colorMismatch struct { + x, y int + wantRGB, gotRGB [3]uint32 +} + +func (m colorMismatch) String() string { + return fmt.Sprintf("%3d,%-3d got 0x%04x%04x%04x, want 0x%04x%04x%04x", + m.x, m.y, + m.gotRGB[0], m.gotRGB[1], m.gotRGB[2], + m.wantRGB[0], m.wantRGB[1], m.wantRGB[2], + ) +} + +func checkImageCorners(img image.Image, + topLeft, topRight, botLeft, botRight color.Color) error { + // The colors are split in four rectangular sections. Check the corners + // of each of the sections. We check the corners left to right, top to + // bottom, like when reading left-to-right text. + + size := img.Bounds().Size() + var mismatches []colorMismatch + + checkColor := func(x, y int, want color.Color) { + r, g, b, _ := want.RGBA() + got := img.At(x, y) + r_, g_, b_, _ := got.RGBA() + if r_ != r || g_ != g || b_ != b { + mismatches = append(mismatches, colorMismatch{ + x: x, + y: y, + wantRGB: [3]uint32{r, g, b}, + gotRGB: [3]uint32{r_, g_, b_}, + }) + } + } + + { + minX, minY := 5, 5 + maxX, maxY := (size.X/2)-5, (size.Y/2)-5 + checkColor(minX, minY, topLeft) + checkColor(maxX, minY, topLeft) + checkColor(minX, maxY, topLeft) + checkColor(maxX, maxY, topLeft) + } + { + minX, minY := (size.X/2)+5, 5 + maxX, maxY := size.X-5, (size.Y/2)-5 + checkColor(minX, minY, topRight) + checkColor(maxX, minY, topRight) + checkColor(minX, maxY, topRight) + checkColor(maxX, maxY, topRight) + } + { + minX, minY := 5, (size.Y/2)+5 + maxX, maxY := (size.X/2)-5, size.Y-5 + checkColor(minX, minY, botLeft) + checkColor(maxX, minY, botLeft) + checkColor(minX, maxY, botLeft) + checkColor(maxX, maxY, botLeft) + } + { + minX, minY := (size.X/2)+5, (size.Y/2)+5 + maxX, maxY := size.X-5, size.Y-5 + checkColor(minX, minY, botRight) + checkColor(maxX, minY, botRight) + checkColor(minX, maxY, botRight) + checkColor(maxX, maxY, botRight) + } + if n := len(mismatches); n > 0 { + b := new(strings.Builder) + fmt.Fprintf(b, "encountered %d color mismatches:\n", n) + for _, m := range mismatches { + fmt.Fprintf(b, "%s\n", m) + } + return errors.New(b.String()) + } + return nil +} + +func (d *driverBase) waitForFrame() { + d.Helper() + + if d.frameNotifs == nil { + // Start the goroutine that reads output lines and notifies of + // new frames via frameNotifs. The test doesn't wait for this + // goroutine to finish; it will naturally end when the output + // reader reaches an error like EOF. + d.frameNotifs = make(chan bool, 1) + if d.output == nil { + d.Fatal("need an output reader to be notified of frames") + } + go func() { + scanner := bufio.NewScanner(d.output) + for scanner.Scan() { + line := scanner.Text() + d.Log(line) + if strings.Contains(line, "gio frame ready") { + d.frameNotifs <- true + } + } + // Since we're only interested in the output while the + // app runs, and we don't know when it finishes here, + // ignore "already closed" pipe errors. + if err := scanner.Err(); err != nil && !errors.Is(err, + os.ErrClosed) { + d.Errorf("reading app output: %v", err) + } + }() + } + + // Unfortunately, there isn't a way to select on a test failing, since + // testing.T doesn't have anything like a context or a "done" channel. + // + // We can't let selects block forever, since the default -test.timeout + // is ten minutes - far too long for tests that take seconds. + // + // For now, a static short timeout is better than nothing. 5s is plenty + // for our simple test app to render on any device. + select { + case <-d.frameNotifs: + case <-time.After(5 * time.Second): + d.Fatalf("timed out waiting for a frame to be ready") + } +} + +func (d *driverBase) needPrograms(names ...string) { + d.Helper() + for _, name := range names { + if _, err := exec.LookPath(name); err != nil { + d.Skipf("%s needed to run", name) + } + } +} + +func (d *driverBase) tempDir(name string) string { + d.Helper() + dir, err := ioutil.TempDir("", name) + if err != nil { + d.Fatal(err) + } + d.Cleanup(func() { os.RemoveAll(dir) }) + return dir +} + +func (d *driverBase) gogio(args ...string) { + d.Helper() + prog, err := os.Executable() + if err != nil { + d.Fatal(err) + } + cmd := exec.Command(prog, args...) + cmd.Env = append(os.Environ(), "RUN_GOGIO=1") + if out, err := cmd.CombinedOutput(); err != nil { + d.Fatalf("gogio error: %s:\n%s", err, out) + } +} diff --git a/gio/cmd/gogio/help.go b/gio/cmd/gogio/help.go new file mode 100644 index 0000000..c83d7a3 --- /dev/null +++ b/gio/cmd/gogio/help.go @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +const mainUsage = `The gogio command builds and packages Gio (realy.lol/gio) programs. + +Usage: + + gogio -target [flags] [run arguments] + +The gogio tool builds and packages Gio programs for platforms where additional +metadata or support files are required. + +The package argument specifies an import path or a single Go source file to +package. Any run arguments are appended to os.Args at runtime. + +Compiled Java class files from jar files in the package directory are +included in Android builds. + +The mandatory -target flag selects the target platform: ios or android for the +mobile platforms, tvos for Apple's tvOS, js for WebAssembly/WebGL. + +The -arch flag specifies a comma separated list of GOARCHs to include. The +default is all supported architectures. + +The -o flag specifies an output file or directory, depending on the target. + +The -buildmode flag selects the build mode. Two build modes are available, exe +and archive. Buildmode exe outputs an .ipa file for iOS or tvOS, an .apk file +for Android or a directory with the WebAssembly module and support files for +a browser. + +The -ldflags and -tags flags pass extra linker flags and tags to the go tool. + +As a special case for iOS or tvOS, specifying a path that ends with ".app" +will output an app directory suitable for a simulator. + +The other buildmode is archive, which will output an .aar library for Android +or a .framework for iOS and tvOS. + +The -icon flag specifies a path to a PNG image to use as app icon on iOS and Android. +If left unspecified, the appicon.png file from the main package is used +(if it exists). + +The -appid flag specifies the package name for Android or the bundle id for +iOS and tvOS. A bundle id must be provisioned through Xcode before the gogio +tool can use it. + +The -version flag specifies the integer version code for Android and the last +component of the 1.0.X version for iOS and tvOS. + +For Android builds the -minsdk flag specify the minimum SDK level. For example, +use -minsdk 22 to target Android 5.1 (Lollipop) and later. + +For Windows builds the -minsdk flag specify the minimum OS version. For example, +use -mindk 10 to target Windows 10 only, -minsdk 6 for Windows Vista and later. + +The -work flag prints the path to the working directory and suppress +its deletion. + +The -x flag will print all the external commands executed by the gogio tool. + +The -signkey flag specifies the path of the keystore, used for signing Android apk/aab files. + +The -signpass flag specifies the password of the keystore, ignored if -signkey is not provided. +` diff --git a/gio/cmd/gogio/iosbuild.go b/gio/cmd/gogio/iosbuild.go new file mode 100644 index 0000000..9674431 --- /dev/null +++ b/gio/cmd/gogio/iosbuild.go @@ -0,0 +1,591 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "archive/zip" + "crypto/sha1" + "encoding/hex" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "golang.org/x/sync/errgroup" +) + +const minIOSVersion = "9.0" + +func buildIOS(tmpDir, target string, bi *buildInfo) error { + appName := bi.name + switch *buildMode { + case "archive": + framework := *destPath + if framework == "" { + framework = fmt.Sprintf("%s.framework", strings.Title(appName)) + } + return archiveIOS(tmpDir, target, framework, bi) + case "exe": + out := *destPath + if out == "" { + out = appName + ".ipa" + } + forDevice := strings.HasSuffix(out, ".ipa") + // Filter out unsupported architectures. + for i := len(bi.archs) - 1; i >= 0; i-- { + switch bi.archs[i] { + case "arm", "arm64": + if forDevice { + continue + } + case "386", "amd64": + if !forDevice { + continue + } + } + + bi.archs = append(bi.archs[:i], bi.archs[i+1:]...) + } + tmpFramework := filepath.Join(tmpDir, "Gio.framework") + if err := archiveIOS(tmpDir, target, tmpFramework, bi); err != nil { + return err + } + if !forDevice && !strings.HasSuffix(out, ".app") { + return fmt.Errorf("the specified output directory %q does not end in .app or .ipa", + out) + } + if !forDevice { + return exeIOS(tmpDir, target, out, bi) + } + payload := filepath.Join(tmpDir, "Payload") + appDir := filepath.Join(payload, appName+".app") + if err := os.MkdirAll(appDir, 0755); err != nil { + return err + } + if err := exeIOS(tmpDir, target, appDir, bi); err != nil { + return err + } + if err := signIOS(bi, tmpDir, appDir); err != nil { + return err + } + return zipDir(out, tmpDir, "Payload") + default: + panic("unreachable") + } +} + +func signIOS(bi *buildInfo, tmpDir, app string) error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + provPattern := filepath.Join(home, "Library", "MobileDevice", + "Provisioning Profiles", "*.mobileprovision") + provisions, err := filepath.Glob(provPattern) + if err != nil { + return err + } + provInfo := filepath.Join(tmpDir, "provision.plist") + var avail []string + for _, prov := range provisions { + // Decode the provision file to a plist. + _, err := runCmd(exec.Command("security", "cms", "-D", "-i", prov, "-o", + provInfo)) + if err != nil { + return err + } + expUnix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", + "Print:ExpirationDate", provInfo)) + if err != nil { + return err + } + exp, err := time.Parse(time.UnixDate, expUnix) + if err != nil { + return fmt.Errorf("sign: failed to parse expiration date from %q: %v", + prov, err) + } + if exp.Before(time.Now()) { + continue + } + appIDPrefix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", + "Print:ApplicationIdentifierPrefix:0", provInfo)) + if err != nil { + return err + } + provAppID, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", + "Print:Entitlements:application-identifier", provInfo)) + if err != nil { + return err + } + expAppID := fmt.Sprintf("%s.%s", appIDPrefix, bi.appID) + avail = append(avail, provAppID) + if expAppID != provAppID { + continue + } + // Copy provisioning file. + embedded := filepath.Join(app, "embedded.mobileprovision") + if err := copyFile(embedded, prov); err != nil { + return err + } + certDER, err := runCmdRaw(exec.Command("/usr/libexec/PlistBuddy", "-c", + "Print:DeveloperCertificates:0", provInfo)) + if err != nil { + return err + } + // Omit trailing newline. + certDER = certDER[:len(certDER)-1] + entitlements, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", + "-x", "-c", "Print:Entitlements", provInfo)) + if err != nil { + return err + } + entFile := filepath.Join(tmpDir, "entitlements.plist") + if err := ioutil.WriteFile(entFile, []byte(entitlements), + 0660); err != nil { + return err + } + identity := sha1.Sum(certDER) + idHex := hex.EncodeToString(identity[:]) + _, err = runCmd(exec.Command("codesign", "-s", idHex, "-v", + "--entitlements", entFile, app)) + return err + } + return fmt.Errorf("sign: no valid provisioning profile found for bundle id %q among %v", + bi.appID, avail) +} + +func exeIOS(tmpDir, target, app string, bi *buildInfo) error { + if bi.appID == "" { + return errors.New("app id is empty; use -appid to set it") + } + if err := os.RemoveAll(app); err != nil { + return err + } + if err := os.Mkdir(app, 0755); err != nil { + return err + } + mainm := filepath.Join(tmpDir, "main.m") + const mainmSrc = `@import UIKit; +@import Gio; + +@interface GioAppDelegate : UIResponder +@property (strong, nonatomic) UIWindow *window; +@end + +@implementation GioAppDelegate +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + GioViewController *controller = [[GioViewController alloc] initWithNibName:nil bundle:nil]; + self.window.rootViewController = controller; + [self.window makeKeyAndVisible]; + return YES; +} +@end + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([GioAppDelegate class])); + } +}` + if err := ioutil.WriteFile(mainm, []byte(mainmSrc), 0660); err != nil { + return err + } + appName := strings.Title(bi.name) + exe := filepath.Join(app, appName) + lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create") + var builds errgroup.Group + for _, a := range bi.archs { + clang, cflags, err := iosCompilerFor(target, a) + if err != nil { + return err + } + exeSlice := filepath.Join(tmpDir, "app-"+a) + lipo.Args = append(lipo.Args, exeSlice) + compile := exec.Command(clang, cflags...) + compile.Args = append(compile.Args, + "-Werror", + "-fmodules", + "-fobjc-arc", + "-x", "objective-c", + "-F", tmpDir, + "-o", exeSlice, + mainm, + ) + builds.Go(func() error { + _, err := runCmd(compile) + return err + }) + } + if err := builds.Wait(); err != nil { + return err + } + if _, err := runCmd(lipo); err != nil { + return err + } + infoPlist := buildInfoPlist(bi) + plistFile := filepath.Join(app, "Info.plist") + if err := ioutil.WriteFile(plistFile, []byte(infoPlist), 0660); err != nil { + return err + } + if _, err := os.Stat(bi.iconPath); err == nil { + assetPlist, err := iosIcons(bi, tmpDir, app, bi.iconPath) + if err != nil { + return err + } + // Merge assets plist with Info.plist + cmd := exec.Command( + "/usr/libexec/PlistBuddy", + "-c", "Merge "+assetPlist, + plistFile, + ) + if _, err := runCmd(cmd); err != nil { + return err + } + } + if _, err := runCmd(exec.Command("plutil", "-convert", "binary1", + plistFile)); err != nil { + return err + } + return nil +} + +// iosIcons builds an asset catalog and compile it with the Xcode command actool. +// iosIcons returns the asset plist file to be merged into Info.plist. +func iosIcons(bi *buildInfo, tmpDir, appDir, icon string) (string, error) { + assets := filepath.Join(tmpDir, "Assets.xcassets") + if err := os.Mkdir(assets, 0700); err != nil { + return "", err + } + appIcon := filepath.Join(assets, "AppIcon.appiconset") + err := buildIcons(appIcon, icon, []iconVariant{ + {path: "ios_2x.png", size: 120}, + {path: "ios_3x.png", size: 180}, + // The App Store icon is not allowed to contain + // transparent pixels. + {path: "ios_store.png", size: 1024, fill: true}, + }) + if err != nil { + return "", err + } + contentJson := `{ + "images" : [ + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "ios_2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "ios_3x.png", + "scale" : "3x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "ios_store.png", + "scale" : "1x" + } + ] +}` + contentFile := filepath.Join(appIcon, "Contents.json") + if err := ioutil.WriteFile(contentFile, []byte(contentJson), + 0600); err != nil { + return "", err + } + assetPlist := filepath.Join(tmpDir, "assets.plist") + compile := exec.Command( + "actool", + "--compile", appDir, + "--platform", iosPlatformFor(bi.target), + "--minimum-deployment-target", minIOSVersion, + "--app-icon", "AppIcon", + "--output-partial-info-plist", assetPlist, + assets) + _, err = runCmd(compile) + return assetPlist, err +} + +func buildInfoPlist(bi *buildInfo) string { + appName := strings.Title(bi.name) + platform := iosPlatformFor(bi.target) + var supportPlatform string + switch bi.target { + case "ios": + supportPlatform = "iPhoneOS" + case "tvos": + supportPlatform = "AppleTVOS" + } + return fmt.Sprintf(` + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + %s + CFBundleIdentifier + %s + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + %s + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0.%d + CFBundleVersion + %d + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + arm64 + DTPlatformName + %s + DTPlatformVersion + 12.4 + MinimumOSVersion + %s + UIDeviceFamily + + 1 + + CFBundleSupportedPlatforms + + %s + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 16G73 + DTSDKBuild + 16G73 + DTSDKName + %s12.4 + DTXcode + 1030 + DTXcodeBuild + 10G8 + +`, appName, bi.appID, appName, bi.version, bi.version, platform, + minIOSVersion, supportPlatform, platform) +} + +func iosPlatformFor(target string) string { + switch target { + case "ios": + return "iphoneos" + case "tvos": + return "appletvos" + default: + panic("invalid platform " + target) + } +} + +func archiveIOS(tmpDir, target, frameworkRoot string, bi *buildInfo) error { + framework := filepath.Base(frameworkRoot) + const suf = ".framework" + if !strings.HasSuffix(framework, suf) { + return fmt.Errorf("the specified output %q does not end in '.framework'", + frameworkRoot) + } + framework = framework[:len(framework)-len(suf)] + if err := os.RemoveAll(frameworkRoot); err != nil { + return err + } + frameworkDir := filepath.Join(frameworkRoot, "Versions", "A") + for _, dir := range []string{"Headers", "Modules"} { + p := filepath.Join(frameworkDir, dir) + if err := os.MkdirAll(p, 0755); err != nil { + return err + } + } + symlinks := [][2]string{ + {"Versions/Current/Headers", "Headers"}, + {"Versions/Current/Modules", "Modules"}, + {"Versions/Current/" + framework, framework}, + {"A", filepath.Join("Versions", "Current")}, + } + for _, l := range symlinks { + if err := os.Symlink(l[0], filepath.Join(frameworkRoot, + l[1])); err != nil && !os.IsExist(err) { + return err + } + } + exe := filepath.Join(frameworkDir, framework) + lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create") + var builds errgroup.Group + tags := bi.tags + goos := "ios" + supportsIOS, err := supportsGOOS("ios") + if err != nil { + return err + } + if !supportsIOS { + // Go 1.15 and earlier target iOS with GOOS=darwin, tags=ios. + goos = "darwin" + tags = "ios " + tags + } + for _, a := range bi.archs { + clang, cflags, err := iosCompilerFor(target, a) + if err != nil { + return err + } + lib := filepath.Join(tmpDir, "gio-"+a) + cmd := exec.Command( + "go", + "build", + "-ldflags=-s -w "+bi.ldflags, + "-buildmode=c-archive", + "-o", lib, + "-tags", tags, + bi.pkgPath, + ) + lipo.Args = append(lipo.Args, lib) + cflagsLine := strings.Join(cflags, " ") + cmd.Env = append( + os.Environ(), + "GOOS="+goos, + "GOARCH="+a, + "CGO_ENABLED=1", + "CC="+clang, + "CGO_CFLAGS="+cflagsLine, + "CGO_LDFLAGS="+cflagsLine, + ) + builds.Go(func() error { + _, err := runCmd(cmd) + return err + }) + } + if err := builds.Wait(); err != nil { + return err + } + if _, err := runCmd(lipo); err != nil { + return err + } + appDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", + "realy.lol/gio/app/internal/wm")) + if err != nil { + return err + } + headerDst := filepath.Join(frameworkDir, "Headers", framework+".h") + headerSrc := filepath.Join(appDir, "framework_ios.h") + if err := copyFile(headerDst, headerSrc); err != nil { + return err + } + module := fmt.Sprintf(`framework module "%s" { + header "%[1]s.h" + + export * +}`, framework) + moduleFile := filepath.Join(frameworkDir, "Modules", "module.modulemap") + return ioutil.WriteFile(moduleFile, []byte(module), 0644) +} + +func supportsGOOS(wantGoos string) (bool, error) { + geese, err := runCmd(exec.Command("go", "tool", "dist", "list")) + if err != nil { + return false, err + } + for _, pair := range strings.Split(geese, "\n") { + s := strings.SplitN(pair, "/", 2) + if len(s) != 2 { + return false, fmt.Errorf("go tool dist list: invalid GOOS/GOARCH pair: %s", + pair) + } + goos := s[0] + if goos == wantGoos { + return true, nil + } + } + return false, nil +} + +func iosCompilerFor(target, arch string) (string, []string, error) { + var platformSDK string + var platformOS string + switch target { + case "ios": + platformOS = "ios" + platformSDK = "iphone" + case "tvos": + platformOS = "tvos" + platformSDK = "appletv" + } + switch arch { + case "arm", "arm64": + platformSDK += "os" + case "386", "amd64": + platformOS += "-simulator" + platformSDK += "simulator" + default: + return "", nil, fmt.Errorf("unsupported -arch: %s", arch) + } + sdkPath, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, + "--show-sdk-path")) + if err != nil { + return "", nil, err + } + clang, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--find", + "clang")) + if err != nil { + return "", nil, err + } + cflags := []string{ + "-fembed-bitcode", + "-arch", allArchs[arch].iosArch, + "-isysroot", sdkPath, + "-m" + platformOS + "-version-min=" + minIOSVersion, + } + return clang, cflags, nil +} + +func zipDir(dst, base, dir string) (err error) { + f, err := os.Create(dst) + if err != nil { + return err + } + defer func() { + if cerr := f.Close(); err == nil { + err = cerr + } + }() + zipf := zip.NewWriter(f) + err = filepath.Walk(filepath.Join(base, dir), + func(path string, f os.FileInfo, err error) error { + if err != nil { + return err + } + if f.IsDir() { + return nil + } + rel := filepath.ToSlash(path[len(base)+1:]) + entry, err := zipf.Create(rel) + if err != nil { + return err + } + src, err := os.Open(path) + if err != nil { + return err + } + defer src.Close() + _, err = io.Copy(entry, src) + return err + }) + if err != nil { + return err + } + return zipf.Close() +} diff --git a/gio/cmd/gogio/js_test.go b/gio/cmd/gogio/js_test.go new file mode 100644 index 0000000..2918737 --- /dev/null +++ b/gio/cmd/gogio/js_test.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "bytes" + "context" + "errors" + "image" + "image/png" + "io" + "net/http" + "net/http/httptest" + "os/exec" + + "github.com/chromedp/cdproto/runtime" + "github.com/chromedp/chromedp" + +) + +type JSTestDriver struct { + driverBase + + // ctx is the chromedp context. + ctx context.Context +} + +func (d *JSTestDriver) Start(path string) { + if raceEnabled { + d.Skipf("js/wasm doesn't support -race; skipping") + } + + // First, build the app. + dir := d.tempDir("gio-endtoend-js") + d.gogio("-target=js", "-o="+dir, path) + + // Second, start Chrome. + opts := append(chromedp.DefaultExecAllocatorOptions[:], + chromedp.Flag("headless", *headless), + ) + + actx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) + d.Cleanup(cancel) + + ctx, cancel := chromedp.NewContext(actx, + // Send all logf/errf calls to t.Logf + chromedp.WithLogf(d.Logf), + ) + d.Cleanup(cancel) + d.ctx = ctx + + if err := chromedp.Run(ctx); err != nil { + if errors.Is(err, exec.ErrNotFound) { + d.Skipf("test requires Chrome to be installed: %v", err) + return + } + d.Fatal(err) + } + pr, pw := io.Pipe() + d.Cleanup(func() { pw.Close() }) + d.output = pr + chromedp.ListenTarget(ctx, func(ev interface{}) { + switch ev := ev.(type) { + case *runtime.EventConsoleAPICalled: + switch ev.Type { + case "log", "info", "warning", "error": + var b bytes.Buffer + b.WriteString("console.") + b.WriteString(string(ev.Type)) + b.WriteString("(") + for i, arg := range ev.Args { + if i > 0 { + b.WriteString(", ") + } + b.Write(arg.Value) + } + b.WriteString(")\n") + pw.Write(b.Bytes()) + } + } + }) + + // Third, serve the app folder, set the browser tab dimensions, and + // navigate to the folder. + ts := httptest.NewServer(http.FileServer(http.Dir(dir))) + d.Cleanup(ts.Close) + + if err := chromedp.Run(ctx, + chromedp.EmulateViewport(int64(d.width), int64(d.height)), + chromedp.Navigate(ts.URL), + ); err != nil { + d.Fatal(err) + } + + // Wait for the gio app to render. + d.waitForFrame() +} + +func (d *JSTestDriver) Screenshot() image.Image { + var buf []byte + if err := chromedp.Run(d.ctx, + chromedp.CaptureScreenshot(&buf), + ); err != nil { + d.Fatal(err) + } + img, err := png.Decode(bytes.NewReader(buf)) + if err != nil { + d.Fatal(err) + } + return img +} + +func (d *JSTestDriver) Click(x, y int) { + if err := chromedp.Run(d.ctx, + chromedp.MouseClickXY(float64(x), float64(y)), + ); err != nil { + d.Fatal(err) + } + + // Wait for the gio app to render after this click. + d.waitForFrame() +} diff --git a/gio/cmd/gogio/jsbuild.go b/gio/cmd/gogio/jsbuild.go new file mode 100644 index 0000000..58bccc1 --- /dev/null +++ b/gio/cmd/gogio/jsbuild.go @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "golang.org/x/tools/go/packages" +) + +func buildJS(bi *buildInfo) error { + out := *destPath + if out == "" { + out = bi.name + } + if err := os.MkdirAll(out, 0700); err != nil { + return err + } + cmd := exec.Command( + "go", + "build", + "-ldflags="+bi.ldflags, + "-tags="+bi.tags, + "-o", filepath.Join(out, "main.wasm"), + bi.pkgPath, + ) + cmd.Env = append( + os.Environ(), + "GOOS=js", + "GOARCH=wasm", + ) + _, err := runCmd(cmd) + if err != nil { + return err + } + if err := ioutil.WriteFile(filepath.Join(out, "index.html"), []byte(jsIndex), 0600); err != nil { + return err + } + goroot, err := runCmd(exec.Command("go", "env", "GOROOT")) + if err != nil { + return err + } + wasmJS := filepath.Join(goroot, "misc", "wasm", "wasm_exec.js") + if _, err := os.Stat(wasmJS); err != nil { + return fmt.Errorf("failed to find $GOROOT/misc/wasm/wasm_exec.js driver: %v", err) + } + pkgs, err := packages.Load(&packages.Config{ + Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps, + Env: append(os.Environ(), "GOOS=js", "GOARCH=wasm"), + }, bi.pkgPath) + if err != nil { + return err + } + extraJS, err := findPackagesJS(pkgs[0], make(map[string]bool)) + if err != nil { + return err + } + + return mergeJSFiles(filepath.Join(out, "wasm.js"), append([]string{wasmJS}, extraJS...)...) +} + +func findPackagesJS(p *packages.Package, visited map[string]bool) (extraJS []string, err error) { + if len(p.GoFiles) == 0 { + return nil, nil + } + js, err := filepath.Glob(filepath.Join(filepath.Dir(p.GoFiles[0]), "*_js.js")) + if err != nil { + return nil, err + } + extraJS = append(extraJS, js...) + for _, imp := range p.Imports { + if !visited[imp.ID] { + extra, err := findPackagesJS(imp, visited) + if err != nil { + return nil, err + } + extraJS = append(extraJS, extra...) + visited[imp.ID] = true + } + } + return extraJS, nil +} + +// mergeJSFiles will merge all files into a single `wasm.js`. It will prepend the jsSetGo +// and append the jsStartGo. +func mergeJSFiles(dst string, files ...string) (err error) { + w, err := os.Create(dst) + if err != nil { + return err + } + defer func() { + if cerr := w.Close(); err != nil { + err = cerr + } + }() + _, err = io.Copy(w, strings.NewReader(jsSetGo)) + if err != nil { + return err + } + for i := range files { + r, err := os.Open(files[i]) + if err != nil { + return err + } + _, err = io.Copy(w, r) + r.Close() + if err != nil { + return err + } + } + _, err = io.Copy(w, strings.NewReader(jsStartGo)) + return err +} + +const ( + jsIndex = ` + + + + + + + + + + +` + // jsSetGo sets the `window.go` variable. + jsSetGo = `(() => { + window.go = {argv: [], env: {}, importObject: {go: {}}}; + const argv = new URLSearchParams(location.search).get("argv"); + if (argv) { + window.go["argv"] = argv.split(" "); + } +})();` + // jsStartGo initializes the main.wasm. + jsStartGo = `(() => { + defaultGo = new Go(); + Object.assign(defaultGo["argv"], defaultGo["argv"].concat(go["argv"])); + Object.assign(defaultGo["env"], go["env"]); + for (let key in go["importObject"]) { + if (typeof defaultGo["importObject"][key] === "undefined") { + defaultGo["importObject"][key] = {}; + } + Object.assign(defaultGo["importObject"][key], go["importObject"][key]); + } + window.go = defaultGo; + if (!WebAssembly.instantiateStreaming) { // polyfill + WebAssembly.instantiateStreaming = async (resp, importObject) => { + const source = await (await resp).arrayBuffer(); + return await WebAssembly.instantiate(source, importObject); + }; + } + WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => { + go.run(result.instance); + }); +})();` +) diff --git a/gio/cmd/gogio/main.go b/gio/cmd/gogio/main.go new file mode 100644 index 0000000..da35401 --- /dev/null +++ b/gio/cmd/gogio/main.go @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "image" + "image/color" + "image/png" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "golang.org/x/image/draw" + "golang.org/x/sync/errgroup" +) + +var ( + target = flag.String("target", "", "specify target (ios, tvos, android, js).\n") + archNames = flag.String("arch", "", "specify architecture(s) to include (arm, arm64, amd64).") + minsdk = flag.Int("minsdk", 0, "specify the minimum supported operating system level") + buildMode = flag.String("buildmode", "exe", "specify buildmode (archive, exe)") + destPath = flag.String("o", "", "output file or directory.\nFor -target ios or tvos, use the .app suffix to target simulators.") + appID = flag.String("appid", "", "app identifier (for -buildmode=exe)") + version = flag.Int("version", 1, "app version (for -buildmode=exe)") + printCommands = flag.Bool("x", false, "print the commands") + keepWorkdir = flag.Bool("work", false, "print the name of the temporary work directory and do not delete it when exiting.") + linkMode = flag.String("linkmode", "", "set the -linkmode flag of the go tool") + extraLdflags = flag.String("ldflags", "", "extra flags to the Go linker") + extraTags = flag.String("tags", "", "extra tags to the Go tool") + iconPath = flag.String("icon", "", "specify an icon for iOS and Android") + signKey = flag.String("signkey", "", "specify the path of the keystore to be used to sign Android apk files.") + signPass = flag.String("signpass", "", "specify the password to decrypt the signkey.") + noStrip = flag.Bool("nostrip", false, "leave debugging symbols in produced .so files") +) + +func main() { + flag.Usage = func() { + fmt.Fprint(os.Stderr, mainUsage) + } + flag.Parse() + if err := flagValidate(); err != nil { + fmt.Fprintf(os.Stderr, "gogio: %v\n", err) + os.Exit(1) + } + buildInfo, err := newBuildInfo(flag.Arg(0)) + if err != nil { + fmt.Fprintf(os.Stderr, "gogio: %v\n", err) + os.Exit(1) + } + if err := build(buildInfo); err != nil { + fmt.Fprintf(os.Stderr, "gogio: %v\n", err) + os.Exit(1) + } + os.Exit(0) +} + +func flagValidate() error { + pkgPathArg := flag.Arg(0) + if pkgPathArg == "" { + return errors.New("specify a package") + } + if *target == "" { + return errors.New("please specify -target") + } + switch *target { + case "ios", "tvos", "android", "js", "windows": + default: + return fmt.Errorf("invalid -target %s", *target) + } + switch *buildMode { + case "archive", "exe": + default: + return fmt.Errorf("invalid -buildmode %s", *buildMode) + } + return nil +} + +func build(bi *buildInfo) error { + tmpDir, err := ioutil.TempDir("", "gogio-") + if err != nil { + return err + } + if *keepWorkdir { + fmt.Fprintf(os.Stderr, "WORKDIR=%s\n", tmpDir) + } else { + defer os.RemoveAll(tmpDir) + } + switch *target { + case "js": + return buildJS(bi) + case "ios", "tvos": + return buildIOS(tmpDir, *target, bi) + case "android": + return buildAndroid(tmpDir, bi) + case "windows": + return buildWindows(tmpDir, bi) + default: + panic("unreachable") + } +} + +func runCmdRaw(cmd *exec.Cmd) ([]byte, error) { + if *printCommands { + fmt.Printf("%s\n", strings.Join(cmd.Args, " ")) + } + out, err := cmd.Output() + if err == nil { + return out, nil + } + if err, ok := err.(*exec.ExitError); ok { + return nil, fmt.Errorf("%s failed: %s%s", strings.Join(cmd.Args, " "), out, err.Stderr) + } + return nil, err +} + +func runCmd(cmd *exec.Cmd) (string, error) { + out, err := runCmdRaw(cmd) + return string(bytes.TrimSpace(out)), err +} + +func copyFile(dst, src string) (err error) { + r, err := os.Open(src) + if err != nil { + return err + } + defer r.Close() + w, err := os.Create(dst) + if err != nil { + return err + } + defer func() { + if cerr := w.Close(); err == nil { + err = cerr + } + }() + _, err = io.Copy(w, r) + return err +} + +type arch struct { + iosArch string + jniArch string + clangArch string +} + +var allArchs = map[string]arch{ + "arm": { + iosArch: "armv7", + jniArch: "armeabi-v7a", + clangArch: "armv7a-linux-androideabi", + }, + "arm64": { + iosArch: "arm64", + jniArch: "arm64-v8a", + clangArch: "aarch64-linux-android", + }, + "386": { + iosArch: "i386", + jniArch: "x86", + clangArch: "i686-linux-android", + }, + "amd64": { + iosArch: "x86_64", + jniArch: "x86_64", + clangArch: "x86_64-linux-android", + }, +} + +type iconVariant struct { + path string + size int + fill bool +} + +func buildIcons(baseDir, icon string, variants []iconVariant) error { + f, err := os.Open(icon) + if err != nil { + return err + } + defer f.Close() + img, _, err := image.Decode(f) + if err != nil { + return err + } + var resizes errgroup.Group + for _, v := range variants { + v := v + resizes.Go(func() (err error) { + path := filepath.Join(baseDir, v.path) + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return err + } + f, err := os.Create(path) + if err != nil { + return err + } + defer func() { + if cerr := f.Close(); err == nil { + err = cerr + } + }() + return png.Encode(f, resizeIcon(v, img)) + }) + } + return resizes.Wait() +} + +func resizeIcon(v iconVariant, img image.Image) *image.NRGBA { + scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: v.size, Y: v.size}}) + op := draw.Src + if v.fill { + op = draw.Over + draw.Draw(scaled, scaled.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src) + } + draw.CatmullRom.Scale(scaled, scaled.Bounds(), img, img.Bounds(), op, nil) + + return scaled +} diff --git a/gio/cmd/gogio/main_test.go b/gio/cmd/gogio/main_test.go new file mode 100644 index 0000000..98dcb27 --- /dev/null +++ b/gio/cmd/gogio/main_test.go @@ -0,0 +1,17 @@ +package main + +import ( + "os" + "testing" +) + +func TestMain(m *testing.M) { + if os.Getenv("RUN_GOGIO") != "" { + // Allow the end-to-end tests to call the gogio tool without + // having to build it from scratch, nor having to refactor the + // main function to avoid using global variables. + main() + os.Exit(0) // main already exits, but just in case. + } + os.Exit(m.Run()) +} diff --git a/gio/cmd/gogio/permission.go b/gio/cmd/gogio/permission.go new file mode 100644 index 0000000..b22fcef --- /dev/null +++ b/gio/cmd/gogio/permission.go @@ -0,0 +1,33 @@ +package main + +var AndroidPermissions = map[string][]string{ + "network": { + "android.permission.INTERNET", + }, + "networkstate": { + "android.permission.ACCESS_NETWORK_STATE", + }, + "bluetooth": { + "android.permission.BLUETOOTH", + "android.permission.BLUETOOTH_ADMIN", + "android.permission.ACCESS_FINE_LOCATION", + }, + "camera": { + "android.permission.CAMERA", + }, + "storage": { + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE", + }, +} + +var AndroidFeatures = map[string][]string{ + "default": {`glEsVersion="0x00020000"`, `name="android.hardware.type.pc"`}, + "bluetooth": { + `name="android.hardware.bluetooth"`, + `name="android.hardware.bluetooth_le"`, + }, + "camera": { + `name="android.hardware.camera"`, + }, +} diff --git a/gio/cmd/gogio/race_test.go b/gio/cmd/gogio/race_test.go new file mode 100644 index 0000000..0749936 --- /dev/null +++ b/gio/cmd/gogio/race_test.go @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build race + +package main_test + +func init() { raceEnabled = true } diff --git a/gio/cmd/gogio/testdata/testdata.go b/gio/cmd/gogio/testdata/testdata.go new file mode 100644 index 0000000..b5c2493 --- /dev/null +++ b/gio/cmd/gogio/testdata/testdata.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// A simple app used for gogio's end-to-end tests. +package main + +import ( + "fmt" + "image" + "image/color" + "log" + + "realy.lol/gio/app" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" +) + +func main() { + go func() { + w := app.NewWindow() + if err := loop(w); err != nil { + log.Fatal(err) + } + }() + app.Main() +} + +type notifyFrame int + +const ( + notifyNone notifyFrame = iota + notifyInvalidate + notifyPrint +) + +// notify keeps track of whether we want to print to stdout to notify the user +// when a frame is ready. Initially we want to notify about the first frame. +var notify = notifyInvalidate + +type ( + C = layout.Context + D = layout.Dimensions +) + +func loop(w *app.Window) error { + topLeft := quarterWidget{ + color: color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff}, + } + topRight := quarterWidget{ + color: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, + } + botLeft := quarterWidget{ + color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}, + } + botRight := quarterWidget{ + color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x80}, + } + + var ops op.Ops + for { + e := <-w.Events() + switch e := e.(type) { + case system.DestroyEvent: + return e.Err + case system.FrameEvent: + gtx := layout.NewContext(&ops, e) + // Clear background to white, even on embedded platforms such as webassembly. + paint.Fill(gtx.Ops, color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}) + layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Flexed(1, func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + // r1c1 + layout.Flexed(1, + func(gtx C) D { return topLeft.Layout(gtx) }), + // r1c2 + layout.Flexed(1, + func(gtx C) D { return topRight.Layout(gtx) }), + ) + }), + layout.Flexed(1, func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + // r2c1 + layout.Flexed(1, + func(gtx C) D { return botLeft.Layout(gtx) }), + // r2c2 + layout.Flexed(1, + func(gtx C) D { return botRight.Layout(gtx) }), + ) + }), + ) + + e.Frame(gtx.Ops) + + switch notify { + case notifyInvalidate: + notify = notifyPrint + w.Invalidate() + case notifyPrint: + notify = notifyNone + fmt.Println("gio frame ready") + } + } + } +} + +// quarterWidget paints a quarter of the screen with one color. When clicked, it +// turns red, going back to its normal color when clicked again. +type quarterWidget struct { + color color.NRGBA + + clicked bool +} + +var red = color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff} + +func (w *quarterWidget) Layout(gtx layout.Context) layout.Dimensions { + var color color.NRGBA + if w.clicked { + color = red + } else { + color = w.color + } + + r := image.Rectangle{Max: gtx.Constraints.Max} + paint.FillShape(gtx.Ops, color, clip.Rect(r).Op()) + + pointer.Rect(image.Rectangle{ + Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y), + }).Add(gtx.Ops) + pointer.InputOp{ + Tag: w, + Types: pointer.Press, + }.Add(gtx.Ops) + + for _, e := range gtx.Events(w) { + if e, ok := e.(pointer.Event); ok && e.Type == pointer.Press { + w.clicked = !w.clicked + // notify when we're done updating the frame. + notify = notifyInvalidate + } + } + return layout.Dimensions{Size: gtx.Constraints.Max} +} diff --git a/gio/cmd/gogio/wayland_test.go b/gio/cmd/gogio/wayland_test.go new file mode 100644 index 0000000..df10410 --- /dev/null +++ b/gio/cmd/gogio/wayland_test.go @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "bufio" + "bytes" + "context" + "fmt" + "image" + "image/png" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "sync" + "text/template" + "time" +) + +type WaylandTestDriver struct { + driverBase + + runtimeDir string + socket string + display string +} + +// No bars or anything fancy. Just a white background with our dimensions. +var tmplSwayConfig = template.Must(template.New("").Parse(` +output * bg #FFFFFF solid_color +output * mode {{.Width}}x{{.Height}} +default_border none +`)) + +var rxSwayReady = regexp.MustCompile(`Running compositor on wayland display '(.*)'`) + +func (d *WaylandTestDriver) Start(path string) { + // We want os.Environ, so that it can e.g. find $DISPLAY to run within + // X11. wlroots env vars are documented at: + // https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md + env := os.Environ() + if *headless { + env = append(env, "WLR_BACKENDS=headless") + } + + d.needPrograms( + "sway", // to run a wayland compositor + "grim", // to take screenshots + "swaymsg", // to send input + ) + + // First, build the app. + dir := d.tempDir("gio-endtoend-wayland") + bin := filepath.Join(dir, "red") + flags := []string{"build", "-tags", "nox11", "-o=" + bin} + if raceEnabled { + flags = append(flags, "-race") + } + flags = append(flags, path) + cmd := exec.Command("go", flags...) + if out, err := cmd.CombinedOutput(); err != nil { + d.Fatalf("could not build app: %s:\n%s", err, out) + } + + conf := filepath.Join(dir, "config") + f, err := os.Create(conf) + if err != nil { + d.Fatal(err) + } + defer f.Close() + if err := tmplSwayConfig.Execute(f, struct{ Width, Height int }{ + d.width, d.height, + }); err != nil { + d.Fatal(err) + } + + d.socket = filepath.Join(dir, "socket") + env = append(env, "SWAYSOCK="+d.socket) + d.runtimeDir = dir + env = append(env, "XDG_RUNTIME_DIR="+d.runtimeDir) + + var wg sync.WaitGroup + d.Cleanup(wg.Wait) + + // First, start sway. + { + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, "sway", "--config", conf, "--verbose") + cmd.Env = env + stderr, err := cmd.StderrPipe() + if err != nil { + d.Fatal(err) + } + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + d.Cleanup(func() { + // Give it a chance to exit gracefully, cleaning up + // after itself. After 10ms, the deferred cancel above + // will signal an os.Kill. + cmd.Process.Signal(os.Interrupt) + time.Sleep(10 * time.Millisecond) + }) + + // Wait for sway to be ready. We probably don't need a deadline + // here. + br := bufio.NewReader(stderr) + for { + line, err := br.ReadString('\n') + if err != nil { + d.Fatal(err) + } + if m := rxSwayReady.FindStringSubmatch(line); m != nil { + d.display = m[1] + break + } + } + + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil && !strings.Contains(err.Error(), "interrupt") { + // Don't print all stderr, since we use --verbose. + // TODO(mvdan): if it's useful, probably filter + // errors and show them. + d.Error(err) + } + wg.Done() + }() + } + + // Then, start our program on the sway compositor above. + { + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, bin) + cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display} + output, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + cmd.Stderr = cmd.Stdout + d.output = output + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil { + d.Error(err) + } + wg.Done() + }() + } + + // Wait for the gio app to render. + d.waitForFrame() +} + +func (d *WaylandTestDriver) Screenshot() image.Image { + cmd := exec.Command("grim", "/dev/stdout") + cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display} + out, err := cmd.CombinedOutput() + if err != nil { + d.Errorf("%s", out) + d.Fatal(err) + } + img, err := png.Decode(bytes.NewReader(out)) + if err != nil { + d.Fatal(err) + } + return img +} + +func (d *WaylandTestDriver) swaymsg(args ...interface{}) { + strs := []string{"--socket", d.socket} + for _, arg := range args { + strs = append(strs, fmt.Sprint(arg)) + } + cmd := exec.Command("swaymsg", strs...) + if out, err := cmd.CombinedOutput(); err != nil { + d.Errorf("%s", out) + d.Fatal(err) + } +} + +func (d *WaylandTestDriver) Click(x, y int) { + d.swaymsg("seat", "-", "cursor", "set", x, y) + d.swaymsg("seat", "-", "cursor", "press", "button1") + d.swaymsg("seat", "-", "cursor", "release", "button1") + + // Wait for the gio app to render after this click. + d.waitForFrame() +} diff --git a/gio/cmd/gogio/windows_test.go b/gio/cmd/gogio/windows_test.go new file mode 100644 index 0000000..996b511 --- /dev/null +++ b/gio/cmd/gogio/windows_test.go @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "context" + "image" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "sync" + "time" + + "golang.org/x/image/draw" +) + +// Wine is tightly coupled with X11 at the moment, and we can reuse the same +// methods to automate screenshots and clicks. The main difference is how we +// build and run the app. + +// The only quirk is that it seems impossible for the Wine window to take the +// entirety of the X server's dimensions, even if we try to resize it to take +// the entire display. It seems to want to leave some vertical space empty, +// presumably for window decorations or the "start" bar on Windows. To work +// around that, make the X server 50x50px bigger, and crop the screenshots back +// to the original size. + +type WineTestDriver struct { + X11TestDriver +} + +func (d *WineTestDriver) Start(path string) { + d.needPrograms("wine") + + // First, build the app. + bin := filepath.Join(d.tempDir("gio-endtoend-windows"), "red.exe") + flags := []string{"build", "-o=" + bin} + if raceEnabled { + if runtime.GOOS != "windows" { + // cross-compilation disables CGo, which breaks -race. + d.Skipf("can't cross-compile -race for Windows; skipping") + } + flags = append(flags, "-race") + } + flags = append(flags, path) + cmd := exec.Command("go", flags...) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "GOOS=windows") + if out, err := cmd.CombinedOutput(); err != nil { + d.Fatalf("could not build app: %s:\n%s", err, out) + } + + var wg sync.WaitGroup + d.Cleanup(wg.Wait) + + // Add 50x50px to the display dimensions, as discussed earlier. + d.startServer(&wg, d.width+50, d.height+50) + + // Then, start our program via Wine on the X server above. + { + cacheDir, err := os.UserCacheDir() + if err != nil { + d.Fatal(err) + } + // Use a wine directory separate from the default ~/.wine, so + // that the user's winecfg doesn't affect our test. This will + // default to ~/.cache/gio-e2e-wine. We use the user's cache, + // to reuse a previously set up wineprefix. + wineprefix := filepath.Join(cacheDir, "gio-e2e-wine") + + // First, ensure that wineprefix is up to date with wineboot. + // Wait for this separately from the first frame, as setting up + // a new prefix might take 5s on its own. + env := []string{ + "DISPLAY=" + d.display, + "WINEDEBUG=fixme-all", // hide "fixme" noise + "WINEPREFIX=" + wineprefix, + + // Disable wine-gecko (Explorer) and wine-mono (.NET). + // Otherwise, if not installed, wineboot will get stuck + // with a prompt to install them on the virtual X + // display. Moreover, Gio doesn't need either, and wine + // is faster without them. + "WINEDLLOVERRIDES=mscoree,mshtml=", + } + { + start := time.Now() + cmd := exec.Command("wine", "wineboot", "-i") + cmd.Env = env + // Use a combined output pipe instead of CombinedOutput, + // so that we only wait for the child process to exit, + // and we don't need to wait for all of wine's + // grandchildren to exit and stop writing. This is + // relevant as wine leaves "wineserver" lingering for + // three seconds by default, to be reused later. + stdout, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + cmd.Stderr = cmd.Stdout + if err := cmd.Run(); err != nil { + io.Copy(os.Stderr, stdout) + d.Fatal(err) + } + d.Logf("set up WINEPREFIX in %s", time.Since(start)) + } + + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, "wine", bin) + cmd.Env = env + output, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + cmd.Stderr = cmd.Stdout + d.output = output + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil { + d.Error(err) + } + wg.Done() + }() + } + // Wait for the gio app to render. + d.waitForFrame() + + // xdotool seems to fail at actually moving the window if we use it + // immediately after Gio is ready. Why? + // We can't tell if the windowmove operation worked until we take a + // screenshot, because the getwindowgeometry op reports the 0x0 + // coordinates even if the window wasn't moved properly. + // A sleep of ~20ms seems to be enough on an idle laptop. Use 20x that. + // TODO(mvdan): revisit this, when you have a spare three hours. + time.Sleep(400 * time.Millisecond) + id := d.xdotool("search", "--sync", "--onlyvisible", "--name", "Gio") + d.xdotool("windowmove", "--sync", id, 0, 0) +} + +func (d *WineTestDriver) Screenshot() image.Image { + img := d.X11TestDriver.Screenshot() + // Crop the screenshot back to the original dimensions. + cropped := image.NewRGBA(image.Rect(0, 0, d.width, d.height)) + draw.Draw(cropped, cropped.Bounds(), img, image.Point{}, draw.Src) + return cropped +} diff --git a/gio/cmd/gogio/windowsbuild.go b/gio/cmd/gogio/windowsbuild.go new file mode 100644 index 0000000..1af8668 --- /dev/null +++ b/gio/cmd/gogio/windowsbuild.go @@ -0,0 +1,412 @@ +package main + +import ( + "bytes" + "encoding/binary" + "fmt" + "image/png" + "io" + "math" + "os" + "os/exec" + "path/filepath" + "reflect" + "strconv" + "strings" + "text/template" + + "github.com/akavel/rsrc/binutil" + "github.com/akavel/rsrc/coff" + "golang.org/x/text/encoding/unicode" +) + +func buildWindows(tmpDir string, bi *buildInfo) error { + builder := &windowsBuilder{TempDir: tmpDir} + builder.DestDir = *destPath + if builder.DestDir == "" { + builder.DestDir = bi.pkgPath + } + + name := bi.name + if *destPath != "" { + if filepath.Ext(*destPath) != ".exe" { + return fmt.Errorf("invalid output name %q, it must end with `.exe`", *destPath) + } + name = filepath.Base(*destPath) + } + name = strings.TrimSuffix(name, ".exe") + sdk := bi.minsdk + if sdk > 10 { + return fmt.Errorf("invalid minsdk (%d) it's higher than Windows 10", sdk) + } + version := strconv.Itoa(bi.version) + if bi.version > math.MaxUint16 { + return fmt.Errorf("version (%d) is larger than the maximum (%d)", bi.version, math.MaxUint16) + } + + for _, arch := range bi.archs { + builder.Coff = coff.NewRSRC() + builder.Coff.Arch(arch) + + if err := builder.embedIcon(bi.iconPath); err != nil { + return err + } + + if err := builder.embedManifest(windowsManifest{ + Version: "1.0.0." + version, + WindowsVersion: sdk, + Name: name, + }); err != nil { + return fmt.Errorf("can't create manifest: %v", err) + } + + if err := builder.embedInfo(windowsResources{ + Version: [2]uint32{uint32(1) << 16, uint32(bi.version)}, + VersionHuman: "1.0.0." + version, + Name: name, + Language: 0x0400, // Process Default Language: https://docs.microsoft.com/en-us/previous-versions/ms957130(v=msdn.10) + }); err != nil { + return fmt.Errorf("can't create info: %v", err) + } + + if err := builder.buildResource(bi, name, arch); err != nil { + return fmt.Errorf("can't build the resources: %v", err) + } + + if err := builder.buildProgram(bi, name, arch); err != nil { + return err + } + } + + return nil +} + +type ( + windowsResources struct { + Version [2]uint32 + VersionHuman string + Language uint16 + Name string + } + windowsManifest struct { + Version string + WindowsVersion int + Name string + } + windowsBuilder struct { + TempDir string + DestDir string + Coff *coff.Coff + } +) + +const ( + // https://docs.microsoft.com/en-us/windows/win32/menurc/resource-types + windowsResourceIcon = 3 + windowsResourceIconGroup = windowsResourceIcon + 11 + windowsResourceManifest = 24 + windowsResourceVersion = 16 +) + +type bufferCoff struct { + bytes.Buffer +} + +func (b *bufferCoff) Size() int64 { + return int64(b.Len()) +} + +func (b *windowsBuilder) embedIcon(path string) (err error) { + iconFile, err := os.Open(path) + if err != nil { + return fmt.Errorf("can't read the icon located at %s: %v", path, err) + } + defer iconFile.Close() + + iconImage, err := png.Decode(iconFile) + if err != nil { + return fmt.Errorf("can't decode the PNG file (%s): %v", path, err) + } + + sizes := []int{16, 32, 48, 64, 128, 256} + var iconHeader bufferCoff + + // GRPICONDIR structure. + if err := binary.Write(&iconHeader, binary.LittleEndian, [3]uint16{0, 1, uint16(len(sizes))}); err != nil { + return err + } + + for _, size := range sizes { + var iconBuffer bufferCoff + + if err := png.Encode(&iconBuffer, resizeIcon(iconVariant{size: size, fill: false}, iconImage)); err != nil { + return fmt.Errorf("can't encode image: %v", err) + } + + b.Coff.AddResource(windowsResourceIcon, uint16(size), &iconBuffer) + + if err := binary.Write(&iconHeader, binary.LittleEndian, struct { + Size [2]uint8 + Color [2]uint8 + Planes uint16 + BitCount uint16 + Length uint32 + Id uint16 + }{ + Size: [2]uint8{uint8(size % 256), uint8(size % 256)}, // "0" means 256px. + Planes: 1, + BitCount: 32, + Length: uint32(iconBuffer.Len()), + Id: uint16(size), + }); err != nil { + return err + } + } + + b.Coff.AddResource(windowsResourceIconGroup, 1, &iconHeader) + + return nil +} + +func (b *windowsBuilder) buildResource(buildInfo *buildInfo, name string, arch string) error { + out, err := os.Create(filepath.Join(buildInfo.pkgPath, name+"_windows_"+arch+".syso")) + if err != nil { + return err + } + defer out.Close() + b.Coff.Freeze() + + // See https://github.com/akavel/rsrc/internal/write.go#L13. + w := binutil.Writer{W: out} + binutil.Walk(b.Coff, func(v reflect.Value, path string) error { + if binutil.Plain(v.Kind()) { + w.WriteLE(v.Interface()) + return nil + } + vv, ok := v.Interface().(binutil.SizedReader) + if ok { + w.WriteFromSized(vv) + return binutil.WALK_SKIP + } + return nil + }) + + if w.Err != nil { + return fmt.Errorf("error writing output file: %s", w.Err) + } + + return nil +} + +func (b *windowsBuilder) buildProgram(buildInfo *buildInfo, name string, arch string) error { + dest := b.DestDir + if len(buildInfo.archs) > 1 { + dest = filepath.Join(filepath.Dir(b.DestDir), name+"_"+arch+".exe") + } + + cmd := exec.Command( + "go", + "build", + "-ldflags=-H=windowsgui "+buildInfo.ldflags, + "-tags="+buildInfo.tags, + "-o", dest, + buildInfo.pkgPath, + ) + cmd.Env = append( + os.Environ(), + "GOOS=windows", + "GOARCH="+arch, + ) + _, err := runCmd(cmd) + return err +} + +func (b *windowsBuilder) embedManifest(v windowsManifest) error { + t, err := template.New("manifest").Parse(` + + + {{.Name}} + + + {{if (le .WindowsVersion 10)}} +{{end}} + {{if (le .WindowsVersion 9)}} +{{end}} + {{if (le .WindowsVersion 8)}} +{{end}} + {{if (le .WindowsVersion 7)}} +{{end}} + {{if (le .WindowsVersion 6)}} +{{end}} + + + + + + + + + + + + true + + +`) + if err != nil { + return err + } + + var manifest bufferCoff + if err := t.Execute(&manifest, v); err != nil { + return err + } + + b.Coff.AddResource(windowsResourceManifest, 1, &manifest) + + return nil +} + +func (b *windowsBuilder) embedInfo(v windowsResources) error { + page := uint16(1) + + // https://docs.microsoft.com/pt-br/windows/win32/menurc/vs-versioninfo + t := newValue(valueBinary, "VS_VERSION_INFO", []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/api/VerRsrc/ns-verrsrc-vs_fixedfileinfo + windowsInfoValueFixed{ + Signature: 0xFEEF04BD, + StructVersion: 0x00010000, + FileVersion: v.Version, + ProductVersion: v.Version, + FileFlagMask: 0x3F, + FileFlags: 0, + FileOS: 0x40004, + FileType: 0x1, + FileSubType: 0, + }, + // https://docs.microsoft.com/pt-br/windows/win32/menurc/stringfileinfo + newValue(valueText, "StringFileInfo", []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/menurc/stringtable + newValue(valueText, fmt.Sprintf("%04X%04X", v.Language, page), []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/menurc/string-str + newValue(valueText, "ProductVersion", v.VersionHuman), + newValue(valueText, "FileVersion", v.VersionHuman), + newValue(valueText, "FileDescription", v.Name), + newValue(valueText, "ProductName", v.Name), + // TODO include more data: gogio must have some way to provide such information (like Company Name, Copyright...) + }), + }), + // https://docs.microsoft.com/pt-br/windows/win32/menurc/varfileinfo + newValue(valueBinary, "VarFileInfo", []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/menurc/var-str + newValue(valueBinary, "Translation", uint32(page)<<16|uint32(v.Language)), + }), + }) + + // For some reason the ValueLength of the VS_VERSIONINFO must be the byte-length of `windowsInfoValueFixed`: + t.ValueLength = 52 + + var verrsrc bufferCoff + if _, err := t.WriteTo(&verrsrc); err != nil { + return err + } + + b.Coff.AddResource(windowsResourceVersion, 1, &verrsrc) + + return nil +} + +type windowsInfoValueFixed struct { + Signature uint32 + StructVersion uint32 + FileVersion [2]uint32 + ProductVersion [2]uint32 + FileFlagMask uint32 + FileFlags uint32 + FileOS uint32 + FileType uint32 + FileSubType uint32 + FileDate [2]uint32 +} + +func (v windowsInfoValueFixed) WriteTo(w io.Writer) (_ int64, err error) { + return 0, binary.Write(w, binary.LittleEndian, v) +} + +type windowsInfoValue struct { + Length uint16 + ValueLength uint16 + Type uint16 + Key []byte + Value []byte +} + +func (v windowsInfoValue) WriteTo(w io.Writer) (_ int64, err error) { + // binary.Write doesn't support []byte inside struct. + if err = binary.Write(w, binary.LittleEndian, [3]uint16{v.Length, v.ValueLength, v.Type}); err != nil { + return 0, err + } + if _, err = w.Write(v.Key); err != nil { + return 0, err + } + if _, err = w.Write(v.Value); err != nil { + return 0, err + } + return 0, nil +} + +const ( + valueBinary uint16 = 0 + valueText uint16 = 1 +) + +func newValue(valueType uint16, key string, input interface{}) windowsInfoValue { + v := windowsInfoValue{ + Type: valueType, + Length: 6, + } + + padding := func(in []byte) []byte { + if l := uint16(len(in)) + v.Length; l%4 != 0 { + return append(in, make([]byte, 4-l%4)...) + } + return in + } + + v.Key = padding(utf16Encode(key)) + v.Length += uint16(len(v.Key)) + + switch in := input.(type) { + case string: + v.Value = padding(utf16Encode(in)) + v.ValueLength = uint16(len(v.Value) / 2) + case []io.WriterTo: + var buff bytes.Buffer + for k := range in { + if _, err := in[k].WriteTo(&buff); err != nil { + panic(err) + } + } + v.Value = buff.Bytes() + default: + var buff bytes.Buffer + if err := binary.Write(&buff, binary.LittleEndian, in); err != nil { + panic(err) + } + v.ValueLength = uint16(buff.Len()) + v.Value = buff.Bytes() + } + + v.Length += uint16(len(v.Value)) + + return v +} + +// utf16Encode encodes the string to UTF16 with null-termination. +func utf16Encode(s string) []byte { + b, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder().Bytes([]byte(s)) + if err != nil { + panic(err) + } + return append(b, 0x00, 0x00) // null-termination. +} diff --git a/gio/cmd/gogio/x11_test.go b/gio/cmd/gogio/x11_test.go new file mode 100644 index 0000000..9bb3174 --- /dev/null +++ b/gio/cmd/gogio/x11_test.go @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "bytes" + "context" + "fmt" + "image" + "image/png" + "io" + "math/rand" + "os" + "os/exec" + "path/filepath" + "sync" + "time" +) + +type X11TestDriver struct { + driverBase + + display string +} + +func (d *X11TestDriver) Start(path string) { + // First, build the app. + bin := filepath.Join(d.tempDir("gio-endtoend-x11"), "red") + flags := []string{"build", "-tags", "nowayland", "-o=" + bin} + if raceEnabled { + flags = append(flags, "-race") + } + flags = append(flags, path) + cmd := exec.Command("go", flags...) + if out, err := cmd.CombinedOutput(); err != nil { + d.Fatalf("could not build app: %s:\n%s", err, out) + } + + var wg sync.WaitGroup + d.Cleanup(wg.Wait) + + d.startServer(&wg, d.width, d.height) + + // Then, start our program on the X server above. + { + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, bin) + cmd.Env = []string{"DISPLAY=" + d.display} + output, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + cmd.Stderr = cmd.Stdout + d.output = output + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil { + d.Error(err) + } + wg.Done() + }() + } + + // Wait for the gio app to render. + d.waitForFrame() +} + +func (d *X11TestDriver) startServer(wg *sync.WaitGroup, width, height int) { + // Pick a random display number between 1 and 100,000. Most machines + // will only be using :0, so there's only a 0.001% chance of two + // concurrent test runs to run into a conflict. + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + d.display = fmt.Sprintf(":%d", rnd.Intn(100000)+1) + + var xprog string + xflags := []string{ + "-wr", // we want a white background; the default is black + } + if *headless { + xprog = "Xvfb" // virtual X server + xflags = append(xflags, "-screen", "0", fmt.Sprintf("%dx%dx24", width, height)) + } else { + xprog = "Xephyr" // nested X server as a window + xflags = append(xflags, "-screen", fmt.Sprintf("%dx%d", width, height)) + } + xflags = append(xflags, d.display) + + d.needPrograms( + xprog, // to run the X server + "scrot", // to take screenshots + "xdotool", // to send input + ) + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, xprog, xflags...) + combined := &bytes.Buffer{} + cmd.Stdout = combined + cmd.Stderr = combined + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + d.Cleanup(func() { + // Give it a chance to exit gracefully, cleaning up + // after itself. After 10ms, the deferred cancel above + // will signal an os.Kill. + cmd.Process.Signal(os.Interrupt) + time.Sleep(10 * time.Millisecond) + }) + + // Wait for the X server to be ready. The socket path isn't + // terribly portable, but that's okay for now. + withRetries(d.T, time.Second, func() error { + socket := fmt.Sprintf("/tmp/.X11-unix/X%s", d.display[1:]) + _, err := os.Stat(socket) + return err + }) + + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil { + // Print all output and error. + io.Copy(os.Stdout, combined) + d.Error(err) + } + wg.Done() + }() +} + +func (d *X11TestDriver) Screenshot() image.Image { + cmd := exec.Command("scrot", "--silent", "--overwrite", "/dev/stdout") + cmd.Env = []string{"DISPLAY=" + d.display} + out, err := cmd.CombinedOutput() + if err != nil { + d.Errorf("%s", out) + d.Fatal(err) + } + img, err := png.Decode(bytes.NewReader(out)) + if err != nil { + d.Fatal(err) + } + return img +} + +func (d *X11TestDriver) xdotool(args ...interface{}) string { + d.Helper() + strs := make([]string, len(args)) + for i, arg := range args { + strs[i] = fmt.Sprint(arg) + } + cmd := exec.Command("xdotool", strs...) + cmd.Env = []string{"DISPLAY=" + d.display} + out, err := cmd.CombinedOutput() + if err != nil { + d.Errorf("%s", out) + d.Fatal(err) + } + return string(bytes.TrimSpace(out)) +} + +func (d *X11TestDriver) Click(x, y int) { + d.xdotool("mousemove", "--sync", x, y) + d.xdotool("click", "1") + + // Wait for the gio app to render after this click. + d.waitForFrame() +} diff --git a/gio/f32/affine.go b/gio/f32/affine.go new file mode 100644 index 0000000..667f7e9 --- /dev/null +++ b/gio/f32/affine.go @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32 + +import ( + "fmt" + "math" +) + +// Affine2D represents an affine 2D transformation. The zero value if Affine2D +// represents the identity transform. +type Affine2D struct { + // in order to make the zero value of Affine2D represent the identity + // transform we store it with the identity matrix subtracted, that is + // if the actual transformation matrix is: + // [sx, hx, ox] + // [hy, sy, oy] + // [ 0, 0, 1] + // we store a = sx-1 and e = sy-1 + a, b, c float32 + d, e, f float32 +} + +// NewAffine2D creates a new Affine2D transform from the matrix elements +// in row major order. The rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1]. +func NewAffine2D(sx, hx, ox, hy, sy, oy float32) Affine2D { + return Affine2D{ + a: sx - 1, b: hx, c: ox, + d: hy, e: sy - 1, f: oy, + } +} + +// Offset the transformation. +func (a Affine2D) Offset(offset Point) Affine2D { + return Affine2D{ + a.a, a.b, a.c + offset.X, + a.d, a.e, a.f + offset.Y, + } +} + +// Scale the transformation around the given origin. +func (a Affine2D) Scale(origin, factor Point) Affine2D { + if origin == (Point{}) { + return a.scale(factor) + } + a = a.Offset(origin.Mul(-1)) + a = a.scale(factor) + return a.Offset(origin) +} + +// Rotate the transformation by the given angle (in radians) counter clockwise around the given origin. +func (a Affine2D) Rotate(origin Point, radians float32) Affine2D { + if origin == (Point{}) { + return a.rotate(radians) + } + a = a.Offset(origin.Mul(-1)) + a = a.rotate(radians) + return a.Offset(origin) +} + +// Shear the transformation by the given angle (in radians) around the given origin. +func (a Affine2D) Shear(origin Point, radiansX, radiansY float32) Affine2D { + if origin == (Point{}) { + return a.shear(radiansX, radiansY) + } + a = a.Offset(origin.Mul(-1)) + a = a.shear(radiansX, radiansY) + return a.Offset(origin) +} + +// Mul returns A*B. +func (A Affine2D) Mul(B Affine2D) (r Affine2D) { + r.a = (A.a+1)*(B.a+1) + A.b*B.d - 1 + r.b = (A.a+1)*B.b + A.b*(B.e+1) + r.c = (A.a+1)*B.c + A.b*B.f + A.c + r.d = A.d*(B.a+1) + (A.e+1)*B.d + r.e = A.d*B.b + (A.e+1)*(B.e+1) - 1 + r.f = A.d*B.c + (A.e+1)*B.f + A.f + return r +} + +// Invert the transformation. Note that if the matrix is close to singular +// numerical errors may become large or infinity. +func (a Affine2D) Invert() Affine2D { + if a.a == 0 && a.b == 0 && a.d == 0 && a.e == 0 { + return Affine2D{a: 0, b: 0, c: -a.c, d: 0, e: 0, f: -a.f} + } + a.a += 1 + a.e += 1 + det := a.a*a.e - a.b*a.d + a.a, a.e = a.e/det, a.a/det + a.b, a.d = -a.b/det, -a.d/det + temp := a.c + a.c = -a.a*a.c - a.b*a.f + a.f = -a.d*temp - a.e*a.f + a.a -= 1 + a.e -= 1 + return a +} + +// Transform p by returning a*p. +func (a Affine2D) Transform(p Point) Point { + return Point{ + X: p.X*(a.a+1) + p.Y*a.b + a.c, + Y: p.X*a.d + p.Y*(a.e+1) + a.f, + } +} + +// Elems returns the matrix elements of the transform in row-major order. The +// rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1]. +func (a Affine2D) Elems() (sx, hx, ox, hy, sy, oy float32) { + return a.a + 1, a.b, a.c, a.d, a.e + 1, a.f +} + +func (a Affine2D) scale(factor Point) Affine2D { + return Affine2D{ + (a.a+1)*factor.X - 1, a.b * factor.X, a.c * factor.X, + a.d * factor.Y, (a.e+1)*factor.Y - 1, a.f * factor.Y, + } +} + +func (a Affine2D) rotate(radians float32) Affine2D { + sin, cos := math.Sincos(float64(radians)) + s, c := float32(sin), float32(cos) + return Affine2D{ + (a.a+1)*c - a.d*s - 1, a.b*c - (a.e+1)*s, a.c*c - a.f*s, + (a.a+1)*s + a.d*c, a.b*s + (a.e+1)*c - 1, a.c*s + a.f*c, + } +} + +func (a Affine2D) shear(radiansX, radiansY float32) Affine2D { + tx := float32(math.Tan(float64(radiansX))) + ty := float32(math.Tan(float64(radiansY))) + return Affine2D{ + (a.a + 1) + a.d*tx - 1, a.b + (a.e+1)*tx, a.c + a.f*tx, + (a.a+1)*ty + a.d, a.b*ty + (a.e + 1) - 1, a.f*ty + a.f, + } +} + +func (a Affine2D) String() string { + sx, hx, ox, hy, sy, oy := a.Elems() + return fmt.Sprintf("[[%f %f %f] [%f %f %f]]", sx, hx, ox, hy, sy, oy) +} diff --git a/gio/f32/affine_test.go b/gio/f32/affine_test.go new file mode 100644 index 0000000..4077b8d --- /dev/null +++ b/gio/f32/affine_test.go @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32 + +import ( + "math" + "testing" +) + +func eq(p1, p2 Point) bool { + tol := 1e-5 + dx, dy := p2.X-p1.X, p2.Y-p1.Y + return math.Abs(math.Sqrt(float64(dx*dx+dy*dy))) < tol +} + +func eqaff(x, y Affine2D) bool { + tol := 1e-5 + return math.Abs(float64(x.a-y.a)) < tol && + math.Abs(float64(x.b-y.b)) < tol && + math.Abs(float64(x.c-y.c)) < tol && + math.Abs(float64(x.d-y.d)) < tol && + math.Abs(float64(x.e-y.e)) < tol && + math.Abs(float64(x.f-y.f)) < tol +} + +func TestTransformOffset(t *testing.T) { + p := Point{X: 1, Y: 2} + o := Point{X: 2, Y: -3} + + r := Affine2D{}.Offset(o).Transform(p) + if !eq(r, Pt(3, -1)) { + t.Errorf("offset transformation mismatch: have %v, want {3 -1}", r) + } + i := Affine2D{}.Offset(o).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("offset transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestTransformScale(t *testing.T) { + p := Point{X: 1, Y: 2} + s := Point{X: -1, Y: 2} + + r := Affine2D{}.Scale(Point{}, s).Transform(p) + if !eq(r, Pt(-1, 4)) { + t.Errorf("scale transformation mismatch: have %v, want {-1 4}", r) + } + i := Affine2D{}.Scale(Point{}, s).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("scale transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestTransformRotate(t *testing.T) { + p := Point{X: 1, Y: 0} + a := float32(math.Pi / 2) + + r := Affine2D{}.Rotate(Point{}, a).Transform(p) + if !eq(r, Pt(0, 1)) { + t.Errorf("rotate transformation mismatch: have %v, want {0 1}", r) + } + i := Affine2D{}.Rotate(Point{}, a).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("rotate transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestTransformShear(t *testing.T) { + p := Point{X: 1, Y: 1} + + r := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Transform(p) + if !eq(r, Pt(2, 1)) { + t.Errorf("shear transformation mismatch: have %v, want {2 1}", r) + } + i := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("shear transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestTransformMultiply(t *testing.T) { + p := Point{X: 1, Y: 2} + o := Point{X: 2, Y: -3} + s := Point{X: -1, Y: 2} + a := float32(-math.Pi / 2) + + r := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Transform(p) + if !eq(r, Pt(1, 3)) { + t.Errorf("complex transformation mismatch: have %v, want {1 3}", r) + } + i := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("complex transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestPrimes(t *testing.T) { + xa := NewAffine2D(9, 11, 13, 17, 19, 23) + xb := NewAffine2D(29, 31, 37, 43, 47, 53) + + pa := Point{X: 2, Y: 3} + pb := Point{X: 5, Y: 7} + + for _, test := range []struct { + x Affine2D + p Point + exp Point + }{ + {x: xa, p: pa, exp: Pt(64, 114)}, + {x: xa, p: pb, exp: Pt(135, 241)}, + {x: xb, p: pa, exp: Pt(188, 280)}, + {x: xb, p: pb, exp: Pt(399, 597)}, + } { + got := test.x.Transform(test.p) + if !eq(got, test.exp) { + t.Errorf("%v.Transform(%v): have %v, want %v", test.x, test.p, got, test.exp) + } + } + + for _, test := range []struct { + x Affine2D + exp Affine2D + }{ + {x: xa, exp: NewAffine2D(-1.1875, 0.6875, -0.375, 1.0625, -0.5625, -0.875)}, + {x: xb, exp: NewAffine2D(1.5666667, -1.0333333, -3.2000008, -1.4333333, 1-0.03333336, 1.7999992)}, + } { + got := test.x.Invert() + if !eqaff(got, test.exp) { + t.Errorf("%v.Invert(): have %v, want %v", test.x, got, test.exp) + } + } + + got := xa.Mul(xb) + exp := NewAffine2D(734, 796, 929, 1310, 1420, 1659) + if !eqaff(got, exp) { + t.Errorf("%v.Mul(%v): have %v, want %v", xa, xb, got, exp) + } +} + +func TestTransformScaleAround(t *testing.T) { + p := Pt(-1, -1) + target := Pt(-6, -13) + pt := Affine2D{}.Scale(Pt(4, 5), Pt(2, 3)).Transform(p) + if !eq(pt, target) { + t.Log(pt, "!=", target) + t.Error("Scale not as expected") + } +} + +func TestTransformRotateAround(t *testing.T) { + p := Pt(-1, -1) + pt := Affine2D{}.Rotate(Pt(1, 1), -math.Pi/2).Transform(p) + target := Pt(-1, 3) + if !eq(pt, target) { + t.Log(pt, "!=", target) + t.Error("Rotate not as expected") + } +} + +func TestMulOrder(t *testing.T) { + A := Affine2D{}.Offset(Pt(100, 100)) + B := Affine2D{}.Scale(Point{}, Pt(2, 2)) + _ = A + _ = B + + T1 := Affine2D{}.Offset(Pt(100, 100)).Scale(Point{}, Pt(2, 2)) + T2 := B.Mul(A) + + if T1 != T2 { + t.Log(T1) + t.Log(T2) + t.Error("multiplication / transform order not as expected") + } +} + +func BenchmarkTransformOffset(b *testing.B) { + p := Point{X: 1, Y: 2} + o := Point{X: 0.5, Y: 0.5} + aff := Affine2D{}.Offset(o) + + for i := 0; i < b.N; i++ { + p = aff.Transform(p) + } + _ = p +} + +func BenchmarkTransformScale(b *testing.B) { + p := Point{X: 1, Y: 2} + s := Point{X: 0.5, Y: 0.5} + aff := Affine2D{}.Scale(Point{}, s) + for i := 0; i < b.N; i++ { + p = aff.Transform(p) + } + _ = p +} + +func BenchmarkTransformRotate(b *testing.B) { + p := Point{X: 1, Y: 2} + a := float32(math.Pi / 2) + aff := Affine2D{}.Rotate(Point{}, a) + for i := 0; i < b.N; i++ { + p = aff.Transform(p) + } + _ = p +} + +func BenchmarkTransformTranslateMultiply(b *testing.B) { + a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) + t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}) + + for i := 0; i < b.N; i++ { + a = a.Mul(t) + } +} + +func BenchmarkTransformScaleMultiply(b *testing.B) { + a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) + t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}).Scale(Point{}, Point{X: 0.4, Y: -0.5}) + + for i := 0; i < b.N; i++ { + a = a.Mul(t) + } +} + +func BenchmarkTransformMultiply(b *testing.B) { + a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) + t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}).Rotate(Point{}, math.Pi/7) + + for i := 0; i < b.N; i++ { + a = a.Mul(t) + } +} diff --git a/gio/f32/f32.go b/gio/f32/f32.go new file mode 100644 index 0000000..69745ba --- /dev/null +++ b/gio/f32/f32.go @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package f32 is a float32 implementation of package image's +Point and Rectangle. + +The coordinate space has the origin in the top left +corner with the axes extending right and down. +*/ +package f32 + +import "strconv" + +// A Point is a two dimensional point. +type Point struct { + X, Y float32 +} + +// String return a string representation of p. +func (p Point) String() string { + return "(" + strconv.FormatFloat(float64(p.X), 'f', -1, 32) + + "," + strconv.FormatFloat(float64(p.Y), 'f', -1, 32) + ")" +} + +// A Rectangle contains the points (X, Y) where Min.X <= X < Max.X, +// Min.Y <= Y < Max.Y. +type Rectangle struct { + Min, Max Point +} + +// String return a string representation of r. +func (r Rectangle) String() string { + return r.Min.String() + "-" + r.Max.String() +} + +// Rect is a shorthand for Rectangle{Point{x0, y0}, Point{x1, y1}}. +// The returned Rectangle has x0 and y0 swapped if necessary so that +// it's correctly formed. +func Rect(x0, y0, x1, y1 float32) Rectangle { + if x0 > x1 { + x0, x1 = x1, x0 + } + if y0 > y1 { + y0, y1 = y1, y0 + } + return Rectangle{Point{x0, y0}, Point{x1, y1}} +} + +// Pt is shorthand for Point{X: x, Y: y}. +func Pt(x, y float32) Point { + return Point{X: x, Y: y} +} + +// Add return the point p+p2. +func (p Point) Add(p2 Point) Point { + return Point{X: p.X + p2.X, Y: p.Y + p2.Y} +} + +// Sub returns the vector p-p2. +func (p Point) Sub(p2 Point) Point { + return Point{X: p.X - p2.X, Y: p.Y - p2.Y} +} + +// Mul returns p scaled by s. +func (p Point) Mul(s float32) Point { + return Point{X: p.X * s, Y: p.Y * s} +} + +// In reports whether p is in r. +func (p Point) In(r Rectangle) bool { + return r.Min.X <= p.X && p.X < r.Max.X && + r.Min.Y <= p.Y && p.Y < r.Max.Y +} + +// Size returns r's width and height. +func (r Rectangle) Size() Point { + return Point{X: r.Dx(), Y: r.Dy()} +} + +// Dx returns r's width. +func (r Rectangle) Dx() float32 { + return r.Max.X - r.Min.X +} + +// Dy returns r's Height. +func (r Rectangle) Dy() float32 { + return r.Max.Y - r.Min.Y +} + +// Intersect returns the intersection of r and s. +func (r Rectangle) Intersect(s Rectangle) Rectangle { + if r.Min.X < s.Min.X { + r.Min.X = s.Min.X + } + if r.Min.Y < s.Min.Y { + r.Min.Y = s.Min.Y + } + if r.Max.X > s.Max.X { + r.Max.X = s.Max.X + } + if r.Max.Y > s.Max.Y { + r.Max.Y = s.Max.Y + } + return r +} + +// Union returns the union of r and s. +func (r Rectangle) Union(s Rectangle) Rectangle { + if r.Min.X > s.Min.X { + r.Min.X = s.Min.X + } + if r.Min.Y > s.Min.Y { + r.Min.Y = s.Min.Y + } + if r.Max.X < s.Max.X { + r.Max.X = s.Max.X + } + if r.Max.Y < s.Max.Y { + r.Max.Y = s.Max.Y + } + return r +} + +// Canon returns the canonical version of r, where Min is to +// the upper left of Max. +func (r Rectangle) Canon() Rectangle { + if r.Max.X < r.Min.X { + r.Min.X, r.Max.X = r.Max.X, r.Min.X + } + if r.Max.Y < r.Min.Y { + r.Min.Y, r.Max.Y = r.Max.Y, r.Min.Y + } + return r +} + +// Empty reports whether r represents the empty area. +func (r Rectangle) Empty() bool { + return r.Min.X >= r.Max.X || r.Min.Y >= r.Max.Y +} + +// Add offsets r with the vector p. +func (r Rectangle) Add(p Point) Rectangle { + return Rectangle{ + Point{r.Min.X + p.X, r.Min.Y + p.Y}, + Point{r.Max.X + p.X, r.Max.Y + p.Y}, + } +} + +// Sub offsets r with the vector -p. +func (r Rectangle) Sub(p Point) Rectangle { + return Rectangle{ + Point{r.Min.X - p.X, r.Min.Y - p.Y}, + Point{r.Max.X - p.X, r.Max.Y - p.Y}, + } +} diff --git a/gio/font/gofont/gofont.go b/gio/font/gofont/gofont.go new file mode 100644 index 0000000..9dedcd5 --- /dev/null +++ b/gio/font/gofont/gofont.go @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package gofont exports the Go fonts as a text.Collection. +// +// See https://blog.golang.org/go-fonts for a description of the +// fonts, and the golang.org/x/image/font/gofont packages for the +// font data. +package gofont + +import ( + "fmt" + "sync" + + "golang.org/x/image/font/gofont/gobold" + "golang.org/x/image/font/gofont/gobolditalic" + "golang.org/x/image/font/gofont/goitalic" + "golang.org/x/image/font/gofont/gomedium" + "golang.org/x/image/font/gofont/gomediumitalic" + "golang.org/x/image/font/gofont/gomono" + "golang.org/x/image/font/gofont/gomonobold" + "golang.org/x/image/font/gofont/gomonobolditalic" + "golang.org/x/image/font/gofont/gomonoitalic" + "golang.org/x/image/font/gofont/goregular" + "golang.org/x/image/font/gofont/gosmallcaps" + "golang.org/x/image/font/gofont/gosmallcapsitalic" + + "realy.lol/gio/font/opentype" + "realy.lol/gio/text" +) + +var ( + once sync.Once + collection []text.FontFace +) + +func Collection() []text.FontFace { + once.Do(func() { + register(text.Font{}, goregular.TTF) + register(text.Font{Style: text.Italic}, goitalic.TTF) + register(text.Font{Weight: text.Bold}, gobold.TTF) + register(text.Font{Style: text.Italic, Weight: text.Bold}, + gobolditalic.TTF) + register(text.Font{Weight: text.Medium}, gomedium.TTF) + register(text.Font{Weight: text.Medium, Style: text.Italic}, + gomediumitalic.TTF) + register(text.Font{Variant: "Mono"}, gomono.TTF) + register(text.Font{Variant: "Mono", Weight: text.Bold}, gomonobold.TTF) + register(text.Font{Variant: "Mono", Weight: text.Bold, + Style: text.Italic}, gomonobolditalic.TTF) + register(text.Font{Variant: "Mono", Style: text.Italic}, + gomonoitalic.TTF) + register(text.Font{Variant: "Smallcaps"}, gosmallcaps.TTF) + register(text.Font{Variant: "Smallcaps", Style: text.Italic}, + gosmallcapsitalic.TTF) + // Ensure that any outside appends will not reuse the backing store. + n := len(collection) + collection = collection[:n:n] + }) + return collection +} + +func register(fnt text.Font, ttf []byte) { + face, err := opentype.Parse(ttf) + if err != nil { + panic(fmt.Errorf("failed to parse font: %v", err)) + } + fnt.Typeface = "Go" + collection = append(collection, text.FontFace{Font: fnt, Face: face}) +} diff --git a/gio/font/opentype/opentype.go b/gio/font/opentype/opentype.go new file mode 100644 index 0000000..dd74e73 --- /dev/null +++ b/gio/font/opentype/opentype.go @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package opentype implements text layout and shaping for OpenType +// files. +package opentype + +import ( + "bytes" + "io" + "unicode" + "unicode/utf8" + + "golang.org/x/image/font" + "golang.org/x/image/font/sfnt" + "golang.org/x/image/math/fixed" + + "realy.lol/gio/f32" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/text" +) + +// Font implements text.Face. Its methods are safe to use +// concurrently. +type Font struct { + font *sfnt.Font +} + +// Collection is a collection of one or more fonts. When used as a text.Face, +// each rune will be assigned a glyph from the first font in the collection +// that supports it. +type Collection struct { + fonts []*opentype +} + +type opentype struct { + Font *sfnt.Font + Hinting font.Hinting +} + +// a glyph represents a rune and its advance according to a Font. +// TODO: remove this type and work on io.Readers directly. +type glyph struct { + Rune rune + Advance fixed.Int26_6 +} + +// NewFont parses an SFNT font, such as TTF or OTF data, from a []byte +// data source. +func Parse(src []byte) (*Font, error) { + fnt, err := sfnt.Parse(src) + if err != nil { + return nil, err + } + return &Font{font: fnt}, nil +} + +// ParseCollection parses an SFNT font collection, such as TTC or OTC data, +// from a []byte data source. +// +// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, +// it will return a collection containing 1 font. +func ParseCollection(src []byte) (*Collection, error) { + c, err := sfnt.ParseCollection(src) + if err != nil { + return nil, err + } + return newCollectionFrom(c) +} + +// ParseCollectionReaderAt parses an SFNT collection, such as TTC or OTC data, +// from an io.ReaderAt data source. +// +// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, it +// will return a collection containing 1 font. +func ParseCollectionReaderAt(src io.ReaderAt) (*Collection, error) { + c, err := sfnt.ParseCollectionReaderAt(src) + if err != nil { + return nil, err + } + return newCollectionFrom(c) +} + +func newCollectionFrom(coll *sfnt.Collection) (*Collection, error) { + fonts := make([]*opentype, coll.NumFonts()) + for i := range fonts { + fnt, err := coll.Font(i) + if err != nil { + return nil, err + } + fonts[i] = &opentype{ + Font: fnt, + Hinting: font.HintingFull, + } + } + return &Collection{fonts: fonts}, nil +} + +// NumFonts returns the number of fonts in the collection. +func (c *Collection) NumFonts() int { + return len(c.fonts) +} + +// Font returns the i'th font in the collection. +func (c *Collection) Font(i int) (*Font, error) { + if i < 0 || len(c.fonts) <= i { + return nil, sfnt.ErrNotFound + } + return &Font{font: c.fonts[i].Font}, nil +} + +func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int, + txt io.Reader) ([]text.Line, error) { + glyphs, err := readGlyphs(txt) + if err != nil { + return nil, err + } + fonts := []*opentype{{Font: f.font, Hinting: font.HintingFull}} + var buf sfnt.Buffer + return layoutText(&buf, ppem, maxWidth, fonts, glyphs) +} + +func (f *Font) Shape(ppem fixed.Int26_6, str text.Layout) op.CallOp { + var buf sfnt.Buffer + return textPath(&buf, ppem, + []*opentype{{Font: f.font, Hinting: font.HintingFull}}, str) +} + +func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics { + o := &opentype{Font: f.font, Hinting: font.HintingFull} + var buf sfnt.Buffer + return o.Metrics(&buf, ppem) +} + +func (c *Collection) Layout(ppem fixed.Int26_6, maxWidth int, + txt io.Reader) ([]text.Line, error) { + glyphs, err := readGlyphs(txt) + if err != nil { + return nil, err + } + var buf sfnt.Buffer + return layoutText(&buf, ppem, maxWidth, c.fonts, glyphs) +} + +func (c *Collection) Shape(ppem fixed.Int26_6, str text.Layout) op.CallOp { + var buf sfnt.Buffer + return textPath(&buf, ppem, c.fonts, str) +} + +func fontForGlyph(buf *sfnt.Buffer, fonts []*opentype, r rune) *opentype { + if len(fonts) < 1 { + return nil + } + for _, f := range fonts { + if f.HasGlyph(buf, r) { + return f + } + } + return fonts[0] // Use replacement character from the first font if necessary +} + +func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, + fonts []*opentype, glyphs []glyph) ([]text.Line, error) { + var lines []text.Line + var nextLine text.Line + updateBounds := func(f *opentype) { + m := f.Metrics(sbuf, ppem) + if m.Ascent > nextLine.Ascent { + nextLine.Ascent = m.Ascent + } + // m.Height is equal to m.Ascent + m.Descent + linegap. + // Compute the descent including the linegap. + descent := m.Height - m.Ascent + if descent > nextLine.Descent { + nextLine.Descent = descent + } + b := f.Bounds(sbuf, ppem) + nextLine.Bounds = nextLine.Bounds.Union(b) + } + maxDotX := fixed.I(maxWidth) + type state struct { + r rune + f *opentype + adv fixed.Int26_6 + x fixed.Int26_6 + idx int + len int + valid bool + } + var prev, word state + endLine := func() { + if prev.f == nil && len(fonts) > 0 { + prev.f = fonts[0] + } + updateBounds(prev.f) + nextLine.Layout = toLayout(glyphs[:prev.idx:prev.idx]) + nextLine.Width = prev.x + prev.adv + nextLine.Bounds.Max.X += prev.x + lines = append(lines, nextLine) + glyphs = glyphs[prev.idx:] + nextLine = text.Line{} + prev = state{} + word = state{} + } + for prev.idx < len(glyphs) { + g := &glyphs[prev.idx] + next := state{ + r: g.Rune, + f: fontForGlyph(sbuf, fonts, g.Rune), + idx: prev.idx + 1, + len: prev.len + utf8.RuneLen(g.Rune), + x: prev.x + prev.adv, + } + if next.f != nil { + if next.f != prev.f { + updateBounds(next.f) + } + next.adv, next.valid = next.f.GlyphAdvance(sbuf, ppem, g.Rune) + } + if g.Rune == '\n' { + // The newline is zero width; use the previous + // character for line measurements. + prev.idx = next.idx + prev.len = next.len + endLine() + continue + } + var k fixed.Int26_6 + if prev.valid && next.f != nil { + k = next.f.Kern(sbuf, ppem, prev.r, next.r) + } + // Break the line if we're out of space. + if prev.idx > 0 && next.x+next.adv+k > maxDotX { + // If the line contains no word breaks, break off the last rune. + if word.idx == 0 { + word = prev + } + next.x -= word.x + word.adv + next.idx -= word.idx + next.len -= word.len + prev = word + endLine() + } else if k != 0 { + glyphs[prev.idx-1].Advance += k + next.x += k + } + g.Advance = next.adv + if unicode.IsSpace(g.Rune) { + word = next + } + prev = next + } + endLine() + return lines, nil +} + +// toLayout converts a slice of glyphs to a text.Layout. +func toLayout(glyphs []glyph) text.Layout { + var buf bytes.Buffer + advs := make([]fixed.Int26_6, len(glyphs)) + for i, g := range glyphs { + buf.WriteRune(g.Rune) + advs[i] = glyphs[i].Advance + } + return text.Layout{Text: buf.String(), Advances: advs} +} + +func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, fonts []*opentype, + str text.Layout) op.CallOp { + var lastPos f32.Point + var builder clip.Path + ops := new(op.Ops) + m := op.Record(ops) + var x fixed.Int26_6 + builder.Begin(ops) + rune := 0 + for _, r := range str.Text { + if !unicode.IsSpace(r) { + f := fontForGlyph(buf, fonts, r) + if f == nil { + continue + } + segs, ok := f.LoadGlyph(buf, ppem, r) + if !ok { + continue + } + // Move to glyph position. + pos := f32.Point{ + X: float32(x) / 64, + } + builder.Move(pos.Sub(lastPos)) + lastPos = pos + var lastArg f32.Point + // Convert sfnt.Segments to relative segments. + for _, fseg := range segs { + nargs := 1 + switch fseg.Op { + case sfnt.SegmentOpQuadTo: + nargs = 2 + case sfnt.SegmentOpCubeTo: + nargs = 3 + } + var args [3]f32.Point + for i := 0; i < nargs; i++ { + a := f32.Point{ + X: float32(fseg.Args[i].X) / 64, + Y: float32(fseg.Args[i].Y) / 64, + } + args[i] = a.Sub(lastArg) + if i == nargs-1 { + lastArg = a + } + } + switch fseg.Op { + case sfnt.SegmentOpMoveTo: + builder.Move(args[0]) + case sfnt.SegmentOpLineTo: + builder.Line(args[0]) + case sfnt.SegmentOpQuadTo: + builder.Quad(args[0], args[1]) + case sfnt.SegmentOpCubeTo: + builder.Cube(args[0], args[1], args[2]) + default: + panic("unsupported segment op") + } + } + lastPos = lastPos.Add(lastArg) + } + x += str.Advances[rune] + rune++ + } + clip.Outline{ + Path: builder.End(), + }.Op().Add(ops) + return m.Stop() +} + +func readGlyphs(r io.Reader) ([]glyph, error) { + var glyphs []glyph + buf := make([]byte, 0, 1024) + for { + n, err := r.Read(buf[len(buf):cap(buf)]) + buf = buf[:len(buf)+n] + lim := len(buf) + // Read full runes if possible. + if err != io.EOF { + lim -= utf8.UTFMax - 1 + } + i := 0 + for i < lim { + c, s := utf8.DecodeRune(buf[i:]) + i += s + glyphs = append(glyphs, glyph{Rune: c}) + } + n = copy(buf, buf[i:]) + buf = buf[:n] + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + } + return glyphs, nil +} + +func (f *opentype) HasGlyph(buf *sfnt.Buffer, r rune) bool { + g, err := f.Font.GlyphIndex(buf, r) + return g != 0 && err == nil +} + +func (f *opentype) GlyphAdvance(buf *sfnt.Buffer, ppem fixed.Int26_6, + r rune) (advance fixed.Int26_6, ok bool) { + g, err := f.Font.GlyphIndex(buf, r) + if err != nil { + return 0, false + } + adv, err := f.Font.GlyphAdvance(buf, g, ppem, f.Hinting) + return adv, err == nil +} + +func (f *opentype) Kern(buf *sfnt.Buffer, ppem fixed.Int26_6, + r0, r1 rune) fixed.Int26_6 { + g0, err := f.Font.GlyphIndex(buf, r0) + if err != nil { + return 0 + } + g1, err := f.Font.GlyphIndex(buf, r1) + if err != nil { + return 0 + } + adv, err := f.Font.Kern(buf, g0, g1, ppem, f.Hinting) + if err != nil { + return 0 + } + return adv +} + +func (f *opentype) Metrics(buf *sfnt.Buffer, ppem fixed.Int26_6) font.Metrics { + m, _ := f.Font.Metrics(buf, ppem, f.Hinting) + return m +} + +func (f *opentype) Bounds(buf *sfnt.Buffer, + ppem fixed.Int26_6) fixed.Rectangle26_6 { + r, _ := f.Font.Bounds(buf, ppem, f.Hinting) + return r +} + +func (f *opentype) LoadGlyph(buf *sfnt.Buffer, ppem fixed.Int26_6, + r rune) ([]sfnt.Segment, bool) { + g, err := f.Font.GlyphIndex(buf, r) + if err != nil { + return nil, false + } + segs, err := f.Font.LoadGlyph(buf, g, ppem, nil) + if err != nil { + return nil, false + } + return segs, true +} diff --git a/gio/font/opentype/opentype_test.go b/gio/font/opentype/opentype_test.go new file mode 100644 index 0000000..d72708e --- /dev/null +++ b/gio/font/opentype/opentype_test.go @@ -0,0 +1,222 @@ +package opentype + +import ( + "bytes" + "compress/gzip" + "encoding/binary" + "fmt" + "io/ioutil" + "os" + "strings" + "testing" + + "golang.org/x/image/font" + "golang.org/x/image/font/gofont/goregular" + "golang.org/x/image/font/sfnt" + "golang.org/x/image/math/fixed" + + "realy.lol/gio/internal/ops" + "realy.lol/gio/op" + "realy.lol/gio/text" +) + +func TestCollectionAsFace(t *testing.T) { + // Load two fonts with disjoint glyphs. Font 1 supports only '1', and font 2 supports only '2'. + // The fonts have different glyphs for the replacement character (".notdef"). + font1, ttf1, err := decompressFontFile("testdata/only1.ttf.gz") + if err != nil { + t.Fatalf("failed to load test font 1: %v", err) + } + font2, ttf2, err := decompressFontFile("testdata/only2.ttf.gz") + if err != nil { + t.Fatalf("failed to load test font 2: %v", err) + } + + otc := mergeFonts(ttf1, ttf2) + coll, err := ParseCollection(otc) + if err != nil { + t.Fatalf("failed to load merged test font: %v", err) + } + + shapeValid1, err := shapeRune(font1, '1') + if err != nil { + t.Fatalf("failed shaping valid glyph with font 1: %v", err) + } + shapeInvalid1, err := shapeRune(font1, '3') + if err != nil { + t.Fatalf("failed shaping invalid glyph with font 1: %v", err) + } + shapeValid2, err := shapeRune(font2, '2') + if err != nil { + t.Fatalf("failed shaping valid glyph with font 2: %v", err) + } + shapeInvalid2, err := shapeRune(font2, + '3') // Same invalid glyph as before to test replacement glyph difference + if err != nil { + t.Fatalf("failed shaping invalid glyph with font 2: %v", err) + } + shapeCollValid1, err := shapeRune(coll, '1') + if err != nil { + t.Fatalf("failed shaping valid glyph for font 1 with font collection: %v", + err) + } + shapeCollValid2, err := shapeRune(coll, '2') + if err != nil { + t.Fatalf("failed shaping valid glyph for font 2 with font collection: %v", + err) + } + shapeCollInvalid, err := shapeRune(coll, + '4') // Different invalid glyph to confirm use of the replacement glyph + if err != nil { + t.Fatalf("failed shaping invalid glyph with font collection: %v", err) + } + + // All shapes from the original fonts should be distinct because the glyphs are distinct, including the replacement + // glyphs. + distinctShapes := []op.CallOp{shapeValid1, shapeInvalid1, shapeValid2, + shapeInvalid2} + for i := 0; i < len(distinctShapes); i++ { + for j := i + 1; j < len(distinctShapes); j++ { + if areShapesEqual(distinctShapes[i], distinctShapes[j]) { + t.Errorf("font shapes %d and %d are not distinct", i, j) + } + } + } + + // Font collections should render glyphs from the first supported font. Replacement glyphs should come from the + // first font in all cases. + if !areShapesEqual(shapeCollValid1, shapeValid1) { + t.Error("font collection did not render the valid glyph using font 1") + } + if !areShapesEqual(shapeCollValid2, shapeValid2) { + t.Error("font collection did not render the valid glyph using font 2") + } + if !areShapesEqual(shapeCollInvalid, shapeInvalid1) { + t.Error("font collection did not render the invalid glyph using the replacement from font 1") + } +} + +func TestEmptyString(t *testing.T) { + face, err := Parse(goregular.TTF) + if err != nil { + t.Fatal(err) + } + + ppem := fixed.I(200) + + lines, err := face.Layout(ppem, 2000, strings.NewReader("")) + if err != nil { + t.Fatal(err) + } + if len(lines) == 0 { + t.Fatalf("Layout returned no lines for empty string; expected 1") + } + l := lines[0] + exp, err := face.font.Bounds(new(sfnt.Buffer), ppem, font.HintingFull) + if err != nil { + t.Fatal(err) + } + if got := l.Bounds; got != exp { + t.Errorf("got bounds %+v for empty string; expected %+v", got, exp) + } +} + +func decompressFontFile(name string) (*Font, []byte, error) { + f, err := os.Open(name) + if err != nil { + return nil, nil, fmt.Errorf("could not open file for reading: %s: %v", + name, err) + } + defer f.Close() + gz, err := gzip.NewReader(f) + if err != nil { + return nil, nil, fmt.Errorf("font file contains invalid gzip data: %v", + err) + } + src, err := ioutil.ReadAll(gz) + if err != nil { + return nil, nil, fmt.Errorf("failed to decompress font file: %v", err) + } + fnt, err := Parse(src) + if err != nil { + return nil, nil, fmt.Errorf("file did not contain a valid font: %v", + err) + } + return fnt, src, nil +} + +// mergeFonts produces a trivial OpenType Collection (OTC) file for two source fonts. +// It makes many assumptions and is not meant for general use. +// For file format details, see https://docs.microsoft.com/en-us/typography/opentype/spec/otff +// For a robust tool to generate these files, see https://pypi.org/project/afdko/ +func mergeFonts(ttf1, ttf2 []byte) []byte { + // Locations to place the two embedded fonts. All of the offsets to the fonts' internal tables will need to be + // shifted from the start of the file by the appropriate amount, and then everything will work as expected. + offset1 := uint32(20) // Length of OpenType collection headers + offset2 := offset1 + uint32(len(ttf1)) + + var buf bytes.Buffer + _, _ = buf.Write([]byte("ttcf\x00\x01\x00\x00\x00\x00\x00\x02")) + _ = binary.Write(&buf, binary.BigEndian, offset1) + _ = binary.Write(&buf, binary.BigEndian, offset2) + + // Inline function to copy a font into the collection verbatim, except for adding an offset to all of the font's + // table positions. + copyOffsetTTF := func(ttf []byte, offset uint32) { + _, _ = buf.Write(ttf[:12]) + numTables := binary.BigEndian.Uint16(ttf[4:6]) + for i := uint16(0); i < numTables; i++ { + p := 12 + 16*i + _, _ = buf.Write(ttf[p : p+8]) + tblLoc := binary.BigEndian.Uint32(ttf[p+8:p+12]) + offset + _ = binary.Write(&buf, binary.BigEndian, tblLoc) + _, _ = buf.Write(ttf[p+12 : p+16]) + } + _, _ = buf.Write(ttf[12+16*numTables:]) + } + copyOffsetTTF(ttf1, offset1) + copyOffsetTTF(ttf2, offset2) + + return buf.Bytes() +} + +// shapeRune uses a given Face to shape exactly one rune at a fixed size, then returns the resulting shape data. +func shapeRune(f text.Face, r rune) (op.CallOp, error) { + ppem := fixed.I(200) + lines, err := f.Layout(ppem, 2000, strings.NewReader(string(r))) + if err != nil { + return op.CallOp{}, err + } + if len(lines) != 1 { + return op.CallOp{}, fmt.Errorf("unexpected rendering for \"U+%08X\": got %d lines (expected: 1)", + r, len(lines)) + } + return f.Shape(ppem, lines[0].Layout), nil +} + +// areShapesEqual returns true iff both given text shapes are produced with identical operations. +func areShapesEqual(shape1, shape2 op.CallOp) bool { + var ops1, ops2 op.Ops + shape1.Add(&ops1) + shape2.Add(&ops2) + var r1, r2 ops.Reader + r1.Reset(&ops1) + r2.Reset(&ops2) + for { + encOp1, ok1 := r1.Decode() + encOp2, ok2 := r2.Decode() + if ok1 != ok2 { + return false + } + if !ok1 { + break + } + if len(encOp1.Refs) > 0 || len(encOp2.Refs) > 0 { + panic("unexpected ops with refs in font shaping test") + } + if !bytes.Equal(encOp1.Data, encOp2.Data) { + return false + } + } + return true +} diff --git a/gio/font/opentype/testdata/only1.ttf.gz b/gio/font/opentype/testdata/only1.ttf.gz new file mode 100644 index 0000000..544159d Binary files /dev/null and b/gio/font/opentype/testdata/only1.ttf.gz differ diff --git a/gio/font/opentype/testdata/only2.ttf.gz b/gio/font/opentype/testdata/only2.ttf.gz new file mode 100644 index 0000000..87a3e68 Binary files /dev/null and b/gio/font/opentype/testdata/only2.ttf.gz differ diff --git a/gio/gesture/gesture.go b/gio/gesture/gesture.go new file mode 100644 index 0000000..bc0324a --- /dev/null +++ b/gio/gesture/gesture.go @@ -0,0 +1,437 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package gesture implements common pointer gestures. + +Gestures accept low level pointer Events from an event +Queue and detect higher level actions such as clicks +and scrolling. +*/ +package gesture + +import ( + "image" + "math" + "runtime" + "time" + + "realy.lol/gio/f32" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/op" + "realy.lol/gio/unit" + + "realy.lol/gio/internal/fling" +) + +// The duration is somewhat arbitrary. +const doubleClickDuration = 300 * time.Millisecond + +// Click detects click gestures in the form +// of ClickEvents. +type Click struct { + // clickedAt is the timestamp at which + // the last click occurred. + clickedAt time.Duration + // clicks is incremented if successive clicks + // are performed within a fixed duration. + clicks int + // pressed tracks whether the pointer is pressed. + pressed bool + // entered tracks whether the pointer is inside the gesture. + entered bool + // pid is the pointer.ID. + pid pointer.ID + Button pointer.Buttons +} + +type ClickState uint8 + +// ClickEvent represent a click action, either a +// TypePress for the beginning of a click or a +// TypeClick for a completed click. +type ClickEvent struct { + Type ClickType + Position f32.Point + Source pointer.Source + Modifiers key.Modifiers + // NumClicks records successive clicks occurring + // within a short duration of each other. + NumClicks int + Button pointer.Buttons +} + +type ClickType uint8 + +// Drag detects drag gestures in the form of pointer.Drag events. +type Drag struct { + dragging bool + pid pointer.ID + start f32.Point + grab bool +} + +// Scroll detects scroll gestures and reduces them to +// scroll distances. Scroll recognizes mouse wheel +// movements as well as drag and fling touch gestures. +type Scroll struct { + dragging bool + axis Axis + estimator fling.Extrapolation + flinger fling.Animation + pid pointer.ID + grab bool + last int + // Leftover scroll. + scroll float32 +} + +type ScrollState uint8 + +type Axis uint8 + +const ( + Horizontal Axis = iota + Vertical + Both +) + +const ( + // TypePress is reported for the first pointer + // press. + TypePress ClickType = iota + // TypeClick is reported when a click action + // is complete. + TypeClick + // TypeCancel is reported when the gesture is + // cancelled. + TypeCancel +) + +const ( + // StateIdle is the default scroll state. + StateIdle ScrollState = iota + // StateDrag is reported during drag gestures. + StateDragging + // StateFlinging is reported when a fling is + // in progress. + StateFlinging +) + +var touchSlop = unit.Dp(3) + +// Add the handler to the operation list to receive click events. +func (c *Click) Add(ops *op.Ops) { + op := pointer.InputOp{ + Tag: c, + Types: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave, + } + op.Add(ops) +} + +// Hovered returns whether a pointer is inside the area. +func (c *Click) Hovered() bool { + return c.entered +} + +// Pressed returns whether a pointer is pressing. +func (c *Click) Pressed() bool { + return c.pressed +} + +// Events returns the next click event, if any. +func (c *Click) Events(q event.Queue) []ClickEvent { + var events []ClickEvent + for _, evt := range q.Events(c) { + // I.S(evt) + e, ok := evt.(pointer.Event) + if !ok { + continue + } + switch e.Type { + case pointer.Release: + if !c.pressed || c.pid != e.PointerID { + break + } + c.pressed = false + if c.entered { + if e.Time-c.clickedAt < doubleClickDuration || + (c.clicks == 2 && e.Time-c.clickedAt < doubleClickDuration*2) { + c.clicks++ + } else { + c.clicks = 1 + } + c.clickedAt = e.Time + events = append(events, ClickEvent{ + Type: TypeClick, Position: e.Position, Source: e.Source, + Modifiers: e.Modifiers, + Button: e.Buttons, NumClicks: c.clicks, + }) + } else { + events = append(events, ClickEvent{Type: TypeCancel}) + } + case pointer.Cancel: + wasPressed := c.pressed + c.pressed = false + c.entered = false + if wasPressed { + events = append(events, ClickEvent{Type: TypeCancel}) + } + case pointer.Press: + if c.pressed { + break + } + // if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonPrimary { + // break + // } + if !c.entered { + c.pid = e.PointerID + } + if c.pid != e.PointerID { + break + } + c.pressed = true + events = append(events, ClickEvent{ + Type: TypePress, Position: e.Position, Source: e.Source, + Modifiers: e.Modifiers, Button: e.Buttons, + }) + case pointer.Leave: + if !c.pressed { + c.pid = e.PointerID + } + if c.pid == e.PointerID { + c.entered = false + } + case pointer.Enter: + if !c.pressed { + c.pid = e.PointerID + } + if c.pid == e.PointerID { + c.entered = true + } + } + } + return events +} + +func (ClickEvent) ImplementsEvent() {} + +// Add the handler to the operation list to receive scroll events. +func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) { + oph := pointer.InputOp{ + Tag: s, + Grab: s.grab, + Types: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll, + ScrollBounds: bounds, + } + oph.Add(ops) + if s.flinger.Active() { + op.InvalidateOp{}.Add(ops) + } +} + +// Stop any remaining fling movement. +func (s *Scroll) Stop() { + s.flinger = fling.Animation{} +} + +// Scroll detects the scrolling distance from the available events and +// ongoing fling gestures. +func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, + axis Axis) int { + if s.axis != axis { + s.axis = axis + return 0 + } + total := 0 + for _, evt := range q.Events(s) { + e, ok := evt.(pointer.Event) + if !ok { + continue + } + switch e.Type { + case pointer.Press: + if s.dragging { + break + } + // Only scroll on touch drags, or on Android where mice + // drags also scroll by convention. + if e.Source != pointer.Touch && runtime.GOOS != "android" { + break + } + s.Stop() + s.estimator = fling.Extrapolation{} + v := s.val(e.Position) + s.last = int(math.Round(float64(v))) + s.estimator.Sample(e.Time, v) + s.dragging = true + s.pid = e.PointerID + case pointer.Release: + if s.pid != e.PointerID { + break + } + fling := s.estimator.Estimate() + if slop, d := float32(cfg.Px(touchSlop)), fling.Distance; d < -slop || d > slop { + s.flinger.Start(cfg, t, fling.Velocity) + } + fallthrough + case pointer.Cancel: + s.dragging = false + s.grab = false + case pointer.Scroll: + switch s.axis { + case Horizontal: + s.scroll += e.Scroll.X + case Vertical: + s.scroll += e.Scroll.Y + } + iscroll := int(s.scroll) + s.scroll -= float32(iscroll) + total += iscroll + case pointer.Drag: + if !s.dragging || s.pid != e.PointerID { + continue + } + val := s.val(e.Position) + s.estimator.Sample(e.Time, val) + v := int(math.Round(float64(val))) + dist := s.last - v + if e.Priority < pointer.Grabbed { + slop := cfg.Px(touchSlop) + if dist := dist; dist >= slop || -slop >= dist { + s.grab = true + } + } else { + s.last = v + total += dist + } + } + } + total += s.flinger.Tick(t) + return total +} + +func (s *Scroll) val(p f32.Point) float32 { + if s.axis == Horizontal { + return p.X + } else { + return p.Y + } +} + +// State reports the scroll state. +func (s *Scroll) State() ScrollState { + switch { + case s.flinger.Active(): + return StateFlinging + case s.dragging: + return StateDragging + default: + return StateIdle + } +} + +// Add the handler to the operation list to receive drag events. +func (d *Drag) Add(ops *op.Ops) { + op := pointer.InputOp{ + Tag: d, + Grab: d.grab, + Types: pointer.Press | pointer.Drag | pointer.Release, + } + op.Add(ops) +} + +// Events returns the next drag events, if any. +func (d *Drag) Events(cfg unit.Metric, q event.Queue, + axis Axis) []pointer.Event { + var events []pointer.Event + for _, e := range q.Events(d) { + e, ok := e.(pointer.Event) + if !ok { + continue + } + + switch e.Type { + case pointer.Press: + if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) { + continue + } + if d.dragging { + continue + } + d.dragging = true + d.pid = e.PointerID + d.start = e.Position + case pointer.Drag: + if !d.dragging || e.PointerID != d.pid { + continue + } + switch axis { + case Horizontal: + e.Position.Y = d.start.Y + case Vertical: + e.Position.X = d.start.X + case Both: + // Do nothing + } + if e.Priority < pointer.Grabbed { + diff := e.Position.Sub(d.start) + slop := cfg.Px(touchSlop) + if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) { + d.grab = true + } + } + case pointer.Release, pointer.Cancel: + if !d.dragging || e.PointerID != d.pid { + continue + } + d.dragging = false + d.grab = false + } + + events = append(events, e) + } + + return events +} + +// Dragging reports whether it's currently in use. +func (d *Drag) Dragging() bool { return d.dragging } + +func (a Axis) String() string { + switch a { + case Horizontal: + return "Horizontal" + case Vertical: + return "Vertical" + default: + panic("invalid Axis") + } +} + +func (ct ClickType) String() string { + switch ct { + case TypePress: + return "TypePress" + case TypeClick: + return "TypeClick" + case TypeCancel: + return "TypeCancel" + default: + panic("invalid ClickType") + } +} + +func (s ScrollState) String() string { + switch s { + case StateIdle: + return "StateIdle" + case StateDragging: + return "StateDragging" + case StateFlinging: + return "StateFlinging" + default: + panic("unreachable") + } +} diff --git a/gio/gesture/gesture_test.go b/gio/gesture/gesture_test.go new file mode 100644 index 0000000..d2f69ea --- /dev/null +++ b/gio/gesture/gesture_test.go @@ -0,0 +1,88 @@ +package gesture + +import ( + "testing" + "time" + + "realy.lol/gio/io/event" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/router" + "realy.lol/gio/op" +) + +func TestMouseClicks(t *testing.T) { + for _, tc := range []struct { + label string + events []event.Event + clicks []int // number of combined clicks per click (single, double...) + }{ + { + label: "single click", + events: mouseClickEvents(200 * time.Millisecond), + clicks: []int{1}, + }, + { + label: "double click", + events: mouseClickEvents( + 100*time.Millisecond, + 100*time.Millisecond+doubleClickDuration-1), + clicks: []int{1, 2}, + }, + { + label: "two single clicks", + events: mouseClickEvents( + 100*time.Millisecond, + 100*time.Millisecond+doubleClickDuration+1), + clicks: []int{1, 1}, + }, + } { + t.Run(tc.label, func(t *testing.T) { + var click Click + var ops op.Ops + click.Add(&ops) + + var r router.Router + r.Frame(&ops) + r.Queue(tc.events...) + + events := click.Events(&r) + clicks := filterMouseClicks(events) + if got, want := len(clicks), len(tc.clicks); got != want { + t.Fatalf("got %d mouse clicks, expected %d", got, want) + } + + for i, click := range clicks { + if got, want := click.NumClicks, tc.clicks[i]; got != want { + t.Errorf("got %d combined mouse clicks, expected %d", got, + want) + } + } + }) + } +} + +func mouseClickEvents(times ...time.Duration) []event.Event { + press := pointer.Event{ + Type: pointer.Press, + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + } + events := make([]event.Event, 0, 2*len(times)) + for _, t := range times { + release := press + release.Type = pointer.Release + release.Time = t + events = append(events, press, release) + } + return events +} + +func filterMouseClicks(events []ClickEvent) []ClickEvent { + var clicks []ClickEvent + for _, ev := range events { + if ev.Type == TypeClick { + clicks = append(clicks, ev) + } + } + return clicks +} diff --git a/gio/gesture/log.go b/gio/gesture/log.go new file mode 100644 index 0000000..9e79319 --- /dev/null +++ b/gio/gesture/log.go @@ -0,0 +1,9 @@ +package gesture + +// import ( +// "github.com/p9c/log" +// +// "github.com/p9c/gel/version" +// ) +// +// var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/gio/giold/.builds/apple.yml b/gio/giold/.builds/apple.yml new file mode 100644 index 0000000..fde6bcb --- /dev/null +++ b/gio/giold/.builds/apple.yml @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: Unlicense OR MIT +image: debian/testing +packages: + - clang + - cmake + - curl + - autoconf + - libxml2-dev + - libssl-dev + - libz-dev + - llvm-dev # for cctools + - uuid-dev ## for cctools + - libplist-utils # for gogio + - golang +sources: + - https://git.sr.ht/~eliasnaur/applesdks + - https://git.sr.ht/~eliasnaur/gio + - https://git.sr.ht/~eliasnaur/giouiorg + - https://github.com/tpoechtrager/cctools-port.git + - https://github.com/tpoechtrager/apple-libtapi.git + - https://github.com/mackyle/xar.git +environment: + APPLE_TOOLCHAIN_ROOT: /home/build/appletools + PATH: /home/build/go/bin:/usr/bin +tasks: + - prepare_toolchain: | + mkdir -p $APPLE_TOOLCHAIN_ROOT + cd $APPLE_TOOLCHAIN_ROOT + tar xJf /home/build/applesdks/applesdks.tar.xz + mkdir bin tools + cd bin + ln -s ../toolchain/bin/x86_64-apple-darwin19-ld ld + ln -s ../toolchain/bin/x86_64-apple-darwin19-ar ar + ln -s /home/build/cctools-port/cctools/misc/lipo lipo + ln -s ../tools/appletoolchain xcrun + ln -s /usr/bin/plistutil plutil + cd ../tools + ln -s appletoolchain clang-ios + ln -s appletoolchain clang-macos + - install_appletoolchain: | + cd giouiorg + go build -o $APPLE_TOOLCHAIN_ROOT/tools ./cmd/appletoolchain + - build_xar: | + cd xar/xar + ac_cv_lib_crypto_OpenSSL_add_all_ciphers=yes CC=clang ./autogen.sh --prefix=/usr + make + sudo make install + - build_libtapi: | + cd apple-libtapi + INSTALLPREFIX=$APPLE_TOOLCHAIN_ROOT/libtapi ./build.sh + ./install.sh + - build_cctools: | + cd cctools-port/cctools + ./configure --prefix $APPLE_TOOLCHAIN_ROOT/toolchain --with-libtapi=$APPLE_TOOLCHAIN_ROOT/libtapi --target=x86_64-apple-darwin19 + make install + - test_macos: | + cd gio + export PATH=/home/build/appletools/bin:$PATH + CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-macos GOOS=darwin CGO_ENABLED=1 go build ./... + - test_ios: | + cd gio + CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-ios GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -tags ios ./... + - install_gogio: | + cd gio/cmd + go install ./gogio + - test_ios_gogio: | + mkdir tmp + cd tmp + go mod init example.com + go get -d github.com/p9c/p9/pkg/gel/gio/example/kitchen + export PATH=/home/build/appletools/bin:$PATH + gogio -target ios -o app.app github.com/p9c/p9/pkg/gel/gio/example/kitchen diff --git a/gio/giold/.builds/freebsd.yml b/gio/giold/.builds/freebsd.yml new file mode 100644 index 0000000..1816b70 --- /dev/null +++ b/gio/giold/.builds/freebsd.yml @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: Unlicense OR MIT +image: freebsd/11.x +packages: + - libX11 + - libxkbcommon + - libXcursor + - libXfixes + - wayland + - mesa-libs + - xorg-vfbserver +sources: + - https://git.sr.ht/~eliasnaur/gio +environment: + PATH: /home/build/sdk/go/bin:/bin:/usr/local/bin:/usr/bin +tasks: + - install_go1_14: | + mkdir -p /home/build/sdk + curl https://dl.google.com/go/go1.14.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf - + - test_gio: | + export EGL_PLATFORM=surfaceless # for headless tests + cd gio + go test ./... + - test_cmd: | + cd gio/cmd + go test ./... diff --git a/gio/giold/.builds/linux.yml b/gio/giold/.builds/linux.yml new file mode 100644 index 0000000..55cb5a5 --- /dev/null +++ b/gio/giold/.builds/linux.yml @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: Unlicense OR MIT +image: debian/testing +packages: + - curl + - pkg-config + - libwayland-dev + - libx11-dev + - libx11-xcb-dev + - libxkbcommon-dev + - libxkbcommon-x11-dev + - libgles2-mesa-dev + - libegl1-mesa-dev + - libffi-dev + - libxcursor-dev + - libxrandr-dev + - libxinerama-dev + - libxi-dev + - libxxf86vm-dev + - wine + - xvfb + - xdotool + - scrot + - sway + - grim + - wine + - unzip +sources: + - https://git.sr.ht/~eliasnaur/gio +environment: + GOFLAGS: -mod=readonly + PATH: /home/build/sdk/go/bin:/usr/bin:/home/build/go/bin:/home/build/android/tools/bin + ANDROID_SDK_ROOT: /home/build/android + android_sdk_tools_zip: sdk-tools-linux-3859397.zip + android_ndk_zip: android-ndk-r20-linux-x86_64.zip + github_mirror: git@github.com:gioui/gio +secrets: + - 75d8a1eb-5fc5-4074-8a36-db6015d6ed5a +tasks: + - install_go1_14: | + mkdir -p /home/build/sdk + curl -s https://dl.google.com/go/go1.14.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf - + - test_gio: | + cd gio + export EGL_PLATFORM=surfaceless # for headless tests + go test -race ./... + GOOS=windows go test -exec=wine ./... + GOOS=js GOARCH=wasm go build -o /dev/null ./... + - install_chrome: | + curl -s https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' + sudo apt-get -qq update + sudo apt-get -qq install -y google-chrome-stable + - test_cmd: | + cd gio/cmd + go test ./... + go test -race ./... + cd gogio # since we need -modfile to point at the parent directory + GOFLAGS=-modfile=../go.local.mod go test + - install_jdk8: | + curl -so jdk.deb "https://cdn.azul.com/zulu/bin/zulu8.42.0.21-ca-jdk8.0.232-linux_amd64.deb" + sudo apt-get -qq install -y -f ./jdk.deb + - install_android: | + mkdir android + cd android + curl -so sdk-tools.zip https://dl.google.com/android/repository/$android_sdk_tools_zip + unzip -q sdk-tools.zip + rm sdk-tools.zip + curl -so ndk.zip https://dl.google.com/android/repository/$android_ndk_zip + unzip -q ndk.zip + rm ndk.zip + mv android-ndk-* ndk-bundle + yes|sdkmanager --licenses + sdkmanager "platforms;android-29" "build-tools;29.0.2" + - test_android: | + cd gio + CC=$ANDROID_SDK_ROOT/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build ./... + - install_gogio: | + cd gio/cmd + go install ./gogio + - test_android_gogio: | + mkdir tmp + cd tmp + go mod init example.com + go get -d github.com/p9c/p9/pkg/gel/gio/example/kitchen + gogio -target android github.com/p9c/p9/pkg/gel/gio/example/kitchen + - check_gofmt: | + cd gio + test -z "$(gofmt -s -l .)" + - check_sign_off: | + set +x -e + cd gio + for hash in $(git log -n 20 --format="%H"); do + message=$(git log -1 --format=%B $hash) + if [[ ! "$message" =~ "Signed-off-by: " ]]; then + echo "Missing 'Signed-off-by' in commit $hash" + exit 1 + fi + done + - mirror: | + # mirror to github + ssh-keyscan github.com > "$HOME"/.ssh/known_hosts && cd gio && git push --mirror "$github_mirror" || echo "failed mirroring" diff --git a/gio/giold/.builds/openbsd.yml b/gio/giold/.builds/openbsd.yml new file mode 100644 index 0000000..757e80f --- /dev/null +++ b/gio/giold/.builds/openbsd.yml @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Unlicense OR MIT +image: openbsd/latest +packages: + - libxkbcommon + - go +sources: + - https://git.sr.ht/~eliasnaur/gio +environment: + PATH: /home/build/sdk/go/bin:/bin:/usr/local/bin:/usr/bin +tasks: + - install_go1_14: | + mkdir -p /home/build/sdk + curl https://dl.google.com/go/go1.14.src.tar.gz | tar -C /home/build/sdk -xzf - + cd /home/build/sdk/go/src + ./make.bash + - test_gio: | + cd gio + go test ./... + - test_cmd: | + cd gio/cmd + go test ./... diff --git a/gio/giold/LICENSE b/gio/giold/LICENSE new file mode 100644 index 0000000..81f4733 --- /dev/null +++ b/gio/giold/LICENSE @@ -0,0 +1,63 @@ +This project is provided under the terms of the UNLICENSE or +the MIT license denoted by the following SPDX identifier: + +SPDX-License-Identifier: Unlicense OR MIT + +You may use the project under the terms of either license. + +Both licenses are reproduced below. + +---- +The MIT License (MIT) + +Copyright (c) 2019 The Gio authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +--- + + + +--- +The UNLICENSE + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to +--- diff --git a/gio/giold/README.md b/gio/giold/README.md new file mode 100644 index 0000000..634cb42 --- /dev/null +++ b/gio/giold/README.md @@ -0,0 +1,26 @@ +# Gio - https://github.com/p9c/p9/pkg/gel/gio + +Immediate mode GUI programs in Go for Android, iOS, macOS, Linux, +FreeBSD, OpenBSD, Windows, and WebAssembly (experimental). + +# Installation, examples, documentation + +Go to [github.com/p9c/p9/pkg/gel/gio](https://github.com/p9c/p9/pkg/gel/gio). + +[![builds.sr.ht status](https://builds.sr.ht/~eliasnaur/gio.svg)](https://builds.sr.ht/~eliasnaur/gio) + +## Issues + +File bugs and TODOs through the [issue tracker](https://todo.sr.ht/~eliasnaur/gio) or send an email +to [~eliasnaur/gio@todo.sr.ht](mailto:~eliasnaur/gio@todo.sr.ht). For general discussion, use the +mailing list: [~eliasnaur/gio@lists.sr.ht](mailto:~eliasnaur/gio@lists.sr.ht). + +## Contributing + +Post discussion to the [mailing list](https://lists.sr.ht/~eliasnaur/gio) and patches to +[gio-patches](https://lists.sr.ht/~eliasnaur/gio-patches). No Sourcehut +account is required and you can post without being subscribed. + +See the [contribution guide](https://github.com/p9c/p9/pkg/gel/gio/doc/contribute) for more details. + +An [official GitHub mirror](https://github.com/gioui/gio) is available. diff --git a/gio/giold/app/app.go b/gio/giold/app/app.go new file mode 100644 index 0000000..e9fbdf7 --- /dev/null +++ b/gio/giold/app/app.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +import ( + "os" + "strings" + + "realy.lol/gio/app/internal/wm" +) + +// extraArgs contains extra arguments to append to +// os.Args. The arguments are separated with |. +// Useful for running programs on mobiles where the +// command line is not available. +// Set with the go linker flag -X. +var extraArgs string + +func init() { + if extraArgs != "" { + args := strings.Split(extraArgs, "|") + os.Args = append(os.Args, args...) + } +} + +// DataDir returns a path to use for application-specific +// configuration data. +// On desktop systems, DataDir use os.UserConfigDir. +// On iOS NSDocumentDirectory is queried. +// For Android Context.getFilesDir is used. +// +// BUG: DataDir blocks on Android until init functions +// have completed. +func DataDir() (string, error) { + return dataDir() +} + +// Main must be called last from the program main function. +// On most platforms Main blocks forever, for Android and +// iOS it returns immediately to give control of the main +// thread back to the system. +// +// Calling Main is necessary because some operating systems +// require control of the main thread of the program for +// running windows. +func Main() { + wm.Main() +} diff --git a/gio/giold/app/app_android.go b/gio/giold/app/app_android.go new file mode 100644 index 0000000..060544f --- /dev/null +++ b/gio/giold/app/app_android.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +import ( + "realy.lol/gio/app/internal/wm" +) + +type ViewEvent = wm.ViewEvent + +// JavaVM returns the global JNI JavaVM. +func JavaVM() uintptr { + return wm.JavaVM() +} + +// AppContext returns the global Application context as a JNI +// jobject. +func AppContext() uintptr { + return wm.AppContext() +} diff --git a/gio/giold/app/datadir.go b/gio/giold/app/datadir.go new file mode 100644 index 0000000..31e5453 --- /dev/null +++ b/gio/giold/app/datadir.go @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build !android + +package app + +import "os" + +func dataDir() (string, error) { + return os.UserConfigDir() +} diff --git a/gio/giold/app/datadir_android.go b/gio/giold/app/datadir_android.go new file mode 100644 index 0000000..cbbc6c4 --- /dev/null +++ b/gio/giold/app/datadir_android.go @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build android +// +build android + +package app + +import "C" + +import ( + "os" + "path/filepath" + "sync" + + "realy.lol/gio/app/internal/wm" +) + +var ( + dataDirOnce sync.Once + dataPath string +) + +func dataDir() (string, error) { + dataDirOnce.Do(func() { + dataPath = wm.GetDataDir() + // Set XDG_CACHE_HOME to make os.UserCacheDir work. + if _, exists := os.LookupEnv("XDG_CACHE_HOME"); !exists { + cachePath := filepath.Join(dataPath, "cache") + os.Setenv("XDG_CACHE_HOME", cachePath) + } + // Set XDG_CONFIG_HOME to make os.UserConfigDir work. + if _, exists := os.LookupEnv("XDG_CONFIG_HOME"); !exists { + cfgPath := filepath.Join(dataPath, "config") + os.Setenv("XDG_CONFIG_HOME", cfgPath) + } + // Set HOME to make os.UserHomeDir work. + if _, exists := os.LookupEnv("HOME"); !exists { + os.Setenv("HOME", dataPath) + } + }) + return dataPath, nil +} diff --git a/gio/giold/app/doc.go b/gio/giold/app/doc.go new file mode 100644 index 0000000..fb0826a --- /dev/null +++ b/gio/giold/app/doc.go @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package app provides a platform-independent interface to operating system +functionality for running graphical user interfaces. + +See https://realy.lol/gio for instructions to set up and run Gio programs. + +Windows + +Create a new Window by calling NewWindow. On mobile platforms or when Gio +is embedded in another project, NewWindow merely connects with a previously +created window. + +A Window is run by receiving events from its Events channel. The most +important event is FrameEvent that prompts an update of the window +contents and state. + +For example: + + import "realy.lol/gio/unit" + + w := app.NewWindow() + for e := range w.Events() { + if e, ok := e.(system.FrameEvent); ok { + ops.Reset() + // Add operations to ops. + ... + // Completely replace the window contents and state. + e.Frame(ops) + } + } + +A program must keep receiving events from the event channel until +DestroyEvent is received. + +Main + +The Main function must be called from a program's main function, to hand over +control of the main thread to operating systems that need it. + +Because Main is also blocking on some platforms, the event loop of a Window must run in a goroutine. + +For example, to display a blank but otherwise functional window: + + package main + + import "realy.lol/gio/app" + + func main() { + go func() { + w := app.NewWindow() + for range w.Events() { + } + }() + app.Main() + } + + +Event queue + +A FrameEvent's Queue method returns an event.Queue implementation that distributes +incoming events to the event handlers declared in the last frame. +See the realy.lol/gio/io/event package for more information about event handlers. + +Permissions + +The packages under realy.lol/gio/app/permission should be imported +by a Gio program or by one of its dependencies to indicate that specific +operating-system permissions are required. Please see documentation for +package realy.lol/gio/app/permission for more information. +*/ +package app diff --git a/gio/giold/app/internal/log/log.go b/gio/giold/app/internal/log/log.go new file mode 100644 index 0000000..731ae49 --- /dev/null +++ b/gio/giold/app/internal/log/log.go @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package points standard output, standard error and the standard +// library package log to the platform logger. +package log + +var appID = "gio" diff --git a/gio/giold/app/internal/log/log_android.go b/gio/giold/app/internal/log/log_android.go new file mode 100644 index 0000000..1245598 --- /dev/null +++ b/gio/giold/app/internal/log/log_android.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package log + +/* +#cgo LDFLAGS: -llog + +#include +#include +*/ +import "C" + +import ( + "bufio" + "log" + "os" + "runtime" + "syscall" + "unsafe" +) + +// 1024 is the truncation limit from android/log.h, plus a \n. +const logLineLimit = 1024 + +var logTag = C.CString(appID) + +func init() { + // Android's logcat already includes timestamps. + log.SetFlags(log.Flags() &^ log.LstdFlags) + log.SetOutput(new(androidLogWriter)) + + // Redirect stdout and stderr to the Android logger. + logFd(os.Stdout.Fd()) + logFd(os.Stderr.Fd()) +} + +type androidLogWriter struct { + // buf has room for the maximum log line, plus a terminating '\0'. + buf [logLineLimit + 1]byte +} + +func (w *androidLogWriter) Write(data []byte) (int, error) { + n := 0 + for len(data) > 0 { + msg := data + // Truncate the buffer, leaving space for the '\0'. + if max := len(w.buf) - 1; len(msg) > max { + msg = msg[:max] + } + buf := w.buf[:len(msg)+1] + copy(buf, msg) + // Terminating '\0'. + buf[len(msg)] = 0 + C.__android_log_write(C.ANDROID_LOG_INFO, logTag, (*C.char)(unsafe.Pointer(&buf[0]))) + n += len(msg) + data = data[len(msg):] + } + return n, nil +} + +func logFd(fd uintptr) { + r, w, err := os.Pipe() + if err != nil { + panic(err) + } + if err := syscall.Dup3(int(w.Fd()), int(fd), syscall.O_CLOEXEC); err != nil { + panic(err) + } + go func() { + lineBuf := bufio.NewReaderSize(r, logLineLimit) + // The buffer to pass to C, including the terminating '\0'. + buf := make([]byte, lineBuf.Size()+1) + cbuf := (*C.char)(unsafe.Pointer(&buf[0])) + for { + line, _, err := lineBuf.ReadLine() + if err != nil { + break + } + copy(buf, line) + buf[len(line)] = 0 + C.__android_log_write(C.ANDROID_LOG_INFO, logTag, cbuf) + } + // The garbage collector doesn't know that w's fd was dup'ed. + // Avoid finalizing w, and thereby avoid its finalizer closing its fd. + runtime.KeepAlive(w) + }() +} diff --git a/gio/giold/app/internal/log/log_ios.go b/gio/giold/app/internal/log/log_ios.go new file mode 100644 index 0000000..6a041db --- /dev/null +++ b/gio/giold/app/internal/log/log_ios.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build darwin && ios +// +build darwin,ios + +package log + +/* +#cgo CFLAGS: -Werror -fmodules -fobjc-arc -x objective-c + +__attribute__ ((visibility ("hidden"))) void nslog(char *str); +*/ +import "C" + +import ( + "bufio" + "io" + "log" + "unsafe" + + _ "realy.lol/gio/internal/cocoainit" +) + +func init() { + // macOS Console already includes timestamps. + log.SetFlags(log.Flags() &^ log.LstdFlags) + log.SetOutput(newNSLogWriter()) +} + +func newNSLogWriter() io.Writer { + r, w := io.Pipe() + go func() { + // 1024 is an arbitrary truncation limit, taken from Android's + // log buffer size. + lineBuf := bufio.NewReaderSize(r, 1024) + // The buffer to pass to C, including the terminating '\0'. + buf := make([]byte, lineBuf.Size()+1) + cbuf := (*C.char)(unsafe.Pointer(&buf[0])) + for { + line, _, err := lineBuf.ReadLine() + if err != nil { + break + } + copy(buf, line) + buf[len(line)] = 0 + C.nslog(cbuf) + } + }() + return w +} diff --git a/gio/giold/app/internal/log/log_ios.m b/gio/giold/app/internal/log/log_ios.m new file mode 100644 index 0000000..201bc36 --- /dev/null +++ b/gio/giold/app/internal/log/log_ios.m @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,ios + +@import Foundation; + +#include "_cgo_export.h" + +void nslog(char *str) { + NSLog(@"%@", @(str)); +} diff --git a/gio/giold/app/internal/log/log_windows.go b/gio/giold/app/internal/log/log_windows.go new file mode 100644 index 0000000..13c5fe4 --- /dev/null +++ b/gio/giold/app/internal/log/log_windows.go @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package log + +import ( + "log" + "syscall" + "unsafe" +) + +type logger struct{} + +var ( + kernel32 = syscall.NewLazyDLL("kernel32") + outputDebugStringW = kernel32.NewProc("OutputDebugStringW") + debugView *logger +) + +func init() { + // Windows DebugView already includes timestamps. + if syscall.Stderr == 0 { + log.SetFlags(log.Flags() &^ log.LstdFlags) + log.SetOutput(debugView) + } +} + +func (l *logger) Write(buf []byte) (int, error) { + p, err := syscall.UTF16PtrFromString(string(buf)) + if err != nil { + return 0, err + } + outputDebugStringW.Call(uintptr(unsafe.Pointer(p))) + return len(buf), nil +} diff --git a/gio/giold/app/internal/windows/windows.go b/gio/giold/app/internal/windows/windows.go new file mode 100644 index 0000000..8af575e --- /dev/null +++ b/gio/giold/app/internal/windows/windows.go @@ -0,0 +1,673 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build windows + +package windows + +import ( + "fmt" + "runtime" + "time" + "unsafe" + + syscall "golang.org/x/sys/windows" +) + +type Rect struct { + Left, Top, Right, Bottom int32 +} + +type WndClassEx struct { + CbSize uint32 + Style uint32 + LpfnWndProc uintptr + CnClsExtra int32 + CbWndExtra int32 + HInstance syscall.Handle + HIcon syscall.Handle + HCursor syscall.Handle + HbrBackground syscall.Handle + LpszMenuName *uint16 + LpszClassName *uint16 + HIconSm syscall.Handle +} + +type Msg struct { + Hwnd syscall.Handle + Message uint32 + WParam uintptr + LParam uintptr + Time uint32 + Pt Point + LPrivate uint32 +} + +type Point struct { + X, Y int32 +} + +type MinMaxInfo struct { + PtReserved Point + PtMaxSize Point + PtMaxPosition Point + PtMinTrackSize Point + PtMaxTrackSize Point +} + +type WindowPlacement struct { + length uint32 + flags uint32 + showCmd uint32 + ptMinPosition Point + ptMaxPosition Point + rcNormalPosition Rect + rcDevice Rect +} + +type MonitorInfo struct { + cbSize uint32 + Monitor Rect + WorkArea Rect + Flags uint32 +} + +const ( + TRUE = 1 + + CS_HREDRAW = 0x0002 + CS_VREDRAW = 0x0001 + CS_OWNDC = 0x0020 + + CW_USEDEFAULT = -2147483648 + + GWL_STYLE = ^(uint32(16) - 1) // -16 + HWND_TOPMOST = ^(uint32(1) - 1) // -1 + + HTCLIENT = 1 + + IDC_ARROW = 32512 + IDC_IBEAM = 32513 + IDC_HAND = 32649 + IDC_CROSS = 32515 + IDC_SIZENS = 32645 + IDC_SIZEWE = 32644 + IDC_SIZEALL = 32646 + + INFINITE = 0xFFFFFFFF + + LOGPIXELSX = 88 + + MDT_EFFECTIVE_DPI = 0 + + MONITOR_DEFAULTTOPRIMARY = 1 + + SIZE_MAXIMIZED = 2 + SIZE_MINIMIZED = 1 + SIZE_RESTORED = 0 + + SW_SHOWDEFAULT = 10 + + SWP_FRAMECHANGED = 0x0020 + SWP_NOMOVE = 0x0002 + SWP_NOOWNERZORDER = 0x0200 + SWP_NOSIZE = 0x0001 + SWP_NOZORDER = 0x0004 + + USER_TIMER_MINIMUM = 0x0000000A + + VK_CONTROL = 0x11 + VK_LWIN = 0x5B + VK_MENU = 0x12 + VK_RWIN = 0x5C + VK_SHIFT = 0x10 + + VK_BACK = 0x08 + VK_DELETE = 0x2e + VK_DOWN = 0x28 + VK_END = 0x23 + VK_ESCAPE = 0x1b + VK_HOME = 0x24 + VK_LEFT = 0x25 + VK_NEXT = 0x22 + VK_PRIOR = 0x21 + VK_RIGHT = 0x27 + VK_RETURN = 0x0d + VK_SPACE = 0x20 + VK_TAB = 0x09 + VK_UP = 0x26 + + VK_F1 = 0x70 + VK_F2 = 0x71 + VK_F3 = 0x72 + VK_F4 = 0x73 + VK_F5 = 0x74 + VK_F6 = 0x75 + VK_F7 = 0x76 + VK_F8 = 0x77 + VK_F9 = 0x78 + VK_F10 = 0x79 + VK_F11 = 0x7A + VK_F12 = 0x7B + + VK_OEM_1 = 0xba + VK_OEM_PLUS = 0xbb + VK_OEM_COMMA = 0xbc + VK_OEM_MINUS = 0xbd + VK_OEM_PERIOD = 0xbe + VK_OEM_2 = 0xbf + VK_OEM_3 = 0xc0 + VK_OEM_4 = 0xdb + VK_OEM_5 = 0xdc + VK_OEM_6 = 0xdd + VK_OEM_7 = 0xde + VK_OEM_102 = 0xe2 + + UNICODE_NOCHAR = 65535 + + WM_CANCELMODE = 0x001F + WM_CHAR = 0x0102 + WM_CREATE = 0x0001 + WM_DPICHANGED = 0x02E0 + WM_DESTROY = 0x0002 + WM_ERASEBKGND = 0x0014 + WM_KEYDOWN = 0x0100 + WM_KEYUP = 0x0101 + WM_LBUTTONDOWN = 0x0201 + WM_LBUTTONUP = 0x0202 + WM_MBUTTONDOWN = 0x0207 + WM_MBUTTONUP = 0x0208 + WM_MOUSEMOVE = 0x0200 + WM_MOUSEWHEEL = 0x020A + WM_MOUSEHWHEEL = 0x020E + WM_PAINT = 0x000F + WM_CLOSE = 0x0010 + WM_QUIT = 0x0012 + WM_SETCURSOR = 0x0020 + WM_SETFOCUS = 0x0007 + WM_KILLFOCUS = 0x0008 + WM_SHOWWINDOW = 0x0018 + WM_SIZE = 0x0005 + WM_SYSKEYDOWN = 0x0104 + WM_SYSKEYUP = 0x0105 + WM_RBUTTONDOWN = 0x0204 + WM_RBUTTONUP = 0x0205 + WM_TIMER = 0x0113 + WM_UNICHAR = 0x0109 + WM_USER = 0x0400 + WM_GETMINMAXINFO = 0x0024 + + WS_CLIPCHILDREN = 0x00010000 + WS_CLIPSIBLINGS = 0x04000000 + WS_VISIBLE = 0x10000000 + WS_OVERLAPPED = 0x00000000 + WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | + WS_MINIMIZEBOX | WS_MAXIMIZEBOX + WS_CAPTION = 0x00C00000 + WS_SYSMENU = 0x00080000 + WS_THICKFRAME = 0x00040000 + WS_MINIMIZEBOX = 0x00020000 + WS_MAXIMIZEBOX = 0x00010000 + + WS_EX_APPWINDOW = 0x00040000 + WS_EX_WINDOWEDGE = 0x00000100 + + QS_ALLINPUT = 0x04FF + + MWMO_WAITALL = 0x0001 + MWMO_INPUTAVAILABLE = 0x0004 + + WAIT_OBJECT_0 = 0 + + PM_REMOVE = 0x0001 + PM_NOREMOVE = 0x0000 + + GHND = 0x0042 + + CF_UNICODETEXT = 13 + IMAGE_BITMAP = 0 + IMAGE_ICON = 1 + IMAGE_CURSOR = 2 + + LR_CREATEDIBSECTION = 0x00002000 + LR_DEFAULTCOLOR = 0x00000000 + LR_DEFAULTSIZE = 0x00000040 + LR_LOADFROMFILE = 0x00000010 + LR_LOADMAP3DCOLORS = 0x00001000 + LR_LOADTRANSPARENT = 0x00000020 + LR_MONOCHROME = 0x00000001 + LR_SHARED = 0x00008000 + LR_VGACOLOR = 0x00000080 +) + +var ( + kernel32 = syscall.NewLazySystemDLL("kernel32.dll") + _GetModuleHandleW = kernel32.NewProc("GetModuleHandleW") + _GlobalAlloc = kernel32.NewProc("GlobalAlloc") + _GlobalFree = kernel32.NewProc("GlobalFree") + _GlobalLock = kernel32.NewProc("GlobalLock") + _GlobalUnlock = kernel32.NewProc("GlobalUnlock") + + user32 = syscall.NewLazySystemDLL("user32.dll") + _AdjustWindowRectEx = user32.NewProc("AdjustWindowRectEx") + _CallMsgFilter = user32.NewProc("CallMsgFilterW") + _CloseClipboard = user32.NewProc("CloseClipboard") + _CreateWindowEx = user32.NewProc("CreateWindowExW") + _DefWindowProc = user32.NewProc("DefWindowProcW") + _DestroyWindow = user32.NewProc("DestroyWindow") + _DispatchMessage = user32.NewProc("DispatchMessageW") + _EmptyClipboard = user32.NewProc("EmptyClipboard") + _GetClientRect = user32.NewProc("GetClientRect") + _GetClipboardData = user32.NewProc("GetClipboardData") + _GetDC = user32.NewProc("GetDC") + _GetDpiForWindow = user32.NewProc("GetDpiForWindow") + _GetKeyState = user32.NewProc("GetKeyState") + _GetMessage = user32.NewProc("GetMessageW") + _GetMessageTime = user32.NewProc("GetMessageTime") + _GetMonitorInfo = user32.NewProc("GetMonitorInfoW") + _GetWindowLong = user32.NewProc("GetWindowLongPtrW") + _GetWindowPlacement = user32.NewProc("GetWindowPlacement") + _KillTimer = user32.NewProc("KillTimer") + _LoadCursor = user32.NewProc("LoadCursorW") + _LoadImage = user32.NewProc("LoadImageW") + _MonitorFromPoint = user32.NewProc("MonitorFromPoint") + _MonitorFromWindow = user32.NewProc("MonitorFromWindow") + _MoveWindow = user32.NewProc("MoveWindow") + _MsgWaitForMultipleObjectsEx = user32.NewProc("MsgWaitForMultipleObjectsEx") + _OpenClipboard = user32.NewProc("OpenClipboard") + _PeekMessage = user32.NewProc("PeekMessageW") + _PostMessage = user32.NewProc("PostMessageW") + _PostQuitMessage = user32.NewProc("PostQuitMessage") + _ReleaseCapture = user32.NewProc("ReleaseCapture") + _RegisterClassExW = user32.NewProc("RegisterClassExW") + _ReleaseDC = user32.NewProc("ReleaseDC") + _ScreenToClient = user32.NewProc("ScreenToClient") + _ShowWindow = user32.NewProc("ShowWindow") + _SetCapture = user32.NewProc("SetCapture") + _SetCursor = user32.NewProc("SetCursor") + _SetClipboardData = user32.NewProc("SetClipboardData") + _SetForegroundWindow = user32.NewProc("SetForegroundWindow") + _SetFocus = user32.NewProc("SetFocus") + _SetProcessDPIAware = user32.NewProc("SetProcessDPIAware") + _SetTimer = user32.NewProc("SetTimer") + _SetWindowLong = user32.NewProc("SetWindowLongPtrW") + _SetWindowPlacement = user32.NewProc("SetWindowPlacement") + _SetWindowPos = user32.NewProc("SetWindowPos") + _SetWindowText = user32.NewProc("SetWindowTextW") + _TranslateMessage = user32.NewProc("TranslateMessage") + _UnregisterClass = user32.NewProc("UnregisterClassW") + _UpdateWindow = user32.NewProc("UpdateWindow") + + shcore = syscall.NewLazySystemDLL("shcore") + _GetDpiForMonitor = shcore.NewProc("GetDpiForMonitor") + + gdi32 = syscall.NewLazySystemDLL("gdi32") + _GetDeviceCaps = gdi32.NewProc("GetDeviceCaps") +) + +func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) { + _AdjustWindowRectEx.Call(uintptr(unsafe.Pointer(r)), uintptr(dwStyle), uintptr(bMenu), uintptr(dwExStyle)) + issue34474KeepAlive(r) +} + +func CallMsgFilter(m *Msg, nCode uintptr) bool { + r, _, _ := _CallMsgFilter.Call(uintptr(unsafe.Pointer(m)), nCode) + issue34474KeepAlive(m) + return r != 0 +} + +func CloseClipboard() error { + r, _, err := _CloseClipboard.Call() + if r == 0 { + return fmt.Errorf("CloseClipboard: %v", err) + } + return nil +} + +func CreateWindowEx(dwExStyle uint32, lpClassName uint16, lpWindowName string, dwStyle uint32, x, y, w, h int32, hWndParent, hMenu, hInstance syscall.Handle, lpParam uintptr) (syscall.Handle, error) { + wname := syscall.StringToUTF16Ptr(lpWindowName) + hwnd, _, err := _CreateWindowEx.Call( + uintptr(dwExStyle), + uintptr(lpClassName), + uintptr(unsafe.Pointer(wname)), + uintptr(dwStyle), + uintptr(x), uintptr(y), + uintptr(w), uintptr(h), + uintptr(hWndParent), + uintptr(hMenu), + uintptr(hInstance), + uintptr(lpParam)) + issue34474KeepAlive(wname) + if hwnd == 0 { + return 0, fmt.Errorf("CreateWindowEx failed: %v", err) + } + return syscall.Handle(hwnd), nil +} + +func DefWindowProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr { + r, _, _ := _DefWindowProc.Call(uintptr(hwnd), uintptr(msg), wparam, lparam) + return r +} + +func DestroyWindow(hwnd syscall.Handle) { + _DestroyWindow.Call(uintptr(hwnd)) +} + +func DispatchMessage(m *Msg) { + _DispatchMessage.Call(uintptr(unsafe.Pointer(m))) + issue34474KeepAlive(m) +} + +func EmptyClipboard() error { + r, _, err := _EmptyClipboard.Call() + if r == 0 { + return fmt.Errorf("EmptyClipboard: %v", err) + } + return nil +} + +func GetClientRect(hwnd syscall.Handle, r *Rect) { + _GetClientRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(r))) + issue34474KeepAlive(r) +} + +func GetClipboardData(format uint32) (syscall.Handle, error) { + r, _, err := _GetClipboardData.Call(uintptr(format)) + if r == 0 { + return 0, fmt.Errorf("GetClipboardData: %v", err) + } + return syscall.Handle(r), nil +} + +func GetDC(hwnd syscall.Handle) (syscall.Handle, error) { + hdc, _, err := _GetDC.Call(uintptr(hwnd)) + if hdc == 0 { + return 0, fmt.Errorf("GetDC failed: %v", err) + } + return syscall.Handle(hdc), nil +} + +func GetModuleHandle() (syscall.Handle, error) { + h, _, err := _GetModuleHandleW.Call(uintptr(0)) + if h == 0 { + return 0, fmt.Errorf("GetModuleHandleW failed: %v", err) + } + return syscall.Handle(h), nil +} + +func getDeviceCaps(hdc syscall.Handle, index int32) int { + c, _, _ := _GetDeviceCaps.Call(uintptr(hdc), uintptr(index)) + return int(c) +} + +func getDpiForMonitor(hmonitor syscall.Handle, dpiType uint32) int { + var dpiX, dpiY uintptr + _GetDpiForMonitor.Call(uintptr(hmonitor), uintptr(dpiType), uintptr(unsafe.Pointer(&dpiX)), uintptr(unsafe.Pointer(&dpiY))) + return int(dpiX) +} + +// GetSystemDPI returns the effective DPI of the system. +func GetSystemDPI() int { + // Check for GetDpiForMonitor, introduced in Windows 8.1. + if _GetDpiForMonitor.Find() == nil { + hmon := monitorFromPoint(Point{}, MONITOR_DEFAULTTOPRIMARY) + return getDpiForMonitor(hmon, MDT_EFFECTIVE_DPI) + } else { + // Fall back to the physical device DPI. + screenDC, err := GetDC(0) + if err != nil { + return 96 + } + defer ReleaseDC(screenDC) + return getDeviceCaps(screenDC, LOGPIXELSX) + } +} + +func GetKeyState(nVirtKey int32) int16 { + c, _, _ := _GetKeyState.Call(uintptr(nVirtKey)) + return int16(c) +} + +func GetMessage(m *Msg, hwnd syscall.Handle, wMsgFilterMin, wMsgFilterMax uint32) int32 { + r, _, _ := _GetMessage.Call(uintptr(unsafe.Pointer(m)), + uintptr(hwnd), + uintptr(wMsgFilterMin), + uintptr(wMsgFilterMax)) + issue34474KeepAlive(m) + return int32(r) +} + +func GetMessageTime() time.Duration { + r, _, _ := _GetMessageTime.Call() + return time.Duration(r) * time.Millisecond +} + +// GetWindowDPI returns the effective DPI of the window. +func GetWindowDPI(hwnd syscall.Handle) int { + // Check for GetDpiForWindow, introduced in Windows 10. + if _GetDpiForWindow.Find() == nil { + dpi, _, _ := _GetDpiForWindow.Call(uintptr(hwnd)) + return int(dpi) + } else { + return GetSystemDPI() + } +} + +func GetWindowPlacement(hwnd syscall.Handle) *WindowPlacement { + var wp WindowPlacement + wp.length = uint32(unsafe.Sizeof(wp)) + _GetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&wp))) + return &wp +} + +func GetMonitorInfo(hwnd syscall.Handle) MonitorInfo { + var mi MonitorInfo + mi.cbSize = uint32(unsafe.Sizeof(mi)) + v, _, _ := _MonitorFromWindow.Call(uintptr(hwnd), MONITOR_DEFAULTTOPRIMARY) + _GetMonitorInfo.Call(v, uintptr(unsafe.Pointer(&mi))) + return mi +} + +func GetWindowLong(hwnd syscall.Handle) (style uintptr) { + style, _, _ = _GetWindowLong.Call(uintptr(hwnd), uintptr(GWL_STYLE)) + return +} + +func SetWindowLong(hwnd syscall.Handle, idx uint32, style uintptr) { + _SetWindowLong.Call(uintptr(hwnd), uintptr(idx), style) +} + +func SetWindowPlacement(hwnd syscall.Handle, wp *WindowPlacement) { + _SetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wp))) +} + +func SetWindowPos(hwnd syscall.Handle, hwndInsertAfter uint32, x, y, dx, dy int32, style uintptr) { + _SetWindowPos.Call(uintptr(hwnd), uintptr(hwndInsertAfter), + uintptr(x), uintptr(y), + uintptr(dx), uintptr(dy), + style, + ) +} + +func SetWindowText(hwnd syscall.Handle, title string) { + wname := syscall.StringToUTF16Ptr(title) + _SetWindowText.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wname))) +} + +func GlobalAlloc(size int) (syscall.Handle, error) { + r, _, err := _GlobalAlloc.Call(GHND, uintptr(size)) + if r == 0 { + return 0, fmt.Errorf("GlobalAlloc: %v", err) + } + return syscall.Handle(r), nil +} + +func GlobalFree(h syscall.Handle) { + _GlobalFree.Call(uintptr(h)) +} + +func GlobalLock(h syscall.Handle) (uintptr, error) { + r, _, err := _GlobalLock.Call(uintptr(h)) + if r == 0 { + return 0, fmt.Errorf("GlobalLock: %v", err) + } + return r, nil +} + +func GlobalUnlock(h syscall.Handle) { + _GlobalUnlock.Call(uintptr(h)) +} + +func KillTimer(hwnd syscall.Handle, nIDEvent uintptr) error { + r, _, err := _SetTimer.Call(uintptr(hwnd), uintptr(nIDEvent), 0, 0) + if r == 0 { + return fmt.Errorf("KillTimer failed: %v", err) + } + return nil +} + +func LoadCursor(curID uint16) (syscall.Handle, error) { + h, _, err := _LoadCursor.Call(0, uintptr(curID)) + if h == 0 { + return 0, fmt.Errorf("LoadCursorW failed: %v", err) + } + return syscall.Handle(h), nil +} + +func LoadImage(hInst syscall.Handle, res uint32, typ uint32, cx, cy int, fuload uint32) (syscall.Handle, error) { + h, _, err := _LoadImage.Call(uintptr(hInst), uintptr(res), uintptr(typ), uintptr(cx), uintptr(cy), uintptr(fuload)) + if h == 0 { + return 0, fmt.Errorf("LoadImageW failed: %v", err) + } + return syscall.Handle(h), nil +} + +func MoveWindow(hwnd syscall.Handle, x, y, width, height int32, repaint bool) { + var paint uintptr + if repaint { + paint = TRUE + } + _MoveWindow.Call(uintptr(hwnd), uintptr(x), uintptr(y), uintptr(width), uintptr(height), paint) +} + +func monitorFromPoint(pt Point, flags uint32) syscall.Handle { + r, _, _ := _MonitorFromPoint.Call(uintptr(pt.X), uintptr(pt.Y), uintptr(flags)) + return syscall.Handle(r) +} + +func MsgWaitForMultipleObjectsEx(nCount uint32, pHandles uintptr, millis, mask, flags uint32) (uint32, error) { + r, _, err := _MsgWaitForMultipleObjectsEx.Call(uintptr(nCount), pHandles, uintptr(millis), uintptr(mask), uintptr(flags)) + res := uint32(r) + if res == 0xFFFFFFFF { + return 0, fmt.Errorf("MsgWaitForMultipleObjectsEx failed: %v", err) + } + return res, nil +} + +func OpenClipboard(hwnd syscall.Handle) error { + r, _, err := _OpenClipboard.Call(uintptr(hwnd)) + if r == 0 { + return fmt.Errorf("OpenClipboard: %v", err) + } + return nil +} + +func PeekMessage(m *Msg, hwnd syscall.Handle, wMsgFilterMin, wMsgFilterMax, wRemoveMsg uint32) bool { + r, _, _ := _PeekMessage.Call(uintptr(unsafe.Pointer(m)), uintptr(hwnd), uintptr(wMsgFilterMin), uintptr(wMsgFilterMax), uintptr(wRemoveMsg)) + issue34474KeepAlive(m) + return r != 0 +} + +func PostQuitMessage(exitCode uintptr) { + _PostQuitMessage.Call(exitCode) +} + +func PostMessage(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) error { + r, _, err := _PostMessage.Call(uintptr(hwnd), uintptr(msg), wParam, lParam) + if r == 0 { + return fmt.Errorf("PostMessage failed: %v", err) + } + return nil +} + +func ReleaseCapture() bool { + r, _, _ := _ReleaseCapture.Call() + return r != 0 +} + +func RegisterClassEx(cls *WndClassEx) (uint16, error) { + a, _, err := _RegisterClassExW.Call(uintptr(unsafe.Pointer(cls))) + issue34474KeepAlive(cls) + if a == 0 { + return 0, fmt.Errorf("RegisterClassExW failed: %v", err) + } + return uint16(a), nil +} + +func ReleaseDC(hdc syscall.Handle) { + _ReleaseDC.Call(uintptr(hdc)) +} + +func SetForegroundWindow(hwnd syscall.Handle) { + _SetForegroundWindow.Call(uintptr(hwnd)) +} + +func SetFocus(hwnd syscall.Handle) { + _SetFocus.Call(uintptr(hwnd)) +} + +func SetProcessDPIAware() { + _SetProcessDPIAware.Call() +} + +func SetCapture(hwnd syscall.Handle) syscall.Handle { + r, _, _ := _SetCapture.Call(uintptr(hwnd)) + return syscall.Handle(r) +} + +func SetClipboardData(format uint32, mem syscall.Handle) error { + r, _, err := _SetClipboardData.Call(uintptr(format), uintptr(mem)) + if r == 0 { + return fmt.Errorf("SetClipboardData: %v", err) + } + return nil +} + +func SetCursor(h syscall.Handle) { + _SetCursor.Call(uintptr(h)) +} + +func SetTimer(hwnd syscall.Handle, nIDEvent uintptr, uElapse uint32, timerProc uintptr) error { + r, _, err := _SetTimer.Call(uintptr(hwnd), uintptr(nIDEvent), uintptr(uElapse), timerProc) + if r == 0 { + return fmt.Errorf("SetTimer failed: %v", err) + } + return nil +} + +func ScreenToClient(hwnd syscall.Handle, p *Point) { + _ScreenToClient.Call(uintptr(hwnd), uintptr(unsafe.Pointer(p))) + issue34474KeepAlive(p) +} + +func ShowWindow(hwnd syscall.Handle, nCmdShow int32) { + _ShowWindow.Call(uintptr(hwnd), uintptr(nCmdShow)) +} + +func TranslateMessage(m *Msg) { + _TranslateMessage.Call(uintptr(unsafe.Pointer(m))) + issue34474KeepAlive(m) +} + +func UnregisterClass(cls uint16, hInst syscall.Handle) { + _UnregisterClass.Call(uintptr(cls), uintptr(hInst)) +} + +func UpdateWindow(hwnd syscall.Handle) { + _UpdateWindow.Call(uintptr(hwnd)) +} + +// issue34474KeepAlive calls runtime.KeepAlive as a +// workaround for golang.org/issue/34474. +func issue34474KeepAlive(v interface{}) { + runtime.KeepAlive(v) +} diff --git a/gio/giold/app/internal/wm/Gio.java b/gio/giold/app/internal/wm/Gio.java new file mode 100644 index 0000000..33e1a68 --- /dev/null +++ b/gio/giold/app/internal/wm/Gio.java @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package org.gioui; + +import android.content.ClipboardManager; +import android.content.ClipData; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import java.io.UnsupportedEncodingException; + +public final class Gio { + private static final Object initLock = new Object(); + private static boolean jniLoaded; + private static final Handler handler = new Handler(Looper.getMainLooper()); + + /** + * init loads and initializes the Go native library and runs + * the Go main function. + * + * It is exported for use by Android apps that need to run Go code + * outside the lifecycle of the Gio activity. + */ + public static synchronized void init(Context appCtx) { + synchronized (initLock) { + if (jniLoaded) { + return; + } + String dataDir = appCtx.getFilesDir().getAbsolutePath(); + byte[] dataDirUTF8; + try { + dataDirUTF8 = dataDir.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + System.loadLibrary("gio"); + runGoMain(dataDirUTF8, appCtx); + jniLoaded = true; + } + } + + static private native void runGoMain(byte[] dataDir, Context context); + + static void writeClipboard(Context ctx, String s) { + ClipboardManager m = (ClipboardManager)ctx.getSystemService(Context.CLIPBOARD_SERVICE); + m.setPrimaryClip(ClipData.newPlainText(null, s)); + } + + static String readClipboard(Context ctx) { + ClipboardManager m = (ClipboardManager)ctx.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData c = m.getPrimaryClip(); + if (c == null || c.getItemCount() < 1) { + return null; + } + return c.getItemAt(0).coerceToText(ctx).toString(); + } + + static void wakeupMainThread() { + handler.post(new Runnable() { + @Override public void run() { + scheduleMainFuncs(); + } + }); + } + + static private native void scheduleMainFuncs(); +} diff --git a/gio/giold/app/internal/wm/GioActivity.java b/gio/giold/app/internal/wm/GioActivity.java new file mode 100644 index 0000000..260d4b6 --- /dev/null +++ b/gio/giold/app/internal/wm/GioActivity.java @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package org.gioui; + +import android.app.Activity; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +public final class GioActivity extends Activity { + private GioView view; + + @Override public void onCreate(Bundle state) { + super.onCreate(state); + + Window w = getWindow(); + + this.view = new GioView(this); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + this.view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + this.view.setLayoutParams(new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT)); + setContentView(view); + } + + @Override public void onDestroy() { + view.destroy(); + super.onDestroy(); + } + + @Override public void onStart() { + super.onStart(); + view.start(); + } + + @Override public void onStop() { + view.stop(); + super.onStop(); + } + + @Override public void onConfigurationChanged(Configuration c) { + super.onConfigurationChanged(c); + view.configurationChanged(); + } + + @Override public void onLowMemory() { + super.onLowMemory(); + view.lowMemory(); + } + + @Override public void onBackPressed() { + if (!view.backPressed()) + super.onBackPressed(); + } +} diff --git a/gio/giold/app/internal/wm/GioView.java b/gio/giold/app/internal/wm/GioView.java new file mode 100644 index 0000000..7ed9f05 --- /dev/null +++ b/gio/giold/app/internal/wm/GioView.java @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package org.gioui; + +import java.lang.Class; +import java.lang.IllegalAccessException; +import java.lang.InstantiationException; +import java.lang.ExceptionInInitializerError; +import java.lang.SecurityException; +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; +import android.os.Build; +import android.text.Editable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Choreographer; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.PointerIcon; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.WindowInsets; +import android.view.Surface; +import android.view.SurfaceView; +import android.view.SurfaceHolder; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.EditorInfo; + +import java.io.UnsupportedEncodingException; + +public final class GioView extends SurfaceView implements Choreographer.FrameCallback { + private static boolean jniLoaded; + + private final SurfaceHolder.Callback surfCallbacks; + private final View.OnFocusChangeListener focusCallback; + private final InputMethodManager imm; + private final float scrollXScale; + private final float scrollYScale; + + private long nhandle; + + public GioView(Context context) { + this(context, null); + } + + public GioView(Context context, AttributeSet attrs) { + super(context, attrs); + + // Late initialization of the Go runtime to wait for a valid context. + Gio.init(context.getApplicationContext()); + + // Set background color to transparent to avoid a flickering + // issue on ChromeOS. + setBackgroundColor(Color.argb(0, 0, 0, 0)); + + ViewConfiguration conf = ViewConfiguration.get(context); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + scrollXScale = conf.getScaledHorizontalScrollFactor(); + scrollYScale = conf.getScaledVerticalScrollFactor(); + + // The platform focus highlight is not aware of Gio's widgets. + setDefaultFocusHighlightEnabled(false); + } else { + float listItemHeight = 48; // dp + float px = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + listItemHeight, + getResources().getDisplayMetrics() + ); + scrollXScale = px; + scrollYScale = px; + } + + nhandle = onCreateView(this); + imm = (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); + setFocusable(true); + setFocusableInTouchMode(true); + focusCallback = new View.OnFocusChangeListener() { + @Override public void onFocusChange(View v, boolean focus) { + GioView.this.onFocusChange(nhandle, focus); + } + }; + setOnFocusChangeListener(focusCallback); + surfCallbacks = new SurfaceHolder.Callback() { + @Override public void surfaceCreated(SurfaceHolder holder) { + // Ignore; surfaceChanged is guaranteed to be called immediately after this. + } + @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + onSurfaceChanged(nhandle, getHolder().getSurface()); + } + @Override public void surfaceDestroyed(SurfaceHolder holder) { + onSurfaceDestroyed(nhandle); + } + }; + getHolder().addCallback(surfCallbacks); + } + + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { + onKeyEvent(nhandle, keyCode, event.getUnicodeChar(), event.getEventTime()); + return false; + } + + @Override public boolean onGenericMotionEvent(MotionEvent event) { + dispatchMotionEvent(event); + return true; + } + + @Override public boolean onTouchEvent(MotionEvent event) { + // Ask for unbuffered events. Flutter and Chrome do it + // so assume it's good for us as well. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + requestUnbufferedDispatch(event); + } + + dispatchMotionEvent(event); + return true; + } + + private void setCursor(int id) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return; + } + PointerIcon pointerIcon = PointerIcon.getSystemIcon(getContext(), id); + setPointerIcon(pointerIcon); + } + + private void dispatchMotionEvent(MotionEvent event) { + for (int j = 0; j < event.getHistorySize(); j++) { + long time = event.getHistoricalEventTime(j); + for (int i = 0; i < event.getPointerCount(); i++) { + onTouchEvent( + nhandle, + event.ACTION_MOVE, + event.getPointerId(i), + event.getToolType(i), + event.getHistoricalX(i, j), + event.getHistoricalY(i, j), + scrollXScale*event.getHistoricalAxisValue(MotionEvent.AXIS_HSCROLL, i, j), + scrollYScale*event.getHistoricalAxisValue(MotionEvent.AXIS_VSCROLL, i, j), + event.getButtonState(), + time); + } + } + int act = event.getActionMasked(); + int idx = event.getActionIndex(); + for (int i = 0; i < event.getPointerCount(); i++) { + int pact = event.ACTION_MOVE; + if (i == idx) { + pact = act; + } + onTouchEvent( + nhandle, + pact, + event.getPointerId(i), + event.getToolType(i), + event.getX(i), event.getY(i), + scrollXScale*event.getAxisValue(MotionEvent.AXIS_HSCROLL, i), + scrollYScale*event.getAxisValue(MotionEvent.AXIS_VSCROLL, i), + event.getButtonState(), + event.getEventTime()); + } + } + + @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return new InputConnection(this); + } + + void showTextInput() { + GioView.this.requestFocus(); + imm.showSoftInput(GioView.this, 0); + } + + void hideTextInput() { + imm.hideSoftInputFromWindow(getWindowToken(), 0); + } + + @Override protected boolean fitSystemWindows(Rect insets) { + onWindowInsets(nhandle, insets.top, insets.right, insets.bottom, insets.left); + return true; + } + + void postFrameCallback() { + Choreographer.getInstance().removeFrameCallback(this); + Choreographer.getInstance().postFrameCallback(this); + } + + @Override public void doFrame(long nanos) { + onFrameCallback(nhandle, nanos); + } + + int getDensity() { + return getResources().getDisplayMetrics().densityDpi; + } + + float getFontScale() { + return getResources().getConfiguration().fontScale; + } + + void start() { + onStartView(nhandle); + } + + void stop() { + onStopView(nhandle); + } + + void destroy() { + setOnFocusChangeListener(null); + getHolder().removeCallback(surfCallbacks); + onDestroyView(nhandle); + nhandle = 0; + } + + void configurationChanged() { + onConfigurationChanged(nhandle); + } + + void lowMemory() { + onLowMemory(); + } + + boolean backPressed() { + return onBack(nhandle); + } + + static private native long onCreateView(GioView view); + static private native void onDestroyView(long handle); + static private native void onStartView(long handle); + static private native void onStopView(long handle); + static private native void onSurfaceDestroyed(long handle); + static private native void onSurfaceChanged(long handle, Surface surface); + static private native void onConfigurationChanged(long handle); + static private native void onWindowInsets(long handle, int top, int right, int bottom, int left); + static private native void onLowMemory(); + static private native void onTouchEvent(long handle, int action, int pointerID, int tool, float x, float y, float scrollX, float scrollY, int buttons, long time); + static private native void onKeyEvent(long handle, int code, int character, long time); + static private native void onFrameCallback(long handle, long nanos); + static private native boolean onBack(long handle); + static private native void onFocusChange(long handle, boolean focus); + + private static class InputConnection extends BaseInputConnection { + private final Editable editable; + + InputConnection(View view) { + // Passing false enables "dummy mode", where the BaseInputConnection + // attempts to convert IME operations to key events. + super(view, false); + editable = Editable.Factory.getInstance().newEditable(""); + } + + @Override public Editable getEditable() { + return editable; + } + } +} diff --git a/gio/giold/app/internal/wm/d3d11_windows.go b/gio/giold/app/internal/wm/d3d11_windows.go new file mode 100644 index 0000000..8368933 --- /dev/null +++ b/gio/giold/app/internal/wm/d3d11_windows.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +import ( + "fmt" + "unsafe" + + "realy.lol/gio/gpu" + "realy.lol/gio/internal/d3d11" +) + +type d3d11Context struct { + win *window + dev *d3d11.Device + ctx *d3d11.DeviceContext + + swchain *d3d11.IDXGISwapChain + renderTarget *d3d11.RenderTargetView + depthView *d3d11.DepthStencilView + width, height int +} + +const debug = false + +func init() { + drivers = append(drivers, gpuAPI{ + priority: 1, + initializer: func(w *window) (Context, error) { + hwnd, _, _ := w.HWND() + var flags uint32 + if debug { + flags |= d3d11.CREATE_DEVICE_DEBUG + } + dev, ctx, _, err := d3d11.CreateDevice( + d3d11.DRIVER_TYPE_HARDWARE, + flags, + ) + if err != nil { + return nil, fmt.Errorf("NewContext: %v", err) + } + swchain, err := d3d11.CreateSwapChain(dev, hwnd) + if err != nil { + d3d11.IUnknownRelease(unsafe.Pointer(ctx), ctx.Vtbl.Release) + d3d11.IUnknownRelease(unsafe.Pointer(dev), dev.Vtbl.Release) + return nil, err + } + return &d3d11Context{win: w, dev: dev, ctx: ctx, + swchain: swchain}, nil + }, + }) +} + +func (c *d3d11Context) API() gpu.API { + return gpu.Direct3D11{Device: unsafe.Pointer(c.dev)} +} + +func (c *d3d11Context) Present() error { + err := c.swchain.Present(1, 0) + if err == nil { + return nil + } + if err, ok := err.(d3d11.ErrorCode); ok { + switch err.Code { + case d3d11.DXGI_STATUS_OCCLUDED: + // Ignore + return nil + case d3d11.DXGI_ERROR_DEVICE_RESET, d3d11.DXGI_ERROR_DEVICE_REMOVED, d3d11.D3DDDIERR_DEVICEREMOVED: + return ErrDeviceLost + } + } + return err +} + +func (c *d3d11Context) MakeCurrent() error { + _, width, height := c.win.HWND() + if c.renderTarget != nil && width == c.width && height == c.height { + c.ctx.OMSetRenderTargets(c.renderTarget, c.depthView) + return nil + } + c.releaseFBO() + if err := c.swchain.ResizeBuffers(0, 0, 0, d3d11.DXGI_FORMAT_UNKNOWN, + 0); err != nil { + return err + } + c.width = width + c.height = height + + desc, err := c.swchain.GetDesc() + if err != nil { + return err + } + backBuffer, err := c.swchain.GetBuffer(0, &d3d11.IID_Texture2D) + if err != nil { + return err + } + texture := (*d3d11.Resource)(unsafe.Pointer(backBuffer)) + renderTarget, err := c.dev.CreateRenderTargetView(texture) + d3d11.IUnknownRelease(unsafe.Pointer(backBuffer), backBuffer.Vtbl.Release) + if err != nil { + return err + } + depthView, err := d3d11.CreateDepthView(c.dev, int(desc.BufferDesc.Width), + int(desc.BufferDesc.Height), 24) + if err != nil { + d3d11.IUnknownRelease(unsafe.Pointer(renderTarget), + renderTarget.Vtbl.Release) + return err + } + c.renderTarget = renderTarget + c.depthView = depthView + + c.ctx.OMSetRenderTargets(c.renderTarget, c.depthView) + return nil +} + +func (c *d3d11Context) Lock() {} + +func (c *d3d11Context) Unlock() {} + +func (c *d3d11Context) Release() { + c.releaseFBO() + if c.swchain != nil { + d3d11.IUnknownRelease(unsafe.Pointer(c.swchain), c.swchain.Vtbl.Release) + } + if c.ctx != nil { + d3d11.IUnknownRelease(unsafe.Pointer(c.ctx), c.ctx.Vtbl.Release) + } + if c.dev != nil { + d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release) + } + *c = d3d11Context{} +} + +func (c *d3d11Context) releaseFBO() { + if c.depthView != nil { + d3d11.IUnknownRelease(unsafe.Pointer(c.depthView), + c.depthView.Vtbl.Release) + c.depthView = nil + } + if c.renderTarget != nil { + d3d11.IUnknownRelease(unsafe.Pointer(c.renderTarget), + c.renderTarget.Vtbl.Release) + c.renderTarget = nil + } +} diff --git a/gio/giold/app/internal/wm/egl_android.go b/gio/giold/app/internal/wm/egl_android.go new file mode 100644 index 0000000..50e38ad --- /dev/null +++ b/gio/giold/app/internal/wm/egl_android.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +/* +#include +*/ +import "C" + +import ( + "unsafe" + + "realy.lol/gio/internal/egl" +) + +type context struct { + win *window + *egl.Context +} + +func (w *window) NewContext() (Context, error) { + ctx, err := egl.NewContext(nil) + if err != nil { + return nil, err + } + return &context{win: w, Context: ctx}, nil +} + +func (c *context) Release() { + if c.Context != nil { + c.Context.Release() + c.Context = nil + } +} + +func (c *context) MakeCurrent() error { + c.Context.ReleaseSurface() + win, width, height := c.win.nativeWindow(c.Context.VisualID()) + if win == nil { + return nil + } + eglSurf := egl.NativeWindowType(unsafe.Pointer(win)) + if err := c.Context.CreateSurface(eglSurf, width, height); err != nil { + return err + } + if err := c.Context.MakeCurrent(); err != nil { + return err + } + return nil +} + +func (c *context) Lock() {} + +func (c *context) Unlock() {} diff --git a/gio/giold/app/internal/wm/egl_wayland.go b/gio/giold/app/internal/wm/egl_wayland.go new file mode 100644 index 0000000..0cd5b6c --- /dev/null +++ b/gio/giold/app/internal/wm/egl_wayland.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build (linux && !android && !nowayland) || freebsd +// +build linux,!android,!nowayland freebsd + +package wm + +import ( + "errors" + "unsafe" + + "realy.lol/gio/internal/egl" +) + +/* +#cgo linux pkg-config: egl wayland-egl +#cgo freebsd openbsd LDFLAGS: -lwayland-egl +#cgo CFLAGS: -DEGL_NO_X11 + +#include +#include +#include +*/ +import "C" + +type context struct { + win *window + *egl.Context + eglWin *C.struct_wl_egl_window +} + +func (w *window) NewContext() (Context, error) { + disp := egl.NativeDisplayType(unsafe.Pointer(w.display())) + ctx, err := egl.NewContext(disp) + if err != nil { + return nil, err + } + return &context{Context: ctx, win: w}, nil +} + +func (c *context) Release() { + if c.Context != nil { + c.Context.Release() + c.Context = nil + } + if c.eglWin != nil { + C.wl_egl_window_destroy(c.eglWin) + c.eglWin = nil + } +} + +func (c *context) MakeCurrent() error { + c.Context.ReleaseSurface() + if c.eglWin != nil { + C.wl_egl_window_destroy(c.eglWin) + c.eglWin = nil + } + surf, width, height := c.win.surface() + if surf == nil { + return errors.New("wayland: no surface") + } + eglWin := C.wl_egl_window_create(surf, C.int(width), C.int(height)) + if eglWin == nil { + return errors.New("wayland: wl_egl_window_create failed") + } + c.eglWin = eglWin + eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin))) + if err := c.Context.CreateSurface(eglSurf, width, height); err != nil { + return err + } + return c.Context.MakeCurrent() +} + +func (c *context) Lock() {} + +func (c *context) Unlock() {} diff --git a/gio/giold/app/internal/wm/egl_windows.go b/gio/giold/app/internal/wm/egl_windows.go new file mode 100644 index 0000000..ce7645c --- /dev/null +++ b/gio/giold/app/internal/wm/egl_windows.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +import ( + "realy.lol/gio/internal/egl" +) + +type glContext struct { + win *window + *egl.Context +} + +func init() { + drivers = append(drivers, gpuAPI{ + priority: 2, + initializer: func(w *window) (Context, error) { + disp := egl.NativeDisplayType(w.HDC()) + ctx, err := egl.NewContext(disp) + if err != nil { + return nil, err + } + return &glContext{win: w, Context: ctx}, nil + }, + }) +} + +func (c *glContext) Release() { + if c.Context != nil { + c.Context.Release() + c.Context = nil + } +} + +func (c *glContext) MakeCurrent() error { + c.Context.ReleaseSurface() + win, width, height := c.win.HWND() + eglSurf := egl.NativeWindowType(win) + if err := c.Context.CreateSurface(eglSurf, width, height); err != nil { + return err + } + if err := c.Context.MakeCurrent(); err != nil { + return err + } + c.Context.EnableVSync(true) + return nil +} + +func (c *glContext) Lock() {} + +func (c *glContext) Unlock() {} diff --git a/gio/giold/app/internal/wm/egl_x11.go b/gio/giold/app/internal/wm/egl_x11.go new file mode 100644 index 0000000..556cd78 --- /dev/null +++ b/gio/giold/app/internal/wm/egl_x11.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build (linux && !android && !nox11) || freebsd || openbsd +// +build linux,!android,!nox11 freebsd openbsd + +package wm + +import ( + "unsafe" + + "realy.lol/gio/internal/egl" +) + +type x11Context struct { + win *x11Window + *egl.Context +} + +func (w *x11Window) NewContext() (Context, error) { + disp := egl.NativeDisplayType(unsafe.Pointer(w.display())) + ctx, err := egl.NewContext(disp) + if err != nil { + return nil, err + } + return &x11Context{win: w, Context: ctx}, nil +} + +func (c *x11Context) Release() { + if c.Context != nil { + c.Context.Release() + c.Context = nil + } +} + +func (c *x11Context) MakeCurrent() error { + c.Context.ReleaseSurface() + win, width, height := c.win.window() + eglSurf := egl.NativeWindowType(uintptr(win)) + if err := c.Context.CreateSurface(eglSurf, width, height); err != nil { + return err + } + if err := c.Context.MakeCurrent(); err != nil { + return err + } + c.Context.EnableVSync(true) + return nil +} + +func (c *x11Context) Lock() {} + +func (c *x11Context) Unlock() {} diff --git a/gio/giold/app/internal/wm/framework_ios.h b/gio/giold/app/internal/wm/framework_ios.h new file mode 100644 index 0000000..18e5a02 --- /dev/null +++ b/gio/giold/app/internal/wm/framework_ios.h @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +#include + +@interface GioViewController : UIViewController +@end diff --git a/gio/giold/app/internal/wm/gl_ios.go b/gio/giold/app/internal/wm/gl_ios.go new file mode 100644 index 0000000..b3e7a47 --- /dev/null +++ b/gio/giold/app/internal/wm/gl_ios.go @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build darwin && ios +// +build darwin,ios + +package wm + +/* +#include +#include +#include + +__attribute__ ((visibility ("hidden"))) int gio_renderbufferStorage(CFTypeRef ctx, CFTypeRef layer, GLenum buffer); +__attribute__ ((visibility ("hidden"))) int gio_presentRenderbuffer(CFTypeRef ctx, GLenum buffer); +__attribute__ ((visibility ("hidden"))) int gio_makeCurrent(CFTypeRef ctx); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createContext(void); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createGLLayer(void); +*/ +import "C" + +import ( + "errors" + "fmt" + + "realy.lol/gio/gpu" + "realy.lol/gio/internal/gl" +) + +type context struct { + owner *window + c *gl.Functions + ctx C.CFTypeRef + layer C.CFTypeRef + init bool + frameBuffer gl.Framebuffer + colorBuffer, depthBuffer gl.Renderbuffer +} + +func init() { + layerFactory = func() uintptr { + return uintptr(C.gio_createGLLayer()) + } +} + +func newContext(w *window) (*context, error) { + ctx := C.gio_createContext() + if ctx == 0 { + return nil, fmt.Errorf("failed to create EAGLContext") + } + c := &context{ + ctx: ctx, + owner: w, + layer: C.CFTypeRef(w.contextLayer()), + c: new(gl.Functions), + } + return c, nil +} + +func (c *context) API() gpu.API { + return gpu.OpenGL{} +} + +func (c *context) Release() { + if c.ctx == 0 { + return + } + C.gio_renderbufferStorage(c.ctx, 0, C.GLenum(gl.RENDERBUFFER)) + c.c.DeleteFramebuffer(c.frameBuffer) + c.c.DeleteRenderbuffer(c.colorBuffer) + c.c.DeleteRenderbuffer(c.depthBuffer) + C.gio_makeCurrent(0) + C.CFRelease(c.ctx) + c.ctx = 0 +} + +func (c *context) Present() error { + if c.layer == 0 { + panic("context is not active") + } + // Discard depth buffer as recommended in + // https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/WorkingwithEAGLContexts/WorkingwithEAGLContexts.html + c.c.BindFramebuffer(gl.FRAMEBUFFER, c.frameBuffer) + c.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT) + c.c.BindRenderbuffer(gl.RENDERBUFFER, c.colorBuffer) + if C.gio_presentRenderbuffer(c.ctx, C.GLenum(gl.RENDERBUFFER)) == 0 { + return errors.New("presentRenderBuffer failed") + } + return nil +} + +func (c *context) Lock() {} + +func (c *context) Unlock() {} + +func (c *context) MakeCurrent() error { + if C.gio_makeCurrent(c.ctx) == 0 { + C.CFRelease(c.ctx) + c.ctx = 0 + return errors.New("[EAGLContext setCurrentContext] failed") + } + if !c.init { + c.init = true + c.frameBuffer = c.c.CreateFramebuffer() + c.colorBuffer = c.c.CreateRenderbuffer() + c.depthBuffer = c.c.CreateRenderbuffer() + } + if !c.owner.isVisible() { + // Make sure any in-flight GL commands are complete. + c.c.Finish() + return nil + } + currentRB := gl.Renderbuffer{uint(c.c.GetInteger(gl.RENDERBUFFER_BINDING))} + c.c.BindRenderbuffer(gl.RENDERBUFFER, c.colorBuffer) + if C.gio_renderbufferStorage(c.ctx, c.layer, + C.GLenum(gl.RENDERBUFFER)) == 0 { + return errors.New("renderbufferStorage failed") + } + w := c.c.GetRenderbufferParameteri(gl.RENDERBUFFER, gl.RENDERBUFFER_WIDTH) + h := c.c.GetRenderbufferParameteri(gl.RENDERBUFFER, gl.RENDERBUFFER_HEIGHT) + c.c.BindRenderbuffer(gl.RENDERBUFFER, c.depthBuffer) + c.c.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h) + c.c.BindRenderbuffer(gl.RENDERBUFFER, currentRB) + c.c.BindFramebuffer(gl.FRAMEBUFFER, c.frameBuffer) + c.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, + gl.RENDERBUFFER, c.colorBuffer) + c.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, + gl.RENDERBUFFER, c.depthBuffer) + if st := c.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { + return fmt.Errorf("framebuffer incomplete, status: %#x\n", st) + } + return nil +} + +func (w *window) NewContext() (Context, error) { + return newContext(w) +} diff --git a/gio/giold/app/internal/wm/gl_ios.m b/gio/giold/app/internal/wm/gl_ios.m new file mode 100644 index 0000000..065ea97 --- /dev/null +++ b/gio/giold/app/internal/wm/gl_ios.m @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,ios + +@import UIKit; +@import OpenGLES; + +#include "_cgo_export.h" + +int gio_renderbufferStorage(CFTypeRef ctxRef, CFTypeRef layerRef, GLenum buffer) { + EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; + CAEAGLLayer *layer = (__bridge CAEAGLLayer *)layerRef; + return (int)[ctx renderbufferStorage:buffer fromDrawable:layer]; +} + +int gio_presentRenderbuffer(CFTypeRef ctxRef, GLenum buffer) { + EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; + return (int)[ctx presentRenderbuffer:buffer]; +} + +int gio_makeCurrent(CFTypeRef ctxRef) { + EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; + return (int)[EAGLContext setCurrentContext:ctx]; +} + +CFTypeRef gio_createContext(void) { + EAGLContext *ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; + if (ctx == nil) { + return nil; + } + return CFBridgingRetain(ctx); +} + +CFTypeRef gio_createGLLayer(void) { + CAEAGLLayer *layer = [[CAEAGLLayer layer] init]; + if (layer == nil) { + return nil; + } + layer.drawableProperties = @{kEAGLDrawablePropertyColorFormat: kEAGLColorFormatSRGBA8}; + layer.opaque = YES; + layer.anchorPoint = CGPointMake(0, 0); + return CFBridgingRetain(layer); +} diff --git a/gio/giold/app/internal/wm/gl_js.go b/gio/giold/app/internal/wm/gl_js.go new file mode 100644 index 0000000..0a931b6 --- /dev/null +++ b/gio/giold/app/internal/wm/gl_js.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +import ( + "errors" + "syscall/js" + + "realy.lol/gio/gpu" + "realy.lol/gio/internal/gl" + "realy.lol/gio/internal/srgb" +) + +type context struct { + ctx js.Value + cnv js.Value + srgbFBO *srgb.FBO +} + +func newContext(w *window) (*context, error) { + args := map[string]interface{}{ + // Enable low latency rendering. + // See https://developers.google.com/web/updates/2019/05/desynchronized. + "desynchronized": true, + "preserveDrawingBuffer": true, + } + ctx := w.cnv.Call("getContext", "webgl2", args) + if ctx.IsNull() { + ctx = w.cnv.Call("getContext", "webgl", args) + } + if ctx.IsNull() { + return nil, errors.New("app: webgl is not supported") + } + c := &context{ + ctx: ctx, + cnv: w.cnv, + } + return c, nil +} + +func (c *context) API() gpu.API { + return gpu.OpenGL{Context: gl.Context(c.ctx)} +} + +func (c *context) Release() { + if c.srgbFBO != nil { + c.srgbFBO.Release() + c.srgbFBO = nil + } +} + +func (c *context) Present() error { + if c.srgbFBO != nil { + c.srgbFBO.Blit() + } + if c.srgbFBO != nil { + c.srgbFBO.AfterPresent() + } + if c.ctx.Call("isContextLost").Bool() { + return errors.New("context lost") + } + return nil +} + +func (c *context) Lock() {} + +func (c *context) Unlock() {} + +func (c *context) MakeCurrent() error { + if c.srgbFBO == nil { + var err error + c.srgbFBO, err = srgb.New(gl.Context(c.ctx)) + if err != nil { + c.Release() + c.srgbFBO = nil + return err + } + } + w, h := c.cnv.Get("width").Int(), c.cnv.Get("height").Int() + if err := c.srgbFBO.Refresh(w, h); err != nil { + c.Release() + return err + } + return nil +} + +func (w *window) NewContext() (Context, error) { + return newContext(w) +} diff --git a/gio/giold/app/internal/wm/gl_macos.go b/gio/giold/app/internal/wm/gl_macos.go new file mode 100644 index 0000000..a95557b --- /dev/null +++ b/gio/giold/app/internal/wm/gl_macos.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build darwin && !ios +// +build darwin,!ios + +package wm + +import ( + "realy.lol/gio/gpu" + "realy.lol/gio/internal/gl" +) + +/* +#include +#include +#include +#include + +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createGLView(void); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_contextForView(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) void gio_makeCurrentContext(CFTypeRef ctx); +__attribute__ ((visibility ("hidden"))) void gio_flushContextBuffer(CFTypeRef ctx); +__attribute__ ((visibility ("hidden"))) void gio_clearCurrentContext(void); +__attribute__ ((visibility ("hidden"))) void gio_lockContext(CFTypeRef ctxRef); +__attribute__ ((visibility ("hidden"))) void gio_unlockContext(CFTypeRef ctxRef); +*/ +import "C" + +type context struct { + c *gl.Functions + ctx C.CFTypeRef + view C.CFTypeRef +} + +func init() { + viewFactory = func() C.CFTypeRef { + return C.gio_createGLView() + } +} + +func newContext(w *window) (*context, error) { + view := w.contextView() + ctx := C.gio_contextForView(view) + c := &context{ + ctx: ctx, + view: view, + } + return c, nil +} + +func (c *context) API() gpu.API { + return gpu.OpenGL{} +} + +func (c *context) Release() { + c.Lock() + defer c.Unlock() + C.gio_clearCurrentContext() + // We could release the context with [view clearGLContext] + // and rely on [view openGLContext] auto-creating a new context. + // However that second context is not properly set up by + // OpenGLContextView, so we'll stay on the safe side and keep + // the first context around. +} + +func (c *context) Present() error { + // Assume the caller already locked the context. + C.glFlush() + return nil +} + +func (c *context) Lock() { + C.gio_lockContext(c.ctx) +} + +func (c *context) Unlock() { + C.gio_unlockContext(c.ctx) +} + +func (c *context) MakeCurrent() error { + c.Lock() + defer c.Unlock() + C.gio_makeCurrentContext(c.ctx) + return nil +} + +func (w *window) NewContext() (Context, error) { + return newContext(w) +} diff --git a/gio/giold/app/internal/wm/gl_macos.m b/gio/giold/app/internal/wm/gl_macos.m new file mode 100644 index 0000000..576aa40 --- /dev/null +++ b/gio/giold/app/internal/wm/gl_macos.m @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,!ios + +@import AppKit; + +#include +#include +#include +#include "_cgo_export.h" + +static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) { + NSPoint p = [view convertPoint:[event locationInWindow] fromView:nil]; + if (!event.hasPreciseScrollingDeltas) { + // dx and dy are in rows and columns. + dx *= 10; + dy *= 10; + } + gio_onMouse((__bridge CFTypeRef)view, typ, [NSEvent pressedMouseButtons], p.x, p.y, dx, dy, [event timestamp], [event modifierFlags]); +} + +@interface GioView : NSOpenGLView +@end + +@implementation GioView +- (instancetype)initWithFrame:(NSRect)frameRect + pixelFormat:(NSOpenGLPixelFormat *)format { + return [super initWithFrame:frameRect pixelFormat:format]; +} +- (void)prepareOpenGL { + [super prepareOpenGL]; + // Bind a default VBA to emulate OpenGL ES 2. + GLuint defVBA; + glGenVertexArrays(1, &defVBA); + glBindVertexArray(defVBA); + glEnable(GL_FRAMEBUFFER_SRGB); +} +- (BOOL)isFlipped { + return YES; +} +- (void)update { + [super update]; + [self setNeedsDisplay:YES]; +} +- (void)drawRect:(NSRect)r { + gio_onDraw((__bridge CFTypeRef)self); +} +- (void)mouseDown:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_DOWN, 0, 0); +} +- (void)mouseUp:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_UP, 0, 0); +} +- (void)middleMouseDown:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_DOWN, 0, 0); +} +- (void)middletMouseUp:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_UP, 0, 0); +} +- (void)rightMouseDown:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_DOWN, 0, 0); +} +- (void)rightMouseUp:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_UP, 0, 0); +} +- (void)mouseMoved:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_MOVE, 0, 0); +} +- (void)mouseDragged:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_MOVE, 0, 0); +} +- (void)scrollWheel:(NSEvent *)event { + CGFloat dx = -event.scrollingDeltaX; + CGFloat dy = -event.scrollingDeltaY; + handleMouse(self, event, GIO_MOUSE_SCROLL, dx, dy); +} +- (void)keyDown:(NSEvent *)event { + NSString *keys = [event charactersIgnoringModifiers]; + gio_onKeys((__bridge CFTypeRef)self, (char *)[keys UTF8String], [event timestamp], [event modifierFlags], true); + [self interpretKeyEvents:[NSArray arrayWithObject:event]]; +} +- (void)keyUp:(NSEvent *)event { + NSString *keys = [event charactersIgnoringModifiers]; + gio_onKeys((__bridge CFTypeRef)self, (char *)[keys UTF8String], [event timestamp], [event modifierFlags], false); +} +- (void)insertText:(id)string { + const char *utf8 = [string UTF8String]; + gio_onText((__bridge CFTypeRef)self, (char *)utf8); +} +- (void)doCommandBySelector:(SEL)sel { + // Don't pass commands up the responder chain. + // They will end up in a beep. +} +@end + +CFTypeRef gio_createGLView(void) { + @autoreleasepool { + NSOpenGLPixelFormatAttribute attr[] = { + NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, + NSOpenGLPFAColorSize, 24, + NSOpenGLPFADepthSize, 16, + NSOpenGLPFAAccelerated, + // Opt-in to automatic GPU switching. CGL-only property. + kCGLPFASupportsAutomaticGraphicsSwitching, + NSOpenGLPFAAllowOfflineRenderers, + 0 + }; + id pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr]; + + NSRect frame = NSMakeRect(0, 0, 0, 0); + GioView* view = [[GioView alloc] initWithFrame:frame pixelFormat:pixFormat]; + + [view setWantsBestResolutionOpenGLSurface:YES]; + [view setWantsLayer:YES]; // The default in Mojave. + + return CFBridgingRetain(view); + } +} + +void gio_setNeedsDisplay(CFTypeRef viewRef) { + NSOpenGLView *view = (__bridge NSOpenGLView *)viewRef; + [view setNeedsDisplay:YES]; +} + +CFTypeRef gio_contextForView(CFTypeRef viewRef) { + NSOpenGLView *view = (__bridge NSOpenGLView *)viewRef; + return (__bridge CFTypeRef)view.openGLContext; +} + +void gio_clearCurrentContext(void) { + [NSOpenGLContext clearCurrentContext]; +} + +void gio_makeCurrentContext(CFTypeRef ctxRef) { + NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; + [ctx makeCurrentContext]; +} + +void gio_lockContext(CFTypeRef ctxRef) { + NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; + CGLLockContext([ctx CGLContextObj]); +} + +void gio_unlockContext(CFTypeRef ctxRef) { + NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; + CGLUnlockContext([ctx CGLContextObj]); +} diff --git a/gio/giold/app/internal/wm/os_android.c b/gio/giold/app/internal/wm/os_android.c new file mode 100644 index 0000000..8a2c62d --- /dev/null +++ b/gio/giold/app/internal/wm/os_android.c @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +#include +#include "_cgo_export.h" + +jint gio_jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version) { + return (*vm)->GetEnv(vm, (void **)env, version); +} + +jint gio_jni_GetJavaVM(JNIEnv *env, JavaVM **jvm) { + return (*env)->GetJavaVM(env, jvm); +} + +jint gio_jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args) { + return (*vm)->AttachCurrentThread(vm, p_env, thr_args); +} + +jint gio_jni_DetachCurrentThread(JavaVM *vm) { + return (*vm)->DetachCurrentThread(vm); +} + +jobject gio_jni_NewGlobalRef(JNIEnv *env, jobject obj) { + return (*env)->NewGlobalRef(env, obj); +} + +void gio_jni_DeleteGlobalRef(JNIEnv *env, jobject obj) { + (*env)->DeleteGlobalRef(env, obj); +} + +jclass gio_jni_GetObjectClass(JNIEnv *env, jobject obj) { + return (*env)->GetObjectClass(env, obj); +} + +jmethodID gio_jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { + return (*env)->GetMethodID(env, clazz, name, sig); +} + +jmethodID gio_jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { + return (*env)->GetStaticMethodID(env, clazz, name, sig); +} + +jfloat gio_jni_CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID) { + return (*env)->CallFloatMethod(env, obj, methodID); +} + +jint gio_jni_CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID) { + return (*env)->CallIntMethod(env, obj, methodID); +} + +void gio_jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args) { + (*env)->CallStaticVoidMethodA(env, cls, methodID, args); +} + +void gio_jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args) { + (*env)->CallVoidMethodA(env, obj, methodID, args); +} + +jbyte *gio_jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr) { + return (*env)->GetByteArrayElements(env, arr, NULL); +} + +void gio_jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *bytes) { + (*env)->ReleaseByteArrayElements(env, arr, bytes, JNI_ABORT); +} + +jsize gio_jni_GetArrayLength(JNIEnv *env, jbyteArray arr) { + return (*env)->GetArrayLength(env, arr); +} + +jstring gio_jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len) { + return (*env)->NewString(env, unicodeChars, len); +} + +jsize gio_jni_GetStringLength(JNIEnv *env, jstring str) { + return (*env)->GetStringLength(env, str); +} + +const jchar *gio_jni_GetStringChars(JNIEnv *env, jstring str) { + return (*env)->GetStringChars(env, str, NULL); +} + +jthrowable gio_jni_ExceptionOccurred(JNIEnv *env) { + return (*env)->ExceptionOccurred(env); +} + +void gio_jni_ExceptionClear(JNIEnv *env) { + (*env)->ExceptionClear(env); +} + +jobject gio_jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) { + return (*env)->CallObjectMethodA(env, obj, method, args); +} + +jobject gio_jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) { + return (*env)->CallStaticObjectMethodA(env, cls, method, args); +} diff --git a/gio/giold/app/internal/wm/os_android.go b/gio/giold/app/internal/wm/os_android.go new file mode 100644 index 0000000..a690c42 --- /dev/null +++ b/gio/giold/app/internal/wm/os_android.go @@ -0,0 +1,785 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +/* +#cgo CFLAGS: -Werror +#cgo LDFLAGS: -landroid + +#include +#include +#include +#include +#include + +__attribute__ ((visibility ("hidden"))) jint gio_jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version); +__attribute__ ((visibility ("hidden"))) jint gio_jni_GetJavaVM(JNIEnv *env, JavaVM **jvm); +__attribute__ ((visibility ("hidden"))) jint gio_jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args); +__attribute__ ((visibility ("hidden"))) jint gio_jni_DetachCurrentThread(JavaVM *vm); + +__attribute__ ((visibility ("hidden"))) jobject gio_jni_NewGlobalRef(JNIEnv *env, jobject obj); +__attribute__ ((visibility ("hidden"))) void gio_jni_DeleteGlobalRef(JNIEnv *env, jobject obj); +__attribute__ ((visibility ("hidden"))) jclass gio_jni_GetObjectClass(JNIEnv *env, jobject obj); +__attribute__ ((visibility ("hidden"))) jmethodID gio_jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); +__attribute__ ((visibility ("hidden"))) jmethodID gio_jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); +__attribute__ ((visibility ("hidden"))) jfloat gio_jni_CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID); +__attribute__ ((visibility ("hidden"))) jint gio_jni_CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID); +__attribute__ ((visibility ("hidden"))) void gio_jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args); +__attribute__ ((visibility ("hidden"))) void gio_jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args); +__attribute__ ((visibility ("hidden"))) jbyte *gio_jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr); +__attribute__ ((visibility ("hidden"))) void gio_jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *bytes); +__attribute__ ((visibility ("hidden"))) jsize gio_jni_GetArrayLength(JNIEnv *env, jbyteArray arr); +__attribute__ ((visibility ("hidden"))) jstring gio_jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len); +__attribute__ ((visibility ("hidden"))) jsize gio_jni_GetStringLength(JNIEnv *env, jstring str); +__attribute__ ((visibility ("hidden"))) const jchar *gio_jni_GetStringChars(JNIEnv *env, jstring str); +__attribute__ ((visibility ("hidden"))) jthrowable gio_jni_ExceptionOccurred(JNIEnv *env); +__attribute__ ((visibility ("hidden"))) void gio_jni_ExceptionClear(JNIEnv *env); +__attribute__ ((visibility ("hidden"))) jobject gio_jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args); +__attribute__ ((visibility ("hidden"))) jobject gio_jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args); +*/ +import "C" + +import ( + "errors" + "fmt" + "image" + "reflect" + "runtime" + "runtime/debug" + "sync" + "time" + "unicode/utf16" + "unsafe" + + "realy.lol/gio/f32" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/unit" +) + +type window struct { + callbacks Callbacks + + view C.jobject + + dpi int + fontScale float32 + insets system.Insets + + stage system.Stage + started bool + + state, newState windowState + + // mu protects the fields following it. + mu sync.Mutex + win *C.ANativeWindow + animating bool +} + +// windowState tracks the View or Activity specific state lost when Android +// re-creates our Activity. +type windowState struct { + cursor *pointer.CursorName +} + +// gioView hold cached JNI methods for GioView. +var gioView struct { + once sync.Once + getDensity C.jmethodID + getFontScale C.jmethodID + showTextInput C.jmethodID + hideTextInput C.jmethodID + postFrameCallback C.jmethodID + setCursor C.jmethodID +} + +// ViewEvent is sent whenever the Window's underlying Android view +// changes. +type ViewEvent struct { + // View is a JNI global reference to the android.view.View + // instance backing the Window. The reference is valid until + // the next ViewEvent is received. + // A zero View means that there is currently no view attached. + View uintptr +} + +type jvalue uint64 // The largest JNI type fits in 64 bits. + +var dataDirChan = make(chan string, 1) + +var android struct { + // mu protects all fields of this structure. However, once a + // non-nil jvm is returned from javaVM, all the other fields may + // be accessed unlocked. + mu sync.Mutex + jvm *C.JavaVM + + // appCtx is the global Android App context. + appCtx C.jobject + // gioCls is the class of the Gio class. + gioCls C.jclass + + mwriteClipboard C.jmethodID + mreadClipboard C.jmethodID + mwakeupMainThread C.jmethodID +} + +// view maps from GioView JNI refenreces to windows. +var views = make(map[C.jlong]*window) + +// windows maps from Callbacks to windows +var windows = make(map[Callbacks]*window) + +var mainWindow = newWindowRendezvous() + +var mainFuncs = make(chan func(env *C.JNIEnv), 1) + +func getMethodID(env *C.JNIEnv, class C.jclass, + method, sig string) C.jmethodID { + m := C.CString(method) + defer C.free(unsafe.Pointer(m)) + s := C.CString(sig) + defer C.free(unsafe.Pointer(s)) + jm := C.gio_jni_GetMethodID(env, class, m, s) + if err := exception(env); err != nil { + panic(err) + } + return jm +} + +func getStaticMethodID(env *C.JNIEnv, class C.jclass, + method, sig string) C.jmethodID { + m := C.CString(method) + defer C.free(unsafe.Pointer(m)) + s := C.CString(sig) + defer C.free(unsafe.Pointer(s)) + jm := C.gio_jni_GetStaticMethodID(env, class, m, s) + if err := exception(env); err != nil { + panic(err) + } + return jm +} + +//export Java_org_gioui_Gio_runGoMain +func Java_org_gioui_Gio_runGoMain(env *C.JNIEnv, class C.jclass, + jdataDir C.jbyteArray, context C.jobject) { + initJVM(env, class, context) + dirBytes := C.gio_jni_GetByteArrayElements(env, jdataDir) + if dirBytes == nil { + panic("runGoMain: GetByteArrayElements failed") + } + n := C.gio_jni_GetArrayLength(env, jdataDir) + dataDir := C.GoStringN((*C.char)(unsafe.Pointer(dirBytes)), n) + dataDirChan <- dataDir + C.gio_jni_ReleaseByteArrayElements(env, jdataDir, dirBytes) + + runMain() +} + +func initJVM(env *C.JNIEnv, gio C.jclass, ctx C.jobject) { + android.mu.Lock() + defer android.mu.Unlock() + if res := C.gio_jni_GetJavaVM(env, &android.jvm); res != 0 { + panic("gio: GetJavaVM failed") + } + android.appCtx = C.gio_jni_NewGlobalRef(env, ctx) + android.gioCls = C.jclass(C.gio_jni_NewGlobalRef(env, C.jobject(gio))) + android.mwriteClipboard = getStaticMethodID(env, gio, "writeClipboard", + "(Landroid/content/Context;Ljava/lang/String;)V") + android.mreadClipboard = getStaticMethodID(env, gio, "readClipboard", + "(Landroid/content/Context;)Ljava/lang/String;") + android.mwakeupMainThread = getStaticMethodID(env, gio, "wakeupMainThread", + "()V") +} + +func JavaVM() uintptr { + jvm := javaVM() + return uintptr(unsafe.Pointer(jvm)) +} + +func javaVM() *C.JavaVM { + android.mu.Lock() + defer android.mu.Unlock() + return android.jvm +} + +func AppContext() uintptr { + android.mu.Lock() + defer android.mu.Unlock() + return uintptr(android.appCtx) +} + +func GetDataDir() string { + return <-dataDirChan +} + +//export Java_org_gioui_GioView_onCreateView +func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, + view C.jobject) C.jlong { + gioView.once.Do(func() { + m := &gioView + m.getDensity = getMethodID(env, class, "getDensity", "()I") + m.getFontScale = getMethodID(env, class, "getFontScale", "()F") + m.showTextInput = getMethodID(env, class, "showTextInput", "()V") + m.hideTextInput = getMethodID(env, class, "hideTextInput", "()V") + m.postFrameCallback = getMethodID(env, class, "postFrameCallback", + "()V") + m.setCursor = getMethodID(env, class, "setCursor", "(I)V") + }) + view = C.gio_jni_NewGlobalRef(env, view) + wopts := <-mainWindow.out + w, ok := windows[wopts.window] + if !ok { + w = &window{ + callbacks: wopts.window, + } + windows[wopts.window] = w + } + w.callbacks.SetDriver(w) + w.view = view + handle := C.jlong(view) + views[handle] = w + w.loadConfig(env, class) + applyStateDiff(env, view, windowState{}, w.state) + w.setStage(system.StagePaused) + w.callbacks.Event(ViewEvent{View: uintptr(view)}) + return handle +} + +//export Java_org_gioui_GioView_onDestroyView +func Java_org_gioui_GioView_onDestroyView(env *C.JNIEnv, class C.jclass, + handle C.jlong) { + w := views[handle] + w.callbacks.Event(ViewEvent{View: 0}) + w.callbacks.SetDriver(nil) + delete(views, handle) + C.gio_jni_DeleteGlobalRef(env, w.view) + w.view = 0 +} + +//export Java_org_gioui_GioView_onStopView +func Java_org_gioui_GioView_onStopView(env *C.JNIEnv, class C.jclass, + handle C.jlong) { + w := views[handle] + w.started = false + w.setStage(system.StagePaused) +} + +//export Java_org_gioui_GioView_onStartView +func Java_org_gioui_GioView_onStartView(env *C.JNIEnv, class C.jclass, + handle C.jlong) { + w := views[handle] + w.started = true + if w.aNativeWindow() != nil { + w.setVisible() + } +} + +//export Java_org_gioui_GioView_onSurfaceDestroyed +func Java_org_gioui_GioView_onSurfaceDestroyed(env *C.JNIEnv, class C.jclass, + handle C.jlong) { + w := views[handle] + w.mu.Lock() + w.win = nil + w.mu.Unlock() + w.setStage(system.StagePaused) +} + +//export Java_org_gioui_GioView_onSurfaceChanged +func Java_org_gioui_GioView_onSurfaceChanged(env *C.JNIEnv, class C.jclass, + handle C.jlong, surf C.jobject) { + w := views[handle] + w.mu.Lock() + w.win = C.ANativeWindow_fromSurface(env, surf) + w.mu.Unlock() + if w.started { + w.setVisible() + } +} + +//export Java_org_gioui_GioView_onLowMemory +func Java_org_gioui_GioView_onLowMemory() { + runtime.GC() + debug.FreeOSMemory() +} + +//export Java_org_gioui_GioView_onConfigurationChanged +func Java_org_gioui_GioView_onConfigurationChanged(env *C.JNIEnv, + class C.jclass, view C.jlong) { + w := views[view] + w.loadConfig(env, class) + if w.stage >= system.StageRunning { + w.draw(true) + } +} + +//export Java_org_gioui_GioView_onFrameCallback +func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, + view C.jlong, nanos C.jlong) { + w, exist := views[view] + if !exist { + return + } + if w.stage < system.StageRunning { + return + } + w.mu.Lock() + anim := w.animating + w.mu.Unlock() + if anim { + runInJVM(javaVM(), func(env *C.JNIEnv) { + callVoidMethod(env, w.view, gioView.postFrameCallback) + }) + w.draw(false) + } +} + +//export Java_org_gioui_GioView_onBack +func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, + view C.jlong) C.jboolean { + w := views[view] + ev := &system.CommandEvent{Type: system.CommandBack} + w.callbacks.Event(ev) + if ev.Cancel { + return C.JNI_TRUE + } + return C.JNI_FALSE +} + +//export Java_org_gioui_GioView_onFocusChange +func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, + view C.jlong, focus C.jboolean) { + w := views[view] + w.callbacks.Event(key.FocusEvent{Focus: focus == C.JNI_TRUE}) +} + +//export Java_org_gioui_GioView_onWindowInsets +func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, + view C.jlong, top, right, bottom, left C.jint) { + w := views[view] + w.insets = system.Insets{ + Top: unit.Px(float32(top)), + Right: unit.Px(float32(right)), + Bottom: unit.Px(float32(bottom)), + Left: unit.Px(float32(left)), + } + if w.stage >= system.StageRunning { + w.draw(true) + } +} + +func (w *window) setVisible() { + win := w.aNativeWindow() + width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) + if width == 0 || height == 0 { + return + } + w.setStage(system.StageRunning) + w.draw(true) +} + +func (w *window) setStage(stage system.Stage) { + if stage == w.stage { + return + } + w.stage = stage + w.callbacks.Event(system.StageEvent{stage}) +} + +func (w *window) nativeWindow(visID int) (*C.ANativeWindow, int, int) { + win := w.aNativeWindow() + var width, height int + if win != nil { + if C.ANativeWindow_setBuffersGeometry(win, 0, 0, + C.int32_t(visID)) != 0 { + panic(errors.New("ANativeWindow_setBuffersGeometry failed")) + } + w, h := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) + width, height = int(w), int(h) + } + return win, width, height +} + +func (w *window) aNativeWindow() *C.ANativeWindow { + w.mu.Lock() + defer w.mu.Unlock() + return w.win +} + +func (w *window) loadConfig(env *C.JNIEnv, class C.jclass) { + dpi := int(C.gio_jni_CallIntMethod(env, w.view, gioView.getDensity)) + w.fontScale = float32(C.gio_jni_CallFloatMethod(env, w.view, + gioView.getFontScale)) + switch dpi { + case C.ACONFIGURATION_DENSITY_NONE, + C.ACONFIGURATION_DENSITY_DEFAULT, + C.ACONFIGURATION_DENSITY_ANY: + // Assume standard density. + w.dpi = C.ACONFIGURATION_DENSITY_MEDIUM + default: + w.dpi = int(dpi) + } +} + +func (w *window) SetAnimating(anim bool) { + w.mu.Lock() + w.animating = anim + w.mu.Unlock() + if anim { + runOnMain(func(env *C.JNIEnv) { + if w.view == 0 { + // View was destroyed while switching to main thread. + return + } + callVoidMethod(env, w.view, gioView.postFrameCallback) + }) + } +} + +func (w *window) draw(sync bool) { + win := w.aNativeWindow() + width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) + if width == 0 || height == 0 { + return + } + const inchPrDp = 1.0 / 160 + ppdp := float32(w.dpi) * inchPrDp + w.callbacks.Event(FrameEvent{ + FrameEvent: system.FrameEvent{ + Now: time.Now(), + Size: image.Point{ + X: int(width), + Y: int(height), + }, + Insets: w.insets, + Metric: unit.Metric{ + PxPerDp: ppdp, + PxPerSp: w.fontScale * ppdp, + }, + }, + Sync: sync, + }) +} + +type keyMapper func(devId, keyCode C.int32_t) rune + +func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) { + if jvm == nil { + panic("nil JVM") + } + runtime.LockOSThread() + defer runtime.UnlockOSThread() + var env *C.JNIEnv + if res := C.gio_jni_GetEnv(jvm, &env, C.JNI_VERSION_1_6); res != C.JNI_OK { + if res != C.JNI_EDETACHED { + panic(fmt.Errorf("JNI GetEnv failed with error %d", res)) + } + if C.gio_jni_AttachCurrentThread(jvm, &env, nil) != C.JNI_OK { + panic(errors.New("runInJVM: AttachCurrentThread failed")) + } + defer C.gio_jni_DetachCurrentThread(jvm) + } + + f(env) +} + +func convertKeyCode(code C.jint) (string, bool) { + var n string + switch code { + case C.AKEYCODE_DPAD_UP: + n = key.NameUpArrow + case C.AKEYCODE_DPAD_DOWN: + n = key.NameDownArrow + case C.AKEYCODE_DPAD_LEFT: + n = key.NameLeftArrow + case C.AKEYCODE_DPAD_RIGHT: + n = key.NameRightArrow + case C.AKEYCODE_FORWARD_DEL: + n = key.NameDeleteForward + case C.AKEYCODE_DEL: + n = key.NameDeleteBackward + case C.AKEYCODE_NUMPAD_ENTER: + n = key.NameEnter + case C.AKEYCODE_ENTER: + n = key.NameEnter + default: + return "", false + } + return n, true +} + +//export Java_org_gioui_GioView_onKeyEvent +func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, + handle C.jlong, keyCode, r C.jint, t C.jlong) { + w := views[handle] + if n, ok := convertKeyCode(keyCode); ok { + w.callbacks.Event(key.Event{Name: n}) + } + if r != 0 { + w.callbacks.Event(key.EditEvent{Text: string(rune(r))}) + } +} + +//export Java_org_gioui_GioView_onTouchEvent +func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, + handle C.jlong, action, pointerID, tool C.jint, + x, y, scrollX, scrollY C.jfloat, jbtns C.jint, t C.jlong) { + w := views[handle] + var typ pointer.Type + switch action { + case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN: + typ = pointer.Press + case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP: + typ = pointer.Release + case C.AMOTION_EVENT_ACTION_CANCEL: + typ = pointer.Cancel + case C.AMOTION_EVENT_ACTION_MOVE: + typ = pointer.Move + case C.AMOTION_EVENT_ACTION_SCROLL: + typ = pointer.Scroll + default: + return + } + var src pointer.Source + var btns pointer.Buttons + if jbtns&C.AMOTION_EVENT_BUTTON_PRIMARY != 0 { + btns |= pointer.ButtonPrimary + } + if jbtns&C.AMOTION_EVENT_BUTTON_SECONDARY != 0 { + btns |= pointer.ButtonSecondary + } + if jbtns&C.AMOTION_EVENT_BUTTON_TERTIARY != 0 { + btns |= pointer.ButtonTertiary + } + switch tool { + case C.AMOTION_EVENT_TOOL_TYPE_FINGER: + src = pointer.Touch + case C.AMOTION_EVENT_TOOL_TYPE_MOUSE: + src = pointer.Mouse + case C.AMOTION_EVENT_TOOL_TYPE_UNKNOWN: + // For example, triggered via 'adb shell input tap'. + // Instead of discarding it, treat it as a touch event. + src = pointer.Touch + default: + return + } + w.callbacks.Event(pointer.Event{ + Type: typ, + Source: src, + Buttons: btns, + PointerID: pointer.ID(pointerID), + Time: time.Duration(t) * time.Millisecond, + Position: f32.Point{X: float32(x), Y: float32(y)}, + Scroll: f32.Pt(float32(scrollX), float32(scrollY)), + }) +} + +func (w *window) ShowTextInput(show bool) { + runOnMain(func(env *C.JNIEnv) { + if w.view == 0 { + return + } + if show { + callVoidMethod(env, w.view, gioView.showTextInput) + } else { + callVoidMethod(env, w.view, gioView.hideTextInput) + } + }) +} + +func javaString(env *C.JNIEnv, str string) C.jstring { + if str == "" { + return 0 + } + utf16Chars := utf16.Encode([]rune(str)) + return C.gio_jni_NewString(env, (*C.jchar)(unsafe.Pointer(&utf16Chars[0])), + C.int(len(utf16Chars))) +} + +func varArgs(args []jvalue) *C.jvalue { + if len(args) == 0 { + return nil + } + return (*C.jvalue)(unsafe.Pointer(&args[0])) +} + +func callStaticVoidMethod(env *C.JNIEnv, cls C.jclass, method C.jmethodID, + args ...jvalue) error { + C.gio_jni_CallStaticVoidMethodA(env, cls, method, varArgs(args)) + return exception(env) +} + +func callStaticObjectMethod(env *C.JNIEnv, cls C.jclass, method C.jmethodID, + args ...jvalue) (C.jobject, error) { + res := C.gio_jni_CallStaticObjectMethodA(env, cls, method, varArgs(args)) + return res, exception(env) +} + +func callVoidMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID, + args ...jvalue) error { + C.gio_jni_CallVoidMethodA(env, obj, method, varArgs(args)) + return exception(env) +} + +func callObjectMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID, + args ...jvalue) (C.jobject, error) { + res := C.gio_jni_CallObjectMethodA(env, obj, method, varArgs(args)) + return res, exception(env) +} + +// exception returns an error corresponding to the pending +// exception, or nil if no exception is pending. The pending +// exception is cleared. +func exception(env *C.JNIEnv) error { + thr := C.gio_jni_ExceptionOccurred(env) + if thr == 0 { + return nil + } + C.gio_jni_ExceptionClear(env) + cls := getObjectClass(env, C.jobject(thr)) + toString := getMethodID(env, cls, "toString", "()Ljava/lang/String;") + msg, err := callObjectMethod(env, C.jobject(thr), toString) + if err != nil { + return err + } + return errors.New(goString(env, C.jstring(msg))) +} + +func getObjectClass(env *C.JNIEnv, obj C.jobject) C.jclass { + if obj == 0 { + panic("null object") + } + cls := C.gio_jni_GetObjectClass(env, C.jobject(obj)) + if err := exception(env); err != nil { + // GetObjectClass should never fail. + panic(err) + } + return cls +} + +// goString converts the JVM jstring to a Go string. +func goString(env *C.JNIEnv, str C.jstring) string { + if str == 0 { + return "" + } + strlen := C.gio_jni_GetStringLength(env, C.jstring(str)) + chars := C.gio_jni_GetStringChars(env, C.jstring(str)) + var utf16Chars []uint16 + hdr := (*reflect.SliceHeader)(unsafe.Pointer(&utf16Chars)) + hdr.Data = uintptr(unsafe.Pointer(chars)) + hdr.Cap = int(strlen) + hdr.Len = int(strlen) + utf8 := utf16.Decode(utf16Chars) + return string(utf8) +} + +func Main() { +} + +func NewWindow(window Callbacks, opts *Options) error { + mainWindow.in <- windowAndOptions{window, opts} + return <-mainWindow.errs +} + +func (w *window) WriteClipboard(s string) { + runOnMain(func(env *C.JNIEnv) { + jstr := javaString(env, s) + callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard, + jvalue(android.appCtx), jvalue(jstr)) + }) +} + +func (w *window) ReadClipboard() { + runOnMain(func(env *C.JNIEnv) { + c, err := callStaticObjectMethod(env, android.gioCls, + android.mreadClipboard, + jvalue(android.appCtx)) + if err != nil { + return + } + content := goString(env, C.jstring(c)) + w.callbacks.Event(clipboard.Event{Text: content}) + }) +} + +func (w *window) Option(opts *Options) {} + +func (w *window) SetCursor(name pointer.CursorName) { + w.setState(func(state *windowState) { + state.cursor = &name + }) +} + +// setState adjust the window state on the main thread. +func (w *window) setState(f func(state *windowState)) { + runOnMain(func(env *C.JNIEnv) { + f(&w.newState) + if w.view == 0 { + // No View attached. The state will be applied at next onCreateView. + return + } + old := w.state + state := w.newState + applyStateDiff(env, w.view, old, state) + w.state = state + }) +} + +func applyStateDiff(env *C.JNIEnv, view C.jobject, old, state windowState) { + if state.cursor != nil && old.cursor != state.cursor { + setCursor(env, view, *state.cursor) + } +} + +func setCursor(env *C.JNIEnv, view C.jobject, name pointer.CursorName) { + var curID int + switch name { + default: + fallthrough + case pointer.CursorDefault: + curID = 1000 // TYPE_ARROW + case pointer.CursorText: + curID = 1008 // TYPE_TEXT + case pointer.CursorPointer: + curID = 1002 // TYPE_HAND + case pointer.CursorCrossHair: + curID = 1007 // TYPE_CROSSHAIR + case pointer.CursorColResize: + curID = 1014 // TYPE_HORIZONTAL_DOUBLE_ARROW + case pointer.CursorRowResize: + curID = 1015 // TYPE_VERTICAL_DOUBLE_ARROW + case pointer.CursorNone: + curID = 0 // TYPE_NULL + } + callVoidMethod(env, view, gioView.setCursor, jvalue(curID)) +} + +// Close the window. Not implemented for Android. +func (w *window) Close() {} + +// runOnMain runs a function on the Java main thread. +func runOnMain(f func(env *C.JNIEnv)) { + go func() { + mainFuncs <- f + runInJVM(javaVM(), func(env *C.JNIEnv) { + callStaticVoidMethod(env, android.gioCls, android.mwakeupMainThread) + }) + }() +} + +//export Java_org_gioui_Gio_scheduleMainFuncs +func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) { + for { + select { + case f := <-mainFuncs: + f(env) + default: + return + } + } +} + +func (_ ViewEvent) ImplementsEvent() {} diff --git a/gio/giold/app/internal/wm/os_darwin.go b/gio/giold/app/internal/wm/os_darwin.go new file mode 100644 index 0000000..9bd7a17 --- /dev/null +++ b/gio/giold/app/internal/wm/os_darwin.go @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +/* +#include + +__attribute__ ((visibility ("hidden"))) void gio_wakeupMainThread(void); +__attribute__ ((visibility ("hidden"))) NSUInteger gio_nsstringLength(CFTypeRef str); +__attribute__ ((visibility ("hidden"))) void gio_nsstringGetCharacters(CFTypeRef str, unichar *chars, NSUInteger loc, NSUInteger length); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createDisplayLink(void); +__attribute__ ((visibility ("hidden"))) void gio_releaseDisplayLink(CFTypeRef dl); +__attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl); +__attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl); +__attribute__ ((visibility ("hidden"))) void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did); +__attribute__ ((visibility ("hidden"))) void gio_hideCursor(); +__attribute__ ((visibility ("hidden"))) void gio_showCursor(); +__attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID); +__attribute__ ((visibility ("hidden"))) bool gio_isMainThread(); +*/ +import "C" +import ( + "errors" + "sync" + "sync/atomic" + "time" + "unicode/utf16" + "unsafe" + + "realy.lol/gio/io/pointer" +) + +// displayLink is the state for a display link (CVDisplayLinkRef on macOS, +// CADisplayLink on iOS). It runs a state-machine goroutine that keeps the +// display link running for a while after being stopped to avoid the thread +// start/stop overhead and because the CVDisplayLink sometimes fails to +// start, stop and start again within a short duration. +type displayLink struct { + callback func() + // states is for starting or stopping the display link. + states chan bool + // done is closed when the display link is destroyed. + done chan struct{} + // dids receives the display id when the callback owner is moved + // to a different screen. + dids chan uint64 + // running tracks the desired state of the link. running is accessed + // with atomic. + running uint32 +} + +// displayLinks maps CFTypeRefs to *displayLinks. +var displayLinks sync.Map + +var mainFuncs = make(chan func(), 1) + +// runOnMain runs the function on the main thread. +func runOnMain(f func()) { + if C.gio_isMainThread() { + f() + return + } + go func() { + mainFuncs <- f + C.gio_wakeupMainThread() + }() +} + +//export gio_dispatchMainFuncs +func gio_dispatchMainFuncs() { + for { + select { + case f := <-mainFuncs: + f() + default: + return + } + } +} + +// nsstringToString converts a NSString to a Go string, and +// releases the original string. +func nsstringToString(str C.CFTypeRef) string { + if str == 0 { + return "" + } + defer C.CFRelease(str) + n := C.gio_nsstringLength(str) + if n == 0 { + return "" + } + chars := make([]uint16, n) + C.gio_nsstringGetCharacters(str, (*C.unichar)(unsafe.Pointer(&chars[0])), 0, + n) + utf8 := utf16.Decode(chars) + return string(utf8) +} + +func NewDisplayLink(callback func()) (*displayLink, error) { + d := &displayLink{ + callback: callback, + done: make(chan struct{}), + states: make(chan bool), + dids: make(chan uint64), + } + dl := C.gio_createDisplayLink() + if dl == 0 { + return nil, errors.New("app: failed to create display link") + } + go d.run(dl) + return d, nil +} + +func (d *displayLink) run(dl C.CFTypeRef) { + defer C.gio_releaseDisplayLink(dl) + displayLinks.Store(dl, d) + defer displayLinks.Delete(dl) + var stopTimer *time.Timer + var tchan <-chan time.Time + started := false + for { + select { + case <-tchan: + tchan = nil + started = false + C.gio_stopDisplayLink(dl) + case start := <-d.states: + switch { + case !start && tchan == nil: + // stopTimeout is the delay before stopping the display link to + // avoid the overhead of frequently starting and stopping the + // link thread. + const stopTimeout = 500 * time.Millisecond + if stopTimer == nil { + stopTimer = time.NewTimer(stopTimeout) + } else { + // stopTimer is always drained when tchan == nil. + stopTimer.Reset(stopTimeout) + } + tchan = stopTimer.C + atomic.StoreUint32(&d.running, 0) + case start: + if tchan != nil && !stopTimer.Stop() { + <-tchan + } + tchan = nil + atomic.StoreUint32(&d.running, 1) + if !started { + started = true + C.gio_startDisplayLink(dl) + } + } + case did := <-d.dids: + C.gio_setDisplayLinkDisplay(dl, C.uint64_t(did)) + case <-d.done: + return + } + } +} + +func (d *displayLink) Start() { + d.states <- true +} + +func (d *displayLink) Stop() { + d.states <- false +} + +func (d *displayLink) Close() { + close(d.done) +} + +func (d *displayLink) SetDisplayID(did uint64) { + d.dids <- did +} + +//export gio_onFrameCallback +func gio_onFrameCallback(dl C.CFTypeRef) { + if d, exists := displayLinks.Load(dl); exists { + d := d.(*displayLink) + if atomic.LoadUint32(&d.running) != 0 { + d.callback() + } + } +} + +// windowSetCursor updates the cursor from the current one to a new one +// and returns the new one. +func windowSetCursor(from, to pointer.CursorName) pointer.CursorName { + if from == to { + return to + } + var curID int + switch to { + default: + to = pointer.CursorDefault + fallthrough + case pointer.CursorDefault: + curID = 1 + case pointer.CursorText: + curID = 2 + case pointer.CursorPointer: + curID = 3 + case pointer.CursorCrossHair: + curID = 4 + case pointer.CursorColResize: + curID = 5 + case pointer.CursorRowResize: + curID = 6 + case pointer.CursorGrab: + curID = 7 + case pointer.CursorNone: + runOnMain(func() { + C.gio_hideCursor() + }) + return to + } + runOnMain(func() { + if from == pointer.CursorNone { + C.gio_showCursor() + } + C.gio_setCursor(C.NSUInteger(curID)) + }) + return to +} diff --git a/gio/giold/app/internal/wm/os_darwin.m b/gio/giold/app/internal/wm/os_darwin.m new file mode 100644 index 0000000..8d37371 --- /dev/null +++ b/gio/giold/app/internal/wm/os_darwin.m @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +@import Dispatch; +@import Foundation; + +#include "_cgo_export.h" + +void gio_wakeupMainThread(void) { + dispatch_async(dispatch_get_main_queue(), ^{ + gio_dispatchMainFuncs(); + }); +} + +bool gio_isMainThread() { + return [NSThread isMainThread]; +} + +NSUInteger gio_nsstringLength(CFTypeRef cstr) { + NSString *str = (__bridge NSString *)cstr; + return [str length]; +} + +void gio_nsstringGetCharacters(CFTypeRef cstr, unichar *chars, NSUInteger loc, NSUInteger length) { + NSString *str = (__bridge NSString *)cstr; + [str getCharacters:chars range:NSMakeRange(loc, length)]; +} diff --git a/gio/giold/app/internal/wm/os_ios.go b/gio/giold/app/internal/wm/os_ios.go new file mode 100644 index 0000000..62d854f --- /dev/null +++ b/gio/giold/app/internal/wm/os_ios.go @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build darwin && ios +// +build darwin,ios + +package wm + +/* +#cgo CFLAGS: -DGLES_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c + +#include +#include +#include + +struct drawParams { + CGFloat dpi, sdpi; + CGFloat width, height; + CGFloat top, right, bottom, left; +}; + +__attribute__ ((visibility ("hidden"))) void gio_showTextInput(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) void gio_hideTextInput(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) void gio_addLayerToView(CFTypeRef viewRef, CFTypeRef layerRef); +__attribute__ ((visibility ("hidden"))) void gio_updateView(CFTypeRef viewRef, CFTypeRef layerRef); +__attribute__ ((visibility ("hidden"))) void gio_removeLayer(CFTypeRef layerRef); +__attribute__ ((visibility ("hidden"))) struct drawParams gio_viewDrawParams(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_readClipboard(void); +__attribute__ ((visibility ("hidden"))) void gio_writeClipboard(unichar *chars, NSUInteger length); +*/ +import "C" + +import ( + "image" + "runtime" + "runtime/debug" + "sync/atomic" + "time" + "unicode/utf16" + "unsafe" + + "realy.lol/gio/f32" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/unit" +) + +type window struct { + view C.CFTypeRef + w Callbacks + displayLink *displayLink + + layer C.CFTypeRef + visible atomic.Value + cursor pointer.CursorName + + pointerMap []C.CFTypeRef +} + +var mainWindow = newWindowRendezvous() + +var layerFactory func() uintptr + +var views = make(map[C.CFTypeRef]*window) + +func init() { + // Darwin requires UI operations happen on the main thread only. + runtime.LockOSThread() +} + +//export onCreate +func onCreate(view C.CFTypeRef) { + w := &window{ + view: view, + } + dl, err := NewDisplayLink(func() { + w.draw(false) + }) + if err != nil { + panic(err) + } + w.displayLink = dl + wopts := <-mainWindow.out + w.w = wopts.window + w.w.SetDriver(w) + w.visible.Store(false) + w.layer = C.CFTypeRef(layerFactory()) + C.gio_addLayerToView(view, w.layer) + views[view] = w + w.w.Event(system.StageEvent{Stage: system.StagePaused}) +} + +//export gio_onDraw +func gio_onDraw(view C.CFTypeRef) { + w := views[view] + w.draw(true) +} + +func (w *window) draw(sync bool) { + params := C.gio_viewDrawParams(w.view) + if params.width == 0 || params.height == 0 { + return + } + wasVisible := w.isVisible() + w.visible.Store(true) + C.gio_updateView(w.view, w.layer) + if !wasVisible { + w.w.Event(system.StageEvent{Stage: system.StageRunning}) + } + const inchPrDp = 1.0 / 163 + w.w.Event(FrameEvent{ + FrameEvent: system.FrameEvent{ + Now: time.Now(), + Size: image.Point{ + X: int(params.width + .5), + Y: int(params.height + .5), + }, + Insets: system.Insets{ + Top: unit.Px(float32(params.top)), + Right: unit.Px(float32(params.right)), + Bottom: unit.Px(float32(params.bottom)), + Left: unit.Px(float32(params.left)), + }, + Metric: unit.Metric{ + PxPerDp: float32(params.dpi) * inchPrDp, + PxPerSp: float32(params.sdpi) * inchPrDp, + }, + }, + Sync: sync, + }) +} + +//export onStop +func onStop(view C.CFTypeRef) { + w := views[view] + w.visible.Store(false) + w.w.Event(system.StageEvent{Stage: system.StagePaused}) +} + +//export onDestroy +func onDestroy(view C.CFTypeRef) { + w := views[view] + delete(views, view) + w.w.Event(system.DestroyEvent{}) + w.displayLink.Close() + C.gio_removeLayer(w.layer) + C.CFRelease(w.layer) + w.layer = 0 + w.view = 0 +} + +//export onFocus +func onFocus(view C.CFTypeRef, focus int) { + w := views[view] + w.w.Event(key.FocusEvent{Focus: focus != 0}) +} + +//export onLowMemory +func onLowMemory() { + runtime.GC() + debug.FreeOSMemory() +} + +//export onUpArrow +func onUpArrow(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameUpArrow) +} + +//export onDownArrow +func onDownArrow(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameDownArrow) +} + +//export onLeftArrow +func onLeftArrow(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameLeftArrow) +} + +//export onRightArrow +func onRightArrow(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameRightArrow) +} + +//export onDeleteBackward +func onDeleteBackward(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameDeleteBackward) +} + +//export onText +func onText(view C.CFTypeRef, str *C.char) { + w := views[view] + w.w.Event(key.EditEvent{ + Text: C.GoString(str), + }) +} + +//export onTouch +func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, + x, y C.CGFloat, ti C.double) { + var typ pointer.Type + switch phase { + case C.UITouchPhaseBegan: + typ = pointer.Press + case C.UITouchPhaseMoved: + typ = pointer.Move + case C.UITouchPhaseEnded: + typ = pointer.Release + case C.UITouchPhaseCancelled: + typ = pointer.Cancel + default: + return + } + w := views[view] + t := time.Duration(float64(ti) * float64(time.Second)) + p := f32.Point{X: float32(x), Y: float32(y)} + w.w.Event(pointer.Event{ + Type: typ, + Source: pointer.Touch, + PointerID: w.lookupTouch(last != 0, touchRef), + Position: p, + Time: t, + }) +} + +func (w *window) ReadClipboard() { + runOnMain(func() { + content := nsstringToString(C.gio_readClipboard()) + w.w.Event(clipboard.Event{Text: content}) + }) +} + +func (w *window) WriteClipboard(s string) { + u16 := utf16.Encode([]rune(s)) + runOnMain(func() { + var chars *C.unichar + if len(u16) > 0 { + chars = (*C.unichar)(unsafe.Pointer(&u16[0])) + } + C.gio_writeClipboard(chars, C.NSUInteger(len(u16))) + }) +} + +func (w *window) Option(opts *Options) {} + +func (w *window) SetAnimating(anim bool) { + v := w.view + if v == 0 { + return + } + if anim { + w.displayLink.Start() + } else { + w.displayLink.Stop() + } +} + +func (w *window) SetCursor(name pointer.CursorName) { + w.cursor = windowSetCursor(w.cursor, name) +} + +func (w *window) onKeyCommand(name string) { + w.w.Event(key.Event{ + Name: name, + }) +} + +// lookupTouch maps an UITouch pointer value to an index. If +// last is set, the map is cleared. +func (w *window) lookupTouch(last bool, touch C.CFTypeRef) pointer.ID { + id := -1 + for i, ref := range w.pointerMap { + if ref == touch { + id = i + break + } + } + if id == -1 { + id = len(w.pointerMap) + w.pointerMap = append(w.pointerMap, touch) + } + if last { + w.pointerMap = w.pointerMap[:0] + } + return pointer.ID(id) +} + +func (w *window) contextLayer() uintptr { + return uintptr(w.layer) +} + +func (w *window) isVisible() bool { + return w.visible.Load().(bool) +} + +func (w *window) ShowTextInput(show bool) { + v := w.view + if v == 0 { + return + } + C.CFRetain(v) + runOnMain(func() { + defer C.CFRelease(v) + if show { + C.gio_showTextInput(w.view) + } else { + C.gio_hideTextInput(w.view) + } + }) +} + +// Close the window. Not implemented for iOS. +func (w *window) Close() {} + +func NewWindow(win Callbacks, opts *Options) error { + mainWindow.in <- windowAndOptions{win, opts} + return <-mainWindow.errs +} + +func Main() { +} + +//export gio_runMain +func gio_runMain() { + runMain() +} diff --git a/gio/giold/app/internal/wm/os_ios.m b/gio/giold/app/internal/wm/os_ios.m new file mode 100644 index 0000000..f1e556d --- /dev/null +++ b/gio/giold/app/internal/wm/os_ios.m @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,ios + +@import UIKit; + +#include +#include "_cgo_export.h" +#include "framework_ios.h" + +@interface GioView: UIView +@end + +@implementation GioViewController + +CGFloat _keyboardHeight; + +- (void)loadView { + gio_runMain(); + + CGRect zeroFrame = CGRectMake(0, 0, 0, 0); + self.view = [[UIView alloc] initWithFrame:zeroFrame]; + self.view.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0); + UIView *drawView = [[GioView alloc] initWithFrame:zeroFrame]; + [self.view addSubview: drawView]; +#ifndef TARGET_OS_TV + drawView.multipleTouchEnabled = YES; +#endif + drawView.preservesSuperviewLayoutMargins = YES; + drawView.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0); + onCreate((__bridge CFTypeRef)drawView); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillChange:) + name:UIKeyboardWillShowNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillChange:) + name:UIKeyboardWillChangeFrameNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillHide:) + name:UIKeyboardWillHideNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver: self + selector: @selector(applicationDidEnterBackground:) + name: UIApplicationDidEnterBackgroundNotification + object: nil]; + [[NSNotificationCenter defaultCenter] addObserver: self + selector: @selector(applicationWillEnterForeground:) + name: UIApplicationWillEnterForegroundNotification + object: nil]; +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + UIView *drawView = self.view.subviews[0]; + if (drawView != nil) { + gio_onDraw((__bridge CFTypeRef)drawView); + } +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + UIView *drawView = self.view.subviews[0]; + if (drawView != nil) { + onStop((__bridge CFTypeRef)drawView); + } +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + CFTypeRef viewRef = (__bridge CFTypeRef)self.view.subviews[0]; + onDestroy(viewRef); +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + UIView *view = self.view.subviews[0]; + CGRect frame = self.view.bounds; + // Adjust view bounds to make room for the keyboard. + frame.size.height -= _keyboardHeight; + view.frame = frame; + gio_onDraw((__bridge CFTypeRef)view); +} + +- (void)didReceiveMemoryWarning { + onLowMemory(); + [super didReceiveMemoryWarning]; +} + +- (void)keyboardWillChange:(NSNotification *)note { + NSDictionary *userInfo = note.userInfo; + CGRect f = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + _keyboardHeight = f.size.height; + [self.view setNeedsLayout]; +} + +- (void)keyboardWillHide:(NSNotification *)note { + _keyboardHeight = 0.0; + [self.view setNeedsLayout]; +} +@end + +static void handleTouches(int last, UIView *view, NSSet *touches, UIEvent *event) { + CGFloat scale = view.contentScaleFactor; + NSUInteger i = 0; + NSUInteger n = [touches count]; + CFTypeRef viewRef = (__bridge CFTypeRef)view; + for (UITouch *touch in touches) { + CFTypeRef touchRef = (__bridge CFTypeRef)touch; + i++; + NSArray *coalescedTouches = [event coalescedTouchesForTouch:touch]; + NSUInteger j = 0; + NSUInteger m = [coalescedTouches count]; + for (UITouch *coalescedTouch in [event coalescedTouchesForTouch:touch]) { + CGPoint loc = [coalescedTouch locationInView:view]; + j++; + int lastTouch = last && i == n && j == m; + onTouch(lastTouch, viewRef, touchRef, touch.phase, loc.x*scale, loc.y*scale, [coalescedTouch timestamp]); + } + } +} + +@implementation GioView +NSArray *_keyCommands; ++ (void)onFrameCallback:(CADisplayLink *)link { + gio_onFrameCallback((__bridge CFTypeRef)link); +} + +- (void)willMoveToWindow:(UIWindow *)newWindow { + if (self.window != nil) { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIWindowDidBecomeKeyNotification + object:self.window]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIWindowDidResignKeyNotification + object:self.window]; + } + self.contentScaleFactor = newWindow.screen.nativeScale; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(onWindowDidBecomeKey:) + name:UIWindowDidBecomeKeyNotification + object:newWindow]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(onWindowDidResignKey:) + name:UIWindowDidResignKeyNotification + object:newWindow]; +} + +- (void)onWindowDidBecomeKey:(NSNotification *)note { + if (self.isFirstResponder) { + onFocus((__bridge CFTypeRef)self, YES); + } +} + +- (void)onWindowDidResignKey:(NSNotification *)note { + if (self.isFirstResponder) { + onFocus((__bridge CFTypeRef)self, NO); + } +} + +- (void)dealloc { +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + handleTouches(0, self, touches, event); +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + handleTouches(0, self, touches, event); +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + handleTouches(1, self, touches, event); +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + handleTouches(1, self, touches, event); +} + +- (void)insertText:(NSString *)text { + onText((__bridge CFTypeRef)self, (char *)text.UTF8String); +} + +- (BOOL)canBecomeFirstResponder { + return YES; +} + +- (BOOL)hasText { + return YES; +} + +- (void)deleteBackward { + onDeleteBackward((__bridge CFTypeRef)self); +} + +- (void)onUpArrow { + onUpArrow((__bridge CFTypeRef)self); +} + +- (void)onDownArrow { + onDownArrow((__bridge CFTypeRef)self); +} + +- (void)onLeftArrow { + onLeftArrow((__bridge CFTypeRef)self); +} + +- (void)onRightArrow { + onRightArrow((__bridge CFTypeRef)self); +} + +- (NSArray *)keyCommands { + if (_keyCommands == nil) { + _keyCommands = @[ + [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow + modifierFlags:0 + action:@selector(onUpArrow)], + [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow + modifierFlags:0 + action:@selector(onDownArrow)], + [UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow + modifierFlags:0 + action:@selector(onLeftArrow)], + [UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow + modifierFlags:0 + action:@selector(onRightArrow)] + ]; + } + return _keyCommands; +} +@end + +void gio_writeClipboard(unichar *chars, NSUInteger length) { + @autoreleasepool { + NSString *s = [NSString string]; + if (length > 0) { + s = [NSString stringWithCharacters:chars length:length]; + } + UIPasteboard *p = UIPasteboard.generalPasteboard; + p.string = s; + } +} + +CFTypeRef gio_readClipboard(void) { + @autoreleasepool { + UIPasteboard *p = UIPasteboard.generalPasteboard; + return (__bridge_retained CFTypeRef)p.string; + } +} + +void gio_showTextInput(CFTypeRef viewRef) { + UIView *view = (__bridge UIView *)viewRef; + [view becomeFirstResponder]; +} + +void gio_hideTextInput(CFTypeRef viewRef) { + UIView *view = (__bridge UIView *)viewRef; + [view resignFirstResponder]; +} + +void gio_addLayerToView(CFTypeRef viewRef, CFTypeRef layerRef) { + UIView *view = (__bridge UIView *)viewRef; + CALayer *layer = (__bridge CALayer *)layerRef; + [view.layer addSublayer:layer]; +} + +void gio_updateView(CFTypeRef viewRef, CFTypeRef layerRef) { + UIView *view = (__bridge UIView *)viewRef; + CAEAGLLayer *layer = (__bridge CAEAGLLayer *)layerRef; + layer.contentsScale = view.contentScaleFactor; + layer.bounds = view.bounds; +} + +void gio_removeLayer(CFTypeRef layerRef) { + CALayer *layer = (__bridge CALayer *)layerRef; + [layer removeFromSuperlayer]; +} + +struct drawParams gio_viewDrawParams(CFTypeRef viewRef) { + UIView *v = (__bridge UIView *)viewRef; + struct drawParams params; + CGFloat scale = v.layer.contentsScale; + // Use 163 as the standard ppi on iOS. + params.dpi = 163*scale; + params.sdpi = params.dpi; + UIEdgeInsets insets = v.layoutMargins; + if (@available(iOS 11.0, tvOS 11.0, *)) { + UIFontMetrics *metrics = [UIFontMetrics defaultMetrics]; + params.sdpi = [metrics scaledValueForValue:params.sdpi]; + insets = v.safeAreaInsets; + } + params.width = v.bounds.size.width*scale; + params.height = v.bounds.size.height*scale; + params.top = insets.top*scale; + params.right = insets.right*scale; + params.bottom = insets.bottom*scale; + params.left = insets.left*scale; + return params; +} + +CFTypeRef gio_createDisplayLink(void) { + CADisplayLink *dl = [CADisplayLink displayLinkWithTarget:[GioView class] selector:@selector(onFrameCallback:)]; + dl.paused = YES; + NSRunLoop *runLoop = [NSRunLoop mainRunLoop]; + [dl addToRunLoop:runLoop forMode:[runLoop currentMode]]; + return (__bridge_retained CFTypeRef)dl; +} + +int gio_startDisplayLink(CFTypeRef dlref) { + CADisplayLink *dl = (__bridge CADisplayLink *)dlref; + dl.paused = NO; + return 0; +} + +int gio_stopDisplayLink(CFTypeRef dlref) { + CADisplayLink *dl = (__bridge CADisplayLink *)dlref; + dl.paused = YES; + return 0; +} + +void gio_releaseDisplayLink(CFTypeRef dlref) { + CADisplayLink *dl = (__bridge CADisplayLink *)dlref; + [dl invalidate]; + CFRelease(dlref); +} + +void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did) { + // Nothing to do on iOS. +} + +void gio_hideCursor() { + // Not supported. +} + +void gio_showCursor() { + // Not supported. +} + +void gio_setCursor(NSUInteger curID) { + // Not supported. +} diff --git a/gio/giold/app/internal/wm/os_js.go b/gio/giold/app/internal/wm/os_js.go new file mode 100644 index 0000000..f30f7bc --- /dev/null +++ b/gio/giold/app/internal/wm/os_js.go @@ -0,0 +1,656 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +import ( + "image" + "strings" + "sync" + "syscall/js" + "time" + "unicode" + "unicode/utf8" + + "realy.lol/gio/f32" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/unit" +) + +type window struct { + window js.Value + document js.Value + clipboard js.Value + cnv js.Value + tarea js.Value + w Callbacks + redraw js.Func + clipboardCallback js.Func + requestAnimationFrame js.Value + browserHistory js.Value + visualViewport js.Value + cleanfuncs []func() + touches []js.Value + composing bool + requestFocus bool + + chanAnimation chan struct{} + chanRedraw chan struct{} + + mu sync.Mutex + size f32.Point + inset f32.Point + scale float32 + animating bool + // animRequested tracks whether a requestAnimationFrame callback + // is pending. + animRequested bool +} + +func NewWindow(win Callbacks, opts *Options) error { + doc := js.Global().Get("document") + cont := getContainer(doc) + cnv := createCanvas(doc) + cont.Call("appendChild", cnv) + tarea := createTextArea(doc) + cont.Call("appendChild", tarea) + w := &window{ + cnv: cnv, + document: doc, + tarea: tarea, + window: js.Global().Get("window"), + clipboard: js.Global().Get("navigator").Get("clipboard"), + } + w.requestAnimationFrame = w.window.Get("requestAnimationFrame") + w.browserHistory = w.window.Get("history") + w.visualViewport = w.window.Get("visualViewport") + if w.visualViewport.IsUndefined() { + w.visualViewport = w.window + } + w.chanAnimation = make(chan struct{}, 1) + w.chanRedraw = make(chan struct{}, 1) + w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} { + w.chanAnimation <- struct{}{} + return nil + }) + w.clipboardCallback = w.funcOf(func(this js.Value, + args []js.Value) interface{} { + content := args[0].String() + win.Event(clipboard.Event{Text: content}) + return nil + }) + w.addEventListeners() + w.addHistory() + w.Option(opts) + w.w = win + + go func() { + defer w.cleanup() + w.w.SetDriver(w) + w.blur() + w.w.Event(system.StageEvent{Stage: system.StageRunning}) + w.resize() + w.draw(true) + for { + select { + case <-w.chanAnimation: + w.animCallback() + case <-w.chanRedraw: + w.draw(true) + } + } + }() + return nil +} + +func getContainer(doc js.Value) js.Value { + cont := doc.Call("getElementById", "giowindow") + if !cont.IsNull() { + return cont + } + cont = doc.Call("createElement", "DIV") + doc.Get("body").Call("appendChild", cont) + return cont +} + +func createTextArea(doc js.Value) js.Value { + tarea := doc.Call("createElement", "input") + style := tarea.Get("style") + style.Set("width", "1px") + style.Set("height", "1px") + style.Set("opacity", "0") + style.Set("border", "0") + style.Set("padding", "0") + tarea.Set("autocomplete", "off") + tarea.Set("autocorrect", "off") + tarea.Set("autocapitalize", "off") + tarea.Set("spellcheck", false) + return tarea +} + +func createCanvas(doc js.Value) js.Value { + cnv := doc.Call("createElement", "canvas") + style := cnv.Get("style") + style.Set("position", "fixed") + style.Set("width", "100%") + style.Set("height", "100%") + return cnv +} + +func (w *window) cleanup() { + // Cleanup in the opposite order of + // construction. + for i := len(w.cleanfuncs) - 1; i >= 0; i-- { + w.cleanfuncs[i]() + } + w.cleanfuncs = nil +} + +func (w *window) addEventListeners() { + w.addEventListener(w.visualViewport, "resize", + func(this js.Value, args []js.Value) interface{} { + w.resize() + w.chanRedraw <- struct{}{} + return nil + }) + w.addEventListener(w.window, "contextmenu", + func(this js.Value, args []js.Value) interface{} { + args[0].Call("preventDefault") + return nil + }) + w.addEventListener(w.window, "popstate", + func(this js.Value, args []js.Value) interface{} { + ev := &system.CommandEvent{Type: system.CommandBack} + w.w.Event(ev) + if ev.Cancel { + return w.browserHistory.Call("forward") + } + + return w.browserHistory.Call("back") + }) + w.addEventListener(w.document, "visibilitychange", + func(this js.Value, args []js.Value) interface{} { + ev := system.StageEvent{} + switch w.document.Get("visibilityState").String() { + case "hidden", "prerender", "unloaded": + ev.Stage = system.StagePaused + default: + ev.Stage = system.StageRunning + } + w.w.Event(ev) + return nil + }) + w.addEventListener(w.cnv, "mousemove", + func(this js.Value, args []js.Value) interface{} { + w.pointerEvent(pointer.Move, 0, 0, args[0]) + return nil + }) + w.addEventListener(w.cnv, "mousedown", + func(this js.Value, args []js.Value) interface{} { + w.pointerEvent(pointer.Press, 0, 0, args[0]) + if w.requestFocus { + w.focus() + w.requestFocus = false + } + return nil + }) + w.addEventListener(w.cnv, "mouseup", + func(this js.Value, args []js.Value) interface{} { + w.pointerEvent(pointer.Release, 0, 0, args[0]) + return nil + }) + w.addEventListener(w.cnv, "wheel", + func(this js.Value, args []js.Value) interface{} { + e := args[0] + dx, dy := e.Get("deltaX").Float(), e.Get("deltaY").Float() + mode := e.Get("deltaMode").Int() + switch mode { + case 0x01: // DOM_DELTA_LINE + dx *= 10 + dy *= 10 + case 0x02: // DOM_DELTA_PAGE + dx *= 120 + dy *= 120 + } + w.pointerEvent(pointer.Scroll, float32(dx), float32(dy), e) + return nil + }) + w.addEventListener(w.cnv, "touchstart", + func(this js.Value, args []js.Value) interface{} { + w.touchEvent(pointer.Press, args[0]) + if w.requestFocus { + w.focus() // iOS can only focus inside a Touch event. + w.requestFocus = false + } + return nil + }) + w.addEventListener(w.cnv, "touchend", + func(this js.Value, args []js.Value) interface{} { + w.touchEvent(pointer.Release, args[0]) + return nil + }) + w.addEventListener(w.cnv, "touchmove", + func(this js.Value, args []js.Value) interface{} { + w.touchEvent(pointer.Move, args[0]) + return nil + }) + w.addEventListener(w.cnv, "touchcancel", + func(this js.Value, args []js.Value) interface{} { + // Cancel all touches even if only one touch was cancelled. + for i := range w.touches { + w.touches[i] = js.Null() + } + w.touches = w.touches[:0] + w.w.Event(pointer.Event{ + Type: pointer.Cancel, + Source: pointer.Touch, + }) + return nil + }) + w.addEventListener(w.tarea, "focus", + func(this js.Value, args []js.Value) interface{} { + w.w.Event(key.FocusEvent{Focus: true}) + return nil + }) + w.addEventListener(w.tarea, "blur", + func(this js.Value, args []js.Value) interface{} { + w.w.Event(key.FocusEvent{Focus: false}) + w.blur() + return nil + }) + w.addEventListener(w.tarea, "keydown", + func(this js.Value, args []js.Value) interface{} { + w.keyEvent(args[0], key.Press) + return nil + }) + w.addEventListener(w.tarea, "keyup", + func(this js.Value, args []js.Value) interface{} { + w.keyEvent(args[0], key.Release) + return nil + }) + w.addEventListener(w.tarea, "compositionstart", + func(this js.Value, args []js.Value) interface{} { + w.composing = true + return nil + }) + w.addEventListener(w.tarea, "compositionend", + func(this js.Value, args []js.Value) interface{} { + w.composing = false + w.flushInput() + return nil + }) + w.addEventListener(w.tarea, "input", + func(this js.Value, args []js.Value) interface{} { + if w.composing { + return nil + } + w.flushInput() + return nil + }) + w.addEventListener(w.tarea, "paste", + func(this js.Value, args []js.Value) interface{} { + if w.clipboard.IsUndefined() { + return nil + } + // Prevents duplicated-paste, since "paste" is already handled through Clipboard API. + args[0].Call("preventDefault") + return nil + }) +} + +func (w *window) addHistory() { + w.browserHistory.Call("pushState", nil, nil, + w.window.Get("location").Get("href")) +} + +func (w *window) flushInput() { + val := w.tarea.Get("value").String() + w.tarea.Set("value", "") + w.w.Event(key.EditEvent{Text: string(val)}) +} + +func (w *window) blur() { + w.tarea.Call("blur") + w.requestFocus = false +} + +func (w *window) focus() { + w.tarea.Call("focus") + w.requestFocus = true +} + +func (w *window) keyEvent(e js.Value, ks key.State) { + k := e.Get("key").String() + if n, ok := translateKey(k); ok { + cmd := key.Event{ + Name: n, + Modifiers: modifiersFor(e), + State: ks, + } + w.w.Event(cmd) + } +} + +// modifiersFor returns the modifier set for a DOM MouseEvent or +// KeyEvent. +func modifiersFor(e js.Value) key.Modifiers { + var mods key.Modifiers + if e.Get("getModifierState").IsUndefined() { + // Some browsers doesn't support getModifierState. + return mods + } + if e.Call("getModifierState", "Alt").Bool() { + mods |= key.ModAlt + } + if e.Call("getModifierState", "Control").Bool() { + mods |= key.ModCtrl + } + if e.Call("getModifierState", "Shift").Bool() { + mods |= key.ModShift + } + return mods +} + +func (w *window) touchEvent(typ pointer.Type, e js.Value) { + e.Call("preventDefault") + t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond + changedTouches := e.Get("changedTouches") + n := changedTouches.Length() + rect := w.cnv.Call("getBoundingClientRect") + w.mu.Lock() + scale := w.scale + w.mu.Unlock() + var mods key.Modifiers + if e.Get("shiftKey").Bool() { + mods |= key.ModShift + } + if e.Get("altKey").Bool() { + mods |= key.ModAlt + } + if e.Get("ctrlKey").Bool() { + mods |= key.ModCtrl + } + for i := 0; i < n; i++ { + touch := changedTouches.Index(i) + pid := w.touchIDFor(touch) + x, y := touch.Get("clientX").Float(), touch.Get("clientY").Float() + x -= rect.Get("left").Float() + y -= rect.Get("top").Float() + pos := f32.Point{ + X: float32(x) * scale, + Y: float32(y) * scale, + } + w.w.Event(pointer.Event{ + Type: typ, + Source: pointer.Touch, + Position: pos, + PointerID: pid, + Time: t, + Modifiers: mods, + }) + } +} + +func (w *window) touchIDFor(touch js.Value) pointer.ID { + id := touch.Get("identifier") + for i, id2 := range w.touches { + if id2.Equal(id) { + return pointer.ID(i) + } + } + pid := pointer.ID(len(w.touches)) + w.touches = append(w.touches, id) + return pid +} + +func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) { + e.Call("preventDefault") + x, y := e.Get("clientX").Float(), e.Get("clientY").Float() + rect := w.cnv.Call("getBoundingClientRect") + x -= rect.Get("left").Float() + y -= rect.Get("top").Float() + w.mu.Lock() + scale := w.scale + w.mu.Unlock() + pos := f32.Point{ + X: float32(x) * scale, + Y: float32(y) * scale, + } + scroll := f32.Point{ + X: dx * scale, + Y: dy * scale, + } + t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond + jbtns := e.Get("buttons").Int() + var btns pointer.Buttons + if jbtns&1 != 0 { + btns |= pointer.ButtonPrimary + } + if jbtns&2 != 0 { + btns |= pointer.ButtonSecondary + } + if jbtns&4 != 0 { + btns |= pointer.ButtonTertiary + } + w.w.Event(pointer.Event{ + Type: typ, + Source: pointer.Mouse, + Buttons: btns, + Position: pos, + Scroll: scroll, + Time: t, + Modifiers: modifiersFor(e), + }) +} + +func (w *window) addEventListener(this js.Value, event string, + f func(this js.Value, args []js.Value) interface{}) { + jsf := w.funcOf(f) + this.Call("addEventListener", event, jsf) + w.cleanfuncs = append(w.cleanfuncs, func() { + this.Call("removeEventListener", event, jsf) + }) +} + +// funcOf is like js.FuncOf but adds the js.Func to a list of +// functions to be released during cleanup. +func (w *window) funcOf(f func(this js.Value, + args []js.Value) interface{}) js.Func { + jsf := js.FuncOf(f) + w.cleanfuncs = append(w.cleanfuncs, jsf.Release) + return jsf +} + +func (w *window) animCallback() { + w.mu.Lock() + anim := w.animating + w.animRequested = anim + if anim { + w.requestAnimationFrame.Invoke(w.redraw) + } + w.mu.Unlock() + if anim { + w.draw(false) + } +} + +func (w *window) SetAnimating(anim bool) { + w.mu.Lock() + defer w.mu.Unlock() + w.animating = anim + if anim && !w.animRequested { + w.animRequested = true + w.requestAnimationFrame.Invoke(w.redraw) + } +} + +func (w *window) ReadClipboard() { + if w.clipboard.IsUndefined() { + return + } + if w.clipboard.Get("readText").IsUndefined() { + return + } + w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback) +} + +func (w *window) WriteClipboard(s string) { + if w.clipboard.IsUndefined() { + return + } + if w.clipboard.Get("writeText").IsUndefined() { + return + } + w.clipboard.Call("writeText", s) +} + +func (w *window) Option(opts *Options) { + if o := opts.WindowMode; o != nil { + w.windowMode(*o) + } +} + +func (w *window) SetCursor(name pointer.CursorName) { + style := w.cnv.Get("style") + style.Set("cursor", string(name)) +} + +func (w *window) ShowTextInput(show bool) { + // Run in a goroutine to avoid a deadlock if the + // focus change result in an event. + go func() { + if show { + w.focus() + } else { + w.blur() + } + }() +} + +// Close the window. Not implemented for js. +func (w *window) Close() {} + +func (w *window) resize() { + w.mu.Lock() + defer w.mu.Unlock() + + w.scale = float32(w.window.Get("devicePixelRatio").Float()) + + rect := w.cnv.Call("getBoundingClientRect") + w.size.X = float32(rect.Get("width").Float()) * w.scale + w.size.Y = float32(rect.Get("height").Float()) * w.scale + + if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() { + w.inset.X = w.size.X - float32(vx.Float())*w.scale + w.inset.Y = w.size.Y - float32(vy.Float())*w.scale + } + + if w.size.X == 0 || w.size.Y == 0 { + return + } + + w.cnv.Set("width", int(w.size.X+.5)) + w.cnv.Set("height", int(w.size.Y+.5)) +} + +func (w *window) draw(sync bool) { + width, height, insets, metric := w.config() + if metric == (unit.Metric{}) || width == 0 || height == 0 { + return + } + + w.w.Event(FrameEvent{ + FrameEvent: system.FrameEvent{ + Now: time.Now(), + Size: image.Point{ + X: width, + Y: height, + }, + Insets: insets, + Metric: metric, + }, + Sync: sync, + }) +} + +func (w *window) config() (int, int, system.Insets, unit.Metric) { + w.mu.Lock() + defer w.mu.Unlock() + + return int(w.size.X + .5), int(w.size.Y + .5), system.Insets{ + Bottom: unit.Px(w.inset.Y), + Right: unit.Px(w.inset.X), + }, unit.Metric{ + PxPerDp: w.scale, + PxPerSp: w.scale, + } +} + +func (w *window) windowMode(mode WindowMode) { + switch mode { + case Windowed: + if fs := w.document.Get("fullscreenElement"); !fs.Truthy() { + return // Browser is already Windowed. + } + if !w.document.Get("exitFullscreen").Truthy() { + return // Browser doesn't support such feature. + } + w.document.Call("exitFullscreen") + case Fullscreen: + elem := w.document.Get("documentElement") + if !elem.Get("requestFullscreen").Truthy() { + return // Browser doesn't support such feature. + } + elem.Call("requestFullscreen") + } +} + +func Main() { + select {} +} + +func translateKey(k string) (string, bool) { + var n string + switch k { + case "ArrowUp": + n = key.NameUpArrow + case "ArrowDown": + n = key.NameDownArrow + case "ArrowLeft": + n = key.NameLeftArrow + case "ArrowRight": + n = key.NameRightArrow + case "Escape": + n = key.NameEscape + case "Enter": + n = key.NameReturn + case "Backspace": + n = key.NameDeleteBackward + case "Delete": + n = key.NameDeleteForward + case "Home": + n = key.NameHome + case "End": + n = key.NameEnd + case "PageUp": + n = key.NamePageUp + case "PageDown": + n = key.NamePageDown + case "Tab": + n = key.NameTab + case " ": + n = key.NameSpace + case "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12": + n = k + default: + r, s := utf8.DecodeRuneInString(k) + // If there is exactly one printable character, return that. + if s == len(k) && unicode.IsPrint(r) { + return strings.ToUpper(k), true + } + return "", false + } + return n, true +} diff --git a/gio/giold/app/internal/wm/os_macos.go b/gio/giold/app/internal/wm/os_macos.go new file mode 100644 index 0000000..f93a5b8 --- /dev/null +++ b/gio/giold/app/internal/wm/os_macos.go @@ -0,0 +1,516 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build darwin && !ios +// +build darwin,!ios + +package wm + +import ( + "errors" + "image" + "runtime" + "time" + "unicode" + "unicode/utf16" + "unsafe" + + "realy.lol/gio/f32" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/unit" + + _ "realy.lol/gio/internal/cocoainit" +) + +/* +#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c + +#include + +#define GIO_MOUSE_MOVE 1 +#define GIO_MOUSE_UP 2 +#define GIO_MOUSE_DOWN 3 +#define GIO_MOUSE_SCROLL 4 + +__attribute__ ((visibility ("hidden"))) void gio_main(void); +__attribute__ ((visibility ("hidden"))) CGFloat gio_viewWidth(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) CGFloat gio_viewHeight(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) CGFloat gio_getViewBackingScale(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) CGFloat gio_getScreenBackingScale(void); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_readClipboard(void); +__attribute__ ((visibility ("hidden"))) void gio_writeClipboard(unichar *chars, NSUInteger length); +__attribute__ ((visibility ("hidden"))) void gio_setNeedsDisplay(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) void gio_toggleFullScreen(CFTypeRef windowRef); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight); +__attribute__ ((visibility ("hidden"))) void gio_makeKeyAndOrderFront(CFTypeRef windowRef); +__attribute__ ((visibility ("hidden"))) NSPoint gio_cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft); +__attribute__ ((visibility ("hidden"))) void gio_close(CFTypeRef windowRef); +__attribute__ ((visibility ("hidden"))) void gio_setSize(CFTypeRef windowRef, CGFloat width, CGFloat height); +__attribute__ ((visibility ("hidden"))) void gio_setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height); +__attribute__ ((visibility ("hidden"))) void gio_setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height); +__attribute__ ((visibility ("hidden"))) void gio_setTitle(CFTypeRef windowRef, const char *title); +*/ +import "C" + +func init() { + // Darwin requires that UI operations happen on the main thread only. + runtime.LockOSThread() +} + +type window struct { + view C.CFTypeRef + window C.CFTypeRef + w Callbacks + stage system.Stage + displayLink *displayLink + cursor pointer.CursorName + + scale float32 + mode WindowMode +} + +// viewMap is the mapping from Cocoa NSViews to Go windows. +var viewMap = make(map[C.CFTypeRef]*window) + +var viewFactory func() C.CFTypeRef + +// launched is closed when applicationDidFinishLaunching is called. +var launched = make(chan struct{}) + +// nextTopLeft is the offset to use for the next window's call to +// cascadeTopLeftFromPoint. +var nextTopLeft C.NSPoint + +// mustView is like lookupView, except that it panics +// if the view isn't mapped. +func mustView(view C.CFTypeRef) *window { + w, ok := lookupView(view) + if !ok { + panic("no window for view") + } + return w +} + +func lookupView(view C.CFTypeRef) (*window, bool) { + w, exists := viewMap[view] + if !exists { + return nil, false + } + return w, true +} + +func deleteView(view C.CFTypeRef) { + delete(viewMap, view) +} + +func insertView(view C.CFTypeRef, w *window) { + viewMap[view] = w +} + +func (w *window) contextView() C.CFTypeRef { + return w.view +} + +func (w *window) ReadClipboard() { + runOnMain(func() { + content := nsstringToString(C.gio_readClipboard()) + w.w.Event(clipboard.Event{Text: content}) + }) +} + +func (w *window) WriteClipboard(s string) { + u16 := utf16.Encode([]rune(s)) + runOnMain(func() { + var chars *C.unichar + if len(u16) > 0 { + chars = (*C.unichar)(unsafe.Pointer(&u16[0])) + } + C.gio_writeClipboard(chars, C.NSUInteger(len(u16))) + }) +} + +func (w *window) Option(opts *Options) { + w.runOnMain(func() { + screenScale := float32(C.gio_getScreenBackingScale()) + cfg := configFor(screenScale) + val := func(v unit.Value) float32 { + return float32(cfg.Px(v)) / screenScale + } + if o := opts.Size; o != nil { + width := val(o.Width) + height := val(o.Height) + if width > 0 || height > 0 { + C.gio_setSize(w.window, C.CGFloat(width), C.CGFloat(height)) + } + } + if o := opts.MinSize; o != nil { + width := val(o.Width) + height := val(o.Height) + if width > 0 || height > 0 { + C.gio_setMinSize(w.window, C.CGFloat(width), C.CGFloat(height)) + } + } + if o := opts.MaxSize; o != nil { + width := val(o.Width) + height := val(o.Height) + if width > 0 || height > 0 { + C.gio_setMaxSize(w.window, C.CGFloat(width), C.CGFloat(height)) + } + } + if o := opts.Title; o != nil { + title := C.CString(*o) + defer C.free(unsafe.Pointer(title)) + C.gio_setTitle(w.window, title) + } + if o := opts.WindowMode; o != nil { + w.SetWindowMode(*o) + } + }) +} + +func (w *window) SetWindowMode(mode WindowMode) { + switch mode { + case w.mode: + case Windowed, Fullscreen: + C.gio_toggleFullScreen(w.window) + w.mode = mode + } +} + +func (w *window) SetCursor(name pointer.CursorName) { + w.cursor = windowSetCursor(w.cursor, name) +} + +func (w *window) ShowTextInput(show bool) {} + +func (w *window) SetAnimating(anim bool) { + if anim { + w.displayLink.Start() + } else { + w.displayLink.Stop() + } +} + +func (w *window) runOnMain(f func()) { + runOnMain(func() { + // Make sure the view is still valid. The window might've been closed + // during the switch to the main thread. + if w.view != 0 { + f() + } + }) +} + +func (w *window) Close() { + w.runOnMain(func() { + C.gio_close(w.window) + }) +} + +func (w *window) setStage(stage system.Stage) { + if stage == w.stage { + return + } + w.stage = stage + w.w.Event(system.StageEvent{Stage: stage}) +} + +//export gio_onKeys +func gio_onKeys(view C.CFTypeRef, cstr *C.char, ti C.double, mods C.NSUInteger, + keyDown C.bool) { + str := C.GoString(cstr) + kmods := convertMods(mods) + ks := key.Release + if keyDown { + ks = key.Press + } + w := mustView(view) + for _, k := range str { + if n, ok := convertKey(k); ok { + w.w.Event(key.Event{ + Name: n, + Modifiers: kmods, + State: ks, + }) + } + } +} + +//export gio_onText +func gio_onText(view C.CFTypeRef, cstr *C.char) { + str := C.GoString(cstr) + w := mustView(view) + w.w.Event(key.EditEvent{Text: str}) +} + +//export gio_onMouse +func gio_onMouse(view C.CFTypeRef, cdir C.int, cbtns C.NSUInteger, + x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) { + var typ pointer.Type + switch cdir { + case C.GIO_MOUSE_MOVE: + typ = pointer.Move + case C.GIO_MOUSE_UP: + typ = pointer.Release + case C.GIO_MOUSE_DOWN: + typ = pointer.Press + case C.GIO_MOUSE_SCROLL: + typ = pointer.Scroll + default: + panic("invalid direction") + } + var btns pointer.Buttons + if cbtns&(1<<0) != 0 { + btns |= pointer.ButtonPrimary + } + if cbtns&(1<<1) != 0 { + btns |= pointer.ButtonSecondary + } + if cbtns&(1<<2) != 0 { + btns |= pointer.ButtonTertiary + } + t := time.Duration(float64(ti)*float64(time.Second) + .5) + w := mustView(view) + xf, yf := float32(x)*w.scale, float32(y)*w.scale + dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale + w.w.Event(pointer.Event{ + Type: typ, + Source: pointer.Mouse, + Time: t, + Buttons: btns, + Position: f32.Point{X: xf, Y: yf}, + Scroll: f32.Point{X: dxf, Y: dyf}, + Modifiers: convertMods(mods), + }) +} + +//export gio_onDraw +func gio_onDraw(view C.CFTypeRef) { + w := mustView(view) + w.draw() +} + +//export gio_onFocus +func gio_onFocus(view C.CFTypeRef, focus C.int) { + w := mustView(view) + w.w.Event(key.FocusEvent{Focus: focus == 1}) + w.SetCursor(w.cursor) +} + +//export gio_onChangeScreen +func gio_onChangeScreen(view C.CFTypeRef, did uint64) { + w := mustView(view) + w.displayLink.SetDisplayID(did) +} + +func (w *window) draw() { + w.scale = float32(C.gio_getViewBackingScale(w.view)) + wf, hf := float32(C.gio_viewWidth(w.view)), float32(C.gio_viewHeight(w.view)) + if wf == 0 || hf == 0 { + return + } + width := int(wf*w.scale + .5) + height := int(hf*w.scale + .5) + cfg := configFor(w.scale) + w.setStage(system.StageRunning) + w.w.Event(FrameEvent{ + FrameEvent: system.FrameEvent{ + Now: time.Now(), + Size: image.Point{ + X: width, + Y: height, + }, + Metric: cfg, + }, + Sync: true, + }) +} + +func configFor(scale float32) unit.Metric { + return unit.Metric{ + PxPerDp: scale, + PxPerSp: scale, + } +} + +//export gio_onClose +func gio_onClose(view C.CFTypeRef) { + w := mustView(view) + w.displayLink.Close() + deleteView(view) + w.w.Event(system.DestroyEvent{}) + C.CFRelease(w.view) + w.view = 0 + C.CFRelease(w.window) + w.window = 0 +} + +//export gio_onHide +func gio_onHide(view C.CFTypeRef) { + w := mustView(view) + w.setStage(system.StagePaused) +} + +//export gio_onShow +func gio_onShow(view C.CFTypeRef) { + w := mustView(view) + w.setStage(system.StageRunning) +} + +//export gio_onAppHide +func gio_onAppHide() { + for _, w := range viewMap { + w.setStage(system.StagePaused) + } +} + +//export gio_onAppShow +func gio_onAppShow() { + for _, w := range viewMap { + w.setStage(system.StageRunning) + } +} + +//export gio_onFinishLaunching +func gio_onFinishLaunching() { + close(launched) +} + +func NewWindow(win Callbacks, opts *Options) error { + <-launched + errch := make(chan error) + runOnMain(func() { + w, err := newWindow(opts) + if err != nil { + errch <- err + return + } + errch <- nil + win.SetDriver(w) + w.w = win + w.window = C.gio_createWindow(w.view, nil, 0, 0, 0, 0, 0, 0) + w.Option(opts) + if nextTopLeft.x == 0 && nextTopLeft.y == 0 { + // cascadeTopLeftFromPoint treats (0, 0) as a no-op, + // and just returns the offset we need for the first window. + nextTopLeft = C.gio_cascadeTopLeftFromPoint(w.window, nextTopLeft) + } + nextTopLeft = C.gio_cascadeTopLeftFromPoint(w.window, nextTopLeft) + C.gio_makeKeyAndOrderFront(w.window) + }) + return <-errch +} + +func newWindow(opts *Options) (*window, error) { + view := viewFactory() + if view == 0 { + return nil, errors.New("CreateWindow: failed to create view") + } + scale := float32(C.gio_getViewBackingScale(view)) + w := &window{ + view: view, + scale: scale, + } + dl, err := NewDisplayLink(func() { + w.runOnMain(func() { + C.gio_setNeedsDisplay(w.view) + }) + }) + w.displayLink = dl + if err != nil { + C.CFRelease(view) + return nil, err + } + insertView(view, w) + return w, nil +} + +func Main() { + C.gio_main() +} + +func convertKey(k rune) (string, bool) { + var n string + switch k { + case 0x1b: + n = key.NameEscape + case C.NSLeftArrowFunctionKey: + n = key.NameLeftArrow + case C.NSRightArrowFunctionKey: + n = key.NameRightArrow + case C.NSUpArrowFunctionKey: + n = key.NameUpArrow + case C.NSDownArrowFunctionKey: + n = key.NameDownArrow + case 0xd: + n = key.NameReturn + case 0x3: + n = key.NameEnter + case C.NSHomeFunctionKey: + n = key.NameHome + case C.NSEndFunctionKey: + n = key.NameEnd + case 0x7f: + n = key.NameDeleteBackward + case C.NSDeleteFunctionKey: + n = key.NameDeleteForward + case C.NSPageUpFunctionKey: + n = key.NamePageUp + case C.NSPageDownFunctionKey: + n = key.NamePageDown + case C.NSF1FunctionKey: + n = "F1" + case C.NSF2FunctionKey: + n = "F2" + case C.NSF3FunctionKey: + n = "F3" + case C.NSF4FunctionKey: + n = "F4" + case C.NSF5FunctionKey: + n = "F5" + case C.NSF6FunctionKey: + n = "F6" + case C.NSF7FunctionKey: + n = "F7" + case C.NSF8FunctionKey: + n = "F8" + case C.NSF9FunctionKey: + n = "F9" + case C.NSF10FunctionKey: + n = "F10" + case C.NSF11FunctionKey: + n = "F11" + case C.NSF12FunctionKey: + n = "F12" + case 0x09, 0x19: + n = key.NameTab + case 0x20: + n = key.NameSpace + default: + k = unicode.ToUpper(k) + if !unicode.IsPrint(k) { + return "", false + } + n = string(k) + } + return n, true +} + +func convertMods(mods C.NSUInteger) key.Modifiers { + var kmods key.Modifiers + if mods&C.NSAlternateKeyMask != 0 { + kmods |= key.ModAlt + } + if mods&C.NSControlKeyMask != 0 { + kmods |= key.ModCtrl + } + if mods&C.NSCommandKeyMask != 0 { + kmods |= key.ModCommand + } + if mods&C.NSShiftKeyMask != 0 { + kmods |= key.ModShift + } + return kmods +} diff --git a/gio/giold/app/internal/wm/os_macos.m b/gio/giold/app/internal/wm/os_macos.m new file mode 100644 index 0000000..7980d53 --- /dev/null +++ b/gio/giold/app/internal/wm/os_macos.m @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,!ios + +@import AppKit; + +#include "_cgo_export.h" + +@interface GioAppDelegate : NSObject +@end + +@interface GioWindowDelegate : NSObject +@end + +@implementation GioWindowDelegate +- (void)windowWillMiniaturize:(NSNotification *)notification { + NSWindow *window = (NSWindow *)[notification object]; + gio_onHide((__bridge CFTypeRef)window.contentView); +} +- (void)windowDidDeminiaturize:(NSNotification *)notification { + NSWindow *window = (NSWindow *)[notification object]; + gio_onShow((__bridge CFTypeRef)window.contentView); +} +- (void)windowDidChangeScreen:(NSNotification *)notification { + NSWindow *window = (NSWindow *)[notification object]; + CGDirectDisplayID dispID = [[[window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue]; + CFTypeRef view = (__bridge CFTypeRef)window.contentView; + gio_onChangeScreen(view, dispID); +} +- (void)windowDidBecomeKey:(NSNotification *)notification { + NSWindow *window = (NSWindow *)[notification object]; + gio_onFocus((__bridge CFTypeRef)window.contentView, 1); +} +- (void)windowDidResignKey:(NSNotification *)notification { + NSWindow *window = (NSWindow *)[notification object]; + gio_onFocus((__bridge CFTypeRef)window.contentView, 0); +} +- (void)windowWillClose:(NSNotification *)notification { + NSWindow *window = (NSWindow *)[notification object]; + window.delegate = nil; + gio_onClose((__bridge CFTypeRef)window.contentView); +} +@end + +// Delegates are weakly referenced from their peers. Nothing +// else holds a strong reference to our window delegate, so +// keep a single global reference instead. +static GioWindowDelegate *globalWindowDel; + +void gio_writeClipboard(unichar *chars, NSUInteger length) { + @autoreleasepool { + NSString *s = [NSString string]; + if (length > 0) { + s = [NSString stringWithCharacters:chars length:length]; + } + NSPasteboard *p = NSPasteboard.generalPasteboard; + [p declareTypes:@[NSPasteboardTypeString] owner:nil]; + [p setString:s forType:NSPasteboardTypeString]; + } +} + +CFTypeRef gio_readClipboard(void) { + @autoreleasepool { + NSPasteboard *p = NSPasteboard.generalPasteboard; + NSString *content = [p stringForType:NSPasteboardTypeString]; + return (__bridge_retained CFTypeRef)content; + } +} + +CGFloat gio_viewHeight(CFTypeRef viewRef) { + NSView *view = (__bridge NSView *)viewRef; + return [view bounds].size.height; +} + +CGFloat gio_viewWidth(CFTypeRef viewRef) { + NSView *view = (__bridge NSView *)viewRef; + return [view bounds].size.width; +} + +CGFloat gio_getScreenBackingScale(void) { + return [NSScreen.mainScreen backingScaleFactor]; +} + +CGFloat gio_getViewBackingScale(CFTypeRef viewRef) { + NSView *view = (__bridge NSView *)viewRef; + return [view.window backingScaleFactor]; +} + +void gio_hideCursor() { + @autoreleasepool { + [NSCursor hide]; + } +} + +void gio_showCursor() { + @autoreleasepool { + [NSCursor unhide]; + } +} + +void gio_setCursor(NSUInteger curID) { + @autoreleasepool { + switch (curID) { + case 1: + [NSCursor.arrowCursor set]; + break; + case 2: + [NSCursor.IBeamCursor set]; + break; + case 3: + [NSCursor.pointingHandCursor set]; + break; + case 4: + [NSCursor.crosshairCursor set]; + break; + case 5: + [NSCursor.resizeLeftRightCursor set]; + break; + case 6: + [NSCursor.resizeUpDownCursor set]; + break; + case 7: + [NSCursor.openHandCursor set]; + break; + default: + [NSCursor.arrowCursor set]; + break; + } + } +} + +static CVReturn displayLinkCallback(CVDisplayLinkRef dl, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) { + gio_onFrameCallback(dl); + return kCVReturnSuccess; +} + +CFTypeRef gio_createDisplayLink(void) { + CVDisplayLinkRef dl; + CVDisplayLinkCreateWithActiveCGDisplays(&dl); + CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, nil); + return dl; +} + +int gio_startDisplayLink(CFTypeRef dl) { + return CVDisplayLinkStart((CVDisplayLinkRef)dl); +} + +int gio_stopDisplayLink(CFTypeRef dl) { + return CVDisplayLinkStop((CVDisplayLinkRef)dl); +} + +void gio_releaseDisplayLink(CFTypeRef dl) { + CVDisplayLinkRelease((CVDisplayLinkRef)dl); +} + +void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did) { + CVDisplayLinkSetCurrentCGDisplay((CVDisplayLinkRef)dl, (CGDirectDisplayID)did); +} + +NSPoint gio_cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft) { + NSWindow *window = (__bridge NSWindow *)windowRef; + return [window cascadeTopLeftFromPoint:topLeft]; +} + +void gio_makeKeyAndOrderFront(CFTypeRef windowRef) { + NSWindow *window = (__bridge NSWindow *)windowRef; + [window makeKeyAndOrderFront:nil]; +} + +void gio_toggleFullScreen(CFTypeRef windowRef) { + NSWindow *window = (__bridge NSWindow *)windowRef; + [window toggleFullScreen:nil]; +} + +CFTypeRef gio_createWindow(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight) { + @autoreleasepool { + NSRect rect = NSMakeRect(0, 0, width, height); + NSUInteger styleMask = NSTitledWindowMask | + NSResizableWindowMask | + NSMiniaturizableWindowMask | + NSClosableWindowMask; + + NSWindow* window = [[NSWindow alloc] initWithContentRect:rect + styleMask:styleMask + backing:NSBackingStoreBuffered + defer:NO]; + if (minWidth > 0 || minHeight > 0) { + window.contentMinSize = NSMakeSize(minWidth, minHeight); + } + if (maxWidth > 0 || maxHeight > 0) { + window.contentMaxSize = NSMakeSize(maxWidth, maxHeight); + } + [window setAcceptsMouseMovedEvents:YES]; + if (title != nil) { + window.title = [NSString stringWithUTF8String: title]; + } + NSView *view = (__bridge NSView *)viewRef; + [window setContentView:view]; + [window makeFirstResponder:view]; + window.releasedWhenClosed = NO; + window.delegate = globalWindowDel; + return (__bridge_retained CFTypeRef)window; + } +} + +void gio_close(CFTypeRef windowRef) { + NSWindow* window = (__bridge NSWindow *)windowRef; + [window performClose:nil]; +} + +void gio_setSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { + NSWindow* window = (__bridge NSWindow *)windowRef; + NSSize size = NSMakeSize(width, height); + [window setContentSize:size]; +} + +void gio_setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { + NSWindow* window = (__bridge NSWindow *)windowRef; + window.contentMinSize = NSMakeSize(width, height); +} + +void gio_setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { + NSWindow* window = (__bridge NSWindow *)windowRef; + window.contentMaxSize = NSMakeSize(width, height); +} + +void gio_setTitle(CFTypeRef windowRef, const char *title) { + NSWindow* window = (__bridge NSWindow *)windowRef; + window.title = [NSString stringWithUTF8String: title]; +} + +@implementation GioAppDelegate +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; + gio_onFinishLaunching(); +} +- (void)applicationDidHide:(NSNotification *)aNotification { + gio_onAppHide(); +} +- (void)applicationWillUnhide:(NSNotification *)notification { + gio_onAppShow(); +} +@end + +void gio_main() { + @autoreleasepool { + [NSApplication sharedApplication]; + GioAppDelegate *del = [[GioAppDelegate alloc] init]; + [NSApp setDelegate:del]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + + NSMenuItem *mainMenu = [NSMenuItem new]; + + NSMenu *menu = [NSMenu new]; + NSMenuItem *hideMenuItem = [[NSMenuItem alloc] initWithTitle:@"Hide" + action:@selector(hide:) + keyEquivalent:@"h"]; + [menu addItem:hideMenuItem]; + NSMenuItem *quitMenuItem = [[NSMenuItem alloc] initWithTitle:@"Quit" + action:@selector(terminate:) + keyEquivalent:@"q"]; + [menu addItem:quitMenuItem]; + [mainMenu setSubmenu:menu]; + NSMenu *menuBar = [NSMenu new]; + [menuBar addItem:mainMenu]; + [NSApp setMainMenu:menuBar]; + + globalWindowDel = [[GioWindowDelegate alloc] init]; + + [NSApp run]; + } +} diff --git a/gio/giold/app/internal/wm/os_unix.go b/gio/giold/app/internal/wm/os_unix.go new file mode 100644 index 0000000..8143100 --- /dev/null +++ b/gio/giold/app/internal/wm/os_unix.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build linux,!android freebsd openbsd + +package wm + +import ( + "errors" +) + +func Main() { + select {} +} + +type windowDriver func(Callbacks, *Options) error + +// Instead of creating files with build tags for each combination of wayland +/- x11 +// let each driver initialize these variables with their own version of createWindow. +var wlDriver, x11Driver windowDriver + +func NewWindow(window Callbacks, opts *Options) error { + var errFirst error + for _, d := range []windowDriver{x11Driver, wlDriver} { + if d == nil { + continue + } + err := d(window, opts) + if err == nil { + return nil + } + if errFirst == nil { + errFirst = err + } + } + if errFirst != nil { + return errFirst + } + return errors.New("app: no window driver available") +} diff --git a/gio/giold/app/internal/wm/os_wayland.c b/gio/giold/app/internal/wm/os_wayland.c new file mode 100644 index 0000000..5c1c075 --- /dev/null +++ b/gio/giold/app/internal/wm/os_wayland.c @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build linux,!android,!nowayland freebsd + +#include +#include "wayland_xdg_shell.h" +#include "wayland_text_input.h" +#include "_cgo_export.h" + +const struct wl_registry_listener gio_registry_listener = { + // Cast away const parameter. + .global = (void (*)(void *, struct wl_registry *, uint32_t, const char *, uint32_t))gio_onRegistryGlobal, + .global_remove = gio_onRegistryGlobalRemove +}; + +const struct wl_surface_listener gio_surface_listener = { + .enter = gio_onSurfaceEnter, + .leave = gio_onSurfaceLeave, +}; + +const struct xdg_surface_listener gio_xdg_surface_listener = { + .configure = gio_onXdgSurfaceConfigure, +}; + +const struct xdg_toplevel_listener gio_xdg_toplevel_listener = { + .configure = gio_onToplevelConfigure, + .close = gio_onToplevelClose, +}; + +static void xdg_wm_base_handle_ping(void *data, struct xdg_wm_base *wm, uint32_t serial) { + xdg_wm_base_pong(wm, serial); +} + +const struct xdg_wm_base_listener gio_xdg_wm_base_listener = { + .ping = xdg_wm_base_handle_ping, +}; + +const struct wl_callback_listener gio_callback_listener = { + .done = gio_onFrameDone, +}; + +const struct wl_output_listener gio_output_listener = { + // Cast away const parameter. + .geometry = (void (*)(void *, struct wl_output *, int32_t, int32_t, int32_t, int32_t, int32_t, const char *, const char *, int32_t))gio_onOutputGeometry, + .mode = gio_onOutputMode, + .done = gio_onOutputDone, + .scale = gio_onOutputScale, +}; + +const struct wl_seat_listener gio_seat_listener = { + .capabilities = gio_onSeatCapabilities, + // Cast away const parameter. + .name = (void (*)(void *, struct wl_seat *, const char *))gio_onSeatName, +}; + +const struct wl_pointer_listener gio_pointer_listener = { + .enter = gio_onPointerEnter, + .leave = gio_onPointerLeave, + .motion = gio_onPointerMotion, + .button = gio_onPointerButton, + .axis = gio_onPointerAxis, + .frame = gio_onPointerFrame, + .axis_source = gio_onPointerAxisSource, + .axis_stop = gio_onPointerAxisStop, + .axis_discrete = gio_onPointerAxisDiscrete, +}; + +const struct wl_touch_listener gio_touch_listener = { + .down = gio_onTouchDown, + .up = gio_onTouchUp, + .motion = gio_onTouchMotion, + .frame = gio_onTouchFrame, + .cancel = gio_onTouchCancel, +}; + +const struct wl_keyboard_listener gio_keyboard_listener = { + .keymap = gio_onKeyboardKeymap, + .enter = gio_onKeyboardEnter, + .leave = gio_onKeyboardLeave, + .key = gio_onKeyboardKey, + .modifiers = gio_onKeyboardModifiers, + .repeat_info = gio_onKeyboardRepeatInfo +}; + +const struct zwp_text_input_v3_listener gio_zwp_text_input_v3_listener = { + .enter = gio_onTextInputEnter, + .leave = gio_onTextInputLeave, + // Cast away const parameter. + .preedit_string = (void (*)(void *, struct zwp_text_input_v3 *, const char *, int32_t, int32_t))gio_onTextInputPreeditString, + .commit_string = (void (*)(void *, struct zwp_text_input_v3 *, const char *))gio_onTextInputCommitString, + .delete_surrounding_text = gio_onTextInputDeleteSurroundingText, + .done = gio_onTextInputDone +}; + +const struct wl_data_device_listener gio_data_device_listener = { + .data_offer = gio_onDataDeviceOffer, + .enter = gio_onDataDeviceEnter, + .leave = gio_onDataDeviceLeave, + .motion = gio_onDataDeviceMotion, + .drop = gio_onDataDeviceDrop, + .selection = gio_onDataDeviceSelection, +}; + +const struct wl_data_offer_listener gio_data_offer_listener = { + .offer = (void (*)(void *, struct wl_data_offer *, const char *))gio_onDataOfferOffer, + .source_actions = gio_onDataOfferSourceActions, + .action = gio_onDataOfferAction, +}; + +const struct wl_data_source_listener gio_data_source_listener = { + .target = (void (*)(void *, struct wl_data_source *, const char *))gio_onDataSourceTarget, + .send = (void (*)(void *, struct wl_data_source *, const char *, int32_t))gio_onDataSourceSend, + .cancelled = gio_onDataSourceCancelled, + .dnd_drop_performed = gio_onDataSourceDNDDropPerformed, + .dnd_finished = gio_onDataSourceDNDFinished, + .action = gio_onDataSourceAction, +}; diff --git a/gio/giold/app/internal/wm/os_wayland.go b/gio/giold/app/internal/wm/os_wayland.go new file mode 100644 index 0000000..f59bc1e --- /dev/null +++ b/gio/giold/app/internal/wm/os_wayland.go @@ -0,0 +1,1694 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build (linux && !android && !nowayland) || freebsd +// +build linux,!android,!nowayland freebsd + +package wm + +import ( + "bytes" + "errors" + "fmt" + "image" + "io" + "io/ioutil" + "math" + "os" + "os/exec" + "strconv" + "sync" + "time" + "unsafe" + + syscall "golang.org/x/sys/unix" + + "realy.lol/gio/app/internal/xkb" + "realy.lol/gio/f32" + "realy.lol/gio/internal/fling" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/unit" +) + +// Use wayland-scanner to generate glue code for the xdg-shell and xdg-decoration extensions. +//go:generate wayland-scanner client-header /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml wayland_xdg_shell.h +//go:generate wayland-scanner private-code /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml wayland_xdg_shell.c + +//go:generate wayland-scanner client-header /usr/share/wayland-protocols/unstable/text-input/text-input-unstable-v3.xml wayland_text_input.h +//go:generate wayland-scanner private-code /usr/share/wayland-protocols/unstable/text-input/text-input-unstable-v3.xml wayland_text_input.c + +//go:generate wayland-scanner client-header /usr/share/wayland-protocols/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml wayland_xdg_decoration.h +//go:generate wayland-scanner private-code /usr/share/wayland-protocols/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml wayland_xdg_decoration.c + +//go:generate sed -i "1s;^;// +build linux,!android,!nowayland freebsd\\n\\n;" wayland_xdg_shell.c +//go:generate sed -i "1s;^;// +build linux,!android,!nowayland freebsd\\n\\n;" wayland_xdg_decoration.c +//go:generate sed -i "1s;^;// +build linux,!android,!nowayland freebsd\\n\\n;" wayland_text_input.c + +/* +#cgo linux pkg-config: wayland-client wayland-cursor +#cgo freebsd openbsd LDFLAGS: -lwayland-client -lwayland-cursor +#cgo freebsd CFLAGS: -I/usr/local/include +#cgo freebsd LDFLAGS: -L/usr/local/lib + +#include +#include +#include +#include "wayland_text_input.h" +#include "wayland_xdg_shell.h" +#include "wayland_xdg_decoration.h" + +extern const struct wl_registry_listener gio_registry_listener; +extern const struct wl_surface_listener gio_surface_listener; +extern const struct xdg_surface_listener gio_xdg_surface_listener; +extern const struct xdg_toplevel_listener gio_xdg_toplevel_listener; +extern const struct xdg_wm_base_listener gio_xdg_wm_base_listener; +extern const struct wl_callback_listener gio_callback_listener; +extern const struct wl_output_listener gio_output_listener; +extern const struct wl_seat_listener gio_seat_listener; +extern const struct wl_pointer_listener gio_pointer_listener; +extern const struct wl_touch_listener gio_touch_listener; +extern const struct wl_keyboard_listener gio_keyboard_listener; +extern const struct zwp_text_input_v3_listener gio_zwp_text_input_v3_listener; +extern const struct wl_data_device_listener gio_data_device_listener; +extern const struct wl_data_offer_listener gio_data_offer_listener; +extern const struct wl_data_source_listener gio_data_source_listener; +*/ +import "C" + +type wlDisplay struct { + disp *C.struct_wl_display + reg *C.struct_wl_registry + compositor *C.struct_wl_compositor + wm *C.struct_xdg_wm_base + imm *C.struct_zwp_text_input_manager_v3 + shm *C.struct_wl_shm + dataDeviceManager *C.struct_wl_data_device_manager + decor *C.struct_zxdg_decoration_manager_v1 + seat *wlSeat + xkb *xkb.Context + outputMap map[C.uint32_t]*C.struct_wl_output + outputConfig map[*C.struct_wl_output]*wlOutput + + // Notification pipe fds. + notify struct { + read, write int + } + + repeat repeatState +} + +type wlSeat struct { + disp *wlDisplay + seat *C.struct_wl_seat + name C.uint32_t + pointer *C.struct_wl_pointer + touch *C.struct_wl_touch + keyboard *C.struct_wl_keyboard + im *C.struct_zwp_text_input_v3 + + // The most recent input serial. + serial C.uint32_t + + pointerFocus *window + keyboardFocus *window + touchFoci map[C.int32_t]*window + + // Clipboard support. + dataDev *C.struct_wl_data_device + // offers is a map from active wl_data_offers to + // the list of mime types they support. + offers map[*C.struct_wl_data_offer][]string + // clipboard is the wl_data_offer for the clipboard. + clipboard *C.struct_wl_data_offer + // mimeType is the chosen mime type of clipboard. + mimeType string + // source represents the clipboard content of the most recent + // clipboard write, if any. + source *C.struct_wl_data_source + // content is the data belonging to source. + content []byte +} + +type repeatState struct { + rate int + delay time.Duration + + key uint32 + win Callbacks + stopC chan struct{} + + start time.Duration + last time.Duration + mu sync.Mutex + now time.Duration +} + +type window struct { + w Callbacks + disp *wlDisplay + surf *C.struct_wl_surface + wmSurf *C.struct_xdg_surface + topLvl *C.struct_xdg_toplevel + decor *C.struct_zxdg_toplevel_decoration_v1 + ppdp, ppsp float32 + scroll struct { + time time.Duration + steps image.Point + dist f32.Point + } + pointerBtns pointer.Buttons + lastPos f32.Point + lastTouch f32.Point + + cursor struct { + theme *C.struct_wl_cursor_theme + cursor *C.struct_wl_cursor + surf *C.struct_wl_surface + } + + fling struct { + yExtrapolation fling.Extrapolation + xExtrapolation fling.Extrapolation + anim fling.Animation + start bool + dir f32.Point + } + + stage system.Stage + dead bool + lastFrameCallback *C.struct_wl_callback + + mu sync.Mutex + animating bool + opts *Options + needAck bool + // The most recent configure serial waiting to be ack'ed. + serial C.uint32_t + width int + height int + newScale bool + scale int + // readClipboard tracks whether a ClipboardEvent is requested. + readClipboard bool + // writeClipboard is set whenever a clipboard write is requested. + writeClipboard *string +} + +type poller struct { + pollfds [2]syscall.PollFd + // buf is scratch space for draining the notification pipe. + buf [100]byte +} + +type wlOutput struct { + width int + height int + physWidth int + physHeight int + transform C.int32_t + scale int + windows []*window +} + +// callbackMap maps Wayland native handles to corresponding Go +// references. It is necessary because the the Wayland client API +// forces the use of callbacks and storing pointers to Go values +// in C is forbidden. +var callbackMap sync.Map + +// clipboardMimeTypes is a list of supported clipboard mime types, in +// order of preference. +var clipboardMimeTypes = []string{"text/plain;charset=utf8", "UTF8_STRING", + "text/plain", "TEXT", "STRING"} + +func init() { + wlDriver = newWLWindow +} + +func newWLWindow(window Callbacks, opts *Options) error { + d, err := newWLDisplay() + if err != nil { + return err + } + w, err := d.createNativeWindow(opts) + if err != nil { + d.destroy() + return err + } + w.w = window + go func() { + defer d.destroy() + defer w.destroy() + w.w.SetDriver(w) + if err := w.loop(); err != nil { + panic(err) + } + }() + return nil +} + +func (d *wlDisplay) writeClipboard(content []byte) error { + s := d.seat + if s == nil { + return nil + } + // Clear old offer. + if s.source != nil { + C.wl_data_source_destroy(s.source) + s.source = nil + s.content = nil + } + if d.dataDeviceManager == nil || s.dataDev == nil { + return nil + } + s.content = content + s.source = C.wl_data_device_manager_create_data_source(d.dataDeviceManager) + C.wl_data_source_add_listener(s.source, &C.gio_data_source_listener, + unsafe.Pointer(s.seat)) + for _, mime := range clipboardMimeTypes { + C.wl_data_source_offer(s.source, C.CString(mime)) + } + C.wl_data_device_set_selection(s.dataDev, s.source, s.serial) + return nil +} + +func (d *wlDisplay) readClipboard() (io.ReadCloser, error) { + s := d.seat + if s == nil { + return nil, nil + } + if s.clipboard == nil { + return nil, nil + } + r, w, err := os.Pipe() + if err != nil { + return nil, err + } + // wl_data_offer_receive performs and implicit dup(2) of the write end + // of the pipe. Close our version. + defer w.Close() + cmimeType := C.CString(s.mimeType) + defer C.free(unsafe.Pointer(cmimeType)) + C.wl_data_offer_receive(s.clipboard, cmimeType, C.int(w.Fd())) + return r, nil +} + +func (d *wlDisplay) createNativeWindow(opts *Options) (*window, error) { + if d.compositor == nil { + return nil, errors.New("wayland: no compositor available") + } + if d.wm == nil { + return nil, errors.New("wayland: no xdg_wm_base available") + } + if d.shm == nil { + return nil, errors.New("wayland: no wl_shm available") + } + if len(d.outputMap) == 0 { + return nil, errors.New("wayland: no outputs available") + } + var scale int + for _, conf := range d.outputConfig { + if s := conf.scale; s > scale { + scale = s + } + } + ppdp := detectUIScale() + + w := &window{ + disp: d, + scale: scale, + newScale: scale != 1, + ppdp: ppdp, + ppsp: ppdp, + } + w.surf = C.wl_compositor_create_surface(d.compositor) + if w.surf == nil { + w.destroy() + return nil, errors.New("wayland: wl_compositor_create_surface failed") + } + callbackStore(unsafe.Pointer(w.surf), w) + w.wmSurf = C.xdg_wm_base_get_xdg_surface(d.wm, w.surf) + if w.wmSurf == nil { + w.destroy() + return nil, errors.New("wayland: xdg_wm_base_get_xdg_surface failed") + } + w.topLvl = C.xdg_surface_get_toplevel(w.wmSurf) + if w.topLvl == nil { + w.destroy() + return nil, errors.New("wayland: xdg_surface_get_toplevel failed") + } + w.cursor.theme = C.wl_cursor_theme_load(nil, 32, d.shm) + if w.cursor.theme == nil { + w.destroy() + return nil, errors.New("wayland: wl_cursor_theme_load failed") + } + cname := C.CString("left_ptr") + defer C.free(unsafe.Pointer(cname)) + w.cursor.cursor = C.wl_cursor_theme_get_cursor(w.cursor.theme, cname) + if w.cursor.cursor == nil { + w.destroy() + return nil, errors.New("wayland: wl_cursor_theme_get_cursor failed") + } + w.cursor.surf = C.wl_compositor_create_surface(d.compositor) + if w.cursor.surf == nil { + w.destroy() + return nil, errors.New("wayland: wl_compositor_create_surface failed") + } + C.xdg_wm_base_add_listener(d.wm, &C.gio_xdg_wm_base_listener, + unsafe.Pointer(w.surf)) + C.wl_surface_add_listener(w.surf, &C.gio_surface_listener, + unsafe.Pointer(w.surf)) + C.xdg_surface_add_listener(w.wmSurf, &C.gio_xdg_surface_listener, + unsafe.Pointer(w.surf)) + C.xdg_toplevel_add_listener(w.topLvl, &C.gio_xdg_toplevel_listener, + unsafe.Pointer(w.surf)) + + w.setOptions(opts) + + if d.decor != nil { + // Request server side decorations. + w.decor = C.zxdg_decoration_manager_v1_get_toplevel_decoration(d.decor, + w.topLvl) + C.zxdg_toplevel_decoration_v1_set_mode(w.decor, + C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE) + } + w.updateOpaqueRegion() + C.wl_surface_commit(w.surf) + return w, nil +} + +func callbackDelete(k unsafe.Pointer) { + callbackMap.Delete(k) +} + +func callbackStore(k unsafe.Pointer, v interface{}) { + callbackMap.Store(k, v) +} + +func callbackLoad(k unsafe.Pointer) interface{} { + v, exists := callbackMap.Load(k) + if !exists { + panic("missing callback entry") + } + return v +} + +//export gio_onSeatCapabilities +func gio_onSeatCapabilities(data unsafe.Pointer, seat *C.struct_wl_seat, + caps C.uint32_t) { + s := callbackLoad(data).(*wlSeat) + s.updateCaps(caps) +} + +// flushOffers remove all wl_data_offers that isn't the clipboard +// content. +func (s *wlSeat) flushOffers() { + for o := range s.offers { + if o == s.clipboard { + continue + } + // We're only interested in clipboard offers. + delete(s.offers, o) + callbackDelete(unsafe.Pointer(o)) + C.wl_data_offer_destroy(o) + } +} + +func (s *wlSeat) destroy() { + if s.source != nil { + C.wl_data_source_destroy(s.source) + s.source = nil + } + if s.im != nil { + C.zwp_text_input_v3_destroy(s.im) + s.im = nil + } + if s.pointer != nil { + C.wl_pointer_release(s.pointer) + } + if s.touch != nil { + C.wl_touch_release(s.touch) + } + if s.keyboard != nil { + C.wl_keyboard_release(s.keyboard) + } + s.clipboard = nil + s.flushOffers() + if s.dataDev != nil { + C.wl_data_device_release(s.dataDev) + } + if s.seat != nil { + callbackDelete(unsafe.Pointer(s.seat)) + C.wl_seat_release(s.seat) + } +} + +func (s *wlSeat) updateCaps(caps C.uint32_t) { + if s.im == nil && s.disp.imm != nil { + s.im = C.zwp_text_input_manager_v3_get_text_input(s.disp.imm, s.seat) + C.zwp_text_input_v3_add_listener(s.im, + &C.gio_zwp_text_input_v3_listener, unsafe.Pointer(s.seat)) + } + switch { + case s.pointer == nil && caps&C.WL_SEAT_CAPABILITY_POINTER != 0: + s.pointer = C.wl_seat_get_pointer(s.seat) + C.wl_pointer_add_listener(s.pointer, &C.gio_pointer_listener, + unsafe.Pointer(s.seat)) + case s.pointer != nil && caps&C.WL_SEAT_CAPABILITY_POINTER == 0: + C.wl_pointer_release(s.pointer) + s.pointer = nil + } + switch { + case s.touch == nil && caps&C.WL_SEAT_CAPABILITY_TOUCH != 0: + s.touch = C.wl_seat_get_touch(s.seat) + C.wl_touch_add_listener(s.touch, &C.gio_touch_listener, + unsafe.Pointer(s.seat)) + case s.touch != nil && caps&C.WL_SEAT_CAPABILITY_TOUCH == 0: + C.wl_touch_release(s.touch) + s.touch = nil + } + switch { + case s.keyboard == nil && caps&C.WL_SEAT_CAPABILITY_KEYBOARD != 0: + s.keyboard = C.wl_seat_get_keyboard(s.seat) + C.wl_keyboard_add_listener(s.keyboard, &C.gio_keyboard_listener, + unsafe.Pointer(s.seat)) + case s.keyboard != nil && caps&C.WL_SEAT_CAPABILITY_KEYBOARD == 0: + C.wl_keyboard_release(s.keyboard) + s.keyboard = nil + } +} + +//export gio_onSeatName +func gio_onSeatName(data unsafe.Pointer, seat *C.struct_wl_seat, name *C.char) { +} + +//export gio_onXdgSurfaceConfigure +func gio_onXdgSurfaceConfigure(data unsafe.Pointer, + wmSurf *C.struct_xdg_surface, serial C.uint32_t) { + w := callbackLoad(data).(*window) + w.mu.Lock() + w.serial = serial + w.needAck = true + w.mu.Unlock() + w.setStage(system.StageRunning) + w.draw(true) +} + +//export gio_onToplevelClose +func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) { + w := callbackLoad(data).(*window) + w.dead = true +} + +//export gio_onToplevelConfigure +func gio_onToplevelConfigure(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel, + width, height C.int32_t, states *C.struct_wl_array) { + w := callbackLoad(data).(*window) + if width != 0 && height != 0 { + w.mu.Lock() + defer w.mu.Unlock() + w.width = int(width) + w.height = int(height) + w.updateOpaqueRegion() + } +} + +//export gio_onOutputMode +func gio_onOutputMode(data unsafe.Pointer, output *C.struct_wl_output, + flags C.uint32_t, width, height, refresh C.int32_t) { + if flags&C.WL_OUTPUT_MODE_CURRENT == 0 { + return + } + d := callbackLoad(data).(*wlDisplay) + c := d.outputConfig[output] + c.width = int(width) + c.height = int(height) +} + +//export gio_onOutputGeometry +func gio_onOutputGeometry(data unsafe.Pointer, output *C.struct_wl_output, + x, y, physWidth, physHeight, subpixel C.int32_t, make, model *C.char, + transform C.int32_t) { + d := callbackLoad(data).(*wlDisplay) + c := d.outputConfig[output] + c.transform = transform + c.physWidth = int(physWidth) + c.physHeight = int(physHeight) +} + +//export gio_onOutputScale +func gio_onOutputScale(data unsafe.Pointer, output *C.struct_wl_output, + scale C.int32_t) { + d := callbackLoad(data).(*wlDisplay) + c := d.outputConfig[output] + c.scale = int(scale) +} + +//export gio_onOutputDone +func gio_onOutputDone(data unsafe.Pointer, output *C.struct_wl_output) { + d := callbackLoad(data).(*wlDisplay) + conf := d.outputConfig[output] + for _, w := range conf.windows { + w.draw(true) + } +} + +//export gio_onSurfaceEnter +func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, + output *C.struct_wl_output) { + w := callbackLoad(data).(*window) + conf := w.disp.outputConfig[output] + var found bool + for _, w2 := range conf.windows { + if w2 == w { + found = true + break + } + } + if !found { + conf.windows = append(conf.windows, w) + } + w.updateOutputs() +} + +//export gio_onSurfaceLeave +func gio_onSurfaceLeave(data unsafe.Pointer, surf *C.struct_wl_surface, + output *C.struct_wl_output) { + w := callbackLoad(data).(*window) + conf := w.disp.outputConfig[output] + for i, w2 := range conf.windows { + if w2 == w { + conf.windows = append(conf.windows[:i], conf.windows[i+1:]...) + break + } + } + w.updateOutputs() +} + +//export gio_onRegistryGlobal +func gio_onRegistryGlobal(data unsafe.Pointer, reg *C.struct_wl_registry, + name C.uint32_t, cintf *C.char, version C.uint32_t) { + d := callbackLoad(data).(*wlDisplay) + switch C.GoString(cintf) { + case "wl_compositor": + d.compositor = (*C.struct_wl_compositor)(C.wl_registry_bind(reg, name, + &C.wl_compositor_interface, 3)) + case "wl_output": + output := (*C.struct_wl_output)(C.wl_registry_bind(reg, name, + &C.wl_output_interface, 2)) + C.wl_output_add_listener(output, &C.gio_output_listener, + unsafe.Pointer(d.disp)) + d.outputMap[name] = output + d.outputConfig[output] = new(wlOutput) + case "wl_seat": + if d.seat != nil { + break + } + s := (*C.struct_wl_seat)(C.wl_registry_bind(reg, name, + &C.wl_seat_interface, 5)) + if s == nil { + // No support for v5 protocol. + break + } + d.seat = &wlSeat{ + disp: d, + name: name, + seat: s, + offers: make(map[*C.struct_wl_data_offer][]string), + touchFoci: make(map[C.int32_t]*window), + } + callbackStore(unsafe.Pointer(s), d.seat) + C.wl_seat_add_listener(s, &C.gio_seat_listener, unsafe.Pointer(s)) + if d.dataDeviceManager == nil { + break + } + d.seat.dataDev = C.wl_data_device_manager_get_data_device(d.dataDeviceManager, + s) + if d.seat.dataDev == nil { + break + } + callbackStore(unsafe.Pointer(d.seat.dataDev), d.seat) + C.wl_data_device_add_listener(d.seat.dataDev, + &C.gio_data_device_listener, unsafe.Pointer(d.seat.dataDev)) + case "wl_shm": + d.shm = (*C.struct_wl_shm)(C.wl_registry_bind(reg, name, + &C.wl_shm_interface, 1)) + case "xdg_wm_base": + d.wm = (*C.struct_xdg_wm_base)(C.wl_registry_bind(reg, name, + &C.xdg_wm_base_interface, 1)) + case "zxdg_decoration_manager_v1": + d.decor = (*C.struct_zxdg_decoration_manager_v1)(C.wl_registry_bind(reg, + name, &C.zxdg_decoration_manager_v1_interface, 1)) + // TODO: Implement and test text-input support. + /*case "zwp_text_input_manager_v3": + d.imm = (*C.struct_zwp_text_input_manager_v3)(C.wl_registry_bind(reg, name, &C.zwp_text_input_manager_v3_interface, 1))*/ + case "wl_data_device_manager": + d.dataDeviceManager = (*C.struct_wl_data_device_manager)(C.wl_registry_bind(reg, + name, &C.wl_data_device_manager_interface, 3)) + } +} + +//export gio_onDataOfferOffer +func gio_onDataOfferOffer(data unsafe.Pointer, offer *C.struct_wl_data_offer, + mime *C.char) { + s := callbackLoad(data).(*wlSeat) + s.offers[offer] = append(s.offers[offer], C.GoString(mime)) +} + +//export gio_onDataOfferSourceActions +func gio_onDataOfferSourceActions(data unsafe.Pointer, + offer *C.struct_wl_data_offer, acts C.uint32_t) { +} + +//export gio_onDataOfferAction +func gio_onDataOfferAction(data unsafe.Pointer, offer *C.struct_wl_data_offer, + act C.uint32_t) { +} + +//export gio_onDataDeviceOffer +func gio_onDataDeviceOffer(data unsafe.Pointer, + dataDev *C.struct_wl_data_device, id *C.struct_wl_data_offer) { + s := callbackLoad(data).(*wlSeat) + callbackStore(unsafe.Pointer(id), s) + C.wl_data_offer_add_listener(id, &C.gio_data_offer_listener, + unsafe.Pointer(id)) + s.offers[id] = nil +} + +//export gio_onDataDeviceEnter +func gio_onDataDeviceEnter(data unsafe.Pointer, + dataDev *C.struct_wl_data_device, serial C.uint32_t, + surf *C.struct_wl_surface, x, y C.wl_fixed_t, id *C.struct_wl_data_offer) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + s.flushOffers() +} + +//export gio_onDataDeviceLeave +func gio_onDataDeviceLeave(data unsafe.Pointer, + dataDev *C.struct_wl_data_device) { +} + +//export gio_onDataDeviceMotion +func gio_onDataDeviceMotion(data unsafe.Pointer, + dataDev *C.struct_wl_data_device, t C.uint32_t, x, y C.wl_fixed_t) { +} + +//export gio_onDataDeviceDrop +func gio_onDataDeviceDrop(data unsafe.Pointer, + dataDev *C.struct_wl_data_device) { +} + +//export gio_onDataDeviceSelection +func gio_onDataDeviceSelection(data unsafe.Pointer, + dataDev *C.struct_wl_data_device, id *C.struct_wl_data_offer) { + s := callbackLoad(data).(*wlSeat) + defer s.flushOffers() + s.clipboard = nil +loop: + for _, want := range clipboardMimeTypes { + for _, got := range s.offers[id] { + if want != got { + continue + } + s.clipboard = id + s.mimeType = got + break loop + } + } +} + +//export gio_onRegistryGlobalRemove +func gio_onRegistryGlobalRemove(data unsafe.Pointer, reg *C.struct_wl_registry, + name C.uint32_t) { + d := callbackLoad(data).(*wlDisplay) + if s := d.seat; s != nil && name == s.name { + s.destroy() + d.seat = nil + } + if output, exists := d.outputMap[name]; exists { + C.wl_output_destroy(output) + delete(d.outputMap, name) + delete(d.outputConfig, output) + } +} + +//export gio_onTouchDown +func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, + serial, t C.uint32_t, surf *C.struct_wl_surface, id C.int32_t, + x, y C.wl_fixed_t) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + w := callbackLoad(unsafe.Pointer(surf)).(*window) + s.touchFoci[id] = w + w.lastTouch = f32.Point{ + X: fromFixed(x) * float32(w.scale), + Y: fromFixed(y) * float32(w.scale), + } + w.w.Event(pointer.Event{ + Type: pointer.Press, + Source: pointer.Touch, + Position: w.lastTouch, + PointerID: pointer.ID(id), + Time: time.Duration(t) * time.Millisecond, + Modifiers: w.disp.xkb.Modifiers(), + }) +} + +//export gio_onTouchUp +func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, + serial, t C.uint32_t, id C.int32_t) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + w := s.touchFoci[id] + delete(s.touchFoci, id) + w.w.Event(pointer.Event{ + Type: pointer.Release, + Source: pointer.Touch, + Position: w.lastTouch, + PointerID: pointer.ID(id), + Time: time.Duration(t) * time.Millisecond, + Modifiers: w.disp.xkb.Modifiers(), + }) +} + +//export gio_onTouchMotion +func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, + t C.uint32_t, id C.int32_t, x, y C.wl_fixed_t) { + s := callbackLoad(data).(*wlSeat) + w := s.touchFoci[id] + w.lastTouch = f32.Point{ + X: fromFixed(x) * float32(w.scale), + Y: fromFixed(y) * float32(w.scale), + } + w.w.Event(pointer.Event{ + Type: pointer.Move, + Position: w.lastTouch, + Source: pointer.Touch, + PointerID: pointer.ID(id), + Time: time.Duration(t) * time.Millisecond, + Modifiers: w.disp.xkb.Modifiers(), + }) +} + +//export gio_onTouchFrame +func gio_onTouchFrame(data unsafe.Pointer, touch *C.struct_wl_touch) { +} + +//export gio_onTouchCancel +func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) { + s := callbackLoad(data).(*wlSeat) + for id, w := range s.touchFoci { + delete(s.touchFoci, id) + w.w.Event(pointer.Event{ + Type: pointer.Cancel, + Source: pointer.Touch, + }) + } +} + +//export gio_onPointerEnter +func gio_onPointerEnter(data unsafe.Pointer, pointer *C.struct_wl_pointer, + serial C.uint32_t, surf *C.struct_wl_surface, x, y C.wl_fixed_t) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + w := callbackLoad(unsafe.Pointer(surf)).(*window) + s.pointerFocus = w + w.setCursor(pointer, serial) + w.lastPos = f32.Point{X: fromFixed(x), Y: fromFixed(y)} +} + +//export gio_onPointerLeave +func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, + serial C.uint32_t, surface *C.struct_wl_surface) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial +} + +//export gio_onPointerMotion +func gio_onPointerMotion(data unsafe.Pointer, p *C.struct_wl_pointer, + t C.uint32_t, x, y C.wl_fixed_t) { + s := callbackLoad(data).(*wlSeat) + w := s.pointerFocus + w.resetFling() + w.onPointerMotion(x, y, t) +} + +//export gio_onPointerButton +func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, + serial, t, wbtn, state C.uint32_t) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + w := s.pointerFocus + // From linux-event-codes.h. + const ( + BTN_LEFT = 0x110 + BTN_RIGHT = 0x111 + BTN_MIDDLE = 0x112 + ) + var btn pointer.Buttons + switch wbtn { + case BTN_LEFT: + btn = pointer.ButtonPrimary + case BTN_RIGHT: + btn = pointer.ButtonSecondary + case BTN_MIDDLE: + btn = pointer.ButtonTertiary + default: + return + } + var typ pointer.Type + switch state { + case 0: + w.pointerBtns &^= btn + typ = pointer.Release + case 1: + w.pointerBtns |= btn + typ = pointer.Press + } + w.flushScroll() + w.resetFling() + w.w.Event(pointer.Event{ + Type: typ, + Source: pointer.Mouse, + Buttons: w.pointerBtns, + Position: w.lastPos, + Time: time.Duration(t) * time.Millisecond, + Modifiers: w.disp.xkb.Modifiers(), + }) +} + +//export gio_onPointerAxis +func gio_onPointerAxis(data unsafe.Pointer, p *C.struct_wl_pointer, + t, axis C.uint32_t, value C.wl_fixed_t) { + s := callbackLoad(data).(*wlSeat) + w := s.pointerFocus + v := fromFixed(value) + w.resetFling() + if w.scroll.dist == (f32.Point{}) { + w.scroll.time = time.Duration(t) * time.Millisecond + } + switch axis { + case C.WL_POINTER_AXIS_HORIZONTAL_SCROLL: + w.scroll.dist.X += v + case C.WL_POINTER_AXIS_VERTICAL_SCROLL: + w.scroll.dist.Y += v + } +} + +//export gio_onPointerFrame +func gio_onPointerFrame(data unsafe.Pointer, p *C.struct_wl_pointer) { + s := callbackLoad(data).(*wlSeat) + w := s.pointerFocus + w.flushScroll() + w.flushFling() +} + +func (w *window) flushFling() { + if !w.fling.start { + return + } + w.fling.start = false + estx, esty := w.fling.xExtrapolation.Estimate(), w.fling.yExtrapolation.Estimate() + w.fling.xExtrapolation = fling.Extrapolation{} + w.fling.yExtrapolation = fling.Extrapolation{} + vel := float32(math.Sqrt(float64(estx.Velocity*estx.Velocity + esty.Velocity*esty.Velocity))) + _, _, c := w.config() + if !w.fling.anim.Start(c, time.Now(), vel) { + return + } + invDist := 1 / vel + w.fling.dir.X = estx.Velocity * invDist + w.fling.dir.Y = esty.Velocity * invDist + // Wake up the window loop. + w.disp.wakeup() +} + +//export gio_onPointerAxisSource +func gio_onPointerAxisSource(data unsafe.Pointer, pointer *C.struct_wl_pointer, + source C.uint32_t) { +} + +//export gio_onPointerAxisStop +func gio_onPointerAxisStop(data unsafe.Pointer, p *C.struct_wl_pointer, + t, axis C.uint32_t) { + s := callbackLoad(data).(*wlSeat) + w := s.pointerFocus + w.fling.start = true +} + +//export gio_onPointerAxisDiscrete +func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer, + axis C.uint32_t, discrete C.int32_t) { + s := callbackLoad(data).(*wlSeat) + w := s.pointerFocus + w.resetFling() + switch axis { + case C.WL_POINTER_AXIS_HORIZONTAL_SCROLL: + w.scroll.steps.X += int(discrete) + case C.WL_POINTER_AXIS_VERTICAL_SCROLL: + w.scroll.steps.Y += int(discrete) + } +} + +func (w *window) ReadClipboard() { + w.mu.Lock() + w.readClipboard = true + w.mu.Unlock() + w.disp.wakeup() +} + +func (w *window) WriteClipboard(s string) { + w.mu.Lock() + w.writeClipboard = &s + w.mu.Unlock() + w.disp.wakeup() +} + +func (w *window) Option(opts *Options) { + w.mu.Lock() + w.opts = opts + w.mu.Unlock() + w.disp.wakeup() +} + +func (w *window) setOptions(opts *Options) { + _, _, cfg := w.config() + if o := opts.Size; o != nil { + w.width = cfg.Px(o.Width) + w.height = cfg.Px(o.Height) + } + if o := opts.Title; o != nil { + title := C.CString(*o) + C.xdg_toplevel_set_title(w.topLvl, title) + C.free(unsafe.Pointer(title)) + } +} + +func (w *window) SetCursor(name pointer.CursorName) { + if name == pointer.CursorNone { + C.wl_pointer_set_cursor(w.disp.seat.pointer, w.serial, nil, 0, 0) + return + } + switch name { + default: + fallthrough + case pointer.CursorDefault: + name = "left_ptr" + case pointer.CursorText: + name = "xterm" + case pointer.CursorPointer: + name = "hand1" + case pointer.CursorCrossHair: + name = "crosshair" + case pointer.CursorRowResize: + name = "top_side" + case pointer.CursorColResize: + name = "left_side" + case pointer.CursorGrab: + name = "hand1" + } + cname := C.CString(string(name)) + defer C.free(unsafe.Pointer(cname)) + c := C.wl_cursor_theme_get_cursor(w.cursor.theme, cname) + if c == nil { + return + } + w.cursor.cursor = c + w.setCursor(w.disp.seat.pointer, w.serial) +} + +func (w *window) setCursor(pointer *C.struct_wl_pointer, serial C.uint32_t) { + // Get images[0]. + img := *w.cursor.cursor.images + buf := C.wl_cursor_image_get_buffer(img) + if buf == nil { + return + } + C.wl_pointer_set_cursor(pointer, serial, w.cursor.surf, + C.int32_t(img.hotspot_x), C.int32_t(img.hotspot_y)) + C.wl_surface_attach(w.cursor.surf, buf, 0, 0) + C.wl_surface_damage(w.cursor.surf, 0, 0, C.int32_t(img.width), + C.int32_t(img.height)) + C.wl_surface_commit(w.cursor.surf) +} + +func (w *window) resetFling() { + w.fling.start = false + w.fling.anim = fling.Animation{} +} + +//export gio_onKeyboardKeymap +func gio_onKeyboardKeymap(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, + format C.uint32_t, fd C.int32_t, size C.uint32_t) { + defer syscall.Close(int(fd)) + s := callbackLoad(data).(*wlSeat) + s.disp.repeat.Stop(0) + s.disp.xkb.DestroyKeymapState() + if format != C.WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 { + return + } + if err := s.disp.xkb.LoadKeymap(int(format), int(fd), + int(size)); err != nil { + // TODO: Do better. + panic(err) + } +} + +//export gio_onKeyboardEnter +func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, + serial C.uint32_t, surf *C.struct_wl_surface, keys *C.struct_wl_array) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + w := callbackLoad(unsafe.Pointer(surf)).(*window) + s.keyboardFocus = w + s.disp.repeat.Stop(0) + w.w.Event(key.FocusEvent{Focus: true}) +} + +//export gio_onKeyboardLeave +func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, + serial C.uint32_t, surf *C.struct_wl_surface) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + s.disp.repeat.Stop(0) + w := s.keyboardFocus + w.w.Event(key.FocusEvent{Focus: false}) +} + +//export gio_onKeyboardKey +func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, + serial, timestamp, keyCode, state C.uint32_t) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + w := s.keyboardFocus + t := time.Duration(timestamp) * time.Millisecond + s.disp.repeat.Stop(t) + w.resetFling() + kc := mapXKBKeycode(uint32(keyCode)) + ks := mapXKBKeyState(uint32(state)) + for _, e := range w.disp.xkb.DispatchKey(kc, ks) { + w.w.Event(e) + } + if state != C.WL_KEYBOARD_KEY_STATE_PRESSED { + return + } + if w.disp.xkb.IsRepeatKey(kc) { + w.disp.repeat.Start(w, kc, t) + } +} + +func mapXKBKeycode(keyCode uint32) uint32 { + // According to the xkb_v1 spec: "to determine the xkb keycode, clients must add 8 to the key event keycode." + return keyCode + 8 +} + +func mapXKBKeyState(state uint32) key.State { + switch state { + case C.WL_KEYBOARD_KEY_STATE_RELEASED: + return key.Release + default: + return key.Press + } +} + +func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) { + if r.rate <= 0 { + return + } + stopC := make(chan struct{}) + r.start = t + r.last = 0 + r.now = 0 + r.stopC = stopC + r.key = keyCode + r.win = w.w + rate, delay := r.rate, r.delay + go func() { + timer := time.NewTimer(delay) + for { + select { + case <-timer.C: + case <-stopC: + close(stopC) + return + } + r.Advance(delay) + w.disp.wakeup() + delay = time.Second / time.Duration(rate) + timer.Reset(delay) + } + }() +} + +func (r *repeatState) Stop(t time.Duration) { + if r.stopC == nil { + return + } + r.stopC <- struct{}{} + <-r.stopC + r.stopC = nil + t -= r.start + if r.now > t { + r.now = t + } +} + +func (r *repeatState) Advance(dt time.Duration) { + r.mu.Lock() + defer r.mu.Unlock() + r.now += dt +} + +func (r *repeatState) Repeat(d *wlDisplay) { + if r.rate <= 0 { + return + } + r.mu.Lock() + now := r.now + r.mu.Unlock() + for { + var delay time.Duration + if r.last < r.delay { + delay = r.delay + } else { + delay = time.Second / time.Duration(r.rate) + } + if r.last+delay > now { + break + } + for _, e := range d.xkb.DispatchKey(r.key, key.Press) { + r.win.Event(e) + } + r.last += delay + } +} + +//export gio_onFrameDone +func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, + t C.uint32_t) { + C.wl_callback_destroy(callback) + w := callbackLoad(data).(*window) + if w.lastFrameCallback == callback { + w.lastFrameCallback = nil + w.draw(false) + } +} + +func (w *window) loop() error { + var p poller + for { + if err := w.disp.dispatch(&p); err != nil { + return err + } + if w.dead { + w.w.Event(system.DestroyEvent{}) + break + } + w.process() + } + return nil +} + +func (w *window) process() { + w.mu.Lock() + readClipboard := w.readClipboard + writeClipboard := w.writeClipboard + opts := w.opts + w.readClipboard = false + w.writeClipboard = nil + w.opts = nil + w.mu.Unlock() + if readClipboard { + r, err := w.disp.readClipboard() + // Send empty responses on unavailable clipboards or errors. + if r == nil || err != nil { + w.w.Event(clipboard.Event{}) + return + } + // Don't let slow clipboard transfers block event loop. + go func() { + defer r.Close() + data, _ := ioutil.ReadAll(r) + w.w.Event(clipboard.Event{Text: string(data)}) + }() + } + if writeClipboard != nil { + w.disp.writeClipboard([]byte(*writeClipboard)) + } + if opts != nil { + w.setOptions(opts) + } + // pass false to skip unnecessary drawing. + w.draw(false) +} + +func (d *wlDisplay) dispatch(p *poller) error { + dispfd := C.wl_display_get_fd(d.disp) + // Poll for events and notifications. + pollfds := append(p.pollfds[:0], + syscall.PollFd{Fd: int32(dispfd), + Events: syscall.POLLIN | syscall.POLLERR}, + syscall.PollFd{Fd: int32(d.notify.read), + Events: syscall.POLLIN | syscall.POLLERR}, + ) + dispFd := &pollfds[0] + if ret, err := C.wl_display_flush(d.disp); ret < 0 { + if err != syscall.EAGAIN { + return fmt.Errorf("wayland: wl_display_flush failed: %v", err) + } + // EAGAIN means the output buffer was full. Poll for + // POLLOUT to know when we can write again. + dispFd.Events |= syscall.POLLOUT + } + if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR { + return fmt.Errorf("wayland: poll failed: %v", err) + } + // Clear notifications. + for { + _, err := syscall.Read(d.notify.read, p.buf[:]) + if err == syscall.EAGAIN { + break + } + if err != nil { + return fmt.Errorf("wayland: read from notify pipe failed: %v", err) + } + } + // Handle events + switch { + case dispFd.Revents&syscall.POLLIN != 0: + if ret, err := C.wl_display_dispatch(d.disp); ret < 0 { + return fmt.Errorf("wayland: wl_display_dispatch failed: %v", err) + } + case dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0: + return errors.New("wayland: display file descriptor gone") + } + d.repeat.Repeat(d) + return nil +} + +func (w *window) SetAnimating(anim bool) { + w.mu.Lock() + w.animating = anim + w.mu.Unlock() + w.disp.wakeup() +} + +// Wakeup wakes up the event loop through the notification pipe. +func (d *wlDisplay) wakeup() { + oneByte := make([]byte, 1) + if _, err := syscall.Write(d.notify.write, + oneByte); err != nil && err != syscall.EAGAIN { + panic(fmt.Errorf("failed to write to pipe: %v", err)) + } +} + +func (w *window) destroy() { + if w.cursor.surf != nil { + C.wl_surface_destroy(w.cursor.surf) + } + if w.cursor.theme != nil { + C.wl_cursor_theme_destroy(w.cursor.theme) + } + if w.topLvl != nil { + C.xdg_toplevel_destroy(w.topLvl) + } + if w.surf != nil { + C.wl_surface_destroy(w.surf) + } + if w.wmSurf != nil { + C.xdg_surface_destroy(w.wmSurf) + } + if w.decor != nil { + C.zxdg_toplevel_decoration_v1_destroy(w.decor) + } + callbackDelete(unsafe.Pointer(w.surf)) +} + +//export gio_onKeyboardModifiers +func gio_onKeyboardModifiers(data unsafe.Pointer, + keyboard *C.struct_wl_keyboard, + serial, depressed, latched, locked, group C.uint32_t) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial + d := s.disp + d.repeat.Stop(0) + if d.xkb == nil { + return + } + d.xkb.UpdateMask(uint32(depressed), uint32(latched), uint32(locked), + uint32(group), uint32(group), uint32(group)) +} + +//export gio_onKeyboardRepeatInfo +func gio_onKeyboardRepeatInfo(data unsafe.Pointer, + keyboard *C.struct_wl_keyboard, rate, delay C.int32_t) { + s := callbackLoad(data).(*wlSeat) + d := s.disp + d.repeat.Stop(0) + d.repeat.rate = int(rate) + d.repeat.delay = time.Duration(delay) * time.Millisecond +} + +//export gio_onTextInputEnter +func gio_onTextInputEnter(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, + surf *C.struct_wl_surface) { +} + +//export gio_onTextInputLeave +func gio_onTextInputLeave(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, + surf *C.struct_wl_surface) { +} + +//export gio_onTextInputPreeditString +func gio_onTextInputPreeditString(data unsafe.Pointer, + im *C.struct_zwp_text_input_v3, ctxt *C.char, begin, end C.int32_t) { +} + +//export gio_onTextInputCommitString +func gio_onTextInputCommitString(data unsafe.Pointer, + im *C.struct_zwp_text_input_v3, ctxt *C.char) { +} + +//export gio_onTextInputDeleteSurroundingText +func gio_onTextInputDeleteSurroundingText(data unsafe.Pointer, + im *C.struct_zwp_text_input_v3, before, after C.uint32_t) { +} + +//export gio_onTextInputDone +func gio_onTextInputDone(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, + serial C.uint32_t) { + s := callbackLoad(data).(*wlSeat) + s.serial = serial +} + +//export gio_onDataSourceTarget +func gio_onDataSourceTarget(data unsafe.Pointer, + source *C.struct_wl_data_source, mime *C.char) { +} + +//export gio_onDataSourceSend +func gio_onDataSourceSend(data unsafe.Pointer, source *C.struct_wl_data_source, + mime *C.char, fd C.int32_t) { + s := callbackLoad(data).(*wlSeat) + content := s.content + go func() { + defer syscall.Close(int(fd)) + syscall.Write(int(fd), content) + }() +} + +//export gio_onDataSourceCancelled +func gio_onDataSourceCancelled(data unsafe.Pointer, + source *C.struct_wl_data_source) { + s := callbackLoad(data).(*wlSeat) + if s.source == source { + s.content = nil + s.source = nil + } + C.wl_data_source_destroy(source) +} + +//export gio_onDataSourceDNDDropPerformed +func gio_onDataSourceDNDDropPerformed(data unsafe.Pointer, + source *C.struct_wl_data_source) { +} + +//export gio_onDataSourceDNDFinished +func gio_onDataSourceDNDFinished(data unsafe.Pointer, + source *C.struct_wl_data_source) { +} + +//export gio_onDataSourceAction +func gio_onDataSourceAction(data unsafe.Pointer, + source *C.struct_wl_data_source, act C.uint32_t) { +} + +func (w *window) flushScroll() { + var fling f32.Point + if w.fling.anim.Active() { + dist := float32(w.fling.anim.Tick(time.Now())) + fling = w.fling.dir.Mul(dist) + } + // The Wayland reported scroll distance for + // discrete scroll axes is only 10 pixels, where + // 100 seems more appropriate. + const discreteScale = 10 + if w.scroll.steps.X != 0 { + w.scroll.dist.X *= discreteScale + } + if w.scroll.steps.Y != 0 { + w.scroll.dist.Y *= discreteScale + } + total := w.scroll.dist.Add(fling) + if total == (f32.Point{}) { + return + } + w.w.Event(pointer.Event{ + Type: pointer.Scroll, + Source: pointer.Mouse, + Buttons: w.pointerBtns, + Position: w.lastPos, + Scroll: total, + Time: w.scroll.time, + Modifiers: w.disp.xkb.Modifiers(), + }) + if w.scroll.steps == (image.Point{}) { + w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X) + w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y) + } + w.scroll.dist = f32.Point{} + w.scroll.steps = image.Point{} +} + +func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) { + w.flushScroll() + w.lastPos = f32.Point{ + X: fromFixed(x) * float32(w.scale), + Y: fromFixed(y) * float32(w.scale), + } + w.w.Event(pointer.Event{ + Type: pointer.Move, + Position: w.lastPos, + Buttons: w.pointerBtns, + Source: pointer.Mouse, + Time: time.Duration(t) * time.Millisecond, + Modifiers: w.disp.xkb.Modifiers(), + }) +} + +func (w *window) updateOpaqueRegion() { + reg := C.wl_compositor_create_region(w.disp.compositor) + C.wl_region_add(reg, 0, 0, C.int32_t(w.width), C.int32_t(w.height)) + C.wl_surface_set_opaque_region(w.surf, reg) + C.wl_region_destroy(reg) +} + +func (w *window) updateOutputs() { + scale := 1 + var found bool + for _, conf := range w.disp.outputConfig { + for _, w2 := range conf.windows { + if w2 == w { + found = true + if conf.scale > scale { + scale = conf.scale + } + } + } + } + w.mu.Lock() + if found && scale != w.scale { + w.scale = scale + w.newScale = true + } + w.mu.Unlock() + if !found { + w.setStage(system.StagePaused) + } else { + w.setStage(system.StageRunning) + w.draw(true) + } +} + +func (w *window) config() (int, int, unit.Metric) { + width, height := w.width*w.scale, w.height*w.scale + return width, height, unit.Metric{ + PxPerDp: w.ppdp * float32(w.scale), + PxPerSp: w.ppsp * float32(w.scale), + } +} + +func (w *window) draw(sync bool) { + w.flushScroll() + w.mu.Lock() + anim := w.animating || w.fling.anim.Active() + dead := w.dead + w.mu.Unlock() + if dead || (!anim && !sync) { + return + } + width, height, cfg := w.config() + if cfg == (unit.Metric{}) { + return + } + if anim && w.lastFrameCallback == nil { + w.lastFrameCallback = C.wl_surface_frame(w.surf) + // Use the surface as listener data for gio_onFrameDone. + C.wl_callback_add_listener(w.lastFrameCallback, + &C.gio_callback_listener, unsafe.Pointer(w.surf)) + } + w.w.Event(FrameEvent{ + FrameEvent: system.FrameEvent{ + Now: time.Now(), + Size: image.Point{ + X: width, + Y: height, + }, + Metric: cfg, + }, + Sync: sync, + }) +} + +func (w *window) setStage(s system.Stage) { + if s == w.stage { + return + } + w.stage = s + w.w.Event(system.StageEvent{Stage: s}) +} + +func (w *window) display() *C.struct_wl_display { + return w.disp.disp +} + +func (w *window) surface() (*C.struct_wl_surface, int, int) { + if w.needAck { + C.xdg_surface_ack_configure(w.wmSurf, w.serial) + w.needAck = false + } + width, height, scale := w.width, w.height, w.scale + if w.newScale { + C.wl_surface_set_buffer_scale(w.surf, C.int32_t(scale)) + w.newScale = false + } + return w.surf, width * scale, height * scale +} + +func (w *window) ShowTextInput(show bool) {} + +// Close the window. Not implemented for Wayland. +func (w *window) Close() {} + +// detectUIScale reports the system UI scale, or 1.0 if it fails. +func detectUIScale() float32 { + // TODO: What about other window environments? + out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", + "text-scaling-factor").Output() + if err != nil { + return 1.0 + } + scale, err := strconv.ParseFloat(string(bytes.TrimSpace(out)), 32) + if err != nil { + return 1.0 + } + return float32(scale) +} + +func newWLDisplay() (*wlDisplay, error) { + d := &wlDisplay{ + outputMap: make(map[C.uint32_t]*C.struct_wl_output), + outputConfig: make(map[*C.struct_wl_output]*wlOutput), + } + pipe := make([]int, 2) + if err := syscall.Pipe2(pipe, + syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil { + return nil, fmt.Errorf("wayland: failed to create pipe: %v", err) + } + d.notify.read = pipe[0] + d.notify.write = pipe[1] + xkb, err := xkb.New() + if err != nil { + d.destroy() + return nil, fmt.Errorf("wayland: %v", err) + } + d.xkb = xkb + d.disp, err = C.wl_display_connect(nil) + if d.disp == nil { + d.destroy() + return nil, fmt.Errorf("wayland: wl_display_connect failed: %v", err) + } + callbackMap.Store(unsafe.Pointer(d.disp), d) + d.reg = C.wl_display_get_registry(d.disp) + if d.reg == nil { + d.destroy() + return nil, errors.New("wayland: wl_display_get_registry failed") + } + C.wl_registry_add_listener(d.reg, &C.gio_registry_listener, + unsafe.Pointer(d.disp)) + // Wait for the server to register all its globals to the + // registry listener (gio_onRegistryGlobal). + C.wl_display_roundtrip(d.disp) + // Configuration listeners are added to outputs by gio_onRegistryGlobal. + // We need another roundtrip to get the initial output configurations + // through the gio_onOutput* callbacks. + C.wl_display_roundtrip(d.disp) + return d, nil +} + +func (d *wlDisplay) destroy() { + if d.notify.write != 0 { + syscall.Close(d.notify.write) + d.notify.write = 0 + } + if d.notify.read != 0 { + syscall.Close(d.notify.read) + d.notify.read = 0 + } + d.repeat.Stop(0) + if d.xkb != nil { + d.xkb.Destroy() + d.xkb = nil + } + if d.seat != nil { + d.seat.destroy() + d.seat = nil + } + if d.imm != nil { + C.zwp_text_input_manager_v3_destroy(d.imm) + } + if d.decor != nil { + C.zxdg_decoration_manager_v1_destroy(d.decor) + } + if d.shm != nil { + C.wl_shm_destroy(d.shm) + } + if d.compositor != nil { + C.wl_compositor_destroy(d.compositor) + } + if d.wm != nil { + C.xdg_wm_base_destroy(d.wm) + } + for _, output := range d.outputMap { + C.wl_output_destroy(output) + } + if d.reg != nil { + C.wl_registry_destroy(d.reg) + } + if d.disp != nil { + C.wl_display_disconnect(d.disp) + callbackDelete(unsafe.Pointer(d.disp)) + } +} + +// fromFixed converts a Wayland wl_fixed_t 23.8 number to float32. +func fromFixed(v C.wl_fixed_t) float32 { + // Convert to float64 to avoid overflow. + // From wayland-util.h. + b := ((1023 + 44) << 52) + (1 << 51) + uint64(v) + f := math.Float64frombits(b) - (3 << 43) + return float32(f) +} diff --git a/gio/giold/app/internal/wm/os_windows.go b/gio/giold/app/internal/wm/os_windows.go new file mode 100644 index 0000000..ba83a87 --- /dev/null +++ b/gio/giold/app/internal/wm/os_windows.go @@ -0,0 +1,805 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package wm + +import ( + "errors" + "fmt" + "image" + "reflect" + "runtime" + "sort" + "strings" + "sync" + "time" + "unicode" + "unsafe" + + syscall "golang.org/x/sys/windows" + + "realy.lol/gio/app/internal/windows" + "realy.lol/gio/unit" + gowindows "golang.org/x/sys/windows" + + "realy.lol/gio/f32" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" +) + +type winConstraints struct { + minWidth, minHeight int32 + maxWidth, maxHeight int32 +} + +type winDeltas struct { + width int32 + height int32 +} + +type window struct { + hwnd syscall.Handle + hdc syscall.Handle + w Callbacks + width int + height int + stage system.Stage + pointerBtns pointer.Buttons + + // cursorIn tracks whether the cursor was inside the window according + // to the most recent WM_SETCURSOR. + cursorIn bool + cursor syscall.Handle + + // placement saves the previous window position when in full screen mode. + placement *windows.WindowPlacement + + mu sync.Mutex + animating bool + + minmax winConstraints + deltas winDeltas + opts *Options +} + +const ( + _WM_REDRAW = windows.WM_USER + iota + _WM_CURSOR + _WM_OPTION +) + +type gpuAPI struct { + priority int + initializer func(w *window) (Context, error) +} + +// drivers is the list of potential Context implementations. +var drivers []gpuAPI + +// winMap maps win32 HWNDs to *windows. +var winMap sync.Map + +// iconID is the ID of the icon in the resource file. +const iconID = 1 + +var resources struct { + once sync.Once + // handle is the module handle from GetModuleHandle. + handle syscall.Handle + // class is the Gio window class from RegisterClassEx. + class uint16 + // cursor is the arrow cursor resource. + cursor syscall.Handle +} + +func Main() { + select {} +} + +func NewWindow(window Callbacks, opts *Options) error { + cerr := make(chan error) + go func() { + // GetMessage and PeekMessage can filter on a window HWND, but + // then thread-specific messages such as WM_QUIT are ignored. + // Instead lock the thread so window messages arrive through + // unfiltered GetMessage calls. + runtime.LockOSThread() + w, err := createNativeWindow(opts) + if err != nil { + cerr <- err + return + } + defer w.destroy() + cerr <- nil + winMap.Store(w.hwnd, w) + defer winMap.Delete(w.hwnd) + w.w = window + w.w.SetDriver(w) + defer w.w.Event(system.DestroyEvent{}) + w.Option(opts) + windows.ShowWindow(w.hwnd, windows.SW_SHOWDEFAULT) + windows.SetForegroundWindow(w.hwnd) + windows.SetFocus(w.hwnd) + // Since the window class for the cursor is null, + // set it here to show the cursor. + w.SetCursor(pointer.CursorDefault) + if err := w.loop(); err != nil { + panic(err) + } + }() + return <-cerr +} + +// initResources initializes the resources global. +func initResources() error { + windows.SetProcessDPIAware() + hInst, err := windows.GetModuleHandle() + if err != nil { + return err + } + resources.handle = hInst + c, err := windows.LoadCursor(windows.IDC_ARROW) + if err != nil { + return err + } + resources.cursor = c + icon, _ := windows.LoadImage(hInst, iconID, windows.IMAGE_ICON, 0, 0, + windows.LR_DEFAULTSIZE|windows.LR_SHARED) + wcls := windows.WndClassEx{ + CbSize: uint32(unsafe.Sizeof(windows.WndClassEx{})), + Style: windows.CS_HREDRAW | windows.CS_VREDRAW | windows.CS_OWNDC, + LpfnWndProc: syscall.NewCallback(windowProc), + HInstance: hInst, + HIcon: icon, + LpszClassName: syscall.StringToUTF16Ptr("GioWindow"), + } + cls, err := windows.RegisterClassEx(&wcls) + if err != nil { + return err + } + resources.class = cls + return nil +} + +func getWindowConstraints(cfg unit.Metric, opts *Options) winConstraints { + var minmax winConstraints + if o := opts.MinSize; o != nil { + minmax.minWidth = int32(cfg.Px(o.Width)) + minmax.minHeight = int32(cfg.Px(o.Height)) + } + if o := opts.MaxSize; o != nil { + minmax.maxWidth = int32(cfg.Px(o.Width)) + minmax.maxHeight = int32(cfg.Px(o.Height)) + } + return minmax +} + +func createNativeWindow(opts *Options) (*window, error) { + var resErr error + resources.once.Do(func() { + resErr = initResources() + }) + if resErr != nil { + return nil, resErr + } + dpi := windows.GetSystemDPI() + cfg := configForDPI(dpi) + dwStyle := uint32(windows.WS_OVERLAPPEDWINDOW) + dwExStyle := uint32(windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE) + + hwnd, err := windows.CreateWindowEx(dwExStyle, + resources.class, + "", + dwStyle|windows.WS_CLIPSIBLINGS|windows.WS_CLIPCHILDREN, + windows.CW_USEDEFAULT, windows.CW_USEDEFAULT, + windows.CW_USEDEFAULT, windows.CW_USEDEFAULT, + 0, + 0, + resources.handle, + 0) + if err != nil { + return nil, err + } + w := &window{ + hwnd: hwnd, + minmax: getWindowConstraints(cfg, opts), + opts: opts, + } + w.hdc, err = windows.GetDC(hwnd) + if err != nil { + return nil, err + } + return w, nil +} + +func windowProc(hwnd syscall.Handle, msg uint32, + wParam, lParam uintptr) uintptr { + win, exists := winMap.Load(hwnd) + if !exists { + return windows.DefWindowProc(hwnd, msg, wParam, lParam) + } + + w := win.(*window) + + switch msg { + case windows.WM_UNICHAR: + if wParam == windows.UNICODE_NOCHAR { + // Tell the system that we accept WM_UNICHAR messages. + return windows.TRUE + } + fallthrough + case windows.WM_CHAR: + if r := rune(wParam); unicode.IsPrint(r) { + w.w.Event(key.EditEvent{Text: string(r)}) + } + // The message is processed. + return windows.TRUE + case windows.WM_DPICHANGED: + // Let Windows know we're prepared for runtime DPI changes. + return windows.TRUE + case windows.WM_ERASEBKGND: + // Avoid flickering between GPU content and background color. + return windows.TRUE + case windows.WM_KEYDOWN, windows.WM_KEYUP, windows.WM_SYSKEYDOWN, windows.WM_SYSKEYUP: + if n, ok := convertKeyCode(wParam); ok { + e := key.Event{ + Name: n, + Modifiers: getModifiers(), + State: key.Press, + } + if msg == windows.WM_KEYUP || msg == windows.WM_SYSKEYUP { + e.State = key.Release + } + + w.w.Event(e) + + if (wParam == windows.VK_F10) && (msg == windows.WM_SYSKEYDOWN || msg == windows.WM_SYSKEYUP) { + // Reserve F10 for ourselves, and don't let it open the system menu. Other Windows programs + // such as cmd.exe and graphical debuggers also reserve F10. + return 0 + } + } + case windows.WM_LBUTTONDOWN: + w.pointerButton(pointer.ButtonPrimary, true, lParam, getModifiers()) + case windows.WM_LBUTTONUP: + w.pointerButton(pointer.ButtonPrimary, false, lParam, getModifiers()) + case windows.WM_RBUTTONDOWN: + w.pointerButton(pointer.ButtonSecondary, true, lParam, getModifiers()) + case windows.WM_RBUTTONUP: + w.pointerButton(pointer.ButtonSecondary, false, lParam, getModifiers()) + case windows.WM_MBUTTONDOWN: + w.pointerButton(pointer.ButtonTertiary, true, lParam, getModifiers()) + case windows.WM_MBUTTONUP: + w.pointerButton(pointer.ButtonTertiary, false, lParam, getModifiers()) + case windows.WM_CANCELMODE: + w.w.Event(pointer.Event{ + Type: pointer.Cancel, + }) + case windows.WM_SETFOCUS: + w.w.Event(key.FocusEvent{Focus: true}) + case windows.WM_KILLFOCUS: + w.w.Event(key.FocusEvent{Focus: false}) + case windows.WM_MOUSEMOVE: + x, y := coordsFromlParam(lParam) + p := f32.Point{X: float32(x), Y: float32(y)} + w.w.Event(pointer.Event{ + Type: pointer.Move, + Source: pointer.Mouse, + Position: p, + Buttons: w.pointerBtns, + Time: windows.GetMessageTime(), + }) + case windows.WM_MOUSEWHEEL: + w.scrollEvent(wParam, lParam, false) + case windows.WM_MOUSEHWHEEL: + w.scrollEvent(wParam, lParam, true) + case windows.WM_DESTROY: + windows.PostQuitMessage(0) + case windows.WM_PAINT: + w.draw(true) + case windows.WM_SIZE: + switch wParam { + case windows.SIZE_MINIMIZED: + w.setStage(system.StagePaused) + case windows.SIZE_MAXIMIZED, windows.SIZE_RESTORED: + w.setStage(system.StageRunning) + } + case windows.WM_GETMINMAXINFO: + mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam))) + if w.minmax.minWidth > 0 || w.minmax.minHeight > 0 { + mm.PtMinTrackSize = windows.Point{ + X: w.minmax.minWidth + w.deltas.width, + Y: w.minmax.minHeight + w.deltas.height, + } + } + if w.minmax.maxWidth > 0 || w.minmax.maxHeight > 0 { + mm.PtMaxTrackSize = windows.Point{ + X: w.minmax.maxWidth + w.deltas.width, + Y: w.minmax.maxHeight + w.deltas.height, + } + } + case windows.WM_SETCURSOR: + w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT + fallthrough + case _WM_CURSOR: + if w.cursorIn { + windows.SetCursor(w.cursor) + return windows.TRUE + } + case _WM_OPTION: + w.setOptions() + } + + return windows.DefWindowProc(hwnd, msg, wParam, lParam) +} + +func getModifiers() key.Modifiers { + var kmods key.Modifiers + if windows.GetKeyState(windows.VK_LWIN)&0x1000 != 0 || windows.GetKeyState(windows.VK_RWIN)&0x1000 != 0 { + kmods |= key.ModSuper + } + if windows.GetKeyState(windows.VK_MENU)&0x1000 != 0 { + kmods |= key.ModAlt + } + if windows.GetKeyState(windows.VK_CONTROL)&0x1000 != 0 { + kmods |= key.ModCtrl + } + if windows.GetKeyState(windows.VK_SHIFT)&0x1000 != 0 { + kmods |= key.ModShift + } + return kmods +} + +func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, + kmods key.Modifiers) { + var typ pointer.Type + if press { + typ = pointer.Press + if w.pointerBtns == 0 { + windows.SetCapture(w.hwnd) + } + w.pointerBtns |= btn + } else { + typ = pointer.Release + w.pointerBtns &^= btn + if w.pointerBtns == 0 { + windows.ReleaseCapture() + } + } + x, y := coordsFromlParam(lParam) + p := f32.Point{X: float32(x), Y: float32(y)} + w.w.Event(pointer.Event{ + Type: typ, + Source: pointer.Mouse, + Position: p, + Buttons: w.pointerBtns, + Time: windows.GetMessageTime(), + Modifiers: kmods, + }) +} + +func coordsFromlParam(lParam uintptr) (int, int) { + x := int(int16(lParam & 0xffff)) + y := int(int16((lParam >> 16) & 0xffff)) + return x, y +} + +func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool) { + x, y := coordsFromlParam(lParam) + // The WM_MOUSEWHEEL coordinates are in screen coordinates, in contrast + // to other mouse events. + np := windows.Point{X: int32(x), Y: int32(y)} + windows.ScreenToClient(w.hwnd, &np) + p := f32.Point{X: float32(np.X), Y: float32(np.Y)} + dist := float32(int16(wParam >> 16)) + var sp f32.Point + if horizontal { + sp.X = dist + } else { + sp.Y = -dist + } + w.w.Event(pointer.Event{ + Type: pointer.Scroll, + Source: pointer.Mouse, + Position: p, + Buttons: w.pointerBtns, + Scroll: sp, + Time: windows.GetMessageTime(), + }) +} + +// Adapted from https://blogs.msdn.microsoft.com/oldnewthing/20060126-00/?p=32513/ +func (w *window) loop() error { + msg := new(windows.Msg) +loop: + for { + w.mu.Lock() + anim := w.animating + w.mu.Unlock() + if anim && !windows.PeekMessage(msg, 0, 0, 0, windows.PM_NOREMOVE) { + w.draw(false) + continue + } + switch ret := windows.GetMessage(msg, 0, 0, 0); ret { + case -1: + return errors.New("GetMessage failed") + case 0: + // WM_QUIT received. + break loop + } + windows.TranslateMessage(msg) + windows.DispatchMessage(msg) + } + return nil +} + +func (w *window) SetAnimating(anim bool) { + w.mu.Lock() + w.animating = anim + w.mu.Unlock() + if anim { + w.postRedraw() + } +} + +func (w *window) postRedraw() { + if err := windows.PostMessage(w.hwnd, _WM_REDRAW, 0, 0); err != nil { + panic(err) + } +} + +func (w *window) setStage(s system.Stage) { + w.stage = s + w.w.Event(system.StageEvent{Stage: s}) +} + +func (w *window) draw(sync bool) { + var r windows.Rect + windows.GetClientRect(w.hwnd, &r) + w.width = int(r.Right - r.Left) + w.height = int(r.Bottom - r.Top) + if w.width == 0 || w.height == 0 { + return + } + dpi := windows.GetWindowDPI(w.hwnd) + cfg := configForDPI(dpi) + w.minmax = getWindowConstraints(cfg, w.opts) + w.w.Event(FrameEvent{ + FrameEvent: system.FrameEvent{ + Now: time.Now(), + Size: image.Point{ + X: w.width, + Y: w.height, + }, + Metric: cfg, + }, + Sync: sync, + }) +} + +func (w *window) destroy() { + if w.hdc != 0 { + windows.ReleaseDC(w.hdc) + w.hdc = 0 + } + if w.hwnd != 0 { + windows.DestroyWindow(w.hwnd) + w.hwnd = 0 + } +} + +func (w *window) NewContext() (Context, error) { + sort.Slice(drivers, func(i, j int) bool { + return drivers[i].priority < drivers[j].priority + }) + var errs []string + for _, b := range drivers { + ctx, err := b.initializer(w) + if err == nil { + return ctx, nil + } + errs = append(errs, err.Error()) + } + if len(errs) > 0 { + return nil, fmt.Errorf("NewContext: failed to create a GPU device, tried: %s", + strings.Join(errs, ", ")) + } + return nil, errors.New("NewContext: no available GPU drivers") +} + +func (w *window) ReadClipboard() { + w.readClipboard() +} + +func (w *window) readClipboard() error { + if err := windows.OpenClipboard(w.hwnd); err != nil { + return err + } + defer windows.CloseClipboard() + mem, err := windows.GetClipboardData(windows.CF_UNICODETEXT) + if err != nil { + return err + } + ptr, err := windows.GlobalLock(mem) + if err != nil { + return err + } + defer windows.GlobalUnlock(mem) + content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr))) + go func() { + w.w.Event(clipboard.Event{Text: content}) + }() + return nil +} + +func (w *window) Option(opts *Options) { + w.mu.Lock() + w.opts = opts + w.mu.Unlock() + if err := windows.PostMessage(w.hwnd, _WM_OPTION, 0, 0); err != nil { + panic(err) + } +} + +func (w *window) setOptions() { + w.mu.Lock() + opts := w.opts + w.mu.Unlock() + if o := opts.Size; o != nil { + dpi := windows.GetSystemDPI() + cfg := configForDPI(dpi) + width := int32(cfg.Px(o.Width)) + height := int32(cfg.Px(o.Height)) + + // Include the window decorations. + wr := windows.Rect{ + Right: width, + Bottom: height, + } + dwStyle := uint32(windows.WS_OVERLAPPEDWINDOW) + dwExStyle := uint32(windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE) + windows.AdjustWindowRectEx(&wr, dwStyle, 0, dwExStyle) + + dw, dh := width, height + width = wr.Right - wr.Left + height = wr.Bottom - wr.Top + w.deltas.width = width - dw + w.deltas.height = height - dh + + w.opts.Size = o + windows.MoveWindow(w.hwnd, 0, 0, width, height, true) + } + if o := opts.MinSize; o != nil { + w.opts.MinSize = o + } + if o := opts.MaxSize; o != nil { + w.opts.MaxSize = o + } + if o := opts.Title; o != nil { + windows.SetWindowText(w.hwnd, *opts.Title) + } + if o := opts.WindowMode; o != nil { + w.SetWindowMode(*o) + } +} + +func (w *window) SetWindowMode(mode WindowMode) { + // https://devblogs.microsoft.com/oldnewthing/20100412-00/?p=14353 + switch mode { + case Windowed: + if w.placement == nil { + return + } + windows.SetWindowPlacement(w.hwnd, w.placement) + w.placement = nil + style := windows.GetWindowLong(w.hwnd) + windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, + style|windows.WS_OVERLAPPEDWINDOW) + windows.SetWindowPos(w.hwnd, windows.HWND_TOPMOST, + 0, 0, 0, 0, + windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED, + ) + case Fullscreen: + if w.placement != nil { + return + } + w.placement = windows.GetWindowPlacement(w.hwnd) + style := windows.GetWindowLong(w.hwnd) + windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, + style&^windows.WS_OVERLAPPEDWINDOW) + mi := windows.GetMonitorInfo(w.hwnd) + windows.SetWindowPos(w.hwnd, 0, + mi.Monitor.Left, mi.Monitor.Top, + mi.Monitor.Right-mi.Monitor.Left, + mi.Monitor.Bottom-mi.Monitor.Top, + windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED, + ) + } +} + +func (w *window) WriteClipboard(s string) { + w.writeClipboard(s) +} + +func (w *window) writeClipboard(s string) error { + if err := windows.OpenClipboard(w.hwnd); err != nil { + return err + } + defer windows.CloseClipboard() + if err := windows.EmptyClipboard(); err != nil { + return err + } + u16, err := gowindows.UTF16FromString(s) + if err != nil { + return err + } + n := len(u16) * int(unsafe.Sizeof(u16[0])) + mem, err := windows.GlobalAlloc(n) + if err != nil { + return err + } + ptr, err := windows.GlobalLock(mem) + if err != nil { + windows.GlobalFree(mem) + return err + } + var u16v []uint16 + hdr := (*reflect.SliceHeader)(unsafe.Pointer(&u16v)) + hdr.Data = ptr + hdr.Cap = len(u16) + hdr.Len = len(u16) + copy(u16v, u16) + windows.GlobalUnlock(mem) + if err := windows.SetClipboardData(windows.CF_UNICODETEXT, + mem); err != nil { + windows.GlobalFree(mem) + return err + } + return nil +} + +func (w *window) SetCursor(name pointer.CursorName) { + c, err := loadCursor(name) + if err != nil { + c = resources.cursor + } + w.cursor = c + if err := windows.PostMessage(w.hwnd, _WM_CURSOR, 0, 0); err != nil { + panic(err) + } +} + +func loadCursor(name pointer.CursorName) (syscall.Handle, error) { + var curID uint16 + switch name { + default: + fallthrough + case pointer.CursorDefault: + return resources.cursor, nil + case pointer.CursorText: + curID = windows.IDC_IBEAM + case pointer.CursorPointer: + curID = windows.IDC_HAND + case pointer.CursorCrossHair: + curID = windows.IDC_CROSS + case pointer.CursorColResize: + curID = windows.IDC_SIZEWE + case pointer.CursorRowResize: + curID = windows.IDC_SIZENS + case pointer.CursorGrab: + curID = windows.IDC_SIZEALL + case pointer.CursorNone: + return 0, nil + } + return windows.LoadCursor(curID) +} + +func (w *window) ShowTextInput(show bool) {} + +func (w *window) HDC() syscall.Handle { + return w.hdc +} + +func (w *window) HWND() (syscall.Handle, int, int) { + return w.hwnd, w.width, w.height +} + +func (w *window) Close() { + windows.PostMessage(w.hwnd, windows.WM_CLOSE, 0, 0) +} + +func convertKeyCode(code uintptr) (string, bool) { + if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' { + return string(rune(code)), true + } + var r string + switch code { + case windows.VK_ESCAPE: + r = key.NameEscape + case windows.VK_LEFT: + r = key.NameLeftArrow + case windows.VK_RIGHT: + r = key.NameRightArrow + case windows.VK_RETURN: + r = key.NameReturn + case windows.VK_UP: + r = key.NameUpArrow + case windows.VK_DOWN: + r = key.NameDownArrow + case windows.VK_HOME: + r = key.NameHome + case windows.VK_END: + r = key.NameEnd + case windows.VK_BACK: + r = key.NameDeleteBackward + case windows.VK_DELETE: + r = key.NameDeleteForward + case windows.VK_PRIOR: + r = key.NamePageUp + case windows.VK_NEXT: + r = key.NamePageDown + case windows.VK_F1: + r = "F1" + case windows.VK_F2: + r = "F2" + case windows.VK_F3: + r = "F3" + case windows.VK_F4: + r = "F4" + case windows.VK_F5: + r = "F5" + case windows.VK_F6: + r = "F6" + case windows.VK_F7: + r = "F7" + case windows.VK_F8: + r = "F8" + case windows.VK_F9: + r = "F9" + case windows.VK_F10: + r = "F10" + case windows.VK_F11: + r = "F11" + case windows.VK_F12: + r = "F12" + case windows.VK_TAB: + r = key.NameTab + case windows.VK_SPACE: + r = key.NameSpace + case windows.VK_OEM_1: + r = ";" + case windows.VK_OEM_PLUS: + r = "+" + case windows.VK_OEM_COMMA: + r = "," + case windows.VK_OEM_MINUS: + r = "-" + case windows.VK_OEM_PERIOD: + r = "." + case windows.VK_OEM_2: + r = "/" + case windows.VK_OEM_3: + r = "`" + case windows.VK_OEM_4: + r = "[" + case windows.VK_OEM_5, windows.VK_OEM_102: + r = "\\" + case windows.VK_OEM_6: + r = "]" + case windows.VK_OEM_7: + r = "'" + default: + return "", false + } + return r, true +} + +func configForDPI(dpi int) unit.Metric { + const inchPrDp = 1.0 / 96.0 + ppdp := float32(dpi) * inchPrDp + return unit.Metric{ + PxPerDp: ppdp, + PxPerSp: ppdp, + } +} diff --git a/gio/giold/app/internal/wm/os_x11.go b/gio/giold/app/internal/wm/os_x11.go new file mode 100644 index 0000000..f17f36f --- /dev/null +++ b/gio/giold/app/internal/wm/os_x11.go @@ -0,0 +1,799 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build (linux && !android && !nox11) || freebsd || openbsd +// +build linux,!android,!nox11 freebsd openbsd + +package wm + +/* +#cgo openbsd CFLAGS: -I/usr/X11R6/include -I/usr/local/include +#cgo openbsd LDFLAGS: -L/usr/X11R6/lib -L/usr/local/lib +#cgo freebsd openbsd LDFLAGS: -lX11 -lxkbcommon -lxkbcommon-x11 -lX11-xcb -lXcursor -lXfixes +#cgo linux pkg-config: x11 xkbcommon xkbcommon-x11 x11-xcb xcursor xfixes + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +*/ +import "C" +import ( + "errors" + "fmt" + "image" + "os" + "path/filepath" + "strconv" + "sync" + "time" + "unsafe" + + "realy.lol/gio/f32" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/unit" + + syscall "golang.org/x/sys/unix" + + "realy.lol/gio/app/internal/xkb" +) + +type x11Window struct { + w Callbacks + x *C.Display + xkb *xkb.Context + xkbEventBase C.int + xw C.Window + + atoms struct { + // "UTF8_STRING". + utf8string C.Atom + // "text/plain;charset=utf-8". + plaintext C.Atom + // "TARGETS" + targets C.Atom + // "CLIPBOARD". + clipboard C.Atom + // "CLIPBOARD_CONTENT", the clipboard destination property. + clipboardContent C.Atom + // "WM_DELETE_WINDOW" + evDelWindow C.Atom + // "ATOM" + atom C.Atom + // "GTK_TEXT_BUFFER_CONTENTS" + gtk_text_buffer_contents C.Atom + // "_NET_WM_NAME" + wmName C.Atom + // "_NET_WM_STATE" + wmState C.Atom + // _NET_WM_STATE_FULLSCREEN" + wmStateFullscreen C.Atom + } + stage system.Stage + cfg unit.Metric + width int + height int + notify struct { + read, write int + } + dead bool + + mu sync.Mutex + animating bool + opts *Options + + pointerBtns pointer.Buttons + + clipboard struct { + read bool + write *string + content []byte + } + cursor pointer.CursorName + mode WindowMode +} + +func (w *x11Window) SetAnimating(anim bool) { + w.mu.Lock() + w.animating = anim + w.mu.Unlock() + if anim { + w.wakeup() + } +} + +func (w *x11Window) ReadClipboard() { + w.mu.Lock() + w.clipboard.read = true + w.mu.Unlock() + w.wakeup() +} + +func (w *x11Window) WriteClipboard(s string) { + w.mu.Lock() + w.clipboard.write = &s + w.mu.Unlock() + w.wakeup() +} + +func (w *x11Window) Option(opts *Options) { + w.mu.Lock() + w.opts = opts + w.mu.Unlock() + w.wakeup() +} + +func (w *x11Window) setOptions() { + w.mu.Lock() + opts := w.opts + w.opts = nil + w.mu.Unlock() + if opts == nil { + return + } + var shints C.XSizeHints + if o := opts.MinSize; o != nil { + shints.min_width = C.int(w.cfg.Px(o.Width)) + shints.min_height = C.int(w.cfg.Px(o.Height)) + shints.flags = C.PMinSize + } + if o := opts.MaxSize; o != nil { + shints.max_width = C.int(w.cfg.Px(o.Width)) + shints.max_height = C.int(w.cfg.Px(o.Height)) + shints.flags = shints.flags | C.PMaxSize + } + if shints.flags != 0 { + C.XSetWMNormalHints(w.x, w.xw, &shints) + } + + var title string + if o := opts.Title; o != nil { + title = *o + } + ctitle := C.CString(title) + defer C.free(unsafe.Pointer(ctitle)) + C.XStoreName(w.x, w.xw, ctitle) + // set _NET_WM_NAME as well for UTF-8 support in window title. + C.XSetTextProperty(w.x, w.xw, + &C.XTextProperty{ + value: (*C.uchar)(unsafe.Pointer(ctitle)), + encoding: w.atoms.utf8string, + format: 8, + nitems: C.ulong(len(title)), + }, + w.atoms.wmName) + + if o := opts.WindowMode; o != nil { + w.SetWindowMode(*o) + } +} + +func (w *x11Window) SetCursor(name pointer.CursorName) { + switch name { + case pointer.CursorNone: + w.cursor = name + C.XFixesHideCursor(w.x, w.xw) + return + case pointer.CursorGrab: + name = "hand1" + } + if w.cursor == pointer.CursorNone { + C.XFixesShowCursor(w.x, w.xw) + } + cname := C.CString(string(name)) + defer C.free(unsafe.Pointer(cname)) + c := C.XcursorLibraryLoadCursor(w.x, cname) + if c == 0 { + name = pointer.CursorDefault + } + w.cursor = name + // If c if null (i.e. name was not found), + // XDefineCursor will use the default cursor. + C.XDefineCursor(w.x, w.xw, c) +} + +func (w *x11Window) SetWindowMode(mode WindowMode) { + switch mode { + case w.mode: + return + case Windowed: + C.XDeleteProperty(w.x, w.xw, w.atoms.wmStateFullscreen) + case Fullscreen: + C.XChangeProperty(w.x, w.xw, w.atoms.wmState, C.XA_ATOM, + 32, C.PropModeReplace, + (*C.uchar)(unsafe.Pointer(&w.atoms.wmStateFullscreen)), 1, + ) + default: + return + } + w.mode = mode + // "A Client wishing to change the state of a window MUST send + // a _NET_WM_STATE client message to the root window (see below)." + var xev C.XEvent + ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev)) + *ev = C.XClientMessageEvent{ + _type: C.ClientMessage, + display: w.x, + window: w.xw, + message_type: w.atoms.wmState, + format: 32, + } + arr := (*[5]C.long)(unsafe.Pointer(&ev.data)) + arr[0] = 2 // _NET_WM_STATE_TOGGLE + arr[1] = C.long(w.atoms.wmStateFullscreen) + arr[2] = 0 + arr[3] = 1 // application + arr[4] = 0 + C.XSendEvent( + w.x, + C.XDefaultRootWindow(w.x), // MUST be the root window + C.False, + C.SubstructureNotifyMask|C.SubstructureRedirectMask, + &xev, + ) +} + +func (w *x11Window) ShowTextInput(show bool) {} + +// Close the window. +func (w *x11Window) Close() { + w.mu.Lock() + defer w.mu.Unlock() + + var xev C.XEvent + ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev)) + *ev = C.XClientMessageEvent{ + _type: C.ClientMessage, + display: w.x, + window: w.xw, + message_type: w.atom("WM_PROTOCOLS", true), + format: 32, + } + arr := (*[5]C.long)(unsafe.Pointer(&ev.data)) + arr[0] = C.long(w.atoms.evDelWindow) + arr[1] = C.CurrentTime + C.XSendEvent(w.x, w.xw, C.False, C.NoEventMask, &xev) +} + +var x11OneByte = make([]byte, 1) + +func (w *x11Window) wakeup() { + if _, err := syscall.Write(w.notify.write, + x11OneByte); err != nil && err != syscall.EAGAIN { + panic(fmt.Errorf("failed to write to pipe: %v", err)) + } +} + +func (w *x11Window) display() *C.Display { + return w.x +} + +func (w *x11Window) window() (C.Window, int, int) { + return w.xw, w.width, w.height +} + +func (w *x11Window) setStage(s system.Stage) { + if s == w.stage { + return + } + w.stage = s + w.w.Event(system.StageEvent{Stage: s}) +} + +func (w *x11Window) loop() { + h := x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)} + xfd := C.XConnectionNumber(w.x) + + // Poll for events and notifications. + pollfds := []syscall.PollFd{ + {Fd: int32(xfd), Events: syscall.POLLIN | syscall.POLLERR}, + {Fd: int32(w.notify.read), Events: syscall.POLLIN | syscall.POLLERR}, + } + xEvents := &pollfds[0].Revents + // Plenty of room for a backlog of notifications. + buf := make([]byte, 100) + +loop: + for !w.dead { + var syn, anim bool + // Check for pending draw events before checking animation or blocking. + // This fixes an issue on Xephyr where on startup XPending() > 0 but + // poll will still block. This also prevents no-op calls to poll. + if syn = h.handleEvents(); !syn { + w.mu.Lock() + anim = w.animating + w.mu.Unlock() + if !anim { + // Clear poll events. + *xEvents = 0 + // Wait for X event or gio notification. + if _, err := syscall.Poll(pollfds, + -1); err != nil && err != syscall.EINTR { + panic(fmt.Errorf("x11 loop: poll failed: %w", err)) + } + switch { + case *xEvents&syscall.POLLIN != 0: + syn = h.handleEvents() + if w.dead { + break loop + } + case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0: + break loop + } + } + } + w.setOptions() + // Clear notifications. + for { + _, err := syscall.Read(w.notify.read, buf) + if err == syscall.EAGAIN { + break + } + if err != nil { + panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", + err)) + } + } + + if anim || syn { + w.w.Event(FrameEvent{ + FrameEvent: system.FrameEvent{ + Now: time.Now(), + Size: image.Point{ + X: w.width, + Y: w.height, + }, + Metric: w.cfg, + }, + Sync: syn, + }) + } + w.mu.Lock() + readClipboard := w.clipboard.read + writeClipboard := w.clipboard.write + w.clipboard.read = false + w.clipboard.write = nil + w.mu.Unlock() + if readClipboard { + C.XDeleteProperty(w.x, w.xw, w.atoms.clipboardContent) + C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, + w.atoms.clipboardContent, w.xw, C.CurrentTime) + } + if writeClipboard != nil { + w.clipboard.content = []byte(*writeClipboard) + C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime) + } + } + w.w.Event(system.DestroyEvent{Err: nil}) +} + +func (w *x11Window) destroy() { + if w.notify.write != 0 { + syscall.Close(w.notify.write) + w.notify.write = 0 + } + if w.notify.read != 0 { + syscall.Close(w.notify.read) + w.notify.read = 0 + } + if w.xkb != nil { + w.xkb.Destroy() + w.xkb = nil + } + C.XDestroyWindow(w.x, w.xw) + C.XCloseDisplay(w.x) +} + +// atom is a wrapper around XInternAtom. Callers should cache the result +// in order to limit round-trips to the X server. +// +func (w *x11Window) atom(name string, onlyIfExists bool) C.Atom { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + flag := C.Bool(C.False) + if onlyIfExists { + flag = C.True + } + return C.XInternAtom(w.x, cname, flag) +} + +// x11EventHandler wraps static variables for the main event loop. +// Its sole purpose is to prevent heap allocation and reduce clutter +// in x11window.loop. +// +type x11EventHandler struct { + w *x11Window + text []byte + xev *C.XEvent +} + +// handleEvents returns true if the window needs to be redrawn. +// +func (h *x11EventHandler) handleEvents() bool { + w := h.w + xev := h.xev + redraw := false + for C.XPending(w.x) != 0 { + C.XNextEvent(w.x, xev) + if C.XFilterEvent(xev, C.None) == C.True { + continue + } + switch _type := (*C.XAnyEvent)(unsafe.Pointer(xev))._type; _type { + case h.w.xkbEventBase: + xkbEvent := (*C.XkbAnyEvent)(unsafe.Pointer(xev)) + switch xkbEvent.xkb_type { + case C.XkbNewKeyboardNotify, C.XkbMapNotify: + if err := h.w.updateXkbKeymap(); err != nil { + panic(err) + } + case C.XkbStateNotify: + state := (*C.XkbStateNotifyEvent)(unsafe.Pointer(xev)) + h.w.xkb.UpdateMask(uint32(state.base_mods), + uint32(state.latched_mods), uint32(state.locked_mods), + uint32(state.base_group), uint32(state.latched_group), + uint32(state.locked_group)) + } + case C.KeyPress, C.KeyRelease: + ks := key.Press + if _type == C.KeyRelease { + ks = key.Release + } + kevt := (*C.XKeyPressedEvent)(unsafe.Pointer(xev)) + for _, e := range h.w.xkb.DispatchKey(uint32(kevt.keycode), ks) { + w.w.Event(e) + } + case C.ButtonPress, C.ButtonRelease: + bevt := (*C.XButtonEvent)(unsafe.Pointer(xev)) + ev := pointer.Event{ + Type: pointer.Press, + Source: pointer.Mouse, + Position: f32.Point{ + X: float32(bevt.x), + Y: float32(bevt.y), + }, + Time: time.Duration(bevt.time) * time.Millisecond, + Modifiers: w.xkb.Modifiers(), + } + if bevt._type == C.ButtonRelease { + ev.Type = pointer.Release + } + var btn pointer.Buttons + const scrollScale = 10 + switch bevt.button { + case C.Button1: + btn = pointer.ButtonPrimary + case C.Button2: + btn = pointer.ButtonTertiary + case C.Button3: + btn = pointer.ButtonSecondary + case C.Button4: + // scroll up + ev.Type = pointer.Scroll + ev.Scroll.Y = -scrollScale + case C.Button5: + // scroll down + ev.Type = pointer.Scroll + ev.Scroll.Y = +scrollScale + case 6: + // http://xahlee.info/linux/linux_x11_mouse_button_number.html + // scroll left + ev.Type = pointer.Scroll + ev.Scroll.X = -scrollScale * 2 + case 7: + // scroll right + ev.Type = pointer.Scroll + ev.Scroll.X = +scrollScale * 2 + default: + continue + } + switch _type { + case C.ButtonPress: + w.pointerBtns |= btn + case C.ButtonRelease: + w.pointerBtns |= btn + } + ev.Buttons = w.pointerBtns + w.w.Event(ev) + w.pointerBtns = 0 + case C.MotionNotify: + mevt := (*C.XMotionEvent)(unsafe.Pointer(xev)) + w.w.Event(pointer.Event{ + Type: pointer.Move, + Source: pointer.Mouse, + Buttons: w.pointerBtns, + Position: f32.Point{ + X: float32(mevt.x), + Y: float32(mevt.y), + }, + Time: time.Duration(mevt.time) * time.Millisecond, + Modifiers: w.xkb.Modifiers(), + }) + case C.Expose: // update + // redraw only on the last expose event + redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0 + case C.FocusIn: + w.w.Event(key.FocusEvent{Focus: true}) + case C.FocusOut: + w.w.Event(key.FocusEvent{Focus: false}) + case C.ConfigureNotify: // window configuration change + cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev)) + w.width = int(cevt.width) + w.height = int(cevt.height) + // redraw will be done by a later expose event + case C.SelectionNotify: + cevt := (*C.XSelectionEvent)(unsafe.Pointer(xev)) + prop := w.atoms.clipboardContent + if cevt.property != prop { + break + } + if cevt.selection != w.atoms.clipboard { + break + } + var text C.XTextProperty + if st := C.XGetTextProperty(w.x, w.xw, &text, prop); st == 0 { + // Failed; ignore. + break + } + if text.format != 8 || text.encoding != w.atoms.utf8string { + // Ignore non-utf-8 encoded strings. + break + } + str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), + C.int(text.nitems)) + w.w.Event(clipboard.Event{Text: str}) + case C.SelectionRequest: + cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev)) + if cevt.selection != w.atoms.clipboard || cevt.property == C.None { + // Unsupported clipboard or obsolete requestor. + break + } + notify := func() { + var xev C.XEvent + ev := (*C.XSelectionEvent)(unsafe.Pointer(&xev)) + *ev = C.XSelectionEvent{ + _type: C.SelectionNotify, + display: cevt.display, + requestor: cevt.requestor, + selection: cevt.selection, + target: cevt.target, + property: cevt.property, + time: cevt.time, + } + C.XSendEvent(w.x, cevt.requestor, 0, 0, &xev) + } + switch cevt.target { + case w.atoms.targets: + // The requestor wants the supported clipboard + // formats. First write the targets... + formats := [...]C.long{ + C.long(w.atoms.targets), + C.long(w.atoms.utf8string), + C.long(w.atoms.plaintext), + // GTK clients need this. + C.long(w.atoms.gtk_text_buffer_contents), + } + C.XChangeProperty(w.x, cevt.requestor, cevt.property, + w.atoms.atom, + 32 /* bitwidth of formats */, C.PropModeReplace, + (*C.uchar)(unsafe.Pointer(&formats)), C.int(len(formats)), + ) + // ...then notify the requestor. + notify() + case w.atoms.plaintext, w.atoms.utf8string, w.atoms.gtk_text_buffer_contents: + content := w.clipboard.content + var ptr *C.uchar + if len(content) > 0 { + ptr = (*C.uchar)(unsafe.Pointer(&content[0])) + } + C.XChangeProperty(w.x, cevt.requestor, cevt.property, + cevt.target, + 8 /* bitwidth */, C.PropModeReplace, + ptr, C.int(len(content)), + ) + notify() + } + case C.ClientMessage: // extensions + cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev)) + switch *(*C.long)(unsafe.Pointer(&cevt.data)) { + case C.long(w.atoms.evDelWindow): + w.dead = true + return false + } + } + } + return redraw +} + +var ( + x11Threads sync.Once +) + +func init() { + x11Driver = newX11Window +} + +func newX11Window(gioWin Callbacks, opts *Options) error { + var err error + + pipe := make([]int, 2) + if err := syscall.Pipe2(pipe, + syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil { + return fmt.Errorf("NewX11Window: failed to create pipe: %w", err) + } + + x11Threads.Do(func() { + if C.XInitThreads() == 0 { + err = errors.New("x11: threads init failed") + } + C.XrmInitialize() + }) + if err != nil { + return err + } + dpy := C.XOpenDisplay(nil) + if dpy == nil { + return errors.New("x11: cannot connect to the X server") + } + var major, minor C.int = C.XkbMajorVersion, C.XkbMinorVersion + var xkbEventBase C.int + if C.XkbQueryExtension(dpy, nil, &xkbEventBase, nil, &major, + &minor) != C.True { + C.XCloseDisplay(dpy) + return errors.New("x11: XkbQueryExtension failed") + } + const bits = C.uint(C.XkbNewKeyboardNotifyMask | C.XkbMapNotifyMask | C.XkbStateNotifyMask) + if C.XkbSelectEvents(dpy, C.XkbUseCoreKbd, bits, bits) != C.True { + C.XCloseDisplay(dpy) + return errors.New("x11: XkbSelectEvents failed") + } + xkb, err := xkb.New() + if err != nil { + C.XCloseDisplay(dpy) + return fmt.Errorf("x11: %v", err) + } + + ppsp := x11DetectUIScale(dpy) + cfg := unit.Metric{PxPerDp: ppsp, PxPerSp: ppsp} + swa := C.XSetWindowAttributes{ + event_mask: C.ExposureMask | C.FocusChangeMask | // update + C.KeyPressMask | C.KeyReleaseMask | // keyboard + C.ButtonPressMask | C.ButtonReleaseMask | // mouse clicks + C.PointerMotionMask | // mouse movement + C.StructureNotifyMask, // resize + background_pixmap: C.None, + override_redirect: C.False, + } + var width, height int + if o := opts.Size; o != nil { + width = cfg.Px(o.Width) + height = cfg.Px(o.Height) + } + win := C.XCreateWindow(dpy, C.XDefaultRootWindow(dpy), + 0, 0, C.uint(width), C.uint(height), + 0, C.CopyFromParent, C.InputOutput, nil, + C.CWEventMask|C.CWBackPixmap|C.CWOverrideRedirect, &swa) + + w := &x11Window{ + w: gioWin, x: dpy, xw: win, + width: width, + height: height, + cfg: cfg, + xkb: xkb, + xkbEventBase: xkbEventBase, + } + w.notify.read = pipe[0] + w.notify.write = pipe[1] + + if err := w.updateXkbKeymap(); err != nil { + w.destroy() + return err + } + + var hints C.XWMHints + hints.input = C.True + hints.flags = C.InputHint + C.XSetWMHints(dpy, win, &hints) + + name := C.CString(filepath.Base(os.Args[0])) + defer C.free(unsafe.Pointer(name)) + wmhints := C.XClassHint{name, name} + C.XSetClassHint(dpy, win, &wmhints) + + w.atoms.utf8string = w.atom("UTF8_STRING", false) + w.atoms.plaintext = w.atom("text/plain;charset=utf-8", false) + w.atoms.gtk_text_buffer_contents = w.atom("GTK_TEXT_BUFFER_CONTENTS", false) + w.atoms.evDelWindow = w.atom("WM_DELETE_WINDOW", false) + w.atoms.clipboard = w.atom("CLIPBOARD", false) + w.atoms.clipboardContent = w.atom("CLIPBOARD_CONTENT", false) + w.atoms.atom = w.atom("ATOM", false) + w.atoms.targets = w.atom("TARGETS", false) + w.atoms.wmName = w.atom("_NET_WM_NAME", false) + w.atoms.wmState = w.atom("_NET_WM_STATE", false) + w.atoms.wmStateFullscreen = w.atom("_NET_WM_STATE_FULLSCREEN", false) + + // extensions + C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1) + + w.Option(opts) + + // make the window visible on the screen + C.XMapWindow(dpy, win) + + go func() { + w.w.SetDriver(w) + w.setStage(system.StageRunning) + w.loop() + w.destroy() + }() + return nil +} + +// detectUIScale reports the system UI scale, or 1.0 if it fails. +func x11DetectUIScale(dpy *C.Display) float32 { + // default fixed DPI value used in most desktop UI toolkits + const defaultDesktopDPI = 96 + var scale float32 = 1.0 + + // Get actual DPI from X resource Xft.dpi (set by GTK and Qt). + // This value is entirely based on user preferences and conflates both + // screen (UI) scaling and font scale. + rms := C.XResourceManagerString(dpy) + if rms != nil { + db := C.XrmGetStringDatabase(rms) + if db != nil { + var ( + t *C.char + v C.XrmValue + ) + if C.XrmGetResource(db, + (*C.char)(unsafe.Pointer(&[]byte("Xft.dpi\x00")[0])), + (*C.char)(unsafe.Pointer(&[]byte("Xft.Dpi\x00")[0])), &t, + &v) != C.False { + if t != nil && C.GoString(t) == "String" { + f, err := strconv.ParseFloat(C.GoString(v.addr), 32) + if err == nil { + scale = float32(f) / defaultDesktopDPI + } + } + } + C.XrmDestroyDatabase(db) + } + } + + return scale +} + +func (w *x11Window) updateXkbKeymap() error { + w.xkb.DestroyKeymapState() + ctx := (*C.struct_xkb_context)(unsafe.Pointer(w.xkb.Ctx)) + xcb := C.XGetXCBConnection(w.x) + if xcb == nil { + return errors.New("x11: XGetXCBConnection failed") + } + xkbDevID := C.xkb_x11_get_core_keyboard_device_id(xcb) + if xkbDevID == -1 { + return errors.New("x11: xkb_x11_get_core_keyboard_device_id failed") + } + keymap := C.xkb_x11_keymap_new_from_device(ctx, xcb, xkbDevID, + C.XKB_KEYMAP_COMPILE_NO_FLAGS) + if keymap == nil { + return errors.New("x11: xkb_x11_keymap_new_from_device failed") + } + state := C.xkb_x11_state_new_from_device(keymap, xcb, xkbDevID) + if state == nil { + C.xkb_keymap_unref(keymap) + return errors.New("x11: xkb_x11_keymap_new_from_device failed") + } + w.xkb.SetKeymap(unsafe.Pointer(keymap), unsafe.Pointer(state)) + return nil +} diff --git a/gio/giold/app/internal/wm/runmain.go b/gio/giold/app/internal/wm/runmain.go new file mode 100644 index 0000000..4617217 --- /dev/null +++ b/gio/giold/app/internal/wm/runmain.go @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build android darwin,ios + +package wm + +// Android only supports non-Java programs as c-shared libraries. +// Unfortunately, Go does not run a program's main function in +// library mode. To make Gio programs simpler and uniform, we'll +// link to the main function here and call it from Java. + +import ( + "sync" + _ "unsafe" // for go:linkname +) + +//go:linkname mainMain main.main +func mainMain() + +var runMainOnce sync.Once + +func runMain() { + runMainOnce.Do(func() { + // Indirect call, since the linker does not know the address of main when + // laying down this package. + fn := mainMain + fn() + }) +} diff --git a/gio/giold/app/internal/wm/wayland_text_input.c b/gio/giold/app/internal/wm/wayland_text_input.c new file mode 100644 index 0000000..de01dd5 --- /dev/null +++ b/gio/giold/app/internal/wm/wayland_text_input.c @@ -0,0 +1,98 @@ +// +build linux,!android,!nowayland freebsd + +/* Generated by wayland-scanner 1.21.0 */ + +/* + * Copyright Ā© 2012, 2013 Intel Corporation + * Copyright Ā© 2015, 2016 Jan Arne Petersen + * Copyright Ā© 2017, 2018 Red Hat, Inc. + * Copyright Ā© 2018 Purism SPC + * + * Permission to use, copy, modify, distribute, and sell this + * software and its documentation for any purpose is hereby granted + * without fee, provided that the above copyright notice appear in + * all copies and that both that copyright notice and this permission + * notice appear in supporting documentation, and that the name of + * the copyright holders not be used in advertising or publicity + * pertaining to distribution of the software without specific, + * written prior permission. The copyright holders make no + * representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied + * warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + * THIS SOFTWARE. + */ + +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_seat_interface; +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface zwp_text_input_v3_interface; + +static const struct wl_interface *text_input_unstable_v3_types[] = { + NULL, + NULL, + NULL, + NULL, + &wl_surface_interface, + &wl_surface_interface, + &zwp_text_input_v3_interface, + &wl_seat_interface, +}; + +static const struct wl_message zwp_text_input_v3_requests[] = { + { "destroy", "", text_input_unstable_v3_types + 0 }, + { "enable", "", text_input_unstable_v3_types + 0 }, + { "disable", "", text_input_unstable_v3_types + 0 }, + { "set_surrounding_text", "sii", text_input_unstable_v3_types + 0 }, + { "set_text_change_cause", "u", text_input_unstable_v3_types + 0 }, + { "set_content_type", "uu", text_input_unstable_v3_types + 0 }, + { "set_cursor_rectangle", "iiii", text_input_unstable_v3_types + 0 }, + { "commit", "", text_input_unstable_v3_types + 0 }, +}; + +static const struct wl_message zwp_text_input_v3_events[] = { + { "enter", "o", text_input_unstable_v3_types + 4 }, + { "leave", "o", text_input_unstable_v3_types + 5 }, + { "preedit_string", "?sii", text_input_unstable_v3_types + 0 }, + { "commit_string", "?s", text_input_unstable_v3_types + 0 }, + { "delete_surrounding_text", "uu", text_input_unstable_v3_types + 0 }, + { "done", "u", text_input_unstable_v3_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwp_text_input_v3_interface = { + "zwp_text_input_v3", 1, + 8, zwp_text_input_v3_requests, + 6, zwp_text_input_v3_events, +}; + +static const struct wl_message zwp_text_input_manager_v3_requests[] = { + { "destroy", "", text_input_unstable_v3_types + 0 }, + { "get_text_input", "no", text_input_unstable_v3_types + 6 }, +}; + +WL_PRIVATE const struct wl_interface zwp_text_input_manager_v3_interface = { + "zwp_text_input_manager_v3", 1, + 2, zwp_text_input_manager_v3_requests, + 0, NULL, +}; + diff --git a/gio/giold/app/internal/wm/wayland_text_input.h b/gio/giold/app/internal/wm/wayland_text_input.h new file mode 100644 index 0000000..b1bb886 --- /dev/null +++ b/gio/giold/app/internal/wm/wayland_text_input.h @@ -0,0 +1,838 @@ +/* Generated by wayland-scanner 1.21.0 */ + +#ifndef TEXT_INPUT_UNSTABLE_V3_CLIENT_PROTOCOL_H +#define TEXT_INPUT_UNSTABLE_V3_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_text_input_unstable_v3 The text_input_unstable_v3 protocol + * Protocol for composing text + * + * @section page_desc_text_input_unstable_v3 Description + * + * This protocol allows compositors to act as input methods and to send text + * to applications. A text input object is used to manage state of what are + * typically text entry fields in the application. + * + * This document adheres to the RFC 2119 when using words like "must", + * "should", "may", etc. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + * + * @section page_ifaces_text_input_unstable_v3 Interfaces + * - @subpage page_iface_zwp_text_input_v3 - text input + * - @subpage page_iface_zwp_text_input_manager_v3 - text input manager + * @section page_copyright_text_input_unstable_v3 Copyright + *
+ *
+ * Copyright Ā© 2012, 2013 Intel Corporation
+ * Copyright Ā© 2015, 2016 Jan Arne Petersen
+ * Copyright Ā© 2017, 2018 Red Hat, Inc.
+ * Copyright Ā© 2018       Purism SPC
+ *
+ * Permission to use, copy, modify, distribute, and sell this
+ * software and its documentation for any purpose is hereby granted
+ * without fee, provided that the above copyright notice appear in
+ * all copies and that both that copyright notice and this permission
+ * notice appear in supporting documentation, and that the name of
+ * the copyright holders not be used in advertising or publicity
+ * pertaining to distribution of the software without specific,
+ * written prior permission.  The copyright holders make no
+ * representations about the suitability of this software for any
+ * purpose.  It is provided "as is" without express or implied
+ * warranty.
+ *
+ * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+ * THIS SOFTWARE.
+ * 
+ */ +struct wl_seat; +struct wl_surface; +struct zwp_text_input_manager_v3; +struct zwp_text_input_v3; + +#ifndef ZWP_TEXT_INPUT_V3_INTERFACE +#define ZWP_TEXT_INPUT_V3_INTERFACE +/** + * @page page_iface_zwp_text_input_v3 zwp_text_input_v3 + * @section page_iface_zwp_text_input_v3_desc Description + * + * The zwp_text_input_v3 interface represents text input and input methods + * associated with a seat. It provides enter/leave events to follow the + * text input focus for a seat. + * + * Requests are used to enable/disable the text-input object and set + * state information like surrounding and selected text or the content type. + * The information about the entered text is sent to the text-input object + * via the preedit_string and commit_string events. + * + * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices + * must not point to middle bytes inside a code point: they must either + * point to the first byte of a code point or to the end of the buffer. + * Lengths must be measured between two valid indices. + * + * Focus moving throughout surfaces will result in the emission of + * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused + * surface must commit zwp_text_input_v3.enable and + * zwp_text_input_v3.disable requests as the keyboard focus moves across + * editable and non-editable elements of the UI. Those two requests are not + * expected to be paired with each other, the compositor must be able to + * handle consecutive series of the same request. + * + * State is sent by the state requests (set_surrounding_text, + * set_content_type and set_cursor_rectangle) and a commit request. After an + * enter event or disable request all state information is invalidated and + * needs to be resent by the client. + * @section page_iface_zwp_text_input_v3_api API + * See @ref iface_zwp_text_input_v3. + */ +/** + * @defgroup iface_zwp_text_input_v3 The zwp_text_input_v3 interface + * + * The zwp_text_input_v3 interface represents text input and input methods + * associated with a seat. It provides enter/leave events to follow the + * text input focus for a seat. + * + * Requests are used to enable/disable the text-input object and set + * state information like surrounding and selected text or the content type. + * The information about the entered text is sent to the text-input object + * via the preedit_string and commit_string events. + * + * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices + * must not point to middle bytes inside a code point: they must either + * point to the first byte of a code point or to the end of the buffer. + * Lengths must be measured between two valid indices. + * + * Focus moving throughout surfaces will result in the emission of + * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused + * surface must commit zwp_text_input_v3.enable and + * zwp_text_input_v3.disable requests as the keyboard focus moves across + * editable and non-editable elements of the UI. Those two requests are not + * expected to be paired with each other, the compositor must be able to + * handle consecutive series of the same request. + * + * State is sent by the state requests (set_surrounding_text, + * set_content_type and set_cursor_rectangle) and a commit request. After an + * enter event or disable request all state information is invalidated and + * needs to be resent by the client. + */ +extern const struct wl_interface zwp_text_input_v3_interface; +#endif +#ifndef ZWP_TEXT_INPUT_MANAGER_V3_INTERFACE +#define ZWP_TEXT_INPUT_MANAGER_V3_INTERFACE +/** + * @page page_iface_zwp_text_input_manager_v3 zwp_text_input_manager_v3 + * @section page_iface_zwp_text_input_manager_v3_desc Description + * + * A factory for text-input objects. This object is a global singleton. + * @section page_iface_zwp_text_input_manager_v3_api API + * See @ref iface_zwp_text_input_manager_v3. + */ +/** + * @defgroup iface_zwp_text_input_manager_v3 The zwp_text_input_manager_v3 interface + * + * A factory for text-input objects. This object is a global singleton. + */ +extern const struct wl_interface zwp_text_input_manager_v3_interface; +#endif + +#ifndef ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM +#define ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM +/** + * @ingroup iface_zwp_text_input_v3 + * text change reason + * + * Reason for the change of surrounding text or cursor posision. + */ +enum zwp_text_input_v3_change_cause { + /** + * input method caused the change + */ + ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD = 0, + /** + * something else than the input method caused the change + */ + ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER = 1, +}; +#endif /* ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM */ + +#ifndef ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM +#define ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM +/** + * @ingroup iface_zwp_text_input_v3 + * content hint + * + * Content hint is a bitmask to allow to modify the behavior of the text + * input. + */ +enum zwp_text_input_v3_content_hint { + /** + * no special behavior + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE = 0x0, + /** + * suggest word completions + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION = 0x1, + /** + * suggest word corrections + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK = 0x2, + /** + * switch to uppercase letters at the start of a sentence + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION = 0x4, + /** + * prefer lowercase letters + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_LOWERCASE = 0x8, + /** + * prefer uppercase letters + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_UPPERCASE = 0x10, + /** + * prefer casing for titles and headings (can be language dependent) + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE = 0x20, + /** + * characters should be hidden + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_HIDDEN_TEXT = 0x40, + /** + * typed text should not be stored + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA = 0x80, + /** + * just Latin characters should be entered + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_LATIN = 0x100, + /** + * the text input is multiline + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_MULTILINE = 0x200, +}; +#endif /* ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM */ + +#ifndef ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM +#define ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM +/** + * @ingroup iface_zwp_text_input_v3 + * content purpose + * + * The content purpose allows to specify the primary purpose of a text + * input. + * + * This allows an input method to show special purpose input panels with + * extra characters or to disallow some characters. + */ +enum zwp_text_input_v3_content_purpose { + /** + * default input, allowing all characters + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL = 0, + /** + * allow only alphabetic characters + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA = 1, + /** + * allow only digits + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS = 2, + /** + * input a number (including decimal separator and sign) + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER = 3, + /** + * input a phone number + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE = 4, + /** + * input an URL + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL = 5, + /** + * input an email address + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL = 6, + /** + * input a name of a person + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME = 7, + /** + * input a password (combine with sensitive_data hint) + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD = 8, + /** + * input is a numeric password (combine with sensitive_data hint) + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PIN = 9, + /** + * input a date + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATE = 10, + /** + * input a time + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TIME = 11, + /** + * input a date and time + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATETIME = 12, + /** + * input for a terminal + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL = 13, +}; +#endif /* ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM */ + +/** + * @ingroup iface_zwp_text_input_v3 + * @struct zwp_text_input_v3_listener + */ +struct zwp_text_input_v3_listener { + /** + * enter event + * + * Notification that this seat's text-input focus is on a certain + * surface. + * + * If client has created multiple text input objects, compositor + * must send this event to all of them. + * + * When the seat has the keyboard capability the text-input focus + * follows the keyboard focus. This event sets the current surface + * for the text-input object. + */ + void (*enter)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + struct wl_surface *surface); + /** + * leave event + * + * Notification that this seat's text-input focus is no longer on + * a certain surface. The client should reset any preedit string + * previously set. + * + * The leave notification clears the current surface. It is sent + * before the enter notification for the new focus. After leave + * event, compositor must ignore requests from any text input + * instances until next enter event. + * + * When the seat has the keyboard capability the text-input focus + * follows the keyboard focus. + */ + void (*leave)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + struct wl_surface *surface); + /** + * pre-edit + * + * Notify when a new composing text (pre-edit) should be set at + * the current cursor position. Any previously set composing text + * must be removed. Any previously existing selected text must be + * removed. + * + * The argument text contains the pre-edit string buffer. + * + * The parameters cursor_begin and cursor_end are counted in bytes + * relative to the beginning of the submitted text buffer. Cursor + * should be hidden when both are equal to -1. + * + * They could be represented by the client as a line if both values + * are the same, or as a text highlight otherwise. + * + * Values set with this event are double-buffered. They must be + * applied and reset to initial on the next zwp_text_input_v3.done + * event. + * + * The initial value of text is an empty string, and cursor_begin, + * cursor_end and cursor_hidden are all 0. + */ + void (*preedit_string)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + const char *text, + int32_t cursor_begin, + int32_t cursor_end); + /** + * text commit + * + * Notify when text should be inserted into the editor widget. + * The text to commit could be either just a single character after + * a key press or the result of some composing (pre-edit). + * + * Values set with this event are double-buffered. They must be + * applied and reset to initial on the next zwp_text_input_v3.done + * event. + * + * The initial value of text is an empty string. + */ + void (*commit_string)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + const char *text); + /** + * delete surrounding text + * + * Notify when the text around the current cursor position should + * be deleted. + * + * Before_length and after_length are the number of bytes before + * and after the current cursor index (excluding the selection) to + * delete. + * + * If a preedit text is present, in effect before_length is counted + * from the beginning of it, and after_length from its end (see + * done event sequence). + * + * Values set with this event are double-buffered. They must be + * applied and reset to initial on the next zwp_text_input_v3.done + * event. + * + * The initial values of both before_length and after_length are 0. + * @param before_length length of text before current cursor position + * @param after_length length of text after current cursor position + */ + void (*delete_surrounding_text)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t before_length, + uint32_t after_length); + /** + * apply changes + * + * Instruct the application to apply changes to state requested + * by the preedit_string, commit_string and delete_surrounding_text + * events. The state relating to these events is double-buffered, + * and each one modifies the pending state. This event replaces the + * current state with the pending state. + * + * The application must proceed by evaluating the changes in the + * following order: + * + * 1. Replace existing preedit string with the cursor. 2. Delete + * requested surrounding text. 3. Insert commit string with the + * cursor at its end. 4. Calculate surrounding text to send. 5. + * Insert new preedit text in cursor position. 6. Place cursor + * inside preedit text. + * + * The serial number reflects the last state of the + * zwp_text_input_v3 object known to the compositor. The value of + * the serial argument must be equal to the number of commit + * requests already issued on that object. + * + * When the client receives a done event with a serial different + * than the number of past commit requests, it must proceed with + * evaluating and applying the changes as normal, except it should + * not change the current state of the zwp_text_input_v3 object. + * All pending state requests (set_surrounding_text, + * set_content_type and set_cursor_rectangle) on the + * zwp_text_input_v3 object should be sent and committed after + * receiving a zwp_text_input_v3.done event with a matching serial. + */ + void (*done)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t serial); +}; + +/** + * @ingroup iface_zwp_text_input_v3 + */ +static inline int +zwp_text_input_v3_add_listener(struct zwp_text_input_v3 *zwp_text_input_v3, + const struct zwp_text_input_v3_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zwp_text_input_v3, + (void (**)(void)) listener, data); +} + +#define ZWP_TEXT_INPUT_V3_DESTROY 0 +#define ZWP_TEXT_INPUT_V3_ENABLE 1 +#define ZWP_TEXT_INPUT_V3_DISABLE 2 +#define ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT 3 +#define ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE 4 +#define ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE 5 +#define ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE 6 +#define ZWP_TEXT_INPUT_V3_COMMIT 7 + +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_ENTER_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_LEAVE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_PREEDIT_STRING_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_COMMIT_STRING_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DELETE_SURROUNDING_TEXT_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DONE_SINCE_VERSION 1 + +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_ENABLE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DISABLE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_COMMIT_SINCE_VERSION 1 + +/** @ingroup iface_zwp_text_input_v3 */ +static inline void +zwp_text_input_v3_set_user_data(struct zwp_text_input_v3 *zwp_text_input_v3, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwp_text_input_v3, user_data); +} + +/** @ingroup iface_zwp_text_input_v3 */ +static inline void * +zwp_text_input_v3_get_user_data(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwp_text_input_v3); +} + +static inline uint32_t +zwp_text_input_v3_get_version(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + return wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Destroy the wp_text_input object. Also disables all surfaces enabled + * through this wp_text_input object. + */ +static inline void +zwp_text_input_v3_destroy(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Requests text input on the surface previously obtained from the enter + * event. + * + * This request must be issued every time the active text input changes + * to a new one, including within the current surface. Use + * zwp_text_input_v3.disable when there is no longer any input focus on + * the current surface. + * + * Clients must not enable more than one text input on the single seat + * and should disable the current text input before enabling the new one. + * At most one instance of text input may be in enabled state per instance, + * Requests to enable the another text input when some text input is active + * must be ignored by compositor. + * + * This request resets all state associated with previous enable, disable, + * set_surrounding_text, set_text_change_cause, set_content_type, and + * set_cursor_rectangle requests, as well as the state associated with + * preedit_string, commit_string, and delete_surrounding_text events. + * + * The set_surrounding_text, set_content_type and set_cursor_rectangle + * requests must follow if the text input supports the necessary + * functionality. + * + * State set with this request is double-buffered. It will get applied on + * the next zwp_text_input_v3.commit request, and stay valid until the + * next committed enable or disable request. + * + * The changes must be applied by the compositor after issuing a + * zwp_text_input_v3.commit request. + */ +static inline void +zwp_text_input_v3_enable(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_ENABLE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Explicitly disable text input on the current surface (typically when + * there is no focus on any text entry inside the surface). + * + * State set with this request is double-buffered. It will get applied on + * the next zwp_text_input_v3.commit request. + */ +static inline void +zwp_text_input_v3_disable(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_DISABLE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Sets the surrounding plain text around the input, excluding the preedit + * text. + * + * The client should notify the compositor of any changes in any of the + * values carried with this request, including changes caused by handling + * incoming text-input events as well as changes caused by other + * mechanisms like keyboard typing. + * + * If the client is unaware of the text around the cursor, it should not + * issue this request, to signify lack of support to the compositor. + * + * Text is UTF-8 encoded, and should include the cursor position, the + * complete selection and additional characters before and after them. + * There is a maximum length of wayland messages, so text can not be + * longer than 4000 bytes. + * + * Cursor is the byte offset of the cursor within text buffer. + * + * Anchor is the byte offset of the selection anchor within text buffer. + * If there is no selected text, anchor is the same as cursor. + * + * If any preedit text is present, it is replaced with a cursor for the + * purpose of this event. + * + * Values set with this request are double-buffered. They will get applied + * on the next zwp_text_input_v3.commit request, and stay valid until the + * next committed enable or disable request. + * + * The initial state for affected fields is empty, meaning that the text + * input does not support sending surrounding text. If the empty values + * get applied, subsequent attempts to change them may have no effect. + */ +static inline void +zwp_text_input_v3_set_surrounding_text(struct zwp_text_input_v3 *zwp_text_input_v3, const char *text, int32_t cursor, int32_t anchor) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, text, cursor, anchor); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Tells the compositor why the text surrounding the cursor changed. + * + * Whenever the client detects an external change in text, cursor, or + * anchor posision, it must issue this request to the compositor. This + * request is intended to give the input method a chance to update the + * preedit text in an appropriate way, e.g. by removing it when the user + * starts typing with a keyboard. + * + * cause describes the source of the change. + * + * The value set with this request is double-buffered. It must be applied + * and reset to initial at the next zwp_text_input_v3.commit request. + * + * The initial value of cause is input_method. + */ +static inline void +zwp_text_input_v3_set_text_change_cause(struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t cause) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, cause); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Sets the content purpose and content hint. While the purpose is the + * basic purpose of an input field, the hint flags allow to modify some of + * the behavior. + * + * Values set with this request are double-buffered. They will get applied + * on the next zwp_text_input_v3.commit request. + * Subsequent attempts to update them may have no effect. The values + * remain valid until the next committed enable or disable request. + * + * The initial value for hint is none, and the initial value for purpose + * is normal. + */ +static inline void +zwp_text_input_v3_set_content_type(struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t hint, uint32_t purpose) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, hint, purpose); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Marks an area around the cursor as a x, y, width, height rectangle in + * surface local coordinates. + * + * Allows the compositor to put a window with word suggestions near the + * cursor, without obstructing the text being input. + * + * If the client is unaware of the position of edited text, it should not + * issue this request, to signify lack of support to the compositor. + * + * Values set with this request are double-buffered. They will get applied + * on the next zwp_text_input_v3.commit request, and stay valid until the + * next committed enable or disable request. + * + * The initial values describing a cursor rectangle are empty. That means + * the text input does not support describing the cursor area. If the + * empty values get applied, subsequent attempts to change them may have + * no effect. + */ +static inline void +zwp_text_input_v3_set_cursor_rectangle(struct zwp_text_input_v3 *zwp_text_input_v3, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, x, y, width, height); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Atomically applies state changes recently sent to the compositor. + * + * The commit request establishes and updates the state of the client, and + * must be issued after any changes to apply them. + * + * Text input state (enabled status, content purpose, content hint, + * surrounding text and change cause, cursor rectangle) is conceptually + * double-buffered within the context of a text input, i.e. between a + * committed enable request and the following committed enable or disable + * request. + * + * Protocol requests modify the pending state, as opposed to the current + * state in use by the input method. A commit request atomically applies + * all pending state, replacing the current state. After commit, the new + * pending state is as documented for each related request. + * + * Requests are applied in the order of arrival. + * + * Neither current nor pending state are modified unless noted otherwise. + * + * The compositor must count the number of commit requests coming from + * each zwp_text_input_v3 object and use the count as the serial in done + * events. + */ +static inline void +zwp_text_input_v3_commit(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_COMMIT, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0); +} + +#define ZWP_TEXT_INPUT_MANAGER_V3_DESTROY 0 +#define ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT 1 + + +/** + * @ingroup iface_zwp_text_input_manager_v3 + */ +#define ZWP_TEXT_INPUT_MANAGER_V3_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_manager_v3 + */ +#define ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT_SINCE_VERSION 1 + +/** @ingroup iface_zwp_text_input_manager_v3 */ +static inline void +zwp_text_input_manager_v3_set_user_data(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwp_text_input_manager_v3, user_data); +} + +/** @ingroup iface_zwp_text_input_manager_v3 */ +static inline void * +zwp_text_input_manager_v3_get_user_data(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwp_text_input_manager_v3); +} + +static inline uint32_t +zwp_text_input_manager_v3_get_version(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) +{ + return wl_proxy_get_version((struct wl_proxy *) zwp_text_input_manager_v3); +} + +/** + * @ingroup iface_zwp_text_input_manager_v3 + * + * Destroy the wp_text_input_manager object. + */ +static inline void +zwp_text_input_manager_v3_destroy(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_manager_v3, + ZWP_TEXT_INPUT_MANAGER_V3_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_manager_v3), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_zwp_text_input_manager_v3 + * + * Creates a new text-input object for a given seat. + */ +static inline struct zwp_text_input_v3 * +zwp_text_input_manager_v3_get_text_input(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3, struct wl_seat *seat) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_manager_v3, + ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT, &zwp_text_input_v3_interface, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_manager_v3), 0, NULL, seat); + + return (struct zwp_text_input_v3 *) id; +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/gio/giold/app/internal/wm/wayland_xdg_decoration.c b/gio/giold/app/internal/wm/wayland_xdg_decoration.c new file mode 100644 index 0000000..78f9328 --- /dev/null +++ b/gio/giold/app/internal/wm/wayland_xdg_decoration.c @@ -0,0 +1,77 @@ +// +build linux,!android,!nowayland freebsd + +/* Generated by wayland-scanner 1.21.0 */ + +/* + * Copyright Ā© 2018 Simon Ser + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface xdg_toplevel_interface; +extern const struct wl_interface zxdg_toplevel_decoration_v1_interface; + +static const struct wl_interface *xdg_decoration_unstable_v1_types[] = { + NULL, + &zxdg_toplevel_decoration_v1_interface, + &xdg_toplevel_interface, +}; + +static const struct wl_message zxdg_decoration_manager_v1_requests[] = { + { "destroy", "", xdg_decoration_unstable_v1_types + 0 }, + { "get_toplevel_decoration", "no", xdg_decoration_unstable_v1_types + 1 }, +}; + +WL_PRIVATE const struct wl_interface zxdg_decoration_manager_v1_interface = { + "zxdg_decoration_manager_v1", 1, + 2, zxdg_decoration_manager_v1_requests, + 0, NULL, +}; + +static const struct wl_message zxdg_toplevel_decoration_v1_requests[] = { + { "destroy", "", xdg_decoration_unstable_v1_types + 0 }, + { "set_mode", "u", xdg_decoration_unstable_v1_types + 0 }, + { "unset_mode", "", xdg_decoration_unstable_v1_types + 0 }, +}; + +static const struct wl_message zxdg_toplevel_decoration_v1_events[] = { + { "configure", "u", xdg_decoration_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zxdg_toplevel_decoration_v1_interface = { + "zxdg_toplevel_decoration_v1", 1, + 3, zxdg_toplevel_decoration_v1_requests, + 1, zxdg_toplevel_decoration_v1_events, +}; + diff --git a/gio/giold/app/internal/wm/wayland_xdg_decoration.h b/gio/giold/app/internal/wm/wayland_xdg_decoration.h new file mode 100644 index 0000000..286c236 --- /dev/null +++ b/gio/giold/app/internal/wm/wayland_xdg_decoration.h @@ -0,0 +1,378 @@ +/* Generated by wayland-scanner 1.21.0 */ + +#ifndef XDG_DECORATION_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define XDG_DECORATION_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_xdg_decoration_unstable_v1 The xdg_decoration_unstable_v1 protocol + * @section page_ifaces_xdg_decoration_unstable_v1 Interfaces + * - @subpage page_iface_zxdg_decoration_manager_v1 - window decoration manager + * - @subpage page_iface_zxdg_toplevel_decoration_v1 - decoration object for a toplevel surface + * @section page_copyright_xdg_decoration_unstable_v1 Copyright + *
+ *
+ * Copyright Ā© 2018 Simon Ser
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * 
+ */ +struct xdg_toplevel; +struct zxdg_decoration_manager_v1; +struct zxdg_toplevel_decoration_v1; + +#ifndef ZXDG_DECORATION_MANAGER_V1_INTERFACE +#define ZXDG_DECORATION_MANAGER_V1_INTERFACE +/** + * @page page_iface_zxdg_decoration_manager_v1 zxdg_decoration_manager_v1 + * @section page_iface_zxdg_decoration_manager_v1_desc Description + * + * This interface allows a compositor to announce support for server-side + * decorations. + * + * A window decoration is a set of window controls as deemed appropriate by + * the party managing them, such as user interface components used to move, + * resize and change a window's state. + * + * A client can use this protocol to request being decorated by a supporting + * compositor. + * + * If compositor and client do not negotiate the use of a server-side + * decoration using this protocol, clients continue to self-decorate as they + * see fit. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + * @section page_iface_zxdg_decoration_manager_v1_api API + * See @ref iface_zxdg_decoration_manager_v1. + */ +/** + * @defgroup iface_zxdg_decoration_manager_v1 The zxdg_decoration_manager_v1 interface + * + * This interface allows a compositor to announce support for server-side + * decorations. + * + * A window decoration is a set of window controls as deemed appropriate by + * the party managing them, such as user interface components used to move, + * resize and change a window's state. + * + * A client can use this protocol to request being decorated by a supporting + * compositor. + * + * If compositor and client do not negotiate the use of a server-side + * decoration using this protocol, clients continue to self-decorate as they + * see fit. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + */ +extern const struct wl_interface zxdg_decoration_manager_v1_interface; +#endif +#ifndef ZXDG_TOPLEVEL_DECORATION_V1_INTERFACE +#define ZXDG_TOPLEVEL_DECORATION_V1_INTERFACE +/** + * @page page_iface_zxdg_toplevel_decoration_v1 zxdg_toplevel_decoration_v1 + * @section page_iface_zxdg_toplevel_decoration_v1_desc Description + * + * The decoration object allows the compositor to toggle server-side window + * decorations for a toplevel surface. The client can request to switch to + * another mode. + * + * The xdg_toplevel_decoration object must be destroyed before its + * xdg_toplevel. + * @section page_iface_zxdg_toplevel_decoration_v1_api API + * See @ref iface_zxdg_toplevel_decoration_v1. + */ +/** + * @defgroup iface_zxdg_toplevel_decoration_v1 The zxdg_toplevel_decoration_v1 interface + * + * The decoration object allows the compositor to toggle server-side window + * decorations for a toplevel surface. The client can request to switch to + * another mode. + * + * The xdg_toplevel_decoration object must be destroyed before its + * xdg_toplevel. + */ +extern const struct wl_interface zxdg_toplevel_decoration_v1_interface; +#endif + +#define ZXDG_DECORATION_MANAGER_V1_DESTROY 0 +#define ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION 1 + + +/** + * @ingroup iface_zxdg_decoration_manager_v1 + */ +#define ZXDG_DECORATION_MANAGER_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_decoration_manager_v1 + */ +#define ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION_SINCE_VERSION 1 + +/** @ingroup iface_zxdg_decoration_manager_v1 */ +static inline void +zxdg_decoration_manager_v1_set_user_data(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zxdg_decoration_manager_v1, user_data); +} + +/** @ingroup iface_zxdg_decoration_manager_v1 */ +static inline void * +zxdg_decoration_manager_v1_get_user_data(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zxdg_decoration_manager_v1); +} + +static inline uint32_t +zxdg_decoration_manager_v1_get_version(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zxdg_decoration_manager_v1); +} + +/** + * @ingroup iface_zxdg_decoration_manager_v1 + * + * Destroy the decoration manager. This doesn't destroy objects created + * with the manager. + */ +static inline void +zxdg_decoration_manager_v1_destroy(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zxdg_decoration_manager_v1, + ZXDG_DECORATION_MANAGER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_decoration_manager_v1), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_zxdg_decoration_manager_v1 + * + * Create a new decoration object associated with the given toplevel. + * + * Creating an xdg_toplevel_decoration from an xdg_toplevel which has a + * buffer attached or committed is a client error, and any attempts by a + * client to attach or manipulate a buffer prior to the first + * xdg_toplevel_decoration.configure event must also be treated as + * errors. + */ +static inline struct zxdg_toplevel_decoration_v1 * +zxdg_decoration_manager_v1_get_toplevel_decoration(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1, struct xdg_toplevel *toplevel) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) zxdg_decoration_manager_v1, + ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION, &zxdg_toplevel_decoration_v1_interface, wl_proxy_get_version((struct wl_proxy *) zxdg_decoration_manager_v1), 0, NULL, toplevel); + + return (struct zxdg_toplevel_decoration_v1 *) id; +} + +#ifndef ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM +#define ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM +enum zxdg_toplevel_decoration_v1_error { + /** + * xdg_toplevel has a buffer attached before configure + */ + ZXDG_TOPLEVEL_DECORATION_V1_ERROR_UNCONFIGURED_BUFFER = 0, + /** + * xdg_toplevel already has a decoration object + */ + ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ALREADY_CONSTRUCTED = 1, + /** + * xdg_toplevel destroyed before the decoration object + */ + ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ORPHANED = 2, +}; +#endif /* ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM */ + +#ifndef ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM +#define ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * window decoration modes + * + * These values describe window decoration modes. + */ +enum zxdg_toplevel_decoration_v1_mode { + /** + * no server-side window decoration + */ + ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE = 1, + /** + * server-side window decoration + */ + ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE = 2, +}; +#endif /* ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM */ + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * @struct zxdg_toplevel_decoration_v1_listener + */ +struct zxdg_toplevel_decoration_v1_listener { + /** + * suggest a surface change + * + * The configure event asks the client to change its decoration + * mode. The configured state should not be applied immediately. + * Clients must send an ack_configure in response to this event. + * See xdg_surface.configure and xdg_surface.ack_configure for + * details. + * + * A configure event can be sent at any time. The specified mode + * must be obeyed by the client. + * @param mode the decoration mode + */ + void (*configure)(void *data, + struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, + uint32_t mode); +}; + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +static inline int +zxdg_toplevel_decoration_v1_add_listener(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, + const struct zxdg_toplevel_decoration_v1_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zxdg_toplevel_decoration_v1, + (void (**)(void)) listener, data); +} + +#define ZXDG_TOPLEVEL_DECORATION_V1_DESTROY 0 +#define ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE 1 +#define ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE 2 + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +#define ZXDG_TOPLEVEL_DECORATION_V1_CONFIGURE_SINCE_VERSION 1 + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +#define ZXDG_TOPLEVEL_DECORATION_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +#define ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +#define ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE_SINCE_VERSION 1 + +/** @ingroup iface_zxdg_toplevel_decoration_v1 */ +static inline void +zxdg_toplevel_decoration_v1_set_user_data(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zxdg_toplevel_decoration_v1, user_data); +} + +/** @ingroup iface_zxdg_toplevel_decoration_v1 */ +static inline void * +zxdg_toplevel_decoration_v1_get_user_data(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zxdg_toplevel_decoration_v1); +} + +static inline uint32_t +zxdg_toplevel_decoration_v1_get_version(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zxdg_toplevel_decoration_v1); +} + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * + * Switch back to a mode without any server-side decorations at the next + * commit. + */ +static inline void +zxdg_toplevel_decoration_v1_destroy(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zxdg_toplevel_decoration_v1, + ZXDG_TOPLEVEL_DECORATION_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_toplevel_decoration_v1), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * + * Set the toplevel surface decoration mode. This informs the compositor + * that the client prefers the provided decoration mode. + * + * After requesting a decoration mode, the compositor will respond by + * emitting an xdg_surface.configure event. The client should then update + * its content, drawing it without decorations if the received mode is + * server-side decorations. The client must also acknowledge the configure + * when committing the new content (see xdg_surface.ack_configure). + * + * The compositor can decide not to use the client's mode and enforce a + * different mode instead. + * + * Clients whose decoration mode depend on the xdg_toplevel state may send + * a set_mode request in response to an xdg_surface.configure event and wait + * for the next xdg_surface.configure event to prevent unwanted state. + * Such clients are responsible for preventing configure loops and must + * make sure not to send multiple successive set_mode requests with the + * same decoration mode. + */ +static inline void +zxdg_toplevel_decoration_v1_set_mode(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, uint32_t mode) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zxdg_toplevel_decoration_v1, + ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_toplevel_decoration_v1), 0, mode); +} + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * + * Unset the toplevel surface decoration mode. This informs the compositor + * that the client doesn't prefer a particular decoration mode. + * + * This request has the same semantics as set_mode. + */ +static inline void +zxdg_toplevel_decoration_v1_unset_mode(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zxdg_toplevel_decoration_v1, + ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_toplevel_decoration_v1), 0); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/gio/giold/app/internal/wm/wayland_xdg_shell.c b/gio/giold/app/internal/wm/wayland_xdg_shell.c new file mode 100644 index 0000000..4ed2659 --- /dev/null +++ b/gio/giold/app/internal/wm/wayland_xdg_shell.c @@ -0,0 +1,185 @@ +// +build linux,!android,!nowayland freebsd + +/* Generated by wayland-scanner 1.21.0 */ + +/* + * Copyright Ā© 2008-2013 Kristian HĆøgsberg + * Copyright Ā© 2013 Rafael Antognolli + * Copyright Ā© 2013 Jasper St. Pierre + * Copyright Ā© 2010-2013 Intel Corporation + * Copyright Ā© 2015-2017 Samsung Electronics Co., Ltd + * Copyright Ā© 2015-2017 Red Hat Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_output_interface; +extern const struct wl_interface wl_seat_interface; +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface xdg_popup_interface; +extern const struct wl_interface xdg_positioner_interface; +extern const struct wl_interface xdg_surface_interface; +extern const struct wl_interface xdg_toplevel_interface; + +static const struct wl_interface *xdg_shell_types[] = { + NULL, + NULL, + NULL, + NULL, + &xdg_positioner_interface, + &xdg_surface_interface, + &wl_surface_interface, + &xdg_toplevel_interface, + &xdg_popup_interface, + &xdg_surface_interface, + &xdg_positioner_interface, + &xdg_toplevel_interface, + &wl_seat_interface, + NULL, + NULL, + NULL, + &wl_seat_interface, + NULL, + &wl_seat_interface, + NULL, + NULL, + &wl_output_interface, + &wl_seat_interface, + NULL, + &xdg_positioner_interface, + NULL, +}; + +static const struct wl_message xdg_wm_base_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "create_positioner", "n", xdg_shell_types + 4 }, + { "get_xdg_surface", "no", xdg_shell_types + 5 }, + { "pong", "u", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_wm_base_events[] = { + { "ping", "u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_wm_base_interface = { + "xdg_wm_base", 5, + 4, xdg_wm_base_requests, + 1, xdg_wm_base_events, +}; + +static const struct wl_message xdg_positioner_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "set_size", "ii", xdg_shell_types + 0 }, + { "set_anchor_rect", "iiii", xdg_shell_types + 0 }, + { "set_anchor", "u", xdg_shell_types + 0 }, + { "set_gravity", "u", xdg_shell_types + 0 }, + { "set_constraint_adjustment", "u", xdg_shell_types + 0 }, + { "set_offset", "ii", xdg_shell_types + 0 }, + { "set_reactive", "3", xdg_shell_types + 0 }, + { "set_parent_size", "3ii", xdg_shell_types + 0 }, + { "set_parent_configure", "3u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_positioner_interface = { + "xdg_positioner", 5, + 10, xdg_positioner_requests, + 0, NULL, +}; + +static const struct wl_message xdg_surface_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "get_toplevel", "n", xdg_shell_types + 7 }, + { "get_popup", "n?oo", xdg_shell_types + 8 }, + { "set_window_geometry", "iiii", xdg_shell_types + 0 }, + { "ack_configure", "u", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_surface_events[] = { + { "configure", "u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_surface_interface = { + "xdg_surface", 5, + 5, xdg_surface_requests, + 1, xdg_surface_events, +}; + +static const struct wl_message xdg_toplevel_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "set_parent", "?o", xdg_shell_types + 11 }, + { "set_title", "s", xdg_shell_types + 0 }, + { "set_app_id", "s", xdg_shell_types + 0 }, + { "show_window_menu", "ouii", xdg_shell_types + 12 }, + { "move", "ou", xdg_shell_types + 16 }, + { "resize", "ouu", xdg_shell_types + 18 }, + { "set_max_size", "ii", xdg_shell_types + 0 }, + { "set_min_size", "ii", xdg_shell_types + 0 }, + { "set_maximized", "", xdg_shell_types + 0 }, + { "unset_maximized", "", xdg_shell_types + 0 }, + { "set_fullscreen", "?o", xdg_shell_types + 21 }, + { "unset_fullscreen", "", xdg_shell_types + 0 }, + { "set_minimized", "", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_toplevel_events[] = { + { "configure", "iia", xdg_shell_types + 0 }, + { "close", "", xdg_shell_types + 0 }, + { "configure_bounds", "4ii", xdg_shell_types + 0 }, + { "wm_capabilities", "5a", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_toplevel_interface = { + "xdg_toplevel", 5, + 14, xdg_toplevel_requests, + 4, xdg_toplevel_events, +}; + +static const struct wl_message xdg_popup_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "grab", "ou", xdg_shell_types + 22 }, + { "reposition", "3ou", xdg_shell_types + 24 }, +}; + +static const struct wl_message xdg_popup_events[] = { + { "configure", "iiii", xdg_shell_types + 0 }, + { "popup_done", "", xdg_shell_types + 0 }, + { "repositioned", "3u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_popup_interface = { + "xdg_popup", 5, + 3, xdg_popup_requests, + 3, xdg_popup_events, +}; + diff --git a/gio/giold/app/internal/wm/wayland_xdg_shell.h b/gio/giold/app/internal/wm/wayland_xdg_shell.h new file mode 100644 index 0000000..aa14e2e --- /dev/null +++ b/gio/giold/app/internal/wm/wayland_xdg_shell.h @@ -0,0 +1,2280 @@ +/* Generated by wayland-scanner 1.21.0 */ + +#ifndef XDG_SHELL_CLIENT_PROTOCOL_H +#define XDG_SHELL_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_xdg_shell The xdg_shell protocol + * @section page_ifaces_xdg_shell Interfaces + * - @subpage page_iface_xdg_wm_base - create desktop-style surfaces + * - @subpage page_iface_xdg_positioner - child surface positioner + * - @subpage page_iface_xdg_surface - desktop user interface surface base interface + * - @subpage page_iface_xdg_toplevel - toplevel surface + * - @subpage page_iface_xdg_popup - short-lived, popup surfaces for menus + * @section page_copyright_xdg_shell Copyright + *
+ *
+ * Copyright Ā© 2008-2013 Kristian HĆøgsberg
+ * Copyright Ā© 2013      Rafael Antognolli
+ * Copyright Ā© 2013      Jasper St. Pierre
+ * Copyright Ā© 2010-2013 Intel Corporation
+ * Copyright Ā© 2015-2017 Samsung Electronics Co., Ltd
+ * Copyright Ā© 2015-2017 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * 
+ */ +struct wl_output; +struct wl_seat; +struct wl_surface; +struct xdg_popup; +struct xdg_positioner; +struct xdg_surface; +struct xdg_toplevel; +struct xdg_wm_base; + +#ifndef XDG_WM_BASE_INTERFACE +#define XDG_WM_BASE_INTERFACE +/** + * @page page_iface_xdg_wm_base xdg_wm_base + * @section page_iface_xdg_wm_base_desc Description + * + * The xdg_wm_base interface is exposed as a global object enabling clients + * to turn their wl_surfaces into windows in a desktop environment. It + * defines the basic functionality needed for clients and the compositor to + * create windows that can be dragged, resized, maximized, etc, as well as + * creating transient windows such as popup menus. + * @section page_iface_xdg_wm_base_api API + * See @ref iface_xdg_wm_base. + */ +/** + * @defgroup iface_xdg_wm_base The xdg_wm_base interface + * + * The xdg_wm_base interface is exposed as a global object enabling clients + * to turn their wl_surfaces into windows in a desktop environment. It + * defines the basic functionality needed for clients and the compositor to + * create windows that can be dragged, resized, maximized, etc, as well as + * creating transient windows such as popup menus. + */ +extern const struct wl_interface xdg_wm_base_interface; +#endif +#ifndef XDG_POSITIONER_INTERFACE +#define XDG_POSITIONER_INTERFACE +/** + * @page page_iface_xdg_positioner xdg_positioner + * @section page_iface_xdg_positioner_desc Description + * + * The xdg_positioner provides a collection of rules for the placement of a + * child surface relative to a parent surface. Rules can be defined to ensure + * the child surface remains within the visible area's borders, and to + * specify how the child surface changes its position, such as sliding along + * an axis, or flipping around a rectangle. These positioner-created rules are + * constrained by the requirement that a child surface must intersect with or + * be at least partially adjacent to its parent surface. + * + * See the various requests for details about possible rules. + * + * At the time of the request, the compositor makes a copy of the rules + * specified by the xdg_positioner. Thus, after the request is complete the + * xdg_positioner object can be destroyed or reused; further changes to the + * object will have no effect on previous usages. + * + * For an xdg_positioner object to be considered complete, it must have a + * non-zero size set by set_size, and a non-zero anchor rectangle set by + * set_anchor_rect. Passing an incomplete xdg_positioner object when + * positioning a surface raises an invalid_positioner error. + * @section page_iface_xdg_positioner_api API + * See @ref iface_xdg_positioner. + */ +/** + * @defgroup iface_xdg_positioner The xdg_positioner interface + * + * The xdg_positioner provides a collection of rules for the placement of a + * child surface relative to a parent surface. Rules can be defined to ensure + * the child surface remains within the visible area's borders, and to + * specify how the child surface changes its position, such as sliding along + * an axis, or flipping around a rectangle. These positioner-created rules are + * constrained by the requirement that a child surface must intersect with or + * be at least partially adjacent to its parent surface. + * + * See the various requests for details about possible rules. + * + * At the time of the request, the compositor makes a copy of the rules + * specified by the xdg_positioner. Thus, after the request is complete the + * xdg_positioner object can be destroyed or reused; further changes to the + * object will have no effect on previous usages. + * + * For an xdg_positioner object to be considered complete, it must have a + * non-zero size set by set_size, and a non-zero anchor rectangle set by + * set_anchor_rect. Passing an incomplete xdg_positioner object when + * positioning a surface raises an invalid_positioner error. + */ +extern const struct wl_interface xdg_positioner_interface; +#endif +#ifndef XDG_SURFACE_INTERFACE +#define XDG_SURFACE_INTERFACE +/** + * @page page_iface_xdg_surface xdg_surface + * @section page_iface_xdg_surface_desc Description + * + * An interface that may be implemented by a wl_surface, for + * implementations that provide a desktop-style user interface. + * + * It provides a base set of functionality required to construct user + * interface elements requiring management by the compositor, such as + * toplevel windows, menus, etc. The types of functionality are split into + * xdg_surface roles. + * + * Creating an xdg_surface does not set the role for a wl_surface. In order + * to map an xdg_surface, the client must create a role-specific object + * using, e.g., get_toplevel, get_popup. The wl_surface for any given + * xdg_surface can have at most one role, and may not be assigned any role + * not based on xdg_surface. + * + * A role must be assigned before any other requests are made to the + * xdg_surface object. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_surface state to take effect. + * + * Creating an xdg_surface from a wl_surface which has a buffer attached or + * committed is a client error, and any attempts by a client to attach or + * manipulate a buffer prior to the first xdg_surface.configure call must + * also be treated as errors. + * + * After creating a role-specific object and setting it up, the client must + * perform an initial commit without any buffer attached. The compositor + * will reply with an xdg_surface.configure event. The client must + * acknowledge it and is then allowed to attach a buffer to map the surface. + * + * Mapping an xdg_surface-based role surface is defined as making it + * possible for the surface to be shown by the compositor. Note that + * a mapped surface is not guaranteed to be visible once it is mapped. + * + * For an xdg_surface to be mapped by the compositor, the following + * conditions must be met: + * (1) the client has assigned an xdg_surface-based role to the surface + * (2) the client has set and committed the xdg_surface state and the + * role-dependent state to the surface + * (3) the client has committed a buffer to the surface + * + * A newly-unmapped surface is considered to have met condition (1) out + * of the 3 required conditions for mapping a surface if its role surface + * has not been destroyed, i.e. the client must perform the initial commit + * again before attaching a buffer. + * @section page_iface_xdg_surface_api API + * See @ref iface_xdg_surface. + */ +/** + * @defgroup iface_xdg_surface The xdg_surface interface + * + * An interface that may be implemented by a wl_surface, for + * implementations that provide a desktop-style user interface. + * + * It provides a base set of functionality required to construct user + * interface elements requiring management by the compositor, such as + * toplevel windows, menus, etc. The types of functionality are split into + * xdg_surface roles. + * + * Creating an xdg_surface does not set the role for a wl_surface. In order + * to map an xdg_surface, the client must create a role-specific object + * using, e.g., get_toplevel, get_popup. The wl_surface for any given + * xdg_surface can have at most one role, and may not be assigned any role + * not based on xdg_surface. + * + * A role must be assigned before any other requests are made to the + * xdg_surface object. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_surface state to take effect. + * + * Creating an xdg_surface from a wl_surface which has a buffer attached or + * committed is a client error, and any attempts by a client to attach or + * manipulate a buffer prior to the first xdg_surface.configure call must + * also be treated as errors. + * + * After creating a role-specific object and setting it up, the client must + * perform an initial commit without any buffer attached. The compositor + * will reply with an xdg_surface.configure event. The client must + * acknowledge it and is then allowed to attach a buffer to map the surface. + * + * Mapping an xdg_surface-based role surface is defined as making it + * possible for the surface to be shown by the compositor. Note that + * a mapped surface is not guaranteed to be visible once it is mapped. + * + * For an xdg_surface to be mapped by the compositor, the following + * conditions must be met: + * (1) the client has assigned an xdg_surface-based role to the surface + * (2) the client has set and committed the xdg_surface state and the + * role-dependent state to the surface + * (3) the client has committed a buffer to the surface + * + * A newly-unmapped surface is considered to have met condition (1) out + * of the 3 required conditions for mapping a surface if its role surface + * has not been destroyed, i.e. the client must perform the initial commit + * again before attaching a buffer. + */ +extern const struct wl_interface xdg_surface_interface; +#endif +#ifndef XDG_TOPLEVEL_INTERFACE +#define XDG_TOPLEVEL_INTERFACE +/** + * @page page_iface_xdg_toplevel xdg_toplevel + * @section page_iface_xdg_toplevel_desc Description + * + * This interface defines an xdg_surface role which allows a surface to, + * among other things, set window-like properties such as maximize, + * fullscreen, and minimize, set application-specific metadata like title and + * id, and well as trigger user interactive operations such as interactive + * resize and move. + * + * Unmapping an xdg_toplevel means that the surface cannot be shown + * by the compositor until it is explicitly mapped again. + * All active operations (e.g., move, resize) are canceled and all + * attributes (e.g. title, state, stacking, ...) are discarded for + * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + * the state it had right after xdg_surface.get_toplevel. The client + * can re-map the toplevel by perfoming a commit without any buffer + * attached, waiting for a configure event and handling it as usual (see + * xdg_surface description). + * + * Attaching a null buffer to a toplevel unmaps the surface. + * @section page_iface_xdg_toplevel_api API + * See @ref iface_xdg_toplevel. + */ +/** + * @defgroup iface_xdg_toplevel The xdg_toplevel interface + * + * This interface defines an xdg_surface role which allows a surface to, + * among other things, set window-like properties such as maximize, + * fullscreen, and minimize, set application-specific metadata like title and + * id, and well as trigger user interactive operations such as interactive + * resize and move. + * + * Unmapping an xdg_toplevel means that the surface cannot be shown + * by the compositor until it is explicitly mapped again. + * All active operations (e.g., move, resize) are canceled and all + * attributes (e.g. title, state, stacking, ...) are discarded for + * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + * the state it had right after xdg_surface.get_toplevel. The client + * can re-map the toplevel by perfoming a commit without any buffer + * attached, waiting for a configure event and handling it as usual (see + * xdg_surface description). + * + * Attaching a null buffer to a toplevel unmaps the surface. + */ +extern const struct wl_interface xdg_toplevel_interface; +#endif +#ifndef XDG_POPUP_INTERFACE +#define XDG_POPUP_INTERFACE +/** + * @page page_iface_xdg_popup xdg_popup + * @section page_iface_xdg_popup_desc Description + * + * A popup surface is a short-lived, temporary surface. It can be used to + * implement for example menus, popovers, tooltips and other similar user + * interface concepts. + * + * A popup can be made to take an explicit grab. See xdg_popup.grab for + * details. + * + * When the popup is dismissed, a popup_done event will be sent out, and at + * the same time the surface will be unmapped. See the xdg_popup.popup_done + * event for details. + * + * Explicitly destroying the xdg_popup object will also dismiss the popup and + * unmap the surface. Clients that want to dismiss the popup when another + * surface of their own is clicked should dismiss the popup using the destroy + * request. + * + * A newly created xdg_popup will be stacked on top of all previously created + * xdg_popup surfaces associated with the same xdg_toplevel. + * + * The parent of an xdg_popup must be mapped (see the xdg_surface + * description) before the xdg_popup itself. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_popup state to take effect. + * @section page_iface_xdg_popup_api API + * See @ref iface_xdg_popup. + */ +/** + * @defgroup iface_xdg_popup The xdg_popup interface + * + * A popup surface is a short-lived, temporary surface. It can be used to + * implement for example menus, popovers, tooltips and other similar user + * interface concepts. + * + * A popup can be made to take an explicit grab. See xdg_popup.grab for + * details. + * + * When the popup is dismissed, a popup_done event will be sent out, and at + * the same time the surface will be unmapped. See the xdg_popup.popup_done + * event for details. + * + * Explicitly destroying the xdg_popup object will also dismiss the popup and + * unmap the surface. Clients that want to dismiss the popup when another + * surface of their own is clicked should dismiss the popup using the destroy + * request. + * + * A newly created xdg_popup will be stacked on top of all previously created + * xdg_popup surfaces associated with the same xdg_toplevel. + * + * The parent of an xdg_popup must be mapped (see the xdg_surface + * description) before the xdg_popup itself. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_popup state to take effect. + */ +extern const struct wl_interface xdg_popup_interface; +#endif + +#ifndef XDG_WM_BASE_ERROR_ENUM +#define XDG_WM_BASE_ERROR_ENUM +enum xdg_wm_base_error { + /** + * given wl_surface has another role + */ + XDG_WM_BASE_ERROR_ROLE = 0, + /** + * xdg_wm_base was destroyed before children + */ + XDG_WM_BASE_ERROR_DEFUNCT_SURFACES = 1, + /** + * the client tried to map or destroy a non-topmost popup + */ + XDG_WM_BASE_ERROR_NOT_THE_TOPMOST_POPUP = 2, + /** + * the client specified an invalid popup parent surface + */ + XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT = 3, + /** + * the client provided an invalid surface state + */ + XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE = 4, + /** + * the client provided an invalid positioner + */ + XDG_WM_BASE_ERROR_INVALID_POSITIONER = 5, + /** + * the client didnā€™t respond to a ping event in time + */ + XDG_WM_BASE_ERROR_UNRESPONSIVE = 6, +}; +#endif /* XDG_WM_BASE_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_wm_base + * @struct xdg_wm_base_listener + */ +struct xdg_wm_base_listener { + /** + * check if the client is alive + * + * The ping event asks the client if it's still alive. Pass the + * serial specified in the event back to the compositor by sending + * a "pong" request back with the specified serial. See + * xdg_wm_base.pong. + * + * Compositors can use this to determine if the client is still + * alive. It's unspecified what will happen if the client doesn't + * respond to the ping request, or in what timeframe. Clients + * should try to respond in a reasonable amount of time. The + * ā€œunresponsiveā€ error is provided for compositors that wish + * to disconnect unresponsive clients. + * + * A compositor is free to ping in any way it wants, but a client + * must always respond to any xdg_wm_base object it created. + * @param serial pass this to the pong request + */ + void (*ping)(void *data, + struct xdg_wm_base *xdg_wm_base, + uint32_t serial); +}; + +/** + * @ingroup iface_xdg_wm_base + */ +static inline int +xdg_wm_base_add_listener(struct xdg_wm_base *xdg_wm_base, + const struct xdg_wm_base_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_wm_base, + (void (**)(void)) listener, data); +} + +#define XDG_WM_BASE_DESTROY 0 +#define XDG_WM_BASE_CREATE_POSITIONER 1 +#define XDG_WM_BASE_GET_XDG_SURFACE 2 +#define XDG_WM_BASE_PONG 3 + +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_PING_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_CREATE_POSITIONER_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_PONG_SINCE_VERSION 1 + +/** @ingroup iface_xdg_wm_base */ +static inline void +xdg_wm_base_set_user_data(struct xdg_wm_base *xdg_wm_base, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_wm_base, user_data); +} + +/** @ingroup iface_xdg_wm_base */ +static inline void * +xdg_wm_base_get_user_data(struct xdg_wm_base *xdg_wm_base) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_wm_base); +} + +static inline uint32_t +xdg_wm_base_get_version(struct xdg_wm_base *xdg_wm_base) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_wm_base); +} + +/** + * @ingroup iface_xdg_wm_base + * + * Destroy this xdg_wm_base object. + * + * Destroying a bound xdg_wm_base object while there are surfaces + * still alive created by this xdg_wm_base object instance is illegal + * and will result in a defunct_surfaces error. + */ +static inline void +xdg_wm_base_destroy(struct xdg_wm_base *xdg_wm_base) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_wm_base + * + * Create a positioner object. A positioner object is used to position + * surfaces relative to some parent surface. See the interface description + * and xdg_surface.get_popup for details. + */ +static inline struct xdg_positioner * +xdg_wm_base_create_positioner(struct xdg_wm_base *xdg_wm_base) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_CREATE_POSITIONER, &xdg_positioner_interface, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, NULL); + + return (struct xdg_positioner *) id; +} + +/** + * @ingroup iface_xdg_wm_base + * + * This creates an xdg_surface for the given surface. While xdg_surface + * itself is not a role, the corresponding surface may only be assigned + * a role extending xdg_surface, such as xdg_toplevel or xdg_popup. It is + * illegal to create an xdg_surface for a wl_surface which already has an + * assigned role and this will result in a role error. + * + * This creates an xdg_surface for the given surface. An xdg_surface is + * used as basis to define a role to a given surface, such as xdg_toplevel + * or xdg_popup. It also manages functionality shared between xdg_surface + * based surface roles. + * + * See the documentation of xdg_surface for more details about what an + * xdg_surface is and how it is used. + */ +static inline struct xdg_surface * +xdg_wm_base_get_xdg_surface(struct xdg_wm_base *xdg_wm_base, struct wl_surface *surface) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_GET_XDG_SURFACE, &xdg_surface_interface, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, NULL, surface); + + return (struct xdg_surface *) id; +} + +/** + * @ingroup iface_xdg_wm_base + * + * A client must respond to a ping event with a pong request or + * the client may be deemed unresponsive. See xdg_wm_base.ping + * and xdg_wm_base.error.unresponsive. + */ +static inline void +xdg_wm_base_pong(struct xdg_wm_base *xdg_wm_base, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_PONG, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, serial); +} + +#ifndef XDG_POSITIONER_ERROR_ENUM +#define XDG_POSITIONER_ERROR_ENUM +enum xdg_positioner_error { + /** + * invalid input provided + */ + XDG_POSITIONER_ERROR_INVALID_INPUT = 0, +}; +#endif /* XDG_POSITIONER_ERROR_ENUM */ + +#ifndef XDG_POSITIONER_ANCHOR_ENUM +#define XDG_POSITIONER_ANCHOR_ENUM +enum xdg_positioner_anchor { + XDG_POSITIONER_ANCHOR_NONE = 0, + XDG_POSITIONER_ANCHOR_TOP = 1, + XDG_POSITIONER_ANCHOR_BOTTOM = 2, + XDG_POSITIONER_ANCHOR_LEFT = 3, + XDG_POSITIONER_ANCHOR_RIGHT = 4, + XDG_POSITIONER_ANCHOR_TOP_LEFT = 5, + XDG_POSITIONER_ANCHOR_BOTTOM_LEFT = 6, + XDG_POSITIONER_ANCHOR_TOP_RIGHT = 7, + XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT = 8, +}; +#endif /* XDG_POSITIONER_ANCHOR_ENUM */ + +#ifndef XDG_POSITIONER_GRAVITY_ENUM +#define XDG_POSITIONER_GRAVITY_ENUM +enum xdg_positioner_gravity { + XDG_POSITIONER_GRAVITY_NONE = 0, + XDG_POSITIONER_GRAVITY_TOP = 1, + XDG_POSITIONER_GRAVITY_BOTTOM = 2, + XDG_POSITIONER_GRAVITY_LEFT = 3, + XDG_POSITIONER_GRAVITY_RIGHT = 4, + XDG_POSITIONER_GRAVITY_TOP_LEFT = 5, + XDG_POSITIONER_GRAVITY_BOTTOM_LEFT = 6, + XDG_POSITIONER_GRAVITY_TOP_RIGHT = 7, + XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT = 8, +}; +#endif /* XDG_POSITIONER_GRAVITY_ENUM */ + +#ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM +#define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM +/** + * @ingroup iface_xdg_positioner + * constraint adjustments + * + * The constraint adjustment value define ways the compositor will adjust + * the position of the surface, if the unadjusted position would result + * in the surface being partly constrained. + * + * Whether a surface is considered 'constrained' is left to the compositor + * to determine. For example, the surface may be partly outside the + * compositor's defined 'work area', thus necessitating the child surface's + * position be adjusted until it is entirely inside the work area. + * + * The adjustments can be combined, according to a defined precedence: 1) + * Flip, 2) Slide, 3) Resize. + */ +enum xdg_positioner_constraint_adjustment { + /** + * don't move the child surface when constrained + * + * Don't alter the surface position even if it is constrained on + * some axis, for example partially outside the edge of an output. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE = 0, + /** + * move along the x axis until unconstrained + * + * Slide the surface along the x axis until it is no longer + * constrained. + * + * First try to slide towards the direction of the gravity on the x + * axis until either the edge in the opposite direction of the + * gravity is unconstrained or the edge in the direction of the + * gravity is constrained. + * + * Then try to slide towards the opposite direction of the gravity + * on the x axis until either the edge in the direction of the + * gravity is unconstrained or the edge in the opposite direction + * of the gravity is constrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X = 1, + /** + * move along the y axis until unconstrained + * + * Slide the surface along the y axis until it is no longer + * constrained. + * + * First try to slide towards the direction of the gravity on the y + * axis until either the edge in the opposite direction of the + * gravity is unconstrained or the edge in the direction of the + * gravity is constrained. + * + * Then try to slide towards the opposite direction of the gravity + * on the y axis until either the edge in the direction of the + * gravity is unconstrained or the edge in the opposite direction + * of the gravity is constrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y = 2, + /** + * invert the anchor and gravity on the x axis + * + * Invert the anchor and gravity on the x axis if the surface is + * constrained on the x axis. For example, if the left edge of the + * surface is constrained, the gravity is 'left' and the anchor is + * 'left', change the gravity to 'right' and the anchor to 'right'. + * + * If the adjusted position also ends up being constrained, the + * resulting position of the flip_x adjustment will be the one + * before the adjustment. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X = 4, + /** + * invert the anchor and gravity on the y axis + * + * Invert the anchor and gravity on the y axis if the surface is + * constrained on the y axis. For example, if the bottom edge of + * the surface is constrained, the gravity is 'bottom' and the + * anchor is 'bottom', change the gravity to 'top' and the anchor + * to 'top'. + * + * The adjusted position is calculated given the original anchor + * rectangle and offset, but with the new flipped anchor and + * gravity values. + * + * If the adjusted position also ends up being constrained, the + * resulting position of the flip_y adjustment will be the one + * before the adjustment. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y = 8, + /** + * horizontally resize the surface + * + * Resize the surface horizontally so that it is completely + * unconstrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X = 16, + /** + * vertically resize the surface + * + * Resize the surface vertically so that it is completely + * unconstrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y = 32, +}; +#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM */ + +#define XDG_POSITIONER_DESTROY 0 +#define XDG_POSITIONER_SET_SIZE 1 +#define XDG_POSITIONER_SET_ANCHOR_RECT 2 +#define XDG_POSITIONER_SET_ANCHOR 3 +#define XDG_POSITIONER_SET_GRAVITY 4 +#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT 5 +#define XDG_POSITIONER_SET_OFFSET 6 +#define XDG_POSITIONER_SET_REACTIVE 7 +#define XDG_POSITIONER_SET_PARENT_SIZE 8 +#define XDG_POSITIONER_SET_PARENT_CONFIGURE 9 + + +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_ANCHOR_RECT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_ANCHOR_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_GRAVITY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_OFFSET_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION 3 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_PARENT_SIZE_SINCE_VERSION 3 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_PARENT_CONFIGURE_SINCE_VERSION 3 + +/** @ingroup iface_xdg_positioner */ +static inline void +xdg_positioner_set_user_data(struct xdg_positioner *xdg_positioner, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_positioner, user_data); +} + +/** @ingroup iface_xdg_positioner */ +static inline void * +xdg_positioner_get_user_data(struct xdg_positioner *xdg_positioner) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_positioner); +} + +static inline uint32_t +xdg_positioner_get_version(struct xdg_positioner *xdg_positioner) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_positioner); +} + +/** + * @ingroup iface_xdg_positioner + * + * Notify the compositor that the xdg_positioner will no longer be used. + */ +static inline void +xdg_positioner_destroy(struct xdg_positioner *xdg_positioner) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the size of the surface that is to be positioned with the positioner + * object. The size is in surface-local coordinates and corresponds to the + * window geometry. See xdg_surface.set_window_geometry. + * + * If a zero or negative size is set the invalid_input error is raised. + */ +static inline void +xdg_positioner_set_size(struct xdg_positioner *xdg_positioner, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, width, height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify the anchor rectangle within the parent surface that the child + * surface will be placed relative to. The rectangle is relative to the + * window geometry as defined by xdg_surface.set_window_geometry of the + * parent surface. + * + * When the xdg_positioner object is used to position a child surface, the + * anchor rectangle may not extend outside the window geometry of the + * positioned child's parent surface. + * + * If a negative size is set the invalid_input error is raised. + */ +static inline void +xdg_positioner_set_anchor_rect(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_ANCHOR_RECT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y, width, height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Defines the anchor point for the anchor rectangle. The specified anchor + * is used derive an anchor point that the child surface will be + * positioned relative to. If a corner anchor is set (e.g. 'top_left' or + * 'bottom_right'), the anchor point will be at the specified corner; + * otherwise, the derived anchor point will be centered on the specified + * edge, or in the center of the anchor rectangle if no edge is specified. + */ +static inline void +xdg_positioner_set_anchor(struct xdg_positioner *xdg_positioner, uint32_t anchor) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_ANCHOR, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, anchor); +} + +/** + * @ingroup iface_xdg_positioner + * + * Defines in what direction a surface should be positioned, relative to + * the anchor point of the parent surface. If a corner gravity is + * specified (e.g. 'bottom_right' or 'top_left'), then the child surface + * will be placed towards the specified gravity; otherwise, the child + * surface will be centered over the anchor point on any axis that had no + * gravity specified. If the gravity is not in the ā€˜gravityā€™ enum, an + * invalid_input error is raised. + */ +static inline void +xdg_positioner_set_gravity(struct xdg_positioner *xdg_positioner, uint32_t gravity) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_GRAVITY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, gravity); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify how the window should be positioned if the originally intended + * position caused the surface to be constrained, meaning at least + * partially outside positioning boundaries set by the compositor. The + * adjustment is set by constructing a bitmask describing the adjustment to + * be made when the surface is constrained on that axis. + * + * If no bit for one axis is set, the compositor will assume that the child + * surface should not change its position on that axis when constrained. + * + * If more than one bit for one axis is set, the order of how adjustments + * are applied is specified in the corresponding adjustment descriptions. + * + * The default adjustment is none. + */ +static inline void +xdg_positioner_set_constraint_adjustment(struct xdg_positioner *xdg_positioner, uint32_t constraint_adjustment) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, constraint_adjustment); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify the surface position offset relative to the position of the + * anchor on the anchor rectangle and the anchor on the surface. For + * example if the anchor of the anchor rectangle is at (x, y), the surface + * has the gravity bottom|right, and the offset is (ox, oy), the calculated + * surface position will be (x + ox, y + oy). The offset position of the + * surface is the one used for constraint testing. See + * set_constraint_adjustment. + * + * An example use case is placing a popup menu on top of a user interface + * element, while aligning the user interface element of the parent surface + * with some user interface element placed somewhere in the popup surface. + */ +static inline void +xdg_positioner_set_offset(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_OFFSET, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y); +} + +/** + * @ingroup iface_xdg_positioner + * + * When set reactive, the surface is reconstrained if the conditions used + * for constraining changed, e.g. the parent window moved. + * + * If the conditions changed and the popup was reconstrained, an + * xdg_popup.configure event is sent with updated geometry, followed by an + * xdg_surface.configure event. + */ +static inline void +xdg_positioner_set_reactive(struct xdg_positioner *xdg_positioner) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_REACTIVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the parent window geometry the compositor should use when + * positioning the popup. The compositor may use this information to + * determine the future state the popup should be constrained using. If + * this doesn't match the dimension of the parent the popup is eventually + * positioned against, the behavior is undefined. + * + * The arguments are given in the surface-local coordinate space. + */ +static inline void +xdg_positioner_set_parent_size(struct xdg_positioner *xdg_positioner, int32_t parent_width, int32_t parent_height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_PARENT_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, parent_width, parent_height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the serial of an xdg_surface.configure event this positioner will be + * used in response to. The compositor may use this information together + * with set_parent_size to determine what future state the popup should be + * constrained using. + */ +static inline void +xdg_positioner_set_parent_configure(struct xdg_positioner *xdg_positioner, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_PARENT_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, serial); +} + +#ifndef XDG_SURFACE_ERROR_ENUM +#define XDG_SURFACE_ERROR_ENUM +enum xdg_surface_error { + /** + * Surface was not fully constructed + */ + XDG_SURFACE_ERROR_NOT_CONSTRUCTED = 1, + /** + * Surface was already constructed + */ + XDG_SURFACE_ERROR_ALREADY_CONSTRUCTED = 2, + /** + * Attaching a buffer to an unconfigured surface + */ + XDG_SURFACE_ERROR_UNCONFIGURED_BUFFER = 3, + /** + * Invalid serial number when acking a configure event + */ + XDG_SURFACE_ERROR_INVALID_SERIAL = 4, + /** + * Width or height was zero or negative + */ + XDG_SURFACE_ERROR_INVALID_SIZE = 5, + /** + * Surface was destroyed before its role object + */ + XDG_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 6, +}; +#endif /* XDG_SURFACE_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_surface + * @struct xdg_surface_listener + */ +struct xdg_surface_listener { + /** + * suggest a surface change + * + * The configure event marks the end of a configure sequence. A + * configure sequence is a set of one or more events configuring + * the state of the xdg_surface, including the final + * xdg_surface.configure event. + * + * Where applicable, xdg_surface surface roles will during a + * configure sequence extend this event as a latched state sent as + * events before the xdg_surface.configure event. Such events + * should be considered to make up a set of atomically applied + * configuration states, where the xdg_surface.configure commits + * the accumulated state. + * + * Clients should arrange their surface for the new states, and + * then send an ack_configure request with the serial sent in this + * configure event at some point before committing the new surface. + * + * If the client receives multiple configure events before it can + * respond to one, it is free to discard all but the last event it + * received. + * @param serial serial of the configure event + */ + void (*configure)(void *data, + struct xdg_surface *xdg_surface, + uint32_t serial); +}; + +/** + * @ingroup iface_xdg_surface + */ +static inline int +xdg_surface_add_listener(struct xdg_surface *xdg_surface, + const struct xdg_surface_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_surface, + (void (**)(void)) listener, data); +} + +#define XDG_SURFACE_DESTROY 0 +#define XDG_SURFACE_GET_TOPLEVEL 1 +#define XDG_SURFACE_GET_POPUP 2 +#define XDG_SURFACE_SET_WINDOW_GEOMETRY 3 +#define XDG_SURFACE_ACK_CONFIGURE 4 + +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_CONFIGURE_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_GET_TOPLEVEL_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_GET_POPUP_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_SET_WINDOW_GEOMETRY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_ACK_CONFIGURE_SINCE_VERSION 1 + +/** @ingroup iface_xdg_surface */ +static inline void +xdg_surface_set_user_data(struct xdg_surface *xdg_surface, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_surface, user_data); +} + +/** @ingroup iface_xdg_surface */ +static inline void * +xdg_surface_get_user_data(struct xdg_surface *xdg_surface) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_surface); +} + +static inline uint32_t +xdg_surface_get_version(struct xdg_surface *xdg_surface) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_surface); +} + +/** + * @ingroup iface_xdg_surface + * + * Destroy the xdg_surface object. An xdg_surface must only be destroyed + * after its role object has been destroyed, otherwise + * a defunct_role_object error is raised. + */ +static inline void +xdg_surface_destroy(struct xdg_surface *xdg_surface) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_surface + * + * This creates an xdg_toplevel object for the given xdg_surface and gives + * the associated wl_surface the xdg_toplevel role. + * + * See the documentation of xdg_toplevel for more details about what an + * xdg_toplevel is and how it is used. + */ +static inline struct xdg_toplevel * +xdg_surface_get_toplevel(struct xdg_surface *xdg_surface) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_GET_TOPLEVEL, &xdg_toplevel_interface, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, NULL); + + return (struct xdg_toplevel *) id; +} + +/** + * @ingroup iface_xdg_surface + * + * This creates an xdg_popup object for the given xdg_surface and gives + * the associated wl_surface the xdg_popup role. + * + * If null is passed as a parent, a parent surface must be specified using + * some other protocol, before committing the initial state. + * + * See the documentation of xdg_popup for more details about what an + * xdg_popup is and how it is used. + */ +static inline struct xdg_popup * +xdg_surface_get_popup(struct xdg_surface *xdg_surface, struct xdg_surface *parent, struct xdg_positioner *positioner) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_GET_POPUP, &xdg_popup_interface, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, NULL, parent, positioner); + + return (struct xdg_popup *) id; +} + +/** + * @ingroup iface_xdg_surface + * + * The window geometry of a surface is its "visible bounds" from the + * user's perspective. Client-side decorations often have invisible + * portions like drop-shadows which should be ignored for the + * purposes of aligning, placing and constraining windows. + * + * The window geometry is double buffered, and will be applied at the + * time wl_surface.commit of the corresponding wl_surface is called. + * + * When maintaining a position, the compositor should treat the (x, y) + * coordinate of the window geometry as the top left corner of the window. + * A client changing the (x, y) window geometry coordinate should in + * general not alter the position of the window. + * + * Once the window geometry of the surface is set, it is not possible to + * unset it, and it will remain the same until set_window_geometry is + * called again, even if a new subsurface or buffer is attached. + * + * If never set, the value is the full bounds of the surface, + * including any subsurfaces. This updates dynamically on every + * commit. This unset is meant for extremely simple clients. + * + * The arguments are given in the surface-local coordinate space of + * the wl_surface associated with this xdg_surface. + * + * The width and height must be greater than zero. Setting an invalid size + * will raise an invalid_size error. When applied, the effective window + * geometry will be the set window geometry clamped to the bounding + * rectangle of the combined geometry of the surface of the xdg_surface and + * the associated subsurfaces. + */ +static inline void +xdg_surface_set_window_geometry(struct xdg_surface *xdg_surface, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_SET_WINDOW_GEOMETRY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, x, y, width, height); +} + +/** + * @ingroup iface_xdg_surface + * + * When a configure event is received, if a client commits the + * surface in response to the configure event, then the client + * must make an ack_configure request sometime before the commit + * request, passing along the serial of the configure event. + * + * For instance, for toplevel surfaces the compositor might use this + * information to move a surface to the top left only when the client has + * drawn itself for the maximized or fullscreen state. + * + * If the client receives multiple configure events before it + * can respond to one, it only has to ack the last configure event. + * Acking a configure event that was never sent raises an invalid_serial + * error. + * + * A client is not required to commit immediately after sending + * an ack_configure request - it may even ack_configure several times + * before its next surface commit. + * + * A client may send multiple ack_configure requests before committing, but + * only the last request sent before a commit indicates which configure + * event the client really is responding to. + * + * Sending an ack_configure request consumes the serial number sent with + * the request, as well as serial numbers sent by all configure events + * sent on this xdg_surface prior to the configure event referenced by + * the committed serial. + * + * It is an error to issue multiple ack_configure requests referencing a + * serial from the same configure event, or to issue an ack_configure + * request referencing a serial from a configure event issued before the + * event identified by the last ack_configure request for the same + * xdg_surface. Doing so will raise an invalid_serial error. + */ +static inline void +xdg_surface_ack_configure(struct xdg_surface *xdg_surface, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_ACK_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, serial); +} + +#ifndef XDG_TOPLEVEL_ERROR_ENUM +#define XDG_TOPLEVEL_ERROR_ENUM +enum xdg_toplevel_error { + /** + * provided value is not a valid variant of the resize_edge enum + */ + XDG_TOPLEVEL_ERROR_INVALID_RESIZE_EDGE = 0, + /** + * invalid parent toplevel + */ + XDG_TOPLEVEL_ERROR_INVALID_PARENT = 1, + /** + * client provided an invalid min or max size + */ + XDG_TOPLEVEL_ERROR_INVALID_SIZE = 2, +}; +#endif /* XDG_TOPLEVEL_ERROR_ENUM */ + +#ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM +#define XDG_TOPLEVEL_RESIZE_EDGE_ENUM +/** + * @ingroup iface_xdg_toplevel + * edge values for resizing + * + * These values are used to indicate which edge of a surface + * is being dragged in a resize operation. + */ +enum xdg_toplevel_resize_edge { + XDG_TOPLEVEL_RESIZE_EDGE_NONE = 0, + XDG_TOPLEVEL_RESIZE_EDGE_TOP = 1, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM = 2, + XDG_TOPLEVEL_RESIZE_EDGE_LEFT = 4, + XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT = 5, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT = 6, + XDG_TOPLEVEL_RESIZE_EDGE_RIGHT = 8, + XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT = 9, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT = 10, +}; +#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM */ + +#ifndef XDG_TOPLEVEL_STATE_ENUM +#define XDG_TOPLEVEL_STATE_ENUM +/** + * @ingroup iface_xdg_toplevel + * types of state on the surface + * + * The different state values used on the surface. This is designed for + * state values like maximized, fullscreen. It is paired with the + * configure event to ensure that both the client and the compositor + * setting the state can be synchronized. + * + * States set in this way are double-buffered. They will get applied on + * the next commit. + */ +enum xdg_toplevel_state { + /** + * the surface is maximized + * the surface is maximized + * + * The surface is maximized. The window geometry specified in the + * configure event must be obeyed by the client. + * + * The client should draw without shadow or other decoration + * outside of the window geometry. + */ + XDG_TOPLEVEL_STATE_MAXIMIZED = 1, + /** + * the surface is fullscreen + * the surface is fullscreen + * + * The surface is fullscreen. The window geometry specified in + * the configure event is a maximum; the client cannot resize + * beyond it. For a surface to cover the whole fullscreened area, + * the geometry dimensions must be obeyed by the client. For more + * details, see xdg_toplevel.set_fullscreen. + */ + XDG_TOPLEVEL_STATE_FULLSCREEN = 2, + /** + * the surface is being resized + * the surface is being resized + * + * The surface is being resized. The window geometry specified in + * the configure event is a maximum; the client cannot resize + * beyond it. Clients that have aspect ratio or cell sizing + * configuration can use a smaller size, however. + */ + XDG_TOPLEVEL_STATE_RESIZING = 3, + /** + * the surface is now activated + * the surface is now activated + * + * Client window decorations should be painted as if the window + * is active. Do not assume this means that the window actually has + * keyboard or pointer focus. + */ + XDG_TOPLEVEL_STATE_ACTIVATED = 4, + /** + * the surfaceā€™s left edge is tiled + * + * The window is currently in a tiled layout and the left edge is + * considered to be adjacent to another part of the tiling grid. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_LEFT = 5, + /** + * the surfaceā€™s right edge is tiled + * + * The window is currently in a tiled layout and the right edge + * is considered to be adjacent to another part of the tiling grid. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_RIGHT = 6, + /** + * the surfaceā€™s top edge is tiled + * + * The window is currently in a tiled layout and the top edge is + * considered to be adjacent to another part of the tiling grid. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_TOP = 7, + /** + * the surfaceā€™s bottom edge is tiled + * + * The window is currently in a tiled layout and the bottom edge + * is considered to be adjacent to another part of the tiling grid. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_BOTTOM = 8, +}; +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_RIGHT_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_TOP_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_BOTTOM_SINCE_VERSION 2 +#endif /* XDG_TOPLEVEL_STATE_ENUM */ + +#ifndef XDG_TOPLEVEL_WM_CAPABILITIES_ENUM +#define XDG_TOPLEVEL_WM_CAPABILITIES_ENUM +enum xdg_toplevel_wm_capabilities { + /** + * show_window_menu is available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU = 1, + /** + * set_maximized and unset_maximized are available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE = 2, + /** + * set_fullscreen and unset_fullscreen are available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN = 3, + /** + * set_minimized is available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE = 4, +}; +#endif /* XDG_TOPLEVEL_WM_CAPABILITIES_ENUM */ + +/** + * @ingroup iface_xdg_toplevel + * @struct xdg_toplevel_listener + */ +struct xdg_toplevel_listener { + /** + * suggest a surface change + * + * This configure event asks the client to resize its toplevel + * surface or to change its state. The configured state should not + * be applied immediately. See xdg_surface.configure for details. + * + * The width and height arguments specify a hint to the window + * about how its surface should be resized in window geometry + * coordinates. See set_window_geometry. + * + * If the width or height arguments are zero, it means the client + * should decide its own window dimension. This may happen when the + * compositor needs to configure the state of the surface but + * doesn't have any information about any previous or expected + * dimension. + * + * The states listed in the event specify how the width/height + * arguments should be interpreted, and possibly how it should be + * drawn. + * + * Clients must send an ack_configure in response to this event. + * See xdg_surface.configure and xdg_surface.ack_configure for + * details. + */ + void (*configure)(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, + int32_t height, + struct wl_array *states); + /** + * surface wants to be closed + * + * The close event is sent by the compositor when the user wants + * the surface to be closed. This should be equivalent to the user + * clicking the close button in client-side decorations, if your + * application has any. + * + * This is only a request that the user intends to close the + * window. The client may choose to ignore this request, or show a + * dialog to ask the user to save their data, etc. + */ + void (*close)(void *data, + struct xdg_toplevel *xdg_toplevel); + /** + * recommended window geometry bounds + * + * The configure_bounds event may be sent prior to a + * xdg_toplevel.configure event to communicate the bounds a window + * geometry size is recommended to constrain to. + * + * The passed width and height are in surface coordinate space. If + * width and height are 0, it means bounds is unknown and + * equivalent to as if no configure_bounds event was ever sent for + * this surface. + * + * The bounds can for example correspond to the size of a monitor + * excluding any panels or other shell components, so that a + * surface isn't created in a way that it cannot fit. + * + * The bounds may change at any point, and in such a case, a new + * xdg_toplevel.configure_bounds will be sent, followed by + * xdg_toplevel.configure and xdg_surface.configure. + * @since 4 + */ + void (*configure_bounds)(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, + int32_t height); + /** + * compositor capabilities + * + * This event advertises the capabilities supported by the + * compositor. If a capability isn't supported, clients should hide + * or disable the UI elements that expose this functionality. For + * instance, if the compositor doesn't advertise support for + * minimized toplevels, a button triggering the set_minimized + * request should not be displayed. + * + * The compositor will ignore requests it doesn't support. For + * instance, a compositor which doesn't advertise support for + * minimized will ignore set_minimized requests. + * + * Compositors must send this event once before the first + * xdg_surface.configure event. When the capabilities change, + * compositors must send this event again and then send an + * xdg_surface.configure event. + * + * The configured state should not be applied immediately. See + * xdg_surface.configure for details. + * + * The capabilities are sent as an array of 32-bit unsigned + * integers in native endianness. + * @param capabilities array of 32-bit capabilities + * @since 5 + */ + void (*wm_capabilities)(void *data, + struct xdg_toplevel *xdg_toplevel, + struct wl_array *capabilities); +}; + +/** + * @ingroup iface_xdg_toplevel + */ +static inline int +xdg_toplevel_add_listener(struct xdg_toplevel *xdg_toplevel, + const struct xdg_toplevel_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_toplevel, + (void (**)(void)) listener, data); +} + +#define XDG_TOPLEVEL_DESTROY 0 +#define XDG_TOPLEVEL_SET_PARENT 1 +#define XDG_TOPLEVEL_SET_TITLE 2 +#define XDG_TOPLEVEL_SET_APP_ID 3 +#define XDG_TOPLEVEL_SHOW_WINDOW_MENU 4 +#define XDG_TOPLEVEL_MOVE 5 +#define XDG_TOPLEVEL_RESIZE 6 +#define XDG_TOPLEVEL_SET_MAX_SIZE 7 +#define XDG_TOPLEVEL_SET_MIN_SIZE 8 +#define XDG_TOPLEVEL_SET_MAXIMIZED 9 +#define XDG_TOPLEVEL_UNSET_MAXIMIZED 10 +#define XDG_TOPLEVEL_SET_FULLSCREEN 11 +#define XDG_TOPLEVEL_UNSET_FULLSCREEN 12 +#define XDG_TOPLEVEL_SET_MINIMIZED 13 + +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CLOSE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION 4 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION 5 + +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_PARENT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_TITLE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_APP_ID_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SHOW_WINDOW_MENU_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_MOVE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_RESIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MAX_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MIN_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MAXIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_UNSET_MAXIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_FULLSCREEN_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_UNSET_FULLSCREEN_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MINIMIZED_SINCE_VERSION 1 + +/** @ingroup iface_xdg_toplevel */ +static inline void +xdg_toplevel_set_user_data(struct xdg_toplevel *xdg_toplevel, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_toplevel, user_data); +} + +/** @ingroup iface_xdg_toplevel */ +static inline void * +xdg_toplevel_get_user_data(struct xdg_toplevel *xdg_toplevel) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_toplevel); +} + +static inline uint32_t +xdg_toplevel_get_version(struct xdg_toplevel *xdg_toplevel) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_toplevel); +} + +/** + * @ingroup iface_xdg_toplevel + * + * This request destroys the role surface and unmaps the surface; + * see "Unmapping" behavior in interface section for details. + */ +static inline void +xdg_toplevel_destroy(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set the "parent" of this surface. This surface should be stacked + * above the parent surface and all other ancestor surfaces. + * + * Parent surfaces should be set on dialogs, toolboxes, or other + * "auxiliary" surfaces, so that the parent is raised when the dialog + * is raised. + * + * Setting a null parent for a child surface unsets its parent. Setting + * a null parent for a surface which currently has no parent is a no-op. + * + * Only mapped surfaces can have child surfaces. Setting a parent which + * is not mapped is equivalent to setting a null parent. If a surface + * becomes unmapped, its children's parent is set to the parent of + * the now-unmapped surface. If the now-unmapped surface has no parent, + * its children's parent is unset. If the now-unmapped surface becomes + * mapped again, its parent-child relationship is not restored. + * + * The parent toplevel must not be one of the child toplevel's + * descendants, and the parent must be different from the child toplevel, + * otherwise the invalid_parent protocol error is raised. + */ +static inline void +xdg_toplevel_set_parent(struct xdg_toplevel *xdg_toplevel, struct xdg_toplevel *parent) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_PARENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, parent); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a short title for the surface. + * + * This string may be used to identify the surface in a task bar, + * window list, or other user interface elements provided by the + * compositor. + * + * The string must be encoded in UTF-8. + */ +static inline void +xdg_toplevel_set_title(struct xdg_toplevel *xdg_toplevel, const char *title) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_TITLE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, title); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set an application identifier for the surface. + * + * The app ID identifies the general class of applications to which + * the surface belongs. The compositor can use this to group multiple + * surfaces together, or to determine how to launch a new application. + * + * For D-Bus activatable applications, the app ID is used as the D-Bus + * service name. + * + * The compositor shell will try to group application surfaces together + * by their app ID. As a best practice, it is suggested to select app + * ID's that match the basename of the application's .desktop file. + * For example, "org.freedesktop.FooViewer" where the .desktop file is + * "org.freedesktop.FooViewer.desktop". + * + * Like other properties, a set_app_id request can be sent after the + * xdg_toplevel has been mapped to update the property. + * + * See the desktop-entry specification [0] for more details on + * application identifiers and how they relate to well-known D-Bus + * names and .desktop files. + * + * [0] https://standards.freedesktop.org/desktop-entry-spec/ + */ +static inline void +xdg_toplevel_set_app_id(struct xdg_toplevel *xdg_toplevel, const char *app_id) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_APP_ID, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, app_id); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Clients implementing client-side decorations might want to show + * a context menu when right-clicking on the decorations, giving the + * user a menu that they can use to maximize or minimize the window. + * + * This request asks the compositor to pop up such a window menu at + * the given position, relative to the local surface coordinates of + * the parent surface. There are no guarantees as to what menu items + * the window menu contains, or even if a window menu will be drawn + * at all. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. + */ +static inline void +xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, int32_t x, int32_t y) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SHOW_WINDOW_MENU, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, x, y); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Start an interactive, user-driven move of the surface. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. The passed + * serial is used to determine the type of interactive move (touch, + * pointer, etc). + * + * The server may ignore move requests depending on the state of + * the surface (e.g. fullscreen or maximized), or if the passed serial + * is no longer valid. + * + * If triggered, the surface will lose the focus of the device + * (wl_pointer, wl_touch, etc) used for the move. It is up to the + * compositor to visually indicate that the move is taking place, such as + * updating a pointer cursor, during the move. There is no guarantee + * that the device focus will return when the move is completed. + */ +static inline void +xdg_toplevel_move(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_MOVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Start a user-driven, interactive resize of the surface. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. The passed + * serial is used to determine the type of interactive resize (touch, + * pointer, etc). + * + * The server may ignore resize requests depending on the state of + * the surface (e.g. fullscreen or maximized). + * + * If triggered, the client will receive configure events with the + * "resize" state enum value and the expected sizes. See the "resize" + * enum value for more details about what is required. The client + * must also acknowledge configure events using "ack_configure". After + * the resize is completed, the client will receive another "configure" + * event without the resize state. + * + * If triggered, the surface also will lose the focus of the device + * (wl_pointer, wl_touch, etc) used for the resize. It is up to the + * compositor to visually indicate that the resize is taking place, + * such as updating a pointer cursor, during the resize. There is no + * guarantee that the device focus will return when the resize is + * completed. + * + * The edges parameter specifies how the surface should be resized, and + * is one of the values of the resize_edge enum. Values not matching + * a variant of the enum will cause a protocol error. The compositor + * may use this information to update the surface position for example + * when dragging the top left corner. The compositor may also use + * this information to adapt its behavior, e.g. choose an appropriate + * cursor image. + */ +static inline void +xdg_toplevel_resize(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, uint32_t edges) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_RESIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, edges); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a maximum size for the window. + * + * The client can specify a maximum size so that the compositor does + * not try to configure the window beyond this size. + * + * The width and height arguments are in window geometry coordinates. + * See xdg_surface.set_window_geometry. + * + * Values set in this way are double-buffered. They will get applied + * on the next commit. + * + * The compositor can use this information to allow or disallow + * different states like maximize or fullscreen and draw accurate + * animations. + * + * Similarly, a tiling window manager may use this information to + * place and resize client windows in a more effective way. + * + * The client should not rely on the compositor to obey the maximum + * size. The compositor may decide to ignore the values set by the + * client and request a larger size. + * + * If never set, or a value of zero in the request, means that the + * client has no expected maximum size in the given dimension. + * As a result, a client wishing to reset the maximum size + * to an unspecified state can use zero for width and height in the + * request. + * + * Requesting a maximum size to be smaller than the minimum size of + * a surface is illegal and will result in an invalid_size error. + * + * The width and height must be greater than or equal to zero. Using + * strictly negative values for width or height will result in a + * invalid_size error. + */ +static inline void +xdg_toplevel_set_max_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MAX_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a minimum size for the window. + * + * The client can specify a minimum size so that the compositor does + * not try to configure the window below this size. + * + * The width and height arguments are in window geometry coordinates. + * See xdg_surface.set_window_geometry. + * + * Values set in this way are double-buffered. They will get applied + * on the next commit. + * + * The compositor can use this information to allow or disallow + * different states like maximize or fullscreen and draw accurate + * animations. + * + * Similarly, a tiling window manager may use this information to + * place and resize client windows in a more effective way. + * + * The client should not rely on the compositor to obey the minimum + * size. The compositor may decide to ignore the values set by the + * client and request a smaller size. + * + * If never set, or a value of zero in the request, means that the + * client has no expected minimum size in the given dimension. + * As a result, a client wishing to reset the minimum size + * to an unspecified state can use zero for width and height in the + * request. + * + * Requesting a minimum size to be larger than the maximum size of + * a surface is illegal and will result in an invalid_size error. + * + * The width and height must be greater than or equal to zero. Using + * strictly negative values for width and height will result in a + * invalid_size error. + */ +static inline void +xdg_toplevel_set_min_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MIN_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Maximize the surface. + * + * After requesting that the surface should be maximized, the compositor + * will respond by emitting a configure event. Whether this configure + * actually sets the window maximized is subject to compositor policies. + * The client must then update its content, drawing in the configured + * state. The client must also acknowledge the configure when committing + * the new content (see ack_configure). + * + * It is up to the compositor to decide how and where to maximize the + * surface, for example which output and what region of the screen should + * be used. + * + * If the surface was already maximized, the compositor will still emit + * a configure event with the "maximized" state. + * + * If the surface is in a fullscreen state, this request has no direct + * effect. It may alter the state the surface is returned to when + * unmaximized unless overridden by the compositor. + */ +static inline void +xdg_toplevel_set_maximized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Unmaximize the surface. + * + * After requesting that the surface should be unmaximized, the compositor + * will respond by emitting a configure event. Whether this actually + * un-maximizes the window is subject to compositor policies. + * If available and applicable, the compositor will include the window + * geometry dimensions the window had prior to being maximized in the + * configure event. The client must then update its content, drawing it in + * the configured state. The client must also acknowledge the configure + * when committing the new content (see ack_configure). + * + * It is up to the compositor to position the surface after it was + * unmaximized; usually the position the surface had before maximizing, if + * applicable. + * + * If the surface was already not maximized, the compositor will still + * emit a configure event without the "maximized" state. + * + * If the surface is in a fullscreen state, this request has no direct + * effect. It may alter the state the surface is returned to when + * unmaximized unless overridden by the compositor. + */ +static inline void +xdg_toplevel_unset_maximized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_UNSET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Make the surface fullscreen. + * + * After requesting that the surface should be fullscreened, the + * compositor will respond by emitting a configure event. Whether the + * client is actually put into a fullscreen state is subject to compositor + * policies. The client must also acknowledge the configure when + * committing the new content (see ack_configure). + * + * The output passed by the request indicates the client's preference as + * to which display it should be set fullscreen on. If this value is NULL, + * it's up to the compositor to choose which display will be used to map + * this surface. + * + * If the surface doesn't cover the whole output, the compositor will + * position the surface in the center of the output and compensate with + * with border fill covering the rest of the output. The content of the + * border fill is undefined, but should be assumed to be in some way that + * attempts to blend into the surrounding area (e.g. solid black). + * + * If the fullscreened surface is not opaque, the compositor must make + * sure that other screen content not part of the same surface tree (made + * up of subsurfaces, popups or similarly coupled surfaces) are not + * visible below the fullscreened surface. + */ +static inline void +xdg_toplevel_set_fullscreen(struct xdg_toplevel *xdg_toplevel, struct wl_output *output) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, output); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Make the surface no longer fullscreen. + * + * After requesting that the surface should be unfullscreened, the + * compositor will respond by emitting a configure event. + * Whether this actually removes the fullscreen state of the client is + * subject to compositor policies. + * + * Making a surface unfullscreen sets states for the surface based on the following: + * * the state(s) it may have had before becoming fullscreen + * * any state(s) decided by the compositor + * * any state(s) requested by the client while the surface was fullscreen + * + * The compositor may include the previous window geometry dimensions in + * the configure event, if applicable. + * + * The client must also acknowledge the configure when committing the new + * content (see ack_configure). + */ +static inline void +xdg_toplevel_unset_fullscreen(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_UNSET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Request that the compositor minimize your surface. There is no + * way to know if the surface is currently minimized, nor is there + * any way to unset minimization on this surface. + * + * If you are looking to throttle redrawing when minimized, please + * instead use the wl_surface.frame event for this, as this will + * also work with live previews on windows in Alt-Tab, Expose or + * similar compositor features. + */ +static inline void +xdg_toplevel_set_minimized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MINIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +#ifndef XDG_POPUP_ERROR_ENUM +#define XDG_POPUP_ERROR_ENUM +enum xdg_popup_error { + /** + * tried to grab after being mapped + */ + XDG_POPUP_ERROR_INVALID_GRAB = 0, +}; +#endif /* XDG_POPUP_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_popup + * @struct xdg_popup_listener + */ +struct xdg_popup_listener { + /** + * configure the popup surface + * + * This event asks the popup surface to configure itself given + * the configuration. The configured state should not be applied + * immediately. See xdg_surface.configure for details. + * + * The x and y arguments represent the position the popup was + * placed at given the xdg_positioner rule, relative to the upper + * left corner of the window geometry of the parent surface. + * + * For version 2 or older, the configure event for an xdg_popup is + * only ever sent once for the initial configuration. Starting with + * version 3, it may be sent again if the popup is setup with an + * xdg_positioner with set_reactive requested, or in response to + * xdg_popup.reposition requests. + * @param x x position relative to parent surface window geometry + * @param y y position relative to parent surface window geometry + * @param width window geometry width + * @param height window geometry height + */ + void (*configure)(void *data, + struct xdg_popup *xdg_popup, + int32_t x, + int32_t y, + int32_t width, + int32_t height); + /** + * popup interaction is done + * + * The popup_done event is sent out when a popup is dismissed by + * the compositor. The client should destroy the xdg_popup object + * at this point. + */ + void (*popup_done)(void *data, + struct xdg_popup *xdg_popup); + /** + * signal the completion of a repositioned request + * + * The repositioned event is sent as part of a popup + * configuration sequence, together with xdg_popup.configure and + * lastly xdg_surface.configure to notify the completion of a + * reposition request. + * + * The repositioned event is to notify about the completion of a + * xdg_popup.reposition request. The token argument is the token + * passed in the xdg_popup.reposition request. + * + * Immediately after this event is emitted, xdg_popup.configure and + * xdg_surface.configure will be sent with the updated size and + * position, as well as a new configure serial. + * + * The client should optionally update the content of the popup, + * but must acknowledge the new popup configuration for the new + * position to take effect. See xdg_surface.ack_configure for + * details. + * @param token reposition request token + * @since 3 + */ + void (*repositioned)(void *data, + struct xdg_popup *xdg_popup, + uint32_t token); +}; + +/** + * @ingroup iface_xdg_popup + */ +static inline int +xdg_popup_add_listener(struct xdg_popup *xdg_popup, + const struct xdg_popup_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_popup, + (void (**)(void)) listener, data); +} + +#define XDG_POPUP_DESTROY 0 +#define XDG_POPUP_GRAB 1 +#define XDG_POPUP_REPOSITION 2 + +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_POPUP_DONE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_REPOSITIONED_SINCE_VERSION 3 + +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_GRAB_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_REPOSITION_SINCE_VERSION 3 + +/** @ingroup iface_xdg_popup */ +static inline void +xdg_popup_set_user_data(struct xdg_popup *xdg_popup, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_popup, user_data); +} + +/** @ingroup iface_xdg_popup */ +static inline void * +xdg_popup_get_user_data(struct xdg_popup *xdg_popup) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_popup); +} + +static inline uint32_t +xdg_popup_get_version(struct xdg_popup *xdg_popup) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_popup); +} + +/** + * @ingroup iface_xdg_popup + * + * This destroys the popup. Explicitly destroying the xdg_popup + * object will also dismiss the popup, and unmap the surface. + * + * If this xdg_popup is not the "topmost" popup, a protocol error + * will be sent. + */ +static inline void +xdg_popup_destroy(struct xdg_popup *xdg_popup) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_popup + * + * This request makes the created popup take an explicit grab. An explicit + * grab will be dismissed when the user dismisses the popup, or when the + * client destroys the xdg_popup. This can be done by the user clicking + * outside the surface, using the keyboard, or even locking the screen + * through closing the lid or a timeout. + * + * If the compositor denies the grab, the popup will be immediately + * dismissed. + * + * This request must be used in response to some sort of user action like a + * button press, key press, or touch down event. The serial number of the + * event should be passed as 'serial'. + * + * The parent of a grabbing popup must either be an xdg_toplevel surface or + * another xdg_popup with an explicit grab. If the parent is another + * xdg_popup it means that the popups are nested, with this popup now being + * the topmost popup. + * + * Nested popups must be destroyed in the reverse order they were created + * in, e.g. the only popup you are allowed to destroy at all times is the + * topmost one. + * + * When compositors choose to dismiss a popup, they may dismiss every + * nested grabbing popup as well. When a compositor dismisses popups, it + * will follow the same dismissing order as required from the client. + * + * If the topmost grabbing popup is destroyed, the grab will be returned to + * the parent of the popup, if that parent previously had an explicit grab. + * + * If the parent is a grabbing popup which has already been dismissed, this + * popup will be immediately dismissed. If the parent is a popup that did + * not take an explicit grab, an error will be raised. + * + * During a popup grab, the client owning the grab will receive pointer + * and touch events for all their surfaces as normal (similar to an + * "owner-events" grab in X11 parlance), while the top most grabbing popup + * will always have keyboard focus. + */ +static inline void +xdg_popup_grab(struct xdg_popup *xdg_popup, struct wl_seat *seat, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_GRAB, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, seat, serial); +} + +/** + * @ingroup iface_xdg_popup + * + * Reposition an already-mapped popup. The popup will be placed given the + * details in the passed xdg_positioner object, and a + * xdg_popup.repositioned followed by xdg_popup.configure and + * xdg_surface.configure will be emitted in response. Any parameters set + * by the previous positioner will be discarded. + * + * The passed token will be sent in the corresponding + * xdg_popup.repositioned event. The new popup position will not take + * effect until the corresponding configure event is acknowledged by the + * client. See xdg_popup.repositioned for details. The token itself is + * opaque, and has no other special meaning. + * + * If multiple reposition requests are sent, the compositor may skip all + * but the last one. + * + * If the popup is repositioned in response to a configure event for its + * parent, the client should send an xdg_positioner.set_parent_configure + * and possibly an xdg_positioner.set_parent_size request to allow the + * compositor to properly constrain the popup. + * + * If the popup is repositioned together with a parent that is being + * resized, but not in response to a configure event, the client should + * send an xdg_positioner.set_parent_size request. + */ +static inline void +xdg_popup_reposition(struct xdg_popup *xdg_popup, struct xdg_positioner *positioner, uint32_t token) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_REPOSITION, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, positioner, token); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/gio/giold/app/internal/wm/window.go b/gio/giold/app/internal/wm/window.go new file mode 100644 index 0000000..82e3c38 --- /dev/null +++ b/gio/giold/app/internal/wm/window.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// package wm implements platform specific windows +// and GPU contexts. +package wm + +import ( + "errors" + + "realy.lol/gio/gpu" + "realy.lol/gio/io/event" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/unit" +) + +type Size struct { + Width unit.Value + Height unit.Value +} + +type Options struct { + Size *Size + MinSize *Size + MaxSize *Size + Title *string + WindowMode *WindowMode +} + +type WindowMode uint8 + +const ( + Windowed WindowMode = iota + Fullscreen +) + +type FrameEvent struct { + system.FrameEvent + + Sync bool +} + +type Callbacks interface { + SetDriver(d Driver) + Event(e event.Event) +} + +type Context interface { + API() gpu.API + Present() error + MakeCurrent() error + Release() + Lock() + Unlock() +} + +// ErrDeviceLost is returned from Context.Present when +// the underlying GPU device is gone and should be +// recreated. +var ErrDeviceLost = errors.New("GPU device lost") + +// Driver is the interface for the platform implementation +// of a window. +type Driver interface { + // SetAnimating sets the animation flag. When the window is animating, + // FrameEvents are delivered as fast as the display can handle them. + SetAnimating(anim bool) + // ShowTextInput updates the virtual keyboard state. + ShowTextInput(show bool) + NewContext() (Context, error) + + // ReadClipboard requests the clipboard content. + ReadClipboard() + // WriteClipboard requests a clipboard write. + WriteClipboard(s string) + + // Option processes option changes. + Option(opts *Options) + + // SetCursor updates the current cursor to name. + SetCursor(name pointer.CursorName) + + // Close the window. + Close() +} + +type windowRendezvous struct { + in chan windowAndOptions + out chan windowAndOptions + errs chan error +} + +type windowAndOptions struct { + window Callbacks + opts *Options +} + +func newWindowRendezvous() *windowRendezvous { + wr := &windowRendezvous{ + in: make(chan windowAndOptions), + out: make(chan windowAndOptions), + errs: make(chan error), + } + go func() { + var main windowAndOptions + var out chan windowAndOptions + for { + select { + case w := <-wr.in: + var err error + if main.window != nil { + err = errors.New("multiple windows are not supported") + } + wr.errs <- err + main = w + out = wr.out + case out <- main: + } + } + }() + return wr +} diff --git a/gio/giold/app/internal/xkb/xkb_unix.go b/gio/giold/app/internal/xkb/xkb_unix.go new file mode 100644 index 0000000..be72a58 --- /dev/null +++ b/gio/giold/app/internal/xkb/xkb_unix.go @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build (linux && !android) || freebsd || openbsd +// +build linux,!android freebsd openbsd + +// Package xkb implements a Go interface for the X Keyboard Extension library. +package xkb + +import ( + "errors" + "fmt" + "os" + "syscall" + "unicode" + "unicode/utf8" + "unsafe" + + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" +) + +/* +#cgo linux pkg-config: xkbcommon +#cgo freebsd openbsd CFLAGS: -I/usr/local/include +#cgo freebsd openbsd LDFLAGS: -L/usr/local/lib -lxkbcommon + +#include +#include +#include +*/ +import "C" + +type Context struct { + Ctx *C.struct_xkb_context + keyMap *C.struct_xkb_keymap + state *C.struct_xkb_state + compTable *C.struct_xkb_compose_table + compState *C.struct_xkb_compose_state + utf8Buf []byte +} + +var ( + _XKB_MOD_NAME_CTRL = []byte("Control\x00") + _XKB_MOD_NAME_SHIFT = []byte("Shift\x00") + _XKB_MOD_NAME_ALT = []byte("Mod1\x00") + _XKB_MOD_NAME_LOGO = []byte("Mod4\x00") +) + +func (x *Context) Destroy() { + if x.compState != nil { + C.xkb_compose_state_unref(x.compState) + x.compState = nil + } + if x.compTable != nil { + C.xkb_compose_table_unref(x.compTable) + x.compTable = nil + } + x.DestroyKeymapState() + if x.Ctx != nil { + C.xkb_context_unref(x.Ctx) + x.Ctx = nil + } +} + +func New() (*Context, error) { + ctx := &Context{ + Ctx: C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS), + } + if ctx.Ctx == nil { + return nil, errors.New("newXKB: xkb_context_new failed") + } + locale := os.Getenv("LC_ALL") + if locale == "" { + locale = os.Getenv("LC_CTYPE") + } + if locale == "" { + locale = os.Getenv("LANG") + } + if locale == "" { + locale = "C" + } + cloc := C.CString(locale) + defer C.free(unsafe.Pointer(cloc)) + ctx.compTable = C.xkb_compose_table_new_from_locale(ctx.Ctx, cloc, + C.XKB_COMPOSE_COMPILE_NO_FLAGS) + if ctx.compTable == nil { + ctx.Destroy() + return nil, errors.New("newXKB: xkb_compose_table_new_from_locale failed") + } + ctx.compState = C.xkb_compose_state_new(ctx.compTable, + C.XKB_COMPOSE_STATE_NO_FLAGS) + if ctx.compState == nil { + ctx.Destroy() + return nil, errors.New("newXKB: xkb_compose_state_new failed") + } + return ctx, nil +} + +func (x *Context) DestroyKeymapState() { + if x.state != nil { + C.xkb_state_unref(x.state) + x.state = nil + } + if x.keyMap != nil { + C.xkb_keymap_unref(x.keyMap) + x.keyMap = nil + } +} + +// SetKeymap sets the keymap and state. The context takes ownership of the +// keymap and state and frees them in Destroy. +func (x *Context) SetKeymap(xkbKeyMap, xkbState unsafe.Pointer) { + x.DestroyKeymapState() + x.keyMap = (*C.struct_xkb_keymap)(xkbKeyMap) + x.state = (*C.struct_xkb_state)(xkbState) +} + +func (x *Context) LoadKeymap(format int, fd int, size int) error { + x.DestroyKeymapState() + mapData, err := syscall.Mmap(int(fd), 0, int(size), syscall.PROT_READ, + syscall.MAP_SHARED) + if err != nil { + return fmt.Errorf("newXKB: mmap of keymap failed: %v", err) + } + defer syscall.Munmap(mapData) + keyMap := C.xkb_keymap_new_from_buffer(x.Ctx, + (*C.char)(unsafe.Pointer(&mapData[0])), C.size_t(size-1), + C.XKB_KEYMAP_FORMAT_TEXT_V1, C.XKB_KEYMAP_COMPILE_NO_FLAGS) + if keyMap == nil { + return errors.New("newXKB: xkb_keymap_new_from_buffer failed") + } + state := C.xkb_state_new(keyMap) + if state == nil { + C.xkb_keymap_unref(keyMap) + return errors.New("newXKB: xkb_state_new failed") + } + x.keyMap = keyMap + x.state = state + return nil +} + +func (x *Context) Modifiers() key.Modifiers { + var mods key.Modifiers + if C.xkb_state_mod_name_is_active(x.state, + (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_CTRL[0])), + C.XKB_STATE_MODS_EFFECTIVE) == 1 { + mods |= key.ModCtrl + } + if C.xkb_state_mod_name_is_active(x.state, + (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_SHIFT[0])), + C.XKB_STATE_MODS_EFFECTIVE) == 1 { + mods |= key.ModShift + } + if C.xkb_state_mod_name_is_active(x.state, + (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_ALT[0])), + C.XKB_STATE_MODS_EFFECTIVE) == 1 { + mods |= key.ModAlt + } + if C.xkb_state_mod_name_is_active(x.state, + (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_LOGO[0])), + C.XKB_STATE_MODS_EFFECTIVE) == 1 { + mods |= key.ModSuper + } + return mods +} + +func (x *Context) DispatchKey(keyCode uint32, + state key.State) (events []event.Event) { + if x.state == nil { + return + } + kc := C.xkb_keycode_t(keyCode) + if len(x.utf8Buf) == 0 { + x.utf8Buf = make([]byte, 1) + } + sym := C.xkb_state_key_get_one_sym(x.state, kc) + if name, ok := convertKeysym(sym); ok { + cmd := key.Event{ + Name: name, + Modifiers: x.Modifiers(), + State: state, + } + // Ensure that a physical backtab key is translated to + // Shift-Tab. + if sym == C.XKB_KEY_ISO_Left_Tab { + cmd.Modifiers |= key.ModShift + } + events = append(events, cmd) + } + C.xkb_compose_state_feed(x.compState, sym) + var str []byte + switch C.xkb_compose_state_get_status(x.compState) { + case C.XKB_COMPOSE_CANCELLED, C.XKB_COMPOSE_COMPOSING: + return + case C.XKB_COMPOSE_COMPOSED: + size := C.xkb_compose_state_get_utf8(x.compState, + (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf))) + if int(size) >= len(x.utf8Buf) { + x.utf8Buf = make([]byte, size+1) + size = C.xkb_compose_state_get_utf8(x.compState, + (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), + C.size_t(len(x.utf8Buf))) + } + C.xkb_compose_state_reset(x.compState) + str = x.utf8Buf[:size] + case C.XKB_COMPOSE_NOTHING: + mod := x.Modifiers() + if mod&(key.ModCtrl|key.ModAlt|key.ModSuper) == 0 { + str = x.charsForKeycode(kc) + } + } + // Report only printable runes. + var n int + for n < len(str) { + r, s := utf8.DecodeRune(str) + if unicode.IsPrint(r) { + n += s + } else { + copy(str[n:], str[n+s:]) + str = str[:len(str)-s] + } + } + if state == key.Press && len(str) > 0 { + events = append(events, key.EditEvent{Text: string(str)}) + } + return +} + +func (x *Context) charsForKeycode(keyCode C.xkb_keycode_t) []byte { + size := C.xkb_state_key_get_utf8(x.state, keyCode, + (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf))) + if int(size) >= len(x.utf8Buf) { + x.utf8Buf = make([]byte, size+1) + size = C.xkb_state_key_get_utf8(x.state, keyCode, + (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf))) + } + return x.utf8Buf[:size] +} + +func (x *Context) IsRepeatKey(keyCode uint32) bool { + kc := C.xkb_keycode_t(keyCode) + return C.xkb_keymap_key_repeats(x.keyMap, kc) == 1 +} + +func (x *Context) UpdateMask(depressed, latched, locked, depressedGroup, latchedGroup, lockedGroup uint32) { + if x.state == nil { + return + } + C.xkb_state_update_mask(x.state, C.xkb_mod_mask_t(depressed), + C.xkb_mod_mask_t(latched), C.xkb_mod_mask_t(locked), + C.xkb_layout_index_t(depressedGroup), + C.xkb_layout_index_t(latchedGroup), C.xkb_layout_index_t(lockedGroup)) +} + +func convertKeysym(s C.xkb_keysym_t) (string, bool) { + if 'a' <= s && s <= 'z' { + return string(rune(s - 'a' + 'A')), true + } + if ' ' < s && s <= '~' { + return string(rune(s)), true + } + var n string + switch s { + case C.XKB_KEY_Escape: + n = key.NameEscape + case C.XKB_KEY_Left: + n = key.NameLeftArrow + case C.XKB_KEY_Right: + n = key.NameRightArrow + case C.XKB_KEY_Return: + n = key.NameReturn + case C.XKB_KEY_KP_Enter: + n = key.NameEnter + case C.XKB_KEY_Up: + n = key.NameUpArrow + case C.XKB_KEY_Down: + n = key.NameDownArrow + case C.XKB_KEY_Home: + n = key.NameHome + case C.XKB_KEY_End: + n = key.NameEnd + case C.XKB_KEY_BackSpace: + n = key.NameDeleteBackward + case C.XKB_KEY_Delete: + n = key.NameDeleteForward + case C.XKB_KEY_Page_Up: + n = key.NamePageUp + case C.XKB_KEY_Page_Down: + n = key.NamePageDown + case C.XKB_KEY_F1: + n = "F1" + case C.XKB_KEY_F2: + n = "F2" + case C.XKB_KEY_F3: + n = "F3" + case C.XKB_KEY_F4: + n = "F4" + case C.XKB_KEY_F5: + n = "F5" + case C.XKB_KEY_F6: + n = "F6" + case C.XKB_KEY_F7: + n = "F7" + case C.XKB_KEY_F8: + n = "F8" + case C.XKB_KEY_F9: + n = "F9" + case C.XKB_KEY_F10: + n = "F10" + case C.XKB_KEY_F11: + n = "F11" + case C.XKB_KEY_F12: + n = "F12" + case C.XKB_KEY_Tab, C.XKB_KEY_KP_Tab, C.XKB_KEY_ISO_Left_Tab: + n = key.NameTab + case 0x20, C.XKB_KEY_KP_Space: + n = key.NameSpace + default: + return "", false + } + return n, true +} diff --git a/gio/giold/app/loop.go b/gio/giold/app/loop.go new file mode 100644 index 0000000..6b2a57a --- /dev/null +++ b/gio/giold/app/loop.go @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +import ( + "image" + "image/color" + "runtime" + + "realy.lol/gio/app/internal/wm" + "realy.lol/gio/gpu" + "realy.lol/gio/op" +) + +type renderLoop struct { + summary string + drawing bool + err error + + frames chan frame + results chan frameResult + refresh chan struct{} + refreshErr chan error + ack chan struct{} + stop chan struct{} + stopped chan struct{} +} + +type frame struct { + viewport image.Point + ops *op.Ops +} + +type frameResult struct { + profile string + err error +} + +func newLoop(ctx wm.Context) (*renderLoop, error) { + l := &renderLoop{ + frames: make(chan frame), + results: make(chan frameResult), + refresh: make(chan struct{}), + refreshErr: make(chan error), + // Ack is buffered so GPU commands can be issued after + // ack'ing the frame. + ack: make(chan struct{}, 1), + stop: make(chan struct{}), + stopped: make(chan struct{}), + } + if err := l.renderLoop(ctx); err != nil { + return nil, err + } + return l, nil +} + +func (l *renderLoop) renderLoop(ctx wm.Context) error { + // GL Operations must happen on a single OS thread, so + // pass initialization result through a channel. + initErr := make(chan error) + go func() { + defer close(l.stopped) + runtime.LockOSThread() + // Don't UnlockOSThread to avoid reuse by the Go runtime. + + if err := ctx.MakeCurrent(); err != nil { + initErr <- err + return + } + g, err := gpu.New(ctx.API()) + if err != nil { + initErr <- err + return + } + defer g.Release() + initErr <- nil + loop: + for { + select { + case <-l.refresh: + l.refreshErr <- ctx.MakeCurrent() + case frame := <-l.frames: + ctx.Lock() + if runtime.GOOS == "js" { + // Use transparent black when Gio is embedded, to allow mixing of Gio and + // foreign content below. + g.Clear(color.NRGBA{A: 0x00, R: 0x00, G: 0x00, B: 0x00}) + } else { + g.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}) + } + g.Collect(frame.viewport, frame.ops) + // Signal that we're done with the frame ops. + l.ack <- struct{}{} + var res frameResult + res.err = g.Frame() + if res.err == nil { + res.err = ctx.Present() + } + res.profile = g.Profile() + ctx.Unlock() + l.results <- res + case <-l.stop: + break loop + } + } + }() + return <-initErr +} + +func (l *renderLoop) Release() { + // Flush error. + l.Flush() + close(l.stop) + <-l.stopped + l.stop = nil +} + +func (l *renderLoop) Flush() error { + if l.drawing { + st := <-l.results + l.setErr(st.err) + if st.profile != "" { + l.summary = st.profile + } + l.drawing = false + } + return l.err +} + +func (l *renderLoop) Summary() string { + return l.summary +} + +func (l *renderLoop) Refresh() { + if l.err != nil { + return + } + // Make sure any pending frame is complete. + l.Flush() + l.refresh <- struct{}{} + l.setErr(<-l.refreshErr) +} + +// Draw initiates a draw of a frame. It returns a channel +// than signals when the frame is no longer being accessed. +func (l *renderLoop) Draw(viewport image.Point, + frameOps *op.Ops) <-chan struct{} { + if l.err != nil { + l.ack <- struct{}{} + return l.ack + } + l.Flush() + l.frames <- frame{viewport, frameOps} + l.drawing = true + return l.ack +} + +func (l *renderLoop) setErr(err error) { + if l.err == nil { + l.err = err + } +} diff --git a/gio/giold/app/permission/bluetooth/main.go b/gio/giold/app/permission/bluetooth/main.go new file mode 100644 index 0000000..392bbbe --- /dev/null +++ b/gio/giold/app/permission/bluetooth/main.go @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package bluetooth implements permissions to access Bluetooth and Bluetooth +Low Energy hardware, including the ability to discover and pair devices. + +Android + +The following entries will be added to AndroidManifest.xml: + + + + + + + +Note that ACCESS_FINE_LOCATION is required on Android before the Bluetooth +device may be used. +See https://developer.android.com/guide/topics/connectivity/bluetooth. + +ACCESS_FINE_LOCATION is a "dangerous" permission. See documentation for +package realy.lol/gio/app/permission for more information. +*/ +package bluetooth diff --git a/gio/giold/app/permission/camera/main.go b/gio/giold/app/permission/camera/main.go new file mode 100644 index 0000000..1e89a31 --- /dev/null +++ b/gio/giold/app/permission/camera/main.go @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package camera implements permissions to access camera hardware. + +Android + +The following entries will be added to AndroidManifest.xml: + + + + +CAMERA is a "dangerous" permission. See documentation for package +realy.lol/gio/app/permission for more information. +*/ +package camera diff --git a/gio/giold/app/permission/doc.go b/gio/giold/app/permission/doc.go new file mode 100644 index 0000000..878a5cb --- /dev/null +++ b/gio/giold/app/permission/doc.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package permission includes sub-packages that should be imported +by a Gio program or by one of its dependencies to indicate that specific +operating-system permissions are required. For example, if a Gio +program requires access to a device's Bluetooth interface, it +should import "realy.lol/gio/app/permission/bluetooth" as follows: + + package main + + import ( + "realy.lol/gio/app" + _ "realy.lol/gio/app/permission/bluetooth" + ) + + func main() { + ... + } + +Since there are no exported identifiers in the app/permission/bluetooth +package, the import uses the anonymous identifier (_) as the imported +package name. + +As a special case, the gogio tool detects when a program directly or +indirectly depends on the "net" package from the Go standard library as an +indication that the program requires network access permissions. If a program +requires network permissions but does not directly or indirectly import +"net", it will be necessary to add the following code somewhere in the +program's source code: + + import ( + ... + _ "net" + ) + +Android -- Dangerous Permissions + +Certain permissions on Android are marked with a protection level of +"dangerous". This means that, in addition to including the relevant +Gio permission packages, your app will need to prompt the user +specifically to request access. To access the Android Activity +required for prompting, use app.ViewEvent (only available on Android). +app.ViewEvent exposes the underlying Android View, on which the +getContext method returns the Activity. + +For more information on dangerous permissions, see: +https://developer.android.com/guide/topics/permissions/overview#dangerous_permissions +*/ +package permission diff --git a/gio/giold/app/permission/networkstate/main.go b/gio/giold/app/permission/networkstate/main.go new file mode 100644 index 0000000..c594219 --- /dev/null +++ b/gio/giold/app/permission/networkstate/main.go @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package networkstate implements permissions to access network connectivity information. + +Android + +The following entries will be added to AndroidManifest.xml: + + + +*/ +package networkstate diff --git a/gio/giold/app/permission/storage/main.go b/gio/giold/app/permission/storage/main.go new file mode 100644 index 0000000..623a624 --- /dev/null +++ b/gio/giold/app/permission/storage/main.go @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package storage implements read and write storage permissions +on mobile devices. + +Android + +The following entries will be added to AndroidManifest.xml: + + + + +READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE are "dangerous" permissions. +See documentation for package realy.lol/gio/app/permission for more information. +*/ +package storage diff --git a/gio/giold/app/sigpipe_darwin.go b/gio/giold/app/sigpipe_darwin.go new file mode 100644 index 0000000..aca19b7 --- /dev/null +++ b/gio/giold/app/sigpipe_darwin.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build !go1.14 + +// Work around golang.org/issue/33384, fixed in CL 191785, +// to be released in Go 1.14. + +package app + +import ( + "os" + "os/signal" + "syscall" +) + +func init() { + signal.Notify(make(chan os.Signal), syscall.SIGPIPE) +} diff --git a/gio/giold/app/window.go b/gio/giold/app/window.go new file mode 100644 index 0000000..815e1e6 --- /dev/null +++ b/gio/giold/app/window.go @@ -0,0 +1,531 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +import ( + "errors" + "fmt" + "image" + "time" + + "realy.lol/gio/io/event" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/profile" + "realy.lol/gio/io/router" + "realy.lol/gio/io/system" + "realy.lol/gio/op" + "realy.lol/gio/unit" + + _ "realy.lol/gio/app/internal/log" + "realy.lol/gio/app/internal/wm" +) + +// WindowOption configures a wm. +type Option func(opts *wm.Options) + +// Window represents an operating system wm. +type Window struct { + driver wm.Driver + ctx wm.Context + loop *renderLoop + + // driverFuncs is a channel of functions to run when + // the Window has a valid driver. + driverFuncs chan func() + + out chan event.Event + in chan event.Event + ack chan struct{} + invalidates chan struct{} + frames chan *op.Ops + frameAck chan struct{} + // dead is closed when the window is destroyed. + dead chan struct{} + + stage system.Stage + animating bool + hasNextFrame bool + nextFrame time.Time + delayedDraw *time.Timer + + queue queue + cursor pointer.CursorName + + callbacks callbacks +} + +type callbacks struct { + w *Window +} + +// queue is an event.Queue implementation that distributes system events +// to the input handlers declared in the most recent frame. +type queue struct { + q router.Router +} + +// driverEvent is sent when a new native driver +// is available for the wm. +type driverEvent struct { + driver wm.Driver +} + +// Pre-allocate the ack event to avoid garbage. +var ackEvent event.Event + +// NewWindow creates a new window for a set of window +// options. The options are hints; the platform is free to +// ignore or adjust them. +// +// If the current program is running on iOS and Android, +// NewWindow returns the window previously created by the +// platform. +// +// Calling NewWindow more than once is not supported on +// iOS, Android, WebAssembly. +func NewWindow(options ...Option) *Window { + opts := new(wm.Options) + // Default options. + Size(unit.Px(800), unit.Px(600))(opts) + Title("Gio")(opts) + + for _, o := range options { + o(opts) + } + + w := &Window{ + in: make(chan event.Event), + out: make(chan event.Event), + ack: make(chan struct{}), + invalidates: make(chan struct{}, 1), + frames: make(chan *op.Ops), + frameAck: make(chan struct{}), + driverFuncs: make(chan func()), + dead: make(chan struct{}), + } + w.callbacks.w = w + go w.run(opts) + return w +} + +// Events returns the channel where events are delivered. +func (w *Window) Events() <-chan event.Event { + return w.out +} + +// update updates the wm. Paint operations updates the +// window contents, input operations declare input handlers, +// and so on. The supplied operations list completely replaces +// the window state from previous calls. +func (w *Window) update(frame *op.Ops) { + w.frames <- frame + <-w.frameAck +} + +func (w *Window) validateAndProcess(frameStart time.Time, size image.Point, + sync bool, frame *op.Ops) error { + for { + if w.loop != nil { + if err := w.loop.Flush(); err != nil { + w.destroyGPU() + if err == wm.ErrDeviceLost { + continue + } + return err + } + } + if w.loop == nil { + var err error + w.ctx, err = w.driver.NewContext() + if err != nil { + return err + } + w.loop, err = newLoop(w.ctx) + if err != nil { + w.ctx.Release() + return err + } + } + w.processFrame(frameStart, size, frame) + if sync { + if err := w.loop.Flush(); err != nil { + w.destroyGPU() + if err == wm.ErrDeviceLost { + continue + } + return err + } + } + return nil + } +} + +func (w *Window) processFrame(frameStart time.Time, size image.Point, + frame *op.Ops) { + sync := w.loop.Draw(size, frame) + w.queue.q.Frame(frame) + switch w.queue.q.TextInputState() { + case router.TextInputOpen: + w.driver.ShowTextInput(true) + case router.TextInputClose: + w.driver.ShowTextInput(false) + } + if txt, ok := w.queue.q.WriteClipboard(); ok { + go w.WriteClipboard(txt) + } + if w.queue.q.ReadClipboard() { + go w.ReadClipboard() + } + if w.queue.q.Profiling() { + frameDur := time.Since(frameStart) + frameDur = frameDur.Truncate(100 * time.Microsecond) + q := 100 * time.Microsecond + timings := fmt.Sprintf("tot:%7s %s", frameDur.Round(q), + w.loop.Summary()) + w.queue.q.Queue(profile.Event{Timings: timings}) + } + if t, ok := w.queue.q.WakeupTime(); ok { + w.setNextFrame(t) + } + // Opportunistically check whether Invalidate has been called, to avoid + // stopping and starting animation mode. + select { + case <-w.invalidates: + w.setNextFrame(time.Time{}) + default: + } + w.updateAnimation() + // Wait for the GPU goroutine to finish processing frame. + <-sync +} + +// Invalidate the window such that a FrameEvent will be generated immediately. +// If the window is inactive, the event is sent when the window becomes active. +// +// Note that Invalidate is intended for externally triggered updates, such as a +// response from a network request. InvalidateOp is more efficient for animation +// and similar internal updates. +// +// Invalidate is safe for concurrent use. +func (w *Window) Invalidate() { + select { + case w.invalidates <- struct{}{}: + default: + } +} + +// Option applies the options to the window. +func (w *Window) Option(opts ...Option) { + go w.driverDo(func() { + o := new(wm.Options) + for _, opt := range opts { + opt(o) + } + w.driver.Option(o) + }) +} + +// ReadClipboard initiates a read of the clipboard in the form +// of a clipboard.Event. Multiple reads may be coalesced +// to a single event. +func (w *Window) ReadClipboard() { + go w.driverDo(func() { + w.driver.ReadClipboard() + }) +} + +// WriteClipboard writes a string to the clipboard. +func (w *Window) WriteClipboard(s string) { + go w.driverDo(func() { + w.driver.WriteClipboard(s) + }) +} + +// SetCursorName changes the current window cursor to name. +func (w *Window) SetCursorName(name pointer.CursorName) { + go w.driverDo(func() { + w.driver.SetCursor(name) + }) +} + +// Close the wm. The window's event loop should exit when it receives +// system.DestroyEvent. +// +// Currently, only macOS, Windows and X11 drivers implement this functionality, +// all others are stubbed. +func (w *Window) Close() { + go w.driverDo(func() { + w.driver.Close() + }) +} + +// driverDo waits for the window to have a valid driver attached and calls f. +// It does nothing if the if the window was destroyed while waiting. +func (w *Window) driverDo(f func()) { + select { + case w.driverFuncs <- f: + case <-w.dead: + } +} + +func (w *Window) updateAnimation() { + animate := false + if w.delayedDraw != nil { + w.delayedDraw.Stop() + w.delayedDraw = nil + } + if w.stage >= system.StageRunning && w.hasNextFrame { + if dt := time.Until(w.nextFrame); dt <= 0 { + animate = true + } else { + w.delayedDraw = time.NewTimer(dt) + } + } + if animate != w.animating { + w.animating = animate + w.driver.SetAnimating(animate) + } +} + +func (w *Window) setNextFrame(at time.Time) { + if !w.hasNextFrame || at.Before(w.nextFrame) { + w.hasNextFrame = true + w.nextFrame = at + } +} + +func (c *callbacks) SetDriver(d wm.Driver) { + c.Event(driverEvent{d}) +} + +func (c *callbacks) Event(e event.Event) { + select { + case c.w.in <- e: + <-c.w.ack + case <-c.w.dead: + } +} + +func (w *Window) waitAck() { + // Send a dummy event; when it gets through we + // know the application has processed the previous event. + w.out <- ackEvent +} + +// Prematurely destroy the window and wait for the native window +// destroy event. +func (w *Window) destroy(err error) { + w.destroyGPU() + // Ack the current event. + w.ack <- struct{}{} + w.out <- system.DestroyEvent{Err: err} + close(w.dead) + for e := range w.in { + w.ack <- struct{}{} + if _, ok := e.(system.DestroyEvent); ok { + return + } + } +} + +func (w *Window) destroyGPU() { + if w.loop != nil { + w.loop.Release() + w.loop = nil + } + if w.ctx != nil { + w.ctx.Release() + w.ctx = nil + } +} + +// waitFrame waits for the client to either call FrameEvent.Frame +// or to continue event handling. It returns whether the client +// called Frame or not. +func (w *Window) waitFrame() (*op.Ops, bool) { + select { + case frame := <-w.frames: + // The client called FrameEvent.Frame. + return frame, true + case w.out <- ackEvent: + // The client ignored FrameEvent and continued processing + // events. + return nil, false + } +} + +func (w *Window) run(opts *wm.Options) { + defer close(w.in) + defer close(w.out) + if err := wm.NewWindow(&w.callbacks, opts); err != nil { + w.out <- system.DestroyEvent{Err: err} + return + } + for { + var driverFuncs chan func() + if w.driver != nil { + driverFuncs = w.driverFuncs + } + var timer <-chan time.Time + if w.delayedDraw != nil { + timer = w.delayedDraw.C + } + select { + case <-timer: + w.setNextFrame(time.Time{}) + w.updateAnimation() + case <-w.invalidates: + w.setNextFrame(time.Time{}) + w.updateAnimation() + case f := <-driverFuncs: + f() + case e := <-w.in: + switch e2 := e.(type) { + case system.StageEvent: + if w.loop != nil { + if e2.Stage < system.StageRunning { + w.destroyGPU() + } else { + w.loop.Refresh() + } + } + w.stage = e2.Stage + w.updateAnimation() + w.out <- e + w.waitAck() + case wm.FrameEvent: + if e2.Size == (image.Point{}) { + panic(errors.New("internal error: zero-sized Draw")) + } + if w.stage < system.StageRunning { + // No drawing if not visible. + break + } + frameStart := time.Now() + w.hasNextFrame = false + e2.Frame = w.update + e2.Queue = &w.queue + w.out <- e2.FrameEvent + if w.loop != nil { + if e2.Sync { + w.loop.Refresh() + } + } + frame, gotFrame := w.waitFrame() + err := w.validateAndProcess(frameStart, e2.Size, e2.Sync, frame) + if gotFrame { + // We're done with frame, let the client continue. + w.frameAck <- struct{}{} + } + if err != nil { + w.destroyGPU() + w.destroy(err) + return + } + w.updateCursor() + case *system.CommandEvent: + w.out <- e + w.waitAck() + case driverEvent: + w.driver = e2.driver + case system.DestroyEvent: + w.destroyGPU() + w.out <- e2 + w.ack <- struct{}{} + return + case event.Event: + if w.queue.q.Queue(e2) { + w.setNextFrame(time.Time{}) + w.updateAnimation() + } + w.updateCursor() + w.out <- e + } + w.ack <- struct{}{} + } + } +} + +func (w *Window) updateCursor() { + if c := w.queue.q.Cursor(); c != w.cursor { + w.cursor = c + w.SetCursorName(c) + } +} + +func (q *queue) Events(k event.Tag) []event.Event { + return q.q.Events(k) +} + +const ( + // Windowed is the normal window mode with OS specific window decorations. + Windowed = wm.Windowed + // Fullscreen is the full screen window mode. + Fullscreen = wm.Fullscreen +) + +// WindowMode sets the window mode. +// +// Supported platforms are macOS, X11 and Windows. +func WindowMode(mode wm.WindowMode) Option { + return func(opts *wm.Options) { + opts.WindowMode = &mode + } +} + +// Title sets the title of the wm. +func Title(t string) Option { + return func(opts *wm.Options) { + opts.Title = &t + } +} + +// Size sets the size of the wm. +func Size(w, h unit.Value) Option { + if w.V <= 0 { + panic("width must be larger than or equal to 0") + } + if h.V <= 0 { + panic("height must be larger than or equal to 0") + } + return func(opts *wm.Options) { + opts.Size = &wm.Size{ + Width: w, + Height: h, + } + } +} + +// MaxSize sets the maximum size of the wm. +func MaxSize(w, h unit.Value) Option { + if w.V <= 0 { + panic("width must be larger than or equal to 0") + } + if h.V <= 0 { + panic("height must be larger than or equal to 0") + } + return func(opts *wm.Options) { + opts.MaxSize = &wm.Size{ + Width: w, + Height: h, + } + } +} + +// MinSize sets the minimum size of the wm. +func MinSize(w, h unit.Value) Option { + if w.V <= 0 { + panic("width must be larger than or equal to 0") + } + if h.V <= 0 { + panic("height must be larger than or equal to 0") + } + return func(opts *wm.Options) { + opts.MinSize = &wm.Size{ + Width: w, + Height: h, + } + } +} + +func (driverEvent) ImplementsEvent() {} diff --git a/gio/giold/cmd/go.local.sum b/gio/giold/cmd/go.local.sum new file mode 100644 index 0000000..c197ac2 --- /dev/null +++ b/gio/giold/cmd/go.local.sum @@ -0,0 +1,53 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o= +github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg= +github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g= +github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194= +github.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs= +github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0= +github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197 h1:7+SpRyhoo46QjKkYInQXpcfxx3TYFEYkn131lwGE9/0= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/gio/giold/cmd/go.sum b/gio/giold/cmd/go.sum new file mode 100644 index 0000000..c197ac2 --- /dev/null +++ b/gio/giold/cmd/go.sum @@ -0,0 +1,53 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o= +github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg= +github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g= +github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194= +github.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs= +github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0= +github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197 h1:7+SpRyhoo46QjKkYInQXpcfxx3TYFEYkn131lwGE9/0= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/gio/giold/cmd/gogio/android_test.go b/gio/giold/cmd/gogio/android_test.go new file mode 100644 index 0000000..e73386f --- /dev/null +++ b/gio/giold/cmd/gogio/android_test.go @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "bytes" + "context" + "fmt" + "image" + "image/png" + "os" + "os/exec" + "path/filepath" + "regexp" +) + +type AndroidTestDriver struct { + driverBase + + sdkDir string + adbPath string +} + +var rxAdbDevice = regexp.MustCompile(`(.*)\s+device$`) + +func (d *AndroidTestDriver) Start(path string) { + d.sdkDir = os.Getenv("ANDROID_SDK_ROOT") + if d.sdkDir == "" { + d.Skipf("Android SDK is required; set $ANDROID_SDK_ROOT") + } + d.adbPath = filepath.Join(d.sdkDir, "platform-tools", "adb") + if _, err := os.Stat(d.adbPath); os.IsNotExist(err) { + d.Skipf("adb not found") + } + + devOut := bytes.TrimSpace(d.adb("devices")) + devices := rxAdbDevice.FindAllSubmatch(devOut, -1) + switch len(devices) { + case 0: + d.Skipf("no Android devices attached via adb; skipping") + case 1: + default: + d.Skipf("multiple Android devices attached via adb; skipping") + } + + // If the device is attached but asleep, it's probably just charging. + // Don't use it; the screen needs to be on and unlocked for the test to + // work. + if !bytes.Contains( + d.adb("shell", "dumpsys", "power"), + []byte(" mWakefulness=Awake"), + ) { + d.Skipf("Android device isn't awake; skipping") + } + + // First, build the app. + apk := filepath.Join(d.tempDir("gio-endtoend-android"), "e2e.apk") + d.gogio("-target=android", "-appid="+appid, "-o="+apk, path) + + // Make sure the app isn't installed already, and try to uninstall it + // when we finish. Previous failed test runs might have left the app. + d.tryUninstall() + d.adb("install", apk) + d.Cleanup(d.tryUninstall) + + // Force our e2e app to be fullscreen, so that the android system bar at + // the top doesn't mess with our screenshots. + // TODO(mvdan): is there a way to do this via gio, so that we don't need + // to set up a global Android setting via the shell? + d.adb("shell", "settings", "put", "global", "policy_control", "immersive.full="+appid) + + // Make sure the app isn't already running. + d.adb("shell", "pm", "clear", appid) + + // Start listening for log messages. + { + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, d.adbPath, + "logcat", + "-s", // suppress other logs + "-T1", // don't show previous log messages + appid+":*", // show all logs from our gio app ID + ) + output, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + cmd.Stderr = cmd.Stdout + d.output = output + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + } + + // Start the app. + d.adb("shell", "monkey", "-p", appid, "1") + + // Wait for the gio app to render. + d.waitForFrame() +} + +func (d *AndroidTestDriver) Screenshot() image.Image { + out := d.adb("shell", "screencap", "-p") + img, err := png.Decode(bytes.NewReader(out)) + if err != nil { + d.Fatal(err) + } + return img +} + +func (d *AndroidTestDriver) tryUninstall() { + cmd := exec.Command(d.adbPath, "shell", "pm", "uninstall", appid) + out, err := cmd.CombinedOutput() + if err != nil { + if bytes.Contains(out, []byte("Unknown package")) { + // The package is not installed. Don't log anything. + return + } + d.Logf("could not uninstall: %v\n%s", err, out) + } +} + +func (d *AndroidTestDriver) adb(args ...interface{}) []byte { + strs := []string{} + for _, arg := range args { + strs = append(strs, fmt.Sprint(arg)) + } + cmd := exec.Command(d.adbPath, strs...) + out, err := cmd.CombinedOutput() + if err != nil { + d.Errorf("%s", out) + d.Fatal(err) + } + return out +} + +func (d *AndroidTestDriver) Click(x, y int) { + d.adb("shell", "input", "tap", x, y) + + // Wait for the gio app to render after this click. + d.waitForFrame() +} diff --git a/gio/giold/cmd/gogio/androidbuild.go b/gio/giold/cmd/gogio/androidbuild.go new file mode 100644 index 0000000..4a055b9 --- /dev/null +++ b/gio/giold/cmd/gogio/androidbuild.go @@ -0,0 +1,1032 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "archive/zip" + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "text/template" + + "golang.org/x/sync/errgroup" + "golang.org/x/tools/go/packages" +) + +type androidTools struct { + buildtools string + androidjar string +} + +// zip.Writer with a sticky error. +type zipWriter struct { + err error + w *zip.Writer +} + +// Writer that saves any errors. +type errWriter struct { + w io.Writer + err *error +} + +var exeSuffix string + +type manifestData struct { + AppID string + Version int + MinSDK int + TargetSDK int + Permissions []string + Features []string + IconSnip string + AppName string +} + +const ( + themes = ` + + +` + themesV21 = ` + + +` +) + +func init() { + if runtime.GOOS == "windows" { + exeSuffix = ".exe" + } +} + +func buildAndroid(tmpDir string, bi *buildInfo) error { + sdk := os.Getenv("ANDROID_SDK_ROOT") + if sdk == "" { + return errors.New("please set ANDROID_SDK_ROOT to the Android SDK path") + } + if _, err := os.Stat(sdk); err != nil { + return err + } + platform, err := latestPlatform(sdk) + if err != nil { + return err + } + buildtools, err := latestTools(sdk) + if err != nil { + return err + } + + tools := &androidTools{ + buildtools: buildtools, + androidjar: filepath.Join(platform, "android.jar"), + } + perms := []string{"default"} + const permPref = "realy.lol/gio/app/permission/" + cfg := &packages.Config{ + Mode: packages.NeedName + + packages.NeedFiles + + packages.NeedImports + + packages.NeedDeps, + Env: append( + os.Environ(), + "GOOS=android", + "CGO_ENABLED=1", + ), + } + pkgs, err := packages.Load(cfg, bi.pkgPath) + if err != nil { + return err + } + var extraJars []string + visitedPkgs := make(map[string]bool) + var visitPkg func(*packages.Package) error + visitPkg = func(p *packages.Package) error { + if len(p.GoFiles) == 0 { + return nil + } + dir := filepath.Dir(p.GoFiles[0]) + jars, err := filepath.Glob(filepath.Join(dir, "*.jar")) + if err != nil { + return err + } + extraJars = append(extraJars, jars...) + switch { + case p.PkgPath == "net": + perms = append(perms, "network") + case strings.HasPrefix(p.PkgPath, permPref): + perms = append(perms, p.PkgPath[len(permPref):]) + } + + for _, imp := range p.Imports { + if !visitedPkgs[imp.ID] { + visitPkg(imp) + visitedPkgs[imp.ID] = true + } + } + return nil + } + if err := visitPkg(pkgs[0]); err != nil { + return err + } + + if err := compileAndroid(tmpDir, tools, bi); err != nil { + return err + } + switch *buildMode { + case "archive": + return archiveAndroid(tmpDir, bi, perms) + case "exe": + file := *destPath + if file == "" { + file = fmt.Sprintf("%s.apk", bi.name) + } + + isBundle := false + switch filepath.Ext(file) { + case ".apk": + case ".aab": + isBundle = true + default: + return fmt.Errorf("the specified output %q does not end in '.apk' or '.aab'", + file) + } + + if err := exeAndroid(tmpDir, tools, bi, extraJars, perms, + isBundle); err != nil { + return err + } + if isBundle { + return signAAB(tmpDir, file, tools, bi) + } + return signAPK(tmpDir, file, tools, bi) + default: + panic("unreachable") + } +} + +func compileAndroid(tmpDir string, tools *androidTools, + bi *buildInfo) (err error) { + androidHome := os.Getenv("ANDROID_SDK_ROOT") + if androidHome == "" { + return errors.New("ANDROID_SDK_ROOT is not set. Please point it to the root of the Android SDK") + } + javac, err := findJavaC() + if err != nil { + return fmt.Errorf("could not find javac: %v", err) + } + ndkRoot, err := findNDK(androidHome) + if err != nil { + return err + } + minSDK := 16 + if bi.minsdk > minSDK { + minSDK = bi.minsdk + } + tcRoot := filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", + archNDK()) + var builds errgroup.Group + for _, a := range bi.archs { + arch := allArchs[a] + clang, err := latestCompiler(tcRoot, a, minSDK) + if err != nil { + return fmt.Errorf("%s. Please make sure you have NDK >= r19c installed. Use the command `sdkmanager ndk-bundle` to install it.", + err) + } + if runtime.GOOS == "windows" { + // Because of https://github.com/android-ndk/ndk/issues/920, + // we need NDK r19c, not just r19b. Check for the presence of + // clang++.cmd which is only available in r19c. + clangpp := clang + "++.cmd" + if _, err := os.Stat(clangpp); err != nil { + return fmt.Errorf("NDK version r19b detected, but >= r19c is required. Use the command `sdkmanager ndk-bundle` to install it") + } + } + archDir := filepath.Join(tmpDir, "jni", arch.jniArch) + if err := os.MkdirAll(archDir, 0755); err != nil { + return fmt.Errorf("failed to create %q: %v", archDir, err) + } + libFile := filepath.Join(archDir, "libgio.so") + cmd := exec.Command( + "go", + "build", + "-ldflags=-w -s "+bi.ldflags, + "-buildmode=c-shared", + "-tags", bi.tags, + "-o", libFile, + bi.pkgPath, + ) + cmd.Env = append( + os.Environ(), + "GOOS=android", + "GOARCH="+a, + "GOARM=7", // Avoid softfloat. + "CGO_ENABLED=1", + "CC="+clang, + ) + builds.Go(func() error { + _, err := runCmd(cmd) + return err + }) + } + appDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", + "realy.lol/gio/app/internal/wm")) + if err != nil { + return err + } + javaFiles, err := filepath.Glob(filepath.Join(appDir, "*.java")) + if err != nil { + return err + } + if len(javaFiles) > 0 { + classes := filepath.Join(tmpDir, "classes") + if err := os.MkdirAll(classes, 0755); err != nil { + return err + } + javac := exec.Command( + javac, + "-target", "1.8", + "-source", "1.8", + "-sourcepath", appDir, + "-bootclasspath", tools.androidjar, + "-d", classes, + ) + javac.Args = append(javac.Args, javaFiles...) + builds.Go(func() error { + _, err := runCmd(javac) + return err + }) + } + return builds.Wait() +} + +func archiveAndroid(tmpDir string, bi *buildInfo, perms []string) (err error) { + aarFile := *destPath + if aarFile == "" { + aarFile = fmt.Sprintf("%s.aar", bi.name) + } + if filepath.Ext(aarFile) != ".aar" { + return fmt.Errorf("the specified output %q does not end in '.aar'", + aarFile) + } + aar, err := os.Create(aarFile) + if err != nil { + return err + } + defer func() { + if cerr := aar.Close(); err == nil { + err = cerr + } + }() + aarw := newZipWriter(aar) + defer aarw.Close() + aarw.Create("R.txt") + themesXML := aarw.Create("res/values/themes.xml") + themesXML.Write([]byte(themes)) + themesXML21 := aarw.Create("res/values-v21/themes.xml") + themesXML21.Write([]byte(themesV21)) + permissions, features := getPermissions(perms) + // Disable input emulation on ChromeOS. + manifest := aarw.Create("AndroidManifest.xml") + manifestSrc := manifestData{ + AppID: bi.appID, + MinSDK: bi.minsdk, + Permissions: permissions, + Features: features, + } + tmpl, err := template.New("manifest").Parse( + ` + +{{range .Permissions}} +{{end}}{{range .Features}} +{{end}} +`) + if err != nil { + panic(err) + } + err = tmpl.Execute(manifest, manifestSrc) + proguard := aarw.Create("proguard.txt") + proguard.Write([]byte(`-keep class org.gioui.** { *; }`)) + + for _, a := range bi.archs { + arch := allArchs[a] + libFile := filepath.Join("jni", arch.jniArch, "libgio.so") + aarw.Add(filepath.ToSlash(libFile), filepath.Join(tmpDir, libFile)) + } + classes := filepath.Join(tmpDir, "classes") + if _, err := os.Stat(classes); err == nil { + jarFile := filepath.Join(tmpDir, "classes.jar") + if err := writeJar(jarFile, classes); err != nil { + return err + } + aarw.Add("classes.jar", jarFile) + } + return aarw.Close() +} + +func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, + extraJars, perms []string, isBundle bool) (err error) { + classes := filepath.Join(tmpDir, "classes") + var classFiles []string + err = filepath.Walk(classes, + func(path string, f os.FileInfo, err error) error { + if err != nil { + return err + } + if filepath.Ext(path) == ".class" { + classFiles = append(classFiles, path) + } + return nil + }) + classFiles = append(classFiles, extraJars...) + dexDir := filepath.Join(tmpDir, "apk") + if err := os.MkdirAll(dexDir, 0755); err != nil { + return err + } + if len(classFiles) > 0 { + d8 := exec.Command( + filepath.Join(tools.buildtools, "d8"), + "--classpath", tools.androidjar, + "--output", dexDir, + ) + d8.Args = append(d8.Args, classFiles...) + if _, err := runCmd(d8); err != nil { + return err + } + } + + // Compile resources. + resDir := filepath.Join(tmpDir, "res") + valDir := filepath.Join(resDir, "values") + v21Dir := filepath.Join(resDir, "values-v21") + for _, dir := range []string{valDir, v21Dir} { + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + } + iconSnip := "" + if _, err := os.Stat(bi.iconPath); err == nil { + err := buildIcons(resDir, bi.iconPath, []iconVariant{ + {path: filepath.Join("mipmap-hdpi", "ic_launcher.png"), size: 72}, + {path: filepath.Join("mipmap-xhdpi", "ic_launcher.png"), size: 96}, + {path: filepath.Join("mipmap-xxhdpi", "ic_launcher.png"), + size: 144}, + {path: filepath.Join("mipmap-xxxhdpi", "ic_launcher.png"), + size: 192}, + }) + if err != nil { + return err + } + iconSnip = `android:icon="@mipmap/ic_launcher"` + } + err = ioutil.WriteFile(filepath.Join(valDir, "themes.xml"), []byte(themes), + 0660) + if err != nil { + return err + } + err = ioutil.WriteFile(filepath.Join(v21Dir, "themes.xml"), + []byte(themesV21), 0660) + if err != nil { + return err + } + resZip := filepath.Join(tmpDir, "resources.zip") + aapt2 := filepath.Join(tools.buildtools, "aapt2") + _, err = runCmd(exec.Command( + aapt2, + "compile", + "-o", resZip, + "--dir", resDir)) + if err != nil { + return err + } + + // Link APK. + // Currently, new apps must have a target SDK version of at least 30. + // https://developer.android.com/distribute/best-practices/develop/target-sdk + targetSDK := 30 + if bi.minsdk > targetSDK { + targetSDK = bi.minsdk + } + minSDK := 16 + if bi.minsdk > minSDK { + minSDK = bi.minsdk + } + permissions, features := getPermissions(perms) + appName := strings.Title(bi.name) + manifestSrc := manifestData{ + AppID: bi.appID, + Version: bi.version, + MinSDK: minSDK, + TargetSDK: targetSDK, + Permissions: permissions, + Features: features, + IconSnip: iconSnip, + AppName: appName, + } + tmpl, err := template.New("test").Parse( + ` + + +{{range .Permissions}} +{{end}}{{range .Features}} +{{end}} + + + + + + + +`) + var manifestBuffer bytes.Buffer + if err := tmpl.Execute(&manifestBuffer, manifestSrc); err != nil { + return err + } + manifest := filepath.Join(tmpDir, "AndroidManifest.xml") + if err := ioutil.WriteFile(manifest, manifestBuffer.Bytes(), + 0660); err != nil { + return err + } + + linkAPK := filepath.Join(tmpDir, "link.apk") + + args := []string{ + "link", + "--manifest", manifest, + "-I", tools.androidjar, + "-o", linkAPK, + } + if isBundle { + args = append(args, "--proto-format") + } + args = append(args, resZip) + + if _, err := runCmd(exec.Command(aapt2, args...)); err != nil { + return err + } + + // The Go standard library archive/zip doesn't support appending to zip + // files. Copy files from `link.apk` (generated by aapt2) along with classes.dex and + // the Go libraries to a new `app.zip` file. + + // Load link.apk as zip. + linkAPKZip, err := zip.OpenReader(linkAPK) + if err != nil { + return err + } + defer linkAPKZip.Close() + + // Create new "APK". + unsignedAPK := filepath.Join(tmpDir, "app.zip") + unsignedAPKFile, err := os.Create(unsignedAPK) + if err != nil { + return err + } + defer func() { + if cerr := unsignedAPKFile.Close(); err == nil { + err = cerr + } + }() + unsignedAPKZip := zip.NewWriter(unsignedAPKFile) + defer unsignedAPKZip.Close() + + // Copy files from linkAPK to unsignedAPK. + for _, f := range linkAPKZip.File { + header := zip.FileHeader{ + Name: f.FileHeader.Name, + Method: f.FileHeader.Method, + } + + if isBundle { + // AAB have pre-defined folders. + switch header.Name { + case "AndroidManifest.xml": + header.Name = "manifest/AndroidManifest.xml" + } + } + + w, err := unsignedAPKZip.CreateHeader(&header) + if err != nil { + return err + } + r, err := f.Open() + if err != nil { + return err + } + if _, err := io.Copy(w, r); err != nil { + return err + } + } + + // Append new files (that doesn't exists inside the link.apk). + appendToZip := func(path string, file string) error { + f, err := os.Open(file) + if err != nil { + return err + } + defer f.Close() + w, err := unsignedAPKZip.CreateHeader(&zip.FileHeader{ + Name: filepath.ToSlash(path), + Method: zip.Deflate, + }) + if err != nil { + return err + } + _, err = io.Copy(w, f) + return err + } + + // Append Go binaries (libgio.so). + for _, a := range bi.archs { + arch := allArchs[a] + libFile := filepath.Join(arch.jniArch, "libgio.so") + if err := appendToZip(filepath.Join("lib", libFile), + filepath.Join(tmpDir, "jni", libFile)); err != nil { + return err + } + } + + // Append classes.dex. + classesFolder := "classes.dex" + if isBundle { + classesFolder = "dex/classes.dex" + } + if err := appendToZip(classesFolder, + filepath.Join(dexDir, "classes.dex")); err != nil { + return err + } + + return unsignedAPKZip.Close() +} + +func signAPK(tmpDir string, apkFile string, tools *androidTools, + bi *buildInfo) error { + if err := zipalign(tools, filepath.Join(tmpDir, "app.zip"), + apkFile); err != nil { + return err + } + + if bi.key == "" { + if err := defaultAndroidKeystore(tmpDir, bi); err != nil { + return err + } + } + + _, err := runCmd(exec.Command( + filepath.Join(tools.buildtools, "apksigner"), + "sign", + "--ks-pass", "pass:"+bi.password, + "--ks", bi.key, + apkFile, + )) + + return err +} + +func signAAB(tmpDir string, aabFile string, tools *androidTools, + bi *buildInfo) error { + allBundleTools, err := filepath.Glob(filepath.Join(tools.buildtools, + "bundletool*.jar")) + if err != nil { + return err + } + + bundletool := "" + for _, v := range allBundleTools { + bundletool = v + break + } + + if bundletool == "" { + return fmt.Errorf("bundletool was not found at %s. Download it from https://github.com/google/bundletool/releases and move to the respective folder", + tools.buildtools) + } + + _, err = runCmd(exec.Command( + "java", + "-jar", bundletool, + "build-bundle", + "--modules="+filepath.Join(tmpDir, "app.zip"), + "--output="+filepath.Join(tmpDir, "app.aab"), + )) + if err != nil { + return err + } + + if err := zipalign(tools, filepath.Join(tmpDir, "app.aab"), + aabFile); err != nil { + return err + } + + if bi.key == "" { + if err := defaultAndroidKeystore(tmpDir, bi); err != nil { + return err + } + } + + keytoolList, err := runCmd(exec.Command( + "keytool", + "-keystore", bi.key, + "-list", + "-keypass", bi.password, + "-v", + )) + if err != nil { + return err + } + + var alias string + for _, t := range strings.Split(keytoolList, "\n") { + if i, _ := fmt.Sscanf(t, "Alias name: %s", &alias); i > 0 { + break + } + } + + _, err = runCmd(exec.Command( + filepath.Join("jarsigner"), + "-sigalg", "SHA256withRSA", + "-digestalg", "SHA-256", + "-keystore", bi.key, + "-storepass", bi.password, + aabFile, + strings.TrimSpace(alias), + )) + + return err +} + +func zipalign(tools *androidTools, input, output string) error { + _, err := runCmd(exec.Command( + filepath.Join(tools.buildtools, "zipalign"), + "-f", + "4", // 32-bit alignment. + input, + output, + )) + return err +} + +func defaultAndroidKeystore(tmpDir string, bi *buildInfo) error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + + // Use debug.keystore, if exists. + bi.key = filepath.Join(home, ".android", "debug.keystore") + bi.password = "android" + if _, err := os.Stat(bi.key); err == nil { + return nil + } + + // Generate new key. + bi.key = filepath.Join(tmpDir, "sign.keystore") + keytool, err := findKeytool() + if err != nil { + return err + } + _, err = runCmd(exec.Command( + keytool, + "-genkey", + "-keystore", bi.key, + "-storepass", bi.password, + "-alias", "android", + "-keyalg", "RSA", "-keysize", "2048", + "-validity", "10000", + "-noprompt", + "-dname", "CN=android", + )) + return err +} + +func findNDK(androidHome string) (string, error) { + ndks, err := filepath.Glob(filepath.Join(androidHome, "ndk", "*")) + if err != nil { + return "", err + } + if bestNDK, found := latestVersionPath(ndks); found { + return bestNDK, nil + } + // The old NDK path was $ANDROID_SDK_ROOT/ndk-bundle. + ndkBundle := filepath.Join(androidHome, "ndk-bundle") + if _, err := os.Stat(ndkBundle); err == nil { + return ndkBundle, nil + } + // Certain non-standard NDK isntallations set the $ANDROID_NDK_ROOT + // environment variable + if ndkBundle, ok := os.LookupEnv("ANDROID_NDK_ROOT"); ok { + if _, err := os.Stat(ndkBundle); err == nil { + return ndkBundle, nil + } + } + + return "", fmt.Errorf("no NDK found in $ANDROID_SDK_ROOT (%s). Set $ANDROID_NDK_ROOT or use `sdkmanager ndk-bundle` to install the NDK", + androidHome) +} + +func findKeytool() (string, error) { + keytool, err := exec.LookPath("keytool") + if err == nil { + return keytool, err + } + javaHome := os.Getenv("JAVA_HOME") + if javaHome == "" { + return "", err + } + keytool = filepath.Join(javaHome, "jre", "bin", "keytool"+exeSuffix) + if _, serr := os.Stat(keytool); serr == nil { + return keytool, nil + } + return "", err +} + +func findJavaC() (string, error) { + javac, err := exec.LookPath("javac") + if err == nil { + return javac, err + } + javaHome := os.Getenv("JAVA_HOME") + if javaHome == "" { + return "", err + } + javac = filepath.Join(javaHome, "bin", "javac"+exeSuffix) + if _, serr := os.Stat(javac); serr == nil { + return javac, nil + } + return "", err +} + +func writeJar(jarFile, dir string) (err error) { + jar, err := os.Create(jarFile) + if err != nil { + return err + } + defer func() { + if cerr := jar.Close(); err == nil { + err = cerr + } + }() + jarw := newZipWriter(jar) + const manifestHeader = `Manifest-Version: 1.0 +Created-By: 1.0 (Go) + +` + jarw.Create("META-INF/MANIFEST.MF").Write([]byte(manifestHeader)) + err = filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { + if err != nil { + return err + } + if f.IsDir() { + return nil + } + if filepath.Ext(path) == ".class" { + rel := filepath.ToSlash(path[len(dir)+1:]) + jarw.Add(rel, path) + } + return nil + }) + if err != nil { + return err + } + return jarw.Close() +} + +func archNDK() string { + var arch string + switch runtime.GOARCH { + case "386": + arch = "x86" + case "amd64": + arch = "x86_64" + default: + panic("unsupported GOARCH: " + runtime.GOARCH) + } + return runtime.GOOS + "-" + arch +} + +func getPermissions(ps []string) ([]string, []string) { + var permissions, features []string + seenPermissions := make(map[string]bool) + seenFeatures := make(map[string]bool) + for _, perm := range ps { + for _, x := range AndroidPermissions[perm] { + if !seenPermissions[x] { + permissions = append(permissions, x) + seenPermissions[x] = true + } + } + for _, x := range AndroidFeatures[perm] { + if !seenFeatures[x] { + features = append(features, x) + seenFeatures[x] = true + } + } + } + return permissions, features +} + +func latestPlatform(sdk string) (string, error) { + allPlats, err := filepath.Glob(filepath.Join(sdk, "platforms", "android-*")) + if err != nil { + return "", err + } + var bestVer int + var bestPlat string + for _, platform := range allPlats { + _, name := filepath.Split(platform) + // The glob above guarantees the "android-" prefix. + verStr := name[len("android-"):] + ver, err := strconv.Atoi(verStr) + if err != nil { + continue + } + if ver < bestVer { + continue + } + bestVer = ver + bestPlat = platform + } + if bestPlat == "" { + return "", fmt.Errorf("no platforms found in %q", sdk) + } + return bestPlat, nil +} + +func latestCompiler(tcRoot, a string, minsdk int) (string, error) { + arch := allArchs[a] + allComps, err := filepath.Glob(filepath.Join(tcRoot, "bin", + arch.clangArch+"*-clang")) + if err != nil { + return "", err + } + var bestVer int + var firstVer int + var bestCompiler string + var firstCompiler string + for _, compiler := range allComps { + var ver int + pattern := filepath.Join(tcRoot, "bin", arch.clangArch) + "%d-clang" + if n, err := fmt.Sscanf(compiler, pattern, &ver); n < 1 || err != nil { + continue + } + if firstCompiler == "" || ver < firstVer { + firstVer = ver + firstCompiler = compiler + } + if ver < bestVer { + continue + } + if ver > minsdk { + continue + } + bestVer = ver + bestCompiler = compiler + } + if bestCompiler == "" { + bestCompiler = firstCompiler + } + if bestCompiler == "" { + return "", fmt.Errorf("no NDK compiler found for architecture %s in %s", + a, tcRoot) + } + return bestCompiler, nil +} + +func latestTools(sdk string) (string, error) { + allTools, err := filepath.Glob(filepath.Join(sdk, "build-tools", "*")) + if err != nil { + return "", err + } + tools, found := latestVersionPath(allTools) + if !found { + return "", fmt.Errorf("no build-tools found in %q", sdk) + } + return tools, nil +} + +// latestVersionFile finds the path with the highest version +// among paths on the form +// +// /some/path/major.minor.patch +func latestVersionPath(paths []string) (string, bool) { + var bestVer [3]int + var bestDir string +loop: + for _, path := range paths { + name := filepath.Base(path) + s := strings.SplitN(name, ".", 3) + if len(s) != len(bestVer) { + continue + } + var version [3]int + for i, v := range s { + v, err := strconv.Atoi(v) + if err != nil { + continue loop + } + if v < bestVer[i] { + continue loop + } + if v > bestVer[i] { + break + } + version[i] = v + } + bestVer = version + bestDir = path + } + return bestDir, bestDir != "" +} + +func newZipWriter(w io.Writer) *zipWriter { + return &zipWriter{ + w: zip.NewWriter(w), + } +} + +func (z *zipWriter) Close() error { + err := z.w.Close() + if z.err == nil { + z.err = err + } + return z.err +} + +func (z *zipWriter) Create(name string) io.Writer { + if z.err != nil { + return ioutil.Discard + } + w, err := z.w.Create(name) + if err != nil { + z.err = err + return ioutil.Discard + } + return &errWriter{w: w, err: &z.err} +} + +func (z *zipWriter) Store(name, file string) { + z.add(name, file, false) +} + +func (z *zipWriter) Add(name, file string) { + z.add(name, file, true) +} + +func (z *zipWriter) add(name, file string, compressed bool) { + if z.err != nil { + return + } + f, err := os.Open(file) + if err != nil { + z.err = err + return + } + defer f.Close() + fh := &zip.FileHeader{ + Name: name, + } + if compressed { + fh.Method = zip.Deflate + } + w, err := z.w.CreateHeader(fh) + if err != nil { + z.err = err + return + } + if _, err := io.Copy(w, f); err != nil { + z.err = err + return + } +} + +func (w *errWriter) Write(p []byte) (n int, err error) { + if err := *w.err; err != nil { + return 0, err + } + n, err = w.w.Write(p) + *w.err = err + return +} diff --git a/gio/giold/cmd/gogio/build_info.go b/gio/giold/cmd/gogio/build_info.go new file mode 100644 index 0000000..ecda1f3 --- /dev/null +++ b/gio/giold/cmd/gogio/build_info.go @@ -0,0 +1,160 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" +) + +type buildInfo struct { + appID string + archs []string + ldflags string + minsdk int + name string + pkgDir string + pkgPath string + iconPath string + tags string + target string + version int + key string + password string +} + +func newBuildInfo(pkgPath string) (*buildInfo, error) { + pkgMetadata, err := getPkgMetadata(pkgPath) + if err != nil { + return nil, err + } + appID := getAppID(pkgMetadata) + appIcon := filepath.Join(pkgMetadata.Dir, "appicon.png") + if *iconPath != "" { + appIcon = *iconPath + } + bi := &buildInfo{ + appID: appID, + archs: getArchs(), + ldflags: getLdFlags(appID), + minsdk: *minsdk, + name: getPkgName(pkgMetadata), + pkgDir: pkgMetadata.Dir, + pkgPath: pkgPath, + iconPath: appIcon, + tags: *extraTags, + target: *target, + version: *version, + key: *signKey, + password: *signPass, + } + return bi, nil +} + +func getArchs() []string { + if *archNames != "" { + return strings.Split(*archNames, ",") + } + switch *target { + case "js": + return []string{"wasm"} + case "ios", "tvos": + // Only 64-bit support. + return []string{"arm64", "amd64"} + case "android": + return []string{"arm", "arm64", "386", "amd64"} + case "windows": + goarch := os.Getenv("GOARCH") + if goarch == "" { + goarch = runtime.GOARCH + } + return []string{goarch} + default: + // TODO: Add flag tests. + panic("The target value has already been validated, this will never execute.") + } +} + +func getLdFlags(appID string) string { + var ldflags []string + if extra := *extraLdflags; extra != "" { + ldflags = append(ldflags, strings.Split(extra, " ")...) + } + // Pass appID along, to be used for logging on platforms like Android. + ldflags = append(ldflags, + fmt.Sprintf("-X realy.lol/gio/app/internal/log.appID=%s", appID)) + // Pass along all remaining arguments to the app. + if appArgs := flag.Args()[1:]; len(appArgs) > 0 { + ldflags = append(ldflags, + fmt.Sprintf("-X realy.lol/gio/app.extraArgs=%s", + strings.Join(appArgs, "|"))) + } + if m := *linkMode; m != "" { + ldflags = append(ldflags, "-linkmode="+m) + } + return strings.Join(ldflags, " ") +} + +type packageMetadata struct { + PkgPath string + Dir string +} + +func getPkgMetadata(pkgPath string) (*packageMetadata, error) { + pkgImportPath, err := runCmd(exec.Command("go", "list", "-f", + "{{.ImportPath}}", pkgPath)) + if err != nil { + return nil, err + } + pkgDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", pkgPath)) + if err != nil { + return nil, err + } + return &packageMetadata{ + PkgPath: pkgImportPath, + Dir: pkgDir, + }, nil +} + +func getAppID(pkgMetadata *packageMetadata) string { + if *appID != "" { + return *appID + } + elems := strings.Split(pkgMetadata.PkgPath, "/") + domain := strings.Split(elems[0], ".") + name := "" + if len(elems) > 1 { + name = "." + elems[len(elems)-1] + } + if len(elems) < 2 && len(domain) < 2 { + name = "." + domain[0] + domain[0] = "localhost" + } else { + for i := 0; i < len(domain)/2; i++ { + opp := len(domain) - 1 - i + domain[i], domain[opp] = domain[opp], domain[i] + } + } + + pkgDomain := strings.Join(domain, ".") + appid := []rune(pkgDomain + name) + + // a Java-language-style package name may contain upper- and lower-case + // letters and underscores with individual parts separated by '.'. + // https://developer.android.com/guide/topics/manifest/manifest-element + for i, c := range appid { + if !('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || + c == '_' || c == '.') { + appid[i] = '_' + } + } + return string(appid) +} + +func getPkgName(pkgMetadata *packageMetadata) string { + return path.Base(pkgMetadata.PkgPath) +} diff --git a/gio/giold/cmd/gogio/build_info_test.go b/gio/giold/cmd/gogio/build_info_test.go new file mode 100644 index 0000000..397e2a3 --- /dev/null +++ b/gio/giold/cmd/gogio/build_info_test.go @@ -0,0 +1,32 @@ +package main + +import "testing" + +type expval struct { + in, out string +} + +func TestAppID(t *testing.T) { + t.Parallel() + + tests := []expval{ + {"example", "localhost.example"}, + {"example.com", "com.example"}, + {"www.example.com", "com.example.www"}, + {"examplecom/app", "examplecom.app"}, + {"example.com/app", "com.example.app"}, + {"www.example.com/app", "com.example.www.app"}, + {"www.en.example.com/app", "com.example.en.www.app"}, + {"example.com/dir/app", "com.example.app"}, + {"example.com/dir.ext/app", "com.example.app"}, + {"example.com/dir/app.ext", "com.example.app.ext"}, + {"example-com.net/dir/app", "net.example_com.app"}, + } + + for i, test := range tests { + got := getAppID(&packageMetadata{PkgPath: test.in}) + if exp := test.out; got != exp { + t.Errorf("(%d): expected '%s', got '%s'", i, exp, got) + } + } +} diff --git a/gio/giold/cmd/gogio/doc.go b/gio/giold/cmd/gogio/doc.go new file mode 100644 index 0000000..6b788fd --- /dev/null +++ b/gio/giold/cmd/gogio/doc.go @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +The gogio tool builds and packages Gio programs for Android, iOS/tvOS +and WebAssembly. + +Run gogio with no arguments for instructions, or see the examples at +https://realy.lol/gio. +*/ +package main diff --git a/gio/giold/cmd/gogio/e2e_test.go b/gio/giold/cmd/gogio/e2e_test.go new file mode 100644 index 0000000..893f580 --- /dev/null +++ b/gio/giold/cmd/gogio/e2e_test.go @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "bufio" + "errors" + "flag" + "fmt" + "image" + "image/color" + "io" + "io/ioutil" + "os" + "os/exec" + "strings" + "testing" + "time" +) + +var raceEnabled = false + +var headless = flag.Bool("headless", true, + "run end-to-end tests in headless mode") + +const appid = "localhost.gogio.endtoend" + +// TestDriver is implemented by each of the platforms we can run end-to-end +// tests on. None of its methods return any errors, as the errors are directly +// reported to testing.T via methods like Fatal. +type TestDriver interface { + initBase(t *testing.T, width, height int) + + // Start opens the Gio app found at path. The driver should attempt to + // run the app with the base driver's width and height, and the + // platform's background should be white. + // + // When the function returns, the gio app must be ready to use on the + // platform, with its initial frame fully drawn. + Start(path string) + + // Screenshot takes a screenshot of the Gio app on the platform. + Screenshot() image.Image + + // Click performs a pointer click at the specified coordinates, + // including both press and release. It returns when the next frame is + // fully drawn. + Click(x, y int) +} + +type driverBase struct { + *testing.T + + width, height int + + output io.Reader + frameNotifs chan bool +} + +func (d *driverBase) initBase(t *testing.T, width, height int) { + d.T = t + d.width, d.height = width, height +} + +func TestEndToEnd(t *testing.T) { + if testing.Short() { + t.Skipf("end-to-end tests tend to be slow") + } + + t.Parallel() + + const ( + testdataWithGoImportPkgPath = "realy.lol/gio/cmd/gogio/testdata" + testdataWithRelativePkgPath = "testdata/testdata.go" + ) + // Keep this list local, to not reuse TestDriver objects. + subtests := []struct { + name string + driver TestDriver + pkgPath string + }{ + {"X11 using go import path", &X11TestDriver{}, + testdataWithGoImportPkgPath}, + {"X11", &X11TestDriver{}, testdataWithRelativePkgPath}, + {"Wayland", &WaylandTestDriver{}, testdataWithRelativePkgPath}, + {"JS", &JSTestDriver{}, testdataWithRelativePkgPath}, + {"Android", &AndroidTestDriver{}, testdataWithRelativePkgPath}, + {"Windows", &WineTestDriver{}, testdataWithRelativePkgPath}, + } + + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + subtest := subtest // copy the changing loop variable + t.Parallel() + runEndToEndTest(t, subtest.driver, subtest.pkgPath) + }) + } +} + +func runEndToEndTest(t *testing.T, driver TestDriver, pkgPath string) { + size := image.Point{X: 800, Y: 600} + driver.initBase(t, size.X, size.Y) + + t.Log("starting driver and gio app") + driver.Start(pkgPath) + + beef := color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff} + white := color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} + black := color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff} + gray := color.NRGBA{R: 0xbb, G: 0xbb, B: 0xbb, A: 0xff} + red := color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff} + + // These are the four colors at the beginning. + t.Log("taking initial screenshot") + withRetries(t, 4*time.Second, func() error { + img := driver.Screenshot() + size = img.Bounds().Size() // override the default size + return checkImageCorners(img, beef, white, black, gray) + }) + + // TODO(mvdan): implement this properly in the Wayland driver; swaymsg + // almost works to automate clicks, but the button presses end up in the + // wrong coordinates. + if _, ok := driver.(*WaylandTestDriver); ok { + return + } + + // Click the first and last sections to turn them red. + t.Log("clicking twice and taking another screenshot") + driver.Click(1*(size.X/4), 1*(size.Y/4)) + driver.Click(3*(size.X/4), 3*(size.Y/4)) + withRetries(t, 4*time.Second, func() error { + img := driver.Screenshot() + return checkImageCorners(img, red, white, black, red) + }) +} + +// withRetries keeps retrying fn until it succeeds, or until the timeout is hit. +// It uses a rudimentary kind of backoff, which starts with 100ms delays. As +// such, timeout should generally be in the order of seconds. +func withRetries(t *testing.T, timeout time.Duration, fn func() error) { + t.Helper() + + timeoutTimer := time.NewTimer(timeout) + defer timeoutTimer.Stop() + backoff := 100 * time.Millisecond + + tries := 0 + var lastErr error + for { + if lastErr = fn(); lastErr == nil { + return + } + tries++ + t.Logf("retrying after %s", backoff) + + // Use a timer instead of a sleep, so that the timeout can stop + // the backoff early. Don't reuse this timer, since we're not in + // a hot loop, and we don't want tricky code. + backoffTimer := time.NewTimer(backoff) + defer backoffTimer.Stop() + + select { + case <-timeoutTimer.C: + t.Errorf("last error: %v", lastErr) + t.Fatalf("hit timeout of %s after %d tries", timeout, tries) + case <-backoffTimer.C: + } + + // Keep doubling it until a maximum. With the start at 100ms, + // we'll do: 100ms, 200ms, 400ms, 800ms, 1.6s, and 2s forever. + backoff *= 2 + if max := 2 * time.Second; backoff > max { + backoff = max + } + } +} + +type colorMismatch struct { + x, y int + wantRGB, gotRGB [3]uint32 +} + +func (m colorMismatch) String() string { + return fmt.Sprintf("%3d,%-3d got 0x%04x%04x%04x, want 0x%04x%04x%04x", + m.x, m.y, + m.gotRGB[0], m.gotRGB[1], m.gotRGB[2], + m.wantRGB[0], m.wantRGB[1], m.wantRGB[2], + ) +} + +func checkImageCorners(img image.Image, + topLeft, topRight, botLeft, botRight color.Color) error { + // The colors are split in four rectangular sections. Check the corners + // of each of the sections. We check the corners left to right, top to + // bottom, like when reading left-to-right text. + + size := img.Bounds().Size() + var mismatches []colorMismatch + + checkColor := func(x, y int, want color.Color) { + r, g, b, _ := want.RGBA() + got := img.At(x, y) + r_, g_, b_, _ := got.RGBA() + if r_ != r || g_ != g || b_ != b { + mismatches = append(mismatches, colorMismatch{ + x: x, + y: y, + wantRGB: [3]uint32{r, g, b}, + gotRGB: [3]uint32{r_, g_, b_}, + }) + } + } + + { + minX, minY := 5, 5 + maxX, maxY := (size.X/2)-5, (size.Y/2)-5 + checkColor(minX, minY, topLeft) + checkColor(maxX, minY, topLeft) + checkColor(minX, maxY, topLeft) + checkColor(maxX, maxY, topLeft) + } + { + minX, minY := (size.X/2)+5, 5 + maxX, maxY := size.X-5, (size.Y/2)-5 + checkColor(minX, minY, topRight) + checkColor(maxX, minY, topRight) + checkColor(minX, maxY, topRight) + checkColor(maxX, maxY, topRight) + } + { + minX, minY := 5, (size.Y/2)+5 + maxX, maxY := (size.X/2)-5, size.Y-5 + checkColor(minX, minY, botLeft) + checkColor(maxX, minY, botLeft) + checkColor(minX, maxY, botLeft) + checkColor(maxX, maxY, botLeft) + } + { + minX, minY := (size.X/2)+5, (size.Y/2)+5 + maxX, maxY := size.X-5, size.Y-5 + checkColor(minX, minY, botRight) + checkColor(maxX, minY, botRight) + checkColor(minX, maxY, botRight) + checkColor(maxX, maxY, botRight) + } + if n := len(mismatches); n > 0 { + b := new(strings.Builder) + fmt.Fprintf(b, "encountered %d color mismatches:\n", n) + for _, m := range mismatches { + fmt.Fprintf(b, "%s\n", m) + } + return errors.New(b.String()) + } + return nil +} + +func (d *driverBase) waitForFrame() { + d.Helper() + + if d.frameNotifs == nil { + // Start the goroutine that reads output lines and notifies of + // new frames via frameNotifs. The test doesn't wait for this + // goroutine to finish; it will naturally end when the output + // reader reaches an error like EOF. + d.frameNotifs = make(chan bool, 1) + if d.output == nil { + d.Fatal("need an output reader to be notified of frames") + } + go func() { + scanner := bufio.NewScanner(d.output) + for scanner.Scan() { + line := scanner.Text() + d.Log(line) + if strings.Contains(line, "gio frame ready") { + d.frameNotifs <- true + } + } + // Since we're only interested in the output while the + // app runs, and we don't know when it finishes here, + // ignore "already closed" pipe errors. + if err := scanner.Err(); err != nil && !errors.Is(err, + os.ErrClosed) { + d.Errorf("reading app output: %v", err) + } + }() + } + + // Unfortunately, there isn't a way to select on a test failing, since + // testing.T doesn't have anything like a context or a "done" channel. + // + // We can't let selects block forever, since the default -test.timeout + // is ten minutes - far too long for tests that take seconds. + // + // For now, a static short timeout is better than nothing. 5s is plenty + // for our simple test app to render on any device. + select { + case <-d.frameNotifs: + case <-time.After(5 * time.Second): + d.Fatalf("timed out waiting for a frame to be ready") + } +} + +func (d *driverBase) needPrograms(names ...string) { + d.Helper() + for _, name := range names { + if _, err := exec.LookPath(name); err != nil { + d.Skipf("%s needed to run", name) + } + } +} + +func (d *driverBase) tempDir(name string) string { + d.Helper() + dir, err := ioutil.TempDir("", name) + if err != nil { + d.Fatal(err) + } + d.Cleanup(func() { os.RemoveAll(dir) }) + return dir +} + +func (d *driverBase) gogio(args ...string) { + d.Helper() + prog, err := os.Executable() + if err != nil { + d.Fatal(err) + } + cmd := exec.Command(prog, args...) + cmd.Env = append(os.Environ(), "RUN_GOGIO=1") + if out, err := cmd.CombinedOutput(); err != nil { + d.Fatalf("gogio error: %s:\n%s", err, out) + } +} diff --git a/gio/giold/cmd/gogio/help.go b/gio/giold/cmd/gogio/help.go new file mode 100644 index 0000000..c83d7a3 --- /dev/null +++ b/gio/giold/cmd/gogio/help.go @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +const mainUsage = `The gogio command builds and packages Gio (realy.lol/gio) programs. + +Usage: + + gogio -target [flags] [run arguments] + +The gogio tool builds and packages Gio programs for platforms where additional +metadata or support files are required. + +The package argument specifies an import path or a single Go source file to +package. Any run arguments are appended to os.Args at runtime. + +Compiled Java class files from jar files in the package directory are +included in Android builds. + +The mandatory -target flag selects the target platform: ios or android for the +mobile platforms, tvos for Apple's tvOS, js for WebAssembly/WebGL. + +The -arch flag specifies a comma separated list of GOARCHs to include. The +default is all supported architectures. + +The -o flag specifies an output file or directory, depending on the target. + +The -buildmode flag selects the build mode. Two build modes are available, exe +and archive. Buildmode exe outputs an .ipa file for iOS or tvOS, an .apk file +for Android or a directory with the WebAssembly module and support files for +a browser. + +The -ldflags and -tags flags pass extra linker flags and tags to the go tool. + +As a special case for iOS or tvOS, specifying a path that ends with ".app" +will output an app directory suitable for a simulator. + +The other buildmode is archive, which will output an .aar library for Android +or a .framework for iOS and tvOS. + +The -icon flag specifies a path to a PNG image to use as app icon on iOS and Android. +If left unspecified, the appicon.png file from the main package is used +(if it exists). + +The -appid flag specifies the package name for Android or the bundle id for +iOS and tvOS. A bundle id must be provisioned through Xcode before the gogio +tool can use it. + +The -version flag specifies the integer version code for Android and the last +component of the 1.0.X version for iOS and tvOS. + +For Android builds the -minsdk flag specify the minimum SDK level. For example, +use -minsdk 22 to target Android 5.1 (Lollipop) and later. + +For Windows builds the -minsdk flag specify the minimum OS version. For example, +use -mindk 10 to target Windows 10 only, -minsdk 6 for Windows Vista and later. + +The -work flag prints the path to the working directory and suppress +its deletion. + +The -x flag will print all the external commands executed by the gogio tool. + +The -signkey flag specifies the path of the keystore, used for signing Android apk/aab files. + +The -signpass flag specifies the password of the keystore, ignored if -signkey is not provided. +` diff --git a/gio/giold/cmd/gogio/iosbuild.go b/gio/giold/cmd/gogio/iosbuild.go new file mode 100644 index 0000000..9674431 --- /dev/null +++ b/gio/giold/cmd/gogio/iosbuild.go @@ -0,0 +1,591 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "archive/zip" + "crypto/sha1" + "encoding/hex" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "golang.org/x/sync/errgroup" +) + +const minIOSVersion = "9.0" + +func buildIOS(tmpDir, target string, bi *buildInfo) error { + appName := bi.name + switch *buildMode { + case "archive": + framework := *destPath + if framework == "" { + framework = fmt.Sprintf("%s.framework", strings.Title(appName)) + } + return archiveIOS(tmpDir, target, framework, bi) + case "exe": + out := *destPath + if out == "" { + out = appName + ".ipa" + } + forDevice := strings.HasSuffix(out, ".ipa") + // Filter out unsupported architectures. + for i := len(bi.archs) - 1; i >= 0; i-- { + switch bi.archs[i] { + case "arm", "arm64": + if forDevice { + continue + } + case "386", "amd64": + if !forDevice { + continue + } + } + + bi.archs = append(bi.archs[:i], bi.archs[i+1:]...) + } + tmpFramework := filepath.Join(tmpDir, "Gio.framework") + if err := archiveIOS(tmpDir, target, tmpFramework, bi); err != nil { + return err + } + if !forDevice && !strings.HasSuffix(out, ".app") { + return fmt.Errorf("the specified output directory %q does not end in .app or .ipa", + out) + } + if !forDevice { + return exeIOS(tmpDir, target, out, bi) + } + payload := filepath.Join(tmpDir, "Payload") + appDir := filepath.Join(payload, appName+".app") + if err := os.MkdirAll(appDir, 0755); err != nil { + return err + } + if err := exeIOS(tmpDir, target, appDir, bi); err != nil { + return err + } + if err := signIOS(bi, tmpDir, appDir); err != nil { + return err + } + return zipDir(out, tmpDir, "Payload") + default: + panic("unreachable") + } +} + +func signIOS(bi *buildInfo, tmpDir, app string) error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + provPattern := filepath.Join(home, "Library", "MobileDevice", + "Provisioning Profiles", "*.mobileprovision") + provisions, err := filepath.Glob(provPattern) + if err != nil { + return err + } + provInfo := filepath.Join(tmpDir, "provision.plist") + var avail []string + for _, prov := range provisions { + // Decode the provision file to a plist. + _, err := runCmd(exec.Command("security", "cms", "-D", "-i", prov, "-o", + provInfo)) + if err != nil { + return err + } + expUnix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", + "Print:ExpirationDate", provInfo)) + if err != nil { + return err + } + exp, err := time.Parse(time.UnixDate, expUnix) + if err != nil { + return fmt.Errorf("sign: failed to parse expiration date from %q: %v", + prov, err) + } + if exp.Before(time.Now()) { + continue + } + appIDPrefix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", + "Print:ApplicationIdentifierPrefix:0", provInfo)) + if err != nil { + return err + } + provAppID, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", + "Print:Entitlements:application-identifier", provInfo)) + if err != nil { + return err + } + expAppID := fmt.Sprintf("%s.%s", appIDPrefix, bi.appID) + avail = append(avail, provAppID) + if expAppID != provAppID { + continue + } + // Copy provisioning file. + embedded := filepath.Join(app, "embedded.mobileprovision") + if err := copyFile(embedded, prov); err != nil { + return err + } + certDER, err := runCmdRaw(exec.Command("/usr/libexec/PlistBuddy", "-c", + "Print:DeveloperCertificates:0", provInfo)) + if err != nil { + return err + } + // Omit trailing newline. + certDER = certDER[:len(certDER)-1] + entitlements, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", + "-x", "-c", "Print:Entitlements", provInfo)) + if err != nil { + return err + } + entFile := filepath.Join(tmpDir, "entitlements.plist") + if err := ioutil.WriteFile(entFile, []byte(entitlements), + 0660); err != nil { + return err + } + identity := sha1.Sum(certDER) + idHex := hex.EncodeToString(identity[:]) + _, err = runCmd(exec.Command("codesign", "-s", idHex, "-v", + "--entitlements", entFile, app)) + return err + } + return fmt.Errorf("sign: no valid provisioning profile found for bundle id %q among %v", + bi.appID, avail) +} + +func exeIOS(tmpDir, target, app string, bi *buildInfo) error { + if bi.appID == "" { + return errors.New("app id is empty; use -appid to set it") + } + if err := os.RemoveAll(app); err != nil { + return err + } + if err := os.Mkdir(app, 0755); err != nil { + return err + } + mainm := filepath.Join(tmpDir, "main.m") + const mainmSrc = `@import UIKit; +@import Gio; + +@interface GioAppDelegate : UIResponder +@property (strong, nonatomic) UIWindow *window; +@end + +@implementation GioAppDelegate +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + GioViewController *controller = [[GioViewController alloc] initWithNibName:nil bundle:nil]; + self.window.rootViewController = controller; + [self.window makeKeyAndVisible]; + return YES; +} +@end + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([GioAppDelegate class])); + } +}` + if err := ioutil.WriteFile(mainm, []byte(mainmSrc), 0660); err != nil { + return err + } + appName := strings.Title(bi.name) + exe := filepath.Join(app, appName) + lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create") + var builds errgroup.Group + for _, a := range bi.archs { + clang, cflags, err := iosCompilerFor(target, a) + if err != nil { + return err + } + exeSlice := filepath.Join(tmpDir, "app-"+a) + lipo.Args = append(lipo.Args, exeSlice) + compile := exec.Command(clang, cflags...) + compile.Args = append(compile.Args, + "-Werror", + "-fmodules", + "-fobjc-arc", + "-x", "objective-c", + "-F", tmpDir, + "-o", exeSlice, + mainm, + ) + builds.Go(func() error { + _, err := runCmd(compile) + return err + }) + } + if err := builds.Wait(); err != nil { + return err + } + if _, err := runCmd(lipo); err != nil { + return err + } + infoPlist := buildInfoPlist(bi) + plistFile := filepath.Join(app, "Info.plist") + if err := ioutil.WriteFile(plistFile, []byte(infoPlist), 0660); err != nil { + return err + } + if _, err := os.Stat(bi.iconPath); err == nil { + assetPlist, err := iosIcons(bi, tmpDir, app, bi.iconPath) + if err != nil { + return err + } + // Merge assets plist with Info.plist + cmd := exec.Command( + "/usr/libexec/PlistBuddy", + "-c", "Merge "+assetPlist, + plistFile, + ) + if _, err := runCmd(cmd); err != nil { + return err + } + } + if _, err := runCmd(exec.Command("plutil", "-convert", "binary1", + plistFile)); err != nil { + return err + } + return nil +} + +// iosIcons builds an asset catalog and compile it with the Xcode command actool. +// iosIcons returns the asset plist file to be merged into Info.plist. +func iosIcons(bi *buildInfo, tmpDir, appDir, icon string) (string, error) { + assets := filepath.Join(tmpDir, "Assets.xcassets") + if err := os.Mkdir(assets, 0700); err != nil { + return "", err + } + appIcon := filepath.Join(assets, "AppIcon.appiconset") + err := buildIcons(appIcon, icon, []iconVariant{ + {path: "ios_2x.png", size: 120}, + {path: "ios_3x.png", size: 180}, + // The App Store icon is not allowed to contain + // transparent pixels. + {path: "ios_store.png", size: 1024, fill: true}, + }) + if err != nil { + return "", err + } + contentJson := `{ + "images" : [ + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "ios_2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "ios_3x.png", + "scale" : "3x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "ios_store.png", + "scale" : "1x" + } + ] +}` + contentFile := filepath.Join(appIcon, "Contents.json") + if err := ioutil.WriteFile(contentFile, []byte(contentJson), + 0600); err != nil { + return "", err + } + assetPlist := filepath.Join(tmpDir, "assets.plist") + compile := exec.Command( + "actool", + "--compile", appDir, + "--platform", iosPlatformFor(bi.target), + "--minimum-deployment-target", minIOSVersion, + "--app-icon", "AppIcon", + "--output-partial-info-plist", assetPlist, + assets) + _, err = runCmd(compile) + return assetPlist, err +} + +func buildInfoPlist(bi *buildInfo) string { + appName := strings.Title(bi.name) + platform := iosPlatformFor(bi.target) + var supportPlatform string + switch bi.target { + case "ios": + supportPlatform = "iPhoneOS" + case "tvos": + supportPlatform = "AppleTVOS" + } + return fmt.Sprintf(` + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + %s + CFBundleIdentifier + %s + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + %s + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0.%d + CFBundleVersion + %d + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + arm64 + DTPlatformName + %s + DTPlatformVersion + 12.4 + MinimumOSVersion + %s + UIDeviceFamily + + 1 + + CFBundleSupportedPlatforms + + %s + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 16G73 + DTSDKBuild + 16G73 + DTSDKName + %s12.4 + DTXcode + 1030 + DTXcodeBuild + 10G8 + +`, appName, bi.appID, appName, bi.version, bi.version, platform, + minIOSVersion, supportPlatform, platform) +} + +func iosPlatformFor(target string) string { + switch target { + case "ios": + return "iphoneos" + case "tvos": + return "appletvos" + default: + panic("invalid platform " + target) + } +} + +func archiveIOS(tmpDir, target, frameworkRoot string, bi *buildInfo) error { + framework := filepath.Base(frameworkRoot) + const suf = ".framework" + if !strings.HasSuffix(framework, suf) { + return fmt.Errorf("the specified output %q does not end in '.framework'", + frameworkRoot) + } + framework = framework[:len(framework)-len(suf)] + if err := os.RemoveAll(frameworkRoot); err != nil { + return err + } + frameworkDir := filepath.Join(frameworkRoot, "Versions", "A") + for _, dir := range []string{"Headers", "Modules"} { + p := filepath.Join(frameworkDir, dir) + if err := os.MkdirAll(p, 0755); err != nil { + return err + } + } + symlinks := [][2]string{ + {"Versions/Current/Headers", "Headers"}, + {"Versions/Current/Modules", "Modules"}, + {"Versions/Current/" + framework, framework}, + {"A", filepath.Join("Versions", "Current")}, + } + for _, l := range symlinks { + if err := os.Symlink(l[0], filepath.Join(frameworkRoot, + l[1])); err != nil && !os.IsExist(err) { + return err + } + } + exe := filepath.Join(frameworkDir, framework) + lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create") + var builds errgroup.Group + tags := bi.tags + goos := "ios" + supportsIOS, err := supportsGOOS("ios") + if err != nil { + return err + } + if !supportsIOS { + // Go 1.15 and earlier target iOS with GOOS=darwin, tags=ios. + goos = "darwin" + tags = "ios " + tags + } + for _, a := range bi.archs { + clang, cflags, err := iosCompilerFor(target, a) + if err != nil { + return err + } + lib := filepath.Join(tmpDir, "gio-"+a) + cmd := exec.Command( + "go", + "build", + "-ldflags=-s -w "+bi.ldflags, + "-buildmode=c-archive", + "-o", lib, + "-tags", tags, + bi.pkgPath, + ) + lipo.Args = append(lipo.Args, lib) + cflagsLine := strings.Join(cflags, " ") + cmd.Env = append( + os.Environ(), + "GOOS="+goos, + "GOARCH="+a, + "CGO_ENABLED=1", + "CC="+clang, + "CGO_CFLAGS="+cflagsLine, + "CGO_LDFLAGS="+cflagsLine, + ) + builds.Go(func() error { + _, err := runCmd(cmd) + return err + }) + } + if err := builds.Wait(); err != nil { + return err + } + if _, err := runCmd(lipo); err != nil { + return err + } + appDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", + "realy.lol/gio/app/internal/wm")) + if err != nil { + return err + } + headerDst := filepath.Join(frameworkDir, "Headers", framework+".h") + headerSrc := filepath.Join(appDir, "framework_ios.h") + if err := copyFile(headerDst, headerSrc); err != nil { + return err + } + module := fmt.Sprintf(`framework module "%s" { + header "%[1]s.h" + + export * +}`, framework) + moduleFile := filepath.Join(frameworkDir, "Modules", "module.modulemap") + return ioutil.WriteFile(moduleFile, []byte(module), 0644) +} + +func supportsGOOS(wantGoos string) (bool, error) { + geese, err := runCmd(exec.Command("go", "tool", "dist", "list")) + if err != nil { + return false, err + } + for _, pair := range strings.Split(geese, "\n") { + s := strings.SplitN(pair, "/", 2) + if len(s) != 2 { + return false, fmt.Errorf("go tool dist list: invalid GOOS/GOARCH pair: %s", + pair) + } + goos := s[0] + if goos == wantGoos { + return true, nil + } + } + return false, nil +} + +func iosCompilerFor(target, arch string) (string, []string, error) { + var platformSDK string + var platformOS string + switch target { + case "ios": + platformOS = "ios" + platformSDK = "iphone" + case "tvos": + platformOS = "tvos" + platformSDK = "appletv" + } + switch arch { + case "arm", "arm64": + platformSDK += "os" + case "386", "amd64": + platformOS += "-simulator" + platformSDK += "simulator" + default: + return "", nil, fmt.Errorf("unsupported -arch: %s", arch) + } + sdkPath, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, + "--show-sdk-path")) + if err != nil { + return "", nil, err + } + clang, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--find", + "clang")) + if err != nil { + return "", nil, err + } + cflags := []string{ + "-fembed-bitcode", + "-arch", allArchs[arch].iosArch, + "-isysroot", sdkPath, + "-m" + platformOS + "-version-min=" + minIOSVersion, + } + return clang, cflags, nil +} + +func zipDir(dst, base, dir string) (err error) { + f, err := os.Create(dst) + if err != nil { + return err + } + defer func() { + if cerr := f.Close(); err == nil { + err = cerr + } + }() + zipf := zip.NewWriter(f) + err = filepath.Walk(filepath.Join(base, dir), + func(path string, f os.FileInfo, err error) error { + if err != nil { + return err + } + if f.IsDir() { + return nil + } + rel := filepath.ToSlash(path[len(base)+1:]) + entry, err := zipf.Create(rel) + if err != nil { + return err + } + src, err := os.Open(path) + if err != nil { + return err + } + defer src.Close() + _, err = io.Copy(entry, src) + return err + }) + if err != nil { + return err + } + return zipf.Close() +} diff --git a/gio/giold/cmd/gogio/js_test.go b/gio/giold/cmd/gogio/js_test.go new file mode 100644 index 0000000..2918737 --- /dev/null +++ b/gio/giold/cmd/gogio/js_test.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "bytes" + "context" + "errors" + "image" + "image/png" + "io" + "net/http" + "net/http/httptest" + "os/exec" + + "github.com/chromedp/cdproto/runtime" + "github.com/chromedp/chromedp" + +) + +type JSTestDriver struct { + driverBase + + // ctx is the chromedp context. + ctx context.Context +} + +func (d *JSTestDriver) Start(path string) { + if raceEnabled { + d.Skipf("js/wasm doesn't support -race; skipping") + } + + // First, build the app. + dir := d.tempDir("gio-endtoend-js") + d.gogio("-target=js", "-o="+dir, path) + + // Second, start Chrome. + opts := append(chromedp.DefaultExecAllocatorOptions[:], + chromedp.Flag("headless", *headless), + ) + + actx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) + d.Cleanup(cancel) + + ctx, cancel := chromedp.NewContext(actx, + // Send all logf/errf calls to t.Logf + chromedp.WithLogf(d.Logf), + ) + d.Cleanup(cancel) + d.ctx = ctx + + if err := chromedp.Run(ctx); err != nil { + if errors.Is(err, exec.ErrNotFound) { + d.Skipf("test requires Chrome to be installed: %v", err) + return + } + d.Fatal(err) + } + pr, pw := io.Pipe() + d.Cleanup(func() { pw.Close() }) + d.output = pr + chromedp.ListenTarget(ctx, func(ev interface{}) { + switch ev := ev.(type) { + case *runtime.EventConsoleAPICalled: + switch ev.Type { + case "log", "info", "warning", "error": + var b bytes.Buffer + b.WriteString("console.") + b.WriteString(string(ev.Type)) + b.WriteString("(") + for i, arg := range ev.Args { + if i > 0 { + b.WriteString(", ") + } + b.Write(arg.Value) + } + b.WriteString(")\n") + pw.Write(b.Bytes()) + } + } + }) + + // Third, serve the app folder, set the browser tab dimensions, and + // navigate to the folder. + ts := httptest.NewServer(http.FileServer(http.Dir(dir))) + d.Cleanup(ts.Close) + + if err := chromedp.Run(ctx, + chromedp.EmulateViewport(int64(d.width), int64(d.height)), + chromedp.Navigate(ts.URL), + ); err != nil { + d.Fatal(err) + } + + // Wait for the gio app to render. + d.waitForFrame() +} + +func (d *JSTestDriver) Screenshot() image.Image { + var buf []byte + if err := chromedp.Run(d.ctx, + chromedp.CaptureScreenshot(&buf), + ); err != nil { + d.Fatal(err) + } + img, err := png.Decode(bytes.NewReader(buf)) + if err != nil { + d.Fatal(err) + } + return img +} + +func (d *JSTestDriver) Click(x, y int) { + if err := chromedp.Run(d.ctx, + chromedp.MouseClickXY(float64(x), float64(y)), + ); err != nil { + d.Fatal(err) + } + + // Wait for the gio app to render after this click. + d.waitForFrame() +} diff --git a/gio/giold/cmd/gogio/jsbuild.go b/gio/giold/cmd/gogio/jsbuild.go new file mode 100644 index 0000000..58bccc1 --- /dev/null +++ b/gio/giold/cmd/gogio/jsbuild.go @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "golang.org/x/tools/go/packages" +) + +func buildJS(bi *buildInfo) error { + out := *destPath + if out == "" { + out = bi.name + } + if err := os.MkdirAll(out, 0700); err != nil { + return err + } + cmd := exec.Command( + "go", + "build", + "-ldflags="+bi.ldflags, + "-tags="+bi.tags, + "-o", filepath.Join(out, "main.wasm"), + bi.pkgPath, + ) + cmd.Env = append( + os.Environ(), + "GOOS=js", + "GOARCH=wasm", + ) + _, err := runCmd(cmd) + if err != nil { + return err + } + if err := ioutil.WriteFile(filepath.Join(out, "index.html"), []byte(jsIndex), 0600); err != nil { + return err + } + goroot, err := runCmd(exec.Command("go", "env", "GOROOT")) + if err != nil { + return err + } + wasmJS := filepath.Join(goroot, "misc", "wasm", "wasm_exec.js") + if _, err := os.Stat(wasmJS); err != nil { + return fmt.Errorf("failed to find $GOROOT/misc/wasm/wasm_exec.js driver: %v", err) + } + pkgs, err := packages.Load(&packages.Config{ + Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps, + Env: append(os.Environ(), "GOOS=js", "GOARCH=wasm"), + }, bi.pkgPath) + if err != nil { + return err + } + extraJS, err := findPackagesJS(pkgs[0], make(map[string]bool)) + if err != nil { + return err + } + + return mergeJSFiles(filepath.Join(out, "wasm.js"), append([]string{wasmJS}, extraJS...)...) +} + +func findPackagesJS(p *packages.Package, visited map[string]bool) (extraJS []string, err error) { + if len(p.GoFiles) == 0 { + return nil, nil + } + js, err := filepath.Glob(filepath.Join(filepath.Dir(p.GoFiles[0]), "*_js.js")) + if err != nil { + return nil, err + } + extraJS = append(extraJS, js...) + for _, imp := range p.Imports { + if !visited[imp.ID] { + extra, err := findPackagesJS(imp, visited) + if err != nil { + return nil, err + } + extraJS = append(extraJS, extra...) + visited[imp.ID] = true + } + } + return extraJS, nil +} + +// mergeJSFiles will merge all files into a single `wasm.js`. It will prepend the jsSetGo +// and append the jsStartGo. +func mergeJSFiles(dst string, files ...string) (err error) { + w, err := os.Create(dst) + if err != nil { + return err + } + defer func() { + if cerr := w.Close(); err != nil { + err = cerr + } + }() + _, err = io.Copy(w, strings.NewReader(jsSetGo)) + if err != nil { + return err + } + for i := range files { + r, err := os.Open(files[i]) + if err != nil { + return err + } + _, err = io.Copy(w, r) + r.Close() + if err != nil { + return err + } + } + _, err = io.Copy(w, strings.NewReader(jsStartGo)) + return err +} + +const ( + jsIndex = ` + + + + + + + + + + +` + // jsSetGo sets the `window.go` variable. + jsSetGo = `(() => { + window.go = {argv: [], env: {}, importObject: {go: {}}}; + const argv = new URLSearchParams(location.search).get("argv"); + if (argv) { + window.go["argv"] = argv.split(" "); + } +})();` + // jsStartGo initializes the main.wasm. + jsStartGo = `(() => { + defaultGo = new Go(); + Object.assign(defaultGo["argv"], defaultGo["argv"].concat(go["argv"])); + Object.assign(defaultGo["env"], go["env"]); + for (let key in go["importObject"]) { + if (typeof defaultGo["importObject"][key] === "undefined") { + defaultGo["importObject"][key] = {}; + } + Object.assign(defaultGo["importObject"][key], go["importObject"][key]); + } + window.go = defaultGo; + if (!WebAssembly.instantiateStreaming) { // polyfill + WebAssembly.instantiateStreaming = async (resp, importObject) => { + const source = await (await resp).arrayBuffer(); + return await WebAssembly.instantiate(source, importObject); + }; + } + WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => { + go.run(result.instance); + }); +})();` +) diff --git a/gio/giold/cmd/gogio/main.go b/gio/giold/cmd/gogio/main.go new file mode 100644 index 0000000..da35401 --- /dev/null +++ b/gio/giold/cmd/gogio/main.go @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "image" + "image/color" + "image/png" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "golang.org/x/image/draw" + "golang.org/x/sync/errgroup" +) + +var ( + target = flag.String("target", "", "specify target (ios, tvos, android, js).\n") + archNames = flag.String("arch", "", "specify architecture(s) to include (arm, arm64, amd64).") + minsdk = flag.Int("minsdk", 0, "specify the minimum supported operating system level") + buildMode = flag.String("buildmode", "exe", "specify buildmode (archive, exe)") + destPath = flag.String("o", "", "output file or directory.\nFor -target ios or tvos, use the .app suffix to target simulators.") + appID = flag.String("appid", "", "app identifier (for -buildmode=exe)") + version = flag.Int("version", 1, "app version (for -buildmode=exe)") + printCommands = flag.Bool("x", false, "print the commands") + keepWorkdir = flag.Bool("work", false, "print the name of the temporary work directory and do not delete it when exiting.") + linkMode = flag.String("linkmode", "", "set the -linkmode flag of the go tool") + extraLdflags = flag.String("ldflags", "", "extra flags to the Go linker") + extraTags = flag.String("tags", "", "extra tags to the Go tool") + iconPath = flag.String("icon", "", "specify an icon for iOS and Android") + signKey = flag.String("signkey", "", "specify the path of the keystore to be used to sign Android apk files.") + signPass = flag.String("signpass", "", "specify the password to decrypt the signkey.") + noStrip = flag.Bool("nostrip", false, "leave debugging symbols in produced .so files") +) + +func main() { + flag.Usage = func() { + fmt.Fprint(os.Stderr, mainUsage) + } + flag.Parse() + if err := flagValidate(); err != nil { + fmt.Fprintf(os.Stderr, "gogio: %v\n", err) + os.Exit(1) + } + buildInfo, err := newBuildInfo(flag.Arg(0)) + if err != nil { + fmt.Fprintf(os.Stderr, "gogio: %v\n", err) + os.Exit(1) + } + if err := build(buildInfo); err != nil { + fmt.Fprintf(os.Stderr, "gogio: %v\n", err) + os.Exit(1) + } + os.Exit(0) +} + +func flagValidate() error { + pkgPathArg := flag.Arg(0) + if pkgPathArg == "" { + return errors.New("specify a package") + } + if *target == "" { + return errors.New("please specify -target") + } + switch *target { + case "ios", "tvos", "android", "js", "windows": + default: + return fmt.Errorf("invalid -target %s", *target) + } + switch *buildMode { + case "archive", "exe": + default: + return fmt.Errorf("invalid -buildmode %s", *buildMode) + } + return nil +} + +func build(bi *buildInfo) error { + tmpDir, err := ioutil.TempDir("", "gogio-") + if err != nil { + return err + } + if *keepWorkdir { + fmt.Fprintf(os.Stderr, "WORKDIR=%s\n", tmpDir) + } else { + defer os.RemoveAll(tmpDir) + } + switch *target { + case "js": + return buildJS(bi) + case "ios", "tvos": + return buildIOS(tmpDir, *target, bi) + case "android": + return buildAndroid(tmpDir, bi) + case "windows": + return buildWindows(tmpDir, bi) + default: + panic("unreachable") + } +} + +func runCmdRaw(cmd *exec.Cmd) ([]byte, error) { + if *printCommands { + fmt.Printf("%s\n", strings.Join(cmd.Args, " ")) + } + out, err := cmd.Output() + if err == nil { + return out, nil + } + if err, ok := err.(*exec.ExitError); ok { + return nil, fmt.Errorf("%s failed: %s%s", strings.Join(cmd.Args, " "), out, err.Stderr) + } + return nil, err +} + +func runCmd(cmd *exec.Cmd) (string, error) { + out, err := runCmdRaw(cmd) + return string(bytes.TrimSpace(out)), err +} + +func copyFile(dst, src string) (err error) { + r, err := os.Open(src) + if err != nil { + return err + } + defer r.Close() + w, err := os.Create(dst) + if err != nil { + return err + } + defer func() { + if cerr := w.Close(); err == nil { + err = cerr + } + }() + _, err = io.Copy(w, r) + return err +} + +type arch struct { + iosArch string + jniArch string + clangArch string +} + +var allArchs = map[string]arch{ + "arm": { + iosArch: "armv7", + jniArch: "armeabi-v7a", + clangArch: "armv7a-linux-androideabi", + }, + "arm64": { + iosArch: "arm64", + jniArch: "arm64-v8a", + clangArch: "aarch64-linux-android", + }, + "386": { + iosArch: "i386", + jniArch: "x86", + clangArch: "i686-linux-android", + }, + "amd64": { + iosArch: "x86_64", + jniArch: "x86_64", + clangArch: "x86_64-linux-android", + }, +} + +type iconVariant struct { + path string + size int + fill bool +} + +func buildIcons(baseDir, icon string, variants []iconVariant) error { + f, err := os.Open(icon) + if err != nil { + return err + } + defer f.Close() + img, _, err := image.Decode(f) + if err != nil { + return err + } + var resizes errgroup.Group + for _, v := range variants { + v := v + resizes.Go(func() (err error) { + path := filepath.Join(baseDir, v.path) + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return err + } + f, err := os.Create(path) + if err != nil { + return err + } + defer func() { + if cerr := f.Close(); err == nil { + err = cerr + } + }() + return png.Encode(f, resizeIcon(v, img)) + }) + } + return resizes.Wait() +} + +func resizeIcon(v iconVariant, img image.Image) *image.NRGBA { + scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: v.size, Y: v.size}}) + op := draw.Src + if v.fill { + op = draw.Over + draw.Draw(scaled, scaled.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src) + } + draw.CatmullRom.Scale(scaled, scaled.Bounds(), img, img.Bounds(), op, nil) + + return scaled +} diff --git a/gio/giold/cmd/gogio/main_test.go b/gio/giold/cmd/gogio/main_test.go new file mode 100644 index 0000000..98dcb27 --- /dev/null +++ b/gio/giold/cmd/gogio/main_test.go @@ -0,0 +1,17 @@ +package main + +import ( + "os" + "testing" +) + +func TestMain(m *testing.M) { + if os.Getenv("RUN_GOGIO") != "" { + // Allow the end-to-end tests to call the gogio tool without + // having to build it from scratch, nor having to refactor the + // main function to avoid using global variables. + main() + os.Exit(0) // main already exits, but just in case. + } + os.Exit(m.Run()) +} diff --git a/gio/giold/cmd/gogio/permission.go b/gio/giold/cmd/gogio/permission.go new file mode 100644 index 0000000..b22fcef --- /dev/null +++ b/gio/giold/cmd/gogio/permission.go @@ -0,0 +1,33 @@ +package main + +var AndroidPermissions = map[string][]string{ + "network": { + "android.permission.INTERNET", + }, + "networkstate": { + "android.permission.ACCESS_NETWORK_STATE", + }, + "bluetooth": { + "android.permission.BLUETOOTH", + "android.permission.BLUETOOTH_ADMIN", + "android.permission.ACCESS_FINE_LOCATION", + }, + "camera": { + "android.permission.CAMERA", + }, + "storage": { + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE", + }, +} + +var AndroidFeatures = map[string][]string{ + "default": {`glEsVersion="0x00020000"`, `name="android.hardware.type.pc"`}, + "bluetooth": { + `name="android.hardware.bluetooth"`, + `name="android.hardware.bluetooth_le"`, + }, + "camera": { + `name="android.hardware.camera"`, + }, +} diff --git a/gio/giold/cmd/gogio/race_test.go b/gio/giold/cmd/gogio/race_test.go new file mode 100644 index 0000000..0749936 --- /dev/null +++ b/gio/giold/cmd/gogio/race_test.go @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build race + +package main_test + +func init() { raceEnabled = true } diff --git a/gio/giold/cmd/gogio/testdata/testdata.go b/gio/giold/cmd/gogio/testdata/testdata.go new file mode 100644 index 0000000..b5c2493 --- /dev/null +++ b/gio/giold/cmd/gogio/testdata/testdata.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// A simple app used for gogio's end-to-end tests. +package main + +import ( + "fmt" + "image" + "image/color" + "log" + + "realy.lol/gio/app" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/system" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" +) + +func main() { + go func() { + w := app.NewWindow() + if err := loop(w); err != nil { + log.Fatal(err) + } + }() + app.Main() +} + +type notifyFrame int + +const ( + notifyNone notifyFrame = iota + notifyInvalidate + notifyPrint +) + +// notify keeps track of whether we want to print to stdout to notify the user +// when a frame is ready. Initially we want to notify about the first frame. +var notify = notifyInvalidate + +type ( + C = layout.Context + D = layout.Dimensions +) + +func loop(w *app.Window) error { + topLeft := quarterWidget{ + color: color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff}, + } + topRight := quarterWidget{ + color: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, + } + botLeft := quarterWidget{ + color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}, + } + botRight := quarterWidget{ + color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x80}, + } + + var ops op.Ops + for { + e := <-w.Events() + switch e := e.(type) { + case system.DestroyEvent: + return e.Err + case system.FrameEvent: + gtx := layout.NewContext(&ops, e) + // Clear background to white, even on embedded platforms such as webassembly. + paint.Fill(gtx.Ops, color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}) + layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Flexed(1, func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + // r1c1 + layout.Flexed(1, + func(gtx C) D { return topLeft.Layout(gtx) }), + // r1c2 + layout.Flexed(1, + func(gtx C) D { return topRight.Layout(gtx) }), + ) + }), + layout.Flexed(1, func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + // r2c1 + layout.Flexed(1, + func(gtx C) D { return botLeft.Layout(gtx) }), + // r2c2 + layout.Flexed(1, + func(gtx C) D { return botRight.Layout(gtx) }), + ) + }), + ) + + e.Frame(gtx.Ops) + + switch notify { + case notifyInvalidate: + notify = notifyPrint + w.Invalidate() + case notifyPrint: + notify = notifyNone + fmt.Println("gio frame ready") + } + } + } +} + +// quarterWidget paints a quarter of the screen with one color. When clicked, it +// turns red, going back to its normal color when clicked again. +type quarterWidget struct { + color color.NRGBA + + clicked bool +} + +var red = color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff} + +func (w *quarterWidget) Layout(gtx layout.Context) layout.Dimensions { + var color color.NRGBA + if w.clicked { + color = red + } else { + color = w.color + } + + r := image.Rectangle{Max: gtx.Constraints.Max} + paint.FillShape(gtx.Ops, color, clip.Rect(r).Op()) + + pointer.Rect(image.Rectangle{ + Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y), + }).Add(gtx.Ops) + pointer.InputOp{ + Tag: w, + Types: pointer.Press, + }.Add(gtx.Ops) + + for _, e := range gtx.Events(w) { + if e, ok := e.(pointer.Event); ok && e.Type == pointer.Press { + w.clicked = !w.clicked + // notify when we're done updating the frame. + notify = notifyInvalidate + } + } + return layout.Dimensions{Size: gtx.Constraints.Max} +} diff --git a/gio/giold/cmd/gogio/wayland_test.go b/gio/giold/cmd/gogio/wayland_test.go new file mode 100644 index 0000000..df10410 --- /dev/null +++ b/gio/giold/cmd/gogio/wayland_test.go @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "bufio" + "bytes" + "context" + "fmt" + "image" + "image/png" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "sync" + "text/template" + "time" +) + +type WaylandTestDriver struct { + driverBase + + runtimeDir string + socket string + display string +} + +// No bars or anything fancy. Just a white background with our dimensions. +var tmplSwayConfig = template.Must(template.New("").Parse(` +output * bg #FFFFFF solid_color +output * mode {{.Width}}x{{.Height}} +default_border none +`)) + +var rxSwayReady = regexp.MustCompile(`Running compositor on wayland display '(.*)'`) + +func (d *WaylandTestDriver) Start(path string) { + // We want os.Environ, so that it can e.g. find $DISPLAY to run within + // X11. wlroots env vars are documented at: + // https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md + env := os.Environ() + if *headless { + env = append(env, "WLR_BACKENDS=headless") + } + + d.needPrograms( + "sway", // to run a wayland compositor + "grim", // to take screenshots + "swaymsg", // to send input + ) + + // First, build the app. + dir := d.tempDir("gio-endtoend-wayland") + bin := filepath.Join(dir, "red") + flags := []string{"build", "-tags", "nox11", "-o=" + bin} + if raceEnabled { + flags = append(flags, "-race") + } + flags = append(flags, path) + cmd := exec.Command("go", flags...) + if out, err := cmd.CombinedOutput(); err != nil { + d.Fatalf("could not build app: %s:\n%s", err, out) + } + + conf := filepath.Join(dir, "config") + f, err := os.Create(conf) + if err != nil { + d.Fatal(err) + } + defer f.Close() + if err := tmplSwayConfig.Execute(f, struct{ Width, Height int }{ + d.width, d.height, + }); err != nil { + d.Fatal(err) + } + + d.socket = filepath.Join(dir, "socket") + env = append(env, "SWAYSOCK="+d.socket) + d.runtimeDir = dir + env = append(env, "XDG_RUNTIME_DIR="+d.runtimeDir) + + var wg sync.WaitGroup + d.Cleanup(wg.Wait) + + // First, start sway. + { + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, "sway", "--config", conf, "--verbose") + cmd.Env = env + stderr, err := cmd.StderrPipe() + if err != nil { + d.Fatal(err) + } + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + d.Cleanup(func() { + // Give it a chance to exit gracefully, cleaning up + // after itself. After 10ms, the deferred cancel above + // will signal an os.Kill. + cmd.Process.Signal(os.Interrupt) + time.Sleep(10 * time.Millisecond) + }) + + // Wait for sway to be ready. We probably don't need a deadline + // here. + br := bufio.NewReader(stderr) + for { + line, err := br.ReadString('\n') + if err != nil { + d.Fatal(err) + } + if m := rxSwayReady.FindStringSubmatch(line); m != nil { + d.display = m[1] + break + } + } + + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil && !strings.Contains(err.Error(), "interrupt") { + // Don't print all stderr, since we use --verbose. + // TODO(mvdan): if it's useful, probably filter + // errors and show them. + d.Error(err) + } + wg.Done() + }() + } + + // Then, start our program on the sway compositor above. + { + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, bin) + cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display} + output, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + cmd.Stderr = cmd.Stdout + d.output = output + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil { + d.Error(err) + } + wg.Done() + }() + } + + // Wait for the gio app to render. + d.waitForFrame() +} + +func (d *WaylandTestDriver) Screenshot() image.Image { + cmd := exec.Command("grim", "/dev/stdout") + cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display} + out, err := cmd.CombinedOutput() + if err != nil { + d.Errorf("%s", out) + d.Fatal(err) + } + img, err := png.Decode(bytes.NewReader(out)) + if err != nil { + d.Fatal(err) + } + return img +} + +func (d *WaylandTestDriver) swaymsg(args ...interface{}) { + strs := []string{"--socket", d.socket} + for _, arg := range args { + strs = append(strs, fmt.Sprint(arg)) + } + cmd := exec.Command("swaymsg", strs...) + if out, err := cmd.CombinedOutput(); err != nil { + d.Errorf("%s", out) + d.Fatal(err) + } +} + +func (d *WaylandTestDriver) Click(x, y int) { + d.swaymsg("seat", "-", "cursor", "set", x, y) + d.swaymsg("seat", "-", "cursor", "press", "button1") + d.swaymsg("seat", "-", "cursor", "release", "button1") + + // Wait for the gio app to render after this click. + d.waitForFrame() +} diff --git a/gio/giold/cmd/gogio/windows_test.go b/gio/giold/cmd/gogio/windows_test.go new file mode 100644 index 0000000..996b511 --- /dev/null +++ b/gio/giold/cmd/gogio/windows_test.go @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "context" + "image" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "sync" + "time" + + "golang.org/x/image/draw" +) + +// Wine is tightly coupled with X11 at the moment, and we can reuse the same +// methods to automate screenshots and clicks. The main difference is how we +// build and run the app. + +// The only quirk is that it seems impossible for the Wine window to take the +// entirety of the X server's dimensions, even if we try to resize it to take +// the entire display. It seems to want to leave some vertical space empty, +// presumably for window decorations or the "start" bar on Windows. To work +// around that, make the X server 50x50px bigger, and crop the screenshots back +// to the original size. + +type WineTestDriver struct { + X11TestDriver +} + +func (d *WineTestDriver) Start(path string) { + d.needPrograms("wine") + + // First, build the app. + bin := filepath.Join(d.tempDir("gio-endtoend-windows"), "red.exe") + flags := []string{"build", "-o=" + bin} + if raceEnabled { + if runtime.GOOS != "windows" { + // cross-compilation disables CGo, which breaks -race. + d.Skipf("can't cross-compile -race for Windows; skipping") + } + flags = append(flags, "-race") + } + flags = append(flags, path) + cmd := exec.Command("go", flags...) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "GOOS=windows") + if out, err := cmd.CombinedOutput(); err != nil { + d.Fatalf("could not build app: %s:\n%s", err, out) + } + + var wg sync.WaitGroup + d.Cleanup(wg.Wait) + + // Add 50x50px to the display dimensions, as discussed earlier. + d.startServer(&wg, d.width+50, d.height+50) + + // Then, start our program via Wine on the X server above. + { + cacheDir, err := os.UserCacheDir() + if err != nil { + d.Fatal(err) + } + // Use a wine directory separate from the default ~/.wine, so + // that the user's winecfg doesn't affect our test. This will + // default to ~/.cache/gio-e2e-wine. We use the user's cache, + // to reuse a previously set up wineprefix. + wineprefix := filepath.Join(cacheDir, "gio-e2e-wine") + + // First, ensure that wineprefix is up to date with wineboot. + // Wait for this separately from the first frame, as setting up + // a new prefix might take 5s on its own. + env := []string{ + "DISPLAY=" + d.display, + "WINEDEBUG=fixme-all", // hide "fixme" noise + "WINEPREFIX=" + wineprefix, + + // Disable wine-gecko (Explorer) and wine-mono (.NET). + // Otherwise, if not installed, wineboot will get stuck + // with a prompt to install them on the virtual X + // display. Moreover, Gio doesn't need either, and wine + // is faster without them. + "WINEDLLOVERRIDES=mscoree,mshtml=", + } + { + start := time.Now() + cmd := exec.Command("wine", "wineboot", "-i") + cmd.Env = env + // Use a combined output pipe instead of CombinedOutput, + // so that we only wait for the child process to exit, + // and we don't need to wait for all of wine's + // grandchildren to exit and stop writing. This is + // relevant as wine leaves "wineserver" lingering for + // three seconds by default, to be reused later. + stdout, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + cmd.Stderr = cmd.Stdout + if err := cmd.Run(); err != nil { + io.Copy(os.Stderr, stdout) + d.Fatal(err) + } + d.Logf("set up WINEPREFIX in %s", time.Since(start)) + } + + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, "wine", bin) + cmd.Env = env + output, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + cmd.Stderr = cmd.Stdout + d.output = output + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil { + d.Error(err) + } + wg.Done() + }() + } + // Wait for the gio app to render. + d.waitForFrame() + + // xdotool seems to fail at actually moving the window if we use it + // immediately after Gio is ready. Why? + // We can't tell if the windowmove operation worked until we take a + // screenshot, because the getwindowgeometry op reports the 0x0 + // coordinates even if the window wasn't moved properly. + // A sleep of ~20ms seems to be enough on an idle laptop. Use 20x that. + // TODO(mvdan): revisit this, when you have a spare three hours. + time.Sleep(400 * time.Millisecond) + id := d.xdotool("search", "--sync", "--onlyvisible", "--name", "Gio") + d.xdotool("windowmove", "--sync", id, 0, 0) +} + +func (d *WineTestDriver) Screenshot() image.Image { + img := d.X11TestDriver.Screenshot() + // Crop the screenshot back to the original dimensions. + cropped := image.NewRGBA(image.Rect(0, 0, d.width, d.height)) + draw.Draw(cropped, cropped.Bounds(), img, image.Point{}, draw.Src) + return cropped +} diff --git a/gio/giold/cmd/gogio/windowsbuild.go b/gio/giold/cmd/gogio/windowsbuild.go new file mode 100644 index 0000000..1af8668 --- /dev/null +++ b/gio/giold/cmd/gogio/windowsbuild.go @@ -0,0 +1,412 @@ +package main + +import ( + "bytes" + "encoding/binary" + "fmt" + "image/png" + "io" + "math" + "os" + "os/exec" + "path/filepath" + "reflect" + "strconv" + "strings" + "text/template" + + "github.com/akavel/rsrc/binutil" + "github.com/akavel/rsrc/coff" + "golang.org/x/text/encoding/unicode" +) + +func buildWindows(tmpDir string, bi *buildInfo) error { + builder := &windowsBuilder{TempDir: tmpDir} + builder.DestDir = *destPath + if builder.DestDir == "" { + builder.DestDir = bi.pkgPath + } + + name := bi.name + if *destPath != "" { + if filepath.Ext(*destPath) != ".exe" { + return fmt.Errorf("invalid output name %q, it must end with `.exe`", *destPath) + } + name = filepath.Base(*destPath) + } + name = strings.TrimSuffix(name, ".exe") + sdk := bi.minsdk + if sdk > 10 { + return fmt.Errorf("invalid minsdk (%d) it's higher than Windows 10", sdk) + } + version := strconv.Itoa(bi.version) + if bi.version > math.MaxUint16 { + return fmt.Errorf("version (%d) is larger than the maximum (%d)", bi.version, math.MaxUint16) + } + + for _, arch := range bi.archs { + builder.Coff = coff.NewRSRC() + builder.Coff.Arch(arch) + + if err := builder.embedIcon(bi.iconPath); err != nil { + return err + } + + if err := builder.embedManifest(windowsManifest{ + Version: "1.0.0." + version, + WindowsVersion: sdk, + Name: name, + }); err != nil { + return fmt.Errorf("can't create manifest: %v", err) + } + + if err := builder.embedInfo(windowsResources{ + Version: [2]uint32{uint32(1) << 16, uint32(bi.version)}, + VersionHuman: "1.0.0." + version, + Name: name, + Language: 0x0400, // Process Default Language: https://docs.microsoft.com/en-us/previous-versions/ms957130(v=msdn.10) + }); err != nil { + return fmt.Errorf("can't create info: %v", err) + } + + if err := builder.buildResource(bi, name, arch); err != nil { + return fmt.Errorf("can't build the resources: %v", err) + } + + if err := builder.buildProgram(bi, name, arch); err != nil { + return err + } + } + + return nil +} + +type ( + windowsResources struct { + Version [2]uint32 + VersionHuman string + Language uint16 + Name string + } + windowsManifest struct { + Version string + WindowsVersion int + Name string + } + windowsBuilder struct { + TempDir string + DestDir string + Coff *coff.Coff + } +) + +const ( + // https://docs.microsoft.com/en-us/windows/win32/menurc/resource-types + windowsResourceIcon = 3 + windowsResourceIconGroup = windowsResourceIcon + 11 + windowsResourceManifest = 24 + windowsResourceVersion = 16 +) + +type bufferCoff struct { + bytes.Buffer +} + +func (b *bufferCoff) Size() int64 { + return int64(b.Len()) +} + +func (b *windowsBuilder) embedIcon(path string) (err error) { + iconFile, err := os.Open(path) + if err != nil { + return fmt.Errorf("can't read the icon located at %s: %v", path, err) + } + defer iconFile.Close() + + iconImage, err := png.Decode(iconFile) + if err != nil { + return fmt.Errorf("can't decode the PNG file (%s): %v", path, err) + } + + sizes := []int{16, 32, 48, 64, 128, 256} + var iconHeader bufferCoff + + // GRPICONDIR structure. + if err := binary.Write(&iconHeader, binary.LittleEndian, [3]uint16{0, 1, uint16(len(sizes))}); err != nil { + return err + } + + for _, size := range sizes { + var iconBuffer bufferCoff + + if err := png.Encode(&iconBuffer, resizeIcon(iconVariant{size: size, fill: false}, iconImage)); err != nil { + return fmt.Errorf("can't encode image: %v", err) + } + + b.Coff.AddResource(windowsResourceIcon, uint16(size), &iconBuffer) + + if err := binary.Write(&iconHeader, binary.LittleEndian, struct { + Size [2]uint8 + Color [2]uint8 + Planes uint16 + BitCount uint16 + Length uint32 + Id uint16 + }{ + Size: [2]uint8{uint8(size % 256), uint8(size % 256)}, // "0" means 256px. + Planes: 1, + BitCount: 32, + Length: uint32(iconBuffer.Len()), + Id: uint16(size), + }); err != nil { + return err + } + } + + b.Coff.AddResource(windowsResourceIconGroup, 1, &iconHeader) + + return nil +} + +func (b *windowsBuilder) buildResource(buildInfo *buildInfo, name string, arch string) error { + out, err := os.Create(filepath.Join(buildInfo.pkgPath, name+"_windows_"+arch+".syso")) + if err != nil { + return err + } + defer out.Close() + b.Coff.Freeze() + + // See https://github.com/akavel/rsrc/internal/write.go#L13. + w := binutil.Writer{W: out} + binutil.Walk(b.Coff, func(v reflect.Value, path string) error { + if binutil.Plain(v.Kind()) { + w.WriteLE(v.Interface()) + return nil + } + vv, ok := v.Interface().(binutil.SizedReader) + if ok { + w.WriteFromSized(vv) + return binutil.WALK_SKIP + } + return nil + }) + + if w.Err != nil { + return fmt.Errorf("error writing output file: %s", w.Err) + } + + return nil +} + +func (b *windowsBuilder) buildProgram(buildInfo *buildInfo, name string, arch string) error { + dest := b.DestDir + if len(buildInfo.archs) > 1 { + dest = filepath.Join(filepath.Dir(b.DestDir), name+"_"+arch+".exe") + } + + cmd := exec.Command( + "go", + "build", + "-ldflags=-H=windowsgui "+buildInfo.ldflags, + "-tags="+buildInfo.tags, + "-o", dest, + buildInfo.pkgPath, + ) + cmd.Env = append( + os.Environ(), + "GOOS=windows", + "GOARCH="+arch, + ) + _, err := runCmd(cmd) + return err +} + +func (b *windowsBuilder) embedManifest(v windowsManifest) error { + t, err := template.New("manifest").Parse(` + + + {{.Name}} + + + {{if (le .WindowsVersion 10)}} +{{end}} + {{if (le .WindowsVersion 9)}} +{{end}} + {{if (le .WindowsVersion 8)}} +{{end}} + {{if (le .WindowsVersion 7)}} +{{end}} + {{if (le .WindowsVersion 6)}} +{{end}} + + + + + + + + + + + + true + + +`) + if err != nil { + return err + } + + var manifest bufferCoff + if err := t.Execute(&manifest, v); err != nil { + return err + } + + b.Coff.AddResource(windowsResourceManifest, 1, &manifest) + + return nil +} + +func (b *windowsBuilder) embedInfo(v windowsResources) error { + page := uint16(1) + + // https://docs.microsoft.com/pt-br/windows/win32/menurc/vs-versioninfo + t := newValue(valueBinary, "VS_VERSION_INFO", []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/api/VerRsrc/ns-verrsrc-vs_fixedfileinfo + windowsInfoValueFixed{ + Signature: 0xFEEF04BD, + StructVersion: 0x00010000, + FileVersion: v.Version, + ProductVersion: v.Version, + FileFlagMask: 0x3F, + FileFlags: 0, + FileOS: 0x40004, + FileType: 0x1, + FileSubType: 0, + }, + // https://docs.microsoft.com/pt-br/windows/win32/menurc/stringfileinfo + newValue(valueText, "StringFileInfo", []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/menurc/stringtable + newValue(valueText, fmt.Sprintf("%04X%04X", v.Language, page), []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/menurc/string-str + newValue(valueText, "ProductVersion", v.VersionHuman), + newValue(valueText, "FileVersion", v.VersionHuman), + newValue(valueText, "FileDescription", v.Name), + newValue(valueText, "ProductName", v.Name), + // TODO include more data: gogio must have some way to provide such information (like Company Name, Copyright...) + }), + }), + // https://docs.microsoft.com/pt-br/windows/win32/menurc/varfileinfo + newValue(valueBinary, "VarFileInfo", []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/menurc/var-str + newValue(valueBinary, "Translation", uint32(page)<<16|uint32(v.Language)), + }), + }) + + // For some reason the ValueLength of the VS_VERSIONINFO must be the byte-length of `windowsInfoValueFixed`: + t.ValueLength = 52 + + var verrsrc bufferCoff + if _, err := t.WriteTo(&verrsrc); err != nil { + return err + } + + b.Coff.AddResource(windowsResourceVersion, 1, &verrsrc) + + return nil +} + +type windowsInfoValueFixed struct { + Signature uint32 + StructVersion uint32 + FileVersion [2]uint32 + ProductVersion [2]uint32 + FileFlagMask uint32 + FileFlags uint32 + FileOS uint32 + FileType uint32 + FileSubType uint32 + FileDate [2]uint32 +} + +func (v windowsInfoValueFixed) WriteTo(w io.Writer) (_ int64, err error) { + return 0, binary.Write(w, binary.LittleEndian, v) +} + +type windowsInfoValue struct { + Length uint16 + ValueLength uint16 + Type uint16 + Key []byte + Value []byte +} + +func (v windowsInfoValue) WriteTo(w io.Writer) (_ int64, err error) { + // binary.Write doesn't support []byte inside struct. + if err = binary.Write(w, binary.LittleEndian, [3]uint16{v.Length, v.ValueLength, v.Type}); err != nil { + return 0, err + } + if _, err = w.Write(v.Key); err != nil { + return 0, err + } + if _, err = w.Write(v.Value); err != nil { + return 0, err + } + return 0, nil +} + +const ( + valueBinary uint16 = 0 + valueText uint16 = 1 +) + +func newValue(valueType uint16, key string, input interface{}) windowsInfoValue { + v := windowsInfoValue{ + Type: valueType, + Length: 6, + } + + padding := func(in []byte) []byte { + if l := uint16(len(in)) + v.Length; l%4 != 0 { + return append(in, make([]byte, 4-l%4)...) + } + return in + } + + v.Key = padding(utf16Encode(key)) + v.Length += uint16(len(v.Key)) + + switch in := input.(type) { + case string: + v.Value = padding(utf16Encode(in)) + v.ValueLength = uint16(len(v.Value) / 2) + case []io.WriterTo: + var buff bytes.Buffer + for k := range in { + if _, err := in[k].WriteTo(&buff); err != nil { + panic(err) + } + } + v.Value = buff.Bytes() + default: + var buff bytes.Buffer + if err := binary.Write(&buff, binary.LittleEndian, in); err != nil { + panic(err) + } + v.ValueLength = uint16(buff.Len()) + v.Value = buff.Bytes() + } + + v.Length += uint16(len(v.Value)) + + return v +} + +// utf16Encode encodes the string to UTF16 with null-termination. +func utf16Encode(s string) []byte { + b, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder().Bytes([]byte(s)) + if err != nil { + panic(err) + } + return append(b, 0x00, 0x00) // null-termination. +} diff --git a/gio/giold/cmd/gogio/x11_test.go b/gio/giold/cmd/gogio/x11_test.go new file mode 100644 index 0000000..9bb3174 --- /dev/null +++ b/gio/giold/cmd/gogio/x11_test.go @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "bytes" + "context" + "fmt" + "image" + "image/png" + "io" + "math/rand" + "os" + "os/exec" + "path/filepath" + "sync" + "time" +) + +type X11TestDriver struct { + driverBase + + display string +} + +func (d *X11TestDriver) Start(path string) { + // First, build the app. + bin := filepath.Join(d.tempDir("gio-endtoend-x11"), "red") + flags := []string{"build", "-tags", "nowayland", "-o=" + bin} + if raceEnabled { + flags = append(flags, "-race") + } + flags = append(flags, path) + cmd := exec.Command("go", flags...) + if out, err := cmd.CombinedOutput(); err != nil { + d.Fatalf("could not build app: %s:\n%s", err, out) + } + + var wg sync.WaitGroup + d.Cleanup(wg.Wait) + + d.startServer(&wg, d.width, d.height) + + // Then, start our program on the X server above. + { + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, bin) + cmd.Env = []string{"DISPLAY=" + d.display} + output, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + cmd.Stderr = cmd.Stdout + d.output = output + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil { + d.Error(err) + } + wg.Done() + }() + } + + // Wait for the gio app to render. + d.waitForFrame() +} + +func (d *X11TestDriver) startServer(wg *sync.WaitGroup, width, height int) { + // Pick a random display number between 1 and 100,000. Most machines + // will only be using :0, so there's only a 0.001% chance of two + // concurrent test runs to run into a conflict. + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + d.display = fmt.Sprintf(":%d", rnd.Intn(100000)+1) + + var xprog string + xflags := []string{ + "-wr", // we want a white background; the default is black + } + if *headless { + xprog = "Xvfb" // virtual X server + xflags = append(xflags, "-screen", "0", fmt.Sprintf("%dx%dx24", width, height)) + } else { + xprog = "Xephyr" // nested X server as a window + xflags = append(xflags, "-screen", fmt.Sprintf("%dx%d", width, height)) + } + xflags = append(xflags, d.display) + + d.needPrograms( + xprog, // to run the X server + "scrot", // to take screenshots + "xdotool", // to send input + ) + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, xprog, xflags...) + combined := &bytes.Buffer{} + cmd.Stdout = combined + cmd.Stderr = combined + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + d.Cleanup(func() { + // Give it a chance to exit gracefully, cleaning up + // after itself. After 10ms, the deferred cancel above + // will signal an os.Kill. + cmd.Process.Signal(os.Interrupt) + time.Sleep(10 * time.Millisecond) + }) + + // Wait for the X server to be ready. The socket path isn't + // terribly portable, but that's okay for now. + withRetries(d.T, time.Second, func() error { + socket := fmt.Sprintf("/tmp/.X11-unix/X%s", d.display[1:]) + _, err := os.Stat(socket) + return err + }) + + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil { + // Print all output and error. + io.Copy(os.Stdout, combined) + d.Error(err) + } + wg.Done() + }() +} + +func (d *X11TestDriver) Screenshot() image.Image { + cmd := exec.Command("scrot", "--silent", "--overwrite", "/dev/stdout") + cmd.Env = []string{"DISPLAY=" + d.display} + out, err := cmd.CombinedOutput() + if err != nil { + d.Errorf("%s", out) + d.Fatal(err) + } + img, err := png.Decode(bytes.NewReader(out)) + if err != nil { + d.Fatal(err) + } + return img +} + +func (d *X11TestDriver) xdotool(args ...interface{}) string { + d.Helper() + strs := make([]string, len(args)) + for i, arg := range args { + strs[i] = fmt.Sprint(arg) + } + cmd := exec.Command("xdotool", strs...) + cmd.Env = []string{"DISPLAY=" + d.display} + out, err := cmd.CombinedOutput() + if err != nil { + d.Errorf("%s", out) + d.Fatal(err) + } + return string(bytes.TrimSpace(out)) +} + +func (d *X11TestDriver) Click(x, y int) { + d.xdotool("mousemove", "--sync", x, y) + d.xdotool("click", "1") + + // Wait for the gio app to render after this click. + d.waitForFrame() +} diff --git a/gio/giold/f32/affine.go b/gio/giold/f32/affine.go new file mode 100644 index 0000000..667f7e9 --- /dev/null +++ b/gio/giold/f32/affine.go @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32 + +import ( + "fmt" + "math" +) + +// Affine2D represents an affine 2D transformation. The zero value if Affine2D +// represents the identity transform. +type Affine2D struct { + // in order to make the zero value of Affine2D represent the identity + // transform we store it with the identity matrix subtracted, that is + // if the actual transformation matrix is: + // [sx, hx, ox] + // [hy, sy, oy] + // [ 0, 0, 1] + // we store a = sx-1 and e = sy-1 + a, b, c float32 + d, e, f float32 +} + +// NewAffine2D creates a new Affine2D transform from the matrix elements +// in row major order. The rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1]. +func NewAffine2D(sx, hx, ox, hy, sy, oy float32) Affine2D { + return Affine2D{ + a: sx - 1, b: hx, c: ox, + d: hy, e: sy - 1, f: oy, + } +} + +// Offset the transformation. +func (a Affine2D) Offset(offset Point) Affine2D { + return Affine2D{ + a.a, a.b, a.c + offset.X, + a.d, a.e, a.f + offset.Y, + } +} + +// Scale the transformation around the given origin. +func (a Affine2D) Scale(origin, factor Point) Affine2D { + if origin == (Point{}) { + return a.scale(factor) + } + a = a.Offset(origin.Mul(-1)) + a = a.scale(factor) + return a.Offset(origin) +} + +// Rotate the transformation by the given angle (in radians) counter clockwise around the given origin. +func (a Affine2D) Rotate(origin Point, radians float32) Affine2D { + if origin == (Point{}) { + return a.rotate(radians) + } + a = a.Offset(origin.Mul(-1)) + a = a.rotate(radians) + return a.Offset(origin) +} + +// Shear the transformation by the given angle (in radians) around the given origin. +func (a Affine2D) Shear(origin Point, radiansX, radiansY float32) Affine2D { + if origin == (Point{}) { + return a.shear(radiansX, radiansY) + } + a = a.Offset(origin.Mul(-1)) + a = a.shear(radiansX, radiansY) + return a.Offset(origin) +} + +// Mul returns A*B. +func (A Affine2D) Mul(B Affine2D) (r Affine2D) { + r.a = (A.a+1)*(B.a+1) + A.b*B.d - 1 + r.b = (A.a+1)*B.b + A.b*(B.e+1) + r.c = (A.a+1)*B.c + A.b*B.f + A.c + r.d = A.d*(B.a+1) + (A.e+1)*B.d + r.e = A.d*B.b + (A.e+1)*(B.e+1) - 1 + r.f = A.d*B.c + (A.e+1)*B.f + A.f + return r +} + +// Invert the transformation. Note that if the matrix is close to singular +// numerical errors may become large or infinity. +func (a Affine2D) Invert() Affine2D { + if a.a == 0 && a.b == 0 && a.d == 0 && a.e == 0 { + return Affine2D{a: 0, b: 0, c: -a.c, d: 0, e: 0, f: -a.f} + } + a.a += 1 + a.e += 1 + det := a.a*a.e - a.b*a.d + a.a, a.e = a.e/det, a.a/det + a.b, a.d = -a.b/det, -a.d/det + temp := a.c + a.c = -a.a*a.c - a.b*a.f + a.f = -a.d*temp - a.e*a.f + a.a -= 1 + a.e -= 1 + return a +} + +// Transform p by returning a*p. +func (a Affine2D) Transform(p Point) Point { + return Point{ + X: p.X*(a.a+1) + p.Y*a.b + a.c, + Y: p.X*a.d + p.Y*(a.e+1) + a.f, + } +} + +// Elems returns the matrix elements of the transform in row-major order. The +// rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1]. +func (a Affine2D) Elems() (sx, hx, ox, hy, sy, oy float32) { + return a.a + 1, a.b, a.c, a.d, a.e + 1, a.f +} + +func (a Affine2D) scale(factor Point) Affine2D { + return Affine2D{ + (a.a+1)*factor.X - 1, a.b * factor.X, a.c * factor.X, + a.d * factor.Y, (a.e+1)*factor.Y - 1, a.f * factor.Y, + } +} + +func (a Affine2D) rotate(radians float32) Affine2D { + sin, cos := math.Sincos(float64(radians)) + s, c := float32(sin), float32(cos) + return Affine2D{ + (a.a+1)*c - a.d*s - 1, a.b*c - (a.e+1)*s, a.c*c - a.f*s, + (a.a+1)*s + a.d*c, a.b*s + (a.e+1)*c - 1, a.c*s + a.f*c, + } +} + +func (a Affine2D) shear(radiansX, radiansY float32) Affine2D { + tx := float32(math.Tan(float64(radiansX))) + ty := float32(math.Tan(float64(radiansY))) + return Affine2D{ + (a.a + 1) + a.d*tx - 1, a.b + (a.e+1)*tx, a.c + a.f*tx, + (a.a+1)*ty + a.d, a.b*ty + (a.e + 1) - 1, a.f*ty + a.f, + } +} + +func (a Affine2D) String() string { + sx, hx, ox, hy, sy, oy := a.Elems() + return fmt.Sprintf("[[%f %f %f] [%f %f %f]]", sx, hx, ox, hy, sy, oy) +} diff --git a/gio/giold/f32/affine_test.go b/gio/giold/f32/affine_test.go new file mode 100644 index 0000000..4077b8d --- /dev/null +++ b/gio/giold/f32/affine_test.go @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32 + +import ( + "math" + "testing" +) + +func eq(p1, p2 Point) bool { + tol := 1e-5 + dx, dy := p2.X-p1.X, p2.Y-p1.Y + return math.Abs(math.Sqrt(float64(dx*dx+dy*dy))) < tol +} + +func eqaff(x, y Affine2D) bool { + tol := 1e-5 + return math.Abs(float64(x.a-y.a)) < tol && + math.Abs(float64(x.b-y.b)) < tol && + math.Abs(float64(x.c-y.c)) < tol && + math.Abs(float64(x.d-y.d)) < tol && + math.Abs(float64(x.e-y.e)) < tol && + math.Abs(float64(x.f-y.f)) < tol +} + +func TestTransformOffset(t *testing.T) { + p := Point{X: 1, Y: 2} + o := Point{X: 2, Y: -3} + + r := Affine2D{}.Offset(o).Transform(p) + if !eq(r, Pt(3, -1)) { + t.Errorf("offset transformation mismatch: have %v, want {3 -1}", r) + } + i := Affine2D{}.Offset(o).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("offset transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestTransformScale(t *testing.T) { + p := Point{X: 1, Y: 2} + s := Point{X: -1, Y: 2} + + r := Affine2D{}.Scale(Point{}, s).Transform(p) + if !eq(r, Pt(-1, 4)) { + t.Errorf("scale transformation mismatch: have %v, want {-1 4}", r) + } + i := Affine2D{}.Scale(Point{}, s).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("scale transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestTransformRotate(t *testing.T) { + p := Point{X: 1, Y: 0} + a := float32(math.Pi / 2) + + r := Affine2D{}.Rotate(Point{}, a).Transform(p) + if !eq(r, Pt(0, 1)) { + t.Errorf("rotate transformation mismatch: have %v, want {0 1}", r) + } + i := Affine2D{}.Rotate(Point{}, a).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("rotate transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestTransformShear(t *testing.T) { + p := Point{X: 1, Y: 1} + + r := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Transform(p) + if !eq(r, Pt(2, 1)) { + t.Errorf("shear transformation mismatch: have %v, want {2 1}", r) + } + i := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("shear transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestTransformMultiply(t *testing.T) { + p := Point{X: 1, Y: 2} + o := Point{X: 2, Y: -3} + s := Point{X: -1, Y: 2} + a := float32(-math.Pi / 2) + + r := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Transform(p) + if !eq(r, Pt(1, 3)) { + t.Errorf("complex transformation mismatch: have %v, want {1 3}", r) + } + i := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("complex transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestPrimes(t *testing.T) { + xa := NewAffine2D(9, 11, 13, 17, 19, 23) + xb := NewAffine2D(29, 31, 37, 43, 47, 53) + + pa := Point{X: 2, Y: 3} + pb := Point{X: 5, Y: 7} + + for _, test := range []struct { + x Affine2D + p Point + exp Point + }{ + {x: xa, p: pa, exp: Pt(64, 114)}, + {x: xa, p: pb, exp: Pt(135, 241)}, + {x: xb, p: pa, exp: Pt(188, 280)}, + {x: xb, p: pb, exp: Pt(399, 597)}, + } { + got := test.x.Transform(test.p) + if !eq(got, test.exp) { + t.Errorf("%v.Transform(%v): have %v, want %v", test.x, test.p, got, test.exp) + } + } + + for _, test := range []struct { + x Affine2D + exp Affine2D + }{ + {x: xa, exp: NewAffine2D(-1.1875, 0.6875, -0.375, 1.0625, -0.5625, -0.875)}, + {x: xb, exp: NewAffine2D(1.5666667, -1.0333333, -3.2000008, -1.4333333, 1-0.03333336, 1.7999992)}, + } { + got := test.x.Invert() + if !eqaff(got, test.exp) { + t.Errorf("%v.Invert(): have %v, want %v", test.x, got, test.exp) + } + } + + got := xa.Mul(xb) + exp := NewAffine2D(734, 796, 929, 1310, 1420, 1659) + if !eqaff(got, exp) { + t.Errorf("%v.Mul(%v): have %v, want %v", xa, xb, got, exp) + } +} + +func TestTransformScaleAround(t *testing.T) { + p := Pt(-1, -1) + target := Pt(-6, -13) + pt := Affine2D{}.Scale(Pt(4, 5), Pt(2, 3)).Transform(p) + if !eq(pt, target) { + t.Log(pt, "!=", target) + t.Error("Scale not as expected") + } +} + +func TestTransformRotateAround(t *testing.T) { + p := Pt(-1, -1) + pt := Affine2D{}.Rotate(Pt(1, 1), -math.Pi/2).Transform(p) + target := Pt(-1, 3) + if !eq(pt, target) { + t.Log(pt, "!=", target) + t.Error("Rotate not as expected") + } +} + +func TestMulOrder(t *testing.T) { + A := Affine2D{}.Offset(Pt(100, 100)) + B := Affine2D{}.Scale(Point{}, Pt(2, 2)) + _ = A + _ = B + + T1 := Affine2D{}.Offset(Pt(100, 100)).Scale(Point{}, Pt(2, 2)) + T2 := B.Mul(A) + + if T1 != T2 { + t.Log(T1) + t.Log(T2) + t.Error("multiplication / transform order not as expected") + } +} + +func BenchmarkTransformOffset(b *testing.B) { + p := Point{X: 1, Y: 2} + o := Point{X: 0.5, Y: 0.5} + aff := Affine2D{}.Offset(o) + + for i := 0; i < b.N; i++ { + p = aff.Transform(p) + } + _ = p +} + +func BenchmarkTransformScale(b *testing.B) { + p := Point{X: 1, Y: 2} + s := Point{X: 0.5, Y: 0.5} + aff := Affine2D{}.Scale(Point{}, s) + for i := 0; i < b.N; i++ { + p = aff.Transform(p) + } + _ = p +} + +func BenchmarkTransformRotate(b *testing.B) { + p := Point{X: 1, Y: 2} + a := float32(math.Pi / 2) + aff := Affine2D{}.Rotate(Point{}, a) + for i := 0; i < b.N; i++ { + p = aff.Transform(p) + } + _ = p +} + +func BenchmarkTransformTranslateMultiply(b *testing.B) { + a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) + t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}) + + for i := 0; i < b.N; i++ { + a = a.Mul(t) + } +} + +func BenchmarkTransformScaleMultiply(b *testing.B) { + a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) + t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}).Scale(Point{}, Point{X: 0.4, Y: -0.5}) + + for i := 0; i < b.N; i++ { + a = a.Mul(t) + } +} + +func BenchmarkTransformMultiply(b *testing.B) { + a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) + t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}).Rotate(Point{}, math.Pi/7) + + for i := 0; i < b.N; i++ { + a = a.Mul(t) + } +} diff --git a/gio/giold/f32/f32.go b/gio/giold/f32/f32.go new file mode 100644 index 0000000..69745ba --- /dev/null +++ b/gio/giold/f32/f32.go @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package f32 is a float32 implementation of package image's +Point and Rectangle. + +The coordinate space has the origin in the top left +corner with the axes extending right and down. +*/ +package f32 + +import "strconv" + +// A Point is a two dimensional point. +type Point struct { + X, Y float32 +} + +// String return a string representation of p. +func (p Point) String() string { + return "(" + strconv.FormatFloat(float64(p.X), 'f', -1, 32) + + "," + strconv.FormatFloat(float64(p.Y), 'f', -1, 32) + ")" +} + +// A Rectangle contains the points (X, Y) where Min.X <= X < Max.X, +// Min.Y <= Y < Max.Y. +type Rectangle struct { + Min, Max Point +} + +// String return a string representation of r. +func (r Rectangle) String() string { + return r.Min.String() + "-" + r.Max.String() +} + +// Rect is a shorthand for Rectangle{Point{x0, y0}, Point{x1, y1}}. +// The returned Rectangle has x0 and y0 swapped if necessary so that +// it's correctly formed. +func Rect(x0, y0, x1, y1 float32) Rectangle { + if x0 > x1 { + x0, x1 = x1, x0 + } + if y0 > y1 { + y0, y1 = y1, y0 + } + return Rectangle{Point{x0, y0}, Point{x1, y1}} +} + +// Pt is shorthand for Point{X: x, Y: y}. +func Pt(x, y float32) Point { + return Point{X: x, Y: y} +} + +// Add return the point p+p2. +func (p Point) Add(p2 Point) Point { + return Point{X: p.X + p2.X, Y: p.Y + p2.Y} +} + +// Sub returns the vector p-p2. +func (p Point) Sub(p2 Point) Point { + return Point{X: p.X - p2.X, Y: p.Y - p2.Y} +} + +// Mul returns p scaled by s. +func (p Point) Mul(s float32) Point { + return Point{X: p.X * s, Y: p.Y * s} +} + +// In reports whether p is in r. +func (p Point) In(r Rectangle) bool { + return r.Min.X <= p.X && p.X < r.Max.X && + r.Min.Y <= p.Y && p.Y < r.Max.Y +} + +// Size returns r's width and height. +func (r Rectangle) Size() Point { + return Point{X: r.Dx(), Y: r.Dy()} +} + +// Dx returns r's width. +func (r Rectangle) Dx() float32 { + return r.Max.X - r.Min.X +} + +// Dy returns r's Height. +func (r Rectangle) Dy() float32 { + return r.Max.Y - r.Min.Y +} + +// Intersect returns the intersection of r and s. +func (r Rectangle) Intersect(s Rectangle) Rectangle { + if r.Min.X < s.Min.X { + r.Min.X = s.Min.X + } + if r.Min.Y < s.Min.Y { + r.Min.Y = s.Min.Y + } + if r.Max.X > s.Max.X { + r.Max.X = s.Max.X + } + if r.Max.Y > s.Max.Y { + r.Max.Y = s.Max.Y + } + return r +} + +// Union returns the union of r and s. +func (r Rectangle) Union(s Rectangle) Rectangle { + if r.Min.X > s.Min.X { + r.Min.X = s.Min.X + } + if r.Min.Y > s.Min.Y { + r.Min.Y = s.Min.Y + } + if r.Max.X < s.Max.X { + r.Max.X = s.Max.X + } + if r.Max.Y < s.Max.Y { + r.Max.Y = s.Max.Y + } + return r +} + +// Canon returns the canonical version of r, where Min is to +// the upper left of Max. +func (r Rectangle) Canon() Rectangle { + if r.Max.X < r.Min.X { + r.Min.X, r.Max.X = r.Max.X, r.Min.X + } + if r.Max.Y < r.Min.Y { + r.Min.Y, r.Max.Y = r.Max.Y, r.Min.Y + } + return r +} + +// Empty reports whether r represents the empty area. +func (r Rectangle) Empty() bool { + return r.Min.X >= r.Max.X || r.Min.Y >= r.Max.Y +} + +// Add offsets r with the vector p. +func (r Rectangle) Add(p Point) Rectangle { + return Rectangle{ + Point{r.Min.X + p.X, r.Min.Y + p.Y}, + Point{r.Max.X + p.X, r.Max.Y + p.Y}, + } +} + +// Sub offsets r with the vector -p. +func (r Rectangle) Sub(p Point) Rectangle { + return Rectangle{ + Point{r.Min.X - p.X, r.Min.Y - p.Y}, + Point{r.Max.X - p.X, r.Max.Y - p.Y}, + } +} diff --git a/gio/giold/font/gofont/gofont.go b/gio/giold/font/gofont/gofont.go new file mode 100644 index 0000000..9dedcd5 --- /dev/null +++ b/gio/giold/font/gofont/gofont.go @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package gofont exports the Go fonts as a text.Collection. +// +// See https://blog.golang.org/go-fonts for a description of the +// fonts, and the golang.org/x/image/font/gofont packages for the +// font data. +package gofont + +import ( + "fmt" + "sync" + + "golang.org/x/image/font/gofont/gobold" + "golang.org/x/image/font/gofont/gobolditalic" + "golang.org/x/image/font/gofont/goitalic" + "golang.org/x/image/font/gofont/gomedium" + "golang.org/x/image/font/gofont/gomediumitalic" + "golang.org/x/image/font/gofont/gomono" + "golang.org/x/image/font/gofont/gomonobold" + "golang.org/x/image/font/gofont/gomonobolditalic" + "golang.org/x/image/font/gofont/gomonoitalic" + "golang.org/x/image/font/gofont/goregular" + "golang.org/x/image/font/gofont/gosmallcaps" + "golang.org/x/image/font/gofont/gosmallcapsitalic" + + "realy.lol/gio/font/opentype" + "realy.lol/gio/text" +) + +var ( + once sync.Once + collection []text.FontFace +) + +func Collection() []text.FontFace { + once.Do(func() { + register(text.Font{}, goregular.TTF) + register(text.Font{Style: text.Italic}, goitalic.TTF) + register(text.Font{Weight: text.Bold}, gobold.TTF) + register(text.Font{Style: text.Italic, Weight: text.Bold}, + gobolditalic.TTF) + register(text.Font{Weight: text.Medium}, gomedium.TTF) + register(text.Font{Weight: text.Medium, Style: text.Italic}, + gomediumitalic.TTF) + register(text.Font{Variant: "Mono"}, gomono.TTF) + register(text.Font{Variant: "Mono", Weight: text.Bold}, gomonobold.TTF) + register(text.Font{Variant: "Mono", Weight: text.Bold, + Style: text.Italic}, gomonobolditalic.TTF) + register(text.Font{Variant: "Mono", Style: text.Italic}, + gomonoitalic.TTF) + register(text.Font{Variant: "Smallcaps"}, gosmallcaps.TTF) + register(text.Font{Variant: "Smallcaps", Style: text.Italic}, + gosmallcapsitalic.TTF) + // Ensure that any outside appends will not reuse the backing store. + n := len(collection) + collection = collection[:n:n] + }) + return collection +} + +func register(fnt text.Font, ttf []byte) { + face, err := opentype.Parse(ttf) + if err != nil { + panic(fmt.Errorf("failed to parse font: %v", err)) + } + fnt.Typeface = "Go" + collection = append(collection, text.FontFace{Font: fnt, Face: face}) +} diff --git a/gio/giold/font/opentype/opentype.go b/gio/giold/font/opentype/opentype.go new file mode 100644 index 0000000..dd74e73 --- /dev/null +++ b/gio/giold/font/opentype/opentype.go @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package opentype implements text layout and shaping for OpenType +// files. +package opentype + +import ( + "bytes" + "io" + "unicode" + "unicode/utf8" + + "golang.org/x/image/font" + "golang.org/x/image/font/sfnt" + "golang.org/x/image/math/fixed" + + "realy.lol/gio/f32" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/text" +) + +// Font implements text.Face. Its methods are safe to use +// concurrently. +type Font struct { + font *sfnt.Font +} + +// Collection is a collection of one or more fonts. When used as a text.Face, +// each rune will be assigned a glyph from the first font in the collection +// that supports it. +type Collection struct { + fonts []*opentype +} + +type opentype struct { + Font *sfnt.Font + Hinting font.Hinting +} + +// a glyph represents a rune and its advance according to a Font. +// TODO: remove this type and work on io.Readers directly. +type glyph struct { + Rune rune + Advance fixed.Int26_6 +} + +// NewFont parses an SFNT font, such as TTF or OTF data, from a []byte +// data source. +func Parse(src []byte) (*Font, error) { + fnt, err := sfnt.Parse(src) + if err != nil { + return nil, err + } + return &Font{font: fnt}, nil +} + +// ParseCollection parses an SFNT font collection, such as TTC or OTC data, +// from a []byte data source. +// +// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, +// it will return a collection containing 1 font. +func ParseCollection(src []byte) (*Collection, error) { + c, err := sfnt.ParseCollection(src) + if err != nil { + return nil, err + } + return newCollectionFrom(c) +} + +// ParseCollectionReaderAt parses an SFNT collection, such as TTC or OTC data, +// from an io.ReaderAt data source. +// +// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, it +// will return a collection containing 1 font. +func ParseCollectionReaderAt(src io.ReaderAt) (*Collection, error) { + c, err := sfnt.ParseCollectionReaderAt(src) + if err != nil { + return nil, err + } + return newCollectionFrom(c) +} + +func newCollectionFrom(coll *sfnt.Collection) (*Collection, error) { + fonts := make([]*opentype, coll.NumFonts()) + for i := range fonts { + fnt, err := coll.Font(i) + if err != nil { + return nil, err + } + fonts[i] = &opentype{ + Font: fnt, + Hinting: font.HintingFull, + } + } + return &Collection{fonts: fonts}, nil +} + +// NumFonts returns the number of fonts in the collection. +func (c *Collection) NumFonts() int { + return len(c.fonts) +} + +// Font returns the i'th font in the collection. +func (c *Collection) Font(i int) (*Font, error) { + if i < 0 || len(c.fonts) <= i { + return nil, sfnt.ErrNotFound + } + return &Font{font: c.fonts[i].Font}, nil +} + +func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int, + txt io.Reader) ([]text.Line, error) { + glyphs, err := readGlyphs(txt) + if err != nil { + return nil, err + } + fonts := []*opentype{{Font: f.font, Hinting: font.HintingFull}} + var buf sfnt.Buffer + return layoutText(&buf, ppem, maxWidth, fonts, glyphs) +} + +func (f *Font) Shape(ppem fixed.Int26_6, str text.Layout) op.CallOp { + var buf sfnt.Buffer + return textPath(&buf, ppem, + []*opentype{{Font: f.font, Hinting: font.HintingFull}}, str) +} + +func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics { + o := &opentype{Font: f.font, Hinting: font.HintingFull} + var buf sfnt.Buffer + return o.Metrics(&buf, ppem) +} + +func (c *Collection) Layout(ppem fixed.Int26_6, maxWidth int, + txt io.Reader) ([]text.Line, error) { + glyphs, err := readGlyphs(txt) + if err != nil { + return nil, err + } + var buf sfnt.Buffer + return layoutText(&buf, ppem, maxWidth, c.fonts, glyphs) +} + +func (c *Collection) Shape(ppem fixed.Int26_6, str text.Layout) op.CallOp { + var buf sfnt.Buffer + return textPath(&buf, ppem, c.fonts, str) +} + +func fontForGlyph(buf *sfnt.Buffer, fonts []*opentype, r rune) *opentype { + if len(fonts) < 1 { + return nil + } + for _, f := range fonts { + if f.HasGlyph(buf, r) { + return f + } + } + return fonts[0] // Use replacement character from the first font if necessary +} + +func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, + fonts []*opentype, glyphs []glyph) ([]text.Line, error) { + var lines []text.Line + var nextLine text.Line + updateBounds := func(f *opentype) { + m := f.Metrics(sbuf, ppem) + if m.Ascent > nextLine.Ascent { + nextLine.Ascent = m.Ascent + } + // m.Height is equal to m.Ascent + m.Descent + linegap. + // Compute the descent including the linegap. + descent := m.Height - m.Ascent + if descent > nextLine.Descent { + nextLine.Descent = descent + } + b := f.Bounds(sbuf, ppem) + nextLine.Bounds = nextLine.Bounds.Union(b) + } + maxDotX := fixed.I(maxWidth) + type state struct { + r rune + f *opentype + adv fixed.Int26_6 + x fixed.Int26_6 + idx int + len int + valid bool + } + var prev, word state + endLine := func() { + if prev.f == nil && len(fonts) > 0 { + prev.f = fonts[0] + } + updateBounds(prev.f) + nextLine.Layout = toLayout(glyphs[:prev.idx:prev.idx]) + nextLine.Width = prev.x + prev.adv + nextLine.Bounds.Max.X += prev.x + lines = append(lines, nextLine) + glyphs = glyphs[prev.idx:] + nextLine = text.Line{} + prev = state{} + word = state{} + } + for prev.idx < len(glyphs) { + g := &glyphs[prev.idx] + next := state{ + r: g.Rune, + f: fontForGlyph(sbuf, fonts, g.Rune), + idx: prev.idx + 1, + len: prev.len + utf8.RuneLen(g.Rune), + x: prev.x + prev.adv, + } + if next.f != nil { + if next.f != prev.f { + updateBounds(next.f) + } + next.adv, next.valid = next.f.GlyphAdvance(sbuf, ppem, g.Rune) + } + if g.Rune == '\n' { + // The newline is zero width; use the previous + // character for line measurements. + prev.idx = next.idx + prev.len = next.len + endLine() + continue + } + var k fixed.Int26_6 + if prev.valid && next.f != nil { + k = next.f.Kern(sbuf, ppem, prev.r, next.r) + } + // Break the line if we're out of space. + if prev.idx > 0 && next.x+next.adv+k > maxDotX { + // If the line contains no word breaks, break off the last rune. + if word.idx == 0 { + word = prev + } + next.x -= word.x + word.adv + next.idx -= word.idx + next.len -= word.len + prev = word + endLine() + } else if k != 0 { + glyphs[prev.idx-1].Advance += k + next.x += k + } + g.Advance = next.adv + if unicode.IsSpace(g.Rune) { + word = next + } + prev = next + } + endLine() + return lines, nil +} + +// toLayout converts a slice of glyphs to a text.Layout. +func toLayout(glyphs []glyph) text.Layout { + var buf bytes.Buffer + advs := make([]fixed.Int26_6, len(glyphs)) + for i, g := range glyphs { + buf.WriteRune(g.Rune) + advs[i] = glyphs[i].Advance + } + return text.Layout{Text: buf.String(), Advances: advs} +} + +func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, fonts []*opentype, + str text.Layout) op.CallOp { + var lastPos f32.Point + var builder clip.Path + ops := new(op.Ops) + m := op.Record(ops) + var x fixed.Int26_6 + builder.Begin(ops) + rune := 0 + for _, r := range str.Text { + if !unicode.IsSpace(r) { + f := fontForGlyph(buf, fonts, r) + if f == nil { + continue + } + segs, ok := f.LoadGlyph(buf, ppem, r) + if !ok { + continue + } + // Move to glyph position. + pos := f32.Point{ + X: float32(x) / 64, + } + builder.Move(pos.Sub(lastPos)) + lastPos = pos + var lastArg f32.Point + // Convert sfnt.Segments to relative segments. + for _, fseg := range segs { + nargs := 1 + switch fseg.Op { + case sfnt.SegmentOpQuadTo: + nargs = 2 + case sfnt.SegmentOpCubeTo: + nargs = 3 + } + var args [3]f32.Point + for i := 0; i < nargs; i++ { + a := f32.Point{ + X: float32(fseg.Args[i].X) / 64, + Y: float32(fseg.Args[i].Y) / 64, + } + args[i] = a.Sub(lastArg) + if i == nargs-1 { + lastArg = a + } + } + switch fseg.Op { + case sfnt.SegmentOpMoveTo: + builder.Move(args[0]) + case sfnt.SegmentOpLineTo: + builder.Line(args[0]) + case sfnt.SegmentOpQuadTo: + builder.Quad(args[0], args[1]) + case sfnt.SegmentOpCubeTo: + builder.Cube(args[0], args[1], args[2]) + default: + panic("unsupported segment op") + } + } + lastPos = lastPos.Add(lastArg) + } + x += str.Advances[rune] + rune++ + } + clip.Outline{ + Path: builder.End(), + }.Op().Add(ops) + return m.Stop() +} + +func readGlyphs(r io.Reader) ([]glyph, error) { + var glyphs []glyph + buf := make([]byte, 0, 1024) + for { + n, err := r.Read(buf[len(buf):cap(buf)]) + buf = buf[:len(buf)+n] + lim := len(buf) + // Read full runes if possible. + if err != io.EOF { + lim -= utf8.UTFMax - 1 + } + i := 0 + for i < lim { + c, s := utf8.DecodeRune(buf[i:]) + i += s + glyphs = append(glyphs, glyph{Rune: c}) + } + n = copy(buf, buf[i:]) + buf = buf[:n] + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + } + return glyphs, nil +} + +func (f *opentype) HasGlyph(buf *sfnt.Buffer, r rune) bool { + g, err := f.Font.GlyphIndex(buf, r) + return g != 0 && err == nil +} + +func (f *opentype) GlyphAdvance(buf *sfnt.Buffer, ppem fixed.Int26_6, + r rune) (advance fixed.Int26_6, ok bool) { + g, err := f.Font.GlyphIndex(buf, r) + if err != nil { + return 0, false + } + adv, err := f.Font.GlyphAdvance(buf, g, ppem, f.Hinting) + return adv, err == nil +} + +func (f *opentype) Kern(buf *sfnt.Buffer, ppem fixed.Int26_6, + r0, r1 rune) fixed.Int26_6 { + g0, err := f.Font.GlyphIndex(buf, r0) + if err != nil { + return 0 + } + g1, err := f.Font.GlyphIndex(buf, r1) + if err != nil { + return 0 + } + adv, err := f.Font.Kern(buf, g0, g1, ppem, f.Hinting) + if err != nil { + return 0 + } + return adv +} + +func (f *opentype) Metrics(buf *sfnt.Buffer, ppem fixed.Int26_6) font.Metrics { + m, _ := f.Font.Metrics(buf, ppem, f.Hinting) + return m +} + +func (f *opentype) Bounds(buf *sfnt.Buffer, + ppem fixed.Int26_6) fixed.Rectangle26_6 { + r, _ := f.Font.Bounds(buf, ppem, f.Hinting) + return r +} + +func (f *opentype) LoadGlyph(buf *sfnt.Buffer, ppem fixed.Int26_6, + r rune) ([]sfnt.Segment, bool) { + g, err := f.Font.GlyphIndex(buf, r) + if err != nil { + return nil, false + } + segs, err := f.Font.LoadGlyph(buf, g, ppem, nil) + if err != nil { + return nil, false + } + return segs, true +} diff --git a/gio/giold/font/opentype/opentype_test.go b/gio/giold/font/opentype/opentype_test.go new file mode 100644 index 0000000..d72708e --- /dev/null +++ b/gio/giold/font/opentype/opentype_test.go @@ -0,0 +1,222 @@ +package opentype + +import ( + "bytes" + "compress/gzip" + "encoding/binary" + "fmt" + "io/ioutil" + "os" + "strings" + "testing" + + "golang.org/x/image/font" + "golang.org/x/image/font/gofont/goregular" + "golang.org/x/image/font/sfnt" + "golang.org/x/image/math/fixed" + + "realy.lol/gio/internal/ops" + "realy.lol/gio/op" + "realy.lol/gio/text" +) + +func TestCollectionAsFace(t *testing.T) { + // Load two fonts with disjoint glyphs. Font 1 supports only '1', and font 2 supports only '2'. + // The fonts have different glyphs for the replacement character (".notdef"). + font1, ttf1, err := decompressFontFile("testdata/only1.ttf.gz") + if err != nil { + t.Fatalf("failed to load test font 1: %v", err) + } + font2, ttf2, err := decompressFontFile("testdata/only2.ttf.gz") + if err != nil { + t.Fatalf("failed to load test font 2: %v", err) + } + + otc := mergeFonts(ttf1, ttf2) + coll, err := ParseCollection(otc) + if err != nil { + t.Fatalf("failed to load merged test font: %v", err) + } + + shapeValid1, err := shapeRune(font1, '1') + if err != nil { + t.Fatalf("failed shaping valid glyph with font 1: %v", err) + } + shapeInvalid1, err := shapeRune(font1, '3') + if err != nil { + t.Fatalf("failed shaping invalid glyph with font 1: %v", err) + } + shapeValid2, err := shapeRune(font2, '2') + if err != nil { + t.Fatalf("failed shaping valid glyph with font 2: %v", err) + } + shapeInvalid2, err := shapeRune(font2, + '3') // Same invalid glyph as before to test replacement glyph difference + if err != nil { + t.Fatalf("failed shaping invalid glyph with font 2: %v", err) + } + shapeCollValid1, err := shapeRune(coll, '1') + if err != nil { + t.Fatalf("failed shaping valid glyph for font 1 with font collection: %v", + err) + } + shapeCollValid2, err := shapeRune(coll, '2') + if err != nil { + t.Fatalf("failed shaping valid glyph for font 2 with font collection: %v", + err) + } + shapeCollInvalid, err := shapeRune(coll, + '4') // Different invalid glyph to confirm use of the replacement glyph + if err != nil { + t.Fatalf("failed shaping invalid glyph with font collection: %v", err) + } + + // All shapes from the original fonts should be distinct because the glyphs are distinct, including the replacement + // glyphs. + distinctShapes := []op.CallOp{shapeValid1, shapeInvalid1, shapeValid2, + shapeInvalid2} + for i := 0; i < len(distinctShapes); i++ { + for j := i + 1; j < len(distinctShapes); j++ { + if areShapesEqual(distinctShapes[i], distinctShapes[j]) { + t.Errorf("font shapes %d and %d are not distinct", i, j) + } + } + } + + // Font collections should render glyphs from the first supported font. Replacement glyphs should come from the + // first font in all cases. + if !areShapesEqual(shapeCollValid1, shapeValid1) { + t.Error("font collection did not render the valid glyph using font 1") + } + if !areShapesEqual(shapeCollValid2, shapeValid2) { + t.Error("font collection did not render the valid glyph using font 2") + } + if !areShapesEqual(shapeCollInvalid, shapeInvalid1) { + t.Error("font collection did not render the invalid glyph using the replacement from font 1") + } +} + +func TestEmptyString(t *testing.T) { + face, err := Parse(goregular.TTF) + if err != nil { + t.Fatal(err) + } + + ppem := fixed.I(200) + + lines, err := face.Layout(ppem, 2000, strings.NewReader("")) + if err != nil { + t.Fatal(err) + } + if len(lines) == 0 { + t.Fatalf("Layout returned no lines for empty string; expected 1") + } + l := lines[0] + exp, err := face.font.Bounds(new(sfnt.Buffer), ppem, font.HintingFull) + if err != nil { + t.Fatal(err) + } + if got := l.Bounds; got != exp { + t.Errorf("got bounds %+v for empty string; expected %+v", got, exp) + } +} + +func decompressFontFile(name string) (*Font, []byte, error) { + f, err := os.Open(name) + if err != nil { + return nil, nil, fmt.Errorf("could not open file for reading: %s: %v", + name, err) + } + defer f.Close() + gz, err := gzip.NewReader(f) + if err != nil { + return nil, nil, fmt.Errorf("font file contains invalid gzip data: %v", + err) + } + src, err := ioutil.ReadAll(gz) + if err != nil { + return nil, nil, fmt.Errorf("failed to decompress font file: %v", err) + } + fnt, err := Parse(src) + if err != nil { + return nil, nil, fmt.Errorf("file did not contain a valid font: %v", + err) + } + return fnt, src, nil +} + +// mergeFonts produces a trivial OpenType Collection (OTC) file for two source fonts. +// It makes many assumptions and is not meant for general use. +// For file format details, see https://docs.microsoft.com/en-us/typography/opentype/spec/otff +// For a robust tool to generate these files, see https://pypi.org/project/afdko/ +func mergeFonts(ttf1, ttf2 []byte) []byte { + // Locations to place the two embedded fonts. All of the offsets to the fonts' internal tables will need to be + // shifted from the start of the file by the appropriate amount, and then everything will work as expected. + offset1 := uint32(20) // Length of OpenType collection headers + offset2 := offset1 + uint32(len(ttf1)) + + var buf bytes.Buffer + _, _ = buf.Write([]byte("ttcf\x00\x01\x00\x00\x00\x00\x00\x02")) + _ = binary.Write(&buf, binary.BigEndian, offset1) + _ = binary.Write(&buf, binary.BigEndian, offset2) + + // Inline function to copy a font into the collection verbatim, except for adding an offset to all of the font's + // table positions. + copyOffsetTTF := func(ttf []byte, offset uint32) { + _, _ = buf.Write(ttf[:12]) + numTables := binary.BigEndian.Uint16(ttf[4:6]) + for i := uint16(0); i < numTables; i++ { + p := 12 + 16*i + _, _ = buf.Write(ttf[p : p+8]) + tblLoc := binary.BigEndian.Uint32(ttf[p+8:p+12]) + offset + _ = binary.Write(&buf, binary.BigEndian, tblLoc) + _, _ = buf.Write(ttf[p+12 : p+16]) + } + _, _ = buf.Write(ttf[12+16*numTables:]) + } + copyOffsetTTF(ttf1, offset1) + copyOffsetTTF(ttf2, offset2) + + return buf.Bytes() +} + +// shapeRune uses a given Face to shape exactly one rune at a fixed size, then returns the resulting shape data. +func shapeRune(f text.Face, r rune) (op.CallOp, error) { + ppem := fixed.I(200) + lines, err := f.Layout(ppem, 2000, strings.NewReader(string(r))) + if err != nil { + return op.CallOp{}, err + } + if len(lines) != 1 { + return op.CallOp{}, fmt.Errorf("unexpected rendering for \"U+%08X\": got %d lines (expected: 1)", + r, len(lines)) + } + return f.Shape(ppem, lines[0].Layout), nil +} + +// areShapesEqual returns true iff both given text shapes are produced with identical operations. +func areShapesEqual(shape1, shape2 op.CallOp) bool { + var ops1, ops2 op.Ops + shape1.Add(&ops1) + shape2.Add(&ops2) + var r1, r2 ops.Reader + r1.Reset(&ops1) + r2.Reset(&ops2) + for { + encOp1, ok1 := r1.Decode() + encOp2, ok2 := r2.Decode() + if ok1 != ok2 { + return false + } + if !ok1 { + break + } + if len(encOp1.Refs) > 0 || len(encOp2.Refs) > 0 { + panic("unexpected ops with refs in font shaping test") + } + if !bytes.Equal(encOp1.Data, encOp2.Data) { + return false + } + } + return true +} diff --git a/gio/giold/font/opentype/testdata/only1.ttf.gz b/gio/giold/font/opentype/testdata/only1.ttf.gz new file mode 100644 index 0000000..544159d Binary files /dev/null and b/gio/giold/font/opentype/testdata/only1.ttf.gz differ diff --git a/gio/giold/font/opentype/testdata/only2.ttf.gz b/gio/giold/font/opentype/testdata/only2.ttf.gz new file mode 100644 index 0000000..87a3e68 Binary files /dev/null and b/gio/giold/font/opentype/testdata/only2.ttf.gz differ diff --git a/gio/giold/gesture/gesture.go b/gio/giold/gesture/gesture.go new file mode 100644 index 0000000..bc0324a --- /dev/null +++ b/gio/giold/gesture/gesture.go @@ -0,0 +1,437 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package gesture implements common pointer gestures. + +Gestures accept low level pointer Events from an event +Queue and detect higher level actions such as clicks +and scrolling. +*/ +package gesture + +import ( + "image" + "math" + "runtime" + "time" + + "realy.lol/gio/f32" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/op" + "realy.lol/gio/unit" + + "realy.lol/gio/internal/fling" +) + +// The duration is somewhat arbitrary. +const doubleClickDuration = 300 * time.Millisecond + +// Click detects click gestures in the form +// of ClickEvents. +type Click struct { + // clickedAt is the timestamp at which + // the last click occurred. + clickedAt time.Duration + // clicks is incremented if successive clicks + // are performed within a fixed duration. + clicks int + // pressed tracks whether the pointer is pressed. + pressed bool + // entered tracks whether the pointer is inside the gesture. + entered bool + // pid is the pointer.ID. + pid pointer.ID + Button pointer.Buttons +} + +type ClickState uint8 + +// ClickEvent represent a click action, either a +// TypePress for the beginning of a click or a +// TypeClick for a completed click. +type ClickEvent struct { + Type ClickType + Position f32.Point + Source pointer.Source + Modifiers key.Modifiers + // NumClicks records successive clicks occurring + // within a short duration of each other. + NumClicks int + Button pointer.Buttons +} + +type ClickType uint8 + +// Drag detects drag gestures in the form of pointer.Drag events. +type Drag struct { + dragging bool + pid pointer.ID + start f32.Point + grab bool +} + +// Scroll detects scroll gestures and reduces them to +// scroll distances. Scroll recognizes mouse wheel +// movements as well as drag and fling touch gestures. +type Scroll struct { + dragging bool + axis Axis + estimator fling.Extrapolation + flinger fling.Animation + pid pointer.ID + grab bool + last int + // Leftover scroll. + scroll float32 +} + +type ScrollState uint8 + +type Axis uint8 + +const ( + Horizontal Axis = iota + Vertical + Both +) + +const ( + // TypePress is reported for the first pointer + // press. + TypePress ClickType = iota + // TypeClick is reported when a click action + // is complete. + TypeClick + // TypeCancel is reported when the gesture is + // cancelled. + TypeCancel +) + +const ( + // StateIdle is the default scroll state. + StateIdle ScrollState = iota + // StateDrag is reported during drag gestures. + StateDragging + // StateFlinging is reported when a fling is + // in progress. + StateFlinging +) + +var touchSlop = unit.Dp(3) + +// Add the handler to the operation list to receive click events. +func (c *Click) Add(ops *op.Ops) { + op := pointer.InputOp{ + Tag: c, + Types: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave, + } + op.Add(ops) +} + +// Hovered returns whether a pointer is inside the area. +func (c *Click) Hovered() bool { + return c.entered +} + +// Pressed returns whether a pointer is pressing. +func (c *Click) Pressed() bool { + return c.pressed +} + +// Events returns the next click event, if any. +func (c *Click) Events(q event.Queue) []ClickEvent { + var events []ClickEvent + for _, evt := range q.Events(c) { + // I.S(evt) + e, ok := evt.(pointer.Event) + if !ok { + continue + } + switch e.Type { + case pointer.Release: + if !c.pressed || c.pid != e.PointerID { + break + } + c.pressed = false + if c.entered { + if e.Time-c.clickedAt < doubleClickDuration || + (c.clicks == 2 && e.Time-c.clickedAt < doubleClickDuration*2) { + c.clicks++ + } else { + c.clicks = 1 + } + c.clickedAt = e.Time + events = append(events, ClickEvent{ + Type: TypeClick, Position: e.Position, Source: e.Source, + Modifiers: e.Modifiers, + Button: e.Buttons, NumClicks: c.clicks, + }) + } else { + events = append(events, ClickEvent{Type: TypeCancel}) + } + case pointer.Cancel: + wasPressed := c.pressed + c.pressed = false + c.entered = false + if wasPressed { + events = append(events, ClickEvent{Type: TypeCancel}) + } + case pointer.Press: + if c.pressed { + break + } + // if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonPrimary { + // break + // } + if !c.entered { + c.pid = e.PointerID + } + if c.pid != e.PointerID { + break + } + c.pressed = true + events = append(events, ClickEvent{ + Type: TypePress, Position: e.Position, Source: e.Source, + Modifiers: e.Modifiers, Button: e.Buttons, + }) + case pointer.Leave: + if !c.pressed { + c.pid = e.PointerID + } + if c.pid == e.PointerID { + c.entered = false + } + case pointer.Enter: + if !c.pressed { + c.pid = e.PointerID + } + if c.pid == e.PointerID { + c.entered = true + } + } + } + return events +} + +func (ClickEvent) ImplementsEvent() {} + +// Add the handler to the operation list to receive scroll events. +func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) { + oph := pointer.InputOp{ + Tag: s, + Grab: s.grab, + Types: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll, + ScrollBounds: bounds, + } + oph.Add(ops) + if s.flinger.Active() { + op.InvalidateOp{}.Add(ops) + } +} + +// Stop any remaining fling movement. +func (s *Scroll) Stop() { + s.flinger = fling.Animation{} +} + +// Scroll detects the scrolling distance from the available events and +// ongoing fling gestures. +func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, + axis Axis) int { + if s.axis != axis { + s.axis = axis + return 0 + } + total := 0 + for _, evt := range q.Events(s) { + e, ok := evt.(pointer.Event) + if !ok { + continue + } + switch e.Type { + case pointer.Press: + if s.dragging { + break + } + // Only scroll on touch drags, or on Android where mice + // drags also scroll by convention. + if e.Source != pointer.Touch && runtime.GOOS != "android" { + break + } + s.Stop() + s.estimator = fling.Extrapolation{} + v := s.val(e.Position) + s.last = int(math.Round(float64(v))) + s.estimator.Sample(e.Time, v) + s.dragging = true + s.pid = e.PointerID + case pointer.Release: + if s.pid != e.PointerID { + break + } + fling := s.estimator.Estimate() + if slop, d := float32(cfg.Px(touchSlop)), fling.Distance; d < -slop || d > slop { + s.flinger.Start(cfg, t, fling.Velocity) + } + fallthrough + case pointer.Cancel: + s.dragging = false + s.grab = false + case pointer.Scroll: + switch s.axis { + case Horizontal: + s.scroll += e.Scroll.X + case Vertical: + s.scroll += e.Scroll.Y + } + iscroll := int(s.scroll) + s.scroll -= float32(iscroll) + total += iscroll + case pointer.Drag: + if !s.dragging || s.pid != e.PointerID { + continue + } + val := s.val(e.Position) + s.estimator.Sample(e.Time, val) + v := int(math.Round(float64(val))) + dist := s.last - v + if e.Priority < pointer.Grabbed { + slop := cfg.Px(touchSlop) + if dist := dist; dist >= slop || -slop >= dist { + s.grab = true + } + } else { + s.last = v + total += dist + } + } + } + total += s.flinger.Tick(t) + return total +} + +func (s *Scroll) val(p f32.Point) float32 { + if s.axis == Horizontal { + return p.X + } else { + return p.Y + } +} + +// State reports the scroll state. +func (s *Scroll) State() ScrollState { + switch { + case s.flinger.Active(): + return StateFlinging + case s.dragging: + return StateDragging + default: + return StateIdle + } +} + +// Add the handler to the operation list to receive drag events. +func (d *Drag) Add(ops *op.Ops) { + op := pointer.InputOp{ + Tag: d, + Grab: d.grab, + Types: pointer.Press | pointer.Drag | pointer.Release, + } + op.Add(ops) +} + +// Events returns the next drag events, if any. +func (d *Drag) Events(cfg unit.Metric, q event.Queue, + axis Axis) []pointer.Event { + var events []pointer.Event + for _, e := range q.Events(d) { + e, ok := e.(pointer.Event) + if !ok { + continue + } + + switch e.Type { + case pointer.Press: + if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) { + continue + } + if d.dragging { + continue + } + d.dragging = true + d.pid = e.PointerID + d.start = e.Position + case pointer.Drag: + if !d.dragging || e.PointerID != d.pid { + continue + } + switch axis { + case Horizontal: + e.Position.Y = d.start.Y + case Vertical: + e.Position.X = d.start.X + case Both: + // Do nothing + } + if e.Priority < pointer.Grabbed { + diff := e.Position.Sub(d.start) + slop := cfg.Px(touchSlop) + if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) { + d.grab = true + } + } + case pointer.Release, pointer.Cancel: + if !d.dragging || e.PointerID != d.pid { + continue + } + d.dragging = false + d.grab = false + } + + events = append(events, e) + } + + return events +} + +// Dragging reports whether it's currently in use. +func (d *Drag) Dragging() bool { return d.dragging } + +func (a Axis) String() string { + switch a { + case Horizontal: + return "Horizontal" + case Vertical: + return "Vertical" + default: + panic("invalid Axis") + } +} + +func (ct ClickType) String() string { + switch ct { + case TypePress: + return "TypePress" + case TypeClick: + return "TypeClick" + case TypeCancel: + return "TypeCancel" + default: + panic("invalid ClickType") + } +} + +func (s ScrollState) String() string { + switch s { + case StateIdle: + return "StateIdle" + case StateDragging: + return "StateDragging" + case StateFlinging: + return "StateFlinging" + default: + panic("unreachable") + } +} diff --git a/gio/giold/gesture/gesture_test.go b/gio/giold/gesture/gesture_test.go new file mode 100644 index 0000000..d2f69ea --- /dev/null +++ b/gio/giold/gesture/gesture_test.go @@ -0,0 +1,88 @@ +package gesture + +import ( + "testing" + "time" + + "realy.lol/gio/io/event" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/router" + "realy.lol/gio/op" +) + +func TestMouseClicks(t *testing.T) { + for _, tc := range []struct { + label string + events []event.Event + clicks []int // number of combined clicks per click (single, double...) + }{ + { + label: "single click", + events: mouseClickEvents(200 * time.Millisecond), + clicks: []int{1}, + }, + { + label: "double click", + events: mouseClickEvents( + 100*time.Millisecond, + 100*time.Millisecond+doubleClickDuration-1), + clicks: []int{1, 2}, + }, + { + label: "two single clicks", + events: mouseClickEvents( + 100*time.Millisecond, + 100*time.Millisecond+doubleClickDuration+1), + clicks: []int{1, 1}, + }, + } { + t.Run(tc.label, func(t *testing.T) { + var click Click + var ops op.Ops + click.Add(&ops) + + var r router.Router + r.Frame(&ops) + r.Queue(tc.events...) + + events := click.Events(&r) + clicks := filterMouseClicks(events) + if got, want := len(clicks), len(tc.clicks); got != want { + t.Fatalf("got %d mouse clicks, expected %d", got, want) + } + + for i, click := range clicks { + if got, want := click.NumClicks, tc.clicks[i]; got != want { + t.Errorf("got %d combined mouse clicks, expected %d", got, + want) + } + } + }) + } +} + +func mouseClickEvents(times ...time.Duration) []event.Event { + press := pointer.Event{ + Type: pointer.Press, + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + } + events := make([]event.Event, 0, 2*len(times)) + for _, t := range times { + release := press + release.Type = pointer.Release + release.Time = t + events = append(events, press, release) + } + return events +} + +func filterMouseClicks(events []ClickEvent) []ClickEvent { + var clicks []ClickEvent + for _, ev := range events { + if ev.Type == TypeClick { + clicks = append(clicks, ev) + } + } + return clicks +} diff --git a/gio/giold/gesture/log.go b/gio/giold/gesture/log.go new file mode 100644 index 0000000..9e79319 --- /dev/null +++ b/gio/giold/gesture/log.go @@ -0,0 +1,9 @@ +package gesture + +// import ( +// "github.com/p9c/log" +// +// "github.com/p9c/gel/version" +// ) +// +// var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/gio/giold/gpu/api.go b/gio/giold/gpu/api.go new file mode 100644 index 0000000..1a87684 --- /dev/null +++ b/gio/giold/gpu/api.go @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import "realy.lol/gio/gpu/internal/driver" + +// An API carries the necessary GPU API specific resources to create a Device. +// There is an API type for each supported GPU API such as OpenGL and Direct3D. +type API = driver.API + +// OpenGL denotes the OpenGL or OpenGL ES API. +type OpenGL = driver.OpenGL + +// Direct3D11 denotes the Direct3D API. +type Direct3D11 = driver.Direct3D11 diff --git a/gio/giold/gpu/caches.go b/gio/giold/gpu/caches.go new file mode 100644 index 0000000..3dd93cf --- /dev/null +++ b/gio/giold/gpu/caches.go @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "fmt" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/ops" +) + +type resourceCache struct { + res map[interface{}]resource + newRes map[interface{}]resource +} + +// opCache is like a resourceCache but using concrete types and a +// freelist instead of two maps to avoid runtime.mapaccess2 calls +// since benchmarking showed them as a bottleneck. +type opCache struct { + // store the index + 1 in cache this key is stored in + index map[ops.Key]int + // list of indexes in cache that are free and can be used + freelist []int + cache []opCacheValue +} + +type opCacheValue struct { + data pathData + // computePath is the encoded path for compute. + computePath encoder + + bounds f32.Rectangle + // the fields below are handled by opCache + key ops.Key + keep bool +} + +func newResourceCache() *resourceCache { + return &resourceCache{ + res: make(map[interface{}]resource), + newRes: make(map[interface{}]resource), + } +} + +func (r *resourceCache) get(key interface{}) (resource, bool) { + v, exists := r.res[key] + if exists { + r.newRes[key] = v + } + return v, exists +} + +func (r *resourceCache) put(key interface{}, val resource) { + if _, exists := r.newRes[key]; exists { + panic(fmt.Errorf("key exists, %p", key)) + } + r.res[key] = val + r.newRes[key] = val +} + +func (r *resourceCache) frame() { + for k, v := range r.res { + if _, exists := r.newRes[k]; !exists { + delete(r.res, k) + v.release() + } + } + for k, v := range r.newRes { + delete(r.newRes, k) + r.res[k] = v + } +} + +func (r *resourceCache) release() { + for _, v := range r.newRes { + v.release() + } + r.newRes = nil + r.res = nil +} + +func newOpCache() *opCache { + return &opCache{ + index: make(map[ops.Key]int), + freelist: make([]int, 0), + cache: make([]opCacheValue, 0), + } +} + +func (r *opCache) get(key ops.Key) (o opCacheValue, exist bool) { + v := r.index[key] + if v == 0 { + return + } + r.cache[v-1].keep = true + return r.cache[v-1], true +} + +func (r *opCache) put(key ops.Key, val opCacheValue) { + v := r.index[key] + val.keep = true + val.key = key + if v == 0 { + // not in cache + i := len(r.cache) + if len(r.freelist) > 0 { + i = r.freelist[len(r.freelist)-1] + r.freelist = r.freelist[:len(r.freelist)-1] + r.cache[i] = val + } else { + r.cache = append(r.cache, val) + } + r.index[key] = i + 1 + } else { + r.cache[v-1] = val + } +} + +func (r *opCache) frame() { + r.freelist = r.freelist[:0] + for i, v := range r.cache { + r.cache[i].keep = false + if v.keep { + continue + } + if v.data.data != nil { + v.data.release() + r.cache[i].data.data = nil + } + delete(r.index, v.key) + r.freelist = append(r.freelist, i) + } +} + +func (r *opCache) release() { + for i := range r.cache { + r.cache[i].keep = false + } + r.frame() + r.index = nil + r.freelist = nil + r.cache = nil +} diff --git a/gio/giold/gpu/clip.go b/gio/giold/gpu/clip.go new file mode 100644 index 0000000..7e24449 --- /dev/null +++ b/gio/giold/gpu/clip.go @@ -0,0 +1,98 @@ +package gpu + +import ( + "realy.lol/gio/f32" + "realy.lol/gio/internal/stroke" +) + +type quadSplitter struct { + bounds f32.Rectangle + contour uint32 + d *drawOps +} + +func encodeQuadTo(data []byte, meta uint32, from, ctrl, to f32.Point) { + // NW. + encodeVertex(data, meta, -1, 1, from, ctrl, to) + // NE. + encodeVertex(data[vertStride:], meta, 1, 1, from, ctrl, to) + // SW. + encodeVertex(data[vertStride*2:], meta, -1, -1, from, ctrl, to) + // SE. + encodeVertex(data[vertStride*3:], meta, 1, -1, from, ctrl, to) +} + +func encodeVertex(data []byte, meta uint32, cornerx, cornery int16, + from, ctrl, to f32.Point) { + var corner float32 + if cornerx == 1 { + corner += .5 + } + if cornery == 1 { + corner += .25 + } + v := vertex{ + Corner: corner, + FromX: from.X, + FromY: from.Y, + CtrlX: ctrl.X, + CtrlY: ctrl.Y, + ToX: to.X, + ToY: to.Y, + } + v.encode(data, meta) +} + +func (qs *quadSplitter) encodeQuadTo(from, ctrl, to f32.Point) { + data := qs.d.writeVertCache(vertStride * 4) + encodeQuadTo(data, qs.contour, from, ctrl, to) +} + +func (qs *quadSplitter) splitAndEncode(quad stroke.QuadSegment) { + cbnd := f32.Rectangle{ + Min: quad.From, + Max: quad.To, + }.Canon() + from, ctrl, to := quad.From, quad.Ctrl, quad.To + + // If the curve contain areas where a vertical line + // intersects it twice, split the curve in two x monotone + // lower and upper curves. The stencil fragment program + // expects only one intersection per curve. + + // Find the t where the derivative in x is 0. + v0 := ctrl.Sub(from) + v1 := to.Sub(ctrl) + d := v0.X - v1.X + // t = v0 / d. Split if t is in ]0;1[. + if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X { + t := v0.X / d + ctrl0 := from.Mul(1 - t).Add(ctrl.Mul(t)) + ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t)) + mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t)) + qs.encodeQuadTo(from, ctrl0, mid) + qs.encodeQuadTo(mid, ctrl1, to) + if mid.X > cbnd.Max.X { + cbnd.Max.X = mid.X + } + if mid.X < cbnd.Min.X { + cbnd.Min.X = mid.X + } + } else { + qs.encodeQuadTo(from, ctrl, to) + } + // Find the y extremum, if any. + d = v0.Y - v1.Y + if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y { + t := v0.Y / d + y := (1-t)*(1-t)*from.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y + if y > cbnd.Max.Y { + cbnd.Max.Y = y + } + if y < cbnd.Min.Y { + cbnd.Min.Y = y + } + } + + qs.bounds = qs.bounds.Union(cbnd) +} diff --git a/gio/giold/gpu/compute.go b/gio/giold/gpu/compute.go new file mode 100644 index 0000000..e7c7fd6 --- /dev/null +++ b/gio/giold/gpu/compute.go @@ -0,0 +1,1093 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "encoding/binary" + "errors" + "fmt" + "image" + "image/color" + "math/bits" + "time" + "unsafe" + + "realy.lol/gio/f32" + "realy.lol/gio/gpu/internal/driver" + "realy.lol/gio/internal/byteslice" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/internal/ops" + "realy.lol/gio/internal/scene" + "realy.lol/gio/layout" + "realy.lol/gio/op" +) + +type compute struct { + ctx driver.Device + enc encoder + + drawOps drawOps + texOps []textureOp + cache *resourceCache + maxTextureDim int + + programs struct { + elements driver.Program + tileAlloc driver.Program + pathCoarse driver.Program + backdrop driver.Program + binning driver.Program + coarse driver.Program + kernel4 driver.Program + } + buffers struct { + config driver.Buffer + scene sizedBuffer + state sizedBuffer + memory sizedBuffer + } + output struct { + size image.Point + // image is the output texture. Note that it is in RGBA format, + // but contains data in sRGB. See blitOutput for more detail. + image driver.Texture + blitProg driver.Program + } + // images contains ImageOp images packed into a texture atlas. + images struct { + packer packer + // positions maps imageOpData.handles to positions inside tex. + positions map[interface{}]image.Point + tex driver.Texture + } + // materials contains the pre-processed materials (transformed images for + // now, gradients etc. later) packed in a texture atlas. The atlas is used + // as source in kernel4. + materials struct { + // offsets maps texture ops to the offsets to put in their FillImage commands. + offsets map[textureKey]image.Point + + prog driver.Program + layout driver.InputLayout + + packer packer + + tex driver.Texture + fbo driver.Framebuffer + quads []materialVertex + + bufSize int + buffer driver.Buffer + } + timers struct { + profile string + t *timers + elements *timer + tileAlloc *timer + pathCoarse *timer + backdropBinning *timer + coarse *timer + kernel4 *timer + } + + // The following fields hold scratch space to avoid garbage. + zeroSlice []byte + memHeader *memoryHeader + conf *config +} + +// materialVertex describes a vertex of a quad used to render a transformed +// material. +type materialVertex struct { + posX, posY float32 + u, v float32 +} + +// textureKey identifies textureOp. +type textureKey struct { + handle interface{} + transform f32.Affine2D +} + +// textureOp represents an imageOp that requires texture space. +type textureOp struct { + // sceneIdx is the index in the scene that contains the fill image command + // that corresponds to the operation. + sceneIdx int + key textureKey + img imageOpData + + // pos is the position of the untransformed image in the images texture. + pos image.Point +} + +type encoder struct { + scene []scene.Command + npath int + npathseg int + ntrans int +} + +type encodeState struct { + trans f32.Affine2D + clip f32.Rectangle +} + +type sizedBuffer struct { + size int + buffer driver.Buffer +} + +// config matches Config in setup.h +type config struct { + n_elements uint32 // paths + n_pathseg uint32 + width_in_tiles uint32 + height_in_tiles uint32 + tile_alloc memAlloc + bin_alloc memAlloc + ptcl_alloc memAlloc + pathseg_alloc memAlloc + anno_alloc memAlloc + trans_alloc memAlloc +} + +// memAlloc matches Alloc in mem.h +type memAlloc struct { + offset uint32 + // size uint32 +} + +// memoryHeader matches the header of Memory in mem.h. +type memoryHeader struct { + mem_offset uint32 + mem_error uint32 +} + +// GPU structure sizes and constants. +const ( + tileWidthPx = 32 + tileHeightPx = 32 + ptclInitialAlloc = 1024 + kernel4OutputUnit = 2 + kernel4AtlasUnit = 3 + + pathSize = 12 + binSize = 8 + pathsegSize = 52 + annoSize = 32 + transSize = 24 + stateSize = 60 + stateStride = 4 + 2*stateSize +) + +// mem.h constants. +const ( + memNoError = 0 // NO_ERROR + memMallocFailed = 1 // ERR_MALLOC_FAILED +) + +func newCompute(ctx driver.Device) (*compute, error) { + maxDim := ctx.Caps().MaxTextureSize + // Large atlas textures cause artifacts due to precision loss in + // shaders. + if cap := 8192; maxDim > cap { + maxDim = cap + } + g := &compute{ + ctx: ctx, + cache: newResourceCache(), + maxTextureDim: maxDim, + conf: new(config), + memHeader: new(memoryHeader), + } + + blitProg, err := ctx.NewProgram(shader_copy_vert, shader_copy_frag) + if err != nil { + g.Release() + return nil, err + } + g.output.blitProg = blitProg + + materialProg, err := ctx.NewProgram(shader_material_vert, + shader_material_frag) + if err != nil { + g.Release() + return nil, err + } + g.materials.prog = materialProg + progLayout, err := ctx.NewInputLayout(shader_material_vert, + []driver.InputDesc{ + {Type: driver.DataTypeFloat, Size: 2, Offset: 0}, + {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2}, + }) + if err != nil { + g.Release() + return nil, err + } + g.materials.layout = progLayout + + g.drawOps.pathCache = newOpCache() + g.drawOps.compute = true + + buf, err := ctx.NewBuffer(driver.BufferBindingShaderStorage, + int(unsafe.Sizeof(config{}))) + if err != nil { + g.Release() + return nil, err + } + g.buffers.config = buf + + shaders := []struct { + prog *driver.Program + src driver.ShaderSources + }{ + {&g.programs.elements, shader_elements_comp}, + {&g.programs.tileAlloc, shader_tile_alloc_comp}, + {&g.programs.pathCoarse, shader_path_coarse_comp}, + {&g.programs.backdrop, shader_backdrop_comp}, + {&g.programs.binning, shader_binning_comp}, + {&g.programs.coarse, shader_coarse_comp}, + {&g.programs.kernel4, shader_kernel4_comp}, + } + for _, shader := range shaders { + p, err := ctx.NewComputeProgram(shader.src) + if err != nil { + g.Release() + return nil, err + } + *shader.prog = p + } + return g, nil +} + +func (g *compute) Collect(viewport image.Point, ops *op.Ops) { + g.drawOps.reset(g.cache, viewport) + g.drawOps.collect(g.ctx, g.cache, ops, viewport) + for _, img := range g.drawOps.allImageOps { + expandPathOp(img.path, img.clip) + } + if g.drawOps.profile && g.timers.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) { + t := &g.timers + t.t = newTimers(g.ctx) + t.elements = g.timers.t.newTimer() + t.tileAlloc = g.timers.t.newTimer() + t.pathCoarse = g.timers.t.newTimer() + t.backdropBinning = g.timers.t.newTimer() + t.coarse = g.timers.t.newTimer() + t.kernel4 = g.timers.t.newTimer() + } +} + +func (g *compute) Clear(col color.NRGBA) { + g.drawOps.clear = true + g.drawOps.clearColor = f32color.LinearFromSRGB(col) +} + +func (g *compute) Frame() error { + viewport := g.drawOps.viewport + tileDims := image.Point{ + X: (viewport.X + tileWidthPx - 1) / tileWidthPx, + Y: (viewport.Y + tileHeightPx - 1) / tileHeightPx, + } + + defFBO := g.ctx.BeginFrame() + defer g.ctx.EndFrame() + + if err := g.encode(viewport); err != nil { + return err + } + if err := g.uploadImages(); err != nil { + return err + } + if err := g.renderMaterials(); err != nil { + return err + } + if err := g.render(tileDims); err != nil { + return err + } + g.ctx.BindFramebuffer(defFBO) + g.blitOutput(viewport) + g.cache.frame() + g.drawOps.pathCache.frame() + t := &g.timers + if g.drawOps.profile && t.t.ready() { + et, tat, pct, bbt := t.elements.Elapsed, t.tileAlloc.Elapsed, t.pathCoarse.Elapsed, t.backdropBinning.Elapsed + ct, k4t := t.coarse.Elapsed, t.kernel4.Elapsed + ft := et + tat + pct + bbt + ct + k4t + q := 100 * time.Microsecond + ft = ft.Round(q) + et, tat, pct, bbt = et.Round(q), tat.Round(q), pct.Round(q), bbt.Round(q) + ct, k4t = ct.Round(q), k4t.Round(q) + t.profile = fmt.Sprintf("ft:%7s et:%7s tat:%7s pct:%7s bbt:%7s ct:%7s k4t:%7s", + ft, et, tat, pct, bbt, ct, k4t) + } + g.drawOps.clear = false + return nil +} + +func (g *compute) Profile() string { + return g.timers.profile +} + +// blitOutput copies the compute render output to the output FBO. We need to +// copy because compute shaders can only write to textures, not FBOs. Compute +// shader can only write to RGBA textures, but since we actually render in sRGB +// format we can't use glBlitFramebuffer, because it does sRGB conversion. +func (g *compute) blitOutput(viewport image.Point) { + if !g.drawOps.clear { + g.ctx.BlendFunc(driver.BlendFactorOne, + driver.BlendFactorOneMinusSrcAlpha) + g.ctx.SetBlend(true) + defer g.ctx.SetBlend(false) + } + g.ctx.Viewport(0, 0, viewport.X, viewport.Y) + g.ctx.BindTexture(0, g.output.image) + g.ctx.BindProgram(g.output.blitProg) + g.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4) +} + +func (g *compute) encode(viewport image.Point) error { + g.texOps = g.texOps[:0] + g.enc.reset() + + // Flip Y-axis. + flipY := f32.Affine2D{}.Scale(f32.Pt(0, 0), f32.Pt(1, -1)).Offset(f32.Pt(0, + float32(viewport.Y))) + g.enc.transform(flipY) + if g.drawOps.clear { + g.enc.rect(f32.Rectangle{Max: layout.FPt(viewport)}) + g.enc.fillColor(f32color.NRGBAToRGBA(g.drawOps.clearColor.SRGB())) + } + return g.encodeOps(flipY, viewport, g.drawOps.allImageOps) +} + +func (g *compute) renderMaterials() error { + m := &g.materials + m.quads = m.quads[:0] + resize := false + reclaimed := false +restart: + for { + for _, op := range g.texOps { + if off, exists := m.offsets[op.key]; exists { + g.enc.setFillImageOffset(op.sceneIdx, off) + continue + } + quad, bounds := g.materialQuad(op.key.transform, op.img, op.pos) + + // A material is clipped to avoid drawing outside its bounds inside the atlas. However, + // imprecision in the clipping may cause a single pixel overflow. Be safe. + size := bounds.Size().Add(image.Pt(1, 1)) + place, fits := m.packer.tryAdd(size) + if !fits { + m.offsets = nil + m.quads = m.quads[:0] + m.packer.clear() + if !reclaimed { + // Some images may no longer be in use, try again + // after clearing existing maps. + reclaimed = true + } else { + m.packer.maxDim += 256 + resize = true + if m.packer.maxDim > g.maxTextureDim { + return errors.New("compute: no space left in material atlas") + } + } + m.packer.newPage() + continue restart + } + // Position quad to match place. + offset := place.Pos.Sub(bounds.Min) + offsetf := layout.FPt(offset) + for i := range quad { + quad[i].posX += offsetf.X + quad[i].posY += offsetf.Y + } + // Draw quad as two triangles. + m.quads = append(m.quads, quad[0], quad[1], quad[3], quad[3], + quad[1], quad[2]) + if m.offsets == nil { + m.offsets = make(map[textureKey]image.Point) + } + m.offsets[op.key] = offset + g.enc.setFillImageOffset(op.sceneIdx, offset) + } + break + } + if len(m.quads) == 0 { + return nil + } + texSize := m.packer.maxDim + if resize { + if m.fbo != nil { + m.fbo.Release() + m.fbo = nil + } + if m.tex != nil { + m.tex.Release() + m.tex = nil + } + handle, err := g.ctx.NewTexture(driver.TextureFormatRGBA8, texSize, + texSize, + driver.FilterNearest, driver.FilterNearest, + driver.BufferBindingShaderStorage|driver.BufferBindingFramebuffer) + if err != nil { + return fmt.Errorf("compute: failed to create material atlas: %v", + err) + } + m.tex = handle + fbo, err := g.ctx.NewFramebuffer(handle, 0) + if err != nil { + return fmt.Errorf("compute: failed to create material framebuffer: %v", + err) + } + m.fbo = fbo + } + // TODO: move to shaders. + // Transform to clip space: [-1, -1] - [1, 1]. + clip := f32.Affine2D{}.Scale(f32.Pt(0, 0), + f32.Pt(2/float32(texSize), 2/float32(texSize))).Offset(f32.Pt(-1, -1)) + for i, v := range m.quads { + p := clip.Transform(f32.Pt(v.posX, v.posY)) + m.quads[i].posX = p.X + m.quads[i].posY = p.Y + } + vertexData := byteslice.Slice(m.quads) + if len(vertexData) > m.bufSize { + if m.buffer != nil { + m.buffer.Release() + m.buffer = nil + } + n := pow2Ceil(len(vertexData)) + buf, err := g.ctx.NewBuffer(driver.BufferBindingVertices, n) + if err != nil { + return err + } + m.bufSize = n + m.buffer = buf + } + m.buffer.Upload(vertexData) + g.ctx.BindTexture(0, g.images.tex) + g.ctx.BindFramebuffer(m.fbo) + g.ctx.Viewport(0, 0, texSize, texSize) + if reclaimed { + g.ctx.Clear(0, 0, 0, 0) + } + g.ctx.BindProgram(m.prog) + g.ctx.BindVertexBuffer(m.buffer, int(unsafe.Sizeof(m.quads[0])), 0) + g.ctx.BindInputLayout(m.layout) + g.ctx.DrawArrays(driver.DrawModeTriangles, 0, len(m.quads)) + return nil +} + +func (g *compute) uploadImages() error { + // padding is the number of pixels added to the right and below + // images, to avoid atlas filtering artifacts. + const padding = 1 + + a := &g.images + var uploads map[interface{}]*image.RGBA + resize := false + reclaimed := false +restart: + for { + for i, op := range g.texOps { + if pos, exists := a.positions[op.img.handle]; exists { + g.texOps[i].pos = pos + continue + } + size := op.img.src.Bounds().Size().Add(image.Pt(padding, padding)) + place, fits := a.packer.tryAdd(size) + if !fits { + a.positions = nil + uploads = nil + a.packer.clear() + if !reclaimed { + // Some images may no longer be in use, try again + // after clearing existing maps. + reclaimed = true + } else { + a.packer.maxDim += 256 + resize = true + if a.packer.maxDim > g.maxTextureDim { + return errors.New("compute: no space left in image atlas") + } + } + a.packer.newPage() + continue restart + } + if a.positions == nil { + a.positions = make(map[interface{}]image.Point) + } + a.positions[op.img.handle] = place.Pos + g.texOps[i].pos = place.Pos + if uploads == nil { + uploads = make(map[interface{}]*image.RGBA) + } + uploads[op.img.handle] = op.img.src + } + break + } + if len(uploads) == 0 { + return nil + } + if resize { + if a.tex != nil { + a.tex.Release() + a.tex = nil + } + sz := a.packer.maxDim + handle, err := g.ctx.NewTexture(driver.TextureFormatSRGB, sz, sz, + driver.FilterLinear, driver.FilterLinear, + driver.BufferBindingTexture) + if err != nil { + return fmt.Errorf("compute: failed to create image atlas: %v", err) + } + a.tex = handle + } + for h, img := range uploads { + pos, ok := a.positions[h] + if !ok { + panic("compute: internal error: image not placed") + } + size := img.Bounds().Size() + driver.UploadImage(a.tex, pos, img) + rightPadding := image.Pt(padding, size.Y) + a.tex.Upload(image.Pt(pos.X+size.X, pos.Y), rightPadding, + g.zeros(rightPadding.X*rightPadding.Y*4)) + bottomPadding := image.Pt(size.X, padding) + a.tex.Upload(image.Pt(pos.X, pos.Y+size.Y), bottomPadding, + g.zeros(bottomPadding.X*bottomPadding.Y*4)) + } + return nil +} + +func pow2Ceil(v int) int { + exp := bits.Len(uint(v)) + if bits.OnesCount(uint(v)) == 1 { + exp-- + } + return 1 << exp +} + +// materialQuad constructs a quad that represents the transformed image. It returns the quad +// and its bounds. +func (g *compute) materialQuad(M f32.Affine2D, img imageOpData, + uvPos image.Point) ([4]materialVertex, image.Rectangle) { + imgSize := layout.FPt(img.src.Bounds().Size()) + sx, hx, ox, hy, sy, oy := M.Elems() + transOff := f32.Pt(ox, oy) + // The 4 corners of the image rectangle transformed by M, excluding its offset, are: + // + // q0: M * (0, 0) q3: M * (w, 0) + // q1: M * (0, h) q2: M * (w, h) + // + // Note that q0 = M*0 = 0, q2 = q1 + q3. + q0 := f32.Pt(0, 0) + q1 := f32.Pt(hx*imgSize.Y, sy*imgSize.Y) + q3 := f32.Pt(sx*imgSize.X, hy*imgSize.X) + q2 := q1.Add(q3) + q0 = q0.Add(transOff) + q1 = q1.Add(transOff) + q2 = q2.Add(transOff) + q3 = q3.Add(transOff) + + boundsf := f32.Rectangle{ + Min: min(min(q0, q1), min(q2, q3)), + Max: max(max(q0, q1), max(q2, q3)), + } + + bounds := boundRectF(boundsf) + uvPosf := layout.FPt(uvPos) + atlasScale := 1 / float32(g.images.packer.maxDim) + uvBounds := f32.Rectangle{ + Min: uvPosf.Mul(atlasScale), + Max: uvPosf.Add(imgSize).Mul(atlasScale), + } + quad := [4]materialVertex{ + {posX: q0.X, posY: q0.Y, u: uvBounds.Min.X, v: uvBounds.Min.Y}, + {posX: q1.X, posY: q1.Y, u: uvBounds.Min.X, v: uvBounds.Max.Y}, + {posX: q2.X, posY: q2.Y, u: uvBounds.Max.X, v: uvBounds.Max.Y}, + {posX: q3.X, posY: q3.Y, u: uvBounds.Max.X, v: uvBounds.Min.Y}, + } + return quad, bounds +} + +func max(p1, p2 f32.Point) f32.Point { + p := p1 + if p2.X > p.X { + p.X = p2.X + } + if p2.Y > p.Y { + p.Y = p2.Y + } + return p +} + +func min(p1, p2 f32.Point) f32.Point { + p := p1 + if p2.X < p.X { + p.X = p2.X + } + if p2.Y < p.Y { + p.Y = p2.Y + } + return p +} + +func (g *compute) encodeOps(trans f32.Affine2D, viewport image.Point, + ops []imageOp) error { + for _, op := range ops { + bounds := layout.FRect(op.clip) + // clip is the union of all drawing affected by the clipping + // operation. TODO: tighten. + clip := f32.Rect(0, 0, float32(viewport.X), float32(viewport.Y)) + nclips := g.encodeClipStack(clip, bounds, op.path, false) + m := op.material + switch m.material { + case materialTexture: + t := trans.Mul(m.trans) + g.texOps = append(g.texOps, textureOp{ + sceneIdx: len(g.enc.scene), + img: m.data, + key: textureKey{ + transform: t, + handle: m.data.handle, + }, + }) + // Add fill command, its offset is resolved and filled in renderMaterials. + g.enc.fillImage(0) + case materialColor: + g.enc.fillColor(f32color.NRGBAToRGBA(op.material.color.SRGB())) + case materialLinearGradient: + // TODO: implement. + g.enc.fillColor(f32color.NRGBAToRGBA(op.material.color1.SRGB())) + default: + panic("not implemented") + } + if op.path != nil && op.path.path { + g.enc.fillMode(scene.FillModeNonzero) + g.enc.transform(op.path.trans.Invert()) + } + // Pop the clip stack. + for i := 0; i < nclips; i++ { + g.enc.endClip(clip) + } + } + return nil +} + +// encodeClips encodes a stack of clip paths and return the stack depth. +func (g *compute) encodeClipStack(clip, bounds f32.Rectangle, p *pathOp, + begin bool) int { + nclips := 0 + if p != nil && p.parent != nil { + nclips += g.encodeClipStack(clip, bounds, p.parent, true) + nclips += 1 + } + isStroke := p.stroke.Width > 0 + if p != nil && p.path { + if isStroke { + g.enc.fillMode(scene.FillModeStroke) + g.enc.lineWidth(p.stroke.Width) + } + pathData, _ := g.drawOps.pathCache.get(p.pathKey) + g.enc.transform(p.trans) + g.enc.append(pathData.computePath) + } else { + g.enc.rect(bounds) + } + if begin { + g.enc.beginClip(clip) + if isStroke { + g.enc.fillMode(scene.FillModeNonzero) + } + if p != nil && p.path { + g.enc.transform(p.trans.Invert()) + } + } + return nclips +} + +func encodePath(verts []byte) encoder { + var enc encoder + for len(verts) >= scene.CommandSize+4 { + cmd := ops.DecodeCommand(verts[4:]) + enc.scene = append(enc.scene, cmd) + enc.npathseg++ + verts = verts[scene.CommandSize+4:] + } + return enc +} + +func (g *compute) render(tileDims image.Point) error { + const ( + // wgSize is the largest and most common workgroup size. + wgSize = 128 + // PARTITION_SIZE from elements.comp + partitionSize = 32 * 4 + ) + widthInBins := (tileDims.X + 15) / 16 + heightInBins := (tileDims.Y + 7) / 8 + if widthInBins*heightInBins > wgSize { + return fmt.Errorf("gpu: output too large (%dx%d)", + tileDims.X*tileWidthPx, tileDims.Y*tileHeightPx) + } + + // Pad scene with zeroes to avoid reading garbage in elements.comp. + scenePadding := partitionSize - len(g.enc.scene)%partitionSize + g.enc.scene = append(g.enc.scene, make([]scene.Command, scenePadding)...) + + realloced := false + scene := byteslice.Slice(g.enc.scene) + if s := len(scene); s > g.buffers.scene.size { + realloced = true + paddedCap := s * 11 / 10 + if err := g.buffers.scene.ensureCapacity(g.ctx, paddedCap); err != nil { + return err + } + } + g.buffers.scene.buffer.Upload(scene) + + w, h := tileDims.X*tileWidthPx, tileDims.Y*tileHeightPx + if g.output.size.X != w || g.output.size.Y != h { + if err := g.resizeOutput(image.Pt(w, h)); err != nil { + return err + } + } + g.ctx.BindImageTexture(kernel4OutputUnit, g.output.image, + driver.AccessWrite, driver.TextureFormatRGBA8) + if t := g.materials.tex; t != nil { + g.ctx.BindImageTexture(kernel4AtlasUnit, t, driver.AccessRead, + driver.TextureFormatRGBA8) + } + + // alloc is the number of allocated bytes for static buffers. + var alloc uint32 + round := func(v, quantum int) int { + return (v + quantum - 1) &^ (quantum - 1) + } + malloc := func(size int) memAlloc { + size = round(size, 4) + offset := alloc + alloc += uint32(size) + return memAlloc{offset /*, uint32(size)*/} + } + + *g.conf = config{ + n_elements: uint32(g.enc.npath), + n_pathseg: uint32(g.enc.npathseg), + width_in_tiles: uint32(tileDims.X), + height_in_tiles: uint32(tileDims.Y), + tile_alloc: malloc(g.enc.npath * pathSize), + bin_alloc: malloc(round(g.enc.npath, wgSize) * binSize), + ptcl_alloc: malloc(tileDims.X * tileDims.Y * ptclInitialAlloc), + pathseg_alloc: malloc(g.enc.npathseg * pathsegSize), + anno_alloc: malloc(g.enc.npath * annoSize), + trans_alloc: malloc(g.enc.ntrans * transSize), + } + + numPartitions := (g.enc.numElements() + 127) / 128 + // clearSize is the atomic partition counter plus flag and 2 states per partition. + clearSize := 4 + numPartitions*stateStride + if clearSize > g.buffers.state.size { + realloced = true + paddedCap := clearSize * 11 / 10 + if err := g.buffers.state.ensureCapacity(g.ctx, paddedCap); err != nil { + return err + } + } + + g.buffers.config.Upload(byteslice.Struct(g.conf)) + + minSize := int(unsafe.Sizeof(memoryHeader{})) + int(alloc) + if minSize > g.buffers.memory.size { + realloced = true + // Add space for dynamic GPU allocations. + const sizeBump = 4 * 1024 * 1024 + minSize += sizeBump + if err := g.buffers.memory.ensureCapacity(g.ctx, minSize); err != nil { + return err + } + } + for { + *g.memHeader = memoryHeader{ + mem_offset: alloc, + } + g.buffers.memory.buffer.Upload(byteslice.Struct(g.memHeader)) + g.buffers.state.buffer.Upload(g.zeros(clearSize)) + + if realloced { + realloced = false + g.bindBuffers() + } + t := &g.timers + g.ctx.MemoryBarrier() + t.elements.begin() + g.ctx.BindProgram(g.programs.elements) + g.ctx.DispatchCompute(numPartitions, 1, 1) + g.ctx.MemoryBarrier() + t.elements.end() + t.tileAlloc.begin() + g.ctx.BindProgram(g.programs.tileAlloc) + g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1) + g.ctx.MemoryBarrier() + t.tileAlloc.end() + t.pathCoarse.begin() + g.ctx.BindProgram(g.programs.pathCoarse) + g.ctx.DispatchCompute((g.enc.npathseg+31)/32, 1, 1) + g.ctx.MemoryBarrier() + t.pathCoarse.end() + t.backdropBinning.begin() + g.ctx.BindProgram(g.programs.backdrop) + g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1) + // No barrier needed between backdrop and binning. + g.ctx.BindProgram(g.programs.binning) + g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1) + g.ctx.MemoryBarrier() + t.backdropBinning.end() + t.coarse.begin() + g.ctx.BindProgram(g.programs.coarse) + g.ctx.DispatchCompute(widthInBins, heightInBins, 1) + g.ctx.MemoryBarrier() + t.coarse.end() + t.kernel4.begin() + g.ctx.BindProgram(g.programs.kernel4) + g.ctx.DispatchCompute(tileDims.X, tileDims.Y, 1) + g.ctx.MemoryBarrier() + t.kernel4.end() + + if err := g.buffers.memory.buffer.Download(byteslice.Struct(g.memHeader)); err != nil { + if err == driver.ErrContentLost { + continue + } + return err + } + switch errCode := g.memHeader.mem_error; errCode { + case memNoError: + return nil + case memMallocFailed: + // Resize memory and try again. + realloced = true + sz := g.buffers.memory.size * 15 / 10 + if err := g.buffers.memory.ensureCapacity(g.ctx, sz); err != nil { + return err + } + continue + default: + return fmt.Errorf("compute: shader program failed with error %d", + errCode) + } + } +} + +// zeros returns a byte slice with size bytes of zeros. +func (g *compute) zeros(size int) []byte { + if cap(g.zeroSlice) < size { + g.zeroSlice = append(g.zeroSlice, make([]byte, size)...) + } + return g.zeroSlice[:size] +} + +func (g *compute) resizeOutput(size image.Point) error { + if g.output.image != nil { + g.output.image.Release() + g.output.image = nil + } + img, err := g.ctx.NewTexture(driver.TextureFormatRGBA8, size.X, size.Y, + driver.FilterNearest, + driver.FilterNearest, + driver.BufferBindingShaderStorage|driver.BufferBindingTexture) + if err != nil { + return err + } + g.output.image = img + g.output.size = size + return nil +} + +func (g *compute) Release() { + if g.drawOps.pathCache != nil { + g.drawOps.pathCache.release() + } + if g.cache != nil { + g.cache.release() + } + progs := []driver.Program{ + g.programs.elements, + g.programs.tileAlloc, + g.programs.pathCoarse, + g.programs.backdrop, + g.programs.binning, + g.programs.coarse, + g.programs.kernel4, + } + if p := g.output.blitProg; p != nil { + p.Release() + } + for _, p := range progs { + if p != nil { + p.Release() + } + } + g.buffers.scene.release() + g.buffers.state.release() + g.buffers.memory.release() + if b := g.buffers.config; b != nil { + b.Release() + } + if g.output.image != nil { + g.output.image.Release() + } + if g.images.tex != nil { + g.images.tex.Release() + } + if g.materials.layout != nil { + g.materials.layout.Release() + } + if g.materials.prog != nil { + g.materials.prog.Release() + } + if g.materials.fbo != nil { + g.materials.fbo.Release() + } + if g.materials.tex != nil { + g.materials.tex.Release() + } + if g.materials.buffer != nil { + g.materials.buffer.Release() + } + if g.timers.t != nil { + g.timers.t.release() + } + + *g = compute{} +} + +func (g *compute) bindBuffers() { + bindStorageBuffers(g.programs.elements, g.buffers.memory.buffer, + g.buffers.config, g.buffers.scene.buffer, g.buffers.state.buffer) + bindStorageBuffers(g.programs.tileAlloc, g.buffers.memory.buffer, + g.buffers.config) + bindStorageBuffers(g.programs.pathCoarse, g.buffers.memory.buffer, + g.buffers.config) + bindStorageBuffers(g.programs.backdrop, g.buffers.memory.buffer, + g.buffers.config) + bindStorageBuffers(g.programs.binning, g.buffers.memory.buffer, + g.buffers.config) + bindStorageBuffers(g.programs.coarse, g.buffers.memory.buffer, + g.buffers.config) + bindStorageBuffers(g.programs.kernel4, g.buffers.memory.buffer, + g.buffers.config) +} + +func (b *sizedBuffer) release() { + if b.buffer == nil { + return + } + b.buffer.Release() + *b = sizedBuffer{} +} + +func (b *sizedBuffer) ensureCapacity(ctx driver.Device, size int) error { + if b.size >= size { + return nil + } + if b.buffer != nil { + b.release() + } + buf, err := ctx.NewBuffer(driver.BufferBindingShaderStorage, size) + if err != nil { + return err + } + b.buffer = buf + b.size = size + return nil +} + +func bindStorageBuffers(prog driver.Program, buffers ...driver.Buffer) { + for i, buf := range buffers { + prog.SetStorageBuffer(i, buf) + } +} + +var bo = binary.LittleEndian + +func (e *encoder) reset() { + e.scene = e.scene[:0] + e.npath = 0 + e.npathseg = 0 + e.ntrans = 0 +} + +func (e *encoder) numElements() int { + return len(e.scene) +} + +func (e *encoder) append(e2 encoder) { + e.scene = append(e.scene, e2.scene...) + e.npath += e2.npath + e.npathseg += e2.npathseg + e.ntrans += e2.ntrans +} + +func (e *encoder) transform(m f32.Affine2D) { + e.scene = append(e.scene, scene.Transform(m)) + e.ntrans++ +} + +func (e *encoder) lineWidth(width float32) { + e.scene = append(e.scene, scene.SetLineWidth(width)) +} + +func (e *encoder) fillMode(mode scene.FillMode) { + e.scene = append(e.scene, scene.SetFillMode(mode)) +} + +func (e *encoder) beginClip(bbox f32.Rectangle) { + e.scene = append(e.scene, scene.BeginClip(bbox)) + e.npath++ +} + +func (e *encoder) endClip(bbox f32.Rectangle) { + e.scene = append(e.scene, scene.EndClip(bbox)) + e.npath++ +} + +func (e *encoder) rect(r f32.Rectangle) { + // Rectangle corners, clock-wise. + c0, c1, c2, c3 := r.Min, f32.Pt(r.Min.X, r.Max.Y), r.Max, f32.Pt(r.Max.X, + r.Min.Y) + e.line(c0, c1) + e.line(c1, c2) + e.line(c2, c3) + e.line(c3, c0) +} + +func (e *encoder) fillColor(col color.RGBA) { + e.scene = append(e.scene, scene.FillColor(col)) + e.npath++ +} + +func (e *encoder) setFillImageOffset(index int, offset image.Point) { + x := int16(offset.X) + y := int16(offset.Y) + e.scene[index][2] = uint32(uint16(x)) | uint32(uint16(y))<<16 +} + +func (e *encoder) fillImage(index int) { + e.scene = append(e.scene, scene.FillImage(index)) + e.npath++ +} + +func (e *encoder) line(start, end f32.Point) { + e.scene = append(e.scene, scene.Line(start, end)) + e.npathseg++ +} + +func (e *encoder) quad(start, ctrl, end f32.Point) { + e.scene = append(e.scene, scene.Quad(start, ctrl, end)) + e.npathseg++ +} diff --git a/gio/giold/gpu/gen.go b/gio/giold/gpu/gen.go new file mode 100644 index 0000000..238f002 --- /dev/null +++ b/gio/giold/gpu/gen.go @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +//go:generate go run ./internal/convertshaders -package gpu diff --git a/gio/giold/gpu/gpu.go b/gio/giold/gpu/gpu.go new file mode 100644 index 0000000..7ff12e5 --- /dev/null +++ b/gio/giold/gpu/gpu.go @@ -0,0 +1,1505 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package gpu implements the rendering of Gio drawing operations. It +is used by package app and package app/headless and is otherwise not +useful except for integrating with external window implementations. +*/ +package gpu + +import ( + "encoding/binary" + "errors" + "fmt" + "image" + "image/color" + "math" + "os" + "reflect" + "time" + "unsafe" + + "realy.lol/gio/f32" + "realy.lol/gio/gpu/internal/driver" + "realy.lol/gio/internal/byteslice" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/internal/opconst" + "realy.lol/gio/internal/ops" + "realy.lol/gio/internal/scene" + "realy.lol/gio/internal/stroke" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + + // Register backends. + _ "realy.lol/gio/gpu/internal/d3d11" + _ "realy.lol/gio/gpu/internal/opengl" +) + +type GPU interface { + // Release non-Go resources. The GPU is no longer valid after Release. + Release() + // Clear sets the clear color for the next Frame. + Clear(color color.NRGBA) + // Collect the graphics operations from frame, given the viewport. + Collect(viewport image.Point, frame *op.Ops) + // Frame clears the color buffer and draws the collected operations. + Frame() error + // Profile returns the last available profiling information. Profiling + // information is requested when Collect sees a ProfileOp, and the result + // is available through Profile at some later time. + Profile() string +} + +type gpu struct { + cache *resourceCache + + profile string + timers *timers + frameStart time.Time + zopsTimer, stencilTimer, coverTimer, cleanupTimer *timer + drawOps drawOps + ctx driver.Device + renderer *renderer +} + +type renderer struct { + ctx driver.Device + blitter *blitter + pather *pather + packer packer + intersections packer +} + +type drawOps struct { + profile bool + reader ops.Reader + states []drawState + cache *resourceCache + vertCache []byte + viewport image.Point + clear bool + clearColor f32color.RGBA + // allImageOps is the combined list of imageOps and + // zimageOps, in drawing order. + allImageOps []imageOp + imageOps []imageOp + // zimageOps are the rectangle clipped opaque images + // that can use fast front-to-back rendering with z-test + // and no blending. + zimageOps []imageOp + pathOps []*pathOp + pathOpCache []pathOp + qs quadSplitter + pathCache *opCache + // hack for the compute renderer to access + // converted path data. + compute bool +} + +type drawState struct { + clip f32.Rectangle + t f32.Affine2D + cpath *pathOp + rect bool + + matType materialType + // Current paint.ImageOp + image imageOpData + // Current paint.ColorOp, if any. + color color.NRGBA + + // Current paint.LinearGradientOp. + stop1 f32.Point + stop2 f32.Point + color1 color.NRGBA + color2 color.NRGBA +} + +type pathOp struct { + off f32.Point + // clip is the union of all + // later clip rectangles. + clip image.Rectangle + bounds f32.Rectangle + pathKey ops.Key + path bool + pathVerts []byte + parent *pathOp + place placement + + // For compute + trans f32.Affine2D + stroke clip.StrokeStyle +} + +type imageOp struct { + z float32 + path *pathOp + clip image.Rectangle + material material + clipType clipType + place placement +} + +func decodeStrokeOp(data []byte) clip.StrokeStyle { + _ = data[4] + if opconst.OpType(data[0]) != opconst.TypeStroke { + panic("invalid op") + } + bo := binary.LittleEndian + return clip.StrokeStyle{ + Width: math.Float32frombits(bo.Uint32(data[1:])), + } +} + +type quadsOp struct { + key ops.Key + aux []byte +} + +type material struct { + material materialType + opaque bool + // For materialTypeColor. + color f32color.RGBA + // For materialTypeLinearGradient. + color1 f32color.RGBA + color2 f32color.RGBA + // For materialTypeTexture. + data imageOpData + uvTrans f32.Affine2D + + // For the compute backend. + trans f32.Affine2D +} + +// clipOp is the shadow of clip.Op. +type clipOp struct { + // TODO: Use image.Rectangle? + bounds f32.Rectangle + outline bool +} + +// imageOpData is the shadow of paint.ImageOp. +type imageOpData struct { + src *image.RGBA + handle interface{} +} + +type linearGradientOpData struct { + stop1 f32.Point + color1 color.NRGBA + stop2 f32.Point + color2 color.NRGBA +} + +func (op *clipOp) decode(data []byte) { + if opconst.OpType(data[0]) != opconst.TypeClip { + panic("invalid op") + } + bo := binary.LittleEndian + r := image.Rectangle{ + Min: image.Point{ + X: int(int32(bo.Uint32(data[1:]))), + Y: int(int32(bo.Uint32(data[5:]))), + }, + Max: image.Point{ + X: int(int32(bo.Uint32(data[9:]))), + Y: int(int32(bo.Uint32(data[13:]))), + }, + } + *op = clipOp{ + bounds: layout.FRect(r), + outline: data[17] == 1, + } +} + +func decodeImageOp(data []byte, refs []interface{}) imageOpData { + if opconst.OpType(data[0]) != opconst.TypeImage { + panic("invalid op") + } + handle := refs[1] + if handle == nil { + return imageOpData{} + } + return imageOpData{ + src: refs[0].(*image.RGBA), + handle: handle, + } +} + +func decodeColorOp(data []byte) color.NRGBA { + if opconst.OpType(data[0]) != opconst.TypeColor { + panic("invalid op") + } + return color.NRGBA{ + R: data[1], + G: data[2], + B: data[3], + A: data[4], + } +} + +func decodeLinearGradientOp(data []byte) linearGradientOpData { + if opconst.OpType(data[0]) != opconst.TypeLinearGradient { + panic("invalid op") + } + bo := binary.LittleEndian + return linearGradientOpData{ + stop1: f32.Point{ + X: math.Float32frombits(bo.Uint32(data[1:])), + Y: math.Float32frombits(bo.Uint32(data[5:])), + }, + stop2: f32.Point{ + X: math.Float32frombits(bo.Uint32(data[9:])), + Y: math.Float32frombits(bo.Uint32(data[13:])), + }, + color1: color.NRGBA{ + R: data[17+0], + G: data[17+1], + B: data[17+2], + A: data[17+3], + }, + color2: color.NRGBA{ + R: data[21+0], + G: data[21+1], + B: data[21+2], + A: data[21+3], + }, + } +} + +type clipType uint8 + +type resource interface { + release() +} + +type texture struct { + src *image.RGBA + tex driver.Texture +} + +type blitter struct { + ctx driver.Device + viewport image.Point + prog [3]*program + layout driver.InputLayout + colUniforms *blitColUniforms + texUniforms *blitTexUniforms + linearGradientUniforms *blitLinearGradientUniforms + quadVerts driver.Buffer +} + +type blitColUniforms struct { + vert struct { + blitUniforms + _ [12]byte // Padding to a multiple of 16. + } + frag struct { + colorUniforms + } +} + +type blitTexUniforms struct { + vert struct { + blitUniforms + _ [12]byte // Padding to a multiple of 16. + } +} + +type blitLinearGradientUniforms struct { + vert struct { + blitUniforms + _ [12]byte // Padding to a multiple of 16. + } + frag struct { + gradientUniforms + } +} + +type uniformBuffer struct { + buf driver.Buffer + ptr []byte +} + +type program struct { + prog driver.Program + vertUniforms *uniformBuffer + fragUniforms *uniformBuffer +} + +type blitUniforms struct { + transform [4]float32 + uvTransformR1 [4]float32 + uvTransformR2 [4]float32 + z float32 +} + +type colorUniforms struct { + color f32color.RGBA +} + +type gradientUniforms struct { + color1 f32color.RGBA + color2 f32color.RGBA +} + +type materialType uint8 + +const ( + clipTypeNone clipType = iota + clipTypePath + clipTypeIntersection +) + +const ( + materialColor materialType = iota + materialLinearGradient + materialTexture +) + +func New(api API) (GPU, error) { + d, err := driver.NewDevice(api) + if err != nil { + return nil, err + } + forceCompute := os.Getenv("GIORENDERER") == "forcecompute" + feats := d.Caps().Features + switch { + case !forceCompute && feats.Has(driver.FeatureFloatRenderTargets): + return newGPU(d) + case feats.Has(driver.FeatureCompute): + return newCompute(d) + default: + return nil, errors.New("gpu: no support for float render targets nor compute") + } +} + +func newGPU(ctx driver.Device) (*gpu, error) { + g := &gpu{ + cache: newResourceCache(), + } + g.drawOps.pathCache = newOpCache() + if err := g.init(ctx); err != nil { + return nil, err + } + return g, nil +} + +func (g *gpu) init(ctx driver.Device) error { + g.ctx = ctx + g.renderer = newRenderer(ctx) + return nil +} + +func (g *gpu) Clear(col color.NRGBA) { + g.drawOps.clear = true + g.drawOps.clearColor = f32color.LinearFromSRGB(col) +} + +func (g *gpu) Release() { + g.renderer.release() + g.drawOps.pathCache.release() + g.cache.release() + if g.timers != nil { + g.timers.release() + } + g.ctx.Release() +} + +func (g *gpu) Collect(viewport image.Point, frameOps *op.Ops) { + g.renderer.blitter.viewport = viewport + g.renderer.pather.viewport = viewport + g.drawOps.reset(g.cache, viewport) + g.drawOps.collect(g.ctx, g.cache, frameOps, viewport) + g.frameStart = time.Now() + if g.drawOps.profile && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) { + g.timers = newTimers(g.ctx) + g.zopsTimer = g.timers.newTimer() + g.stencilTimer = g.timers.newTimer() + g.coverTimer = g.timers.newTimer() + g.cleanupTimer = g.timers.newTimer() + } +} + +func (g *gpu) Frame() error { + defFBO := g.ctx.BeginFrame() + defer g.ctx.EndFrame() + viewport := g.renderer.blitter.viewport + for _, img := range g.drawOps.imageOps { + expandPathOp(img.path, img.clip) + } + if g.drawOps.profile { + g.zopsTimer.begin() + } + g.ctx.BindFramebuffer(defFBO) + g.ctx.DepthFunc(driver.DepthFuncGreater) + // Note that Clear must be before ClearDepth if nothing else is rendered + // (len(zimageOps) == 0). If not, the Fairphone 2 will corrupt the depth buffer. + if g.drawOps.clear { + g.drawOps.clear = false + g.ctx.Clear(g.drawOps.clearColor.Float32()) + } + g.ctx.ClearDepth(0.0) + g.ctx.Viewport(0, 0, viewport.X, viewport.Y) + g.renderer.drawZOps(g.cache, g.drawOps.zimageOps) + g.zopsTimer.end() + g.stencilTimer.begin() + g.ctx.SetBlend(true) + g.renderer.packStencils(&g.drawOps.pathOps) + g.renderer.stencilClips(g.drawOps.pathCache, g.drawOps.pathOps) + g.renderer.packIntersections(g.drawOps.imageOps) + g.renderer.intersect(g.drawOps.imageOps) + g.stencilTimer.end() + g.coverTimer.begin() + g.ctx.BindFramebuffer(defFBO) + g.ctx.Viewport(0, 0, viewport.X, viewport.Y) + g.renderer.drawOps(g.cache, g.drawOps.imageOps) + g.ctx.SetBlend(false) + g.renderer.pather.stenciler.invalidateFBO() + g.coverTimer.end() + g.ctx.BindFramebuffer(defFBO) + g.cleanupTimer.begin() + g.cache.frame() + g.drawOps.pathCache.frame() + g.cleanupTimer.end() + if g.drawOps.profile && g.timers.ready() { + zt, st, covt, cleant := g.zopsTimer.Elapsed, g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed + ft := zt + st + covt + cleant + q := 100 * time.Microsecond + zt, st, covt = zt.Round(q), st.Round(q), covt.Round(q) + frameDur := time.Since(g.frameStart).Round(q) + ft = ft.Round(q) + g.profile = fmt.Sprintf("draw:%7s gpu:%7s zt:%7s st:%7s cov:%7s", + frameDur, ft, zt, st, covt) + } + return nil +} + +func (g *gpu) Profile() string { + return g.profile +} + +func (r *renderer) texHandle(cache *resourceCache, + data imageOpData) driver.Texture { + var tex *texture + t, exists := cache.get(data.handle) + if !exists { + t = &texture{ + src: data.src, + } + cache.put(data.handle, t) + } + tex = t.(*texture) + if tex.tex != nil { + return tex.tex + } + handle, err := r.ctx.NewTexture(driver.TextureFormatSRGB, + data.src.Bounds().Dx(), data.src.Bounds().Dy(), driver.FilterLinear, + driver.FilterLinear, driver.BufferBindingTexture) + if err != nil { + panic(err) + } + driver.UploadImage(handle, image.Pt(0, 0), data.src) + tex.tex = handle + return tex.tex +} + +func (t *texture) release() { + if t.tex != nil { + t.tex.Release() + } +} + +func newRenderer(ctx driver.Device) *renderer { + r := &renderer{ + ctx: ctx, + blitter: newBlitter(ctx), + pather: newPather(ctx), + } + + maxDim := ctx.Caps().MaxTextureSize + // Large atlas textures cause artifacts due to precision loss in + // shaders. + if cap := 8192; maxDim > cap { + maxDim = cap + } + + r.packer.maxDim = maxDim + r.intersections.maxDim = maxDim + return r +} + +func (r *renderer) release() { + r.pather.release() + r.blitter.release() +} + +func newBlitter(ctx driver.Device) *blitter { + quadVerts, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices, + byteslice.Slice([]float32{ + -1, +1, 0, 0, + +1, +1, 1, 0, + -1, -1, 0, 1, + +1, -1, 1, 1, + }), + ) + if err != nil { + panic(err) + } + b := &blitter{ + ctx: ctx, + quadVerts: quadVerts, + } + b.colUniforms = new(blitColUniforms) + b.texUniforms = new(blitTexUniforms) + b.linearGradientUniforms = new(blitLinearGradientUniforms) + prog, layout, err := createColorPrograms(ctx, shader_blit_vert, + shader_blit_frag, + [3]interface{}{&b.colUniforms.vert, &b.linearGradientUniforms.vert, + &b.texUniforms.vert}, + [3]interface{}{&b.colUniforms.frag, &b.linearGradientUniforms.frag, + nil}, + ) + if err != nil { + panic(err) + } + b.prog = prog + b.layout = layout + return b +} + +func (b *blitter) release() { + b.quadVerts.Release() + for _, p := range b.prog { + p.Release() + } + b.layout.Release() +} + +func createColorPrograms(b driver.Device, vsSrc driver.ShaderSources, + fsSrc [3]driver.ShaderSources, + vertUniforms, fragUniforms [3]interface{}) ([3]*program, driver.InputLayout, + error) { + var progs [3]*program + { + prog, err := b.NewProgram(vsSrc, fsSrc[materialTexture]) + if err != nil { + return progs, nil, err + } + var vertBuffer, fragBuffer *uniformBuffer + if u := vertUniforms[materialTexture]; u != nil { + vertBuffer = newUniformBuffer(b, u) + prog.SetVertexUniforms(vertBuffer.buf) + } + if u := fragUniforms[materialTexture]; u != nil { + fragBuffer = newUniformBuffer(b, u) + prog.SetFragmentUniforms(fragBuffer.buf) + } + progs[materialTexture] = newProgram(prog, vertBuffer, fragBuffer) + } + { + var vertBuffer, fragBuffer *uniformBuffer + prog, err := b.NewProgram(vsSrc, fsSrc[materialColor]) + if err != nil { + progs[materialTexture].Release() + return progs, nil, err + } + if u := vertUniforms[materialColor]; u != nil { + vertBuffer = newUniformBuffer(b, u) + prog.SetVertexUniforms(vertBuffer.buf) + } + if u := fragUniforms[materialColor]; u != nil { + fragBuffer = newUniformBuffer(b, u) + prog.SetFragmentUniforms(fragBuffer.buf) + } + progs[materialColor] = newProgram(prog, vertBuffer, fragBuffer) + } + { + var vertBuffer, fragBuffer *uniformBuffer + prog, err := b.NewProgram(vsSrc, fsSrc[materialLinearGradient]) + if err != nil { + progs[materialTexture].Release() + progs[materialColor].Release() + return progs, nil, err + } + if u := vertUniforms[materialLinearGradient]; u != nil { + vertBuffer = newUniformBuffer(b, u) + prog.SetVertexUniforms(vertBuffer.buf) + } + if u := fragUniforms[materialLinearGradient]; u != nil { + fragBuffer = newUniformBuffer(b, u) + prog.SetFragmentUniforms(fragBuffer.buf) + } + progs[materialLinearGradient] = newProgram(prog, vertBuffer, fragBuffer) + } + layout, err := b.NewInputLayout(vsSrc, []driver.InputDesc{ + {Type: driver.DataTypeFloat, Size: 2, Offset: 0}, + {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2}, + }) + if err != nil { + progs[materialTexture].Release() + progs[materialColor].Release() + progs[materialLinearGradient].Release() + return progs, nil, err + } + return progs, layout, nil +} + +func (r *renderer) stencilClips(pathCache *opCache, ops []*pathOp) { + if len(r.packer.sizes) == 0 { + return + } + fbo := -1 + r.pather.begin(r.packer.sizes) + for _, p := range ops { + if fbo != p.place.Idx { + fbo = p.place.Idx + f := r.pather.stenciler.cover(fbo) + r.ctx.BindFramebuffer(f.fbo) + r.ctx.Clear(0.0, 0.0, 0.0, 0.0) + } + v, _ := pathCache.get(p.pathKey) + r.pather.stencilPath(p.clip, p.off, p.place.Pos, v.data) + } +} + +func (r *renderer) intersect(ops []imageOp) { + if len(r.intersections.sizes) == 0 { + return + } + fbo := -1 + r.pather.stenciler.beginIntersect(r.intersections.sizes) + r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0) + r.ctx.BindInputLayout(r.pather.stenciler.iprog.layout) + for _, img := range ops { + if img.clipType != clipTypeIntersection { + continue + } + if fbo != img.place.Idx { + fbo = img.place.Idx + f := r.pather.stenciler.intersections.fbos[fbo] + r.ctx.BindFramebuffer(f.fbo) + r.ctx.Clear(1.0, 0.0, 0.0, 0.0) + } + r.ctx.Viewport(img.place.Pos.X, img.place.Pos.Y, img.clip.Dx(), + img.clip.Dy()) + r.intersectPath(img.path, img.clip) + } +} + +func (r *renderer) intersectPath(p *pathOp, clip image.Rectangle) { + if p.parent != nil { + r.intersectPath(p.parent, clip) + } + if !p.path { + return + } + uv := image.Rectangle{ + Min: p.place.Pos, + Max: p.place.Pos.Add(p.clip.Size()), + } + o := clip.Min.Sub(p.clip.Min) + sub := image.Rectangle{ + Min: o, + Max: o.Add(clip.Size()), + } + fbo := r.pather.stenciler.cover(p.place.Idx) + r.ctx.BindTexture(0, fbo.tex) + coverScale, coverOff := texSpaceTransform(layout.FRect(uv), fbo.size) + subScale, subOff := texSpaceTransform(layout.FRect(sub), p.clip.Size()) + r.pather.stenciler.iprog.uniforms.vert.uvTransform = [4]float32{coverScale.X, + coverScale.Y, coverOff.X, coverOff.Y} + r.pather.stenciler.iprog.uniforms.vert.subUVTransform = [4]float32{subScale.X, + subScale.Y, subOff.X, subOff.Y} + r.pather.stenciler.iprog.prog.UploadUniforms() + r.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4) +} + +func (r *renderer) packIntersections(ops []imageOp) { + r.intersections.clear() + for i, img := range ops { + var npaths int + var onePath *pathOp + for p := img.path; p != nil; p = p.parent { + if p.path { + onePath = p + npaths++ + } + } + switch npaths { + case 0: + case 1: + place := onePath.place + place.Pos = place.Pos.Sub(onePath.clip.Min).Add(img.clip.Min) + ops[i].place = place + ops[i].clipType = clipTypePath + default: + sz := image.Point{X: img.clip.Dx(), Y: img.clip.Dy()} + place, ok := r.intersections.add(sz) + if !ok { + panic("internal error: if the intersection fit, the intersection should fit as well") + } + ops[i].clipType = clipTypeIntersection + ops[i].place = place + } + } +} + +func (r *renderer) packStencils(pops *[]*pathOp) { + r.packer.clear() + ops := *pops + // Allocate atlas space for cover textures. + var i int + for i < len(ops) { + p := ops[i] + if p.clip.Empty() { + ops[i] = ops[len(ops)-1] + ops = ops[:len(ops)-1] + continue + } + sz := image.Point{X: p.clip.Dx(), Y: p.clip.Dy()} + place, ok := r.packer.add(sz) + if !ok { + // The clip area is at most the entire screen. Hopefully no + // screen is larger than GL_MAX_TEXTURE_SIZE. + panic(fmt.Errorf("clip area %v is larger than maximum texture size %dx%d", + p.clip, r.packer.maxDim, r.packer.maxDim)) + } + p.place = place + i++ + } + *pops = ops +} + +// intersects intersects clip and b where b is offset by off. +// ceilRect returns a bounding image.Rectangle for a f32.Rectangle. +func boundRectF(r f32.Rectangle) image.Rectangle { + return image.Rectangle{ + Min: image.Point{ + X: int(floor(r.Min.X)), + Y: int(floor(r.Min.Y)), + }, + Max: image.Point{ + X: int(ceil(r.Max.X)), + Y: int(ceil(r.Max.Y)), + }, + } +} + +func ceil(v float32) int { + return int(math.Ceil(float64(v))) +} + +func floor(v float32) int { + return int(math.Floor(float64(v))) +} + +func (d *drawOps) reset(cache *resourceCache, viewport image.Point) { + d.profile = false + d.cache = cache + d.viewport = viewport + d.imageOps = d.imageOps[:0] + d.allImageOps = d.allImageOps[:0] + d.zimageOps = d.zimageOps[:0] + d.pathOps = d.pathOps[:0] + d.pathOpCache = d.pathOpCache[:0] + d.vertCache = d.vertCache[:0] +} + +func (d *drawOps) collect(ctx driver.Device, cache *resourceCache, root *op.Ops, + viewport image.Point) { + clip := f32.Rectangle{ + Max: f32.Point{X: float32(viewport.X), Y: float32(viewport.Y)}, + } + d.reader.Reset(root) + state := drawState{ + clip: clip, + rect: true, + color: color.NRGBA{A: 0xff}, + } + d.collectOps(&d.reader, state) + for _, p := range d.pathOps { + if v, exists := d.pathCache.get(p.pathKey); !exists || v.data.data == nil { + data := buildPath(ctx, p.pathVerts) + var computePath encoder + if d.compute { + computePath = encodePath(p.pathVerts) + } + d.pathCache.put(p.pathKey, opCacheValue{ + data: data, + bounds: p.bounds, + computePath: computePath, + }) + } + p.pathVerts = nil + } +} + +func (d *drawOps) newPathOp() *pathOp { + d.pathOpCache = append(d.pathOpCache, pathOp{}) + return &d.pathOpCache[len(d.pathOpCache)-1] +} + +func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, + bounds f32.Rectangle, off f32.Point, tr f32.Affine2D, + stroke clip.StrokeStyle) { + npath := d.newPathOp() + *npath = pathOp{ + parent: state.cpath, + bounds: bounds, + off: off, + trans: tr, + stroke: stroke, + } + state.cpath = npath + if len(aux) > 0 { + state.rect = false + state.cpath.pathKey = auxKey + state.cpath.path = true + state.cpath.pathVerts = aux + d.pathOps = append(d.pathOps, state.cpath) + } +} + +// split a transform into two parts, one which is pur offset and the +// other representing the scaling, shearing and rotation part +func splitTransform(t f32.Affine2D) (srs f32.Affine2D, offset f32.Point) { + sx, hx, ox, hy, sy, oy := t.Elems() + offset = f32.Point{X: ox, Y: oy} + srs = f32.NewAffine2D(sx, hx, 0, hy, sy, 0) + return +} + +func (d *drawOps) save(id int, state drawState) { + if extra := id - len(d.states) + 1; extra > 0 { + d.states = append(d.states, make([]drawState, extra)...) + } + d.states[id] = state +} + +func (d *drawOps) collectOps(r *ops.Reader, state drawState) { + var ( + quads quadsOp + str clip.StrokeStyle + z int + ) + d.save(opconst.InitialStateID, state) +loop: + for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { + switch opconst.OpType(encOp.Data[0]) { + case opconst.TypeProfile: + d.profile = true + case opconst.TypeTransform: + dop := ops.DecodeTransform(encOp.Data) + state.t = state.t.Mul(dop) + + case opconst.TypeStroke: + str = decodeStrokeOp(encOp.Data) + + case opconst.TypePath: + encOp, ok = r.Decode() + if !ok { + break loop + } + quads.aux = encOp.Data[opconst.TypeAuxLen:] + quads.key = encOp.Key + + case opconst.TypeClip: + var op clipOp + op.decode(encOp.Data) + bounds := op.bounds + trans, off := splitTransform(state.t) + if len(quads.aux) > 0 { + // There is a clipping path, build the gpu data and update the + // cache key such that it will be equal only if the transform is the + // same also. Use cached data if we have it. + quads.key = quads.key.SetTransform(trans) + if v, ok := d.pathCache.get(quads.key); ok { + // Since the GPU data exists in the cache aux will not be used. + // Why is this not used for the offset shapes? + op.bounds = v.bounds + } else { + pathData, bounds := d.buildVerts( + quads.aux, trans, op.outline, str, + ) + op.bounds = bounds + if !d.compute { + quads.aux = pathData + } + // add it to the cache, without GPU data, so the transform can be + // reused. + d.pathCache.put(quads.key, opCacheValue{bounds: op.bounds}) + } + } else { + quads.aux, op.bounds, _ = d.boundsForTransformedRect(bounds, + trans) + quads.key = encOp.Key + quads.key.SetTransform(trans) + } + state.clip = state.clip.Intersect(op.bounds.Add(off)) + d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, state.t, + str) + quads = quadsOp{} + str = clip.StrokeStyle{} + + case opconst.TypeColor: + state.matType = materialColor + state.color = decodeColorOp(encOp.Data) + case opconst.TypeLinearGradient: + state.matType = materialLinearGradient + op := decodeLinearGradientOp(encOp.Data) + state.stop1 = op.stop1 + state.stop2 = op.stop2 + state.color1 = op.color1 + state.color2 = op.color2 + case opconst.TypeImage: + state.matType = materialTexture + state.image = decodeImageOp(encOp.Data, encOp.Refs) + case opconst.TypePaint: + // Transform (if needed) the painting rectangle and if so generate a clip path, + // for those cases also compute a partialTrans that maps texture coordinates between + // the new bounding rectangle and the transformed original paint rectangle. + trans, off := splitTransform(state.t) + // Fill the clip area, unless the material is a (bounded) image. + // TODO: Find a tighter bound. + inf := float32(1e6) + dst := f32.Rect(-inf, -inf, inf, inf) + if state.matType == materialTexture { + dst = layout.FRect(state.image.src.Rect) + } + clipData, bnd, partialTrans := d.boundsForTransformedRect(dst, + trans) + cl := state.clip.Intersect(bnd.Add(off)) + if cl.Empty() { + continue + } + + wasrect := state.rect + if clipData != nil { + // The paint operation is sheared or rotated, add a clip path representing + // this transformed rectangle. + encOp.Key.SetTransform(trans) + d.addClipPath(&state, clipData, encOp.Key, bnd, off, state.t, + clip.StrokeStyle{}) + } + + bounds := boundRectF(cl) + mat := state.materialFor(bnd, off, partialTrans, bounds, state.t) + + if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && state.rect && mat.opaque && (mat.material == materialColor) { + // The image is a uniform opaque color and takes up the whole screen. + // Scrap images up to and including this image and set clear color. + d.allImageOps = d.allImageOps[:0] + d.zimageOps = d.zimageOps[:0] + d.imageOps = d.imageOps[:0] + z = 0 + d.clearColor = mat.color.Opaque() + d.clear = true + continue + } + z++ + if z != int(uint16(z)) { + // TODO(eliasnaur) realy.lol/gio/issue/127. + panic("more than 65k paint objects not supported") + } + // Assume 16-bit depth buffer. + const zdepth = 1 << 16 + // Convert z to window-space, assuming depth range [0;1]. + zf := float32(z)*2/zdepth - 1.0 + img := imageOp{ + z: zf, + path: state.cpath, + clip: bounds, + material: mat, + } + + d.allImageOps = append(d.allImageOps, img) + if state.rect && img.material.opaque { + d.zimageOps = append(d.zimageOps, img) + } else { + d.imageOps = append(d.imageOps, img) + } + if clipData != nil { + // we added a clip path that should not remain + state.cpath = state.cpath.parent + state.rect = wasrect + } + case opconst.TypeSave: + id := ops.DecodeSave(encOp.Data) + d.save(id, state) + case opconst.TypeLoad: + id, mask := ops.DecodeLoad(encOp.Data) + s := d.states[id] + if mask&opconst.TransformState != 0 { + state.t = s.t + } + if mask&^opconst.TransformState != 0 { + state = s + } + } + } +} + +func expandPathOp(p *pathOp, clip image.Rectangle) { + for p != nil { + pclip := p.clip + if !pclip.Empty() { + clip = clip.Union(pclip) + } + p.clip = clip + p = p.parent + } +} + +func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, + partTrans f32.Affine2D, clip image.Rectangle, trans f32.Affine2D) material { + var m material + switch d.matType { + case materialColor: + m.material = materialColor + m.color = f32color.LinearFromSRGB(d.color) + m.opaque = m.color.A == 1.0 + case materialLinearGradient: + m.material = materialLinearGradient + + m.color1 = f32color.LinearFromSRGB(d.color1) + m.color2 = f32color.LinearFromSRGB(d.color2) + m.opaque = m.color1.A == 1.0 && m.color2.A == 1.0 + + m.uvTrans = partTrans.Mul(gradientSpaceTransform(clip, off, d.stop1, + d.stop2)) + case materialTexture: + m.material = materialTexture + dr := boundRectF(rect.Add(off)) + sz := d.image.src.Bounds().Size() + sr := f32.Rectangle{ + Max: f32.Point{ + X: float32(sz.X), + Y: float32(sz.Y), + }, + } + dx := float32(dr.Dx()) + sdx := sr.Dx() + sr.Min.X += float32(clip.Min.X-dr.Min.X) * sdx / dx + sr.Max.X -= float32(dr.Max.X-clip.Max.X) * sdx / dx + dy := float32(dr.Dy()) + sdy := sr.Dy() + sr.Min.Y += float32(clip.Min.Y-dr.Min.Y) * sdy / dy + sr.Max.Y -= float32(dr.Max.Y-clip.Max.Y) * sdy / dy + uvScale, uvOffset := texSpaceTransform(sr, sz) + m.uvTrans = partTrans.Mul(f32.Affine2D{}.Scale(f32.Point{}, + uvScale).Offset(uvOffset)) + m.trans = trans + m.data = d.image + } + return m +} + +func (r *renderer) drawZOps(cache *resourceCache, ops []imageOp) { + r.ctx.SetDepthTest(true) + r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0) + r.ctx.BindInputLayout(r.blitter.layout) + // Render front to back. + for i := len(ops) - 1; i >= 0; i-- { + img := ops[i] + m := img.material + switch m.material { + case materialTexture: + r.ctx.BindTexture(0, r.texHandle(cache, m.data)) + } + drc := img.clip + scale, off := clipSpaceTransform(drc, r.blitter.viewport) + r.blitter.blit(img.z, m.material, m.color, m.color1, m.color2, scale, + off, m.uvTrans) + } + r.ctx.SetDepthTest(false) +} + +func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) { + r.ctx.SetDepthTest(true) + r.ctx.DepthMask(false) + r.ctx.BlendFunc(driver.BlendFactorOne, driver.BlendFactorOneMinusSrcAlpha) + r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0) + r.ctx.BindInputLayout(r.pather.coverer.layout) + var coverTex driver.Texture + for _, img := range ops { + m := img.material + switch m.material { + case materialTexture: + r.ctx.BindTexture(0, r.texHandle(cache, m.data)) + } + drc := img.clip + + scale, off := clipSpaceTransform(drc, r.blitter.viewport) + var fbo stencilFBO + switch img.clipType { + case clipTypeNone: + r.blitter.blit(img.z, m.material, m.color, m.color1, m.color2, + scale, off, m.uvTrans) + continue + case clipTypePath: + fbo = r.pather.stenciler.cover(img.place.Idx) + case clipTypeIntersection: + fbo = r.pather.stenciler.intersections.fbos[img.place.Idx] + } + if coverTex != fbo.tex { + coverTex = fbo.tex + r.ctx.BindTexture(1, coverTex) + } + uv := image.Rectangle{ + Min: img.place.Pos, + Max: img.place.Pos.Add(drc.Size()), + } + coverScale, coverOff := texSpaceTransform(layout.FRect(uv), fbo.size) + r.pather.cover(img.z, m.material, m.color, m.color1, m.color2, scale, + off, m.uvTrans, coverScale, coverOff) + } + r.ctx.DepthMask(true) + r.ctx.SetDepthTest(false) +} + +func (b *blitter) blit(z float32, mat materialType, col f32color.RGBA, + col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D) { + p := b.prog[mat] + b.ctx.BindProgram(p.prog) + var uniforms *blitUniforms + switch mat { + case materialColor: + b.colUniforms.frag.color = col + uniforms = &b.colUniforms.vert.blitUniforms + case materialTexture: + t1, t2, t3, t4, t5, t6 := uvTrans.Elems() + b.texUniforms.vert.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, + 0} + b.texUniforms.vert.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, + 0} + uniforms = &b.texUniforms.vert.blitUniforms + case materialLinearGradient: + b.linearGradientUniforms.frag.color1 = col1 + b.linearGradientUniforms.frag.color2 = col2 + + t1, t2, t3, t4, t5, t6 := uvTrans.Elems() + b.linearGradientUniforms.vert.blitUniforms.uvTransformR1 = [4]float32{t1, + t2, t3, 0} + b.linearGradientUniforms.vert.blitUniforms.uvTransformR2 = [4]float32{t4, + t5, t6, 0} + uniforms = &b.linearGradientUniforms.vert.blitUniforms + } + uniforms.z = z + uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y} + p.UploadUniforms() + b.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4) +} + +// newUniformBuffer creates a new GPU uniform buffer backed by the +// structure uniformBlock points to. +func newUniformBuffer(b driver.Device, + uniformBlock interface{}) *uniformBuffer { + ref := reflect.ValueOf(uniformBlock) + // Determine the size of the uniforms structure, *uniforms. + size := ref.Elem().Type().Size() + // Map the uniforms structure as a byte slice. + ptr := (*[1 << 30]byte)(unsafe.Pointer(ref.Pointer()))[:size:size] + ubuf, err := b.NewBuffer(driver.BufferBindingUniforms, len(ptr)) + if err != nil { + panic(err) + } + return &uniformBuffer{buf: ubuf, ptr: ptr} +} + +func (u *uniformBuffer) Upload() { + u.buf.Upload(u.ptr) +} + +func (u *uniformBuffer) Release() { + u.buf.Release() + u.buf = nil +} + +func newProgram(prog driver.Program, + vertUniforms, fragUniforms *uniformBuffer) *program { + if vertUniforms != nil { + prog.SetVertexUniforms(vertUniforms.buf) + } + if fragUniforms != nil { + prog.SetFragmentUniforms(fragUniforms.buf) + } + return &program{prog: prog, vertUniforms: vertUniforms, + fragUniforms: fragUniforms} +} + +func (p *program) UploadUniforms() { + if p.vertUniforms != nil { + p.vertUniforms.Upload() + } + if p.fragUniforms != nil { + p.fragUniforms.Upload() + } +} + +func (p *program) Release() { + p.prog.Release() + p.prog = nil + if p.vertUniforms != nil { + p.vertUniforms.Release() + p.vertUniforms = nil + } + if p.fragUniforms != nil { + p.fragUniforms.Release() + p.fragUniforms = nil + } +} + +// texSpaceTransform return the scale and offset that transforms the given subimage +// into quad texture coordinates. +func texSpaceTransform(r f32.Rectangle, bounds image.Point) (f32.Point, + f32.Point) { + size := f32.Point{X: float32(bounds.X), Y: float32(bounds.Y)} + scale := f32.Point{X: r.Dx() / size.X, Y: r.Dy() / size.Y} + offset := f32.Point{X: r.Min.X / size.X, Y: r.Min.Y / size.Y} + return scale, offset +} + +// gradientSpaceTransform transforms stop1 and stop2 to [(0,0), (1,1)]. +func gradientSpaceTransform(clip image.Rectangle, off f32.Point, + stop1, stop2 f32.Point) f32.Affine2D { + d := stop2.Sub(stop1) + l := float32(math.Sqrt(float64(d.X*d.X + d.Y*d.Y))) + a := float32(math.Atan2(float64(-d.Y), float64(d.X))) + + // TODO: optimize + zp := f32.Point{} + return f32.Affine2D{}. + Scale(zp, layout.FPt(clip.Size())). // scale to pixel space + Offset(zp.Sub(off).Add(layout.FPt(clip.Min))). // offset to clip space + Offset(zp.Sub(stop1)). // offset to first stop point + Rotate(zp, a). // rotate to align gradient + Scale(zp, f32.Pt(1/l, 1/l)) // scale gradient to right size +} + +// clipSpaceTransform returns the scale and offset that transforms the given +// rectangle from a viewport into OpenGL clip space. +func clipSpaceTransform(r image.Rectangle, viewport image.Point) (f32.Point, + f32.Point) { + // First, transform UI coordinates to OpenGL coordinates: + // + // [(-1, +1) (+1, +1)] + // [(-1, -1) (+1, -1)] + // + x, y := float32(r.Min.X), float32(r.Min.Y) + w, h := float32(r.Dx()), float32(r.Dy()) + vx, vy := 2/float32(viewport.X), 2/float32(viewport.Y) + x = x*vx - 1 + y = 1 - y*vy + w *= vx + h *= vy + + // Then, compute the transformation from the fullscreen quad to + // the rectangle at (x, y) and dimensions (w, h). + scale := f32.Point{X: w * .5, Y: h * .5} + offset := f32.Point{X: x + w*.5, Y: y - h*.5} + + return scale, offset +} + +// Fill in maximal Y coordinates of the NW and NE corners. +func fillMaxY(verts []byte) { + contour := 0 + bo := binary.LittleEndian + for len(verts) > 0 { + maxy := float32(math.Inf(-1)) + i := 0 + for ; i+vertStride*4 <= len(verts); i += vertStride * 4 { + vert := verts[i : i+vertStride] + // MaxY contains the integer contour index. + pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).MaxY)):])) + if contour != pathContour { + contour = pathContour + break + } + fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).FromY)):])) + ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).CtrlY)):])) + toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).ToY)):])) + if fromy > maxy { + maxy = fromy + } + if ctrly > maxy { + maxy = ctrly + } + if toy > maxy { + maxy = toy + } + } + fillContourMaxY(maxy, verts[:i]) + verts = verts[i:] + } +} + +func fillContourMaxY(maxy float32, verts []byte) { + bo := binary.LittleEndian + for i := 0; i < len(verts); i += vertStride { + off := int(unsafe.Offsetof(((*vertex)(nil)).MaxY)) + bo.PutUint32(verts[i+off:], math.Float32bits(maxy)) + } +} + +func (d *drawOps) writeVertCache(n int) []byte { + d.vertCache = append(d.vertCache, make([]byte, n)...) + return d.vertCache[len(d.vertCache)-n:] +} + +// transform, split paths as needed, calculate maxY, bounds and create GPU vertices. +func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, + str clip.StrokeStyle) (verts []byte, bounds f32.Rectangle) { + inf := float32(math.Inf(+1)) + d.qs.bounds = f32.Rectangle{ + Min: f32.Point{X: inf, Y: inf}, + Max: f32.Point{X: -inf, Y: -inf}, + } + d.qs.d = d + startLength := len(d.vertCache) + + switch { + case str.Width > 0: + // Stroke path. + ss := stroke.StrokeStyle{ + Width: str.Width, + Miter: str.Miter, + Cap: stroke.StrokeCap(str.Cap), + Join: stroke.StrokeJoin(str.Join), + } + quads := stroke.StrokePathCommands(ss, stroke.DashOp{}, pathData) + for _, quad := range quads { + d.qs.contour = quad.Contour + quad.Quad = quad.Quad.Transform(tr) + + d.qs.splitAndEncode(quad.Quad) + } + + case outline: + decodeToOutlineQuads(&d.qs, tr, pathData) + } + + fillMaxY(d.vertCache[startLength:]) + return d.vertCache[startLength:], d.qs.bounds +} + +// decodeOutlineQuads decodes scene commands, splits them into quadratic bĆ©ziers +// as needed and feeds them to the supplied splitter. +func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) { + for len(pathData) >= scene.CommandSize+4 { + qs.contour = bo.Uint32(pathData) + cmd := ops.DecodeCommand(pathData[4:]) + switch cmd.Op() { + case scene.OpLine: + var q stroke.QuadSegment + q.From, q.To = scene.DecodeLine(cmd) + q.Ctrl = q.From.Add(q.To).Mul(.5) + q = q.Transform(tr) + qs.splitAndEncode(q) + case scene.OpQuad: + var q stroke.QuadSegment + q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) + q = q.Transform(tr) + qs.splitAndEncode(q) + case scene.OpCubic: + for _, q := range stroke.SplitCubic(scene.DecodeCubic(cmd)) { + q = q.Transform(tr) + qs.splitAndEncode(q) + } + default: + panic("unsupported scene command") + } + pathData = pathData[scene.CommandSize+4:] + } +} + +// create GPU vertices for transformed r, find the bounds and establish texture transform. +func (d *drawOps) boundsForTransformedRect(r f32.Rectangle, + tr f32.Affine2D) (aux []byte, bnd f32.Rectangle, ptr f32.Affine2D) { + if isPureOffset(tr) { + // fast-path to allow blitting of pure rectangles + _, _, ox, _, _, oy := tr.Elems() + off := f32.Pt(ox, oy) + bnd.Min = r.Min.Add(off) + bnd.Max = r.Max.Add(off) + return + } + + // transform all corners, find new bounds + corners := [4]f32.Point{ + tr.Transform(r.Min), tr.Transform(f32.Pt(r.Max.X, r.Min.Y)), + tr.Transform(r.Max), tr.Transform(f32.Pt(r.Min.X, r.Max.Y)), + } + bnd.Min = f32.Pt(math.MaxFloat32, math.MaxFloat32) + bnd.Max = f32.Pt(-math.MaxFloat32, -math.MaxFloat32) + for _, c := range corners { + if c.X < bnd.Min.X { + bnd.Min.X = c.X + } + if c.Y < bnd.Min.Y { + bnd.Min.Y = c.Y + } + if c.X > bnd.Max.X { + bnd.Max.X = c.X + } + if c.Y > bnd.Max.Y { + bnd.Max.Y = c.Y + } + } + + // build the GPU vertices + l := len(d.vertCache) + if !d.compute { + d.vertCache = append(d.vertCache, make([]byte, vertStride*4*4)...) + aux = d.vertCache[l:] + encodeQuadTo(aux, 0, corners[0], corners[0].Add(corners[1]).Mul(0.5), + corners[1]) + encodeQuadTo(aux[vertStride*4:], 0, corners[1], + corners[1].Add(corners[2]).Mul(0.5), corners[2]) + encodeQuadTo(aux[vertStride*4*2:], 0, corners[2], + corners[2].Add(corners[3]).Mul(0.5), corners[3]) + encodeQuadTo(aux[vertStride*4*3:], 0, corners[3], + corners[3].Add(corners[0]).Mul(0.5), corners[0]) + fillMaxY(aux) + } else { + d.vertCache = append(d.vertCache, + make([]byte, (scene.CommandSize+4)*4)...) + aux = d.vertCache[l:] + buf := aux + bo := binary.LittleEndian + bo.PutUint32(buf, 0) // Contour + ops.EncodeCommand(buf[4:], scene.Line(r.Min, f32.Pt(r.Max.X, r.Min.Y))) + buf = buf[4+scene.CommandSize:] + bo.PutUint32(buf, 0) + ops.EncodeCommand(buf[4:], scene.Line(f32.Pt(r.Max.X, r.Min.Y), r.Max)) + buf = buf[4+scene.CommandSize:] + bo.PutUint32(buf, 0) + ops.EncodeCommand(buf[4:], scene.Line(r.Max, f32.Pt(r.Min.X, r.Max.Y))) + buf = buf[4+scene.CommandSize:] + bo.PutUint32(buf, 0) + ops.EncodeCommand(buf[4:], scene.Line(f32.Pt(r.Min.X, r.Max.Y), r.Min)) + } + + // establish the transform mapping from bounds rectangle to transformed corners + var P1, P2, P3 f32.Point + P1.X = (corners[1].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X) + P1.Y = (corners[1].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y) + P2.X = (corners[2].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X) + P2.Y = (corners[2].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y) + P3.X = (corners[3].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X) + P3.Y = (corners[3].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y) + sx, sy := P2.X-P3.X, P2.Y-P3.Y + ptr = f32.NewAffine2D(sx, P2.X-P1.X, P1.X-sx, sy, P2.Y-P1.Y, + P1.Y-sy).Invert() + + return +} + +func isPureOffset(t f32.Affine2D) bool { + a, b, _, d, e, _ := t.Elems() + return a == 1 && b == 0 && d == 0 && e == 1 +} diff --git a/gio/giold/gpu/headless/driver_test.go b/gio/giold/gpu/headless/driver_test.go new file mode 100644 index 0000000..07c0b06 --- /dev/null +++ b/gio/giold/gpu/headless/driver_test.go @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "bytes" + "flag" + "image" + "image/color" + "image/png" + "io/ioutil" + "runtime" + "testing" + + "realy.lol/gio/gpu/internal/driver" + "realy.lol/gio/internal/byteslice" + "realy.lol/gio/internal/f32color" +) + +var dumpImages = flag.Bool("saveimages", false, "save test images") + +var clearCol = color.NRGBA{A: 0xff, R: 0xde, G: 0xad, B: 0xbe} +var clearColExpect = f32color.NRGBAToRGBA(clearCol) + +func TestFramebufferClear(t *testing.T) { + b := newDriver(t) + sz := image.Point{X: 800, Y: 600} + fbo := setupFBO(t, b, sz) + img := screenshot(t, b, fbo, sz) + if got := img.RGBAAt(0, 0); got != clearColExpect { + t.Errorf("got color %v, expected %v", got, clearColExpect) + } +} + +func TestSimpleShader(t *testing.T) { + b := newDriver(t) + sz := image.Point{X: 800, Y: 600} + fbo := setupFBO(t, b, sz) + p, err := b.NewProgram(shader_simple_vert, shader_simple_frag) + if err != nil { + t.Fatal(err) + } + defer p.Release() + b.BindProgram(p) + b.DrawArrays(driver.DrawModeTriangles, 0, 3) + img := screenshot(t, b, fbo, sz) + if got := img.RGBAAt(0, 0); got != clearColExpect { + t.Errorf("got color %v, expected %v", got, clearColExpect) + } + // Just off the center to catch inverted triangles. + cx, cy := 300, 400 + shaderCol := f32color.RGBA{R: .25, G: .55, B: .75, A: 1.0} + if got, exp := img.RGBAAt(cx, + cy), shaderCol.SRGB(); got != f32color.NRGBAToRGBA(exp) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(exp)) + } +} + +func TestInputShader(t *testing.T) { + b := newDriver(t) + sz := image.Point{X: 800, Y: 600} + fbo := setupFBO(t, b, sz) + p, err := b.NewProgram(shader_input_vert, shader_simple_frag) + if err != nil { + t.Fatal(err) + } + defer p.Release() + b.BindProgram(p) + buf, err := b.NewImmutableBuffer(driver.BufferBindingVertices, + byteslice.Slice([]float32{ + 0, .5, .5, 1, + -.5, -.5, .5, 1, + .5, -.5, .5, 1, + }), + ) + if err != nil { + t.Fatal(err) + } + defer buf.Release() + b.BindVertexBuffer(buf, 4*4, 0) + layout, err := b.NewInputLayout(shader_input_vert, []driver.InputDesc{ + { + Type: driver.DataTypeFloat, + Size: 4, + Offset: 0, + }, + }) + if err != nil { + t.Fatal(err) + } + defer layout.Release() + b.BindInputLayout(layout) + b.DrawArrays(driver.DrawModeTriangles, 0, 3) + img := screenshot(t, b, fbo, sz) + if got := img.RGBAAt(0, 0); got != clearColExpect { + t.Errorf("got color %v, expected %v", got, clearColExpect) + } + cx, cy := 300, 400 + shaderCol := f32color.RGBA{R: .25, G: .55, B: .75, A: 1.0} + if got, exp := img.RGBAAt(cx, + cy), shaderCol.SRGB(); got != f32color.NRGBAToRGBA(exp) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(exp)) + } +} + +func TestFramebuffers(t *testing.T) { + b := newDriver(t) + sz := image.Point{X: 800, Y: 600} + fbo1 := newFBO(t, b, sz) + fbo2 := newFBO(t, b, sz) + var ( + col1 = color.NRGBA{R: 0xac, G: 0xbd, B: 0xef, A: 0xde} + col2 = color.NRGBA{R: 0xfe, G: 0xba, B: 0xbe, A: 0xca} + ) + fcol1, fcol2 := f32color.LinearFromSRGB(col1), f32color.LinearFromSRGB(col2) + b.BindFramebuffer(fbo1) + b.Clear(fcol1.Float32()) + b.BindFramebuffer(fbo2) + b.Clear(fcol2.Float32()) + img := screenshot(t, b, fbo1, sz) + if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col1) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col1)) + } + img = screenshot(t, b, fbo2, sz) + if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col2) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col2)) + } +} + +func setupFBO(t *testing.T, b driver.Device, + size image.Point) driver.Framebuffer { + fbo := newFBO(t, b, size) + b.BindFramebuffer(fbo) + // ClearColor accepts linear RGBA colors, while 8-bit colors + // are in the sRGB color space. + col := f32color.LinearFromSRGB(clearCol) + b.Clear(col.Float32()) + b.ClearDepth(0.0) + b.Viewport(0, 0, size.X, size.Y) + return fbo +} + +func newFBO(t *testing.T, b driver.Device, + size image.Point) driver.Framebuffer { + fboTex, err := b.NewTexture( + driver.TextureFormatSRGB, + size.X, size.Y, + driver.FilterNearest, driver.FilterNearest, + driver.BufferBindingFramebuffer, + ) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + fboTex.Release() + }) + const depthBits = 16 + fbo, err := b.NewFramebuffer(fboTex, depthBits) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + fbo.Release() + }) + return fbo +} + +func newDriver(t *testing.T) driver.Device { + ctx, err := newContext() + if err != nil { + t.Skipf("no context available: %v", err) + } + runtime.LockOSThread() + if err := ctx.MakeCurrent(); err != nil { + t.Fatal(err) + } + b, err := driver.NewDevice(ctx.API()) + if err != nil { + t.Fatal(err) + } + b.BeginFrame() + t.Cleanup(func() { + b.EndFrame() + ctx.ReleaseCurrent() + runtime.UnlockOSThread() + ctx.Release() + }) + return b +} + +func screenshot(t *testing.T, d driver.Device, fbo driver.Framebuffer, + size image.Point) *image.RGBA { + img, err := driver.DownloadImage(d, fbo, image.Rectangle{Max: size}) + if err != nil { + t.Fatal(err) + } + if *dumpImages { + if err := saveImage(t.Name()+".png", img); err != nil { + t.Error(err) + } + } + return img +} + +func saveImage(file string, img image.Image) error { + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return err + } + return ioutil.WriteFile(file, buf.Bytes(), 0666) +} diff --git a/gio/giold/gpu/headless/gen.go b/gio/giold/gpu/headless/gen.go new file mode 100644 index 0000000..b9e1fed --- /dev/null +++ b/gio/giold/gpu/headless/gen.go @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +//go:generate go run ../internal/convertshaders -package headless diff --git a/gio/giold/gpu/headless/headless.go b/gio/giold/gpu/headless/headless.go new file mode 100644 index 0000000..0f2e172 --- /dev/null +++ b/gio/giold/gpu/headless/headless.go @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package headless implements headless windows for rendering +// an operation list to an image. +package headless + +import ( + "image" + "image/color" + "runtime" + + "realy.lol/gio/gpu" + "realy.lol/gio/gpu/internal/driver" + "realy.lol/gio/op" +) + +// Window is a headless window. +type Window struct { + size image.Point + ctx context + dev driver.Device + gpu gpu.GPU + fboTex driver.Texture + fbo driver.Framebuffer +} + +type context interface { + API() gpu.API + MakeCurrent() error + ReleaseCurrent() + Release() +} + +// NewWindow creates a new headless window. +func NewWindow(width, height int) (*Window, error) { + ctx, err := newContext() + if err != nil { + return nil, err + } + w := &Window{ + size: image.Point{X: width, Y: height}, + ctx: ctx, + } + err = contextDo(ctx, func() error { + api := ctx.API() + dev, err := driver.NewDevice(api) + if err != nil { + return err + } + dev.Viewport(0, 0, width, height) + fboTex, err := dev.NewTexture( + driver.TextureFormatSRGB, + width, height, + driver.FilterNearest, driver.FilterNearest, + driver.BufferBindingFramebuffer, + ) + if err != nil { + return nil + } + const depthBits = 16 + fbo, err := dev.NewFramebuffer(fboTex, depthBits) + if err != nil { + fboTex.Release() + return err + } + gp, err := gpu.New(api) + if err != nil { + fbo.Release() + fboTex.Release() + return err + } + w.fboTex = fboTex + w.fbo = fbo + w.gpu = gp + w.dev = dev + return err + }) + if err != nil { + ctx.Release() + return nil, err + } + return w, nil +} + +// Release resources associated with the window. +func (w *Window) Release() { + contextDo(w.ctx, func() error { + if w.fbo != nil { + w.fbo.Release() + w.fbo = nil + } + if w.fboTex != nil { + w.fboTex.Release() + w.fboTex = nil + } + if w.gpu != nil { + w.gpu.Release() + w.gpu = nil + } + return nil + }) + if w.ctx != nil { + w.ctx.Release() + w.ctx = nil + } +} + +// Frame replace the window content and state with the +// operation list. +func (w *Window) Frame(frame *op.Ops) error { + return contextDo(w.ctx, func() error { + w.dev.BindFramebuffer(w.fbo) + w.gpu.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}) + w.gpu.Collect(w.size, frame) + return w.gpu.Frame() + }) +} + +// Screenshot returns an image with the content of the window. +func (w *Window) Screenshot() (*image.RGBA, error) { + var img *image.RGBA + err := contextDo(w.ctx, func() error { + var err error + img, err = driver.DownloadImage(w.dev, w.fbo, + image.Rectangle{Max: w.size}) + return err + }) + if err != nil { + return nil, err + } + return img, nil +} + +func contextDo(ctx context, f func() error) error { + errCh := make(chan error) + go func() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if err := ctx.MakeCurrent(); err != nil { + errCh <- err + return + } + err := f() + ctx.ReleaseCurrent() + errCh <- err + }() + return <-errCh +} diff --git a/gio/giold/gpu/headless/headless_darwin.go b/gio/giold/gpu/headless/headless_darwin.go new file mode 100644 index 0000000..75a233a --- /dev/null +++ b/gio/giold/gpu/headless/headless_darwin.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "realy.lol/gio/gpu" + _ "realy.lol/gio/internal/cocoainit" +) + +/* +#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c + +#include + +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_headless_newContext(void); +__attribute__ ((visibility ("hidden"))) void gio_headless_releaseContext(CFTypeRef ctxRef); +__attribute__ ((visibility ("hidden"))) void gio_headless_clearCurrentContext(CFTypeRef ctxRef); +__attribute__ ((visibility ("hidden"))) void gio_headless_makeCurrentContext(CFTypeRef ctxRef); +__attribute__ ((visibility ("hidden"))) void gio_headless_prepareContext(CFTypeRef ctxRef); +*/ +import "C" + +type nsContext struct { + ctx C.CFTypeRef + prepared bool +} + +func newGLContext() (context, error) { + ctx := C.gio_headless_newContext() + return &nsContext{ctx: ctx}, nil +} + +func (c *nsContext) API() gpu.API { + return gpu.OpenGL{} +} + +func (c *nsContext) MakeCurrent() error { + C.gio_headless_makeCurrentContext(c.ctx) + if !c.prepared { + C.gio_headless_prepareContext(c.ctx) + c.prepared = true + } + return nil +} + +func (c *nsContext) ReleaseCurrent() { + C.gio_headless_clearCurrentContext(c.ctx) +} + +func (d *nsContext) Release() { + if d.ctx != 0 { + C.gio_headless_releaseContext(d.ctx) + d.ctx = 0 + } +} diff --git a/gio/giold/gpu/headless/headless_egl.go b/gio/giold/gpu/headless/headless_egl.go new file mode 100644 index 0000000..7d8c1e4 --- /dev/null +++ b/gio/giold/gpu/headless/headless_egl.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build linux || freebsd || windows || openbsd +// +build linux freebsd windows openbsd + +package headless + +import ( + "realy.lol/gio/internal/egl" +) + +func newGLContext() (context, error) { + return egl.NewContext(egl.EGL_DEFAULT_DISPLAY) +} diff --git a/gio/giold/gpu/headless/headless_gl.go b/gio/giold/gpu/headless/headless_gl.go new file mode 100644 index 0000000..c00083e --- /dev/null +++ b/gio/giold/gpu/headless/headless_gl.go @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build !windows + +package headless + +func newContext() (context, error) { + return newGLContext() +} diff --git a/gio/giold/gpu/headless/headless_ios.m b/gio/giold/gpu/headless/headless_ios.m new file mode 100644 index 0000000..fd72d25 --- /dev/null +++ b/gio/giold/gpu/headless/headless_ios.m @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,ios + +@import OpenGLES; + +#include +#include "_cgo_export.h" + +void gio_headless_releaseContext(CFTypeRef ctxRef) { + CFBridgingRelease(ctxRef); +} + +CFTypeRef gio_headless_newContext(void) { + EAGLContext *ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; + if (ctx == nil) { + return nil; + } + return CFBridgingRetain(ctx); +} + +void gio_headless_clearCurrentContext(CFTypeRef ctxRef) { + [EAGLContext setCurrentContext:nil]; +} + +void gio_headless_makeCurrentContext(CFTypeRef ctxRef) { + EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; + [EAGLContext setCurrentContext:ctx]; +} + +void gio_headless_prepareContext(CFTypeRef ctxRef) { +} diff --git a/gio/giold/gpu/headless/headless_js.go b/gio/giold/gpu/headless/headless_js.go new file mode 100644 index 0000000..f79963e --- /dev/null +++ b/gio/giold/gpu/headless/headless_js.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "errors" + "syscall/js" + + "realy.lol/gio/gpu" + "realy.lol/gio/internal/gl" +) + +type jsContext struct { + ctx js.Value +} + +func newGLContext() (context, error) { + doc := js.Global().Get("document") + cnv := doc.Call("createElement", "canvas") + ctx := cnv.Call("getContext", "webgl2") + if ctx.IsNull() { + ctx = cnv.Call("getContext", "webgl") + } + if ctx.IsNull() { + return nil, errors.New("headless: webgl is not supported") + } + c := &jsContext{ + ctx: ctx, + } + return c, nil +} + +func (c *jsContext) API() gpu.API { + return gpu.OpenGL{Context: gl.Context(c.ctx)} +} + +func (c *jsContext) Release() { +} + +func (c *jsContext) ReleaseCurrent() { +} + +func (c *jsContext) MakeCurrent() error { + return nil +} diff --git a/gio/giold/gpu/headless/headless_macos.m b/gio/giold/gpu/headless/headless_macos.m new file mode 100644 index 0000000..46deb37 --- /dev/null +++ b/gio/giold/gpu/headless/headless_macos.m @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,!ios + +@import AppKit; +@import OpenGL; +@import OpenGL.GL; +@import OpenGL.GL3; + +#include +#include "_cgo_export.h" + +void gio_headless_releaseContext(CFTypeRef ctxRef) { + CFBridgingRelease(ctxRef); +} + +CFTypeRef gio_headless_newContext(void) { + NSOpenGLPixelFormatAttribute attr[] = { + NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, + NSOpenGLPFAColorSize, 24, + NSOpenGLPFAAccelerated, + // Opt-in to automatic GPU switching. CGL-only property. + kCGLPFASupportsAutomaticGraphicsSwitching, + NSOpenGLPFAAllowOfflineRenderers, + 0 + }; + NSOpenGLPixelFormat *pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr]; + if (pixFormat == nil) { + return NULL; + } + NSOpenGLContext *ctx = [[NSOpenGLContext alloc] initWithFormat:pixFormat shareContext:nil]; + return CFBridgingRetain(ctx); +} + +void gio_headless_clearCurrentContext(CFTypeRef ctxRef) { + NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; + CGLUnlockContext([ctx CGLContextObj]); + [NSOpenGLContext clearCurrentContext]; +} + +void gio_headless_makeCurrentContext(CFTypeRef ctxRef) { + NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; + [ctx makeCurrentContext]; + CGLLockContext([ctx CGLContextObj]); +} + +void gio_headless_prepareContext(CFTypeRef ctxRef) { + // Bind a default VBA to emulate OpenGL ES 2. + GLuint defVBA; + glGenVertexArrays(1, &defVBA); + glBindVertexArray(defVBA); + glEnable(GL_FRAMEBUFFER_SRGB); +} diff --git a/gio/giold/gpu/headless/headless_test.go b/gio/giold/gpu/headless/headless_test.go new file mode 100644 index 0000000..3ceec3f --- /dev/null +++ b/gio/giold/gpu/headless/headless_test.go @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "image" + "image/color" + "testing" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" +) + +func TestHeadless(t *testing.T) { + w, release := newTestWindow(t) + defer release() + + sz := w.size + col := color.NRGBA{A: 0xff, R: 0xca, G: 0xfe} + var ops op.Ops + paint.ColorOp{Color: col}.Add(&ops) + // Paint only part of the screen to avoid the glClear optimization. + paint.FillShape(&ops, col, + clip.Rect(image.Rect(0, 0, sz.X-100, sz.Y-100)).Op()) + if err := w.Frame(&ops); err != nil { + t.Fatal(err) + } + + img, err := w.Screenshot() + if err != nil { + t.Fatal(err) + } + if isz := img.Bounds().Size(); isz != sz { + t.Errorf("got %v screenshot, expected %v", isz, sz) + } + if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col)) + } +} + +func TestClipping(t *testing.T) { + w, release := newTestWindow(t) + defer release() + + col := color.NRGBA{A: 0xff, R: 0xca, G: 0xfe} + col2 := color.NRGBA{A: 0xff, R: 0x00, G: 0xfe} + var ops op.Ops + paint.ColorOp{Color: col}.Add(&ops) + clip.RRect{ + Rect: f32.Rectangle{ + Min: f32.Point{X: 50, Y: 50}, + Max: f32.Point{X: 250, Y: 250}, + }, + SE: 75, + }.Add(&ops) + paint.PaintOp{}.Add(&ops) + paint.ColorOp{Color: col2}.Add(&ops) + clip.RRect{ + Rect: f32.Rectangle{ + Min: f32.Point{X: 100, Y: 100}, + Max: f32.Point{X: 350, Y: 350}, + }, + NW: 75, + }.Add(&ops) + paint.PaintOp{}.Add(&ops) + if err := w.Frame(&ops); err != nil { + t.Fatal(err) + } + + img, err := w.Screenshot() + if err != nil { + t.Fatal(err) + } + if *dumpImages { + if err := saveImage("clip.png", img); err != nil { + t.Fatal(err) + } + } + bg := color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff} + tests := []struct { + x, y int + color color.NRGBA + }{ + {120, 120, col}, + {130, 130, col2}, + {210, 210, col2}, + {230, 230, bg}, + } + for _, test := range tests { + if got := img.RGBAAt(test.x, + test.y); got != f32color.NRGBAToRGBA(test.color) { + t.Errorf("(%d,%d): got color %v, expected %v", test.x, test.y, got, + f32color.NRGBAToRGBA(test.color)) + } + } +} + +func TestDepth(t *testing.T) { + w, release := newTestWindow(t) + defer release() + var ops op.Ops + + blue := color.NRGBA{B: 0xFF, A: 0xFF} + paint.FillShape(&ops, blue, clip.Rect(image.Rect(0, 0, 50, 100)).Op()) + red := color.NRGBA{R: 0xFF, A: 0xFF} + paint.FillShape(&ops, red, clip.Rect(image.Rect(0, 0, 100, 50)).Op()) + if err := w.Frame(&ops); err != nil { + t.Fatal(err) + } + + img, err := w.Screenshot() + if err != nil { + t.Fatal(err) + } + if *dumpImages { + if err := saveImage("depth.png", img); err != nil { + t.Fatal(err) + } + } + tests := []struct { + x, y int + color color.NRGBA + }{ + {25, 25, red}, + {75, 25, red}, + {25, 75, blue}, + } + for _, test := range tests { + if got := img.RGBAAt(test.x, + test.y); got != f32color.NRGBAToRGBA(test.color) { + t.Errorf("(%d,%d): got color %v, expected %v", test.x, test.y, got, + f32color.NRGBAToRGBA(test.color)) + } + } +} + +func newTestWindow(t *testing.T) (*Window, func()) { + t.Helper() + sz := image.Point{X: 800, Y: 600} + w, err := NewWindow(sz.X, sz.Y) + if err != nil { + t.Skipf("headless windows not supported: %v", err) + } + return w, func() { + w.Release() + } +} diff --git a/gio/giold/gpu/headless/headless_windows.go b/gio/giold/gpu/headless/headless_windows.go new file mode 100644 index 0000000..bd42d12 --- /dev/null +++ b/gio/giold/gpu/headless/headless_windows.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "unsafe" + + "realy.lol/gio/gpu" + "realy.lol/gio/internal/d3d11" +) + +type d3d11Context struct { + dev *d3d11.Device +} + +func newContext() (context, error) { + dev, ctx, _, err := d3d11.CreateDevice( + d3d11.DRIVER_TYPE_HARDWARE, + 0, + ) + if err != nil { + return nil, err + } + // Don't need it. + d3d11.IUnknownRelease(unsafe.Pointer(ctx), ctx.Vtbl.Release) + return &d3d11Context{dev: dev}, nil +} + +func (c *d3d11Context) API() gpu.API { + return gpu.Direct3D11{Device: unsafe.Pointer(c.dev)} +} + +func (c *d3d11Context) MakeCurrent() error { + return nil +} + +func (c *d3d11Context) ReleaseCurrent() { +} + +func (c *d3d11Context) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release) + c.dev = nil +} diff --git a/gio/giold/gpu/headless/shaders.go b/gio/giold/gpu/headless/shaders.go new file mode 100644 index 0000000..95e05b2 --- /dev/null +++ b/gio/giold/gpu/headless/shaders.go @@ -0,0 +1,233 @@ +// Code generated by build.go. DO NOT EDIT. + +package headless + +import "realy.lol/gio/gpu/internal/driver" + +var ( + shader_input_vert = driver.ShaderSources{ + Name: "input.vert", + Inputs: []driver.InputLocation{{Name: "position", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 4}}, + GLSL100ES: `#version 100 + +attribute vec4 position; + +void main() +{ + gl_Position = position; +} + +`, + GLSL300ES: `#version 300 es + +layout(location = 0) in vec4 position; + +void main() +{ + gl_Position = position; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +in vec4 position; + +void main() +{ + gl_Position = position; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +in vec4 position; + +void main() +{ + gl_Position = position; +} + +`, + HLSL: "DXBC\x1eĀ»\x11\xd3iX7\xd4F\xb9\xa4\xf4R\xf9J\x01\x00\x00\x00\x10\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\x9c\x00\x00\x00\xe0\x00\x00\x00\\\x01\x00\x00\xa8\x01\x00\x00\xdc\x01\x00\x00Aon9\\\x00\x00\x00\\\x00\x00\x00\x00\x02\xfe\xff4\x00\x00\x00(\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x01\x00$\x00\x00\x00\x00\x00\x00\x02\xfe\xff\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x04\x00\x00\x04\x00\x00\x03\xc0\x00\x00\xff\x90\x00\x00\xe4\xa0\x00\x00\xe4\x90\x01\x00\x00\x02\x00\x00\f\xc0\x00\x00\xe4\x90\xff\xff\x00\x00SHDR<\x00\x00\x00@\x00\x01\x00\x0f\x00\x00\x00_\x00\x00\x03\xf2\x10\x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x006\x00\x00\x05\xf2 \x10\x00\x00\x00\x00\x00F\x1e\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x0f\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00", + } + shader_simple_frag = driver.ShaderSources{ + Name: "simple.frag", + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +void main() +{ + gl_FragData[0] = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0); +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(location = 0) out vec4 fragColor; + +void main() +{ + fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +out vec4 fragColor; + +void main() +{ + fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +out vec4 fragColor; + +void main() +{ + fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0); +} + +`, + HLSL: "DXBC\xf5F\xdef$)\xa8\xbbV\xeas\xb5ks\x12r\x01\x00\x00\x00\xdc\x01\x00\x00\x06\x00\x00\x008\x00\x00\x00\x90\x00\x00\x00\xd0\x00\x00\x00L\x01\x00\x00\x98\x01\x00\x00\xa8\x01\x00\x00Aon9P\x00\x00\x00P\x00\x00\x00\x00\x02\xff\xff,\x00\x00\x00$\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x80>\xcd\xcc\f?\x00\x00@?\x00\x00\x80?\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\xa0\xff\xff\x00\x00SHDR8\x00\x00\x00@\x00\x00\x00\x0e\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x006\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80>\xcd\xcc\f?\x00\x00@?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\b\x00\x00\x00\x00\x00\x00\x00\b\x00\x00\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + } + shader_simple_vert = driver.ShaderSources{ + Name: "simple.vert", + GLSL100ES: `#version 100 + +void main() +{ + float x; + float y; + if (gl_VertexID == 0) + { + x = 0.0; + y = 0.5; + } + else + { + if (gl_VertexID == 1) + { + x = 0.5; + y = -0.5; + } + else + { + x = -0.5; + y = -0.5; + } + } + gl_Position = vec4(x, y, 0.5, 1.0); +} + +`, + GLSL300ES: `#version 300 es + +void main() +{ + float x; + float y; + if (gl_VertexID == 0) + { + x = 0.0; + y = 0.5; + } + else + { + if (gl_VertexID == 1) + { + x = 0.5; + y = -0.5; + } + else + { + x = -0.5; + y = -0.5; + } + } + gl_Position = vec4(x, y, 0.5, 1.0); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +void main() +{ + float x; + float y; + if (gl_VertexID == 0) + { + x = 0.0; + y = 0.5; + } + else + { + if (gl_VertexID == 1) + { + x = 0.5; + y = -0.5; + } + else + { + x = -0.5; + y = -0.5; + } + } + gl_Position = vec4(x, y, 0.5, 1.0); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +void main() +{ + float x; + float y; + if (gl_VertexID == 0) + { + x = 0.0; + y = 0.5; + } + else + { + if (gl_VertexID == 1) + { + x = 0.5; + y = -0.5; + } + else + { + x = -0.5; + y = -0.5; + } + } + gl_Position = vec4(x, y, 0.5, 1.0); +} + +`, + HLSL: "DXBC\xc8 \\\"\xec\xe9\xb2)@\xdf|Z(\xea\f\xb8\x01\x00\x00\x00H\x02\x00\x00\x05\x00\x00\x004\x00\x00\x00\x80\x00\x00\x00\xb4\x00\x00\x00\xe8\x00\x00\x00\xcc\x01\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00SV_VertexID\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00SHDR\xdc\x00\x00\x00@\x00\x01\x007\x00\x00\x00`\x00\x00\x04\x12\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00 \x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x01\x00\x00\x007\x00\x00\x0f2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x007\x00\x00\f2 \x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x05\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + } +) diff --git a/gio/giold/gpu/headless/shaders/input.vert b/gio/giold/gpu/headless/shaders/input.vert new file mode 100644 index 0000000..ed9a4bd --- /dev/null +++ b/gio/giold/gpu/headless/shaders/input.vert @@ -0,0 +1,11 @@ +#version 310 es + +// SPDX-License-Identifier: Unlicense OR MIT + +precision highp float; + +layout(location=0) in vec4 position; + +void main() { + gl_Position = position; +} diff --git a/gio/giold/gpu/headless/shaders/simple.frag b/gio/giold/gpu/headless/shaders/simple.frag new file mode 100644 index 0000000..4614f33 --- /dev/null +++ b/gio/giold/gpu/headless/shaders/simple.frag @@ -0,0 +1,11 @@ +#version 310 es + +// SPDX-License-Identifier: Unlicense OR MIT + +precision mediump float; + +layout(location = 0) out vec4 fragColor; + +void main() { + fragColor = vec4(.25, .55, .75, 1.0); +} diff --git a/gio/giold/gpu/headless/shaders/simple.vert b/gio/giold/gpu/headless/shaders/simple.vert new file mode 100644 index 0000000..a226816 --- /dev/null +++ b/gio/giold/gpu/headless/shaders/simple.vert @@ -0,0 +1,20 @@ +#version 310 es + +// SPDX-License-Identifier: Unlicense OR MIT + +precision highp float; + +void main() { + float x, y; + if (gl_VertexIndex == 0) { + x = 0.0; + y = .5; + } else if (gl_VertexIndex == 1) { + x = .5; + y = -.5; + } else { + x = -.5; + y = -.5; + } + gl_Position = vec4(x, y, 0.5, 1.0); +} diff --git a/gio/giold/gpu/internal/convertshaders/glslvalidate.go b/gio/giold/gpu/internal/convertshaders/glslvalidate.go new file mode 100644 index 0000000..0d02a29 --- /dev/null +++ b/gio/giold/gpu/internal/convertshaders/glslvalidate.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os/exec" + "path/filepath" +) + +// GLSLValidator is OpenGL reference compiler. +type GLSLValidator struct { + Bin string + WorkDir WorkDir +} + +func NewGLSLValidator() *GLSLValidator { return &GLSLValidator{Bin: "glslangValidator"} } + +// Convert converts a glsl shader to spirv. +func (glsl *GLSLValidator) Convert(path, variant string, hlsl bool, input []byte) ([]byte, error) { + base := glsl.WorkDir.Path(filepath.Base(path), variant) + pathout := base + ".out" + + cmd := exec.Command(glsl.Bin, + "--stdin", + "-I"+filepath.Dir(path), + "-V", // OpenGL ES 3.1. + "-w", // Suppress warnings. + "-S", filepath.Ext(path)[1:], + "-o", pathout, + ) + if hlsl { + cmd.Args = append(cmd.Args, "-DHLSL") + } + cmd.Stdin = bytes.NewBuffer(input) + + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("%s\nfailed to run %v: %w", out, cmd.Args, err) + } + + compiled, err := ioutil.ReadFile(pathout) + if err != nil { + return nil, fmt.Errorf("unable to read output %q: %w", pathout, err) + } + + return compiled, nil +} diff --git a/gio/giold/gpu/internal/convertshaders/hlsl.go b/gio/giold/gpu/internal/convertshaders/hlsl.go new file mode 100644 index 0000000..a007925 --- /dev/null +++ b/gio/giold/gpu/internal/convertshaders/hlsl.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +// FXC is hlsl compiler that targets ShaderModel 5.x and lower. +type FXC struct { + Bin string + WorkDir WorkDir +} + +func NewFXC() *FXC { return &FXC{Bin: "fxc.exe"} } + +// Compile compiles the input shader. +func (fxc *FXC) Compile(path, variant string, input []byte, entryPoint string, profileVersion string) (string, error) { + base := fxc.WorkDir.Path(filepath.Base(path), variant, profileVersion) + pathin := base + ".in" + pathout := base + ".out" + result := pathout + + if err := fxc.WorkDir.WriteFile(pathin, input); err != nil { + return "", fmt.Errorf("unable to write shader to disk: %w", err) + } + + cmd := exec.Command(fxc.Bin) + if runtime.GOOS != "windows" { + cmd = exec.Command("wine", fxc.Bin) + if err := winepath(&pathin, &pathout); err != nil { + return "", err + } + } + + var profile string + switch filepath.Ext(path) { + case ".frag": + profile = "ps_" + profileVersion + case ".vert": + profile = "vs_" + profileVersion + case ".comp": + profile = "cs_" + profileVersion + default: + return "", fmt.Errorf("unrecognized shader type %s", path) + } + + cmd.Args = append(cmd.Args, + "/Fo", pathout, + "/T", profile, + "/E", entryPoint, + pathin, + ) + + output, err := cmd.CombinedOutput() + if err != nil { + info := "" + if runtime.GOOS != "windows" { + info = "If the fxc tool cannot be found, set WINEPATH to the Windows path for the Windows SDK.\n" + } + return "", fmt.Errorf("%s\n%sfailed to run %v: %w", output, info, cmd.Args, err) + } + + compiled, err := ioutil.ReadFile(result) + if err != nil { + return "", fmt.Errorf("unable to read output %q: %w", pathout, err) + } + + return string(compiled), nil +} + +// DXC is hlsl compiler that targets ShaderModel 6.0 and newer. +type DXC struct { + Bin string + WorkDir WorkDir +} + +func NewDXC() *DXC { return &DXC{Bin: "dxc"} } + +// Compile compiles the input shader. +func (dxc *DXC) Compile(path, variant string, input []byte, entryPoint string, profile string) (string, error) { + base := dxc.WorkDir.Path(filepath.Base(path), variant, profile) + pathin := base + ".in" + pathout := base + ".out" + result := pathout + + if err := dxc.WorkDir.WriteFile(pathin, input); err != nil { + return "", fmt.Errorf("unable to write shader to disk: %w", err) + } + + cmd := exec.Command(dxc.Bin) + + cmd.Args = append(cmd.Args, + "-Fo", pathout, + "-T", profile, + "-E", entryPoint, + "-Qstrip_reflect", + pathin, + ) + + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("%s\nfailed to run %v: %w", output, cmd.Args, err) + } + + compiled, err := ioutil.ReadFile(result) + if err != nil { + return "", fmt.Errorf("unable to read output %q: %w", pathout, err) + } + + return string(compiled), nil +} + +// winepath uses the winepath tool to convert a paths to Windows format. +// The returned path can be used as arguments for Windows command line tools. +func winepath(paths ...*string) error { + winepath := exec.Command("winepath", "--windows") + for _, path := range paths { + winepath.Args = append(winepath.Args, *path) + } + // Use a pipe instead of Output, because winepath may have left wineserver + // running for several seconds as a grandchild. + out, err := winepath.StdoutPipe() + if err != nil { + return fmt.Errorf("unable to start winepath: %w", err) + } + if err := winepath.Start(); err != nil { + return fmt.Errorf("unable to start winepath: %w", err) + } + var buf bytes.Buffer + if _, err := io.Copy(&buf, out); err != nil { + return fmt.Errorf("unable to run winepath: %w", err) + } + winPaths := strings.Split(strings.TrimSpace(buf.String()), "\n") + for i, path := range paths { + *path = winPaths[i] + } + return nil +} diff --git a/gio/giold/gpu/internal/convertshaders/main.go b/gio/giold/gpu/internal/convertshaders/main.go new file mode 100644 index 0000000..a0589dc --- /dev/null +++ b/gio/giold/gpu/internal/convertshaders/main.go @@ -0,0 +1,436 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "text/template" + + "realy.lol/gio/gpu/internal/driver" +) + +func main() { + packageName := flag.String("package", "", "specify Go package name") + workdir := flag.String("work", "", + "temporary working directory (default TEMP)") + shadersDir := flag.String("dir", "shaders", "shaders directory") + directCompute := flag.Bool("directcompute", false, + "enable compiling DirectCompute shaders") + + flag.Parse() + + var work WorkDir + cleanup := func() {} + if *workdir == "" { + tempdir, err := ioutil.TempDir("", "shader-convert") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to create tempdir: %v\n", err) + os.Exit(1) + } + cleanup = func() { os.RemoveAll(tempdir) } + defer cleanup() + + work = WorkDir(tempdir) + } else { + if abs, err := filepath.Abs(*workdir); err == nil { + *workdir = abs + } + work = WorkDir(*workdir) + } + + var out bytes.Buffer + conv := NewConverter(work, *packageName, *shadersDir, *directCompute) + if err := conv.Run(&out); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + cleanup() + os.Exit(1) + } + + if err := ioutil.WriteFile("shaders.go", out.Bytes(), 0644); err != nil { + fmt.Fprintf(os.Stderr, "failed to create shaders: %v\n", err) + cleanup() + os.Exit(1) + } + + cmd := exec.Command("gofmt", "-s", "-w", "shaders.go") + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "formatting shaders.go failed: %v\n", err) + cleanup() + os.Exit(1) + } +} + +type Converter struct { + workDir WorkDir + shadersDir string + directCompute bool + + packageName string + + glslvalidator *GLSLValidator + spirv *SPIRVCross + fxc *FXC +} + +func NewConverter(workDir WorkDir, packageName, shadersDir string, + directCompute bool) *Converter { + if abs, err := filepath.Abs(shadersDir); err == nil { + shadersDir = abs + } + + conv := &Converter{} + conv.workDir = workDir + conv.shadersDir = shadersDir + conv.directCompute = directCompute + + conv.packageName = packageName + + conv.glslvalidator = NewGLSLValidator() + conv.spirv = NewSPIRVCross() + conv.fxc = NewFXC() + + verifyBinaryPath(&conv.glslvalidator.Bin) + verifyBinaryPath(&conv.spirv.Bin) + // We cannot check fxc since it may depend on wine. + + conv.glslvalidator.WorkDir = workDir.Dir("glslvalidator") + conv.fxc.WorkDir = workDir.Dir("fxc") + conv.spirv.WorkDir = workDir.Dir("spirv") + + return conv +} + +func verifyBinaryPath(bin *string) { + new, err := exec.LookPath(*bin) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to find %q: %v\n", *bin, err) + } else { + *bin = new + } +} + +func (conv *Converter) Run(out io.Writer) error { + shaders, err := filepath.Glob(filepath.Join(conv.shadersDir, "*")) + if len(shaders) == 0 || err != nil { + return fmt.Errorf("failed to list shaders in %q: %w", conv.shadersDir, + err) + } + + sort.Strings(shaders) + + var workers Workers + + type ShaderResult struct { + Path string + Shaders []driver.ShaderSources + Error error + } + shaderResults := make([]ShaderResult, len(shaders)) + + for i, shaderPath := range shaders { + i, shaderPath := i, shaderPath + + switch filepath.Ext(shaderPath) { + case ".vert", ".frag": + workers.Go(func() { + shaders, err := conv.Shader(shaderPath) + shaderResults[i] = ShaderResult{ + Path: shaderPath, + Shaders: shaders, + Error: err, + } + }) + case ".comp": + workers.Go(func() { + shaders, err := conv.ComputeShader(shaderPath) + shaderResults[i] = ShaderResult{ + Path: shaderPath, + Shaders: shaders, + Error: err, + } + }) + default: + continue + } + } + + workers.Wait() + + var allErrors string + for _, r := range shaderResults { + if r.Error != nil { + if len(allErrors) > 0 { + allErrors += "\n\n" + } + allErrors += "--- " + r.Path + " --- \n\n" + r.Error.Error() + "\n" + } + } + if len(allErrors) > 0 { + return errors.New(allErrors) + } + + fmt.Fprintf(out, "// Code generated by build.go. DO NOT EDIT.\n\n") + fmt.Fprintf(out, "package %s\n\n", conv.packageName) + fmt.Fprintf(out, "import %q\n\n", "realy.lol/gio/gpu/internal/driver") + + fmt.Fprintf(out, "var (\n") + + for _, r := range shaderResults { + if len(r.Shaders) == 0 { + continue + } + + name := filepath.Base(r.Path) + name = strings.ReplaceAll(name, ".", "_") + fmt.Fprintf(out, "\tshader_%s = ", name) + + multiVariant := len(r.Shaders) > 1 + if multiVariant { + fmt.Fprintf(out, "[...]driver.ShaderSources{\n") + } + + for _, src := range r.Shaders { + fmt.Fprintf(out, "driver.ShaderSources{\n") + fmt.Fprintf(out, "Name: %#v,\n", src.Name) + if len(src.Inputs) > 0 { + fmt.Fprintf(out, "Inputs: %#v,\n", src.Inputs) + } + if u := src.Uniforms; len(u.Blocks) > 0 { + fmt.Fprintf(out, "Uniforms: driver.UniformsReflection{\n") + fmt.Fprintf(out, "Blocks: %#v,\n", u.Blocks) + fmt.Fprintf(out, "Locations: %#v,\n", u.Locations) + fmt.Fprintf(out, "Size: %d,\n", u.Size) + fmt.Fprintf(out, "},\n") + } + if len(src.Textures) > 0 { + fmt.Fprintf(out, "Textures: %#v,\n", src.Textures) + } + if len(src.GLSL100ES) > 0 { + fmt.Fprintf(out, "GLSL100ES: `%s`,\n", src.GLSL100ES) + } + if len(src.GLSL300ES) > 0 { + fmt.Fprintf(out, "GLSL300ES: `%s`,\n", src.GLSL300ES) + } + if len(src.GLSL310ES) > 0 { + fmt.Fprintf(out, "GLSL310ES: `%s`,\n", src.GLSL310ES) + } + if len(src.GLSL130) > 0 { + fmt.Fprintf(out, "GLSL130: `%s`,\n", src.GLSL130) + } + if len(src.GLSL150) > 0 { + fmt.Fprintf(out, "GLSL150: `%s`,\n", src.GLSL150) + } + if len(src.HLSL) > 0 { + fmt.Fprintf(out, "HLSL: %q,\n", src.HLSL) + } + fmt.Fprintf(out, "}") + if multiVariant { + fmt.Fprintf(out, ",") + } + fmt.Fprintf(out, "\n") + } + if multiVariant { + fmt.Fprintf(out, "}\n") + } + } + fmt.Fprintf(out, ")\n") + + return nil +} + +func (conv *Converter) Shader(shaderPath string) ([]driver.ShaderSources, + error) { + type Variant struct { + FetchColorExpr string + Header string + } + variantArgs := [...]Variant{ + { + FetchColorExpr: `_color.color`, + Header: `layout(binding=0) uniform Color { vec4 color; } _color;`, + }, + { + FetchColorExpr: `mix(_gradient.color1, _gradient.color2, clamp(vUV.x, 0.0, 1.0))`, + Header: `layout(binding=0) uniform Gradient { vec4 color1; vec4 color2; } _gradient;`, + }, + { + FetchColorExpr: `texture(tex, vUV)`, + Header: `layout(binding=0) uniform sampler2D tex;`, + }, + } + + shaderTemplate, err := template.ParseFiles(shaderPath) + if err != nil { + return nil, fmt.Errorf("failed to parse template %q: %w", shaderPath, + err) + } + + var variants []driver.ShaderSources + for i, variantArg := range variantArgs { + variantName := strconv.Itoa(i) + var buf bytes.Buffer + err := shaderTemplate.Execute(&buf, variantArg) + if err != nil { + return nil, fmt.Errorf("failed to execute template %q with %#v: %w", + shaderPath, variantArg, err) + } + + var sources driver.ShaderSources + sources.Name = filepath.Base(shaderPath) + + // Ignore error; some shaders are not meant to run in GLSL 1.00. + sources.GLSL100ES, _, _ = conv.ShaderVariant(shaderPath, variantName, + buf.Bytes(), "es", "100") + + var metadata Metadata + sources.GLSL300ES, metadata, err = conv.ShaderVariant(shaderPath, + variantName, buf.Bytes(), "es", "300") + if err != nil { + return nil, fmt.Errorf("failed to convert GLSL300ES:\n%w", err) + } + + sources.GLSL130, _, err = conv.ShaderVariant(shaderPath, variantName, + buf.Bytes(), "glsl", "130") + if err != nil { + return nil, fmt.Errorf("failed to convert GLSL130:\n%w", err) + } + + hlsl, _, err := conv.ShaderVariant(shaderPath, variantName, buf.Bytes(), + "hlsl", "40") + if err != nil { + return nil, fmt.Errorf("failed to convert HLSL:\n%w", err) + } + sources.HLSL, err = conv.fxc.Compile(shaderPath, variantName, + []byte(hlsl), "main", "4_0_level_9_1") + if err != nil { + // Attempt shader model 4.0. Only the gpu/headless + // test shaders use features not supported by level + // 9.1. + sources.HLSL, err = conv.fxc.Compile(shaderPath, variantName, + []byte(hlsl), "main", "4_0") + if err != nil { + return nil, fmt.Errorf("failed to compile HLSL: %w", err) + } + } + + sources.GLSL150, _, err = conv.ShaderVariant(shaderPath, variantName, + buf.Bytes(), "glsl", "150") + if err != nil { + return nil, fmt.Errorf("failed to convert GLSL150:\n%w", err) + } + + sources.Uniforms = metadata.Uniforms + sources.Inputs = metadata.Inputs + sources.Textures = metadata.Textures + + variants = append(variants, sources) + } + + // If the shader don't use the variant arguments, output only a single version. + if variants[0].GLSL100ES == variants[1].GLSL100ES { + variants = variants[:1] + } + + return variants, nil +} + +func (conv *Converter) ShaderVariant(shaderPath, variant string, src []byte, + lang, profile string) (string, Metadata, error) { + spirv, err := conv.glslvalidator.Convert(shaderPath, variant, + lang == "hlsl", src) + if err != nil { + return "", Metadata{}, fmt.Errorf("failed to generate SPIR-V for %q: %w", + shaderPath, err) + } + + dst, err := conv.spirv.Convert(shaderPath, variant, spirv, lang, profile) + if err != nil { + return "", Metadata{}, fmt.Errorf("failed to convert shader %q: %w", + shaderPath, err) + } + + meta, err := conv.spirv.Metadata(shaderPath, variant, spirv) + if err != nil { + return "", Metadata{}, fmt.Errorf("failed to extract metadata for shader %q: %w", + shaderPath, err) + } + + return dst, meta, nil +} + +func (conv *Converter) ComputeShader(shaderPath string) ([]driver.ShaderSources, + error) { + shader, err := ioutil.ReadFile(shaderPath) + if err != nil { + return nil, fmt.Errorf("failed to load shader %q: %w", shaderPath, err) + } + + spirv, err := conv.glslvalidator.Convert(shaderPath, "", false, shader) + if err != nil { + return nil, fmt.Errorf("failed to convert compute shader %q: %w", + shaderPath, err) + } + + var sources driver.ShaderSources + sources.Name = filepath.Base(shaderPath) + + sources.GLSL310ES, err = conv.spirv.Convert(shaderPath, "", spirv, "es", + "310") + if err != nil { + return nil, fmt.Errorf("failed to convert es compute shader %q: %w", + shaderPath, err) + } + sources.GLSL310ES = unixLineEnding(sources.GLSL310ES) + + hlslSource, err := conv.spirv.Convert(shaderPath, "", spirv, "hlsl", "50") + if err != nil { + return nil, fmt.Errorf("failed to convert hlsl compute shader %q: %w", + shaderPath, err) + } + + dxil, err := conv.fxc.Compile(shaderPath, "0", []byte(hlslSource), "main", + "5_0") + if err != nil { + return nil, fmt.Errorf("failed to compile hlsl compute shader %q: %w", + shaderPath, err) + } + if conv.directCompute { + sources.HLSL = dxil + } + + return []driver.ShaderSources{sources}, nil +} + +// Workers implements wait group with synchronous logging. +type Workers struct { + running sync.WaitGroup +} + +func (lg *Workers) Go(fn func()) { + lg.running.Add(1) + go func() { + defer lg.running.Done() + fn() + }() +} + +func (lg *Workers) Wait() { + lg.running.Wait() +} + +func unixLineEnding(s string) string { + return strings.ReplaceAll(s, "\r\n", "\n") +} diff --git a/gio/giold/gpu/internal/convertshaders/spirvcross.go b/gio/giold/gpu/internal/convertshaders/spirvcross.go new file mode 100644 index 0000000..4252469 --- /dev/null +++ b/gio/giold/gpu/internal/convertshaders/spirvcross.go @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "encoding/json" + "fmt" + "os/exec" + "path/filepath" + "sort" + "strings" + + "realy.lol/gio/gpu/internal/driver" +) + +// Metadata contains reflection data about a shader. +type Metadata struct { + Uniforms driver.UniformsReflection + Inputs []driver.InputLocation + Textures []driver.TextureBinding +} + +// SPIRVCross cross-compiles spirv shaders to es, hlsl and others. +type SPIRVCross struct { + Bin string + WorkDir WorkDir +} + +func NewSPIRVCross() *SPIRVCross { return &SPIRVCross{Bin: "spirv-cross"} } + +// Convert converts compute shader from spirv format to a target format. +func (spirv *SPIRVCross) Convert(path, variant string, shader []byte, + target, version string) (string, error) { + base := spirv.WorkDir.Path(filepath.Base(path), variant) + + if err := spirv.WorkDir.WriteFile(base, shader); err != nil { + return "", fmt.Errorf("unable to write shader to disk: %w", err) + } + + var cmd *exec.Cmd + switch target { + case "glsl": + cmd = exec.Command(spirv.Bin, + "--no-es", + "--version", version, + ) + case "es": + cmd = exec.Command(spirv.Bin, + "--es", + "--version", version, + ) + case "hlsl": + cmd = exec.Command(spirv.Bin, + "--hlsl", + "--shader-model", version, + ) + default: + return "", fmt.Errorf("unknown target %q", target) + } + cmd.Args = append(cmd.Args, "--no-420pack-extension", base) + + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("%s\nfailed to run %v: %w", out, cmd.Args, err) + } + s := string(out) + if target != "hlsl" { + // Strip Windows \r in line endings. + s = unixLineEnding(s) + } + + return s, nil +} + +// Metadata extracts metadata for a SPIR-V shader. +func (spirv *SPIRVCross) Metadata(path, variant string, + shader []byte) (Metadata, error) { + base := spirv.WorkDir.Path(filepath.Base(path), variant) + + if err := spirv.WorkDir.WriteFile(base, shader); err != nil { + return Metadata{}, fmt.Errorf("unable to write shader to disk: %w", err) + } + + cmd := exec.Command(spirv.Bin, + base, + "--reflect", + ) + + out, err := cmd.Output() + if err != nil { + return Metadata{}, fmt.Errorf("failed to run %v: %w", cmd.Args, err) + } + + meta, err := parseMetadata(out) + if err != nil { + return Metadata{}, fmt.Errorf("%s\nfailed to parse metadata: %w", out, + err) + } + + return meta, nil +} + +func parseMetadata(data []byte) (Metadata, error) { + var reflect struct { + Types map[string]struct { + Name string `json:"name"` + Members []struct { + Name string `json:"name"` + Type string `json:"type"` + Offset int `json:"offset"` + } `json:"members"` + } `json:"types"` + Inputs []struct { + Name string `json:"name"` + Type string `json:"type"` + Location int `json:"location"` + } `json:"inputs"` + Textures []struct { + Name string `json:"name"` + Type string `json:"type"` + Set int `json:"set"` + Binding int `json:"binding"` + } `json:"textures"` + UBOs []struct { + Name string `json:"name"` + Type string `json:"type"` + BlockSize int `json:"block_size"` + Set int `json:"set"` + Binding int `json:"binding"` + } `json:"ubos"` + } + if err := json.Unmarshal(data, &reflect); err != nil { + return Metadata{}, fmt.Errorf("failed to parse reflection data: %w", + err) + } + + var m Metadata + + for _, input := range reflect.Inputs { + dataType, dataSize, err := parseDataType(input.Type) + if err != nil { + return Metadata{}, fmt.Errorf("parseReflection: %v", err) + } + m.Inputs = append(m.Inputs, driver.InputLocation{ + Name: input.Name, + Location: input.Location, + Semantic: "TEXCOORD", + SemanticIndex: input.Location, + Type: dataType, + Size: dataSize, + }) + } + + sort.Slice(m.Inputs, func(i, j int) bool { + return m.Inputs[i].Location < m.Inputs[j].Location + }) + + blockOffset := 0 + for _, block := range reflect.UBOs { + m.Uniforms.Blocks = append(m.Uniforms.Blocks, driver.UniformBlock{ + Name: block.Name, + Binding: block.Binding, + }) + t := reflect.Types[block.Type] + // By convention uniform block variables are named by prepending an underscore + // and converting to lowercase. + blockVar := "_" + strings.ToLower(block.Name) + for _, member := range t.Members { + dataType, size, err := parseDataType(member.Type) + if err != nil { + return Metadata{}, fmt.Errorf("failed to parse reflection data: %v", + err) + } + m.Uniforms.Locations = append(m.Uniforms.Locations, + driver.UniformLocation{ + Name: fmt.Sprintf("%s.%s", blockVar, member.Name), + Type: dataType, + Size: size, + Offset: blockOffset + member.Offset, + }) + } + blockOffset += block.BlockSize + } + m.Uniforms.Size = blockOffset + + for _, texture := range reflect.Textures { + m.Textures = append(m.Textures, driver.TextureBinding{ + Name: texture.Name, + Binding: texture.Binding, + }) + } + + // return m, fmt.Errorf("not yet!: %+v", reflect) + return m, nil +} + +func parseDataType(t string) (driver.DataType, int, error) { + switch t { + case "float": + return driver.DataTypeFloat, 1, nil + case "vec2": + return driver.DataTypeFloat, 2, nil + case "vec3": + return driver.DataTypeFloat, 3, nil + case "vec4": + return driver.DataTypeFloat, 4, nil + case "int": + return driver.DataTypeInt, 1, nil + case "int2": + return driver.DataTypeInt, 2, nil + case "int3": + return driver.DataTypeInt, 3, nil + case "int4": + return driver.DataTypeInt, 4, nil + default: + return 0, 0, fmt.Errorf("unsupported input data type: %s", t) + } +} diff --git a/gio/giold/gpu/internal/convertshaders/workdir.go b/gio/giold/gpu/internal/convertshaders/workdir.go new file mode 100644 index 0000000..4c1c092 --- /dev/null +++ b/gio/giold/gpu/internal/convertshaders/workdir.go @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +type WorkDir string + +func (wd WorkDir) Dir(path string) WorkDir { + dirname := filepath.Join(string(wd), path) + if err := os.Mkdir(dirname, 0755); err != nil { + if !os.IsExist(err) { + fmt.Fprintf(os.Stderr, "failed to create %q: %v\n", dirname, err) + } + } + return WorkDir(dirname) +} + +func (wd WorkDir) Path(path ...string) (fullpath string) { + return filepath.Join(string(wd), strings.Join(path, ".")) +} + +func (wd WorkDir) WriteFile(path string, data []byte) error { + err := ioutil.WriteFile(path, data, 0644) + if err != nil { + return fmt.Errorf("unable to create %v: %w", path, err) + } + return nil +} diff --git a/gio/giold/gpu/internal/d3d11/d3d11.go b/gio/giold/gpu/internal/d3d11/d3d11.go new file mode 100644 index 0000000..3ddf7c3 --- /dev/null +++ b/gio/giold/gpu/internal/d3d11/d3d11.go @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// This file exists so this package builds on non-Windows platforms. + +package d3d11 diff --git a/gio/giold/gpu/internal/d3d11/d3d11_windows.go b/gio/giold/gpu/internal/d3d11/d3d11_windows.go new file mode 100644 index 0000000..217ea98 --- /dev/null +++ b/gio/giold/gpu/internal/d3d11/d3d11_windows.go @@ -0,0 +1,787 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package d3d11 + +import ( + "errors" + "fmt" + "image" + "math" + "reflect" + "unsafe" + + "golang.org/x/sys/windows" + + "realy.lol/gio/gpu/internal/driver" + "realy.lol/gio/internal/d3d11" +) + +type Backend struct { + dev *d3d11.Device + ctx *d3d11.DeviceContext + + // Temporary storage to avoid garbage. + clearColor [4]float32 + viewport d3d11.VIEWPORT + depthState depthState + blendState blendState + + // Current program. + prog *Program + + caps driver.Caps + + // fbo is the currently bound fbo. + fbo *Framebuffer + + floatFormat uint32 + + // cached state objects. + depthStates map[depthState]*d3d11.DepthStencilState + blendStates map[blendState]*d3d11.BlendState +} + +type blendState struct { + enable bool + sfactor driver.BlendFactor + dfactor driver.BlendFactor +} + +type depthState struct { + enable bool + mask bool + fn driver.DepthFunc +} + +type Texture struct { + backend *Backend + format uint32 + bindings driver.BufferBinding + tex *d3d11.Texture2D + sampler *d3d11.SamplerState + resView *d3d11.ShaderResourceView + width int + height int +} + +type Program struct { + backend *Backend + + vert struct { + shader *d3d11.VertexShader + uniforms *Buffer + } + frag struct { + shader *d3d11.PixelShader + uniforms *Buffer + } +} + +type Framebuffer struct { + dev *d3d11.Device + ctx *d3d11.DeviceContext + format uint32 + resource *d3d11.Resource + renderTarget *d3d11.RenderTargetView + depthView *d3d11.DepthStencilView + foreign bool +} + +type Buffer struct { + backend *Backend + bind uint32 + buf *d3d11.Buffer + immutable bool +} + +type InputLayout struct { + layout *d3d11.InputLayout +} + +func init() { + driver.NewDirect3D11Device = newDirect3D11Device +} + +func detectFloatFormat(dev *d3d11.Device) (uint32, bool) { + formats := []uint32{ + d3d11.DXGI_FORMAT_R16_FLOAT, + d3d11.DXGI_FORMAT_R32_FLOAT, + d3d11.DXGI_FORMAT_R16G16_FLOAT, + d3d11.DXGI_FORMAT_R32G32_FLOAT, + // These last two are really wasteful, but c'est la vie. + d3d11.DXGI_FORMAT_R16G16B16A16_FLOAT, + d3d11.DXGI_FORMAT_R32G32B32A32_FLOAT, + } + for _, format := range formats { + need := uint32(d3d11.FORMAT_SUPPORT_TEXTURE2D | d3d11.FORMAT_SUPPORT_RENDER_TARGET) + if support, _ := dev.CheckFormatSupport(format); support&need == need { + return format, true + } + } + return 0, false +} + +func newDirect3D11Device(api driver.Direct3D11) (driver.Device, error) { + dev := (*d3d11.Device)(api.Device) + b := &Backend{ + dev: dev, + ctx: dev.GetImmediateContext(), + caps: driver.Caps{ + MaxTextureSize: 2048, // 9.1 maximum + }, + depthStates: make(map[depthState]*d3d11.DepthStencilState), + blendStates: make(map[blendState]*d3d11.BlendState), + } + featLvl := dev.GetFeatureLevel() + if featLvl < d3d11.FEATURE_LEVEL_9_1 { + d3d11.IUnknownRelease(unsafe.Pointer(dev), dev.Vtbl.Release) + d3d11.IUnknownRelease(unsafe.Pointer(b.ctx), b.ctx.Vtbl.Release) + return nil, fmt.Errorf("d3d11: feature level too low: %d", featLvl) + } + switch { + case featLvl >= d3d11.FEATURE_LEVEL_11_0: + b.caps.MaxTextureSize = 16384 + case featLvl >= d3d11.FEATURE_LEVEL_9_3: + b.caps.MaxTextureSize = 4096 + } + if fmt, ok := detectFloatFormat(dev); ok { + b.floatFormat = fmt + b.caps.Features |= driver.FeatureFloatRenderTargets + } + // Enable depth mask to match OpenGL. + b.depthState.mask = true + // Disable backface culling to match OpenGL. + state, err := dev.CreateRasterizerState(&d3d11.RASTERIZER_DESC{ + CullMode: d3d11.CULL_NONE, + FillMode: d3d11.FILL_SOLID, + DepthClipEnable: 1, + }) + if err != nil { + return nil, err + } + defer d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release) + b.ctx.RSSetState(state) + return b, nil +} + +func (b *Backend) BeginFrame() driver.Framebuffer { + renderTarget, depthView := b.ctx.OMGetRenderTargets() + // Assume someone else is holding on to the render targets. + if renderTarget != nil { + d3d11.IUnknownRelease(unsafe.Pointer(renderTarget), + renderTarget.Vtbl.Release) + } + if depthView != nil { + d3d11.IUnknownRelease(unsafe.Pointer(depthView), depthView.Vtbl.Release) + } + return &Framebuffer{ctx: b.ctx, dev: b.dev, renderTarget: renderTarget, + depthView: depthView, foreign: true} +} + +func (b *Backend) EndFrame() { +} + +func (b *Backend) Caps() driver.Caps { + return b.caps +} + +func (b *Backend) NewTimer() driver.Timer { + panic("timers not supported") +} + +func (b *Backend) IsTimeContinuous() bool { + panic("timers not supported") +} + +func (b *Backend) Release() { + for _, state := range b.depthStates { + d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release) + } + for _, state := range b.blendStates { + d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release) + } + d3d11.IUnknownRelease(unsafe.Pointer(b.ctx), b.ctx.Vtbl.Release) + *b = Backend{} +} + +func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, + minFilter, magFilter driver.TextureFilter, + bindings driver.BufferBinding) (driver.Texture, error) { + var d3dfmt uint32 + switch format { + case driver.TextureFormatFloat: + d3dfmt = b.floatFormat + case driver.TextureFormatSRGB: + d3dfmt = d3d11.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB + default: + return nil, fmt.Errorf("unsupported texture format %d", format) + } + tex, err := b.dev.CreateTexture2D(&d3d11.TEXTURE2D_DESC{ + Width: uint32(width), + Height: uint32(height), + MipLevels: 1, + ArraySize: 1, + Format: d3dfmt, + SampleDesc: d3d11.DXGI_SAMPLE_DESC{ + Count: 1, + Quality: 0, + }, + BindFlags: convBufferBinding(bindings), + }) + if err != nil { + return nil, err + } + var ( + sampler *d3d11.SamplerState + resView *d3d11.ShaderResourceView + ) + if bindings&driver.BufferBindingTexture != 0 { + var filter uint32 + switch { + case minFilter == driver.FilterNearest && magFilter == driver.FilterNearest: + filter = d3d11.FILTER_MIN_MAG_MIP_POINT + case minFilter == driver.FilterLinear && magFilter == driver.FilterLinear: + filter = d3d11.FILTER_MIN_MAG_LINEAR_MIP_POINT + default: + d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) + return nil, fmt.Errorf("unsupported texture filter combination %d, %d", + minFilter, magFilter) + } + var err error + sampler, err = b.dev.CreateSamplerState(&d3d11.SAMPLER_DESC{ + Filter: filter, + AddressU: d3d11.TEXTURE_ADDRESS_CLAMP, + AddressV: d3d11.TEXTURE_ADDRESS_CLAMP, + AddressW: d3d11.TEXTURE_ADDRESS_CLAMP, + MaxAnisotropy: 1, + MinLOD: -math.MaxFloat32, + MaxLOD: math.MaxFloat32, + }) + if err != nil { + d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) + return nil, err + } + resView, err = b.dev.CreateShaderResourceViewTEX2D( + (*d3d11.Resource)(unsafe.Pointer(tex)), + &d3d11.SHADER_RESOURCE_VIEW_DESC_TEX2D{ + SHADER_RESOURCE_VIEW_DESC: d3d11.SHADER_RESOURCE_VIEW_DESC{ + Format: d3dfmt, + ViewDimension: d3d11.SRV_DIMENSION_TEXTURE2D, + }, + Texture2D: d3d11.TEX2D_SRV{ + MostDetailedMip: 0, + MipLevels: ^uint32(0), + }, + }, + ) + if err != nil { + d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) + d3d11.IUnknownRelease(unsafe.Pointer(sampler), sampler.Vtbl.Release) + return nil, err + } + } + return &Texture{backend: b, format: d3dfmt, tex: tex, sampler: sampler, + resView: resView, bindings: bindings, width: width, height: height}, nil +} + +func (b *Backend) NewFramebuffer(tex driver.Texture, + depthBits int) (driver.Framebuffer, error) { + d3dtex := tex.(*Texture) + if d3dtex.bindings&driver.BufferBindingFramebuffer == 0 { + return nil, errors.New("the texture was created without BufferBindingFramebuffer binding") + } + resource := (*d3d11.Resource)(unsafe.Pointer(d3dtex.tex)) + renderTarget, err := b.dev.CreateRenderTargetView(resource) + if err != nil { + return nil, err + } + fbo := &Framebuffer{ctx: b.ctx, dev: b.dev, format: d3dtex.format, + resource: resource, renderTarget: renderTarget} + if depthBits > 0 { + depthView, err := d3d11.CreateDepthView(b.dev, d3dtex.width, + d3dtex.height, depthBits) + if err != nil { + d3d11.IUnknownRelease(unsafe.Pointer(renderTarget), + renderTarget.Vtbl.Release) + return nil, err + } + fbo.depthView = depthView + } + return fbo, nil +} + +func (b *Backend) NewInputLayout(vertexShader driver.ShaderSources, + layout []driver.InputDesc) (driver.InputLayout, error) { + if len(vertexShader.Inputs) != len(layout) { + return nil, fmt.Errorf("NewInputLayout: got %d inputs, expected %d", + len(layout), len(vertexShader.Inputs)) + } + descs := make([]d3d11.INPUT_ELEMENT_DESC, len(layout)) + for i, l := range layout { + inp := vertexShader.Inputs[i] + cname, err := windows.BytePtrFromString(inp.Semantic) + if err != nil { + return nil, err + } + var format uint32 + switch l.Type { + case driver.DataTypeFloat: + switch l.Size { + case 1: + format = d3d11.DXGI_FORMAT_R32_FLOAT + case 2: + format = d3d11.DXGI_FORMAT_R32G32_FLOAT + case 3: + format = d3d11.DXGI_FORMAT_R32G32B32_FLOAT + case 4: + format = d3d11.DXGI_FORMAT_R32G32B32A32_FLOAT + default: + panic("unsupported data size") + } + case driver.DataTypeShort: + switch l.Size { + case 1: + format = d3d11.DXGI_FORMAT_R16_SINT + case 2: + format = d3d11.DXGI_FORMAT_R16G16_SINT + default: + panic("unsupported data size") + } + default: + panic("unsupported data type") + } + descs[i] = d3d11.INPUT_ELEMENT_DESC{ + SemanticName: cname, + SemanticIndex: uint32(inp.SemanticIndex), + Format: format, + AlignedByteOffset: uint32(l.Offset), + } + } + l, err := b.dev.CreateInputLayout(descs, []byte(vertexShader.HLSL)) + if err != nil { + return nil, err + } + return &InputLayout{layout: l}, nil +} + +func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer, + error) { + if typ&driver.BufferBindingUniforms != 0 { + if typ != driver.BufferBindingUniforms { + return nil, errors.New("uniform buffers cannot have other bindings") + } + if size%16 != 0 { + return nil, fmt.Errorf("constant buffer size is %d, expected a multiple of 16", + size) + } + } + bind := convBufferBinding(typ) + buf, err := b.dev.CreateBuffer(&d3d11.BUFFER_DESC{ + ByteWidth: uint32(size), + BindFlags: bind, + }, nil) + if err != nil { + return nil, err + } + return &Buffer{backend: b, buf: buf, bind: bind}, nil +} + +func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding, + data []byte) (driver.Buffer, error) { + if typ&driver.BufferBindingUniforms != 0 { + if typ != driver.BufferBindingUniforms { + return nil, errors.New("uniform buffers cannot have other bindings") + } + if len(data)%16 != 0 { + return nil, fmt.Errorf("constant buffer size is %d, expected a multiple of 16", + len(data)) + } + } + bind := convBufferBinding(typ) + buf, err := b.dev.CreateBuffer(&d3d11.BUFFER_DESC{ + ByteWidth: uint32(len(data)), + Usage: d3d11.USAGE_IMMUTABLE, + BindFlags: bind, + }, data) + if err != nil { + return nil, err + } + return &Buffer{backend: b, buf: buf, bind: bind, immutable: true}, nil +} + +func (b *Backend) NewComputeProgram(shader driver.ShaderSources) (driver.Program, + error) { + panic("not implemented") +} + +func (b *Backend) NewProgram(vertexShader, fragmentShader driver.ShaderSources) (driver.Program, + error) { + vs, err := b.dev.CreateVertexShader([]byte(vertexShader.HLSL)) + if err != nil { + return nil, err + } + ps, err := b.dev.CreatePixelShader([]byte(fragmentShader.HLSL)) + if err != nil { + return nil, err + } + p := &Program{backend: b} + p.vert.shader = vs + p.frag.shader = ps + return p, nil +} + +func (b *Backend) Clear(colr, colg, colb, cola float32) { + b.clearColor = [4]float32{colr, colg, colb, cola} + b.ctx.ClearRenderTargetView(b.fbo.renderTarget, &b.clearColor) +} + +func (b *Backend) ClearDepth(depth float32) { + if b.fbo.depthView != nil { + b.ctx.ClearDepthStencilView(b.fbo.depthView, + d3d11.CLEAR_DEPTH|d3d11.CLEAR_STENCIL, depth, 0) + } +} + +func (b *Backend) Viewport(x, y, width, height int) { + b.viewport = d3d11.VIEWPORT{ + TopLeftX: float32(x), + TopLeftY: float32(y), + Width: float32(width), + Height: float32(height), + MinDepth: 0.0, + MaxDepth: 1.0, + } + b.ctx.RSSetViewports(&b.viewport) +} + +func (b *Backend) DrawArrays(mode driver.DrawMode, off, count int) { + b.prepareDraw(mode) + b.ctx.Draw(uint32(count), uint32(off)) +} + +func (b *Backend) DrawElements(mode driver.DrawMode, off, count int) { + b.prepareDraw(mode) + b.ctx.DrawIndexed(uint32(count), uint32(off), 0) +} + +func (b *Backend) prepareDraw(mode driver.DrawMode) { + if p := b.prog; p != nil { + b.ctx.VSSetShader(p.vert.shader) + b.ctx.PSSetShader(p.frag.shader) + if buf := p.vert.uniforms; buf != nil { + b.ctx.VSSetConstantBuffers(buf.buf) + } + if buf := p.frag.uniforms; buf != nil { + b.ctx.PSSetConstantBuffers(buf.buf) + } + } + var topology uint32 + switch mode { + case driver.DrawModeTriangles: + topology = d3d11.PRIMITIVE_TOPOLOGY_TRIANGLELIST + case driver.DrawModeTriangleStrip: + topology = d3d11.PRIMITIVE_TOPOLOGY_TRIANGLESTRIP + default: + panic("unsupported draw mode") + } + b.ctx.IASetPrimitiveTopology(topology) + + depthState, ok := b.depthStates[b.depthState] + if !ok { + var desc d3d11.DEPTH_STENCIL_DESC + if b.depthState.enable { + desc.DepthEnable = 1 + } + if b.depthState.mask { + desc.DepthWriteMask = d3d11.DEPTH_WRITE_MASK_ALL + } + switch b.depthState.fn { + case driver.DepthFuncGreater: + desc.DepthFunc = d3d11.COMPARISON_GREATER + case driver.DepthFuncGreaterEqual: + desc.DepthFunc = d3d11.COMPARISON_GREATER_EQUAL + default: + panic("unsupported depth func") + } + var err error + depthState, err = b.dev.CreateDepthStencilState(&desc) + if err != nil { + panic(err) + } + b.depthStates[b.depthState] = depthState + } + b.ctx.OMSetDepthStencilState(depthState, 0) + + blendState, ok := b.blendStates[b.blendState] + if !ok { + var desc d3d11.BLEND_DESC + t0 := &desc.RenderTarget[0] + t0.RenderTargetWriteMask = d3d11.COLOR_WRITE_ENABLE_ALL + t0.BlendOp = d3d11.BLEND_OP_ADD + t0.BlendOpAlpha = d3d11.BLEND_OP_ADD + if b.blendState.enable { + t0.BlendEnable = 1 + } + scol, salpha := toBlendFactor(b.blendState.sfactor) + dcol, dalpha := toBlendFactor(b.blendState.dfactor) + t0.SrcBlend = scol + t0.SrcBlendAlpha = salpha + t0.DestBlend = dcol + t0.DestBlendAlpha = dalpha + var err error + blendState, err = b.dev.CreateBlendState(&desc) + if err != nil { + panic(err) + } + b.blendStates[b.blendState] = blendState + } + b.ctx.OMSetBlendState(blendState, nil, 0xffffffff) +} + +func (b *Backend) DepthFunc(f driver.DepthFunc) { + b.depthState.fn = f +} + +func (b *Backend) SetBlend(enable bool) { + b.blendState.enable = enable +} + +func (b *Backend) SetDepthTest(enable bool) { + b.depthState.enable = enable +} + +func (b *Backend) DepthMask(mask bool) { + b.depthState.mask = mask +} + +func (b *Backend) BlendFunc(sfactor, dfactor driver.BlendFactor) { + b.blendState.sfactor = sfactor + b.blendState.dfactor = dfactor +} + +func (b *Backend) BindImageTexture(unit int, tex driver.Texture, + access driver.AccessBits, f driver.TextureFormat) { + panic("not implemented") +} + +func (b *Backend) MemoryBarrier() { + panic("not implemented") +} + +func (b *Backend) DispatchCompute(x, y, z int) { + panic("not implemented") +} + +func (t *Texture) Upload(offset, size image.Point, pixels []byte) { + stride := size.X * 4 + dst := &d3d11.BOX{ + Left: uint32(offset.X), + Top: uint32(offset.Y), + Right: uint32(offset.X + size.X), + Bottom: uint32(offset.Y + size.Y), + Front: 0, + Back: 1, + } + res := (*d3d11.Resource)(unsafe.Pointer(t.tex)) + t.backend.ctx.UpdateSubresource(res, dst, uint32(stride), + uint32(len(pixels)), pixels) +} + +func (t *Texture) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(t.tex), t.tex.Vtbl.Release) + t.tex = nil + if t.sampler != nil { + d3d11.IUnknownRelease(unsafe.Pointer(t.sampler), t.sampler.Vtbl.Release) + t.sampler = nil + } + if t.resView != nil { + d3d11.IUnknownRelease(unsafe.Pointer(t.resView), t.resView.Vtbl.Release) + t.resView = nil + } +} + +func (b *Backend) BindTexture(unit int, tex driver.Texture) { + t := tex.(*Texture) + b.ctx.PSSetSamplers(uint32(unit), t.sampler) + b.ctx.PSSetShaderResources(uint32(unit), t.resView) +} + +func (b *Backend) BindProgram(prog driver.Program) { + b.prog = prog.(*Program) +} + +func (p *Program) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(p.vert.shader), + p.vert.shader.Vtbl.Release) + d3d11.IUnknownRelease(unsafe.Pointer(p.frag.shader), + p.frag.shader.Vtbl.Release) + p.vert.shader = nil + p.frag.shader = nil +} + +func (p *Program) SetStorageBuffer(binding int, buffer driver.Buffer) { + panic("not implemented") +} + +func (p *Program) SetVertexUniforms(buf driver.Buffer) { + p.vert.uniforms = buf.(*Buffer) +} + +func (p *Program) SetFragmentUniforms(buf driver.Buffer) { + p.frag.uniforms = buf.(*Buffer) +} + +func (b *Backend) BindVertexBuffer(buf driver.Buffer, stride, offset int) { + b.ctx.IASetVertexBuffers(buf.(*Buffer).buf, uint32(stride), uint32(offset)) +} + +func (b *Backend) BindIndexBuffer(buf driver.Buffer) { + b.ctx.IASetIndexBuffer(buf.(*Buffer).buf, d3d11.DXGI_FORMAT_R16_UINT, 0) +} + +func (b *Buffer) Download(data []byte) error { + panic("not implemented") +} + +func (b *Buffer) Upload(data []byte) { + b.backend.ctx.UpdateSubresource((*d3d11.Resource)(unsafe.Pointer(b.buf)), + nil, 0, 0, data) +} + +func (b *Buffer) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(b.buf), b.buf.Vtbl.Release) + b.buf = nil +} + +func (f *Framebuffer) ReadPixels(src image.Rectangle, pixels []byte) error { + if f.resource == nil { + return errors.New("framebuffer does not support ReadPixels") + } + w, h := src.Dx(), src.Dy() + tex, err := f.dev.CreateTexture2D(&d3d11.TEXTURE2D_DESC{ + Width: uint32(w), + Height: uint32(h), + MipLevels: 1, + ArraySize: 1, + Format: f.format, + SampleDesc: d3d11.DXGI_SAMPLE_DESC{ + Count: 1, + Quality: 0, + }, + Usage: d3d11.USAGE_STAGING, + CPUAccessFlags: d3d11.CPU_ACCESS_READ, + }) + if err != nil { + return fmt.Errorf("ReadPixels: %v", err) + } + defer d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) + res := (*d3d11.Resource)(unsafe.Pointer(tex)) + f.ctx.CopySubresourceRegion( + res, + 0, // Destination subresource. + 0, 0, 0, // Destination coordinates (x, y, z). + f.resource, + 0, // Source subresource. + &d3d11.BOX{ + Left: uint32(src.Min.X), + Top: uint32(src.Min.Y), + Right: uint32(src.Max.X), + Bottom: uint32(src.Max.Y), + Front: 0, + Back: 1, + }, + ) + resMap, err := f.ctx.Map(res, 0, d3d11.MAP_READ, 0) + if err != nil { + return fmt.Errorf("ReadPixels: %v", err) + } + defer f.ctx.Unmap(res, 0) + srcPitch := w * 4 + dstPitch := int(resMap.RowPitch) + mapSize := dstPitch * h + data := sliceOf(resMap.PData, mapSize) + width := w * 4 + for r := 0; r < h; r++ { + pixels := pixels[r*srcPitch:] + copy(pixels[:width], data[r*dstPitch:]) + } + return nil +} + +func (b *Backend) BindFramebuffer(fbo driver.Framebuffer) { + b.fbo = fbo.(*Framebuffer) + b.ctx.OMSetRenderTargets(b.fbo.renderTarget, b.fbo.depthView) +} + +func (f *Framebuffer) Invalidate() { +} + +func (f *Framebuffer) Release() { + if f.foreign { + panic("framebuffer not created by NewFramebuffer") + } + if f.renderTarget != nil { + d3d11.IUnknownRelease(unsafe.Pointer(f.renderTarget), + f.renderTarget.Vtbl.Release) + f.renderTarget = nil + } + if f.depthView != nil { + d3d11.IUnknownRelease(unsafe.Pointer(f.depthView), + f.depthView.Vtbl.Release) + f.depthView = nil + } +} + +func (b *Backend) BindInputLayout(layout driver.InputLayout) { + b.ctx.IASetInputLayout(layout.(*InputLayout).layout) +} + +func (l *InputLayout) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(l.layout), l.layout.Vtbl.Release) + l.layout = nil +} + +func convBufferBinding(typ driver.BufferBinding) uint32 { + var bindings uint32 + if typ&driver.BufferBindingVertices != 0 { + bindings |= d3d11.BIND_VERTEX_BUFFER + } + if typ&driver.BufferBindingIndices != 0 { + bindings |= d3d11.BIND_INDEX_BUFFER + } + if typ&driver.BufferBindingUniforms != 0 { + bindings |= d3d11.BIND_CONSTANT_BUFFER + } + if typ&driver.BufferBindingTexture != 0 { + bindings |= d3d11.BIND_SHADER_RESOURCE + } + if typ&driver.BufferBindingFramebuffer != 0 { + bindings |= d3d11.BIND_RENDER_TARGET + } + return bindings +} + +func toBlendFactor(f driver.BlendFactor) (uint32, uint32) { + switch f { + case driver.BlendFactorOne: + return d3d11.BLEND_ONE, d3d11.BLEND_ONE + case driver.BlendFactorOneMinusSrcAlpha: + return d3d11.BLEND_INV_SRC_ALPHA, d3d11.BLEND_INV_SRC_ALPHA + case driver.BlendFactorZero: + return d3d11.BLEND_ZERO, d3d11.BLEND_ZERO + case driver.BlendFactorDstColor: + return d3d11.BLEND_DEST_COLOR, d3d11.BLEND_DEST_ALPHA + default: + panic("unsupported blend source factor") + } +} + +// sliceOf returns a slice from a (native) pointer. +func sliceOf(ptr uintptr, cap int) []byte { + var data []byte + h := (*reflect.SliceHeader)(unsafe.Pointer(&data)) + h.Data = ptr + h.Cap = cap + h.Len = cap + return data +} diff --git a/gio/giold/gpu/internal/driver/api.go b/gio/giold/gpu/internal/driver/api.go new file mode 100644 index 0000000..6e0d846 --- /dev/null +++ b/gio/giold/gpu/internal/driver/api.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package driver + +import ( + "fmt" + "unsafe" + + "realy.lol/gio/internal/gl" +) + +// See gpu/api.go for documentation for the API types + +type API interface { + implementsAPI() +} + +type OpenGL struct { + // Context contains the WebGL context for WebAssembly platforms. It is + // empty for all other platforms; an OpenGL context is assumed current when + // calling NewDevice. + Context gl.Context +} + +type Direct3D11 struct { + // Device contains a *ID3D11Device. + Device unsafe.Pointer +} + +// API specific device constructors. +var ( + NewOpenGLDevice func(api OpenGL) (Device, error) + NewDirect3D11Device func(api Direct3D11) (Device, error) +) + +// NewDevice creates a new Device given the api. +// +// Note that the device does not assume ownership of the resources contained in +// api; the caller must ensure the resources are valid until the device is +// released. +func NewDevice(api API) (Device, error) { + switch api := api.(type) { + case OpenGL: + if NewOpenGLDevice != nil { + return NewOpenGLDevice(api) + } + case Direct3D11: + if NewDirect3D11Device != nil { + return NewDirect3D11Device(api) + } + } + return nil, fmt.Errorf("driver: no driver available for the API %T", api) +} + +func (OpenGL) implementsAPI() {} +func (Direct3D11) implementsAPI() {} diff --git a/gio/giold/gpu/internal/driver/driver.go b/gio/giold/gpu/internal/driver/driver.go new file mode 100644 index 0000000..14d3d85 --- /dev/null +++ b/gio/giold/gpu/internal/driver/driver.go @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package driver + +import ( + "errors" + "image" + "time" +) + +// Device represents the abstraction of underlying GPU +// APIs such as OpenGL, Direct3D useful for rendering Gio +// operations. +type Device interface { + BeginFrame() Framebuffer + EndFrame() + Caps() Caps + NewTimer() Timer + // IsContinuousTime reports whether all timer measurements + // are valid at the point of call. + IsTimeContinuous() bool + NewTexture(format TextureFormat, width, height int, minFilter, magFilter TextureFilter, bindings BufferBinding) (Texture, error) + NewFramebuffer(tex Texture, depthBits int) (Framebuffer, error) + NewImmutableBuffer(typ BufferBinding, data []byte) (Buffer, error) + NewBuffer(typ BufferBinding, size int) (Buffer, error) + NewComputeProgram(shader ShaderSources) (Program, error) + NewProgram(vertexShader, fragmentShader ShaderSources) (Program, error) + NewInputLayout(vertexShader ShaderSources, layout []InputDesc) (InputLayout, error) + + DepthFunc(f DepthFunc) + ClearDepth(d float32) + Clear(r, g, b, a float32) + Viewport(x, y, width, height int) + DrawArrays(mode DrawMode, off, count int) + DrawElements(mode DrawMode, off, count int) + SetBlend(enable bool) + SetDepthTest(enable bool) + DepthMask(mask bool) + BlendFunc(sfactor, dfactor BlendFactor) + + BindInputLayout(i InputLayout) + BindProgram(p Program) + BindFramebuffer(f Framebuffer) + BindTexture(unit int, t Texture) + BindVertexBuffer(b Buffer, stride, offset int) + BindIndexBuffer(b Buffer) + BindImageTexture(unit int, texture Texture, access AccessBits, format TextureFormat) + + MemoryBarrier() + DispatchCompute(x, y, z int) + + Release() +} + +type ShaderSources struct { + Name string + GLSL100ES string + GLSL300ES string + GLSL310ES string + GLSL130 string + GLSL150 string + HLSL string + Uniforms UniformsReflection + Inputs []InputLocation + Textures []TextureBinding +} + +type UniformsReflection struct { + Blocks []UniformBlock + Locations []UniformLocation + Size int +} + +type TextureBinding struct { + Name string + Binding int +} + +type UniformBlock struct { + Name string + Binding int +} + +type UniformLocation struct { + Name string + Type DataType + Size int + Offset int +} + +type InputLocation struct { + // For GLSL. + Name string + Location int + // For HLSL. + Semantic string + SemanticIndex int + + Type DataType + Size int +} + +// InputDesc describes a vertex attribute as laid out in a Buffer. +type InputDesc struct { + Type DataType + Size int + + Offset int +} + +// InputLayout is the driver specific representation of the mapping +// between Buffers and shader attributes. +type InputLayout interface { + Release() +} + +type AccessBits uint8 + +type BlendFactor uint8 + +type DrawMode uint8 + +type TextureFilter uint8 +type TextureFormat uint8 + +type BufferBinding uint8 + +type DataType uint8 + +type DepthFunc uint8 + +type Features uint + +type Caps struct { + // BottomLeftOrigin is true if the driver has the origin in the lower left + // corner. The OpenGL driver returns true. + BottomLeftOrigin bool + Features Features + MaxTextureSize int +} + +type Program interface { + Release() + SetStorageBuffer(binding int, buf Buffer) + SetVertexUniforms(buf Buffer) + SetFragmentUniforms(buf Buffer) +} + +type Buffer interface { + Release() + Upload(data []byte) + Download(data []byte) error +} + +type Framebuffer interface { + Invalidate() + Release() + ReadPixels(src image.Rectangle, pixels []byte) error +} + +type Timer interface { + Begin() + End() + Duration() (time.Duration, bool) + Release() +} + +type Texture interface { + Upload(offset, size image.Point, pixels []byte) + Release() +} + +const ( + DepthFuncGreater DepthFunc = iota + DepthFuncGreaterEqual +) + +const ( + DataTypeFloat DataType = iota + DataTypeInt + DataTypeShort +) + +const ( + BufferBindingIndices BufferBinding = 1 << iota + BufferBindingVertices + BufferBindingUniforms + BufferBindingTexture + BufferBindingFramebuffer + BufferBindingShaderStorage +) + +const ( + TextureFormatSRGB TextureFormat = iota + TextureFormatFloat + TextureFormatRGBA8 +) + +const ( + AccessRead AccessBits = 1 + iota + AccessWrite +) + +const ( + FilterNearest TextureFilter = iota + FilterLinear +) + +const ( + FeatureTimers Features = 1 << iota + FeatureFloatRenderTargets + FeatureCompute +) + +const ( + DrawModeTriangleStrip DrawMode = iota + DrawModeTriangles +) + +const ( + BlendFactorOne BlendFactor = iota + BlendFactorOneMinusSrcAlpha + BlendFactorZero + BlendFactorDstColor +) + +var ErrContentLost = errors.New("buffer content lost") + +func (f Features) Has(feats Features) bool { + return f&feats == feats +} + +func DownloadImage(d Device, f Framebuffer, r image.Rectangle) (*image.RGBA, error) { + img := image.NewRGBA(r) + if err := f.ReadPixels(r, img.Pix); err != nil { + return nil, err + } + if d.Caps().BottomLeftOrigin { + // OpenGL origin is in the lower-left corner. Flip the image to + // match. + flipImageY(r.Dx()*4, r.Dy(), img.Pix) + } + return img, nil +} + +func flipImageY(stride, height int, pixels []byte) { + // Flip image in y-direction. OpenGL's origin is in the lower + // left corner. + row := make([]uint8, stride) + for y := 0; y < height/2; y++ { + y1 := height - y - 1 + dest := y1 * stride + src := y * stride + copy(row, pixels[dest:]) + copy(pixels[dest:], pixels[src:src+len(row)]) + copy(pixels[src:], row) + } +} + +func UploadImage(t Texture, offset image.Point, img *image.RGBA) { + var pixels []byte + size := img.Bounds().Size() + if img.Stride != size.X*4 { + panic("unsupported stride") + } + start := img.PixOffset(0, 0) + end := img.PixOffset(size.X, size.Y-1) + pixels = img.Pix[start:end] + t.Upload(offset, size, pixels) +} diff --git a/gio/giold/gpu/internal/opengl/opengl.go b/gio/giold/gpu/internal/opengl/opengl.go new file mode 100644 index 0000000..e41dbc8 --- /dev/null +++ b/gio/giold/gpu/internal/opengl/opengl.go @@ -0,0 +1,998 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package opengl + +import ( + "errors" + "fmt" + "image" + "strings" + "time" + "unsafe" + + "realy.lol/gio/gpu/internal/driver" + "realy.lol/gio/internal/gl" +) + +// Backend implements driver.Device. +type Backend struct { + funcs *gl.Functions + + state glstate + + glver [2]int + gles bool + ubo bool + feats driver.Caps + // floatTriple holds the settings for floating point + // textures. + floatTriple textureTriple + // Single channel alpha textures. + alphaTriple textureTriple + srgbaTriple textureTriple +} + +// State tracking. +type glstate struct { + // nattr is the current number of enabled vertex arrays. + nattr int + prog *gpuProgram + texUnits [4]*gpuTexture + layout *gpuInputLayout + buffer bufferBinding +} + +type bufferBinding struct { + buf *gpuBuffer + offset int + stride int +} + +type gpuTimer struct { + funcs *gl.Functions + obj gl.Query +} + +type gpuTexture struct { + backend *Backend + obj gl.Texture + triple textureTriple + width int + height int +} + +type gpuFramebuffer struct { + backend *Backend + obj gl.Framebuffer + hasDepth bool + depthBuf gl.Renderbuffer + foreign bool +} + +type gpuBuffer struct { + backend *Backend + hasBuffer bool + obj gl.Buffer + typ driver.BufferBinding + size int + immutable bool + version int + // For emulation of uniform buffers. + data []byte +} + +type gpuProgram struct { + backend *Backend + obj gl.Program + nattr int + vertUniforms uniformsTracker + fragUniforms uniformsTracker + storage [storageBindings]*gpuBuffer +} + +type uniformsTracker struct { + locs []uniformLocation + size int + buf *gpuBuffer + version int +} + +type uniformLocation struct { + uniform gl.Uniform + offset int + typ driver.DataType + size int +} + +type gpuInputLayout struct { + inputs []driver.InputLocation + layout []driver.InputDesc +} + +// textureTriple holds the type settings for +// a TexImage2D call. +type textureTriple struct { + internalFormat gl.Enum + format gl.Enum + typ gl.Enum +} + +type Context = gl.Context + +const ( + storageBindings = 32 +) + +func init() { + driver.NewOpenGLDevice = newOpenGLDevice +} + +func newOpenGLDevice(api driver.OpenGL) (driver.Device, error) { + f, err := gl.NewFunctions(api.Context) + if err != nil { + return nil, err + } + exts := strings.Split(f.GetString(gl.EXTENSIONS), " ") + glVer := f.GetString(gl.VERSION) + ver, gles, err := gl.ParseGLVersion(glVer) + if err != nil { + return nil, err + } + floatTriple, ffboErr := floatTripleFor(f, ver, exts) + srgbaTriple, err := srgbaTripleFor(ver, exts) + if err != nil { + return nil, err + } + gles30 := gles && ver[0] >= 3 + gles31 := gles && (ver[0] > 3 || (ver[0] == 3 && ver[1] >= 1)) + gl40 := !gles && ver[0] >= 4 + b := &Backend{ + glver: ver, + gles: gles, + ubo: gles30 || gl40, + funcs: f, + floatTriple: floatTriple, + alphaTriple: alphaTripleFor(ver), + srgbaTriple: srgbaTriple, + } + b.feats.BottomLeftOrigin = true + if ffboErr == nil { + b.feats.Features |= driver.FeatureFloatRenderTargets + } + if gles31 { + b.feats.Features |= driver.FeatureCompute + } + if hasExtension(exts, + "GL_EXT_disjoint_timer_query_webgl2") || hasExtension(exts, + "GL_EXT_disjoint_timer_query") { + b.feats.Features |= driver.FeatureTimers + } + b.feats.MaxTextureSize = f.GetInteger(gl.MAX_TEXTURE_SIZE) + return b, nil +} + +func (b *Backend) BeginFrame() driver.Framebuffer { + // Assume GL state is reset between frames. + b.state = glstate{} + fboID := gl.Framebuffer(b.funcs.GetBinding(gl.FRAMEBUFFER_BINDING)) + return &gpuFramebuffer{backend: b, obj: fboID, foreign: true} +} + +func (b *Backend) EndFrame() { + b.funcs.ActiveTexture(gl.TEXTURE0) +} + +func (b *Backend) Caps() driver.Caps { + return b.feats +} + +func (b *Backend) NewTimer() driver.Timer { + return &gpuTimer{ + funcs: b.funcs, + obj: b.funcs.CreateQuery(), + } +} + +func (b *Backend) IsTimeContinuous() bool { + return b.funcs.GetInteger(gl.GPU_DISJOINT_EXT) == gl.FALSE +} + +func (b *Backend) NewFramebuffer(tex driver.Texture, + depthBits int) (driver.Framebuffer, error) { + glErr(b.funcs) + gltex := tex.(*gpuTexture) + fb := b.funcs.CreateFramebuffer() + fbo := &gpuFramebuffer{backend: b, obj: fb} + b.BindFramebuffer(fbo) + if err := glErr(b.funcs); err != nil { + fbo.Release() + return nil, err + } + b.funcs.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, gltex.obj, 0) + if depthBits > 0 { + size := gl.Enum(gl.DEPTH_COMPONENT16) + switch { + case depthBits > 24: + size = gl.DEPTH_COMPONENT32F + case depthBits > 16: + size = gl.DEPTH_COMPONENT24 + } + depthBuf := b.funcs.CreateRenderbuffer() + b.funcs.BindRenderbuffer(gl.RENDERBUFFER, depthBuf) + b.funcs.RenderbufferStorage(gl.RENDERBUFFER, size, gltex.width, + gltex.height) + b.funcs.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, + gl.RENDERBUFFER, depthBuf) + fbo.depthBuf = depthBuf + fbo.hasDepth = true + if err := glErr(b.funcs); err != nil { + fbo.Release() + return nil, err + } + } + if st := b.funcs.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { + fbo.Release() + return nil, fmt.Errorf("incomplete framebuffer, status = 0x%x, err = %d", + st, b.funcs.GetError()) + } + return fbo, nil +} + +func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, + minFilter, magFilter driver.TextureFilter, + binding driver.BufferBinding) (driver.Texture, error) { + glErr(b.funcs) + tex := &gpuTexture{backend: b, obj: b.funcs.CreateTexture(), width: width, + height: height} + switch format { + case driver.TextureFormatFloat: + tex.triple = b.floatTriple + case driver.TextureFormatSRGB: + tex.triple = b.srgbaTriple + case driver.TextureFormatRGBA8: + tex.triple = textureTriple{gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE} + default: + return nil, errors.New("unsupported texture format") + } + b.BindTexture(0, tex) + b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, + toTexFilter(magFilter)) + b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, + toTexFilter(minFilter)) + b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + if b.gles && b.glver[0] >= 3 { + // Immutable textures are required for BindImageTexture, and can't hurt otherwise. + b.funcs.TexStorage2D(gl.TEXTURE_2D, 1, tex.triple.internalFormat, width, + height) + } else { + b.funcs.TexImage2D(gl.TEXTURE_2D, 0, tex.triple.internalFormat, width, + height, tex.triple.format, tex.triple.typ) + } + if err := glErr(b.funcs); err != nil { + tex.Release() + return nil, err + } + return tex, nil +} + +func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer, + error) { + glErr(b.funcs) + buf := &gpuBuffer{backend: b, typ: typ, size: size} + if typ&driver.BufferBindingUniforms != 0 { + if typ != driver.BufferBindingUniforms { + return nil, errors.New("uniforms buffers cannot be bound as anything else") + } + if !b.ubo { + // GLES 2 doesn't support uniform buffers. + buf.data = make([]byte, size) + } + } + if typ&^driver.BufferBindingUniforms != 0 || b.ubo { + buf.hasBuffer = true + buf.obj = b.funcs.CreateBuffer() + if err := glErr(b.funcs); err != nil { + buf.Release() + return nil, err + } + firstBinding := firstBufferType(typ) + b.funcs.BindBuffer(firstBinding, buf.obj) + b.funcs.BufferData(firstBinding, size, gl.DYNAMIC_DRAW) + } + return buf, nil +} + +func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding, + data []byte) (driver.Buffer, error) { + glErr(b.funcs) + obj := b.funcs.CreateBuffer() + buf := &gpuBuffer{backend: b, obj: obj, typ: typ, size: len(data), + hasBuffer: true} + firstBinding := firstBufferType(typ) + b.funcs.BindBuffer(firstBinding, buf.obj) + b.funcs.BufferData(firstBinding, len(data), gl.STATIC_DRAW) + buf.Upload(data) + buf.immutable = true + if err := glErr(b.funcs); err != nil { + buf.Release() + return nil, err + } + return buf, nil +} + +func glErr(f *gl.Functions) error { + if st := f.GetError(); st != gl.NO_ERROR { + return fmt.Errorf("glGetError: %#x", st) + } + return nil +} + +func (b *Backend) Release() { +} + +func (b *Backend) MemoryBarrier() { + b.funcs.MemoryBarrier(gl.ALL_BARRIER_BITS) +} + +func (b *Backend) DispatchCompute(x, y, z int) { + if p := b.state.prog; p != nil { + for binding, buf := range p.storage { + if buf != nil { + b.funcs.BindBufferBase(gl.SHADER_STORAGE_BUFFER, binding, + buf.obj) + } + } + } + b.funcs.DispatchCompute(x, y, z) +} + +func (b *Backend) BindImageTexture(unit int, tex driver.Texture, + access driver.AccessBits, f driver.TextureFormat) { + t := tex.(*gpuTexture) + var acc gl.Enum + switch access { + case driver.AccessWrite: + acc = gl.WRITE_ONLY + case driver.AccessRead: + acc = gl.READ_ONLY + default: + panic("unsupported access bits") + } + var format gl.Enum + switch f { + case driver.TextureFormatRGBA8: + format = gl.RGBA8 + default: + panic("unsupported format") + } + b.funcs.BindImageTexture(unit, t.obj, 0, false, 0, acc, format) +} + +func (b *Backend) bindTexture(unit int, t *gpuTexture) { + if b.state.texUnits[unit] != t { + b.funcs.ActiveTexture(gl.TEXTURE0 + gl.Enum(unit)) + b.funcs.BindTexture(gl.TEXTURE_2D, t.obj) + b.state.texUnits[unit] = t + } +} + +func (b *Backend) useProgram(p *gpuProgram) { + if b.state.prog != p { + p.backend.funcs.UseProgram(p.obj) + b.state.prog = p + } +} + +func (b *Backend) enableVertexArrays(n int) { + // Enable needed arrays. + for i := b.state.nattr; i < n; i++ { + b.funcs.EnableVertexAttribArray(gl.Attrib(i)) + } + // Disable extra arrays. + for i := n; i < b.state.nattr; i++ { + b.funcs.DisableVertexAttribArray(gl.Attrib(i)) + } + b.state.nattr = n +} + +func (b *Backend) SetDepthTest(enable bool) { + if enable { + b.funcs.Enable(gl.DEPTH_TEST) + } else { + b.funcs.Disable(gl.DEPTH_TEST) + } +} + +func (b *Backend) BlendFunc(sfactor, dfactor driver.BlendFactor) { + b.funcs.BlendFunc(toGLBlendFactor(sfactor), toGLBlendFactor(dfactor)) +} + +func toGLBlendFactor(f driver.BlendFactor) gl.Enum { + switch f { + case driver.BlendFactorOne: + return gl.ONE + case driver.BlendFactorOneMinusSrcAlpha: + return gl.ONE_MINUS_SRC_ALPHA + case driver.BlendFactorZero: + return gl.ZERO + case driver.BlendFactorDstColor: + return gl.DST_COLOR + default: + panic("unsupported blend factor") + } +} + +func (b *Backend) DepthMask(mask bool) { + b.funcs.DepthMask(mask) +} + +func (b *Backend) SetBlend(enable bool) { + if enable { + b.funcs.Enable(gl.BLEND) + } else { + b.funcs.Disable(gl.BLEND) + } +} + +func (b *Backend) DrawElements(mode driver.DrawMode, off, count int) { + b.prepareDraw() + // off is in 16-bit indices, but DrawElements take a byte offset. + byteOff := off * 2 + b.funcs.DrawElements(toGLDrawMode(mode), count, gl.UNSIGNED_SHORT, byteOff) +} + +func (b *Backend) DrawArrays(mode driver.DrawMode, off, count int) { + b.prepareDraw() + b.funcs.DrawArrays(toGLDrawMode(mode), off, count) +} + +func (b *Backend) prepareDraw() { + nattr := b.state.prog.nattr + b.enableVertexArrays(nattr) + if nattr > 0 { + b.setupVertexArrays() + } + if p := b.state.prog; p != nil { + p.updateUniforms() + } +} + +func toGLDrawMode(mode driver.DrawMode) gl.Enum { + switch mode { + case driver.DrawModeTriangleStrip: + return gl.TRIANGLE_STRIP + case driver.DrawModeTriangles: + return gl.TRIANGLES + default: + panic("unsupported draw mode") + } +} + +func (b *Backend) Viewport(x, y, width, height int) { + b.funcs.Viewport(x, y, width, height) +} + +func (b *Backend) Clear(colR, colG, colB, colA float32) { + b.funcs.ClearColor(colR, colG, colB, colA) + b.funcs.Clear(gl.COLOR_BUFFER_BIT) +} + +func (b *Backend) ClearDepth(d float32) { + b.funcs.ClearDepthf(d) + b.funcs.Clear(gl.DEPTH_BUFFER_BIT) +} + +func (b *Backend) DepthFunc(f driver.DepthFunc) { + var glfunc gl.Enum + switch f { + case driver.DepthFuncGreater: + glfunc = gl.GREATER + case driver.DepthFuncGreaterEqual: + glfunc = gl.GEQUAL + default: + panic("unsupported depth func") + } + b.funcs.DepthFunc(glfunc) +} + +func (b *Backend) NewInputLayout(vs driver.ShaderSources, + layout []driver.InputDesc) (driver.InputLayout, error) { + if len(vs.Inputs) != len(layout) { + return nil, fmt.Errorf("NewInputLayout: got %d inputs, expected %d", + len(layout), len(vs.Inputs)) + } + for i, inp := range vs.Inputs { + if exp, got := inp.Size, layout[i].Size; exp != got { + return nil, fmt.Errorf("NewInputLayout: data size mismatch for %q: got %d expected %d", + inp.Name, got, exp) + } + } + return &gpuInputLayout{ + inputs: vs.Inputs, + layout: layout, + }, nil +} + +func (b *Backend) NewComputeProgram(src driver.ShaderSources) (driver.Program, + error) { + p, err := gl.CreateComputeProgram(b.funcs, src.GLSL310ES) + if err != nil { + return nil, fmt.Errorf("%s: %v", src.Name, err) + } + gpuProg := &gpuProgram{ + backend: b, + obj: p, + } + return gpuProg, nil +} + +func (b *Backend) NewProgram(vertShader, fragShader driver.ShaderSources) (driver.Program, + error) { + attr := make([]string, len(vertShader.Inputs)) + for _, inp := range vertShader.Inputs { + attr[inp.Location] = inp.Name + } + vsrc, fsrc := vertShader.GLSL100ES, fragShader.GLSL100ES + if b.glver[0] >= 3 { + // OpenGL (ES) 3.0. + switch { + case b.gles: + vsrc, fsrc = vertShader.GLSL300ES, fragShader.GLSL300ES + case b.glver[0] >= 4 || b.glver[1] >= 2: + // OpenGL 3.2 Core only accepts glsl 1.50 or newer. + vsrc, fsrc = vertShader.GLSL150, fragShader.GLSL150 + default: + vsrc, fsrc = vertShader.GLSL130, fragShader.GLSL130 + } + } + p, err := gl.CreateProgram(b.funcs, vsrc, fsrc, attr) + if err != nil { + return nil, err + } + gpuProg := &gpuProgram{ + backend: b, + obj: p, + nattr: len(attr), + } + b.BindProgram(gpuProg) + // Bind texture uniforms. + for _, tex := range vertShader.Textures { + u := b.funcs.GetUniformLocation(p, tex.Name) + if u.Valid() { + b.funcs.Uniform1i(u, tex.Binding) + } + } + for _, tex := range fragShader.Textures { + u := b.funcs.GetUniformLocation(p, tex.Name) + if u.Valid() { + b.funcs.Uniform1i(u, tex.Binding) + } + } + if b.ubo { + for _, block := range vertShader.Uniforms.Blocks { + blockIdx := b.funcs.GetUniformBlockIndex(p, block.Name) + if blockIdx != gl.INVALID_INDEX { + b.funcs.UniformBlockBinding(p, blockIdx, uint(block.Binding)) + } + } + // To match Direct3D 11 with separate vertex and fragment + // shader uniform buffers, offset all fragment blocks to be + // located after the vertex blocks. + off := len(vertShader.Uniforms.Blocks) + for _, block := range fragShader.Uniforms.Blocks { + blockIdx := b.funcs.GetUniformBlockIndex(p, block.Name) + if blockIdx != gl.INVALID_INDEX { + b.funcs.UniformBlockBinding(p, blockIdx, + uint(block.Binding+off)) + } + } + } else { + gpuProg.vertUniforms.setup(b.funcs, p, vertShader.Uniforms.Size, + vertShader.Uniforms.Locations) + gpuProg.fragUniforms.setup(b.funcs, p, fragShader.Uniforms.Size, + fragShader.Uniforms.Locations) + } + return gpuProg, nil +} + +func lookupUniform(funcs *gl.Functions, p gl.Program, + loc driver.UniformLocation) uniformLocation { + u := funcs.GetUniformLocation(p, loc.Name) + if !u.Valid() { + panic(fmt.Errorf("uniform %q not found", loc.Name)) + } + return uniformLocation{uniform: u, offset: loc.Offset, typ: loc.Type, + size: loc.Size} +} + +func (p *gpuProgram) SetStorageBuffer(binding int, buffer driver.Buffer) { + buf := buffer.(*gpuBuffer) + if buf.typ&driver.BufferBindingShaderStorage == 0 { + panic("not a shader storage buffer") + } + p.storage[binding] = buf +} + +func (p *gpuProgram) SetVertexUniforms(buffer driver.Buffer) { + p.vertUniforms.setBuffer(buffer) +} + +func (p *gpuProgram) SetFragmentUniforms(buffer driver.Buffer) { + p.fragUniforms.setBuffer(buffer) +} + +func (p *gpuProgram) updateUniforms() { + f := p.backend.funcs + if p.backend.ubo { + if b := p.vertUniforms.buf; b != nil { + f.BindBufferBase(gl.UNIFORM_BUFFER, 0, b.obj) + } + if b := p.fragUniforms.buf; b != nil { + f.BindBufferBase(gl.UNIFORM_BUFFER, 1, b.obj) + } + } else { + p.vertUniforms.update(f) + p.fragUniforms.update(f) + } +} + +func (b *Backend) BindProgram(prog driver.Program) { + p := prog.(*gpuProgram) + b.useProgram(p) +} + +func (p *gpuProgram) Release() { + p.backend.funcs.DeleteProgram(p.obj) +} + +func (u *uniformsTracker) setup(funcs *gl.Functions, p gl.Program, + uniformSize int, uniforms []driver.UniformLocation) { + u.locs = make([]uniformLocation, len(uniforms)) + for i, uniform := range uniforms { + u.locs[i] = lookupUniform(funcs, p, uniform) + } + u.size = uniformSize +} + +func (u *uniformsTracker) setBuffer(buffer driver.Buffer) { + buf := buffer.(*gpuBuffer) + if buf.typ&driver.BufferBindingUniforms == 0 { + panic("not a uniform buffer") + } + if buf.size < u.size { + panic(fmt.Errorf("uniform buffer too small, got %d need %d", buf.size, + u.size)) + } + u.buf = buf + // Force update. + u.version = buf.version - 1 +} + +func (p *uniformsTracker) update(funcs *gl.Functions) { + b := p.buf + if b == nil || b.version == p.version { + return + } + p.version = b.version + data := b.data + for _, u := range p.locs { + data := data[u.offset:] + switch { + case u.typ == driver.DataTypeFloat && u.size == 1: + data := data[:4] + v := *(*[1]float32)(unsafe.Pointer(&data[0])) + funcs.Uniform1f(u.uniform, v[0]) + case u.typ == driver.DataTypeFloat && u.size == 2: + data := data[:8] + v := *(*[2]float32)(unsafe.Pointer(&data[0])) + funcs.Uniform2f(u.uniform, v[0], v[1]) + case u.typ == driver.DataTypeFloat && u.size == 3: + data := data[:12] + v := *(*[3]float32)(unsafe.Pointer(&data[0])) + funcs.Uniform3f(u.uniform, v[0], v[1], v[2]) + case u.typ == driver.DataTypeFloat && u.size == 4: + data := data[:16] + v := *(*[4]float32)(unsafe.Pointer(&data[0])) + funcs.Uniform4f(u.uniform, v[0], v[1], v[2], v[3]) + default: + panic("unsupported uniform data type or size") + } + } +} + +func (b *gpuBuffer) Upload(data []byte) { + if b.immutable { + panic("immutable buffer") + } + if len(data) > b.size { + panic("buffer size overflow") + } + b.version++ + copy(b.data, data) + if b.hasBuffer { + firstBinding := firstBufferType(b.typ) + b.backend.funcs.BindBuffer(firstBinding, b.obj) + if len(data) == b.size { + // the iOS GL implementation doesn't recognize when BufferSubData + // clears the entire buffer. Tell it and avoid GPU stalls. + // See also https://github.com/godotengine/godot/issues/23956. + b.backend.funcs.BufferData(firstBinding, b.size, gl.DYNAMIC_DRAW) + } + b.backend.funcs.BufferSubData(firstBinding, 0, data) + } +} + +func (b *gpuBuffer) Download(data []byte) error { + if len(data) > b.size { + panic("buffer size overflow") + } + if !b.hasBuffer { + copy(data, b.data) + return nil + } + firstBinding := firstBufferType(b.typ) + b.backend.funcs.BindBuffer(firstBinding, b.obj) + bufferMap := b.backend.funcs.MapBufferRange(firstBinding, 0, len(data), + gl.MAP_READ_BIT) + if bufferMap == nil { + return fmt.Errorf("MapBufferRange: error %#x", + b.backend.funcs.GetError()) + } + copy(data, bufferMap) + if !b.backend.funcs.UnmapBuffer(firstBinding) { + return driver.ErrContentLost + } + return nil +} + +func (b *gpuBuffer) Release() { + if b.hasBuffer { + b.backend.funcs.DeleteBuffer(b.obj) + b.hasBuffer = false + } +} + +func (b *Backend) BindVertexBuffer(buf driver.Buffer, stride, offset int) { + gbuf := buf.(*gpuBuffer) + if gbuf.typ&driver.BufferBindingVertices == 0 { + panic("not a vertex buffer") + } + b.state.buffer = bufferBinding{buf: gbuf, stride: stride, offset: offset} +} + +func (b *Backend) setupVertexArrays() { + layout := b.state.layout + if layout == nil { + return + } + buf := b.state.buffer + b.funcs.BindBuffer(gl.ARRAY_BUFFER, buf.buf.obj) + for i, inp := range layout.inputs { + l := layout.layout[i] + var gltyp gl.Enum + switch l.Type { + case driver.DataTypeFloat: + gltyp = gl.FLOAT + case driver.DataTypeShort: + gltyp = gl.SHORT + default: + panic("unsupported data type") + } + b.funcs.VertexAttribPointer(gl.Attrib(inp.Location), l.Size, gltyp, + false, buf.stride, buf.offset+l.Offset) + } +} + +func (b *Backend) BindIndexBuffer(buf driver.Buffer) { + gbuf := buf.(*gpuBuffer) + if gbuf.typ&driver.BufferBindingIndices == 0 { + panic("not an index buffer") + } + b.funcs.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, gbuf.obj) +} + +func (b *Backend) BlitFramebuffer(dst, src driver.Framebuffer, + srect, drect image.Rectangle) { + b.funcs.BindFramebuffer(gl.DRAW_FRAMEBUFFER, dst.(*gpuFramebuffer).obj) + b.funcs.BindFramebuffer(gl.READ_FRAMEBUFFER, src.(*gpuFramebuffer).obj) + b.funcs.BlitFramebuffer( + srect.Min.X, srect.Min.Y, srect.Max.X, srect.Max.Y, + drect.Min.X, drect.Min.Y, drect.Max.X, drect.Max.Y, + gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT|gl.STENCIL_BUFFER_BIT, + gl.NEAREST) +} + +func (f *gpuFramebuffer) ReadPixels(src image.Rectangle, pixels []byte) error { + glErr(f.backend.funcs) + f.backend.BindFramebuffer(f) + if len(pixels) < src.Dx()*src.Dy()*4 { + return errors.New("unexpected RGBA size") + } + f.backend.funcs.ReadPixels(src.Min.X, src.Min.Y, src.Dx(), src.Dy(), + gl.RGBA, gl.UNSIGNED_BYTE, pixels) + return glErr(f.backend.funcs) +} + +func (b *Backend) BindFramebuffer(fbo driver.Framebuffer) { + b.funcs.BindFramebuffer(gl.FRAMEBUFFER, fbo.(*gpuFramebuffer).obj) +} + +func (f *gpuFramebuffer) Invalidate() { + f.backend.BindFramebuffer(f) + f.backend.funcs.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0) +} + +func (f *gpuFramebuffer) Release() { + if f.foreign { + panic("framebuffer not created by NewFramebuffer") + } + f.backend.funcs.DeleteFramebuffer(f.obj) + if f.hasDepth { + f.backend.funcs.DeleteRenderbuffer(f.depthBuf) + } +} + +func toTexFilter(f driver.TextureFilter) int { + switch f { + case driver.FilterNearest: + return gl.NEAREST + case driver.FilterLinear: + return gl.LINEAR + default: + panic("unsupported texture filter") + } +} + +func (b *Backend) BindTexture(unit int, t driver.Texture) { + b.bindTexture(unit, t.(*gpuTexture)) +} + +func (t *gpuTexture) Release() { + t.backend.funcs.DeleteTexture(t.obj) +} + +func (t *gpuTexture) Upload(offset, size image.Point, pixels []byte) { + if min := size.X * size.Y * 4; min > len(pixels) { + panic(fmt.Errorf("size %d larger than data %d", min, len(pixels))) + } + t.backend.BindTexture(0, t) + t.backend.funcs.TexSubImage2D(gl.TEXTURE_2D, 0, offset.X, offset.Y, size.X, + size.Y, t.triple.format, t.triple.typ, pixels) +} + +func (t *gpuTimer) Begin() { + t.funcs.BeginQuery(gl.TIME_ELAPSED_EXT, t.obj) +} + +func (t *gpuTimer) End() { + t.funcs.EndQuery(gl.TIME_ELAPSED_EXT) +} + +func (t *gpuTimer) ready() bool { + return t.funcs.GetQueryObjectuiv(t.obj, + gl.QUERY_RESULT_AVAILABLE) == gl.TRUE +} + +func (t *gpuTimer) Release() { + t.funcs.DeleteQuery(t.obj) +} + +func (t *gpuTimer) Duration() (time.Duration, bool) { + if !t.ready() { + return 0, false + } + nanos := t.funcs.GetQueryObjectuiv(t.obj, gl.QUERY_RESULT) + return time.Duration(nanos), true +} + +func (b *Backend) BindInputLayout(l driver.InputLayout) { + b.state.layout = l.(*gpuInputLayout) +} + +func (l *gpuInputLayout) Release() {} + +// floatTripleFor determines the best texture triple for floating point FBOs. +func floatTripleFor(f *gl.Functions, ver [2]int, exts []string) (textureTriple, + error) { + var triples []textureTriple + if ver[0] >= 3 { + triples = append(triples, + textureTriple{gl.R16F, gl.Enum(gl.RED), gl.Enum(gl.HALF_FLOAT)}) + } + // According to the OES_texture_half_float specification, EXT_color_buffer_half_float is needed to + // render to FBOs. However, the Safari WebGL1 implementation does support half-float FBOs but does not + // report EXT_color_buffer_half_float support. The triples are verified below, so it doesn't matter if we're + // wrong. + if hasExtension(exts, "GL_OES_texture_half_float") || hasExtension(exts, + "GL_EXT_color_buffer_half_float") { + // Try single channel. + triples = append(triples, + textureTriple{gl.LUMINANCE, gl.Enum(gl.LUMINANCE), + gl.Enum(gl.HALF_FLOAT_OES)}) + // Fallback to 4 channels. + triples = append(triples, textureTriple{gl.RGBA, gl.Enum(gl.RGBA), + gl.Enum(gl.HALF_FLOAT_OES)}) + } + if hasExtension(exts, "GL_OES_texture_float") || hasExtension(exts, + "GL_EXT_color_buffer_float") { + triples = append(triples, + textureTriple{gl.RGBA, gl.Enum(gl.RGBA), gl.Enum(gl.FLOAT)}) + } + tex := f.CreateTexture() + defer f.DeleteTexture(tex) + f.BindTexture(gl.TEXTURE_2D, tex) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) + fbo := f.CreateFramebuffer() + defer f.DeleteFramebuffer(fbo) + defFBO := gl.Framebuffer(f.GetBinding(gl.FRAMEBUFFER_BINDING)) + f.BindFramebuffer(gl.FRAMEBUFFER, fbo) + defer f.BindFramebuffer(gl.FRAMEBUFFER, defFBO) + var attempts []string + for _, tt := range triples { + const size = 256 + f.TexImage2D(gl.TEXTURE_2D, 0, tt.internalFormat, size, size, tt.format, + tt.typ) + f.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, tex, 0) + st := f.CheckFramebufferStatus(gl.FRAMEBUFFER) + if st == gl.FRAMEBUFFER_COMPLETE { + return tt, nil + } + attempts = append(attempts, + fmt.Sprintf("(0x%x, 0x%x, 0x%x): 0x%x", tt.internalFormat, + tt.format, tt.typ, st)) + } + return textureTriple{}, fmt.Errorf("floating point fbos not supported (attempted %s)", + attempts) +} + +func srgbaTripleFor(ver [2]int, exts []string) (textureTriple, error) { + switch { + case ver[0] >= 3: + return textureTriple{gl.SRGB8_ALPHA8, gl.Enum(gl.RGBA), + gl.Enum(gl.UNSIGNED_BYTE)}, nil + case hasExtension(exts, "GL_EXT_sRGB"): + return textureTriple{gl.SRGB_ALPHA_EXT, gl.Enum(gl.SRGB_ALPHA_EXT), + gl.Enum(gl.UNSIGNED_BYTE)}, nil + default: + return textureTriple{}, errors.New("no sRGB texture formats found") + } +} + +func alphaTripleFor(ver [2]int) textureTriple { + intf, f := gl.Enum(gl.R8), gl.Enum(gl.RED) + if ver[0] < 3 { + // R8, RED not supported on OpenGL ES 2.0. + intf, f = gl.LUMINANCE, gl.Enum(gl.LUMINANCE) + } + return textureTriple{intf, f, gl.UNSIGNED_BYTE} +} + +func hasExtension(exts []string, ext string) bool { + for _, e := range exts { + if ext == e { + return true + } + } + return false +} + +func firstBufferType(typ driver.BufferBinding) gl.Enum { + switch { + case typ&driver.BufferBindingIndices != 0: + return gl.ELEMENT_ARRAY_BUFFER + case typ&driver.BufferBindingVertices != 0: + return gl.ARRAY_BUFFER + case typ&driver.BufferBindingUniforms != 0: + return gl.UNIFORM_BUFFER + case typ&driver.BufferBindingShaderStorage != 0: + return gl.SHADER_STORAGE_BUFFER + default: + panic("unsupported buffer type") + } +} diff --git a/gio/giold/gpu/internal/rendertest/bench_test.go b/gio/giold/gpu/internal/rendertest/bench_test.go new file mode 100644 index 0000000..ac4ec5f --- /dev/null +++ b/gio/giold/gpu/internal/rendertest/bench_test.go @@ -0,0 +1,321 @@ +package rendertest + +import ( + "image" + "image/color" + "math" + "testing" + + "realy.lol/gio/f32" + "realy.lol/gio/font/gofont" + "realy.lol/gio/gpu/headless" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/widget/material" +) + +// use some global variables for benchmarking so as to not pollute +// the reported allocs with allocations that we do not want to count. +var ( + c1, c2, c3 = make(chan op.CallOp), make(chan op.CallOp), make(chan op.CallOp) + op1, op2, op3 op.Ops +) + +func setupBenchmark(b *testing.B) (layout.Context, *headless.Window, + *material.Theme) { + sz := image.Point{X: 1024, Y: 1200} + w := newWindow(b, sz.X, sz.Y) + ops := new(op.Ops) + gtx := layout.Context{ + Ops: ops, + Constraints: layout.Exact(sz), + } + th := material.NewTheme(gofont.Collection()) + return gtx, w, th +} + +func resetOps(gtx layout.Context) { + gtx.Ops.Reset() + op1.Reset() + op2.Reset() + op3.Reset() +} + +func finishBenchmark(b *testing.B, w *headless.Window) { + b.StopTimer() + if *dumpImages { + img, err := w.Screenshot() + w.Release() + if err != nil { + b.Error(err) + } + if err := saveImage(b.Name()+".png", img); err != nil { + b.Error(err) + } + } +} + +func BenchmarkDrawUICached(b *testing.B) { + // As BenchmarkDraw but the same op.Ops every time that is not reset - this + // should thus allow for maximal cache usage. + gtx, w, th := setupBenchmark(b) + drawCore(gtx, th) + w.Frame(gtx.Ops) + b.ResetTimer() + for i := 0; i < b.N; i++ { + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func BenchmarkDrawUI(b *testing.B) { + // BenchmarkDraw is intended as a reasonable overall benchmark for + // the drawing performance of the full drawing pipeline, in each iteration + // resetting the ops and drawing, similar to how a typical UI would function. + // This will allow font caching across frames. + gtx, w, th := setupBenchmark(b) + drawCore(gtx, th) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + + p := op.Save(gtx.Ops) + off := float32(math.Mod(float64(i)/10, 10)) + op.Offset(f32.Pt(off, off)).Add(gtx.Ops) + + drawCore(gtx, th) + + p.Load() + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func BenchmarkDrawUITransformed(b *testing.B) { + // Like BenchmarkDraw UI but transformed at every frame + gtx, w, th := setupBenchmark(b) + drawCore(gtx, th) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + + p := op.Save(gtx.Ops) + angle := float32(math.Mod(float64(i)/1000, 0.05)) + a := f32.Affine2D{}.Shear(f32.Point{}, angle, angle).Rotate(f32.Point{}, + angle) + op.Affine(a).Add(gtx.Ops) + + drawCore(gtx, th) + + p.Load() + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func Benchmark1000Circles(b *testing.B) { + // Benchmark1000Shapes draws 1000 individual shapes such that no caching between + // shapes will be possible and resets buffers on each operation to prevent caching + // between frames. + gtx, w, _ := setupBenchmark(b) + draw1000Circles(gtx) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + draw1000Circles(gtx) + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func Benchmark1000CirclesInstanced(b *testing.B) { + // Like Benchmark1000Circles but will record them and thus allow for caching between + // them. + gtx, w, _ := setupBenchmark(b) + draw1000CirclesInstanced(gtx) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + draw1000CirclesInstanced(gtx) + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func draw1000Circles(gtx layout.Context) { + ops := gtx.Ops + for x := 0; x < 100; x++ { + p := op.Save(ops) + op.Offset(f32.Pt(float32(x*10), 0)).Add(ops) + for y := 0; y < 10; y++ { + paint.FillShape(ops, + color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, + A: 120}, + clip.RRect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5, + NW: 5}.Op(ops), + ) + op.Offset(f32.Pt(0, float32(100))).Add(ops) + } + p.Load() + } +} + +func draw1000CirclesInstanced(gtx layout.Context) { + ops := gtx.Ops + + r := op.Record(ops) + clip.RRect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5, + NW: 5}.Add(ops) + paint.PaintOp{}.Add(ops) + c := r.Stop() + + for x := 0; x < 100; x++ { + p := op.Save(ops) + op.Offset(f32.Pt(float32(x*10), 0)).Add(ops) + for y := 0; y < 10; y++ { + pi := op.Save(ops) + paint.ColorOp{Color: color.NRGBA{R: 100 + uint8(x), + G: 100 + uint8(y), B: 100, A: 120}}.Add(ops) + c.Add(ops) + pi.Load() + op.Offset(f32.Pt(0, float32(100))).Add(ops) + } + p.Load() + } +} + +func drawCore(gtx layout.Context, th *material.Theme) { + c1 := drawIndividualShapes(gtx, th) + c2 := drawShapeInstances(gtx, th) + c3 := drawText(gtx, th) + + (<-c1).Add(gtx.Ops) + (<-c2).Add(gtx.Ops) + (<-c3).Add(gtx.Ops) +} + +func drawIndividualShapes(gtx layout.Context, + th *material.Theme) chan op.CallOp { + // draw 81 rounded rectangles of different solid colors - each one individually + go func() { + ops := &op1 + c := op.Record(ops) + for x := 0; x < 9; x++ { + p := op.Save(ops) + op.Offset(f32.Pt(float32(x*50), 0)).Add(ops) + for y := 0; y < 9; y++ { + paint.FillShape(ops, + color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, + A: 120}, + clip.RRect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10, + SW: 10, NW: 10}.Op(ops), + ) + op.Offset(f32.Pt(0, float32(50))).Add(ops) + } + p.Load() + } + c1 <- c.Stop() + }() + return c1 +} + +func drawShapeInstances(gtx layout.Context, th *material.Theme) chan op.CallOp { + // draw 400 textured circle instances, each with individual transform + go func() { + ops := &op2 + co := op.Record(ops) + + r := op.Record(ops) + clip.RRect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10, SW: 10, + NW: 10}.Add(ops) + paint.PaintOp{}.Add(ops) + c := r.Stop() + + squares.Add(ops) + rad := float32(0) + for x := 0; x < 20; x++ { + for y := 0; y < 20; y++ { + p := op.Save(ops) + op.Offset(f32.Pt(float32(x*50+25), float32(y*50+25))).Add(ops) + c.Add(ops) + p.Load() + rad += math.Pi * 2 / 400 + } + } + c2 <- co.Stop() + }() + return c2 +} + +func drawText(gtx layout.Context, th *material.Theme) chan op.CallOp { + // draw 40 lines of text with different transforms. + go func() { + ops := &op3 + c := op.Record(ops) + + txt := material.H6(th, "") + for x := 0; x < 40; x++ { + txt.Text = textRows[x] + p := op.Save(ops) + op.Offset(f32.Pt(float32(0), float32(24*x))).Add(ops) + gtx.Ops = ops + txt.Layout(gtx) + p.Load() + } + c3 <- c.Stop() + }() + return c3 +} + +var textRows = []string{ + "1. I learned from my grandfather, Verus, to use good manners, and to", + "put restraint on anger. 2. In the famous memory of my father I had a", + "pattern of modesty and manliness. 3. Of my mother I learned to be", + "pious and generous; to keep myself not only from evil deeds, but even", + "from evil thoughts; and to live with a simplicity which is far from", + "customary among the rich. 4. I owe it to my great-grandfather that I", + "did not attend public lectures and discussions, but had good and able", + "teachers at home; and I owe him also the knowledge that for things of", + "this nature a man should count no expense too great.", + "5. My tutor taught me not to favour either green or blue at the", + "chariot races, nor, in the contests of gladiators, to be a supporter", + "either of light or heavy armed. He taught me also to endure labour;", + "not to need many things; to serve myself without troubling others; not", + "to intermeddle in the affairs of others, and not easily to listen to", + "slanders against them.", + "6. Of Diognetus I had the lesson not to busy myself about vain things;", + "not to credit the great professions of such as pretend to work", + "wonders, or of sorcerers about their charms, and their expelling of", + "Demons and the like; not to keep quails (for fighting or divination),", + "nor to run after such things; to suffer freedom of speech in others,", + "and to apply myself heartily to philosophy. Him also I must thank for", + "my hearing first Bacchius, then Tandasis and Marcianus; that I wrote", + "dialogues in my youth, and took a liking to the philosopher's pallet", + "and skins, and to the other things which, by the Grecian discipline,", + "belong to that profession.", + "7. To Rusticus I owe my first apprehensions that my nature needed", + "reform and cure; and that I did not fall into the ambition of the", + "common Sophists, either by composing speculative writings or by", + "declaiming harangues of exhortation in public; further, that I never", + "strove to be admired by ostentation of great patience in an ascetic", + "life, or by display of activity and application; that I gave over the", + "study of rhetoric, poetry, and the graces of language; and that I did", + "not pace my house in my senatorial robes, or practise any similar", + "affectation. I observed also the simplicity of style in his letters,", + "particularly in that which he wrote to my mother from Sinuessa. I", + "learned from him to be easily appeased, and to be readily reconciled", + "with those who had displeased me or given cause of offence, so soon as", + "they inclined to make their peace; to read with care; not to rest", + "satisfied with a slight and superficial knowledge; nor quickly to", + "assent to great talkers. I have him to thank that I met with the", +} diff --git a/gio/giold/gpu/internal/rendertest/clip_test.go b/gio/giold/gpu/internal/rendertest/clip_test.go new file mode 100644 index 0000000..d12bb90 --- /dev/null +++ b/gio/giold/gpu/internal/rendertest/clip_test.go @@ -0,0 +1,581 @@ +package rendertest + +import ( + "image" + "math" + "testing" + + "golang.org/x/image/colornames" + + "realy.lol/gio/f32" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" +) + +func TestPaintRect(t *testing.T) { + run(t, func(o *op.Ops) { + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) + }, func(r result) { + r.expect(0, 0, colornames.Red) + r.expect(49, 0, colornames.Red) + r.expect(50, 0, transparent) + r.expect(10, 50, transparent) + }) +} + +func TestPaintClippedRect(t *testing.T) { + run(t, func(o *op.Ops) { + clip.RRect{Rect: f32.Rect(25, 25, 60, 60)}.Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(24, 35, transparent) + r.expect(25, 35, colornames.Red) + r.expect(50, 0, transparent) + r.expect(10, 50, transparent) + }) +} + +func TestPaintClippedCircle(t *testing.T) { + run(t, func(o *op.Ops) { + r := float32(10) + clip.RRect{Rect: f32.Rect(20, 20, 40, 40), SE: r, SW: r, NW: r, + NE: r}.Add(o) + clip.Rect(image.Rect(0, 0, 30, 50)).Add(o) + paint.Fill(o, red) + }, func(r result) { + r.expect(21, 21, transparent) + r.expect(25, 30, colornames.Red) + r.expect(31, 30, transparent) + }) +} + +func TestPaintArc(t *testing.T) { + run(t, func(o *op.Ops) { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(0, 20)) + p.Line(f32.Pt(10, 0)) + p.Arc(f32.Pt(10, 0), f32.Pt(40, 0), math.Pi) + p.Line(f32.Pt(30, 0)) + p.Line(f32.Pt(0, 25)) + p.Arc(f32.Pt(-10, 5), f32.Pt(10, 15), -math.Pi) + p.Line(f32.Pt(0, 25)) + p.Arc(f32.Pt(10, 10), f32.Pt(10, 10), 2*math.Pi) + p.Line(f32.Pt(-10, 0)) + p.Arc(f32.Pt(-10, 0), f32.Pt(-40, 0), -math.Pi) + p.Line(f32.Pt(-10, 0)) + p.Line(f32.Pt(0, -10)) + p.Arc(f32.Pt(-10, -20), f32.Pt(10, -5), math.Pi) + p.Line(f32.Pt(0, -10)) + p.Line(f32.Pt(-50, 0)) + p.Close() + clip.Outline{ + Path: p.End(), + }.Op().Add(o) + + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(0, 25, colornames.Red) + r.expect(0, 15, transparent) + }) +} + +func TestPaintAbsolute(t *testing.T) { + run(t, func(o *op.Ops) { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(100, + 100)) // offset the initial pen position to test "MoveTo" + + p.MoveTo(f32.Pt(20, 20)) + p.LineTo(f32.Pt(80, 20)) + p.QuadTo(f32.Pt(80, 80), f32.Pt(20, 80)) + p.Close() + clip.Outline{ + Path: p.End(), + }.Op().Add(o) + + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(30, 30, colornames.Red) + r.expect(79, 79, transparent) + r.expect(90, 90, transparent) + }) +} + +func TestPaintTexture(t *testing.T) { + run(t, func(o *op.Ops) { + squares.Add(o) + scale(80.0/512, 80.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(0, 0, colornames.Blue) + r.expect(79, 10, colornames.Green) + r.expect(80, 0, transparent) + r.expect(10, 80, transparent) + }) +} + +func TestTexturedStrokeClipped(t *testing.T) { + run(t, func(o *op.Ops) { + smallSquares.Add(o) + op.Offset(f32.Pt(50, 50)).Add(o) + clip.Stroke{ + Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), + Style: clip.StrokeStyle{ + Width: 10, + }, + }.Op().Add(o) + clip.RRect{Rect: f32.Rect(-30, -30, 60, 60)}.Add(o) + op.Offset(f32.Pt(-10, -10)).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + }) +} + +func TestTexturedStroke(t *testing.T) { + run(t, func(o *op.Ops) { + smallSquares.Add(o) + op.Offset(f32.Pt(50, 50)).Add(o) + clip.Stroke{ + Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), + Style: clip.StrokeStyle{ + Width: 10, + }, + }.Op().Add(o) + op.Offset(f32.Pt(-10, -10)).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + }) +} + +func TestPaintClippedTexture(t *testing.T) { + run(t, func(o *op.Ops) { + squares.Add(o) + clip.RRect{Rect: f32.Rect(0, 0, 40, 40)}.Add(o) + scale(80.0/512, 80.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(40, 40, transparent) + r.expect(25, 35, colornames.Blue) + }) +} + +func TestStrokedPathBevelFlat(t *testing.T) { + run(t, func(o *op.Ops) { + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + + paint.Fill(o, red) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Red) + }) +} + +func TestStrokedPathBevelRound(t *testing.T) { + run(t, func(o *op.Ops) { + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.RoundCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + + paint.Fill(o, red) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Red) + }) +} + +func TestStrokedPathBevelSquare(t *testing.T) { + run(t, func(o *op.Ops) { + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.SquareCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + + paint.Fill(o, red) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Red) + }) +} + +func TestStrokedPathRoundRound(t *testing.T) { + run(t, func(o *op.Ops) { + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.RoundCap, + Join: clip.RoundJoin, + }, + }.Op().Add(o) + + paint.Fill(o, red) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Red) + }) +} + +func TestStrokedPathFlatMiter(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: 5, + }, + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, colornames.Red) + }) +} + +func TestStrokedPathFlatMiterInf(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, colornames.Red) + }) +} + +func TestStrokedPathZeroWidth(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(10, 50)) + p.Line(f32.Pt(50, 0)) + clip.Stroke{ + Path: p.End(), + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + + paint.Fill(o, black) + stk.Load() + } + + { + stk := op.Save(o) + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(10, 50)) + p.Line(f32.Pt(30, 0)) + clip.Stroke{ + Path: p.End(), + }.Op().Add(o) // width=0, disable stroke + + paint.Fill(o, red) + stk.Load() + } + + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Black) + r.expect(30, 50, colornames.Black) + r.expect(65, 50, transparent) + }) +} + +func TestDashedPathFlatCapEllipse(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newEllipsePath(o) + + var dash clip.Dash + dash.Begin(o) + dash.Dash(5) + dash.Dash(3) + + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + Dashes: dash.End(), + }.Op().Add(o) + + paint.Fill( + o, + red, + ) + stk.Load() + } + { + stk := op.Save(o) + p := newEllipsePath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + }, + }.Op().Add(o) + + paint.Fill( + o, + black, + ) + stk.Load() + } + + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(0, 62, colornames.Red) + r.expect(0, 65, colornames.Black) + }) +} + +func TestDashedPathFlatCapZ(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newZigZagPath(o) + var dash clip.Dash + dash.Begin(o) + dash.Dash(5) + dash.Dash(3) + + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + Dashes: dash.End(), + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, colornames.Red) + r.expect(46, 12, transparent) + }) +} + +func TestDashedPathFlatCapZNoDash(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newZigZagPath(o) + var dash clip.Dash + dash.Begin(o) + dash.Phase(1) + + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + Dashes: dash.End(), + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + { + stk := op.Save(o) + clip.Stroke{ + Path: newZigZagPath(o), + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, colornames.Red) + r.expect(46, 12, colornames.Red) + }) +} + +func TestDashedPathFlatCapZNoPath(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + var dash clip.Dash + dash.Begin(o) + dash.Dash(0) + clip.Stroke{ + Path: newZigZagPath(o), + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + Dashes: dash.End(), + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, transparent) + r.expect(46, 12, transparent) + }) +} + +func newStrokedPath(o *op.Ops) clip.PathSpec { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(10, 50)) + p.Line(f32.Pt(10, 0)) + p.Arc(f32.Pt(10, 0), f32.Pt(20, 0), math.Pi) + p.Line(f32.Pt(10, 0)) + p.Line(f32.Pt(10, 10)) + p.Arc(f32.Pt(0, 30), f32.Pt(0, 30), 2*math.Pi) + p.Line(f32.Pt(-20, 0)) + p.Quad(f32.Pt(-10, -10), f32.Pt(-30, 30)) + return p.End() +} + +func newZigZagPath(o *op.Ops) clip.PathSpec { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(40, 10)) + p.Line(f32.Pt(50, 0)) + p.Line(f32.Pt(-50, 50)) + p.Line(f32.Pt(50, 0)) + p.Quad(f32.Pt(-50, 20), f32.Pt(-50, 50)) + p.Line(f32.Pt(50, 0)) + return p.End() +} + +func newEllipsePath(o *op.Ops) clip.PathSpec { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(0, 65)) + p.Line(f32.Pt(20, 0)) + p.Arc(f32.Pt(20, 0), f32.Pt(70, 0), 2*math.Pi) + return p.End() +} diff --git a/gio/giold/gpu/internal/rendertest/doc.go b/gio/giold/gpu/internal/rendertest/doc.go new file mode 100644 index 0000000..9f6948e --- /dev/null +++ b/gio/giold/gpu/internal/rendertest/doc.go @@ -0,0 +1,2 @@ +// Package rendertest is intended for testing of drawing ops only. +package rendertest diff --git a/gio/giold/gpu/internal/rendertest/refs/TestBuildOffscreen.png b/gio/giold/gpu/internal/rendertest/refs/TestBuildOffscreen.png new file mode 100644 index 0000000..fb50427 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestBuildOffscreen.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png b/gio/giold/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png new file mode 100644 index 0000000..8ff717b Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestClipOffset.png b/gio/giold/gpu/internal/rendertest/refs/TestClipOffset.png new file mode 100644 index 0000000..6396fb4 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestClipOffset.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestClipPaintOffset.png b/gio/giold/gpu/internal/rendertest/refs/TestClipPaintOffset.png new file mode 100644 index 0000000..0fe37e6 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestClipPaintOffset.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestClipRotate.png b/gio/giold/gpu/internal/rendertest/refs/TestClipRotate.png new file mode 100644 index 0000000..e6c15e3 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestClipRotate.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestClipScale.png b/gio/giold/gpu/internal/rendertest/refs/TestClipScale.png new file mode 100644 index 0000000..6396fb4 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestClipScale.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestComplicatedTransform.png b/gio/giold/gpu/internal/rendertest/refs/TestComplicatedTransform.png new file mode 100644 index 0000000..4a92e3c Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestComplicatedTransform.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png new file mode 100644 index 0000000..79bae38 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png new file mode 100644 index 0000000..12212e9 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png new file mode 100644 index 0000000..d315f0f Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png new file mode 100644 index 0000000..94c160e Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestDeferredPaint.png b/gio/giold/gpu/internal/rendertest/refs/TestDeferredPaint.png new file mode 100644 index 0000000..b562f12 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestDeferredPaint.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestDepthOverlap.png b/gio/giold/gpu/internal/rendertest/refs/TestDepthOverlap.png new file mode 100644 index 0000000..9d416b9 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestDepthOverlap.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestLinearGradient.png b/gio/giold/gpu/internal/rendertest/refs/TestLinearGradient.png new file mode 100644 index 0000000..c3c007c Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestLinearGradient.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestLinearGradientAngled.png b/gio/giold/gpu/internal/rendertest/refs/TestLinearGradientAngled.png new file mode 100644 index 0000000..3ba0734 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestLinearGradientAngled.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestNegativeOverlaps.png b/gio/giold/gpu/internal/rendertest/refs/TestNegativeOverlaps.png new file mode 100644 index 0000000..fb50427 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestNegativeOverlaps.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestNoClipFromPaint.png b/gio/giold/gpu/internal/rendertest/refs/TestNoClipFromPaint.png new file mode 100644 index 0000000..e774064 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestNoClipFromPaint.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png b/gio/giold/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png new file mode 100644 index 0000000..515a4d2 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestOffsetTexture.png b/gio/giold/gpu/internal/rendertest/refs/TestOffsetTexture.png new file mode 100644 index 0000000..87386e8 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestOffsetTexture.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintAbsolute.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintAbsolute.png new file mode 100644 index 0000000..dd09760 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintAbsolute.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintArc.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintArc.png new file mode 100644 index 0000000..f432914 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintArc.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedBorder.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedBorder.png new file mode 100644 index 0000000..f8fcfbb Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedBorder.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedCircle.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedCircle.png new file mode 100644 index 0000000..bdf1fce Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedCircle.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedCirle.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedCirle.png new file mode 100644 index 0000000..c8cf2f6 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedCirle.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedRect.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedRect.png new file mode 100644 index 0000000..c1dd7a0 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedRect.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedTexture.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedTexture.png new file mode 100644 index 0000000..ae0e066 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedTexture.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintOffset.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintOffset.png new file mode 100644 index 0000000..82394d5 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintOffset.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintRect.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintRect.png new file mode 100644 index 0000000..f942601 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintRect.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintRotate.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintRotate.png new file mode 100644 index 0000000..fe15d7d Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintRotate.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintShear.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintShear.png new file mode 100644 index 0000000..6d1a4c9 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintShear.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintTexture.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintTexture.png new file mode 100644 index 0000000..9120231 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintTexture.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png b/gio/giold/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png new file mode 100644 index 0000000..da201dc Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestReuseStencil.png b/gio/giold/gpu/internal/rendertest/refs/TestReuseStencil.png new file mode 100644 index 0000000..349db1f Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestReuseStencil.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestRotateClipTexture.png b/gio/giold/gpu/internal/rendertest/refs/TestRotateClipTexture.png new file mode 100644 index 0000000..56c3182 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestRotateClipTexture.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestRotateTexture.png b/gio/giold/gpu/internal/rendertest/refs/TestRotateTexture.png new file mode 100644 index 0000000..e56c972 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestRotateTexture.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png new file mode 100644 index 0000000..9d442f5 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png new file mode 100644 index 0000000..a37235c Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png new file mode 100644 index 0000000..8d2919d Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png new file mode 100644 index 0000000..ae6472a Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png new file mode 100644 index 0000000..d315f0f Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png new file mode 100644 index 0000000..8ef5a94 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png new file mode 100644 index 0000000..0fc6fe8 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestTexturedStroke.png b/gio/giold/gpu/internal/rendertest/refs/TestTexturedStroke.png new file mode 100644 index 0000000..637c932 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestTexturedStroke.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png b/gio/giold/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png new file mode 100644 index 0000000..637c932 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestTransformMacro.png b/gio/giold/gpu/internal/rendertest/refs/TestTransformMacro.png new file mode 100644 index 0000000..a9cce29 Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestTransformMacro.png differ diff --git a/gio/giold/gpu/internal/rendertest/refs/TestTransformOrder.png b/gio/giold/gpu/internal/rendertest/refs/TestTransformOrder.png new file mode 100644 index 0000000..720ca3c Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestTransformOrder.png differ diff --git a/gio/giold/gpu/internal/rendertest/render_test.go b/gio/giold/gpu/internal/rendertest/render_test.go new file mode 100644 index 0000000..efa60a6 --- /dev/null +++ b/gio/giold/gpu/internal/rendertest/render_test.go @@ -0,0 +1,358 @@ +package rendertest + +import ( + "image" + "image/color" + "math" + "testing" + + "golang.org/x/image/colornames" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" +) + +func TestTransformMacro(t *testing.T) { + // testcase resulting from original bug when rendering layout.Stacked + + // Build clip-path. + c := constSqPath() + + run(t, func(o *op.Ops) { + + // render the first Stacked item + m1 := op.Record(o) + dr := image.Rect(0, 0, 128, 50) + paint.FillShape(o, black, clip.Rect(dr).Op()) + c1 := m1.Stop() + + // Render the second stacked item + m2 := op.Record(o) + paint.ColorOp{Color: red}.Add(o) + // Simulate a draw text call + stack := op.Save(o) + op.Offset(f32.Pt(0, 10)).Add(o) + + // Apply the clip-path. + c.Add(o) + + paint.PaintOp{}.Add(o) + stack.Load() + + c2 := m2.Stop() + + // Call each of them in a transform + s1 := op.Save(o) + op.Offset(f32.Pt(0, 0)).Add(o) + c1.Add(o) + s1.Load() + s2 := op.Save(o) + op.Offset(f32.Pt(0, 0)).Add(o) + c2.Add(o) + s2.Load() + }, func(r result) { + r.expect(5, 15, colornames.Red) + r.expect(15, 15, colornames.Black) + r.expect(11, 51, transparent) + }) +} + +func TestRepeatedPaintsZ(t *testing.T) { + run(t, func(o *op.Ops) { + // Draw a rectangle + paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 128, 50)).Op()) + + builder := clip.Path{} + builder.Begin(o) + builder.Move(f32.Pt(0, 0)) + builder.Line(f32.Pt(10, 0)) + builder.Line(f32.Pt(0, 10)) + builder.Line(f32.Pt(-10, 0)) + builder.Line(f32.Pt(0, -10)) + p := builder.End() + clip.Outline{ + Path: p, + }.Op().Add(o) + paint.Fill(o, red) + }, func(r result) { + r.expect(5, 5, colornames.Red) + r.expect(11, 15, colornames.Black) + r.expect(11, 51, transparent) + }) +} + +func TestNoClipFromPaint(t *testing.T) { + // ensure that a paint operation does not pollute the state + // by leaving any clip paths in place. + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Rotate(f32.Pt(20, 20), math.Pi/4) + op.Affine(a).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(10, 10, 30, 30)).Op()) + a = f32.Affine2D{}.Rotate(f32.Pt(20, 20), -math.Pi/4) + op.Affine(a).Add(o) + + paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) + }, func(r result) { + r.expect(1, 1, colornames.Black) + r.expect(20, 20, colornames.Black) + r.expect(49, 49, colornames.Black) + r.expect(51, 51, transparent) + }) +} + +func TestDeferredPaint(t *testing.T) { + run(t, func(o *op.Ops) { + state := op.Save(o) + clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o) + paint.ColorOp{Color: color.NRGBA{A: 0xff, G: 0xff}}.Add(o) + paint.PaintOp{}.Add(o) + + op.Affine(f32.Affine2D{}.Offset(f32.Pt(20, 20))).Add(o) + m := op.Record(o) + clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o) + paint.ColorOp{Color: color.NRGBA{A: 0xff, R: 0xff, G: 0xff}}.Add(o) + paint.PaintOp{}.Add(o) + paintMacro := m.Stop() + op.Defer(o, paintMacro) + + state.Load() + op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Add(o) + clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o) + paint.ColorOp{Color: color.NRGBA{A: 0xff, B: 0xff}}.Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + }) +} + +func constSqPath() op.CallOp { + innerOps := new(op.Ops) + m := op.Record(innerOps) + builder := clip.Path{} + builder.Begin(innerOps) + builder.Move(f32.Pt(0, 0)) + builder.Line(f32.Pt(10, 0)) + builder.Line(f32.Pt(0, 10)) + builder.Line(f32.Pt(-10, 0)) + builder.Line(f32.Pt(0, -10)) + p := builder.End() + clip.Outline{Path: p}.Op().Add(innerOps) + return m.Stop() +} + +func constSqCirc() op.CallOp { + innerOps := new(op.Ops) + m := op.Record(innerOps) + clip.RRect{Rect: f32.Rect(0, 0, 40, 40), + NW: 20, NE: 20, SW: 20, SE: 20}.Add(innerOps) + return m.Stop() +} + +func drawChild(ops *op.Ops, text op.CallOp) op.CallOp { + r1 := op.Record(ops) + text.Add(ops) + paint.PaintOp{}.Add(ops) + return r1.Stop() +} + +func TestReuseStencil(t *testing.T) { + txt := constSqPath() + run(t, func(ops *op.Ops) { + c1 := drawChild(ops, txt) + c2 := drawChild(ops, txt) + + // lay out the children + stack1 := op.Save(ops) + c1.Add(ops) + stack1.Load() + + stack2 := op.Save(ops) + op.Offset(f32.Pt(0, 50)).Add(ops) + c2.Add(ops) + stack2.Load() + }, func(r result) { + r.expect(5, 5, colornames.Black) + r.expect(5, 55, colornames.Black) + }) +} + +func TestBuildOffscreen(t *testing.T) { + // Check that something we in one frame build outside the screen + // still is rendered correctly if moved into the screen in a later + // frame. + + txt := constSqCirc() + draw := func(off float32, o *op.Ops) { + s := op.Save(o) + op.Offset(f32.Pt(0, off)).Add(o) + txt.Add(o) + paint.PaintOp{}.Add(o) + s.Load() + } + + multiRun(t, + frame( + func(ops *op.Ops) { + draw(-100, ops) + }, func(r result) { + r.expect(5, 5, transparent) + r.expect(20, 20, transparent) + }), + frame( + func(ops *op.Ops) { + draw(0, ops) + }, func(r result) { + r.expect(2, 2, transparent) + r.expect(20, 20, colornames.Black) + r.expect(38, 38, transparent) + })) +} + +func TestNegativeOverlaps(t *testing.T) { + run(t, func(ops *op.Ops) { + clip.RRect{Rect: f32.Rect(50, 50, 100, 100)}.Add(ops) + clip.Rect(image.Rect(0, 120, 100, 122)).Add(ops) + paint.PaintOp{}.Add(ops) + }, func(r result) { + r.expect(60, 60, transparent) + r.expect(60, 110, transparent) + r.expect(60, 120, transparent) + r.expect(60, 122, transparent) + }) +} + +func TestDepthOverlap(t *testing.T) { + run(t, func(ops *op.Ops) { + stack := op.Save(ops) + paint.FillShape(ops, red, clip.Rect{Max: image.Pt(128, 64)}.Op()) + stack.Load() + + stack = op.Save(ops) + paint.FillShape(ops, green, clip.Rect{Max: image.Pt(64, 128)}.Op()) + stack.Load() + }, func(r result) { + r.expect(96, 32, colornames.Red) + r.expect(32, 96, colornames.Green) + r.expect(32, 32, colornames.Green) + }) +} + +type Gradient struct { + From, To color.NRGBA +} + +var gradients = []Gradient{ + {From: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF}, + To: color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}}, + {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}, + To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}}, + {From: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}, + To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}}, + {From: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}, + To: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}}, + {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0xFF, A: 0xFF}, + To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}}, + {From: color.NRGBA{R: 0xFF, G: 0xFF, B: 0x19, A: 0xFF}, + To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}}, +} + +func TestLinearGradient(t *testing.T) { + t.Skip("linear gradients don't support transformations") + + const gradienth = 8 + // 0.5 offset from ends to ensure that the center of the pixel + // aligns with gradient from and to colors. + pixelAligned := f32.Rect(0.5, 0, 127.5, gradienth) + samples := []int{0, 12, 32, 64, 96, 115, 127} + + run(t, func(ops *op.Ops) { + gr := f32.Rect(0, 0, 128, gradienth) + for _, g := range gradients { + paint.LinearGradientOp{ + Stop1: f32.Pt(gr.Min.X, gr.Min.Y), + Color1: g.From, + Stop2: f32.Pt(gr.Max.X, gr.Min.Y), + Color2: g.To, + }.Add(ops) + st := op.Save(ops) + clip.RRect{Rect: gr}.Add(ops) + op.Affine(f32.Affine2D{}.Offset(pixelAligned.Min)).Add(ops) + scale(pixelAligned.Dx()/128, 1).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + gr = gr.Add(f32.Pt(0, gradienth)) + } + }, func(r result) { + gr := pixelAligned + for _, g := range gradients { + from := f32color.LinearFromSRGB(g.From) + to := f32color.LinearFromSRGB(g.To) + for _, p := range samples { + exp := lerp(from, to, float32(p)/float32(r.img.Bounds().Dx()-1)) + r.expect(p, int(gr.Min.Y+gradienth/2), + f32color.NRGBAToRGBA(exp.SRGB())) + } + gr = gr.Add(f32.Pt(0, gradienth)) + } + }) +} + +func TestLinearGradientAngled(t *testing.T) { + run(t, func(ops *op.Ops) { + paint.LinearGradientOp{ + Stop1: f32.Pt(64, 64), + Color1: black, + Stop2: f32.Pt(0, 0), + Color2: red, + }.Add(ops) + st := op.Save(ops) + clip.Rect(image.Rect(0, 0, 64, 64)).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + + paint.LinearGradientOp{ + Stop1: f32.Pt(64, 64), + Color1: white, + Stop2: f32.Pt(128, 0), + Color2: green, + }.Add(ops) + st = op.Save(ops) + clip.Rect(image.Rect(64, 0, 128, 64)).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + + paint.LinearGradientOp{ + Stop1: f32.Pt(64, 64), + Color1: black, + Stop2: f32.Pt(128, 128), + Color2: blue, + }.Add(ops) + st = op.Save(ops) + clip.Rect(image.Rect(64, 64, 128, 128)).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + + paint.LinearGradientOp{ + Stop1: f32.Pt(64, 64), + Color1: white, + Stop2: f32.Pt(0, 128), + Color2: magenta, + }.Add(ops) + st = op.Save(ops) + clip.Rect(image.Rect(0, 64, 64, 128)).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + }, func(r result) {}) +} + +// lerp calculates linear interpolation with color b and p. +func lerp(a, b f32color.RGBA, p float32) f32color.RGBA { + return f32color.RGBA{ + R: a.R*(1-p) + b.R*p, + G: a.G*(1-p) + b.G*p, + B: a.B*(1-p) + b.B*p, + A: a.A*(1-p) + b.A*p, + } +} diff --git a/gio/giold/gpu/internal/rendertest/transform_test.go b/gio/giold/gpu/internal/rendertest/transform_test.go new file mode 100644 index 0000000..b00aa7e --- /dev/null +++ b/gio/giold/gpu/internal/rendertest/transform_test.go @@ -0,0 +1,204 @@ +package rendertest + +import ( + "image" + "math" + "testing" + + "golang.org/x/image/colornames" + + "realy.lol/gio/f32" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" +) + +func TestPaintOffset(t *testing.T) { + run(t, func(o *op.Ops) { + op.Offset(f32.Pt(10, 20)).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(59, 30, colornames.Red) + r.expect(60, 30, transparent) + r.expect(10, 70, transparent) + }) +} + +func TestPaintRotate(t *testing.T) { + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/8) + op.Affine(a).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(20, 20, 60, 60)).Op()) + }, func(r result) { + r.expect(40, 40, colornames.Red) + r.expect(50, 19, colornames.Red) + r.expect(59, 19, transparent) + r.expect(21, 21, transparent) + }) +} + +func TestPaintShear(t *testing.T) { + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0) + op.Affine(a).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 40, 40)).Op()) + }, func(r result) { + r.expect(10, 30, transparent) + }) +} + +func TestClipPaintOffset(t *testing.T) { + run(t, func(o *op.Ops) { + clip.RRect{Rect: f32.Rect(10, 10, 30, 30)}.Add(o) + op.Offset(f32.Pt(20, 20)).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 100, 100)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(19, 19, transparent) + r.expect(20, 20, colornames.Red) + r.expect(30, 30, transparent) + }) +} + +func TestClipOffset(t *testing.T) { + run(t, func(o *op.Ops) { + op.Offset(f32.Pt(20, 20)).Add(o) + clip.RRect{Rect: f32.Rect(10, 10, 30, 30)}.Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 100, 100)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(29, 29, transparent) + r.expect(30, 30, colornames.Red) + r.expect(49, 49, colornames.Red) + r.expect(50, 50, transparent) + }) +} + +func TestClipScale(t *testing.T) { + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 2)).Offset(f32.Pt(10, + 10)) + op.Affine(a).Add(o) + clip.RRect{Rect: f32.Rect(10, 10, 20, 20)}.Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 1000, 1000)).Op()) + }, func(r result) { + r.expect(19+10, 19+10, transparent) + r.expect(20+10, 20+10, colornames.Red) + r.expect(39+10, 39+10, colornames.Red) + r.expect(40+10, 40+10, transparent) + }) +} + +func TestClipRotate(t *testing.T) { + run(t, func(o *op.Ops) { + op.Affine(f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/4)).Add(o) + clip.RRect{Rect: f32.Rect(30, 30, 50, 50)}.Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 40, 100, 100)).Op()) + }, func(r result) { + r.expect(39, 39, transparent) + r.expect(41, 41, colornames.Red) + r.expect(50, 50, transparent) + }) +} + +func TestOffsetTexture(t *testing.T) { + run(t, func(o *op.Ops) { + op.Offset(f32.Pt(15, 15)).Add(o) + squares.Add(o) + scale(50.0/512, 50.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(14, 20, transparent) + r.expect(66, 20, transparent) + r.expect(16, 64, colornames.Green) + r.expect(64, 16, colornames.Green) + }) +} + +func TestOffsetScaleTexture(t *testing.T) { + run(t, func(o *op.Ops) { + op.Offset(f32.Pt(15, 15)).Add(o) + squares.Add(o) + op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 1))).Add(o) + scale(50.0/512, 50.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(114, 64, colornames.Blue) + r.expect(116, 64, transparent) + }) +} + +func TestRotateTexture(t *testing.T) { + run(t, func(o *op.Ops) { + defer op.Save(o).Load() + squares.Add(o) + a := f32.Affine2D{}.Offset(f32.Pt(30, 30)).Rotate(f32.Pt(40, 40), + math.Pi/4) + op.Affine(a).Add(o) + scale(20.0/512, 20.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(40, 40-12, colornames.Blue) + r.expect(40+12, 40, colornames.Green) + }) +} + +func TestRotateClipTexture(t *testing.T) { + run(t, func(o *op.Ops) { + squares.Add(o) + a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), math.Pi/8) + op.Affine(a).Add(o) + clip.RRect{Rect: f32.Rect(30, 30, 50, 50)}.Add(o) + op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Add(o) + scale(60.0/512, 60.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(37, 39, colornames.Green) + r.expect(36, 39, colornames.Green) + r.expect(35, 39, colornames.Green) + r.expect(34, 39, colornames.Green) + r.expect(33, 39, colornames.Green) + }) +} + +func TestComplicatedTransform(t *testing.T) { + run(t, func(o *op.Ops) { + squares.Add(o) + + clip.RRect{Rect: f32.Rect(0, 0, 100, 100), SE: 50, SW: 50, NW: 50, + NE: 50}.Add(o) + + a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0) + op.Affine(a).Add(o) + clip.RRect{Rect: f32.Rect(0, 0, 50, 40)}.Add(o) + + scale(50.0/512, 50.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(20, 5, transparent) + }) +} + +func TestTransformOrder(t *testing.T) { + // check the ordering of operations bot in affine and in gpu stack. + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Offset(f32.Pt(64, 64)) + op.Affine(a).Add(o) + + b := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(8, 8)) + op.Affine(b).Add(o) + + c := f32.Affine2D{}.Offset(f32.Pt(-10, -10)).Scale(f32.Point{}, + f32.Pt(0.5, 0.5)) + op.Affine(c).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 20, 20)).Op()) + }, func(r result) { + // centered and with radius 40 + r.expect(64-41, 64, transparent) + r.expect(64-39, 64, colornames.Red) + r.expect(64+39, 64, colornames.Red) + r.expect(64+41, 64, transparent) + }) +} diff --git a/gio/giold/gpu/internal/rendertest/util_test.go b/gio/giold/gpu/internal/rendertest/util_test.go new file mode 100644 index 0000000..74c6f5f --- /dev/null +++ b/gio/giold/gpu/internal/rendertest/util_test.go @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package rendertest + +import ( + "bytes" + "flag" + "fmt" + "image" + "image/color" + "image/draw" + "image/png" + "io/ioutil" + "path/filepath" + "strconv" + "testing" + + "golang.org/x/image/colornames" + + "realy.lol/gio/f32" + "realy.lol/gio/gpu/headless" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/op" + "realy.lol/gio/op/paint" +) + +var ( + dumpImages = flag.Bool("saveimages", false, "save test images") + squares paint.ImageOp + smallSquares paint.ImageOp +) + +var ( + red = f32color.RGBAToNRGBA(colornames.Red) + green = f32color.RGBAToNRGBA(colornames.Green) + blue = f32color.RGBAToNRGBA(colornames.Blue) + magenta = f32color.RGBAToNRGBA(colornames.Magenta) + black = f32color.RGBAToNRGBA(colornames.Black) + white = f32color.RGBAToNRGBA(colornames.White) + transparent = color.RGBA{} +) + +func init() { + squares = buildSquares(512) + smallSquares = buildSquares(50) +} + +func buildSquares(size int) paint.ImageOp { + sub := size / 4 + im := image.NewNRGBA(image.Rect(0, 0, size, size)) + c1, c2 := image.NewUniform(colornames.Green), image.NewUniform(colornames.Blue) + for r := 0; r < 4; r++ { + for c := 0; c < 4; c++ { + c1, c2 = c2, c1 + draw.Draw(im, image.Rect(r*sub, c*sub, r*sub+sub, c*sub+sub), c1, + image.Point{}, draw.Over) + } + c1, c2 = c2, c1 + } + return paint.NewImageOp(im) +} + +func drawImage(t *testing.T, size int, ops *op.Ops, + draw func(o *op.Ops)) (im *image.RGBA, err error) { + sz := image.Point{X: size, Y: size} + w := newWindow(t, sz.X, sz.Y) + draw(ops) + if err := w.Frame(ops); err != nil { + return nil, err + } + return w.Screenshot() +} + +func run(t *testing.T, f func(o *op.Ops), c func(r result)) { + // draw a few times and check that it is correct each time, to + // ensure any caching effects still generate the correct images. + var img *image.RGBA + var err error + ops := new(op.Ops) + for i := 0; i < 3; i++ { + ops.Reset() + img, err = drawImage(t, 128, ops, f) + if err != nil { + t.Error("error rendering:", err) + return + } + // check for a reference image and make sure we are identical. + if !verifyRef(t, img, 0) { + name := fmt.Sprintf("%s-%d-bad.png", t.Name(), i) + if err := saveImage(name, img); err != nil { + t.Error(err) + } + } + c(result{t: t, img: img}) + } + + if *dumpImages { + if err := saveImage(t.Name()+".png", img); err != nil { + t.Error(err) + } + } +} + +func frame(f func(o *op.Ops), c func(r result)) frameT { + return frameT{f: f, c: c} +} + +type frameT struct { + f func(o *op.Ops) + c func(r result) +} + +// multiRun is used to run test cases over multiple frames, typically +// to test caching interactions. +func multiRun(t *testing.T, frames ...frameT) { + // draw a few times and check that it is correct each time, to + // ensure any caching effects still generate the correct images. + var img *image.RGBA + var err error + sz := image.Point{X: 128, Y: 128} + w := newWindow(t, sz.X, sz.Y) + ops := new(op.Ops) + for i := range frames { + ops.Reset() + frames[i].f(ops) + if err := w.Frame(ops); err != nil { + t.Errorf("rendering failed: %v", err) + continue + } + img, err = w.Screenshot() + if err != nil { + t.Errorf("screenshot failed: %v", err) + continue + } + // Check for a reference image and make sure they are identical. + ok := verifyRef(t, img, i) + if frames[i].c != nil { + frames[i].c(result{t: t, img: img}) + } + if *dumpImages || !ok { + name := t.Name() + ".png" + if i != 0 { + name = t.Name() + "_" + strconv.Itoa(i) + ".png" + } + if err := saveImage(name, img); err != nil { + t.Error(err) + } + } + } + +} + +func verifyRef(t *testing.T, img *image.RGBA, frame int) (ok bool) { + // ensure identical to ref data + path := filepath.Join("refs", t.Name()+".png") + if frame != 0 { + path = filepath.Join("refs", t.Name()+"_"+strconv.Itoa(frame)+".png") + } + b, err := ioutil.ReadFile(path) + if err != nil { + t.Error("could not open ref:", err) + return + } + r, err := png.Decode(bytes.NewReader(b)) + if err != nil { + t.Error("could not decode ref:", err) + return + } + if img.Bounds() != r.Bounds() { + t.Errorf("reference image is %v, expected %v", r.Bounds(), img.Bounds()) + return false + } + var ref *image.RGBA + switch r := r.(type) { + case *image.RGBA: + ref = r + case *image.NRGBA: + ref = image.NewRGBA(r.Bounds()) + bnd := r.Bounds() + for x := bnd.Min.X; x < bnd.Max.X; x++ { + for y := bnd.Min.Y; y < bnd.Max.Y; y++ { + ref.SetRGBA(x, y, f32color.NRGBAToRGBA(r.NRGBAAt(x, y))) + } + } + default: + t.Fatalf("reference image is a %T, expected *image.NRGBA or *image.RGBA", + r) + } + bnd := img.Bounds() + for x := bnd.Min.X; x < bnd.Max.X; x++ { + for y := bnd.Min.Y; y < bnd.Max.Y; y++ { + exp := ref.RGBAAt(x, y) + got := img.RGBAAt(x, y) + if !colorsClose(exp, got) { + t.Error("not equal to ref at", x, y, " ", got, exp) + return false + } + } + } + return true +} + +func colorsClose(c1, c2 color.RGBA) bool { + const delta = 0.01 // magic value obtained from experimentation. + return yiqEqApprox(c1, c2, delta) +} + +// yiqEqApprox compares the colors of 2 pixels, in the NTSC YIQ color space, +// as described in: +// +// Measuring perceived color difference using YIQ NTSC +// transmission color space in mobile applications. +// Yuriy Kotsarenko, Fernando Ramos. +// +// An electronic version is available at: +// +// - http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf +func yiqEqApprox(c1, c2 color.RGBA, d2 float64) bool { + const max = 35215.0 // difference between 2 maximally different pixels. + + var ( + r1 = float64(c1.R) + g1 = float64(c1.G) + b1 = float64(c1.B) + + r2 = float64(c2.R) + g2 = float64(c2.G) + b2 = float64(c2.B) + + y1 = r1*0.29889531 + g1*0.58662247 + b1*0.11448223 + i1 = r1*0.59597799 - g1*0.27417610 - b1*0.32180189 + q1 = r1*0.21147017 - g1*0.52261711 + b1*0.31114694 + + y2 = r2*0.29889531 + g2*0.58662247 + b2*0.11448223 + i2 = r2*0.59597799 - g2*0.27417610 - b2*0.32180189 + q2 = r2*0.21147017 - g2*0.52261711 + b2*0.31114694 + + y = y1 - y2 + i = i1 - i2 + q = q1 - q2 + + diff = 0.5053*y*y + 0.299*i*i + 0.1957*q*q + ) + return diff <= max*d2 +} + +func (r result) expect(x, y int, col color.RGBA) { + r.t.Helper() + if r.img == nil { + return + } + c := r.img.RGBAAt(x, y) + if !colorsClose(c, col) { + r.t.Error("expected ", col, " at ", "(", x, ",", y, ") but got ", c) + } +} + +type result struct { + t *testing.T + img *image.RGBA +} + +func saveImage(file string, img *image.RGBA) error { + // Only NRGBA images are losslessly encoded by png.Encode. + nrgba := image.NewNRGBA(img.Bounds()) + bnd := img.Bounds() + for x := bnd.Min.X; x < bnd.Max.X; x++ { + for y := bnd.Min.Y; y < bnd.Max.Y; y++ { + nrgba.SetNRGBA(x, y, f32color.RGBAToNRGBA(img.RGBAAt(x, y))) + } + } + var buf bytes.Buffer + if err := png.Encode(&buf, nrgba); err != nil { + return err + } + return ioutil.WriteFile(file, buf.Bytes(), 0666) +} + +func newWindow(t testing.TB, width, height int) *headless.Window { + w, err := headless.NewWindow(width, height) + if err != nil { + t.Skipf("failed to create headless window, skipping: %v", err) + } + t.Cleanup(w.Release) + return w +} + +func scale(sx, sy float32) op.TransformOp { + return op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(sx, sy))) +} diff --git a/gio/giold/gpu/pack.go b/gio/giold/gpu/pack.go new file mode 100644 index 0000000..c4dbaad --- /dev/null +++ b/gio/giold/gpu/pack.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "image" +) + +// packer packs a set of many smaller rectangles into +// much fewer larger atlases. +type packer struct { + maxDim int + spaces []image.Rectangle + + sizes []image.Point + pos image.Point +} + +type placement struct { + Idx int + Pos image.Point +} + +// add adds the given rectangle to the atlases and +// return the allocated position. +func (p *packer) add(s image.Point) (placement, bool) { + if place, ok := p.tryAdd(s); ok { + return place, true + } + p.newPage() + return p.tryAdd(s) +} + +func (p *packer) clear() { + p.sizes = p.sizes[:0] + p.spaces = p.spaces[:0] +} + +func (p *packer) newPage() { + p.pos = image.Point{} + p.sizes = append(p.sizes, image.Point{}) + p.spaces = p.spaces[:0] + p.spaces = append(p.spaces, image.Rectangle{ + Max: image.Point{X: p.maxDim, Y: p.maxDim}, + }) +} + +func (p *packer) tryAdd(s image.Point) (placement, bool) { + // Go backwards to prioritize smaller spaces first. + for i := len(p.spaces) - 1; i >= 0; i-- { + space := p.spaces[i] + rightSpace := space.Dx() - s.X + bottomSpace := space.Dy() - s.Y + if rightSpace >= 0 && bottomSpace >= 0 { + // Remove space. + p.spaces[i] = p.spaces[len(p.spaces)-1] + p.spaces = p.spaces[:len(p.spaces)-1] + // Put s in the top left corner and add the (at most) + // two smaller spaces. + pos := space.Min + if bottomSpace > 0 { + p.spaces = append(p.spaces, image.Rectangle{ + Min: image.Point{X: pos.X, Y: pos.Y + s.Y}, + Max: image.Point{X: space.Max.X, Y: space.Max.Y}, + }) + } + if rightSpace > 0 { + p.spaces = append(p.spaces, image.Rectangle{ + Min: image.Point{X: pos.X + s.X, Y: pos.Y}, + Max: image.Point{X: space.Max.X, Y: pos.Y + s.Y}, + }) + } + idx := len(p.sizes) - 1 + size := &p.sizes[idx] + if x := pos.X + s.X; x > size.X { + size.X = x + } + if y := pos.Y + s.Y; y > size.Y { + size.Y = y + } + return placement{Idx: idx, Pos: pos}, true + } + } + return placement{}, false +} diff --git a/gio/giold/gpu/path.go b/gio/giold/gpu/path.go new file mode 100644 index 0000000..4670f03 --- /dev/null +++ b/gio/giold/gpu/path.go @@ -0,0 +1,438 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +// GPU accelerated path drawing using the algorithms from +// Pathfinder (https://github.com/servo/pathfinder). + +import ( + "encoding/binary" + "image" + "math" + "unsafe" + + "realy.lol/gio/f32" + "realy.lol/gio/gpu/internal/driver" + "realy.lol/gio/internal/byteslice" + "realy.lol/gio/internal/f32color" +) + +type pather struct { + ctx driver.Device + + viewport image.Point + + stenciler *stenciler + coverer *coverer +} + +type coverer struct { + ctx driver.Device + prog [3]*program + texUniforms *coverTexUniforms + colUniforms *coverColUniforms + linearGradientUniforms *coverLinearGradientUniforms + layout driver.InputLayout +} + +type coverTexUniforms struct { + vert struct { + coverUniforms + _ [12]byte // Padding to multiple of 16. + } +} + +type coverColUniforms struct { + vert struct { + coverUniforms + _ [12]byte // Padding to multiple of 16. + } + frag struct { + colorUniforms + } +} + +type coverLinearGradientUniforms struct { + vert struct { + coverUniforms + _ [12]byte // Padding to multiple of 16. + } + frag struct { + gradientUniforms + } +} + +type coverUniforms struct { + transform [4]float32 + uvCoverTransform [4]float32 + uvTransformR1 [4]float32 + uvTransformR2 [4]float32 + z float32 +} + +type stenciler struct { + ctx driver.Device + prog struct { + prog *program + uniforms *stencilUniforms + layout driver.InputLayout + } + iprog struct { + prog *program + uniforms *intersectUniforms + layout driver.InputLayout + } + fbos fboSet + intersections fboSet + indexBuf driver.Buffer +} + +type stencilUniforms struct { + vert struct { + transform [4]float32 + pathOffset [2]float32 + _ [8]byte // Padding to multiple of 16. + } +} + +type intersectUniforms struct { + vert struct { + uvTransform [4]float32 + subUVTransform [4]float32 + } +} + +type fboSet struct { + fbos []stencilFBO +} + +type stencilFBO struct { + size image.Point + fbo driver.Framebuffer + tex driver.Texture +} + +type pathData struct { + ncurves int + data driver.Buffer +} + +// vertex data suitable for passing to vertex programs. +type vertex struct { + // Corner encodes the corner: +0.5 for south, +.25 for east. + Corner float32 + MaxY float32 + FromX, FromY float32 + CtrlX, CtrlY float32 + ToX, ToY float32 +} + +func (v vertex) encode(d []byte, maxy uint32) { + bo := binary.LittleEndian + bo.PutUint32(d[0:], math.Float32bits(v.Corner)) + bo.PutUint32(d[4:], maxy) + bo.PutUint32(d[8:], math.Float32bits(v.FromX)) + bo.PutUint32(d[12:], math.Float32bits(v.FromY)) + bo.PutUint32(d[16:], math.Float32bits(v.CtrlX)) + bo.PutUint32(d[20:], math.Float32bits(v.CtrlY)) + bo.PutUint32(d[24:], math.Float32bits(v.ToX)) + bo.PutUint32(d[28:], math.Float32bits(v.ToY)) +} + +const ( + // Number of path quads per draw batch. + pathBatchSize = 10000 + // Size of a vertex as sent to gpu + vertStride = 8 * 4 +) + +func newPather(ctx driver.Device) *pather { + return &pather{ + ctx: ctx, + stenciler: newStenciler(ctx), + coverer: newCoverer(ctx), + } +} + +func newCoverer(ctx driver.Device) *coverer { + c := &coverer{ + ctx: ctx, + } + c.colUniforms = new(coverColUniforms) + c.texUniforms = new(coverTexUniforms) + c.linearGradientUniforms = new(coverLinearGradientUniforms) + prog, layout, err := createColorPrograms(ctx, shader_cover_vert, + shader_cover_frag, + [3]interface{}{&c.colUniforms.vert, &c.linearGradientUniforms.vert, + &c.texUniforms.vert}, + [3]interface{}{&c.colUniforms.frag, &c.linearGradientUniforms.frag, + nil}, + ) + if err != nil { + panic(err) + } + c.prog = prog + c.layout = layout + return c +} + +func newStenciler(ctx driver.Device) *stenciler { + // Allocate a suitably large index buffer for drawing paths. + indices := make([]uint16, pathBatchSize*6) + for i := 0; i < pathBatchSize; i++ { + i := uint16(i) + indices[i*6+0] = i*4 + 0 + indices[i*6+1] = i*4 + 1 + indices[i*6+2] = i*4 + 2 + indices[i*6+3] = i*4 + 2 + indices[i*6+4] = i*4 + 1 + indices[i*6+5] = i*4 + 3 + } + indexBuf, err := ctx.NewImmutableBuffer(driver.BufferBindingIndices, + byteslice.Slice(indices)) + if err != nil { + panic(err) + } + progLayout, err := ctx.NewInputLayout(shader_stencil_vert, + []driver.InputDesc{ + {Type: driver.DataTypeFloat, Size: 1, + Offset: int(unsafe.Offsetof((*(*vertex)(nil)).Corner))}, + {Type: driver.DataTypeFloat, Size: 1, + Offset: int(unsafe.Offsetof((*(*vertex)(nil)).MaxY))}, + {Type: driver.DataTypeFloat, Size: 2, + Offset: int(unsafe.Offsetof((*(*vertex)(nil)).FromX))}, + {Type: driver.DataTypeFloat, Size: 2, + Offset: int(unsafe.Offsetof((*(*vertex)(nil)).CtrlX))}, + {Type: driver.DataTypeFloat, Size: 2, + Offset: int(unsafe.Offsetof((*(*vertex)(nil)).ToX))}, + }) + if err != nil { + panic(err) + } + iprogLayout, err := ctx.NewInputLayout(shader_intersect_vert, + []driver.InputDesc{ + {Type: driver.DataTypeFloat, Size: 2, Offset: 0}, + {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2}, + }) + if err != nil { + panic(err) + } + st := &stenciler{ + ctx: ctx, + indexBuf: indexBuf, + } + prog, err := ctx.NewProgram(shader_stencil_vert, shader_stencil_frag) + if err != nil { + panic(err) + } + st.prog.uniforms = new(stencilUniforms) + vertUniforms := newUniformBuffer(ctx, &st.prog.uniforms.vert) + st.prog.prog = newProgram(prog, vertUniforms, nil) + st.prog.layout = progLayout + iprog, err := ctx.NewProgram(shader_intersect_vert, shader_intersect_frag) + if err != nil { + panic(err) + } + st.iprog.uniforms = new(intersectUniforms) + vertUniforms = newUniformBuffer(ctx, &st.iprog.uniforms.vert) + st.iprog.prog = newProgram(iprog, vertUniforms, nil) + st.iprog.layout = iprogLayout + return st +} + +func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) { + // Add fbos. + for i := len(s.fbos); i < len(sizes); i++ { + s.fbos = append(s.fbos, stencilFBO{}) + } + // Resize fbos. + for i, sz := range sizes { + f := &s.fbos[i] + // Resizing or recreating FBOs can introduce rendering stalls. + // Avoid if the space waste is not too high. + resize := sz.X > f.size.X || sz.Y > f.size.Y + waste := float32(sz.X*sz.Y) / float32(f.size.X*f.size.Y) + resize = resize || waste > 1.2 + if resize { + if f.fbo != nil { + f.fbo.Release() + f.tex.Release() + } + tex, err := ctx.NewTexture(driver.TextureFormatFloat, sz.X, sz.Y, + driver.FilterNearest, driver.FilterNearest, + driver.BufferBindingTexture|driver.BufferBindingFramebuffer) + if err != nil { + panic(err) + } + fbo, err := ctx.NewFramebuffer(tex, 0) + if err != nil { + panic(err) + } + f.size = sz + f.tex = tex + f.fbo = fbo + } + } + // Delete extra fbos. + s.delete(ctx, len(sizes)) +} + +func (s *fboSet) invalidate(ctx driver.Device) { + for _, f := range s.fbos { + f.fbo.Invalidate() + } +} + +func (s *fboSet) delete(ctx driver.Device, idx int) { + for i := idx; i < len(s.fbos); i++ { + f := s.fbos[i] + f.fbo.Release() + f.tex.Release() + } + s.fbos = s.fbos[:idx] +} + +func (s *stenciler) release() { + s.fbos.delete(s.ctx, 0) + s.prog.layout.Release() + s.prog.prog.Release() + s.iprog.layout.Release() + s.iprog.prog.Release() + s.indexBuf.Release() +} + +func (p *pather) release() { + p.stenciler.release() + p.coverer.release() +} + +func (c *coverer) release() { + for _, p := range c.prog { + p.Release() + } + c.layout.Release() +} + +func buildPath(ctx driver.Device, p []byte) pathData { + buf, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices, p) + if err != nil { + panic(err) + } + return pathData{ + ncurves: len(p) / vertStride, + data: buf, + } +} + +func (p pathData) release() { + p.data.Release() +} + +func (p *pather) begin(sizes []image.Point) { + p.stenciler.begin(sizes) +} + +func (p *pather) stencilPath(bounds image.Rectangle, offset f32.Point, + uv image.Point, data pathData) { + p.stenciler.stencilPath(bounds, offset, uv, data) +} + +func (s *stenciler) beginIntersect(sizes []image.Point) { + s.ctx.BlendFunc(driver.BlendFactorDstColor, driver.BlendFactorZero) + // 8 bit coverage is enough, but OpenGL ES only supports single channel + // floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if + // no floating point support is available. + s.intersections.resize(s.ctx, sizes) + s.ctx.BindProgram(s.iprog.prog.prog) +} + +func (s *stenciler) invalidateFBO() { + s.intersections.invalidate(s.ctx) + s.fbos.invalidate(s.ctx) +} + +func (s *stenciler) cover(idx int) stencilFBO { + return s.fbos.fbos[idx] +} + +func (s *stenciler) begin(sizes []image.Point) { + s.ctx.BlendFunc(driver.BlendFactorOne, driver.BlendFactorOne) + s.fbos.resize(s.ctx, sizes) + s.ctx.BindProgram(s.prog.prog.prog) + s.ctx.BindInputLayout(s.prog.layout) + s.ctx.BindIndexBuffer(s.indexBuf) +} + +func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, + uv image.Point, data pathData) { + s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy()) + // Transform UI coordinates to OpenGL coordinates. + texSize := f32.Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())} + scale := f32.Point{X: 2 / texSize.X, Y: 2 / texSize.Y} + orig := f32.Point{X: -1 - float32(bounds.Min.X)*2/texSize.X, + Y: -1 - float32(bounds.Min.Y)*2/texSize.Y} + s.prog.uniforms.vert.transform = [4]float32{scale.X, scale.Y, orig.X, + orig.Y} + s.prog.uniforms.vert.pathOffset = [2]float32{offset.X, offset.Y} + s.prog.prog.UploadUniforms() + // Draw in batches that fit in uint16 indices. + start := 0 + nquads := data.ncurves / 4 + for start < nquads { + batch := nquads - start + if max := pathBatchSize; batch > max { + batch = max + } + off := vertStride * start * 4 + s.ctx.BindVertexBuffer(data.data, vertStride, off) + s.ctx.DrawElements(driver.DrawModeTriangles, 0, batch*6) + start += batch + } +} + +func (p *pather) cover(z float32, mat materialType, col f32color.RGBA, + col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, + coverScale, coverOff f32.Point) { + p.coverer.cover(z, mat, col, col1, col2, scale, off, uvTrans, coverScale, + coverOff) +} + +func (c *coverer) cover(z float32, mat materialType, col f32color.RGBA, + col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, + coverScale, coverOff f32.Point) { + p := c.prog[mat] + c.ctx.BindProgram(p.prog) + var uniforms *coverUniforms + switch mat { + case materialColor: + c.colUniforms.frag.color = col + uniforms = &c.colUniforms.vert.coverUniforms + case materialLinearGradient: + c.linearGradientUniforms.frag.color1 = col1 + c.linearGradientUniforms.frag.color2 = col2 + + t1, t2, t3, t4, t5, t6 := uvTrans.Elems() + c.linearGradientUniforms.vert.uvTransformR1 = [4]float32{t1, t2, t3, 0} + c.linearGradientUniforms.vert.uvTransformR2 = [4]float32{t4, t5, t6, 0} + uniforms = &c.linearGradientUniforms.vert.coverUniforms + case materialTexture: + t1, t2, t3, t4, t5, t6 := uvTrans.Elems() + c.texUniforms.vert.uvTransformR1 = [4]float32{t1, t2, t3, 0} + c.texUniforms.vert.uvTransformR2 = [4]float32{t4, t5, t6, 0} + uniforms = &c.texUniforms.vert.coverUniforms + } + uniforms.z = z + uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y} + uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, + coverOff.X, coverOff.Y} + p.UploadUniforms() + c.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4) +} + +func init() { + // Check that struct vertex has the expected size and + // that it contains no padding. + if unsafe.Sizeof(*(*vertex)(nil)) != vertStride { + panic("unexpected struct size") + } +} diff --git a/gio/giold/gpu/shaders.go b/gio/giold/gpu/shaders.go new file mode 100644 index 0000000..7df7cb5 --- /dev/null +++ b/gio/giold/gpu/shaders.go @@ -0,0 +1,6694 @@ +// Code generated by build.go. DO NOT EDIT. + +package gpu + +import "realy.lol/gio/gpu/internal/driver" + +var ( + shader_backdrop_comp = driver.ShaderSources{ + Name: "backdrop.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct AnnotatedTag +{ + uint tag; + uint flags; +}; + +struct PathRef +{ + uint offset; +}; + +struct TileRef +{ + uint offset; +}; + +struct Path +{ + uvec4 bbox; + TileRef tiles; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _77; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _191; + +shared uint sh_row_width[128]; +shared Alloc sh_row_alloc[128]; +shared uint sh_row_count[128]; + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _77.memory[offset]; + return v; +} + +AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +uint fill_mode_from_flags(uint flags) +{ + return flags & 1u; +} + +Path Path_read(Alloc a, PathRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Path s; + s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16)); + s.tiles = TileRef(raw2); + return s; +} + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _77.memory[offset] = val; +} + +void main() +{ + if (_77.mem_error != 0u) + { + return; + } + uint th_ix = gl_LocalInvocationID.x; + uint element_ix = gl_GlobalInvocationID.x; + AnnotatedRef ref = AnnotatedRef(_191.conf.anno_alloc.offset + (element_ix * 32u)); + uint row_count = 0u; + if (element_ix < _191.conf.n_elements) + { + Alloc param; + param.offset = _191.conf.anno_alloc.offset; + AnnotatedRef param_1 = ref; + AnnotatedTag tag = Annotated_tag(param, param_1); + switch (tag.tag) + { + case 2u: + case 3u: + case 1u: + { + uint param_2 = tag.flags; + if (fill_mode_from_flags(param_2) != 0u) + { + break; + } + PathRef path_ref = PathRef(_191.conf.tile_alloc.offset + (element_ix * 12u)); + Alloc param_3; + param_3.offset = _191.conf.tile_alloc.offset; + PathRef param_4 = path_ref; + Path path = Path_read(param_3, param_4); + sh_row_width[th_ix] = path.bbox.z - path.bbox.x; + row_count = path.bbox.w - path.bbox.y; + bool _267 = row_count == 1u; + bool _273; + if (_267) + { + _273 = path.bbox.y > 0u; + } + else + { + _273 = _267; + } + if (_273) + { + row_count = 0u; + } + uint param_5 = path.tiles.offset; + uint param_6 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u; + Alloc path_alloc = new_alloc(param_5, param_6); + sh_row_alloc[th_ix] = path_alloc; + break; + } + } + } + sh_row_count[th_ix] = row_count; + for (uint i = 0u; i < 7u; i++) + { + barrier(); + if (th_ix >= uint(1 << int(i))) + { + row_count += sh_row_count[th_ix - uint(1 << int(i))]; + } + barrier(); + sh_row_count[th_ix] = row_count; + } + barrier(); + uint total_rows = sh_row_count[127]; + uint _395; + for (uint row = th_ix; row < total_rows; row += 128u) + { + uint el_ix = 0u; + for (uint i_1 = 0u; i_1 < 7u; i_1++) + { + uint probe = el_ix + uint(64 >> int(i_1)); + if (row >= sh_row_count[probe - 1u]) + { + el_ix = probe; + } + } + uint width = sh_row_width[el_ix]; + if (width > 0u) + { + Alloc tiles_alloc = sh_row_alloc[el_ix]; + if (el_ix > 0u) + { + _395 = sh_row_count[el_ix - 1u]; + } + else + { + _395 = 0u; + } + uint seq_ix = row - _395; + uint tile_el_ix = ((tiles_alloc.offset >> uint(2)) + 1u) + ((seq_ix * 2u) * width); + Alloc param_7 = tiles_alloc; + uint param_8 = tile_el_ix; + uint sum = read_mem(param_7, param_8); + for (uint x = 1u; x < width; x++) + { + tile_el_ix += 2u; + Alloc param_9 = tiles_alloc; + uint param_10 = tile_el_ix; + sum += read_mem(param_9, param_10); + Alloc param_11 = tiles_alloc; + uint param_12 = tile_el_ix; + uint param_13 = sum; + write_mem(param_11, param_12, param_13); + } + } + } +} + +`, + } + shader_binning_comp = driver.ShaderSources{ + Name: "binning.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct MallocResult +{ + Alloc alloc; + bool failed; +}; + +struct AnnoEndClipRef +{ + uint offset; +}; + +struct AnnoEndClip +{ + vec4 bbox; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct AnnotatedTag +{ + uint tag; + uint flags; +}; + +struct BinInstanceRef +{ + uint offset; +}; + +struct BinInstance +{ + uint element_ix; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _88; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _254; + +shared uint bitmaps[4][128]; +shared bool sh_alloc_failed; +shared uint count[4][128]; +shared Alloc sh_chunk_alloc[128]; + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _88.memory[offset]; + return v; +} + +AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +AnnoEndClip AnnoEndClip_read(Alloc a, AnnoEndClipRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + AnnoEndClip s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + return s; +} + +AnnoEndClip Annotated_EndClip_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoEndClipRef param_1 = AnnoEndClipRef(ref.offset + 4u); + return AnnoEndClip_read(param, param_1); +} + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +MallocResult malloc(uint size) +{ + MallocResult r; + r.failed = false; + uint _94 = atomicAdd(_88.mem_offset, size); + uint offset = _94; + uint param = offset; + uint param_1 = size; + r.alloc = new_alloc(param, param_1); + if ((offset + size) > uint(int(uint(_88.memory.length())) * 4)) + { + r.failed = true; + uint _115 = atomicMax(_88.mem_error, 1u); + return r; + } + return r; +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _88.memory[offset] = val; +} + +void BinInstance_write(Alloc a, BinInstanceRef ref, BinInstance s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.element_ix; + write_mem(param, param_1, param_2); +} + +void main() +{ + if (_88.mem_error != 0u) + { + return; + } + uint my_n_elements = _254.conf.n_elements; + uint my_partition = gl_WorkGroupID.x; + for (uint i = 0u; i < 4u; i++) + { + bitmaps[i][gl_LocalInvocationID.x] = 0u; + } + if (gl_LocalInvocationID.x == 0u) + { + sh_alloc_failed = false; + } + barrier(); + uint element_ix = (my_partition * 128u) + gl_LocalInvocationID.x; + AnnotatedRef ref = AnnotatedRef(_254.conf.anno_alloc.offset + (element_ix * 32u)); + uint tag = 0u; + if (element_ix < my_n_elements) + { + Alloc param; + param.offset = _254.conf.anno_alloc.offset; + AnnotatedRef param_1 = ref; + tag = Annotated_tag(param, param_1).tag; + } + int x0 = 0; + int y0 = 0; + int x1 = 0; + int y1 = 0; + switch (tag) + { + case 1u: + case 2u: + case 3u: + case 4u: + { + Alloc param_2; + param_2.offset = _254.conf.anno_alloc.offset; + AnnotatedRef param_3 = ref; + AnnoEndClip clip = Annotated_EndClip_read(param_2, param_3); + x0 = int(floor(clip.bbox.x * 0.001953125)); + y0 = int(floor(clip.bbox.y * 0.00390625)); + x1 = int(ceil(clip.bbox.z * 0.001953125)); + y1 = int(ceil(clip.bbox.w * 0.00390625)); + break; + } + } + uint width_in_bins = ((_254.conf.width_in_tiles + 16u) - 1u) / 16u; + uint height_in_bins = ((_254.conf.height_in_tiles + 8u) - 1u) / 8u; + x0 = clamp(x0, 0, int(width_in_bins)); + x1 = clamp(x1, x0, int(width_in_bins)); + y0 = clamp(y0, 0, int(height_in_bins)); + y1 = clamp(y1, y0, int(height_in_bins)); + if (x0 == x1) + { + y1 = y0; + } + int x = x0; + int y = y0; + uint my_slice = gl_LocalInvocationID.x / 32u; + uint my_mask = uint(1 << int(gl_LocalInvocationID.x & 31u)); + while (y < y1) + { + uint _438 = atomicOr(bitmaps[my_slice][(uint(y) * width_in_bins) + uint(x)], my_mask); + x++; + if (x == x1) + { + x = x0; + y++; + } + } + barrier(); + uint element_count = 0u; + for (uint i_1 = 0u; i_1 < 4u; i_1++) + { + element_count += uint(bitCount(bitmaps[i_1][gl_LocalInvocationID.x])); + count[i_1][gl_LocalInvocationID.x] = element_count; + } + uint param_4 = 0u; + uint param_5 = 0u; + Alloc chunk_alloc = new_alloc(param_4, param_5); + if (element_count != 0u) + { + uint param_6 = element_count * 4u; + MallocResult _487 = malloc(param_6); + MallocResult chunk = _487; + chunk_alloc = chunk.alloc; + sh_chunk_alloc[gl_LocalInvocationID.x] = chunk_alloc; + if (chunk.failed) + { + sh_alloc_failed = true; + } + } + uint out_ix = (_254.conf.bin_alloc.offset >> uint(2)) + (((my_partition * 128u) + gl_LocalInvocationID.x) * 2u); + Alloc param_7; + param_7.offset = _254.conf.bin_alloc.offset; + uint param_8 = out_ix; + uint param_9 = element_count; + write_mem(param_7, param_8, param_9); + Alloc param_10; + param_10.offset = _254.conf.bin_alloc.offset; + uint param_11 = out_ix + 1u; + uint param_12 = chunk_alloc.offset; + write_mem(param_10, param_11, param_12); + barrier(); + if (sh_alloc_failed) + { + return; + } + x = x0; + y = y0; + while (y < y1) + { + uint bin_ix = (uint(y) * width_in_bins) + uint(x); + uint out_mask = bitmaps[my_slice][bin_ix]; + if ((out_mask & my_mask) != 0u) + { + uint idx = uint(bitCount(out_mask & (my_mask - 1u))); + if (my_slice > 0u) + { + idx += count[my_slice - 1u][bin_ix]; + } + Alloc out_alloc = sh_chunk_alloc[bin_ix]; + uint out_offset = out_alloc.offset + (idx * 4u); + Alloc param_13 = out_alloc; + BinInstanceRef param_14 = BinInstanceRef(out_offset); + BinInstance param_15 = BinInstance(element_ix); + BinInstance_write(param_13, param_14, param_15); + } + x++; + if (x == x1) + { + x = x0; + y++; + } + } +} + +`, + } + shader_blit_frag = [...]driver.ShaderSources{ + { + Name: "blit.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Color", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_color.color", Type: 0x0, Size: 4, Offset: 0}}, + Size: 16, + }, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +struct Color +{ + vec4 color; +}; + +uniform Color _color; + +varying vec2 vUV; + +void main() +{ + gl_FragData[0] = _color.color; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(std140) uniform Color +{ + vec4 color; +} _color; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Color +{ + vec4 color; +}; + +uniform Color _color; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Color +{ + vec4 color; +} _color; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; +} + +`, + HLSL: "DXBC,\xc1\x9c\x85P\xbc\xab\x8a.\x9e\b\xdd\xf7\xd2\x18\xa2\x01\x00\x00\x00t\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\x84\x00\x00\x00\xcc\x00\x00\x00H\x01\x00\x00\f\x02\x00\x00@\x02\x00\x00Aon9D\x00\x00\x00D\x00\x00\x00\x00\x02\xff\xff\x14\x00\x00\x000\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x00\x000\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\xa0\xff\xff\x00\x00SHDR@\x00\x00\x00@\x00\x00\x00\x10\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x006\x00\x00\x06\xf2 \x10\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xbc\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x94\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Color\x00\xab\xab<\x00\x00\x00\x01\x00\x00\x00\\\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00t\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\x84\x00\x00\x00\x00\x00\x00\x00_color_color\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + { + Name: "blit.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Gradient", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_gradient.color1", Type: 0x0, Size: 4, Offset: 0}, {Name: "_gradient.color2", Type: 0x0, Size: 4, Offset: 16}}, + Size: 32, + }, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +struct Gradient +{ + vec4 color1; + vec4 color2; +}; + +uniform Gradient _gradient; + +varying vec2 vUV; + +void main() +{ + gl_FragData[0] = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(std140) uniform Gradient +{ + vec4 color1; + vec4 color2; +} _gradient; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Gradient +{ + vec4 color1; + vec4 color2; +}; + +uniform Gradient _gradient; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Gradient +{ + vec4 color1; + vec4 color2; +} _gradient; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); +} + +`, + HLSL: "DXBCdZ\xb9AA\xb2\xa5-Ī£c\xb9\xdc\xfd]\xae\x01\x00\x00\x00P\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xcc\x00\x00\x00t\x01\x00\x00\xf0\x01\x00\x00\xe8\x02\x00\x00\x1c\x03\x00\x00Aon9\x8c\x00\x00\x00\x8c\x00\x00\x00\x00\x02\xff\xff\\\x00\x00\x000\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x00\x000\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x01\x00\x00\x02\x00\x00\x18\x80\x00\x00\x00\xb0\x01\x00\x00\x02\x01\x00\x0f\x80\x00\x00\xe4\xa0\x02\x00\x00\x03\x01\x00\x0f\x80\x01\x00\xe4\x81\x01\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\x0f\x80\x00\x00\xff\x80\x01\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xa0\x00\x00\x00@\x00\x00\x00(\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00b\x10\x00\x03\x12\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006 \x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x8e \x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x002\x00\x00\n\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xf0\x00\x00\x00\x01\x00\x00\x00H\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xc5\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Gradient\x00\xab\xab\xab<\x00\x00\x00\x02\x00\x00\x00`\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x00\x00\xb4\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x00\x00_gradient_color1\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_gradient_color2\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x01\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + { + Name: "blit.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +varying vec2 vUV; + +void main() +{ + gl_FragData[0] = texture2D(tex, vUV); +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = texture(tex, vUV); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = texture(tex, vUV); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = texture(tex, vUV); +} + +`, + HLSL: "DXBC\xb7?\x1d\xb1\x80Ķ€\xa3W\t\xfbZ\x9fV\xd6\xda\x01\x00\x00\x00\x94\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\xa4\x00\x00\x00\x10\x01\x00\x00\x8c\x01\x00\x00,\x02\x00\x00`\x02\x00\x00Aon9d\x00\x00\x00d\x00\x00\x00\x00\x02\xff\xff<\x00\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDRd\x00\x00\x00@\x00\x00\x00\x19\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00E\x00\x00\t\xf2 \x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00m\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00i\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + } + shader_blit_vert = driver.ShaderSources{ + Name: "blit.vert", + Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.uvTransformR1", Type: 0x0, Size: 4, Offset: 16}, {Name: "_block.uvTransformR2", Type: 0x0, Size: 4, Offset: 32}, {Name: "_block.z", Type: 0x0, Size: 1, Offset: 48}}, + Size: 52, + }, + GLSL100ES: `#version 100 + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 transform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +}; + +uniform Block _block; + +attribute vec2 pos; +varying vec2 vUV; +attribute vec2 uv; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec2 p = (pos * _block.transform.xy) + _block.transform.zw; + vec4 param = vec4(p, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; +} + +`, + GLSL300ES: `#version 300 es + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(std140) uniform Block +{ + vec4 transform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +} _block; + +layout(location = 0) in vec2 pos; +out vec2 vUV; +layout(location = 1) in vec2 uv; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec2 p = (pos * _block.transform.xy) + _block.transform.zw; + vec4 param = vec4(p, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 transform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +}; + +uniform Block _block; + +in vec2 pos; +out vec2 vUV; +in vec2 uv; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec2 p = (pos * _block.transform.xy) + _block.transform.zw; + vec4 param = vec4(p, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(binding = 0, std140) uniform Block +{ + vec4 transform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +} _block; + +in vec2 pos; +out vec2 vUV; +in vec2 uv; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec2 p = (pos * _block.transform.xy) + _block.transform.zw; + vec4 param = vec4(p, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; +} + +`, + HLSL: "DXBC\x80\xa7\xa0\x9e\xbb\xa1\xa3\x1b\x85\xac\xb6\xe9\xfb\xe6W\x03\x01\x00\x00\x00\xc8\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00$\x01\x00\x00T\x02\x00\x00\xd0\x02\x00\x00$\x04\x00\x00p\x04\x00\x00Aon9\xe4\x00\x00\x00\xe4\x00\x00\x00\x00\x02\xfe\xff\xb0\x00\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x04\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x05\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\a\x80\x01\x00Đ\x05\x00Š \x05\x00Å \b\x00\x00\x03\x00\x00\x01\xe0\x02\x00\xe4\xa0\x00\x00\xe4\x80\b\x00\x00\x03\x00\x00\x02\xe0\x03\x00\xe4\xa0\x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x90\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\a\x80\x05\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\f\xc0\x04\x00\x00\xa0\x00\x00d\x80\x00\x00$\x80\xff\xff\x00\x00SHDR(\x01\x00\x00@\x00\x01\x00J\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x04\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x006\x00\x00\x052\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x10\x00\x00\b\x12 \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x10\x00\x00\b\" \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x002\x00\x00\v2 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\nB \x10\x00\x01\x00\x00\x00\n\x80 \x00\x00\x00\x00\x00\x03\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\x01@\x00\x00\x00\x00\x00?6\x00\x00\x05\x82 \x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\b\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFL\x01\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00$\x01\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x04\x00\x00\x00\\\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\xf5\x00\x00\x00 \x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\n\x01\x00\x000\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00\x14\x01\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_uvTransformR1\x00_block_uvTransformR2\x00_block_z\x00\xab\x00\x00\x03\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_coarse_comp = driver.ShaderSources{ + Name: "coarse.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct MallocResult +{ + Alloc alloc; + bool failed; +}; + +struct AnnoImageRef +{ + uint offset; +}; + +struct AnnoImage +{ + vec4 bbox; + float linewidth; + uint index; + ivec2 offset; +}; + +struct AnnoColorRef +{ + uint offset; +}; + +struct AnnoColor +{ + vec4 bbox; + float linewidth; + uint rgba_color; +}; + +struct AnnoBeginClipRef +{ + uint offset; +}; + +struct AnnoBeginClip +{ + vec4 bbox; + float linewidth; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct AnnotatedTag +{ + uint tag; + uint flags; +}; + +struct BinInstanceRef +{ + uint offset; +}; + +struct BinInstance +{ + uint element_ix; +}; + +struct PathRef +{ + uint offset; +}; + +struct TileRef +{ + uint offset; +}; + +struct Path +{ + uvec4 bbox; + TileRef tiles; +}; + +struct TileSegRef +{ + uint offset; +}; + +struct Tile +{ + TileSegRef tile; + int backdrop; +}; + +struct CmdStrokeRef +{ + uint offset; +}; + +struct CmdStroke +{ + uint tile_ref; + float half_width; +}; + +struct CmdFillRef +{ + uint offset; +}; + +struct CmdFill +{ + uint tile_ref; + int backdrop; +}; + +struct CmdColorRef +{ + uint offset; +}; + +struct CmdColor +{ + uint rgba_color; +}; + +struct CmdImageRef +{ + uint offset; +}; + +struct CmdImage +{ + uint index; + ivec2 offset; +}; + +struct CmdJumpRef +{ + uint offset; +}; + +struct CmdJump +{ + uint new_ref; +}; + +struct CmdRef +{ + uint offset; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _276; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _1066; + +shared uint sh_bitmaps[4][128]; +shared Alloc sh_part_elements[128]; +shared uint sh_part_count[128]; +shared uint sh_elements[128]; +shared uint sh_tile_stride[128]; +shared uint sh_tile_width[128]; +shared uint sh_tile_x0[128]; +shared uint sh_tile_y0[128]; +shared uint sh_tile_base[128]; +shared uint sh_tile_count[128]; + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +Alloc slice_mem(Alloc a, uint offset, uint size) +{ + uint param = a.offset + offset; + uint param_1 = size; + return new_alloc(param, param_1); +} + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _276.memory[offset]; + return v; +} + +BinInstanceRef BinInstance_index(BinInstanceRef ref, uint index) +{ + return BinInstanceRef(ref.offset + (index * 4u)); +} + +BinInstance BinInstance_read(Alloc a, BinInstanceRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + BinInstance s; + s.element_ix = raw0; + return s; +} + +AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +Path Path_read(Alloc a, PathRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Path s; + s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16)); + s.tiles = TileRef(raw2); + return s; +} + +void write_tile_alloc(uint el_ix, Alloc a) +{ +} + +Alloc read_tile_alloc(uint el_ix) +{ + uint param = 0u; + uint param_1 = uint(int(uint(_276.memory.length())) * 4); + return new_alloc(param, param_1); +} + +Tile Tile_read(Alloc a, TileRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Tile s; + s.tile = TileSegRef(raw0); + s.backdrop = int(raw1); + return s; +} + +AnnoColor AnnoColor_read(Alloc a, AnnoColorRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + AnnoColor s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.linewidth = uintBitsToFloat(raw4); + s.rgba_color = raw5; + return s; +} + +AnnoColor Annotated_Color_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoColorRef param_1 = AnnoColorRef(ref.offset + 4u); + return AnnoColor_read(param, param_1); +} + +MallocResult malloc(uint size) +{ + MallocResult r; + r.failed = false; + uint _282 = atomicAdd(_276.mem_offset, size); + uint offset = _282; + uint param = offset; + uint param_1 = size; + r.alloc = new_alloc(param, param_1); + if ((offset + size) > uint(int(uint(_276.memory.length())) * 4)) + { + r.failed = true; + uint _303 = atomicMax(_276.mem_error, 1u); + return r; + } + return r; +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _276.memory[offset] = val; +} + +void CmdJump_write(Alloc a, CmdJumpRef ref, CmdJump s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.new_ref; + write_mem(param, param_1, param_2); +} + +void Cmd_Jump_write(Alloc a, CmdRef ref, CmdJump s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 9u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdJumpRef param_4 = CmdJumpRef(ref.offset + 4u); + CmdJump param_5 = s; + CmdJump_write(param_3, param_4, param_5); +} + +bool alloc_cmd(inout Alloc cmd_alloc, inout CmdRef cmd_ref, inout uint cmd_limit) +{ + if (cmd_ref.offset < cmd_limit) + { + return true; + } + uint param = 1024u; + MallocResult _968 = malloc(param); + MallocResult new_cmd = _968; + if (new_cmd.failed) + { + return false; + } + CmdJump jump = CmdJump(new_cmd.alloc.offset); + Alloc param_1 = cmd_alloc; + CmdRef param_2 = cmd_ref; + CmdJump param_3 = jump; + Cmd_Jump_write(param_1, param_2, param_3); + cmd_alloc = new_cmd.alloc; + cmd_ref = CmdRef(cmd_alloc.offset); + cmd_limit = (cmd_alloc.offset + 1024u) - 36u; + return true; +} + +uint fill_mode_from_flags(uint flags) +{ + return flags & 1u; +} + +void CmdFill_write(Alloc a, CmdFillRef ref, CmdFill s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.tile_ref; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = uint(s.backdrop); + write_mem(param_3, param_4, param_5); +} + +void Cmd_Fill_write(Alloc a, CmdRef ref, CmdFill s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 1u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdFillRef param_4 = CmdFillRef(ref.offset + 4u); + CmdFill param_5 = s; + CmdFill_write(param_3, param_4, param_5); +} + +void Cmd_Solid_write(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 3u; + write_mem(param, param_1, param_2); +} + +void CmdStroke_write(Alloc a, CmdStrokeRef ref, CmdStroke s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.tile_ref; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.half_width); + write_mem(param_3, param_4, param_5); +} + +void Cmd_Stroke_write(Alloc a, CmdRef ref, CmdStroke s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 2u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdStrokeRef param_4 = CmdStrokeRef(ref.offset + 4u); + CmdStroke param_5 = s; + CmdStroke_write(param_3, param_4, param_5); +} + +void write_fill(Alloc alloc, inout CmdRef cmd_ref, uint flags, Tile tile, float linewidth) +{ + uint param = flags; + if (fill_mode_from_flags(param) == 0u) + { + if (tile.tile.offset != 0u) + { + CmdFill cmd_fill = CmdFill(tile.tile.offset, tile.backdrop); + Alloc param_1 = alloc; + CmdRef param_2 = cmd_ref; + CmdFill param_3 = cmd_fill; + Cmd_Fill_write(param_1, param_2, param_3); + cmd_ref.offset += 12u; + } + else + { + Alloc param_4 = alloc; + CmdRef param_5 = cmd_ref; + Cmd_Solid_write(param_4, param_5); + cmd_ref.offset += 4u; + } + } + else + { + CmdStroke cmd_stroke = CmdStroke(tile.tile.offset, 0.5 * linewidth); + Alloc param_6 = alloc; + CmdRef param_7 = cmd_ref; + CmdStroke param_8 = cmd_stroke; + Cmd_Stroke_write(param_6, param_7, param_8); + cmd_ref.offset += 12u; + } +} + +void CmdColor_write(Alloc a, CmdColorRef ref, CmdColor s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.rgba_color; + write_mem(param, param_1, param_2); +} + +void Cmd_Color_write(Alloc a, CmdRef ref, CmdColor s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 5u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdColorRef param_4 = CmdColorRef(ref.offset + 4u); + CmdColor param_5 = s; + CmdColor_write(param_3, param_4, param_5); +} + +AnnoImage AnnoImage_read(Alloc a, AnnoImageRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 6u; + uint raw6 = read_mem(param_12, param_13); + AnnoImage s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.linewidth = uintBitsToFloat(raw4); + s.index = raw5; + s.offset = ivec2(int(raw6 << uint(16)) >> 16, int(raw6) >> 16); + return s; +} + +AnnoImage Annotated_Image_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoImageRef param_1 = AnnoImageRef(ref.offset + 4u); + return AnnoImage_read(param, param_1); +} + +void CmdImage_write(Alloc a, CmdImageRef ref, CmdImage s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.index; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = (uint(s.offset.x) & 65535u) | (uint(s.offset.y) << uint(16)); + write_mem(param_3, param_4, param_5); +} + +void Cmd_Image_write(Alloc a, CmdRef ref, CmdImage s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 6u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdImageRef param_4 = CmdImageRef(ref.offset + 4u); + CmdImage param_5 = s; + CmdImage_write(param_3, param_4, param_5); +} + +AnnoBeginClip AnnoBeginClip_read(Alloc a, AnnoBeginClipRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + AnnoBeginClip s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.linewidth = uintBitsToFloat(raw4); + return s; +} + +AnnoBeginClip Annotated_BeginClip_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoBeginClipRef param_1 = AnnoBeginClipRef(ref.offset + 4u); + return AnnoBeginClip_read(param, param_1); +} + +void Cmd_BeginClip_write(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 7u; + write_mem(param, param_1, param_2); +} + +void Cmd_EndClip_write(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 8u; + write_mem(param, param_1, param_2); +} + +void Cmd_End_write(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 0u; + write_mem(param, param_1, param_2); +} + +void alloc_write(Alloc a, uint offset, Alloc alloc) +{ + Alloc param = a; + uint param_1 = offset >> uint(2); + uint param_2 = alloc.offset; + write_mem(param, param_1, param_2); +} + +void main() +{ + if (_276.mem_error != 0u) + { + return; + } + uint width_in_bins = ((_1066.conf.width_in_tiles + 16u) - 1u) / 16u; + uint bin_ix = (width_in_bins * gl_WorkGroupID.y) + gl_WorkGroupID.x; + uint partition_ix = 0u; + uint n_partitions = ((_1066.conf.n_elements + 128u) - 1u) / 128u; + uint th_ix = gl_LocalInvocationID.x; + uint bin_tile_x = 16u * gl_WorkGroupID.x; + uint bin_tile_y = 8u * gl_WorkGroupID.y; + uint tile_x = gl_LocalInvocationID.x % 16u; + uint tile_y = gl_LocalInvocationID.x / 16u; + uint this_tile_ix = (((bin_tile_y + tile_y) * _1066.conf.width_in_tiles) + bin_tile_x) + tile_x; + Alloc param; + param.offset = _1066.conf.ptcl_alloc.offset; + uint param_1 = this_tile_ix * 1024u; + uint param_2 = 1024u; + Alloc cmd_alloc = slice_mem(param, param_1, param_2); + CmdRef cmd_ref = CmdRef(cmd_alloc.offset); + uint cmd_limit = (cmd_ref.offset + 1024u) - 36u; + uint clip_depth = 0u; + uint clip_zero_depth = 0u; + uint clip_one_mask = 0u; + uint rd_ix = 0u; + uint wr_ix = 0u; + uint part_start_ix = 0u; + uint ready_ix = 0u; + Alloc param_3 = cmd_alloc; + uint param_4 = 0u; + uint param_5 = 8u; + Alloc scratch_alloc = slice_mem(param_3, param_4, param_5); + cmd_ref.offset += 8u; + uint num_begin_slots = 0u; + uint begin_slot = 0u; + Alloc param_6; + Alloc param_8; + uint _1354; + uint element_ix; + AnnotatedRef ref; + Alloc param_16; + Alloc param_18; + uint tile_count; + Alloc param_24; + uint _1667; + bool include_tile; + Alloc param_29; + Tile tile_1; + Alloc param_34; + Alloc param_50; + Alloc param_66; + while (true) + { + for (uint i = 0u; i < 4u; i++) + { + sh_bitmaps[i][th_ix] = 0u; + } + bool _1406; + for (;;) + { + if ((ready_ix == wr_ix) && (partition_ix < n_partitions)) + { + part_start_ix = ready_ix; + uint count = 0u; + bool _1204 = th_ix < 128u; + bool _1212; + if (_1204) + { + _1212 = (partition_ix + th_ix) < n_partitions; + } + else + { + _1212 = _1204; + } + if (_1212) + { + uint in_ix = (_1066.conf.bin_alloc.offset >> uint(2)) + ((((partition_ix + th_ix) * 128u) + bin_ix) * 2u); + param_6.offset = _1066.conf.bin_alloc.offset; + uint param_7 = in_ix; + count = read_mem(param_6, param_7); + param_8.offset = _1066.conf.bin_alloc.offset; + uint param_9 = in_ix + 1u; + uint offset = read_mem(param_8, param_9); + uint param_10 = offset; + uint param_11 = count * 4u; + sh_part_elements[th_ix] = new_alloc(param_10, param_11); + } + for (uint i_1 = 0u; i_1 < 7u; i_1++) + { + if (th_ix < 128u) + { + sh_part_count[th_ix] = count; + } + barrier(); + if (th_ix < 128u) + { + if (th_ix >= uint(1 << int(i_1))) + { + count += sh_part_count[th_ix - uint(1 << int(i_1))]; + } + } + barrier(); + } + if (th_ix < 128u) + { + sh_part_count[th_ix] = part_start_ix + count; + } + barrier(); + ready_ix = sh_part_count[127]; + partition_ix += 128u; + } + uint ix = rd_ix + th_ix; + if ((ix >= wr_ix) && (ix < ready_ix)) + { + uint part_ix = 0u; + for (uint i_2 = 0u; i_2 < 7u; i_2++) + { + uint probe = part_ix + uint(64 >> int(i_2)); + if (ix >= sh_part_count[probe - 1u]) + { + part_ix = probe; + } + } + if (part_ix > 0u) + { + _1354 = sh_part_count[part_ix - 1u]; + } + else + { + _1354 = part_start_ix; + } + ix -= _1354; + Alloc bin_alloc = sh_part_elements[part_ix]; + BinInstanceRef inst_ref = BinInstanceRef(bin_alloc.offset); + BinInstanceRef param_12 = inst_ref; + uint param_13 = ix; + Alloc param_14 = bin_alloc; + BinInstanceRef param_15 = BinInstance_index(param_12, param_13); + BinInstance inst = BinInstance_read(param_14, param_15); + sh_elements[th_ix] = inst.element_ix; + } + barrier(); + wr_ix = min((rd_ix + 128u), ready_ix); + bool _1396 = (wr_ix - rd_ix) < 128u; + if (_1396) + { + _1406 = (wr_ix < ready_ix) || (partition_ix < n_partitions); + } + else + { + _1406 = _1396; + } + if (_1406) + { + continue; + } + else + { + break; + } + } + uint tag = 0u; + if ((th_ix + rd_ix) < wr_ix) + { + element_ix = sh_elements[th_ix]; + ref = AnnotatedRef(_1066.conf.anno_alloc.offset + (element_ix * 32u)); + param_16.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_17 = ref; + tag = Annotated_tag(param_16, param_17).tag; + } + switch (tag) + { + case 1u: + case 2u: + case 3u: + case 4u: + { + uint path_ix = element_ix; + param_18.offset = _1066.conf.tile_alloc.offset; + PathRef param_19 = PathRef(_1066.conf.tile_alloc.offset + (path_ix * 12u)); + Path path = Path_read(param_18, param_19); + uint stride = path.bbox.z - path.bbox.x; + sh_tile_stride[th_ix] = stride; + int dx = int(path.bbox.x) - int(bin_tile_x); + int dy = int(path.bbox.y) - int(bin_tile_y); + int x0 = clamp(dx, 0, 16); + int y0 = clamp(dy, 0, 8); + int x1 = clamp(int(path.bbox.z) - int(bin_tile_x), 0, 16); + int y1 = clamp(int(path.bbox.w) - int(bin_tile_y), 0, 8); + sh_tile_width[th_ix] = uint(x1 - x0); + sh_tile_x0[th_ix] = uint(x0); + sh_tile_y0[th_ix] = uint(y0); + tile_count = uint(x1 - x0) * uint(y1 - y0); + uint base = path.tiles.offset - (((uint(dy) * stride) + uint(dx)) * 8u); + sh_tile_base[th_ix] = base; + uint param_20 = path.tiles.offset; + uint param_21 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u; + Alloc path_alloc = new_alloc(param_20, param_21); + uint param_22 = th_ix; + Alloc param_23 = path_alloc; + write_tile_alloc(param_22, param_23); + break; + } + default: + { + tile_count = 0u; + break; + } + } + sh_tile_count[th_ix] = tile_count; + for (uint i_3 = 0u; i_3 < 7u; i_3++) + { + barrier(); + if (th_ix >= uint(1 << int(i_3))) + { + tile_count += sh_tile_count[th_ix - uint(1 << int(i_3))]; + } + barrier(); + sh_tile_count[th_ix] = tile_count; + } + barrier(); + uint total_tile_count = sh_tile_count[127]; + for (uint ix_1 = th_ix; ix_1 < total_tile_count; ix_1 += 128u) + { + uint el_ix = 0u; + for (uint i_4 = 0u; i_4 < 7u; i_4++) + { + uint probe_1 = el_ix + uint(64 >> int(i_4)); + if (ix_1 >= sh_tile_count[probe_1 - 1u]) + { + el_ix = probe_1; + } + } + AnnotatedRef ref_1 = AnnotatedRef(_1066.conf.anno_alloc.offset + (sh_elements[el_ix] * 32u)); + param_24.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_25 = ref_1; + uint tag_1 = Annotated_tag(param_24, param_25).tag; + if (el_ix > 0u) + { + _1667 = sh_tile_count[el_ix - 1u]; + } + else + { + _1667 = 0u; + } + uint seq_ix = ix_1 - _1667; + uint width = sh_tile_width[el_ix]; + uint x = sh_tile_x0[el_ix] + (seq_ix % width); + uint y = sh_tile_y0[el_ix] + (seq_ix / width); + if ((tag_1 == 3u) || (tag_1 == 4u)) + { + include_tile = true; + } + else + { + uint param_26 = el_ix; + Alloc param_27 = read_tile_alloc(param_26); + TileRef param_28 = TileRef(sh_tile_base[el_ix] + (((sh_tile_stride[el_ix] * y) + x) * 8u)); + Tile tile = Tile_read(param_27, param_28); + bool _1728 = tile.tile.offset != 0u; + bool _1735; + if (!_1728) + { + _1735 = tile.backdrop != 0; + } + else + { + _1735 = _1728; + } + include_tile = _1735; + } + if (include_tile) + { + uint el_slice = el_ix / 32u; + uint el_mask = uint(1 << int(el_ix & 31u)); + uint _1755 = atomicOr(sh_bitmaps[el_slice][(y * 16u) + x], el_mask); + } + } + barrier(); + uint slice_ix = 0u; + uint bitmap = sh_bitmaps[0][th_ix]; + while (true) + { + if (bitmap == 0u) + { + slice_ix++; + if (slice_ix == 4u) + { + break; + } + bitmap = sh_bitmaps[slice_ix][th_ix]; + if (bitmap == 0u) + { + continue; + } + } + uint element_ref_ix = (slice_ix * 32u) + uint(findLSB(bitmap)); + uint element_ix_1 = sh_elements[element_ref_ix]; + bitmap &= (bitmap - 1u); + ref = AnnotatedRef(_1066.conf.anno_alloc.offset + (element_ix_1 * 32u)); + param_29.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_30 = ref; + AnnotatedTag tag_2 = Annotated_tag(param_29, param_30); + if (clip_zero_depth == 0u) + { + switch (tag_2.tag) + { + case 1u: + { + uint param_31 = element_ref_ix; + Alloc param_32 = read_tile_alloc(param_31); + TileRef param_33 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u)); + tile_1 = Tile_read(param_32, param_33); + param_34.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_35 = ref; + AnnoColor fill = Annotated_Color_read(param_34, param_35); + Alloc param_36 = cmd_alloc; + CmdRef param_37 = cmd_ref; + uint param_38 = cmd_limit; + bool _1865 = alloc_cmd(param_36, param_37, param_38); + cmd_alloc = param_36; + cmd_ref = param_37; + cmd_limit = param_38; + if (!_1865) + { + break; + } + Alloc param_39 = cmd_alloc; + CmdRef param_40 = cmd_ref; + uint param_41 = tag_2.flags; + Tile param_42 = tile_1; + float param_43 = fill.linewidth; + write_fill(param_39, param_40, param_41, param_42, param_43); + cmd_ref = param_40; + Alloc param_44 = cmd_alloc; + CmdRef param_45 = cmd_ref; + CmdColor param_46 = CmdColor(fill.rgba_color); + Cmd_Color_write(param_44, param_45, param_46); + cmd_ref.offset += 8u; + break; + } + case 2u: + { + uint param_47 = element_ref_ix; + Alloc param_48 = read_tile_alloc(param_47); + TileRef param_49 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u)); + tile_1 = Tile_read(param_48, param_49); + param_50.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_51 = ref; + AnnoImage fill_img = Annotated_Image_read(param_50, param_51); + Alloc param_52 = cmd_alloc; + CmdRef param_53 = cmd_ref; + uint param_54 = cmd_limit; + bool _1935 = alloc_cmd(param_52, param_53, param_54); + cmd_alloc = param_52; + cmd_ref = param_53; + cmd_limit = param_54; + if (!_1935) + { + break; + } + Alloc param_55 = cmd_alloc; + CmdRef param_56 = cmd_ref; + uint param_57 = tag_2.flags; + Tile param_58 = tile_1; + float param_59 = fill_img.linewidth; + write_fill(param_55, param_56, param_57, param_58, param_59); + cmd_ref = param_56; + Alloc param_60 = cmd_alloc; + CmdRef param_61 = cmd_ref; + CmdImage param_62 = CmdImage(fill_img.index, fill_img.offset); + Cmd_Image_write(param_60, param_61, param_62); + cmd_ref.offset += 12u; + break; + } + case 3u: + { + uint param_63 = element_ref_ix; + Alloc param_64 = read_tile_alloc(param_63); + TileRef param_65 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u)); + tile_1 = Tile_read(param_64, param_65); + bool _1994 = tile_1.tile.offset == 0u; + bool _2000; + if (_1994) + { + _2000 = tile_1.backdrop == 0; + } + else + { + _2000 = _1994; + } + if (_2000) + { + clip_zero_depth = clip_depth + 1u; + } + else + { + if ((tile_1.tile.offset == 0u) && (clip_depth < 32u)) + { + clip_one_mask |= uint(1 << int(clip_depth)); + } + else + { + param_66.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_67 = ref; + AnnoBeginClip begin_clip = Annotated_BeginClip_read(param_66, param_67); + Alloc param_68 = cmd_alloc; + CmdRef param_69 = cmd_ref; + uint param_70 = cmd_limit; + bool _2035 = alloc_cmd(param_68, param_69, param_70); + cmd_alloc = param_68; + cmd_ref = param_69; + cmd_limit = param_70; + if (!_2035) + { + break; + } + Alloc param_71 = cmd_alloc; + CmdRef param_72 = cmd_ref; + uint param_73 = tag_2.flags; + Tile param_74 = tile_1; + float param_75 = begin_clip.linewidth; + write_fill(param_71, param_72, param_73, param_74, param_75); + cmd_ref = param_72; + Alloc param_76 = cmd_alloc; + CmdRef param_77 = cmd_ref; + Cmd_BeginClip_write(param_76, param_77); + cmd_ref.offset += 4u; + if (clip_depth < 32u) + { + clip_one_mask &= uint(~(1 << int(clip_depth))); + } + begin_slot++; + num_begin_slots = max(num_begin_slots, begin_slot); + } + } + clip_depth++; + break; + } + case 4u: + { + clip_depth--; + bool _2087 = clip_depth >= 32u; + bool _2097; + if (!_2087) + { + _2097 = (clip_one_mask & uint(1 << int(clip_depth))) == 0u; + } + else + { + _2097 = _2087; + } + if (_2097) + { + Alloc param_78 = cmd_alloc; + CmdRef param_79 = cmd_ref; + uint param_80 = cmd_limit; + bool _2106 = alloc_cmd(param_78, param_79, param_80); + cmd_alloc = param_78; + cmd_ref = param_79; + cmd_limit = param_80; + if (!_2106) + { + break; + } + Alloc param_81 = cmd_alloc; + CmdRef param_82 = cmd_ref; + Cmd_Solid_write(param_81, param_82); + cmd_ref.offset += 4u; + begin_slot--; + Alloc param_83 = cmd_alloc; + CmdRef param_84 = cmd_ref; + Cmd_EndClip_write(param_83, param_84); + cmd_ref.offset += 4u; + } + break; + } + } + } + else + { + switch (tag_2.tag) + { + case 3u: + { + clip_depth++; + break; + } + case 4u: + { + if (clip_depth == clip_zero_depth) + { + clip_zero_depth = 0u; + } + clip_depth--; + break; + } + } + } + } + barrier(); + rd_ix += 128u; + if ((rd_ix >= ready_ix) && (partition_ix >= n_partitions)) + { + break; + } + } + bool _2171 = (bin_tile_x + tile_x) < _1066.conf.width_in_tiles; + bool _2180; + if (_2171) + { + _2180 = (bin_tile_y + tile_y) < _1066.conf.height_in_tiles; + } + else + { + _2180 = _2171; + } + if (_2180) + { + Alloc param_85 = cmd_alloc; + CmdRef param_86 = cmd_ref; + Cmd_End_write(param_85, param_86); + if (num_begin_slots > 0u) + { + uint scratch_size = (((num_begin_slots * 32u) * 32u) * 2u) * 4u; + uint param_87 = scratch_size; + MallocResult _2201 = malloc(param_87); + MallocResult scratch = _2201; + Alloc param_88 = scratch_alloc; + uint param_89 = scratch_alloc.offset; + Alloc param_90 = scratch.alloc; + alloc_write(param_88, param_89, param_90); + } + } +} + +`, + } + shader_copy_frag = driver.ShaderSources{ + Name: "copy.frag", + Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}}, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +layout(location = 0) out highp vec4 fragColor; + +highp vec3 sRGBtoRGB(highp vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375)); + highp vec3 below = rgb / vec3(12.9200000762939453125); + highp vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625)); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + highp vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0); + highp vec3 param = texel.xyz; + highp vec3 rgb = sRGBtoRGB(param); + fragColor = vec4(rgb, texel.w); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +out vec4 fragColor; + +vec3 sRGBtoRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375)); + vec3 below = rgb / vec3(12.9200000762939453125); + vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625)); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0); + vec3 param = texel.xyz; + vec3 rgb = sRGBtoRGB(param); + fragColor = vec4(rgb, texel.w); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +out vec4 fragColor; + +vec3 sRGBtoRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375)); + vec3 below = rgb / vec3(12.9200000762939453125); + vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625)); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0); + vec3 param = texel.xyz; + vec3 rgb = sRGBtoRGB(param); + fragColor = vec4(rgb, texel.w); +} + +`, + HLSL: "DXBC\xe6\x89_t\x8b\xfc\xea8\xd9'\xad5.ƈk\x01\x00\x00\x00H\x03\x00\x00\x05\x00\x00\x004\x00\x00\x00\xa4\x00\x00\x00\xd8\x00\x00\x00\f\x01\x00\x00\xcc\x02\x00\x00RDEFh\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00@\x00\x00\x00<\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x03\x00\x00SV_Position\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xabSHDR\xb8\x01\x00\x00@\x00\x00\x00n\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00d \x00\x042\x10\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x00\x1b\x00\x00\x052\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00\x00\a\xf2\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\xaeGa=\xaeGa=\xaeGa=\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00o\xa7r?o\xa7r?o\xa7r?\x00\x00\x00\x00/\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00\x9a\x99\x19@\x9a\x99\x19@\x9a\x99\x19@\x00\x00\x00\x00\x19\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x1d\x00\x00\nr\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\xe6\xae%=\xe6\xae%=\xe6\xae%=\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x00\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x91\x83\x9e=\x91\x83\x9e=\x91\x83\x9e=\x00\x00\x00\x006\x00\x00\x05\x82 \x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x007\x00\x00\tr \x10\x00\x00\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\r\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + } + shader_copy_vert = driver.ShaderSources{ + Name: "copy.vert", + GLSL100ES: `#version 100 + +void main() +{ + for (int spvDummy6 = 0; spvDummy6 < 1; spvDummy6++) + { + if (gl_VertexID == 0) + { + gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); + break; + } + else if (gl_VertexID == 1) + { + gl_Position = vec4(1.0, 1.0, 0.0, 1.0); + break; + } + else if (gl_VertexID == 2) + { + gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); + break; + } + else if (gl_VertexID == 3) + { + gl_Position = vec4(1.0, -1.0, 0.0, 1.0); + break; + } + } +} + +`, + GLSL300ES: `#version 300 es + +void main() +{ + switch (gl_VertexID) + { + case 0: + { + gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); + break; + } + case 1: + { + gl_Position = vec4(1.0, 1.0, 0.0, 1.0); + break; + } + case 2: + { + gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); + break; + } + case 3: + { + gl_Position = vec4(1.0, -1.0, 0.0, 1.0); + break; + } + } +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +void main() +{ + switch (gl_VertexID) + { + case 0: + { + gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); + break; + } + case 1: + { + gl_Position = vec4(1.0, 1.0, 0.0, 1.0); + break; + } + case 2: + { + gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); + break; + } + case 3: + { + gl_Position = vec4(1.0, -1.0, 0.0, 1.0); + break; + } + } +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +void main() +{ + switch (gl_VertexID) + { + case 0: + { + gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); + break; + } + case 1: + { + gl_Position = vec4(1.0, 1.0, 0.0, 1.0); + break; + } + case 2: + { + gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); + break; + } + case 3: + { + gl_Position = vec4(1.0, -1.0, 0.0, 1.0); + break; + } + } +} + +`, + HLSL: "DXBC\x99\xb4[\xef]IX\xa2Qh\x9f\xb6!\x1cR\xe7\x01\x00\x00\x00\xc0\x02\x00\x00\x05\x00\x00\x004\x00\x00\x00\x80\x00\x00\x00\xb4\x00\x00\x00\xe8\x00\x00\x00D\x02\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00SV_VertexID\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00SHDRT\x01\x00\x00@\x00\x01\x00U\x00\x00\x00`\x00\x00\x04\x12\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00L\x00\x00\x03\n\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x03\x01@\x00\x00\x00\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x01\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80?\x00\x00\x80?\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x02\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x03\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80?\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\n\x00\x00\x016\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x01\x17\x00\x00\x016\x00\x00\x05\xb2 \x10\x00\x00\x00\x00\x00F\b\x10\x00\x00\x00\x00\x006\x00\x00\x05B \x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x14\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + } + shader_cover_frag = [...]driver.ShaderSources{ + { + Name: "cover.frag", + Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Color", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_color.color", Type: 0x0, Size: 4, Offset: 0}}, + Size: 16, + }, + Textures: []driver.TextureBinding{{Name: "cover", Binding: 1}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +struct Color +{ + vec4 color; +}; + +uniform Color _color; + +uniform mediump sampler2D cover; + +varying highp vec2 vCoverUV; +varying vec2 vUV; + +void main() +{ + gl_FragData[0] = _color.color; + float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0); + gl_FragData[0] *= cover_1; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(std140) uniform Color +{ + vec4 color; +} _color; + +uniform mediump sampler2D cover; + +layout(location = 0) out vec4 fragColor; +in highp vec2 vCoverUV; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Color +{ + vec4 color; +}; + +uniform Color _color; + +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vCoverUV; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Color +{ + vec4 color; +} _color; + +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vCoverUV; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + HLSL: "DXBC\x88\x01{\x0f\x94\xca3\xeb\xabßø\xa1\xbfL1\xbf\x01\x00\x00\x00\xa4\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xcc\x00\x00\x00\x90\x01\x00\x00\f\x02\x00\x00$\x03\x00\x00p\x03\x00\x00Aon9\x8c\x00\x00\x00\x8c\x00\x00\x00\x00\x02\xff\xffX\x00\x00\x004\x00\x00\x00\x01\x00(\x00\x00\x004\x00\x00\x004\x00\x01\x00$\x00\x00\x004\x00\x01\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0#\x00\x00\x02\x00\x00\x11\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\x00\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xbc\x00\x00\x00@\x00\x00\x00/\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?8\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x10\x01\x00\x00\x01\x00\x00\x00\x98\x00\x00\x00\x03\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xe8\x00\x00\x00|\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x8b\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\x91\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00_cover_sampler\x00cover\x00Color\x00\xab\x91\x00\x00\x00\x01\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd8\x00\x00\x00\x00\x00\x00\x00_color_color\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + { + Name: "cover.frag", + Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Gradient", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_gradient.color1", Type: 0x0, Size: 4, Offset: 0}, {Name: "_gradient.color2", Type: 0x0, Size: 4, Offset: 16}}, + Size: 32, + }, + Textures: []driver.TextureBinding{{Name: "cover", Binding: 1}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +struct Gradient +{ + vec4 color1; + vec4 color2; +}; + +uniform Gradient _gradient; + +uniform mediump sampler2D cover; + +varying vec2 vUV; +varying highp vec2 vCoverUV; + +void main() +{ + gl_FragData[0] = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); + float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0); + gl_FragData[0] *= cover_1; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(std140) uniform Gradient +{ + vec4 color1; + vec4 color2; +} _gradient; + +uniform mediump sampler2D cover; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; +in highp vec2 vCoverUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Gradient +{ + vec4 color1; + vec4 color2; +}; + +uniform Gradient _gradient; + +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vUV; +in vec2 vCoverUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Gradient +{ + vec4 color1; + vec4 color2; +} _gradient; + +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vUV; +in vec2 vCoverUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + HLSL: "DXBCj\xa0\x9e\x8d\x1eƌO\rJ\xea\x8f\x17\x11o\x98\x01\x00\x00\x00\x80\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00\b\x01\x00\x008\x02\x00\x00\xb4\x02\x00\x00\x00\x04\x00\x00L\x04\x00\x00Aon9\xc8\x00\x00\x00\xc8\x00\x00\x00\x00\x02\xff\xff\x94\x00\x00\x004\x00\x00\x00\x01\x00(\x00\x00\x004\x00\x00\x004\x00\x01\x00$\x00\x00\x004\x00\x01\x01\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x01\x00\x00\x02\x00\x00\x12\x80\x00\x00\xff\xb0\x01\x00\x00\x02\x01\x00\x0f\x80\x00\x00\xe4\xa0\x02\x00\x00\x03\x01\x00\x0f\x80\x01\x00\xe4\x81\x01\x00\xe4\xa0\x04\x00\x00\x04\x01\x00\x0f\x80\x00\x00U\x80\x01\x00\xe4\x80\x00\x00\xe4\xa0#\x00\x00\x02\x00\x00\x11\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\x00\x80\x01\x00\xe4\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR(\x01\x00\x00@\x00\x00\x00J\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03B\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006 \x00\x05\x12\x00\x10\x00\x00\x00\x00\x00*\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x8e \x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x002\x00\x00\n\xf2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?8\x00\x00\a\xf2 \x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x01\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\a\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x01\x00\x00\x01\x00\x00\x00\x9c\x00\x00\x00\x03\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x19\x01\x00\x00|\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x8b\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\x91\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00_cover_sampler\x00cover\x00Gradient\x00\xab\xab\x91\x00\x00\x00\x02\x00\x00\x00\xb4\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe4\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x00\x00\b\x01\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x00\x00_gradient_color1\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_gradient_color2\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x04\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + { + Name: "cover.frag", + Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}, {Name: "cover", Binding: 1}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; +uniform mediump sampler2D cover; + +varying vec2 vUV; +varying highp vec2 vCoverUV; + +void main() +{ + gl_FragData[0] = texture2D(tex, vUV); + float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0); + gl_FragData[0] *= cover_1; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; +uniform mediump sampler2D cover; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; +in highp vec2 vCoverUV; + +void main() +{ + fragColor = texture(tex, vUV); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vUV; +in vec2 vCoverUV; + +void main() +{ + fragColor = texture(tex, vUV); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vUV; +in vec2 vCoverUV; + +void main() +{ + fragColor = texture(tex, vUV); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + HLSL: "DXBC\x99\x16l`\xf6:k\xa2Y$\xa1,\xfd\xcdJE\x01\x00\x00\x00\xd8\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xec\x00\x00\x00\xe8\x01\x00\x00d\x02\x00\x00X\x03\x00\x00\xa4\x03\x00\x00Aon9\xac\x00\x00\x00\xac\x00\x00\x00\x00\x02\xff\xff\x80\x00\x00\x00,\x00\x00\x00\x00\x00,\x00\x00\x00,\x00\x00\x00,\x00\x02\x00$\x00\x00\x00,\x00\x00\x00\x00\x00\x01\x01\x01\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0\x1f\x00\x00\x02\x00\x00\x00\x90\x01\b\x0f\xa0\x01\x00\x00\x02\x00\x00\x03\x80\x00\x00\x1b\xb0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\x80\x00\b\xe4\xa0B\x00\x00\x03\x01\x00\x0f\x80\x00\x00\xe4\xb0\x01\b\xe4\xa0#\x00\x00\x02\x01\x00\x11\x80\x01\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\x80\x01\x00\x00\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xf4\x00\x00\x00@\x00\x00\x00=\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03\xc2\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?E\x00\x00\t\xf2\x00\x10\x00\x01\x00\x00\x00\xe6\x1a\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x008\x00\x00\a\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x05\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xec\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xc2\x00\x00\x00\x9c\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\xa9\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\xb8\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\xbc\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00_cover_sampler\x00tex\x00cover\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\f\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + } + shader_cover_vert = driver.ShaderSources{ + Name: "cover.vert", + Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.uvCoverTransform", Type: 0x0, Size: 4, Offset: 16}, {Name: "_block.uvTransformR1", Type: 0x0, Size: 4, Offset: 32}, {Name: "_block.uvTransformR2", Type: 0x0, Size: 4, Offset: 48}, {Name: "_block.z", Type: 0x0, Size: 1, Offset: 64}}, + Size: 68, + }, + GLSL100ES: `#version 100 + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 transform; + vec4 uvCoverTransform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +}; + +uniform Block _block; + +attribute vec2 pos; +varying vec2 vUV; +attribute vec2 uv; +varying vec2 vCoverUV; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; + m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_4 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_3, param_4); + vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy; +} + +`, + GLSL300ES: `#version 300 es + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(std140) uniform Block +{ + vec4 transform; + vec4 uvCoverTransform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +} _block; + +layout(location = 0) in vec2 pos; +out vec2 vUV; +layout(location = 1) in vec2 uv; +out vec2 vCoverUV; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; + m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_4 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_3, param_4); + vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 transform; + vec4 uvCoverTransform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +}; + +uniform Block _block; + +in vec2 pos; +out vec2 vUV; +in vec2 uv; +out vec2 vCoverUV; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; + m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_4 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_3, param_4); + vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(binding = 0, std140) uniform Block +{ + vec4 transform; + vec4 uvCoverTransform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +} _block; + +in vec2 pos; +out vec2 vUV; +in vec2 uv; +out vec2 vCoverUV; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; + m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_4 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_3, param_4); + vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy; +} + +`, + HLSL: "DXBCx\xefn{F\v\x88%\xc6\x05\x8f4h\xe4\xaaP\x01\x00\x00\x00\xd8\x05\x00\x00\x06\x00\x00\x008\x00\x00\x00x\x01\x00\x00\x1c\x03\x00\x00\x98\x03\x00\x00\x1c\x05\x00\x00h\x05\x00\x00Aon98\x01\x00\x008\x01\x00\x00\x00\x02\xfe\xff\x04\x01\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x05\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x06\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x80\xbf\x00\x00\x00?\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\a\x80\x01\x00Đ\x06\x00Š \x06\x00Å \b\x00\x00\x03\x00\x00\b\xe0\x03\x00\xe4\xa0\x00\x00\xe4\x80\b\x00\x00\x03\x00\x00\x04\xe0\x04\x00\xe4\xa0\x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\x80\x01\x00\xe1\x90\x06\x00\xe4\xa0\x06\x00\xe1\xa0\x05\x00\x00\x03\x00\x00\x03\x80\x00\x00\xe4\x80\x06\x00\xe2\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x01\x80\x01\x00\x00\x90\x04\x00\x00\x04\x00\x00\x03\xe0\x00\x00\xe4\x80\x02\x00\xe4\xa0\x02\x00\xee\xa0\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x90\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\v\x80\x06\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\f\xc0\x05\x00\x00\xa0\x00\x00t\x80\x00\x004\x80\xff\xff\x00\x00SHDR\x9c\x01\x00\x00@\x00\x01\x00g\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x05\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00e\x00\x00\x03\xc2 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006\x00\x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x006\x00\x00\x052\x00\x10\x00\x01\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x0f\x00\x00\n\"\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x96\x05\x10\x00\x01\x00\x00\x002\x00\x00\v2 \x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x01\x00\x00\x00\x10\x00\x00\bB \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x10\x00\x00\b\x82 \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x03\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x002\x00\x00\v2 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\nB \x10\x00\x01\x00\x00\x00\n\x80 \x00\x00\x00\x00\x00\x04\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\x01@\x00\x00\x00\x00\x00?6\x00\x00\x05\x82 \x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\v\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF|\x01\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00T\x01\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x05\x00\x00\x00\\\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00\xf8\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00\x10\x01\x00\x00 \x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00%\x01\x00\x000\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00:\x01\x00\x00@\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00D\x01\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_uvCoverTransform\x00_block_uvTransformR1\x00_block_uvTransformR2\x00_block_z\x00\xab\x00\x00\x03\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNh\x00\x00\x00\x03\x00\x00\x00\b\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00P\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x03\x00\x00Y\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_elements_comp = driver.ShaderSources{ + Name: "elements.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct ElementRef +{ + uint offset; +}; + +struct LineSegRef +{ + uint offset; +}; + +struct LineSeg +{ + vec2 p0; + vec2 p1; +}; + +struct QuadSegRef +{ + uint offset; +}; + +struct QuadSeg +{ + vec2 p0; + vec2 p1; + vec2 p2; +}; + +struct CubicSegRef +{ + uint offset; +}; + +struct CubicSeg +{ + vec2 p0; + vec2 p1; + vec2 p2; + vec2 p3; +}; + +struct FillColorRef +{ + uint offset; +}; + +struct FillColor +{ + uint rgba_color; +}; + +struct FillImageRef +{ + uint offset; +}; + +struct FillImage +{ + uint index; + ivec2 offset; +}; + +struct SetLineWidthRef +{ + uint offset; +}; + +struct SetLineWidth +{ + float width; +}; + +struct TransformRef +{ + uint offset; +}; + +struct Transform +{ + vec4 mat; + vec2 translate; +}; + +struct ClipRef +{ + uint offset; +}; + +struct Clip +{ + vec4 bbox; +}; + +struct SetFillModeRef +{ + uint offset; +}; + +struct SetFillMode +{ + uint fill_mode; +}; + +struct ElementTag +{ + uint tag; + uint flags; +}; + +struct StateRef +{ + uint offset; +}; + +struct State +{ + vec4 mat; + vec2 translate; + vec4 bbox; + float linewidth; + uint flags; + uint path_count; + uint pathseg_count; + uint trans_count; +}; + +struct AnnoImageRef +{ + uint offset; +}; + +struct AnnoImage +{ + vec4 bbox; + float linewidth; + uint index; + ivec2 offset; +}; + +struct AnnoColorRef +{ + uint offset; +}; + +struct AnnoColor +{ + vec4 bbox; + float linewidth; + uint rgba_color; +}; + +struct AnnoBeginClipRef +{ + uint offset; +}; + +struct AnnoBeginClip +{ + vec4 bbox; + float linewidth; +}; + +struct AnnoEndClipRef +{ + uint offset; +}; + +struct AnnoEndClip +{ + vec4 bbox; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct PathCubicRef +{ + uint offset; +}; + +struct PathCubic +{ + vec2 p0; + vec2 p1; + vec2 p2; + vec2 p3; + uint path_ix; + uint trans_ix; + vec2 stroke; +}; + +struct PathSegRef +{ + uint offset; +}; + +struct TransformSegRef +{ + uint offset; +}; + +struct TransformSeg +{ + vec4 mat; + vec2 translate; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _294; + +layout(binding = 2, std430) readonly buffer SceneBuf +{ + uint scene[]; +} _323; + +layout(binding = 3, std430) coherent buffer StateBuf +{ + uint part_counter; + uint state[]; +} _779; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _2435; + +shared uint sh_part_ix; +shared State sh_state[32]; +shared State sh_prefix; + +ElementTag Element_tag(ElementRef ref) +{ + uint tag_and_flags = _323.scene[ref.offset >> uint(2)]; + return ElementTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +LineSeg LineSeg_read(LineSegRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + LineSeg s; + s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + return s; +} + +LineSeg Element_Line_read(ElementRef ref) +{ + LineSegRef param = LineSegRef(ref.offset + 4u); + return LineSeg_read(param); +} + +QuadSeg QuadSeg_read(QuadSegRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + uint raw4 = _323.scene[ix + 4u]; + uint raw5 = _323.scene[ix + 5u]; + QuadSeg s; + s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + return s; +} + +QuadSeg Element_Quad_read(ElementRef ref) +{ + QuadSegRef param = QuadSegRef(ref.offset + 4u); + return QuadSeg_read(param); +} + +CubicSeg CubicSeg_read(CubicSegRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + uint raw4 = _323.scene[ix + 4u]; + uint raw5 = _323.scene[ix + 5u]; + uint raw6 = _323.scene[ix + 6u]; + uint raw7 = _323.scene[ix + 7u]; + CubicSeg s; + s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + s.p3 = vec2(uintBitsToFloat(raw6), uintBitsToFloat(raw7)); + return s; +} + +CubicSeg Element_Cubic_read(ElementRef ref) +{ + CubicSegRef param = CubicSegRef(ref.offset + 4u); + return CubicSeg_read(param); +} + +SetLineWidth SetLineWidth_read(SetLineWidthRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + SetLineWidth s; + s.width = uintBitsToFloat(raw0); + return s; +} + +SetLineWidth Element_SetLineWidth_read(ElementRef ref) +{ + SetLineWidthRef param = SetLineWidthRef(ref.offset + 4u); + return SetLineWidth_read(param); +} + +Transform Transform_read(TransformRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + uint raw4 = _323.scene[ix + 4u]; + uint raw5 = _323.scene[ix + 5u]; + Transform s; + s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + return s; +} + +Transform Element_Transform_read(ElementRef ref) +{ + TransformRef param = TransformRef(ref.offset + 4u); + return Transform_read(param); +} + +SetFillMode SetFillMode_read(SetFillModeRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + SetFillMode s; + s.fill_mode = raw0; + return s; +} + +SetFillMode Element_SetFillMode_read(ElementRef ref) +{ + SetFillModeRef param = SetFillModeRef(ref.offset + 4u); + return SetFillMode_read(param); +} + +State map_element(ElementRef ref) +{ + ElementRef param = ref; + uint tag = Element_tag(param).tag; + State c; + c.bbox = vec4(0.0); + c.mat = vec4(1.0, 0.0, 0.0, 1.0); + c.translate = vec2(0.0); + c.linewidth = 1.0; + c.flags = 0u; + c.path_count = 0u; + c.pathseg_count = 0u; + c.trans_count = 0u; + switch (tag) + { + case 1u: + { + ElementRef param_1 = ref; + LineSeg line = Element_Line_read(param_1); + vec2 _1919 = min(line.p0, line.p1); + c.bbox = vec4(_1919.x, _1919.y, c.bbox.z, c.bbox.w); + vec2 _1927 = max(line.p0, line.p1); + c.bbox = vec4(c.bbox.x, c.bbox.y, _1927.x, _1927.y); + c.pathseg_count = 1u; + break; + } + case 2u: + { + ElementRef param_2 = ref; + QuadSeg quad = Element_Quad_read(param_2); + vec2 _1944 = min(min(quad.p0, quad.p1), quad.p2); + c.bbox = vec4(_1944.x, _1944.y, c.bbox.z, c.bbox.w); + vec2 _1955 = max(max(quad.p0, quad.p1), quad.p2); + c.bbox = vec4(c.bbox.x, c.bbox.y, _1955.x, _1955.y); + c.pathseg_count = 1u; + break; + } + case 3u: + { + ElementRef param_3 = ref; + CubicSeg cubic = Element_Cubic_read(param_3); + vec2 _1975 = min(min(cubic.p0, cubic.p1), min(cubic.p2, cubic.p3)); + c.bbox = vec4(_1975.x, _1975.y, c.bbox.z, c.bbox.w); + vec2 _1989 = max(max(cubic.p0, cubic.p1), max(cubic.p2, cubic.p3)); + c.bbox = vec4(c.bbox.x, c.bbox.y, _1989.x, _1989.y); + c.pathseg_count = 1u; + break; + } + case 4u: + case 9u: + case 7u: + { + c.flags = 4u; + c.path_count = 1u; + break; + } + case 8u: + { + c.path_count = 1u; + break; + } + case 5u: + { + ElementRef param_4 = ref; + SetLineWidth lw = Element_SetLineWidth_read(param_4); + c.linewidth = lw.width; + c.flags = 1u; + break; + } + case 6u: + { + ElementRef param_5 = ref; + Transform t = Element_Transform_read(param_5); + c.mat = t.mat; + c.translate = t.translate; + c.trans_count = 1u; + break; + } + case 10u: + { + ElementRef param_6 = ref; + SetFillMode fm = Element_SetFillMode_read(param_6); + c.flags = 8u | (fm.fill_mode << uint(4)); + break; + } + } + return c; +} + +ElementRef Element_index(ElementRef ref, uint index) +{ + return ElementRef(ref.offset + (index * 36u)); +} + +State combine_state(State a, State b) +{ + State c; + c.bbox.x = (min(a.mat.x * b.bbox.x, a.mat.x * b.bbox.z) + min(a.mat.z * b.bbox.y, a.mat.z * b.bbox.w)) + a.translate.x; + c.bbox.y = (min(a.mat.y * b.bbox.x, a.mat.y * b.bbox.z) + min(a.mat.w * b.bbox.y, a.mat.w * b.bbox.w)) + a.translate.y; + c.bbox.z = (max(a.mat.x * b.bbox.x, a.mat.x * b.bbox.z) + max(a.mat.z * b.bbox.y, a.mat.z * b.bbox.w)) + a.translate.x; + c.bbox.w = (max(a.mat.y * b.bbox.x, a.mat.y * b.bbox.z) + max(a.mat.w * b.bbox.y, a.mat.w * b.bbox.w)) + a.translate.y; + bool _1657 = (a.flags & 4u) == 0u; + bool _1665; + if (_1657) + { + _1665 = b.bbox.z <= b.bbox.x; + } + else + { + _1665 = _1657; + } + bool _1673; + if (_1665) + { + _1673 = b.bbox.w <= b.bbox.y; + } + else + { + _1673 = _1665; + } + if (_1673) + { + c.bbox = a.bbox; + } + else + { + bool _1683 = (a.flags & 4u) == 0u; + bool _1690; + if (_1683) + { + _1690 = (b.flags & 2u) == 0u; + } + else + { + _1690 = _1683; + } + bool _1707; + if (_1690) + { + bool _1697 = a.bbox.z > a.bbox.x; + bool _1706; + if (!_1697) + { + _1706 = a.bbox.w > a.bbox.y; + } + else + { + _1706 = _1697; + } + _1707 = _1706; + } + else + { + _1707 = _1690; + } + if (_1707) + { + vec2 _1716 = min(a.bbox.xy, c.bbox.xy); + c.bbox = vec4(_1716.x, _1716.y, c.bbox.z, c.bbox.w); + vec2 _1726 = max(a.bbox.zw, c.bbox.zw); + c.bbox = vec4(c.bbox.x, c.bbox.y, _1726.x, _1726.y); + } + } + c.mat.x = (a.mat.x * b.mat.x) + (a.mat.z * b.mat.y); + c.mat.y = (a.mat.y * b.mat.x) + (a.mat.w * b.mat.y); + c.mat.z = (a.mat.x * b.mat.z) + (a.mat.z * b.mat.w); + c.mat.w = (a.mat.y * b.mat.z) + (a.mat.w * b.mat.w); + c.translate.x = ((a.mat.x * b.translate.x) + (a.mat.z * b.translate.y)) + a.translate.x; + c.translate.y = ((a.mat.y * b.translate.x) + (a.mat.w * b.translate.y)) + a.translate.y; + float _1812; + if ((b.flags & 1u) == 0u) + { + _1812 = a.linewidth; + } + else + { + _1812 = b.linewidth; + } + c.linewidth = _1812; + c.flags = (a.flags & 11u) | b.flags; + c.flags |= ((a.flags & 4u) >> uint(1)); + uint _1842; + if ((b.flags & 8u) == 0u) + { + _1842 = a.flags; + } + else + { + _1842 = b.flags; + } + uint fill_mode = _1842; + fill_mode &= 16u; + c.flags = (c.flags & 4294967279u) | fill_mode; + c.path_count = a.path_count + b.path_count; + c.pathseg_count = a.pathseg_count + b.pathseg_count; + c.trans_count = a.trans_count + b.trans_count; + return c; +} + +StateRef state_aggregate_ref(uint partition_ix) +{ + return StateRef(4u + (partition_ix * 124u)); +} + +void State_write(StateRef ref, State s) +{ + uint ix = ref.offset >> uint(2); + _779.state[ix + 0u] = floatBitsToUint(s.mat.x); + _779.state[ix + 1u] = floatBitsToUint(s.mat.y); + _779.state[ix + 2u] = floatBitsToUint(s.mat.z); + _779.state[ix + 3u] = floatBitsToUint(s.mat.w); + _779.state[ix + 4u] = floatBitsToUint(s.translate.x); + _779.state[ix + 5u] = floatBitsToUint(s.translate.y); + _779.state[ix + 6u] = floatBitsToUint(s.bbox.x); + _779.state[ix + 7u] = floatBitsToUint(s.bbox.y); + _779.state[ix + 8u] = floatBitsToUint(s.bbox.z); + _779.state[ix + 9u] = floatBitsToUint(s.bbox.w); + _779.state[ix + 10u] = floatBitsToUint(s.linewidth); + _779.state[ix + 11u] = s.flags; + _779.state[ix + 12u] = s.path_count; + _779.state[ix + 13u] = s.pathseg_count; + _779.state[ix + 14u] = s.trans_count; +} + +StateRef state_prefix_ref(uint partition_ix) +{ + return StateRef((4u + (partition_ix * 124u)) + 60u); +} + +uint state_flag_index(uint partition_ix) +{ + return partition_ix * 31u; +} + +State State_read(StateRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _779.state[ix + 0u]; + uint raw1 = _779.state[ix + 1u]; + uint raw2 = _779.state[ix + 2u]; + uint raw3 = _779.state[ix + 3u]; + uint raw4 = _779.state[ix + 4u]; + uint raw5 = _779.state[ix + 5u]; + uint raw6 = _779.state[ix + 6u]; + uint raw7 = _779.state[ix + 7u]; + uint raw8 = _779.state[ix + 8u]; + uint raw9 = _779.state[ix + 9u]; + uint raw10 = _779.state[ix + 10u]; + uint raw11 = _779.state[ix + 11u]; + uint raw12 = _779.state[ix + 12u]; + uint raw13 = _779.state[ix + 13u]; + uint raw14 = _779.state[ix + 14u]; + State s; + s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + s.bbox = vec4(uintBitsToFloat(raw6), uintBitsToFloat(raw7), uintBitsToFloat(raw8), uintBitsToFloat(raw9)); + s.linewidth = uintBitsToFloat(raw10); + s.flags = raw11; + s.path_count = raw12; + s.pathseg_count = raw13; + s.trans_count = raw14; + return s; +} + +uint fill_mode_from_flags(uint flags) +{ + return flags & 1u; +} + +vec2 get_linewidth(State st) +{ + return vec2(length(st.mat.xz), length(st.mat.yw)) * (0.5 * st.linewidth); +} + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _294.memory[offset] = val; +} + +void PathCubic_write(Alloc a, PathCubicRef ref, PathCubic s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.p0.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.p0.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.p1.x); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.p1.y); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.p2.x); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = floatBitsToUint(s.p2.y); + write_mem(param_15, param_16, param_17); + Alloc param_18 = a; + uint param_19 = ix + 6u; + uint param_20 = floatBitsToUint(s.p3.x); + write_mem(param_18, param_19, param_20); + Alloc param_21 = a; + uint param_22 = ix + 7u; + uint param_23 = floatBitsToUint(s.p3.y); + write_mem(param_21, param_22, param_23); + Alloc param_24 = a; + uint param_25 = ix + 8u; + uint param_26 = s.path_ix; + write_mem(param_24, param_25, param_26); + Alloc param_27 = a; + uint param_28 = ix + 9u; + uint param_29 = s.trans_ix; + write_mem(param_27, param_28, param_29); + Alloc param_30 = a; + uint param_31 = ix + 10u; + uint param_32 = floatBitsToUint(s.stroke.x); + write_mem(param_30, param_31, param_32); + Alloc param_33 = a; + uint param_34 = ix + 11u; + uint param_35 = floatBitsToUint(s.stroke.y); + write_mem(param_33, param_34, param_35); +} + +void PathSeg_Cubic_write(Alloc a, PathSegRef ref, uint flags, PathCubic s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = (flags << uint(16)) | 1u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + PathCubicRef param_4 = PathCubicRef(ref.offset + 4u); + PathCubic param_5 = s; + PathCubic_write(param_3, param_4, param_5); +} + +FillColor FillColor_read(FillColorRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + FillColor s; + s.rgba_color = raw0; + return s; +} + +FillColor Element_FillColor_read(ElementRef ref) +{ + FillColorRef param = FillColorRef(ref.offset + 4u); + return FillColor_read(param); +} + +void AnnoColor_write(Alloc a, AnnoColorRef ref, AnnoColor s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.bbox.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.bbox.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.bbox.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.bbox.w); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.linewidth); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = s.rgba_color; + write_mem(param_15, param_16, param_17); +} + +void Annotated_Color_write(Alloc a, AnnotatedRef ref, uint flags, AnnoColor s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = (flags << uint(16)) | 1u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + AnnoColorRef param_4 = AnnoColorRef(ref.offset + 4u); + AnnoColor param_5 = s; + AnnoColor_write(param_3, param_4, param_5); +} + +FillImage FillImage_read(FillImageRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + FillImage s; + s.index = raw0; + s.offset = ivec2(int(raw1 << uint(16)) >> 16, int(raw1) >> 16); + return s; +} + +FillImage Element_FillImage_read(ElementRef ref) +{ + FillImageRef param = FillImageRef(ref.offset + 4u); + return FillImage_read(param); +} + +void AnnoImage_write(Alloc a, AnnoImageRef ref, AnnoImage s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.bbox.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.bbox.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.bbox.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.bbox.w); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.linewidth); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = s.index; + write_mem(param_15, param_16, param_17); + Alloc param_18 = a; + uint param_19 = ix + 6u; + uint param_20 = (uint(s.offset.x) & 65535u) | (uint(s.offset.y) << uint(16)); + write_mem(param_18, param_19, param_20); +} + +void Annotated_Image_write(Alloc a, AnnotatedRef ref, uint flags, AnnoImage s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = (flags << uint(16)) | 2u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + AnnoImageRef param_4 = AnnoImageRef(ref.offset + 4u); + AnnoImage param_5 = s; + AnnoImage_write(param_3, param_4, param_5); +} + +Clip Clip_read(ClipRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + Clip s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + return s; +} + +Clip Element_BeginClip_read(ElementRef ref) +{ + ClipRef param = ClipRef(ref.offset + 4u); + return Clip_read(param); +} + +void AnnoBeginClip_write(Alloc a, AnnoBeginClipRef ref, AnnoBeginClip s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.bbox.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.bbox.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.bbox.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.bbox.w); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.linewidth); + write_mem(param_12, param_13, param_14); +} + +void Annotated_BeginClip_write(Alloc a, AnnotatedRef ref, uint flags, AnnoBeginClip s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = (flags << uint(16)) | 3u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + AnnoBeginClipRef param_4 = AnnoBeginClipRef(ref.offset + 4u); + AnnoBeginClip param_5 = s; + AnnoBeginClip_write(param_3, param_4, param_5); +} + +Clip Element_EndClip_read(ElementRef ref) +{ + ClipRef param = ClipRef(ref.offset + 4u); + return Clip_read(param); +} + +void AnnoEndClip_write(Alloc a, AnnoEndClipRef ref, AnnoEndClip s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.bbox.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.bbox.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.bbox.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.bbox.w); + write_mem(param_9, param_10, param_11); +} + +void Annotated_EndClip_write(Alloc a, AnnotatedRef ref, AnnoEndClip s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 4u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + AnnoEndClipRef param_4 = AnnoEndClipRef(ref.offset + 4u); + AnnoEndClip param_5 = s; + AnnoEndClip_write(param_3, param_4, param_5); +} + +void TransformSeg_write(Alloc a, TransformSegRef ref, TransformSeg s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.mat.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.mat.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.mat.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.mat.w); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.translate.x); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = floatBitsToUint(s.translate.y); + write_mem(param_15, param_16, param_17); +} + +void main() +{ + if (_294.mem_error != 0u) + { + return; + } + if (gl_LocalInvocationID.x == 0u) + { + uint _2069 = atomicAdd(_779.part_counter, 1u); + sh_part_ix = _2069; + } + barrier(); + uint part_ix = sh_part_ix; + uint ix = (part_ix * 128u) + (gl_LocalInvocationID.x * 4u); + ElementRef ref = ElementRef(ix * 36u); + ElementRef param = ref; + State th_state[4]; + th_state[0] = map_element(param); + for (uint i = 1u; i < 4u; i++) + { + ElementRef param_1 = ref; + uint param_2 = i; + ElementRef param_3 = Element_index(param_1, param_2); + State param_4 = th_state[i - 1u]; + State param_5 = map_element(param_3); + th_state[i] = combine_state(param_4, param_5); + } + State agg = th_state[3]; + sh_state[gl_LocalInvocationID.x] = agg; + for (uint i_1 = 0u; i_1 < 5u; i_1++) + { + barrier(); + if (gl_LocalInvocationID.x >= uint(1 << int(i_1))) + { + State other = sh_state[gl_LocalInvocationID.x - uint(1 << int(i_1))]; + State param_6 = other; + State param_7 = agg; + agg = combine_state(param_6, param_7); + } + barrier(); + sh_state[gl_LocalInvocationID.x] = agg; + } + State exclusive; + exclusive.bbox = vec4(0.0); + exclusive.mat = vec4(1.0, 0.0, 0.0, 1.0); + exclusive.translate = vec2(0.0); + exclusive.linewidth = 1.0; + exclusive.flags = 0u; + exclusive.path_count = 0u; + exclusive.pathseg_count = 0u; + exclusive.trans_count = 0u; + if (gl_LocalInvocationID.x == 31u) + { + uint param_8 = part_ix; + StateRef param_9 = state_aggregate_ref(param_8); + State param_10 = agg; + State_write(param_9, param_10); + uint flag = 1u; + memoryBarrierBuffer(); + if (part_ix == 0u) + { + uint param_11 = part_ix; + StateRef param_12 = state_prefix_ref(param_11); + State param_13 = agg; + State_write(param_12, param_13); + flag = 2u; + } + uint param_14 = part_ix; + _779.state[state_flag_index(param_14)] = flag; + if (part_ix != 0u) + { + uint look_back_ix = part_ix - 1u; + uint their_ix = 0u; + State their_agg; + while (true) + { + uint param_15 = look_back_ix; + flag = _779.state[state_flag_index(param_15)]; + if (flag == 2u) + { + uint param_16 = look_back_ix; + StateRef param_17 = state_prefix_ref(param_16); + State their_prefix = State_read(param_17); + State param_18 = their_prefix; + State param_19 = exclusive; + exclusive = combine_state(param_18, param_19); + break; + } + else + { + if (flag == 1u) + { + uint param_20 = look_back_ix; + StateRef param_21 = state_aggregate_ref(param_20); + their_agg = State_read(param_21); + State param_22 = their_agg; + State param_23 = exclusive; + exclusive = combine_state(param_22, param_23); + look_back_ix--; + their_ix = 0u; + continue; + } + } + ElementRef ref_1 = ElementRef(((look_back_ix * 128u) + their_ix) * 36u); + ElementRef param_24 = ref_1; + State s = map_element(param_24); + if (their_ix == 0u) + { + their_agg = s; + } + else + { + State param_25 = their_agg; + State param_26 = s; + their_agg = combine_state(param_25, param_26); + } + their_ix++; + if (their_ix == 128u) + { + State param_27 = their_agg; + State param_28 = exclusive; + exclusive = combine_state(param_27, param_28); + if (look_back_ix == 0u) + { + break; + } + look_back_ix--; + their_ix = 0u; + } + } + State param_29 = exclusive; + State param_30 = agg; + State inclusive_prefix = combine_state(param_29, param_30); + sh_prefix = exclusive; + uint param_31 = part_ix; + StateRef param_32 = state_prefix_ref(param_31); + State param_33 = inclusive_prefix; + State_write(param_32, param_33); + memoryBarrierBuffer(); + flag = 2u; + uint param_34 = part_ix; + _779.state[state_flag_index(param_34)] = flag; + } + } + barrier(); + if (part_ix != 0u) + { + exclusive = sh_prefix; + } + State row = exclusive; + if (gl_LocalInvocationID.x > 0u) + { + State other_1 = sh_state[gl_LocalInvocationID.x - 1u]; + State param_35 = row; + State param_36 = other_1; + row = combine_state(param_35, param_36); + } + PathCubic path_cubic; + PathSegRef path_out_ref; + Alloc param_45; + Alloc param_51; + Alloc param_57; + AnnoColor anno_fill; + AnnotatedRef out_ref; + Alloc param_63; + AnnoImage anno_img; + Alloc param_69; + AnnoBeginClip anno_begin_clip; + Alloc param_75; + Alloc param_80; + Alloc param_83; + for (uint i_2 = 0u; i_2 < 4u; i_2++) + { + State param_37 = row; + State param_38 = th_state[i_2]; + State st = combine_state(param_37, param_38); + ElementRef param_39 = ref; + uint param_40 = i_2; + ElementRef this_ref = Element_index(param_39, param_40); + ElementRef param_41 = this_ref; + ElementTag tag = Element_tag(param_41); + uint param_42 = st.flags >> uint(4); + uint fill_mode = fill_mode_from_flags(param_42); + bool is_stroke = fill_mode == 1u; + switch (tag.tag) + { + case 1u: + { + ElementRef param_43 = this_ref; + LineSeg line = Element_Line_read(param_43); + path_cubic.p0 = line.p0; + path_cubic.p1 = mix(line.p0, line.p1, vec2(0.3333333432674407958984375)); + path_cubic.p2 = mix(line.p1, line.p0, vec2(0.3333333432674407958984375)); + path_cubic.p3 = line.p1; + path_cubic.path_ix = st.path_count; + path_cubic.trans_ix = st.trans_count; + if (is_stroke) + { + State param_44 = st; + path_cubic.stroke = get_linewidth(param_44); + } + else + { + path_cubic.stroke = vec2(0.0); + } + path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u)); + param_45.offset = _2435.conf.pathseg_alloc.offset; + PathSegRef param_46 = path_out_ref; + uint param_47 = fill_mode; + PathCubic param_48 = path_cubic; + PathSeg_Cubic_write(param_45, param_46, param_47, param_48); + break; + } + case 2u: + { + ElementRef param_49 = this_ref; + QuadSeg quad = Element_Quad_read(param_49); + path_cubic.p0 = quad.p0; + path_cubic.p1 = mix(quad.p1, quad.p0, vec2(0.3333333432674407958984375)); + path_cubic.p2 = mix(quad.p1, quad.p2, vec2(0.3333333432674407958984375)); + path_cubic.p3 = quad.p2; + path_cubic.path_ix = st.path_count; + path_cubic.trans_ix = st.trans_count; + if (is_stroke) + { + State param_50 = st; + path_cubic.stroke = get_linewidth(param_50); + } + else + { + path_cubic.stroke = vec2(0.0); + } + path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u)); + param_51.offset = _2435.conf.pathseg_alloc.offset; + PathSegRef param_52 = path_out_ref; + uint param_53 = fill_mode; + PathCubic param_54 = path_cubic; + PathSeg_Cubic_write(param_51, param_52, param_53, param_54); + break; + } + case 3u: + { + ElementRef param_55 = this_ref; + CubicSeg cubic = Element_Cubic_read(param_55); + path_cubic.p0 = cubic.p0; + path_cubic.p1 = cubic.p1; + path_cubic.p2 = cubic.p2; + path_cubic.p3 = cubic.p3; + path_cubic.path_ix = st.path_count; + path_cubic.trans_ix = st.trans_count; + if (is_stroke) + { + State param_56 = st; + path_cubic.stroke = get_linewidth(param_56); + } + else + { + path_cubic.stroke = vec2(0.0); + } + path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u)); + param_57.offset = _2435.conf.pathseg_alloc.offset; + PathSegRef param_58 = path_out_ref; + uint param_59 = fill_mode; + PathCubic param_60 = path_cubic; + PathSeg_Cubic_write(param_57, param_58, param_59, param_60); + break; + } + case 4u: + { + ElementRef param_61 = this_ref; + FillColor fill = Element_FillColor_read(param_61); + anno_fill.rgba_color = fill.rgba_color; + if (is_stroke) + { + State param_62 = st; + vec2 lw = get_linewidth(param_62); + anno_fill.bbox = st.bbox + vec4(-lw, lw); + anno_fill.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z))); + } + else + { + anno_fill.bbox = st.bbox; + anno_fill.linewidth = 0.0; + } + out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u)); + param_63.offset = _2435.conf.anno_alloc.offset; + AnnotatedRef param_64 = out_ref; + uint param_65 = fill_mode; + AnnoColor param_66 = anno_fill; + Annotated_Color_write(param_63, param_64, param_65, param_66); + break; + } + case 9u: + { + ElementRef param_67 = this_ref; + FillImage fill_img = Element_FillImage_read(param_67); + anno_img.index = fill_img.index; + anno_img.offset = fill_img.offset; + if (is_stroke) + { + State param_68 = st; + vec2 lw_1 = get_linewidth(param_68); + anno_img.bbox = st.bbox + vec4(-lw_1, lw_1); + anno_img.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z))); + } + else + { + anno_img.bbox = st.bbox; + anno_img.linewidth = 0.0; + } + out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u)); + param_69.offset = _2435.conf.anno_alloc.offset; + AnnotatedRef param_70 = out_ref; + uint param_71 = fill_mode; + AnnoImage param_72 = anno_img; + Annotated_Image_write(param_69, param_70, param_71, param_72); + break; + } + case 7u: + { + ElementRef param_73 = this_ref; + Clip begin_clip = Element_BeginClip_read(param_73); + anno_begin_clip.bbox = begin_clip.bbox; + if (is_stroke) + { + State param_74 = st; + vec2 lw_2 = get_linewidth(param_74); + anno_begin_clip.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z))); + } + else + { + anno_fill.linewidth = 0.0; + } + out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u)); + param_75.offset = _2435.conf.anno_alloc.offset; + AnnotatedRef param_76 = out_ref; + uint param_77 = fill_mode; + AnnoBeginClip param_78 = anno_begin_clip; + Annotated_BeginClip_write(param_75, param_76, param_77, param_78); + break; + } + case 8u: + { + ElementRef param_79 = this_ref; + Clip end_clip = Element_EndClip_read(param_79); + AnnoEndClip anno_end_clip = AnnoEndClip(end_clip.bbox); + out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u)); + param_80.offset = _2435.conf.anno_alloc.offset; + AnnotatedRef param_81 = out_ref; + AnnoEndClip param_82 = anno_end_clip; + Annotated_EndClip_write(param_80, param_81, param_82); + break; + } + case 6u: + { + TransformSeg transform = TransformSeg(st.mat, st.translate); + TransformSegRef trans_ref = TransformSegRef(_2435.conf.trans_alloc.offset + ((st.trans_count - 1u) * 24u)); + param_83.offset = _2435.conf.trans_alloc.offset; + TransformSegRef param_84 = trans_ref; + TransformSeg param_85 = transform; + TransformSeg_write(param_83, param_84, param_85); + break; + } + } + } +} + +`, + } + shader_intersect_frag = driver.ShaderSources{ + Name: "intersect.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Textures: []driver.TextureBinding{{Name: "cover", Binding: 0}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +uniform mediump sampler2D cover; + +varying highp vec2 vUV; + +void main() +{ + float cover_1 = abs(texture2D(cover, vUV).x); + gl_FragData[0].x = cover_1; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D cover; + +in highp vec2 vUV; +layout(location = 0) out vec4 fragColor; + +void main() +{ + float cover_1 = abs(texture(cover, vUV).x); + fragColor.x = cover_1; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D cover; + +in vec2 vUV; +out vec4 fragColor; + +void main() +{ + float cover_1 = abs(texture(cover, vUV).x); + fragColor.x = cover_1; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D cover; + +in vec2 vUV; +out vec4 fragColor; + +void main() +{ + float cover_1 = abs(texture(cover, vUV).x); + fragColor.x = cover_1; +} + +`, + HLSL: "DXBC\xe0\xe4\x03\x8c\xacVF\x82l\xe7|\xc3T\xa6'\xef\x01\x00\x00\x00\b\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xd4\x00\x00\x00\x80\x01\x00\x00\xfc\x01\x00\x00\xa0\x02\x00\x00\xd4\x02\x00\x00Aon9\x94\x00\x00\x00\x94\x00\x00\x00\x00\x02\xff\xffl\x00\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0#\x00\x00\x02\x00\x00\x01\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x0e\x80\x00\x00\x00\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xa4\x00\x00\x00@\x00\x00\x00)\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x006\x00\x00\x06\x12 \x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xe2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x9c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00q\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00k\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_cover_sampler\x00cover\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + } + shader_intersect_vert = driver.ShaderSources{ + Name: "intersect.vert", + Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_block.uvTransform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.subUVTransform", Type: 0x0, Size: 4, Offset: 16}}, + Size: 32, + }, + GLSL100ES: `#version 100 + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 uvTransform; + vec4 subUVTransform; +}; + +uniform Block _block; + +attribute vec2 pos; +attribute vec2 uv; +varying vec2 vUV; + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0)); + vec3 param_1 = vec3(pos, 1.0); + vec3 p = transform3x2(param, param_1); + gl_Position = vec4(p, 1.0); + m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_3 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_2, param_3); + vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw; + m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_5 = vec3(vUV, 1.0); + vUV = transform3x2(param_4, param_5).xy; + vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw; +} + +`, + GLSL300ES: `#version 300 es + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(std140) uniform Block +{ + vec4 uvTransform; + vec4 subUVTransform; +} _block; + +layout(location = 0) in vec2 pos; +layout(location = 1) in vec2 uv; +out vec2 vUV; + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0)); + vec3 param_1 = vec3(pos, 1.0); + vec3 p = transform3x2(param, param_1); + gl_Position = vec4(p, 1.0); + m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_3 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_2, param_3); + vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw; + m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_5 = vec3(vUV, 1.0); + vUV = transform3x2(param_4, param_5).xy; + vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 uvTransform; + vec4 subUVTransform; +}; + +uniform Block _block; + +in vec2 pos; +in vec2 uv; +out vec2 vUV; + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0)); + vec3 param_1 = vec3(pos, 1.0); + vec3 p = transform3x2(param, param_1); + gl_Position = vec4(p, 1.0); + m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_3 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_2, param_3); + vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw; + m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_5 = vec3(vUV, 1.0); + vUV = transform3x2(param_4, param_5).xy; + vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(binding = 0, std140) uniform Block +{ + vec4 uvTransform; + vec4 subUVTransform; +} _block; + +in vec2 pos; +in vec2 uv; +out vec2 vUV; + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0)); + vec3 param_1 = vec3(pos, 1.0); + vec3 p = transform3x2(param, param_1); + gl_Position = vec4(p, 1.0); + m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_3 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_2, param_3); + vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw; + m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_5 = vec3(vUV, 1.0); + vUV = transform3x2(param_4, param_5).xy; + vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw; +} + +`, + HLSL: "DXBCxH\xc4I\xbe\x0f[|\nl\x899\xe0\xb8\xcb?\x01\x00\x00\x00\xdc\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00L\x01\x00\x00\xc4\x02\x00\x00@\x03\x00\x008\x04\x00\x00\x84\x04\x00\x00Aon9\f\x01\x00\x00\f\x01\x00\x00\x00\x02\xfe\xff\xd8\x00\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x03\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x80\xbf\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\x03\x80\x01\x00U\x90\x03\x00\xe4\xa0\x03\x00\xe1\xa0\x05\x00\x00\x03\x00\x00\x03\x80\x00\x00\xe4\x80\x03\x00\xe2\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x01\x80\x01\x00\x00\x90\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x80\x02\x00\xe4\xa0\x02\x00\xee\xa0\x01\x00\x00\x02\x00\x00\x04\x80\x03\x00\x00\xa0\b\x00\x00\x03\x00\x00\b\x80\x03\x00É \x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\xe0\x00\x00\xec\x80\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x90\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\f\xc0\x03\x00\x00\xa0\xff\xff\x00\x00SHDRp\x01\x00\x00@\x00\x01\x00\\\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x006\x00\x00\x05\"\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?6\x00\x00\x05R\x00\x10\x00\x00\x00\x00\x00V\x14\x10\x00\x01\x00\x00\x00\x0f\x00\x00\n\x82\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x002\x00\x00\v2\x00\x10\x00\x00\x00\x00\x00\xe6\n\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x0f\x00\x00\n\x82\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x96\x05\x10\x00\x00\x00\x00\x002\x00\x00\v2 \x10\x00\x00\x00\x00\x00\xc6\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\x052 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x01\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\n\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xf0\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\xc6\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x02\x00\x00\x00\\\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00_block_uvTransform\x00\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_subUVTransform\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_kernel4_comp = driver.ShaderSources{ + Name: "kernel4.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 16, local_size_y = 8, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct CmdStrokeRef +{ + uint offset; +}; + +struct CmdStroke +{ + uint tile_ref; + float half_width; +}; + +struct CmdFillRef +{ + uint offset; +}; + +struct CmdFill +{ + uint tile_ref; + int backdrop; +}; + +struct CmdColorRef +{ + uint offset; +}; + +struct CmdColor +{ + uint rgba_color; +}; + +struct CmdImageRef +{ + uint offset; +}; + +struct CmdImage +{ + uint index; + ivec2 offset; +}; + +struct CmdAlphaRef +{ + uint offset; +}; + +struct CmdAlpha +{ + float alpha; +}; + +struct CmdJumpRef +{ + uint offset; +}; + +struct CmdJump +{ + uint new_ref; +}; + +struct CmdRef +{ + uint offset; +}; + +struct CmdTag +{ + uint tag; + uint flags; +}; + +struct TileSegRef +{ + uint offset; +}; + +struct TileSeg +{ + vec2 origin; + vec2 vector; + float y_edge; + TileSegRef next; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _196; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _693; + +layout(binding = 3, rgba8) uniform readonly highp image2D images[1]; +layout(binding = 2, rgba8) uniform writeonly highp image2D image; + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +Alloc slice_mem(Alloc a, uint offset, uint size) +{ + uint param = a.offset + offset; + uint param_1 = size; + return new_alloc(param, param_1); +} + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _196.memory[offset]; + return v; +} + +Alloc alloc_read(Alloc a, uint offset) +{ + Alloc param = a; + uint param_1 = offset >> uint(2); + Alloc alloc; + alloc.offset = read_mem(param, param_1); + return alloc; +} + +CmdTag Cmd_tag(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return CmdTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +CmdStroke CmdStroke_read(Alloc a, CmdStrokeRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + CmdStroke s; + s.tile_ref = raw0; + s.half_width = uintBitsToFloat(raw1); + return s; +} + +CmdStroke Cmd_Stroke_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdStrokeRef param_1 = CmdStrokeRef(ref.offset + 4u); + return CmdStroke_read(param, param_1); +} + +TileSeg TileSeg_read(Alloc a, TileSegRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + TileSeg s; + s.origin = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.vector = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.y_edge = uintBitsToFloat(raw4); + s.next = TileSegRef(raw5); + return s; +} + +uvec2 chunk_offset(uint i) +{ + return uvec2((i % 2u) * 16u, (i / 2u) * 8u); +} + +CmdFill CmdFill_read(Alloc a, CmdFillRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + CmdFill s; + s.tile_ref = raw0; + s.backdrop = int(raw1); + return s; +} + +CmdFill Cmd_Fill_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdFillRef param_1 = CmdFillRef(ref.offset + 4u); + return CmdFill_read(param, param_1); +} + +CmdAlpha CmdAlpha_read(Alloc a, CmdAlphaRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + CmdAlpha s; + s.alpha = uintBitsToFloat(raw0); + return s; +} + +CmdAlpha Cmd_Alpha_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdAlphaRef param_1 = CmdAlphaRef(ref.offset + 4u); + return CmdAlpha_read(param, param_1); +} + +CmdColor CmdColor_read(Alloc a, CmdColorRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + CmdColor s; + s.rgba_color = raw0; + return s; +} + +CmdColor Cmd_Color_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdColorRef param_1 = CmdColorRef(ref.offset + 4u); + return CmdColor_read(param, param_1); +} + +vec3 fromsRGB(vec3 srgb) +{ + bvec3 cutoff = greaterThanEqual(srgb, vec3(0.040449999272823333740234375)); + vec3 below = srgb / vec3(12.9200000762939453125); + vec3 above = pow((srgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625)); + return mix(below, above, cutoff); +} + +vec4 unpacksRGB(uint srgba) +{ + vec4 color = unpackUnorm4x8(srgba).wzyx; + vec3 param = color.xyz; + return vec4(fromsRGB(param), color.w); +} + +CmdImage CmdImage_read(Alloc a, CmdImageRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + CmdImage s; + s.index = raw0; + s.offset = ivec2(int(raw1 << uint(16)) >> 16, int(raw1) >> 16); + return s; +} + +CmdImage Cmd_Image_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdImageRef param_1 = CmdImageRef(ref.offset + 4u); + return CmdImage_read(param, param_1); +} + +vec4[8] fillImage(uvec2 xy, CmdImage cmd_img) +{ + vec4 rgba[8]; + for (uint i = 0u; i < 8u; i++) + { + uint param = i; + ivec2 uv = ivec2(xy + chunk_offset(param)) + cmd_img.offset; + vec4 fg_rgba = imageLoad(images[0], uv); + vec3 param_1 = fg_rgba.xyz; + vec3 _663 = fromsRGB(param_1); + fg_rgba = vec4(_663.x, _663.y, _663.z, fg_rgba.w); + rgba[i] = fg_rgba; + } + return rgba; +} + +vec3 tosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return mix(below, above, cutoff); +} + +uint packsRGB(inout vec4 rgba) +{ + vec3 param = rgba.xyz; + rgba = vec4(tosRGB(param), rgba.w); + return packUnorm4x8(rgba.wzyx); +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _196.memory[offset] = val; +} + +CmdJump CmdJump_read(Alloc a, CmdJumpRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + CmdJump s; + s.new_ref = raw0; + return s; +} + +CmdJump Cmd_Jump_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdJumpRef param_1 = CmdJumpRef(ref.offset + 4u); + return CmdJump_read(param, param_1); +} + +void main() +{ + if (_196.mem_error != 0u) + { + return; + } + uint tile_ix = (gl_WorkGroupID.y * _693.conf.width_in_tiles) + gl_WorkGroupID.x; + Alloc param; + param.offset = _693.conf.ptcl_alloc.offset; + uint param_1 = tile_ix * 1024u; + uint param_2 = 1024u; + Alloc cmd_alloc = slice_mem(param, param_1, param_2); + CmdRef cmd_ref = CmdRef(cmd_alloc.offset); + Alloc param_3 = cmd_alloc; + uint param_4 = cmd_ref.offset; + Alloc scratch_alloc = alloc_read(param_3, param_4); + cmd_ref.offset += 8u; + uvec2 xy_uint = uvec2(gl_LocalInvocationID.x + (32u * gl_WorkGroupID.x), gl_LocalInvocationID.y + (32u * gl_WorkGroupID.y)); + vec2 xy = vec2(xy_uint); + vec4 rgba[8]; + for (uint i = 0u; i < 8u; i++) + { + rgba[i] = vec4(0.0); + } + uint clip_depth = 0u; + float df[8]; + TileSegRef tile_seg_ref; + float area[8]; + uint base_ix; + while (true) + { + Alloc param_5 = cmd_alloc; + CmdRef param_6 = cmd_ref; + uint tag = Cmd_tag(param_5, param_6).tag; + if (tag == 0u) + { + break; + } + switch (tag) + { + case 2u: + { + Alloc param_7 = cmd_alloc; + CmdRef param_8 = cmd_ref; + CmdStroke stroke = Cmd_Stroke_read(param_7, param_8); + for (uint k = 0u; k < 8u; k++) + { + df[k] = 1000000000.0; + } + tile_seg_ref = TileSegRef(stroke.tile_ref); + do + { + uint param_9 = tile_seg_ref.offset; + uint param_10 = 24u; + Alloc param_11 = new_alloc(param_9, param_10); + TileSegRef param_12 = tile_seg_ref; + TileSeg seg = TileSeg_read(param_11, param_12); + vec2 line_vec = seg.vector; + for (uint k_1 = 0u; k_1 < 8u; k_1++) + { + vec2 dpos = (xy + vec2(0.5)) - seg.origin; + uint param_13 = k_1; + dpos += vec2(chunk_offset(param_13)); + float t = clamp(dot(line_vec, dpos) / dot(line_vec, line_vec), 0.0, 1.0); + df[k_1] = min(df[k_1], length((line_vec * t) - dpos)); + } + tile_seg_ref = seg.next; + } while (tile_seg_ref.offset != 0u); + for (uint k_2 = 0u; k_2 < 8u; k_2++) + { + area[k_2] = clamp((stroke.half_width + 0.5) - df[k_2], 0.0, 1.0); + } + cmd_ref.offset += 12u; + break; + } + case 1u: + { + Alloc param_14 = cmd_alloc; + CmdRef param_15 = cmd_ref; + CmdFill fill = Cmd_Fill_read(param_14, param_15); + for (uint k_3 = 0u; k_3 < 8u; k_3++) + { + area[k_3] = float(fill.backdrop); + } + tile_seg_ref = TileSegRef(fill.tile_ref); + do + { + uint param_16 = tile_seg_ref.offset; + uint param_17 = 24u; + Alloc param_18 = new_alloc(param_16, param_17); + TileSegRef param_19 = tile_seg_ref; + TileSeg seg_1 = TileSeg_read(param_18, param_19); + for (uint k_4 = 0u; k_4 < 8u; k_4++) + { + uint param_20 = k_4; + vec2 my_xy = xy + vec2(chunk_offset(param_20)); + vec2 start = seg_1.origin - my_xy; + vec2 end = start + seg_1.vector; + vec2 window = clamp(vec2(start.y, end.y), vec2(0.0), vec2(1.0)); + if (!(window.x == window.y)) + { + vec2 t_1 = (window - vec2(start.y)) / vec2(seg_1.vector.y); + vec2 xs = vec2(mix(start.x, end.x, t_1.x), mix(start.x, end.x, t_1.y)); + float xmin = min(min(xs.x, xs.y), 1.0) - 9.9999999747524270787835121154785e-07; + float xmax = max(xs.x, xs.y); + float b = min(xmax, 1.0); + float c = max(b, 0.0); + float d = max(xmin, 0.0); + float a = ((b + (0.5 * ((d * d) - (c * c)))) - xmin) / (xmax - xmin); + area[k_4] += (a * (window.x - window.y)); + } + area[k_4] += (sign(seg_1.vector.x) * clamp((my_xy.y - seg_1.y_edge) + 1.0, 0.0, 1.0)); + } + tile_seg_ref = seg_1.next; + } while (tile_seg_ref.offset != 0u); + for (uint k_5 = 0u; k_5 < 8u; k_5++) + { + area[k_5] = min(abs(area[k_5]), 1.0); + } + cmd_ref.offset += 12u; + break; + } + case 3u: + { + for (uint k_6 = 0u; k_6 < 8u; k_6++) + { + area[k_6] = 1.0; + } + cmd_ref.offset += 4u; + break; + } + case 4u: + { + Alloc param_21 = cmd_alloc; + CmdRef param_22 = cmd_ref; + CmdAlpha alpha = Cmd_Alpha_read(param_21, param_22); + for (uint k_7 = 0u; k_7 < 8u; k_7++) + { + area[k_7] = alpha.alpha; + } + cmd_ref.offset += 8u; + break; + } + case 5u: + { + Alloc param_23 = cmd_alloc; + CmdRef param_24 = cmd_ref; + CmdColor color = Cmd_Color_read(param_23, param_24); + uint param_25 = color.rgba_color; + vec4 fg = unpacksRGB(param_25); + for (uint k_8 = 0u; k_8 < 8u; k_8++) + { + vec4 fg_k = fg * area[k_8]; + rgba[k_8] = (rgba[k_8] * (1.0 - fg_k.w)) + fg_k; + } + cmd_ref.offset += 8u; + break; + } + case 6u: + { + Alloc param_26 = cmd_alloc; + CmdRef param_27 = cmd_ref; + CmdImage fill_img = Cmd_Image_read(param_26, param_27); + uvec2 param_28 = xy_uint; + CmdImage param_29 = fill_img; + vec4 img[8] = fillImage(param_28, param_29); + for (uint k_9 = 0u; k_9 < 8u; k_9++) + { + vec4 fg_k_1 = img[k_9] * area[k_9]; + rgba[k_9] = (rgba[k_9] * (1.0 - fg_k_1.w)) + fg_k_1; + } + cmd_ref.offset += 12u; + break; + } + case 7u: + { + base_ix = (scratch_alloc.offset >> uint(2)) + (2u * ((((clip_depth * 32u) * 32u) + gl_LocalInvocationID.x) + (32u * gl_LocalInvocationID.y))); + for (uint k_10 = 0u; k_10 < 8u; k_10++) + { + uint param_30 = k_10; + uvec2 offset = chunk_offset(param_30); + vec4 param_31 = vec4(rgba[k_10]); + uint _1286 = packsRGB(param_31); + uint srgb = _1286; + float alpha_1 = clamp(abs(area[k_10]), 0.0, 1.0); + Alloc param_32 = scratch_alloc; + uint param_33 = (base_ix + 0u) + (2u * (offset.x + (offset.y * 32u))); + uint param_34 = srgb; + write_mem(param_32, param_33, param_34); + Alloc param_35 = scratch_alloc; + uint param_36 = (base_ix + 1u) + (2u * (offset.x + (offset.y * 32u))); + uint param_37 = floatBitsToUint(alpha_1); + write_mem(param_35, param_36, param_37); + rgba[k_10] = vec4(0.0); + } + clip_depth++; + cmd_ref.offset += 4u; + break; + } + case 8u: + { + clip_depth--; + base_ix = (scratch_alloc.offset >> uint(2)) + (2u * ((((clip_depth * 32u) * 32u) + gl_LocalInvocationID.x) + (32u * gl_LocalInvocationID.y))); + for (uint k_11 = 0u; k_11 < 8u; k_11++) + { + uint param_38 = k_11; + uvec2 offset_1 = chunk_offset(param_38); + Alloc param_39 = scratch_alloc; + uint param_40 = (base_ix + 0u) + (2u * (offset_1.x + (offset_1.y * 32u))); + uint srgb_1 = read_mem(param_39, param_40); + Alloc param_41 = scratch_alloc; + uint param_42 = (base_ix + 1u) + (2u * (offset_1.x + (offset_1.y * 32u))); + uint alpha_2 = read_mem(param_41, param_42); + uint param_43 = srgb_1; + vec4 bg = unpacksRGB(param_43); + vec4 fg_1 = (rgba[k_11] * area[k_11]) * uintBitsToFloat(alpha_2); + rgba[k_11] = (bg * (1.0 - fg_1.w)) + fg_1; + } + cmd_ref.offset += 4u; + break; + } + case 9u: + { + Alloc param_44 = cmd_alloc; + CmdRef param_45 = cmd_ref; + cmd_ref = CmdRef(Cmd_Jump_read(param_44, param_45).new_ref); + cmd_alloc.offset = cmd_ref.offset; + break; + } + } + } + for (uint i_1 = 0u; i_1 < 8u; i_1++) + { + uint param_46 = i_1; + vec3 param_47 = rgba[i_1].xyz; + imageStore(image, ivec2(xy_uint + chunk_offset(param_46)), vec4(tosRGB(param_47), rgba[i_1].w)); + } +} + +`, + } + shader_material_frag = driver.ShaderSources{ + Name: "material.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +varying vec2 vUV; + +vec3 RGBtosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texture2D(tex, vUV); + vec3 param = texel.xyz; + vec3 _59 = RGBtosRGB(param); + texel = vec4(_59.x, _59.y, _59.z, texel.w); + gl_FragData[0] = texel; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +in vec2 vUV; +layout(location = 0) out vec4 fragColor; + +vec3 RGBtosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texture(tex, vUV); + vec3 param = texel.xyz; + vec3 _59 = RGBtosRGB(param); + texel = vec4(_59.x, _59.y, _59.z, texel.w); + fragColor = texel; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +in vec2 vUV; +out vec4 fragColor; + +vec3 RGBtosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texture(tex, vUV); + vec3 param = texel.xyz; + vec3 _59 = RGBtosRGB(param); + texel = vec4(_59.x, _59.y, _59.z, texel.w); + fragColor = texel; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +in vec2 vUV; +out vec4 fragColor; + +vec3 RGBtosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texture(tex, vUV); + vec3 param = texel.xyz; + vec3 _59 = RGBtosRGB(param); + texel = vec4(_59.x, _59.y, _59.z, texel.w); + fragColor = texel; +} + +`, + HLSL: "DXBC\x9e\x87LD\xf3\x17\n\x06\\\xb7\x98\x94\xa9PKe\x01\x00\x00\x00\xc8\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00\xbc\x01\x00\x00D\x03\x00\x00\xc0\x03\x00\x00`\x04\x00\x00\x94\x04\x00\x00Aon9|\x01\x00\x00|\x01\x00\x00\x00\x02\xff\xffT\x01\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0=\n\x87?\xaeGa\xbd\x00\x00\x00\x00\x00\x00\x00\x00Q\x00\x00\x05\x01\x00\x0f\xa0\x1c.M\xbbR\xb8NAvT\xd5>\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x0f\x00\x00\x02\x01\x00\x01\x80\x00\x00\x00\x80\x0f\x00\x00\x02\x01\x00\x02\x80\x00\x00U\x80\x0f\x00\x00\x02\x01\x00\x04\x80\x00\x00\xaa\x80\x05\x00\x00\x03\x01\x00\a\x80\x01\x00\xe4\x80\x01\x00\xaa\xa0\x0e\x00\x00\x02\x02\x00\x01\x80\x01\x00\x00\x80\x0e\x00\x00\x02\x02\x00\x02\x80\x01\x00U\x80\x0e\x00\x00\x02\x02\x00\x04\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\a\x80\x02\x00\xe4\x80\x00\x00\x00\xa0\x00\x00U\xa0\x02\x00\x00\x03\x01\x00\b\x80\x00\x00\x00\x80\x01\x00\x00\xa0\x05\x00\x00\x03\x02\x00\a\x80\x00\x00\xe4\x80\x01\x00U\xa0X\x00\x00\x04\x00\x00\x01\x80\x01\x00\xff\x80\x01\x00\x00\x80\x02\x00\x00\x80\x02\x00\x00\x03\x01\x00\x01\x80\x00\x00U\x80\x01\x00\x00\xa0X\x00\x00\x04\x00\x00\x02\x80\x01\x00\x00\x80\x01\x00U\x80\x02\x00U\x80\x02\x00\x00\x03\x01\x00\x01\x80\x00\x00\xaa\x80\x01\x00\x00\xa0X\x00\x00\x04\x00\x00\x04\x80\x01\x00\x00\x80\x01\x00\xaa\x80\x02\x00\xaa\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\x80\x01\x00\x00@\x00\x00\x00`\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x00/\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00vT\xd5>vT\xd5>vT\xd5>\x00\x00\x00\x00\x19\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x002\x00\x00\x0fr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00=\n\x87?=\n\x87?=\n\x87?\x00\x00\x00\x00\x02@\x00\x00\xaeGa\xbd\xaeGa\xbd\xaeGa\xbd\x00\x00\x00\x00\x1d\x00\x00\nr\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x1c.M;\x1c.M;\x1c.M;\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x00\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00R\xb8NAR\xb8NAR\xb8NA\x00\x00\x00\x006\x00\x00\x05\x82 \x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x007\x00\x00\tr \x10\x00\x00\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\n\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00m\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00i\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + } + shader_material_vert = driver.ShaderSources{ + Name: "material.vert", + Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + GLSL100ES: `#version 100 + +varying vec2 vUV; +attribute vec2 uv; +attribute vec2 pos; + +void main() +{ + vUV = uv; + gl_Position = vec4(pos, 0.0, 1.0); +} + +`, + GLSL300ES: `#version 300 es + +out vec2 vUV; +layout(location = 1) in vec2 uv; +layout(location = 0) in vec2 pos; + +void main() +{ + vUV = uv; + gl_Position = vec4(pos, 0.0, 1.0); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +out vec2 vUV; +in vec2 uv; +in vec2 pos; + +void main() +{ + vUV = uv; + gl_Position = vec4(pos, 0.0, 1.0); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +out vec2 vUV; +in vec2 uv; +in vec2 pos; + +void main() +{ + vUV = uv; + gl_Position = vec4(pos, 0.0, 1.0); +} + +`, + HLSL: "DXBCg\xc0\xae\x16\xd8\xe1\xbdl~ń\xf1\xc4\xf6dV\x01\x00\x00\x00\xc4\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\xc8\x00\x00\x00X\x01\x00\x00\xd4\x01\x00\x00 \x02\x00\x00l\x02\x00\x00Aon9\x88\x00\x00\x00\x88\x00\x00\x00\x00\x02\xfe\xff`\x00\x00\x00(\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x01\x00$\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x01\x00\x0f\xa0\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x90\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\x03\xe0\x01\x00\xe4\x90\x01\x00\x00\x02\x00\x00\f\xc0\x01\x00D\xa0\xff\xff\x00\x00SHDR\x88\x00\x00\x00@\x00\x01\x00\"\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x006\x00\x00\x052 \x10\x00\x00\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x052 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x01\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_path_coarse_comp = driver.ShaderSources{ + Name: "path_coarse.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct MallocResult +{ + Alloc alloc; + bool failed; +}; + +struct PathCubicRef +{ + uint offset; +}; + +struct PathCubic +{ + vec2 p0; + vec2 p1; + vec2 p2; + vec2 p3; + uint path_ix; + uint trans_ix; + vec2 stroke; +}; + +struct PathSegRef +{ + uint offset; +}; + +struct PathSegTag +{ + uint tag; + uint flags; +}; + +struct TileRef +{ + uint offset; +}; + +struct PathRef +{ + uint offset; +}; + +struct Path +{ + uvec4 bbox; + TileRef tiles; +}; + +struct TileSegRef +{ + uint offset; +}; + +struct TileSeg +{ + vec2 origin; + vec2 vector; + float y_edge; + TileSegRef next; +}; + +struct TransformSegRef +{ + uint offset; +}; + +struct TransformSeg +{ + vec4 mat; + vec2 translate; +}; + +struct SubdivResult +{ + float val; + float a0; + float a2; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _149; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _788; + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _149.memory[offset]; + return v; +} + +PathSegTag PathSeg_tag(Alloc a, PathSegRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return PathSegTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +PathCubic PathCubic_read(Alloc a, PathCubicRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 6u; + uint raw6 = read_mem(param_12, param_13); + Alloc param_14 = a; + uint param_15 = ix + 7u; + uint raw7 = read_mem(param_14, param_15); + Alloc param_16 = a; + uint param_17 = ix + 8u; + uint raw8 = read_mem(param_16, param_17); + Alloc param_18 = a; + uint param_19 = ix + 9u; + uint raw9 = read_mem(param_18, param_19); + Alloc param_20 = a; + uint param_21 = ix + 10u; + uint raw10 = read_mem(param_20, param_21); + Alloc param_22 = a; + uint param_23 = ix + 11u; + uint raw11 = read_mem(param_22, param_23); + PathCubic s; + s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + s.p3 = vec2(uintBitsToFloat(raw6), uintBitsToFloat(raw7)); + s.path_ix = raw8; + s.trans_ix = raw9; + s.stroke = vec2(uintBitsToFloat(raw10), uintBitsToFloat(raw11)); + return s; +} + +PathCubic PathSeg_Cubic_read(Alloc a, PathSegRef ref) +{ + Alloc param = a; + PathCubicRef param_1 = PathCubicRef(ref.offset + 4u); + return PathCubic_read(param, param_1); +} + +TransformSeg TransformSeg_read(Alloc a, TransformSegRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + TransformSeg s; + s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + return s; +} + +vec2 eval_cubic(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t) +{ + float mt = 1.0 - t; + return (p0 * ((mt * mt) * mt)) + (((p1 * ((mt * mt) * 3.0)) + (((p2 * (mt * 3.0)) + (p3 * t)) * t)) * t); +} + +float approx_parabola_integral(float x) +{ + return x * inversesqrt(sqrt(0.3300000131130218505859375 + (0.201511204242706298828125 + ((0.25 * x) * x)))); +} + +SubdivResult estimate_subdiv(vec2 p0, vec2 p1, vec2 p2, float sqrt_tol) +{ + vec2 d01 = p1 - p0; + vec2 d12 = p2 - p1; + vec2 dd = d01 - d12; + float _cross = ((p2.x - p0.x) * dd.y) - ((p2.y - p0.y) * dd.x); + float x0 = ((d01.x * dd.x) + (d01.y * dd.y)) / _cross; + float x2 = ((d12.x * dd.x) + (d12.y * dd.y)) / _cross; + float scale = abs(_cross / (length(dd) * (x2 - x0))); + float param = x0; + float a0 = approx_parabola_integral(param); + float param_1 = x2; + float a2 = approx_parabola_integral(param_1); + float val = 0.0; + if (scale < 1000000000.0) + { + float da = abs(a2 - a0); + float sqrt_scale = sqrt(scale); + if (sign(x0) == sign(x2)) + { + val = da * sqrt_scale; + } + else + { + float xmin = sqrt_tol / sqrt_scale; + float param_2 = xmin; + val = (sqrt_tol * da) / approx_parabola_integral(param_2); + } + } + return SubdivResult(val, a0, a2); +} + +uint fill_mode_from_flags(uint flags) +{ + return flags & 1u; +} + +Path Path_read(Alloc a, PathRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Path s; + s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16)); + s.tiles = TileRef(raw2); + return s; +} + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +float approx_parabola_inv_integral(float x) +{ + return x * sqrt(0.61000001430511474609375 + (0.1520999968051910400390625 + ((0.25 * x) * x))); +} + +vec2 eval_quad(vec2 p0, vec2 p1, vec2 p2, float t) +{ + float mt = 1.0 - t; + return (p0 * (mt * mt)) + (((p1 * (mt * 2.0)) + (p2 * t)) * t); +} + +MallocResult malloc(uint size) +{ + MallocResult r; + r.failed = false; + uint _155 = atomicAdd(_149.mem_offset, size); + uint offset = _155; + uint param = offset; + uint param_1 = size; + r.alloc = new_alloc(param, param_1); + if ((offset + size) > uint(int(uint(_149.memory.length())) * 4)) + { + r.failed = true; + uint _176 = atomicMax(_149.mem_error, 1u); + return r; + } + return r; +} + +TileRef Tile_index(TileRef ref, uint index) +{ + return TileRef(ref.offset + (index * 8u)); +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _149.memory[offset] = val; +} + +void TileSeg_write(Alloc a, TileSegRef ref, TileSeg s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.origin.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.origin.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.vector.x); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.vector.y); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.y_edge); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = s.next.offset; + write_mem(param_15, param_16, param_17); +} + +void main() +{ + if (_149.mem_error != 0u) + { + return; + } + uint element_ix = gl_GlobalInvocationID.x; + PathSegRef ref = PathSegRef(_788.conf.pathseg_alloc.offset + (element_ix * 52u)); + PathSegTag tag = PathSegTag(0u, 0u); + if (element_ix < _788.conf.n_pathseg) + { + Alloc param; + param.offset = _788.conf.pathseg_alloc.offset; + PathSegRef param_1 = ref; + tag = PathSeg_tag(param, param_1); + } + switch (tag.tag) + { + case 1u: + { + Alloc param_2; + param_2.offset = _788.conf.pathseg_alloc.offset; + PathSegRef param_3 = ref; + PathCubic cubic = PathSeg_Cubic_read(param_2, param_3); + uint trans_ix = cubic.trans_ix; + if (trans_ix > 0u) + { + TransformSegRef trans_ref = TransformSegRef(_788.conf.trans_alloc.offset + ((trans_ix - 1u) * 24u)); + Alloc param_4; + param_4.offset = _788.conf.trans_alloc.offset; + TransformSegRef param_5 = trans_ref; + TransformSeg trans = TransformSeg_read(param_4, param_5); + cubic.p0 = ((trans.mat.xy * cubic.p0.x) + (trans.mat.zw * cubic.p0.y)) + trans.translate; + cubic.p1 = ((trans.mat.xy * cubic.p1.x) + (trans.mat.zw * cubic.p1.y)) + trans.translate; + cubic.p2 = ((trans.mat.xy * cubic.p2.x) + (trans.mat.zw * cubic.p2.y)) + trans.translate; + cubic.p3 = ((trans.mat.xy * cubic.p3.x) + (trans.mat.zw * cubic.p3.y)) + trans.translate; + } + vec2 err_v = (((cubic.p2 - cubic.p1) * 3.0) + cubic.p0) - cubic.p3; + float err = (err_v.x * err_v.x) + (err_v.y * err_v.y); + uint n_quads = max(uint(ceil(pow(err * 3.7037036418914794921875, 0.16666667163372039794921875))), 1u); + float val = 0.0; + vec2 qp0 = cubic.p0; + float _step = 1.0 / float(n_quads); + for (uint i = 0u; i < n_quads; i++) + { + float t = float(i + 1u) * _step; + vec2 param_6 = cubic.p0; + vec2 param_7 = cubic.p1; + vec2 param_8 = cubic.p2; + vec2 param_9 = cubic.p3; + float param_10 = t; + vec2 qp2 = eval_cubic(param_6, param_7, param_8, param_9, param_10); + vec2 param_11 = cubic.p0; + vec2 param_12 = cubic.p1; + vec2 param_13 = cubic.p2; + vec2 param_14 = cubic.p3; + float param_15 = t - (0.5 * _step); + vec2 qp1 = eval_cubic(param_11, param_12, param_13, param_14, param_15); + qp1 = (qp1 * 2.0) - ((qp0 + qp2) * 0.5); + vec2 param_16 = qp0; + vec2 param_17 = qp1; + vec2 param_18 = qp2; + float param_19 = 0.4743416607379913330078125; + SubdivResult params = estimate_subdiv(param_16, param_17, param_18, param_19); + val += params.val; + qp0 = qp2; + } + uint n = max(uint(ceil((val * 0.5) / 0.4743416607379913330078125)), 1u); + uint param_20 = tag.flags; + bool is_stroke = fill_mode_from_flags(param_20) == 1u; + uint path_ix = cubic.path_ix; + Alloc param_21; + param_21.offset = _788.conf.tile_alloc.offset; + PathRef param_22 = PathRef(_788.conf.tile_alloc.offset + (path_ix * 12u)); + Path path = Path_read(param_21, param_22); + uint param_23 = path.tiles.offset; + uint param_24 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u; + Alloc path_alloc = new_alloc(param_23, param_24); + ivec4 bbox = ivec4(path.bbox); + vec2 p0 = cubic.p0; + qp0 = cubic.p0; + float v_step = val / float(n); + int n_out = 1; + float val_sum = 0.0; + vec2 p1; + float _1309; + TileSeg tile_seg; + for (uint i_1 = 0u; i_1 < n_quads; i_1++) + { + float t_1 = float(i_1 + 1u) * _step; + vec2 param_25 = cubic.p0; + vec2 param_26 = cubic.p1; + vec2 param_27 = cubic.p2; + vec2 param_28 = cubic.p3; + float param_29 = t_1; + vec2 qp2_1 = eval_cubic(param_25, param_26, param_27, param_28, param_29); + vec2 param_30 = cubic.p0; + vec2 param_31 = cubic.p1; + vec2 param_32 = cubic.p2; + vec2 param_33 = cubic.p3; + float param_34 = t_1 - (0.5 * _step); + vec2 qp1_1 = eval_cubic(param_30, param_31, param_32, param_33, param_34); + qp1_1 = (qp1_1 * 2.0) - ((qp0 + qp2_1) * 0.5); + vec2 param_35 = qp0; + vec2 param_36 = qp1_1; + vec2 param_37 = qp2_1; + float param_38 = 0.4743416607379913330078125; + SubdivResult params_1 = estimate_subdiv(param_35, param_36, param_37, param_38); + float param_39 = params_1.a0; + float u0 = approx_parabola_inv_integral(param_39); + float param_40 = params_1.a2; + float u2 = approx_parabola_inv_integral(param_40); + float uscale = 1.0 / (u2 - u0); + float target = float(n_out) * v_step; + for (;;) + { + bool _1202 = uint(n_out) == n; + bool _1212; + if (!_1202) + { + _1212 = target < (val_sum + params_1.val); + } + else + { + _1212 = _1202; + } + if (_1212) + { + if (uint(n_out) == n) + { + p1 = cubic.p3; + } + else + { + float u = (target - val_sum) / params_1.val; + float a = mix(params_1.a0, params_1.a2, u); + float param_41 = a; + float au = approx_parabola_inv_integral(param_41); + float t_2 = (au - u0) * uscale; + vec2 param_42 = qp0; + vec2 param_43 = qp1_1; + vec2 param_44 = qp2_1; + float param_45 = t_2; + p1 = eval_quad(param_42, param_43, param_44, param_45); + } + float xmin = min(p0.x, p1.x) - cubic.stroke.x; + float xmax = max(p0.x, p1.x) + cubic.stroke.x; + float ymin = min(p0.y, p1.y) - cubic.stroke.y; + float ymax = max(p0.y, p1.y) + cubic.stroke.y; + float dx = p1.x - p0.x; + float dy = p1.y - p0.y; + if (abs(dy) < 9.999999717180685365747194737196e-10) + { + _1309 = 1000000000.0; + } + else + { + _1309 = dx / dy; + } + float invslope = _1309; + float c = (cubic.stroke.x + (abs(invslope) * (16.0 + cubic.stroke.y))) * 0.03125; + float b = invslope; + float a_1 = (p0.x - ((p0.y - 16.0) * b)) * 0.03125; + int x0 = int(floor(xmin * 0.03125)); + int x1 = int(floor(xmax * 0.03125) + 1.0); + int y0 = int(floor(ymin * 0.03125)); + int y1 = int(floor(ymax * 0.03125) + 1.0); + x0 = clamp(x0, bbox.x, bbox.z); + y0 = clamp(y0, bbox.y, bbox.w); + x1 = clamp(x1, bbox.x, bbox.z); + y1 = clamp(y1, bbox.y, bbox.w); + float xc = a_1 + (b * float(y0)); + int stride = bbox.z - bbox.x; + int base = ((y0 - bbox.y) * stride) - bbox.x; + uint n_tile_alloc = uint((x1 - x0) * (y1 - y0)); + uint param_46 = n_tile_alloc * 24u; + MallocResult _1424 = malloc(param_46); + MallocResult tile_alloc = _1424; + if (tile_alloc.failed) + { + return; + } + uint tile_offset = tile_alloc.alloc.offset; + int xray = int(floor(p0.x * 0.03125)); + int last_xray = int(floor(p1.x * 0.03125)); + if (p0.y > p1.y) + { + int tmp = xray; + xray = last_xray; + last_xray = tmp; + } + for (int y = y0; y < y1; y++) + { + float tile_y0 = float(y * 32); + int xbackdrop = max((xray + 1), bbox.x); + bool _1478 = !is_stroke; + bool _1488; + if (_1478) + { + _1488 = min(p0.y, p1.y) < tile_y0; + } + else + { + _1488 = _1478; + } + bool _1495; + if (_1488) + { + _1495 = xbackdrop < bbox.z; + } + else + { + _1495 = _1488; + } + if (_1495) + { + int backdrop = (p1.y < p0.y) ? 1 : (-1); + TileRef param_47 = path.tiles; + uint param_48 = uint(base + xbackdrop); + TileRef tile_ref = Tile_index(param_47, param_48); + uint tile_el = tile_ref.offset >> uint(2); + Alloc param_49 = path_alloc; + uint param_50 = tile_el + 1u; + if (touch_mem(param_49, param_50)) + { + uint _1533 = atomicAdd(_149.memory[tile_el + 1u], uint(backdrop)); + } + } + int next_xray = last_xray; + if (y < (y1 - 1)) + { + float tile_y1 = float((y + 1) * 32); + float x_edge = mix(p0.x, p1.x, (tile_y1 - p0.y) / dy); + next_xray = int(floor(x_edge * 0.03125)); + } + int min_xray = min(xray, next_xray); + int max_xray = max(xray, next_xray); + int xx0 = min(int(floor(xc - c)), min_xray); + int xx1 = max(int(ceil(xc + c)), (max_xray + 1)); + xx0 = clamp(xx0, x0, x1); + xx1 = clamp(xx1, x0, x1); + for (int x = xx0; x < xx1; x++) + { + float tile_x0 = float(x * 32); + TileRef param_51 = TileRef(path.tiles.offset); + uint param_52 = uint(base + x); + TileRef tile_ref_1 = Tile_index(param_51, param_52); + uint tile_el_1 = tile_ref_1.offset >> uint(2); + uint old = 0u; + Alloc param_53 = path_alloc; + uint param_54 = tile_el_1; + if (touch_mem(param_53, param_54)) + { + uint _1636 = atomicExchange(_149.memory[tile_el_1], tile_offset); + old = _1636; + } + tile_seg.origin = p0; + tile_seg.vector = p1 - p0; + float y_edge = 0.0; + if (!is_stroke) + { + y_edge = mix(p0.y, p1.y, (tile_x0 - p0.x) / dx); + if (min(p0.x, p1.x) < tile_x0) + { + vec2 p = vec2(tile_x0, y_edge); + if (p0.x > p1.x) + { + tile_seg.vector = p - p0; + } + else + { + tile_seg.origin = p; + tile_seg.vector = p1 - p; + } + if (tile_seg.vector.x == 0.0) + { + tile_seg.vector.x = sign(p1.x - p0.x) * 9.999999717180685365747194737196e-10; + } + } + if ((x <= min_xray) || (max_xray < x)) + { + y_edge = 1000000000.0; + } + } + tile_seg.y_edge = y_edge; + tile_seg.next.offset = old; + Alloc param_55 = tile_alloc.alloc; + TileSegRef param_56 = TileSegRef(tile_offset); + TileSeg param_57 = tile_seg; + TileSeg_write(param_55, param_56, param_57); + tile_offset += 24u; + } + xc += b; + base += stride; + xray = next_xray; + } + n_out++; + target += v_step; + p0 = p1; + continue; + } + else + { + break; + } + } + val_sum += params_1.val; + qp0 = qp2_1; + } + break; + } + } +} + +`, + } + shader_stencil_frag = driver.ShaderSources{ + Name: "stencil.frag", + Inputs: []driver.InputLocation{{Name: "vFrom", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vCtrl", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}, {Name: "vTo", Location: 2, Semantic: "TEXCOORD", SemanticIndex: 2, Type: 0x0, Size: 2}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +varying vec2 vTo; +varying vec2 vFrom; +varying vec2 vCtrl; + +void main() +{ + float dx = vTo.x - vFrom.x; + bool increasing = vTo.x >= vFrom.x; + bvec2 _35 = bvec2(increasing); + vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y); + bvec2 _41 = bvec2(increasing); + vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y); + vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5)); + float midx = mix(extent.x, extent.y, 0.5); + float x0 = midx - left.x; + vec2 p1 = vCtrl - left; + vec2 v = right - vCtrl; + float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0))); + float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t); + vec2 d_half = mix(p1, v, vec2(t)); + float dy = d_half.y / d_half.x; + float width = extent.y - extent.x; + dy = abs(dy * width); + vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy); + sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0)); + float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w)); + area *= width; + if (width == 0.0) + { + area = 0.0; + } + gl_FragData[0].x = area; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +in vec2 vTo; +in vec2 vFrom; +in vec2 vCtrl; +layout(location = 0) out vec4 fragCover; + +void main() +{ + float dx = vTo.x - vFrom.x; + bool increasing = vTo.x >= vFrom.x; + bvec2 _35 = bvec2(increasing); + vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y); + bvec2 _41 = bvec2(increasing); + vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y); + vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5)); + float midx = mix(extent.x, extent.y, 0.5); + float x0 = midx - left.x; + vec2 p1 = vCtrl - left; + vec2 v = right - vCtrl; + float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0))); + float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t); + vec2 d_half = mix(p1, v, vec2(t)); + float dy = d_half.y / d_half.x; + float width = extent.y - extent.x; + dy = abs(dy * width); + vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy); + sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0)); + float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w)); + area *= width; + if (width == 0.0) + { + area = 0.0; + } + fragCover.x = area; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +in vec2 vTo; +in vec2 vFrom; +in vec2 vCtrl; +out vec4 fragCover; + +void main() +{ + float dx = vTo.x - vFrom.x; + bool increasing = vTo.x >= vFrom.x; + bvec2 _35 = bvec2(increasing); + vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y); + bvec2 _41 = bvec2(increasing); + vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y); + vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5)); + float midx = mix(extent.x, extent.y, 0.5); + float x0 = midx - left.x; + vec2 p1 = vCtrl - left; + vec2 v = right - vCtrl; + float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0))); + float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t); + vec2 d_half = mix(p1, v, vec2(t)); + float dy = d_half.y / d_half.x; + float width = extent.y - extent.x; + dy = abs(dy * width); + vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy); + sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0)); + float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w)); + area *= width; + if (width == 0.0) + { + area = 0.0; + } + fragCover.x = area; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +in vec2 vTo; +in vec2 vFrom; +in vec2 vCtrl; +out vec4 fragCover; + +void main() +{ + float dx = vTo.x - vFrom.x; + bool increasing = vTo.x >= vFrom.x; + bvec2 _35 = bvec2(increasing); + vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y); + bvec2 _41 = bvec2(increasing); + vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y); + vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5)); + float midx = mix(extent.x, extent.y, 0.5); + float x0 = midx - left.x; + vec2 p1 = vCtrl - left; + vec2 v = right - vCtrl; + float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0))); + float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t); + vec2 d_half = mix(p1, v, vec2(t)); + float dy = d_half.y / d_half.x; + float width = extent.y - extent.x; + dy = abs(dy * width); + vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy); + sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0)); + float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w)); + area *= width; + if (width == 0.0) + { + area = 0.0; + } + fragCover.x = area; +} + +`, + HLSL: "DXBC\x94!\xb9\x13L\xba\r\x11\x8f\xc7\xce\x0eAs\xec\xe1\x01\x00\x00\x00\\\n\x00\x00\x06\x00\x00\x008\x00\x00\x00\x9c\x03\x00\x00\xfc\b\x00\x00x\t\x00\x00\xc4\t\x00\x00(\n\x00\x00Aon9\\\x03\x00\x00\\\x03\x00\x00\x00\x02\xff\xff8\x03\x00\x00$\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x00\xbf\x00\x00\x00?\x00\x00\x80?\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x80\x01\x00\x03\xb0\v\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\xb0\x00\x00\x00\xa0\v\x00\x00\x03\x00\x00\x02\x80\x01\x00\x00\xb0\x00\x00\x00\xa0\n\x00\x00\x03\x01\x00\x03\x80\x00\x00\xe4\x80\x00\x00U\xa0\x02\x00\x00\x03\x00\x00\x01\x80\x01\x00\x00\x81\x01\x00U\x80\x04\x00\x00\x04\x00\x00\x02\x80\x00\x00\x00\x80\x00\x00U\xa0\x01\x00\x00\x80\x01\x00\x00\x02\x01\x00\x03\x80\x00\x00\xe4\xb0\n\x00\x00\x03\x02\x00\x01\x80\x01\x00\x00\x80\x01\x00\x00\xb0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x02\x00\x00\x81\v\x00\x00\x03\x03\x00\x01\x80\x01\x00\x00\xb0\x01\x00\x00\x80\x02\x00\x00\x03\x00\x00\x04\x80\x01\x00\x00\x81\x01\x00\x00\xb0X\x00\x00\x04\x03\x00\x02\x80\x00\x00\xaa\x80\x01\x00U\xb0\x01\x00U\x80X\x00\x00\x04\x02\x00\x02\x80\x00\x00\xaa\x80\x01\x00U\x80\x01\x00U\xb0\x02\x00\x00\x03\x00\x00\f\x80\x03\x00\x1b\x80\x00\x00\xe4\xb1\x02\x00\x00\x03\x01\x00\x03\x80\x02\x00\xe4\x81\x00\x00\x1b\xb0\x02\x00\x00\x03\x01\x00\x04\x80\x00\x00\xff\x80\x01\x00\x00\x81\x05\x00\x00\x03\x01\x00\x04\x80\x00\x00U\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x04\x80\x01\x00\x00\x80\x01\x00\x00\x80\x01\x00\xaa\x80\a\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x06\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x02\x00\x00\x03\x01\x00\x04\x80\x01\x00\xaa\x80\x01\x00\x00\x80\x06\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x04\x80\x00\x00U\x80\x01\x00U\x80\x02\x00U\x80\x12\x00\x00\x04\x02\x00\x03\x80\x00\x00U\x80\x00\x00\x1b\x80\x01\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x00\x00\xaa\xb0\x12\x00\x00\x04\x02\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x01\x00\xaa\x80\x06\x00\x00\x02\x00\x00\x02\x80\x02\x00\x00\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x02\x00U\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00\x00\x80\x00\x00U\x80#\x00\x00\x02\x00\x00\x02\x80\x00\x00U\x80\x04\x00\x00\x04\x01\x00\x01\x80\x00\x00U\x80\x00\x00U\xa0\x02\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x02\x80\x00\x00U\x80\x00\x00\x00\xa0\x02\x00\xaa\x80\x06\x00\x00\x02\x00\x00\x02\x80\x00\x00U\x80\x02\x00\x00\x03\x00\x00\f\x80\x02\x00\xaa\x81\x00\x00\x1b\xa0\x05\x00\x00\x03\x01\x00\b\x80\x00\x00U\x80\x00\x00\xff\x80\x05\x00\x00\x03\x01\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x02\x00\x00\x03\x01\x00\x1f\x80\x01\x00\xe4\x80\x00\x00U\xa0\x04\x00\x00\x04\x00\x00\x02\x80\x01\x00\xaa\x80\x01\x00U\x81\x01\x00\xaa\x80\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\xaa\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x01\x00\x00\x81\x00\x00U\x80\x04\x00\x00\x04\x00\x00\x02\x80\x01\x00\x00\x80\x01\x00\xff\x80\x00\x00U\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00U\xa0X\x00\x00\x04\x00\x00\x01\x80\x00\x00\x00\x81\x00\x00\xff\xa0\x00\x00U\x80\x01\x00\x00\x02\x00\x00\x0e\x80\x00\x00\xff\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDRX\x05\x00\x00@\x00\x00\x00V\x01\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03\xc2\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x006\x00\x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x006\x00\x00\x05\"\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x004\x00\x00\n2\x00\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x003\x00\x00\n2\x00\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\"\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\n\x00\x10\x00\x00\x00\x00\x003\x00\x00\a2\x00\x10\x00\x01\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x01\x00\x00\x00\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x004\x00\x00\a2\x00\x10\x00\x02\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x01\x00\x00\x00\x1d\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x007\x00\x00\tB\x00\x10\x00\x02\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x01\x00\x00\x00\x1a\x10\x10\x00\x00\x00\x00\x007\x00\x00\tB\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x01\x00\x00\x00\x00\x00\x00\br\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00\xa6\x1b\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x00\x00\x00\x00V\t\x10\x80A\x00\x00\x00\x01\x00\x00\x00\xa6\x1e\x10\x00\x00\x00\x00\x00\x00\x00\x00\b\xb2\x00\x10\x00\x01\x00\x00\x00\xa6\x0e\x10\x80A\x00\x00\x00\x00\x00\x00\x00F\b\x10\x00\x02\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00K\x00\x00\x05\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\a\x12\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x0e\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\xc2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00V\r\x10\x00\x01\x00\x00\x00\xa6\x0e\x10\x00\x00\x00\x00\x00\x0e\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x008\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x82\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x02\x00\x00\x00:\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\b\x82\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\v2\x00\x10\x00\x01\x00\x00\x00\x06\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\r2\x00\x10\x00\x02\x00\x00\x00\xa6\n\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00\x0e\x00\x00\b\xc2\x00\x10\x00\x02\x00\x00\x00\x06\x04\x10\x00\x01\x00\x00\x00\xa6\n\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x00 \x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x0e\x10\x00\x02\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00?\x00\x00\x00?\x00\x00\x00?2\x00\x00\n\x12\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00\x1a\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00:\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x18\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00?7\x00\x00\t\x12 \x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x006\x00\x00\b\xe2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00)\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\\\x00\x00\x00\x03\x00\x00\x00\b\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00P\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\f\x00\x00P\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + } + shader_stencil_vert = driver.ShaderSources{ + Name: "stencil.vert", + Inputs: []driver.InputLocation{{Name: "corner", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 1}, {Name: "maxy", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 1}, {Name: "from", Location: 2, Semantic: "TEXCOORD", SemanticIndex: 2, Type: 0x0, Size: 2}, {Name: "ctrl", Location: 3, Semantic: "TEXCOORD", SemanticIndex: 3, Type: 0x0, Size: 2}, {Name: "to", Location: 4, Semantic: "TEXCOORD", SemanticIndex: 4, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.pathOffset", Type: 0x0, Size: 2, Offset: 16}}, + Size: 24, + }, + GLSL100ES: `#version 100 + +struct Block +{ + vec4 transform; + vec2 pathOffset; +}; + +uniform Block _block; + +attribute vec2 from; +attribute vec2 ctrl; +attribute vec2 to; +attribute float maxy; +attribute float corner; +varying vec2 vFrom; +varying vec2 vCtrl; +varying vec2 vTo; + +void main() +{ + vec2 from_1 = from + _block.pathOffset; + vec2 ctrl_1 = ctrl + _block.pathOffset; + vec2 to_1 = to + _block.pathOffset; + float maxy_1 = maxy + _block.pathOffset.y; + float c = corner; + vec2 pos; + if (c >= 0.375) + { + c -= 0.5; + pos.y = maxy_1 + 1.0; + } + else + { + pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0; + } + if (c >= 0.125) + { + pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0; + } + else + { + pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0; + } + vFrom = from_1 - pos; + vCtrl = ctrl_1 - pos; + vTo = to_1 - pos; + pos = (pos * _block.transform.xy) + _block.transform.zw; + gl_Position = vec4(pos, 1.0, 1.0); +} + +`, + GLSL300ES: `#version 300 es + +layout(std140) uniform Block +{ + vec4 transform; + vec2 pathOffset; +} _block; + +layout(location = 2) in vec2 from; +layout(location = 3) in vec2 ctrl; +layout(location = 4) in vec2 to; +layout(location = 1) in float maxy; +layout(location = 0) in float corner; +out vec2 vFrom; +out vec2 vCtrl; +out vec2 vTo; + +void main() +{ + vec2 from_1 = from + _block.pathOffset; + vec2 ctrl_1 = ctrl + _block.pathOffset; + vec2 to_1 = to + _block.pathOffset; + float maxy_1 = maxy + _block.pathOffset.y; + float c = corner; + vec2 pos; + if (c >= 0.375) + { + c -= 0.5; + pos.y = maxy_1 + 1.0; + } + else + { + pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0; + } + if (c >= 0.125) + { + pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0; + } + else + { + pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0; + } + vFrom = from_1 - pos; + vCtrl = ctrl_1 - pos; + vTo = to_1 - pos; + pos = (pos * _block.transform.xy) + _block.transform.zw; + gl_Position = vec4(pos, 1.0, 1.0); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Block +{ + vec4 transform; + vec2 pathOffset; +}; + +uniform Block _block; + +in vec2 from; +in vec2 ctrl; +in vec2 to; +in float maxy; +in float corner; +out vec2 vFrom; +out vec2 vCtrl; +out vec2 vTo; + +void main() +{ + vec2 from_1 = from + _block.pathOffset; + vec2 ctrl_1 = ctrl + _block.pathOffset; + vec2 to_1 = to + _block.pathOffset; + float maxy_1 = maxy + _block.pathOffset.y; + float c = corner; + vec2 pos; + if (c >= 0.375) + { + c -= 0.5; + pos.y = maxy_1 + 1.0; + } + else + { + pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0; + } + if (c >= 0.125) + { + pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0; + } + else + { + pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0; + } + vFrom = from_1 - pos; + vCtrl = ctrl_1 - pos; + vTo = to_1 - pos; + pos = (pos * _block.transform.xy) + _block.transform.zw; + gl_Position = vec4(pos, 1.0, 1.0); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Block +{ + vec4 transform; + vec2 pathOffset; +} _block; + +in vec2 from; +in vec2 ctrl; +in vec2 to; +in float maxy; +in float corner; +out vec2 vFrom; +out vec2 vCtrl; +out vec2 vTo; + +void main() +{ + vec2 from_1 = from + _block.pathOffset; + vec2 ctrl_1 = ctrl + _block.pathOffset; + vec2 to_1 = to + _block.pathOffset; + float maxy_1 = maxy + _block.pathOffset.y; + float c = corner; + vec2 pos; + if (c >= 0.375) + { + c -= 0.5; + pos.y = maxy_1 + 1.0; + } + else + { + pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0; + } + if (c >= 0.125) + { + pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0; + } + else + { + pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0; + } + vFrom = from_1 - pos; + vCtrl = ctrl_1 - pos; + vTo = to_1 - pos; + pos = (pos * _block.transform.xy) + _block.transform.zw; + gl_Position = vec4(pos, 1.0, 1.0); +} + +`, + HLSL: "DXBC\xa5!\xd8\x10\xb4n\x90\xe3\xd9U\xdb\xe2\xb6~I0\x01\x00\x00\x00\x10\b\x00\x00\x06\x00\x00\x008\x00\x00\x00L\x02\x00\x00t\x05\x00\x00\xf0\x05\x00\x00\xf4\x06\x00\x00\x88\a\x00\x00Aon9\f\x02\x00\x00\f\x02\x00\x00\x00\x02\xfe\xff\xd8\x01\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x03\x00\x0f\xa0\x00\x00\xc0>\x00\x00\x80?\x00\x00\x80\xbf\x00\x00\x00\xbfQ\x00\x00\x05\x04\x00\x0f\xa0\x00\x00\x00>\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x02\x80\x02\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x03\x80\x03\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x04\x80\x04\x00\x0f\x90\x02\x00\x00\x03\x00\x00\x01\x80\x01\x00\x00\x90\x02\x00U\xa0\x02\x00\x00\x03\x00\x00\x04\x80\x00\x00\x00\x80\x03\x00U\xa0\r\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\x90\x03\x00\x00\xa0\x01\x00\x00\x02\x01\x00\x04\x80\x00\x00\x00\x90\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00\x00\x90\x03\x00\xff\xa0\x02\x00\x00\x03\x02\x00\x03\x80\x02\x00\xe4\x90\x02\x00\xe4\xa0\x02\x00\x00\x03\x02\x00\f\x80\x03\x00\x14\x90\x02\x00\x14\xa0\n\x00\x00\x03\x03\x00\x03\x80\x02\x00\xee\x80\x02\x00\xe1\x80\x02\x00\x00\x03\x03\x00\f\x80\x04\x00D\x90\x02\x00D\xa0\n\x00\x00\x03\x03\x00\x03\x80\x03\x00\xeb\x80\x03\x00\xe4\x80\x02\x00\x00\x03\x01\x00\x03\x80\x03\x00\xe4\x80\x03\x00\xaa\xa0\x12\x00\x00\x04\x04\x00\x06\x80\x00\x00\x00\x80\x00\x00\xe4\x80\x01\x00Ȁ\r\x00\x00\x03\x00\x00\x01\x80\x04\x00U\x80\x04\x00\x00\xa0\v\x00\x00\x03\x00\x00\x02\x80\x02\x00\xff\x80\x02\x00\x00\x80\v\x00\x00\x03\x00\x00\x02\x80\x03\x00\xaa\x80\x00\x00U\x80\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x03\x00U\xa0\x12\x00\x00\x04\x04\x00\x01\x80\x00\x00\x00\x80\x00\x00U\x80\x01\x00U\x80\x02\x00\x00\x03\x00\x00\x0f\xe0\x02\x00\xe4\x80\x04\x00(\x81\x02\x00\x00\x03\x01\x00\x03\xe0\x03\x00\xee\x80\x04\x00\xe8\x81\x04\x00\x00\x04\x00\x00\x03\x80\x04\x00\xe8\x80\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\f\xc0\x03\x00U\xa0\xff\xff\x00\x00SHDR \x03\x00\x00@\x00\x01\x00\xc8\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00_\x00\x00\x03\x12\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x03\x12\x10\x10\x00\x01\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x02\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x03\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x04\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00e\x00\x00\x03\xc2 \x10\x00\x00\x00\x00\x00e\x00\x00\x032 \x10\x00\x01\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x02\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x04\x00\x00\x00\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x00\x1a\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x1d\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\xc0>\x00\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\xbf6\x00\x00\x05B\x00\x10\x00\x01\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\b2\x00\x10\x00\x02\x00\x00\x00F\x10\x10\x00\x02\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x02\x00\x00\x00\x06\x14\x10\x00\x03\x00\x00\x00\x06\x84 \x00\x00\x00\x00\x00\x01\x00\x00\x003\x00\x00\a2\x00\x10\x00\x03\x00\x00\x00\xb6\x0f\x10\x00\x02\x00\x00\x00\x16\x05\x10\x00\x02\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x03\x00\x00\x00\x06\x14\x10\x00\x04\x00\x00\x00\x06\x84 \x00\x00\x00\x00\x00\x01\x00\x00\x003\x00\x00\a2\x00\x10\x00\x03\x00\x00\x00\xb6\x0f\x10\x00\x03\x00\x00\x00F\x00\x10\x00\x03\x00\x00\x00\x00\x00\x00\n2\x00\x10\x00\x01\x00\x00\x00F\x00\x10\x00\x03\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80\xbf\x00\x00\x00\x00\x00\x00\x00\x007\x00\x00\tb\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00V\x06\x10\x00\x00\x00\x00\x00\xa6\b\x10\x00\x01\x00\x00\x00\x1d\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00>4\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x02\x00\x00\x00\n\x00\x10\x00\x02\x00\x00\x004\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x03\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?7\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x86\b\x10\x80A\x00\x00\x00\x00\x00\x00\x00F\x0e\x10\x00\x02\x00\x00\x00\x00\x00\x00\b2 \x10\x00\x01\x00\x00\x00\x86\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\xe6\n\x10\x00\x03\x00\x00\x002\x00\x00\v2 \x10\x00\x02\x00\x00\x00\x86\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x02\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x16\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xfc\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\xd4\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x02\x00\x00\x00\\\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\b\x00\x00\x00\x02\x00\x00\x00\xc4\x00\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_pathOffset\x00\xab\xab\x01\x00\x03\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\x8c\x00\x00\x00\x05\x00\x00\x00\b\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x80\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x01\x01\x00\x00\x80\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x03\x03\x00\x00\x80\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x03\x00\x00\x00\x03\x03\x00\x00\x80\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN\x80\x00\x00\x00\x04\x00\x00\x00\b\x00\x00\x00h\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00h\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x03\x00\x00h\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\f\x00\x00q\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_tile_alloc_comp = driver.ShaderSources{ + Name: "tile_alloc.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct MallocResult +{ + Alloc alloc; + bool failed; +}; + +struct AnnoEndClipRef +{ + uint offset; +}; + +struct AnnoEndClip +{ + vec4 bbox; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct AnnotatedTag +{ + uint tag; + uint flags; +}; + +struct PathRef +{ + uint offset; +}; + +struct TileRef +{ + uint offset; +}; + +struct Path +{ + uvec4 bbox; + TileRef tiles; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _96; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _309; + +shared uint sh_tile_count[128]; +shared MallocResult sh_tile_alloc; + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _96.memory[offset]; + return v; +} + +AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +AnnoEndClip AnnoEndClip_read(Alloc a, AnnoEndClipRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + AnnoEndClip s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + return s; +} + +AnnoEndClip Annotated_EndClip_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoEndClipRef param_1 = AnnoEndClipRef(ref.offset + 4u); + return AnnoEndClip_read(param, param_1); +} + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +MallocResult malloc(uint size) +{ + MallocResult r; + r.failed = false; + uint _102 = atomicAdd(_96.mem_offset, size); + uint offset = _102; + uint param = offset; + uint param_1 = size; + r.alloc = new_alloc(param, param_1); + if ((offset + size) > uint(int(uint(_96.memory.length())) * 4)) + { + r.failed = true; + uint _123 = atomicMax(_96.mem_error, 1u); + return r; + } + return r; +} + +Alloc slice_mem(Alloc a, uint offset, uint size) +{ + uint param = a.offset + offset; + uint param_1 = size; + return new_alloc(param, param_1); +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _96.memory[offset] = val; +} + +void Path_write(Alloc a, PathRef ref, Path s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.bbox.x | (s.bbox.y << uint(16)); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = s.bbox.z | (s.bbox.w << uint(16)); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = s.tiles.offset; + write_mem(param_6, param_7, param_8); +} + +void main() +{ + if (_96.mem_error != 0u) + { + return; + } + uint th_ix = gl_LocalInvocationID.x; + uint element_ix = gl_GlobalInvocationID.x; + PathRef path_ref = PathRef(_309.conf.tile_alloc.offset + (element_ix * 12u)); + AnnotatedRef ref = AnnotatedRef(_309.conf.anno_alloc.offset + (element_ix * 32u)); + uint tag = 0u; + if (element_ix < _309.conf.n_elements) + { + Alloc param; + param.offset = _309.conf.anno_alloc.offset; + AnnotatedRef param_1 = ref; + tag = Annotated_tag(param, param_1).tag; + } + int x0 = 0; + int y0 = 0; + int x1 = 0; + int y1 = 0; + switch (tag) + { + case 1u: + case 2u: + case 3u: + case 4u: + { + Alloc param_2; + param_2.offset = _309.conf.anno_alloc.offset; + AnnotatedRef param_3 = ref; + AnnoEndClip clip = Annotated_EndClip_read(param_2, param_3); + x0 = int(floor(clip.bbox.x * 0.03125)); + y0 = int(floor(clip.bbox.y * 0.03125)); + x1 = int(ceil(clip.bbox.z * 0.03125)); + y1 = int(ceil(clip.bbox.w * 0.03125)); + break; + } + } + x0 = clamp(x0, 0, int(_309.conf.width_in_tiles)); + y0 = clamp(y0, 0, int(_309.conf.height_in_tiles)); + x1 = clamp(x1, 0, int(_309.conf.width_in_tiles)); + y1 = clamp(y1, 0, int(_309.conf.height_in_tiles)); + Path path; + path.bbox = uvec4(uint(x0), uint(y0), uint(x1), uint(y1)); + uint tile_count = uint((x1 - x0) * (y1 - y0)); + if (tag == 4u) + { + tile_count = 0u; + } + sh_tile_count[th_ix] = tile_count; + uint total_tile_count = tile_count; + for (uint i = 0u; i < 7u; i++) + { + barrier(); + if (th_ix >= uint(1 << int(i))) + { + total_tile_count += sh_tile_count[th_ix - uint(1 << int(i))]; + } + barrier(); + sh_tile_count[th_ix] = total_tile_count; + } + if (th_ix == 127u) + { + uint param_4 = total_tile_count * 8u; + MallocResult _482 = malloc(param_4); + sh_tile_alloc = _482; + } + barrier(); + MallocResult alloc_start = sh_tile_alloc; + if (alloc_start.failed) + { + return; + } + if (element_ix < _309.conf.n_elements) + { + uint _499; + if (th_ix > 0u) + { + _499 = sh_tile_count[th_ix - 1u]; + } + else + { + _499 = 0u; + } + uint tile_subix = _499; + Alloc param_5 = alloc_start.alloc; + uint param_6 = 8u * tile_subix; + uint param_7 = 8u * tile_count; + Alloc tiles_alloc = slice_mem(param_5, param_6, param_7); + path.tiles = TileRef(tiles_alloc.offset); + Alloc param_8; + param_8.offset = _309.conf.tile_alloc.offset; + PathRef param_9 = path_ref; + Path param_10 = path; + Path_write(param_8, param_9, param_10); + } + uint total_count = sh_tile_count[127] * 2u; + uint start_ix = alloc_start.alloc.offset >> uint(2); + for (uint i_1 = th_ix; i_1 < total_count; i_1 += 128u) + { + Alloc param_11 = alloc_start.alloc; + uint param_12 = start_ix + i_1; + uint param_13 = 0u; + write_mem(param_11, param_12, param_13); + } +} + +`, + } +) diff --git a/gio/giold/gpu/timer.go b/gio/giold/gpu/timer.go new file mode 100644 index 0000000..6e0bd4a --- /dev/null +++ b/gio/giold/gpu/timer.go @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "time" + + "realy.lol/gio/gpu/internal/driver" +) + +type timers struct { + backend driver.Device + timers []*timer +} + +type timer struct { + Elapsed time.Duration + backend driver.Device + timer driver.Timer + state timerState +} + +type timerState uint8 + +const ( + timerIdle timerState = iota + timerRunning + timerWaiting +) + +func newTimers(b driver.Device) *timers { + return &timers{ + backend: b, + } +} + +func (t *timers) newTimer() *timer { + if t == nil { + return nil + } + tt := &timer{ + backend: t.backend, + timer: t.backend.NewTimer(), + } + t.timers = append(t.timers, tt) + return tt +} + +func (t *timer) begin() { + if t == nil || t.state != timerIdle { + return + } + t.timer.Begin() + t.state = timerRunning +} + +func (t *timer) end() { + if t == nil || t.state != timerRunning { + return + } + t.timer.End() + t.state = timerWaiting +} + +func (t *timers) ready() bool { + if t == nil { + return false + } + for _, tt := range t.timers { + switch tt.state { + case timerIdle: + continue + case timerRunning: + return false + } + d, ok := tt.timer.Duration() + if !ok { + return false + } + tt.state = timerIdle + tt.Elapsed = d + } + return t.backend.IsTimeContinuous() +} + +func (t *timers) release() { + if t == nil { + return + } + for _, tt := range t.timers { + tt.timer.Release() + } + t.timers = nil +} diff --git a/gio/giold/internal/byteslice/byteslice.go b/gio/giold/internal/byteslice/byteslice.go new file mode 100644 index 0000000..26ebdb2 --- /dev/null +++ b/gio/giold/internal/byteslice/byteslice.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package byteslice provides byte slice views of other Go values such as +// slices and structs. +package byteslice + +import ( + "reflect" + "unsafe" +) + +// Struct returns a byte slice view of a struct. +func Struct(s interface{}) []byte { + v := reflect.ValueOf(s).Elem() + sz := int(v.Type().Size()) + var res []byte + h := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + h.Data = uintptr(unsafe.Pointer(v.UnsafeAddr())) + h.Cap = sz + h.Len = sz + return res +} + +// Uint32 returns a byte slice view of a uint32 slice. +func Uint32(s []uint32) []byte { + n := len(s) + if n == 0 { + return nil + } + blen := n * int(unsafe.Sizeof(s[0])) + return (*[1 << 30]byte)(unsafe.Pointer(&s[0]))[:blen:blen] +} + +// Slice returns a byte slice view of a slice. +func Slice(s interface{}) []byte { + v := reflect.ValueOf(s) + first := v.Index(0) + sz := int(first.Type().Size()) + var res []byte + h := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + h.Data = first.UnsafeAddr() + h.Cap = v.Cap() * sz + h.Len = v.Len() * sz + return res +} diff --git a/gio/giold/internal/cocoainit/cocoa_darwin.go b/gio/giold/internal/cocoainit/cocoa_darwin.go new file mode 100644 index 0000000..2a34e57 --- /dev/null +++ b/gio/giold/internal/cocoainit/cocoa_darwin.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package cocoainit initializes support for multithreaded +// programs in Cocoa. +package cocoainit + +/* +#cgo CFLAGS: -xobjective-c -fmodules -fobjc-arc +#import + +static inline void activate_cocoa_multithreading() { + [[NSThread new] start]; +} +#pragma GCC visibility push(hidden) +*/ +import "C" + +func init() { + C.activate_cocoa_multithreading() +} diff --git a/gio/giold/internal/d3d11/d3d11_windows.go b/gio/giold/internal/d3d11/d3d11_windows.go new file mode 100644 index 0000000..f33eb61 --- /dev/null +++ b/gio/giold/internal/d3d11/d3d11_windows.go @@ -0,0 +1,1470 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package d3d11 + +import ( + "fmt" + "math" + "syscall" + "unsafe" + + "realy.lol/gio/internal/f32color" + + "golang.org/x/sys/windows" +) + +type DXGI_SWAP_CHAIN_DESC struct { + BufferDesc DXGI_MODE_DESC + SampleDesc DXGI_SAMPLE_DESC + BufferUsage uint32 + BufferCount uint32 + OutputWindow windows.Handle + Windowed uint32 + SwapEffect uint32 + Flags uint32 +} + +type DXGI_SAMPLE_DESC struct { + Count uint32 + Quality uint32 +} + +type DXGI_MODE_DESC struct { + Width uint32 + Height uint32 + RefreshRate DXGI_RATIONAL + Format uint32 + ScanlineOrdering uint32 + Scaling uint32 +} + +type DXGI_RATIONAL struct { + Numerator uint32 + Denominator uint32 +} + +type TEXTURE2D_DESC struct { + Width uint32 + Height uint32 + MipLevels uint32 + ArraySize uint32 + Format uint32 + SampleDesc DXGI_SAMPLE_DESC + Usage uint32 + BindFlags uint32 + CPUAccessFlags uint32 + MiscFlags uint32 +} + +type SAMPLER_DESC struct { + Filter uint32 + AddressU uint32 + AddressV uint32 + AddressW uint32 + MipLODBias float32 + MaxAnisotropy uint32 + ComparisonFunc uint32 + BorderColor [4]float32 + MinLOD float32 + MaxLOD float32 +} + +type SHADER_RESOURCE_VIEW_DESC_TEX2D struct { + SHADER_RESOURCE_VIEW_DESC + Texture2D TEX2D_SRV +} + +type SHADER_RESOURCE_VIEW_DESC struct { + Format uint32 + ViewDimension uint32 +} + +type TEX2D_SRV struct { + MostDetailedMip uint32 + MipLevels uint32 +} + +type INPUT_ELEMENT_DESC struct { + SemanticName *byte + SemanticIndex uint32 + Format uint32 + InputSlot uint32 + AlignedByteOffset uint32 + InputSlotClass uint32 + InstanceDataStepRate uint32 +} + +type IDXGISwapChain struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + GetDevice uintptr + Present uintptr + GetBuffer uintptr + SetFullscreenState uintptr + GetFullscreenState uintptr + GetDesc uintptr + ResizeBuffers uintptr + ResizeTarget uintptr + GetContainingOutput uintptr + GetFrameStatistics uintptr + GetLastPresentCount uintptr + } +} + +type Device struct { + Vtbl *struct { + _IUnknownVTbl + CreateBuffer uintptr + CreateTexture1D uintptr + CreateTexture2D uintptr + CreateTexture3D uintptr + CreateShaderResourceView uintptr + CreateUnorderedAccessView uintptr + CreateRenderTargetView uintptr + CreateDepthStencilView uintptr + CreateInputLayout uintptr + CreateVertexShader uintptr + CreateGeometryShader uintptr + CreateGeometryShaderWithStreamOutput uintptr + CreatePixelShader uintptr + CreateHullShader uintptr + CreateDomainShader uintptr + CreateComputeShader uintptr + CreateClassLinkage uintptr + CreateBlendState uintptr + CreateDepthStencilState uintptr + CreateRasterizerState uintptr + CreateSamplerState uintptr + CreateQuery uintptr + CreatePredicate uintptr + CreateCounter uintptr + CreateDeferredContext uintptr + OpenSharedResource uintptr + CheckFormatSupport uintptr + CheckMultisampleQualityLevels uintptr + CheckCounterInfo uintptr + CheckCounter uintptr + CheckFeatureSupport uintptr + GetPrivateData uintptr + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetFeatureLevel uintptr + GetCreationFlags uintptr + GetDeviceRemovedReason uintptr + GetImmediateContext uintptr + SetExceptionMode uintptr + GetExceptionMode uintptr + } +} + +type DeviceContext struct { + Vtbl *struct { + _IUnknownVTbl + GetDevice uintptr + GetPrivateData uintptr + SetPrivateData uintptr + SetPrivateDataInterface uintptr + VSSetConstantBuffers uintptr + PSSetShaderResources uintptr + PSSetShader uintptr + PSSetSamplers uintptr + VSSetShader uintptr + DrawIndexed uintptr + Draw uintptr + Map uintptr + Unmap uintptr + PSSetConstantBuffers uintptr + IASetInputLayout uintptr + IASetVertexBuffers uintptr + IASetIndexBuffer uintptr + DrawIndexedInstanced uintptr + DrawInstanced uintptr + GSSetConstantBuffers uintptr + GSSetShader uintptr + IASetPrimitiveTopology uintptr + VSSetShaderResources uintptr + VSSetSamplers uintptr + Begin uintptr + End uintptr + GetData uintptr + SetPredication uintptr + GSSetShaderResources uintptr + GSSetSamplers uintptr + OMSetRenderTargets uintptr + OMSetRenderTargetsAndUnorderedAccessViews uintptr + OMSetBlendState uintptr + OMSetDepthStencilState uintptr + SOSetTargets uintptr + DrawAuto uintptr + DrawIndexedInstancedIndirect uintptr + DrawInstancedIndirect uintptr + Dispatch uintptr + DispatchIndirect uintptr + RSSetState uintptr + RSSetViewports uintptr + RSSetScissorRects uintptr + CopySubresourceRegion uintptr + CopyResource uintptr + UpdateSubresource uintptr + CopyStructureCount uintptr + ClearRenderTargetView uintptr + ClearUnorderedAccessViewUint uintptr + ClearUnorderedAccessViewFloat uintptr + ClearDepthStencilView uintptr + GenerateMips uintptr + SetResourceMinLOD uintptr + GetResourceMinLOD uintptr + ResolveSubresource uintptr + ExecuteCommandList uintptr + HSSetShaderResources uintptr + HSSetShader uintptr + HSSetSamplers uintptr + HSSetConstantBuffers uintptr + DSSetShaderResources uintptr + DSSetShader uintptr + DSSetSamplers uintptr + DSSetConstantBuffers uintptr + CSSetShaderResources uintptr + CSSetUnorderedAccessViews uintptr + CSSetShader uintptr + CSSetSamplers uintptr + CSSetConstantBuffers uintptr + VSGetConstantBuffers uintptr + PSGetShaderResources uintptr + PSGetShader uintptr + PSGetSamplers uintptr + VSGetShader uintptr + PSGetConstantBuffers uintptr + IAGetInputLayout uintptr + IAGetVertexBuffers uintptr + IAGetIndexBuffer uintptr + GSGetConstantBuffers uintptr + GSGetShader uintptr + IAGetPrimitiveTopology uintptr + VSGetShaderResources uintptr + VSGetSamplers uintptr + GetPredication uintptr + GSGetShaderResources uintptr + GSGetSamplers uintptr + OMGetRenderTargets uintptr + OMGetRenderTargetsAndUnorderedAccessViews uintptr + OMGetBlendState uintptr + OMGetDepthStencilState uintptr + SOGetTargets uintptr + RSGetState uintptr + RSGetViewports uintptr + RSGetScissorRects uintptr + HSGetShaderResources uintptr + HSGetShader uintptr + HSGetSamplers uintptr + HSGetConstantBuffers uintptr + DSGetShaderResources uintptr + DSGetShader uintptr + DSGetSamplers uintptr + DSGetConstantBuffers uintptr + CSGetShaderResources uintptr + CSGetUnorderedAccessViews uintptr + CSGetShader uintptr + CSGetSamplers uintptr + CSGetConstantBuffers uintptr + ClearState uintptr + Flush uintptr + GetType uintptr + GetContextFlags uintptr + FinishCommandList uintptr + } +} + +type RenderTargetView struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type Resource struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type Texture2D struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type Buffer struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type SamplerState struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type PixelShader struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type ShaderResourceView struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type DepthStencilView struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type BlendState struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type DepthStencilState struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type VertexShader struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type RasterizerState struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type InputLayout struct { + Vtbl *struct { + _IUnknownVTbl + GetBufferPointer uintptr + GetBufferSize uintptr + } +} + +type DEPTH_STENCIL_DESC struct { + DepthEnable uint32 + DepthWriteMask uint32 + DepthFunc uint32 + StencilEnable uint32 + StencilReadMask uint8 + StencilWriteMask uint8 + FrontFace DEPTH_STENCILOP_DESC + BackFace DEPTH_STENCILOP_DESC +} + +type DEPTH_STENCILOP_DESC struct { + StencilFailOp uint32 + StencilDepthFailOp uint32 + StencilPassOp uint32 + StencilFunc uint32 +} + +type DEPTH_STENCIL_VIEW_DESC_TEX2D struct { + Format uint32 + ViewDimension uint32 + Flags uint32 + Texture2D TEX2D_DSV +} + +type TEX2D_DSV struct { + MipSlice uint32 +} + +type BLEND_DESC struct { + AlphaToCoverageEnable uint32 + IndependentBlendEnable uint32 + RenderTarget [8]RENDER_TARGET_BLEND_DESC +} + +type RENDER_TARGET_BLEND_DESC struct { + BlendEnable uint32 + SrcBlend uint32 + DestBlend uint32 + BlendOp uint32 + SrcBlendAlpha uint32 + DestBlendAlpha uint32 + BlendOpAlpha uint32 + RenderTargetWriteMask uint8 +} + +type IDXGIObject struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + } +} + +type IDXGIAdapter struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + EnumOutputs uintptr + GetDesc uintptr + CheckInterfaceSupport uintptr + GetDesc1 uintptr + } +} + +type IDXGIFactory struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + EnumAdapters uintptr + MakeWindowAssociation uintptr + GetWindowAssociation uintptr + CreateSwapChain uintptr + CreateSoftwareAdapter uintptr + } +} + +type IDXGIDevice struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + GetAdapter uintptr + CreateSurface uintptr + QueryResourceResidency uintptr + SetGPUThreadPriority uintptr + GetGPUThreadPriority uintptr + } +} + +type IUnknown struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type _IUnknownVTbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr +} + +type BUFFER_DESC struct { + ByteWidth uint32 + Usage uint32 + BindFlags uint32 + CPUAccessFlags uint32 + MiscFlags uint32 + StructureByteStride uint32 +} + +type GUID struct { + Data1 uint32 + Data2 uint16 + Data3 uint16 + Data4_0 uint8 + Data4_1 uint8 + Data4_2 uint8 + Data4_3 uint8 + Data4_4 uint8 + Data4_5 uint8 + Data4_6 uint8 + Data4_7 uint8 +} + +type VIEWPORT struct { + TopLeftX float32 + TopLeftY float32 + Width float32 + Height float32 + MinDepth float32 + MaxDepth float32 +} + +type SUBRESOURCE_DATA struct { + pSysMem *byte +} + +type BOX struct { + Left uint32 + Top uint32 + Front uint32 + Right uint32 + Bottom uint32 + Back uint32 +} + +type MAPPED_SUBRESOURCE struct { + PData uintptr + RowPitch uint32 + DepthPitch uint32 +} + +type ErrorCode struct { + Name string + Code uint32 +} + +type RASTERIZER_DESC struct { + FillMode uint32 + CullMode uint32 + FrontCounterClockwise uint32 + DepthBias int32 + DepthBiasClamp float32 + SlopeScaledDepthBias float32 + DepthClipEnable uint32 + ScissorEnable uint32 + MultisampleEnable uint32 + AntialiasedLineEnable uint32 +} + +var ( + IID_Texture2D = GUID{0x6f15aaf2, 0xd208, 0x4e89, 0x9a, 0xb4, 0x48, 0x95, + 0x35, 0xd3, 0x4f, 0x9c} + IID_IDXGIDevice = GUID{0x54ec77fa, 0x1377, 0x44e6, 0x8c, 0x32, 0x88, 0xfd, + 0x5f, 0x44, 0xc8, 0x4c} + IID_IDXGIFactory = GUID{0x7b7166ec, 0x21c7, 0x44ae, 0xb2, 0x1a, 0xc9, 0xae, + 0x32, 0x1a, 0xe3, 0x69} +) + +var ( + d3d11 = windows.NewLazySystemDLL("d3d11.dll") + + _D3D11CreateDevice = d3d11.NewProc("D3D11CreateDevice") + _D3D11CreateDeviceAndSwapChain = d3d11.NewProc("D3D11CreateDeviceAndSwapChain") +) + +const ( + SDK_VERSION = 7 + DRIVER_TYPE_HARDWARE = 1 + + DXGI_FORMAT_UNKNOWN = 0 + DXGI_FORMAT_R16_FLOAT = 54 + DXGI_FORMAT_R32_FLOAT = 41 + DXGI_FORMAT_R32G32_FLOAT = 16 + DXGI_FORMAT_R32G32B32_FLOAT = 6 + DXGI_FORMAT_R32G32B32A32_FLOAT = 2 + DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29 + DXGI_FORMAT_R16_SINT = 59 + DXGI_FORMAT_R16G16_SINT = 38 + DXGI_FORMAT_R16_UINT = 57 + DXGI_FORMAT_D24_UNORM_S8_UINT = 45 + DXGI_FORMAT_R16G16_FLOAT = 34 + DXGI_FORMAT_R16G16B16A16_FLOAT = 10 + + FORMAT_SUPPORT_TEXTURE2D = 0x20 + FORMAT_SUPPORT_RENDER_TARGET = 0x4000 + + DXGI_USAGE_RENDER_TARGET_OUTPUT = 1 << (1 + 4) + + CPU_ACCESS_READ = 0x20000 + + MAP_READ = 1 + + DXGI_SWAP_EFFECT_DISCARD = 0 + + FEATURE_LEVEL_9_1 = 0x9100 + FEATURE_LEVEL_9_3 = 0x9300 + FEATURE_LEVEL_11_0 = 0xb000 + + USAGE_IMMUTABLE = 1 + USAGE_STAGING = 3 + + BIND_VERTEX_BUFFER = 0x1 + BIND_INDEX_BUFFER = 0x2 + BIND_CONSTANT_BUFFER = 0x4 + BIND_SHADER_RESOURCE = 0x8 + BIND_RENDER_TARGET = 0x20 + BIND_DEPTH_STENCIL = 0x40 + + PRIMITIVE_TOPOLOGY_TRIANGLELIST = 4 + PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 5 + + FILTER_MIN_MAG_LINEAR_MIP_POINT = 0x14 + FILTER_MIN_MAG_MIP_POINT = 0 + + TEXTURE_ADDRESS_MIRROR = 2 + TEXTURE_ADDRESS_CLAMP = 3 + TEXTURE_ADDRESS_WRAP = 1 + + SRV_DIMENSION_TEXTURE2D = 4 + + CREATE_DEVICE_DEBUG = 0x2 + + FILL_SOLID = 3 + + CULL_NONE = 1 + + CLEAR_DEPTH = 0x1 + CLEAR_STENCIL = 0x2 + + DSV_DIMENSION_TEXTURE2D = 3 + + DEPTH_WRITE_MASK_ALL = 1 + + COMPARISON_GREATER = 5 + COMPARISON_GREATER_EQUAL = 7 + + BLEND_OP_ADD = 1 + BLEND_ONE = 2 + BLEND_INV_SRC_ALPHA = 6 + BLEND_ZERO = 1 + BLEND_DEST_COLOR = 9 + BLEND_DEST_ALPHA = 7 + + COLOR_WRITE_ENABLE_ALL = 1 | 2 | 4 | 8 + + DXGI_STATUS_OCCLUDED = 0x087A0001 + DXGI_ERROR_DEVICE_RESET = 0x887A0007 + DXGI_ERROR_DEVICE_REMOVED = 0x887A0005 + D3DDDIERR_DEVICEREMOVED = 1<<31 | 0x876<<16 | 2160 +) + +func CreateDevice(driverType uint32, flags uint32) (*Device, *DeviceContext, + uint32, error) { + var ( + dev *Device + ctx *DeviceContext + featLvl uint32 + ) + r, _, _ := _D3D11CreateDevice.Call( + 0, // pAdapter + uintptr(driverType), // driverType + 0, // Software + uintptr(flags), // Flags + 0, // pFeatureLevels + 0, // FeatureLevels + SDK_VERSION, // SDKVersion + uintptr(unsafe.Pointer(&dev)), // ppDevice + uintptr(unsafe.Pointer(&featLvl)), // pFeatureLevel + uintptr(unsafe.Pointer(&ctx)), // ppImmediateContext + ) + if r != 0 { + return nil, nil, 0, ErrorCode{Name: "D3D11CreateDevice", + Code: uint32(r)} + } + return dev, ctx, featLvl, nil +} + +func CreateDeviceAndSwapChain(driverType uint32, flags uint32, + swapDesc *DXGI_SWAP_CHAIN_DESC) (*Device, *DeviceContext, *IDXGISwapChain, + uint32, error) { + var ( + dev *Device + ctx *DeviceContext + swchain *IDXGISwapChain + featLvl uint32 + ) + r, _, _ := _D3D11CreateDeviceAndSwapChain.Call( + 0, // pAdapter + uintptr(driverType), // driverType + 0, // Software + uintptr(flags), // Flags + 0, // pFeatureLevels + 0, // FeatureLevels + SDK_VERSION, // SDKVersion + uintptr(unsafe.Pointer(swapDesc)), // pSwapChainDesc + uintptr(unsafe.Pointer(&swchain)), // ppSwapChain + uintptr(unsafe.Pointer(&dev)), // ppDevice + uintptr(unsafe.Pointer(&featLvl)), // pFeatureLevel + uintptr(unsafe.Pointer(&ctx)), // ppImmediateContext + ) + if r != 0 { + return nil, nil, nil, 0, ErrorCode{Name: "D3D11CreateDeviceAndSwapChain", + Code: uint32(r)} + } + return dev, ctx, swchain, featLvl, nil +} + +func (d *Device) CheckFormatSupport(format uint32) (uint32, error) { + var support uint32 + r, _, _ := syscall.Syscall( + d.Vtbl.CheckFormatSupport, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(format), + uintptr(unsafe.Pointer(&support)), + ) + if r != 0 { + return 0, ErrorCode{Name: "DeviceCheckFormatSupport", Code: uint32(r)} + } + return support, nil +} + +func (d *Device) CreateBuffer(desc *BUFFER_DESC, data []byte) (*Buffer, error) { + var dataDesc *SUBRESOURCE_DATA + if len(data) > 0 { + dataDesc = &SUBRESOURCE_DATA{ + pSysMem: &data[0], + } + } + var buf *Buffer + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateBuffer, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(dataDesc)), + uintptr(unsafe.Pointer(&buf)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateBuffer", Code: uint32(r)} + } + return buf, nil +} + +func (d *Device) CreateDepthStencilViewTEX2D(res *Resource, + desc *DEPTH_STENCIL_VIEW_DESC_TEX2D) (*DepthStencilView, error) { + var view *DepthStencilView + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateDepthStencilView, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(res)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&view)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateDepthStencilView", + Code: uint32(r)} + } + return view, nil +} + +func (d *Device) CreatePixelShader(bytecode []byte) (*PixelShader, error) { + var shader *PixelShader + r, _, _ := syscall.Syscall6( + d.Vtbl.CreatePixelShader, + 5, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(&bytecode[0])), + uintptr(len(bytecode)), + 0, // pClassLinkage + uintptr(unsafe.Pointer(&shader)), + 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreatePixelShader", Code: uint32(r)} + } + return shader, nil +} + +func (d *Device) CreateVertexShader(bytecode []byte) (*VertexShader, error) { + var shader *VertexShader + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateVertexShader, + 5, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(&bytecode[0])), + uintptr(len(bytecode)), + 0, // pClassLinkage + uintptr(unsafe.Pointer(&shader)), + 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateVertexShader", Code: uint32(r)} + } + return shader, nil +} + +func (d *Device) CreateShaderResourceViewTEX2D(res *Resource, + desc *SHADER_RESOURCE_VIEW_DESC_TEX2D) (*ShaderResourceView, error) { + var resView *ShaderResourceView + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateShaderResourceView, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(res)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&resView)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateShaderResourceView", + Code: uint32(r)} + } + return resView, nil +} + +func (d *Device) CreateRasterizerState(desc *RASTERIZER_DESC) (*RasterizerState, + error) { + var state *RasterizerState + r, _, _ := syscall.Syscall( + d.Vtbl.CreateRasterizerState, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&state)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateRasterizerState", + Code: uint32(r)} + } + return state, nil +} + +func (d *Device) CreateInputLayout(descs []INPUT_ELEMENT_DESC, + bytecode []byte) (*InputLayout, error) { + var pdesc *INPUT_ELEMENT_DESC + if len(descs) > 0 { + pdesc = &descs[0] + } + var layout *InputLayout + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateInputLayout, + 6, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(pdesc)), + uintptr(len(descs)), + uintptr(unsafe.Pointer(&bytecode[0])), + uintptr(len(bytecode)), + uintptr(unsafe.Pointer(&layout)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateInputLayout", Code: uint32(r)} + } + return layout, nil +} + +func (d *Device) CreateSamplerState(desc *SAMPLER_DESC) (*SamplerState, error) { + var sampler *SamplerState + r, _, _ := syscall.Syscall( + d.Vtbl.CreateSamplerState, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&sampler)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateSamplerState", Code: uint32(r)} + } + return sampler, nil +} + +func (d *Device) CreateTexture2D(desc *TEXTURE2D_DESC) (*Texture2D, error) { + var tex *Texture2D + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateTexture2D, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + 0, // pInitialData + uintptr(unsafe.Pointer(&tex)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "CreateTexture2D", Code: uint32(r)} + } + return tex, nil +} + +func (d *Device) CreateRenderTargetView(res *Resource) (*RenderTargetView, + error) { + var target *RenderTargetView + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateRenderTargetView, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(res)), + 0, // pDesc + uintptr(unsafe.Pointer(&target)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateRenderTargetView", + Code: uint32(r)} + } + return target, nil +} + +func (d *Device) CreateBlendState(desc *BLEND_DESC) (*BlendState, error) { + var state *BlendState + r, _, _ := syscall.Syscall( + d.Vtbl.CreateBlendState, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&state)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateBlendState", Code: uint32(r)} + } + return state, nil +} + +func (d *Device) CreateDepthStencilState(desc *DEPTH_STENCIL_DESC) (*DepthStencilState, + error) { + var state *DepthStencilState + r, _, _ := syscall.Syscall( + d.Vtbl.CreateDepthStencilState, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&state)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateDepthStencilState", + Code: uint32(r)} + } + return state, nil +} + +func (d *Device) GetFeatureLevel() int { + lvl, _, _ := syscall.Syscall( + d.Vtbl.GetFeatureLevel, + 1, + uintptr(unsafe.Pointer(d)), + 0, 0, + ) + return int(lvl) +} + +func (d *Device) GetImmediateContext() *DeviceContext { + var ctx *DeviceContext + syscall.Syscall( + d.Vtbl.GetImmediateContext, + 2, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(&ctx)), + 0, + ) + return ctx +} + +func (s *IDXGISwapChain) GetDesc() (DXGI_SWAP_CHAIN_DESC, error) { + var desc DXGI_SWAP_CHAIN_DESC + r, _, _ := syscall.Syscall( + s.Vtbl.GetDesc, + 2, + uintptr(unsafe.Pointer(s)), + uintptr(unsafe.Pointer(&desc)), + 0, + ) + if r != 0 { + return DXGI_SWAP_CHAIN_DESC{}, ErrorCode{Name: "IDXGISwapChainGetDesc", + Code: uint32(r)} + } + return desc, nil +} + +func (s *IDXGISwapChain) ResizeBuffers(buffers, width, height, newFormat, flags uint32) error { + r, _, _ := syscall.Syscall6( + s.Vtbl.ResizeBuffers, + 6, + uintptr(unsafe.Pointer(s)), + uintptr(buffers), + uintptr(width), + uintptr(height), + uintptr(newFormat), + uintptr(flags), + ) + if r != 0 { + return ErrorCode{Name: "IDXGISwapChainResizeBuffers", Code: uint32(r)} + } + return nil +} + +func (s *IDXGISwapChain) Present(SyncInterval int, Flags uint32) error { + r, _, _ := syscall.Syscall( + s.Vtbl.Present, + 3, + uintptr(unsafe.Pointer(s)), + uintptr(SyncInterval), + uintptr(Flags), + ) + if r != 0 { + return ErrorCode{Name: "IDXGISwapChainPresent", Code: uint32(r)} + } + return nil +} + +func (s *IDXGISwapChain) GetBuffer(index int, riid *GUID) (*IUnknown, error) { + var buf *IUnknown + r, _, _ := syscall.Syscall6( + s.Vtbl.GetBuffer, + 4, + uintptr(unsafe.Pointer(s)), + uintptr(index), + uintptr(unsafe.Pointer(riid)), + uintptr(unsafe.Pointer(&buf)), + 0, + 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "IDXGISwapChainGetBuffer", Code: uint32(r)} + } + return buf, nil +} + +func (c *DeviceContext) Unmap(resource *Resource, subResource uint32) { + syscall.Syscall( + c.Vtbl.Unmap, + 3, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(resource)), + uintptr(subResource), + ) +} + +func (c *DeviceContext) Map(resource *Resource, + subResource, mapType, mapFlags uint32) (MAPPED_SUBRESOURCE, error) { + var resMap MAPPED_SUBRESOURCE + r, _, _ := syscall.Syscall6( + c.Vtbl.Map, + 6, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(resource)), + uintptr(subResource), + uintptr(mapType), + uintptr(mapFlags), + uintptr(unsafe.Pointer(&resMap)), + ) + if r != 0 { + return resMap, ErrorCode{Name: "DeviceContextMap", Code: uint32(r)} + } + return resMap, nil +} + +func (c *DeviceContext) CopySubresourceRegion(dst *Resource, + dstSubresource, dstX, dstY, dstZ uint32, src *Resource, + srcSubresource uint32, srcBox *BOX) { + syscall.Syscall9( + c.Vtbl.CopySubresourceRegion, + 9, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(dst)), + uintptr(dstSubresource), + uintptr(dstX), + uintptr(dstY), + uintptr(dstZ), + uintptr(unsafe.Pointer(src)), + uintptr(srcSubresource), + uintptr(unsafe.Pointer(srcBox)), + ) +} + +func (c *DeviceContext) ClearDepthStencilView(target *DepthStencilView, + flags uint32, depth float32, stencil uint8) { + syscall.Syscall6( + c.Vtbl.ClearDepthStencilView, + 5, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(target)), + uintptr(flags), + uintptr(math.Float32bits(depth)), + uintptr(stencil), + 0, + ) +} + +func (c *DeviceContext) ClearRenderTargetView(target *RenderTargetView, + color *[4]float32) { + syscall.Syscall( + c.Vtbl.ClearRenderTargetView, + 3, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(target)), + uintptr(unsafe.Pointer(color)), + ) +} + +func (c *DeviceContext) RSSetViewports(viewport *VIEWPORT) { + syscall.Syscall( + c.Vtbl.RSSetViewports, + 3, + uintptr(unsafe.Pointer(c)), + 1, // NumViewports + uintptr(unsafe.Pointer(viewport)), + ) +} + +func (c *DeviceContext) VSSetShader(s *VertexShader) { + syscall.Syscall6( + c.Vtbl.VSSetShader, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(s)), + 0, // ppClassInstances + 0, // NumClassInstances + 0, 0, + ) +} + +func (c *DeviceContext) VSSetConstantBuffers(b *Buffer) { + syscall.Syscall6( + c.Vtbl.VSSetConstantBuffers, + 4, + uintptr(unsafe.Pointer(c)), + 0, // StartSlot + 1, // NumBuffers + uintptr(unsafe.Pointer(&b)), + 0, 0, + ) +} + +func (c *DeviceContext) PSSetConstantBuffers(b *Buffer) { + syscall.Syscall6( + c.Vtbl.PSSetConstantBuffers, + 4, + uintptr(unsafe.Pointer(c)), + 0, // StartSlot + 1, // NumBuffers + uintptr(unsafe.Pointer(&b)), + 0, 0, + ) +} + +func (c *DeviceContext) PSSetShaderResources(startSlot uint32, + s *ShaderResourceView) { + syscall.Syscall6( + c.Vtbl.PSSetShaderResources, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(startSlot), + 1, // NumViews + uintptr(unsafe.Pointer(&s)), + 0, 0, + ) +} + +func (c *DeviceContext) PSSetSamplers(startSlot uint32, s *SamplerState) { + syscall.Syscall6( + c.Vtbl.PSSetSamplers, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(startSlot), + 1, // NumSamplers + uintptr(unsafe.Pointer(&s)), + 0, 0, + ) +} + +func (c *DeviceContext) PSSetShader(s *PixelShader) { + syscall.Syscall6( + c.Vtbl.PSSetShader, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(s)), + 0, // ppClassInstances + 0, // NumClassInstances + 0, 0, + ) +} + +func (c *DeviceContext) UpdateSubresource(res *Resource, dstBox *BOX, + rowPitch, depthPitch uint32, data []byte) { + syscall.Syscall9( + c.Vtbl.UpdateSubresource, + 7, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(res)), + 0, // DstSubresource + uintptr(unsafe.Pointer(dstBox)), + uintptr(unsafe.Pointer(&data[0])), + uintptr(rowPitch), + uintptr(depthPitch), + 0, 0, + ) +} + +func (c *DeviceContext) RSSetState(state *RasterizerState) { + syscall.Syscall( + c.Vtbl.RSSetState, + 2, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(state)), + 0, + ) +} + +func (c *DeviceContext) IASetInputLayout(layout *InputLayout) { + syscall.Syscall( + c.Vtbl.IASetInputLayout, + 2, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(layout)), + 0, + ) +} + +func (c *DeviceContext) IASetIndexBuffer(buf *Buffer, format, offset uint32) { + syscall.Syscall6( + c.Vtbl.IASetIndexBuffer, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(buf)), + uintptr(format), + uintptr(offset), + 0, 0, + ) +} + +func (c *DeviceContext) IASetVertexBuffers(buf *Buffer, stride, offset uint32) { + syscall.Syscall6( + c.Vtbl.IASetVertexBuffers, + 6, + uintptr(unsafe.Pointer(c)), + 0, // StartSlot + 1, // NumBuffers, + uintptr(unsafe.Pointer(&buf)), + uintptr(unsafe.Pointer(&stride)), + uintptr(unsafe.Pointer(&offset)), + ) +} + +func (c *DeviceContext) IASetPrimitiveTopology(mode uint32) { + syscall.Syscall( + c.Vtbl.IASetPrimitiveTopology, + 2, + uintptr(unsafe.Pointer(c)), + uintptr(mode), + 0, + ) +} + +func (c *DeviceContext) OMGetRenderTargets() (*RenderTargetView, + *DepthStencilView) { + var ( + target *RenderTargetView + depthStencilView *DepthStencilView + ) + syscall.Syscall6( + c.Vtbl.OMGetRenderTargets, + 4, + uintptr(unsafe.Pointer(c)), + 1, // NumViews + uintptr(unsafe.Pointer(&target)), + uintptr(unsafe.Pointer(&depthStencilView)), + 0, 0, + ) + return target, depthStencilView +} + +func (c *DeviceContext) OMSetRenderTargets(target *RenderTargetView, + depthStencil *DepthStencilView) { + syscall.Syscall6( + c.Vtbl.OMSetRenderTargets, + 4, + uintptr(unsafe.Pointer(c)), + 1, // NumViews + uintptr(unsafe.Pointer(&target)), + uintptr(unsafe.Pointer(depthStencil)), + 0, 0, + ) +} + +func (c *DeviceContext) Draw(count, start uint32) { + syscall.Syscall( + c.Vtbl.Draw, + 3, + uintptr(unsafe.Pointer(c)), + uintptr(count), + uintptr(start), + ) +} + +func (c *DeviceContext) DrawIndexed(count, start uint32, base int32) { + syscall.Syscall6( + c.Vtbl.DrawIndexed, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(count), + uintptr(start), + uintptr(base), + 0, 0, + ) +} + +func (c *DeviceContext) OMSetBlendState(state *BlendState, + factor *f32color.RGBA, sampleMask uint32) { + syscall.Syscall6( + c.Vtbl.OMSetBlendState, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(state)), + uintptr(unsafe.Pointer(factor)), + uintptr(sampleMask), + 0, 0, + ) +} + +func (c *DeviceContext) OMSetDepthStencilState(state *DepthStencilState, + stencilRef uint32) { + syscall.Syscall( + c.Vtbl.OMSetDepthStencilState, + 3, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(state)), + uintptr(stencilRef), + ) +} + +func (d *IDXGIObject) GetParent(guid *GUID) (*IDXGIObject, error) { + var parent *IDXGIObject + r, _, _ := syscall.Syscall( + d.Vtbl.GetParent, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(guid)), + uintptr(unsafe.Pointer(&parent)), + ) + if r != 0 { + return nil, ErrorCode{Name: "IDXGIObjectGetParent", Code: uint32(r)} + } + return parent, nil +} + +func (d *IDXGIFactory) CreateSwapChain(device *IUnknown, + desc *DXGI_SWAP_CHAIN_DESC) (*IDXGISwapChain, error) { + var swchain *IDXGISwapChain + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateSwapChain, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(device)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&swchain)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "IDXGIFactory", Code: uint32(r)} + } + return swchain, nil +} + +func (d *IDXGIDevice) GetAdapter() (*IDXGIAdapter, error) { + var adapter *IDXGIAdapter + r, _, _ := syscall.Syscall( + d.Vtbl.GetAdapter, + 2, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(&adapter)), + 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "IDXGIDeviceGetAdapter", Code: uint32(r)} + } + return adapter, nil +} + +func IUnknownQueryInterface(obj unsafe.Pointer, queryInterfaceMethod uintptr, + guid *GUID) (*IUnknown, error) { + var ref *IUnknown + r, _, _ := syscall.Syscall( + queryInterfaceMethod, + 3, + uintptr(obj), + uintptr(unsafe.Pointer(guid)), + uintptr(unsafe.Pointer(&ref)), + ) + if r != 0 { + return nil, ErrorCode{Name: "IUnknownQueryInterface", Code: uint32(r)} + } + return ref, nil +} + +func IUnknownRelease(obj unsafe.Pointer, releaseMethod uintptr) { + syscall.Syscall( + releaseMethod, + 1, + uintptr(obj), + 0, + 0, + ) +} + +func (e ErrorCode) Error() string { + return fmt.Sprintf("%s: %#x", e.Name, e.Code) +} + +func CreateSwapChain(dev *Device, hwnd windows.Handle) (*IDXGISwapChain, + error) { + dxgiDev, err := IUnknownQueryInterface(unsafe.Pointer(dev), + dev.Vtbl.QueryInterface, &IID_IDXGIDevice) + if err != nil { + return nil, fmt.Errorf("NewContext: %v", err) + } + adapter, err := (*IDXGIDevice)(unsafe.Pointer(dxgiDev)).GetAdapter() + IUnknownRelease(unsafe.Pointer(dxgiDev), dxgiDev.Vtbl.Release) + if err != nil { + return nil, fmt.Errorf("NewContext: %v", err) + } + dxgiFactory, err := (*IDXGIObject)(unsafe.Pointer(adapter)).GetParent(&IID_IDXGIFactory) + IUnknownRelease(unsafe.Pointer(adapter), adapter.Vtbl.Release) + if err != nil { + return nil, fmt.Errorf("NewContext: %v", err) + } + swchain, err := (*IDXGIFactory)(unsafe.Pointer(dxgiFactory)).CreateSwapChain( + (*IUnknown)(unsafe.Pointer(dev)), + &DXGI_SWAP_CHAIN_DESC{ + BufferDesc: DXGI_MODE_DESC{ + Format: DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, + }, + SampleDesc: DXGI_SAMPLE_DESC{ + Count: 1, + }, + BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT, + BufferCount: 1, + OutputWindow: hwnd, + Windowed: 1, + SwapEffect: DXGI_SWAP_EFFECT_DISCARD, + }, + ) + IUnknownRelease(unsafe.Pointer(dxgiFactory), dxgiFactory.Vtbl.Release) + if err != nil { + return nil, fmt.Errorf("NewContext: %v", err) + } + return swchain, nil +} + +func CreateDepthView(d *Device, + width, height, depthBits int) (*DepthStencilView, error) { + depthTex, err := d.CreateTexture2D(&TEXTURE2D_DESC{ + Width: uint32(width), + Height: uint32(height), + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_D24_UNORM_S8_UINT, + SampleDesc: DXGI_SAMPLE_DESC{ + Count: 1, + Quality: 0, + }, + BindFlags: BIND_DEPTH_STENCIL, + }) + if err != nil { + return nil, err + } + depthView, err := d.CreateDepthStencilViewTEX2D( + (*Resource)(unsafe.Pointer(depthTex)), + &DEPTH_STENCIL_VIEW_DESC_TEX2D{ + Format: DXGI_FORMAT_D24_UNORM_S8_UINT, + ViewDimension: DSV_DIMENSION_TEXTURE2D, + }, + ) + IUnknownRelease(unsafe.Pointer(depthTex), depthTex.Vtbl.Release) + return depthView, err +} diff --git a/gio/giold/internal/egl/egl.go b/gio/giold/internal/egl/egl.go new file mode 100644 index 0000000..5a23650 --- /dev/null +++ b/gio/giold/internal/egl/egl.go @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build linux || windows || freebsd || openbsd +// +build linux windows freebsd openbsd + +package egl + +import ( + "errors" + "fmt" + "runtime" + "strings" + + "realy.lol/gio/gpu" + "realy.lol/gio/internal/gl" + "realy.lol/gio/internal/srgb" +) + +type Context struct { + c *gl.Functions + disp _EGLDisplay + eglCtx *eglContext + eglSurf _EGLSurface + width, height int + refreshFBO bool + // For sRGB emulation. + srgbFBO *srgb.FBO +} + +type eglContext struct { + config _EGLConfig + ctx _EGLContext + visualID int + srgb bool + surfaceless bool +} + +var ( + nilEGLDisplay _EGLDisplay + nilEGLSurface _EGLSurface + nilEGLContext _EGLContext + nilEGLConfig _EGLConfig + EGL_DEFAULT_DISPLAY NativeDisplayType +) + +const ( + _EGL_ALPHA_SIZE = 0x3021 + _EGL_BLUE_SIZE = 0x3022 + _EGL_CONFIG_CAVEAT = 0x3027 + _EGL_CONTEXT_CLIENT_VERSION = 0x3098 + _EGL_DEPTH_SIZE = 0x3025 + _EGL_GL_COLORSPACE_KHR = 0x309d + _EGL_GL_COLORSPACE_SRGB_KHR = 0x3089 + _EGL_GREEN_SIZE = 0x3023 + _EGL_EXTENSIONS = 0x3055 + _EGL_NATIVE_VISUAL_ID = 0x302e + _EGL_NONE = 0x3038 + _EGL_OPENGL_ES2_BIT = 0x4 + _EGL_RED_SIZE = 0x3024 + _EGL_RENDERABLE_TYPE = 0x3040 + _EGL_SURFACE_TYPE = 0x3033 + _EGL_WINDOW_BIT = 0x4 +) + +func (c *Context) Release() { + if c.srgbFBO != nil { + c.srgbFBO.Release() + c.srgbFBO = nil + } + c.ReleaseSurface() + if c.eglCtx != nil { + eglDestroyContext(c.disp, c.eglCtx.ctx) + c.eglCtx = nil + } + c.disp = nilEGLDisplay +} + +func (c *Context) Present() error { + if c.srgbFBO != nil { + c.srgbFBO.Blit() + } + if !eglSwapBuffers(c.disp, c.eglSurf) { + return fmt.Errorf("eglSwapBuffers failed (%x)", eglGetError()) + } + if c.srgbFBO != nil { + c.srgbFBO.AfterPresent() + } + return nil +} + +func NewContext(disp NativeDisplayType) (*Context, error) { + if err := loadEGL(); err != nil { + return nil, err + } + eglDisp := eglGetDisplay(disp) + // eglGetDisplay can return EGL_NO_DISPLAY yet no error + // (EGL_SUCCESS), in which case a default EGL display might be + // available. + if eglDisp == nilEGLDisplay { + eglDisp = eglGetDisplay(EGL_DEFAULT_DISPLAY) + } + if eglDisp == nilEGLDisplay { + return nil, fmt.Errorf("eglGetDisplay failed: 0x%x", eglGetError()) + } + eglCtx, err := createContext(eglDisp) + if err != nil { + return nil, err + } + f, err := gl.NewFunctions(nil) + if err != nil { + return nil, err + } + c := &Context{ + disp: eglDisp, + eglCtx: eglCtx, + c: f, + } + return c, nil +} + +func (c *Context) API() gpu.API { + return gpu.OpenGL{} +} + +func (c *Context) ReleaseSurface() { + if c.eglSurf == nilEGLSurface { + return + } + // Make sure any in-flight GL commands are complete. + c.c.Finish() + c.ReleaseCurrent() + eglDestroySurface(c.disp, c.eglSurf) + c.eglSurf = nilEGLSurface +} + +func (c *Context) VisualID() int { + return c.eglCtx.visualID +} + +func (c *Context) CreateSurface(win NativeWindowType, width, height int) error { + eglSurf, err := createSurface(c.disp, c.eglCtx, win) + c.eglSurf = eglSurf + c.width = width + c.height = height + c.refreshFBO = true + return err +} + +func (c *Context) ReleaseCurrent() { + if c.disp != nilEGLDisplay { + eglMakeCurrent(c.disp, nilEGLSurface, nilEGLSurface, nilEGLContext) + } +} + +func (c *Context) MakeCurrent() error { + if c.eglSurf == nilEGLSurface && !c.eglCtx.surfaceless { + return errors.New("no surface created yet EGL_KHR_surfaceless_context is not supported") + } + if !eglMakeCurrent(c.disp, c.eglSurf, c.eglSurf, c.eglCtx.ctx) { + return fmt.Errorf("eglMakeCurrent error 0x%x", eglGetError()) + } + if c.eglCtx.srgb || c.eglSurf == nilEGLSurface { + return nil + } + if c.srgbFBO == nil { + var err error + c.srgbFBO, err = srgb.New(nil) + if err != nil { + c.ReleaseCurrent() + return err + } + } + if c.refreshFBO { + c.refreshFBO = false + if err := c.srgbFBO.Refresh(c.width, c.height); err != nil { + c.ReleaseCurrent() + return err + } + } + return nil +} + +func (c *Context) EnableVSync(enable bool) { + if enable { + eglSwapInterval(c.disp, 1) + } else { + eglSwapInterval(c.disp, 0) + } +} + +func hasExtension(exts []string, ext string) bool { + for _, e := range exts { + if ext == e { + return true + } + } + return false +} + +func createContext(disp _EGLDisplay) (*eglContext, error) { + major, minor, ret := eglInitialize(disp) + if !ret { + return nil, fmt.Errorf("eglInitialize failed: 0x%x", eglGetError()) + } + // sRGB framebuffer support on EGL 1.5 or if EGL_KHR_gl_colorspace is supported. + exts := strings.Split(eglQueryString(disp, _EGL_EXTENSIONS), " ") + srgb := major > 1 || minor >= 5 || hasExtension(exts, + "EGL_KHR_gl_colorspace") + attribs := []_EGLint{ + _EGL_RENDERABLE_TYPE, _EGL_OPENGL_ES2_BIT, + _EGL_SURFACE_TYPE, _EGL_WINDOW_BIT, + _EGL_BLUE_SIZE, 8, + _EGL_GREEN_SIZE, 8, + _EGL_RED_SIZE, 8, + _EGL_CONFIG_CAVEAT, _EGL_NONE, + } + if srgb { + if runtime.GOOS == "linux" || runtime.GOOS == "android" { + // Some Mesa drivers crash if an sRGB framebuffer is requested without alpha. + // https://bugs.freedesktop.org/show_bug.cgi?id=107782. + // + // Also, some Android devices (Samsung S9) needs alpha for sRGB to work. + attribs = append(attribs, _EGL_ALPHA_SIZE, 8) + } + // Only request a depth buffer if we're going to render directly to the framebuffer. + attribs = append(attribs, _EGL_DEPTH_SIZE, 16) + } + attribs = append(attribs, _EGL_NONE) + eglCfg, ret := eglChooseConfig(disp, attribs) + if !ret { + return nil, fmt.Errorf("eglChooseConfig failed: 0x%x", eglGetError()) + } + if eglCfg == nilEGLConfig { + supportsNoCfg := hasExtension(exts, "EGL_KHR_no_config_context") + if !supportsNoCfg { + return nil, errors.New("eglChooseConfig returned no configs") + } + } + var visID _EGLint + if eglCfg != nilEGLConfig { + var ok bool + visID, ok = eglGetConfigAttrib(disp, eglCfg, _EGL_NATIVE_VISUAL_ID) + if !ok { + return nil, errors.New("newContext: eglGetConfigAttrib for _EGL_NATIVE_VISUAL_ID failed") + } + } + ctxAttribs := []_EGLint{ + _EGL_CONTEXT_CLIENT_VERSION, 3, + _EGL_NONE, + } + eglCtx := eglCreateContext(disp, eglCfg, nilEGLContext, ctxAttribs) + if eglCtx == nilEGLContext { + // Fall back to OpenGL ES 2 and rely on extensions. + ctxAttribs := []_EGLint{ + _EGL_CONTEXT_CLIENT_VERSION, 2, + _EGL_NONE, + } + eglCtx = eglCreateContext(disp, eglCfg, nilEGLContext, ctxAttribs) + if eglCtx == nilEGLContext { + return nil, fmt.Errorf("eglCreateContext failed: 0x%x", + eglGetError()) + } + } + return &eglContext{ + config: _EGLConfig(eglCfg), + ctx: _EGLContext(eglCtx), + visualID: int(visID), + srgb: srgb, + surfaceless: hasExtension(exts, "EGL_KHR_surfaceless_context"), + }, nil +} + +func createSurface(disp _EGLDisplay, eglCtx *eglContext, + win NativeWindowType) (_EGLSurface, error) { + var surfAttribs []_EGLint + if eglCtx.srgb { + surfAttribs = append(surfAttribs, _EGL_GL_COLORSPACE_KHR, + _EGL_GL_COLORSPACE_SRGB_KHR) + } + surfAttribs = append(surfAttribs, _EGL_NONE) + eglSurf := eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs) + if eglSurf == nilEGLSurface && eglCtx.srgb { + // Try again without sRGB + eglCtx.srgb = false + surfAttribs = []_EGLint{_EGL_NONE} + eglSurf = eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs) + } + if eglSurf == nilEGLSurface { + return nilEGLSurface, fmt.Errorf("newContext: eglCreateWindowSurface failed 0x%x (sRGB=%v)", + eglGetError(), eglCtx.srgb) + } + return eglSurf, nil +} diff --git a/gio/giold/internal/egl/egl_unix.go b/gio/giold/internal/egl/egl_unix.go new file mode 100644 index 0000000..059dd55 --- /dev/null +++ b/gio/giold/internal/egl/egl_unix.go @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build linux freebsd openbsd + +package egl + +/* +#cgo linux,!android pkg-config: egl +#cgo freebsd openbsd android LDFLAGS: -lEGL +#cgo freebsd CFLAGS: -I/usr/local/include +#cgo freebsd LDFLAGS: -L/usr/local/lib +#cgo openbsd CFLAGS: -I/usr/X11R6/include +#cgo openbsd LDFLAGS: -L/usr/X11R6/lib +#cgo CFLAGS: -DEGL_NO_X11 + +#include +#include +*/ +import "C" + +type ( + _EGLint = C.EGLint + _EGLDisplay = C.EGLDisplay + _EGLConfig = C.EGLConfig + _EGLContext = C.EGLContext + _EGLSurface = C.EGLSurface + NativeDisplayType = C.EGLNativeDisplayType + NativeWindowType = C.EGLNativeWindowType +) + +func loadEGL() error { + return nil +} + +func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) { + var cfg C.EGLConfig + var ncfg C.EGLint + if C.eglChooseConfig(disp, &attribs[0], &cfg, 1, &ncfg) != C.EGL_TRUE { + return nilEGLConfig, false + } + return _EGLConfig(cfg), true +} + +func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext, attribs []_EGLint) _EGLContext { + ctx := C.eglCreateContext(disp, cfg, shareCtx, &attribs[0]) + return _EGLContext(ctx) +} + +func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool { + return C.eglDestroySurface(disp, surf) == C.EGL_TRUE +} + +func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool { + return C.eglDestroyContext(disp, ctx) == C.EGL_TRUE +} + +func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig, attr _EGLint) (_EGLint, bool) { + var val _EGLint + ret := C.eglGetConfigAttrib(disp, cfg, attr, &val) + return val, ret == C.EGL_TRUE +} + +func eglGetError() _EGLint { + return C.eglGetError() +} + +func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) { + var maj, min _EGLint + ret := C.eglInitialize(disp, &maj, &min) + return maj, min, ret == C.EGL_TRUE +} + +func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface, ctx _EGLContext) bool { + return C.eglMakeCurrent(disp, draw, read, ctx) == C.EGL_TRUE +} + +func eglReleaseThread() bool { + return C.eglReleaseThread() == C.EGL_TRUE +} + +func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool { + return C.eglSwapBuffers(disp, surf) == C.EGL_TRUE +} + +func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool { + return C.eglSwapInterval(disp, interval) == C.EGL_TRUE +} + +func eglTerminate(disp _EGLDisplay) bool { + return C.eglTerminate(disp) == C.EGL_TRUE +} + +func eglQueryString(disp _EGLDisplay, name _EGLint) string { + return C.GoString(C.eglQueryString(disp, name)) +} + +func eglGetDisplay(disp NativeDisplayType) _EGLDisplay { + return C.eglGetDisplay(disp) +} + +func eglCreateWindowSurface(disp _EGLDisplay, conf _EGLConfig, win NativeWindowType, attribs []_EGLint) _EGLSurface { + eglSurf := C.eglCreateWindowSurface(disp, conf, win, &attribs[0]) + return eglSurf +} diff --git a/gio/giold/internal/egl/egl_windows.go b/gio/giold/internal/egl/egl_windows.go new file mode 100644 index 0000000..5df5c65 --- /dev/null +++ b/gio/giold/internal/egl/egl_windows.go @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package egl + +import ( + "fmt" + "runtime" + "sync" + "unsafe" + + syscall "golang.org/x/sys/windows" + + "realy.lol/gio/internal/gl" +) + +type ( + _EGLint int32 + _EGLDisplay uintptr + _EGLConfig uintptr + _EGLContext uintptr + _EGLSurface uintptr + NativeDisplayType uintptr + NativeWindowType uintptr +) + +var ( + libEGL = syscall.NewLazyDLL("libEGL.dll") + _eglChooseConfig = libEGL.NewProc("eglChooseConfig") + _eglCreateContext = libEGL.NewProc("eglCreateContext") + _eglCreateWindowSurface = libEGL.NewProc("eglCreateWindowSurface") + _eglDestroyContext = libEGL.NewProc("eglDestroyContext") + _eglDestroySurface = libEGL.NewProc("eglDestroySurface") + _eglGetConfigAttrib = libEGL.NewProc("eglGetConfigAttrib") + _eglGetDisplay = libEGL.NewProc("eglGetDisplay") + _eglGetError = libEGL.NewProc("eglGetError") + _eglInitialize = libEGL.NewProc("eglInitialize") + _eglMakeCurrent = libEGL.NewProc("eglMakeCurrent") + _eglReleaseThread = libEGL.NewProc("eglReleaseThread") + _eglSwapInterval = libEGL.NewProc("eglSwapInterval") + _eglSwapBuffers = libEGL.NewProc("eglSwapBuffers") + _eglTerminate = libEGL.NewProc("eglTerminate") + _eglQueryString = libEGL.NewProc("eglQueryString") +) + +var loadOnce sync.Once + +func loadEGL() error { + var err error + loadOnce.Do(func() { + err = loadDLLs() + }) + return err +} + +func loadDLLs() error { + if err := loadDLL(libEGL, "libEGL.dll"); err != nil { + return err + } + if err := loadDLL(gl.LibGLESv2, "libGLESv2.dll"); err != nil { + return err + } + // d3dcompiler_47.dll is needed internally for shader compilation to function. + return loadDLL(syscall.NewLazyDLL("d3dcompiler_47.dll"), + "d3dcompiler_47.dll") +} + +func loadDLL(dll *syscall.LazyDLL, name string) error { + err := dll.Load() + if err != nil { + return fmt.Errorf("egl: failed to load %s: %v", name, err) + } + return nil +} + +func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) { + var cfg _EGLConfig + var ncfg _EGLint + a := &attribs[0] + r, _, _ := _eglChooseConfig.Call(uintptr(disp), uintptr(unsafe.Pointer(a)), + uintptr(unsafe.Pointer(&cfg)), 1, uintptr(unsafe.Pointer(&ncfg))) + issue34474KeepAlive(a) + return cfg, r != 0 +} + +func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext, + attribs []_EGLint) _EGLContext { + a := &attribs[0] + c, _, _ := _eglCreateContext.Call(uintptr(disp), uintptr(cfg), + uintptr(shareCtx), uintptr(unsafe.Pointer(a))) + issue34474KeepAlive(a) + return _EGLContext(c) +} + +func eglCreateWindowSurface(disp _EGLDisplay, cfg _EGLConfig, + win NativeWindowType, attribs []_EGLint) _EGLSurface { + a := &attribs[0] + s, _, _ := _eglCreateWindowSurface.Call(uintptr(disp), uintptr(cfg), + uintptr(win), uintptr(unsafe.Pointer(a))) + issue34474KeepAlive(a) + return _EGLSurface(s) +} + +func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool { + r, _, _ := _eglDestroySurface.Call(uintptr(disp), uintptr(surf)) + return r != 0 +} + +func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool { + r, _, _ := _eglDestroyContext.Call(uintptr(disp), uintptr(ctx)) + return r != 0 +} + +func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig, + attr _EGLint) (_EGLint, bool) { + var val uintptr + r, _, _ := _eglGetConfigAttrib.Call(uintptr(disp), uintptr(cfg), + uintptr(attr), uintptr(unsafe.Pointer(&val))) + return _EGLint(val), r != 0 +} + +func eglGetDisplay(disp NativeDisplayType) _EGLDisplay { + d, _, _ := _eglGetDisplay.Call(uintptr(disp)) + return _EGLDisplay(d) +} + +func eglGetError() _EGLint { + e, _, _ := _eglGetError.Call() + return _EGLint(e) +} + +func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) { + var maj, min uintptr + r, _, _ := _eglInitialize.Call(uintptr(disp), uintptr(unsafe.Pointer(&maj)), + uintptr(unsafe.Pointer(&min))) + return _EGLint(maj), _EGLint(min), r != 0 +} + +func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface, + ctx _EGLContext) bool { + r, _, _ := _eglMakeCurrent.Call(uintptr(disp), uintptr(draw), uintptr(read), + uintptr(ctx)) + return r != 0 +} + +func eglReleaseThread() bool { + r, _, _ := _eglReleaseThread.Call() + return r != 0 +} + +func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool { + r, _, _ := _eglSwapInterval.Call(uintptr(disp), uintptr(interval)) + return r != 0 +} + +func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool { + r, _, _ := _eglSwapBuffers.Call(uintptr(disp), uintptr(surf)) + return r != 0 +} + +func eglTerminate(disp _EGLDisplay) bool { + r, _, _ := _eglTerminate.Call(uintptr(disp)) + return r != 0 +} + +func eglQueryString(disp _EGLDisplay, name _EGLint) string { + r, _, _ := _eglQueryString.Call(uintptr(disp), uintptr(name)) + return syscall.BytePtrToString((*byte)(unsafe.Pointer(r))) +} + +// issue34474KeepAlive calls runtime.KeepAlive as a +// workaround for golang.org/issue/34474. +func issue34474KeepAlive(v interface{}) { + runtime.KeepAlive(v) +} diff --git a/gio/giold/internal/f32color/rgba.go b/gio/giold/internal/f32color/rgba.go new file mode 100644 index 0000000..eecf018 --- /dev/null +++ b/gio/giold/internal/f32color/rgba.go @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32color + +import ( + "image/color" + "math" +) + +// RGBA is a 32 bit floating point linear premultiplied color space. +type RGBA struct { + R, G, B, A float32 +} + +// Array returns rgba values in a [4]float32 array. +func (rgba RGBA) Array() [4]float32 { + return [4]float32{rgba.R, rgba.G, rgba.B, rgba.A} +} + +// Float32 returns r, g, b, a values. +func (col RGBA) Float32() (r, g, b, a float32) { + return col.R, col.G, col.B, col.A +} + +// SRGBA converts from linear to sRGB color space. +func (col RGBA) SRGB() color.NRGBA { + if col.A == 0 { + return color.NRGBA{} + } + return color.NRGBA{ + R: uint8(linearTosRGB(col.R/col.A)*255 + .5), + G: uint8(linearTosRGB(col.G/col.A)*255 + .5), + B: uint8(linearTosRGB(col.B/col.A)*255 + .5), + A: uint8(col.A*255 + .5), + } +} + +// Luminance calculates the relative luminance of a linear RGBA color. +// Normalized to 0 for black and 1 for white. +// +// See https://www.w3.org/TR/WCAG20/#relativeluminancedef for more details +func (col RGBA) Luminance() float32 { + return 0.2126*col.R + 0.7152*col.G + 0.0722*col.B +} + +// Opaque returns the color without alpha component. +func (col RGBA) Opaque() RGBA { + col.A = 1.0 + return col +} + +// LinearFromSRGB converts from col in the sRGB colorspace to RGBA. +func LinearFromSRGB(col color.NRGBA) RGBA { + af := float32(col.A) / 0xFF + return RGBA{ + R: sRGBToLinear(float32(col.R)/0xff) * af, + G: sRGBToLinear(float32(col.G)/0xff) * af, + B: sRGBToLinear(float32(col.B)/0xff) * af, + A: af, + } +} + +// NRGBAToRGBA converts from non-premultiplied sRGB color to premultiplied sRGB color. +// +// Each component in the result is `sRGBToLinear(c * alpha)`, where `c` +// is the linear color. +func NRGBAToRGBA(col color.NRGBA) color.RGBA { + if col.A == 0xFF { + return color.RGBA(col) + } + c := LinearFromSRGB(col) + return color.RGBA{ + R: uint8(linearTosRGB(c.R)*255 + .5), + G: uint8(linearTosRGB(c.G)*255 + .5), + B: uint8(linearTosRGB(c.B)*255 + .5), + A: col.A, + } +} + +// NRGBAToLinearRGBA converts from non-premultiplied sRGB color to premultiplied linear RGBA color. +// +// Each component in the result is `c * alpha`, where `c` is the linear color. +func NRGBAToLinearRGBA(col color.NRGBA) color.RGBA { + if col.A == 0xFF { + return color.RGBA(col) + } + c := LinearFromSRGB(col) + return color.RGBA{ + R: uint8(c.R*255 + .5), + G: uint8(c.G*255 + .5), + B: uint8(c.B*255 + .5), + A: col.A, + } +} + +// RGBAToNRGBA converts from premultiplied sRGB color to non-premultiplied sRGB color. +func RGBAToNRGBA(col color.RGBA) color.NRGBA { + if col.A == 0xFF { + return color.NRGBA(col) + } + + linear := RGBA{ + R: sRGBToLinear(float32(col.R) / 0xff), + G: sRGBToLinear(float32(col.G) / 0xff), + B: sRGBToLinear(float32(col.B) / 0xff), + A: float32(col.A) / 0xff, + } + + return linear.SRGB() +} + +// linearTosRGB transforms color value from linear to sRGB. +func linearTosRGB(c float32) float32 { + // Formula from EXT_sRGB. + switch { + case c <= 0: + return 0 + case 0 < c && c < 0.0031308: + return 12.92 * c + case 0.0031308 <= c && c < 1: + return 1.055*float32(math.Pow(float64(c), 0.41666)) - 0.055 + } + + return 1 +} + +// sRGBToLinear transforms color value from sRGB to linear. +func sRGBToLinear(c float32) float32 { + // Formula from EXT_sRGB. + if c <= 0.04045 { + return c / 12.92 + } else { + return float32(math.Pow(float64((c+0.055)/1.055), 2.4)) + } +} + +// MulAlpha applies the alpha to the color. +func MulAlpha(c color.NRGBA, alpha uint8) color.NRGBA { + c.A = uint8(uint32(c.A) * uint32(alpha) / 0xFF) + return c +} + +// Disabled blends color towards the luminance and multiplies alpha. +// Blending towards luminance will desaturate the color. +// Multiplying alpha blends the color together more with the background. +func Disabled(c color.NRGBA) (d color.NRGBA) { + const r = 80 // blend ratio + lum := approxLuminance(c) + return color.NRGBA{ + R: byte((int(c.R)*r + int(lum)*(256-r)) / 256), + G: byte((int(c.G)*r + int(lum)*(256-r)) / 256), + B: byte((int(c.B)*r + int(lum)*(256-r)) / 256), + A: byte(int(c.A) * (128 + 32) / 256), + } +} + +// Hovered blends color towards a brighter color. +func Hovered(c color.NRGBA) (d color.NRGBA) { + const r = 0x20 // lighten ratio + return color.NRGBA{ + R: byte(255 - int(255-c.R)*(255-r)/256), + G: byte(255 - int(255-c.G)*(255-r)/256), + B: byte(255 - int(255-c.B)*(255-r)/256), + A: c.A, + } +} + +// approxLuminance is a fast approximate version of RGBA.Luminance. +func approxLuminance(c color.NRGBA) byte { + const ( + r = 13933 // 0.2126 * 256 * 256 + g = 46871 // 0.7152 * 256 * 256 + b = 4732 // 0.0722 * 256 * 256 + t = r + g + b + ) + return byte((r*int(c.R) + g*int(c.G) + b*int(c.B)) / t) +} diff --git a/gio/giold/internal/f32color/rgba_test.go b/gio/giold/internal/f32color/rgba_test.go new file mode 100644 index 0000000..ea0f871 --- /dev/null +++ b/gio/giold/internal/f32color/rgba_test.go @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32color + +import ( + "image/color" + "testing" +) + +func TestNRGBAToLinearRGBA_Boundary(t *testing.T) { + for col := 0; col <= 0xFF; col++ { + for alpha := 0; alpha <= 0xFF; alpha++ { + in := color.NRGBA{R: uint8(col), A: uint8(alpha)} + premul := NRGBAToLinearRGBA(in) + if premul.A != uint8(alpha) { + t.Errorf("%v: got %v expected %v", in, premul.A, alpha) + } + if premul.R > premul.A { + t.Errorf("%v: R=%v > A=%v", in, premul.R, premul.A) + } + } + } +} + +func TestLinearToRGBARoundtrip(t *testing.T) { + for col := 0; col <= 0xFF; col++ { + for alpha := 0; alpha <= 0xFF; alpha++ { + want := color.NRGBA{R: uint8(col), A: uint8(alpha)} + if alpha == 0 { + want.R = 0 + } + got := LinearFromSRGB(want).SRGB() + if want != got { + t.Errorf("got %v expected %v", got, want) + } + } + } +} diff --git a/gio/giold/internal/fling/animation.go b/gio/giold/internal/fling/animation.go new file mode 100644 index 0000000..82a2b8e --- /dev/null +++ b/gio/giold/internal/fling/animation.go @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package fling + +import ( + "math" + "runtime" + "time" + + "realy.lol/gio/unit" +) + +type Animation struct { + // Current offset in pixels. + x float32 + // Initial time. + t0 time.Time + // Initial velocity in pixels pr second. + v0 float32 +} + +var ( + // Pixels/second. + minFlingVelocity = unit.Dp(50) + maxFlingVelocity = unit.Dp(8000) +) + +const ( + thresholdVelocity = 1 +) + +// Start a fling given a starting velocity. Returns whether a +// fling was started. +func (f *Animation) Start(c unit.Metric, now time.Time, velocity float32) bool { + min := float32(c.Px(minFlingVelocity)) + v := velocity + if -min <= v && v <= min { + return false + } + max := float32(c.Px(maxFlingVelocity)) + if v > max { + v = max + } else if v < -max { + v = -max + } + f.init(now, v) + return true +} + +func (f *Animation) init(now time.Time, v0 float32) { + f.t0 = now + f.v0 = v0 + f.x = 0 +} + +func (f *Animation) Active() bool { + return f.v0 != 0 +} + +// Tick computes and returns a fling distance since +// the last time Tick was called. +func (f *Animation) Tick(now time.Time) int { + if !f.Active() { + return 0 + } + var k float32 + if runtime.GOOS == "darwin" { + k = -2 // iOS + } else { + k = -4.2 // Android and default + } + t := now.Sub(f.t0) + // The acceleration x''(t) of a point mass with a drag + // force, f, proportional with velocity, x'(t), is + // governed by the equation + // + // x''(t) = kx'(t) + // + // Given the starting position x(0) = 0, the starting + // velocity x'(0) = v0, the position is then + // given by + // + // x(t) = v0*e^(k*t)/k - v0/k + // + ekt := float32(math.Exp(float64(k) * t.Seconds())) + x := f.v0*ekt/k - f.v0/k + dist := x - f.x + idist := int(dist) + f.x += float32(idist) + // Solving for the velocity x'(t) gives us + // + // x'(t) = v0*e^(k*t) + v := f.v0 * ekt + if -thresholdVelocity < v && v < thresholdVelocity { + f.v0 = 0 + } + return idist +} diff --git a/gio/giold/internal/fling/extrapolation.go b/gio/giold/internal/fling/extrapolation.go new file mode 100644 index 0000000..655ef84 --- /dev/null +++ b/gio/giold/internal/fling/extrapolation.go @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package fling + +import ( + "math" + "strconv" + "strings" + "time" +) + +// Extrapolation computes a 1-dimensional velocity estimate +// for a set of timestamped points using the least squares +// fit of a 2nd order polynomial. The same method is used +// by Android. +type Extrapolation struct { + // Index into points. + idx int + // Circular buffer of samples. + samples []sample + lastValue float32 + // Pre-allocated cache for samples. + cache [historySize]sample + + // Filtered values and times + values [historySize]float32 + times [historySize]float32 +} + +type sample struct { + t time.Duration + v float32 +} + +type matrix struct { + rows, cols int + data []float32 +} + +type Estimate struct { + Velocity float32 + Distance float32 +} + +type coefficients [degree + 1]float32 + +const ( + degree = 2 + historySize = 20 + maxAge = 100 * time.Millisecond + maxSampleGap = 40 * time.Millisecond +) + +// SampleDelta adds a relative sample to the estimation. +func (e *Extrapolation) SampleDelta(t time.Duration, delta float32) { + val := delta + e.lastValue + e.Sample(t, val) +} + +// Sample adds an absolute sample to the estimation. +func (e *Extrapolation) Sample(t time.Duration, val float32) { + e.lastValue = val + if e.samples == nil { + e.samples = e.cache[:0] + } + s := sample{ + t: t, + v: val, + } + if e.idx == len(e.samples) && e.idx < cap(e.samples) { + e.samples = append(e.samples, s) + } else { + e.samples[e.idx] = s + } + e.idx++ + if e.idx == cap(e.samples) { + e.idx = 0 + } +} + +// Velocity returns an estimate of the implied velocity and +// distance for the points sampled, or zero if the estimation method +// failed. +func (e *Extrapolation) Estimate() Estimate { + if len(e.samples) == 0 { + return Estimate{} + } + values := e.values[:0] + times := e.times[:0] + first := e.get(0) + t := first.t + // Walk backwards collecting samples. + for i := 0; i < len(e.samples); i++ { + p := e.get(-i) + age := first.t - p.t + if age >= maxAge || t-p.t >= maxSampleGap { + // If the samples are too old or + // too much time passed between samples + // assume they're not part of the fling. + break + } + t = p.t + values = append(values, first.v-p.v) + times = append(times, float32((-age).Seconds())) + } + coef, ok := polyFit(times, values) + if !ok { + return Estimate{} + } + dist := values[len(values)-1] - values[0] + return Estimate{ + Velocity: coef[1], + Distance: dist, + } +} + +func (e *Extrapolation) get(i int) sample { + idx := (e.idx + i - 1 + len(e.samples)) % len(e.samples) + return e.samples[idx] +} + +// fit computes the least squares polynomial fit for +// the set of points in X, Y. If the fitting fails +// because of contradicting or insufficient data, +// fit returns false. +func polyFit(X, Y []float32) (coefficients, bool) { + if len(X) != len(Y) { + panic("X and Y lengths differ") + } + if len(X) <= degree { + // Not enough points to fit a curve. + return coefficients{}, false + } + + // Use a method similar to Android's VelocityTracker.cpp: + // https://android.googlesource.com/platform/frameworks/base/+/56a2301/libs/androidfw/VelocityTracker.cpp + // where all weights are 1. + + // First, expand the X vector to the matrix A in column-major order. + A := newMatrix(degree+1, len(X)) + for i, x := range X { + A.set(0, i, 1) + for j := 1; j < A.rows; j++ { + A.set(j, i, A.get(j-1, i)*x) + } + } + + Q, Rt, ok := decomposeQR(A) + if !ok { + return coefficients{}, false + } + // Solve R*B = Qt*Y for B, which is then the polynomial coefficients. + // Since R is upper triangular, we can proceed from bottom right to + // upper left. + // https://en.wikipedia.org/wiki/Non-linear_least_squares + var B coefficients + for i := Q.rows - 1; i >= 0; i-- { + B[i] = dot(Q.col(i), Y) + for j := Q.rows - 1; j > i; j-- { + B[i] -= Rt.get(i, j) * B[j] + } + B[i] /= Rt.get(i, i) + } + return B, true +} + +// decomposeQR computes and returns Q, Rt where Q*transpose(Rt) = A, if +// possible. R is guaranteed to be upper triangular and only the square +// part of Rt is returned. +func decomposeQR(A *matrix) (*matrix, *matrix, bool) { + // Gram-Schmidt QR decompose A where Q*R = A. + // https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process + Q := newMatrix(A.rows, A.cols) // Column-major. + Rt := newMatrix(A.rows, A.rows) // R transposed, row-major. + for i := 0; i < Q.rows; i++ { + // Copy A column. + for j := 0; j < Q.cols; j++ { + Q.set(i, j, A.get(i, j)) + } + // Subtract projections. Note that int the projection + // + // proju a = / u + // + // the normalized column e replaces u, where = 1: + // + // proje a = / e = e + for j := 0; j < i; j++ { + d := dot(Q.col(j), Q.col(i)) + for k := 0; k < Q.cols; k++ { + Q.set(i, k, Q.get(i, k)-d*Q.get(j, k)) + } + } + // Normalize Q columns. + n := norm(Q.col(i)) + if n < 0.000001 { + // Degenerate data, no solution. + return nil, nil, false + } + invNorm := 1 / n + for j := 0; j < Q.cols; j++ { + Q.set(i, j, Q.get(i, j)*invNorm) + } + // Update Rt. + for j := i; j < Rt.cols; j++ { + Rt.set(i, j, dot(Q.col(i), A.col(j))) + } + } + return Q, Rt, true +} + +func norm(V []float32) float32 { + var n float32 + for _, v := range V { + n += v * v + } + return float32(math.Sqrt(float64(n))) +} + +func dot(V1, V2 []float32) float32 { + var d float32 + for i, v1 := range V1 { + d += v1 * V2[i] + } + return d +} + +func newMatrix(rows, cols int) *matrix { + return &matrix{ + rows: rows, + cols: cols, + data: make([]float32, rows*cols), + } +} + +func (m *matrix) set(row, col int, v float32) { + if row < 0 || row >= m.rows { + panic("row out of range") + } + if col < 0 || col >= m.cols { + panic("col out of range") + } + m.data[row*m.cols+col] = v +} + +func (m *matrix) get(row, col int) float32 { + if row < 0 || row >= m.rows { + panic("row out of range") + } + if col < 0 || col >= m.cols { + panic("col out of range") + } + return m.data[row*m.cols+col] +} + +func (m *matrix) col(c int) []float32 { + return m.data[c*m.cols : (c+1)*m.cols] +} + +func (m *matrix) approxEqual(m2 *matrix) bool { + if m.rows != m2.rows || m.cols != m2.cols { + return false + } + const epsilon = 0.00001 + for row := 0; row < m.rows; row++ { + for col := 0; col < m.cols; col++ { + d := m2.get(row, col) - m.get(row, col) + if d < -epsilon || d > epsilon { + return false + } + } + } + return true +} + +func (m *matrix) transpose() *matrix { + t := &matrix{ + rows: m.cols, + cols: m.rows, + data: make([]float32, len(m.data)), + } + for i := 0; i < m.rows; i++ { + for j := 0; j < m.cols; j++ { + t.set(j, i, m.get(i, j)) + } + } + return t +} + +func (m *matrix) mul(m2 *matrix) *matrix { + if m.rows != m2.cols { + panic("mismatched matrices") + } + mm := &matrix{ + rows: m.rows, + cols: m2.cols, + data: make([]float32, m.rows*m2.cols), + } + for i := 0; i < mm.rows; i++ { + for j := 0; j < mm.cols; j++ { + var v float32 + for k := 0; k < m.rows; k++ { + v += m.get(k, j) * m2.get(i, k) + } + mm.set(i, j, v) + } + } + return mm +} + +func (m *matrix) String() string { + var b strings.Builder + for i := 0; i < m.rows; i++ { + for j := 0; j < m.cols; j++ { + v := m.get(i, j) + b.WriteString(strconv.FormatFloat(float64(v), 'g', -1, 32)) + b.WriteString(", ") + } + b.WriteString("\n") + } + return b.String() +} + +func (c coefficients) approxEqual(c2 coefficients) bool { + const epsilon = 0.00001 + for i, v := range c { + d := v - c2[i] + if d < -epsilon || d > epsilon { + return false + } + } + return true +} diff --git a/gio/giold/internal/fling/extrapolation_test.go b/gio/giold/internal/fling/extrapolation_test.go new file mode 100644 index 0000000..3f9d982 --- /dev/null +++ b/gio/giold/internal/fling/extrapolation_test.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package fling + +import "testing" + +func TestDecomposeQR(t *testing.T) { + A := &matrix{ + rows: 3, cols: 3, + data: []float32{ + 12, 6, -4, + -51, 167, 24, + 4, -68, -41, + }, + } + Q, Rt, ok := decomposeQR(A) + if !ok { + t.Fatal("decomposeQR failed") + } + R := Rt.transpose() + QR := Q.mul(R) + if !A.approxEqual(QR) { + t.Log("A\n", A) + t.Log("Q\n", Q) + t.Log("R\n", R) + t.Log("QR\n", QR) + t.Fatal("Q*R not approximately equal to A") + } +} + +func TestFit(t *testing.T) { + X := []float32{-1, 0, 1} + Y := []float32{2, 0, 2} + + got, ok := polyFit(X, Y) + if !ok { + t.Fatal("polyFit failed") + } + want := coefficients{0, 0, 2} + if !got.approxEqual(want) { + t.Fatalf("polyFit: got %v want %v", got, want) + } +} diff --git a/gio/giold/internal/gl/gl.go b/gio/giold/internal/gl/gl.go new file mode 100644 index 0000000..9696c71 --- /dev/null +++ b/gio/giold/internal/gl/gl.go @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +type ( + Attrib uint + Enum uint +) + +const ( + ALL_BARRIER_BITS = 0xffffffff + ARRAY_BUFFER = 0x8892 + BLEND = 0xbe2 + CLAMP_TO_EDGE = 0x812f + COLOR_ATTACHMENT0 = 0x8ce0 + COLOR_BUFFER_BIT = 0x4000 + COMPILE_STATUS = 0x8b81 + COMPUTE_SHADER = 0x91B9 + DEPTH_BUFFER_BIT = 0x100 + DEPTH_ATTACHMENT = 0x8d00 + DEPTH_COMPONENT16 = 0x81a5 + DEPTH_COMPONENT24 = 0x81A6 + DEPTH_COMPONENT32F = 0x8CAC + DEPTH_TEST = 0xb71 + DRAW_FRAMEBUFFER = 0x8CA9 + DST_COLOR = 0x306 + DYNAMIC_DRAW = 0x88E8 + DYNAMIC_READ = 0x88E9 + ELEMENT_ARRAY_BUFFER = 0x8893 + EXTENSIONS = 0x1f03 + FALSE = 0 + FLOAT = 0x1406 + FRAGMENT_SHADER = 0x8b30 + FRAMEBUFFER = 0x8d40 + FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING = 0x8210 + FRAMEBUFFER_BINDING = 0x8ca6 + FRAMEBUFFER_COMPLETE = 0x8cd5 + HALF_FLOAT = 0x140b + HALF_FLOAT_OES = 0x8d61 + INFO_LOG_LENGTH = 0x8B84 + INVALID_INDEX = ^uint(0) + GREATER = 0x204 + GEQUAL = 0x206 + LINEAR = 0x2601 + LINK_STATUS = 0x8b82 + LUMINANCE = 0x1909 + MAP_READ_BIT = 0x0001 + MAX_TEXTURE_SIZE = 0xd33 + NEAREST = 0x2600 + NO_ERROR = 0x0 + NUM_EXTENSIONS = 0x821D + ONE = 0x1 + ONE_MINUS_SRC_ALPHA = 0x303 + PROGRAM_BINARY_LENGTH = 0x8741 + QUERY_RESULT = 0x8866 + QUERY_RESULT_AVAILABLE = 0x8867 + R16F = 0x822d + R8 = 0x8229 + READ_FRAMEBUFFER = 0x8ca8 + READ_ONLY = 0x88B8 + READ_WRITE = 0x88BA + RED = 0x1903 + RENDERER = 0x1F01 + RENDERBUFFER = 0x8d41 + RENDERBUFFER_BINDING = 0x8ca7 + RENDERBUFFER_HEIGHT = 0x8d43 + RENDERBUFFER_WIDTH = 0x8d42 + RGB = 0x1907 + RGBA = 0x1908 + RGBA8 = 0x8058 + SHADER_STORAGE_BUFFER = 0x90D2 + SHORT = 0x1402 + SRGB = 0x8c40 + SRGB_ALPHA_EXT = 0x8c42 + SRGB8 = 0x8c41 + SRGB8_ALPHA8 = 0x8c43 + STATIC_DRAW = 0x88e4 + STENCIL_BUFFER_BIT = 0x00000400 + TEXTURE_2D = 0xde1 + TEXTURE_MAG_FILTER = 0x2800 + TEXTURE_MIN_FILTER = 0x2801 + TEXTURE_WRAP_S = 0x2802 + TEXTURE_WRAP_T = 0x2803 + TEXTURE0 = 0x84c0 + TEXTURE1 = 0x84c1 + TRIANGLE_STRIP = 0x5 + TRIANGLES = 0x4 + TRUE = 1 + UNIFORM_BUFFER = 0x8A11 + UNPACK_ALIGNMENT = 0xcf5 + UNSIGNED_BYTE = 0x1401 + UNSIGNED_SHORT = 0x1403 + VERSION = 0x1f02 + VERTEX_SHADER = 0x8b31 + WRITE_ONLY = 0x88B9 + ZERO = 0x0 + + // EXT_disjoint_timer_query + TIME_ELAPSED_EXT = 0x88BF + GPU_DISJOINT_EXT = 0x8FBB +) + +var _ interface { + ActiveTexture(texture Enum) + AttachShader(p Program, s Shader) + BeginQuery(target Enum, query Query) + BindAttribLocation(p Program, a Attrib, name string) + BindBuffer(target Enum, b Buffer) + BindBufferBase(target Enum, index int, buffer Buffer) + BindFramebuffer(target Enum, fb Framebuffer) + BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) + BindRenderbuffer(target Enum, fb Renderbuffer) + BindTexture(target Enum, t Texture) + BlendEquation(mode Enum) + BlendFunc(sfactor, dfactor Enum) + BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) + BufferData(target Enum, size int, usage Enum) + BufferSubData(target Enum, offset int, src []byte) + CheckFramebufferStatus(target Enum) Enum + Clear(mask Enum) + ClearColor(red, green, blue, alpha float32) + ClearDepthf(d float32) + CompileShader(s Shader) + CreateBuffer() Buffer + CreateFramebuffer() Framebuffer + CreateProgram() Program + CreateQuery() Query + CreateRenderbuffer() Renderbuffer + CreateShader(ty Enum) Shader + CreateTexture() Texture + DeleteBuffer(v Buffer) + DeleteFramebuffer(v Framebuffer) + DeleteProgram(p Program) + DeleteQuery(query Query) + DeleteRenderbuffer(r Renderbuffer) + DeleteShader(s Shader) + DeleteTexture(v Texture) + DepthFunc(f Enum) + DepthMask(mask bool) + DisableVertexAttribArray(a Attrib) + Disable(cap Enum) + DispatchCompute(x, y, z int) + DrawArrays(mode Enum, first, count int) + DrawElements(mode Enum, count int, ty Enum, offset int) + Enable(cap Enum) + EnableVertexAttribArray(a Attrib) + EndQuery(target Enum) + FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) + FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) + GetBinding(pname Enum) Object + GetError() Enum + GetInteger(pname Enum) int + GetProgrami(p Program, pname Enum) int + GetProgramInfoLog(p Program) string + GetQueryObjectuiv(query Query, pname Enum) uint + GetShaderi(s Shader, pname Enum) int + GetShaderInfoLog(s Shader) string + GetString(pname Enum) string + GetUniformBlockIndex(p Program, name string) uint + GetUniformLocation(p Program, name string) Uniform + InvalidateFramebuffer(target, attachment Enum) + LinkProgram(p Program) + MapBufferRange(target Enum, offset, length int, access Enum) []byte + MemoryBarrier(barriers Enum) + ReadPixels(x, y, width, height int, format, ty Enum, data []byte) + RenderbufferStorage(target, internalformat Enum, width, height int) + ShaderSource(s Shader, src string) + TexImage2D(target Enum, level int, internalFormat Enum, width, height int, format, ty Enum) + TexParameteri(target, pname Enum, param int) + TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) + TexSubImage2D(target Enum, level, xoff, yoff int, width, height int, format, ty Enum, data []byte) + UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) + Uniform1f(dst Uniform, v float32) + Uniform1i(dst Uniform, v int) + Uniform2f(dst Uniform, v0, v1 float32) + Uniform3f(dst Uniform, v0, v1, v2 float32) + Uniform4f(dst Uniform, v0, v1, v2, v3 float32) + UseProgram(p Program) + UnmapBuffer(target Enum) bool + VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) + Viewport(x, y, width, height int) +} = (*Functions)(nil) diff --git a/gio/giold/internal/gl/gl_js.go b/gio/giold/internal/gl/gl_js.go new file mode 100644 index 0000000..13890a7 --- /dev/null +++ b/gio/giold/internal/gl/gl_js.go @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import ( + "errors" + "strings" + "syscall/js" +) + +type Functions struct { + Ctx js.Value + EXT_disjoint_timer_query js.Value + EXT_disjoint_timer_query_webgl2 js.Value + + // Cached reference to the Uint8Array JS type. + uint8Array js.Value + + // Cached JS arrays. + arrayBuf js.Value + int32Buf js.Value +} + +type Context js.Value + +func NewFunctions(ctx Context) (*Functions, error) { + f := &Functions{ + Ctx: js.Value(ctx), + uint8Array: js.Global().Get("Uint8Array"), + } + if err := f.Init(); err != nil { + return nil, err + } + return f, nil +} + +func (f *Functions) Init() error { + webgl2Class := js.Global().Get("WebGL2RenderingContext") + iswebgl2 := !webgl2Class.IsUndefined() && f.Ctx.InstanceOf(webgl2Class) + if !iswebgl2 { + f.EXT_disjoint_timer_query = f.getExtension("EXT_disjoint_timer_query") + if f.getExtension("OES_texture_half_float").IsNull() && f.getExtension("OES_texture_float").IsNull() { + return errors.New("gl: no support for neither OES_texture_half_float nor OES_texture_float") + } + if f.getExtension("EXT_sRGB").IsNull() { + return errors.New("gl: EXT_sRGB not supported") + } + } else { + // WebGL2 extensions. + f.EXT_disjoint_timer_query_webgl2 = f.getExtension("EXT_disjoint_timer_query_webgl2") + if f.getExtension("EXT_color_buffer_half_float").IsNull() && f.getExtension("EXT_color_buffer_float").IsNull() { + return errors.New("gl: no support for neither EXT_color_buffer_half_float nor EXT_color_buffer_float") + } + } + return nil +} + +func (f *Functions) getExtension(name string) js.Value { + return f.Ctx.Call("getExtension", name) +} + +func (f *Functions) ActiveTexture(t Enum) { + f.Ctx.Call("activeTexture", int(t)) +} +func (f *Functions) AttachShader(p Program, s Shader) { + f.Ctx.Call("attachShader", js.Value(p), js.Value(s)) +} +func (f *Functions) BeginQuery(target Enum, query Query) { + if !f.EXT_disjoint_timer_query_webgl2.IsNull() { + f.Ctx.Call("beginQuery", int(target), js.Value(query)) + } else { + f.EXT_disjoint_timer_query.Call("beginQueryEXT", int(target), js.Value(query)) + } +} +func (f *Functions) BindAttribLocation(p Program, a Attrib, name string) { + f.Ctx.Call("bindAttribLocation", js.Value(p), int(a), name) +} +func (f *Functions) BindBuffer(target Enum, b Buffer) { + f.Ctx.Call("bindBuffer", int(target), js.Value(b)) +} +func (f *Functions) BindBufferBase(target Enum, index int, b Buffer) { + f.Ctx.Call("bindBufferBase", int(target), index, js.Value(b)) +} +func (f *Functions) BindFramebuffer(target Enum, fb Framebuffer) { + f.Ctx.Call("bindFramebuffer", int(target), js.Value(fb)) +} +func (f *Functions) BindRenderbuffer(target Enum, rb Renderbuffer) { + f.Ctx.Call("bindRenderbuffer", int(target), js.Value(rb)) +} +func (f *Functions) BindTexture(target Enum, t Texture) { + f.Ctx.Call("bindTexture", int(target), js.Value(t)) +} +func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) { + panic("not implemented") +} +func (f *Functions) BlendEquation(mode Enum) { + f.Ctx.Call("blendEquation", int(mode)) +} +func (f *Functions) BlendFunc(sfactor, dfactor Enum) { + f.Ctx.Call("blendFunc", int(sfactor), int(dfactor)) +} +func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) { + panic("not implemented") +} +func (f *Functions) BufferData(target Enum, size int, usage Enum) { + f.Ctx.Call("bufferData", int(target), size, int(usage)) +} +func (f *Functions) BufferSubData(target Enum, offset int, src []byte) { + f.Ctx.Call("bufferSubData", int(target), offset, f.byteArrayOf(src)) +} +func (f *Functions) CheckFramebufferStatus(target Enum) Enum { + return Enum(f.Ctx.Call("checkFramebufferStatus", int(target)).Int()) +} +func (f *Functions) Clear(mask Enum) { + f.Ctx.Call("clear", int(mask)) +} +func (f *Functions) ClearColor(red, green, blue, alpha float32) { + f.Ctx.Call("clearColor", red, green, blue, alpha) +} +func (f *Functions) ClearDepthf(d float32) { + f.Ctx.Call("clearDepth", d) +} +func (f *Functions) CompileShader(s Shader) { + f.Ctx.Call("compileShader", js.Value(s)) +} +func (f *Functions) CreateBuffer() Buffer { + return Buffer(f.Ctx.Call("createBuffer")) +} +func (f *Functions) CreateFramebuffer() Framebuffer { + return Framebuffer(f.Ctx.Call("createFramebuffer")) +} +func (f *Functions) CreateProgram() Program { + return Program(f.Ctx.Call("createProgram")) +} +func (f *Functions) CreateQuery() Query { + return Query(f.Ctx.Call("createQuery")) +} +func (f *Functions) CreateRenderbuffer() Renderbuffer { + return Renderbuffer(f.Ctx.Call("createRenderbuffer")) +} +func (f *Functions) CreateShader(ty Enum) Shader { + return Shader(f.Ctx.Call("createShader", int(ty))) +} +func (f *Functions) CreateTexture() Texture { + return Texture(f.Ctx.Call("createTexture")) +} +func (f *Functions) DeleteBuffer(v Buffer) { + f.Ctx.Call("deleteBuffer", js.Value(v)) +} +func (f *Functions) DeleteFramebuffer(v Framebuffer) { + f.Ctx.Call("deleteFramebuffer", js.Value(v)) +} +func (f *Functions) DeleteProgram(p Program) { + f.Ctx.Call("deleteProgram", js.Value(p)) +} +func (f *Functions) DeleteQuery(query Query) { + if !f.EXT_disjoint_timer_query_webgl2.IsNull() { + f.Ctx.Call("deleteQuery", js.Value(query)) + } else { + f.EXT_disjoint_timer_query.Call("deleteQueryEXT", js.Value(query)) + } +} +func (f *Functions) DeleteShader(s Shader) { + f.Ctx.Call("deleteShader", js.Value(s)) +} +func (f *Functions) DeleteRenderbuffer(v Renderbuffer) { + f.Ctx.Call("deleteRenderbuffer", js.Value(v)) +} +func (f *Functions) DeleteTexture(v Texture) { + f.Ctx.Call("deleteTexture", js.Value(v)) +} +func (f *Functions) DepthFunc(fn Enum) { + f.Ctx.Call("depthFunc", int(fn)) +} +func (f *Functions) DepthMask(mask bool) { + f.Ctx.Call("depthMask", mask) +} +func (f *Functions) DisableVertexAttribArray(a Attrib) { + f.Ctx.Call("disableVertexAttribArray", int(a)) +} +func (f *Functions) Disable(cap Enum) { + f.Ctx.Call("disable", int(cap)) +} +func (f *Functions) DrawArrays(mode Enum, first, count int) { + f.Ctx.Call("drawArrays", int(mode), first, count) +} +func (f *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { + f.Ctx.Call("drawElements", int(mode), count, int(ty), offset) +} +func (f *Functions) DispatchCompute(x, y, z int) { + panic("not implemented") +} +func (f *Functions) Enable(cap Enum) { + f.Ctx.Call("enable", int(cap)) +} +func (f *Functions) EnableVertexAttribArray(a Attrib) { + f.Ctx.Call("enableVertexAttribArray", int(a)) +} +func (f *Functions) EndQuery(target Enum) { + if !f.EXT_disjoint_timer_query_webgl2.IsNull() { + f.Ctx.Call("endQuery", int(target)) + } else { + f.EXT_disjoint_timer_query.Call("endQueryEXT", int(target)) + } +} +func (f *Functions) Finish() { + f.Ctx.Call("finish") +} +func (f *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { + f.Ctx.Call("framebufferRenderbuffer", int(target), int(attachment), int(renderbuffertarget), js.Value(renderbuffer)) +} +func (f *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { + f.Ctx.Call("framebufferTexture2D", int(target), int(attachment), int(texTarget), js.Value(t), level) +} +func (f *Functions) GetError() Enum { + // Avoid slow getError calls. See gio#179. + return 0 +} +func (f *Functions) GetRenderbufferParameteri(target, pname Enum) int { + return paramVal(f.Ctx.Call("getRenderbufferParameteri", int(pname))) +} +func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { + return paramVal(f.Ctx.Call("getFramebufferAttachmentParameter", int(target), int(attachment), int(pname))) +} +func (f *Functions) GetBinding(pname Enum) Object { + return Object(f.Ctx.Call("getParameter", int(pname))) +} +func (f *Functions) GetInteger(pname Enum) int { + return paramVal(f.Ctx.Call("getParameter", int(pname))) +} +func (f *Functions) GetProgrami(p Program, pname Enum) int { + return paramVal(f.Ctx.Call("getProgramParameter", js.Value(p), int(pname))) +} +func (f *Functions) GetProgramInfoLog(p Program) string { + return f.Ctx.Call("getProgramInfoLog", js.Value(p)).String() +} +func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { + if !f.EXT_disjoint_timer_query_webgl2.IsNull() { + return uint(paramVal(f.Ctx.Call("getQueryParameter", js.Value(query), int(pname)))) + } else { + return uint(paramVal(f.EXT_disjoint_timer_query.Call("getQueryObjectEXT", js.Value(query), int(pname)))) + } +} +func (f *Functions) GetShaderi(s Shader, pname Enum) int { + return paramVal(f.Ctx.Call("getShaderParameter", js.Value(s), int(pname))) +} +func (f *Functions) GetShaderInfoLog(s Shader) string { + return f.Ctx.Call("getShaderInfoLog", js.Value(s)).String() +} +func (f *Functions) GetString(pname Enum) string { + switch pname { + case EXTENSIONS: + extsjs := f.Ctx.Call("getSupportedExtensions") + var exts []string + for i := 0; i < extsjs.Length(); i++ { + exts = append(exts, "GL_"+extsjs.Index(i).String()) + } + return strings.Join(exts, " ") + default: + return f.Ctx.Call("getParameter", int(pname)).String() + } +} +func (f *Functions) GetUniformBlockIndex(p Program, name string) uint { + return uint(paramVal(f.Ctx.Call("getUniformBlockIndex", js.Value(p), name))) +} +func (f *Functions) GetUniformLocation(p Program, name string) Uniform { + return Uniform(f.Ctx.Call("getUniformLocation", js.Value(p), name)) +} +func (f *Functions) InvalidateFramebuffer(target, attachment Enum) { + fn := f.Ctx.Get("invalidateFramebuffer") + if !fn.IsUndefined() { + if f.int32Buf.IsUndefined() { + f.int32Buf = js.Global().Get("Int32Array").New(1) + } + f.int32Buf.SetIndex(0, int32(attachment)) + f.Ctx.Call("invalidateFramebuffer", int(target), f.int32Buf) + } +} +func (f *Functions) LinkProgram(p Program) { + f.Ctx.Call("linkProgram", js.Value(p)) +} +func (f *Functions) PixelStorei(pname Enum, param int32) { + f.Ctx.Call("pixelStorei", int(pname), param) +} +func (f *Functions) MemoryBarrier(barriers Enum) { + panic("not implemented") +} +func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte { + panic("not implemented") +} +func (f *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { + f.Ctx.Call("renderbufferStorage", int(target), int(internalformat), width, height) +} +func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) { + ba := f.byteArrayOf(data) + f.Ctx.Call("readPixels", x, y, width, height, int(format), int(ty), ba) + js.CopyBytesToGo(data, ba) +} +func (f *Functions) Scissor(x, y, width, height int32) { + f.Ctx.Call("scissor", x, y, width, height) +} +func (f *Functions) ShaderSource(s Shader, src string) { + f.Ctx.Call("shaderSource", js.Value(s), src) +} +func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width, height int, format, ty Enum) { + f.Ctx.Call("texImage2D", int(target), int(level), int(internalFormat), int(width), int(height), 0, int(format), int(ty), nil) +} +func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) { + f.Ctx.Call("texStorage2D", int(target), levels, int(internalFormat), width, height) +} +func (f *Functions) TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) { + f.Ctx.Call("texSubImage2D", int(target), level, x, y, width, height, int(format), int(ty), f.byteArrayOf(data)) +} +func (f *Functions) TexParameteri(target, pname Enum, param int) { + f.Ctx.Call("texParameteri", int(target), int(pname), int(param)) +} +func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) { + f.Ctx.Call("uniformBlockBinding", js.Value(p), int(uniformBlockIndex), int(uniformBlockBinding)) +} +func (f *Functions) Uniform1f(dst Uniform, v float32) { + f.Ctx.Call("uniform1f", js.Value(dst), v) +} +func (f *Functions) Uniform1i(dst Uniform, v int) { + f.Ctx.Call("uniform1i", js.Value(dst), v) +} +func (f *Functions) Uniform2f(dst Uniform, v0, v1 float32) { + f.Ctx.Call("uniform2f", js.Value(dst), v0, v1) +} +func (f *Functions) Uniform3f(dst Uniform, v0, v1, v2 float32) { + f.Ctx.Call("uniform3f", js.Value(dst), v0, v1, v2) +} +func (f *Functions) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) { + f.Ctx.Call("uniform4f", js.Value(dst), v0, v1, v2, v3) +} +func (f *Functions) UseProgram(p Program) { + f.Ctx.Call("useProgram", js.Value(p)) +} +func (f *Functions) UnmapBuffer(target Enum) bool { + panic("not implemented") +} +func (f *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) { + f.Ctx.Call("vertexAttribPointer", int(dst), size, int(ty), normalized, stride, offset) +} +func (f *Functions) Viewport(x, y, width, height int) { + f.Ctx.Call("viewport", x, y, width, height) +} + +func (f *Functions) byteArrayOf(data []byte) js.Value { + if len(data) == 0 { + return js.Null() + } + f.resizeByteBuffer(len(data)) + ba := f.uint8Array.New(f.arrayBuf, int(0), int(len(data))) + js.CopyBytesToJS(ba, data) + return ba +} + +func (f *Functions) resizeByteBuffer(n int) { + if n == 0 { + return + } + if !f.arrayBuf.IsUndefined() && f.arrayBuf.Length() >= n { + return + } + f.arrayBuf = js.Global().Get("ArrayBuffer").New(n) +} + +func paramVal(v js.Value) int { + switch v.Type() { + case js.TypeBoolean: + if b := v.Bool(); b { + return 1 + } else { + return 0 + } + case js.TypeNumber: + return v.Int() + default: + panic("unknown parameter type") + } +} diff --git a/gio/giold/internal/gl/gl_unix.go b/gio/giold/internal/gl/gl_unix.go new file mode 100644 index 0000000..a1d017a --- /dev/null +++ b/gio/giold/internal/gl/gl_unix.go @@ -0,0 +1,635 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin linux freebsd openbsd + +package gl + +import ( + "runtime" + "strings" + "unsafe" +) + +/* +#cgo CFLAGS: -Werror +#cgo linux,!android pkg-config: glesv2 +#cgo linux freebsd LDFLAGS: -ldl +#cgo freebsd openbsd android LDFLAGS: -lGLESv2 +#cgo freebsd CFLAGS: -I/usr/local/include +#cgo freebsd LDFLAGS: -L/usr/local/lib +#cgo openbsd CFLAGS: -I/usr/X11R6/include +#cgo openbsd LDFLAGS: -L/usr/X11R6/lib +#cgo darwin,!ios CFLAGS: -DGL_SILENCE_DEPRECATION +#cgo darwin,!ios LDFLAGS: -framework OpenGL +#cgo darwin,ios CFLAGS: -DGLES_SILENCE_DEPRECATION +#cgo darwin,ios LDFLAGS: -framework OpenGLES + +#include +#define __USE_GNU +#include + +#ifdef __APPLE__ + #include "TargetConditionals.h" + #if TARGET_OS_IPHONE + #include + #else + #include + #endif +#else +#include +#include +#endif + +static void (*_glBindBufferBase)(GLenum target, GLuint index, GLuint buffer); +static GLuint (*_glGetUniformBlockIndex)(GLuint program, const GLchar *uniformBlockName); +static void (*_glUniformBlockBinding)(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding); +static void (*_glInvalidateFramebuffer)(GLenum target, GLsizei numAttachments, const GLenum *attachments); + +static void (*_glBeginQuery)(GLenum target, GLuint id); +static void (*_glDeleteQueries)(GLsizei n, const GLuint *ids); +static void (*_glEndQuery)(GLenum target); +static void (*_glGenQueries)(GLsizei n, GLuint *ids); +static void (*_glGetProgramBinary)(GLuint program, GLsizei bufsize, GLsizei *length, GLenum *binaryFormat, void *binary); +static void (*_glGetQueryObjectuiv)(GLuint id, GLenum pname, GLuint *params); +static const GLubyte* (*_glGetStringi)(GLenum name, GLuint index); +static void (*_glMemoryBarrier)(GLbitfield barriers); +static void (*_glDispatchCompute)(GLuint x, GLuint y, GLuint z); +static void* (*_glMapBufferRange)(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access); +static GLboolean (*_glUnmapBuffer)(GLenum target); +static void (*_glBindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format); +static void (*_glTexStorage2D)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height); +static void (*_glBlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter); + +// The pointer-free version of glVertexAttribPointer, to avoid the Cgo pointer checks. +__attribute__ ((visibility ("hidden"))) void gio_glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, uintptr_t offset) { + glVertexAttribPointer(index, size, type, normalized, stride, (const GLvoid *)offset); +} + +// The pointer-free version of glDrawElements, to avoid the Cgo pointer checks. +__attribute__ ((visibility ("hidden"))) void gio_glDrawElements(GLenum mode, GLsizei count, GLenum type, const uintptr_t offset) { + glDrawElements(mode, count, type, (const GLvoid *)offset); +} + +__attribute__ ((visibility ("hidden"))) void gio_glBindBufferBase(GLenum target, GLuint index, GLuint buffer) { + _glBindBufferBase(target, index, buffer); +} + +__attribute__ ((visibility ("hidden"))) void gio_glUniformBlockBinding(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding) { + _glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); +} + +__attribute__ ((visibility ("hidden"))) GLuint gio_glGetUniformBlockIndex(GLuint program, const GLchar *uniformBlockName) { + return _glGetUniformBlockIndex(program, uniformBlockName); +} + +__attribute__ ((visibility ("hidden"))) void gio_glInvalidateFramebuffer(GLenum target, GLenum attachment) { + // Framebuffer invalidation is just a hint and can safely be ignored. + if (_glInvalidateFramebuffer != NULL) { + _glInvalidateFramebuffer(target, 1, &attachment); + } +} + +__attribute__ ((visibility ("hidden"))) void gio_glBeginQuery(GLenum target, GLenum attachment) { + _glBeginQuery(target, attachment); +} + +__attribute__ ((visibility ("hidden"))) void gio_glDeleteQueries(GLsizei n, const GLuint *ids) { + _glDeleteQueries(n, ids); +} + +__attribute__ ((visibility ("hidden"))) void gio_glEndQuery(GLenum target) { + _glEndQuery(target); +} + +__attribute__ ((visibility ("hidden"))) const GLubyte* gio_glGetStringi(GLenum name, GLuint index) { + if (_glGetStringi == NULL) { + return NULL; + } + return _glGetStringi(name, index); +} + +__attribute__ ((visibility ("hidden"))) void gio_glGenQueries(GLsizei n, GLuint *ids) { + _glGenQueries(n, ids); +} + +__attribute__ ((visibility ("hidden"))) void gio_glGetProgramBinary(GLuint program, GLsizei bufsize, GLsizei *length, GLenum *binaryFormat, void *binary) { + _glGetProgramBinary(program, bufsize, length, binaryFormat, binary); +} + +__attribute__ ((visibility ("hidden"))) void gio_glGetQueryObjectuiv(GLuint id, GLenum pname, GLuint *params) { + _glGetQueryObjectuiv(id, pname, params); +} + +__attribute__ ((visibility ("hidden"))) void gio_glMemoryBarrier(GLbitfield barriers) { + _glMemoryBarrier(barriers); +} + +__attribute__ ((visibility ("hidden"))) void gio_glDispatchCompute(GLuint x, GLuint y, GLuint z) { + _glDispatchCompute(x, y, z); +} + +__attribute__ ((visibility ("hidden"))) void *gio_glMapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access) { + return _glMapBufferRange(target, offset, length, access); +} + +__attribute__ ((visibility ("hidden"))) GLboolean gio_glUnmapBuffer(GLenum target) { + return _glUnmapBuffer(target); +} + +__attribute__ ((visibility ("hidden"))) void gio_glBindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format) { + _glBindImageTexture(unit, texture, level, layered, layer, access, format); +} + +__attribute__ ((visibility ("hidden"))) void gio_glTexStorage2D(GLenum target, GLsizei levels, GLenum internalFormat, GLsizei width, GLsizei height) { + _glTexStorage2D(target, levels, internalFormat, width, height); +} + +__attribute__ ((visibility ("hidden"))) void gio_glBlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { + _glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); +} + +__attribute__((constructor)) static void gio_loadGLFunctions() { + // Load libGLESv3 if available. + dlopen("libGLESv3.so", RTLD_NOW | RTLD_GLOBAL); + + _glBindBufferBase = dlsym(RTLD_DEFAULT, "glBindBufferBase"); + _glGetUniformBlockIndex = dlsym(RTLD_DEFAULT, "glGetUniformBlockIndex"); + _glUniformBlockBinding = dlsym(RTLD_DEFAULT, "glUniformBlockBinding"); + _glInvalidateFramebuffer = dlsym(RTLD_DEFAULT, "glInvalidateFramebuffer"); + _glGetStringi = dlsym(RTLD_DEFAULT, "glGetStringi"); + // Fall back to EXT_invalidate_framebuffer if available. + if (_glInvalidateFramebuffer == NULL) { + _glInvalidateFramebuffer = dlsym(RTLD_DEFAULT, "glDiscardFramebufferEXT"); + } + + _glBeginQuery = dlsym(RTLD_DEFAULT, "glBeginQuery"); + if (_glBeginQuery == NULL) + _glBeginQuery = dlsym(RTLD_DEFAULT, "glBeginQueryEXT"); + _glDeleteQueries = dlsym(RTLD_DEFAULT, "glDeleteQueries"); + if (_glDeleteQueries == NULL) + _glDeleteQueries = dlsym(RTLD_DEFAULT, "glDeleteQueriesEXT"); + _glEndQuery = dlsym(RTLD_DEFAULT, "glEndQuery"); + if (_glEndQuery == NULL) + _glEndQuery = dlsym(RTLD_DEFAULT, "glEndQueryEXT"); + _glGenQueries = dlsym(RTLD_DEFAULT, "glGenQueries"); + if (_glGenQueries == NULL) + _glGenQueries = dlsym(RTLD_DEFAULT, "glGenQueriesEXT"); + _glGetQueryObjectuiv = dlsym(RTLD_DEFAULT, "glGetQueryObjectuiv"); + if (_glGetQueryObjectuiv == NULL) + _glGetQueryObjectuiv = dlsym(RTLD_DEFAULT, "glGetQueryObjectuivEXT"); + + _glMemoryBarrier = dlsym(RTLD_DEFAULT, "glMemoryBarrier"); + _glDispatchCompute = dlsym(RTLD_DEFAULT, "glDispatchCompute"); + _glMapBufferRange = dlsym(RTLD_DEFAULT, "glMapBufferRange"); + _glUnmapBuffer = dlsym(RTLD_DEFAULT, "glUnmapBuffer"); + _glBindImageTexture = dlsym(RTLD_DEFAULT, "glBindImageTexture"); + _glTexStorage2D = dlsym(RTLD_DEFAULT, "glTexStorage2D"); + _glBlitFramebuffer = dlsym(RTLD_DEFAULT, "glBlitFramebuffer"); + _glGetProgramBinary = dlsym(RTLD_DEFAULT, "glGetProgramBinary"); +} +*/ +import "C" + +type Context interface{} + +type Functions struct { + // Query caches. + uints [100]C.GLuint + ints [100]C.GLint +} + +func NewFunctions(ctx Context) (*Functions, error) { + if ctx != nil { + panic("non-nil context") + } + return new(Functions), nil +} + +func (f *Functions) ActiveTexture(texture Enum) { + C.glActiveTexture(C.GLenum(texture)) +} + +func (f *Functions) AttachShader(p Program, s Shader) { + C.glAttachShader(C.GLuint(p.V), C.GLuint(s.V)) +} + +func (f *Functions) BeginQuery(target Enum, query Query) { + C.gio_glBeginQuery(C.GLenum(target), C.GLenum(query.V)) +} + +func (f *Functions) BindAttribLocation(p Program, a Attrib, name string) { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + C.glBindAttribLocation(C.GLuint(p.V), C.GLuint(a), cname) +} + +func (f *Functions) BindBufferBase(target Enum, index int, b Buffer) { + C.gio_glBindBufferBase(C.GLenum(target), C.GLuint(index), C.GLuint(b.V)) +} + +func (f *Functions) BindBuffer(target Enum, b Buffer) { + C.glBindBuffer(C.GLenum(target), C.GLuint(b.V)) +} + +func (f *Functions) BindFramebuffer(target Enum, fb Framebuffer) { + C.glBindFramebuffer(C.GLenum(target), C.GLuint(fb.V)) +} + +func (f *Functions) BindRenderbuffer(target Enum, fb Renderbuffer) { + C.glBindRenderbuffer(C.GLenum(target), C.GLuint(fb.V)) +} + +func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) { + l := C.GLboolean(C.GL_FALSE) + if layered { + l = C.GL_TRUE + } + C.gio_glBindImageTexture(C.GLuint(unit), C.GLuint(t.V), C.GLint(level), l, C.GLint(layer), C.GLenum(access), C.GLenum(format)) +} + +func (f *Functions) BindTexture(target Enum, t Texture) { + C.glBindTexture(C.GLenum(target), C.GLuint(t.V)) +} + +func (f *Functions) BlendEquation(mode Enum) { + C.glBlendEquation(C.GLenum(mode)) +} + +func (f *Functions) BlendFunc(sfactor, dfactor Enum) { + C.glBlendFunc(C.GLenum(sfactor), C.GLenum(dfactor)) +} + +func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) { + C.gio_glBlitFramebuffer( + C.GLint(sx0), C.GLint(sy0), C.GLint(sx1), C.GLint(sy1), + C.GLint(dx0), C.GLint(dy0), C.GLint(dx1), C.GLint(dy1), + C.GLenum(mask), C.GLenum(filter), + ) +} + +func (f *Functions) BufferData(target Enum, size int, usage Enum) { + C.glBufferData(C.GLenum(target), C.GLsizeiptr(size), nil, C.GLenum(usage)) +} + +func (f *Functions) BufferSubData(target Enum, offset int, src []byte) { + var p unsafe.Pointer + if len(src) > 0 { + p = unsafe.Pointer(&src[0]) + } + C.glBufferSubData(C.GLenum(target), C.GLintptr(offset), C.GLsizeiptr(len(src)), p) +} + +func (f *Functions) CheckFramebufferStatus(target Enum) Enum { + return Enum(C.glCheckFramebufferStatus(C.GLenum(target))) +} + +func (f *Functions) Clear(mask Enum) { + C.glClear(C.GLbitfield(mask)) +} + +func (f *Functions) ClearColor(red float32, green float32, blue float32, alpha float32) { + C.glClearColor(C.GLfloat(red), C.GLfloat(green), C.GLfloat(blue), C.GLfloat(alpha)) +} + +func (f *Functions) ClearDepthf(d float32) { + C.glClearDepthf(C.GLfloat(d)) +} + +func (f *Functions) CompileShader(s Shader) { + C.glCompileShader(C.GLuint(s.V)) +} + +func (f *Functions) CreateBuffer() Buffer { + C.glGenBuffers(1, &f.uints[0]) + return Buffer{uint(f.uints[0])} +} + +func (f *Functions) CreateFramebuffer() Framebuffer { + C.glGenFramebuffers(1, &f.uints[0]) + return Framebuffer{uint(f.uints[0])} +} + +func (f *Functions) CreateProgram() Program { + return Program{uint(C.glCreateProgram())} +} + +func (f *Functions) CreateQuery() Query { + C.gio_glGenQueries(1, &f.uints[0]) + return Query{uint(f.uints[0])} +} + +func (f *Functions) CreateRenderbuffer() Renderbuffer { + C.glGenRenderbuffers(1, &f.uints[0]) + return Renderbuffer{uint(f.uints[0])} +} + +func (f *Functions) CreateShader(ty Enum) Shader { + return Shader{uint(C.glCreateShader(C.GLenum(ty)))} +} + +func (f *Functions) CreateTexture() Texture { + C.glGenTextures(1, &f.uints[0]) + return Texture{uint(f.uints[0])} +} + +func (f *Functions) DeleteBuffer(v Buffer) { + f.uints[0] = C.GLuint(v.V) + C.glDeleteBuffers(1, &f.uints[0]) +} + +func (f *Functions) DeleteFramebuffer(v Framebuffer) { + f.uints[0] = C.GLuint(v.V) + C.glDeleteFramebuffers(1, &f.uints[0]) +} + +func (f *Functions) DeleteProgram(p Program) { + C.glDeleteProgram(C.GLuint(p.V)) +} + +func (f *Functions) DeleteQuery(query Query) { + f.uints[0] = C.GLuint(query.V) + C.gio_glDeleteQueries(1, &f.uints[0]) +} + +func (f *Functions) DeleteRenderbuffer(v Renderbuffer) { + f.uints[0] = C.GLuint(v.V) + C.glDeleteRenderbuffers(1, &f.uints[0]) +} + +func (f *Functions) DeleteShader(s Shader) { + C.glDeleteShader(C.GLuint(s.V)) +} + +func (f *Functions) DeleteTexture(v Texture) { + f.uints[0] = C.GLuint(v.V) + C.glDeleteTextures(1, &f.uints[0]) +} + +func (f *Functions) DepthFunc(v Enum) { + C.glDepthFunc(C.GLenum(v)) +} + +func (f *Functions) DepthMask(mask bool) { + m := C.GLboolean(C.GL_FALSE) + if mask { + m = C.GLboolean(C.GL_TRUE) + } + C.glDepthMask(m) +} + +func (f *Functions) DisableVertexAttribArray(a Attrib) { + C.glDisableVertexAttribArray(C.GLuint(a)) +} + +func (f *Functions) Disable(cap Enum) { + C.glDisable(C.GLenum(cap)) +} + +func (f *Functions) DrawArrays(mode Enum, first int, count int) { + C.glDrawArrays(C.GLenum(mode), C.GLint(first), C.GLsizei(count)) +} + +func (f *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { + C.gio_glDrawElements(C.GLenum(mode), C.GLsizei(count), C.GLenum(ty), C.uintptr_t(offset)) +} + +func (f *Functions) DispatchCompute(x, y, z int) { + C.gio_glDispatchCompute(C.GLuint(x), C.GLuint(y), C.GLuint(z)) +} + +func (f *Functions) Enable(cap Enum) { + C.glEnable(C.GLenum(cap)) +} + +func (f *Functions) EndQuery(target Enum) { + C.gio_glEndQuery(C.GLenum(target)) +} + +func (f *Functions) EnableVertexAttribArray(a Attrib) { + C.glEnableVertexAttribArray(C.GLuint(a)) +} + +func (f *Functions) Finish() { + C.glFinish() +} + +func (f *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { + C.glFramebufferRenderbuffer(C.GLenum(target), C.GLenum(attachment), C.GLenum(renderbuffertarget), C.GLuint(renderbuffer.V)) +} + +func (f *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { + C.glFramebufferTexture2D(C.GLenum(target), C.GLenum(attachment), C.GLenum(texTarget), C.GLuint(t.V), C.GLint(level)) +} + +func (c *Functions) GetBinding(pname Enum) Object { + return Object{uint(c.GetInteger(pname))} +} + +func (f *Functions) GetError() Enum { + return Enum(C.glGetError()) +} + +func (f *Functions) GetRenderbufferParameteri(target, pname Enum) int { + C.glGetRenderbufferParameteriv(C.GLenum(target), C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { + C.glGetFramebufferAttachmentParameteriv(C.GLenum(target), C.GLenum(attachment), C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetInteger(pname Enum) int { + C.glGetIntegerv(C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetProgrami(p Program, pname Enum) int { + C.glGetProgramiv(C.GLuint(p.V), C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetProgramBinary(p Program) []byte { + sz := f.GetProgrami(p, PROGRAM_BINARY_LENGTH) + if sz == 0 { + return nil + } + buf := make([]byte, sz) + var format C.GLenum + C.gio_glGetProgramBinary(C.GLuint(p.V), C.GLsizei(sz), nil, &format, unsafe.Pointer(&buf[0])) + return buf +} + +func (f *Functions) GetProgramInfoLog(p Program) string { + n := f.GetProgrami(p, INFO_LOG_LENGTH) + buf := make([]byte, n) + C.glGetProgramInfoLog(C.GLuint(p.V), C.GLsizei(len(buf)), nil, (*C.GLchar)(unsafe.Pointer(&buf[0]))) + return string(buf) +} + +func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { + C.gio_glGetQueryObjectuiv(C.GLuint(query.V), C.GLenum(pname), &f.uints[0]) + return uint(f.uints[0]) +} + +func (f *Functions) GetShaderi(s Shader, pname Enum) int { + C.glGetShaderiv(C.GLuint(s.V), C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetShaderInfoLog(s Shader) string { + n := f.GetShaderi(s, INFO_LOG_LENGTH) + buf := make([]byte, n) + C.glGetShaderInfoLog(C.GLuint(s.V), C.GLsizei(len(buf)), nil, (*C.GLchar)(unsafe.Pointer(&buf[0]))) + return string(buf) +} + +func (f *Functions) GetStringi(pname Enum, index int) string { + str := C.gio_glGetStringi(C.GLenum(pname), C.GLuint(index)) + if str == nil { + return "" + } + return C.GoString((*C.char)(unsafe.Pointer(str))) +} + +func (f *Functions) GetString(pname Enum) string { + switch { + case runtime.GOOS == "darwin" && pname == EXTENSIONS: + // macOS OpenGL 3 core profile doesn't support glGetString(GL_EXTENSIONS). + // Use glGetStringi(GL_EXTENSIONS, ). + var exts []string + nexts := f.GetInteger(NUM_EXTENSIONS) + for i := 0; i < nexts; i++ { + ext := f.GetStringi(EXTENSIONS, i) + exts = append(exts, ext) + } + return strings.Join(exts, " ") + default: + str := C.glGetString(C.GLenum(pname)) + return C.GoString((*C.char)(unsafe.Pointer(str))) + } +} + +func (f *Functions) GetUniformBlockIndex(p Program, name string) uint { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + return uint(C.gio_glGetUniformBlockIndex(C.GLuint(p.V), cname)) +} + +func (f *Functions) GetUniformLocation(p Program, name string) Uniform { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + return Uniform{int(C.glGetUniformLocation(C.GLuint(p.V), cname))} +} + +func (f *Functions) InvalidateFramebuffer(target, attachment Enum) { + C.gio_glInvalidateFramebuffer(C.GLenum(target), C.GLenum(attachment)) +} + +func (f *Functions) LinkProgram(p Program) { + C.glLinkProgram(C.GLuint(p.V)) +} + +func (f *Functions) PixelStorei(pname Enum, param int32) { + C.glPixelStorei(C.GLenum(pname), C.GLint(param)) +} + +func (f *Functions) MemoryBarrier(barriers Enum) { + C.gio_glMemoryBarrier(C.GLbitfield(barriers)) +} + +func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte { + p := C.gio_glMapBufferRange(C.GLenum(target), C.GLintptr(offset), C.GLsizeiptr(length), C.GLbitfield(access)) + if p == nil { + return nil + } + return (*[1 << 30]byte)(p)[:length:length] +} + +func (f *Functions) Scissor(x, y, width, height int32) { + C.glScissor(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height)) +} + +func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) { + var p unsafe.Pointer + if len(data) > 0 { + p = unsafe.Pointer(&data[0]) + } + C.glReadPixels(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height), C.GLenum(format), C.GLenum(ty), p) +} + +func (f *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { + C.glRenderbufferStorage(C.GLenum(target), C.GLenum(internalformat), C.GLsizei(width), C.GLsizei(height)) +} + +func (f *Functions) ShaderSource(s Shader, src string) { + csrc := C.CString(src) + defer C.free(unsafe.Pointer(csrc)) + strlen := C.GLint(len(src)) + C.glShaderSource(C.GLuint(s.V), 1, &csrc, &strlen) +} + +func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width int, height int, format Enum, ty Enum) { + C.glTexImage2D(C.GLenum(target), C.GLint(level), C.GLint(internalFormat), C.GLsizei(width), C.GLsizei(height), 0, C.GLenum(format), C.GLenum(ty), nil) +} + +func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) { + C.gio_glTexStorage2D(C.GLenum(target), C.GLsizei(levels), C.GLenum(internalFormat), C.GLsizei(width), C.GLsizei(height)) +} + +func (f *Functions) TexSubImage2D(target Enum, level int, x int, y int, width int, height int, format Enum, ty Enum, data []byte) { + var p unsafe.Pointer + if len(data) > 0 { + p = unsafe.Pointer(&data[0]) + } + C.glTexSubImage2D(C.GLenum(target), C.GLint(level), C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height), C.GLenum(format), C.GLenum(ty), p) +} + +func (f *Functions) TexParameteri(target, pname Enum, param int) { + C.glTexParameteri(C.GLenum(target), C.GLenum(pname), C.GLint(param)) +} + +func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) { + C.gio_glUniformBlockBinding(C.GLuint(p.V), C.GLuint(uniformBlockIndex), C.GLuint(uniformBlockBinding)) +} + +func (f *Functions) Uniform1f(dst Uniform, v float32) { + C.glUniform1f(C.GLint(dst.V), C.GLfloat(v)) +} + +func (f *Functions) Uniform1i(dst Uniform, v int) { + C.glUniform1i(C.GLint(dst.V), C.GLint(v)) +} + +func (f *Functions) Uniform2f(dst Uniform, v0 float32, v1 float32) { + C.glUniform2f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1)) +} + +func (f *Functions) Uniform3f(dst Uniform, v0 float32, v1 float32, v2 float32) { + C.glUniform3f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1), C.GLfloat(v2)) +} + +func (f *Functions) Uniform4f(dst Uniform, v0 float32, v1 float32, v2 float32, v3 float32) { + C.glUniform4f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1), C.GLfloat(v2), C.GLfloat(v3)) +} + +func (f *Functions) UseProgram(p Program) { + C.glUseProgram(C.GLuint(p.V)) +} + +func (f *Functions) UnmapBuffer(target Enum) bool { + r := C.gio_glUnmapBuffer(C.GLenum(target)) + return r == C.GL_TRUE +} + +func (f *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride int, offset int) { + var n C.GLboolean = C.GL_FALSE + if normalized { + n = C.GL_TRUE + } + C.gio_glVertexAttribPointer(C.GLuint(dst), C.GLint(size), C.GLenum(ty), n, C.GLsizei(stride), C.uintptr_t(offset)) +} + +func (f *Functions) Viewport(x int, y int, width int, height int) { + C.glViewport(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height)) +} diff --git a/gio/giold/internal/gl/gl_windows.go b/gio/giold/internal/gl/gl_windows.go new file mode 100644 index 0000000..099c82b --- /dev/null +++ b/gio/giold/internal/gl/gl_windows.go @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import ( + "math" + "runtime" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + LibGLESv2 = windows.NewLazyDLL("libGLESv2.dll") + _glActiveTexture = LibGLESv2.NewProc("glActiveTexture") + _glAttachShader = LibGLESv2.NewProc("glAttachShader") + _glBeginQuery = LibGLESv2.NewProc("glBeginQuery") + _glBindAttribLocation = LibGLESv2.NewProc("glBindAttribLocation") + _glBindBuffer = LibGLESv2.NewProc("glBindBuffer") + _glBindBufferBase = LibGLESv2.NewProc("glBindBufferBase") + _glBindFramebuffer = LibGLESv2.NewProc("glBindFramebuffer") + _glBindRenderbuffer = LibGLESv2.NewProc("glBindRenderbuffer") + _glBindTexture = LibGLESv2.NewProc("glBindTexture") + _glBlendEquation = LibGLESv2.NewProc("glBlendEquation") + _glBlendFunc = LibGLESv2.NewProc("glBlendFunc") + _glBufferData = LibGLESv2.NewProc("glBufferData") + _glBufferSubData = LibGLESv2.NewProc("glBufferSubData") + _glCheckFramebufferStatus = LibGLESv2.NewProc("glCheckFramebufferStatus") + _glClear = LibGLESv2.NewProc("glClear") + _glClearColor = LibGLESv2.NewProc("glClearColor") + _glClearDepthf = LibGLESv2.NewProc("glClearDepthf") + _glDeleteQueries = LibGLESv2.NewProc("glDeleteQueries") + _glCompileShader = LibGLESv2.NewProc("glCompileShader") + _glGenBuffers = LibGLESv2.NewProc("glGenBuffers") + _glGenFramebuffers = LibGLESv2.NewProc("glGenFramebuffers") + _glGetUniformBlockIndex = LibGLESv2.NewProc("glGetUniformBlockIndex") + _glCreateProgram = LibGLESv2.NewProc("glCreateProgram") + _glGenRenderbuffers = LibGLESv2.NewProc("glGenRenderbuffers") + _glCreateShader = LibGLESv2.NewProc("glCreateShader") + _glGenTextures = LibGLESv2.NewProc("glGenTextures") + _glDeleteBuffers = LibGLESv2.NewProc("glDeleteBuffers") + _glDeleteFramebuffers = LibGLESv2.NewProc("glDeleteFramebuffers") + _glDeleteProgram = LibGLESv2.NewProc("glDeleteProgram") + _glDeleteShader = LibGLESv2.NewProc("glDeleteShader") + _glDeleteRenderbuffers = LibGLESv2.NewProc("glDeleteRenderbuffers") + _glDeleteTextures = LibGLESv2.NewProc("glDeleteTextures") + _glDepthFunc = LibGLESv2.NewProc("glDepthFunc") + _glDepthMask = LibGLESv2.NewProc("glDepthMask") + _glDisableVertexAttribArray = LibGLESv2.NewProc("glDisableVertexAttribArray") + _glDisable = LibGLESv2.NewProc("glDisable") + _glDrawArrays = LibGLESv2.NewProc("glDrawArrays") + _glDrawElements = LibGLESv2.NewProc("glDrawElements") + _glEnable = LibGLESv2.NewProc("glEnable") + _glEnableVertexAttribArray = LibGLESv2.NewProc("glEnableVertexAttribArray") + _glEndQuery = LibGLESv2.NewProc("glEndQuery") + _glFinish = LibGLESv2.NewProc("glFinish") + _glFramebufferRenderbuffer = LibGLESv2.NewProc("glFramebufferRenderbuffer") + _glFramebufferTexture2D = LibGLESv2.NewProc("glFramebufferTexture2D") + _glGenQueries = LibGLESv2.NewProc("glGenQueries") + _glGetError = LibGLESv2.NewProc("glGetError") + _glGetRenderbufferParameteri = LibGLESv2.NewProc("glGetRenderbufferParameteri") + _glGetFramebufferAttachmentParameteri = LibGLESv2.NewProc("glGetFramebufferAttachmentParameteri") + _glGetIntegerv = LibGLESv2.NewProc("glGetIntegerv") + _glGetProgramiv = LibGLESv2.NewProc("glGetProgramiv") + _glGetProgramInfoLog = LibGLESv2.NewProc("glGetProgramInfoLog") + _glGetQueryObjectuiv = LibGLESv2.NewProc("glGetQueryObjectuiv") + _glGetShaderiv = LibGLESv2.NewProc("glGetShaderiv") + _glGetShaderInfoLog = LibGLESv2.NewProc("glGetShaderInfoLog") + _glGetString = LibGLESv2.NewProc("glGetString") + _glGetUniformLocation = LibGLESv2.NewProc("glGetUniformLocation") + _glInvalidateFramebuffer = LibGLESv2.NewProc("glInvalidateFramebuffer") + _glLinkProgram = LibGLESv2.NewProc("glLinkProgram") + _glPixelStorei = LibGLESv2.NewProc("glPixelStorei") + _glReadPixels = LibGLESv2.NewProc("glReadPixels") + _glRenderbufferStorage = LibGLESv2.NewProc("glRenderbufferStorage") + _glScissor = LibGLESv2.NewProc("glScissor") + _glShaderSource = LibGLESv2.NewProc("glShaderSource") + _glTexImage2D = LibGLESv2.NewProc("glTexImage2D") + _glTexStorage2D = LibGLESv2.NewProc("glTexStorage2D") + _glTexSubImage2D = LibGLESv2.NewProc("glTexSubImage2D") + _glTexParameteri = LibGLESv2.NewProc("glTexParameteri") + _glUniformBlockBinding = LibGLESv2.NewProc("glUniformBlockBinding") + _glUniform1f = LibGLESv2.NewProc("glUniform1f") + _glUniform1i = LibGLESv2.NewProc("glUniform1i") + _glUniform2f = LibGLESv2.NewProc("glUniform2f") + _glUniform3f = LibGLESv2.NewProc("glUniform3f") + _glUniform4f = LibGLESv2.NewProc("glUniform4f") + _glUseProgram = LibGLESv2.NewProc("glUseProgram") + _glVertexAttribPointer = LibGLESv2.NewProc("glVertexAttribPointer") + _glViewport = LibGLESv2.NewProc("glViewport") +) + +type Functions struct { + // Query caches. + int32s [100]int32 +} + +type Context interface{} + +func NewFunctions(ctx Context) (*Functions, error) { + if ctx != nil { + panic("non-nil context") + } + return new(Functions), nil +} + +func (c *Functions) ActiveTexture(t Enum) { + syscall.Syscall(_glActiveTexture.Addr(), 1, uintptr(t), 0, 0) +} +func (c *Functions) AttachShader(p Program, s Shader) { + syscall.Syscall(_glAttachShader.Addr(), 2, uintptr(p.V), uintptr(s.V), 0) +} +func (f *Functions) BeginQuery(target Enum, query Query) { + syscall.Syscall(_glBeginQuery.Addr(), 2, uintptr(target), uintptr(query.V), 0) +} +func (c *Functions) BindAttribLocation(p Program, a Attrib, name string) { + cname := cString(name) + c0 := &cname[0] + syscall.Syscall(_glBindAttribLocation.Addr(), 3, uintptr(p.V), uintptr(a), uintptr(unsafe.Pointer(c0))) + issue34474KeepAlive(c) +} +func (c *Functions) BindBuffer(target Enum, b Buffer) { + syscall.Syscall(_glBindBuffer.Addr(), 2, uintptr(target), uintptr(b.V), 0) +} +func (c *Functions) BindBufferBase(target Enum, index int, b Buffer) { + syscall.Syscall(_glBindBufferBase.Addr(), 3, uintptr(target), uintptr(index), uintptr(b.V)) +} +func (c *Functions) BindFramebuffer(target Enum, fb Framebuffer) { + syscall.Syscall(_glBindFramebuffer.Addr(), 2, uintptr(target), uintptr(fb.V), 0) +} +func (c *Functions) BindRenderbuffer(target Enum, rb Renderbuffer) { + syscall.Syscall(_glBindRenderbuffer.Addr(), 2, uintptr(target), uintptr(rb.V), 0) +} +func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) { + panic("not implemented") +} +func (c *Functions) BindTexture(target Enum, t Texture) { + syscall.Syscall(_glBindTexture.Addr(), 2, uintptr(target), uintptr(t.V), 0) +} +func (c *Functions) BlendEquation(mode Enum) { + syscall.Syscall(_glBlendEquation.Addr(), 1, uintptr(mode), 0, 0) +} +func (c *Functions) BlendFunc(sfactor, dfactor Enum) { + syscall.Syscall(_glBlendFunc.Addr(), 2, uintptr(sfactor), uintptr(dfactor), 0) +} +func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) { + panic("not implemented") +} +func (c *Functions) BufferData(target Enum, size int, usage Enum) { + syscall.Syscall6(_glBufferData.Addr(), 4, uintptr(target), uintptr(size), 0, uintptr(usage), 0, 0) +} +func (f *Functions) BufferSubData(target Enum, offset int, src []byte) { + if n := len(src); n > 0 { + s0 := &src[0] + syscall.Syscall6(_glBufferSubData.Addr(), 4, uintptr(target), uintptr(offset), uintptr(n), uintptr(unsafe.Pointer(s0)), 0, 0) + issue34474KeepAlive(s0) + } +} +func (c *Functions) CheckFramebufferStatus(target Enum) Enum { + s, _, _ := syscall.Syscall(_glCheckFramebufferStatus.Addr(), 1, uintptr(target), 0, 0) + return Enum(s) +} +func (c *Functions) Clear(mask Enum) { + syscall.Syscall(_glClear.Addr(), 1, uintptr(mask), 0, 0) +} +func (c *Functions) ClearColor(red, green, blue, alpha float32) { + syscall.Syscall6(_glClearColor.Addr(), 4, uintptr(math.Float32bits(red)), uintptr(math.Float32bits(green)), uintptr(math.Float32bits(blue)), uintptr(math.Float32bits(alpha)), 0, 0) +} +func (c *Functions) ClearDepthf(d float32) { + syscall.Syscall(_glClearDepthf.Addr(), 1, uintptr(math.Float32bits(d)), 0, 0) +} +func (c *Functions) CompileShader(s Shader) { + syscall.Syscall(_glCompileShader.Addr(), 1, uintptr(s.V), 0, 0) +} +func (c *Functions) CreateBuffer() Buffer { + var buf uintptr + syscall.Syscall(_glGenBuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&buf)), 0) + return Buffer{uint(buf)} +} +func (c *Functions) CreateFramebuffer() Framebuffer { + var fb uintptr + syscall.Syscall(_glGenFramebuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&fb)), 0) + return Framebuffer{uint(fb)} +} +func (c *Functions) CreateProgram() Program { + p, _, _ := syscall.Syscall(_glCreateProgram.Addr(), 0, 0, 0, 0) + return Program{uint(p)} +} +func (f *Functions) CreateQuery() Query { + var q uintptr + syscall.Syscall(_glGenQueries.Addr(), 2, 1, uintptr(unsafe.Pointer(&q)), 0) + return Query{uint(q)} +} +func (c *Functions) CreateRenderbuffer() Renderbuffer { + var rb uintptr + syscall.Syscall(_glGenRenderbuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&rb)), 0) + return Renderbuffer{uint(rb)} +} +func (c *Functions) CreateShader(ty Enum) Shader { + s, _, _ := syscall.Syscall(_glCreateShader.Addr(), 1, uintptr(ty), 0, 0) + return Shader{uint(s)} +} +func (c *Functions) CreateTexture() Texture { + var t uintptr + syscall.Syscall(_glGenTextures.Addr(), 2, 1, uintptr(unsafe.Pointer(&t)), 0) + return Texture{uint(t)} +} +func (c *Functions) DeleteBuffer(v Buffer) { + syscall.Syscall(_glDeleteBuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v)), 0) +} +func (c *Functions) DeleteFramebuffer(v Framebuffer) { + syscall.Syscall(_glDeleteFramebuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0) +} +func (c *Functions) DeleteProgram(p Program) { + syscall.Syscall(_glDeleteProgram.Addr(), 1, uintptr(p.V), 0, 0) +} +func (f *Functions) DeleteQuery(query Query) { + syscall.Syscall(_glDeleteQueries.Addr(), 2, 1, uintptr(unsafe.Pointer(&query.V)), 0) +} +func (c *Functions) DeleteShader(s Shader) { + syscall.Syscall(_glDeleteShader.Addr(), 1, uintptr(s.V), 0, 0) +} +func (c *Functions) DeleteRenderbuffer(v Renderbuffer) { + syscall.Syscall(_glDeleteRenderbuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0) +} +func (c *Functions) DeleteTexture(v Texture) { + syscall.Syscall(_glDeleteTextures.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0) +} +func (c *Functions) DepthFunc(f Enum) { + syscall.Syscall(_glDepthFunc.Addr(), 1, uintptr(f), 0, 0) +} +func (c *Functions) DepthMask(mask bool) { + var m uintptr + if mask { + m = 1 + } + syscall.Syscall(_glDepthMask.Addr(), 1, m, 0, 0) +} +func (c *Functions) DisableVertexAttribArray(a Attrib) { + syscall.Syscall(_glDisableVertexAttribArray.Addr(), 1, uintptr(a), 0, 0) +} +func (c *Functions) Disable(cap Enum) { + syscall.Syscall(_glDisable.Addr(), 1, uintptr(cap), 0, 0) +} +func (c *Functions) DrawArrays(mode Enum, first, count int) { + syscall.Syscall(_glDrawArrays.Addr(), 3, uintptr(mode), uintptr(first), uintptr(count)) +} +func (c *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { + syscall.Syscall6(_glDrawElements.Addr(), 4, uintptr(mode), uintptr(count), uintptr(ty), uintptr(offset), 0, 0) +} +func (f *Functions) DispatchCompute(x, y, z int) { + panic("not implemented") +} +func (c *Functions) Enable(cap Enum) { + syscall.Syscall(_glEnable.Addr(), 1, uintptr(cap), 0, 0) +} +func (c *Functions) EnableVertexAttribArray(a Attrib) { + syscall.Syscall(_glEnableVertexAttribArray.Addr(), 1, uintptr(a), 0, 0) +} +func (f *Functions) EndQuery(target Enum) { + syscall.Syscall(_glEndQuery.Addr(), 1, uintptr(target), 0, 0) +} +func (c *Functions) Finish() { + syscall.Syscall(_glFinish.Addr(), 0, 0, 0, 0) +} +func (c *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { + syscall.Syscall6(_glFramebufferRenderbuffer.Addr(), 4, uintptr(target), uintptr(attachment), uintptr(renderbuffertarget), uintptr(renderbuffer.V), 0, 0) +} +func (c *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { + syscall.Syscall6(_glFramebufferTexture2D.Addr(), 5, uintptr(target), uintptr(attachment), uintptr(texTarget), uintptr(t.V), uintptr(level), 0) +} +func (f *Functions) GetUniformBlockIndex(p Program, name string) uint { + cname := cString(name) + c0 := &cname[0] + u, _, _ := syscall.Syscall(_glGetUniformBlockIndex.Addr(), 2, uintptr(p.V), uintptr(unsafe.Pointer(c0)), 0) + issue34474KeepAlive(c0) + return uint(u) +} +func (c *Functions) GetBinding(pname Enum) Object { + return Object{uint(c.GetInteger(pname))} +} +func (c *Functions) GetError() Enum { + e, _, _ := syscall.Syscall(_glGetError.Addr(), 0, 0, 0, 0) + return Enum(e) +} +func (c *Functions) GetRenderbufferParameteri(target, pname Enum) int { + p, _, _ := syscall.Syscall(_glGetRenderbufferParameteri.Addr(), 2, uintptr(target), uintptr(pname), 0) + return int(p) +} +func (c *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { + p, _, _ := syscall.Syscall(_glGetFramebufferAttachmentParameteri.Addr(), 3, uintptr(target), uintptr(attachment), uintptr(pname)) + return int(p) +} +func (c *Functions) GetInteger(pname Enum) int { + syscall.Syscall(_glGetIntegerv.Addr(), 2, uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])), 0) + return int(c.int32s[0]) +} +func (c *Functions) GetProgrami(p Program, pname Enum) int { + syscall.Syscall(_glGetProgramiv.Addr(), 3, uintptr(p.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0]))) + return int(c.int32s[0]) +} +func (c *Functions) GetProgramInfoLog(p Program) string { + n := c.GetProgrami(p, INFO_LOG_LENGTH) + buf := make([]byte, n) + syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0) + return string(buf) +} +func (c *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { + syscall.Syscall(_glGetQueryObjectuiv.Addr(), 3, uintptr(query.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0]))) + return uint(c.int32s[0]) +} +func (c *Functions) GetShaderi(s Shader, pname Enum) int { + syscall.Syscall(_glGetShaderiv.Addr(), 3, uintptr(s.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0]))) + return int(c.int32s[0]) +} +func (c *Functions) GetShaderInfoLog(s Shader) string { + n := c.GetShaderi(s, INFO_LOG_LENGTH) + buf := make([]byte, n) + syscall.Syscall6(_glGetShaderInfoLog.Addr(), 4, uintptr(s.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0) + return string(buf) +} +func (c *Functions) GetString(pname Enum) string { + s, _, _ := syscall.Syscall(_glGetString.Addr(), 1, uintptr(pname), 0, 0) + return windows.BytePtrToString((*byte)(unsafe.Pointer(s))) +} +func (c *Functions) GetUniformLocation(p Program, name string) Uniform { + cname := cString(name) + c0 := &cname[0] + u, _, _ := syscall.Syscall(_glGetUniformLocation.Addr(), 2, uintptr(p.V), uintptr(unsafe.Pointer(c0)), 0) + issue34474KeepAlive(c0) + return Uniform{int(u)} +} +func (c *Functions) InvalidateFramebuffer(target, attachment Enum) { + addr := _glInvalidateFramebuffer.Addr() + if addr == 0 { + // InvalidateFramebuffer is just a hint. Skip it if not supported. + return + } + syscall.Syscall(addr, 3, uintptr(target), 1, uintptr(unsafe.Pointer(&attachment))) +} +func (c *Functions) LinkProgram(p Program) { + syscall.Syscall(_glLinkProgram.Addr(), 1, uintptr(p.V), 0, 0) +} +func (c *Functions) PixelStorei(pname Enum, param int32) { + syscall.Syscall(_glPixelStorei.Addr(), 2, uintptr(pname), uintptr(param), 0) +} +func (f *Functions) MemoryBarrier(barriers Enum) { + panic("not implemented") +} +func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte { + panic("not implemented") +} +func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) { + d0 := &data[0] + syscall.Syscall9(_glReadPixels.Addr(), 7, uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(format), uintptr(ty), uintptr(unsafe.Pointer(d0)), 0, 0) + issue34474KeepAlive(d0) +} +func (c *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { + syscall.Syscall6(_glRenderbufferStorage.Addr(), 4, uintptr(target), uintptr(internalformat), uintptr(width), uintptr(height), 0, 0) +} +func (c *Functions) Scissor(x, y, width, height int32) { + syscall.Syscall6(_glScissor.Addr(), 4, uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0, 0) +} +func (c *Functions) ShaderSource(s Shader, src string) { + var n uintptr = uintptr(len(src)) + psrc := &src + syscall.Syscall6(_glShaderSource.Addr(), 4, uintptr(s.V), 1, uintptr(unsafe.Pointer(psrc)), uintptr(unsafe.Pointer(&n)), 0, 0) + issue34474KeepAlive(psrc) +} +func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width int, height int, format Enum, ty Enum) { + syscall.Syscall9(_glTexImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(internalFormat), uintptr(width), uintptr(height), 0, uintptr(format), uintptr(ty), 0) +} +func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) { + syscall.Syscall6(_glTexStorage2D.Addr(), 5, uintptr(target), uintptr(levels), uintptr(internalFormat), uintptr(width), uintptr(height), 0) +} +func (c *Functions) TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) { + d0 := &data[0] + syscall.Syscall9(_glTexSubImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(format), uintptr(ty), uintptr(unsafe.Pointer(d0))) + issue34474KeepAlive(d0) +} +func (c *Functions) TexParameteri(target, pname Enum, param int) { + syscall.Syscall(_glTexParameteri.Addr(), 3, uintptr(target), uintptr(pname), uintptr(param)) +} +func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) { + syscall.Syscall(_glUniformBlockBinding.Addr(), 3, uintptr(p.V), uintptr(uniformBlockIndex), uintptr(uniformBlockBinding)) +} +func (c *Functions) Uniform1f(dst Uniform, v float32) { + syscall.Syscall(_glUniform1f.Addr(), 2, uintptr(dst.V), uintptr(math.Float32bits(v)), 0) +} +func (c *Functions) Uniform1i(dst Uniform, v int) { + syscall.Syscall(_glUniform1i.Addr(), 2, uintptr(dst.V), uintptr(v), 0) +} +func (c *Functions) Uniform2f(dst Uniform, v0, v1 float32) { + syscall.Syscall(_glUniform2f.Addr(), 3, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1))) +} +func (c *Functions) Uniform3f(dst Uniform, v0, v1, v2 float32) { + syscall.Syscall6(_glUniform3f.Addr(), 4, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)), uintptr(math.Float32bits(v2)), 0, 0) +} +func (c *Functions) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) { + syscall.Syscall6(_glUniform4f.Addr(), 5, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)), uintptr(math.Float32bits(v2)), uintptr(math.Float32bits(v3)), 0) +} +func (c *Functions) UseProgram(p Program) { + syscall.Syscall(_glUseProgram.Addr(), 1, uintptr(p.V), 0, 0) +} +func (f *Functions) UnmapBuffer(target Enum) bool { + panic("not implemented") +} +func (c *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) { + var norm uintptr + if normalized { + norm = 1 + } + syscall.Syscall6(_glVertexAttribPointer.Addr(), 6, uintptr(dst), uintptr(size), uintptr(ty), norm, uintptr(stride), uintptr(offset)) +} +func (c *Functions) Viewport(x, y, width, height int) { + syscall.Syscall6(_glViewport.Addr(), 4, uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0, 0) +} + +func cString(s string) []byte { + b := make([]byte, len(s)+1) + copy(b, s) + return b +} + +// issue34474KeepAlive calls runtime.KeepAlive as a +// workaround for golang.org/issue/34474. +func issue34474KeepAlive(v interface{}) { + runtime.KeepAlive(v) +} diff --git a/gio/giold/internal/gl/types.go b/gio/giold/internal/gl/types.go new file mode 100644 index 0000000..45db3be --- /dev/null +++ b/gio/giold/internal/gl/types.go @@ -0,0 +1,27 @@ +// +build !js + +package gl + +type ( + Buffer struct{ V uint } + Framebuffer struct{ V uint } + Program struct{ V uint } + Renderbuffer struct{ V uint } + Shader struct{ V uint } + Texture struct{ V uint } + Query struct{ V uint } + Uniform struct{ V int } + Object struct{ V uint } +) + +func (u Uniform) Valid() bool { + return u.V != -1 +} + +func (p Program) Valid() bool { + return p.V != 0 +} + +func (s Shader) Valid() bool { + return s.V != 0 +} diff --git a/gio/giold/internal/gl/types_js.go b/gio/giold/internal/gl/types_js.go new file mode 100644 index 0000000..584c2af --- /dev/null +++ b/gio/giold/internal/gl/types_js.go @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import "syscall/js" + +type ( + Buffer js.Value + Framebuffer js.Value + Program js.Value + Renderbuffer js.Value + Shader js.Value + Texture js.Value + Query js.Value + Uniform js.Value + Object js.Value +) + +func (p Program) Valid() bool { + return !js.Value(p).IsUndefined() && !js.Value(p).IsNull() +} + +func (s Shader) Valid() bool { + return !js.Value(s).IsUndefined() && !js.Value(s).IsNull() +} + +func (u Uniform) Valid() bool { + return !js.Value(u).IsUndefined() && !js.Value(u).IsNull() +} diff --git a/gio/giold/internal/gl/util.go b/gio/giold/internal/gl/util.go new file mode 100644 index 0000000..3d5b44b --- /dev/null +++ b/gio/giold/internal/gl/util.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import ( + "errors" + "fmt" + "strings" +) + +func CreateProgram(ctx *Functions, vsSrc, fsSrc string, attribs []string) (Program, error) { + vs, err := createShader(ctx, VERTEX_SHADER, vsSrc) + if err != nil { + return Program{}, err + } + defer ctx.DeleteShader(vs) + fs, err := createShader(ctx, FRAGMENT_SHADER, fsSrc) + if err != nil { + return Program{}, err + } + defer ctx.DeleteShader(fs) + prog := ctx.CreateProgram() + if !prog.Valid() { + return Program{}, errors.New("glCreateProgram failed") + } + ctx.AttachShader(prog, vs) + ctx.AttachShader(prog, fs) + for i, a := range attribs { + ctx.BindAttribLocation(prog, Attrib(i), a) + } + ctx.LinkProgram(prog) + if ctx.GetProgrami(prog, LINK_STATUS) == 0 { + log := ctx.GetProgramInfoLog(prog) + ctx.DeleteProgram(prog) + return Program{}, fmt.Errorf("program link failed: %s", strings.TrimSpace(log)) + } + return prog, nil +} + +func CreateComputeProgram(ctx *Functions, src string) (Program, error) { + cs, err := createShader(ctx, COMPUTE_SHADER, src) + if err != nil { + return Program{}, err + } + defer ctx.DeleteShader(cs) + prog := ctx.CreateProgram() + if !prog.Valid() { + return Program{}, errors.New("glCreateProgram failed") + } + ctx.AttachShader(prog, cs) + ctx.LinkProgram(prog) + if ctx.GetProgrami(prog, LINK_STATUS) == 0 { + log := ctx.GetProgramInfoLog(prog) + ctx.DeleteProgram(prog) + return Program{}, fmt.Errorf("program link failed: %s", strings.TrimSpace(log)) + } + return prog, nil +} + +func createShader(ctx *Functions, typ Enum, src string) (Shader, error) { + sh := ctx.CreateShader(typ) + if !sh.Valid() { + return Shader{}, errors.New("glCreateShader failed") + } + ctx.ShaderSource(sh, src) + ctx.CompileShader(sh) + if ctx.GetShaderi(sh, COMPILE_STATUS) == 0 { + log := ctx.GetShaderInfoLog(sh) + ctx.DeleteShader(sh) + return Shader{}, fmt.Errorf("shader compilation failed: %s", strings.TrimSpace(log)) + } + return sh, nil +} + +func ParseGLVersion(glVer string) (version [2]int, gles bool, err error) { + var ver [2]int + if _, err := fmt.Sscanf(glVer, "OpenGL ES %d.%d", &ver[0], &ver[1]); err == nil { + return ver, true, nil + } else if _, err := fmt.Sscanf(glVer, "WebGL %d.%d", &ver[0], &ver[1]); err == nil { + // WebGL major version v corresponds to OpenGL ES version v + 1 + ver[0]++ + return ver, true, nil + } else if _, err := fmt.Sscanf(glVer, "%d.%d", &ver[0], &ver[1]); err == nil { + return ver, false, nil + } + return ver, false, fmt.Errorf("failed to parse OpenGL ES version (%s)", glVer) +} diff --git a/gio/giold/internal/opconst/ops.go b/gio/giold/internal/opconst/ops.go new file mode 100644 index 0000000..db9dd8d --- /dev/null +++ b/gio/giold/internal/opconst/ops.go @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package opconst + +type OpType byte + +// Start at a high number for easier debugging. +const firstOpIndex = 200 + +const ( + TypeMacro OpType = iota + firstOpIndex + TypeCall + TypeDefer + TypeTransform + TypeInvalidate + TypeImage + TypePaint + TypeColor + TypeLinearGradient + TypeArea + TypePointerInput + TypePass + TypeClipboardRead + TypeClipboardWrite + TypeKeyInput + TypeKeyFocus + TypeKeySoftKeyboard + TypeSave + TypeLoad + TypeAux + TypeClip + TypeProfile + TypeCursor + TypePath + TypeStroke +) + +const ( + TypeMacroLen = 1 + 4 + 4 + TypeCallLen = 1 + 4 + 4 + TypeDeferLen = 1 + TypeTransformLen = 1 + 4*6 + TypeRedrawLen = 1 + 8 + TypeImageLen = 1 + TypePaintLen = 1 + TypeColorLen = 1 + 4 + TypeLinearGradientLen = 1 + 8*2 + 4*2 + TypeAreaLen = 1 + 1 + 4*4 + TypePointerInputLen = 1 + 1 + 1 + 2*4 + 2*4 + TypePassLen = 1 + 1 + TypeClipboardReadLen = 1 + TypeClipboardWriteLen = 1 + TypeKeyInputLen = 1 + TypeKeyFocusLen = 1 + TypeKeySoftKeyboardLen = 1 + 1 + TypeSaveLen = 1 + 4 + TypeLoadLen = 1 + 1 + 4 + TypeAuxLen = 1 + TypeClipLen = 1 + 4*4 + 1 + TypeProfileLen = 1 + TypeCursorLen = 1 + 1 + TypePathLen = 1 + TypeStrokeLen = 1 + 4 +) + +// StateMask is a bitmask of state types a load operation +// should restore. +type StateMask uint8 + +const ( + TransformState StateMask = 1 << iota + + AllState = ^StateMask(0) +) + +// InitialStateID is the ID for saving and loading +// the initial operation state. +const InitialStateID = 0 + +func (t OpType) Size() int { + return [...]int{ + TypeMacroLen, + TypeCallLen, + TypeDeferLen, + TypeTransformLen, + TypeRedrawLen, + TypeImageLen, + TypePaintLen, + TypeColorLen, + TypeLinearGradientLen, + TypeAreaLen, + TypePointerInputLen, + TypePassLen, + TypeClipboardReadLen, + TypeClipboardWriteLen, + TypeKeyInputLen, + TypeKeyFocusLen, + TypeKeySoftKeyboardLen, + TypeSaveLen, + TypeLoadLen, + TypeAuxLen, + TypeClipLen, + TypeProfileLen, + TypeCursorLen, + TypePathLen, + TypeStrokeLen, + }[t-firstOpIndex] +} + +func (t OpType) NumRefs() int { + switch t { + case TypeKeyInput, TypeKeyFocus, TypePointerInput, TypeProfile, TypeCall, TypeClipboardRead, TypeClipboardWrite, TypeCursor: + return 1 + case TypeImage: + return 2 + default: + return 0 + } +} diff --git a/gio/giold/internal/ops/ops.go b/gio/giold/internal/ops/ops.go new file mode 100644 index 0000000..a25839f --- /dev/null +++ b/gio/giold/internal/ops/ops.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package ops + +import ( + "encoding/binary" + "math" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/byteslice" + "realy.lol/gio/internal/opconst" + "realy.lol/gio/internal/scene" +) + +func DecodeCommand(d []byte) scene.Command { + var cmd scene.Command + copy(byteslice.Uint32(cmd[:]), d) + return cmd +} + +func EncodeCommand(out []byte, cmd scene.Command) { + copy(out, byteslice.Uint32(cmd[:])) +} + +func DecodeTransform(data []byte) (t f32.Affine2D) { + if opconst.OpType(data[0]) != opconst.TypeTransform { + panic("invalid op") + } + data = data[1:] + data = data[:4*6] + + bo := binary.LittleEndian + a := math.Float32frombits(bo.Uint32(data)) + b := math.Float32frombits(bo.Uint32(data[4*1:])) + c := math.Float32frombits(bo.Uint32(data[4*2:])) + d := math.Float32frombits(bo.Uint32(data[4*3:])) + e := math.Float32frombits(bo.Uint32(data[4*4:])) + f := math.Float32frombits(bo.Uint32(data[4*5:])) + return f32.NewAffine2D(a, b, c, d, e, f) +} + +// DecodeSave decodes the state id of a save op. +func DecodeSave(data []byte) int { + if opconst.OpType(data[0]) != opconst.TypeSave { + panic("invalid op") + } + bo := binary.LittleEndian + return int(bo.Uint32(data[1:])) +} + +// DecodeLoad decodes the state id and mask of a load op. +func DecodeLoad(data []byte) (int, opconst.StateMask) { + if opconst.OpType(data[0]) != opconst.TypeLoad { + panic("invalid op") + } + bo := binary.LittleEndian + return int(bo.Uint32(data[2:])), opconst.StateMask(data[1]) +} diff --git a/gio/giold/internal/ops/reader.go b/gio/giold/internal/ops/reader.go new file mode 100644 index 0000000..8465446 --- /dev/null +++ b/gio/giold/internal/ops/reader.go @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package ops + +import ( + "encoding/binary" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/opconst" + "realy.lol/gio/op" +) + +// Reader parses an ops list. +type Reader struct { + pc PC + stack []macro + ops *op.Ops + deferOps op.Ops + deferDone bool +} + +// EncodedOp represents an encoded op returned by +// Reader. +type EncodedOp struct { + Key Key + Data []byte + Refs []interface{} +} + +// Key is a unique key for a given op. +type Key struct { + ops *op.Ops + pc int + version int + sx, hx, sy, hy float32 +} + +// Shadow of op.MacroOp. +type macroOp struct { + ops *op.Ops + pc PC +} + +// PC is an instruction counter for an operation list. +type PC struct { + data int + refs int +} + +type macro struct { + ops *op.Ops + retPC PC + endPC PC +} + +type opMacroDef struct { + endpc PC +} + +// Reset start reading from the beginning of ops. +func (r *Reader) Reset(ops *op.Ops) { + r.ResetAt(ops, PC{}) +} + +// ResetAt is like Reset, except it starts reading from pc. +func (r *Reader) ResetAt(ops *op.Ops, pc PC) { + r.stack = r.stack[:0] + r.deferOps.Reset() + r.deferDone = false + r.pc = pc + r.ops = ops +} + +// NewPC returns a PC representing the current instruction counter of +// ops. +func NewPC(ops *op.Ops) PC { + return PC{ + data: len(ops.Data()), + refs: len(ops.Refs()), + } +} + +func (k Key) SetTransform(t f32.Affine2D) Key { + sx, hx, _, hy, sy, _ := t.Elems() + k.sx = sx + k.hx = hx + k.hy = hy + k.sy = sy + return k +} + +func (r *Reader) Decode() (EncodedOp, bool) { + if r.ops == nil { + return EncodedOp{}, false + } + deferring := false + for { + if len(r.stack) > 0 { + b := r.stack[len(r.stack)-1] + if r.pc == b.endPC { + r.ops = b.ops + r.pc = b.retPC + r.stack = r.stack[:len(r.stack)-1] + continue + } + } + data := r.ops.Data() + data = data[r.pc.data:] + refs := r.ops.Refs() + if len(data) == 0 { + if r.deferDone { + return EncodedOp{}, false + } + r.deferDone = true + // Execute deferred macros. + r.ops = &r.deferOps + r.pc = PC{} + continue + } + key := Key{ops: r.ops, pc: r.pc.data, version: r.ops.Version()} + t := opconst.OpType(data[0]) + n := t.Size() + nrefs := t.NumRefs() + data = data[:n] + refs = refs[r.pc.refs:] + refs = refs[:nrefs] + switch t { + case opconst.TypeDefer: + deferring = true + r.pc.data += n + r.pc.refs += nrefs + continue + case opconst.TypeAux: + // An Aux operations is always wrapped in a macro, and + // its length is the remaining space. + block := r.stack[len(r.stack)-1] + n += block.endPC.data - r.pc.data - opconst.TypeAuxLen + data = data[:n] + case opconst.TypeCall: + if deferring { + deferring = false + // Copy macro for deferred execution. + if t.NumRefs() != 1 { + panic("internal error: unexpected number of macro refs") + } + deferData := r.deferOps.Write1(t.Size(), refs[0]) + copy(deferData, data) + continue + } + var op macroOp + op.decode(data, refs) + macroData := op.ops.Data()[op.pc.data:] + if opconst.OpType(macroData[0]) != opconst.TypeMacro { + panic("invalid macro reference") + } + var opDef opMacroDef + opDef.decode(macroData[:opconst.TypeMacro.Size()]) + retPC := r.pc + retPC.data += n + retPC.refs += nrefs + r.stack = append(r.stack, macro{ + ops: r.ops, + retPC: retPC, + endPC: opDef.endpc, + }) + r.ops = op.ops + r.pc = op.pc + r.pc.data += opconst.TypeMacro.Size() + r.pc.refs += opconst.TypeMacro.NumRefs() + continue + case opconst.TypeMacro: + var op opMacroDef + op.decode(data) + r.pc = op.endpc + continue + } + r.pc.data += n + r.pc.refs += nrefs + return EncodedOp{Key: key, Data: data, Refs: refs}, true + } +} + +func (op *opMacroDef) decode(data []byte) { + if opconst.OpType(data[0]) != opconst.TypeMacro { + panic("invalid op") + } + bo := binary.LittleEndian + data = data[:9] + dataIdx := int(int32(bo.Uint32(data[1:]))) + refsIdx := int(int32(bo.Uint32(data[5:]))) + *op = opMacroDef{ + endpc: PC{ + data: dataIdx, + refs: refsIdx, + }, + } +} + +func (m *macroOp) decode(data []byte, refs []interface{}) { + if opconst.OpType(data[0]) != opconst.TypeCall { + panic("invalid op") + } + data = data[:9] + bo := binary.LittleEndian + dataIdx := int(int32(bo.Uint32(data[1:]))) + refsIdx := int(int32(bo.Uint32(data[5:]))) + *m = macroOp{ + ops: refs[0].(*op.Ops), + pc: PC{ + data: dataIdx, + refs: refsIdx, + }, + } +} diff --git a/gio/giold/internal/scene/scene.go b/gio/giold/internal/scene/scene.go new file mode 100644 index 0000000..8761a13 --- /dev/null +++ b/gio/giold/internal/scene/scene.go @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package scene encodes and decodes graphics commands in the format used by the +// compute renderer. +package scene + +import ( + "fmt" + "image/color" + "math" + "unsafe" + + "realy.lol/gio/f32" +) + +type Op uint32 + +type Command [sceneElemSize / 4]uint32 + +// GPU commands from scene.h +const ( + OpNop Op = iota + OpLine + OpQuad + OpCubic + OpFillColor + OpLineWidth + OpTransform + OpBeginClip + OpEndClip + OpFillImage + OpSetFillMode +) + +// FillModes, from setup.h. +type FillMode uint32 + +const ( + FillModeNonzero = 0 + FillModeStroke = 1 +) + +const CommandSize = int(unsafe.Sizeof(Command{})) + +const sceneElemSize = 36 + +func (c Command) Op() Op { + return Op(c[0]) +} + +func (c Command) String() string { + switch Op(c[0]) { + case OpNop: + return "nop" + case OpLine: + from, to := DecodeLine(c) + return fmt.Sprintf("line(%v, %v)", from, to) + case OpQuad: + from, ctrl, to := DecodeQuad(c) + return fmt.Sprintf("quad(%v, %v, %v)", from, ctrl, to) + case OpCubic: + from, ctrl0, ctrl1, to := DecodeCubic(c) + return fmt.Sprintf("cubic(%v, %v, %v, %v)", from, ctrl0, ctrl1, to) + case OpFillColor: + return "fillcolor" + case OpLineWidth: + return "linewidth" + case OpTransform: + t := f32.NewAffine2D( + math.Float32frombits(c[1]), + math.Float32frombits(c[3]), + math.Float32frombits(c[5]), + math.Float32frombits(c[2]), + math.Float32frombits(c[4]), + math.Float32frombits(c[6]), + ) + return fmt.Sprintf("transform (%v)", t) + case OpBeginClip: + bounds := f32.Rectangle{ + Min: f32.Pt(math.Float32frombits(c[1]), math.Float32frombits(c[2])), + Max: f32.Pt(math.Float32frombits(c[3]), math.Float32frombits(c[4])), + } + return fmt.Sprintf("beginclip (%v)", bounds) + case OpEndClip: + bounds := f32.Rectangle{ + Min: f32.Pt(math.Float32frombits(c[1]), math.Float32frombits(c[2])), + Max: f32.Pt(math.Float32frombits(c[3]), math.Float32frombits(c[4])), + } + return fmt.Sprintf("endclip (%v)", bounds) + case OpFillImage: + return "fillimage" + case OpSetFillMode: + return "setfillmode" + default: + panic("unreachable") + } +} + +func Line(start, end f32.Point) Command { + return Command{ + 0: uint32(OpLine), + 1: math.Float32bits(start.X), + 2: math.Float32bits(start.Y), + 3: math.Float32bits(end.X), + 4: math.Float32bits(end.Y), + } +} + +func Cubic(start, ctrl0, ctrl1, end f32.Point) Command { + return Command{ + 0: uint32(OpCubic), + 1: math.Float32bits(start.X), + 2: math.Float32bits(start.Y), + 3: math.Float32bits(ctrl0.X), + 4: math.Float32bits(ctrl0.Y), + 5: math.Float32bits(ctrl1.X), + 6: math.Float32bits(ctrl1.Y), + 7: math.Float32bits(end.X), + 8: math.Float32bits(end.Y), + } +} + +func Quad(start, ctrl, end f32.Point) Command { + return Command{ + 0: uint32(OpQuad), + 1: math.Float32bits(start.X), + 2: math.Float32bits(start.Y), + 3: math.Float32bits(ctrl.X), + 4: math.Float32bits(ctrl.Y), + 5: math.Float32bits(end.X), + 6: math.Float32bits(end.Y), + } +} + +func Transform(m f32.Affine2D) Command { + sx, hx, ox, hy, sy, oy := m.Elems() + return Command{ + 0: uint32(OpTransform), + 1: math.Float32bits(sx), + 2: math.Float32bits(hy), + 3: math.Float32bits(hx), + 4: math.Float32bits(sy), + 5: math.Float32bits(ox), + 6: math.Float32bits(oy), + } +} + +func SetLineWidth(width float32) Command { + return Command{ + 0: uint32(OpLineWidth), + 1: math.Float32bits(width), + } +} + +func BeginClip(bbox f32.Rectangle) Command { + return Command{ + 0: uint32(OpBeginClip), + 1: math.Float32bits(bbox.Min.X), + 2: math.Float32bits(bbox.Min.Y), + 3: math.Float32bits(bbox.Max.X), + 4: math.Float32bits(bbox.Max.Y), + } +} + +func EndClip(bbox f32.Rectangle) Command { + return Command{ + 0: uint32(OpEndClip), + 1: math.Float32bits(bbox.Min.X), + 2: math.Float32bits(bbox.Min.Y), + 3: math.Float32bits(bbox.Max.X), + 4: math.Float32bits(bbox.Max.Y), + } +} + +func FillColor(col color.RGBA) Command { + return Command{ + 0: uint32(OpFillColor), + 1: uint32(col.R)<<24 | uint32(col.G)<<16 | uint32(col.B)<<8 | uint32(col.A), + } +} + +func FillImage(index int) Command { + return Command{ + 0: uint32(OpFillImage), + 1: uint32(index), + } +} + +func SetFillMode(mode FillMode) Command { + return Command{ + 0: uint32(OpSetFillMode), + 1: uint32(mode), + } +} + +func DecodeLine(cmd Command) (from, to f32.Point) { + if cmd[0] != uint32(OpLine) { + panic("invalid command") + } + from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2])) + to = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4])) + return +} + +func DecodeQuad(cmd Command) (from, ctrl, to f32.Point) { + if cmd[0] != uint32(OpQuad) { + panic("invalid command") + } + from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2])) + ctrl = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4])) + to = f32.Pt(math.Float32frombits(cmd[5]), math.Float32frombits(cmd[6])) + return +} + +func DecodeCubic(cmd Command) (from, ctrl0, ctrl1, to f32.Point) { + if cmd[0] != uint32(OpCubic) { + panic("invalid command") + } + from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2])) + ctrl0 = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4])) + ctrl1 = f32.Pt(math.Float32frombits(cmd[5]), math.Float32frombits(cmd[6])) + to = f32.Pt(math.Float32frombits(cmd[7]), math.Float32frombits(cmd[8])) + return +} diff --git a/gio/giold/internal/srgb/srgb.go b/gio/giold/internal/srgb/srgb.go new file mode 100644 index 0000000..1cd67cf --- /dev/null +++ b/gio/giold/internal/srgb/srgb.go @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package srgb + +import ( + "fmt" + "runtime" + "strings" + + "realy.lol/gio/internal/byteslice" + "realy.lol/gio/internal/gl" +) + +// FBO implements an intermediate sRGB FBO +// for gamma-correct rendering on platforms without +// sRGB enabled native framebuffers. +type FBO struct { + c *gl.Functions + width, height int + frameBuffer gl.Framebuffer + depthBuffer gl.Renderbuffer + colorTex gl.Texture + blitted bool + quad gl.Buffer + prog gl.Program + gl3 bool +} + +func New(ctx gl.Context) (*FBO, error) { + f, err := gl.NewFunctions(ctx) + if err != nil { + return nil, err + } + var gl3 bool + glVer := f.GetString(gl.VERSION) + ver, _, err := gl.ParseGLVersion(glVer) + if err != nil { + return nil, err + } + if ver[0] >= 3 { + gl3 = true + } else { + exts := f.GetString(gl.EXTENSIONS) + if !strings.Contains(exts, "EXT_sRGB") { + return nil, fmt.Errorf("no support for OpenGL ES 3 nor EXT_sRGB") + } + } + s := &FBO{ + c: f, + gl3: gl3, + frameBuffer: f.CreateFramebuffer(), + colorTex: f.CreateTexture(), + depthBuffer: f.CreateRenderbuffer(), + } + f.BindTexture(gl.TEXTURE_2D, s.colorTex) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) + return s, nil +} + +func (s *FBO) Blit() { + if !s.blitted { + prog, err := gl.CreateProgram(s.c, blitVSrc, blitFSrc, + []string{"pos", "uv"}) + if err != nil { + panic(err) + } + s.prog = prog + s.c.UseProgram(prog) + s.c.Uniform1i(s.c.GetUniformLocation(prog, "tex"), 0) + s.quad = s.c.CreateBuffer() + s.c.BindBuffer(gl.ARRAY_BUFFER, s.quad) + coords := byteslice.Slice([]float32{ + -1, +1, 0, 1, + +1, +1, 1, 1, + -1, -1, 0, 0, + +1, -1, 1, 0, + }) + s.c.BufferData(gl.ARRAY_BUFFER, len(coords), gl.STATIC_DRAW) + s.c.BufferSubData(gl.ARRAY_BUFFER, 0, coords) + s.blitted = true + } + s.c.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{}) + s.c.UseProgram(s.prog) + s.c.BindTexture(gl.TEXTURE_2D, s.colorTex) + s.c.BindBuffer(gl.ARRAY_BUFFER, s.quad) + s.c.VertexAttribPointer(0 /* pos */, 2, gl.FLOAT, false, 4*4, 0) + s.c.VertexAttribPointer(1 /* uv */, 2, gl.FLOAT, false, 4*4, 4*2) + s.c.EnableVertexAttribArray(0) + s.c.EnableVertexAttribArray(1) + s.c.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) + s.c.BindTexture(gl.TEXTURE_2D, gl.Texture{}) + s.c.DisableVertexAttribArray(0) + s.c.DisableVertexAttribArray(1) + s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer) + s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0) + s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT) + // The Android emulator requires framebuffer 0 bound at eglSwapBuffer time. + // Bind the sRGB framebuffer again in afterPresent. + s.c.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{}) +} + +func (s *FBO) AfterPresent() { + s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer) +} + +func (s *FBO) Refresh(w, h int) error { + s.width, s.height = w, h + if w == 0 || h == 0 { + return nil + } + s.c.BindTexture(gl.TEXTURE_2D, s.colorTex) + if s.gl3 { + s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB8_ALPHA8, w, h, gl.RGBA, + gl.UNSIGNED_BYTE) + } else /* EXT_sRGB */ { + s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB_ALPHA_EXT, w, h, + gl.SRGB_ALPHA_EXT, gl.UNSIGNED_BYTE) + } + currentRB := gl.Renderbuffer(s.c.GetBinding(gl.RENDERBUFFER_BINDING)) + s.c.BindRenderbuffer(gl.RENDERBUFFER, s.depthBuffer) + s.c.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h) + s.c.BindRenderbuffer(gl.RENDERBUFFER, currentRB) + s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer) + s.c.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, s.colorTex, 0) + s.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, + gl.RENDERBUFFER, s.depthBuffer) + if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { + return fmt.Errorf("sRGB framebuffer incomplete (%dx%d), status: %#x error: %x", + s.width, s.height, st, s.c.GetError()) + } + + if runtime.GOOS == "js" { + // With macOS Safari, rendering to and then reading from a SRGB8_ALPHA8 + // texture result in twice gamma corrected colors. Using a plain RGBA + // texture seems to work. + s.c.ClearColor(.5, .5, .5, 1.0) + s.c.Clear(gl.COLOR_BUFFER_BIT) + var pixel [4]byte + s.c.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel[:]) + if pixel[0] == 128 { // Correct sRGB color value is ~188 + s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, gl.RGBA, + gl.UNSIGNED_BYTE) + if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { + return fmt.Errorf("fallback RGBA framebuffer incomplete (%dx%d), status: %#x error: %x", + s.width, s.height, st, s.c.GetError()) + } + } + } + + return nil +} + +func (s *FBO) Release() { + s.c.DeleteFramebuffer(s.frameBuffer) + s.c.DeleteTexture(s.colorTex) + s.c.DeleteRenderbuffer(s.depthBuffer) + if s.blitted { + s.c.DeleteBuffer(s.quad) + s.c.DeleteProgram(s.prog) + } + s.c = nil +} + +const ( + blitVSrc = ` +#version 100 + +precision highp float; + +attribute vec2 pos; +attribute vec2 uv; + +varying vec2 vUV; + +void main() { + gl_Position = vec4(pos, 0, 1); + vUV = uv; +} +` + blitFSrc = ` +#version 100 + +precision mediump float; + +uniform sampler2D tex; +varying vec2 vUV; + +vec3 gamma(vec3 rgb) { + vec3 exp = vec3(1.055)*pow(rgb, vec3(0.41666)) - vec3(0.055); + vec3 lin = rgb * vec3(12.92); + bvec3 cut = lessThan(rgb, vec3(0.0031308)); + return vec3(cut.r ? lin.r : exp.r, cut.g ? lin.g : exp.g, cut.b ? lin.b : exp.b); +} + +void main() { + vec4 col = texture2D(tex, vUV); + vec3 rgb = col.rgb; + rgb = gamma(rgb); + gl_FragColor = vec4(rgb, col.a); +} +` +) diff --git a/gio/giold/internal/stroke/dash.go b/gio/giold/internal/stroke/dash.go new file mode 100644 index 0000000..c57a032 --- /dev/null +++ b/gio/giold/internal/stroke/dash.go @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// The algorithms to compute dashes have been extracted, adapted from +// (and used as a reference implementation): +// - github.com/tdewolff/canvas (Licensed under MIT) + +package stroke + +import ( + "math" + "sort" + + "realy.lol/gio/f32" +) + +type DashOp struct { + Phase float32 + Dashes []float32 +} + +func IsSolidLine(sty DashOp) bool { + return sty.Phase == 0 && len(sty.Dashes) == 0 +} + +func (qs StrokeQuads) dash(sty DashOp) StrokeQuads { + sty = dashCanonical(sty) + + switch { + case len(sty.Dashes) == 0: + return qs + case len(sty.Dashes) == 1 && sty.Dashes[0] == 0.0: + return StrokeQuads{} + } + + if len(sty.Dashes)%2 == 1 { + // If the dash pattern is of uneven length, dash and space lengths + // alternate. The following duplicates the pattern so that uneven + // indices are always spaces. + sty.Dashes = append(sty.Dashes, sty.Dashes...) + } + + var ( + i0, pos0 = dashStart(sty) + out StrokeQuads + + contour uint32 = 1 + ) + + for _, ps := range qs.split() { + var ( + i = i0 + pos = pos0 + t []float64 + length = ps.len() + ) + for pos+sty.Dashes[i] < length { + pos += sty.Dashes[i] + if 0.0 < pos { + t = append(t, float64(pos)) + } + i++ + if i == len(sty.Dashes) { + i = 0 + } + } + + j0 := 0 + endsInDash := i%2 == 0 + if len(t)%2 == 1 && endsInDash || len(t)%2 == 0 && !endsInDash { + j0 = 1 + } + + var ( + qd StrokeQuads + pd = ps.splitAt(&contour, t...) + ) + for j := j0; j < len(pd)-1; j += 2 { + qd = qd.append(pd[j]) + } + if endsInDash { + if ps.closed() { + qd = pd[len(pd)-1].append(qd) + } else { + qd = qd.append(pd[len(pd)-1]) + } + } + out = out.append(qd) + contour++ + } + return out +} + +func dashCanonical(sty DashOp) DashOp { + var ( + o = sty + ds = o.Dashes + ) + + if len(sty.Dashes) == 0 { + return sty + } + + // Remove zeros except first and last. + for i := 1; i < len(ds)-1; i++ { + if f32Eq(ds[i], 0.0) { + ds[i-1] += ds[i+1] + ds = append(ds[:i], ds[i+2:]...) + i-- + } + } + + // Remove first zero, collapse with second and last. + if f32Eq(ds[0], 0.0) { + if len(ds) < 3 { + return DashOp{ + Phase: 0.0, + Dashes: []float32{0.0}, + } + } + o.Phase -= ds[1] + ds[len(ds)-1] += ds[1] + ds = ds[2:] + } + + // Remove last zero, collapse with fist and second to last. + if f32Eq(ds[len(ds)-1], 0.0) { + if len(ds) < 3 { + return DashOp{} + } + o.Phase += ds[len(ds)-2] + ds[0] += ds[len(ds)-2] + ds = ds[:len(ds)-2] + } + + // If there are zeros or negatives, don't draw dashes. + for i := 0; i < len(ds); i++ { + if ds[i] < 0.0 || f32Eq(ds[i], 0.0) { + return DashOp{ + Phase: 0.0, + Dashes: []float32{0.0}, + } + } + } + + // Remove repeated patterns. +loop: + for len(ds)%2 == 0 { + mid := len(ds) / 2 + for i := 0; i < mid; i++ { + if !f32Eq(ds[i], ds[mid+i]) { + break loop + } + } + ds = ds[:mid] + } + return o +} + +func dashStart(sty DashOp) (int, float32) { + i0 := 0 // i0 is the index into dashes. + for sty.Dashes[i0] <= sty.Phase { + sty.Phase -= sty.Dashes[i0] + i0++ + if i0 == len(sty.Dashes) { + i0 = 0 + } + } + // pos0 may be negative if the offset lands halfway into dash. + pos0 := -sty.Phase + if sty.Phase < 0.0 { + var sum float32 + for _, d := range sty.Dashes { + sum += d + } + pos0 = -(sum + sty.Phase) // handle negative offsets + } + return i0, pos0 +} + +func (qs StrokeQuads) len() float32 { + var sum float32 + for i := range qs { + q := qs[i].Quad + sum += quadBezierLen(q.From, q.Ctrl, q.To) + } + return sum +} + +// splitAt splits the path into separate paths at the specified intervals +// along the path. +// splitAt updates the provided contour counter as it splits the segments. +func (qs StrokeQuads) splitAt(contour *uint32, ts ...float64) []StrokeQuads { + if len(ts) == 0 { + qs.setContour(*contour) + return []StrokeQuads{qs} + } + + sort.Float64s(ts) + if ts[0] == 0 { + ts = ts[1:] + } + + var ( + j int // index into ts + t float64 // current position along curve + ) + + var oo []StrokeQuads + var oi StrokeQuads + push := func() { + oo = append(oo, oi) + oi = nil + } + + for _, ps := range qs.split() { + for _, q := range ps { + if j == len(ts) { + oi = append(oi, q) + continue + } + speed := func(t float64) float64 { + return float64(lenPt(quadBezierD1(q.Quad.From, q.Quad.Ctrl, + q.Quad.To, float32(t)))) + } + invL, dt := invSpeedPolynomialChebyshevApprox(20, gaussLegendre7, + speed, 0, 1) + + var ( + t0 float64 + r0 = q.Quad.From + r1 = q.Quad.Ctrl + r2 = q.Quad.To + + // from keeps track of the start of the 'running' segment. + from = r0 + ) + for j < len(ts) && t < ts[j] && ts[j] <= t+dt { + tj := invL(ts[j] - t) + tsub := (tj - t0) / (1.0 - t0) + t0 = tj + + var q1 f32.Point + _, q1, _, r0, r1, r2 = quadBezierSplit(r0, r1, r2, + float32(tsub)) + + oi = append(oi, StrokeQuad{ + Contour: *contour, + Quad: QuadSegment{ + From: from, + Ctrl: q1, + To: r0, + }, + }) + push() + (*contour)++ + + from = r0 + j++ + } + if !f64Eq(t0, 1) { + if len(oi) > 0 { + r0 = oi.pen() + } + oi = append(oi, StrokeQuad{ + Contour: *contour, + Quad: QuadSegment{ + From: r0, + Ctrl: r1, + To: r2, + }, + }) + } + t += dt + } + } + if len(oi) > 0 { + push() + (*contour)++ + } + + return oo +} + +func f32Eq(a, b float32) bool { + const epsilon = 1e-10 + return math.Abs(float64(a-b)) < epsilon +} + +func f64Eq(a, b float64) bool { + const epsilon = 1e-10 + return math.Abs(a-b) < epsilon +} + +func invSpeedPolynomialChebyshevApprox(N int, gaussLegendre gaussLegendreFunc, + fp func(float64) float64, tmin, tmax float64) (func(float64) float64, + float64) { + // The TODOs below are copied verbatim from tdewolff/canvas: + // + // TODO: find better way to determine N. For Arc 10 seems fine, for some + // Quads 10 is too low, for Cube depending on inflection points is + // maybe not the best indicator + // + // TODO: track efficiency, how many times is fp called? + // Does a look-up table make more sense? + fLength := func(t float64) float64 { + return math.Abs(gaussLegendre(fp, tmin, t)) + } + totalLength := fLength(tmax) + t := func(L float64) float64 { + return bisectionMethod(fLength, L, tmin, tmax) + } + return polynomialChebyshevApprox(N, t, 0.0, totalLength, tmin, + tmax), totalLength +} + +func polynomialChebyshevApprox(N int, f func(float64) float64, + xmin, xmax, ymin, ymax float64) func(float64) float64 { + var ( + invN = 1.0 / float64(N) + fs = make([]float64, N) + ) + for k := 0; k < N; k++ { + u := math.Cos(math.Pi * (float64(k+1) - 0.5) * invN) + fs[k] = f(xmin + 0.5*(xmax-xmin)*(u+1)) + } + + c := make([]float64, N) + for j := 0; j < N; j++ { + var a float64 + for k := 0; k < N; k++ { + a += fs[k] * math.Cos(float64(j)*math.Pi*(float64(k+1)-0.5)/float64(N)) + } + c[j] = 2 * invN * a + } + + if ymax < ymin { + ymin, ymax = ymax, ymin + } + return func(x float64) float64 { + x = math.Min(xmax, math.Max(xmin, x)) + u := (x-xmin)/(xmax-xmin)*2 - 1 + var a float64 + for j := 0; j < N; j++ { + a += c[j] * math.Cos(float64(j)*math.Acos(u)) + } + y := -0.5*c[0] + a + if !math.IsNaN(ymin) && !math.IsNaN(ymax) { + y = math.Min(ymax, math.Max(ymin, y)) + } + return y + } +} + +// bisectionMethod finds the value x for which f(x) = y in the interval x +// in [xmin, xmax] using the bisection method. +func bisectionMethod(f func(float64) float64, y, xmin, xmax float64) float64 { + const ( + maxIter = 100 + tolerance = 0.001 // 0.1% + ) + + var ( + n = 0 + x float64 + tolX = math.Abs(xmax-xmin) * tolerance + tolY = math.Abs(f(xmax)-f(xmin)) * tolerance + ) + for { + x = 0.5 * (xmin + xmax) + if n >= maxIter { + return x + } + + dy := f(x) - y + switch { + case math.Abs(dy) < tolY, math.Abs(0.5*(xmax-xmin)) < tolX: + return x + case dy > 0: + xmax = x + default: + xmin = x + } + n++ + } +} + +type gaussLegendreFunc func(func(float64) float64, float64, float64) float64 + +// Gauss-Legendre quadrature integration from a to b with n=7 +func gaussLegendre7(f func(float64) float64, a, b float64) float64 { + c := 0.5 * (b - a) + d := 0.5 * (a + b) + Qd1 := f(-0.949108*c + d) + Qd2 := f(-0.741531*c + d) + Qd3 := f(-0.405845*c + d) + Qd4 := f(d) + Qd5 := f(0.405845*c + d) + Qd6 := f(0.741531*c + d) + Qd7 := f(0.949108*c + d) + return c * (0.129485*(Qd1+Qd7) + 0.279705*(Qd2+Qd6) + 0.381830*(Qd3+Qd5) + 0.417959*Qd4) +} diff --git a/gio/giold/internal/stroke/stroke.go b/gio/giold/internal/stroke/stroke.go new file mode 100644 index 0000000..b88a432 --- /dev/null +++ b/gio/giold/internal/stroke/stroke.go @@ -0,0 +1,902 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Most of the algorithms to compute strokes and their offsets have been +// extracted, adapted from (and used as a reference implementation): +// - github.com/tdewolff/canvas (Licensed under MIT) +// +// These algorithms have been implemented from: +// Fast, precise flattening of cubic BĆ©zier path and offset curves +// Thomas F. Hain, et al. +// +// An electronic version is available at: +// https://seant23.files.wordpress.com/2010/11/fastpreciseflatteningofbeziercurve.pdf +// +// Possible improvements (in term of speed and/or accuracy) on these +// algorithms are: +// +// - Polar Stroking: New Theory and Methods for Stroking Paths, +// M. Kilgard +// https://arxiv.org/pdf/2007.00308.pdf +// +// - https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html +// R. Levien + +// Package stroke implements conversion of strokes to filled outlines. It is used as a +// fallback for stroke configurations not natively supported by the renderer. +package stroke + +import ( + "encoding/binary" + "math" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/ops" + "realy.lol/gio/internal/scene" +) + +// The following are copies of types from op/clip to avoid a circular import of +// that package. +// TODO: when the old renderer is gone, this package can be merged with +// op/clip, eliminating the duplicate types. +type StrokeStyle struct { + Width float32 + Miter float32 + Cap StrokeCap + Join StrokeJoin +} + +type StrokeCap uint8 + +const ( + RoundCap StrokeCap = iota + FlatCap + SquareCap +) + +type StrokeJoin uint8 + +const ( + RoundJoin StrokeJoin = iota + BevelJoin +) + +// strokeTolerance is used to reconcile rounding errors arising +// when splitting quads into smaller and smaller segments to approximate +// them into straight lines, and when joining back segments. +// +// The magic value of 0.01 was found by striking a compromise between +// aesthetic looking (curves did look like curves, even after linearization) +// and speed. +const strokeTolerance = 0.01 + +type QuadSegment struct { + From, Ctrl, To f32.Point +} + +type StrokeQuad struct { + Contour uint32 + Quad QuadSegment +} + +type strokeState struct { + p0, p1 f32.Point // p0 is the start point, p1 the end point. + n0, n1 f32.Point // n0 is the normal vector at the start point, n1 at the end point. + r0, r1 float32 // r0 is the curvature at the start point, r1 at the end point. + ctl f32.Point // ctl is the control point of the quadratic BĆ©zier segment. +} + +type StrokeQuads []StrokeQuad + +func (qs *StrokeQuads) setContour(n uint32) { + for i := range *qs { + (*qs)[i].Contour = n + } +} + +func (qs *StrokeQuads) pen() f32.Point { + return (*qs)[len(*qs)-1].Quad.To +} + +func (qs *StrokeQuads) closed() bool { + beg := (*qs)[0].Quad.From + end := (*qs)[len(*qs)-1].Quad.To + return f32Eq(beg.X, end.X) && f32Eq(beg.Y, end.Y) +} + +func (qs *StrokeQuads) lineTo(pt f32.Point) { + end := qs.pen() + *qs = append(*qs, StrokeQuad{ + Quad: QuadSegment{ + From: end, + Ctrl: end.Add(pt).Mul(0.5), + To: pt, + }, + }) +} + +func (qs *StrokeQuads) arc(f1, f2 f32.Point, angle float32) { + const segments = 16 + pen := qs.pen() + m := ArcTransform(pen, f1.Add(pen), f2.Add(pen), angle, segments) + for i := 0; i < segments; i++ { + p0 := qs.pen() + p1 := m.Transform(p0) + p2 := m.Transform(p1) + ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5)) + *qs = append(*qs, StrokeQuad{ + Quad: QuadSegment{ + From: p0, Ctrl: ctl, To: p2, + }, + }) + } +} + +// split splits a slice of quads into slices of quads grouped +// by contours (ie: splitted at move-to boundaries). +func (qs StrokeQuads) split() []StrokeQuads { + if len(qs) == 0 { + return nil + } + + var ( + c uint32 + o []StrokeQuads + i = len(o) + ) + for _, q := range qs { + if q.Contour != c { + c = q.Contour + i = len(o) + o = append(o, StrokeQuads{}) + } + o[i] = append(o[i], q) + } + + return o +} + +func (qs StrokeQuads) stroke(stroke StrokeStyle, dashes DashOp) StrokeQuads { + if !IsSolidLine(dashes) { + qs = qs.dash(dashes) + } + + var ( + o StrokeQuads + hw = 0.5 * stroke.Width + ) + + for _, ps := range qs.split() { + rhs, lhs := ps.offset(hw, stroke) + switch lhs { + case nil: + o = o.append(rhs) + default: + // Closed path. + // Inner path should go opposite direction to cancel outer path. + switch { + case ps.ccw(): + lhs = lhs.reverse() + o = o.append(rhs) + o = o.append(lhs) + default: + rhs = rhs.reverse() + o = o.append(lhs) + o = o.append(rhs) + } + } + } + + return o +} + +// offset returns the right-hand and left-hand sides of the path, offset by +// the half-width hw. +// The stroke handles how segments are joined and ends are capped. +func (qs StrokeQuads) offset(hw float32, + stroke StrokeStyle) (rhs, lhs StrokeQuads) { + var ( + states []strokeState + beg = qs[0].Quad.From + end = qs[len(qs)-1].Quad.To + closed = beg == end + ) + for i := range qs { + q := qs[i].Quad + + var ( + n0 = strokePathNorm(q.From, q.Ctrl, q.To, 0, hw) + n1 = strokePathNorm(q.From, q.Ctrl, q.To, 1, hw) + r0 = strokePathCurv(q.From, q.Ctrl, q.To, 0) + r1 = strokePathCurv(q.From, q.Ctrl, q.To, 1) + ) + states = append(states, strokeState{ + p0: q.From, + p1: q.To, + n0: n0, + n1: n1, + r0: r0, + r1: r1, + ctl: q.Ctrl, + }) + } + + for i, state := range states { + rhs = rhs.append(strokeQuadBezier(state, +hw, strokeTolerance)) + lhs = lhs.append(strokeQuadBezier(state, -hw, strokeTolerance)) + + // join the current and next segments + if hasNext := i+1 < len(states); hasNext || closed { + var next strokeState + switch { + case hasNext: + next = states[i+1] + case closed: + next = states[0] + } + if state.n1 != next.n0 { + strokePathJoin(stroke, &rhs, &lhs, hw, state.p1, state.n1, + next.n0, state.r1, next.r0) + } + } + } + + if closed { + rhs.close() + lhs.close() + return rhs, lhs + } + + qbeg := &states[0] + qend := &states[len(states)-1] + + // Default to counter-clockwise direction. + lhs = lhs.reverse() + strokePathCap(stroke, &rhs, hw, qend.p1, qend.n1) + + rhs = rhs.append(lhs) + strokePathCap(stroke, &rhs, hw, qbeg.p0, qbeg.n0.Mul(-1)) + + rhs.close() + + return rhs, nil +} + +func (qs *StrokeQuads) close() { + p0 := (*qs)[len(*qs)-1].Quad.To + p1 := (*qs)[0].Quad.From + + if p1 == p0 { + return + } + + *qs = append(*qs, StrokeQuad{ + Quad: QuadSegment{ + From: p0, + Ctrl: p0.Add(p1).Mul(0.5), + To: p1, + }, + }) +} + +// ccw returns whether the path is counter-clockwise. +func (qs StrokeQuads) ccw() bool { + // Use the Shoelace formula: + // https://en.wikipedia.org/wiki/Shoelace_formula + var area float32 + for _, ps := range qs.split() { + for i := 1; i < len(ps); i++ { + pi := ps[i].Quad.To + pj := ps[i-1].Quad.To + area += (pi.X - pj.X) * (pi.Y + pj.Y) + } + } + return area <= 0.0 +} + +func (qs StrokeQuads) reverse() StrokeQuads { + if len(qs) == 0 { + return nil + } + + ps := make(StrokeQuads, 0, len(qs)) + for i := range qs { + q := qs[len(qs)-1-i] + q.Quad.To, q.Quad.From = q.Quad.From, q.Quad.To + ps = append(ps, q) + } + + return ps +} + +func (qs StrokeQuads) append(ps StrokeQuads) StrokeQuads { + switch { + case len(ps) == 0: + return qs + case len(qs) == 0: + return ps + } + + // Consolidate quads and smooth out rounding errors. + // We need to also check for the strokeTolerance to correctly handle + // join/cap points or on-purpose disjoint quads. + p0 := qs[len(qs)-1].Quad.To + p1 := ps[0].Quad.From + if p0 != p1 && lenPt(p0.Sub(p1)) < strokeTolerance { + qs = append(qs, StrokeQuad{ + Quad: QuadSegment{ + From: p0, + Ctrl: p0.Add(p1).Mul(0.5), + To: p1, + }, + }) + } + return append(qs, ps...) +} + +func (q QuadSegment) Transform(t f32.Affine2D) QuadSegment { + q.From = t.Transform(q.From) + q.Ctrl = t.Transform(q.Ctrl) + q.To = t.Transform(q.To) + return q +} + +// strokePathNorm returns the normal vector at t. +func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point { + switch t { + case 0: + n := p1.Sub(p0) + if n.X == 0 && n.Y == 0 { + return f32.Point{} + } + n = rot90CW(n) + return normPt(n, d) + case 1: + n := p2.Sub(p1) + if n.X == 0 && n.Y == 0 { + return f32.Point{} + } + n = rot90CW(n) + return normPt(n, d) + } + panic("impossible") +} + +func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) } +func rot90CCW(p f32.Point) f32.Point { return f32.Pt(-p.Y, +p.X) } + +// cosPt returns the cosine of the opening angle between p and q. +func cosPt(p, q f32.Point) float32 { + np := math.Hypot(float64(p.X), float64(p.Y)) + nq := math.Hypot(float64(q.X), float64(q.Y)) + return dotPt(p, q) / float32(np*nq) +} + +func normPt(p f32.Point, l float32) f32.Point { + d := math.Hypot(float64(p.X), float64(p.Y)) + l64 := float64(l) + if math.Abs(d-l64) < 1e-10 { + return f32.Point{} + } + n := float32(l64 / d) + return f32.Point{X: p.X * n, Y: p.Y * n} +} + +func lenPt(p f32.Point) float32 { + return float32(math.Hypot(float64(p.X), float64(p.Y))) +} + +func dotPt(p, q f32.Point) float32 { + return p.X*q.X + p.Y*q.Y +} + +func perpDot(p, q f32.Point) float32 { + return p.X*q.Y - p.Y*q.X +} + +// strokePathCurv returns the curvature at t, along the quadratic BĆ©zier +// curve defined by the triplet (beg, ctl, end). +func strokePathCurv(beg, ctl, end f32.Point, t float32) float32 { + var ( + d1p = quadBezierD1(beg, ctl, end, t) + d2p = quadBezierD2(beg, ctl, end, t) + + // Negative when bending right, ie: the curve is CW at this point. + a = float64(perpDot(d1p, d2p)) + ) + + // We check early that the segment isn't too line-like and + // save a costly call to math.Pow that will be discarded by dividing + // with a too small 'a'. + if math.Abs(a) < 1e-10 { + return float32(math.NaN()) + } + return float32(math.Pow(float64(d1p.X*d1p.X+d1p.Y*d1p.Y), 1.5) / a) +} + +// quadBezierSample returns the point on the BĆ©zier curve at t. +// B(t) = (1-t)^2 P0 + 2(1-t)t P1 + t^2 P2 +func quadBezierSample(p0, p1, p2 f32.Point, t float32) f32.Point { + t1 := 1 - t + c0 := t1 * t1 + c1 := 2 * t1 * t + c2 := t * t + + o := p0.Mul(c0) + o = o.Add(p1.Mul(c1)) + o = o.Add(p2.Mul(c2)) + return o +} + +// quadBezierD1 returns the first derivative of the BĆ©zier curve with respect to t. +// B'(t) = 2(1-t)(P1 - P0) + 2t(P2 - P1) +func quadBezierD1(p0, p1, p2 f32.Point, t float32) f32.Point { + p10 := p1.Sub(p0).Mul(2 * (1 - t)) + p21 := p2.Sub(p1).Mul(2 * t) + + return p10.Add(p21) +} + +// quadBezierD2 returns the second derivative of the BĆ©zier curve with respect to t: +// B''(t) = 2(P2 - 2P1 + P0) +func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point { + p := p2.Sub(p1.Mul(2)).Add(p0) + return p.Mul(2) +} + +// quadBezierLen returns the length of the BĆ©zier curve. +// See: +// https://malczak.linuxpl.com/blog/quadratic-bezier-curve-length/ +func quadBezierLen(p0, p1, p2 f32.Point) float32 { + a := p0.Sub(p1.Mul(2)).Add(p2) + b := p1.Mul(2).Sub(p0.Mul(2)) + A := float64(4 * dotPt(a, a)) + B := float64(4 * dotPt(a, b)) + C := float64(dotPt(b, b)) + if f64Eq(A, 0.0) { + // p1 is in the middle between p0 and p2, + // so it is a straight line from p0 to p2. + return lenPt(p2.Sub(p0)) + } + + Sabc := 2 * math.Sqrt(A+B+C) + A2 := math.Sqrt(A) + A32 := 2 * A * A2 + C2 := 2 * math.Sqrt(C) + BA := B / A2 + return float32((A32*Sabc + A2*B*(Sabc-C2) + (4*C*A-B*B)*math.Log((2*A2+BA+Sabc)/(BA+C2))) / (4 * A32)) +} + +func strokeQuadBezier(state strokeState, d, flatness float32) StrokeQuads { + // Gio strokes are only quadratic BĆ©zier curves, w/o any inflection point. + // So we just have to flatten them. + var qs StrokeQuads + return flattenQuadBezier(qs, state.p0, state.ctl, state.p1, d, flatness) +} + +// flattenQuadBezier splits a BĆ©zier quadratic curve into linear sub-segments, +// themselves also encoded as BĆ©zier (degenerate, flat) quadratic curves. +func flattenQuadBezier(qs StrokeQuads, p0, p1, p2 f32.Point, + d, flatness float32) StrokeQuads { + var ( + t float32 + flat64 = float64(flatness) + ) + for t < 1 { + s2 := float64((p2.X-p0.X)*(p1.Y-p0.Y) - (p2.Y-p0.Y)*(p1.X-p0.X)) + den := math.Hypot(float64(p1.X-p0.X), float64(p1.Y-p0.Y)) + if s2*den == 0.0 { + break + } + + s2 /= den + t = 2.0 * float32(math.Sqrt(flat64/3.0/math.Abs(s2))) + if t >= 1.0 { + break + } + var q0, q1, q2 f32.Point + q0, q1, q2, p0, p1, p2 = quadBezierSplit(p0, p1, p2, t) + qs.addLine(q0, q1, q2, 0, d) + } + qs.addLine(p0, p1, p2, 1, d) + return qs +} + +func (qs *StrokeQuads) addLine(p0, ctrl, p1 f32.Point, t, d float32) { + + switch i := len(*qs); i { + case 0: + p0 = p0.Add(strokePathNorm(p0, ctrl, p1, 0, d)) + default: + // Address possible rounding errors and use previous point. + p0 = (*qs)[i-1].Quad.To + } + + p1 = p1.Add(strokePathNorm(p0, ctrl, p1, 1, d)) + + *qs = append(*qs, + StrokeQuad{ + Quad: QuadSegment{ + From: p0, + Ctrl: p0.Add(p1).Mul(0.5), + To: p1, + }, + }, + ) +} + +// quadInterp returns the interpolated point at t. +func quadInterp(p, q f32.Point, t float32) f32.Point { + return f32.Pt( + (1-t)*p.X+t*q.X, + (1-t)*p.Y+t*q.Y, + ) +} + +// quadBezierSplit returns the pair of triplets (from,ctrl,to) BĆ©zier curve, +// split before (resp. after) the provided parametric t value. +func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point, + f32.Point, f32.Point, f32.Point, f32.Point) { + + var ( + b0 = p0 + b1 = quadInterp(p0, p1, t) + b2 = quadBezierSample(p0, p1, p2, t) + + a0 = b2 + a1 = quadInterp(p1, p2, t) + a2 = p2 + ) + + return b0, b1, b2, a0, a1, a2 +} + +// strokePathJoin joins the two paths rhs and lhs, according to the provided +// stroke operation. +func strokePathJoin(stroke StrokeStyle, rhs, lhs *StrokeQuads, hw float32, + pivot, n0, n1 f32.Point, r0, r1 float32) { + if stroke.Miter > 0 { + strokePathMiterJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1) + return + } + switch stroke.Join { + case BevelJoin: + strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + case RoundJoin: + strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + default: + panic("impossible") + } +} + +func strokePathBevelJoin(rhs, lhs *StrokeQuads, hw float32, + pivot, n0, n1 f32.Point, r0, r1 float32) { + + rp := pivot.Add(n1) + lp := pivot.Sub(n1) + + rhs.lineTo(rp) + lhs.lineTo(lp) +} + +func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, + pivot, n0, n1 f32.Point, r0, r1 float32) { + rp := pivot.Add(n1) + lp := pivot.Sub(n1) + cw := dotPt(rot90CW(n0), n1) >= 0.0 + switch { + case cw: + // Path bends to the right, ie. CW (or 180 degree turn). + c := pivot.Sub(lhs.pen()) + angle := -math.Acos(float64(cosPt(n0, n1))) + lhs.arc(c, c, float32(angle)) + lhs.lineTo(lp) // Add a line to accommodate for rounding errors. + rhs.lineTo(rp) + default: + // Path bends to the left, ie. CCW. + angle := math.Acos(float64(cosPt(n0, n1))) + c := pivot.Sub(rhs.pen()) + rhs.arc(c, c, float32(angle)) + rhs.lineTo(rp) // Add a line to accommodate for rounding errors. + lhs.lineTo(lp) + } +} + +func strokePathMiterJoin(stroke StrokeStyle, rhs, lhs *StrokeQuads, hw float32, + pivot, n0, n1 f32.Point, r0, r1 float32) { + if n0 == n1.Mul(-1) { + strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + return + } + + // This is to handle nearly linear joints that would be clipped otherwise. + limit := math.Max(float64(stroke.Miter), 1.001) + + cw := dotPt(rot90CW(n0), n1) >= 0.0 + if cw { + // hw is used to calculate |R|. + // When running CW, n0 and n1 point the other way, + // so the sign of r0 and r1 is negated. + hw = -hw + } + hw64 := float64(hw) + + cos := math.Sqrt(0.5 * (1 + float64(cosPt(n0, n1)))) + d := hw64 / cos + if math.Abs(limit*hw64) < math.Abs(d) { + stroke.Miter = 0 // Set miter to zero to disable the miter joint. + strokePathJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1) + return + } + mid := pivot.Add(normPt(n0.Add(n1), float32(d))) + + rp := pivot.Add(n1) + lp := pivot.Sub(n1) + switch { + case cw: + // Path bends to the right, ie. CW. + lhs.lineTo(mid) + default: + // Path bends to the left, ie. CCW. + rhs.lineTo(mid) + } + rhs.lineTo(rp) + lhs.lineTo(lp) +} + +// strokePathCap caps the provided path qs, according to the provided stroke operation. +func strokePathCap(stroke StrokeStyle, qs *StrokeQuads, hw float32, + pivot, n0 f32.Point) { + switch stroke.Cap { + case FlatCap: + strokePathFlatCap(qs, hw, pivot, n0) + case SquareCap: + strokePathSquareCap(qs, hw, pivot, n0) + case RoundCap: + strokePathRoundCap(qs, hw, pivot, n0) + default: + panic("impossible") + } +} + +// strokePathFlatCap caps the start or end of a path with a flat cap. +func strokePathFlatCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { + end := pivot.Sub(n0) + qs.lineTo(end) +} + +// strokePathSquareCap caps the start or end of a path with a square cap. +func strokePathSquareCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { + var ( + e = pivot.Add(rot90CCW(n0)) + corner1 = e.Add(n0) + corner2 = e.Sub(n0) + end = pivot.Sub(n0) + ) + + qs.lineTo(corner1) + qs.lineTo(corner2) + qs.lineTo(end) +} + +// strokePathRoundCap caps the start or end of a path with a round cap. +func strokePathRoundCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { + c := pivot.Sub(qs.pen()) + qs.arc(c, c, math.Pi) +} + +// ArcTransform computes a transformation that can be used for generating quadratic bĆ©zier +// curve approximations for an arc. +// +// The math is extracted from the following paper: +// "Drawing an elliptical arc using polylines, quadratic or +// cubic Bezier curves", L. Maisonobe +// An electronic version may be found at: +// http://spaceroots.org/documents/ellipse/elliptical-arc.pdf +func ArcTransform(p, f1, f2 f32.Point, angle float32, + segments int) f32.Affine2D { + c := f32.Point{ + X: 0.5 * (f1.X + f2.X), + Y: 0.5 * (f1.Y + f2.Y), + } + + // semi-major axis: 2a = |PF1| + |PF2| + a := 0.5 * (dist(f1, p) + dist(f2, p)) + + // semi-minor axis: c^2 = a^2+b^2 (c: focal distance) + f := dist(f1, c) + b := math.Sqrt(a*a - f*f) + + var rx, ry, alpha, start float64 + switch { + case a > b: + rx = a + ry = b + default: + rx = b + ry = a + } + + var x float64 + switch { + case f1 == c || f2 == c: + // degenerate case of a circle. + alpha = 0 + default: + switch { + case f1.X > c.X: + x = float64(f1.X - c.X) + alpha = math.Acos(x / f) + case f1.X < c.X: + x = float64(f2.X - c.X) + alpha = math.Acos(x / f) + case f1.X == c.X: + // special case of a "vertical" ellipse. + alpha = math.Pi / 2 + if f1.Y < c.Y { + alpha = -alpha + } + } + } + + start = math.Acos(float64(p.X-c.X) / dist(c, p)) + if c.Y > p.Y { + start = -start + } + start -= alpha + + var ( + Īø = angle / float32(segments) + ref f32.Affine2D // transform from absolute frame to ellipse-based one + rot f32.Affine2D // rotation matrix for each segment + inv f32.Affine2D // transform from ellipse-based frame to absolute one + ) + ref = ref.Offset(f32.Point{}.Sub(c)) + ref = ref.Rotate(f32.Point{}, float32(-alpha)) + ref = ref.Scale(f32.Point{}, f32.Point{ + X: float32(1 / rx), + Y: float32(1 / ry), + }) + inv = ref.Invert() + rot = rot.Rotate(f32.Point{}, float32(0.5*Īø)) + + // Instead of invoking math.Sincos for every segment, compute a rotation + // matrix once and apply for each segment. + // Before applying the rotation matrix rot, transform the coordinates + // to a frame centered to the ellipse (and warped into a unit circle), then rotate. + // Finally, transform back into the original frame. + return inv.Mul(rot).Mul(ref) +} + +func dist(p1, p2 f32.Point) float64 { + var ( + x1 = float64(p1.X) + y1 = float64(p1.Y) + x2 = float64(p2.X) + y2 = float64(p2.Y) + dx = x2 - x1 + dy = y2 - y1 + ) + return math.Hypot(dx, dy) +} + +func StrokePathCommands(style StrokeStyle, dashes DashOp, + scene []byte) StrokeQuads { + quads := decodeToStrokeQuads(scene) + return quads.stroke(style, dashes) +} + +// decodeToStrokeQuads decodes scene commands to quads ready to stroke. +func decodeToStrokeQuads(pathData []byte) StrokeQuads { + quads := make(StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4)) + for len(pathData) >= scene.CommandSize+4 { + contour := binary.LittleEndian.Uint32(pathData) + cmd := ops.DecodeCommand(pathData[4:]) + switch cmd.Op() { + case scene.OpLine: + var q QuadSegment + q.From, q.To = scene.DecodeLine(cmd) + q.Ctrl = q.From.Add(q.To).Mul(.5) + quad := StrokeQuad{ + Contour: contour, + Quad: q, + } + quads = append(quads, quad) + case scene.OpQuad: + var q QuadSegment + q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) + quad := StrokeQuad{ + Contour: contour, + Quad: q, + } + quads = append(quads, quad) + case scene.OpCubic: + for _, q := range SplitCubic(scene.DecodeCubic(cmd)) { + quad := StrokeQuad{ + Contour: contour, + Quad: q, + } + quads = append(quads, quad) + } + default: + panic("unsupported scene command") + } + pathData = pathData[scene.CommandSize+4:] + } + return quads +} + +func SplitCubic(from, ctrl0, ctrl1, to f32.Point) []QuadSegment { + quads := make([]QuadSegment, 0, 10) + // Set the maximum distance proportionally to the longest side + // of the bounding rectangle. + hull := f32.Rectangle{ + Min: from, + Max: ctrl0, + }.Canon().Add(ctrl1).Add(to) + l := hull.Dx() + if h := hull.Dy(); h > l { + l = h + } + approxCubeTo(&quads, 0, l*0.001, from, ctrl0, ctrl1, to) + return quads +} + +// approxCubeTo approximates a cubic BĆ©zier by a series of quadratic +// curves. +func approxCubeTo(quads *[]QuadSegment, splits int, maxDist float32, + from, ctrl0, ctrl1, to f32.Point) int { + // The idea is from + // https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html + // where a quadratic approximates a cubic by eliminating its tĀ³ term + // from its polynomial expression anchored at the starting point: + // + // P(t) = pen + 3t(ctrl0 - pen) + 3tĀ²(ctrl1 - 2ctrl0 + pen) + tĀ³(to - 3ctrl1 + 3ctrl0 - pen) + // + // The control point for the new quadratic Q1 that shares starting point, pen, with P is + // + // C1 = (3ctrl0 - pen)/2 + // + // The reverse cubic anchored at the end point has the polynomial + // + // P'(t) = to + 3t(ctrl1 - to) + 3tĀ²(ctrl0 - 2ctrl1 + to) + tĀ³(pen - 3ctrl0 + 3ctrl1 - to) + // + // The corresponding quadratic Q2 that shares the end point, to, with P has control + // point + // + // C2 = (3ctrl1 - to)/2 + // + // The combined quadratic BĆ©zier, Q, shares both start and end points with its cubic + // and use the midpoint between the two curves Q1 and Q2 as control point: + // + // C = (3ctrl0 - pen + 3ctrl1 - to)/4 + c := ctrl0.Mul(3).Sub(from).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0) + const maxSplits = 32 + if splits >= maxSplits { + *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to}) + return splits + } + // The maximum distance between the cubic P and its approximation Q given t + // can be shown to be + // + // d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen| + // + // To save a square root, compare dĀ² with the squared tolerance. + v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(from) + d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36) + if d2 <= maxDist*maxDist { + *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to}) + return splits + } + // De Casteljau split the curve and approximate the halves. + t := float32(0.5) + c0 := from.Add(ctrl0.Sub(from).Mul(t)) + c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t)) + c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t)) + c01 := c0.Add(c1.Sub(c0).Mul(t)) + c12 := c1.Add(c2.Sub(c1).Mul(t)) + c0112 := c01.Add(c12.Sub(c01).Mul(t)) + splits++ + splits = approxCubeTo(quads, splits, maxDist, from, c0, c01, c0112) + splits = approxCubeTo(quads, splits, maxDist, c0112, c12, c2, to) + return splits +} diff --git a/gio/giold/io/clipboard/clipboard.go b/gio/giold/io/clipboard/clipboard.go new file mode 100644 index 0000000..3d1e64c --- /dev/null +++ b/gio/giold/io/clipboard/clipboard.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clipboard + +import ( + "realy.lol/gio/internal/opconst" + "realy.lol/gio/io/event" + "realy.lol/gio/op" +) + +// Event is generated when the clipboard content is requested. +type Event struct { + Text string +} + +// ReadOp requests the text of the clipboard, delivered to +// the current handler through an Event. +type ReadOp struct { + Tag event.Tag +} + +// WriteOp copies Text to the clipboard. +type WriteOp struct { + Text string +} + +func (h ReadOp) Add(o *op.Ops) { + data := o.Write1(opconst.TypeClipboardReadLen, h.Tag) + data[0] = byte(opconst.TypeClipboardRead) +} + +func (h WriteOp) Add(o *op.Ops) { + data := o.Write1(opconst.TypeClipboardWriteLen, &h.Text) + data[0] = byte(opconst.TypeClipboardWrite) +} + +func (Event) ImplementsEvent() {} diff --git a/gio/giold/io/event/event.go b/gio/giold/io/event/event.go new file mode 100644 index 0000000..998dccb --- /dev/null +++ b/gio/giold/io/event/event.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package event contains the types for event handling. + +The Queue interface is the protocol for receiving external events. + +For example: + + var queue event.Queue = ... + + for _, e := range queue.Events(h) { + switch e.(type) { + ... + } + } + +In general, handlers must be declared before events become +available. Other packages such as pointer and key provide +the means for declaring handlers for specific event types. + +The following example declares a handler ready for key input: + + import realy.lol/gio/io/key + + ops := new(op.Ops) + var h *Handler = ... + key.InputOp{Tag: h}.Add(ops) + +*/ +package event + +// Queue maps an event handler key to the events +// available to the handler. +type Queue interface { + // Events returns the available events for an + // event handler tag. + Events(t Tag) []Event +} + +// Tag is the stable identifier for an event handler. +// For a handler h, the tag is typically &h. +type Tag interface{} + +// Event is the marker interface for events. +type Event interface { + ImplementsEvent() +} diff --git a/gio/giold/io/key/key.go b/gio/giold/io/key/key.go new file mode 100644 index 0000000..913dbb2 --- /dev/null +++ b/gio/giold/io/key/key.go @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package key implements key and text events and operations. + +The InputOp operations is used for declaring key input handlers. Use +an implementation of the Queue interface from package ui to receive +events. +*/ +package key + +import ( + "fmt" + "strings" + + "realy.lol/gio/internal/opconst" + "realy.lol/gio/io/event" + "realy.lol/gio/op" +) + +// InputOp declares a handler ready for key events. +// Key events are in general only delivered to the +// focused key handler. +type InputOp struct { + Tag event.Tag +} + +// SoftKeyboardOp shows or hide the on-screen keyboard, if available. +// It replaces any previous SoftKeyboardOp. +type SoftKeyboardOp struct { + Show bool +} + +// FocusOp sets or clears the keyboard focus. It replaces any previous +// FocusOp in the same frame. +type FocusOp struct { + // Tag is the new focus. The focus is cleared if Tag is nil, or if Tag + // has no InputOp in the same frame. + Tag event.Tag +} + +// A FocusEvent is generated when a handler gains or loses +// focus. +type FocusEvent struct { + Focus bool +} + +// An Event is generated when a key is pressed. For text input +// use EditEvent. +type Event struct { + // Name of the key. For letters, the upper case form is used, via + // unicode.ToUpper. The shift modifier is taken into account, all other + // modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1" + // combinations both give the Name "!" with the US keyboard layout. + Name string + // Modifiers is the set of active modifiers when the key was pressed. + Modifiers Modifiers + // State is the state of the key when the event was fired. + State State +} + +// An EditEvent is generated when text is input. +type EditEvent struct { + Text string +} + +// State is the state of a key during an event. +type State uint8 + +const ( + // Press is the state of a pressed key. + Press State = iota + // Release is the state of a key that has been released. + // + // Note: release events are only implemented on the following platforms: + // macOS, Linux, Windows, WebAssembly. + Release +) + +// Modifiers +type Modifiers uint32 + +const ( + // ModCtrl is the ctrl modifier key. + ModCtrl Modifiers = 1 << iota + // ModCommand is the command modifier key + // found on Apple keyboards. + ModCommand + // ModShift is the shift modifier key. + ModShift + // ModAlt is the alt modifier key, or the option + // key on Apple keyboards. + ModAlt + // ModSuper is the "logo" modifier key, often + // represented by a Windows logo. + ModSuper +) + +const ( + // Names for special keys. + NameLeftArrow = "ā†" + NameRightArrow = "ā†’" + NameUpArrow = "ā†‘" + NameDownArrow = "ā†“" + NameReturn = "āŽ" + NameEnter = "āŒ¤" + NameEscape = "āŽ‹" + NameHome = "ā‡±" + NameEnd = "ā‡²" + NameDeleteBackward = "āŒ«" + NameDeleteForward = "āŒ¦" + NamePageUp = "ā‡ž" + NamePageDown = "ā‡Ÿ" + NameTab = "ā‡„" + NameSpace = "Space" +) + +// Contain reports whether m contains all modifiers +// in m2. +func (m Modifiers) Contain(m2 Modifiers) bool { + return m&m2 == m2 +} + +func (h InputOp) Add(o *op.Ops) { + if h.Tag == nil { + panic("Tag must be non-nil") + } + data := o.Write1(opconst.TypeKeyInputLen, h.Tag) + data[0] = byte(opconst.TypeKeyInput) +} + +func (h SoftKeyboardOp) Add(o *op.Ops) { + data := o.Write(opconst.TypeKeySoftKeyboardLen) + data[0] = byte(opconst.TypeKeySoftKeyboard) + if h.Show { + data[1] = 1 + } +} + +func (h FocusOp) Add(o *op.Ops) { + data := o.Write1(opconst.TypeKeyFocusLen, h.Tag) + data[0] = byte(opconst.TypeKeyFocus) +} + +func (EditEvent) ImplementsEvent() {} +func (Event) ImplementsEvent() {} +func (FocusEvent) ImplementsEvent() {} + +func (e Event) String() string { + return fmt.Sprintf("%v %v %v}", e.Name, e.Modifiers, e.State) +} + +func (m Modifiers) String() string { + var strs []string + if m.Contain(ModCtrl) { + strs = append(strs, "ModCtrl") + } + if m.Contain(ModCommand) { + strs = append(strs, "ModCommand") + } + if m.Contain(ModShift) { + strs = append(strs, "ModShift") + } + if m.Contain(ModAlt) { + strs = append(strs, "ModAlt") + } + if m.Contain(ModSuper) { + strs = append(strs, "ModSuper") + } + return strings.Join(strs, "|") +} + +func (s State) String() string { + switch s { + case Press: + return "Press" + case Release: + return "Release" + default: + panic("invalid State") + } +} diff --git a/gio/giold/io/key/mod.go b/gio/giold/io/key/mod.go new file mode 100644 index 0000000..c5db56c --- /dev/null +++ b/gio/giold/io/key/mod.go @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build !darwin + +package key + +// ModShortcut is the platform's shortcut modifier, usually the Ctrl +// key. On Apple platforms it is the Cmd key. +const ModShortcut = ModCtrl diff --git a/gio/giold/io/key/mod_darwin.go b/gio/giold/io/key/mod_darwin.go new file mode 100644 index 0000000..c0f1437 --- /dev/null +++ b/gio/giold/io/key/mod_darwin.go @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package key + +// ModShortcut is the platform's shortcut modifier, usually the Ctrl +// key. On Apple platforms it is the Cmd key. +const ModShortcut = ModCommand diff --git a/gio/giold/io/pointer/doc.go b/gio/giold/io/pointer/doc.go new file mode 100644 index 0000000..7243b94 --- /dev/null +++ b/gio/giold/io/pointer/doc.go @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package pointer implements pointer events and operations. +A pointer is either a mouse controlled cursor or a touch +object such as a finger. + +The InputOp operation is used to declare a handler ready for pointer +events. Use an event.Queue to receive events. + +Types + +Only events that match a specified list of types are delivered to a handler. + +For example, to receive Press, Drag, and Release events (but not Move, Enter, +Leave, or Scroll): + + var ops op.Ops + var h *Handler = ... + + pointer.InputOp{ + Tag: h, + Types: pointer.Press | pointer.Drag | pointer.Release, + }.Add(ops) + +Cancel events are always delivered. + +Areas + +The area operations are used for specifying the area where +subsequent InputOp are active. + +For example, to set up a rectangular hit area: + + r := image.Rectangle{...} + pointer.Rect(r).Add(ops) + pointer.InputOp{Tag: h}.Add(ops) + +Note that areas compound: the effective area of multiple area +operations is the intersection of the areas. + +Matching events + +StackOp operations and input handlers form an implicit tree. +Each stack operation is a node, and each input handler is associated +with the most recent node. + +For example: + + ops := new(op.Ops) + var stack op.StackOp + var h1, h2 *Handler + + state := op.Save(ops) + pointer.InputOp{Tag: h1}.Add(Ops) + state.Load() + + state = op.Save(ops) + pointer.InputOp{Tag: h2}.Add(ops) + state.Load() + +implies a tree of two inner nodes, each with one pointer handler. + +When determining which handlers match an Event, only handlers whose +areas contain the event position are considered. The matching +proceeds as follows. + +First, the foremost matching handler is included. If the handler +has pass-through enabled, this step is repeated. + +Then, all matching handlers from the current node and all parent +nodes are included. + +In the example above, all events will go to h2 only even though both +handlers have the same area (the entire screen). + +Pass-through + +The PassOp operations controls the pass-through setting. A handler's +pass-through setting is recorded along with the InputOp. + +Pass-through handlers are useful for overlay widgets such as a hidden +side drawer. When the user touches the side, both the (transparent) +drawer handle and the interface below should receive pointer events. + +Disambiguation + +When more than one handler matches a pointer event, the event queue +follows a set of rules for distributing the event. + +As long as the pointer has not received a Press event, all +matching handlers receive all events. + +When a pointer is pressed, the set of matching handlers is +recorded. The set is not updated according to the pointer position +and hit areas. Rather, handlers stay in the matching set until they +no longer appear in a InputOp or when another handler in the set +grabs the pointer. + +A handler can exclude all other handler from its matching sets +by setting the Grab flag in its InputOp. The Grab flag is sticky +and stays in effect until the handler no longer appears in any +matching sets. + +The losing handlers are notified by a Cancel event. + +For multiple grabbing handlers, the foremost handler wins. + +Priorities + +Handlers know their position in a matching set of a pointer through +event priorities. The Shared priority is for matching sets with +multiple handlers; the Grabbed priority indicate exclusive access. + +Priorities are useful for deferred gesture matching. + +Consider a scrollable list of clickable elements. When the user touches an +element, it is unknown whether the gesture is a click on the element +or a drag (scroll) of the list. While the click handler might light up +the element in anticipation of a click, the scrolling handler does not +scroll on finger movements with lower than Grabbed priority. + +Should the user release the finger, the click handler registers a click. + +However, if the finger moves beyond a threshold, the scrolling handler +determines that the gesture is a drag and sets its Grab flag. The +click handler receives a Cancel (removing the highlight) and further +movements for the scroll handler has priority Grabbed, scrolling the +list. +*/ +package pointer diff --git a/gio/giold/io/pointer/pointer.go b/gio/giold/io/pointer/pointer.go new file mode 100644 index 0000000..f3aafae --- /dev/null +++ b/gio/giold/io/pointer/pointer.go @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package pointer + +import ( + "encoding/binary" + "fmt" + "image" + "strings" + "time" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/opconst" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/op" +) + +// Event is a pointer event. +type Event struct { + Type Type + Source Source + // PointerID is the id for the pointer and can be used + // to track a particular pointer from Press to + // Release or Cancel. + PointerID ID + // Priority is the priority of the receiving handler + // for this event. + Priority Priority + // Time is when the event was received. The + // timestamp is relative to an undefined base. + Time time.Duration + // Buttons are the set of pressed mouse buttons for this event. + Buttons Buttons + // Position is the position of the event, relative to + // the current transformation, as set by op.TransformOp. + Position f32.Point + // Scroll is the scroll amount, if any. + Scroll f32.Point + // Modifiers is the set of active modifiers when + // the mouse button was pressed. + Modifiers key.Modifiers +} + +// AreaOp updates the hit area to the intersection of the current +// hit area and the area. The area is transformed before applying +// it. +type AreaOp struct { + kind areaKind + rect image.Rectangle +} + +// CursorNameOp sets the cursor for the current area. +type CursorNameOp struct { + Name CursorName +} + +// InputOp declares an input handler ready for pointer +// events. +type InputOp struct { + Tag event.Tag + // Grab, if set, request that the handler get + // Grabbed priority. + Grab bool + // Types is a bitwise-or of event types to receive. + Types Type + // ScrollBounds describe the maximum scrollable distances in both + // axes. Specifically, any Event e delivered to Tag will satisfy + // + // ScrollBounds.Min.X <= e.Scroll.X <= ScrollBounds.Max.X (horizontal axis) + // ScrollBounds.Min.Y <= e.Scroll.Y <= ScrollBounds.Max.Y (vertical axis) + ScrollBounds image.Rectangle +} + +// PassOp sets the pass-through mode. +type PassOp struct { + Pass bool +} + +type ID uint16 + +// Type of an Event. +type Type uint8 + +// Priority of an Event. +type Priority uint8 + +// Source of an Event. +type Source uint8 + +// Buttons is a set of mouse buttons +type Buttons uint8 + +// CursorName is the name of a cursor. +type CursorName string + +// Must match app/internal/input.areaKind +type areaKind uint8 + +const ( + // CursorDefault is the default cursor. + CursorDefault CursorName = "" + // CursorText is the cursor for text. + CursorText CursorName = "text" + // CursorPointer is the cursor for a link. + CursorPointer CursorName = "pointer" + // CursorCrossHair is the cursor for precise location. + CursorCrossHair CursorName = "crosshair" + // CursorColResize is the cursor for vertical resize. + CursorColResize CursorName = "col-resize" + // CursorRowResize is the cursor for horizontal resize. + CursorRowResize CursorName = "row-resize" + // CursorGrab is the cursor for moving object in any direction. + CursorGrab CursorName = "grab" + // CursorNone hides the cursor. To show it again, use any other cursor. + CursorNone CursorName = "none" +) + +const ( + // A Cancel event is generated when the current gesture is + // interrupted by other handlers or the system. + Cancel Type = (1 << iota) >> 1 + // Press of a pointer. + Press + // Release of a pointer. + Release + // Move of a pointer. + Move + // Drag of a pointer. + Drag + // Pointer enters an area watching for pointer input + Enter + // Pointer leaves an area watching for pointer input + Leave + // Scroll of a pointer. + Scroll +) + +const ( + // Mouse generated event. + Mouse Source = iota + // Touch generated event. + Touch +) + +const ( + // Shared priority is for handlers that + // are part of a matching set larger than 1. + Shared Priority = iota + // Foremost priority is like Shared, but the + // handler is the foremost of the matching set. + Foremost + // Grabbed is used for matching sets of size 1. + Grabbed +) + +const ( + // ButtonPrimary is the primary button, usually the left button for a + // right-handed user. + ButtonPrimary Buttons = 1 << iota + // ButtonSecondary is the secondary button, usually the right button for a + // right-handed user. + ButtonSecondary + // ButtonTertiary is the tertiary button, usually the middle button. + ButtonTertiary +) + +const ( + areaRect areaKind = iota + areaEllipse +) + +// Rect constructs a rectangular hit area. +func Rect(size image.Rectangle) AreaOp { + return AreaOp{ + kind: areaRect, + rect: size, + } +} + +// Ellipse constructs an ellipsoid hit area. +func Ellipse(size image.Rectangle) AreaOp { + return AreaOp{ + kind: areaEllipse, + rect: size, + } +} + +func (op AreaOp) Add(o *op.Ops) { + data := o.Write(opconst.TypeAreaLen) + data[0] = byte(opconst.TypeArea) + data[1] = byte(op.kind) + bo := binary.LittleEndian + bo.PutUint32(data[2:], uint32(op.rect.Min.X)) + bo.PutUint32(data[6:], uint32(op.rect.Min.Y)) + bo.PutUint32(data[10:], uint32(op.rect.Max.X)) + bo.PutUint32(data[14:], uint32(op.rect.Max.Y)) +} + +func (op CursorNameOp) Add(o *op.Ops) { + data := o.Write1(opconst.TypeCursorLen, op.Name) + data[0] = byte(opconst.TypeCursor) +} + +// Add panics if the scroll range does not contain zero. +func (op InputOp) Add(o *op.Ops) { + if op.Tag == nil { + panic("Tag must be non-nil") + } + if b := op.ScrollBounds; b.Min.X > 0 || b.Max.X < 0 || b.Min.Y > 0 || b.Max.Y < 0 { + panic(fmt.Errorf("invalid scroll range value %v", b)) + } + data := o.Write1(opconst.TypePointerInputLen, op.Tag) + data[0] = byte(opconst.TypePointerInput) + if op.Grab { + data[1] = 1 + } + data[2] = byte(op.Types) + bo := binary.LittleEndian + bo.PutUint32(data[3:], uint32(op.ScrollBounds.Min.X)) + bo.PutUint32(data[7:], uint32(op.ScrollBounds.Min.Y)) + bo.PutUint32(data[11:], uint32(op.ScrollBounds.Max.X)) + bo.PutUint32(data[15:], uint32(op.ScrollBounds.Max.Y)) +} + +func (op PassOp) Add(o *op.Ops) { + data := o.Write(opconst.TypePassLen) + data[0] = byte(opconst.TypePass) + if op.Pass { + data[1] = 1 + } +} + +func (t Type) String() string { + switch t { + case Press: + return "Press" + case Release: + return "Release" + case Cancel: + return "Cancel" + case Move: + return "Move" + case Drag: + return "Drag" + case Enter: + return "Enter" + case Leave: + return "Leave" + case Scroll: + return "Scroll" + default: + panic("unknown Type") + } +} + +func (p Priority) String() string { + switch p { + case Shared: + return "Shared" + case Foremost: + return "Foremost" + case Grabbed: + return "Grabbed" + default: + panic("unknown priority") + } +} + +func (s Source) String() string { + switch s { + case Mouse: + return "Mouse" + case Touch: + return "Touch" + default: + panic("unknown source") + } +} + +// Contain reports whether the set b contains +// all of the buttons. +func (b Buttons) Contain(buttons Buttons) bool { + return b&buttons == buttons +} + +func (b Buttons) String() string { + var strs []string + if b.Contain(ButtonPrimary) { + strs = append(strs, "ButtonPrimary") + } + if b.Contain(ButtonSecondary) { + strs = append(strs, "ButtonSecondary") + } + if b.Contain(ButtonTertiary) { + strs = append(strs, "ButtonTertiary") + } + return strings.Join(strs, "|") +} + +func (c CursorName) String() string { + if c == CursorDefault { + return "default" + } + return string(c) +} + +func (Event) ImplementsEvent() {} diff --git a/gio/giold/io/profile/profile.go b/gio/giold/io/profile/profile.go new file mode 100644 index 0000000..58be154 --- /dev/null +++ b/gio/giold/io/profile/profile.go @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package profiles provides access to rendering +// profiles. +package profile + +import ( + "realy.lol/gio/internal/opconst" + "realy.lol/gio/io/event" + "realy.lol/gio/op" +) + +// Op registers a handler for receiving +// Events. +type Op struct { + Tag event.Tag +} + +// Event contains profile data from a single +// rendered frame. +type Event struct { + // Timings. Very likely to change. + Timings string +} + +func (p Op) Add(o *op.Ops) { + data := o.Write1(opconst.TypeProfileLen, p.Tag) + data[0] = byte(opconst.TypeProfile) +} + +func (p Event) ImplementsEvent() {} diff --git a/gio/giold/io/router/clipboard.go b/gio/giold/io/router/clipboard.go new file mode 100644 index 0000000..122c9bc --- /dev/null +++ b/gio/giold/io/router/clipboard.go @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "realy.lol/gio/internal/opconst" + "realy.lol/gio/internal/ops" + "realy.lol/gio/io/event" +) + +type clipboardQueue struct { + receivers map[event.Tag]struct{} + // request avoid read clipboard every frame while waiting. + requested bool + text *string + reader ops.Reader +} + +// WriteClipboard returns the most recent text to be copied +// to the clipboard, if any. +func (q *clipboardQueue) WriteClipboard() (string, bool) { + if q.text == nil { + return "", false + } + text := *q.text + q.text = nil + return text, true +} + +// ReadClipboard reports if any new handler is waiting +// to read the clipboard. +func (q *clipboardQueue) ReadClipboard() bool { + if len(q.receivers) <= 0 || q.requested { + return false + } + q.requested = true + return true +} + +func (q *clipboardQueue) Push(e event.Event, events *handlerEvents) { + for r := range q.receivers { + events.Add(r, e) + delete(q.receivers, r) + } +} + +func (q *clipboardQueue) ProcessWriteClipboard(d []byte, refs []interface{}) { + if opconst.OpType(d[0]) != opconst.TypeClipboardWrite { + panic("invalid op") + } + q.text = refs[0].(*string) +} + +func (q *clipboardQueue) ProcessReadClipboard(d []byte, refs []interface{}) { + if opconst.OpType(d[0]) != opconst.TypeClipboardRead { + panic("invalid op") + } + if q.receivers == nil { + q.receivers = make(map[event.Tag]struct{}) + } + tag := refs[0].(event.Tag) + if _, ok := q.receivers[tag]; !ok { + q.receivers[tag] = struct{}{} + q.requested = false + } +} diff --git a/gio/giold/io/router/clipboard_test.go b/gio/giold/io/router/clipboard_test.go new file mode 100644 index 0000000..ac5ebe7 --- /dev/null +++ b/gio/giold/io/router/clipboard_test.go @@ -0,0 +1,155 @@ +package router + +import ( + "testing" + + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/event" + "realy.lol/gio/op" +) + +func TestClipboardDuplicateEvent(t *testing.T) { + ops, router, handler := new(op.Ops), new(Router), make([]int, 2) + + // Both must receive the event once + clipboard.ReadOp{Tag: &handler[0]}.Add(ops) + clipboard.ReadOp{Tag: &handler[1]}.Add(ops) + + router.Frame(ops) + event := clipboard.Event{Text: "Test"} + router.Queue(event) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), true) + assertClipboardEvent(t, router.Events(&handler[1]), true) + ops.Reset() + + // No ReadOp + + router.Frame(ops) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), false) + assertClipboardEvent(t, router.Events(&handler[1]), false) + ops.Reset() + + clipboard.ReadOp{Tag: &handler[0]}.Add(ops) + + router.Frame(ops) + // No ClipboardEvent sent + assertClipboardReadOp(t, router, 1) + assertClipboardEvent(t, router.Events(&handler[0]), false) + assertClipboardEvent(t, router.Events(&handler[1]), false) + ops.Reset() +} + +func TestQueueProcessReadClipboard(t *testing.T) { + ops, router, handler := new(op.Ops), new(Router), make([]int, 2) + ops.Reset() + + // Request read + clipboard.ReadOp{Tag: &handler[0]}.Add(ops) + + router.Frame(ops) + assertClipboardReadOp(t, router, 1) + ops.Reset() + + for i := 0; i < 3; i++ { + // No ReadOp + // One receiver must still wait for response + + router.Frame(ops) + assertClipboardReadOpDuplicated(t, router, 1) + ops.Reset() + } + + router.Frame(ops) + // Send the clipboard event + event := clipboard.Event{Text: "Text 2"} + router.Queue(event) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), true) + ops.Reset() + + // No ReadOp + // There's no receiver waiting + + router.Frame(ops) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), false) + ops.Reset() +} + +func TestQueueProcessWriteClipboard(t *testing.T) { + ops, router := new(op.Ops), new(Router) + ops.Reset() + + clipboard.WriteOp{Text: "Write 1"}.Add(ops) + + router.Frame(ops) + assertClipboardWriteOp(t, router, "Write 1") + ops.Reset() + + // No WriteOp + + router.Frame(ops) + assertClipboardWriteOp(t, router, "") + ops.Reset() + + clipboard.WriteOp{Text: "Write 2"}.Add(ops) + + router.Frame(ops) + assertClipboardReadOp(t, router, 0) + assertClipboardWriteOp(t, router, "Write 2") + ops.Reset() +} + +func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) { + t.Helper() + var evtClipboard int + for _, e := range events { + switch e.(type) { + case clipboard.Event: + evtClipboard++ + } + } + if evtClipboard <= 0 && expected { + t.Error("expected to receive some event") + } + if evtClipboard > 0 && !expected { + t.Error("unexpected event received") + } +} + +func assertClipboardReadOp(t *testing.T, router *Router, expected int) { + t.Helper() + if len(router.cqueue.receivers) != expected { + t.Error("unexpected number of receivers") + } + if router.cqueue.ReadClipboard() != (expected > 0) { + t.Error("missing requests") + } +} + +func assertClipboardReadOpDuplicated(t *testing.T, router *Router, + expected int) { + t.Helper() + if len(router.cqueue.receivers) != expected { + t.Error("receivers removed") + } + if router.cqueue.ReadClipboard() != false { + t.Error("duplicated requests") + } +} + +func assertClipboardWriteOp(t *testing.T, router *Router, expected string) { + t.Helper() + if (router.cqueue.text != nil) != (expected != "") { + t.Error("text not defined") + } + text, ok := router.cqueue.WriteClipboard() + if ok != (expected != "") { + t.Error("duplicated requests") + } + if text != expected { + t.Errorf("got text %s, expected %s", text, expected) + } +} diff --git a/gio/giold/io/router/key.go b/gio/giold/io/router/key.go new file mode 100644 index 0000000..0fe946e --- /dev/null +++ b/gio/giold/io/router/key.go @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "realy.lol/gio/internal/opconst" + "realy.lol/gio/internal/ops" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/op" +) + +type TextInputState uint8 + +type keyQueue struct { + focus event.Tag + handlers map[event.Tag]*keyHandler + reader ops.Reader + state TextInputState +} + +type keyHandler struct { + // visible will be true if the InputOp is present + // in the current frame. + visible bool + new bool +} + +const ( + TextInputKeep TextInputState = iota + TextInputClose + TextInputOpen +) + +// InputState returns the last text input state as +// determined in Frame. +func (q *keyQueue) InputState() TextInputState { + return q.state +} + +func (q *keyQueue) Frame(root *op.Ops, events *handlerEvents) { + if q.handlers == nil { + q.handlers = make(map[event.Tag]*keyHandler) + } + for _, h := range q.handlers { + h.visible, h.new = false, false + } + q.reader.Reset(root) + + focus, changed, state := q.resolveFocus(events) + for k, h := range q.handlers { + if !h.visible { + delete(q.handlers, k) + if q.focus == k { + // Remove the focus from the handler that is no longer visible. + q.focus = nil + state = TextInputClose + } + } else if h.new && k != focus { + // Reset the handler on (each) first appearance, but don't trigger redraw. + events.AddNoRedraw(k, key.FocusEvent{Focus: false}) + } + } + if changed && focus != nil { + if _, exists := q.handlers[focus]; !exists { + focus = nil + } + } + if changed && focus != q.focus { + if q.focus != nil { + events.Add(q.focus, key.FocusEvent{Focus: false}) + } + q.focus = focus + if q.focus != nil { + events.Add(q.focus, key.FocusEvent{Focus: true}) + } else { + state = TextInputClose + } + } + q.state = state +} + +func (q *keyQueue) Push(e event.Event, events *handlerEvents) { + if q.focus != nil { + events.Add(q.focus, e) + } +} + +func (q *keyQueue) resolveFocus(events *handlerEvents) (focus event.Tag, + changed bool, state TextInputState) { + for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() { + switch opconst.OpType(encOp.Data[0]) { + case opconst.TypeKeyFocus: + op := decodeFocusOp(encOp.Data, encOp.Refs) + changed = true + focus = op.Tag + case opconst.TypeKeySoftKeyboard: + op := decodeSoftKeyboardOp(encOp.Data, encOp.Refs) + if op.Show { + state = TextInputOpen + } else { + state = TextInputClose + } + case opconst.TypeKeyInput: + op := decodeKeyInputOp(encOp.Data, encOp.Refs) + h, ok := q.handlers[op.Tag] + if !ok { + h = &keyHandler{new: true} + q.handlers[op.Tag] = h + } + h.visible = true + } + } + return +} + +func decodeKeyInputOp(d []byte, refs []interface{}) key.InputOp { + if opconst.OpType(d[0]) != opconst.TypeKeyInput { + panic("invalid op") + } + return key.InputOp{ + Tag: refs[0].(event.Tag), + } +} + +func decodeSoftKeyboardOp(d []byte, refs []interface{}) key.SoftKeyboardOp { + if opconst.OpType(d[0]) != opconst.TypeKeySoftKeyboard { + panic("invalid op") + } + return key.SoftKeyboardOp{ + Show: d[1] != 0, + } +} + +func decodeFocusOp(d []byte, refs []interface{}) key.FocusOp { + if opconst.OpType(d[0]) != opconst.TypeKeyFocus { + panic("invalid op") + } + return key.FocusOp{ + Tag: refs[0], + } +} diff --git a/gio/giold/io/router/key_test.go b/gio/giold/io/router/key_test.go new file mode 100644 index 0000000..59176df --- /dev/null +++ b/gio/giold/io/router/key_test.go @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "reflect" + "testing" + + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/op" +) + +func TestKeyWakeup(t *testing.T) { + handler := new(int) + var ops op.Ops + key.InputOp{Tag: handler}.Add(&ops) + + var r Router + // Test that merely adding a handler doesn't trigger redraw. + r.Frame(&ops) + if _, wake := r.WakeupTime(); wake { + t.Errorf("adding key.InputOp triggered a redraw") + } + // However, adding a handler queues a Focus(false) event. + if evts := r.Events(handler); len(evts) != 1 { + t.Errorf("no Focus event for newly registered key.InputOp") + } + // Verify that r.Events does trigger a redraw. + r.Frame(&ops) + if _, wake := r.WakeupTime(); !wake { + t.Errorf("key.FocusEvent event didn't trigger a redraw") + } +} + +func TestKeyMultiples(t *testing.T) { + handlers := make([]int, 3) + ops := new(op.Ops) + r := new(Router) + + key.SoftKeyboardOp{Show: true}.Add(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.FocusOp{Tag: &handlers[2]}.Add(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + + // The last one must be focused: + key.InputOp{Tag: &handlers[2]}.Add(ops) + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), false) + assertKeyEvent(t, r.Events(&handlers[1]), false) + assertKeyEvent(t, r.Events(&handlers[2]), true) + assertFocus(t, r, &handlers[2]) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeyStacked(t *testing.T) { + handlers := make([]int, 4) + ops := new(op.Ops) + r := new(Router) + + s := op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.FocusOp{Tag: nil}.Add(ops) + s.Load() + s = op.Save(ops) + key.SoftKeyboardOp{Show: false}.Add(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + key.FocusOp{Tag: &handlers[1]}.Add(ops) + s.Load() + s = op.Save(ops) + key.InputOp{Tag: &handlers[2]}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Load() + s = op.Save(ops) + key.InputOp{Tag: &handlers[3]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), false) + assertKeyEvent(t, r.Events(&handlers[1]), true) + assertKeyEvent(t, r.Events(&handlers[2]), false) + assertKeyEvent(t, r.Events(&handlers[3]), false) + assertFocus(t, r, &handlers[1]) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeySoftKeyboardNoFocus(t *testing.T) { + ops := new(op.Ops) + r := new(Router) + + // It's possible to open the keyboard + // without any active focus: + key.SoftKeyboardOp{Show: true}.Add(ops) + + r.Frame(ops) + + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeyRemoveFocus(t *testing.T) { + handlers := make([]int, 2) + ops := new(op.Ops) + r := new(Router) + + // New InputOp with Focus and Keyboard: + s := op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.FocusOp{Tag: &handlers[0]}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Load() + + // New InputOp without any focus: + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + // Add some key events: + event := event.Event(key.Event{Name: key.NameTab, + Modifiers: key.ModShortcut, State: key.Press}) + r.Queue(event) + + assertKeyEvent(t, r.Events(&handlers[0]), true, event) + assertKeyEvent(t, r.Events(&handlers[1]), false) + assertFocus(t, r, &handlers[0]) + assertKeyboard(t, r, TextInputOpen) + + ops.Reset() + + // Will get the focus removed: + s = op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + s.Load() + + // Unchanged: + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + // Remove focus by focusing on a tag that don't exist. + s = op.Save(ops) + key.FocusOp{Tag: new(int)}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputClose) + + ops.Reset() + + s = op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + s.Load() + + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEventUnexpected(t, r.Events(&handlers[0])) + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputKeep) + + ops.Reset() + + // Set focus to InputOp which already + // exists in the previous frame: + s = op.Save(ops) + key.FocusOp{Tag: &handlers[0]}.Add(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Load() + + // Remove focus. + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + key.FocusOp{Tag: nil}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeyFocusedInvisible(t *testing.T) { + handlers := make([]int, 2) + ops := new(op.Ops) + r := new(Router) + + // Set new InputOp with focus: + s := op.Save(ops) + key.FocusOp{Tag: &handlers[0]}.Add(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Load() + + // Set new InputOp without focus: + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), true) + assertKeyEvent(t, r.Events(&handlers[1]), false) + assertFocus(t, r, &handlers[0]) + assertKeyboard(t, r, TextInputOpen) + + ops.Reset() + + // + // Removed first (focused) element! + // + + // Unchanged: + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEventUnexpected(t, r.Events(&handlers[0])) + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputClose) + + ops.Reset() + + // Respawn the first element: + // It must receive one `Event{Focus: false}`. + s = op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + s.Load() + + // Unchanged + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), false) + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputKeep) + +} + +func assertKeyEvent(t *testing.T, events []event.Event, expected bool, + expectedInputs ...event.Event) { + t.Helper() + var evtFocus int + var evtKeyPress int + for _, e := range events { + switch ev := e.(type) { + case key.FocusEvent: + if ev.Focus != expected { + t.Errorf("focus is expected to be %v, got %v", expected, + ev.Focus) + } + evtFocus++ + case key.Event, key.EditEvent: + if len(expectedInputs) <= evtKeyPress { + t.Errorf("unexpected key events") + } + if !reflect.DeepEqual(ev, expectedInputs[evtKeyPress]) { + t.Errorf("expected %v events, got %v", + expectedInputs[evtKeyPress], ev) + } + evtKeyPress++ + } + } + if evtFocus <= 0 { + t.Errorf("expected focus event") + } + if evtFocus > 1 { + t.Errorf("expected single focus event") + } + if evtKeyPress != len(expectedInputs) { + t.Errorf("expected key events") + } +} + +func assertKeyEventUnexpected(t *testing.T, events []event.Event) { + t.Helper() + var evtFocus int + for _, e := range events { + switch e.(type) { + case key.FocusEvent: + evtFocus++ + } + } + if evtFocus > 1 { + t.Errorf("unexpected focus event") + } +} + +func assertFocus(t *testing.T, router *Router, expected event.Tag) { + t.Helper() + if router.kqueue.focus != expected { + t.Errorf("expected %v to be focused, got %v", expected, + router.kqueue.focus) + } +} + +func assertKeyboard(t *testing.T, router *Router, expected TextInputState) { + t.Helper() + if router.kqueue.state != expected { + t.Errorf("expected %v keyboard, got %v", expected, router.kqueue.state) + } +} diff --git a/gio/giold/io/router/pointer.go b/gio/giold/io/router/pointer.go new file mode 100644 index 0000000..588657c --- /dev/null +++ b/gio/giold/io/router/pointer.go @@ -0,0 +1,515 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "encoding/binary" + "image" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/opconst" + "realy.lol/gio/internal/ops" + "realy.lol/gio/io/event" + "realy.lol/gio/io/pointer" + "realy.lol/gio/op" +) + +type pointerQueue struct { + hitTree []hitNode + areas []areaNode + cursors []cursorNode + cursor pointer.CursorName + handlers map[event.Tag]*pointerHandler + pointers []pointerInfo + reader ops.Reader + + // states holds the storage for save/restore ops. + states []collectState + scratch []event.Tag +} + +type hitNode struct { + next int + area int + // Pass tracks the most recent PassOp mode. + pass bool + + // For handler nodes. + tag event.Tag +} + +type cursorNode struct { + name pointer.CursorName + area int +} + +type pointerInfo struct { + id pointer.ID + pressed bool + handlers []event.Tag + // last tracks the last pointer event received, + // used while processing frame events. + last pointer.Event + + // entered tracks the tags that contain the pointer. + entered []event.Tag +} + +type pointerHandler struct { + area int + active bool + wantsGrab bool + types pointer.Type + // min and max horizontal/vertical scroll + scrollRange image.Rectangle +} + +type areaOp struct { + kind areaKind + rect f32.Rectangle +} + +type areaNode struct { + trans f32.Affine2D + next int + area areaOp +} + +type areaKind uint8 + +// collectState represents the state for collectHandlers +type collectState struct { + t f32.Affine2D + area int + node int + pass bool +} + +const ( + areaRect areaKind = iota + areaEllipse +) + +func (q *pointerQueue) save(id int, state collectState) { + if extra := id - len(q.states) + 1; extra > 0 { + q.states = append(q.states, make([]collectState, extra)...) + } + q.states[id] = state +} + +func (q *pointerQueue) collectHandlers(r *ops.Reader, events *handlerEvents) { + state := collectState{ + area: -1, + node: -1, + } + q.save(opconst.InitialStateID, state) + for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { + switch opconst.OpType(encOp.Data[0]) { + case opconst.TypeSave: + id := ops.DecodeSave(encOp.Data) + q.save(id, state) + case opconst.TypeLoad: + id, mask := ops.DecodeLoad(encOp.Data) + s := q.states[id] + if mask&opconst.TransformState != 0 { + state.t = s.t + } + if mask&^opconst.TransformState != 0 { + state = s + } + case opconst.TypePass: + state.pass = encOp.Data[1] != 0 + case opconst.TypeArea: + var op areaOp + op.Decode(encOp.Data) + q.areas = append(q.areas, + areaNode{trans: state.t, next: state.area, area: op}) + state.area = len(q.areas) - 1 + q.hitTree = append(q.hitTree, hitNode{ + next: state.node, + area: state.area, + pass: state.pass, + }) + state.node = len(q.hitTree) - 1 + case opconst.TypeTransform: + dop := ops.DecodeTransform(encOp.Data) + state.t = state.t.Mul(dop) + case opconst.TypePointerInput: + op := pointer.InputOp{ + Tag: encOp.Refs[0].(event.Tag), + Grab: encOp.Data[1] != 0, + Types: pointer.Type(encOp.Data[2]), + } + q.hitTree = append(q.hitTree, hitNode{ + next: state.node, + area: state.area, + pass: state.pass, + tag: op.Tag, + }) + state.node = len(q.hitTree) - 1 + h, ok := q.handlers[op.Tag] + if !ok { + h = new(pointerHandler) + q.handlers[op.Tag] = h + // Cancel handlers on (each) first appearance, but don't + // trigger redraw. + events.AddNoRedraw(op.Tag, pointer.Event{Type: pointer.Cancel}) + } + h.active = true + h.area = state.area + h.wantsGrab = h.wantsGrab || op.Grab + h.types = h.types | op.Types + bo := binary.LittleEndian.Uint32 + h.scrollRange = image.Rectangle{ + Min: image.Point{ + X: int(int32(bo(encOp.Data[3:]))), + Y: int(int32(bo(encOp.Data[7:]))), + }, + Max: image.Point{ + X: int(int32(bo(encOp.Data[11:]))), + Y: int(int32(bo(encOp.Data[15:]))), + }, + } + case opconst.TypeCursor: + q.cursors = append(q.cursors, cursorNode{ + name: encOp.Refs[0].(pointer.CursorName), + area: len(q.areas) - 1, + }) + } + } +} + +func (q *pointerQueue) opHit(handlers *[]event.Tag, pos f32.Point) { + // Track whether we're passing through hits. + pass := true + idx := len(q.hitTree) - 1 + for idx >= 0 { + n := &q.hitTree[idx] + if !q.hit(n.area, pos) { + idx-- + continue + } + pass = pass && n.pass + if pass { + idx-- + } else { + idx = n.next + } + if n.tag != nil { + if _, exists := q.handlers[n.tag]; exists { + *handlers = append(*handlers, n.tag) + } + } + } +} + +func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point { + if areaIdx == -1 { + return p + } + return q.areas[areaIdx].trans.Invert().Transform(p) +} + +func (q *pointerQueue) hit(areaIdx int, p f32.Point) bool { + for areaIdx != -1 { + a := &q.areas[areaIdx] + p := a.trans.Invert().Transform(p) + if !a.area.Hit(p) { + return false + } + areaIdx = a.next + } + return true +} + +func (q *pointerQueue) reset() { + if q.handlers == nil { + q.handlers = make(map[event.Tag]*pointerHandler) + } +} + +func (q *pointerQueue) Frame(root *op.Ops, events *handlerEvents) { + q.reset() + for _, h := range q.handlers { + // Reset handler. + h.active = false + h.wantsGrab = false + h.types = 0 + } + q.hitTree = q.hitTree[:0] + q.areas = q.areas[:0] + q.cursors = q.cursors[:0] + q.reader.Reset(root) + q.collectHandlers(&q.reader, events) + for k, h := range q.handlers { + if !h.active { + q.dropHandlers(events, k) + delete(q.handlers, k) + } + if h.wantsGrab { + for _, p := range q.pointers { + if !p.pressed { + continue + } + for i, k2 := range p.handlers { + if k2 == k { + // Drop other handlers that lost their grab. + dropped := make([]event.Tag, 0, len(p.handlers)-1) + dropped = append(dropped, p.handlers[:i]...) + dropped = append(dropped, p.handlers[i+1:]...) + cancelHandlers(events, dropped...) + q.dropHandlers(events, dropped...) + break + } + } + } + } + } + for i := range q.pointers { + p := &q.pointers[i] + q.deliverEnterLeaveEvents(p, events, p.last) + } +} + +func cancelHandlers(events *handlerEvents, tags ...event.Tag) { + for _, k := range tags { + events.Add(k, pointer.Event{Type: pointer.Cancel}) + } +} + +func (q *pointerQueue) dropHandlers(events *handlerEvents, tags ...event.Tag) { + for _, k := range tags { + for i := range q.pointers { + p := &q.pointers[i] + for i := len(p.handlers) - 1; i >= 0; i-- { + if p.handlers[i] == k { + p.handlers = append(p.handlers[:i], p.handlers[i+1:]...) + } + } + for i := len(p.entered) - 1; i >= 0; i-- { + if p.entered[i] == k { + p.entered = append(p.entered[:i], p.entered[i+1:]...) + } + } + } + } +} + +func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) { + q.reset() + if e.Type == pointer.Cancel { + q.pointers = q.pointers[:0] + for k := range q.handlers { + cancelHandlers(events, k) + q.dropHandlers(events, k) + } + return + } + pidx := -1 + for i, p := range q.pointers { + if p.id == e.PointerID { + pidx = i + break + } + } + if pidx == -1 { + q.pointers = append(q.pointers, pointerInfo{id: e.PointerID}) + pidx = len(q.pointers) - 1 + } + p := &q.pointers[pidx] + p.last = e + + if e.Type == pointer.Move && p.pressed { + e.Type = pointer.Drag + } + + if e.Type == pointer.Release { + q.deliverEvent(p, events, e) + p.pressed = false + } + q.deliverEnterLeaveEvents(p, events, e) + + if !p.pressed { + p.handlers = append(p.handlers[:0], q.scratch...) + } + if e.Type == pointer.Press { + p.pressed = true + } + switch e.Type { + case pointer.Release: + case pointer.Scroll: + q.deliverScrollEvent(p, events, e) + default: + q.deliverEvent(p, events, e) + } + if !p.pressed && len(p.entered) == 0 { + // No longer need to track pointer. + q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...) + } +} + +func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, + e pointer.Event) { + foremost := true + if p.pressed && len(p.handlers) == 1 { + e.Priority = pointer.Grabbed + foremost = false + } + for _, k := range p.handlers { + h := q.handlers[k] + if e.Type&h.types == 0 { + continue + } + e := e + if foremost { + foremost = false + e.Priority = pointer.Foremost + } + e.Position = q.invTransform(h.area, e.Position) + events.Add(k, e) + } +} + +func (q *pointerQueue) deliverScrollEvent(p *pointerInfo, events *handlerEvents, + e pointer.Event) { + foremost := true + if p.pressed && len(p.handlers) == 1 { + e.Priority = pointer.Grabbed + foremost = false + } + var sx, sy = e.Scroll.X, e.Scroll.Y + for _, k := range p.handlers { + if sx == 0 && sy == 0 { + return + } + h := q.handlers[k] + // Distribute the scroll to the handler based on its ScrollRange. + sx, e.Scroll.X = setScrollEvent(sx, h.scrollRange.Min.X, + h.scrollRange.Max.X) + sy, e.Scroll.Y = setScrollEvent(sy, h.scrollRange.Min.Y, + h.scrollRange.Max.Y) + e := e + if foremost { + foremost = false + e.Priority = pointer.Foremost + } + e.Position = q.invTransform(h.area, e.Position) + events.Add(k, e) + } +} + +func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, + events *handlerEvents, e pointer.Event) { + q.scratch = q.scratch[:0] + q.opHit(&q.scratch, e.Position) + if p.pressed { + // Filter out non-participating handlers. + for i := len(q.scratch) - 1; i >= 0; i-- { + if _, found := searchTag(p.handlers, q.scratch[i]); !found { + q.scratch = append(q.scratch[:i], q.scratch[i+1:]...) + } + } + } + hits := q.scratch + if e.Source != pointer.Mouse && !p.pressed && e.Type != pointer.Press { + // Consider non-mouse pointers leaving when they're released. + hits = nil + } + // Deliver Leave events. + for _, k := range p.entered { + if _, found := searchTag(hits, k); found { + continue + } + h := q.handlers[k] + e.Type = pointer.Leave + + if e.Type&h.types != 0 { + e.Position = q.invTransform(h.area, e.Position) + events.Add(k, e) + } + } + // Deliver Enter events and update cursor. + q.cursor = pointer.CursorDefault + for _, k := range hits { + h := q.handlers[k] + for i := len(q.cursors) - 1; i >= 0; i-- { + if c := q.cursors[i]; c.area == h.area { + q.cursor = c.name + break + } + } + if _, found := searchTag(p.entered, k); found { + continue + } + e.Type = pointer.Enter + + if e.Type&h.types != 0 { + e.Position = q.invTransform(h.area, e.Position) + events.Add(k, e) + } + } + p.entered = append(p.entered[:0], hits...) +} + +func searchTag(tags []event.Tag, tag event.Tag) (int, bool) { + for i, t := range tags { + if t == tag { + return i, true + } + } + return 0, false +} + +func opDecodeFloat32(d []byte) float32 { + return float32(int32(binary.LittleEndian.Uint32(d))) +} + +func (op *areaOp) Decode(d []byte) { + if opconst.OpType(d[0]) != opconst.TypeArea { + panic("invalid op") + } + rect := f32.Rectangle{ + Min: f32.Point{ + X: opDecodeFloat32(d[2:]), + Y: opDecodeFloat32(d[6:]), + }, + Max: f32.Point{ + X: opDecodeFloat32(d[10:]), + Y: opDecodeFloat32(d[14:]), + }, + } + *op = areaOp{ + kind: areaKind(d[1]), + rect: rect, + } +} + +func (op *areaOp) Hit(pos f32.Point) bool { + pos = pos.Sub(op.rect.Min) + size := op.rect.Size() + switch op.kind { + case areaRect: + return 0 <= pos.X && pos.X < size.X && + 0 <= pos.Y && pos.Y < size.Y + case areaEllipse: + rx := size.X / 2 + ry := size.Y / 2 + xh := pos.X - rx + yk := pos.Y - ry + // The ellipse function works in all cases because + // 0/0 is not <= 1. + return (xh*xh)/(rx*rx)+(yk*yk)/(ry*ry) <= 1 + default: + panic("invalid area kind") + } +} + +func setScrollEvent(scroll float32, min, max int) (left, scrolled float32) { + if v := float32(max); scroll > v { + return scroll - v, v + } + if v := float32(min); scroll < v { + return scroll - v, v + } + return 0, scroll +} diff --git a/gio/giold/io/router/pointer_test.go b/gio/giold/io/router/pointer_test.go new file mode 100644 index 0000000..5a28d0e --- /dev/null +++ b/gio/giold/io/router/pointer_test.go @@ -0,0 +1,787 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "fmt" + "image" + "reflect" + "testing" + + "realy.lol/gio/f32" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/op" +) + +func TestPointerWakeup(t *testing.T) { + handler := new(int) + var ops op.Ops + addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100)) + + var r Router + // Test that merely adding a handler doesn't trigger redraw. + r.Frame(&ops) + if _, wake := r.WakeupTime(); wake { + t.Errorf("adding pointer.InputOp triggered a redraw") + } + // However, adding a handler queues a Cancel event. + assertEventSequence(t, r.Events(handler), pointer.Cancel) + // Verify that r.Events does trigger a redraw. + r.Frame(&ops) + if _, wake := r.WakeupTime(); !wake { + t.Errorf("pointer.Cancel event didn't trigger a redraw") + } +} + +func TestPointerDrag(t *testing.T) { + handler := new(int) + var ops op.Ops + addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100)) + + var r Router + r.Frame(&ops) + r.Queue( + // Press. + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + // Move outside the area. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(150, 150), + }, + ) + assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter, + pointer.Press, pointer.Leave, pointer.Drag) +} + +func TestPointerDragNegative(t *testing.T) { + handler := new(int) + var ops op.Ops + addPointerHandler(&ops, handler, image.Rect(-100, -100, 0, 0)) + + var r Router + r.Frame(&ops) + r.Queue( + // Press. + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(-50, -50), + }, + // Move outside the area. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(-150, -150), + }, + ) + assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter, + pointer.Press, pointer.Leave, pointer.Drag) +} + +func TestPointerGrab(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + handler3 := new(int) + var ops op.Ops + + types := pointer.Press | pointer.Release + + pointer.InputOp{Tag: handler1, Types: types, Grab: true}.Add(&ops) + pointer.InputOp{Tag: handler2, Types: types}.Add(&ops) + pointer.InputOp{Tag: handler3, Types: types}.Add(&ops) + + var r Router + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Press) + assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Press) + assertEventSequence(t, r.Events(handler3), pointer.Cancel, pointer.Press) + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Release, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Release) + assertEventSequence(t, r.Events(handler2), pointer.Cancel) + assertEventSequence(t, r.Events(handler3), pointer.Cancel) +} + +func TestPointerMove(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + var ops op.Ops + + types := pointer.Move | pointer.Enter | pointer.Leave + + // Handler 1 area: (0, 0) - (100, 100) + pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops) + pointer.InputOp{Tag: handler1, Types: types}.Add(&ops) + // Handler 2 area: (50, 50) - (100, 100) (areas intersect). + pointer.Rect(image.Rect(50, 50, 200, 200)).Add(&ops) + pointer.InputOp{Tag: handler2, Types: types}.Add(&ops) + + var r Router + r.Frame(&ops) + r.Queue( + // Hit both handlers. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + // Hit handler 1. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(49, 50), + }, + // Hit no handlers. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(100, 50), + }, + pointer.Event{ + Type: pointer.Cancel, + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, + pointer.Move, pointer.Move, pointer.Leave, pointer.Cancel) + assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter, + pointer.Move, pointer.Leave, pointer.Cancel) +} + +func TestPointerTypes(t *testing.T) { + handler := new(int) + var ops op.Ops + pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops) + pointer.InputOp{ + Tag: handler, + Types: pointer.Press | pointer.Release, + }.Add(&ops) + + var r Router + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(150, 150), + }, + pointer.Event{ + Type: pointer.Release, + Position: f32.Pt(150, 150), + }, + ) + assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Press, + pointer.Release) +} + +func TestPointerPriority(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + handler3 := new(int) + var ops op.Ops + + st := op.Save(&ops) + pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops) + pointer.InputOp{ + Tag: handler1, + Types: pointer.Scroll, + ScrollBounds: image.Rectangle{Max: image.Point{X: 100}}, + }.Add(&ops) + + pointer.Rect(image.Rect(0, 0, 100, 50)).Add(&ops) + pointer.InputOp{ + Tag: handler2, + Types: pointer.Scroll, + ScrollBounds: image.Rectangle{Max: image.Point{X: 20}}, + }.Add(&ops) + st.Load() + + pointer.Rect(image.Rect(0, 100, 100, 200)).Add(&ops) + pointer.InputOp{ + Tag: handler3, + Types: pointer.Scroll, + ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}}, + }.Add(&ops) + + var r Router + r.Frame(&ops) + r.Queue( + // Hit handler 1 and 2. + pointer.Event{ + Type: pointer.Scroll, + Position: f32.Pt(50, 25), + Scroll: f32.Pt(50, 0), + }, + // Hit handler 1. + pointer.Event{ + Type: pointer.Scroll, + Position: f32.Pt(50, 75), + Scroll: f32.Pt(50, 50), + }, + // Hit handler 3. + pointer.Event{ + Type: pointer.Scroll, + Position: f32.Pt(50, 150), + Scroll: f32.Pt(-30, -30), + }, + // Hit no handlers. + pointer.Event{ + Type: pointer.Scroll, + Position: f32.Pt(50, 225), + }, + ) + + hev1 := r.Events(handler1) + hev2 := r.Events(handler2) + hev3 := r.Events(handler3) + assertEventSequence(t, hev1, pointer.Cancel, pointer.Scroll, pointer.Scroll) + assertEventSequence(t, hev2, pointer.Cancel, pointer.Scroll) + assertEventSequence(t, hev3, pointer.Cancel, pointer.Scroll) + assertEventPriorities(t, hev1, pointer.Shared, pointer.Shared, + pointer.Foremost) + assertEventPriorities(t, hev2, pointer.Shared, pointer.Foremost) + assertEventPriorities(t, hev3, pointer.Shared, pointer.Foremost) + assertScrollEvent(t, hev1[1], f32.Pt(30, 0)) + assertScrollEvent(t, hev2[1], f32.Pt(20, 0)) + assertScrollEvent(t, hev1[2], f32.Pt(50, 0)) + assertScrollEvent(t, hev3[1], f32.Pt(-20, -30)) +} + +func TestPointerEnterLeave(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + var ops op.Ops + + // Handler 1 area: (0, 0) - (100, 100) + addPointerHandler(&ops, handler1, image.Rect(0, 0, 100, 100)) + + // Handler 2 area: (50, 50) - (200, 200) (areas overlap). + addPointerHandler(&ops, handler2, image.Rect(50, 50, 200, 200)) + + var r Router + r.Frame(&ops) + // Hit both handlers. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + // First event for a handler is always a Cancel. + // Only handler2 should receive the enter/move events because it is on top + // and handler1 is not an ancestor in the hit tree. + assertEventSequence(t, r.Events(handler1), pointer.Cancel) + assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter, + pointer.Move) + + // Leave the second area by moving into the first. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(45, 45), + }, + ) + // The cursor leaves handler2 and enters handler1. + assertEventSequence(t, r.Events(handler1), pointer.Enter, pointer.Move) + assertEventSequence(t, r.Events(handler2), pointer.Leave) + + // Move, but stay within the same hit area. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(40, 40), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Move) + assertEventSequence(t, r.Events(handler2)) + + // Move outside of both inputs. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(300, 300), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Leave) + assertEventSequence(t, r.Events(handler2)) + + // Check that a Press event generates Enter Events. + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(125, 125), + }, + ) + assertEventSequence(t, r.Events(handler1)) + assertEventSequence(t, r.Events(handler2), pointer.Enter, pointer.Press) + + // Check that a drag only affects the participating handlers. + r.Queue( + // Leave + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(25, 25), + }, + // Enter + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler1)) + assertEventSequence(t, r.Events(handler2), pointer.Leave, pointer.Drag, + pointer.Enter, pointer.Drag) + + // Check that a Release event generates Enter/Leave Events. + r.Queue( + pointer.Event{ + Type: pointer.Release, + Position: f32.Pt(25, + 25), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Enter) + // The second handler gets the release event because the press started inside it. + assertEventSequence(t, r.Events(handler2), pointer.Release, pointer.Leave) + +} + +func TestMultipleAreas(t *testing.T) { + handler := new(int) + + var ops op.Ops + + addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100)) + st := op.Save(&ops) + pointer.Rect(image.Rect(50, 50, 200, 200)).Add(&ops) + // Second area has no Types set, yet should receive events because + // Types for the same handles are or-ed together. + pointer.InputOp{Tag: handler}.Add(&ops) + st.Load() + + var r Router + r.Frame(&ops) + // Hit first area, then second area, then both. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(25, 25), + }, + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(150, 150), + }, + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter, + pointer.Move, pointer.Move, pointer.Move) +} + +func TestPointerEnterLeaveNested(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + var ops op.Ops + + types := pointer.Press | pointer.Move | pointer.Release | pointer.Enter | pointer.Leave + + // Handler 1 area: (0, 0) - (100, 100) + pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops) + pointer.InputOp{Tag: handler1, Types: types}.Add(&ops) + + // Handler 2 area: (25, 25) - (75, 75) (nested within first). + pointer.Rect(image.Rect(25, 25, 75, 75)).Add(&ops) + pointer.InputOp{Tag: handler2, Types: types}.Add(&ops) + + var r Router + r.Frame(&ops) + // Hit both handlers. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + // First event for a handler is always a Cancel. + // Both handlers should receive the Enter and Move events because handler2 is a child of handler1. + assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, + pointer.Move) + assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter, + pointer.Move) + + // Leave the second area by moving into the first. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(20, 20), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Move) + assertEventSequence(t, r.Events(handler2), pointer.Leave) + + // Move, but stay within the same hit area. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(10, 10), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Move) + assertEventSequence(t, r.Events(handler2)) + + // Move outside of both inputs. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(200, 200), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Leave) + assertEventSequence(t, r.Events(handler2)) + + // Check that a Press event generates Enter Events. + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Enter, pointer.Press) + assertEventSequence(t, r.Events(handler2), pointer.Enter, pointer.Press) + + // Check that a Release event generates Enter/Leave Events. + r.Queue( + pointer.Event{ + Type: pointer.Release, + Position: f32.Pt(20, 20), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Release) + assertEventSequence(t, r.Events(handler2), pointer.Release, pointer.Leave) +} + +func TestPointerActiveInputDisappears(t *testing.T) { + handler1 := new(int) + var ops op.Ops + var r Router + + // Draw handler. + ops.Reset() + addPointerHandler(&ops, handler1, image.Rect(0, 0, 100, 100)) + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(25, 25), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, + pointer.Move) + + // Re-render with handler missing. + ops.Reset() + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(25, 25), + }, + ) + assertEventSequence(t, r.Events(handler1)) +} + +func TestMultitouch(t *testing.T) { + var ops op.Ops + + // Add two separate handlers. + h1, h2 := new(int), new(int) + addPointerHandler(&ops, h1, image.Rect(0, 0, 100, 100)) + addPointerHandler(&ops, h2, image.Rect(0, 100, 100, 200)) + + h1pt, h2pt := f32.Pt(0, 0), f32.Pt(0, 100) + var p1, p2 pointer.ID = 0, 1 + + var r Router + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: h1pt, + PointerID: p1, + }, + ) + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: h2pt, + PointerID: p2, + }, + ) + r.Queue( + pointer.Event{ + Type: pointer.Release, + Position: h2pt, + PointerID: p2, + }, + ) + assertEventSequence(t, r.Events(h1), pointer.Cancel, pointer.Enter, + pointer.Press) + assertEventSequence(t, r.Events(h2), pointer.Cancel, pointer.Enter, + pointer.Press, pointer.Release) +} + +func TestCursorNameOp(t *testing.T) { + ops := new(op.Ops) + var r Router + var h, h2 int + var widget2 func() + widget := func() { + // This is the area where the cursor is changed to CursorPointer. + pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops) + // The cursor is checked and changed upon cursor movement. + pointer.InputOp{Tag: &h}.Add(ops) + pointer.CursorNameOp{Name: pointer.CursorPointer}.Add(ops) + if widget2 != nil { + widget2() + } + } + // Register the handlers. + widget() + // No cursor change as the mouse has not moved yet. + if got, want := r.Cursor(), pointer.CursorDefault; got != want { + t.Errorf("got %q; want %q", got, want) + } + + _at := func(x, y float32) pointer.Event { + return pointer.Event{ + Type: pointer.Move, + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Position: f32.Pt(x, y), + } + } + for _, tc := range []struct { + label string + event interface{} + want pointer.CursorName + }{ + {label: "move inside", + event: _at(50, 50), + want: pointer.CursorPointer, + }, + {label: "move outside", + event: _at(200, 200), + want: pointer.CursorDefault, + }, + {label: "move back inside", + event: _at(50, 50), + want: pointer.CursorPointer, + }, + {label: "send key events while inside", + event: []event.Event{ + key.Event{Name: "A", State: key.Press}, + key.Event{Name: "A", State: key.Release}, + }, + want: pointer.CursorPointer, + }, + {label: "send key events while outside", + event: []event.Event{ + _at(200, 200), + key.Event{Name: "A", State: key.Press}, + key.Event{Name: "A", State: key.Release}, + }, + want: pointer.CursorDefault, + }, + {label: "add new input on top while inside", + event: func() []event.Event { + widget2 = func() { + pointer.InputOp{Tag: &h2}.Add(ops) + pointer.CursorNameOp{Name: pointer.CursorCrossHair}.Add(ops) + } + return []event.Event{ + _at(50, 50), + key.Event{ + Name: "A", + State: key.Press, + }, + } + }, + want: pointer.CursorCrossHair, + }, + {label: "remove input on top while inside", + event: func() []event.Event { + widget2 = nil + return []event.Event{ + _at(50, 50), + key.Event{ + Name: "A", + State: key.Press, + }, + } + }, + want: pointer.CursorPointer, + }, + } { + t.Run(tc.label, func(t *testing.T) { + ops.Reset() + widget() + r.Frame(ops) + switch ev := tc.event.(type) { + case event.Event: + r.Queue(ev) + case []event.Event: + r.Queue(ev...) + case func() event.Event: + r.Queue(ev()) + case func() []event.Event: + r.Queue(ev()...) + default: + panic(fmt.Sprintf("unkown event %T", ev)) + } + widget() + r.Frame(ops) + // The cursor should now have been changed if the mouse moved over the declared area. + if got, want := r.Cursor(), tc.want; got != want { + t.Errorf("got %q; want %q", got, want) + } + }) + } +} + +// addPointerHandler adds a pointer.InputOp for the tag in a +// rectangular area. +func addPointerHandler(ops *op.Ops, tag event.Tag, area image.Rectangle) { + defer op.Save(ops).Load() + pointer.Rect(area).Add(ops) + pointer.InputOp{ + Tag: tag, + Types: pointer.Press | pointer.Release | pointer.Move | pointer.Drag | pointer.Enter | pointer.Leave, + }.Add(ops) +} + +// pointerTypes converts a sequence of event.Event to their pointer.Types. It assumes +// that all input events are of underlying type pointer.Event, and thus will +// panic if some are not. +func pointerTypes(events []event.Event) []pointer.Type { + var types []pointer.Type + for _, e := range events { + if e, ok := e.(pointer.Event); ok { + types = append(types, e.Type) + } + } + return types +} + +// assertEventSequence checks that the provided events match the expected pointer event types +// in the provided order. +func assertEventSequence(t *testing.T, events []event.Event, + expected ...pointer.Type) { + t.Helper() + got := pointerTypes(events) + if !reflect.DeepEqual(got, expected) { + t.Errorf("expected %v events, got %v", expected, got) + } +} + +// assertEventPriorities checks that the pointer.Event priorities of events match prios. +func assertEventPriorities(t *testing.T, events []event.Event, + prios ...pointer.Priority) { + t.Helper() + var got []pointer.Priority + for _, e := range events { + if e, ok := e.(pointer.Event); ok { + got = append(got, e.Priority) + } + } + if !reflect.DeepEqual(got, prios) { + t.Errorf("expected priorities %v, got %v", prios, got) + } +} + +// assertScrollEvent checks that the event scrolling amount matches the supplied value. +func assertScrollEvent(t *testing.T, ev event.Event, scroll f32.Point) { + t.Helper() + if got, want := ev.(pointer.Event).Scroll, scroll; got != want { + t.Errorf("got %v; want %v", got, want) + } +} + +func BenchmarkRouterAdd(b *testing.B) { + // Set this to the number of overlapping handlers that you want to + // evaluate performance for. Typical values for the example applications + // are 1-3, though checking highers values helps evaluate performance for + // more complex applications. + const startingHandlerCount = 3 + const maxHandlerCount = 100 + for i := startingHandlerCount; i < maxHandlerCount; i *= 3 { + handlerCount := i + b.Run(fmt.Sprintf("%d-handlers", i), func(b *testing.B) { + handlers := make([]event.Tag, handlerCount) + for i := 0; i < handlerCount; i++ { + h := new(int) + *h = i + handlers[i] = h + } + var ops op.Ops + + for i := range handlers { + pointer.Rect(image.Rectangle{ + Max: image.Point{ + X: 100, + Y: 100, + }, + }).Add(&ops) + pointer.InputOp{ + Tag: handlers[i], + Types: pointer.Move, + }.Add(&ops) + } + var r Router + r.Frame(&ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + } + }) + } +} + +var benchAreaOp areaOp + +func BenchmarkAreaOp_Decode(b *testing.B) { + ops := new(op.Ops) + pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops) + for i := 0; i < b.N; i++ { + benchAreaOp.Decode(ops.Data()) + } +} + +func BenchmarkAreaOp_Hit(b *testing.B) { + ops := new(op.Ops) + pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops) + benchAreaOp.Decode(ops.Data()) + for i := 0; i < b.N; i++ { + benchAreaOp.Hit(f32.Pt(50, 50)) + } +} diff --git a/gio/giold/io/router/router.go b/gio/giold/io/router/router.go new file mode 100644 index 0000000..f7e251b --- /dev/null +++ b/gio/giold/io/router/router.go @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package router implements Router, a event.Queue implementation +that that disambiguates and routes events to handlers declared +in operation lists. + +Router is used by app.Window and is otherwise only useful for +using Gio with external window implementations. +*/ +package router + +import ( + "encoding/binary" + "time" + + "realy.lol/gio/internal/opconst" + "realy.lol/gio/internal/ops" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/profile" + "realy.lol/gio/op" +) + +// Router is a Queue implementation that routes events +// to handlers declared in operation lists. +type Router struct { + pqueue pointerQueue + kqueue keyQueue + cqueue clipboardQueue + + handlers handlerEvents + + reader ops.Reader + + // InvalidateOp summary. + wakeup bool + wakeupTime time.Time + + // ProfileOp summary. + profHandlers map[event.Tag]struct{} + profile profile.Event +} + +type handlerEvents struct { + handlers map[event.Tag][]event.Event + hadEvents bool +} + +// Events returns the available events for the handler key. +func (q *Router) Events(k event.Tag) []event.Event { + events := q.handlers.Events(k) + if _, isprof := q.profHandlers[k]; isprof { + delete(q.profHandlers, k) + events = append(events, q.profile) + } + return events +} + +// Frame replaces the declared handlers from the supplied +// operation list. The text input state, wakeup time and whether +// there are active profile handlers is also saved. +func (q *Router) Frame(ops *op.Ops) { + q.handlers.Clear() + q.wakeup = false + for k := range q.profHandlers { + delete(q.profHandlers, k) + } + q.reader.Reset(ops) + q.collect() + + q.pqueue.Frame(ops, &q.handlers) + q.kqueue.Frame(ops, &q.handlers) + if q.handlers.HadEvents() { + q.wakeup = true + q.wakeupTime = time.Time{} + } +} + +// Queue an event and report whether at least one handler had an event queued. +func (q *Router) Queue(events ...event.Event) bool { + for _, e := range events { + switch e := e.(type) { + case profile.Event: + q.profile = e + case pointer.Event: + q.pqueue.Push(e, &q.handlers) + case key.EditEvent, key.Event, key.FocusEvent: + q.kqueue.Push(e, &q.handlers) + case clipboard.Event: + q.cqueue.Push(e, &q.handlers) + } + } + return q.handlers.HadEvents() +} + +// TextInputState returns the input state from the most recent +// call to Frame. +func (q *Router) TextInputState() TextInputState { + return q.kqueue.InputState() +} + +// WriteClipboard returns the most recent text to be copied +// to the clipboard, if any. +func (q *Router) WriteClipboard() (string, bool) { + return q.cqueue.WriteClipboard() +} + +// ReadClipboard reports if any new handler is waiting +// to read the clipboard. +func (q *Router) ReadClipboard() bool { + return q.cqueue.ReadClipboard() +} + +// Cursor returns the last cursor set. +func (q *Router) Cursor() pointer.CursorName { + return q.pqueue.cursor +} + +func (q *Router) collect() { + for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() { + switch opconst.OpType(encOp.Data[0]) { + case opconst.TypeInvalidate: + op := decodeInvalidateOp(encOp.Data) + if !q.wakeup || op.At.Before(q.wakeupTime) { + q.wakeup = true + q.wakeupTime = op.At + } + case opconst.TypeProfile: + op := decodeProfileOp(encOp.Data, encOp.Refs) + if q.profHandlers == nil { + q.profHandlers = make(map[event.Tag]struct{}) + } + q.profHandlers[op.Tag] = struct{}{} + case opconst.TypeClipboardRead: + q.cqueue.ProcessReadClipboard(encOp.Data, encOp.Refs) + case opconst.TypeClipboardWrite: + q.cqueue.ProcessWriteClipboard(encOp.Data, encOp.Refs) + } + } +} + +// Profiling reports whether there was profile handlers in the +// most recent Frame call. +func (q *Router) Profiling() bool { + return len(q.profHandlers) > 0 +} + +// WakeupTime returns the most recent time for doing another frame, +// as determined from the last call to Frame. +func (q *Router) WakeupTime() (time.Time, bool) { + return q.wakeupTime, q.wakeup +} + +func (h *handlerEvents) init() { + if h.handlers == nil { + h.handlers = make(map[event.Tag][]event.Event) + } +} + +func (h *handlerEvents) AddNoRedraw(k event.Tag, e event.Event) { + h.init() + h.handlers[k] = append(h.handlers[k], e) +} + +func (h *handlerEvents) Add(k event.Tag, e event.Event) { + h.AddNoRedraw(k, e) + h.hadEvents = true +} + +func (h *handlerEvents) HadEvents() bool { + u := h.hadEvents + h.hadEvents = false + return u +} + +func (h *handlerEvents) Events(k event.Tag) []event.Event { + if events, ok := h.handlers[k]; ok { + h.handlers[k] = h.handlers[k][:0] + // Schedule another frame if we delivered events to the user + // to flush half-updated state. This is important when an + // event changes UI state that has already been laid out. In + // the worst case, we waste a frame, increasing power usage. + // + // Gio is expected to grow the ability to construct + // frame-to-frame differences and only render to changed + // areas. In that case, the waste of a spurious frame should + // be minimal. + h.hadEvents = h.hadEvents || len(events) > 0 + return events + } + return nil +} + +func (h *handlerEvents) Clear() { + for k := range h.handlers { + delete(h.handlers, k) + } +} + +func decodeProfileOp(d []byte, refs []interface{}) profile.Op { + if opconst.OpType(d[0]) != opconst.TypeProfile { + panic("invalid op") + } + return profile.Op{ + Tag: refs[0].(event.Tag), + } +} + +func decodeInvalidateOp(d []byte) op.InvalidateOp { + bo := binary.LittleEndian + if opconst.OpType(d[0]) != opconst.TypeInvalidate { + panic("invalid op") + } + var o op.InvalidateOp + if nanos := bo.Uint64(d[1:]); nanos > 0 { + o.At = time.Unix(0, int64(nanos)) + } + return o +} diff --git a/gio/giold/io/system/system.go b/gio/giold/io/system/system.go new file mode 100644 index 0000000..14e4dd7 --- /dev/null +++ b/gio/giold/io/system/system.go @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package system contains events usually handled at the top-level +// program level. +package system + +import ( + "image" + "time" + + "realy.lol/gio/io/event" + "realy.lol/gio/op" + "realy.lol/gio/unit" +) + +// A FrameEvent requests a new frame in the form of a list of +// operations that describes what to display and how to handle +// input. +type FrameEvent struct { + // Now is the current animation. Use Now instead of time.Now to + // synchronize animation and to avoid the time.Now call overhead. + Now time.Time + // Metric converts device independent dp and sp to device pixels. + Metric unit.Metric + // Size is the dimensions of the window. + Size image.Point + // Insets is the insets to apply. + Insets Insets + // Frame is the callback to supply the list of + // operations to complete the FrameEvent. + // + // Note that the operation list and the operations themselves + // may not be mutated until another FrameEvent is received from + // the same event source. + // That means that calls to frame.Reset and changes to referenced + // data such as ImageOp backing images should happen between + // receiving a FrameEvent and calling Frame. + // + // Example: + // + // var w *app.Window + // var frame *op.Ops + // for e := range w.Events() { + // if e, ok := e.(system.FrameEvent); ok { + // // Call frame.Reset and manipulate images for ImageOps + // // here. + // e.Frame(frame) + // } + // } + Frame func(frame *op.Ops) + // Queue supplies the events for event handlers. + Queue event.Queue +} + +// DestroyEvent is the last event sent through +// a window event channel. +type DestroyEvent struct { + // Err is nil for normal window closures. If a + // window is prematurely closed, Err is the cause. + Err error +} + +// Insets is the space taken up by +// system decoration such as translucent +// system bars and software keyboards. +type Insets struct { + Top, Bottom, Left, Right unit.Value +} + +// A StageEvent is generated whenever the stage of a +// Window changes. +type StageEvent struct { + Stage Stage +} + +// CommandEvent is a system event. Unlike most other events, CommandEvent is +// delivered as a pointer to allow Cancel to suppress it. +type CommandEvent struct { + Type CommandType + // Cancel suppress the default action of the command. + Cancel bool +} + +// Stage of a Window. +type Stage uint8 + +// CommandType is the type of a CommandEvent. +type CommandType uint8 + +const ( + // StagePaused is the Stage for inactive Windows. + // Inactive Windows don't receive FrameEvents. + StagePaused Stage = iota + // StateRunning is for active Windows. + StageRunning +) + +const ( + // CommandBack is the command for a back action + // such as the Android back button. + CommandBack CommandType = iota +) + +func (l Stage) String() string { + switch l { + case StagePaused: + return "StagePaused" + case StageRunning: + return "StageRunning" + default: + panic("unexpected Stage value") + } +} + +func (FrameEvent) ImplementsEvent() {} +func (StageEvent) ImplementsEvent() {} +func (*CommandEvent) ImplementsEvent() {} +func (DestroyEvent) ImplementsEvent() {} diff --git a/gio/giold/layout/alloc_test.go b/gio/giold/layout/alloc_test.go new file mode 100644 index 0000000..1df19e9 --- /dev/null +++ b/gio/giold/layout/alloc_test.go @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build !race +// +build !race + +package layout + +import ( + "image" + "testing" + + "realy.lol/gio/op" +) + +func TestStackAllocs(t *testing.T) { + var ops op.Ops + allocs := testing.AllocsPerRun(1, func() { + ops.Reset() + gtx := Context{ + Ops: &ops, + } + Stack{}.Layout(gtx, + Stacked(func(gtx Context) Dimensions { + return Dimensions{Size: image.Point{X: 50, Y: 50}} + }), + ) + }) + if allocs != 0 { + t.Errorf("expected no allocs, got %f", allocs) + } +} + +func TestFlexAllocs(t *testing.T) { + var ops op.Ops + allocs := testing.AllocsPerRun(1, func() { + ops.Reset() + gtx := Context{ + Ops: &ops, + } + Flex{}.Layout(gtx, + Rigid(func(gtx Context) Dimensions { + return Dimensions{Size: image.Point{X: 50, Y: 50}} + }), + ) + }) + if allocs != 0 { + t.Errorf("expected no allocs, got %f", allocs) + } +} diff --git a/gio/giold/layout/context.go b/gio/giold/layout/context.go new file mode 100644 index 0000000..4f8d2c8 --- /dev/null +++ b/gio/giold/layout/context.go @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "time" + + "realy.lol/gio/f32" + "realy.lol/gio/io/event" + "realy.lol/gio/io/system" + "realy.lol/gio/op" + "realy.lol/gio/unit" +) + +// Context carries the state needed by almost all layouts and widgets. +// A zero value Context never returns events, map units to pixels +// with a scale of 1.0, and returns the zero time from Now. +type Context struct { + // Constraints track the constraints for the active widget or + // layout. + Constraints Constraints + + Metric unit.Metric + // By convention, a nil Queue is a signal to widgets to draw themselves + // in a disabled state. + Queue event.Queue + // Now is the animation time. + Now time.Time + + *op.Ops +} + +// NewContext is a shorthand for +// +// Context{ +// Ops: ops, +// Now: e.Now, +// Queue: e.Queue, +// Config: e.Config, +// Constraints: Exact(e.Size), +// } +// +// NewContext calls ops.Reset and adjusts ops for e.Insets. +func NewContext(ops *op.Ops, e system.FrameEvent) Context { + ops.Reset() + + size := e.Size + + if e.Insets != (system.Insets{}) { + left := e.Metric.Px(e.Insets.Left) + top := e.Metric.Px(e.Insets.Top) + op.Offset(f32.Point{ + X: float32(left), + Y: float32(top), + }).Add(ops) + + size.X -= left + e.Metric.Px(e.Insets.Right) + size.Y -= top + e.Metric.Px(e.Insets.Bottom) + } + + return Context{ + Ops: ops, + Now: e.Now, + Queue: e.Queue, + Metric: e.Metric, + Constraints: Exact(size), + } +} + +// Px maps the value to pixels. +func (c Context) Px(v unit.Value) int { + return c.Metric.Px(v) +} + +// Events returns the events available for the key. If no +// queue is configured, Events returns nil. +func (c Context) Events(k event.Tag) []event.Event { + if c.Queue == nil { + return nil + } + return c.Queue.Events(k) +} + +// Disabled returns a copy of this context with a nil Queue, +// blocking events to widgets using it. +// +// By convention, a nil Queue is a signal to widgets to draw themselves +// in a disabled state. +func (c Context) Disabled() Context { + c.Queue = nil + return c +} diff --git a/gio/giold/layout/doc.go b/gio/giold/layout/doc.go new file mode 100644 index 0000000..3824084 --- /dev/null +++ b/gio/giold/layout/doc.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package layout implements layouts common to GUI programs. + +Constraints and dimensions + +Constraints and dimensions form the interface between layouts and +interface child elements. This package operates on Widgets, functions +that compute Dimensions from a a set of constraints for acceptable +widths and heights. Both the constraints and dimensions are maintained +in an implicit Context to keep the Widget declaration short. + +For example, to add space above a widget: + + var gtx layout.Context + + // Configure a top inset. + inset := layout.Inset{Top: unit.Dp(8), ...} + // Use the inset to lay out a widget. + inset.Layout(gtx, func() { + // Lay out widget and determine its size given the constraints + // in gtx.Constraints. + ... + return layout.Dimensions{...} + }) + +Note that the example does not generate any garbage even though the +Inset is transient. Layouts that don't accept user input are designed +to not escape to the heap during their use. + +Layout operations are recursive: a child in a layout operation can +itself be another layout. That way, complex user interfaces can +be created from a few generic layouts. + +This example both aligns and insets a child: + + inset := layout.Inset{...} + inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + align := layout.Alignment(...) + return align.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return widget.Layout(gtx, ...) + }) + }) + +More complex layouts such as Stack and Flex lay out multiple children, +and stateful layouts such as List accept user input. + +*/ +package layout diff --git a/gio/giold/layout/example_test.go b/gio/giold/layout/example_test.go new file mode 100644 index 0000000..9636c8d --- /dev/null +++ b/gio/giold/layout/example_test.go @@ -0,0 +1,137 @@ +package layout_test + +import ( + "fmt" + "image" + + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/unit" +) + +func ExampleInset() { + gtx := layout.Context{ + Ops: new(op.Ops), + // Loose constraints with no minimal size. + Constraints: layout.Constraints{ + Max: image.Point{X: 100, Y: 100}, + }, + } + + // Inset all edges by 10. + inset := layout.UniformInset(unit.Dp(10)) + dims := inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + // Lay out a 50x50 sized widget. + dims := layoutWidget(gtx, 50, 50) + fmt.Println(dims.Size) + return dims + }) + + fmt.Println(dims.Size) + + // Output: + // (50,50) + // (70,70) +} + +func ExampleDirection() { + gtx := layout.Context{ + Ops: new(op.Ops), + // Rigid constraints with both minimum and maximum set. + Constraints: layout.Exact(image.Point{X: 100, Y: 100}), + } + + dims := layout.Center.Layout(gtx, + func(gtx layout.Context) layout.Dimensions { + // Lay out a 50x50 sized widget. + dims := layoutWidget(gtx, 50, 50) + fmt.Println(dims.Size) + return dims + }) + + fmt.Println(dims.Size) + + // Output: + // (50,50) + // (100,100) +} + +func ExampleFlex() { + gtx := layout.Context{ + Ops: new(op.Ops), + // Rigid constraints with both minimum and maximum set. + Constraints: layout.Exact(image.Point{X: 100, Y: 100}), + } + + layout.Flex{WeightSum: 2}.Layout(gtx, + // Rigid 10x10 widget. + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + fmt.Printf("Rigid: %v\n", gtx.Constraints) + return layoutWidget(gtx, 10, 10) + }), + // Child with 50% space allowance. + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + fmt.Printf("50%%: %v\n", gtx.Constraints) + return layoutWidget(gtx, 10, 10) + }), + ) + + // Output: + // Rigid: {(0,100) (100,100)} + // 50%: {(45,100) (45,100)} +} + +func ExampleStack() { + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Constraints{ + Max: image.Point{X: 100, Y: 100}, + }, + } + + layout.Stack{}.Layout(gtx, + // Force widget to the same size as the second. + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + fmt.Printf("Expand: %v\n", gtx.Constraints) + return layoutWidget(gtx, 10, 10) + }), + // Rigid 50x50 widget. + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + return layoutWidget(gtx, 50, 50) + }), + ) + + // Output: + // Expand: {(50,50) (100,100)} +} + +func ExampleList() { + gtx := layout.Context{ + Ops: new(op.Ops), + // Rigid constraints with both minimum and maximum set. + Constraints: layout.Exact(image.Point{X: 100, Y: 100}), + } + + // The list is 1e6 elements, but only 5 fit the constraints. + const listLen = 1e6 + + var list layout.List + list.Layout(gtx, listLen, + func(gtx layout.Context, i int) layout.Dimensions { + return layoutWidget(gtx, 20, 20) + }) + + fmt.Println(list.Position.Count) + + // Output: + // 5 +} + +func layoutWidget(ctx layout.Context, width, height int) layout.Dimensions { + return layout.Dimensions{ + Size: image.Point{ + X: width, + Y: height, + }, + } +} diff --git a/gio/giold/layout/flex.go b/gio/giold/layout/flex.go new file mode 100644 index 0000000..50d936d --- /dev/null +++ b/gio/giold/layout/flex.go @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "realy.lol/gio/op" +) + +// Flex lays out child elements along an axis, +// according to alignment and weights. +type Flex struct { + // Axis is the main axis, either Horizontal or Vertical. + Axis Axis + // Spacing controls the distribution of space left after + // layout. + Spacing Spacing + // Alignment is the alignment in the cross axis. + Alignment Alignment + // WeightSum is the sum of weights used for the weighted + // size of Flexed children. If WeightSum is zero, the sum + // of all Flexed weights is used. + WeightSum float32 +} + +// FlexChild is the descriptor for a Flex child. +type FlexChild struct { + flex bool + weight float32 + + widget Widget + + // Scratch space. + call op.CallOp + dims Dimensions +} + +// Spacing determine the spacing mode for a Flex. +type Spacing uint8 + +const ( + // SpaceEnd leaves space at the end. + SpaceEnd Spacing = iota + // SpaceStart leaves space at the start. + SpaceStart + // SpaceSides shares space between the start and end. + SpaceSides + // SpaceAround distributes space evenly between children, + // with half as much space at the start and end. + SpaceAround + // SpaceBetween distributes space evenly between children, + // leaving no space at the start and end. + SpaceBetween + // SpaceEvenly distributes space evenly between children and + // at the start and end. + SpaceEvenly +) + +// Rigid returns a Flex child with a maximal constraint of the +// remaining space. +func Rigid(widget Widget) FlexChild { + return FlexChild{ + widget: widget, + } +} + +// Flexed returns a Flex child forced to take up weight fraction of the +// space left over from Rigid children. The fraction is weight +// divided by either the weight sum of all Flexed children or the Flex +// WeightSum if non zero. +func Flexed(weight float32, widget Widget) FlexChild { + return FlexChild{ + flex: true, + weight: weight, + widget: widget, + } +} + +// Layout a list of children. The position of the children are +// determined by the specified order, but Rigid children are laid out +// before Flexed children. +func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions { + size := 0 + cs := gtx.Constraints + mainMin, mainMax := f.Axis.mainConstraint(cs) + crossMin, crossMax := f.Axis.crossConstraint(cs) + remaining := mainMax + var totalWeight float32 + cgtx := gtx + // Lay out Rigid children. + for i, child := range children { + if child.flex { + totalWeight += child.weight + continue + } + macro := op.Record(gtx.Ops) + cgtx.Constraints = f.Axis.constraints(0, remaining, crossMin, crossMax) + dims := child.widget(cgtx) + c := macro.Stop() + sz := f.Axis.Convert(dims.Size).X + size += sz + remaining -= sz + if remaining < 0 { + remaining = 0 + } + children[i].call = c + children[i].dims = dims + } + if w := f.WeightSum; w != 0 { + totalWeight = w + } + // fraction is the rounding error from a Flex weighting. + var fraction float32 + flexTotal := remaining + // Lay out Flexed children. + for i, child := range children { + if !child.flex { + continue + } + var flexSize int + if remaining > 0 && totalWeight > 0 { + // Apply weight and add any leftover fraction from a + // previous Flexed. + childSize := float32(flexTotal) * child.weight / totalWeight + flexSize = int(childSize + fraction + .5) + fraction = childSize - float32(flexSize) + if flexSize > remaining { + flexSize = remaining + } + } + macro := op.Record(gtx.Ops) + cgtx.Constraints = f.Axis.constraints(flexSize, flexSize, crossMin, + crossMax) + dims := child.widget(cgtx) + c := macro.Stop() + sz := f.Axis.Convert(dims.Size).X + size += sz + remaining -= sz + if remaining < 0 { + remaining = 0 + } + children[i].call = c + children[i].dims = dims + } + var maxCross int + var maxBaseline int + for _, child := range children { + if c := f.Axis.Convert(child.dims.Size).Y; c > maxCross { + maxCross = c + } + if b := child.dims.Size.Y - child.dims.Baseline; b > maxBaseline { + maxBaseline = b + } + } + var space int + if mainMin > size { + space = mainMin - size + } + var mainSize int + switch f.Spacing { + case SpaceSides: + mainSize += space / 2 + case SpaceStart: + mainSize += space + case SpaceEvenly: + mainSize += space / (1 + len(children)) + case SpaceAround: + if len(children) > 0 { + mainSize += space / (len(children) * 2) + } + } + for i, child := range children { + dims := child.dims + b := dims.Size.Y - dims.Baseline + var cross int + switch f.Alignment { + case End: + cross = maxCross - f.Axis.Convert(dims.Size).Y + case Middle: + cross = (maxCross - f.Axis.Convert(dims.Size).Y) / 2 + case Baseline: + if f.Axis == Horizontal { + cross = maxBaseline - b + } + } + stack := op.Save(gtx.Ops) + pt := f.Axis.Convert(image.Pt(mainSize, cross)) + op.Offset(FPt(pt)).Add(gtx.Ops) + child.call.Add(gtx.Ops) + stack.Load() + mainSize += f.Axis.Convert(dims.Size).X + if i < len(children)-1 { + switch f.Spacing { + case SpaceEvenly: + mainSize += space / (1 + len(children)) + case SpaceAround: + if len(children) > 0 { + mainSize += space / len(children) + } + case SpaceBetween: + if len(children) > 1 { + mainSize += space / (len(children) - 1) + } + } + } + } + switch f.Spacing { + case SpaceSides: + mainSize += space / 2 + case SpaceEnd: + mainSize += space + case SpaceEvenly: + mainSize += space / (1 + len(children)) + case SpaceAround: + if len(children) > 0 { + mainSize += space / (len(children) * 2) + } + } + sz := f.Axis.Convert(image.Pt(mainSize, maxCross)) + return Dimensions{Size: sz, Baseline: sz.Y - maxBaseline} +} + +func (s Spacing) String() string { + switch s { + case SpaceEnd: + return "SpaceEnd" + case SpaceStart: + return "SpaceStart" + case SpaceSides: + return "SpaceSides" + case SpaceAround: + return "SpaceAround" + case SpaceBetween: + return "SpaceAround" + case SpaceEvenly: + return "SpaceEvenly" + default: + panic("unreachable") + } +} diff --git a/gio/giold/layout/layout.go b/gio/giold/layout/layout.go new file mode 100644 index 0000000..6a4bdd2 --- /dev/null +++ b/gio/giold/layout/layout.go @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "realy.lol/gio/f32" + "realy.lol/gio/op" + "realy.lol/gio/unit" +) + +// Constraints represent the minimum and maximum size of a widget. +// +// A widget does not have to treat its constraints as "hard". For +// example, if it's passed a constraint with a minimum size that's +// smaller than its actual minimum size, it should return its minimum +// size dimensions instead. Parent widgets should deal appropriately +// with child widgets that return dimensions that do not fit their +// constraints (for example, by clipping). +type Constraints struct { + Min, Max image.Point +} + +// Dimensions are the resolved size and baseline for a widget. +// +// Baseline is the distance from the bottom of a widget to the baseline of +// any text it contains (or 0). The purpose is to be able to align text +// that span multiple widgets. +type Dimensions struct { + Size image.Point + Baseline int +} + +// Axis is the Horizontal or Vertical direction. +type Axis uint8 + +// Alignment is the mutual alignment of a list of widgets. +type Alignment uint8 + +// Direction is the alignment of widgets relative to a containing +// space. +type Direction uint8 + +// Widget is a function scope for drawing, processing events and +// computing dimensions for a user interface element. +type Widget func(gtx Context) Dimensions + +const ( + Start Alignment = iota + End + Middle + Baseline +) + +const ( + NW Direction = iota + N + NE + E + SE + S + SW + W + Center +) + +const ( + Horizontal Axis = iota + Vertical +) + +// Exact returns the Constraints with the minimum and maximum size +// set to size. +func Exact(size image.Point) Constraints { + return Constraints{ + Min: size, Max: size, + } +} + +// FPt converts an point to a f32.Point. +func FPt(p image.Point) f32.Point { + return f32.Point{ + X: float32(p.X), Y: float32(p.Y), + } +} + +// FRect converts a rectangle to a f32.Rectangle. +func FRect(r image.Rectangle) f32.Rectangle { + return f32.Rectangle{ + Min: FPt(r.Min), Max: FPt(r.Max), + } +} + +// Constrain a size so each dimension is in the range [min;max]. +func (c Constraints) Constrain(size image.Point) image.Point { + if min := c.Min.X; size.X < min { + size.X = min + } + if min := c.Min.Y; size.Y < min { + size.Y = min + } + if max := c.Max.X; size.X > max { + size.X = max + } + if max := c.Max.Y; size.Y > max { + size.Y = max + } + return size +} + +// Inset adds space around a widget by decreasing its maximum +// constraints. The minimum constraints will be adjusted to ensure +// they do not exceed the maximum. +type Inset struct { + Top, Right, Bottom, Left unit.Value +} + +// Layout a widget. +func (in Inset) Layout(gtx Context, w Widget) Dimensions { + top := gtx.Px(in.Top) + right := gtx.Px(in.Right) + bottom := gtx.Px(in.Bottom) + left := gtx.Px(in.Left) + mcs := gtx.Constraints + mcs.Max.X -= left + right + if mcs.Max.X < 0 { + left = 0 + right = 0 + mcs.Max.X = 0 + } + if mcs.Min.X > mcs.Max.X { + mcs.Min.X = mcs.Max.X + } + mcs.Max.Y -= top + bottom + if mcs.Max.Y < 0 { + bottom = 0 + top = 0 + mcs.Max.Y = 0 + } + if mcs.Min.Y > mcs.Max.Y { + mcs.Min.Y = mcs.Max.Y + } + stack := op.Save(gtx.Ops) + op.Offset(FPt(image.Point{X: left, Y: top})).Add(gtx.Ops) + gtx.Constraints = mcs + dims := w(gtx) + stack.Load() + return Dimensions{ + Size: dims.Size.Add(image.Point{X: right + left, Y: top + bottom}), + Baseline: dims.Baseline + bottom, + } +} + +// UniformInset returns an Inset with a single inset applied to all +// edges. +func UniformInset(v unit.Value) Inset { + return Inset{Top: v, Right: v, Bottom: v, Left: v} +} + +// Layout a widget according to the direction. +// The widget is called with the context constraints minimum cleared. +func (d Direction) Layout(gtx Context, w Widget) Dimensions { + macro := op.Record(gtx.Ops) + cs := gtx.Constraints + gtx.Constraints.Min = image.Point{} + dims := w(gtx) + call := macro.Stop() + sz := dims.Size + if sz.X < cs.Min.X { + sz.X = cs.Min.X + } + if sz.Y < cs.Min.Y { + sz.Y = cs.Min.Y + } + + defer op.Save(gtx.Ops).Load() + p := d.Position(dims.Size, sz) + op.Offset(FPt(p)).Add(gtx.Ops) + call.Add(gtx.Ops) + + return Dimensions{ + Size: sz, + Baseline: dims.Baseline + sz.Y - dims.Size.Y - p.Y, + } +} + +// Position calculates widget position according to the direction. +func (d Direction) Position(widget, bounds image.Point) image.Point { + var p image.Point + + switch d { + case N, S, Center: + p.X = (bounds.X - widget.X) / 2 + case NE, SE, E: + p.X = bounds.X - widget.X + } + + switch d { + case W, Center, E: + p.Y = (bounds.Y - widget.Y) / 2 + case SW, S, SE: + p.Y = bounds.Y - widget.Y + } + + return p +} + +// Spacer adds space between widgets. +type Spacer struct { + Width, Height unit.Value +} + +func (s Spacer) Layout(gtx Context) Dimensions { + return Dimensions{ + Size: image.Point{ + X: gtx.Px(s.Width), + Y: gtx.Px(s.Height), + }, + } +} + +func (a Alignment) String() string { + switch a { + case Start: + return "Start" + case End: + return "End" + case Middle: + return "Middle" + case Baseline: + return "Baseline" + default: + panic("unreachable") + } +} + +// Convert a point in (x, y) coordinates to (main, cross) coordinates, +// or vice versa. Specifically, Convert((x, y)) returns (x, y) unchanged +// for the horizontal axis, or (y, x) for the vertical axis. +func (a Axis) Convert(pt image.Point) image.Point { + if a == Horizontal { + return pt + } + return image.Pt(pt.Y, pt.X) +} + +// mainConstraint returns the min and max main constraints for axis a. +func (a Axis) mainConstraint(cs Constraints) (int, int) { + if a == Horizontal { + return cs.Min.X, cs.Max.X + } + return cs.Min.Y, cs.Max.Y +} + +// crossConstraint returns the min and max cross constraints for axis a. +func (a Axis) crossConstraint(cs Constraints) (int, int) { + if a == Horizontal { + return cs.Min.Y, cs.Max.Y + } + return cs.Min.X, cs.Max.X +} + +// constraints returns the constraints for axis a. +func (a Axis) constraints(mainMin, mainMax, crossMin, crossMax int) Constraints { + if a == Horizontal { + return Constraints{Min: image.Pt(mainMin, crossMin), + Max: image.Pt(mainMax, crossMax)} + } + return Constraints{Min: image.Pt(crossMin, mainMin), + Max: image.Pt(crossMax, mainMax)} +} + +func (a Axis) String() string { + switch a { + case Horizontal: + return "Horizontal" + case Vertical: + return "Vertical" + default: + panic("unreachable") + } +} + +func (d Direction) String() string { + switch d { + case NW: + return "NW" + case N: + return "N" + case NE: + return "NE" + case E: + return "E" + case SE: + return "SE" + case S: + return "S" + case SW: + return "SW" + case W: + return "W" + case Center: + return "Center" + default: + panic("unreachable") + } +} diff --git a/gio/giold/layout/layout_test.go b/gio/giold/layout/layout_test.go new file mode 100644 index 0000000..b04863c --- /dev/null +++ b/gio/giold/layout/layout_test.go @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + "testing" + + "realy.lol/gio/op" +) + +func TestStack(t *testing.T) { + gtx := Context{ + Ops: new(op.Ops), + Constraints: Constraints{ + Max: image.Pt(100, 100), + }, + } + exp := image.Point{X: 60, Y: 70} + dims := Stack{Alignment: Center}.Layout(gtx, + Expanded(func(gtx Context) Dimensions { + return Dimensions{Size: exp} + }), + Stacked(func(gtx Context) Dimensions { + return Dimensions{Size: image.Point{X: 50, Y: 50}} + }), + ) + if got := dims.Size; got != exp { + t.Errorf("Stack ignored Expanded size, got %v expected %v", got, exp) + } +} diff --git a/gio/giold/layout/list.go b/gio/giold/layout/list.go new file mode 100644 index 0000000..45c884e --- /dev/null +++ b/gio/giold/layout/list.go @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "realy.lol/gio/gesture" + "realy.lol/gio/io/pointer" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" +) + +type scrollChild struct { + size image.Point + call op.CallOp +} + +// List displays a subsection of a potentially infinitely +// large underlying list. List accepts user input to scroll +// the subsection. +type List struct { + Axis Axis + // ScrollToEnd instructs the list to stay scrolled to the far end position + // once reached. A List with ScrollToEnd == true and Position.BeforeEnd == + // false draws its content with the last item at the bottom of the list + // area. + ScrollToEnd bool + // Alignment is the cross axis alignment of list elements. + Alignment Alignment + + cs Constraints + scroll gesture.Scroll + scrollDelta int + + // Position is updated during Layout. To save the list scroll position, + // just save Position after Layout finishes. To scroll the list + // programmatically, update Position (e.g. restore it from a saved value) + // before calling Layout. + Position Position + + len int + + // maxSize is the total size of visible children. + maxSize int + children []scrollChild + dir iterationDir +} + +// ListElement is a function that computes the dimensions of +// a list element. +type ListElement func(gtx Context, index int) Dimensions + +type iterationDir uint8 + +// Position is a List scroll offset represented as an offset from the top edge +// of a child element. +type Position struct { + // BeforeEnd tracks whether the List position is before the very end. We + // use "before end" instead of "at end" so that the zero value of a + // Position struct is useful. + // + // When laying out a list, if ScrollToEnd is true and BeforeEnd is false, + // then First and Offset are ignored, and the list is drawn with the last + // item at the bottom. If ScrollToEnd is false then BeforeEnd is ignored. + BeforeEnd bool + // First is the index of the first visible child. + First int + // Offset is the distance in pixels from the top edge to the child at index + // First. + Offset int + // OffsetLast is the signed distance in pixels from the bottom edge to the + // bottom edge of the child at index First+Count. + OffsetLast int + // Count is the number of visible children. + Count int +} + +const ( + iterateNone iterationDir = iota + iterateForward + iterateBackward +) + +const inf = 1e6 + +// init prepares the list for iterating through its children with next. +func (l *List) init(gtx Context, len int) { + if l.more() { + panic("unfinished child") + } + l.cs = gtx.Constraints + l.maxSize = 0 + l.children = l.children[:0] + l.len = len + l.update(gtx) + if l.scrollToEnd() || l.Position.First > len { + l.Position.Offset = 0 + l.Position.First = len + } +} + +// Layout the List. +func (l *List) Layout(gtx Context, len int, w ListElement) Dimensions { + l.init(gtx, len) + crossMin, crossMax := l.Axis.crossConstraint(gtx.Constraints) + gtx.Constraints = l.Axis.constraints(0, inf, crossMin, crossMax) + macro := op.Record(gtx.Ops) + for l.next(); l.more(); l.next() { + child := op.Record(gtx.Ops) + dims := w(gtx, l.index()) + call := child.Stop() + l.end(dims, call) + } + return l.layout(gtx.Ops, macro) +} + +func (l *List) scrollToEnd() bool { + return l.ScrollToEnd && !l.Position.BeforeEnd +} + +// Dragging reports whether the List is being dragged. +func (l *List) Dragging() bool { + return l.scroll.State() == gesture.StateDragging +} + +func (l *List) update(gtx Context) { + d := l.scroll.Scroll(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis)) + l.scrollDelta = d + l.Position.Offset += d +} + +// next advances to the next child. +func (l *List) next() { + l.dir = l.nextDir() + // The user scroll offset is applied after scrolling to + // list end. + if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 { + l.Position.BeforeEnd = true + l.Position.Offset += l.scrollDelta + l.dir = l.nextDir() + } +} + +// index is current child's position in the underlying list. +func (l *List) index() int { + switch l.dir { + case iterateBackward: + return l.Position.First - 1 + case iterateForward: + return l.Position.First + len(l.children) + default: + panic("Index called before Next") + } +} + +// more reports whether more children are needed. +func (l *List) more() bool { + return l.dir != iterateNone +} + +func (l *List) nextDir() iterationDir { + _, vsize := l.Axis.mainConstraint(l.cs) + last := l.Position.First + len(l.children) + // Clamp offset. + if l.maxSize-l.Position.Offset < vsize && last == l.len { + l.Position.Offset = l.maxSize - vsize + } + if l.Position.Offset < 0 && l.Position.First == 0 { + l.Position.Offset = 0 + } + switch { + case len(l.children) == l.len: + return iterateNone + case l.maxSize-l.Position.Offset < vsize: + return iterateForward + case l.Position.Offset < 0: + return iterateBackward + } + return iterateNone +} + +// End the current child by specifying its dimensions. +func (l *List) end(dims Dimensions, call op.CallOp) { + child := scrollChild{dims.Size, call} + mainSize := l.Axis.Convert(child.size).X + l.maxSize += mainSize + switch l.dir { + case iterateForward: + l.children = append(l.children, child) + case iterateBackward: + l.children = append(l.children, scrollChild{}) + copy(l.children[1:], l.children) + l.children[0] = child + l.Position.First-- + l.Position.Offset += mainSize + default: + panic("call Next before End") + } + l.dir = iterateNone +} + +// Layout the List and return its dimensions. +func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { + if l.more() { + panic("unfinished child") + } + mainMin, mainMax := l.Axis.mainConstraint(l.cs) + children := l.children + // Skip invisible children + for len(children) > 0 { + sz := children[0].size + mainSize := l.Axis.Convert(sz).X + if l.Position.Offset < mainSize { + // First child is partially visible. + break + } + l.Position.First++ + l.Position.Offset -= mainSize + children = children[1:] + } + size := -l.Position.Offset + var maxCross int + for i, child := range children { + sz := l.Axis.Convert(child.size) + if c := sz.Y; c > maxCross { + maxCross = c + } + size += sz.X + if size >= mainMax { + children = children[:i+1] + break + } + } + l.Position.Count = len(children) + l.Position.OffsetLast = mainMax - size + pos := -l.Position.Offset + // ScrollToEnd lists are end aligned. + if space := l.Position.OffsetLast; l.ScrollToEnd && space > 0 { + pos += space + } + for _, child := range children { + sz := l.Axis.Convert(child.size) + var cross int + switch l.Alignment { + case End: + cross = maxCross - sz.Y + case Middle: + cross = (maxCross - sz.Y) / 2 + } + childSize := sz.X + max := childSize + pos + if max > mainMax { + max = mainMax + } + min := pos + if min < 0 { + min = 0 + } + r := image.Rectangle{ + Min: l.Axis.Convert(image.Pt(min, -inf)), + Max: l.Axis.Convert(image.Pt(max, inf)), + } + stack := op.Save(ops) + clip.Rect(r).Add(ops) + pt := l.Axis.Convert(image.Pt(pos, cross)) + op.Offset(FPt(pt)).Add(ops) + child.call.Add(ops) + stack.Load() + pos += childSize + } + atStart := l.Position.First == 0 && l.Position.Offset <= 0 + atEnd := l.Position.First+len(children) == l.len && mainMax >= pos + if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 { + l.scroll.Stop() + } + l.Position.BeforeEnd = !atEnd + if pos < mainMin { + pos = mainMin + } + if pos > mainMax { + pos = mainMax + } + dims := l.Axis.Convert(image.Pt(pos, maxCross)) + call := macro.Stop() + defer op.Save(ops).Load() + pointer.Rect(image.Rectangle{Max: dims}).Add(ops) + + var min, max int + if o := l.Position.Offset; o > 0 { + // Use the size of the invisible part as scroll boundary. + min = -o + } else if l.Position.First > 0 { + min = -inf + } + if o := l.Position.OffsetLast; o < 0 { + max = -o + } else if l.Position.First+l.Position.Count < l.len { + max = inf + } + scrollRange := image.Rectangle{ + Min: l.Axis.Convert(image.Pt(min, 0)), + Max: l.Axis.Convert(image.Pt(max, 0)), + } + l.scroll.Add(ops, scrollRange) + + call.Add(ops) + return Dimensions{Size: dims} +} diff --git a/gio/giold/layout/list_test.go b/gio/giold/layout/list_test.go new file mode 100644 index 0000000..6a026b3 --- /dev/null +++ b/gio/giold/layout/list_test.go @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + "testing" + + "realy.lol/gio/f32" + "realy.lol/gio/io/event" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/router" + "realy.lol/gio/op" +) + +func TestListPosition(t *testing.T) { + _s := func(e ...event.Event) []event.Event { return e } + r := new(router.Router) + gtx := Context{ + Ops: new(op.Ops), + Constraints: Constraints{ + Max: image.Pt(20, 10), + }, + Queue: r, + } + el := func(gtx Context, idx int) Dimensions { + return Dimensions{Size: image.Pt(10, 10)} + } + for _, tc := range []struct { + label string + num int + scroll []event.Event + first int + count int + offset int + last int + }{ + {label: "no item", last: 20}, + {label: "1 visible 0 hidden", num: 1, count: 1, last: 10}, + {label: "2 visible 0 hidden", num: 2, count: 2}, + {label: "2 visible 1 hidden", num: 3, count: 2}, + {label: "3 visible 0 hidden small scroll", num: 3, count: 3, offset: 5, + last: -5, + scroll: _s( + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Position: f32.Pt(0, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Type: pointer.Scroll, + Scroll: f32.Pt(5, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Release, + Position: f32.Pt(5, 0), + }, + )}, + {label: "3 visible 0 hidden small scroll 2", num: 3, count: 3, + offset: 3, last: -7, + scroll: _s( + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Position: f32.Pt(0, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Type: pointer.Scroll, + Scroll: f32.Pt(3, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Release, + Position: f32.Pt(5, 0), + }, + )}, + {label: "2 visible 1 hidden large scroll", num: 3, count: 2, first: 1, + scroll: _s( + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Position: f32.Pt(0, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Type: pointer.Scroll, + Scroll: f32.Pt(10, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Release, + Position: f32.Pt(15, 0), + }, + )}, + } { + t.Run(tc.label, func(t *testing.T) { + gtx.Ops.Reset() + + var list List + // Initialize the list. + list.Layout(gtx, tc.num, el) + // Generate the scroll events. + r.Frame(gtx.Ops) + r.Queue(tc.scroll...) + // Let the list process the events. + list.Layout(gtx, tc.num, el) + + pos := list.Position + if got, want := pos.First, tc.first; got != want { + t.Errorf("List: invalid first position: got %v; want %v", got, + want) + } + if got, want := pos.Count, tc.count; got != want { + t.Errorf("List: invalid number of visible children: got %v; want %v", + got, want) + } + if got, want := pos.Offset, tc.offset; got != want { + t.Errorf("List: invalid first visible offset: got %v; want %v", + got, want) + } + if got, want := pos.OffsetLast, tc.last; got != want { + t.Errorf("List: invalid last visible offset: got %v; want %v", + got, want) + } + }) + } +} diff --git a/gio/giold/layout/stack.go b/gio/giold/layout/stack.go new file mode 100644 index 0000000..f46a091 --- /dev/null +++ b/gio/giold/layout/stack.go @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "realy.lol/gio/op" +) + +// Stack lays out child elements on top of each other, +// according to an alignment direction. +type Stack struct { + // Alignment is the direction to align children + // smaller than the available space. + Alignment Direction +} + +// StackChild represents a child for a Stack layout. +type StackChild struct { + expanded bool + widget Widget + + // Scratch space. + call op.CallOp + dims Dimensions +} + +// Stacked returns a Stack child that is laid out with no minimum +// constraints and the maximum constraints passed to Stack.Layout. +func Stacked(w Widget) StackChild { + return StackChild{ + widget: w, + } +} + +// Expanded returns a Stack child with the minimum constraints set +// to the largest Stacked child. The maximum constraints are set to +// the same as passed to Stack.Layout. +func Expanded(w Widget) StackChild { + return StackChild{ + expanded: true, + widget: w, + } +} + +// Layout a stack of children. The position of the children are +// determined by the specified order, but Stacked children are laid out +// before Expanded children. +func (s Stack) Layout(gtx Context, children ...StackChild) Dimensions { + var maxSZ image.Point + // First lay out Stacked children. + cgtx := gtx + cgtx.Constraints.Min = image.Point{} + for i, w := range children { + if w.expanded { + continue + } + macro := op.Record(gtx.Ops) + dims := w.widget(cgtx) + call := macro.Stop() + if w := dims.Size.X; w > maxSZ.X { + maxSZ.X = w + } + if h := dims.Size.Y; h > maxSZ.Y { + maxSZ.Y = h + } + children[i].call = call + children[i].dims = dims + } + // Then lay out Expanded children. + for i, w := range children { + if !w.expanded { + continue + } + macro := op.Record(gtx.Ops) + cgtx.Constraints.Min = maxSZ + dims := w.widget(cgtx) + call := macro.Stop() + if w := dims.Size.X; w > maxSZ.X { + maxSZ.X = w + } + if h := dims.Size.Y; h > maxSZ.Y { + maxSZ.Y = h + } + children[i].call = call + children[i].dims = dims + } + + maxSZ = gtx.Constraints.Constrain(maxSZ) + var baseline int + for _, ch := range children { + sz := ch.dims.Size + var p image.Point + switch s.Alignment { + case N, S, Center: + p.X = (maxSZ.X - sz.X) / 2 + case NE, SE, E: + p.X = maxSZ.X - sz.X + } + switch s.Alignment { + case W, Center, E: + p.Y = (maxSZ.Y - sz.Y) / 2 + case SW, S, SE: + p.Y = maxSZ.Y - sz.Y + } + stack := op.Save(gtx.Ops) + op.Offset(FPt(p)).Add(gtx.Ops) + ch.call.Add(gtx.Ops) + stack.Load() + if baseline == 0 { + if b := ch.dims.Baseline; b != 0 { + baseline = b + maxSZ.Y - sz.Y - p.Y + } + } + } + return Dimensions{ + Size: maxSZ, + Baseline: baseline, + } +} diff --git a/gio/giold/op/clip/clip.go b/gio/giold/op/clip/clip.go new file mode 100644 index 0000000..360d89f --- /dev/null +++ b/gio/giold/op/clip/clip.go @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clip + +import ( + "encoding/binary" + "image" + "math" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/opconst" + "realy.lol/gio/internal/ops" + "realy.lol/gio/internal/scene" + "realy.lol/gio/internal/stroke" + "realy.lol/gio/op" +) + +// Op represents a clip area. Op intersects the current clip area with +// itself. +type Op struct { + bounds image.Rectangle + path PathSpec + + outline bool + stroke StrokeStyle + dashes DashSpec +} + +func (p Op) Add(o *op.Ops) { + str := p.stroke + dashes := p.dashes + path := p.path + outline := p.outline + approx := str.Width > 0 && !(dashes == DashSpec{} && str.Miter == 0 && str.Join == RoundJoin && str.Cap == RoundCap) + if approx { + // If the stroke is not natively supported by the compute renderer, construct a filled path + // that approximates it. + path = p.approximateStroke(o) + dashes = DashSpec{} + str = StrokeStyle{} + outline = true + } + + if path.hasSegments { + data := o.Write(opconst.TypePathLen) + data[0] = byte(opconst.TypePath) + path.spec.Add(o) + } + + if str.Width > 0 { + data := o.Write(opconst.TypeStrokeLen) + data[0] = byte(opconst.TypeStroke) + bo := binary.LittleEndian + bo.PutUint32(data[1:], math.Float32bits(str.Width)) + } + + data := o.Write(opconst.TypeClipLen) + data[0] = byte(opconst.TypeClip) + bo := binary.LittleEndian + bo.PutUint32(data[1:], uint32(p.bounds.Min.X)) + bo.PutUint32(data[5:], uint32(p.bounds.Min.Y)) + bo.PutUint32(data[9:], uint32(p.bounds.Max.X)) + bo.PutUint32(data[13:], uint32(p.bounds.Max.Y)) + if outline { + data[17] = byte(1) + } +} + +func (p Op) approximateStroke(o *op.Ops) PathSpec { + if !p.path.hasSegments { + return PathSpec{} + } + + var r ops.Reader + // Add path op for us to decode. Use a macro to omit it from later decodes. + ignore := op.Record(o) + r.ResetAt(o, ops.NewPC(o)) + p.path.spec.Add(o) + ignore.Stop() + encOp, ok := r.Decode() + if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux { + panic("corrupt path data") + } + pathData := encOp.Data[opconst.TypeAuxLen:] + + // Decode dashes in a similar way. + var dashes stroke.DashOp + if p.dashes.phase != 0 || p.dashes.size > 0 { + ignore := op.Record(o) + r.ResetAt(o, ops.NewPC(o)) + p.dashes.spec.Add(o) + ignore.Stop() + encOp, ok := r.Decode() + if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux { + panic("corrupt dash data") + } + dashes.Dashes = make([]float32, p.dashes.size) + dashData := encOp.Data[opconst.TypeAuxLen:] + bo := binary.LittleEndian + for i := range dashes.Dashes { + dashes.Dashes[i] = math.Float32frombits(bo.Uint32(dashData[i*4:])) + } + dashes.Phase = p.dashes.phase + } + + // Approximate and output path data. + var outline Path + outline.Begin(o) + ss := stroke.StrokeStyle{ + Width: p.stroke.Width, + Miter: p.stroke.Miter, + Cap: stroke.StrokeCap(p.stroke.Cap), + Join: stroke.StrokeJoin(p.stroke.Join), + } + quads := stroke.StrokePathCommands(ss, dashes, pathData) + pen := f32.Pt(0, 0) + for _, quad := range quads { + q := quad.Quad + if q.From != pen { + pen = q.From + outline.MoveTo(pen) + } + outline.contour = int(quad.Contour) + outline.QuadTo(q.Ctrl, q.To) + } + return outline.End() +} + +type PathSpec struct { + spec op.CallOp + // open is true if any path contour is not closed. A closed contour starts + // and ends in the same point. + open bool + // hasSegments tracks whether there are any segments in the path. + hasSegments bool +} + +// Path constructs a Op clip path described by lines and +// BĆ©zier curves, where drawing outside the Path is discarded. +// The inside-ness of a pixel is determines by the non-zero winding rule, +// similar to the SVG rule of the same name. +// +// Path generates no garbage and can be used for dynamic paths; path +// data is stored directly in the Ops list supplied to Begin. +type Path struct { + ops *op.Ops + open bool + contour int + pen f32.Point + macro op.MacroOp + start f32.Point + hasSegments bool +} + +// Pos returns the current pen position. +func (p *Path) Pos() f32.Point { return p.pen } + +// Begin the path, storing the path data and final Op into ops. +func (p *Path) Begin(ops *op.Ops) { + p.ops = ops + p.macro = op.Record(ops) + // Write the TypeAux opcode + data := ops.Write(opconst.TypeAuxLen) + data[0] = byte(opconst.TypeAux) +} + +// End returns a PathSpec ready to use in clipping operations. +func (p *Path) End() PathSpec { + c := p.macro.Stop() + return PathSpec{ + spec: c, + open: p.open || p.pen != p.start, + hasSegments: p.hasSegments, + } +} + +// Move moves the pen by the amount specified by delta. +func (p *Path) Move(delta f32.Point) { + to := delta.Add(p.pen) + p.MoveTo(to) +} + +// MoveTo moves the pen to the specified absolute coordinate. +func (p *Path) MoveTo(to f32.Point) { + p.open = p.open || p.pen != p.start + p.end() + p.pen = to + p.start = to +} + +// end completes the current contour. +func (p *Path) end() { + p.contour++ +} + +// Line moves the pen by the amount specified by delta, recording a line. +func (p *Path) Line(delta f32.Point) { + to := delta.Add(p.pen) + p.LineTo(to) +} + +// LineTo moves the pen to the absolute point specified, recording a line. +func (p *Path) LineTo(to f32.Point) { + data := p.ops.Write(scene.CommandSize + 4) + bo := binary.LittleEndian + bo.PutUint32(data[0:], uint32(p.contour)) + ops.EncodeCommand(data[4:], scene.Line(p.pen, to)) + p.pen = to + p.hasSegments = true +} + +// Quad records a quadratic BĆ©zier from the pen to end +// with the control point ctrl. +func (p *Path) Quad(ctrl, to f32.Point) { + ctrl = ctrl.Add(p.pen) + to = to.Add(p.pen) + p.QuadTo(ctrl, to) +} + +// QuadTo records a quadratic BĆ©zier from the pen to end +// with the control point ctrl, with absolute coordinates. +func (p *Path) QuadTo(ctrl, to f32.Point) { + data := p.ops.Write(scene.CommandSize + 4) + bo := binary.LittleEndian + bo.PutUint32(data[0:], uint32(p.contour)) + ops.EncodeCommand(data[4:], scene.Quad(p.pen, ctrl, to)) + p.pen = to + p.hasSegments = true +} + +// Arc adds an elliptical arc to the path. The implied ellipse is defined +// by its focus points f1 and f2. +// The arc starts in the current point and ends angle radians along the ellipse boundary. +// The sign of angle determines the direction; positive being counter-clockwise, +// negative clockwise. +func (p *Path) Arc(f1, f2 f32.Point, angle float32) { + f1 = f1.Add(p.pen) + f2 = f2.Add(p.pen) + const segments = 16 + m := stroke.ArcTransform(p.pen, f1, f2, angle, segments) + + for i := 0; i < segments; i++ { + p0 := p.pen + p1 := m.Transform(p0) + p2 := m.Transform(p1) + ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5)) + p.QuadTo(ctl, p2) + } +} + +// Cube records a cubic BĆ©zier from the pen through +// two control points ending in to. +func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) { + p.CubeTo(p.pen.Add(ctrl0), p.pen.Add(ctrl1), p.pen.Add(to)) +} + +// CubeTo records a cubic BĆ©zier from the pen through +// two control points ending in to, with absolute coordinates. +func (p *Path) CubeTo(ctrl0, ctrl1, to f32.Point) { + if ctrl0 == p.pen && ctrl1 == p.pen && to == p.pen { + return + } + data := p.ops.Write(scene.CommandSize + 4) + bo := binary.LittleEndian + bo.PutUint32(data[0:], uint32(p.contour)) + ops.EncodeCommand(data[4:], scene.Cubic(p.pen, ctrl0, ctrl1, to)) + p.pen = to + p.hasSegments = true +} + +// Close closes the path contour. +func (p *Path) Close() { + if p.pen != p.start { + p.LineTo(p.start) + } + p.end() +} + +// Outline represents the area inside of a path, according to the +// non-zero winding rule. +type Outline struct { + Path PathSpec +} + +// Op returns a clip operation representing the outline. +func (o Outline) Op() Op { + if o.Path.open { + panic("not all path contours are closed") + } + return Op{ + path: o.Path, + outline: true, + } +} diff --git a/gio/giold/op/clip/clip_test.go b/gio/giold/op/clip/clip_test.go new file mode 100644 index 0000000..7962c6d --- /dev/null +++ b/gio/giold/op/clip/clip_test.go @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clip + +import ( + "testing" + + "realy.lol/gio/f32" + "realy.lol/gio/op" +) + +func TestOpenPathOutlinePanic(t *testing.T) { + defer func() { + if err := recover(); err == nil { + t.Error("Outline of an open path didn't panic") + } + }() + var p Path + p.Begin(new(op.Ops)) + p.Line(f32.Pt(10, 10)) + Outline{Path: p.End()}.Op() +} diff --git a/gio/giold/op/clip/doc.go b/gio/giold/op/clip/doc.go new file mode 100644 index 0000000..6ba5546 --- /dev/null +++ b/gio/giold/op/clip/doc.go @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package clip provides operations for clipping paint operations. +Drawing outside the current clip area is ignored. + +The current clip is initially the infinite set. An Op sets the clip +to the intersection of the current clip and the clip area it +represents. If you need to reset the current clip to its value +before applying an Op, use op.StackOp. + +General clipping areas are constructed with Path. Simpler special +cases such as rectangular clip areas also exist as convenient +constructors. +*/ +package clip diff --git a/gio/giold/op/clip/shapes.go b/gio/giold/op/clip/shapes.go new file mode 100644 index 0000000..9ea84e3 --- /dev/null +++ b/gio/giold/op/clip/shapes.go @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clip + +import ( + "image" + "math" + + "realy.lol/gio/f32" + "realy.lol/gio/op" +) + +// Rect represents the clip area of a pixel-aligned rectangle. +type Rect image.Rectangle + +// Op returns the op for the rectangle. +func (r Rect) Op() Op { + return Op{ + bounds: image.Rectangle(r), + outline: true, + } +} + +// Add the clip operation. +func (r Rect) Add(ops *op.Ops) { + r.Op().Add(ops) +} + +// UniformRRect returns an RRect with all corner radii set to the +// provided radius. +func UniformRRect(rect f32.Rectangle, radius float32) RRect { + return RRect{ + Rect: rect, + SE: radius, + SW: radius, + NE: radius, + NW: radius, + } +} + +// RRect represents the clip area of a rectangle with rounded +// corners. +// +// Specify a square with corner radii equal to half the square size to +// construct a circular clip area. +type RRect struct { + Rect f32.Rectangle + // The corner radii. + SE, SW, NW, NE float32 +} + +// Op returns the op for the rounded rectangle. +func (rr RRect) Op(ops *op.Ops) Op { + if rr.SE == 0 && rr.SW == 0 && rr.NW == 0 && rr.NE == 0 { + r := image.Rectangle{ + Min: image.Point{X: int(rr.Rect.Min.X), Y: int(rr.Rect.Min.Y)}, + Max: image.Point{X: int(rr.Rect.Max.X), Y: int(rr.Rect.Max.Y)}, + } + // Only use Rect if rr is pixel-aligned, as Rect is guaranteed to be. + if fPt(r.Min) == rr.Rect.Min && fPt(r.Max) == rr.Rect.Max { + return Rect(r).Op() + } + } + return Outline{Path: rr.Path(ops)}.Op() +} + +// Add the rectangle clip. +func (rr RRect) Add(ops *op.Ops) { + rr.Op(ops).Add(ops) +} + +// Path returns the PathSpec for the rounded rectangle. +func (rr RRect) Path(ops *op.Ops) PathSpec { + var p Path + p.Begin(ops) + + // https://pomax.github.io/bezierinfo/#circles_cubic. + const q = 4 * (math.Sqrt2 - 1) / 3 + const iq = 1 - q + + se, sw, nw, ne := rr.SE, rr.SW, rr.NW, rr.NE + w, n, e, s := rr.Rect.Min.X, rr.Rect.Min.Y, rr.Rect.Max.X, rr.Rect.Max.Y + + p.MoveTo(f32.Point{X: w + nw, Y: n}) + p.LineTo(f32.Point{X: e - ne, Y: n}) // N + p.CubeTo( // NE + f32.Point{X: e - ne*iq, Y: n}, + f32.Point{X: e, Y: n + ne*iq}, + f32.Point{X: e, Y: n + ne}) + p.LineTo(f32.Point{X: e, Y: s - se}) // E + p.CubeTo( // SE + f32.Point{X: e, Y: s - se*iq}, + f32.Point{X: e - se*iq, Y: s}, + f32.Point{X: e - se, Y: s}) + p.LineTo(f32.Point{X: w + sw, Y: s}) // S + p.CubeTo( // SW + f32.Point{X: w + sw*iq, Y: s}, + f32.Point{X: w, Y: s - sw*iq}, + f32.Point{X: w, Y: s - sw}) + p.LineTo(f32.Point{X: w, Y: n + nw}) // W + p.CubeTo( // NW + f32.Point{X: w, Y: n + nw*iq}, + f32.Point{X: w + nw*iq, Y: n}, + f32.Point{X: w + nw, Y: n}) + + return p.End() +} + +// Circle represents the clip area of a circle. +type Circle struct { + Center f32.Point + Radius float32 +} + +// Op returns the op for the circle. +func (c Circle) Op(ops *op.Ops) Op { + return Outline{Path: c.Path(ops)}.Op() +} + +// Add the circle clip. +func (c Circle) Add(ops *op.Ops) { + c.Op(ops).Add(ops) +} + +// Path returns the PathSpec for the circle. +func (c Circle) Path(ops *op.Ops) PathSpec { + var p Path + p.Begin(ops) + + center := c.Center + r := c.Radius + + // https://pomax.github.io/bezierinfo/#circles_cubic. + const q = 4 * (math.Sqrt2 - 1) / 3 + + curve := r * q + top := f32.Point{X: center.X, Y: center.Y - r} + + p.MoveTo(top) + p.CubeTo( + f32.Point{X: center.X + curve, Y: center.Y - r}, + f32.Point{X: center.X + r, Y: center.Y - curve}, + f32.Point{X: center.X + r, Y: center.Y}, + ) + p.CubeTo( + f32.Point{X: center.X + r, Y: center.Y + curve}, + f32.Point{X: center.X + curve, Y: center.Y + r}, + f32.Point{X: center.X, Y: center.Y + r}, + ) + p.CubeTo( + f32.Point{X: center.X - curve, Y: center.Y + r}, + f32.Point{X: center.X - r, Y: center.Y + curve}, + f32.Point{X: center.X - r, Y: center.Y}, + ) + p.CubeTo( + f32.Point{X: center.X - r, Y: center.Y - curve}, + f32.Point{X: center.X - curve, Y: center.Y - r}, + top, + ) + return p.End() +} + +func fPt(p image.Point) f32.Point { + return f32.Point{ + X: float32(p.X), Y: float32(p.Y), + } +} diff --git a/gio/giold/op/clip/stroke.go b/gio/giold/op/clip/stroke.go new file mode 100644 index 0000000..8610eab --- /dev/null +++ b/gio/giold/op/clip/stroke.go @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clip + +import ( + "encoding/binary" + "math" + + "realy.lol/gio/internal/opconst" + "realy.lol/gio/op" +) + +// Stroke represents a stroked path. +type Stroke struct { + Path PathSpec + Style StrokeStyle + + // Dashes specify the dashes of the stroke. + // The empty value denotes no dashes. + Dashes DashSpec +} + +// Op returns a clip operation representing the stroke. +func (s Stroke) Op() Op { + return Op{ + path: s.Path, + stroke: s.Style, + dashes: s.Dashes, + } +} + +// StrokeStyle describes how a path should be stroked. +type StrokeStyle struct { + Width float32 // Width of the stroked path. + + // Miter is the limit to apply to a miter joint. + // The zero Miter disables the miter joint; setting Miter to +āˆž + // unconditionally enables the miter joint. + Miter float32 + Cap StrokeCap // Cap describes the head or tail of a stroked path. + Join StrokeJoin // Join describes how stroked paths are collated. +} + +// StrokeCap describes the head or tail of a stroked path. +type StrokeCap uint8 + +const ( + // RoundCap caps stroked paths with a round cap, joining the right-hand and + // left-hand sides of a stroked path with a half disc of diameter the + // stroked path's width. + RoundCap StrokeCap = iota + + // FlatCap caps stroked paths with a flat cap, joining the right-hand + // and left-hand sides of a stroked path with a straight line. + FlatCap + + // SquareCap caps stroked paths with a square cap, joining the right-hand + // and left-hand sides of a stroked path with a half square of length + // the stroked path's width. + SquareCap +) + +// StrokeJoin describes how stroked paths are collated. +type StrokeJoin uint8 + +const ( + // RoundJoin joins path segments with a round segment. + RoundJoin StrokeJoin = iota + + // BevelJoin joins path segments with sharp bevels. + BevelJoin +) + +// Dash records dashes' lengths and phase for a stroked path. +type Dash struct { + ops *op.Ops + macro op.MacroOp + phase float32 + size uint8 // size of the pattern +} + +func (d *Dash) Begin(ops *op.Ops) { + d.ops = ops + d.macro = op.Record(ops) + // Write the TypeAux opcode + data := ops.Write(opconst.TypeAuxLen) + data[0] = byte(opconst.TypeAux) +} + +func (d *Dash) Phase(v float32) { + d.phase = v +} + +func (d *Dash) Dash(length float32) { + if d.size == math.MaxUint8 { + panic("clip: dash pattern too large") + } + data := d.ops.Write(4) + bo := binary.LittleEndian + bo.PutUint32(data[0:], math.Float32bits(length)) + d.size++ +} + +func (d *Dash) End() DashSpec { + c := d.macro.Stop() + return DashSpec{ + spec: c, + phase: d.phase, + size: d.size, + } +} + +// DashSpec describes a dashed pattern. +type DashSpec struct { + spec op.CallOp + phase float32 + size uint8 // size of the pattern +} diff --git a/gio/giold/op/op.go b/gio/giold/op/op.go new file mode 100644 index 0000000..f29aa0b --- /dev/null +++ b/gio/giold/op/op.go @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* + +Package op implements operations for updating a user interface. + +Gio programs use operations, or ops, for describing their user +interfaces. There are operations for drawing, defining input +handlers, changing window properties as well as operations for +controlling the execution of other operations. + +Ops represents a list of operations. The most important use +for an Ops list is to describe a complete user interface update +to a ui/app.Window's Update method. + +Drawing a colored square: + + import "realy.lol/gio/unit" + import "realy.lol/gio/app" + import "realy.lol/gio/op/paint" + + var w app.Window + var e system.FrameEvent + ops := new(op.Ops) + ... + ops.Reset() + paint.ColorOp{Color: ...}.Add(ops) + paint.PaintOp{Rect: ...}.Add(ops) + e.Frame(ops) + +State + +An Ops list can be viewed as a very simple virtual machine: it has an implicit +mutable state stack and execution flow can be controlled with macros. + +The Save function saves the current state for later restoring: + + ops := new(op.Ops) + // Save the current state, in particular the transform. + state := op.Save(ops) + // Apply a transform to subsequent operations. + op.Offset(...).Add(ops) + ... + // Restore the previous transform. + state.Load() + +You can also use this one-line to save the current state and restore it at the +end of a function : + + defer op.Save(ops).Load() + +The MacroOp records a list of operations to be executed later: + + ops := new(op.Ops) + macro := op.Record(ops) + // Record operations by adding them. + op.InvalidateOp{}.Add(ops) + ... + // End recording. + call := macro.Stop() + + // replay the recorded operations: + call.Add(ops) + +*/ +package op + +import ( + "encoding/binary" + "math" + "time" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/opconst" +) + +// Ops holds a list of operations. Operations are stored in +// serialized form to avoid garbage during construction of +// the ops list. +type Ops struct { + // version is incremented at each Reset. + version int + // data contains the serialized operations. + data []byte + // refs hold external references for operations. + refs []interface{} + // nextStateID is the id allocated for the next + // StateOp. + nextStateID int + + macroStack stack +} + +// StateOp represents a saved operation snapshop to be restored +// later. +type StateOp struct { + id int + macroID int + ops *Ops +} + +// MacroOp records a list of operations for later use. +type MacroOp struct { + ops *Ops + id stackID + pc pc +} + +// CallOp invokes the operations recorded by Record. +type CallOp struct { + // Ops is the list of operations to invoke. + ops *Ops + pc pc +} + +// InvalidateOp requests a redraw at the given time. Use +// the zero value to request an immediate redraw. +type InvalidateOp struct { + At time.Time +} + +// TransformOp applies a transform to the current transform. The zero value +// for TransformOp represents the identity transform. +type TransformOp struct { + t f32.Affine2D +} + +// stack tracks the integer identities of MacroOp +// operations to ensure correct pairing of Record/End. +type stack struct { + currentID int + nextID int +} + +type stackID struct { + id int + prev int +} + +type pc struct { + data int + refs int +} + +// Defer executes c after all other operations have completed, +// including previously deferred operations. +// Defer saves the current transformation and restores it prior +// to execution. All other operation state is reset. +// +// Note that deferred operations are executed in first-in-first-out +// order, unlike the Go facility of the same name. +func Defer(o *Ops, c CallOp) { + if c.ops == nil { + return + } + state := Save(o) + // Wrap c in a macro that loads the saved state before execution. + m := Record(o) + load(o, opconst.InitialStateID, opconst.AllState) + load(o, state.id, opconst.TransformState) + c.Add(o) + c = m.Stop() + // A Defer is recorded as a TypeDefer followed by the + // wrapped macro. + data := o.Write(opconst.TypeDeferLen) + data[0] = byte(opconst.TypeDefer) + c.Add(o) +} + +// Save the current operations state. +func Save(o *Ops) StateOp { + o.nextStateID++ + s := StateOp{ + ops: o, + id: o.nextStateID, + macroID: o.macroStack.currentID, + } + save(o, s.id) + return s +} + +// save records a save of the operations state to +// id. +func save(o *Ops, id int) { + bo := binary.LittleEndian + data := o.Write(opconst.TypeSaveLen) + data[0] = byte(opconst.TypeSave) + bo.PutUint32(data[1:], uint32(id)) +} + +// Load a previously saved operations state. +func (s StateOp) Load() { + if s.ops.macroStack.currentID != s.macroID { + panic("load in a different macro than save") + } + if s.id == 0 { + panic("zero-value op") + } + load(s.ops, s.id, opconst.AllState) +} + +// load a previously saved operations state given +// its ID. Only state included in mask is affected. +func load(o *Ops, id int, m opconst.StateMask) { + bo := binary.LittleEndian + data := o.Write(opconst.TypeLoadLen) + data[0] = byte(opconst.TypeLoad) + data[1] = byte(m) + bo.PutUint32(data[2:], uint32(id)) +} + +// Reset the Ops, preparing it for re-use. Reset invalidates +// any recorded macros. +func (o *Ops) Reset() { + o.macroStack = stack{} + // Leave references to the GC. + for i := range o.refs { + o.refs[i] = nil + } + o.data = o.data[:0] + o.refs = o.refs[:0] + o.nextStateID = 0 + o.version++ +} + +// Data is for internal use only. +func (o *Ops) Data() []byte { + return o.data +} + +// Refs is for internal use only. +func (o *Ops) Refs() []interface{} { + return o.refs +} + +// Version is for internal use only. +func (o *Ops) Version() int { + return o.version +} + +// Write is for internal use only. +func (o *Ops) Write(n int) []byte { + o.data = append(o.data, make([]byte, n)...) + return o.data[len(o.data)-n:] +} + +// Write1 is for internal use only. +func (o *Ops) Write1(n int, ref1 interface{}) []byte { + o.data = append(o.data, make([]byte, n)...) + o.refs = append(o.refs, ref1) + return o.data[len(o.data)-n:] +} + +// Write2 is for internal use only. +func (o *Ops) Write2(n int, ref1, ref2 interface{}) []byte { + o.data = append(o.data, make([]byte, n)...) + o.refs = append(o.refs, ref1, ref2) + return o.data[len(o.data)-n:] +} + +func (o *Ops) pc() pc { + return pc{data: len(o.data), refs: len(o.refs)} +} + +// Record a macro of operations. +func Record(o *Ops) MacroOp { + m := MacroOp{ + ops: o, + id: o.macroStack.push(), + pc: o.pc(), + } + // Reserve room for a macro definition. Updated in Stop. + m.ops.Write(opconst.TypeMacroLen) + m.fill() + return m +} + +// Stop ends a previously started recording and returns an +// operation for replaying it. +func (m MacroOp) Stop() CallOp { + m.ops.macroStack.pop(m.id) + m.fill() + return CallOp{ + ops: m.ops, + pc: m.pc, + } +} + +func (m MacroOp) fill() { + pc := m.ops.pc() + // Fill out the macro definition reserved in Record. + data := m.ops.data[m.pc.data:] + data = data[:opconst.TypeMacroLen] + data[0] = byte(opconst.TypeMacro) + bo := binary.LittleEndian + bo.PutUint32(data[1:], uint32(pc.data)) + bo.PutUint32(data[5:], uint32(pc.refs)) +} + +// Add the recorded list of operations. Add +// panics if the Ops containing the recording +// has been reset. +func (c CallOp) Add(o *Ops) { + if c.ops == nil { + return + } + data := o.Write1(opconst.TypeCallLen, c.ops) + data[0] = byte(opconst.TypeCall) + bo := binary.LittleEndian + bo.PutUint32(data[1:], uint32(c.pc.data)) + bo.PutUint32(data[5:], uint32(c.pc.refs)) +} + +func (r InvalidateOp) Add(o *Ops) { + data := o.Write(opconst.TypeRedrawLen) + data[0] = byte(opconst.TypeInvalidate) + bo := binary.LittleEndian + // UnixNano cannot represent the zero time. + if t := r.At; !t.IsZero() { + nanos := t.UnixNano() + if nanos > 0 { + bo.PutUint64(data[1:], uint64(nanos)) + } + } +} + +// Offset creates a TransformOp with the offset o. +func Offset(o f32.Point) TransformOp { + return TransformOp{t: f32.Affine2D{}.Offset(o)} +} + +// Affine creates a TransformOp representing the transformation a. +func Affine(a f32.Affine2D) TransformOp { + return TransformOp{t: a} +} + +func (t TransformOp) Add(o *Ops) { + data := o.Write(opconst.TypeTransformLen) + data[0] = byte(opconst.TypeTransform) + bo := binary.LittleEndian + a, b, c, d, e, f := t.t.Elems() + bo.PutUint32(data[1:], math.Float32bits(a)) + bo.PutUint32(data[1+4*1:], math.Float32bits(b)) + bo.PutUint32(data[1+4*2:], math.Float32bits(c)) + bo.PutUint32(data[1+4*3:], math.Float32bits(d)) + bo.PutUint32(data[1+4*4:], math.Float32bits(e)) + bo.PutUint32(data[1+4*5:], math.Float32bits(f)) +} + +func (s *stack) push() stackID { + s.nextID++ + sid := stackID{ + id: s.nextID, + prev: s.currentID, + } + s.currentID = s.nextID + return sid +} + +func (s *stack) check(sid stackID) { + if s.currentID != sid.id { + panic("unbalanced operation") + } +} + +func (s *stack) pop(sid stackID) { + s.check(sid) + s.currentID = sid.prev +} diff --git a/gio/giold/op/paint/doc.go b/gio/giold/op/paint/doc.go new file mode 100644 index 0000000..79054ab --- /dev/null +++ b/gio/giold/op/paint/doc.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package paint provides drawing operations for 2D graphics. + +The PaintOp operation fills the current clip with the current brush, +taking the current transformation into account. + +The current brush is set by either a ColorOp for a constant color, or +ImageOp for an image, or LinearGradientOp for gradients. + +All color.NRGBA values are in the sRGB color space. +*/ +package paint diff --git a/gio/giold/op/paint/paint.go b/gio/giold/op/paint/paint.go new file mode 100644 index 0000000..e53a763 --- /dev/null +++ b/gio/giold/op/paint/paint.go @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package paint + +import ( + "encoding/binary" + "image" + "image/color" + "image/draw" + "math" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/opconst" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" +) + +// ImageOp sets the brush to an image. +// +// Note: the ImageOp may keep a reference to the backing image. +// See NewImageOp for details. +type ImageOp struct { + uniform bool + color color.NRGBA + src *image.RGBA + + // handle is a key to uniquely identify this ImageOp + // in a map of cached textures. + handle interface{} +} + +// ColorOp sets the brush to a constant color. +type ColorOp struct { + Color color.NRGBA +} + +// LinearGradientOp sets the brush to a gradient starting at stop1 with color1 and +// ending at stop2 with color2. +type LinearGradientOp struct { + Stop1 f32.Point + Color1 color.NRGBA + Stop2 f32.Point + Color2 color.NRGBA +} + +// PaintOp fills fills the current clip area with the current brush. +type PaintOp struct { +} + +// NewImageOp creates an ImageOp backed by src. See +// realy.lol/gio/io/system.FrameEvent for a description of when data +// referenced by operations is safe to re-use. +// +// NewImageOp assumes the backing image is immutable, and may cache a +// copy of its contents in a GPU-friendly way. Create new ImageOps to +// ensure that changes to an image is reflected in the display of +// it. +func NewImageOp(src image.Image) ImageOp { + switch src := src.(type) { + case *image.Uniform: + col := color.NRGBAModel.Convert(src.C).(color.NRGBA) + return ImageOp{ + uniform: true, + color: col, + } + case *image.RGBA: + bounds := src.Bounds() + if bounds.Min == (image.Point{}) && src.Stride == bounds.Dx()*4 { + return ImageOp{ + src: src, + handle: new(int), + } + } + } + + sz := src.Bounds().Size() + // Copy the image into a GPU friendly format. + dst := image.NewRGBA(image.Rectangle{ + Max: sz, + }) + draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src) + return ImageOp{ + src: dst, + handle: new(int), + } +} + +func (i ImageOp) Size() image.Point { + if i.src == nil { + return image.Point{} + } + return i.src.Bounds().Size() +} + +func (i ImageOp) Add(o *op.Ops) { + if i.uniform { + ColorOp{ + Color: i.color, + }.Add(o) + return + } + data := o.Write2(opconst.TypeImageLen, i.src, i.handle) + data[0] = byte(opconst.TypeImage) +} + +func (c ColorOp) Add(o *op.Ops) { + data := o.Write(opconst.TypeColorLen) + data[0] = byte(opconst.TypeColor) + data[1] = c.Color.R + data[2] = c.Color.G + data[3] = c.Color.B + data[4] = c.Color.A +} + +func (c LinearGradientOp) Add(o *op.Ops) { + data := o.Write(opconst.TypeLinearGradientLen) + data[0] = byte(opconst.TypeLinearGradient) + + bo := binary.LittleEndian + bo.PutUint32(data[1:], math.Float32bits(c.Stop1.X)) + bo.PutUint32(data[5:], math.Float32bits(c.Stop1.Y)) + bo.PutUint32(data[9:], math.Float32bits(c.Stop2.X)) + bo.PutUint32(data[13:], math.Float32bits(c.Stop2.Y)) + + data[17+0] = c.Color1.R + data[17+1] = c.Color1.G + data[17+2] = c.Color1.B + data[17+3] = c.Color1.A + data[21+0] = c.Color2.R + data[21+1] = c.Color2.G + data[21+2] = c.Color2.B + data[21+3] = c.Color2.A +} + +func (d PaintOp) Add(o *op.Ops) { + data := o.Write(opconst.TypePaintLen) + data[0] = byte(opconst.TypePaint) +} + +// FillShape fills the clip shape with a color. +func FillShape(ops *op.Ops, c color.NRGBA, shape clip.Op) { + defer op.Save(ops).Load() + shape.Add(ops) + Fill(ops, c) +} + +// Fill paints an infinitely large plane with the provided color. It +// is intended to be used with a clip.Op already in place to limit +// the painted area. Use FillShape unless you need to paint several +// times within the same clip.Op. +func Fill(ops *op.Ops, c color.NRGBA) { + defer op.Save(ops).Load() + ColorOp{Color: c}.Add(ops) + PaintOp{}.Add(ops) +} diff --git a/gio/giold/text/lru.go b/gio/giold/text/lru.go new file mode 100644 index 0000000..4f1c033 --- /dev/null +++ b/gio/giold/text/lru.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "golang.org/x/image/math/fixed" + + "realy.lol/gio/op" +) + +type layoutCache struct { + m map[layoutKey]*layoutElem + head, tail *layoutElem +} + +type pathCache struct { + m map[pathKey]*path + head, tail *path +} + +type layoutElem struct { + next, prev *layoutElem + key layoutKey + layout []Line +} + +type path struct { + next, prev *path + key pathKey + val op.CallOp +} + +type layoutKey struct { + ppem fixed.Int26_6 + maxWidth int + str string +} + +type pathKey struct { + ppem fixed.Int26_6 + str string +} + +const maxSize = 1000 + +func (l *layoutCache) Get(k layoutKey) ([]Line, bool) { + if lt, ok := l.m[k]; ok { + l.remove(lt) + l.insert(lt) + return lt.layout, true + } + return nil, false +} + +func (l *layoutCache) Put(k layoutKey, lt []Line) { + if l.m == nil { + l.m = make(map[layoutKey]*layoutElem) + l.head = new(layoutElem) + l.tail = new(layoutElem) + l.head.prev = l.tail + l.tail.next = l.head + } + val := &layoutElem{key: k, layout: lt} + l.m[k] = val + l.insert(val) + if len(l.m) > maxSize { + oldest := l.tail.next + l.remove(oldest) + delete(l.m, oldest.key) + } +} + +func (l *layoutCache) remove(lt *layoutElem) { + lt.next.prev = lt.prev + lt.prev.next = lt.next +} + +func (l *layoutCache) insert(lt *layoutElem) { + lt.next = l.head + lt.prev = l.head.prev + lt.prev.next = lt + lt.next.prev = lt +} + +func (c *pathCache) Get(k pathKey) (op.CallOp, bool) { + if v, ok := c.m[k]; ok { + c.remove(v) + c.insert(v) + return v.val, true + } + return op.CallOp{}, false +} + +func (c *pathCache) Put(k pathKey, v op.CallOp) { + if c.m == nil { + c.m = make(map[pathKey]*path) + c.head = new(path) + c.tail = new(path) + c.head.prev = c.tail + c.tail.next = c.head + } + val := &path{key: k, val: v} + c.m[k] = val + c.insert(val) + if len(c.m) > maxSize { + oldest := c.tail.next + c.remove(oldest) + delete(c.m, oldest.key) + } +} + +func (c *pathCache) remove(v *path) { + v.next.prev = v.prev + v.prev.next = v.next +} + +func (c *pathCache) insert(v *path) { + v.next = c.head + v.prev = c.head.prev + v.prev.next = v + v.next.prev = v +} diff --git a/gio/giold/text/lru_test.go b/gio/giold/text/lru_test.go new file mode 100644 index 0000000..fb8d8d1 --- /dev/null +++ b/gio/giold/text/lru_test.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "strconv" + "testing" + + "realy.lol/gio/op" +) + +func TestLayoutLRU(t *testing.T) { + c := new(layoutCache) + put := func(i int) { + c.Put(layoutKey{str: strconv.Itoa(i)}, nil) + } + get := func(i int) bool { + _, ok := c.Get(layoutKey{str: strconv.Itoa(i)}) + return ok + } + testLRU(t, put, get) +} + +func TestPathLRU(t *testing.T) { + c := new(pathCache) + put := func(i int) { + c.Put(pathKey{str: strconv.Itoa(i)}, op.CallOp{}) + } + get := func(i int) bool { + _, ok := c.Get(pathKey{str: strconv.Itoa(i)}) + return ok + } + testLRU(t, put, get) +} + +func testLRU(t *testing.T, put func(i int), get func(i int) bool) { + for i := 0; i < maxSize; i++ { + put(i) + } + for i := 0; i < maxSize; i++ { + if !get(i) { + t.Fatalf("key %d was evicted", i) + } + } + put(maxSize) + for i := 1; i < maxSize+1; i++ { + if !get(i) { + t.Fatalf("key %d was evicted", i) + } + } + if i := 0; get(i) { + t.Fatalf("key %d was not evicted", i) + } +} diff --git a/gio/giold/text/shaper.go b/gio/giold/text/shaper.go new file mode 100644 index 0000000..88f1fbf --- /dev/null +++ b/gio/giold/text/shaper.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "io" + "strings" + + "golang.org/x/image/math/fixed" + + "realy.lol/gio/op" +) + +// Shaper implements layout and shaping of text. +type Shaper interface { + // Layout a text according to a set of options. + Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, + error) + // LayoutString is Layout for strings. + LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line + // Shape a line of text and return a clipping operation for its outline. + Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp +} + +// A FontFace is a Font and a matching Face. +type FontFace struct { + Font Font + Face Face +} + +// Cache implements cached layout and shaping of text from a set of +// registered fonts. +// +// If a font matches no registered shape, Cache falls back to the +// first registered face. +// +// The LayoutString and ShapeString results are cached and re-used if +// possible. +type Cache struct { + def Typeface + faces map[Font]*faceCache +} + +type faceCache struct { + face Face + layoutCache layoutCache + pathCache pathCache +} + +func (c *Cache) lookup(font Font) *faceCache { + f := c.faceForStyle(font) + if f == nil { + font.Typeface = c.def + f = c.faceForStyle(font) + } + return f +} + +func (c *Cache) faceForStyle(font Font) *faceCache { + tf := c.faces[font] + if tf == nil { + font := font + font.Weight = Normal + tf = c.faces[font] + } + if tf == nil { + font := font + font.Style = Regular + tf = c.faces[font] + } + if tf == nil { + font := font + font.Style = Regular + font.Weight = Normal + tf = c.faces[font] + } + return tf +} + +func NewCache(collection []FontFace) *Cache { + c := &Cache{ + faces: make(map[Font]*faceCache), + } + for i, ff := range collection { + if i == 0 { + c.def = ff.Font.Typeface + } + c.faces[ff.Font] = &faceCache{face: ff.Face} + } + return c +} + +// Layout implements the Shaper interface. +func (s *Cache) Layout(font Font, size fixed.Int26_6, maxWidth int, + txt io.Reader) ([]Line, error) { + cache := s.lookup(font) + return cache.face.Layout(size, maxWidth, txt) +} + +// LayoutString is a caching implementation of the Shaper interface. +func (s *Cache) LayoutString(font Font, size fixed.Int26_6, maxWidth int, + str string) []Line { + cache := s.lookup(font) + return cache.layout(size, maxWidth, str) +} + +// Shape is a caching implementation of the Shaper interface. Shape assumes that the layout +// argument is unchanged from a call to Layout or LayoutString. +func (s *Cache) Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp { + cache := s.lookup(font) + return cache.shape(size, layout) +} + +func (f *faceCache) layout(ppem fixed.Int26_6, maxWidth int, + str string) []Line { + if f == nil { + return nil + } + lk := layoutKey{ + ppem: ppem, + maxWidth: maxWidth, + str: str, + } + if l, ok := f.layoutCache.Get(lk); ok { + return l + } + l, _ := f.face.Layout(ppem, maxWidth, strings.NewReader(str)) + f.layoutCache.Put(lk, l) + return l +} + +func (f *faceCache) shape(ppem fixed.Int26_6, layout Layout) op.CallOp { + if f == nil { + return op.CallOp{} + } + pk := pathKey{ + ppem: ppem, + str: layout.Text, + } + if clip, ok := f.pathCache.Get(pk); ok { + return clip + } + clip := f.face.Shape(ppem, layout) + f.pathCache.Put(pk, clip) + return clip +} diff --git a/gio/giold/text/text.go b/gio/giold/text/text.go new file mode 100644 index 0000000..b50cc8a --- /dev/null +++ b/gio/giold/text/text.go @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "io" + + "golang.org/x/image/math/fixed" + + "realy.lol/gio/op" +) + +// A Line contains the measurements of a line of text. +type Line struct { + Layout Layout + // Width is the width of the line. + Width fixed.Int26_6 + // Ascent is the height above the baseline. + Ascent fixed.Int26_6 + // Descent is the height below the baseline, including + // the line gap. + Descent fixed.Int26_6 + // Bounds is the visible bounds of the line. + Bounds fixed.Rectangle26_6 +} + +type Layout struct { + Text string + Advances []fixed.Int26_6 +} + +// Style is the font style. +type Style int + +// Weight is a font weight, in CSS units subtracted 400 so the zero value +// is normal text weight. +type Weight int + +// Font specify a particular typeface variant, style and weight. +type Font struct { + Typeface Typeface + Variant Variant + Style Style + // Weight is the text weight. If zero, Normal is used instead. + Weight Weight +} + +// Face implements text layout and shaping for a particular font. All +// methods must be safe for concurrent use. +type Face interface { + Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) + Shape(ppem fixed.Int26_6, str Layout) op.CallOp +} + +// Typeface identifies a particular typeface design. The empty +// string denotes the default typeface. +type Typeface string + +// Variant denotes a typeface variant such as "Mono" or "Smallcaps". +type Variant string + +type Alignment uint8 + +const ( + Start Alignment = iota + End + Middle +) + +const ( + Regular Style = iota + Italic +) + +const ( + Normal Weight = 400 - 400 + Medium Weight = 500 - 400 + Bold Weight = 600 - 400 +) + +func (a Alignment) String() string { + switch a { + case Start: + return "Start" + case End: + return "End" + case Middle: + return "Middle" + default: + panic("unreachable") + } +} diff --git a/gio/giold/unit/unit.go b/gio/giold/unit/unit.go new file mode 100644 index 0000000..fd2245c --- /dev/null +++ b/gio/giold/unit/unit.go @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* + +Package unit implements device independent units and values. + +A Value is a value with a Unit attached. + +Device independent pixel, or dp, is the unit for sizes independent of +the underlying display device. + +Scaled pixels, or sp, is the unit for text sizes. An sp is like dp with +text scaling applied. + +Finally, pixels, or px, is the unit for display dependent pixels. Their +size vary between platforms and displays. + +To maintain a constant visual size across platforms and displays, always +use dps or sps to define user interfaces. Only use pixels for derived +values. + +*/ +package unit + +import ( + "fmt" + "math" +) + +// Value is a value with a unit. +type Value struct { + V float32 + U Unit +} + +// Unit represents a unit for a Value. +type Unit uint8 + +// Metric converts Values to device-dependent pixels, px. The zero +// value represents a 1-to-1 scale from dp, sp to pixels. +type Metric struct { + // PxPerDp is the device-dependent pixels per dp. + PxPerDp float32 + // PxPerSp is the device-dependent pixels per sp. + PxPerSp float32 +} + +const ( + // UnitPx represent device pixels in the resolution of + // the underlying display. + UnitPx Unit = iota + // UnitDp represents device independent pixels. 1 dp will + // have the same apparent size across platforms and + // display resolutions. + UnitDp + // UnitSp is like UnitDp but for font sizes. + UnitSp +) + +// Px returns the Value for v device pixels. +func Px(v float32) Value { + return Value{V: v, U: UnitPx} +} + +// Dp returns the Value for v device independent +// pixels. +func Dp(v float32) Value { + return Value{V: v, U: UnitDp} +} + +// Sp returns the Value for v scaled dps. +func Sp(v float32) Value { + return Value{V: v, U: UnitSp} +} + +// Scale returns the value scaled by s. +func (v Value) Scale(s float32) Value { + v.V *= s + return v +} + +func (v Value) String() string { + return fmt.Sprintf("%g%s", v.V, v.U) +} + +func (u Unit) String() string { + switch u { + case UnitPx: + return "px" + case UnitDp: + return "dp" + case UnitSp: + return "sp" + default: + panic("unknown unit") + } +} + +// Add a list of Values. +func Add(c Metric, values ...Value) Value { + var sum Value + for _, v := range values { + sum, v = compatible(c, sum, v) + sum.V += v.V + } + return sum +} + +// Max returns the maximum of a list of Values. +func Max(c Metric, values ...Value) Value { + var max Value + for _, v := range values { + max, v = compatible(c, max, v) + if v.V > max.V { + max.V = v.V + } + } + return max +} + +func (c Metric) Px(v Value) int { + var r float32 + switch v.U { + case UnitPx: + r = v.V + case UnitDp: + s := c.PxPerDp + if s == 0 { + s = 1 + } + r = s * v.V + case UnitSp: + s := c.PxPerSp + if s == 0 { + s = 1 + } + r = s * v.V + default: + panic("unknown unit") + } + return int(math.Round(float64(r))) +} + +func compatible(c Metric, v1, v2 Value) (Value, Value) { + if v1.U == v2.U { + return v1, v2 + } + if v1.V == 0 { + v1.U = v2.U + return v1, v2 + } + if v2.V == 0 { + v2.U = v1.U + return v1, v2 + } + return Px(float32(c.Px(v1))), Px(float32(c.Px(v2))) +} diff --git a/gio/giold/widget/bool.go b/gio/giold/widget/bool.go new file mode 100644 index 0000000..feb4a8a --- /dev/null +++ b/gio/giold/widget/bool.go @@ -0,0 +1,44 @@ +package widget + +import ( + "realy.lol/gio/layout" +) + +type Bool struct { + Value bool + + clk Clickable + + changed bool +} + +// Changed reports whether Value has changed since the last +// call to Changed. +func (b *Bool) Changed() bool { + changed := b.changed + b.changed = false + return changed +} + +// Hovered returns whether pointer is over the element. +func (b *Bool) Hovered() bool { + return b.clk.Hovered() +} + +// Pressed returns whether pointer is pressing the element. +func (b *Bool) Pressed() bool { + return b.clk.Pressed() +} + +func (b *Bool) History() []Press { + return b.clk.History() +} + +func (b *Bool) Layout(gtx layout.Context) layout.Dimensions { + dims := b.clk.Layout(gtx) + for b.clk.Clicked() { + b.Value = !b.Value + b.changed = true + } + return dims +} diff --git a/gio/giold/widget/border.go b/gio/giold/widget/border.go new file mode 100644 index 0000000..e4bed6a --- /dev/null +++ b/gio/giold/widget/border.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image/color" + + "realy.lol/gio/f32" + "realy.lol/gio/layout" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" +) + +// Border lays out a widget and draws a border inside it. +type Border struct { + Color color.NRGBA + CornerRadius unit.Value + Width unit.Value +} + +func (b Border) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { + dims := w(gtx) + sz := layout.FPt(dims.Size) + + rr := float32(gtx.Px(b.CornerRadius)) + width := float32(gtx.Px(b.Width)) + sz.X -= width + sz.Y -= width + + r := f32.Rectangle{Max: sz} + r = r.Add(f32.Point{X: width * 0.5, Y: width * 0.5}) + + paint.FillShape(gtx.Ops, + b.Color, + clip.Stroke{ + Path: clip.UniformRRect(r, rr).Path(gtx.Ops), + Style: clip.StrokeStyle{Width: width}, + }.Op(), + ) + + return dims +} diff --git a/gio/giold/widget/buffer.go b/gio/giold/widget/buffer.go new file mode 100644 index 0000000..e658d56 --- /dev/null +++ b/gio/giold/widget/buffer.go @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "io" + "strings" + "unicode/utf8" +) + +// editBuffer implements a gap buffer for text editing. +type editBuffer struct { + // pos is the byte position for Read and ReadRune. + pos int + + // The gap start and end in bytes. + gapstart, gapend int + text []byte + + // changed tracks whether the buffer content + // has changed since the last call to Changed. + changed bool +} + +const minSpace = 5 + +func (e *editBuffer) Changed() bool { + c := e.changed + e.changed = false + return c +} + +func (e *editBuffer) deleteRunes(caret, runes int) int { + e.moveGap(caret, 0) + for ; runes < 0 && e.gapstart > 0; runes++ { + _, s := utf8.DecodeLastRune(e.text[:e.gapstart]) + e.gapstart -= s + caret -= s + e.changed = e.changed || s > 0 + } + for ; runes > 0 && e.gapend < len(e.text); runes-- { + _, s := utf8.DecodeRune(e.text[e.gapend:]) + e.gapend += s + e.changed = e.changed || s > 0 + } + return caret +} + +// moveGap moves the gap to the caret position. After returning, +// the gap is guaranteed to be at least space bytes long. +func (e *editBuffer) moveGap(caret, space int) { + if e.gapLen() < space { + if space < minSpace { + space = minSpace + } + txt := make([]byte, e.len()+space) + // Expand to capacity. + txt = txt[:cap(txt)] + gaplen := len(txt) - e.len() + if caret > e.gapstart { + copy(txt, e.text[:e.gapstart]) + copy(txt[caret+gaplen:], e.text[caret:]) + copy(txt[e.gapstart:], e.text[e.gapend:caret+e.gapLen()]) + } else { + copy(txt, e.text[:caret]) + copy(txt[e.gapstart+gaplen:], e.text[e.gapend:]) + copy(txt[caret+gaplen:], e.text[caret:e.gapstart]) + } + e.text = txt + e.gapstart = caret + e.gapend = e.gapstart + gaplen + } else { + if caret > e.gapstart { + copy(e.text[e.gapstart:], e.text[e.gapend:caret+e.gapLen()]) + } else { + copy(e.text[caret+e.gapLen():], e.text[caret:e.gapstart]) + } + l := e.gapLen() + e.gapstart = caret + e.gapend = e.gapstart + l + } +} + +func (e *editBuffer) len() int { + return len(e.text) - e.gapLen() +} + +func (e *editBuffer) gapLen() int { + return e.gapend - e.gapstart +} + +func (e *editBuffer) Reset() { + e.Seek(0, io.SeekStart) +} + +// Seek implements io.Seeker +func (e *editBuffer) Seek(offset int64, whence int) (ret int64, err error) { + switch whence { + case io.SeekStart: + e.pos = int(offset) + case io.SeekCurrent: + e.pos += int(offset) + case io.SeekEnd: + e.pos = e.len() - int(offset) + } + if e.pos < 0 { + e.pos = 0 + } else if e.pos > e.len() { + e.pos = e.len() + } + return int64(e.pos), nil +} + +func (e *editBuffer) Read(p []byte) (int, error) { + if e.pos == e.len() { + return 0, io.EOF + } + var total int + if e.pos < e.gapstart { + n := copy(p, e.text[e.pos:e.gapstart]) + p = p[n:] + total += n + e.pos += n + } + if e.pos >= e.gapstart { + n := copy(p, e.text[e.pos+e.gapLen():]) + total += n + e.pos += n + } + if e.pos > e.len() { + panic("hey!") + } + return total, nil +} + +func (e *editBuffer) ReadRune() (rune, int, error) { + if e.pos == e.len() { + return 0, 0, io.EOF + } + r, s := e.runeAt(e.pos) + e.pos += s + return r, s, nil +} + +func (e *editBuffer) String() string { + var b strings.Builder + b.Grow(e.len()) + b.Write(e.text[:e.gapstart]) + b.Write(e.text[e.gapend:]) + return b.String() +} + +func (e *editBuffer) prepend(caret int, s string) { + e.moveGap(caret, len(s)) + copy(e.text[caret:], s) + e.gapstart += len(s) + e.changed = e.changed || len(s) > 0 +} + +func (e *editBuffer) runeBefore(idx int) (rune, int) { + if idx > e.gapstart { + idx += e.gapLen() + } + return utf8.DecodeLastRune(e.text[:idx]) +} + +func (e *editBuffer) runeAt(idx int) (rune, int) { + if idx >= e.gapstart { + idx += e.gapLen() + } + return utf8.DecodeRune(e.text[idx:]) +} diff --git a/gio/giold/widget/button.go b/gio/giold/widget/button.go new file mode 100644 index 0000000..2c23c5d --- /dev/null +++ b/gio/giold/widget/button.go @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + "time" + + "realy.lol/gio/f32" + "realy.lol/gio/gesture" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" +) + +// Clickable represents a clickable area. +type Clickable struct { + click gesture.Click + clicks []Click + // prevClicks is the index into clicks that marks the clicks + // from the most recent Layout call. prevClicks is used to keep + // clicks bounded. + prevClicks int + history []Press +} + +// Click represents a click. +type Click struct { + Modifiers key.Modifiers + NumClicks int +} + +// Press represents a past pointer press. +type Press struct { + // Position of the press. + Position f32.Point + // Start is when the press began. + Start time.Time + // End is when the press was ended by a release or cancel. + // A zero End means it hasn't ended yet. + End time.Time + // Cancelled is true for cancelled presses. + Cancelled bool +} + +// Click executes a simple programmatic click +func (b *Clickable) Click() { + b.clicks = append(b.clicks, Click{ + Modifiers: 0, + NumClicks: 1, + }) +} + +// Clicked reports whether there are pending clicks as would be +// reported by Clicks. If so, Clicked removes the earliest click. +func (b *Clickable) Clicked() bool { + if len(b.clicks) == 0 { + return false + } + n := copy(b.clicks, b.clicks[1:]) + b.clicks = b.clicks[:n] + if b.prevClicks > 0 { + b.prevClicks-- + } + return true +} + +// Hovered returns whether pointer is over the element. +func (b *Clickable) Hovered() bool { + return b.click.Hovered() +} + +// Pressed returns whether pointer is pressing the element. +func (b *Clickable) Pressed() bool { + return b.click.Pressed() +} + +// Clicks returns and clear the clicks since the last call to Clicks. +func (b *Clickable) Clicks() []Click { + clicks := b.clicks + b.clicks = nil + b.prevClicks = 0 + return clicks +} + +// History is the past pointer presses useful for drawing markers. +// History is retained for a short duration (about a second). +func (b *Clickable) History() []Press { + return b.history +} + +// Layout and update the button state +func (b *Clickable) Layout(gtx layout.Context) layout.Dimensions { + b.update(gtx) + stack := op.Save(gtx.Ops) + pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops) + b.click.Add(gtx.Ops) + stack.Load() + for len(b.history) > 0 { + c := b.history[0] + if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second { + break + } + n := copy(b.history, b.history[1:]) + b.history = b.history[:n] + } + return layout.Dimensions{Size: gtx.Constraints.Min} +} + +// update the button state by processing events. +func (b *Clickable) update(gtx layout.Context) { + // Flush clicks from before the last update. + n := copy(b.clicks, b.clicks[b.prevClicks:]) + b.clicks = b.clicks[:n] + b.prevClicks = n + + for _, e := range b.click.Events(gtx) { + switch e.Type { + case gesture.TypeClick: + b.clicks = append(b.clicks, Click{ + Modifiers: e.Modifiers, + NumClicks: e.NumClicks, + }) + if l := len(b.history); l > 0 { + b.history[l-1].End = gtx.Now + } + case gesture.TypeCancel: + for i := range b.history { + b.history[i].Cancelled = true + if b.history[i].End.IsZero() { + b.history[i].End = gtx.Now + } + } + case gesture.TypePress: + b.history = append(b.history, Press{ + Position: e.Position, + Start: gtx.Now, + }) + } + } +} diff --git a/gio/giold/widget/doc.go b/gio/giold/widget/doc.go new file mode 100644 index 0000000..df4e55f --- /dev/null +++ b/gio/giold/widget/doc.go @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package widget implements state tracking and event handling of +// common user interface controls. To draw widgets, use a theme +// packages such as package realy.lol/gio/widget/material. +package widget diff --git a/gio/giold/widget/editor.go b/gio/giold/widget/editor.go new file mode 100644 index 0000000..e44f54f --- /dev/null +++ b/gio/giold/widget/editor.go @@ -0,0 +1,1328 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "bufio" + "bytes" + "image" + "io" + "math" + "runtime" + "sort" + "strings" + "time" + "unicode" + "unicode/utf8" + + "realy.lol/gio/f32" + "realy.lol/gio/gesture" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" + + "golang.org/x/image/math/fixed" +) + +// Editor implements an editable and scrollable text area. +type Editor struct { + Alignment text.Alignment + // SingleLine force the text to stay on a single line. + // SingleLine also sets the scrolling direction to + // horizontal. + SingleLine bool + // Submit enabled translation of carriage return keys to SubmitEvents. + // If not enabled, carriage returns are inserted as newlines in the text. + Submit bool + // Mask replaces the visual display of each rune in the contents with the given rune. + // Newline characters are not masked. When non-zero, the unmasked contents + // are accessed by Len, Text, and SetText. + Mask rune + + eventKey int + font text.Font + shaper text.Shaper + textSize fixed.Int26_6 + blinkStart time.Time + focused bool + rr editBuffer + maskReader maskReader + lastMask rune + maxWidth int + viewSize image.Point + valid bool + lines []text.Line + shapes []line + dims layout.Dimensions + requestFocus bool + + caret struct { + on bool + scroll bool + // start is the current caret position, and also the start position of + // selected text. end is the end positon of selected text. If start.ofs + // == end.ofs, then there's no selection. Note that it's possible (and + // common) that the caret (start) is after the end, e.g. after + // Shift-DownArrow. + start combinedPos + end combinedPos + } + + dragging bool + dragger gesture.Drag + scroller gesture.Scroll + scrollOff image.Point + + clicker gesture.Click + + // events is the list of events not yet processed. + events []EditorEvent + // prevEvents is the number of events from the previous frame. + prevEvents int +} + +type maskReader struct { + // rr is the underlying reader. + rr io.RuneReader + maskBuf [utf8.UTFMax]byte + // mask is the utf-8 encoded mask rune. + mask []byte + // overflow contains excess mask bytes left over after the last Read call. + overflow []byte +} + +// combinedPos is a point in the editor. +type combinedPos struct { + // editorBuffer offset. The other three fields are based off of this one. + ofs int + + // lineCol.Y = line (offset into Editor.lines), and X = col (offset into + // Editor.lines[Y]) + lineCol screenPos + + // Pixel coordinates + x fixed.Int26_6 + y int + + // xoff is the offset to the current position when moving between lines. + xoff fixed.Int26_6 +} + +type selectionAction int + +const ( + selectionExtend selectionAction = iota + selectionClear +) + +func (m *maskReader) Reset(r io.RuneReader, mr rune) { + m.rr = r + n := utf8.EncodeRune(m.maskBuf[:], mr) + m.mask = m.maskBuf[:n] +} + +// Read reads from the underlying reader and replaces every +// rune with the mask rune. +func (m *maskReader) Read(b []byte) (n int, err error) { + for len(b) > 0 { + var replacement []byte + if len(m.overflow) > 0 { + replacement = m.overflow + } else { + var r rune + r, _, err = m.rr.ReadRune() + if err != nil { + break + } + if r == '\n' { + replacement = []byte{'\n'} + } else { + replacement = m.mask + } + } + nn := copy(b, replacement) + m.overflow = replacement[nn:] + n += nn + b = b[nn:] + } + return n, err +} + +type EditorEvent interface { + isEditorEvent() +} + +// A ChangeEvent is generated for every user change to the text. +type ChangeEvent struct{} + +// A SubmitEvent is generated when Submit is set +// and a carriage return key is pressed. +type SubmitEvent struct { + Text string +} + +// A SelectEvent is generated when the user selects some text, or changes the +// selection (e.g. with a shift-click), including if they remove the +// selection. The selected text is not part of the event, on the theory that +// it could be a relatively expensive operation (for a large editor), most +// applications won't actually care about it, and those that do can call +// Editor.SelectedText() (which can be empty). +type SelectEvent struct{} + +type line struct { + offset image.Point + clip op.CallOp + selected bool + selectionYOffs int + selectionSize image.Point +} + +const ( + blinksPerSecond = 1 + maxBlinkDuration = 10 * time.Second +) + +// Events returns available editor events. +func (e *Editor) Events() []EditorEvent { + events := e.events + e.events = nil + e.prevEvents = 0 + return events +} + +func (e *Editor) processEvents(gtx layout.Context) { + // Flush events from before the previous Layout. + n := copy(e.events, e.events[e.prevEvents:]) + e.events = e.events[:n] + e.prevEvents = n + + if e.shaper == nil { + // Can't process events without a shaper. + return + } + oldStart, oldLen := min(e.caret.start.ofs, + e.caret.end.ofs), e.SelectionLen() + e.processPointer(gtx) + e.processKey(gtx) + // Queue a SelectEvent if the selection changed, including if it went away. + if newStart, newLen := min(e.caret.start.ofs, + e.caret.end.ofs), e.SelectionLen(); oldStart != newStart || oldLen != newLen { + e.events = append(e.events, SelectEvent{}) + } +} + +func (e *Editor) makeValid(positions ...*combinedPos) { + if e.valid { + return + } + e.lines, e.dims = e.layoutText(e.shaper) + e.makeValidCaret(positions...) + e.valid = true +} + +func (e *Editor) processPointer(gtx layout.Context) { + sbounds := e.scrollBounds() + var smin, smax int + var axis gesture.Axis + if e.SingleLine { + axis = gesture.Horizontal + smin, smax = sbounds.Min.X, sbounds.Max.X + } else { + axis = gesture.Vertical + smin, smax = sbounds.Min.Y, sbounds.Max.Y + } + sdist := e.scroller.Scroll(gtx.Metric, gtx, gtx.Now, axis) + var soff int + if e.SingleLine { + e.scrollRel(sdist, 0) + soff = e.scrollOff.X + } else { + e.scrollRel(0, sdist) + soff = e.scrollOff.Y + } + for _, evt := range e.clickDragEvents(gtx) { + switch evt := evt.(type) { + case gesture.ClickEvent: + switch { + case evt.Type == gesture.TypePress && evt.Source == pointer.Mouse, + evt.Type == gesture.TypeClick: + prevCaretPos := e.caret.start + e.blinkStart = gtx.Now + e.moveCoord(image.Point{ + X: int(math.Round(float64(evt.Position.X))), + Y: int(math.Round(float64(evt.Position.Y))), + }) + e.requestFocus = true + if e.scroller.State() != gesture.StateFlinging { + e.caret.scroll = true + } + + if evt.Modifiers == key.ModShift { + // If they clicked closer to the end, then change the end to + // where the caret used to be (effectively swapping start & end). + if abs(e.caret.end.ofs-e.caret.start.ofs) < abs(e.caret.start.ofs-prevCaretPos.ofs) { + e.caret.end = prevCaretPos + } + } else { + e.ClearSelection() + } + e.dragging = true + + // Process a double-click. + if evt.NumClicks == 2 { + e.moveWord(-1, selectionClear) + e.moveWord(1, selectionExtend) + e.dragging = false + } + } + case pointer.Event: + release := false + switch { + case evt.Type == pointer.Release && evt.Source == pointer.Mouse: + release = true + fallthrough + case evt.Type == pointer.Drag && evt.Source == pointer.Mouse: + if e.dragging { + e.blinkStart = gtx.Now + e.moveCoord(image.Point{ + X: int(math.Round(float64(evt.Position.X))), + Y: int(math.Round(float64(evt.Position.Y))), + }) + e.caret.scroll = true + + if release { + e.dragging = false + } + } + } + } + } + + if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) { + e.scroller.Stop() + } +} + +func (e *Editor) clickDragEvents(gtx layout.Context) []event.Event { + var combinedEvents []event.Event + for _, evt := range e.clicker.Events(gtx) { + combinedEvents = append(combinedEvents, evt) + } + for _, evt := range e.dragger.Events(gtx.Metric, gtx, gesture.Both) { + combinedEvents = append(combinedEvents, evt) + } + return combinedEvents +} + +func (e *Editor) processKey(gtx layout.Context) { + if e.rr.Changed() { + e.events = append(e.events, ChangeEvent{}) + } + for _, ke := range gtx.Events(&e.eventKey) { + e.blinkStart = gtx.Now + switch ke := ke.(type) { + case key.FocusEvent: + e.focused = ke.Focus + case key.Event: + if !e.focused || ke.State != key.Press { + break + } + if e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) { + if !ke.Modifiers.Contain(key.ModShift) { + e.events = append(e.events, SubmitEvent{ + Text: e.Text(), + }) + continue + } + } + if e.command(gtx, ke) { + e.caret.scroll = true + e.scroller.Stop() + } + case key.EditEvent: + e.caret.scroll = true + e.scroller.Stop() + e.append(ke.Text) + // Complete a paste event, initiated by Shortcut-V in Editor.command(). + case clipboard.Event: + e.caret.scroll = true + e.scroller.Stop() + e.append(ke.Text) + } + if e.rr.Changed() { + e.events = append(e.events, ChangeEvent{}) + } + } +} + +func (e *Editor) moveLines(distance int, selAct selectionAction) { + e.caret.start = e.movePosToLine(e.caret.start, + e.caret.start.x+e.caret.start.xoff, e.caret.start.lineCol.Y+distance) + e.updateSelection(selAct) +} + +func (e *Editor) command(gtx layout.Context, k key.Event) bool { + modSkip := key.ModCtrl + if runtime.GOOS == "darwin" { + modSkip = key.ModAlt + } + moveByWord := k.Modifiers.Contain(modSkip) + selAct := selectionClear + if k.Modifiers.Contain(key.ModShift) { + selAct = selectionExtend + } + switch k.Name { + case key.NameReturn, key.NameEnter: + e.append("\n") + case key.NameDeleteBackward: + if moveByWord { + e.deleteWord(-1) + } else { + e.Delete(-1) + } + case key.NameDeleteForward: + if moveByWord { + e.deleteWord(1) + } else { + e.Delete(1) + } + case key.NameUpArrow: + e.moveLines(-1, selAct) + case key.NameDownArrow: + e.moveLines(+1, selAct) + case key.NameLeftArrow: + if moveByWord { + e.moveWord(-1, selAct) + } else { + if selAct == selectionClear { + e.ClearSelection() + } + e.MoveCaret(-1, -1*int(selAct)) + } + case key.NameRightArrow: + if moveByWord { + e.moveWord(1, selAct) + } else { + if selAct == selectionClear { + e.ClearSelection() + } + e.MoveCaret(1, int(selAct)) + } + case key.NamePageUp: + e.movePages(-1, selAct) + case key.NamePageDown: + e.movePages(+1, selAct) + case key.NameHome: + e.moveStart(selAct) + case key.NameEnd: + e.moveEnd(selAct) + // Initiate a paste operation, by requesting the clipboard contents; other + // half is in Editor.processKey() under clipboard.Event. + case "V": + if k.Modifiers != key.ModShortcut { + return false + } + clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops) + // Copy or Cut selection -- ignored if nothing selected. + case "C", "X": + if k.Modifiers != key.ModShortcut { + return false + } + if text := e.SelectedText(); text != "" { + clipboard.WriteOp{Text: text}.Add(gtx.Ops) + if k.Name == "X" { + e.Delete(1) + } + } + // Select all + case "A": + if k.Modifiers != key.ModShortcut { + return false + } + e.caret.end, e.caret.start = e.offsetToScreenPos2(0, e.Len()) + default: + return false + } + return true +} + +// Focus requests the input focus for the Editor. +func (e *Editor) Focus() { + e.requestFocus = true +} + +// Focused returns whether the editor is focused or not. +func (e *Editor) Focused() bool { + return e.focused +} + +// Layout lays out the editor. +func (e *Editor) Layout(gtx layout.Context, sh text.Shaper, font text.Font, + size unit.Value) layout.Dimensions { + textSize := fixed.I(gtx.Px(size)) + if e.font != font || e.textSize != textSize { + e.invalidate() + e.font = font + e.textSize = textSize + } + maxWidth := gtx.Constraints.Max.X + if e.SingleLine { + maxWidth = inf + } + if maxWidth != e.maxWidth { + e.maxWidth = maxWidth + e.invalidate() + } + if sh != e.shaper { + e.shaper = sh + e.invalidate() + } + if e.Mask != e.lastMask { + e.lastMask = e.Mask + e.invalidate() + } + + e.makeValid() + e.processEvents(gtx) + e.makeValid() + + if viewSize := gtx.Constraints.Constrain(e.dims.Size); viewSize != e.viewSize { + e.viewSize = viewSize + e.invalidate() + } + e.makeValid() + + return e.layout(gtx) +} + +func (e *Editor) layout(gtx layout.Context) layout.Dimensions { + // Adjust scrolling for new viewport and layout. + e.scrollRel(0, 0) + + if e.caret.scroll { + e.caret.scroll = false + e.scrollToCaret() + } + + off := image.Point{ + X: -e.scrollOff.X, + Y: -e.scrollOff.Y, + } + clip := textPadding(e.lines) + clip.Max = clip.Max.Add(e.viewSize) + startSel, endSel := sortPoints(e.caret.start.lineCol, e.caret.end.lineCol) + it := segmentIterator{ + startSel: startSel, + endSel: endSel, + Lines: e.lines, + Clip: clip, + Alignment: e.Alignment, + Width: e.viewSize.X, + Offset: off, + } + e.shapes = e.shapes[:0] + for { + layout, off, selected, yOffs, size, ok := it.Next() + if !ok { + break + } + path := e.shaper.Shape(e.font, e.textSize, layout) + e.shapes = append(e.shapes, line{off, path, selected, yOffs, size}) + } + + key.InputOp{Tag: &e.eventKey}.Add(gtx.Ops) + if e.requestFocus { + key.FocusOp{Tag: &e.eventKey}.Add(gtx.Ops) + key.SoftKeyboardOp{Show: true}.Add(gtx.Ops) + } + e.requestFocus = false + pointerPadding := gtx.Px(unit.Dp(4)) + r := image.Rectangle{Max: e.viewSize} + r.Min.X -= pointerPadding + r.Min.Y -= pointerPadding + r.Max.X += pointerPadding + r.Max.X += pointerPadding + pointer.Rect(r).Add(gtx.Ops) + pointer.CursorNameOp{Name: pointer.CursorText}.Add(gtx.Ops) + + var scrollRange image.Rectangle + if e.SingleLine { + scrollRange.Min.X = -e.scrollOff.X + scrollRange.Max.X = max(0, e.dims.Size.X-(e.scrollOff.X+e.viewSize.X)) + } else { + scrollRange.Min.Y = -e.scrollOff.Y + scrollRange.Max.Y = max(0, e.dims.Size.Y-(e.scrollOff.Y+e.viewSize.Y)) + } + e.scroller.Add(gtx.Ops, scrollRange) + + e.clicker.Add(gtx.Ops) + e.dragger.Add(gtx.Ops) + e.caret.on = false + if e.focused { + now := gtx.Now + dt := now.Sub(e.blinkStart) + blinking := dt < maxBlinkDuration + const timePerBlink = time.Second / blinksPerSecond + nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2)) + if blinking { + redraw := op.InvalidateOp{At: nextBlink} + redraw.Add(gtx.Ops) + } + e.caret.on = e.focused && (!blinking || dt%timePerBlink < timePerBlink/2) + } + + return layout.Dimensions{Size: e.viewSize, Baseline: e.dims.Baseline} +} + +// PaintSelection paints the contrasting background for selected text. +func (e *Editor) PaintSelection(gtx layout.Context) { + cl := textPadding(e.lines) + cl.Max = cl.Max.Add(e.viewSize) + clip.Rect(cl).Add(gtx.Ops) + for _, shape := range e.shapes { + if !shape.selected { + continue + } + stack := op.Save(gtx.Ops) + offset := shape.offset + offset.Y += shape.selectionYOffs + op.Offset(layout.FPt(offset)).Add(gtx.Ops) + clip.Rect(image.Rectangle{Max: shape.selectionSize}).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + } +} + +func (e *Editor) PaintText(gtx layout.Context) { + cl := textPadding(e.lines) + cl.Max = cl.Max.Add(e.viewSize) + clip.Rect(cl).Add(gtx.Ops) + for _, shape := range e.shapes { + stack := op.Save(gtx.Ops) + op.Offset(layout.FPt(shape.offset)).Add(gtx.Ops) + shape.clip.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + } +} + +func (e *Editor) PaintCaret(gtx layout.Context) { + if !e.caret.on { + return + } + e.makeValid() + carWidth := fixed.I(gtx.Px(unit.Dp(1))) + carX := e.caret.start.x + carY := e.caret.start.y + + defer op.Save(gtx.Ops).Load() + carX -= carWidth / 2 + carAsc, carDesc := -e.lines[e.caret.start.lineCol.Y].Bounds.Min.Y, e.lines[e.caret.start.lineCol.Y].Bounds.Max.Y + carRect := image.Rectangle{ + Min: image.Point{X: carX.Ceil(), Y: carY - carAsc.Ceil()}, + Max: image.Point{X: carX.Ceil() + carWidth.Ceil(), + Y: carY + carDesc.Ceil()}, + } + carRect = carRect.Add(image.Point{ + X: -e.scrollOff.X, + Y: -e.scrollOff.Y, + }) + cl := textPadding(e.lines) + // Account for caret width to each side. + whalf := (carWidth / 2).Ceil() + if cl.Max.X < whalf { + cl.Max.X = whalf + } + if cl.Min.X > -whalf { + cl.Min.X = -whalf + } + cl.Max = cl.Max.Add(e.viewSize) + carRect = cl.Intersect(carRect) + if !carRect.Empty() { + st := op.Save(gtx.Ops) + clip.Rect(carRect).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + st.Load() + } +} + +// Len is the length of the editor contents. +func (e *Editor) Len() int { + return e.rr.len() +} + +// Text returns the contents of the editor. +func (e *Editor) Text() string { + return e.rr.String() +} + +// SetText replaces the contents of the editor, clearing any selection first. +func (e *Editor) SetText(s string) { + e.rr = editBuffer{} + e.caret.start = combinedPos{} + e.caret.end = combinedPos{} + e.prepend(s) +} + +func (e *Editor) scrollBounds() image.Rectangle { + var b image.Rectangle + if e.SingleLine { + if len(e.lines) > 0 { + b.Min.X = align(e.Alignment, e.lines[0].Width, e.viewSize.X).Floor() + if b.Min.X > 0 { + b.Min.X = 0 + } + } + b.Max.X = e.dims.Size.X + b.Min.X - e.viewSize.X + } else { + b.Max.Y = e.dims.Size.Y - e.viewSize.Y + } + return b +} + +func (e *Editor) scrollRel(dx, dy int) { + e.scrollAbs(e.scrollOff.X+dx, e.scrollOff.Y+dy) +} + +func (e *Editor) scrollAbs(x, y int) { + e.scrollOff.X = x + e.scrollOff.Y = y + b := e.scrollBounds() + if e.scrollOff.X > b.Max.X { + e.scrollOff.X = b.Max.X + } + if e.scrollOff.X < b.Min.X { + e.scrollOff.X = b.Min.X + } + if e.scrollOff.Y > b.Max.Y { + e.scrollOff.Y = b.Max.Y + } + if e.scrollOff.Y < b.Min.Y { + e.scrollOff.Y = b.Min.Y + } +} + +func (e *Editor) moveCoord(pos image.Point) { + var ( + prevDesc fixed.Int26_6 + carLine int + y int + ) + for _, l := range e.lines { + y += (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + if y+prevDesc.Ceil() >= pos.Y+e.scrollOff.Y { + break + } + carLine++ + } + x := fixed.I(pos.X + e.scrollOff.X) + e.caret.start = e.movePosToLine(e.caret.start, x, carLine) + e.caret.start.xoff = 0 +} + +func (e *Editor) layoutText(s text.Shaper) ([]text.Line, layout.Dimensions) { + e.rr.Reset() + var r io.Reader = &e.rr + if e.Mask != 0 { + e.maskReader.Reset(&e.rr, e.Mask) + r = &e.maskReader + } + var lines []text.Line + if s != nil { + lines, _ = s.Layout(e.font, e.textSize, e.maxWidth, r) + } else { + lines, _ = nullLayout(r) + } + dims := linesDimens(lines) + for i := 0; i < len(lines)-1; i++ { + // To avoid layout flickering while editing, assume a soft newline takes + // up all available space. + if layout := lines[i].Layout; len(layout.Text) > 0 { + r := layout.Text[len(layout.Text)-1] + if r != '\n' { + dims.Size.X = e.maxWidth + break + } + } + } + return lines, dims +} + +// CaretPos returns the line & column numbers of the caret. +func (e *Editor) CaretPos() (line, col int) { + e.makeValid() + return e.caret.start.lineCol.Y, e.caret.start.lineCol.X +} + +// CaretCoords returns the coordinates of the caret, relative to the +// editor itself. +func (e *Editor) CaretCoords() f32.Point { + e.makeValid() + return f32.Pt(float32(e.caret.start.x)/64, float32(e.caret.start.y)) +} + +// offsetToScreenPos2 is a utility function to shortcut the common case of +// wanting the positions of exactly two offsets. +func (e *Editor) offsetToScreenPos2(o1, o2 int) (combinedPos, combinedPos) { + cp1, iter := e.offsetToScreenPos(o1) + return cp1, iter(o2) +} + +// offsetToScreenPos takes an offset into the editor text (e.g. +// e.caret.end.ofs) and returns a combinedPos that corresponds to its current +// screen position, as well as an iterator that lets you get the combinedPos +// of a later offset. The offsets given to offsetToScreenPos and to the +// returned iterator must be sorted, lowest first, and they must be valid (0 +// <= offset <= e.Len()). +// +// This function is written this way to take advantage of previous work done +// for offsets after the first. Otherwise you have to start from the top each +// time. +func (e *Editor) offsetToScreenPos(offset int) (combinedPos, + func(int) combinedPos) { + var col, line, idx int + var x fixed.Int26_6 + + l := e.lines[line] + y := l.Ascent.Ceil() + prevDesc := l.Descent + + iter := func(offset int) combinedPos { + LOOP: + for { + for ; col < len(l.Layout.Advances); col++ { + if idx >= offset { + break LOOP + } + + x += l.Layout.Advances[col] + _, s := e.rr.runeAt(idx) + idx += s + } + if lastLine := line == len(e.lines)-1; lastLine || idx > offset { + break LOOP + } + + line++ + x = 0 + col = 0 + l = e.lines[line] + y += (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + } + return combinedPos{ + lineCol: screenPos{Y: line, X: col}, + x: x + align(e.Alignment, e.lines[line].Width, e.viewSize.X), + y: y, + ofs: offset, + } + } + return iter(offset), iter +} + +func (e *Editor) invalidate() { + e.valid = false +} + +// Delete runes from the caret position. The sign of runes specifies the +// direction to delete: positive is forward, negative is backward. +// +// If there is a selection, it is deleted and counts as a single rune. +func (e *Editor) Delete(runes int) { + if runes == 0 { + return + } + + if l := e.caret.end.ofs - e.caret.start.ofs; l != 0 { + e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, l) + runes -= sign(runes) + } + + e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, runes) + e.caret.start.xoff = 0 + e.ClearSelection() + e.invalidate() +} + +// Insert inserts text at the caret, moving the caret forward. If there is a +// selection, Insert overwrites it. +func (e *Editor) Insert(s string) { + e.append(s) + e.caret.scroll = true +} + +// append inserts s at the cursor, leaving the caret is at the end of s. If +// there is a selection, append overwrites it. +// xxx|yyy + append zzz => xxxzzz|yyy +func (e *Editor) append(s string) { + e.prepend(s) + e.caret.start.ofs += len(s) + e.caret.end.ofs = e.caret.start.ofs +} + +// prepend inserts s after the cursor; the caret does not change. If there is +// a selection, prepend overwrites it. +// xxx|yyy + prepend zzz => xxx|zzzyyy +func (e *Editor) prepend(s string) { + if e.SingleLine { + s = strings.ReplaceAll(s, "\n", " ") + } + e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, + e.caret.end.ofs-e.caret.start.ofs) // Delete any selection first. + e.rr.prepend(e.caret.start.ofs, s) + e.caret.start.xoff = 0 + e.invalidate() +} + +func (e *Editor) movePages(pages int, selAct selectionAction) { + e.makeValid() + y := e.caret.start.y + pages*e.viewSize.Y + var ( + prevDesc fixed.Int26_6 + carLine2 int + ) + y2 := e.lines[0].Ascent.Ceil() + for i := 1; i < len(e.lines); i++ { + if y2 >= y { + break + } + l := e.lines[i] + h := (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + if y2+h-y >= y-y2 { + break + } + y2 += h + carLine2++ + } + e.caret.start = e.movePosToLine(e.caret.start, + e.caret.start.x+e.caret.start.xoff, carLine2) + e.updateSelection(selAct) +} + +func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6, + line int) combinedPos { + e.makeValid(&pos) + if line < 0 { + line = 0 + } + if line >= len(e.lines) { + line = len(e.lines) - 1 + } + + prevDesc := e.lines[line].Descent + for pos.lineCol.Y < line { + pos = e.movePosToEnd(pos) + l := e.lines[pos.lineCol.Y] + _, s := e.rr.runeAt(pos.ofs) + pos.ofs += s + pos.y += (prevDesc + l.Ascent).Ceil() + pos.lineCol.X = 0 + prevDesc = l.Descent + pos.lineCol.Y++ + } + for pos.lineCol.Y > line { + pos = e.movePosToStart(pos) + l := e.lines[pos.lineCol.Y] + _, s := e.rr.runeBefore(pos.ofs) + pos.ofs -= s + pos.y -= (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + pos.lineCol.Y-- + l = e.lines[pos.lineCol.Y] + pos.lineCol.X = len(l.Layout.Advances) - 1 + } + + pos = e.movePosToStart(pos) + l := e.lines[line] + pos.x = align(e.Alignment, l.Width, e.viewSize.X) + // Only move past the end of the last line + end := 0 + if line < len(e.lines)-1 { + end = 1 + } + // Move to rune closest to x. + for i := 0; i < len(l.Layout.Advances)-end; i++ { + adv := l.Layout.Advances[i] + if pos.x >= x { + break + } + if pos.x+adv-x >= x-pos.x { + break + } + pos.x += adv + _, s := e.rr.runeAt(pos.ofs) + pos.ofs += s + pos.lineCol.X++ + } + pos.xoff = x - pos.x + return pos +} + +// MoveCaret moves the caret (aka selection start) and the selection end +// relative to their current positions. Positive distances moves forward, +// negative distances moves backward. Distances are in runes. +func (e *Editor) MoveCaret(startDelta, endDelta int) { + e.makeValid() + keepSame := e.caret.start.ofs == e.caret.end.ofs && startDelta == endDelta + e.caret.start = e.movePos(e.caret.start, startDelta) + e.caret.start.xoff = 0 + // If they were in the same place, and we're moving them the same distance, + // just assign the new position, instead of recalculating it. + if keepSame { + e.caret.end = e.caret.start + } else { + e.caret.end = e.movePos(e.caret.end, endDelta) + e.caret.end.xoff = 0 + } +} + +func (e *Editor) movePos(pos combinedPos, distance int) combinedPos { + for ; distance < 0 && pos.ofs > 0; distance++ { + if pos.lineCol.X == 0 { + // Move to end of previous line. + pos = e.movePosToLine(pos, fixed.I(e.maxWidth), pos.lineCol.Y-1) + continue + } + l := e.lines[pos.lineCol.Y].Layout + _, s := e.rr.runeBefore(pos.ofs) + pos.ofs -= s + pos.lineCol.X-- + pos.x -= l.Advances[pos.lineCol.X] + } + for ; distance > 0 && pos.ofs < e.rr.len(); distance-- { + l := e.lines[pos.lineCol.Y].Layout + // Only move past the end of the last line + end := 0 + if pos.lineCol.Y < len(e.lines)-1 { + end = 1 + } + if pos.lineCol.X >= len(l.Advances)-end { + // Move to start of next line. + pos = e.movePosToLine(pos, 0, pos.lineCol.Y+1) + continue + } + pos.x += l.Advances[pos.lineCol.X] + _, s := e.rr.runeAt(pos.ofs) + pos.ofs += s + pos.lineCol.X++ + } + return pos +} + +func (e *Editor) moveStart(selAct selectionAction) { + e.caret.start = e.movePosToStart(e.caret.start) + e.updateSelection(selAct) +} + +func (e *Editor) movePosToStart(pos combinedPos) combinedPos { + e.makeValid(&pos) + layout := e.lines[pos.lineCol.Y].Layout + for i := pos.lineCol.X - 1; i >= 0; i-- { + _, s := e.rr.runeBefore(pos.ofs) + pos.ofs -= s + pos.x -= layout.Advances[i] + } + pos.lineCol.X = 0 + pos.xoff = -pos.x + return pos +} + +func (e *Editor) moveEnd(selAct selectionAction) { + e.caret.start = e.movePosToEnd(e.caret.start) + e.updateSelection(selAct) +} + +func (e *Editor) movePosToEnd(pos combinedPos) combinedPos { + e.makeValid(&pos) + l := e.lines[pos.lineCol.Y] + // Only move past the end of the last line + end := 0 + if pos.lineCol.Y < len(e.lines)-1 { + end = 1 + } + layout := l.Layout + for i := pos.lineCol.X; i < len(layout.Advances)-end; i++ { + adv := layout.Advances[i] + _, s := e.rr.runeAt(pos.ofs) + pos.ofs += s + pos.x += adv + pos.lineCol.X++ + } + a := align(e.Alignment, l.Width, e.viewSize.X) + pos.xoff = l.Width + a - pos.x + return pos +} + +// moveWord moves the caret to the next word in the specified direction. +// Positive is forward, negative is backward. +// Absolute values greater than one will skip that many words. +func (e *Editor) moveWord(distance int, selAct selectionAction) { + e.makeValid() + // split the distance information into constituent parts to be + // used independently. + words, direction := distance, 1 + if distance < 0 { + words, direction = distance*-1, -1 + } + // atEnd if caret is at either side of the buffer. + atEnd := func() bool { + return e.caret.start.ofs == 0 || e.caret.start.ofs == e.rr.len() + } + // next returns the appropriate rune given the direction. + next := func() (r rune) { + if direction < 0 { + r, _ = e.rr.runeBefore(e.caret.start.ofs) + } else { + r, _ = e.rr.runeAt(e.caret.start.ofs) + } + return r + } + for ii := 0; ii < words; ii++ { + for r := next(); unicode.IsSpace(r) && !atEnd(); r = next() { + e.MoveCaret(direction, 0) + } + e.MoveCaret(direction, 0) + for r := next(); !unicode.IsSpace(r) && !atEnd(); r = next() { + e.MoveCaret(direction, 0) + } + } + e.updateSelection(selAct) +} + +// deleteWord deletes the next word(s) in the specified direction. +// Unlike moveWord, deleteWord treats whitespace as a word itself. +// Positive is forward, negative is backward. +// Absolute values greater than one will delete that many words. +// The selection counts as a single word. +func (e *Editor) deleteWord(distance int) { + if distance == 0 { + return + } + + e.makeValid() + + if e.caret.start.ofs != e.caret.end.ofs { + e.Delete(1) + distance -= sign(distance) + } + if distance == 0 { + return + } + + // split the distance information into constituent parts to be + // used independently. + words, direction := distance, 1 + if distance < 0 { + words, direction = distance*-1, -1 + } + // atEnd if offset is at or beyond either side of the buffer. + atEnd := func(offset int) bool { + idx := e.caret.start.ofs + offset*direction + return idx <= 0 || idx >= e.rr.len() + } + // next returns the appropriate rune given the direction and offset. + next := func(offset int) (r rune) { + idx := e.caret.start.ofs + offset*direction + if idx < 0 { + idx = 0 + } else if idx > e.rr.len() { + idx = e.rr.len() + } + if direction < 0 { + r, _ = e.rr.runeBefore(idx) + } else { + r, _ = e.rr.runeAt(idx) + } + return r + } + var runes = 1 + for ii := 0; ii < words; ii++ { + if r := next(runes); unicode.IsSpace(r) { + for r := next(runes); unicode.IsSpace(r) && !atEnd(runes); r = next(runes) { + runes += 1 + } + } else { + for r := next(runes); !unicode.IsSpace(r) && !atEnd(runes); r = next(runes) { + runes += 1 + } + } + } + e.Delete(runes * direction) +} + +func (e *Editor) scrollToCaret() { + e.makeValid() + l := e.lines[e.caret.start.lineCol.Y] + if e.SingleLine { + var dist int + if d := e.caret.start.x.Floor() - e.scrollOff.X; d < 0 { + dist = d + } else if d := e.caret.start.x.Ceil() - (e.scrollOff.X + e.viewSize.X); d > 0 { + dist = d + } + e.scrollRel(dist, 0) + } else { + miny := e.caret.start.y - l.Ascent.Ceil() + maxy := e.caret.start.y + l.Descent.Ceil() + var dist int + if d := miny - e.scrollOff.Y; d < 0 { + dist = d + } else if d := maxy - (e.scrollOff.Y + e.viewSize.Y); d > 0 { + dist = d + } + e.scrollRel(0, dist) + } +} + +// NumLines returns the number of lines in the editor. +func (e *Editor) NumLines() int { + e.makeValid() + return len(e.lines) +} + +// SelectionLen returns the length of the selection, in bytes; it is +// equivalent to len(e.SelectedText()). +func (e *Editor) SelectionLen() int { + return abs(e.caret.start.ofs - e.caret.end.ofs) +} + +// Selection returns the start and end of the selection, as offsets into the +// editor text. start can be > end. +func (e *Editor) Selection() (start, end int) { + return e.caret.start.ofs, e.caret.end.ofs +} + +// SetCaret moves the caret to start, and sets the selection end to end. start +// and end are in bytes, and represent offsets into the editor text. start and +// end must be at a rune boundary. +func (e *Editor) SetCaret(start, end int) { + e.makeValid() + // Constrain start and end to [0, e.Len()]. + l := e.Len() + start = max(min(start, l), 0) + end = max(min(end, l), 0) + e.caret.start.ofs, e.caret.end.ofs = start, end + e.makeValidCaret() + e.caret.scroll = true + e.scroller.Stop() +} + +func (e *Editor) makeValidCaret(positions ...*combinedPos) { + // Jump through some hoops to order the offsets given to offsetToScreenPos, + // but still be able to update them correctly with the results thereof. + positions = append(positions, &e.caret.start, &e.caret.end) + sort.Slice(positions, func(i, j int) bool { + return positions[i].ofs < positions[j].ofs + }) + var iter func(offset int) combinedPos + *positions[0], iter = e.offsetToScreenPos(positions[0].ofs) + for _, cp := range positions[1:] { + *cp = iter(cp.ofs) + } +} + +// SelectedText returns the currently selected text (if any) from the editor. +func (e *Editor) SelectedText() string { + l := e.SelectionLen() + if l == 0 { + return "" + } + buf := make([]byte, l) + e.rr.Seek(int64(min(e.caret.start.ofs, e.caret.end.ofs)), io.SeekStart) + _, err := e.rr.Read(buf) + if err != nil { + // The only error that rr.Read can return is EOF, which just means no + // selection, but we've already made sure that shouldn't happen. + panic("impossible error because end is before e.rr.Len()") + } + return string(buf) +} + +func (e *Editor) updateSelection(selAct selectionAction) { + if selAct == selectionClear { + e.ClearSelection() + } +} + +// ClearSelection clears the selection, by setting the selection end equal to +// the selection start. +func (e *Editor) ClearSelection() { + e.caret.end = e.caret.start +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func abs(n int) int { + if n < 0 { + return -n + } + return n +} + +func sign(n int) int { + switch { + case n < 0: + return -1 + case n > 0: + return 1 + default: + return 0 + } +} + +// sortPoints returns a and b sorted such that a2 <= b2. +func sortPoints(a, b screenPos) (a2, b2 screenPos) { + if b.Less(a) { + return b, a + } + return a, b +} + +func nullLayout(r io.Reader) ([]text.Line, error) { + rr := bufio.NewReader(r) + var rerr error + var n int + var buf bytes.Buffer + for { + r, s, err := rr.ReadRune() + n += s + buf.WriteRune(r) + if err != nil { + rerr = err + break + } + } + return []text.Line{ + { + Layout: text.Layout{ + Text: buf.String(), + Advances: make([]fixed.Int26_6, n), + }, + }, + }, rerr +} + +func (s ChangeEvent) isEditorEvent() {} +func (s SubmitEvent) isEditorEvent() {} +func (s SelectEvent) isEditorEvent() {} diff --git a/gio/giold/widget/editor_test.go b/gio/giold/widget/editor_test.go new file mode 100644 index 0000000..6b37b50 --- /dev/null +++ b/gio/giold/widget/editor_test.go @@ -0,0 +1,540 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "fmt" + "image" + "math/rand" + "reflect" + "strings" + "testing" + "testing/quick" + "unicode" + + "realy.lol/gio/f32" + "realy.lol/gio/font/gofont" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/text" + "realy.lol/gio/unit" + "golang.org/x/image/math/fixed" +) + +func TestEditor(t *testing.T) { + e := new(Editor) + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + + e.SetCaret(0, 0) // shouldn't panic + assertCaret(t, e, 0, 0, 0) + e.SetText("Ʀbc\naĆøĆ„ā€¢") + e.Layout(gtx, cache, font, fontSize) + assertCaret(t, e, 0, 0, 0) + e.moveEnd(selectionClear) + assertCaret(t, e, 0, 3, len("Ʀbc")) + e.MoveCaret(+1, +1) + assertCaret(t, e, 1, 0, len("Ʀbc\n")) + e.MoveCaret(-1, -1) + assertCaret(t, e, 0, 3, len("Ʀbc")) + e.moveLines(+1, +1) + assertCaret(t, e, 1, 3, len("Ʀbc\naĆøĆ„")) + e.moveEnd(selectionClear) + assertCaret(t, e, 1, 4, len("Ʀbc\naĆøĆ„ā€¢")) + e.MoveCaret(+1, +1) + assertCaret(t, e, 1, 4, len("Ʀbc\naĆøĆ„ā€¢")) + + e.SetCaret(0, 0) + assertCaret(t, e, 0, 0, 0) + e.SetCaret(len("Ʀ"), len("Ʀ")) + assertCaret(t, e, 0, 1, 2) + e.SetCaret(len("Ʀbc\naĆøĆ„ā€¢"), len("Ʀbc\naĆøĆ„ā€¢")) + assertCaret(t, e, 1, 4, len("Ʀbc\naĆøĆ„ā€¢")) + + // Ensure that password masking does not affect caret behavior + e.MoveCaret(-3, -3) + assertCaret(t, e, 1, 1, len("Ʀbc\na")) + e.Mask = '*' + e.Layout(gtx, cache, font, fontSize) + assertCaret(t, e, 1, 1, len("Ʀbc\na")) + e.MoveCaret(-3, -3) + assertCaret(t, e, 0, 2, len("Ʀb")) + e.Mask = '\U0001F92B' + e.Layout(gtx, cache, font, fontSize) + e.moveEnd(selectionClear) + assertCaret(t, e, 0, 3, len("Ʀbc")) + + // When a password mask is applied, it should replace all visible glyphs + for i, line := range e.lines { + for j, r := range line.Layout.Text { + if r != e.Mask && !unicode.IsSpace(r) { + t.Errorf("glyph at (%d, %d) is unmasked rune %d", i, j, r) + } + } + } +} + +func TestEditorDimensions(t *testing.T) { + e := new(Editor) + tq := &testQueue{ + events: []event.Event{ + key.EditEvent{Text: "A"}, + }, + } + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Constraints{Max: image.Pt(100, 100)}, + Queue: tq, + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + dims := e.Layout(gtx, cache, font, fontSize) + if dims.Size.X == 0 { + t.Errorf("EditEvent was not reflected in Editor width") + } +} + +// assertCaret asserts that the editor caret is at a particular line +// and column, and that the byte position matches as well. +func assertCaret(t *testing.T, e *Editor, line, col, bytes int) { + t.Helper() + gotLine, gotCol := e.CaretPos() + if gotLine != line || gotCol != col { + t.Errorf("caret at (%d, %d), expected (%d, %d)", gotLine, gotCol, line, + col) + } + if bytes != e.caret.start.ofs { + t.Errorf("caret at buffer position %d, expected %d", e.caret.start.ofs, + bytes) + } +} + +type editMutation int + +const ( + setText editMutation = iota + moveRune + moveLine + movePage + moveStart + moveEnd + moveCoord + moveWord + deleteWord + moveLast // Mark end; never generated. +) + +func TestEditorCaretConsistency(t *testing.T) { + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + for _, a := range []text.Alignment{text.Start, text.Middle, text.End} { + e := &Editor{ + Alignment: a, + } + e.Layout(gtx, cache, font, fontSize) + + consistent := func() error { + t.Helper() + gotLine, gotCol := e.CaretPos() + gotCoords := e.CaretCoords() + want, _ := e.offsetToScreenPos(e.caret.start.ofs) + wantCoords := f32.Pt(float32(want.x)/64, float32(want.y)) + if want.lineCol.Y == gotLine && want.lineCol.X == gotCol && gotCoords == wantCoords { + return nil + } + return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s", + gotLine, gotCol, gotCoords, want.lineCol.Y, want.lineCol.X, + wantCoords) + } + if err := consistent(); err != nil { + t.Errorf("initial editor inconsistency (alignment %s): %v", a, err) + } + + move := func(mutation editMutation, str string, distance int8, + x, y uint16) bool { + switch mutation { + case setText: + e.SetText(str) + e.Layout(gtx, cache, font, fontSize) + case moveRune: + e.MoveCaret(int(distance), int(distance)) + case moveLine: + e.moveLines(int(distance), selectionClear) + case movePage: + e.movePages(int(distance), selectionClear) + case moveStart: + e.moveStart(selectionClear) + case moveEnd: + e.moveEnd(selectionClear) + case moveCoord: + e.moveCoord(image.Pt(int(x), int(y))) + case moveWord: + e.moveWord(int(distance), selectionClear) + case deleteWord: + e.deleteWord(int(distance)) + default: + return false + } + if err := consistent(); err != nil { + t.Error(err) + return false + } + return true + } + if err := quick.Check(move, nil); err != nil { + t.Errorf("editor inconsistency (alignment %s): %v", a, err) + } + } +} + +func TestEditorMoveWord(t *testing.T) { + type Test struct { + Text string + Start int + Skip int + Want int + } + tests := []Test{ + {"", 0, 0, 0}, + {"", 0, -1, 0}, + {"", 0, 1, 0}, + {"hello", 0, -1, 0}, + {"hello", 0, 1, 5}, + {"hello world", 3, 1, 5}, + {"hello world", 3, -1, 0}, + {"hello world", 8, -1, 6}, + {"hello world", 8, 1, 11}, + {"hello world", 3, 1, 5}, + {"hello world", 3, 2, 14}, + {"hello world", 8, 1, 14}, + {"hello world", 8, -1, 0}, + {"hello brave new world", 0, 3, 15}, + } + setup := func(t string) *Editor { + e := new(Editor) + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + e.SetText(t) + e.Layout(gtx, cache, font, fontSize) + return e + } + for ii, tt := range tests { + e := setup(tt.Text) + e.MoveCaret(tt.Start, tt.Start) + e.moveWord(tt.Skip, selectionClear) + if e.caret.start.ofs != tt.Want { + t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, + e.caret.start.ofs, tt.Want) + } + } +} + +func TestEditorDeleteWord(t *testing.T) { + type Test struct { + Text string + Start int + Selection int + Delete int + + Want int + Result string + } + tests := []Test{ + // No text selected + {"", 0, 0, 0, 0, ""}, + {"", 0, 0, -1, 0, ""}, + {"", 0, 0, 1, 0, ""}, + {"", 0, 0, -2, 0, ""}, + {"", 0, 0, 2, 0, ""}, + {"hello", 0, 0, -1, 0, "hello"}, + {"hello", 0, 0, 1, 0, ""}, + + // Document (imho) incorrect behavior w.r.t. deleting spaces following + // words. + {"hello world", 0, 0, 1, 0, + " world"}, // Should be "world", if you ask me. + {"hello world", 0, 0, 2, 0, "world"}, // Should be "". + {"hello ", 0, 0, 1, 0, " "}, // Should be "". + {"hello world", 11, 0, -1, 6, "hello "}, // Should be "hello". + {"hello world", 11, 0, -2, 5, "hello"}, // Should be "". + {"hello ", 6, 0, -1, 0, ""}, // Correct result. + + {"hello world", 3, 0, 1, 3, "hel world"}, + {"hello world", 3, 0, -1, 0, "lo world"}, + {"hello world", 8, 0, -1, 6, "hello rld"}, + {"hello world", 8, 0, 1, 8, "hello wo"}, + {"hello world", 3, 0, 1, 3, "hel world"}, + {"hello world", 3, 0, 2, 3, "helworld"}, + {"hello world", 8, 0, 1, 8, "hello "}, + {"hello world", 8, 0, -1, 5, "hello world"}, + {"hello brave new world", 0, 0, 3, 0, " new world"}, + // Add selected text. + // + // Several permutations must be tested: + // - select from the left or right + // - Delete + or - + // - abs(Delete) == 1 or > 1 + // + // "brave |" selected; caret at | + {"hello there brave new world", 12, 6, 1, 12, + "hello there new world"}, // #16 + {"hello there brave new world", 12, 6, 2, 12, + "hello there world"}, // The two spaces after "there" are actually suboptimal, if you ask me. See also above cases. + {"hello there brave new world", 12, 6, -1, 12, "hello there new world"}, + {"hello there brave new world", 12, 6, -2, 6, "hello new world"}, + // "|brave " selected + {"hello there brave new world", 18, -6, 1, 12, + "hello there new world"}, // #20 + {"hello there brave new world", 18, -6, 2, 12, + "hello there world"}, // ditto + {"hello there brave new world", 18, -6, -1, 12, + "hello there new world"}, + {"hello there brave new world", 18, -6, -2, 6, "hello new world"}, + // Random edge cases + {"hello there brave new world", 12, 6, 99, 12, "hello there "}, + {"hello there brave new world", 18, -6, -99, 0, "new world"}, + } + setup := func(t string) *Editor { + e := new(Editor) + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + e.SetText(t) + e.Layout(gtx, cache, font, fontSize) + return e + } + for ii, tt := range tests { + e := setup(tt.Text) + e.MoveCaret(tt.Start, tt.Start) + e.MoveCaret(0, tt.Selection) + e.deleteWord(tt.Delete) + if e.caret.start.ofs != tt.Want { + t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, + e.caret.start.ofs, tt.Want) + } + if e.Text() != tt.Result { + t.Fatalf("[%d] deleteWord: invalid result: got %q, want %q", ii, + e.Text(), tt.Result) + } + } +} + +func TestEditorNoLayout(t *testing.T) { + var e Editor + e.SetText("hi!\n") + e.MoveCaret(1, 1) +} + +// Generate generates a value of itself, for testing/quick. +func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value { + t := editMutation(rand.Intn(int(moveLast))) + return reflect.ValueOf(t) +} + +// TestSelect tests the selection code. It lays out an editor with several +// lines in it, selects some text, verifies the selection, resizes the editor +// to make it much narrower (which makes the lines in the editor reflow), and +// then verifies that the updated (col, line) positions of the selected text +// are where we expect. +func TestSelect(t *testing.T) { + e := new(Editor) + e.SetText(`a123456789a +b123456789b +c123456789c +d123456789d +e123456789e +f123456789f +g123456789g +`) + + gtx := layout.Context{Ops: new(op.Ops)} + cache := text.NewCache(gofont.Collection()) + font := text.Font{} + fontSize := unit.Px(10) + + selected := func(start, end int) string { + // Layout once with no events; populate e.lines. + gtx.Queue = nil + e.Layout(gtx, cache, font, fontSize) + _ = e.Events() // throw away any events from this layout + + // Build the selection events + startPos, endPos := e.offsetToScreenPos2(sortInts(start, end)) + tq := &testQueue{ + events: []event.Event{ + pointer.Event{ + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Source: pointer.Mouse, + Position: f32.Pt(textWidth(e, startPos.lineCol.Y, 0, + startPos.lineCol.X), textHeight(e, startPos.lineCol.Y)), + }, + pointer.Event{ + Type: pointer.Release, + Source: pointer.Mouse, + Position: f32.Pt(textWidth(e, endPos.lineCol.Y, 0, + endPos.lineCol.X), textHeight(e, endPos.lineCol.Y)), + }, + }, + } + gtx.Queue = tq + + e.Layout(gtx, cache, font, fontSize) + for _, evt := range e.Events() { + switch evt.(type) { + case SelectEvent: + return e.SelectedText() + } + } + return "" + } + + type testCase struct { + // input text offsets + start, end int + + // expected selected text + selection string + // expected line/col positions of selection after resize + startPos, endPos screenPos + } + + for n, tst := range []testCase{ + {0, 1, "a", screenPos{}, screenPos{Y: 0, X: 1}}, + {0, 4, "a123", screenPos{}, screenPos{Y: 0, X: 4}}, + {0, 11, "a123456789a", screenPos{}, screenPos{Y: 1, X: 5}}, + {2, 6, "2345", screenPos{Y: 0, X: 2}, screenPos{Y: 1, X: 0}}, + {41, 66, "56789d\ne123456789e\nf12345", screenPos{Y: 6, X: 5}, + screenPos{Y: 11, X: 0}}, + } { + // printLines(e) + + gtx.Constraints = layout.Exact(image.Pt(100, 100)) + if got := selected(tst.start, tst.end); got != tst.selection { + t.Errorf("Test %d pt1: Expected %q, got %q", n, tst.selection, got) + continue + } + + // Constrain the editor to roughly 6 columns wide and redraw + gtx.Constraints = layout.Exact(image.Pt(36, 36)) + // Keep existing selection + gtx.Queue = nil + e.Layout(gtx, cache, font, fontSize) + + if e.caret.end.lineCol != tst.startPos || e.caret.start.lineCol != tst.endPos { + t.Errorf("Test %d pt2: Expected %#v, %#v; got %#v, %#v", + n, + e.caret.end.lineCol, e.caret.start.lineCol, + tst.startPos, tst.endPos) + continue + } + + // printLines(e) + } +} + +// Verify that an existing selection is dismissed when you press arrow keys. +func TestSelectMove(t *testing.T) { + e := new(Editor) + e.SetText(`0123456789`) + + gtx := layout.Context{Ops: new(op.Ops)} + cache := text.NewCache(gofont.Collection()) + font := text.Font{} + fontSize := unit.Px(10) + + // Layout once to populate e.lines and get focus. + gtx.Queue = newQueue(key.FocusEvent{Focus: true}) + e.Layout(gtx, cache, font, fontSize) + + testKey := func(keyName string) { + // Select 345 + e.SetCaret(3, 6) + if expected, got := "345", e.SelectedText(); expected != got { + t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got) + } + + // Press the key + gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName}) + e.Layout(gtx, cache, font, fontSize) + + if expected, got := "", e.SelectedText(); expected != got { + t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got) + } + } + + testKey(key.NameLeftArrow) + testKey(key.NameRightArrow) + testKey(key.NameUpArrow) + testKey(key.NameDownArrow) +} + +func textWidth(e *Editor, lineNum, colStart, colEnd int) float32 { + var w fixed.Int26_6 + advances := e.lines[lineNum].Layout.Advances + if colEnd > len(advances) { + colEnd = len(advances) + } + for _, adv := range advances[colStart:colEnd] { + w += adv + } + return float32(w.Floor()) +} + +func textHeight(e *Editor, lineNum int) float32 { + var h fixed.Int26_6 + for _, line := range e.lines[0:lineNum] { + h += line.Ascent + line.Descent + } + return float32(h.Floor() + 1) +} + +type testQueue struct { + events []event.Event +} + +func newQueue(e ...event.Event) *testQueue { + return &testQueue{events: e} +} + +func (q *testQueue) Events(_ event.Tag) []event.Event { + return q.events +} + +func printLines(e *Editor) { + for n, line := range e.lines { + text := strings.TrimSuffix(line.Layout.Text, "\n") + fmt.Printf("%d: %s\n", n, text) + } +} + +// sortInts returns a and b sorted such that a2 <= b2. +func sortInts(a, b int) (a2, b2 int) { + if b < a { + return b, a + } + return a, b +} diff --git a/gio/giold/widget/enum.go b/gio/giold/widget/enum.go new file mode 100644 index 0000000..1ef721a --- /dev/null +++ b/gio/giold/widget/enum.go @@ -0,0 +1,77 @@ +package widget + +import ( + "image" + + "realy.lol/gio/gesture" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" +) + +type Enum struct { + Value string + hovered string + hovering bool + + changed bool + + clicks []gesture.Click + values []string +} + +func index(vs []string, t string) int { + for i, v := range vs { + if v == t { + return i + } + } + return -1 +} + +// Changed reports whether Value has changed by user interaction since the last +// call to Changed. +func (e *Enum) Changed() bool { + changed := e.changed + e.changed = false + return changed +} + +// Hovered returns the key that is highlighted, or false if none are. +func (e *Enum) Hovered() (string, bool) { + return e.hovered, e.hovering +} + +// Layout adds the event handler for key. +func (e *Enum) Layout(gtx layout.Context, key string) layout.Dimensions { + defer op.Save(gtx.Ops).Load() + pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops) + + if index(e.values, key) == -1 { + e.values = append(e.values, key) + e.clicks = append(e.clicks, gesture.Click{}) + e.clicks[len(e.clicks)-1].Add(gtx.Ops) + } else { + idx := index(e.values, key) + clk := &e.clicks[idx] + for _, ev := range clk.Events(gtx) { + switch ev.Type { + case gesture.TypeClick: + if new := e.values[idx]; new != e.Value { + e.Value = new + e.changed = true + } + } + } + if e.hovering && e.hovered == key { + e.hovering = false + } + if clk.Hovered() { + e.hovered = key + e.hovering = true + } + clk.Add(gtx.Ops) + } + + return layout.Dimensions{Size: gtx.Constraints.Min} +} diff --git a/gio/giold/widget/example_test.go b/gio/giold/widget/example_test.go new file mode 100644 index 0000000..f5e9cf5 --- /dev/null +++ b/gio/giold/widget/example_test.go @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget_test + +import ( + "fmt" + "image" + + "realy.lol/gio/f32" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/router" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/widget" +) + +func ExampleClickable_passthrough() { + // When laying out clickable widgets on top of each other, + // pointer events can be passed down for the underlying + // widgets to pick them up. + var button1, button2 widget.Clickable + var r router.Router + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + Queue: &r, + } + + // widget lays out two buttons on top of each other. + widget := func() { + // button2 completely covers button1, but PassOp allows pointer + // events to pass through to button1. + button1.Layout(gtx) + // PassOp is applied to the area defined by button1. + pointer.PassOp{Pass: true}.Add(gtx.Ops) + button2.Layout(gtx) + } + + // The first layout and call to Frame declare the Clickable handlers + // to the input router, so the following pointer events are propagated. + widget() + r.Frame(gtx.Ops) + // Simulate one click on the buttons by sending a Press and Release event. + r.Queue( + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Release, + Position: f32.Pt(50, 50), + }, + ) + // The second layout ensures that the click event is registered by the buttons. + widget() + + if button1.Clicked() { + fmt.Println("button1 clicked!") + } + if button2.Clicked() { + fmt.Println("button2 clicked!") + } + + // Output: + // button1 clicked! + // button2 clicked! +} diff --git a/gio/giold/widget/fit.go b/gio/giold/widget/fit.go new file mode 100644 index 0000000..08adb74 --- /dev/null +++ b/gio/giold/widget/fit.go @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + + "realy.lol/gio/f32" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" +) + +// Fit scales a widget to fit and clip to the constraints. +type Fit uint8 + +const ( + // Unscaled does not alter the scale of a widget. + Unscaled Fit = iota + // Contain scales widget as large as possible without cropping + // and it preserves aspect-ratio. + Contain + // Cover scales the widget to cover the constraint area and + // preserves aspect-ratio. + Cover + // ScaleDown scales the widget smaller without cropping, + // when it exceeds the constraint area. + // It preserves aspect-ratio. + ScaleDown + // Fill stretches the widget to the constraints and does not + // preserve aspect-ratio. + Fill +) + +// scale adds clip and scale operations to fit dims to the constraints. +// It positions the widget to the appropriate position. +// It returns dimensions modified accordingly. +func (fit Fit) scale(gtx layout.Context, pos layout.Direction, + dims layout.Dimensions) layout.Dimensions { + widgetSize := dims.Size + + if fit == Unscaled || dims.Size.X == 0 || dims.Size.Y == 0 { + dims.Size = gtx.Constraints.Constrain(dims.Size) + clip.Rect{Max: dims.Size}.Add(gtx.Ops) + + offset := pos.Position(widgetSize, dims.Size) + op.Offset(layout.FPt(offset)).Add(gtx.Ops) + dims.Baseline += offset.Y + return dims + } + + scale := f32.Point{ + X: float32(gtx.Constraints.Max.X) / float32(dims.Size.X), + Y: float32(gtx.Constraints.Max.Y) / float32(dims.Size.Y), + } + + switch fit { + case Contain: + if scale.Y < scale.X { + scale.X = scale.Y + } else { + scale.Y = scale.X + } + case Cover: + if scale.Y > scale.X { + scale.X = scale.Y + } else { + scale.Y = scale.X + } + case ScaleDown: + if scale.Y < scale.X { + scale.X = scale.Y + } else { + scale.Y = scale.X + } + + // The widget would need to be scaled up, no change needed. + if scale.X >= 1 { + dims.Size = gtx.Constraints.Constrain(dims.Size) + clip.Rect{Max: dims.Size}.Add(gtx.Ops) + + offset := pos.Position(widgetSize, dims.Size) + op.Offset(layout.FPt(offset)).Add(gtx.Ops) + dims.Baseline += offset.Y + return dims + } + case Fill: + } + + var scaledSize image.Point + scaledSize.X = int(float32(widgetSize.X) * scale.X) + scaledSize.Y = int(float32(widgetSize.Y) * scale.Y) + dims.Size = gtx.Constraints.Constrain(scaledSize) + dims.Baseline = int(float32(dims.Baseline) * scale.Y) + + clip.Rect{Max: dims.Size}.Add(gtx.Ops) + + offset := pos.Position(scaledSize, dims.Size) + op.Affine(f32.Affine2D{}. + Scale(f32.Point{}, scale). + Offset(layout.FPt(offset)), + ).Add(gtx.Ops) + + dims.Baseline += offset.Y + + return dims +} diff --git a/gio/giold/widget/fit_test.go b/gio/giold/widget/fit_test.go new file mode 100644 index 0000000..925ad34 --- /dev/null +++ b/gio/giold/widget/fit_test.go @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "bytes" + "encoding/binary" + "image" + "math" + "testing" + + "realy.lol/gio/f32" + "realy.lol/gio/layout" + "realy.lol/gio/op" +) + +func TestFit(t *testing.T) { + type test struct { + Dims image.Point + Scale f32.Point + Result image.Point + } + + fittests := [...][]test{ + Unscaled: { + { + Dims: image.Point{0, 0}, + Scale: f32.Point{X: 1, Y: 1}, + Result: image.Point{X: 0, Y: 0}, + }, { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 1, Y: 1}, + Result: image.Point{X: 50, Y: 25}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 1, Y: 1}, + Result: image.Point{X: 50, Y: 100}, + }}, + Contain: { + { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 2, Y: 2}, + Result: image.Point{X: 100, Y: 50}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 0.5, Y: 0.5}, + Result: image.Point{X: 25, Y: 100}, + }}, + Cover: { + { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 4, Y: 4}, + Result: image.Point{X: 100, Y: 100}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 2, Y: 2}, + Result: image.Point{X: 100, Y: 100}, + }}, + ScaleDown: { + { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 1, Y: 1}, + Result: image.Point{X: 50, Y: 25}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 0.5, Y: 0.5}, + Result: image.Point{X: 25, Y: 100}, + }}, + Fill: { + { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 2, Y: 4}, + Result: image.Point{X: 100, Y: 100}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 2, Y: 0.5}, + Result: image.Point{X: 100, Y: 100}, + }}, + } + + for fit, tests := range fittests { + fit := Fit(fit) + for i, test := range tests { + ops := new(op.Ops) + gtx := layout.Context{ + Ops: ops, + Constraints: layout.Constraints{ + Max: image.Point{X: 100, Y: 100}, + }, + } + + result := fit.scale(gtx, layout.NW, + layout.Dimensions{Size: test.Dims}) + + if test.Scale.X != 1 || test.Scale.Y != 1 { + opsdata := gtx.Ops.Data() + scaleX := float32Bytes(test.Scale.X) + scaleY := float32Bytes(test.Scale.Y) + if !bytes.Contains(opsdata, scaleX) { + t.Errorf("did not find scale.X:%v (%x) in ops: %x", + test.Scale.X, scaleX, opsdata) + } + if !bytes.Contains(opsdata, scaleY) { + t.Errorf("did not find scale.Y:%v (%x) in ops: %x", + test.Scale.Y, scaleY, opsdata) + } + } + + if result.Size != test.Result { + t.Errorf("fit %v, #%v: expected %#v, got %#v", fit, i, + test.Result, result.Size) + } + } + } +} + +func float32Bytes(v float32) []byte { + var dst [4]byte + binary.LittleEndian.PutUint32(dst[:], math.Float32bits(v)) + return dst[:] +} diff --git a/gio/giold/widget/float.go b/gio/giold/widget/float.go new file mode 100644 index 0000000..e26e296 --- /dev/null +++ b/gio/giold/widget/float.go @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + + "realy.lol/gio/gesture" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" +) + +// Float is for selecting a value in a range. +type Float struct { + Value float32 + Axis layout.Axis + + drag gesture.Drag + pos float32 // position normalized to [0, 1] + length float32 + changed bool +} + +// Dragging returns whether the value is being interacted with. +func (f *Float) Dragging() bool { return f.drag.Dragging() } + +// Layout updates the value according to drag events along the f's main axis. +// +// The range of f is set by the minimum constraints main axis value. +func (f *Float) Layout(gtx layout.Context, pointerMargin int, + min, max float32) layout.Dimensions { + size := gtx.Constraints.Min + f.length = float32(f.Axis.Convert(size).X) + + var de *pointer.Event + for _, e := range f.drag.Events(gtx.Metric, gtx, gesture.Axis(f.Axis)) { + if e.Type == pointer.Press || e.Type == pointer.Drag { + de = &e + } + } + + value := f.Value + if de != nil { + xy := de.Position.X + if f.Axis == layout.Vertical { + xy = de.Position.Y + } + f.pos = xy / f.length + value = min + (max-min)*f.pos + } else if min != max { + f.pos = (value - min) / (max - min) + } + // Unconditionally call setValue in case min, max, or value changed. + f.setValue(value, min, max) + + if f.pos < 0 { + f.pos = 0 + } else if f.pos > 1 { + f.pos = 1 + } + + defer op.Save(gtx.Ops).Load() + margin := f.Axis.Convert(image.Pt(pointerMargin, 0)) + rect := image.Rectangle{ + Min: margin.Mul(-1), + Max: size.Add(margin), + } + pointer.Rect(rect).Add(gtx.Ops) + f.drag.Add(gtx.Ops) + + return layout.Dimensions{Size: size} +} + +func (f *Float) setValue(value, min, max float32) { + if min > max { + min, max = max, min + } + if value < min { + value = min + } else if value > max { + value = max + } + if f.Value != value { + f.Value = value + f.changed = true + } +} + +// Pos reports the selected position. +func (f *Float) Pos() float32 { + return f.pos * f.length +} + +// Changed reports whether the value has changed since +// the last call to Changed. +func (f *Float) Changed() bool { + changed := f.changed + f.changed = false + return changed +} diff --git a/gio/giold/widget/icon.go b/gio/giold/widget/icon.go new file mode 100644 index 0000000..6f37d48 --- /dev/null +++ b/gio/giold/widget/icon.go @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + "image/color" + "image/draw" + + "golang.org/x/exp/shiny/iconvg" + + "realy.lol/gio/internal/f32color" + "realy.lol/gio/layout" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" +) + +type Icon struct { + Color color.NRGBA + src []byte + // Cached values. + op paint.ImageOp + imgSize int + imgColor color.NRGBA +} + +// NewIcon returns a new Icon from IconVG data. +func NewIcon(data []byte) (*Icon, error) { + _, err := iconvg.DecodeMetadata(data) + if err != nil { + return nil, err + } + return &Icon{src: data, Color: color.NRGBA{A: 0xff}}, nil +} + +func (ic *Icon) Layout(gtx layout.Context, sz unit.Value) layout.Dimensions { + ico := ic.image(gtx.Px(sz)) + ico.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + return layout.Dimensions{ + Size: ico.Size(), + } +} + +func (ic *Icon) image(sz int) paint.ImageOp { + if sz == ic.imgSize && ic.Color == ic.imgColor { + return ic.op + } + m, _ := iconvg.DecodeMetadata(ic.src) + dx, dy := m.ViewBox.AspectRatio() + img := image.NewRGBA(image.Rectangle{Max: image.Point{X: sz, + Y: int(float32(sz) * dy / dx)}}) + var ico iconvg.Rasterizer + ico.SetDstImage(img, img.Bounds(), draw.Src) + m.Palette[0] = f32color.NRGBAToLinearRGBA(ic.Color) + iconvg.Decode(&ico, ic.src, &iconvg.DecodeOptions{ + Palette: &m.Palette, + }) + ic.op = paint.NewImageOp(img) + ic.imgSize = sz + ic.imgColor = ic.Color + return ic.op +} diff --git a/gio/giold/widget/icon_test.go b/gio/giold/widget/icon_test.go new file mode 100644 index 0000000..1a3e8d9 --- /dev/null +++ b/gio/giold/widget/icon_test.go @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + "image/color" + "testing" + + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/unit" + "golang.org/x/exp/shiny/materialdesign/icons" +) + +func TestIcon_Alpha(t *testing.T) { + icon, err := NewIcon(icons.ToggleCheckBox) + if err != nil { + t.Fatal(err) + } + + icon.Color = color.NRGBA{B: 0xff, A: 0x40} + + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + + _ = icon.Layout(gtx, unit.Sp(18)) +} diff --git a/gio/giold/widget/image.go b/gio/giold/widget/image.go new file mode 100644 index 0000000..0e0351f --- /dev/null +++ b/gio/giold/widget/image.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + + "realy.lol/gio/f32" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" +) + +// Image is a widget that displays an image. +type Image struct { + // Src is the image to display. + Src paint.ImageOp + // Fit specifies how to scale the image to the constraints. + // By default it does not do any scaling. + Fit Fit + // Position specifies where to position the image within + // the constraints. + Position layout.Direction + // Scale is the ratio of image pixels to + // dps. If Scale is zero Image falls back to + // a scale that match a standard 72 DPI. + Scale float32 +} + +const defaultScale = float32(160.0 / 72.0) + +func (im Image) Layout(gtx layout.Context) layout.Dimensions { + defer op.Save(gtx.Ops).Load() + + scale := im.Scale + if scale == 0 { + scale = defaultScale + } + + size := im.Src.Size() + wf, hf := float32(size.X), float32(size.Y) + w, h := gtx.Px(unit.Dp(wf*scale)), gtx.Px(unit.Dp(hf*scale)) + + dims := im.Fit.scale(gtx, im.Position, + layout.Dimensions{Size: image.Pt(w, h)}) + + pixelScale := scale * gtx.Metric.PxPerDp + op.Affine(f32.Affine2D{}.Scale(f32.Point{}, + f32.Pt(pixelScale, pixelScale))).Add(gtx.Ops) + + im.Src.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + + return dims +} diff --git a/gio/giold/widget/image_test.go b/gio/giold/widget/image_test.go new file mode 100644 index 0000000..774dfc7 --- /dev/null +++ b/gio/giold/widget/image_test.go @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + "testing" + + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/paint" +) + +func TestImageScale(t *testing.T) { + var ops op.Ops + gtx := layout.Context{ + Ops: &ops, + Constraints: layout.Constraints{ + Max: image.Pt(50, 50), + }, + } + imgSize := image.Pt(10, 10) + img := image.NewNRGBA(image.Rectangle{Max: imgSize}) + imgOp := paint.NewImageOp(img) + + // Ensure the default scales correctly. + dims := Image{Src: imgOp}.Layout(gtx) + expectedSize := imgSize + expectedSize.X = int(float32(expectedSize.X) * defaultScale) + expectedSize.Y = int(float32(expectedSize.Y) * defaultScale) + if dims.Size != expectedSize { + t.Fatalf("non-scaled image is wrong size, expected %v, got %v", + expectedSize, dims.Size) + } + + // Ensure scaling the image via the Scale field works. + currentScale := float32(0.5) + dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) + expectedSize = imgSize + expectedSize.X = int(float32(expectedSize.X) * currentScale) + expectedSize.Y = int(float32(expectedSize.Y) * currentScale) + if dims.Size != expectedSize { + t.Fatalf(".5 scale image is wrong size, expected %v, got %v", + expectedSize, dims.Size) + } + + // Ensure the image responds to changes in DPI. + currentScale = float32(1) + gtx.Metric.PxPerDp = 2 + dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) + expectedSize = imgSize + expectedSize.X = int(float32(expectedSize.X) * currentScale * gtx.Metric.PxPerDp) + expectedSize.Y = int(float32(expectedSize.Y) * currentScale * gtx.Metric.PxPerDp) + if dims.Size != expectedSize { + t.Fatalf("HiDPI non-scaled image is wrong size, expected %v, got %v", + expectedSize, dims.Size) + } + + // Ensure scaling the image responds to changes in DPI. + currentScale = float32(.5) + gtx.Metric.PxPerDp = 2 + dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) + expectedSize = imgSize + expectedSize.X = int(float32(expectedSize.X) * currentScale * gtx.Metric.PxPerDp) + expectedSize.Y = int(float32(expectedSize.Y) * currentScale * gtx.Metric.PxPerDp) + if dims.Size != expectedSize { + t.Fatalf("HiDPI .5 scale image is wrong size, expected %v, got %v", + expectedSize, dims.Size) + } +} diff --git a/gio/giold/widget/label.go b/gio/giold/widget/label.go new file mode 100644 index 0000000..acf6b50 --- /dev/null +++ b/gio/giold/widget/label.go @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "fmt" + "image" + "unicode/utf8" + + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" + + "golang.org/x/image/math/fixed" +) + +// Label is a widget for laying out and drawing text. +type Label struct { + // Alignment specify the text alignment. + Alignment text.Alignment + // MaxLines limits the number of lines. Zero means no limit. + MaxLines int +} + +// screenPos describes a character position (in text line and column numbers, +// not pixels): Y = line number, X = rune column. +type screenPos image.Point + +type segmentIterator struct { + Lines []text.Line + Clip image.Rectangle + Alignment text.Alignment + Width int + Offset image.Point + startSel screenPos + endSel screenPos + + pos screenPos // current position + line text.Line // current line + layout text.Layout // current line's Layout + + // pixel positions + off fixed.Point26_6 + y, prevDesc fixed.Int26_6 +} + +const inf = 1e6 + +func (l *segmentIterator) Next() (text.Layout, image.Point, bool, int, + image.Point, bool) { + for l.pos.Y < len(l.Lines) { + if l.pos.X == 0 { + l.line = l.Lines[l.pos.Y] + + // Calculate X & Y pixel coordinates of left edge of line. We need y + // for the next line, so it's in l, but we only need x here, so it's + // not. + x := align(l.Alignment, l.line.Width, l.Width) + fixed.I(l.Offset.X) + l.y += l.prevDesc + l.line.Ascent + l.prevDesc = l.line.Descent + // Align baseline and line start to the pixel grid. + l.off = fixed.Point26_6{X: fixed.I(x.Floor()), + Y: fixed.I(l.y.Ceil())} + l.y = l.off.Y + l.off.Y += fixed.I(l.Offset.Y) + if (l.off.Y + l.line.Bounds.Min.Y).Floor() > l.Clip.Max.Y { + break + } + + if (l.off.Y + l.line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y { + // This line is outside/before the clip area; go on to the next line. + l.pos.Y++ + continue + } + + // Copy the line's Layout, since we slice it up later. + l.layout = l.line.Layout + + // Find the left edge of the text visible in the l.Clip clipping + // area. + for len(l.layout.Advances) > 0 { + _, n := utf8.DecodeRuneInString(l.layout.Text) + adv := l.layout.Advances[0] + if (l.off.X + adv + l.line.Bounds.Max.X - l.line.Width).Ceil() >= l.Clip.Min.X { + break + } + l.off.X += adv + l.layout.Text = l.layout.Text[n:] + l.layout.Advances = l.layout.Advances[1:] + l.pos.X++ + } + } + + selected := l.inSelection() + endx := l.off.X + rune := 0 + nextLine := true + retLayout := l.layout + for n := range l.layout.Text { + selChanged := selected != l.inSelection() + beyondClipEdge := (endx + l.line.Bounds.Min.X).Floor() > l.Clip.Max.X + if selChanged || beyondClipEdge { + retLayout.Advances = l.layout.Advances[:rune] + retLayout.Text = l.layout.Text[:n] + if selChanged { + // Save the rest of the line + l.layout.Advances = l.layout.Advances[rune:] + l.layout.Text = l.layout.Text[n:] + nextLine = false + } + break + } + endx += l.layout.Advances[rune] + rune++ + l.pos.X++ + } + offFloor := image.Point{X: l.off.X.Floor(), Y: l.off.Y.Floor()} + + // Calculate the width & height if the returned text. + // + // If there's a better way to do this, I'm all ears. + var d fixed.Int26_6 + for _, adv := range retLayout.Advances { + d += adv + } + size := image.Point{ + X: d.Ceil(), + Y: (l.line.Ascent + l.line.Descent).Ceil(), + } + + if nextLine { + l.pos.Y++ + l.pos.X = 0 + } else { + l.off.X = endx + } + + return retLayout, offFloor, selected, l.prevDesc.Ceil() - size.Y, size, true + } + return text.Layout{}, image.Point{}, false, 0, image.Point{}, false +} + +func (l *segmentIterator) inSelection() bool { + return l.startSel.LessOrEqual(l.pos) && + l.pos.Less(l.endSel) +} + +func (p1 screenPos) LessOrEqual(p2 screenPos) bool { + return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X <= p2.X) +} + +func (p1 screenPos) Less(p2 screenPos) bool { + return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X < p2.X) +} + +func (l Label) Layout(gtx layout.Context, s text.Shaper, font text.Font, + size unit.Value, txt string) layout.Dimensions { + cs := gtx.Constraints + textSize := fixed.I(gtx.Px(size)) + lines := s.LayoutString(font, textSize, cs.Max.X, txt) + if max := l.MaxLines; max > 0 && len(lines) > max { + lines = lines[:max] + } + dims := linesDimens(lines) + dims.Size = cs.Constrain(dims.Size) + cl := textPadding(lines) + cl.Max = cl.Max.Add(dims.Size) + it := segmentIterator{ + Lines: lines, + Clip: cl, + Alignment: l.Alignment, + Width: dims.Size.X, + } + for { + l, off, _, _, _, ok := it.Next() + if !ok { + break + } + stack := op.Save(gtx.Ops) + op.Offset(layout.FPt(off)).Add(gtx.Ops) + s.Shape(font, textSize, l).Add(gtx.Ops) + clip.Rect(cl.Sub(off)).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + } + return dims +} + +func textPadding(lines []text.Line) (padding image.Rectangle) { + if len(lines) == 0 { + return + } + first := lines[0] + if d := first.Ascent + first.Bounds.Min.Y; d < 0 { + padding.Min.Y = d.Ceil() + } + last := lines[len(lines)-1] + if d := last.Bounds.Max.Y - last.Descent; d > 0 { + padding.Max.Y = d.Ceil() + } + if d := first.Bounds.Min.X; d < 0 { + padding.Min.X = d.Ceil() + } + if d := first.Bounds.Max.X - first.Width; d > 0 { + padding.Max.X = d.Ceil() + } + return +} + +func linesDimens(lines []text.Line) layout.Dimensions { + var width fixed.Int26_6 + var h int + var baseline int + if len(lines) > 0 { + baseline = lines[0].Ascent.Ceil() + var prevDesc fixed.Int26_6 + for _, l := range lines { + h += (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + if l.Width > width { + width = l.Width + } + } + h += lines[len(lines)-1].Descent.Ceil() + } + w := width.Ceil() + return layout.Dimensions{ + Size: image.Point{ + X: w, + Y: h, + }, + Baseline: h - baseline, + } +} + +func align(align text.Alignment, width fixed.Int26_6, + maxWidth int) fixed.Int26_6 { + mw := fixed.I(maxWidth) + switch align { + case text.Middle: + return fixed.I(((mw - width) / 2).Floor()) + case text.End: + return fixed.I((mw - width).Floor()) + case text.Start: + return 0 + default: + panic(fmt.Errorf("unknown alignment %v", align)) + } +} diff --git a/gio/giold/widget/material/button.go b/gio/giold/widget/material/button.go new file mode 100644 index 0000000..78bfcf2 --- /dev/null +++ b/gio/giold/widget/material/button.go @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + "math" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +type ButtonStyle struct { + Text string + // Color is the text color. + Color color.NRGBA + Font text.Font + TextSize unit.Value + Background color.NRGBA + CornerRadius unit.Value + Inset layout.Inset + Button *widget.Clickable + shaper text.Shaper +} + +type ButtonLayoutStyle struct { + Background color.NRGBA + CornerRadius unit.Value + Button *widget.Clickable +} + +type IconButtonStyle struct { + Background color.NRGBA + // Color is the icon color. + Color color.NRGBA + Icon *widget.Icon + // Size is the icon size. + Size unit.Value + Inset layout.Inset + Button *widget.Clickable +} + +func Button(th *Theme, button *widget.Clickable, txt string) ButtonStyle { + return ButtonStyle{ + Text: txt, + Color: th.Palette.ContrastFg, + CornerRadius: unit.Dp(4), + Background: th.Palette.ContrastBg, + TextSize: th.TextSize.Scale(14.0 / 16.0), + Inset: layout.Inset{ + Top: unit.Dp(10), Bottom: unit.Dp(10), + Left: unit.Dp(12), Right: unit.Dp(12), + }, + Button: button, + shaper: th.Shaper, + } +} + +func ButtonLayout(th *Theme, button *widget.Clickable) ButtonLayoutStyle { + return ButtonLayoutStyle{ + Button: button, + Background: th.Palette.ContrastBg, + CornerRadius: unit.Dp(4), + } +} + +func IconButton(th *Theme, button *widget.Clickable, + icon *widget.Icon) IconButtonStyle { + return IconButtonStyle{ + Background: th.Palette.ContrastBg, + Color: th.Palette.ContrastFg, + Icon: icon, + Size: unit.Dp(24), + Inset: layout.UniformInset(unit.Dp(12)), + Button: button, + } +} + +// Clickable lays out a rectangular clickable widget without further +// decoration. +func Clickable(gtx layout.Context, button *widget.Clickable, + w layout.Widget) layout.Dimensions { + return layout.Stack{}.Layout(gtx, + layout.Expanded(button.Layout), + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + clip.Rect{Max: gtx.Constraints.Min}.Add(gtx.Ops) + for _, c := range button.History() { + drawInk(gtx, c) + } + return layout.Dimensions{Size: gtx.Constraints.Min} + }), + layout.Stacked(w), + ) +} + +func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions { + return ButtonLayoutStyle{ + Background: b.Background, + CornerRadius: b.CornerRadius, + Button: b.Button, + }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + paint.ColorOp{Color: b.Color}.Add(gtx.Ops) + return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper, + b.Font, b.TextSize, b.Text) + }) + }) +} + +func (b ButtonLayoutStyle) Layout(gtx layout.Context, + w layout.Widget) layout.Dimensions { + min := gtx.Constraints.Min + return layout.Stack{Alignment: layout.Center}.Layout(gtx, + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + rr := float32(gtx.Px(b.CornerRadius)) + clip.UniformRRect(f32.Rectangle{Max: f32.Point{ + X: float32(gtx.Constraints.Min.X), + Y: float32(gtx.Constraints.Min.Y), + }}, rr).Add(gtx.Ops) + background := b.Background + switch { + case gtx.Queue == nil: + background = f32color.Disabled(b.Background) + case b.Button.Hovered(): + background = f32color.Hovered(b.Background) + } + paint.Fill(gtx.Ops, background) + for _, c := range b.Button.History() { + drawInk(gtx, c) + } + return layout.Dimensions{Size: gtx.Constraints.Min} + }), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + gtx.Constraints.Min = min + return layout.Center.Layout(gtx, w) + }), + layout.Expanded(b.Button.Layout), + ) +} + +func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions { + return layout.Stack{Alignment: layout.Center}.Layout(gtx, + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + sizex, sizey := gtx.Constraints.Min.X, gtx.Constraints.Min.Y + sizexf, sizeyf := float32(sizex), float32(sizey) + rr := (sizexf + sizeyf) * .25 + clip.UniformRRect(f32.Rectangle{ + Max: f32.Point{X: sizexf, Y: sizeyf}, + }, rr).Add(gtx.Ops) + background := b.Background + switch { + case gtx.Queue == nil: + background = f32color.Disabled(b.Background) + case b.Button.Hovered(): + background = f32color.Hovered(b.Background) + } + paint.Fill(gtx.Ops, background) + for _, c := range b.Button.History() { + drawInk(gtx, c) + } + return layout.Dimensions{Size: gtx.Constraints.Min} + }), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + return b.Inset.Layout(gtx, + func(gtx layout.Context) layout.Dimensions { + size := gtx.Px(b.Size) + if b.Icon != nil { + b.Icon.Color = b.Color + b.Icon.Layout(gtx, unit.Px(float32(size))) + } + return layout.Dimensions{ + Size: image.Point{X: size, Y: size}, + } + }) + }), + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + pointer.Ellipse(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops) + return b.Button.Layout(gtx) + }), + ) +} + +func drawInk(gtx layout.Context, c widget.Press) { + // duration is the number of seconds for the + // completed animation: expand while fading in, then + // out. + const ( + expandDuration = float32(0.5) + fadeDuration = float32(0.9) + ) + + now := gtx.Now + + t := float32(now.Sub(c.Start).Seconds()) + + end := c.End + if end.IsZero() { + // If the press hasn't ended, don't fade-out. + end = now + } + + endt := float32(end.Sub(c.Start).Seconds()) + + // Compute the fade-in/out position in [0;1]. + var alphat float32 + { + var haste float32 + if c.Cancelled { + // If the press was cancelled before the inkwell + // was fully faded in, fast forward the animation + // to match the fade-out. + if h := 0.5 - endt/fadeDuration; h > 0 { + haste = h + } + } + // Fade in. + half1 := t/fadeDuration + haste + if half1 > 0.5 { + half1 = 0.5 + } + + // Fade out. + half2 := float32(now.Sub(end).Seconds()) + half2 /= fadeDuration + half2 += haste + if half2 > 0.5 { + // Too old. + return + } + + alphat = half1 + half2 + } + + // Compute the expand position in [0;1]. + sizet := t + if c.Cancelled { + // Freeze expansion of cancelled presses. + sizet = endt + } + sizet /= expandDuration + + // Animate only ended presses, and presses that are fading in. + if !c.End.IsZero() || sizet <= 1.0 { + op.InvalidateOp{}.Add(gtx.Ops) + } + + if sizet > 1.0 { + sizet = 1.0 + } + + if alphat > .5 { + // Start fadeout after half the animation. + alphat = 1.0 - alphat + } + // Twice the speed to attain fully faded in at 0.5. + t2 := alphat * 2 + // BeziĆ©r ease-in curve. + alphaBezier := t2 * t2 * (3.0 - 2.0*t2) + sizeBezier := sizet * sizet * (3.0 - 2.0*sizet) + size := float32(gtx.Constraints.Min.X) + if h := float32(gtx.Constraints.Min.Y); h > size { + size = h + } + // Cover the entire constraints min rectangle. + size *= 2 * float32(math.Sqrt(2)) + // Apply curve values to size and color. + size *= sizeBezier + alpha := 0.7 * alphaBezier + const col = 0.8 + ba, bc := byte(alpha*0xff), byte(col*0xff) + defer op.Save(gtx.Ops).Load() + rgba := f32color.MulAlpha(color.NRGBA{A: 0xff, R: bc, G: bc, B: bc}, ba) + ink := paint.ColorOp{Color: rgba} + ink.Add(gtx.Ops) + rr := size * .5 + op.Offset(c.Position.Add(f32.Point{ + X: -rr, + Y: -rr, + })).Add(gtx.Ops) + clip.UniformRRect(f32.Rectangle{Max: f32.Pt(size, size)}, rr).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) +} diff --git a/gio/giold/widget/material/checkable.go b/gio/giold/widget/material/checkable.go new file mode 100644 index 0000000..e895b81 --- /dev/null +++ b/gio/giold/widget/material/checkable.go @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +type checkable struct { + Label string + Color color.NRGBA + Font text.Font + TextSize unit.Value + IconColor color.NRGBA + Size unit.Value + shaper text.Shaper + checkedStateIcon *widget.Icon + uncheckedStateIcon *widget.Icon +} + +func (c *checkable) layout(gtx layout.Context, + checked, hovered bool) layout.Dimensions { + var icon *widget.Icon + if checked { + icon = c.checkedStateIcon + } else { + icon = c.uncheckedStateIcon + } + + dims := layout.Flex{Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Stack{Alignment: layout.Center}.Layout(gtx, + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + size := gtx.Px(c.Size) * 4 / 3 + dims := layout.Dimensions{ + Size: image.Point{X: size, Y: size}, + } + if !hovered { + return dims + } + + background := f32color.MulAlpha(c.IconColor, 70) + + radius := float32(size) / 2 + paint.FillShape(gtx.Ops, background, + clip.Circle{ + Center: f32.Point{X: radius, Y: radius}, + Radius: radius, + }.Op(gtx.Ops)) + + return dims + }), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(2)).Layout(gtx, + func(gtx layout.Context) layout.Dimensions { + size := gtx.Px(c.Size) + icon.Color = c.IconColor + if gtx.Queue == nil { + icon.Color = f32color.Disabled(icon.Color) + } + icon.Layout(gtx, unit.Px(float32(size))) + return layout.Dimensions{ + Size: image.Point{X: size, Y: size}, + } + }) + }), + ) + }), + + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(2)).Layout(gtx, + func(gtx layout.Context) layout.Dimensions { + paint.ColorOp{Color: c.Color}.Add(gtx.Ops) + return widget.Label{}.Layout(gtx, c.shaper, c.Font, + c.TextSize, c.Label) + }) + }), + ) + pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops) + return dims +} diff --git a/gio/giold/widget/material/checkbox.go b/gio/giold/widget/material/checkbox.go new file mode 100644 index 0000000..2483cfe --- /dev/null +++ b/gio/giold/widget/material/checkbox.go @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "realy.lol/gio/layout" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +type CheckBoxStyle struct { + checkable + CheckBox *widget.Bool +} + +func CheckBox(th *Theme, checkBox *widget.Bool, label string) CheckBoxStyle { + return CheckBoxStyle{ + CheckBox: checkBox, + checkable: checkable{ + Label: label, + Color: th.Palette.Fg, + IconColor: th.Palette.ContrastBg, + TextSize: th.TextSize.Scale(14.0 / 16.0), + Size: unit.Dp(26), + shaper: th.Shaper, + checkedStateIcon: th.Icon.CheckBoxChecked, + uncheckedStateIcon: th.Icon.CheckBoxUnchecked, + }, + } +} + +// Layout updates the checkBox and displays it. +func (c CheckBoxStyle) Layout(gtx layout.Context) layout.Dimensions { + dims := c.layout(gtx, c.CheckBox.Value, c.CheckBox.Hovered()) + gtx.Constraints.Min = dims.Size + c.CheckBox.Layout(gtx) + return dims +} diff --git a/gio/giold/widget/material/doc.go b/gio/giold/widget/material/doc.go new file mode 100644 index 0000000..715f5a0 --- /dev/null +++ b/gio/giold/widget/material/doc.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package material implements the Material design. +// +// To maximize reusability and visual flexibility, user interface controls are +// split into two parts: the stateful widget and the stateless drawing of it. +// +// For example, widget.Clickable encapsulates the state and event +// handling of all clickable areas, while the Theme is responsible to +// draw a specific area, for example a button. +// +// This snippet defines a button that prints a message when clicked: +// +// var gtx layout.Context +// button := new(widget.Clickable) +// +// for button.Clicked(gtx) { +// fmt.Println("Clicked!") +// } +// +// Use a Theme to draw the button: +// +// theme := material.NewTheme(...) +// +// material.Button(theme, "Click me!").Layout(gtx, button) +// +// Customization +// +// Quite often, a program needs to customize the theme-provided defaults. Several +// options are available, depending on the nature of the change. +// +// Mandatory parameters: Some parameters are not part of the widget state but +// have no obvious default. In the program above, the button text is a +// parameter to the Theme.Button method. +// +// Theme-global parameters: For changing the look of all widgets drawn with a +// particular theme, adjust the `Theme` fields: +// +// theme.Color.Primary = color.NRGBA{...} +// +// Widget-local parameters: For changing the look of a particular widget, +// adjust the widget specific theme object: +// +// btn := material.Button(theme, "Click me!") +// btn.Font.Style = text.Italic +// btn.Layout(gtx, button) +// +// Widget variants: A widget can have several distinct representations even +// though the underlying state is the same. A widget.Clickable can be drawn as a +// round icon button: +// +// icon := material.NewIcon(...) +// +// material.IconButton(theme, icon).Layout(gtx, button) +// +// Specialized widgets: Theme both define a generic Label method +// that takes a text size, and specialized methods for standard text +// sizes such as Theme.H1 and Theme.Body2. +package material diff --git a/gio/giold/widget/material/editor.go b/gio/giold/widget/material/editor.go new file mode 100644 index 0000000..93d02cf --- /dev/null +++ b/gio/giold/widget/material/editor.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image/color" + + "realy.lol/gio/internal/f32color" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +type EditorStyle struct { + Font text.Font + TextSize unit.Value + // Color is the text color. + Color color.NRGBA + // Hint contains the text displayed when the editor is empty. + Hint string + // HintColor is the color of hint text. + HintColor color.NRGBA + // SelectionColor is the color of the background for selected text. + SelectionColor color.NRGBA + Editor *widget.Editor + + shaper text.Shaper +} + +func Editor(th *Theme, editor *widget.Editor, hint string) EditorStyle { + return EditorStyle{ + Editor: editor, + TextSize: th.TextSize, + Color: th.Palette.Fg, + shaper: th.Shaper, + Hint: hint, + HintColor: f32color.MulAlpha(th.Palette.Fg, 0xbb), + SelectionColor: f32color.MulAlpha(th.Palette.ContrastBg, 0x60), + } +} + +func (e EditorStyle) Layout(gtx layout.Context) layout.Dimensions { + defer op.Save(gtx.Ops).Load() + macro := op.Record(gtx.Ops) + paint.ColorOp{Color: e.HintColor}.Add(gtx.Ops) + var maxlines int + if e.Editor.SingleLine { + maxlines = 1 + } + tl := widget.Label{Alignment: e.Editor.Alignment, MaxLines: maxlines} + dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint) + call := macro.Stop() + if w := dims.Size.X; gtx.Constraints.Min.X < w { + gtx.Constraints.Min.X = w + } + if h := dims.Size.Y; gtx.Constraints.Min.Y < h { + gtx.Constraints.Min.Y = h + } + dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize) + disabled := gtx.Queue == nil + if e.Editor.Len() > 0 { + paint.ColorOp{Color: blendDisabledColor(disabled, + e.SelectionColor)}.Add(gtx.Ops) + e.Editor.PaintSelection(gtx) + paint.ColorOp{Color: blendDisabledColor(disabled, e.Color)}.Add(gtx.Ops) + e.Editor.PaintText(gtx) + } else { + call.Add(gtx.Ops) + } + if !disabled { + paint.ColorOp{Color: e.Color}.Add(gtx.Ops) + e.Editor.PaintCaret(gtx) + } + return dims +} + +func blendDisabledColor(disabled bool, c color.NRGBA) color.NRGBA { + if disabled { + return f32color.Disabled(c) + } + return c +} diff --git a/gio/giold/widget/material/label.go b/gio/giold/widget/material/label.go new file mode 100644 index 0000000..80c4b02 --- /dev/null +++ b/gio/giold/widget/material/label.go @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image/color" + + "realy.lol/gio/layout" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +type LabelStyle struct { + // Face defines the text style. + Font text.Font + // Color is the text color. + Color color.NRGBA + // Alignment specify the text alignment. + Alignment text.Alignment + // MaxLines limits the number of lines. Zero means no limit. + MaxLines int + Text string + TextSize unit.Value + + shaper text.Shaper +} + +func H1(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(96.0/16.0), txt) +} + +func H2(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(60.0/16.0), txt) +} + +func H3(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(48.0/16.0), txt) +} + +func H4(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(34.0/16.0), txt) +} + +func H5(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(24.0/16.0), txt) +} + +func H6(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(20.0/16.0), txt) +} + +func Body1(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize, txt) +} + +func Body2(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(14.0/16.0), txt) +} + +func Caption(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(12.0/16.0), txt) +} + +func Label(th *Theme, size unit.Value, txt string) LabelStyle { + return LabelStyle{ + Text: txt, + Color: th.Palette.Fg, + TextSize: size, + shaper: th.Shaper, + } +} + +func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions { + paint.ColorOp{Color: l.Color}.Add(gtx.Ops) + tl := widget.Label{Alignment: l.Alignment, MaxLines: l.MaxLines} + return tl.Layout(gtx, l.shaper, l.Font, l.TextSize, l.Text) +} diff --git a/gio/giold/widget/material/loader.go b/gio/giold/widget/material/loader.go new file mode 100644 index 0000000..77afede --- /dev/null +++ b/gio/giold/widget/material/loader.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + "math" + "time" + + "realy.lol/gio/f32" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" +) + +type LoaderStyle struct { + Color color.NRGBA +} + +func Loader(th *Theme) LoaderStyle { + return LoaderStyle{ + Color: th.Palette.ContrastBg, + } +} + +func (l LoaderStyle) Layout(gtx layout.Context) layout.Dimensions { + diam := gtx.Constraints.Min.X + if minY := gtx.Constraints.Min.Y; minY > diam { + diam = minY + } + if diam == 0 { + diam = gtx.Px(unit.Dp(24)) + } + sz := gtx.Constraints.Constrain(image.Pt(diam, diam)) + radius := float64(sz.X) * .5 + defer op.Save(gtx.Ops).Load() + op.Offset(f32.Pt(float32(radius), float32(radius))).Add(gtx.Ops) + + dt := (time.Duration(gtx.Now.UnixNano()) % (time.Second)).Seconds() + startAngle := dt * math.Pi * 2 + endAngle := startAngle + math.Pi*1.5 + + clipLoader(gtx.Ops, startAngle, endAngle, radius) + paint.ColorOp{ + Color: l.Color, + }.Add(gtx.Ops) + op.Offset(f32.Pt(-float32(radius), -float32(radius))).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + op.InvalidateOp{}.Add(gtx.Ops) + return layout.Dimensions{ + Size: sz, + } +} + +func clipLoader(ops *op.Ops, startAngle, endAngle, radius float64) { + const thickness = .25 + + var ( + width = float32(radius * thickness) + delta = float32(endAngle - startAngle) + + vy, vx = math.Sincos(startAngle) + + pen = f32.Pt(float32(vx), float32(vy)).Mul(float32(radius)) + center = f32.Pt(0, 0).Sub(pen) + + p clip.Path + ) + + p.Begin(ops) + p.Move(pen) + p.Arc(center, center, delta) + clip.Stroke{ + Path: p.End(), + Style: clip.StrokeStyle{ + Width: width, + Cap: clip.FlatCap, + }, + }.Op().Add(ops) +} diff --git a/gio/giold/widget/material/progressbar.go b/gio/giold/widget/material/progressbar.go new file mode 100644 index 0000000..98ae4cf --- /dev/null +++ b/gio/giold/widget/material/progressbar.go @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/layout" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" +) + +type ProgressBarStyle struct { + Color color.NRGBA + TrackColor color.NRGBA + Progress float32 +} + +func ProgressBar(th *Theme, progress float32) ProgressBarStyle { + return ProgressBarStyle{ + Progress: progress, + Color: th.Palette.ContrastBg, + TrackColor: f32color.MulAlpha(th.Palette.Fg, 0x88), + } +} + +func (p ProgressBarStyle) Layout(gtx layout.Context) layout.Dimensions { + shader := func(width float32, color color.NRGBA) layout.Dimensions { + maxHeight := unit.Dp(4) + rr := float32(gtx.Px(unit.Dp(2))) + + d := image.Point{X: int(width), Y: gtx.Px(maxHeight)} + + height := float32(gtx.Px(maxHeight)) + clip.UniformRRect(f32.Rectangle{Max: f32.Pt(width, height)}, + rr).Add(gtx.Ops) + paint.ColorOp{Color: color}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + + return layout.Dimensions{Size: d} + } + + progressBarWidth := float32(gtx.Constraints.Max.X) + return layout.Stack{Alignment: layout.W}.Layout(gtx, + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + return shader(progressBarWidth, p.TrackColor) + }), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + fillWidth := progressBarWidth * clamp1(p.Progress) + fillColor := p.Color + if gtx.Queue == nil { + fillColor = f32color.Disabled(fillColor) + } + return shader(fillWidth, fillColor) + }), + ) +} + +// clamp1 limits v to range [0..1]. +func clamp1(v float32) float32 { + if v >= 1 { + return 1 + } else if v <= 0 { + return 0 + } else { + return v + } +} diff --git a/gio/giold/widget/material/radiobutton.go b/gio/giold/widget/material/radiobutton.go new file mode 100644 index 0000000..79dd763 --- /dev/null +++ b/gio/giold/widget/material/radiobutton.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "realy.lol/gio/layout" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +type RadioButtonStyle struct { + checkable + Key string + Group *widget.Enum +} + +// RadioButton returns a RadioButton with a label. The key specifies +// the value for the Enum. +func RadioButton(th *Theme, group *widget.Enum, + key, label string) RadioButtonStyle { + return RadioButtonStyle{ + Group: group, + checkable: checkable{ + Label: label, + + Color: th.Palette.Fg, + IconColor: th.Palette.ContrastBg, + TextSize: th.TextSize.Scale(14.0 / 16.0), + Size: unit.Dp(26), + shaper: th.Shaper, + checkedStateIcon: th.Icon.RadioChecked, + uncheckedStateIcon: th.Icon.RadioUnchecked, + }, + Key: key, + } +} + +// Layout updates enum and displays the radio button. +func (r RadioButtonStyle) Layout(gtx layout.Context) layout.Dimensions { + hovered, hovering := r.Group.Hovered() + dims := r.layout(gtx, r.Group.Value == r.Key, hovering && hovered == r.Key) + gtx.Constraints.Min = dims.Size + r.Group.Layout(gtx, r.Key) + return dims +} diff --git a/gio/giold/widget/material/slider.go b/gio/giold/widget/material/slider.go new file mode 100644 index 0000000..e038d75 --- /dev/null +++ b/gio/giold/widget/material/slider.go @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +// Slider is for selecting a value in a range. +func Slider(th *Theme, float *widget.Float, min, max float32) SliderStyle { + return SliderStyle{ + Min: min, + Max: max, + Color: th.Palette.ContrastBg, + Float: float, + FingerSize: th.FingerSize, + } +} + +type SliderStyle struct { + Min, Max float32 + Color color.NRGBA + Float *widget.Float + + FingerSize unit.Value +} + +func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions { + thumbRadius := gtx.Px(unit.Dp(6)) + trackWidth := gtx.Px(unit.Dp(2)) + + axis := s.Float.Axis + // Keep a minimum length so that the track is always visible. + minLength := thumbRadius + 3*thumbRadius + thumbRadius + // Try to expand to finger size, but only if the constraints + // allow for it. + touchSizePx := min(gtx.Px(s.FingerSize), + axis.Convert(gtx.Constraints.Max).Y) + sizeMain := max(axis.Convert(gtx.Constraints.Min).X, minLength) + sizeCross := max(2*thumbRadius, touchSizePx) + size := axis.Convert(image.Pt(sizeMain, sizeCross)) + + st := op.Save(gtx.Ops) + o := axis.Convert(image.Pt(thumbRadius, 0)) + op.Offset(layout.FPt(o)).Add(gtx.Ops) + gtx.Constraints.Min = axis.Convert(image.Pt(sizeMain-2*thumbRadius, + sizeCross)) + s.Float.Layout(gtx, thumbRadius, s.Min, s.Max) + gtx.Constraints.Min = gtx.Constraints.Min.Add(axis.Convert(image.Pt(0, + sizeCross))) + thumbPos := thumbRadius + int(s.Float.Pos()) + st.Load() + + color := s.Color + if gtx.Queue == nil { + color = f32color.Disabled(color) + } + + // Draw track before thumb. + st = op.Save(gtx.Ops) + track := image.Rectangle{ + Min: axis.Convert(image.Pt(thumbRadius, sizeCross/2-trackWidth/2)), + Max: axis.Convert(image.Pt(thumbPos, sizeCross/2+trackWidth/2)), + } + clip.Rect(track).Add(gtx.Ops) + paint.Fill(gtx.Ops, color) + st.Load() + + // Draw track after thumb. + st = op.Save(gtx.Ops) + track = image.Rectangle{ + Min: axis.Convert(image.Pt(thumbPos, axis.Convert(track.Min).Y)), + Max: axis.Convert(image.Pt(sizeMain-thumbRadius, + axis.Convert(track.Max).Y)), + } + clip.Rect(track).Add(gtx.Ops) + paint.Fill(gtx.Ops, f32color.MulAlpha(color, 96)) + st.Load() + + // Draw thumb. + pt := axis.Convert(image.Pt(thumbPos, sizeCross/2)) + paint.FillShape(gtx.Ops, color, + clip.Circle{ + Center: f32.Point{X: float32(pt.X), Y: float32(pt.Y)}, + Radius: float32(thumbRadius), + }.Op(gtx.Ops)) + + return layout.Dimensions{Size: size} +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/gio/giold/widget/material/switch.go b/gio/giold/widget/material/switch.go new file mode 100644 index 0000000..14a4134 --- /dev/null +++ b/gio/giold/widget/material/switch.go @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +type SwitchStyle struct { + Color struct { + Enabled color.NRGBA + Disabled color.NRGBA + Track color.NRGBA + } + Switch *widget.Bool +} + +// Switch is for selecting a boolean value. +func Switch(th *Theme, swtch *widget.Bool) SwitchStyle { + sw := SwitchStyle{ + Switch: swtch, + } + sw.Color.Enabled = th.Palette.ContrastBg + sw.Color.Disabled = th.Palette.Bg + sw.Color.Track = f32color.MulAlpha(th.Palette.Fg, 0x88) + return sw +} + +// Layout updates the switch and displays it. +func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions { + trackWidth := gtx.Px(unit.Dp(36)) + trackHeight := gtx.Px(unit.Dp(16)) + thumbSize := gtx.Px(unit.Dp(20)) + trackOff := float32(thumbSize-trackHeight) * .5 + + // Draw track. + stack := op.Save(gtx.Ops) + trackCorner := float32(trackHeight) / 2 + trackRect := f32.Rectangle{Max: f32.Point{ + X: float32(trackWidth), + Y: float32(trackHeight), + }} + col := s.Color.Disabled + if s.Switch.Value { + col = s.Color.Enabled + } + if gtx.Queue == nil { + col = f32color.Disabled(col) + } + trackColor := s.Color.Track + op.Offset(f32.Point{Y: trackOff}).Add(gtx.Ops) + clip.UniformRRect(trackRect, trackCorner).Add(gtx.Ops) + paint.ColorOp{Color: trackColor}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + + // Draw thumb ink. + stack = op.Save(gtx.Ops) + inkSize := gtx.Px(unit.Dp(44)) + rr := float32(inkSize) * .5 + inkOff := f32.Point{ + X: float32(trackWidth)*.5 - rr, + Y: -rr + float32(trackHeight)*.5 + trackOff, + } + op.Offset(inkOff).Add(gtx.Ops) + gtx.Constraints.Min = image.Pt(inkSize, inkSize) + clip.UniformRRect(f32.Rectangle{Max: layout.FPt(gtx.Constraints.Min)}, + rr).Add(gtx.Ops) + for _, p := range s.Switch.History() { + drawInk(gtx, p) + } + stack.Load() + + // Compute thumb offset and color. + stack = op.Save(gtx.Ops) + if s.Switch.Value { + off := trackWidth - thumbSize + op.Offset(f32.Point{X: float32(off)}).Add(gtx.Ops) + } + + thumbRadius := float32(thumbSize) / 2 + + // Draw hover. + if s.Switch.Hovered() { + r := 1.7 * thumbRadius + background := f32color.MulAlpha(s.Color.Enabled, 70) + paint.FillShape(gtx.Ops, background, + clip.Circle{ + Center: f32.Point{X: thumbRadius, Y: thumbRadius}, + Radius: r, + }.Op(gtx.Ops)) + } + + // Draw thumb shadow, a translucent disc slightly larger than the + // thumb itself. + // Center shadow horizontally and slightly adjust its Y. + paint.FillShape(gtx.Ops, argb(0x55000000), + clip.Circle{ + Center: f32.Point{X: thumbRadius, Y: thumbRadius + .25}, + Radius: thumbRadius + 1, + }.Op(gtx.Ops)) + + // Draw thumb. + paint.FillShape(gtx.Ops, col, + clip.Circle{ + Center: f32.Point{X: thumbRadius, Y: thumbRadius}, + Radius: thumbRadius, + }.Op(gtx.Ops)) + + // Set up click area. + stack = op.Save(gtx.Ops) + clickSize := gtx.Px(unit.Dp(40)) + clickOff := f32.Point{ + X: (float32(trackWidth) - float32(clickSize)) * .5, + Y: (float32(trackHeight)-float32(clickSize))*.5 + trackOff, + } + op.Offset(clickOff).Add(gtx.Ops) + sz := image.Pt(clickSize, clickSize) + pointer.Ellipse(image.Rectangle{Max: sz}).Add(gtx.Ops) + gtx.Constraints.Min = sz + s.Switch.Layout(gtx) + stack.Load() + + dims := image.Point{X: trackWidth, Y: thumbSize} + return layout.Dimensions{Size: dims} +} diff --git a/gio/giold/widget/material/theme.go b/gio/giold/widget/material/theme.go new file mode 100644 index 0000000..e19f7df --- /dev/null +++ b/gio/giold/widget/material/theme.go @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image/color" + + "golang.org/x/exp/shiny/materialdesign/icons" + + "realy.lol/gio/text" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +// Palette contains the minimal set of colors that a widget may need to +// draw itself. +type Palette struct { + // Bg is the background color atop which content is currently being + // drawn. + Bg color.NRGBA + + // Fg is a color suitable for drawing on top of Bg. + Fg color.NRGBA + + // ContrastBg is a color used to draw attention to active, + // important, interactive widgets such as buttons. + ContrastBg color.NRGBA + + // ContrastFg is a color suitable for content drawn on top of + // ContrastBg. + ContrastFg color.NRGBA +} + +type Theme struct { + Shaper text.Shaper + Palette + TextSize unit.Value + Icon struct { + CheckBoxChecked *widget.Icon + CheckBoxUnchecked *widget.Icon + RadioChecked *widget.Icon + RadioUnchecked *widget.Icon + } + + // FingerSize is the minimum touch target size. + FingerSize unit.Value +} + +func NewTheme(fontCollection []text.FontFace) *Theme { + t := &Theme{ + Shaper: text.NewCache(fontCollection), + } + t.Palette = Palette{ + Fg: rgb(0x000000), + Bg: rgb(0xffffff), + ContrastBg: rgb(0x3f51b5), + ContrastFg: rgb(0xffffff), + } + t.TextSize = unit.Sp(16) + + t.Icon.CheckBoxChecked = mustIcon(widget.NewIcon(icons.ToggleCheckBox)) + t.Icon.CheckBoxUnchecked = mustIcon(widget.NewIcon(icons.ToggleCheckBoxOutlineBlank)) + t.Icon.RadioChecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonChecked)) + t.Icon.RadioUnchecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonUnchecked)) + + // 38dp is on the lower end of possible finger size. + t.FingerSize = unit.Dp(38) + + return t +} + +func (t Theme) WithPalette(p Palette) Theme { + t.Palette = p + return t +} + +func mustIcon(ic *widget.Icon, err error) *widget.Icon { + if err != nil { + panic(err) + } + return ic +} + +func rgb(c uint32) color.NRGBA { + return argb(0xff000000 | c) +} + +func argb(c uint32) color.NRGBA { + return color.NRGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), + B: uint8(c)} +} diff --git a/gio/gpu/api.go b/gio/gpu/api.go new file mode 100644 index 0000000..1a87684 --- /dev/null +++ b/gio/gpu/api.go @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import "realy.lol/gio/gpu/internal/driver" + +// An API carries the necessary GPU API specific resources to create a Device. +// There is an API type for each supported GPU API such as OpenGL and Direct3D. +type API = driver.API + +// OpenGL denotes the OpenGL or OpenGL ES API. +type OpenGL = driver.OpenGL + +// Direct3D11 denotes the Direct3D API. +type Direct3D11 = driver.Direct3D11 diff --git a/gio/gpu/caches.go b/gio/gpu/caches.go new file mode 100644 index 0000000..3dd93cf --- /dev/null +++ b/gio/gpu/caches.go @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "fmt" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/ops" +) + +type resourceCache struct { + res map[interface{}]resource + newRes map[interface{}]resource +} + +// opCache is like a resourceCache but using concrete types and a +// freelist instead of two maps to avoid runtime.mapaccess2 calls +// since benchmarking showed them as a bottleneck. +type opCache struct { + // store the index + 1 in cache this key is stored in + index map[ops.Key]int + // list of indexes in cache that are free and can be used + freelist []int + cache []opCacheValue +} + +type opCacheValue struct { + data pathData + // computePath is the encoded path for compute. + computePath encoder + + bounds f32.Rectangle + // the fields below are handled by opCache + key ops.Key + keep bool +} + +func newResourceCache() *resourceCache { + return &resourceCache{ + res: make(map[interface{}]resource), + newRes: make(map[interface{}]resource), + } +} + +func (r *resourceCache) get(key interface{}) (resource, bool) { + v, exists := r.res[key] + if exists { + r.newRes[key] = v + } + return v, exists +} + +func (r *resourceCache) put(key interface{}, val resource) { + if _, exists := r.newRes[key]; exists { + panic(fmt.Errorf("key exists, %p", key)) + } + r.res[key] = val + r.newRes[key] = val +} + +func (r *resourceCache) frame() { + for k, v := range r.res { + if _, exists := r.newRes[k]; !exists { + delete(r.res, k) + v.release() + } + } + for k, v := range r.newRes { + delete(r.newRes, k) + r.res[k] = v + } +} + +func (r *resourceCache) release() { + for _, v := range r.newRes { + v.release() + } + r.newRes = nil + r.res = nil +} + +func newOpCache() *opCache { + return &opCache{ + index: make(map[ops.Key]int), + freelist: make([]int, 0), + cache: make([]opCacheValue, 0), + } +} + +func (r *opCache) get(key ops.Key) (o opCacheValue, exist bool) { + v := r.index[key] + if v == 0 { + return + } + r.cache[v-1].keep = true + return r.cache[v-1], true +} + +func (r *opCache) put(key ops.Key, val opCacheValue) { + v := r.index[key] + val.keep = true + val.key = key + if v == 0 { + // not in cache + i := len(r.cache) + if len(r.freelist) > 0 { + i = r.freelist[len(r.freelist)-1] + r.freelist = r.freelist[:len(r.freelist)-1] + r.cache[i] = val + } else { + r.cache = append(r.cache, val) + } + r.index[key] = i + 1 + } else { + r.cache[v-1] = val + } +} + +func (r *opCache) frame() { + r.freelist = r.freelist[:0] + for i, v := range r.cache { + r.cache[i].keep = false + if v.keep { + continue + } + if v.data.data != nil { + v.data.release() + r.cache[i].data.data = nil + } + delete(r.index, v.key) + r.freelist = append(r.freelist, i) + } +} + +func (r *opCache) release() { + for i := range r.cache { + r.cache[i].keep = false + } + r.frame() + r.index = nil + r.freelist = nil + r.cache = nil +} diff --git a/gio/gpu/clip.go b/gio/gpu/clip.go new file mode 100644 index 0000000..7e24449 --- /dev/null +++ b/gio/gpu/clip.go @@ -0,0 +1,98 @@ +package gpu + +import ( + "realy.lol/gio/f32" + "realy.lol/gio/internal/stroke" +) + +type quadSplitter struct { + bounds f32.Rectangle + contour uint32 + d *drawOps +} + +func encodeQuadTo(data []byte, meta uint32, from, ctrl, to f32.Point) { + // NW. + encodeVertex(data, meta, -1, 1, from, ctrl, to) + // NE. + encodeVertex(data[vertStride:], meta, 1, 1, from, ctrl, to) + // SW. + encodeVertex(data[vertStride*2:], meta, -1, -1, from, ctrl, to) + // SE. + encodeVertex(data[vertStride*3:], meta, 1, -1, from, ctrl, to) +} + +func encodeVertex(data []byte, meta uint32, cornerx, cornery int16, + from, ctrl, to f32.Point) { + var corner float32 + if cornerx == 1 { + corner += .5 + } + if cornery == 1 { + corner += .25 + } + v := vertex{ + Corner: corner, + FromX: from.X, + FromY: from.Y, + CtrlX: ctrl.X, + CtrlY: ctrl.Y, + ToX: to.X, + ToY: to.Y, + } + v.encode(data, meta) +} + +func (qs *quadSplitter) encodeQuadTo(from, ctrl, to f32.Point) { + data := qs.d.writeVertCache(vertStride * 4) + encodeQuadTo(data, qs.contour, from, ctrl, to) +} + +func (qs *quadSplitter) splitAndEncode(quad stroke.QuadSegment) { + cbnd := f32.Rectangle{ + Min: quad.From, + Max: quad.To, + }.Canon() + from, ctrl, to := quad.From, quad.Ctrl, quad.To + + // If the curve contain areas where a vertical line + // intersects it twice, split the curve in two x monotone + // lower and upper curves. The stencil fragment program + // expects only one intersection per curve. + + // Find the t where the derivative in x is 0. + v0 := ctrl.Sub(from) + v1 := to.Sub(ctrl) + d := v0.X - v1.X + // t = v0 / d. Split if t is in ]0;1[. + if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X { + t := v0.X / d + ctrl0 := from.Mul(1 - t).Add(ctrl.Mul(t)) + ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t)) + mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t)) + qs.encodeQuadTo(from, ctrl0, mid) + qs.encodeQuadTo(mid, ctrl1, to) + if mid.X > cbnd.Max.X { + cbnd.Max.X = mid.X + } + if mid.X < cbnd.Min.X { + cbnd.Min.X = mid.X + } + } else { + qs.encodeQuadTo(from, ctrl, to) + } + // Find the y extremum, if any. + d = v0.Y - v1.Y + if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y { + t := v0.Y / d + y := (1-t)*(1-t)*from.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y + if y > cbnd.Max.Y { + cbnd.Max.Y = y + } + if y < cbnd.Min.Y { + cbnd.Min.Y = y + } + } + + qs.bounds = qs.bounds.Union(cbnd) +} diff --git a/gio/gpu/compute.go b/gio/gpu/compute.go new file mode 100644 index 0000000..e7c7fd6 --- /dev/null +++ b/gio/gpu/compute.go @@ -0,0 +1,1093 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "encoding/binary" + "errors" + "fmt" + "image" + "image/color" + "math/bits" + "time" + "unsafe" + + "realy.lol/gio/f32" + "realy.lol/gio/gpu/internal/driver" + "realy.lol/gio/internal/byteslice" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/internal/ops" + "realy.lol/gio/internal/scene" + "realy.lol/gio/layout" + "realy.lol/gio/op" +) + +type compute struct { + ctx driver.Device + enc encoder + + drawOps drawOps + texOps []textureOp + cache *resourceCache + maxTextureDim int + + programs struct { + elements driver.Program + tileAlloc driver.Program + pathCoarse driver.Program + backdrop driver.Program + binning driver.Program + coarse driver.Program + kernel4 driver.Program + } + buffers struct { + config driver.Buffer + scene sizedBuffer + state sizedBuffer + memory sizedBuffer + } + output struct { + size image.Point + // image is the output texture. Note that it is in RGBA format, + // but contains data in sRGB. See blitOutput for more detail. + image driver.Texture + blitProg driver.Program + } + // images contains ImageOp images packed into a texture atlas. + images struct { + packer packer + // positions maps imageOpData.handles to positions inside tex. + positions map[interface{}]image.Point + tex driver.Texture + } + // materials contains the pre-processed materials (transformed images for + // now, gradients etc. later) packed in a texture atlas. The atlas is used + // as source in kernel4. + materials struct { + // offsets maps texture ops to the offsets to put in their FillImage commands. + offsets map[textureKey]image.Point + + prog driver.Program + layout driver.InputLayout + + packer packer + + tex driver.Texture + fbo driver.Framebuffer + quads []materialVertex + + bufSize int + buffer driver.Buffer + } + timers struct { + profile string + t *timers + elements *timer + tileAlloc *timer + pathCoarse *timer + backdropBinning *timer + coarse *timer + kernel4 *timer + } + + // The following fields hold scratch space to avoid garbage. + zeroSlice []byte + memHeader *memoryHeader + conf *config +} + +// materialVertex describes a vertex of a quad used to render a transformed +// material. +type materialVertex struct { + posX, posY float32 + u, v float32 +} + +// textureKey identifies textureOp. +type textureKey struct { + handle interface{} + transform f32.Affine2D +} + +// textureOp represents an imageOp that requires texture space. +type textureOp struct { + // sceneIdx is the index in the scene that contains the fill image command + // that corresponds to the operation. + sceneIdx int + key textureKey + img imageOpData + + // pos is the position of the untransformed image in the images texture. + pos image.Point +} + +type encoder struct { + scene []scene.Command + npath int + npathseg int + ntrans int +} + +type encodeState struct { + trans f32.Affine2D + clip f32.Rectangle +} + +type sizedBuffer struct { + size int + buffer driver.Buffer +} + +// config matches Config in setup.h +type config struct { + n_elements uint32 // paths + n_pathseg uint32 + width_in_tiles uint32 + height_in_tiles uint32 + tile_alloc memAlloc + bin_alloc memAlloc + ptcl_alloc memAlloc + pathseg_alloc memAlloc + anno_alloc memAlloc + trans_alloc memAlloc +} + +// memAlloc matches Alloc in mem.h +type memAlloc struct { + offset uint32 + // size uint32 +} + +// memoryHeader matches the header of Memory in mem.h. +type memoryHeader struct { + mem_offset uint32 + mem_error uint32 +} + +// GPU structure sizes and constants. +const ( + tileWidthPx = 32 + tileHeightPx = 32 + ptclInitialAlloc = 1024 + kernel4OutputUnit = 2 + kernel4AtlasUnit = 3 + + pathSize = 12 + binSize = 8 + pathsegSize = 52 + annoSize = 32 + transSize = 24 + stateSize = 60 + stateStride = 4 + 2*stateSize +) + +// mem.h constants. +const ( + memNoError = 0 // NO_ERROR + memMallocFailed = 1 // ERR_MALLOC_FAILED +) + +func newCompute(ctx driver.Device) (*compute, error) { + maxDim := ctx.Caps().MaxTextureSize + // Large atlas textures cause artifacts due to precision loss in + // shaders. + if cap := 8192; maxDim > cap { + maxDim = cap + } + g := &compute{ + ctx: ctx, + cache: newResourceCache(), + maxTextureDim: maxDim, + conf: new(config), + memHeader: new(memoryHeader), + } + + blitProg, err := ctx.NewProgram(shader_copy_vert, shader_copy_frag) + if err != nil { + g.Release() + return nil, err + } + g.output.blitProg = blitProg + + materialProg, err := ctx.NewProgram(shader_material_vert, + shader_material_frag) + if err != nil { + g.Release() + return nil, err + } + g.materials.prog = materialProg + progLayout, err := ctx.NewInputLayout(shader_material_vert, + []driver.InputDesc{ + {Type: driver.DataTypeFloat, Size: 2, Offset: 0}, + {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2}, + }) + if err != nil { + g.Release() + return nil, err + } + g.materials.layout = progLayout + + g.drawOps.pathCache = newOpCache() + g.drawOps.compute = true + + buf, err := ctx.NewBuffer(driver.BufferBindingShaderStorage, + int(unsafe.Sizeof(config{}))) + if err != nil { + g.Release() + return nil, err + } + g.buffers.config = buf + + shaders := []struct { + prog *driver.Program + src driver.ShaderSources + }{ + {&g.programs.elements, shader_elements_comp}, + {&g.programs.tileAlloc, shader_tile_alloc_comp}, + {&g.programs.pathCoarse, shader_path_coarse_comp}, + {&g.programs.backdrop, shader_backdrop_comp}, + {&g.programs.binning, shader_binning_comp}, + {&g.programs.coarse, shader_coarse_comp}, + {&g.programs.kernel4, shader_kernel4_comp}, + } + for _, shader := range shaders { + p, err := ctx.NewComputeProgram(shader.src) + if err != nil { + g.Release() + return nil, err + } + *shader.prog = p + } + return g, nil +} + +func (g *compute) Collect(viewport image.Point, ops *op.Ops) { + g.drawOps.reset(g.cache, viewport) + g.drawOps.collect(g.ctx, g.cache, ops, viewport) + for _, img := range g.drawOps.allImageOps { + expandPathOp(img.path, img.clip) + } + if g.drawOps.profile && g.timers.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) { + t := &g.timers + t.t = newTimers(g.ctx) + t.elements = g.timers.t.newTimer() + t.tileAlloc = g.timers.t.newTimer() + t.pathCoarse = g.timers.t.newTimer() + t.backdropBinning = g.timers.t.newTimer() + t.coarse = g.timers.t.newTimer() + t.kernel4 = g.timers.t.newTimer() + } +} + +func (g *compute) Clear(col color.NRGBA) { + g.drawOps.clear = true + g.drawOps.clearColor = f32color.LinearFromSRGB(col) +} + +func (g *compute) Frame() error { + viewport := g.drawOps.viewport + tileDims := image.Point{ + X: (viewport.X + tileWidthPx - 1) / tileWidthPx, + Y: (viewport.Y + tileHeightPx - 1) / tileHeightPx, + } + + defFBO := g.ctx.BeginFrame() + defer g.ctx.EndFrame() + + if err := g.encode(viewport); err != nil { + return err + } + if err := g.uploadImages(); err != nil { + return err + } + if err := g.renderMaterials(); err != nil { + return err + } + if err := g.render(tileDims); err != nil { + return err + } + g.ctx.BindFramebuffer(defFBO) + g.blitOutput(viewport) + g.cache.frame() + g.drawOps.pathCache.frame() + t := &g.timers + if g.drawOps.profile && t.t.ready() { + et, tat, pct, bbt := t.elements.Elapsed, t.tileAlloc.Elapsed, t.pathCoarse.Elapsed, t.backdropBinning.Elapsed + ct, k4t := t.coarse.Elapsed, t.kernel4.Elapsed + ft := et + tat + pct + bbt + ct + k4t + q := 100 * time.Microsecond + ft = ft.Round(q) + et, tat, pct, bbt = et.Round(q), tat.Round(q), pct.Round(q), bbt.Round(q) + ct, k4t = ct.Round(q), k4t.Round(q) + t.profile = fmt.Sprintf("ft:%7s et:%7s tat:%7s pct:%7s bbt:%7s ct:%7s k4t:%7s", + ft, et, tat, pct, bbt, ct, k4t) + } + g.drawOps.clear = false + return nil +} + +func (g *compute) Profile() string { + return g.timers.profile +} + +// blitOutput copies the compute render output to the output FBO. We need to +// copy because compute shaders can only write to textures, not FBOs. Compute +// shader can only write to RGBA textures, but since we actually render in sRGB +// format we can't use glBlitFramebuffer, because it does sRGB conversion. +func (g *compute) blitOutput(viewport image.Point) { + if !g.drawOps.clear { + g.ctx.BlendFunc(driver.BlendFactorOne, + driver.BlendFactorOneMinusSrcAlpha) + g.ctx.SetBlend(true) + defer g.ctx.SetBlend(false) + } + g.ctx.Viewport(0, 0, viewport.X, viewport.Y) + g.ctx.BindTexture(0, g.output.image) + g.ctx.BindProgram(g.output.blitProg) + g.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4) +} + +func (g *compute) encode(viewport image.Point) error { + g.texOps = g.texOps[:0] + g.enc.reset() + + // Flip Y-axis. + flipY := f32.Affine2D{}.Scale(f32.Pt(0, 0), f32.Pt(1, -1)).Offset(f32.Pt(0, + float32(viewport.Y))) + g.enc.transform(flipY) + if g.drawOps.clear { + g.enc.rect(f32.Rectangle{Max: layout.FPt(viewport)}) + g.enc.fillColor(f32color.NRGBAToRGBA(g.drawOps.clearColor.SRGB())) + } + return g.encodeOps(flipY, viewport, g.drawOps.allImageOps) +} + +func (g *compute) renderMaterials() error { + m := &g.materials + m.quads = m.quads[:0] + resize := false + reclaimed := false +restart: + for { + for _, op := range g.texOps { + if off, exists := m.offsets[op.key]; exists { + g.enc.setFillImageOffset(op.sceneIdx, off) + continue + } + quad, bounds := g.materialQuad(op.key.transform, op.img, op.pos) + + // A material is clipped to avoid drawing outside its bounds inside the atlas. However, + // imprecision in the clipping may cause a single pixel overflow. Be safe. + size := bounds.Size().Add(image.Pt(1, 1)) + place, fits := m.packer.tryAdd(size) + if !fits { + m.offsets = nil + m.quads = m.quads[:0] + m.packer.clear() + if !reclaimed { + // Some images may no longer be in use, try again + // after clearing existing maps. + reclaimed = true + } else { + m.packer.maxDim += 256 + resize = true + if m.packer.maxDim > g.maxTextureDim { + return errors.New("compute: no space left in material atlas") + } + } + m.packer.newPage() + continue restart + } + // Position quad to match place. + offset := place.Pos.Sub(bounds.Min) + offsetf := layout.FPt(offset) + for i := range quad { + quad[i].posX += offsetf.X + quad[i].posY += offsetf.Y + } + // Draw quad as two triangles. + m.quads = append(m.quads, quad[0], quad[1], quad[3], quad[3], + quad[1], quad[2]) + if m.offsets == nil { + m.offsets = make(map[textureKey]image.Point) + } + m.offsets[op.key] = offset + g.enc.setFillImageOffset(op.sceneIdx, offset) + } + break + } + if len(m.quads) == 0 { + return nil + } + texSize := m.packer.maxDim + if resize { + if m.fbo != nil { + m.fbo.Release() + m.fbo = nil + } + if m.tex != nil { + m.tex.Release() + m.tex = nil + } + handle, err := g.ctx.NewTexture(driver.TextureFormatRGBA8, texSize, + texSize, + driver.FilterNearest, driver.FilterNearest, + driver.BufferBindingShaderStorage|driver.BufferBindingFramebuffer) + if err != nil { + return fmt.Errorf("compute: failed to create material atlas: %v", + err) + } + m.tex = handle + fbo, err := g.ctx.NewFramebuffer(handle, 0) + if err != nil { + return fmt.Errorf("compute: failed to create material framebuffer: %v", + err) + } + m.fbo = fbo + } + // TODO: move to shaders. + // Transform to clip space: [-1, -1] - [1, 1]. + clip := f32.Affine2D{}.Scale(f32.Pt(0, 0), + f32.Pt(2/float32(texSize), 2/float32(texSize))).Offset(f32.Pt(-1, -1)) + for i, v := range m.quads { + p := clip.Transform(f32.Pt(v.posX, v.posY)) + m.quads[i].posX = p.X + m.quads[i].posY = p.Y + } + vertexData := byteslice.Slice(m.quads) + if len(vertexData) > m.bufSize { + if m.buffer != nil { + m.buffer.Release() + m.buffer = nil + } + n := pow2Ceil(len(vertexData)) + buf, err := g.ctx.NewBuffer(driver.BufferBindingVertices, n) + if err != nil { + return err + } + m.bufSize = n + m.buffer = buf + } + m.buffer.Upload(vertexData) + g.ctx.BindTexture(0, g.images.tex) + g.ctx.BindFramebuffer(m.fbo) + g.ctx.Viewport(0, 0, texSize, texSize) + if reclaimed { + g.ctx.Clear(0, 0, 0, 0) + } + g.ctx.BindProgram(m.prog) + g.ctx.BindVertexBuffer(m.buffer, int(unsafe.Sizeof(m.quads[0])), 0) + g.ctx.BindInputLayout(m.layout) + g.ctx.DrawArrays(driver.DrawModeTriangles, 0, len(m.quads)) + return nil +} + +func (g *compute) uploadImages() error { + // padding is the number of pixels added to the right and below + // images, to avoid atlas filtering artifacts. + const padding = 1 + + a := &g.images + var uploads map[interface{}]*image.RGBA + resize := false + reclaimed := false +restart: + for { + for i, op := range g.texOps { + if pos, exists := a.positions[op.img.handle]; exists { + g.texOps[i].pos = pos + continue + } + size := op.img.src.Bounds().Size().Add(image.Pt(padding, padding)) + place, fits := a.packer.tryAdd(size) + if !fits { + a.positions = nil + uploads = nil + a.packer.clear() + if !reclaimed { + // Some images may no longer be in use, try again + // after clearing existing maps. + reclaimed = true + } else { + a.packer.maxDim += 256 + resize = true + if a.packer.maxDim > g.maxTextureDim { + return errors.New("compute: no space left in image atlas") + } + } + a.packer.newPage() + continue restart + } + if a.positions == nil { + a.positions = make(map[interface{}]image.Point) + } + a.positions[op.img.handle] = place.Pos + g.texOps[i].pos = place.Pos + if uploads == nil { + uploads = make(map[interface{}]*image.RGBA) + } + uploads[op.img.handle] = op.img.src + } + break + } + if len(uploads) == 0 { + return nil + } + if resize { + if a.tex != nil { + a.tex.Release() + a.tex = nil + } + sz := a.packer.maxDim + handle, err := g.ctx.NewTexture(driver.TextureFormatSRGB, sz, sz, + driver.FilterLinear, driver.FilterLinear, + driver.BufferBindingTexture) + if err != nil { + return fmt.Errorf("compute: failed to create image atlas: %v", err) + } + a.tex = handle + } + for h, img := range uploads { + pos, ok := a.positions[h] + if !ok { + panic("compute: internal error: image not placed") + } + size := img.Bounds().Size() + driver.UploadImage(a.tex, pos, img) + rightPadding := image.Pt(padding, size.Y) + a.tex.Upload(image.Pt(pos.X+size.X, pos.Y), rightPadding, + g.zeros(rightPadding.X*rightPadding.Y*4)) + bottomPadding := image.Pt(size.X, padding) + a.tex.Upload(image.Pt(pos.X, pos.Y+size.Y), bottomPadding, + g.zeros(bottomPadding.X*bottomPadding.Y*4)) + } + return nil +} + +func pow2Ceil(v int) int { + exp := bits.Len(uint(v)) + if bits.OnesCount(uint(v)) == 1 { + exp-- + } + return 1 << exp +} + +// materialQuad constructs a quad that represents the transformed image. It returns the quad +// and its bounds. +func (g *compute) materialQuad(M f32.Affine2D, img imageOpData, + uvPos image.Point) ([4]materialVertex, image.Rectangle) { + imgSize := layout.FPt(img.src.Bounds().Size()) + sx, hx, ox, hy, sy, oy := M.Elems() + transOff := f32.Pt(ox, oy) + // The 4 corners of the image rectangle transformed by M, excluding its offset, are: + // + // q0: M * (0, 0) q3: M * (w, 0) + // q1: M * (0, h) q2: M * (w, h) + // + // Note that q0 = M*0 = 0, q2 = q1 + q3. + q0 := f32.Pt(0, 0) + q1 := f32.Pt(hx*imgSize.Y, sy*imgSize.Y) + q3 := f32.Pt(sx*imgSize.X, hy*imgSize.X) + q2 := q1.Add(q3) + q0 = q0.Add(transOff) + q1 = q1.Add(transOff) + q2 = q2.Add(transOff) + q3 = q3.Add(transOff) + + boundsf := f32.Rectangle{ + Min: min(min(q0, q1), min(q2, q3)), + Max: max(max(q0, q1), max(q2, q3)), + } + + bounds := boundRectF(boundsf) + uvPosf := layout.FPt(uvPos) + atlasScale := 1 / float32(g.images.packer.maxDim) + uvBounds := f32.Rectangle{ + Min: uvPosf.Mul(atlasScale), + Max: uvPosf.Add(imgSize).Mul(atlasScale), + } + quad := [4]materialVertex{ + {posX: q0.X, posY: q0.Y, u: uvBounds.Min.X, v: uvBounds.Min.Y}, + {posX: q1.X, posY: q1.Y, u: uvBounds.Min.X, v: uvBounds.Max.Y}, + {posX: q2.X, posY: q2.Y, u: uvBounds.Max.X, v: uvBounds.Max.Y}, + {posX: q3.X, posY: q3.Y, u: uvBounds.Max.X, v: uvBounds.Min.Y}, + } + return quad, bounds +} + +func max(p1, p2 f32.Point) f32.Point { + p := p1 + if p2.X > p.X { + p.X = p2.X + } + if p2.Y > p.Y { + p.Y = p2.Y + } + return p +} + +func min(p1, p2 f32.Point) f32.Point { + p := p1 + if p2.X < p.X { + p.X = p2.X + } + if p2.Y < p.Y { + p.Y = p2.Y + } + return p +} + +func (g *compute) encodeOps(trans f32.Affine2D, viewport image.Point, + ops []imageOp) error { + for _, op := range ops { + bounds := layout.FRect(op.clip) + // clip is the union of all drawing affected by the clipping + // operation. TODO: tighten. + clip := f32.Rect(0, 0, float32(viewport.X), float32(viewport.Y)) + nclips := g.encodeClipStack(clip, bounds, op.path, false) + m := op.material + switch m.material { + case materialTexture: + t := trans.Mul(m.trans) + g.texOps = append(g.texOps, textureOp{ + sceneIdx: len(g.enc.scene), + img: m.data, + key: textureKey{ + transform: t, + handle: m.data.handle, + }, + }) + // Add fill command, its offset is resolved and filled in renderMaterials. + g.enc.fillImage(0) + case materialColor: + g.enc.fillColor(f32color.NRGBAToRGBA(op.material.color.SRGB())) + case materialLinearGradient: + // TODO: implement. + g.enc.fillColor(f32color.NRGBAToRGBA(op.material.color1.SRGB())) + default: + panic("not implemented") + } + if op.path != nil && op.path.path { + g.enc.fillMode(scene.FillModeNonzero) + g.enc.transform(op.path.trans.Invert()) + } + // Pop the clip stack. + for i := 0; i < nclips; i++ { + g.enc.endClip(clip) + } + } + return nil +} + +// encodeClips encodes a stack of clip paths and return the stack depth. +func (g *compute) encodeClipStack(clip, bounds f32.Rectangle, p *pathOp, + begin bool) int { + nclips := 0 + if p != nil && p.parent != nil { + nclips += g.encodeClipStack(clip, bounds, p.parent, true) + nclips += 1 + } + isStroke := p.stroke.Width > 0 + if p != nil && p.path { + if isStroke { + g.enc.fillMode(scene.FillModeStroke) + g.enc.lineWidth(p.stroke.Width) + } + pathData, _ := g.drawOps.pathCache.get(p.pathKey) + g.enc.transform(p.trans) + g.enc.append(pathData.computePath) + } else { + g.enc.rect(bounds) + } + if begin { + g.enc.beginClip(clip) + if isStroke { + g.enc.fillMode(scene.FillModeNonzero) + } + if p != nil && p.path { + g.enc.transform(p.trans.Invert()) + } + } + return nclips +} + +func encodePath(verts []byte) encoder { + var enc encoder + for len(verts) >= scene.CommandSize+4 { + cmd := ops.DecodeCommand(verts[4:]) + enc.scene = append(enc.scene, cmd) + enc.npathseg++ + verts = verts[scene.CommandSize+4:] + } + return enc +} + +func (g *compute) render(tileDims image.Point) error { + const ( + // wgSize is the largest and most common workgroup size. + wgSize = 128 + // PARTITION_SIZE from elements.comp + partitionSize = 32 * 4 + ) + widthInBins := (tileDims.X + 15) / 16 + heightInBins := (tileDims.Y + 7) / 8 + if widthInBins*heightInBins > wgSize { + return fmt.Errorf("gpu: output too large (%dx%d)", + tileDims.X*tileWidthPx, tileDims.Y*tileHeightPx) + } + + // Pad scene with zeroes to avoid reading garbage in elements.comp. + scenePadding := partitionSize - len(g.enc.scene)%partitionSize + g.enc.scene = append(g.enc.scene, make([]scene.Command, scenePadding)...) + + realloced := false + scene := byteslice.Slice(g.enc.scene) + if s := len(scene); s > g.buffers.scene.size { + realloced = true + paddedCap := s * 11 / 10 + if err := g.buffers.scene.ensureCapacity(g.ctx, paddedCap); err != nil { + return err + } + } + g.buffers.scene.buffer.Upload(scene) + + w, h := tileDims.X*tileWidthPx, tileDims.Y*tileHeightPx + if g.output.size.X != w || g.output.size.Y != h { + if err := g.resizeOutput(image.Pt(w, h)); err != nil { + return err + } + } + g.ctx.BindImageTexture(kernel4OutputUnit, g.output.image, + driver.AccessWrite, driver.TextureFormatRGBA8) + if t := g.materials.tex; t != nil { + g.ctx.BindImageTexture(kernel4AtlasUnit, t, driver.AccessRead, + driver.TextureFormatRGBA8) + } + + // alloc is the number of allocated bytes for static buffers. + var alloc uint32 + round := func(v, quantum int) int { + return (v + quantum - 1) &^ (quantum - 1) + } + malloc := func(size int) memAlloc { + size = round(size, 4) + offset := alloc + alloc += uint32(size) + return memAlloc{offset /*, uint32(size)*/} + } + + *g.conf = config{ + n_elements: uint32(g.enc.npath), + n_pathseg: uint32(g.enc.npathseg), + width_in_tiles: uint32(tileDims.X), + height_in_tiles: uint32(tileDims.Y), + tile_alloc: malloc(g.enc.npath * pathSize), + bin_alloc: malloc(round(g.enc.npath, wgSize) * binSize), + ptcl_alloc: malloc(tileDims.X * tileDims.Y * ptclInitialAlloc), + pathseg_alloc: malloc(g.enc.npathseg * pathsegSize), + anno_alloc: malloc(g.enc.npath * annoSize), + trans_alloc: malloc(g.enc.ntrans * transSize), + } + + numPartitions := (g.enc.numElements() + 127) / 128 + // clearSize is the atomic partition counter plus flag and 2 states per partition. + clearSize := 4 + numPartitions*stateStride + if clearSize > g.buffers.state.size { + realloced = true + paddedCap := clearSize * 11 / 10 + if err := g.buffers.state.ensureCapacity(g.ctx, paddedCap); err != nil { + return err + } + } + + g.buffers.config.Upload(byteslice.Struct(g.conf)) + + minSize := int(unsafe.Sizeof(memoryHeader{})) + int(alloc) + if minSize > g.buffers.memory.size { + realloced = true + // Add space for dynamic GPU allocations. + const sizeBump = 4 * 1024 * 1024 + minSize += sizeBump + if err := g.buffers.memory.ensureCapacity(g.ctx, minSize); err != nil { + return err + } + } + for { + *g.memHeader = memoryHeader{ + mem_offset: alloc, + } + g.buffers.memory.buffer.Upload(byteslice.Struct(g.memHeader)) + g.buffers.state.buffer.Upload(g.zeros(clearSize)) + + if realloced { + realloced = false + g.bindBuffers() + } + t := &g.timers + g.ctx.MemoryBarrier() + t.elements.begin() + g.ctx.BindProgram(g.programs.elements) + g.ctx.DispatchCompute(numPartitions, 1, 1) + g.ctx.MemoryBarrier() + t.elements.end() + t.tileAlloc.begin() + g.ctx.BindProgram(g.programs.tileAlloc) + g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1) + g.ctx.MemoryBarrier() + t.tileAlloc.end() + t.pathCoarse.begin() + g.ctx.BindProgram(g.programs.pathCoarse) + g.ctx.DispatchCompute((g.enc.npathseg+31)/32, 1, 1) + g.ctx.MemoryBarrier() + t.pathCoarse.end() + t.backdropBinning.begin() + g.ctx.BindProgram(g.programs.backdrop) + g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1) + // No barrier needed between backdrop and binning. + g.ctx.BindProgram(g.programs.binning) + g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1) + g.ctx.MemoryBarrier() + t.backdropBinning.end() + t.coarse.begin() + g.ctx.BindProgram(g.programs.coarse) + g.ctx.DispatchCompute(widthInBins, heightInBins, 1) + g.ctx.MemoryBarrier() + t.coarse.end() + t.kernel4.begin() + g.ctx.BindProgram(g.programs.kernel4) + g.ctx.DispatchCompute(tileDims.X, tileDims.Y, 1) + g.ctx.MemoryBarrier() + t.kernel4.end() + + if err := g.buffers.memory.buffer.Download(byteslice.Struct(g.memHeader)); err != nil { + if err == driver.ErrContentLost { + continue + } + return err + } + switch errCode := g.memHeader.mem_error; errCode { + case memNoError: + return nil + case memMallocFailed: + // Resize memory and try again. + realloced = true + sz := g.buffers.memory.size * 15 / 10 + if err := g.buffers.memory.ensureCapacity(g.ctx, sz); err != nil { + return err + } + continue + default: + return fmt.Errorf("compute: shader program failed with error %d", + errCode) + } + } +} + +// zeros returns a byte slice with size bytes of zeros. +func (g *compute) zeros(size int) []byte { + if cap(g.zeroSlice) < size { + g.zeroSlice = append(g.zeroSlice, make([]byte, size)...) + } + return g.zeroSlice[:size] +} + +func (g *compute) resizeOutput(size image.Point) error { + if g.output.image != nil { + g.output.image.Release() + g.output.image = nil + } + img, err := g.ctx.NewTexture(driver.TextureFormatRGBA8, size.X, size.Y, + driver.FilterNearest, + driver.FilterNearest, + driver.BufferBindingShaderStorage|driver.BufferBindingTexture) + if err != nil { + return err + } + g.output.image = img + g.output.size = size + return nil +} + +func (g *compute) Release() { + if g.drawOps.pathCache != nil { + g.drawOps.pathCache.release() + } + if g.cache != nil { + g.cache.release() + } + progs := []driver.Program{ + g.programs.elements, + g.programs.tileAlloc, + g.programs.pathCoarse, + g.programs.backdrop, + g.programs.binning, + g.programs.coarse, + g.programs.kernel4, + } + if p := g.output.blitProg; p != nil { + p.Release() + } + for _, p := range progs { + if p != nil { + p.Release() + } + } + g.buffers.scene.release() + g.buffers.state.release() + g.buffers.memory.release() + if b := g.buffers.config; b != nil { + b.Release() + } + if g.output.image != nil { + g.output.image.Release() + } + if g.images.tex != nil { + g.images.tex.Release() + } + if g.materials.layout != nil { + g.materials.layout.Release() + } + if g.materials.prog != nil { + g.materials.prog.Release() + } + if g.materials.fbo != nil { + g.materials.fbo.Release() + } + if g.materials.tex != nil { + g.materials.tex.Release() + } + if g.materials.buffer != nil { + g.materials.buffer.Release() + } + if g.timers.t != nil { + g.timers.t.release() + } + + *g = compute{} +} + +func (g *compute) bindBuffers() { + bindStorageBuffers(g.programs.elements, g.buffers.memory.buffer, + g.buffers.config, g.buffers.scene.buffer, g.buffers.state.buffer) + bindStorageBuffers(g.programs.tileAlloc, g.buffers.memory.buffer, + g.buffers.config) + bindStorageBuffers(g.programs.pathCoarse, g.buffers.memory.buffer, + g.buffers.config) + bindStorageBuffers(g.programs.backdrop, g.buffers.memory.buffer, + g.buffers.config) + bindStorageBuffers(g.programs.binning, g.buffers.memory.buffer, + g.buffers.config) + bindStorageBuffers(g.programs.coarse, g.buffers.memory.buffer, + g.buffers.config) + bindStorageBuffers(g.programs.kernel4, g.buffers.memory.buffer, + g.buffers.config) +} + +func (b *sizedBuffer) release() { + if b.buffer == nil { + return + } + b.buffer.Release() + *b = sizedBuffer{} +} + +func (b *sizedBuffer) ensureCapacity(ctx driver.Device, size int) error { + if b.size >= size { + return nil + } + if b.buffer != nil { + b.release() + } + buf, err := ctx.NewBuffer(driver.BufferBindingShaderStorage, size) + if err != nil { + return err + } + b.buffer = buf + b.size = size + return nil +} + +func bindStorageBuffers(prog driver.Program, buffers ...driver.Buffer) { + for i, buf := range buffers { + prog.SetStorageBuffer(i, buf) + } +} + +var bo = binary.LittleEndian + +func (e *encoder) reset() { + e.scene = e.scene[:0] + e.npath = 0 + e.npathseg = 0 + e.ntrans = 0 +} + +func (e *encoder) numElements() int { + return len(e.scene) +} + +func (e *encoder) append(e2 encoder) { + e.scene = append(e.scene, e2.scene...) + e.npath += e2.npath + e.npathseg += e2.npathseg + e.ntrans += e2.ntrans +} + +func (e *encoder) transform(m f32.Affine2D) { + e.scene = append(e.scene, scene.Transform(m)) + e.ntrans++ +} + +func (e *encoder) lineWidth(width float32) { + e.scene = append(e.scene, scene.SetLineWidth(width)) +} + +func (e *encoder) fillMode(mode scene.FillMode) { + e.scene = append(e.scene, scene.SetFillMode(mode)) +} + +func (e *encoder) beginClip(bbox f32.Rectangle) { + e.scene = append(e.scene, scene.BeginClip(bbox)) + e.npath++ +} + +func (e *encoder) endClip(bbox f32.Rectangle) { + e.scene = append(e.scene, scene.EndClip(bbox)) + e.npath++ +} + +func (e *encoder) rect(r f32.Rectangle) { + // Rectangle corners, clock-wise. + c0, c1, c2, c3 := r.Min, f32.Pt(r.Min.X, r.Max.Y), r.Max, f32.Pt(r.Max.X, + r.Min.Y) + e.line(c0, c1) + e.line(c1, c2) + e.line(c2, c3) + e.line(c3, c0) +} + +func (e *encoder) fillColor(col color.RGBA) { + e.scene = append(e.scene, scene.FillColor(col)) + e.npath++ +} + +func (e *encoder) setFillImageOffset(index int, offset image.Point) { + x := int16(offset.X) + y := int16(offset.Y) + e.scene[index][2] = uint32(uint16(x)) | uint32(uint16(y))<<16 +} + +func (e *encoder) fillImage(index int) { + e.scene = append(e.scene, scene.FillImage(index)) + e.npath++ +} + +func (e *encoder) line(start, end f32.Point) { + e.scene = append(e.scene, scene.Line(start, end)) + e.npathseg++ +} + +func (e *encoder) quad(start, ctrl, end f32.Point) { + e.scene = append(e.scene, scene.Quad(start, ctrl, end)) + e.npathseg++ +} diff --git a/gio/gpu/gen.go b/gio/gpu/gen.go new file mode 100644 index 0000000..238f002 --- /dev/null +++ b/gio/gpu/gen.go @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +//go:generate go run ./internal/convertshaders -package gpu diff --git a/gio/gpu/gpu.go b/gio/gpu/gpu.go new file mode 100644 index 0000000..7ff12e5 --- /dev/null +++ b/gio/gpu/gpu.go @@ -0,0 +1,1505 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package gpu implements the rendering of Gio drawing operations. It +is used by package app and package app/headless and is otherwise not +useful except for integrating with external window implementations. +*/ +package gpu + +import ( + "encoding/binary" + "errors" + "fmt" + "image" + "image/color" + "math" + "os" + "reflect" + "time" + "unsafe" + + "realy.lol/gio/f32" + "realy.lol/gio/gpu/internal/driver" + "realy.lol/gio/internal/byteslice" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/internal/opconst" + "realy.lol/gio/internal/ops" + "realy.lol/gio/internal/scene" + "realy.lol/gio/internal/stroke" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + + // Register backends. + _ "realy.lol/gio/gpu/internal/d3d11" + _ "realy.lol/gio/gpu/internal/opengl" +) + +type GPU interface { + // Release non-Go resources. The GPU is no longer valid after Release. + Release() + // Clear sets the clear color for the next Frame. + Clear(color color.NRGBA) + // Collect the graphics operations from frame, given the viewport. + Collect(viewport image.Point, frame *op.Ops) + // Frame clears the color buffer and draws the collected operations. + Frame() error + // Profile returns the last available profiling information. Profiling + // information is requested when Collect sees a ProfileOp, and the result + // is available through Profile at some later time. + Profile() string +} + +type gpu struct { + cache *resourceCache + + profile string + timers *timers + frameStart time.Time + zopsTimer, stencilTimer, coverTimer, cleanupTimer *timer + drawOps drawOps + ctx driver.Device + renderer *renderer +} + +type renderer struct { + ctx driver.Device + blitter *blitter + pather *pather + packer packer + intersections packer +} + +type drawOps struct { + profile bool + reader ops.Reader + states []drawState + cache *resourceCache + vertCache []byte + viewport image.Point + clear bool + clearColor f32color.RGBA + // allImageOps is the combined list of imageOps and + // zimageOps, in drawing order. + allImageOps []imageOp + imageOps []imageOp + // zimageOps are the rectangle clipped opaque images + // that can use fast front-to-back rendering with z-test + // and no blending. + zimageOps []imageOp + pathOps []*pathOp + pathOpCache []pathOp + qs quadSplitter + pathCache *opCache + // hack for the compute renderer to access + // converted path data. + compute bool +} + +type drawState struct { + clip f32.Rectangle + t f32.Affine2D + cpath *pathOp + rect bool + + matType materialType + // Current paint.ImageOp + image imageOpData + // Current paint.ColorOp, if any. + color color.NRGBA + + // Current paint.LinearGradientOp. + stop1 f32.Point + stop2 f32.Point + color1 color.NRGBA + color2 color.NRGBA +} + +type pathOp struct { + off f32.Point + // clip is the union of all + // later clip rectangles. + clip image.Rectangle + bounds f32.Rectangle + pathKey ops.Key + path bool + pathVerts []byte + parent *pathOp + place placement + + // For compute + trans f32.Affine2D + stroke clip.StrokeStyle +} + +type imageOp struct { + z float32 + path *pathOp + clip image.Rectangle + material material + clipType clipType + place placement +} + +func decodeStrokeOp(data []byte) clip.StrokeStyle { + _ = data[4] + if opconst.OpType(data[0]) != opconst.TypeStroke { + panic("invalid op") + } + bo := binary.LittleEndian + return clip.StrokeStyle{ + Width: math.Float32frombits(bo.Uint32(data[1:])), + } +} + +type quadsOp struct { + key ops.Key + aux []byte +} + +type material struct { + material materialType + opaque bool + // For materialTypeColor. + color f32color.RGBA + // For materialTypeLinearGradient. + color1 f32color.RGBA + color2 f32color.RGBA + // For materialTypeTexture. + data imageOpData + uvTrans f32.Affine2D + + // For the compute backend. + trans f32.Affine2D +} + +// clipOp is the shadow of clip.Op. +type clipOp struct { + // TODO: Use image.Rectangle? + bounds f32.Rectangle + outline bool +} + +// imageOpData is the shadow of paint.ImageOp. +type imageOpData struct { + src *image.RGBA + handle interface{} +} + +type linearGradientOpData struct { + stop1 f32.Point + color1 color.NRGBA + stop2 f32.Point + color2 color.NRGBA +} + +func (op *clipOp) decode(data []byte) { + if opconst.OpType(data[0]) != opconst.TypeClip { + panic("invalid op") + } + bo := binary.LittleEndian + r := image.Rectangle{ + Min: image.Point{ + X: int(int32(bo.Uint32(data[1:]))), + Y: int(int32(bo.Uint32(data[5:]))), + }, + Max: image.Point{ + X: int(int32(bo.Uint32(data[9:]))), + Y: int(int32(bo.Uint32(data[13:]))), + }, + } + *op = clipOp{ + bounds: layout.FRect(r), + outline: data[17] == 1, + } +} + +func decodeImageOp(data []byte, refs []interface{}) imageOpData { + if opconst.OpType(data[0]) != opconst.TypeImage { + panic("invalid op") + } + handle := refs[1] + if handle == nil { + return imageOpData{} + } + return imageOpData{ + src: refs[0].(*image.RGBA), + handle: handle, + } +} + +func decodeColorOp(data []byte) color.NRGBA { + if opconst.OpType(data[0]) != opconst.TypeColor { + panic("invalid op") + } + return color.NRGBA{ + R: data[1], + G: data[2], + B: data[3], + A: data[4], + } +} + +func decodeLinearGradientOp(data []byte) linearGradientOpData { + if opconst.OpType(data[0]) != opconst.TypeLinearGradient { + panic("invalid op") + } + bo := binary.LittleEndian + return linearGradientOpData{ + stop1: f32.Point{ + X: math.Float32frombits(bo.Uint32(data[1:])), + Y: math.Float32frombits(bo.Uint32(data[5:])), + }, + stop2: f32.Point{ + X: math.Float32frombits(bo.Uint32(data[9:])), + Y: math.Float32frombits(bo.Uint32(data[13:])), + }, + color1: color.NRGBA{ + R: data[17+0], + G: data[17+1], + B: data[17+2], + A: data[17+3], + }, + color2: color.NRGBA{ + R: data[21+0], + G: data[21+1], + B: data[21+2], + A: data[21+3], + }, + } +} + +type clipType uint8 + +type resource interface { + release() +} + +type texture struct { + src *image.RGBA + tex driver.Texture +} + +type blitter struct { + ctx driver.Device + viewport image.Point + prog [3]*program + layout driver.InputLayout + colUniforms *blitColUniforms + texUniforms *blitTexUniforms + linearGradientUniforms *blitLinearGradientUniforms + quadVerts driver.Buffer +} + +type blitColUniforms struct { + vert struct { + blitUniforms + _ [12]byte // Padding to a multiple of 16. + } + frag struct { + colorUniforms + } +} + +type blitTexUniforms struct { + vert struct { + blitUniforms + _ [12]byte // Padding to a multiple of 16. + } +} + +type blitLinearGradientUniforms struct { + vert struct { + blitUniforms + _ [12]byte // Padding to a multiple of 16. + } + frag struct { + gradientUniforms + } +} + +type uniformBuffer struct { + buf driver.Buffer + ptr []byte +} + +type program struct { + prog driver.Program + vertUniforms *uniformBuffer + fragUniforms *uniformBuffer +} + +type blitUniforms struct { + transform [4]float32 + uvTransformR1 [4]float32 + uvTransformR2 [4]float32 + z float32 +} + +type colorUniforms struct { + color f32color.RGBA +} + +type gradientUniforms struct { + color1 f32color.RGBA + color2 f32color.RGBA +} + +type materialType uint8 + +const ( + clipTypeNone clipType = iota + clipTypePath + clipTypeIntersection +) + +const ( + materialColor materialType = iota + materialLinearGradient + materialTexture +) + +func New(api API) (GPU, error) { + d, err := driver.NewDevice(api) + if err != nil { + return nil, err + } + forceCompute := os.Getenv("GIORENDERER") == "forcecompute" + feats := d.Caps().Features + switch { + case !forceCompute && feats.Has(driver.FeatureFloatRenderTargets): + return newGPU(d) + case feats.Has(driver.FeatureCompute): + return newCompute(d) + default: + return nil, errors.New("gpu: no support for float render targets nor compute") + } +} + +func newGPU(ctx driver.Device) (*gpu, error) { + g := &gpu{ + cache: newResourceCache(), + } + g.drawOps.pathCache = newOpCache() + if err := g.init(ctx); err != nil { + return nil, err + } + return g, nil +} + +func (g *gpu) init(ctx driver.Device) error { + g.ctx = ctx + g.renderer = newRenderer(ctx) + return nil +} + +func (g *gpu) Clear(col color.NRGBA) { + g.drawOps.clear = true + g.drawOps.clearColor = f32color.LinearFromSRGB(col) +} + +func (g *gpu) Release() { + g.renderer.release() + g.drawOps.pathCache.release() + g.cache.release() + if g.timers != nil { + g.timers.release() + } + g.ctx.Release() +} + +func (g *gpu) Collect(viewport image.Point, frameOps *op.Ops) { + g.renderer.blitter.viewport = viewport + g.renderer.pather.viewport = viewport + g.drawOps.reset(g.cache, viewport) + g.drawOps.collect(g.ctx, g.cache, frameOps, viewport) + g.frameStart = time.Now() + if g.drawOps.profile && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) { + g.timers = newTimers(g.ctx) + g.zopsTimer = g.timers.newTimer() + g.stencilTimer = g.timers.newTimer() + g.coverTimer = g.timers.newTimer() + g.cleanupTimer = g.timers.newTimer() + } +} + +func (g *gpu) Frame() error { + defFBO := g.ctx.BeginFrame() + defer g.ctx.EndFrame() + viewport := g.renderer.blitter.viewport + for _, img := range g.drawOps.imageOps { + expandPathOp(img.path, img.clip) + } + if g.drawOps.profile { + g.zopsTimer.begin() + } + g.ctx.BindFramebuffer(defFBO) + g.ctx.DepthFunc(driver.DepthFuncGreater) + // Note that Clear must be before ClearDepth if nothing else is rendered + // (len(zimageOps) == 0). If not, the Fairphone 2 will corrupt the depth buffer. + if g.drawOps.clear { + g.drawOps.clear = false + g.ctx.Clear(g.drawOps.clearColor.Float32()) + } + g.ctx.ClearDepth(0.0) + g.ctx.Viewport(0, 0, viewport.X, viewport.Y) + g.renderer.drawZOps(g.cache, g.drawOps.zimageOps) + g.zopsTimer.end() + g.stencilTimer.begin() + g.ctx.SetBlend(true) + g.renderer.packStencils(&g.drawOps.pathOps) + g.renderer.stencilClips(g.drawOps.pathCache, g.drawOps.pathOps) + g.renderer.packIntersections(g.drawOps.imageOps) + g.renderer.intersect(g.drawOps.imageOps) + g.stencilTimer.end() + g.coverTimer.begin() + g.ctx.BindFramebuffer(defFBO) + g.ctx.Viewport(0, 0, viewport.X, viewport.Y) + g.renderer.drawOps(g.cache, g.drawOps.imageOps) + g.ctx.SetBlend(false) + g.renderer.pather.stenciler.invalidateFBO() + g.coverTimer.end() + g.ctx.BindFramebuffer(defFBO) + g.cleanupTimer.begin() + g.cache.frame() + g.drawOps.pathCache.frame() + g.cleanupTimer.end() + if g.drawOps.profile && g.timers.ready() { + zt, st, covt, cleant := g.zopsTimer.Elapsed, g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed + ft := zt + st + covt + cleant + q := 100 * time.Microsecond + zt, st, covt = zt.Round(q), st.Round(q), covt.Round(q) + frameDur := time.Since(g.frameStart).Round(q) + ft = ft.Round(q) + g.profile = fmt.Sprintf("draw:%7s gpu:%7s zt:%7s st:%7s cov:%7s", + frameDur, ft, zt, st, covt) + } + return nil +} + +func (g *gpu) Profile() string { + return g.profile +} + +func (r *renderer) texHandle(cache *resourceCache, + data imageOpData) driver.Texture { + var tex *texture + t, exists := cache.get(data.handle) + if !exists { + t = &texture{ + src: data.src, + } + cache.put(data.handle, t) + } + tex = t.(*texture) + if tex.tex != nil { + return tex.tex + } + handle, err := r.ctx.NewTexture(driver.TextureFormatSRGB, + data.src.Bounds().Dx(), data.src.Bounds().Dy(), driver.FilterLinear, + driver.FilterLinear, driver.BufferBindingTexture) + if err != nil { + panic(err) + } + driver.UploadImage(handle, image.Pt(0, 0), data.src) + tex.tex = handle + return tex.tex +} + +func (t *texture) release() { + if t.tex != nil { + t.tex.Release() + } +} + +func newRenderer(ctx driver.Device) *renderer { + r := &renderer{ + ctx: ctx, + blitter: newBlitter(ctx), + pather: newPather(ctx), + } + + maxDim := ctx.Caps().MaxTextureSize + // Large atlas textures cause artifacts due to precision loss in + // shaders. + if cap := 8192; maxDim > cap { + maxDim = cap + } + + r.packer.maxDim = maxDim + r.intersections.maxDim = maxDim + return r +} + +func (r *renderer) release() { + r.pather.release() + r.blitter.release() +} + +func newBlitter(ctx driver.Device) *blitter { + quadVerts, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices, + byteslice.Slice([]float32{ + -1, +1, 0, 0, + +1, +1, 1, 0, + -1, -1, 0, 1, + +1, -1, 1, 1, + }), + ) + if err != nil { + panic(err) + } + b := &blitter{ + ctx: ctx, + quadVerts: quadVerts, + } + b.colUniforms = new(blitColUniforms) + b.texUniforms = new(blitTexUniforms) + b.linearGradientUniforms = new(blitLinearGradientUniforms) + prog, layout, err := createColorPrograms(ctx, shader_blit_vert, + shader_blit_frag, + [3]interface{}{&b.colUniforms.vert, &b.linearGradientUniforms.vert, + &b.texUniforms.vert}, + [3]interface{}{&b.colUniforms.frag, &b.linearGradientUniforms.frag, + nil}, + ) + if err != nil { + panic(err) + } + b.prog = prog + b.layout = layout + return b +} + +func (b *blitter) release() { + b.quadVerts.Release() + for _, p := range b.prog { + p.Release() + } + b.layout.Release() +} + +func createColorPrograms(b driver.Device, vsSrc driver.ShaderSources, + fsSrc [3]driver.ShaderSources, + vertUniforms, fragUniforms [3]interface{}) ([3]*program, driver.InputLayout, + error) { + var progs [3]*program + { + prog, err := b.NewProgram(vsSrc, fsSrc[materialTexture]) + if err != nil { + return progs, nil, err + } + var vertBuffer, fragBuffer *uniformBuffer + if u := vertUniforms[materialTexture]; u != nil { + vertBuffer = newUniformBuffer(b, u) + prog.SetVertexUniforms(vertBuffer.buf) + } + if u := fragUniforms[materialTexture]; u != nil { + fragBuffer = newUniformBuffer(b, u) + prog.SetFragmentUniforms(fragBuffer.buf) + } + progs[materialTexture] = newProgram(prog, vertBuffer, fragBuffer) + } + { + var vertBuffer, fragBuffer *uniformBuffer + prog, err := b.NewProgram(vsSrc, fsSrc[materialColor]) + if err != nil { + progs[materialTexture].Release() + return progs, nil, err + } + if u := vertUniforms[materialColor]; u != nil { + vertBuffer = newUniformBuffer(b, u) + prog.SetVertexUniforms(vertBuffer.buf) + } + if u := fragUniforms[materialColor]; u != nil { + fragBuffer = newUniformBuffer(b, u) + prog.SetFragmentUniforms(fragBuffer.buf) + } + progs[materialColor] = newProgram(prog, vertBuffer, fragBuffer) + } + { + var vertBuffer, fragBuffer *uniformBuffer + prog, err := b.NewProgram(vsSrc, fsSrc[materialLinearGradient]) + if err != nil { + progs[materialTexture].Release() + progs[materialColor].Release() + return progs, nil, err + } + if u := vertUniforms[materialLinearGradient]; u != nil { + vertBuffer = newUniformBuffer(b, u) + prog.SetVertexUniforms(vertBuffer.buf) + } + if u := fragUniforms[materialLinearGradient]; u != nil { + fragBuffer = newUniformBuffer(b, u) + prog.SetFragmentUniforms(fragBuffer.buf) + } + progs[materialLinearGradient] = newProgram(prog, vertBuffer, fragBuffer) + } + layout, err := b.NewInputLayout(vsSrc, []driver.InputDesc{ + {Type: driver.DataTypeFloat, Size: 2, Offset: 0}, + {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2}, + }) + if err != nil { + progs[materialTexture].Release() + progs[materialColor].Release() + progs[materialLinearGradient].Release() + return progs, nil, err + } + return progs, layout, nil +} + +func (r *renderer) stencilClips(pathCache *opCache, ops []*pathOp) { + if len(r.packer.sizes) == 0 { + return + } + fbo := -1 + r.pather.begin(r.packer.sizes) + for _, p := range ops { + if fbo != p.place.Idx { + fbo = p.place.Idx + f := r.pather.stenciler.cover(fbo) + r.ctx.BindFramebuffer(f.fbo) + r.ctx.Clear(0.0, 0.0, 0.0, 0.0) + } + v, _ := pathCache.get(p.pathKey) + r.pather.stencilPath(p.clip, p.off, p.place.Pos, v.data) + } +} + +func (r *renderer) intersect(ops []imageOp) { + if len(r.intersections.sizes) == 0 { + return + } + fbo := -1 + r.pather.stenciler.beginIntersect(r.intersections.sizes) + r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0) + r.ctx.BindInputLayout(r.pather.stenciler.iprog.layout) + for _, img := range ops { + if img.clipType != clipTypeIntersection { + continue + } + if fbo != img.place.Idx { + fbo = img.place.Idx + f := r.pather.stenciler.intersections.fbos[fbo] + r.ctx.BindFramebuffer(f.fbo) + r.ctx.Clear(1.0, 0.0, 0.0, 0.0) + } + r.ctx.Viewport(img.place.Pos.X, img.place.Pos.Y, img.clip.Dx(), + img.clip.Dy()) + r.intersectPath(img.path, img.clip) + } +} + +func (r *renderer) intersectPath(p *pathOp, clip image.Rectangle) { + if p.parent != nil { + r.intersectPath(p.parent, clip) + } + if !p.path { + return + } + uv := image.Rectangle{ + Min: p.place.Pos, + Max: p.place.Pos.Add(p.clip.Size()), + } + o := clip.Min.Sub(p.clip.Min) + sub := image.Rectangle{ + Min: o, + Max: o.Add(clip.Size()), + } + fbo := r.pather.stenciler.cover(p.place.Idx) + r.ctx.BindTexture(0, fbo.tex) + coverScale, coverOff := texSpaceTransform(layout.FRect(uv), fbo.size) + subScale, subOff := texSpaceTransform(layout.FRect(sub), p.clip.Size()) + r.pather.stenciler.iprog.uniforms.vert.uvTransform = [4]float32{coverScale.X, + coverScale.Y, coverOff.X, coverOff.Y} + r.pather.stenciler.iprog.uniforms.vert.subUVTransform = [4]float32{subScale.X, + subScale.Y, subOff.X, subOff.Y} + r.pather.stenciler.iprog.prog.UploadUniforms() + r.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4) +} + +func (r *renderer) packIntersections(ops []imageOp) { + r.intersections.clear() + for i, img := range ops { + var npaths int + var onePath *pathOp + for p := img.path; p != nil; p = p.parent { + if p.path { + onePath = p + npaths++ + } + } + switch npaths { + case 0: + case 1: + place := onePath.place + place.Pos = place.Pos.Sub(onePath.clip.Min).Add(img.clip.Min) + ops[i].place = place + ops[i].clipType = clipTypePath + default: + sz := image.Point{X: img.clip.Dx(), Y: img.clip.Dy()} + place, ok := r.intersections.add(sz) + if !ok { + panic("internal error: if the intersection fit, the intersection should fit as well") + } + ops[i].clipType = clipTypeIntersection + ops[i].place = place + } + } +} + +func (r *renderer) packStencils(pops *[]*pathOp) { + r.packer.clear() + ops := *pops + // Allocate atlas space for cover textures. + var i int + for i < len(ops) { + p := ops[i] + if p.clip.Empty() { + ops[i] = ops[len(ops)-1] + ops = ops[:len(ops)-1] + continue + } + sz := image.Point{X: p.clip.Dx(), Y: p.clip.Dy()} + place, ok := r.packer.add(sz) + if !ok { + // The clip area is at most the entire screen. Hopefully no + // screen is larger than GL_MAX_TEXTURE_SIZE. + panic(fmt.Errorf("clip area %v is larger than maximum texture size %dx%d", + p.clip, r.packer.maxDim, r.packer.maxDim)) + } + p.place = place + i++ + } + *pops = ops +} + +// intersects intersects clip and b where b is offset by off. +// ceilRect returns a bounding image.Rectangle for a f32.Rectangle. +func boundRectF(r f32.Rectangle) image.Rectangle { + return image.Rectangle{ + Min: image.Point{ + X: int(floor(r.Min.X)), + Y: int(floor(r.Min.Y)), + }, + Max: image.Point{ + X: int(ceil(r.Max.X)), + Y: int(ceil(r.Max.Y)), + }, + } +} + +func ceil(v float32) int { + return int(math.Ceil(float64(v))) +} + +func floor(v float32) int { + return int(math.Floor(float64(v))) +} + +func (d *drawOps) reset(cache *resourceCache, viewport image.Point) { + d.profile = false + d.cache = cache + d.viewport = viewport + d.imageOps = d.imageOps[:0] + d.allImageOps = d.allImageOps[:0] + d.zimageOps = d.zimageOps[:0] + d.pathOps = d.pathOps[:0] + d.pathOpCache = d.pathOpCache[:0] + d.vertCache = d.vertCache[:0] +} + +func (d *drawOps) collect(ctx driver.Device, cache *resourceCache, root *op.Ops, + viewport image.Point) { + clip := f32.Rectangle{ + Max: f32.Point{X: float32(viewport.X), Y: float32(viewport.Y)}, + } + d.reader.Reset(root) + state := drawState{ + clip: clip, + rect: true, + color: color.NRGBA{A: 0xff}, + } + d.collectOps(&d.reader, state) + for _, p := range d.pathOps { + if v, exists := d.pathCache.get(p.pathKey); !exists || v.data.data == nil { + data := buildPath(ctx, p.pathVerts) + var computePath encoder + if d.compute { + computePath = encodePath(p.pathVerts) + } + d.pathCache.put(p.pathKey, opCacheValue{ + data: data, + bounds: p.bounds, + computePath: computePath, + }) + } + p.pathVerts = nil + } +} + +func (d *drawOps) newPathOp() *pathOp { + d.pathOpCache = append(d.pathOpCache, pathOp{}) + return &d.pathOpCache[len(d.pathOpCache)-1] +} + +func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, + bounds f32.Rectangle, off f32.Point, tr f32.Affine2D, + stroke clip.StrokeStyle) { + npath := d.newPathOp() + *npath = pathOp{ + parent: state.cpath, + bounds: bounds, + off: off, + trans: tr, + stroke: stroke, + } + state.cpath = npath + if len(aux) > 0 { + state.rect = false + state.cpath.pathKey = auxKey + state.cpath.path = true + state.cpath.pathVerts = aux + d.pathOps = append(d.pathOps, state.cpath) + } +} + +// split a transform into two parts, one which is pur offset and the +// other representing the scaling, shearing and rotation part +func splitTransform(t f32.Affine2D) (srs f32.Affine2D, offset f32.Point) { + sx, hx, ox, hy, sy, oy := t.Elems() + offset = f32.Point{X: ox, Y: oy} + srs = f32.NewAffine2D(sx, hx, 0, hy, sy, 0) + return +} + +func (d *drawOps) save(id int, state drawState) { + if extra := id - len(d.states) + 1; extra > 0 { + d.states = append(d.states, make([]drawState, extra)...) + } + d.states[id] = state +} + +func (d *drawOps) collectOps(r *ops.Reader, state drawState) { + var ( + quads quadsOp + str clip.StrokeStyle + z int + ) + d.save(opconst.InitialStateID, state) +loop: + for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { + switch opconst.OpType(encOp.Data[0]) { + case opconst.TypeProfile: + d.profile = true + case opconst.TypeTransform: + dop := ops.DecodeTransform(encOp.Data) + state.t = state.t.Mul(dop) + + case opconst.TypeStroke: + str = decodeStrokeOp(encOp.Data) + + case opconst.TypePath: + encOp, ok = r.Decode() + if !ok { + break loop + } + quads.aux = encOp.Data[opconst.TypeAuxLen:] + quads.key = encOp.Key + + case opconst.TypeClip: + var op clipOp + op.decode(encOp.Data) + bounds := op.bounds + trans, off := splitTransform(state.t) + if len(quads.aux) > 0 { + // There is a clipping path, build the gpu data and update the + // cache key such that it will be equal only if the transform is the + // same also. Use cached data if we have it. + quads.key = quads.key.SetTransform(trans) + if v, ok := d.pathCache.get(quads.key); ok { + // Since the GPU data exists in the cache aux will not be used. + // Why is this not used for the offset shapes? + op.bounds = v.bounds + } else { + pathData, bounds := d.buildVerts( + quads.aux, trans, op.outline, str, + ) + op.bounds = bounds + if !d.compute { + quads.aux = pathData + } + // add it to the cache, without GPU data, so the transform can be + // reused. + d.pathCache.put(quads.key, opCacheValue{bounds: op.bounds}) + } + } else { + quads.aux, op.bounds, _ = d.boundsForTransformedRect(bounds, + trans) + quads.key = encOp.Key + quads.key.SetTransform(trans) + } + state.clip = state.clip.Intersect(op.bounds.Add(off)) + d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, state.t, + str) + quads = quadsOp{} + str = clip.StrokeStyle{} + + case opconst.TypeColor: + state.matType = materialColor + state.color = decodeColorOp(encOp.Data) + case opconst.TypeLinearGradient: + state.matType = materialLinearGradient + op := decodeLinearGradientOp(encOp.Data) + state.stop1 = op.stop1 + state.stop2 = op.stop2 + state.color1 = op.color1 + state.color2 = op.color2 + case opconst.TypeImage: + state.matType = materialTexture + state.image = decodeImageOp(encOp.Data, encOp.Refs) + case opconst.TypePaint: + // Transform (if needed) the painting rectangle and if so generate a clip path, + // for those cases also compute a partialTrans that maps texture coordinates between + // the new bounding rectangle and the transformed original paint rectangle. + trans, off := splitTransform(state.t) + // Fill the clip area, unless the material is a (bounded) image. + // TODO: Find a tighter bound. + inf := float32(1e6) + dst := f32.Rect(-inf, -inf, inf, inf) + if state.matType == materialTexture { + dst = layout.FRect(state.image.src.Rect) + } + clipData, bnd, partialTrans := d.boundsForTransformedRect(dst, + trans) + cl := state.clip.Intersect(bnd.Add(off)) + if cl.Empty() { + continue + } + + wasrect := state.rect + if clipData != nil { + // The paint operation is sheared or rotated, add a clip path representing + // this transformed rectangle. + encOp.Key.SetTransform(trans) + d.addClipPath(&state, clipData, encOp.Key, bnd, off, state.t, + clip.StrokeStyle{}) + } + + bounds := boundRectF(cl) + mat := state.materialFor(bnd, off, partialTrans, bounds, state.t) + + if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && state.rect && mat.opaque && (mat.material == materialColor) { + // The image is a uniform opaque color and takes up the whole screen. + // Scrap images up to and including this image and set clear color. + d.allImageOps = d.allImageOps[:0] + d.zimageOps = d.zimageOps[:0] + d.imageOps = d.imageOps[:0] + z = 0 + d.clearColor = mat.color.Opaque() + d.clear = true + continue + } + z++ + if z != int(uint16(z)) { + // TODO(eliasnaur) realy.lol/gio/issue/127. + panic("more than 65k paint objects not supported") + } + // Assume 16-bit depth buffer. + const zdepth = 1 << 16 + // Convert z to window-space, assuming depth range [0;1]. + zf := float32(z)*2/zdepth - 1.0 + img := imageOp{ + z: zf, + path: state.cpath, + clip: bounds, + material: mat, + } + + d.allImageOps = append(d.allImageOps, img) + if state.rect && img.material.opaque { + d.zimageOps = append(d.zimageOps, img) + } else { + d.imageOps = append(d.imageOps, img) + } + if clipData != nil { + // we added a clip path that should not remain + state.cpath = state.cpath.parent + state.rect = wasrect + } + case opconst.TypeSave: + id := ops.DecodeSave(encOp.Data) + d.save(id, state) + case opconst.TypeLoad: + id, mask := ops.DecodeLoad(encOp.Data) + s := d.states[id] + if mask&opconst.TransformState != 0 { + state.t = s.t + } + if mask&^opconst.TransformState != 0 { + state = s + } + } + } +} + +func expandPathOp(p *pathOp, clip image.Rectangle) { + for p != nil { + pclip := p.clip + if !pclip.Empty() { + clip = clip.Union(pclip) + } + p.clip = clip + p = p.parent + } +} + +func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, + partTrans f32.Affine2D, clip image.Rectangle, trans f32.Affine2D) material { + var m material + switch d.matType { + case materialColor: + m.material = materialColor + m.color = f32color.LinearFromSRGB(d.color) + m.opaque = m.color.A == 1.0 + case materialLinearGradient: + m.material = materialLinearGradient + + m.color1 = f32color.LinearFromSRGB(d.color1) + m.color2 = f32color.LinearFromSRGB(d.color2) + m.opaque = m.color1.A == 1.0 && m.color2.A == 1.0 + + m.uvTrans = partTrans.Mul(gradientSpaceTransform(clip, off, d.stop1, + d.stop2)) + case materialTexture: + m.material = materialTexture + dr := boundRectF(rect.Add(off)) + sz := d.image.src.Bounds().Size() + sr := f32.Rectangle{ + Max: f32.Point{ + X: float32(sz.X), + Y: float32(sz.Y), + }, + } + dx := float32(dr.Dx()) + sdx := sr.Dx() + sr.Min.X += float32(clip.Min.X-dr.Min.X) * sdx / dx + sr.Max.X -= float32(dr.Max.X-clip.Max.X) * sdx / dx + dy := float32(dr.Dy()) + sdy := sr.Dy() + sr.Min.Y += float32(clip.Min.Y-dr.Min.Y) * sdy / dy + sr.Max.Y -= float32(dr.Max.Y-clip.Max.Y) * sdy / dy + uvScale, uvOffset := texSpaceTransform(sr, sz) + m.uvTrans = partTrans.Mul(f32.Affine2D{}.Scale(f32.Point{}, + uvScale).Offset(uvOffset)) + m.trans = trans + m.data = d.image + } + return m +} + +func (r *renderer) drawZOps(cache *resourceCache, ops []imageOp) { + r.ctx.SetDepthTest(true) + r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0) + r.ctx.BindInputLayout(r.blitter.layout) + // Render front to back. + for i := len(ops) - 1; i >= 0; i-- { + img := ops[i] + m := img.material + switch m.material { + case materialTexture: + r.ctx.BindTexture(0, r.texHandle(cache, m.data)) + } + drc := img.clip + scale, off := clipSpaceTransform(drc, r.blitter.viewport) + r.blitter.blit(img.z, m.material, m.color, m.color1, m.color2, scale, + off, m.uvTrans) + } + r.ctx.SetDepthTest(false) +} + +func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) { + r.ctx.SetDepthTest(true) + r.ctx.DepthMask(false) + r.ctx.BlendFunc(driver.BlendFactorOne, driver.BlendFactorOneMinusSrcAlpha) + r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0) + r.ctx.BindInputLayout(r.pather.coverer.layout) + var coverTex driver.Texture + for _, img := range ops { + m := img.material + switch m.material { + case materialTexture: + r.ctx.BindTexture(0, r.texHandle(cache, m.data)) + } + drc := img.clip + + scale, off := clipSpaceTransform(drc, r.blitter.viewport) + var fbo stencilFBO + switch img.clipType { + case clipTypeNone: + r.blitter.blit(img.z, m.material, m.color, m.color1, m.color2, + scale, off, m.uvTrans) + continue + case clipTypePath: + fbo = r.pather.stenciler.cover(img.place.Idx) + case clipTypeIntersection: + fbo = r.pather.stenciler.intersections.fbos[img.place.Idx] + } + if coverTex != fbo.tex { + coverTex = fbo.tex + r.ctx.BindTexture(1, coverTex) + } + uv := image.Rectangle{ + Min: img.place.Pos, + Max: img.place.Pos.Add(drc.Size()), + } + coverScale, coverOff := texSpaceTransform(layout.FRect(uv), fbo.size) + r.pather.cover(img.z, m.material, m.color, m.color1, m.color2, scale, + off, m.uvTrans, coverScale, coverOff) + } + r.ctx.DepthMask(true) + r.ctx.SetDepthTest(false) +} + +func (b *blitter) blit(z float32, mat materialType, col f32color.RGBA, + col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D) { + p := b.prog[mat] + b.ctx.BindProgram(p.prog) + var uniforms *blitUniforms + switch mat { + case materialColor: + b.colUniforms.frag.color = col + uniforms = &b.colUniforms.vert.blitUniforms + case materialTexture: + t1, t2, t3, t4, t5, t6 := uvTrans.Elems() + b.texUniforms.vert.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, + 0} + b.texUniforms.vert.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, + 0} + uniforms = &b.texUniforms.vert.blitUniforms + case materialLinearGradient: + b.linearGradientUniforms.frag.color1 = col1 + b.linearGradientUniforms.frag.color2 = col2 + + t1, t2, t3, t4, t5, t6 := uvTrans.Elems() + b.linearGradientUniforms.vert.blitUniforms.uvTransformR1 = [4]float32{t1, + t2, t3, 0} + b.linearGradientUniforms.vert.blitUniforms.uvTransformR2 = [4]float32{t4, + t5, t6, 0} + uniforms = &b.linearGradientUniforms.vert.blitUniforms + } + uniforms.z = z + uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y} + p.UploadUniforms() + b.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4) +} + +// newUniformBuffer creates a new GPU uniform buffer backed by the +// structure uniformBlock points to. +func newUniformBuffer(b driver.Device, + uniformBlock interface{}) *uniformBuffer { + ref := reflect.ValueOf(uniformBlock) + // Determine the size of the uniforms structure, *uniforms. + size := ref.Elem().Type().Size() + // Map the uniforms structure as a byte slice. + ptr := (*[1 << 30]byte)(unsafe.Pointer(ref.Pointer()))[:size:size] + ubuf, err := b.NewBuffer(driver.BufferBindingUniforms, len(ptr)) + if err != nil { + panic(err) + } + return &uniformBuffer{buf: ubuf, ptr: ptr} +} + +func (u *uniformBuffer) Upload() { + u.buf.Upload(u.ptr) +} + +func (u *uniformBuffer) Release() { + u.buf.Release() + u.buf = nil +} + +func newProgram(prog driver.Program, + vertUniforms, fragUniforms *uniformBuffer) *program { + if vertUniforms != nil { + prog.SetVertexUniforms(vertUniforms.buf) + } + if fragUniforms != nil { + prog.SetFragmentUniforms(fragUniforms.buf) + } + return &program{prog: prog, vertUniforms: vertUniforms, + fragUniforms: fragUniforms} +} + +func (p *program) UploadUniforms() { + if p.vertUniforms != nil { + p.vertUniforms.Upload() + } + if p.fragUniforms != nil { + p.fragUniforms.Upload() + } +} + +func (p *program) Release() { + p.prog.Release() + p.prog = nil + if p.vertUniforms != nil { + p.vertUniforms.Release() + p.vertUniforms = nil + } + if p.fragUniforms != nil { + p.fragUniforms.Release() + p.fragUniforms = nil + } +} + +// texSpaceTransform return the scale and offset that transforms the given subimage +// into quad texture coordinates. +func texSpaceTransform(r f32.Rectangle, bounds image.Point) (f32.Point, + f32.Point) { + size := f32.Point{X: float32(bounds.X), Y: float32(bounds.Y)} + scale := f32.Point{X: r.Dx() / size.X, Y: r.Dy() / size.Y} + offset := f32.Point{X: r.Min.X / size.X, Y: r.Min.Y / size.Y} + return scale, offset +} + +// gradientSpaceTransform transforms stop1 and stop2 to [(0,0), (1,1)]. +func gradientSpaceTransform(clip image.Rectangle, off f32.Point, + stop1, stop2 f32.Point) f32.Affine2D { + d := stop2.Sub(stop1) + l := float32(math.Sqrt(float64(d.X*d.X + d.Y*d.Y))) + a := float32(math.Atan2(float64(-d.Y), float64(d.X))) + + // TODO: optimize + zp := f32.Point{} + return f32.Affine2D{}. + Scale(zp, layout.FPt(clip.Size())). // scale to pixel space + Offset(zp.Sub(off).Add(layout.FPt(clip.Min))). // offset to clip space + Offset(zp.Sub(stop1)). // offset to first stop point + Rotate(zp, a). // rotate to align gradient + Scale(zp, f32.Pt(1/l, 1/l)) // scale gradient to right size +} + +// clipSpaceTransform returns the scale and offset that transforms the given +// rectangle from a viewport into OpenGL clip space. +func clipSpaceTransform(r image.Rectangle, viewport image.Point) (f32.Point, + f32.Point) { + // First, transform UI coordinates to OpenGL coordinates: + // + // [(-1, +1) (+1, +1)] + // [(-1, -1) (+1, -1)] + // + x, y := float32(r.Min.X), float32(r.Min.Y) + w, h := float32(r.Dx()), float32(r.Dy()) + vx, vy := 2/float32(viewport.X), 2/float32(viewport.Y) + x = x*vx - 1 + y = 1 - y*vy + w *= vx + h *= vy + + // Then, compute the transformation from the fullscreen quad to + // the rectangle at (x, y) and dimensions (w, h). + scale := f32.Point{X: w * .5, Y: h * .5} + offset := f32.Point{X: x + w*.5, Y: y - h*.5} + + return scale, offset +} + +// Fill in maximal Y coordinates of the NW and NE corners. +func fillMaxY(verts []byte) { + contour := 0 + bo := binary.LittleEndian + for len(verts) > 0 { + maxy := float32(math.Inf(-1)) + i := 0 + for ; i+vertStride*4 <= len(verts); i += vertStride * 4 { + vert := verts[i : i+vertStride] + // MaxY contains the integer contour index. + pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).MaxY)):])) + if contour != pathContour { + contour = pathContour + break + } + fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).FromY)):])) + ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).CtrlY)):])) + toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).ToY)):])) + if fromy > maxy { + maxy = fromy + } + if ctrly > maxy { + maxy = ctrly + } + if toy > maxy { + maxy = toy + } + } + fillContourMaxY(maxy, verts[:i]) + verts = verts[i:] + } +} + +func fillContourMaxY(maxy float32, verts []byte) { + bo := binary.LittleEndian + for i := 0; i < len(verts); i += vertStride { + off := int(unsafe.Offsetof(((*vertex)(nil)).MaxY)) + bo.PutUint32(verts[i+off:], math.Float32bits(maxy)) + } +} + +func (d *drawOps) writeVertCache(n int) []byte { + d.vertCache = append(d.vertCache, make([]byte, n)...) + return d.vertCache[len(d.vertCache)-n:] +} + +// transform, split paths as needed, calculate maxY, bounds and create GPU vertices. +func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, + str clip.StrokeStyle) (verts []byte, bounds f32.Rectangle) { + inf := float32(math.Inf(+1)) + d.qs.bounds = f32.Rectangle{ + Min: f32.Point{X: inf, Y: inf}, + Max: f32.Point{X: -inf, Y: -inf}, + } + d.qs.d = d + startLength := len(d.vertCache) + + switch { + case str.Width > 0: + // Stroke path. + ss := stroke.StrokeStyle{ + Width: str.Width, + Miter: str.Miter, + Cap: stroke.StrokeCap(str.Cap), + Join: stroke.StrokeJoin(str.Join), + } + quads := stroke.StrokePathCommands(ss, stroke.DashOp{}, pathData) + for _, quad := range quads { + d.qs.contour = quad.Contour + quad.Quad = quad.Quad.Transform(tr) + + d.qs.splitAndEncode(quad.Quad) + } + + case outline: + decodeToOutlineQuads(&d.qs, tr, pathData) + } + + fillMaxY(d.vertCache[startLength:]) + return d.vertCache[startLength:], d.qs.bounds +} + +// decodeOutlineQuads decodes scene commands, splits them into quadratic bĆ©ziers +// as needed and feeds them to the supplied splitter. +func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) { + for len(pathData) >= scene.CommandSize+4 { + qs.contour = bo.Uint32(pathData) + cmd := ops.DecodeCommand(pathData[4:]) + switch cmd.Op() { + case scene.OpLine: + var q stroke.QuadSegment + q.From, q.To = scene.DecodeLine(cmd) + q.Ctrl = q.From.Add(q.To).Mul(.5) + q = q.Transform(tr) + qs.splitAndEncode(q) + case scene.OpQuad: + var q stroke.QuadSegment + q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) + q = q.Transform(tr) + qs.splitAndEncode(q) + case scene.OpCubic: + for _, q := range stroke.SplitCubic(scene.DecodeCubic(cmd)) { + q = q.Transform(tr) + qs.splitAndEncode(q) + } + default: + panic("unsupported scene command") + } + pathData = pathData[scene.CommandSize+4:] + } +} + +// create GPU vertices for transformed r, find the bounds and establish texture transform. +func (d *drawOps) boundsForTransformedRect(r f32.Rectangle, + tr f32.Affine2D) (aux []byte, bnd f32.Rectangle, ptr f32.Affine2D) { + if isPureOffset(tr) { + // fast-path to allow blitting of pure rectangles + _, _, ox, _, _, oy := tr.Elems() + off := f32.Pt(ox, oy) + bnd.Min = r.Min.Add(off) + bnd.Max = r.Max.Add(off) + return + } + + // transform all corners, find new bounds + corners := [4]f32.Point{ + tr.Transform(r.Min), tr.Transform(f32.Pt(r.Max.X, r.Min.Y)), + tr.Transform(r.Max), tr.Transform(f32.Pt(r.Min.X, r.Max.Y)), + } + bnd.Min = f32.Pt(math.MaxFloat32, math.MaxFloat32) + bnd.Max = f32.Pt(-math.MaxFloat32, -math.MaxFloat32) + for _, c := range corners { + if c.X < bnd.Min.X { + bnd.Min.X = c.X + } + if c.Y < bnd.Min.Y { + bnd.Min.Y = c.Y + } + if c.X > bnd.Max.X { + bnd.Max.X = c.X + } + if c.Y > bnd.Max.Y { + bnd.Max.Y = c.Y + } + } + + // build the GPU vertices + l := len(d.vertCache) + if !d.compute { + d.vertCache = append(d.vertCache, make([]byte, vertStride*4*4)...) + aux = d.vertCache[l:] + encodeQuadTo(aux, 0, corners[0], corners[0].Add(corners[1]).Mul(0.5), + corners[1]) + encodeQuadTo(aux[vertStride*4:], 0, corners[1], + corners[1].Add(corners[2]).Mul(0.5), corners[2]) + encodeQuadTo(aux[vertStride*4*2:], 0, corners[2], + corners[2].Add(corners[3]).Mul(0.5), corners[3]) + encodeQuadTo(aux[vertStride*4*3:], 0, corners[3], + corners[3].Add(corners[0]).Mul(0.5), corners[0]) + fillMaxY(aux) + } else { + d.vertCache = append(d.vertCache, + make([]byte, (scene.CommandSize+4)*4)...) + aux = d.vertCache[l:] + buf := aux + bo := binary.LittleEndian + bo.PutUint32(buf, 0) // Contour + ops.EncodeCommand(buf[4:], scene.Line(r.Min, f32.Pt(r.Max.X, r.Min.Y))) + buf = buf[4+scene.CommandSize:] + bo.PutUint32(buf, 0) + ops.EncodeCommand(buf[4:], scene.Line(f32.Pt(r.Max.X, r.Min.Y), r.Max)) + buf = buf[4+scene.CommandSize:] + bo.PutUint32(buf, 0) + ops.EncodeCommand(buf[4:], scene.Line(r.Max, f32.Pt(r.Min.X, r.Max.Y))) + buf = buf[4+scene.CommandSize:] + bo.PutUint32(buf, 0) + ops.EncodeCommand(buf[4:], scene.Line(f32.Pt(r.Min.X, r.Max.Y), r.Min)) + } + + // establish the transform mapping from bounds rectangle to transformed corners + var P1, P2, P3 f32.Point + P1.X = (corners[1].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X) + P1.Y = (corners[1].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y) + P2.X = (corners[2].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X) + P2.Y = (corners[2].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y) + P3.X = (corners[3].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X) + P3.Y = (corners[3].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y) + sx, sy := P2.X-P3.X, P2.Y-P3.Y + ptr = f32.NewAffine2D(sx, P2.X-P1.X, P1.X-sx, sy, P2.Y-P1.Y, + P1.Y-sy).Invert() + + return +} + +func isPureOffset(t f32.Affine2D) bool { + a, b, _, d, e, _ := t.Elems() + return a == 1 && b == 0 && d == 0 && e == 1 +} diff --git a/gio/gpu/headless/driver_test.go b/gio/gpu/headless/driver_test.go new file mode 100644 index 0000000..07c0b06 --- /dev/null +++ b/gio/gpu/headless/driver_test.go @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "bytes" + "flag" + "image" + "image/color" + "image/png" + "io/ioutil" + "runtime" + "testing" + + "realy.lol/gio/gpu/internal/driver" + "realy.lol/gio/internal/byteslice" + "realy.lol/gio/internal/f32color" +) + +var dumpImages = flag.Bool("saveimages", false, "save test images") + +var clearCol = color.NRGBA{A: 0xff, R: 0xde, G: 0xad, B: 0xbe} +var clearColExpect = f32color.NRGBAToRGBA(clearCol) + +func TestFramebufferClear(t *testing.T) { + b := newDriver(t) + sz := image.Point{X: 800, Y: 600} + fbo := setupFBO(t, b, sz) + img := screenshot(t, b, fbo, sz) + if got := img.RGBAAt(0, 0); got != clearColExpect { + t.Errorf("got color %v, expected %v", got, clearColExpect) + } +} + +func TestSimpleShader(t *testing.T) { + b := newDriver(t) + sz := image.Point{X: 800, Y: 600} + fbo := setupFBO(t, b, sz) + p, err := b.NewProgram(shader_simple_vert, shader_simple_frag) + if err != nil { + t.Fatal(err) + } + defer p.Release() + b.BindProgram(p) + b.DrawArrays(driver.DrawModeTriangles, 0, 3) + img := screenshot(t, b, fbo, sz) + if got := img.RGBAAt(0, 0); got != clearColExpect { + t.Errorf("got color %v, expected %v", got, clearColExpect) + } + // Just off the center to catch inverted triangles. + cx, cy := 300, 400 + shaderCol := f32color.RGBA{R: .25, G: .55, B: .75, A: 1.0} + if got, exp := img.RGBAAt(cx, + cy), shaderCol.SRGB(); got != f32color.NRGBAToRGBA(exp) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(exp)) + } +} + +func TestInputShader(t *testing.T) { + b := newDriver(t) + sz := image.Point{X: 800, Y: 600} + fbo := setupFBO(t, b, sz) + p, err := b.NewProgram(shader_input_vert, shader_simple_frag) + if err != nil { + t.Fatal(err) + } + defer p.Release() + b.BindProgram(p) + buf, err := b.NewImmutableBuffer(driver.BufferBindingVertices, + byteslice.Slice([]float32{ + 0, .5, .5, 1, + -.5, -.5, .5, 1, + .5, -.5, .5, 1, + }), + ) + if err != nil { + t.Fatal(err) + } + defer buf.Release() + b.BindVertexBuffer(buf, 4*4, 0) + layout, err := b.NewInputLayout(shader_input_vert, []driver.InputDesc{ + { + Type: driver.DataTypeFloat, + Size: 4, + Offset: 0, + }, + }) + if err != nil { + t.Fatal(err) + } + defer layout.Release() + b.BindInputLayout(layout) + b.DrawArrays(driver.DrawModeTriangles, 0, 3) + img := screenshot(t, b, fbo, sz) + if got := img.RGBAAt(0, 0); got != clearColExpect { + t.Errorf("got color %v, expected %v", got, clearColExpect) + } + cx, cy := 300, 400 + shaderCol := f32color.RGBA{R: .25, G: .55, B: .75, A: 1.0} + if got, exp := img.RGBAAt(cx, + cy), shaderCol.SRGB(); got != f32color.NRGBAToRGBA(exp) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(exp)) + } +} + +func TestFramebuffers(t *testing.T) { + b := newDriver(t) + sz := image.Point{X: 800, Y: 600} + fbo1 := newFBO(t, b, sz) + fbo2 := newFBO(t, b, sz) + var ( + col1 = color.NRGBA{R: 0xac, G: 0xbd, B: 0xef, A: 0xde} + col2 = color.NRGBA{R: 0xfe, G: 0xba, B: 0xbe, A: 0xca} + ) + fcol1, fcol2 := f32color.LinearFromSRGB(col1), f32color.LinearFromSRGB(col2) + b.BindFramebuffer(fbo1) + b.Clear(fcol1.Float32()) + b.BindFramebuffer(fbo2) + b.Clear(fcol2.Float32()) + img := screenshot(t, b, fbo1, sz) + if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col1) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col1)) + } + img = screenshot(t, b, fbo2, sz) + if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col2) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col2)) + } +} + +func setupFBO(t *testing.T, b driver.Device, + size image.Point) driver.Framebuffer { + fbo := newFBO(t, b, size) + b.BindFramebuffer(fbo) + // ClearColor accepts linear RGBA colors, while 8-bit colors + // are in the sRGB color space. + col := f32color.LinearFromSRGB(clearCol) + b.Clear(col.Float32()) + b.ClearDepth(0.0) + b.Viewport(0, 0, size.X, size.Y) + return fbo +} + +func newFBO(t *testing.T, b driver.Device, + size image.Point) driver.Framebuffer { + fboTex, err := b.NewTexture( + driver.TextureFormatSRGB, + size.X, size.Y, + driver.FilterNearest, driver.FilterNearest, + driver.BufferBindingFramebuffer, + ) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + fboTex.Release() + }) + const depthBits = 16 + fbo, err := b.NewFramebuffer(fboTex, depthBits) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + fbo.Release() + }) + return fbo +} + +func newDriver(t *testing.T) driver.Device { + ctx, err := newContext() + if err != nil { + t.Skipf("no context available: %v", err) + } + runtime.LockOSThread() + if err := ctx.MakeCurrent(); err != nil { + t.Fatal(err) + } + b, err := driver.NewDevice(ctx.API()) + if err != nil { + t.Fatal(err) + } + b.BeginFrame() + t.Cleanup(func() { + b.EndFrame() + ctx.ReleaseCurrent() + runtime.UnlockOSThread() + ctx.Release() + }) + return b +} + +func screenshot(t *testing.T, d driver.Device, fbo driver.Framebuffer, + size image.Point) *image.RGBA { + img, err := driver.DownloadImage(d, fbo, image.Rectangle{Max: size}) + if err != nil { + t.Fatal(err) + } + if *dumpImages { + if err := saveImage(t.Name()+".png", img); err != nil { + t.Error(err) + } + } + return img +} + +func saveImage(file string, img image.Image) error { + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return err + } + return ioutil.WriteFile(file, buf.Bytes(), 0666) +} diff --git a/gio/gpu/headless/gen.go b/gio/gpu/headless/gen.go new file mode 100644 index 0000000..b9e1fed --- /dev/null +++ b/gio/gpu/headless/gen.go @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +//go:generate go run ../internal/convertshaders -package headless diff --git a/gio/gpu/headless/headless.go b/gio/gpu/headless/headless.go new file mode 100644 index 0000000..0f2e172 --- /dev/null +++ b/gio/gpu/headless/headless.go @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package headless implements headless windows for rendering +// an operation list to an image. +package headless + +import ( + "image" + "image/color" + "runtime" + + "realy.lol/gio/gpu" + "realy.lol/gio/gpu/internal/driver" + "realy.lol/gio/op" +) + +// Window is a headless window. +type Window struct { + size image.Point + ctx context + dev driver.Device + gpu gpu.GPU + fboTex driver.Texture + fbo driver.Framebuffer +} + +type context interface { + API() gpu.API + MakeCurrent() error + ReleaseCurrent() + Release() +} + +// NewWindow creates a new headless window. +func NewWindow(width, height int) (*Window, error) { + ctx, err := newContext() + if err != nil { + return nil, err + } + w := &Window{ + size: image.Point{X: width, Y: height}, + ctx: ctx, + } + err = contextDo(ctx, func() error { + api := ctx.API() + dev, err := driver.NewDevice(api) + if err != nil { + return err + } + dev.Viewport(0, 0, width, height) + fboTex, err := dev.NewTexture( + driver.TextureFormatSRGB, + width, height, + driver.FilterNearest, driver.FilterNearest, + driver.BufferBindingFramebuffer, + ) + if err != nil { + return nil + } + const depthBits = 16 + fbo, err := dev.NewFramebuffer(fboTex, depthBits) + if err != nil { + fboTex.Release() + return err + } + gp, err := gpu.New(api) + if err != nil { + fbo.Release() + fboTex.Release() + return err + } + w.fboTex = fboTex + w.fbo = fbo + w.gpu = gp + w.dev = dev + return err + }) + if err != nil { + ctx.Release() + return nil, err + } + return w, nil +} + +// Release resources associated with the window. +func (w *Window) Release() { + contextDo(w.ctx, func() error { + if w.fbo != nil { + w.fbo.Release() + w.fbo = nil + } + if w.fboTex != nil { + w.fboTex.Release() + w.fboTex = nil + } + if w.gpu != nil { + w.gpu.Release() + w.gpu = nil + } + return nil + }) + if w.ctx != nil { + w.ctx.Release() + w.ctx = nil + } +} + +// Frame replace the window content and state with the +// operation list. +func (w *Window) Frame(frame *op.Ops) error { + return contextDo(w.ctx, func() error { + w.dev.BindFramebuffer(w.fbo) + w.gpu.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}) + w.gpu.Collect(w.size, frame) + return w.gpu.Frame() + }) +} + +// Screenshot returns an image with the content of the window. +func (w *Window) Screenshot() (*image.RGBA, error) { + var img *image.RGBA + err := contextDo(w.ctx, func() error { + var err error + img, err = driver.DownloadImage(w.dev, w.fbo, + image.Rectangle{Max: w.size}) + return err + }) + if err != nil { + return nil, err + } + return img, nil +} + +func contextDo(ctx context, f func() error) error { + errCh := make(chan error) + go func() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if err := ctx.MakeCurrent(); err != nil { + errCh <- err + return + } + err := f() + ctx.ReleaseCurrent() + errCh <- err + }() + return <-errCh +} diff --git a/gio/gpu/headless/headless_darwin.go b/gio/gpu/headless/headless_darwin.go new file mode 100644 index 0000000..75a233a --- /dev/null +++ b/gio/gpu/headless/headless_darwin.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "realy.lol/gio/gpu" + _ "realy.lol/gio/internal/cocoainit" +) + +/* +#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c + +#include + +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_headless_newContext(void); +__attribute__ ((visibility ("hidden"))) void gio_headless_releaseContext(CFTypeRef ctxRef); +__attribute__ ((visibility ("hidden"))) void gio_headless_clearCurrentContext(CFTypeRef ctxRef); +__attribute__ ((visibility ("hidden"))) void gio_headless_makeCurrentContext(CFTypeRef ctxRef); +__attribute__ ((visibility ("hidden"))) void gio_headless_prepareContext(CFTypeRef ctxRef); +*/ +import "C" + +type nsContext struct { + ctx C.CFTypeRef + prepared bool +} + +func newGLContext() (context, error) { + ctx := C.gio_headless_newContext() + return &nsContext{ctx: ctx}, nil +} + +func (c *nsContext) API() gpu.API { + return gpu.OpenGL{} +} + +func (c *nsContext) MakeCurrent() error { + C.gio_headless_makeCurrentContext(c.ctx) + if !c.prepared { + C.gio_headless_prepareContext(c.ctx) + c.prepared = true + } + return nil +} + +func (c *nsContext) ReleaseCurrent() { + C.gio_headless_clearCurrentContext(c.ctx) +} + +func (d *nsContext) Release() { + if d.ctx != 0 { + C.gio_headless_releaseContext(d.ctx) + d.ctx = 0 + } +} diff --git a/gio/gpu/headless/headless_egl.go b/gio/gpu/headless/headless_egl.go new file mode 100644 index 0000000..7d8c1e4 --- /dev/null +++ b/gio/gpu/headless/headless_egl.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build linux || freebsd || windows || openbsd +// +build linux freebsd windows openbsd + +package headless + +import ( + "realy.lol/gio/internal/egl" +) + +func newGLContext() (context, error) { + return egl.NewContext(egl.EGL_DEFAULT_DISPLAY) +} diff --git a/gio/gpu/headless/headless_gl.go b/gio/gpu/headless/headless_gl.go new file mode 100644 index 0000000..c00083e --- /dev/null +++ b/gio/gpu/headless/headless_gl.go @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build !windows + +package headless + +func newContext() (context, error) { + return newGLContext() +} diff --git a/gio/gpu/headless/headless_ios.m b/gio/gpu/headless/headless_ios.m new file mode 100644 index 0000000..fd72d25 --- /dev/null +++ b/gio/gpu/headless/headless_ios.m @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,ios + +@import OpenGLES; + +#include +#include "_cgo_export.h" + +void gio_headless_releaseContext(CFTypeRef ctxRef) { + CFBridgingRelease(ctxRef); +} + +CFTypeRef gio_headless_newContext(void) { + EAGLContext *ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; + if (ctx == nil) { + return nil; + } + return CFBridgingRetain(ctx); +} + +void gio_headless_clearCurrentContext(CFTypeRef ctxRef) { + [EAGLContext setCurrentContext:nil]; +} + +void gio_headless_makeCurrentContext(CFTypeRef ctxRef) { + EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; + [EAGLContext setCurrentContext:ctx]; +} + +void gio_headless_prepareContext(CFTypeRef ctxRef) { +} diff --git a/gio/gpu/headless/headless_js.go b/gio/gpu/headless/headless_js.go new file mode 100644 index 0000000..f79963e --- /dev/null +++ b/gio/gpu/headless/headless_js.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "errors" + "syscall/js" + + "realy.lol/gio/gpu" + "realy.lol/gio/internal/gl" +) + +type jsContext struct { + ctx js.Value +} + +func newGLContext() (context, error) { + doc := js.Global().Get("document") + cnv := doc.Call("createElement", "canvas") + ctx := cnv.Call("getContext", "webgl2") + if ctx.IsNull() { + ctx = cnv.Call("getContext", "webgl") + } + if ctx.IsNull() { + return nil, errors.New("headless: webgl is not supported") + } + c := &jsContext{ + ctx: ctx, + } + return c, nil +} + +func (c *jsContext) API() gpu.API { + return gpu.OpenGL{Context: gl.Context(c.ctx)} +} + +func (c *jsContext) Release() { +} + +func (c *jsContext) ReleaseCurrent() { +} + +func (c *jsContext) MakeCurrent() error { + return nil +} diff --git a/gio/gpu/headless/headless_macos.m b/gio/gpu/headless/headless_macos.m new file mode 100644 index 0000000..46deb37 --- /dev/null +++ b/gio/gpu/headless/headless_macos.m @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,!ios + +@import AppKit; +@import OpenGL; +@import OpenGL.GL; +@import OpenGL.GL3; + +#include +#include "_cgo_export.h" + +void gio_headless_releaseContext(CFTypeRef ctxRef) { + CFBridgingRelease(ctxRef); +} + +CFTypeRef gio_headless_newContext(void) { + NSOpenGLPixelFormatAttribute attr[] = { + NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, + NSOpenGLPFAColorSize, 24, + NSOpenGLPFAAccelerated, + // Opt-in to automatic GPU switching. CGL-only property. + kCGLPFASupportsAutomaticGraphicsSwitching, + NSOpenGLPFAAllowOfflineRenderers, + 0 + }; + NSOpenGLPixelFormat *pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr]; + if (pixFormat == nil) { + return NULL; + } + NSOpenGLContext *ctx = [[NSOpenGLContext alloc] initWithFormat:pixFormat shareContext:nil]; + return CFBridgingRetain(ctx); +} + +void gio_headless_clearCurrentContext(CFTypeRef ctxRef) { + NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; + CGLUnlockContext([ctx CGLContextObj]); + [NSOpenGLContext clearCurrentContext]; +} + +void gio_headless_makeCurrentContext(CFTypeRef ctxRef) { + NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; + [ctx makeCurrentContext]; + CGLLockContext([ctx CGLContextObj]); +} + +void gio_headless_prepareContext(CFTypeRef ctxRef) { + // Bind a default VBA to emulate OpenGL ES 2. + GLuint defVBA; + glGenVertexArrays(1, &defVBA); + glBindVertexArray(defVBA); + glEnable(GL_FRAMEBUFFER_SRGB); +} diff --git a/gio/gpu/headless/headless_test.go b/gio/gpu/headless/headless_test.go new file mode 100644 index 0000000..3ceec3f --- /dev/null +++ b/gio/gpu/headless/headless_test.go @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "image" + "image/color" + "testing" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" +) + +func TestHeadless(t *testing.T) { + w, release := newTestWindow(t) + defer release() + + sz := w.size + col := color.NRGBA{A: 0xff, R: 0xca, G: 0xfe} + var ops op.Ops + paint.ColorOp{Color: col}.Add(&ops) + // Paint only part of the screen to avoid the glClear optimization. + paint.FillShape(&ops, col, + clip.Rect(image.Rect(0, 0, sz.X-100, sz.Y-100)).Op()) + if err := w.Frame(&ops); err != nil { + t.Fatal(err) + } + + img, err := w.Screenshot() + if err != nil { + t.Fatal(err) + } + if isz := img.Bounds().Size(); isz != sz { + t.Errorf("got %v screenshot, expected %v", isz, sz) + } + if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col)) + } +} + +func TestClipping(t *testing.T) { + w, release := newTestWindow(t) + defer release() + + col := color.NRGBA{A: 0xff, R: 0xca, G: 0xfe} + col2 := color.NRGBA{A: 0xff, R: 0x00, G: 0xfe} + var ops op.Ops + paint.ColorOp{Color: col}.Add(&ops) + clip.RRect{ + Rect: f32.Rectangle{ + Min: f32.Point{X: 50, Y: 50}, + Max: f32.Point{X: 250, Y: 250}, + }, + SE: 75, + }.Add(&ops) + paint.PaintOp{}.Add(&ops) + paint.ColorOp{Color: col2}.Add(&ops) + clip.RRect{ + Rect: f32.Rectangle{ + Min: f32.Point{X: 100, Y: 100}, + Max: f32.Point{X: 350, Y: 350}, + }, + NW: 75, + }.Add(&ops) + paint.PaintOp{}.Add(&ops) + if err := w.Frame(&ops); err != nil { + t.Fatal(err) + } + + img, err := w.Screenshot() + if err != nil { + t.Fatal(err) + } + if *dumpImages { + if err := saveImage("clip.png", img); err != nil { + t.Fatal(err) + } + } + bg := color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff} + tests := []struct { + x, y int + color color.NRGBA + }{ + {120, 120, col}, + {130, 130, col2}, + {210, 210, col2}, + {230, 230, bg}, + } + for _, test := range tests { + if got := img.RGBAAt(test.x, + test.y); got != f32color.NRGBAToRGBA(test.color) { + t.Errorf("(%d,%d): got color %v, expected %v", test.x, test.y, got, + f32color.NRGBAToRGBA(test.color)) + } + } +} + +func TestDepth(t *testing.T) { + w, release := newTestWindow(t) + defer release() + var ops op.Ops + + blue := color.NRGBA{B: 0xFF, A: 0xFF} + paint.FillShape(&ops, blue, clip.Rect(image.Rect(0, 0, 50, 100)).Op()) + red := color.NRGBA{R: 0xFF, A: 0xFF} + paint.FillShape(&ops, red, clip.Rect(image.Rect(0, 0, 100, 50)).Op()) + if err := w.Frame(&ops); err != nil { + t.Fatal(err) + } + + img, err := w.Screenshot() + if err != nil { + t.Fatal(err) + } + if *dumpImages { + if err := saveImage("depth.png", img); err != nil { + t.Fatal(err) + } + } + tests := []struct { + x, y int + color color.NRGBA + }{ + {25, 25, red}, + {75, 25, red}, + {25, 75, blue}, + } + for _, test := range tests { + if got := img.RGBAAt(test.x, + test.y); got != f32color.NRGBAToRGBA(test.color) { + t.Errorf("(%d,%d): got color %v, expected %v", test.x, test.y, got, + f32color.NRGBAToRGBA(test.color)) + } + } +} + +func newTestWindow(t *testing.T) (*Window, func()) { + t.Helper() + sz := image.Point{X: 800, Y: 600} + w, err := NewWindow(sz.X, sz.Y) + if err != nil { + t.Skipf("headless windows not supported: %v", err) + } + return w, func() { + w.Release() + } +} diff --git a/gio/gpu/headless/headless_windows.go b/gio/gpu/headless/headless_windows.go new file mode 100644 index 0000000..bd42d12 --- /dev/null +++ b/gio/gpu/headless/headless_windows.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "unsafe" + + "realy.lol/gio/gpu" + "realy.lol/gio/internal/d3d11" +) + +type d3d11Context struct { + dev *d3d11.Device +} + +func newContext() (context, error) { + dev, ctx, _, err := d3d11.CreateDevice( + d3d11.DRIVER_TYPE_HARDWARE, + 0, + ) + if err != nil { + return nil, err + } + // Don't need it. + d3d11.IUnknownRelease(unsafe.Pointer(ctx), ctx.Vtbl.Release) + return &d3d11Context{dev: dev}, nil +} + +func (c *d3d11Context) API() gpu.API { + return gpu.Direct3D11{Device: unsafe.Pointer(c.dev)} +} + +func (c *d3d11Context) MakeCurrent() error { + return nil +} + +func (c *d3d11Context) ReleaseCurrent() { +} + +func (c *d3d11Context) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release) + c.dev = nil +} diff --git a/gio/gpu/headless/shaders.go b/gio/gpu/headless/shaders.go new file mode 100644 index 0000000..95e05b2 --- /dev/null +++ b/gio/gpu/headless/shaders.go @@ -0,0 +1,233 @@ +// Code generated by build.go. DO NOT EDIT. + +package headless + +import "realy.lol/gio/gpu/internal/driver" + +var ( + shader_input_vert = driver.ShaderSources{ + Name: "input.vert", + Inputs: []driver.InputLocation{{Name: "position", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 4}}, + GLSL100ES: `#version 100 + +attribute vec4 position; + +void main() +{ + gl_Position = position; +} + +`, + GLSL300ES: `#version 300 es + +layout(location = 0) in vec4 position; + +void main() +{ + gl_Position = position; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +in vec4 position; + +void main() +{ + gl_Position = position; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +in vec4 position; + +void main() +{ + gl_Position = position; +} + +`, + HLSL: "DXBC\x1eĀ»\x11\xd3iX7\xd4F\xb9\xa4\xf4R\xf9J\x01\x00\x00\x00\x10\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\x9c\x00\x00\x00\xe0\x00\x00\x00\\\x01\x00\x00\xa8\x01\x00\x00\xdc\x01\x00\x00Aon9\\\x00\x00\x00\\\x00\x00\x00\x00\x02\xfe\xff4\x00\x00\x00(\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x01\x00$\x00\x00\x00\x00\x00\x00\x02\xfe\xff\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x04\x00\x00\x04\x00\x00\x03\xc0\x00\x00\xff\x90\x00\x00\xe4\xa0\x00\x00\xe4\x90\x01\x00\x00\x02\x00\x00\f\xc0\x00\x00\xe4\x90\xff\xff\x00\x00SHDR<\x00\x00\x00@\x00\x01\x00\x0f\x00\x00\x00_\x00\x00\x03\xf2\x10\x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x006\x00\x00\x05\xf2 \x10\x00\x00\x00\x00\x00F\x1e\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x0f\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00", + } + shader_simple_frag = driver.ShaderSources{ + Name: "simple.frag", + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +void main() +{ + gl_FragData[0] = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0); +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(location = 0) out vec4 fragColor; + +void main() +{ + fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +out vec4 fragColor; + +void main() +{ + fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +out vec4 fragColor; + +void main() +{ + fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0); +} + +`, + HLSL: "DXBC\xf5F\xdef$)\xa8\xbbV\xeas\xb5ks\x12r\x01\x00\x00\x00\xdc\x01\x00\x00\x06\x00\x00\x008\x00\x00\x00\x90\x00\x00\x00\xd0\x00\x00\x00L\x01\x00\x00\x98\x01\x00\x00\xa8\x01\x00\x00Aon9P\x00\x00\x00P\x00\x00\x00\x00\x02\xff\xff,\x00\x00\x00$\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x80>\xcd\xcc\f?\x00\x00@?\x00\x00\x80?\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\xa0\xff\xff\x00\x00SHDR8\x00\x00\x00@\x00\x00\x00\x0e\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x006\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80>\xcd\xcc\f?\x00\x00@?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\b\x00\x00\x00\x00\x00\x00\x00\b\x00\x00\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + } + shader_simple_vert = driver.ShaderSources{ + Name: "simple.vert", + GLSL100ES: `#version 100 + +void main() +{ + float x; + float y; + if (gl_VertexID == 0) + { + x = 0.0; + y = 0.5; + } + else + { + if (gl_VertexID == 1) + { + x = 0.5; + y = -0.5; + } + else + { + x = -0.5; + y = -0.5; + } + } + gl_Position = vec4(x, y, 0.5, 1.0); +} + +`, + GLSL300ES: `#version 300 es + +void main() +{ + float x; + float y; + if (gl_VertexID == 0) + { + x = 0.0; + y = 0.5; + } + else + { + if (gl_VertexID == 1) + { + x = 0.5; + y = -0.5; + } + else + { + x = -0.5; + y = -0.5; + } + } + gl_Position = vec4(x, y, 0.5, 1.0); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +void main() +{ + float x; + float y; + if (gl_VertexID == 0) + { + x = 0.0; + y = 0.5; + } + else + { + if (gl_VertexID == 1) + { + x = 0.5; + y = -0.5; + } + else + { + x = -0.5; + y = -0.5; + } + } + gl_Position = vec4(x, y, 0.5, 1.0); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +void main() +{ + float x; + float y; + if (gl_VertexID == 0) + { + x = 0.0; + y = 0.5; + } + else + { + if (gl_VertexID == 1) + { + x = 0.5; + y = -0.5; + } + else + { + x = -0.5; + y = -0.5; + } + } + gl_Position = vec4(x, y, 0.5, 1.0); +} + +`, + HLSL: "DXBC\xc8 \\\"\xec\xe9\xb2)@\xdf|Z(\xea\f\xb8\x01\x00\x00\x00H\x02\x00\x00\x05\x00\x00\x004\x00\x00\x00\x80\x00\x00\x00\xb4\x00\x00\x00\xe8\x00\x00\x00\xcc\x01\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00SV_VertexID\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00SHDR\xdc\x00\x00\x00@\x00\x01\x007\x00\x00\x00`\x00\x00\x04\x12\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00 \x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x01\x00\x00\x007\x00\x00\x0f2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x007\x00\x00\f2 \x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x05\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + } +) diff --git a/gio/gpu/headless/shaders/input.vert b/gio/gpu/headless/shaders/input.vert new file mode 100644 index 0000000..ed9a4bd --- /dev/null +++ b/gio/gpu/headless/shaders/input.vert @@ -0,0 +1,11 @@ +#version 310 es + +// SPDX-License-Identifier: Unlicense OR MIT + +precision highp float; + +layout(location=0) in vec4 position; + +void main() { + gl_Position = position; +} diff --git a/gio/gpu/headless/shaders/simple.frag b/gio/gpu/headless/shaders/simple.frag new file mode 100644 index 0000000..4614f33 --- /dev/null +++ b/gio/gpu/headless/shaders/simple.frag @@ -0,0 +1,11 @@ +#version 310 es + +// SPDX-License-Identifier: Unlicense OR MIT + +precision mediump float; + +layout(location = 0) out vec4 fragColor; + +void main() { + fragColor = vec4(.25, .55, .75, 1.0); +} diff --git a/gio/gpu/headless/shaders/simple.vert b/gio/gpu/headless/shaders/simple.vert new file mode 100644 index 0000000..a226816 --- /dev/null +++ b/gio/gpu/headless/shaders/simple.vert @@ -0,0 +1,20 @@ +#version 310 es + +// SPDX-License-Identifier: Unlicense OR MIT + +precision highp float; + +void main() { + float x, y; + if (gl_VertexIndex == 0) { + x = 0.0; + y = .5; + } else if (gl_VertexIndex == 1) { + x = .5; + y = -.5; + } else { + x = -.5; + y = -.5; + } + gl_Position = vec4(x, y, 0.5, 1.0); +} diff --git a/gio/gpu/internal/convertshaders/glslvalidate.go b/gio/gpu/internal/convertshaders/glslvalidate.go new file mode 100644 index 0000000..0d02a29 --- /dev/null +++ b/gio/gpu/internal/convertshaders/glslvalidate.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os/exec" + "path/filepath" +) + +// GLSLValidator is OpenGL reference compiler. +type GLSLValidator struct { + Bin string + WorkDir WorkDir +} + +func NewGLSLValidator() *GLSLValidator { return &GLSLValidator{Bin: "glslangValidator"} } + +// Convert converts a glsl shader to spirv. +func (glsl *GLSLValidator) Convert(path, variant string, hlsl bool, input []byte) ([]byte, error) { + base := glsl.WorkDir.Path(filepath.Base(path), variant) + pathout := base + ".out" + + cmd := exec.Command(glsl.Bin, + "--stdin", + "-I"+filepath.Dir(path), + "-V", // OpenGL ES 3.1. + "-w", // Suppress warnings. + "-S", filepath.Ext(path)[1:], + "-o", pathout, + ) + if hlsl { + cmd.Args = append(cmd.Args, "-DHLSL") + } + cmd.Stdin = bytes.NewBuffer(input) + + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("%s\nfailed to run %v: %w", out, cmd.Args, err) + } + + compiled, err := ioutil.ReadFile(pathout) + if err != nil { + return nil, fmt.Errorf("unable to read output %q: %w", pathout, err) + } + + return compiled, nil +} diff --git a/gio/gpu/internal/convertshaders/hlsl.go b/gio/gpu/internal/convertshaders/hlsl.go new file mode 100644 index 0000000..a007925 --- /dev/null +++ b/gio/gpu/internal/convertshaders/hlsl.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +// FXC is hlsl compiler that targets ShaderModel 5.x and lower. +type FXC struct { + Bin string + WorkDir WorkDir +} + +func NewFXC() *FXC { return &FXC{Bin: "fxc.exe"} } + +// Compile compiles the input shader. +func (fxc *FXC) Compile(path, variant string, input []byte, entryPoint string, profileVersion string) (string, error) { + base := fxc.WorkDir.Path(filepath.Base(path), variant, profileVersion) + pathin := base + ".in" + pathout := base + ".out" + result := pathout + + if err := fxc.WorkDir.WriteFile(pathin, input); err != nil { + return "", fmt.Errorf("unable to write shader to disk: %w", err) + } + + cmd := exec.Command(fxc.Bin) + if runtime.GOOS != "windows" { + cmd = exec.Command("wine", fxc.Bin) + if err := winepath(&pathin, &pathout); err != nil { + return "", err + } + } + + var profile string + switch filepath.Ext(path) { + case ".frag": + profile = "ps_" + profileVersion + case ".vert": + profile = "vs_" + profileVersion + case ".comp": + profile = "cs_" + profileVersion + default: + return "", fmt.Errorf("unrecognized shader type %s", path) + } + + cmd.Args = append(cmd.Args, + "/Fo", pathout, + "/T", profile, + "/E", entryPoint, + pathin, + ) + + output, err := cmd.CombinedOutput() + if err != nil { + info := "" + if runtime.GOOS != "windows" { + info = "If the fxc tool cannot be found, set WINEPATH to the Windows path for the Windows SDK.\n" + } + return "", fmt.Errorf("%s\n%sfailed to run %v: %w", output, info, cmd.Args, err) + } + + compiled, err := ioutil.ReadFile(result) + if err != nil { + return "", fmt.Errorf("unable to read output %q: %w", pathout, err) + } + + return string(compiled), nil +} + +// DXC is hlsl compiler that targets ShaderModel 6.0 and newer. +type DXC struct { + Bin string + WorkDir WorkDir +} + +func NewDXC() *DXC { return &DXC{Bin: "dxc"} } + +// Compile compiles the input shader. +func (dxc *DXC) Compile(path, variant string, input []byte, entryPoint string, profile string) (string, error) { + base := dxc.WorkDir.Path(filepath.Base(path), variant, profile) + pathin := base + ".in" + pathout := base + ".out" + result := pathout + + if err := dxc.WorkDir.WriteFile(pathin, input); err != nil { + return "", fmt.Errorf("unable to write shader to disk: %w", err) + } + + cmd := exec.Command(dxc.Bin) + + cmd.Args = append(cmd.Args, + "-Fo", pathout, + "-T", profile, + "-E", entryPoint, + "-Qstrip_reflect", + pathin, + ) + + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("%s\nfailed to run %v: %w", output, cmd.Args, err) + } + + compiled, err := ioutil.ReadFile(result) + if err != nil { + return "", fmt.Errorf("unable to read output %q: %w", pathout, err) + } + + return string(compiled), nil +} + +// winepath uses the winepath tool to convert a paths to Windows format. +// The returned path can be used as arguments for Windows command line tools. +func winepath(paths ...*string) error { + winepath := exec.Command("winepath", "--windows") + for _, path := range paths { + winepath.Args = append(winepath.Args, *path) + } + // Use a pipe instead of Output, because winepath may have left wineserver + // running for several seconds as a grandchild. + out, err := winepath.StdoutPipe() + if err != nil { + return fmt.Errorf("unable to start winepath: %w", err) + } + if err := winepath.Start(); err != nil { + return fmt.Errorf("unable to start winepath: %w", err) + } + var buf bytes.Buffer + if _, err := io.Copy(&buf, out); err != nil { + return fmt.Errorf("unable to run winepath: %w", err) + } + winPaths := strings.Split(strings.TrimSpace(buf.String()), "\n") + for i, path := range paths { + *path = winPaths[i] + } + return nil +} diff --git a/gio/gpu/internal/convertshaders/main.go b/gio/gpu/internal/convertshaders/main.go new file mode 100644 index 0000000..a0589dc --- /dev/null +++ b/gio/gpu/internal/convertshaders/main.go @@ -0,0 +1,436 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "text/template" + + "realy.lol/gio/gpu/internal/driver" +) + +func main() { + packageName := flag.String("package", "", "specify Go package name") + workdir := flag.String("work", "", + "temporary working directory (default TEMP)") + shadersDir := flag.String("dir", "shaders", "shaders directory") + directCompute := flag.Bool("directcompute", false, + "enable compiling DirectCompute shaders") + + flag.Parse() + + var work WorkDir + cleanup := func() {} + if *workdir == "" { + tempdir, err := ioutil.TempDir("", "shader-convert") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to create tempdir: %v\n", err) + os.Exit(1) + } + cleanup = func() { os.RemoveAll(tempdir) } + defer cleanup() + + work = WorkDir(tempdir) + } else { + if abs, err := filepath.Abs(*workdir); err == nil { + *workdir = abs + } + work = WorkDir(*workdir) + } + + var out bytes.Buffer + conv := NewConverter(work, *packageName, *shadersDir, *directCompute) + if err := conv.Run(&out); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + cleanup() + os.Exit(1) + } + + if err := ioutil.WriteFile("shaders.go", out.Bytes(), 0644); err != nil { + fmt.Fprintf(os.Stderr, "failed to create shaders: %v\n", err) + cleanup() + os.Exit(1) + } + + cmd := exec.Command("gofmt", "-s", "-w", "shaders.go") + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "formatting shaders.go failed: %v\n", err) + cleanup() + os.Exit(1) + } +} + +type Converter struct { + workDir WorkDir + shadersDir string + directCompute bool + + packageName string + + glslvalidator *GLSLValidator + spirv *SPIRVCross + fxc *FXC +} + +func NewConverter(workDir WorkDir, packageName, shadersDir string, + directCompute bool) *Converter { + if abs, err := filepath.Abs(shadersDir); err == nil { + shadersDir = abs + } + + conv := &Converter{} + conv.workDir = workDir + conv.shadersDir = shadersDir + conv.directCompute = directCompute + + conv.packageName = packageName + + conv.glslvalidator = NewGLSLValidator() + conv.spirv = NewSPIRVCross() + conv.fxc = NewFXC() + + verifyBinaryPath(&conv.glslvalidator.Bin) + verifyBinaryPath(&conv.spirv.Bin) + // We cannot check fxc since it may depend on wine. + + conv.glslvalidator.WorkDir = workDir.Dir("glslvalidator") + conv.fxc.WorkDir = workDir.Dir("fxc") + conv.spirv.WorkDir = workDir.Dir("spirv") + + return conv +} + +func verifyBinaryPath(bin *string) { + new, err := exec.LookPath(*bin) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to find %q: %v\n", *bin, err) + } else { + *bin = new + } +} + +func (conv *Converter) Run(out io.Writer) error { + shaders, err := filepath.Glob(filepath.Join(conv.shadersDir, "*")) + if len(shaders) == 0 || err != nil { + return fmt.Errorf("failed to list shaders in %q: %w", conv.shadersDir, + err) + } + + sort.Strings(shaders) + + var workers Workers + + type ShaderResult struct { + Path string + Shaders []driver.ShaderSources + Error error + } + shaderResults := make([]ShaderResult, len(shaders)) + + for i, shaderPath := range shaders { + i, shaderPath := i, shaderPath + + switch filepath.Ext(shaderPath) { + case ".vert", ".frag": + workers.Go(func() { + shaders, err := conv.Shader(shaderPath) + shaderResults[i] = ShaderResult{ + Path: shaderPath, + Shaders: shaders, + Error: err, + } + }) + case ".comp": + workers.Go(func() { + shaders, err := conv.ComputeShader(shaderPath) + shaderResults[i] = ShaderResult{ + Path: shaderPath, + Shaders: shaders, + Error: err, + } + }) + default: + continue + } + } + + workers.Wait() + + var allErrors string + for _, r := range shaderResults { + if r.Error != nil { + if len(allErrors) > 0 { + allErrors += "\n\n" + } + allErrors += "--- " + r.Path + " --- \n\n" + r.Error.Error() + "\n" + } + } + if len(allErrors) > 0 { + return errors.New(allErrors) + } + + fmt.Fprintf(out, "// Code generated by build.go. DO NOT EDIT.\n\n") + fmt.Fprintf(out, "package %s\n\n", conv.packageName) + fmt.Fprintf(out, "import %q\n\n", "realy.lol/gio/gpu/internal/driver") + + fmt.Fprintf(out, "var (\n") + + for _, r := range shaderResults { + if len(r.Shaders) == 0 { + continue + } + + name := filepath.Base(r.Path) + name = strings.ReplaceAll(name, ".", "_") + fmt.Fprintf(out, "\tshader_%s = ", name) + + multiVariant := len(r.Shaders) > 1 + if multiVariant { + fmt.Fprintf(out, "[...]driver.ShaderSources{\n") + } + + for _, src := range r.Shaders { + fmt.Fprintf(out, "driver.ShaderSources{\n") + fmt.Fprintf(out, "Name: %#v,\n", src.Name) + if len(src.Inputs) > 0 { + fmt.Fprintf(out, "Inputs: %#v,\n", src.Inputs) + } + if u := src.Uniforms; len(u.Blocks) > 0 { + fmt.Fprintf(out, "Uniforms: driver.UniformsReflection{\n") + fmt.Fprintf(out, "Blocks: %#v,\n", u.Blocks) + fmt.Fprintf(out, "Locations: %#v,\n", u.Locations) + fmt.Fprintf(out, "Size: %d,\n", u.Size) + fmt.Fprintf(out, "},\n") + } + if len(src.Textures) > 0 { + fmt.Fprintf(out, "Textures: %#v,\n", src.Textures) + } + if len(src.GLSL100ES) > 0 { + fmt.Fprintf(out, "GLSL100ES: `%s`,\n", src.GLSL100ES) + } + if len(src.GLSL300ES) > 0 { + fmt.Fprintf(out, "GLSL300ES: `%s`,\n", src.GLSL300ES) + } + if len(src.GLSL310ES) > 0 { + fmt.Fprintf(out, "GLSL310ES: `%s`,\n", src.GLSL310ES) + } + if len(src.GLSL130) > 0 { + fmt.Fprintf(out, "GLSL130: `%s`,\n", src.GLSL130) + } + if len(src.GLSL150) > 0 { + fmt.Fprintf(out, "GLSL150: `%s`,\n", src.GLSL150) + } + if len(src.HLSL) > 0 { + fmt.Fprintf(out, "HLSL: %q,\n", src.HLSL) + } + fmt.Fprintf(out, "}") + if multiVariant { + fmt.Fprintf(out, ",") + } + fmt.Fprintf(out, "\n") + } + if multiVariant { + fmt.Fprintf(out, "}\n") + } + } + fmt.Fprintf(out, ")\n") + + return nil +} + +func (conv *Converter) Shader(shaderPath string) ([]driver.ShaderSources, + error) { + type Variant struct { + FetchColorExpr string + Header string + } + variantArgs := [...]Variant{ + { + FetchColorExpr: `_color.color`, + Header: `layout(binding=0) uniform Color { vec4 color; } _color;`, + }, + { + FetchColorExpr: `mix(_gradient.color1, _gradient.color2, clamp(vUV.x, 0.0, 1.0))`, + Header: `layout(binding=0) uniform Gradient { vec4 color1; vec4 color2; } _gradient;`, + }, + { + FetchColorExpr: `texture(tex, vUV)`, + Header: `layout(binding=0) uniform sampler2D tex;`, + }, + } + + shaderTemplate, err := template.ParseFiles(shaderPath) + if err != nil { + return nil, fmt.Errorf("failed to parse template %q: %w", shaderPath, + err) + } + + var variants []driver.ShaderSources + for i, variantArg := range variantArgs { + variantName := strconv.Itoa(i) + var buf bytes.Buffer + err := shaderTemplate.Execute(&buf, variantArg) + if err != nil { + return nil, fmt.Errorf("failed to execute template %q with %#v: %w", + shaderPath, variantArg, err) + } + + var sources driver.ShaderSources + sources.Name = filepath.Base(shaderPath) + + // Ignore error; some shaders are not meant to run in GLSL 1.00. + sources.GLSL100ES, _, _ = conv.ShaderVariant(shaderPath, variantName, + buf.Bytes(), "es", "100") + + var metadata Metadata + sources.GLSL300ES, metadata, err = conv.ShaderVariant(shaderPath, + variantName, buf.Bytes(), "es", "300") + if err != nil { + return nil, fmt.Errorf("failed to convert GLSL300ES:\n%w", err) + } + + sources.GLSL130, _, err = conv.ShaderVariant(shaderPath, variantName, + buf.Bytes(), "glsl", "130") + if err != nil { + return nil, fmt.Errorf("failed to convert GLSL130:\n%w", err) + } + + hlsl, _, err := conv.ShaderVariant(shaderPath, variantName, buf.Bytes(), + "hlsl", "40") + if err != nil { + return nil, fmt.Errorf("failed to convert HLSL:\n%w", err) + } + sources.HLSL, err = conv.fxc.Compile(shaderPath, variantName, + []byte(hlsl), "main", "4_0_level_9_1") + if err != nil { + // Attempt shader model 4.0. Only the gpu/headless + // test shaders use features not supported by level + // 9.1. + sources.HLSL, err = conv.fxc.Compile(shaderPath, variantName, + []byte(hlsl), "main", "4_0") + if err != nil { + return nil, fmt.Errorf("failed to compile HLSL: %w", err) + } + } + + sources.GLSL150, _, err = conv.ShaderVariant(shaderPath, variantName, + buf.Bytes(), "glsl", "150") + if err != nil { + return nil, fmt.Errorf("failed to convert GLSL150:\n%w", err) + } + + sources.Uniforms = metadata.Uniforms + sources.Inputs = metadata.Inputs + sources.Textures = metadata.Textures + + variants = append(variants, sources) + } + + // If the shader don't use the variant arguments, output only a single version. + if variants[0].GLSL100ES == variants[1].GLSL100ES { + variants = variants[:1] + } + + return variants, nil +} + +func (conv *Converter) ShaderVariant(shaderPath, variant string, src []byte, + lang, profile string) (string, Metadata, error) { + spirv, err := conv.glslvalidator.Convert(shaderPath, variant, + lang == "hlsl", src) + if err != nil { + return "", Metadata{}, fmt.Errorf("failed to generate SPIR-V for %q: %w", + shaderPath, err) + } + + dst, err := conv.spirv.Convert(shaderPath, variant, spirv, lang, profile) + if err != nil { + return "", Metadata{}, fmt.Errorf("failed to convert shader %q: %w", + shaderPath, err) + } + + meta, err := conv.spirv.Metadata(shaderPath, variant, spirv) + if err != nil { + return "", Metadata{}, fmt.Errorf("failed to extract metadata for shader %q: %w", + shaderPath, err) + } + + return dst, meta, nil +} + +func (conv *Converter) ComputeShader(shaderPath string) ([]driver.ShaderSources, + error) { + shader, err := ioutil.ReadFile(shaderPath) + if err != nil { + return nil, fmt.Errorf("failed to load shader %q: %w", shaderPath, err) + } + + spirv, err := conv.glslvalidator.Convert(shaderPath, "", false, shader) + if err != nil { + return nil, fmt.Errorf("failed to convert compute shader %q: %w", + shaderPath, err) + } + + var sources driver.ShaderSources + sources.Name = filepath.Base(shaderPath) + + sources.GLSL310ES, err = conv.spirv.Convert(shaderPath, "", spirv, "es", + "310") + if err != nil { + return nil, fmt.Errorf("failed to convert es compute shader %q: %w", + shaderPath, err) + } + sources.GLSL310ES = unixLineEnding(sources.GLSL310ES) + + hlslSource, err := conv.spirv.Convert(shaderPath, "", spirv, "hlsl", "50") + if err != nil { + return nil, fmt.Errorf("failed to convert hlsl compute shader %q: %w", + shaderPath, err) + } + + dxil, err := conv.fxc.Compile(shaderPath, "0", []byte(hlslSource), "main", + "5_0") + if err != nil { + return nil, fmt.Errorf("failed to compile hlsl compute shader %q: %w", + shaderPath, err) + } + if conv.directCompute { + sources.HLSL = dxil + } + + return []driver.ShaderSources{sources}, nil +} + +// Workers implements wait group with synchronous logging. +type Workers struct { + running sync.WaitGroup +} + +func (lg *Workers) Go(fn func()) { + lg.running.Add(1) + go func() { + defer lg.running.Done() + fn() + }() +} + +func (lg *Workers) Wait() { + lg.running.Wait() +} + +func unixLineEnding(s string) string { + return strings.ReplaceAll(s, "\r\n", "\n") +} diff --git a/gio/gpu/internal/convertshaders/spirvcross.go b/gio/gpu/internal/convertshaders/spirvcross.go new file mode 100644 index 0000000..4252469 --- /dev/null +++ b/gio/gpu/internal/convertshaders/spirvcross.go @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "encoding/json" + "fmt" + "os/exec" + "path/filepath" + "sort" + "strings" + + "realy.lol/gio/gpu/internal/driver" +) + +// Metadata contains reflection data about a shader. +type Metadata struct { + Uniforms driver.UniformsReflection + Inputs []driver.InputLocation + Textures []driver.TextureBinding +} + +// SPIRVCross cross-compiles spirv shaders to es, hlsl and others. +type SPIRVCross struct { + Bin string + WorkDir WorkDir +} + +func NewSPIRVCross() *SPIRVCross { return &SPIRVCross{Bin: "spirv-cross"} } + +// Convert converts compute shader from spirv format to a target format. +func (spirv *SPIRVCross) Convert(path, variant string, shader []byte, + target, version string) (string, error) { + base := spirv.WorkDir.Path(filepath.Base(path), variant) + + if err := spirv.WorkDir.WriteFile(base, shader); err != nil { + return "", fmt.Errorf("unable to write shader to disk: %w", err) + } + + var cmd *exec.Cmd + switch target { + case "glsl": + cmd = exec.Command(spirv.Bin, + "--no-es", + "--version", version, + ) + case "es": + cmd = exec.Command(spirv.Bin, + "--es", + "--version", version, + ) + case "hlsl": + cmd = exec.Command(spirv.Bin, + "--hlsl", + "--shader-model", version, + ) + default: + return "", fmt.Errorf("unknown target %q", target) + } + cmd.Args = append(cmd.Args, "--no-420pack-extension", base) + + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("%s\nfailed to run %v: %w", out, cmd.Args, err) + } + s := string(out) + if target != "hlsl" { + // Strip Windows \r in line endings. + s = unixLineEnding(s) + } + + return s, nil +} + +// Metadata extracts metadata for a SPIR-V shader. +func (spirv *SPIRVCross) Metadata(path, variant string, + shader []byte) (Metadata, error) { + base := spirv.WorkDir.Path(filepath.Base(path), variant) + + if err := spirv.WorkDir.WriteFile(base, shader); err != nil { + return Metadata{}, fmt.Errorf("unable to write shader to disk: %w", err) + } + + cmd := exec.Command(spirv.Bin, + base, + "--reflect", + ) + + out, err := cmd.Output() + if err != nil { + return Metadata{}, fmt.Errorf("failed to run %v: %w", cmd.Args, err) + } + + meta, err := parseMetadata(out) + if err != nil { + return Metadata{}, fmt.Errorf("%s\nfailed to parse metadata: %w", out, + err) + } + + return meta, nil +} + +func parseMetadata(data []byte) (Metadata, error) { + var reflect struct { + Types map[string]struct { + Name string `json:"name"` + Members []struct { + Name string `json:"name"` + Type string `json:"type"` + Offset int `json:"offset"` + } `json:"members"` + } `json:"types"` + Inputs []struct { + Name string `json:"name"` + Type string `json:"type"` + Location int `json:"location"` + } `json:"inputs"` + Textures []struct { + Name string `json:"name"` + Type string `json:"type"` + Set int `json:"set"` + Binding int `json:"binding"` + } `json:"textures"` + UBOs []struct { + Name string `json:"name"` + Type string `json:"type"` + BlockSize int `json:"block_size"` + Set int `json:"set"` + Binding int `json:"binding"` + } `json:"ubos"` + } + if err := json.Unmarshal(data, &reflect); err != nil { + return Metadata{}, fmt.Errorf("failed to parse reflection data: %w", + err) + } + + var m Metadata + + for _, input := range reflect.Inputs { + dataType, dataSize, err := parseDataType(input.Type) + if err != nil { + return Metadata{}, fmt.Errorf("parseReflection: %v", err) + } + m.Inputs = append(m.Inputs, driver.InputLocation{ + Name: input.Name, + Location: input.Location, + Semantic: "TEXCOORD", + SemanticIndex: input.Location, + Type: dataType, + Size: dataSize, + }) + } + + sort.Slice(m.Inputs, func(i, j int) bool { + return m.Inputs[i].Location < m.Inputs[j].Location + }) + + blockOffset := 0 + for _, block := range reflect.UBOs { + m.Uniforms.Blocks = append(m.Uniforms.Blocks, driver.UniformBlock{ + Name: block.Name, + Binding: block.Binding, + }) + t := reflect.Types[block.Type] + // By convention uniform block variables are named by prepending an underscore + // and converting to lowercase. + blockVar := "_" + strings.ToLower(block.Name) + for _, member := range t.Members { + dataType, size, err := parseDataType(member.Type) + if err != nil { + return Metadata{}, fmt.Errorf("failed to parse reflection data: %v", + err) + } + m.Uniforms.Locations = append(m.Uniforms.Locations, + driver.UniformLocation{ + Name: fmt.Sprintf("%s.%s", blockVar, member.Name), + Type: dataType, + Size: size, + Offset: blockOffset + member.Offset, + }) + } + blockOffset += block.BlockSize + } + m.Uniforms.Size = blockOffset + + for _, texture := range reflect.Textures { + m.Textures = append(m.Textures, driver.TextureBinding{ + Name: texture.Name, + Binding: texture.Binding, + }) + } + + // return m, fmt.Errorf("not yet!: %+v", reflect) + return m, nil +} + +func parseDataType(t string) (driver.DataType, int, error) { + switch t { + case "float": + return driver.DataTypeFloat, 1, nil + case "vec2": + return driver.DataTypeFloat, 2, nil + case "vec3": + return driver.DataTypeFloat, 3, nil + case "vec4": + return driver.DataTypeFloat, 4, nil + case "int": + return driver.DataTypeInt, 1, nil + case "int2": + return driver.DataTypeInt, 2, nil + case "int3": + return driver.DataTypeInt, 3, nil + case "int4": + return driver.DataTypeInt, 4, nil + default: + return 0, 0, fmt.Errorf("unsupported input data type: %s", t) + } +} diff --git a/gio/gpu/internal/convertshaders/workdir.go b/gio/gpu/internal/convertshaders/workdir.go new file mode 100644 index 0000000..4c1c092 --- /dev/null +++ b/gio/gpu/internal/convertshaders/workdir.go @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +type WorkDir string + +func (wd WorkDir) Dir(path string) WorkDir { + dirname := filepath.Join(string(wd), path) + if err := os.Mkdir(dirname, 0755); err != nil { + if !os.IsExist(err) { + fmt.Fprintf(os.Stderr, "failed to create %q: %v\n", dirname, err) + } + } + return WorkDir(dirname) +} + +func (wd WorkDir) Path(path ...string) (fullpath string) { + return filepath.Join(string(wd), strings.Join(path, ".")) +} + +func (wd WorkDir) WriteFile(path string, data []byte) error { + err := ioutil.WriteFile(path, data, 0644) + if err != nil { + return fmt.Errorf("unable to create %v: %w", path, err) + } + return nil +} diff --git a/gio/gpu/internal/d3d11/d3d11.go b/gio/gpu/internal/d3d11/d3d11.go new file mode 100644 index 0000000..3ddf7c3 --- /dev/null +++ b/gio/gpu/internal/d3d11/d3d11.go @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// This file exists so this package builds on non-Windows platforms. + +package d3d11 diff --git a/gio/gpu/internal/d3d11/d3d11_windows.go b/gio/gpu/internal/d3d11/d3d11_windows.go new file mode 100644 index 0000000..217ea98 --- /dev/null +++ b/gio/gpu/internal/d3d11/d3d11_windows.go @@ -0,0 +1,787 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package d3d11 + +import ( + "errors" + "fmt" + "image" + "math" + "reflect" + "unsafe" + + "golang.org/x/sys/windows" + + "realy.lol/gio/gpu/internal/driver" + "realy.lol/gio/internal/d3d11" +) + +type Backend struct { + dev *d3d11.Device + ctx *d3d11.DeviceContext + + // Temporary storage to avoid garbage. + clearColor [4]float32 + viewport d3d11.VIEWPORT + depthState depthState + blendState blendState + + // Current program. + prog *Program + + caps driver.Caps + + // fbo is the currently bound fbo. + fbo *Framebuffer + + floatFormat uint32 + + // cached state objects. + depthStates map[depthState]*d3d11.DepthStencilState + blendStates map[blendState]*d3d11.BlendState +} + +type blendState struct { + enable bool + sfactor driver.BlendFactor + dfactor driver.BlendFactor +} + +type depthState struct { + enable bool + mask bool + fn driver.DepthFunc +} + +type Texture struct { + backend *Backend + format uint32 + bindings driver.BufferBinding + tex *d3d11.Texture2D + sampler *d3d11.SamplerState + resView *d3d11.ShaderResourceView + width int + height int +} + +type Program struct { + backend *Backend + + vert struct { + shader *d3d11.VertexShader + uniforms *Buffer + } + frag struct { + shader *d3d11.PixelShader + uniforms *Buffer + } +} + +type Framebuffer struct { + dev *d3d11.Device + ctx *d3d11.DeviceContext + format uint32 + resource *d3d11.Resource + renderTarget *d3d11.RenderTargetView + depthView *d3d11.DepthStencilView + foreign bool +} + +type Buffer struct { + backend *Backend + bind uint32 + buf *d3d11.Buffer + immutable bool +} + +type InputLayout struct { + layout *d3d11.InputLayout +} + +func init() { + driver.NewDirect3D11Device = newDirect3D11Device +} + +func detectFloatFormat(dev *d3d11.Device) (uint32, bool) { + formats := []uint32{ + d3d11.DXGI_FORMAT_R16_FLOAT, + d3d11.DXGI_FORMAT_R32_FLOAT, + d3d11.DXGI_FORMAT_R16G16_FLOAT, + d3d11.DXGI_FORMAT_R32G32_FLOAT, + // These last two are really wasteful, but c'est la vie. + d3d11.DXGI_FORMAT_R16G16B16A16_FLOAT, + d3d11.DXGI_FORMAT_R32G32B32A32_FLOAT, + } + for _, format := range formats { + need := uint32(d3d11.FORMAT_SUPPORT_TEXTURE2D | d3d11.FORMAT_SUPPORT_RENDER_TARGET) + if support, _ := dev.CheckFormatSupport(format); support&need == need { + return format, true + } + } + return 0, false +} + +func newDirect3D11Device(api driver.Direct3D11) (driver.Device, error) { + dev := (*d3d11.Device)(api.Device) + b := &Backend{ + dev: dev, + ctx: dev.GetImmediateContext(), + caps: driver.Caps{ + MaxTextureSize: 2048, // 9.1 maximum + }, + depthStates: make(map[depthState]*d3d11.DepthStencilState), + blendStates: make(map[blendState]*d3d11.BlendState), + } + featLvl := dev.GetFeatureLevel() + if featLvl < d3d11.FEATURE_LEVEL_9_1 { + d3d11.IUnknownRelease(unsafe.Pointer(dev), dev.Vtbl.Release) + d3d11.IUnknownRelease(unsafe.Pointer(b.ctx), b.ctx.Vtbl.Release) + return nil, fmt.Errorf("d3d11: feature level too low: %d", featLvl) + } + switch { + case featLvl >= d3d11.FEATURE_LEVEL_11_0: + b.caps.MaxTextureSize = 16384 + case featLvl >= d3d11.FEATURE_LEVEL_9_3: + b.caps.MaxTextureSize = 4096 + } + if fmt, ok := detectFloatFormat(dev); ok { + b.floatFormat = fmt + b.caps.Features |= driver.FeatureFloatRenderTargets + } + // Enable depth mask to match OpenGL. + b.depthState.mask = true + // Disable backface culling to match OpenGL. + state, err := dev.CreateRasterizerState(&d3d11.RASTERIZER_DESC{ + CullMode: d3d11.CULL_NONE, + FillMode: d3d11.FILL_SOLID, + DepthClipEnable: 1, + }) + if err != nil { + return nil, err + } + defer d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release) + b.ctx.RSSetState(state) + return b, nil +} + +func (b *Backend) BeginFrame() driver.Framebuffer { + renderTarget, depthView := b.ctx.OMGetRenderTargets() + // Assume someone else is holding on to the render targets. + if renderTarget != nil { + d3d11.IUnknownRelease(unsafe.Pointer(renderTarget), + renderTarget.Vtbl.Release) + } + if depthView != nil { + d3d11.IUnknownRelease(unsafe.Pointer(depthView), depthView.Vtbl.Release) + } + return &Framebuffer{ctx: b.ctx, dev: b.dev, renderTarget: renderTarget, + depthView: depthView, foreign: true} +} + +func (b *Backend) EndFrame() { +} + +func (b *Backend) Caps() driver.Caps { + return b.caps +} + +func (b *Backend) NewTimer() driver.Timer { + panic("timers not supported") +} + +func (b *Backend) IsTimeContinuous() bool { + panic("timers not supported") +} + +func (b *Backend) Release() { + for _, state := range b.depthStates { + d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release) + } + for _, state := range b.blendStates { + d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release) + } + d3d11.IUnknownRelease(unsafe.Pointer(b.ctx), b.ctx.Vtbl.Release) + *b = Backend{} +} + +func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, + minFilter, magFilter driver.TextureFilter, + bindings driver.BufferBinding) (driver.Texture, error) { + var d3dfmt uint32 + switch format { + case driver.TextureFormatFloat: + d3dfmt = b.floatFormat + case driver.TextureFormatSRGB: + d3dfmt = d3d11.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB + default: + return nil, fmt.Errorf("unsupported texture format %d", format) + } + tex, err := b.dev.CreateTexture2D(&d3d11.TEXTURE2D_DESC{ + Width: uint32(width), + Height: uint32(height), + MipLevels: 1, + ArraySize: 1, + Format: d3dfmt, + SampleDesc: d3d11.DXGI_SAMPLE_DESC{ + Count: 1, + Quality: 0, + }, + BindFlags: convBufferBinding(bindings), + }) + if err != nil { + return nil, err + } + var ( + sampler *d3d11.SamplerState + resView *d3d11.ShaderResourceView + ) + if bindings&driver.BufferBindingTexture != 0 { + var filter uint32 + switch { + case minFilter == driver.FilterNearest && magFilter == driver.FilterNearest: + filter = d3d11.FILTER_MIN_MAG_MIP_POINT + case minFilter == driver.FilterLinear && magFilter == driver.FilterLinear: + filter = d3d11.FILTER_MIN_MAG_LINEAR_MIP_POINT + default: + d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) + return nil, fmt.Errorf("unsupported texture filter combination %d, %d", + minFilter, magFilter) + } + var err error + sampler, err = b.dev.CreateSamplerState(&d3d11.SAMPLER_DESC{ + Filter: filter, + AddressU: d3d11.TEXTURE_ADDRESS_CLAMP, + AddressV: d3d11.TEXTURE_ADDRESS_CLAMP, + AddressW: d3d11.TEXTURE_ADDRESS_CLAMP, + MaxAnisotropy: 1, + MinLOD: -math.MaxFloat32, + MaxLOD: math.MaxFloat32, + }) + if err != nil { + d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) + return nil, err + } + resView, err = b.dev.CreateShaderResourceViewTEX2D( + (*d3d11.Resource)(unsafe.Pointer(tex)), + &d3d11.SHADER_RESOURCE_VIEW_DESC_TEX2D{ + SHADER_RESOURCE_VIEW_DESC: d3d11.SHADER_RESOURCE_VIEW_DESC{ + Format: d3dfmt, + ViewDimension: d3d11.SRV_DIMENSION_TEXTURE2D, + }, + Texture2D: d3d11.TEX2D_SRV{ + MostDetailedMip: 0, + MipLevels: ^uint32(0), + }, + }, + ) + if err != nil { + d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) + d3d11.IUnknownRelease(unsafe.Pointer(sampler), sampler.Vtbl.Release) + return nil, err + } + } + return &Texture{backend: b, format: d3dfmt, tex: tex, sampler: sampler, + resView: resView, bindings: bindings, width: width, height: height}, nil +} + +func (b *Backend) NewFramebuffer(tex driver.Texture, + depthBits int) (driver.Framebuffer, error) { + d3dtex := tex.(*Texture) + if d3dtex.bindings&driver.BufferBindingFramebuffer == 0 { + return nil, errors.New("the texture was created without BufferBindingFramebuffer binding") + } + resource := (*d3d11.Resource)(unsafe.Pointer(d3dtex.tex)) + renderTarget, err := b.dev.CreateRenderTargetView(resource) + if err != nil { + return nil, err + } + fbo := &Framebuffer{ctx: b.ctx, dev: b.dev, format: d3dtex.format, + resource: resource, renderTarget: renderTarget} + if depthBits > 0 { + depthView, err := d3d11.CreateDepthView(b.dev, d3dtex.width, + d3dtex.height, depthBits) + if err != nil { + d3d11.IUnknownRelease(unsafe.Pointer(renderTarget), + renderTarget.Vtbl.Release) + return nil, err + } + fbo.depthView = depthView + } + return fbo, nil +} + +func (b *Backend) NewInputLayout(vertexShader driver.ShaderSources, + layout []driver.InputDesc) (driver.InputLayout, error) { + if len(vertexShader.Inputs) != len(layout) { + return nil, fmt.Errorf("NewInputLayout: got %d inputs, expected %d", + len(layout), len(vertexShader.Inputs)) + } + descs := make([]d3d11.INPUT_ELEMENT_DESC, len(layout)) + for i, l := range layout { + inp := vertexShader.Inputs[i] + cname, err := windows.BytePtrFromString(inp.Semantic) + if err != nil { + return nil, err + } + var format uint32 + switch l.Type { + case driver.DataTypeFloat: + switch l.Size { + case 1: + format = d3d11.DXGI_FORMAT_R32_FLOAT + case 2: + format = d3d11.DXGI_FORMAT_R32G32_FLOAT + case 3: + format = d3d11.DXGI_FORMAT_R32G32B32_FLOAT + case 4: + format = d3d11.DXGI_FORMAT_R32G32B32A32_FLOAT + default: + panic("unsupported data size") + } + case driver.DataTypeShort: + switch l.Size { + case 1: + format = d3d11.DXGI_FORMAT_R16_SINT + case 2: + format = d3d11.DXGI_FORMAT_R16G16_SINT + default: + panic("unsupported data size") + } + default: + panic("unsupported data type") + } + descs[i] = d3d11.INPUT_ELEMENT_DESC{ + SemanticName: cname, + SemanticIndex: uint32(inp.SemanticIndex), + Format: format, + AlignedByteOffset: uint32(l.Offset), + } + } + l, err := b.dev.CreateInputLayout(descs, []byte(vertexShader.HLSL)) + if err != nil { + return nil, err + } + return &InputLayout{layout: l}, nil +} + +func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer, + error) { + if typ&driver.BufferBindingUniforms != 0 { + if typ != driver.BufferBindingUniforms { + return nil, errors.New("uniform buffers cannot have other bindings") + } + if size%16 != 0 { + return nil, fmt.Errorf("constant buffer size is %d, expected a multiple of 16", + size) + } + } + bind := convBufferBinding(typ) + buf, err := b.dev.CreateBuffer(&d3d11.BUFFER_DESC{ + ByteWidth: uint32(size), + BindFlags: bind, + }, nil) + if err != nil { + return nil, err + } + return &Buffer{backend: b, buf: buf, bind: bind}, nil +} + +func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding, + data []byte) (driver.Buffer, error) { + if typ&driver.BufferBindingUniforms != 0 { + if typ != driver.BufferBindingUniforms { + return nil, errors.New("uniform buffers cannot have other bindings") + } + if len(data)%16 != 0 { + return nil, fmt.Errorf("constant buffer size is %d, expected a multiple of 16", + len(data)) + } + } + bind := convBufferBinding(typ) + buf, err := b.dev.CreateBuffer(&d3d11.BUFFER_DESC{ + ByteWidth: uint32(len(data)), + Usage: d3d11.USAGE_IMMUTABLE, + BindFlags: bind, + }, data) + if err != nil { + return nil, err + } + return &Buffer{backend: b, buf: buf, bind: bind, immutable: true}, nil +} + +func (b *Backend) NewComputeProgram(shader driver.ShaderSources) (driver.Program, + error) { + panic("not implemented") +} + +func (b *Backend) NewProgram(vertexShader, fragmentShader driver.ShaderSources) (driver.Program, + error) { + vs, err := b.dev.CreateVertexShader([]byte(vertexShader.HLSL)) + if err != nil { + return nil, err + } + ps, err := b.dev.CreatePixelShader([]byte(fragmentShader.HLSL)) + if err != nil { + return nil, err + } + p := &Program{backend: b} + p.vert.shader = vs + p.frag.shader = ps + return p, nil +} + +func (b *Backend) Clear(colr, colg, colb, cola float32) { + b.clearColor = [4]float32{colr, colg, colb, cola} + b.ctx.ClearRenderTargetView(b.fbo.renderTarget, &b.clearColor) +} + +func (b *Backend) ClearDepth(depth float32) { + if b.fbo.depthView != nil { + b.ctx.ClearDepthStencilView(b.fbo.depthView, + d3d11.CLEAR_DEPTH|d3d11.CLEAR_STENCIL, depth, 0) + } +} + +func (b *Backend) Viewport(x, y, width, height int) { + b.viewport = d3d11.VIEWPORT{ + TopLeftX: float32(x), + TopLeftY: float32(y), + Width: float32(width), + Height: float32(height), + MinDepth: 0.0, + MaxDepth: 1.0, + } + b.ctx.RSSetViewports(&b.viewport) +} + +func (b *Backend) DrawArrays(mode driver.DrawMode, off, count int) { + b.prepareDraw(mode) + b.ctx.Draw(uint32(count), uint32(off)) +} + +func (b *Backend) DrawElements(mode driver.DrawMode, off, count int) { + b.prepareDraw(mode) + b.ctx.DrawIndexed(uint32(count), uint32(off), 0) +} + +func (b *Backend) prepareDraw(mode driver.DrawMode) { + if p := b.prog; p != nil { + b.ctx.VSSetShader(p.vert.shader) + b.ctx.PSSetShader(p.frag.shader) + if buf := p.vert.uniforms; buf != nil { + b.ctx.VSSetConstantBuffers(buf.buf) + } + if buf := p.frag.uniforms; buf != nil { + b.ctx.PSSetConstantBuffers(buf.buf) + } + } + var topology uint32 + switch mode { + case driver.DrawModeTriangles: + topology = d3d11.PRIMITIVE_TOPOLOGY_TRIANGLELIST + case driver.DrawModeTriangleStrip: + topology = d3d11.PRIMITIVE_TOPOLOGY_TRIANGLESTRIP + default: + panic("unsupported draw mode") + } + b.ctx.IASetPrimitiveTopology(topology) + + depthState, ok := b.depthStates[b.depthState] + if !ok { + var desc d3d11.DEPTH_STENCIL_DESC + if b.depthState.enable { + desc.DepthEnable = 1 + } + if b.depthState.mask { + desc.DepthWriteMask = d3d11.DEPTH_WRITE_MASK_ALL + } + switch b.depthState.fn { + case driver.DepthFuncGreater: + desc.DepthFunc = d3d11.COMPARISON_GREATER + case driver.DepthFuncGreaterEqual: + desc.DepthFunc = d3d11.COMPARISON_GREATER_EQUAL + default: + panic("unsupported depth func") + } + var err error + depthState, err = b.dev.CreateDepthStencilState(&desc) + if err != nil { + panic(err) + } + b.depthStates[b.depthState] = depthState + } + b.ctx.OMSetDepthStencilState(depthState, 0) + + blendState, ok := b.blendStates[b.blendState] + if !ok { + var desc d3d11.BLEND_DESC + t0 := &desc.RenderTarget[0] + t0.RenderTargetWriteMask = d3d11.COLOR_WRITE_ENABLE_ALL + t0.BlendOp = d3d11.BLEND_OP_ADD + t0.BlendOpAlpha = d3d11.BLEND_OP_ADD + if b.blendState.enable { + t0.BlendEnable = 1 + } + scol, salpha := toBlendFactor(b.blendState.sfactor) + dcol, dalpha := toBlendFactor(b.blendState.dfactor) + t0.SrcBlend = scol + t0.SrcBlendAlpha = salpha + t0.DestBlend = dcol + t0.DestBlendAlpha = dalpha + var err error + blendState, err = b.dev.CreateBlendState(&desc) + if err != nil { + panic(err) + } + b.blendStates[b.blendState] = blendState + } + b.ctx.OMSetBlendState(blendState, nil, 0xffffffff) +} + +func (b *Backend) DepthFunc(f driver.DepthFunc) { + b.depthState.fn = f +} + +func (b *Backend) SetBlend(enable bool) { + b.blendState.enable = enable +} + +func (b *Backend) SetDepthTest(enable bool) { + b.depthState.enable = enable +} + +func (b *Backend) DepthMask(mask bool) { + b.depthState.mask = mask +} + +func (b *Backend) BlendFunc(sfactor, dfactor driver.BlendFactor) { + b.blendState.sfactor = sfactor + b.blendState.dfactor = dfactor +} + +func (b *Backend) BindImageTexture(unit int, tex driver.Texture, + access driver.AccessBits, f driver.TextureFormat) { + panic("not implemented") +} + +func (b *Backend) MemoryBarrier() { + panic("not implemented") +} + +func (b *Backend) DispatchCompute(x, y, z int) { + panic("not implemented") +} + +func (t *Texture) Upload(offset, size image.Point, pixels []byte) { + stride := size.X * 4 + dst := &d3d11.BOX{ + Left: uint32(offset.X), + Top: uint32(offset.Y), + Right: uint32(offset.X + size.X), + Bottom: uint32(offset.Y + size.Y), + Front: 0, + Back: 1, + } + res := (*d3d11.Resource)(unsafe.Pointer(t.tex)) + t.backend.ctx.UpdateSubresource(res, dst, uint32(stride), + uint32(len(pixels)), pixels) +} + +func (t *Texture) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(t.tex), t.tex.Vtbl.Release) + t.tex = nil + if t.sampler != nil { + d3d11.IUnknownRelease(unsafe.Pointer(t.sampler), t.sampler.Vtbl.Release) + t.sampler = nil + } + if t.resView != nil { + d3d11.IUnknownRelease(unsafe.Pointer(t.resView), t.resView.Vtbl.Release) + t.resView = nil + } +} + +func (b *Backend) BindTexture(unit int, tex driver.Texture) { + t := tex.(*Texture) + b.ctx.PSSetSamplers(uint32(unit), t.sampler) + b.ctx.PSSetShaderResources(uint32(unit), t.resView) +} + +func (b *Backend) BindProgram(prog driver.Program) { + b.prog = prog.(*Program) +} + +func (p *Program) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(p.vert.shader), + p.vert.shader.Vtbl.Release) + d3d11.IUnknownRelease(unsafe.Pointer(p.frag.shader), + p.frag.shader.Vtbl.Release) + p.vert.shader = nil + p.frag.shader = nil +} + +func (p *Program) SetStorageBuffer(binding int, buffer driver.Buffer) { + panic("not implemented") +} + +func (p *Program) SetVertexUniforms(buf driver.Buffer) { + p.vert.uniforms = buf.(*Buffer) +} + +func (p *Program) SetFragmentUniforms(buf driver.Buffer) { + p.frag.uniforms = buf.(*Buffer) +} + +func (b *Backend) BindVertexBuffer(buf driver.Buffer, stride, offset int) { + b.ctx.IASetVertexBuffers(buf.(*Buffer).buf, uint32(stride), uint32(offset)) +} + +func (b *Backend) BindIndexBuffer(buf driver.Buffer) { + b.ctx.IASetIndexBuffer(buf.(*Buffer).buf, d3d11.DXGI_FORMAT_R16_UINT, 0) +} + +func (b *Buffer) Download(data []byte) error { + panic("not implemented") +} + +func (b *Buffer) Upload(data []byte) { + b.backend.ctx.UpdateSubresource((*d3d11.Resource)(unsafe.Pointer(b.buf)), + nil, 0, 0, data) +} + +func (b *Buffer) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(b.buf), b.buf.Vtbl.Release) + b.buf = nil +} + +func (f *Framebuffer) ReadPixels(src image.Rectangle, pixels []byte) error { + if f.resource == nil { + return errors.New("framebuffer does not support ReadPixels") + } + w, h := src.Dx(), src.Dy() + tex, err := f.dev.CreateTexture2D(&d3d11.TEXTURE2D_DESC{ + Width: uint32(w), + Height: uint32(h), + MipLevels: 1, + ArraySize: 1, + Format: f.format, + SampleDesc: d3d11.DXGI_SAMPLE_DESC{ + Count: 1, + Quality: 0, + }, + Usage: d3d11.USAGE_STAGING, + CPUAccessFlags: d3d11.CPU_ACCESS_READ, + }) + if err != nil { + return fmt.Errorf("ReadPixels: %v", err) + } + defer d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) + res := (*d3d11.Resource)(unsafe.Pointer(tex)) + f.ctx.CopySubresourceRegion( + res, + 0, // Destination subresource. + 0, 0, 0, // Destination coordinates (x, y, z). + f.resource, + 0, // Source subresource. + &d3d11.BOX{ + Left: uint32(src.Min.X), + Top: uint32(src.Min.Y), + Right: uint32(src.Max.X), + Bottom: uint32(src.Max.Y), + Front: 0, + Back: 1, + }, + ) + resMap, err := f.ctx.Map(res, 0, d3d11.MAP_READ, 0) + if err != nil { + return fmt.Errorf("ReadPixels: %v", err) + } + defer f.ctx.Unmap(res, 0) + srcPitch := w * 4 + dstPitch := int(resMap.RowPitch) + mapSize := dstPitch * h + data := sliceOf(resMap.PData, mapSize) + width := w * 4 + for r := 0; r < h; r++ { + pixels := pixels[r*srcPitch:] + copy(pixels[:width], data[r*dstPitch:]) + } + return nil +} + +func (b *Backend) BindFramebuffer(fbo driver.Framebuffer) { + b.fbo = fbo.(*Framebuffer) + b.ctx.OMSetRenderTargets(b.fbo.renderTarget, b.fbo.depthView) +} + +func (f *Framebuffer) Invalidate() { +} + +func (f *Framebuffer) Release() { + if f.foreign { + panic("framebuffer not created by NewFramebuffer") + } + if f.renderTarget != nil { + d3d11.IUnknownRelease(unsafe.Pointer(f.renderTarget), + f.renderTarget.Vtbl.Release) + f.renderTarget = nil + } + if f.depthView != nil { + d3d11.IUnknownRelease(unsafe.Pointer(f.depthView), + f.depthView.Vtbl.Release) + f.depthView = nil + } +} + +func (b *Backend) BindInputLayout(layout driver.InputLayout) { + b.ctx.IASetInputLayout(layout.(*InputLayout).layout) +} + +func (l *InputLayout) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(l.layout), l.layout.Vtbl.Release) + l.layout = nil +} + +func convBufferBinding(typ driver.BufferBinding) uint32 { + var bindings uint32 + if typ&driver.BufferBindingVertices != 0 { + bindings |= d3d11.BIND_VERTEX_BUFFER + } + if typ&driver.BufferBindingIndices != 0 { + bindings |= d3d11.BIND_INDEX_BUFFER + } + if typ&driver.BufferBindingUniforms != 0 { + bindings |= d3d11.BIND_CONSTANT_BUFFER + } + if typ&driver.BufferBindingTexture != 0 { + bindings |= d3d11.BIND_SHADER_RESOURCE + } + if typ&driver.BufferBindingFramebuffer != 0 { + bindings |= d3d11.BIND_RENDER_TARGET + } + return bindings +} + +func toBlendFactor(f driver.BlendFactor) (uint32, uint32) { + switch f { + case driver.BlendFactorOne: + return d3d11.BLEND_ONE, d3d11.BLEND_ONE + case driver.BlendFactorOneMinusSrcAlpha: + return d3d11.BLEND_INV_SRC_ALPHA, d3d11.BLEND_INV_SRC_ALPHA + case driver.BlendFactorZero: + return d3d11.BLEND_ZERO, d3d11.BLEND_ZERO + case driver.BlendFactorDstColor: + return d3d11.BLEND_DEST_COLOR, d3d11.BLEND_DEST_ALPHA + default: + panic("unsupported blend source factor") + } +} + +// sliceOf returns a slice from a (native) pointer. +func sliceOf(ptr uintptr, cap int) []byte { + var data []byte + h := (*reflect.SliceHeader)(unsafe.Pointer(&data)) + h.Data = ptr + h.Cap = cap + h.Len = cap + return data +} diff --git a/gio/gpu/internal/driver/api.go b/gio/gpu/internal/driver/api.go new file mode 100644 index 0000000..6e0d846 --- /dev/null +++ b/gio/gpu/internal/driver/api.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package driver + +import ( + "fmt" + "unsafe" + + "realy.lol/gio/internal/gl" +) + +// See gpu/api.go for documentation for the API types + +type API interface { + implementsAPI() +} + +type OpenGL struct { + // Context contains the WebGL context for WebAssembly platforms. It is + // empty for all other platforms; an OpenGL context is assumed current when + // calling NewDevice. + Context gl.Context +} + +type Direct3D11 struct { + // Device contains a *ID3D11Device. + Device unsafe.Pointer +} + +// API specific device constructors. +var ( + NewOpenGLDevice func(api OpenGL) (Device, error) + NewDirect3D11Device func(api Direct3D11) (Device, error) +) + +// NewDevice creates a new Device given the api. +// +// Note that the device does not assume ownership of the resources contained in +// api; the caller must ensure the resources are valid until the device is +// released. +func NewDevice(api API) (Device, error) { + switch api := api.(type) { + case OpenGL: + if NewOpenGLDevice != nil { + return NewOpenGLDevice(api) + } + case Direct3D11: + if NewDirect3D11Device != nil { + return NewDirect3D11Device(api) + } + } + return nil, fmt.Errorf("driver: no driver available for the API %T", api) +} + +func (OpenGL) implementsAPI() {} +func (Direct3D11) implementsAPI() {} diff --git a/gio/gpu/internal/driver/driver.go b/gio/gpu/internal/driver/driver.go new file mode 100644 index 0000000..14d3d85 --- /dev/null +++ b/gio/gpu/internal/driver/driver.go @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package driver + +import ( + "errors" + "image" + "time" +) + +// Device represents the abstraction of underlying GPU +// APIs such as OpenGL, Direct3D useful for rendering Gio +// operations. +type Device interface { + BeginFrame() Framebuffer + EndFrame() + Caps() Caps + NewTimer() Timer + // IsContinuousTime reports whether all timer measurements + // are valid at the point of call. + IsTimeContinuous() bool + NewTexture(format TextureFormat, width, height int, minFilter, magFilter TextureFilter, bindings BufferBinding) (Texture, error) + NewFramebuffer(tex Texture, depthBits int) (Framebuffer, error) + NewImmutableBuffer(typ BufferBinding, data []byte) (Buffer, error) + NewBuffer(typ BufferBinding, size int) (Buffer, error) + NewComputeProgram(shader ShaderSources) (Program, error) + NewProgram(vertexShader, fragmentShader ShaderSources) (Program, error) + NewInputLayout(vertexShader ShaderSources, layout []InputDesc) (InputLayout, error) + + DepthFunc(f DepthFunc) + ClearDepth(d float32) + Clear(r, g, b, a float32) + Viewport(x, y, width, height int) + DrawArrays(mode DrawMode, off, count int) + DrawElements(mode DrawMode, off, count int) + SetBlend(enable bool) + SetDepthTest(enable bool) + DepthMask(mask bool) + BlendFunc(sfactor, dfactor BlendFactor) + + BindInputLayout(i InputLayout) + BindProgram(p Program) + BindFramebuffer(f Framebuffer) + BindTexture(unit int, t Texture) + BindVertexBuffer(b Buffer, stride, offset int) + BindIndexBuffer(b Buffer) + BindImageTexture(unit int, texture Texture, access AccessBits, format TextureFormat) + + MemoryBarrier() + DispatchCompute(x, y, z int) + + Release() +} + +type ShaderSources struct { + Name string + GLSL100ES string + GLSL300ES string + GLSL310ES string + GLSL130 string + GLSL150 string + HLSL string + Uniforms UniformsReflection + Inputs []InputLocation + Textures []TextureBinding +} + +type UniformsReflection struct { + Blocks []UniformBlock + Locations []UniformLocation + Size int +} + +type TextureBinding struct { + Name string + Binding int +} + +type UniformBlock struct { + Name string + Binding int +} + +type UniformLocation struct { + Name string + Type DataType + Size int + Offset int +} + +type InputLocation struct { + // For GLSL. + Name string + Location int + // For HLSL. + Semantic string + SemanticIndex int + + Type DataType + Size int +} + +// InputDesc describes a vertex attribute as laid out in a Buffer. +type InputDesc struct { + Type DataType + Size int + + Offset int +} + +// InputLayout is the driver specific representation of the mapping +// between Buffers and shader attributes. +type InputLayout interface { + Release() +} + +type AccessBits uint8 + +type BlendFactor uint8 + +type DrawMode uint8 + +type TextureFilter uint8 +type TextureFormat uint8 + +type BufferBinding uint8 + +type DataType uint8 + +type DepthFunc uint8 + +type Features uint + +type Caps struct { + // BottomLeftOrigin is true if the driver has the origin in the lower left + // corner. The OpenGL driver returns true. + BottomLeftOrigin bool + Features Features + MaxTextureSize int +} + +type Program interface { + Release() + SetStorageBuffer(binding int, buf Buffer) + SetVertexUniforms(buf Buffer) + SetFragmentUniforms(buf Buffer) +} + +type Buffer interface { + Release() + Upload(data []byte) + Download(data []byte) error +} + +type Framebuffer interface { + Invalidate() + Release() + ReadPixels(src image.Rectangle, pixels []byte) error +} + +type Timer interface { + Begin() + End() + Duration() (time.Duration, bool) + Release() +} + +type Texture interface { + Upload(offset, size image.Point, pixels []byte) + Release() +} + +const ( + DepthFuncGreater DepthFunc = iota + DepthFuncGreaterEqual +) + +const ( + DataTypeFloat DataType = iota + DataTypeInt + DataTypeShort +) + +const ( + BufferBindingIndices BufferBinding = 1 << iota + BufferBindingVertices + BufferBindingUniforms + BufferBindingTexture + BufferBindingFramebuffer + BufferBindingShaderStorage +) + +const ( + TextureFormatSRGB TextureFormat = iota + TextureFormatFloat + TextureFormatRGBA8 +) + +const ( + AccessRead AccessBits = 1 + iota + AccessWrite +) + +const ( + FilterNearest TextureFilter = iota + FilterLinear +) + +const ( + FeatureTimers Features = 1 << iota + FeatureFloatRenderTargets + FeatureCompute +) + +const ( + DrawModeTriangleStrip DrawMode = iota + DrawModeTriangles +) + +const ( + BlendFactorOne BlendFactor = iota + BlendFactorOneMinusSrcAlpha + BlendFactorZero + BlendFactorDstColor +) + +var ErrContentLost = errors.New("buffer content lost") + +func (f Features) Has(feats Features) bool { + return f&feats == feats +} + +func DownloadImage(d Device, f Framebuffer, r image.Rectangle) (*image.RGBA, error) { + img := image.NewRGBA(r) + if err := f.ReadPixels(r, img.Pix); err != nil { + return nil, err + } + if d.Caps().BottomLeftOrigin { + // OpenGL origin is in the lower-left corner. Flip the image to + // match. + flipImageY(r.Dx()*4, r.Dy(), img.Pix) + } + return img, nil +} + +func flipImageY(stride, height int, pixels []byte) { + // Flip image in y-direction. OpenGL's origin is in the lower + // left corner. + row := make([]uint8, stride) + for y := 0; y < height/2; y++ { + y1 := height - y - 1 + dest := y1 * stride + src := y * stride + copy(row, pixels[dest:]) + copy(pixels[dest:], pixels[src:src+len(row)]) + copy(pixels[src:], row) + } +} + +func UploadImage(t Texture, offset image.Point, img *image.RGBA) { + var pixels []byte + size := img.Bounds().Size() + if img.Stride != size.X*4 { + panic("unsupported stride") + } + start := img.PixOffset(0, 0) + end := img.PixOffset(size.X, size.Y-1) + pixels = img.Pix[start:end] + t.Upload(offset, size, pixels) +} diff --git a/gio/gpu/internal/opengl/opengl.go b/gio/gpu/internal/opengl/opengl.go new file mode 100644 index 0000000..e41dbc8 --- /dev/null +++ b/gio/gpu/internal/opengl/opengl.go @@ -0,0 +1,998 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package opengl + +import ( + "errors" + "fmt" + "image" + "strings" + "time" + "unsafe" + + "realy.lol/gio/gpu/internal/driver" + "realy.lol/gio/internal/gl" +) + +// Backend implements driver.Device. +type Backend struct { + funcs *gl.Functions + + state glstate + + glver [2]int + gles bool + ubo bool + feats driver.Caps + // floatTriple holds the settings for floating point + // textures. + floatTriple textureTriple + // Single channel alpha textures. + alphaTriple textureTriple + srgbaTriple textureTriple +} + +// State tracking. +type glstate struct { + // nattr is the current number of enabled vertex arrays. + nattr int + prog *gpuProgram + texUnits [4]*gpuTexture + layout *gpuInputLayout + buffer bufferBinding +} + +type bufferBinding struct { + buf *gpuBuffer + offset int + stride int +} + +type gpuTimer struct { + funcs *gl.Functions + obj gl.Query +} + +type gpuTexture struct { + backend *Backend + obj gl.Texture + triple textureTriple + width int + height int +} + +type gpuFramebuffer struct { + backend *Backend + obj gl.Framebuffer + hasDepth bool + depthBuf gl.Renderbuffer + foreign bool +} + +type gpuBuffer struct { + backend *Backend + hasBuffer bool + obj gl.Buffer + typ driver.BufferBinding + size int + immutable bool + version int + // For emulation of uniform buffers. + data []byte +} + +type gpuProgram struct { + backend *Backend + obj gl.Program + nattr int + vertUniforms uniformsTracker + fragUniforms uniformsTracker + storage [storageBindings]*gpuBuffer +} + +type uniformsTracker struct { + locs []uniformLocation + size int + buf *gpuBuffer + version int +} + +type uniformLocation struct { + uniform gl.Uniform + offset int + typ driver.DataType + size int +} + +type gpuInputLayout struct { + inputs []driver.InputLocation + layout []driver.InputDesc +} + +// textureTriple holds the type settings for +// a TexImage2D call. +type textureTriple struct { + internalFormat gl.Enum + format gl.Enum + typ gl.Enum +} + +type Context = gl.Context + +const ( + storageBindings = 32 +) + +func init() { + driver.NewOpenGLDevice = newOpenGLDevice +} + +func newOpenGLDevice(api driver.OpenGL) (driver.Device, error) { + f, err := gl.NewFunctions(api.Context) + if err != nil { + return nil, err + } + exts := strings.Split(f.GetString(gl.EXTENSIONS), " ") + glVer := f.GetString(gl.VERSION) + ver, gles, err := gl.ParseGLVersion(glVer) + if err != nil { + return nil, err + } + floatTriple, ffboErr := floatTripleFor(f, ver, exts) + srgbaTriple, err := srgbaTripleFor(ver, exts) + if err != nil { + return nil, err + } + gles30 := gles && ver[0] >= 3 + gles31 := gles && (ver[0] > 3 || (ver[0] == 3 && ver[1] >= 1)) + gl40 := !gles && ver[0] >= 4 + b := &Backend{ + glver: ver, + gles: gles, + ubo: gles30 || gl40, + funcs: f, + floatTriple: floatTriple, + alphaTriple: alphaTripleFor(ver), + srgbaTriple: srgbaTriple, + } + b.feats.BottomLeftOrigin = true + if ffboErr == nil { + b.feats.Features |= driver.FeatureFloatRenderTargets + } + if gles31 { + b.feats.Features |= driver.FeatureCompute + } + if hasExtension(exts, + "GL_EXT_disjoint_timer_query_webgl2") || hasExtension(exts, + "GL_EXT_disjoint_timer_query") { + b.feats.Features |= driver.FeatureTimers + } + b.feats.MaxTextureSize = f.GetInteger(gl.MAX_TEXTURE_SIZE) + return b, nil +} + +func (b *Backend) BeginFrame() driver.Framebuffer { + // Assume GL state is reset between frames. + b.state = glstate{} + fboID := gl.Framebuffer(b.funcs.GetBinding(gl.FRAMEBUFFER_BINDING)) + return &gpuFramebuffer{backend: b, obj: fboID, foreign: true} +} + +func (b *Backend) EndFrame() { + b.funcs.ActiveTexture(gl.TEXTURE0) +} + +func (b *Backend) Caps() driver.Caps { + return b.feats +} + +func (b *Backend) NewTimer() driver.Timer { + return &gpuTimer{ + funcs: b.funcs, + obj: b.funcs.CreateQuery(), + } +} + +func (b *Backend) IsTimeContinuous() bool { + return b.funcs.GetInteger(gl.GPU_DISJOINT_EXT) == gl.FALSE +} + +func (b *Backend) NewFramebuffer(tex driver.Texture, + depthBits int) (driver.Framebuffer, error) { + glErr(b.funcs) + gltex := tex.(*gpuTexture) + fb := b.funcs.CreateFramebuffer() + fbo := &gpuFramebuffer{backend: b, obj: fb} + b.BindFramebuffer(fbo) + if err := glErr(b.funcs); err != nil { + fbo.Release() + return nil, err + } + b.funcs.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, gltex.obj, 0) + if depthBits > 0 { + size := gl.Enum(gl.DEPTH_COMPONENT16) + switch { + case depthBits > 24: + size = gl.DEPTH_COMPONENT32F + case depthBits > 16: + size = gl.DEPTH_COMPONENT24 + } + depthBuf := b.funcs.CreateRenderbuffer() + b.funcs.BindRenderbuffer(gl.RENDERBUFFER, depthBuf) + b.funcs.RenderbufferStorage(gl.RENDERBUFFER, size, gltex.width, + gltex.height) + b.funcs.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, + gl.RENDERBUFFER, depthBuf) + fbo.depthBuf = depthBuf + fbo.hasDepth = true + if err := glErr(b.funcs); err != nil { + fbo.Release() + return nil, err + } + } + if st := b.funcs.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { + fbo.Release() + return nil, fmt.Errorf("incomplete framebuffer, status = 0x%x, err = %d", + st, b.funcs.GetError()) + } + return fbo, nil +} + +func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, + minFilter, magFilter driver.TextureFilter, + binding driver.BufferBinding) (driver.Texture, error) { + glErr(b.funcs) + tex := &gpuTexture{backend: b, obj: b.funcs.CreateTexture(), width: width, + height: height} + switch format { + case driver.TextureFormatFloat: + tex.triple = b.floatTriple + case driver.TextureFormatSRGB: + tex.triple = b.srgbaTriple + case driver.TextureFormatRGBA8: + tex.triple = textureTriple{gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE} + default: + return nil, errors.New("unsupported texture format") + } + b.BindTexture(0, tex) + b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, + toTexFilter(magFilter)) + b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, + toTexFilter(minFilter)) + b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + if b.gles && b.glver[0] >= 3 { + // Immutable textures are required for BindImageTexture, and can't hurt otherwise. + b.funcs.TexStorage2D(gl.TEXTURE_2D, 1, tex.triple.internalFormat, width, + height) + } else { + b.funcs.TexImage2D(gl.TEXTURE_2D, 0, tex.triple.internalFormat, width, + height, tex.triple.format, tex.triple.typ) + } + if err := glErr(b.funcs); err != nil { + tex.Release() + return nil, err + } + return tex, nil +} + +func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer, + error) { + glErr(b.funcs) + buf := &gpuBuffer{backend: b, typ: typ, size: size} + if typ&driver.BufferBindingUniforms != 0 { + if typ != driver.BufferBindingUniforms { + return nil, errors.New("uniforms buffers cannot be bound as anything else") + } + if !b.ubo { + // GLES 2 doesn't support uniform buffers. + buf.data = make([]byte, size) + } + } + if typ&^driver.BufferBindingUniforms != 0 || b.ubo { + buf.hasBuffer = true + buf.obj = b.funcs.CreateBuffer() + if err := glErr(b.funcs); err != nil { + buf.Release() + return nil, err + } + firstBinding := firstBufferType(typ) + b.funcs.BindBuffer(firstBinding, buf.obj) + b.funcs.BufferData(firstBinding, size, gl.DYNAMIC_DRAW) + } + return buf, nil +} + +func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding, + data []byte) (driver.Buffer, error) { + glErr(b.funcs) + obj := b.funcs.CreateBuffer() + buf := &gpuBuffer{backend: b, obj: obj, typ: typ, size: len(data), + hasBuffer: true} + firstBinding := firstBufferType(typ) + b.funcs.BindBuffer(firstBinding, buf.obj) + b.funcs.BufferData(firstBinding, len(data), gl.STATIC_DRAW) + buf.Upload(data) + buf.immutable = true + if err := glErr(b.funcs); err != nil { + buf.Release() + return nil, err + } + return buf, nil +} + +func glErr(f *gl.Functions) error { + if st := f.GetError(); st != gl.NO_ERROR { + return fmt.Errorf("glGetError: %#x", st) + } + return nil +} + +func (b *Backend) Release() { +} + +func (b *Backend) MemoryBarrier() { + b.funcs.MemoryBarrier(gl.ALL_BARRIER_BITS) +} + +func (b *Backend) DispatchCompute(x, y, z int) { + if p := b.state.prog; p != nil { + for binding, buf := range p.storage { + if buf != nil { + b.funcs.BindBufferBase(gl.SHADER_STORAGE_BUFFER, binding, + buf.obj) + } + } + } + b.funcs.DispatchCompute(x, y, z) +} + +func (b *Backend) BindImageTexture(unit int, tex driver.Texture, + access driver.AccessBits, f driver.TextureFormat) { + t := tex.(*gpuTexture) + var acc gl.Enum + switch access { + case driver.AccessWrite: + acc = gl.WRITE_ONLY + case driver.AccessRead: + acc = gl.READ_ONLY + default: + panic("unsupported access bits") + } + var format gl.Enum + switch f { + case driver.TextureFormatRGBA8: + format = gl.RGBA8 + default: + panic("unsupported format") + } + b.funcs.BindImageTexture(unit, t.obj, 0, false, 0, acc, format) +} + +func (b *Backend) bindTexture(unit int, t *gpuTexture) { + if b.state.texUnits[unit] != t { + b.funcs.ActiveTexture(gl.TEXTURE0 + gl.Enum(unit)) + b.funcs.BindTexture(gl.TEXTURE_2D, t.obj) + b.state.texUnits[unit] = t + } +} + +func (b *Backend) useProgram(p *gpuProgram) { + if b.state.prog != p { + p.backend.funcs.UseProgram(p.obj) + b.state.prog = p + } +} + +func (b *Backend) enableVertexArrays(n int) { + // Enable needed arrays. + for i := b.state.nattr; i < n; i++ { + b.funcs.EnableVertexAttribArray(gl.Attrib(i)) + } + // Disable extra arrays. + for i := n; i < b.state.nattr; i++ { + b.funcs.DisableVertexAttribArray(gl.Attrib(i)) + } + b.state.nattr = n +} + +func (b *Backend) SetDepthTest(enable bool) { + if enable { + b.funcs.Enable(gl.DEPTH_TEST) + } else { + b.funcs.Disable(gl.DEPTH_TEST) + } +} + +func (b *Backend) BlendFunc(sfactor, dfactor driver.BlendFactor) { + b.funcs.BlendFunc(toGLBlendFactor(sfactor), toGLBlendFactor(dfactor)) +} + +func toGLBlendFactor(f driver.BlendFactor) gl.Enum { + switch f { + case driver.BlendFactorOne: + return gl.ONE + case driver.BlendFactorOneMinusSrcAlpha: + return gl.ONE_MINUS_SRC_ALPHA + case driver.BlendFactorZero: + return gl.ZERO + case driver.BlendFactorDstColor: + return gl.DST_COLOR + default: + panic("unsupported blend factor") + } +} + +func (b *Backend) DepthMask(mask bool) { + b.funcs.DepthMask(mask) +} + +func (b *Backend) SetBlend(enable bool) { + if enable { + b.funcs.Enable(gl.BLEND) + } else { + b.funcs.Disable(gl.BLEND) + } +} + +func (b *Backend) DrawElements(mode driver.DrawMode, off, count int) { + b.prepareDraw() + // off is in 16-bit indices, but DrawElements take a byte offset. + byteOff := off * 2 + b.funcs.DrawElements(toGLDrawMode(mode), count, gl.UNSIGNED_SHORT, byteOff) +} + +func (b *Backend) DrawArrays(mode driver.DrawMode, off, count int) { + b.prepareDraw() + b.funcs.DrawArrays(toGLDrawMode(mode), off, count) +} + +func (b *Backend) prepareDraw() { + nattr := b.state.prog.nattr + b.enableVertexArrays(nattr) + if nattr > 0 { + b.setupVertexArrays() + } + if p := b.state.prog; p != nil { + p.updateUniforms() + } +} + +func toGLDrawMode(mode driver.DrawMode) gl.Enum { + switch mode { + case driver.DrawModeTriangleStrip: + return gl.TRIANGLE_STRIP + case driver.DrawModeTriangles: + return gl.TRIANGLES + default: + panic("unsupported draw mode") + } +} + +func (b *Backend) Viewport(x, y, width, height int) { + b.funcs.Viewport(x, y, width, height) +} + +func (b *Backend) Clear(colR, colG, colB, colA float32) { + b.funcs.ClearColor(colR, colG, colB, colA) + b.funcs.Clear(gl.COLOR_BUFFER_BIT) +} + +func (b *Backend) ClearDepth(d float32) { + b.funcs.ClearDepthf(d) + b.funcs.Clear(gl.DEPTH_BUFFER_BIT) +} + +func (b *Backend) DepthFunc(f driver.DepthFunc) { + var glfunc gl.Enum + switch f { + case driver.DepthFuncGreater: + glfunc = gl.GREATER + case driver.DepthFuncGreaterEqual: + glfunc = gl.GEQUAL + default: + panic("unsupported depth func") + } + b.funcs.DepthFunc(glfunc) +} + +func (b *Backend) NewInputLayout(vs driver.ShaderSources, + layout []driver.InputDesc) (driver.InputLayout, error) { + if len(vs.Inputs) != len(layout) { + return nil, fmt.Errorf("NewInputLayout: got %d inputs, expected %d", + len(layout), len(vs.Inputs)) + } + for i, inp := range vs.Inputs { + if exp, got := inp.Size, layout[i].Size; exp != got { + return nil, fmt.Errorf("NewInputLayout: data size mismatch for %q: got %d expected %d", + inp.Name, got, exp) + } + } + return &gpuInputLayout{ + inputs: vs.Inputs, + layout: layout, + }, nil +} + +func (b *Backend) NewComputeProgram(src driver.ShaderSources) (driver.Program, + error) { + p, err := gl.CreateComputeProgram(b.funcs, src.GLSL310ES) + if err != nil { + return nil, fmt.Errorf("%s: %v", src.Name, err) + } + gpuProg := &gpuProgram{ + backend: b, + obj: p, + } + return gpuProg, nil +} + +func (b *Backend) NewProgram(vertShader, fragShader driver.ShaderSources) (driver.Program, + error) { + attr := make([]string, len(vertShader.Inputs)) + for _, inp := range vertShader.Inputs { + attr[inp.Location] = inp.Name + } + vsrc, fsrc := vertShader.GLSL100ES, fragShader.GLSL100ES + if b.glver[0] >= 3 { + // OpenGL (ES) 3.0. + switch { + case b.gles: + vsrc, fsrc = vertShader.GLSL300ES, fragShader.GLSL300ES + case b.glver[0] >= 4 || b.glver[1] >= 2: + // OpenGL 3.2 Core only accepts glsl 1.50 or newer. + vsrc, fsrc = vertShader.GLSL150, fragShader.GLSL150 + default: + vsrc, fsrc = vertShader.GLSL130, fragShader.GLSL130 + } + } + p, err := gl.CreateProgram(b.funcs, vsrc, fsrc, attr) + if err != nil { + return nil, err + } + gpuProg := &gpuProgram{ + backend: b, + obj: p, + nattr: len(attr), + } + b.BindProgram(gpuProg) + // Bind texture uniforms. + for _, tex := range vertShader.Textures { + u := b.funcs.GetUniformLocation(p, tex.Name) + if u.Valid() { + b.funcs.Uniform1i(u, tex.Binding) + } + } + for _, tex := range fragShader.Textures { + u := b.funcs.GetUniformLocation(p, tex.Name) + if u.Valid() { + b.funcs.Uniform1i(u, tex.Binding) + } + } + if b.ubo { + for _, block := range vertShader.Uniforms.Blocks { + blockIdx := b.funcs.GetUniformBlockIndex(p, block.Name) + if blockIdx != gl.INVALID_INDEX { + b.funcs.UniformBlockBinding(p, blockIdx, uint(block.Binding)) + } + } + // To match Direct3D 11 with separate vertex and fragment + // shader uniform buffers, offset all fragment blocks to be + // located after the vertex blocks. + off := len(vertShader.Uniforms.Blocks) + for _, block := range fragShader.Uniforms.Blocks { + blockIdx := b.funcs.GetUniformBlockIndex(p, block.Name) + if blockIdx != gl.INVALID_INDEX { + b.funcs.UniformBlockBinding(p, blockIdx, + uint(block.Binding+off)) + } + } + } else { + gpuProg.vertUniforms.setup(b.funcs, p, vertShader.Uniforms.Size, + vertShader.Uniforms.Locations) + gpuProg.fragUniforms.setup(b.funcs, p, fragShader.Uniforms.Size, + fragShader.Uniforms.Locations) + } + return gpuProg, nil +} + +func lookupUniform(funcs *gl.Functions, p gl.Program, + loc driver.UniformLocation) uniformLocation { + u := funcs.GetUniformLocation(p, loc.Name) + if !u.Valid() { + panic(fmt.Errorf("uniform %q not found", loc.Name)) + } + return uniformLocation{uniform: u, offset: loc.Offset, typ: loc.Type, + size: loc.Size} +} + +func (p *gpuProgram) SetStorageBuffer(binding int, buffer driver.Buffer) { + buf := buffer.(*gpuBuffer) + if buf.typ&driver.BufferBindingShaderStorage == 0 { + panic("not a shader storage buffer") + } + p.storage[binding] = buf +} + +func (p *gpuProgram) SetVertexUniforms(buffer driver.Buffer) { + p.vertUniforms.setBuffer(buffer) +} + +func (p *gpuProgram) SetFragmentUniforms(buffer driver.Buffer) { + p.fragUniforms.setBuffer(buffer) +} + +func (p *gpuProgram) updateUniforms() { + f := p.backend.funcs + if p.backend.ubo { + if b := p.vertUniforms.buf; b != nil { + f.BindBufferBase(gl.UNIFORM_BUFFER, 0, b.obj) + } + if b := p.fragUniforms.buf; b != nil { + f.BindBufferBase(gl.UNIFORM_BUFFER, 1, b.obj) + } + } else { + p.vertUniforms.update(f) + p.fragUniforms.update(f) + } +} + +func (b *Backend) BindProgram(prog driver.Program) { + p := prog.(*gpuProgram) + b.useProgram(p) +} + +func (p *gpuProgram) Release() { + p.backend.funcs.DeleteProgram(p.obj) +} + +func (u *uniformsTracker) setup(funcs *gl.Functions, p gl.Program, + uniformSize int, uniforms []driver.UniformLocation) { + u.locs = make([]uniformLocation, len(uniforms)) + for i, uniform := range uniforms { + u.locs[i] = lookupUniform(funcs, p, uniform) + } + u.size = uniformSize +} + +func (u *uniformsTracker) setBuffer(buffer driver.Buffer) { + buf := buffer.(*gpuBuffer) + if buf.typ&driver.BufferBindingUniforms == 0 { + panic("not a uniform buffer") + } + if buf.size < u.size { + panic(fmt.Errorf("uniform buffer too small, got %d need %d", buf.size, + u.size)) + } + u.buf = buf + // Force update. + u.version = buf.version - 1 +} + +func (p *uniformsTracker) update(funcs *gl.Functions) { + b := p.buf + if b == nil || b.version == p.version { + return + } + p.version = b.version + data := b.data + for _, u := range p.locs { + data := data[u.offset:] + switch { + case u.typ == driver.DataTypeFloat && u.size == 1: + data := data[:4] + v := *(*[1]float32)(unsafe.Pointer(&data[0])) + funcs.Uniform1f(u.uniform, v[0]) + case u.typ == driver.DataTypeFloat && u.size == 2: + data := data[:8] + v := *(*[2]float32)(unsafe.Pointer(&data[0])) + funcs.Uniform2f(u.uniform, v[0], v[1]) + case u.typ == driver.DataTypeFloat && u.size == 3: + data := data[:12] + v := *(*[3]float32)(unsafe.Pointer(&data[0])) + funcs.Uniform3f(u.uniform, v[0], v[1], v[2]) + case u.typ == driver.DataTypeFloat && u.size == 4: + data := data[:16] + v := *(*[4]float32)(unsafe.Pointer(&data[0])) + funcs.Uniform4f(u.uniform, v[0], v[1], v[2], v[3]) + default: + panic("unsupported uniform data type or size") + } + } +} + +func (b *gpuBuffer) Upload(data []byte) { + if b.immutable { + panic("immutable buffer") + } + if len(data) > b.size { + panic("buffer size overflow") + } + b.version++ + copy(b.data, data) + if b.hasBuffer { + firstBinding := firstBufferType(b.typ) + b.backend.funcs.BindBuffer(firstBinding, b.obj) + if len(data) == b.size { + // the iOS GL implementation doesn't recognize when BufferSubData + // clears the entire buffer. Tell it and avoid GPU stalls. + // See also https://github.com/godotengine/godot/issues/23956. + b.backend.funcs.BufferData(firstBinding, b.size, gl.DYNAMIC_DRAW) + } + b.backend.funcs.BufferSubData(firstBinding, 0, data) + } +} + +func (b *gpuBuffer) Download(data []byte) error { + if len(data) > b.size { + panic("buffer size overflow") + } + if !b.hasBuffer { + copy(data, b.data) + return nil + } + firstBinding := firstBufferType(b.typ) + b.backend.funcs.BindBuffer(firstBinding, b.obj) + bufferMap := b.backend.funcs.MapBufferRange(firstBinding, 0, len(data), + gl.MAP_READ_BIT) + if bufferMap == nil { + return fmt.Errorf("MapBufferRange: error %#x", + b.backend.funcs.GetError()) + } + copy(data, bufferMap) + if !b.backend.funcs.UnmapBuffer(firstBinding) { + return driver.ErrContentLost + } + return nil +} + +func (b *gpuBuffer) Release() { + if b.hasBuffer { + b.backend.funcs.DeleteBuffer(b.obj) + b.hasBuffer = false + } +} + +func (b *Backend) BindVertexBuffer(buf driver.Buffer, stride, offset int) { + gbuf := buf.(*gpuBuffer) + if gbuf.typ&driver.BufferBindingVertices == 0 { + panic("not a vertex buffer") + } + b.state.buffer = bufferBinding{buf: gbuf, stride: stride, offset: offset} +} + +func (b *Backend) setupVertexArrays() { + layout := b.state.layout + if layout == nil { + return + } + buf := b.state.buffer + b.funcs.BindBuffer(gl.ARRAY_BUFFER, buf.buf.obj) + for i, inp := range layout.inputs { + l := layout.layout[i] + var gltyp gl.Enum + switch l.Type { + case driver.DataTypeFloat: + gltyp = gl.FLOAT + case driver.DataTypeShort: + gltyp = gl.SHORT + default: + panic("unsupported data type") + } + b.funcs.VertexAttribPointer(gl.Attrib(inp.Location), l.Size, gltyp, + false, buf.stride, buf.offset+l.Offset) + } +} + +func (b *Backend) BindIndexBuffer(buf driver.Buffer) { + gbuf := buf.(*gpuBuffer) + if gbuf.typ&driver.BufferBindingIndices == 0 { + panic("not an index buffer") + } + b.funcs.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, gbuf.obj) +} + +func (b *Backend) BlitFramebuffer(dst, src driver.Framebuffer, + srect, drect image.Rectangle) { + b.funcs.BindFramebuffer(gl.DRAW_FRAMEBUFFER, dst.(*gpuFramebuffer).obj) + b.funcs.BindFramebuffer(gl.READ_FRAMEBUFFER, src.(*gpuFramebuffer).obj) + b.funcs.BlitFramebuffer( + srect.Min.X, srect.Min.Y, srect.Max.X, srect.Max.Y, + drect.Min.X, drect.Min.Y, drect.Max.X, drect.Max.Y, + gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT|gl.STENCIL_BUFFER_BIT, + gl.NEAREST) +} + +func (f *gpuFramebuffer) ReadPixels(src image.Rectangle, pixels []byte) error { + glErr(f.backend.funcs) + f.backend.BindFramebuffer(f) + if len(pixels) < src.Dx()*src.Dy()*4 { + return errors.New("unexpected RGBA size") + } + f.backend.funcs.ReadPixels(src.Min.X, src.Min.Y, src.Dx(), src.Dy(), + gl.RGBA, gl.UNSIGNED_BYTE, pixels) + return glErr(f.backend.funcs) +} + +func (b *Backend) BindFramebuffer(fbo driver.Framebuffer) { + b.funcs.BindFramebuffer(gl.FRAMEBUFFER, fbo.(*gpuFramebuffer).obj) +} + +func (f *gpuFramebuffer) Invalidate() { + f.backend.BindFramebuffer(f) + f.backend.funcs.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0) +} + +func (f *gpuFramebuffer) Release() { + if f.foreign { + panic("framebuffer not created by NewFramebuffer") + } + f.backend.funcs.DeleteFramebuffer(f.obj) + if f.hasDepth { + f.backend.funcs.DeleteRenderbuffer(f.depthBuf) + } +} + +func toTexFilter(f driver.TextureFilter) int { + switch f { + case driver.FilterNearest: + return gl.NEAREST + case driver.FilterLinear: + return gl.LINEAR + default: + panic("unsupported texture filter") + } +} + +func (b *Backend) BindTexture(unit int, t driver.Texture) { + b.bindTexture(unit, t.(*gpuTexture)) +} + +func (t *gpuTexture) Release() { + t.backend.funcs.DeleteTexture(t.obj) +} + +func (t *gpuTexture) Upload(offset, size image.Point, pixels []byte) { + if min := size.X * size.Y * 4; min > len(pixels) { + panic(fmt.Errorf("size %d larger than data %d", min, len(pixels))) + } + t.backend.BindTexture(0, t) + t.backend.funcs.TexSubImage2D(gl.TEXTURE_2D, 0, offset.X, offset.Y, size.X, + size.Y, t.triple.format, t.triple.typ, pixels) +} + +func (t *gpuTimer) Begin() { + t.funcs.BeginQuery(gl.TIME_ELAPSED_EXT, t.obj) +} + +func (t *gpuTimer) End() { + t.funcs.EndQuery(gl.TIME_ELAPSED_EXT) +} + +func (t *gpuTimer) ready() bool { + return t.funcs.GetQueryObjectuiv(t.obj, + gl.QUERY_RESULT_AVAILABLE) == gl.TRUE +} + +func (t *gpuTimer) Release() { + t.funcs.DeleteQuery(t.obj) +} + +func (t *gpuTimer) Duration() (time.Duration, bool) { + if !t.ready() { + return 0, false + } + nanos := t.funcs.GetQueryObjectuiv(t.obj, gl.QUERY_RESULT) + return time.Duration(nanos), true +} + +func (b *Backend) BindInputLayout(l driver.InputLayout) { + b.state.layout = l.(*gpuInputLayout) +} + +func (l *gpuInputLayout) Release() {} + +// floatTripleFor determines the best texture triple for floating point FBOs. +func floatTripleFor(f *gl.Functions, ver [2]int, exts []string) (textureTriple, + error) { + var triples []textureTriple + if ver[0] >= 3 { + triples = append(triples, + textureTriple{gl.R16F, gl.Enum(gl.RED), gl.Enum(gl.HALF_FLOAT)}) + } + // According to the OES_texture_half_float specification, EXT_color_buffer_half_float is needed to + // render to FBOs. However, the Safari WebGL1 implementation does support half-float FBOs but does not + // report EXT_color_buffer_half_float support. The triples are verified below, so it doesn't matter if we're + // wrong. + if hasExtension(exts, "GL_OES_texture_half_float") || hasExtension(exts, + "GL_EXT_color_buffer_half_float") { + // Try single channel. + triples = append(triples, + textureTriple{gl.LUMINANCE, gl.Enum(gl.LUMINANCE), + gl.Enum(gl.HALF_FLOAT_OES)}) + // Fallback to 4 channels. + triples = append(triples, textureTriple{gl.RGBA, gl.Enum(gl.RGBA), + gl.Enum(gl.HALF_FLOAT_OES)}) + } + if hasExtension(exts, "GL_OES_texture_float") || hasExtension(exts, + "GL_EXT_color_buffer_float") { + triples = append(triples, + textureTriple{gl.RGBA, gl.Enum(gl.RGBA), gl.Enum(gl.FLOAT)}) + } + tex := f.CreateTexture() + defer f.DeleteTexture(tex) + f.BindTexture(gl.TEXTURE_2D, tex) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) + fbo := f.CreateFramebuffer() + defer f.DeleteFramebuffer(fbo) + defFBO := gl.Framebuffer(f.GetBinding(gl.FRAMEBUFFER_BINDING)) + f.BindFramebuffer(gl.FRAMEBUFFER, fbo) + defer f.BindFramebuffer(gl.FRAMEBUFFER, defFBO) + var attempts []string + for _, tt := range triples { + const size = 256 + f.TexImage2D(gl.TEXTURE_2D, 0, tt.internalFormat, size, size, tt.format, + tt.typ) + f.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, tex, 0) + st := f.CheckFramebufferStatus(gl.FRAMEBUFFER) + if st == gl.FRAMEBUFFER_COMPLETE { + return tt, nil + } + attempts = append(attempts, + fmt.Sprintf("(0x%x, 0x%x, 0x%x): 0x%x", tt.internalFormat, + tt.format, tt.typ, st)) + } + return textureTriple{}, fmt.Errorf("floating point fbos not supported (attempted %s)", + attempts) +} + +func srgbaTripleFor(ver [2]int, exts []string) (textureTriple, error) { + switch { + case ver[0] >= 3: + return textureTriple{gl.SRGB8_ALPHA8, gl.Enum(gl.RGBA), + gl.Enum(gl.UNSIGNED_BYTE)}, nil + case hasExtension(exts, "GL_EXT_sRGB"): + return textureTriple{gl.SRGB_ALPHA_EXT, gl.Enum(gl.SRGB_ALPHA_EXT), + gl.Enum(gl.UNSIGNED_BYTE)}, nil + default: + return textureTriple{}, errors.New("no sRGB texture formats found") + } +} + +func alphaTripleFor(ver [2]int) textureTriple { + intf, f := gl.Enum(gl.R8), gl.Enum(gl.RED) + if ver[0] < 3 { + // R8, RED not supported on OpenGL ES 2.0. + intf, f = gl.LUMINANCE, gl.Enum(gl.LUMINANCE) + } + return textureTriple{intf, f, gl.UNSIGNED_BYTE} +} + +func hasExtension(exts []string, ext string) bool { + for _, e := range exts { + if ext == e { + return true + } + } + return false +} + +func firstBufferType(typ driver.BufferBinding) gl.Enum { + switch { + case typ&driver.BufferBindingIndices != 0: + return gl.ELEMENT_ARRAY_BUFFER + case typ&driver.BufferBindingVertices != 0: + return gl.ARRAY_BUFFER + case typ&driver.BufferBindingUniforms != 0: + return gl.UNIFORM_BUFFER + case typ&driver.BufferBindingShaderStorage != 0: + return gl.SHADER_STORAGE_BUFFER + default: + panic("unsupported buffer type") + } +} diff --git a/gio/gpu/internal/rendertest/bench_test.go b/gio/gpu/internal/rendertest/bench_test.go new file mode 100644 index 0000000..ac4ec5f --- /dev/null +++ b/gio/gpu/internal/rendertest/bench_test.go @@ -0,0 +1,321 @@ +package rendertest + +import ( + "image" + "image/color" + "math" + "testing" + + "realy.lol/gio/f32" + "realy.lol/gio/font/gofont" + "realy.lol/gio/gpu/headless" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/widget/material" +) + +// use some global variables for benchmarking so as to not pollute +// the reported allocs with allocations that we do not want to count. +var ( + c1, c2, c3 = make(chan op.CallOp), make(chan op.CallOp), make(chan op.CallOp) + op1, op2, op3 op.Ops +) + +func setupBenchmark(b *testing.B) (layout.Context, *headless.Window, + *material.Theme) { + sz := image.Point{X: 1024, Y: 1200} + w := newWindow(b, sz.X, sz.Y) + ops := new(op.Ops) + gtx := layout.Context{ + Ops: ops, + Constraints: layout.Exact(sz), + } + th := material.NewTheme(gofont.Collection()) + return gtx, w, th +} + +func resetOps(gtx layout.Context) { + gtx.Ops.Reset() + op1.Reset() + op2.Reset() + op3.Reset() +} + +func finishBenchmark(b *testing.B, w *headless.Window) { + b.StopTimer() + if *dumpImages { + img, err := w.Screenshot() + w.Release() + if err != nil { + b.Error(err) + } + if err := saveImage(b.Name()+".png", img); err != nil { + b.Error(err) + } + } +} + +func BenchmarkDrawUICached(b *testing.B) { + // As BenchmarkDraw but the same op.Ops every time that is not reset - this + // should thus allow for maximal cache usage. + gtx, w, th := setupBenchmark(b) + drawCore(gtx, th) + w.Frame(gtx.Ops) + b.ResetTimer() + for i := 0; i < b.N; i++ { + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func BenchmarkDrawUI(b *testing.B) { + // BenchmarkDraw is intended as a reasonable overall benchmark for + // the drawing performance of the full drawing pipeline, in each iteration + // resetting the ops and drawing, similar to how a typical UI would function. + // This will allow font caching across frames. + gtx, w, th := setupBenchmark(b) + drawCore(gtx, th) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + + p := op.Save(gtx.Ops) + off := float32(math.Mod(float64(i)/10, 10)) + op.Offset(f32.Pt(off, off)).Add(gtx.Ops) + + drawCore(gtx, th) + + p.Load() + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func BenchmarkDrawUITransformed(b *testing.B) { + // Like BenchmarkDraw UI but transformed at every frame + gtx, w, th := setupBenchmark(b) + drawCore(gtx, th) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + + p := op.Save(gtx.Ops) + angle := float32(math.Mod(float64(i)/1000, 0.05)) + a := f32.Affine2D{}.Shear(f32.Point{}, angle, angle).Rotate(f32.Point{}, + angle) + op.Affine(a).Add(gtx.Ops) + + drawCore(gtx, th) + + p.Load() + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func Benchmark1000Circles(b *testing.B) { + // Benchmark1000Shapes draws 1000 individual shapes such that no caching between + // shapes will be possible and resets buffers on each operation to prevent caching + // between frames. + gtx, w, _ := setupBenchmark(b) + draw1000Circles(gtx) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + draw1000Circles(gtx) + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func Benchmark1000CirclesInstanced(b *testing.B) { + // Like Benchmark1000Circles but will record them and thus allow for caching between + // them. + gtx, w, _ := setupBenchmark(b) + draw1000CirclesInstanced(gtx) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + draw1000CirclesInstanced(gtx) + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func draw1000Circles(gtx layout.Context) { + ops := gtx.Ops + for x := 0; x < 100; x++ { + p := op.Save(ops) + op.Offset(f32.Pt(float32(x*10), 0)).Add(ops) + for y := 0; y < 10; y++ { + paint.FillShape(ops, + color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, + A: 120}, + clip.RRect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5, + NW: 5}.Op(ops), + ) + op.Offset(f32.Pt(0, float32(100))).Add(ops) + } + p.Load() + } +} + +func draw1000CirclesInstanced(gtx layout.Context) { + ops := gtx.Ops + + r := op.Record(ops) + clip.RRect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5, + NW: 5}.Add(ops) + paint.PaintOp{}.Add(ops) + c := r.Stop() + + for x := 0; x < 100; x++ { + p := op.Save(ops) + op.Offset(f32.Pt(float32(x*10), 0)).Add(ops) + for y := 0; y < 10; y++ { + pi := op.Save(ops) + paint.ColorOp{Color: color.NRGBA{R: 100 + uint8(x), + G: 100 + uint8(y), B: 100, A: 120}}.Add(ops) + c.Add(ops) + pi.Load() + op.Offset(f32.Pt(0, float32(100))).Add(ops) + } + p.Load() + } +} + +func drawCore(gtx layout.Context, th *material.Theme) { + c1 := drawIndividualShapes(gtx, th) + c2 := drawShapeInstances(gtx, th) + c3 := drawText(gtx, th) + + (<-c1).Add(gtx.Ops) + (<-c2).Add(gtx.Ops) + (<-c3).Add(gtx.Ops) +} + +func drawIndividualShapes(gtx layout.Context, + th *material.Theme) chan op.CallOp { + // draw 81 rounded rectangles of different solid colors - each one individually + go func() { + ops := &op1 + c := op.Record(ops) + for x := 0; x < 9; x++ { + p := op.Save(ops) + op.Offset(f32.Pt(float32(x*50), 0)).Add(ops) + for y := 0; y < 9; y++ { + paint.FillShape(ops, + color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, + A: 120}, + clip.RRect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10, + SW: 10, NW: 10}.Op(ops), + ) + op.Offset(f32.Pt(0, float32(50))).Add(ops) + } + p.Load() + } + c1 <- c.Stop() + }() + return c1 +} + +func drawShapeInstances(gtx layout.Context, th *material.Theme) chan op.CallOp { + // draw 400 textured circle instances, each with individual transform + go func() { + ops := &op2 + co := op.Record(ops) + + r := op.Record(ops) + clip.RRect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10, SW: 10, + NW: 10}.Add(ops) + paint.PaintOp{}.Add(ops) + c := r.Stop() + + squares.Add(ops) + rad := float32(0) + for x := 0; x < 20; x++ { + for y := 0; y < 20; y++ { + p := op.Save(ops) + op.Offset(f32.Pt(float32(x*50+25), float32(y*50+25))).Add(ops) + c.Add(ops) + p.Load() + rad += math.Pi * 2 / 400 + } + } + c2 <- co.Stop() + }() + return c2 +} + +func drawText(gtx layout.Context, th *material.Theme) chan op.CallOp { + // draw 40 lines of text with different transforms. + go func() { + ops := &op3 + c := op.Record(ops) + + txt := material.H6(th, "") + for x := 0; x < 40; x++ { + txt.Text = textRows[x] + p := op.Save(ops) + op.Offset(f32.Pt(float32(0), float32(24*x))).Add(ops) + gtx.Ops = ops + txt.Layout(gtx) + p.Load() + } + c3 <- c.Stop() + }() + return c3 +} + +var textRows = []string{ + "1. I learned from my grandfather, Verus, to use good manners, and to", + "put restraint on anger. 2. In the famous memory of my father I had a", + "pattern of modesty and manliness. 3. Of my mother I learned to be", + "pious and generous; to keep myself not only from evil deeds, but even", + "from evil thoughts; and to live with a simplicity which is far from", + "customary among the rich. 4. I owe it to my great-grandfather that I", + "did not attend public lectures and discussions, but had good and able", + "teachers at home; and I owe him also the knowledge that for things of", + "this nature a man should count no expense too great.", + "5. My tutor taught me not to favour either green or blue at the", + "chariot races, nor, in the contests of gladiators, to be a supporter", + "either of light or heavy armed. He taught me also to endure labour;", + "not to need many things; to serve myself without troubling others; not", + "to intermeddle in the affairs of others, and not easily to listen to", + "slanders against them.", + "6. Of Diognetus I had the lesson not to busy myself about vain things;", + "not to credit the great professions of such as pretend to work", + "wonders, or of sorcerers about their charms, and their expelling of", + "Demons and the like; not to keep quails (for fighting or divination),", + "nor to run after such things; to suffer freedom of speech in others,", + "and to apply myself heartily to philosophy. Him also I must thank for", + "my hearing first Bacchius, then Tandasis and Marcianus; that I wrote", + "dialogues in my youth, and took a liking to the philosopher's pallet", + "and skins, and to the other things which, by the Grecian discipline,", + "belong to that profession.", + "7. To Rusticus I owe my first apprehensions that my nature needed", + "reform and cure; and that I did not fall into the ambition of the", + "common Sophists, either by composing speculative writings or by", + "declaiming harangues of exhortation in public; further, that I never", + "strove to be admired by ostentation of great patience in an ascetic", + "life, or by display of activity and application; that I gave over the", + "study of rhetoric, poetry, and the graces of language; and that I did", + "not pace my house in my senatorial robes, or practise any similar", + "affectation. I observed also the simplicity of style in his letters,", + "particularly in that which he wrote to my mother from Sinuessa. I", + "learned from him to be easily appeased, and to be readily reconciled", + "with those who had displeased me or given cause of offence, so soon as", + "they inclined to make their peace; to read with care; not to rest", + "satisfied with a slight and superficial knowledge; nor quickly to", + "assent to great talkers. I have him to thank that I met with the", +} diff --git a/gio/gpu/internal/rendertest/clip_test.go b/gio/gpu/internal/rendertest/clip_test.go new file mode 100644 index 0000000..d12bb90 --- /dev/null +++ b/gio/gpu/internal/rendertest/clip_test.go @@ -0,0 +1,581 @@ +package rendertest + +import ( + "image" + "math" + "testing" + + "golang.org/x/image/colornames" + + "realy.lol/gio/f32" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" +) + +func TestPaintRect(t *testing.T) { + run(t, func(o *op.Ops) { + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) + }, func(r result) { + r.expect(0, 0, colornames.Red) + r.expect(49, 0, colornames.Red) + r.expect(50, 0, transparent) + r.expect(10, 50, transparent) + }) +} + +func TestPaintClippedRect(t *testing.T) { + run(t, func(o *op.Ops) { + clip.RRect{Rect: f32.Rect(25, 25, 60, 60)}.Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(24, 35, transparent) + r.expect(25, 35, colornames.Red) + r.expect(50, 0, transparent) + r.expect(10, 50, transparent) + }) +} + +func TestPaintClippedCircle(t *testing.T) { + run(t, func(o *op.Ops) { + r := float32(10) + clip.RRect{Rect: f32.Rect(20, 20, 40, 40), SE: r, SW: r, NW: r, + NE: r}.Add(o) + clip.Rect(image.Rect(0, 0, 30, 50)).Add(o) + paint.Fill(o, red) + }, func(r result) { + r.expect(21, 21, transparent) + r.expect(25, 30, colornames.Red) + r.expect(31, 30, transparent) + }) +} + +func TestPaintArc(t *testing.T) { + run(t, func(o *op.Ops) { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(0, 20)) + p.Line(f32.Pt(10, 0)) + p.Arc(f32.Pt(10, 0), f32.Pt(40, 0), math.Pi) + p.Line(f32.Pt(30, 0)) + p.Line(f32.Pt(0, 25)) + p.Arc(f32.Pt(-10, 5), f32.Pt(10, 15), -math.Pi) + p.Line(f32.Pt(0, 25)) + p.Arc(f32.Pt(10, 10), f32.Pt(10, 10), 2*math.Pi) + p.Line(f32.Pt(-10, 0)) + p.Arc(f32.Pt(-10, 0), f32.Pt(-40, 0), -math.Pi) + p.Line(f32.Pt(-10, 0)) + p.Line(f32.Pt(0, -10)) + p.Arc(f32.Pt(-10, -20), f32.Pt(10, -5), math.Pi) + p.Line(f32.Pt(0, -10)) + p.Line(f32.Pt(-50, 0)) + p.Close() + clip.Outline{ + Path: p.End(), + }.Op().Add(o) + + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(0, 25, colornames.Red) + r.expect(0, 15, transparent) + }) +} + +func TestPaintAbsolute(t *testing.T) { + run(t, func(o *op.Ops) { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(100, + 100)) // offset the initial pen position to test "MoveTo" + + p.MoveTo(f32.Pt(20, 20)) + p.LineTo(f32.Pt(80, 20)) + p.QuadTo(f32.Pt(80, 80), f32.Pt(20, 80)) + p.Close() + clip.Outline{ + Path: p.End(), + }.Op().Add(o) + + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(30, 30, colornames.Red) + r.expect(79, 79, transparent) + r.expect(90, 90, transparent) + }) +} + +func TestPaintTexture(t *testing.T) { + run(t, func(o *op.Ops) { + squares.Add(o) + scale(80.0/512, 80.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(0, 0, colornames.Blue) + r.expect(79, 10, colornames.Green) + r.expect(80, 0, transparent) + r.expect(10, 80, transparent) + }) +} + +func TestTexturedStrokeClipped(t *testing.T) { + run(t, func(o *op.Ops) { + smallSquares.Add(o) + op.Offset(f32.Pt(50, 50)).Add(o) + clip.Stroke{ + Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), + Style: clip.StrokeStyle{ + Width: 10, + }, + }.Op().Add(o) + clip.RRect{Rect: f32.Rect(-30, -30, 60, 60)}.Add(o) + op.Offset(f32.Pt(-10, -10)).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + }) +} + +func TestTexturedStroke(t *testing.T) { + run(t, func(o *op.Ops) { + smallSquares.Add(o) + op.Offset(f32.Pt(50, 50)).Add(o) + clip.Stroke{ + Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), + Style: clip.StrokeStyle{ + Width: 10, + }, + }.Op().Add(o) + op.Offset(f32.Pt(-10, -10)).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + }) +} + +func TestPaintClippedTexture(t *testing.T) { + run(t, func(o *op.Ops) { + squares.Add(o) + clip.RRect{Rect: f32.Rect(0, 0, 40, 40)}.Add(o) + scale(80.0/512, 80.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(40, 40, transparent) + r.expect(25, 35, colornames.Blue) + }) +} + +func TestStrokedPathBevelFlat(t *testing.T) { + run(t, func(o *op.Ops) { + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + + paint.Fill(o, red) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Red) + }) +} + +func TestStrokedPathBevelRound(t *testing.T) { + run(t, func(o *op.Ops) { + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.RoundCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + + paint.Fill(o, red) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Red) + }) +} + +func TestStrokedPathBevelSquare(t *testing.T) { + run(t, func(o *op.Ops) { + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.SquareCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + + paint.Fill(o, red) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Red) + }) +} + +func TestStrokedPathRoundRound(t *testing.T) { + run(t, func(o *op.Ops) { + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.RoundCap, + Join: clip.RoundJoin, + }, + }.Op().Add(o) + + paint.Fill(o, red) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Red) + }) +} + +func TestStrokedPathFlatMiter(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: 5, + }, + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, colornames.Red) + }) +} + +func TestStrokedPathFlatMiterInf(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, colornames.Red) + }) +} + +func TestStrokedPathZeroWidth(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(10, 50)) + p.Line(f32.Pt(50, 0)) + clip.Stroke{ + Path: p.End(), + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + + paint.Fill(o, black) + stk.Load() + } + + { + stk := op.Save(o) + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(10, 50)) + p.Line(f32.Pt(30, 0)) + clip.Stroke{ + Path: p.End(), + }.Op().Add(o) // width=0, disable stroke + + paint.Fill(o, red) + stk.Load() + } + + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Black) + r.expect(30, 50, colornames.Black) + r.expect(65, 50, transparent) + }) +} + +func TestDashedPathFlatCapEllipse(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newEllipsePath(o) + + var dash clip.Dash + dash.Begin(o) + dash.Dash(5) + dash.Dash(3) + + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + Dashes: dash.End(), + }.Op().Add(o) + + paint.Fill( + o, + red, + ) + stk.Load() + } + { + stk := op.Save(o) + p := newEllipsePath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + }, + }.Op().Add(o) + + paint.Fill( + o, + black, + ) + stk.Load() + } + + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(0, 62, colornames.Red) + r.expect(0, 65, colornames.Black) + }) +} + +func TestDashedPathFlatCapZ(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newZigZagPath(o) + var dash clip.Dash + dash.Begin(o) + dash.Dash(5) + dash.Dash(3) + + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + Dashes: dash.End(), + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, colornames.Red) + r.expect(46, 12, transparent) + }) +} + +func TestDashedPathFlatCapZNoDash(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newZigZagPath(o) + var dash clip.Dash + dash.Begin(o) + dash.Phase(1) + + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + Dashes: dash.End(), + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + { + stk := op.Save(o) + clip.Stroke{ + Path: newZigZagPath(o), + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, colornames.Red) + r.expect(46, 12, colornames.Red) + }) +} + +func TestDashedPathFlatCapZNoPath(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + var dash clip.Dash + dash.Begin(o) + dash.Dash(0) + clip.Stroke{ + Path: newZigZagPath(o), + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + Dashes: dash.End(), + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, transparent) + r.expect(46, 12, transparent) + }) +} + +func newStrokedPath(o *op.Ops) clip.PathSpec { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(10, 50)) + p.Line(f32.Pt(10, 0)) + p.Arc(f32.Pt(10, 0), f32.Pt(20, 0), math.Pi) + p.Line(f32.Pt(10, 0)) + p.Line(f32.Pt(10, 10)) + p.Arc(f32.Pt(0, 30), f32.Pt(0, 30), 2*math.Pi) + p.Line(f32.Pt(-20, 0)) + p.Quad(f32.Pt(-10, -10), f32.Pt(-30, 30)) + return p.End() +} + +func newZigZagPath(o *op.Ops) clip.PathSpec { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(40, 10)) + p.Line(f32.Pt(50, 0)) + p.Line(f32.Pt(-50, 50)) + p.Line(f32.Pt(50, 0)) + p.Quad(f32.Pt(-50, 20), f32.Pt(-50, 50)) + p.Line(f32.Pt(50, 0)) + return p.End() +} + +func newEllipsePath(o *op.Ops) clip.PathSpec { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(0, 65)) + p.Line(f32.Pt(20, 0)) + p.Arc(f32.Pt(20, 0), f32.Pt(70, 0), 2*math.Pi) + return p.End() +} diff --git a/gio/gpu/internal/rendertest/doc.go b/gio/gpu/internal/rendertest/doc.go new file mode 100644 index 0000000..9f6948e --- /dev/null +++ b/gio/gpu/internal/rendertest/doc.go @@ -0,0 +1,2 @@ +// Package rendertest is intended for testing of drawing ops only. +package rendertest diff --git a/gio/gpu/internal/rendertest/refs/TestBuildOffscreen.png b/gio/gpu/internal/rendertest/refs/TestBuildOffscreen.png new file mode 100644 index 0000000..fb50427 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestBuildOffscreen.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png b/gio/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png new file mode 100644 index 0000000..8ff717b Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestClipOffset.png b/gio/gpu/internal/rendertest/refs/TestClipOffset.png new file mode 100644 index 0000000..6396fb4 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestClipOffset.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestClipPaintOffset.png b/gio/gpu/internal/rendertest/refs/TestClipPaintOffset.png new file mode 100644 index 0000000..0fe37e6 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestClipPaintOffset.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestClipRotate.png b/gio/gpu/internal/rendertest/refs/TestClipRotate.png new file mode 100644 index 0000000..e6c15e3 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestClipRotate.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestClipScale.png b/gio/gpu/internal/rendertest/refs/TestClipScale.png new file mode 100644 index 0000000..6396fb4 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestClipScale.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestComplicatedTransform.png b/gio/gpu/internal/rendertest/refs/TestComplicatedTransform.png new file mode 100644 index 0000000..4a92e3c Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestComplicatedTransform.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png new file mode 100644 index 0000000..79bae38 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png new file mode 100644 index 0000000..12212e9 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png new file mode 100644 index 0000000..d315f0f Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png new file mode 100644 index 0000000..94c160e Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestDeferredPaint.png b/gio/gpu/internal/rendertest/refs/TestDeferredPaint.png new file mode 100644 index 0000000..b562f12 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestDeferredPaint.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestDepthOverlap.png b/gio/gpu/internal/rendertest/refs/TestDepthOverlap.png new file mode 100644 index 0000000..9d416b9 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestDepthOverlap.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestLinearGradient.png b/gio/gpu/internal/rendertest/refs/TestLinearGradient.png new file mode 100644 index 0000000..c3c007c Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestLinearGradient.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestLinearGradientAngled.png b/gio/gpu/internal/rendertest/refs/TestLinearGradientAngled.png new file mode 100644 index 0000000..3ba0734 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestLinearGradientAngled.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestNegativeOverlaps.png b/gio/gpu/internal/rendertest/refs/TestNegativeOverlaps.png new file mode 100644 index 0000000..fb50427 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestNegativeOverlaps.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestNoClipFromPaint.png b/gio/gpu/internal/rendertest/refs/TestNoClipFromPaint.png new file mode 100644 index 0000000..e774064 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestNoClipFromPaint.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png b/gio/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png new file mode 100644 index 0000000..515a4d2 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestOffsetTexture.png b/gio/gpu/internal/rendertest/refs/TestOffsetTexture.png new file mode 100644 index 0000000..87386e8 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestOffsetTexture.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestPaintAbsolute.png b/gio/gpu/internal/rendertest/refs/TestPaintAbsolute.png new file mode 100644 index 0000000..dd09760 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintAbsolute.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestPaintArc.png b/gio/gpu/internal/rendertest/refs/TestPaintArc.png new file mode 100644 index 0000000..f432914 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintArc.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestPaintClippedBorder.png b/gio/gpu/internal/rendertest/refs/TestPaintClippedBorder.png new file mode 100644 index 0000000..f8fcfbb Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintClippedBorder.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestPaintClippedCircle.png b/gio/gpu/internal/rendertest/refs/TestPaintClippedCircle.png new file mode 100644 index 0000000..bdf1fce Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintClippedCircle.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestPaintClippedCirle.png b/gio/gpu/internal/rendertest/refs/TestPaintClippedCirle.png new file mode 100644 index 0000000..c8cf2f6 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintClippedCirle.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestPaintClippedRect.png b/gio/gpu/internal/rendertest/refs/TestPaintClippedRect.png new file mode 100644 index 0000000..c1dd7a0 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintClippedRect.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestPaintClippedTexture.png b/gio/gpu/internal/rendertest/refs/TestPaintClippedTexture.png new file mode 100644 index 0000000..ae0e066 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintClippedTexture.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestPaintOffset.png b/gio/gpu/internal/rendertest/refs/TestPaintOffset.png new file mode 100644 index 0000000..82394d5 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintOffset.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestPaintRect.png b/gio/gpu/internal/rendertest/refs/TestPaintRect.png new file mode 100644 index 0000000..f942601 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintRect.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestPaintRotate.png b/gio/gpu/internal/rendertest/refs/TestPaintRotate.png new file mode 100644 index 0000000..fe15d7d Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintRotate.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestPaintShear.png b/gio/gpu/internal/rendertest/refs/TestPaintShear.png new file mode 100644 index 0000000..6d1a4c9 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintShear.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestPaintTexture.png b/gio/gpu/internal/rendertest/refs/TestPaintTexture.png new file mode 100644 index 0000000..9120231 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintTexture.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png b/gio/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png new file mode 100644 index 0000000..da201dc Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestReuseStencil.png b/gio/gpu/internal/rendertest/refs/TestReuseStencil.png new file mode 100644 index 0000000..349db1f Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestReuseStencil.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestRotateClipTexture.png b/gio/gpu/internal/rendertest/refs/TestRotateClipTexture.png new file mode 100644 index 0000000..56c3182 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestRotateClipTexture.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestRotateTexture.png b/gio/gpu/internal/rendertest/refs/TestRotateTexture.png new file mode 100644 index 0000000..e56c972 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestRotateTexture.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png new file mode 100644 index 0000000..9d442f5 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png new file mode 100644 index 0000000..a37235c Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png new file mode 100644 index 0000000..8d2919d Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png new file mode 100644 index 0000000..ae6472a Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png new file mode 100644 index 0000000..d315f0f Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png new file mode 100644 index 0000000..8ef5a94 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png new file mode 100644 index 0000000..0fc6fe8 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestTexturedStroke.png b/gio/gpu/internal/rendertest/refs/TestTexturedStroke.png new file mode 100644 index 0000000..637c932 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestTexturedStroke.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png b/gio/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png new file mode 100644 index 0000000..637c932 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestTransformMacro.png b/gio/gpu/internal/rendertest/refs/TestTransformMacro.png new file mode 100644 index 0000000..a9cce29 Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestTransformMacro.png differ diff --git a/gio/gpu/internal/rendertest/refs/TestTransformOrder.png b/gio/gpu/internal/rendertest/refs/TestTransformOrder.png new file mode 100644 index 0000000..720ca3c Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestTransformOrder.png differ diff --git a/gio/gpu/internal/rendertest/render_test.go b/gio/gpu/internal/rendertest/render_test.go new file mode 100644 index 0000000..efa60a6 --- /dev/null +++ b/gio/gpu/internal/rendertest/render_test.go @@ -0,0 +1,358 @@ +package rendertest + +import ( + "image" + "image/color" + "math" + "testing" + + "golang.org/x/image/colornames" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" +) + +func TestTransformMacro(t *testing.T) { + // testcase resulting from original bug when rendering layout.Stacked + + // Build clip-path. + c := constSqPath() + + run(t, func(o *op.Ops) { + + // render the first Stacked item + m1 := op.Record(o) + dr := image.Rect(0, 0, 128, 50) + paint.FillShape(o, black, clip.Rect(dr).Op()) + c1 := m1.Stop() + + // Render the second stacked item + m2 := op.Record(o) + paint.ColorOp{Color: red}.Add(o) + // Simulate a draw text call + stack := op.Save(o) + op.Offset(f32.Pt(0, 10)).Add(o) + + // Apply the clip-path. + c.Add(o) + + paint.PaintOp{}.Add(o) + stack.Load() + + c2 := m2.Stop() + + // Call each of them in a transform + s1 := op.Save(o) + op.Offset(f32.Pt(0, 0)).Add(o) + c1.Add(o) + s1.Load() + s2 := op.Save(o) + op.Offset(f32.Pt(0, 0)).Add(o) + c2.Add(o) + s2.Load() + }, func(r result) { + r.expect(5, 15, colornames.Red) + r.expect(15, 15, colornames.Black) + r.expect(11, 51, transparent) + }) +} + +func TestRepeatedPaintsZ(t *testing.T) { + run(t, func(o *op.Ops) { + // Draw a rectangle + paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 128, 50)).Op()) + + builder := clip.Path{} + builder.Begin(o) + builder.Move(f32.Pt(0, 0)) + builder.Line(f32.Pt(10, 0)) + builder.Line(f32.Pt(0, 10)) + builder.Line(f32.Pt(-10, 0)) + builder.Line(f32.Pt(0, -10)) + p := builder.End() + clip.Outline{ + Path: p, + }.Op().Add(o) + paint.Fill(o, red) + }, func(r result) { + r.expect(5, 5, colornames.Red) + r.expect(11, 15, colornames.Black) + r.expect(11, 51, transparent) + }) +} + +func TestNoClipFromPaint(t *testing.T) { + // ensure that a paint operation does not pollute the state + // by leaving any clip paths in place. + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Rotate(f32.Pt(20, 20), math.Pi/4) + op.Affine(a).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(10, 10, 30, 30)).Op()) + a = f32.Affine2D{}.Rotate(f32.Pt(20, 20), -math.Pi/4) + op.Affine(a).Add(o) + + paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) + }, func(r result) { + r.expect(1, 1, colornames.Black) + r.expect(20, 20, colornames.Black) + r.expect(49, 49, colornames.Black) + r.expect(51, 51, transparent) + }) +} + +func TestDeferredPaint(t *testing.T) { + run(t, func(o *op.Ops) { + state := op.Save(o) + clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o) + paint.ColorOp{Color: color.NRGBA{A: 0xff, G: 0xff}}.Add(o) + paint.PaintOp{}.Add(o) + + op.Affine(f32.Affine2D{}.Offset(f32.Pt(20, 20))).Add(o) + m := op.Record(o) + clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o) + paint.ColorOp{Color: color.NRGBA{A: 0xff, R: 0xff, G: 0xff}}.Add(o) + paint.PaintOp{}.Add(o) + paintMacro := m.Stop() + op.Defer(o, paintMacro) + + state.Load() + op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Add(o) + clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o) + paint.ColorOp{Color: color.NRGBA{A: 0xff, B: 0xff}}.Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + }) +} + +func constSqPath() op.CallOp { + innerOps := new(op.Ops) + m := op.Record(innerOps) + builder := clip.Path{} + builder.Begin(innerOps) + builder.Move(f32.Pt(0, 0)) + builder.Line(f32.Pt(10, 0)) + builder.Line(f32.Pt(0, 10)) + builder.Line(f32.Pt(-10, 0)) + builder.Line(f32.Pt(0, -10)) + p := builder.End() + clip.Outline{Path: p}.Op().Add(innerOps) + return m.Stop() +} + +func constSqCirc() op.CallOp { + innerOps := new(op.Ops) + m := op.Record(innerOps) + clip.RRect{Rect: f32.Rect(0, 0, 40, 40), + NW: 20, NE: 20, SW: 20, SE: 20}.Add(innerOps) + return m.Stop() +} + +func drawChild(ops *op.Ops, text op.CallOp) op.CallOp { + r1 := op.Record(ops) + text.Add(ops) + paint.PaintOp{}.Add(ops) + return r1.Stop() +} + +func TestReuseStencil(t *testing.T) { + txt := constSqPath() + run(t, func(ops *op.Ops) { + c1 := drawChild(ops, txt) + c2 := drawChild(ops, txt) + + // lay out the children + stack1 := op.Save(ops) + c1.Add(ops) + stack1.Load() + + stack2 := op.Save(ops) + op.Offset(f32.Pt(0, 50)).Add(ops) + c2.Add(ops) + stack2.Load() + }, func(r result) { + r.expect(5, 5, colornames.Black) + r.expect(5, 55, colornames.Black) + }) +} + +func TestBuildOffscreen(t *testing.T) { + // Check that something we in one frame build outside the screen + // still is rendered correctly if moved into the screen in a later + // frame. + + txt := constSqCirc() + draw := func(off float32, o *op.Ops) { + s := op.Save(o) + op.Offset(f32.Pt(0, off)).Add(o) + txt.Add(o) + paint.PaintOp{}.Add(o) + s.Load() + } + + multiRun(t, + frame( + func(ops *op.Ops) { + draw(-100, ops) + }, func(r result) { + r.expect(5, 5, transparent) + r.expect(20, 20, transparent) + }), + frame( + func(ops *op.Ops) { + draw(0, ops) + }, func(r result) { + r.expect(2, 2, transparent) + r.expect(20, 20, colornames.Black) + r.expect(38, 38, transparent) + })) +} + +func TestNegativeOverlaps(t *testing.T) { + run(t, func(ops *op.Ops) { + clip.RRect{Rect: f32.Rect(50, 50, 100, 100)}.Add(ops) + clip.Rect(image.Rect(0, 120, 100, 122)).Add(ops) + paint.PaintOp{}.Add(ops) + }, func(r result) { + r.expect(60, 60, transparent) + r.expect(60, 110, transparent) + r.expect(60, 120, transparent) + r.expect(60, 122, transparent) + }) +} + +func TestDepthOverlap(t *testing.T) { + run(t, func(ops *op.Ops) { + stack := op.Save(ops) + paint.FillShape(ops, red, clip.Rect{Max: image.Pt(128, 64)}.Op()) + stack.Load() + + stack = op.Save(ops) + paint.FillShape(ops, green, clip.Rect{Max: image.Pt(64, 128)}.Op()) + stack.Load() + }, func(r result) { + r.expect(96, 32, colornames.Red) + r.expect(32, 96, colornames.Green) + r.expect(32, 32, colornames.Green) + }) +} + +type Gradient struct { + From, To color.NRGBA +} + +var gradients = []Gradient{ + {From: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF}, + To: color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}}, + {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}, + To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}}, + {From: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}, + To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}}, + {From: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}, + To: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}}, + {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0xFF, A: 0xFF}, + To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}}, + {From: color.NRGBA{R: 0xFF, G: 0xFF, B: 0x19, A: 0xFF}, + To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}}, +} + +func TestLinearGradient(t *testing.T) { + t.Skip("linear gradients don't support transformations") + + const gradienth = 8 + // 0.5 offset from ends to ensure that the center of the pixel + // aligns with gradient from and to colors. + pixelAligned := f32.Rect(0.5, 0, 127.5, gradienth) + samples := []int{0, 12, 32, 64, 96, 115, 127} + + run(t, func(ops *op.Ops) { + gr := f32.Rect(0, 0, 128, gradienth) + for _, g := range gradients { + paint.LinearGradientOp{ + Stop1: f32.Pt(gr.Min.X, gr.Min.Y), + Color1: g.From, + Stop2: f32.Pt(gr.Max.X, gr.Min.Y), + Color2: g.To, + }.Add(ops) + st := op.Save(ops) + clip.RRect{Rect: gr}.Add(ops) + op.Affine(f32.Affine2D{}.Offset(pixelAligned.Min)).Add(ops) + scale(pixelAligned.Dx()/128, 1).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + gr = gr.Add(f32.Pt(0, gradienth)) + } + }, func(r result) { + gr := pixelAligned + for _, g := range gradients { + from := f32color.LinearFromSRGB(g.From) + to := f32color.LinearFromSRGB(g.To) + for _, p := range samples { + exp := lerp(from, to, float32(p)/float32(r.img.Bounds().Dx()-1)) + r.expect(p, int(gr.Min.Y+gradienth/2), + f32color.NRGBAToRGBA(exp.SRGB())) + } + gr = gr.Add(f32.Pt(0, gradienth)) + } + }) +} + +func TestLinearGradientAngled(t *testing.T) { + run(t, func(ops *op.Ops) { + paint.LinearGradientOp{ + Stop1: f32.Pt(64, 64), + Color1: black, + Stop2: f32.Pt(0, 0), + Color2: red, + }.Add(ops) + st := op.Save(ops) + clip.Rect(image.Rect(0, 0, 64, 64)).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + + paint.LinearGradientOp{ + Stop1: f32.Pt(64, 64), + Color1: white, + Stop2: f32.Pt(128, 0), + Color2: green, + }.Add(ops) + st = op.Save(ops) + clip.Rect(image.Rect(64, 0, 128, 64)).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + + paint.LinearGradientOp{ + Stop1: f32.Pt(64, 64), + Color1: black, + Stop2: f32.Pt(128, 128), + Color2: blue, + }.Add(ops) + st = op.Save(ops) + clip.Rect(image.Rect(64, 64, 128, 128)).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + + paint.LinearGradientOp{ + Stop1: f32.Pt(64, 64), + Color1: white, + Stop2: f32.Pt(0, 128), + Color2: magenta, + }.Add(ops) + st = op.Save(ops) + clip.Rect(image.Rect(0, 64, 64, 128)).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + }, func(r result) {}) +} + +// lerp calculates linear interpolation with color b and p. +func lerp(a, b f32color.RGBA, p float32) f32color.RGBA { + return f32color.RGBA{ + R: a.R*(1-p) + b.R*p, + G: a.G*(1-p) + b.G*p, + B: a.B*(1-p) + b.B*p, + A: a.A*(1-p) + b.A*p, + } +} diff --git a/gio/gpu/internal/rendertest/transform_test.go b/gio/gpu/internal/rendertest/transform_test.go new file mode 100644 index 0000000..b00aa7e --- /dev/null +++ b/gio/gpu/internal/rendertest/transform_test.go @@ -0,0 +1,204 @@ +package rendertest + +import ( + "image" + "math" + "testing" + + "golang.org/x/image/colornames" + + "realy.lol/gio/f32" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" +) + +func TestPaintOffset(t *testing.T) { + run(t, func(o *op.Ops) { + op.Offset(f32.Pt(10, 20)).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(59, 30, colornames.Red) + r.expect(60, 30, transparent) + r.expect(10, 70, transparent) + }) +} + +func TestPaintRotate(t *testing.T) { + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/8) + op.Affine(a).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(20, 20, 60, 60)).Op()) + }, func(r result) { + r.expect(40, 40, colornames.Red) + r.expect(50, 19, colornames.Red) + r.expect(59, 19, transparent) + r.expect(21, 21, transparent) + }) +} + +func TestPaintShear(t *testing.T) { + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0) + op.Affine(a).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 40, 40)).Op()) + }, func(r result) { + r.expect(10, 30, transparent) + }) +} + +func TestClipPaintOffset(t *testing.T) { + run(t, func(o *op.Ops) { + clip.RRect{Rect: f32.Rect(10, 10, 30, 30)}.Add(o) + op.Offset(f32.Pt(20, 20)).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 100, 100)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(19, 19, transparent) + r.expect(20, 20, colornames.Red) + r.expect(30, 30, transparent) + }) +} + +func TestClipOffset(t *testing.T) { + run(t, func(o *op.Ops) { + op.Offset(f32.Pt(20, 20)).Add(o) + clip.RRect{Rect: f32.Rect(10, 10, 30, 30)}.Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 100, 100)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(29, 29, transparent) + r.expect(30, 30, colornames.Red) + r.expect(49, 49, colornames.Red) + r.expect(50, 50, transparent) + }) +} + +func TestClipScale(t *testing.T) { + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 2)).Offset(f32.Pt(10, + 10)) + op.Affine(a).Add(o) + clip.RRect{Rect: f32.Rect(10, 10, 20, 20)}.Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 1000, 1000)).Op()) + }, func(r result) { + r.expect(19+10, 19+10, transparent) + r.expect(20+10, 20+10, colornames.Red) + r.expect(39+10, 39+10, colornames.Red) + r.expect(40+10, 40+10, transparent) + }) +} + +func TestClipRotate(t *testing.T) { + run(t, func(o *op.Ops) { + op.Affine(f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/4)).Add(o) + clip.RRect{Rect: f32.Rect(30, 30, 50, 50)}.Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 40, 100, 100)).Op()) + }, func(r result) { + r.expect(39, 39, transparent) + r.expect(41, 41, colornames.Red) + r.expect(50, 50, transparent) + }) +} + +func TestOffsetTexture(t *testing.T) { + run(t, func(o *op.Ops) { + op.Offset(f32.Pt(15, 15)).Add(o) + squares.Add(o) + scale(50.0/512, 50.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(14, 20, transparent) + r.expect(66, 20, transparent) + r.expect(16, 64, colornames.Green) + r.expect(64, 16, colornames.Green) + }) +} + +func TestOffsetScaleTexture(t *testing.T) { + run(t, func(o *op.Ops) { + op.Offset(f32.Pt(15, 15)).Add(o) + squares.Add(o) + op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 1))).Add(o) + scale(50.0/512, 50.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(114, 64, colornames.Blue) + r.expect(116, 64, transparent) + }) +} + +func TestRotateTexture(t *testing.T) { + run(t, func(o *op.Ops) { + defer op.Save(o).Load() + squares.Add(o) + a := f32.Affine2D{}.Offset(f32.Pt(30, 30)).Rotate(f32.Pt(40, 40), + math.Pi/4) + op.Affine(a).Add(o) + scale(20.0/512, 20.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(40, 40-12, colornames.Blue) + r.expect(40+12, 40, colornames.Green) + }) +} + +func TestRotateClipTexture(t *testing.T) { + run(t, func(o *op.Ops) { + squares.Add(o) + a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), math.Pi/8) + op.Affine(a).Add(o) + clip.RRect{Rect: f32.Rect(30, 30, 50, 50)}.Add(o) + op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Add(o) + scale(60.0/512, 60.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(37, 39, colornames.Green) + r.expect(36, 39, colornames.Green) + r.expect(35, 39, colornames.Green) + r.expect(34, 39, colornames.Green) + r.expect(33, 39, colornames.Green) + }) +} + +func TestComplicatedTransform(t *testing.T) { + run(t, func(o *op.Ops) { + squares.Add(o) + + clip.RRect{Rect: f32.Rect(0, 0, 100, 100), SE: 50, SW: 50, NW: 50, + NE: 50}.Add(o) + + a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0) + op.Affine(a).Add(o) + clip.RRect{Rect: f32.Rect(0, 0, 50, 40)}.Add(o) + + scale(50.0/512, 50.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(20, 5, transparent) + }) +} + +func TestTransformOrder(t *testing.T) { + // check the ordering of operations bot in affine and in gpu stack. + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Offset(f32.Pt(64, 64)) + op.Affine(a).Add(o) + + b := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(8, 8)) + op.Affine(b).Add(o) + + c := f32.Affine2D{}.Offset(f32.Pt(-10, -10)).Scale(f32.Point{}, + f32.Pt(0.5, 0.5)) + op.Affine(c).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 20, 20)).Op()) + }, func(r result) { + // centered and with radius 40 + r.expect(64-41, 64, transparent) + r.expect(64-39, 64, colornames.Red) + r.expect(64+39, 64, colornames.Red) + r.expect(64+41, 64, transparent) + }) +} diff --git a/gio/gpu/internal/rendertest/util_test.go b/gio/gpu/internal/rendertest/util_test.go new file mode 100644 index 0000000..74c6f5f --- /dev/null +++ b/gio/gpu/internal/rendertest/util_test.go @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package rendertest + +import ( + "bytes" + "flag" + "fmt" + "image" + "image/color" + "image/draw" + "image/png" + "io/ioutil" + "path/filepath" + "strconv" + "testing" + + "golang.org/x/image/colornames" + + "realy.lol/gio/f32" + "realy.lol/gio/gpu/headless" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/op" + "realy.lol/gio/op/paint" +) + +var ( + dumpImages = flag.Bool("saveimages", false, "save test images") + squares paint.ImageOp + smallSquares paint.ImageOp +) + +var ( + red = f32color.RGBAToNRGBA(colornames.Red) + green = f32color.RGBAToNRGBA(colornames.Green) + blue = f32color.RGBAToNRGBA(colornames.Blue) + magenta = f32color.RGBAToNRGBA(colornames.Magenta) + black = f32color.RGBAToNRGBA(colornames.Black) + white = f32color.RGBAToNRGBA(colornames.White) + transparent = color.RGBA{} +) + +func init() { + squares = buildSquares(512) + smallSquares = buildSquares(50) +} + +func buildSquares(size int) paint.ImageOp { + sub := size / 4 + im := image.NewNRGBA(image.Rect(0, 0, size, size)) + c1, c2 := image.NewUniform(colornames.Green), image.NewUniform(colornames.Blue) + for r := 0; r < 4; r++ { + for c := 0; c < 4; c++ { + c1, c2 = c2, c1 + draw.Draw(im, image.Rect(r*sub, c*sub, r*sub+sub, c*sub+sub), c1, + image.Point{}, draw.Over) + } + c1, c2 = c2, c1 + } + return paint.NewImageOp(im) +} + +func drawImage(t *testing.T, size int, ops *op.Ops, + draw func(o *op.Ops)) (im *image.RGBA, err error) { + sz := image.Point{X: size, Y: size} + w := newWindow(t, sz.X, sz.Y) + draw(ops) + if err := w.Frame(ops); err != nil { + return nil, err + } + return w.Screenshot() +} + +func run(t *testing.T, f func(o *op.Ops), c func(r result)) { + // draw a few times and check that it is correct each time, to + // ensure any caching effects still generate the correct images. + var img *image.RGBA + var err error + ops := new(op.Ops) + for i := 0; i < 3; i++ { + ops.Reset() + img, err = drawImage(t, 128, ops, f) + if err != nil { + t.Error("error rendering:", err) + return + } + // check for a reference image and make sure we are identical. + if !verifyRef(t, img, 0) { + name := fmt.Sprintf("%s-%d-bad.png", t.Name(), i) + if err := saveImage(name, img); err != nil { + t.Error(err) + } + } + c(result{t: t, img: img}) + } + + if *dumpImages { + if err := saveImage(t.Name()+".png", img); err != nil { + t.Error(err) + } + } +} + +func frame(f func(o *op.Ops), c func(r result)) frameT { + return frameT{f: f, c: c} +} + +type frameT struct { + f func(o *op.Ops) + c func(r result) +} + +// multiRun is used to run test cases over multiple frames, typically +// to test caching interactions. +func multiRun(t *testing.T, frames ...frameT) { + // draw a few times and check that it is correct each time, to + // ensure any caching effects still generate the correct images. + var img *image.RGBA + var err error + sz := image.Point{X: 128, Y: 128} + w := newWindow(t, sz.X, sz.Y) + ops := new(op.Ops) + for i := range frames { + ops.Reset() + frames[i].f(ops) + if err := w.Frame(ops); err != nil { + t.Errorf("rendering failed: %v", err) + continue + } + img, err = w.Screenshot() + if err != nil { + t.Errorf("screenshot failed: %v", err) + continue + } + // Check for a reference image and make sure they are identical. + ok := verifyRef(t, img, i) + if frames[i].c != nil { + frames[i].c(result{t: t, img: img}) + } + if *dumpImages || !ok { + name := t.Name() + ".png" + if i != 0 { + name = t.Name() + "_" + strconv.Itoa(i) + ".png" + } + if err := saveImage(name, img); err != nil { + t.Error(err) + } + } + } + +} + +func verifyRef(t *testing.T, img *image.RGBA, frame int) (ok bool) { + // ensure identical to ref data + path := filepath.Join("refs", t.Name()+".png") + if frame != 0 { + path = filepath.Join("refs", t.Name()+"_"+strconv.Itoa(frame)+".png") + } + b, err := ioutil.ReadFile(path) + if err != nil { + t.Error("could not open ref:", err) + return + } + r, err := png.Decode(bytes.NewReader(b)) + if err != nil { + t.Error("could not decode ref:", err) + return + } + if img.Bounds() != r.Bounds() { + t.Errorf("reference image is %v, expected %v", r.Bounds(), img.Bounds()) + return false + } + var ref *image.RGBA + switch r := r.(type) { + case *image.RGBA: + ref = r + case *image.NRGBA: + ref = image.NewRGBA(r.Bounds()) + bnd := r.Bounds() + for x := bnd.Min.X; x < bnd.Max.X; x++ { + for y := bnd.Min.Y; y < bnd.Max.Y; y++ { + ref.SetRGBA(x, y, f32color.NRGBAToRGBA(r.NRGBAAt(x, y))) + } + } + default: + t.Fatalf("reference image is a %T, expected *image.NRGBA or *image.RGBA", + r) + } + bnd := img.Bounds() + for x := bnd.Min.X; x < bnd.Max.X; x++ { + for y := bnd.Min.Y; y < bnd.Max.Y; y++ { + exp := ref.RGBAAt(x, y) + got := img.RGBAAt(x, y) + if !colorsClose(exp, got) { + t.Error("not equal to ref at", x, y, " ", got, exp) + return false + } + } + } + return true +} + +func colorsClose(c1, c2 color.RGBA) bool { + const delta = 0.01 // magic value obtained from experimentation. + return yiqEqApprox(c1, c2, delta) +} + +// yiqEqApprox compares the colors of 2 pixels, in the NTSC YIQ color space, +// as described in: +// +// Measuring perceived color difference using YIQ NTSC +// transmission color space in mobile applications. +// Yuriy Kotsarenko, Fernando Ramos. +// +// An electronic version is available at: +// +// - http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf +func yiqEqApprox(c1, c2 color.RGBA, d2 float64) bool { + const max = 35215.0 // difference between 2 maximally different pixels. + + var ( + r1 = float64(c1.R) + g1 = float64(c1.G) + b1 = float64(c1.B) + + r2 = float64(c2.R) + g2 = float64(c2.G) + b2 = float64(c2.B) + + y1 = r1*0.29889531 + g1*0.58662247 + b1*0.11448223 + i1 = r1*0.59597799 - g1*0.27417610 - b1*0.32180189 + q1 = r1*0.21147017 - g1*0.52261711 + b1*0.31114694 + + y2 = r2*0.29889531 + g2*0.58662247 + b2*0.11448223 + i2 = r2*0.59597799 - g2*0.27417610 - b2*0.32180189 + q2 = r2*0.21147017 - g2*0.52261711 + b2*0.31114694 + + y = y1 - y2 + i = i1 - i2 + q = q1 - q2 + + diff = 0.5053*y*y + 0.299*i*i + 0.1957*q*q + ) + return diff <= max*d2 +} + +func (r result) expect(x, y int, col color.RGBA) { + r.t.Helper() + if r.img == nil { + return + } + c := r.img.RGBAAt(x, y) + if !colorsClose(c, col) { + r.t.Error("expected ", col, " at ", "(", x, ",", y, ") but got ", c) + } +} + +type result struct { + t *testing.T + img *image.RGBA +} + +func saveImage(file string, img *image.RGBA) error { + // Only NRGBA images are losslessly encoded by png.Encode. + nrgba := image.NewNRGBA(img.Bounds()) + bnd := img.Bounds() + for x := bnd.Min.X; x < bnd.Max.X; x++ { + for y := bnd.Min.Y; y < bnd.Max.Y; y++ { + nrgba.SetNRGBA(x, y, f32color.RGBAToNRGBA(img.RGBAAt(x, y))) + } + } + var buf bytes.Buffer + if err := png.Encode(&buf, nrgba); err != nil { + return err + } + return ioutil.WriteFile(file, buf.Bytes(), 0666) +} + +func newWindow(t testing.TB, width, height int) *headless.Window { + w, err := headless.NewWindow(width, height) + if err != nil { + t.Skipf("failed to create headless window, skipping: %v", err) + } + t.Cleanup(w.Release) + return w +} + +func scale(sx, sy float32) op.TransformOp { + return op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(sx, sy))) +} diff --git a/gio/gpu/pack.go b/gio/gpu/pack.go new file mode 100644 index 0000000..c4dbaad --- /dev/null +++ b/gio/gpu/pack.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "image" +) + +// packer packs a set of many smaller rectangles into +// much fewer larger atlases. +type packer struct { + maxDim int + spaces []image.Rectangle + + sizes []image.Point + pos image.Point +} + +type placement struct { + Idx int + Pos image.Point +} + +// add adds the given rectangle to the atlases and +// return the allocated position. +func (p *packer) add(s image.Point) (placement, bool) { + if place, ok := p.tryAdd(s); ok { + return place, true + } + p.newPage() + return p.tryAdd(s) +} + +func (p *packer) clear() { + p.sizes = p.sizes[:0] + p.spaces = p.spaces[:0] +} + +func (p *packer) newPage() { + p.pos = image.Point{} + p.sizes = append(p.sizes, image.Point{}) + p.spaces = p.spaces[:0] + p.spaces = append(p.spaces, image.Rectangle{ + Max: image.Point{X: p.maxDim, Y: p.maxDim}, + }) +} + +func (p *packer) tryAdd(s image.Point) (placement, bool) { + // Go backwards to prioritize smaller spaces first. + for i := len(p.spaces) - 1; i >= 0; i-- { + space := p.spaces[i] + rightSpace := space.Dx() - s.X + bottomSpace := space.Dy() - s.Y + if rightSpace >= 0 && bottomSpace >= 0 { + // Remove space. + p.spaces[i] = p.spaces[len(p.spaces)-1] + p.spaces = p.spaces[:len(p.spaces)-1] + // Put s in the top left corner and add the (at most) + // two smaller spaces. + pos := space.Min + if bottomSpace > 0 { + p.spaces = append(p.spaces, image.Rectangle{ + Min: image.Point{X: pos.X, Y: pos.Y + s.Y}, + Max: image.Point{X: space.Max.X, Y: space.Max.Y}, + }) + } + if rightSpace > 0 { + p.spaces = append(p.spaces, image.Rectangle{ + Min: image.Point{X: pos.X + s.X, Y: pos.Y}, + Max: image.Point{X: space.Max.X, Y: pos.Y + s.Y}, + }) + } + idx := len(p.sizes) - 1 + size := &p.sizes[idx] + if x := pos.X + s.X; x > size.X { + size.X = x + } + if y := pos.Y + s.Y; y > size.Y { + size.Y = y + } + return placement{Idx: idx, Pos: pos}, true + } + } + return placement{}, false +} diff --git a/gio/gpu/path.go b/gio/gpu/path.go new file mode 100644 index 0000000..4670f03 --- /dev/null +++ b/gio/gpu/path.go @@ -0,0 +1,438 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +// GPU accelerated path drawing using the algorithms from +// Pathfinder (https://github.com/servo/pathfinder). + +import ( + "encoding/binary" + "image" + "math" + "unsafe" + + "realy.lol/gio/f32" + "realy.lol/gio/gpu/internal/driver" + "realy.lol/gio/internal/byteslice" + "realy.lol/gio/internal/f32color" +) + +type pather struct { + ctx driver.Device + + viewport image.Point + + stenciler *stenciler + coverer *coverer +} + +type coverer struct { + ctx driver.Device + prog [3]*program + texUniforms *coverTexUniforms + colUniforms *coverColUniforms + linearGradientUniforms *coverLinearGradientUniforms + layout driver.InputLayout +} + +type coverTexUniforms struct { + vert struct { + coverUniforms + _ [12]byte // Padding to multiple of 16. + } +} + +type coverColUniforms struct { + vert struct { + coverUniforms + _ [12]byte // Padding to multiple of 16. + } + frag struct { + colorUniforms + } +} + +type coverLinearGradientUniforms struct { + vert struct { + coverUniforms + _ [12]byte // Padding to multiple of 16. + } + frag struct { + gradientUniforms + } +} + +type coverUniforms struct { + transform [4]float32 + uvCoverTransform [4]float32 + uvTransformR1 [4]float32 + uvTransformR2 [4]float32 + z float32 +} + +type stenciler struct { + ctx driver.Device + prog struct { + prog *program + uniforms *stencilUniforms + layout driver.InputLayout + } + iprog struct { + prog *program + uniforms *intersectUniforms + layout driver.InputLayout + } + fbos fboSet + intersections fboSet + indexBuf driver.Buffer +} + +type stencilUniforms struct { + vert struct { + transform [4]float32 + pathOffset [2]float32 + _ [8]byte // Padding to multiple of 16. + } +} + +type intersectUniforms struct { + vert struct { + uvTransform [4]float32 + subUVTransform [4]float32 + } +} + +type fboSet struct { + fbos []stencilFBO +} + +type stencilFBO struct { + size image.Point + fbo driver.Framebuffer + tex driver.Texture +} + +type pathData struct { + ncurves int + data driver.Buffer +} + +// vertex data suitable for passing to vertex programs. +type vertex struct { + // Corner encodes the corner: +0.5 for south, +.25 for east. + Corner float32 + MaxY float32 + FromX, FromY float32 + CtrlX, CtrlY float32 + ToX, ToY float32 +} + +func (v vertex) encode(d []byte, maxy uint32) { + bo := binary.LittleEndian + bo.PutUint32(d[0:], math.Float32bits(v.Corner)) + bo.PutUint32(d[4:], maxy) + bo.PutUint32(d[8:], math.Float32bits(v.FromX)) + bo.PutUint32(d[12:], math.Float32bits(v.FromY)) + bo.PutUint32(d[16:], math.Float32bits(v.CtrlX)) + bo.PutUint32(d[20:], math.Float32bits(v.CtrlY)) + bo.PutUint32(d[24:], math.Float32bits(v.ToX)) + bo.PutUint32(d[28:], math.Float32bits(v.ToY)) +} + +const ( + // Number of path quads per draw batch. + pathBatchSize = 10000 + // Size of a vertex as sent to gpu + vertStride = 8 * 4 +) + +func newPather(ctx driver.Device) *pather { + return &pather{ + ctx: ctx, + stenciler: newStenciler(ctx), + coverer: newCoverer(ctx), + } +} + +func newCoverer(ctx driver.Device) *coverer { + c := &coverer{ + ctx: ctx, + } + c.colUniforms = new(coverColUniforms) + c.texUniforms = new(coverTexUniforms) + c.linearGradientUniforms = new(coverLinearGradientUniforms) + prog, layout, err := createColorPrograms(ctx, shader_cover_vert, + shader_cover_frag, + [3]interface{}{&c.colUniforms.vert, &c.linearGradientUniforms.vert, + &c.texUniforms.vert}, + [3]interface{}{&c.colUniforms.frag, &c.linearGradientUniforms.frag, + nil}, + ) + if err != nil { + panic(err) + } + c.prog = prog + c.layout = layout + return c +} + +func newStenciler(ctx driver.Device) *stenciler { + // Allocate a suitably large index buffer for drawing paths. + indices := make([]uint16, pathBatchSize*6) + for i := 0; i < pathBatchSize; i++ { + i := uint16(i) + indices[i*6+0] = i*4 + 0 + indices[i*6+1] = i*4 + 1 + indices[i*6+2] = i*4 + 2 + indices[i*6+3] = i*4 + 2 + indices[i*6+4] = i*4 + 1 + indices[i*6+5] = i*4 + 3 + } + indexBuf, err := ctx.NewImmutableBuffer(driver.BufferBindingIndices, + byteslice.Slice(indices)) + if err != nil { + panic(err) + } + progLayout, err := ctx.NewInputLayout(shader_stencil_vert, + []driver.InputDesc{ + {Type: driver.DataTypeFloat, Size: 1, + Offset: int(unsafe.Offsetof((*(*vertex)(nil)).Corner))}, + {Type: driver.DataTypeFloat, Size: 1, + Offset: int(unsafe.Offsetof((*(*vertex)(nil)).MaxY))}, + {Type: driver.DataTypeFloat, Size: 2, + Offset: int(unsafe.Offsetof((*(*vertex)(nil)).FromX))}, + {Type: driver.DataTypeFloat, Size: 2, + Offset: int(unsafe.Offsetof((*(*vertex)(nil)).CtrlX))}, + {Type: driver.DataTypeFloat, Size: 2, + Offset: int(unsafe.Offsetof((*(*vertex)(nil)).ToX))}, + }) + if err != nil { + panic(err) + } + iprogLayout, err := ctx.NewInputLayout(shader_intersect_vert, + []driver.InputDesc{ + {Type: driver.DataTypeFloat, Size: 2, Offset: 0}, + {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2}, + }) + if err != nil { + panic(err) + } + st := &stenciler{ + ctx: ctx, + indexBuf: indexBuf, + } + prog, err := ctx.NewProgram(shader_stencil_vert, shader_stencil_frag) + if err != nil { + panic(err) + } + st.prog.uniforms = new(stencilUniforms) + vertUniforms := newUniformBuffer(ctx, &st.prog.uniforms.vert) + st.prog.prog = newProgram(prog, vertUniforms, nil) + st.prog.layout = progLayout + iprog, err := ctx.NewProgram(shader_intersect_vert, shader_intersect_frag) + if err != nil { + panic(err) + } + st.iprog.uniforms = new(intersectUniforms) + vertUniforms = newUniformBuffer(ctx, &st.iprog.uniforms.vert) + st.iprog.prog = newProgram(iprog, vertUniforms, nil) + st.iprog.layout = iprogLayout + return st +} + +func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) { + // Add fbos. + for i := len(s.fbos); i < len(sizes); i++ { + s.fbos = append(s.fbos, stencilFBO{}) + } + // Resize fbos. + for i, sz := range sizes { + f := &s.fbos[i] + // Resizing or recreating FBOs can introduce rendering stalls. + // Avoid if the space waste is not too high. + resize := sz.X > f.size.X || sz.Y > f.size.Y + waste := float32(sz.X*sz.Y) / float32(f.size.X*f.size.Y) + resize = resize || waste > 1.2 + if resize { + if f.fbo != nil { + f.fbo.Release() + f.tex.Release() + } + tex, err := ctx.NewTexture(driver.TextureFormatFloat, sz.X, sz.Y, + driver.FilterNearest, driver.FilterNearest, + driver.BufferBindingTexture|driver.BufferBindingFramebuffer) + if err != nil { + panic(err) + } + fbo, err := ctx.NewFramebuffer(tex, 0) + if err != nil { + panic(err) + } + f.size = sz + f.tex = tex + f.fbo = fbo + } + } + // Delete extra fbos. + s.delete(ctx, len(sizes)) +} + +func (s *fboSet) invalidate(ctx driver.Device) { + for _, f := range s.fbos { + f.fbo.Invalidate() + } +} + +func (s *fboSet) delete(ctx driver.Device, idx int) { + for i := idx; i < len(s.fbos); i++ { + f := s.fbos[i] + f.fbo.Release() + f.tex.Release() + } + s.fbos = s.fbos[:idx] +} + +func (s *stenciler) release() { + s.fbos.delete(s.ctx, 0) + s.prog.layout.Release() + s.prog.prog.Release() + s.iprog.layout.Release() + s.iprog.prog.Release() + s.indexBuf.Release() +} + +func (p *pather) release() { + p.stenciler.release() + p.coverer.release() +} + +func (c *coverer) release() { + for _, p := range c.prog { + p.Release() + } + c.layout.Release() +} + +func buildPath(ctx driver.Device, p []byte) pathData { + buf, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices, p) + if err != nil { + panic(err) + } + return pathData{ + ncurves: len(p) / vertStride, + data: buf, + } +} + +func (p pathData) release() { + p.data.Release() +} + +func (p *pather) begin(sizes []image.Point) { + p.stenciler.begin(sizes) +} + +func (p *pather) stencilPath(bounds image.Rectangle, offset f32.Point, + uv image.Point, data pathData) { + p.stenciler.stencilPath(bounds, offset, uv, data) +} + +func (s *stenciler) beginIntersect(sizes []image.Point) { + s.ctx.BlendFunc(driver.BlendFactorDstColor, driver.BlendFactorZero) + // 8 bit coverage is enough, but OpenGL ES only supports single channel + // floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if + // no floating point support is available. + s.intersections.resize(s.ctx, sizes) + s.ctx.BindProgram(s.iprog.prog.prog) +} + +func (s *stenciler) invalidateFBO() { + s.intersections.invalidate(s.ctx) + s.fbos.invalidate(s.ctx) +} + +func (s *stenciler) cover(idx int) stencilFBO { + return s.fbos.fbos[idx] +} + +func (s *stenciler) begin(sizes []image.Point) { + s.ctx.BlendFunc(driver.BlendFactorOne, driver.BlendFactorOne) + s.fbos.resize(s.ctx, sizes) + s.ctx.BindProgram(s.prog.prog.prog) + s.ctx.BindInputLayout(s.prog.layout) + s.ctx.BindIndexBuffer(s.indexBuf) +} + +func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, + uv image.Point, data pathData) { + s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy()) + // Transform UI coordinates to OpenGL coordinates. + texSize := f32.Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())} + scale := f32.Point{X: 2 / texSize.X, Y: 2 / texSize.Y} + orig := f32.Point{X: -1 - float32(bounds.Min.X)*2/texSize.X, + Y: -1 - float32(bounds.Min.Y)*2/texSize.Y} + s.prog.uniforms.vert.transform = [4]float32{scale.X, scale.Y, orig.X, + orig.Y} + s.prog.uniforms.vert.pathOffset = [2]float32{offset.X, offset.Y} + s.prog.prog.UploadUniforms() + // Draw in batches that fit in uint16 indices. + start := 0 + nquads := data.ncurves / 4 + for start < nquads { + batch := nquads - start + if max := pathBatchSize; batch > max { + batch = max + } + off := vertStride * start * 4 + s.ctx.BindVertexBuffer(data.data, vertStride, off) + s.ctx.DrawElements(driver.DrawModeTriangles, 0, batch*6) + start += batch + } +} + +func (p *pather) cover(z float32, mat materialType, col f32color.RGBA, + col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, + coverScale, coverOff f32.Point) { + p.coverer.cover(z, mat, col, col1, col2, scale, off, uvTrans, coverScale, + coverOff) +} + +func (c *coverer) cover(z float32, mat materialType, col f32color.RGBA, + col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, + coverScale, coverOff f32.Point) { + p := c.prog[mat] + c.ctx.BindProgram(p.prog) + var uniforms *coverUniforms + switch mat { + case materialColor: + c.colUniforms.frag.color = col + uniforms = &c.colUniforms.vert.coverUniforms + case materialLinearGradient: + c.linearGradientUniforms.frag.color1 = col1 + c.linearGradientUniforms.frag.color2 = col2 + + t1, t2, t3, t4, t5, t6 := uvTrans.Elems() + c.linearGradientUniforms.vert.uvTransformR1 = [4]float32{t1, t2, t3, 0} + c.linearGradientUniforms.vert.uvTransformR2 = [4]float32{t4, t5, t6, 0} + uniforms = &c.linearGradientUniforms.vert.coverUniforms + case materialTexture: + t1, t2, t3, t4, t5, t6 := uvTrans.Elems() + c.texUniforms.vert.uvTransformR1 = [4]float32{t1, t2, t3, 0} + c.texUniforms.vert.uvTransformR2 = [4]float32{t4, t5, t6, 0} + uniforms = &c.texUniforms.vert.coverUniforms + } + uniforms.z = z + uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y} + uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, + coverOff.X, coverOff.Y} + p.UploadUniforms() + c.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4) +} + +func init() { + // Check that struct vertex has the expected size and + // that it contains no padding. + if unsafe.Sizeof(*(*vertex)(nil)) != vertStride { + panic("unexpected struct size") + } +} diff --git a/gio/gpu/shaders.go b/gio/gpu/shaders.go new file mode 100644 index 0000000..7df7cb5 --- /dev/null +++ b/gio/gpu/shaders.go @@ -0,0 +1,6694 @@ +// Code generated by build.go. DO NOT EDIT. + +package gpu + +import "realy.lol/gio/gpu/internal/driver" + +var ( + shader_backdrop_comp = driver.ShaderSources{ + Name: "backdrop.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct AnnotatedTag +{ + uint tag; + uint flags; +}; + +struct PathRef +{ + uint offset; +}; + +struct TileRef +{ + uint offset; +}; + +struct Path +{ + uvec4 bbox; + TileRef tiles; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _77; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _191; + +shared uint sh_row_width[128]; +shared Alloc sh_row_alloc[128]; +shared uint sh_row_count[128]; + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _77.memory[offset]; + return v; +} + +AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +uint fill_mode_from_flags(uint flags) +{ + return flags & 1u; +} + +Path Path_read(Alloc a, PathRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Path s; + s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16)); + s.tiles = TileRef(raw2); + return s; +} + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _77.memory[offset] = val; +} + +void main() +{ + if (_77.mem_error != 0u) + { + return; + } + uint th_ix = gl_LocalInvocationID.x; + uint element_ix = gl_GlobalInvocationID.x; + AnnotatedRef ref = AnnotatedRef(_191.conf.anno_alloc.offset + (element_ix * 32u)); + uint row_count = 0u; + if (element_ix < _191.conf.n_elements) + { + Alloc param; + param.offset = _191.conf.anno_alloc.offset; + AnnotatedRef param_1 = ref; + AnnotatedTag tag = Annotated_tag(param, param_1); + switch (tag.tag) + { + case 2u: + case 3u: + case 1u: + { + uint param_2 = tag.flags; + if (fill_mode_from_flags(param_2) != 0u) + { + break; + } + PathRef path_ref = PathRef(_191.conf.tile_alloc.offset + (element_ix * 12u)); + Alloc param_3; + param_3.offset = _191.conf.tile_alloc.offset; + PathRef param_4 = path_ref; + Path path = Path_read(param_3, param_4); + sh_row_width[th_ix] = path.bbox.z - path.bbox.x; + row_count = path.bbox.w - path.bbox.y; + bool _267 = row_count == 1u; + bool _273; + if (_267) + { + _273 = path.bbox.y > 0u; + } + else + { + _273 = _267; + } + if (_273) + { + row_count = 0u; + } + uint param_5 = path.tiles.offset; + uint param_6 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u; + Alloc path_alloc = new_alloc(param_5, param_6); + sh_row_alloc[th_ix] = path_alloc; + break; + } + } + } + sh_row_count[th_ix] = row_count; + for (uint i = 0u; i < 7u; i++) + { + barrier(); + if (th_ix >= uint(1 << int(i))) + { + row_count += sh_row_count[th_ix - uint(1 << int(i))]; + } + barrier(); + sh_row_count[th_ix] = row_count; + } + barrier(); + uint total_rows = sh_row_count[127]; + uint _395; + for (uint row = th_ix; row < total_rows; row += 128u) + { + uint el_ix = 0u; + for (uint i_1 = 0u; i_1 < 7u; i_1++) + { + uint probe = el_ix + uint(64 >> int(i_1)); + if (row >= sh_row_count[probe - 1u]) + { + el_ix = probe; + } + } + uint width = sh_row_width[el_ix]; + if (width > 0u) + { + Alloc tiles_alloc = sh_row_alloc[el_ix]; + if (el_ix > 0u) + { + _395 = sh_row_count[el_ix - 1u]; + } + else + { + _395 = 0u; + } + uint seq_ix = row - _395; + uint tile_el_ix = ((tiles_alloc.offset >> uint(2)) + 1u) + ((seq_ix * 2u) * width); + Alloc param_7 = tiles_alloc; + uint param_8 = tile_el_ix; + uint sum = read_mem(param_7, param_8); + for (uint x = 1u; x < width; x++) + { + tile_el_ix += 2u; + Alloc param_9 = tiles_alloc; + uint param_10 = tile_el_ix; + sum += read_mem(param_9, param_10); + Alloc param_11 = tiles_alloc; + uint param_12 = tile_el_ix; + uint param_13 = sum; + write_mem(param_11, param_12, param_13); + } + } + } +} + +`, + } + shader_binning_comp = driver.ShaderSources{ + Name: "binning.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct MallocResult +{ + Alloc alloc; + bool failed; +}; + +struct AnnoEndClipRef +{ + uint offset; +}; + +struct AnnoEndClip +{ + vec4 bbox; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct AnnotatedTag +{ + uint tag; + uint flags; +}; + +struct BinInstanceRef +{ + uint offset; +}; + +struct BinInstance +{ + uint element_ix; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _88; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _254; + +shared uint bitmaps[4][128]; +shared bool sh_alloc_failed; +shared uint count[4][128]; +shared Alloc sh_chunk_alloc[128]; + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _88.memory[offset]; + return v; +} + +AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +AnnoEndClip AnnoEndClip_read(Alloc a, AnnoEndClipRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + AnnoEndClip s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + return s; +} + +AnnoEndClip Annotated_EndClip_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoEndClipRef param_1 = AnnoEndClipRef(ref.offset + 4u); + return AnnoEndClip_read(param, param_1); +} + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +MallocResult malloc(uint size) +{ + MallocResult r; + r.failed = false; + uint _94 = atomicAdd(_88.mem_offset, size); + uint offset = _94; + uint param = offset; + uint param_1 = size; + r.alloc = new_alloc(param, param_1); + if ((offset + size) > uint(int(uint(_88.memory.length())) * 4)) + { + r.failed = true; + uint _115 = atomicMax(_88.mem_error, 1u); + return r; + } + return r; +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _88.memory[offset] = val; +} + +void BinInstance_write(Alloc a, BinInstanceRef ref, BinInstance s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.element_ix; + write_mem(param, param_1, param_2); +} + +void main() +{ + if (_88.mem_error != 0u) + { + return; + } + uint my_n_elements = _254.conf.n_elements; + uint my_partition = gl_WorkGroupID.x; + for (uint i = 0u; i < 4u; i++) + { + bitmaps[i][gl_LocalInvocationID.x] = 0u; + } + if (gl_LocalInvocationID.x == 0u) + { + sh_alloc_failed = false; + } + barrier(); + uint element_ix = (my_partition * 128u) + gl_LocalInvocationID.x; + AnnotatedRef ref = AnnotatedRef(_254.conf.anno_alloc.offset + (element_ix * 32u)); + uint tag = 0u; + if (element_ix < my_n_elements) + { + Alloc param; + param.offset = _254.conf.anno_alloc.offset; + AnnotatedRef param_1 = ref; + tag = Annotated_tag(param, param_1).tag; + } + int x0 = 0; + int y0 = 0; + int x1 = 0; + int y1 = 0; + switch (tag) + { + case 1u: + case 2u: + case 3u: + case 4u: + { + Alloc param_2; + param_2.offset = _254.conf.anno_alloc.offset; + AnnotatedRef param_3 = ref; + AnnoEndClip clip = Annotated_EndClip_read(param_2, param_3); + x0 = int(floor(clip.bbox.x * 0.001953125)); + y0 = int(floor(clip.bbox.y * 0.00390625)); + x1 = int(ceil(clip.bbox.z * 0.001953125)); + y1 = int(ceil(clip.bbox.w * 0.00390625)); + break; + } + } + uint width_in_bins = ((_254.conf.width_in_tiles + 16u) - 1u) / 16u; + uint height_in_bins = ((_254.conf.height_in_tiles + 8u) - 1u) / 8u; + x0 = clamp(x0, 0, int(width_in_bins)); + x1 = clamp(x1, x0, int(width_in_bins)); + y0 = clamp(y0, 0, int(height_in_bins)); + y1 = clamp(y1, y0, int(height_in_bins)); + if (x0 == x1) + { + y1 = y0; + } + int x = x0; + int y = y0; + uint my_slice = gl_LocalInvocationID.x / 32u; + uint my_mask = uint(1 << int(gl_LocalInvocationID.x & 31u)); + while (y < y1) + { + uint _438 = atomicOr(bitmaps[my_slice][(uint(y) * width_in_bins) + uint(x)], my_mask); + x++; + if (x == x1) + { + x = x0; + y++; + } + } + barrier(); + uint element_count = 0u; + for (uint i_1 = 0u; i_1 < 4u; i_1++) + { + element_count += uint(bitCount(bitmaps[i_1][gl_LocalInvocationID.x])); + count[i_1][gl_LocalInvocationID.x] = element_count; + } + uint param_4 = 0u; + uint param_5 = 0u; + Alloc chunk_alloc = new_alloc(param_4, param_5); + if (element_count != 0u) + { + uint param_6 = element_count * 4u; + MallocResult _487 = malloc(param_6); + MallocResult chunk = _487; + chunk_alloc = chunk.alloc; + sh_chunk_alloc[gl_LocalInvocationID.x] = chunk_alloc; + if (chunk.failed) + { + sh_alloc_failed = true; + } + } + uint out_ix = (_254.conf.bin_alloc.offset >> uint(2)) + (((my_partition * 128u) + gl_LocalInvocationID.x) * 2u); + Alloc param_7; + param_7.offset = _254.conf.bin_alloc.offset; + uint param_8 = out_ix; + uint param_9 = element_count; + write_mem(param_7, param_8, param_9); + Alloc param_10; + param_10.offset = _254.conf.bin_alloc.offset; + uint param_11 = out_ix + 1u; + uint param_12 = chunk_alloc.offset; + write_mem(param_10, param_11, param_12); + barrier(); + if (sh_alloc_failed) + { + return; + } + x = x0; + y = y0; + while (y < y1) + { + uint bin_ix = (uint(y) * width_in_bins) + uint(x); + uint out_mask = bitmaps[my_slice][bin_ix]; + if ((out_mask & my_mask) != 0u) + { + uint idx = uint(bitCount(out_mask & (my_mask - 1u))); + if (my_slice > 0u) + { + idx += count[my_slice - 1u][bin_ix]; + } + Alloc out_alloc = sh_chunk_alloc[bin_ix]; + uint out_offset = out_alloc.offset + (idx * 4u); + Alloc param_13 = out_alloc; + BinInstanceRef param_14 = BinInstanceRef(out_offset); + BinInstance param_15 = BinInstance(element_ix); + BinInstance_write(param_13, param_14, param_15); + } + x++; + if (x == x1) + { + x = x0; + y++; + } + } +} + +`, + } + shader_blit_frag = [...]driver.ShaderSources{ + { + Name: "blit.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Color", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_color.color", Type: 0x0, Size: 4, Offset: 0}}, + Size: 16, + }, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +struct Color +{ + vec4 color; +}; + +uniform Color _color; + +varying vec2 vUV; + +void main() +{ + gl_FragData[0] = _color.color; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(std140) uniform Color +{ + vec4 color; +} _color; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Color +{ + vec4 color; +}; + +uniform Color _color; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Color +{ + vec4 color; +} _color; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; +} + +`, + HLSL: "DXBC,\xc1\x9c\x85P\xbc\xab\x8a.\x9e\b\xdd\xf7\xd2\x18\xa2\x01\x00\x00\x00t\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\x84\x00\x00\x00\xcc\x00\x00\x00H\x01\x00\x00\f\x02\x00\x00@\x02\x00\x00Aon9D\x00\x00\x00D\x00\x00\x00\x00\x02\xff\xff\x14\x00\x00\x000\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x00\x000\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\xa0\xff\xff\x00\x00SHDR@\x00\x00\x00@\x00\x00\x00\x10\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x006\x00\x00\x06\xf2 \x10\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xbc\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x94\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Color\x00\xab\xab<\x00\x00\x00\x01\x00\x00\x00\\\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00t\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\x84\x00\x00\x00\x00\x00\x00\x00_color_color\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + { + Name: "blit.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Gradient", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_gradient.color1", Type: 0x0, Size: 4, Offset: 0}, {Name: "_gradient.color2", Type: 0x0, Size: 4, Offset: 16}}, + Size: 32, + }, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +struct Gradient +{ + vec4 color1; + vec4 color2; +}; + +uniform Gradient _gradient; + +varying vec2 vUV; + +void main() +{ + gl_FragData[0] = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(std140) uniform Gradient +{ + vec4 color1; + vec4 color2; +} _gradient; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Gradient +{ + vec4 color1; + vec4 color2; +}; + +uniform Gradient _gradient; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Gradient +{ + vec4 color1; + vec4 color2; +} _gradient; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); +} + +`, + HLSL: "DXBCdZ\xb9AA\xb2\xa5-Ī£c\xb9\xdc\xfd]\xae\x01\x00\x00\x00P\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xcc\x00\x00\x00t\x01\x00\x00\xf0\x01\x00\x00\xe8\x02\x00\x00\x1c\x03\x00\x00Aon9\x8c\x00\x00\x00\x8c\x00\x00\x00\x00\x02\xff\xff\\\x00\x00\x000\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x00\x000\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x01\x00\x00\x02\x00\x00\x18\x80\x00\x00\x00\xb0\x01\x00\x00\x02\x01\x00\x0f\x80\x00\x00\xe4\xa0\x02\x00\x00\x03\x01\x00\x0f\x80\x01\x00\xe4\x81\x01\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\x0f\x80\x00\x00\xff\x80\x01\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xa0\x00\x00\x00@\x00\x00\x00(\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00b\x10\x00\x03\x12\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006 \x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x8e \x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x002\x00\x00\n\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xf0\x00\x00\x00\x01\x00\x00\x00H\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xc5\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Gradient\x00\xab\xab\xab<\x00\x00\x00\x02\x00\x00\x00`\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x00\x00\xb4\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x00\x00_gradient_color1\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_gradient_color2\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x01\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + { + Name: "blit.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +varying vec2 vUV; + +void main() +{ + gl_FragData[0] = texture2D(tex, vUV); +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = texture(tex, vUV); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = texture(tex, vUV); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = texture(tex, vUV); +} + +`, + HLSL: "DXBC\xb7?\x1d\xb1\x80Ķ€\xa3W\t\xfbZ\x9fV\xd6\xda\x01\x00\x00\x00\x94\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\xa4\x00\x00\x00\x10\x01\x00\x00\x8c\x01\x00\x00,\x02\x00\x00`\x02\x00\x00Aon9d\x00\x00\x00d\x00\x00\x00\x00\x02\xff\xff<\x00\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDRd\x00\x00\x00@\x00\x00\x00\x19\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00E\x00\x00\t\xf2 \x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00m\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00i\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + } + shader_blit_vert = driver.ShaderSources{ + Name: "blit.vert", + Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.uvTransformR1", Type: 0x0, Size: 4, Offset: 16}, {Name: "_block.uvTransformR2", Type: 0x0, Size: 4, Offset: 32}, {Name: "_block.z", Type: 0x0, Size: 1, Offset: 48}}, + Size: 52, + }, + GLSL100ES: `#version 100 + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 transform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +}; + +uniform Block _block; + +attribute vec2 pos; +varying vec2 vUV; +attribute vec2 uv; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec2 p = (pos * _block.transform.xy) + _block.transform.zw; + vec4 param = vec4(p, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; +} + +`, + GLSL300ES: `#version 300 es + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(std140) uniform Block +{ + vec4 transform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +} _block; + +layout(location = 0) in vec2 pos; +out vec2 vUV; +layout(location = 1) in vec2 uv; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec2 p = (pos * _block.transform.xy) + _block.transform.zw; + vec4 param = vec4(p, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 transform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +}; + +uniform Block _block; + +in vec2 pos; +out vec2 vUV; +in vec2 uv; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec2 p = (pos * _block.transform.xy) + _block.transform.zw; + vec4 param = vec4(p, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(binding = 0, std140) uniform Block +{ + vec4 transform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +} _block; + +in vec2 pos; +out vec2 vUV; +in vec2 uv; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec2 p = (pos * _block.transform.xy) + _block.transform.zw; + vec4 param = vec4(p, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; +} + +`, + HLSL: "DXBC\x80\xa7\xa0\x9e\xbb\xa1\xa3\x1b\x85\xac\xb6\xe9\xfb\xe6W\x03\x01\x00\x00\x00\xc8\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00$\x01\x00\x00T\x02\x00\x00\xd0\x02\x00\x00$\x04\x00\x00p\x04\x00\x00Aon9\xe4\x00\x00\x00\xe4\x00\x00\x00\x00\x02\xfe\xff\xb0\x00\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x04\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x05\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\a\x80\x01\x00Đ\x05\x00Š \x05\x00Å \b\x00\x00\x03\x00\x00\x01\xe0\x02\x00\xe4\xa0\x00\x00\xe4\x80\b\x00\x00\x03\x00\x00\x02\xe0\x03\x00\xe4\xa0\x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x90\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\a\x80\x05\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\f\xc0\x04\x00\x00\xa0\x00\x00d\x80\x00\x00$\x80\xff\xff\x00\x00SHDR(\x01\x00\x00@\x00\x01\x00J\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x04\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x006\x00\x00\x052\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x10\x00\x00\b\x12 \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x10\x00\x00\b\" \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x002\x00\x00\v2 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\nB \x10\x00\x01\x00\x00\x00\n\x80 \x00\x00\x00\x00\x00\x03\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\x01@\x00\x00\x00\x00\x00?6\x00\x00\x05\x82 \x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\b\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFL\x01\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00$\x01\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x04\x00\x00\x00\\\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\xf5\x00\x00\x00 \x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\n\x01\x00\x000\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00\x14\x01\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_uvTransformR1\x00_block_uvTransformR2\x00_block_z\x00\xab\x00\x00\x03\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_coarse_comp = driver.ShaderSources{ + Name: "coarse.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct MallocResult +{ + Alloc alloc; + bool failed; +}; + +struct AnnoImageRef +{ + uint offset; +}; + +struct AnnoImage +{ + vec4 bbox; + float linewidth; + uint index; + ivec2 offset; +}; + +struct AnnoColorRef +{ + uint offset; +}; + +struct AnnoColor +{ + vec4 bbox; + float linewidth; + uint rgba_color; +}; + +struct AnnoBeginClipRef +{ + uint offset; +}; + +struct AnnoBeginClip +{ + vec4 bbox; + float linewidth; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct AnnotatedTag +{ + uint tag; + uint flags; +}; + +struct BinInstanceRef +{ + uint offset; +}; + +struct BinInstance +{ + uint element_ix; +}; + +struct PathRef +{ + uint offset; +}; + +struct TileRef +{ + uint offset; +}; + +struct Path +{ + uvec4 bbox; + TileRef tiles; +}; + +struct TileSegRef +{ + uint offset; +}; + +struct Tile +{ + TileSegRef tile; + int backdrop; +}; + +struct CmdStrokeRef +{ + uint offset; +}; + +struct CmdStroke +{ + uint tile_ref; + float half_width; +}; + +struct CmdFillRef +{ + uint offset; +}; + +struct CmdFill +{ + uint tile_ref; + int backdrop; +}; + +struct CmdColorRef +{ + uint offset; +}; + +struct CmdColor +{ + uint rgba_color; +}; + +struct CmdImageRef +{ + uint offset; +}; + +struct CmdImage +{ + uint index; + ivec2 offset; +}; + +struct CmdJumpRef +{ + uint offset; +}; + +struct CmdJump +{ + uint new_ref; +}; + +struct CmdRef +{ + uint offset; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _276; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _1066; + +shared uint sh_bitmaps[4][128]; +shared Alloc sh_part_elements[128]; +shared uint sh_part_count[128]; +shared uint sh_elements[128]; +shared uint sh_tile_stride[128]; +shared uint sh_tile_width[128]; +shared uint sh_tile_x0[128]; +shared uint sh_tile_y0[128]; +shared uint sh_tile_base[128]; +shared uint sh_tile_count[128]; + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +Alloc slice_mem(Alloc a, uint offset, uint size) +{ + uint param = a.offset + offset; + uint param_1 = size; + return new_alloc(param, param_1); +} + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _276.memory[offset]; + return v; +} + +BinInstanceRef BinInstance_index(BinInstanceRef ref, uint index) +{ + return BinInstanceRef(ref.offset + (index * 4u)); +} + +BinInstance BinInstance_read(Alloc a, BinInstanceRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + BinInstance s; + s.element_ix = raw0; + return s; +} + +AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +Path Path_read(Alloc a, PathRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Path s; + s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16)); + s.tiles = TileRef(raw2); + return s; +} + +void write_tile_alloc(uint el_ix, Alloc a) +{ +} + +Alloc read_tile_alloc(uint el_ix) +{ + uint param = 0u; + uint param_1 = uint(int(uint(_276.memory.length())) * 4); + return new_alloc(param, param_1); +} + +Tile Tile_read(Alloc a, TileRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Tile s; + s.tile = TileSegRef(raw0); + s.backdrop = int(raw1); + return s; +} + +AnnoColor AnnoColor_read(Alloc a, AnnoColorRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + AnnoColor s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.linewidth = uintBitsToFloat(raw4); + s.rgba_color = raw5; + return s; +} + +AnnoColor Annotated_Color_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoColorRef param_1 = AnnoColorRef(ref.offset + 4u); + return AnnoColor_read(param, param_1); +} + +MallocResult malloc(uint size) +{ + MallocResult r; + r.failed = false; + uint _282 = atomicAdd(_276.mem_offset, size); + uint offset = _282; + uint param = offset; + uint param_1 = size; + r.alloc = new_alloc(param, param_1); + if ((offset + size) > uint(int(uint(_276.memory.length())) * 4)) + { + r.failed = true; + uint _303 = atomicMax(_276.mem_error, 1u); + return r; + } + return r; +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _276.memory[offset] = val; +} + +void CmdJump_write(Alloc a, CmdJumpRef ref, CmdJump s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.new_ref; + write_mem(param, param_1, param_2); +} + +void Cmd_Jump_write(Alloc a, CmdRef ref, CmdJump s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 9u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdJumpRef param_4 = CmdJumpRef(ref.offset + 4u); + CmdJump param_5 = s; + CmdJump_write(param_3, param_4, param_5); +} + +bool alloc_cmd(inout Alloc cmd_alloc, inout CmdRef cmd_ref, inout uint cmd_limit) +{ + if (cmd_ref.offset < cmd_limit) + { + return true; + } + uint param = 1024u; + MallocResult _968 = malloc(param); + MallocResult new_cmd = _968; + if (new_cmd.failed) + { + return false; + } + CmdJump jump = CmdJump(new_cmd.alloc.offset); + Alloc param_1 = cmd_alloc; + CmdRef param_2 = cmd_ref; + CmdJump param_3 = jump; + Cmd_Jump_write(param_1, param_2, param_3); + cmd_alloc = new_cmd.alloc; + cmd_ref = CmdRef(cmd_alloc.offset); + cmd_limit = (cmd_alloc.offset + 1024u) - 36u; + return true; +} + +uint fill_mode_from_flags(uint flags) +{ + return flags & 1u; +} + +void CmdFill_write(Alloc a, CmdFillRef ref, CmdFill s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.tile_ref; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = uint(s.backdrop); + write_mem(param_3, param_4, param_5); +} + +void Cmd_Fill_write(Alloc a, CmdRef ref, CmdFill s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 1u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdFillRef param_4 = CmdFillRef(ref.offset + 4u); + CmdFill param_5 = s; + CmdFill_write(param_3, param_4, param_5); +} + +void Cmd_Solid_write(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 3u; + write_mem(param, param_1, param_2); +} + +void CmdStroke_write(Alloc a, CmdStrokeRef ref, CmdStroke s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.tile_ref; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.half_width); + write_mem(param_3, param_4, param_5); +} + +void Cmd_Stroke_write(Alloc a, CmdRef ref, CmdStroke s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 2u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdStrokeRef param_4 = CmdStrokeRef(ref.offset + 4u); + CmdStroke param_5 = s; + CmdStroke_write(param_3, param_4, param_5); +} + +void write_fill(Alloc alloc, inout CmdRef cmd_ref, uint flags, Tile tile, float linewidth) +{ + uint param = flags; + if (fill_mode_from_flags(param) == 0u) + { + if (tile.tile.offset != 0u) + { + CmdFill cmd_fill = CmdFill(tile.tile.offset, tile.backdrop); + Alloc param_1 = alloc; + CmdRef param_2 = cmd_ref; + CmdFill param_3 = cmd_fill; + Cmd_Fill_write(param_1, param_2, param_3); + cmd_ref.offset += 12u; + } + else + { + Alloc param_4 = alloc; + CmdRef param_5 = cmd_ref; + Cmd_Solid_write(param_4, param_5); + cmd_ref.offset += 4u; + } + } + else + { + CmdStroke cmd_stroke = CmdStroke(tile.tile.offset, 0.5 * linewidth); + Alloc param_6 = alloc; + CmdRef param_7 = cmd_ref; + CmdStroke param_8 = cmd_stroke; + Cmd_Stroke_write(param_6, param_7, param_8); + cmd_ref.offset += 12u; + } +} + +void CmdColor_write(Alloc a, CmdColorRef ref, CmdColor s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.rgba_color; + write_mem(param, param_1, param_2); +} + +void Cmd_Color_write(Alloc a, CmdRef ref, CmdColor s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 5u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdColorRef param_4 = CmdColorRef(ref.offset + 4u); + CmdColor param_5 = s; + CmdColor_write(param_3, param_4, param_5); +} + +AnnoImage AnnoImage_read(Alloc a, AnnoImageRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 6u; + uint raw6 = read_mem(param_12, param_13); + AnnoImage s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.linewidth = uintBitsToFloat(raw4); + s.index = raw5; + s.offset = ivec2(int(raw6 << uint(16)) >> 16, int(raw6) >> 16); + return s; +} + +AnnoImage Annotated_Image_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoImageRef param_1 = AnnoImageRef(ref.offset + 4u); + return AnnoImage_read(param, param_1); +} + +void CmdImage_write(Alloc a, CmdImageRef ref, CmdImage s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.index; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = (uint(s.offset.x) & 65535u) | (uint(s.offset.y) << uint(16)); + write_mem(param_3, param_4, param_5); +} + +void Cmd_Image_write(Alloc a, CmdRef ref, CmdImage s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 6u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdImageRef param_4 = CmdImageRef(ref.offset + 4u); + CmdImage param_5 = s; + CmdImage_write(param_3, param_4, param_5); +} + +AnnoBeginClip AnnoBeginClip_read(Alloc a, AnnoBeginClipRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + AnnoBeginClip s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.linewidth = uintBitsToFloat(raw4); + return s; +} + +AnnoBeginClip Annotated_BeginClip_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoBeginClipRef param_1 = AnnoBeginClipRef(ref.offset + 4u); + return AnnoBeginClip_read(param, param_1); +} + +void Cmd_BeginClip_write(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 7u; + write_mem(param, param_1, param_2); +} + +void Cmd_EndClip_write(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 8u; + write_mem(param, param_1, param_2); +} + +void Cmd_End_write(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 0u; + write_mem(param, param_1, param_2); +} + +void alloc_write(Alloc a, uint offset, Alloc alloc) +{ + Alloc param = a; + uint param_1 = offset >> uint(2); + uint param_2 = alloc.offset; + write_mem(param, param_1, param_2); +} + +void main() +{ + if (_276.mem_error != 0u) + { + return; + } + uint width_in_bins = ((_1066.conf.width_in_tiles + 16u) - 1u) / 16u; + uint bin_ix = (width_in_bins * gl_WorkGroupID.y) + gl_WorkGroupID.x; + uint partition_ix = 0u; + uint n_partitions = ((_1066.conf.n_elements + 128u) - 1u) / 128u; + uint th_ix = gl_LocalInvocationID.x; + uint bin_tile_x = 16u * gl_WorkGroupID.x; + uint bin_tile_y = 8u * gl_WorkGroupID.y; + uint tile_x = gl_LocalInvocationID.x % 16u; + uint tile_y = gl_LocalInvocationID.x / 16u; + uint this_tile_ix = (((bin_tile_y + tile_y) * _1066.conf.width_in_tiles) + bin_tile_x) + tile_x; + Alloc param; + param.offset = _1066.conf.ptcl_alloc.offset; + uint param_1 = this_tile_ix * 1024u; + uint param_2 = 1024u; + Alloc cmd_alloc = slice_mem(param, param_1, param_2); + CmdRef cmd_ref = CmdRef(cmd_alloc.offset); + uint cmd_limit = (cmd_ref.offset + 1024u) - 36u; + uint clip_depth = 0u; + uint clip_zero_depth = 0u; + uint clip_one_mask = 0u; + uint rd_ix = 0u; + uint wr_ix = 0u; + uint part_start_ix = 0u; + uint ready_ix = 0u; + Alloc param_3 = cmd_alloc; + uint param_4 = 0u; + uint param_5 = 8u; + Alloc scratch_alloc = slice_mem(param_3, param_4, param_5); + cmd_ref.offset += 8u; + uint num_begin_slots = 0u; + uint begin_slot = 0u; + Alloc param_6; + Alloc param_8; + uint _1354; + uint element_ix; + AnnotatedRef ref; + Alloc param_16; + Alloc param_18; + uint tile_count; + Alloc param_24; + uint _1667; + bool include_tile; + Alloc param_29; + Tile tile_1; + Alloc param_34; + Alloc param_50; + Alloc param_66; + while (true) + { + for (uint i = 0u; i < 4u; i++) + { + sh_bitmaps[i][th_ix] = 0u; + } + bool _1406; + for (;;) + { + if ((ready_ix == wr_ix) && (partition_ix < n_partitions)) + { + part_start_ix = ready_ix; + uint count = 0u; + bool _1204 = th_ix < 128u; + bool _1212; + if (_1204) + { + _1212 = (partition_ix + th_ix) < n_partitions; + } + else + { + _1212 = _1204; + } + if (_1212) + { + uint in_ix = (_1066.conf.bin_alloc.offset >> uint(2)) + ((((partition_ix + th_ix) * 128u) + bin_ix) * 2u); + param_6.offset = _1066.conf.bin_alloc.offset; + uint param_7 = in_ix; + count = read_mem(param_6, param_7); + param_8.offset = _1066.conf.bin_alloc.offset; + uint param_9 = in_ix + 1u; + uint offset = read_mem(param_8, param_9); + uint param_10 = offset; + uint param_11 = count * 4u; + sh_part_elements[th_ix] = new_alloc(param_10, param_11); + } + for (uint i_1 = 0u; i_1 < 7u; i_1++) + { + if (th_ix < 128u) + { + sh_part_count[th_ix] = count; + } + barrier(); + if (th_ix < 128u) + { + if (th_ix >= uint(1 << int(i_1))) + { + count += sh_part_count[th_ix - uint(1 << int(i_1))]; + } + } + barrier(); + } + if (th_ix < 128u) + { + sh_part_count[th_ix] = part_start_ix + count; + } + barrier(); + ready_ix = sh_part_count[127]; + partition_ix += 128u; + } + uint ix = rd_ix + th_ix; + if ((ix >= wr_ix) && (ix < ready_ix)) + { + uint part_ix = 0u; + for (uint i_2 = 0u; i_2 < 7u; i_2++) + { + uint probe = part_ix + uint(64 >> int(i_2)); + if (ix >= sh_part_count[probe - 1u]) + { + part_ix = probe; + } + } + if (part_ix > 0u) + { + _1354 = sh_part_count[part_ix - 1u]; + } + else + { + _1354 = part_start_ix; + } + ix -= _1354; + Alloc bin_alloc = sh_part_elements[part_ix]; + BinInstanceRef inst_ref = BinInstanceRef(bin_alloc.offset); + BinInstanceRef param_12 = inst_ref; + uint param_13 = ix; + Alloc param_14 = bin_alloc; + BinInstanceRef param_15 = BinInstance_index(param_12, param_13); + BinInstance inst = BinInstance_read(param_14, param_15); + sh_elements[th_ix] = inst.element_ix; + } + barrier(); + wr_ix = min((rd_ix + 128u), ready_ix); + bool _1396 = (wr_ix - rd_ix) < 128u; + if (_1396) + { + _1406 = (wr_ix < ready_ix) || (partition_ix < n_partitions); + } + else + { + _1406 = _1396; + } + if (_1406) + { + continue; + } + else + { + break; + } + } + uint tag = 0u; + if ((th_ix + rd_ix) < wr_ix) + { + element_ix = sh_elements[th_ix]; + ref = AnnotatedRef(_1066.conf.anno_alloc.offset + (element_ix * 32u)); + param_16.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_17 = ref; + tag = Annotated_tag(param_16, param_17).tag; + } + switch (tag) + { + case 1u: + case 2u: + case 3u: + case 4u: + { + uint path_ix = element_ix; + param_18.offset = _1066.conf.tile_alloc.offset; + PathRef param_19 = PathRef(_1066.conf.tile_alloc.offset + (path_ix * 12u)); + Path path = Path_read(param_18, param_19); + uint stride = path.bbox.z - path.bbox.x; + sh_tile_stride[th_ix] = stride; + int dx = int(path.bbox.x) - int(bin_tile_x); + int dy = int(path.bbox.y) - int(bin_tile_y); + int x0 = clamp(dx, 0, 16); + int y0 = clamp(dy, 0, 8); + int x1 = clamp(int(path.bbox.z) - int(bin_tile_x), 0, 16); + int y1 = clamp(int(path.bbox.w) - int(bin_tile_y), 0, 8); + sh_tile_width[th_ix] = uint(x1 - x0); + sh_tile_x0[th_ix] = uint(x0); + sh_tile_y0[th_ix] = uint(y0); + tile_count = uint(x1 - x0) * uint(y1 - y0); + uint base = path.tiles.offset - (((uint(dy) * stride) + uint(dx)) * 8u); + sh_tile_base[th_ix] = base; + uint param_20 = path.tiles.offset; + uint param_21 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u; + Alloc path_alloc = new_alloc(param_20, param_21); + uint param_22 = th_ix; + Alloc param_23 = path_alloc; + write_tile_alloc(param_22, param_23); + break; + } + default: + { + tile_count = 0u; + break; + } + } + sh_tile_count[th_ix] = tile_count; + for (uint i_3 = 0u; i_3 < 7u; i_3++) + { + barrier(); + if (th_ix >= uint(1 << int(i_3))) + { + tile_count += sh_tile_count[th_ix - uint(1 << int(i_3))]; + } + barrier(); + sh_tile_count[th_ix] = tile_count; + } + barrier(); + uint total_tile_count = sh_tile_count[127]; + for (uint ix_1 = th_ix; ix_1 < total_tile_count; ix_1 += 128u) + { + uint el_ix = 0u; + for (uint i_4 = 0u; i_4 < 7u; i_4++) + { + uint probe_1 = el_ix + uint(64 >> int(i_4)); + if (ix_1 >= sh_tile_count[probe_1 - 1u]) + { + el_ix = probe_1; + } + } + AnnotatedRef ref_1 = AnnotatedRef(_1066.conf.anno_alloc.offset + (sh_elements[el_ix] * 32u)); + param_24.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_25 = ref_1; + uint tag_1 = Annotated_tag(param_24, param_25).tag; + if (el_ix > 0u) + { + _1667 = sh_tile_count[el_ix - 1u]; + } + else + { + _1667 = 0u; + } + uint seq_ix = ix_1 - _1667; + uint width = sh_tile_width[el_ix]; + uint x = sh_tile_x0[el_ix] + (seq_ix % width); + uint y = sh_tile_y0[el_ix] + (seq_ix / width); + if ((tag_1 == 3u) || (tag_1 == 4u)) + { + include_tile = true; + } + else + { + uint param_26 = el_ix; + Alloc param_27 = read_tile_alloc(param_26); + TileRef param_28 = TileRef(sh_tile_base[el_ix] + (((sh_tile_stride[el_ix] * y) + x) * 8u)); + Tile tile = Tile_read(param_27, param_28); + bool _1728 = tile.tile.offset != 0u; + bool _1735; + if (!_1728) + { + _1735 = tile.backdrop != 0; + } + else + { + _1735 = _1728; + } + include_tile = _1735; + } + if (include_tile) + { + uint el_slice = el_ix / 32u; + uint el_mask = uint(1 << int(el_ix & 31u)); + uint _1755 = atomicOr(sh_bitmaps[el_slice][(y * 16u) + x], el_mask); + } + } + barrier(); + uint slice_ix = 0u; + uint bitmap = sh_bitmaps[0][th_ix]; + while (true) + { + if (bitmap == 0u) + { + slice_ix++; + if (slice_ix == 4u) + { + break; + } + bitmap = sh_bitmaps[slice_ix][th_ix]; + if (bitmap == 0u) + { + continue; + } + } + uint element_ref_ix = (slice_ix * 32u) + uint(findLSB(bitmap)); + uint element_ix_1 = sh_elements[element_ref_ix]; + bitmap &= (bitmap - 1u); + ref = AnnotatedRef(_1066.conf.anno_alloc.offset + (element_ix_1 * 32u)); + param_29.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_30 = ref; + AnnotatedTag tag_2 = Annotated_tag(param_29, param_30); + if (clip_zero_depth == 0u) + { + switch (tag_2.tag) + { + case 1u: + { + uint param_31 = element_ref_ix; + Alloc param_32 = read_tile_alloc(param_31); + TileRef param_33 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u)); + tile_1 = Tile_read(param_32, param_33); + param_34.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_35 = ref; + AnnoColor fill = Annotated_Color_read(param_34, param_35); + Alloc param_36 = cmd_alloc; + CmdRef param_37 = cmd_ref; + uint param_38 = cmd_limit; + bool _1865 = alloc_cmd(param_36, param_37, param_38); + cmd_alloc = param_36; + cmd_ref = param_37; + cmd_limit = param_38; + if (!_1865) + { + break; + } + Alloc param_39 = cmd_alloc; + CmdRef param_40 = cmd_ref; + uint param_41 = tag_2.flags; + Tile param_42 = tile_1; + float param_43 = fill.linewidth; + write_fill(param_39, param_40, param_41, param_42, param_43); + cmd_ref = param_40; + Alloc param_44 = cmd_alloc; + CmdRef param_45 = cmd_ref; + CmdColor param_46 = CmdColor(fill.rgba_color); + Cmd_Color_write(param_44, param_45, param_46); + cmd_ref.offset += 8u; + break; + } + case 2u: + { + uint param_47 = element_ref_ix; + Alloc param_48 = read_tile_alloc(param_47); + TileRef param_49 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u)); + tile_1 = Tile_read(param_48, param_49); + param_50.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_51 = ref; + AnnoImage fill_img = Annotated_Image_read(param_50, param_51); + Alloc param_52 = cmd_alloc; + CmdRef param_53 = cmd_ref; + uint param_54 = cmd_limit; + bool _1935 = alloc_cmd(param_52, param_53, param_54); + cmd_alloc = param_52; + cmd_ref = param_53; + cmd_limit = param_54; + if (!_1935) + { + break; + } + Alloc param_55 = cmd_alloc; + CmdRef param_56 = cmd_ref; + uint param_57 = tag_2.flags; + Tile param_58 = tile_1; + float param_59 = fill_img.linewidth; + write_fill(param_55, param_56, param_57, param_58, param_59); + cmd_ref = param_56; + Alloc param_60 = cmd_alloc; + CmdRef param_61 = cmd_ref; + CmdImage param_62 = CmdImage(fill_img.index, fill_img.offset); + Cmd_Image_write(param_60, param_61, param_62); + cmd_ref.offset += 12u; + break; + } + case 3u: + { + uint param_63 = element_ref_ix; + Alloc param_64 = read_tile_alloc(param_63); + TileRef param_65 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u)); + tile_1 = Tile_read(param_64, param_65); + bool _1994 = tile_1.tile.offset == 0u; + bool _2000; + if (_1994) + { + _2000 = tile_1.backdrop == 0; + } + else + { + _2000 = _1994; + } + if (_2000) + { + clip_zero_depth = clip_depth + 1u; + } + else + { + if ((tile_1.tile.offset == 0u) && (clip_depth < 32u)) + { + clip_one_mask |= uint(1 << int(clip_depth)); + } + else + { + param_66.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_67 = ref; + AnnoBeginClip begin_clip = Annotated_BeginClip_read(param_66, param_67); + Alloc param_68 = cmd_alloc; + CmdRef param_69 = cmd_ref; + uint param_70 = cmd_limit; + bool _2035 = alloc_cmd(param_68, param_69, param_70); + cmd_alloc = param_68; + cmd_ref = param_69; + cmd_limit = param_70; + if (!_2035) + { + break; + } + Alloc param_71 = cmd_alloc; + CmdRef param_72 = cmd_ref; + uint param_73 = tag_2.flags; + Tile param_74 = tile_1; + float param_75 = begin_clip.linewidth; + write_fill(param_71, param_72, param_73, param_74, param_75); + cmd_ref = param_72; + Alloc param_76 = cmd_alloc; + CmdRef param_77 = cmd_ref; + Cmd_BeginClip_write(param_76, param_77); + cmd_ref.offset += 4u; + if (clip_depth < 32u) + { + clip_one_mask &= uint(~(1 << int(clip_depth))); + } + begin_slot++; + num_begin_slots = max(num_begin_slots, begin_slot); + } + } + clip_depth++; + break; + } + case 4u: + { + clip_depth--; + bool _2087 = clip_depth >= 32u; + bool _2097; + if (!_2087) + { + _2097 = (clip_one_mask & uint(1 << int(clip_depth))) == 0u; + } + else + { + _2097 = _2087; + } + if (_2097) + { + Alloc param_78 = cmd_alloc; + CmdRef param_79 = cmd_ref; + uint param_80 = cmd_limit; + bool _2106 = alloc_cmd(param_78, param_79, param_80); + cmd_alloc = param_78; + cmd_ref = param_79; + cmd_limit = param_80; + if (!_2106) + { + break; + } + Alloc param_81 = cmd_alloc; + CmdRef param_82 = cmd_ref; + Cmd_Solid_write(param_81, param_82); + cmd_ref.offset += 4u; + begin_slot--; + Alloc param_83 = cmd_alloc; + CmdRef param_84 = cmd_ref; + Cmd_EndClip_write(param_83, param_84); + cmd_ref.offset += 4u; + } + break; + } + } + } + else + { + switch (tag_2.tag) + { + case 3u: + { + clip_depth++; + break; + } + case 4u: + { + if (clip_depth == clip_zero_depth) + { + clip_zero_depth = 0u; + } + clip_depth--; + break; + } + } + } + } + barrier(); + rd_ix += 128u; + if ((rd_ix >= ready_ix) && (partition_ix >= n_partitions)) + { + break; + } + } + bool _2171 = (bin_tile_x + tile_x) < _1066.conf.width_in_tiles; + bool _2180; + if (_2171) + { + _2180 = (bin_tile_y + tile_y) < _1066.conf.height_in_tiles; + } + else + { + _2180 = _2171; + } + if (_2180) + { + Alloc param_85 = cmd_alloc; + CmdRef param_86 = cmd_ref; + Cmd_End_write(param_85, param_86); + if (num_begin_slots > 0u) + { + uint scratch_size = (((num_begin_slots * 32u) * 32u) * 2u) * 4u; + uint param_87 = scratch_size; + MallocResult _2201 = malloc(param_87); + MallocResult scratch = _2201; + Alloc param_88 = scratch_alloc; + uint param_89 = scratch_alloc.offset; + Alloc param_90 = scratch.alloc; + alloc_write(param_88, param_89, param_90); + } + } +} + +`, + } + shader_copy_frag = driver.ShaderSources{ + Name: "copy.frag", + Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}}, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +layout(location = 0) out highp vec4 fragColor; + +highp vec3 sRGBtoRGB(highp vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375)); + highp vec3 below = rgb / vec3(12.9200000762939453125); + highp vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625)); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + highp vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0); + highp vec3 param = texel.xyz; + highp vec3 rgb = sRGBtoRGB(param); + fragColor = vec4(rgb, texel.w); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +out vec4 fragColor; + +vec3 sRGBtoRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375)); + vec3 below = rgb / vec3(12.9200000762939453125); + vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625)); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0); + vec3 param = texel.xyz; + vec3 rgb = sRGBtoRGB(param); + fragColor = vec4(rgb, texel.w); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +out vec4 fragColor; + +vec3 sRGBtoRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375)); + vec3 below = rgb / vec3(12.9200000762939453125); + vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625)); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0); + vec3 param = texel.xyz; + vec3 rgb = sRGBtoRGB(param); + fragColor = vec4(rgb, texel.w); +} + +`, + HLSL: "DXBC\xe6\x89_t\x8b\xfc\xea8\xd9'\xad5.ƈk\x01\x00\x00\x00H\x03\x00\x00\x05\x00\x00\x004\x00\x00\x00\xa4\x00\x00\x00\xd8\x00\x00\x00\f\x01\x00\x00\xcc\x02\x00\x00RDEFh\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00@\x00\x00\x00<\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x03\x00\x00SV_Position\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xabSHDR\xb8\x01\x00\x00@\x00\x00\x00n\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00d \x00\x042\x10\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x00\x1b\x00\x00\x052\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00\x00\a\xf2\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\xaeGa=\xaeGa=\xaeGa=\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00o\xa7r?o\xa7r?o\xa7r?\x00\x00\x00\x00/\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00\x9a\x99\x19@\x9a\x99\x19@\x9a\x99\x19@\x00\x00\x00\x00\x19\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x1d\x00\x00\nr\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\xe6\xae%=\xe6\xae%=\xe6\xae%=\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x00\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x91\x83\x9e=\x91\x83\x9e=\x91\x83\x9e=\x00\x00\x00\x006\x00\x00\x05\x82 \x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x007\x00\x00\tr \x10\x00\x00\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\r\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + } + shader_copy_vert = driver.ShaderSources{ + Name: "copy.vert", + GLSL100ES: `#version 100 + +void main() +{ + for (int spvDummy6 = 0; spvDummy6 < 1; spvDummy6++) + { + if (gl_VertexID == 0) + { + gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); + break; + } + else if (gl_VertexID == 1) + { + gl_Position = vec4(1.0, 1.0, 0.0, 1.0); + break; + } + else if (gl_VertexID == 2) + { + gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); + break; + } + else if (gl_VertexID == 3) + { + gl_Position = vec4(1.0, -1.0, 0.0, 1.0); + break; + } + } +} + +`, + GLSL300ES: `#version 300 es + +void main() +{ + switch (gl_VertexID) + { + case 0: + { + gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); + break; + } + case 1: + { + gl_Position = vec4(1.0, 1.0, 0.0, 1.0); + break; + } + case 2: + { + gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); + break; + } + case 3: + { + gl_Position = vec4(1.0, -1.0, 0.0, 1.0); + break; + } + } +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +void main() +{ + switch (gl_VertexID) + { + case 0: + { + gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); + break; + } + case 1: + { + gl_Position = vec4(1.0, 1.0, 0.0, 1.0); + break; + } + case 2: + { + gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); + break; + } + case 3: + { + gl_Position = vec4(1.0, -1.0, 0.0, 1.0); + break; + } + } +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +void main() +{ + switch (gl_VertexID) + { + case 0: + { + gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); + break; + } + case 1: + { + gl_Position = vec4(1.0, 1.0, 0.0, 1.0); + break; + } + case 2: + { + gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); + break; + } + case 3: + { + gl_Position = vec4(1.0, -1.0, 0.0, 1.0); + break; + } + } +} + +`, + HLSL: "DXBC\x99\xb4[\xef]IX\xa2Qh\x9f\xb6!\x1cR\xe7\x01\x00\x00\x00\xc0\x02\x00\x00\x05\x00\x00\x004\x00\x00\x00\x80\x00\x00\x00\xb4\x00\x00\x00\xe8\x00\x00\x00D\x02\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00SV_VertexID\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00SHDRT\x01\x00\x00@\x00\x01\x00U\x00\x00\x00`\x00\x00\x04\x12\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00L\x00\x00\x03\n\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x03\x01@\x00\x00\x00\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x01\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80?\x00\x00\x80?\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x02\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x03\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80?\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\n\x00\x00\x016\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x01\x17\x00\x00\x016\x00\x00\x05\xb2 \x10\x00\x00\x00\x00\x00F\b\x10\x00\x00\x00\x00\x006\x00\x00\x05B \x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x14\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + } + shader_cover_frag = [...]driver.ShaderSources{ + { + Name: "cover.frag", + Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Color", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_color.color", Type: 0x0, Size: 4, Offset: 0}}, + Size: 16, + }, + Textures: []driver.TextureBinding{{Name: "cover", Binding: 1}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +struct Color +{ + vec4 color; +}; + +uniform Color _color; + +uniform mediump sampler2D cover; + +varying highp vec2 vCoverUV; +varying vec2 vUV; + +void main() +{ + gl_FragData[0] = _color.color; + float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0); + gl_FragData[0] *= cover_1; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(std140) uniform Color +{ + vec4 color; +} _color; + +uniform mediump sampler2D cover; + +layout(location = 0) out vec4 fragColor; +in highp vec2 vCoverUV; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Color +{ + vec4 color; +}; + +uniform Color _color; + +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vCoverUV; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Color +{ + vec4 color; +} _color; + +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vCoverUV; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + HLSL: "DXBC\x88\x01{\x0f\x94\xca3\xeb\xabßø\xa1\xbfL1\xbf\x01\x00\x00\x00\xa4\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xcc\x00\x00\x00\x90\x01\x00\x00\f\x02\x00\x00$\x03\x00\x00p\x03\x00\x00Aon9\x8c\x00\x00\x00\x8c\x00\x00\x00\x00\x02\xff\xffX\x00\x00\x004\x00\x00\x00\x01\x00(\x00\x00\x004\x00\x00\x004\x00\x01\x00$\x00\x00\x004\x00\x01\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0#\x00\x00\x02\x00\x00\x11\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\x00\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xbc\x00\x00\x00@\x00\x00\x00/\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?8\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x10\x01\x00\x00\x01\x00\x00\x00\x98\x00\x00\x00\x03\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xe8\x00\x00\x00|\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x8b\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\x91\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00_cover_sampler\x00cover\x00Color\x00\xab\x91\x00\x00\x00\x01\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd8\x00\x00\x00\x00\x00\x00\x00_color_color\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + { + Name: "cover.frag", + Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Gradient", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_gradient.color1", Type: 0x0, Size: 4, Offset: 0}, {Name: "_gradient.color2", Type: 0x0, Size: 4, Offset: 16}}, + Size: 32, + }, + Textures: []driver.TextureBinding{{Name: "cover", Binding: 1}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +struct Gradient +{ + vec4 color1; + vec4 color2; +}; + +uniform Gradient _gradient; + +uniform mediump sampler2D cover; + +varying vec2 vUV; +varying highp vec2 vCoverUV; + +void main() +{ + gl_FragData[0] = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); + float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0); + gl_FragData[0] *= cover_1; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(std140) uniform Gradient +{ + vec4 color1; + vec4 color2; +} _gradient; + +uniform mediump sampler2D cover; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; +in highp vec2 vCoverUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Gradient +{ + vec4 color1; + vec4 color2; +}; + +uniform Gradient _gradient; + +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vUV; +in vec2 vCoverUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Gradient +{ + vec4 color1; + vec4 color2; +} _gradient; + +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vUV; +in vec2 vCoverUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + HLSL: "DXBCj\xa0\x9e\x8d\x1eƌO\rJ\xea\x8f\x17\x11o\x98\x01\x00\x00\x00\x80\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00\b\x01\x00\x008\x02\x00\x00\xb4\x02\x00\x00\x00\x04\x00\x00L\x04\x00\x00Aon9\xc8\x00\x00\x00\xc8\x00\x00\x00\x00\x02\xff\xff\x94\x00\x00\x004\x00\x00\x00\x01\x00(\x00\x00\x004\x00\x00\x004\x00\x01\x00$\x00\x00\x004\x00\x01\x01\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x01\x00\x00\x02\x00\x00\x12\x80\x00\x00\xff\xb0\x01\x00\x00\x02\x01\x00\x0f\x80\x00\x00\xe4\xa0\x02\x00\x00\x03\x01\x00\x0f\x80\x01\x00\xe4\x81\x01\x00\xe4\xa0\x04\x00\x00\x04\x01\x00\x0f\x80\x00\x00U\x80\x01\x00\xe4\x80\x00\x00\xe4\xa0#\x00\x00\x02\x00\x00\x11\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\x00\x80\x01\x00\xe4\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR(\x01\x00\x00@\x00\x00\x00J\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03B\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006 \x00\x05\x12\x00\x10\x00\x00\x00\x00\x00*\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x8e \x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x002\x00\x00\n\xf2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?8\x00\x00\a\xf2 \x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x01\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\a\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x01\x00\x00\x01\x00\x00\x00\x9c\x00\x00\x00\x03\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x19\x01\x00\x00|\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x8b\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\x91\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00_cover_sampler\x00cover\x00Gradient\x00\xab\xab\x91\x00\x00\x00\x02\x00\x00\x00\xb4\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe4\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x00\x00\b\x01\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x00\x00_gradient_color1\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_gradient_color2\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x04\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + { + Name: "cover.frag", + Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}, {Name: "cover", Binding: 1}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; +uniform mediump sampler2D cover; + +varying vec2 vUV; +varying highp vec2 vCoverUV; + +void main() +{ + gl_FragData[0] = texture2D(tex, vUV); + float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0); + gl_FragData[0] *= cover_1; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; +uniform mediump sampler2D cover; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; +in highp vec2 vCoverUV; + +void main() +{ + fragColor = texture(tex, vUV); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vUV; +in vec2 vCoverUV; + +void main() +{ + fragColor = texture(tex, vUV); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vUV; +in vec2 vCoverUV; + +void main() +{ + fragColor = texture(tex, vUV); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + HLSL: "DXBC\x99\x16l`\xf6:k\xa2Y$\xa1,\xfd\xcdJE\x01\x00\x00\x00\xd8\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xec\x00\x00\x00\xe8\x01\x00\x00d\x02\x00\x00X\x03\x00\x00\xa4\x03\x00\x00Aon9\xac\x00\x00\x00\xac\x00\x00\x00\x00\x02\xff\xff\x80\x00\x00\x00,\x00\x00\x00\x00\x00,\x00\x00\x00,\x00\x00\x00,\x00\x02\x00$\x00\x00\x00,\x00\x00\x00\x00\x00\x01\x01\x01\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0\x1f\x00\x00\x02\x00\x00\x00\x90\x01\b\x0f\xa0\x01\x00\x00\x02\x00\x00\x03\x80\x00\x00\x1b\xb0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\x80\x00\b\xe4\xa0B\x00\x00\x03\x01\x00\x0f\x80\x00\x00\xe4\xb0\x01\b\xe4\xa0#\x00\x00\x02\x01\x00\x11\x80\x01\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\x80\x01\x00\x00\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xf4\x00\x00\x00@\x00\x00\x00=\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03\xc2\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?E\x00\x00\t\xf2\x00\x10\x00\x01\x00\x00\x00\xe6\x1a\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x008\x00\x00\a\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x05\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xec\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xc2\x00\x00\x00\x9c\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\xa9\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\xb8\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\xbc\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00_cover_sampler\x00tex\x00cover\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\f\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + } + shader_cover_vert = driver.ShaderSources{ + Name: "cover.vert", + Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.uvCoverTransform", Type: 0x0, Size: 4, Offset: 16}, {Name: "_block.uvTransformR1", Type: 0x0, Size: 4, Offset: 32}, {Name: "_block.uvTransformR2", Type: 0x0, Size: 4, Offset: 48}, {Name: "_block.z", Type: 0x0, Size: 1, Offset: 64}}, + Size: 68, + }, + GLSL100ES: `#version 100 + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 transform; + vec4 uvCoverTransform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +}; + +uniform Block _block; + +attribute vec2 pos; +varying vec2 vUV; +attribute vec2 uv; +varying vec2 vCoverUV; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; + m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_4 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_3, param_4); + vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy; +} + +`, + GLSL300ES: `#version 300 es + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(std140) uniform Block +{ + vec4 transform; + vec4 uvCoverTransform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +} _block; + +layout(location = 0) in vec2 pos; +out vec2 vUV; +layout(location = 1) in vec2 uv; +out vec2 vCoverUV; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; + m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_4 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_3, param_4); + vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 transform; + vec4 uvCoverTransform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +}; + +uniform Block _block; + +in vec2 pos; +out vec2 vUV; +in vec2 uv; +out vec2 vCoverUV; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; + m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_4 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_3, param_4); + vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(binding = 0, std140) uniform Block +{ + vec4 transform; + vec4 uvCoverTransform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +} _block; + +in vec2 pos; +out vec2 vUV; +in vec2 uv; +out vec2 vCoverUV; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; + m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_4 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_3, param_4); + vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy; +} + +`, + HLSL: "DXBCx\xefn{F\v\x88%\xc6\x05\x8f4h\xe4\xaaP\x01\x00\x00\x00\xd8\x05\x00\x00\x06\x00\x00\x008\x00\x00\x00x\x01\x00\x00\x1c\x03\x00\x00\x98\x03\x00\x00\x1c\x05\x00\x00h\x05\x00\x00Aon98\x01\x00\x008\x01\x00\x00\x00\x02\xfe\xff\x04\x01\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x05\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x06\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x80\xbf\x00\x00\x00?\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\a\x80\x01\x00Đ\x06\x00Š \x06\x00Å \b\x00\x00\x03\x00\x00\b\xe0\x03\x00\xe4\xa0\x00\x00\xe4\x80\b\x00\x00\x03\x00\x00\x04\xe0\x04\x00\xe4\xa0\x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\x80\x01\x00\xe1\x90\x06\x00\xe4\xa0\x06\x00\xe1\xa0\x05\x00\x00\x03\x00\x00\x03\x80\x00\x00\xe4\x80\x06\x00\xe2\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x01\x80\x01\x00\x00\x90\x04\x00\x00\x04\x00\x00\x03\xe0\x00\x00\xe4\x80\x02\x00\xe4\xa0\x02\x00\xee\xa0\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x90\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\v\x80\x06\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\f\xc0\x05\x00\x00\xa0\x00\x00t\x80\x00\x004\x80\xff\xff\x00\x00SHDR\x9c\x01\x00\x00@\x00\x01\x00g\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x05\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00e\x00\x00\x03\xc2 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006\x00\x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x006\x00\x00\x052\x00\x10\x00\x01\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x0f\x00\x00\n\"\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x96\x05\x10\x00\x01\x00\x00\x002\x00\x00\v2 \x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x01\x00\x00\x00\x10\x00\x00\bB \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x10\x00\x00\b\x82 \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x03\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x002\x00\x00\v2 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\nB \x10\x00\x01\x00\x00\x00\n\x80 \x00\x00\x00\x00\x00\x04\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\x01@\x00\x00\x00\x00\x00?6\x00\x00\x05\x82 \x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\v\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF|\x01\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00T\x01\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x05\x00\x00\x00\\\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00\xf8\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00\x10\x01\x00\x00 \x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00%\x01\x00\x000\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00:\x01\x00\x00@\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00D\x01\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_uvCoverTransform\x00_block_uvTransformR1\x00_block_uvTransformR2\x00_block_z\x00\xab\x00\x00\x03\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNh\x00\x00\x00\x03\x00\x00\x00\b\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00P\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x03\x00\x00Y\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_elements_comp = driver.ShaderSources{ + Name: "elements.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct ElementRef +{ + uint offset; +}; + +struct LineSegRef +{ + uint offset; +}; + +struct LineSeg +{ + vec2 p0; + vec2 p1; +}; + +struct QuadSegRef +{ + uint offset; +}; + +struct QuadSeg +{ + vec2 p0; + vec2 p1; + vec2 p2; +}; + +struct CubicSegRef +{ + uint offset; +}; + +struct CubicSeg +{ + vec2 p0; + vec2 p1; + vec2 p2; + vec2 p3; +}; + +struct FillColorRef +{ + uint offset; +}; + +struct FillColor +{ + uint rgba_color; +}; + +struct FillImageRef +{ + uint offset; +}; + +struct FillImage +{ + uint index; + ivec2 offset; +}; + +struct SetLineWidthRef +{ + uint offset; +}; + +struct SetLineWidth +{ + float width; +}; + +struct TransformRef +{ + uint offset; +}; + +struct Transform +{ + vec4 mat; + vec2 translate; +}; + +struct ClipRef +{ + uint offset; +}; + +struct Clip +{ + vec4 bbox; +}; + +struct SetFillModeRef +{ + uint offset; +}; + +struct SetFillMode +{ + uint fill_mode; +}; + +struct ElementTag +{ + uint tag; + uint flags; +}; + +struct StateRef +{ + uint offset; +}; + +struct State +{ + vec4 mat; + vec2 translate; + vec4 bbox; + float linewidth; + uint flags; + uint path_count; + uint pathseg_count; + uint trans_count; +}; + +struct AnnoImageRef +{ + uint offset; +}; + +struct AnnoImage +{ + vec4 bbox; + float linewidth; + uint index; + ivec2 offset; +}; + +struct AnnoColorRef +{ + uint offset; +}; + +struct AnnoColor +{ + vec4 bbox; + float linewidth; + uint rgba_color; +}; + +struct AnnoBeginClipRef +{ + uint offset; +}; + +struct AnnoBeginClip +{ + vec4 bbox; + float linewidth; +}; + +struct AnnoEndClipRef +{ + uint offset; +}; + +struct AnnoEndClip +{ + vec4 bbox; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct PathCubicRef +{ + uint offset; +}; + +struct PathCubic +{ + vec2 p0; + vec2 p1; + vec2 p2; + vec2 p3; + uint path_ix; + uint trans_ix; + vec2 stroke; +}; + +struct PathSegRef +{ + uint offset; +}; + +struct TransformSegRef +{ + uint offset; +}; + +struct TransformSeg +{ + vec4 mat; + vec2 translate; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _294; + +layout(binding = 2, std430) readonly buffer SceneBuf +{ + uint scene[]; +} _323; + +layout(binding = 3, std430) coherent buffer StateBuf +{ + uint part_counter; + uint state[]; +} _779; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _2435; + +shared uint sh_part_ix; +shared State sh_state[32]; +shared State sh_prefix; + +ElementTag Element_tag(ElementRef ref) +{ + uint tag_and_flags = _323.scene[ref.offset >> uint(2)]; + return ElementTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +LineSeg LineSeg_read(LineSegRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + LineSeg s; + s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + return s; +} + +LineSeg Element_Line_read(ElementRef ref) +{ + LineSegRef param = LineSegRef(ref.offset + 4u); + return LineSeg_read(param); +} + +QuadSeg QuadSeg_read(QuadSegRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + uint raw4 = _323.scene[ix + 4u]; + uint raw5 = _323.scene[ix + 5u]; + QuadSeg s; + s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + return s; +} + +QuadSeg Element_Quad_read(ElementRef ref) +{ + QuadSegRef param = QuadSegRef(ref.offset + 4u); + return QuadSeg_read(param); +} + +CubicSeg CubicSeg_read(CubicSegRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + uint raw4 = _323.scene[ix + 4u]; + uint raw5 = _323.scene[ix + 5u]; + uint raw6 = _323.scene[ix + 6u]; + uint raw7 = _323.scene[ix + 7u]; + CubicSeg s; + s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + s.p3 = vec2(uintBitsToFloat(raw6), uintBitsToFloat(raw7)); + return s; +} + +CubicSeg Element_Cubic_read(ElementRef ref) +{ + CubicSegRef param = CubicSegRef(ref.offset + 4u); + return CubicSeg_read(param); +} + +SetLineWidth SetLineWidth_read(SetLineWidthRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + SetLineWidth s; + s.width = uintBitsToFloat(raw0); + return s; +} + +SetLineWidth Element_SetLineWidth_read(ElementRef ref) +{ + SetLineWidthRef param = SetLineWidthRef(ref.offset + 4u); + return SetLineWidth_read(param); +} + +Transform Transform_read(TransformRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + uint raw4 = _323.scene[ix + 4u]; + uint raw5 = _323.scene[ix + 5u]; + Transform s; + s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + return s; +} + +Transform Element_Transform_read(ElementRef ref) +{ + TransformRef param = TransformRef(ref.offset + 4u); + return Transform_read(param); +} + +SetFillMode SetFillMode_read(SetFillModeRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + SetFillMode s; + s.fill_mode = raw0; + return s; +} + +SetFillMode Element_SetFillMode_read(ElementRef ref) +{ + SetFillModeRef param = SetFillModeRef(ref.offset + 4u); + return SetFillMode_read(param); +} + +State map_element(ElementRef ref) +{ + ElementRef param = ref; + uint tag = Element_tag(param).tag; + State c; + c.bbox = vec4(0.0); + c.mat = vec4(1.0, 0.0, 0.0, 1.0); + c.translate = vec2(0.0); + c.linewidth = 1.0; + c.flags = 0u; + c.path_count = 0u; + c.pathseg_count = 0u; + c.trans_count = 0u; + switch (tag) + { + case 1u: + { + ElementRef param_1 = ref; + LineSeg line = Element_Line_read(param_1); + vec2 _1919 = min(line.p0, line.p1); + c.bbox = vec4(_1919.x, _1919.y, c.bbox.z, c.bbox.w); + vec2 _1927 = max(line.p0, line.p1); + c.bbox = vec4(c.bbox.x, c.bbox.y, _1927.x, _1927.y); + c.pathseg_count = 1u; + break; + } + case 2u: + { + ElementRef param_2 = ref; + QuadSeg quad = Element_Quad_read(param_2); + vec2 _1944 = min(min(quad.p0, quad.p1), quad.p2); + c.bbox = vec4(_1944.x, _1944.y, c.bbox.z, c.bbox.w); + vec2 _1955 = max(max(quad.p0, quad.p1), quad.p2); + c.bbox = vec4(c.bbox.x, c.bbox.y, _1955.x, _1955.y); + c.pathseg_count = 1u; + break; + } + case 3u: + { + ElementRef param_3 = ref; + CubicSeg cubic = Element_Cubic_read(param_3); + vec2 _1975 = min(min(cubic.p0, cubic.p1), min(cubic.p2, cubic.p3)); + c.bbox = vec4(_1975.x, _1975.y, c.bbox.z, c.bbox.w); + vec2 _1989 = max(max(cubic.p0, cubic.p1), max(cubic.p2, cubic.p3)); + c.bbox = vec4(c.bbox.x, c.bbox.y, _1989.x, _1989.y); + c.pathseg_count = 1u; + break; + } + case 4u: + case 9u: + case 7u: + { + c.flags = 4u; + c.path_count = 1u; + break; + } + case 8u: + { + c.path_count = 1u; + break; + } + case 5u: + { + ElementRef param_4 = ref; + SetLineWidth lw = Element_SetLineWidth_read(param_4); + c.linewidth = lw.width; + c.flags = 1u; + break; + } + case 6u: + { + ElementRef param_5 = ref; + Transform t = Element_Transform_read(param_5); + c.mat = t.mat; + c.translate = t.translate; + c.trans_count = 1u; + break; + } + case 10u: + { + ElementRef param_6 = ref; + SetFillMode fm = Element_SetFillMode_read(param_6); + c.flags = 8u | (fm.fill_mode << uint(4)); + break; + } + } + return c; +} + +ElementRef Element_index(ElementRef ref, uint index) +{ + return ElementRef(ref.offset + (index * 36u)); +} + +State combine_state(State a, State b) +{ + State c; + c.bbox.x = (min(a.mat.x * b.bbox.x, a.mat.x * b.bbox.z) + min(a.mat.z * b.bbox.y, a.mat.z * b.bbox.w)) + a.translate.x; + c.bbox.y = (min(a.mat.y * b.bbox.x, a.mat.y * b.bbox.z) + min(a.mat.w * b.bbox.y, a.mat.w * b.bbox.w)) + a.translate.y; + c.bbox.z = (max(a.mat.x * b.bbox.x, a.mat.x * b.bbox.z) + max(a.mat.z * b.bbox.y, a.mat.z * b.bbox.w)) + a.translate.x; + c.bbox.w = (max(a.mat.y * b.bbox.x, a.mat.y * b.bbox.z) + max(a.mat.w * b.bbox.y, a.mat.w * b.bbox.w)) + a.translate.y; + bool _1657 = (a.flags & 4u) == 0u; + bool _1665; + if (_1657) + { + _1665 = b.bbox.z <= b.bbox.x; + } + else + { + _1665 = _1657; + } + bool _1673; + if (_1665) + { + _1673 = b.bbox.w <= b.bbox.y; + } + else + { + _1673 = _1665; + } + if (_1673) + { + c.bbox = a.bbox; + } + else + { + bool _1683 = (a.flags & 4u) == 0u; + bool _1690; + if (_1683) + { + _1690 = (b.flags & 2u) == 0u; + } + else + { + _1690 = _1683; + } + bool _1707; + if (_1690) + { + bool _1697 = a.bbox.z > a.bbox.x; + bool _1706; + if (!_1697) + { + _1706 = a.bbox.w > a.bbox.y; + } + else + { + _1706 = _1697; + } + _1707 = _1706; + } + else + { + _1707 = _1690; + } + if (_1707) + { + vec2 _1716 = min(a.bbox.xy, c.bbox.xy); + c.bbox = vec4(_1716.x, _1716.y, c.bbox.z, c.bbox.w); + vec2 _1726 = max(a.bbox.zw, c.bbox.zw); + c.bbox = vec4(c.bbox.x, c.bbox.y, _1726.x, _1726.y); + } + } + c.mat.x = (a.mat.x * b.mat.x) + (a.mat.z * b.mat.y); + c.mat.y = (a.mat.y * b.mat.x) + (a.mat.w * b.mat.y); + c.mat.z = (a.mat.x * b.mat.z) + (a.mat.z * b.mat.w); + c.mat.w = (a.mat.y * b.mat.z) + (a.mat.w * b.mat.w); + c.translate.x = ((a.mat.x * b.translate.x) + (a.mat.z * b.translate.y)) + a.translate.x; + c.translate.y = ((a.mat.y * b.translate.x) + (a.mat.w * b.translate.y)) + a.translate.y; + float _1812; + if ((b.flags & 1u) == 0u) + { + _1812 = a.linewidth; + } + else + { + _1812 = b.linewidth; + } + c.linewidth = _1812; + c.flags = (a.flags & 11u) | b.flags; + c.flags |= ((a.flags & 4u) >> uint(1)); + uint _1842; + if ((b.flags & 8u) == 0u) + { + _1842 = a.flags; + } + else + { + _1842 = b.flags; + } + uint fill_mode = _1842; + fill_mode &= 16u; + c.flags = (c.flags & 4294967279u) | fill_mode; + c.path_count = a.path_count + b.path_count; + c.pathseg_count = a.pathseg_count + b.pathseg_count; + c.trans_count = a.trans_count + b.trans_count; + return c; +} + +StateRef state_aggregate_ref(uint partition_ix) +{ + return StateRef(4u + (partition_ix * 124u)); +} + +void State_write(StateRef ref, State s) +{ + uint ix = ref.offset >> uint(2); + _779.state[ix + 0u] = floatBitsToUint(s.mat.x); + _779.state[ix + 1u] = floatBitsToUint(s.mat.y); + _779.state[ix + 2u] = floatBitsToUint(s.mat.z); + _779.state[ix + 3u] = floatBitsToUint(s.mat.w); + _779.state[ix + 4u] = floatBitsToUint(s.translate.x); + _779.state[ix + 5u] = floatBitsToUint(s.translate.y); + _779.state[ix + 6u] = floatBitsToUint(s.bbox.x); + _779.state[ix + 7u] = floatBitsToUint(s.bbox.y); + _779.state[ix + 8u] = floatBitsToUint(s.bbox.z); + _779.state[ix + 9u] = floatBitsToUint(s.bbox.w); + _779.state[ix + 10u] = floatBitsToUint(s.linewidth); + _779.state[ix + 11u] = s.flags; + _779.state[ix + 12u] = s.path_count; + _779.state[ix + 13u] = s.pathseg_count; + _779.state[ix + 14u] = s.trans_count; +} + +StateRef state_prefix_ref(uint partition_ix) +{ + return StateRef((4u + (partition_ix * 124u)) + 60u); +} + +uint state_flag_index(uint partition_ix) +{ + return partition_ix * 31u; +} + +State State_read(StateRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _779.state[ix + 0u]; + uint raw1 = _779.state[ix + 1u]; + uint raw2 = _779.state[ix + 2u]; + uint raw3 = _779.state[ix + 3u]; + uint raw4 = _779.state[ix + 4u]; + uint raw5 = _779.state[ix + 5u]; + uint raw6 = _779.state[ix + 6u]; + uint raw7 = _779.state[ix + 7u]; + uint raw8 = _779.state[ix + 8u]; + uint raw9 = _779.state[ix + 9u]; + uint raw10 = _779.state[ix + 10u]; + uint raw11 = _779.state[ix + 11u]; + uint raw12 = _779.state[ix + 12u]; + uint raw13 = _779.state[ix + 13u]; + uint raw14 = _779.state[ix + 14u]; + State s; + s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + s.bbox = vec4(uintBitsToFloat(raw6), uintBitsToFloat(raw7), uintBitsToFloat(raw8), uintBitsToFloat(raw9)); + s.linewidth = uintBitsToFloat(raw10); + s.flags = raw11; + s.path_count = raw12; + s.pathseg_count = raw13; + s.trans_count = raw14; + return s; +} + +uint fill_mode_from_flags(uint flags) +{ + return flags & 1u; +} + +vec2 get_linewidth(State st) +{ + return vec2(length(st.mat.xz), length(st.mat.yw)) * (0.5 * st.linewidth); +} + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _294.memory[offset] = val; +} + +void PathCubic_write(Alloc a, PathCubicRef ref, PathCubic s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.p0.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.p0.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.p1.x); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.p1.y); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.p2.x); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = floatBitsToUint(s.p2.y); + write_mem(param_15, param_16, param_17); + Alloc param_18 = a; + uint param_19 = ix + 6u; + uint param_20 = floatBitsToUint(s.p3.x); + write_mem(param_18, param_19, param_20); + Alloc param_21 = a; + uint param_22 = ix + 7u; + uint param_23 = floatBitsToUint(s.p3.y); + write_mem(param_21, param_22, param_23); + Alloc param_24 = a; + uint param_25 = ix + 8u; + uint param_26 = s.path_ix; + write_mem(param_24, param_25, param_26); + Alloc param_27 = a; + uint param_28 = ix + 9u; + uint param_29 = s.trans_ix; + write_mem(param_27, param_28, param_29); + Alloc param_30 = a; + uint param_31 = ix + 10u; + uint param_32 = floatBitsToUint(s.stroke.x); + write_mem(param_30, param_31, param_32); + Alloc param_33 = a; + uint param_34 = ix + 11u; + uint param_35 = floatBitsToUint(s.stroke.y); + write_mem(param_33, param_34, param_35); +} + +void PathSeg_Cubic_write(Alloc a, PathSegRef ref, uint flags, PathCubic s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = (flags << uint(16)) | 1u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + PathCubicRef param_4 = PathCubicRef(ref.offset + 4u); + PathCubic param_5 = s; + PathCubic_write(param_3, param_4, param_5); +} + +FillColor FillColor_read(FillColorRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + FillColor s; + s.rgba_color = raw0; + return s; +} + +FillColor Element_FillColor_read(ElementRef ref) +{ + FillColorRef param = FillColorRef(ref.offset + 4u); + return FillColor_read(param); +} + +void AnnoColor_write(Alloc a, AnnoColorRef ref, AnnoColor s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.bbox.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.bbox.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.bbox.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.bbox.w); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.linewidth); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = s.rgba_color; + write_mem(param_15, param_16, param_17); +} + +void Annotated_Color_write(Alloc a, AnnotatedRef ref, uint flags, AnnoColor s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = (flags << uint(16)) | 1u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + AnnoColorRef param_4 = AnnoColorRef(ref.offset + 4u); + AnnoColor param_5 = s; + AnnoColor_write(param_3, param_4, param_5); +} + +FillImage FillImage_read(FillImageRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + FillImage s; + s.index = raw0; + s.offset = ivec2(int(raw1 << uint(16)) >> 16, int(raw1) >> 16); + return s; +} + +FillImage Element_FillImage_read(ElementRef ref) +{ + FillImageRef param = FillImageRef(ref.offset + 4u); + return FillImage_read(param); +} + +void AnnoImage_write(Alloc a, AnnoImageRef ref, AnnoImage s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.bbox.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.bbox.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.bbox.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.bbox.w); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.linewidth); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = s.index; + write_mem(param_15, param_16, param_17); + Alloc param_18 = a; + uint param_19 = ix + 6u; + uint param_20 = (uint(s.offset.x) & 65535u) | (uint(s.offset.y) << uint(16)); + write_mem(param_18, param_19, param_20); +} + +void Annotated_Image_write(Alloc a, AnnotatedRef ref, uint flags, AnnoImage s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = (flags << uint(16)) | 2u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + AnnoImageRef param_4 = AnnoImageRef(ref.offset + 4u); + AnnoImage param_5 = s; + AnnoImage_write(param_3, param_4, param_5); +} + +Clip Clip_read(ClipRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + Clip s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + return s; +} + +Clip Element_BeginClip_read(ElementRef ref) +{ + ClipRef param = ClipRef(ref.offset + 4u); + return Clip_read(param); +} + +void AnnoBeginClip_write(Alloc a, AnnoBeginClipRef ref, AnnoBeginClip s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.bbox.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.bbox.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.bbox.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.bbox.w); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.linewidth); + write_mem(param_12, param_13, param_14); +} + +void Annotated_BeginClip_write(Alloc a, AnnotatedRef ref, uint flags, AnnoBeginClip s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = (flags << uint(16)) | 3u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + AnnoBeginClipRef param_4 = AnnoBeginClipRef(ref.offset + 4u); + AnnoBeginClip param_5 = s; + AnnoBeginClip_write(param_3, param_4, param_5); +} + +Clip Element_EndClip_read(ElementRef ref) +{ + ClipRef param = ClipRef(ref.offset + 4u); + return Clip_read(param); +} + +void AnnoEndClip_write(Alloc a, AnnoEndClipRef ref, AnnoEndClip s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.bbox.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.bbox.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.bbox.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.bbox.w); + write_mem(param_9, param_10, param_11); +} + +void Annotated_EndClip_write(Alloc a, AnnotatedRef ref, AnnoEndClip s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 4u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + AnnoEndClipRef param_4 = AnnoEndClipRef(ref.offset + 4u); + AnnoEndClip param_5 = s; + AnnoEndClip_write(param_3, param_4, param_5); +} + +void TransformSeg_write(Alloc a, TransformSegRef ref, TransformSeg s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.mat.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.mat.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.mat.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.mat.w); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.translate.x); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = floatBitsToUint(s.translate.y); + write_mem(param_15, param_16, param_17); +} + +void main() +{ + if (_294.mem_error != 0u) + { + return; + } + if (gl_LocalInvocationID.x == 0u) + { + uint _2069 = atomicAdd(_779.part_counter, 1u); + sh_part_ix = _2069; + } + barrier(); + uint part_ix = sh_part_ix; + uint ix = (part_ix * 128u) + (gl_LocalInvocationID.x * 4u); + ElementRef ref = ElementRef(ix * 36u); + ElementRef param = ref; + State th_state[4]; + th_state[0] = map_element(param); + for (uint i = 1u; i < 4u; i++) + { + ElementRef param_1 = ref; + uint param_2 = i; + ElementRef param_3 = Element_index(param_1, param_2); + State param_4 = th_state[i - 1u]; + State param_5 = map_element(param_3); + th_state[i] = combine_state(param_4, param_5); + } + State agg = th_state[3]; + sh_state[gl_LocalInvocationID.x] = agg; + for (uint i_1 = 0u; i_1 < 5u; i_1++) + { + barrier(); + if (gl_LocalInvocationID.x >= uint(1 << int(i_1))) + { + State other = sh_state[gl_LocalInvocationID.x - uint(1 << int(i_1))]; + State param_6 = other; + State param_7 = agg; + agg = combine_state(param_6, param_7); + } + barrier(); + sh_state[gl_LocalInvocationID.x] = agg; + } + State exclusive; + exclusive.bbox = vec4(0.0); + exclusive.mat = vec4(1.0, 0.0, 0.0, 1.0); + exclusive.translate = vec2(0.0); + exclusive.linewidth = 1.0; + exclusive.flags = 0u; + exclusive.path_count = 0u; + exclusive.pathseg_count = 0u; + exclusive.trans_count = 0u; + if (gl_LocalInvocationID.x == 31u) + { + uint param_8 = part_ix; + StateRef param_9 = state_aggregate_ref(param_8); + State param_10 = agg; + State_write(param_9, param_10); + uint flag = 1u; + memoryBarrierBuffer(); + if (part_ix == 0u) + { + uint param_11 = part_ix; + StateRef param_12 = state_prefix_ref(param_11); + State param_13 = agg; + State_write(param_12, param_13); + flag = 2u; + } + uint param_14 = part_ix; + _779.state[state_flag_index(param_14)] = flag; + if (part_ix != 0u) + { + uint look_back_ix = part_ix - 1u; + uint their_ix = 0u; + State their_agg; + while (true) + { + uint param_15 = look_back_ix; + flag = _779.state[state_flag_index(param_15)]; + if (flag == 2u) + { + uint param_16 = look_back_ix; + StateRef param_17 = state_prefix_ref(param_16); + State their_prefix = State_read(param_17); + State param_18 = their_prefix; + State param_19 = exclusive; + exclusive = combine_state(param_18, param_19); + break; + } + else + { + if (flag == 1u) + { + uint param_20 = look_back_ix; + StateRef param_21 = state_aggregate_ref(param_20); + their_agg = State_read(param_21); + State param_22 = their_agg; + State param_23 = exclusive; + exclusive = combine_state(param_22, param_23); + look_back_ix--; + their_ix = 0u; + continue; + } + } + ElementRef ref_1 = ElementRef(((look_back_ix * 128u) + their_ix) * 36u); + ElementRef param_24 = ref_1; + State s = map_element(param_24); + if (their_ix == 0u) + { + their_agg = s; + } + else + { + State param_25 = their_agg; + State param_26 = s; + their_agg = combine_state(param_25, param_26); + } + their_ix++; + if (their_ix == 128u) + { + State param_27 = their_agg; + State param_28 = exclusive; + exclusive = combine_state(param_27, param_28); + if (look_back_ix == 0u) + { + break; + } + look_back_ix--; + their_ix = 0u; + } + } + State param_29 = exclusive; + State param_30 = agg; + State inclusive_prefix = combine_state(param_29, param_30); + sh_prefix = exclusive; + uint param_31 = part_ix; + StateRef param_32 = state_prefix_ref(param_31); + State param_33 = inclusive_prefix; + State_write(param_32, param_33); + memoryBarrierBuffer(); + flag = 2u; + uint param_34 = part_ix; + _779.state[state_flag_index(param_34)] = flag; + } + } + barrier(); + if (part_ix != 0u) + { + exclusive = sh_prefix; + } + State row = exclusive; + if (gl_LocalInvocationID.x > 0u) + { + State other_1 = sh_state[gl_LocalInvocationID.x - 1u]; + State param_35 = row; + State param_36 = other_1; + row = combine_state(param_35, param_36); + } + PathCubic path_cubic; + PathSegRef path_out_ref; + Alloc param_45; + Alloc param_51; + Alloc param_57; + AnnoColor anno_fill; + AnnotatedRef out_ref; + Alloc param_63; + AnnoImage anno_img; + Alloc param_69; + AnnoBeginClip anno_begin_clip; + Alloc param_75; + Alloc param_80; + Alloc param_83; + for (uint i_2 = 0u; i_2 < 4u; i_2++) + { + State param_37 = row; + State param_38 = th_state[i_2]; + State st = combine_state(param_37, param_38); + ElementRef param_39 = ref; + uint param_40 = i_2; + ElementRef this_ref = Element_index(param_39, param_40); + ElementRef param_41 = this_ref; + ElementTag tag = Element_tag(param_41); + uint param_42 = st.flags >> uint(4); + uint fill_mode = fill_mode_from_flags(param_42); + bool is_stroke = fill_mode == 1u; + switch (tag.tag) + { + case 1u: + { + ElementRef param_43 = this_ref; + LineSeg line = Element_Line_read(param_43); + path_cubic.p0 = line.p0; + path_cubic.p1 = mix(line.p0, line.p1, vec2(0.3333333432674407958984375)); + path_cubic.p2 = mix(line.p1, line.p0, vec2(0.3333333432674407958984375)); + path_cubic.p3 = line.p1; + path_cubic.path_ix = st.path_count; + path_cubic.trans_ix = st.trans_count; + if (is_stroke) + { + State param_44 = st; + path_cubic.stroke = get_linewidth(param_44); + } + else + { + path_cubic.stroke = vec2(0.0); + } + path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u)); + param_45.offset = _2435.conf.pathseg_alloc.offset; + PathSegRef param_46 = path_out_ref; + uint param_47 = fill_mode; + PathCubic param_48 = path_cubic; + PathSeg_Cubic_write(param_45, param_46, param_47, param_48); + break; + } + case 2u: + { + ElementRef param_49 = this_ref; + QuadSeg quad = Element_Quad_read(param_49); + path_cubic.p0 = quad.p0; + path_cubic.p1 = mix(quad.p1, quad.p0, vec2(0.3333333432674407958984375)); + path_cubic.p2 = mix(quad.p1, quad.p2, vec2(0.3333333432674407958984375)); + path_cubic.p3 = quad.p2; + path_cubic.path_ix = st.path_count; + path_cubic.trans_ix = st.trans_count; + if (is_stroke) + { + State param_50 = st; + path_cubic.stroke = get_linewidth(param_50); + } + else + { + path_cubic.stroke = vec2(0.0); + } + path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u)); + param_51.offset = _2435.conf.pathseg_alloc.offset; + PathSegRef param_52 = path_out_ref; + uint param_53 = fill_mode; + PathCubic param_54 = path_cubic; + PathSeg_Cubic_write(param_51, param_52, param_53, param_54); + break; + } + case 3u: + { + ElementRef param_55 = this_ref; + CubicSeg cubic = Element_Cubic_read(param_55); + path_cubic.p0 = cubic.p0; + path_cubic.p1 = cubic.p1; + path_cubic.p2 = cubic.p2; + path_cubic.p3 = cubic.p3; + path_cubic.path_ix = st.path_count; + path_cubic.trans_ix = st.trans_count; + if (is_stroke) + { + State param_56 = st; + path_cubic.stroke = get_linewidth(param_56); + } + else + { + path_cubic.stroke = vec2(0.0); + } + path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u)); + param_57.offset = _2435.conf.pathseg_alloc.offset; + PathSegRef param_58 = path_out_ref; + uint param_59 = fill_mode; + PathCubic param_60 = path_cubic; + PathSeg_Cubic_write(param_57, param_58, param_59, param_60); + break; + } + case 4u: + { + ElementRef param_61 = this_ref; + FillColor fill = Element_FillColor_read(param_61); + anno_fill.rgba_color = fill.rgba_color; + if (is_stroke) + { + State param_62 = st; + vec2 lw = get_linewidth(param_62); + anno_fill.bbox = st.bbox + vec4(-lw, lw); + anno_fill.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z))); + } + else + { + anno_fill.bbox = st.bbox; + anno_fill.linewidth = 0.0; + } + out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u)); + param_63.offset = _2435.conf.anno_alloc.offset; + AnnotatedRef param_64 = out_ref; + uint param_65 = fill_mode; + AnnoColor param_66 = anno_fill; + Annotated_Color_write(param_63, param_64, param_65, param_66); + break; + } + case 9u: + { + ElementRef param_67 = this_ref; + FillImage fill_img = Element_FillImage_read(param_67); + anno_img.index = fill_img.index; + anno_img.offset = fill_img.offset; + if (is_stroke) + { + State param_68 = st; + vec2 lw_1 = get_linewidth(param_68); + anno_img.bbox = st.bbox + vec4(-lw_1, lw_1); + anno_img.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z))); + } + else + { + anno_img.bbox = st.bbox; + anno_img.linewidth = 0.0; + } + out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u)); + param_69.offset = _2435.conf.anno_alloc.offset; + AnnotatedRef param_70 = out_ref; + uint param_71 = fill_mode; + AnnoImage param_72 = anno_img; + Annotated_Image_write(param_69, param_70, param_71, param_72); + break; + } + case 7u: + { + ElementRef param_73 = this_ref; + Clip begin_clip = Element_BeginClip_read(param_73); + anno_begin_clip.bbox = begin_clip.bbox; + if (is_stroke) + { + State param_74 = st; + vec2 lw_2 = get_linewidth(param_74); + anno_begin_clip.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z))); + } + else + { + anno_fill.linewidth = 0.0; + } + out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u)); + param_75.offset = _2435.conf.anno_alloc.offset; + AnnotatedRef param_76 = out_ref; + uint param_77 = fill_mode; + AnnoBeginClip param_78 = anno_begin_clip; + Annotated_BeginClip_write(param_75, param_76, param_77, param_78); + break; + } + case 8u: + { + ElementRef param_79 = this_ref; + Clip end_clip = Element_EndClip_read(param_79); + AnnoEndClip anno_end_clip = AnnoEndClip(end_clip.bbox); + out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u)); + param_80.offset = _2435.conf.anno_alloc.offset; + AnnotatedRef param_81 = out_ref; + AnnoEndClip param_82 = anno_end_clip; + Annotated_EndClip_write(param_80, param_81, param_82); + break; + } + case 6u: + { + TransformSeg transform = TransformSeg(st.mat, st.translate); + TransformSegRef trans_ref = TransformSegRef(_2435.conf.trans_alloc.offset + ((st.trans_count - 1u) * 24u)); + param_83.offset = _2435.conf.trans_alloc.offset; + TransformSegRef param_84 = trans_ref; + TransformSeg param_85 = transform; + TransformSeg_write(param_83, param_84, param_85); + break; + } + } + } +} + +`, + } + shader_intersect_frag = driver.ShaderSources{ + Name: "intersect.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Textures: []driver.TextureBinding{{Name: "cover", Binding: 0}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +uniform mediump sampler2D cover; + +varying highp vec2 vUV; + +void main() +{ + float cover_1 = abs(texture2D(cover, vUV).x); + gl_FragData[0].x = cover_1; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D cover; + +in highp vec2 vUV; +layout(location = 0) out vec4 fragColor; + +void main() +{ + float cover_1 = abs(texture(cover, vUV).x); + fragColor.x = cover_1; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D cover; + +in vec2 vUV; +out vec4 fragColor; + +void main() +{ + float cover_1 = abs(texture(cover, vUV).x); + fragColor.x = cover_1; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D cover; + +in vec2 vUV; +out vec4 fragColor; + +void main() +{ + float cover_1 = abs(texture(cover, vUV).x); + fragColor.x = cover_1; +} + +`, + HLSL: "DXBC\xe0\xe4\x03\x8c\xacVF\x82l\xe7|\xc3T\xa6'\xef\x01\x00\x00\x00\b\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xd4\x00\x00\x00\x80\x01\x00\x00\xfc\x01\x00\x00\xa0\x02\x00\x00\xd4\x02\x00\x00Aon9\x94\x00\x00\x00\x94\x00\x00\x00\x00\x02\xff\xffl\x00\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0#\x00\x00\x02\x00\x00\x01\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x0e\x80\x00\x00\x00\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xa4\x00\x00\x00@\x00\x00\x00)\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x006\x00\x00\x06\x12 \x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xe2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x9c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00q\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00k\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_cover_sampler\x00cover\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + } + shader_intersect_vert = driver.ShaderSources{ + Name: "intersect.vert", + Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_block.uvTransform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.subUVTransform", Type: 0x0, Size: 4, Offset: 16}}, + Size: 32, + }, + GLSL100ES: `#version 100 + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 uvTransform; + vec4 subUVTransform; +}; + +uniform Block _block; + +attribute vec2 pos; +attribute vec2 uv; +varying vec2 vUV; + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0)); + vec3 param_1 = vec3(pos, 1.0); + vec3 p = transform3x2(param, param_1); + gl_Position = vec4(p, 1.0); + m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_3 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_2, param_3); + vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw; + m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_5 = vec3(vUV, 1.0); + vUV = transform3x2(param_4, param_5).xy; + vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw; +} + +`, + GLSL300ES: `#version 300 es + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(std140) uniform Block +{ + vec4 uvTransform; + vec4 subUVTransform; +} _block; + +layout(location = 0) in vec2 pos; +layout(location = 1) in vec2 uv; +out vec2 vUV; + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0)); + vec3 param_1 = vec3(pos, 1.0); + vec3 p = transform3x2(param, param_1); + gl_Position = vec4(p, 1.0); + m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_3 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_2, param_3); + vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw; + m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_5 = vec3(vUV, 1.0); + vUV = transform3x2(param_4, param_5).xy; + vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 uvTransform; + vec4 subUVTransform; +}; + +uniform Block _block; + +in vec2 pos; +in vec2 uv; +out vec2 vUV; + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0)); + vec3 param_1 = vec3(pos, 1.0); + vec3 p = transform3x2(param, param_1); + gl_Position = vec4(p, 1.0); + m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_3 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_2, param_3); + vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw; + m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_5 = vec3(vUV, 1.0); + vUV = transform3x2(param_4, param_5).xy; + vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(binding = 0, std140) uniform Block +{ + vec4 uvTransform; + vec4 subUVTransform; +} _block; + +in vec2 pos; +in vec2 uv; +out vec2 vUV; + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0)); + vec3 param_1 = vec3(pos, 1.0); + vec3 p = transform3x2(param, param_1); + gl_Position = vec4(p, 1.0); + m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_3 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_2, param_3); + vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw; + m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_5 = vec3(vUV, 1.0); + vUV = transform3x2(param_4, param_5).xy; + vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw; +} + +`, + HLSL: "DXBCxH\xc4I\xbe\x0f[|\nl\x899\xe0\xb8\xcb?\x01\x00\x00\x00\xdc\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00L\x01\x00\x00\xc4\x02\x00\x00@\x03\x00\x008\x04\x00\x00\x84\x04\x00\x00Aon9\f\x01\x00\x00\f\x01\x00\x00\x00\x02\xfe\xff\xd8\x00\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x03\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x80\xbf\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\x03\x80\x01\x00U\x90\x03\x00\xe4\xa0\x03\x00\xe1\xa0\x05\x00\x00\x03\x00\x00\x03\x80\x00\x00\xe4\x80\x03\x00\xe2\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x01\x80\x01\x00\x00\x90\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x80\x02\x00\xe4\xa0\x02\x00\xee\xa0\x01\x00\x00\x02\x00\x00\x04\x80\x03\x00\x00\xa0\b\x00\x00\x03\x00\x00\b\x80\x03\x00É \x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\xe0\x00\x00\xec\x80\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x90\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\f\xc0\x03\x00\x00\xa0\xff\xff\x00\x00SHDRp\x01\x00\x00@\x00\x01\x00\\\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x006\x00\x00\x05\"\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?6\x00\x00\x05R\x00\x10\x00\x00\x00\x00\x00V\x14\x10\x00\x01\x00\x00\x00\x0f\x00\x00\n\x82\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x002\x00\x00\v2\x00\x10\x00\x00\x00\x00\x00\xe6\n\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x0f\x00\x00\n\x82\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x96\x05\x10\x00\x00\x00\x00\x002\x00\x00\v2 \x10\x00\x00\x00\x00\x00\xc6\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\x052 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x01\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\n\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xf0\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\xc6\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x02\x00\x00\x00\\\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00_block_uvTransform\x00\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_subUVTransform\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_kernel4_comp = driver.ShaderSources{ + Name: "kernel4.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 16, local_size_y = 8, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct CmdStrokeRef +{ + uint offset; +}; + +struct CmdStroke +{ + uint tile_ref; + float half_width; +}; + +struct CmdFillRef +{ + uint offset; +}; + +struct CmdFill +{ + uint tile_ref; + int backdrop; +}; + +struct CmdColorRef +{ + uint offset; +}; + +struct CmdColor +{ + uint rgba_color; +}; + +struct CmdImageRef +{ + uint offset; +}; + +struct CmdImage +{ + uint index; + ivec2 offset; +}; + +struct CmdAlphaRef +{ + uint offset; +}; + +struct CmdAlpha +{ + float alpha; +}; + +struct CmdJumpRef +{ + uint offset; +}; + +struct CmdJump +{ + uint new_ref; +}; + +struct CmdRef +{ + uint offset; +}; + +struct CmdTag +{ + uint tag; + uint flags; +}; + +struct TileSegRef +{ + uint offset; +}; + +struct TileSeg +{ + vec2 origin; + vec2 vector; + float y_edge; + TileSegRef next; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _196; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _693; + +layout(binding = 3, rgba8) uniform readonly highp image2D images[1]; +layout(binding = 2, rgba8) uniform writeonly highp image2D image; + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +Alloc slice_mem(Alloc a, uint offset, uint size) +{ + uint param = a.offset + offset; + uint param_1 = size; + return new_alloc(param, param_1); +} + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _196.memory[offset]; + return v; +} + +Alloc alloc_read(Alloc a, uint offset) +{ + Alloc param = a; + uint param_1 = offset >> uint(2); + Alloc alloc; + alloc.offset = read_mem(param, param_1); + return alloc; +} + +CmdTag Cmd_tag(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return CmdTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +CmdStroke CmdStroke_read(Alloc a, CmdStrokeRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + CmdStroke s; + s.tile_ref = raw0; + s.half_width = uintBitsToFloat(raw1); + return s; +} + +CmdStroke Cmd_Stroke_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdStrokeRef param_1 = CmdStrokeRef(ref.offset + 4u); + return CmdStroke_read(param, param_1); +} + +TileSeg TileSeg_read(Alloc a, TileSegRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + TileSeg s; + s.origin = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.vector = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.y_edge = uintBitsToFloat(raw4); + s.next = TileSegRef(raw5); + return s; +} + +uvec2 chunk_offset(uint i) +{ + return uvec2((i % 2u) * 16u, (i / 2u) * 8u); +} + +CmdFill CmdFill_read(Alloc a, CmdFillRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + CmdFill s; + s.tile_ref = raw0; + s.backdrop = int(raw1); + return s; +} + +CmdFill Cmd_Fill_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdFillRef param_1 = CmdFillRef(ref.offset + 4u); + return CmdFill_read(param, param_1); +} + +CmdAlpha CmdAlpha_read(Alloc a, CmdAlphaRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + CmdAlpha s; + s.alpha = uintBitsToFloat(raw0); + return s; +} + +CmdAlpha Cmd_Alpha_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdAlphaRef param_1 = CmdAlphaRef(ref.offset + 4u); + return CmdAlpha_read(param, param_1); +} + +CmdColor CmdColor_read(Alloc a, CmdColorRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + CmdColor s; + s.rgba_color = raw0; + return s; +} + +CmdColor Cmd_Color_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdColorRef param_1 = CmdColorRef(ref.offset + 4u); + return CmdColor_read(param, param_1); +} + +vec3 fromsRGB(vec3 srgb) +{ + bvec3 cutoff = greaterThanEqual(srgb, vec3(0.040449999272823333740234375)); + vec3 below = srgb / vec3(12.9200000762939453125); + vec3 above = pow((srgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625)); + return mix(below, above, cutoff); +} + +vec4 unpacksRGB(uint srgba) +{ + vec4 color = unpackUnorm4x8(srgba).wzyx; + vec3 param = color.xyz; + return vec4(fromsRGB(param), color.w); +} + +CmdImage CmdImage_read(Alloc a, CmdImageRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + CmdImage s; + s.index = raw0; + s.offset = ivec2(int(raw1 << uint(16)) >> 16, int(raw1) >> 16); + return s; +} + +CmdImage Cmd_Image_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdImageRef param_1 = CmdImageRef(ref.offset + 4u); + return CmdImage_read(param, param_1); +} + +vec4[8] fillImage(uvec2 xy, CmdImage cmd_img) +{ + vec4 rgba[8]; + for (uint i = 0u; i < 8u; i++) + { + uint param = i; + ivec2 uv = ivec2(xy + chunk_offset(param)) + cmd_img.offset; + vec4 fg_rgba = imageLoad(images[0], uv); + vec3 param_1 = fg_rgba.xyz; + vec3 _663 = fromsRGB(param_1); + fg_rgba = vec4(_663.x, _663.y, _663.z, fg_rgba.w); + rgba[i] = fg_rgba; + } + return rgba; +} + +vec3 tosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return mix(below, above, cutoff); +} + +uint packsRGB(inout vec4 rgba) +{ + vec3 param = rgba.xyz; + rgba = vec4(tosRGB(param), rgba.w); + return packUnorm4x8(rgba.wzyx); +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _196.memory[offset] = val; +} + +CmdJump CmdJump_read(Alloc a, CmdJumpRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + CmdJump s; + s.new_ref = raw0; + return s; +} + +CmdJump Cmd_Jump_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdJumpRef param_1 = CmdJumpRef(ref.offset + 4u); + return CmdJump_read(param, param_1); +} + +void main() +{ + if (_196.mem_error != 0u) + { + return; + } + uint tile_ix = (gl_WorkGroupID.y * _693.conf.width_in_tiles) + gl_WorkGroupID.x; + Alloc param; + param.offset = _693.conf.ptcl_alloc.offset; + uint param_1 = tile_ix * 1024u; + uint param_2 = 1024u; + Alloc cmd_alloc = slice_mem(param, param_1, param_2); + CmdRef cmd_ref = CmdRef(cmd_alloc.offset); + Alloc param_3 = cmd_alloc; + uint param_4 = cmd_ref.offset; + Alloc scratch_alloc = alloc_read(param_3, param_4); + cmd_ref.offset += 8u; + uvec2 xy_uint = uvec2(gl_LocalInvocationID.x + (32u * gl_WorkGroupID.x), gl_LocalInvocationID.y + (32u * gl_WorkGroupID.y)); + vec2 xy = vec2(xy_uint); + vec4 rgba[8]; + for (uint i = 0u; i < 8u; i++) + { + rgba[i] = vec4(0.0); + } + uint clip_depth = 0u; + float df[8]; + TileSegRef tile_seg_ref; + float area[8]; + uint base_ix; + while (true) + { + Alloc param_5 = cmd_alloc; + CmdRef param_6 = cmd_ref; + uint tag = Cmd_tag(param_5, param_6).tag; + if (tag == 0u) + { + break; + } + switch (tag) + { + case 2u: + { + Alloc param_7 = cmd_alloc; + CmdRef param_8 = cmd_ref; + CmdStroke stroke = Cmd_Stroke_read(param_7, param_8); + for (uint k = 0u; k < 8u; k++) + { + df[k] = 1000000000.0; + } + tile_seg_ref = TileSegRef(stroke.tile_ref); + do + { + uint param_9 = tile_seg_ref.offset; + uint param_10 = 24u; + Alloc param_11 = new_alloc(param_9, param_10); + TileSegRef param_12 = tile_seg_ref; + TileSeg seg = TileSeg_read(param_11, param_12); + vec2 line_vec = seg.vector; + for (uint k_1 = 0u; k_1 < 8u; k_1++) + { + vec2 dpos = (xy + vec2(0.5)) - seg.origin; + uint param_13 = k_1; + dpos += vec2(chunk_offset(param_13)); + float t = clamp(dot(line_vec, dpos) / dot(line_vec, line_vec), 0.0, 1.0); + df[k_1] = min(df[k_1], length((line_vec * t) - dpos)); + } + tile_seg_ref = seg.next; + } while (tile_seg_ref.offset != 0u); + for (uint k_2 = 0u; k_2 < 8u; k_2++) + { + area[k_2] = clamp((stroke.half_width + 0.5) - df[k_2], 0.0, 1.0); + } + cmd_ref.offset += 12u; + break; + } + case 1u: + { + Alloc param_14 = cmd_alloc; + CmdRef param_15 = cmd_ref; + CmdFill fill = Cmd_Fill_read(param_14, param_15); + for (uint k_3 = 0u; k_3 < 8u; k_3++) + { + area[k_3] = float(fill.backdrop); + } + tile_seg_ref = TileSegRef(fill.tile_ref); + do + { + uint param_16 = tile_seg_ref.offset; + uint param_17 = 24u; + Alloc param_18 = new_alloc(param_16, param_17); + TileSegRef param_19 = tile_seg_ref; + TileSeg seg_1 = TileSeg_read(param_18, param_19); + for (uint k_4 = 0u; k_4 < 8u; k_4++) + { + uint param_20 = k_4; + vec2 my_xy = xy + vec2(chunk_offset(param_20)); + vec2 start = seg_1.origin - my_xy; + vec2 end = start + seg_1.vector; + vec2 window = clamp(vec2(start.y, end.y), vec2(0.0), vec2(1.0)); + if (!(window.x == window.y)) + { + vec2 t_1 = (window - vec2(start.y)) / vec2(seg_1.vector.y); + vec2 xs = vec2(mix(start.x, end.x, t_1.x), mix(start.x, end.x, t_1.y)); + float xmin = min(min(xs.x, xs.y), 1.0) - 9.9999999747524270787835121154785e-07; + float xmax = max(xs.x, xs.y); + float b = min(xmax, 1.0); + float c = max(b, 0.0); + float d = max(xmin, 0.0); + float a = ((b + (0.5 * ((d * d) - (c * c)))) - xmin) / (xmax - xmin); + area[k_4] += (a * (window.x - window.y)); + } + area[k_4] += (sign(seg_1.vector.x) * clamp((my_xy.y - seg_1.y_edge) + 1.0, 0.0, 1.0)); + } + tile_seg_ref = seg_1.next; + } while (tile_seg_ref.offset != 0u); + for (uint k_5 = 0u; k_5 < 8u; k_5++) + { + area[k_5] = min(abs(area[k_5]), 1.0); + } + cmd_ref.offset += 12u; + break; + } + case 3u: + { + for (uint k_6 = 0u; k_6 < 8u; k_6++) + { + area[k_6] = 1.0; + } + cmd_ref.offset += 4u; + break; + } + case 4u: + { + Alloc param_21 = cmd_alloc; + CmdRef param_22 = cmd_ref; + CmdAlpha alpha = Cmd_Alpha_read(param_21, param_22); + for (uint k_7 = 0u; k_7 < 8u; k_7++) + { + area[k_7] = alpha.alpha; + } + cmd_ref.offset += 8u; + break; + } + case 5u: + { + Alloc param_23 = cmd_alloc; + CmdRef param_24 = cmd_ref; + CmdColor color = Cmd_Color_read(param_23, param_24); + uint param_25 = color.rgba_color; + vec4 fg = unpacksRGB(param_25); + for (uint k_8 = 0u; k_8 < 8u; k_8++) + { + vec4 fg_k = fg * area[k_8]; + rgba[k_8] = (rgba[k_8] * (1.0 - fg_k.w)) + fg_k; + } + cmd_ref.offset += 8u; + break; + } + case 6u: + { + Alloc param_26 = cmd_alloc; + CmdRef param_27 = cmd_ref; + CmdImage fill_img = Cmd_Image_read(param_26, param_27); + uvec2 param_28 = xy_uint; + CmdImage param_29 = fill_img; + vec4 img[8] = fillImage(param_28, param_29); + for (uint k_9 = 0u; k_9 < 8u; k_9++) + { + vec4 fg_k_1 = img[k_9] * area[k_9]; + rgba[k_9] = (rgba[k_9] * (1.0 - fg_k_1.w)) + fg_k_1; + } + cmd_ref.offset += 12u; + break; + } + case 7u: + { + base_ix = (scratch_alloc.offset >> uint(2)) + (2u * ((((clip_depth * 32u) * 32u) + gl_LocalInvocationID.x) + (32u * gl_LocalInvocationID.y))); + for (uint k_10 = 0u; k_10 < 8u; k_10++) + { + uint param_30 = k_10; + uvec2 offset = chunk_offset(param_30); + vec4 param_31 = vec4(rgba[k_10]); + uint _1286 = packsRGB(param_31); + uint srgb = _1286; + float alpha_1 = clamp(abs(area[k_10]), 0.0, 1.0); + Alloc param_32 = scratch_alloc; + uint param_33 = (base_ix + 0u) + (2u * (offset.x + (offset.y * 32u))); + uint param_34 = srgb; + write_mem(param_32, param_33, param_34); + Alloc param_35 = scratch_alloc; + uint param_36 = (base_ix + 1u) + (2u * (offset.x + (offset.y * 32u))); + uint param_37 = floatBitsToUint(alpha_1); + write_mem(param_35, param_36, param_37); + rgba[k_10] = vec4(0.0); + } + clip_depth++; + cmd_ref.offset += 4u; + break; + } + case 8u: + { + clip_depth--; + base_ix = (scratch_alloc.offset >> uint(2)) + (2u * ((((clip_depth * 32u) * 32u) + gl_LocalInvocationID.x) + (32u * gl_LocalInvocationID.y))); + for (uint k_11 = 0u; k_11 < 8u; k_11++) + { + uint param_38 = k_11; + uvec2 offset_1 = chunk_offset(param_38); + Alloc param_39 = scratch_alloc; + uint param_40 = (base_ix + 0u) + (2u * (offset_1.x + (offset_1.y * 32u))); + uint srgb_1 = read_mem(param_39, param_40); + Alloc param_41 = scratch_alloc; + uint param_42 = (base_ix + 1u) + (2u * (offset_1.x + (offset_1.y * 32u))); + uint alpha_2 = read_mem(param_41, param_42); + uint param_43 = srgb_1; + vec4 bg = unpacksRGB(param_43); + vec4 fg_1 = (rgba[k_11] * area[k_11]) * uintBitsToFloat(alpha_2); + rgba[k_11] = (bg * (1.0 - fg_1.w)) + fg_1; + } + cmd_ref.offset += 4u; + break; + } + case 9u: + { + Alloc param_44 = cmd_alloc; + CmdRef param_45 = cmd_ref; + cmd_ref = CmdRef(Cmd_Jump_read(param_44, param_45).new_ref); + cmd_alloc.offset = cmd_ref.offset; + break; + } + } + } + for (uint i_1 = 0u; i_1 < 8u; i_1++) + { + uint param_46 = i_1; + vec3 param_47 = rgba[i_1].xyz; + imageStore(image, ivec2(xy_uint + chunk_offset(param_46)), vec4(tosRGB(param_47), rgba[i_1].w)); + } +} + +`, + } + shader_material_frag = driver.ShaderSources{ + Name: "material.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +varying vec2 vUV; + +vec3 RGBtosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texture2D(tex, vUV); + vec3 param = texel.xyz; + vec3 _59 = RGBtosRGB(param); + texel = vec4(_59.x, _59.y, _59.z, texel.w); + gl_FragData[0] = texel; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +in vec2 vUV; +layout(location = 0) out vec4 fragColor; + +vec3 RGBtosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texture(tex, vUV); + vec3 param = texel.xyz; + vec3 _59 = RGBtosRGB(param); + texel = vec4(_59.x, _59.y, _59.z, texel.w); + fragColor = texel; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +in vec2 vUV; +out vec4 fragColor; + +vec3 RGBtosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texture(tex, vUV); + vec3 param = texel.xyz; + vec3 _59 = RGBtosRGB(param); + texel = vec4(_59.x, _59.y, _59.z, texel.w); + fragColor = texel; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +in vec2 vUV; +out vec4 fragColor; + +vec3 RGBtosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texture(tex, vUV); + vec3 param = texel.xyz; + vec3 _59 = RGBtosRGB(param); + texel = vec4(_59.x, _59.y, _59.z, texel.w); + fragColor = texel; +} + +`, + HLSL: "DXBC\x9e\x87LD\xf3\x17\n\x06\\\xb7\x98\x94\xa9PKe\x01\x00\x00\x00\xc8\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00\xbc\x01\x00\x00D\x03\x00\x00\xc0\x03\x00\x00`\x04\x00\x00\x94\x04\x00\x00Aon9|\x01\x00\x00|\x01\x00\x00\x00\x02\xff\xffT\x01\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0=\n\x87?\xaeGa\xbd\x00\x00\x00\x00\x00\x00\x00\x00Q\x00\x00\x05\x01\x00\x0f\xa0\x1c.M\xbbR\xb8NAvT\xd5>\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x0f\x00\x00\x02\x01\x00\x01\x80\x00\x00\x00\x80\x0f\x00\x00\x02\x01\x00\x02\x80\x00\x00U\x80\x0f\x00\x00\x02\x01\x00\x04\x80\x00\x00\xaa\x80\x05\x00\x00\x03\x01\x00\a\x80\x01\x00\xe4\x80\x01\x00\xaa\xa0\x0e\x00\x00\x02\x02\x00\x01\x80\x01\x00\x00\x80\x0e\x00\x00\x02\x02\x00\x02\x80\x01\x00U\x80\x0e\x00\x00\x02\x02\x00\x04\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\a\x80\x02\x00\xe4\x80\x00\x00\x00\xa0\x00\x00U\xa0\x02\x00\x00\x03\x01\x00\b\x80\x00\x00\x00\x80\x01\x00\x00\xa0\x05\x00\x00\x03\x02\x00\a\x80\x00\x00\xe4\x80\x01\x00U\xa0X\x00\x00\x04\x00\x00\x01\x80\x01\x00\xff\x80\x01\x00\x00\x80\x02\x00\x00\x80\x02\x00\x00\x03\x01\x00\x01\x80\x00\x00U\x80\x01\x00\x00\xa0X\x00\x00\x04\x00\x00\x02\x80\x01\x00\x00\x80\x01\x00U\x80\x02\x00U\x80\x02\x00\x00\x03\x01\x00\x01\x80\x00\x00\xaa\x80\x01\x00\x00\xa0X\x00\x00\x04\x00\x00\x04\x80\x01\x00\x00\x80\x01\x00\xaa\x80\x02\x00\xaa\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\x80\x01\x00\x00@\x00\x00\x00`\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x00/\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00vT\xd5>vT\xd5>vT\xd5>\x00\x00\x00\x00\x19\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x002\x00\x00\x0fr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00=\n\x87?=\n\x87?=\n\x87?\x00\x00\x00\x00\x02@\x00\x00\xaeGa\xbd\xaeGa\xbd\xaeGa\xbd\x00\x00\x00\x00\x1d\x00\x00\nr\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x1c.M;\x1c.M;\x1c.M;\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x00\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00R\xb8NAR\xb8NAR\xb8NA\x00\x00\x00\x006\x00\x00\x05\x82 \x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x007\x00\x00\tr \x10\x00\x00\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\n\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00m\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00i\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + } + shader_material_vert = driver.ShaderSources{ + Name: "material.vert", + Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + GLSL100ES: `#version 100 + +varying vec2 vUV; +attribute vec2 uv; +attribute vec2 pos; + +void main() +{ + vUV = uv; + gl_Position = vec4(pos, 0.0, 1.0); +} + +`, + GLSL300ES: `#version 300 es + +out vec2 vUV; +layout(location = 1) in vec2 uv; +layout(location = 0) in vec2 pos; + +void main() +{ + vUV = uv; + gl_Position = vec4(pos, 0.0, 1.0); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +out vec2 vUV; +in vec2 uv; +in vec2 pos; + +void main() +{ + vUV = uv; + gl_Position = vec4(pos, 0.0, 1.0); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +out vec2 vUV; +in vec2 uv; +in vec2 pos; + +void main() +{ + vUV = uv; + gl_Position = vec4(pos, 0.0, 1.0); +} + +`, + HLSL: "DXBCg\xc0\xae\x16\xd8\xe1\xbdl~ń\xf1\xc4\xf6dV\x01\x00\x00\x00\xc4\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\xc8\x00\x00\x00X\x01\x00\x00\xd4\x01\x00\x00 \x02\x00\x00l\x02\x00\x00Aon9\x88\x00\x00\x00\x88\x00\x00\x00\x00\x02\xfe\xff`\x00\x00\x00(\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x01\x00$\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x01\x00\x0f\xa0\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x90\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\x03\xe0\x01\x00\xe4\x90\x01\x00\x00\x02\x00\x00\f\xc0\x01\x00D\xa0\xff\xff\x00\x00SHDR\x88\x00\x00\x00@\x00\x01\x00\"\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x006\x00\x00\x052 \x10\x00\x00\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x052 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x01\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_path_coarse_comp = driver.ShaderSources{ + Name: "path_coarse.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct MallocResult +{ + Alloc alloc; + bool failed; +}; + +struct PathCubicRef +{ + uint offset; +}; + +struct PathCubic +{ + vec2 p0; + vec2 p1; + vec2 p2; + vec2 p3; + uint path_ix; + uint trans_ix; + vec2 stroke; +}; + +struct PathSegRef +{ + uint offset; +}; + +struct PathSegTag +{ + uint tag; + uint flags; +}; + +struct TileRef +{ + uint offset; +}; + +struct PathRef +{ + uint offset; +}; + +struct Path +{ + uvec4 bbox; + TileRef tiles; +}; + +struct TileSegRef +{ + uint offset; +}; + +struct TileSeg +{ + vec2 origin; + vec2 vector; + float y_edge; + TileSegRef next; +}; + +struct TransformSegRef +{ + uint offset; +}; + +struct TransformSeg +{ + vec4 mat; + vec2 translate; +}; + +struct SubdivResult +{ + float val; + float a0; + float a2; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _149; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _788; + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _149.memory[offset]; + return v; +} + +PathSegTag PathSeg_tag(Alloc a, PathSegRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return PathSegTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +PathCubic PathCubic_read(Alloc a, PathCubicRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 6u; + uint raw6 = read_mem(param_12, param_13); + Alloc param_14 = a; + uint param_15 = ix + 7u; + uint raw7 = read_mem(param_14, param_15); + Alloc param_16 = a; + uint param_17 = ix + 8u; + uint raw8 = read_mem(param_16, param_17); + Alloc param_18 = a; + uint param_19 = ix + 9u; + uint raw9 = read_mem(param_18, param_19); + Alloc param_20 = a; + uint param_21 = ix + 10u; + uint raw10 = read_mem(param_20, param_21); + Alloc param_22 = a; + uint param_23 = ix + 11u; + uint raw11 = read_mem(param_22, param_23); + PathCubic s; + s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + s.p3 = vec2(uintBitsToFloat(raw6), uintBitsToFloat(raw7)); + s.path_ix = raw8; + s.trans_ix = raw9; + s.stroke = vec2(uintBitsToFloat(raw10), uintBitsToFloat(raw11)); + return s; +} + +PathCubic PathSeg_Cubic_read(Alloc a, PathSegRef ref) +{ + Alloc param = a; + PathCubicRef param_1 = PathCubicRef(ref.offset + 4u); + return PathCubic_read(param, param_1); +} + +TransformSeg TransformSeg_read(Alloc a, TransformSegRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + TransformSeg s; + s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + return s; +} + +vec2 eval_cubic(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t) +{ + float mt = 1.0 - t; + return (p0 * ((mt * mt) * mt)) + (((p1 * ((mt * mt) * 3.0)) + (((p2 * (mt * 3.0)) + (p3 * t)) * t)) * t); +} + +float approx_parabola_integral(float x) +{ + return x * inversesqrt(sqrt(0.3300000131130218505859375 + (0.201511204242706298828125 + ((0.25 * x) * x)))); +} + +SubdivResult estimate_subdiv(vec2 p0, vec2 p1, vec2 p2, float sqrt_tol) +{ + vec2 d01 = p1 - p0; + vec2 d12 = p2 - p1; + vec2 dd = d01 - d12; + float _cross = ((p2.x - p0.x) * dd.y) - ((p2.y - p0.y) * dd.x); + float x0 = ((d01.x * dd.x) + (d01.y * dd.y)) / _cross; + float x2 = ((d12.x * dd.x) + (d12.y * dd.y)) / _cross; + float scale = abs(_cross / (length(dd) * (x2 - x0))); + float param = x0; + float a0 = approx_parabola_integral(param); + float param_1 = x2; + float a2 = approx_parabola_integral(param_1); + float val = 0.0; + if (scale < 1000000000.0) + { + float da = abs(a2 - a0); + float sqrt_scale = sqrt(scale); + if (sign(x0) == sign(x2)) + { + val = da * sqrt_scale; + } + else + { + float xmin = sqrt_tol / sqrt_scale; + float param_2 = xmin; + val = (sqrt_tol * da) / approx_parabola_integral(param_2); + } + } + return SubdivResult(val, a0, a2); +} + +uint fill_mode_from_flags(uint flags) +{ + return flags & 1u; +} + +Path Path_read(Alloc a, PathRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Path s; + s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16)); + s.tiles = TileRef(raw2); + return s; +} + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +float approx_parabola_inv_integral(float x) +{ + return x * sqrt(0.61000001430511474609375 + (0.1520999968051910400390625 + ((0.25 * x) * x))); +} + +vec2 eval_quad(vec2 p0, vec2 p1, vec2 p2, float t) +{ + float mt = 1.0 - t; + return (p0 * (mt * mt)) + (((p1 * (mt * 2.0)) + (p2 * t)) * t); +} + +MallocResult malloc(uint size) +{ + MallocResult r; + r.failed = false; + uint _155 = atomicAdd(_149.mem_offset, size); + uint offset = _155; + uint param = offset; + uint param_1 = size; + r.alloc = new_alloc(param, param_1); + if ((offset + size) > uint(int(uint(_149.memory.length())) * 4)) + { + r.failed = true; + uint _176 = atomicMax(_149.mem_error, 1u); + return r; + } + return r; +} + +TileRef Tile_index(TileRef ref, uint index) +{ + return TileRef(ref.offset + (index * 8u)); +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _149.memory[offset] = val; +} + +void TileSeg_write(Alloc a, TileSegRef ref, TileSeg s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.origin.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.origin.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.vector.x); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.vector.y); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.y_edge); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = s.next.offset; + write_mem(param_15, param_16, param_17); +} + +void main() +{ + if (_149.mem_error != 0u) + { + return; + } + uint element_ix = gl_GlobalInvocationID.x; + PathSegRef ref = PathSegRef(_788.conf.pathseg_alloc.offset + (element_ix * 52u)); + PathSegTag tag = PathSegTag(0u, 0u); + if (element_ix < _788.conf.n_pathseg) + { + Alloc param; + param.offset = _788.conf.pathseg_alloc.offset; + PathSegRef param_1 = ref; + tag = PathSeg_tag(param, param_1); + } + switch (tag.tag) + { + case 1u: + { + Alloc param_2; + param_2.offset = _788.conf.pathseg_alloc.offset; + PathSegRef param_3 = ref; + PathCubic cubic = PathSeg_Cubic_read(param_2, param_3); + uint trans_ix = cubic.trans_ix; + if (trans_ix > 0u) + { + TransformSegRef trans_ref = TransformSegRef(_788.conf.trans_alloc.offset + ((trans_ix - 1u) * 24u)); + Alloc param_4; + param_4.offset = _788.conf.trans_alloc.offset; + TransformSegRef param_5 = trans_ref; + TransformSeg trans = TransformSeg_read(param_4, param_5); + cubic.p0 = ((trans.mat.xy * cubic.p0.x) + (trans.mat.zw * cubic.p0.y)) + trans.translate; + cubic.p1 = ((trans.mat.xy * cubic.p1.x) + (trans.mat.zw * cubic.p1.y)) + trans.translate; + cubic.p2 = ((trans.mat.xy * cubic.p2.x) + (trans.mat.zw * cubic.p2.y)) + trans.translate; + cubic.p3 = ((trans.mat.xy * cubic.p3.x) + (trans.mat.zw * cubic.p3.y)) + trans.translate; + } + vec2 err_v = (((cubic.p2 - cubic.p1) * 3.0) + cubic.p0) - cubic.p3; + float err = (err_v.x * err_v.x) + (err_v.y * err_v.y); + uint n_quads = max(uint(ceil(pow(err * 3.7037036418914794921875, 0.16666667163372039794921875))), 1u); + float val = 0.0; + vec2 qp0 = cubic.p0; + float _step = 1.0 / float(n_quads); + for (uint i = 0u; i < n_quads; i++) + { + float t = float(i + 1u) * _step; + vec2 param_6 = cubic.p0; + vec2 param_7 = cubic.p1; + vec2 param_8 = cubic.p2; + vec2 param_9 = cubic.p3; + float param_10 = t; + vec2 qp2 = eval_cubic(param_6, param_7, param_8, param_9, param_10); + vec2 param_11 = cubic.p0; + vec2 param_12 = cubic.p1; + vec2 param_13 = cubic.p2; + vec2 param_14 = cubic.p3; + float param_15 = t - (0.5 * _step); + vec2 qp1 = eval_cubic(param_11, param_12, param_13, param_14, param_15); + qp1 = (qp1 * 2.0) - ((qp0 + qp2) * 0.5); + vec2 param_16 = qp0; + vec2 param_17 = qp1; + vec2 param_18 = qp2; + float param_19 = 0.4743416607379913330078125; + SubdivResult params = estimate_subdiv(param_16, param_17, param_18, param_19); + val += params.val; + qp0 = qp2; + } + uint n = max(uint(ceil((val * 0.5) / 0.4743416607379913330078125)), 1u); + uint param_20 = tag.flags; + bool is_stroke = fill_mode_from_flags(param_20) == 1u; + uint path_ix = cubic.path_ix; + Alloc param_21; + param_21.offset = _788.conf.tile_alloc.offset; + PathRef param_22 = PathRef(_788.conf.tile_alloc.offset + (path_ix * 12u)); + Path path = Path_read(param_21, param_22); + uint param_23 = path.tiles.offset; + uint param_24 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u; + Alloc path_alloc = new_alloc(param_23, param_24); + ivec4 bbox = ivec4(path.bbox); + vec2 p0 = cubic.p0; + qp0 = cubic.p0; + float v_step = val / float(n); + int n_out = 1; + float val_sum = 0.0; + vec2 p1; + float _1309; + TileSeg tile_seg; + for (uint i_1 = 0u; i_1 < n_quads; i_1++) + { + float t_1 = float(i_1 + 1u) * _step; + vec2 param_25 = cubic.p0; + vec2 param_26 = cubic.p1; + vec2 param_27 = cubic.p2; + vec2 param_28 = cubic.p3; + float param_29 = t_1; + vec2 qp2_1 = eval_cubic(param_25, param_26, param_27, param_28, param_29); + vec2 param_30 = cubic.p0; + vec2 param_31 = cubic.p1; + vec2 param_32 = cubic.p2; + vec2 param_33 = cubic.p3; + float param_34 = t_1 - (0.5 * _step); + vec2 qp1_1 = eval_cubic(param_30, param_31, param_32, param_33, param_34); + qp1_1 = (qp1_1 * 2.0) - ((qp0 + qp2_1) * 0.5); + vec2 param_35 = qp0; + vec2 param_36 = qp1_1; + vec2 param_37 = qp2_1; + float param_38 = 0.4743416607379913330078125; + SubdivResult params_1 = estimate_subdiv(param_35, param_36, param_37, param_38); + float param_39 = params_1.a0; + float u0 = approx_parabola_inv_integral(param_39); + float param_40 = params_1.a2; + float u2 = approx_parabola_inv_integral(param_40); + float uscale = 1.0 / (u2 - u0); + float target = float(n_out) * v_step; + for (;;) + { + bool _1202 = uint(n_out) == n; + bool _1212; + if (!_1202) + { + _1212 = target < (val_sum + params_1.val); + } + else + { + _1212 = _1202; + } + if (_1212) + { + if (uint(n_out) == n) + { + p1 = cubic.p3; + } + else + { + float u = (target - val_sum) / params_1.val; + float a = mix(params_1.a0, params_1.a2, u); + float param_41 = a; + float au = approx_parabola_inv_integral(param_41); + float t_2 = (au - u0) * uscale; + vec2 param_42 = qp0; + vec2 param_43 = qp1_1; + vec2 param_44 = qp2_1; + float param_45 = t_2; + p1 = eval_quad(param_42, param_43, param_44, param_45); + } + float xmin = min(p0.x, p1.x) - cubic.stroke.x; + float xmax = max(p0.x, p1.x) + cubic.stroke.x; + float ymin = min(p0.y, p1.y) - cubic.stroke.y; + float ymax = max(p0.y, p1.y) + cubic.stroke.y; + float dx = p1.x - p0.x; + float dy = p1.y - p0.y; + if (abs(dy) < 9.999999717180685365747194737196e-10) + { + _1309 = 1000000000.0; + } + else + { + _1309 = dx / dy; + } + float invslope = _1309; + float c = (cubic.stroke.x + (abs(invslope) * (16.0 + cubic.stroke.y))) * 0.03125; + float b = invslope; + float a_1 = (p0.x - ((p0.y - 16.0) * b)) * 0.03125; + int x0 = int(floor(xmin * 0.03125)); + int x1 = int(floor(xmax * 0.03125) + 1.0); + int y0 = int(floor(ymin * 0.03125)); + int y1 = int(floor(ymax * 0.03125) + 1.0); + x0 = clamp(x0, bbox.x, bbox.z); + y0 = clamp(y0, bbox.y, bbox.w); + x1 = clamp(x1, bbox.x, bbox.z); + y1 = clamp(y1, bbox.y, bbox.w); + float xc = a_1 + (b * float(y0)); + int stride = bbox.z - bbox.x; + int base = ((y0 - bbox.y) * stride) - bbox.x; + uint n_tile_alloc = uint((x1 - x0) * (y1 - y0)); + uint param_46 = n_tile_alloc * 24u; + MallocResult _1424 = malloc(param_46); + MallocResult tile_alloc = _1424; + if (tile_alloc.failed) + { + return; + } + uint tile_offset = tile_alloc.alloc.offset; + int xray = int(floor(p0.x * 0.03125)); + int last_xray = int(floor(p1.x * 0.03125)); + if (p0.y > p1.y) + { + int tmp = xray; + xray = last_xray; + last_xray = tmp; + } + for (int y = y0; y < y1; y++) + { + float tile_y0 = float(y * 32); + int xbackdrop = max((xray + 1), bbox.x); + bool _1478 = !is_stroke; + bool _1488; + if (_1478) + { + _1488 = min(p0.y, p1.y) < tile_y0; + } + else + { + _1488 = _1478; + } + bool _1495; + if (_1488) + { + _1495 = xbackdrop < bbox.z; + } + else + { + _1495 = _1488; + } + if (_1495) + { + int backdrop = (p1.y < p0.y) ? 1 : (-1); + TileRef param_47 = path.tiles; + uint param_48 = uint(base + xbackdrop); + TileRef tile_ref = Tile_index(param_47, param_48); + uint tile_el = tile_ref.offset >> uint(2); + Alloc param_49 = path_alloc; + uint param_50 = tile_el + 1u; + if (touch_mem(param_49, param_50)) + { + uint _1533 = atomicAdd(_149.memory[tile_el + 1u], uint(backdrop)); + } + } + int next_xray = last_xray; + if (y < (y1 - 1)) + { + float tile_y1 = float((y + 1) * 32); + float x_edge = mix(p0.x, p1.x, (tile_y1 - p0.y) / dy); + next_xray = int(floor(x_edge * 0.03125)); + } + int min_xray = min(xray, next_xray); + int max_xray = max(xray, next_xray); + int xx0 = min(int(floor(xc - c)), min_xray); + int xx1 = max(int(ceil(xc + c)), (max_xray + 1)); + xx0 = clamp(xx0, x0, x1); + xx1 = clamp(xx1, x0, x1); + for (int x = xx0; x < xx1; x++) + { + float tile_x0 = float(x * 32); + TileRef param_51 = TileRef(path.tiles.offset); + uint param_52 = uint(base + x); + TileRef tile_ref_1 = Tile_index(param_51, param_52); + uint tile_el_1 = tile_ref_1.offset >> uint(2); + uint old = 0u; + Alloc param_53 = path_alloc; + uint param_54 = tile_el_1; + if (touch_mem(param_53, param_54)) + { + uint _1636 = atomicExchange(_149.memory[tile_el_1], tile_offset); + old = _1636; + } + tile_seg.origin = p0; + tile_seg.vector = p1 - p0; + float y_edge = 0.0; + if (!is_stroke) + { + y_edge = mix(p0.y, p1.y, (tile_x0 - p0.x) / dx); + if (min(p0.x, p1.x) < tile_x0) + { + vec2 p = vec2(tile_x0, y_edge); + if (p0.x > p1.x) + { + tile_seg.vector = p - p0; + } + else + { + tile_seg.origin = p; + tile_seg.vector = p1 - p; + } + if (tile_seg.vector.x == 0.0) + { + tile_seg.vector.x = sign(p1.x - p0.x) * 9.999999717180685365747194737196e-10; + } + } + if ((x <= min_xray) || (max_xray < x)) + { + y_edge = 1000000000.0; + } + } + tile_seg.y_edge = y_edge; + tile_seg.next.offset = old; + Alloc param_55 = tile_alloc.alloc; + TileSegRef param_56 = TileSegRef(tile_offset); + TileSeg param_57 = tile_seg; + TileSeg_write(param_55, param_56, param_57); + tile_offset += 24u; + } + xc += b; + base += stride; + xray = next_xray; + } + n_out++; + target += v_step; + p0 = p1; + continue; + } + else + { + break; + } + } + val_sum += params_1.val; + qp0 = qp2_1; + } + break; + } + } +} + +`, + } + shader_stencil_frag = driver.ShaderSources{ + Name: "stencil.frag", + Inputs: []driver.InputLocation{{Name: "vFrom", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vCtrl", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}, {Name: "vTo", Location: 2, Semantic: "TEXCOORD", SemanticIndex: 2, Type: 0x0, Size: 2}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +varying vec2 vTo; +varying vec2 vFrom; +varying vec2 vCtrl; + +void main() +{ + float dx = vTo.x - vFrom.x; + bool increasing = vTo.x >= vFrom.x; + bvec2 _35 = bvec2(increasing); + vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y); + bvec2 _41 = bvec2(increasing); + vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y); + vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5)); + float midx = mix(extent.x, extent.y, 0.5); + float x0 = midx - left.x; + vec2 p1 = vCtrl - left; + vec2 v = right - vCtrl; + float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0))); + float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t); + vec2 d_half = mix(p1, v, vec2(t)); + float dy = d_half.y / d_half.x; + float width = extent.y - extent.x; + dy = abs(dy * width); + vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy); + sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0)); + float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w)); + area *= width; + if (width == 0.0) + { + area = 0.0; + } + gl_FragData[0].x = area; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +in vec2 vTo; +in vec2 vFrom; +in vec2 vCtrl; +layout(location = 0) out vec4 fragCover; + +void main() +{ + float dx = vTo.x - vFrom.x; + bool increasing = vTo.x >= vFrom.x; + bvec2 _35 = bvec2(increasing); + vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y); + bvec2 _41 = bvec2(increasing); + vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y); + vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5)); + float midx = mix(extent.x, extent.y, 0.5); + float x0 = midx - left.x; + vec2 p1 = vCtrl - left; + vec2 v = right - vCtrl; + float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0))); + float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t); + vec2 d_half = mix(p1, v, vec2(t)); + float dy = d_half.y / d_half.x; + float width = extent.y - extent.x; + dy = abs(dy * width); + vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy); + sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0)); + float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w)); + area *= width; + if (width == 0.0) + { + area = 0.0; + } + fragCover.x = area; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +in vec2 vTo; +in vec2 vFrom; +in vec2 vCtrl; +out vec4 fragCover; + +void main() +{ + float dx = vTo.x - vFrom.x; + bool increasing = vTo.x >= vFrom.x; + bvec2 _35 = bvec2(increasing); + vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y); + bvec2 _41 = bvec2(increasing); + vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y); + vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5)); + float midx = mix(extent.x, extent.y, 0.5); + float x0 = midx - left.x; + vec2 p1 = vCtrl - left; + vec2 v = right - vCtrl; + float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0))); + float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t); + vec2 d_half = mix(p1, v, vec2(t)); + float dy = d_half.y / d_half.x; + float width = extent.y - extent.x; + dy = abs(dy * width); + vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy); + sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0)); + float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w)); + area *= width; + if (width == 0.0) + { + area = 0.0; + } + fragCover.x = area; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +in vec2 vTo; +in vec2 vFrom; +in vec2 vCtrl; +out vec4 fragCover; + +void main() +{ + float dx = vTo.x - vFrom.x; + bool increasing = vTo.x >= vFrom.x; + bvec2 _35 = bvec2(increasing); + vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y); + bvec2 _41 = bvec2(increasing); + vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y); + vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5)); + float midx = mix(extent.x, extent.y, 0.5); + float x0 = midx - left.x; + vec2 p1 = vCtrl - left; + vec2 v = right - vCtrl; + float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0))); + float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t); + vec2 d_half = mix(p1, v, vec2(t)); + float dy = d_half.y / d_half.x; + float width = extent.y - extent.x; + dy = abs(dy * width); + vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy); + sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0)); + float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w)); + area *= width; + if (width == 0.0) + { + area = 0.0; + } + fragCover.x = area; +} + +`, + HLSL: "DXBC\x94!\xb9\x13L\xba\r\x11\x8f\xc7\xce\x0eAs\xec\xe1\x01\x00\x00\x00\\\n\x00\x00\x06\x00\x00\x008\x00\x00\x00\x9c\x03\x00\x00\xfc\b\x00\x00x\t\x00\x00\xc4\t\x00\x00(\n\x00\x00Aon9\\\x03\x00\x00\\\x03\x00\x00\x00\x02\xff\xff8\x03\x00\x00$\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x00\xbf\x00\x00\x00?\x00\x00\x80?\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x80\x01\x00\x03\xb0\v\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\xb0\x00\x00\x00\xa0\v\x00\x00\x03\x00\x00\x02\x80\x01\x00\x00\xb0\x00\x00\x00\xa0\n\x00\x00\x03\x01\x00\x03\x80\x00\x00\xe4\x80\x00\x00U\xa0\x02\x00\x00\x03\x00\x00\x01\x80\x01\x00\x00\x81\x01\x00U\x80\x04\x00\x00\x04\x00\x00\x02\x80\x00\x00\x00\x80\x00\x00U\xa0\x01\x00\x00\x80\x01\x00\x00\x02\x01\x00\x03\x80\x00\x00\xe4\xb0\n\x00\x00\x03\x02\x00\x01\x80\x01\x00\x00\x80\x01\x00\x00\xb0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x02\x00\x00\x81\v\x00\x00\x03\x03\x00\x01\x80\x01\x00\x00\xb0\x01\x00\x00\x80\x02\x00\x00\x03\x00\x00\x04\x80\x01\x00\x00\x81\x01\x00\x00\xb0X\x00\x00\x04\x03\x00\x02\x80\x00\x00\xaa\x80\x01\x00U\xb0\x01\x00U\x80X\x00\x00\x04\x02\x00\x02\x80\x00\x00\xaa\x80\x01\x00U\x80\x01\x00U\xb0\x02\x00\x00\x03\x00\x00\f\x80\x03\x00\x1b\x80\x00\x00\xe4\xb1\x02\x00\x00\x03\x01\x00\x03\x80\x02\x00\xe4\x81\x00\x00\x1b\xb0\x02\x00\x00\x03\x01\x00\x04\x80\x00\x00\xff\x80\x01\x00\x00\x81\x05\x00\x00\x03\x01\x00\x04\x80\x00\x00U\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x04\x80\x01\x00\x00\x80\x01\x00\x00\x80\x01\x00\xaa\x80\a\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x06\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x02\x00\x00\x03\x01\x00\x04\x80\x01\x00\xaa\x80\x01\x00\x00\x80\x06\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x04\x80\x00\x00U\x80\x01\x00U\x80\x02\x00U\x80\x12\x00\x00\x04\x02\x00\x03\x80\x00\x00U\x80\x00\x00\x1b\x80\x01\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x00\x00\xaa\xb0\x12\x00\x00\x04\x02\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x01\x00\xaa\x80\x06\x00\x00\x02\x00\x00\x02\x80\x02\x00\x00\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x02\x00U\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00\x00\x80\x00\x00U\x80#\x00\x00\x02\x00\x00\x02\x80\x00\x00U\x80\x04\x00\x00\x04\x01\x00\x01\x80\x00\x00U\x80\x00\x00U\xa0\x02\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x02\x80\x00\x00U\x80\x00\x00\x00\xa0\x02\x00\xaa\x80\x06\x00\x00\x02\x00\x00\x02\x80\x00\x00U\x80\x02\x00\x00\x03\x00\x00\f\x80\x02\x00\xaa\x81\x00\x00\x1b\xa0\x05\x00\x00\x03\x01\x00\b\x80\x00\x00U\x80\x00\x00\xff\x80\x05\x00\x00\x03\x01\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x02\x00\x00\x03\x01\x00\x1f\x80\x01\x00\xe4\x80\x00\x00U\xa0\x04\x00\x00\x04\x00\x00\x02\x80\x01\x00\xaa\x80\x01\x00U\x81\x01\x00\xaa\x80\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\xaa\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x01\x00\x00\x81\x00\x00U\x80\x04\x00\x00\x04\x00\x00\x02\x80\x01\x00\x00\x80\x01\x00\xff\x80\x00\x00U\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00U\xa0X\x00\x00\x04\x00\x00\x01\x80\x00\x00\x00\x81\x00\x00\xff\xa0\x00\x00U\x80\x01\x00\x00\x02\x00\x00\x0e\x80\x00\x00\xff\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDRX\x05\x00\x00@\x00\x00\x00V\x01\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03\xc2\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x006\x00\x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x006\x00\x00\x05\"\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x004\x00\x00\n2\x00\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x003\x00\x00\n2\x00\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\"\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\n\x00\x10\x00\x00\x00\x00\x003\x00\x00\a2\x00\x10\x00\x01\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x01\x00\x00\x00\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x004\x00\x00\a2\x00\x10\x00\x02\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x01\x00\x00\x00\x1d\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x007\x00\x00\tB\x00\x10\x00\x02\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x01\x00\x00\x00\x1a\x10\x10\x00\x00\x00\x00\x007\x00\x00\tB\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x01\x00\x00\x00\x00\x00\x00\br\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00\xa6\x1b\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x00\x00\x00\x00V\t\x10\x80A\x00\x00\x00\x01\x00\x00\x00\xa6\x1e\x10\x00\x00\x00\x00\x00\x00\x00\x00\b\xb2\x00\x10\x00\x01\x00\x00\x00\xa6\x0e\x10\x80A\x00\x00\x00\x00\x00\x00\x00F\b\x10\x00\x02\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00K\x00\x00\x05\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\a\x12\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x0e\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\xc2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00V\r\x10\x00\x01\x00\x00\x00\xa6\x0e\x10\x00\x00\x00\x00\x00\x0e\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x008\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x82\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x02\x00\x00\x00:\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\b\x82\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\v2\x00\x10\x00\x01\x00\x00\x00\x06\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\r2\x00\x10\x00\x02\x00\x00\x00\xa6\n\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00\x0e\x00\x00\b\xc2\x00\x10\x00\x02\x00\x00\x00\x06\x04\x10\x00\x01\x00\x00\x00\xa6\n\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x00 \x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x0e\x10\x00\x02\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00?\x00\x00\x00?\x00\x00\x00?2\x00\x00\n\x12\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00\x1a\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00:\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x18\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00?7\x00\x00\t\x12 \x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x006\x00\x00\b\xe2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00)\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\\\x00\x00\x00\x03\x00\x00\x00\b\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00P\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\f\x00\x00P\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + } + shader_stencil_vert = driver.ShaderSources{ + Name: "stencil.vert", + Inputs: []driver.InputLocation{{Name: "corner", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 1}, {Name: "maxy", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 1}, {Name: "from", Location: 2, Semantic: "TEXCOORD", SemanticIndex: 2, Type: 0x0, Size: 2}, {Name: "ctrl", Location: 3, Semantic: "TEXCOORD", SemanticIndex: 3, Type: 0x0, Size: 2}, {Name: "to", Location: 4, Semantic: "TEXCOORD", SemanticIndex: 4, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.pathOffset", Type: 0x0, Size: 2, Offset: 16}}, + Size: 24, + }, + GLSL100ES: `#version 100 + +struct Block +{ + vec4 transform; + vec2 pathOffset; +}; + +uniform Block _block; + +attribute vec2 from; +attribute vec2 ctrl; +attribute vec2 to; +attribute float maxy; +attribute float corner; +varying vec2 vFrom; +varying vec2 vCtrl; +varying vec2 vTo; + +void main() +{ + vec2 from_1 = from + _block.pathOffset; + vec2 ctrl_1 = ctrl + _block.pathOffset; + vec2 to_1 = to + _block.pathOffset; + float maxy_1 = maxy + _block.pathOffset.y; + float c = corner; + vec2 pos; + if (c >= 0.375) + { + c -= 0.5; + pos.y = maxy_1 + 1.0; + } + else + { + pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0; + } + if (c >= 0.125) + { + pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0; + } + else + { + pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0; + } + vFrom = from_1 - pos; + vCtrl = ctrl_1 - pos; + vTo = to_1 - pos; + pos = (pos * _block.transform.xy) + _block.transform.zw; + gl_Position = vec4(pos, 1.0, 1.0); +} + +`, + GLSL300ES: `#version 300 es + +layout(std140) uniform Block +{ + vec4 transform; + vec2 pathOffset; +} _block; + +layout(location = 2) in vec2 from; +layout(location = 3) in vec2 ctrl; +layout(location = 4) in vec2 to; +layout(location = 1) in float maxy; +layout(location = 0) in float corner; +out vec2 vFrom; +out vec2 vCtrl; +out vec2 vTo; + +void main() +{ + vec2 from_1 = from + _block.pathOffset; + vec2 ctrl_1 = ctrl + _block.pathOffset; + vec2 to_1 = to + _block.pathOffset; + float maxy_1 = maxy + _block.pathOffset.y; + float c = corner; + vec2 pos; + if (c >= 0.375) + { + c -= 0.5; + pos.y = maxy_1 + 1.0; + } + else + { + pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0; + } + if (c >= 0.125) + { + pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0; + } + else + { + pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0; + } + vFrom = from_1 - pos; + vCtrl = ctrl_1 - pos; + vTo = to_1 - pos; + pos = (pos * _block.transform.xy) + _block.transform.zw; + gl_Position = vec4(pos, 1.0, 1.0); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Block +{ + vec4 transform; + vec2 pathOffset; +}; + +uniform Block _block; + +in vec2 from; +in vec2 ctrl; +in vec2 to; +in float maxy; +in float corner; +out vec2 vFrom; +out vec2 vCtrl; +out vec2 vTo; + +void main() +{ + vec2 from_1 = from + _block.pathOffset; + vec2 ctrl_1 = ctrl + _block.pathOffset; + vec2 to_1 = to + _block.pathOffset; + float maxy_1 = maxy + _block.pathOffset.y; + float c = corner; + vec2 pos; + if (c >= 0.375) + { + c -= 0.5; + pos.y = maxy_1 + 1.0; + } + else + { + pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0; + } + if (c >= 0.125) + { + pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0; + } + else + { + pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0; + } + vFrom = from_1 - pos; + vCtrl = ctrl_1 - pos; + vTo = to_1 - pos; + pos = (pos * _block.transform.xy) + _block.transform.zw; + gl_Position = vec4(pos, 1.0, 1.0); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Block +{ + vec4 transform; + vec2 pathOffset; +} _block; + +in vec2 from; +in vec2 ctrl; +in vec2 to; +in float maxy; +in float corner; +out vec2 vFrom; +out vec2 vCtrl; +out vec2 vTo; + +void main() +{ + vec2 from_1 = from + _block.pathOffset; + vec2 ctrl_1 = ctrl + _block.pathOffset; + vec2 to_1 = to + _block.pathOffset; + float maxy_1 = maxy + _block.pathOffset.y; + float c = corner; + vec2 pos; + if (c >= 0.375) + { + c -= 0.5; + pos.y = maxy_1 + 1.0; + } + else + { + pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0; + } + if (c >= 0.125) + { + pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0; + } + else + { + pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0; + } + vFrom = from_1 - pos; + vCtrl = ctrl_1 - pos; + vTo = to_1 - pos; + pos = (pos * _block.transform.xy) + _block.transform.zw; + gl_Position = vec4(pos, 1.0, 1.0); +} + +`, + HLSL: "DXBC\xa5!\xd8\x10\xb4n\x90\xe3\xd9U\xdb\xe2\xb6~I0\x01\x00\x00\x00\x10\b\x00\x00\x06\x00\x00\x008\x00\x00\x00L\x02\x00\x00t\x05\x00\x00\xf0\x05\x00\x00\xf4\x06\x00\x00\x88\a\x00\x00Aon9\f\x02\x00\x00\f\x02\x00\x00\x00\x02\xfe\xff\xd8\x01\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x03\x00\x0f\xa0\x00\x00\xc0>\x00\x00\x80?\x00\x00\x80\xbf\x00\x00\x00\xbfQ\x00\x00\x05\x04\x00\x0f\xa0\x00\x00\x00>\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x02\x80\x02\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x03\x80\x03\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x04\x80\x04\x00\x0f\x90\x02\x00\x00\x03\x00\x00\x01\x80\x01\x00\x00\x90\x02\x00U\xa0\x02\x00\x00\x03\x00\x00\x04\x80\x00\x00\x00\x80\x03\x00U\xa0\r\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\x90\x03\x00\x00\xa0\x01\x00\x00\x02\x01\x00\x04\x80\x00\x00\x00\x90\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00\x00\x90\x03\x00\xff\xa0\x02\x00\x00\x03\x02\x00\x03\x80\x02\x00\xe4\x90\x02\x00\xe4\xa0\x02\x00\x00\x03\x02\x00\f\x80\x03\x00\x14\x90\x02\x00\x14\xa0\n\x00\x00\x03\x03\x00\x03\x80\x02\x00\xee\x80\x02\x00\xe1\x80\x02\x00\x00\x03\x03\x00\f\x80\x04\x00D\x90\x02\x00D\xa0\n\x00\x00\x03\x03\x00\x03\x80\x03\x00\xeb\x80\x03\x00\xe4\x80\x02\x00\x00\x03\x01\x00\x03\x80\x03\x00\xe4\x80\x03\x00\xaa\xa0\x12\x00\x00\x04\x04\x00\x06\x80\x00\x00\x00\x80\x00\x00\xe4\x80\x01\x00Ȁ\r\x00\x00\x03\x00\x00\x01\x80\x04\x00U\x80\x04\x00\x00\xa0\v\x00\x00\x03\x00\x00\x02\x80\x02\x00\xff\x80\x02\x00\x00\x80\v\x00\x00\x03\x00\x00\x02\x80\x03\x00\xaa\x80\x00\x00U\x80\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x03\x00U\xa0\x12\x00\x00\x04\x04\x00\x01\x80\x00\x00\x00\x80\x00\x00U\x80\x01\x00U\x80\x02\x00\x00\x03\x00\x00\x0f\xe0\x02\x00\xe4\x80\x04\x00(\x81\x02\x00\x00\x03\x01\x00\x03\xe0\x03\x00\xee\x80\x04\x00\xe8\x81\x04\x00\x00\x04\x00\x00\x03\x80\x04\x00\xe8\x80\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\f\xc0\x03\x00U\xa0\xff\xff\x00\x00SHDR \x03\x00\x00@\x00\x01\x00\xc8\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00_\x00\x00\x03\x12\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x03\x12\x10\x10\x00\x01\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x02\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x03\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x04\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00e\x00\x00\x03\xc2 \x10\x00\x00\x00\x00\x00e\x00\x00\x032 \x10\x00\x01\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x02\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x04\x00\x00\x00\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x00\x1a\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x1d\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\xc0>\x00\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\xbf6\x00\x00\x05B\x00\x10\x00\x01\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\b2\x00\x10\x00\x02\x00\x00\x00F\x10\x10\x00\x02\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x02\x00\x00\x00\x06\x14\x10\x00\x03\x00\x00\x00\x06\x84 \x00\x00\x00\x00\x00\x01\x00\x00\x003\x00\x00\a2\x00\x10\x00\x03\x00\x00\x00\xb6\x0f\x10\x00\x02\x00\x00\x00\x16\x05\x10\x00\x02\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x03\x00\x00\x00\x06\x14\x10\x00\x04\x00\x00\x00\x06\x84 \x00\x00\x00\x00\x00\x01\x00\x00\x003\x00\x00\a2\x00\x10\x00\x03\x00\x00\x00\xb6\x0f\x10\x00\x03\x00\x00\x00F\x00\x10\x00\x03\x00\x00\x00\x00\x00\x00\n2\x00\x10\x00\x01\x00\x00\x00F\x00\x10\x00\x03\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80\xbf\x00\x00\x00\x00\x00\x00\x00\x007\x00\x00\tb\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00V\x06\x10\x00\x00\x00\x00\x00\xa6\b\x10\x00\x01\x00\x00\x00\x1d\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00>4\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x02\x00\x00\x00\n\x00\x10\x00\x02\x00\x00\x004\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x03\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?7\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x86\b\x10\x80A\x00\x00\x00\x00\x00\x00\x00F\x0e\x10\x00\x02\x00\x00\x00\x00\x00\x00\b2 \x10\x00\x01\x00\x00\x00\x86\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\xe6\n\x10\x00\x03\x00\x00\x002\x00\x00\v2 \x10\x00\x02\x00\x00\x00\x86\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x02\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x16\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xfc\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\xd4\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x02\x00\x00\x00\\\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\b\x00\x00\x00\x02\x00\x00\x00\xc4\x00\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_pathOffset\x00\xab\xab\x01\x00\x03\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\x8c\x00\x00\x00\x05\x00\x00\x00\b\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x80\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x01\x01\x00\x00\x80\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x03\x03\x00\x00\x80\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x03\x00\x00\x00\x03\x03\x00\x00\x80\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN\x80\x00\x00\x00\x04\x00\x00\x00\b\x00\x00\x00h\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00h\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x03\x00\x00h\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\f\x00\x00q\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_tile_alloc_comp = driver.ShaderSources{ + Name: "tile_alloc.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct MallocResult +{ + Alloc alloc; + bool failed; +}; + +struct AnnoEndClipRef +{ + uint offset; +}; + +struct AnnoEndClip +{ + vec4 bbox; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct AnnotatedTag +{ + uint tag; + uint flags; +}; + +struct PathRef +{ + uint offset; +}; + +struct TileRef +{ + uint offset; +}; + +struct Path +{ + uvec4 bbox; + TileRef tiles; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _96; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _309; + +shared uint sh_tile_count[128]; +shared MallocResult sh_tile_alloc; + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _96.memory[offset]; + return v; +} + +AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +AnnoEndClip AnnoEndClip_read(Alloc a, AnnoEndClipRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + AnnoEndClip s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + return s; +} + +AnnoEndClip Annotated_EndClip_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoEndClipRef param_1 = AnnoEndClipRef(ref.offset + 4u); + return AnnoEndClip_read(param, param_1); +} + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +MallocResult malloc(uint size) +{ + MallocResult r; + r.failed = false; + uint _102 = atomicAdd(_96.mem_offset, size); + uint offset = _102; + uint param = offset; + uint param_1 = size; + r.alloc = new_alloc(param, param_1); + if ((offset + size) > uint(int(uint(_96.memory.length())) * 4)) + { + r.failed = true; + uint _123 = atomicMax(_96.mem_error, 1u); + return r; + } + return r; +} + +Alloc slice_mem(Alloc a, uint offset, uint size) +{ + uint param = a.offset + offset; + uint param_1 = size; + return new_alloc(param, param_1); +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _96.memory[offset] = val; +} + +void Path_write(Alloc a, PathRef ref, Path s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.bbox.x | (s.bbox.y << uint(16)); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = s.bbox.z | (s.bbox.w << uint(16)); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = s.tiles.offset; + write_mem(param_6, param_7, param_8); +} + +void main() +{ + if (_96.mem_error != 0u) + { + return; + } + uint th_ix = gl_LocalInvocationID.x; + uint element_ix = gl_GlobalInvocationID.x; + PathRef path_ref = PathRef(_309.conf.tile_alloc.offset + (element_ix * 12u)); + AnnotatedRef ref = AnnotatedRef(_309.conf.anno_alloc.offset + (element_ix * 32u)); + uint tag = 0u; + if (element_ix < _309.conf.n_elements) + { + Alloc param; + param.offset = _309.conf.anno_alloc.offset; + AnnotatedRef param_1 = ref; + tag = Annotated_tag(param, param_1).tag; + } + int x0 = 0; + int y0 = 0; + int x1 = 0; + int y1 = 0; + switch (tag) + { + case 1u: + case 2u: + case 3u: + case 4u: + { + Alloc param_2; + param_2.offset = _309.conf.anno_alloc.offset; + AnnotatedRef param_3 = ref; + AnnoEndClip clip = Annotated_EndClip_read(param_2, param_3); + x0 = int(floor(clip.bbox.x * 0.03125)); + y0 = int(floor(clip.bbox.y * 0.03125)); + x1 = int(ceil(clip.bbox.z * 0.03125)); + y1 = int(ceil(clip.bbox.w * 0.03125)); + break; + } + } + x0 = clamp(x0, 0, int(_309.conf.width_in_tiles)); + y0 = clamp(y0, 0, int(_309.conf.height_in_tiles)); + x1 = clamp(x1, 0, int(_309.conf.width_in_tiles)); + y1 = clamp(y1, 0, int(_309.conf.height_in_tiles)); + Path path; + path.bbox = uvec4(uint(x0), uint(y0), uint(x1), uint(y1)); + uint tile_count = uint((x1 - x0) * (y1 - y0)); + if (tag == 4u) + { + tile_count = 0u; + } + sh_tile_count[th_ix] = tile_count; + uint total_tile_count = tile_count; + for (uint i = 0u; i < 7u; i++) + { + barrier(); + if (th_ix >= uint(1 << int(i))) + { + total_tile_count += sh_tile_count[th_ix - uint(1 << int(i))]; + } + barrier(); + sh_tile_count[th_ix] = total_tile_count; + } + if (th_ix == 127u) + { + uint param_4 = total_tile_count * 8u; + MallocResult _482 = malloc(param_4); + sh_tile_alloc = _482; + } + barrier(); + MallocResult alloc_start = sh_tile_alloc; + if (alloc_start.failed) + { + return; + } + if (element_ix < _309.conf.n_elements) + { + uint _499; + if (th_ix > 0u) + { + _499 = sh_tile_count[th_ix - 1u]; + } + else + { + _499 = 0u; + } + uint tile_subix = _499; + Alloc param_5 = alloc_start.alloc; + uint param_6 = 8u * tile_subix; + uint param_7 = 8u * tile_count; + Alloc tiles_alloc = slice_mem(param_5, param_6, param_7); + path.tiles = TileRef(tiles_alloc.offset); + Alloc param_8; + param_8.offset = _309.conf.tile_alloc.offset; + PathRef param_9 = path_ref; + Path param_10 = path; + Path_write(param_8, param_9, param_10); + } + uint total_count = sh_tile_count[127] * 2u; + uint start_ix = alloc_start.alloc.offset >> uint(2); + for (uint i_1 = th_ix; i_1 < total_count; i_1 += 128u) + { + Alloc param_11 = alloc_start.alloc; + uint param_12 = start_ix + i_1; + uint param_13 = 0u; + write_mem(param_11, param_12, param_13); + } +} + +`, + } +) diff --git a/gio/gpu/timer.go b/gio/gpu/timer.go new file mode 100644 index 0000000..6e0bd4a --- /dev/null +++ b/gio/gpu/timer.go @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "time" + + "realy.lol/gio/gpu/internal/driver" +) + +type timers struct { + backend driver.Device + timers []*timer +} + +type timer struct { + Elapsed time.Duration + backend driver.Device + timer driver.Timer + state timerState +} + +type timerState uint8 + +const ( + timerIdle timerState = iota + timerRunning + timerWaiting +) + +func newTimers(b driver.Device) *timers { + return &timers{ + backend: b, + } +} + +func (t *timers) newTimer() *timer { + if t == nil { + return nil + } + tt := &timer{ + backend: t.backend, + timer: t.backend.NewTimer(), + } + t.timers = append(t.timers, tt) + return tt +} + +func (t *timer) begin() { + if t == nil || t.state != timerIdle { + return + } + t.timer.Begin() + t.state = timerRunning +} + +func (t *timer) end() { + if t == nil || t.state != timerRunning { + return + } + t.timer.End() + t.state = timerWaiting +} + +func (t *timers) ready() bool { + if t == nil { + return false + } + for _, tt := range t.timers { + switch tt.state { + case timerIdle: + continue + case timerRunning: + return false + } + d, ok := tt.timer.Duration() + if !ok { + return false + } + tt.state = timerIdle + tt.Elapsed = d + } + return t.backend.IsTimeContinuous() +} + +func (t *timers) release() { + if t == nil { + return + } + for _, tt := range t.timers { + tt.timer.Release() + } + t.timers = nil +} diff --git a/gio/internal/byteslice/byteslice.go b/gio/internal/byteslice/byteslice.go new file mode 100644 index 0000000..26ebdb2 --- /dev/null +++ b/gio/internal/byteslice/byteslice.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package byteslice provides byte slice views of other Go values such as +// slices and structs. +package byteslice + +import ( + "reflect" + "unsafe" +) + +// Struct returns a byte slice view of a struct. +func Struct(s interface{}) []byte { + v := reflect.ValueOf(s).Elem() + sz := int(v.Type().Size()) + var res []byte + h := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + h.Data = uintptr(unsafe.Pointer(v.UnsafeAddr())) + h.Cap = sz + h.Len = sz + return res +} + +// Uint32 returns a byte slice view of a uint32 slice. +func Uint32(s []uint32) []byte { + n := len(s) + if n == 0 { + return nil + } + blen := n * int(unsafe.Sizeof(s[0])) + return (*[1 << 30]byte)(unsafe.Pointer(&s[0]))[:blen:blen] +} + +// Slice returns a byte slice view of a slice. +func Slice(s interface{}) []byte { + v := reflect.ValueOf(s) + first := v.Index(0) + sz := int(first.Type().Size()) + var res []byte + h := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + h.Data = first.UnsafeAddr() + h.Cap = v.Cap() * sz + h.Len = v.Len() * sz + return res +} diff --git a/gio/internal/cocoainit/cocoa_darwin.go b/gio/internal/cocoainit/cocoa_darwin.go new file mode 100644 index 0000000..2a34e57 --- /dev/null +++ b/gio/internal/cocoainit/cocoa_darwin.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package cocoainit initializes support for multithreaded +// programs in Cocoa. +package cocoainit + +/* +#cgo CFLAGS: -xobjective-c -fmodules -fobjc-arc +#import + +static inline void activate_cocoa_multithreading() { + [[NSThread new] start]; +} +#pragma GCC visibility push(hidden) +*/ +import "C" + +func init() { + C.activate_cocoa_multithreading() +} diff --git a/gio/internal/d3d11/d3d11_windows.go b/gio/internal/d3d11/d3d11_windows.go new file mode 100644 index 0000000..f33eb61 --- /dev/null +++ b/gio/internal/d3d11/d3d11_windows.go @@ -0,0 +1,1470 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package d3d11 + +import ( + "fmt" + "math" + "syscall" + "unsafe" + + "realy.lol/gio/internal/f32color" + + "golang.org/x/sys/windows" +) + +type DXGI_SWAP_CHAIN_DESC struct { + BufferDesc DXGI_MODE_DESC + SampleDesc DXGI_SAMPLE_DESC + BufferUsage uint32 + BufferCount uint32 + OutputWindow windows.Handle + Windowed uint32 + SwapEffect uint32 + Flags uint32 +} + +type DXGI_SAMPLE_DESC struct { + Count uint32 + Quality uint32 +} + +type DXGI_MODE_DESC struct { + Width uint32 + Height uint32 + RefreshRate DXGI_RATIONAL + Format uint32 + ScanlineOrdering uint32 + Scaling uint32 +} + +type DXGI_RATIONAL struct { + Numerator uint32 + Denominator uint32 +} + +type TEXTURE2D_DESC struct { + Width uint32 + Height uint32 + MipLevels uint32 + ArraySize uint32 + Format uint32 + SampleDesc DXGI_SAMPLE_DESC + Usage uint32 + BindFlags uint32 + CPUAccessFlags uint32 + MiscFlags uint32 +} + +type SAMPLER_DESC struct { + Filter uint32 + AddressU uint32 + AddressV uint32 + AddressW uint32 + MipLODBias float32 + MaxAnisotropy uint32 + ComparisonFunc uint32 + BorderColor [4]float32 + MinLOD float32 + MaxLOD float32 +} + +type SHADER_RESOURCE_VIEW_DESC_TEX2D struct { + SHADER_RESOURCE_VIEW_DESC + Texture2D TEX2D_SRV +} + +type SHADER_RESOURCE_VIEW_DESC struct { + Format uint32 + ViewDimension uint32 +} + +type TEX2D_SRV struct { + MostDetailedMip uint32 + MipLevels uint32 +} + +type INPUT_ELEMENT_DESC struct { + SemanticName *byte + SemanticIndex uint32 + Format uint32 + InputSlot uint32 + AlignedByteOffset uint32 + InputSlotClass uint32 + InstanceDataStepRate uint32 +} + +type IDXGISwapChain struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + GetDevice uintptr + Present uintptr + GetBuffer uintptr + SetFullscreenState uintptr + GetFullscreenState uintptr + GetDesc uintptr + ResizeBuffers uintptr + ResizeTarget uintptr + GetContainingOutput uintptr + GetFrameStatistics uintptr + GetLastPresentCount uintptr + } +} + +type Device struct { + Vtbl *struct { + _IUnknownVTbl + CreateBuffer uintptr + CreateTexture1D uintptr + CreateTexture2D uintptr + CreateTexture3D uintptr + CreateShaderResourceView uintptr + CreateUnorderedAccessView uintptr + CreateRenderTargetView uintptr + CreateDepthStencilView uintptr + CreateInputLayout uintptr + CreateVertexShader uintptr + CreateGeometryShader uintptr + CreateGeometryShaderWithStreamOutput uintptr + CreatePixelShader uintptr + CreateHullShader uintptr + CreateDomainShader uintptr + CreateComputeShader uintptr + CreateClassLinkage uintptr + CreateBlendState uintptr + CreateDepthStencilState uintptr + CreateRasterizerState uintptr + CreateSamplerState uintptr + CreateQuery uintptr + CreatePredicate uintptr + CreateCounter uintptr + CreateDeferredContext uintptr + OpenSharedResource uintptr + CheckFormatSupport uintptr + CheckMultisampleQualityLevels uintptr + CheckCounterInfo uintptr + CheckCounter uintptr + CheckFeatureSupport uintptr + GetPrivateData uintptr + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetFeatureLevel uintptr + GetCreationFlags uintptr + GetDeviceRemovedReason uintptr + GetImmediateContext uintptr + SetExceptionMode uintptr + GetExceptionMode uintptr + } +} + +type DeviceContext struct { + Vtbl *struct { + _IUnknownVTbl + GetDevice uintptr + GetPrivateData uintptr + SetPrivateData uintptr + SetPrivateDataInterface uintptr + VSSetConstantBuffers uintptr + PSSetShaderResources uintptr + PSSetShader uintptr + PSSetSamplers uintptr + VSSetShader uintptr + DrawIndexed uintptr + Draw uintptr + Map uintptr + Unmap uintptr + PSSetConstantBuffers uintptr + IASetInputLayout uintptr + IASetVertexBuffers uintptr + IASetIndexBuffer uintptr + DrawIndexedInstanced uintptr + DrawInstanced uintptr + GSSetConstantBuffers uintptr + GSSetShader uintptr + IASetPrimitiveTopology uintptr + VSSetShaderResources uintptr + VSSetSamplers uintptr + Begin uintptr + End uintptr + GetData uintptr + SetPredication uintptr + GSSetShaderResources uintptr + GSSetSamplers uintptr + OMSetRenderTargets uintptr + OMSetRenderTargetsAndUnorderedAccessViews uintptr + OMSetBlendState uintptr + OMSetDepthStencilState uintptr + SOSetTargets uintptr + DrawAuto uintptr + DrawIndexedInstancedIndirect uintptr + DrawInstancedIndirect uintptr + Dispatch uintptr + DispatchIndirect uintptr + RSSetState uintptr + RSSetViewports uintptr + RSSetScissorRects uintptr + CopySubresourceRegion uintptr + CopyResource uintptr + UpdateSubresource uintptr + CopyStructureCount uintptr + ClearRenderTargetView uintptr + ClearUnorderedAccessViewUint uintptr + ClearUnorderedAccessViewFloat uintptr + ClearDepthStencilView uintptr + GenerateMips uintptr + SetResourceMinLOD uintptr + GetResourceMinLOD uintptr + ResolveSubresource uintptr + ExecuteCommandList uintptr + HSSetShaderResources uintptr + HSSetShader uintptr + HSSetSamplers uintptr + HSSetConstantBuffers uintptr + DSSetShaderResources uintptr + DSSetShader uintptr + DSSetSamplers uintptr + DSSetConstantBuffers uintptr + CSSetShaderResources uintptr + CSSetUnorderedAccessViews uintptr + CSSetShader uintptr + CSSetSamplers uintptr + CSSetConstantBuffers uintptr + VSGetConstantBuffers uintptr + PSGetShaderResources uintptr + PSGetShader uintptr + PSGetSamplers uintptr + VSGetShader uintptr + PSGetConstantBuffers uintptr + IAGetInputLayout uintptr + IAGetVertexBuffers uintptr + IAGetIndexBuffer uintptr + GSGetConstantBuffers uintptr + GSGetShader uintptr + IAGetPrimitiveTopology uintptr + VSGetShaderResources uintptr + VSGetSamplers uintptr + GetPredication uintptr + GSGetShaderResources uintptr + GSGetSamplers uintptr + OMGetRenderTargets uintptr + OMGetRenderTargetsAndUnorderedAccessViews uintptr + OMGetBlendState uintptr + OMGetDepthStencilState uintptr + SOGetTargets uintptr + RSGetState uintptr + RSGetViewports uintptr + RSGetScissorRects uintptr + HSGetShaderResources uintptr + HSGetShader uintptr + HSGetSamplers uintptr + HSGetConstantBuffers uintptr + DSGetShaderResources uintptr + DSGetShader uintptr + DSGetSamplers uintptr + DSGetConstantBuffers uintptr + CSGetShaderResources uintptr + CSGetUnorderedAccessViews uintptr + CSGetShader uintptr + CSGetSamplers uintptr + CSGetConstantBuffers uintptr + ClearState uintptr + Flush uintptr + GetType uintptr + GetContextFlags uintptr + FinishCommandList uintptr + } +} + +type RenderTargetView struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type Resource struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type Texture2D struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type Buffer struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type SamplerState struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type PixelShader struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type ShaderResourceView struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type DepthStencilView struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type BlendState struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type DepthStencilState struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type VertexShader struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type RasterizerState struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type InputLayout struct { + Vtbl *struct { + _IUnknownVTbl + GetBufferPointer uintptr + GetBufferSize uintptr + } +} + +type DEPTH_STENCIL_DESC struct { + DepthEnable uint32 + DepthWriteMask uint32 + DepthFunc uint32 + StencilEnable uint32 + StencilReadMask uint8 + StencilWriteMask uint8 + FrontFace DEPTH_STENCILOP_DESC + BackFace DEPTH_STENCILOP_DESC +} + +type DEPTH_STENCILOP_DESC struct { + StencilFailOp uint32 + StencilDepthFailOp uint32 + StencilPassOp uint32 + StencilFunc uint32 +} + +type DEPTH_STENCIL_VIEW_DESC_TEX2D struct { + Format uint32 + ViewDimension uint32 + Flags uint32 + Texture2D TEX2D_DSV +} + +type TEX2D_DSV struct { + MipSlice uint32 +} + +type BLEND_DESC struct { + AlphaToCoverageEnable uint32 + IndependentBlendEnable uint32 + RenderTarget [8]RENDER_TARGET_BLEND_DESC +} + +type RENDER_TARGET_BLEND_DESC struct { + BlendEnable uint32 + SrcBlend uint32 + DestBlend uint32 + BlendOp uint32 + SrcBlendAlpha uint32 + DestBlendAlpha uint32 + BlendOpAlpha uint32 + RenderTargetWriteMask uint8 +} + +type IDXGIObject struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + } +} + +type IDXGIAdapter struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + EnumOutputs uintptr + GetDesc uintptr + CheckInterfaceSupport uintptr + GetDesc1 uintptr + } +} + +type IDXGIFactory struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + EnumAdapters uintptr + MakeWindowAssociation uintptr + GetWindowAssociation uintptr + CreateSwapChain uintptr + CreateSoftwareAdapter uintptr + } +} + +type IDXGIDevice struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + GetAdapter uintptr + CreateSurface uintptr + QueryResourceResidency uintptr + SetGPUThreadPriority uintptr + GetGPUThreadPriority uintptr + } +} + +type IUnknown struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type _IUnknownVTbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr +} + +type BUFFER_DESC struct { + ByteWidth uint32 + Usage uint32 + BindFlags uint32 + CPUAccessFlags uint32 + MiscFlags uint32 + StructureByteStride uint32 +} + +type GUID struct { + Data1 uint32 + Data2 uint16 + Data3 uint16 + Data4_0 uint8 + Data4_1 uint8 + Data4_2 uint8 + Data4_3 uint8 + Data4_4 uint8 + Data4_5 uint8 + Data4_6 uint8 + Data4_7 uint8 +} + +type VIEWPORT struct { + TopLeftX float32 + TopLeftY float32 + Width float32 + Height float32 + MinDepth float32 + MaxDepth float32 +} + +type SUBRESOURCE_DATA struct { + pSysMem *byte +} + +type BOX struct { + Left uint32 + Top uint32 + Front uint32 + Right uint32 + Bottom uint32 + Back uint32 +} + +type MAPPED_SUBRESOURCE struct { + PData uintptr + RowPitch uint32 + DepthPitch uint32 +} + +type ErrorCode struct { + Name string + Code uint32 +} + +type RASTERIZER_DESC struct { + FillMode uint32 + CullMode uint32 + FrontCounterClockwise uint32 + DepthBias int32 + DepthBiasClamp float32 + SlopeScaledDepthBias float32 + DepthClipEnable uint32 + ScissorEnable uint32 + MultisampleEnable uint32 + AntialiasedLineEnable uint32 +} + +var ( + IID_Texture2D = GUID{0x6f15aaf2, 0xd208, 0x4e89, 0x9a, 0xb4, 0x48, 0x95, + 0x35, 0xd3, 0x4f, 0x9c} + IID_IDXGIDevice = GUID{0x54ec77fa, 0x1377, 0x44e6, 0x8c, 0x32, 0x88, 0xfd, + 0x5f, 0x44, 0xc8, 0x4c} + IID_IDXGIFactory = GUID{0x7b7166ec, 0x21c7, 0x44ae, 0xb2, 0x1a, 0xc9, 0xae, + 0x32, 0x1a, 0xe3, 0x69} +) + +var ( + d3d11 = windows.NewLazySystemDLL("d3d11.dll") + + _D3D11CreateDevice = d3d11.NewProc("D3D11CreateDevice") + _D3D11CreateDeviceAndSwapChain = d3d11.NewProc("D3D11CreateDeviceAndSwapChain") +) + +const ( + SDK_VERSION = 7 + DRIVER_TYPE_HARDWARE = 1 + + DXGI_FORMAT_UNKNOWN = 0 + DXGI_FORMAT_R16_FLOAT = 54 + DXGI_FORMAT_R32_FLOAT = 41 + DXGI_FORMAT_R32G32_FLOAT = 16 + DXGI_FORMAT_R32G32B32_FLOAT = 6 + DXGI_FORMAT_R32G32B32A32_FLOAT = 2 + DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29 + DXGI_FORMAT_R16_SINT = 59 + DXGI_FORMAT_R16G16_SINT = 38 + DXGI_FORMAT_R16_UINT = 57 + DXGI_FORMAT_D24_UNORM_S8_UINT = 45 + DXGI_FORMAT_R16G16_FLOAT = 34 + DXGI_FORMAT_R16G16B16A16_FLOAT = 10 + + FORMAT_SUPPORT_TEXTURE2D = 0x20 + FORMAT_SUPPORT_RENDER_TARGET = 0x4000 + + DXGI_USAGE_RENDER_TARGET_OUTPUT = 1 << (1 + 4) + + CPU_ACCESS_READ = 0x20000 + + MAP_READ = 1 + + DXGI_SWAP_EFFECT_DISCARD = 0 + + FEATURE_LEVEL_9_1 = 0x9100 + FEATURE_LEVEL_9_3 = 0x9300 + FEATURE_LEVEL_11_0 = 0xb000 + + USAGE_IMMUTABLE = 1 + USAGE_STAGING = 3 + + BIND_VERTEX_BUFFER = 0x1 + BIND_INDEX_BUFFER = 0x2 + BIND_CONSTANT_BUFFER = 0x4 + BIND_SHADER_RESOURCE = 0x8 + BIND_RENDER_TARGET = 0x20 + BIND_DEPTH_STENCIL = 0x40 + + PRIMITIVE_TOPOLOGY_TRIANGLELIST = 4 + PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 5 + + FILTER_MIN_MAG_LINEAR_MIP_POINT = 0x14 + FILTER_MIN_MAG_MIP_POINT = 0 + + TEXTURE_ADDRESS_MIRROR = 2 + TEXTURE_ADDRESS_CLAMP = 3 + TEXTURE_ADDRESS_WRAP = 1 + + SRV_DIMENSION_TEXTURE2D = 4 + + CREATE_DEVICE_DEBUG = 0x2 + + FILL_SOLID = 3 + + CULL_NONE = 1 + + CLEAR_DEPTH = 0x1 + CLEAR_STENCIL = 0x2 + + DSV_DIMENSION_TEXTURE2D = 3 + + DEPTH_WRITE_MASK_ALL = 1 + + COMPARISON_GREATER = 5 + COMPARISON_GREATER_EQUAL = 7 + + BLEND_OP_ADD = 1 + BLEND_ONE = 2 + BLEND_INV_SRC_ALPHA = 6 + BLEND_ZERO = 1 + BLEND_DEST_COLOR = 9 + BLEND_DEST_ALPHA = 7 + + COLOR_WRITE_ENABLE_ALL = 1 | 2 | 4 | 8 + + DXGI_STATUS_OCCLUDED = 0x087A0001 + DXGI_ERROR_DEVICE_RESET = 0x887A0007 + DXGI_ERROR_DEVICE_REMOVED = 0x887A0005 + D3DDDIERR_DEVICEREMOVED = 1<<31 | 0x876<<16 | 2160 +) + +func CreateDevice(driverType uint32, flags uint32) (*Device, *DeviceContext, + uint32, error) { + var ( + dev *Device + ctx *DeviceContext + featLvl uint32 + ) + r, _, _ := _D3D11CreateDevice.Call( + 0, // pAdapter + uintptr(driverType), // driverType + 0, // Software + uintptr(flags), // Flags + 0, // pFeatureLevels + 0, // FeatureLevels + SDK_VERSION, // SDKVersion + uintptr(unsafe.Pointer(&dev)), // ppDevice + uintptr(unsafe.Pointer(&featLvl)), // pFeatureLevel + uintptr(unsafe.Pointer(&ctx)), // ppImmediateContext + ) + if r != 0 { + return nil, nil, 0, ErrorCode{Name: "D3D11CreateDevice", + Code: uint32(r)} + } + return dev, ctx, featLvl, nil +} + +func CreateDeviceAndSwapChain(driverType uint32, flags uint32, + swapDesc *DXGI_SWAP_CHAIN_DESC) (*Device, *DeviceContext, *IDXGISwapChain, + uint32, error) { + var ( + dev *Device + ctx *DeviceContext + swchain *IDXGISwapChain + featLvl uint32 + ) + r, _, _ := _D3D11CreateDeviceAndSwapChain.Call( + 0, // pAdapter + uintptr(driverType), // driverType + 0, // Software + uintptr(flags), // Flags + 0, // pFeatureLevels + 0, // FeatureLevels + SDK_VERSION, // SDKVersion + uintptr(unsafe.Pointer(swapDesc)), // pSwapChainDesc + uintptr(unsafe.Pointer(&swchain)), // ppSwapChain + uintptr(unsafe.Pointer(&dev)), // ppDevice + uintptr(unsafe.Pointer(&featLvl)), // pFeatureLevel + uintptr(unsafe.Pointer(&ctx)), // ppImmediateContext + ) + if r != 0 { + return nil, nil, nil, 0, ErrorCode{Name: "D3D11CreateDeviceAndSwapChain", + Code: uint32(r)} + } + return dev, ctx, swchain, featLvl, nil +} + +func (d *Device) CheckFormatSupport(format uint32) (uint32, error) { + var support uint32 + r, _, _ := syscall.Syscall( + d.Vtbl.CheckFormatSupport, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(format), + uintptr(unsafe.Pointer(&support)), + ) + if r != 0 { + return 0, ErrorCode{Name: "DeviceCheckFormatSupport", Code: uint32(r)} + } + return support, nil +} + +func (d *Device) CreateBuffer(desc *BUFFER_DESC, data []byte) (*Buffer, error) { + var dataDesc *SUBRESOURCE_DATA + if len(data) > 0 { + dataDesc = &SUBRESOURCE_DATA{ + pSysMem: &data[0], + } + } + var buf *Buffer + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateBuffer, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(dataDesc)), + uintptr(unsafe.Pointer(&buf)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateBuffer", Code: uint32(r)} + } + return buf, nil +} + +func (d *Device) CreateDepthStencilViewTEX2D(res *Resource, + desc *DEPTH_STENCIL_VIEW_DESC_TEX2D) (*DepthStencilView, error) { + var view *DepthStencilView + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateDepthStencilView, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(res)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&view)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateDepthStencilView", + Code: uint32(r)} + } + return view, nil +} + +func (d *Device) CreatePixelShader(bytecode []byte) (*PixelShader, error) { + var shader *PixelShader + r, _, _ := syscall.Syscall6( + d.Vtbl.CreatePixelShader, + 5, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(&bytecode[0])), + uintptr(len(bytecode)), + 0, // pClassLinkage + uintptr(unsafe.Pointer(&shader)), + 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreatePixelShader", Code: uint32(r)} + } + return shader, nil +} + +func (d *Device) CreateVertexShader(bytecode []byte) (*VertexShader, error) { + var shader *VertexShader + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateVertexShader, + 5, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(&bytecode[0])), + uintptr(len(bytecode)), + 0, // pClassLinkage + uintptr(unsafe.Pointer(&shader)), + 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateVertexShader", Code: uint32(r)} + } + return shader, nil +} + +func (d *Device) CreateShaderResourceViewTEX2D(res *Resource, + desc *SHADER_RESOURCE_VIEW_DESC_TEX2D) (*ShaderResourceView, error) { + var resView *ShaderResourceView + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateShaderResourceView, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(res)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&resView)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateShaderResourceView", + Code: uint32(r)} + } + return resView, nil +} + +func (d *Device) CreateRasterizerState(desc *RASTERIZER_DESC) (*RasterizerState, + error) { + var state *RasterizerState + r, _, _ := syscall.Syscall( + d.Vtbl.CreateRasterizerState, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&state)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateRasterizerState", + Code: uint32(r)} + } + return state, nil +} + +func (d *Device) CreateInputLayout(descs []INPUT_ELEMENT_DESC, + bytecode []byte) (*InputLayout, error) { + var pdesc *INPUT_ELEMENT_DESC + if len(descs) > 0 { + pdesc = &descs[0] + } + var layout *InputLayout + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateInputLayout, + 6, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(pdesc)), + uintptr(len(descs)), + uintptr(unsafe.Pointer(&bytecode[0])), + uintptr(len(bytecode)), + uintptr(unsafe.Pointer(&layout)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateInputLayout", Code: uint32(r)} + } + return layout, nil +} + +func (d *Device) CreateSamplerState(desc *SAMPLER_DESC) (*SamplerState, error) { + var sampler *SamplerState + r, _, _ := syscall.Syscall( + d.Vtbl.CreateSamplerState, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&sampler)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateSamplerState", Code: uint32(r)} + } + return sampler, nil +} + +func (d *Device) CreateTexture2D(desc *TEXTURE2D_DESC) (*Texture2D, error) { + var tex *Texture2D + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateTexture2D, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + 0, // pInitialData + uintptr(unsafe.Pointer(&tex)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "CreateTexture2D", Code: uint32(r)} + } + return tex, nil +} + +func (d *Device) CreateRenderTargetView(res *Resource) (*RenderTargetView, + error) { + var target *RenderTargetView + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateRenderTargetView, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(res)), + 0, // pDesc + uintptr(unsafe.Pointer(&target)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateRenderTargetView", + Code: uint32(r)} + } + return target, nil +} + +func (d *Device) CreateBlendState(desc *BLEND_DESC) (*BlendState, error) { + var state *BlendState + r, _, _ := syscall.Syscall( + d.Vtbl.CreateBlendState, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&state)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateBlendState", Code: uint32(r)} + } + return state, nil +} + +func (d *Device) CreateDepthStencilState(desc *DEPTH_STENCIL_DESC) (*DepthStencilState, + error) { + var state *DepthStencilState + r, _, _ := syscall.Syscall( + d.Vtbl.CreateDepthStencilState, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&state)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateDepthStencilState", + Code: uint32(r)} + } + return state, nil +} + +func (d *Device) GetFeatureLevel() int { + lvl, _, _ := syscall.Syscall( + d.Vtbl.GetFeatureLevel, + 1, + uintptr(unsafe.Pointer(d)), + 0, 0, + ) + return int(lvl) +} + +func (d *Device) GetImmediateContext() *DeviceContext { + var ctx *DeviceContext + syscall.Syscall( + d.Vtbl.GetImmediateContext, + 2, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(&ctx)), + 0, + ) + return ctx +} + +func (s *IDXGISwapChain) GetDesc() (DXGI_SWAP_CHAIN_DESC, error) { + var desc DXGI_SWAP_CHAIN_DESC + r, _, _ := syscall.Syscall( + s.Vtbl.GetDesc, + 2, + uintptr(unsafe.Pointer(s)), + uintptr(unsafe.Pointer(&desc)), + 0, + ) + if r != 0 { + return DXGI_SWAP_CHAIN_DESC{}, ErrorCode{Name: "IDXGISwapChainGetDesc", + Code: uint32(r)} + } + return desc, nil +} + +func (s *IDXGISwapChain) ResizeBuffers(buffers, width, height, newFormat, flags uint32) error { + r, _, _ := syscall.Syscall6( + s.Vtbl.ResizeBuffers, + 6, + uintptr(unsafe.Pointer(s)), + uintptr(buffers), + uintptr(width), + uintptr(height), + uintptr(newFormat), + uintptr(flags), + ) + if r != 0 { + return ErrorCode{Name: "IDXGISwapChainResizeBuffers", Code: uint32(r)} + } + return nil +} + +func (s *IDXGISwapChain) Present(SyncInterval int, Flags uint32) error { + r, _, _ := syscall.Syscall( + s.Vtbl.Present, + 3, + uintptr(unsafe.Pointer(s)), + uintptr(SyncInterval), + uintptr(Flags), + ) + if r != 0 { + return ErrorCode{Name: "IDXGISwapChainPresent", Code: uint32(r)} + } + return nil +} + +func (s *IDXGISwapChain) GetBuffer(index int, riid *GUID) (*IUnknown, error) { + var buf *IUnknown + r, _, _ := syscall.Syscall6( + s.Vtbl.GetBuffer, + 4, + uintptr(unsafe.Pointer(s)), + uintptr(index), + uintptr(unsafe.Pointer(riid)), + uintptr(unsafe.Pointer(&buf)), + 0, + 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "IDXGISwapChainGetBuffer", Code: uint32(r)} + } + return buf, nil +} + +func (c *DeviceContext) Unmap(resource *Resource, subResource uint32) { + syscall.Syscall( + c.Vtbl.Unmap, + 3, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(resource)), + uintptr(subResource), + ) +} + +func (c *DeviceContext) Map(resource *Resource, + subResource, mapType, mapFlags uint32) (MAPPED_SUBRESOURCE, error) { + var resMap MAPPED_SUBRESOURCE + r, _, _ := syscall.Syscall6( + c.Vtbl.Map, + 6, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(resource)), + uintptr(subResource), + uintptr(mapType), + uintptr(mapFlags), + uintptr(unsafe.Pointer(&resMap)), + ) + if r != 0 { + return resMap, ErrorCode{Name: "DeviceContextMap", Code: uint32(r)} + } + return resMap, nil +} + +func (c *DeviceContext) CopySubresourceRegion(dst *Resource, + dstSubresource, dstX, dstY, dstZ uint32, src *Resource, + srcSubresource uint32, srcBox *BOX) { + syscall.Syscall9( + c.Vtbl.CopySubresourceRegion, + 9, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(dst)), + uintptr(dstSubresource), + uintptr(dstX), + uintptr(dstY), + uintptr(dstZ), + uintptr(unsafe.Pointer(src)), + uintptr(srcSubresource), + uintptr(unsafe.Pointer(srcBox)), + ) +} + +func (c *DeviceContext) ClearDepthStencilView(target *DepthStencilView, + flags uint32, depth float32, stencil uint8) { + syscall.Syscall6( + c.Vtbl.ClearDepthStencilView, + 5, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(target)), + uintptr(flags), + uintptr(math.Float32bits(depth)), + uintptr(stencil), + 0, + ) +} + +func (c *DeviceContext) ClearRenderTargetView(target *RenderTargetView, + color *[4]float32) { + syscall.Syscall( + c.Vtbl.ClearRenderTargetView, + 3, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(target)), + uintptr(unsafe.Pointer(color)), + ) +} + +func (c *DeviceContext) RSSetViewports(viewport *VIEWPORT) { + syscall.Syscall( + c.Vtbl.RSSetViewports, + 3, + uintptr(unsafe.Pointer(c)), + 1, // NumViewports + uintptr(unsafe.Pointer(viewport)), + ) +} + +func (c *DeviceContext) VSSetShader(s *VertexShader) { + syscall.Syscall6( + c.Vtbl.VSSetShader, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(s)), + 0, // ppClassInstances + 0, // NumClassInstances + 0, 0, + ) +} + +func (c *DeviceContext) VSSetConstantBuffers(b *Buffer) { + syscall.Syscall6( + c.Vtbl.VSSetConstantBuffers, + 4, + uintptr(unsafe.Pointer(c)), + 0, // StartSlot + 1, // NumBuffers + uintptr(unsafe.Pointer(&b)), + 0, 0, + ) +} + +func (c *DeviceContext) PSSetConstantBuffers(b *Buffer) { + syscall.Syscall6( + c.Vtbl.PSSetConstantBuffers, + 4, + uintptr(unsafe.Pointer(c)), + 0, // StartSlot + 1, // NumBuffers + uintptr(unsafe.Pointer(&b)), + 0, 0, + ) +} + +func (c *DeviceContext) PSSetShaderResources(startSlot uint32, + s *ShaderResourceView) { + syscall.Syscall6( + c.Vtbl.PSSetShaderResources, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(startSlot), + 1, // NumViews + uintptr(unsafe.Pointer(&s)), + 0, 0, + ) +} + +func (c *DeviceContext) PSSetSamplers(startSlot uint32, s *SamplerState) { + syscall.Syscall6( + c.Vtbl.PSSetSamplers, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(startSlot), + 1, // NumSamplers + uintptr(unsafe.Pointer(&s)), + 0, 0, + ) +} + +func (c *DeviceContext) PSSetShader(s *PixelShader) { + syscall.Syscall6( + c.Vtbl.PSSetShader, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(s)), + 0, // ppClassInstances + 0, // NumClassInstances + 0, 0, + ) +} + +func (c *DeviceContext) UpdateSubresource(res *Resource, dstBox *BOX, + rowPitch, depthPitch uint32, data []byte) { + syscall.Syscall9( + c.Vtbl.UpdateSubresource, + 7, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(res)), + 0, // DstSubresource + uintptr(unsafe.Pointer(dstBox)), + uintptr(unsafe.Pointer(&data[0])), + uintptr(rowPitch), + uintptr(depthPitch), + 0, 0, + ) +} + +func (c *DeviceContext) RSSetState(state *RasterizerState) { + syscall.Syscall( + c.Vtbl.RSSetState, + 2, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(state)), + 0, + ) +} + +func (c *DeviceContext) IASetInputLayout(layout *InputLayout) { + syscall.Syscall( + c.Vtbl.IASetInputLayout, + 2, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(layout)), + 0, + ) +} + +func (c *DeviceContext) IASetIndexBuffer(buf *Buffer, format, offset uint32) { + syscall.Syscall6( + c.Vtbl.IASetIndexBuffer, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(buf)), + uintptr(format), + uintptr(offset), + 0, 0, + ) +} + +func (c *DeviceContext) IASetVertexBuffers(buf *Buffer, stride, offset uint32) { + syscall.Syscall6( + c.Vtbl.IASetVertexBuffers, + 6, + uintptr(unsafe.Pointer(c)), + 0, // StartSlot + 1, // NumBuffers, + uintptr(unsafe.Pointer(&buf)), + uintptr(unsafe.Pointer(&stride)), + uintptr(unsafe.Pointer(&offset)), + ) +} + +func (c *DeviceContext) IASetPrimitiveTopology(mode uint32) { + syscall.Syscall( + c.Vtbl.IASetPrimitiveTopology, + 2, + uintptr(unsafe.Pointer(c)), + uintptr(mode), + 0, + ) +} + +func (c *DeviceContext) OMGetRenderTargets() (*RenderTargetView, + *DepthStencilView) { + var ( + target *RenderTargetView + depthStencilView *DepthStencilView + ) + syscall.Syscall6( + c.Vtbl.OMGetRenderTargets, + 4, + uintptr(unsafe.Pointer(c)), + 1, // NumViews + uintptr(unsafe.Pointer(&target)), + uintptr(unsafe.Pointer(&depthStencilView)), + 0, 0, + ) + return target, depthStencilView +} + +func (c *DeviceContext) OMSetRenderTargets(target *RenderTargetView, + depthStencil *DepthStencilView) { + syscall.Syscall6( + c.Vtbl.OMSetRenderTargets, + 4, + uintptr(unsafe.Pointer(c)), + 1, // NumViews + uintptr(unsafe.Pointer(&target)), + uintptr(unsafe.Pointer(depthStencil)), + 0, 0, + ) +} + +func (c *DeviceContext) Draw(count, start uint32) { + syscall.Syscall( + c.Vtbl.Draw, + 3, + uintptr(unsafe.Pointer(c)), + uintptr(count), + uintptr(start), + ) +} + +func (c *DeviceContext) DrawIndexed(count, start uint32, base int32) { + syscall.Syscall6( + c.Vtbl.DrawIndexed, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(count), + uintptr(start), + uintptr(base), + 0, 0, + ) +} + +func (c *DeviceContext) OMSetBlendState(state *BlendState, + factor *f32color.RGBA, sampleMask uint32) { + syscall.Syscall6( + c.Vtbl.OMSetBlendState, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(state)), + uintptr(unsafe.Pointer(factor)), + uintptr(sampleMask), + 0, 0, + ) +} + +func (c *DeviceContext) OMSetDepthStencilState(state *DepthStencilState, + stencilRef uint32) { + syscall.Syscall( + c.Vtbl.OMSetDepthStencilState, + 3, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(state)), + uintptr(stencilRef), + ) +} + +func (d *IDXGIObject) GetParent(guid *GUID) (*IDXGIObject, error) { + var parent *IDXGIObject + r, _, _ := syscall.Syscall( + d.Vtbl.GetParent, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(guid)), + uintptr(unsafe.Pointer(&parent)), + ) + if r != 0 { + return nil, ErrorCode{Name: "IDXGIObjectGetParent", Code: uint32(r)} + } + return parent, nil +} + +func (d *IDXGIFactory) CreateSwapChain(device *IUnknown, + desc *DXGI_SWAP_CHAIN_DESC) (*IDXGISwapChain, error) { + var swchain *IDXGISwapChain + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateSwapChain, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(device)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&swchain)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "IDXGIFactory", Code: uint32(r)} + } + return swchain, nil +} + +func (d *IDXGIDevice) GetAdapter() (*IDXGIAdapter, error) { + var adapter *IDXGIAdapter + r, _, _ := syscall.Syscall( + d.Vtbl.GetAdapter, + 2, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(&adapter)), + 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "IDXGIDeviceGetAdapter", Code: uint32(r)} + } + return adapter, nil +} + +func IUnknownQueryInterface(obj unsafe.Pointer, queryInterfaceMethod uintptr, + guid *GUID) (*IUnknown, error) { + var ref *IUnknown + r, _, _ := syscall.Syscall( + queryInterfaceMethod, + 3, + uintptr(obj), + uintptr(unsafe.Pointer(guid)), + uintptr(unsafe.Pointer(&ref)), + ) + if r != 0 { + return nil, ErrorCode{Name: "IUnknownQueryInterface", Code: uint32(r)} + } + return ref, nil +} + +func IUnknownRelease(obj unsafe.Pointer, releaseMethod uintptr) { + syscall.Syscall( + releaseMethod, + 1, + uintptr(obj), + 0, + 0, + ) +} + +func (e ErrorCode) Error() string { + return fmt.Sprintf("%s: %#x", e.Name, e.Code) +} + +func CreateSwapChain(dev *Device, hwnd windows.Handle) (*IDXGISwapChain, + error) { + dxgiDev, err := IUnknownQueryInterface(unsafe.Pointer(dev), + dev.Vtbl.QueryInterface, &IID_IDXGIDevice) + if err != nil { + return nil, fmt.Errorf("NewContext: %v", err) + } + adapter, err := (*IDXGIDevice)(unsafe.Pointer(dxgiDev)).GetAdapter() + IUnknownRelease(unsafe.Pointer(dxgiDev), dxgiDev.Vtbl.Release) + if err != nil { + return nil, fmt.Errorf("NewContext: %v", err) + } + dxgiFactory, err := (*IDXGIObject)(unsafe.Pointer(adapter)).GetParent(&IID_IDXGIFactory) + IUnknownRelease(unsafe.Pointer(adapter), adapter.Vtbl.Release) + if err != nil { + return nil, fmt.Errorf("NewContext: %v", err) + } + swchain, err := (*IDXGIFactory)(unsafe.Pointer(dxgiFactory)).CreateSwapChain( + (*IUnknown)(unsafe.Pointer(dev)), + &DXGI_SWAP_CHAIN_DESC{ + BufferDesc: DXGI_MODE_DESC{ + Format: DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, + }, + SampleDesc: DXGI_SAMPLE_DESC{ + Count: 1, + }, + BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT, + BufferCount: 1, + OutputWindow: hwnd, + Windowed: 1, + SwapEffect: DXGI_SWAP_EFFECT_DISCARD, + }, + ) + IUnknownRelease(unsafe.Pointer(dxgiFactory), dxgiFactory.Vtbl.Release) + if err != nil { + return nil, fmt.Errorf("NewContext: %v", err) + } + return swchain, nil +} + +func CreateDepthView(d *Device, + width, height, depthBits int) (*DepthStencilView, error) { + depthTex, err := d.CreateTexture2D(&TEXTURE2D_DESC{ + Width: uint32(width), + Height: uint32(height), + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_D24_UNORM_S8_UINT, + SampleDesc: DXGI_SAMPLE_DESC{ + Count: 1, + Quality: 0, + }, + BindFlags: BIND_DEPTH_STENCIL, + }) + if err != nil { + return nil, err + } + depthView, err := d.CreateDepthStencilViewTEX2D( + (*Resource)(unsafe.Pointer(depthTex)), + &DEPTH_STENCIL_VIEW_DESC_TEX2D{ + Format: DXGI_FORMAT_D24_UNORM_S8_UINT, + ViewDimension: DSV_DIMENSION_TEXTURE2D, + }, + ) + IUnknownRelease(unsafe.Pointer(depthTex), depthTex.Vtbl.Release) + return depthView, err +} diff --git a/gio/internal/egl/egl.go b/gio/internal/egl/egl.go new file mode 100644 index 0000000..5a23650 --- /dev/null +++ b/gio/internal/egl/egl.go @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build linux || windows || freebsd || openbsd +// +build linux windows freebsd openbsd + +package egl + +import ( + "errors" + "fmt" + "runtime" + "strings" + + "realy.lol/gio/gpu" + "realy.lol/gio/internal/gl" + "realy.lol/gio/internal/srgb" +) + +type Context struct { + c *gl.Functions + disp _EGLDisplay + eglCtx *eglContext + eglSurf _EGLSurface + width, height int + refreshFBO bool + // For sRGB emulation. + srgbFBO *srgb.FBO +} + +type eglContext struct { + config _EGLConfig + ctx _EGLContext + visualID int + srgb bool + surfaceless bool +} + +var ( + nilEGLDisplay _EGLDisplay + nilEGLSurface _EGLSurface + nilEGLContext _EGLContext + nilEGLConfig _EGLConfig + EGL_DEFAULT_DISPLAY NativeDisplayType +) + +const ( + _EGL_ALPHA_SIZE = 0x3021 + _EGL_BLUE_SIZE = 0x3022 + _EGL_CONFIG_CAVEAT = 0x3027 + _EGL_CONTEXT_CLIENT_VERSION = 0x3098 + _EGL_DEPTH_SIZE = 0x3025 + _EGL_GL_COLORSPACE_KHR = 0x309d + _EGL_GL_COLORSPACE_SRGB_KHR = 0x3089 + _EGL_GREEN_SIZE = 0x3023 + _EGL_EXTENSIONS = 0x3055 + _EGL_NATIVE_VISUAL_ID = 0x302e + _EGL_NONE = 0x3038 + _EGL_OPENGL_ES2_BIT = 0x4 + _EGL_RED_SIZE = 0x3024 + _EGL_RENDERABLE_TYPE = 0x3040 + _EGL_SURFACE_TYPE = 0x3033 + _EGL_WINDOW_BIT = 0x4 +) + +func (c *Context) Release() { + if c.srgbFBO != nil { + c.srgbFBO.Release() + c.srgbFBO = nil + } + c.ReleaseSurface() + if c.eglCtx != nil { + eglDestroyContext(c.disp, c.eglCtx.ctx) + c.eglCtx = nil + } + c.disp = nilEGLDisplay +} + +func (c *Context) Present() error { + if c.srgbFBO != nil { + c.srgbFBO.Blit() + } + if !eglSwapBuffers(c.disp, c.eglSurf) { + return fmt.Errorf("eglSwapBuffers failed (%x)", eglGetError()) + } + if c.srgbFBO != nil { + c.srgbFBO.AfterPresent() + } + return nil +} + +func NewContext(disp NativeDisplayType) (*Context, error) { + if err := loadEGL(); err != nil { + return nil, err + } + eglDisp := eglGetDisplay(disp) + // eglGetDisplay can return EGL_NO_DISPLAY yet no error + // (EGL_SUCCESS), in which case a default EGL display might be + // available. + if eglDisp == nilEGLDisplay { + eglDisp = eglGetDisplay(EGL_DEFAULT_DISPLAY) + } + if eglDisp == nilEGLDisplay { + return nil, fmt.Errorf("eglGetDisplay failed: 0x%x", eglGetError()) + } + eglCtx, err := createContext(eglDisp) + if err != nil { + return nil, err + } + f, err := gl.NewFunctions(nil) + if err != nil { + return nil, err + } + c := &Context{ + disp: eglDisp, + eglCtx: eglCtx, + c: f, + } + return c, nil +} + +func (c *Context) API() gpu.API { + return gpu.OpenGL{} +} + +func (c *Context) ReleaseSurface() { + if c.eglSurf == nilEGLSurface { + return + } + // Make sure any in-flight GL commands are complete. + c.c.Finish() + c.ReleaseCurrent() + eglDestroySurface(c.disp, c.eglSurf) + c.eglSurf = nilEGLSurface +} + +func (c *Context) VisualID() int { + return c.eglCtx.visualID +} + +func (c *Context) CreateSurface(win NativeWindowType, width, height int) error { + eglSurf, err := createSurface(c.disp, c.eglCtx, win) + c.eglSurf = eglSurf + c.width = width + c.height = height + c.refreshFBO = true + return err +} + +func (c *Context) ReleaseCurrent() { + if c.disp != nilEGLDisplay { + eglMakeCurrent(c.disp, nilEGLSurface, nilEGLSurface, nilEGLContext) + } +} + +func (c *Context) MakeCurrent() error { + if c.eglSurf == nilEGLSurface && !c.eglCtx.surfaceless { + return errors.New("no surface created yet EGL_KHR_surfaceless_context is not supported") + } + if !eglMakeCurrent(c.disp, c.eglSurf, c.eglSurf, c.eglCtx.ctx) { + return fmt.Errorf("eglMakeCurrent error 0x%x", eglGetError()) + } + if c.eglCtx.srgb || c.eglSurf == nilEGLSurface { + return nil + } + if c.srgbFBO == nil { + var err error + c.srgbFBO, err = srgb.New(nil) + if err != nil { + c.ReleaseCurrent() + return err + } + } + if c.refreshFBO { + c.refreshFBO = false + if err := c.srgbFBO.Refresh(c.width, c.height); err != nil { + c.ReleaseCurrent() + return err + } + } + return nil +} + +func (c *Context) EnableVSync(enable bool) { + if enable { + eglSwapInterval(c.disp, 1) + } else { + eglSwapInterval(c.disp, 0) + } +} + +func hasExtension(exts []string, ext string) bool { + for _, e := range exts { + if ext == e { + return true + } + } + return false +} + +func createContext(disp _EGLDisplay) (*eglContext, error) { + major, minor, ret := eglInitialize(disp) + if !ret { + return nil, fmt.Errorf("eglInitialize failed: 0x%x", eglGetError()) + } + // sRGB framebuffer support on EGL 1.5 or if EGL_KHR_gl_colorspace is supported. + exts := strings.Split(eglQueryString(disp, _EGL_EXTENSIONS), " ") + srgb := major > 1 || minor >= 5 || hasExtension(exts, + "EGL_KHR_gl_colorspace") + attribs := []_EGLint{ + _EGL_RENDERABLE_TYPE, _EGL_OPENGL_ES2_BIT, + _EGL_SURFACE_TYPE, _EGL_WINDOW_BIT, + _EGL_BLUE_SIZE, 8, + _EGL_GREEN_SIZE, 8, + _EGL_RED_SIZE, 8, + _EGL_CONFIG_CAVEAT, _EGL_NONE, + } + if srgb { + if runtime.GOOS == "linux" || runtime.GOOS == "android" { + // Some Mesa drivers crash if an sRGB framebuffer is requested without alpha. + // https://bugs.freedesktop.org/show_bug.cgi?id=107782. + // + // Also, some Android devices (Samsung S9) needs alpha for sRGB to work. + attribs = append(attribs, _EGL_ALPHA_SIZE, 8) + } + // Only request a depth buffer if we're going to render directly to the framebuffer. + attribs = append(attribs, _EGL_DEPTH_SIZE, 16) + } + attribs = append(attribs, _EGL_NONE) + eglCfg, ret := eglChooseConfig(disp, attribs) + if !ret { + return nil, fmt.Errorf("eglChooseConfig failed: 0x%x", eglGetError()) + } + if eglCfg == nilEGLConfig { + supportsNoCfg := hasExtension(exts, "EGL_KHR_no_config_context") + if !supportsNoCfg { + return nil, errors.New("eglChooseConfig returned no configs") + } + } + var visID _EGLint + if eglCfg != nilEGLConfig { + var ok bool + visID, ok = eglGetConfigAttrib(disp, eglCfg, _EGL_NATIVE_VISUAL_ID) + if !ok { + return nil, errors.New("newContext: eglGetConfigAttrib for _EGL_NATIVE_VISUAL_ID failed") + } + } + ctxAttribs := []_EGLint{ + _EGL_CONTEXT_CLIENT_VERSION, 3, + _EGL_NONE, + } + eglCtx := eglCreateContext(disp, eglCfg, nilEGLContext, ctxAttribs) + if eglCtx == nilEGLContext { + // Fall back to OpenGL ES 2 and rely on extensions. + ctxAttribs := []_EGLint{ + _EGL_CONTEXT_CLIENT_VERSION, 2, + _EGL_NONE, + } + eglCtx = eglCreateContext(disp, eglCfg, nilEGLContext, ctxAttribs) + if eglCtx == nilEGLContext { + return nil, fmt.Errorf("eglCreateContext failed: 0x%x", + eglGetError()) + } + } + return &eglContext{ + config: _EGLConfig(eglCfg), + ctx: _EGLContext(eglCtx), + visualID: int(visID), + srgb: srgb, + surfaceless: hasExtension(exts, "EGL_KHR_surfaceless_context"), + }, nil +} + +func createSurface(disp _EGLDisplay, eglCtx *eglContext, + win NativeWindowType) (_EGLSurface, error) { + var surfAttribs []_EGLint + if eglCtx.srgb { + surfAttribs = append(surfAttribs, _EGL_GL_COLORSPACE_KHR, + _EGL_GL_COLORSPACE_SRGB_KHR) + } + surfAttribs = append(surfAttribs, _EGL_NONE) + eglSurf := eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs) + if eglSurf == nilEGLSurface && eglCtx.srgb { + // Try again without sRGB + eglCtx.srgb = false + surfAttribs = []_EGLint{_EGL_NONE} + eglSurf = eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs) + } + if eglSurf == nilEGLSurface { + return nilEGLSurface, fmt.Errorf("newContext: eglCreateWindowSurface failed 0x%x (sRGB=%v)", + eglGetError(), eglCtx.srgb) + } + return eglSurf, nil +} diff --git a/gio/internal/egl/egl_unix.go b/gio/internal/egl/egl_unix.go new file mode 100644 index 0000000..059dd55 --- /dev/null +++ b/gio/internal/egl/egl_unix.go @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build linux freebsd openbsd + +package egl + +/* +#cgo linux,!android pkg-config: egl +#cgo freebsd openbsd android LDFLAGS: -lEGL +#cgo freebsd CFLAGS: -I/usr/local/include +#cgo freebsd LDFLAGS: -L/usr/local/lib +#cgo openbsd CFLAGS: -I/usr/X11R6/include +#cgo openbsd LDFLAGS: -L/usr/X11R6/lib +#cgo CFLAGS: -DEGL_NO_X11 + +#include +#include +*/ +import "C" + +type ( + _EGLint = C.EGLint + _EGLDisplay = C.EGLDisplay + _EGLConfig = C.EGLConfig + _EGLContext = C.EGLContext + _EGLSurface = C.EGLSurface + NativeDisplayType = C.EGLNativeDisplayType + NativeWindowType = C.EGLNativeWindowType +) + +func loadEGL() error { + return nil +} + +func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) { + var cfg C.EGLConfig + var ncfg C.EGLint + if C.eglChooseConfig(disp, &attribs[0], &cfg, 1, &ncfg) != C.EGL_TRUE { + return nilEGLConfig, false + } + return _EGLConfig(cfg), true +} + +func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext, attribs []_EGLint) _EGLContext { + ctx := C.eglCreateContext(disp, cfg, shareCtx, &attribs[0]) + return _EGLContext(ctx) +} + +func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool { + return C.eglDestroySurface(disp, surf) == C.EGL_TRUE +} + +func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool { + return C.eglDestroyContext(disp, ctx) == C.EGL_TRUE +} + +func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig, attr _EGLint) (_EGLint, bool) { + var val _EGLint + ret := C.eglGetConfigAttrib(disp, cfg, attr, &val) + return val, ret == C.EGL_TRUE +} + +func eglGetError() _EGLint { + return C.eglGetError() +} + +func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) { + var maj, min _EGLint + ret := C.eglInitialize(disp, &maj, &min) + return maj, min, ret == C.EGL_TRUE +} + +func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface, ctx _EGLContext) bool { + return C.eglMakeCurrent(disp, draw, read, ctx) == C.EGL_TRUE +} + +func eglReleaseThread() bool { + return C.eglReleaseThread() == C.EGL_TRUE +} + +func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool { + return C.eglSwapBuffers(disp, surf) == C.EGL_TRUE +} + +func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool { + return C.eglSwapInterval(disp, interval) == C.EGL_TRUE +} + +func eglTerminate(disp _EGLDisplay) bool { + return C.eglTerminate(disp) == C.EGL_TRUE +} + +func eglQueryString(disp _EGLDisplay, name _EGLint) string { + return C.GoString(C.eglQueryString(disp, name)) +} + +func eglGetDisplay(disp NativeDisplayType) _EGLDisplay { + return C.eglGetDisplay(disp) +} + +func eglCreateWindowSurface(disp _EGLDisplay, conf _EGLConfig, win NativeWindowType, attribs []_EGLint) _EGLSurface { + eglSurf := C.eglCreateWindowSurface(disp, conf, win, &attribs[0]) + return eglSurf +} diff --git a/gio/internal/egl/egl_windows.go b/gio/internal/egl/egl_windows.go new file mode 100644 index 0000000..5df5c65 --- /dev/null +++ b/gio/internal/egl/egl_windows.go @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package egl + +import ( + "fmt" + "runtime" + "sync" + "unsafe" + + syscall "golang.org/x/sys/windows" + + "realy.lol/gio/internal/gl" +) + +type ( + _EGLint int32 + _EGLDisplay uintptr + _EGLConfig uintptr + _EGLContext uintptr + _EGLSurface uintptr + NativeDisplayType uintptr + NativeWindowType uintptr +) + +var ( + libEGL = syscall.NewLazyDLL("libEGL.dll") + _eglChooseConfig = libEGL.NewProc("eglChooseConfig") + _eglCreateContext = libEGL.NewProc("eglCreateContext") + _eglCreateWindowSurface = libEGL.NewProc("eglCreateWindowSurface") + _eglDestroyContext = libEGL.NewProc("eglDestroyContext") + _eglDestroySurface = libEGL.NewProc("eglDestroySurface") + _eglGetConfigAttrib = libEGL.NewProc("eglGetConfigAttrib") + _eglGetDisplay = libEGL.NewProc("eglGetDisplay") + _eglGetError = libEGL.NewProc("eglGetError") + _eglInitialize = libEGL.NewProc("eglInitialize") + _eglMakeCurrent = libEGL.NewProc("eglMakeCurrent") + _eglReleaseThread = libEGL.NewProc("eglReleaseThread") + _eglSwapInterval = libEGL.NewProc("eglSwapInterval") + _eglSwapBuffers = libEGL.NewProc("eglSwapBuffers") + _eglTerminate = libEGL.NewProc("eglTerminate") + _eglQueryString = libEGL.NewProc("eglQueryString") +) + +var loadOnce sync.Once + +func loadEGL() error { + var err error + loadOnce.Do(func() { + err = loadDLLs() + }) + return err +} + +func loadDLLs() error { + if err := loadDLL(libEGL, "libEGL.dll"); err != nil { + return err + } + if err := loadDLL(gl.LibGLESv2, "libGLESv2.dll"); err != nil { + return err + } + // d3dcompiler_47.dll is needed internally for shader compilation to function. + return loadDLL(syscall.NewLazyDLL("d3dcompiler_47.dll"), + "d3dcompiler_47.dll") +} + +func loadDLL(dll *syscall.LazyDLL, name string) error { + err := dll.Load() + if err != nil { + return fmt.Errorf("egl: failed to load %s: %v", name, err) + } + return nil +} + +func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) { + var cfg _EGLConfig + var ncfg _EGLint + a := &attribs[0] + r, _, _ := _eglChooseConfig.Call(uintptr(disp), uintptr(unsafe.Pointer(a)), + uintptr(unsafe.Pointer(&cfg)), 1, uintptr(unsafe.Pointer(&ncfg))) + issue34474KeepAlive(a) + return cfg, r != 0 +} + +func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext, + attribs []_EGLint) _EGLContext { + a := &attribs[0] + c, _, _ := _eglCreateContext.Call(uintptr(disp), uintptr(cfg), + uintptr(shareCtx), uintptr(unsafe.Pointer(a))) + issue34474KeepAlive(a) + return _EGLContext(c) +} + +func eglCreateWindowSurface(disp _EGLDisplay, cfg _EGLConfig, + win NativeWindowType, attribs []_EGLint) _EGLSurface { + a := &attribs[0] + s, _, _ := _eglCreateWindowSurface.Call(uintptr(disp), uintptr(cfg), + uintptr(win), uintptr(unsafe.Pointer(a))) + issue34474KeepAlive(a) + return _EGLSurface(s) +} + +func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool { + r, _, _ := _eglDestroySurface.Call(uintptr(disp), uintptr(surf)) + return r != 0 +} + +func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool { + r, _, _ := _eglDestroyContext.Call(uintptr(disp), uintptr(ctx)) + return r != 0 +} + +func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig, + attr _EGLint) (_EGLint, bool) { + var val uintptr + r, _, _ := _eglGetConfigAttrib.Call(uintptr(disp), uintptr(cfg), + uintptr(attr), uintptr(unsafe.Pointer(&val))) + return _EGLint(val), r != 0 +} + +func eglGetDisplay(disp NativeDisplayType) _EGLDisplay { + d, _, _ := _eglGetDisplay.Call(uintptr(disp)) + return _EGLDisplay(d) +} + +func eglGetError() _EGLint { + e, _, _ := _eglGetError.Call() + return _EGLint(e) +} + +func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) { + var maj, min uintptr + r, _, _ := _eglInitialize.Call(uintptr(disp), uintptr(unsafe.Pointer(&maj)), + uintptr(unsafe.Pointer(&min))) + return _EGLint(maj), _EGLint(min), r != 0 +} + +func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface, + ctx _EGLContext) bool { + r, _, _ := _eglMakeCurrent.Call(uintptr(disp), uintptr(draw), uintptr(read), + uintptr(ctx)) + return r != 0 +} + +func eglReleaseThread() bool { + r, _, _ := _eglReleaseThread.Call() + return r != 0 +} + +func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool { + r, _, _ := _eglSwapInterval.Call(uintptr(disp), uintptr(interval)) + return r != 0 +} + +func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool { + r, _, _ := _eglSwapBuffers.Call(uintptr(disp), uintptr(surf)) + return r != 0 +} + +func eglTerminate(disp _EGLDisplay) bool { + r, _, _ := _eglTerminate.Call(uintptr(disp)) + return r != 0 +} + +func eglQueryString(disp _EGLDisplay, name _EGLint) string { + r, _, _ := _eglQueryString.Call(uintptr(disp), uintptr(name)) + return syscall.BytePtrToString((*byte)(unsafe.Pointer(r))) +} + +// issue34474KeepAlive calls runtime.KeepAlive as a +// workaround for golang.org/issue/34474. +func issue34474KeepAlive(v interface{}) { + runtime.KeepAlive(v) +} diff --git a/gio/internal/f32color/rgba.go b/gio/internal/f32color/rgba.go new file mode 100644 index 0000000..eecf018 --- /dev/null +++ b/gio/internal/f32color/rgba.go @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32color + +import ( + "image/color" + "math" +) + +// RGBA is a 32 bit floating point linear premultiplied color space. +type RGBA struct { + R, G, B, A float32 +} + +// Array returns rgba values in a [4]float32 array. +func (rgba RGBA) Array() [4]float32 { + return [4]float32{rgba.R, rgba.G, rgba.B, rgba.A} +} + +// Float32 returns r, g, b, a values. +func (col RGBA) Float32() (r, g, b, a float32) { + return col.R, col.G, col.B, col.A +} + +// SRGBA converts from linear to sRGB color space. +func (col RGBA) SRGB() color.NRGBA { + if col.A == 0 { + return color.NRGBA{} + } + return color.NRGBA{ + R: uint8(linearTosRGB(col.R/col.A)*255 + .5), + G: uint8(linearTosRGB(col.G/col.A)*255 + .5), + B: uint8(linearTosRGB(col.B/col.A)*255 + .5), + A: uint8(col.A*255 + .5), + } +} + +// Luminance calculates the relative luminance of a linear RGBA color. +// Normalized to 0 for black and 1 for white. +// +// See https://www.w3.org/TR/WCAG20/#relativeluminancedef for more details +func (col RGBA) Luminance() float32 { + return 0.2126*col.R + 0.7152*col.G + 0.0722*col.B +} + +// Opaque returns the color without alpha component. +func (col RGBA) Opaque() RGBA { + col.A = 1.0 + return col +} + +// LinearFromSRGB converts from col in the sRGB colorspace to RGBA. +func LinearFromSRGB(col color.NRGBA) RGBA { + af := float32(col.A) / 0xFF + return RGBA{ + R: sRGBToLinear(float32(col.R)/0xff) * af, + G: sRGBToLinear(float32(col.G)/0xff) * af, + B: sRGBToLinear(float32(col.B)/0xff) * af, + A: af, + } +} + +// NRGBAToRGBA converts from non-premultiplied sRGB color to premultiplied sRGB color. +// +// Each component in the result is `sRGBToLinear(c * alpha)`, where `c` +// is the linear color. +func NRGBAToRGBA(col color.NRGBA) color.RGBA { + if col.A == 0xFF { + return color.RGBA(col) + } + c := LinearFromSRGB(col) + return color.RGBA{ + R: uint8(linearTosRGB(c.R)*255 + .5), + G: uint8(linearTosRGB(c.G)*255 + .5), + B: uint8(linearTosRGB(c.B)*255 + .5), + A: col.A, + } +} + +// NRGBAToLinearRGBA converts from non-premultiplied sRGB color to premultiplied linear RGBA color. +// +// Each component in the result is `c * alpha`, where `c` is the linear color. +func NRGBAToLinearRGBA(col color.NRGBA) color.RGBA { + if col.A == 0xFF { + return color.RGBA(col) + } + c := LinearFromSRGB(col) + return color.RGBA{ + R: uint8(c.R*255 + .5), + G: uint8(c.G*255 + .5), + B: uint8(c.B*255 + .5), + A: col.A, + } +} + +// RGBAToNRGBA converts from premultiplied sRGB color to non-premultiplied sRGB color. +func RGBAToNRGBA(col color.RGBA) color.NRGBA { + if col.A == 0xFF { + return color.NRGBA(col) + } + + linear := RGBA{ + R: sRGBToLinear(float32(col.R) / 0xff), + G: sRGBToLinear(float32(col.G) / 0xff), + B: sRGBToLinear(float32(col.B) / 0xff), + A: float32(col.A) / 0xff, + } + + return linear.SRGB() +} + +// linearTosRGB transforms color value from linear to sRGB. +func linearTosRGB(c float32) float32 { + // Formula from EXT_sRGB. + switch { + case c <= 0: + return 0 + case 0 < c && c < 0.0031308: + return 12.92 * c + case 0.0031308 <= c && c < 1: + return 1.055*float32(math.Pow(float64(c), 0.41666)) - 0.055 + } + + return 1 +} + +// sRGBToLinear transforms color value from sRGB to linear. +func sRGBToLinear(c float32) float32 { + // Formula from EXT_sRGB. + if c <= 0.04045 { + return c / 12.92 + } else { + return float32(math.Pow(float64((c+0.055)/1.055), 2.4)) + } +} + +// MulAlpha applies the alpha to the color. +func MulAlpha(c color.NRGBA, alpha uint8) color.NRGBA { + c.A = uint8(uint32(c.A) * uint32(alpha) / 0xFF) + return c +} + +// Disabled blends color towards the luminance and multiplies alpha. +// Blending towards luminance will desaturate the color. +// Multiplying alpha blends the color together more with the background. +func Disabled(c color.NRGBA) (d color.NRGBA) { + const r = 80 // blend ratio + lum := approxLuminance(c) + return color.NRGBA{ + R: byte((int(c.R)*r + int(lum)*(256-r)) / 256), + G: byte((int(c.G)*r + int(lum)*(256-r)) / 256), + B: byte((int(c.B)*r + int(lum)*(256-r)) / 256), + A: byte(int(c.A) * (128 + 32) / 256), + } +} + +// Hovered blends color towards a brighter color. +func Hovered(c color.NRGBA) (d color.NRGBA) { + const r = 0x20 // lighten ratio + return color.NRGBA{ + R: byte(255 - int(255-c.R)*(255-r)/256), + G: byte(255 - int(255-c.G)*(255-r)/256), + B: byte(255 - int(255-c.B)*(255-r)/256), + A: c.A, + } +} + +// approxLuminance is a fast approximate version of RGBA.Luminance. +func approxLuminance(c color.NRGBA) byte { + const ( + r = 13933 // 0.2126 * 256 * 256 + g = 46871 // 0.7152 * 256 * 256 + b = 4732 // 0.0722 * 256 * 256 + t = r + g + b + ) + return byte((r*int(c.R) + g*int(c.G) + b*int(c.B)) / t) +} diff --git a/gio/internal/f32color/rgba_test.go b/gio/internal/f32color/rgba_test.go new file mode 100644 index 0000000..ea0f871 --- /dev/null +++ b/gio/internal/f32color/rgba_test.go @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32color + +import ( + "image/color" + "testing" +) + +func TestNRGBAToLinearRGBA_Boundary(t *testing.T) { + for col := 0; col <= 0xFF; col++ { + for alpha := 0; alpha <= 0xFF; alpha++ { + in := color.NRGBA{R: uint8(col), A: uint8(alpha)} + premul := NRGBAToLinearRGBA(in) + if premul.A != uint8(alpha) { + t.Errorf("%v: got %v expected %v", in, premul.A, alpha) + } + if premul.R > premul.A { + t.Errorf("%v: R=%v > A=%v", in, premul.R, premul.A) + } + } + } +} + +func TestLinearToRGBARoundtrip(t *testing.T) { + for col := 0; col <= 0xFF; col++ { + for alpha := 0; alpha <= 0xFF; alpha++ { + want := color.NRGBA{R: uint8(col), A: uint8(alpha)} + if alpha == 0 { + want.R = 0 + } + got := LinearFromSRGB(want).SRGB() + if want != got { + t.Errorf("got %v expected %v", got, want) + } + } + } +} diff --git a/gio/internal/fling/animation.go b/gio/internal/fling/animation.go new file mode 100644 index 0000000..82a2b8e --- /dev/null +++ b/gio/internal/fling/animation.go @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package fling + +import ( + "math" + "runtime" + "time" + + "realy.lol/gio/unit" +) + +type Animation struct { + // Current offset in pixels. + x float32 + // Initial time. + t0 time.Time + // Initial velocity in pixels pr second. + v0 float32 +} + +var ( + // Pixels/second. + minFlingVelocity = unit.Dp(50) + maxFlingVelocity = unit.Dp(8000) +) + +const ( + thresholdVelocity = 1 +) + +// Start a fling given a starting velocity. Returns whether a +// fling was started. +func (f *Animation) Start(c unit.Metric, now time.Time, velocity float32) bool { + min := float32(c.Px(minFlingVelocity)) + v := velocity + if -min <= v && v <= min { + return false + } + max := float32(c.Px(maxFlingVelocity)) + if v > max { + v = max + } else if v < -max { + v = -max + } + f.init(now, v) + return true +} + +func (f *Animation) init(now time.Time, v0 float32) { + f.t0 = now + f.v0 = v0 + f.x = 0 +} + +func (f *Animation) Active() bool { + return f.v0 != 0 +} + +// Tick computes and returns a fling distance since +// the last time Tick was called. +func (f *Animation) Tick(now time.Time) int { + if !f.Active() { + return 0 + } + var k float32 + if runtime.GOOS == "darwin" { + k = -2 // iOS + } else { + k = -4.2 // Android and default + } + t := now.Sub(f.t0) + // The acceleration x''(t) of a point mass with a drag + // force, f, proportional with velocity, x'(t), is + // governed by the equation + // + // x''(t) = kx'(t) + // + // Given the starting position x(0) = 0, the starting + // velocity x'(0) = v0, the position is then + // given by + // + // x(t) = v0*e^(k*t)/k - v0/k + // + ekt := float32(math.Exp(float64(k) * t.Seconds())) + x := f.v0*ekt/k - f.v0/k + dist := x - f.x + idist := int(dist) + f.x += float32(idist) + // Solving for the velocity x'(t) gives us + // + // x'(t) = v0*e^(k*t) + v := f.v0 * ekt + if -thresholdVelocity < v && v < thresholdVelocity { + f.v0 = 0 + } + return idist +} diff --git a/gio/internal/fling/extrapolation.go b/gio/internal/fling/extrapolation.go new file mode 100644 index 0000000..655ef84 --- /dev/null +++ b/gio/internal/fling/extrapolation.go @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package fling + +import ( + "math" + "strconv" + "strings" + "time" +) + +// Extrapolation computes a 1-dimensional velocity estimate +// for a set of timestamped points using the least squares +// fit of a 2nd order polynomial. The same method is used +// by Android. +type Extrapolation struct { + // Index into points. + idx int + // Circular buffer of samples. + samples []sample + lastValue float32 + // Pre-allocated cache for samples. + cache [historySize]sample + + // Filtered values and times + values [historySize]float32 + times [historySize]float32 +} + +type sample struct { + t time.Duration + v float32 +} + +type matrix struct { + rows, cols int + data []float32 +} + +type Estimate struct { + Velocity float32 + Distance float32 +} + +type coefficients [degree + 1]float32 + +const ( + degree = 2 + historySize = 20 + maxAge = 100 * time.Millisecond + maxSampleGap = 40 * time.Millisecond +) + +// SampleDelta adds a relative sample to the estimation. +func (e *Extrapolation) SampleDelta(t time.Duration, delta float32) { + val := delta + e.lastValue + e.Sample(t, val) +} + +// Sample adds an absolute sample to the estimation. +func (e *Extrapolation) Sample(t time.Duration, val float32) { + e.lastValue = val + if e.samples == nil { + e.samples = e.cache[:0] + } + s := sample{ + t: t, + v: val, + } + if e.idx == len(e.samples) && e.idx < cap(e.samples) { + e.samples = append(e.samples, s) + } else { + e.samples[e.idx] = s + } + e.idx++ + if e.idx == cap(e.samples) { + e.idx = 0 + } +} + +// Velocity returns an estimate of the implied velocity and +// distance for the points sampled, or zero if the estimation method +// failed. +func (e *Extrapolation) Estimate() Estimate { + if len(e.samples) == 0 { + return Estimate{} + } + values := e.values[:0] + times := e.times[:0] + first := e.get(0) + t := first.t + // Walk backwards collecting samples. + for i := 0; i < len(e.samples); i++ { + p := e.get(-i) + age := first.t - p.t + if age >= maxAge || t-p.t >= maxSampleGap { + // If the samples are too old or + // too much time passed between samples + // assume they're not part of the fling. + break + } + t = p.t + values = append(values, first.v-p.v) + times = append(times, float32((-age).Seconds())) + } + coef, ok := polyFit(times, values) + if !ok { + return Estimate{} + } + dist := values[len(values)-1] - values[0] + return Estimate{ + Velocity: coef[1], + Distance: dist, + } +} + +func (e *Extrapolation) get(i int) sample { + idx := (e.idx + i - 1 + len(e.samples)) % len(e.samples) + return e.samples[idx] +} + +// fit computes the least squares polynomial fit for +// the set of points in X, Y. If the fitting fails +// because of contradicting or insufficient data, +// fit returns false. +func polyFit(X, Y []float32) (coefficients, bool) { + if len(X) != len(Y) { + panic("X and Y lengths differ") + } + if len(X) <= degree { + // Not enough points to fit a curve. + return coefficients{}, false + } + + // Use a method similar to Android's VelocityTracker.cpp: + // https://android.googlesource.com/platform/frameworks/base/+/56a2301/libs/androidfw/VelocityTracker.cpp + // where all weights are 1. + + // First, expand the X vector to the matrix A in column-major order. + A := newMatrix(degree+1, len(X)) + for i, x := range X { + A.set(0, i, 1) + for j := 1; j < A.rows; j++ { + A.set(j, i, A.get(j-1, i)*x) + } + } + + Q, Rt, ok := decomposeQR(A) + if !ok { + return coefficients{}, false + } + // Solve R*B = Qt*Y for B, which is then the polynomial coefficients. + // Since R is upper triangular, we can proceed from bottom right to + // upper left. + // https://en.wikipedia.org/wiki/Non-linear_least_squares + var B coefficients + for i := Q.rows - 1; i >= 0; i-- { + B[i] = dot(Q.col(i), Y) + for j := Q.rows - 1; j > i; j-- { + B[i] -= Rt.get(i, j) * B[j] + } + B[i] /= Rt.get(i, i) + } + return B, true +} + +// decomposeQR computes and returns Q, Rt where Q*transpose(Rt) = A, if +// possible. R is guaranteed to be upper triangular and only the square +// part of Rt is returned. +func decomposeQR(A *matrix) (*matrix, *matrix, bool) { + // Gram-Schmidt QR decompose A where Q*R = A. + // https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process + Q := newMatrix(A.rows, A.cols) // Column-major. + Rt := newMatrix(A.rows, A.rows) // R transposed, row-major. + for i := 0; i < Q.rows; i++ { + // Copy A column. + for j := 0; j < Q.cols; j++ { + Q.set(i, j, A.get(i, j)) + } + // Subtract projections. Note that int the projection + // + // proju a = / u + // + // the normalized column e replaces u, where = 1: + // + // proje a = / e = e + for j := 0; j < i; j++ { + d := dot(Q.col(j), Q.col(i)) + for k := 0; k < Q.cols; k++ { + Q.set(i, k, Q.get(i, k)-d*Q.get(j, k)) + } + } + // Normalize Q columns. + n := norm(Q.col(i)) + if n < 0.000001 { + // Degenerate data, no solution. + return nil, nil, false + } + invNorm := 1 / n + for j := 0; j < Q.cols; j++ { + Q.set(i, j, Q.get(i, j)*invNorm) + } + // Update Rt. + for j := i; j < Rt.cols; j++ { + Rt.set(i, j, dot(Q.col(i), A.col(j))) + } + } + return Q, Rt, true +} + +func norm(V []float32) float32 { + var n float32 + for _, v := range V { + n += v * v + } + return float32(math.Sqrt(float64(n))) +} + +func dot(V1, V2 []float32) float32 { + var d float32 + for i, v1 := range V1 { + d += v1 * V2[i] + } + return d +} + +func newMatrix(rows, cols int) *matrix { + return &matrix{ + rows: rows, + cols: cols, + data: make([]float32, rows*cols), + } +} + +func (m *matrix) set(row, col int, v float32) { + if row < 0 || row >= m.rows { + panic("row out of range") + } + if col < 0 || col >= m.cols { + panic("col out of range") + } + m.data[row*m.cols+col] = v +} + +func (m *matrix) get(row, col int) float32 { + if row < 0 || row >= m.rows { + panic("row out of range") + } + if col < 0 || col >= m.cols { + panic("col out of range") + } + return m.data[row*m.cols+col] +} + +func (m *matrix) col(c int) []float32 { + return m.data[c*m.cols : (c+1)*m.cols] +} + +func (m *matrix) approxEqual(m2 *matrix) bool { + if m.rows != m2.rows || m.cols != m2.cols { + return false + } + const epsilon = 0.00001 + for row := 0; row < m.rows; row++ { + for col := 0; col < m.cols; col++ { + d := m2.get(row, col) - m.get(row, col) + if d < -epsilon || d > epsilon { + return false + } + } + } + return true +} + +func (m *matrix) transpose() *matrix { + t := &matrix{ + rows: m.cols, + cols: m.rows, + data: make([]float32, len(m.data)), + } + for i := 0; i < m.rows; i++ { + for j := 0; j < m.cols; j++ { + t.set(j, i, m.get(i, j)) + } + } + return t +} + +func (m *matrix) mul(m2 *matrix) *matrix { + if m.rows != m2.cols { + panic("mismatched matrices") + } + mm := &matrix{ + rows: m.rows, + cols: m2.cols, + data: make([]float32, m.rows*m2.cols), + } + for i := 0; i < mm.rows; i++ { + for j := 0; j < mm.cols; j++ { + var v float32 + for k := 0; k < m.rows; k++ { + v += m.get(k, j) * m2.get(i, k) + } + mm.set(i, j, v) + } + } + return mm +} + +func (m *matrix) String() string { + var b strings.Builder + for i := 0; i < m.rows; i++ { + for j := 0; j < m.cols; j++ { + v := m.get(i, j) + b.WriteString(strconv.FormatFloat(float64(v), 'g', -1, 32)) + b.WriteString(", ") + } + b.WriteString("\n") + } + return b.String() +} + +func (c coefficients) approxEqual(c2 coefficients) bool { + const epsilon = 0.00001 + for i, v := range c { + d := v - c2[i] + if d < -epsilon || d > epsilon { + return false + } + } + return true +} diff --git a/gio/internal/fling/extrapolation_test.go b/gio/internal/fling/extrapolation_test.go new file mode 100644 index 0000000..3f9d982 --- /dev/null +++ b/gio/internal/fling/extrapolation_test.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package fling + +import "testing" + +func TestDecomposeQR(t *testing.T) { + A := &matrix{ + rows: 3, cols: 3, + data: []float32{ + 12, 6, -4, + -51, 167, 24, + 4, -68, -41, + }, + } + Q, Rt, ok := decomposeQR(A) + if !ok { + t.Fatal("decomposeQR failed") + } + R := Rt.transpose() + QR := Q.mul(R) + if !A.approxEqual(QR) { + t.Log("A\n", A) + t.Log("Q\n", Q) + t.Log("R\n", R) + t.Log("QR\n", QR) + t.Fatal("Q*R not approximately equal to A") + } +} + +func TestFit(t *testing.T) { + X := []float32{-1, 0, 1} + Y := []float32{2, 0, 2} + + got, ok := polyFit(X, Y) + if !ok { + t.Fatal("polyFit failed") + } + want := coefficients{0, 0, 2} + if !got.approxEqual(want) { + t.Fatalf("polyFit: got %v want %v", got, want) + } +} diff --git a/gio/internal/gl/gl.go b/gio/internal/gl/gl.go new file mode 100644 index 0000000..9696c71 --- /dev/null +++ b/gio/internal/gl/gl.go @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +type ( + Attrib uint + Enum uint +) + +const ( + ALL_BARRIER_BITS = 0xffffffff + ARRAY_BUFFER = 0x8892 + BLEND = 0xbe2 + CLAMP_TO_EDGE = 0x812f + COLOR_ATTACHMENT0 = 0x8ce0 + COLOR_BUFFER_BIT = 0x4000 + COMPILE_STATUS = 0x8b81 + COMPUTE_SHADER = 0x91B9 + DEPTH_BUFFER_BIT = 0x100 + DEPTH_ATTACHMENT = 0x8d00 + DEPTH_COMPONENT16 = 0x81a5 + DEPTH_COMPONENT24 = 0x81A6 + DEPTH_COMPONENT32F = 0x8CAC + DEPTH_TEST = 0xb71 + DRAW_FRAMEBUFFER = 0x8CA9 + DST_COLOR = 0x306 + DYNAMIC_DRAW = 0x88E8 + DYNAMIC_READ = 0x88E9 + ELEMENT_ARRAY_BUFFER = 0x8893 + EXTENSIONS = 0x1f03 + FALSE = 0 + FLOAT = 0x1406 + FRAGMENT_SHADER = 0x8b30 + FRAMEBUFFER = 0x8d40 + FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING = 0x8210 + FRAMEBUFFER_BINDING = 0x8ca6 + FRAMEBUFFER_COMPLETE = 0x8cd5 + HALF_FLOAT = 0x140b + HALF_FLOAT_OES = 0x8d61 + INFO_LOG_LENGTH = 0x8B84 + INVALID_INDEX = ^uint(0) + GREATER = 0x204 + GEQUAL = 0x206 + LINEAR = 0x2601 + LINK_STATUS = 0x8b82 + LUMINANCE = 0x1909 + MAP_READ_BIT = 0x0001 + MAX_TEXTURE_SIZE = 0xd33 + NEAREST = 0x2600 + NO_ERROR = 0x0 + NUM_EXTENSIONS = 0x821D + ONE = 0x1 + ONE_MINUS_SRC_ALPHA = 0x303 + PROGRAM_BINARY_LENGTH = 0x8741 + QUERY_RESULT = 0x8866 + QUERY_RESULT_AVAILABLE = 0x8867 + R16F = 0x822d + R8 = 0x8229 + READ_FRAMEBUFFER = 0x8ca8 + READ_ONLY = 0x88B8 + READ_WRITE = 0x88BA + RED = 0x1903 + RENDERER = 0x1F01 + RENDERBUFFER = 0x8d41 + RENDERBUFFER_BINDING = 0x8ca7 + RENDERBUFFER_HEIGHT = 0x8d43 + RENDERBUFFER_WIDTH = 0x8d42 + RGB = 0x1907 + RGBA = 0x1908 + RGBA8 = 0x8058 + SHADER_STORAGE_BUFFER = 0x90D2 + SHORT = 0x1402 + SRGB = 0x8c40 + SRGB_ALPHA_EXT = 0x8c42 + SRGB8 = 0x8c41 + SRGB8_ALPHA8 = 0x8c43 + STATIC_DRAW = 0x88e4 + STENCIL_BUFFER_BIT = 0x00000400 + TEXTURE_2D = 0xde1 + TEXTURE_MAG_FILTER = 0x2800 + TEXTURE_MIN_FILTER = 0x2801 + TEXTURE_WRAP_S = 0x2802 + TEXTURE_WRAP_T = 0x2803 + TEXTURE0 = 0x84c0 + TEXTURE1 = 0x84c1 + TRIANGLE_STRIP = 0x5 + TRIANGLES = 0x4 + TRUE = 1 + UNIFORM_BUFFER = 0x8A11 + UNPACK_ALIGNMENT = 0xcf5 + UNSIGNED_BYTE = 0x1401 + UNSIGNED_SHORT = 0x1403 + VERSION = 0x1f02 + VERTEX_SHADER = 0x8b31 + WRITE_ONLY = 0x88B9 + ZERO = 0x0 + + // EXT_disjoint_timer_query + TIME_ELAPSED_EXT = 0x88BF + GPU_DISJOINT_EXT = 0x8FBB +) + +var _ interface { + ActiveTexture(texture Enum) + AttachShader(p Program, s Shader) + BeginQuery(target Enum, query Query) + BindAttribLocation(p Program, a Attrib, name string) + BindBuffer(target Enum, b Buffer) + BindBufferBase(target Enum, index int, buffer Buffer) + BindFramebuffer(target Enum, fb Framebuffer) + BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) + BindRenderbuffer(target Enum, fb Renderbuffer) + BindTexture(target Enum, t Texture) + BlendEquation(mode Enum) + BlendFunc(sfactor, dfactor Enum) + BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) + BufferData(target Enum, size int, usage Enum) + BufferSubData(target Enum, offset int, src []byte) + CheckFramebufferStatus(target Enum) Enum + Clear(mask Enum) + ClearColor(red, green, blue, alpha float32) + ClearDepthf(d float32) + CompileShader(s Shader) + CreateBuffer() Buffer + CreateFramebuffer() Framebuffer + CreateProgram() Program + CreateQuery() Query + CreateRenderbuffer() Renderbuffer + CreateShader(ty Enum) Shader + CreateTexture() Texture + DeleteBuffer(v Buffer) + DeleteFramebuffer(v Framebuffer) + DeleteProgram(p Program) + DeleteQuery(query Query) + DeleteRenderbuffer(r Renderbuffer) + DeleteShader(s Shader) + DeleteTexture(v Texture) + DepthFunc(f Enum) + DepthMask(mask bool) + DisableVertexAttribArray(a Attrib) + Disable(cap Enum) + DispatchCompute(x, y, z int) + DrawArrays(mode Enum, first, count int) + DrawElements(mode Enum, count int, ty Enum, offset int) + Enable(cap Enum) + EnableVertexAttribArray(a Attrib) + EndQuery(target Enum) + FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) + FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) + GetBinding(pname Enum) Object + GetError() Enum + GetInteger(pname Enum) int + GetProgrami(p Program, pname Enum) int + GetProgramInfoLog(p Program) string + GetQueryObjectuiv(query Query, pname Enum) uint + GetShaderi(s Shader, pname Enum) int + GetShaderInfoLog(s Shader) string + GetString(pname Enum) string + GetUniformBlockIndex(p Program, name string) uint + GetUniformLocation(p Program, name string) Uniform + InvalidateFramebuffer(target, attachment Enum) + LinkProgram(p Program) + MapBufferRange(target Enum, offset, length int, access Enum) []byte + MemoryBarrier(barriers Enum) + ReadPixels(x, y, width, height int, format, ty Enum, data []byte) + RenderbufferStorage(target, internalformat Enum, width, height int) + ShaderSource(s Shader, src string) + TexImage2D(target Enum, level int, internalFormat Enum, width, height int, format, ty Enum) + TexParameteri(target, pname Enum, param int) + TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) + TexSubImage2D(target Enum, level, xoff, yoff int, width, height int, format, ty Enum, data []byte) + UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) + Uniform1f(dst Uniform, v float32) + Uniform1i(dst Uniform, v int) + Uniform2f(dst Uniform, v0, v1 float32) + Uniform3f(dst Uniform, v0, v1, v2 float32) + Uniform4f(dst Uniform, v0, v1, v2, v3 float32) + UseProgram(p Program) + UnmapBuffer(target Enum) bool + VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) + Viewport(x, y, width, height int) +} = (*Functions)(nil) diff --git a/gio/internal/gl/gl_js.go b/gio/internal/gl/gl_js.go new file mode 100644 index 0000000..13890a7 --- /dev/null +++ b/gio/internal/gl/gl_js.go @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import ( + "errors" + "strings" + "syscall/js" +) + +type Functions struct { + Ctx js.Value + EXT_disjoint_timer_query js.Value + EXT_disjoint_timer_query_webgl2 js.Value + + // Cached reference to the Uint8Array JS type. + uint8Array js.Value + + // Cached JS arrays. + arrayBuf js.Value + int32Buf js.Value +} + +type Context js.Value + +func NewFunctions(ctx Context) (*Functions, error) { + f := &Functions{ + Ctx: js.Value(ctx), + uint8Array: js.Global().Get("Uint8Array"), + } + if err := f.Init(); err != nil { + return nil, err + } + return f, nil +} + +func (f *Functions) Init() error { + webgl2Class := js.Global().Get("WebGL2RenderingContext") + iswebgl2 := !webgl2Class.IsUndefined() && f.Ctx.InstanceOf(webgl2Class) + if !iswebgl2 { + f.EXT_disjoint_timer_query = f.getExtension("EXT_disjoint_timer_query") + if f.getExtension("OES_texture_half_float").IsNull() && f.getExtension("OES_texture_float").IsNull() { + return errors.New("gl: no support for neither OES_texture_half_float nor OES_texture_float") + } + if f.getExtension("EXT_sRGB").IsNull() { + return errors.New("gl: EXT_sRGB not supported") + } + } else { + // WebGL2 extensions. + f.EXT_disjoint_timer_query_webgl2 = f.getExtension("EXT_disjoint_timer_query_webgl2") + if f.getExtension("EXT_color_buffer_half_float").IsNull() && f.getExtension("EXT_color_buffer_float").IsNull() { + return errors.New("gl: no support for neither EXT_color_buffer_half_float nor EXT_color_buffer_float") + } + } + return nil +} + +func (f *Functions) getExtension(name string) js.Value { + return f.Ctx.Call("getExtension", name) +} + +func (f *Functions) ActiveTexture(t Enum) { + f.Ctx.Call("activeTexture", int(t)) +} +func (f *Functions) AttachShader(p Program, s Shader) { + f.Ctx.Call("attachShader", js.Value(p), js.Value(s)) +} +func (f *Functions) BeginQuery(target Enum, query Query) { + if !f.EXT_disjoint_timer_query_webgl2.IsNull() { + f.Ctx.Call("beginQuery", int(target), js.Value(query)) + } else { + f.EXT_disjoint_timer_query.Call("beginQueryEXT", int(target), js.Value(query)) + } +} +func (f *Functions) BindAttribLocation(p Program, a Attrib, name string) { + f.Ctx.Call("bindAttribLocation", js.Value(p), int(a), name) +} +func (f *Functions) BindBuffer(target Enum, b Buffer) { + f.Ctx.Call("bindBuffer", int(target), js.Value(b)) +} +func (f *Functions) BindBufferBase(target Enum, index int, b Buffer) { + f.Ctx.Call("bindBufferBase", int(target), index, js.Value(b)) +} +func (f *Functions) BindFramebuffer(target Enum, fb Framebuffer) { + f.Ctx.Call("bindFramebuffer", int(target), js.Value(fb)) +} +func (f *Functions) BindRenderbuffer(target Enum, rb Renderbuffer) { + f.Ctx.Call("bindRenderbuffer", int(target), js.Value(rb)) +} +func (f *Functions) BindTexture(target Enum, t Texture) { + f.Ctx.Call("bindTexture", int(target), js.Value(t)) +} +func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) { + panic("not implemented") +} +func (f *Functions) BlendEquation(mode Enum) { + f.Ctx.Call("blendEquation", int(mode)) +} +func (f *Functions) BlendFunc(sfactor, dfactor Enum) { + f.Ctx.Call("blendFunc", int(sfactor), int(dfactor)) +} +func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) { + panic("not implemented") +} +func (f *Functions) BufferData(target Enum, size int, usage Enum) { + f.Ctx.Call("bufferData", int(target), size, int(usage)) +} +func (f *Functions) BufferSubData(target Enum, offset int, src []byte) { + f.Ctx.Call("bufferSubData", int(target), offset, f.byteArrayOf(src)) +} +func (f *Functions) CheckFramebufferStatus(target Enum) Enum { + return Enum(f.Ctx.Call("checkFramebufferStatus", int(target)).Int()) +} +func (f *Functions) Clear(mask Enum) { + f.Ctx.Call("clear", int(mask)) +} +func (f *Functions) ClearColor(red, green, blue, alpha float32) { + f.Ctx.Call("clearColor", red, green, blue, alpha) +} +func (f *Functions) ClearDepthf(d float32) { + f.Ctx.Call("clearDepth", d) +} +func (f *Functions) CompileShader(s Shader) { + f.Ctx.Call("compileShader", js.Value(s)) +} +func (f *Functions) CreateBuffer() Buffer { + return Buffer(f.Ctx.Call("createBuffer")) +} +func (f *Functions) CreateFramebuffer() Framebuffer { + return Framebuffer(f.Ctx.Call("createFramebuffer")) +} +func (f *Functions) CreateProgram() Program { + return Program(f.Ctx.Call("createProgram")) +} +func (f *Functions) CreateQuery() Query { + return Query(f.Ctx.Call("createQuery")) +} +func (f *Functions) CreateRenderbuffer() Renderbuffer { + return Renderbuffer(f.Ctx.Call("createRenderbuffer")) +} +func (f *Functions) CreateShader(ty Enum) Shader { + return Shader(f.Ctx.Call("createShader", int(ty))) +} +func (f *Functions) CreateTexture() Texture { + return Texture(f.Ctx.Call("createTexture")) +} +func (f *Functions) DeleteBuffer(v Buffer) { + f.Ctx.Call("deleteBuffer", js.Value(v)) +} +func (f *Functions) DeleteFramebuffer(v Framebuffer) { + f.Ctx.Call("deleteFramebuffer", js.Value(v)) +} +func (f *Functions) DeleteProgram(p Program) { + f.Ctx.Call("deleteProgram", js.Value(p)) +} +func (f *Functions) DeleteQuery(query Query) { + if !f.EXT_disjoint_timer_query_webgl2.IsNull() { + f.Ctx.Call("deleteQuery", js.Value(query)) + } else { + f.EXT_disjoint_timer_query.Call("deleteQueryEXT", js.Value(query)) + } +} +func (f *Functions) DeleteShader(s Shader) { + f.Ctx.Call("deleteShader", js.Value(s)) +} +func (f *Functions) DeleteRenderbuffer(v Renderbuffer) { + f.Ctx.Call("deleteRenderbuffer", js.Value(v)) +} +func (f *Functions) DeleteTexture(v Texture) { + f.Ctx.Call("deleteTexture", js.Value(v)) +} +func (f *Functions) DepthFunc(fn Enum) { + f.Ctx.Call("depthFunc", int(fn)) +} +func (f *Functions) DepthMask(mask bool) { + f.Ctx.Call("depthMask", mask) +} +func (f *Functions) DisableVertexAttribArray(a Attrib) { + f.Ctx.Call("disableVertexAttribArray", int(a)) +} +func (f *Functions) Disable(cap Enum) { + f.Ctx.Call("disable", int(cap)) +} +func (f *Functions) DrawArrays(mode Enum, first, count int) { + f.Ctx.Call("drawArrays", int(mode), first, count) +} +func (f *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { + f.Ctx.Call("drawElements", int(mode), count, int(ty), offset) +} +func (f *Functions) DispatchCompute(x, y, z int) { + panic("not implemented") +} +func (f *Functions) Enable(cap Enum) { + f.Ctx.Call("enable", int(cap)) +} +func (f *Functions) EnableVertexAttribArray(a Attrib) { + f.Ctx.Call("enableVertexAttribArray", int(a)) +} +func (f *Functions) EndQuery(target Enum) { + if !f.EXT_disjoint_timer_query_webgl2.IsNull() { + f.Ctx.Call("endQuery", int(target)) + } else { + f.EXT_disjoint_timer_query.Call("endQueryEXT", int(target)) + } +} +func (f *Functions) Finish() { + f.Ctx.Call("finish") +} +func (f *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { + f.Ctx.Call("framebufferRenderbuffer", int(target), int(attachment), int(renderbuffertarget), js.Value(renderbuffer)) +} +func (f *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { + f.Ctx.Call("framebufferTexture2D", int(target), int(attachment), int(texTarget), js.Value(t), level) +} +func (f *Functions) GetError() Enum { + // Avoid slow getError calls. See gio#179. + return 0 +} +func (f *Functions) GetRenderbufferParameteri(target, pname Enum) int { + return paramVal(f.Ctx.Call("getRenderbufferParameteri", int(pname))) +} +func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { + return paramVal(f.Ctx.Call("getFramebufferAttachmentParameter", int(target), int(attachment), int(pname))) +} +func (f *Functions) GetBinding(pname Enum) Object { + return Object(f.Ctx.Call("getParameter", int(pname))) +} +func (f *Functions) GetInteger(pname Enum) int { + return paramVal(f.Ctx.Call("getParameter", int(pname))) +} +func (f *Functions) GetProgrami(p Program, pname Enum) int { + return paramVal(f.Ctx.Call("getProgramParameter", js.Value(p), int(pname))) +} +func (f *Functions) GetProgramInfoLog(p Program) string { + return f.Ctx.Call("getProgramInfoLog", js.Value(p)).String() +} +func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { + if !f.EXT_disjoint_timer_query_webgl2.IsNull() { + return uint(paramVal(f.Ctx.Call("getQueryParameter", js.Value(query), int(pname)))) + } else { + return uint(paramVal(f.EXT_disjoint_timer_query.Call("getQueryObjectEXT", js.Value(query), int(pname)))) + } +} +func (f *Functions) GetShaderi(s Shader, pname Enum) int { + return paramVal(f.Ctx.Call("getShaderParameter", js.Value(s), int(pname))) +} +func (f *Functions) GetShaderInfoLog(s Shader) string { + return f.Ctx.Call("getShaderInfoLog", js.Value(s)).String() +} +func (f *Functions) GetString(pname Enum) string { + switch pname { + case EXTENSIONS: + extsjs := f.Ctx.Call("getSupportedExtensions") + var exts []string + for i := 0; i < extsjs.Length(); i++ { + exts = append(exts, "GL_"+extsjs.Index(i).String()) + } + return strings.Join(exts, " ") + default: + return f.Ctx.Call("getParameter", int(pname)).String() + } +} +func (f *Functions) GetUniformBlockIndex(p Program, name string) uint { + return uint(paramVal(f.Ctx.Call("getUniformBlockIndex", js.Value(p), name))) +} +func (f *Functions) GetUniformLocation(p Program, name string) Uniform { + return Uniform(f.Ctx.Call("getUniformLocation", js.Value(p), name)) +} +func (f *Functions) InvalidateFramebuffer(target, attachment Enum) { + fn := f.Ctx.Get("invalidateFramebuffer") + if !fn.IsUndefined() { + if f.int32Buf.IsUndefined() { + f.int32Buf = js.Global().Get("Int32Array").New(1) + } + f.int32Buf.SetIndex(0, int32(attachment)) + f.Ctx.Call("invalidateFramebuffer", int(target), f.int32Buf) + } +} +func (f *Functions) LinkProgram(p Program) { + f.Ctx.Call("linkProgram", js.Value(p)) +} +func (f *Functions) PixelStorei(pname Enum, param int32) { + f.Ctx.Call("pixelStorei", int(pname), param) +} +func (f *Functions) MemoryBarrier(barriers Enum) { + panic("not implemented") +} +func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte { + panic("not implemented") +} +func (f *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { + f.Ctx.Call("renderbufferStorage", int(target), int(internalformat), width, height) +} +func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) { + ba := f.byteArrayOf(data) + f.Ctx.Call("readPixels", x, y, width, height, int(format), int(ty), ba) + js.CopyBytesToGo(data, ba) +} +func (f *Functions) Scissor(x, y, width, height int32) { + f.Ctx.Call("scissor", x, y, width, height) +} +func (f *Functions) ShaderSource(s Shader, src string) { + f.Ctx.Call("shaderSource", js.Value(s), src) +} +func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width, height int, format, ty Enum) { + f.Ctx.Call("texImage2D", int(target), int(level), int(internalFormat), int(width), int(height), 0, int(format), int(ty), nil) +} +func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) { + f.Ctx.Call("texStorage2D", int(target), levels, int(internalFormat), width, height) +} +func (f *Functions) TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) { + f.Ctx.Call("texSubImage2D", int(target), level, x, y, width, height, int(format), int(ty), f.byteArrayOf(data)) +} +func (f *Functions) TexParameteri(target, pname Enum, param int) { + f.Ctx.Call("texParameteri", int(target), int(pname), int(param)) +} +func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) { + f.Ctx.Call("uniformBlockBinding", js.Value(p), int(uniformBlockIndex), int(uniformBlockBinding)) +} +func (f *Functions) Uniform1f(dst Uniform, v float32) { + f.Ctx.Call("uniform1f", js.Value(dst), v) +} +func (f *Functions) Uniform1i(dst Uniform, v int) { + f.Ctx.Call("uniform1i", js.Value(dst), v) +} +func (f *Functions) Uniform2f(dst Uniform, v0, v1 float32) { + f.Ctx.Call("uniform2f", js.Value(dst), v0, v1) +} +func (f *Functions) Uniform3f(dst Uniform, v0, v1, v2 float32) { + f.Ctx.Call("uniform3f", js.Value(dst), v0, v1, v2) +} +func (f *Functions) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) { + f.Ctx.Call("uniform4f", js.Value(dst), v0, v1, v2, v3) +} +func (f *Functions) UseProgram(p Program) { + f.Ctx.Call("useProgram", js.Value(p)) +} +func (f *Functions) UnmapBuffer(target Enum) bool { + panic("not implemented") +} +func (f *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) { + f.Ctx.Call("vertexAttribPointer", int(dst), size, int(ty), normalized, stride, offset) +} +func (f *Functions) Viewport(x, y, width, height int) { + f.Ctx.Call("viewport", x, y, width, height) +} + +func (f *Functions) byteArrayOf(data []byte) js.Value { + if len(data) == 0 { + return js.Null() + } + f.resizeByteBuffer(len(data)) + ba := f.uint8Array.New(f.arrayBuf, int(0), int(len(data))) + js.CopyBytesToJS(ba, data) + return ba +} + +func (f *Functions) resizeByteBuffer(n int) { + if n == 0 { + return + } + if !f.arrayBuf.IsUndefined() && f.arrayBuf.Length() >= n { + return + } + f.arrayBuf = js.Global().Get("ArrayBuffer").New(n) +} + +func paramVal(v js.Value) int { + switch v.Type() { + case js.TypeBoolean: + if b := v.Bool(); b { + return 1 + } else { + return 0 + } + case js.TypeNumber: + return v.Int() + default: + panic("unknown parameter type") + } +} diff --git a/gio/internal/gl/gl_unix.go b/gio/internal/gl/gl_unix.go new file mode 100644 index 0000000..a1d017a --- /dev/null +++ b/gio/internal/gl/gl_unix.go @@ -0,0 +1,635 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin linux freebsd openbsd + +package gl + +import ( + "runtime" + "strings" + "unsafe" +) + +/* +#cgo CFLAGS: -Werror +#cgo linux,!android pkg-config: glesv2 +#cgo linux freebsd LDFLAGS: -ldl +#cgo freebsd openbsd android LDFLAGS: -lGLESv2 +#cgo freebsd CFLAGS: -I/usr/local/include +#cgo freebsd LDFLAGS: -L/usr/local/lib +#cgo openbsd CFLAGS: -I/usr/X11R6/include +#cgo openbsd LDFLAGS: -L/usr/X11R6/lib +#cgo darwin,!ios CFLAGS: -DGL_SILENCE_DEPRECATION +#cgo darwin,!ios LDFLAGS: -framework OpenGL +#cgo darwin,ios CFLAGS: -DGLES_SILENCE_DEPRECATION +#cgo darwin,ios LDFLAGS: -framework OpenGLES + +#include +#define __USE_GNU +#include + +#ifdef __APPLE__ + #include "TargetConditionals.h" + #if TARGET_OS_IPHONE + #include + #else + #include + #endif +#else +#include +#include +#endif + +static void (*_glBindBufferBase)(GLenum target, GLuint index, GLuint buffer); +static GLuint (*_glGetUniformBlockIndex)(GLuint program, const GLchar *uniformBlockName); +static void (*_glUniformBlockBinding)(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding); +static void (*_glInvalidateFramebuffer)(GLenum target, GLsizei numAttachments, const GLenum *attachments); + +static void (*_glBeginQuery)(GLenum target, GLuint id); +static void (*_glDeleteQueries)(GLsizei n, const GLuint *ids); +static void (*_glEndQuery)(GLenum target); +static void (*_glGenQueries)(GLsizei n, GLuint *ids); +static void (*_glGetProgramBinary)(GLuint program, GLsizei bufsize, GLsizei *length, GLenum *binaryFormat, void *binary); +static void (*_glGetQueryObjectuiv)(GLuint id, GLenum pname, GLuint *params); +static const GLubyte* (*_glGetStringi)(GLenum name, GLuint index); +static void (*_glMemoryBarrier)(GLbitfield barriers); +static void (*_glDispatchCompute)(GLuint x, GLuint y, GLuint z); +static void* (*_glMapBufferRange)(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access); +static GLboolean (*_glUnmapBuffer)(GLenum target); +static void (*_glBindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format); +static void (*_glTexStorage2D)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height); +static void (*_glBlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter); + +// The pointer-free version of glVertexAttribPointer, to avoid the Cgo pointer checks. +__attribute__ ((visibility ("hidden"))) void gio_glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, uintptr_t offset) { + glVertexAttribPointer(index, size, type, normalized, stride, (const GLvoid *)offset); +} + +// The pointer-free version of glDrawElements, to avoid the Cgo pointer checks. +__attribute__ ((visibility ("hidden"))) void gio_glDrawElements(GLenum mode, GLsizei count, GLenum type, const uintptr_t offset) { + glDrawElements(mode, count, type, (const GLvoid *)offset); +} + +__attribute__ ((visibility ("hidden"))) void gio_glBindBufferBase(GLenum target, GLuint index, GLuint buffer) { + _glBindBufferBase(target, index, buffer); +} + +__attribute__ ((visibility ("hidden"))) void gio_glUniformBlockBinding(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding) { + _glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); +} + +__attribute__ ((visibility ("hidden"))) GLuint gio_glGetUniformBlockIndex(GLuint program, const GLchar *uniformBlockName) { + return _glGetUniformBlockIndex(program, uniformBlockName); +} + +__attribute__ ((visibility ("hidden"))) void gio_glInvalidateFramebuffer(GLenum target, GLenum attachment) { + // Framebuffer invalidation is just a hint and can safely be ignored. + if (_glInvalidateFramebuffer != NULL) { + _glInvalidateFramebuffer(target, 1, &attachment); + } +} + +__attribute__ ((visibility ("hidden"))) void gio_glBeginQuery(GLenum target, GLenum attachment) { + _glBeginQuery(target, attachment); +} + +__attribute__ ((visibility ("hidden"))) void gio_glDeleteQueries(GLsizei n, const GLuint *ids) { + _glDeleteQueries(n, ids); +} + +__attribute__ ((visibility ("hidden"))) void gio_glEndQuery(GLenum target) { + _glEndQuery(target); +} + +__attribute__ ((visibility ("hidden"))) const GLubyte* gio_glGetStringi(GLenum name, GLuint index) { + if (_glGetStringi == NULL) { + return NULL; + } + return _glGetStringi(name, index); +} + +__attribute__ ((visibility ("hidden"))) void gio_glGenQueries(GLsizei n, GLuint *ids) { + _glGenQueries(n, ids); +} + +__attribute__ ((visibility ("hidden"))) void gio_glGetProgramBinary(GLuint program, GLsizei bufsize, GLsizei *length, GLenum *binaryFormat, void *binary) { + _glGetProgramBinary(program, bufsize, length, binaryFormat, binary); +} + +__attribute__ ((visibility ("hidden"))) void gio_glGetQueryObjectuiv(GLuint id, GLenum pname, GLuint *params) { + _glGetQueryObjectuiv(id, pname, params); +} + +__attribute__ ((visibility ("hidden"))) void gio_glMemoryBarrier(GLbitfield barriers) { + _glMemoryBarrier(barriers); +} + +__attribute__ ((visibility ("hidden"))) void gio_glDispatchCompute(GLuint x, GLuint y, GLuint z) { + _glDispatchCompute(x, y, z); +} + +__attribute__ ((visibility ("hidden"))) void *gio_glMapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access) { + return _glMapBufferRange(target, offset, length, access); +} + +__attribute__ ((visibility ("hidden"))) GLboolean gio_glUnmapBuffer(GLenum target) { + return _glUnmapBuffer(target); +} + +__attribute__ ((visibility ("hidden"))) void gio_glBindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format) { + _glBindImageTexture(unit, texture, level, layered, layer, access, format); +} + +__attribute__ ((visibility ("hidden"))) void gio_glTexStorage2D(GLenum target, GLsizei levels, GLenum internalFormat, GLsizei width, GLsizei height) { + _glTexStorage2D(target, levels, internalFormat, width, height); +} + +__attribute__ ((visibility ("hidden"))) void gio_glBlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { + _glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); +} + +__attribute__((constructor)) static void gio_loadGLFunctions() { + // Load libGLESv3 if available. + dlopen("libGLESv3.so", RTLD_NOW | RTLD_GLOBAL); + + _glBindBufferBase = dlsym(RTLD_DEFAULT, "glBindBufferBase"); + _glGetUniformBlockIndex = dlsym(RTLD_DEFAULT, "glGetUniformBlockIndex"); + _glUniformBlockBinding = dlsym(RTLD_DEFAULT, "glUniformBlockBinding"); + _glInvalidateFramebuffer = dlsym(RTLD_DEFAULT, "glInvalidateFramebuffer"); + _glGetStringi = dlsym(RTLD_DEFAULT, "glGetStringi"); + // Fall back to EXT_invalidate_framebuffer if available. + if (_glInvalidateFramebuffer == NULL) { + _glInvalidateFramebuffer = dlsym(RTLD_DEFAULT, "glDiscardFramebufferEXT"); + } + + _glBeginQuery = dlsym(RTLD_DEFAULT, "glBeginQuery"); + if (_glBeginQuery == NULL) + _glBeginQuery = dlsym(RTLD_DEFAULT, "glBeginQueryEXT"); + _glDeleteQueries = dlsym(RTLD_DEFAULT, "glDeleteQueries"); + if (_glDeleteQueries == NULL) + _glDeleteQueries = dlsym(RTLD_DEFAULT, "glDeleteQueriesEXT"); + _glEndQuery = dlsym(RTLD_DEFAULT, "glEndQuery"); + if (_glEndQuery == NULL) + _glEndQuery = dlsym(RTLD_DEFAULT, "glEndQueryEXT"); + _glGenQueries = dlsym(RTLD_DEFAULT, "glGenQueries"); + if (_glGenQueries == NULL) + _glGenQueries = dlsym(RTLD_DEFAULT, "glGenQueriesEXT"); + _glGetQueryObjectuiv = dlsym(RTLD_DEFAULT, "glGetQueryObjectuiv"); + if (_glGetQueryObjectuiv == NULL) + _glGetQueryObjectuiv = dlsym(RTLD_DEFAULT, "glGetQueryObjectuivEXT"); + + _glMemoryBarrier = dlsym(RTLD_DEFAULT, "glMemoryBarrier"); + _glDispatchCompute = dlsym(RTLD_DEFAULT, "glDispatchCompute"); + _glMapBufferRange = dlsym(RTLD_DEFAULT, "glMapBufferRange"); + _glUnmapBuffer = dlsym(RTLD_DEFAULT, "glUnmapBuffer"); + _glBindImageTexture = dlsym(RTLD_DEFAULT, "glBindImageTexture"); + _glTexStorage2D = dlsym(RTLD_DEFAULT, "glTexStorage2D"); + _glBlitFramebuffer = dlsym(RTLD_DEFAULT, "glBlitFramebuffer"); + _glGetProgramBinary = dlsym(RTLD_DEFAULT, "glGetProgramBinary"); +} +*/ +import "C" + +type Context interface{} + +type Functions struct { + // Query caches. + uints [100]C.GLuint + ints [100]C.GLint +} + +func NewFunctions(ctx Context) (*Functions, error) { + if ctx != nil { + panic("non-nil context") + } + return new(Functions), nil +} + +func (f *Functions) ActiveTexture(texture Enum) { + C.glActiveTexture(C.GLenum(texture)) +} + +func (f *Functions) AttachShader(p Program, s Shader) { + C.glAttachShader(C.GLuint(p.V), C.GLuint(s.V)) +} + +func (f *Functions) BeginQuery(target Enum, query Query) { + C.gio_glBeginQuery(C.GLenum(target), C.GLenum(query.V)) +} + +func (f *Functions) BindAttribLocation(p Program, a Attrib, name string) { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + C.glBindAttribLocation(C.GLuint(p.V), C.GLuint(a), cname) +} + +func (f *Functions) BindBufferBase(target Enum, index int, b Buffer) { + C.gio_glBindBufferBase(C.GLenum(target), C.GLuint(index), C.GLuint(b.V)) +} + +func (f *Functions) BindBuffer(target Enum, b Buffer) { + C.glBindBuffer(C.GLenum(target), C.GLuint(b.V)) +} + +func (f *Functions) BindFramebuffer(target Enum, fb Framebuffer) { + C.glBindFramebuffer(C.GLenum(target), C.GLuint(fb.V)) +} + +func (f *Functions) BindRenderbuffer(target Enum, fb Renderbuffer) { + C.glBindRenderbuffer(C.GLenum(target), C.GLuint(fb.V)) +} + +func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) { + l := C.GLboolean(C.GL_FALSE) + if layered { + l = C.GL_TRUE + } + C.gio_glBindImageTexture(C.GLuint(unit), C.GLuint(t.V), C.GLint(level), l, C.GLint(layer), C.GLenum(access), C.GLenum(format)) +} + +func (f *Functions) BindTexture(target Enum, t Texture) { + C.glBindTexture(C.GLenum(target), C.GLuint(t.V)) +} + +func (f *Functions) BlendEquation(mode Enum) { + C.glBlendEquation(C.GLenum(mode)) +} + +func (f *Functions) BlendFunc(sfactor, dfactor Enum) { + C.glBlendFunc(C.GLenum(sfactor), C.GLenum(dfactor)) +} + +func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) { + C.gio_glBlitFramebuffer( + C.GLint(sx0), C.GLint(sy0), C.GLint(sx1), C.GLint(sy1), + C.GLint(dx0), C.GLint(dy0), C.GLint(dx1), C.GLint(dy1), + C.GLenum(mask), C.GLenum(filter), + ) +} + +func (f *Functions) BufferData(target Enum, size int, usage Enum) { + C.glBufferData(C.GLenum(target), C.GLsizeiptr(size), nil, C.GLenum(usage)) +} + +func (f *Functions) BufferSubData(target Enum, offset int, src []byte) { + var p unsafe.Pointer + if len(src) > 0 { + p = unsafe.Pointer(&src[0]) + } + C.glBufferSubData(C.GLenum(target), C.GLintptr(offset), C.GLsizeiptr(len(src)), p) +} + +func (f *Functions) CheckFramebufferStatus(target Enum) Enum { + return Enum(C.glCheckFramebufferStatus(C.GLenum(target))) +} + +func (f *Functions) Clear(mask Enum) { + C.glClear(C.GLbitfield(mask)) +} + +func (f *Functions) ClearColor(red float32, green float32, blue float32, alpha float32) { + C.glClearColor(C.GLfloat(red), C.GLfloat(green), C.GLfloat(blue), C.GLfloat(alpha)) +} + +func (f *Functions) ClearDepthf(d float32) { + C.glClearDepthf(C.GLfloat(d)) +} + +func (f *Functions) CompileShader(s Shader) { + C.glCompileShader(C.GLuint(s.V)) +} + +func (f *Functions) CreateBuffer() Buffer { + C.glGenBuffers(1, &f.uints[0]) + return Buffer{uint(f.uints[0])} +} + +func (f *Functions) CreateFramebuffer() Framebuffer { + C.glGenFramebuffers(1, &f.uints[0]) + return Framebuffer{uint(f.uints[0])} +} + +func (f *Functions) CreateProgram() Program { + return Program{uint(C.glCreateProgram())} +} + +func (f *Functions) CreateQuery() Query { + C.gio_glGenQueries(1, &f.uints[0]) + return Query{uint(f.uints[0])} +} + +func (f *Functions) CreateRenderbuffer() Renderbuffer { + C.glGenRenderbuffers(1, &f.uints[0]) + return Renderbuffer{uint(f.uints[0])} +} + +func (f *Functions) CreateShader(ty Enum) Shader { + return Shader{uint(C.glCreateShader(C.GLenum(ty)))} +} + +func (f *Functions) CreateTexture() Texture { + C.glGenTextures(1, &f.uints[0]) + return Texture{uint(f.uints[0])} +} + +func (f *Functions) DeleteBuffer(v Buffer) { + f.uints[0] = C.GLuint(v.V) + C.glDeleteBuffers(1, &f.uints[0]) +} + +func (f *Functions) DeleteFramebuffer(v Framebuffer) { + f.uints[0] = C.GLuint(v.V) + C.glDeleteFramebuffers(1, &f.uints[0]) +} + +func (f *Functions) DeleteProgram(p Program) { + C.glDeleteProgram(C.GLuint(p.V)) +} + +func (f *Functions) DeleteQuery(query Query) { + f.uints[0] = C.GLuint(query.V) + C.gio_glDeleteQueries(1, &f.uints[0]) +} + +func (f *Functions) DeleteRenderbuffer(v Renderbuffer) { + f.uints[0] = C.GLuint(v.V) + C.glDeleteRenderbuffers(1, &f.uints[0]) +} + +func (f *Functions) DeleteShader(s Shader) { + C.glDeleteShader(C.GLuint(s.V)) +} + +func (f *Functions) DeleteTexture(v Texture) { + f.uints[0] = C.GLuint(v.V) + C.glDeleteTextures(1, &f.uints[0]) +} + +func (f *Functions) DepthFunc(v Enum) { + C.glDepthFunc(C.GLenum(v)) +} + +func (f *Functions) DepthMask(mask bool) { + m := C.GLboolean(C.GL_FALSE) + if mask { + m = C.GLboolean(C.GL_TRUE) + } + C.glDepthMask(m) +} + +func (f *Functions) DisableVertexAttribArray(a Attrib) { + C.glDisableVertexAttribArray(C.GLuint(a)) +} + +func (f *Functions) Disable(cap Enum) { + C.glDisable(C.GLenum(cap)) +} + +func (f *Functions) DrawArrays(mode Enum, first int, count int) { + C.glDrawArrays(C.GLenum(mode), C.GLint(first), C.GLsizei(count)) +} + +func (f *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { + C.gio_glDrawElements(C.GLenum(mode), C.GLsizei(count), C.GLenum(ty), C.uintptr_t(offset)) +} + +func (f *Functions) DispatchCompute(x, y, z int) { + C.gio_glDispatchCompute(C.GLuint(x), C.GLuint(y), C.GLuint(z)) +} + +func (f *Functions) Enable(cap Enum) { + C.glEnable(C.GLenum(cap)) +} + +func (f *Functions) EndQuery(target Enum) { + C.gio_glEndQuery(C.GLenum(target)) +} + +func (f *Functions) EnableVertexAttribArray(a Attrib) { + C.glEnableVertexAttribArray(C.GLuint(a)) +} + +func (f *Functions) Finish() { + C.glFinish() +} + +func (f *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { + C.glFramebufferRenderbuffer(C.GLenum(target), C.GLenum(attachment), C.GLenum(renderbuffertarget), C.GLuint(renderbuffer.V)) +} + +func (f *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { + C.glFramebufferTexture2D(C.GLenum(target), C.GLenum(attachment), C.GLenum(texTarget), C.GLuint(t.V), C.GLint(level)) +} + +func (c *Functions) GetBinding(pname Enum) Object { + return Object{uint(c.GetInteger(pname))} +} + +func (f *Functions) GetError() Enum { + return Enum(C.glGetError()) +} + +func (f *Functions) GetRenderbufferParameteri(target, pname Enum) int { + C.glGetRenderbufferParameteriv(C.GLenum(target), C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { + C.glGetFramebufferAttachmentParameteriv(C.GLenum(target), C.GLenum(attachment), C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetInteger(pname Enum) int { + C.glGetIntegerv(C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetProgrami(p Program, pname Enum) int { + C.glGetProgramiv(C.GLuint(p.V), C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetProgramBinary(p Program) []byte { + sz := f.GetProgrami(p, PROGRAM_BINARY_LENGTH) + if sz == 0 { + return nil + } + buf := make([]byte, sz) + var format C.GLenum + C.gio_glGetProgramBinary(C.GLuint(p.V), C.GLsizei(sz), nil, &format, unsafe.Pointer(&buf[0])) + return buf +} + +func (f *Functions) GetProgramInfoLog(p Program) string { + n := f.GetProgrami(p, INFO_LOG_LENGTH) + buf := make([]byte, n) + C.glGetProgramInfoLog(C.GLuint(p.V), C.GLsizei(len(buf)), nil, (*C.GLchar)(unsafe.Pointer(&buf[0]))) + return string(buf) +} + +func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { + C.gio_glGetQueryObjectuiv(C.GLuint(query.V), C.GLenum(pname), &f.uints[0]) + return uint(f.uints[0]) +} + +func (f *Functions) GetShaderi(s Shader, pname Enum) int { + C.glGetShaderiv(C.GLuint(s.V), C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetShaderInfoLog(s Shader) string { + n := f.GetShaderi(s, INFO_LOG_LENGTH) + buf := make([]byte, n) + C.glGetShaderInfoLog(C.GLuint(s.V), C.GLsizei(len(buf)), nil, (*C.GLchar)(unsafe.Pointer(&buf[0]))) + return string(buf) +} + +func (f *Functions) GetStringi(pname Enum, index int) string { + str := C.gio_glGetStringi(C.GLenum(pname), C.GLuint(index)) + if str == nil { + return "" + } + return C.GoString((*C.char)(unsafe.Pointer(str))) +} + +func (f *Functions) GetString(pname Enum) string { + switch { + case runtime.GOOS == "darwin" && pname == EXTENSIONS: + // macOS OpenGL 3 core profile doesn't support glGetString(GL_EXTENSIONS). + // Use glGetStringi(GL_EXTENSIONS, ). + var exts []string + nexts := f.GetInteger(NUM_EXTENSIONS) + for i := 0; i < nexts; i++ { + ext := f.GetStringi(EXTENSIONS, i) + exts = append(exts, ext) + } + return strings.Join(exts, " ") + default: + str := C.glGetString(C.GLenum(pname)) + return C.GoString((*C.char)(unsafe.Pointer(str))) + } +} + +func (f *Functions) GetUniformBlockIndex(p Program, name string) uint { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + return uint(C.gio_glGetUniformBlockIndex(C.GLuint(p.V), cname)) +} + +func (f *Functions) GetUniformLocation(p Program, name string) Uniform { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + return Uniform{int(C.glGetUniformLocation(C.GLuint(p.V), cname))} +} + +func (f *Functions) InvalidateFramebuffer(target, attachment Enum) { + C.gio_glInvalidateFramebuffer(C.GLenum(target), C.GLenum(attachment)) +} + +func (f *Functions) LinkProgram(p Program) { + C.glLinkProgram(C.GLuint(p.V)) +} + +func (f *Functions) PixelStorei(pname Enum, param int32) { + C.glPixelStorei(C.GLenum(pname), C.GLint(param)) +} + +func (f *Functions) MemoryBarrier(barriers Enum) { + C.gio_glMemoryBarrier(C.GLbitfield(barriers)) +} + +func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte { + p := C.gio_glMapBufferRange(C.GLenum(target), C.GLintptr(offset), C.GLsizeiptr(length), C.GLbitfield(access)) + if p == nil { + return nil + } + return (*[1 << 30]byte)(p)[:length:length] +} + +func (f *Functions) Scissor(x, y, width, height int32) { + C.glScissor(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height)) +} + +func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) { + var p unsafe.Pointer + if len(data) > 0 { + p = unsafe.Pointer(&data[0]) + } + C.glReadPixels(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height), C.GLenum(format), C.GLenum(ty), p) +} + +func (f *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { + C.glRenderbufferStorage(C.GLenum(target), C.GLenum(internalformat), C.GLsizei(width), C.GLsizei(height)) +} + +func (f *Functions) ShaderSource(s Shader, src string) { + csrc := C.CString(src) + defer C.free(unsafe.Pointer(csrc)) + strlen := C.GLint(len(src)) + C.glShaderSource(C.GLuint(s.V), 1, &csrc, &strlen) +} + +func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width int, height int, format Enum, ty Enum) { + C.glTexImage2D(C.GLenum(target), C.GLint(level), C.GLint(internalFormat), C.GLsizei(width), C.GLsizei(height), 0, C.GLenum(format), C.GLenum(ty), nil) +} + +func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) { + C.gio_glTexStorage2D(C.GLenum(target), C.GLsizei(levels), C.GLenum(internalFormat), C.GLsizei(width), C.GLsizei(height)) +} + +func (f *Functions) TexSubImage2D(target Enum, level int, x int, y int, width int, height int, format Enum, ty Enum, data []byte) { + var p unsafe.Pointer + if len(data) > 0 { + p = unsafe.Pointer(&data[0]) + } + C.glTexSubImage2D(C.GLenum(target), C.GLint(level), C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height), C.GLenum(format), C.GLenum(ty), p) +} + +func (f *Functions) TexParameteri(target, pname Enum, param int) { + C.glTexParameteri(C.GLenum(target), C.GLenum(pname), C.GLint(param)) +} + +func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) { + C.gio_glUniformBlockBinding(C.GLuint(p.V), C.GLuint(uniformBlockIndex), C.GLuint(uniformBlockBinding)) +} + +func (f *Functions) Uniform1f(dst Uniform, v float32) { + C.glUniform1f(C.GLint(dst.V), C.GLfloat(v)) +} + +func (f *Functions) Uniform1i(dst Uniform, v int) { + C.glUniform1i(C.GLint(dst.V), C.GLint(v)) +} + +func (f *Functions) Uniform2f(dst Uniform, v0 float32, v1 float32) { + C.glUniform2f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1)) +} + +func (f *Functions) Uniform3f(dst Uniform, v0 float32, v1 float32, v2 float32) { + C.glUniform3f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1), C.GLfloat(v2)) +} + +func (f *Functions) Uniform4f(dst Uniform, v0 float32, v1 float32, v2 float32, v3 float32) { + C.glUniform4f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1), C.GLfloat(v2), C.GLfloat(v3)) +} + +func (f *Functions) UseProgram(p Program) { + C.glUseProgram(C.GLuint(p.V)) +} + +func (f *Functions) UnmapBuffer(target Enum) bool { + r := C.gio_glUnmapBuffer(C.GLenum(target)) + return r == C.GL_TRUE +} + +func (f *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride int, offset int) { + var n C.GLboolean = C.GL_FALSE + if normalized { + n = C.GL_TRUE + } + C.gio_glVertexAttribPointer(C.GLuint(dst), C.GLint(size), C.GLenum(ty), n, C.GLsizei(stride), C.uintptr_t(offset)) +} + +func (f *Functions) Viewport(x int, y int, width int, height int) { + C.glViewport(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height)) +} diff --git a/gio/internal/gl/gl_windows.go b/gio/internal/gl/gl_windows.go new file mode 100644 index 0000000..099c82b --- /dev/null +++ b/gio/internal/gl/gl_windows.go @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import ( + "math" + "runtime" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + LibGLESv2 = windows.NewLazyDLL("libGLESv2.dll") + _glActiveTexture = LibGLESv2.NewProc("glActiveTexture") + _glAttachShader = LibGLESv2.NewProc("glAttachShader") + _glBeginQuery = LibGLESv2.NewProc("glBeginQuery") + _glBindAttribLocation = LibGLESv2.NewProc("glBindAttribLocation") + _glBindBuffer = LibGLESv2.NewProc("glBindBuffer") + _glBindBufferBase = LibGLESv2.NewProc("glBindBufferBase") + _glBindFramebuffer = LibGLESv2.NewProc("glBindFramebuffer") + _glBindRenderbuffer = LibGLESv2.NewProc("glBindRenderbuffer") + _glBindTexture = LibGLESv2.NewProc("glBindTexture") + _glBlendEquation = LibGLESv2.NewProc("glBlendEquation") + _glBlendFunc = LibGLESv2.NewProc("glBlendFunc") + _glBufferData = LibGLESv2.NewProc("glBufferData") + _glBufferSubData = LibGLESv2.NewProc("glBufferSubData") + _glCheckFramebufferStatus = LibGLESv2.NewProc("glCheckFramebufferStatus") + _glClear = LibGLESv2.NewProc("glClear") + _glClearColor = LibGLESv2.NewProc("glClearColor") + _glClearDepthf = LibGLESv2.NewProc("glClearDepthf") + _glDeleteQueries = LibGLESv2.NewProc("glDeleteQueries") + _glCompileShader = LibGLESv2.NewProc("glCompileShader") + _glGenBuffers = LibGLESv2.NewProc("glGenBuffers") + _glGenFramebuffers = LibGLESv2.NewProc("glGenFramebuffers") + _glGetUniformBlockIndex = LibGLESv2.NewProc("glGetUniformBlockIndex") + _glCreateProgram = LibGLESv2.NewProc("glCreateProgram") + _glGenRenderbuffers = LibGLESv2.NewProc("glGenRenderbuffers") + _glCreateShader = LibGLESv2.NewProc("glCreateShader") + _glGenTextures = LibGLESv2.NewProc("glGenTextures") + _glDeleteBuffers = LibGLESv2.NewProc("glDeleteBuffers") + _glDeleteFramebuffers = LibGLESv2.NewProc("glDeleteFramebuffers") + _glDeleteProgram = LibGLESv2.NewProc("glDeleteProgram") + _glDeleteShader = LibGLESv2.NewProc("glDeleteShader") + _glDeleteRenderbuffers = LibGLESv2.NewProc("glDeleteRenderbuffers") + _glDeleteTextures = LibGLESv2.NewProc("glDeleteTextures") + _glDepthFunc = LibGLESv2.NewProc("glDepthFunc") + _glDepthMask = LibGLESv2.NewProc("glDepthMask") + _glDisableVertexAttribArray = LibGLESv2.NewProc("glDisableVertexAttribArray") + _glDisable = LibGLESv2.NewProc("glDisable") + _glDrawArrays = LibGLESv2.NewProc("glDrawArrays") + _glDrawElements = LibGLESv2.NewProc("glDrawElements") + _glEnable = LibGLESv2.NewProc("glEnable") + _glEnableVertexAttribArray = LibGLESv2.NewProc("glEnableVertexAttribArray") + _glEndQuery = LibGLESv2.NewProc("glEndQuery") + _glFinish = LibGLESv2.NewProc("glFinish") + _glFramebufferRenderbuffer = LibGLESv2.NewProc("glFramebufferRenderbuffer") + _glFramebufferTexture2D = LibGLESv2.NewProc("glFramebufferTexture2D") + _glGenQueries = LibGLESv2.NewProc("glGenQueries") + _glGetError = LibGLESv2.NewProc("glGetError") + _glGetRenderbufferParameteri = LibGLESv2.NewProc("glGetRenderbufferParameteri") + _glGetFramebufferAttachmentParameteri = LibGLESv2.NewProc("glGetFramebufferAttachmentParameteri") + _glGetIntegerv = LibGLESv2.NewProc("glGetIntegerv") + _glGetProgramiv = LibGLESv2.NewProc("glGetProgramiv") + _glGetProgramInfoLog = LibGLESv2.NewProc("glGetProgramInfoLog") + _glGetQueryObjectuiv = LibGLESv2.NewProc("glGetQueryObjectuiv") + _glGetShaderiv = LibGLESv2.NewProc("glGetShaderiv") + _glGetShaderInfoLog = LibGLESv2.NewProc("glGetShaderInfoLog") + _glGetString = LibGLESv2.NewProc("glGetString") + _glGetUniformLocation = LibGLESv2.NewProc("glGetUniformLocation") + _glInvalidateFramebuffer = LibGLESv2.NewProc("glInvalidateFramebuffer") + _glLinkProgram = LibGLESv2.NewProc("glLinkProgram") + _glPixelStorei = LibGLESv2.NewProc("glPixelStorei") + _glReadPixels = LibGLESv2.NewProc("glReadPixels") + _glRenderbufferStorage = LibGLESv2.NewProc("glRenderbufferStorage") + _glScissor = LibGLESv2.NewProc("glScissor") + _glShaderSource = LibGLESv2.NewProc("glShaderSource") + _glTexImage2D = LibGLESv2.NewProc("glTexImage2D") + _glTexStorage2D = LibGLESv2.NewProc("glTexStorage2D") + _glTexSubImage2D = LibGLESv2.NewProc("glTexSubImage2D") + _glTexParameteri = LibGLESv2.NewProc("glTexParameteri") + _glUniformBlockBinding = LibGLESv2.NewProc("glUniformBlockBinding") + _glUniform1f = LibGLESv2.NewProc("glUniform1f") + _glUniform1i = LibGLESv2.NewProc("glUniform1i") + _glUniform2f = LibGLESv2.NewProc("glUniform2f") + _glUniform3f = LibGLESv2.NewProc("glUniform3f") + _glUniform4f = LibGLESv2.NewProc("glUniform4f") + _glUseProgram = LibGLESv2.NewProc("glUseProgram") + _glVertexAttribPointer = LibGLESv2.NewProc("glVertexAttribPointer") + _glViewport = LibGLESv2.NewProc("glViewport") +) + +type Functions struct { + // Query caches. + int32s [100]int32 +} + +type Context interface{} + +func NewFunctions(ctx Context) (*Functions, error) { + if ctx != nil { + panic("non-nil context") + } + return new(Functions), nil +} + +func (c *Functions) ActiveTexture(t Enum) { + syscall.Syscall(_glActiveTexture.Addr(), 1, uintptr(t), 0, 0) +} +func (c *Functions) AttachShader(p Program, s Shader) { + syscall.Syscall(_glAttachShader.Addr(), 2, uintptr(p.V), uintptr(s.V), 0) +} +func (f *Functions) BeginQuery(target Enum, query Query) { + syscall.Syscall(_glBeginQuery.Addr(), 2, uintptr(target), uintptr(query.V), 0) +} +func (c *Functions) BindAttribLocation(p Program, a Attrib, name string) { + cname := cString(name) + c0 := &cname[0] + syscall.Syscall(_glBindAttribLocation.Addr(), 3, uintptr(p.V), uintptr(a), uintptr(unsafe.Pointer(c0))) + issue34474KeepAlive(c) +} +func (c *Functions) BindBuffer(target Enum, b Buffer) { + syscall.Syscall(_glBindBuffer.Addr(), 2, uintptr(target), uintptr(b.V), 0) +} +func (c *Functions) BindBufferBase(target Enum, index int, b Buffer) { + syscall.Syscall(_glBindBufferBase.Addr(), 3, uintptr(target), uintptr(index), uintptr(b.V)) +} +func (c *Functions) BindFramebuffer(target Enum, fb Framebuffer) { + syscall.Syscall(_glBindFramebuffer.Addr(), 2, uintptr(target), uintptr(fb.V), 0) +} +func (c *Functions) BindRenderbuffer(target Enum, rb Renderbuffer) { + syscall.Syscall(_glBindRenderbuffer.Addr(), 2, uintptr(target), uintptr(rb.V), 0) +} +func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) { + panic("not implemented") +} +func (c *Functions) BindTexture(target Enum, t Texture) { + syscall.Syscall(_glBindTexture.Addr(), 2, uintptr(target), uintptr(t.V), 0) +} +func (c *Functions) BlendEquation(mode Enum) { + syscall.Syscall(_glBlendEquation.Addr(), 1, uintptr(mode), 0, 0) +} +func (c *Functions) BlendFunc(sfactor, dfactor Enum) { + syscall.Syscall(_glBlendFunc.Addr(), 2, uintptr(sfactor), uintptr(dfactor), 0) +} +func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) { + panic("not implemented") +} +func (c *Functions) BufferData(target Enum, size int, usage Enum) { + syscall.Syscall6(_glBufferData.Addr(), 4, uintptr(target), uintptr(size), 0, uintptr(usage), 0, 0) +} +func (f *Functions) BufferSubData(target Enum, offset int, src []byte) { + if n := len(src); n > 0 { + s0 := &src[0] + syscall.Syscall6(_glBufferSubData.Addr(), 4, uintptr(target), uintptr(offset), uintptr(n), uintptr(unsafe.Pointer(s0)), 0, 0) + issue34474KeepAlive(s0) + } +} +func (c *Functions) CheckFramebufferStatus(target Enum) Enum { + s, _, _ := syscall.Syscall(_glCheckFramebufferStatus.Addr(), 1, uintptr(target), 0, 0) + return Enum(s) +} +func (c *Functions) Clear(mask Enum) { + syscall.Syscall(_glClear.Addr(), 1, uintptr(mask), 0, 0) +} +func (c *Functions) ClearColor(red, green, blue, alpha float32) { + syscall.Syscall6(_glClearColor.Addr(), 4, uintptr(math.Float32bits(red)), uintptr(math.Float32bits(green)), uintptr(math.Float32bits(blue)), uintptr(math.Float32bits(alpha)), 0, 0) +} +func (c *Functions) ClearDepthf(d float32) { + syscall.Syscall(_glClearDepthf.Addr(), 1, uintptr(math.Float32bits(d)), 0, 0) +} +func (c *Functions) CompileShader(s Shader) { + syscall.Syscall(_glCompileShader.Addr(), 1, uintptr(s.V), 0, 0) +} +func (c *Functions) CreateBuffer() Buffer { + var buf uintptr + syscall.Syscall(_glGenBuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&buf)), 0) + return Buffer{uint(buf)} +} +func (c *Functions) CreateFramebuffer() Framebuffer { + var fb uintptr + syscall.Syscall(_glGenFramebuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&fb)), 0) + return Framebuffer{uint(fb)} +} +func (c *Functions) CreateProgram() Program { + p, _, _ := syscall.Syscall(_glCreateProgram.Addr(), 0, 0, 0, 0) + return Program{uint(p)} +} +func (f *Functions) CreateQuery() Query { + var q uintptr + syscall.Syscall(_glGenQueries.Addr(), 2, 1, uintptr(unsafe.Pointer(&q)), 0) + return Query{uint(q)} +} +func (c *Functions) CreateRenderbuffer() Renderbuffer { + var rb uintptr + syscall.Syscall(_glGenRenderbuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&rb)), 0) + return Renderbuffer{uint(rb)} +} +func (c *Functions) CreateShader(ty Enum) Shader { + s, _, _ := syscall.Syscall(_glCreateShader.Addr(), 1, uintptr(ty), 0, 0) + return Shader{uint(s)} +} +func (c *Functions) CreateTexture() Texture { + var t uintptr + syscall.Syscall(_glGenTextures.Addr(), 2, 1, uintptr(unsafe.Pointer(&t)), 0) + return Texture{uint(t)} +} +func (c *Functions) DeleteBuffer(v Buffer) { + syscall.Syscall(_glDeleteBuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v)), 0) +} +func (c *Functions) DeleteFramebuffer(v Framebuffer) { + syscall.Syscall(_glDeleteFramebuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0) +} +func (c *Functions) DeleteProgram(p Program) { + syscall.Syscall(_glDeleteProgram.Addr(), 1, uintptr(p.V), 0, 0) +} +func (f *Functions) DeleteQuery(query Query) { + syscall.Syscall(_glDeleteQueries.Addr(), 2, 1, uintptr(unsafe.Pointer(&query.V)), 0) +} +func (c *Functions) DeleteShader(s Shader) { + syscall.Syscall(_glDeleteShader.Addr(), 1, uintptr(s.V), 0, 0) +} +func (c *Functions) DeleteRenderbuffer(v Renderbuffer) { + syscall.Syscall(_glDeleteRenderbuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0) +} +func (c *Functions) DeleteTexture(v Texture) { + syscall.Syscall(_glDeleteTextures.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0) +} +func (c *Functions) DepthFunc(f Enum) { + syscall.Syscall(_glDepthFunc.Addr(), 1, uintptr(f), 0, 0) +} +func (c *Functions) DepthMask(mask bool) { + var m uintptr + if mask { + m = 1 + } + syscall.Syscall(_glDepthMask.Addr(), 1, m, 0, 0) +} +func (c *Functions) DisableVertexAttribArray(a Attrib) { + syscall.Syscall(_glDisableVertexAttribArray.Addr(), 1, uintptr(a), 0, 0) +} +func (c *Functions) Disable(cap Enum) { + syscall.Syscall(_glDisable.Addr(), 1, uintptr(cap), 0, 0) +} +func (c *Functions) DrawArrays(mode Enum, first, count int) { + syscall.Syscall(_glDrawArrays.Addr(), 3, uintptr(mode), uintptr(first), uintptr(count)) +} +func (c *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { + syscall.Syscall6(_glDrawElements.Addr(), 4, uintptr(mode), uintptr(count), uintptr(ty), uintptr(offset), 0, 0) +} +func (f *Functions) DispatchCompute(x, y, z int) { + panic("not implemented") +} +func (c *Functions) Enable(cap Enum) { + syscall.Syscall(_glEnable.Addr(), 1, uintptr(cap), 0, 0) +} +func (c *Functions) EnableVertexAttribArray(a Attrib) { + syscall.Syscall(_glEnableVertexAttribArray.Addr(), 1, uintptr(a), 0, 0) +} +func (f *Functions) EndQuery(target Enum) { + syscall.Syscall(_glEndQuery.Addr(), 1, uintptr(target), 0, 0) +} +func (c *Functions) Finish() { + syscall.Syscall(_glFinish.Addr(), 0, 0, 0, 0) +} +func (c *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { + syscall.Syscall6(_glFramebufferRenderbuffer.Addr(), 4, uintptr(target), uintptr(attachment), uintptr(renderbuffertarget), uintptr(renderbuffer.V), 0, 0) +} +func (c *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { + syscall.Syscall6(_glFramebufferTexture2D.Addr(), 5, uintptr(target), uintptr(attachment), uintptr(texTarget), uintptr(t.V), uintptr(level), 0) +} +func (f *Functions) GetUniformBlockIndex(p Program, name string) uint { + cname := cString(name) + c0 := &cname[0] + u, _, _ := syscall.Syscall(_glGetUniformBlockIndex.Addr(), 2, uintptr(p.V), uintptr(unsafe.Pointer(c0)), 0) + issue34474KeepAlive(c0) + return uint(u) +} +func (c *Functions) GetBinding(pname Enum) Object { + return Object{uint(c.GetInteger(pname))} +} +func (c *Functions) GetError() Enum { + e, _, _ := syscall.Syscall(_glGetError.Addr(), 0, 0, 0, 0) + return Enum(e) +} +func (c *Functions) GetRenderbufferParameteri(target, pname Enum) int { + p, _, _ := syscall.Syscall(_glGetRenderbufferParameteri.Addr(), 2, uintptr(target), uintptr(pname), 0) + return int(p) +} +func (c *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { + p, _, _ := syscall.Syscall(_glGetFramebufferAttachmentParameteri.Addr(), 3, uintptr(target), uintptr(attachment), uintptr(pname)) + return int(p) +} +func (c *Functions) GetInteger(pname Enum) int { + syscall.Syscall(_glGetIntegerv.Addr(), 2, uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])), 0) + return int(c.int32s[0]) +} +func (c *Functions) GetProgrami(p Program, pname Enum) int { + syscall.Syscall(_glGetProgramiv.Addr(), 3, uintptr(p.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0]))) + return int(c.int32s[0]) +} +func (c *Functions) GetProgramInfoLog(p Program) string { + n := c.GetProgrami(p, INFO_LOG_LENGTH) + buf := make([]byte, n) + syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0) + return string(buf) +} +func (c *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { + syscall.Syscall(_glGetQueryObjectuiv.Addr(), 3, uintptr(query.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0]))) + return uint(c.int32s[0]) +} +func (c *Functions) GetShaderi(s Shader, pname Enum) int { + syscall.Syscall(_glGetShaderiv.Addr(), 3, uintptr(s.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0]))) + return int(c.int32s[0]) +} +func (c *Functions) GetShaderInfoLog(s Shader) string { + n := c.GetShaderi(s, INFO_LOG_LENGTH) + buf := make([]byte, n) + syscall.Syscall6(_glGetShaderInfoLog.Addr(), 4, uintptr(s.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0) + return string(buf) +} +func (c *Functions) GetString(pname Enum) string { + s, _, _ := syscall.Syscall(_glGetString.Addr(), 1, uintptr(pname), 0, 0) + return windows.BytePtrToString((*byte)(unsafe.Pointer(s))) +} +func (c *Functions) GetUniformLocation(p Program, name string) Uniform { + cname := cString(name) + c0 := &cname[0] + u, _, _ := syscall.Syscall(_glGetUniformLocation.Addr(), 2, uintptr(p.V), uintptr(unsafe.Pointer(c0)), 0) + issue34474KeepAlive(c0) + return Uniform{int(u)} +} +func (c *Functions) InvalidateFramebuffer(target, attachment Enum) { + addr := _glInvalidateFramebuffer.Addr() + if addr == 0 { + // InvalidateFramebuffer is just a hint. Skip it if not supported. + return + } + syscall.Syscall(addr, 3, uintptr(target), 1, uintptr(unsafe.Pointer(&attachment))) +} +func (c *Functions) LinkProgram(p Program) { + syscall.Syscall(_glLinkProgram.Addr(), 1, uintptr(p.V), 0, 0) +} +func (c *Functions) PixelStorei(pname Enum, param int32) { + syscall.Syscall(_glPixelStorei.Addr(), 2, uintptr(pname), uintptr(param), 0) +} +func (f *Functions) MemoryBarrier(barriers Enum) { + panic("not implemented") +} +func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte { + panic("not implemented") +} +func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) { + d0 := &data[0] + syscall.Syscall9(_glReadPixels.Addr(), 7, uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(format), uintptr(ty), uintptr(unsafe.Pointer(d0)), 0, 0) + issue34474KeepAlive(d0) +} +func (c *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { + syscall.Syscall6(_glRenderbufferStorage.Addr(), 4, uintptr(target), uintptr(internalformat), uintptr(width), uintptr(height), 0, 0) +} +func (c *Functions) Scissor(x, y, width, height int32) { + syscall.Syscall6(_glScissor.Addr(), 4, uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0, 0) +} +func (c *Functions) ShaderSource(s Shader, src string) { + var n uintptr = uintptr(len(src)) + psrc := &src + syscall.Syscall6(_glShaderSource.Addr(), 4, uintptr(s.V), 1, uintptr(unsafe.Pointer(psrc)), uintptr(unsafe.Pointer(&n)), 0, 0) + issue34474KeepAlive(psrc) +} +func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width int, height int, format Enum, ty Enum) { + syscall.Syscall9(_glTexImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(internalFormat), uintptr(width), uintptr(height), 0, uintptr(format), uintptr(ty), 0) +} +func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) { + syscall.Syscall6(_glTexStorage2D.Addr(), 5, uintptr(target), uintptr(levels), uintptr(internalFormat), uintptr(width), uintptr(height), 0) +} +func (c *Functions) TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) { + d0 := &data[0] + syscall.Syscall9(_glTexSubImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(format), uintptr(ty), uintptr(unsafe.Pointer(d0))) + issue34474KeepAlive(d0) +} +func (c *Functions) TexParameteri(target, pname Enum, param int) { + syscall.Syscall(_glTexParameteri.Addr(), 3, uintptr(target), uintptr(pname), uintptr(param)) +} +func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) { + syscall.Syscall(_glUniformBlockBinding.Addr(), 3, uintptr(p.V), uintptr(uniformBlockIndex), uintptr(uniformBlockBinding)) +} +func (c *Functions) Uniform1f(dst Uniform, v float32) { + syscall.Syscall(_glUniform1f.Addr(), 2, uintptr(dst.V), uintptr(math.Float32bits(v)), 0) +} +func (c *Functions) Uniform1i(dst Uniform, v int) { + syscall.Syscall(_glUniform1i.Addr(), 2, uintptr(dst.V), uintptr(v), 0) +} +func (c *Functions) Uniform2f(dst Uniform, v0, v1 float32) { + syscall.Syscall(_glUniform2f.Addr(), 3, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1))) +} +func (c *Functions) Uniform3f(dst Uniform, v0, v1, v2 float32) { + syscall.Syscall6(_glUniform3f.Addr(), 4, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)), uintptr(math.Float32bits(v2)), 0, 0) +} +func (c *Functions) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) { + syscall.Syscall6(_glUniform4f.Addr(), 5, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)), uintptr(math.Float32bits(v2)), uintptr(math.Float32bits(v3)), 0) +} +func (c *Functions) UseProgram(p Program) { + syscall.Syscall(_glUseProgram.Addr(), 1, uintptr(p.V), 0, 0) +} +func (f *Functions) UnmapBuffer(target Enum) bool { + panic("not implemented") +} +func (c *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) { + var norm uintptr + if normalized { + norm = 1 + } + syscall.Syscall6(_glVertexAttribPointer.Addr(), 6, uintptr(dst), uintptr(size), uintptr(ty), norm, uintptr(stride), uintptr(offset)) +} +func (c *Functions) Viewport(x, y, width, height int) { + syscall.Syscall6(_glViewport.Addr(), 4, uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0, 0) +} + +func cString(s string) []byte { + b := make([]byte, len(s)+1) + copy(b, s) + return b +} + +// issue34474KeepAlive calls runtime.KeepAlive as a +// workaround for golang.org/issue/34474. +func issue34474KeepAlive(v interface{}) { + runtime.KeepAlive(v) +} diff --git a/gio/internal/gl/types.go b/gio/internal/gl/types.go new file mode 100644 index 0000000..45db3be --- /dev/null +++ b/gio/internal/gl/types.go @@ -0,0 +1,27 @@ +// +build !js + +package gl + +type ( + Buffer struct{ V uint } + Framebuffer struct{ V uint } + Program struct{ V uint } + Renderbuffer struct{ V uint } + Shader struct{ V uint } + Texture struct{ V uint } + Query struct{ V uint } + Uniform struct{ V int } + Object struct{ V uint } +) + +func (u Uniform) Valid() bool { + return u.V != -1 +} + +func (p Program) Valid() bool { + return p.V != 0 +} + +func (s Shader) Valid() bool { + return s.V != 0 +} diff --git a/gio/internal/gl/types_js.go b/gio/internal/gl/types_js.go new file mode 100644 index 0000000..584c2af --- /dev/null +++ b/gio/internal/gl/types_js.go @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import "syscall/js" + +type ( + Buffer js.Value + Framebuffer js.Value + Program js.Value + Renderbuffer js.Value + Shader js.Value + Texture js.Value + Query js.Value + Uniform js.Value + Object js.Value +) + +func (p Program) Valid() bool { + return !js.Value(p).IsUndefined() && !js.Value(p).IsNull() +} + +func (s Shader) Valid() bool { + return !js.Value(s).IsUndefined() && !js.Value(s).IsNull() +} + +func (u Uniform) Valid() bool { + return !js.Value(u).IsUndefined() && !js.Value(u).IsNull() +} diff --git a/gio/internal/gl/util.go b/gio/internal/gl/util.go new file mode 100644 index 0000000..3d5b44b --- /dev/null +++ b/gio/internal/gl/util.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import ( + "errors" + "fmt" + "strings" +) + +func CreateProgram(ctx *Functions, vsSrc, fsSrc string, attribs []string) (Program, error) { + vs, err := createShader(ctx, VERTEX_SHADER, vsSrc) + if err != nil { + return Program{}, err + } + defer ctx.DeleteShader(vs) + fs, err := createShader(ctx, FRAGMENT_SHADER, fsSrc) + if err != nil { + return Program{}, err + } + defer ctx.DeleteShader(fs) + prog := ctx.CreateProgram() + if !prog.Valid() { + return Program{}, errors.New("glCreateProgram failed") + } + ctx.AttachShader(prog, vs) + ctx.AttachShader(prog, fs) + for i, a := range attribs { + ctx.BindAttribLocation(prog, Attrib(i), a) + } + ctx.LinkProgram(prog) + if ctx.GetProgrami(prog, LINK_STATUS) == 0 { + log := ctx.GetProgramInfoLog(prog) + ctx.DeleteProgram(prog) + return Program{}, fmt.Errorf("program link failed: %s", strings.TrimSpace(log)) + } + return prog, nil +} + +func CreateComputeProgram(ctx *Functions, src string) (Program, error) { + cs, err := createShader(ctx, COMPUTE_SHADER, src) + if err != nil { + return Program{}, err + } + defer ctx.DeleteShader(cs) + prog := ctx.CreateProgram() + if !prog.Valid() { + return Program{}, errors.New("glCreateProgram failed") + } + ctx.AttachShader(prog, cs) + ctx.LinkProgram(prog) + if ctx.GetProgrami(prog, LINK_STATUS) == 0 { + log := ctx.GetProgramInfoLog(prog) + ctx.DeleteProgram(prog) + return Program{}, fmt.Errorf("program link failed: %s", strings.TrimSpace(log)) + } + return prog, nil +} + +func createShader(ctx *Functions, typ Enum, src string) (Shader, error) { + sh := ctx.CreateShader(typ) + if !sh.Valid() { + return Shader{}, errors.New("glCreateShader failed") + } + ctx.ShaderSource(sh, src) + ctx.CompileShader(sh) + if ctx.GetShaderi(sh, COMPILE_STATUS) == 0 { + log := ctx.GetShaderInfoLog(sh) + ctx.DeleteShader(sh) + return Shader{}, fmt.Errorf("shader compilation failed: %s", strings.TrimSpace(log)) + } + return sh, nil +} + +func ParseGLVersion(glVer string) (version [2]int, gles bool, err error) { + var ver [2]int + if _, err := fmt.Sscanf(glVer, "OpenGL ES %d.%d", &ver[0], &ver[1]); err == nil { + return ver, true, nil + } else if _, err := fmt.Sscanf(glVer, "WebGL %d.%d", &ver[0], &ver[1]); err == nil { + // WebGL major version v corresponds to OpenGL ES version v + 1 + ver[0]++ + return ver, true, nil + } else if _, err := fmt.Sscanf(glVer, "%d.%d", &ver[0], &ver[1]); err == nil { + return ver, false, nil + } + return ver, false, fmt.Errorf("failed to parse OpenGL ES version (%s)", glVer) +} diff --git a/gio/internal/opconst/ops.go b/gio/internal/opconst/ops.go new file mode 100644 index 0000000..db9dd8d --- /dev/null +++ b/gio/internal/opconst/ops.go @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package opconst + +type OpType byte + +// Start at a high number for easier debugging. +const firstOpIndex = 200 + +const ( + TypeMacro OpType = iota + firstOpIndex + TypeCall + TypeDefer + TypeTransform + TypeInvalidate + TypeImage + TypePaint + TypeColor + TypeLinearGradient + TypeArea + TypePointerInput + TypePass + TypeClipboardRead + TypeClipboardWrite + TypeKeyInput + TypeKeyFocus + TypeKeySoftKeyboard + TypeSave + TypeLoad + TypeAux + TypeClip + TypeProfile + TypeCursor + TypePath + TypeStroke +) + +const ( + TypeMacroLen = 1 + 4 + 4 + TypeCallLen = 1 + 4 + 4 + TypeDeferLen = 1 + TypeTransformLen = 1 + 4*6 + TypeRedrawLen = 1 + 8 + TypeImageLen = 1 + TypePaintLen = 1 + TypeColorLen = 1 + 4 + TypeLinearGradientLen = 1 + 8*2 + 4*2 + TypeAreaLen = 1 + 1 + 4*4 + TypePointerInputLen = 1 + 1 + 1 + 2*4 + 2*4 + TypePassLen = 1 + 1 + TypeClipboardReadLen = 1 + TypeClipboardWriteLen = 1 + TypeKeyInputLen = 1 + TypeKeyFocusLen = 1 + TypeKeySoftKeyboardLen = 1 + 1 + TypeSaveLen = 1 + 4 + TypeLoadLen = 1 + 1 + 4 + TypeAuxLen = 1 + TypeClipLen = 1 + 4*4 + 1 + TypeProfileLen = 1 + TypeCursorLen = 1 + 1 + TypePathLen = 1 + TypeStrokeLen = 1 + 4 +) + +// StateMask is a bitmask of state types a load operation +// should restore. +type StateMask uint8 + +const ( + TransformState StateMask = 1 << iota + + AllState = ^StateMask(0) +) + +// InitialStateID is the ID for saving and loading +// the initial operation state. +const InitialStateID = 0 + +func (t OpType) Size() int { + return [...]int{ + TypeMacroLen, + TypeCallLen, + TypeDeferLen, + TypeTransformLen, + TypeRedrawLen, + TypeImageLen, + TypePaintLen, + TypeColorLen, + TypeLinearGradientLen, + TypeAreaLen, + TypePointerInputLen, + TypePassLen, + TypeClipboardReadLen, + TypeClipboardWriteLen, + TypeKeyInputLen, + TypeKeyFocusLen, + TypeKeySoftKeyboardLen, + TypeSaveLen, + TypeLoadLen, + TypeAuxLen, + TypeClipLen, + TypeProfileLen, + TypeCursorLen, + TypePathLen, + TypeStrokeLen, + }[t-firstOpIndex] +} + +func (t OpType) NumRefs() int { + switch t { + case TypeKeyInput, TypeKeyFocus, TypePointerInput, TypeProfile, TypeCall, TypeClipboardRead, TypeClipboardWrite, TypeCursor: + return 1 + case TypeImage: + return 2 + default: + return 0 + } +} diff --git a/gio/internal/ops/ops.go b/gio/internal/ops/ops.go new file mode 100644 index 0000000..a25839f --- /dev/null +++ b/gio/internal/ops/ops.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package ops + +import ( + "encoding/binary" + "math" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/byteslice" + "realy.lol/gio/internal/opconst" + "realy.lol/gio/internal/scene" +) + +func DecodeCommand(d []byte) scene.Command { + var cmd scene.Command + copy(byteslice.Uint32(cmd[:]), d) + return cmd +} + +func EncodeCommand(out []byte, cmd scene.Command) { + copy(out, byteslice.Uint32(cmd[:])) +} + +func DecodeTransform(data []byte) (t f32.Affine2D) { + if opconst.OpType(data[0]) != opconst.TypeTransform { + panic("invalid op") + } + data = data[1:] + data = data[:4*6] + + bo := binary.LittleEndian + a := math.Float32frombits(bo.Uint32(data)) + b := math.Float32frombits(bo.Uint32(data[4*1:])) + c := math.Float32frombits(bo.Uint32(data[4*2:])) + d := math.Float32frombits(bo.Uint32(data[4*3:])) + e := math.Float32frombits(bo.Uint32(data[4*4:])) + f := math.Float32frombits(bo.Uint32(data[4*5:])) + return f32.NewAffine2D(a, b, c, d, e, f) +} + +// DecodeSave decodes the state id of a save op. +func DecodeSave(data []byte) int { + if opconst.OpType(data[0]) != opconst.TypeSave { + panic("invalid op") + } + bo := binary.LittleEndian + return int(bo.Uint32(data[1:])) +} + +// DecodeLoad decodes the state id and mask of a load op. +func DecodeLoad(data []byte) (int, opconst.StateMask) { + if opconst.OpType(data[0]) != opconst.TypeLoad { + panic("invalid op") + } + bo := binary.LittleEndian + return int(bo.Uint32(data[2:])), opconst.StateMask(data[1]) +} diff --git a/gio/internal/ops/reader.go b/gio/internal/ops/reader.go new file mode 100644 index 0000000..8465446 --- /dev/null +++ b/gio/internal/ops/reader.go @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package ops + +import ( + "encoding/binary" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/opconst" + "realy.lol/gio/op" +) + +// Reader parses an ops list. +type Reader struct { + pc PC + stack []macro + ops *op.Ops + deferOps op.Ops + deferDone bool +} + +// EncodedOp represents an encoded op returned by +// Reader. +type EncodedOp struct { + Key Key + Data []byte + Refs []interface{} +} + +// Key is a unique key for a given op. +type Key struct { + ops *op.Ops + pc int + version int + sx, hx, sy, hy float32 +} + +// Shadow of op.MacroOp. +type macroOp struct { + ops *op.Ops + pc PC +} + +// PC is an instruction counter for an operation list. +type PC struct { + data int + refs int +} + +type macro struct { + ops *op.Ops + retPC PC + endPC PC +} + +type opMacroDef struct { + endpc PC +} + +// Reset start reading from the beginning of ops. +func (r *Reader) Reset(ops *op.Ops) { + r.ResetAt(ops, PC{}) +} + +// ResetAt is like Reset, except it starts reading from pc. +func (r *Reader) ResetAt(ops *op.Ops, pc PC) { + r.stack = r.stack[:0] + r.deferOps.Reset() + r.deferDone = false + r.pc = pc + r.ops = ops +} + +// NewPC returns a PC representing the current instruction counter of +// ops. +func NewPC(ops *op.Ops) PC { + return PC{ + data: len(ops.Data()), + refs: len(ops.Refs()), + } +} + +func (k Key) SetTransform(t f32.Affine2D) Key { + sx, hx, _, hy, sy, _ := t.Elems() + k.sx = sx + k.hx = hx + k.hy = hy + k.sy = sy + return k +} + +func (r *Reader) Decode() (EncodedOp, bool) { + if r.ops == nil { + return EncodedOp{}, false + } + deferring := false + for { + if len(r.stack) > 0 { + b := r.stack[len(r.stack)-1] + if r.pc == b.endPC { + r.ops = b.ops + r.pc = b.retPC + r.stack = r.stack[:len(r.stack)-1] + continue + } + } + data := r.ops.Data() + data = data[r.pc.data:] + refs := r.ops.Refs() + if len(data) == 0 { + if r.deferDone { + return EncodedOp{}, false + } + r.deferDone = true + // Execute deferred macros. + r.ops = &r.deferOps + r.pc = PC{} + continue + } + key := Key{ops: r.ops, pc: r.pc.data, version: r.ops.Version()} + t := opconst.OpType(data[0]) + n := t.Size() + nrefs := t.NumRefs() + data = data[:n] + refs = refs[r.pc.refs:] + refs = refs[:nrefs] + switch t { + case opconst.TypeDefer: + deferring = true + r.pc.data += n + r.pc.refs += nrefs + continue + case opconst.TypeAux: + // An Aux operations is always wrapped in a macro, and + // its length is the remaining space. + block := r.stack[len(r.stack)-1] + n += block.endPC.data - r.pc.data - opconst.TypeAuxLen + data = data[:n] + case opconst.TypeCall: + if deferring { + deferring = false + // Copy macro for deferred execution. + if t.NumRefs() != 1 { + panic("internal error: unexpected number of macro refs") + } + deferData := r.deferOps.Write1(t.Size(), refs[0]) + copy(deferData, data) + continue + } + var op macroOp + op.decode(data, refs) + macroData := op.ops.Data()[op.pc.data:] + if opconst.OpType(macroData[0]) != opconst.TypeMacro { + panic("invalid macro reference") + } + var opDef opMacroDef + opDef.decode(macroData[:opconst.TypeMacro.Size()]) + retPC := r.pc + retPC.data += n + retPC.refs += nrefs + r.stack = append(r.stack, macro{ + ops: r.ops, + retPC: retPC, + endPC: opDef.endpc, + }) + r.ops = op.ops + r.pc = op.pc + r.pc.data += opconst.TypeMacro.Size() + r.pc.refs += opconst.TypeMacro.NumRefs() + continue + case opconst.TypeMacro: + var op opMacroDef + op.decode(data) + r.pc = op.endpc + continue + } + r.pc.data += n + r.pc.refs += nrefs + return EncodedOp{Key: key, Data: data, Refs: refs}, true + } +} + +func (op *opMacroDef) decode(data []byte) { + if opconst.OpType(data[0]) != opconst.TypeMacro { + panic("invalid op") + } + bo := binary.LittleEndian + data = data[:9] + dataIdx := int(int32(bo.Uint32(data[1:]))) + refsIdx := int(int32(bo.Uint32(data[5:]))) + *op = opMacroDef{ + endpc: PC{ + data: dataIdx, + refs: refsIdx, + }, + } +} + +func (m *macroOp) decode(data []byte, refs []interface{}) { + if opconst.OpType(data[0]) != opconst.TypeCall { + panic("invalid op") + } + data = data[:9] + bo := binary.LittleEndian + dataIdx := int(int32(bo.Uint32(data[1:]))) + refsIdx := int(int32(bo.Uint32(data[5:]))) + *m = macroOp{ + ops: refs[0].(*op.Ops), + pc: PC{ + data: dataIdx, + refs: refsIdx, + }, + } +} diff --git a/gio/internal/scene/scene.go b/gio/internal/scene/scene.go new file mode 100644 index 0000000..8761a13 --- /dev/null +++ b/gio/internal/scene/scene.go @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package scene encodes and decodes graphics commands in the format used by the +// compute renderer. +package scene + +import ( + "fmt" + "image/color" + "math" + "unsafe" + + "realy.lol/gio/f32" +) + +type Op uint32 + +type Command [sceneElemSize / 4]uint32 + +// GPU commands from scene.h +const ( + OpNop Op = iota + OpLine + OpQuad + OpCubic + OpFillColor + OpLineWidth + OpTransform + OpBeginClip + OpEndClip + OpFillImage + OpSetFillMode +) + +// FillModes, from setup.h. +type FillMode uint32 + +const ( + FillModeNonzero = 0 + FillModeStroke = 1 +) + +const CommandSize = int(unsafe.Sizeof(Command{})) + +const sceneElemSize = 36 + +func (c Command) Op() Op { + return Op(c[0]) +} + +func (c Command) String() string { + switch Op(c[0]) { + case OpNop: + return "nop" + case OpLine: + from, to := DecodeLine(c) + return fmt.Sprintf("line(%v, %v)", from, to) + case OpQuad: + from, ctrl, to := DecodeQuad(c) + return fmt.Sprintf("quad(%v, %v, %v)", from, ctrl, to) + case OpCubic: + from, ctrl0, ctrl1, to := DecodeCubic(c) + return fmt.Sprintf("cubic(%v, %v, %v, %v)", from, ctrl0, ctrl1, to) + case OpFillColor: + return "fillcolor" + case OpLineWidth: + return "linewidth" + case OpTransform: + t := f32.NewAffine2D( + math.Float32frombits(c[1]), + math.Float32frombits(c[3]), + math.Float32frombits(c[5]), + math.Float32frombits(c[2]), + math.Float32frombits(c[4]), + math.Float32frombits(c[6]), + ) + return fmt.Sprintf("transform (%v)", t) + case OpBeginClip: + bounds := f32.Rectangle{ + Min: f32.Pt(math.Float32frombits(c[1]), math.Float32frombits(c[2])), + Max: f32.Pt(math.Float32frombits(c[3]), math.Float32frombits(c[4])), + } + return fmt.Sprintf("beginclip (%v)", bounds) + case OpEndClip: + bounds := f32.Rectangle{ + Min: f32.Pt(math.Float32frombits(c[1]), math.Float32frombits(c[2])), + Max: f32.Pt(math.Float32frombits(c[3]), math.Float32frombits(c[4])), + } + return fmt.Sprintf("endclip (%v)", bounds) + case OpFillImage: + return "fillimage" + case OpSetFillMode: + return "setfillmode" + default: + panic("unreachable") + } +} + +func Line(start, end f32.Point) Command { + return Command{ + 0: uint32(OpLine), + 1: math.Float32bits(start.X), + 2: math.Float32bits(start.Y), + 3: math.Float32bits(end.X), + 4: math.Float32bits(end.Y), + } +} + +func Cubic(start, ctrl0, ctrl1, end f32.Point) Command { + return Command{ + 0: uint32(OpCubic), + 1: math.Float32bits(start.X), + 2: math.Float32bits(start.Y), + 3: math.Float32bits(ctrl0.X), + 4: math.Float32bits(ctrl0.Y), + 5: math.Float32bits(ctrl1.X), + 6: math.Float32bits(ctrl1.Y), + 7: math.Float32bits(end.X), + 8: math.Float32bits(end.Y), + } +} + +func Quad(start, ctrl, end f32.Point) Command { + return Command{ + 0: uint32(OpQuad), + 1: math.Float32bits(start.X), + 2: math.Float32bits(start.Y), + 3: math.Float32bits(ctrl.X), + 4: math.Float32bits(ctrl.Y), + 5: math.Float32bits(end.X), + 6: math.Float32bits(end.Y), + } +} + +func Transform(m f32.Affine2D) Command { + sx, hx, ox, hy, sy, oy := m.Elems() + return Command{ + 0: uint32(OpTransform), + 1: math.Float32bits(sx), + 2: math.Float32bits(hy), + 3: math.Float32bits(hx), + 4: math.Float32bits(sy), + 5: math.Float32bits(ox), + 6: math.Float32bits(oy), + } +} + +func SetLineWidth(width float32) Command { + return Command{ + 0: uint32(OpLineWidth), + 1: math.Float32bits(width), + } +} + +func BeginClip(bbox f32.Rectangle) Command { + return Command{ + 0: uint32(OpBeginClip), + 1: math.Float32bits(bbox.Min.X), + 2: math.Float32bits(bbox.Min.Y), + 3: math.Float32bits(bbox.Max.X), + 4: math.Float32bits(bbox.Max.Y), + } +} + +func EndClip(bbox f32.Rectangle) Command { + return Command{ + 0: uint32(OpEndClip), + 1: math.Float32bits(bbox.Min.X), + 2: math.Float32bits(bbox.Min.Y), + 3: math.Float32bits(bbox.Max.X), + 4: math.Float32bits(bbox.Max.Y), + } +} + +func FillColor(col color.RGBA) Command { + return Command{ + 0: uint32(OpFillColor), + 1: uint32(col.R)<<24 | uint32(col.G)<<16 | uint32(col.B)<<8 | uint32(col.A), + } +} + +func FillImage(index int) Command { + return Command{ + 0: uint32(OpFillImage), + 1: uint32(index), + } +} + +func SetFillMode(mode FillMode) Command { + return Command{ + 0: uint32(OpSetFillMode), + 1: uint32(mode), + } +} + +func DecodeLine(cmd Command) (from, to f32.Point) { + if cmd[0] != uint32(OpLine) { + panic("invalid command") + } + from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2])) + to = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4])) + return +} + +func DecodeQuad(cmd Command) (from, ctrl, to f32.Point) { + if cmd[0] != uint32(OpQuad) { + panic("invalid command") + } + from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2])) + ctrl = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4])) + to = f32.Pt(math.Float32frombits(cmd[5]), math.Float32frombits(cmd[6])) + return +} + +func DecodeCubic(cmd Command) (from, ctrl0, ctrl1, to f32.Point) { + if cmd[0] != uint32(OpCubic) { + panic("invalid command") + } + from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2])) + ctrl0 = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4])) + ctrl1 = f32.Pt(math.Float32frombits(cmd[5]), math.Float32frombits(cmd[6])) + to = f32.Pt(math.Float32frombits(cmd[7]), math.Float32frombits(cmd[8])) + return +} diff --git a/gio/internal/srgb/srgb.go b/gio/internal/srgb/srgb.go new file mode 100644 index 0000000..1cd67cf --- /dev/null +++ b/gio/internal/srgb/srgb.go @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package srgb + +import ( + "fmt" + "runtime" + "strings" + + "realy.lol/gio/internal/byteslice" + "realy.lol/gio/internal/gl" +) + +// FBO implements an intermediate sRGB FBO +// for gamma-correct rendering on platforms without +// sRGB enabled native framebuffers. +type FBO struct { + c *gl.Functions + width, height int + frameBuffer gl.Framebuffer + depthBuffer gl.Renderbuffer + colorTex gl.Texture + blitted bool + quad gl.Buffer + prog gl.Program + gl3 bool +} + +func New(ctx gl.Context) (*FBO, error) { + f, err := gl.NewFunctions(ctx) + if err != nil { + return nil, err + } + var gl3 bool + glVer := f.GetString(gl.VERSION) + ver, _, err := gl.ParseGLVersion(glVer) + if err != nil { + return nil, err + } + if ver[0] >= 3 { + gl3 = true + } else { + exts := f.GetString(gl.EXTENSIONS) + if !strings.Contains(exts, "EXT_sRGB") { + return nil, fmt.Errorf("no support for OpenGL ES 3 nor EXT_sRGB") + } + } + s := &FBO{ + c: f, + gl3: gl3, + frameBuffer: f.CreateFramebuffer(), + colorTex: f.CreateTexture(), + depthBuffer: f.CreateRenderbuffer(), + } + f.BindTexture(gl.TEXTURE_2D, s.colorTex) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) + return s, nil +} + +func (s *FBO) Blit() { + if !s.blitted { + prog, err := gl.CreateProgram(s.c, blitVSrc, blitFSrc, + []string{"pos", "uv"}) + if err != nil { + panic(err) + } + s.prog = prog + s.c.UseProgram(prog) + s.c.Uniform1i(s.c.GetUniformLocation(prog, "tex"), 0) + s.quad = s.c.CreateBuffer() + s.c.BindBuffer(gl.ARRAY_BUFFER, s.quad) + coords := byteslice.Slice([]float32{ + -1, +1, 0, 1, + +1, +1, 1, 1, + -1, -1, 0, 0, + +1, -1, 1, 0, + }) + s.c.BufferData(gl.ARRAY_BUFFER, len(coords), gl.STATIC_DRAW) + s.c.BufferSubData(gl.ARRAY_BUFFER, 0, coords) + s.blitted = true + } + s.c.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{}) + s.c.UseProgram(s.prog) + s.c.BindTexture(gl.TEXTURE_2D, s.colorTex) + s.c.BindBuffer(gl.ARRAY_BUFFER, s.quad) + s.c.VertexAttribPointer(0 /* pos */, 2, gl.FLOAT, false, 4*4, 0) + s.c.VertexAttribPointer(1 /* uv */, 2, gl.FLOAT, false, 4*4, 4*2) + s.c.EnableVertexAttribArray(0) + s.c.EnableVertexAttribArray(1) + s.c.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) + s.c.BindTexture(gl.TEXTURE_2D, gl.Texture{}) + s.c.DisableVertexAttribArray(0) + s.c.DisableVertexAttribArray(1) + s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer) + s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0) + s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT) + // The Android emulator requires framebuffer 0 bound at eglSwapBuffer time. + // Bind the sRGB framebuffer again in afterPresent. + s.c.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{}) +} + +func (s *FBO) AfterPresent() { + s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer) +} + +func (s *FBO) Refresh(w, h int) error { + s.width, s.height = w, h + if w == 0 || h == 0 { + return nil + } + s.c.BindTexture(gl.TEXTURE_2D, s.colorTex) + if s.gl3 { + s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB8_ALPHA8, w, h, gl.RGBA, + gl.UNSIGNED_BYTE) + } else /* EXT_sRGB */ { + s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB_ALPHA_EXT, w, h, + gl.SRGB_ALPHA_EXT, gl.UNSIGNED_BYTE) + } + currentRB := gl.Renderbuffer(s.c.GetBinding(gl.RENDERBUFFER_BINDING)) + s.c.BindRenderbuffer(gl.RENDERBUFFER, s.depthBuffer) + s.c.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h) + s.c.BindRenderbuffer(gl.RENDERBUFFER, currentRB) + s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer) + s.c.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, s.colorTex, 0) + s.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, + gl.RENDERBUFFER, s.depthBuffer) + if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { + return fmt.Errorf("sRGB framebuffer incomplete (%dx%d), status: %#x error: %x", + s.width, s.height, st, s.c.GetError()) + } + + if runtime.GOOS == "js" { + // With macOS Safari, rendering to and then reading from a SRGB8_ALPHA8 + // texture result in twice gamma corrected colors. Using a plain RGBA + // texture seems to work. + s.c.ClearColor(.5, .5, .5, 1.0) + s.c.Clear(gl.COLOR_BUFFER_BIT) + var pixel [4]byte + s.c.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel[:]) + if pixel[0] == 128 { // Correct sRGB color value is ~188 + s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, gl.RGBA, + gl.UNSIGNED_BYTE) + if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { + return fmt.Errorf("fallback RGBA framebuffer incomplete (%dx%d), status: %#x error: %x", + s.width, s.height, st, s.c.GetError()) + } + } + } + + return nil +} + +func (s *FBO) Release() { + s.c.DeleteFramebuffer(s.frameBuffer) + s.c.DeleteTexture(s.colorTex) + s.c.DeleteRenderbuffer(s.depthBuffer) + if s.blitted { + s.c.DeleteBuffer(s.quad) + s.c.DeleteProgram(s.prog) + } + s.c = nil +} + +const ( + blitVSrc = ` +#version 100 + +precision highp float; + +attribute vec2 pos; +attribute vec2 uv; + +varying vec2 vUV; + +void main() { + gl_Position = vec4(pos, 0, 1); + vUV = uv; +} +` + blitFSrc = ` +#version 100 + +precision mediump float; + +uniform sampler2D tex; +varying vec2 vUV; + +vec3 gamma(vec3 rgb) { + vec3 exp = vec3(1.055)*pow(rgb, vec3(0.41666)) - vec3(0.055); + vec3 lin = rgb * vec3(12.92); + bvec3 cut = lessThan(rgb, vec3(0.0031308)); + return vec3(cut.r ? lin.r : exp.r, cut.g ? lin.g : exp.g, cut.b ? lin.b : exp.b); +} + +void main() { + vec4 col = texture2D(tex, vUV); + vec3 rgb = col.rgb; + rgb = gamma(rgb); + gl_FragColor = vec4(rgb, col.a); +} +` +) diff --git a/gio/internal/stroke/dash.go b/gio/internal/stroke/dash.go new file mode 100644 index 0000000..c57a032 --- /dev/null +++ b/gio/internal/stroke/dash.go @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// The algorithms to compute dashes have been extracted, adapted from +// (and used as a reference implementation): +// - github.com/tdewolff/canvas (Licensed under MIT) + +package stroke + +import ( + "math" + "sort" + + "realy.lol/gio/f32" +) + +type DashOp struct { + Phase float32 + Dashes []float32 +} + +func IsSolidLine(sty DashOp) bool { + return sty.Phase == 0 && len(sty.Dashes) == 0 +} + +func (qs StrokeQuads) dash(sty DashOp) StrokeQuads { + sty = dashCanonical(sty) + + switch { + case len(sty.Dashes) == 0: + return qs + case len(sty.Dashes) == 1 && sty.Dashes[0] == 0.0: + return StrokeQuads{} + } + + if len(sty.Dashes)%2 == 1 { + // If the dash pattern is of uneven length, dash and space lengths + // alternate. The following duplicates the pattern so that uneven + // indices are always spaces. + sty.Dashes = append(sty.Dashes, sty.Dashes...) + } + + var ( + i0, pos0 = dashStart(sty) + out StrokeQuads + + contour uint32 = 1 + ) + + for _, ps := range qs.split() { + var ( + i = i0 + pos = pos0 + t []float64 + length = ps.len() + ) + for pos+sty.Dashes[i] < length { + pos += sty.Dashes[i] + if 0.0 < pos { + t = append(t, float64(pos)) + } + i++ + if i == len(sty.Dashes) { + i = 0 + } + } + + j0 := 0 + endsInDash := i%2 == 0 + if len(t)%2 == 1 && endsInDash || len(t)%2 == 0 && !endsInDash { + j0 = 1 + } + + var ( + qd StrokeQuads + pd = ps.splitAt(&contour, t...) + ) + for j := j0; j < len(pd)-1; j += 2 { + qd = qd.append(pd[j]) + } + if endsInDash { + if ps.closed() { + qd = pd[len(pd)-1].append(qd) + } else { + qd = qd.append(pd[len(pd)-1]) + } + } + out = out.append(qd) + contour++ + } + return out +} + +func dashCanonical(sty DashOp) DashOp { + var ( + o = sty + ds = o.Dashes + ) + + if len(sty.Dashes) == 0 { + return sty + } + + // Remove zeros except first and last. + for i := 1; i < len(ds)-1; i++ { + if f32Eq(ds[i], 0.0) { + ds[i-1] += ds[i+1] + ds = append(ds[:i], ds[i+2:]...) + i-- + } + } + + // Remove first zero, collapse with second and last. + if f32Eq(ds[0], 0.0) { + if len(ds) < 3 { + return DashOp{ + Phase: 0.0, + Dashes: []float32{0.0}, + } + } + o.Phase -= ds[1] + ds[len(ds)-1] += ds[1] + ds = ds[2:] + } + + // Remove last zero, collapse with fist and second to last. + if f32Eq(ds[len(ds)-1], 0.0) { + if len(ds) < 3 { + return DashOp{} + } + o.Phase += ds[len(ds)-2] + ds[0] += ds[len(ds)-2] + ds = ds[:len(ds)-2] + } + + // If there are zeros or negatives, don't draw dashes. + for i := 0; i < len(ds); i++ { + if ds[i] < 0.0 || f32Eq(ds[i], 0.0) { + return DashOp{ + Phase: 0.0, + Dashes: []float32{0.0}, + } + } + } + + // Remove repeated patterns. +loop: + for len(ds)%2 == 0 { + mid := len(ds) / 2 + for i := 0; i < mid; i++ { + if !f32Eq(ds[i], ds[mid+i]) { + break loop + } + } + ds = ds[:mid] + } + return o +} + +func dashStart(sty DashOp) (int, float32) { + i0 := 0 // i0 is the index into dashes. + for sty.Dashes[i0] <= sty.Phase { + sty.Phase -= sty.Dashes[i0] + i0++ + if i0 == len(sty.Dashes) { + i0 = 0 + } + } + // pos0 may be negative if the offset lands halfway into dash. + pos0 := -sty.Phase + if sty.Phase < 0.0 { + var sum float32 + for _, d := range sty.Dashes { + sum += d + } + pos0 = -(sum + sty.Phase) // handle negative offsets + } + return i0, pos0 +} + +func (qs StrokeQuads) len() float32 { + var sum float32 + for i := range qs { + q := qs[i].Quad + sum += quadBezierLen(q.From, q.Ctrl, q.To) + } + return sum +} + +// splitAt splits the path into separate paths at the specified intervals +// along the path. +// splitAt updates the provided contour counter as it splits the segments. +func (qs StrokeQuads) splitAt(contour *uint32, ts ...float64) []StrokeQuads { + if len(ts) == 0 { + qs.setContour(*contour) + return []StrokeQuads{qs} + } + + sort.Float64s(ts) + if ts[0] == 0 { + ts = ts[1:] + } + + var ( + j int // index into ts + t float64 // current position along curve + ) + + var oo []StrokeQuads + var oi StrokeQuads + push := func() { + oo = append(oo, oi) + oi = nil + } + + for _, ps := range qs.split() { + for _, q := range ps { + if j == len(ts) { + oi = append(oi, q) + continue + } + speed := func(t float64) float64 { + return float64(lenPt(quadBezierD1(q.Quad.From, q.Quad.Ctrl, + q.Quad.To, float32(t)))) + } + invL, dt := invSpeedPolynomialChebyshevApprox(20, gaussLegendre7, + speed, 0, 1) + + var ( + t0 float64 + r0 = q.Quad.From + r1 = q.Quad.Ctrl + r2 = q.Quad.To + + // from keeps track of the start of the 'running' segment. + from = r0 + ) + for j < len(ts) && t < ts[j] && ts[j] <= t+dt { + tj := invL(ts[j] - t) + tsub := (tj - t0) / (1.0 - t0) + t0 = tj + + var q1 f32.Point + _, q1, _, r0, r1, r2 = quadBezierSplit(r0, r1, r2, + float32(tsub)) + + oi = append(oi, StrokeQuad{ + Contour: *contour, + Quad: QuadSegment{ + From: from, + Ctrl: q1, + To: r0, + }, + }) + push() + (*contour)++ + + from = r0 + j++ + } + if !f64Eq(t0, 1) { + if len(oi) > 0 { + r0 = oi.pen() + } + oi = append(oi, StrokeQuad{ + Contour: *contour, + Quad: QuadSegment{ + From: r0, + Ctrl: r1, + To: r2, + }, + }) + } + t += dt + } + } + if len(oi) > 0 { + push() + (*contour)++ + } + + return oo +} + +func f32Eq(a, b float32) bool { + const epsilon = 1e-10 + return math.Abs(float64(a-b)) < epsilon +} + +func f64Eq(a, b float64) bool { + const epsilon = 1e-10 + return math.Abs(a-b) < epsilon +} + +func invSpeedPolynomialChebyshevApprox(N int, gaussLegendre gaussLegendreFunc, + fp func(float64) float64, tmin, tmax float64) (func(float64) float64, + float64) { + // The TODOs below are copied verbatim from tdewolff/canvas: + // + // TODO: find better way to determine N. For Arc 10 seems fine, for some + // Quads 10 is too low, for Cube depending on inflection points is + // maybe not the best indicator + // + // TODO: track efficiency, how many times is fp called? + // Does a look-up table make more sense? + fLength := func(t float64) float64 { + return math.Abs(gaussLegendre(fp, tmin, t)) + } + totalLength := fLength(tmax) + t := func(L float64) float64 { + return bisectionMethod(fLength, L, tmin, tmax) + } + return polynomialChebyshevApprox(N, t, 0.0, totalLength, tmin, + tmax), totalLength +} + +func polynomialChebyshevApprox(N int, f func(float64) float64, + xmin, xmax, ymin, ymax float64) func(float64) float64 { + var ( + invN = 1.0 / float64(N) + fs = make([]float64, N) + ) + for k := 0; k < N; k++ { + u := math.Cos(math.Pi * (float64(k+1) - 0.5) * invN) + fs[k] = f(xmin + 0.5*(xmax-xmin)*(u+1)) + } + + c := make([]float64, N) + for j := 0; j < N; j++ { + var a float64 + for k := 0; k < N; k++ { + a += fs[k] * math.Cos(float64(j)*math.Pi*(float64(k+1)-0.5)/float64(N)) + } + c[j] = 2 * invN * a + } + + if ymax < ymin { + ymin, ymax = ymax, ymin + } + return func(x float64) float64 { + x = math.Min(xmax, math.Max(xmin, x)) + u := (x-xmin)/(xmax-xmin)*2 - 1 + var a float64 + for j := 0; j < N; j++ { + a += c[j] * math.Cos(float64(j)*math.Acos(u)) + } + y := -0.5*c[0] + a + if !math.IsNaN(ymin) && !math.IsNaN(ymax) { + y = math.Min(ymax, math.Max(ymin, y)) + } + return y + } +} + +// bisectionMethod finds the value x for which f(x) = y in the interval x +// in [xmin, xmax] using the bisection method. +func bisectionMethod(f func(float64) float64, y, xmin, xmax float64) float64 { + const ( + maxIter = 100 + tolerance = 0.001 // 0.1% + ) + + var ( + n = 0 + x float64 + tolX = math.Abs(xmax-xmin) * tolerance + tolY = math.Abs(f(xmax)-f(xmin)) * tolerance + ) + for { + x = 0.5 * (xmin + xmax) + if n >= maxIter { + return x + } + + dy := f(x) - y + switch { + case math.Abs(dy) < tolY, math.Abs(0.5*(xmax-xmin)) < tolX: + return x + case dy > 0: + xmax = x + default: + xmin = x + } + n++ + } +} + +type gaussLegendreFunc func(func(float64) float64, float64, float64) float64 + +// Gauss-Legendre quadrature integration from a to b with n=7 +func gaussLegendre7(f func(float64) float64, a, b float64) float64 { + c := 0.5 * (b - a) + d := 0.5 * (a + b) + Qd1 := f(-0.949108*c + d) + Qd2 := f(-0.741531*c + d) + Qd3 := f(-0.405845*c + d) + Qd4 := f(d) + Qd5 := f(0.405845*c + d) + Qd6 := f(0.741531*c + d) + Qd7 := f(0.949108*c + d) + return c * (0.129485*(Qd1+Qd7) + 0.279705*(Qd2+Qd6) + 0.381830*(Qd3+Qd5) + 0.417959*Qd4) +} diff --git a/gio/internal/stroke/stroke.go b/gio/internal/stroke/stroke.go new file mode 100644 index 0000000..b88a432 --- /dev/null +++ b/gio/internal/stroke/stroke.go @@ -0,0 +1,902 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Most of the algorithms to compute strokes and their offsets have been +// extracted, adapted from (and used as a reference implementation): +// - github.com/tdewolff/canvas (Licensed under MIT) +// +// These algorithms have been implemented from: +// Fast, precise flattening of cubic BĆ©zier path and offset curves +// Thomas F. Hain, et al. +// +// An electronic version is available at: +// https://seant23.files.wordpress.com/2010/11/fastpreciseflatteningofbeziercurve.pdf +// +// Possible improvements (in term of speed and/or accuracy) on these +// algorithms are: +// +// - Polar Stroking: New Theory and Methods for Stroking Paths, +// M. Kilgard +// https://arxiv.org/pdf/2007.00308.pdf +// +// - https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html +// R. Levien + +// Package stroke implements conversion of strokes to filled outlines. It is used as a +// fallback for stroke configurations not natively supported by the renderer. +package stroke + +import ( + "encoding/binary" + "math" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/ops" + "realy.lol/gio/internal/scene" +) + +// The following are copies of types from op/clip to avoid a circular import of +// that package. +// TODO: when the old renderer is gone, this package can be merged with +// op/clip, eliminating the duplicate types. +type StrokeStyle struct { + Width float32 + Miter float32 + Cap StrokeCap + Join StrokeJoin +} + +type StrokeCap uint8 + +const ( + RoundCap StrokeCap = iota + FlatCap + SquareCap +) + +type StrokeJoin uint8 + +const ( + RoundJoin StrokeJoin = iota + BevelJoin +) + +// strokeTolerance is used to reconcile rounding errors arising +// when splitting quads into smaller and smaller segments to approximate +// them into straight lines, and when joining back segments. +// +// The magic value of 0.01 was found by striking a compromise between +// aesthetic looking (curves did look like curves, even after linearization) +// and speed. +const strokeTolerance = 0.01 + +type QuadSegment struct { + From, Ctrl, To f32.Point +} + +type StrokeQuad struct { + Contour uint32 + Quad QuadSegment +} + +type strokeState struct { + p0, p1 f32.Point // p0 is the start point, p1 the end point. + n0, n1 f32.Point // n0 is the normal vector at the start point, n1 at the end point. + r0, r1 float32 // r0 is the curvature at the start point, r1 at the end point. + ctl f32.Point // ctl is the control point of the quadratic BĆ©zier segment. +} + +type StrokeQuads []StrokeQuad + +func (qs *StrokeQuads) setContour(n uint32) { + for i := range *qs { + (*qs)[i].Contour = n + } +} + +func (qs *StrokeQuads) pen() f32.Point { + return (*qs)[len(*qs)-1].Quad.To +} + +func (qs *StrokeQuads) closed() bool { + beg := (*qs)[0].Quad.From + end := (*qs)[len(*qs)-1].Quad.To + return f32Eq(beg.X, end.X) && f32Eq(beg.Y, end.Y) +} + +func (qs *StrokeQuads) lineTo(pt f32.Point) { + end := qs.pen() + *qs = append(*qs, StrokeQuad{ + Quad: QuadSegment{ + From: end, + Ctrl: end.Add(pt).Mul(0.5), + To: pt, + }, + }) +} + +func (qs *StrokeQuads) arc(f1, f2 f32.Point, angle float32) { + const segments = 16 + pen := qs.pen() + m := ArcTransform(pen, f1.Add(pen), f2.Add(pen), angle, segments) + for i := 0; i < segments; i++ { + p0 := qs.pen() + p1 := m.Transform(p0) + p2 := m.Transform(p1) + ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5)) + *qs = append(*qs, StrokeQuad{ + Quad: QuadSegment{ + From: p0, Ctrl: ctl, To: p2, + }, + }) + } +} + +// split splits a slice of quads into slices of quads grouped +// by contours (ie: splitted at move-to boundaries). +func (qs StrokeQuads) split() []StrokeQuads { + if len(qs) == 0 { + return nil + } + + var ( + c uint32 + o []StrokeQuads + i = len(o) + ) + for _, q := range qs { + if q.Contour != c { + c = q.Contour + i = len(o) + o = append(o, StrokeQuads{}) + } + o[i] = append(o[i], q) + } + + return o +} + +func (qs StrokeQuads) stroke(stroke StrokeStyle, dashes DashOp) StrokeQuads { + if !IsSolidLine(dashes) { + qs = qs.dash(dashes) + } + + var ( + o StrokeQuads + hw = 0.5 * stroke.Width + ) + + for _, ps := range qs.split() { + rhs, lhs := ps.offset(hw, stroke) + switch lhs { + case nil: + o = o.append(rhs) + default: + // Closed path. + // Inner path should go opposite direction to cancel outer path. + switch { + case ps.ccw(): + lhs = lhs.reverse() + o = o.append(rhs) + o = o.append(lhs) + default: + rhs = rhs.reverse() + o = o.append(lhs) + o = o.append(rhs) + } + } + } + + return o +} + +// offset returns the right-hand and left-hand sides of the path, offset by +// the half-width hw. +// The stroke handles how segments are joined and ends are capped. +func (qs StrokeQuads) offset(hw float32, + stroke StrokeStyle) (rhs, lhs StrokeQuads) { + var ( + states []strokeState + beg = qs[0].Quad.From + end = qs[len(qs)-1].Quad.To + closed = beg == end + ) + for i := range qs { + q := qs[i].Quad + + var ( + n0 = strokePathNorm(q.From, q.Ctrl, q.To, 0, hw) + n1 = strokePathNorm(q.From, q.Ctrl, q.To, 1, hw) + r0 = strokePathCurv(q.From, q.Ctrl, q.To, 0) + r1 = strokePathCurv(q.From, q.Ctrl, q.To, 1) + ) + states = append(states, strokeState{ + p0: q.From, + p1: q.To, + n0: n0, + n1: n1, + r0: r0, + r1: r1, + ctl: q.Ctrl, + }) + } + + for i, state := range states { + rhs = rhs.append(strokeQuadBezier(state, +hw, strokeTolerance)) + lhs = lhs.append(strokeQuadBezier(state, -hw, strokeTolerance)) + + // join the current and next segments + if hasNext := i+1 < len(states); hasNext || closed { + var next strokeState + switch { + case hasNext: + next = states[i+1] + case closed: + next = states[0] + } + if state.n1 != next.n0 { + strokePathJoin(stroke, &rhs, &lhs, hw, state.p1, state.n1, + next.n0, state.r1, next.r0) + } + } + } + + if closed { + rhs.close() + lhs.close() + return rhs, lhs + } + + qbeg := &states[0] + qend := &states[len(states)-1] + + // Default to counter-clockwise direction. + lhs = lhs.reverse() + strokePathCap(stroke, &rhs, hw, qend.p1, qend.n1) + + rhs = rhs.append(lhs) + strokePathCap(stroke, &rhs, hw, qbeg.p0, qbeg.n0.Mul(-1)) + + rhs.close() + + return rhs, nil +} + +func (qs *StrokeQuads) close() { + p0 := (*qs)[len(*qs)-1].Quad.To + p1 := (*qs)[0].Quad.From + + if p1 == p0 { + return + } + + *qs = append(*qs, StrokeQuad{ + Quad: QuadSegment{ + From: p0, + Ctrl: p0.Add(p1).Mul(0.5), + To: p1, + }, + }) +} + +// ccw returns whether the path is counter-clockwise. +func (qs StrokeQuads) ccw() bool { + // Use the Shoelace formula: + // https://en.wikipedia.org/wiki/Shoelace_formula + var area float32 + for _, ps := range qs.split() { + for i := 1; i < len(ps); i++ { + pi := ps[i].Quad.To + pj := ps[i-1].Quad.To + area += (pi.X - pj.X) * (pi.Y + pj.Y) + } + } + return area <= 0.0 +} + +func (qs StrokeQuads) reverse() StrokeQuads { + if len(qs) == 0 { + return nil + } + + ps := make(StrokeQuads, 0, len(qs)) + for i := range qs { + q := qs[len(qs)-1-i] + q.Quad.To, q.Quad.From = q.Quad.From, q.Quad.To + ps = append(ps, q) + } + + return ps +} + +func (qs StrokeQuads) append(ps StrokeQuads) StrokeQuads { + switch { + case len(ps) == 0: + return qs + case len(qs) == 0: + return ps + } + + // Consolidate quads and smooth out rounding errors. + // We need to also check for the strokeTolerance to correctly handle + // join/cap points or on-purpose disjoint quads. + p0 := qs[len(qs)-1].Quad.To + p1 := ps[0].Quad.From + if p0 != p1 && lenPt(p0.Sub(p1)) < strokeTolerance { + qs = append(qs, StrokeQuad{ + Quad: QuadSegment{ + From: p0, + Ctrl: p0.Add(p1).Mul(0.5), + To: p1, + }, + }) + } + return append(qs, ps...) +} + +func (q QuadSegment) Transform(t f32.Affine2D) QuadSegment { + q.From = t.Transform(q.From) + q.Ctrl = t.Transform(q.Ctrl) + q.To = t.Transform(q.To) + return q +} + +// strokePathNorm returns the normal vector at t. +func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point { + switch t { + case 0: + n := p1.Sub(p0) + if n.X == 0 && n.Y == 0 { + return f32.Point{} + } + n = rot90CW(n) + return normPt(n, d) + case 1: + n := p2.Sub(p1) + if n.X == 0 && n.Y == 0 { + return f32.Point{} + } + n = rot90CW(n) + return normPt(n, d) + } + panic("impossible") +} + +func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) } +func rot90CCW(p f32.Point) f32.Point { return f32.Pt(-p.Y, +p.X) } + +// cosPt returns the cosine of the opening angle between p and q. +func cosPt(p, q f32.Point) float32 { + np := math.Hypot(float64(p.X), float64(p.Y)) + nq := math.Hypot(float64(q.X), float64(q.Y)) + return dotPt(p, q) / float32(np*nq) +} + +func normPt(p f32.Point, l float32) f32.Point { + d := math.Hypot(float64(p.X), float64(p.Y)) + l64 := float64(l) + if math.Abs(d-l64) < 1e-10 { + return f32.Point{} + } + n := float32(l64 / d) + return f32.Point{X: p.X * n, Y: p.Y * n} +} + +func lenPt(p f32.Point) float32 { + return float32(math.Hypot(float64(p.X), float64(p.Y))) +} + +func dotPt(p, q f32.Point) float32 { + return p.X*q.X + p.Y*q.Y +} + +func perpDot(p, q f32.Point) float32 { + return p.X*q.Y - p.Y*q.X +} + +// strokePathCurv returns the curvature at t, along the quadratic BĆ©zier +// curve defined by the triplet (beg, ctl, end). +func strokePathCurv(beg, ctl, end f32.Point, t float32) float32 { + var ( + d1p = quadBezierD1(beg, ctl, end, t) + d2p = quadBezierD2(beg, ctl, end, t) + + // Negative when bending right, ie: the curve is CW at this point. + a = float64(perpDot(d1p, d2p)) + ) + + // We check early that the segment isn't too line-like and + // save a costly call to math.Pow that will be discarded by dividing + // with a too small 'a'. + if math.Abs(a) < 1e-10 { + return float32(math.NaN()) + } + return float32(math.Pow(float64(d1p.X*d1p.X+d1p.Y*d1p.Y), 1.5) / a) +} + +// quadBezierSample returns the point on the BĆ©zier curve at t. +// B(t) = (1-t)^2 P0 + 2(1-t)t P1 + t^2 P2 +func quadBezierSample(p0, p1, p2 f32.Point, t float32) f32.Point { + t1 := 1 - t + c0 := t1 * t1 + c1 := 2 * t1 * t + c2 := t * t + + o := p0.Mul(c0) + o = o.Add(p1.Mul(c1)) + o = o.Add(p2.Mul(c2)) + return o +} + +// quadBezierD1 returns the first derivative of the BĆ©zier curve with respect to t. +// B'(t) = 2(1-t)(P1 - P0) + 2t(P2 - P1) +func quadBezierD1(p0, p1, p2 f32.Point, t float32) f32.Point { + p10 := p1.Sub(p0).Mul(2 * (1 - t)) + p21 := p2.Sub(p1).Mul(2 * t) + + return p10.Add(p21) +} + +// quadBezierD2 returns the second derivative of the BĆ©zier curve with respect to t: +// B''(t) = 2(P2 - 2P1 + P0) +func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point { + p := p2.Sub(p1.Mul(2)).Add(p0) + return p.Mul(2) +} + +// quadBezierLen returns the length of the BĆ©zier curve. +// See: +// https://malczak.linuxpl.com/blog/quadratic-bezier-curve-length/ +func quadBezierLen(p0, p1, p2 f32.Point) float32 { + a := p0.Sub(p1.Mul(2)).Add(p2) + b := p1.Mul(2).Sub(p0.Mul(2)) + A := float64(4 * dotPt(a, a)) + B := float64(4 * dotPt(a, b)) + C := float64(dotPt(b, b)) + if f64Eq(A, 0.0) { + // p1 is in the middle between p0 and p2, + // so it is a straight line from p0 to p2. + return lenPt(p2.Sub(p0)) + } + + Sabc := 2 * math.Sqrt(A+B+C) + A2 := math.Sqrt(A) + A32 := 2 * A * A2 + C2 := 2 * math.Sqrt(C) + BA := B / A2 + return float32((A32*Sabc + A2*B*(Sabc-C2) + (4*C*A-B*B)*math.Log((2*A2+BA+Sabc)/(BA+C2))) / (4 * A32)) +} + +func strokeQuadBezier(state strokeState, d, flatness float32) StrokeQuads { + // Gio strokes are only quadratic BĆ©zier curves, w/o any inflection point. + // So we just have to flatten them. + var qs StrokeQuads + return flattenQuadBezier(qs, state.p0, state.ctl, state.p1, d, flatness) +} + +// flattenQuadBezier splits a BĆ©zier quadratic curve into linear sub-segments, +// themselves also encoded as BĆ©zier (degenerate, flat) quadratic curves. +func flattenQuadBezier(qs StrokeQuads, p0, p1, p2 f32.Point, + d, flatness float32) StrokeQuads { + var ( + t float32 + flat64 = float64(flatness) + ) + for t < 1 { + s2 := float64((p2.X-p0.X)*(p1.Y-p0.Y) - (p2.Y-p0.Y)*(p1.X-p0.X)) + den := math.Hypot(float64(p1.X-p0.X), float64(p1.Y-p0.Y)) + if s2*den == 0.0 { + break + } + + s2 /= den + t = 2.0 * float32(math.Sqrt(flat64/3.0/math.Abs(s2))) + if t >= 1.0 { + break + } + var q0, q1, q2 f32.Point + q0, q1, q2, p0, p1, p2 = quadBezierSplit(p0, p1, p2, t) + qs.addLine(q0, q1, q2, 0, d) + } + qs.addLine(p0, p1, p2, 1, d) + return qs +} + +func (qs *StrokeQuads) addLine(p0, ctrl, p1 f32.Point, t, d float32) { + + switch i := len(*qs); i { + case 0: + p0 = p0.Add(strokePathNorm(p0, ctrl, p1, 0, d)) + default: + // Address possible rounding errors and use previous point. + p0 = (*qs)[i-1].Quad.To + } + + p1 = p1.Add(strokePathNorm(p0, ctrl, p1, 1, d)) + + *qs = append(*qs, + StrokeQuad{ + Quad: QuadSegment{ + From: p0, + Ctrl: p0.Add(p1).Mul(0.5), + To: p1, + }, + }, + ) +} + +// quadInterp returns the interpolated point at t. +func quadInterp(p, q f32.Point, t float32) f32.Point { + return f32.Pt( + (1-t)*p.X+t*q.X, + (1-t)*p.Y+t*q.Y, + ) +} + +// quadBezierSplit returns the pair of triplets (from,ctrl,to) BĆ©zier curve, +// split before (resp. after) the provided parametric t value. +func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point, + f32.Point, f32.Point, f32.Point, f32.Point) { + + var ( + b0 = p0 + b1 = quadInterp(p0, p1, t) + b2 = quadBezierSample(p0, p1, p2, t) + + a0 = b2 + a1 = quadInterp(p1, p2, t) + a2 = p2 + ) + + return b0, b1, b2, a0, a1, a2 +} + +// strokePathJoin joins the two paths rhs and lhs, according to the provided +// stroke operation. +func strokePathJoin(stroke StrokeStyle, rhs, lhs *StrokeQuads, hw float32, + pivot, n0, n1 f32.Point, r0, r1 float32) { + if stroke.Miter > 0 { + strokePathMiterJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1) + return + } + switch stroke.Join { + case BevelJoin: + strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + case RoundJoin: + strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + default: + panic("impossible") + } +} + +func strokePathBevelJoin(rhs, lhs *StrokeQuads, hw float32, + pivot, n0, n1 f32.Point, r0, r1 float32) { + + rp := pivot.Add(n1) + lp := pivot.Sub(n1) + + rhs.lineTo(rp) + lhs.lineTo(lp) +} + +func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, + pivot, n0, n1 f32.Point, r0, r1 float32) { + rp := pivot.Add(n1) + lp := pivot.Sub(n1) + cw := dotPt(rot90CW(n0), n1) >= 0.0 + switch { + case cw: + // Path bends to the right, ie. CW (or 180 degree turn). + c := pivot.Sub(lhs.pen()) + angle := -math.Acos(float64(cosPt(n0, n1))) + lhs.arc(c, c, float32(angle)) + lhs.lineTo(lp) // Add a line to accommodate for rounding errors. + rhs.lineTo(rp) + default: + // Path bends to the left, ie. CCW. + angle := math.Acos(float64(cosPt(n0, n1))) + c := pivot.Sub(rhs.pen()) + rhs.arc(c, c, float32(angle)) + rhs.lineTo(rp) // Add a line to accommodate for rounding errors. + lhs.lineTo(lp) + } +} + +func strokePathMiterJoin(stroke StrokeStyle, rhs, lhs *StrokeQuads, hw float32, + pivot, n0, n1 f32.Point, r0, r1 float32) { + if n0 == n1.Mul(-1) { + strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + return + } + + // This is to handle nearly linear joints that would be clipped otherwise. + limit := math.Max(float64(stroke.Miter), 1.001) + + cw := dotPt(rot90CW(n0), n1) >= 0.0 + if cw { + // hw is used to calculate |R|. + // When running CW, n0 and n1 point the other way, + // so the sign of r0 and r1 is negated. + hw = -hw + } + hw64 := float64(hw) + + cos := math.Sqrt(0.5 * (1 + float64(cosPt(n0, n1)))) + d := hw64 / cos + if math.Abs(limit*hw64) < math.Abs(d) { + stroke.Miter = 0 // Set miter to zero to disable the miter joint. + strokePathJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1) + return + } + mid := pivot.Add(normPt(n0.Add(n1), float32(d))) + + rp := pivot.Add(n1) + lp := pivot.Sub(n1) + switch { + case cw: + // Path bends to the right, ie. CW. + lhs.lineTo(mid) + default: + // Path bends to the left, ie. CCW. + rhs.lineTo(mid) + } + rhs.lineTo(rp) + lhs.lineTo(lp) +} + +// strokePathCap caps the provided path qs, according to the provided stroke operation. +func strokePathCap(stroke StrokeStyle, qs *StrokeQuads, hw float32, + pivot, n0 f32.Point) { + switch stroke.Cap { + case FlatCap: + strokePathFlatCap(qs, hw, pivot, n0) + case SquareCap: + strokePathSquareCap(qs, hw, pivot, n0) + case RoundCap: + strokePathRoundCap(qs, hw, pivot, n0) + default: + panic("impossible") + } +} + +// strokePathFlatCap caps the start or end of a path with a flat cap. +func strokePathFlatCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { + end := pivot.Sub(n0) + qs.lineTo(end) +} + +// strokePathSquareCap caps the start or end of a path with a square cap. +func strokePathSquareCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { + var ( + e = pivot.Add(rot90CCW(n0)) + corner1 = e.Add(n0) + corner2 = e.Sub(n0) + end = pivot.Sub(n0) + ) + + qs.lineTo(corner1) + qs.lineTo(corner2) + qs.lineTo(end) +} + +// strokePathRoundCap caps the start or end of a path with a round cap. +func strokePathRoundCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { + c := pivot.Sub(qs.pen()) + qs.arc(c, c, math.Pi) +} + +// ArcTransform computes a transformation that can be used for generating quadratic bĆ©zier +// curve approximations for an arc. +// +// The math is extracted from the following paper: +// "Drawing an elliptical arc using polylines, quadratic or +// cubic Bezier curves", L. Maisonobe +// An electronic version may be found at: +// http://spaceroots.org/documents/ellipse/elliptical-arc.pdf +func ArcTransform(p, f1, f2 f32.Point, angle float32, + segments int) f32.Affine2D { + c := f32.Point{ + X: 0.5 * (f1.X + f2.X), + Y: 0.5 * (f1.Y + f2.Y), + } + + // semi-major axis: 2a = |PF1| + |PF2| + a := 0.5 * (dist(f1, p) + dist(f2, p)) + + // semi-minor axis: c^2 = a^2+b^2 (c: focal distance) + f := dist(f1, c) + b := math.Sqrt(a*a - f*f) + + var rx, ry, alpha, start float64 + switch { + case a > b: + rx = a + ry = b + default: + rx = b + ry = a + } + + var x float64 + switch { + case f1 == c || f2 == c: + // degenerate case of a circle. + alpha = 0 + default: + switch { + case f1.X > c.X: + x = float64(f1.X - c.X) + alpha = math.Acos(x / f) + case f1.X < c.X: + x = float64(f2.X - c.X) + alpha = math.Acos(x / f) + case f1.X == c.X: + // special case of a "vertical" ellipse. + alpha = math.Pi / 2 + if f1.Y < c.Y { + alpha = -alpha + } + } + } + + start = math.Acos(float64(p.X-c.X) / dist(c, p)) + if c.Y > p.Y { + start = -start + } + start -= alpha + + var ( + Īø = angle / float32(segments) + ref f32.Affine2D // transform from absolute frame to ellipse-based one + rot f32.Affine2D // rotation matrix for each segment + inv f32.Affine2D // transform from ellipse-based frame to absolute one + ) + ref = ref.Offset(f32.Point{}.Sub(c)) + ref = ref.Rotate(f32.Point{}, float32(-alpha)) + ref = ref.Scale(f32.Point{}, f32.Point{ + X: float32(1 / rx), + Y: float32(1 / ry), + }) + inv = ref.Invert() + rot = rot.Rotate(f32.Point{}, float32(0.5*Īø)) + + // Instead of invoking math.Sincos for every segment, compute a rotation + // matrix once and apply for each segment. + // Before applying the rotation matrix rot, transform the coordinates + // to a frame centered to the ellipse (and warped into a unit circle), then rotate. + // Finally, transform back into the original frame. + return inv.Mul(rot).Mul(ref) +} + +func dist(p1, p2 f32.Point) float64 { + var ( + x1 = float64(p1.X) + y1 = float64(p1.Y) + x2 = float64(p2.X) + y2 = float64(p2.Y) + dx = x2 - x1 + dy = y2 - y1 + ) + return math.Hypot(dx, dy) +} + +func StrokePathCommands(style StrokeStyle, dashes DashOp, + scene []byte) StrokeQuads { + quads := decodeToStrokeQuads(scene) + return quads.stroke(style, dashes) +} + +// decodeToStrokeQuads decodes scene commands to quads ready to stroke. +func decodeToStrokeQuads(pathData []byte) StrokeQuads { + quads := make(StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4)) + for len(pathData) >= scene.CommandSize+4 { + contour := binary.LittleEndian.Uint32(pathData) + cmd := ops.DecodeCommand(pathData[4:]) + switch cmd.Op() { + case scene.OpLine: + var q QuadSegment + q.From, q.To = scene.DecodeLine(cmd) + q.Ctrl = q.From.Add(q.To).Mul(.5) + quad := StrokeQuad{ + Contour: contour, + Quad: q, + } + quads = append(quads, quad) + case scene.OpQuad: + var q QuadSegment + q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) + quad := StrokeQuad{ + Contour: contour, + Quad: q, + } + quads = append(quads, quad) + case scene.OpCubic: + for _, q := range SplitCubic(scene.DecodeCubic(cmd)) { + quad := StrokeQuad{ + Contour: contour, + Quad: q, + } + quads = append(quads, quad) + } + default: + panic("unsupported scene command") + } + pathData = pathData[scene.CommandSize+4:] + } + return quads +} + +func SplitCubic(from, ctrl0, ctrl1, to f32.Point) []QuadSegment { + quads := make([]QuadSegment, 0, 10) + // Set the maximum distance proportionally to the longest side + // of the bounding rectangle. + hull := f32.Rectangle{ + Min: from, + Max: ctrl0, + }.Canon().Add(ctrl1).Add(to) + l := hull.Dx() + if h := hull.Dy(); h > l { + l = h + } + approxCubeTo(&quads, 0, l*0.001, from, ctrl0, ctrl1, to) + return quads +} + +// approxCubeTo approximates a cubic BĆ©zier by a series of quadratic +// curves. +func approxCubeTo(quads *[]QuadSegment, splits int, maxDist float32, + from, ctrl0, ctrl1, to f32.Point) int { + // The idea is from + // https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html + // where a quadratic approximates a cubic by eliminating its tĀ³ term + // from its polynomial expression anchored at the starting point: + // + // P(t) = pen + 3t(ctrl0 - pen) + 3tĀ²(ctrl1 - 2ctrl0 + pen) + tĀ³(to - 3ctrl1 + 3ctrl0 - pen) + // + // The control point for the new quadratic Q1 that shares starting point, pen, with P is + // + // C1 = (3ctrl0 - pen)/2 + // + // The reverse cubic anchored at the end point has the polynomial + // + // P'(t) = to + 3t(ctrl1 - to) + 3tĀ²(ctrl0 - 2ctrl1 + to) + tĀ³(pen - 3ctrl0 + 3ctrl1 - to) + // + // The corresponding quadratic Q2 that shares the end point, to, with P has control + // point + // + // C2 = (3ctrl1 - to)/2 + // + // The combined quadratic BĆ©zier, Q, shares both start and end points with its cubic + // and use the midpoint between the two curves Q1 and Q2 as control point: + // + // C = (3ctrl0 - pen + 3ctrl1 - to)/4 + c := ctrl0.Mul(3).Sub(from).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0) + const maxSplits = 32 + if splits >= maxSplits { + *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to}) + return splits + } + // The maximum distance between the cubic P and its approximation Q given t + // can be shown to be + // + // d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen| + // + // To save a square root, compare dĀ² with the squared tolerance. + v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(from) + d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36) + if d2 <= maxDist*maxDist { + *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to}) + return splits + } + // De Casteljau split the curve and approximate the halves. + t := float32(0.5) + c0 := from.Add(ctrl0.Sub(from).Mul(t)) + c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t)) + c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t)) + c01 := c0.Add(c1.Sub(c0).Mul(t)) + c12 := c1.Add(c2.Sub(c1).Mul(t)) + c0112 := c01.Add(c12.Sub(c01).Mul(t)) + splits++ + splits = approxCubeTo(quads, splits, maxDist, from, c0, c01, c0112) + splits = approxCubeTo(quads, splits, maxDist, c0112, c12, c2, to) + return splits +} diff --git a/gio/io/clipboard/clipboard.go b/gio/io/clipboard/clipboard.go new file mode 100644 index 0000000..3d1e64c --- /dev/null +++ b/gio/io/clipboard/clipboard.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clipboard + +import ( + "realy.lol/gio/internal/opconst" + "realy.lol/gio/io/event" + "realy.lol/gio/op" +) + +// Event is generated when the clipboard content is requested. +type Event struct { + Text string +} + +// ReadOp requests the text of the clipboard, delivered to +// the current handler through an Event. +type ReadOp struct { + Tag event.Tag +} + +// WriteOp copies Text to the clipboard. +type WriteOp struct { + Text string +} + +func (h ReadOp) Add(o *op.Ops) { + data := o.Write1(opconst.TypeClipboardReadLen, h.Tag) + data[0] = byte(opconst.TypeClipboardRead) +} + +func (h WriteOp) Add(o *op.Ops) { + data := o.Write1(opconst.TypeClipboardWriteLen, &h.Text) + data[0] = byte(opconst.TypeClipboardWrite) +} + +func (Event) ImplementsEvent() {} diff --git a/gio/io/event/event.go b/gio/io/event/event.go new file mode 100644 index 0000000..998dccb --- /dev/null +++ b/gio/io/event/event.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package event contains the types for event handling. + +The Queue interface is the protocol for receiving external events. + +For example: + + var queue event.Queue = ... + + for _, e := range queue.Events(h) { + switch e.(type) { + ... + } + } + +In general, handlers must be declared before events become +available. Other packages such as pointer and key provide +the means for declaring handlers for specific event types. + +The following example declares a handler ready for key input: + + import realy.lol/gio/io/key + + ops := new(op.Ops) + var h *Handler = ... + key.InputOp{Tag: h}.Add(ops) + +*/ +package event + +// Queue maps an event handler key to the events +// available to the handler. +type Queue interface { + // Events returns the available events for an + // event handler tag. + Events(t Tag) []Event +} + +// Tag is the stable identifier for an event handler. +// For a handler h, the tag is typically &h. +type Tag interface{} + +// Event is the marker interface for events. +type Event interface { + ImplementsEvent() +} diff --git a/gio/io/key/key.go b/gio/io/key/key.go new file mode 100644 index 0000000..913dbb2 --- /dev/null +++ b/gio/io/key/key.go @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package key implements key and text events and operations. + +The InputOp operations is used for declaring key input handlers. Use +an implementation of the Queue interface from package ui to receive +events. +*/ +package key + +import ( + "fmt" + "strings" + + "realy.lol/gio/internal/opconst" + "realy.lol/gio/io/event" + "realy.lol/gio/op" +) + +// InputOp declares a handler ready for key events. +// Key events are in general only delivered to the +// focused key handler. +type InputOp struct { + Tag event.Tag +} + +// SoftKeyboardOp shows or hide the on-screen keyboard, if available. +// It replaces any previous SoftKeyboardOp. +type SoftKeyboardOp struct { + Show bool +} + +// FocusOp sets or clears the keyboard focus. It replaces any previous +// FocusOp in the same frame. +type FocusOp struct { + // Tag is the new focus. The focus is cleared if Tag is nil, or if Tag + // has no InputOp in the same frame. + Tag event.Tag +} + +// A FocusEvent is generated when a handler gains or loses +// focus. +type FocusEvent struct { + Focus bool +} + +// An Event is generated when a key is pressed. For text input +// use EditEvent. +type Event struct { + // Name of the key. For letters, the upper case form is used, via + // unicode.ToUpper. The shift modifier is taken into account, all other + // modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1" + // combinations both give the Name "!" with the US keyboard layout. + Name string + // Modifiers is the set of active modifiers when the key was pressed. + Modifiers Modifiers + // State is the state of the key when the event was fired. + State State +} + +// An EditEvent is generated when text is input. +type EditEvent struct { + Text string +} + +// State is the state of a key during an event. +type State uint8 + +const ( + // Press is the state of a pressed key. + Press State = iota + // Release is the state of a key that has been released. + // + // Note: release events are only implemented on the following platforms: + // macOS, Linux, Windows, WebAssembly. + Release +) + +// Modifiers +type Modifiers uint32 + +const ( + // ModCtrl is the ctrl modifier key. + ModCtrl Modifiers = 1 << iota + // ModCommand is the command modifier key + // found on Apple keyboards. + ModCommand + // ModShift is the shift modifier key. + ModShift + // ModAlt is the alt modifier key, or the option + // key on Apple keyboards. + ModAlt + // ModSuper is the "logo" modifier key, often + // represented by a Windows logo. + ModSuper +) + +const ( + // Names for special keys. + NameLeftArrow = "ā†" + NameRightArrow = "ā†’" + NameUpArrow = "ā†‘" + NameDownArrow = "ā†“" + NameReturn = "āŽ" + NameEnter = "āŒ¤" + NameEscape = "āŽ‹" + NameHome = "ā‡±" + NameEnd = "ā‡²" + NameDeleteBackward = "āŒ«" + NameDeleteForward = "āŒ¦" + NamePageUp = "ā‡ž" + NamePageDown = "ā‡Ÿ" + NameTab = "ā‡„" + NameSpace = "Space" +) + +// Contain reports whether m contains all modifiers +// in m2. +func (m Modifiers) Contain(m2 Modifiers) bool { + return m&m2 == m2 +} + +func (h InputOp) Add(o *op.Ops) { + if h.Tag == nil { + panic("Tag must be non-nil") + } + data := o.Write1(opconst.TypeKeyInputLen, h.Tag) + data[0] = byte(opconst.TypeKeyInput) +} + +func (h SoftKeyboardOp) Add(o *op.Ops) { + data := o.Write(opconst.TypeKeySoftKeyboardLen) + data[0] = byte(opconst.TypeKeySoftKeyboard) + if h.Show { + data[1] = 1 + } +} + +func (h FocusOp) Add(o *op.Ops) { + data := o.Write1(opconst.TypeKeyFocusLen, h.Tag) + data[0] = byte(opconst.TypeKeyFocus) +} + +func (EditEvent) ImplementsEvent() {} +func (Event) ImplementsEvent() {} +func (FocusEvent) ImplementsEvent() {} + +func (e Event) String() string { + return fmt.Sprintf("%v %v %v}", e.Name, e.Modifiers, e.State) +} + +func (m Modifiers) String() string { + var strs []string + if m.Contain(ModCtrl) { + strs = append(strs, "ModCtrl") + } + if m.Contain(ModCommand) { + strs = append(strs, "ModCommand") + } + if m.Contain(ModShift) { + strs = append(strs, "ModShift") + } + if m.Contain(ModAlt) { + strs = append(strs, "ModAlt") + } + if m.Contain(ModSuper) { + strs = append(strs, "ModSuper") + } + return strings.Join(strs, "|") +} + +func (s State) String() string { + switch s { + case Press: + return "Press" + case Release: + return "Release" + default: + panic("invalid State") + } +} diff --git a/gio/io/key/mod.go b/gio/io/key/mod.go new file mode 100644 index 0000000..c5db56c --- /dev/null +++ b/gio/io/key/mod.go @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build !darwin + +package key + +// ModShortcut is the platform's shortcut modifier, usually the Ctrl +// key. On Apple platforms it is the Cmd key. +const ModShortcut = ModCtrl diff --git a/gio/io/key/mod_darwin.go b/gio/io/key/mod_darwin.go new file mode 100644 index 0000000..c0f1437 --- /dev/null +++ b/gio/io/key/mod_darwin.go @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package key + +// ModShortcut is the platform's shortcut modifier, usually the Ctrl +// key. On Apple platforms it is the Cmd key. +const ModShortcut = ModCommand diff --git a/gio/io/pointer/doc.go b/gio/io/pointer/doc.go new file mode 100644 index 0000000..7243b94 --- /dev/null +++ b/gio/io/pointer/doc.go @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package pointer implements pointer events and operations. +A pointer is either a mouse controlled cursor or a touch +object such as a finger. + +The InputOp operation is used to declare a handler ready for pointer +events. Use an event.Queue to receive events. + +Types + +Only events that match a specified list of types are delivered to a handler. + +For example, to receive Press, Drag, and Release events (but not Move, Enter, +Leave, or Scroll): + + var ops op.Ops + var h *Handler = ... + + pointer.InputOp{ + Tag: h, + Types: pointer.Press | pointer.Drag | pointer.Release, + }.Add(ops) + +Cancel events are always delivered. + +Areas + +The area operations are used for specifying the area where +subsequent InputOp are active. + +For example, to set up a rectangular hit area: + + r := image.Rectangle{...} + pointer.Rect(r).Add(ops) + pointer.InputOp{Tag: h}.Add(ops) + +Note that areas compound: the effective area of multiple area +operations is the intersection of the areas. + +Matching events + +StackOp operations and input handlers form an implicit tree. +Each stack operation is a node, and each input handler is associated +with the most recent node. + +For example: + + ops := new(op.Ops) + var stack op.StackOp + var h1, h2 *Handler + + state := op.Save(ops) + pointer.InputOp{Tag: h1}.Add(Ops) + state.Load() + + state = op.Save(ops) + pointer.InputOp{Tag: h2}.Add(ops) + state.Load() + +implies a tree of two inner nodes, each with one pointer handler. + +When determining which handlers match an Event, only handlers whose +areas contain the event position are considered. The matching +proceeds as follows. + +First, the foremost matching handler is included. If the handler +has pass-through enabled, this step is repeated. + +Then, all matching handlers from the current node and all parent +nodes are included. + +In the example above, all events will go to h2 only even though both +handlers have the same area (the entire screen). + +Pass-through + +The PassOp operations controls the pass-through setting. A handler's +pass-through setting is recorded along with the InputOp. + +Pass-through handlers are useful for overlay widgets such as a hidden +side drawer. When the user touches the side, both the (transparent) +drawer handle and the interface below should receive pointer events. + +Disambiguation + +When more than one handler matches a pointer event, the event queue +follows a set of rules for distributing the event. + +As long as the pointer has not received a Press event, all +matching handlers receive all events. + +When a pointer is pressed, the set of matching handlers is +recorded. The set is not updated according to the pointer position +and hit areas. Rather, handlers stay in the matching set until they +no longer appear in a InputOp or when another handler in the set +grabs the pointer. + +A handler can exclude all other handler from its matching sets +by setting the Grab flag in its InputOp. The Grab flag is sticky +and stays in effect until the handler no longer appears in any +matching sets. + +The losing handlers are notified by a Cancel event. + +For multiple grabbing handlers, the foremost handler wins. + +Priorities + +Handlers know their position in a matching set of a pointer through +event priorities. The Shared priority is for matching sets with +multiple handlers; the Grabbed priority indicate exclusive access. + +Priorities are useful for deferred gesture matching. + +Consider a scrollable list of clickable elements. When the user touches an +element, it is unknown whether the gesture is a click on the element +or a drag (scroll) of the list. While the click handler might light up +the element in anticipation of a click, the scrolling handler does not +scroll on finger movements with lower than Grabbed priority. + +Should the user release the finger, the click handler registers a click. + +However, if the finger moves beyond a threshold, the scrolling handler +determines that the gesture is a drag and sets its Grab flag. The +click handler receives a Cancel (removing the highlight) and further +movements for the scroll handler has priority Grabbed, scrolling the +list. +*/ +package pointer diff --git a/gio/io/pointer/pointer.go b/gio/io/pointer/pointer.go new file mode 100644 index 0000000..f3aafae --- /dev/null +++ b/gio/io/pointer/pointer.go @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package pointer + +import ( + "encoding/binary" + "fmt" + "image" + "strings" + "time" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/opconst" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/op" +) + +// Event is a pointer event. +type Event struct { + Type Type + Source Source + // PointerID is the id for the pointer and can be used + // to track a particular pointer from Press to + // Release or Cancel. + PointerID ID + // Priority is the priority of the receiving handler + // for this event. + Priority Priority + // Time is when the event was received. The + // timestamp is relative to an undefined base. + Time time.Duration + // Buttons are the set of pressed mouse buttons for this event. + Buttons Buttons + // Position is the position of the event, relative to + // the current transformation, as set by op.TransformOp. + Position f32.Point + // Scroll is the scroll amount, if any. + Scroll f32.Point + // Modifiers is the set of active modifiers when + // the mouse button was pressed. + Modifiers key.Modifiers +} + +// AreaOp updates the hit area to the intersection of the current +// hit area and the area. The area is transformed before applying +// it. +type AreaOp struct { + kind areaKind + rect image.Rectangle +} + +// CursorNameOp sets the cursor for the current area. +type CursorNameOp struct { + Name CursorName +} + +// InputOp declares an input handler ready for pointer +// events. +type InputOp struct { + Tag event.Tag + // Grab, if set, request that the handler get + // Grabbed priority. + Grab bool + // Types is a bitwise-or of event types to receive. + Types Type + // ScrollBounds describe the maximum scrollable distances in both + // axes. Specifically, any Event e delivered to Tag will satisfy + // + // ScrollBounds.Min.X <= e.Scroll.X <= ScrollBounds.Max.X (horizontal axis) + // ScrollBounds.Min.Y <= e.Scroll.Y <= ScrollBounds.Max.Y (vertical axis) + ScrollBounds image.Rectangle +} + +// PassOp sets the pass-through mode. +type PassOp struct { + Pass bool +} + +type ID uint16 + +// Type of an Event. +type Type uint8 + +// Priority of an Event. +type Priority uint8 + +// Source of an Event. +type Source uint8 + +// Buttons is a set of mouse buttons +type Buttons uint8 + +// CursorName is the name of a cursor. +type CursorName string + +// Must match app/internal/input.areaKind +type areaKind uint8 + +const ( + // CursorDefault is the default cursor. + CursorDefault CursorName = "" + // CursorText is the cursor for text. + CursorText CursorName = "text" + // CursorPointer is the cursor for a link. + CursorPointer CursorName = "pointer" + // CursorCrossHair is the cursor for precise location. + CursorCrossHair CursorName = "crosshair" + // CursorColResize is the cursor for vertical resize. + CursorColResize CursorName = "col-resize" + // CursorRowResize is the cursor for horizontal resize. + CursorRowResize CursorName = "row-resize" + // CursorGrab is the cursor for moving object in any direction. + CursorGrab CursorName = "grab" + // CursorNone hides the cursor. To show it again, use any other cursor. + CursorNone CursorName = "none" +) + +const ( + // A Cancel event is generated when the current gesture is + // interrupted by other handlers or the system. + Cancel Type = (1 << iota) >> 1 + // Press of a pointer. + Press + // Release of a pointer. + Release + // Move of a pointer. + Move + // Drag of a pointer. + Drag + // Pointer enters an area watching for pointer input + Enter + // Pointer leaves an area watching for pointer input + Leave + // Scroll of a pointer. + Scroll +) + +const ( + // Mouse generated event. + Mouse Source = iota + // Touch generated event. + Touch +) + +const ( + // Shared priority is for handlers that + // are part of a matching set larger than 1. + Shared Priority = iota + // Foremost priority is like Shared, but the + // handler is the foremost of the matching set. + Foremost + // Grabbed is used for matching sets of size 1. + Grabbed +) + +const ( + // ButtonPrimary is the primary button, usually the left button for a + // right-handed user. + ButtonPrimary Buttons = 1 << iota + // ButtonSecondary is the secondary button, usually the right button for a + // right-handed user. + ButtonSecondary + // ButtonTertiary is the tertiary button, usually the middle button. + ButtonTertiary +) + +const ( + areaRect areaKind = iota + areaEllipse +) + +// Rect constructs a rectangular hit area. +func Rect(size image.Rectangle) AreaOp { + return AreaOp{ + kind: areaRect, + rect: size, + } +} + +// Ellipse constructs an ellipsoid hit area. +func Ellipse(size image.Rectangle) AreaOp { + return AreaOp{ + kind: areaEllipse, + rect: size, + } +} + +func (op AreaOp) Add(o *op.Ops) { + data := o.Write(opconst.TypeAreaLen) + data[0] = byte(opconst.TypeArea) + data[1] = byte(op.kind) + bo := binary.LittleEndian + bo.PutUint32(data[2:], uint32(op.rect.Min.X)) + bo.PutUint32(data[6:], uint32(op.rect.Min.Y)) + bo.PutUint32(data[10:], uint32(op.rect.Max.X)) + bo.PutUint32(data[14:], uint32(op.rect.Max.Y)) +} + +func (op CursorNameOp) Add(o *op.Ops) { + data := o.Write1(opconst.TypeCursorLen, op.Name) + data[0] = byte(opconst.TypeCursor) +} + +// Add panics if the scroll range does not contain zero. +func (op InputOp) Add(o *op.Ops) { + if op.Tag == nil { + panic("Tag must be non-nil") + } + if b := op.ScrollBounds; b.Min.X > 0 || b.Max.X < 0 || b.Min.Y > 0 || b.Max.Y < 0 { + panic(fmt.Errorf("invalid scroll range value %v", b)) + } + data := o.Write1(opconst.TypePointerInputLen, op.Tag) + data[0] = byte(opconst.TypePointerInput) + if op.Grab { + data[1] = 1 + } + data[2] = byte(op.Types) + bo := binary.LittleEndian + bo.PutUint32(data[3:], uint32(op.ScrollBounds.Min.X)) + bo.PutUint32(data[7:], uint32(op.ScrollBounds.Min.Y)) + bo.PutUint32(data[11:], uint32(op.ScrollBounds.Max.X)) + bo.PutUint32(data[15:], uint32(op.ScrollBounds.Max.Y)) +} + +func (op PassOp) Add(o *op.Ops) { + data := o.Write(opconst.TypePassLen) + data[0] = byte(opconst.TypePass) + if op.Pass { + data[1] = 1 + } +} + +func (t Type) String() string { + switch t { + case Press: + return "Press" + case Release: + return "Release" + case Cancel: + return "Cancel" + case Move: + return "Move" + case Drag: + return "Drag" + case Enter: + return "Enter" + case Leave: + return "Leave" + case Scroll: + return "Scroll" + default: + panic("unknown Type") + } +} + +func (p Priority) String() string { + switch p { + case Shared: + return "Shared" + case Foremost: + return "Foremost" + case Grabbed: + return "Grabbed" + default: + panic("unknown priority") + } +} + +func (s Source) String() string { + switch s { + case Mouse: + return "Mouse" + case Touch: + return "Touch" + default: + panic("unknown source") + } +} + +// Contain reports whether the set b contains +// all of the buttons. +func (b Buttons) Contain(buttons Buttons) bool { + return b&buttons == buttons +} + +func (b Buttons) String() string { + var strs []string + if b.Contain(ButtonPrimary) { + strs = append(strs, "ButtonPrimary") + } + if b.Contain(ButtonSecondary) { + strs = append(strs, "ButtonSecondary") + } + if b.Contain(ButtonTertiary) { + strs = append(strs, "ButtonTertiary") + } + return strings.Join(strs, "|") +} + +func (c CursorName) String() string { + if c == CursorDefault { + return "default" + } + return string(c) +} + +func (Event) ImplementsEvent() {} diff --git a/gio/io/profile/profile.go b/gio/io/profile/profile.go new file mode 100644 index 0000000..58be154 --- /dev/null +++ b/gio/io/profile/profile.go @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package profiles provides access to rendering +// profiles. +package profile + +import ( + "realy.lol/gio/internal/opconst" + "realy.lol/gio/io/event" + "realy.lol/gio/op" +) + +// Op registers a handler for receiving +// Events. +type Op struct { + Tag event.Tag +} + +// Event contains profile data from a single +// rendered frame. +type Event struct { + // Timings. Very likely to change. + Timings string +} + +func (p Op) Add(o *op.Ops) { + data := o.Write1(opconst.TypeProfileLen, p.Tag) + data[0] = byte(opconst.TypeProfile) +} + +func (p Event) ImplementsEvent() {} diff --git a/gio/io/router/clipboard.go b/gio/io/router/clipboard.go new file mode 100644 index 0000000..122c9bc --- /dev/null +++ b/gio/io/router/clipboard.go @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "realy.lol/gio/internal/opconst" + "realy.lol/gio/internal/ops" + "realy.lol/gio/io/event" +) + +type clipboardQueue struct { + receivers map[event.Tag]struct{} + // request avoid read clipboard every frame while waiting. + requested bool + text *string + reader ops.Reader +} + +// WriteClipboard returns the most recent text to be copied +// to the clipboard, if any. +func (q *clipboardQueue) WriteClipboard() (string, bool) { + if q.text == nil { + return "", false + } + text := *q.text + q.text = nil + return text, true +} + +// ReadClipboard reports if any new handler is waiting +// to read the clipboard. +func (q *clipboardQueue) ReadClipboard() bool { + if len(q.receivers) <= 0 || q.requested { + return false + } + q.requested = true + return true +} + +func (q *clipboardQueue) Push(e event.Event, events *handlerEvents) { + for r := range q.receivers { + events.Add(r, e) + delete(q.receivers, r) + } +} + +func (q *clipboardQueue) ProcessWriteClipboard(d []byte, refs []interface{}) { + if opconst.OpType(d[0]) != opconst.TypeClipboardWrite { + panic("invalid op") + } + q.text = refs[0].(*string) +} + +func (q *clipboardQueue) ProcessReadClipboard(d []byte, refs []interface{}) { + if opconst.OpType(d[0]) != opconst.TypeClipboardRead { + panic("invalid op") + } + if q.receivers == nil { + q.receivers = make(map[event.Tag]struct{}) + } + tag := refs[0].(event.Tag) + if _, ok := q.receivers[tag]; !ok { + q.receivers[tag] = struct{}{} + q.requested = false + } +} diff --git a/gio/io/router/clipboard_test.go b/gio/io/router/clipboard_test.go new file mode 100644 index 0000000..ac5ebe7 --- /dev/null +++ b/gio/io/router/clipboard_test.go @@ -0,0 +1,155 @@ +package router + +import ( + "testing" + + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/event" + "realy.lol/gio/op" +) + +func TestClipboardDuplicateEvent(t *testing.T) { + ops, router, handler := new(op.Ops), new(Router), make([]int, 2) + + // Both must receive the event once + clipboard.ReadOp{Tag: &handler[0]}.Add(ops) + clipboard.ReadOp{Tag: &handler[1]}.Add(ops) + + router.Frame(ops) + event := clipboard.Event{Text: "Test"} + router.Queue(event) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), true) + assertClipboardEvent(t, router.Events(&handler[1]), true) + ops.Reset() + + // No ReadOp + + router.Frame(ops) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), false) + assertClipboardEvent(t, router.Events(&handler[1]), false) + ops.Reset() + + clipboard.ReadOp{Tag: &handler[0]}.Add(ops) + + router.Frame(ops) + // No ClipboardEvent sent + assertClipboardReadOp(t, router, 1) + assertClipboardEvent(t, router.Events(&handler[0]), false) + assertClipboardEvent(t, router.Events(&handler[1]), false) + ops.Reset() +} + +func TestQueueProcessReadClipboard(t *testing.T) { + ops, router, handler := new(op.Ops), new(Router), make([]int, 2) + ops.Reset() + + // Request read + clipboard.ReadOp{Tag: &handler[0]}.Add(ops) + + router.Frame(ops) + assertClipboardReadOp(t, router, 1) + ops.Reset() + + for i := 0; i < 3; i++ { + // No ReadOp + // One receiver must still wait for response + + router.Frame(ops) + assertClipboardReadOpDuplicated(t, router, 1) + ops.Reset() + } + + router.Frame(ops) + // Send the clipboard event + event := clipboard.Event{Text: "Text 2"} + router.Queue(event) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), true) + ops.Reset() + + // No ReadOp + // There's no receiver waiting + + router.Frame(ops) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), false) + ops.Reset() +} + +func TestQueueProcessWriteClipboard(t *testing.T) { + ops, router := new(op.Ops), new(Router) + ops.Reset() + + clipboard.WriteOp{Text: "Write 1"}.Add(ops) + + router.Frame(ops) + assertClipboardWriteOp(t, router, "Write 1") + ops.Reset() + + // No WriteOp + + router.Frame(ops) + assertClipboardWriteOp(t, router, "") + ops.Reset() + + clipboard.WriteOp{Text: "Write 2"}.Add(ops) + + router.Frame(ops) + assertClipboardReadOp(t, router, 0) + assertClipboardWriteOp(t, router, "Write 2") + ops.Reset() +} + +func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) { + t.Helper() + var evtClipboard int + for _, e := range events { + switch e.(type) { + case clipboard.Event: + evtClipboard++ + } + } + if evtClipboard <= 0 && expected { + t.Error("expected to receive some event") + } + if evtClipboard > 0 && !expected { + t.Error("unexpected event received") + } +} + +func assertClipboardReadOp(t *testing.T, router *Router, expected int) { + t.Helper() + if len(router.cqueue.receivers) != expected { + t.Error("unexpected number of receivers") + } + if router.cqueue.ReadClipboard() != (expected > 0) { + t.Error("missing requests") + } +} + +func assertClipboardReadOpDuplicated(t *testing.T, router *Router, + expected int) { + t.Helper() + if len(router.cqueue.receivers) != expected { + t.Error("receivers removed") + } + if router.cqueue.ReadClipboard() != false { + t.Error("duplicated requests") + } +} + +func assertClipboardWriteOp(t *testing.T, router *Router, expected string) { + t.Helper() + if (router.cqueue.text != nil) != (expected != "") { + t.Error("text not defined") + } + text, ok := router.cqueue.WriteClipboard() + if ok != (expected != "") { + t.Error("duplicated requests") + } + if text != expected { + t.Errorf("got text %s, expected %s", text, expected) + } +} diff --git a/gio/io/router/key.go b/gio/io/router/key.go new file mode 100644 index 0000000..0fe946e --- /dev/null +++ b/gio/io/router/key.go @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "realy.lol/gio/internal/opconst" + "realy.lol/gio/internal/ops" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/op" +) + +type TextInputState uint8 + +type keyQueue struct { + focus event.Tag + handlers map[event.Tag]*keyHandler + reader ops.Reader + state TextInputState +} + +type keyHandler struct { + // visible will be true if the InputOp is present + // in the current frame. + visible bool + new bool +} + +const ( + TextInputKeep TextInputState = iota + TextInputClose + TextInputOpen +) + +// InputState returns the last text input state as +// determined in Frame. +func (q *keyQueue) InputState() TextInputState { + return q.state +} + +func (q *keyQueue) Frame(root *op.Ops, events *handlerEvents) { + if q.handlers == nil { + q.handlers = make(map[event.Tag]*keyHandler) + } + for _, h := range q.handlers { + h.visible, h.new = false, false + } + q.reader.Reset(root) + + focus, changed, state := q.resolveFocus(events) + for k, h := range q.handlers { + if !h.visible { + delete(q.handlers, k) + if q.focus == k { + // Remove the focus from the handler that is no longer visible. + q.focus = nil + state = TextInputClose + } + } else if h.new && k != focus { + // Reset the handler on (each) first appearance, but don't trigger redraw. + events.AddNoRedraw(k, key.FocusEvent{Focus: false}) + } + } + if changed && focus != nil { + if _, exists := q.handlers[focus]; !exists { + focus = nil + } + } + if changed && focus != q.focus { + if q.focus != nil { + events.Add(q.focus, key.FocusEvent{Focus: false}) + } + q.focus = focus + if q.focus != nil { + events.Add(q.focus, key.FocusEvent{Focus: true}) + } else { + state = TextInputClose + } + } + q.state = state +} + +func (q *keyQueue) Push(e event.Event, events *handlerEvents) { + if q.focus != nil { + events.Add(q.focus, e) + } +} + +func (q *keyQueue) resolveFocus(events *handlerEvents) (focus event.Tag, + changed bool, state TextInputState) { + for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() { + switch opconst.OpType(encOp.Data[0]) { + case opconst.TypeKeyFocus: + op := decodeFocusOp(encOp.Data, encOp.Refs) + changed = true + focus = op.Tag + case opconst.TypeKeySoftKeyboard: + op := decodeSoftKeyboardOp(encOp.Data, encOp.Refs) + if op.Show { + state = TextInputOpen + } else { + state = TextInputClose + } + case opconst.TypeKeyInput: + op := decodeKeyInputOp(encOp.Data, encOp.Refs) + h, ok := q.handlers[op.Tag] + if !ok { + h = &keyHandler{new: true} + q.handlers[op.Tag] = h + } + h.visible = true + } + } + return +} + +func decodeKeyInputOp(d []byte, refs []interface{}) key.InputOp { + if opconst.OpType(d[0]) != opconst.TypeKeyInput { + panic("invalid op") + } + return key.InputOp{ + Tag: refs[0].(event.Tag), + } +} + +func decodeSoftKeyboardOp(d []byte, refs []interface{}) key.SoftKeyboardOp { + if opconst.OpType(d[0]) != opconst.TypeKeySoftKeyboard { + panic("invalid op") + } + return key.SoftKeyboardOp{ + Show: d[1] != 0, + } +} + +func decodeFocusOp(d []byte, refs []interface{}) key.FocusOp { + if opconst.OpType(d[0]) != opconst.TypeKeyFocus { + panic("invalid op") + } + return key.FocusOp{ + Tag: refs[0], + } +} diff --git a/gio/io/router/key_test.go b/gio/io/router/key_test.go new file mode 100644 index 0000000..59176df --- /dev/null +++ b/gio/io/router/key_test.go @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "reflect" + "testing" + + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/op" +) + +func TestKeyWakeup(t *testing.T) { + handler := new(int) + var ops op.Ops + key.InputOp{Tag: handler}.Add(&ops) + + var r Router + // Test that merely adding a handler doesn't trigger redraw. + r.Frame(&ops) + if _, wake := r.WakeupTime(); wake { + t.Errorf("adding key.InputOp triggered a redraw") + } + // However, adding a handler queues a Focus(false) event. + if evts := r.Events(handler); len(evts) != 1 { + t.Errorf("no Focus event for newly registered key.InputOp") + } + // Verify that r.Events does trigger a redraw. + r.Frame(&ops) + if _, wake := r.WakeupTime(); !wake { + t.Errorf("key.FocusEvent event didn't trigger a redraw") + } +} + +func TestKeyMultiples(t *testing.T) { + handlers := make([]int, 3) + ops := new(op.Ops) + r := new(Router) + + key.SoftKeyboardOp{Show: true}.Add(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.FocusOp{Tag: &handlers[2]}.Add(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + + // The last one must be focused: + key.InputOp{Tag: &handlers[2]}.Add(ops) + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), false) + assertKeyEvent(t, r.Events(&handlers[1]), false) + assertKeyEvent(t, r.Events(&handlers[2]), true) + assertFocus(t, r, &handlers[2]) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeyStacked(t *testing.T) { + handlers := make([]int, 4) + ops := new(op.Ops) + r := new(Router) + + s := op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.FocusOp{Tag: nil}.Add(ops) + s.Load() + s = op.Save(ops) + key.SoftKeyboardOp{Show: false}.Add(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + key.FocusOp{Tag: &handlers[1]}.Add(ops) + s.Load() + s = op.Save(ops) + key.InputOp{Tag: &handlers[2]}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Load() + s = op.Save(ops) + key.InputOp{Tag: &handlers[3]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), false) + assertKeyEvent(t, r.Events(&handlers[1]), true) + assertKeyEvent(t, r.Events(&handlers[2]), false) + assertKeyEvent(t, r.Events(&handlers[3]), false) + assertFocus(t, r, &handlers[1]) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeySoftKeyboardNoFocus(t *testing.T) { + ops := new(op.Ops) + r := new(Router) + + // It's possible to open the keyboard + // without any active focus: + key.SoftKeyboardOp{Show: true}.Add(ops) + + r.Frame(ops) + + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeyRemoveFocus(t *testing.T) { + handlers := make([]int, 2) + ops := new(op.Ops) + r := new(Router) + + // New InputOp with Focus and Keyboard: + s := op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.FocusOp{Tag: &handlers[0]}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Load() + + // New InputOp without any focus: + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + // Add some key events: + event := event.Event(key.Event{Name: key.NameTab, + Modifiers: key.ModShortcut, State: key.Press}) + r.Queue(event) + + assertKeyEvent(t, r.Events(&handlers[0]), true, event) + assertKeyEvent(t, r.Events(&handlers[1]), false) + assertFocus(t, r, &handlers[0]) + assertKeyboard(t, r, TextInputOpen) + + ops.Reset() + + // Will get the focus removed: + s = op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + s.Load() + + // Unchanged: + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + // Remove focus by focusing on a tag that don't exist. + s = op.Save(ops) + key.FocusOp{Tag: new(int)}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputClose) + + ops.Reset() + + s = op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + s.Load() + + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEventUnexpected(t, r.Events(&handlers[0])) + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputKeep) + + ops.Reset() + + // Set focus to InputOp which already + // exists in the previous frame: + s = op.Save(ops) + key.FocusOp{Tag: &handlers[0]}.Add(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Load() + + // Remove focus. + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + key.FocusOp{Tag: nil}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeyFocusedInvisible(t *testing.T) { + handlers := make([]int, 2) + ops := new(op.Ops) + r := new(Router) + + // Set new InputOp with focus: + s := op.Save(ops) + key.FocusOp{Tag: &handlers[0]}.Add(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Load() + + // Set new InputOp without focus: + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), true) + assertKeyEvent(t, r.Events(&handlers[1]), false) + assertFocus(t, r, &handlers[0]) + assertKeyboard(t, r, TextInputOpen) + + ops.Reset() + + // + // Removed first (focused) element! + // + + // Unchanged: + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEventUnexpected(t, r.Events(&handlers[0])) + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputClose) + + ops.Reset() + + // Respawn the first element: + // It must receive one `Event{Focus: false}`. + s = op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + s.Load() + + // Unchanged + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), false) + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputKeep) + +} + +func assertKeyEvent(t *testing.T, events []event.Event, expected bool, + expectedInputs ...event.Event) { + t.Helper() + var evtFocus int + var evtKeyPress int + for _, e := range events { + switch ev := e.(type) { + case key.FocusEvent: + if ev.Focus != expected { + t.Errorf("focus is expected to be %v, got %v", expected, + ev.Focus) + } + evtFocus++ + case key.Event, key.EditEvent: + if len(expectedInputs) <= evtKeyPress { + t.Errorf("unexpected key events") + } + if !reflect.DeepEqual(ev, expectedInputs[evtKeyPress]) { + t.Errorf("expected %v events, got %v", + expectedInputs[evtKeyPress], ev) + } + evtKeyPress++ + } + } + if evtFocus <= 0 { + t.Errorf("expected focus event") + } + if evtFocus > 1 { + t.Errorf("expected single focus event") + } + if evtKeyPress != len(expectedInputs) { + t.Errorf("expected key events") + } +} + +func assertKeyEventUnexpected(t *testing.T, events []event.Event) { + t.Helper() + var evtFocus int + for _, e := range events { + switch e.(type) { + case key.FocusEvent: + evtFocus++ + } + } + if evtFocus > 1 { + t.Errorf("unexpected focus event") + } +} + +func assertFocus(t *testing.T, router *Router, expected event.Tag) { + t.Helper() + if router.kqueue.focus != expected { + t.Errorf("expected %v to be focused, got %v", expected, + router.kqueue.focus) + } +} + +func assertKeyboard(t *testing.T, router *Router, expected TextInputState) { + t.Helper() + if router.kqueue.state != expected { + t.Errorf("expected %v keyboard, got %v", expected, router.kqueue.state) + } +} diff --git a/gio/io/router/pointer.go b/gio/io/router/pointer.go new file mode 100644 index 0000000..588657c --- /dev/null +++ b/gio/io/router/pointer.go @@ -0,0 +1,515 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "encoding/binary" + "image" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/opconst" + "realy.lol/gio/internal/ops" + "realy.lol/gio/io/event" + "realy.lol/gio/io/pointer" + "realy.lol/gio/op" +) + +type pointerQueue struct { + hitTree []hitNode + areas []areaNode + cursors []cursorNode + cursor pointer.CursorName + handlers map[event.Tag]*pointerHandler + pointers []pointerInfo + reader ops.Reader + + // states holds the storage for save/restore ops. + states []collectState + scratch []event.Tag +} + +type hitNode struct { + next int + area int + // Pass tracks the most recent PassOp mode. + pass bool + + // For handler nodes. + tag event.Tag +} + +type cursorNode struct { + name pointer.CursorName + area int +} + +type pointerInfo struct { + id pointer.ID + pressed bool + handlers []event.Tag + // last tracks the last pointer event received, + // used while processing frame events. + last pointer.Event + + // entered tracks the tags that contain the pointer. + entered []event.Tag +} + +type pointerHandler struct { + area int + active bool + wantsGrab bool + types pointer.Type + // min and max horizontal/vertical scroll + scrollRange image.Rectangle +} + +type areaOp struct { + kind areaKind + rect f32.Rectangle +} + +type areaNode struct { + trans f32.Affine2D + next int + area areaOp +} + +type areaKind uint8 + +// collectState represents the state for collectHandlers +type collectState struct { + t f32.Affine2D + area int + node int + pass bool +} + +const ( + areaRect areaKind = iota + areaEllipse +) + +func (q *pointerQueue) save(id int, state collectState) { + if extra := id - len(q.states) + 1; extra > 0 { + q.states = append(q.states, make([]collectState, extra)...) + } + q.states[id] = state +} + +func (q *pointerQueue) collectHandlers(r *ops.Reader, events *handlerEvents) { + state := collectState{ + area: -1, + node: -1, + } + q.save(opconst.InitialStateID, state) + for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { + switch opconst.OpType(encOp.Data[0]) { + case opconst.TypeSave: + id := ops.DecodeSave(encOp.Data) + q.save(id, state) + case opconst.TypeLoad: + id, mask := ops.DecodeLoad(encOp.Data) + s := q.states[id] + if mask&opconst.TransformState != 0 { + state.t = s.t + } + if mask&^opconst.TransformState != 0 { + state = s + } + case opconst.TypePass: + state.pass = encOp.Data[1] != 0 + case opconst.TypeArea: + var op areaOp + op.Decode(encOp.Data) + q.areas = append(q.areas, + areaNode{trans: state.t, next: state.area, area: op}) + state.area = len(q.areas) - 1 + q.hitTree = append(q.hitTree, hitNode{ + next: state.node, + area: state.area, + pass: state.pass, + }) + state.node = len(q.hitTree) - 1 + case opconst.TypeTransform: + dop := ops.DecodeTransform(encOp.Data) + state.t = state.t.Mul(dop) + case opconst.TypePointerInput: + op := pointer.InputOp{ + Tag: encOp.Refs[0].(event.Tag), + Grab: encOp.Data[1] != 0, + Types: pointer.Type(encOp.Data[2]), + } + q.hitTree = append(q.hitTree, hitNode{ + next: state.node, + area: state.area, + pass: state.pass, + tag: op.Tag, + }) + state.node = len(q.hitTree) - 1 + h, ok := q.handlers[op.Tag] + if !ok { + h = new(pointerHandler) + q.handlers[op.Tag] = h + // Cancel handlers on (each) first appearance, but don't + // trigger redraw. + events.AddNoRedraw(op.Tag, pointer.Event{Type: pointer.Cancel}) + } + h.active = true + h.area = state.area + h.wantsGrab = h.wantsGrab || op.Grab + h.types = h.types | op.Types + bo := binary.LittleEndian.Uint32 + h.scrollRange = image.Rectangle{ + Min: image.Point{ + X: int(int32(bo(encOp.Data[3:]))), + Y: int(int32(bo(encOp.Data[7:]))), + }, + Max: image.Point{ + X: int(int32(bo(encOp.Data[11:]))), + Y: int(int32(bo(encOp.Data[15:]))), + }, + } + case opconst.TypeCursor: + q.cursors = append(q.cursors, cursorNode{ + name: encOp.Refs[0].(pointer.CursorName), + area: len(q.areas) - 1, + }) + } + } +} + +func (q *pointerQueue) opHit(handlers *[]event.Tag, pos f32.Point) { + // Track whether we're passing through hits. + pass := true + idx := len(q.hitTree) - 1 + for idx >= 0 { + n := &q.hitTree[idx] + if !q.hit(n.area, pos) { + idx-- + continue + } + pass = pass && n.pass + if pass { + idx-- + } else { + idx = n.next + } + if n.tag != nil { + if _, exists := q.handlers[n.tag]; exists { + *handlers = append(*handlers, n.tag) + } + } + } +} + +func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point { + if areaIdx == -1 { + return p + } + return q.areas[areaIdx].trans.Invert().Transform(p) +} + +func (q *pointerQueue) hit(areaIdx int, p f32.Point) bool { + for areaIdx != -1 { + a := &q.areas[areaIdx] + p := a.trans.Invert().Transform(p) + if !a.area.Hit(p) { + return false + } + areaIdx = a.next + } + return true +} + +func (q *pointerQueue) reset() { + if q.handlers == nil { + q.handlers = make(map[event.Tag]*pointerHandler) + } +} + +func (q *pointerQueue) Frame(root *op.Ops, events *handlerEvents) { + q.reset() + for _, h := range q.handlers { + // Reset handler. + h.active = false + h.wantsGrab = false + h.types = 0 + } + q.hitTree = q.hitTree[:0] + q.areas = q.areas[:0] + q.cursors = q.cursors[:0] + q.reader.Reset(root) + q.collectHandlers(&q.reader, events) + for k, h := range q.handlers { + if !h.active { + q.dropHandlers(events, k) + delete(q.handlers, k) + } + if h.wantsGrab { + for _, p := range q.pointers { + if !p.pressed { + continue + } + for i, k2 := range p.handlers { + if k2 == k { + // Drop other handlers that lost their grab. + dropped := make([]event.Tag, 0, len(p.handlers)-1) + dropped = append(dropped, p.handlers[:i]...) + dropped = append(dropped, p.handlers[i+1:]...) + cancelHandlers(events, dropped...) + q.dropHandlers(events, dropped...) + break + } + } + } + } + } + for i := range q.pointers { + p := &q.pointers[i] + q.deliverEnterLeaveEvents(p, events, p.last) + } +} + +func cancelHandlers(events *handlerEvents, tags ...event.Tag) { + for _, k := range tags { + events.Add(k, pointer.Event{Type: pointer.Cancel}) + } +} + +func (q *pointerQueue) dropHandlers(events *handlerEvents, tags ...event.Tag) { + for _, k := range tags { + for i := range q.pointers { + p := &q.pointers[i] + for i := len(p.handlers) - 1; i >= 0; i-- { + if p.handlers[i] == k { + p.handlers = append(p.handlers[:i], p.handlers[i+1:]...) + } + } + for i := len(p.entered) - 1; i >= 0; i-- { + if p.entered[i] == k { + p.entered = append(p.entered[:i], p.entered[i+1:]...) + } + } + } + } +} + +func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) { + q.reset() + if e.Type == pointer.Cancel { + q.pointers = q.pointers[:0] + for k := range q.handlers { + cancelHandlers(events, k) + q.dropHandlers(events, k) + } + return + } + pidx := -1 + for i, p := range q.pointers { + if p.id == e.PointerID { + pidx = i + break + } + } + if pidx == -1 { + q.pointers = append(q.pointers, pointerInfo{id: e.PointerID}) + pidx = len(q.pointers) - 1 + } + p := &q.pointers[pidx] + p.last = e + + if e.Type == pointer.Move && p.pressed { + e.Type = pointer.Drag + } + + if e.Type == pointer.Release { + q.deliverEvent(p, events, e) + p.pressed = false + } + q.deliverEnterLeaveEvents(p, events, e) + + if !p.pressed { + p.handlers = append(p.handlers[:0], q.scratch...) + } + if e.Type == pointer.Press { + p.pressed = true + } + switch e.Type { + case pointer.Release: + case pointer.Scroll: + q.deliverScrollEvent(p, events, e) + default: + q.deliverEvent(p, events, e) + } + if !p.pressed && len(p.entered) == 0 { + // No longer need to track pointer. + q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...) + } +} + +func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, + e pointer.Event) { + foremost := true + if p.pressed && len(p.handlers) == 1 { + e.Priority = pointer.Grabbed + foremost = false + } + for _, k := range p.handlers { + h := q.handlers[k] + if e.Type&h.types == 0 { + continue + } + e := e + if foremost { + foremost = false + e.Priority = pointer.Foremost + } + e.Position = q.invTransform(h.area, e.Position) + events.Add(k, e) + } +} + +func (q *pointerQueue) deliverScrollEvent(p *pointerInfo, events *handlerEvents, + e pointer.Event) { + foremost := true + if p.pressed && len(p.handlers) == 1 { + e.Priority = pointer.Grabbed + foremost = false + } + var sx, sy = e.Scroll.X, e.Scroll.Y + for _, k := range p.handlers { + if sx == 0 && sy == 0 { + return + } + h := q.handlers[k] + // Distribute the scroll to the handler based on its ScrollRange. + sx, e.Scroll.X = setScrollEvent(sx, h.scrollRange.Min.X, + h.scrollRange.Max.X) + sy, e.Scroll.Y = setScrollEvent(sy, h.scrollRange.Min.Y, + h.scrollRange.Max.Y) + e := e + if foremost { + foremost = false + e.Priority = pointer.Foremost + } + e.Position = q.invTransform(h.area, e.Position) + events.Add(k, e) + } +} + +func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, + events *handlerEvents, e pointer.Event) { + q.scratch = q.scratch[:0] + q.opHit(&q.scratch, e.Position) + if p.pressed { + // Filter out non-participating handlers. + for i := len(q.scratch) - 1; i >= 0; i-- { + if _, found := searchTag(p.handlers, q.scratch[i]); !found { + q.scratch = append(q.scratch[:i], q.scratch[i+1:]...) + } + } + } + hits := q.scratch + if e.Source != pointer.Mouse && !p.pressed && e.Type != pointer.Press { + // Consider non-mouse pointers leaving when they're released. + hits = nil + } + // Deliver Leave events. + for _, k := range p.entered { + if _, found := searchTag(hits, k); found { + continue + } + h := q.handlers[k] + e.Type = pointer.Leave + + if e.Type&h.types != 0 { + e.Position = q.invTransform(h.area, e.Position) + events.Add(k, e) + } + } + // Deliver Enter events and update cursor. + q.cursor = pointer.CursorDefault + for _, k := range hits { + h := q.handlers[k] + for i := len(q.cursors) - 1; i >= 0; i-- { + if c := q.cursors[i]; c.area == h.area { + q.cursor = c.name + break + } + } + if _, found := searchTag(p.entered, k); found { + continue + } + e.Type = pointer.Enter + + if e.Type&h.types != 0 { + e.Position = q.invTransform(h.area, e.Position) + events.Add(k, e) + } + } + p.entered = append(p.entered[:0], hits...) +} + +func searchTag(tags []event.Tag, tag event.Tag) (int, bool) { + for i, t := range tags { + if t == tag { + return i, true + } + } + return 0, false +} + +func opDecodeFloat32(d []byte) float32 { + return float32(int32(binary.LittleEndian.Uint32(d))) +} + +func (op *areaOp) Decode(d []byte) { + if opconst.OpType(d[0]) != opconst.TypeArea { + panic("invalid op") + } + rect := f32.Rectangle{ + Min: f32.Point{ + X: opDecodeFloat32(d[2:]), + Y: opDecodeFloat32(d[6:]), + }, + Max: f32.Point{ + X: opDecodeFloat32(d[10:]), + Y: opDecodeFloat32(d[14:]), + }, + } + *op = areaOp{ + kind: areaKind(d[1]), + rect: rect, + } +} + +func (op *areaOp) Hit(pos f32.Point) bool { + pos = pos.Sub(op.rect.Min) + size := op.rect.Size() + switch op.kind { + case areaRect: + return 0 <= pos.X && pos.X < size.X && + 0 <= pos.Y && pos.Y < size.Y + case areaEllipse: + rx := size.X / 2 + ry := size.Y / 2 + xh := pos.X - rx + yk := pos.Y - ry + // The ellipse function works in all cases because + // 0/0 is not <= 1. + return (xh*xh)/(rx*rx)+(yk*yk)/(ry*ry) <= 1 + default: + panic("invalid area kind") + } +} + +func setScrollEvent(scroll float32, min, max int) (left, scrolled float32) { + if v := float32(max); scroll > v { + return scroll - v, v + } + if v := float32(min); scroll < v { + return scroll - v, v + } + return 0, scroll +} diff --git a/gio/io/router/pointer_test.go b/gio/io/router/pointer_test.go new file mode 100644 index 0000000..5a28d0e --- /dev/null +++ b/gio/io/router/pointer_test.go @@ -0,0 +1,787 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "fmt" + "image" + "reflect" + "testing" + + "realy.lol/gio/f32" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/op" +) + +func TestPointerWakeup(t *testing.T) { + handler := new(int) + var ops op.Ops + addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100)) + + var r Router + // Test that merely adding a handler doesn't trigger redraw. + r.Frame(&ops) + if _, wake := r.WakeupTime(); wake { + t.Errorf("adding pointer.InputOp triggered a redraw") + } + // However, adding a handler queues a Cancel event. + assertEventSequence(t, r.Events(handler), pointer.Cancel) + // Verify that r.Events does trigger a redraw. + r.Frame(&ops) + if _, wake := r.WakeupTime(); !wake { + t.Errorf("pointer.Cancel event didn't trigger a redraw") + } +} + +func TestPointerDrag(t *testing.T) { + handler := new(int) + var ops op.Ops + addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100)) + + var r Router + r.Frame(&ops) + r.Queue( + // Press. + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + // Move outside the area. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(150, 150), + }, + ) + assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter, + pointer.Press, pointer.Leave, pointer.Drag) +} + +func TestPointerDragNegative(t *testing.T) { + handler := new(int) + var ops op.Ops + addPointerHandler(&ops, handler, image.Rect(-100, -100, 0, 0)) + + var r Router + r.Frame(&ops) + r.Queue( + // Press. + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(-50, -50), + }, + // Move outside the area. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(-150, -150), + }, + ) + assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter, + pointer.Press, pointer.Leave, pointer.Drag) +} + +func TestPointerGrab(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + handler3 := new(int) + var ops op.Ops + + types := pointer.Press | pointer.Release + + pointer.InputOp{Tag: handler1, Types: types, Grab: true}.Add(&ops) + pointer.InputOp{Tag: handler2, Types: types}.Add(&ops) + pointer.InputOp{Tag: handler3, Types: types}.Add(&ops) + + var r Router + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Press) + assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Press) + assertEventSequence(t, r.Events(handler3), pointer.Cancel, pointer.Press) + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Release, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Release) + assertEventSequence(t, r.Events(handler2), pointer.Cancel) + assertEventSequence(t, r.Events(handler3), pointer.Cancel) +} + +func TestPointerMove(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + var ops op.Ops + + types := pointer.Move | pointer.Enter | pointer.Leave + + // Handler 1 area: (0, 0) - (100, 100) + pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops) + pointer.InputOp{Tag: handler1, Types: types}.Add(&ops) + // Handler 2 area: (50, 50) - (100, 100) (areas intersect). + pointer.Rect(image.Rect(50, 50, 200, 200)).Add(&ops) + pointer.InputOp{Tag: handler2, Types: types}.Add(&ops) + + var r Router + r.Frame(&ops) + r.Queue( + // Hit both handlers. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + // Hit handler 1. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(49, 50), + }, + // Hit no handlers. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(100, 50), + }, + pointer.Event{ + Type: pointer.Cancel, + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, + pointer.Move, pointer.Move, pointer.Leave, pointer.Cancel) + assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter, + pointer.Move, pointer.Leave, pointer.Cancel) +} + +func TestPointerTypes(t *testing.T) { + handler := new(int) + var ops op.Ops + pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops) + pointer.InputOp{ + Tag: handler, + Types: pointer.Press | pointer.Release, + }.Add(&ops) + + var r Router + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(150, 150), + }, + pointer.Event{ + Type: pointer.Release, + Position: f32.Pt(150, 150), + }, + ) + assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Press, + pointer.Release) +} + +func TestPointerPriority(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + handler3 := new(int) + var ops op.Ops + + st := op.Save(&ops) + pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops) + pointer.InputOp{ + Tag: handler1, + Types: pointer.Scroll, + ScrollBounds: image.Rectangle{Max: image.Point{X: 100}}, + }.Add(&ops) + + pointer.Rect(image.Rect(0, 0, 100, 50)).Add(&ops) + pointer.InputOp{ + Tag: handler2, + Types: pointer.Scroll, + ScrollBounds: image.Rectangle{Max: image.Point{X: 20}}, + }.Add(&ops) + st.Load() + + pointer.Rect(image.Rect(0, 100, 100, 200)).Add(&ops) + pointer.InputOp{ + Tag: handler3, + Types: pointer.Scroll, + ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}}, + }.Add(&ops) + + var r Router + r.Frame(&ops) + r.Queue( + // Hit handler 1 and 2. + pointer.Event{ + Type: pointer.Scroll, + Position: f32.Pt(50, 25), + Scroll: f32.Pt(50, 0), + }, + // Hit handler 1. + pointer.Event{ + Type: pointer.Scroll, + Position: f32.Pt(50, 75), + Scroll: f32.Pt(50, 50), + }, + // Hit handler 3. + pointer.Event{ + Type: pointer.Scroll, + Position: f32.Pt(50, 150), + Scroll: f32.Pt(-30, -30), + }, + // Hit no handlers. + pointer.Event{ + Type: pointer.Scroll, + Position: f32.Pt(50, 225), + }, + ) + + hev1 := r.Events(handler1) + hev2 := r.Events(handler2) + hev3 := r.Events(handler3) + assertEventSequence(t, hev1, pointer.Cancel, pointer.Scroll, pointer.Scroll) + assertEventSequence(t, hev2, pointer.Cancel, pointer.Scroll) + assertEventSequence(t, hev3, pointer.Cancel, pointer.Scroll) + assertEventPriorities(t, hev1, pointer.Shared, pointer.Shared, + pointer.Foremost) + assertEventPriorities(t, hev2, pointer.Shared, pointer.Foremost) + assertEventPriorities(t, hev3, pointer.Shared, pointer.Foremost) + assertScrollEvent(t, hev1[1], f32.Pt(30, 0)) + assertScrollEvent(t, hev2[1], f32.Pt(20, 0)) + assertScrollEvent(t, hev1[2], f32.Pt(50, 0)) + assertScrollEvent(t, hev3[1], f32.Pt(-20, -30)) +} + +func TestPointerEnterLeave(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + var ops op.Ops + + // Handler 1 area: (0, 0) - (100, 100) + addPointerHandler(&ops, handler1, image.Rect(0, 0, 100, 100)) + + // Handler 2 area: (50, 50) - (200, 200) (areas overlap). + addPointerHandler(&ops, handler2, image.Rect(50, 50, 200, 200)) + + var r Router + r.Frame(&ops) + // Hit both handlers. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + // First event for a handler is always a Cancel. + // Only handler2 should receive the enter/move events because it is on top + // and handler1 is not an ancestor in the hit tree. + assertEventSequence(t, r.Events(handler1), pointer.Cancel) + assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter, + pointer.Move) + + // Leave the second area by moving into the first. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(45, 45), + }, + ) + // The cursor leaves handler2 and enters handler1. + assertEventSequence(t, r.Events(handler1), pointer.Enter, pointer.Move) + assertEventSequence(t, r.Events(handler2), pointer.Leave) + + // Move, but stay within the same hit area. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(40, 40), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Move) + assertEventSequence(t, r.Events(handler2)) + + // Move outside of both inputs. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(300, 300), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Leave) + assertEventSequence(t, r.Events(handler2)) + + // Check that a Press event generates Enter Events. + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(125, 125), + }, + ) + assertEventSequence(t, r.Events(handler1)) + assertEventSequence(t, r.Events(handler2), pointer.Enter, pointer.Press) + + // Check that a drag only affects the participating handlers. + r.Queue( + // Leave + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(25, 25), + }, + // Enter + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler1)) + assertEventSequence(t, r.Events(handler2), pointer.Leave, pointer.Drag, + pointer.Enter, pointer.Drag) + + // Check that a Release event generates Enter/Leave Events. + r.Queue( + pointer.Event{ + Type: pointer.Release, + Position: f32.Pt(25, + 25), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Enter) + // The second handler gets the release event because the press started inside it. + assertEventSequence(t, r.Events(handler2), pointer.Release, pointer.Leave) + +} + +func TestMultipleAreas(t *testing.T) { + handler := new(int) + + var ops op.Ops + + addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100)) + st := op.Save(&ops) + pointer.Rect(image.Rect(50, 50, 200, 200)).Add(&ops) + // Second area has no Types set, yet should receive events because + // Types for the same handles are or-ed together. + pointer.InputOp{Tag: handler}.Add(&ops) + st.Load() + + var r Router + r.Frame(&ops) + // Hit first area, then second area, then both. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(25, 25), + }, + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(150, 150), + }, + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter, + pointer.Move, pointer.Move, pointer.Move) +} + +func TestPointerEnterLeaveNested(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + var ops op.Ops + + types := pointer.Press | pointer.Move | pointer.Release | pointer.Enter | pointer.Leave + + // Handler 1 area: (0, 0) - (100, 100) + pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops) + pointer.InputOp{Tag: handler1, Types: types}.Add(&ops) + + // Handler 2 area: (25, 25) - (75, 75) (nested within first). + pointer.Rect(image.Rect(25, 25, 75, 75)).Add(&ops) + pointer.InputOp{Tag: handler2, Types: types}.Add(&ops) + + var r Router + r.Frame(&ops) + // Hit both handlers. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + // First event for a handler is always a Cancel. + // Both handlers should receive the Enter and Move events because handler2 is a child of handler1. + assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, + pointer.Move) + assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter, + pointer.Move) + + // Leave the second area by moving into the first. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(20, 20), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Move) + assertEventSequence(t, r.Events(handler2), pointer.Leave) + + // Move, but stay within the same hit area. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(10, 10), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Move) + assertEventSequence(t, r.Events(handler2)) + + // Move outside of both inputs. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(200, 200), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Leave) + assertEventSequence(t, r.Events(handler2)) + + // Check that a Press event generates Enter Events. + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Enter, pointer.Press) + assertEventSequence(t, r.Events(handler2), pointer.Enter, pointer.Press) + + // Check that a Release event generates Enter/Leave Events. + r.Queue( + pointer.Event{ + Type: pointer.Release, + Position: f32.Pt(20, 20), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Release) + assertEventSequence(t, r.Events(handler2), pointer.Release, pointer.Leave) +} + +func TestPointerActiveInputDisappears(t *testing.T) { + handler1 := new(int) + var ops op.Ops + var r Router + + // Draw handler. + ops.Reset() + addPointerHandler(&ops, handler1, image.Rect(0, 0, 100, 100)) + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(25, 25), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, + pointer.Move) + + // Re-render with handler missing. + ops.Reset() + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(25, 25), + }, + ) + assertEventSequence(t, r.Events(handler1)) +} + +func TestMultitouch(t *testing.T) { + var ops op.Ops + + // Add two separate handlers. + h1, h2 := new(int), new(int) + addPointerHandler(&ops, h1, image.Rect(0, 0, 100, 100)) + addPointerHandler(&ops, h2, image.Rect(0, 100, 100, 200)) + + h1pt, h2pt := f32.Pt(0, 0), f32.Pt(0, 100) + var p1, p2 pointer.ID = 0, 1 + + var r Router + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: h1pt, + PointerID: p1, + }, + ) + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: h2pt, + PointerID: p2, + }, + ) + r.Queue( + pointer.Event{ + Type: pointer.Release, + Position: h2pt, + PointerID: p2, + }, + ) + assertEventSequence(t, r.Events(h1), pointer.Cancel, pointer.Enter, + pointer.Press) + assertEventSequence(t, r.Events(h2), pointer.Cancel, pointer.Enter, + pointer.Press, pointer.Release) +} + +func TestCursorNameOp(t *testing.T) { + ops := new(op.Ops) + var r Router + var h, h2 int + var widget2 func() + widget := func() { + // This is the area where the cursor is changed to CursorPointer. + pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops) + // The cursor is checked and changed upon cursor movement. + pointer.InputOp{Tag: &h}.Add(ops) + pointer.CursorNameOp{Name: pointer.CursorPointer}.Add(ops) + if widget2 != nil { + widget2() + } + } + // Register the handlers. + widget() + // No cursor change as the mouse has not moved yet. + if got, want := r.Cursor(), pointer.CursorDefault; got != want { + t.Errorf("got %q; want %q", got, want) + } + + _at := func(x, y float32) pointer.Event { + return pointer.Event{ + Type: pointer.Move, + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Position: f32.Pt(x, y), + } + } + for _, tc := range []struct { + label string + event interface{} + want pointer.CursorName + }{ + {label: "move inside", + event: _at(50, 50), + want: pointer.CursorPointer, + }, + {label: "move outside", + event: _at(200, 200), + want: pointer.CursorDefault, + }, + {label: "move back inside", + event: _at(50, 50), + want: pointer.CursorPointer, + }, + {label: "send key events while inside", + event: []event.Event{ + key.Event{Name: "A", State: key.Press}, + key.Event{Name: "A", State: key.Release}, + }, + want: pointer.CursorPointer, + }, + {label: "send key events while outside", + event: []event.Event{ + _at(200, 200), + key.Event{Name: "A", State: key.Press}, + key.Event{Name: "A", State: key.Release}, + }, + want: pointer.CursorDefault, + }, + {label: "add new input on top while inside", + event: func() []event.Event { + widget2 = func() { + pointer.InputOp{Tag: &h2}.Add(ops) + pointer.CursorNameOp{Name: pointer.CursorCrossHair}.Add(ops) + } + return []event.Event{ + _at(50, 50), + key.Event{ + Name: "A", + State: key.Press, + }, + } + }, + want: pointer.CursorCrossHair, + }, + {label: "remove input on top while inside", + event: func() []event.Event { + widget2 = nil + return []event.Event{ + _at(50, 50), + key.Event{ + Name: "A", + State: key.Press, + }, + } + }, + want: pointer.CursorPointer, + }, + } { + t.Run(tc.label, func(t *testing.T) { + ops.Reset() + widget() + r.Frame(ops) + switch ev := tc.event.(type) { + case event.Event: + r.Queue(ev) + case []event.Event: + r.Queue(ev...) + case func() event.Event: + r.Queue(ev()) + case func() []event.Event: + r.Queue(ev()...) + default: + panic(fmt.Sprintf("unkown event %T", ev)) + } + widget() + r.Frame(ops) + // The cursor should now have been changed if the mouse moved over the declared area. + if got, want := r.Cursor(), tc.want; got != want { + t.Errorf("got %q; want %q", got, want) + } + }) + } +} + +// addPointerHandler adds a pointer.InputOp for the tag in a +// rectangular area. +func addPointerHandler(ops *op.Ops, tag event.Tag, area image.Rectangle) { + defer op.Save(ops).Load() + pointer.Rect(area).Add(ops) + pointer.InputOp{ + Tag: tag, + Types: pointer.Press | pointer.Release | pointer.Move | pointer.Drag | pointer.Enter | pointer.Leave, + }.Add(ops) +} + +// pointerTypes converts a sequence of event.Event to their pointer.Types. It assumes +// that all input events are of underlying type pointer.Event, and thus will +// panic if some are not. +func pointerTypes(events []event.Event) []pointer.Type { + var types []pointer.Type + for _, e := range events { + if e, ok := e.(pointer.Event); ok { + types = append(types, e.Type) + } + } + return types +} + +// assertEventSequence checks that the provided events match the expected pointer event types +// in the provided order. +func assertEventSequence(t *testing.T, events []event.Event, + expected ...pointer.Type) { + t.Helper() + got := pointerTypes(events) + if !reflect.DeepEqual(got, expected) { + t.Errorf("expected %v events, got %v", expected, got) + } +} + +// assertEventPriorities checks that the pointer.Event priorities of events match prios. +func assertEventPriorities(t *testing.T, events []event.Event, + prios ...pointer.Priority) { + t.Helper() + var got []pointer.Priority + for _, e := range events { + if e, ok := e.(pointer.Event); ok { + got = append(got, e.Priority) + } + } + if !reflect.DeepEqual(got, prios) { + t.Errorf("expected priorities %v, got %v", prios, got) + } +} + +// assertScrollEvent checks that the event scrolling amount matches the supplied value. +func assertScrollEvent(t *testing.T, ev event.Event, scroll f32.Point) { + t.Helper() + if got, want := ev.(pointer.Event).Scroll, scroll; got != want { + t.Errorf("got %v; want %v", got, want) + } +} + +func BenchmarkRouterAdd(b *testing.B) { + // Set this to the number of overlapping handlers that you want to + // evaluate performance for. Typical values for the example applications + // are 1-3, though checking highers values helps evaluate performance for + // more complex applications. + const startingHandlerCount = 3 + const maxHandlerCount = 100 + for i := startingHandlerCount; i < maxHandlerCount; i *= 3 { + handlerCount := i + b.Run(fmt.Sprintf("%d-handlers", i), func(b *testing.B) { + handlers := make([]event.Tag, handlerCount) + for i := 0; i < handlerCount; i++ { + h := new(int) + *h = i + handlers[i] = h + } + var ops op.Ops + + for i := range handlers { + pointer.Rect(image.Rectangle{ + Max: image.Point{ + X: 100, + Y: 100, + }, + }).Add(&ops) + pointer.InputOp{ + Tag: handlers[i], + Types: pointer.Move, + }.Add(&ops) + } + var r Router + r.Frame(&ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + } + }) + } +} + +var benchAreaOp areaOp + +func BenchmarkAreaOp_Decode(b *testing.B) { + ops := new(op.Ops) + pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops) + for i := 0; i < b.N; i++ { + benchAreaOp.Decode(ops.Data()) + } +} + +func BenchmarkAreaOp_Hit(b *testing.B) { + ops := new(op.Ops) + pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops) + benchAreaOp.Decode(ops.Data()) + for i := 0; i < b.N; i++ { + benchAreaOp.Hit(f32.Pt(50, 50)) + } +} diff --git a/gio/io/router/router.go b/gio/io/router/router.go new file mode 100644 index 0000000..f7e251b --- /dev/null +++ b/gio/io/router/router.go @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package router implements Router, a event.Queue implementation +that that disambiguates and routes events to handlers declared +in operation lists. + +Router is used by app.Window and is otherwise only useful for +using Gio with external window implementations. +*/ +package router + +import ( + "encoding/binary" + "time" + + "realy.lol/gio/internal/opconst" + "realy.lol/gio/internal/ops" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/profile" + "realy.lol/gio/op" +) + +// Router is a Queue implementation that routes events +// to handlers declared in operation lists. +type Router struct { + pqueue pointerQueue + kqueue keyQueue + cqueue clipboardQueue + + handlers handlerEvents + + reader ops.Reader + + // InvalidateOp summary. + wakeup bool + wakeupTime time.Time + + // ProfileOp summary. + profHandlers map[event.Tag]struct{} + profile profile.Event +} + +type handlerEvents struct { + handlers map[event.Tag][]event.Event + hadEvents bool +} + +// Events returns the available events for the handler key. +func (q *Router) Events(k event.Tag) []event.Event { + events := q.handlers.Events(k) + if _, isprof := q.profHandlers[k]; isprof { + delete(q.profHandlers, k) + events = append(events, q.profile) + } + return events +} + +// Frame replaces the declared handlers from the supplied +// operation list. The text input state, wakeup time and whether +// there are active profile handlers is also saved. +func (q *Router) Frame(ops *op.Ops) { + q.handlers.Clear() + q.wakeup = false + for k := range q.profHandlers { + delete(q.profHandlers, k) + } + q.reader.Reset(ops) + q.collect() + + q.pqueue.Frame(ops, &q.handlers) + q.kqueue.Frame(ops, &q.handlers) + if q.handlers.HadEvents() { + q.wakeup = true + q.wakeupTime = time.Time{} + } +} + +// Queue an event and report whether at least one handler had an event queued. +func (q *Router) Queue(events ...event.Event) bool { + for _, e := range events { + switch e := e.(type) { + case profile.Event: + q.profile = e + case pointer.Event: + q.pqueue.Push(e, &q.handlers) + case key.EditEvent, key.Event, key.FocusEvent: + q.kqueue.Push(e, &q.handlers) + case clipboard.Event: + q.cqueue.Push(e, &q.handlers) + } + } + return q.handlers.HadEvents() +} + +// TextInputState returns the input state from the most recent +// call to Frame. +func (q *Router) TextInputState() TextInputState { + return q.kqueue.InputState() +} + +// WriteClipboard returns the most recent text to be copied +// to the clipboard, if any. +func (q *Router) WriteClipboard() (string, bool) { + return q.cqueue.WriteClipboard() +} + +// ReadClipboard reports if any new handler is waiting +// to read the clipboard. +func (q *Router) ReadClipboard() bool { + return q.cqueue.ReadClipboard() +} + +// Cursor returns the last cursor set. +func (q *Router) Cursor() pointer.CursorName { + return q.pqueue.cursor +} + +func (q *Router) collect() { + for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() { + switch opconst.OpType(encOp.Data[0]) { + case opconst.TypeInvalidate: + op := decodeInvalidateOp(encOp.Data) + if !q.wakeup || op.At.Before(q.wakeupTime) { + q.wakeup = true + q.wakeupTime = op.At + } + case opconst.TypeProfile: + op := decodeProfileOp(encOp.Data, encOp.Refs) + if q.profHandlers == nil { + q.profHandlers = make(map[event.Tag]struct{}) + } + q.profHandlers[op.Tag] = struct{}{} + case opconst.TypeClipboardRead: + q.cqueue.ProcessReadClipboard(encOp.Data, encOp.Refs) + case opconst.TypeClipboardWrite: + q.cqueue.ProcessWriteClipboard(encOp.Data, encOp.Refs) + } + } +} + +// Profiling reports whether there was profile handlers in the +// most recent Frame call. +func (q *Router) Profiling() bool { + return len(q.profHandlers) > 0 +} + +// WakeupTime returns the most recent time for doing another frame, +// as determined from the last call to Frame. +func (q *Router) WakeupTime() (time.Time, bool) { + return q.wakeupTime, q.wakeup +} + +func (h *handlerEvents) init() { + if h.handlers == nil { + h.handlers = make(map[event.Tag][]event.Event) + } +} + +func (h *handlerEvents) AddNoRedraw(k event.Tag, e event.Event) { + h.init() + h.handlers[k] = append(h.handlers[k], e) +} + +func (h *handlerEvents) Add(k event.Tag, e event.Event) { + h.AddNoRedraw(k, e) + h.hadEvents = true +} + +func (h *handlerEvents) HadEvents() bool { + u := h.hadEvents + h.hadEvents = false + return u +} + +func (h *handlerEvents) Events(k event.Tag) []event.Event { + if events, ok := h.handlers[k]; ok { + h.handlers[k] = h.handlers[k][:0] + // Schedule another frame if we delivered events to the user + // to flush half-updated state. This is important when an + // event changes UI state that has already been laid out. In + // the worst case, we waste a frame, increasing power usage. + // + // Gio is expected to grow the ability to construct + // frame-to-frame differences and only render to changed + // areas. In that case, the waste of a spurious frame should + // be minimal. + h.hadEvents = h.hadEvents || len(events) > 0 + return events + } + return nil +} + +func (h *handlerEvents) Clear() { + for k := range h.handlers { + delete(h.handlers, k) + } +} + +func decodeProfileOp(d []byte, refs []interface{}) profile.Op { + if opconst.OpType(d[0]) != opconst.TypeProfile { + panic("invalid op") + } + return profile.Op{ + Tag: refs[0].(event.Tag), + } +} + +func decodeInvalidateOp(d []byte) op.InvalidateOp { + bo := binary.LittleEndian + if opconst.OpType(d[0]) != opconst.TypeInvalidate { + panic("invalid op") + } + var o op.InvalidateOp + if nanos := bo.Uint64(d[1:]); nanos > 0 { + o.At = time.Unix(0, int64(nanos)) + } + return o +} diff --git a/gio/io/system/system.go b/gio/io/system/system.go new file mode 100644 index 0000000..14e4dd7 --- /dev/null +++ b/gio/io/system/system.go @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package system contains events usually handled at the top-level +// program level. +package system + +import ( + "image" + "time" + + "realy.lol/gio/io/event" + "realy.lol/gio/op" + "realy.lol/gio/unit" +) + +// A FrameEvent requests a new frame in the form of a list of +// operations that describes what to display and how to handle +// input. +type FrameEvent struct { + // Now is the current animation. Use Now instead of time.Now to + // synchronize animation and to avoid the time.Now call overhead. + Now time.Time + // Metric converts device independent dp and sp to device pixels. + Metric unit.Metric + // Size is the dimensions of the window. + Size image.Point + // Insets is the insets to apply. + Insets Insets + // Frame is the callback to supply the list of + // operations to complete the FrameEvent. + // + // Note that the operation list and the operations themselves + // may not be mutated until another FrameEvent is received from + // the same event source. + // That means that calls to frame.Reset and changes to referenced + // data such as ImageOp backing images should happen between + // receiving a FrameEvent and calling Frame. + // + // Example: + // + // var w *app.Window + // var frame *op.Ops + // for e := range w.Events() { + // if e, ok := e.(system.FrameEvent); ok { + // // Call frame.Reset and manipulate images for ImageOps + // // here. + // e.Frame(frame) + // } + // } + Frame func(frame *op.Ops) + // Queue supplies the events for event handlers. + Queue event.Queue +} + +// DestroyEvent is the last event sent through +// a window event channel. +type DestroyEvent struct { + // Err is nil for normal window closures. If a + // window is prematurely closed, Err is the cause. + Err error +} + +// Insets is the space taken up by +// system decoration such as translucent +// system bars and software keyboards. +type Insets struct { + Top, Bottom, Left, Right unit.Value +} + +// A StageEvent is generated whenever the stage of a +// Window changes. +type StageEvent struct { + Stage Stage +} + +// CommandEvent is a system event. Unlike most other events, CommandEvent is +// delivered as a pointer to allow Cancel to suppress it. +type CommandEvent struct { + Type CommandType + // Cancel suppress the default action of the command. + Cancel bool +} + +// Stage of a Window. +type Stage uint8 + +// CommandType is the type of a CommandEvent. +type CommandType uint8 + +const ( + // StagePaused is the Stage for inactive Windows. + // Inactive Windows don't receive FrameEvents. + StagePaused Stage = iota + // StateRunning is for active Windows. + StageRunning +) + +const ( + // CommandBack is the command for a back action + // such as the Android back button. + CommandBack CommandType = iota +) + +func (l Stage) String() string { + switch l { + case StagePaused: + return "StagePaused" + case StageRunning: + return "StageRunning" + default: + panic("unexpected Stage value") + } +} + +func (FrameEvent) ImplementsEvent() {} +func (StageEvent) ImplementsEvent() {} +func (*CommandEvent) ImplementsEvent() {} +func (DestroyEvent) ImplementsEvent() {} diff --git a/gio/layout/alloc_test.go b/gio/layout/alloc_test.go new file mode 100644 index 0000000..1df19e9 --- /dev/null +++ b/gio/layout/alloc_test.go @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//go:build !race +// +build !race + +package layout + +import ( + "image" + "testing" + + "realy.lol/gio/op" +) + +func TestStackAllocs(t *testing.T) { + var ops op.Ops + allocs := testing.AllocsPerRun(1, func() { + ops.Reset() + gtx := Context{ + Ops: &ops, + } + Stack{}.Layout(gtx, + Stacked(func(gtx Context) Dimensions { + return Dimensions{Size: image.Point{X: 50, Y: 50}} + }), + ) + }) + if allocs != 0 { + t.Errorf("expected no allocs, got %f", allocs) + } +} + +func TestFlexAllocs(t *testing.T) { + var ops op.Ops + allocs := testing.AllocsPerRun(1, func() { + ops.Reset() + gtx := Context{ + Ops: &ops, + } + Flex{}.Layout(gtx, + Rigid(func(gtx Context) Dimensions { + return Dimensions{Size: image.Point{X: 50, Y: 50}} + }), + ) + }) + if allocs != 0 { + t.Errorf("expected no allocs, got %f", allocs) + } +} diff --git a/gio/layout/context.go b/gio/layout/context.go new file mode 100644 index 0000000..4f8d2c8 --- /dev/null +++ b/gio/layout/context.go @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "time" + + "realy.lol/gio/f32" + "realy.lol/gio/io/event" + "realy.lol/gio/io/system" + "realy.lol/gio/op" + "realy.lol/gio/unit" +) + +// Context carries the state needed by almost all layouts and widgets. +// A zero value Context never returns events, map units to pixels +// with a scale of 1.0, and returns the zero time from Now. +type Context struct { + // Constraints track the constraints for the active widget or + // layout. + Constraints Constraints + + Metric unit.Metric + // By convention, a nil Queue is a signal to widgets to draw themselves + // in a disabled state. + Queue event.Queue + // Now is the animation time. + Now time.Time + + *op.Ops +} + +// NewContext is a shorthand for +// +// Context{ +// Ops: ops, +// Now: e.Now, +// Queue: e.Queue, +// Config: e.Config, +// Constraints: Exact(e.Size), +// } +// +// NewContext calls ops.Reset and adjusts ops for e.Insets. +func NewContext(ops *op.Ops, e system.FrameEvent) Context { + ops.Reset() + + size := e.Size + + if e.Insets != (system.Insets{}) { + left := e.Metric.Px(e.Insets.Left) + top := e.Metric.Px(e.Insets.Top) + op.Offset(f32.Point{ + X: float32(left), + Y: float32(top), + }).Add(ops) + + size.X -= left + e.Metric.Px(e.Insets.Right) + size.Y -= top + e.Metric.Px(e.Insets.Bottom) + } + + return Context{ + Ops: ops, + Now: e.Now, + Queue: e.Queue, + Metric: e.Metric, + Constraints: Exact(size), + } +} + +// Px maps the value to pixels. +func (c Context) Px(v unit.Value) int { + return c.Metric.Px(v) +} + +// Events returns the events available for the key. If no +// queue is configured, Events returns nil. +func (c Context) Events(k event.Tag) []event.Event { + if c.Queue == nil { + return nil + } + return c.Queue.Events(k) +} + +// Disabled returns a copy of this context with a nil Queue, +// blocking events to widgets using it. +// +// By convention, a nil Queue is a signal to widgets to draw themselves +// in a disabled state. +func (c Context) Disabled() Context { + c.Queue = nil + return c +} diff --git a/gio/layout/doc.go b/gio/layout/doc.go new file mode 100644 index 0000000..3824084 --- /dev/null +++ b/gio/layout/doc.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package layout implements layouts common to GUI programs. + +Constraints and dimensions + +Constraints and dimensions form the interface between layouts and +interface child elements. This package operates on Widgets, functions +that compute Dimensions from a a set of constraints for acceptable +widths and heights. Both the constraints and dimensions are maintained +in an implicit Context to keep the Widget declaration short. + +For example, to add space above a widget: + + var gtx layout.Context + + // Configure a top inset. + inset := layout.Inset{Top: unit.Dp(8), ...} + // Use the inset to lay out a widget. + inset.Layout(gtx, func() { + // Lay out widget and determine its size given the constraints + // in gtx.Constraints. + ... + return layout.Dimensions{...} + }) + +Note that the example does not generate any garbage even though the +Inset is transient. Layouts that don't accept user input are designed +to not escape to the heap during their use. + +Layout operations are recursive: a child in a layout operation can +itself be another layout. That way, complex user interfaces can +be created from a few generic layouts. + +This example both aligns and insets a child: + + inset := layout.Inset{...} + inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + align := layout.Alignment(...) + return align.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return widget.Layout(gtx, ...) + }) + }) + +More complex layouts such as Stack and Flex lay out multiple children, +and stateful layouts such as List accept user input. + +*/ +package layout diff --git a/gio/layout/example_test.go b/gio/layout/example_test.go new file mode 100644 index 0000000..9636c8d --- /dev/null +++ b/gio/layout/example_test.go @@ -0,0 +1,137 @@ +package layout_test + +import ( + "fmt" + "image" + + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/unit" +) + +func ExampleInset() { + gtx := layout.Context{ + Ops: new(op.Ops), + // Loose constraints with no minimal size. + Constraints: layout.Constraints{ + Max: image.Point{X: 100, Y: 100}, + }, + } + + // Inset all edges by 10. + inset := layout.UniformInset(unit.Dp(10)) + dims := inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + // Lay out a 50x50 sized widget. + dims := layoutWidget(gtx, 50, 50) + fmt.Println(dims.Size) + return dims + }) + + fmt.Println(dims.Size) + + // Output: + // (50,50) + // (70,70) +} + +func ExampleDirection() { + gtx := layout.Context{ + Ops: new(op.Ops), + // Rigid constraints with both minimum and maximum set. + Constraints: layout.Exact(image.Point{X: 100, Y: 100}), + } + + dims := layout.Center.Layout(gtx, + func(gtx layout.Context) layout.Dimensions { + // Lay out a 50x50 sized widget. + dims := layoutWidget(gtx, 50, 50) + fmt.Println(dims.Size) + return dims + }) + + fmt.Println(dims.Size) + + // Output: + // (50,50) + // (100,100) +} + +func ExampleFlex() { + gtx := layout.Context{ + Ops: new(op.Ops), + // Rigid constraints with both minimum and maximum set. + Constraints: layout.Exact(image.Point{X: 100, Y: 100}), + } + + layout.Flex{WeightSum: 2}.Layout(gtx, + // Rigid 10x10 widget. + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + fmt.Printf("Rigid: %v\n", gtx.Constraints) + return layoutWidget(gtx, 10, 10) + }), + // Child with 50% space allowance. + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + fmt.Printf("50%%: %v\n", gtx.Constraints) + return layoutWidget(gtx, 10, 10) + }), + ) + + // Output: + // Rigid: {(0,100) (100,100)} + // 50%: {(45,100) (45,100)} +} + +func ExampleStack() { + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Constraints{ + Max: image.Point{X: 100, Y: 100}, + }, + } + + layout.Stack{}.Layout(gtx, + // Force widget to the same size as the second. + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + fmt.Printf("Expand: %v\n", gtx.Constraints) + return layoutWidget(gtx, 10, 10) + }), + // Rigid 50x50 widget. + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + return layoutWidget(gtx, 50, 50) + }), + ) + + // Output: + // Expand: {(50,50) (100,100)} +} + +func ExampleList() { + gtx := layout.Context{ + Ops: new(op.Ops), + // Rigid constraints with both minimum and maximum set. + Constraints: layout.Exact(image.Point{X: 100, Y: 100}), + } + + // The list is 1e6 elements, but only 5 fit the constraints. + const listLen = 1e6 + + var list layout.List + list.Layout(gtx, listLen, + func(gtx layout.Context, i int) layout.Dimensions { + return layoutWidget(gtx, 20, 20) + }) + + fmt.Println(list.Position.Count) + + // Output: + // 5 +} + +func layoutWidget(ctx layout.Context, width, height int) layout.Dimensions { + return layout.Dimensions{ + Size: image.Point{ + X: width, + Y: height, + }, + } +} diff --git a/gio/layout/flex.go b/gio/layout/flex.go new file mode 100644 index 0000000..50d936d --- /dev/null +++ b/gio/layout/flex.go @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "realy.lol/gio/op" +) + +// Flex lays out child elements along an axis, +// according to alignment and weights. +type Flex struct { + // Axis is the main axis, either Horizontal or Vertical. + Axis Axis + // Spacing controls the distribution of space left after + // layout. + Spacing Spacing + // Alignment is the alignment in the cross axis. + Alignment Alignment + // WeightSum is the sum of weights used for the weighted + // size of Flexed children. If WeightSum is zero, the sum + // of all Flexed weights is used. + WeightSum float32 +} + +// FlexChild is the descriptor for a Flex child. +type FlexChild struct { + flex bool + weight float32 + + widget Widget + + // Scratch space. + call op.CallOp + dims Dimensions +} + +// Spacing determine the spacing mode for a Flex. +type Spacing uint8 + +const ( + // SpaceEnd leaves space at the end. + SpaceEnd Spacing = iota + // SpaceStart leaves space at the start. + SpaceStart + // SpaceSides shares space between the start and end. + SpaceSides + // SpaceAround distributes space evenly between children, + // with half as much space at the start and end. + SpaceAround + // SpaceBetween distributes space evenly between children, + // leaving no space at the start and end. + SpaceBetween + // SpaceEvenly distributes space evenly between children and + // at the start and end. + SpaceEvenly +) + +// Rigid returns a Flex child with a maximal constraint of the +// remaining space. +func Rigid(widget Widget) FlexChild { + return FlexChild{ + widget: widget, + } +} + +// Flexed returns a Flex child forced to take up weight fraction of the +// space left over from Rigid children. The fraction is weight +// divided by either the weight sum of all Flexed children or the Flex +// WeightSum if non zero. +func Flexed(weight float32, widget Widget) FlexChild { + return FlexChild{ + flex: true, + weight: weight, + widget: widget, + } +} + +// Layout a list of children. The position of the children are +// determined by the specified order, but Rigid children are laid out +// before Flexed children. +func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions { + size := 0 + cs := gtx.Constraints + mainMin, mainMax := f.Axis.mainConstraint(cs) + crossMin, crossMax := f.Axis.crossConstraint(cs) + remaining := mainMax + var totalWeight float32 + cgtx := gtx + // Lay out Rigid children. + for i, child := range children { + if child.flex { + totalWeight += child.weight + continue + } + macro := op.Record(gtx.Ops) + cgtx.Constraints = f.Axis.constraints(0, remaining, crossMin, crossMax) + dims := child.widget(cgtx) + c := macro.Stop() + sz := f.Axis.Convert(dims.Size).X + size += sz + remaining -= sz + if remaining < 0 { + remaining = 0 + } + children[i].call = c + children[i].dims = dims + } + if w := f.WeightSum; w != 0 { + totalWeight = w + } + // fraction is the rounding error from a Flex weighting. + var fraction float32 + flexTotal := remaining + // Lay out Flexed children. + for i, child := range children { + if !child.flex { + continue + } + var flexSize int + if remaining > 0 && totalWeight > 0 { + // Apply weight and add any leftover fraction from a + // previous Flexed. + childSize := float32(flexTotal) * child.weight / totalWeight + flexSize = int(childSize + fraction + .5) + fraction = childSize - float32(flexSize) + if flexSize > remaining { + flexSize = remaining + } + } + macro := op.Record(gtx.Ops) + cgtx.Constraints = f.Axis.constraints(flexSize, flexSize, crossMin, + crossMax) + dims := child.widget(cgtx) + c := macro.Stop() + sz := f.Axis.Convert(dims.Size).X + size += sz + remaining -= sz + if remaining < 0 { + remaining = 0 + } + children[i].call = c + children[i].dims = dims + } + var maxCross int + var maxBaseline int + for _, child := range children { + if c := f.Axis.Convert(child.dims.Size).Y; c > maxCross { + maxCross = c + } + if b := child.dims.Size.Y - child.dims.Baseline; b > maxBaseline { + maxBaseline = b + } + } + var space int + if mainMin > size { + space = mainMin - size + } + var mainSize int + switch f.Spacing { + case SpaceSides: + mainSize += space / 2 + case SpaceStart: + mainSize += space + case SpaceEvenly: + mainSize += space / (1 + len(children)) + case SpaceAround: + if len(children) > 0 { + mainSize += space / (len(children) * 2) + } + } + for i, child := range children { + dims := child.dims + b := dims.Size.Y - dims.Baseline + var cross int + switch f.Alignment { + case End: + cross = maxCross - f.Axis.Convert(dims.Size).Y + case Middle: + cross = (maxCross - f.Axis.Convert(dims.Size).Y) / 2 + case Baseline: + if f.Axis == Horizontal { + cross = maxBaseline - b + } + } + stack := op.Save(gtx.Ops) + pt := f.Axis.Convert(image.Pt(mainSize, cross)) + op.Offset(FPt(pt)).Add(gtx.Ops) + child.call.Add(gtx.Ops) + stack.Load() + mainSize += f.Axis.Convert(dims.Size).X + if i < len(children)-1 { + switch f.Spacing { + case SpaceEvenly: + mainSize += space / (1 + len(children)) + case SpaceAround: + if len(children) > 0 { + mainSize += space / len(children) + } + case SpaceBetween: + if len(children) > 1 { + mainSize += space / (len(children) - 1) + } + } + } + } + switch f.Spacing { + case SpaceSides: + mainSize += space / 2 + case SpaceEnd: + mainSize += space + case SpaceEvenly: + mainSize += space / (1 + len(children)) + case SpaceAround: + if len(children) > 0 { + mainSize += space / (len(children) * 2) + } + } + sz := f.Axis.Convert(image.Pt(mainSize, maxCross)) + return Dimensions{Size: sz, Baseline: sz.Y - maxBaseline} +} + +func (s Spacing) String() string { + switch s { + case SpaceEnd: + return "SpaceEnd" + case SpaceStart: + return "SpaceStart" + case SpaceSides: + return "SpaceSides" + case SpaceAround: + return "SpaceAround" + case SpaceBetween: + return "SpaceAround" + case SpaceEvenly: + return "SpaceEvenly" + default: + panic("unreachable") + } +} diff --git a/gio/layout/layout.go b/gio/layout/layout.go new file mode 100644 index 0000000..6a4bdd2 --- /dev/null +++ b/gio/layout/layout.go @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "realy.lol/gio/f32" + "realy.lol/gio/op" + "realy.lol/gio/unit" +) + +// Constraints represent the minimum and maximum size of a widget. +// +// A widget does not have to treat its constraints as "hard". For +// example, if it's passed a constraint with a minimum size that's +// smaller than its actual minimum size, it should return its minimum +// size dimensions instead. Parent widgets should deal appropriately +// with child widgets that return dimensions that do not fit their +// constraints (for example, by clipping). +type Constraints struct { + Min, Max image.Point +} + +// Dimensions are the resolved size and baseline for a widget. +// +// Baseline is the distance from the bottom of a widget to the baseline of +// any text it contains (or 0). The purpose is to be able to align text +// that span multiple widgets. +type Dimensions struct { + Size image.Point + Baseline int +} + +// Axis is the Horizontal or Vertical direction. +type Axis uint8 + +// Alignment is the mutual alignment of a list of widgets. +type Alignment uint8 + +// Direction is the alignment of widgets relative to a containing +// space. +type Direction uint8 + +// Widget is a function scope for drawing, processing events and +// computing dimensions for a user interface element. +type Widget func(gtx Context) Dimensions + +const ( + Start Alignment = iota + End + Middle + Baseline +) + +const ( + NW Direction = iota + N + NE + E + SE + S + SW + W + Center +) + +const ( + Horizontal Axis = iota + Vertical +) + +// Exact returns the Constraints with the minimum and maximum size +// set to size. +func Exact(size image.Point) Constraints { + return Constraints{ + Min: size, Max: size, + } +} + +// FPt converts an point to a f32.Point. +func FPt(p image.Point) f32.Point { + return f32.Point{ + X: float32(p.X), Y: float32(p.Y), + } +} + +// FRect converts a rectangle to a f32.Rectangle. +func FRect(r image.Rectangle) f32.Rectangle { + return f32.Rectangle{ + Min: FPt(r.Min), Max: FPt(r.Max), + } +} + +// Constrain a size so each dimension is in the range [min;max]. +func (c Constraints) Constrain(size image.Point) image.Point { + if min := c.Min.X; size.X < min { + size.X = min + } + if min := c.Min.Y; size.Y < min { + size.Y = min + } + if max := c.Max.X; size.X > max { + size.X = max + } + if max := c.Max.Y; size.Y > max { + size.Y = max + } + return size +} + +// Inset adds space around a widget by decreasing its maximum +// constraints. The minimum constraints will be adjusted to ensure +// they do not exceed the maximum. +type Inset struct { + Top, Right, Bottom, Left unit.Value +} + +// Layout a widget. +func (in Inset) Layout(gtx Context, w Widget) Dimensions { + top := gtx.Px(in.Top) + right := gtx.Px(in.Right) + bottom := gtx.Px(in.Bottom) + left := gtx.Px(in.Left) + mcs := gtx.Constraints + mcs.Max.X -= left + right + if mcs.Max.X < 0 { + left = 0 + right = 0 + mcs.Max.X = 0 + } + if mcs.Min.X > mcs.Max.X { + mcs.Min.X = mcs.Max.X + } + mcs.Max.Y -= top + bottom + if mcs.Max.Y < 0 { + bottom = 0 + top = 0 + mcs.Max.Y = 0 + } + if mcs.Min.Y > mcs.Max.Y { + mcs.Min.Y = mcs.Max.Y + } + stack := op.Save(gtx.Ops) + op.Offset(FPt(image.Point{X: left, Y: top})).Add(gtx.Ops) + gtx.Constraints = mcs + dims := w(gtx) + stack.Load() + return Dimensions{ + Size: dims.Size.Add(image.Point{X: right + left, Y: top + bottom}), + Baseline: dims.Baseline + bottom, + } +} + +// UniformInset returns an Inset with a single inset applied to all +// edges. +func UniformInset(v unit.Value) Inset { + return Inset{Top: v, Right: v, Bottom: v, Left: v} +} + +// Layout a widget according to the direction. +// The widget is called with the context constraints minimum cleared. +func (d Direction) Layout(gtx Context, w Widget) Dimensions { + macro := op.Record(gtx.Ops) + cs := gtx.Constraints + gtx.Constraints.Min = image.Point{} + dims := w(gtx) + call := macro.Stop() + sz := dims.Size + if sz.X < cs.Min.X { + sz.X = cs.Min.X + } + if sz.Y < cs.Min.Y { + sz.Y = cs.Min.Y + } + + defer op.Save(gtx.Ops).Load() + p := d.Position(dims.Size, sz) + op.Offset(FPt(p)).Add(gtx.Ops) + call.Add(gtx.Ops) + + return Dimensions{ + Size: sz, + Baseline: dims.Baseline + sz.Y - dims.Size.Y - p.Y, + } +} + +// Position calculates widget position according to the direction. +func (d Direction) Position(widget, bounds image.Point) image.Point { + var p image.Point + + switch d { + case N, S, Center: + p.X = (bounds.X - widget.X) / 2 + case NE, SE, E: + p.X = bounds.X - widget.X + } + + switch d { + case W, Center, E: + p.Y = (bounds.Y - widget.Y) / 2 + case SW, S, SE: + p.Y = bounds.Y - widget.Y + } + + return p +} + +// Spacer adds space between widgets. +type Spacer struct { + Width, Height unit.Value +} + +func (s Spacer) Layout(gtx Context) Dimensions { + return Dimensions{ + Size: image.Point{ + X: gtx.Px(s.Width), + Y: gtx.Px(s.Height), + }, + } +} + +func (a Alignment) String() string { + switch a { + case Start: + return "Start" + case End: + return "End" + case Middle: + return "Middle" + case Baseline: + return "Baseline" + default: + panic("unreachable") + } +} + +// Convert a point in (x, y) coordinates to (main, cross) coordinates, +// or vice versa. Specifically, Convert((x, y)) returns (x, y) unchanged +// for the horizontal axis, or (y, x) for the vertical axis. +func (a Axis) Convert(pt image.Point) image.Point { + if a == Horizontal { + return pt + } + return image.Pt(pt.Y, pt.X) +} + +// mainConstraint returns the min and max main constraints for axis a. +func (a Axis) mainConstraint(cs Constraints) (int, int) { + if a == Horizontal { + return cs.Min.X, cs.Max.X + } + return cs.Min.Y, cs.Max.Y +} + +// crossConstraint returns the min and max cross constraints for axis a. +func (a Axis) crossConstraint(cs Constraints) (int, int) { + if a == Horizontal { + return cs.Min.Y, cs.Max.Y + } + return cs.Min.X, cs.Max.X +} + +// constraints returns the constraints for axis a. +func (a Axis) constraints(mainMin, mainMax, crossMin, crossMax int) Constraints { + if a == Horizontal { + return Constraints{Min: image.Pt(mainMin, crossMin), + Max: image.Pt(mainMax, crossMax)} + } + return Constraints{Min: image.Pt(crossMin, mainMin), + Max: image.Pt(crossMax, mainMax)} +} + +func (a Axis) String() string { + switch a { + case Horizontal: + return "Horizontal" + case Vertical: + return "Vertical" + default: + panic("unreachable") + } +} + +func (d Direction) String() string { + switch d { + case NW: + return "NW" + case N: + return "N" + case NE: + return "NE" + case E: + return "E" + case SE: + return "SE" + case S: + return "S" + case SW: + return "SW" + case W: + return "W" + case Center: + return "Center" + default: + panic("unreachable") + } +} diff --git a/gio/layout/layout_test.go b/gio/layout/layout_test.go new file mode 100644 index 0000000..b04863c --- /dev/null +++ b/gio/layout/layout_test.go @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + "testing" + + "realy.lol/gio/op" +) + +func TestStack(t *testing.T) { + gtx := Context{ + Ops: new(op.Ops), + Constraints: Constraints{ + Max: image.Pt(100, 100), + }, + } + exp := image.Point{X: 60, Y: 70} + dims := Stack{Alignment: Center}.Layout(gtx, + Expanded(func(gtx Context) Dimensions { + return Dimensions{Size: exp} + }), + Stacked(func(gtx Context) Dimensions { + return Dimensions{Size: image.Point{X: 50, Y: 50}} + }), + ) + if got := dims.Size; got != exp { + t.Errorf("Stack ignored Expanded size, got %v expected %v", got, exp) + } +} diff --git a/gio/layout/list.go b/gio/layout/list.go new file mode 100644 index 0000000..45c884e --- /dev/null +++ b/gio/layout/list.go @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "realy.lol/gio/gesture" + "realy.lol/gio/io/pointer" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" +) + +type scrollChild struct { + size image.Point + call op.CallOp +} + +// List displays a subsection of a potentially infinitely +// large underlying list. List accepts user input to scroll +// the subsection. +type List struct { + Axis Axis + // ScrollToEnd instructs the list to stay scrolled to the far end position + // once reached. A List with ScrollToEnd == true and Position.BeforeEnd == + // false draws its content with the last item at the bottom of the list + // area. + ScrollToEnd bool + // Alignment is the cross axis alignment of list elements. + Alignment Alignment + + cs Constraints + scroll gesture.Scroll + scrollDelta int + + // Position is updated during Layout. To save the list scroll position, + // just save Position after Layout finishes. To scroll the list + // programmatically, update Position (e.g. restore it from a saved value) + // before calling Layout. + Position Position + + len int + + // maxSize is the total size of visible children. + maxSize int + children []scrollChild + dir iterationDir +} + +// ListElement is a function that computes the dimensions of +// a list element. +type ListElement func(gtx Context, index int) Dimensions + +type iterationDir uint8 + +// Position is a List scroll offset represented as an offset from the top edge +// of a child element. +type Position struct { + // BeforeEnd tracks whether the List position is before the very end. We + // use "before end" instead of "at end" so that the zero value of a + // Position struct is useful. + // + // When laying out a list, if ScrollToEnd is true and BeforeEnd is false, + // then First and Offset are ignored, and the list is drawn with the last + // item at the bottom. If ScrollToEnd is false then BeforeEnd is ignored. + BeforeEnd bool + // First is the index of the first visible child. + First int + // Offset is the distance in pixels from the top edge to the child at index + // First. + Offset int + // OffsetLast is the signed distance in pixels from the bottom edge to the + // bottom edge of the child at index First+Count. + OffsetLast int + // Count is the number of visible children. + Count int +} + +const ( + iterateNone iterationDir = iota + iterateForward + iterateBackward +) + +const inf = 1e6 + +// init prepares the list for iterating through its children with next. +func (l *List) init(gtx Context, len int) { + if l.more() { + panic("unfinished child") + } + l.cs = gtx.Constraints + l.maxSize = 0 + l.children = l.children[:0] + l.len = len + l.update(gtx) + if l.scrollToEnd() || l.Position.First > len { + l.Position.Offset = 0 + l.Position.First = len + } +} + +// Layout the List. +func (l *List) Layout(gtx Context, len int, w ListElement) Dimensions { + l.init(gtx, len) + crossMin, crossMax := l.Axis.crossConstraint(gtx.Constraints) + gtx.Constraints = l.Axis.constraints(0, inf, crossMin, crossMax) + macro := op.Record(gtx.Ops) + for l.next(); l.more(); l.next() { + child := op.Record(gtx.Ops) + dims := w(gtx, l.index()) + call := child.Stop() + l.end(dims, call) + } + return l.layout(gtx.Ops, macro) +} + +func (l *List) scrollToEnd() bool { + return l.ScrollToEnd && !l.Position.BeforeEnd +} + +// Dragging reports whether the List is being dragged. +func (l *List) Dragging() bool { + return l.scroll.State() == gesture.StateDragging +} + +func (l *List) update(gtx Context) { + d := l.scroll.Scroll(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis)) + l.scrollDelta = d + l.Position.Offset += d +} + +// next advances to the next child. +func (l *List) next() { + l.dir = l.nextDir() + // The user scroll offset is applied after scrolling to + // list end. + if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 { + l.Position.BeforeEnd = true + l.Position.Offset += l.scrollDelta + l.dir = l.nextDir() + } +} + +// index is current child's position in the underlying list. +func (l *List) index() int { + switch l.dir { + case iterateBackward: + return l.Position.First - 1 + case iterateForward: + return l.Position.First + len(l.children) + default: + panic("Index called before Next") + } +} + +// more reports whether more children are needed. +func (l *List) more() bool { + return l.dir != iterateNone +} + +func (l *List) nextDir() iterationDir { + _, vsize := l.Axis.mainConstraint(l.cs) + last := l.Position.First + len(l.children) + // Clamp offset. + if l.maxSize-l.Position.Offset < vsize && last == l.len { + l.Position.Offset = l.maxSize - vsize + } + if l.Position.Offset < 0 && l.Position.First == 0 { + l.Position.Offset = 0 + } + switch { + case len(l.children) == l.len: + return iterateNone + case l.maxSize-l.Position.Offset < vsize: + return iterateForward + case l.Position.Offset < 0: + return iterateBackward + } + return iterateNone +} + +// End the current child by specifying its dimensions. +func (l *List) end(dims Dimensions, call op.CallOp) { + child := scrollChild{dims.Size, call} + mainSize := l.Axis.Convert(child.size).X + l.maxSize += mainSize + switch l.dir { + case iterateForward: + l.children = append(l.children, child) + case iterateBackward: + l.children = append(l.children, scrollChild{}) + copy(l.children[1:], l.children) + l.children[0] = child + l.Position.First-- + l.Position.Offset += mainSize + default: + panic("call Next before End") + } + l.dir = iterateNone +} + +// Layout the List and return its dimensions. +func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { + if l.more() { + panic("unfinished child") + } + mainMin, mainMax := l.Axis.mainConstraint(l.cs) + children := l.children + // Skip invisible children + for len(children) > 0 { + sz := children[0].size + mainSize := l.Axis.Convert(sz).X + if l.Position.Offset < mainSize { + // First child is partially visible. + break + } + l.Position.First++ + l.Position.Offset -= mainSize + children = children[1:] + } + size := -l.Position.Offset + var maxCross int + for i, child := range children { + sz := l.Axis.Convert(child.size) + if c := sz.Y; c > maxCross { + maxCross = c + } + size += sz.X + if size >= mainMax { + children = children[:i+1] + break + } + } + l.Position.Count = len(children) + l.Position.OffsetLast = mainMax - size + pos := -l.Position.Offset + // ScrollToEnd lists are end aligned. + if space := l.Position.OffsetLast; l.ScrollToEnd && space > 0 { + pos += space + } + for _, child := range children { + sz := l.Axis.Convert(child.size) + var cross int + switch l.Alignment { + case End: + cross = maxCross - sz.Y + case Middle: + cross = (maxCross - sz.Y) / 2 + } + childSize := sz.X + max := childSize + pos + if max > mainMax { + max = mainMax + } + min := pos + if min < 0 { + min = 0 + } + r := image.Rectangle{ + Min: l.Axis.Convert(image.Pt(min, -inf)), + Max: l.Axis.Convert(image.Pt(max, inf)), + } + stack := op.Save(ops) + clip.Rect(r).Add(ops) + pt := l.Axis.Convert(image.Pt(pos, cross)) + op.Offset(FPt(pt)).Add(ops) + child.call.Add(ops) + stack.Load() + pos += childSize + } + atStart := l.Position.First == 0 && l.Position.Offset <= 0 + atEnd := l.Position.First+len(children) == l.len && mainMax >= pos + if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 { + l.scroll.Stop() + } + l.Position.BeforeEnd = !atEnd + if pos < mainMin { + pos = mainMin + } + if pos > mainMax { + pos = mainMax + } + dims := l.Axis.Convert(image.Pt(pos, maxCross)) + call := macro.Stop() + defer op.Save(ops).Load() + pointer.Rect(image.Rectangle{Max: dims}).Add(ops) + + var min, max int + if o := l.Position.Offset; o > 0 { + // Use the size of the invisible part as scroll boundary. + min = -o + } else if l.Position.First > 0 { + min = -inf + } + if o := l.Position.OffsetLast; o < 0 { + max = -o + } else if l.Position.First+l.Position.Count < l.len { + max = inf + } + scrollRange := image.Rectangle{ + Min: l.Axis.Convert(image.Pt(min, 0)), + Max: l.Axis.Convert(image.Pt(max, 0)), + } + l.scroll.Add(ops, scrollRange) + + call.Add(ops) + return Dimensions{Size: dims} +} diff --git a/gio/layout/list_test.go b/gio/layout/list_test.go new file mode 100644 index 0000000..6a026b3 --- /dev/null +++ b/gio/layout/list_test.go @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + "testing" + + "realy.lol/gio/f32" + "realy.lol/gio/io/event" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/router" + "realy.lol/gio/op" +) + +func TestListPosition(t *testing.T) { + _s := func(e ...event.Event) []event.Event { return e } + r := new(router.Router) + gtx := Context{ + Ops: new(op.Ops), + Constraints: Constraints{ + Max: image.Pt(20, 10), + }, + Queue: r, + } + el := func(gtx Context, idx int) Dimensions { + return Dimensions{Size: image.Pt(10, 10)} + } + for _, tc := range []struct { + label string + num int + scroll []event.Event + first int + count int + offset int + last int + }{ + {label: "no item", last: 20}, + {label: "1 visible 0 hidden", num: 1, count: 1, last: 10}, + {label: "2 visible 0 hidden", num: 2, count: 2}, + {label: "2 visible 1 hidden", num: 3, count: 2}, + {label: "3 visible 0 hidden small scroll", num: 3, count: 3, offset: 5, + last: -5, + scroll: _s( + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Position: f32.Pt(0, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Type: pointer.Scroll, + Scroll: f32.Pt(5, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Release, + Position: f32.Pt(5, 0), + }, + )}, + {label: "3 visible 0 hidden small scroll 2", num: 3, count: 3, + offset: 3, last: -7, + scroll: _s( + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Position: f32.Pt(0, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Type: pointer.Scroll, + Scroll: f32.Pt(3, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Release, + Position: f32.Pt(5, 0), + }, + )}, + {label: "2 visible 1 hidden large scroll", num: 3, count: 2, first: 1, + scroll: _s( + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Position: f32.Pt(0, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Type: pointer.Scroll, + Scroll: f32.Pt(10, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Release, + Position: f32.Pt(15, 0), + }, + )}, + } { + t.Run(tc.label, func(t *testing.T) { + gtx.Ops.Reset() + + var list List + // Initialize the list. + list.Layout(gtx, tc.num, el) + // Generate the scroll events. + r.Frame(gtx.Ops) + r.Queue(tc.scroll...) + // Let the list process the events. + list.Layout(gtx, tc.num, el) + + pos := list.Position + if got, want := pos.First, tc.first; got != want { + t.Errorf("List: invalid first position: got %v; want %v", got, + want) + } + if got, want := pos.Count, tc.count; got != want { + t.Errorf("List: invalid number of visible children: got %v; want %v", + got, want) + } + if got, want := pos.Offset, tc.offset; got != want { + t.Errorf("List: invalid first visible offset: got %v; want %v", + got, want) + } + if got, want := pos.OffsetLast, tc.last; got != want { + t.Errorf("List: invalid last visible offset: got %v; want %v", + got, want) + } + }) + } +} diff --git a/gio/layout/stack.go b/gio/layout/stack.go new file mode 100644 index 0000000..f46a091 --- /dev/null +++ b/gio/layout/stack.go @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "realy.lol/gio/op" +) + +// Stack lays out child elements on top of each other, +// according to an alignment direction. +type Stack struct { + // Alignment is the direction to align children + // smaller than the available space. + Alignment Direction +} + +// StackChild represents a child for a Stack layout. +type StackChild struct { + expanded bool + widget Widget + + // Scratch space. + call op.CallOp + dims Dimensions +} + +// Stacked returns a Stack child that is laid out with no minimum +// constraints and the maximum constraints passed to Stack.Layout. +func Stacked(w Widget) StackChild { + return StackChild{ + widget: w, + } +} + +// Expanded returns a Stack child with the minimum constraints set +// to the largest Stacked child. The maximum constraints are set to +// the same as passed to Stack.Layout. +func Expanded(w Widget) StackChild { + return StackChild{ + expanded: true, + widget: w, + } +} + +// Layout a stack of children. The position of the children are +// determined by the specified order, but Stacked children are laid out +// before Expanded children. +func (s Stack) Layout(gtx Context, children ...StackChild) Dimensions { + var maxSZ image.Point + // First lay out Stacked children. + cgtx := gtx + cgtx.Constraints.Min = image.Point{} + for i, w := range children { + if w.expanded { + continue + } + macro := op.Record(gtx.Ops) + dims := w.widget(cgtx) + call := macro.Stop() + if w := dims.Size.X; w > maxSZ.X { + maxSZ.X = w + } + if h := dims.Size.Y; h > maxSZ.Y { + maxSZ.Y = h + } + children[i].call = call + children[i].dims = dims + } + // Then lay out Expanded children. + for i, w := range children { + if !w.expanded { + continue + } + macro := op.Record(gtx.Ops) + cgtx.Constraints.Min = maxSZ + dims := w.widget(cgtx) + call := macro.Stop() + if w := dims.Size.X; w > maxSZ.X { + maxSZ.X = w + } + if h := dims.Size.Y; h > maxSZ.Y { + maxSZ.Y = h + } + children[i].call = call + children[i].dims = dims + } + + maxSZ = gtx.Constraints.Constrain(maxSZ) + var baseline int + for _, ch := range children { + sz := ch.dims.Size + var p image.Point + switch s.Alignment { + case N, S, Center: + p.X = (maxSZ.X - sz.X) / 2 + case NE, SE, E: + p.X = maxSZ.X - sz.X + } + switch s.Alignment { + case W, Center, E: + p.Y = (maxSZ.Y - sz.Y) / 2 + case SW, S, SE: + p.Y = maxSZ.Y - sz.Y + } + stack := op.Save(gtx.Ops) + op.Offset(FPt(p)).Add(gtx.Ops) + ch.call.Add(gtx.Ops) + stack.Load() + if baseline == 0 { + if b := ch.dims.Baseline; b != 0 { + baseline = b + maxSZ.Y - sz.Y - p.Y + } + } + } + return Dimensions{ + Size: maxSZ, + Baseline: baseline, + } +} diff --git a/gio/op/clip/clip.go b/gio/op/clip/clip.go new file mode 100644 index 0000000..360d89f --- /dev/null +++ b/gio/op/clip/clip.go @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clip + +import ( + "encoding/binary" + "image" + "math" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/opconst" + "realy.lol/gio/internal/ops" + "realy.lol/gio/internal/scene" + "realy.lol/gio/internal/stroke" + "realy.lol/gio/op" +) + +// Op represents a clip area. Op intersects the current clip area with +// itself. +type Op struct { + bounds image.Rectangle + path PathSpec + + outline bool + stroke StrokeStyle + dashes DashSpec +} + +func (p Op) Add(o *op.Ops) { + str := p.stroke + dashes := p.dashes + path := p.path + outline := p.outline + approx := str.Width > 0 && !(dashes == DashSpec{} && str.Miter == 0 && str.Join == RoundJoin && str.Cap == RoundCap) + if approx { + // If the stroke is not natively supported by the compute renderer, construct a filled path + // that approximates it. + path = p.approximateStroke(o) + dashes = DashSpec{} + str = StrokeStyle{} + outline = true + } + + if path.hasSegments { + data := o.Write(opconst.TypePathLen) + data[0] = byte(opconst.TypePath) + path.spec.Add(o) + } + + if str.Width > 0 { + data := o.Write(opconst.TypeStrokeLen) + data[0] = byte(opconst.TypeStroke) + bo := binary.LittleEndian + bo.PutUint32(data[1:], math.Float32bits(str.Width)) + } + + data := o.Write(opconst.TypeClipLen) + data[0] = byte(opconst.TypeClip) + bo := binary.LittleEndian + bo.PutUint32(data[1:], uint32(p.bounds.Min.X)) + bo.PutUint32(data[5:], uint32(p.bounds.Min.Y)) + bo.PutUint32(data[9:], uint32(p.bounds.Max.X)) + bo.PutUint32(data[13:], uint32(p.bounds.Max.Y)) + if outline { + data[17] = byte(1) + } +} + +func (p Op) approximateStroke(o *op.Ops) PathSpec { + if !p.path.hasSegments { + return PathSpec{} + } + + var r ops.Reader + // Add path op for us to decode. Use a macro to omit it from later decodes. + ignore := op.Record(o) + r.ResetAt(o, ops.NewPC(o)) + p.path.spec.Add(o) + ignore.Stop() + encOp, ok := r.Decode() + if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux { + panic("corrupt path data") + } + pathData := encOp.Data[opconst.TypeAuxLen:] + + // Decode dashes in a similar way. + var dashes stroke.DashOp + if p.dashes.phase != 0 || p.dashes.size > 0 { + ignore := op.Record(o) + r.ResetAt(o, ops.NewPC(o)) + p.dashes.spec.Add(o) + ignore.Stop() + encOp, ok := r.Decode() + if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux { + panic("corrupt dash data") + } + dashes.Dashes = make([]float32, p.dashes.size) + dashData := encOp.Data[opconst.TypeAuxLen:] + bo := binary.LittleEndian + for i := range dashes.Dashes { + dashes.Dashes[i] = math.Float32frombits(bo.Uint32(dashData[i*4:])) + } + dashes.Phase = p.dashes.phase + } + + // Approximate and output path data. + var outline Path + outline.Begin(o) + ss := stroke.StrokeStyle{ + Width: p.stroke.Width, + Miter: p.stroke.Miter, + Cap: stroke.StrokeCap(p.stroke.Cap), + Join: stroke.StrokeJoin(p.stroke.Join), + } + quads := stroke.StrokePathCommands(ss, dashes, pathData) + pen := f32.Pt(0, 0) + for _, quad := range quads { + q := quad.Quad + if q.From != pen { + pen = q.From + outline.MoveTo(pen) + } + outline.contour = int(quad.Contour) + outline.QuadTo(q.Ctrl, q.To) + } + return outline.End() +} + +type PathSpec struct { + spec op.CallOp + // open is true if any path contour is not closed. A closed contour starts + // and ends in the same point. + open bool + // hasSegments tracks whether there are any segments in the path. + hasSegments bool +} + +// Path constructs a Op clip path described by lines and +// BĆ©zier curves, where drawing outside the Path is discarded. +// The inside-ness of a pixel is determines by the non-zero winding rule, +// similar to the SVG rule of the same name. +// +// Path generates no garbage and can be used for dynamic paths; path +// data is stored directly in the Ops list supplied to Begin. +type Path struct { + ops *op.Ops + open bool + contour int + pen f32.Point + macro op.MacroOp + start f32.Point + hasSegments bool +} + +// Pos returns the current pen position. +func (p *Path) Pos() f32.Point { return p.pen } + +// Begin the path, storing the path data and final Op into ops. +func (p *Path) Begin(ops *op.Ops) { + p.ops = ops + p.macro = op.Record(ops) + // Write the TypeAux opcode + data := ops.Write(opconst.TypeAuxLen) + data[0] = byte(opconst.TypeAux) +} + +// End returns a PathSpec ready to use in clipping operations. +func (p *Path) End() PathSpec { + c := p.macro.Stop() + return PathSpec{ + spec: c, + open: p.open || p.pen != p.start, + hasSegments: p.hasSegments, + } +} + +// Move moves the pen by the amount specified by delta. +func (p *Path) Move(delta f32.Point) { + to := delta.Add(p.pen) + p.MoveTo(to) +} + +// MoveTo moves the pen to the specified absolute coordinate. +func (p *Path) MoveTo(to f32.Point) { + p.open = p.open || p.pen != p.start + p.end() + p.pen = to + p.start = to +} + +// end completes the current contour. +func (p *Path) end() { + p.contour++ +} + +// Line moves the pen by the amount specified by delta, recording a line. +func (p *Path) Line(delta f32.Point) { + to := delta.Add(p.pen) + p.LineTo(to) +} + +// LineTo moves the pen to the absolute point specified, recording a line. +func (p *Path) LineTo(to f32.Point) { + data := p.ops.Write(scene.CommandSize + 4) + bo := binary.LittleEndian + bo.PutUint32(data[0:], uint32(p.contour)) + ops.EncodeCommand(data[4:], scene.Line(p.pen, to)) + p.pen = to + p.hasSegments = true +} + +// Quad records a quadratic BĆ©zier from the pen to end +// with the control point ctrl. +func (p *Path) Quad(ctrl, to f32.Point) { + ctrl = ctrl.Add(p.pen) + to = to.Add(p.pen) + p.QuadTo(ctrl, to) +} + +// QuadTo records a quadratic BĆ©zier from the pen to end +// with the control point ctrl, with absolute coordinates. +func (p *Path) QuadTo(ctrl, to f32.Point) { + data := p.ops.Write(scene.CommandSize + 4) + bo := binary.LittleEndian + bo.PutUint32(data[0:], uint32(p.contour)) + ops.EncodeCommand(data[4:], scene.Quad(p.pen, ctrl, to)) + p.pen = to + p.hasSegments = true +} + +// Arc adds an elliptical arc to the path. The implied ellipse is defined +// by its focus points f1 and f2. +// The arc starts in the current point and ends angle radians along the ellipse boundary. +// The sign of angle determines the direction; positive being counter-clockwise, +// negative clockwise. +func (p *Path) Arc(f1, f2 f32.Point, angle float32) { + f1 = f1.Add(p.pen) + f2 = f2.Add(p.pen) + const segments = 16 + m := stroke.ArcTransform(p.pen, f1, f2, angle, segments) + + for i := 0; i < segments; i++ { + p0 := p.pen + p1 := m.Transform(p0) + p2 := m.Transform(p1) + ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5)) + p.QuadTo(ctl, p2) + } +} + +// Cube records a cubic BĆ©zier from the pen through +// two control points ending in to. +func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) { + p.CubeTo(p.pen.Add(ctrl0), p.pen.Add(ctrl1), p.pen.Add(to)) +} + +// CubeTo records a cubic BĆ©zier from the pen through +// two control points ending in to, with absolute coordinates. +func (p *Path) CubeTo(ctrl0, ctrl1, to f32.Point) { + if ctrl0 == p.pen && ctrl1 == p.pen && to == p.pen { + return + } + data := p.ops.Write(scene.CommandSize + 4) + bo := binary.LittleEndian + bo.PutUint32(data[0:], uint32(p.contour)) + ops.EncodeCommand(data[4:], scene.Cubic(p.pen, ctrl0, ctrl1, to)) + p.pen = to + p.hasSegments = true +} + +// Close closes the path contour. +func (p *Path) Close() { + if p.pen != p.start { + p.LineTo(p.start) + } + p.end() +} + +// Outline represents the area inside of a path, according to the +// non-zero winding rule. +type Outline struct { + Path PathSpec +} + +// Op returns a clip operation representing the outline. +func (o Outline) Op() Op { + if o.Path.open { + panic("not all path contours are closed") + } + return Op{ + path: o.Path, + outline: true, + } +} diff --git a/gio/op/clip/clip_test.go b/gio/op/clip/clip_test.go new file mode 100644 index 0000000..7962c6d --- /dev/null +++ b/gio/op/clip/clip_test.go @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clip + +import ( + "testing" + + "realy.lol/gio/f32" + "realy.lol/gio/op" +) + +func TestOpenPathOutlinePanic(t *testing.T) { + defer func() { + if err := recover(); err == nil { + t.Error("Outline of an open path didn't panic") + } + }() + var p Path + p.Begin(new(op.Ops)) + p.Line(f32.Pt(10, 10)) + Outline{Path: p.End()}.Op() +} diff --git a/gio/op/clip/doc.go b/gio/op/clip/doc.go new file mode 100644 index 0000000..6ba5546 --- /dev/null +++ b/gio/op/clip/doc.go @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package clip provides operations for clipping paint operations. +Drawing outside the current clip area is ignored. + +The current clip is initially the infinite set. An Op sets the clip +to the intersection of the current clip and the clip area it +represents. If you need to reset the current clip to its value +before applying an Op, use op.StackOp. + +General clipping areas are constructed with Path. Simpler special +cases such as rectangular clip areas also exist as convenient +constructors. +*/ +package clip diff --git a/gio/op/clip/shapes.go b/gio/op/clip/shapes.go new file mode 100644 index 0000000..9ea84e3 --- /dev/null +++ b/gio/op/clip/shapes.go @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clip + +import ( + "image" + "math" + + "realy.lol/gio/f32" + "realy.lol/gio/op" +) + +// Rect represents the clip area of a pixel-aligned rectangle. +type Rect image.Rectangle + +// Op returns the op for the rectangle. +func (r Rect) Op() Op { + return Op{ + bounds: image.Rectangle(r), + outline: true, + } +} + +// Add the clip operation. +func (r Rect) Add(ops *op.Ops) { + r.Op().Add(ops) +} + +// UniformRRect returns an RRect with all corner radii set to the +// provided radius. +func UniformRRect(rect f32.Rectangle, radius float32) RRect { + return RRect{ + Rect: rect, + SE: radius, + SW: radius, + NE: radius, + NW: radius, + } +} + +// RRect represents the clip area of a rectangle with rounded +// corners. +// +// Specify a square with corner radii equal to half the square size to +// construct a circular clip area. +type RRect struct { + Rect f32.Rectangle + // The corner radii. + SE, SW, NW, NE float32 +} + +// Op returns the op for the rounded rectangle. +func (rr RRect) Op(ops *op.Ops) Op { + if rr.SE == 0 && rr.SW == 0 && rr.NW == 0 && rr.NE == 0 { + r := image.Rectangle{ + Min: image.Point{X: int(rr.Rect.Min.X), Y: int(rr.Rect.Min.Y)}, + Max: image.Point{X: int(rr.Rect.Max.X), Y: int(rr.Rect.Max.Y)}, + } + // Only use Rect if rr is pixel-aligned, as Rect is guaranteed to be. + if fPt(r.Min) == rr.Rect.Min && fPt(r.Max) == rr.Rect.Max { + return Rect(r).Op() + } + } + return Outline{Path: rr.Path(ops)}.Op() +} + +// Add the rectangle clip. +func (rr RRect) Add(ops *op.Ops) { + rr.Op(ops).Add(ops) +} + +// Path returns the PathSpec for the rounded rectangle. +func (rr RRect) Path(ops *op.Ops) PathSpec { + var p Path + p.Begin(ops) + + // https://pomax.github.io/bezierinfo/#circles_cubic. + const q = 4 * (math.Sqrt2 - 1) / 3 + const iq = 1 - q + + se, sw, nw, ne := rr.SE, rr.SW, rr.NW, rr.NE + w, n, e, s := rr.Rect.Min.X, rr.Rect.Min.Y, rr.Rect.Max.X, rr.Rect.Max.Y + + p.MoveTo(f32.Point{X: w + nw, Y: n}) + p.LineTo(f32.Point{X: e - ne, Y: n}) // N + p.CubeTo( // NE + f32.Point{X: e - ne*iq, Y: n}, + f32.Point{X: e, Y: n + ne*iq}, + f32.Point{X: e, Y: n + ne}) + p.LineTo(f32.Point{X: e, Y: s - se}) // E + p.CubeTo( // SE + f32.Point{X: e, Y: s - se*iq}, + f32.Point{X: e - se*iq, Y: s}, + f32.Point{X: e - se, Y: s}) + p.LineTo(f32.Point{X: w + sw, Y: s}) // S + p.CubeTo( // SW + f32.Point{X: w + sw*iq, Y: s}, + f32.Point{X: w, Y: s - sw*iq}, + f32.Point{X: w, Y: s - sw}) + p.LineTo(f32.Point{X: w, Y: n + nw}) // W + p.CubeTo( // NW + f32.Point{X: w, Y: n + nw*iq}, + f32.Point{X: w + nw*iq, Y: n}, + f32.Point{X: w + nw, Y: n}) + + return p.End() +} + +// Circle represents the clip area of a circle. +type Circle struct { + Center f32.Point + Radius float32 +} + +// Op returns the op for the circle. +func (c Circle) Op(ops *op.Ops) Op { + return Outline{Path: c.Path(ops)}.Op() +} + +// Add the circle clip. +func (c Circle) Add(ops *op.Ops) { + c.Op(ops).Add(ops) +} + +// Path returns the PathSpec for the circle. +func (c Circle) Path(ops *op.Ops) PathSpec { + var p Path + p.Begin(ops) + + center := c.Center + r := c.Radius + + // https://pomax.github.io/bezierinfo/#circles_cubic. + const q = 4 * (math.Sqrt2 - 1) / 3 + + curve := r * q + top := f32.Point{X: center.X, Y: center.Y - r} + + p.MoveTo(top) + p.CubeTo( + f32.Point{X: center.X + curve, Y: center.Y - r}, + f32.Point{X: center.X + r, Y: center.Y - curve}, + f32.Point{X: center.X + r, Y: center.Y}, + ) + p.CubeTo( + f32.Point{X: center.X + r, Y: center.Y + curve}, + f32.Point{X: center.X + curve, Y: center.Y + r}, + f32.Point{X: center.X, Y: center.Y + r}, + ) + p.CubeTo( + f32.Point{X: center.X - curve, Y: center.Y + r}, + f32.Point{X: center.X - r, Y: center.Y + curve}, + f32.Point{X: center.X - r, Y: center.Y}, + ) + p.CubeTo( + f32.Point{X: center.X - r, Y: center.Y - curve}, + f32.Point{X: center.X - curve, Y: center.Y - r}, + top, + ) + return p.End() +} + +func fPt(p image.Point) f32.Point { + return f32.Point{ + X: float32(p.X), Y: float32(p.Y), + } +} diff --git a/gio/op/clip/stroke.go b/gio/op/clip/stroke.go new file mode 100644 index 0000000..8610eab --- /dev/null +++ b/gio/op/clip/stroke.go @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clip + +import ( + "encoding/binary" + "math" + + "realy.lol/gio/internal/opconst" + "realy.lol/gio/op" +) + +// Stroke represents a stroked path. +type Stroke struct { + Path PathSpec + Style StrokeStyle + + // Dashes specify the dashes of the stroke. + // The empty value denotes no dashes. + Dashes DashSpec +} + +// Op returns a clip operation representing the stroke. +func (s Stroke) Op() Op { + return Op{ + path: s.Path, + stroke: s.Style, + dashes: s.Dashes, + } +} + +// StrokeStyle describes how a path should be stroked. +type StrokeStyle struct { + Width float32 // Width of the stroked path. + + // Miter is the limit to apply to a miter joint. + // The zero Miter disables the miter joint; setting Miter to +āˆž + // unconditionally enables the miter joint. + Miter float32 + Cap StrokeCap // Cap describes the head or tail of a stroked path. + Join StrokeJoin // Join describes how stroked paths are collated. +} + +// StrokeCap describes the head or tail of a stroked path. +type StrokeCap uint8 + +const ( + // RoundCap caps stroked paths with a round cap, joining the right-hand and + // left-hand sides of a stroked path with a half disc of diameter the + // stroked path's width. + RoundCap StrokeCap = iota + + // FlatCap caps stroked paths with a flat cap, joining the right-hand + // and left-hand sides of a stroked path with a straight line. + FlatCap + + // SquareCap caps stroked paths with a square cap, joining the right-hand + // and left-hand sides of a stroked path with a half square of length + // the stroked path's width. + SquareCap +) + +// StrokeJoin describes how stroked paths are collated. +type StrokeJoin uint8 + +const ( + // RoundJoin joins path segments with a round segment. + RoundJoin StrokeJoin = iota + + // BevelJoin joins path segments with sharp bevels. + BevelJoin +) + +// Dash records dashes' lengths and phase for a stroked path. +type Dash struct { + ops *op.Ops + macro op.MacroOp + phase float32 + size uint8 // size of the pattern +} + +func (d *Dash) Begin(ops *op.Ops) { + d.ops = ops + d.macro = op.Record(ops) + // Write the TypeAux opcode + data := ops.Write(opconst.TypeAuxLen) + data[0] = byte(opconst.TypeAux) +} + +func (d *Dash) Phase(v float32) { + d.phase = v +} + +func (d *Dash) Dash(length float32) { + if d.size == math.MaxUint8 { + panic("clip: dash pattern too large") + } + data := d.ops.Write(4) + bo := binary.LittleEndian + bo.PutUint32(data[0:], math.Float32bits(length)) + d.size++ +} + +func (d *Dash) End() DashSpec { + c := d.macro.Stop() + return DashSpec{ + spec: c, + phase: d.phase, + size: d.size, + } +} + +// DashSpec describes a dashed pattern. +type DashSpec struct { + spec op.CallOp + phase float32 + size uint8 // size of the pattern +} diff --git a/gio/op/op.go b/gio/op/op.go new file mode 100644 index 0000000..f29aa0b --- /dev/null +++ b/gio/op/op.go @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* + +Package op implements operations for updating a user interface. + +Gio programs use operations, or ops, for describing their user +interfaces. There are operations for drawing, defining input +handlers, changing window properties as well as operations for +controlling the execution of other operations. + +Ops represents a list of operations. The most important use +for an Ops list is to describe a complete user interface update +to a ui/app.Window's Update method. + +Drawing a colored square: + + import "realy.lol/gio/unit" + import "realy.lol/gio/app" + import "realy.lol/gio/op/paint" + + var w app.Window + var e system.FrameEvent + ops := new(op.Ops) + ... + ops.Reset() + paint.ColorOp{Color: ...}.Add(ops) + paint.PaintOp{Rect: ...}.Add(ops) + e.Frame(ops) + +State + +An Ops list can be viewed as a very simple virtual machine: it has an implicit +mutable state stack and execution flow can be controlled with macros. + +The Save function saves the current state for later restoring: + + ops := new(op.Ops) + // Save the current state, in particular the transform. + state := op.Save(ops) + // Apply a transform to subsequent operations. + op.Offset(...).Add(ops) + ... + // Restore the previous transform. + state.Load() + +You can also use this one-line to save the current state and restore it at the +end of a function : + + defer op.Save(ops).Load() + +The MacroOp records a list of operations to be executed later: + + ops := new(op.Ops) + macro := op.Record(ops) + // Record operations by adding them. + op.InvalidateOp{}.Add(ops) + ... + // End recording. + call := macro.Stop() + + // replay the recorded operations: + call.Add(ops) + +*/ +package op + +import ( + "encoding/binary" + "math" + "time" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/opconst" +) + +// Ops holds a list of operations. Operations are stored in +// serialized form to avoid garbage during construction of +// the ops list. +type Ops struct { + // version is incremented at each Reset. + version int + // data contains the serialized operations. + data []byte + // refs hold external references for operations. + refs []interface{} + // nextStateID is the id allocated for the next + // StateOp. + nextStateID int + + macroStack stack +} + +// StateOp represents a saved operation snapshop to be restored +// later. +type StateOp struct { + id int + macroID int + ops *Ops +} + +// MacroOp records a list of operations for later use. +type MacroOp struct { + ops *Ops + id stackID + pc pc +} + +// CallOp invokes the operations recorded by Record. +type CallOp struct { + // Ops is the list of operations to invoke. + ops *Ops + pc pc +} + +// InvalidateOp requests a redraw at the given time. Use +// the zero value to request an immediate redraw. +type InvalidateOp struct { + At time.Time +} + +// TransformOp applies a transform to the current transform. The zero value +// for TransformOp represents the identity transform. +type TransformOp struct { + t f32.Affine2D +} + +// stack tracks the integer identities of MacroOp +// operations to ensure correct pairing of Record/End. +type stack struct { + currentID int + nextID int +} + +type stackID struct { + id int + prev int +} + +type pc struct { + data int + refs int +} + +// Defer executes c after all other operations have completed, +// including previously deferred operations. +// Defer saves the current transformation and restores it prior +// to execution. All other operation state is reset. +// +// Note that deferred operations are executed in first-in-first-out +// order, unlike the Go facility of the same name. +func Defer(o *Ops, c CallOp) { + if c.ops == nil { + return + } + state := Save(o) + // Wrap c in a macro that loads the saved state before execution. + m := Record(o) + load(o, opconst.InitialStateID, opconst.AllState) + load(o, state.id, opconst.TransformState) + c.Add(o) + c = m.Stop() + // A Defer is recorded as a TypeDefer followed by the + // wrapped macro. + data := o.Write(opconst.TypeDeferLen) + data[0] = byte(opconst.TypeDefer) + c.Add(o) +} + +// Save the current operations state. +func Save(o *Ops) StateOp { + o.nextStateID++ + s := StateOp{ + ops: o, + id: o.nextStateID, + macroID: o.macroStack.currentID, + } + save(o, s.id) + return s +} + +// save records a save of the operations state to +// id. +func save(o *Ops, id int) { + bo := binary.LittleEndian + data := o.Write(opconst.TypeSaveLen) + data[0] = byte(opconst.TypeSave) + bo.PutUint32(data[1:], uint32(id)) +} + +// Load a previously saved operations state. +func (s StateOp) Load() { + if s.ops.macroStack.currentID != s.macroID { + panic("load in a different macro than save") + } + if s.id == 0 { + panic("zero-value op") + } + load(s.ops, s.id, opconst.AllState) +} + +// load a previously saved operations state given +// its ID. Only state included in mask is affected. +func load(o *Ops, id int, m opconst.StateMask) { + bo := binary.LittleEndian + data := o.Write(opconst.TypeLoadLen) + data[0] = byte(opconst.TypeLoad) + data[1] = byte(m) + bo.PutUint32(data[2:], uint32(id)) +} + +// Reset the Ops, preparing it for re-use. Reset invalidates +// any recorded macros. +func (o *Ops) Reset() { + o.macroStack = stack{} + // Leave references to the GC. + for i := range o.refs { + o.refs[i] = nil + } + o.data = o.data[:0] + o.refs = o.refs[:0] + o.nextStateID = 0 + o.version++ +} + +// Data is for internal use only. +func (o *Ops) Data() []byte { + return o.data +} + +// Refs is for internal use only. +func (o *Ops) Refs() []interface{} { + return o.refs +} + +// Version is for internal use only. +func (o *Ops) Version() int { + return o.version +} + +// Write is for internal use only. +func (o *Ops) Write(n int) []byte { + o.data = append(o.data, make([]byte, n)...) + return o.data[len(o.data)-n:] +} + +// Write1 is for internal use only. +func (o *Ops) Write1(n int, ref1 interface{}) []byte { + o.data = append(o.data, make([]byte, n)...) + o.refs = append(o.refs, ref1) + return o.data[len(o.data)-n:] +} + +// Write2 is for internal use only. +func (o *Ops) Write2(n int, ref1, ref2 interface{}) []byte { + o.data = append(o.data, make([]byte, n)...) + o.refs = append(o.refs, ref1, ref2) + return o.data[len(o.data)-n:] +} + +func (o *Ops) pc() pc { + return pc{data: len(o.data), refs: len(o.refs)} +} + +// Record a macro of operations. +func Record(o *Ops) MacroOp { + m := MacroOp{ + ops: o, + id: o.macroStack.push(), + pc: o.pc(), + } + // Reserve room for a macro definition. Updated in Stop. + m.ops.Write(opconst.TypeMacroLen) + m.fill() + return m +} + +// Stop ends a previously started recording and returns an +// operation for replaying it. +func (m MacroOp) Stop() CallOp { + m.ops.macroStack.pop(m.id) + m.fill() + return CallOp{ + ops: m.ops, + pc: m.pc, + } +} + +func (m MacroOp) fill() { + pc := m.ops.pc() + // Fill out the macro definition reserved in Record. + data := m.ops.data[m.pc.data:] + data = data[:opconst.TypeMacroLen] + data[0] = byte(opconst.TypeMacro) + bo := binary.LittleEndian + bo.PutUint32(data[1:], uint32(pc.data)) + bo.PutUint32(data[5:], uint32(pc.refs)) +} + +// Add the recorded list of operations. Add +// panics if the Ops containing the recording +// has been reset. +func (c CallOp) Add(o *Ops) { + if c.ops == nil { + return + } + data := o.Write1(opconst.TypeCallLen, c.ops) + data[0] = byte(opconst.TypeCall) + bo := binary.LittleEndian + bo.PutUint32(data[1:], uint32(c.pc.data)) + bo.PutUint32(data[5:], uint32(c.pc.refs)) +} + +func (r InvalidateOp) Add(o *Ops) { + data := o.Write(opconst.TypeRedrawLen) + data[0] = byte(opconst.TypeInvalidate) + bo := binary.LittleEndian + // UnixNano cannot represent the zero time. + if t := r.At; !t.IsZero() { + nanos := t.UnixNano() + if nanos > 0 { + bo.PutUint64(data[1:], uint64(nanos)) + } + } +} + +// Offset creates a TransformOp with the offset o. +func Offset(o f32.Point) TransformOp { + return TransformOp{t: f32.Affine2D{}.Offset(o)} +} + +// Affine creates a TransformOp representing the transformation a. +func Affine(a f32.Affine2D) TransformOp { + return TransformOp{t: a} +} + +func (t TransformOp) Add(o *Ops) { + data := o.Write(opconst.TypeTransformLen) + data[0] = byte(opconst.TypeTransform) + bo := binary.LittleEndian + a, b, c, d, e, f := t.t.Elems() + bo.PutUint32(data[1:], math.Float32bits(a)) + bo.PutUint32(data[1+4*1:], math.Float32bits(b)) + bo.PutUint32(data[1+4*2:], math.Float32bits(c)) + bo.PutUint32(data[1+4*3:], math.Float32bits(d)) + bo.PutUint32(data[1+4*4:], math.Float32bits(e)) + bo.PutUint32(data[1+4*5:], math.Float32bits(f)) +} + +func (s *stack) push() stackID { + s.nextID++ + sid := stackID{ + id: s.nextID, + prev: s.currentID, + } + s.currentID = s.nextID + return sid +} + +func (s *stack) check(sid stackID) { + if s.currentID != sid.id { + panic("unbalanced operation") + } +} + +func (s *stack) pop(sid stackID) { + s.check(sid) + s.currentID = sid.prev +} diff --git a/gio/op/paint/doc.go b/gio/op/paint/doc.go new file mode 100644 index 0000000..79054ab --- /dev/null +++ b/gio/op/paint/doc.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package paint provides drawing operations for 2D graphics. + +The PaintOp operation fills the current clip with the current brush, +taking the current transformation into account. + +The current brush is set by either a ColorOp for a constant color, or +ImageOp for an image, or LinearGradientOp for gradients. + +All color.NRGBA values are in the sRGB color space. +*/ +package paint diff --git a/gio/op/paint/paint.go b/gio/op/paint/paint.go new file mode 100644 index 0000000..e53a763 --- /dev/null +++ b/gio/op/paint/paint.go @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package paint + +import ( + "encoding/binary" + "image" + "image/color" + "image/draw" + "math" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/opconst" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" +) + +// ImageOp sets the brush to an image. +// +// Note: the ImageOp may keep a reference to the backing image. +// See NewImageOp for details. +type ImageOp struct { + uniform bool + color color.NRGBA + src *image.RGBA + + // handle is a key to uniquely identify this ImageOp + // in a map of cached textures. + handle interface{} +} + +// ColorOp sets the brush to a constant color. +type ColorOp struct { + Color color.NRGBA +} + +// LinearGradientOp sets the brush to a gradient starting at stop1 with color1 and +// ending at stop2 with color2. +type LinearGradientOp struct { + Stop1 f32.Point + Color1 color.NRGBA + Stop2 f32.Point + Color2 color.NRGBA +} + +// PaintOp fills fills the current clip area with the current brush. +type PaintOp struct { +} + +// NewImageOp creates an ImageOp backed by src. See +// realy.lol/gio/io/system.FrameEvent for a description of when data +// referenced by operations is safe to re-use. +// +// NewImageOp assumes the backing image is immutable, and may cache a +// copy of its contents in a GPU-friendly way. Create new ImageOps to +// ensure that changes to an image is reflected in the display of +// it. +func NewImageOp(src image.Image) ImageOp { + switch src := src.(type) { + case *image.Uniform: + col := color.NRGBAModel.Convert(src.C).(color.NRGBA) + return ImageOp{ + uniform: true, + color: col, + } + case *image.RGBA: + bounds := src.Bounds() + if bounds.Min == (image.Point{}) && src.Stride == bounds.Dx()*4 { + return ImageOp{ + src: src, + handle: new(int), + } + } + } + + sz := src.Bounds().Size() + // Copy the image into a GPU friendly format. + dst := image.NewRGBA(image.Rectangle{ + Max: sz, + }) + draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src) + return ImageOp{ + src: dst, + handle: new(int), + } +} + +func (i ImageOp) Size() image.Point { + if i.src == nil { + return image.Point{} + } + return i.src.Bounds().Size() +} + +func (i ImageOp) Add(o *op.Ops) { + if i.uniform { + ColorOp{ + Color: i.color, + }.Add(o) + return + } + data := o.Write2(opconst.TypeImageLen, i.src, i.handle) + data[0] = byte(opconst.TypeImage) +} + +func (c ColorOp) Add(o *op.Ops) { + data := o.Write(opconst.TypeColorLen) + data[0] = byte(opconst.TypeColor) + data[1] = c.Color.R + data[2] = c.Color.G + data[3] = c.Color.B + data[4] = c.Color.A +} + +func (c LinearGradientOp) Add(o *op.Ops) { + data := o.Write(opconst.TypeLinearGradientLen) + data[0] = byte(opconst.TypeLinearGradient) + + bo := binary.LittleEndian + bo.PutUint32(data[1:], math.Float32bits(c.Stop1.X)) + bo.PutUint32(data[5:], math.Float32bits(c.Stop1.Y)) + bo.PutUint32(data[9:], math.Float32bits(c.Stop2.X)) + bo.PutUint32(data[13:], math.Float32bits(c.Stop2.Y)) + + data[17+0] = c.Color1.R + data[17+1] = c.Color1.G + data[17+2] = c.Color1.B + data[17+3] = c.Color1.A + data[21+0] = c.Color2.R + data[21+1] = c.Color2.G + data[21+2] = c.Color2.B + data[21+3] = c.Color2.A +} + +func (d PaintOp) Add(o *op.Ops) { + data := o.Write(opconst.TypePaintLen) + data[0] = byte(opconst.TypePaint) +} + +// FillShape fills the clip shape with a color. +func FillShape(ops *op.Ops, c color.NRGBA, shape clip.Op) { + defer op.Save(ops).Load() + shape.Add(ops) + Fill(ops, c) +} + +// Fill paints an infinitely large plane with the provided color. It +// is intended to be used with a clip.Op already in place to limit +// the painted area. Use FillShape unless you need to paint several +// times within the same clip.Op. +func Fill(ops *op.Ops, c color.NRGBA) { + defer op.Save(ops).Load() + ColorOp{Color: c}.Add(ops) + PaintOp{}.Add(ops) +} diff --git a/gio/text/lru.go b/gio/text/lru.go new file mode 100644 index 0000000..4f1c033 --- /dev/null +++ b/gio/text/lru.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "golang.org/x/image/math/fixed" + + "realy.lol/gio/op" +) + +type layoutCache struct { + m map[layoutKey]*layoutElem + head, tail *layoutElem +} + +type pathCache struct { + m map[pathKey]*path + head, tail *path +} + +type layoutElem struct { + next, prev *layoutElem + key layoutKey + layout []Line +} + +type path struct { + next, prev *path + key pathKey + val op.CallOp +} + +type layoutKey struct { + ppem fixed.Int26_6 + maxWidth int + str string +} + +type pathKey struct { + ppem fixed.Int26_6 + str string +} + +const maxSize = 1000 + +func (l *layoutCache) Get(k layoutKey) ([]Line, bool) { + if lt, ok := l.m[k]; ok { + l.remove(lt) + l.insert(lt) + return lt.layout, true + } + return nil, false +} + +func (l *layoutCache) Put(k layoutKey, lt []Line) { + if l.m == nil { + l.m = make(map[layoutKey]*layoutElem) + l.head = new(layoutElem) + l.tail = new(layoutElem) + l.head.prev = l.tail + l.tail.next = l.head + } + val := &layoutElem{key: k, layout: lt} + l.m[k] = val + l.insert(val) + if len(l.m) > maxSize { + oldest := l.tail.next + l.remove(oldest) + delete(l.m, oldest.key) + } +} + +func (l *layoutCache) remove(lt *layoutElem) { + lt.next.prev = lt.prev + lt.prev.next = lt.next +} + +func (l *layoutCache) insert(lt *layoutElem) { + lt.next = l.head + lt.prev = l.head.prev + lt.prev.next = lt + lt.next.prev = lt +} + +func (c *pathCache) Get(k pathKey) (op.CallOp, bool) { + if v, ok := c.m[k]; ok { + c.remove(v) + c.insert(v) + return v.val, true + } + return op.CallOp{}, false +} + +func (c *pathCache) Put(k pathKey, v op.CallOp) { + if c.m == nil { + c.m = make(map[pathKey]*path) + c.head = new(path) + c.tail = new(path) + c.head.prev = c.tail + c.tail.next = c.head + } + val := &path{key: k, val: v} + c.m[k] = val + c.insert(val) + if len(c.m) > maxSize { + oldest := c.tail.next + c.remove(oldest) + delete(c.m, oldest.key) + } +} + +func (c *pathCache) remove(v *path) { + v.next.prev = v.prev + v.prev.next = v.next +} + +func (c *pathCache) insert(v *path) { + v.next = c.head + v.prev = c.head.prev + v.prev.next = v + v.next.prev = v +} diff --git a/gio/text/lru_test.go b/gio/text/lru_test.go new file mode 100644 index 0000000..fb8d8d1 --- /dev/null +++ b/gio/text/lru_test.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "strconv" + "testing" + + "realy.lol/gio/op" +) + +func TestLayoutLRU(t *testing.T) { + c := new(layoutCache) + put := func(i int) { + c.Put(layoutKey{str: strconv.Itoa(i)}, nil) + } + get := func(i int) bool { + _, ok := c.Get(layoutKey{str: strconv.Itoa(i)}) + return ok + } + testLRU(t, put, get) +} + +func TestPathLRU(t *testing.T) { + c := new(pathCache) + put := func(i int) { + c.Put(pathKey{str: strconv.Itoa(i)}, op.CallOp{}) + } + get := func(i int) bool { + _, ok := c.Get(pathKey{str: strconv.Itoa(i)}) + return ok + } + testLRU(t, put, get) +} + +func testLRU(t *testing.T, put func(i int), get func(i int) bool) { + for i := 0; i < maxSize; i++ { + put(i) + } + for i := 0; i < maxSize; i++ { + if !get(i) { + t.Fatalf("key %d was evicted", i) + } + } + put(maxSize) + for i := 1; i < maxSize+1; i++ { + if !get(i) { + t.Fatalf("key %d was evicted", i) + } + } + if i := 0; get(i) { + t.Fatalf("key %d was not evicted", i) + } +} diff --git a/gio/text/shaper.go b/gio/text/shaper.go new file mode 100644 index 0000000..88f1fbf --- /dev/null +++ b/gio/text/shaper.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "io" + "strings" + + "golang.org/x/image/math/fixed" + + "realy.lol/gio/op" +) + +// Shaper implements layout and shaping of text. +type Shaper interface { + // Layout a text according to a set of options. + Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, + error) + // LayoutString is Layout for strings. + LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line + // Shape a line of text and return a clipping operation for its outline. + Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp +} + +// A FontFace is a Font and a matching Face. +type FontFace struct { + Font Font + Face Face +} + +// Cache implements cached layout and shaping of text from a set of +// registered fonts. +// +// If a font matches no registered shape, Cache falls back to the +// first registered face. +// +// The LayoutString and ShapeString results are cached and re-used if +// possible. +type Cache struct { + def Typeface + faces map[Font]*faceCache +} + +type faceCache struct { + face Face + layoutCache layoutCache + pathCache pathCache +} + +func (c *Cache) lookup(font Font) *faceCache { + f := c.faceForStyle(font) + if f == nil { + font.Typeface = c.def + f = c.faceForStyle(font) + } + return f +} + +func (c *Cache) faceForStyle(font Font) *faceCache { + tf := c.faces[font] + if tf == nil { + font := font + font.Weight = Normal + tf = c.faces[font] + } + if tf == nil { + font := font + font.Style = Regular + tf = c.faces[font] + } + if tf == nil { + font := font + font.Style = Regular + font.Weight = Normal + tf = c.faces[font] + } + return tf +} + +func NewCache(collection []FontFace) *Cache { + c := &Cache{ + faces: make(map[Font]*faceCache), + } + for i, ff := range collection { + if i == 0 { + c.def = ff.Font.Typeface + } + c.faces[ff.Font] = &faceCache{face: ff.Face} + } + return c +} + +// Layout implements the Shaper interface. +func (s *Cache) Layout(font Font, size fixed.Int26_6, maxWidth int, + txt io.Reader) ([]Line, error) { + cache := s.lookup(font) + return cache.face.Layout(size, maxWidth, txt) +} + +// LayoutString is a caching implementation of the Shaper interface. +func (s *Cache) LayoutString(font Font, size fixed.Int26_6, maxWidth int, + str string) []Line { + cache := s.lookup(font) + return cache.layout(size, maxWidth, str) +} + +// Shape is a caching implementation of the Shaper interface. Shape assumes that the layout +// argument is unchanged from a call to Layout or LayoutString. +func (s *Cache) Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp { + cache := s.lookup(font) + return cache.shape(size, layout) +} + +func (f *faceCache) layout(ppem fixed.Int26_6, maxWidth int, + str string) []Line { + if f == nil { + return nil + } + lk := layoutKey{ + ppem: ppem, + maxWidth: maxWidth, + str: str, + } + if l, ok := f.layoutCache.Get(lk); ok { + return l + } + l, _ := f.face.Layout(ppem, maxWidth, strings.NewReader(str)) + f.layoutCache.Put(lk, l) + return l +} + +func (f *faceCache) shape(ppem fixed.Int26_6, layout Layout) op.CallOp { + if f == nil { + return op.CallOp{} + } + pk := pathKey{ + ppem: ppem, + str: layout.Text, + } + if clip, ok := f.pathCache.Get(pk); ok { + return clip + } + clip := f.face.Shape(ppem, layout) + f.pathCache.Put(pk, clip) + return clip +} diff --git a/gio/text/text.go b/gio/text/text.go new file mode 100644 index 0000000..b50cc8a --- /dev/null +++ b/gio/text/text.go @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "io" + + "golang.org/x/image/math/fixed" + + "realy.lol/gio/op" +) + +// A Line contains the measurements of a line of text. +type Line struct { + Layout Layout + // Width is the width of the line. + Width fixed.Int26_6 + // Ascent is the height above the baseline. + Ascent fixed.Int26_6 + // Descent is the height below the baseline, including + // the line gap. + Descent fixed.Int26_6 + // Bounds is the visible bounds of the line. + Bounds fixed.Rectangle26_6 +} + +type Layout struct { + Text string + Advances []fixed.Int26_6 +} + +// Style is the font style. +type Style int + +// Weight is a font weight, in CSS units subtracted 400 so the zero value +// is normal text weight. +type Weight int + +// Font specify a particular typeface variant, style and weight. +type Font struct { + Typeface Typeface + Variant Variant + Style Style + // Weight is the text weight. If zero, Normal is used instead. + Weight Weight +} + +// Face implements text layout and shaping for a particular font. All +// methods must be safe for concurrent use. +type Face interface { + Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) + Shape(ppem fixed.Int26_6, str Layout) op.CallOp +} + +// Typeface identifies a particular typeface design. The empty +// string denotes the default typeface. +type Typeface string + +// Variant denotes a typeface variant such as "Mono" or "Smallcaps". +type Variant string + +type Alignment uint8 + +const ( + Start Alignment = iota + End + Middle +) + +const ( + Regular Style = iota + Italic +) + +const ( + Normal Weight = 400 - 400 + Medium Weight = 500 - 400 + Bold Weight = 600 - 400 +) + +func (a Alignment) String() string { + switch a { + case Start: + return "Start" + case End: + return "End" + case Middle: + return "Middle" + default: + panic("unreachable") + } +} diff --git a/gio/unit/unit.go b/gio/unit/unit.go new file mode 100644 index 0000000..fd2245c --- /dev/null +++ b/gio/unit/unit.go @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* + +Package unit implements device independent units and values. + +A Value is a value with a Unit attached. + +Device independent pixel, or dp, is the unit for sizes independent of +the underlying display device. + +Scaled pixels, or sp, is the unit for text sizes. An sp is like dp with +text scaling applied. + +Finally, pixels, or px, is the unit for display dependent pixels. Their +size vary between platforms and displays. + +To maintain a constant visual size across platforms and displays, always +use dps or sps to define user interfaces. Only use pixels for derived +values. + +*/ +package unit + +import ( + "fmt" + "math" +) + +// Value is a value with a unit. +type Value struct { + V float32 + U Unit +} + +// Unit represents a unit for a Value. +type Unit uint8 + +// Metric converts Values to device-dependent pixels, px. The zero +// value represents a 1-to-1 scale from dp, sp to pixels. +type Metric struct { + // PxPerDp is the device-dependent pixels per dp. + PxPerDp float32 + // PxPerSp is the device-dependent pixels per sp. + PxPerSp float32 +} + +const ( + // UnitPx represent device pixels in the resolution of + // the underlying display. + UnitPx Unit = iota + // UnitDp represents device independent pixels. 1 dp will + // have the same apparent size across platforms and + // display resolutions. + UnitDp + // UnitSp is like UnitDp but for font sizes. + UnitSp +) + +// Px returns the Value for v device pixels. +func Px(v float32) Value { + return Value{V: v, U: UnitPx} +} + +// Dp returns the Value for v device independent +// pixels. +func Dp(v float32) Value { + return Value{V: v, U: UnitDp} +} + +// Sp returns the Value for v scaled dps. +func Sp(v float32) Value { + return Value{V: v, U: UnitSp} +} + +// Scale returns the value scaled by s. +func (v Value) Scale(s float32) Value { + v.V *= s + return v +} + +func (v Value) String() string { + return fmt.Sprintf("%g%s", v.V, v.U) +} + +func (u Unit) String() string { + switch u { + case UnitPx: + return "px" + case UnitDp: + return "dp" + case UnitSp: + return "sp" + default: + panic("unknown unit") + } +} + +// Add a list of Values. +func Add(c Metric, values ...Value) Value { + var sum Value + for _, v := range values { + sum, v = compatible(c, sum, v) + sum.V += v.V + } + return sum +} + +// Max returns the maximum of a list of Values. +func Max(c Metric, values ...Value) Value { + var max Value + for _, v := range values { + max, v = compatible(c, max, v) + if v.V > max.V { + max.V = v.V + } + } + return max +} + +func (c Metric) Px(v Value) int { + var r float32 + switch v.U { + case UnitPx: + r = v.V + case UnitDp: + s := c.PxPerDp + if s == 0 { + s = 1 + } + r = s * v.V + case UnitSp: + s := c.PxPerSp + if s == 0 { + s = 1 + } + r = s * v.V + default: + panic("unknown unit") + } + return int(math.Round(float64(r))) +} + +func compatible(c Metric, v1, v2 Value) (Value, Value) { + if v1.U == v2.U { + return v1, v2 + } + if v1.V == 0 { + v1.U = v2.U + return v1, v2 + } + if v2.V == 0 { + v2.U = v1.U + return v1, v2 + } + return Px(float32(c.Px(v1))), Px(float32(c.Px(v2))) +} diff --git a/gio/widget/bool.go b/gio/widget/bool.go new file mode 100644 index 0000000..feb4a8a --- /dev/null +++ b/gio/widget/bool.go @@ -0,0 +1,44 @@ +package widget + +import ( + "realy.lol/gio/layout" +) + +type Bool struct { + Value bool + + clk Clickable + + changed bool +} + +// Changed reports whether Value has changed since the last +// call to Changed. +func (b *Bool) Changed() bool { + changed := b.changed + b.changed = false + return changed +} + +// Hovered returns whether pointer is over the element. +func (b *Bool) Hovered() bool { + return b.clk.Hovered() +} + +// Pressed returns whether pointer is pressing the element. +func (b *Bool) Pressed() bool { + return b.clk.Pressed() +} + +func (b *Bool) History() []Press { + return b.clk.History() +} + +func (b *Bool) Layout(gtx layout.Context) layout.Dimensions { + dims := b.clk.Layout(gtx) + for b.clk.Clicked() { + b.Value = !b.Value + b.changed = true + } + return dims +} diff --git a/gio/widget/border.go b/gio/widget/border.go new file mode 100644 index 0000000..e4bed6a --- /dev/null +++ b/gio/widget/border.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image/color" + + "realy.lol/gio/f32" + "realy.lol/gio/layout" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" +) + +// Border lays out a widget and draws a border inside it. +type Border struct { + Color color.NRGBA + CornerRadius unit.Value + Width unit.Value +} + +func (b Border) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { + dims := w(gtx) + sz := layout.FPt(dims.Size) + + rr := float32(gtx.Px(b.CornerRadius)) + width := float32(gtx.Px(b.Width)) + sz.X -= width + sz.Y -= width + + r := f32.Rectangle{Max: sz} + r = r.Add(f32.Point{X: width * 0.5, Y: width * 0.5}) + + paint.FillShape(gtx.Ops, + b.Color, + clip.Stroke{ + Path: clip.UniformRRect(r, rr).Path(gtx.Ops), + Style: clip.StrokeStyle{Width: width}, + }.Op(), + ) + + return dims +} diff --git a/gio/widget/buffer.go b/gio/widget/buffer.go new file mode 100644 index 0000000..e658d56 --- /dev/null +++ b/gio/widget/buffer.go @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "io" + "strings" + "unicode/utf8" +) + +// editBuffer implements a gap buffer for text editing. +type editBuffer struct { + // pos is the byte position for Read and ReadRune. + pos int + + // The gap start and end in bytes. + gapstart, gapend int + text []byte + + // changed tracks whether the buffer content + // has changed since the last call to Changed. + changed bool +} + +const minSpace = 5 + +func (e *editBuffer) Changed() bool { + c := e.changed + e.changed = false + return c +} + +func (e *editBuffer) deleteRunes(caret, runes int) int { + e.moveGap(caret, 0) + for ; runes < 0 && e.gapstart > 0; runes++ { + _, s := utf8.DecodeLastRune(e.text[:e.gapstart]) + e.gapstart -= s + caret -= s + e.changed = e.changed || s > 0 + } + for ; runes > 0 && e.gapend < len(e.text); runes-- { + _, s := utf8.DecodeRune(e.text[e.gapend:]) + e.gapend += s + e.changed = e.changed || s > 0 + } + return caret +} + +// moveGap moves the gap to the caret position. After returning, +// the gap is guaranteed to be at least space bytes long. +func (e *editBuffer) moveGap(caret, space int) { + if e.gapLen() < space { + if space < minSpace { + space = minSpace + } + txt := make([]byte, e.len()+space) + // Expand to capacity. + txt = txt[:cap(txt)] + gaplen := len(txt) - e.len() + if caret > e.gapstart { + copy(txt, e.text[:e.gapstart]) + copy(txt[caret+gaplen:], e.text[caret:]) + copy(txt[e.gapstart:], e.text[e.gapend:caret+e.gapLen()]) + } else { + copy(txt, e.text[:caret]) + copy(txt[e.gapstart+gaplen:], e.text[e.gapend:]) + copy(txt[caret+gaplen:], e.text[caret:e.gapstart]) + } + e.text = txt + e.gapstart = caret + e.gapend = e.gapstart + gaplen + } else { + if caret > e.gapstart { + copy(e.text[e.gapstart:], e.text[e.gapend:caret+e.gapLen()]) + } else { + copy(e.text[caret+e.gapLen():], e.text[caret:e.gapstart]) + } + l := e.gapLen() + e.gapstart = caret + e.gapend = e.gapstart + l + } +} + +func (e *editBuffer) len() int { + return len(e.text) - e.gapLen() +} + +func (e *editBuffer) gapLen() int { + return e.gapend - e.gapstart +} + +func (e *editBuffer) Reset() { + e.Seek(0, io.SeekStart) +} + +// Seek implements io.Seeker +func (e *editBuffer) Seek(offset int64, whence int) (ret int64, err error) { + switch whence { + case io.SeekStart: + e.pos = int(offset) + case io.SeekCurrent: + e.pos += int(offset) + case io.SeekEnd: + e.pos = e.len() - int(offset) + } + if e.pos < 0 { + e.pos = 0 + } else if e.pos > e.len() { + e.pos = e.len() + } + return int64(e.pos), nil +} + +func (e *editBuffer) Read(p []byte) (int, error) { + if e.pos == e.len() { + return 0, io.EOF + } + var total int + if e.pos < e.gapstart { + n := copy(p, e.text[e.pos:e.gapstart]) + p = p[n:] + total += n + e.pos += n + } + if e.pos >= e.gapstart { + n := copy(p, e.text[e.pos+e.gapLen():]) + total += n + e.pos += n + } + if e.pos > e.len() { + panic("hey!") + } + return total, nil +} + +func (e *editBuffer) ReadRune() (rune, int, error) { + if e.pos == e.len() { + return 0, 0, io.EOF + } + r, s := e.runeAt(e.pos) + e.pos += s + return r, s, nil +} + +func (e *editBuffer) String() string { + var b strings.Builder + b.Grow(e.len()) + b.Write(e.text[:e.gapstart]) + b.Write(e.text[e.gapend:]) + return b.String() +} + +func (e *editBuffer) prepend(caret int, s string) { + e.moveGap(caret, len(s)) + copy(e.text[caret:], s) + e.gapstart += len(s) + e.changed = e.changed || len(s) > 0 +} + +func (e *editBuffer) runeBefore(idx int) (rune, int) { + if idx > e.gapstart { + idx += e.gapLen() + } + return utf8.DecodeLastRune(e.text[:idx]) +} + +func (e *editBuffer) runeAt(idx int) (rune, int) { + if idx >= e.gapstart { + idx += e.gapLen() + } + return utf8.DecodeRune(e.text[idx:]) +} diff --git a/gio/widget/button.go b/gio/widget/button.go new file mode 100644 index 0000000..2c23c5d --- /dev/null +++ b/gio/widget/button.go @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + "time" + + "realy.lol/gio/f32" + "realy.lol/gio/gesture" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" +) + +// Clickable represents a clickable area. +type Clickable struct { + click gesture.Click + clicks []Click + // prevClicks is the index into clicks that marks the clicks + // from the most recent Layout call. prevClicks is used to keep + // clicks bounded. + prevClicks int + history []Press +} + +// Click represents a click. +type Click struct { + Modifiers key.Modifiers + NumClicks int +} + +// Press represents a past pointer press. +type Press struct { + // Position of the press. + Position f32.Point + // Start is when the press began. + Start time.Time + // End is when the press was ended by a release or cancel. + // A zero End means it hasn't ended yet. + End time.Time + // Cancelled is true for cancelled presses. + Cancelled bool +} + +// Click executes a simple programmatic click +func (b *Clickable) Click() { + b.clicks = append(b.clicks, Click{ + Modifiers: 0, + NumClicks: 1, + }) +} + +// Clicked reports whether there are pending clicks as would be +// reported by Clicks. If so, Clicked removes the earliest click. +func (b *Clickable) Clicked() bool { + if len(b.clicks) == 0 { + return false + } + n := copy(b.clicks, b.clicks[1:]) + b.clicks = b.clicks[:n] + if b.prevClicks > 0 { + b.prevClicks-- + } + return true +} + +// Hovered returns whether pointer is over the element. +func (b *Clickable) Hovered() bool { + return b.click.Hovered() +} + +// Pressed returns whether pointer is pressing the element. +func (b *Clickable) Pressed() bool { + return b.click.Pressed() +} + +// Clicks returns and clear the clicks since the last call to Clicks. +func (b *Clickable) Clicks() []Click { + clicks := b.clicks + b.clicks = nil + b.prevClicks = 0 + return clicks +} + +// History is the past pointer presses useful for drawing markers. +// History is retained for a short duration (about a second). +func (b *Clickable) History() []Press { + return b.history +} + +// Layout and update the button state +func (b *Clickable) Layout(gtx layout.Context) layout.Dimensions { + b.update(gtx) + stack := op.Save(gtx.Ops) + pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops) + b.click.Add(gtx.Ops) + stack.Load() + for len(b.history) > 0 { + c := b.history[0] + if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second { + break + } + n := copy(b.history, b.history[1:]) + b.history = b.history[:n] + } + return layout.Dimensions{Size: gtx.Constraints.Min} +} + +// update the button state by processing events. +func (b *Clickable) update(gtx layout.Context) { + // Flush clicks from before the last update. + n := copy(b.clicks, b.clicks[b.prevClicks:]) + b.clicks = b.clicks[:n] + b.prevClicks = n + + for _, e := range b.click.Events(gtx) { + switch e.Type { + case gesture.TypeClick: + b.clicks = append(b.clicks, Click{ + Modifiers: e.Modifiers, + NumClicks: e.NumClicks, + }) + if l := len(b.history); l > 0 { + b.history[l-1].End = gtx.Now + } + case gesture.TypeCancel: + for i := range b.history { + b.history[i].Cancelled = true + if b.history[i].End.IsZero() { + b.history[i].End = gtx.Now + } + } + case gesture.TypePress: + b.history = append(b.history, Press{ + Position: e.Position, + Start: gtx.Now, + }) + } + } +} diff --git a/gio/widget/doc.go b/gio/widget/doc.go new file mode 100644 index 0000000..df4e55f --- /dev/null +++ b/gio/widget/doc.go @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package widget implements state tracking and event handling of +// common user interface controls. To draw widgets, use a theme +// packages such as package realy.lol/gio/widget/material. +package widget diff --git a/gio/widget/editor.go b/gio/widget/editor.go new file mode 100644 index 0000000..e44f54f --- /dev/null +++ b/gio/widget/editor.go @@ -0,0 +1,1328 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "bufio" + "bytes" + "image" + "io" + "math" + "runtime" + "sort" + "strings" + "time" + "unicode" + "unicode/utf8" + + "realy.lol/gio/f32" + "realy.lol/gio/gesture" + "realy.lol/gio/io/clipboard" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" + + "golang.org/x/image/math/fixed" +) + +// Editor implements an editable and scrollable text area. +type Editor struct { + Alignment text.Alignment + // SingleLine force the text to stay on a single line. + // SingleLine also sets the scrolling direction to + // horizontal. + SingleLine bool + // Submit enabled translation of carriage return keys to SubmitEvents. + // If not enabled, carriage returns are inserted as newlines in the text. + Submit bool + // Mask replaces the visual display of each rune in the contents with the given rune. + // Newline characters are not masked. When non-zero, the unmasked contents + // are accessed by Len, Text, and SetText. + Mask rune + + eventKey int + font text.Font + shaper text.Shaper + textSize fixed.Int26_6 + blinkStart time.Time + focused bool + rr editBuffer + maskReader maskReader + lastMask rune + maxWidth int + viewSize image.Point + valid bool + lines []text.Line + shapes []line + dims layout.Dimensions + requestFocus bool + + caret struct { + on bool + scroll bool + // start is the current caret position, and also the start position of + // selected text. end is the end positon of selected text. If start.ofs + // == end.ofs, then there's no selection. Note that it's possible (and + // common) that the caret (start) is after the end, e.g. after + // Shift-DownArrow. + start combinedPos + end combinedPos + } + + dragging bool + dragger gesture.Drag + scroller gesture.Scroll + scrollOff image.Point + + clicker gesture.Click + + // events is the list of events not yet processed. + events []EditorEvent + // prevEvents is the number of events from the previous frame. + prevEvents int +} + +type maskReader struct { + // rr is the underlying reader. + rr io.RuneReader + maskBuf [utf8.UTFMax]byte + // mask is the utf-8 encoded mask rune. + mask []byte + // overflow contains excess mask bytes left over after the last Read call. + overflow []byte +} + +// combinedPos is a point in the editor. +type combinedPos struct { + // editorBuffer offset. The other three fields are based off of this one. + ofs int + + // lineCol.Y = line (offset into Editor.lines), and X = col (offset into + // Editor.lines[Y]) + lineCol screenPos + + // Pixel coordinates + x fixed.Int26_6 + y int + + // xoff is the offset to the current position when moving between lines. + xoff fixed.Int26_6 +} + +type selectionAction int + +const ( + selectionExtend selectionAction = iota + selectionClear +) + +func (m *maskReader) Reset(r io.RuneReader, mr rune) { + m.rr = r + n := utf8.EncodeRune(m.maskBuf[:], mr) + m.mask = m.maskBuf[:n] +} + +// Read reads from the underlying reader and replaces every +// rune with the mask rune. +func (m *maskReader) Read(b []byte) (n int, err error) { + for len(b) > 0 { + var replacement []byte + if len(m.overflow) > 0 { + replacement = m.overflow + } else { + var r rune + r, _, err = m.rr.ReadRune() + if err != nil { + break + } + if r == '\n' { + replacement = []byte{'\n'} + } else { + replacement = m.mask + } + } + nn := copy(b, replacement) + m.overflow = replacement[nn:] + n += nn + b = b[nn:] + } + return n, err +} + +type EditorEvent interface { + isEditorEvent() +} + +// A ChangeEvent is generated for every user change to the text. +type ChangeEvent struct{} + +// A SubmitEvent is generated when Submit is set +// and a carriage return key is pressed. +type SubmitEvent struct { + Text string +} + +// A SelectEvent is generated when the user selects some text, or changes the +// selection (e.g. with a shift-click), including if they remove the +// selection. The selected text is not part of the event, on the theory that +// it could be a relatively expensive operation (for a large editor), most +// applications won't actually care about it, and those that do can call +// Editor.SelectedText() (which can be empty). +type SelectEvent struct{} + +type line struct { + offset image.Point + clip op.CallOp + selected bool + selectionYOffs int + selectionSize image.Point +} + +const ( + blinksPerSecond = 1 + maxBlinkDuration = 10 * time.Second +) + +// Events returns available editor events. +func (e *Editor) Events() []EditorEvent { + events := e.events + e.events = nil + e.prevEvents = 0 + return events +} + +func (e *Editor) processEvents(gtx layout.Context) { + // Flush events from before the previous Layout. + n := copy(e.events, e.events[e.prevEvents:]) + e.events = e.events[:n] + e.prevEvents = n + + if e.shaper == nil { + // Can't process events without a shaper. + return + } + oldStart, oldLen := min(e.caret.start.ofs, + e.caret.end.ofs), e.SelectionLen() + e.processPointer(gtx) + e.processKey(gtx) + // Queue a SelectEvent if the selection changed, including if it went away. + if newStart, newLen := min(e.caret.start.ofs, + e.caret.end.ofs), e.SelectionLen(); oldStart != newStart || oldLen != newLen { + e.events = append(e.events, SelectEvent{}) + } +} + +func (e *Editor) makeValid(positions ...*combinedPos) { + if e.valid { + return + } + e.lines, e.dims = e.layoutText(e.shaper) + e.makeValidCaret(positions...) + e.valid = true +} + +func (e *Editor) processPointer(gtx layout.Context) { + sbounds := e.scrollBounds() + var smin, smax int + var axis gesture.Axis + if e.SingleLine { + axis = gesture.Horizontal + smin, smax = sbounds.Min.X, sbounds.Max.X + } else { + axis = gesture.Vertical + smin, smax = sbounds.Min.Y, sbounds.Max.Y + } + sdist := e.scroller.Scroll(gtx.Metric, gtx, gtx.Now, axis) + var soff int + if e.SingleLine { + e.scrollRel(sdist, 0) + soff = e.scrollOff.X + } else { + e.scrollRel(0, sdist) + soff = e.scrollOff.Y + } + for _, evt := range e.clickDragEvents(gtx) { + switch evt := evt.(type) { + case gesture.ClickEvent: + switch { + case evt.Type == gesture.TypePress && evt.Source == pointer.Mouse, + evt.Type == gesture.TypeClick: + prevCaretPos := e.caret.start + e.blinkStart = gtx.Now + e.moveCoord(image.Point{ + X: int(math.Round(float64(evt.Position.X))), + Y: int(math.Round(float64(evt.Position.Y))), + }) + e.requestFocus = true + if e.scroller.State() != gesture.StateFlinging { + e.caret.scroll = true + } + + if evt.Modifiers == key.ModShift { + // If they clicked closer to the end, then change the end to + // where the caret used to be (effectively swapping start & end). + if abs(e.caret.end.ofs-e.caret.start.ofs) < abs(e.caret.start.ofs-prevCaretPos.ofs) { + e.caret.end = prevCaretPos + } + } else { + e.ClearSelection() + } + e.dragging = true + + // Process a double-click. + if evt.NumClicks == 2 { + e.moveWord(-1, selectionClear) + e.moveWord(1, selectionExtend) + e.dragging = false + } + } + case pointer.Event: + release := false + switch { + case evt.Type == pointer.Release && evt.Source == pointer.Mouse: + release = true + fallthrough + case evt.Type == pointer.Drag && evt.Source == pointer.Mouse: + if e.dragging { + e.blinkStart = gtx.Now + e.moveCoord(image.Point{ + X: int(math.Round(float64(evt.Position.X))), + Y: int(math.Round(float64(evt.Position.Y))), + }) + e.caret.scroll = true + + if release { + e.dragging = false + } + } + } + } + } + + if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) { + e.scroller.Stop() + } +} + +func (e *Editor) clickDragEvents(gtx layout.Context) []event.Event { + var combinedEvents []event.Event + for _, evt := range e.clicker.Events(gtx) { + combinedEvents = append(combinedEvents, evt) + } + for _, evt := range e.dragger.Events(gtx.Metric, gtx, gesture.Both) { + combinedEvents = append(combinedEvents, evt) + } + return combinedEvents +} + +func (e *Editor) processKey(gtx layout.Context) { + if e.rr.Changed() { + e.events = append(e.events, ChangeEvent{}) + } + for _, ke := range gtx.Events(&e.eventKey) { + e.blinkStart = gtx.Now + switch ke := ke.(type) { + case key.FocusEvent: + e.focused = ke.Focus + case key.Event: + if !e.focused || ke.State != key.Press { + break + } + if e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) { + if !ke.Modifiers.Contain(key.ModShift) { + e.events = append(e.events, SubmitEvent{ + Text: e.Text(), + }) + continue + } + } + if e.command(gtx, ke) { + e.caret.scroll = true + e.scroller.Stop() + } + case key.EditEvent: + e.caret.scroll = true + e.scroller.Stop() + e.append(ke.Text) + // Complete a paste event, initiated by Shortcut-V in Editor.command(). + case clipboard.Event: + e.caret.scroll = true + e.scroller.Stop() + e.append(ke.Text) + } + if e.rr.Changed() { + e.events = append(e.events, ChangeEvent{}) + } + } +} + +func (e *Editor) moveLines(distance int, selAct selectionAction) { + e.caret.start = e.movePosToLine(e.caret.start, + e.caret.start.x+e.caret.start.xoff, e.caret.start.lineCol.Y+distance) + e.updateSelection(selAct) +} + +func (e *Editor) command(gtx layout.Context, k key.Event) bool { + modSkip := key.ModCtrl + if runtime.GOOS == "darwin" { + modSkip = key.ModAlt + } + moveByWord := k.Modifiers.Contain(modSkip) + selAct := selectionClear + if k.Modifiers.Contain(key.ModShift) { + selAct = selectionExtend + } + switch k.Name { + case key.NameReturn, key.NameEnter: + e.append("\n") + case key.NameDeleteBackward: + if moveByWord { + e.deleteWord(-1) + } else { + e.Delete(-1) + } + case key.NameDeleteForward: + if moveByWord { + e.deleteWord(1) + } else { + e.Delete(1) + } + case key.NameUpArrow: + e.moveLines(-1, selAct) + case key.NameDownArrow: + e.moveLines(+1, selAct) + case key.NameLeftArrow: + if moveByWord { + e.moveWord(-1, selAct) + } else { + if selAct == selectionClear { + e.ClearSelection() + } + e.MoveCaret(-1, -1*int(selAct)) + } + case key.NameRightArrow: + if moveByWord { + e.moveWord(1, selAct) + } else { + if selAct == selectionClear { + e.ClearSelection() + } + e.MoveCaret(1, int(selAct)) + } + case key.NamePageUp: + e.movePages(-1, selAct) + case key.NamePageDown: + e.movePages(+1, selAct) + case key.NameHome: + e.moveStart(selAct) + case key.NameEnd: + e.moveEnd(selAct) + // Initiate a paste operation, by requesting the clipboard contents; other + // half is in Editor.processKey() under clipboard.Event. + case "V": + if k.Modifiers != key.ModShortcut { + return false + } + clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops) + // Copy or Cut selection -- ignored if nothing selected. + case "C", "X": + if k.Modifiers != key.ModShortcut { + return false + } + if text := e.SelectedText(); text != "" { + clipboard.WriteOp{Text: text}.Add(gtx.Ops) + if k.Name == "X" { + e.Delete(1) + } + } + // Select all + case "A": + if k.Modifiers != key.ModShortcut { + return false + } + e.caret.end, e.caret.start = e.offsetToScreenPos2(0, e.Len()) + default: + return false + } + return true +} + +// Focus requests the input focus for the Editor. +func (e *Editor) Focus() { + e.requestFocus = true +} + +// Focused returns whether the editor is focused or not. +func (e *Editor) Focused() bool { + return e.focused +} + +// Layout lays out the editor. +func (e *Editor) Layout(gtx layout.Context, sh text.Shaper, font text.Font, + size unit.Value) layout.Dimensions { + textSize := fixed.I(gtx.Px(size)) + if e.font != font || e.textSize != textSize { + e.invalidate() + e.font = font + e.textSize = textSize + } + maxWidth := gtx.Constraints.Max.X + if e.SingleLine { + maxWidth = inf + } + if maxWidth != e.maxWidth { + e.maxWidth = maxWidth + e.invalidate() + } + if sh != e.shaper { + e.shaper = sh + e.invalidate() + } + if e.Mask != e.lastMask { + e.lastMask = e.Mask + e.invalidate() + } + + e.makeValid() + e.processEvents(gtx) + e.makeValid() + + if viewSize := gtx.Constraints.Constrain(e.dims.Size); viewSize != e.viewSize { + e.viewSize = viewSize + e.invalidate() + } + e.makeValid() + + return e.layout(gtx) +} + +func (e *Editor) layout(gtx layout.Context) layout.Dimensions { + // Adjust scrolling for new viewport and layout. + e.scrollRel(0, 0) + + if e.caret.scroll { + e.caret.scroll = false + e.scrollToCaret() + } + + off := image.Point{ + X: -e.scrollOff.X, + Y: -e.scrollOff.Y, + } + clip := textPadding(e.lines) + clip.Max = clip.Max.Add(e.viewSize) + startSel, endSel := sortPoints(e.caret.start.lineCol, e.caret.end.lineCol) + it := segmentIterator{ + startSel: startSel, + endSel: endSel, + Lines: e.lines, + Clip: clip, + Alignment: e.Alignment, + Width: e.viewSize.X, + Offset: off, + } + e.shapes = e.shapes[:0] + for { + layout, off, selected, yOffs, size, ok := it.Next() + if !ok { + break + } + path := e.shaper.Shape(e.font, e.textSize, layout) + e.shapes = append(e.shapes, line{off, path, selected, yOffs, size}) + } + + key.InputOp{Tag: &e.eventKey}.Add(gtx.Ops) + if e.requestFocus { + key.FocusOp{Tag: &e.eventKey}.Add(gtx.Ops) + key.SoftKeyboardOp{Show: true}.Add(gtx.Ops) + } + e.requestFocus = false + pointerPadding := gtx.Px(unit.Dp(4)) + r := image.Rectangle{Max: e.viewSize} + r.Min.X -= pointerPadding + r.Min.Y -= pointerPadding + r.Max.X += pointerPadding + r.Max.X += pointerPadding + pointer.Rect(r).Add(gtx.Ops) + pointer.CursorNameOp{Name: pointer.CursorText}.Add(gtx.Ops) + + var scrollRange image.Rectangle + if e.SingleLine { + scrollRange.Min.X = -e.scrollOff.X + scrollRange.Max.X = max(0, e.dims.Size.X-(e.scrollOff.X+e.viewSize.X)) + } else { + scrollRange.Min.Y = -e.scrollOff.Y + scrollRange.Max.Y = max(0, e.dims.Size.Y-(e.scrollOff.Y+e.viewSize.Y)) + } + e.scroller.Add(gtx.Ops, scrollRange) + + e.clicker.Add(gtx.Ops) + e.dragger.Add(gtx.Ops) + e.caret.on = false + if e.focused { + now := gtx.Now + dt := now.Sub(e.blinkStart) + blinking := dt < maxBlinkDuration + const timePerBlink = time.Second / blinksPerSecond + nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2)) + if blinking { + redraw := op.InvalidateOp{At: nextBlink} + redraw.Add(gtx.Ops) + } + e.caret.on = e.focused && (!blinking || dt%timePerBlink < timePerBlink/2) + } + + return layout.Dimensions{Size: e.viewSize, Baseline: e.dims.Baseline} +} + +// PaintSelection paints the contrasting background for selected text. +func (e *Editor) PaintSelection(gtx layout.Context) { + cl := textPadding(e.lines) + cl.Max = cl.Max.Add(e.viewSize) + clip.Rect(cl).Add(gtx.Ops) + for _, shape := range e.shapes { + if !shape.selected { + continue + } + stack := op.Save(gtx.Ops) + offset := shape.offset + offset.Y += shape.selectionYOffs + op.Offset(layout.FPt(offset)).Add(gtx.Ops) + clip.Rect(image.Rectangle{Max: shape.selectionSize}).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + } +} + +func (e *Editor) PaintText(gtx layout.Context) { + cl := textPadding(e.lines) + cl.Max = cl.Max.Add(e.viewSize) + clip.Rect(cl).Add(gtx.Ops) + for _, shape := range e.shapes { + stack := op.Save(gtx.Ops) + op.Offset(layout.FPt(shape.offset)).Add(gtx.Ops) + shape.clip.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + } +} + +func (e *Editor) PaintCaret(gtx layout.Context) { + if !e.caret.on { + return + } + e.makeValid() + carWidth := fixed.I(gtx.Px(unit.Dp(1))) + carX := e.caret.start.x + carY := e.caret.start.y + + defer op.Save(gtx.Ops).Load() + carX -= carWidth / 2 + carAsc, carDesc := -e.lines[e.caret.start.lineCol.Y].Bounds.Min.Y, e.lines[e.caret.start.lineCol.Y].Bounds.Max.Y + carRect := image.Rectangle{ + Min: image.Point{X: carX.Ceil(), Y: carY - carAsc.Ceil()}, + Max: image.Point{X: carX.Ceil() + carWidth.Ceil(), + Y: carY + carDesc.Ceil()}, + } + carRect = carRect.Add(image.Point{ + X: -e.scrollOff.X, + Y: -e.scrollOff.Y, + }) + cl := textPadding(e.lines) + // Account for caret width to each side. + whalf := (carWidth / 2).Ceil() + if cl.Max.X < whalf { + cl.Max.X = whalf + } + if cl.Min.X > -whalf { + cl.Min.X = -whalf + } + cl.Max = cl.Max.Add(e.viewSize) + carRect = cl.Intersect(carRect) + if !carRect.Empty() { + st := op.Save(gtx.Ops) + clip.Rect(carRect).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + st.Load() + } +} + +// Len is the length of the editor contents. +func (e *Editor) Len() int { + return e.rr.len() +} + +// Text returns the contents of the editor. +func (e *Editor) Text() string { + return e.rr.String() +} + +// SetText replaces the contents of the editor, clearing any selection first. +func (e *Editor) SetText(s string) { + e.rr = editBuffer{} + e.caret.start = combinedPos{} + e.caret.end = combinedPos{} + e.prepend(s) +} + +func (e *Editor) scrollBounds() image.Rectangle { + var b image.Rectangle + if e.SingleLine { + if len(e.lines) > 0 { + b.Min.X = align(e.Alignment, e.lines[0].Width, e.viewSize.X).Floor() + if b.Min.X > 0 { + b.Min.X = 0 + } + } + b.Max.X = e.dims.Size.X + b.Min.X - e.viewSize.X + } else { + b.Max.Y = e.dims.Size.Y - e.viewSize.Y + } + return b +} + +func (e *Editor) scrollRel(dx, dy int) { + e.scrollAbs(e.scrollOff.X+dx, e.scrollOff.Y+dy) +} + +func (e *Editor) scrollAbs(x, y int) { + e.scrollOff.X = x + e.scrollOff.Y = y + b := e.scrollBounds() + if e.scrollOff.X > b.Max.X { + e.scrollOff.X = b.Max.X + } + if e.scrollOff.X < b.Min.X { + e.scrollOff.X = b.Min.X + } + if e.scrollOff.Y > b.Max.Y { + e.scrollOff.Y = b.Max.Y + } + if e.scrollOff.Y < b.Min.Y { + e.scrollOff.Y = b.Min.Y + } +} + +func (e *Editor) moveCoord(pos image.Point) { + var ( + prevDesc fixed.Int26_6 + carLine int + y int + ) + for _, l := range e.lines { + y += (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + if y+prevDesc.Ceil() >= pos.Y+e.scrollOff.Y { + break + } + carLine++ + } + x := fixed.I(pos.X + e.scrollOff.X) + e.caret.start = e.movePosToLine(e.caret.start, x, carLine) + e.caret.start.xoff = 0 +} + +func (e *Editor) layoutText(s text.Shaper) ([]text.Line, layout.Dimensions) { + e.rr.Reset() + var r io.Reader = &e.rr + if e.Mask != 0 { + e.maskReader.Reset(&e.rr, e.Mask) + r = &e.maskReader + } + var lines []text.Line + if s != nil { + lines, _ = s.Layout(e.font, e.textSize, e.maxWidth, r) + } else { + lines, _ = nullLayout(r) + } + dims := linesDimens(lines) + for i := 0; i < len(lines)-1; i++ { + // To avoid layout flickering while editing, assume a soft newline takes + // up all available space. + if layout := lines[i].Layout; len(layout.Text) > 0 { + r := layout.Text[len(layout.Text)-1] + if r != '\n' { + dims.Size.X = e.maxWidth + break + } + } + } + return lines, dims +} + +// CaretPos returns the line & column numbers of the caret. +func (e *Editor) CaretPos() (line, col int) { + e.makeValid() + return e.caret.start.lineCol.Y, e.caret.start.lineCol.X +} + +// CaretCoords returns the coordinates of the caret, relative to the +// editor itself. +func (e *Editor) CaretCoords() f32.Point { + e.makeValid() + return f32.Pt(float32(e.caret.start.x)/64, float32(e.caret.start.y)) +} + +// offsetToScreenPos2 is a utility function to shortcut the common case of +// wanting the positions of exactly two offsets. +func (e *Editor) offsetToScreenPos2(o1, o2 int) (combinedPos, combinedPos) { + cp1, iter := e.offsetToScreenPos(o1) + return cp1, iter(o2) +} + +// offsetToScreenPos takes an offset into the editor text (e.g. +// e.caret.end.ofs) and returns a combinedPos that corresponds to its current +// screen position, as well as an iterator that lets you get the combinedPos +// of a later offset. The offsets given to offsetToScreenPos and to the +// returned iterator must be sorted, lowest first, and they must be valid (0 +// <= offset <= e.Len()). +// +// This function is written this way to take advantage of previous work done +// for offsets after the first. Otherwise you have to start from the top each +// time. +func (e *Editor) offsetToScreenPos(offset int) (combinedPos, + func(int) combinedPos) { + var col, line, idx int + var x fixed.Int26_6 + + l := e.lines[line] + y := l.Ascent.Ceil() + prevDesc := l.Descent + + iter := func(offset int) combinedPos { + LOOP: + for { + for ; col < len(l.Layout.Advances); col++ { + if idx >= offset { + break LOOP + } + + x += l.Layout.Advances[col] + _, s := e.rr.runeAt(idx) + idx += s + } + if lastLine := line == len(e.lines)-1; lastLine || idx > offset { + break LOOP + } + + line++ + x = 0 + col = 0 + l = e.lines[line] + y += (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + } + return combinedPos{ + lineCol: screenPos{Y: line, X: col}, + x: x + align(e.Alignment, e.lines[line].Width, e.viewSize.X), + y: y, + ofs: offset, + } + } + return iter(offset), iter +} + +func (e *Editor) invalidate() { + e.valid = false +} + +// Delete runes from the caret position. The sign of runes specifies the +// direction to delete: positive is forward, negative is backward. +// +// If there is a selection, it is deleted and counts as a single rune. +func (e *Editor) Delete(runes int) { + if runes == 0 { + return + } + + if l := e.caret.end.ofs - e.caret.start.ofs; l != 0 { + e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, l) + runes -= sign(runes) + } + + e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, runes) + e.caret.start.xoff = 0 + e.ClearSelection() + e.invalidate() +} + +// Insert inserts text at the caret, moving the caret forward. If there is a +// selection, Insert overwrites it. +func (e *Editor) Insert(s string) { + e.append(s) + e.caret.scroll = true +} + +// append inserts s at the cursor, leaving the caret is at the end of s. If +// there is a selection, append overwrites it. +// xxx|yyy + append zzz => xxxzzz|yyy +func (e *Editor) append(s string) { + e.prepend(s) + e.caret.start.ofs += len(s) + e.caret.end.ofs = e.caret.start.ofs +} + +// prepend inserts s after the cursor; the caret does not change. If there is +// a selection, prepend overwrites it. +// xxx|yyy + prepend zzz => xxx|zzzyyy +func (e *Editor) prepend(s string) { + if e.SingleLine { + s = strings.ReplaceAll(s, "\n", " ") + } + e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, + e.caret.end.ofs-e.caret.start.ofs) // Delete any selection first. + e.rr.prepend(e.caret.start.ofs, s) + e.caret.start.xoff = 0 + e.invalidate() +} + +func (e *Editor) movePages(pages int, selAct selectionAction) { + e.makeValid() + y := e.caret.start.y + pages*e.viewSize.Y + var ( + prevDesc fixed.Int26_6 + carLine2 int + ) + y2 := e.lines[0].Ascent.Ceil() + for i := 1; i < len(e.lines); i++ { + if y2 >= y { + break + } + l := e.lines[i] + h := (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + if y2+h-y >= y-y2 { + break + } + y2 += h + carLine2++ + } + e.caret.start = e.movePosToLine(e.caret.start, + e.caret.start.x+e.caret.start.xoff, carLine2) + e.updateSelection(selAct) +} + +func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6, + line int) combinedPos { + e.makeValid(&pos) + if line < 0 { + line = 0 + } + if line >= len(e.lines) { + line = len(e.lines) - 1 + } + + prevDesc := e.lines[line].Descent + for pos.lineCol.Y < line { + pos = e.movePosToEnd(pos) + l := e.lines[pos.lineCol.Y] + _, s := e.rr.runeAt(pos.ofs) + pos.ofs += s + pos.y += (prevDesc + l.Ascent).Ceil() + pos.lineCol.X = 0 + prevDesc = l.Descent + pos.lineCol.Y++ + } + for pos.lineCol.Y > line { + pos = e.movePosToStart(pos) + l := e.lines[pos.lineCol.Y] + _, s := e.rr.runeBefore(pos.ofs) + pos.ofs -= s + pos.y -= (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + pos.lineCol.Y-- + l = e.lines[pos.lineCol.Y] + pos.lineCol.X = len(l.Layout.Advances) - 1 + } + + pos = e.movePosToStart(pos) + l := e.lines[line] + pos.x = align(e.Alignment, l.Width, e.viewSize.X) + // Only move past the end of the last line + end := 0 + if line < len(e.lines)-1 { + end = 1 + } + // Move to rune closest to x. + for i := 0; i < len(l.Layout.Advances)-end; i++ { + adv := l.Layout.Advances[i] + if pos.x >= x { + break + } + if pos.x+adv-x >= x-pos.x { + break + } + pos.x += adv + _, s := e.rr.runeAt(pos.ofs) + pos.ofs += s + pos.lineCol.X++ + } + pos.xoff = x - pos.x + return pos +} + +// MoveCaret moves the caret (aka selection start) and the selection end +// relative to their current positions. Positive distances moves forward, +// negative distances moves backward. Distances are in runes. +func (e *Editor) MoveCaret(startDelta, endDelta int) { + e.makeValid() + keepSame := e.caret.start.ofs == e.caret.end.ofs && startDelta == endDelta + e.caret.start = e.movePos(e.caret.start, startDelta) + e.caret.start.xoff = 0 + // If they were in the same place, and we're moving them the same distance, + // just assign the new position, instead of recalculating it. + if keepSame { + e.caret.end = e.caret.start + } else { + e.caret.end = e.movePos(e.caret.end, endDelta) + e.caret.end.xoff = 0 + } +} + +func (e *Editor) movePos(pos combinedPos, distance int) combinedPos { + for ; distance < 0 && pos.ofs > 0; distance++ { + if pos.lineCol.X == 0 { + // Move to end of previous line. + pos = e.movePosToLine(pos, fixed.I(e.maxWidth), pos.lineCol.Y-1) + continue + } + l := e.lines[pos.lineCol.Y].Layout + _, s := e.rr.runeBefore(pos.ofs) + pos.ofs -= s + pos.lineCol.X-- + pos.x -= l.Advances[pos.lineCol.X] + } + for ; distance > 0 && pos.ofs < e.rr.len(); distance-- { + l := e.lines[pos.lineCol.Y].Layout + // Only move past the end of the last line + end := 0 + if pos.lineCol.Y < len(e.lines)-1 { + end = 1 + } + if pos.lineCol.X >= len(l.Advances)-end { + // Move to start of next line. + pos = e.movePosToLine(pos, 0, pos.lineCol.Y+1) + continue + } + pos.x += l.Advances[pos.lineCol.X] + _, s := e.rr.runeAt(pos.ofs) + pos.ofs += s + pos.lineCol.X++ + } + return pos +} + +func (e *Editor) moveStart(selAct selectionAction) { + e.caret.start = e.movePosToStart(e.caret.start) + e.updateSelection(selAct) +} + +func (e *Editor) movePosToStart(pos combinedPos) combinedPos { + e.makeValid(&pos) + layout := e.lines[pos.lineCol.Y].Layout + for i := pos.lineCol.X - 1; i >= 0; i-- { + _, s := e.rr.runeBefore(pos.ofs) + pos.ofs -= s + pos.x -= layout.Advances[i] + } + pos.lineCol.X = 0 + pos.xoff = -pos.x + return pos +} + +func (e *Editor) moveEnd(selAct selectionAction) { + e.caret.start = e.movePosToEnd(e.caret.start) + e.updateSelection(selAct) +} + +func (e *Editor) movePosToEnd(pos combinedPos) combinedPos { + e.makeValid(&pos) + l := e.lines[pos.lineCol.Y] + // Only move past the end of the last line + end := 0 + if pos.lineCol.Y < len(e.lines)-1 { + end = 1 + } + layout := l.Layout + for i := pos.lineCol.X; i < len(layout.Advances)-end; i++ { + adv := layout.Advances[i] + _, s := e.rr.runeAt(pos.ofs) + pos.ofs += s + pos.x += adv + pos.lineCol.X++ + } + a := align(e.Alignment, l.Width, e.viewSize.X) + pos.xoff = l.Width + a - pos.x + return pos +} + +// moveWord moves the caret to the next word in the specified direction. +// Positive is forward, negative is backward. +// Absolute values greater than one will skip that many words. +func (e *Editor) moveWord(distance int, selAct selectionAction) { + e.makeValid() + // split the distance information into constituent parts to be + // used independently. + words, direction := distance, 1 + if distance < 0 { + words, direction = distance*-1, -1 + } + // atEnd if caret is at either side of the buffer. + atEnd := func() bool { + return e.caret.start.ofs == 0 || e.caret.start.ofs == e.rr.len() + } + // next returns the appropriate rune given the direction. + next := func() (r rune) { + if direction < 0 { + r, _ = e.rr.runeBefore(e.caret.start.ofs) + } else { + r, _ = e.rr.runeAt(e.caret.start.ofs) + } + return r + } + for ii := 0; ii < words; ii++ { + for r := next(); unicode.IsSpace(r) && !atEnd(); r = next() { + e.MoveCaret(direction, 0) + } + e.MoveCaret(direction, 0) + for r := next(); !unicode.IsSpace(r) && !atEnd(); r = next() { + e.MoveCaret(direction, 0) + } + } + e.updateSelection(selAct) +} + +// deleteWord deletes the next word(s) in the specified direction. +// Unlike moveWord, deleteWord treats whitespace as a word itself. +// Positive is forward, negative is backward. +// Absolute values greater than one will delete that many words. +// The selection counts as a single word. +func (e *Editor) deleteWord(distance int) { + if distance == 0 { + return + } + + e.makeValid() + + if e.caret.start.ofs != e.caret.end.ofs { + e.Delete(1) + distance -= sign(distance) + } + if distance == 0 { + return + } + + // split the distance information into constituent parts to be + // used independently. + words, direction := distance, 1 + if distance < 0 { + words, direction = distance*-1, -1 + } + // atEnd if offset is at or beyond either side of the buffer. + atEnd := func(offset int) bool { + idx := e.caret.start.ofs + offset*direction + return idx <= 0 || idx >= e.rr.len() + } + // next returns the appropriate rune given the direction and offset. + next := func(offset int) (r rune) { + idx := e.caret.start.ofs + offset*direction + if idx < 0 { + idx = 0 + } else if idx > e.rr.len() { + idx = e.rr.len() + } + if direction < 0 { + r, _ = e.rr.runeBefore(idx) + } else { + r, _ = e.rr.runeAt(idx) + } + return r + } + var runes = 1 + for ii := 0; ii < words; ii++ { + if r := next(runes); unicode.IsSpace(r) { + for r := next(runes); unicode.IsSpace(r) && !atEnd(runes); r = next(runes) { + runes += 1 + } + } else { + for r := next(runes); !unicode.IsSpace(r) && !atEnd(runes); r = next(runes) { + runes += 1 + } + } + } + e.Delete(runes * direction) +} + +func (e *Editor) scrollToCaret() { + e.makeValid() + l := e.lines[e.caret.start.lineCol.Y] + if e.SingleLine { + var dist int + if d := e.caret.start.x.Floor() - e.scrollOff.X; d < 0 { + dist = d + } else if d := e.caret.start.x.Ceil() - (e.scrollOff.X + e.viewSize.X); d > 0 { + dist = d + } + e.scrollRel(dist, 0) + } else { + miny := e.caret.start.y - l.Ascent.Ceil() + maxy := e.caret.start.y + l.Descent.Ceil() + var dist int + if d := miny - e.scrollOff.Y; d < 0 { + dist = d + } else if d := maxy - (e.scrollOff.Y + e.viewSize.Y); d > 0 { + dist = d + } + e.scrollRel(0, dist) + } +} + +// NumLines returns the number of lines in the editor. +func (e *Editor) NumLines() int { + e.makeValid() + return len(e.lines) +} + +// SelectionLen returns the length of the selection, in bytes; it is +// equivalent to len(e.SelectedText()). +func (e *Editor) SelectionLen() int { + return abs(e.caret.start.ofs - e.caret.end.ofs) +} + +// Selection returns the start and end of the selection, as offsets into the +// editor text. start can be > end. +func (e *Editor) Selection() (start, end int) { + return e.caret.start.ofs, e.caret.end.ofs +} + +// SetCaret moves the caret to start, and sets the selection end to end. start +// and end are in bytes, and represent offsets into the editor text. start and +// end must be at a rune boundary. +func (e *Editor) SetCaret(start, end int) { + e.makeValid() + // Constrain start and end to [0, e.Len()]. + l := e.Len() + start = max(min(start, l), 0) + end = max(min(end, l), 0) + e.caret.start.ofs, e.caret.end.ofs = start, end + e.makeValidCaret() + e.caret.scroll = true + e.scroller.Stop() +} + +func (e *Editor) makeValidCaret(positions ...*combinedPos) { + // Jump through some hoops to order the offsets given to offsetToScreenPos, + // but still be able to update them correctly with the results thereof. + positions = append(positions, &e.caret.start, &e.caret.end) + sort.Slice(positions, func(i, j int) bool { + return positions[i].ofs < positions[j].ofs + }) + var iter func(offset int) combinedPos + *positions[0], iter = e.offsetToScreenPos(positions[0].ofs) + for _, cp := range positions[1:] { + *cp = iter(cp.ofs) + } +} + +// SelectedText returns the currently selected text (if any) from the editor. +func (e *Editor) SelectedText() string { + l := e.SelectionLen() + if l == 0 { + return "" + } + buf := make([]byte, l) + e.rr.Seek(int64(min(e.caret.start.ofs, e.caret.end.ofs)), io.SeekStart) + _, err := e.rr.Read(buf) + if err != nil { + // The only error that rr.Read can return is EOF, which just means no + // selection, but we've already made sure that shouldn't happen. + panic("impossible error because end is before e.rr.Len()") + } + return string(buf) +} + +func (e *Editor) updateSelection(selAct selectionAction) { + if selAct == selectionClear { + e.ClearSelection() + } +} + +// ClearSelection clears the selection, by setting the selection end equal to +// the selection start. +func (e *Editor) ClearSelection() { + e.caret.end = e.caret.start +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func abs(n int) int { + if n < 0 { + return -n + } + return n +} + +func sign(n int) int { + switch { + case n < 0: + return -1 + case n > 0: + return 1 + default: + return 0 + } +} + +// sortPoints returns a and b sorted such that a2 <= b2. +func sortPoints(a, b screenPos) (a2, b2 screenPos) { + if b.Less(a) { + return b, a + } + return a, b +} + +func nullLayout(r io.Reader) ([]text.Line, error) { + rr := bufio.NewReader(r) + var rerr error + var n int + var buf bytes.Buffer + for { + r, s, err := rr.ReadRune() + n += s + buf.WriteRune(r) + if err != nil { + rerr = err + break + } + } + return []text.Line{ + { + Layout: text.Layout{ + Text: buf.String(), + Advances: make([]fixed.Int26_6, n), + }, + }, + }, rerr +} + +func (s ChangeEvent) isEditorEvent() {} +func (s SubmitEvent) isEditorEvent() {} +func (s SelectEvent) isEditorEvent() {} diff --git a/gio/widget/editor_test.go b/gio/widget/editor_test.go new file mode 100644 index 0000000..6b37b50 --- /dev/null +++ b/gio/widget/editor_test.go @@ -0,0 +1,540 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "fmt" + "image" + "math/rand" + "reflect" + "strings" + "testing" + "testing/quick" + "unicode" + + "realy.lol/gio/f32" + "realy.lol/gio/font/gofont" + "realy.lol/gio/io/event" + "realy.lol/gio/io/key" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/text" + "realy.lol/gio/unit" + "golang.org/x/image/math/fixed" +) + +func TestEditor(t *testing.T) { + e := new(Editor) + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + + e.SetCaret(0, 0) // shouldn't panic + assertCaret(t, e, 0, 0, 0) + e.SetText("Ʀbc\naĆøĆ„ā€¢") + e.Layout(gtx, cache, font, fontSize) + assertCaret(t, e, 0, 0, 0) + e.moveEnd(selectionClear) + assertCaret(t, e, 0, 3, len("Ʀbc")) + e.MoveCaret(+1, +1) + assertCaret(t, e, 1, 0, len("Ʀbc\n")) + e.MoveCaret(-1, -1) + assertCaret(t, e, 0, 3, len("Ʀbc")) + e.moveLines(+1, +1) + assertCaret(t, e, 1, 3, len("Ʀbc\naĆøĆ„")) + e.moveEnd(selectionClear) + assertCaret(t, e, 1, 4, len("Ʀbc\naĆøĆ„ā€¢")) + e.MoveCaret(+1, +1) + assertCaret(t, e, 1, 4, len("Ʀbc\naĆøĆ„ā€¢")) + + e.SetCaret(0, 0) + assertCaret(t, e, 0, 0, 0) + e.SetCaret(len("Ʀ"), len("Ʀ")) + assertCaret(t, e, 0, 1, 2) + e.SetCaret(len("Ʀbc\naĆøĆ„ā€¢"), len("Ʀbc\naĆøĆ„ā€¢")) + assertCaret(t, e, 1, 4, len("Ʀbc\naĆøĆ„ā€¢")) + + // Ensure that password masking does not affect caret behavior + e.MoveCaret(-3, -3) + assertCaret(t, e, 1, 1, len("Ʀbc\na")) + e.Mask = '*' + e.Layout(gtx, cache, font, fontSize) + assertCaret(t, e, 1, 1, len("Ʀbc\na")) + e.MoveCaret(-3, -3) + assertCaret(t, e, 0, 2, len("Ʀb")) + e.Mask = '\U0001F92B' + e.Layout(gtx, cache, font, fontSize) + e.moveEnd(selectionClear) + assertCaret(t, e, 0, 3, len("Ʀbc")) + + // When a password mask is applied, it should replace all visible glyphs + for i, line := range e.lines { + for j, r := range line.Layout.Text { + if r != e.Mask && !unicode.IsSpace(r) { + t.Errorf("glyph at (%d, %d) is unmasked rune %d", i, j, r) + } + } + } +} + +func TestEditorDimensions(t *testing.T) { + e := new(Editor) + tq := &testQueue{ + events: []event.Event{ + key.EditEvent{Text: "A"}, + }, + } + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Constraints{Max: image.Pt(100, 100)}, + Queue: tq, + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + dims := e.Layout(gtx, cache, font, fontSize) + if dims.Size.X == 0 { + t.Errorf("EditEvent was not reflected in Editor width") + } +} + +// assertCaret asserts that the editor caret is at a particular line +// and column, and that the byte position matches as well. +func assertCaret(t *testing.T, e *Editor, line, col, bytes int) { + t.Helper() + gotLine, gotCol := e.CaretPos() + if gotLine != line || gotCol != col { + t.Errorf("caret at (%d, %d), expected (%d, %d)", gotLine, gotCol, line, + col) + } + if bytes != e.caret.start.ofs { + t.Errorf("caret at buffer position %d, expected %d", e.caret.start.ofs, + bytes) + } +} + +type editMutation int + +const ( + setText editMutation = iota + moveRune + moveLine + movePage + moveStart + moveEnd + moveCoord + moveWord + deleteWord + moveLast // Mark end; never generated. +) + +func TestEditorCaretConsistency(t *testing.T) { + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + for _, a := range []text.Alignment{text.Start, text.Middle, text.End} { + e := &Editor{ + Alignment: a, + } + e.Layout(gtx, cache, font, fontSize) + + consistent := func() error { + t.Helper() + gotLine, gotCol := e.CaretPos() + gotCoords := e.CaretCoords() + want, _ := e.offsetToScreenPos(e.caret.start.ofs) + wantCoords := f32.Pt(float32(want.x)/64, float32(want.y)) + if want.lineCol.Y == gotLine && want.lineCol.X == gotCol && gotCoords == wantCoords { + return nil + } + return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s", + gotLine, gotCol, gotCoords, want.lineCol.Y, want.lineCol.X, + wantCoords) + } + if err := consistent(); err != nil { + t.Errorf("initial editor inconsistency (alignment %s): %v", a, err) + } + + move := func(mutation editMutation, str string, distance int8, + x, y uint16) bool { + switch mutation { + case setText: + e.SetText(str) + e.Layout(gtx, cache, font, fontSize) + case moveRune: + e.MoveCaret(int(distance), int(distance)) + case moveLine: + e.moveLines(int(distance), selectionClear) + case movePage: + e.movePages(int(distance), selectionClear) + case moveStart: + e.moveStart(selectionClear) + case moveEnd: + e.moveEnd(selectionClear) + case moveCoord: + e.moveCoord(image.Pt(int(x), int(y))) + case moveWord: + e.moveWord(int(distance), selectionClear) + case deleteWord: + e.deleteWord(int(distance)) + default: + return false + } + if err := consistent(); err != nil { + t.Error(err) + return false + } + return true + } + if err := quick.Check(move, nil); err != nil { + t.Errorf("editor inconsistency (alignment %s): %v", a, err) + } + } +} + +func TestEditorMoveWord(t *testing.T) { + type Test struct { + Text string + Start int + Skip int + Want int + } + tests := []Test{ + {"", 0, 0, 0}, + {"", 0, -1, 0}, + {"", 0, 1, 0}, + {"hello", 0, -1, 0}, + {"hello", 0, 1, 5}, + {"hello world", 3, 1, 5}, + {"hello world", 3, -1, 0}, + {"hello world", 8, -1, 6}, + {"hello world", 8, 1, 11}, + {"hello world", 3, 1, 5}, + {"hello world", 3, 2, 14}, + {"hello world", 8, 1, 14}, + {"hello world", 8, -1, 0}, + {"hello brave new world", 0, 3, 15}, + } + setup := func(t string) *Editor { + e := new(Editor) + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + e.SetText(t) + e.Layout(gtx, cache, font, fontSize) + return e + } + for ii, tt := range tests { + e := setup(tt.Text) + e.MoveCaret(tt.Start, tt.Start) + e.moveWord(tt.Skip, selectionClear) + if e.caret.start.ofs != tt.Want { + t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, + e.caret.start.ofs, tt.Want) + } + } +} + +func TestEditorDeleteWord(t *testing.T) { + type Test struct { + Text string + Start int + Selection int + Delete int + + Want int + Result string + } + tests := []Test{ + // No text selected + {"", 0, 0, 0, 0, ""}, + {"", 0, 0, -1, 0, ""}, + {"", 0, 0, 1, 0, ""}, + {"", 0, 0, -2, 0, ""}, + {"", 0, 0, 2, 0, ""}, + {"hello", 0, 0, -1, 0, "hello"}, + {"hello", 0, 0, 1, 0, ""}, + + // Document (imho) incorrect behavior w.r.t. deleting spaces following + // words. + {"hello world", 0, 0, 1, 0, + " world"}, // Should be "world", if you ask me. + {"hello world", 0, 0, 2, 0, "world"}, // Should be "". + {"hello ", 0, 0, 1, 0, " "}, // Should be "". + {"hello world", 11, 0, -1, 6, "hello "}, // Should be "hello". + {"hello world", 11, 0, -2, 5, "hello"}, // Should be "". + {"hello ", 6, 0, -1, 0, ""}, // Correct result. + + {"hello world", 3, 0, 1, 3, "hel world"}, + {"hello world", 3, 0, -1, 0, "lo world"}, + {"hello world", 8, 0, -1, 6, "hello rld"}, + {"hello world", 8, 0, 1, 8, "hello wo"}, + {"hello world", 3, 0, 1, 3, "hel world"}, + {"hello world", 3, 0, 2, 3, "helworld"}, + {"hello world", 8, 0, 1, 8, "hello "}, + {"hello world", 8, 0, -1, 5, "hello world"}, + {"hello brave new world", 0, 0, 3, 0, " new world"}, + // Add selected text. + // + // Several permutations must be tested: + // - select from the left or right + // - Delete + or - + // - abs(Delete) == 1 or > 1 + // + // "brave |" selected; caret at | + {"hello there brave new world", 12, 6, 1, 12, + "hello there new world"}, // #16 + {"hello there brave new world", 12, 6, 2, 12, + "hello there world"}, // The two spaces after "there" are actually suboptimal, if you ask me. See also above cases. + {"hello there brave new world", 12, 6, -1, 12, "hello there new world"}, + {"hello there brave new world", 12, 6, -2, 6, "hello new world"}, + // "|brave " selected + {"hello there brave new world", 18, -6, 1, 12, + "hello there new world"}, // #20 + {"hello there brave new world", 18, -6, 2, 12, + "hello there world"}, // ditto + {"hello there brave new world", 18, -6, -1, 12, + "hello there new world"}, + {"hello there brave new world", 18, -6, -2, 6, "hello new world"}, + // Random edge cases + {"hello there brave new world", 12, 6, 99, 12, "hello there "}, + {"hello there brave new world", 18, -6, -99, 0, "new world"}, + } + setup := func(t string) *Editor { + e := new(Editor) + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + e.SetText(t) + e.Layout(gtx, cache, font, fontSize) + return e + } + for ii, tt := range tests { + e := setup(tt.Text) + e.MoveCaret(tt.Start, tt.Start) + e.MoveCaret(0, tt.Selection) + e.deleteWord(tt.Delete) + if e.caret.start.ofs != tt.Want { + t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, + e.caret.start.ofs, tt.Want) + } + if e.Text() != tt.Result { + t.Fatalf("[%d] deleteWord: invalid result: got %q, want %q", ii, + e.Text(), tt.Result) + } + } +} + +func TestEditorNoLayout(t *testing.T) { + var e Editor + e.SetText("hi!\n") + e.MoveCaret(1, 1) +} + +// Generate generates a value of itself, for testing/quick. +func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value { + t := editMutation(rand.Intn(int(moveLast))) + return reflect.ValueOf(t) +} + +// TestSelect tests the selection code. It lays out an editor with several +// lines in it, selects some text, verifies the selection, resizes the editor +// to make it much narrower (which makes the lines in the editor reflow), and +// then verifies that the updated (col, line) positions of the selected text +// are where we expect. +func TestSelect(t *testing.T) { + e := new(Editor) + e.SetText(`a123456789a +b123456789b +c123456789c +d123456789d +e123456789e +f123456789f +g123456789g +`) + + gtx := layout.Context{Ops: new(op.Ops)} + cache := text.NewCache(gofont.Collection()) + font := text.Font{} + fontSize := unit.Px(10) + + selected := func(start, end int) string { + // Layout once with no events; populate e.lines. + gtx.Queue = nil + e.Layout(gtx, cache, font, fontSize) + _ = e.Events() // throw away any events from this layout + + // Build the selection events + startPos, endPos := e.offsetToScreenPos2(sortInts(start, end)) + tq := &testQueue{ + events: []event.Event{ + pointer.Event{ + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Source: pointer.Mouse, + Position: f32.Pt(textWidth(e, startPos.lineCol.Y, 0, + startPos.lineCol.X), textHeight(e, startPos.lineCol.Y)), + }, + pointer.Event{ + Type: pointer.Release, + Source: pointer.Mouse, + Position: f32.Pt(textWidth(e, endPos.lineCol.Y, 0, + endPos.lineCol.X), textHeight(e, endPos.lineCol.Y)), + }, + }, + } + gtx.Queue = tq + + e.Layout(gtx, cache, font, fontSize) + for _, evt := range e.Events() { + switch evt.(type) { + case SelectEvent: + return e.SelectedText() + } + } + return "" + } + + type testCase struct { + // input text offsets + start, end int + + // expected selected text + selection string + // expected line/col positions of selection after resize + startPos, endPos screenPos + } + + for n, tst := range []testCase{ + {0, 1, "a", screenPos{}, screenPos{Y: 0, X: 1}}, + {0, 4, "a123", screenPos{}, screenPos{Y: 0, X: 4}}, + {0, 11, "a123456789a", screenPos{}, screenPos{Y: 1, X: 5}}, + {2, 6, "2345", screenPos{Y: 0, X: 2}, screenPos{Y: 1, X: 0}}, + {41, 66, "56789d\ne123456789e\nf12345", screenPos{Y: 6, X: 5}, + screenPos{Y: 11, X: 0}}, + } { + // printLines(e) + + gtx.Constraints = layout.Exact(image.Pt(100, 100)) + if got := selected(tst.start, tst.end); got != tst.selection { + t.Errorf("Test %d pt1: Expected %q, got %q", n, tst.selection, got) + continue + } + + // Constrain the editor to roughly 6 columns wide and redraw + gtx.Constraints = layout.Exact(image.Pt(36, 36)) + // Keep existing selection + gtx.Queue = nil + e.Layout(gtx, cache, font, fontSize) + + if e.caret.end.lineCol != tst.startPos || e.caret.start.lineCol != tst.endPos { + t.Errorf("Test %d pt2: Expected %#v, %#v; got %#v, %#v", + n, + e.caret.end.lineCol, e.caret.start.lineCol, + tst.startPos, tst.endPos) + continue + } + + // printLines(e) + } +} + +// Verify that an existing selection is dismissed when you press arrow keys. +func TestSelectMove(t *testing.T) { + e := new(Editor) + e.SetText(`0123456789`) + + gtx := layout.Context{Ops: new(op.Ops)} + cache := text.NewCache(gofont.Collection()) + font := text.Font{} + fontSize := unit.Px(10) + + // Layout once to populate e.lines and get focus. + gtx.Queue = newQueue(key.FocusEvent{Focus: true}) + e.Layout(gtx, cache, font, fontSize) + + testKey := func(keyName string) { + // Select 345 + e.SetCaret(3, 6) + if expected, got := "345", e.SelectedText(); expected != got { + t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got) + } + + // Press the key + gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName}) + e.Layout(gtx, cache, font, fontSize) + + if expected, got := "", e.SelectedText(); expected != got { + t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got) + } + } + + testKey(key.NameLeftArrow) + testKey(key.NameRightArrow) + testKey(key.NameUpArrow) + testKey(key.NameDownArrow) +} + +func textWidth(e *Editor, lineNum, colStart, colEnd int) float32 { + var w fixed.Int26_6 + advances := e.lines[lineNum].Layout.Advances + if colEnd > len(advances) { + colEnd = len(advances) + } + for _, adv := range advances[colStart:colEnd] { + w += adv + } + return float32(w.Floor()) +} + +func textHeight(e *Editor, lineNum int) float32 { + var h fixed.Int26_6 + for _, line := range e.lines[0:lineNum] { + h += line.Ascent + line.Descent + } + return float32(h.Floor() + 1) +} + +type testQueue struct { + events []event.Event +} + +func newQueue(e ...event.Event) *testQueue { + return &testQueue{events: e} +} + +func (q *testQueue) Events(_ event.Tag) []event.Event { + return q.events +} + +func printLines(e *Editor) { + for n, line := range e.lines { + text := strings.TrimSuffix(line.Layout.Text, "\n") + fmt.Printf("%d: %s\n", n, text) + } +} + +// sortInts returns a and b sorted such that a2 <= b2. +func sortInts(a, b int) (a2, b2 int) { + if b < a { + return b, a + } + return a, b +} diff --git a/gio/widget/enum.go b/gio/widget/enum.go new file mode 100644 index 0000000..1ef721a --- /dev/null +++ b/gio/widget/enum.go @@ -0,0 +1,77 @@ +package widget + +import ( + "image" + + "realy.lol/gio/gesture" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" +) + +type Enum struct { + Value string + hovered string + hovering bool + + changed bool + + clicks []gesture.Click + values []string +} + +func index(vs []string, t string) int { + for i, v := range vs { + if v == t { + return i + } + } + return -1 +} + +// Changed reports whether Value has changed by user interaction since the last +// call to Changed. +func (e *Enum) Changed() bool { + changed := e.changed + e.changed = false + return changed +} + +// Hovered returns the key that is highlighted, or false if none are. +func (e *Enum) Hovered() (string, bool) { + return e.hovered, e.hovering +} + +// Layout adds the event handler for key. +func (e *Enum) Layout(gtx layout.Context, key string) layout.Dimensions { + defer op.Save(gtx.Ops).Load() + pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops) + + if index(e.values, key) == -1 { + e.values = append(e.values, key) + e.clicks = append(e.clicks, gesture.Click{}) + e.clicks[len(e.clicks)-1].Add(gtx.Ops) + } else { + idx := index(e.values, key) + clk := &e.clicks[idx] + for _, ev := range clk.Events(gtx) { + switch ev.Type { + case gesture.TypeClick: + if new := e.values[idx]; new != e.Value { + e.Value = new + e.changed = true + } + } + } + if e.hovering && e.hovered == key { + e.hovering = false + } + if clk.Hovered() { + e.hovered = key + e.hovering = true + } + clk.Add(gtx.Ops) + } + + return layout.Dimensions{Size: gtx.Constraints.Min} +} diff --git a/gio/widget/example_test.go b/gio/widget/example_test.go new file mode 100644 index 0000000..f5e9cf5 --- /dev/null +++ b/gio/widget/example_test.go @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget_test + +import ( + "fmt" + "image" + + "realy.lol/gio/f32" + "realy.lol/gio/io/pointer" + "realy.lol/gio/io/router" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/widget" +) + +func ExampleClickable_passthrough() { + // When laying out clickable widgets on top of each other, + // pointer events can be passed down for the underlying + // widgets to pick them up. + var button1, button2 widget.Clickable + var r router.Router + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + Queue: &r, + } + + // widget lays out two buttons on top of each other. + widget := func() { + // button2 completely covers button1, but PassOp allows pointer + // events to pass through to button1. + button1.Layout(gtx) + // PassOp is applied to the area defined by button1. + pointer.PassOp{Pass: true}.Add(gtx.Ops) + button2.Layout(gtx) + } + + // The first layout and call to Frame declare the Clickable handlers + // to the input router, so the following pointer events are propagated. + widget() + r.Frame(gtx.Ops) + // Simulate one click on the buttons by sending a Press and Release event. + r.Queue( + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Release, + Position: f32.Pt(50, 50), + }, + ) + // The second layout ensures that the click event is registered by the buttons. + widget() + + if button1.Clicked() { + fmt.Println("button1 clicked!") + } + if button2.Clicked() { + fmt.Println("button2 clicked!") + } + + // Output: + // button1 clicked! + // button2 clicked! +} diff --git a/gio/widget/fit.go b/gio/widget/fit.go new file mode 100644 index 0000000..08adb74 --- /dev/null +++ b/gio/widget/fit.go @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + + "realy.lol/gio/f32" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" +) + +// Fit scales a widget to fit and clip to the constraints. +type Fit uint8 + +const ( + // Unscaled does not alter the scale of a widget. + Unscaled Fit = iota + // Contain scales widget as large as possible without cropping + // and it preserves aspect-ratio. + Contain + // Cover scales the widget to cover the constraint area and + // preserves aspect-ratio. + Cover + // ScaleDown scales the widget smaller without cropping, + // when it exceeds the constraint area. + // It preserves aspect-ratio. + ScaleDown + // Fill stretches the widget to the constraints and does not + // preserve aspect-ratio. + Fill +) + +// scale adds clip and scale operations to fit dims to the constraints. +// It positions the widget to the appropriate position. +// It returns dimensions modified accordingly. +func (fit Fit) scale(gtx layout.Context, pos layout.Direction, + dims layout.Dimensions) layout.Dimensions { + widgetSize := dims.Size + + if fit == Unscaled || dims.Size.X == 0 || dims.Size.Y == 0 { + dims.Size = gtx.Constraints.Constrain(dims.Size) + clip.Rect{Max: dims.Size}.Add(gtx.Ops) + + offset := pos.Position(widgetSize, dims.Size) + op.Offset(layout.FPt(offset)).Add(gtx.Ops) + dims.Baseline += offset.Y + return dims + } + + scale := f32.Point{ + X: float32(gtx.Constraints.Max.X) / float32(dims.Size.X), + Y: float32(gtx.Constraints.Max.Y) / float32(dims.Size.Y), + } + + switch fit { + case Contain: + if scale.Y < scale.X { + scale.X = scale.Y + } else { + scale.Y = scale.X + } + case Cover: + if scale.Y > scale.X { + scale.X = scale.Y + } else { + scale.Y = scale.X + } + case ScaleDown: + if scale.Y < scale.X { + scale.X = scale.Y + } else { + scale.Y = scale.X + } + + // The widget would need to be scaled up, no change needed. + if scale.X >= 1 { + dims.Size = gtx.Constraints.Constrain(dims.Size) + clip.Rect{Max: dims.Size}.Add(gtx.Ops) + + offset := pos.Position(widgetSize, dims.Size) + op.Offset(layout.FPt(offset)).Add(gtx.Ops) + dims.Baseline += offset.Y + return dims + } + case Fill: + } + + var scaledSize image.Point + scaledSize.X = int(float32(widgetSize.X) * scale.X) + scaledSize.Y = int(float32(widgetSize.Y) * scale.Y) + dims.Size = gtx.Constraints.Constrain(scaledSize) + dims.Baseline = int(float32(dims.Baseline) * scale.Y) + + clip.Rect{Max: dims.Size}.Add(gtx.Ops) + + offset := pos.Position(scaledSize, dims.Size) + op.Affine(f32.Affine2D{}. + Scale(f32.Point{}, scale). + Offset(layout.FPt(offset)), + ).Add(gtx.Ops) + + dims.Baseline += offset.Y + + return dims +} diff --git a/gio/widget/fit_test.go b/gio/widget/fit_test.go new file mode 100644 index 0000000..925ad34 --- /dev/null +++ b/gio/widget/fit_test.go @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "bytes" + "encoding/binary" + "image" + "math" + "testing" + + "realy.lol/gio/f32" + "realy.lol/gio/layout" + "realy.lol/gio/op" +) + +func TestFit(t *testing.T) { + type test struct { + Dims image.Point + Scale f32.Point + Result image.Point + } + + fittests := [...][]test{ + Unscaled: { + { + Dims: image.Point{0, 0}, + Scale: f32.Point{X: 1, Y: 1}, + Result: image.Point{X: 0, Y: 0}, + }, { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 1, Y: 1}, + Result: image.Point{X: 50, Y: 25}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 1, Y: 1}, + Result: image.Point{X: 50, Y: 100}, + }}, + Contain: { + { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 2, Y: 2}, + Result: image.Point{X: 100, Y: 50}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 0.5, Y: 0.5}, + Result: image.Point{X: 25, Y: 100}, + }}, + Cover: { + { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 4, Y: 4}, + Result: image.Point{X: 100, Y: 100}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 2, Y: 2}, + Result: image.Point{X: 100, Y: 100}, + }}, + ScaleDown: { + { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 1, Y: 1}, + Result: image.Point{X: 50, Y: 25}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 0.5, Y: 0.5}, + Result: image.Point{X: 25, Y: 100}, + }}, + Fill: { + { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 2, Y: 4}, + Result: image.Point{X: 100, Y: 100}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 2, Y: 0.5}, + Result: image.Point{X: 100, Y: 100}, + }}, + } + + for fit, tests := range fittests { + fit := Fit(fit) + for i, test := range tests { + ops := new(op.Ops) + gtx := layout.Context{ + Ops: ops, + Constraints: layout.Constraints{ + Max: image.Point{X: 100, Y: 100}, + }, + } + + result := fit.scale(gtx, layout.NW, + layout.Dimensions{Size: test.Dims}) + + if test.Scale.X != 1 || test.Scale.Y != 1 { + opsdata := gtx.Ops.Data() + scaleX := float32Bytes(test.Scale.X) + scaleY := float32Bytes(test.Scale.Y) + if !bytes.Contains(opsdata, scaleX) { + t.Errorf("did not find scale.X:%v (%x) in ops: %x", + test.Scale.X, scaleX, opsdata) + } + if !bytes.Contains(opsdata, scaleY) { + t.Errorf("did not find scale.Y:%v (%x) in ops: %x", + test.Scale.Y, scaleY, opsdata) + } + } + + if result.Size != test.Result { + t.Errorf("fit %v, #%v: expected %#v, got %#v", fit, i, + test.Result, result.Size) + } + } + } +} + +func float32Bytes(v float32) []byte { + var dst [4]byte + binary.LittleEndian.PutUint32(dst[:], math.Float32bits(v)) + return dst[:] +} diff --git a/gio/widget/float.go b/gio/widget/float.go new file mode 100644 index 0000000..e26e296 --- /dev/null +++ b/gio/widget/float.go @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + + "realy.lol/gio/gesture" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" +) + +// Float is for selecting a value in a range. +type Float struct { + Value float32 + Axis layout.Axis + + drag gesture.Drag + pos float32 // position normalized to [0, 1] + length float32 + changed bool +} + +// Dragging returns whether the value is being interacted with. +func (f *Float) Dragging() bool { return f.drag.Dragging() } + +// Layout updates the value according to drag events along the f's main axis. +// +// The range of f is set by the minimum constraints main axis value. +func (f *Float) Layout(gtx layout.Context, pointerMargin int, + min, max float32) layout.Dimensions { + size := gtx.Constraints.Min + f.length = float32(f.Axis.Convert(size).X) + + var de *pointer.Event + for _, e := range f.drag.Events(gtx.Metric, gtx, gesture.Axis(f.Axis)) { + if e.Type == pointer.Press || e.Type == pointer.Drag { + de = &e + } + } + + value := f.Value + if de != nil { + xy := de.Position.X + if f.Axis == layout.Vertical { + xy = de.Position.Y + } + f.pos = xy / f.length + value = min + (max-min)*f.pos + } else if min != max { + f.pos = (value - min) / (max - min) + } + // Unconditionally call setValue in case min, max, or value changed. + f.setValue(value, min, max) + + if f.pos < 0 { + f.pos = 0 + } else if f.pos > 1 { + f.pos = 1 + } + + defer op.Save(gtx.Ops).Load() + margin := f.Axis.Convert(image.Pt(pointerMargin, 0)) + rect := image.Rectangle{ + Min: margin.Mul(-1), + Max: size.Add(margin), + } + pointer.Rect(rect).Add(gtx.Ops) + f.drag.Add(gtx.Ops) + + return layout.Dimensions{Size: size} +} + +func (f *Float) setValue(value, min, max float32) { + if min > max { + min, max = max, min + } + if value < min { + value = min + } else if value > max { + value = max + } + if f.Value != value { + f.Value = value + f.changed = true + } +} + +// Pos reports the selected position. +func (f *Float) Pos() float32 { + return f.pos * f.length +} + +// Changed reports whether the value has changed since +// the last call to Changed. +func (f *Float) Changed() bool { + changed := f.changed + f.changed = false + return changed +} diff --git a/gio/widget/icon.go b/gio/widget/icon.go new file mode 100644 index 0000000..6f37d48 --- /dev/null +++ b/gio/widget/icon.go @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + "image/color" + "image/draw" + + "golang.org/x/exp/shiny/iconvg" + + "realy.lol/gio/internal/f32color" + "realy.lol/gio/layout" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" +) + +type Icon struct { + Color color.NRGBA + src []byte + // Cached values. + op paint.ImageOp + imgSize int + imgColor color.NRGBA +} + +// NewIcon returns a new Icon from IconVG data. +func NewIcon(data []byte) (*Icon, error) { + _, err := iconvg.DecodeMetadata(data) + if err != nil { + return nil, err + } + return &Icon{src: data, Color: color.NRGBA{A: 0xff}}, nil +} + +func (ic *Icon) Layout(gtx layout.Context, sz unit.Value) layout.Dimensions { + ico := ic.image(gtx.Px(sz)) + ico.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + return layout.Dimensions{ + Size: ico.Size(), + } +} + +func (ic *Icon) image(sz int) paint.ImageOp { + if sz == ic.imgSize && ic.Color == ic.imgColor { + return ic.op + } + m, _ := iconvg.DecodeMetadata(ic.src) + dx, dy := m.ViewBox.AspectRatio() + img := image.NewRGBA(image.Rectangle{Max: image.Point{X: sz, + Y: int(float32(sz) * dy / dx)}}) + var ico iconvg.Rasterizer + ico.SetDstImage(img, img.Bounds(), draw.Src) + m.Palette[0] = f32color.NRGBAToLinearRGBA(ic.Color) + iconvg.Decode(&ico, ic.src, &iconvg.DecodeOptions{ + Palette: &m.Palette, + }) + ic.op = paint.NewImageOp(img) + ic.imgSize = sz + ic.imgColor = ic.Color + return ic.op +} diff --git a/gio/widget/icon_test.go b/gio/widget/icon_test.go new file mode 100644 index 0000000..1a3e8d9 --- /dev/null +++ b/gio/widget/icon_test.go @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + "image/color" + "testing" + + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/unit" + "golang.org/x/exp/shiny/materialdesign/icons" +) + +func TestIcon_Alpha(t *testing.T) { + icon, err := NewIcon(icons.ToggleCheckBox) + if err != nil { + t.Fatal(err) + } + + icon.Color = color.NRGBA{B: 0xff, A: 0x40} + + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + + _ = icon.Layout(gtx, unit.Sp(18)) +} diff --git a/gio/widget/image.go b/gio/widget/image.go new file mode 100644 index 0000000..0e0351f --- /dev/null +++ b/gio/widget/image.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + + "realy.lol/gio/f32" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" +) + +// Image is a widget that displays an image. +type Image struct { + // Src is the image to display. + Src paint.ImageOp + // Fit specifies how to scale the image to the constraints. + // By default it does not do any scaling. + Fit Fit + // Position specifies where to position the image within + // the constraints. + Position layout.Direction + // Scale is the ratio of image pixels to + // dps. If Scale is zero Image falls back to + // a scale that match a standard 72 DPI. + Scale float32 +} + +const defaultScale = float32(160.0 / 72.0) + +func (im Image) Layout(gtx layout.Context) layout.Dimensions { + defer op.Save(gtx.Ops).Load() + + scale := im.Scale + if scale == 0 { + scale = defaultScale + } + + size := im.Src.Size() + wf, hf := float32(size.X), float32(size.Y) + w, h := gtx.Px(unit.Dp(wf*scale)), gtx.Px(unit.Dp(hf*scale)) + + dims := im.Fit.scale(gtx, im.Position, + layout.Dimensions{Size: image.Pt(w, h)}) + + pixelScale := scale * gtx.Metric.PxPerDp + op.Affine(f32.Affine2D{}.Scale(f32.Point{}, + f32.Pt(pixelScale, pixelScale))).Add(gtx.Ops) + + im.Src.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + + return dims +} diff --git a/gio/widget/image_test.go b/gio/widget/image_test.go new file mode 100644 index 0000000..774dfc7 --- /dev/null +++ b/gio/widget/image_test.go @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + "testing" + + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/paint" +) + +func TestImageScale(t *testing.T) { + var ops op.Ops + gtx := layout.Context{ + Ops: &ops, + Constraints: layout.Constraints{ + Max: image.Pt(50, 50), + }, + } + imgSize := image.Pt(10, 10) + img := image.NewNRGBA(image.Rectangle{Max: imgSize}) + imgOp := paint.NewImageOp(img) + + // Ensure the default scales correctly. + dims := Image{Src: imgOp}.Layout(gtx) + expectedSize := imgSize + expectedSize.X = int(float32(expectedSize.X) * defaultScale) + expectedSize.Y = int(float32(expectedSize.Y) * defaultScale) + if dims.Size != expectedSize { + t.Fatalf("non-scaled image is wrong size, expected %v, got %v", + expectedSize, dims.Size) + } + + // Ensure scaling the image via the Scale field works. + currentScale := float32(0.5) + dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) + expectedSize = imgSize + expectedSize.X = int(float32(expectedSize.X) * currentScale) + expectedSize.Y = int(float32(expectedSize.Y) * currentScale) + if dims.Size != expectedSize { + t.Fatalf(".5 scale image is wrong size, expected %v, got %v", + expectedSize, dims.Size) + } + + // Ensure the image responds to changes in DPI. + currentScale = float32(1) + gtx.Metric.PxPerDp = 2 + dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) + expectedSize = imgSize + expectedSize.X = int(float32(expectedSize.X) * currentScale * gtx.Metric.PxPerDp) + expectedSize.Y = int(float32(expectedSize.Y) * currentScale * gtx.Metric.PxPerDp) + if dims.Size != expectedSize { + t.Fatalf("HiDPI non-scaled image is wrong size, expected %v, got %v", + expectedSize, dims.Size) + } + + // Ensure scaling the image responds to changes in DPI. + currentScale = float32(.5) + gtx.Metric.PxPerDp = 2 + dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) + expectedSize = imgSize + expectedSize.X = int(float32(expectedSize.X) * currentScale * gtx.Metric.PxPerDp) + expectedSize.Y = int(float32(expectedSize.Y) * currentScale * gtx.Metric.PxPerDp) + if dims.Size != expectedSize { + t.Fatalf("HiDPI .5 scale image is wrong size, expected %v, got %v", + expectedSize, dims.Size) + } +} diff --git a/gio/widget/label.go b/gio/widget/label.go new file mode 100644 index 0000000..acf6b50 --- /dev/null +++ b/gio/widget/label.go @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "fmt" + "image" + "unicode/utf8" + + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" + + "golang.org/x/image/math/fixed" +) + +// Label is a widget for laying out and drawing text. +type Label struct { + // Alignment specify the text alignment. + Alignment text.Alignment + // MaxLines limits the number of lines. Zero means no limit. + MaxLines int +} + +// screenPos describes a character position (in text line and column numbers, +// not pixels): Y = line number, X = rune column. +type screenPos image.Point + +type segmentIterator struct { + Lines []text.Line + Clip image.Rectangle + Alignment text.Alignment + Width int + Offset image.Point + startSel screenPos + endSel screenPos + + pos screenPos // current position + line text.Line // current line + layout text.Layout // current line's Layout + + // pixel positions + off fixed.Point26_6 + y, prevDesc fixed.Int26_6 +} + +const inf = 1e6 + +func (l *segmentIterator) Next() (text.Layout, image.Point, bool, int, + image.Point, bool) { + for l.pos.Y < len(l.Lines) { + if l.pos.X == 0 { + l.line = l.Lines[l.pos.Y] + + // Calculate X & Y pixel coordinates of left edge of line. We need y + // for the next line, so it's in l, but we only need x here, so it's + // not. + x := align(l.Alignment, l.line.Width, l.Width) + fixed.I(l.Offset.X) + l.y += l.prevDesc + l.line.Ascent + l.prevDesc = l.line.Descent + // Align baseline and line start to the pixel grid. + l.off = fixed.Point26_6{X: fixed.I(x.Floor()), + Y: fixed.I(l.y.Ceil())} + l.y = l.off.Y + l.off.Y += fixed.I(l.Offset.Y) + if (l.off.Y + l.line.Bounds.Min.Y).Floor() > l.Clip.Max.Y { + break + } + + if (l.off.Y + l.line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y { + // This line is outside/before the clip area; go on to the next line. + l.pos.Y++ + continue + } + + // Copy the line's Layout, since we slice it up later. + l.layout = l.line.Layout + + // Find the left edge of the text visible in the l.Clip clipping + // area. + for len(l.layout.Advances) > 0 { + _, n := utf8.DecodeRuneInString(l.layout.Text) + adv := l.layout.Advances[0] + if (l.off.X + adv + l.line.Bounds.Max.X - l.line.Width).Ceil() >= l.Clip.Min.X { + break + } + l.off.X += adv + l.layout.Text = l.layout.Text[n:] + l.layout.Advances = l.layout.Advances[1:] + l.pos.X++ + } + } + + selected := l.inSelection() + endx := l.off.X + rune := 0 + nextLine := true + retLayout := l.layout + for n := range l.layout.Text { + selChanged := selected != l.inSelection() + beyondClipEdge := (endx + l.line.Bounds.Min.X).Floor() > l.Clip.Max.X + if selChanged || beyondClipEdge { + retLayout.Advances = l.layout.Advances[:rune] + retLayout.Text = l.layout.Text[:n] + if selChanged { + // Save the rest of the line + l.layout.Advances = l.layout.Advances[rune:] + l.layout.Text = l.layout.Text[n:] + nextLine = false + } + break + } + endx += l.layout.Advances[rune] + rune++ + l.pos.X++ + } + offFloor := image.Point{X: l.off.X.Floor(), Y: l.off.Y.Floor()} + + // Calculate the width & height if the returned text. + // + // If there's a better way to do this, I'm all ears. + var d fixed.Int26_6 + for _, adv := range retLayout.Advances { + d += adv + } + size := image.Point{ + X: d.Ceil(), + Y: (l.line.Ascent + l.line.Descent).Ceil(), + } + + if nextLine { + l.pos.Y++ + l.pos.X = 0 + } else { + l.off.X = endx + } + + return retLayout, offFloor, selected, l.prevDesc.Ceil() - size.Y, size, true + } + return text.Layout{}, image.Point{}, false, 0, image.Point{}, false +} + +func (l *segmentIterator) inSelection() bool { + return l.startSel.LessOrEqual(l.pos) && + l.pos.Less(l.endSel) +} + +func (p1 screenPos) LessOrEqual(p2 screenPos) bool { + return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X <= p2.X) +} + +func (p1 screenPos) Less(p2 screenPos) bool { + return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X < p2.X) +} + +func (l Label) Layout(gtx layout.Context, s text.Shaper, font text.Font, + size unit.Value, txt string) layout.Dimensions { + cs := gtx.Constraints + textSize := fixed.I(gtx.Px(size)) + lines := s.LayoutString(font, textSize, cs.Max.X, txt) + if max := l.MaxLines; max > 0 && len(lines) > max { + lines = lines[:max] + } + dims := linesDimens(lines) + dims.Size = cs.Constrain(dims.Size) + cl := textPadding(lines) + cl.Max = cl.Max.Add(dims.Size) + it := segmentIterator{ + Lines: lines, + Clip: cl, + Alignment: l.Alignment, + Width: dims.Size.X, + } + for { + l, off, _, _, _, ok := it.Next() + if !ok { + break + } + stack := op.Save(gtx.Ops) + op.Offset(layout.FPt(off)).Add(gtx.Ops) + s.Shape(font, textSize, l).Add(gtx.Ops) + clip.Rect(cl.Sub(off)).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + } + return dims +} + +func textPadding(lines []text.Line) (padding image.Rectangle) { + if len(lines) == 0 { + return + } + first := lines[0] + if d := first.Ascent + first.Bounds.Min.Y; d < 0 { + padding.Min.Y = d.Ceil() + } + last := lines[len(lines)-1] + if d := last.Bounds.Max.Y - last.Descent; d > 0 { + padding.Max.Y = d.Ceil() + } + if d := first.Bounds.Min.X; d < 0 { + padding.Min.X = d.Ceil() + } + if d := first.Bounds.Max.X - first.Width; d > 0 { + padding.Max.X = d.Ceil() + } + return +} + +func linesDimens(lines []text.Line) layout.Dimensions { + var width fixed.Int26_6 + var h int + var baseline int + if len(lines) > 0 { + baseline = lines[0].Ascent.Ceil() + var prevDesc fixed.Int26_6 + for _, l := range lines { + h += (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + if l.Width > width { + width = l.Width + } + } + h += lines[len(lines)-1].Descent.Ceil() + } + w := width.Ceil() + return layout.Dimensions{ + Size: image.Point{ + X: w, + Y: h, + }, + Baseline: h - baseline, + } +} + +func align(align text.Alignment, width fixed.Int26_6, + maxWidth int) fixed.Int26_6 { + mw := fixed.I(maxWidth) + switch align { + case text.Middle: + return fixed.I(((mw - width) / 2).Floor()) + case text.End: + return fixed.I((mw - width).Floor()) + case text.Start: + return 0 + default: + panic(fmt.Errorf("unknown alignment %v", align)) + } +} diff --git a/gio/widget/material/button.go b/gio/widget/material/button.go new file mode 100644 index 0000000..78bfcf2 --- /dev/null +++ b/gio/widget/material/button.go @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + "math" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +type ButtonStyle struct { + Text string + // Color is the text color. + Color color.NRGBA + Font text.Font + TextSize unit.Value + Background color.NRGBA + CornerRadius unit.Value + Inset layout.Inset + Button *widget.Clickable + shaper text.Shaper +} + +type ButtonLayoutStyle struct { + Background color.NRGBA + CornerRadius unit.Value + Button *widget.Clickable +} + +type IconButtonStyle struct { + Background color.NRGBA + // Color is the icon color. + Color color.NRGBA + Icon *widget.Icon + // Size is the icon size. + Size unit.Value + Inset layout.Inset + Button *widget.Clickable +} + +func Button(th *Theme, button *widget.Clickable, txt string) ButtonStyle { + return ButtonStyle{ + Text: txt, + Color: th.Palette.ContrastFg, + CornerRadius: unit.Dp(4), + Background: th.Palette.ContrastBg, + TextSize: th.TextSize.Scale(14.0 / 16.0), + Inset: layout.Inset{ + Top: unit.Dp(10), Bottom: unit.Dp(10), + Left: unit.Dp(12), Right: unit.Dp(12), + }, + Button: button, + shaper: th.Shaper, + } +} + +func ButtonLayout(th *Theme, button *widget.Clickable) ButtonLayoutStyle { + return ButtonLayoutStyle{ + Button: button, + Background: th.Palette.ContrastBg, + CornerRadius: unit.Dp(4), + } +} + +func IconButton(th *Theme, button *widget.Clickable, + icon *widget.Icon) IconButtonStyle { + return IconButtonStyle{ + Background: th.Palette.ContrastBg, + Color: th.Palette.ContrastFg, + Icon: icon, + Size: unit.Dp(24), + Inset: layout.UniformInset(unit.Dp(12)), + Button: button, + } +} + +// Clickable lays out a rectangular clickable widget without further +// decoration. +func Clickable(gtx layout.Context, button *widget.Clickable, + w layout.Widget) layout.Dimensions { + return layout.Stack{}.Layout(gtx, + layout.Expanded(button.Layout), + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + clip.Rect{Max: gtx.Constraints.Min}.Add(gtx.Ops) + for _, c := range button.History() { + drawInk(gtx, c) + } + return layout.Dimensions{Size: gtx.Constraints.Min} + }), + layout.Stacked(w), + ) +} + +func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions { + return ButtonLayoutStyle{ + Background: b.Background, + CornerRadius: b.CornerRadius, + Button: b.Button, + }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + paint.ColorOp{Color: b.Color}.Add(gtx.Ops) + return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper, + b.Font, b.TextSize, b.Text) + }) + }) +} + +func (b ButtonLayoutStyle) Layout(gtx layout.Context, + w layout.Widget) layout.Dimensions { + min := gtx.Constraints.Min + return layout.Stack{Alignment: layout.Center}.Layout(gtx, + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + rr := float32(gtx.Px(b.CornerRadius)) + clip.UniformRRect(f32.Rectangle{Max: f32.Point{ + X: float32(gtx.Constraints.Min.X), + Y: float32(gtx.Constraints.Min.Y), + }}, rr).Add(gtx.Ops) + background := b.Background + switch { + case gtx.Queue == nil: + background = f32color.Disabled(b.Background) + case b.Button.Hovered(): + background = f32color.Hovered(b.Background) + } + paint.Fill(gtx.Ops, background) + for _, c := range b.Button.History() { + drawInk(gtx, c) + } + return layout.Dimensions{Size: gtx.Constraints.Min} + }), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + gtx.Constraints.Min = min + return layout.Center.Layout(gtx, w) + }), + layout.Expanded(b.Button.Layout), + ) +} + +func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions { + return layout.Stack{Alignment: layout.Center}.Layout(gtx, + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + sizex, sizey := gtx.Constraints.Min.X, gtx.Constraints.Min.Y + sizexf, sizeyf := float32(sizex), float32(sizey) + rr := (sizexf + sizeyf) * .25 + clip.UniformRRect(f32.Rectangle{ + Max: f32.Point{X: sizexf, Y: sizeyf}, + }, rr).Add(gtx.Ops) + background := b.Background + switch { + case gtx.Queue == nil: + background = f32color.Disabled(b.Background) + case b.Button.Hovered(): + background = f32color.Hovered(b.Background) + } + paint.Fill(gtx.Ops, background) + for _, c := range b.Button.History() { + drawInk(gtx, c) + } + return layout.Dimensions{Size: gtx.Constraints.Min} + }), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + return b.Inset.Layout(gtx, + func(gtx layout.Context) layout.Dimensions { + size := gtx.Px(b.Size) + if b.Icon != nil { + b.Icon.Color = b.Color + b.Icon.Layout(gtx, unit.Px(float32(size))) + } + return layout.Dimensions{ + Size: image.Point{X: size, Y: size}, + } + }) + }), + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + pointer.Ellipse(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops) + return b.Button.Layout(gtx) + }), + ) +} + +func drawInk(gtx layout.Context, c widget.Press) { + // duration is the number of seconds for the + // completed animation: expand while fading in, then + // out. + const ( + expandDuration = float32(0.5) + fadeDuration = float32(0.9) + ) + + now := gtx.Now + + t := float32(now.Sub(c.Start).Seconds()) + + end := c.End + if end.IsZero() { + // If the press hasn't ended, don't fade-out. + end = now + } + + endt := float32(end.Sub(c.Start).Seconds()) + + // Compute the fade-in/out position in [0;1]. + var alphat float32 + { + var haste float32 + if c.Cancelled { + // If the press was cancelled before the inkwell + // was fully faded in, fast forward the animation + // to match the fade-out. + if h := 0.5 - endt/fadeDuration; h > 0 { + haste = h + } + } + // Fade in. + half1 := t/fadeDuration + haste + if half1 > 0.5 { + half1 = 0.5 + } + + // Fade out. + half2 := float32(now.Sub(end).Seconds()) + half2 /= fadeDuration + half2 += haste + if half2 > 0.5 { + // Too old. + return + } + + alphat = half1 + half2 + } + + // Compute the expand position in [0;1]. + sizet := t + if c.Cancelled { + // Freeze expansion of cancelled presses. + sizet = endt + } + sizet /= expandDuration + + // Animate only ended presses, and presses that are fading in. + if !c.End.IsZero() || sizet <= 1.0 { + op.InvalidateOp{}.Add(gtx.Ops) + } + + if sizet > 1.0 { + sizet = 1.0 + } + + if alphat > .5 { + // Start fadeout after half the animation. + alphat = 1.0 - alphat + } + // Twice the speed to attain fully faded in at 0.5. + t2 := alphat * 2 + // BeziĆ©r ease-in curve. + alphaBezier := t2 * t2 * (3.0 - 2.0*t2) + sizeBezier := sizet * sizet * (3.0 - 2.0*sizet) + size := float32(gtx.Constraints.Min.X) + if h := float32(gtx.Constraints.Min.Y); h > size { + size = h + } + // Cover the entire constraints min rectangle. + size *= 2 * float32(math.Sqrt(2)) + // Apply curve values to size and color. + size *= sizeBezier + alpha := 0.7 * alphaBezier + const col = 0.8 + ba, bc := byte(alpha*0xff), byte(col*0xff) + defer op.Save(gtx.Ops).Load() + rgba := f32color.MulAlpha(color.NRGBA{A: 0xff, R: bc, G: bc, B: bc}, ba) + ink := paint.ColorOp{Color: rgba} + ink.Add(gtx.Ops) + rr := size * .5 + op.Offset(c.Position.Add(f32.Point{ + X: -rr, + Y: -rr, + })).Add(gtx.Ops) + clip.UniformRRect(f32.Rectangle{Max: f32.Pt(size, size)}, rr).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) +} diff --git a/gio/widget/material/checkable.go b/gio/widget/material/checkable.go new file mode 100644 index 0000000..e895b81 --- /dev/null +++ b/gio/widget/material/checkable.go @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +type checkable struct { + Label string + Color color.NRGBA + Font text.Font + TextSize unit.Value + IconColor color.NRGBA + Size unit.Value + shaper text.Shaper + checkedStateIcon *widget.Icon + uncheckedStateIcon *widget.Icon +} + +func (c *checkable) layout(gtx layout.Context, + checked, hovered bool) layout.Dimensions { + var icon *widget.Icon + if checked { + icon = c.checkedStateIcon + } else { + icon = c.uncheckedStateIcon + } + + dims := layout.Flex{Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Stack{Alignment: layout.Center}.Layout(gtx, + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + size := gtx.Px(c.Size) * 4 / 3 + dims := layout.Dimensions{ + Size: image.Point{X: size, Y: size}, + } + if !hovered { + return dims + } + + background := f32color.MulAlpha(c.IconColor, 70) + + radius := float32(size) / 2 + paint.FillShape(gtx.Ops, background, + clip.Circle{ + Center: f32.Point{X: radius, Y: radius}, + Radius: radius, + }.Op(gtx.Ops)) + + return dims + }), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(2)).Layout(gtx, + func(gtx layout.Context) layout.Dimensions { + size := gtx.Px(c.Size) + icon.Color = c.IconColor + if gtx.Queue == nil { + icon.Color = f32color.Disabled(icon.Color) + } + icon.Layout(gtx, unit.Px(float32(size))) + return layout.Dimensions{ + Size: image.Point{X: size, Y: size}, + } + }) + }), + ) + }), + + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(2)).Layout(gtx, + func(gtx layout.Context) layout.Dimensions { + paint.ColorOp{Color: c.Color}.Add(gtx.Ops) + return widget.Label{}.Layout(gtx, c.shaper, c.Font, + c.TextSize, c.Label) + }) + }), + ) + pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops) + return dims +} diff --git a/gio/widget/material/checkbox.go b/gio/widget/material/checkbox.go new file mode 100644 index 0000000..2483cfe --- /dev/null +++ b/gio/widget/material/checkbox.go @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "realy.lol/gio/layout" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +type CheckBoxStyle struct { + checkable + CheckBox *widget.Bool +} + +func CheckBox(th *Theme, checkBox *widget.Bool, label string) CheckBoxStyle { + return CheckBoxStyle{ + CheckBox: checkBox, + checkable: checkable{ + Label: label, + Color: th.Palette.Fg, + IconColor: th.Palette.ContrastBg, + TextSize: th.TextSize.Scale(14.0 / 16.0), + Size: unit.Dp(26), + shaper: th.Shaper, + checkedStateIcon: th.Icon.CheckBoxChecked, + uncheckedStateIcon: th.Icon.CheckBoxUnchecked, + }, + } +} + +// Layout updates the checkBox and displays it. +func (c CheckBoxStyle) Layout(gtx layout.Context) layout.Dimensions { + dims := c.layout(gtx, c.CheckBox.Value, c.CheckBox.Hovered()) + gtx.Constraints.Min = dims.Size + c.CheckBox.Layout(gtx) + return dims +} diff --git a/gio/widget/material/doc.go b/gio/widget/material/doc.go new file mode 100644 index 0000000..715f5a0 --- /dev/null +++ b/gio/widget/material/doc.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package material implements the Material design. +// +// To maximize reusability and visual flexibility, user interface controls are +// split into two parts: the stateful widget and the stateless drawing of it. +// +// For example, widget.Clickable encapsulates the state and event +// handling of all clickable areas, while the Theme is responsible to +// draw a specific area, for example a button. +// +// This snippet defines a button that prints a message when clicked: +// +// var gtx layout.Context +// button := new(widget.Clickable) +// +// for button.Clicked(gtx) { +// fmt.Println("Clicked!") +// } +// +// Use a Theme to draw the button: +// +// theme := material.NewTheme(...) +// +// material.Button(theme, "Click me!").Layout(gtx, button) +// +// Customization +// +// Quite often, a program needs to customize the theme-provided defaults. Several +// options are available, depending on the nature of the change. +// +// Mandatory parameters: Some parameters are not part of the widget state but +// have no obvious default. In the program above, the button text is a +// parameter to the Theme.Button method. +// +// Theme-global parameters: For changing the look of all widgets drawn with a +// particular theme, adjust the `Theme` fields: +// +// theme.Color.Primary = color.NRGBA{...} +// +// Widget-local parameters: For changing the look of a particular widget, +// adjust the widget specific theme object: +// +// btn := material.Button(theme, "Click me!") +// btn.Font.Style = text.Italic +// btn.Layout(gtx, button) +// +// Widget variants: A widget can have several distinct representations even +// though the underlying state is the same. A widget.Clickable can be drawn as a +// round icon button: +// +// icon := material.NewIcon(...) +// +// material.IconButton(theme, icon).Layout(gtx, button) +// +// Specialized widgets: Theme both define a generic Label method +// that takes a text size, and specialized methods for standard text +// sizes such as Theme.H1 and Theme.Body2. +package material diff --git a/gio/widget/material/editor.go b/gio/widget/material/editor.go new file mode 100644 index 0000000..93d02cf --- /dev/null +++ b/gio/widget/material/editor.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image/color" + + "realy.lol/gio/internal/f32color" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +type EditorStyle struct { + Font text.Font + TextSize unit.Value + // Color is the text color. + Color color.NRGBA + // Hint contains the text displayed when the editor is empty. + Hint string + // HintColor is the color of hint text. + HintColor color.NRGBA + // SelectionColor is the color of the background for selected text. + SelectionColor color.NRGBA + Editor *widget.Editor + + shaper text.Shaper +} + +func Editor(th *Theme, editor *widget.Editor, hint string) EditorStyle { + return EditorStyle{ + Editor: editor, + TextSize: th.TextSize, + Color: th.Palette.Fg, + shaper: th.Shaper, + Hint: hint, + HintColor: f32color.MulAlpha(th.Palette.Fg, 0xbb), + SelectionColor: f32color.MulAlpha(th.Palette.ContrastBg, 0x60), + } +} + +func (e EditorStyle) Layout(gtx layout.Context) layout.Dimensions { + defer op.Save(gtx.Ops).Load() + macro := op.Record(gtx.Ops) + paint.ColorOp{Color: e.HintColor}.Add(gtx.Ops) + var maxlines int + if e.Editor.SingleLine { + maxlines = 1 + } + tl := widget.Label{Alignment: e.Editor.Alignment, MaxLines: maxlines} + dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint) + call := macro.Stop() + if w := dims.Size.X; gtx.Constraints.Min.X < w { + gtx.Constraints.Min.X = w + } + if h := dims.Size.Y; gtx.Constraints.Min.Y < h { + gtx.Constraints.Min.Y = h + } + dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize) + disabled := gtx.Queue == nil + if e.Editor.Len() > 0 { + paint.ColorOp{Color: blendDisabledColor(disabled, + e.SelectionColor)}.Add(gtx.Ops) + e.Editor.PaintSelection(gtx) + paint.ColorOp{Color: blendDisabledColor(disabled, e.Color)}.Add(gtx.Ops) + e.Editor.PaintText(gtx) + } else { + call.Add(gtx.Ops) + } + if !disabled { + paint.ColorOp{Color: e.Color}.Add(gtx.Ops) + e.Editor.PaintCaret(gtx) + } + return dims +} + +func blendDisabledColor(disabled bool, c color.NRGBA) color.NRGBA { + if disabled { + return f32color.Disabled(c) + } + return c +} diff --git a/gio/widget/material/label.go b/gio/widget/material/label.go new file mode 100644 index 0000000..80c4b02 --- /dev/null +++ b/gio/widget/material/label.go @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image/color" + + "realy.lol/gio/layout" + "realy.lol/gio/op/paint" + "realy.lol/gio/text" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +type LabelStyle struct { + // Face defines the text style. + Font text.Font + // Color is the text color. + Color color.NRGBA + // Alignment specify the text alignment. + Alignment text.Alignment + // MaxLines limits the number of lines. Zero means no limit. + MaxLines int + Text string + TextSize unit.Value + + shaper text.Shaper +} + +func H1(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(96.0/16.0), txt) +} + +func H2(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(60.0/16.0), txt) +} + +func H3(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(48.0/16.0), txt) +} + +func H4(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(34.0/16.0), txt) +} + +func H5(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(24.0/16.0), txt) +} + +func H6(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(20.0/16.0), txt) +} + +func Body1(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize, txt) +} + +func Body2(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(14.0/16.0), txt) +} + +func Caption(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(12.0/16.0), txt) +} + +func Label(th *Theme, size unit.Value, txt string) LabelStyle { + return LabelStyle{ + Text: txt, + Color: th.Palette.Fg, + TextSize: size, + shaper: th.Shaper, + } +} + +func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions { + paint.ColorOp{Color: l.Color}.Add(gtx.Ops) + tl := widget.Label{Alignment: l.Alignment, MaxLines: l.MaxLines} + return tl.Layout(gtx, l.shaper, l.Font, l.TextSize, l.Text) +} diff --git a/gio/widget/material/loader.go b/gio/widget/material/loader.go new file mode 100644 index 0000000..77afede --- /dev/null +++ b/gio/widget/material/loader.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + "math" + "time" + + "realy.lol/gio/f32" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" +) + +type LoaderStyle struct { + Color color.NRGBA +} + +func Loader(th *Theme) LoaderStyle { + return LoaderStyle{ + Color: th.Palette.ContrastBg, + } +} + +func (l LoaderStyle) Layout(gtx layout.Context) layout.Dimensions { + diam := gtx.Constraints.Min.X + if minY := gtx.Constraints.Min.Y; minY > diam { + diam = minY + } + if diam == 0 { + diam = gtx.Px(unit.Dp(24)) + } + sz := gtx.Constraints.Constrain(image.Pt(diam, diam)) + radius := float64(sz.X) * .5 + defer op.Save(gtx.Ops).Load() + op.Offset(f32.Pt(float32(radius), float32(radius))).Add(gtx.Ops) + + dt := (time.Duration(gtx.Now.UnixNano()) % (time.Second)).Seconds() + startAngle := dt * math.Pi * 2 + endAngle := startAngle + math.Pi*1.5 + + clipLoader(gtx.Ops, startAngle, endAngle, radius) + paint.ColorOp{ + Color: l.Color, + }.Add(gtx.Ops) + op.Offset(f32.Pt(-float32(radius), -float32(radius))).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + op.InvalidateOp{}.Add(gtx.Ops) + return layout.Dimensions{ + Size: sz, + } +} + +func clipLoader(ops *op.Ops, startAngle, endAngle, radius float64) { + const thickness = .25 + + var ( + width = float32(radius * thickness) + delta = float32(endAngle - startAngle) + + vy, vx = math.Sincos(startAngle) + + pen = f32.Pt(float32(vx), float32(vy)).Mul(float32(radius)) + center = f32.Pt(0, 0).Sub(pen) + + p clip.Path + ) + + p.Begin(ops) + p.Move(pen) + p.Arc(center, center, delta) + clip.Stroke{ + Path: p.End(), + Style: clip.StrokeStyle{ + Width: width, + Cap: clip.FlatCap, + }, + }.Op().Add(ops) +} diff --git a/gio/widget/material/progressbar.go b/gio/widget/material/progressbar.go new file mode 100644 index 0000000..98ae4cf --- /dev/null +++ b/gio/widget/material/progressbar.go @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/layout" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" +) + +type ProgressBarStyle struct { + Color color.NRGBA + TrackColor color.NRGBA + Progress float32 +} + +func ProgressBar(th *Theme, progress float32) ProgressBarStyle { + return ProgressBarStyle{ + Progress: progress, + Color: th.Palette.ContrastBg, + TrackColor: f32color.MulAlpha(th.Palette.Fg, 0x88), + } +} + +func (p ProgressBarStyle) Layout(gtx layout.Context) layout.Dimensions { + shader := func(width float32, color color.NRGBA) layout.Dimensions { + maxHeight := unit.Dp(4) + rr := float32(gtx.Px(unit.Dp(2))) + + d := image.Point{X: int(width), Y: gtx.Px(maxHeight)} + + height := float32(gtx.Px(maxHeight)) + clip.UniformRRect(f32.Rectangle{Max: f32.Pt(width, height)}, + rr).Add(gtx.Ops) + paint.ColorOp{Color: color}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + + return layout.Dimensions{Size: d} + } + + progressBarWidth := float32(gtx.Constraints.Max.X) + return layout.Stack{Alignment: layout.W}.Layout(gtx, + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + return shader(progressBarWidth, p.TrackColor) + }), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + fillWidth := progressBarWidth * clamp1(p.Progress) + fillColor := p.Color + if gtx.Queue == nil { + fillColor = f32color.Disabled(fillColor) + } + return shader(fillWidth, fillColor) + }), + ) +} + +// clamp1 limits v to range [0..1]. +func clamp1(v float32) float32 { + if v >= 1 { + return 1 + } else if v <= 0 { + return 0 + } else { + return v + } +} diff --git a/gio/widget/material/radiobutton.go b/gio/widget/material/radiobutton.go new file mode 100644 index 0000000..79dd763 --- /dev/null +++ b/gio/widget/material/radiobutton.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "realy.lol/gio/layout" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +type RadioButtonStyle struct { + checkable + Key string + Group *widget.Enum +} + +// RadioButton returns a RadioButton with a label. The key specifies +// the value for the Enum. +func RadioButton(th *Theme, group *widget.Enum, + key, label string) RadioButtonStyle { + return RadioButtonStyle{ + Group: group, + checkable: checkable{ + Label: label, + + Color: th.Palette.Fg, + IconColor: th.Palette.ContrastBg, + TextSize: th.TextSize.Scale(14.0 / 16.0), + Size: unit.Dp(26), + shaper: th.Shaper, + checkedStateIcon: th.Icon.RadioChecked, + uncheckedStateIcon: th.Icon.RadioUnchecked, + }, + Key: key, + } +} + +// Layout updates enum and displays the radio button. +func (r RadioButtonStyle) Layout(gtx layout.Context) layout.Dimensions { + hovered, hovering := r.Group.Hovered() + dims := r.layout(gtx, r.Group.Value == r.Key, hovering && hovered == r.Key) + gtx.Constraints.Min = dims.Size + r.Group.Layout(gtx, r.Key) + return dims +} diff --git a/gio/widget/material/slider.go b/gio/widget/material/slider.go new file mode 100644 index 0000000..e038d75 --- /dev/null +++ b/gio/widget/material/slider.go @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +// Slider is for selecting a value in a range. +func Slider(th *Theme, float *widget.Float, min, max float32) SliderStyle { + return SliderStyle{ + Min: min, + Max: max, + Color: th.Palette.ContrastBg, + Float: float, + FingerSize: th.FingerSize, + } +} + +type SliderStyle struct { + Min, Max float32 + Color color.NRGBA + Float *widget.Float + + FingerSize unit.Value +} + +func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions { + thumbRadius := gtx.Px(unit.Dp(6)) + trackWidth := gtx.Px(unit.Dp(2)) + + axis := s.Float.Axis + // Keep a minimum length so that the track is always visible. + minLength := thumbRadius + 3*thumbRadius + thumbRadius + // Try to expand to finger size, but only if the constraints + // allow for it. + touchSizePx := min(gtx.Px(s.FingerSize), + axis.Convert(gtx.Constraints.Max).Y) + sizeMain := max(axis.Convert(gtx.Constraints.Min).X, minLength) + sizeCross := max(2*thumbRadius, touchSizePx) + size := axis.Convert(image.Pt(sizeMain, sizeCross)) + + st := op.Save(gtx.Ops) + o := axis.Convert(image.Pt(thumbRadius, 0)) + op.Offset(layout.FPt(o)).Add(gtx.Ops) + gtx.Constraints.Min = axis.Convert(image.Pt(sizeMain-2*thumbRadius, + sizeCross)) + s.Float.Layout(gtx, thumbRadius, s.Min, s.Max) + gtx.Constraints.Min = gtx.Constraints.Min.Add(axis.Convert(image.Pt(0, + sizeCross))) + thumbPos := thumbRadius + int(s.Float.Pos()) + st.Load() + + color := s.Color + if gtx.Queue == nil { + color = f32color.Disabled(color) + } + + // Draw track before thumb. + st = op.Save(gtx.Ops) + track := image.Rectangle{ + Min: axis.Convert(image.Pt(thumbRadius, sizeCross/2-trackWidth/2)), + Max: axis.Convert(image.Pt(thumbPos, sizeCross/2+trackWidth/2)), + } + clip.Rect(track).Add(gtx.Ops) + paint.Fill(gtx.Ops, color) + st.Load() + + // Draw track after thumb. + st = op.Save(gtx.Ops) + track = image.Rectangle{ + Min: axis.Convert(image.Pt(thumbPos, axis.Convert(track.Min).Y)), + Max: axis.Convert(image.Pt(sizeMain-thumbRadius, + axis.Convert(track.Max).Y)), + } + clip.Rect(track).Add(gtx.Ops) + paint.Fill(gtx.Ops, f32color.MulAlpha(color, 96)) + st.Load() + + // Draw thumb. + pt := axis.Convert(image.Pt(thumbPos, sizeCross/2)) + paint.FillShape(gtx.Ops, color, + clip.Circle{ + Center: f32.Point{X: float32(pt.X), Y: float32(pt.Y)}, + Radius: float32(thumbRadius), + }.Op(gtx.Ops)) + + return layout.Dimensions{Size: size} +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/gio/widget/material/switch.go b/gio/widget/material/switch.go new file mode 100644 index 0000000..14a4134 --- /dev/null +++ b/gio/widget/material/switch.go @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + + "realy.lol/gio/f32" + "realy.lol/gio/internal/f32color" + "realy.lol/gio/io/pointer" + "realy.lol/gio/layout" + "realy.lol/gio/op" + "realy.lol/gio/op/clip" + "realy.lol/gio/op/paint" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +type SwitchStyle struct { + Color struct { + Enabled color.NRGBA + Disabled color.NRGBA + Track color.NRGBA + } + Switch *widget.Bool +} + +// Switch is for selecting a boolean value. +func Switch(th *Theme, swtch *widget.Bool) SwitchStyle { + sw := SwitchStyle{ + Switch: swtch, + } + sw.Color.Enabled = th.Palette.ContrastBg + sw.Color.Disabled = th.Palette.Bg + sw.Color.Track = f32color.MulAlpha(th.Palette.Fg, 0x88) + return sw +} + +// Layout updates the switch and displays it. +func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions { + trackWidth := gtx.Px(unit.Dp(36)) + trackHeight := gtx.Px(unit.Dp(16)) + thumbSize := gtx.Px(unit.Dp(20)) + trackOff := float32(thumbSize-trackHeight) * .5 + + // Draw track. + stack := op.Save(gtx.Ops) + trackCorner := float32(trackHeight) / 2 + trackRect := f32.Rectangle{Max: f32.Point{ + X: float32(trackWidth), + Y: float32(trackHeight), + }} + col := s.Color.Disabled + if s.Switch.Value { + col = s.Color.Enabled + } + if gtx.Queue == nil { + col = f32color.Disabled(col) + } + trackColor := s.Color.Track + op.Offset(f32.Point{Y: trackOff}).Add(gtx.Ops) + clip.UniformRRect(trackRect, trackCorner).Add(gtx.Ops) + paint.ColorOp{Color: trackColor}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + + // Draw thumb ink. + stack = op.Save(gtx.Ops) + inkSize := gtx.Px(unit.Dp(44)) + rr := float32(inkSize) * .5 + inkOff := f32.Point{ + X: float32(trackWidth)*.5 - rr, + Y: -rr + float32(trackHeight)*.5 + trackOff, + } + op.Offset(inkOff).Add(gtx.Ops) + gtx.Constraints.Min = image.Pt(inkSize, inkSize) + clip.UniformRRect(f32.Rectangle{Max: layout.FPt(gtx.Constraints.Min)}, + rr).Add(gtx.Ops) + for _, p := range s.Switch.History() { + drawInk(gtx, p) + } + stack.Load() + + // Compute thumb offset and color. + stack = op.Save(gtx.Ops) + if s.Switch.Value { + off := trackWidth - thumbSize + op.Offset(f32.Point{X: float32(off)}).Add(gtx.Ops) + } + + thumbRadius := float32(thumbSize) / 2 + + // Draw hover. + if s.Switch.Hovered() { + r := 1.7 * thumbRadius + background := f32color.MulAlpha(s.Color.Enabled, 70) + paint.FillShape(gtx.Ops, background, + clip.Circle{ + Center: f32.Point{X: thumbRadius, Y: thumbRadius}, + Radius: r, + }.Op(gtx.Ops)) + } + + // Draw thumb shadow, a translucent disc slightly larger than the + // thumb itself. + // Center shadow horizontally and slightly adjust its Y. + paint.FillShape(gtx.Ops, argb(0x55000000), + clip.Circle{ + Center: f32.Point{X: thumbRadius, Y: thumbRadius + .25}, + Radius: thumbRadius + 1, + }.Op(gtx.Ops)) + + // Draw thumb. + paint.FillShape(gtx.Ops, col, + clip.Circle{ + Center: f32.Point{X: thumbRadius, Y: thumbRadius}, + Radius: thumbRadius, + }.Op(gtx.Ops)) + + // Set up click area. + stack = op.Save(gtx.Ops) + clickSize := gtx.Px(unit.Dp(40)) + clickOff := f32.Point{ + X: (float32(trackWidth) - float32(clickSize)) * .5, + Y: (float32(trackHeight)-float32(clickSize))*.5 + trackOff, + } + op.Offset(clickOff).Add(gtx.Ops) + sz := image.Pt(clickSize, clickSize) + pointer.Ellipse(image.Rectangle{Max: sz}).Add(gtx.Ops) + gtx.Constraints.Min = sz + s.Switch.Layout(gtx) + stack.Load() + + dims := image.Point{X: trackWidth, Y: thumbSize} + return layout.Dimensions{Size: dims} +} diff --git a/gio/widget/material/theme.go b/gio/widget/material/theme.go new file mode 100644 index 0000000..e19f7df --- /dev/null +++ b/gio/widget/material/theme.go @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image/color" + + "golang.org/x/exp/shiny/materialdesign/icons" + + "realy.lol/gio/text" + "realy.lol/gio/unit" + "realy.lol/gio/widget" +) + +// Palette contains the minimal set of colors that a widget may need to +// draw itself. +type Palette struct { + // Bg is the background color atop which content is currently being + // drawn. + Bg color.NRGBA + + // Fg is a color suitable for drawing on top of Bg. + Fg color.NRGBA + + // ContrastBg is a color used to draw attention to active, + // important, interactive widgets such as buttons. + ContrastBg color.NRGBA + + // ContrastFg is a color suitable for content drawn on top of + // ContrastBg. + ContrastFg color.NRGBA +} + +type Theme struct { + Shaper text.Shaper + Palette + TextSize unit.Value + Icon struct { + CheckBoxChecked *widget.Icon + CheckBoxUnchecked *widget.Icon + RadioChecked *widget.Icon + RadioUnchecked *widget.Icon + } + + // FingerSize is the minimum touch target size. + FingerSize unit.Value +} + +func NewTheme(fontCollection []text.FontFace) *Theme { + t := &Theme{ + Shaper: text.NewCache(fontCollection), + } + t.Palette = Palette{ + Fg: rgb(0x000000), + Bg: rgb(0xffffff), + ContrastBg: rgb(0x3f51b5), + ContrastFg: rgb(0xffffff), + } + t.TextSize = unit.Sp(16) + + t.Icon.CheckBoxChecked = mustIcon(widget.NewIcon(icons.ToggleCheckBox)) + t.Icon.CheckBoxUnchecked = mustIcon(widget.NewIcon(icons.ToggleCheckBoxOutlineBlank)) + t.Icon.RadioChecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonChecked)) + t.Icon.RadioUnchecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonUnchecked)) + + // 38dp is on the lower end of possible finger size. + t.FingerSize = unit.Dp(38) + + return t +} + +func (t Theme) WithPalette(p Palette) Theme { + t.Palette = p + return t +} + +func mustIcon(ic *widget.Icon, err error) *widget.Icon { + if err != nil { + panic(err) + } + return ic +} + +func rgb(c uint32) color.NRGBA { + return argb(0xff000000 | c) +} + +func argb(c uint32) color.NRGBA { + return color.NRGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), + B: uint8(c)} +} diff --git a/realy/version b/realy/version index db62a50..40d22ac 100644 --- a/realy/version +++ b/realy/version @@ -1 +1 @@ -v24.12.23 \ No newline at end of file +v24.12.24 \ No newline at end of file