diff --git a/base/iox/imagex/wrapjs_js.go b/base/iox/imagex/wrapjs_js.go
index 3b4b2c93a4..97a5107f9d 100644
--- a/base/iox/imagex/wrapjs_js.go
+++ b/base/iox/imagex/wrapjs_js.go
@@ -130,6 +130,12 @@ func (im *JSRGBA) Update() {
im.JS.SetRGBA(im.RGBA)
}
+// note: per https://github.com/whatwg/html/issues/4785 there is no way
+// to actually get the data back from the ImageBitmap :(
+
+// Underlying returns the underlying image data as a local image.RGBA.
+// Note that this may not be possible and a nil will be returned, because
+// there is no way to get the image data back from an ImageBitmap at this point.
func (im *JSRGBA) Underlying() image.Image {
return im.RGBA
}
diff --git a/base/iox/imagex/wrapjs_notjs.go b/base/iox/imagex/wrapjs_notjs.go
index ccaff60e33..39e8836dd3 100644
--- a/base/iox/imagex/wrapjs_notjs.go
+++ b/base/iox/imagex/wrapjs_notjs.go
@@ -13,6 +13,12 @@ import (
"github.com/anthonynsimon/bild/transform"
)
+// JSRGBA is a dummy version of this type so code does not have to be
+// conditionalized.
+type JSRGBA struct {
+ *image.RGBA
+}
+
// WrapJS returns a JavaScript optimized wrapper around the given
// [image.Image] on web, and just returns the image on other platforms.
func WrapJS(src image.Image) image.Image {
diff --git a/colors/gradient/linear.go b/colors/gradient/linear.go
index 285ab7700c..d12b8dd937 100644
--- a/colors/gradient/linear.go
+++ b/colors/gradient/linear.go
@@ -14,11 +14,12 @@ import (
"cogentcore.org/core/math32"
)
-// Linear represents a linear gradient. It implements the [image.Image] interface.
+// Linear represents a linear gradient along linear axis from Start to End.
+// It implements the [image.Image] interface.
type Linear struct { //types:add -setters
Base
- // the starting point of the gradient (x1 and y1 in SVG)
+ // the starting point of the gradient axis (x1 and y1 in SVG).
Start math32.Vector2
// the ending point of the gradient (x2 and y2 in SVG)
@@ -64,16 +65,22 @@ func (l *Linear) Update(opacity float32, box math32.Box2, objTransform math32.Ma
l.rStart = l.Box.Min.Add(sz.Mul(l.Start))
l.rEnd = l.Box.Min.Add(sz.Mul(l.End))
} else {
- l.rStart = l.Transform.MulVector2AsPoint(l.Start)
- l.rEnd = l.Transform.MulVector2AsPoint(l.End)
- l.rStart = objTransform.MulVector2AsPoint(l.rStart)
- l.rEnd = objTransform.MulVector2AsPoint(l.rEnd)
+ l.rStart = l.Transform.MulPoint(l.Start)
+ l.rEnd = l.Transform.MulPoint(l.End)
+ l.rStart = objTransform.MulPoint(l.rStart)
+ l.rEnd = objTransform.MulPoint(l.rEnd)
}
l.distance = l.rEnd.Sub(l.rStart)
l.distanceLengthSquared = l.distance.LengthSquared()
}
+// TransformedAxis returns the Start and End axis points as transformed
+// by the last Update call.
+func (l *Linear) TransformedAxis() (start, end math32.Vector2) {
+ return l.rStart, l.rEnd
+}
+
// At returns the color of the linear gradient at the given point
func (l *Linear) At(x, y int) color.Color {
switch len(l.Stops) {
@@ -85,7 +92,7 @@ func (l *Linear) At(x, y int) color.Color {
pt := math32.Vec2(float32(x)+0.5, float32(y)+0.5)
if l.Units == ObjectBoundingBox {
- pt = l.boxTransform.MulVector2AsPoint(pt)
+ pt = l.boxTransform.MulPoint(pt)
}
df := pt.Sub(l.rStart)
pos := (l.distance.X*df.X + l.distance.Y*df.Y) / l.distanceLengthSquared
diff --git a/colors/gradient/radial.go b/colors/gradient/radial.go
index d87c0b655c..8326ad8777 100644
--- a/colors/gradient/radial.go
+++ b/colors/gradient/radial.go
@@ -74,8 +74,8 @@ func (r *Radial) Update(opacity float32, box math32.Box2, objTransform math32.Ma
rs.SetMul(sz)
} else {
ct := objTransform.Mul(r.Transform)
- c = ct.MulVector2AsPoint(c)
- f = ct.MulVector2AsPoint(f)
+ c = ct.MulPoint(c)
+ f = ct.MulPoint(f)
_, _, phi, sx, sy, _ := ct.Decompose()
r.rotTrans = math32.Rotate2D(phi)
rs.SetMul(math32.Vec2(sx, sy))
@@ -96,6 +96,12 @@ func (r *Radial) Update(opacity float32, box math32.Box2, objTransform math32.Ma
r.rCenter, r.rFocal, r.rRadius = c, f, rs
}
+// TransformedCoords returns the coordinate points as transformed
+// by the last Update call.
+func (r *Radial) TransformedCoords() (center, focal, radius math32.Vector2) {
+ return r.rCenter, r.rFocal, r.rRadius
+}
+
const epsilonF = 1e-5
// At returns the color of the radial gradient at the given point
@@ -112,9 +118,9 @@ func (r *Radial) At(x, y int) color.Color {
// pos is just distance from center scaled by radius
pt := math32.Vec2(float32(x)+0.5, float32(y)+0.5)
if r.Units == ObjectBoundingBox {
- pt = r.boxTransform.MulVector2AsPoint(pt)
+ pt = r.boxTransform.MulPoint(pt)
}
- d := r.rotTrans.MulVector2AsVector(pt.Sub(r.rCenter))
+ d := r.rotTrans.MulVector(pt.Sub(r.rCenter))
pos := math32.Sqrt(d.X*d.X/(r.rRadius.X*r.rRadius.X) + (d.Y*d.Y)/(r.rRadius.Y*r.rRadius.Y))
return r.getColor(pos)
}
@@ -124,9 +130,9 @@ func (r *Radial) At(x, y int) color.Color {
pt := math32.Vec2(float32(x)+0.5, float32(y)+0.5)
if r.Units == ObjectBoundingBox {
- pt = r.boxTransform.MulVector2AsPoint(pt)
+ pt = r.boxTransform.MulPoint(pt)
}
- pt = r.rotTrans.MulVector2AsVector(pt)
+ pt = r.rotTrans.MulVector(pt)
e := pt.Div(r.rRadius)
t1, intersects := rayCircleIntersectionF(e, r.rFocal, r.rCenter, 1)
diff --git a/colors/matcolor/scheme.go b/colors/matcolor/scheme.go
index 61ad40b2db..30f0c2905a 100644
--- a/colors/matcolor/scheme.go
+++ b/colors/matcolor/scheme.go
@@ -117,14 +117,14 @@ func NewLightScheme(p *Palette) Scheme {
Custom: map[string]Accent{},
SurfaceDim: p.Neutral.AbsToneUniform(87),
- Surface: p.Neutral.AbsToneUniform(98),
- SurfaceBright: p.Neutral.AbsToneUniform(98),
+ Surface: p.Neutral.AbsToneUniform(99),
+ SurfaceBright: p.Neutral.AbsToneUniform(99),
SurfaceContainerLowest: p.Neutral.AbsToneUniform(100),
- SurfaceContainerLow: p.Neutral.AbsToneUniform(96),
- SurfaceContainer: p.Neutral.AbsToneUniform(94),
- SurfaceContainerHigh: p.Neutral.AbsToneUniform(92),
- SurfaceContainerHighest: p.Neutral.AbsToneUniform(90),
+ SurfaceContainerLow: p.Neutral.AbsToneUniform(98),
+ SurfaceContainer: p.Neutral.AbsToneUniform(96),
+ SurfaceContainerHigh: p.Neutral.AbsToneUniform(94),
+ SurfaceContainerHighest: p.Neutral.AbsToneUniform(92),
SurfaceVariant: p.NeutralVariant.AbsToneUniform(90),
OnSurface: p.NeutralVariant.AbsToneUniform(10),
@@ -134,7 +134,7 @@ func NewLightScheme(p *Palette) Scheme {
InverseOnSurface: p.Neutral.AbsToneUniform(95),
InversePrimary: p.Primary.AbsToneUniform(80),
- Background: p.Neutral.AbsToneUniform(98),
+ Background: p.Neutral.AbsToneUniform(100),
OnBackground: p.Neutral.AbsToneUniform(10),
Outline: p.NeutralVariant.AbsToneUniform(50),
@@ -182,7 +182,7 @@ func NewDarkScheme(p *Palette) Scheme {
InverseOnSurface: p.Neutral.AbsToneUniform(20),
InversePrimary: p.Primary.AbsToneUniform(40),
- Background: p.Neutral.AbsToneUniform(6),
+ Background: p.Neutral.AbsToneUniform(0),
OnBackground: p.Neutral.AbsToneUniform(90),
Outline: p.NeutralVariant.AbsToneUniform(60),
diff --git a/content/bcontent/page.go b/content/bcontent/page.go
index 83cce85dfd..b3758d2b4b 100644
--- a/content/bcontent/page.go
+++ b/content/bcontent/page.go
@@ -52,8 +52,16 @@ type Page struct {
// DateString is only used for parsing the date from the TOML front matter.
DateString string `toml:"Date" json:"-"`
- // Authors are the optional authors of the page.
- Authors []string
+ // Authors are the optional author(s) of the page.
+ Authors string
+
+ // Affiliations are optional institutional affiliations of the authors.
+ // This is only used for the PDF output.
+ Affiliations string
+
+ // Abstract is an optional abstract.
+ // This is only used for the PDF output.
+ Abstract string
// Draft indicates that the page is a draft and should not be visible on the web.
Draft bool
diff --git a/content/buttons.go b/content/buttons.go
index 1101c453e9..d58a32c10e 100644
--- a/content/buttons.go
+++ b/content/buttons.go
@@ -74,6 +74,12 @@ func (ct *Content) MakeToolbar(p *tree.Plan) {
ct.Scene.MenuSearchDialog("Search", "Search "+core.TheApp.Name())
})
})
+ tree.Add(p, func(w *core.Button) {
+ w.SetText("PDF").SetIcon(icons.PictureAsPdf).SetTooltip("PDF generates and opens / downloads the current page as a printable PDF file. See the Settings/Printer panel (Command+,) for settings.")
+ w.OnClick(func(e events.Event) {
+ ct.PagePDF("pdfs")
+ })
+ })
}
func (ct *Content) MenuSearch(items *[]core.ChooserItem) {
diff --git a/content/content.go b/content/content.go
index 7823ea66ff..4647f7e288 100644
--- a/content/content.go
+++ b/content/content.go
@@ -12,15 +12,14 @@ import (
"bytes"
"cmp"
"fmt"
- "io"
"io/fs"
"net/http"
+ "os"
"path/filepath"
"slices"
"strconv"
"strings"
- "github.com/gomarkdown/markdown/ast"
"golang.org/x/exp/maps"
"cogentcore.org/core/base/errors"
@@ -32,10 +31,10 @@ import (
"cogentcore.org/core/htmlcore"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
- "cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/system"
"cogentcore.org/core/text/csl"
+ "cogentcore.org/core/text/paginate"
"cogentcore.org/core/tree"
)
@@ -103,6 +102,10 @@ type Content struct {
// if any (in kebab-case).
currentHeading string
+ // inPDFRender indicates that it is rendering a PDF now, turning off
+ // elements that are not appropriate for that.
+ inPDFRender bool
+
// The previous and next page, if applicable. They must be stored on this struct
// to avoid stale local closure variables.
prevPage, nextPage *bcontent.Page
@@ -126,6 +129,7 @@ func (ct *Content) Init() {
ct.SetSplits(0.2, 0.8)
ct.Context = htmlcore.NewContext()
+ ct.Context.DelayedImageLoad = false // not useful for content
ct.Context.OpenURL = func(url string) {
ct.Open(url)
}
@@ -138,73 +142,9 @@ func (ct *Content) Init() {
errors.Log(ct.embedPage(ctx))
return true
}
- ct.Context.AttributeHandlers["id"] = func(ctx *htmlcore.Context, w io.Writer, node ast.Node, entering bool, tag, value string) bool {
- if ct.currentPage == nil {
- return false
- }
- lbl := ct.currentPage.SpecialLabel(value)
- ch := node.GetChildren()
- if len(ch) == 2 { // image or table
- if entering {
- sty := htmlcore.MDGetAttr(node, "style")
- if sty != "" {
- if img, ok := ch[1].(*ast.Image); ok {
- htmlcore.MDSetAttr(img, "style", sty)
- delete(node.AsContainer().Attribute.Attrs, "style")
- }
- }
- return false
- }
- cp := "\n
" + lbl + ":"
- if img, ok := ch[1].(*ast.Image); ok {
- // fmt.Printf("Image: %s\n", string(img.Destination))
- // fmt.Printf("Image: %#v\n", img)
- nc := len(img.Children)
- if nc > 0 {
- if txt, ok := img.Children[0].(*ast.Text); ok {
- // fmt.Printf("text: %s\n", string(txt.Literal)) // not formatted!
- cp += " " + string(txt.Literal) // todo: not formatted!
- }
- }
- } else {
- title := htmlcore.MDGetAttr(node, "title")
- if title != "" {
- cp += " " + title
- }
- }
- cp += "
\n"
- w.Write([]byte(cp))
- } else if entering {
- cp := "\n" + lbl + ":"
- title := htmlcore.MDGetAttr(node, "title")
- if title != "" {
- cp += " " + title
- }
- cp += "\n"
- w.Write([]byte(cp))
- // fmt.Println("id:", value, lbl)
- // fmt.Printf("%#v\n", node)
- }
- return false
- }
- ct.Context.AddWidgetHandler(func(w core.Widget) {
- switch x := w.(type) {
- case *core.Text:
- x.Styler(func(s *styles.Style) {
- s.Max.X.Ch(120)
- })
- case *core.Image:
- x.Styler(func(s *styles.Style) {
- s.SetAbilities(true, abilities.Clickable, abilities.DoubleClickable)
- s.Overflow.Set(styles.OverflowAuto)
- })
- x.OnDoubleClick(func(e events.Event) {
- d := core.NewBody("Image")
- core.NewImage(d).SetImage(x.Image)
- d.RunWindowDialog(x)
- })
- }
- })
+ ct.Context.ElementHandlers["pre"] = ct.htmlPreHandler
+ ct.Context.AttributeHandlers["id"] = ct.htmlIDAttributeHandler
+ ct.Context.AddWidgetHandler(ct.widgetHandler)
ct.Maker(func(p *tree.Plan) {
if ct.currentPage == nil {
@@ -224,7 +164,7 @@ func (ct *Content) Init() {
}
})
w.Maker(func(p *tree.Plan) {
- if ct.currentPage.Title != "" {
+ if !ct.inPDFRender && ct.currentPage.Title != "" {
tree.Add(p, func(w *core.Text) {
w.SetType(core.TextDisplaySmall)
w.Updater(func() {
@@ -232,11 +172,11 @@ func (ct *Content) Init() {
})
})
}
- if len(ct.currentPage.Authors) > 0 {
+ if !ct.inPDFRender && len(ct.currentPage.Authors) > 0 {
tree.Add(p, func(w *core.Text) {
w.SetType(core.TextTitleLarge)
w.Updater(func() {
- w.SetText("By " + strcase.FormatList(ct.currentPage.Authors...))
+ w.SetText("By " + ct.currentPage.Authors)
})
})
}
@@ -257,7 +197,9 @@ func (ct *Content) Init() {
errors.Log(ct.loadPage(w))
})
})
- ct.makeBottomButtons(p)
+ if !ct.inPDFRender {
+ ct.makeBottomButtons(p)
+ }
})
})
})
@@ -372,6 +314,12 @@ func (ct *Content) addHistory(pg *bcontent.Page) {
ct.saveWebURL()
}
+// reloadPage reloads the current page
+func (ct *Content) reloadPage() {
+ ct.renderedPage = nil
+ ct.Update()
+}
+
// loadPage loads the current page content into the given frame if it is not already loaded.
func (ct *Content) loadPage(w *core.Frame) error {
if ct.renderedPage == ct.currentPage {
@@ -529,3 +477,71 @@ func (ct *Content) setStageTitle() {
rw.SetStageTitle(name)
}
}
+
+// PagePDF generates a PDF of the current page, to given file path
+// (directory). the page name is the file name.
+func (ct *Content) PagePDF(path string) error {
+ if ct.currentPage == nil {
+ return errors.Log(errors.New("Page empty"))
+ }
+ core.MessageSnackbar(ct, "Generating PDF...")
+
+ Settings.PDF.FontScale = (100.0 / core.AppearanceSettings.DocsFontSize)
+
+ ct.inPDFRender = true
+ ct.reloadPage()
+ ct.inPDFRender = false
+
+ refs := ct.PageRefs(ct.currentPage)
+
+ fname := ct.currentPage.Name + ".pdf"
+ if path != "" {
+ os.MkdirAll(path, 0777)
+ fname = filepath.Join(path, fname)
+ }
+ f, err := os.Create(fname)
+ if errors.Log(err) != nil {
+ return err
+ }
+ opts := Settings.PageSettings(ct, ct.currentPage)
+ if refs != nil {
+ paginate.PDF(f, opts.PDF, ct.rightFrame, refs)
+ } else {
+ paginate.PDF(f, opts.PDF, ct.rightFrame)
+ }
+ err = f.Close()
+
+ ct.reloadPage()
+
+ core.MessageSnackbar(ct, "PDF saved to: "+fname)
+ af := errors.Log1(filepath.Abs(fname))
+ core.TheApp.OpenURL("file://" + af)
+ return err
+}
+
+// PageRefs returns a core.Frame with the contents of the references cited
+// on the given page. if References is nil, or error, result will be nil.
+func (ct *Content) PageRefs(page *bcontent.Page) *core.Frame {
+ if ct.References == nil {
+ return nil
+ }
+ sty := csl.APA // todo: settings
+ var b bytes.Buffer
+ _, err := csl.GenerateMarkdown(&b, ct.Source, "## References", ct.References, sty, page.Filename)
+ if errors.Log(err) != nil {
+ return nil
+ }
+ // os.WriteFile("tmp-refs.md", b.Bytes(), 0666)
+
+ fr := core.NewFrame()
+ fr.Styler(func(s *styles.Style) {
+ s.Direction = styles.Column
+ })
+ err = htmlcore.ReadMD(ct.Context, fr, b.Bytes())
+ if errors.Log(err) != nil {
+ return nil
+ }
+ fr.StyleTree()
+ fr.SetScene(ct.Scene)
+ return fr
+}
diff --git a/content/examples/basic/basic.go b/content/examples/basic/basic.go
index 2a2f9ebdde..a913aac88e 100644
--- a/content/examples/basic/basic.go
+++ b/content/examples/basic/basic.go
@@ -10,6 +10,7 @@ import (
"cogentcore.org/core/content"
"cogentcore.org/core/core"
"cogentcore.org/core/htmlcore"
+ _ "cogentcore.org/core/text/tex"
_ "cogentcore.org/core/yaegicore"
)
@@ -17,6 +18,8 @@ import (
var econtent embed.FS
func main() {
+ content.Settings.SiteTitle = "Cogent Content Example"
+ content.OfflineURL = "https://example.com"
b := core.NewBody("Cogent Content Example")
ct := content.NewContent(b).SetContent(econtent)
ct.Context.AddWikilinkHandler(htmlcore.GoDocWikilink("doc", "cogentcore.org/core"))
diff --git a/content/examples/basic/content/button.md b/content/examples/basic/content/button.md
index 9db1c8015e..6a5b4b13b8 100644
--- a/content/examples/basic/content/button.md
+++ b/content/examples/basic/content/button.md
@@ -1,11 +1,21 @@
+++
Categories = ["Widgets"]
+Authors = "Bea A. Author1 and Test Ing Name2"
+Affiliations = "1University of Somwhere 2University of Elsewhere"
+Abstract = "The button is an essential element of any GUI framework, with the capability of triggering actions of any sort. Actions are very important because they allow people to actually do something."
+++
A **button** is a [[widget]] that a user can click on to trigger a described action. See [[func button]] for a button [[value binding|bound]] to a function. There are various [[#types]] of buttons.
## Properties
+Buttons can do math? $x = y^2$ and even display math:
+
+{id="eq_math" title="Math demo"}
+$$
+y = \frac{1}{N} \left( \sum_{i=0}^{100} \frac{f(x^2)}{\sum x^2} \right)
+$$
+
You can make a button with text:
```Go
@@ -89,3 +99,38 @@ Action and menu buttons are the most minimal buttons, and they are typically onl
```Go
core.NewButton(b).SetType(core.ButtonAction).SetText("Action")
```
+
+### Third level header
+
+This is content at the third level.
+
+#### Fourth level header
+
+This is content at the fourth level. wow.
+
+##### Fifth level header
+
+Now this is taking things a bit far.
+
+## Lists are like this
+
+* List item 1
+ * Sub list item 1
+ * Sub list item 2
+* List item 2
+ * Sub list item 1
+ * Sub list item 2
+* List item 3
+ * Sub list item 1
+ * Sub list item 2
+
+1. List item 1
+ 1. Sub list item 1
+ 2. Sub list item 2
+2. List item 2
+ 1. Sub list item 1
+ 2. Sub list item 2
+3. List item 3
+ 1. Sub list item 1
+ 2. Sub list item 2
+
diff --git a/content/examples/basic/content/func-button.md b/content/examples/basic/content/func-button.md
index 1608e9533c..3ce04aab9c 100644
--- a/content/examples/basic/content/func-button.md
+++ b/content/examples/basic/content/func-button.md
@@ -3,3 +3,14 @@ Categories = ["Widgets"]
+++
A **func button** is a [[button]] that is [[value binding|bound]] to a function.
+
+{id="figure_one" style="height:10em"}
+
+
+As you can see in [[#figure_one]], the data turned out just as expected. However, you can never be too careful, because along comes [[#figure_two]], and now look.
+
+{id="figure_two" style="height:5em"}
+
+
+That just goes to show: science is always interesting.
+
diff --git a/content/examples/basic/content/home.md b/content/examples/basic/content/home.md
index e89c7206d4..73b5e9de8d 100644
--- a/content/examples/basic/content/home.md
+++ b/content/examples/basic/content/home.md
@@ -5,6 +5,17 @@ URL = ""
Welcome to **Cogent Content**, built out of [[widget]]s.
{id="image_gopher" style="height:10em"}
-
+
+There are some parameters we need to know:
+
+{id="table_conduction" title="Axonal conduction delays"}
+| Pathway | Minimum | Mean or Median |
+|------------------|-----------|------------------------------|
+| Corticocortical | 2 ms | 2.3 (magno visual) -- ~10 ms |
+| Corticothalamic | 2 ms | ~10 ms |
+| Thalamocortical | 0.5 ms | ~1 ms |
+| Collosal | ~2 ms | ~10 ms |
+
+See [[#image_gopher]] for a cute gopher, and [[#table_conduction]] for some interesting facts.
diff --git a/content/handlers.go b/content/handlers.go
index 2a1e1fe90f..a08213e405 100644
--- a/content/handlers.go
+++ b/content/handlers.go
@@ -6,21 +6,252 @@ package content
import (
"fmt"
+ "io"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/labels"
"cogentcore.org/core/base/strcase"
+ "cogentcore.org/core/colors"
"cogentcore.org/core/content/bcontent"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
+ "cogentcore.org/core/htmlcore"
"cogentcore.org/core/math32"
+ "cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
+ "cogentcore.org/core/styles/units"
"cogentcore.org/core/text/csl"
+ "cogentcore.org/core/text/textcore"
"cogentcore.org/core/tree"
+ "github.com/gomarkdown/markdown/ast"
)
+// BindTextEditor is a function set to [cogentcore.org/core/yaegicore.BindTextEditor]
+// when importing yaegicore, which provides interactive editing functionality for Go
+// code blocks in text editors.
+var BindTextEditor func(ed *textcore.Editor, parent *core.Frame, language string)
+
+// handles the id attribute in htmlcore: needed for equation case
+func (ct *Content) htmlIDAttributeHandler(ctx *htmlcore.Context, w io.Writer, node ast.Node, entering bool, tag, value string) bool {
+ if ct.currentPage == nil {
+ return false
+ }
+ lbl := ct.currentPage.SpecialLabel(value)
+ if lbl == "" {
+ return false
+ }
+ ch := node.GetChildren()
+ if len(ch) == 2 { // image or table
+ return false
+ }
+ if entering {
+ cp := "\n" + lbl + ":"
+ title := htmlcore.MDGetAttr(node, "title")
+ if title != "" {
+ cp += " " + title
+ }
+ cp += "\n"
+ w.Write([]byte(cp))
+ // fmt.Println("id:", value, lbl)
+ // fmt.Printf("%#v\n", node)
+ }
+ return false
+}
+
+func (ct *Content) htmlPreHandler(ctx *htmlcore.Context) bool {
+ hasCode := ctx.Node.FirstChild != nil && ctx.Node.FirstChild.Data == "code"
+ if !hasCode {
+ return false
+ }
+ codeEl := ctx.Node.FirstChild
+ collapsed := htmlcore.GetAttr(codeEl, "collapsed")
+ lang := htmlcore.GetLanguage(htmlcore.GetAttr(codeEl, "class"))
+ id := htmlcore.GetAttr(codeEl, "id")
+ title := htmlcore.GetAttr(codeEl, "title")
+ var ed *textcore.Editor
+ parent := ctx.Parent().AsWidget()
+ fr := core.NewFrame(parent.This)
+ fr.Styler(func(s *styles.Style) {
+ s.Grow.Set(1, 0)
+ s.Direction = styles.Column
+ })
+ fr.SetProperty("paginate-block", true) // no split
+ if id != "" {
+ fr.SetProperty("id", id)
+ fr.SetName(id)
+ ttx := parent.Children[parent.NumChildren()-2].(core.Widget)
+ ttx.AsWidget().SetProperty("id", id) // link target
+ tree.MoveToParent(ttx, fr) // get title text
+ }
+ if collapsed != "" {
+ cl := core.NewCollapser(fr)
+ core.NewText(cl.Summary).SetText("Code").SetText(title)
+ ed = textcore.NewEditor(cl.Details)
+ if collapsed == "false" || collapsed == "-" {
+ cl.Open = true
+ }
+ } else {
+ ed = textcore.NewEditor(fr)
+ }
+ ctx.Node = codeEl
+ if lang != "" {
+ ed.Lines.SetFileExt(lang)
+ }
+ ed.Lines.SetString(htmlcore.ExtractText(ctx))
+ if BindTextEditor != nil && (lang == "Go" || lang == "Goal") {
+ ed.Lines.SpacesToTabs(0, ed.Lines.NumLines()) // Go uses tabs
+ parFrame := core.NewFrame(fr)
+ parFrame.Styler(func(s *styles.Style) {
+ s.Direction = styles.Column
+ s.Grow.Set(1, 0)
+ })
+ // we inherit our Grow.Y from our first child so that
+ // elements that want to grow can do so
+ parFrame.SetOnChildAdded(func(n tree.Node) {
+ if _, ok := n.(*core.Body); ok { // Body should not grow
+ return
+ }
+ wb := core.AsWidget(n)
+ if wb.IndexInParent() != 0 {
+ return
+ }
+ wb.FinalStyler(func(s *styles.Style) {
+ parFrame.Styles.Grow.Y = s.Grow.Y
+ })
+ })
+ BindTextEditor(ed, parFrame, lang)
+ } else {
+ ed.SetReadOnly(true)
+ ed.Lines.Settings.LineNumbers = false
+ ed.Styler(func(s *styles.Style) {
+ s.Border.Width.Zero()
+ s.MaxBorder.Width.Zero()
+ s.StateLayer = 0
+ s.Background = colors.Scheme.SurfaceContainer
+ })
+ }
+ return true
+}
+
+// widgetHandler is htmlcore widget handler for adding our own actions etc.
+func (ct *Content) widgetHandler(w core.Widget) {
+ tag := ""
+ id := ""
+ title := ""
+ wb := w.AsWidget()
+ if t, ok := wb.Properties["tag"]; ok {
+ tag = t.(string)
+ }
+ if t, ok := wb.Properties["id"]; ok {
+ id = t.(string)
+ }
+ if t, ok := wb.Properties["title"]; ok {
+ title = t.(string)
+ }
+ switch x := w.(type) {
+ case *core.Text:
+ hdr := len(tag) > 0 && tag[0] == 'h'
+ x.Styler(func(s *styles.Style) {
+ if tag == "td" {
+ s.Margin.SetVertical(units.Em(0)) // use gap
+ } else {
+ s.Margin.SetVertical(units.Em(core.ConstantSpacing(0.25)))
+ }
+ s.Font.Size.Value *= core.AppearanceSettings.DocsFontSize / 100
+ s.Max.X.In(8) // big enough to not constrain PDF render
+ if hdr {
+ x.SetProperty("paginate-no-break-after", true)
+ }
+ })
+ case *core.Image:
+ ct.widgetHandlerFigure(w, id)
+ x.OnDoubleClick(func(e events.Event) {
+ d := core.NewBody("Image")
+ core.NewImage(d).SetImage(x.Image)
+ d.RunWindowDialog(x)
+ })
+ case *core.SVG:
+ ct.widgetHandlerFigure(w, id)
+ x.OnDoubleClick(func(e events.Event) {
+ d := core.NewBody("SVG")
+ sv := core.NewSVG(d)
+ sv.SVG = x.SVG
+ d.RunWindowDialog(x)
+ })
+ case *core.Frame:
+ switch tag {
+ case "table":
+ if id != "" {
+ lbl := ct.currentPage.SpecialLabel(id)
+ cp := "" + lbl + ":"
+ if title != "" {
+ cp += " " + title
+ }
+ ct.moveToBlockFrame(w, id, cp, true)
+ }
+ x.Styler(func(s *styles.Style) {
+ s.Align.Self = styles.Center
+ })
+ }
+ }
+}
+
+// moveToBlockFrame moves given widget into a block frame with given text
+// widget either at the top or bottom of the new frame.
+func (ct *Content) moveToBlockFrame(w core.Widget, id, txt string, top bool) {
+ wb := w.AsWidget()
+ fr := core.NewFrame(wb.Parent)
+ fr.Styler(func(s *styles.Style) {
+ s.Grow.Set(1, 0)
+ s.Direction = styles.Column
+ })
+ fr.SetProperty("paginate-block", true) // no split
+ fr.SetProperty("id", id)
+ fr.SetName(id)
+ var tx *core.Text
+ if top {
+ tx = core.NewText(fr).SetText(txt)
+ tx.SetProperty("id", id) // good link destination
+ }
+ tree.MoveToParent(w, fr)
+ if !top {
+ tx = core.NewText(fr).SetText(txt)
+ wb.SetProperty("id", id) // link here
+ }
+ tx.Styler(func(s *styles.Style) {
+ s.Max.X.In(8)
+ s.Font.Size.Value *= core.AppearanceSettings.DocsFontSize / 100
+ })
+}
+
+func (ct *Content) widgetHandlerFigure(w core.Widget, id string) {
+ wb := w.AsWidget()
+ fig := false
+ alt := ""
+ if p, ok := wb.Properties["alt"]; ok {
+ alt = p.(string)
+ }
+ if alt != "" && id != "" {
+ fig = true
+ }
+ wb.Styler(func(s *styles.Style) {
+ s.SetAbilities(true, abilities.Clickable, abilities.DoubleClickable)
+ s.Overflow.Set(styles.OverflowAuto)
+ if fig {
+ s.Align.Self = styles.Center
+ }
+ })
+ if !fig {
+ return
+ }
+ altf := htmlcore.MDToHTML(ct.Context, []byte(alt))
+ lbl := ct.currentPage.SpecialLabel(id)
+ lbf := "" + lbl + ": " + string(altf) + "
"
+ ct.moveToBlockFrame(w, id, lbf, false)
+}
+
// citeWikilink processes citation links, which start with @
func (ct *Content) citeWikilink(text string) (url string, label string) {
if len(text) == 0 || text[0] != '@' { // @CiteKey reference citations
@@ -32,7 +263,11 @@ func (ct *Content) citeWikilink(text string) (url string, label string) {
cs = csl.Narrative
ref = ref[1:]
}
- url = "ref://" + ref
+ if ct.inPDFRender {
+ url = "#" + ref
+ } else {
+ url = "ref://" + ref
+ }
if ct.References == nil {
return url, ref
}
@@ -74,6 +309,16 @@ func (ct *Content) mainWikilink(text string) (url string, label string) {
label = name
}
}
+ if ct.inPDFRender {
+ if heading != "" {
+ if pg == ct.currentPage {
+ return "#" + heading, label
+ }
+ return ct.getPrintURL() + "/" + pg.URL + "#" + heading, label
+ }
+ return ct.getPrintURL() + "/" + pg.URL, label
+ }
+
if heading != "" {
return pg.URL + "#" + heading, label
}
diff --git a/content/settings.go b/content/settings.go
new file mode 100644
index 0000000000..9abe2bc62b
--- /dev/null
+++ b/content/settings.go
@@ -0,0 +1,62 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package content
+
+import (
+ "time"
+
+ "cogentcore.org/core/content/bcontent"
+ "cogentcore.org/core/core"
+ "cogentcore.org/core/text/paginate"
+)
+
+func init() {
+ core.AddPrinterSettings()
+ Settings.Defaults()
+}
+
+// Settings are the current settings for content rendering.
+var Settings SettingsData
+
+// SettingsData has settings parameters for content,
+// including PDF rendering options.
+type SettingsData struct {
+ PDF paginate.Options
+
+ // SiteTitle is the title of the site, used in page headings and titles.
+ SiteTitle string
+
+ // PageSettings is a function that returns the settings data to use
+ // for the current page. Can set custom parameters for different pages.
+ // The default sets the PDF Header function to HeaderLeftPageNumber
+ // with current page Title.
+ PageSettings func(ct *Content, curPage *bcontent.Page) *SettingsData
+}
+
+func (s *SettingsData) Defaults() {
+ s.PDF.Defaults()
+ s.PDF.Footer = nil
+
+ s.PageSettings = func(ct *Content, curPage *bcontent.Page) *SettingsData {
+ ps := &SettingsData{}
+ *ps = Settings
+ pt := curPage.Title
+ if ps.SiteTitle != "" && pt == curPage.Name {
+ pt = ps.SiteTitle + ": " + pt
+ }
+ ps.PDF.Header = paginate.NoFirst(paginate.HeaderLeftPageNumber(pt))
+ ur := ct.getPrintURL() + "/" + curPage.URL
+ ura := `` + ur + ``
+ dt := ""
+ if !curPage.Date.IsZero() {
+ dt = curPage.Date.Format("January 2, 2006")
+ } else {
+ dt = time.Now().Format("January 2, 2006")
+ }
+ ps.PDF.Title = paginate.CenteredTitle(pt, curPage.Authors, curPage.Affiliations, ura, dt, curPage.Abstract)
+ ps.PDF.TextStyler = paginate.APAHeaders
+ return ps
+ }
+}
diff --git a/content/typegen.go b/content/typegen.go
index 62b815c568..a9df91b5c8 100644
--- a/content/typegen.go
+++ b/content/typegen.go
@@ -8,7 +8,7 @@ import (
"cogentcore.org/core/types"
)
-var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/content.Content", IDName: "content", Doc: "Content manages and displays the content of a set of pages.", Embeds: []types.Field{{Name: "Splits"}}, Fields: []types.Field{{Name: "Source", Doc: "Source is the source filesystem for the content.\nIt should be set using [Content.SetSource] or [Content.SetContent]."}, {Name: "Context", Doc: "Context is the [htmlcore.Context] used to render the content,\nwhich can be modified for things such as adding wikilink handlers."}, {Name: "References", Doc: "References is a list of references used for generating citation text\nfor literature reference wikilinks in the format [[@CiteKey]]."}, {Name: "pages", Doc: "pages are the pages that constitute the content."}, {Name: "pagesByName", Doc: "pagesByName has the [bcontent.Page] for each [bcontent.Page.Name]\ntransformed into lowercase. See [Content.pageByName] for a helper\nfunction that automatically transforms into lowercase."}, {Name: "pagesByURL", Doc: "pagesByURL has the [bcontent.Page] for each [bcontent.Page.URL]."}, {Name: "pagesByCategory", Doc: "pagesByCategory has the [bcontent.Page]s for each of all [bcontent.Page.Categories]."}, {Name: "categories", Doc: "categories has all unique [bcontent.Page.Categories], sorted such that the categories\nwith the most pages are listed first."}, {Name: "history", Doc: "history is the history of pages that have been visited.\nThe oldest page is first."}, {Name: "historyIndex", Doc: "historyIndex is the current position in [Content.history]."}, {Name: "currentPage", Doc: "currentPage is the currently open page."}, {Name: "renderedPage", Doc: "renderedPage is the most recently rendered page."}, {Name: "leftFrame", Doc: "leftFrame is the frame on the left side of the widget,\nused for displaying the table of contents and the categories."}, {Name: "rightFrame", Doc: "rightFrame is the frame on the right side of the widget,\nused for displaying the page content."}, {Name: "tocNodes", Doc: "tocNodes are all of the tree nodes in the table of contents\nby kebab-case heading name."}, {Name: "currentHeading", Doc: "currentHeading is the currently selected heading in the table of contents,\nif any (in kebab-case)."}, {Name: "prevPage", Doc: "The previous and next page, if applicable. They must be stored on this struct\nto avoid stale local closure variables."}, {Name: "nextPage", Doc: "The previous and next page, if applicable. They must be stored on this struct\nto avoid stale local closure variables."}}})
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/content.Content", IDName: "content", Doc: "Content manages and displays the content of a set of pages.", Embeds: []types.Field{{Name: "Splits"}}, Fields: []types.Field{{Name: "Source", Doc: "Source is the source filesystem for the content.\nIt should be set using [Content.SetSource] or [Content.SetContent]."}, {Name: "Context", Doc: "Context is the [htmlcore.Context] used to render the content,\nwhich can be modified for things such as adding wikilink handlers."}, {Name: "References", Doc: "References is a list of references used for generating citation text\nfor literature reference wikilinks in the format [[@CiteKey]]."}, {Name: "pages", Doc: "pages are the pages that constitute the content."}, {Name: "pagesByName", Doc: "pagesByName has the [bcontent.Page] for each [bcontent.Page.Name]\ntransformed into lowercase. See [Content.pageByName] for a helper\nfunction that automatically transforms into lowercase."}, {Name: "pagesByURL", Doc: "pagesByURL has the [bcontent.Page] for each [bcontent.Page.URL]."}, {Name: "pagesByCategory", Doc: "pagesByCategory has the [bcontent.Page]s for each of all [bcontent.Page.Categories]."}, {Name: "categories", Doc: "categories has all unique [bcontent.Page.Categories], sorted such that the categories\nwith the most pages are listed first. \"Other\" is always last, and is used for pages that\ndo not have a category, unless they are a category themselves."}, {Name: "history", Doc: "history is the history of pages that have been visited.\nThe oldest page is first."}, {Name: "historyIndex", Doc: "historyIndex is the current position in [Content.history]."}, {Name: "currentPage", Doc: "currentPage is the currently open page."}, {Name: "renderedPage", Doc: "renderedPage is the most recently rendered page."}, {Name: "leftFrame", Doc: "leftFrame is the frame on the left side of the widget,\nused for displaying the table of contents and the categories."}, {Name: "rightFrame", Doc: "rightFrame is the frame on the right side of the widget,\nused for displaying the page content."}, {Name: "tocNodes", Doc: "tocNodes are all of the tree nodes in the table of contents\nby kebab-case heading name."}, {Name: "currentHeading", Doc: "currentHeading is the currently selected heading in the table of contents,\nif any (in kebab-case)."}, {Name: "inPDFRender", Doc: "inPDFRender indicates that it is rendering a PDF now, turning off\nelements that are not appropriate for that."}, {Name: "prevPage", Doc: "The previous and next page, if applicable. They must be stored on this struct\nto avoid stale local closure variables."}, {Name: "nextPage", Doc: "The previous and next page, if applicable. They must be stored on this struct\nto avoid stale local closure variables."}}})
// NewContent returns a new [Content] with the given optional parent:
// Content manages and displays the content of a set of pages.
diff --git a/content/url_js.go b/content/url_js.go
index 3eb794fae3..0b885b455a 100644
--- a/content/url_js.go
+++ b/content/url_js.go
@@ -14,11 +14,22 @@ import (
"cogentcore.org/core/base/errors"
)
-// firstContent is the first [Content] used for [Content.getWebURL] or [Content.saveWebURL],
-// which is used to prevent nested [Content] widgets from incorrectly affecting the URL.
-var firstContent *Content
+var (
+ // firstContent is the first [Content] used for [Content.getWebURL] or [Content.saveWebURL],
+ // which is used to prevent nested [Content] widgets from incorrectly affecting the URL.
+ firstContent *Content
-var documentData = js.Global().Get("document").Get("documentElement").Get("dataset")
+ documentData = js.Global().Get("document").Get("documentElement").Get("dataset")
+
+ // OfflineURL is the non-web base url, which can be set to allow
+ // docs to refer to this in frontmatter.
+ OfflineURL = ""
+)
+
+func (ct *Content) getPrintURL() string {
+ p := originalBase.String()
+ return p[:len(p)-1]
+}
// getWebURL returns the current relative web URL that should be passed to [Content.Open]
// on startup and in [Content.handleWebPopState].
@@ -33,7 +44,10 @@ func (ct *Content) getWebURL() string {
if errors.Log(err) != nil {
return ""
}
- return strings.Trim(strings.TrimPrefix(full.String(), base.String()), "/")
+ ur := strings.Trim(strings.TrimPrefix(full.String(), base.String()), "/")
+ // fmt.Println("url is:", ur)
+ OfflineURL = ur
+ return ur
}
// saveWebURL saves the current page URL to the user's address bar and history.
diff --git a/content/url_noop.go b/content/url_noop.go
index faa6ac859a..7e757d2352 100644
--- a/content/url_noop.go
+++ b/content/url_noop.go
@@ -6,6 +6,12 @@
package content
-func (ct *Content) getWebURL() string { return "" }
-func (ct *Content) saveWebURL() {}
-func (ct *Content) handleWebPopState() {}
+// OfflineURL is the non-web base url, which can be set to allow
+// docs to refer to this in frontmatter.
+var OfflineURL = ""
+
+// just for printing
+func (ct *Content) getPrintURL() string { return OfflineURL }
+func (ct *Content) getWebURL() string { return "" }
+func (ct *Content) saveWebURL() {}
+func (ct *Content) handleWebPopState() {}
diff --git a/core/completer.go b/core/completer.go
index a62761be48..0870fc4022 100644
--- a/core/completer.go
+++ b/core/completer.go
@@ -80,7 +80,7 @@ func (c *Complete) Show(ctx Widget, pos image.Point, text string) {
if c.MatchFunc == nil {
return
}
- wait := SystemSettings.CompleteWaitDuration
+ wait := TimingSettings.CompleteWaitDuration
if c.stage != nil {
c.Cancel()
}
@@ -137,8 +137,8 @@ func (c *Complete) showNowImpl(ctx Widget, pos image.Point, text string) bool {
if len(c.completions) == 0 {
return false
}
- if len(c.completions) > SystemSettings.CompleteMaxItems {
- c.completions = c.completions[0:SystemSettings.CompleteMaxItems]
+ if len(c.completions) > AppearanceSettings.MenuMax {
+ c.completions = c.completions[0:AppearanceSettings.MenuMax]
}
sc := NewScene(ctx.AsTree().Name + "-complete")
diff --git a/core/events.go b/core/events.go
index 131d9b4312..9d3fc70f83 100644
--- a/core/events.go
+++ b/core/events.go
@@ -275,7 +275,7 @@ func (em *Events) handlePosEvent(e events.Event) {
}
case events.Scroll:
if !tree.IsNil(em.scroll) {
- scInTime := time.Since(em.lastScrollTime) < DeviceSettings.ScrollFocusTime
+ scInTime := time.Since(em.lastScrollTime) < TimingSettings.ScrollFocusTime
if scInTime {
em.scroll.AsWidget().HandleEvent(e)
if e.IsHandled() {
@@ -439,12 +439,12 @@ func (em *Events) handlePosEvent(e events.Event) {
em.drag.AsWidget().Send(events.DragMove, e) // usually ignored
e.SetHandled()
} else {
- if !tree.IsNil(em.dragPress) && em.dragStartCheck(e, DeviceSettings.DragStartTime, DeviceSettings.DragStartDistance) {
+ if !tree.IsNil(em.dragPress) && em.dragStartCheck(e, TimingSettings.DragStartTime, TimingSettings.DragStartDistance) {
em.cancelRepeatClick()
em.cancelLongPress()
em.dragPress.AsWidget().Send(events.DragStart, e)
e.SetHandled()
- } else if !tree.IsNil(em.slidePress) && em.dragStartCheck(e, DeviceSettings.SlideStartTime, DeviceSettings.DragStartDistance) {
+ } else if !tree.IsNil(em.slidePress) && em.dragStartCheck(e, TimingSettings.SlideStartTime, TimingSettings.DragStartDistance) {
em.cancelRepeatClick()
em.cancelLongPress()
em.slide = em.slidePress
@@ -479,7 +479,7 @@ func (em *Events) handlePosEvent(e events.Event) {
em.setCursorFromStyle()
return
}
- dcInTime := time.Since(em.lastClickTime) < DeviceSettings.DoubleClickInterval
+ dcInTime := time.Since(em.lastClickTime) < TimingSettings.DoubleClickInterval
em.lastClickTime = time.Now()
sentMulti := false
switch {
@@ -594,12 +594,12 @@ func (em *Events) topLongHover() Widget {
// handleLongHover handles long hover events
func (em *Events) handleLongHover(e events.Event) {
- em.handleLong(e, em.topLongHover(), &em.longHoverWidget, &em.longHoverPos, &em.longHoverTimer, events.LongHoverStart, events.LongHoverEnd, DeviceSettings.LongHoverTime, DeviceSettings.LongHoverStopDistance)
+ em.handleLong(e, em.topLongHover(), &em.longHoverWidget, &em.longHoverPos, &em.longHoverTimer, events.LongHoverStart, events.LongHoverEnd, TimingSettings.LongHoverTime, TimingSettings.LongHoverStopDistance)
}
// handleLongPress handles long press events
func (em *Events) handleLongPress(e events.Event) {
- em.handleLong(e, em.press, &em.longPressWidget, &em.longPressPos, &em.longPressTimer, events.LongPressStart, events.LongPressEnd, DeviceSettings.LongPressTime, DeviceSettings.LongPressStopDistance)
+ em.handleLong(e, em.press, &em.longPressWidget, &em.longPressPos, &em.longPressTimer, events.LongPressStart, events.LongPressEnd, TimingSettings.LongPressTime, TimingSettings.LongPressStopDistance)
}
// handleLong is the implementation of [Events.handleLongHover] and
@@ -759,7 +759,7 @@ func (em *Events) startRepeatClickTimer() {
if tree.IsNil(em.repeatClick) || !em.repeatClick.AsWidget().IsVisible() {
return
}
- delay := DeviceSettings.RepeatClickTime
+ delay := TimingSettings.RepeatClickTime
if em.repeatClickTimer == nil {
delay *= 8
}
@@ -1190,7 +1190,7 @@ func (em *Events) managerKeyChordEvents(e events.Event) {
e.SetHandled()
}
case keymap.WinSnapshot:
- img := sc.renderer.Image()
+ img := sc.Renderer.Image()
dstr := time.Now().Format(time.DateOnly + "-" + "15-04-05")
var sz string
if img != nil {
@@ -1203,10 +1203,20 @@ func (em *Events) managerKeyChordEvents(e events.Event) {
MessageSnackbar(sc, "Save screenshot: no render image")
}
sc.RenderWidget()
- sv := paint.RenderToSVG(&sc.Painter)
+ rend := sc.Painter.RenderDone()
+ svr := paint.NewSVGRenderer(sc.Painter.Size)
+ sv := svr.Render(rend).Source()
fnm := filepath.Join(TheApp.AppDataDir(), "screenshot-"+sc.Name+"-"+dstr+".svg")
errors.Log(os.WriteFile(fnm, sv, 0666))
- MessageSnackbar(sc, "Saved SVG screenshot to: "+strings.ReplaceAll(fnm, " ", `\ `)+sz)
+ pdr := paint.NewPDFRenderer(sc.Painter.Size, &sc.Painter.Context().Style.UnitContext)
+ pd := pdr.Render(rend).Source()
+ fnm = filepath.Join(TheApp.AppDataDir(), "screenshot-"+sc.Name+"-"+dstr+".pdf")
+ errors.Log(os.WriteFile(fnm, pd, 0666))
+
+ sc.SetScene(sc)
+ sc.Update()
+
+ MessageSnackbar(sc, "Saved SVG, PDF screenshots to: "+strings.ReplaceAll(fnm, " ", `\ `)+sz)
e.SetHandled()
case keymap.ZoomIn:
win.stepZoom(1)
diff --git a/core/filepicker.go b/core/filepicker.go
index f1619f7d77..7c531961ce 100644
--- a/core/filepicker.go
+++ b/core/filepicker.go
@@ -309,8 +309,8 @@ func (fp *FilePicker) makeFilesRow(p *tree.Plan) {
w.SetSlice(&fp.files)
w.SelectedField = "Name"
w.SelectedValue = fp.selectedFilename
- if SystemSettings.FilePickerSort != "" {
- w.setSortFieldName(SystemSettings.FilePickerSort)
+ if AppearanceSettings.FilePickerSort != "" {
+ w.setSortFieldName(AppearanceSettings.FilePickerSort)
}
w.TableStyler = func(w Widget, s *styles.Style, row, col int) {
fn := fp.files[row].Name
@@ -657,7 +657,7 @@ func (fp *FilePicker) saveSortSettings() {
if sv == nil {
return
}
- SystemSettings.FilePickerSort = sv.sortFieldName()
+ AppearanceSettings.FilePickerSort = sv.sortFieldName()
// fmt.Printf("sort: %v\n", Settings.FilePickerSort)
ErrorSnackbar(fp, SaveSettings(SystemSettings), "Error saving settings")
}
diff --git a/core/frame.go b/core/frame.go
index ef4bb83b4a..f46e3012cd 100644
--- a/core/frame.go
+++ b/core/frame.go
@@ -131,7 +131,7 @@ func (fr *Frame) Init() {
return
case keymap.PageDown:
proc := false
- for st := 0; st < SystemSettings.LayoutPageSteps; st++ {
+ for st := 0; st < TimingSettings.LayoutPageSteps; st++ {
if !fr.focusNextChild(true) {
break
}
@@ -143,7 +143,7 @@ func (fr *Frame) Init() {
return
case keymap.PageUp:
proc := false
- for st := 0; st < SystemSettings.LayoutPageSteps; st++ {
+ for st := 0; st < TimingSettings.LayoutPageSteps; st++ {
if !fr.focusPreviousChild(true) {
break
}
@@ -381,13 +381,13 @@ func (fr *Frame) focusOnName(e events.Event) bool {
delay := e.Time().Sub(fr.focusNameTime)
fr.focusNameTime = e.Time()
if kf == keymap.FocusNext { // tab means go to next match -- don't worry about time
- if fr.focusName == "" || delay > SystemSettings.LayoutFocusNameTabTime {
+ if fr.focusName == "" || delay > TimingSettings.LayoutFocusNameTabTime {
fr.focusName = ""
fr.focusNameLast = nil
return false
}
} else {
- if delay > SystemSettings.LayoutFocusNameTimeout {
+ if delay > TimingSettings.LayoutFocusNameTimeout {
fr.focusName = ""
}
if !unicode.IsPrint(e.KeyRune()) || e.Modifiers() != 0 {
@@ -487,9 +487,6 @@ func (sp *Space) Init() {
s.RenderBox = false
s.Min.X.Ch(1)
s.Min.Y.Em(1)
- s.Padding.Zero()
- s.Margin.Zero()
- s.MaxBorder.Width.Zero()
- s.Border.Width.Zero()
+ s.ZeroSpace()
})
}
diff --git a/core/image.go b/core/image.go
index 8296e7090a..e02b1a901f 100644
--- a/core/image.go
+++ b/core/image.go
@@ -11,8 +11,10 @@ import (
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/icons"
"cogentcore.org/core/math32"
+ "cogentcore.org/core/paint/renderers/rasterx"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
+ "cogentcore.org/core/system"
"cogentcore.org/core/tree"
"golang.org/x/image/draw"
)
@@ -83,7 +85,7 @@ func (im *Image) SizeUp() {
obj := math32.FromPoint(im.Image.Bounds().Size())
osz := styles.ObjectSizeFromFit(im.Styles.ObjectFit, obj, sz.Actual.Content)
sz.Actual.Content = osz
- sz.setTotalFromContent(&sz.Actual)
+ sz.SetTotalFromContent(&sz.Actual)
}
}
@@ -99,6 +101,19 @@ func (im *Image) Render() {
}
sp := im.Geom.ScrollOffset()
+ useLocalResize := false
+ if TheApp.Platform() == system.Web && im.Scene != nil {
+ // we are using an image renderer, e.g. PDF, offline rendering.
+ // note that there is no way to get transformed image back from javascript :(
+ // which would be the cleaner solution here.
+ if _, ok := im.Scene.Renderer.(*rasterx.Renderer); ok {
+ if _, ok := im.prevRenderImage.(*imagex.JSRGBA); ok {
+ im.prevImage = nil // clear
+ }
+ useLocalResize = true
+ }
+ }
+
var rimg image.Image
if im.prevImage == im.Image && im.Styles.ObjectFit == im.prevObjectFit && im.Geom.Size.Actual.Content == im.prevSize {
rimg = im.prevRenderImage
@@ -106,10 +121,17 @@ func (im *Image) Render() {
im.prevImage = im.Image
im.prevObjectFit = im.Styles.ObjectFit
im.prevSize = im.Geom.Size.Actual.Content
- rimg = im.Styles.ResizeImage(im.Image, im.Geom.Size.Actual.Content)
+ if useLocalResize {
+ rimg = im.Styles.ResizeImageLocal(im.Image, im.Geom.Size.Actual.Content)
+ } else {
+ rimg = im.Styles.ResizeImage(im.Image, im.Geom.Size.Actual.Content)
+ }
im.prevRenderImage = rimg
}
- im.Scene.Painter.DrawImage(rimg, r, sp, draw.Over)
+ pim := im.Scene.Painter.DrawImage(rimg, r, sp, draw.Over)
+ if id, ok := im.Properties["id"]; ok {
+ pim.Anchor = id.(string)
+ }
}
func (im *Image) MakeToolbar(p *tree.Plan) {
diff --git a/core/inlinelist.go b/core/inlinelist.go
index 671cbabfa2..f1c92ee89c 100644
--- a/core/inlinelist.go
+++ b/core/inlinelist.go
@@ -33,7 +33,7 @@ func (il *InlineList) Init() {
il.Maker(func(p *tree.Plan) {
sl := reflectx.Underlying(reflect.ValueOf(il.Slice))
- sz := min(sl.Len(), SystemSettings.SliceInlineLength)
+ sz := min(sl.Len(), AppearanceSettings.InlineLengths.Slice)
for i := 0; i < sz; i++ {
itxt := strconv.Itoa(i)
tree.AddNew(p, "value-"+itxt, func() Value {
diff --git a/core/layout.go b/core/layout.go
index cf23411500..4bce1787aa 100644
--- a/core/layout.go
+++ b/core/layout.go
@@ -109,8 +109,8 @@ func (ct geomCT) String() string {
return fmt.Sprintf("Content: %v, \tTotal: %v", ct.Content, ct.Total)
}
-// geomSize has all of the relevant Layout sizes
-type geomSize struct {
+// GeomSize has all of the relevant Layout sizes
+type GeomSize struct {
// Actual is the actual size for the purposes of rendering, representing
// the "external" demands of the widget for space from its parent.
// This is initially the bottom-up constraint computed by SizeUp,
@@ -152,41 +152,41 @@ type geomSize struct {
Max math32.Vector2
}
-func (ls geomSize) String() string {
+func (ls GeomSize) String() string {
return fmt.Sprintf("Actual: %v, \tAlloc: %v", ls.Actual, ls.Alloc)
}
// setInitContentMin sets initial Actual.Content size from given Styles.Min,
// further subject to the current Max constraint.
-func (ls *geomSize) setInitContentMin(styMin math32.Vector2) {
+func (ls *GeomSize) setInitContentMin(styMin math32.Vector2) {
csz := &ls.Actual.Content
*csz = styMin
styles.SetClampMaxVector(csz, ls.Max)
}
// FitSizeMax increases given size to fit given fm value, subject to Max constraints
-func (ls *geomSize) FitSizeMax(to *math32.Vector2, fm math32.Vector2) {
+func (ls *GeomSize) FitSizeMax(to *math32.Vector2, fm math32.Vector2) {
styles.SetClampMinVector(to, fm)
styles.SetClampMaxVector(to, ls.Max)
}
-// setTotalFromContent sets the Total size as Content plus Space
-func (ls *geomSize) setTotalFromContent(ct *geomCT) {
+// SetTotalFromContent sets the Total size as Content plus Space
+func (ls *GeomSize) SetTotalFromContent(ct *geomCT) {
ct.Total = ct.Content.Add(ls.Space)
}
-// setContentFromTotal sets the Content from Total size,
+// SetContentFromTotal sets the Content from Total size,
// subtracting Space
-func (ls *geomSize) setContentFromTotal(ct *geomCT) {
+func (ls *GeomSize) SetContentFromTotal(ct *geomCT) {
ct.Content = ct.Total.Sub(ls.Space)
}
-// geomState contains the the layout geometry state for each widget.
+// GeomState contains the the layout geometry state for each widget.
// Set by the parent Layout during the Layout process.
-type geomState struct {
+type GeomState struct {
// Size has sizing data for the widget: use Actual for rendering.
// Alloc shows the potentially larger space top-down allocated.
- Size geomSize `display:"add-fields"`
+ Size GeomSize `display:"add-fields"`
// Pos is position within the overall Scene that we render into,
// including effects of scroll offset, for both Total outer dimension
@@ -216,14 +216,14 @@ type geomState struct {
ContentBBox image.Rectangle `edit:"-" copier:"-" json:"-" xml:"-" set:"-"`
}
-func (ls *geomState) String() string {
+func (ls *GeomState) String() string {
return "Size: " + ls.Size.String() + "\nPos: " + ls.Pos.String() + "\tCell: " + ls.Cell.String() +
"\tRelPos: " + ls.RelPos.String() + "\tScroll: " + ls.Scroll.String()
}
// contentRangeDim returns the Content bounding box min, max
// along given dimension
-func (ls *geomState) contentRangeDim(d math32.Dims) (cmin, cmax float32) {
+func (ls *GeomState) contentRangeDim(d math32.Dims) (cmin, cmax float32) {
cmin = float32(math32.PointDim(ls.ContentBBox.Min, d))
cmax = float32(math32.PointDim(ls.ContentBBox.Max, d))
return
@@ -231,20 +231,20 @@ func (ls *geomState) contentRangeDim(d math32.Dims) (cmin, cmax float32) {
// totalRect returns Pos.Total -- Size.Actual.Total
// as an image.Rectangle, e.g., for bounding box
-func (ls *geomState) totalRect() image.Rectangle {
+func (ls *GeomState) totalRect() image.Rectangle {
return math32.RectFromPosSizeMax(ls.Pos.Total, ls.Size.Actual.Total)
}
// contentRect returns Pos.Content, Size.Actual.Content
// as an image.Rectangle, e.g., for bounding box.
-func (ls *geomState) contentRect() image.Rectangle {
+func (ls *GeomState) contentRect() image.Rectangle {
return math32.RectFromPosSizeMax(ls.Pos.Content, ls.Size.Actual.Content)
}
// ScrollOffset computes the net scrolling offset as a function of
// the difference between the allocated position and the actual
// content position according to the clipped bounding box.
-func (ls *geomState) ScrollOffset() image.Point {
+func (ls *GeomState) ScrollOffset() image.Point {
return ls.ContentBBox.Min.Sub(ls.Pos.Content.ToPoint())
}
@@ -591,7 +591,7 @@ func (fr *Frame) laySetContentFitOverflow(nsz math32.Vector2, pass LayoutPasses)
}
}
styles.SetClampMaxVector(asz, mx)
- sz.setTotalFromContent(&sz.Actual)
+ sz.SetTotalFromContent(&sz.Actual)
}
// SizeUp (bottom-up) gathers Actual sizes from our Children & Parts,
@@ -608,7 +608,7 @@ func (wb *WidgetBase) SizeUpWidget() {
wb.sizeFromStyle()
wb.sizeUpParts()
sz := &wb.Geom.Size
- sz.setTotalFromContent(&sz.Actual)
+ sz.SetTotalFromContent(&sz.Actual)
}
// spaceFromStyle sets the Space based on Style BoxSpace().Size()
@@ -639,7 +639,7 @@ func (wb *WidgetBase) sizeFromStyle() {
}
sz.Internal.SetZero()
sz.setInitContentMin(sz.Min)
- sz.setTotalFromContent(&sz.Actual)
+ sz.SetTotalFromContent(&sz.Actual)
if DebugSettings.LayoutTrace && (sz.Actual.Content.X > 0 || sz.Actual.Content.Y > 0) {
fmt.Println(wb, "SizeUp from Style:", sz.Actual.Content.String())
}
@@ -678,8 +678,8 @@ func (wb *WidgetBase) updateParentRelSizes() bool {
if got {
sz.FitSizeMax(&sz.Actual.Total, effmin)
sz.FitSizeMax(&sz.Alloc.Total, effmin)
- sz.setContentFromTotal(&sz.Actual)
- sz.setContentFromTotal(&sz.Alloc)
+ sz.SetContentFromTotal(&sz.Actual)
+ sz.SetContentFromTotal(&sz.Alloc)
}
return got
}
@@ -1024,7 +1024,7 @@ func (wb *WidgetBase) sizeDownParts(iter int) bool {
psz := &wb.Parts.Geom.Size
pgrow, _ := wb.growToAllocSize(sz.Actual.Content, sz.Alloc.Content)
psz.Alloc.Total = pgrow // parts = content
- psz.setContentFromTotal(&psz.Alloc)
+ psz.SetContentFromTotal(&psz.Alloc)
redo := wb.Parts.SizeDown(iter)
if redo && DebugSettings.LayoutTrace {
fmt.Println(wb, "Parts triggered redo")
@@ -1113,7 +1113,7 @@ func (fr *Frame) sizeDownFrame(iter int) bool {
fr.updateParentRelSizes()
sz := &fr.Geom.Size
styles.SetClampMaxVector(&sz.Alloc.Content, sz.Max) // can't be more than max..
- sz.setTotalFromContent(&sz.Alloc)
+ sz.SetTotalFromContent(&sz.Alloc)
if DebugSettings.LayoutTrace {
fmt.Println(fr, "Managing Alloc:", sz.Alloc.Content)
}
@@ -1204,8 +1204,8 @@ func (fr *Frame) ManageOverflow(iter int, updateSize bool) bool {
}
fr.This.(Layouter).LayoutSpace() // adds the scroll space
if updateSize {
- sz.setTotalFromContent(&sz.Actual)
- sz.setContentFromTotal(&sz.Alloc) // alloc is *decreased* from any increase in space
+ sz.SetTotalFromContent(&sz.Actual)
+ sz.SetContentFromTotal(&sz.Alloc) // alloc is *decreased* from any increase in space
}
if change && DebugSettings.LayoutTrace {
fmt.Println(fr, "ManageOverflow changed")
@@ -1299,7 +1299,7 @@ func (fr *Frame) sizeDownGrowCells(iter int, extra math32.Vector2) bool {
}
ksz.Alloc.Total.SetDim(ma, asz)
}
- ksz.setContentFromTotal(&ksz.Alloc)
+ ksz.SetContentFromTotal(&ksz.Alloc)
return tree.Continue
})
if exn.X == 0 && exn.Y == 0 {
@@ -1386,7 +1386,7 @@ func (fr *Frame) sizeDownGrowStacked(iter int, extra math32.Vector2) bool {
chg = true
}
ksz.Alloc.Total = asz
- ksz.setContentFromTotal(&ksz.Alloc)
+ ksz.SetContentFromTotal(&ksz.Alloc)
}
return chg
}
@@ -1398,7 +1398,7 @@ func (fr *Frame) sizeDownGrowStacked(iter int, extra math32.Vector2) bool {
chg = true
}
ksz.Alloc.Total = asz
- ksz.setContentFromTotal(&ksz.Alloc)
+ ksz.SetContentFromTotal(&ksz.Alloc)
return tree.Continue
})
return chg
@@ -1432,7 +1432,7 @@ func (fr *Frame) sizeDownAllocActualCells(iter int) {
asz := md.Size.Dim(ma)
ksz.Alloc.Total.SetDim(ma, asz)
}
- ksz.setContentFromTotal(&ksz.Alloc)
+ ksz.SetContentFromTotal(&ksz.Alloc)
return tree.Continue
})
}
@@ -1446,7 +1446,7 @@ func (fr *Frame) sizeDownAllocActualStacked(iter int) {
if kwb != nil {
ksz := &kwb.Geom.Size
ksz.Alloc.Total = asz
- ksz.setContentFromTotal(&ksz.Alloc)
+ ksz.SetContentFromTotal(&ksz.Alloc)
}
return
}
@@ -1455,7 +1455,7 @@ func (fr *Frame) sizeDownAllocActualStacked(iter int) {
fr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
ksz := &cwb.Geom.Size
ksz.Alloc.Total = asz
- ksz.setContentFromTotal(&ksz.Alloc)
+ ksz.SetContentFromTotal(&ksz.Alloc)
return tree.Continue
})
}
@@ -1473,7 +1473,7 @@ func (fr *Frame) sizeDownCustom(iter int) bool {
fr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
ksz := &cwb.Geom.Size
ksz.Alloc.Total = asz
- ksz.setContentFromTotal(&ksz.Alloc)
+ ksz.SetContentFromTotal(&ksz.Alloc)
return tree.Continue
})
redo := fr.sizeDownChildren(iter)
@@ -1505,7 +1505,7 @@ func (wb *WidgetBase) SizeFinal() {
wb.growToAlloc()
wb.styleSizeUpdate() // now that sizes are stable, ensure styling based on size is updated
wb.sizeFinalParts()
- sz.setTotalFromContent(&sz.Actual)
+ sz.SetTotalFromContent(&sz.Actual)
}
// growToAlloc grows our Actual size up to current Alloc size
@@ -1524,7 +1524,7 @@ func (wb *WidgetBase) growToAlloc() bool {
fmt.Println(wb, "GrowToAlloc:", sz.Alloc.Total, "from actual:", sz.Actual.Total)
}
sz.Actual.Total = act // already has max constraint
- sz.setContentFromTotal(&sz.Actual)
+ sz.SetContentFromTotal(&sz.Actual)
}
return change
}
diff --git a/core/mainstage.go b/core/mainstage.go
index f5f91f4ac8..7235579233 100644
--- a/core/mainstage.go
+++ b/core/mainstage.go
@@ -168,7 +168,7 @@ func (st *Stage) addSceneParts() {
np.Y = max(np.Y, minsz)
ng := sc.SceneGeom
ng.Size = np
- sc.resize(ng)
+ sc.Resize(ng)
})
}
}
@@ -266,7 +266,7 @@ func (st *Stage) runWindow() *Stage {
}
if st.NewWindow || currentRenderWindow == nil {
- sc.resize(math32.Geom2DInt{st.renderContext.geom.Pos, sz})
+ sc.Resize(math32.Geom2DInt{st.renderContext.geom.Pos, sz})
win := st.newRenderWindow()
mainRenderWindows.add(win)
setCurrentRenderWindow(win)
@@ -352,7 +352,7 @@ func (st *Stage) runDialog() *Stage {
if st.NewWindow {
st.Mains = nil
- sc.resize(math32.Geom2DInt{st.renderContext.geom.Pos, sz})
+ sc.Resize(math32.Geom2DInt{st.renderContext.geom.Pos, sz})
st.Type = WindowStage // critical: now is its own window!
sc.SceneGeom.Pos = image.Point{} // ignore pos
win := st.newRenderWindow()
diff --git a/core/meter.go b/core/meter.go
index bf81e18bfe..589f798a74 100644
--- a/core/meter.go
+++ b/core/meter.go
@@ -147,7 +147,7 @@ func (m *Meter) Render() {
if m.Text != "" {
sty, tsty := m.Styles.NewRichText()
tx, _ := htmltext.HTMLToRich([]byte(m.Text), sty, nil)
- txt = m.Scene.TextShaper().WrapLines(tx, sty, tsty, &AppearanceSettings.Text, size)
+ txt = m.Scene.TextShaper().WrapLines(tx, sty, tsty, size)
toff = txt.Bounds.Size().DivScalar(2)
}
diff --git a/core/popupstage.go b/core/popupstage.go
index 64403a4e3b..cc44a99960 100644
--- a/core/popupstage.go
+++ b/core/popupstage.go
@@ -101,7 +101,7 @@ func (st *Stage) runPopup() *Stage {
switch st.Type {
case MenuStage:
sz.X += scrollWd * 2
- maxht := int(float32(SystemSettings.MenuMaxHeight) * fontHt)
+ maxht := int(float32(AppearanceSettings.MenuMax) * fontHt)
sz.Y = min(maxht, sz.Y)
case SnackbarStage:
b := msc.SceneGeom.Bounds()
diff --git a/core/render.go b/core/render.go
index d9692dcb90..f8e6d7c5c1 100644
--- a/core/render.go
+++ b/core/render.go
@@ -122,48 +122,58 @@ func (wb *WidgetBase) NeedsRebuild() bool {
return rc.rebuild
}
-// layoutScene does a layout of the scene: Size, Position
-func (sc *Scene) layoutScene() {
+// LayoutScene does a layout of the scene: Size, Position
+func (sc *Scene) LayoutScene() {
if DebugSettings.LayoutTrace {
fmt.Println("\n############################\nLayoutScene SizeUp start:", sc)
}
- sc.SizeUp()
- sz := &sc.Geom.Size
- sz.Alloc.Total.SetPoint(sc.SceneGeom.Size)
- sz.setContentFromTotal(&sz.Alloc)
- // sz.Actual = sz.Alloc // todo: is this needed??
+ sc.layoutFrame(math32.FromPoint(sc.SceneGeom.Size))
+ sc.ApplyScenePos()
+}
+
+// layoutFrame does the frame layout core functionality
+func (fr *Frame) layoutFrame(size math32.Vector2) {
+ fr.SizeUp()
+ sz := &fr.Geom.Size
+ sz.Alloc.Total = size
+ sz.SetContentFromTotal(&sz.Alloc)
if DebugSettings.LayoutTrace {
- fmt.Println("\n############################\nSizeDown start:", sc)
+ fmt.Println("\n############################\nSizeDown start:", fr)
}
maxIter := 3
for iter := 0; iter < maxIter; iter++ { // 3 > 2; 4 same as 3
- redo := sc.SizeDown(iter)
+ redo := fr.SizeDown(iter)
if redo && iter < maxIter-1 {
if DebugSettings.LayoutTrace {
- fmt.Println("\n############################\nSizeDown redo:", sc, "iter:", iter+1)
+ fmt.Println("\n############################\nSizeDown redo:", fr, "iter:", iter+1)
}
} else {
break
}
}
if DebugSettings.LayoutTrace {
- fmt.Println("\n############################\nSizeFinal start:", sc)
+ fmt.Println("\n############################\nSizeFinal start:", fr)
}
- sc.SizeFinal()
+ fr.SizeFinal()
if DebugSettings.LayoutTrace {
- fmt.Println("\n############################\nPosition start:", sc)
+ fmt.Println("\n############################\nPosition start:", fr)
}
- sc.Position()
+ fr.Position()
if DebugSettings.LayoutTrace {
- fmt.Println("\n############################\nScenePos start:", sc)
+ fmt.Println("\n############################\nScenePos start:", fr)
}
- sc.ApplyScenePos()
}
-// layoutRenderScene does a layout and render of the tree:
+// layoutFrame does a layout on the given Frame using given size.
+func (fr *Frame) LayoutFrame(size math32.Vector2) {
+ fr.layoutFrame(size)
+ fr.ApplyScenePos()
+}
+
+// LayoutRenderScene does a layout and render of the tree:
// GetSize, DoLayout, Render. Needed after Config.
-func (sc *Scene) layoutRenderScene() {
- sc.layoutScene()
+func (sc *Scene) LayoutRenderScene() {
+ sc.LayoutScene()
sc.RenderWidget()
}
@@ -229,14 +239,14 @@ func (sc *Scene) doUpdate() bool {
case sc.lastRender.needsRestyle(rc):
// pr := profile.Start("restyle")
sc.applyStyleScene()
- sc.layoutRenderScene()
+ sc.LayoutRenderScene()
sc.setFlag(false, sceneNeedsLayout, sceneNeedsRender)
sc.setFlag(true, sceneImageUpdated)
sc.lastRender.saveRender(rc)
// pr.End()
case sc.hasFlag(sceneNeedsLayout):
// pr := profile.Start("layout")
- sc.layoutRenderScene()
+ sc.LayoutRenderScene()
sc.setFlag(false, sceneNeedsLayout, sceneNeedsRender)
sc.setFlag(true, sceneImageUpdated)
// pr.End()
@@ -290,7 +300,7 @@ func (sc *Scene) doRebuild() {
sc.Stage.Sprites.reset()
sc.updateScene()
sc.applyStyleScene()
- sc.layoutRenderScene()
+ sc.LayoutRenderScene()
}
// contentSize computes the size of the scene based on current content.
@@ -303,7 +313,7 @@ func (sc *Scene) contentSize(initSz image.Point) image.Point {
sc.setFlag(true, sceneContentSizing)
sc.updateScene()
sc.applyStyleScene()
- sc.layoutScene()
+ sc.LayoutScene()
sz := &sc.Geom.Size
psz := sz.Actual.Total
sc.setFlag(false, sceneContentSizing)
@@ -326,6 +336,9 @@ func (wb *WidgetBase) StartRender() bool {
return false
}
wb.Styles.ComputeActualBackground(wb.parentActualBackground())
+ if wb.Scene == nil {
+ return false
+ }
pc := &wb.Scene.Painter
if pc.State == nil {
return false
diff --git a/core/render_notjs.go b/core/render_notjs.go
index e159d98c23..c03fb03b41 100644
--- a/core/render_notjs.go
+++ b/core/render_notjs.go
@@ -20,7 +20,7 @@ import (
// If it returns nil, then the image could not be fetched.
func grabRenderFrom(w Widget) *image.RGBA {
wb := w.AsWidget()
- scimg := wb.Scene.renderer.Image()
+ scimg := wb.Scene.Renderer.Image()
if scimg == nil {
return nil
}
diff --git a/core/rendersource.go b/core/rendersource.go
index 42943e7500..b7f9bffa52 100644
--- a/core/rendersource.go
+++ b/core/rendersource.go
@@ -20,11 +20,11 @@ import (
// SceneSource returns a [composer.Source] for the given scene
// using the given suggested draw operation.
func SceneSource(sc *Scene, op draw.Op) composer.Source {
- if sc.Painter.State == nil || sc.renderer == nil {
+ if sc.Painter.State == nil || sc.Renderer == nil {
return nil
}
render := sc.Painter.RenderDone()
- return &paintSource{render: render, renderer: sc.renderer, drawOp: op, drawPos: sc.SceneGeom.Pos}
+ return &paintSource{render: render, renderer: sc.Renderer, drawOp: op, drawPos: sc.SceneGeom.Pos}
}
// paintSource is the [composer.Source] for [paint.Painter] content, such as for a [Scene].
diff --git a/core/renderwindow.go b/core/renderwindow.go
index 10b31f935b..76a2fcf8be 100644
--- a/core/renderwindow.go
+++ b/core/renderwindow.go
@@ -686,7 +686,7 @@ func (w *renderWindow) renderWindow() {
}
spriteMods := top.Sprites.IsModified()
- spriteUpdateTime := SystemSettings.CursorBlinkTime
+ spriteUpdateTime := TimingSettings.CursorBlinkTime
if spriteUpdateTime == 0 {
spriteUpdateTime = 500 * time.Millisecond
}
diff --git a/core/scene.go b/core/scene.go
index 5a5fb36a87..8a5d271df6 100644
--- a/core/scene.go
+++ b/core/scene.go
@@ -86,7 +86,7 @@ type Scene struct { //core:no-new
selectedWidgetChan chan Widget `json:"-" xml:"-"`
// source renderer for rendering the scene
- renderer render.Renderer `copier:"-" json:"-" xml:"-" display:"-" set:"-"`
+ Renderer render.Renderer `copier:"-" json:"-" xml:"-" display:"-" set:"-"`
// lastRender captures key params from last render.
// If different then a new ApplyStyleScene is needed.
@@ -100,6 +100,9 @@ type Scene struct { //core:no-new
// instead of rendering into the Scene Painter.
directRenders []Widget
+ // this is our own text shaper in case we don't have a render context
+ textShaper shaped.Shaper
+
// flags are atomic bit flags for [Scene] state.
flags sceneFlags
}
@@ -248,6 +251,15 @@ func (sc *Scene) renderContext() *renderContext {
return sm.renderContext
}
+// MakeTextShaper makes our own text shaper for offline use,
+// if not already in place.
+func (sc *Scene) MakeTextShaper() shaped.Shaper {
+ if sc.textShaper == nil {
+ sc.textShaper = shaped.NewShaper()
+ }
+ return sc.textShaper
+}
+
// TextShaper returns the current [shaped.TextShaper], for text shaping.
// may be nil if not yet initialized.
func (sc *Scene) TextShaper() shaped.Shaper {
@@ -255,7 +267,7 @@ func (sc *Scene) TextShaper() shaped.Shaper {
if rc != nil {
return rc.textShaper
}
- return nil
+ return sc.textShaper
}
// RenderWindow returns the current render window for this scene.
@@ -278,12 +290,12 @@ func (sc *Scene) RenderWindow() *renderWindow {
func (sc *Scene) fitInWindow(winGeom math32.Geom2DInt) bool {
geom := sc.SceneGeom
geom = geom.FitInWindow(winGeom)
- return sc.resize(geom)
+ return sc.Resize(geom)
}
-// resize resizes the scene if needed, creating a new image; updates Geom.
+// Resize resizes the scene if needed, creating a new image; updates Geom.
// returns false if the scene is already the correct size.
-func (sc *Scene) resize(geom math32.Geom2DInt) bool {
+func (sc *Scene) Resize(geom math32.Geom2DInt) bool {
if geom.Size.X <= 0 || geom.Size.Y <= 0 {
return false
}
@@ -293,8 +305,8 @@ func (sc *Scene) resize(geom math32.Geom2DInt) bool {
sc.Painter.Paint.UnitContext = sc.Styles.UnitContext
}
sc.SceneGeom.Pos = geom.Pos
- if sc.renderer != nil {
- img := sc.renderer.Image()
+ if sc.Renderer != nil {
+ img := sc.Renderer.Image()
if img != nil {
isz := img.Bounds().Size()
if isz == geom.Size {
@@ -302,11 +314,11 @@ func (sc *Scene) resize(geom math32.Geom2DInt) bool {
}
}
} else {
- sc.renderer = paint.NewSourceRenderer(sz)
+ sc.Renderer = paint.NewSourceRenderer(sz)
}
sc.Painter.Paint.UnitContext = sc.Styles.UnitContext
sc.Painter.State.Init(sc.Painter.Paint, sz)
- sc.renderer.SetSize(units.UnitDot, sz)
+ sc.Renderer.SetSize(units.UnitDot, sz)
sc.SceneGeom.Size = geom.Size // make sure
sc.updateScene()
diff --git a/core/scroll.go b/core/scroll.go
index c593ef4c3d..1bb82b41c6 100644
--- a/core/scroll.go
+++ b/core/scroll.go
@@ -348,7 +348,7 @@ var lastAutoScroll time.Time
func (fr *Frame) AutoScroll(pos math32.Vector2) bool {
now := time.Now()
lag := now.Sub(lastAutoScroll)
- if lag < SystemSettings.LayoutAutoScrollDelay {
+ if lag < TimingSettings.LayoutAutoScrollDelay {
return false
}
did := false
diff --git a/core/settings.go b/core/settings.go
index 931923a734..9e427a2ede 100644
--- a/core/settings.go
+++ b/core/settings.go
@@ -11,6 +11,7 @@ import (
"os/user"
"path/filepath"
"reflect"
+ "slices"
"time"
"cogentcore.org/core/base/errors"
@@ -27,6 +28,7 @@ import (
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/system"
+ "cogentcore.org/core/text/printer"
"cogentcore.org/core/text/rich"
"cogentcore.org/core/text/text"
"cogentcore.org/core/tree"
@@ -36,7 +38,17 @@ import (
// that the user will see in the settings window. It contains the base Cogent Core
// settings by default and should be modified by other apps to add their
// app settings.
-var AllSettings = []Settings{AppearanceSettings, SystemSettings, DeviceSettings, DebugSettings}
+var AllSettings = []Settings{AppearanceSettings, SystemSettings, TimingSettings, DebugSettings}
+
+// AddAppSettings inserts app-specific settings after the AppearanceSettings
+func AddAppSettings(sets ...Settings) {
+ AllSettings = slices.Insert(AllSettings, 1, sets...)
+}
+
+// AddPrinterSettings adds printer settings, for apps that use these.
+func AddPrinterSettings() {
+ AddAppSettings(&printer.Settings)
+}
// Settings is the interface that describes the functionality common
// to all settings data types.
@@ -270,13 +282,44 @@ type AppearanceSettingsData struct { //types:add
// Text specifies text settings including the language, and the
// font families for different styles of fonts.
- Text rich.Settings
+ Text rich.SettingsData
+
+ // only support closing the currently selected active tab;
+ // if this is set to true, pressing the close button on other tabs
+ // will take you to that tab, from which you can close it.
+ OnlyCloseActiveTab bool `default:"false"`
+
+ // the maximum number of items in a menu popup panel;
+ // scroll bars are enforced beyond that size, or for
+ // completion, this is the max number of items shown.
+ MenuMax int `default:"30" min:"5" step:"1"`
+
+ // column to sort by in FilePicker, and :up or :down for direction.
+ // Updated automatically via FilePicker
+ FilePickerSort string `display:"-"`
+
+ // length of inline elements to display for containers in Form widgets.
+ InlineLengths InlineLengths `display:"inline"`
}
func (as *AppearanceSettingsData) Defaults() {
as.Text.Defaults()
}
+// InlineLengths has the length of inline elements to display for containers in Form widgets.
+type InlineLengths struct {
+ // the number of map elements at or below which an inline representation
+ // of the map will be presented, which is more convenient for small #'s of properties
+ Map int `default:"2" min:"1" step:"1"`
+
+ // the number of elemental struct fields at or below which an inline representation
+ // of the struct will be presented, which is more convenient for small structs
+ Struct int `default:"4" min:"2" step:"1"`
+
+ // the number of slice elements below which inline will be used
+ Slice int `default:"4" min:"2" step:"1"`
+}
+
// ConstantSpacing returns a spacing value (padding, margin, gap)
// that will remain constant regardless of changes in the
// [AppearanceSettings.Spacing] setting.
@@ -335,7 +378,7 @@ func (as *AppearanceSettingsData) Apply() { //types:add
if as.Highlighting == "" {
as.Highlighting = "emacs"
}
- rich.DefaultSettings = as.Text
+ rich.Settings = as.Text
// TODO(kai): move HiStyle to a separate text editor settings
// if TheViewInterface != nil {
@@ -384,11 +427,11 @@ func (as *AppearanceSettingsData) ZebraStripesWeight() float32 {
return as.ZebraStripes * 0.002
}
-// DeviceSettings are the global device settings.
-var DeviceSettings = &DeviceSettingsData{
+// TimingSettings are the global timing settings.
+var TimingSettings = &TimingSettingsData{
SettingsBase: SettingsBase{
- Name: "Device",
- File: filepath.Join(TheApp.CogentCoreDataDir(), "device-settings.toml"),
+ Name: "Timing",
+ File: filepath.Join(TheApp.CogentCoreDataDir(), "timing-settings.toml"),
},
}
@@ -409,17 +452,14 @@ func (as *AppearanceSettingsData) SaveScreenZoom() { //types:add
errors.Log(SaveSettings(as))
}
-// DeviceSettingsData is the data type for the device settings.
-type DeviceSettingsData struct { //types:add
+// TimingSettingsData is the data type for the timing settings.
+type TimingSettingsData struct { //types:add
SettingsBase
- // The keyboard shortcut map to use
- KeyMap keymap.MapName
-
- // The keyboard shortcut maps available as options for Key map.
- // If you do not want to have custom key maps, you should leave
- // this unset so that you always have the latest standard key maps.
- KeyMaps option.Option[keymap.Maps]
+ // SnackbarTimeout is the default amount of time until snackbars
+ // disappear (snackbars show short updates about app processes
+ // at the bottom of the screen)
+ SnackbarTimeout time.Duration `default:"5s"`
// The maximum time interval between button press events to count as a double-click
DoubleClickInterval time.Duration `default:"500ms" min:"100ms" step:"50ms"`
@@ -461,22 +501,33 @@ type DeviceSettingsData struct { //types:add
// The maximum number of pixels that mouse/finger can move and still register a long press event
LongPressStopDistance int `default:"50" min:"0" max:"1000" step:"1"`
-}
-func (ds *DeviceSettingsData) Defaults() {
- ds.KeyMap = keymap.DefaultMap
- ds.KeyMaps.Value = keymap.AvailableMaps
+ // the amount of time to wait before offering completions
+ CompleteWaitDuration time.Duration `default:"0ms" min:"0ms" max:"10s" step:"10ms"`
+
+ // time interval for cursor blinking on and off -- set to 0 to disable blinking
+ CursorBlinkTime time.Duration `default:"500ms" min:"0ms" max:"1s" step:"5ms"`
+
+ // The amount of time to wait before trying to autoscroll again
+ LayoutAutoScrollDelay time.Duration `default:"25ms" min:"1ms" step:"5ms"`
+
+ // number of steps to take in PageUp / Down events in terms of number of items
+ LayoutPageSteps int `default:"10" min:"1" step:"1"`
+
+ // the amount of time between keypresses to combine characters into name
+ // to search for within layout -- starts over after this delay.
+ LayoutFocusNameTimeout time.Duration `default:"500ms" min:"0ms" max:"5s" step:"20ms"`
+
+ // the amount of time since last focus name event to allow tab to focus
+ // on next element with same name.
+ LayoutFocusNameTabTime time.Duration `default:"2s" min:"10ms" max:"10s" step:"100ms"`
}
-func (ds *DeviceSettingsData) Apply() {
- if ds.KeyMaps.Valid {
- keymap.AvailableMaps = ds.KeyMaps.Value
- }
- if ds.KeyMap != "" {
- keymap.SetActiveMapName(ds.KeyMap)
- }
+func (ts *TimingSettingsData) Defaults() {
+}
- events.ScrollWheelSpeed = ds.ScrollWheelSpeed
+func (ts *TimingSettingsData) Apply() {
+ events.ScrollWheelSpeed = ts.ScrollWheelSpeed
}
// ScreenSettings are per-screen settings that override the global settings.
@@ -498,21 +549,23 @@ var SystemSettings = &SystemSettingsData{
type SystemSettingsData struct { //types:add
SettingsBase
+ // The keyboard shortcut map to use
+ KeyMap keymap.MapName
+
+ // The keyboard shortcut maps available as options for Key map.
+ // If you do not want to have custom key maps, you should leave
+ // this unset so that you always have the latest standard key maps.
+ KeyMaps option.Option[keymap.Maps]
+
// text editor settings
Editor text.EditorSettings
// whether to use a 24-hour clock (instead of AM and PM)
Clock24 bool `label:"24-hour clock"`
- // SnackbarTimeout is the default amount of time until snackbars
- // disappear (snackbars show short updates about app processes
- // at the bottom of the screen)
- SnackbarTimeout time.Duration `default:"5s"`
-
- // only support closing the currently selected active tab;
- // if this is set to true, pressing the close button on other tabs
- // will take you to that tab, from which you can close it.
- OnlyCloseActiveTab bool `default:"false"`
+ // user info, which is partially filled-out automatically if empty
+ // when settings are first created.
+ User User
// the limit of file size, above which user will be prompted before
// opening / copying, etc.
@@ -521,63 +574,25 @@ type SystemSettingsData struct { //types:add
// maximum number of saved paths to save in FilePicker
SavedPathsMax int `default:"50"`
- // user info, which is partially filled-out automatically if empty
- // when settings are first created.
- User User
-
// favorite paths, shown in FilePickerer and also editable there
FavPaths favoritePaths
-
- // column to sort by in FilePicker, and :up or :down for direction.
- // Updated automatically via FilePicker
- FilePickerSort string `display:"-"`
-
- // the maximum height of any menu popup panel in units of font height;
- // scroll bars are enforced beyond that size.
- MenuMaxHeight int `default:"30" min:"5" step:"1"`
-
- // the amount of time to wait before offering completions
- CompleteWaitDuration time.Duration `default:"0ms" min:"0ms" max:"10s" step:"10ms"`
-
- // the maximum number of completions offered in popup
- CompleteMaxItems int `default:"25" min:"5" step:"1"`
-
- // time interval for cursor blinking on and off -- set to 0 to disable blinking
- CursorBlinkTime time.Duration `default:"500ms" min:"0ms" max:"1s" step:"5ms"`
-
- // The amount of time to wait before trying to autoscroll again
- LayoutAutoScrollDelay time.Duration `default:"25ms" min:"1ms" step:"5ms"`
-
- // number of steps to take in PageUp / Down events in terms of number of items
- LayoutPageSteps int `default:"10" min:"1" step:"1"`
-
- // the amount of time between keypresses to combine characters into name
- // to search for within layout -- starts over after this delay.
- LayoutFocusNameTimeout time.Duration `default:"500ms" min:"0ms" max:"5s" step:"20ms"`
-
- // the amount of time since last focus name event to allow tab to focus
- // on next element with same name.
- LayoutFocusNameTabTime time.Duration `default:"2s" min:"10ms" max:"10s" step:"100ms"`
-
- // the number of map elements at or below which an inline representation
- // of the map will be presented, which is more convenient for small #'s of properties
- MapInlineLength int `default:"2" min:"1" step:"1"`
-
- // the number of elemental struct fields at or below which an inline representation
- // of the struct will be presented, which is more convenient for small structs
- StructInlineLength int `default:"4" min:"2" step:"1"`
-
- // the number of slice elements below which inline will be used
- SliceInlineLength int `default:"4" min:"2" step:"1"`
}
func (ss *SystemSettingsData) Defaults() {
+ ss.KeyMap = keymap.DefaultMap
+ ss.KeyMaps.Value = keymap.AvailableMaps
ss.FavPaths.setToDefaults()
ss.updateUser()
}
// Apply detailed settings to all the relevant settings.
func (ss *SystemSettingsData) Apply() { //types:add
+ if ss.KeyMaps.Valid {
+ keymap.AvailableMaps = ss.KeyMaps.Value
+ }
+ if ss.KeyMap != "" {
+ keymap.SetActiveMapName(ss.KeyMap)
+ }
np := len(ss.FavPaths)
for i := 0; i < np; i++ {
if ss.FavPaths[i].Icon == "" || ss.FavPaths[i].Icon == "folder" {
diff --git a/core/snackbar.go b/core/snackbar.go
index 3f4accdd96..5dac0237ce 100644
--- a/core/snackbar.go
+++ b/core/snackbar.go
@@ -30,7 +30,7 @@ func (bd *Body) NewSnackbar(ctx Widget) *Stage {
ctx = nonNilContext(ctx)
bd.snackbarStyles()
bd.Scene.Stage = NewPopupStage(SnackbarStage, bd.Scene, ctx).
- SetTimeout(SystemSettings.SnackbarTimeout)
+ SetTimeout(TimingSettings.SnackbarTimeout)
return bd.Scene.Stage
}
diff --git a/core/snackbar_test.go b/core/snackbar_test.go
index 56f4b01b38..e3ba943f1a 100644
--- a/core/snackbar_test.go
+++ b/core/snackbar_test.go
@@ -71,10 +71,10 @@ func TestSnackbarError(t *testing.T) {
func TestSnackbarTime(t *testing.T) {
t.Skip("TODO(#1456): fix this test")
- ptimeout := SystemSettings.SnackbarTimeout
- SystemSettings.SnackbarTimeout = 50 * time.Millisecond
+ ptimeout := TimingSettings.SnackbarTimeout
+ TimingSettings.SnackbarTimeout = 50 * time.Millisecond
defer func() {
- SystemSettings.SnackbarTimeout = ptimeout
+ TimingSettings.SnackbarTimeout = ptimeout
}()
times := []time.Duration{0, 25 * time.Millisecond, 75 * time.Millisecond}
for _, tm := range times {
diff --git a/core/splits.go b/core/splits.go
index 74a4c71862..2ac5292f36 100644
--- a/core/splits.go
+++ b/core/splits.go
@@ -730,7 +730,7 @@ func (sl *Splits) SizeDownSetAllocs(iter int) {
ksz := &cwb.Geom.Size
ksz.Alloc.Total.SetDim(dim, szm)
ksz.Alloc.Total.SetDim(odim, szc)
- ksz.setContentFromTotal(&ksz.Alloc)
+ ksz.SetContentFromTotal(&ksz.Alloc)
}
ci := 0
diff --git a/core/stages.go b/core/stages.go
index 1dcb7708b7..e4e39c20e3 100644
--- a/core/stages.go
+++ b/core/stages.go
@@ -216,7 +216,7 @@ func (sm *stages) resize(rg math32.Geom2DInt) bool {
for _, kv := range sm.stack.Order {
st := kv.Value
if st.FullWindow {
- did := st.Scene.resize(rg)
+ did := st.Scene.Resize(rg)
if did {
st.Sprites.reset()
resized = true
diff --git a/core/tabs.go b/core/tabs.go
index 5e34e56d12..2ec2d54db2 100644
--- a/core/tabs.go
+++ b/core/tabs.go
@@ -534,7 +534,7 @@ func (tb *Tab) Init() {
ts := tb.tabs()
idx := ts.tabIndexByName(tb.Name)
// if OnlyCloseActiveTab is on, only process delete when already selected
- if SystemSettings.OnlyCloseActiveTab && !tb.StateIs(states.Selected) {
+ if AppearanceSettings.OnlyCloseActiveTab && !tb.StateIs(states.Selected) {
ts.SelectTabIndex(idx)
} else {
ts.DeleteTabIndex(idx)
diff --git a/core/text.go b/core/text.go
index 2e35a39997..6c0f82878e 100644
--- a/core/text.go
+++ b/core/text.go
@@ -7,7 +7,9 @@ package core
import (
"fmt"
"image"
+ "strconv"
+ "cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
@@ -119,6 +121,13 @@ const (
func (tx *Text) WidgetValue() any { return &tx.Text }
+// PaintText returns the shaped representation of the text,
+// which is needed for some specific special-case rendering
+// situations.
+func (tx *Text) PaintText() *shaped.Lines {
+ return tx.paintText
+}
+
func (tx *Text) Init() {
tx.WidgetBase.Init()
tx.AddContextMenu(tx.contextMenu)
@@ -420,7 +429,27 @@ func (tx *Text) configTextSize(sz math32.Vector2) {
}
sty, tsty := tx.Styles.NewRichText()
tsty.Align, tsty.AlignV = text.Start, text.Start
- tx.paintText = tx.Scene.TextShaper().WrapLines(tx.richText, sty, tsty, &AppearanceSettings.Text, sz)
+ tx.paintText = tx.Scene.TextShaper().WrapLines(tx.richText, sty, tsty, sz)
+ tx.setAnchorFromProperties()
+}
+
+// setAnchorFromProperties sets the Anchor string on paintText based on
+// id and tag properties, which is needed for PDF print formatting.
+// This is the only point at which we know we have the final
+// shaped.Lines for a text item.
+func (tx *Text) setAnchorFromProperties() {
+ anc := ""
+ if id, ok := tx.Properties["id"]; ok {
+ anc = id.(string)
+ }
+ if t, ok := tx.Properties["tag"]; ok {
+ tag := t.(string)
+ if len(tag) > 1 && tag[0] == 'h' { // header
+ level := errors.Log1(strconv.Atoi(tag[1:]))
+ anc += fmt.Sprintf(";Header %d:%s", level, tx.Text)
+ }
+ }
+ tx.paintText.Anchor = anc
}
// configTextAlloc is used for determining how much space the text
@@ -441,10 +470,11 @@ func (tx *Text) configTextAlloc(sz math32.Vector2) math32.Vector2 {
if tsty.Align != text.Start && tsty.AlignV != text.Start {
etxs := *tsty
etxs.Align, etxs.AlignV = text.Start, text.Start
- tx.paintText = tsh.WrapLines(tx.richText, sty, &etxs, &AppearanceSettings.Text, rsz)
+ tx.paintText = tsh.WrapLines(tx.richText, sty, &etxs, rsz)
rsz = tx.paintText.Bounds.Size().Ceil()
}
- tx.paintText = tsh.WrapLines(tx.richText, sty, tsty, &AppearanceSettings.Text, rsz)
+ tx.paintText = tsh.WrapLines(tx.richText, sty, tsty, rsz)
+ tx.setAnchorFromProperties()
return tx.paintText.Bounds.Size().Ceil()
}
@@ -463,7 +493,7 @@ func (tx *Text) SizeUp() {
}
rsz := tx.paintText.Bounds.Size().Ceil()
sz.FitSizeMax(&sz.Actual.Content, rsz)
- sz.setTotalFromContent(&sz.Actual)
+ sz.SetTotalFromContent(&sz.Actual)
if DebugSettings.LayoutTrace {
fmt.Println(tx, "Text SizeUp:", rsz, "Actual:", sz.Actual.Content)
}
@@ -480,7 +510,7 @@ func (tx *Text) SizeDown(iter int) bool {
// start over so we don't reflect hysteresis of prior guess
sz.setInitContentMin(tx.Styles.Min.Dots().Ceil())
sz.FitSizeMax(&sz.Actual.Content, rsz)
- sz.setTotalFromContent(&sz.Actual)
+ sz.SetTotalFromContent(&sz.Actual)
chg := prevContent != sz.Actual.Content
if chg {
if DebugSettings.LayoutTrace {
diff --git a/core/textcursor.go b/core/textcursor.go
index 3e2dad727b..05f86255e0 100644
--- a/core/textcursor.go
+++ b/core/textcursor.go
@@ -63,7 +63,7 @@ func TextCursor(on bool, w *WidgetBase, lastW *tree.Node, name string, width, he
if !turnOn {
isOn := sp.Properties["on"].(bool)
lastSwitch := sp.Properties["lastSwitch"].(time.Time)
- if TheApp.Platform() != system.Offscreen && SystemSettings.CursorBlinkTime > 0 && time.Since(lastSwitch) > SystemSettings.CursorBlinkTime {
+ if TheApp.Platform() != system.Offscreen && TimingSettings.CursorBlinkTime > 0 && time.Since(lastSwitch) > TimingSettings.CursorBlinkTime {
isOn = !isOn
sp.Properties["on"] = isOn
sp.Properties["lastSwitch"] = time.Now()
diff --git a/core/textfield.go b/core/textfield.go
index b0c0e20cc8..198d42c7cf 100644
--- a/core/textfield.go
+++ b/core/textfield.go
@@ -1618,7 +1618,7 @@ func (tf *TextField) configTextSize(sz math32.Vector2) math32.Vector2 {
etxs := *tsty
etxs.Align, etxs.AlignV = text.Start, text.Start // only works with this
tx := rich.NewText(sty, txt)
- tf.renderAll = tf.Scene.TextShaper().WrapLines(tx, sty, &etxs, &AppearanceSettings.Text, sz)
+ tf.renderAll = tf.Scene.TextShaper().WrapLines(tx, sty, &etxs, sz)
rsz := tf.renderAll.Bounds.Size().Ceil()
return rsz
}
@@ -1651,7 +1651,7 @@ func (tf *TextField) SizeUp() {
rsz := tf.configTextSize(availSz)
rsz.SetAdd(icsz)
sz.FitSizeMax(&sz.Actual.Content, rsz)
- sz.setTotalFromContent(&sz.Actual)
+ sz.SetTotalFromContent(&sz.Actual)
tf.lineHeight = tf.Styles.LineHeightDots()
if DebugSettings.LayoutTrace {
fmt.Println(tf, "TextField SizeUp:", rsz, "Actual:", sz.Actual.Content)
@@ -1681,7 +1681,7 @@ func (tf *TextField) SizeDown(iter int) bool {
rsz.Y = max(pgrow.Y, rsz.Y)
}
sz.FitSizeMax(&sz.Actual.Content, rsz)
- sz.setTotalFromContent(&sz.Actual)
+ sz.SetTotalFromContent(&sz.Actual)
sz.Alloc = sz.Actual // this is important for constraining our children layout:
redo := tf.Frame.SizeDown(iter)
return chg || redo
@@ -1741,7 +1741,7 @@ func (tf *TextField) layoutCurrent() {
sty, tsty := tf.Styles.NewRichText()
tsty.Color = colors.ToUniform(clr)
tx := rich.NewText(sty, cur)
- tf.renderVisible = tf.Scene.TextShaper().WrapLines(tx, sty, tsty, &AppearanceSettings.Text, availSz)
+ tf.renderVisible = tf.Scene.TextShaper().WrapLines(tx, sty, tsty, availSz)
tf.renderedRange = tf.dispRange
}
diff --git a/core/toolbar.go b/core/toolbar.go
index 1bf4effbac..e2719bfbf8 100644
--- a/core/toolbar.go
+++ b/core/toolbar.go
@@ -153,7 +153,7 @@ func (tb *Toolbar) moveToOverflow() {
avsz := avail - ovsz
sz := &tb.Geom.Size
sz.Alloc.Total.SetDim(ma, avail)
- sz.setContentFromTotal(&sz.Alloc)
+ sz.SetContentFromTotal(&sz.Alloc)
n := len(tb.Children)
pn := len(tb.allItemsPlan.Children)
ovidx := n - 1
diff --git a/core/tree.go b/core/tree.go
index 9a6ca2bf4f..adac720e4f 100644
--- a/core/tree.go
+++ b/core/tree.go
@@ -592,7 +592,7 @@ func (tr *Tree) SizeUp() {
}
sz := &tr.Geom.Size
sz.Actual.Content = math32.Vec2(w, h)
- sz.setTotalFromContent(&sz.Actual)
+ sz.SetTotalFromContent(&sz.Actual)
sz.Alloc = sz.Actual // need allocation to match!
tr.widgetSize.X = w // stretch
}
@@ -618,7 +618,7 @@ func (tr *Tree) Position() {
sz.Alloc = sz.Actual
psz := &tr.Parts.Geom.Size
psz.Alloc.Total = tr.widgetSize
- psz.setContentFromTotal(&psz.Alloc)
+ psz.SetContentFromTotal(&psz.Alloc)
tr.WidgetBase.Position() // just does our parts
@@ -637,7 +637,7 @@ func (tr *Tree) Position() {
func (tr *Tree) ApplyScenePos() {
sz := &tr.Geom.Size
if sz.Actual.Total == tr.widgetSize {
- sz.setTotalFromContent(&sz.Actual) // restore after scrolling
+ sz.SetTotalFromContent(&sz.Actual) // restore after scrolling
}
tr.WidgetBase.ApplyScenePos()
tr.applyScenePosChildren()
@@ -1467,7 +1467,7 @@ func (tr *Tree) PasteAssign(md mimedata.Mimes) {
return
}
tr.CopyFrom(sl[0]) // nodes with data copy here
- tr.setScene(tr.Scene) // ensure children have scene
+ tr.SetScene(tr.Scene) // ensure children have scene
tr.Update() // could have children
tr.Open()
tr.sendChangeEvent()
@@ -1525,7 +1525,7 @@ func (tr *Tree) pasteAt(md mimedata.Mimes, mod events.DropMods, rel int, actNm s
parent.InsertChild(ns, myidx+i)
nwb := AsWidget(ns)
AsTree(ns).Root = tr.Root
- nwb.setScene(tr.Scene)
+ nwb.SetScene(tr.Scene)
nwb.Update() // incl children
npath := nst.PathFrom(tr.Root)
if mod == events.DropMove && npath == orgpath { // we will be nuked immediately after drag
@@ -1555,7 +1555,7 @@ func (tr *Tree) PasteChildren(md mimedata.Mimes, mod events.DropMods) {
tree.SetUniqueNameIfDuplicate(tr.This, ns)
tr.AddChild(ns)
AsTree(ns).Root = tr.Root
- AsWidget(ns).setScene(tr.Scene)
+ AsWidget(ns).SetScene(tr.Scene)
}
tr.Update()
tr.Open()
diff --git a/core/typegen.go b/core/typegen.go
index 06ca9383b7..1f409a3d98 100644
--- a/core/typegen.go
+++ b/core/typegen.go
@@ -600,7 +600,7 @@ func NewPages(parent ...tree.Node) *Pages { return tree.New[Pages](parent...) }
// Page is the currently open page.
func (t *Pages) SetPage(v string) *Pages { t.Page = v; return t }
-var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Scene", IDName: "scene", Doc: "Scene contains a [Widget] tree, rooted in an embedded [Frame] layout,\nwhich renders into its own [paint.Painter]. The [Scene] is set in a\n[Stage], which the [Scene] has a pointer to.\n\nEach [Scene] contains state specific to its particular usage\nwithin a given [Stage] and overall rendering context, representing the unit\nof rendering in the Cogent Core framework.", Directives: []types.Directive{{Tool: "core", Directive: "no-new"}}, Methods: []types.Method{{Name: "standardContextMenu", Doc: "standardContextMenu adds standard context menu items for the [Scene].", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"m"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Body", Doc: "Body provides the main contents of scenes that use control Bars\nto allow the main window contents to be specified separately\nfrom that dynamic control content. When constructing scenes using\na [Body], you can operate directly on the [Body], which has wrappers\nfor most major Scene functions."}, {Name: "WidgetInit", Doc: "WidgetInit is a function called on every newly created [Widget].\nThis can be used to set global configuration and styling for all\nwidgets in conjunction with [App.SceneInit]."}, {Name: "Bars", Doc: "Bars are functions for creating control bars,\nattached to different sides of a [Scene]. Functions\nare called in forward order so first added are called first."}, {Name: "Data", Doc: "Data is the optional data value being represented by this scene.\nUsed e.g., for recycling views of a given item instead of creating new one."}, {Name: "SceneGeom", Doc: "Size and position relative to overall rendering context."}, {Name: "Painter", Doc: "painter for rendering all widgets in the scene."}, {Name: "Events", Doc: "event manager for this scene."}, {Name: "Stage", Doc: "current stage in which this Scene is set."}, {Name: "Animations", Doc: "Animations are the currently active [Animation]s in this scene."}, {Name: "renderBBoxes", Doc: "renderBBoxes indicates to render colored bounding boxes for all of the widgets\nin the scene. This is enabled by the [Inspector] in select element mode."}, {Name: "renderBBoxHue", Doc: "renderBBoxHue is current hue for rendering bounding box in [Scene.RenderBBoxes] mode."}, {Name: "selectedWidget", Doc: "selectedWidget is the currently selected/hovered widget through the [Inspector] selection mode\nthat should be highlighted with a background color."}, {Name: "selectedWidgetChan", Doc: "selectedWidgetChan is the channel on which the selected widget through the inspect editor\nselection mode is transmitted to the inspect editor after the user is done selecting."}, {Name: "renderer", Doc: "source renderer for rendering the scene"}, {Name: "lastRender", Doc: "lastRender captures key params from last render.\nIf different then a new ApplyStyleScene is needed."}, {Name: "showIter", Doc: "showIter counts up at start of showing a Scene\nto trigger Show event and other steps at start of first show"}, {Name: "directRenders", Doc: "directRenders are widgets that render directly to the [RenderWindow]\ninstead of rendering into the Scene Painter."}, {Name: "flags", Doc: "flags are atomic bit flags for [Scene] state."}}})
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Scene", IDName: "scene", Doc: "Scene contains a [Widget] tree, rooted in an embedded [Frame] layout,\nwhich renders into its own [paint.Painter]. The [Scene] is set in a\n[Stage], which the [Scene] has a pointer to.\n\nEach [Scene] contains state specific to its particular usage\nwithin a given [Stage] and overall rendering context, representing the unit\nof rendering in the Cogent Core framework.", Directives: []types.Directive{{Tool: "core", Directive: "no-new"}}, Methods: []types.Method{{Name: "standardContextMenu", Doc: "standardContextMenu adds standard context menu items for the [Scene].", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"m"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Body", Doc: "Body provides the main contents of scenes that use control Bars\nto allow the main window contents to be specified separately\nfrom that dynamic control content. When constructing scenes using\na [Body], you can operate directly on the [Body], which has wrappers\nfor most major Scene functions."}, {Name: "WidgetInit", Doc: "WidgetInit is a function called on every newly created [Widget].\nThis can be used to set global configuration and styling for all\nwidgets in conjunction with [App.SceneInit]."}, {Name: "Bars", Doc: "Bars are functions for creating control bars,\nattached to different sides of a [Scene]. Functions\nare called in forward order so first added are called first."}, {Name: "Data", Doc: "Data is the optional data value being represented by this scene.\nUsed e.g., for recycling views of a given item instead of creating new one."}, {Name: "SceneGeom", Doc: "Size and position relative to overall rendering context."}, {Name: "Painter", Doc: "painter for rendering all widgets in the scene."}, {Name: "Events", Doc: "event manager for this scene."}, {Name: "Stage", Doc: "current stage in which this Scene is set."}, {Name: "Animations", Doc: "Animations are the currently active [Animation]s in this scene."}, {Name: "renderBBoxes", Doc: "renderBBoxes indicates to render colored bounding boxes for all of the widgets\nin the scene. This is enabled by the [Inspector] in select element mode."}, {Name: "renderBBoxHue", Doc: "renderBBoxHue is current hue for rendering bounding box in [Scene.RenderBBoxes] mode."}, {Name: "selectedWidget", Doc: "selectedWidget is the currently selected/hovered widget through the [Inspector] selection mode\nthat should be highlighted with a background color."}, {Name: "selectedWidgetChan", Doc: "selectedWidgetChan is the channel on which the selected widget through the inspect editor\nselection mode is transmitted to the inspect editor after the user is done selecting."}, {Name: "renderer", Doc: "source renderer for rendering the scene"}, {Name: "lastRender", Doc: "lastRender captures key params from last render.\nIf different then a new ApplyStyleScene is needed."}, {Name: "showIter", Doc: "showIter counts up at start of showing a Scene\nto trigger Show event and other steps at start of first show"}, {Name: "directRenders", Doc: "directRenders are widgets that render directly to the [RenderWindow]\ninstead of rendering into the Scene Painter."}, {Name: "textShaper", Doc: "this is our own text shaper in case we don't have a render context"}, {Name: "flags", Doc: "flags are atomic bit flags for [Scene] state."}}})
// SetWidgetInit sets the [Scene.WidgetInit]:
// WidgetInit is a function called on every newly created [Widget].
@@ -620,13 +620,13 @@ var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Separator", ID
// specified by [styles.Style.Direction].
func NewSeparator(parent ...tree.Node) *Separator { return tree.New[Separator](parent...) }
-var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.AppearanceSettingsData", IDName: "appearance-settings-data", Doc: "AppearanceSettingsData is the data type for the global Cogent Core appearance settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Methods: []types.Method{{Name: "Apply", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "deleteSavedWindowGeometries", Doc: "deleteSavedWindowGeometries deletes the file that saves the position and size of\neach window, by screen, and clear current in-memory cache. You shouldn't generally\nneed to do this, but sometimes it is useful for testing or windows that are\nshowing up in bad places that you can't recover from.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "SaveScreenZoom", Doc: "SaveScreenZoom saves the current zoom factor for the current screen,\nwhich will then be used for this screen instead of overall default.\nUse the Control +/- keyboard shortcut to modify the screen zoom level.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "SettingsBase"}}, Fields: []types.Field{{Name: "Theme", Doc: "the color theme."}, {Name: "Color", Doc: "the primary color used to generate the color scheme."}, {Name: "Zoom", Doc: "overall zoom factor as a percentage of the default zoom.\nUse Control +/- keyboard shortcut to change zoom level anytime.\nScreen-specific zoom factor will be used if present, see 'Screens' field."}, {Name: "Spacing", Doc: "the overall spacing factor as a percentage of the default amount of spacing\n(higher numbers lead to more space and lower numbers lead to higher density)."}, {Name: "FontSize", Doc: "the overall font size factor applied to all text as a percentage\nof the default font size (higher numbers lead to larger text)."}, {Name: "DocsFontSize", Doc: "Font size factor applied only to documentation and other\ndense text contexts, not normal interactive elements.\nIt is a percentage of the base Font size setting (higher numbers\nlead to larger text)."}, {Name: "ZebraStripes", Doc: "the amount that alternating rows are highlighted when showing\ntabular data (set to 0 to disable zebra striping)."}, {Name: "Screens", Doc: "screen-specific settings, which will override overall defaults if set,\nso different screens can use different zoom levels.\nUse 'Save screen zoom' in the toolbar to save the current zoom for the current\nscreen, and Control +/- keyboard shortcut to change this zoom level anytime."}, {Name: "Highlighting", Doc: "text highlighting style / theme."}, {Name: "Text", Doc: "Text specifies text settings including the language, and the\nfont families for different styles of fonts."}}})
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.AppearanceSettingsData", IDName: "appearance-settings-data", Doc: "AppearanceSettingsData is the data type for the global Cogent Core appearance settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Methods: []types.Method{{Name: "Apply", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "deleteSavedWindowGeometries", Doc: "deleteSavedWindowGeometries deletes the file that saves the position and size of\neach window, by screen, and clear current in-memory cache. You shouldn't generally\nneed to do this, but sometimes it is useful for testing or windows that are\nshowing up in bad places that you can't recover from.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "SaveScreenZoom", Doc: "SaveScreenZoom saves the current zoom factor for the current screen,\nwhich will then be used for this screen instead of overall default.\nUse the Control +/- keyboard shortcut to modify the screen zoom level.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "SettingsBase"}}, Fields: []types.Field{{Name: "Theme", Doc: "the color theme."}, {Name: "Color", Doc: "the primary color used to generate the color scheme."}, {Name: "Zoom", Doc: "overall zoom factor as a percentage of the default zoom.\nUse Control +/- keyboard shortcut to change zoom level anytime.\nScreen-specific zoom factor will be used if present, see 'Screens' field."}, {Name: "Spacing", Doc: "the overall spacing factor as a percentage of the default amount of spacing\n(higher numbers lead to more space and lower numbers lead to higher density)."}, {Name: "FontSize", Doc: "the overall font size factor applied to all text as a percentage\nof the default font size (higher numbers lead to larger text)."}, {Name: "DocsFontSize", Doc: "Font size factor applied only to documentation and other\ndense text contexts, not normal interactive elements.\nIt is a percentage of the base Font size setting (higher numbers\nlead to larger text)."}, {Name: "ZebraStripes", Doc: "the amount that alternating rows are highlighted when showing\ntabular data (set to 0 to disable zebra striping)."}, {Name: "Screens", Doc: "screen-specific settings, which will override overall defaults if set,\nso different screens can use different zoom levels.\nUse 'Save screen zoom' in the toolbar to save the current zoom for the current\nscreen, and Control +/- keyboard shortcut to change this zoom level anytime."}, {Name: "Highlighting", Doc: "text highlighting style / theme."}, {Name: "Text", Doc: "Text specifies text settings including the language, and the\nfont families for different styles of fonts."}, {Name: "OnlyCloseActiveTab", Doc: "only support closing the currently selected active tab;\nif this is set to true, pressing the close button on other tabs\nwill take you to that tab, from which you can close it."}, {Name: "MenuMax", Doc: "the maximum number of items in a menu popup panel;\nscroll bars are enforced beyond that size, or for\ncompletion, this is the max number of items shown."}, {Name: "FilePickerSort", Doc: "column to sort by in FilePicker, and :up or :down for direction.\nUpdated automatically via FilePicker"}, {Name: "InlineLengths", Doc: "length of inline elements to display for containers in Form widgets."}}})
-var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.DeviceSettingsData", IDName: "device-settings-data", Doc: "DeviceSettingsData is the data type for the device settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Embeds: []types.Field{{Name: "SettingsBase"}}, Fields: []types.Field{{Name: "KeyMap", Doc: "The keyboard shortcut map to use"}, {Name: "KeyMaps", Doc: "The keyboard shortcut maps available as options for Key map.\nIf you do not want to have custom key maps, you should leave\nthis unset so that you always have the latest standard key maps."}, {Name: "DoubleClickInterval", Doc: "The maximum time interval between button press events to count as a double-click"}, {Name: "ScrollWheelSpeed", Doc: "How fast the scroll wheel moves, which is typically pixels per wheel step\nbut units can be arbitrary. It is generally impossible to standardize speed\nand variable across devices, and we don't have access to the system settings,\nso unfortunately you have to set it here."}, {Name: "ScrollFocusTime", Doc: "The duration over which the current scroll widget retains scroll focus,\nsuch that subsequent scroll events are sent to it."}, {Name: "SlideStartTime", Doc: "The amount of time to wait before initiating a slide event\n(as opposed to a basic press event)"}, {Name: "DragStartTime", Doc: "The amount of time to wait before initiating a drag (drag and drop) event\n(as opposed to a basic press or slide event)"}, {Name: "RepeatClickTime", Doc: "The amount of time to wait between each repeat click event,\nwhen the mouse is pressed down. The first click is 8x this."}, {Name: "DragStartDistance", Doc: "The number of pixels that must be moved before initiating a slide/drag\nevent (as opposed to a basic press event)"}, {Name: "LongHoverTime", Doc: "The amount of time to wait before initiating a long hover event (e.g., for opening a tooltip)"}, {Name: "LongHoverStopDistance", Doc: "The maximum number of pixels that mouse can move and still register a long hover event"}, {Name: "LongPressTime", Doc: "The amount of time to wait before initiating a long press event (e.g., for opening a tooltip)"}, {Name: "LongPressStopDistance", Doc: "The maximum number of pixels that mouse/finger can move and still register a long press event"}}})
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.TimingSettingsData", IDName: "timing-settings-data", Doc: "TimingSettingsData is the data type for the timing settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Embeds: []types.Field{{Name: "SettingsBase"}}, Fields: []types.Field{{Name: "SnackbarTimeout", Doc: "SnackbarTimeout is the default amount of time until snackbars\ndisappear (snackbars show short updates about app processes\nat the bottom of the screen)"}, {Name: "DoubleClickInterval", Doc: "The maximum time interval between button press events to count as a double-click"}, {Name: "ScrollWheelSpeed", Doc: "How fast the scroll wheel moves, which is typically pixels per wheel step\nbut units can be arbitrary. It is generally impossible to standardize speed\nand variable across devices, and we don't have access to the system settings,\nso unfortunately you have to set it here."}, {Name: "ScrollFocusTime", Doc: "The duration over which the current scroll widget retains scroll focus,\nsuch that subsequent scroll events are sent to it."}, {Name: "SlideStartTime", Doc: "The amount of time to wait before initiating a slide event\n(as opposed to a basic press event)"}, {Name: "DragStartTime", Doc: "The amount of time to wait before initiating a drag (drag and drop) event\n(as opposed to a basic press or slide event)"}, {Name: "RepeatClickTime", Doc: "The amount of time to wait between each repeat click event,\nwhen the mouse is pressed down. The first click is 8x this."}, {Name: "DragStartDistance", Doc: "The number of pixels that must be moved before initiating a slide/drag\nevent (as opposed to a basic press event)"}, {Name: "LongHoverTime", Doc: "The amount of time to wait before initiating a long hover event (e.g., for opening a tooltip)"}, {Name: "LongHoverStopDistance", Doc: "The maximum number of pixels that mouse can move and still register a long hover event"}, {Name: "LongPressTime", Doc: "The amount of time to wait before initiating a long press event (e.g., for opening a tooltip)"}, {Name: "LongPressStopDistance", Doc: "The maximum number of pixels that mouse/finger can move and still register a long press event"}, {Name: "CompleteWaitDuration", Doc: "the amount of time to wait before offering completions"}, {Name: "CursorBlinkTime", Doc: "time interval for cursor blinking on and off -- set to 0 to disable blinking"}, {Name: "LayoutAutoScrollDelay", Doc: "The amount of time to wait before trying to autoscroll again"}, {Name: "LayoutPageSteps", Doc: "number of steps to take in PageUp / Down events in terms of number of items"}, {Name: "LayoutFocusNameTimeout", Doc: "the amount of time between keypresses to combine characters into name\nto search for within layout -- starts over after this delay."}, {Name: "LayoutFocusNameTabTime", Doc: "the amount of time since last focus name event to allow tab to focus\non next element with same name."}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.ScreenSettings", IDName: "screen-settings", Doc: "ScreenSettings are per-screen settings that override the global settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "Zoom", Doc: "overall zoom factor as a percentage of the default zoom"}}})
-var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.SystemSettingsData", IDName: "system-settings-data", Doc: "SystemSettingsData is the data type of the global Cogent Core settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Methods: []types.Method{{Name: "Apply", Doc: "Apply detailed settings to all the relevant settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "SettingsBase"}}, Fields: []types.Field{{Name: "Editor", Doc: "text editor settings"}, {Name: "Clock24", Doc: "whether to use a 24-hour clock (instead of AM and PM)"}, {Name: "SnackbarTimeout", Doc: "SnackbarTimeout is the default amount of time until snackbars\ndisappear (snackbars show short updates about app processes\nat the bottom of the screen)"}, {Name: "OnlyCloseActiveTab", Doc: "only support closing the currently selected active tab;\nif this is set to true, pressing the close button on other tabs\nwill take you to that tab, from which you can close it."}, {Name: "BigFileSize", Doc: "the limit of file size, above which user will be prompted before\nopening / copying, etc."}, {Name: "SavedPathsMax", Doc: "maximum number of saved paths to save in FilePicker"}, {Name: "User", Doc: "user info, which is partially filled-out automatically if empty\nwhen settings are first created."}, {Name: "FavPaths", Doc: "favorite paths, shown in FilePickerer and also editable there"}, {Name: "FilePickerSort", Doc: "column to sort by in FilePicker, and :up or :down for direction.\nUpdated automatically via FilePicker"}, {Name: "MenuMaxHeight", Doc: "the maximum height of any menu popup panel in units of font height;\nscroll bars are enforced beyond that size."}, {Name: "CompleteWaitDuration", Doc: "the amount of time to wait before offering completions"}, {Name: "CompleteMaxItems", Doc: "the maximum number of completions offered in popup"}, {Name: "CursorBlinkTime", Doc: "time interval for cursor blinking on and off -- set to 0 to disable blinking"}, {Name: "LayoutAutoScrollDelay", Doc: "The amount of time to wait before trying to autoscroll again"}, {Name: "LayoutPageSteps", Doc: "number of steps to take in PageUp / Down events in terms of number of items"}, {Name: "LayoutFocusNameTimeout", Doc: "the amount of time between keypresses to combine characters into name\nto search for within layout -- starts over after this delay."}, {Name: "LayoutFocusNameTabTime", Doc: "the amount of time since last focus name event to allow tab to focus\non next element with same name."}, {Name: "MapInlineLength", Doc: "the number of map elements at or below which an inline representation\nof the map will be presented, which is more convenient for small #'s of properties"}, {Name: "StructInlineLength", Doc: "the number of elemental struct fields at or below which an inline representation\nof the struct will be presented, which is more convenient for small structs"}, {Name: "SliceInlineLength", Doc: "the number of slice elements below which inline will be used"}}})
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.SystemSettingsData", IDName: "system-settings-data", Doc: "SystemSettingsData is the data type of the global Cogent Core settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Methods: []types.Method{{Name: "Apply", Doc: "Apply detailed settings to all the relevant settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "SettingsBase"}}, Fields: []types.Field{{Name: "KeyMap", Doc: "The keyboard shortcut map to use"}, {Name: "KeyMaps", Doc: "The keyboard shortcut maps available as options for Key map.\nIf you do not want to have custom key maps, you should leave\nthis unset so that you always have the latest standard key maps."}, {Name: "Editor", Doc: "text editor settings"}, {Name: "Clock24", Doc: "whether to use a 24-hour clock (instead of AM and PM)"}, {Name: "User", Doc: "user info, which is partially filled-out automatically if empty\nwhen settings are first created."}, {Name: "BigFileSize", Doc: "the limit of file size, above which user will be prompted before\nopening / copying, etc."}, {Name: "SavedPathsMax", Doc: "maximum number of saved paths to save in FilePicker"}, {Name: "FavPaths", Doc: "favorite paths, shown in FilePickerer and also editable there"}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.User", IDName: "user", Doc: "User basic user information that might be needed for different apps", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Embeds: []types.Field{{Name: "User"}}, Fields: []types.Field{{Name: "Email", Doc: "default email address -- e.g., for recording changes in a version control system"}}})
@@ -1195,7 +1195,7 @@ func NewToolbar(parent ...tree.Node) *Toolbar { return tree.New[Toolbar](parent.
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Treer", IDName: "treer", Doc: "Treer is an interface for [Tree] types\nproviding access to the base [Tree] and\noverridable method hooks for actions taken on the [Tree],\nincluding OnOpen, OnClose, etc.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Methods: []types.Method{{Name: "AsCoreTree", Doc: "AsTree returns the base [Tree] for this node.", Returns: []string{"Tree"}}, {Name: "CanOpen", Doc: "CanOpen returns true if the node is able to open.\nBy default it checks HasChildren(), but could check other properties\nto perform lazy building of the tree.", Returns: []string{"bool"}}, {Name: "OnOpen", Doc: "OnOpen is called when a node is toggled open.\nThe base version does nothing."}, {Name: "OnClose", Doc: "OnClose is called when a node is toggled closed.\nThe base version does nothing."}, {Name: "MimeData", Args: []string{"md"}}, {Name: "Cut"}, {Name: "Copy"}, {Name: "Paste"}, {Name: "DeleteSelected"}, {Name: "DragDrop", Args: []string{"e"}}, {Name: "DropDeleteSource", Args: []string{"e"}}}})
-var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Tree", IDName: "tree", Doc: "Tree provides a graphical representation of a tree structure,\nproviding full navigation and manipulation abilities.\n\nIt does not handle layout by itself, so if you want it to scroll\nseparately from the rest of the surrounding context, you must\nplace it in a [Frame].\n\nIf the [Tree.SyncNode] field is non-nil, typically via the\n[Tree.SyncTree] method, then the Tree mirrors another\ntree structure, and tree editing functions apply to\nthe source tree first, and then to the Tree by sync.\n\nOtherwise, data can be directly encoded in a Tree\nderived type, to represent any kind of tree structure\nand associated data.\n\nStandard [events.Event]s are sent to any listeners, including\n[events.Select], [events.Change], and [events.DoubleClick].\nThe selected nodes are in the root [Tree.SelectedNodes] list;\nselect events are sent to both selected nodes and the root node.\nSee [Tree.IsRootSelected] to check whether a select event on the root\nnode corresponds to the root node or another node.", Methods: []types.Method{{Name: "OpenAll", Doc: "OpenAll opens the node and all of its sub-nodes.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "CloseAll", Doc: "CloseAll closes the node and all of its sub-nodes.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "DeleteSelected", Doc: "DeleteSelected deletes selected items.\nMust be called from first node in selection.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Copy", Doc: "Copy copies the selected items to the clipboard.\nThis must be called on the first item in the selected list.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Cut", Doc: "Cut copies to [system.Clipboard] and deletes selected items.\nThis must be called on the first item in the selected list.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Paste", Doc: "Paste pastes clipboard at given node.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "InsertAfter", Doc: "InsertAfter inserts a new node in the tree\nafter this node, at the same (sibling) level,\nprompting for the type of node to insert.\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "InsertBefore", Doc: "InsertBefore inserts a new node in the tree\nbefore this node, at the same (sibling) level,\nprompting for the type of node to insert\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "AddChildNode", Doc: "AddChildNode adds a new child node to this one in the tree,\nprompting the user for the type of node to add\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "DeleteNode", Doc: "DeleteNode deletes the tree node or sync node corresponding\nto this view node in the sync tree.\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Duplicate", Doc: "Duplicate duplicates the sync node corresponding to this view node in\nthe tree, and inserts the duplicate after this node (as a new sibling).\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "EditNode", Doc: "EditNode pulls up a [Form] dialog for the node.\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "inspectNode", Doc: "inspectNode pulls up a new Inspector window on the node.\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "WidgetBase"}}, Fields: []types.Field{{Name: "SyncNode", Doc: "SyncNode, if non-nil, is the [tree.Node] that this widget is\nviewing in the tree (the source). It should be set using\n[Tree.SyncTree]."}, {Name: "Text", Doc: "Text is the text to display for the tree item label, which automatically\ndefaults to the [tree.Node.Name] of the tree node. It has no effect\nif [Tree.SyncNode] is non-nil."}, {Name: "Icon", Doc: "Icon is an optional icon displayed to the the left of the text label."}, {Name: "IconOpen", Doc: "IconOpen is the icon to use for an open (expanded) branch;\nit defaults to [icons.KeyboardArrowDown]."}, {Name: "IconClosed", Doc: "IconClosed is the icon to use for a closed (collapsed) branch;\nit defaults to [icons.KeyboardArrowRight]."}, {Name: "IconLeaf", Doc: "IconLeaf is the icon to use for a terminal node branch that has no children;\nit defaults to [icons.Blank]."}, {Name: "TreeInit", Doc: "TreeInit is a function that can be set on the root node that is called\nwith each child tree node when it is initialized. It is only\ncalled with the root node itself in [Tree.SetTreeInit], so you\nshould typically call that instead of setting this directly."}, {Name: "Indent", Doc: "Indent is the amount to indent children relative to this node.\nIt should be set in a Styler like all other style properties."}, {Name: "OpenDepth", Doc: "OpenDepth is the depth for nodes be initialized as open (default 4).\nNodes beyond this depth will be initialized as closed."}, {Name: "Closed", Doc: "Closed is whether this tree node is currently toggled closed\n(children not visible)."}, {Name: "SelectMode", Doc: "SelectMode, when set on the root node, determines whether keyboard movements should update selection."}, {Name: "viewIndex", Doc: "linear index of this node within the entire tree.\nupdated on full rebuilds and may sometimes be off,\nbut close enough for expected uses"}, {Name: "widgetSize", Doc: "size of just this node widget.\nour alloc includes all of our children, but we only draw us."}, {Name: "Root", Doc: "Root is the cached root of the tree. It is automatically set."}, {Name: "SelectedNodes", Doc: "SelectedNodes holds the currently selected nodes.\nIt is only set on the root node. See [Tree.GetSelectedNodes]\nfor a version that also works on non-root nodes."}, {Name: "actStateLayer", Doc: "actStateLayer is the actual state layer of the tree, which\nshould be used when rendering it and its parts (but not its children).\nthe reason that it exists is so that the children of the tree\n(other trees) do not inherit its stateful background color, as\nthat does not look good."}, {Name: "inOpen", Doc: "inOpen is set in the Open method to prevent recursive opening for lazy-open nodes."}, {Name: "Branch", Doc: "Branch is the branch widget that is used to open and close the tree node."}}})
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Tree", IDName: "tree", Doc: "Tree provides a graphical representation of a tree structure,\nproviding full navigation and manipulation abilities.\n\nIt does not handle layout by itself, so if you want it to scroll\nseparately from the rest of the surrounding context, you must\nplace it in a [Frame].\n\nIf the [Tree.SyncNode] field is non-nil, typically via the\n[Tree.SyncTree] method, then the Tree mirrors another\ntree structure, and tree editing functions apply to\nthe source tree first, and then to the Tree by sync.\n\nOtherwise, data can be directly encoded in a Tree\nderived type, to represent any kind of tree structure\nand associated data.\n\nStandard [events.Event]s are sent to any listeners, including\n[events.Select], [events.Change], and [events.DoubleClick].\nThe selected nodes are in the root [Tree.SelectedNodes] list;\nselect events are sent to both selected nodes and the root node.\nSee [Tree.IsRootSelected] to check whether a select event on the root\nnode corresponds to the root node or another node.", Methods: []types.Method{{Name: "OpenAll", Doc: "OpenAll opens the node and all of its sub-nodes.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "CloseAll", Doc: "CloseAll closes the node and all of its sub-nodes.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "DeleteSelected", Doc: "DeleteSelected deletes selected items.\nMust be called from first node in selection.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Copy", Doc: "Copy copies the selected items to the clipboard.\nThis must be called on the first item in the selected list.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Cut", Doc: "Cut copies to [system.Clipboard] and deletes selected items.\nThis must be called on the first item in the selected list.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Paste", Doc: "Paste pastes clipboard at given node.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "InsertAfter", Doc: "InsertAfter inserts a new node in the tree\nafter this node, at the same (sibling) level,\nprompting for the type of node to insert.\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "InsertBefore", Doc: "InsertBefore inserts a new node in the tree\nbefore this node, at the same (sibling) level,\nprompting for the type of node to insert\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "AddChildNode", Doc: "AddChildNode adds a new child node to this one in the tree,\nprompting the user for the type of node to add\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "DeleteNode", Doc: "DeleteNode deletes the tree node or sync node corresponding\nto this view node in the sync tree.\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Duplicate", Doc: "Duplicate duplicates this node, and inserts the duplicate after this node\n(as a new sibling). If SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "EditNode", Doc: "EditNode pulls up a [Form] dialog for the node.\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "inspectNode", Doc: "inspectNode pulls up a new Inspector window on the node.\nIf SyncNode is set, operates on Sync Tree.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "WidgetBase"}}, Fields: []types.Field{{Name: "SyncNode", Doc: "SyncNode, if non-nil, is the [tree.Node] that this widget is\nviewing in the tree (the source). It should be set using\n[Tree.SyncTree]."}, {Name: "Text", Doc: "Text is the text to display for the tree item label, which automatically\ndefaults to the [tree.Node.Name] of the tree node. It has no effect\nif [Tree.SyncNode] is non-nil."}, {Name: "Icon", Doc: "Icon is an optional icon displayed to the the left of the text label."}, {Name: "IconOpen", Doc: "IconOpen is the icon to use for an open (expanded) branch;\nit defaults to [icons.KeyboardArrowDown]."}, {Name: "IconClosed", Doc: "IconClosed is the icon to use for a closed (collapsed) branch;\nit defaults to [icons.KeyboardArrowRight]."}, {Name: "IconLeaf", Doc: "IconLeaf is the icon to use for a terminal node branch that has no children;\nit defaults to [icons.Blank]."}, {Name: "TreeInit", Doc: "TreeInit is a function that can be set on the root node that is called\nwith each child tree node when it is initialized. It is only\ncalled with the root node itself in [Tree.SetTreeInit], so you\nshould typically call that instead of setting this directly."}, {Name: "Indent", Doc: "Indent is the amount to indent children relative to this node.\nIt should be set in a Styler like all other style properties."}, {Name: "OpenDepth", Doc: "OpenDepth is the depth for nodes be initialized as open (default 4).\nNodes beyond this depth will be initialized as closed."}, {Name: "Closed", Doc: "Closed is whether this tree node is currently toggled closed\n(children not visible)."}, {Name: "SelectMode", Doc: "SelectMode, when set on the root node, determines whether keyboard movements should update selection."}, {Name: "viewIndex", Doc: "linear index of this node within the entire tree.\nupdated on full rebuilds and may sometimes be off,\nbut close enough for expected uses"}, {Name: "widgetSize", Doc: "size of just this node widget.\nour alloc includes all of our children, but we only draw us."}, {Name: "Root", Doc: "Root is the cached root of the tree. It is automatically set."}, {Name: "SelectedNodes", Doc: "SelectedNodes holds the currently selected nodes.\nIt is only set on the root node. See [Tree.GetSelectedNodes]\nfor a version that also works on non-root nodes."}, {Name: "actStateLayer", Doc: "actStateLayer is the actual state layer of the tree, which\nshould be used when rendering it and its parts (but not its children).\nthe reason that it exists is so that the children of the tree\n(other trees) do not inherit its stateful background color, as\nthat does not look good."}, {Name: "inOpen", Doc: "inOpen is set in the Open method to prevent recursive opening for lazy-open nodes."}, {Name: "Branch", Doc: "Branch is the branch widget that is used to open and close the tree node."}}})
// NewTree returns a new [Tree] with the given optional parent:
// Tree provides a graphical representation of a tree structure,
diff --git a/core/valuer.go b/core/valuer.go
index ef983fdc90..e1427baf36 100644
--- a/core/valuer.go
+++ b/core/valuer.go
@@ -124,13 +124,13 @@ func toValue(value any, tags reflect.StructTag) Value {
return NewSwitch()
case kind == reflect.Struct:
num := reflectx.NumAllFields(uv)
- if !noInline && (inline || num <= SystemSettings.StructInlineLength) {
+ if !noInline && (inline || num <= AppearanceSettings.InlineLengths.Struct) {
return NewForm().SetInline(true)
}
return NewFormButton()
case kind == reflect.Map:
len := uv.Len()
- if !noInline && (inline || len <= SystemSettings.MapInlineLength) {
+ if !noInline && (inline || len <= AppearanceSettings.InlineLengths.Map) {
return NewKeyedList().SetInline(true)
}
return NewKeyedListButton()
@@ -144,7 +144,7 @@ func toValue(value any, tags reflect.StructTag) Value {
return NewTextField()
}
isStruct := (reflectx.NonPointerType(elemType).Kind() == reflect.Struct)
- if !noInline && (inline || (!isStruct && sz <= SystemSettings.SliceInlineLength && !tree.IsNode(elemType))) {
+ if !noInline && (inline || (!isStruct && sz <= AppearanceSettings.InlineLengths.Slice && !tree.IsNode(elemType))) {
return NewInlineList()
}
return NewListButton()
diff --git a/core/widget.go b/core/widget.go
index 3fdc0700be..bc8169bf71 100644
--- a/core/widget.go
+++ b/core/widget.go
@@ -158,7 +158,7 @@ type WidgetBase struct {
Parts *Frame `copier:"-" json:"-" xml:"-" set:"-"`
// Geom has the full layout geometry for size and position of this widget.
- Geom geomState `edit:"-" copier:"-" json:"-" xml:"-" set:"-"`
+ Geom GeomState `edit:"-" copier:"-" json:"-" xml:"-" set:"-"`
// OverrideStyle, if true, indicates override the computed styles of the widget
// and allow directly editing [WidgetBase.Styles]. It is typically only set in
@@ -311,10 +311,10 @@ func (wb *WidgetBase) OnAdd() {
}
}
-// setScene sets the Scene pointer for this widget and all of its children.
+// SetScene sets the Scene pointer for this widget and all of its children.
// This can be necessary when creating widgets outside the usual New* paradigm,
// e.g., when reading from a JSON file.
-func (wb *WidgetBase) setScene(sc *Scene) {
+func (wb *WidgetBase) SetScene(sc *Scene) {
wb.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
cwb.Scene = sc
return tree.Continue
diff --git a/core/widgetevents.go b/core/widgetevents.go
index 45d52ce995..78c92c266b 100644
--- a/core/widgetevents.go
+++ b/core/widgetevents.go
@@ -335,9 +335,8 @@ func (wb *WidgetBase) handleWidgetStateFromMouse() {
}
})
wb.On(events.MouseUp, func(e events.Event) {
- if wb.AbilityIs(abilities.Activatable) {
- wb.SetState(false, states.Active)
- }
+ // note: not contingent on Activatable: always clear on up
+ wb.SetState(false, states.Active)
})
wb.On(events.LongPressStart, func(e events.Event) {
if wb.AbilityIs(abilities.LongPressable) {
diff --git a/docs/docs.go b/docs/docs.go
index 45ff203216..e421b55d22 100644
--- a/docs/docs.go
+++ b/docs/docs.go
@@ -59,6 +59,7 @@ func main() {
b := core.NewBody("Cogent Core Docs")
ct := content.NewContent(b).SetContent(econtent)
ctx := ct.Context
+ content.OfflineURL = "https://cogentcore.org/core"
ctx.AddWikilinkHandler(htmlcore.GoDocWikilink("doc", "cogentcore.org/core"))
b.AddTopBar(func(bar *core.Frame) {
tb := core.NewToolbar(bar)
diff --git a/go.mod b/go.mod
index aa1cc164e5..4de227f8ee 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/Masterminds/vcs v1.13.3
github.com/adrg/strutil v0.3.1
+ github.com/alecthomas/assert/v2 v2.6.0
github.com/alecthomas/chroma/v2 v2.13.0
github.com/anthonynsimon/bild v0.13.0
github.com/aymerick/douceur v0.2.0
@@ -28,6 +29,7 @@ require (
github.com/h2non/filetype v1.1.3
github.com/hack-pad/hackpadfs v0.2.1
github.com/jackmordaunt/icns/v2 v2.2.7
+ github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade
github.com/jinzhu/copier v0.4.0
github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/go-homedir v1.1.0
@@ -41,12 +43,14 @@ require (
golang.org/x/net v0.38.0
golang.org/x/oauth2 v0.27.0
golang.org/x/sys v0.31.0
+ golang.org/x/text v0.23.0
golang.org/x/tools v0.29.0
gopkg.in/yaml.v3 v3.0.1
star-tex.org/x/tex v0.6.0
)
require (
+ github.com/alecthomas/repr v0.4.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
@@ -56,6 +60,7 @@ require (
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.1 // indirect
github.com/hajimehoshi/oto v0.7.1 // indirect
+ github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -68,7 +73,6 @@ require (
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sync v0.12.0 // indirect
- golang.org/x/text v0.23.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
modernc.org/knuth v0.5.4 // indirect
modernc.org/token v1.1.0 // indirect
diff --git a/go.sum b/go.sum
index bb977f0c52..4d1a3ed256 100644
--- a/go.sum
+++ b/go.sum
@@ -100,6 +100,8 @@ github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBD
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jackmordaunt/icns/v2 v2.2.7 h1:K/RbfvuzjmjVY5y4g+XENRs8ZZatwz4YnLHypa2KwQg=
github.com/jackmordaunt/icns/v2 v2.2.7/go.mod h1:ovoTxGguSuoUGKMk5Nn3R7L7BgMQkylsO+bblBuI22A=
+github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
+github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
diff --git a/gpu/doc.go b/gpu/doc.go
index 8097babee6..fcb240abcc 100644
--- a/gpu/doc.go
+++ b/gpu/doc.go
@@ -5,7 +5,7 @@
/*
Package gpu implements a convenient interface to the WebGPU
graphics and compute framework, in Go, using the
-... bindings
+[github.com/cogentcore/webgpu] bindings.
The Cogent Core GUI framework runs on top of this.
*/
diff --git a/gpu/drawmatrix/matrix.go b/gpu/drawmatrix/matrix.go
index 675c2cad22..92487d8cb0 100644
--- a/gpu/drawmatrix/matrix.go
+++ b/gpu/drawmatrix/matrix.go
@@ -159,8 +159,8 @@ func Transform(dr image.Rectangle, sr image.Rectangle, rotDeg float32) math32.Ma
dsz := math32.FromPoint(dr.Size())
rmat := math32.Rotate2D(rad)
- dmnr := rmat.MulVector2AsPoint(math32.FromPoint(dr.Min))
- dmxr := rmat.MulVector2AsPoint(math32.FromPoint(dr.Max))
+ dmnr := rmat.MulPoint(math32.FromPoint(dr.Min))
+ dmxr := rmat.MulPoint(math32.FromPoint(dr.Max))
sx = math32.Abs(dmxr.X-dmnr.X) / float32(sr.Dx())
sy = math32.Abs(dmxr.Y-dmnr.Y) / float32(sr.Dy())
tx = dmnr.X - sx*float32(sr.Min.X)
diff --git a/htmlcore/context.go b/htmlcore/context.go
index 1bbce9a32a..f82a7587e1 100644
--- a/htmlcore/context.go
+++ b/htmlcore/context.go
@@ -93,6 +93,10 @@ type Context struct {
// type of widget, for example.
WidgetHandlers []func(w core.Widget)
+ // DelayedImageLoad causes images to be loaded at a delay.
+ // if not set, then they are loaded immediately.
+ DelayedImageLoad bool
+
// firstRow indicates the start of a table, where number of columns is counted.
firstRow bool
}
@@ -146,36 +150,32 @@ func (c *Context) config(w core.Widget) {
switch attr.Key {
case "id":
wb.SetName(attr.Val)
+ wb.SetProperty("id", attr.Val)
case "style":
- // our CSS parser is strict about semicolons, but
- // they aren't needed in normal inline styles in HTML
- if !strings.HasSuffix(attr.Val, ";") {
- attr.Val += ";"
- }
- decls, err := parser.ParseDeclarations(attr.Val)
- if errors.Log(err) != nil {
- continue
- }
- rule := &css.Rule{Declarations: decls}
- if c.styles == nil {
- c.styles = map[*html.Node][]*css.Rule{}
- }
- c.styles[c.Node] = append(c.styles[c.Node], rule)
+ c.setStyleAttr(c.Node, attr.Val)
default:
wb.SetProperty(attr.Key, attr.Val)
}
}
- wb.SetProperty("tag", c.Node.Data)
- rules := c.styles[c.Node]
- wb.Styler(func(s *styles.Style) {
- for _, rule := range rules {
- for _, decl := range rule.Declarations {
- // TODO(kai/styproperties): parent style and context
- s.FromProperty(s, decl.Property, decl.Value, colors.BaseContext(colors.ToUniform(s.Color)))
- }
- }
- })
- c.handleWidget(w)
+ wb.SetProperty("tag", c.Node.Data) // this is needed by handleWidget in general
+}
+
+func (c *Context) setStyleAttr(node *html.Node, style string) error {
+ // our CSS parser is strict about semicolons, but
+ // they aren't needed in normal inline styles in HTML
+ if !strings.HasSuffix(style, ";") {
+ style += ";"
+ }
+ decls, err := parser.ParseDeclarations(style)
+ if errors.Log(err) != nil {
+ return err
+ }
+ rule := &css.Rule{Declarations: decls}
+ if c.styles == nil {
+ c.styles = map[*html.Node][]*css.Rule{}
+ }
+ c.styles[c.Node] = append(c.styles[c.Node], rule)
+ return nil
}
// InlineParent returns the current parent widget that inline
@@ -258,9 +258,24 @@ func (c *Context) AddWidgetHandler(f func(w core.Widget)) {
c.WidgetHandlers = append(c.WidgetHandlers, f)
}
-// handleWidget calls WidgetHandlers functions on given widget,
+func (c *Context) applyStyleRules(node *html.Node, w core.Widget) {
+ wb := w.AsWidget()
+ rules := c.styles[c.Node]
+ wb.Styler(func(s *styles.Style) {
+ for _, rule := range rules {
+ for _, decl := range rule.Declarations {
+ // TODO(kai/styproperties): parent style and context
+ s.FromProperty(s, decl.Property, decl.Value, colors.BaseContext(colors.ToUniform(s.Color)))
+ }
+ }
+ })
+}
+
+// handleWidget applies accumulated style rules,
+// and calls WidgetHandlers functions on given widget,
// in order added so last one has override priority.
-func (c *Context) handleWidget(w core.Widget) {
+func (c *Context) handleWidget(node *html.Node, w core.Widget) {
+ c.applyStyleRules(node, w)
for _, f := range c.WidgetHandlers {
f(w)
}
diff --git a/htmlcore/handler.go b/htmlcore/handler.go
index d245e496f0..d12210a93d 100644
--- a/htmlcore/handler.go
+++ b/htmlcore/handler.go
@@ -19,7 +19,6 @@ import (
"cogentcore.org/core/core"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
- "cogentcore.org/core/styles/units"
"cogentcore.org/core/text/lines"
"cogentcore.org/core/text/rich"
"cogentcore.org/core/text/text"
@@ -33,7 +32,7 @@ import (
func New[T tree.NodeValue](ctx *Context) *T {
parent := ctx.Parent()
w := tree.New[T](parent)
- ctx.config(any(w).(core.Widget)) // TODO: better htmlcore structure with new config paradigm?
+ ctx.config(any(w).(core.Widget))
return w
}
@@ -54,6 +53,8 @@ func handleElement(ctx *Context) {
return
}
+ var newWidget core.Widget
+
switch tag {
case "script", "title", "meta":
// we don't render anything
@@ -81,6 +82,7 @@ func handleElement(ctx *Context) {
ctx.addStyle(ExtractText(ctx))
case "body", "main", "div", "section", "nav", "footer", "header", "ol", "ul", "blockquote":
w := New[core.Frame](ctx)
+ newWidget = w
ctx.NewParent = w
switch tag {
case "body":
@@ -93,7 +95,7 @@ func handleElement(ctx *Context) {
if tag == "ol" {
w.SetProperty("listCount", 0)
}
- w.Styler(func(s *styles.Style) {
+ w.FinalStyler(func(s *styles.Style) {
s.Grow.Set(1, 0)
s.Padding.Left.Ch(core.ConstantSpacing(float32(4 * ld)))
})
@@ -109,90 +111,49 @@ func handleElement(ctx *Context) {
})
}
case "button":
- New[core.Button](ctx).SetText(ExtractText(ctx))
+ newWidget = New[core.Button](ctx).SetText(ExtractText(ctx))
case "h1":
- handleText(ctx).SetType(core.TextDisplaySmall)
+ newWidget = handleText(ctx, tag).SetType(core.TextDisplaySmall)
case "h2":
- handleText(ctx).SetType(core.TextHeadlineMedium)
+ newWidget = handleText(ctx, tag).SetType(core.TextHeadlineMedium)
case "h3":
- handleText(ctx).SetType(core.TextTitleLarge)
+ newWidget = handleText(ctx, tag).SetType(core.TextTitleLarge)
case "h4":
- handleText(ctx).SetType(core.TextTitleMedium)
+ newWidget = handleText(ctx, tag).SetType(core.TextTitleMedium)
case "h5":
- handleText(ctx).SetType(core.TextTitleSmall)
+ newWidget = handleText(ctx, tag).SetType(core.TextTitleSmall)
case "h6":
- handleText(ctx).SetType(core.TextLabelSmall)
+ newWidget = handleText(ctx, tag).SetType(core.TextLabelSmall)
case "p":
- handleText(ctx)
+ newWidget = handleText(ctx, tag)
case "pre":
hasCode := ctx.Node.FirstChild != nil && ctx.Node.FirstChild.Data == "code"
if hasCode {
codeEl := ctx.Node.FirstChild
- collapsed := GetAttr(codeEl, "collapsed")
- lang := getLanguage(GetAttr(codeEl, "class"))
+ lang := GetLanguage(GetAttr(codeEl, "class"))
id := GetAttr(codeEl, "id")
var ed *textcore.Editor
- var parent tree.Node
- if collapsed != "" {
- cl := New[core.Collapser](ctx)
- summary := core.NewText(cl.Summary).SetText("Code")
- if title := GetAttr(codeEl, "title"); title != "" {
- summary.SetText(title)
- }
- ed = textcore.NewEditor(cl.Details)
- if id != "" {
- cl.Summary.Name = id
- }
- parent = cl.Parent
- if collapsed == "false" || collapsed == "-" {
- cl.Open = true
- }
- } else {
- ed = New[textcore.Editor](ctx)
- if id != "" {
- ed.SetName(id)
- }
- parent = ed.Parent
+ ed = New[textcore.Editor](ctx)
+ if id != "" {
+ ed.SetName(id)
}
+ newWidget = ed
ctx.Node = codeEl
if lang != "" {
ed.Lines.SetFileExt(lang)
}
ed.Lines.SetString(ExtractText(ctx))
- if BindTextEditor != nil && (lang == "Go" || lang == "Goal") {
- ed.Lines.SpacesToTabs(0, ed.Lines.NumLines()) // Go uses tabs
- parFrame := core.NewFrame(parent)
- parFrame.Styler(func(s *styles.Style) {
- s.Direction = styles.Column
- s.Grow.Set(1, 0)
- })
- // we inherit our Grow.Y from our first child so that
- // elements that want to grow can do so
- parFrame.SetOnChildAdded(func(n tree.Node) {
- if _, ok := n.(*core.Body); ok { // Body should not grow
- return
- }
- wb := core.AsWidget(n)
- if wb.IndexInParent() != 0 {
- return
- }
- wb.FinalStyler(func(s *styles.Style) {
- parFrame.Styles.Grow.Y = s.Grow.Y
- })
- })
- BindTextEditor(ed, parFrame, lang)
- } else {
- ed.SetReadOnly(true)
- ed.Lines.Settings.LineNumbers = false
- ed.Styler(func(s *styles.Style) {
- s.Border.Width.Zero()
- s.MaxBorder.Width.Zero()
- s.StateLayer = 0
- s.Background = colors.Scheme.SurfaceContainer
- })
- }
+ ed.SetReadOnly(true)
+ ed.Lines.Settings.LineNumbers = false
+ ed.Styler(func(s *styles.Style) {
+ s.Border.Width.Zero()
+ s.MaxBorder.Width.Zero()
+ s.StateLayer = 0
+ s.Background = colors.Scheme.SurfaceContainer
+ })
} else {
- handleText(ctx).Styler(func(s *styles.Style) {
+ newWidget = handleText(ctx, tag)
+ newWidget.AsWidget().Styler(func(s *styles.Style) {
s.Text.WhiteSpace = text.WhiteSpacePreWrap
})
}
@@ -208,6 +169,7 @@ func handleElement(ctx *Context) {
ctx.Node = ctx.Node.FirstChild.NextSibling
}
text, sublist := handleTextExclude(ctx, "ol", "ul") // exclude other lists
+ newWidget = text
start := ""
if pw, ok := text.Parent.(core.Widget); ok {
pwt := pw.AsTree()
@@ -238,6 +200,7 @@ func handleElement(ctx *Context) {
}
txt, psub := handleTextExclude(ctx, "ol", "ul")
txt.SetText(txt.Text)
+ ctx.handleWidget(cnode, txt)
if psub != nil {
if psub != sublist {
readHTMLNode(ctx, ctx.Parent(), psub)
@@ -250,86 +213,41 @@ func handleElement(ctx *Context) {
readHTMLNode(ctx, ctx.Parent(), sublist)
}
case "img":
- n := ctx.Node
- src := GetAttr(n, "src")
- alt := GetAttr(n, "alt")
- pid := ""
- if ctx.BlockParent != nil {
- pid = GetAttr(n.Parent, "id")
- }
- // Can be either image or svg.
- var img *core.Image
- var svg *core.SVG
- if strings.HasSuffix(src, ".svg") {
- svg = New[core.SVG](ctx)
- svg.SetTooltip(alt)
- if pid != "" {
- svg.SetName(pid)
- }
- } else {
- img = New[core.Image](ctx)
- img.SetTooltip(alt)
- if pid != "" {
- img.SetName(pid)
- }
- }
-
- go func() {
- resp, err := Get(ctx, src)
- if errors.Log(err) != nil {
- return
- }
- defer resp.Body.Close()
- if svg != nil {
- svg.AsyncLock()
- errors.Log(svg.Read(resp.Body))
- svg.Update()
- svg.AsyncUnlock()
- } else {
- im, _, err := imagex.Read(resp.Body)
- if err != nil {
- slog.Error("error loading image", "url", src, "err", err)
- return
- }
- img.AsyncLock()
- img.SetImage(im)
- img.Update()
- img.AsyncUnlock()
- }
- }()
+ newWidget = handleImage(ctx, tag)
case "input":
ityp := GetAttr(ctx.Node, "type")
val := GetAttr(ctx.Node, "value")
switch ityp {
case "number":
fval := float32(errors.Log1(strconv.ParseFloat(val, 32)))
- New[core.Spinner](ctx).SetValue(fval)
+ newWidget = New[core.Spinner](ctx).SetValue(fval)
case "checkbox":
- New[core.Switch](ctx).SetType(core.SwitchCheckbox).
+ newWidget = New[core.Switch](ctx).SetType(core.SwitchCheckbox).
SetState(HasAttr(ctx.Node, "checked"), states.Checked)
case "radio":
- New[core.Switch](ctx).SetType(core.SwitchRadioButton).
+ newWidget = New[core.Switch](ctx).SetType(core.SwitchRadioButton).
SetState(HasAttr(ctx.Node, "checked"), states.Checked)
case "range":
fval := float32(errors.Log1(strconv.ParseFloat(val, 32)))
- New[core.Slider](ctx).SetValue(fval)
+ newWidget = New[core.Slider](ctx).SetValue(fval)
case "button", "submit":
- New[core.Button](ctx).SetText(val)
+ newWidget = New[core.Button](ctx).SetText(val)
case "color":
- core.Bind(val, New[core.ColorButton](ctx))
+ newWidget = core.Bind(val, New[core.ColorButton](ctx))
case "datetime":
- core.Bind(val, New[core.TimeInput](ctx))
+ newWidget = core.Bind(val, New[core.TimeInput](ctx))
case "file":
- core.Bind(val, New[core.FileButton](ctx))
+ newWidget = core.Bind(val, New[core.FileButton](ctx))
default:
- New[core.TextField](ctx).SetText(val)
+ newWidget = New[core.TextField](ctx).SetText(val)
}
case "textarea":
buf := lines.NewLines()
buf.SetText([]byte(ExtractText(ctx)))
- New[textcore.Editor](ctx).SetLines(buf)
+ newWidget = New[textcore.Editor](ctx).SetLines(buf)
case "table":
w := New[core.Frame](ctx)
+ newWidget = w
ctx.NewParent = w
ctx.TableParent = w
ctx.firstRow = true
@@ -337,7 +255,7 @@ func handleElement(ctx *Context) {
w.Styler(func(s *styles.Style) {
s.Display = styles.Grid
s.Overflow.X = styles.OverflowAuto
- s.Grow.Set(1, 1)
+ s.Grow.Set(1, 0)
s.Columns = w.Property("cols").(int)
s.Gap.X.Dp(core.ConstantSpacing(6))
s.Justify.Content = styles.Center
@@ -348,10 +266,11 @@ func handleElement(ctx *Context) {
cols++
ctx.TableParent.SetProperty("cols", cols)
}
- tx := handleText(ctx)
+ tx := handleText(ctx, tag)
if tx.Parent == nil { // if empty we need a real empty text to keep structure
tx = New[core.Text](ctx)
}
+ newWidget = tx
// fmt.Println(tag, "val:", tx.Text)
if tag == "th" {
tx.Styler(func(s *styles.Style) {
@@ -376,22 +295,99 @@ func handleElement(ctx *Context) {
default:
ctx.NewParent = ctx.Parent()
}
+ if newWidget != nil {
+ ctx.handleWidget(ctx.Node, newWidget)
+ }
}
-func (ctx *Context) textStyler(s *styles.Style) {
- s.Margin.SetVertical(units.Em(core.ConstantSpacing(0.25)))
- s.Font.Size.Value *= core.AppearanceSettings.DocsFontSize / 100
- // TODO: it would be ideal for htmlcore to automatically save a scale factor
- // in general and for each domain, that is applied only to page content
- // scale := float32(1.2)
- // s.Font.Size.Value *= scale
- // s.Text.LineHeight.Value *= scale
- // s.Text.LetterSpacing.Value *= scale
+func handleImage(ctx *Context, tag string) core.Widget {
+ pid := ""
+ pstyle := ""
+ if ctx.BlockParent != nil { // these attributes get put on a block parent element
+ pstyle = GetAttr(ctx.Node.Parent, "style")
+ pid = GetAttr(ctx.Node.Parent, "id")
+ }
+ n := ctx.Node
+ src := GetAttr(n, "src")
+ alt := GetAttr(n, "alt")
+ if pstyle != "" {
+ ctx.setStyleAttr(n, pstyle)
+ }
+ var newWidget core.Widget
+ var resp *http.Response
+ var err error
+ if !ctx.DelayedImageLoad {
+ resp, err = Get(ctx, src)
+ if errors.Log(err) != nil {
+ return nil
+ }
+ defer resp.Body.Close()
+ }
+
+ // Can be either image or svg.
+ var img *core.Image
+ var svg *core.SVG
+ if strings.HasSuffix(src, ".svg") {
+ svg = New[core.SVG](ctx)
+ svg.SetTooltip(alt)
+ if pid != "" {
+ svg.SetName(pid)
+ svg.SetProperty("id", pid)
+ }
+ if !ctx.DelayedImageLoad {
+ errors.Log(svg.Read(resp.Body))
+ }
+ newWidget = svg
+ } else {
+ img = New[core.Image](ctx)
+ img.SetTooltip(alt)
+ if pid != "" {
+ img.SetName(pid)
+ img.SetProperty("id", pid)
+ }
+ if !ctx.DelayedImageLoad {
+ im, _, err := imagex.Read(resp.Body)
+ if err != nil {
+ slog.Error("error loading image", "url", src, "err", err)
+ } else {
+ img.SetImage(im)
+ }
+ }
+ newWidget = img
+ }
+
+ if !ctx.DelayedImageLoad {
+ return newWidget
+ }
+ go func() {
+ resp, err := Get(ctx, src)
+ if errors.Log(err) != nil {
+ return
+ }
+ defer resp.Body.Close()
+ if svg != nil {
+ svg.AsyncLock()
+ errors.Log(svg.Read(resp.Body))
+ svg.Update()
+ svg.AsyncUnlock()
+ } else {
+ im, _, err := imagex.Read(resp.Body)
+ if err != nil {
+ slog.Error("error loading image", "url", src, "err", err)
+ return
+ }
+ img.AsyncLock()
+ img.SetImage(im)
+ img.Update()
+ img.AsyncUnlock()
+ }
+ }()
+ return newWidget
}
// handleText creates a new [core.Text] from the given information, setting the text and
// the text click function so that URLs are opened according to [Context.OpenURL].
-func handleText(ctx *Context) *core.Text {
+func handleText(ctx *Context, tag string) *core.Text {
tx, _ := handleTextExclude(ctx)
return tx
}
@@ -407,7 +403,6 @@ func handleTextExclude(ctx *Context, excludeSubs ...string) (*core.Text, *html.N
return core.NewText(), excl
}
tx := New[core.Text](ctx).SetText(et)
- tx.Styler(ctx.textStyler)
tx.HandleTextClick(func(tl *rich.Hyperlink) {
ctx.OpenURL(tl.URL)
})
@@ -423,7 +418,6 @@ func handleTextTag(ctx *Context) *core.Text {
start, end := nodeString(ctx.Node)
str := start + ExtractText(ctx) + end
tx := New[core.Text](ctx).SetText(str)
- tx.Styler(ctx.textStyler)
tx.HandleTextClick(func(tl *rich.Hyperlink) {
ctx.OpenURL(tl.URL)
})
@@ -466,9 +460,9 @@ func HasAttr(n *html.Node, attr string) bool {
})
}
-// getLanguage returns the 'x' in a `language-x` class from the given
+// GetLanguage returns the 'x' in a `language-x` class from the given
// string of class(es).
-func getLanguage(class string) string {
+func GetLanguage(class string) string {
fields := strings.Fields(class)
for _, field := range fields {
if strings.HasPrefix(field, "language-") {
@@ -498,8 +492,3 @@ func Get(ctx *Context, url string) (*http.Response, error) {
}
return resp, nil
}
-
-// BindTextEditor is a function set to [cogentcore.org/core/yaegicore.BindTextEditor]
-// when importing yaegicore, which provides interactive editing functionality for Go
-// code blocks in text editors.
-var BindTextEditor func(ed *textcore.Editor, parent *core.Frame, language string)
diff --git a/htmlcore/html.go b/htmlcore/html.go
index 08d70405f9..6ec8681a25 100644
--- a/htmlcore/html.go
+++ b/htmlcore/html.go
@@ -46,7 +46,7 @@ func readHTMLNode(ctx *Context, parent core.Widget, n *html.Node) error {
case html.TextNode:
str := strings.TrimSpace(n.Data)
if str != "" {
- New[core.Text](ctx).SetText(str)
+ ctx.handleWidget(n, New[core.Text](ctx).SetText(str))
}
case html.ElementNode:
ctx.Node = n
diff --git a/htmlcore/md.go b/htmlcore/md.go
index ca025bbd68..d0529862e9 100644
--- a/htmlcore/md.go
+++ b/htmlcore/md.go
@@ -18,7 +18,7 @@ import (
var divRegex = regexp.MustCompile("
")
-func mdToHTML(ctx *Context, md []byte) []byte {
+func MDToHTML(ctx *Context, md []byte) []byte {
// create markdown parser with extensions
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock | parser.Attributes | parser.Mmark
p := parser.NewWithExtensions(extensions)
@@ -43,7 +43,7 @@ func mdToHTML(ctx *Context, md []byte) []byte {
// ReadMD reads MD (markdown) from the given bytes and adds corresponding
// Cogent Core widgets to the given [core.Widget], using the given context.
func ReadMD(ctx *Context, parent core.Widget, b []byte) error {
- htm := mdToHTML(ctx, b)
+ htm := MDToHTML(ctx, b)
// os.WriteFile("htmlcore_tmp.html", htm, 0666) // note: keep here, needed for debugging
buf := bytes.NewBuffer(htm)
return ReadHTML(ctx, parent, buf)
diff --git a/math32/box2.go b/math32/box2.go
index b52230c042..ae1c004142 100644
--- a/math32/box2.go
+++ b/math32/box2.go
@@ -157,10 +157,10 @@ func (b *Box2) ExpandByBox(box Box2) {
// and computes the resulting spanning Box2 of the transformed points
func (b Box2) MulMatrix2(m Matrix2) Box2 {
var cs [4]Vector2
- cs[0] = m.MulVector2AsPoint(Vec2(b.Min.X, b.Min.Y))
- cs[1] = m.MulVector2AsPoint(Vec2(b.Min.X, b.Max.Y))
- cs[2] = m.MulVector2AsPoint(Vec2(b.Max.X, b.Min.Y))
- cs[3] = m.MulVector2AsPoint(Vec2(b.Max.X, b.Max.Y))
+ cs[0] = m.MulPoint(Vec2(b.Min.X, b.Min.Y))
+ cs[1] = m.MulPoint(Vec2(b.Min.X, b.Max.Y))
+ cs[2] = m.MulPoint(Vec2(b.Max.X, b.Min.Y))
+ cs[3] = m.MulPoint(Vec2(b.Max.X, b.Max.Y))
nb := B2Empty()
for i := 0; i < 4; i++ {
diff --git a/math32/box3.go b/math32/box3.go
index e989ae19a9..aa080cc150 100644
--- a/math32/box3.go
+++ b/math32/box3.go
@@ -55,6 +55,13 @@ func (b *Box3) Set(min, max *Vector3) {
}
}
+// SetMinMax sets this bounding box minimum and maximum coordinates
+// using specific values (see also Set)
+func (b *Box3) SetMinMax(min, max Vector3) {
+ b.Min = min
+ b.Max = max
+}
+
// SetFromPoints sets this bounding box from the specified array of points.
func (b *Box3) SetFromPoints(points []Vector3) {
b.SetEmpty()
@@ -208,15 +215,15 @@ func (b Box3) MulMatrix4(m *Matrix4) Box3 {
// and computes the resulting spanning Box3 of the transformed points
func (b Box3) MulQuat(q Quat) Box3 {
var cs [8]Vector3
- cs[0] = Vec3(b.Min.X, b.Min.Y, b.Min.Z).MulQuat(q)
- cs[1] = Vec3(b.Min.X, b.Min.Y, b.Max.Z).MulQuat(q)
- cs[2] = Vec3(b.Min.X, b.Max.Y, b.Min.Z).MulQuat(q)
- cs[3] = Vec3(b.Max.X, b.Min.Y, b.Min.Z).MulQuat(q)
-
- cs[4] = Vec3(b.Max.X, b.Max.Y, b.Max.Z).MulQuat(q)
- cs[5] = Vec3(b.Max.X, b.Max.Y, b.Min.Z).MulQuat(q)
- cs[6] = Vec3(b.Max.X, b.Min.Y, b.Max.Z).MulQuat(q)
- cs[7] = Vec3(b.Min.X, b.Max.Y, b.Max.Z).MulQuat(q)
+ cs[0] = q.MulVector(Vec3(b.Min.X, b.Min.Y, b.Min.Z))
+ cs[1] = q.MulVector(Vec3(b.Min.X, b.Min.Y, b.Max.Z))
+ cs[2] = q.MulVector(Vec3(b.Min.X, b.Max.Y, b.Min.Z))
+ cs[3] = q.MulVector(Vec3(b.Max.X, b.Min.Y, b.Min.Z))
+
+ cs[4] = q.MulVector(Vec3(b.Max.X, b.Max.Y, b.Max.Z))
+ cs[5] = q.MulVector(Vec3(b.Max.X, b.Max.Y, b.Min.Z))
+ cs[6] = q.MulVector(Vec3(b.Max.X, b.Min.Y, b.Max.Z))
+ cs[7] = q.MulVector(Vec3(b.Min.X, b.Max.Y, b.Max.Z))
nb := B3Empty()
for i := 0; i < 8; i++ {
diff --git a/math32/matrix2.go b/math32/matrix2.go
index 34a7e456fa..533c495b32 100644
--- a/math32/matrix2.go
+++ b/math32/matrix2.go
@@ -39,7 +39,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
-// Matrix2 is a 3x2 matrix.
+// Matrix2 is a 2x3 matrix.
// [XX YX]
// [XY YY]
// [X0 Y0]
@@ -133,23 +133,23 @@ func (a *Matrix2) SetMul(b Matrix2) {
*a = a.Mul(b)
}
-// MulVector2AsVector multiplies the Vector2 as a vector without adding translations.
+// MulVector multiplies the Vector2 as a vector without adding translations.
// This is for directional vectors and not points.
-func (a Matrix2) MulVector2AsVector(v Vector2) Vector2 {
+func (a Matrix2) MulVector(v Vector2) Vector2 {
tx := a.XX*v.X + a.XY*v.Y
ty := a.YX*v.X + a.YY*v.Y
return Vec2(tx, ty)
}
-// MulVector2AsPoint multiplies the Vector2 as a point, including adding translations.
-func (a Matrix2) MulVector2AsPoint(v Vector2) Vector2 {
+// MulPoint multiplies the Vector2 as a point, including adding translations.
+func (a Matrix2) MulPoint(v Vector2) Vector2 {
tx := a.XX*v.X + a.XY*v.Y + a.X0
ty := a.YX*v.X + a.YY*v.Y + a.Y0
return Vec2(tx, ty)
}
-// MulFixedAsPoint multiplies the fixed point as a point, including adding translations.
-func (a Matrix2) MulFixedAsPoint(fp fixed.Point26_6) fixed.Point26_6 {
+// MulFixedPoint multiplies the fixed point as a point, including adding translations.
+func (a Matrix2) MulFixedPoint(fp fixed.Point26_6) fixed.Point26_6 {
x := fixed.Int26_6((float32(fp.X)*a.XX + float32(fp.Y)*a.XY) + a.X0*32)
y := fixed.Int26_6((float32(fp.X)*a.YX + float32(fp.Y)*a.YY) + a.Y0*32)
return fixed.Point26_6{x, y}
diff --git a/math32/matrix2_test.go b/math32/matrix2_test.go
index b519457e00..a78bfcb70c 100644
--- a/math32/matrix2_test.go
+++ b/math32/matrix2_test.go
@@ -45,24 +45,24 @@ func TestMatrix2(t *testing.T) {
rot90 := DegToRad(90)
rot45 := DegToRad(45)
- assert.Equal(t, vx, Identity3().MulVector2AsPoint(vx))
- assert.Equal(t, vy, Identity3().MulVector2AsPoint(vy))
- assert.Equal(t, vxy, Identity3().MulVector2AsPoint(vxy))
+ assert.Equal(t, vx, Identity3().MulPoint(vx))
+ assert.Equal(t, vy, Identity3().MulPoint(vy))
+ assert.Equal(t, vxy, Identity3().MulPoint(vxy))
- assert.Equal(t, vxy, Translate2D(1, 1).MulVector2AsPoint(v0))
+ assert.Equal(t, vxy, Translate2D(1, 1).MulPoint(v0))
- assert.Equal(t, vxy.MulScalar(2), Scale2D(2, 2).MulVector2AsPoint(vxy))
+ assert.Equal(t, vxy.MulScalar(2), Scale2D(2, 2).MulPoint(vxy))
- tolAssertEqualVector(t, vy, Rotate2D(rot90).MulVector2AsPoint(vx)) // left, CCW
- tolAssertEqualVector(t, vx, Rotate2D(-rot90).MulVector2AsPoint(vy)) // right, CW
- tolAssertEqualVector(t, vxy.Normal(), Rotate2D(rot45).MulVector2AsPoint(vx))
- tolAssertEqualVector(t, vxy.Normal(), Rotate2D(-rot45).MulVector2AsPoint(vy))
+ tolAssertEqualVector(t, vy, Rotate2D(rot90).MulPoint(vx)) // left, CCW
+ tolAssertEqualVector(t, vx, Rotate2D(-rot90).MulPoint(vy)) // right, CW
+ tolAssertEqualVector(t, vxy.Normal(), Rotate2D(rot45).MulPoint(vx))
+ tolAssertEqualVector(t, vxy.Normal(), Rotate2D(-rot45).MulPoint(vy))
- tolAssertEqualVector(t, vy, Rotate2D(-rot90).Inverse().MulVector2AsPoint(vx))
- tolAssertEqualVector(t, vx, Rotate2D(rot90).Inverse().MulVector2AsPoint(vy))
+ tolAssertEqualVector(t, vy, Rotate2D(-rot90).Inverse().MulPoint(vx))
+ tolAssertEqualVector(t, vx, Rotate2D(rot90).Inverse().MulPoint(vy))
- tolAssertEqualVector(t, vxy, Rotate2D(-rot45).Mul(Rotate2D(rot45)).MulVector2AsPoint(vxy))
- tolAssertEqualVector(t, vxy, Rotate2D(-rot45).Mul(Rotate2D(-rot45).Inverse()).MulVector2AsPoint(vxy))
+ tolAssertEqualVector(t, vxy, Rotate2D(-rot45).Mul(Rotate2D(rot45)).MulPoint(vxy))
+ tolAssertEqualVector(t, vxy, Rotate2D(-rot45).Mul(Rotate2D(-rot45).Inverse()).MulPoint(vxy))
tolassert.EqualTol(t, -rot90, Rotate2D(-rot90).ExtractRot(), standardTol)
tolassert.EqualTol(t, -rot45, Rotate2D(-rot45).ExtractRot(), standardTol)
@@ -71,7 +71,7 @@ func TestMatrix2(t *testing.T) {
// 1,0 -> scale(2) = 2,0 -> rotate 90 = 0,2 -> trans 1,1 -> 1,3
// multiplication order is *reverse* of "logical" order:
- tolAssertEqualVector(t, Vec2(1, 3), Translate2D(1, 1).Mul(Rotate2D(rot90)).Mul(Scale2D(2, 2)).MulVector2AsPoint(vx))
+ tolAssertEqualVector(t, Vec2(1, 3), Translate2D(1, 1).Mul(Rotate2D(rot90)).Mul(Scale2D(2, 2)).MulPoint(vx))
}
@@ -153,14 +153,14 @@ func TestMatrix2Canvas(t *testing.T) {
p := Vector2{3, 4}
rot90 := DegToRad(90)
rot45 := DegToRad(45)
- tolAssertEqualVector(t, Identity2().Translate(2.0, 2.0).MulVector2AsPoint(p), Vector2{5.0, 6.0})
- tolAssertEqualVector(t, Identity2().Scale(2.0, 2.0).MulVector2AsPoint(p), Vector2{6.0, 8.0})
- tolAssertEqualVector(t, Identity2().Scale(1.0, -1.0).MulVector2AsPoint(p), Vector2{3.0, -4.0})
- tolAssertEqualVector(t, Identity2().ScaleAbout(2.0, -1.0, 2.0, 2.0).MulVector2AsPoint(p), Vector2{4.0, 0.0})
- tolAssertEqualVector(t, Identity2().Shear(1.0, 0.0).MulVector2AsPoint(p), Vector2{7.0, 4.0})
- tolAssertEqualVector(t, Identity2().Rotate(rot90).MulVector2AsPoint(p), p.Rot90CCW())
- tolAssertEqualVector(t, Identity2().RotateAbout(rot90, 5.0, 5.0).MulVector2AsPoint(p), p.Rot(90.0*Pi/180.0, Vector2{5.0, 5.0}))
- tolAssertEqualVector(t, Identity2().Rotate(rot90).Transpose().MulVector2AsPoint(p), p.Rot90CW())
+ tolAssertEqualVector(t, Identity2().Translate(2.0, 2.0).MulPoint(p), Vector2{5.0, 6.0})
+ tolAssertEqualVector(t, Identity2().Scale(2.0, 2.0).MulPoint(p), Vector2{6.0, 8.0})
+ tolAssertEqualVector(t, Identity2().Scale(1.0, -1.0).MulPoint(p), Vector2{3.0, -4.0})
+ tolAssertEqualVector(t, Identity2().ScaleAbout(2.0, -1.0, 2.0, 2.0).MulPoint(p), Vector2{4.0, 0.0})
+ tolAssertEqualVector(t, Identity2().Shear(1.0, 0.0).MulPoint(p), Vector2{7.0, 4.0})
+ tolAssertEqualVector(t, Identity2().Rotate(rot90).MulPoint(p), p.Rot90CCW())
+ tolAssertEqualVector(t, Identity2().RotateAbout(rot90, 5.0, 5.0).MulPoint(p), p.Rot(90.0*Pi/180.0, Vector2{5.0, 5.0}))
+ tolAssertEqualVector(t, Identity2().Rotate(rot90).Transpose().MulPoint(p), p.Rot90CW())
tolAssertEqualMatrix2(t, Identity2().Scale(2.0, 4.0).Inverse(), Identity2().Scale(0.5, 0.25))
tolAssertEqualMatrix2(t, Identity2().Rotate(rot90).Inverse(), Identity2().Rotate(-rot90))
tolAssertEqualMatrix2(t, Identity2().Rotate(rot90).Scale(2.0, 1.0), Identity2().Scale(1.0, 2.0).Rotate(rot90))
diff --git a/math32/matrix3.go b/math32/matrix3.go
index 00ccd0ae95..8ac6e576ce 100644
--- a/math32/matrix3.go
+++ b/math32/matrix3.go
@@ -15,6 +15,16 @@ import "errors"
// Matrix3 is 3x3 matrix organized internally as column matrix.
type Matrix3 [9]float32
+// note: matrix indexes are row,column. matrix dimensions are CxR.
+// nothing like consistency...
+
+// Mat3 constructs a new Matrix3 from given values, in column-wise order.
+func Mat3(n11, n21, n31, n12, n22, n32, n13, n23, n33 float32) Matrix3 {
+ m := Matrix3{}
+ m.Set(n11, n21, n31, n12, n22, n32, n13, n23, n33)
+ return m
+}
+
// Identity3 returns a new identity [Matrix3] matrix.
func Identity3() Matrix3 {
m := Matrix3{}
@@ -49,26 +59,26 @@ func Matrix3Rotate2D(angle float32) Matrix3 {
return Matrix3FromMatrix2(Rotate2D(angle))
}
-// Set sets all the elements of the matrix row by row starting at row1, column1,
-// row1, column2, row1, column3 and so forth.
-func (m *Matrix3) Set(n11, n12, n13, n21, n22, n23, n31, n32, n33 float32) {
+// Set sets all the elements of the matrix col by col starting at row1, column1,
+// row2, column1, row3, column1 and so forth.
+func (m *Matrix3) Set(n11, n21, n31, n12, n22, n32, n13, n23, n33 float32) {
m[0] = n11
- m[3] = n12
- m[6] = n13
m[1] = n21
- m[4] = n22
- m[7] = n23
m[2] = n31
+ m[3] = n12
+ m[4] = n22
m[5] = n32
+ m[6] = n13
+ m[7] = n23
m[8] = n33
}
// SetFromMatrix4 sets the matrix elements based on a Matrix4.
func (m *Matrix3) SetFromMatrix4(src *Matrix4) {
m.Set(
- src[0], src[4], src[8],
- src[1], src[5], src[9],
- src[2], src[6], src[10],
+ src[0], src[1], src[2],
+ src[4], src[5], src[6],
+ src[8], src[9], src[10],
)
}
@@ -79,8 +89,8 @@ func (m *Matrix3) SetFromMatrix4(src *Matrix4) {
// SetFromMatrix2 sets the matrix elements based on a Matrix2.
func (m *Matrix3) SetFromMatrix2(src Matrix2) {
m.Set(
- src.XX, src.YX, src.X0,
- src.XY, src.YY, src.Y0,
+ src.XX, src.XY, src.X0,
+ src.YX, src.YY, src.Y0,
src.X0, src.Y0, 1,
)
}
@@ -186,16 +196,16 @@ func (m *Matrix3) SetMulScalar(s float32) {
m[8] *= s
}
-// MulVector2AsVector multiplies the Vector2 as a vector without adding translations.
+// MulVector multiplies the Vector2 as a vector without adding translations.
// This is for directional vectors and not points.
-func (a Matrix3) MulVector2AsVector(v Vector2) Vector2 {
+func (a Matrix3) MulVector(v Vector2) Vector2 {
tx := a[0]*v.X + a[1]*v.Y
ty := a[3]*v.X + a[4]*v.Y
return Vec2(tx, ty)
}
-// MulVector2AsPoint multiplies the Vector2 as a point, including adding translations.
-func (a Matrix3) MulVector2AsPoint(v Vector2) Vector2 {
+// MulPoint multiplies the Vector2 as a point, including adding translations.
+func (a Matrix3) MulPoint(v Vector2) Vector2 {
tx := a[0]*v.X + a[1]*v.Y + a[2]
ty := a[3]*v.X + a[4]*v.Y + a[5]
return Vec2(tx, ty)
@@ -323,8 +333,7 @@ func (m *Matrix3) SetScaleCols(v Vector3) {
m[8] *= v.Z
}
-/////////////////////////////////////////////////////////////////////////////
-// Special functions
+//////// Special functions
// SetNormalMatrix set this matrix to the matrix that can transform the normal vectors
// from the src matrix which is used transform the vertices (e.g., a ModelView matrix).
diff --git a/math32/matrix3_test.go b/math32/matrix3_test.go
index af8c22d933..9f8bafeb74 100644
--- a/math32/matrix3_test.go
+++ b/math32/matrix3_test.go
@@ -16,27 +16,27 @@ func TestMatrix3(t *testing.T) {
vy := Vec2(0, 1)
vxy := Vec2(1, 1)
- assert.Equal(t, vx, Identity3().MulVector2AsPoint(vx))
- assert.Equal(t, vy, Identity3().MulVector2AsPoint(vy))
- assert.Equal(t, vxy, Identity3().MulVector2AsPoint(vxy))
+ assert.Equal(t, vx, Identity3().MulPoint(vx))
+ assert.Equal(t, vy, Identity3().MulPoint(vy))
+ assert.Equal(t, vxy, Identity3().MulPoint(vxy))
- assert.Equal(t, vxy, Matrix3FromMatrix2(Translate2D(1, 1)).MulVector2AsPoint(v0))
+ assert.Equal(t, vxy, Matrix3FromMatrix2(Translate2D(1, 1)).MulPoint(v0))
- assert.Equal(t, vxy.MulScalar(2), Matrix3FromMatrix2(Scale2D(2, 2)).MulVector2AsPoint(vxy))
+ assert.Equal(t, vxy.MulScalar(2), Matrix3FromMatrix2(Scale2D(2, 2)).MulPoint(vxy))
- tolAssertEqualVector(t, vy, Matrix3FromMatrix2(Rotate2D(DegToRad(90))).MulVector2AsPoint(vx)) // left
- tolAssertEqualVector(t, vx, Matrix3FromMatrix2(Rotate2D(DegToRad(-90))).MulVector2AsPoint(vy)) // right
- tolAssertEqualVector(t, vxy.Normal(), Matrix3FromMatrix2(Rotate2D(DegToRad(45))).MulVector2AsPoint(vx))
- tolAssertEqualVector(t, vxy.Normal(), Matrix3FromMatrix2(Rotate2D(DegToRad(-45))).MulVector2AsPoint(vy))
+ tolAssertEqualVector(t, vy, Matrix3FromMatrix2(Rotate2D(DegToRad(90))).MulPoint(vx)) // left
+ tolAssertEqualVector(t, vx, Matrix3FromMatrix2(Rotate2D(DegToRad(-90))).MulPoint(vy)) // right
+ tolAssertEqualVector(t, vxy.Normal(), Matrix3FromMatrix2(Rotate2D(DegToRad(45))).MulPoint(vx))
+ tolAssertEqualVector(t, vxy.Normal(), Matrix3FromMatrix2(Rotate2D(DegToRad(-45))).MulPoint(vy))
- tolAssertEqualVector(t, vy, Matrix3FromMatrix2(Rotate2D(DegToRad(-90))).Inverse().MulVector2AsPoint(vx)) // left
- tolAssertEqualVector(t, vx, Matrix3FromMatrix2(Rotate2D(DegToRad(90))).Inverse().MulVector2AsPoint(vy)) // right
+ tolAssertEqualVector(t, vy, Matrix3FromMatrix2(Rotate2D(DegToRad(-90))).Inverse().MulPoint(vx)) // left
+ tolAssertEqualVector(t, vx, Matrix3FromMatrix2(Rotate2D(DegToRad(90))).Inverse().MulPoint(vy)) // right
// 1,0 -> scale(2) = 2,0 -> rotate 90 = 0,2 -> trans 1,1 -> 1,3
// multiplication order is *reverse* of "logical" order:
- tolAssertEqualVector(t, Vec2(1, 3), Matrix3Translate2D(1, 1).Mul(Matrix3Rotate2D(DegToRad(90))).Mul(Matrix3Scale2D(2, 2)).MulVector2AsPoint(vx))
+ tolAssertEqualVector(t, Vec2(1, 3), Matrix3Translate2D(1, 1).Mul(Matrix3Rotate2D(DegToRad(90))).Mul(Matrix3Scale2D(2, 2)).MulPoint(vx))
- // xmat := Matrix3Translate2D(1, 1).Mul(Matrix3Rotate2D(DegToRad(90))).Mul(Matrix3Scale2D(2, 2)).MulVector2AsPoint(vx))
+ // xmat := Matrix3Translate2D(1, 1).Mul(Matrix3Rotate2D(DegToRad(90))).Mul(Matrix3Scale2D(2, 2)).MulPoint(vx))
}
func TestMatrix3SetFromMatrix4(t *testing.T) {
diff --git a/math32/quaternion.go b/math32/quaternion.go
index 4f822b2bf8..5b3943b7af 100644
--- a/math32/quaternion.go
+++ b/math32/quaternion.go
@@ -56,7 +56,7 @@ func (q *Quat) FromArray(array []float32, offset int) {
}
// ToArray copies this quaternions's components to array starting at offset.
-func (q *Quat) ToArray(array []float32, offset int) {
+func (q Quat) ToArray(array []float32, offset int) {
array[offset] = q.X
array[offset+1] = q.Y
array[offset+2] = q.Z
@@ -72,12 +72,12 @@ func (q *Quat) SetIdentity() {
}
// IsIdentity returns if this is an identity quaternion.
-func (q *Quat) IsIdentity() bool {
+func (q Quat) IsIdentity() bool {
return q.X == 0 && q.Y == 0 && q.Z == 0 && q.W == 1
}
// IsNil returns true if all values are 0 (uninitialized).
-func (q *Quat) IsNil() bool {
+func (q Quat) IsNil() bool {
return q.X == 0 && q.Y == 0 && q.Z == 0 && q.W == 0
}
@@ -104,9 +104,9 @@ func (q *Quat) SetFromEuler(euler Vector3) {
// ToEuler returns a Vector3 with components as the Euler angles
// from the given quaternion.
-func (q *Quat) ToEuler() Vector3 {
+func (q Quat) ToEuler() Vector3 {
rot := Vector3{}
- rot.SetEulerAnglesFromQuat(*q)
+ rot.SetEulerAnglesFromQuat(q)
return rot
}
@@ -122,20 +122,20 @@ func (q *Quat) SetFromAxisAngle(axis Vector3, angle float32) {
}
// ToAxisAngle returns the Vector4 holding axis and angle of this Quaternion
-func (q *Quat) ToAxisAngle() Vector4 {
+func (q Quat) ToAxisAngle() Vector4 {
aa := Vector4{}
- aa.SetAxisAngleFromQuat(*q)
+ aa.SetAxisAngleFromQuat(q)
return aa
}
// GenGoSet returns code to set values in object at given path (var.member etc)
-func (q *Quat) GenGoSet(path string) string {
+func (q Quat) GenGoSet(path string) string {
aa := q.ToAxisAngle()
return fmt.Sprintf("%s.SetFromAxisAngle(math32.Vec3(%g, %g, %g), %g)", path, aa.X, aa.Y, aa.Z, aa.W)
}
// GenGoNew returns code to create new
-func (q *Quat) GenGoNew() string {
+func (q Quat) GenGoNew() string {
return fmt.Sprintf("math32.Quat{%g, %g, %g, %g}", q.X, q.Y, q.Z, q.W)
}
@@ -213,10 +213,9 @@ func (q *Quat) SetInverse() {
}
// Inverse returns the inverse of this quaternion.
-func (q *Quat) Inverse() Quat {
- nq := *q
- nq.SetInverse()
- return nq
+func (q Quat) Inverse() Quat {
+ q.SetInverse()
+ return q
}
// SetConjugate sets this quaternion to its conjugate.
@@ -227,14 +226,13 @@ func (q *Quat) SetConjugate() {
}
// Conjugate returns the conjugate of this quaternion.
-func (q *Quat) Conjugate() Quat {
- nq := *q
- nq.SetConjugate()
- return nq
+func (q Quat) Conjugate() Quat {
+ q.SetConjugate()
+ return q
}
// Dot returns the dot products of this quaternion with other.
-func (q *Quat) Dot(other Quat) float32 {
+func (q Quat) Dot(other Quat) float32 {
return q.X*other.X + q.Y*other.Y + q.Z*other.Z + q.W*other.W
}
@@ -282,38 +280,45 @@ func (q *Quat) NormalizeFast() {
}
}
-// MulQuats set this quaternion to the multiplication of a by b.
-func (q *Quat) MulQuats(a, b Quat) {
+// MulQuats returns a * b quaternion multiplication.
+func MulQuats(a, b Quat) Quat {
// from http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm
- qax := a.X
- qay := a.Y
- qaz := a.Z
- qaw := a.W
- qbx := b.X
- qby := b.Y
- qbz := b.Z
- qbw := b.W
-
- q.X = qax*qbw + qaw*qbx + qay*qbz - qaz*qby
- q.Y = qay*qbw + qaw*qby + qaz*qbx - qax*qbz
- q.Z = qaz*qbw + qaw*qbz + qax*qby - qay*qbx
- q.W = qaw*qbw - qax*qbx - qay*qby - qaz*qbz
+ var q Quat
+ q.X = a.X*b.W + a.W*b.X + a.Y*b.Z - a.Z*b.Y
+ q.Y = a.Y*b.W + a.W*b.Y + a.Z*b.X - a.X*b.Z
+ q.Z = a.Z*b.W + a.W*b.Z + a.X*b.Y - a.Y*b.X
+ q.W = a.W*b.W - a.X*b.X - a.Y*b.Y - a.Z*b.Z
+ return q
}
// SetMul sets this quaternion to the multiplication of itself by other.
func (q *Quat) SetMul(other Quat) {
- q.MulQuats(*q, other)
+ *q = MulQuats(*q, other)
}
// Mul returns returns multiplication of this quaternion with other
-func (q *Quat) Mul(other Quat) Quat {
- nq := *q
- nq.SetMul(other)
- return nq
+func (q Quat) Mul(other Quat) Quat {
+ q.SetMul(other)
+ return q
+}
+
+// MulVector applies the rotation encoded in the quaternion to the [Vector3].
+func (q Quat) MulVector(v Vector3) Vector3 {
+ xyz := Vec3(q.X, q.Y, q.Z)
+ t := xyz.Cross(v).MulScalar(2)
+ return v.Add(t.MulScalar(q.W)).Add(xyz.Cross(t))
+}
+
+// MulVectorInverse applies the inverse of the rotation encoded in the quaternion
+// to the [Vector3].
+func (q Quat) MulVectorInverse(v Vector3) Vector3 {
+ xyz := Vec3(q.X, q.Y, q.Z)
+ t := xyz.Cross(v).MulScalar(2)
+ return v.Sub(t.MulScalar(q.W)).Add(xyz.Cross(t))
}
-// Slerp sets this quaternion to another quaternion which is the spherically linear interpolation
-// from this quaternion to other using t.
+// Slerp sets this quaternion to another quaternion which is the
+// spherically linear interpolation from this quaternion to other using t.
func (q *Quat) Slerp(other Quat, t float32) {
if t == 0 {
return
diff --git a/math32/quaternion_test.go b/math32/quaternion_test.go
index 00615805ba..ae17e49969 100644
--- a/math32/quaternion_test.go
+++ b/math32/quaternion_test.go
@@ -104,14 +104,27 @@ func TestQuatMulQuats(t *testing.T) {
q1 := Quat{X: 1, Y: 2, Z: 3, W: 4}
q2 := Quat{X: 5, Y: 6, Z: 7, W: 8}
- q := Quat{}
- q.MulQuats(q1, q2)
+ q := MulQuats(q1, q2)
expected := Quat{X: 24, Y: 48, Z: 48, W: -6}
assert.Equal(t, expected, q)
}
+func TestQuatMulVector(t *testing.T) {
+ q1 := Quat{X: 1, Y: 2, Z: 3, W: 4}
+ q1.Normalize() // note: critical
+ v := Vector3{X: 5, Y: 6, Z: 7}
+
+ vr := q1.MulVector(v)
+
+ expected := Vector3{X: 1.7999997, Y: 7.6, Z: 7}
+ assert.Equal(t, expected, vr)
+
+ viv := q1.MulVectorInverse(q1.MulVector(v))
+ assert.Equal(t, v, viv)
+}
+
func TestQuatSlerp(t *testing.T) {
q1 := Quat{X: 1, Y: 2, Z: 3, W: 4}
q2 := Quat{X: 5, Y: 6, Z: 7, W: 8}
diff --git a/math32/vector2.go b/math32/vector2.go
index 8dc6c84d70..3fdbe1d138 100644
--- a/math32/vector2.go
+++ b/math32/vector2.go
@@ -72,128 +72,11 @@ func (v *Vector2) SetFromVector2i(vi Vector2i) {
v.Y = float32(vi.Y)
}
-// SetDim sets the given vector component value by its dimension index.
-func (v *Vector2) SetDim(dim Dims, value float32) {
- switch dim {
- case X:
- v.X = value
- case Y:
- v.Y = value
- default:
- panic("dim is out of range")
- }
-}
-
-// Dim returns the given vector component.
-func (v Vector2) Dim(dim Dims) float32 {
- switch dim {
- case X:
- return v.X
- case Y:
- return v.Y
- default:
- panic("dim is out of range")
- }
-}
-
-// SetPointDim sets the given dimension of the given [image.Point] to the given value.
-func SetPointDim(pt *image.Point, dim Dims, value int) {
- switch dim {
- case X:
- pt.X = value
- case Y:
- pt.Y = value
- default:
- panic("dim is out of range")
- }
-}
-
-// PointDim returns the given dimension of the given [image.Point].
-func PointDim(pt image.Point, dim Dims) int {
- switch dim {
- case X:
- return pt.X
- case Y:
- return pt.Y
- default:
- panic("dim is out of range")
- }
-}
-
-func (a Vector2) String() string {
- return fmt.Sprintf("(%v, %v)", a.X, a.Y)
-}
-
-// SetPoint sets the vector from the given [image.Point].
-func (a *Vector2) SetPoint(pt image.Point) {
- a.X = float32(pt.X)
- a.Y = float32(pt.Y)
-}
-
-// SetFixed sets the vector from the given [fixed.Point26_6].
-func (a *Vector2) SetFixed(pt fixed.Point26_6) {
- a.X = FromFixed(pt.X)
- a.Y = FromFixed(pt.Y)
-}
-
-// ToPoint returns the vector as an [image.Point].
-func (a Vector2) ToPoint() image.Point {
- return image.Point{int(a.X), int(a.Y)}
-}
-
-// ToPointFloor returns the vector as an [image.Point] with all values [Floor]ed.
-func (a Vector2) ToPointFloor() image.Point {
- return image.Point{int(Floor(a.X)), int(Floor(a.Y))}
-}
-
-// ToPointCeil returns the vector as an [image.Point] with all values [Ceil]ed.
-func (a Vector2) ToPointCeil() image.Point {
- return image.Point{int(Ceil(a.X)), int(Ceil(a.Y))}
-}
-
-// ToPointRound returns the vector as an [image.Point] with all values [Round]ed.
-func (a Vector2) ToPointRound() image.Point {
- return image.Point{int(Round(a.X)), int(Round(a.Y))}
-}
-
-// ToFixed returns the vector as a [fixed.Point26_6].
-func (a Vector2) ToFixed() fixed.Point26_6 {
- return ToFixedPoint(a.X, a.Y)
-}
-
-// RectFromPosSizeMax returns an [image.Rectangle] from the floor of pos
-// and ceil of size.
-func RectFromPosSizeMax(pos, size Vector2) image.Rectangle {
- tp := pos.ToPointFloor()
- ts := size.ToPointCeil()
- return image.Rect(tp.X, tp.Y, tp.X+ts.X, tp.Y+ts.Y)
-}
-
-// RectFromPosSizeMin returns an [image.Rectangle] from the ceil of pos
-// and floor of size.
-func RectFromPosSizeMin(pos, size Vector2) image.Rectangle {
- tp := pos.ToPointCeil()
- ts := size.ToPointFloor()
- return image.Rect(tp.X, tp.Y, tp.X+ts.X, tp.Y+ts.Y)
-}
-
// SetZero sets all of the vector's components to zero.
func (v *Vector2) SetZero() {
v.SetScalar(0)
}
-// FromSlice sets this vector's components from the given slice, starting at offset.
-func (v *Vector2) FromSlice(slice []float32, offset int) {
- v.X = slice[offset]
- v.Y = slice[offset+1]
-}
-
-// ToSlice copies this vector's components to the given slice, starting at offset.
-func (v Vector2) ToSlice(slice []float32, offset int) {
- slice[offset] = v.X
- slice[offset+1] = v.Y
-}
-
// Basic math operations:
// Add adds the other given vector to this one and returns the result as a new vector.
@@ -336,70 +219,11 @@ func (v *Vector2) Clamp(min, max Vector2) {
}
}
-// Floor returns this vector with [Floor] applied to each of its components.
-func (v Vector2) Floor() Vector2 {
- return Vec2(Floor(v.X), Floor(v.Y))
-}
-
-// Ceil returns this vector with [Ceil] applied to each of its components.
-func (v Vector2) Ceil() Vector2 {
- return Vec2(Ceil(v.X), Ceil(v.Y))
-}
-
-// Round returns this vector with [Round] applied to each of its components.
-func (v Vector2) Round() Vector2 {
- return Vec2(Round(v.X), Round(v.Y))
-}
-
// Negate returns the vector with each component negated.
func (v Vector2) Negate() Vector2 {
return Vec2(-v.X, -v.Y)
}
-// AddDim returns the vector with the given value added on the given dimension.
-func (a Vector2) AddDim(d Dims, value float32) Vector2 {
- switch d {
- case X:
- a.X += value
- case Y:
- a.Y += value
- }
- return a
-}
-
-// SubDim returns the vector with the given value subtracted on the given dimension.
-func (a Vector2) SubDim(d Dims, value float32) Vector2 {
- switch d {
- case X:
- a.X -= value
- case Y:
- a.Y -= value
- }
- return a
-}
-
-// MulDim returns the vector with the given value multiplied by on the given dimension.
-func (a Vector2) MulDim(d Dims, value float32) Vector2 {
- switch d {
- case X:
- a.X *= value
- case Y:
- a.Y *= value
- }
- return a
-}
-
-// DivDim returns the vector with the given value divided by on the given dimension.
-func (a Vector2) DivDim(d Dims, value float32) Vector2 {
- switch d {
- case X:
- a.X /= value
- case Y:
- a.Y /= value
- }
- return a
-}
-
// Distance, Normal:
// Dot returns the dot product of this vector with the given other vector.
@@ -498,3 +322,187 @@ func (v Vector2) Rot(phi float32, p0 Vector2) Vector2 {
p0.Y + sinphi*(v.X-p0.X) + cosphi*(v.Y-p0.Y),
}
}
+
+func (a Vector2) String() string {
+ return fmt.Sprintf("(%v, %v)", a.X, a.Y)
+}
+
+// SetDim sets the given vector component value by its dimension index.
+func (v *Vector2) SetDim(dim Dims, value float32) {
+ switch dim {
+ case X:
+ v.X = value
+ case Y:
+ v.Y = value
+ default:
+ panic("dim is out of range")
+ }
+}
+
+// Dim returns the given vector component.
+func (v Vector2) Dim(dim Dims) float32 {
+ switch dim {
+ case X:
+ return v.X
+ case Y:
+ return v.Y
+ default:
+ panic("dim is out of range")
+ }
+}
+
+// SetPointDim sets the given dimension of the given [image.Point] to the given value.
+func SetPointDim(pt *image.Point, dim Dims, value int) {
+ switch dim {
+ case X:
+ pt.X = value
+ case Y:
+ pt.Y = value
+ default:
+ panic("dim is out of range")
+ }
+}
+
+// PointDim returns the given dimension of the given [image.Point].
+func PointDim(pt image.Point, dim Dims) int {
+ switch dim {
+ case X:
+ return pt.X
+ case Y:
+ return pt.Y
+ default:
+ panic("dim is out of range")
+ }
+}
+
+// AddDim returns the vector with the given value added on the given dimension.
+func (a Vector2) AddDim(d Dims, value float32) Vector2 {
+ switch d {
+ case X:
+ a.X += value
+ case Y:
+ a.Y += value
+ default:
+ panic("dim is out of range")
+ }
+ return a
+}
+
+// SubDim returns the vector with the given value subtracted on the given dimension.
+func (a Vector2) SubDim(d Dims, value float32) Vector2 {
+ switch d {
+ case X:
+ a.X -= value
+ case Y:
+ a.Y -= value
+ default:
+ panic("dim is out of range")
+ }
+ return a
+}
+
+// MulDim returns the vector with the given value multiplied by on the given dimension.
+func (a Vector2) MulDim(d Dims, value float32) Vector2 {
+ switch d {
+ case X:
+ a.X *= value
+ case Y:
+ a.Y *= value
+ default:
+ panic("dim is out of range")
+ }
+ return a
+}
+
+// DivDim returns the vector with the given value divided by on the given dimension.
+func (a Vector2) DivDim(d Dims, value float32) Vector2 {
+ switch d {
+ case X:
+ a.X /= value
+ case Y:
+ a.Y /= value
+ default:
+ panic("dim is out of range")
+ }
+ return a
+}
+
+// SetPoint sets the vector from the given [image.Point].
+func (a *Vector2) SetPoint(pt image.Point) {
+ a.X = float32(pt.X)
+ a.Y = float32(pt.Y)
+}
+
+// SetFixed sets the vector from the given [fixed.Point26_6].
+func (a *Vector2) SetFixed(pt fixed.Point26_6) {
+ a.X = FromFixed(pt.X)
+ a.Y = FromFixed(pt.Y)
+}
+
+// ToPoint returns the vector as an [image.Point].
+func (a Vector2) ToPoint() image.Point {
+ return image.Point{int(a.X), int(a.Y)}
+}
+
+// ToPointFloor returns the vector as an [image.Point] with all values [Floor]ed.
+func (a Vector2) ToPointFloor() image.Point {
+ return image.Point{int(Floor(a.X)), int(Floor(a.Y))}
+}
+
+// ToPointCeil returns the vector as an [image.Point] with all values [Ceil]ed.
+func (a Vector2) ToPointCeil() image.Point {
+ return image.Point{int(Ceil(a.X)), int(Ceil(a.Y))}
+}
+
+// ToPointRound returns the vector as an [image.Point] with all values [Round]ed.
+func (a Vector2) ToPointRound() image.Point {
+ return image.Point{int(Round(a.X)), int(Round(a.Y))}
+}
+
+// ToFixed returns the vector as a [fixed.Point26_6].
+func (a Vector2) ToFixed() fixed.Point26_6 {
+ return ToFixedPoint(a.X, a.Y)
+}
+
+// RectFromPosSizeMax returns an [image.Rectangle] from the floor of pos
+// and ceil of size.
+func RectFromPosSizeMax(pos, size Vector2) image.Rectangle {
+ tp := pos.ToPointFloor()
+ ts := size.ToPointCeil()
+ return image.Rect(tp.X, tp.Y, tp.X+ts.X, tp.Y+ts.Y)
+}
+
+// RectFromPosSizeMin returns an [image.Rectangle] from the ceil of pos
+// and floor of size.
+func RectFromPosSizeMin(pos, size Vector2) image.Rectangle {
+ tp := pos.ToPointCeil()
+ ts := size.ToPointFloor()
+ return image.Rect(tp.X, tp.Y, tp.X+ts.X, tp.Y+ts.Y)
+}
+
+// FromSlice sets this vector's components from the given slice, starting at offset.
+func (v *Vector2) FromSlice(slice []float32, offset int) {
+ v.X = slice[offset]
+ v.Y = slice[offset+1]
+}
+
+// ToSlice copies this vector's components to the given slice, starting at offset.
+func (v Vector2) ToSlice(slice []float32, offset int) {
+ slice[offset] = v.X
+ slice[offset+1] = v.Y
+}
+
+// Floor returns this vector with [Floor] applied to each of its components.
+func (v Vector2) Floor() Vector2 {
+ return Vec2(Floor(v.X), Floor(v.Y))
+}
+
+// Ceil returns this vector with [Ceil] applied to each of its components.
+func (v Vector2) Ceil() Vector2 {
+ return Vec2(Ceil(v.X), Ceil(v.Y))
+}
+
+// Round returns this vector with [Round] applied to each of its components.
+func (v Vector2) Round() Vector2 {
+ return Vec2(Round(v.X), Round(v.Y))
+}
diff --git a/math32/vector2i.go b/math32/vector2i.go
index 9fa8e2cfc9..ffe60df6b9 100644
--- a/math32/vector2i.go
+++ b/math32/vector2i.go
@@ -17,8 +17,8 @@ type Vector2i struct {
}
// Vec2i returns a new [Vector2i] with the given x and y components.
-func Vec2i(x, y int32) Vector2i {
- return Vector2i{X: x, Y: y}
+func Vec2i(x, y int) Vector2i {
+ return Vector2i{X: int32(x), Y: int32(y)}
}
// Vector2iScalar returns a new [Vector2i] with all components set to the given scalar value.
diff --git a/math32/vector3.go b/math32/vector3.go
index aec01dc91b..ca5255a2a5 100644
--- a/math32/vector3.go
+++ b/math32/vector3.go
@@ -412,25 +412,6 @@ func (v Vector3) MulProjection(m *Matrix4) Vector3 {
(m[2]*v.X + m[6]*v.Y + m[10]*v.Z + m[14]) * d}
}
-// MulQuat returns vector multiplied by specified quaternion and
-// then by the quaternion inverse.
-// It basically applies the rotation encoded in the quaternion to this vector.
-func (v Vector3) MulQuat(q Quat) Vector3 {
- qx := q.X
- qy := q.Y
- qz := q.Z
- qw := q.W
- // calculate quat * vector
- ix := qw*v.X + qy*v.Z - qz*v.Y
- iy := qw*v.Y + qz*v.X - qx*v.Z
- iz := qw*v.Z + qx*v.Y - qy*v.X
- iw := -qx*v.X - qy*v.Y - qz*v.Z
- // calculate result * inverse quat
- return Vector3{ix*qw + iw*-qx + iy*-qz - iz*-qy,
- iy*qw + iw*-qy + iz*-qx - ix*-qz,
- iz*qw + iw*-qz + ix*-qy - iy*-qx}
-}
-
// Cross returns the cross product of this vector with other.
func (v Vector3) Cross(other Vector3) Vector3 {
return Vec3(v.Y*other.Z-v.Z*other.Y, v.Z*other.X-v.X*other.Z, v.X*other.Y-v.Y*other.X)
diff --git a/paint/paint_test.go b/paint/paint_test.go
index 90ad3a2010..869bf9a81e 100644
--- a/paint/paint_test.go
+++ b/paint/paint_test.go
@@ -37,9 +37,12 @@ func RunTest(t *testing.T, nm string, width int, height int, f func(pc *Painter)
rend := pc.RenderDone()
ir := NewImageRenderer(size)
sv := NewSVGRenderer(size)
+ pd := NewPDFRenderer(size, &pc.Context().Style.UnitContext)
ir.Render(rend)
sv.Render(rend)
+ pd.Render(rend)
imagex.Assert(t, ir.Image(), nm)
+
svdir := filepath.Join("testdata", "svg")
dp, fno := filepath.Split(nm)
if dp != "" {
@@ -50,6 +53,15 @@ func RunTest(t *testing.T, nm string, width int, height int, f func(pc *Painter)
svfnm := filepath.Join(svdir, nm) + ".svg"
err := os.WriteFile(svfnm, sv.Source(), 0666)
assert.NoError(t, err)
+
+ pddir := filepath.Join("testdata", "pdf")
+ if dp != "" {
+ pddir = filepath.Join(pddir, dp)
+ }
+ os.MkdirAll(pddir, 0777)
+ pdfnm := filepath.Join(pddir, nm) + ".pdf"
+ err = os.WriteFile(pdfnm, pd.Source(), 0666)
+ assert.NoError(t, err)
}
func TestRender(t *testing.T) {
@@ -98,7 +110,7 @@ func TestRender(t *testing.T) {
tx, err := htmltext.HTMLToRich([]byte("This is HTMLformattedtext"), fsty, nil)
assert.NoError(t, err)
- lns := txtSh.WrapLines(tx, fsty, tsty, &rich.DefaultSettings, math32.Vec2(100, 60))
+ lns := txtSh.WrapLines(tx, fsty, tsty, math32.Vec2(100, 60))
// if tsz.X != 100 || tsz.Y != 60 {
// t.Errorf("unexpected text size: %v", tsz)
// }
diff --git a/paint/painter.go b/paint/painter.go
index 83a492f42f..486eb71362 100644
--- a/paint/painter.go
+++ b/paint/painter.go
@@ -57,8 +57,10 @@ func NewPainter(size math32.Vector2) *Painter {
return pc
}
-func (pc *Painter) Transform() math32.Matrix2 {
- return pc.Context().Transform.Mul(pc.Paint.Transform)
+// Cumulative returns the current cumulative transform function,
+// including the current transform.
+func (pc *Painter) Cumulative() math32.Matrix2 {
+ return pc.Context().Cumulative.Mul(pc.Paint.Transform)
}
//////// Path basics
@@ -446,8 +448,8 @@ func (pc *Painter) DrawBox(pos, size math32.Vector2, img image.Image, op draw.Op
if img == nil {
img = colors.Uniform(color.RGBA{})
}
- pos = pc.Transform().MulVector2AsPoint(pos)
- size = pc.Transform().MulVector2AsVector(size)
+ pos = pc.Cumulative().MulPoint(pos)
+ size = pc.Cumulative().MulVector(size)
br := math32.RectFromPosSizeMax(pos, size)
cb := pc.Context().Bounds.Rect.ToRect()
b := cb.Intersect(br)
@@ -455,7 +457,7 @@ func (pc *Painter) DrawBox(pos, size math32.Vector2, img image.Image, op draw.Op
return
}
if g, ok := img.(gradient.Gradient); ok {
- g.Update(pc.Fill.Opacity, math32.B2FromRect(b), pc.Transform())
+ g.Update(pc.Fill.Opacity, math32.B2FromRect(b), pc.Cumulative())
} else {
img = gradient.ApplyOpacity(img, pc.Fill.Opacity)
}
@@ -508,39 +510,47 @@ func (pc *Painter) SetPixel(x, y int) {
// using the bounds of the source image in rectangle rect, using
// the given draw operration: Over = overlay (alpha blend with destination)
// Src = copy source directly, overwriting destination pixels.
-func (pc *Painter) DrawImage(src image.Image, rect image.Rectangle, srcStart image.Point, op draw.Op) {
- pc.Render.Add(pimage.NewDraw(rect, src, srcStart, op))
+func (pc *Painter) DrawImage(src image.Image, rect image.Rectangle, srcStart image.Point, op draw.Op) *pimage.Params {
+ pim := pimage.NewDraw(rect, src, srcStart, op)
+ pc.Render.Add(pim)
+ return pim
}
// DrawImageAnchored draws the specified image at the specified anchor point.
// The anchor point is x - w * ax, y - h * ay, where w, h is the size of the
// image. Use ax=0.5, ay=0.5 to center the image at the specified point.
-func (pc *Painter) DrawImageAnchored(src image.Image, x, y, ax, ay float32) {
+func (pc *Painter) DrawImageAnchored(src image.Image, x, y, ax, ay float32) *pimage.Params {
s := src.Bounds().Size()
x -= ax * float32(s.X)
y -= ay * float32(s.Y)
- m := pc.Transform().Translate(x, y)
+ m := pc.Cumulative().Translate(x, y)
+ var pim *pimage.Params
if pc.Mask == nil {
- pc.Render.Add(pimage.NewTransform(m, src.Bounds(), src, draw.Over))
+ pim = pimage.NewTransform(m, src.Bounds(), src, draw.Over)
} else {
- pc.Render.Add(pimage.NewTransformMask(m, src.Bounds(), src, draw.Over, pc.Mask, image.Point{}))
+ pim = pimage.NewTransformMask(m, src.Bounds(), src, draw.Over, pc.Mask, image.Point{})
}
+ pc.Render.Add(pim)
+ return pim
}
// DrawImageScaled draws the specified image starting at given upper-left point,
// such that the size of the image is rendered as specified by w, h parameters
// (an additional scaling is applied to the transform matrix used in rendering)
-func (pc *Painter) DrawImageScaled(src image.Image, x, y, w, h float32) {
+func (pc *Painter) DrawImageScaled(src image.Image, x, y, w, h float32) *pimage.Params {
s := src.Bounds().Size()
isz := math32.FromPoint(s)
isc := math32.Vec2(w, h).Div(isz)
- m := pc.Transform().Translate(x, y).Scale(isc.X, isc.Y)
+ m := pc.Cumulative().Translate(x, y).Scale(isc.X, isc.Y)
+ var pim *pimage.Params
if pc.Mask == nil {
- pc.Render.Add(pimage.NewTransform(m, src.Bounds(), src, draw.Over))
+ pim = pimage.NewTransform(m, src.Bounds(), src, draw.Over)
} else {
- pc.Render.Add(pimage.NewTransformMask(m, src.Bounds(), src, draw.Over, pc.Mask, image.Point{}))
+ pim = pimage.NewTransformMask(m, src.Bounds(), src, draw.Over, pc.Mask, image.Point{})
}
+ pc.Render.Add(pim)
+ return pim
}
// BoundingBox computes the bounding box for an element in pixel int
@@ -550,8 +560,8 @@ func (pc *Painter) BoundingBox(minX, minY, maxX, maxY float32) image.Rectangle {
// if pc.Stroke.Color != nil {// todo
// sw = 0.5 * pc.StrokeWidth()
// }
- tmin := pc.Transform().MulVector2AsPoint(math32.Vec2(minX, minY))
- tmax := pc.Transform().MulVector2AsPoint(math32.Vec2(maxX, maxY))
+ tmin := pc.Cumulative().MulPoint(math32.Vec2(minX, minY))
+ tmax := pc.Cumulative().MulPoint(math32.Vec2(maxX, maxY))
tp1 := math32.Vec2(tmin.X-sw, tmin.Y-sw).ToPointFloor()
tp2 := math32.Vec2(tmax.X+sw, tmax.Y+sw).ToPointCeil()
return image.Rect(tp1.X, tp1.Y, tp2.X, tp2.Y)
diff --git a/paint/pdf/README.md b/paint/pdf/README.md
new file mode 100644
index 0000000000..294eca8de7
--- /dev/null
+++ b/paint/pdf/README.md
@@ -0,0 +1,11 @@
+# PDF rendering
+
+This package adapts the PDF rendering code from https://github.com/tdewolff/canvas (Copyright (c) 2015 Taco de Wolff, under an MIT License.) to the Cogent Core rendering framework. It is used for rendering PDFs of content documentation, producing publication-ready output from markdown input. It is also used by the Cogent Canvas SVG drawing app to render PDFs of SVGs.
+
+
+## Helpful links
+
+* Current 2.0 Standard: https://pdfa.org/sponsored-standards/
+* Version 1.2: https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.2.pdf
+* Validator: https://www.pdf-online.com/osa/validate.aspx
+
diff --git a/paint/pdf/context.go b/paint/pdf/context.go
new file mode 100644
index 0000000000..50a1ff6312
--- /dev/null
+++ b/paint/pdf/context.go
@@ -0,0 +1,80 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package pdf
+
+import (
+ "fmt"
+
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/styles"
+)
+
+// context holds the graphics state context to track the corresponding
+// PDF state to optimize setting of styles.
+type context struct {
+
+ // Style is current style: copied from parent context initially.
+ Style styles.Paint
+
+ // Transform is the current transform that has been set.
+ // it is not accumulated.
+ Transform math32.Matrix2
+}
+
+func newContext(sty *styles.Paint, ctm math32.Matrix2) *context {
+ c := &context{}
+ c.Style = *sty
+ c.Transform = ctm
+ return c
+}
+
+// PushStack adds a graphics stack push (q), which must
+// be paired with a corresponding Pop (Q).
+func (w *pdfPage) PushStack() {
+ ctx := w.stack.Peek()
+ fmt.Fprintf(w, " q")
+ w.stack.Push(newContext(&ctx.Style, ctx.Transform))
+}
+
+// PopStack adds a graphics stack pop (Q) which must
+// be paired with a corresponding Push (q).
+func (w *pdfPage) PopStack() {
+ fmt.Fprintf(w, " Q")
+ w.stack.Pop()
+}
+
+// SetTransform adds a cm to set the current matrix transform (CMT).
+func (w *pdfPage) SetTransform(m math32.Matrix2) {
+ rot := m.ExtractRot()
+ m2 := m
+ if rot != 0 {
+ m2 = m.Mul(math32.Rotate2D(-2 * rot))
+ }
+ fmt.Fprintf(w, " %s cm", mat2(m2))
+ ctx := w.stack.Peek()
+ ctx.Transform = m2 // not cumulative!
+}
+
+// PushTransform adds a graphics stack push (q) and then
+// cm to set the current matrix transform (CMT).
+func (w *pdfPage) PushTransform(m math32.Matrix2) {
+ w.PushStack()
+ w.SetTransform(m)
+}
+
+// Cumulative returns the current cumulative transform.
+func (w *pdfPage) Cumulative() math32.Matrix2 {
+ m := math32.Identity2()
+ for _, s := range w.stack {
+ m = m.Mul(s.Transform)
+ }
+ return m
+}
+
+// style() returns the currently active style
+func (w *pdfPage) style() *styles.Paint {
+ ctx := w.stack.Peek()
+ return &ctx.Style
+}
diff --git a/paint/pdf/font.go b/paint/pdf/font.go
new file mode 100644
index 0000000000..b47ecd3dc1
--- /dev/null
+++ b/paint/pdf/font.go
@@ -0,0 +1,331 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This is adapted from https://github.com/tdewolff/canvas
+// Copyright (c) 2015 Taco de Wolff, under an MIT License.
+
+package pdf
+
+import (
+ "cogentcore.org/core/text/rich"
+ "cogentcore.org/core/text/text"
+)
+
+func standardFontName(sty *rich.Style) string {
+ name := "Helvetica"
+ switch sty.Family {
+ case rich.SansSerif:
+ name = "Helvetica"
+ case rich.Serif:
+ name = "Times"
+ case rich.Monospace:
+ name = "Courier"
+ case rich.Cursive:
+ name = "ZapfChancery"
+ case rich.Math:
+ name = "Symbol"
+ case rich.Emoji:
+ name = "ZapfDingbats"
+ }
+ bname := name
+ if sty.Weight > rich.Medium {
+ name += "-Bold"
+ if sty.Slant == rich.Italic {
+ if bname == "Times" {
+ name += "Italic"
+ } else {
+ name += "Oblique"
+ }
+ }
+ } else {
+ if sty.Slant == rich.Italic {
+ if bname == "Times" {
+ name += "-Italic"
+ } else {
+ name += "-Oblique"
+ }
+ }
+ }
+ if name == "Times" {
+ name = "Times-Roman" // ugh
+ }
+ return name
+}
+
+func (w *pdfWriter) getFont(sty *rich.Style, tsty *text.Style) pdfRef {
+ if sty.Family != rich.Custom {
+ stdFont := standardFontName(sty)
+ if ref, ok := w.fontsStd[stdFont]; ok {
+ return ref
+ }
+
+ dict := pdfDict{
+ "Type": pdfName("Font"),
+ "Subtype": pdfName("Type1"),
+ "BaseFont": pdfName(stdFont),
+ "Encoding": pdfName("WinAnsiEncoding"),
+ }
+ ref := w.writeObject(dict)
+ w.fontsStd[stdFont] = ref
+ return ref
+ }
+ // todo: deal with custom
+ /*
+ fonts := w.fontsH
+ if vertical {
+ fonts = w.fontsV
+ }
+ if ref, ok := fonts[font]; ok {
+ return ref
+ }
+ w.objOffsets = append(w.objOffsets, 0)
+ ref := pdfRef(len(w.objOffsets))
+ fonts[font] = ref
+ w.fontSubset[font] = ppath.NewFontSubsetter()
+ return ref
+ */
+ return 0
+}
+
+/*
+func (w *pdfWriter) writeFont(ref pdfRef, font *text.Font, vertical bool) {
+ // subset the font, we only write the used characters to the PDF CMap object to reduce its
+ // length. At the end of the function we add a CID to GID mapping to correctly select the
+ // right glyphID.
+ sfnt := font.SFNT
+ glyphIDs := w.fontSubset[font].List() // also when not subsetting, to minimize cmap table
+ if w.subset {
+ if sfnt.IsCFF && sfnt.CFF != nil {
+ sfnt.CFF.SetGlyphNames(nil)
+ }
+ sfntSubset, err := sfnt.Subset(glyphIDs, canvasFont.SubsetOptions{Tables: canvasFont.KeepPDFTables})
+ if err == nil {
+ sfnt = sfntSubset
+ } else {
+ fmt.Println("WARNING: font subsetting failed:", err)
+ }
+ }
+ fontProgram := sfnt.Write()
+
+ // calculate the character widths for the W array and shorten it
+ f := 1000.0 / float64(font.SFNT.Head.UnitsPerEm)
+ widths := make([]int, len(glyphIDs)+1)
+ for subsetGlyphID, glyphID := range glyphIDs {
+ widths[subsetGlyphID] = int(f*float64(font.SFNT.GlyphAdvance(glyphID)) + 0.5)
+ }
+ DW := widths[0]
+ W := pdfArray{}
+ i, j := 1, 1
+ for k, width := range widths {
+ if k != 0 && width != widths[j] {
+ if 4 < k-j { // at about 5 equal widths, it would be shorter using the other notation format
+ if i < j {
+ arr := pdfArray{}
+ for _, w := range widths[i:j] {
+ arr = append(arr, w)
+ }
+ W = append(W, i, arr)
+ }
+ if widths[j] != DW {
+ W = append(W, j, k-1, widths[j])
+ }
+ i = k
+ }
+ j = k
+ }
+ }
+ if i < len(widths) {
+ arr := pdfArray{}
+ for _, w := range widths[i:] {
+ arr = append(arr, w)
+ }
+ W = append(W, i, arr)
+ }
+
+ // create ToUnicode CMap
+ var bfRange, bfChar strings.Builder
+ var bfRangeCount, bfCharCount int
+ startGlyphID := uint16(0)
+ startUnicode := uint32('\uFFFD')
+ length := uint16(1)
+ for subsetGlyphID, glyphID := range glyphIDs[1:] {
+ unicode := uint32(font.SFNT.Cmap.ToUnicode(glyphID))
+ if 0x010000 <= unicode && unicode <= 0x10FFFF {
+ // UTF-16 surrogates
+ unicode -= 0x10000
+ unicode = (0xD800+(unicode>>10)&0x3FF)<<16 + 0xDC00 + unicode&0x3FF
+ }
+ if uint16(subsetGlyphID+1) == startGlyphID+length && unicode == startUnicode+uint32(length) {
+ length++
+ } else {
+ if 1 < length {
+ fmt.Fprintf(&bfRange, "\n<%04X> <%04X> <%04X>", startGlyphID, startGlyphID+length-1, startUnicode)
+ bfRangeCount++
+ } else {
+ fmt.Fprintf(&bfChar, "\n<%04X> <%04X>", startGlyphID, startUnicode)
+ bfCharCount++
+ }
+ startGlyphID = uint16(subsetGlyphID + 1)
+ startUnicode = unicode
+ length = 1
+ }
+ }
+ if 1 < length {
+ fmt.Fprintf(&bfRange, "\n<%04X> <%04X> <%04X>", startGlyphID, startGlyphID+length-1, startUnicode)
+ bfRangeCount++
+ } else {
+ fmt.Fprintf(&bfChar, "\n<%04X> <%04X>", startGlyphID, startUnicode)
+ bfCharCount++
+ }
+
+ toUnicode := bytes.Buffer{}
+ fmt.Fprintf(&toUnicode, `/CIDInit /ProcSet findresource begin
+12 dict begin
+begincmap
+/CIDSystemInfo <> def
+/CMapName /Adobe-Identity-UCS def
+/CMapType 2 def
+1 begincodespacerange
+<0000> endcodespacerange`)
+ if 0 < bfRangeCount {
+ fmt.Fprintf(&toUnicode, `
+%d beginbfrange%s endbfrange`, bfRangeCount, bfRange.String())
+ }
+ if 0 < bfCharCount {
+ fmt.Fprintf(&toUnicode, `
+%d beginbfchar%s endbfchar`, bfCharCount, bfChar.String())
+ }
+ fmt.Fprintf(&toUnicode, `
+endcmap
+CMapName currentdict /CMap defineresource pop
+end
+end`)
+ toUnicodeStream := pdfStream{
+ dict: pdfDict{},
+ stream: toUnicode.Bytes(),
+ }
+ if w.compress {
+ toUnicodeStream.dict["Filter"] = pdfFilterFlate
+ }
+ toUnicodeRef := w.writeObject(toUnicodeStream)
+
+ // write font program
+ var cidSubtype string
+ var fontfileKey pdfName
+ var fontfileRef pdfRef
+ if font.SFNT.IsTrueType {
+ cidSubtype = "CIDFontType2"
+ fontfileKey = "FontFile2"
+ fontfileRef = w.writeObject(pdfStream{
+ dict: pdfDict{
+ "Filter": pdfFilterFlate,
+ },
+ stream: fontProgram,
+ })
+ } else if font.SFNT.IsCFF {
+ cidSubtype = "CIDFontType0"
+ fontfileKey = "FontFile3"
+ fontfileRef = w.writeObject(pdfStream{
+ dict: pdfDict{
+ "Subtype": pdfName("OpenType"),
+ "Filter": pdfFilterFlate,
+ },
+ stream: fontProgram,
+ })
+ }
+
+ // get name and CID subtype
+ name := font.Name()
+ if records := font.SFNT.Name.Get(canvasFont.NamePostScript); 0 < len(records) {
+ name = records[0].String()
+ }
+ baseFont := strings.ReplaceAll(name, " ", "")
+ if w.subset {
+ baseFont = "SUBSET+" + baseFont // TODO: give unique subset name
+ }
+
+ encoding := "Identity-H"
+ if vertical {
+ encoding = "Identity-V"
+ }
+
+ // in order to support more than 256 characters, we need to use a CIDFont dictionary which must be inside a Type0 font. Character codes in the stream are glyph IDs, however for subsetted fonts they are the _old_ glyph IDs, which is why we need the CIDToGIDMap
+ dict := pdfDict{
+ "Type": pdfName("Font"),
+ "Subtype": pdfName("Type0"),
+ "BaseFont": pdfName(baseFont),
+ "Encoding": pdfName(encoding), // map character codes in the stream to CID with identity encoding, we additionally map CID to GID in the descendant font when subsetting, otherwise that is also identity
+ "ToUnicode": toUnicodeRef,
+ "DescendantFonts": pdfArray{pdfDict{
+ "Type": pdfName("Font"),
+ "Subtype": pdfName(cidSubtype),
+ "BaseFont": pdfName(baseFont),
+ "DW": DW,
+ "W": W,
+ //"CIDToGIDMap": pdfName("Identity"),
+ "CIDSystemInfo": pdfDict{
+ "Registry": "Adobe",
+ "Ordering": "Identity",
+ "Supplement": 0,
+ },
+ "FontDescriptor": pdfDict{
+ "Type": pdfName("FontDescriptor"),
+ "FontName": pdfName(baseFont),
+ "Flags": 4, // Symbolic
+ "FontBBox": pdfArray{
+ int(f * float64(font.SFNT.Head.XMin)),
+ int(f * float64(font.SFNT.Head.YMin)),
+ int(f * float64(font.SFNT.Head.XMax)),
+ int(f * float64(font.SFNT.Head.YMax)),
+ },
+ "ItalicAngle": float64(font.SFNT.Post.ItalicAngle),
+ "Ascent": int(f * float64(font.SFNT.Hhea.Ascender)),
+ "Descent": -int(f * float64(font.SFNT.Hhea.Descender)),
+ "CapHeight": int(f * float64(font.SFNT.OS2.SCapHeight)),
+ "StemV": 80, // taken from Inkscape, should be calculated somehow, maybe use: 10+220*(usWeightClass-50)/900
+ fontfileKey: fontfileRef,
+ },
+ }},
+ }
+
+ if !w.subset {
+ cidToGIDMap := make([]byte, 2*len(glyphIDs))
+ for subsetGlyphID, glyphID := range glyphIDs {
+ j := int(subsetGlyphID) * 2
+ cidToGIDMap[j+0] = byte((glyphID & 0xFF00) >> 8)
+ cidToGIDMap[j+1] = byte(glyphID & 0x00FF)
+ }
+ cidToGIDMapStream := pdfStream{
+ dict: pdfDict{},
+ stream: cidToGIDMap,
+ }
+ if w.compress {
+ cidToGIDMapStream.dict["Filter"] = pdfFilterFlate
+ }
+ cidToGIDMapRef := w.writeObject(cidToGIDMapStream)
+ dict["DescendantFonts"].(pdfArray)[0].(pdfDict)["CIDToGIDMap"] = cidToGIDMapRef
+ }
+
+ w.objOffsets[ref-1] = w.pos
+ w.write("%v 0 obj\n", ref)
+ w.writeVal(dict)
+ w.write("\nendobj\n")
+}
+
+func (w *pdfWriter) writeFonts(fontMap map[*text.Font]pdfRef, vertical bool) {
+ // sort fonts by ref to make PDF deterministic
+ refs := make([]pdfRef, 0, len(fontMap))
+ refMap := make(map[pdfRef]*text.Font, len(fontMap))
+ for font, ref := range fontMap {
+ refs = append(refs, ref)
+ refMap[ref] = font
+ }
+ sort.Slice(refs, func(i, j int) bool {
+ return refs[i] < refs[j]
+ })
+ for _, ref := range refs {
+ w.writeFont(ref, refMap[ref], vertical)
+ }
+}
+*/
diff --git a/paint/pdf/layer.go b/paint/pdf/layer.go
new file mode 100644
index 0000000000..8132a86f41
--- /dev/null
+++ b/paint/pdf/layer.go
@@ -0,0 +1,80 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This is adapted from codeberg.org/go-pdf/fpdf
+// Copyright (c) 2023 The go-pdf Authors and Kurt Jung,
+// under an MIT License.
+
+package pdf
+
+import "fmt"
+
+// pdfLayer is one layer
+type pdfLayer struct {
+ name string
+ visible bool
+ index int
+ ref pdfRef
+}
+
+// pdfLayers is all the layers
+type pdfLayers struct {
+ list []pdfLayer
+ currentLayer int
+ openLayerPane bool
+}
+
+func (w *pdfWriter) layerInit() {
+ w.layers.list = make([]pdfLayer, 0)
+ w.layers.currentLayer = -1
+ w.layers.openLayerPane = true
+}
+
+// AddLayer defines a layer that can be shown or hidden when the document is
+// displayed. name specifies the layer name that the document reader will
+// display in the layer list. visible specifies whether the layer will be
+// initially visible. The return value is unique layer index that must be
+// used in subsequent calls to BeginLayer.
+func (w *pdfWriter) AddLayer(name string, visible bool) int {
+ layno := len(w.layers.list)
+ lay := pdfLayer{name: name, visible: visible, index: layno}
+ lay.ref = w.writeObject(pdfDict{"Type": pdfName("OCG"), "Name": name})
+ w.layers.list = append(w.layers.list, lay)
+ return layno
+}
+
+// BeginLayer is called to begin adding content to the specified layer index
+// in a given page. Layer must have already been added via AddLayer.
+// All content added to the page between a call to BeginLayer and a call to
+// EndLayer is added to the layer specified by id.
+func (w *pdfPage) BeginLayer(id int) {
+ w.EndLayer()
+ if id < 0 || id >= len(w.pdf.layers.list) {
+ return
+ }
+ ocn := fmt.Sprintf("oc%d", id)
+ fmt.Fprintf(w, " /OC /%s BDC", ocn)
+ w.pdf.layers.currentLayer = id
+ if _, ok := w.resources["Properties"]; !ok {
+ w.resources["Properties"] = pdfDict{}
+ }
+ l := &w.pdf.layers.list[id]
+ w.resources["Properties"].(pdfDict)[pdfName(ocn)] = l.ref
+}
+
+// EndLayer is called to stop adding content to the currently active layer.
+// See BeginLayer for more details.
+func (w *pdfPage) EndLayer() {
+ if w.pdf.layers.currentLayer < 0 {
+ return
+ }
+ fmt.Fprintf(w, " EMC")
+ w.pdf.layers.currentLayer = -1
+}
+
+// OpenLayerPane advises the document reader to open the layer pane when the
+// document is initially displayed.
+func (w *pdfWriter) OpenLayerPane() {
+ w.layers.openLayerPane = true
+}
diff --git a/paint/pdf/links.go b/paint/pdf/links.go
new file mode 100644
index 0000000000..5f57cd5c03
--- /dev/null
+++ b/paint/pdf/links.go
@@ -0,0 +1,132 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package pdf
+
+import (
+ "cogentcore.org/core/math32"
+)
+
+// AddAnchor adds a uniquely-named link anchor location.
+// The position is in "default user space" coordinates = standard page coordinates,
+// without the current CTM transform.// This function will handle the base page
+// transform for scaling and flipping of coordinates to top-left system.
+func (w *pdfPage) AddAnchor(name string, pos math32.Vector2) {
+ ms := math32.Scale2D(w.pdf.globalScale, w.pdf.globalScale)
+ pos = ms.MulPoint(pos)
+ if w.pdf.anchors == nil {
+ w.pdf.anchors = make(pdfMap)
+ }
+ // pageNo replaced at end with ref:
+ w.pdf.anchors[name] = pdfArray{w.pageNo, pdfName("XYZ"), 0, w.height - pos.Y, 0}
+ // fmt.Println("anchor:", w.pageNo, name, pos)
+}
+
+// AddLink adds a link annotation. The rect is in "default user space"
+// coordinates = standard page coordinates, without the current CTM transform.
+// This function will handle the base page transform for scaling and
+// flipping of coordinates to top-left system.
+func (w *pdfPage) AddLink(uri string, rect math32.Box2) {
+ ms := math32.Scale2D(w.pdf.globalScale, w.pdf.globalScale)
+ rect = rect.MulMatrix2(ms)
+ annot := pdfDict{
+ "Type": pdfName("Annot"),
+ "Subtype": pdfName("Link"),
+ "Border": pdfArray{0, 0, 0},
+ "Rect": pdfArray{rect.Min.X, w.height - rect.Max.Y, rect.Max.X, w.height - rect.Min.Y},
+ }
+ if len(uri) > 0 && uri[0] == '#' { // local anchor actions
+ annot["Dest"] = uri[1:]
+ } else {
+ annot["Contents"] = uri
+ annot["A"] = pdfDict{
+ "S": pdfName("URI"),
+ pdfName("URI"): uri,
+ }
+ }
+ w.annots = append(w.annots, annot)
+}
+
+//////// outline
+
+// pdfOutline is one outline entry
+type pdfOutline struct {
+ name string
+ level int
+ page int
+ ypos float32 // inverted y position
+
+ parent int // index into outline list
+ count int
+ prev int
+ next int
+ first int
+ last int
+}
+
+// AddOutline adds an outline element. level must be > 0
+func (w *pdfPage) AddOutline(name string, level int, ypos float32) {
+ ol := &pdfOutline{name: name, level: level, page: w.pageNo, ypos: w.height - w.pdf.globalScale*ypos, prev: -1, next: -1, first: -1, last: -1}
+ if len(w.pdf.outlines) == 0 {
+ or := &pdfOutline{name: "Contents", level: 0, page: 0, ypos: w.height, prev: -1, next: -1, first: -1, last: -1}
+ w.pdf.outlines = append(w.pdf.outlines, or)
+ }
+ w.pdf.outlines = append(w.pdf.outlines, ol)
+}
+
+// writeOutlines outputs all the outline elements, and returns the ref to the first one.
+func (w *pdfWriter) writeOutlines() pdfRef {
+ lastLev := make(map[int]int)
+ level := 0
+ n := len(w.outlines)
+ // note: this logic from fpdf
+ for i, o := range w.outlines {
+ if o.level > 0 {
+ parent := lastLev[o.level-1]
+ o.parent = parent
+ w.outlines[parent].last = i
+ w.outlines[parent].count++
+ if o.level > level {
+ w.outlines[parent].first = i
+ }
+ } else {
+ o.parent = n
+ }
+ if o.level <= level && i > 0 {
+ prev := lastLev[o.level]
+ w.outlines[prev].next = i
+ o.prev = prev
+ }
+ lastLev[o.level] = i
+ level = o.level
+ }
+ var refs pdfArray
+ firstRef := pdfRef(len(w.objOffsets) + 1)
+ for i, o := range w.outlines {
+ od := pdfDict{
+ "Title": o.name,
+ "Parent": firstRef + pdfRef(o.parent),
+ "Dest": pdfArray{w.pages[o.page], pdfName("XYZ"), 0, o.ypos, 0},
+ "Count": o.count,
+ }
+ if o.prev != -1 {
+ od[pdfName("Prev")] = firstRef + pdfRef(o.prev)
+ }
+ if o.next != -1 {
+ od[pdfName("Next")] = firstRef + pdfRef(o.next)
+ }
+ if o.first != -1 {
+ od[pdfName("First")] = firstRef + pdfRef(o.first)
+ }
+ if o.last != -1 {
+ od[pdfName("Last")] = firstRef + pdfRef(o.last)
+ }
+ if i == 0 {
+ delete(od, "Parent")
+ }
+ ref := w.writeObject(od)
+ refs = append(refs, ref)
+ }
+ return firstRef
+}
diff --git a/paint/pdf/page.go b/paint/pdf/page.go
new file mode 100644
index 0000000000..5d69e00449
--- /dev/null
+++ b/paint/pdf/page.go
@@ -0,0 +1,154 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This is adapted from https://github.com/tdewolff/canvas
+// Copyright (c) 2015 Taco de Wolff, under an MIT License.
+
+package pdf
+
+import (
+ "bytes"
+ "fmt"
+ "image"
+
+ "cogentcore.org/core/base/stack"
+ "cogentcore.org/core/math32"
+)
+
+type pdfPage struct {
+ *bytes.Buffer
+ pdf *pdfWriter
+ pageNo int
+ width, height float32
+ resources pdfDict
+ annots pdfArray
+
+ graphicsStates map[float32]pdfName
+ stack stack.Stack[*context]
+ inTextObject bool
+ textPosition math32.Vector2
+ textCharSpace float32
+ textRenderMode int
+}
+
+func (w *pdfPage) writePage(parent pdfRef) pdfRef {
+ b := w.Bytes()
+ if 0 < len(b) && b[0] == ' ' {
+ b = b[1:]
+ }
+ stream := pdfStream{
+ dict: pdfDict{},
+ stream: b,
+ }
+ if w.pdf.compress {
+ stream.dict["Filter"] = pdfFilterFlate
+ }
+ contents := w.pdf.writeObject(stream)
+ page := pdfDict{
+ "Type": pdfName("Page"),
+ "Parent": parent,
+ "MediaBox": pdfArray{0.0, 0.0, w.width, w.height},
+ "Resources": w.resources,
+ "Group": pdfDict{
+ "Type": pdfName("Group"),
+ "S": pdfName("Transparency"),
+ "I": true,
+ "CS": pdfName("DeviceRGB"),
+ },
+ "Contents": contents,
+ }
+ if 0 < len(w.annots) {
+ page["Annots"] = w.annots
+ }
+ return w.pdf.writeObject(page)
+}
+
+// DrawImage embeds and draws an image, as a lossless (PNG)
+func (w *pdfPage) DrawImage(img image.Image, m math32.Matrix2) {
+ size := img.Bounds().Size()
+
+ // add clipping path around image for smooth edges when rotating
+ outerRect := math32.B2(0.0, 0.0, float32(size.X), float32(size.Y)).MulMatrix2(m)
+ bl := m.MulPoint(math32.Vector2{0, 0})
+ br := m.MulPoint(math32.Vector2{float32(size.X), 0})
+ tl := m.MulPoint(math32.Vector2{0, float32(size.Y)})
+ tr := m.MulPoint(math32.Vector2{float32(size.X), float32(size.Y)})
+ fmt.Fprintf(w, " q %v %v %v %v re W n", dec(outerRect.Min.X), dec(outerRect.Min.Y), dec(outerRect.Size().X), dec(outerRect.Size().Y))
+ fmt.Fprintf(w, " %v %v m %v %v l %v %v l %v %v l h W n", dec(bl.X), dec(bl.Y), dec(tl.X), dec(tl.Y), dec(tr.X), dec(tr.Y), dec(br.X), dec(br.Y))
+
+ ref := w.embedImage(img)
+ if _, ok := w.resources["XObject"]; !ok {
+ w.resources["XObject"] = pdfDict{}
+ }
+ name := pdfName(fmt.Sprintf("Im%d", len(w.resources["XObject"].(pdfDict))))
+ w.resources["XObject"].(pdfDict)[name] = ref
+
+ m = m.Scale(float32(size.X), float32(size.Y))
+ w.SetAlpha(1.0)
+ fmt.Fprintf(w, " %s cm /%v Do Q", mat2(m), name)
+}
+
+// embedImage does a lossless image embedding.
+func (w *pdfPage) embedImage(img image.Image) pdfRef {
+ if ref, ok := w.pdf.images[img]; ok {
+ return ref
+ }
+
+ var hasMask bool
+ size := img.Bounds().Size()
+ filter := pdfFilterFlate
+ sp := img.Bounds().Min // starting point
+ stream := make([]byte, size.X*size.Y*3)
+ streamMask := make([]byte, size.X*size.Y)
+ for y := size.Y - 1; y >= 0; y-- { // invert
+ for x := range size.X {
+ pi := (size.Y-1-y)*size.X + x
+ i := pi * 3
+ R, G, B, A := img.At(sp.X+x, sp.Y+y).RGBA()
+ if A != 0 {
+ stream[i+0] = byte((R * 65535 / A) >> 8)
+ stream[i+1] = byte((G * 65535 / A) >> 8)
+ stream[i+2] = byte((B * 65535 / A) >> 8)
+ streamMask[pi] = byte(A >> 8)
+ }
+ if A>>8 != 255 {
+ hasMask = true
+ }
+ }
+ }
+
+ dict := pdfDict{
+ "Type": pdfName("XObject"),
+ "Subtype": pdfName("Image"),
+ "Width": size.X,
+ "Height": size.Y,
+ "ColorSpace": pdfName("DeviceRGB"),
+ "BitsPerComponent": 8,
+ "Interpolate": true,
+ "Filter": filter,
+ }
+
+ if hasMask {
+ dict["SMask"] = w.pdf.writeObject(pdfStream{
+ dict: pdfDict{
+ "Type": pdfName("XObject"),
+ "Subtype": pdfName("Image"),
+ "Width": size.X,
+ "Height": size.Y,
+ "ColorSpace": pdfName("DeviceGray"),
+ "BitsPerComponent": 8,
+ "Interpolate": true,
+ "Filter": pdfFilterFlate,
+ },
+ stream: streamMask,
+ })
+ }
+
+ ref := w.pdf.writeObject(pdfStream{
+ dict: dict,
+ stream: stream,
+ })
+ w.pdf.images[img] = ref
+ return ref
+}
diff --git a/paint/pdf/paint.go b/paint/pdf/paint.go
new file mode 100644
index 0000000000..29281bacd2
--- /dev/null
+++ b/paint/pdf/paint.go
@@ -0,0 +1,415 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This is adapted from https://github.com/tdewolff/canvas
+// Copyright (c) 2015 Taco de Wolff, under an MIT License.
+
+package pdf
+
+import (
+ "fmt"
+ "image"
+ "image/color"
+ "log/slog"
+ "reflect"
+
+ "cogentcore.org/core/colors"
+ "cogentcore.org/core/colors/gradient"
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/paint/ppath"
+ "cogentcore.org/core/styles"
+)
+
+// Path renders a path to the canvas using a style and an
+// individual matrix (needed for fill)
+func (r *PDF) Path(path ppath.Path, style *styles.Paint, tr math32.Matrix2) {
+ // PDFs don't support the arcs joiner, miter joiner (not clipped),
+ // or miter joiner (clipped) with non-bevel fallback
+ strokeUnsupported := false
+ if style.Stroke.Join == ppath.JoinArcs {
+ strokeUnsupported = true
+ } else if style.Stroke.Join == ppath.JoinMiter {
+ if style.Stroke.MiterLimit == 0 {
+ strokeUnsupported = true
+ }
+ // } else if _, ok := miter.GapJoiner.(canvas.BevelJoiner); !ok {
+ // strokeUnsupported = true
+ // }
+ }
+ scale := math32.Sqrt(math32.Abs(tr.Det()))
+ stk := style.Stroke
+ stk.Width.Dots *= scale
+ stk.DashOffset, stk.Dashes = ppath.ScaleDash(scale, stk.DashOffset, stk.Dashes)
+
+ // PDFs don't support connecting first and last dashes if path is closed,
+ // so we move the start of the path if this is the case
+ // TODO: closing dashes
+ //if style.DashesClose {
+ // strokeUnsupported = true
+ //}
+
+ closed := false
+ trpath := path.Clone().Transform(tr)
+ data := trpath.ToPDF()
+ if 1 < len(data) && data[len(data)-1] == 'h' {
+ data = data[:len(data)-2]
+ closed = true
+ }
+
+ var bounds math32.Box2
+ if style.HasFill() {
+ if _, ok := style.Fill.Color.(gradient.Gradient); ok {
+ fbox := trpath.FastBounds()
+ cum := r.Cumulative()
+ bounds = fbox.MulMatrix2(cum)
+ }
+ }
+
+ if style.HasStroke() && strokeUnsupported {
+ // todo: handle with optional inclusion of stroke function as _ import
+ /* // style.HasStroke() && strokeUnsupported
+ if style.HasFill() {
+ r.w.SetFill(style.Fill)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ r.w.Write([]byte(" f"))
+ if style.Fill.Rule == canvas.EvenOdd {
+ r.w.Write([]byte("*"))
+ }
+ }
+
+ // stroke settings unsupported by PDF, draw stroke explicitly
+ if style.IsDashed() {
+ path = path.Dash(style.DashOffset, style.Dashes...)
+ }
+ path = path.Stroke(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.Tolerance)
+
+ r.w.SetFill(style.Stroke)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(path.Transform(m).ToPDF()))
+ r.w.Write([]byte(" f"))
+ */
+ // return
+ }
+ if style.HasFill() && !style.HasStroke() {
+ r.w.SetFill(&style.Fill, bounds, tr)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ r.w.Write([]byte(" f"))
+ if style.Fill.Rule == ppath.EvenOdd {
+ r.w.Write([]byte("*"))
+ }
+ } else if !style.HasFill() && style.HasStroke() {
+ r.w.SetStroke(&stk)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ if closed {
+ r.w.Write([]byte(" s"))
+ } else {
+ r.w.Write([]byte(" S"))
+ }
+ } else if style.HasFill() && style.HasStroke() {
+ r.w.SetFill(&style.Fill, bounds, tr)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ r.w.Write([]byte(" f"))
+ if style.Fill.Rule == ppath.EvenOdd {
+ r.w.Write([]byte("*"))
+ }
+
+ r.w.SetStroke(&stk)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ if closed {
+ r.w.Write([]byte(" s"))
+ } else {
+ r.w.Write([]byte(" S"))
+ }
+ }
+}
+
+// SetFill sets the fill style values where different from current.
+// The bounds and matrix are required for gradient fills: pass identity
+// if not available.
+func (w *pdfPage) SetFill(fill *styles.Fill, bounds math32.Box2, m math32.Matrix2) {
+ csty := w.style()
+ if csty.Fill.Color != fill.Color || csty.Fill.Opacity != fill.Opacity {
+ w.SetFillColor(fill, bounds, m)
+ }
+ csty.Fill = *fill
+}
+
+// SetAlpha sets the transparency value.
+func (w *pdfPage) SetAlpha(alpha float32) {
+ gs := w.getOpacityGS(alpha)
+ fmt.Fprintf(w, " /%v gs", gs)
+}
+
+func alphaNorm(c uint8, a float32) dec {
+ if a == 0 {
+ return dec(0)
+ }
+ return dec(float32(c) / 255.0 / a)
+}
+
+func alphaNormColor(c color.RGBA, a float32) [3]dec {
+ var v [3]dec
+ v[0] = alphaNorm(c.R, a)
+ v[1] = alphaNorm(c.G, a)
+ v[2] = alphaNorm(c.B, a)
+ return v
+}
+
+// SetFillColor sets the filling color (image).
+func (w *pdfPage) SetFillColor(fill *styles.Fill, bounds math32.Box2, m math32.Matrix2) {
+ switch x := fill.Color.(type) {
+ // todo: image
+ case *gradient.Linear:
+ fmt.Fprintf(w, " /Pattern cs /%v scn", w.gradientPattern(x, bounds, m))
+ case *gradient.Radial:
+ fmt.Fprintf(w, " /Pattern cs /%v scn", w.gradientPattern(x, bounds, m))
+ case *image.Uniform:
+ var clr color.RGBA
+ if x != nil {
+ clr = colors.ApplyOpacity(colors.AsRGBA(x), fill.Opacity)
+ }
+ a := float32(clr.A) / 255.0
+ if a == 0 {
+ fmt.Fprintf(w, " 0 g")
+ } else if clr.R == clr.G && clr.R == clr.B {
+ fmt.Fprintf(w, " %v g", alphaNorm(clr.R, a))
+ } else {
+ v := alphaNormColor(clr, a)
+ fmt.Fprintf(w, " %v %v %v rg", v[0], v[1], v[2])
+ }
+ w.SetAlpha(a)
+ }
+}
+
+// SetStroke sets the stroke style values where different from current.
+func (w *pdfPage) SetStroke(stroke *styles.Stroke) {
+ csty := w.style()
+ if csty.Stroke.Color != stroke.Color || csty.Stroke.Opacity != stroke.Opacity {
+ w.SetStrokeColor(stroke)
+ }
+ if csty.Stroke.Width.Dots != stroke.Width.Dots {
+ w.SetStrokeWidth(stroke.Width.Dots)
+ }
+ if csty.Stroke.Cap != stroke.Cap {
+ w.SetStrokeCap(stroke.Cap)
+ }
+ if csty.Stroke.Join != stroke.Join || (csty.Stroke.Join == ppath.JoinMiter && csty.Stroke.MiterLimit != stroke.MiterLimit) {
+ w.SetStrokeJoin(stroke.Join, stroke.MiterLimit)
+ }
+ if len(stroke.Dashes) > 0 { // always do
+ w.SetDashes(stroke.DashOffset, stroke.Dashes)
+ } else {
+ if len(csty.Stroke.Dashes) > 0 {
+ w.SetDashes(0, nil)
+ }
+ }
+ csty.Stroke = *stroke
+}
+
+// SetStrokeColor sets the stroking color (image).
+func (w *pdfPage) SetStrokeColor(stroke *styles.Stroke) {
+ switch x := stroke.Color.(type) {
+ case *gradient.Linear:
+ case *gradient.Radial:
+ // TODO: should we unset cs?
+ // fmt.Fprintf(w, " /Pattern cs /%v scn", w.gradientPattern(stroke.Gradient))
+ case *image.Uniform:
+ clr := colors.ApplyOpacity(colors.AsRGBA(x), stroke.Opacity)
+ a := float32(clr.A) / 255.0
+ if clr.R == clr.G && clr.R == clr.B {
+ fmt.Fprintf(w, " %v G", alphaNorm(clr.R, a))
+ } else {
+ v := alphaNormColor(clr, a)
+ fmt.Fprintf(w, " %v %v %v RG", v[0], v[1], v[2])
+ }
+ w.SetAlpha(a)
+ }
+}
+
+// SetStrokeWidth sets the stroke width.
+func (w *pdfPage) SetStrokeWidth(lineWidth float32) {
+ fmt.Fprintf(w, " %v w", dec(lineWidth))
+}
+
+// SetStrokeCap sets the stroke cap type.
+func (w *pdfPage) SetStrokeCap(capper ppath.Caps) {
+ var lineCap int
+ switch capper {
+ case ppath.CapButt:
+ lineCap = 0
+ case ppath.CapRound:
+ lineCap = 1
+ case ppath.CapSquare:
+ lineCap = 2
+ default:
+ slog.Error("pdfWriter", "StrokeCap not supported", capper)
+ }
+ fmt.Fprintf(w, " %d J", lineCap)
+}
+
+// SetStrokeJoin sets the stroke join type.
+func (w *pdfPage) SetStrokeJoin(joiner ppath.Joins, miterLimit float32) {
+ var lineJoin int
+ switch joiner {
+ case ppath.JoinBevel:
+ lineJoin = 2
+ case ppath.JoinRound:
+ lineJoin = 1
+ case ppath.JoinMiter:
+ lineJoin = 0
+ default:
+ slog.Error("pdfWriter", "StrokeJoin not supported", joiner)
+ }
+ fmt.Fprintf(w, " %d j", lineJoin)
+ if lineJoin == 0 {
+ fmt.Fprintf(w, " %v M", dec(miterLimit))
+ }
+}
+
+// SetDashes sets the dash phase and array.
+func (w *pdfPage) SetDashes(dashPhase float32, dashArray []float32) {
+ if len(dashArray)%2 == 1 {
+ dashArray = append(dashArray, dashArray...)
+ }
+
+ // PDF can't handle negative dash phases
+ if dashPhase < 0.0 {
+ totalLength := float32(0.0)
+ for _, dash := range dashArray {
+ totalLength += dash
+ }
+ for dashPhase < 0.0 {
+ dashPhase += totalLength
+ }
+ }
+
+ dashes := append(dashArray, dashPhase)
+ if len(dashes) == 1 {
+ fmt.Fprintf(w, " [] 0 d")
+ dashes[0] = 0.0
+ } else {
+ fmt.Fprintf(w, " [%v", dec(dashes[0]))
+ for _, dash := range dashes[1 : len(dashes)-1] {
+ fmt.Fprintf(w, " %v", dec(dash))
+ }
+ fmt.Fprintf(w, "] %v d", dec(dashes[len(dashes)-1]))
+ }
+}
+
+func (w *pdfPage) getOpacityGS(a float32) pdfName {
+ if name, ok := w.graphicsStates[a]; ok {
+ return name
+ }
+ name := pdfName(fmt.Sprintf("A%d", len(w.graphicsStates)))
+ w.graphicsStates[a] = name
+
+ if _, ok := w.resources["ExtGState"]; !ok {
+ w.resources["ExtGState"] = pdfDict{}
+ }
+ w.resources["ExtGState"].(pdfDict)[name] = pdfDict{
+ "CA": a,
+ "ca": a,
+ }
+ return name
+}
+
+func (w *pdfPage) gradientPattern(gr gradient.Gradient, bounds math32.Box2, m math32.Matrix2) pdfName {
+ cum := w.Cumulative().Mul(m)
+ gr.Update(1, bounds, cum)
+ // TODO: support patterns/gradients with alpha channel
+ shading := pdfDict{
+ "ColorSpace": pdfName("DeviceRGB"),
+ }
+ switch g := gr.(type) {
+ case *gradient.Linear:
+ s, e := g.TransformedAxis()
+ shading["ShadingType"] = 2
+ shading["Coords"] = pdfArray{s.X, s.Y, e.X, e.Y}
+ shading["Function"] = patternStopsFunction(g.Stops)
+ shading["Extend"] = pdfArray{true, true}
+ case *gradient.Radial:
+ c, f, rad := g.TransformedCoords()
+ // r := 0.5 * (math32.Abs(rad.X) + math32.Abs(rad.Y))
+ r := max(math32.Abs(rad.X), math32.Abs(rad.Y))
+ // fmt.Println("c:", c, "ctr:", g.Center)
+ shading["ShadingType"] = 3
+ shading["Coords"] = pdfArray{f.X, f.Y, 0, c.X, c.Y, r}
+ shading["Function"] = patternStopsFunction(g.Stops)
+ shading["Extend"] = pdfArray{true, true}
+ }
+ pattern := pdfDict{
+ "Type": pdfName("Pattern"),
+ "PatternType": 2,
+ "Shading": shading,
+ }
+
+ if _, ok := w.resources["Pattern"]; !ok {
+ w.resources["Pattern"] = pdfDict{}
+ }
+ for name, pat := range w.resources["Pattern"].(pdfDict) {
+ if reflect.DeepEqual(pat, pattern) {
+ return name
+ }
+ }
+ name := pdfName(fmt.Sprintf("P%d", len(w.resources["Pattern"].(pdfDict))))
+ w.resources["Pattern"].(pdfDict)[name] = pattern
+ return name
+}
+
+func patternStopsFunction(stops []gradient.Stop) pdfDict {
+ n := len(stops)
+ if len(stops) < 2 {
+ return pdfDict{}
+ }
+
+ fs := pdfArray{}
+ encode := pdfArray{}
+ bounds := pdfArray{}
+ if !ppath.Equal(stops[0].Pos, 0.0) {
+ fs = append(fs, patternStopFunction(stops[0], stops[0]))
+ encode = append(encode, 0, 1)
+ bounds = append(bounds, stops[0].Pos)
+ }
+ for i := range n - 1 {
+ fs = append(fs, patternStopFunction(stops[i], stops[i+1]))
+ encode = append(encode, 0, 1)
+ if i != 0 {
+ bounds = append(bounds, stops[1].Pos)
+ }
+ }
+ if !ppath.Equal(stops[n-1].Pos, 1.0) {
+ fs = append(fs, patternStopFunction(stops[n-1], stops[n-1]))
+ encode = append(encode, 0, 1)
+ bounds = append(bounds, stops[n-1].Pos)
+ }
+ if len(fs) == 1 {
+ return fs[0].(pdfDict)
+ }
+ return pdfDict{
+ "FunctionType": 3,
+ "Domain": pdfArray{0, 1},
+ "Encode": encode,
+ "Bounds": bounds,
+ "Functions": fs,
+ }
+}
+
+func patternStopFunction(s0, s1 gradient.Stop) pdfDict {
+ a0 := float32(s0.Color.A) / 255.0
+ a1 := float32(s1.Color.A) / 255.0
+ c0 := alphaNormColor(s0.Color, a0)
+ c1 := alphaNormColor(s1.Color, a1)
+ return pdfDict{
+ "FunctionType": 2,
+ "Domain": pdfArray{0, 1},
+ "N": 1,
+ "C0": pdfArray{c0[0], c0[1], c0[2]},
+ "C1": pdfArray{c1[0], c1[1], c1[2]},
+ }
+}
diff --git a/paint/pdf/pdf.go b/paint/pdf/pdf.go
new file mode 100644
index 0000000000..50528fcc71
--- /dev/null
+++ b/paint/pdf/pdf.go
@@ -0,0 +1,181 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This is adapted from https://github.com/tdewolff/canvas
+// Copyright (c) 2015 Taco de Wolff, under an MIT License.
+
+package pdf
+
+import (
+ "image"
+ "io"
+
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/styles/units"
+ "cogentcore.org/core/text/rich"
+)
+
+// todo:
+// * needs to include the basic page xform which we neeed to install on pdf rendering stack.
+// if passed bounds are empty, use path bounds.
+// - compute cumulative from stack, don't pass -- only pass local
+// - add a convenience method with empty bounds and ID transform, for text cases?
+
+// UseStandardFonts sets the [rich.Settings] default fonts to the
+// corresponding PDF defaults, so that text layout works correctly
+// for the PDF rendering. The current settings are returned,
+// and should be passed to [RestorePreviousFonts] when done.
+func UseStandardFonts() rich.SettingsData {
+ prev := rich.Settings
+ rich.Settings.SansSerif = "Helvetica"
+ rich.Settings.Serif = "Times"
+ rich.Settings.Monospace = "Courier"
+ return prev
+}
+
+// RestorePreviousFonts sets the [rich.Settings] default fonts
+// to those returned from [UseStandardFonts]
+func RestorePreviousFonts(s rich.SettingsData) {
+ rich.Settings = s
+}
+
+// type Options struct {
+// Compress bool
+// SubsetFonts bool
+// canvas.ImageEncoding
+// }
+//
+// var DefaultOptions = Options{
+// Compress: true,
+// SubsetFonts: true,
+// ImageEncoding: canvas.Lossless,
+// }
+
+// PDF is a portable document format renderer.
+type PDF struct {
+ w *pdfPage
+ width, height float32
+ // opts *Options
+}
+
+// New returns a portable document format (PDF) renderer.
+// The size is in points.
+func New(w io.Writer, width, height float32, un *units.Context) *PDF {
+ // if opts == nil {
+ // defaultOptions := DefaultOptions
+ // opts = &defaultOptions
+ // }
+
+ page := newPDFWriter(w, un).NewPage(width, height)
+ // page.pdf.SetCompression(opts.Compress)
+ // page.pdf.SetFontSubsetting(opts.SubsetFonts)
+ return &PDF{
+ w: page,
+ width: width,
+ height: height,
+ // opts: opts,
+ }
+}
+
+// SetImageEncoding sets the image encoding to Loss or Lossless.
+// func (r *PDF) SetImageEncoding(enc canvas.ImageEncoding) {
+// r.opts.ImageEncoding = enc
+// }
+
+// SetInfo sets the document's title, subject, keywords, author and creator.
+func (r *PDF) SetInfo(title, subject, keywords, author, creator string) {
+ r.w.pdf.SetTitle(title)
+ r.w.pdf.SetSubject(subject)
+ r.w.pdf.SetKeywords(keywords)
+ r.w.pdf.SetAuthor(author)
+ r.w.pdf.SetCreator(creator)
+}
+
+// SetLang sets the document's language. It must adhere the RFC 3066 specification on Language-Tag, eg. es-CL.
+func (r *PDF) SetLang(lang string) {
+ r.w.pdf.SetLang(lang)
+}
+
+// NewPage starts adds a new page where further rendering will be written to.
+func (r *PDF) NewPage(width, height float32) {
+ r.w = r.w.pdf.NewPage(width, height)
+}
+
+// AddLink adds a link to the PDF document.
+func (r *PDF) AddLink(uri string, rect math32.Box2) {
+ r.w.AddLink(uri, rect)
+}
+
+// Close finished and closes the PDF.
+func (r *PDF) Close() error {
+ return r.w.pdf.Close()
+}
+
+// Size returns the size of the canvas in millimeters.
+func (r *PDF) Size() (float32, float32) {
+ return r.width, r.height
+}
+
+// AddLayer defines a layer that can be shown or hidden when the document is
+// displayed. name specifies the layer name that the document reader will
+// display in the layer list. visible specifies whether the layer will be
+// initially visible. The return value is an integer ID that is used in a call
+// to BeginLayer().
+func (r *PDF) AddLayer(name string, visible bool) (layerID int) {
+ return r.w.pdf.AddLayer(name, visible)
+}
+
+// BeginLayer is called to begin adding content to the specified layer.
+// All content added to the page between a call to BeginLayer and a call to
+// EndLayer is added to the layer specified by id. See AddLayer for more
+// details.
+func (r *PDF) BeginLayer(id int) {
+ r.w.BeginLayer(id)
+}
+
+// EndLayer is called to stop adding content to the currently active layer.
+// See BeginLayer for more details.
+func (r *PDF) EndLayer() {
+ r.w.EndLayer()
+}
+
+// PushStack adds a graphics stack push, which must
+// be paired with a corresponding Pop.
+func (r *PDF) PushStack() {
+ r.w.PushStack()
+}
+
+// PopStack adds a graphics stack pop which must
+// be paired with a corresponding Push.
+func (r *PDF) PopStack() {
+ r.w.PopStack()
+}
+
+// SetTransform adds a cm to set the current matrix transform (CMT).
+func (r *PDF) SetTransform(m math32.Matrix2) {
+ r.w.SetTransform(m)
+}
+
+// PushTransform adds a graphics stack push (q) and then
+// cm to set the current matrix transform (CMT).
+func (r *PDF) PushTransform(m math32.Matrix2) {
+ r.PushStack()
+ r.SetTransform(m)
+}
+
+// Cumulative returns the current cumulative transform.
+func (r *PDF) Cumulative() math32.Matrix2 {
+ return r.w.Cumulative()
+}
+
+// Image renders an image to the canvas using a transformation matrix.
+func (r *PDF) Image(img image.Image, m math32.Matrix2) {
+ r.w.DrawImage(img, m)
+}
+
+// AddAnchor adds a uniquely-named link anchor location,
+// which can then be a target for links.
+func (r *PDF) AddAnchor(name string, pos math32.Vector2) {
+ r.w.AddAnchor(name, pos)
+}
diff --git a/paint/pdf/pdf_test.go b/paint/pdf/pdf_test.go
new file mode 100644
index 0000000000..0999b5d93e
--- /dev/null
+++ b/paint/pdf/pdf_test.go
@@ -0,0 +1,231 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This is adapted from https://github.com/tdewolff/canvas
+// Copyright (c) 2015 Taco de Wolff, under an MIT License.
+
+package pdf
+
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "cogentcore.org/core/colors"
+ "cogentcore.org/core/colors/gradient"
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/paint/ppath"
+ "cogentcore.org/core/styles"
+ "cogentcore.org/core/styles/units"
+ "cogentcore.org/core/text/htmltext"
+ "cogentcore.org/core/text/rich"
+ "cogentcore.org/core/text/shaped"
+ _ "cogentcore.org/core/text/shaped/shapers"
+ _ "cogentcore.org/core/text/tex"
+ "github.com/alecthomas/assert/v2"
+)
+
+// RunTest runs a test for given test case.
+func RunTest(t *testing.T, nm string, width, height float32, f func(pd *PDF, sty *styles.Paint)) {
+ ctx := units.NewContext()
+ ctx.DPI = 72
+ var b bytes.Buffer
+ pd := New(&b, width, height, ctx)
+ sty := styles.NewPaint()
+ sty.UnitContext = *ctx
+ f(pd, sty)
+ pd.Close()
+ os.Mkdir("testdata", 0777)
+ os.WriteFile(filepath.Join("testdata", nm)+".pdf", b.Bytes(), 0666)
+}
+
+func TestPath(t *testing.T) {
+ RunTest(t, "path", 50, 50, func(pd *PDF, sty *styles.Paint) {
+ p := ppath.New().Rectangle(0, 0, 30, 20)
+
+ sty.Stroke.Color = colors.Uniform(colors.Blue)
+ sty.Fill.Color = colors.Uniform(colors.Red)
+ sty.Stroke.Width.Px(2)
+ sty.ToDots()
+
+ tr := math32.Translate2D(10, 20)
+ pd.Path(*p, sty, tr)
+ })
+}
+
+func TestGradientLinear(t *testing.T) {
+ RunTest(t, "gradient-linear", 50, 50, func(pd *PDF, sty *styles.Paint) {
+ pd.PushTransform(math32.Translate2D(10, 5).Scale(.5, .5))
+ p := ppath.New().Rectangle(0, 0, 30, 20)
+ gg := gradient.NewLinear().AddStop(colors.White, 0).AddStop(colors.Red, 1)
+ gg.Start.Set(0, 10)
+ gg.End.Set(30, 10)
+ gg.Units = gradient.UserSpaceOnUse
+ sty.Stroke.Color = colors.Uniform(colors.Blue)
+ sty.Fill.Color = gg
+ sty.Stroke.Width.Px(2)
+ sty.ToDots()
+
+ tr := math32.Translate2D(10, 20)
+ pd.Path(*p, sty, tr)
+ pd.PopStack()
+ })
+}
+
+func TestGradientRadial(t *testing.T) {
+ RunTest(t, "gradient-radial", 50, 50, func(pd *PDF, sty *styles.Paint) {
+ p := ppath.New().Rectangle(0, 0, 30, 20)
+ // todo: this is a different definition than ours..
+ gg := gradient.NewRadial().AddStop(colors.White, 0).AddStop(colors.Red, 1)
+ gg.Units = gradient.UserSpaceOnUse
+ gg.Center.Set(15, 10)
+ gg.Focal = gg.Center
+ gg.Radius.Set(15, 5)
+ sty.Stroke.Color = colors.Uniform(colors.Blue)
+ sty.Fill.Color = gg
+ sty.Stroke.Width.Px(2)
+ sty.ToDots()
+
+ tr := math32.Translate2D(10, 20)
+ pd.Path(*p, sty, tr)
+ })
+}
+
+func TestText(t *testing.T) {
+ RunTest(t, "text", 300, 300, func(pd *PDF, sty *styles.Paint) {
+ prv := UseStandardFonts()
+ sh := shaped.NewShaper()
+
+ src := "PDF can put HTML formatted Text where you want"
+ rsty := &sty.Font
+ tsty := &sty.Text
+
+ tx, err := htmltext.HTMLToRich([]byte(src), rsty, nil)
+ // fmt.Println(tx)
+ assert.NoError(t, err)
+ lns := sh.WrapLines(tx, rsty, tsty, math32.Vec2(250, 250))
+
+ // m := math32.Identity2()
+ m := math32.Rotate2D(math32.DegToRad(15))
+
+ pd.Text(sty, m, math32.Vec2(20, 20), lns)
+ RestorePreviousFonts(prv)
+ })
+}
+
+func TestMathInline(t *testing.T) {
+ RunTest(t, "math-inline", 300, 300, func(pd *PDF, sty *styles.Paint) {
+ prv := UseStandardFonts()
+ sh := shaped.NewShaper()
+
+ src := `y = \frac{1}{N} \left( \sum_{i=0}^{100} \frac{f(x^2)}{\sum x^2} \right)`
+ rsty := &sty.Font
+ tsty := &sty.Text
+
+ tx := rich.NewText(rsty, []rune("math: "))
+ tx.AddMathInline(rsty, src)
+ tx.AddSpan(rsty, []rune(" and we should check line wrapping too"))
+ lns := sh.WrapLines(tx, rsty, tsty, math32.Vec2(250, 250))
+
+ m := math32.Identity2()
+ pd.Text(sty, m, math32.Vec2(20, 20), lns)
+ RestorePreviousFonts(prv)
+ })
+}
+
+func TestMathDisplay(t *testing.T) {
+ RunTest(t, "math-display", 300, 300, func(pd *PDF, sty *styles.Paint) {
+ prv := UseStandardFonts()
+ sh := shaped.NewShaper()
+
+ src := `y = \frac{1}{N} \left( \sum_{i=0}^{100} \frac{f(x^2)}{\sum x^2} \right)`
+ rsty := &sty.Font
+ tsty := &sty.Text
+
+ var tx rich.Text
+ tx.AddMathDisplay(rsty, src)
+ lns := sh.WrapLines(tx, rsty, tsty, math32.Vec2(250, 250))
+
+ m := math32.Identity2()
+ pd.Text(sty, m, math32.Vec2(20, 20), lns)
+ RestorePreviousFonts(prv)
+ })
+}
+
+func TestLinks(t *testing.T) {
+ RunTest(t, "links", 300, 300, func(pd *PDF, sty *styles.Paint) {
+ prv := UseStandardFonts()
+ sh := shaped.NewShaper()
+ m := math32.Identity2()
+ rsty := &sty.Font
+ tsty := &sty.Text
+ sz := math32.Vec2(250, 250)
+
+ txt := func(src string, pos math32.Vector2) {
+ tx, err := htmltext.HTMLToRich([]byte(src), rsty, nil)
+ assert.NoError(t, err)
+ lns := sh.WrapLines(tx, rsty, tsty, sz)
+ pd.Text(sty, m, pos, lns)
+ }
+ txt("Some random text here", math32.Vec2(10, 10))
+ pd.w.AddAnchor("test", math32.Vec2(10, 10))
+ txt(`A link to that text`, math32.Vec2(10, 30))
+
+ RestorePreviousFonts(prv)
+ })
+}
+
+func TestOutline(t *testing.T) {
+ RunTest(t, "outline", 300, 300, func(pd *PDF, sty *styles.Paint) {
+ prv := UseStandardFonts()
+ sh := shaped.NewShaper()
+ m := math32.Identity2()
+ rsty := &sty.Font
+ tsty := &sty.Text
+ sz := math32.Vec2(250, 250)
+
+ txt := func(src string, pos math32.Vector2) {
+ tx, err := htmltext.HTMLToRich([]byte(src), rsty, nil)
+ assert.NoError(t, err)
+ lns := sh.WrapLines(tx, rsty, tsty, sz)
+ pd.Text(sty, m, pos, lns)
+ }
+ txt("Heading 1", math32.Vec2(10, 10))
+ pd.w.AddOutline("Heading 1", 1, 10)
+ txt("Sub Heading", math32.Vec2(10, 30))
+ pd.w.AddOutline("Sub Heading 1", 2, 30)
+ txt("Another Sub Heading", math32.Vec2(10, 50))
+ pd.w.AddOutline("Sub Heading 2", 2, 50)
+ txt("Heading 2", math32.Vec2(10, 70))
+ pd.w.AddOutline("Heading 2", 1, 70)
+
+ RestorePreviousFonts(prv)
+ })
+}
+
+func TestLayers(t *testing.T) {
+ RunTest(t, "layers", 300, 300, func(pd *PDF, sty *styles.Paint) {
+ prv := UseStandardFonts()
+ sh := shaped.NewShaper()
+
+ src := "PDF can put HTML formatted Text where you want"
+ rsty := &sty.Font
+ tsty := &sty.Text
+
+ tx, err := htmltext.HTMLToRich([]byte(src), rsty, nil)
+ // fmt.Println(tx)
+ assert.NoError(t, err)
+ lns := sh.WrapLines(tx, rsty, tsty, math32.Vec2(250, 250))
+
+ // mi := math32.Identity2()
+ mr := math32.Rotate2D(math32.DegToRad(15))
+
+ g := pd.AddLayer("first layer", true)
+ pd.BeginLayer(g)
+ pd.Text(sty, mr, math32.Vec2(20, 20), lns)
+ pd.EndLayer()
+ RestorePreviousFonts(prv)
+ })
+}
diff --git a/paint/pdf/text.go b/paint/pdf/text.go
new file mode 100644
index 0000000000..a2fe929299
--- /dev/null
+++ b/paint/pdf/text.go
@@ -0,0 +1,390 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This is adapted from https://github.com/tdewolff/canvas
+// Copyright (c) 2015 Taco de Wolff, under an MIT License.
+
+package pdf
+
+import (
+ "fmt"
+ "image"
+ "strconv"
+ "strings"
+
+ "cogentcore.org/core/base/errors"
+ "cogentcore.org/core/colors"
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/paint/ppath"
+ "cogentcore.org/core/styles"
+ "cogentcore.org/core/text/rich"
+ "cogentcore.org/core/text/shaped"
+ "cogentcore.org/core/text/shaped/shapers/shapedgt"
+ "cogentcore.org/core/text/text"
+ "cogentcore.org/core/text/textpos"
+ "golang.org/x/text/encoding/charmap"
+)
+
+// Text renders text to the canvas using a transformation matrix,
+// (the translation component specifies the starting offset)
+func (r *PDF) Text(style *styles.Paint, m math32.Matrix2, pos math32.Vector2, lns *shaped.Lines) {
+ if lns.Anchor != "" {
+ anc := lns.Anchor
+ hidx := strings.Index(anc, ";Header ")
+ if hidx >= 0 {
+ if hidx > 0 {
+ r.w.AddAnchor(anc[:hidx], pos)
+ anc = anc[hidx:]
+ }
+ anc = anc[8:]
+ level := errors.Log1(strconv.Atoi(anc[0:1]))
+ anc = anc[2:]
+ r.w.AddOutline(anc, level, pos.Y)
+ } else {
+ r.w.AddAnchor(lns.Anchor, pos)
+ }
+ }
+ mt := m.Mul(math32.Translate2D(pos.X, pos.Y))
+ r.w.PushTransform(mt)
+ off := lns.Offset
+ clr := colors.Uniform(lns.Color)
+ runes := lns.Source.Join()
+ for li := range lns.Lines {
+ ln := &lns.Lines[li]
+ r.textLine(style, m, li, ln, lns, runes, clr, off)
+ }
+ r.links(lns, m, pos)
+ r.w.PopStack()
+}
+
+// TextLine rasterizes the given shaped.Line.
+func (r *PDF) textLine(style *styles.Paint, m math32.Matrix2, li int, ln *shaped.Line, lns *shaped.Lines, runes []rune, clr image.Image, off math32.Vector2) {
+ start := off.Add(ln.Offset)
+ off = start
+ for ri := range ln.Runs {
+ run := ln.Runs[ri].(*shapedgt.Run)
+ r.textRunRegions(m, run, ln, lns, off)
+ if run.Direction.IsVertical() {
+ off.Y += run.Advance()
+ } else {
+ off.X += run.Advance()
+ }
+ }
+ off = start
+ for ri := range ln.Runs {
+ run := ln.Runs[ri].(*shapedgt.Run)
+ r.textRun(style, m, run, ln, lns, runes, clr, off)
+ if run.Direction.IsVertical() {
+ off.Y += run.Advance()
+ } else {
+ off.X += run.Advance()
+ }
+ }
+}
+
+// textRegionFill fills given regions within run with given fill color.
+func (r *PDF) textRegionFill(m math32.Matrix2, run *shapedgt.Run, off math32.Vector2, fill image.Image, ranges []textpos.Range) {
+ if fill == nil {
+ return
+ }
+ idm := math32.Identity2()
+ for _, sel := range ranges {
+ rsel := sel.Intersect(run.Runes())
+ if rsel.Len() == 0 {
+ continue
+ }
+ fi := run.FirstGlyphAt(rsel.Start)
+ li := run.LastGlyphAt(rsel.End - 1)
+ if fi >= 0 && li >= fi {
+ sbb := run.GlyphRegionBounds(fi, li).Canon()
+ r.FillBox(idm, sbb.Translate(off), fill)
+ }
+ }
+}
+
+// textRunRegions draws region fills for given run.
+func (r *PDF) textRunRegions(m math32.Matrix2, run *shapedgt.Run, ln *shaped.Line, lns *shaped.Lines, off math32.Vector2) {
+ idm := math32.Identity2()
+
+ // dir := run.Direction
+ rbb := run.MaxBounds.Translate(off)
+ if run.Background != nil {
+ r.FillBox(idm, rbb, run.Background)
+ }
+ r.textRegionFill(m, run, off, lns.SelectionColor, ln.Selections)
+ r.textRegionFill(m, run, off, lns.HighlightColor, ln.Highlights)
+}
+
+// textRun rasterizes the given text run into the output image using the
+// font face set in the shaping.
+// The text will be drawn starting at the start pixel position.
+func (r *PDF) textRun(style *styles.Paint, m math32.Matrix2, run *shapedgt.Run, ln *shaped.Line, lns *shaped.Lines, runes []rune, clr image.Image, off math32.Vector2) {
+ // dir := run.Direction
+ region := run.Runes()
+ offTrans := math32.Translate2D(off.X, off.Y)
+ rbb := run.MaxBounds.Translate(off)
+ fill := clr
+ if run.FillColor != nil {
+ fill = run.FillColor
+ }
+ fsz := math32.FromFixed(run.Size)
+ lineW := max(fsz/16, 1) // 1 at 16, bigger if biggerr
+ if run.Math.Path != nil {
+ r.w.PushTransform(offTrans)
+ psty := *style
+ psty.Stroke.Color = run.StrokeColor
+ psty.Fill.Color = fill
+ r.Path(*run.Math.Path, &psty, math32.Identity2())
+ r.w.PopStack()
+ return
+ }
+
+ idm := math32.Identity2()
+ if run.Decoration.HasFlag(rich.Underline) || run.Decoration.HasFlag(rich.DottedUnderline) {
+ dash := []float32{2, 2}
+ if run.Decoration.HasFlag(rich.Underline) {
+ dash = nil
+ }
+ if run.Direction.IsVertical() {
+ } else {
+ dec := off.Y + 3
+ r.strokeTextLine(idm, math32.Vec2(rbb.Min.X, dec), math32.Vec2(rbb.Max.X, dec), lineW, fill, dash)
+ }
+ }
+ if run.Decoration.HasFlag(rich.Overline) {
+ if run.Direction.IsVertical() {
+ } else {
+ dec := off.Y - 0.7*rbb.Size().Y
+ r.strokeTextLine(idm, math32.Vec2(rbb.Min.X, dec), math32.Vec2(rbb.Max.X, dec), lineW, fill, nil)
+ }
+ }
+
+ r.w.StartTextObject(offTrans)
+ r.setTextStyle(&run.Font, style, fill, run.StrokeColor, math32.FromFixed(run.Size), lns.LineHeight)
+ raw := string(runes[region.Start:region.End])
+ r.w.WriteText(raw)
+ r.w.EndTextObject()
+
+ if run.Decoration.HasFlag(rich.LineThrough) {
+ if run.Direction.IsVertical() {
+ } else {
+ dec := off.Y - 0.2*rbb.Size().Y
+ r.strokeTextLine(idm, math32.Vec2(rbb.Min.X, dec), math32.Vec2(rbb.Max.X, dec), lineW, fill, nil)
+ }
+ }
+}
+
+func (r *PDF) setTextStrokeColor(clr image.Image) {
+ sc := r.w.style().Stroke
+ sc.Color = clr
+ r.w.SetStroke(&sc)
+}
+
+func (r *PDF) setTextFillColor(clr image.Image) {
+ fc := r.w.style().Fill
+ fc.Color = clr
+ r.w.SetFill(&fc, math32.Box2{}, math32.Identity2())
+}
+
+// setTextStyle applies the given styles.
+func (r *PDF) setTextStyle(fnt *text.Font, style *styles.Paint, fill, stroke image.Image, size, lineHeight float32) {
+ tsty := &style.Text
+ sty := fnt.Style(tsty)
+ r.w.SetFont(sty, tsty)
+ mode := 0
+ if stroke != nil {
+ r.setTextStrokeColor(stroke)
+ }
+ if fill != nil {
+ r.setTextFillColor(fill)
+ if stroke != nil {
+ mode = 2
+ }
+ } else {
+ if stroke != nil {
+ mode = 1
+ }
+ }
+ r.w.SetTextRenderMode(mode)
+}
+
+// strokeTextLine strokes a line for text decoration.
+func (r *PDF) strokeTextLine(m math32.Matrix2, sp, ep math32.Vector2, width float32, clr image.Image, dash []float32) {
+ sty := styles.NewPaint()
+ sty.Fill.Color = nil
+ sty.Stroke.Width.Dots = width
+ sty.Stroke.Color = clr
+ sty.Stroke.Dashes = dash
+ p := ppath.New().Line(sp.X, sp.Y, ep.X, ep.Y)
+ r.Path(*p, sty, m)
+}
+
+// FillBox fills a box in the given color.
+func (r *PDF) FillBox(m math32.Matrix2, bb math32.Box2, clr image.Image) {
+ sty := styles.NewPaint()
+ sty.Stroke.Color = nil
+ sty.Fill.Color = clr
+ sz := bb.Size()
+ p := ppath.New().Rectangle(bb.Min.X, bb.Min.Y, sz.X, sz.Y)
+ r.Path(*p, sty, m)
+}
+
+func (r *PDF) links(lns *shaped.Lines, m math32.Matrix2, pos math32.Vector2) {
+ lks := lns.GetLinks()
+ for _, lk := range lks {
+ // note: link coordinates are in default user space, not current transform.
+ srb := lns.RuneBounds(lk.Range.Start)
+ erb := lns.RuneBounds(lk.Range.End)
+ if erb.Max.X > srb.Max.X {
+ srb.Max.X = erb.Max.X
+ }
+ rb := srb.Translate(pos)
+ rb = rb.MulMatrix2(m)
+ r.w.AddLink(lk.URL, rb)
+ }
+}
+
+// SetFont sets the font.
+func (w *pdfPage) SetFont(sty *rich.Style, tsty *text.Style) error {
+ if !w.inTextObject {
+ return errors.Log(errors.New("pdfWriter: must be in text object"))
+ }
+ size := tsty.FontHeight(sty) // * w.pdf.globalScale
+ ref := w.pdf.getFont(sty, tsty)
+ if _, ok := w.resources["Font"]; !ok {
+ w.resources["Font"] = pdfDict{}
+ } else {
+ for name, fontRef := range w.resources["Font"].(pdfDict) {
+ if ref == fontRef {
+ fmt.Fprintf(w, " /%v %v Tf", name, dec(size))
+ return nil
+ }
+ }
+ }
+
+ name := pdfName(fmt.Sprintf("F%d", len(w.resources["Font"].(pdfDict))))
+ w.resources["Font"].(pdfDict)[name] = ref
+ fmt.Fprintf(w, " /%v %v Tf", name, dec(size))
+ return nil
+}
+
+// SetTextPosition sets the text offset position.
+func (w *pdfPage) SetTextPosition(off math32.Vector2) error {
+ if !w.inTextObject {
+ return errors.Log(errors.New("pdfWriter: must be in text object"))
+ }
+ do := off.Sub(w.textPosition)
+ // and finally apply an offset from there, in reverse for Y
+ fmt.Fprintf(w, " %v %v Td", dec(do.X), dec(-do.Y))
+ w.textPosition = off
+ return nil
+}
+
+// SetTextRenderMode sets the text rendering mode.
+// 0 = fill text, 1 = stroke text, 2 = fill, then stroke.
+// higher numbers support clip path.
+func (w *pdfPage) SetTextRenderMode(mode int) error {
+ if !w.inTextObject {
+ return errors.Log(errors.New("pdfWriter: must be in text object"))
+ }
+ fmt.Fprintf(w, " %d Tr", mode)
+ w.textRenderMode = mode
+ return nil
+}
+
+// SetTextCharSpace sets the text character spacing.
+func (w *pdfPage) SetTextCharSpace(space float32) error {
+ if !w.inTextObject {
+ return errors.Log(errors.New("pdfWriter: must be in text object"))
+ }
+ fmt.Fprintf(w, " %v Tc", dec(space))
+ w.textCharSpace = space
+ return nil
+}
+
+// StartTextObject starts a text object, adding to the graphics
+// CTM transform matrix as given by the arg, and setting an inverting
+// text transform, so text is rendered upright.
+func (w *pdfPage) StartTextObject(m math32.Matrix2) error {
+ if w.inTextObject {
+ return errors.Log(errors.New("pdfWriter: already in text object"))
+ }
+ // set the graphics transform to m first
+ w.PushTransform(m)
+ fmt.Fprintf(w, " BT")
+ // then apply an inversion text matrix
+ tm := math32.Scale2D(1, -1)
+ fmt.Fprintf(w, " %s Tm", mat2(tm))
+ w.inTextObject = true
+ w.textPosition = math32.Vector2{}
+ return nil
+}
+
+// EndTextObject ends a text object.
+func (w *pdfPage) EndTextObject() error {
+ if !w.inTextObject {
+ return errors.Log(errors.New("pdfWriter: must be in text object"))
+ }
+ fmt.Fprintf(w, " ET")
+ w.PopStack()
+ w.inTextObject = false
+ return nil
+}
+
+// WriteText writes text using current text style.
+func (w *pdfPage) WriteText(tx string) error {
+ if !w.inTextObject {
+ return errors.Log(errors.New("pdfWriter: must be in text object"))
+ }
+ if len(tx) == 0 {
+ return nil
+ }
+
+ first := true
+ write := func(s string) {
+ if first {
+ fmt.Fprintf(w, "(")
+ first = false
+ } else {
+ fmt.Fprintf(w, " (")
+ }
+ rs := []rune(s)
+ for _, r := range rs {
+ c, ok := charmap.Windows1252.EncodeRune(r)
+ if !ok {
+ if '\u2000' <= r && r <= '\u200A' {
+ c = ' '
+ }
+ }
+ switch c {
+ case '\n':
+ w.WriteByte('\\')
+ w.WriteByte('n')
+ case '\r':
+ w.WriteByte('\\')
+ w.WriteByte('r')
+ case '\t':
+ w.WriteByte('\\')
+ w.WriteByte('t')
+ case '\b':
+ w.WriteByte('\\')
+ w.WriteByte('b')
+ case '\f':
+ w.WriteByte('\\')
+ w.WriteByte('f')
+ case '\\', '(', ')':
+ w.WriteByte('\\')
+ w.WriteByte(c)
+ default:
+ w.WriteByte(c)
+ }
+ }
+ fmt.Fprintf(w, ")")
+ }
+
+ fmt.Fprintf(w, "[")
+ write(tx)
+ fmt.Fprintf(w, "]TJ")
+ return nil
+}
diff --git a/paint/pdf/writer.go b/paint/pdf/writer.go
new file mode 100644
index 0000000000..9f54e9aade
--- /dev/null
+++ b/paint/pdf/writer.go
@@ -0,0 +1,560 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This is adapted from https://github.com/tdewolff/canvas
+// Copyright (c) 2015 Taco de Wolff, under an MIT License.
+
+package pdf
+
+import (
+ "bytes"
+ "compress/zlib"
+ "encoding/ascii85"
+ "fmt"
+ "image"
+ "io"
+ "math"
+ "slices"
+ "sort"
+ "strings"
+ "time"
+ "unicode/utf16"
+
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/paint/ppath"
+ "cogentcore.org/core/styles"
+ "cogentcore.org/core/styles/units"
+ "golang.org/x/exp/maps"
+)
+
+// TODO: Invalid graphics transparency, Group has a transparency S entry or the S entry is null
+// TODO: Invalid Color space, The operator "g" can't be used without Color Profile
+
+type pdfWriter struct {
+ w io.Writer
+ err error
+
+ unitContext units.Context
+ globalScale float32 // global unit conversion
+ pos int
+ objOffsets []int
+ pages []pdfRef
+
+ page *pdfPage
+ fontsStd map[string]pdfRef
+ // todo: for custom fonts:
+ // fontSubset map[*text.Font]*ppath.FontSubsetter
+ // fontsH map[*text.Font]pdfRef
+ // fontsV map[*text.Font]pdfRef
+ images map[image.Image]pdfRef
+ layers pdfLayers
+ anchors pdfMap // things that can be linked to within doc
+ outlines []*pdfOutline
+ compress bool
+ subset bool
+ title string
+ subject string
+ keywords string
+ author string
+ creator string
+ lang string
+}
+
+func newPDFWriter(writer io.Writer, un *units.Context) *pdfWriter {
+ w := &pdfWriter{
+ w: writer,
+ unitContext: *un,
+ objOffsets: []int{0, 0, 0}, // catalog, metadata, page tree
+ fontsStd: map[string]pdfRef{},
+ // fontSubset: map[*text.Font]*ppath.FontSubsetter{},
+ // fontsH: map[*text.Font]pdfRef{},
+ // fontsV: map[*text.Font]pdfRef{},
+ images: map[image.Image]pdfRef{},
+ compress: false,
+ subset: true,
+ }
+ w.layerInit()
+
+ w.globalScale = w.unitContext.Convert(1, units.UnitDot, units.UnitPt)
+
+ w.write("%%PDF-1.7\n%%Ŧǟċơ\n")
+ return w
+}
+
+// SetCompression enable the compression of the streams.
+func (w *pdfWriter) SetCompression(compress bool) {
+ w.compress = compress
+}
+
+// SeFontSubsetting enables the subsetting of embedded fonts.
+func (w *pdfWriter) SetFontSubsetting(subset bool) {
+ w.subset = subset
+}
+
+// SetTitle sets the document's title.
+func (w *pdfWriter) SetTitle(title string) {
+ w.title = title
+}
+
+// SetSubject sets the document's subject.
+func (w *pdfWriter) SetSubject(subject string) {
+ w.subject = subject
+}
+
+// SetKeywords sets the document's keywords.
+func (w *pdfWriter) SetKeywords(keywords string) {
+ w.keywords = keywords
+}
+
+// SetAuthor sets the document's author.
+func (w *pdfWriter) SetAuthor(author string) {
+ w.author = author
+}
+
+// SetCreator sets the document's creator.
+func (w *pdfWriter) SetCreator(creator string) {
+ w.creator = creator
+}
+
+// SetLang sets the document's language.
+func (w *pdfWriter) SetLang(lang string) {
+ w.lang = lang
+}
+
+func (w *pdfWriter) writeBytes(b []byte) {
+ if w.err != nil {
+ return
+ }
+ n, err := w.w.Write(b)
+ w.pos += n
+ w.err = err
+}
+
+func (w *pdfWriter) write(s string, v ...interface{}) {
+ if w.err != nil {
+ return
+ }
+ n, err := fmt.Fprintf(w.w, s, v...)
+ w.pos += n
+ w.err = err
+}
+
+type pdfRef int
+type pdfName string
+type pdfArray []interface{}
+type pdfDict map[pdfName]interface{}
+type pdfMap map[string]interface{}
+type pdfFilter string
+type pdfStream struct {
+ dict pdfDict
+ stream []byte
+}
+
+const (
+ pdfFilterASCII85 pdfFilter = "ASCII85Decode"
+ pdfFilterFlate pdfFilter = "FlateDecode"
+ pdfFilterDCT pdfFilter = "DCTDecode"
+)
+
+func pdfValContinuesName(val any) bool {
+ switch val.(type) {
+ case string, pdfName, pdfFilter, pdfArray, pdfDict, pdfStream:
+ return false
+ }
+ return true
+}
+
+func (w *pdfWriter) writeVal(i interface{}) {
+ switch v := i.(type) {
+ case bool:
+ if v {
+ w.write("true")
+ } else {
+ w.write("false")
+ }
+ case int:
+ w.write("%d", v)
+ case dec:
+ w.write("%v", v)
+ case float32:
+ w.write("%v", dec(v))
+ case float64:
+ w.write("%v", dec(v))
+ case string:
+ w.write("(%v)", escape(v))
+ case pdfRef:
+ w.write("%v 0 R", v)
+ case pdfName, pdfFilter:
+ w.write("/%v", v)
+ case pdfArray:
+ w.write("[")
+ for j, val := range v {
+ if j != 0 {
+ w.write(" ")
+ }
+ w.writeVal(val)
+ }
+ w.write("]")
+ case pdfDict:
+ w.write("<<")
+ if val, ok := v["Type"]; ok {
+ w.write("/Type")
+ if pdfValContinuesName(val) {
+ w.write(" ")
+ }
+ w.writeVal(val)
+ }
+ if val, ok := v["Subtype"]; ok {
+ w.write("/Subtype")
+ if pdfValContinuesName(val) {
+ w.write(" ")
+ }
+ w.writeVal(val)
+ }
+ if val, ok := v["S"]; ok {
+ w.write("/S")
+ if pdfValContinuesName(val) {
+ w.write(" ")
+ }
+ w.writeVal(val)
+ }
+ keys := []string{}
+ for key := range v {
+ if key != "Type" && key != "Subtype" && key != "S" {
+ keys = append(keys, string(key))
+ }
+ }
+ sort.Strings(keys)
+ for _, key := range keys {
+ w.writeVal(pdfName(key))
+ if pdfValContinuesName(v[pdfName(key)]) {
+ w.write(" ")
+ }
+ w.writeVal(v[pdfName(key)])
+ }
+ w.write(">>")
+ case pdfMap:
+ w.write("<<")
+ keys := maps.Keys(v)
+ sort.Strings(keys)
+ nk := len(keys)
+ for i, key := range keys {
+ w.writeVal(pdfName(key))
+ w.write(" ")
+ w.writeVal(v[key])
+ if i < nk-1 {
+ w.write(" ")
+ }
+ }
+ w.write(">>")
+ case pdfStream:
+ if v.dict == nil {
+ v.dict = pdfDict{}
+ }
+
+ filters := []pdfFilter{}
+ if filter, ok := v.dict["Filter"].(pdfFilter); ok {
+ filters = append(filters, filter)
+ } else if filterArray, ok := v.dict["Filter"].(pdfArray); ok {
+ for i := len(filterArray) - 1; i >= 0; i-- {
+ if filter, ok := filterArray[i].(pdfFilter); ok {
+ filters = append(filters, filter)
+ }
+ }
+ }
+
+ b := v.stream
+ for _, filter := range filters {
+ var b2 bytes.Buffer
+ switch filter {
+ case pdfFilterASCII85:
+ w := ascii85.NewEncoder(&b2)
+ w.Write(b)
+ w.Close()
+ fmt.Fprintf(&b2, "~>")
+ b = b2.Bytes()
+ case pdfFilterFlate:
+ w := zlib.NewWriter(&b2)
+ w.Write(b)
+ w.Close()
+ b = b2.Bytes()
+ default:
+ // assume already in the right format
+ }
+ }
+
+ v.dict["Length"] = len(b)
+ w.writeVal(v.dict)
+ w.write("stream\n")
+ w.writeBytes(b)
+ w.write("\nendstream\n")
+ default:
+ // panic(fmt.Sprintf("unknown PDF type %T", i))
+ }
+}
+
+func (w *pdfWriter) writeObject(val interface{}) pdfRef {
+ // newlines before and after obj and endobj are required by PDF/A
+ w.objOffsets = append(w.objOffsets, w.pos)
+ w.write("%v 0 obj\n", len(w.objOffsets))
+ w.writeVal(val)
+ w.write("\nendobj\n")
+ return pdfRef(len(w.objOffsets))
+}
+
+// Close finished the document.
+func (w *pdfWriter) Close() error {
+ if w.page != nil {
+ w.pages = append(w.pages, w.page.writePage(pdfRef(3)))
+ }
+
+ kids := pdfArray{}
+ for _, page := range w.pages {
+ kids = append(kids, page)
+ }
+
+ // write fonts
+ // w.writeFonts(w.fontsH, false)
+ // w.writeFonts(w.fontsV, false)
+
+ // document catalog
+ catalog := pdfDict{
+ "Type": pdfName("Catalog"),
+ "Pages": pdfRef(3),
+ }
+
+ if len(w.anchors) > 0 {
+ ancrefs := pdfMap{}
+ var nms []string
+ for nm, v := range w.anchors {
+ vary := v.(pdfArray)
+ vary[0] = w.pages[vary[0].(int)] // replace page no with ref
+ anc := w.writeObject(pdfDict{"D": vary})
+ ancrefs[nm] = anc
+ nms = append(nms, nm)
+ }
+ slices.Sort(nms)
+ var nmary pdfArray
+ for _, nm := range nms {
+ nmary = append(nmary, nm, ancrefs[nm])
+ }
+ nmref := w.writeObject(pdfDict{"Names": nmary})
+ catalog[pdfName("Names")] = pdfDict{"Dests": nmref}
+ }
+
+ if len(w.outlines) > 0 {
+ firstRef := w.writeOutlines()
+ first := w.outlines[0]
+ cdict := w.writeObject(pdfDict{
+ "Type:": pdfName("Outlines"),
+ "First": firstRef,
+ "Last": firstRef + pdfRef(first.last),
+ "Count": len(w.outlines),
+ })
+ catalog[pdfName("Outlines")] = cdict
+ }
+
+ if len(w.layers.list) > 0 {
+ var refs, off pdfArray
+ for _, l := range w.layers.list {
+ refs = append(refs, l.ref)
+ if !l.visible {
+ off = append(off, l.ref)
+ }
+ }
+ catalog[pdfName("OCProperties")] = pdfDict{
+ "OCGs": refs,
+ "D": pdfDict{"OFF": off, "Order": refs},
+ }
+ if w.layers.openLayerPane {
+ catalog[pdfName("PageMode")] = pdfName("UseOC")
+ }
+ }
+
+ // document info
+ info := pdfDict{
+ "Producer": "cogentcore/pdf",
+ "CreationDate": time.Now().Format("D:20060102150405Z0700"),
+ }
+
+ encode := func(s string) string {
+ // TODO: make clean
+ ascii := true
+ for _, r := range s {
+ if 0x80 <= r {
+ ascii = false
+ break
+ }
+ }
+ if ascii {
+ return s
+ }
+
+ rs := utf16.Encode([]rune(s))
+ b := make([]byte, 2+2*len(rs))
+ b[0] = 254
+ b[1] = 255
+ for i, r := range rs {
+ b[2+2*i+0] = byte(r >> 8)
+ b[2+2*i+1] = byte(r & 0x00FF)
+ }
+ return string(b)
+ }
+ if w.title != "" {
+ info["Title"] = encode(w.title)
+ }
+ if w.subject != "" {
+ info["Subject"] = encode(w.subject)
+ }
+ if w.keywords != "" {
+ info["Keywords"] = encode(w.keywords)
+ }
+ if w.author != "" {
+ info["Author"] = encode(w.author)
+ }
+ if w.creator != "" {
+ info["Creator"] = encode(w.creator)
+ }
+ if w.lang != "" {
+ catalog["Lang"] = encode(w.creator)
+ }
+
+ // document catalog
+ w.objOffsets[0] = w.pos
+ w.write("%v 0 obj\n", 1)
+ w.writeVal(catalog)
+ w.write("\nendobj\n")
+
+ // document info
+ w.objOffsets[1] = w.pos
+ w.write("%v 0 obj\n", 2)
+ w.writeVal(info)
+ w.write("\nendobj\n")
+
+ // page tree
+ w.objOffsets[2] = w.pos
+ w.write("%v 0 obj\n", 3)
+ w.writeVal(pdfDict{
+ "Type": pdfName("Pages"),
+ "Kids": pdfArray(kids),
+ "Count": len(kids),
+ })
+ w.write("\nendobj\n")
+
+ xrefOffset := w.pos
+ w.write("xref\n0 %d\n0000000000 65535 f \n", len(w.objOffsets)+1)
+ for _, objOffset := range w.objOffsets {
+ w.write("%010d 00000 n \n", objOffset)
+ }
+ w.write("trailer\n")
+ w.writeVal(pdfDict{
+ "Root": pdfRef(1),
+ "Size": len(w.objOffsets) + 1,
+ "Info": pdfRef(2),
+ // TODO: write document ID
+ })
+ w.write("\nstartxref\n%v\n%%%%EOF\n", xrefOffset)
+ return w.err
+}
+
+// NewPage starts a new page.
+func (w *pdfWriter) NewPage(width, height float32) *pdfPage {
+ if w.page != nil {
+ w.pages = append(w.pages, w.page.writePage(pdfRef(3)))
+ }
+
+ // for defaults see https://help.adobe.com/pdfl_sdk/15/PDFL_SDK_HTMLHelp/PDFL_SDK_HTMLHelp/API_References/PDFL_API_Reference/PDFEdit_Layer/General.html#_t_PDEGraphicState
+ w.page = &pdfPage{
+ Buffer: &bytes.Buffer{},
+ pdf: w,
+ width: width,
+ height: height,
+ pageNo: len(w.pages),
+ resources: pdfDict{},
+ graphicsStates: map[float32]pdfName{},
+ inTextObject: false,
+ textPosition: math32.Vector2{},
+ textCharSpace: 0.0,
+ textRenderMode: 0,
+ }
+ w.page.stack.Push(newContext(styles.NewPaint(), math32.Identity2()))
+ w.page.setTopTransform()
+ // fmt.Println("added page:", w.page.pageNo)
+ return w.page
+}
+
+// setTopTransform sets the current transformation matrix so that
+// the top left corner is effectively at 0,0. This is set at the
+// start of each page, to align with standard rendering in cogent core.
+func (w *pdfPage) setTopTransform() {
+ sc := w.pdf.globalScale
+ m := math32.Translate2D(0, w.height).Scale(sc, -sc)
+ w.SetTransform(m)
+}
+
+// dec efficiently prints float values.
+type dec float32
+
+func (f dec) String() string {
+ s := fmt.Sprintf("%.*f", 5, f) // precision
+ s = string(ppath.MinifyDecimal([]byte(s), ppath.Precision))
+ if dec(math.MaxInt32) < f || f < dec(math.MinInt32) {
+ if i := strings.IndexByte(s, '.'); i == -1 {
+ s += ".0"
+ }
+ }
+ return s
+}
+
+// mat2 returns matrix components as a string
+func mat2(m math32.Matrix2) string {
+ return fmt.Sprintf("%v %v %v %v %v %v", dec(m.XX), dec(m.XY), dec(m.YX), dec(m.YY), dec(m.X0), dec(m.Y0))
+}
+
+// Escape special characters in strings
+func escape(s string) string {
+ s = strings.Replace(s, "\\", "\\\\", -1)
+ s = strings.Replace(s, "(", "\\(", -1)
+ s = strings.Replace(s, ")", "\\)", -1)
+ s = strings.Replace(s, "\r", "\\r", -1)
+ return s
+}
+
+// utf8toutf16 converts UTF-8 to UTF-16BE; from http://www.fpdf.org/
+func utf8toutf16(s string, withBOM ...bool) string {
+ bom := true
+ if len(withBOM) > 0 {
+ bom = withBOM[0]
+ }
+ res := make([]byte, 0, 8)
+ if bom {
+ res = append(res, 0xFE, 0xFF)
+ }
+ nb := len(s)
+ i := 0
+ for i < nb {
+ c1 := byte(s[i])
+ i++
+ switch {
+ case c1 >= 224:
+ // 3-byte character
+ c2 := byte(s[i])
+ i++
+ c3 := byte(s[i])
+ i++
+ res = append(res, ((c1&0x0F)<<4)+((c2&0x3C)>>2),
+ ((c2&0x03)<<6)+(c3&0x3F))
+ case c1 >= 192:
+ // 2-byte character
+ c2 := byte(s[i])
+ i++
+ res = append(res, ((c1 & 0x1C) >> 2),
+ ((c1&0x03)<<6)+(c2&0x3F))
+ default:
+ // Single-byte character
+ res = append(res, 0, c1)
+ }
+ }
+ return string(res)
+}
diff --git a/paint/pimage/pimage.go b/paint/pimage/pimage.go
index 61ba5c5465..6068849b5f 100644
--- a/paint/pimage/pimage.go
+++ b/paint/pimage/pimage.go
@@ -65,10 +65,19 @@ type Params struct {
// BlurRadius is the Gaussian standard deviation for Blur function
BlurRadius float32
+
+ // Anchor provides a named link destination associated with this text item.
+ // Used for document navigation, e.g., in PDF rendering. Must be a unique name.
+ // Is set in [core.Text] based on the id property, which is set by [content].
+ Anchor string
}
func (pr *Params) IsRenderItem() {}
+func (p *Params) String() string {
+ return "image: " + p.Cmd.String()
+}
+
// NewClear returns a new Clear that renders entire image with given source image.
func NewClear(src image.Image, sp image.Point, op draw.Op) *Params {
pr := &Params{Cmd: Draw, Rect: image.Rectangle{}, Source: imagex.WrapJS(src), SourcePos: sp, Op: op}
diff --git a/paint/ppath/minify.go b/paint/ppath/minify.go
index 0ef1bae847..d72b9a8c12 100644
--- a/paint/ppath/minify.go
+++ b/paint/ppath/minify.go
@@ -15,8 +15,11 @@ const MaxInt = int(^uint(0) >> 1)
// MinInt is the minimum value of int.
const MinInt = -MaxInt - 1
-// MinifyDecimal minifies a given byte slice containing a decimal and removes superfluous characters. It differs from Number in that it does not parse exponents.
-// It does not parse or output exponents. prec is the number of significant digits. When prec is zero it will keep all digits. Only digits after the dot can be removed to reach the number of significant digits. Very large number may thus have more significant digits.
+// MinifyDecimal minifies a given byte slice containing a decimal and removes superfluous characters.
+// It differs from Number in that it does not parse exponents.
+// It does not parse or output exponents. prec is the number of significant digits.
+// When prec is zero it will keep all digits. Only digits after the dot can be removed
+// to reach the number of significant digits. Very large number may thus have more significant digits.
func MinifyDecimal(num []byte, prec int) []byte {
if len(num) <= 1 {
return num
diff --git a/paint/ppath/transform.go b/paint/ppath/transform.go
index 401921b51d..fafb2541e9 100644
--- a/paint/ppath/transform.go
+++ b/paint/ppath/transform.go
@@ -25,20 +25,20 @@ func (p Path) Transform(m math32.Matrix2) Path {
fmt.Println("path length error:", len(p), i, p)
return p
}
- end := m.MulVector2AsPoint(math32.Vec2(p[i+1], p[i+2]))
+ end := m.MulPoint(math32.Vec2(p[i+1], p[i+2]))
p[i+1] = end.X
p[i+2] = end.Y
case QuadTo:
- cp := m.MulVector2AsPoint(math32.Vec2(p[i+1], p[i+2]))
- end := m.MulVector2AsPoint(math32.Vec2(p[i+3], p[i+4]))
+ cp := m.MulPoint(math32.Vec2(p[i+1], p[i+2]))
+ end := m.MulPoint(math32.Vec2(p[i+3], p[i+4]))
p[i+1] = cp.X
p[i+2] = cp.Y
p[i+3] = end.X
p[i+4] = end.Y
case CubeTo:
- cp1 := m.MulVector2AsPoint(math32.Vec2(p[i+1], p[i+2]))
- cp2 := m.MulVector2AsPoint(math32.Vec2(p[i+3], p[i+4]))
- end := m.MulVector2AsPoint(math32.Vec2(p[i+5], p[i+6]))
+ cp1 := m.MulPoint(math32.Vec2(p[i+1], p[i+2]))
+ cp2 := m.MulPoint(math32.Vec2(p[i+3], p[i+4]))
+ end := m.MulPoint(math32.Vec2(p[i+5], p[i+6]))
p[i+1] = cp1.X
p[i+2] = cp1.Y
p[i+3] = cp2.X
@@ -77,7 +77,7 @@ func (p Path) Transform(m math32.Matrix2) Path {
if xscale*yscale < 0.0 { // flip x or y axis needs flipping of the sweep
sweep = !sweep
}
- end = m.MulVector2AsPoint(end)
+ end = m.MulPoint(end)
p[i+1] = rx
p[i+2] = ry
diff --git a/paint/render/context.go b/paint/render/context.go
index 80143d2985..04f6f23637 100644
--- a/paint/render/context.go
+++ b/paint/render/context.go
@@ -49,9 +49,12 @@ type Context struct {
// Individual elements inherit from this style.
Style styles.Paint
- // Transform is the accumulated transformation matrix.
+ // Transform is the transformation matrix for this stack level.
Transform math32.Matrix2
+ // Cumulative is the accumulated transformation matrix.
+ Cumulative math32.Matrix2
+
// Bounds is the rounded rectangle clip boundary.
// This is applied to the effective Path prior to adding to Render.
Bounds Bounds
@@ -92,12 +95,14 @@ func (ctx *Context) Init(sty *styles.Paint, bounds *Bounds, parent *Context) {
}
if parent == nil {
ctx.Transform = sty.Transform
+ ctx.Cumulative = sty.Transform
ctx.SetBounds(bounds)
ctx.ClipPath = sty.ClipPath
ctx.Mask = sty.Mask
return
}
- ctx.Transform = parent.Transform.Mul(ctx.Style.Transform)
+ ctx.Transform = ctx.Style.Transform
+ ctx.Cumulative = parent.Cumulative.Mul(ctx.Style.Transform)
ctx.Style.InheritFields(&parent.Style)
if bounds == nil {
bounds = &parent.Bounds
diff --git a/paint/render/item.go b/paint/render/item.go
index 3800f34bf8..ca77c97caf 100644
--- a/paint/render/item.go
+++ b/paint/render/item.go
@@ -4,9 +4,13 @@
package render
+import "fmt"
+
// Item is a union interface for render items:
// [Path], [Text], [Image], and [ContextPush].
type Item interface {
+ fmt.Stringer
+
IsRenderItem()
}
@@ -20,11 +24,19 @@ type ContextPush struct {
func (p *ContextPush) IsRenderItem() {
}
+func (p *ContextPush) String() string {
+ return "ctx-push: " + p.Context.Cumulative.String()
+}
+
// ContextPop is a [Context] pop render item, which can be used by renderers
// that track group structure (e.g., SVG).
type ContextPop struct {
}
+func (p *ContextPop) String() string {
+ return "ctx-pop"
+}
+
// interface assertion.
func (p *ContextPop) IsRenderItem() {
}
diff --git a/paint/render/path.go b/paint/render/path.go
index 73c1811a03..c8900af264 100644
--- a/paint/render/path.go
+++ b/paint/render/path.go
@@ -34,3 +34,7 @@ func NewPath(pt ppath.Path, sty *styles.Paint, ctx *Context) *Path {
// interface assertion.
func (p *Path) IsRenderItem() {}
+
+func (p *Path) String() string {
+ return "path: " + p.Path.String()
+}
diff --git a/paint/render/render.go b/paint/render/render.go
index 134ebd1d7a..e84dab4cd9 100644
--- a/paint/render/render.go
+++ b/paint/render/render.go
@@ -7,6 +7,7 @@ package render
import (
"reflect"
"slices"
+ "strings"
"cogentcore.org/core/base/reflectx"
)
@@ -27,6 +28,16 @@ func (pr *Render) Add(item ...Item) *Render {
if reflectx.IsNil(reflect.ValueOf(it)) {
continue
}
+ n := len(*pr)
+ if n > 0 {
+ // eliminate empty push-pop sequences, which occur due to invisible elements
+ if _, ok := it.(*ContextPop); ok {
+ if _, ok := (*pr)[n-1].(*ContextPush); ok {
+ *pr = (*pr)[:n-1]
+ continue
+ }
+ }
+ }
*pr = append(*pr, it)
}
return pr
@@ -37,3 +48,11 @@ func (pr *Render) Add(item ...Item) *Render {
func (pr *Render) Reset() {
*pr = (*pr)[:0]
}
+
+func (pr *Render) String() string {
+ var b strings.Builder
+ for _, it := range *pr {
+ b.WriteString(it.String() + "\n")
+ }
+ return b.String()
+}
diff --git a/paint/render/text.go b/paint/render/text.go
index 9f47eae8de..2fd66f653a 100644
--- a/paint/render/text.go
+++ b/paint/render/text.go
@@ -36,3 +36,7 @@ func NewText(txt *shaped.Lines, sty *styles.Paint, ctx *Context, pos math32.Vect
// interface assertion.
func (tx *Text) IsRenderItem() {}
+
+func (tx *Text) String() string {
+ return "text: " + tx.Text.String()
+}
diff --git a/paint/renderers/htmlcanvas/htmlcanvas.go b/paint/renderers/htmlcanvas/htmlcanvas.go
index 7eeb6f5d92..208218daaf 100644
--- a/paint/renderers/htmlcanvas/htmlcanvas.go
+++ b/paint/renderers/htmlcanvas/htmlcanvas.go
@@ -25,9 +25,10 @@ import (
// Renderer is an HTML canvas renderer.
type Renderer struct {
- Canvas js.Value
- ctx js.Value
- size math32.Vector2
+ Canvas js.Value
+ ctx js.Value
+ size math32.Vector2
+ unitContext units.Context
// curRect is the rectangle of the current object.
curRect image.Rectangle
diff --git a/paint/renderers/htmlcanvas/path.go b/paint/renderers/htmlcanvas/path.go
index 4bd676079a..11bac68dad 100644
--- a/paint/renderers/htmlcanvas/path.go
+++ b/paint/renderers/htmlcanvas/path.go
@@ -58,9 +58,14 @@ func (rs *Renderer) RenderPath(pt *render.Path) {
}
if style.HasStroke() {
scale := math32.Sqrt(math32.Abs(pt.Context.Transform.Det()))
- // note: this is a hack to get the effect of [ppath.VectorEffectNonScalingStroke]
- style.Stroke.Width.Dots /= scale
- rs.setStroke(&style.Stroke)
+ if scale != 1 {
+ // note: this is a hack to get the effect of [ppath.VectorEffectNonScalingStroke]
+ stk := style.Stroke
+ stk.Width.Dots /= scale
+ rs.setStroke(&stk)
+ } else {
+ rs.setStroke(&style.Stroke)
+ }
rs.ctx.Call("stroke")
}
}
diff --git a/paint/renderers/pdfrender/pdfrender.go b/paint/renderers/pdfrender/pdfrender.go
new file mode 100644
index 0000000000..eefd1ecd17
--- /dev/null
+++ b/paint/renderers/pdfrender/pdfrender.go
@@ -0,0 +1,179 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package pdfrender
+
+import (
+ "bytes"
+ "image"
+ "io"
+
+ "cogentcore.org/core/base/iox/imagex"
+ "cogentcore.org/core/base/stack"
+ "cogentcore.org/core/colors/gradient"
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/paint/pdf"
+ "cogentcore.org/core/paint/pimage"
+ "cogentcore.org/core/paint/render"
+ "cogentcore.org/core/styles/units"
+)
+
+// Renderer is the PDF renderer.
+type Renderer struct {
+ size math32.Vector2
+ sizeUnits units.Units
+ unitContext units.Context
+
+ PDF *pdf.PDF
+
+ buff *bytes.Buffer
+
+ // gStack is a stack of graphic state context in the PDF
+ gStack stack.Stack[int]
+}
+
+func New(size math32.Vector2, un *units.Context) render.Renderer {
+ rs := &Renderer{unitContext: *un}
+ rs.SetSize(units.UnitDot, size)
+ return rs
+}
+
+func (rs *Renderer) Image() image.Image {
+ return nil // can't generate an image
+}
+
+func (rs *Renderer) Source() []byte {
+ if rs.buff == nil {
+ return nil
+ }
+ return rs.buff.Bytes()
+}
+
+func (rs *Renderer) Size() (units.Units, math32.Vector2) {
+ return rs.sizeUnits, rs.size
+}
+
+func (rs *Renderer) SetSize(un units.Units, size math32.Vector2) {
+ if rs.sizeUnits == un && rs.size == size {
+ return
+ }
+ rs.sizeUnits = un
+ rs.size = size
+}
+
+// Render is the main rendering function.
+func (rs *Renderer) Render(r render.Render) render.Renderer {
+ rs.buff = &bytes.Buffer{}
+ rs.StartRender(rs.buff)
+ rs.RenderPage(r)
+ rs.EndRender()
+ return rs
+}
+
+// StartRender creates the renderer.
+func (rs *Renderer) StartRender(w io.Writer) {
+ sx := rs.unitContext.Convert(float32(rs.size.X), rs.sizeUnits, units.UnitPt)
+ sy := rs.unitContext.Convert(float32(rs.size.Y), rs.sizeUnits, units.UnitPt)
+ rs.PDF = pdf.New(w, sx, sy, &rs.unitContext)
+ rs.gStack = nil
+}
+
+// EndRender finishes the render
+func (rs *Renderer) EndRender() {
+ rs.PDF.Close()
+}
+
+// AddPage adds a new page of the same size.
+func (rs *Renderer) AddPage() {
+ sx := rs.unitContext.Convert(float32(rs.size.X), rs.sizeUnits, units.UnitPt)
+ sy := rs.unitContext.Convert(float32(rs.size.Y), rs.sizeUnits, units.UnitPt)
+ rs.PDF.NewPage(sx, sy)
+}
+
+// RenderPage renders the content to current PDF page
+func (rs *Renderer) RenderPage(r render.Render) {
+ for _, ri := range r {
+ switch x := ri.(type) {
+ case *render.Path:
+ rs.RenderPath(x)
+ case *pimage.Params:
+ rs.RenderImage(x)
+ case *render.Text:
+ rs.RenderText(x)
+ case *render.ContextPush:
+ rs.PushContext(x)
+ case *render.ContextPop:
+ rs.PopContext(x)
+ }
+ }
+}
+
+func (rs *Renderer) PushTransform(m math32.Matrix2) int {
+ cg := rs.gStack.Peek()
+ g := cg + 1
+ rs.PDF.PushTransform(m)
+ rs.gStack.Push(g)
+ return g
+}
+
+func (rs *Renderer) PopStack() int {
+ cg := rs.gStack.Pop()
+ rs.PDF.PopStack()
+ return cg
+}
+
+func (rs *Renderer) RenderPath(pt *render.Path) {
+ p := pt.Path
+ pc := &pt.Context
+ rs.PDF.Path(p, &pc.Style, pc.Transform)
+}
+
+func (rs *Renderer) PushContext(pt *render.ContextPush) {
+ rs.PushTransform(pt.Context.Transform)
+}
+
+func (rs *Renderer) PopContext(pt *render.ContextPop) {
+ rs.PopStack()
+}
+
+func (rs *Renderer) RenderText(pt *render.Text) {
+ pc := &pt.Context
+ rs.PDF.Text(&pc.Style, pc.Transform, pt.Position, pt.Text)
+}
+
+func (rs *Renderer) RenderImage(pr *pimage.Params) {
+ usrc := imagex.Unwrap(pr.Source)
+ umask := imagex.Unwrap(pr.Mask)
+
+ nilSrc := usrc == nil
+ if r, ok := usrc.(*image.RGBA); ok && r == nil {
+ nilSrc = true
+ }
+ if pr.Rect == (image.Rectangle{}) {
+ pr.Rect = image.Rectangle{Max: rs.size.ToPoint()}
+ }
+
+ if pr.Anchor != "" {
+ rs.PDF.AddAnchor(pr.Anchor, math32.FromPoint(pr.Rect.Min))
+ }
+
+ // todo: handle masks!
+
+ // Fast path for [image.Uniform]
+ if u, ok := usrc.(*image.Uniform); nilSrc || ok && umask == nil {
+ rs.PDF.FillBox(math32.Identity2(), math32.B2FromRect(pr.Rect), u)
+ return
+ }
+
+ if gr, ok := usrc.(gradient.Gradient); ok {
+ _ = gr
+ // todo: handle:
+ return
+ }
+
+ // sz := pr.Rect.Size()
+ m := math32.Translate2D(float32(pr.Rect.Min.X), float32(pr.Rect.Min.Y))
+ rs.PDF.Image(usrc, m)
+ // simg.Pos = math32.FromPoint(pr.Rect.Min)
+}
diff --git a/paint/renderers/rasterx/geom.go b/paint/renderers/rasterx/geom.go
index 655a8e69b9..840ebf1177 100644
--- a/paint/renderers/rasterx/geom.go
+++ b/paint/renderers/rasterx/geom.go
@@ -322,20 +322,20 @@ func (t *MatrixAdder) Reset() {
// Start starts a new path
func (t *MatrixAdder) Start(a fixed.Point26_6) {
- t.Adder.Start(t.M.MulFixedAsPoint(a))
+ t.Adder.Start(t.M.MulFixedPoint(a))
}
// Line adds a linear segment to the current curve.
func (t *MatrixAdder) Line(b fixed.Point26_6) {
- t.Adder.Line(t.M.MulFixedAsPoint(b))
+ t.Adder.Line(t.M.MulFixedPoint(b))
}
// QuadBezier adds a quadratic segment to the current curve.
func (t *MatrixAdder) QuadBezier(b, c fixed.Point26_6) {
- t.Adder.QuadBezier(t.M.MulFixedAsPoint(b), t.M.MulFixedAsPoint(c))
+ t.Adder.QuadBezier(t.M.MulFixedPoint(b), t.M.MulFixedPoint(c))
}
// CubeBezier adds a cubic segment to the current curve.
func (t *MatrixAdder) CubeBezier(b, c, d fixed.Point26_6) {
- t.Adder.CubeBezier(t.M.MulFixedAsPoint(b), t.M.MulFixedAsPoint(c), t.M.MulFixedAsPoint(d))
+ t.Adder.CubeBezier(t.M.MulFixedPoint(b), t.M.MulFixedPoint(c), t.M.MulFixedPoint(d))
}
diff --git a/paint/renderers/rasterx/renderer.go b/paint/renderers/rasterx/renderer.go
index b61c3aacd3..c6a270c82b 100644
--- a/paint/renderers/rasterx/renderer.go
+++ b/paint/renderers/rasterx/renderer.go
@@ -82,7 +82,7 @@ func (rs *Renderer) RenderPath(pt *render.Path) {
}
pc := &pt.Context
rs.Scanner.SetClip(pc.Bounds.Rect.ToRect())
- PathToRasterx(&rs.Path, p, pt.Context.Transform, math32.Vector2{})
+ PathToRasterx(&rs.Path, p, pt.Context.Cumulative, math32.Vector2{})
rs.Fill(pt)
rs.Stroke(pt)
rs.Path.Clear()
@@ -92,18 +92,18 @@ func (rs *Renderer) RenderPath(pt *render.Path) {
func PathToRasterx(rs Adder, p ppath.Path, m math32.Matrix2, off math32.Vector2) {
for s := p.Scanner(); s.Scan(); {
cmd := s.Cmd()
- end := m.MulVector2AsPoint(s.End()).Add(off)
+ end := m.MulPoint(s.End()).Add(off)
switch cmd {
case ppath.MoveTo:
rs.Start(end.ToFixed())
case ppath.LineTo:
rs.Line(end.ToFixed())
case ppath.QuadTo:
- cp1 := m.MulVector2AsPoint(s.CP1()).Add(off)
+ cp1 := m.MulPoint(s.CP1()).Add(off)
rs.QuadBezier(cp1.ToFixed(), end.ToFixed())
case ppath.CubeTo:
- cp1 := m.MulVector2AsPoint(s.CP1()).Add(off)
- cp2 := m.MulVector2AsPoint(s.CP2()).Add(off)
+ cp1 := m.MulPoint(s.CP1()).Add(off)
+ cp2 := m.MulPoint(s.CP2()).Add(off)
rs.CubeBezier(cp1.ToFixed(), cp2.ToFixed(), end.ToFixed())
case ppath.Close:
rs.Stop(true)
@@ -120,7 +120,7 @@ func (rs *Renderer) Stroke(pt *render.Path) {
dash := slices.Clone(sty.Stroke.Dashes)
if dash != nil {
- scx, scy := pc.Transform.ExtractScale()
+ scx, scy := pc.Cumulative.ExtractScale()
sc := 0.5 * (math32.Abs(scx) + math32.Abs(scy))
for i := range dash {
dash[i] *= sc
@@ -143,7 +143,7 @@ func (rs *Renderer) SetColor(sc Scanner, pc *render.Context, clr image.Image, op
fbox := sc.GetPathExtent()
lastRenderBBox := image.Rectangle{Min: image.Point{fbox.Min.X.Floor(), fbox.Min.Y.Floor()},
Max: image.Point{fbox.Max.X.Ceil(), fbox.Max.Y.Ceil()}}
- g.Update(opacity, math32.B2FromRect(lastRenderBBox), pc.Transform)
+ g.Update(opacity, math32.B2FromRect(lastRenderBBox), pc.Cumulative)
sc.SetColor(clr)
} else {
if opacity < 1 {
@@ -187,7 +187,7 @@ func (rs *Renderer) StrokeWidth(pt *render.Path) float32 {
if sty.VectorEffect == ppath.VectorEffectNonScalingStroke {
return dw
}
- sc := MeanScale(pt.Context.Transform)
+ sc := MeanScale(pt.Context.Cumulative)
return sc * dw
}
diff --git a/paint/renderers/rasterx/shapes.go b/paint/renderers/rasterx/shapes.go
index 31bd4f5294..2586e6a81b 100644
--- a/paint/renderers/rasterx/shapes.go
+++ b/paint/renderers/rasterx/shapes.go
@@ -33,7 +33,7 @@ func AddCircle(cx, cy, r float32, p Adder) {
// x and y radius, (rx, ry), rotated around the center by rot degrees.
func AddEllipse(cx, cy, rx, ry, rot float32, p Adder) {
rotRads := rot * math32.Pi / 180
- pt := math32.Identity2().Translate(cx, cy).Rotate(rotRads).Translate(-cx, -cy).MulVector2AsPoint(math32.Vec2(cx+rx, cy))
+ pt := math32.Identity2().Translate(cx, cy).Rotate(rotRads).Translate(-cx, -cy).MulPoint(math32.Vec2(cx+rx, cy))
points := []float32{rx, ry, rot, 1.0, 0.0, pt.X, pt.Y}
p.Start(pt.ToFixed())
AddArc(points, cx, cy, pt.X, pt.Y, p)
diff --git a/paint/renderers/rasterx/text.go b/paint/renderers/rasterx/text.go
index 25db91478c..8ebac88fa9 100644
--- a/paint/renderers/rasterx/text.go
+++ b/paint/renderers/rasterx/text.go
@@ -35,7 +35,7 @@ func (rs *Renderer) RenderText(txt *render.Text) {
// The text will be drawn starting at the start pixel position, which specifies the
// left baseline location of the first text item..
func (rs *Renderer) TextLines(ctx *render.Context, lns *shaped.Lines, pos math32.Vector2) {
- m := ctx.Transform
+ m := ctx.Cumulative
identity := m == math32.Identity2()
off := pos.Add(lns.Offset)
rs.Scanner.SetClip(ctx.Bounds.Rect.ToRect())
@@ -120,7 +120,7 @@ func (rs *Renderer) TextRun(ctx *render.Context, run *shapedgt.Run, ln *shaped.L
lineW := max(fsz/16, 1) // 1 at 16, bigger if biggerr
if run.Math.Path != nil {
rs.Path.Clear()
- PathToRasterx(&rs.Path, *run.Math.Path, ctx.Transform, off)
+ PathToRasterx(&rs.Path, *run.Math.Path, ctx.Cumulative, off)
rf := &rs.Raster.Filler
rf.SetWinding(true)
rf.SetColor(fill)
@@ -204,20 +204,20 @@ func (rs *Renderer) GlyphOutline(ctx *render.Context, run *shapedgt.Run, g *shap
}
rs.Path.Clear()
- m := ctx.Transform
+ m := ctx.Cumulative
for _, s := range outline.Segments {
- p0 := m.MulVector2AsPoint(math32.Vec2(s.Args[0].X*scale+x, -s.Args[0].Y*scale+y))
+ p0 := m.MulPoint(math32.Vec2(s.Args[0].X*scale+x, -s.Args[0].Y*scale+y))
switch s.Op {
case opentype.SegmentOpMoveTo:
rs.Path.Start(p0.ToFixed())
case opentype.SegmentOpLineTo:
rs.Path.Line(p0.ToFixed())
case opentype.SegmentOpQuadTo:
- p1 := m.MulVector2AsPoint(math32.Vec2(s.Args[1].X*scale+x, -s.Args[1].Y*scale+y))
+ p1 := m.MulPoint(math32.Vec2(s.Args[1].X*scale+x, -s.Args[1].Y*scale+y))
rs.Path.QuadBezier(p0.ToFixed(), p1.ToFixed())
case opentype.SegmentOpCubeTo:
- p1 := m.MulVector2AsPoint(math32.Vec2(s.Args[1].X*scale+x, -s.Args[1].Y*scale+y))
- p2 := m.MulVector2AsPoint(math32.Vec2(s.Args[2].X*scale+x, -s.Args[2].Y*scale+y))
+ p1 := m.MulPoint(math32.Vec2(s.Args[1].X*scale+x, -s.Args[1].Y*scale+y))
+ p2 := m.MulPoint(math32.Vec2(s.Args[2].X*scale+x, -s.Args[2].Y*scale+y))
rs.Path.CubeBezier(p0.ToFixed(), p1.ToFixed(), p2.ToFixed())
}
}
@@ -265,11 +265,11 @@ func (rs *Renderer) StrokeBounds(ctx *render.Context, bb math32.Box2, clr color.
ButtCap, nil, nil, Miter,
nil, 0)
rs.Raster.SetColor(colors.Uniform(clr))
- m := ctx.Transform
- rs.Raster.Start(m.MulVector2AsPoint(math32.Vec2(bb.Min.X, bb.Min.Y)).ToFixed())
- rs.Raster.Line(m.MulVector2AsPoint(math32.Vec2(bb.Max.X, bb.Min.Y)).ToFixed())
- rs.Raster.Line(m.MulVector2AsPoint(math32.Vec2(bb.Max.X, bb.Max.Y)).ToFixed())
- rs.Raster.Line(m.MulVector2AsPoint(math32.Vec2(bb.Min.X, bb.Max.Y)).ToFixed())
+ m := ctx.Cumulative
+ rs.Raster.Start(m.MulPoint(math32.Vec2(bb.Min.X, bb.Min.Y)).ToFixed())
+ rs.Raster.Line(m.MulPoint(math32.Vec2(bb.Max.X, bb.Min.Y)).ToFixed())
+ rs.Raster.Line(m.MulPoint(math32.Vec2(bb.Max.X, bb.Max.Y)).ToFixed())
+ rs.Raster.Line(m.MulPoint(math32.Vec2(bb.Min.X, bb.Max.Y)).ToFixed())
rs.Raster.Stop(true)
rs.Raster.Draw()
rs.Raster.Clear()
@@ -277,9 +277,9 @@ func (rs *Renderer) StrokeBounds(ctx *render.Context, bb math32.Box2, clr color.
// StrokeTextLine strokes a line for text decoration.
func (rs *Renderer) StrokeTextLine(ctx *render.Context, sp, ep math32.Vector2, width float32, clr image.Image, dash []float32) {
- m := ctx.Transform
- sp = m.MulVector2AsPoint(sp)
- ep = m.MulVector2AsPoint(ep)
+ m := ctx.Cumulative
+ sp = m.MulPoint(sp)
+ ep = m.MulPoint(ep)
width *= MeanScale(m)
rs.Raster.SetStroke(
math32.ToFixed(width),
@@ -298,11 +298,11 @@ func (rs *Renderer) StrokeTextLine(ctx *render.Context, sp, ep math32.Vector2, w
func (rs *Renderer) FillBounds(ctx *render.Context, bb math32.Box2, clr image.Image) {
rf := &rs.Raster.Filler
rf.SetColor(clr)
- m := ctx.Transform
- rf.Start(m.MulVector2AsPoint(math32.Vec2(bb.Min.X, bb.Min.Y)).ToFixed())
- rf.Line(m.MulVector2AsPoint(math32.Vec2(bb.Max.X, bb.Min.Y)).ToFixed())
- rf.Line(m.MulVector2AsPoint(math32.Vec2(bb.Max.X, bb.Max.Y)).ToFixed())
- rf.Line(m.MulVector2AsPoint(math32.Vec2(bb.Min.X, bb.Max.Y)).ToFixed())
+ m := ctx.Cumulative
+ rf.Start(m.MulPoint(math32.Vec2(bb.Min.X, bb.Min.Y)).ToFixed())
+ rf.Line(m.MulPoint(math32.Vec2(bb.Max.X, bb.Min.Y)).ToFixed())
+ rf.Line(m.MulPoint(math32.Vec2(bb.Max.X, bb.Max.Y)).ToFixed())
+ rf.Line(m.MulPoint(math32.Vec2(bb.Min.X, bb.Max.Y)).ToFixed())
rf.Stop(true)
rf.Draw()
rf.Clear()
diff --git a/paint/renderers/renderers.go b/paint/renderers/renderers.go
index 99e5afadd1..120dcce479 100644
--- a/paint/renderers/renderers.go
+++ b/paint/renderers/renderers.go
@@ -6,10 +6,12 @@ package renderers
import (
"cogentcore.org/core/paint"
+ "cogentcore.org/core/paint/renderers/pdfrender"
"cogentcore.org/core/paint/renderers/svgrender"
_ "cogentcore.org/core/text/shaped/shapers"
)
func init() {
paint.NewSVGRenderer = svgrender.New
+ paint.NewPDFRenderer = pdfrender.New
}
diff --git a/paint/renderers/svgrender/svgrender.go b/paint/renderers/svgrender/svgrender.go
index 399823492d..ae27a78501 100644
--- a/paint/renderers/svgrender/svgrender.go
+++ b/paint/renderers/svgrender/svgrender.go
@@ -188,7 +188,11 @@ func (rs *Renderer) RenderImage(pr *pimage.Params) {
r := svg.NewRect(cg)
r.Pos = math32.FromPoint(pr.Rect.Min)
r.Size = math32.FromPoint(pr.Rect.Size())
- r.SetProperty("fill", colors.AsHex(u.C))
+ if ok {
+ r.SetProperty("fill", colors.AsHex(u.C))
+ } else {
+ r.SetProperty("fill", colors.Transparent)
+ }
return
}
diff --git a/paint/state.go b/paint/state.go
index ba705b5a23..513dcc62c6 100644
--- a/paint/state.go
+++ b/paint/state.go
@@ -14,6 +14,7 @@ import (
"cogentcore.org/core/paint/render"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/sides"
+ "cogentcore.org/core/styles/units"
)
var (
@@ -28,6 +29,10 @@ var (
// NewSVGRenderer returns a structured SVG renderer that can
// generate an SVG vector graphics document from painter content.
NewSVGRenderer func(size math32.Vector2) render.Renderer
+
+ // NewPDFRenderer returns a PDF renderer that can
+ // generate a PDF document from painter content.
+ NewPDFRenderer func(size math32.Vector2, un *units.Context) render.Renderer
)
// RenderToImage is a convenience function that renders the current
@@ -41,13 +46,20 @@ func RenderToImage(pc *Painter) image.Image {
}
// RenderToSVG is a convenience function that renders the current
-// accumulated painter actions to an SVG document using a
-// [NewSVGRenderer].n
+// accumulated painter actions to an SVG document using a [NewSVGRenderer]
func RenderToSVG(pc *Painter) []byte {
rd := NewSVGRenderer(pc.Size)
return rd.Render(pc.RenderDone()).Source()
}
+// RenderToPDF is a convenience function that renders the current
+// accumulated painter actions to a PDF document using a [NewPDFRenderer]
+func RenderToPDF(pc *Painter) []byte {
+ p := pc.RenderDone()
+ rd := NewPDFRenderer(pc.Size, &pc.Context().Style.UnitContext)
+ return rd.Render(p).Source()
+}
+
// The State holds all the current rendering state information used
// while painting. The [Paint] embeds a pointer to this.
type State struct {
diff --git a/paint/text_test.go b/paint/text_test.go
index 989c888e99..0e18c679bd 100644
--- a/paint/text_test.go
+++ b/paint/text_test.go
@@ -44,7 +44,7 @@ func TestTextAscii(t *testing.T) {
y := float32(5)
for _, ts := range lines {
tx := rich.NewText(fsty, []rune(ts))
- lns := txtSh.WrapLines(tx, fsty, tsty, &rich.DefaultSettings, sizef)
+ lns := txtSh.WrapLines(tx, fsty, tsty, sizef)
pos := math32.Vector2{5, y}
pc.DrawText(lns, pos)
y += 20
@@ -66,7 +66,7 @@ func TestTextMarkup(t *testing.T) {
tx, err := htmltext.HTMLToRich([]byte("This is HTMLformattedtext with underline and strikethrough"), fsty, nil)
assert.NoError(t, err)
- lns := txtSh.WrapLines(tx, fsty, tsty, &rich.DefaultSettings, sizef)
+ lns := txtSh.WrapLines(tx, fsty, tsty, sizef)
lns.SelectRegion(textpos.Range{Start: 5, End: 20})
// if tsz.X != 100 || tsz.Y != 40 {
// t.Errorf("unexpected text size: %v", tsz)
@@ -104,7 +104,7 @@ func TestTextLines(t *testing.T) {
tx.AddSpan(&du, []rune("Dotted Underline")).AddSpan(fsty, []rune(" and ")).AddSpan(&uu, []rune("Underline"))
tx.AddSpan(fsty, []rune(" and ")).AddSpan(&ol, []rune("Overline"))
- lns := txtSh.WrapLines(tx, fsty, tsty, &rich.DefaultSettings, sizef)
+ lns := txtSh.WrapLines(tx, fsty, tsty, sizef)
pos := math32.Vector2{10, 10}
// pc.Paint.Transform = math32.Rotate2DAround(math32.DegToRad(-45), pos)
pc.DrawText(lns, pos)
@@ -135,7 +135,7 @@ func TestTextColors(t *testing.T) {
tx.AddSpan(&rd, []rune("Red")).AddSpan(fsty, []rune(" and ")).AddSpan(&bl, []rune("Blue"))
tx.AddSpan(fsty, []rune(" and ")).AddSpan(&gr, []rune("Green"))
- lns := txtSh.WrapLines(tx, fsty, tsty, &rich.DefaultSettings, sizef)
+ lns := txtSh.WrapLines(tx, fsty, tsty, sizef)
pos := math32.Vector2{10, 10}
// pc.Paint.Transform = math32.Rotate2DAround(math32.DegToRad(-45), pos)
pc.DrawText(lns, pos)
diff --git a/styles/object.go b/styles/object.go
index 083bb579a3..c8fc3d7ae6 100644
--- a/styles/object.go
+++ b/styles/object.go
@@ -6,9 +6,11 @@ package styles
import (
"image"
+ "image/draw"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/math32"
+ "github.com/anthonynsimon/bild/transform"
)
// ObjectFits are the different ways in which a replaced element
@@ -96,3 +98,24 @@ func (s *Style) ResizeImage(img image.Image, box math32.Vector2) image.Image {
drect := image.Rect(0, 0, int(min(sz.X, box.X)), int(min(sz.Y, box.Y)))
return imagex.Crop(rimg, drect)
}
+
+// ResizeImageLocal resizes the given image according to [Style.ObjectFit]
+// in an object of the given box size. This version uses local Go image
+// resizing regardless of what platform, for use with local Image rendering.
+func (s *Style) ResizeImageLocal(img image.Image, box math32.Vector2) image.Image {
+ obj := math32.FromPoint(img.Bounds().Size())
+ sz := ObjectSizeFromFit(s.ObjectFit, obj, box)
+ if s.ObjectFit == FitScaleDown && sz.X >= obj.X {
+ return img
+ }
+ szi := sz.ToPointFloor()
+ rimg := transform.Resize(img, szi.X, szi.Y, transform.Linear)
+ if s.ObjectFit != FitCover || (box.X >= sz.X && box.Y >= sz.Y) {
+ return rimg
+ }
+ // need to crop the destination size to the size of the containing object
+ drect := image.Rect(0, 0, int(min(sz.X, box.X)), int(min(sz.Y, box.Y)))
+ dst := image.NewRGBA(drect)
+ draw.Draw(dst, drect, rimg, image.Point{}, draw.Src)
+ return dst
+}
diff --git a/styles/style.go b/styles/style.go
index 043f24fbd6..e731de8358 100644
--- a/styles/style.go
+++ b/styles/style.go
@@ -248,6 +248,15 @@ func (s *Style) Defaults() {
s.Text.Defaults()
}
+// ZeroSpace sets all the spacing elements to zero: Padding, Border, Margin, Gap
+func (s *Style) ZeroSpace() {
+ s.Padding.Zero()
+ s.Margin.Zero()
+ s.MaxBorder.Width.Zero()
+ s.Border.Width.Zero()
+ s.Gap.Zero()
+}
+
// VirtualKeyboards are all of the supported virtual keyboard types
// to display on mobile platforms.
type VirtualKeyboards int32 //enums:enum -trim-prefix Keyboard -transform kebab
diff --git a/styles/style_props.go b/styles/style_props.go
index d288ae6da1..fdf0ff65ad 100644
--- a/styles/style_props.go
+++ b/styles/style_props.go
@@ -516,6 +516,10 @@ var styleFuncs = map[string]styleprops.Func{
func(obj *Text) enums.EnumSetter { return &obj.WhiteSpace }),
"direction": styleprops.Enum(rich.LTR,
func(obj *Text) enums.EnumSetter { return &obj.Direction }),
+ "text-indent": styleprops.Units(units.Value{},
+ func(obj *Text) *units.Value { return &obj.Indent }),
+ "text-hanging": styleprops.Units(units.Value{},
+ func(obj *Text) *units.Value { return &obj.Hanging }),
"tab-size": styleprops.Int(int(4),
func(obj *Text) *int { return &obj.TabSize }),
"select-color": func(obj any, key string, val any, parent any, cc colors.Context) {
diff --git a/styles/text.go b/styles/text.go
index 96331e462d..e5ac793188 100644
--- a/styles/text.go
+++ b/styles/text.go
@@ -45,6 +45,13 @@ type Text struct { //types:add
// unicode text is typically written in a different direction.
Direction rich.Directions
+ // Indent specifies how much to indent the first line in a paragraph (inherited).
+ Indent units.Value
+
+ // Hanging specifies how much to indent all but the first line
+ // in a paragraph (inherited).
+ Hanging units.Value
+
// TabSize specifies the tab size, in number of characters (inherited).
TabSize int
@@ -67,6 +74,8 @@ func (ts *Text) Defaults() {
// ToDots runs ToDots on unit values, to compile down to raw pixels
func (ts *Text) ToDots(uc *units.Context) {
+ ts.Indent.ToDots(uc)
+ ts.Hanging.ToDots(uc)
}
// InheritFields from parent
@@ -76,6 +85,8 @@ func (ts *Text) InheritFields(parent *Text) {
ts.LineHeight = parent.LineHeight
// ts.WhiteSpace = par.WhiteSpace // note: we can't inherit this b/c label base default then gets overwritten
ts.Direction = parent.Direction
+ ts.Indent = parent.Indent
+ ts.Hanging = parent.Hanging
ts.TabSize = parent.TabSize
ts.SelectColor = parent.SelectColor
ts.HighlightColor = parent.HighlightColor
@@ -88,6 +99,8 @@ func (ts *Text) SetText(tsty *text.Style) {
tsty.LineHeight = ts.LineHeight
tsty.WhiteSpace = ts.WhiteSpace
tsty.Direction = ts.Direction
+ tsty.Indent = ts.Indent
+ tsty.Hanging = ts.Hanging
tsty.TabSize = ts.TabSize
tsty.SelectColor = ts.SelectColor
tsty.HighlightColor = ts.HighlightColor
@@ -100,6 +113,8 @@ func (ts *Text) SetFromText(tsty *text.Style) {
ts.LineHeight = tsty.LineHeight
ts.WhiteSpace = tsty.WhiteSpace
ts.Direction = tsty.Direction
+ ts.Indent = tsty.Indent
+ ts.Hanging = tsty.Hanging
ts.TabSize = tsty.TabSize
ts.SelectColor = tsty.SelectColor
ts.HighlightColor = tsty.HighlightColor
diff --git a/styles/units/context.go b/styles/units/context.go
index 8363f3bc9e..8f242104c3 100644
--- a/styles/units/context.go
+++ b/styles/units/context.go
@@ -9,8 +9,8 @@ import (
"cogentcore.org/core/math32"
)
-// Context specifies everything about the current context necessary for converting the number
-// into specific display-dependent pixels
+// Context specifies everything about the current context necessary
+// for converting the number into specific display-dependent pixels.
type Context struct {
// DPI is dots-per-inch of the display
@@ -47,6 +47,12 @@ type Context struct {
Pah float32
}
+func NewContext() *Context {
+ uc := &Context{}
+ uc.Defaults()
+ return uc
+}
+
// Defaults are generic defaults
func (uc *Context) Defaults() {
uc.DPI = DpPerInch
@@ -124,7 +130,7 @@ func (uc *Context) SetFont(em float32) {
uc.FontRem = math32.Round(uc.Dp(16))
}
-// ToDotsFact returns factor needed to convert given unit into raw pixels (dots in DPI)
+// Dots returns factor needed to convert given unit into raw pixels (dots in DPI)
func (uc *Context) Dots(un Units) float32 {
if uc.DPI == 0 {
// log.Printf("gi/units Context was not initialized -- falling back on defaults\n")
@@ -177,11 +183,21 @@ func (uc *Context) Dots(un Units) float32 {
return uc.DPI
}
-// ToDots converts value in given units into raw display pixels (dots in DPI)
+// ToDots converts value in given units into raw display pixels (dots in DPI).
func (uc *Context) ToDots(val float32, un Units) float32 {
return val * uc.Dots(un)
}
+// FromDots converts value in dots to value in given units.
+func (uc *Context) FromDots(val float32, un Units) float32 {
+ return val / uc.Dots(un)
+}
+
+// Convert converts value in given from units to value in given to units.
+func (uc *Context) Convert(val float32, from, to Units) float32 {
+ return val * (uc.Dots(from) / uc.Dots(to))
+}
+
// PxToDots just converts a value from pixels to dots
func (uc *Context) PxToDots(val float32) float32 {
return val * uc.Dots(UnitPx)
diff --git a/styles/units/units_test.go b/styles/units/units_test.go
index 55760b9c8c..31c68d8ec7 100644
--- a/styles/units/units_test.go
+++ b/styles/units/units_test.go
@@ -71,4 +71,7 @@ func TestValueConvert(t *testing.T) {
if s1 != s2 {
t.Errorf("strings don't match: %v != %v\n", s1, s2)
}
+
+ tolassert.Equal(t, 72, ctxt.Convert(1, UnitIn, UnitPt))
+ tolassert.Equal(t, 25.4/72.0, ctxt.Convert(1, UnitPt, UnitMm))
}
diff --git a/svg/circle.go b/svg/circle.go
index 6c230def5b..52c44450f8 100644
--- a/svg/circle.go
+++ b/svg/circle.go
@@ -59,7 +59,7 @@ func (g *Circle) ApplyTransform(sv *SVG, xf math32.Matrix2) {
g.Paint.Transform.SetMul(xf)
g.SetTransformProperty()
} else {
- g.Pos = xf.MulVector2AsPoint(g.Pos)
+ g.Pos = xf.MulPoint(g.Pos)
scx, scy := xf.ExtractScale()
g.Radius *= 0.5 * (scx + scy)
g.GradientApplyTransform(sv, xf)
diff --git a/svg/ellipse.go b/svg/ellipse.go
index 74826dd920..dc3f354020 100644
--- a/svg/ellipse.go
+++ b/svg/ellipse.go
@@ -60,8 +60,8 @@ func (g *Ellipse) ApplyTransform(sv *SVG, xf math32.Matrix2) {
g.SetTransformProperty()
} else {
// todo: this is not the correct transform:
- g.Pos = xf.MulVector2AsPoint(g.Pos)
- g.Radii = xf.MulVector2AsVector(g.Radii)
+ g.Pos = xf.MulPoint(g.Pos)
+ g.Radii = xf.MulVector(g.Radii)
g.GradientApplyTransform(sv, xf)
}
}
diff --git a/svg/image.go b/svg/image.go
index 345d88c3a2..1d391b1d5f 100644
--- a/svg/image.go
+++ b/svg/image.go
@@ -130,8 +130,8 @@ func (g *Image) ApplyTransform(sv *SVG, xf math32.Matrix2) {
g.Paint.Transform.SetMul(xf)
g.SetTransformProperty()
} else {
- g.Pos = xf.MulVector2AsPoint(g.Pos)
- g.Size = xf.MulVector2AsVector(g.Size)
+ g.Pos = xf.MulPoint(g.Pos)
+ g.Size = xf.MulVector(g.Size)
g.GradientApplyTransform(sv, xf)
}
}
diff --git a/svg/io.go b/svg/io.go
index 54e4a28234..f4bbb2bb10 100644
--- a/svg/io.go
+++ b/svg/io.go
@@ -18,6 +18,7 @@ import (
"io/fs"
"log"
"os"
+ "slices"
"strings"
"cogentcore.org/core/base/iox/imagex"
@@ -974,8 +975,12 @@ func MarshalXML(n tree.Node, enc *XMLEncoder, setName string) string {
for k, v := range properties {
sv := reflectx.ToString(v)
switch k {
- case "opacity", "transform":
+ case "opacity":
XMLAddAttr(&se.Attr, k, sv)
+ case "transform":
+ if slices.IndexFunc(se.Attr, func(a xml.Attr) bool { return a.Name.Local == k }) < 0 {
+ XMLAddAttr(&se.Attr, k, sv)
+ }
case "groupmode":
XMLAddAttr(&se.Attr, "inkscape:groupmode", sv)
if st, has := properties["style"]; has {
diff --git a/svg/line.go b/svg/line.go
index 9d5ce2a953..f58f20324a 100644
--- a/svg/line.go
+++ b/svg/line.go
@@ -72,8 +72,8 @@ func (g *Line) ApplyTransform(sv *SVG, xf math32.Matrix2) {
g.Paint.Transform.SetMul(xf)
g.SetTransformProperty()
} else {
- g.Start = xf.MulVector2AsPoint(g.Start)
- g.End = xf.MulVector2AsPoint(g.End)
+ g.Start = xf.MulPoint(g.Start)
+ g.End = xf.MulPoint(g.End)
g.GradientApplyTransform(sv, xf)
}
}
diff --git a/svg/node.go b/svg/node.go
index ce7294efc1..882534f948 100644
--- a/svg/node.go
+++ b/svg/node.go
@@ -177,8 +177,8 @@ func (g *NodeBase) ApplyTransform(sv *SVG, xf math32.Matrix2) {
// operating around given reference point which serves as the effective origin for rotation.
func (g *NodeBase) DeltaTransform(trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2) math32.Matrix2 {
mxi := g.ParentTransform(true).Inverse()
- lpt := mxi.MulVector2AsPoint(pt)
- ltr := mxi.MulVector2AsVector(trans)
+ lpt := mxi.MulPoint(pt)
+ ltr := mxi.MulVector(trans)
xf := math32.Translate2D(lpt.X, lpt.Y).Scale(scale.X, scale.Y).Rotate(rot).Translate(ltr.X, ltr.Y).Translate(-lpt.X, -lpt.Y)
return xf
}
diff --git a/svg/polyline.go b/svg/polyline.go
index 13f6fe1c23..5918c972f7 100644
--- a/svg/polyline.go
+++ b/svg/polyline.go
@@ -80,7 +80,7 @@ func (g *Polyline) ApplyTransform(sv *SVG, xf math32.Matrix2) {
g.SetTransformProperty()
} else {
for i, p := range g.Points {
- p = xf.MulVector2AsPoint(p)
+ p = xf.MulPoint(p)
g.Points[i] = p
}
g.GradientApplyTransform(sv, xf)
diff --git a/svg/rect.go b/svg/rect.go
index 322027a5ad..b5b89562cc 100644
--- a/svg/rect.go
+++ b/svg/rect.go
@@ -70,8 +70,8 @@ func (g *Rect) ApplyTransform(sv *SVG, xf math32.Matrix2) {
g.Paint.Transform.SetMul(xf)
g.SetTransformProperty()
} else {
- g.Pos = xf.MulVector2AsPoint(g.Pos)
- g.Size = xf.MulVector2AsVector(g.Size)
+ g.Pos = xf.MulPoint(g.Pos)
+ g.Size = xf.MulVector(g.Size)
g.GradientApplyTransform(sv, xf)
}
}
diff --git a/svg/svg.go b/svg/svg.go
index e7f3ca0026..c18d720885 100644
--- a/svg/svg.go
+++ b/svg/svg.go
@@ -10,9 +10,11 @@ import (
"bytes"
"image"
"image/color"
+ "os"
"strings"
"sync"
+ "cogentcore.org/core/base/errors"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
@@ -281,6 +283,32 @@ func (sv *SVG) SaveImageSize(fname string, width, height float32) error {
return err
}
+// SavePDF renders the SVG to a PDF and saves it to given filename.
+func (sv *SVG) SavePDF(fname string) error {
+ pos := sv.Geom.Pos
+ sv.Geom.Pos = image.Point{}
+ rend := sv.Render(nil).RenderDone()
+
+ ctx := units.NewContext()
+ vsz := math32.FromPoint(sv.Geom.Size)
+ pd := paint.NewPDFRenderer(vsz, ctx)
+ pd.Render(rend)
+ err := os.WriteFile(fname, pd.Source(), 0666)
+
+ sv.Geom.Pos = pos
+ return err
+}
+
+// CloneSVG returns a copy of this SVG, with given size,
+// by writing and reading the XML source.
+func (sv *SVG) CloneSVG(size math32.Vector2) *SVG {
+ var b bytes.Buffer
+ errors.Log(sv.WriteXML(&b, false))
+ nsv := NewSVG(size)
+ errors.Log(nsv.ReadXML(&b))
+ return nsv
+}
+
func (sv *SVG) FillViewport() {
sty := styles.NewPaint() // has no transform
pc := &paint.Painter{sv.painter.State, sty}
diff --git a/svg/svg_test.go b/svg/svg_test.go
index 2ce2f9529a..9784469620 100644
--- a/svg/svg_test.go
+++ b/svg/svg_test.go
@@ -18,29 +18,50 @@ import (
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/cam/hct"
"cogentcore.org/core/math32"
+ "cogentcore.org/core/paint"
_ "cogentcore.org/core/paint/renderers" // installs default renderer
+ "cogentcore.org/core/styles/units"
. "cogentcore.org/core/svg"
"github.com/go-text/typesetting/font"
"github.com/stretchr/testify/assert"
)
func RunTest(t *testing.T, width, height int, dir, fname string) {
- sv := NewSVG(math32.Vec2(float32(width), float32(height)))
+ // cset := pdf.UseStandardFonts()
+
+ size := math32.Vec2(float32(width), float32(height))
+ sv := NewSVG(size)
svfn := filepath.Join("testdata", dir, fname)
err := sv.OpenXML(svfn)
assert.NoError(t, err)
- img := sv.RenderImage()
- imfn := filepath.Join(dir, "png", strings.TrimSuffix(fname, ".svg"))
+ rend := sv.Render(nil).RenderDone()
+
+ rd := paint.NewImageRenderer(size)
+ img := rd.Render(rend).Image()
+ bnm := strings.TrimSuffix(fname, ".svg")
+ imfn := filepath.Join("png", dir, bnm)
// fmt.Println(svfn, imfn)
imagex.Assert(t, img, imfn)
+
+ pddir := filepath.Join("testdata", "pdf")
+ pdfn := filepath.Join(pddir, dir, bnm+".pdf")
+ ctx := units.NewContext()
+ pd := paint.NewPDFRenderer(size, ctx)
+ pd.Render(rend)
+ os.MkdirAll(filepath.Join(pddir, dir), 0777)
+ err = os.WriteFile(pdfn, pd.Source(), 0666)
+ assert.NoError(t, err)
+
+ // pdf.RestorePreviousFonts(cset)
}
func TestSVG(t *testing.T) {
dir := "svg"
files := fsx.Filenames(filepath.Join("testdata", dir), ".svg")
+ // PDF currently failing: TestShapes4, 6,
for _, fn := range files {
- // if fn != "fig_neuron_as_detect.svg" {
+ // if fn != "fig_neuron_as_detect_test.svg" {
// continue
// }
RunTest(t, 640, 480, dir, fn)
@@ -65,7 +86,7 @@ func TestViewBox(t *testing.T) {
img := sv.RenderImage()
fnm := fmt.Sprintf("%s_%s", fpre, ts)
- imfn := filepath.Join("svg", "png", "viewbox", fnm)
+ imfn := filepath.Join("png", "viewbox", fnm)
// fmt.Println(imfn)
imagex.Assert(t, img, imfn)
}
@@ -174,7 +195,7 @@ func TestFontEmoji(t *testing.T) {
err := sv.ReadXML(b)
assert.NoError(t, err)
img := sv.RenderImage()
- imfn := filepath.Join("svg", "png", "font-emoji", strings.TrimSuffix(fn, ".svg"))
+ imfn := filepath.Join("png", "font-emoji", strings.TrimSuffix(fn, ".svg"))
imagex.Assert(t, img, imfn)
// sv.SaveXML(sfn)
// os.WriteFile(sfn, gd.Source, 0666)
diff --git a/svg/testdata/svg/fig_bp_compute_delta.svg b/svg/testdata/svg/fig_bp_compute_delta.svg
index 4e0bb1eebd..6dc1a7b295 100644
--- a/svg/testdata/svg/fig_bp_compute_delta.svg
+++ b/svg/testdata/svg/fig_bp_compute_delta.svg
@@ -8,23 +8,37 @@
xmlns="http://www.w3.org/2000/svg">
+
+
+
+
@@ -34,20 +48,6 @@
transform="scale(0.4) rotate(180) translate(10,0)"
d="M0 0L5 -5L-12.5 0L5 5z" />
-
-
-
-
@@ -239,10 +239,10 @@
+ style="inkscape:zoom:1;inkscape:document-units:px;inkscape:current-layer:layer1;inkscape:cx:0;inkscape:cy:0;">
+ style="spacingx:32;spacingy:32;type:xygrid;units:px;" />
@@ -678,94 +673,89 @@
@@ -782,7 +772,7 @@
-
-
\ No newline at end of file
+ width="353.75"
+ height="196.25"
+ viewBox="0 0 384 224"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Integration
+
+
+ Output
+
+
+ Inputs
+
+
+ Detector
+
+
+ Neuron
+
+
+ Synapses
+
+
+ Dendrites
+
+
+ Axon
+
+
+ Cell body
+ (membrane
+ potential)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/svg/testdata/svg/test.svg b/svg/testdata/svg/test.svg
index 5679868eea..1630246cc4 100644
--- a/svg/testdata/svg/test.svg
+++ b/svg/testdata/svg/test.svg
@@ -1,37 +1,49 @@
-
diff --git a/svg/text.go b/svg/text.go
index 4cb4157f81..4b44c9c4f8 100644
--- a/svg/text.go
+++ b/svg/text.go
@@ -8,7 +8,6 @@ import (
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/text/htmltext"
- "cogentcore.org/core/text/rich"
"cogentcore.org/core/text/shaped"
"cogentcore.org/core/text/text"
)
@@ -75,7 +74,7 @@ func (g *Text) SetNodePos(pos math32.Vector2) {
g.Pos = pos
for _, kii := range g.Children {
kt := kii.(*Text)
- kt.Pos = g.Paint.Transform.MulVector2AsPoint(pos)
+ kt.Pos = g.Paint.Transform.MulPoint(pos)
}
}
@@ -92,7 +91,7 @@ func (g *Text) LocalBBox(sv *SVG) math32.Box2 {
tx, _ := htmltext.HTMLToRich([]byte(g.Text), &fs, nil)
// fmt.Println(tx)
sz := math32.Vec2(10000, 10000) // no wrapping!!
- g.TextShaped = sv.TextShaper.WrapLines(tx, &fs, &pc.Text, &rich.DefaultSettings, sz)
+ g.TextShaped = sv.TextShaper.WrapLines(tx, &fs, &pc.Text, sz)
baseOff := g.TextShaped.Lines[0].Offset
g.TextShaped.StartAtBaseline() // remove top-left offset
return g.TextShaped.Bounds.Translate(g.Pos.Sub(baseOff))
@@ -194,7 +193,7 @@ func (g *Text) ApplyTransform(sv *SVG, xf math32.Matrix2) {
g.Paint.Transform.SetMul(xf)
g.SetTransformProperty()
} else {
- g.Pos = xf.MulVector2AsPoint(g.Pos)
+ g.Pos = xf.MulPoint(g.Pos)
g.GradientApplyTransform(sv, xf)
}
}
diff --git a/svg/zoom.go b/svg/zoom.go
index 1669618786..338d598417 100644
--- a/svg/zoom.go
+++ b/svg/zoom.go
@@ -112,12 +112,12 @@ func (sv *SVG) ScaleAt(pt image.Point, sc float32) {
sv.setRootTransform()
rxf := sv.Root.Paint.Transform.Inverse()
mpt := math32.FromPoint(pt)
- xpt := rxf.MulVector2AsPoint(mpt)
+ xpt := rxf.MulPoint(mpt)
sv.Scale = sc
sv.setRootTransform()
rxf = sv.Root.Paint.Transform
- npt := rxf.MulVector2AsPoint(xpt) // original point back to screen
+ npt := rxf.MulPoint(xpt) // original point back to screen
dpt := mpt.Sub(npt)
sv.Translate.SetAdd(dpt)
}
diff --git a/system/app.go b/system/app.go
index 2a3754ccfb..f5c0eccceb 100644
--- a/system/app.go
+++ b/system/app.go
@@ -61,6 +61,10 @@ type App interface {
// returns the platform of the underlying operating system.
SystemPlatform() Platforms
+ // SystemLocale returns the https://www.rfc-editor.org/rfc/bcp/bcp47.txt standard
+ // language tag, consisting of language and region, e.g., "en-US", "fr-FR", "ja-JP".
+ SystemLocale() Locale
+
// SystemInfo returns any additional information about the underlying system
// that is not otherwise given. It is used in crash logs.
SystemInfo() string
diff --git a/system/driver/base/app.go b/system/driver/base/app.go
index 11969dcdbf..10c3e34f2f 100644
--- a/system/driver/base/app.go
+++ b/system/driver/base/app.go
@@ -20,6 +20,8 @@ import (
"cogentcore.org/core/events/key"
"cogentcore.org/core/styles"
"cogentcore.org/core/system"
+ "cogentcore.org/core/text/printer"
+ "github.com/jeandeaual/go-locale"
)
// App contains the data and logic common to all implementations of [system.App].
@@ -67,6 +69,9 @@ func Init(a system.App, ab *App) {
ab.This = a
system.TheApp = a
key.SystemPlatform = a.SystemPlatform().String()
+ // sl := a.SystemLocale()
+ // fmt.Println("locale:", sl, sl.Language(), sl.Region())
+ printer.Settings.Defaults() // depends on system.TheApp
}
func (a *App) MainLoop() {
@@ -119,6 +124,10 @@ func (a *App) SystemInfo() string {
return "" // no-op by default
}
+func (a *App) SystemLocale() system.Locale {
+ return system.Locale(errors.Log1(locale.GetLocale()))
+}
+
func (a *App) Name() string {
return a.Nm
}
diff --git a/system/locale.go b/system/locale.go
new file mode 100644
index 0000000000..0ae52e8191
--- /dev/null
+++ b/system/locale.go
@@ -0,0 +1,35 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package system
+
+import "strings"
+
+// Locale represents a https://www.rfc-editor.org/rfc/bcp/bcp47.txt standard
+// language tag, consisting of language and region, e.g., "en-US", "fr-FR", "ja-JP".
+type Locale string
+
+// Language returns the language portion of the locale tag (e.g., en, fr, ja)
+func (l Locale) Language() string {
+ if l == "" {
+ return ""
+ }
+ pos := strings.LastIndex(string(l), "-")
+ if pos < 0 {
+ return string(l)
+ }
+ return string(l)[:pos]
+}
+
+// Region returns the region portion of the locale tag (e.g., US, FR, JA)
+func (l Locale) Region() string {
+ if l == "" {
+ return ""
+ }
+ pos := strings.LastIndex(string(l), "-")
+ if pos < 0 {
+ return ""
+ }
+ return string(l)[pos+1:]
+}
diff --git a/text/csl/apa.go b/text/csl/apa.go
index d1278e4c67..a25ac8e63d 100644
--- a/text/csl/apa.go
+++ b/text/csl/apa.go
@@ -72,8 +72,8 @@ func RefLinks(it *Item, tx *rich.Text) {
tx.AddLink(link, it.URL, it.URL)
}
if it.DOI != "" {
- url := " http://doi.org/" + it.DOI
- tx.AddLink(link, url, url)
+ url := "http://doi.org/" + it.DOI
+ tx.AddLink(link, url, " "+url)
}
}
@@ -118,7 +118,7 @@ func RefAPABook(it *Item) rich.Text {
}
}
if it.Publisher != "" {
- tx.AddSpanString(sty, EnsurePeriod(it.Publisher)+" ")
+ tx.AddSpanString(sty, " "+EnsurePeriod(it.Publisher)+" ")
}
RefLinks(it, &tx)
return tx
diff --git a/text/csl/keylist.go b/text/csl/keylist.go
index 69415be250..fdb08e7f58 100644
--- a/text/csl/keylist.go
+++ b/text/csl/keylist.go
@@ -28,10 +28,12 @@ func NewKeyList(items []Item) *KeyList {
return kl
}
-// AlphaKeys returns an alphabetically sorted list of keys.
+// AlphaKeys returns an alphabetically sorted list of keys (case insensitive).
func (kl *KeyList) AlphaKeys() []string {
ks := slices.Clone(kl.Keys)
- slices.Sort(ks)
+ slices.SortFunc(ks, func(a, b string) int {
+ return strings.Compare(strings.ToLower(a), strings.ToLower(b))
+ })
return ks
}
diff --git a/text/csl/md.go b/text/csl/md.go
index d60e934c11..00d5c10a3b 100644
--- a/text/csl/md.go
+++ b/text/csl/md.go
@@ -7,50 +7,30 @@ package csl
import (
"bufio"
"io"
- "os"
- "path/filepath"
+ "io/fs"
"regexp"
"strings"
"cogentcore.org/core/base/errors"
- "cogentcore.org/core/base/fsx"
+ "cogentcore.org/core/text/htmltext"
)
// GenerateMarkdown extracts markdown citations in the format [@Ref; @Ref]
-// from .md markdown files in given directory, looking up in given source [KeyList],
-// and writing the results in given style to given .md file (references.md default).
+// from given mds markdown files in given filesystem (use os.DirFS for filesys),
+// looking up in given source [KeyList], and writing the results in given style to writer.
// Heading is written first: must include the appropriate markdown heading level
// (## typically). Returns the [KeyList] of references that were cited.
-func GenerateMarkdown(dir, refFile, heading string, kl *KeyList, sty Styles) (*KeyList, error) {
+func GenerateMarkdown(w io.Writer, fsys fs.FS, heading string, kl *KeyList, sty Styles, mds ...string) (*KeyList, error) {
cited := &KeyList{}
- if dir == "" {
- dir = "./"
- }
- mds := fsx.Filenames(dir, ".md")
- if len(mds) == 0 {
- return cited, errors.New("No .md files found in: " + dir)
- }
var errs []error
- for i := range mds {
- mds[i] = filepath.Join(dir, mds[i])
- }
- err := ExtractMarkdownCites(mds, kl, cited)
- if err != nil {
- errs = append(errs, err)
- }
- if refFile == "" {
- refFile = filepath.Join(dir, "references.md")
- }
- of, err := os.Create(refFile)
+ err := ExtractMarkdownCites(fsys, mds, kl, cited)
if err != nil {
errs = append(errs, err)
- return cited, errors.Join(errs...)
}
- defer of.Close()
if heading != "" {
- of.WriteString(heading + "\n\n")
+ w.Write([]byte(heading + "\n\n"))
}
- err = WriteRefsMarkdown(of, cited, sty)
+ err = WriteRefsMarkdown(w, cited, sty)
if err != nil {
errs = append(errs, err)
}
@@ -58,12 +38,13 @@ func GenerateMarkdown(dir, refFile, heading string, kl *KeyList, sty Styles) (*K
}
// ExtractMarkdownCites extracts markdown citations in the format [@Ref; @Ref]
-// from given list of .md files, looking up in given source [KeyList], adding to cited.
-func ExtractMarkdownCites(files []string, src, cited *KeyList) error {
+// from given list of .md files in given FS,
+// looking up in given source [KeyList], adding to cited.
+func ExtractMarkdownCites(fsys fs.FS, files []string, src, cited *KeyList) error {
exp := regexp.MustCompile(`\[(@\^?([[:alnum:]]+-?)+(;[[:blank:]]+)?)+\]`)
var errs []error
for _, fn := range files {
- f, err := os.Open(fn)
+ f, err := fsys.Open(fn)
if err != nil {
errs = append(errs, err)
continue
@@ -108,7 +89,8 @@ func WriteRefsMarkdown(w io.Writer, kl *KeyList, sty Styles) error {
if err != nil {
return err
}
- _, err = w.Write([]byte(string(ref.Join()) + "\n\n")) // todo: ref to markdown!!
+ str := htmltext.RichToHTML(ref)
+ _, err = w.Write([]byte(str + "\n\n")) // todo: ref to markdown!!
if err != nil {
return err
}
diff --git a/text/csl/mdcite/mdcite.go b/text/csl/mdcite/mdcite.go
index 1e22b7c540..0fed65513f 100644
--- a/text/csl/mdcite/mdcite.go
+++ b/text/csl/mdcite/mdcite.go
@@ -5,6 +5,11 @@
package main
import (
+ "os"
+ "path/filepath"
+
+ "cogentcore.org/core/base/errors"
+ "cogentcore.org/core/base/fsx"
"cogentcore.org/core/cli"
"cogentcore.org/core/text/csl"
)
@@ -45,7 +50,24 @@ func Generate(c *Config) error {
return err
}
kl := csl.NewKeyList(refs)
- cited, err := csl.GenerateMarkdown(c.Dir, c.Output, c.Heading, kl, c.Style)
+
+ if c.Dir == "" {
+ c.Dir = "./"
+ }
+ mds := fsx.Filenames(c.Dir, ".md")
+ if len(mds) == 0 {
+ return errors.New("No .md files found in: " + c.Dir)
+ }
+ if c.Output == "" {
+ c.Output = filepath.Join(c.Dir, "references.md")
+ }
+ of, err := os.Create(c.Output)
+ if errors.Log(err) != nil {
+ return err
+ }
+ defer of.Close()
+
+ cited, err := csl.GenerateMarkdown(of, os.DirFS(c.Dir), c.Heading, kl, c.Style, mds...)
cf := c.CitedData
if cf == "" {
cf = "citedrefs.json"
diff --git a/text/fonts/fontbrowser/glyph.go b/text/fonts/fontbrowser/glyph.go
index 329cc77d9f..f14b25be4f 100644
--- a/text/fonts/fontbrowser/glyph.go
+++ b/text/fonts/fontbrowser/glyph.go
@@ -131,7 +131,7 @@ func (gi *Glyph) drawShaped(pc *paint.Painter) {
sty.Size = float32(msz) / tsty.FontSize.Dots
sty.Size *= 0.85
tx := rich.NewText(sty, []rune{gi.Rune})
- lns := gi.Scene.TextShaper().WrapLines(tx, sty, tsty, &core.AppearanceSettings.Text, sz)
+ lns := gi.Scene.TextShaper().WrapLines(tx, sty, tsty, sz)
off := math32.Vec2(0, 0)
if msz > 200 {
o := 0.2 * float32(msz)
diff --git a/text/htmltext/tohtml.go b/text/htmltext/tohtml.go
index 488bb64446..93375640c4 100644
--- a/text/htmltext/tohtml.go
+++ b/text/htmltext/tohtml.go
@@ -14,23 +14,27 @@ import (
func RichToHTML(tx rich.Text) string {
var b strings.Builder
ns := tx.NumSpans()
- var lsty *rich.Style
+ lsty := rich.NewStyle()
for si := range ns {
sty, rs := tx.Span(si)
var stags, etags string
- if sty.Weight != rich.Normal && (lsty == nil || lsty.Weight != sty.Weight) {
+ if sty.Special == rich.Link {
+ b.WriteString(`` + string(rs) + ``)
+ continue
+ }
+ if sty.Weight != rich.Normal && lsty.Weight != sty.Weight {
stags += "<" + sty.Weight.HTMLTag() + ">"
- } else if sty.Weight == rich.Normal && (lsty != nil && lsty.Weight != sty.Weight) {
+ } else if sty.Weight == rich.Normal && lsty.Weight != sty.Weight {
etags += "" + lsty.Weight.HTMLTag() + ">"
}
- if sty.Slant != rich.SlantNormal && (lsty == nil || lsty.Slant != sty.Slant) {
+ if sty.Slant != rich.SlantNormal && lsty.Slant != sty.Slant {
stags += ""
- } else if sty.Slant == rich.SlantNormal && lsty != nil && lsty.Slant != sty.Slant {
+ } else if sty.Slant == rich.SlantNormal && lsty.Slant != sty.Slant {
etags += ""
}
- if sty.Decoration.HasFlag(rich.Underline) && (lsty == nil || !lsty.Decoration.HasFlag(rich.Underline)) {
+ if sty.Decoration.HasFlag(rich.Underline) && !lsty.Decoration.HasFlag(rich.Underline) {
stags += ""
- } else if !sty.Decoration.HasFlag(rich.Underline) && lsty != nil && lsty.Decoration.HasFlag(rich.Underline) {
+ } else if !sty.Decoration.HasFlag(rich.Underline) && lsty.Decoration.HasFlag(rich.Underline) {
etags += ""
}
b.WriteString(etags)
@@ -38,5 +42,14 @@ func RichToHTML(tx rich.Text) string {
b.WriteString(string(rs))
lsty = sty
}
+ if lsty.Slant == rich.Italic {
+ b.WriteString("")
+ }
+ if lsty.Weight != rich.Normal {
+ b.WriteString("" + lsty.Weight.HTMLTag() + ">")
+ }
+ if lsty.Decoration.HasFlag(rich.Underline) {
+ b.WriteString("")
+ }
return b.String()
}
diff --git a/text/paginate/README.md b/text/paginate/README.md
new file mode 100644
index 0000000000..4e3e8aed23
--- /dev/null
+++ b/text/paginate/README.md
@@ -0,0 +1,24 @@
+# Paginate
+
+The `paginate` package takes a set of input Widget trees and returns a corresponding set of page Frame widgets that fit within a specified height, with optional title, headers and footers.
+
+The main purpose is for generating PDF output, via the `PDF` function, which installs default PDF fonts (Helvetica, Times, Courier) and renders output.
+
+The first step involves extracting a list of leaf-level widgets from surrounding core.Frame elements, that are then processed by the layout function to fit into page-sized chunks. This can be controlled by the properties as described below.
+
+## Properties
+
+Properties can be set on widgets to inform the pagination process. This is done by the `content` package, for example. All properties start with `paginate-`.
+
+* `block` -- marks a Frame as a block that is not to be further extracted from in collecting leaves. Only Frame elements that have direction = Column are
+
+* `break` -- starts a new page before this element.
+
+* `no-break-after` -- marks an element to not have a page break inserted after it (e.g., for a header).
+
+## Options and settings
+
+The `Options` type specifies a number of optional formatting elements, which are used by the PDF generator to insert a title and running headers and footers, according to functions that can add any kind of text. See `runners.go` for some examples.
+
+The `printer.Settings` settings are used to determine page size and font formatting defaults.
+
diff --git a/text/paginate/extract.go b/text/paginate/extract.go
new file mode 100644
index 0000000000..a4381aa9d7
--- /dev/null
+++ b/text/paginate/extract.go
@@ -0,0 +1,92 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package paginate
+
+import (
+ "cogentcore.org/core/base/stack"
+ "cogentcore.org/core/core"
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/styles"
+)
+
+// item is one layout item
+type item struct {
+ w core.Widget
+ gap math32.Vector2 // gap to add before this element
+ left float32 // left-side margin from parent frame
+}
+
+func (it *item) String() string {
+ return core.AsWidget(it.w).String()
+}
+
+// extract returns the widget chunks to actually paginate.
+func (p *pager) extract() []*item {
+ widg := core.AsWidget
+
+ ii := 0
+ type posn struct {
+ w core.Widget
+ i int
+ }
+
+ pars := stack.Stack[*posn]{} // stack of parents that we are iterating
+ pars.Push(&posn{p.ins[ii], 0})
+ atEnd := false
+
+ next := func() {
+ start:
+ cp := pars.Peek()
+ cp.i++
+ if cp.i >= cp.w.AsWidget().NumChildren() {
+ pars.Pop()
+ if len(pars) == 0 {
+ ii++
+ if ii >= len(p.ins) {
+ atEnd = true
+ return
+ }
+ pars.Push(&posn{p.ins[ii], 0})
+ return
+ } else {
+ goto start
+ }
+ }
+ }
+
+ var its []*item
+ for {
+ cp := pars.Peek()
+ cpw := cp.w.AsWidget()
+ if cp.i >= cpw.NumChildren() {
+ next()
+ if atEnd {
+ break
+ }
+ continue
+ }
+ gap := cpw.Styles.Gap.Dots().Floor()
+ left := cpw.Styles.Padding.Left.Dots
+ if cp.i == 0 {
+ gap.Y = 0
+ }
+ cw := widg(cpw.Child(cp.i))
+ if fr, ok := cw.This.(*core.Frame); ok {
+ if fr.Styles.Direction == styles.Column {
+ if fr.Property("paginate-block") == nil {
+ pars.Push(&posn{fr.This.(core.Widget), 0})
+ continue
+ }
+ }
+ }
+ its = append(its, &item{w: cw.This.(core.Widget), gap: gap, left: left})
+ next()
+ if atEnd {
+ break
+ }
+ }
+ // fmt.Println("its:", its)
+ return its
+}
diff --git a/text/paginate/layout.go b/text/paginate/layout.go
new file mode 100644
index 0000000000..85df16c464
--- /dev/null
+++ b/text/paginate/layout.go
@@ -0,0 +1,133 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package paginate
+
+import (
+ "cogentcore.org/core/core"
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/styles"
+ "cogentcore.org/core/tree"
+)
+
+// pagify organizes widget list into page-sized chunks.
+func (p *pager) pagify(its []*item) [][]*item {
+ widg := core.AsWidget
+ size := func(it *item) float32 {
+ wb := widg(it.w)
+ ih := wb.Geom.Size.Actual.Total.Y // todo: something wrong with size!
+ return ih + it.gap.Y
+ }
+
+ maxY := p.opts.BodyDots.Y
+
+ var pgs [][]*item
+ var cpg []*item
+ ht := float32(0)
+ n := len(its)
+ for i, ci := range its {
+ cw := widg(ci.w)
+ // fmt.Println(cw)
+ brk := cw.Property("paginate-break") != nil
+ nobrk := cw.Property("paginate-no-break-after") != nil
+ sz := size(ci)
+ over := ht+sz > maxY
+ if !over && nobrk {
+ if i < n-1 {
+ nsz := size(its[i+1]) // extra space to be sure
+ if ht+sz+nsz > maxY {
+ over = true // break now
+ // } else {
+ // fmt.Println("not !over, nobrk:", ht, sz, nsz, maxY, ht+sz+nsz, cw, its[i+1])
+ }
+ }
+ }
+ if brk || over {
+ ht = 0
+ if !brk && len(cpg) == 0 { // no blank pages!
+ cpg = append(cpg, ci)
+ pgs = append(pgs, cpg)
+ cpg = nil
+ continue
+ }
+ pgs = append(pgs, cpg)
+ cpg = nil
+ }
+ ht += sz
+ cpg = append(cpg, ci)
+ }
+ if len(cpg) > 0 {
+ pgs = append(pgs, cpg)
+ }
+ return pgs
+}
+
+func (p *pager) outputPages(pgs [][]*item, newPage func(gap math32.Vector2, pageNo int) (page, body *core.Frame)) []*core.Frame {
+ var outs []*core.Frame
+ for pn, pg := range pgs {
+ lastGap := math32.Vector2{}
+ lastLeft := float32(0)
+ if len(pg) > 0 {
+ lastGap = pg[0].gap
+ }
+ page, body := newPage(lastGap, pn+1)
+ cpar := body
+ for _, it := range pg {
+ gap := it.gap
+ left := it.left
+ if gap != lastGap || left != lastLeft {
+ cpar = p.newOutFrame(body, gap, left)
+ lastGap = gap
+ lastLeft = left
+ }
+ tree.MoveToParent(it.w, cpar)
+ }
+ outs = append(outs, page)
+ }
+ return outs
+}
+
+func (p *pager) newOutFrame(par *core.Frame, gap math32.Vector2, left float32) *core.Frame {
+ var fr *core.Frame
+ if par != nil {
+ fr = core.NewFrame(par)
+ } else {
+ fr = core.NewFrame()
+ }
+ fr.Styler(func(s *styles.Style) {
+ s.Direction = styles.Column
+ s.ZeroSpace()
+ s.Min.X.Dot(p.opts.BodyDots.X)
+ s.Max.X.Dot(p.opts.BodyDots.X)
+ s.Gap.X.Dot(gap.X)
+ s.Gap.Y.Dot(gap.Y)
+ s.Padding.Left.Dot(core.ConstantSpacing(left))
+ })
+ return fr
+}
+
+// pre-render everything in the offscreen scene that will be used for final
+// to get accurate element sizes.
+func (p *pager) preRender(its []*item) {
+ pg := [][]*item{its}
+ op := p.outputPages(pg, func(gap math32.Vector2, pageNo int) (page, body *core.Frame) {
+ fr := p.newOutFrame(nil, gap, 0)
+ page, body = fr, fr
+ return
+ })
+ sc := p.offScene()
+ tree.MoveToParent(op[0], sc)
+ op[0].SetScene(sc)
+ sc.StyleTree()
+ sc.LayoutScene()
+}
+
+// layout reorders items within the pages and generates final output.
+func (p *pager) layout(its []*item) {
+ p.preRender(its)
+ pgs := p.pagify(its)
+ // note: could rearrange elements to put text at bottom and non-text at top?
+ // but this is probably not necessary?
+ p.outs = p.outputPages(pgs, p.newPage)
+}
diff --git a/text/paginate/options.go b/text/paginate/options.go
new file mode 100644
index 0000000000..f3aaf024a7
--- /dev/null
+++ b/text/paginate/options.go
@@ -0,0 +1,65 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:generate core generate -add-types
+
+package paginate
+
+import (
+ "cogentcore.org/core/core"
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/styles/sides"
+ "cogentcore.org/core/styles/units"
+ "cogentcore.org/core/text/printer"
+)
+
+// Options has the parameters for pagination.
+type Options struct {
+ // FontScale is an additional font scaling factor to apply.
+ // This is used in content to reverse the DocsFontSize factor, for example.
+ FontScale float32
+
+ // TextStyler is an optional text styling function to apply to text elements.
+ // Can be used to adjust the sizes and formatting of headers, for example.
+ TextStyler func(tx *core.Text)
+
+ // Title generates the title contents for the first page,
+ // into the given page body frame.
+ Title func(frame *core.Frame, opts *Options)
+
+ // Header generates the header contents for the page, into the given
+ // frame that represents the entire top margin.
+ // See examples in runners.go
+ Header func(frame *core.Frame, opts *Options, pageNo int)
+
+ // Footer generates the footer contents for the page, into the given
+ // frame that represents the entire top margin.
+ // See examples in runners.go
+ Footer func(frame *core.Frame, opts *Options, pageNo int)
+
+ // SizeDots is the total size in dots. Set automatically, but needs to be readable
+ // so is exported.
+ SizeDots math32.Vector2 `edit:"-"`
+
+ // BodyDots (content) size in dots.
+ BodyDots math32.Vector2 `edit:"-"`
+
+ // MargDots is the margin sizes in dots.
+ MargDots sides.Floats `edit:"-"`
+}
+
+func NewOptions() Options {
+ o := Options{}
+ o.Defaults()
+ return o
+}
+
+func (o *Options) Defaults() {
+ o.FontScale = 1
+ o.Footer = CenteredPageNumber
+}
+
+func (o *Options) ToDots(un *units.Context) {
+ o.SizeDots, o.BodyDots, o.MargDots = printer.Settings.ToDots(un)
+}
diff --git a/text/paginate/page.go b/text/paginate/page.go
new file mode 100644
index 0000000000..7c092768a7
--- /dev/null
+++ b/text/paginate/page.go
@@ -0,0 +1,75 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package paginate
+
+import (
+ "fmt"
+
+ "cogentcore.org/core/core"
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/styles"
+)
+
+func styMinMax(s *styles.Style, x, y float32) {
+ s.ZeroSpace()
+ s.Min.X.Dot(x)
+ s.Min.Y.Dot(y)
+ s.Max.X.Dot(x)
+ s.Max.Y.Dot(y)
+}
+
+func (p *pager) newPage(gap math32.Vector2, pageNo int) (page, body *core.Frame) {
+ pn := fmt.Sprintf("page-%d", pageNo)
+
+ page = core.NewFrame()
+ page.SetName(pn)
+ page.Styler(func(s *styles.Style) {
+ s.Direction = styles.Row
+ styMinMax(s, p.opts.SizeDots.X, p.opts.SizeDots.Y)
+ })
+ lmar := core.NewFrame(page)
+ lmar.SetName("left-margin")
+ lmar.Styler(func(s *styles.Style) {
+ s.Direction = styles.Column
+ styMinMax(s, p.opts.MargDots.Left, p.opts.SizeDots.Y)
+ })
+ bfr := core.NewFrame(page)
+ bfr.SetName("body-frame")
+ bfr.Styler(func(s *styles.Style) {
+ s.Direction = styles.Column
+ styMinMax(s, p.opts.BodyDots.X, p.opts.SizeDots.Y)
+ })
+
+ hdr := core.NewFrame(bfr)
+ hdr.SetName("header")
+ hdr.Styler(func(s *styles.Style) {
+ s.Direction = styles.Column
+ styMinMax(s, p.opts.BodyDots.X, p.opts.MargDots.Top)
+ })
+ if p.opts.Header != nil {
+ p.opts.Header(hdr, p.opts, pageNo)
+ }
+
+ body = core.NewFrame(bfr)
+ body.SetName("body")
+ body.Styler(func(s *styles.Style) {
+ s.Direction = styles.Column
+ styMinMax(s, p.opts.BodyDots.X, p.opts.BodyDots.Y)
+ s.Gap.X.Dot(gap.X)
+ s.Gap.Y.Dot(gap.Y)
+ })
+
+ ftr := core.NewFrame(bfr)
+ ftr.SetName("footer")
+ ftr.Styler(func(s *styles.Style) {
+ s.Direction = styles.Column
+ styMinMax(s, p.opts.BodyDots.X, p.opts.MargDots.Bottom)
+ })
+ if p.opts.Footer != nil {
+ p.opts.Footer(ftr, p.opts, pageNo)
+ }
+
+ return
+}
diff --git a/text/paginate/paginate.go b/text/paginate/paginate.go
new file mode 100644
index 0000000000..cd5ae79e70
--- /dev/null
+++ b/text/paginate/paginate.go
@@ -0,0 +1,69 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package paginate
+
+import (
+ "cogentcore.org/core/core"
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/paint"
+ "cogentcore.org/core/styles/units"
+ _ "cogentcore.org/core/text/tex"
+)
+
+// Paginate organizes the given input widget content into frames
+// that each fit within the page size specified in the options.
+// See PDF for function that generates paginated PDFs suitable
+// for printing: it ensures that the content layout matches
+// the page sizes, for example, which is not done in this version.
+func Paginate(opts Options, ins ...core.Widget) []*core.Frame {
+ if len(ins) == 0 {
+ return nil
+ }
+ p := pager{opts: &opts, ins: ins}
+ p.optsUpdate()
+ p.paginate()
+ return p.outs
+}
+
+// pager implements the pagination.
+type pager struct {
+ opts *Options
+ ins []core.Widget
+ outs []*core.Frame
+
+ ctx units.Context
+ sc *core.Scene
+}
+
+// optsUpdate updates the option sizes based on unit context in first input.
+func (p *pager) optsUpdate() {
+ in0 := p.ins[0].AsWidget()
+ p.ctx = in0.Styles.UnitContext
+ p.opts.ToDots(&p.ctx)
+}
+
+func (p *pager) paginate() {
+ its := p.extract()
+ p.layout(its)
+}
+
+// offScene returns a scene suitable for offline rendering,
+// sized to the full page size.
+func (p *pager) offScene() *core.Scene {
+ if p.sc != nil {
+ p.sc.DeleteChildren()
+ return p.sc
+ }
+ sz := p.opts.SizeDots
+ sc := core.NewScene()
+ // critical to be image not htmlcanvas, and not to be the correct size yet..
+ sc.Renderer = paint.NewImageRenderer(sz.SubScalar(1))
+ scsz := math32.Geom2DInt{}
+ scsz.Size = sz.ToPointCeil()
+ sc.Resize(scsz)
+ sc.MakeTextShaper()
+ p.sc = sc
+ return sc
+}
diff --git a/text/paginate/paginate_test.go b/text/paginate/paginate_test.go
new file mode 100644
index 0000000000..2b979df6bb
--- /dev/null
+++ b/text/paginate/paginate_test.go
@@ -0,0 +1,55 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package paginate
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "cogentcore.org/core/core"
+ "cogentcore.org/core/events"
+ "cogentcore.org/core/styles"
+ "cogentcore.org/core/text/rich"
+)
+
+// RunTest runs a test for given test case.
+func RunTest(t *testing.T, nm string, f func() *core.Body) {
+ b := f()
+ // b.AssertRender(t, "text-only")
+ showed := make(chan struct{})
+ b.OnFinal(events.Show, func(e events.Event) {
+ showed <- struct{}{}
+ })
+ b.RunWindow()
+ <-showed
+
+ opts := NewOptions()
+ opts.FontFamily = rich.Serif
+ opts.Header = NoFirst(HeaderLeftPageNumber("This is a test header"))
+ opts.Title = CenteredTitle("This is a Profound Statement of Something Important", "Bea A. Author", "University of Twente Department of Physiology", "March 1, 2024", `https://example.com/testing`, "The thing about this paper is that it is dealing with an issue that should be given more attention, but perhaps it really is hard to understand and that makes it difficult to get the attention it deserves. In any case, we are very proud.")
+ buff := bytes.Buffer{}
+ b.AsyncLock()
+ PDF(&buff, opts, b)
+ b.AsyncUnlock()
+ os.Mkdir("testdata", 0777)
+ os.WriteFile(filepath.Join("testdata", nm)+".pdf", buff.Bytes(), 0666)
+}
+
+func TestTextOnly(t *testing.T) {
+ ttx := "This is testing text, it is only a test. Do not be alarmed. The text must be at least a certain amount wide so that we can see how it flows up to the margin and judge the typesetting qualities etc."
+ RunTest(t, "text-only", func() *core.Body {
+ b := core.NewBody()
+ b.Styler(func(s *styles.Style) {
+ s.Min.X.Ch(80)
+ })
+ for i := range 200 {
+ core.NewText(b).SetText(fmt.Sprintf("Line %d: %s", i, ttx))
+ }
+ return b
+ })
+}
diff --git a/text/paginate/pdf.go b/text/paginate/pdf.go
new file mode 100644
index 0000000000..c051a614ef
--- /dev/null
+++ b/text/paginate/pdf.go
@@ -0,0 +1,126 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package paginate
+
+import (
+ "io"
+
+ "cogentcore.org/core/colors"
+ "cogentcore.org/core/colors/matcolor"
+ "cogentcore.org/core/core"
+ "cogentcore.org/core/paint"
+ "cogentcore.org/core/paint/pdf"
+ "cogentcore.org/core/paint/renderers/pdfrender"
+ "cogentcore.org/core/styles"
+ "cogentcore.org/core/styles/units"
+ "cogentcore.org/core/text/printer"
+ "cogentcore.org/core/text/rich"
+ "cogentcore.org/core/tree"
+)
+
+// PDF generates PDF pages from given input content using given options,
+// writing to the given writer. It re-renders the input widgets with
+// the default PDF fonts in place (Helvetica, Times, Courier),
+// and the size set to the target size as configured in the options.
+// This will produce accurate PDF layout.
+func PDF(w io.Writer, opts Options, ins ...core.Widget) {
+ if len(ins) == 0 {
+ return
+ }
+ cmode := matcolor.SchemeIsDark
+ colors.SetScheme(false)
+ cset := pdf.UseStandardFonts()
+
+ p := pager{opts: &opts, ins: ins}
+ p.ctx = *units.NewContext() // generic, invariant of actual context
+ p.opts.ToDots(&p.ctx)
+ p.assemble()
+ p.paginate()
+
+ sc := p.offScene()
+
+ pdr := paint.NewPDFRenderer(opts.SizeDots, &p.ctx).(*pdfrender.Renderer)
+ pdr.StartRender(w)
+ np := len(p.outs)
+ for i, p := range p.outs {
+ tree.MoveToParent(p, sc)
+ p.SetScene(sc)
+ sc.StyleTree()
+ sc.LayoutScene()
+
+ sc.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool {
+ if tx, ok := cwb.This.(*core.Text); ok {
+ lns := tx.PaintText()
+ if len(lns.Lines) > 0 {
+ ln := &lns.Lines[0]
+ ln.Offset.Y += 3 // todo: seriously, this fixes an otherwise inexplicable offset
+ }
+ }
+ return true
+ })
+
+ p.RenderWidget()
+
+ rend := sc.Painter.RenderDone()
+ pdr.RenderPage(rend)
+ if i < np-1 {
+ pdr.AddPage()
+ }
+ sc.DeleteChildren()
+ }
+ pdr.EndRender()
+ colors.SetScheme(cmode)
+ pdf.RestorePreviousFonts(cset)
+}
+
+// assemble collects everything to be rendered into one big list,
+// and sets the font family and size for text elements.
+// only for full format rendering (e.g., PDF)
+func (p *pager) assemble() {
+ sc := core.AsWidget(p.ins[0]).Scene
+ if p.opts.Title != nil {
+ tf := core.NewFrame()
+ tf.Scene = sc
+ tf.FinalStyler(func(s *styles.Style) {
+ s.Min.X.Dot(p.opts.BodyDots.X)
+ s.Min.Y.Dot(p.opts.BodyDots.Y)
+ })
+ p.opts.Title(tf, p.opts)
+ p.ins = append([]core.Widget{tf.This.(core.Widget)}, p.ins...)
+ tf.StyleTree()
+ }
+ fsc := printer.Settings.FontScale() * p.opts.FontScale
+ for ii, in := range p.ins {
+ iw := core.AsWidget(in)
+
+ iw.FinalStyler(func(s *styles.Style) {
+ s.Min.X.Dot(p.opts.BodyDots.X)
+ s.Min.Y.Dot(p.opts.BodyDots.Y)
+ })
+ if p.opts.Title != nil && ii == 0 { // don't restyle the title
+ continue
+ }
+ iw.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool {
+ if tx, ok := cwb.This.(*core.Text); ok {
+ if _, ok := cwb.Parent.(*core.Frame); ok { // not inside buttons etc
+ cwb.Styler(func(s *styles.Style) {
+ if tx.Styles.Font.Family == rich.SansSerif {
+ s.Font.Family = printer.Settings.FontFamily
+ }
+ if tx.Type == core.TextBodyLarge {
+ s.Text.LineHeight = printer.Settings.LineHeight
+ }
+ s.Font.Size.Value *= fsc
+ s.Color = colors.Uniform(colors.Black) // in case dark mode
+ if p.opts.TextStyler != nil {
+ p.opts.TextStyler(tx)
+ }
+ })
+ }
+ }
+ return true
+ })
+ }
+}
diff --git a/text/paginate/runners.go b/text/paginate/runners.go
new file mode 100644
index 0000000000..10981d7180
--- /dev/null
+++ b/text/paginate/runners.go
@@ -0,0 +1,192 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package paginate
+
+import (
+ "strconv"
+
+ "cogentcore.org/core/base/errors"
+ "cogentcore.org/core/colors"
+ "cogentcore.org/core/core"
+ "cogentcore.org/core/styles"
+ "cogentcore.org/core/text/printer"
+ "cogentcore.org/core/text/rich"
+ "cogentcore.org/core/text/text"
+)
+
+// TextStyler does standard text styling for printout:
+// FontFamily and Black Color (e.g., in case user is in Dark mode).
+func TextStyler(s *styles.Style) {
+ s.Font.Family = printer.Settings.FontFamily
+ s.Color = colors.Uniform(colors.Black)
+}
+
+// CenteredPageNumber generates a page number cenetered in the frame
+// with a 1.5em space above it.
+func CenteredPageNumber(frame *core.Frame, opts *Options, pageNo int) {
+ core.NewSpace(frame).Styler(func(s *styles.Style) { // space before
+ s.Min.Y.Em(1.5)
+ s.Grow.Set(1, 0)
+ })
+ fr := core.NewFrame(frame)
+ fr.Styler(func(s *styles.Style) {
+ s.Direction = styles.Row
+ s.Grow.Set(1, 0)
+ s.Justify.Content = styles.Center
+ })
+ core.NewText(fr).SetText(strconv.Itoa(pageNo)).Styler(func(s *styles.Style) {
+ TextStyler(s)
+ })
+}
+
+// NoFirst excludes the first page for any runner
+func NoFirst(fun func(frame *core.Frame, opts *Options, pageNo int)) func(frame *core.Frame, opts *Options, pageNo int) {
+ return func(frame *core.Frame, opts *Options, pageNo int) {
+ if pageNo == 1 {
+ return
+ }
+ fun(frame, opts, pageNo)
+ }
+}
+
+// HeaderLeftPageNumber adds a running header with page number on the right.
+func HeaderLeftPageNumber(header string) func(frame *core.Frame, opts *Options, pageNo int) {
+ return func(frame *core.Frame, opts *Options, pageNo int) {
+ core.NewStretch(frame)
+ fr := core.NewFrame(frame)
+ fr.Styler(func(s *styles.Style) {
+ s.Direction = styles.Row
+ s.Grow.Set(1, 0)
+ })
+ core.NewText(fr).SetText(header).Styler(func(s *styles.Style) {
+ TextStyler(s)
+ s.SetTextWrap(false)
+ s.Font.Slant = rich.Italic
+ s.Font.Size = printer.Settings.FontSize
+ })
+ core.NewStretch(fr)
+ core.NewText(fr).SetText(strconv.Itoa(pageNo)).Styler(func(s *styles.Style) {
+ TextStyler(s)
+ s.SetTextWrap(false)
+ s.Font.Size = printer.Settings.FontSize
+ })
+ core.NewSpace(frame).Styler(func(s *styles.Style) { // space after
+ s.Min.Y.Em(3)
+ s.Grow.Set(1, 0)
+ })
+ }
+}
+
+// CenteredTitle inserts centered text elements for each element if non-empty.
+func CenteredTitle(title, authors, affiliations, url, date, abstract string) func(frame *core.Frame, opts *Options) {
+ return func(frame *core.Frame, opts *Options) {
+ fr := core.NewFrame(frame)
+ fr.Styler(func(s *styles.Style) {
+ s.Direction = styles.Column
+ s.Grow.Set(1, 0)
+ s.Align.Items = styles.Center
+ })
+ fr.SetProperty("paginate-block", true)
+
+ core.NewStretch(fr).Styler(func(s *styles.Style) { // need this to take up the full width
+ s.Grow.Set(1, 0)
+ s.Min.X.Dot(opts.BodyDots.X)
+ s.Min.Y.Em(.1)
+ })
+ core.NewText(fr).SetText(title).Styler(func(s *styles.Style) {
+ TextStyler(s)
+ s.Font.Size = printer.Settings.FontSize
+ s.Font.Size.Value *= 16.0 / 11
+ s.Text.Align = text.Center
+ })
+
+ if authors != "" {
+ core.NewText(fr).SetText(authors).Styler(func(s *styles.Style) {
+ TextStyler(s)
+ s.Font.Size = printer.Settings.FontSize
+ s.Font.Size.Value *= 12.0 / 11
+ s.Text.Align = text.Center
+ })
+ }
+
+ if affiliations != "" {
+ core.NewText(fr).SetText(affiliations).Styler(func(s *styles.Style) {
+ TextStyler(s)
+ s.Font.Size = printer.Settings.FontSize
+ s.Text.Align = text.Center
+ s.Text.LineHeight = 1.1
+ })
+ }
+ core.NewSpace(fr).Styler(func(s *styles.Style) { s.Min.Y.Em(1) })
+
+ if date != "" {
+ core.NewText(fr).SetText(date).Styler(func(s *styles.Style) {
+ TextStyler(s)
+ s.Font.Size = printer.Settings.FontSize
+ s.Text.Align = text.Center
+ })
+ }
+
+ if url != "" {
+ core.NewText(fr).SetText(url).Styler(func(s *styles.Style) {
+ TextStyler(s)
+ s.Font.Size = printer.Settings.FontSize
+ s.Text.Align = text.Center
+ })
+ }
+
+ if abstract != "" {
+ core.NewText(fr).SetText("Abstract:").Styler(func(s *styles.Style) {
+ TextStyler(s)
+ s.Font.Size = printer.Settings.FontSize
+ s.Font.Size.Value *= 12.0 / 11
+ s.Font.Weight = rich.Bold
+ s.Align.Self = styles.Start
+ })
+ core.NewText(fr).SetText(abstract).Styler(func(s *styles.Style) {
+ TextStyler(s)
+ s.Font.Size = printer.Settings.FontSize
+ s.Text.LineHeight = printer.Settings.LineHeight
+ s.Align.Self = styles.Start
+ })
+ }
+ core.NewSpace(fr).Styler(func(s *styles.Style) { s.Min.Y.Em(1) })
+ }
+}
+
+// APAHeaders is a TextStyler function that sets APA-style headers based
+// on the tag property. The default material design header sizes used onscreen are
+// generally too large for print. This is designed for content, where the
+// second level header ## is used for most top-level headers within a page.
+func APAHeaders(tx *core.Text) {
+ headerLevel := 0
+ if t, ok := tx.Properties["tag"]; ok {
+ tag := t.(string)
+ if len(tag) > 1 && tag[0] == 'h' {
+ headerLevel = errors.Log1(strconv.Atoi(tag[1:]))
+ }
+ }
+ s := &tx.Styles
+ base := printer.Settings.FontSize
+ switch headerLevel {
+ case 1: // e.g., chapter level
+ s.Font.Size = base
+ s.Font.Size.Value *= 16.0 / 11.0
+ s.Font.Weight = rich.Bold
+ case 2:
+ s.Font.Size = base
+ s.Font.Size.Value *= 14.0 / 11.0
+ s.Font.Weight = rich.Bold
+ s.Align.Self = styles.Center
+ case 3:
+ s.Font.Size = base
+ s.Font.Size.Value *= 12.0 / 11.0
+ s.Font.Weight = rich.Bold
+ case 4:
+ s.Font.Size = base
+ s.Font.Weight = rich.Bold
+ s.Font.Slant = rich.Italic
+ }
+}
diff --git a/text/paginate/typegen.go b/text/paginate/typegen.go
new file mode 100644
index 0000000000..16c0aff0c9
--- /dev/null
+++ b/text/paginate/typegen.go
@@ -0,0 +1,15 @@
+// Code generated by "core generate -add-types"; DO NOT EDIT.
+
+package paginate
+
+import (
+ "cogentcore.org/core/types"
+)
+
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/paginate.item", IDName: "item", Doc: "item is one layout item", Fields: []types.Field{{Name: "w"}, {Name: "gap"}, {Name: "left"}}})
+
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/paginate.posn", IDName: "posn", Fields: []types.Field{{Name: "w"}, {Name: "i"}}})
+
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/paginate.Options", IDName: "options", Doc: "Options has the parameters for pagination.", Fields: []types.Field{{Name: "PageSize", Doc: "PageSize specifies a standard page size, or Custom."}, {Name: "Units", Doc: "Units are the units in which size is specified.\nWill automatically be set if PageSize != Custom."}, {Name: "Size", Doc: "Size is the size in given units.\nWill automatically be set if PageSize != Custom."}, {Name: "Margins", Doc: "Margins specify the page margins in the size units."}, {Name: "FontFamily", Doc: "FontFamily specifies the default font family to apply\nto all core.Text elements."}, {Name: "FontSize", Doc: "FontSize specifies the default font size to apply\nto all core.Text elements, if non-zero."}, {Name: "Title", Doc: "Title generates the title contents for the first page,\ninto the given page body frame."}, {Name: "Header", Doc: "Header generates the header contents for the page, into the given\nframe that represents the entire top margin.\nSee examples in runners.go"}, {Name: "Footer", Doc: "Footer generates the footer contents for the page, into the given\nframe that represents the entire top margin.\nSee examples in runners.go"}, {Name: "SizeDots", Doc: "SizeDots is the total size in dots. Set automatically, but needs to be readable\nso is exported."}, {Name: "BodyDots", Doc: "BodyDots (content) size in dots."}, {Name: "MargDots", Doc: "MargDots is the margin sizes in dots."}}})
+
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/paginate.pager", IDName: "pager", Doc: "pager implements the pagination.", Fields: []types.Field{{Name: "opts"}, {Name: "ins"}, {Name: "outs"}, {Name: "ctx"}}})
diff --git a/text/parse/languages/golang/types.go b/text/parse/languages/golang/types.go
index dcfca1ea12..ca3864336c 100644
--- a/text/parse/languages/golang/types.go
+++ b/text/parse/languages/golang/types.go
@@ -291,8 +291,19 @@ func (gl *GoLang) TypeFromASTPrim(fs *parse.FileState, pkg *syms.Symbol, ty *sym
return nil, false
}
+var typeDepth int
+
// TypeFromASTComp handles composite type processing
func (gl *GoLang) TypeFromASTComp(fs *parse.FileState, pkg *syms.Symbol, ty *syms.Type, tyast *parser.AST) (*syms.Type, bool) {
+ typeDepth++
+ if typeDepth > 100 {
+ typeDepth = 0
+ return nil, false
+ }
+ defer func() {
+ typeDepth--
+ }()
+
tnm := gl.ASTTypeName(tyast)
newTy := false
if ty == nil {
diff --git a/text/printer/enumgen.go b/text/printer/enumgen.go
new file mode 100644
index 0000000000..4d0ad4add2
--- /dev/null
+++ b/text/printer/enumgen.go
@@ -0,0 +1,50 @@
+// Code generated by "core generate -add-types"; DO NOT EDIT.
+
+package printer
+
+import (
+ "cogentcore.org/core/enums"
+)
+
+var _PageSizesValues = []PageSizes{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21}
+
+// PageSizesN is the highest valid value for type PageSizes, plus one.
+const PageSizesN PageSizes = 22
+
+var _PageSizesValueMap = map[string]PageSizes{`Custom`: 0, `Img1280x720`: 1, `Img1920x1080`: 2, `Img3840x2160`: 3, `Img7680x4320`: 4, `Img1024x768`: 5, `Img720x480`: 6, `Img640x480`: 7, `Img320x240`: 8, `A4`: 9, `USLetter`: 10, `USLegal`: 11, `A0`: 12, `A1`: 13, `A2`: 14, `A3`: 15, `A5`: 16, `A6`: 17, `A7`: 18, `A8`: 19, `A9`: 20, `A10`: 21}
+
+var _PageSizesDescMap = map[PageSizes]string{0: `Custom = nonstandard`, 1: `Image 1280x720 Px = 720p`, 2: `Image 1920x1080 Px = 1080p HD`, 3: `Image 3840x2160 Px = 4K`, 4: `Image 7680x4320 Px = 8K`, 5: `Image 1024x768 Px = XGA`, 6: `Image 720x480 Px = DVD`, 7: `Image 640x480 Px = VGA`, 8: `Image 320x240 Px = old CRT`, 9: `A4 = 210 x 297 mm`, 10: `USLetter = 8.5 x 11 in = 612 x 792 pt`, 11: `USLegal = 8.5 x 14 in = 612 x 1008 pt`, 12: `A0 = 841 x 1189 mm`, 13: `A1 = 594 x 841 mm`, 14: `A2 = 420 x 594 mm`, 15: `A3 = 297 x 420 mm`, 16: `A5 = 148 x 210 mm`, 17: `A6 = 105 x 148 mm`, 18: `A7 = 74 x 105`, 19: `A8 = 52 x 74 mm`, 20: `A9 = 37 x 52`, 21: `A10 = 26 x 37`}
+
+var _PageSizesMap = map[PageSizes]string{0: `Custom`, 1: `Img1280x720`, 2: `Img1920x1080`, 3: `Img3840x2160`, 4: `Img7680x4320`, 5: `Img1024x768`, 6: `Img720x480`, 7: `Img640x480`, 8: `Img320x240`, 9: `A4`, 10: `USLetter`, 11: `USLegal`, 12: `A0`, 13: `A1`, 14: `A2`, 15: `A3`, 16: `A5`, 17: `A6`, 18: `A7`, 19: `A8`, 20: `A9`, 21: `A10`}
+
+// String returns the string representation of this PageSizes value.
+func (i PageSizes) String() string { return enums.String(i, _PageSizesMap) }
+
+// SetString sets the PageSizes value from its string representation,
+// and returns an error if the string is invalid.
+func (i *PageSizes) SetString(s string) error {
+ return enums.SetString(i, s, _PageSizesValueMap, "PageSizes")
+}
+
+// Int64 returns the PageSizes value as an int64.
+func (i PageSizes) Int64() int64 { return int64(i) }
+
+// SetInt64 sets the PageSizes value from an int64.
+func (i *PageSizes) SetInt64(in int64) { *i = PageSizes(in) }
+
+// Desc returns the description of the PageSizes value.
+func (i PageSizes) Desc() string { return enums.Desc(i, _PageSizesDescMap) }
+
+// PageSizesValues returns all possible values for the type PageSizes.
+func PageSizesValues() []PageSizes { return _PageSizesValues }
+
+// Values returns all possible values for the type PageSizes.
+func (i PageSizes) Values() []enums.Enum { return enums.Values(_PageSizesValues) }
+
+// MarshalText implements the [encoding.TextMarshaler] interface.
+func (i PageSizes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
+
+// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
+func (i *PageSizes) UnmarshalText(text []byte) error {
+ return enums.UnmarshalText(i, text, "PageSizes")
+}
diff --git a/text/printer/pagesizes.go b/text/printer/pagesizes.go
new file mode 100644
index 0000000000..abd5f185c2
--- /dev/null
+++ b/text/printer/pagesizes.go
@@ -0,0 +1,143 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package printer
+
+import (
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/styles/units"
+)
+
+// Sizes are standard physical drawing sizes
+type PageSizes int32 //enums:enum
+
+const (
+ // Custom = nonstandard
+ Custom PageSizes = iota
+
+ // Image 1280x720 Px = 720p
+ Img1280x720
+
+ // Image 1920x1080 Px = 1080p HD
+ Img1920x1080
+
+ // Image 3840x2160 Px = 4K
+ Img3840x2160
+
+ // Image 7680x4320 Px = 8K
+ Img7680x4320
+
+ // Image 1024x768 Px = XGA
+ Img1024x768
+
+ // Image 720x480 Px = DVD
+ Img720x480
+
+ // Image 640x480 Px = VGA
+ Img640x480
+
+ // Image 320x240 Px = old CRT
+ Img320x240
+
+ // A4 = 210 x 297 mm
+ A4
+
+ // USLetter = 8.5 x 11 in = 612 x 792 pt
+ USLetter
+
+ // USLegal = 8.5 x 14 in = 612 x 1008 pt
+ USLegal
+
+ // A0 = 841 x 1189 mm
+ A0
+
+ // A1 = 594 x 841 mm
+ A1
+
+ // A2 = 420 x 594 mm
+ A2
+
+ // A3 = 297 x 420 mm
+ A3
+
+ // A5 = 148 x 210 mm
+ A5
+
+ // A6 = 105 x 148 mm
+ A6
+
+ // A7 = 74 x 105
+ A7
+
+ // A8 = 52 x 74 mm
+ A8
+
+ // A9 = 37 x 52
+ A9
+
+ // A10 = 26 x 37
+ A10
+)
+
+// Size returns the corresponding size values and units.
+func (s PageSizes) Size() (un units.Units, size math32.Vector2) {
+ v := sizesMap[s]
+ return v.un, math32.Vec2(v.x, v.y)
+}
+
+// Match returns a matching standard size for given units and dimension.
+func Match(un units.Units, wd, ht float32) PageSizes {
+ trgl := values{un: un, x: wd, y: ht}
+ trgp := values{un: un, x: ht, y: wd}
+ for k, v := range sizesMap {
+ if *v == trgl || *v == trgp {
+ return k
+ }
+ }
+ return Custom
+}
+
+// values are values for standard sizes
+type values struct {
+ un units.Units
+ x float32
+ y float32
+}
+
+// sizesMap is the map of size values for each standard size
+var sizesMap = map[PageSizes]*values{
+ Img1280x720: {units.UnitPx, 1280, 720},
+ Img1920x1080: {units.UnitPx, 1920, 1080},
+ Img3840x2160: {units.UnitPx, 3840, 2160},
+ Img7680x4320: {units.UnitPx, 7680, 4320},
+ Img1024x768: {units.UnitPx, 1024, 768},
+ Img720x480: {units.UnitPx, 720, 480},
+ Img640x480: {units.UnitPx, 640, 480},
+ Img320x240: {units.UnitPx, 320, 240},
+ A4: {units.UnitMm, 210, 297},
+ USLetter: {units.UnitPt, 612, 792},
+ USLegal: {units.UnitPt, 612, 1008},
+ A0: {units.UnitMm, 841, 1189},
+ A1: {units.UnitMm, 594, 841},
+ A2: {units.UnitMm, 420, 594},
+ A3: {units.UnitMm, 297, 420},
+ A5: {units.UnitMm, 148, 210},
+ A6: {units.UnitMm, 105, 148},
+ A7: {units.UnitMm, 74, 105},
+ A8: {units.UnitMm, 52, 74},
+ A9: {units.UnitMm, 37, 52},
+ A10: {units.UnitMm, 26, 37},
+}
+
+// DefaultPageSizeForRegion returns the default page size based on locale
+// region value, per the https://www.rfc-editor.org/rfc/bcp/bcp47.txt standard.
+// See [system.Locale] and [system.App] for how to get this.
+func DefaultPageSizeForRegion(region string) PageSizes {
+ switch region {
+ case "US", "CA", "BZ", "CL", "CR", "GT", "NI", "PA", "PR", "SV", "VE", "MX", "CO", "PH":
+ return USLetter
+ default:
+ return A4
+ }
+}
diff --git a/text/printer/settings.go b/text/printer/settings.go
new file mode 100644
index 0000000000..b1c7f598e5
--- /dev/null
+++ b/text/printer/settings.go
@@ -0,0 +1,119 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:generate core generate -add-types
+
+// package printer provides standard printer settings.
+package printer
+
+import (
+ "path/filepath"
+
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/styles/sides"
+ "cogentcore.org/core/styles/units"
+ "cogentcore.org/core/system"
+ "cogentcore.org/core/text/rich"
+ "cogentcore.org/core/tree"
+)
+
+// Settings provides the default printer settings.
+// This is initialized by the system.App because it depends on
+// the locale for initialization.
+var Settings SettingsData
+
+// Settings has standard printer settings.
+type SettingsData struct {
+ // PageSize specifies a standard page size, or Custom.
+ PageSize PageSizes
+
+ // Units are the units in which the page size is specified.
+ // Will automatically be set if PageSize != Custom.
+ Units units.Units
+
+ // Size is the page size in given units.
+ // Will automatically be set if PageSize != Custom.
+ Size math32.Vector2
+
+ // Margins specify the page margins in the size units.
+ Margins sides.Floats `display:"inline"`
+
+ // FontFamily specifies the font family to use for printing.
+ // The default SansSerif font used on screen may not be desired
+ // for printouts, where Serif is more typically used.
+ FontFamily rich.Family
+
+ // FontSize specifies the base font size to use for scaling printed
+ // text output (i.e., the default Text font will be this size, with
+ // larger elements scaled appropriately).
+ FontSize units.Value
+
+ // LineHeight is the default line height for standard text elements,
+ // in proportion to the font size (e.g., 1.25), which determines the
+ // spacing between lines.
+ LineHeight float32
+}
+
+func (ps *SettingsData) Defaults() {
+ ps.PageSize = DefaultPageSizeForRegion(system.TheApp.SystemLocale().Region())
+ switch ps.Units {
+ case units.UnitMm:
+ ps.Margins.Set(25) // basically one inch
+ case units.UnitPt:
+ ps.Margins.Set(72)
+ case units.UnitPx:
+ ps.Margins.Set(24)
+ }
+ ps.FontFamily = rich.Serif
+ ps.FontSize.Pt(11)
+ ps.LineHeight = 1.25
+ ps.Update()
+}
+
+func (ps *SettingsData) Update() {
+ if ps.PageSize != Custom {
+ pU := ps.Units
+ ps.Units, ps.Size = ps.PageSize.Size()
+ if pU != ps.Units {
+ uc := units.NewContext()
+ sc := uc.Convert(1, pU, ps.Units)
+ ps.Margins = ps.Margins.MulScalar(sc)
+ }
+ }
+}
+
+func (ps *SettingsData) Apply() {
+ ps.Update()
+}
+
+func (ps *SettingsData) Label() string {
+ return "Printer"
+}
+
+func (ps *SettingsData) Filename() string {
+ return filepath.Join(system.TheApp.CogentCoreDataDir(), "printer-settings.toml")
+}
+
+func (ps *SettingsData) MakeToolbar(p *tree.Plan) {
+}
+
+// ToDots returns the measurement values in rendering dots (actual pixels)
+// based on the given units context.
+// size = page size; body = content area inside margins
+func (ps *SettingsData) ToDots(un *units.Context) (size, body math32.Vector2, margins sides.Floats) {
+ sc := un.ToDots(1, ps.Units)
+ size = ps.Size.MulScalar(sc).Floor()
+ margins = ps.Margins.MulScalar(sc)
+ body.X = size.X - (margins.Left + margins.Right)
+ body.Y = size.Y - (margins.Top + margins.Bottom)
+ return
+}
+
+// FontScale returns the scaling factor based on FontSize,
+// relative to the core default font size of 16 Dp.
+func (ps *SettingsData) FontScale() float32 {
+ uc := units.NewContext()
+ sc := uc.Convert(16, units.UnitDp, ps.FontSize.Unit)
+ return ps.FontSize.Value / sc
+}
diff --git a/text/printer/typegen.go b/text/printer/typegen.go
new file mode 100644
index 0000000000..2c56e39cdc
--- /dev/null
+++ b/text/printer/typegen.go
@@ -0,0 +1,13 @@
+// Code generated by "core generate -add-types"; DO NOT EDIT.
+
+package printer
+
+import (
+ "cogentcore.org/core/types"
+)
+
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/printer.PageSizes", IDName: "page-sizes", Doc: "Sizes are standard physical drawing sizes"})
+
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/printer.values", IDName: "values", Doc: "values are values for standard sizes", Fields: []types.Field{{Name: "un"}, {Name: "x"}, {Name: "y"}}})
+
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/printer.SettingsData", IDName: "settings-data", Doc: "Settings has standard printer settings.", Fields: []types.Field{{Name: "PageSize", Doc: "PageSize specifies a standard page size, or Custom."}, {Name: "Units", Doc: "Units are the units in which size is specified.\nWill automatically be set if PageSize != Custom."}, {Name: "Size", Doc: "Size is the size in given units.\nWill automatically be set if PageSize != Custom."}, {Name: "Margins", Doc: "Margins specify the page margins in the size units."}}})
diff --git a/text/rich/link.go b/text/rich/link.go
index 51a654340e..7ef5e620e1 100644
--- a/text/rich/link.go
+++ b/text/rich/link.go
@@ -16,10 +16,6 @@ type Hyperlink struct {
// URL is the full URL for the link.
URL string
- // Properties are additional properties defined for the link,
- // e.g., from the parsed HTML attributes. TODO: resolve
- // Properties map[string]any
-
// Range defines the starting and ending positions of the link,
// in terms of source rune indexes.
Range textpos.Range
diff --git a/text/rich/settings.go b/text/rich/settings.go
index 5cba0377b7..4227323b61 100644
--- a/text/rich/settings.go
+++ b/text/rich/settings.go
@@ -11,12 +11,14 @@ import (
)
func init() {
- DefaultSettings.Defaults()
+ Settings.Defaults()
}
-// DefaultSettings contains the default global text settings.
-// This will be updated from rich.DefaultSettings.
-var DefaultSettings Settings
+// Settings contains the global text settings,
+// for language, script and font names to use.
+// To use different settings temporarily, save current
+// and swap.
+var Settings SettingsData
// FontName is a special string that provides a font chooser.
// It is aliased to [core.FontName] as well.
@@ -25,7 +27,7 @@ type FontName string
// Settings holds the global settings for rich text styling,
// including language, script, and preferred font faces for
// each category of font.
-type Settings struct {
+type SettingsData struct {
// Language is the preferred language used for rendering text.
Language language.Language
@@ -96,7 +98,7 @@ type Settings struct {
Fangsong FontName
}
-func (rts *Settings) Defaults() {
+func (rts *SettingsData) Defaults() {
rts.Language = language.DefaultLanguage()
rts.SansSerif = "Noto Sans"
rts.Monospace = "Roboto Mono"
@@ -130,7 +132,7 @@ func FamiliesToList(fam string) []string {
}
// Family returns the font family specified by the given [Family] enum.
-func (rts *Settings) Family(fam Family) string {
+func (rts *SettingsData) Family(fam Family) string {
switch fam {
case SansSerif:
return AddFamily(rts.SansSerif, `-apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif, emoji`)
diff --git a/text/rich/style.go b/text/rich/style.go
index 430ba8ccea..abe8b703f7 100644
--- a/text/rich/style.go
+++ b/text/rich/style.go
@@ -118,7 +118,7 @@ func (s *Style) InheritFields(parent *Style) {
// FontFamily returns the font family name(s) based on [Style.Family] and the
// values specified in the given [Settings].
-func (s *Style) FontFamily(ctx *Settings) string {
+func (s *Style) FontFamily(ctx *SettingsData) string {
return ctx.Family(s.Family)
}
@@ -140,7 +140,7 @@ const (
Serif
// Monospace fonts have all glyphs with he same fixed width.
- // Example monospace fonts include Fira Mono, DejaVu Sans Mono,
+ // Example monospace fonts include Courier, Fira Mono, DejaVu Sans Mono,
// Menlo, Consolas, Liberation Mono, Monaco, and Lucida Console.
Monospace
diff --git a/text/shaped/lines.go b/text/shaped/lines.go
index dca22faeed..876682d7b4 100644
--- a/text/shaped/lines.go
+++ b/text/shaped/lines.go
@@ -65,6 +65,11 @@ type Lines struct {
// HighlightColor is the color to use for rendering highlighted regions.
HighlightColor image.Image
+
+ // Anchor provides a named link destination associated with this text item.
+ // Used for document navigation, e.g., in PDF rendering. Must be a unique name.
+ // Is set in [core.Text] based on the id property, which is set by [content].
+ Anchor string
}
// Line is one line of shaped text, containing multiple Runs.
diff --git a/text/shaped/shaped_test.go b/text/shaped/shaped_test.go
index 6219cab0cd..4e735ead4b 100644
--- a/text/shaped/shaped_test.go
+++ b/text/shaped/shaped_test.go
@@ -27,15 +27,12 @@ import (
"cogentcore.org/core/text/text"
"cogentcore.org/core/text/textpos"
"github.com/go-text/typesetting/font"
- "github.com/go-text/typesetting/language"
"github.com/stretchr/testify/assert"
)
// RunTest makes a rendering state, paint, and image with the given size, calls the given
// function, and then asserts the image using [imagex.Assert] with the given name.
-func RunTest(t *testing.T, nm string, width int, height int, f func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings)) {
- rts := &rich.Settings{}
- rts.Defaults()
+func RunTest(t *testing.T, nm string, width int, height int, f func(pc *paint.Painter, sh Shaper, tsty *text.Style)) {
uc := units.Context{}
uc.Defaults()
tsty := text.NewStyle()
@@ -45,13 +42,13 @@ func RunTest(t *testing.T, nm string, width int, height int, f func(pc *paint.Pa
pc := paint.NewPainter(sz)
pc.FillBox(math32.Vector2{}, sz, colors.Uniform(colors.White))
sh := NewShaper()
- f(pc, sh, tsty, rts)
+ f(pc, sh, tsty)
img := paint.RenderToImage(pc)
imagex.Assert(t, img, nm)
}
func TestFontMapper(t *testing.T) {
- RunTest(t, "fontmapper", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings) {
+ RunTest(t, "fontmapper", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
fname := "Noto Sans"
for i := range 2 {
sty := rich.NewStyle()
@@ -80,7 +77,7 @@ func TestFontMapper(t *testing.T) {
tx.AddSpan(&medit, []rune("This is Medium Italic\n"))
tx.AddSpan(&boldit, []rune("This is Bold Italic"))
// fmt.Println(tx)
- lns := sh.WrapLines(tx, sty, tsty, rts, math32.Vec2(250, 250))
+ lns := sh.WrapLines(tx, sty, tsty, math32.Vec2(250, 250))
pos := math32.Vec2(10, float32(10+i*150))
pc.DrawText(lns, pos)
@@ -103,7 +100,7 @@ func TestFontMapper(t *testing.T) {
}
func TestBasic(t *testing.T) {
- RunTest(t, "basic", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings) {
+ RunTest(t, "basic", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
src := "The lazy fox typed in some familiar text"
sr := []rune(src)
@@ -122,7 +119,7 @@ func TestBasic(t *testing.T) {
tx.AddSpan(boldBig, sr[ix:ix+8])
tx.AddSpan(ul, sr[ix+8:])
- lns := sh.WrapLines(tx, plain, tsty, rts, math32.Vec2(250, 250))
+ lns := sh.WrapLines(tx, plain, tsty, math32.Vec2(250, 250))
lns.SelectRegion(textpos.Range{7, 30})
lns.SelectRegion(textpos.Range{34, 40})
pos := math32.Vec2(20, 60)
@@ -155,7 +152,7 @@ func TestBasic(t *testing.T) {
}
func TestHebrew(t *testing.T) {
- RunTest(t, "hebrew", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings) {
+ RunTest(t, "hebrew", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
tsty.Direction = rich.RTL
tsty.FontSize.Dots *= 1.5
@@ -165,15 +162,15 @@ func TestHebrew(t *testing.T) {
plain := rich.NewStyle()
tx := rich.NewText(plain, sr)
- lns := sh.WrapLines(tx, plain, tsty, rts, math32.Vec2(250, 250))
+ lns := sh.WrapLines(tx, plain, tsty, math32.Vec2(250, 250))
pc.DrawText(lns, math32.Vec2(20, 60))
})
}
func TestVertical(t *testing.T) {
- RunTest(t, "nihongo_ttb", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings) {
- rts.Language = "ja"
- rts.Script = language.Han
+ RunTest(t, "nihongo_ttb", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
+ // rts.Language = "ja"
+ // rts.Script = language.Han
tsty.Direction = rich.TTB // rich.BTT // note: apparently BTT is actually never used
tsty.FontSize.Dots *= 1.5
@@ -186,14 +183,14 @@ func TestVertical(t *testing.T) {
sr := []rune(src)
tx := rich.NewText(plain, sr)
- lns := sh.WrapLines(tx, plain, tsty, rts, math32.Vec2(150, 50))
+ lns := sh.WrapLines(tx, plain, tsty, math32.Vec2(150, 50))
// pc.DrawText(lns, math32.Vec2(100, 200))
pc.DrawText(lns, math32.Vec2(60, 100))
})
- RunTest(t, "nihongo_ltr", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings) {
- rts.Language = "ja"
- rts.Script = language.Han
+ RunTest(t, "nihongo_ltr", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
+ // rts.Language = "ja"
+ // rts.Script = language.Han
tsty.FontSize.Dots *= 1.5
// todo: word wrapping and sideways rotation in vertical not currently working
@@ -202,13 +199,13 @@ func TestVertical(t *testing.T) {
plain := rich.NewStyle()
tx := rich.NewText(plain, sr)
- lns := sh.WrapLines(tx, plain, tsty, rts, math32.Vec2(250, 250))
+ lns := sh.WrapLines(tx, plain, tsty, math32.Vec2(250, 250))
pc.DrawText(lns, math32.Vec2(20, 60))
})
}
func TestColors(t *testing.T) {
- RunTest(t, "colors", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings) {
+ RunTest(t, "colors", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
tsty.FontSize.Dots *= 4
stroke := rich.NewStyle().SetStrokeColor(colors.Red).SetBackground(colors.ToUniform(colors.Scheme.Select.Container))
@@ -220,28 +217,28 @@ func TestColors(t *testing.T) {
tx := rich.NewText(stroke, sr[:4])
tx.AddSpan(&big, sr[4:8]).AddSpan(stroke, sr[8:])
- lns := sh.WrapLines(tx, stroke, tsty, rts, math32.Vec2(250, 250))
+ lns := sh.WrapLines(tx, stroke, tsty, math32.Vec2(250, 250))
pc.DrawText(lns, math32.Vec2(20, 10))
})
}
func TestLink(t *testing.T) {
- RunTest(t, "link", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings) {
+ RunTest(t, "link", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
src := `The link and it is cool and`
sty := rich.NewStyle()
tx, err := htmltext.HTMLToRich([]byte(src), sty, nil)
assert.NoError(t, err)
- lns := sh.WrapLines(tx, sty, tsty, rts, math32.Vec2(250, 250))
+ lns := sh.WrapLines(tx, sty, tsty, math32.Vec2(250, 250))
pc.DrawText(lns, math32.Vec2(10, 10))
})
}
func TestSpacePos(t *testing.T) {
- RunTest(t, "space-pos", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings) {
+ RunTest(t, "space-pos", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
src := `The and`
sty := rich.NewStyle()
tx := rich.NewText(sty, []rune(src))
- lns := sh.WrapLines(tx, sty, tsty, rts, math32.Vec2(250, 250))
+ lns := sh.WrapLines(tx, sty, tsty, math32.Vec2(250, 250))
pos := math32.Vec2(10, 10)
pc.DrawText(lns, pos)
@@ -256,13 +253,13 @@ func TestSpacePos(t *testing.T) {
}
func TestLinefeed(t *testing.T) {
- RunTest(t, "linefeed", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings) {
+ RunTest(t, "linefeed", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
src := "Text2D can put HTML formatted Text anywhere you might want"
sty := rich.NewStyle()
tx, err := htmltext.HTMLToRich([]byte(src), sty, nil)
// fmt.Println(tx)
assert.NoError(t, err)
- lns := sh.WrapLines(tx, sty, tsty, rts, math32.Vec2(250, 250))
+ lns := sh.WrapLines(tx, sty, tsty, math32.Vec2(250, 250))
pos := math32.Vec2(10, 10)
pc.DrawText(lns, pos)
@@ -276,27 +273,27 @@ func TestLinefeed(t *testing.T) {
}
func TestLineCentering(t *testing.T) {
- RunTest(t, "linecentering", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings) {
+ RunTest(t, "linecentering", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
src := "This is Line Centering"
// src := "aceg"
sty := rich.NewStyle()
tsty.LineHeight = 3
tx := rich.NewText(sty, []rune(src))
- lns := sh.WrapLines(tx, sty, tsty, rts, math32.Vec2(250, 250))
+ lns := sh.WrapLines(tx, sty, tsty, math32.Vec2(250, 250))
pos := math32.Vec2(10, 10)
pc.DrawText(lns, pos)
})
}
func TestEmoji(t *testing.T) {
- RunTest(t, "emoji", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings) {
+ RunTest(t, "emoji", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
// src := "the \U0001F615\U0001F618\U0001F616 !!" // smileys
src := "the 🎁🎉, !!"
sty := rich.NewStyle()
sty.Size = 3
// sty.Family = rich.Monospace
tx := rich.NewText(sty, []rune(src))
- lns := sh.WrapLines(tx, sty, tsty, rts, math32.Vec2(250, 250))
+ lns := sh.WrapLines(tx, sty, tsty, math32.Vec2(250, 250))
// fmt.Println(lns)
pos := math32.Vec2(10, 10)
pc.DrawText(lns, pos)
@@ -338,13 +335,13 @@ func TestMathInline(t *testing.T) {
// continue
// }
fnm := "math-inline-" + test.name
- RunTest(t, fnm, 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings) {
+ RunTest(t, fnm, 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
src := test.math
sty := rich.NewStyle()
tx := rich.NewText(sty, []rune("math: "))
tx.AddMathInline(sty, src)
tx.AddSpan(sty, []rune(" and we should check line wrapping too"))
- lns := sh.WrapLines(tx, sty, tsty, rts, math32.Vec2(250, 250))
+ lns := sh.WrapLines(tx, sty, tsty, math32.Vec2(250, 250))
pos := math32.Vec2(10, 10)
pc.DrawText(lns, pos)
})
@@ -364,12 +361,12 @@ func TestMathDisplay(t *testing.T) {
// continue
// }
fnm := "math-display-" + test.name
- RunTest(t, fnm, 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings) {
+ RunTest(t, fnm, 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
src := test.math
sty := rich.NewStyle()
var tx rich.Text
tx.AddMathDisplay(sty, src)
- lns := sh.WrapLines(tx, sty, tsty, rts, math32.Vec2(250, 250))
+ lns := sh.WrapLines(tx, sty, tsty, math32.Vec2(250, 250))
pos := math32.Vec2(10, 10)
pc.DrawText(lns, pos)
})
@@ -377,14 +374,14 @@ func TestMathDisplay(t *testing.T) {
}
func TestWhitespacePre(t *testing.T) {
- RunTest(t, "whitespacepre", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style, rts *rich.Settings) {
+ RunTest(t, "whitespacepre", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
tsty.WhiteSpace = text.WhiteSpacePre
src := "This is not going to wrap even if it goes over\nWhiteSpacePre does that for you"
sty := rich.NewStyle()
tx, err := htmltext.HTMLPreToRich([]byte(src), sty, nil)
assert.NoError(t, err)
// fmt.Println(tx)
- lns := sh.WrapLines(tx, sty, tsty, rts, math32.Vec2(250, 250))
+ lns := sh.WrapLines(tx, sty, tsty, math32.Vec2(250, 250))
pos := math32.Vec2(10, 10)
pc.DrawText(lns, pos)
tsty.WhiteSpace = text.WrapAsNeeded
diff --git a/text/shaped/shaper.go b/text/shaped/shaper.go
index c16e4e46d6..ee95c35650 100644
--- a/text/shaped/shaper.go
+++ b/text/shaped/shaper.go
@@ -36,7 +36,7 @@ type Shaper interface {
// The results are only valid until the next call to Shape or WrapParagraph:
// use slices.Clone if needed longer than that.
// This is called under a mutex lock, so it is safe for parallel use.
- Shape(tx rich.Text, tsty *text.Style, rts *rich.Settings) []Run
+ Shape(tx rich.Text, tsty *text.Style) []Run
// WrapLines performs line wrapping and shaping on the given rich text source,
// using the given style information, where the [rich.Style] provides the default
@@ -46,7 +46,7 @@ type Shaper interface {
// source text, and wrapped separately. For horizontal text, the Lines will render with
// a position offset at the upper left corner of the overall bounding box of the text.
// This is called under a mutex lock, so it is safe for parallel use.
- WrapLines(tx rich.Text, defSty *rich.Style, tsty *text.Style, rts *rich.Settings, size math32.Vector2) *Lines
+ WrapLines(tx rich.Text, defSty *rich.Style, tsty *text.Style, size math32.Vector2) *Lines
// FontFamilies returns a list of available font family names on this system.
FontList() []fonts.Info
diff --git a/text/shaped/shapers/shapedgt/shaper.go b/text/shaped/shapers/shapedgt/shaper.go
index 986674f6c8..8666cead9a 100644
--- a/text/shaped/shapers/shapedgt/shaper.go
+++ b/text/shaped/shapers/shapedgt/shaper.go
@@ -83,18 +83,18 @@ func (sh *Shaper) FontMap() *fontscan.FontMap {
// The results are only valid until the next call to Shape or WrapParagraph:
// use slices.Clone if needed longer than that.
// This is called under a mutex lock, so it is safe for parallel use.
-func (sh *Shaper) Shape(tx rich.Text, tsty *text.Style, rts *rich.Settings) []shaped.Run {
+func (sh *Shaper) Shape(tx rich.Text, tsty *text.Style) []shaped.Run {
sh.Lock()
defer sh.Unlock()
- return sh.ShapeText(tx, tsty, rts, tx.Join())
+ return sh.ShapeText(tx, tsty, tx.Join())
}
// ShapeText shapes the spans in the given text using given style and settings,
// returning [shaped.Run] results.
// This should already have the mutex lock, and is used by shapedjs but is
// not an end-user call.
-func (sh *Shaper) ShapeText(tx rich.Text, tsty *text.Style, rts *rich.Settings, txt []rune) []shaped.Run {
- outs := sh.ShapeTextOutput(tx, tsty, rts, txt)
+func (sh *Shaper) ShapeText(tx rich.Text, tsty *text.Style, txt []rune) []shaped.Run {
+ outs := sh.ShapeTextOutput(tx, tsty, txt)
runs := make([]shaped.Run, len(outs))
for i := range outs {
run := &Run{Output: outs[i]}
@@ -118,7 +118,7 @@ func (sh *Shaper) ShapeText(tx rich.Text, tsty *text.Style, rts *rich.Settings,
// returning raw go-text [shaping.Output].
// This should already have the mutex lock, and is used by shapedjs but is
// not an end-user call.
-func (sh *Shaper) ShapeTextOutput(tx rich.Text, tsty *text.Style, rts *rich.Settings, txt []rune) []shaping.Output {
+func (sh *Shaper) ShapeTextOutput(tx rich.Text, tsty *text.Style, txt []rune) []shaping.Output {
if tx.Len() == 0 {
return nil
}
@@ -144,7 +144,7 @@ func (sh *Shaper) ShapeTextOutput(tx rich.Text, tsty *text.Style, rts *rich.Sett
si++ // skip the end special
continue
}
- q := StyleToQuery(sty, tsty, rts)
+ q := StyleToQuery(sty, tsty)
sh.fontMap.SetQuery(q)
in.Text = txt
@@ -153,8 +153,8 @@ func (sh *Shaper) ShapeTextOutput(tx rich.Text, tsty *text.Style, rts *rich.Sett
in.Direction = shaped.GoTextDirection(sty.Direction, tsty)
fsz := tsty.FontHeight(sty)
in.Size = math32.ToFixed(fsz)
- in.Script = rts.Script
- in.Language = rts.Language
+ in.Script = rich.Settings.Script
+ in.Language = rich.Settings.Language
ins := sh.splitter.Split(in, sh.fontMap) // this is essential
for _, in := range ins {
@@ -219,7 +219,7 @@ func DirectionAdvance(dir di.Direction, pos fixed.Point26_6, adv fixed.Int26_6)
}
// StyleToQuery translates the rich.Style to go-text fontscan.Query parameters.
-func StyleToQuery(sty *rich.Style, tsty *text.Style, rts *rich.Settings) fontscan.Query {
+func StyleToQuery(sty *rich.Style, tsty *text.Style) fontscan.Query {
q := fontscan.Query{}
fam := tsty.FontFamily(sty)
q.Families = rich.FamiliesToList(fam)
diff --git a/text/shaped/shapers/shapedgt/wrap.go b/text/shaped/shapers/shapedgt/wrap.go
index 64756014a4..309dcb5fba 100644
--- a/text/shaped/shapers/shapedgt/wrap.go
+++ b/text/shaped/shapers/shapedgt/wrap.go
@@ -24,7 +24,7 @@ import (
// source text, and wrapped separately. For horizontal text, the Lines will render with
// a position offset at the upper left corner of the overall bounding box of the text.
// This is called under a mutex lock, so it is safe for parallel use.
-func (sh *Shaper) WrapLines(tx rich.Text, defSty *rich.Style, tsty *text.Style, rts *rich.Settings, size math32.Vector2) *shaped.Lines {
+func (sh *Shaper) WrapLines(tx rich.Text, defSty *rich.Style, tsty *text.Style, size math32.Vector2) *shaped.Lines {
sh.Lock()
defer sh.Unlock()
if tsty.FontSize.Dots == 0 {
@@ -32,14 +32,14 @@ func (sh *Shaper) WrapLines(tx rich.Text, defSty *rich.Style, tsty *text.Style,
}
txt := tx.Join()
- outs := sh.ShapeTextOutput(tx, tsty, rts, txt)
- lines, truncated := sh.WrapLinesOutput(outs, txt, tx, defSty, tsty, rts, size)
+ outs := sh.ShapeTextOutput(tx, tsty, txt)
+ lines, truncated := sh.WrapLinesOutput(outs, txt, tx, defSty, tsty, size)
return sh.LinesBounds(lines, truncated, tx, defSty, tsty, size)
}
// This should already have the mutex lock, and is used by shapedjs but is
// not an end-user call. Returns new lines and number of truncations.
-func (sh *Shaper) WrapLinesOutput(outs []shaping.Output, txt []rune, tx rich.Text, defSty *rich.Style, tsty *text.Style, rts *rich.Settings, size math32.Vector2) ([]shaping.Line, int) {
+func (sh *Shaper) WrapLinesOutput(outs []shaping.Output, txt []rune, tx rich.Text, defSty *rich.Style, tsty *text.Style, size math32.Vector2) ([]shaping.Line, int) {
lht := tsty.LineHeightDots(defSty)
dir := shaped.GoTextDirection(rich.Default, tsty)
diff --git a/text/shaped/shapers/shapedjs/metrics.go b/text/shaped/shapers/shapedjs/metrics.go
index a5f1250551..fdd446de9f 100644
--- a/text/shaped/shapers/shapedjs/metrics.go
+++ b/text/shaped/shapers/shapedjs/metrics.go
@@ -18,10 +18,10 @@ import (
// has the font ascent and descent information, and the BoundsBox() method returns a full
// bounding box for the given font, centered at the baseline.
// This is called under a mutex lock, so it is safe for parallel use.
-func (sh *Shaper) FontSize(r rune, sty *rich.Style, tsty *text.Style, rts *rich.Settings) shaped.Run {
+func (sh *Shaper) FontSize(r rune, sty *rich.Style, tsty *text.Style) shaped.Run {
sh.Lock()
defer sh.Unlock()
- return sh.fontSize(r, sty, tsty, rts)
+ return sh.fontSize(r, sty, tsty)
}
// LineHeight returns the line height for given font and text style.
@@ -29,27 +29,27 @@ func (sh *Shaper) FontSize(r rune, sty *rich.Style, tsty *text.Style, rts *rich.
// It includes the [text.Style] LineHeight multiplier on the natural
// font-derived line height, which is not generally the same as the font size.
// This is called under a mutex lock, so it is safe for parallel use.
-func (sh *Shaper) LineHeight(sty *rich.Style, tsty *text.Style, rts *rich.Settings) float32 {
+func (sh *Shaper) LineHeight(sty *rich.Style, tsty *text.Style) float32 {
sh.Lock()
defer sh.Unlock()
- return sh.lineHeight(sty, tsty, rts)
+ return sh.lineHeight(sty, tsty)
}
// fontSize returns the font shape sizing information for given font and text style,
// using given rune (often the letter 'm'). The GlyphBounds field of the [Run] result
// has the font ascent and descent information, and the BoundsBox() method returns a full
// bounding box for the given font, centered at the baseline.
-func (sh *Shaper) fontSize(r rune, sty *rich.Style, tsty *text.Style, rts *rich.Settings) shaped.Run {
+func (sh *Shaper) fontSize(r rune, sty *rich.Style, tsty *text.Style) shaped.Run {
tx := rich.NewText(sty, []rune{r})
- return sh.shapeAdjust(tx, tsty, rts, []rune{r})[0]
+ return sh.shapeAdjust(tx, tsty, []rune{r})[0]
}
// lineHeight returns the line height for given font and text style.
// For vertical text directions, this is actually the line width.
// It includes the [text.Style] LineHeight multiplier on the natural
// font-derived line height, which is not generally the same as the font size.
-func (sh *Shaper) lineHeight(sty *rich.Style, tsty *text.Style, rts *rich.Settings) float32 {
- run := sh.fontSize('M', sty, tsty, rts)
+func (sh *Shaper) lineHeight(sty *rich.Style, tsty *text.Style) float32 {
+ run := sh.fontSize('M', sty, tsty)
bb := run.LineBounds()
dir := shaped.GoTextDirection(rich.Default, tsty)
if dir.IsVertical() {
diff --git a/text/shaped/shapers/shapedjs/shaper.go b/text/shaped/shapers/shapedjs/shaper.go
index 69d08dd713..c6fbca8ca8 100644
--- a/text/shaped/shapers/shapedjs/shaper.go
+++ b/text/shaped/shapers/shapedjs/shaper.go
@@ -56,29 +56,29 @@ func NewShaper() shaped.Shaper {
// The results are only valid until the next call to Shape or WrapParagraph:
// use slices.Clone if needed longer than that.
// This is called under a mutex lock, so it is safe for parallel use.
-func (sh *Shaper) Shape(tx rich.Text, tsty *text.Style, rts *rich.Settings) []shaped.Run {
+func (sh *Shaper) Shape(tx rich.Text, tsty *text.Style) []shaped.Run {
sh.Lock()
defer sh.Unlock()
- return sh.shapeAdjust(tx, tsty, rts, tx.Join())
+ return sh.shapeAdjust(tx, tsty, tx.Join())
}
// shapeAdjust turns given input spans into [Runs] of rendered text,
// using given context needed for complete styling.
// The results are only valid until the next call to Shape or WrapParagraph:
// use slices.Clone if needed longer than that.
-func (sh *Shaper) shapeAdjust(tx rich.Text, tsty *text.Style, rts *rich.Settings, txt []rune) []shaped.Run {
- return sh.adjustRuns(sh.ShapeText(tx, tsty, rts, txt), tx, tsty, rts)
+func (sh *Shaper) shapeAdjust(tx rich.Text, tsty *text.Style, txt []rune) []shaped.Run {
+ return sh.adjustRuns(sh.ShapeText(tx, tsty, txt), tx, tsty)
}
// adjustRuns adjusts the given run metrics based on the html measureText results.
// This should already have the mutex lock, and is used by shapedjs but is
// not an end-user call.
-func (sh *Shaper) adjustRuns(runs []shaped.Run, tx rich.Text, tsty *text.Style, rts *rich.Settings) []shaped.Run {
+func (sh *Shaper) adjustRuns(runs []shaped.Run, tx rich.Text, tsty *text.Style) []shaped.Run {
for _, run := range runs {
grun := run.(*shapedgt.Run)
out := &grun.Output
fnt := &grun.Font
- sh.adjustOutput(out, fnt, tx, tsty, rts)
+ sh.adjustOutput(out, fnt, tx, tsty)
}
return runs
}
@@ -91,7 +91,7 @@ func (sh *Shaper) adjustRuns(runs []shaped.Run, tx rich.Text, tsty *text.Style,
// source text, and wrapped separately. For horizontal text, the Lines will render with
// a position offset at the upper left corner of the overall bounding box of the text.
// This is called under a mutex lock, so it is safe for parallel use.
-func (sh *Shaper) WrapLines(tx rich.Text, defSty *rich.Style, tsty *text.Style, rts *rich.Settings, size math32.Vector2) *shaped.Lines {
+func (sh *Shaper) WrapLines(tx rich.Text, defSty *rich.Style, tsty *text.Style, size math32.Vector2) *shaped.Lines {
sh.Lock()
defer sh.Unlock()
if tsty.FontSize.Dots == 0 {
@@ -101,22 +101,22 @@ func (sh *Shaper) WrapLines(tx rich.Text, defSty *rich.Style, tsty *text.Style,
txt := tx.Join()
// sptx := tx.Clone()
// sptx.SplitSpaces() // no advantage to doing this
- outs := sh.ShapeTextOutput(tx, tsty, rts, txt)
+ outs := sh.ShapeTextOutput(tx, tsty, txt)
for oi := range outs {
out := &outs[oi]
si, _, _ := tx.Index(out.Runes.Offset)
sty, _ := tx.Span(si)
fnt := text.NewFont(sty, tsty)
- sh.adjustOutput(out, fnt, tx, tsty, rts)
+ sh.adjustOutput(out, fnt, tx, tsty)
}
- lines, truncated := sh.WrapLinesOutput(outs, txt, tx, defSty, tsty, rts, size)
+ lines, truncated := sh.WrapLinesOutput(outs, txt, tx, defSty, tsty, size)
for _, lno := range lines {
for oi := range lno {
out := &lno[oi]
si, _, _ := tx.Index(out.Runes.Offset)
sty, _ := tx.Span(si)
fnt := text.NewFont(sty, tsty)
- sh.adjustOutput(out, fnt, tx, tsty, rts)
+ sh.adjustOutput(out, fnt, tx, tsty)
}
}
return sh.LinesBounds(lines, truncated, tx, defSty, tsty, size)
@@ -125,7 +125,7 @@ func (sh *Shaper) WrapLines(tx rich.Text, defSty *rich.Style, tsty *text.Style,
// adjustOutput adjusts the given run metrics based on the html measureText results.
// This should already have the mutex lock, and is used by shapedjs but is
// not an end-user call.
-func (sh *Shaper) adjustOutput(out *shaping.Output, fnt *text.Font, tx rich.Text, tsty *text.Style, rts *rich.Settings) {
+func (sh *Shaper) adjustOutput(out *shaping.Output, fnt *text.Font, tx rich.Text, tsty *text.Style) {
rng := textpos.Range{out.Runes.Offset, out.Runes.Offset + out.Runes.Count}
si, sn, ri := tx.Index(rng.Start)
sty, stx := tx.Span(si)
diff --git a/text/tex/tex_test.go b/text/tex/tex_test.go
index 236225ebf2..74195623eb 100644
--- a/text/tex/tex_test.go
+++ b/text/tex/tex_test.go
@@ -74,7 +74,7 @@ func TestTex(t *testing.T) {
// reference text
// sh := shaped.NewShaper()
// tx := rich.NewText(&pc.Font, []rune("a=x"))
- // lns := sh.WrapLines(tx, &pc.Font, &pc.Text, &rich.DefaultSettings, math32.Vec2(1000, 50))
+ // lns := sh.WrapLines(tx, &pc.Font, &pc.Text, &rich.Settings, math32.Vec2(1000, 50))
// pc.DrawText(lns, math32.Vec2(0, 70))
})
}
diff --git a/text/text/font.go b/text/text/font.go
index 1493dafc3c..9e2776c1f7 100644
--- a/text/text/font.go
+++ b/text/text/font.go
@@ -19,7 +19,7 @@ type Font struct {
Size float32
// Family is a nonstandard family name: if standard, then empty,
- // and value is determined by [rich.DefaultSettings] and Style.Family.
+ // and value is determined by [rich.Settings] and Style.Family.
Family string
}
@@ -40,12 +40,12 @@ func (fn *Font) Style(tsty *Style) *rich.Style {
}
// FontFamily returns the string value of the font Family for given [rich.Style],
-// using [text.Style] CustomFont or [rich.DefaultSettings] values.
+// using [text.Style] CustomFont or [rich.Settings] values.
func (ts *Style) FontFamily(sty *rich.Style) string {
if sty.Family == rich.Custom {
return string(ts.CustomFont)
}
- return sty.FontFamily(&rich.DefaultSettings)
+ return sty.FontFamily(&rich.Settings)
}
func (fn *Font) FamilyString(tsty *Style) string {
diff --git a/text/text/props.go b/text/text/props.go
index cfe3672226..8eca9cb161 100644
--- a/text/text/props.go
+++ b/text/text/props.go
@@ -61,6 +61,8 @@ var styleFuncs = map[string]styleprops.Func{
func(obj *Style) enums.EnumSetter { return &obj.Direction }),
"text-indent": styleprops.Units(units.Value{},
func(obj *Style) *units.Value { return &obj.Indent }),
+ "text-hanging": styleprops.Units(units.Value{},
+ func(obj *Style) *units.Value { return &obj.Hanging }),
"tab-size": styleprops.Int(int(4),
func(obj *Style) *int { return &obj.TabSize }),
"select-color": func(obj any, key string, val any, parent any, cc colors.Context) {
diff --git a/text/text/style.go b/text/text/style.go
index b72845749b..b1087346c0 100644
--- a/text/text/style.go
+++ b/text/text/style.go
@@ -69,6 +69,10 @@ type Style struct { //types:add
// Indent specifies how much to indent the first line in a paragraph (inherited).
Indent units.Value
+ // Hanging specifies how much to indent all but the first line
+ // in a paragraph (inherited).
+ Hanging units.Value
+
// TabSize specifies the tab size, in number of characters (inherited).
TabSize int
@@ -110,6 +114,7 @@ func (ts *Style) ToDots(uc *units.Context) {
ts.FontSize.ToDots(uc)
ts.FontSize.Dots = math32.Round(ts.FontSize.Dots)
ts.Indent.ToDots(uc)
+ ts.Hanging.ToDots(uc)
}
// InheritFields from parent
@@ -121,6 +126,7 @@ func (ts *Style) InheritFields(parent *Style) {
// ts.WhiteSpace = par.WhiteSpace // todo: we can't inherit this b/c label base default then gets overwritten
ts.Direction = parent.Direction
ts.Indent = parent.Indent
+ ts.Hanging = parent.Hanging
ts.TabSize = parent.TabSize
ts.SelectColor = parent.SelectColor
ts.HighlightColor = parent.HighlightColor
diff --git a/text/textcore/layout.go b/text/textcore/layout.go
index ce82964508..b17e08d48b 100644
--- a/text/textcore/layout.go
+++ b/text/textcore/layout.go
@@ -44,7 +44,7 @@ func (ed *Base) styleSizes() {
if sh != nil {
lht := ed.Styles.LineHeightDots()
tx := rich.NewText(sty, []rune{'M'})
- r := sh.Shape(tx, tsty, &rich.DefaultSettings)
+ r := sh.Shape(tx, tsty)
ed.charSize.X = math32.Round(r[0].Advance())
ed.charSize.Y = lht
}
diff --git a/text/textcore/render.go b/text/textcore/render.go
index 408ae77f99..5a78460feb 100644
--- a/text/textcore/render.go
+++ b/text/textcore/render.go
@@ -172,7 +172,6 @@ func (ed *Base) renderLine(li, ln int, rpos math32.Vector2, vsel textpos.Region,
vlr := buf.ViewLineRegionNoLock(ed.viewId, ln)
vseli := vlr.Intersect(vsel, ed.linesSize.X)
tx := buf.ViewMarkupLine(ed.viewId, ln)
- ctx := &rich.DefaultSettings
ts := ed.Lines.Settings.TabSize
indent := 0
sty, tsty := ed.Styles.NewRichText()
@@ -181,7 +180,7 @@ func (ed *Base) renderLine(li, ln int, rpos math32.Vector2, vsel textpos.Region,
if ed.tabRender != nil {
return ed.tabRender.Clone()
}
- lns := sh.WrapLines(stx, sty, tsty, ctx, ssz)
+ lns := sh.WrapLines(stx, sty, tsty, ssz)
ed.tabRender = lns
return lns
}
@@ -191,7 +190,7 @@ func (ed *Base) renderLine(li, ln int, rpos math32.Vector2, vsel textpos.Region,
if rc.lns != nil && slices.Compare(rc.tx, txt) == 0 {
return rc.lns
}
- lns := sh.WrapLines(stx, sty, tsty, ctx, ssz)
+ lns := sh.WrapLines(stx, sty, tsty, ssz)
ed.lineRenders[li] = renderCache{tx: txt, lns: lns}
return lns
}
@@ -318,7 +317,7 @@ func (ed *Base) renderLineNumber(pos math32.Vector2, li, ln int) {
if rc.lns != nil && slices.Compare(rc.tx, tx[0]) == 0 { // captures styling
lns = rc.lns
} else {
- lns = sh.WrapLines(tx, sty, tsty, &rich.DefaultSettings, sz)
+ lns = sh.WrapLines(tx, sty, tsty, sz)
ed.lineNoRenders[li] = renderCache{tx: tx[0], lns: lns}
}
pc.DrawText(lns, pos)
diff --git a/xyz/camera.go b/xyz/camera.go
index 0b508666ae..f6d6e6ff64 100644
--- a/xyz/camera.go
+++ b/xyz/camera.go
@@ -155,13 +155,13 @@ func (cm *Camera) Orbit(delX, delY float32) {
// delX rotates around the up vector
dxq := math32.NewQuatAxisAngle(up, math32.DegToRad(delX))
- dx := ctdir.MulQuat(dxq).Sub(ctdir)
+ dx := dxq.MulVector(ctdir).Sub(ctdir)
// delY rotates around the right vector
dyq := math32.NewQuatAxisAngle(right, math32.DegToRad(delY))
- dy := ctdir.MulQuat(dyq).Sub(ctdir)
+ dy := dyq.MulVector(ctdir).Sub(ctdir)
cm.Pose.Pos = cm.Pose.Pos.Add(dx).Add(dy)
- cm.UpDir = cm.UpDir.MulQuat(dyq) // this is only one that affects up
+ cm.UpDir = dyq.MulVector(cm.UpDir) // this is only one that affects up
cm.LookAtTarget()
}
@@ -171,8 +171,8 @@ func (cm *Camera) Orbit(delX, delY float32) {
// current window view)
// and it moves the target by the same increment, changing the target position.
func (cm *Camera) Pan(delX, delY float32) {
- dx := math32.Vec3(-delX, 0, 0).MulQuat(cm.Pose.Quat)
- dy := math32.Vec3(0, -delY, 0).MulQuat(cm.Pose.Quat)
+ dx := cm.Pose.Quat.MulVector(math32.Vec3(-delX, 0, 0))
+ dy := cm.Pose.Quat.MulVector(math32.Vec3(0, -delY, 0))
td := dx.Add(dy)
cm.Pose.Pos.SetAdd(td)
cm.Target.SetAdd(td)
diff --git a/xyz/pose.go b/xyz/pose.go
index b12d45872c..be7cb36988 100644
--- a/xyz/pose.go
+++ b/xyz/pose.go
@@ -117,7 +117,7 @@ func (ps *Pose) UpdateMVPMatrix(viewMat, projectionMat *math32.Matrix4) {
// MoveOnAxis moves (translates) the specified distance on the specified local axis,
// relative to the current rotation orientation.
func (ps *Pose) MoveOnAxis(x, y, z, dist float32) {
- ps.Pos.SetAdd(math32.Vec3(x, y, z).Normal().MulQuat(ps.Quat).MulScalar(dist))
+ ps.Pos.SetAdd(ps.Quat.MulVector(math32.Vec3(x, y, z).Normal()).MulScalar(dist))
}
// MoveOnAxisAbs moves (translates) the specified distance on the specified local axis,
diff --git a/xyz/text2d.go b/xyz/text2d.go
index 4a4bd99b0b..b2db8da6ed 100644
--- a/xyz/text2d.go
+++ b/xyz/text2d.go
@@ -114,7 +114,7 @@ func (txt *Text2D) RenderText() {
}
sz := math32.Vec2(10000, 1000) // just a big size
txt.richText, _ = htmltext.HTMLToRich([]byte(txt.Text), sty, nil)
- txt.textRender = txt.Scene.TextShaper.WrapLines(txt.richText, sty, tsty, &rich.DefaultSettings, sz)
+ txt.textRender = txt.Scene.TextShaper.WrapLines(txt.richText, sty, tsty, sz)
sz = txt.textRender.Bounds.Size().Ceil()
if sz.X == 0 {
sz.X = 10
diff --git a/yaegicore/coresymbols/cogentcore_org-core-content.go b/yaegicore/coresymbols/cogentcore_org-core-content.go
index ccd1ffadae..197406c9c1 100644
--- a/yaegicore/coresymbols/cogentcore_org-core-content.go
+++ b/yaegicore/coresymbols/cogentcore_org-core-content.go
@@ -10,10 +10,14 @@ import (
func init() {
Symbols["cogentcore.org/core/content/content"] = map[string]reflect.Value{
// function, constant and variable definitions
+ "BindTextEditor": reflect.ValueOf(&content.BindTextEditor).Elem(),
"NewContent": reflect.ValueOf(content.NewContent),
"NewPageInitFunc": reflect.ValueOf(&content.NewPageInitFunc).Elem(),
+ "OfflineURL": reflect.ValueOf(&content.OfflineURL).Elem(),
+ "Settings": reflect.ValueOf(&content.Settings).Elem(),
// type definitions
- "Content": reflect.ValueOf((*content.Content)(nil)),
+ "Content": reflect.ValueOf((*content.Content)(nil)),
+ "SettingsData": reflect.ValueOf((*content.SettingsData)(nil)),
}
}
diff --git a/yaegicore/coresymbols/cogentcore_org-core-core.go b/yaegicore/coresymbols/cogentcore_org-core-core.go
index 91bfe591dd..00f3b5fafc 100644
--- a/yaegicore/coresymbols/cogentcore_org-core-core.go
+++ b/yaegicore/coresymbols/cogentcore_org-core-core.go
@@ -51,7 +51,6 @@ func init() {
"CompleterStage": reflect.ValueOf(core.CompleterStage),
"ConstantSpacing": reflect.ValueOf(core.ConstantSpacing),
"DebugSettings": reflect.ValueOf(&core.DebugSettings).Elem(),
- "DeviceSettings": reflect.ValueOf(&core.DeviceSettings).Elem(),
"DialogStage": reflect.ValueOf(core.DialogStage),
"ErrorDialog": reflect.ValueOf(core.ErrorDialog),
"ErrorSnackbar": reflect.ValueOf(core.ErrorSnackbar),
@@ -90,6 +89,7 @@ func init() {
"NewComplete": reflect.ValueOf(core.NewComplete),
"NewDatePicker": reflect.ValueOf(core.NewDatePicker),
"NewDurationInput": reflect.ValueOf(core.NewDurationInput),
+ "NewFieldValue": reflect.ValueOf(core.NewFieldValue),
"NewFileButton": reflect.ValueOf(core.NewFileButton),
"NewFilePicker": reflect.ValueOf(core.NewFilePicker),
"NewFontButton": reflect.ValueOf(core.NewFontButton),
@@ -215,6 +215,7 @@ func init() {
"TileSecondLong": reflect.ValueOf(core.TileSecondLong),
"TileSpan": reflect.ValueOf(core.TileSpan),
"TileSplit": reflect.ValueOf(core.TileSplit),
+ "TimingSettings": reflect.ValueOf(&core.TimingSettings).Elem(),
"ToHTML": reflect.ValueOf(core.ToHTML),
"ToolbarStyles": reflect.ValueOf(core.ToolbarStyles),
"TooltipStage": reflect.ValueOf(core.TooltipStage),
@@ -245,9 +246,9 @@ func init() {
"Complete": reflect.ValueOf((*core.Complete)(nil)),
"DatePicker": reflect.ValueOf((*core.DatePicker)(nil)),
"DebugSettingsData": reflect.ValueOf((*core.DebugSettingsData)(nil)),
- "DeviceSettingsData": reflect.ValueOf((*core.DeviceSettingsData)(nil)),
"DurationInput": reflect.ValueOf((*core.DurationInput)(nil)),
"Events": reflect.ValueOf((*core.Events)(nil)),
+ "FieldWidgeter": reflect.ValueOf((*core.FieldWidgeter)(nil)),
"FileButton": reflect.ValueOf((*core.FileButton)(nil)),
"FilePaths": reflect.ValueOf((*core.FilePaths)(nil)),
"FilePicker": reflect.ValueOf((*core.FilePicker)(nil)),
@@ -260,12 +261,15 @@ func init() {
"Frame": reflect.ValueOf((*core.Frame)(nil)),
"FuncArg": reflect.ValueOf((*core.FuncArg)(nil)),
"FuncButton": reflect.ValueOf((*core.FuncButton)(nil)),
+ "GeomSize": reflect.ValueOf((*core.GeomSize)(nil)),
+ "GeomState": reflect.ValueOf((*core.GeomState)(nil)),
"Handle": reflect.ValueOf((*core.Handle)(nil)),
"HighlightingButton": reflect.ValueOf((*core.HighlightingButton)(nil)),
"HighlightingName": reflect.ValueOf((*core.HighlightingName)(nil)),
"Icon": reflect.ValueOf((*core.Icon)(nil)),
"IconButton": reflect.ValueOf((*core.IconButton)(nil)),
"Image": reflect.ValueOf((*core.Image)(nil)),
+ "InlineLengths": reflect.ValueOf((*core.InlineLengths)(nil)),
"InlineList": reflect.ValueOf((*core.InlineList)(nil)),
"Inspector": reflect.ValueOf((*core.Inspector)(nil)),
"KeyChordButton": reflect.ValueOf((*core.KeyChordButton)(nil)),
@@ -325,6 +329,7 @@ func init() {
"Themes": reflect.ValueOf((*core.Themes)(nil)),
"TimeInput": reflect.ValueOf((*core.TimeInput)(nil)),
"TimePicker": reflect.ValueOf((*core.TimePicker)(nil)),
+ "TimingSettingsData": reflect.ValueOf((*core.TimingSettingsData)(nil)),
"Toolbar": reflect.ValueOf((*core.Toolbar)(nil)),
"ToolbarMaker": reflect.ValueOf((*core.ToolbarMaker)(nil)),
"Tree": reflect.ValueOf((*core.Tree)(nil)),
@@ -341,6 +346,7 @@ func init() {
// interface wrapper definitions
"_ButtonEmbedder": reflect.ValueOf((*_cogentcore_org_core_core_ButtonEmbedder)(nil)),
+ "_FieldWidgeter": reflect.ValueOf((*_cogentcore_org_core_core_FieldWidgeter)(nil)),
"_Layouter": reflect.ValueOf((*_cogentcore_org_core_core_Layouter)(nil)),
"_Lister": reflect.ValueOf((*_cogentcore_org_core_core_Lister)(nil)),
"_MenuSearcher": reflect.ValueOf((*_cogentcore_org_core_core_MenuSearcher)(nil)),
@@ -369,6 +375,16 @@ type _cogentcore_org_core_core_ButtonEmbedder struct {
func (W _cogentcore_org_core_core_ButtonEmbedder) AsButton() *core.Button { return W.WAsButton() }
+// _cogentcore_org_core_core_FieldWidgeter is an interface wrapper for FieldWidgeter type
+type _cogentcore_org_core_core_FieldWidgeter struct {
+ IValue interface{}
+ WFieldWidget func(field string) core.Value
+}
+
+func (W _cogentcore_org_core_core_FieldWidgeter) FieldWidget(field string) core.Value {
+ return W.WFieldWidget(field)
+}
+
// _cogentcore_org_core_core_Layouter is an interface wrapper for Layouter type
type _cogentcore_org_core_core_Layouter struct {
IValue interface{}
@@ -795,11 +811,11 @@ func (W _cogentcore_org_core_core_ValueSetter) SetWidgetValue(value any) error {
// _cogentcore_org_core_core_Valuer is an interface wrapper for Valuer type
type _cogentcore_org_core_core_Valuer struct {
- IValue interface{}
- WValue func() core.Value
+ IValue interface{}
+ WWidget func() core.Value
}
-func (W _cogentcore_org_core_core_Valuer) Value() core.Value { return W.WValue() }
+func (W _cogentcore_org_core_core_Valuer) Widget() core.Value { return W.WWidget() }
// _cogentcore_org_core_core_Widget is an interface wrapper for Widget type
type _cogentcore_org_core_core_Widget struct {
diff --git a/yaegicore/coresymbols/cogentcore_org-core-htmlcore.go b/yaegicore/coresymbols/cogentcore_org-core-htmlcore.go
index e4cbe9d2c8..7fd2b2a36f 100644
--- a/yaegicore/coresymbols/cogentcore_org-core-htmlcore.go
+++ b/yaegicore/coresymbols/cogentcore_org-core-htmlcore.go
@@ -10,11 +10,11 @@ import (
func init() {
Symbols["cogentcore.org/core/htmlcore/htmlcore"] = map[string]reflect.Value{
// function, constant and variable definitions
- "BindTextEditor": reflect.ValueOf(&htmlcore.BindTextEditor).Elem(),
"ExtractText": reflect.ValueOf(htmlcore.ExtractText),
"ExtractTextExclude": reflect.ValueOf(htmlcore.ExtractTextExclude),
"Get": reflect.ValueOf(htmlcore.Get),
"GetAttr": reflect.ValueOf(htmlcore.GetAttr),
+ "GetLanguage": reflect.ValueOf(htmlcore.GetLanguage),
"GetURLFromFS": reflect.ValueOf(htmlcore.GetURLFromFS),
"GoDocWikilink": reflect.ValueOf(htmlcore.GoDocWikilink),
"HasAttr": reflect.ValueOf(htmlcore.HasAttr),
diff --git a/yaegicore/coresymbols/cogentcore_org-core-paint.go b/yaegicore/coresymbols/cogentcore_org-core-paint.go
index 91ababa6f1..596fcd9446 100644
--- a/yaegicore/coresymbols/cogentcore_org-core-paint.go
+++ b/yaegicore/coresymbols/cogentcore_org-core-paint.go
@@ -13,6 +13,7 @@ func init() {
"ClampBorderRadius": reflect.ValueOf(paint.ClampBorderRadius),
"EdgeBlurFactors": reflect.ValueOf(paint.EdgeBlurFactors),
"NewImageRenderer": reflect.ValueOf(&paint.NewImageRenderer).Elem(),
+ "NewPDFRenderer": reflect.ValueOf(&paint.NewPDFRenderer).Elem(),
"NewPainter": reflect.ValueOf(paint.NewPainter),
"NewSVGRenderer": reflect.ValueOf(&paint.NewSVGRenderer).Elem(),
"NewSourceRenderer": reflect.ValueOf(&paint.NewSourceRenderer).Elem(),
diff --git a/yaegicore/coresymbols/cogentcore_org-core-styles-units.go b/yaegicore/coresymbols/cogentcore_org-core-styles-units.go
index 4af7681ff1..3c831cd662 100644
--- a/yaegicore/coresymbols/cogentcore_org-core-styles-units.go
+++ b/yaegicore/coresymbols/cogentcore_org-core-styles-units.go
@@ -27,6 +27,7 @@ func init() {
"Mm": reflect.ValueOf(units.Mm),
"MmPerInch": reflect.ValueOf(constant.MakeFromLiteral("25.39999999999999999965305530480463858111761510372161865234375", token.FLOAT, 0)),
"New": reflect.ValueOf(units.New),
+ "NewContext": reflect.ValueOf(units.NewContext),
"Pc": reflect.ValueOf(units.Pc),
"PcPerInch": reflect.ValueOf(constant.MakeFromLiteral("6", token.INT, 0)),
"Ph": reflect.ValueOf(units.Ph),
diff --git a/yaegicore/coresymbols/cogentcore_org-core-text-rich.go b/yaegicore/coresymbols/cogentcore_org-core-text-rich.go
index 0e6aacb400..04309430f2 100644
--- a/yaegicore/coresymbols/cogentcore_org-core-text-rich.go
+++ b/yaegicore/coresymbols/cogentcore_org-core-text-rich.go
@@ -26,7 +26,6 @@ func init() {
"DecorationsN": reflect.ValueOf(rich.DecorationsN),
"DecorationsValues": reflect.ValueOf(rich.DecorationsValues),
"Default": reflect.ValueOf(rich.Default),
- "DefaultSettings": reflect.ValueOf(&rich.DefaultSettings).Elem(),
"DirectionMask": reflect.ValueOf(constant.MakeFromLiteral("4026531840", token.INT, 0)),
"DirectionStart": reflect.ValueOf(constant.MakeFromLiteral("28", token.INT, 0)),
"DirectionsN": reflect.ValueOf(rich.DirectionsN),
@@ -89,6 +88,7 @@ func init() {
"SemiExpanded": reflect.ValueOf(rich.SemiExpanded),
"Semibold": reflect.ValueOf(rich.Semibold),
"Serif": reflect.ValueOf(rich.Serif),
+ "Settings": reflect.ValueOf(&rich.Settings).Elem(),
"SlantMask": reflect.ValueOf(constant.MakeFromLiteral("2048", token.INT, 0)),
"SlantNormal": reflect.ValueOf(rich.SlantNormal),
"SlantStart": reflect.ValueOf(constant.MakeFromLiteral("11", token.INT, 0)),
@@ -117,17 +117,17 @@ func init() {
"WeightsValues": reflect.ValueOf(rich.WeightsValues),
// type definitions
- "Decorations": reflect.ValueOf((*rich.Decorations)(nil)),
- "Directions": reflect.ValueOf((*rich.Directions)(nil)),
- "Family": reflect.ValueOf((*rich.Family)(nil)),
- "FontName": reflect.ValueOf((*rich.FontName)(nil)),
- "Hyperlink": reflect.ValueOf((*rich.Hyperlink)(nil)),
- "Settings": reflect.ValueOf((*rich.Settings)(nil)),
- "Slants": reflect.ValueOf((*rich.Slants)(nil)),
- "Specials": reflect.ValueOf((*rich.Specials)(nil)),
- "Stretch": reflect.ValueOf((*rich.Stretch)(nil)),
- "Style": reflect.ValueOf((*rich.Style)(nil)),
- "Text": reflect.ValueOf((*rich.Text)(nil)),
- "Weights": reflect.ValueOf((*rich.Weights)(nil)),
+ "Decorations": reflect.ValueOf((*rich.Decorations)(nil)),
+ "Directions": reflect.ValueOf((*rich.Directions)(nil)),
+ "Family": reflect.ValueOf((*rich.Family)(nil)),
+ "FontName": reflect.ValueOf((*rich.FontName)(nil)),
+ "Hyperlink": reflect.ValueOf((*rich.Hyperlink)(nil)),
+ "SettingsData": reflect.ValueOf((*rich.SettingsData)(nil)),
+ "Slants": reflect.ValueOf((*rich.Slants)(nil)),
+ "Specials": reflect.ValueOf((*rich.Specials)(nil)),
+ "Stretch": reflect.ValueOf((*rich.Stretch)(nil)),
+ "Style": reflect.ValueOf((*rich.Style)(nil)),
+ "Text": reflect.ValueOf((*rich.Text)(nil)),
+ "Weights": reflect.ValueOf((*rich.Weights)(nil)),
}
}
diff --git a/yaegicore/yaegicore.go b/yaegicore/yaegicore.go
index 05f57b8188..974a930e42 100644
--- a/yaegicore/yaegicore.go
+++ b/yaegicore/yaegicore.go
@@ -17,7 +17,6 @@ import (
"cogentcore.org/core/content"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
- "cogentcore.org/core/htmlcore"
"cogentcore.org/core/text/textcore"
"cogentcore.org/core/yaegicore/basesymbols"
"cogentcore.org/core/yaegicore/coresymbols"
@@ -51,7 +50,7 @@ type Interpreter interface {
}
func init() {
- htmlcore.BindTextEditor = BindTextEditor
+ content.BindTextEditor = BindTextEditor
content.NewPageInitFunc = ResetGoalInterpreter
coresymbols.Symbols["."] = map[string]reflect.Value{} // make "." available for use
basesymbols.Symbols["."] = map[string]reflect.Value{} // make "." available for use
@@ -103,7 +102,7 @@ func getInterpreter(language string) (in Interpreter, new bool, err error) {
// BindTextEditor binds the given text editor to a yaegi interpreter
// such that the contents of the text editor are interpreted as code
// of the given language, which is run in the context of the given parent widget.
-// It is used as the default value of [htmlcore.BindTextEditor].
+// It is used as the default value of [content.BindTextEditor].
func BindTextEditor(ed *textcore.Editor, parent *core.Frame, language string) {
oc := func() {
in, new, err := getInterpreter(language)