diff --git a/base/keylist/keylist_test.go b/base/keylist/keylist_test.go index b88876ecf0..76ca1c539a 100644 --- a/base/keylist/keylist_test.go +++ b/base/keylist/keylist_test.go @@ -32,6 +32,15 @@ func TestKeyList(t *testing.T) { assert.Equal(t, 0, kl.Values[1]) assert.Equal(t, 2, kl.IndexByKey("key2")) + kl.DeleteByKey("new0") + assert.Equal(t, 0, kl.Values[0]) + assert.Equal(t, 2, kl.Values[1]) + assert.Equal(t, 1, kl.IndexByKey("key2")) + + kl.Add("new0", 3) + assert.Equal(t, 3, kl.Values[2]) + assert.Equal(t, 2, kl.IndexByKey("new0")) + // nm := Make([]KeyValue[string, int]{{"one", 1}, {"two", 2}, {"three", 3}}) // assert.Equal(t, 3, nm.Values[2]) // assert.Equal(t, 2, nm.Values[1]) diff --git a/base/tiered/tiered.go b/base/tiered/tiered.go index ab22d9e01f..895476ec60 100644 --- a/base/tiered/tiered.go +++ b/base/tiered/tiered.go @@ -25,10 +25,10 @@ type Tiered[T any] struct { // Do calls the given function for each tier, // going through first, then normal, then final. -func (t *Tiered[T]) Do(f func(T)) { - f(t.First) - f(t.Normal) - f(t.Final) +func (t *Tiered[T]) Do(f func(*T)) { + f(&t.First) + f(&t.Normal) + f(&t.Final) } // DoWith calls the given function with each tier of this tiered diff --git a/colors/gradient/gradient.go b/colors/gradient/gradient.go index caf8233625..53fac18f67 100644 --- a/colors/gradient/gradient.go +++ b/colors/gradient/gradient.go @@ -82,14 +82,14 @@ type Base struct { //types:add -setters // Stop represents a single stop in a gradient type Stop struct { - // the color of the stop. these should be fully opaque, + // Color of the stop. These should be fully opaque, // with opacity specified separately, for best results, as is done in SVG etc. - Color color.Color + Color color.RGBA - // the position of the stop between 0 and 1 + // Pos is the position of the stop in normalized units between 0 and 1. Pos float32 - // Opacity is the 0-1 level of opacity for this stop + // Opacity is the 0-1 level of opacity for this stop. Opacity float32 } diff --git a/colors/gradient/gradient_test.go b/colors/gradient/gradient_test.go index 94c0abd29e..b5670c7ae8 100644 --- a/colors/gradient/gradient_test.go +++ b/colors/gradient/gradient_test.go @@ -144,15 +144,26 @@ func TestRenderRadial(t *testing.T) { imagex.Assert(t, img, "radial-user-space") } -// func matToRasterx(mat *math32.Matrix2) rasterx.Matrix2D { -// // A = XX -// // B = YX -// // C = XY -// // D = YY -// // E = X0 -// // F = Y0 -// return rasterx.Matrix2D{float64(mat.XX), float64(mat.YX), float64(mat.XY), float64(mat.YY), float64(mat.X0), float64(mat.Y0)} -// } +func TestRenderRadialRotate(t *testing.T) { + r := image.Rectangle{Max: image.Point{128, 128}} + b := math32.B2FromRect(r) + img := image.NewRGBA(r) + g := CopyOf(radialTransformTest) + // g.AsBase().Blend = colors.HCT + g.Update(1, b, math32.Identity2()) + draw.Draw(img, img.Bounds(), g, image.Point{}, draw.Src) + imagex.Assert(t, img, "radial-rot") + + xf := math32.Rotate2D(math32.DegToRad(-45)) + ug := CopyOf(g).(*Radial) + ug.SetUnits(UserSpaceOnUse) + ug.Center.SetMul(ug.Box.Size()) + ug.Focal.SetMul(ug.Box.Size()) + ug.Radius.SetMul(ug.Box.Size()) + ug.Update(1, b, xf) + draw.Draw(img, img.Bounds(), ug, image.Point{}, draw.Src) + imagex.Assert(t, img, "radial-rot-user-space") +} func compareTol(t *testing.T, a, c float32) { if math32.Abs(a-c) > 1.0e-5 { diff --git a/colors/gradient/parse.go b/colors/gradient/parse.go index aac36bb200..644d928090 100644 --- a/colors/gradient/parse.go +++ b/colors/gradient/parse.go @@ -59,7 +59,6 @@ func FromString(str string, ctx ...colors.Context) (image.Image, error) { // TODO(kai): do we need to clone? return img, nil } - str = strings.TrimSpace(str) if strings.HasPrefix(str, "url(") { img := cc.ImageByURL(str) @@ -69,6 +68,9 @@ func FromString(str string, ctx ...colors.Context) (image.Image, error) { return img, nil } str = strings.ToLower(str) + if str == "none" || str == "" { + return nil, nil + } grad := "-gradient" gidx := strings.Index(str, grad) @@ -126,10 +128,14 @@ func FromAny(val any, ctx ...colors.Context) (image.Image, error) { switch v := val.(type) { case color.Color: return colors.Uniform(v), nil + case *color.Color: + return colors.Uniform(*v), nil case image.Image: return v, nil case string: return FromString(v, ctx...) + case *string: + return FromString(*v, ctx...) } return nil, fmt.Errorf("gradient.FromAny: got unsupported type %T", val) } @@ -498,9 +504,6 @@ func readFraction(v string) (float32, error) { } f := float32(f64) f /= d - if f < 0 { - f = 0 - } return f, nil } diff --git a/colors/gradient/radial.go b/colors/gradient/radial.go index 2448a27912..d87c0b655c 100644 --- a/colors/gradient/radial.go +++ b/colors/gradient/radial.go @@ -31,6 +31,8 @@ type Radial struct { //types:add -setters rCenter math32.Vector2 rFocal math32.Vector2 rRadius math32.Vector2 + + rotTrans math32.Matrix2 } var _ Gradient = &Radial{} @@ -62,6 +64,7 @@ func (r *Radial) Update(opacity float32, box math32.Box2, objTransform math32.Ma r.Box = box r.Opacity = opacity r.updateBase() + r.rotTrans = math32.Identity2() c, f, rs := r.Center, r.Focal, r.Radius sz := r.Box.Size() @@ -70,12 +73,12 @@ func (r *Radial) Update(opacity float32, box math32.Box2, objTransform math32.Ma f = r.Box.Min.Add(sz.Mul(f)) rs.SetMul(sz) } else { - c = r.Transform.MulVector2AsPoint(c) - f = r.Transform.MulVector2AsPoint(f) - rs = r.Transform.MulVector2AsVector(rs) - c = objTransform.MulVector2AsPoint(c) - f = objTransform.MulVector2AsPoint(f) - rs = objTransform.MulVector2AsVector(rs) + ct := objTransform.Mul(r.Transform) + c = ct.MulVector2AsPoint(c) + f = ct.MulVector2AsPoint(f) + _, _, phi, sx, sy, _ := ct.Decompose() + r.rotTrans = math32.Rotate2D(phi) + rs.SetMul(math32.Vec2(sx, sy)) } if c != f { f.SetDiv(rs) @@ -111,7 +114,7 @@ func (r *Radial) At(x, y int) color.Color { if r.Units == ObjectBoundingBox { pt = r.boxTransform.MulVector2AsPoint(pt) } - d := pt.Sub(r.rCenter) + d := r.rotTrans.MulVector2AsVector(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) } @@ -123,6 +126,7 @@ func (r *Radial) At(x, y int) color.Color { if r.Units == ObjectBoundingBox { pt = r.boxTransform.MulVector2AsPoint(pt) } + pt = r.rotTrans.MulVector2AsVector(pt) e := pt.Div(r.rRadius) t1, intersects := rayCircleIntersectionF(e, r.rFocal, r.rCenter, 1) diff --git a/colors/gradient/typegen.go b/colors/gradient/typegen.go index 929f6aad98..714b9e71ef 100644 --- a/colors/gradient/typegen.go +++ b/colors/gradient/typegen.go @@ -47,7 +47,7 @@ func (t *Linear) SetStart(v math32.Vector2) *Linear { t.Start = v; return t } // the ending point of the gradient (x2 and y2 in SVG) func (t *Linear) SetEnd(v math32.Vector2) *Linear { t.End = v; return t } -var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/colors/gradient.Radial", IDName: "radial", Doc: "Radial represents a radial gradient. It implements the [image.Image] interface.", Directives: []types.Directive{{Tool: "types", Directive: "add", Args: []string{"-setters"}}}, Embeds: []types.Field{{Name: "Base"}}, Fields: []types.Field{{Name: "Center", Doc: "the center point of the gradient (cx and cy in SVG)"}, {Name: "Focal", Doc: "the focal point of the gradient (fx and fy in SVG)"}, {Name: "Radius", Doc: "the radius of the gradient (rx and ry in SVG)"}, {Name: "rCenter", Doc: "current render version -- transformed by object matrix"}, {Name: "rFocal", Doc: "current render version -- transformed by object matrix"}, {Name: "rRadius", Doc: "current render version -- transformed by object matrix"}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/colors/gradient.Radial", IDName: "radial", Doc: "Radial represents a radial gradient. It implements the [image.Image] interface.", Directives: []types.Directive{{Tool: "types", Directive: "add", Args: []string{"-setters"}}}, Embeds: []types.Field{{Name: "Base"}}, Fields: []types.Field{{Name: "Center", Doc: "the center point of the gradient (cx and cy in SVG)"}, {Name: "Focal", Doc: "the focal point of the gradient (fx and fy in SVG)"}, {Name: "Radius", Doc: "the radius of the gradient (rx and ry in SVG)"}, {Name: "rCenter", Doc: "computed current render versions transformed by object matrix"}, {Name: "rFocal"}, {Name: "rRadius"}, {Name: "rotTrans"}}}) // SetCenter sets the [Radial.Center]: // the center point of the gradient (cx and cy in SVG) diff --git a/colors/image.go b/colors/image.go index c0c962632f..60881c7ba5 100644 --- a/colors/image.go +++ b/colors/image.go @@ -12,6 +12,9 @@ import ( // Uniform returns a new [image.Uniform] filled completely with the given color. // See [ToUniform] for the converse. func Uniform(c color.Color) image.Image { + if c == nil { + return nil + } return image.NewUniform(c) } @@ -24,6 +27,20 @@ func ToUniform(img image.Image) color.RGBA { return AsRGBA(img.At(0, 0)) } +// CloneUniform returns a copy of a Uniform image color, if image is one. +// Otherwise just returns the image back. This is useful when modifying +// an image color, to ensure that it doesn't modify all users of this image. +func CloneUniform(img image.Image) image.Image { + if img == nil { + return nil + } + u, ok := img.(*image.Uniform) + if !ok { + return img + } + return image.NewUniform(AsRGBA(u.C)) +} + // Pattern returns a new unbounded [image.Image] represented by the given pattern function. func Pattern(f func(x, y int) color.Color) image.Image { return &pattern{f} diff --git a/content/typegen.go b/content/typegen.go index 9e812e51f1..62b815c568 100644 --- a/content/typegen.go +++ b/content/typegen.go @@ -3,12 +3,18 @@ package content import ( + "cogentcore.org/core/text/csl" "cogentcore.org/core/tree" "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: "Frame"}}, 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: "pages", Doc: "pages are the pages that constitute the content."}}}) +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."}}}) // NewContent returns a new [Content] with the given optional parent: // Content manages and displays the content of a set of pages. func NewContent(parent ...tree.Node) *Content { return tree.New[Content](parent...) } + +// SetReferences sets the [Content.References]: +// References is a list of references used for generating citation text +// for literature reference wikilinks in the format [[@CiteKey]]. +func (t *Content) SetReferences(v *csl.KeyList) *Content { t.References = v; return t } diff --git a/core/blinker.go b/core/blinker.go deleted file mode 100644 index bcc84a9f29..0000000000 --- a/core/blinker.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2023, 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 core - -import ( - "sync" - "time" -) - -// Blinker manages the logistics of blinking things, such as cursors. -type Blinker struct { - - // Ticker is the [time.Ticker] used to control the blinking. - Ticker *time.Ticker - - // Widget is the current widget subject to blinking. - Widget Widget - - // Func is the function called every tick. - // The mutex is locked at the start but must be unlocked - // when transitioning to locking the render context mutex. - Func func() - - // Use Lock and Unlock on blinker directly. - sync.Mutex -} - -// Blink sets up the blinking; does nothing if already set up. -func (bl *Blinker) Blink(dur time.Duration) { - bl.Lock() - defer bl.Unlock() - if bl.Ticker != nil { - return - } - bl.Ticker = time.NewTicker(dur) - go bl.blinkLoop() -} - -// SetWidget sets the [Blinker.Widget] under mutex lock. -func (bl *Blinker) SetWidget(w Widget) { - bl.Lock() - defer bl.Unlock() - bl.Widget = w -} - -// ResetWidget sets [Blinker.Widget] to nil if it is currently set to the given one. -func (bl *Blinker) ResetWidget(w Widget) { - bl.Lock() - defer bl.Unlock() - if bl.Widget == w { - bl.Widget = nil - } -} - -// blinkLoop is the blinker's main control loop. -func (bl *Blinker) blinkLoop() { - for { - bl.Lock() - if bl.Ticker == nil { - bl.Unlock() - return // shutdown.. - } - bl.Unlock() - <-bl.Ticker.C - bl.Lock() - if bl.Widget == nil { - bl.Unlock() - continue - } - wb := bl.Widget.AsWidget() - if wb.Scene == nil || wb.Scene.Stage.Main == nil { - bl.Widget = nil - bl.Unlock() - continue - } - bl.Func() // we enter the function locked - } - -} - -// QuitClean is a cleanup function to pass to [TheApp.AddQuitCleanFunc] -// that breaks out of the ticker loop. -func (bl *Blinker) QuitClean() { - bl.Lock() - defer bl.Unlock() - if bl.Ticker != nil { - tck := bl.Ticker - bl.Ticker = nil - bl.Widget = nil - tck.Stop() - } -} diff --git a/core/button.go b/core/button.go index 985af0ddd5..0120127c5f 100644 --- a/core/button.go +++ b/core/button.go @@ -130,6 +130,7 @@ func (bt *Button) Init() { } } s.Font.Size.Dp(14) // Button font size is used for text font size + s.IconSize.Set(units.Em(18.0 / 14)) s.Gap.Zero() s.CenterAll() @@ -209,9 +210,6 @@ func (bt *Button) Init() { } if bt.Icon.IsSet() { tree.AddAt(p, "icon", func(w *Icon) { - w.Styler(func(s *styles.Style) { - s.Font.Size.Dp(18) - }) w.Updater(func() { w.SetIcon(bt.Icon) }) @@ -251,10 +249,7 @@ func (bt *Button) Init() { }) tree.AddAt(p, "indicator", func(w *Icon) { w.Styler(func(s *styles.Style) { - s.Min.X.Dp(18) - s.Min.Y.Dp(18) - s.Margin.Zero() - s.Padding.Zero() + s.IconSize.Set(units.Dp(18)) // independent from Em }) w.Updater(func() { w.SetIcon(bt.Indicator) diff --git a/core/chooser.go b/core/chooser.go index a305d1c335..9a3c45e135 100644 --- a/core/chooser.go +++ b/core/chooser.go @@ -183,6 +183,7 @@ func (ch *Chooser) Init() { s.Text.Align = text.Center s.Border.Radius = styles.BorderRadiusSmall s.Padding.Set(units.Dp(8), units.Dp(16)) + s.IconSize.Set(units.Em(18.0 / 16)) s.CenterAll() // textfield handles everything if ch.Editable { @@ -354,6 +355,7 @@ func (ch *Chooser) Init() { w.Maker(func(p *tree.Plan) { tree.AddInit(p, "trail-icon", func(w *Button) { w.Styler(func(s *styles.Style) { + s.IconSize.Set(units.Dp(16)) // independent from Em // indicator does not need to be focused s.SetAbilities(false, abilities.Focusable) }) @@ -380,6 +382,7 @@ func (ch *Chooser) Init() { if !ch.Editable && !ch.IsReadOnly() { tree.AddAt(p, "indicator", func(w *Icon) { w.Styler(func(s *styles.Style) { + s.IconSize.Set(units.Dp(16)) // independent from Em s.Justify.Self = styles.End }) w.Updater(func() { @@ -588,6 +591,18 @@ func (ch *Chooser) makeItemsMenu(m *Scene) { NewSeparator(m) } bt := NewButton(m).SetText(it.GetText()).SetIcon(it.Icon).SetTooltip(it.Tooltip) + bt.Styler(func(s *styles.Style) { + s.IconSize = ch.Styles.IconSize + // Chooser has a bigger font size by default, so we have to normalize + // to account for the smaller default button font size. + if s.IconSize.X.Unit == units.UnitEm { + s.IconSize.X.Value *= 16.0 / 14 + } + if s.IconSize.Y.Unit == units.UnitEm { + s.IconSize.Y.Value *= 16.0 / 14 + } + }) + bt.SetSelected(i == ch.CurrentIndex) bt.OnClick(func(e events.Event) { ch.selectItemEvent(i) diff --git a/core/colorpicker.go b/core/colorpicker.go index 2e5dbc2d82..ef010ab9d2 100644 --- a/core/colorpicker.go +++ b/core/colorpicker.go @@ -67,8 +67,8 @@ func (cp *ColorPicker) Init() { tree.AddChild(w, func(w *TextField) { w.SetTooltip("Hex color") w.Styler(func(s *styles.Style) { - s.Min.X.Em(5) - s.Max.X.Em(5) + s.Min.X.Em(6) + s.Max.X.Em(6) }) w.Updater(func() { w.SetText(colors.AsHex(cp.Color)) @@ -95,6 +95,10 @@ func (cp *ColorPicker) Init() { w.SetMin(0).SetMax(360) w.SetTooltip("The hue, which is the spectral identity of the color (red, green, blue, etc) in degrees") w.OnInput(func(e events.Event) { + cp.Color.SetHue(w.Value) + cp.UpdateInput() + }) + w.OnChange(func(e events.Event) { cp.Color.SetHue(w.Value) cp.UpdateChange() }) @@ -118,6 +122,10 @@ func (cp *ColorPicker) Init() { w.SetMax(cp.Color.MaximumChroma()) }) w.OnInput(func(e events.Event) { + cp.Color.SetChroma(w.Value) + cp.UpdateInput() + }) + w.OnChange(func(e events.Event) { cp.Color.SetChroma(w.Value) cp.UpdateChange() }) @@ -138,6 +146,10 @@ func (cp *ColorPicker) Init() { w.SetMin(0).SetMax(100) w.SetTooltip("The tone, which is the lightness of the color") w.OnInput(func(e events.Event) { + cp.Color.SetTone(w.Value) + cp.UpdateInput() + }) + w.OnChange(func(e events.Event) { cp.Color.SetTone(w.Value) cp.UpdateChange() }) @@ -158,6 +170,10 @@ func (cp *ColorPicker) Init() { w.SetMin(0).SetMax(1) w.SetTooltip("The opacity of the color") w.OnInput(func(e events.Event) { + cp.Color.SetColor(colors.WithAF32(cp.Color, w.Value)) + cp.UpdateInput() + }) + w.OnChange(func(e events.Event) { cp.Color.SetColor(colors.WithAF32(cp.Color, w.Value)) cp.UpdateChange() }) diff --git a/core/enumgen.go b/core/enumgen.go index 10485d67fb..8196e543ad 100644 --- a/core/enumgen.go +++ b/core/enumgen.go @@ -178,16 +178,16 @@ func (i *MeterTypes) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "MeterTypes") } -var _renderWindowFlagsValues = []renderWindowFlags{0, 1, 2, 3, 4, 5} +var _renderWindowFlagsValues = []renderWindowFlags{0, 1, 2, 3, 4} // renderWindowFlagsN is the highest valid value for type renderWindowFlags, plus one. -const renderWindowFlagsN renderWindowFlags = 6 +const renderWindowFlagsN renderWindowFlags = 5 -var _renderWindowFlagsValueMap = map[string]renderWindowFlags{`IsRendering`: 0, `RenderSkipped`: 1, `Resize`: 2, `StopEventLoop`: 3, `Closing`: 4, `GotFocus`: 5} +var _renderWindowFlagsValueMap = map[string]renderWindowFlags{`Resize`: 0, `StopEventLoop`: 1, `Closing`: 2, `GotFocus`: 3, `RenderSkipped`: 4} -var _renderWindowFlagsDescMap = map[renderWindowFlags]string{0: `winIsRendering indicates that the renderAsync function is running.`, 1: `winRenderSkipped indicates that a render update was skipped, so another update will be run to ensure full updating.`, 2: `winResize indicates that the window was just resized.`, 3: `winStopEventLoop indicates that the event loop should be stopped.`, 4: `winClosing is whether the window is closing.`, 5: `winGotFocus indicates that have we received focus.`} +var _renderWindowFlagsDescMap = map[renderWindowFlags]string{0: `winResize indicates that the window was just resized.`, 1: `winStopEventLoop indicates that the event loop should be stopped.`, 2: `winClosing is whether the window is closing.`, 3: `winGotFocus indicates that have we received focus.`, 4: `winRenderSkipped indicates that a render update was skipped, so another update will be run to ensure full updating.`} -var _renderWindowFlagsMap = map[renderWindowFlags]string{0: `IsRendering`, 1: `RenderSkipped`, 2: `Resize`, 3: `StopEventLoop`, 4: `Closing`, 5: `GotFocus`} +var _renderWindowFlagsMap = map[renderWindowFlags]string{0: `Resize`, 1: `StopEventLoop`, 2: `Closing`, 3: `GotFocus`, 4: `RenderSkipped`} // String returns the string representation of this renderWindowFlags value. func (i renderWindowFlags) String() string { return enums.BitFlagString(i, _renderWindowFlagsValues) } diff --git a/core/events.go b/core/events.go index f2e38b94e3..131d9b4312 100644 --- a/core/events.go +++ b/core/events.go @@ -11,6 +11,7 @@ import ( "log/slog" "os" "path/filepath" + "slices" "strings" "sync" "time" @@ -54,6 +55,9 @@ type Events struct { // stack of sprites with mouse pointer in BBox, with any listeners present. spriteInBBox []*Sprite + // stack of hovered sprites: have mouse pointer in BBox. + spriteHovers []*Sprite + // currently pressing sprite. spritePress *Sprite @@ -174,6 +178,8 @@ func (em *Events) handleEvent(e events.Event) { em.handlePosEvent(e) case e.NeedsFocus(): em.handleFocusEvent(e) + default: // eg. os events + em.scene.HandleEvent(e) } } @@ -257,12 +263,14 @@ func (em *Events) handlePosEvent(e events.Event) { em.spriteSlide.handleEvent(e) em.spriteSlide.send(events.SlideMove, e) e.SetHandled() + em.setCursorFromStyle() return } if !tree.IsNil(em.slide) { em.slide.AsWidget().HandleEvent(e) em.slide.AsWidget().Send(events.SlideMove, e) e.SetHandled() + em.setCursorFromStyle() return } case events.Scroll: @@ -273,6 +281,7 @@ func (em *Events) handlePosEvent(e events.Event) { if e.IsHandled() { em.lastScrollTime = time.Now() } + em.setCursorFromStyle() return } em.scroll = nil @@ -280,13 +289,11 @@ func (em *Events) handlePosEvent(e events.Event) { } em.spriteInBBox = nil - if et != events.MouseMove { - em.getSpriteInBBox(sc, e.WindowPos()) - - if len(em.spriteInBBox) > 0 { - if em.handleSpriteEvent(e) { - return - } + em.getSpriteInBBox(sc, e.WindowPos()) + if len(em.spriteInBBox) > 0 { + if em.handleSpriteEvent(e) { + em.setCursorFromStyle() + return } } @@ -298,6 +305,7 @@ func (em *Events) handlePosEvent(e events.Event) { if DebugSettings.EventTrace && et != events.MouseMove { log.Println("Nothing in bbox:", sc.Geom.TotalBBox, "pos:", pos) } + em.setCursorFromStyle() return } @@ -468,6 +476,7 @@ func (em *Events) handlePosEvent(e events.Event) { case events.Left: if sc.selectedWidgetChan != nil { sc.selectedWidgetChan <- up + em.setCursorFromStyle() return } dcInTime := time.Since(em.lastClickTime) < DeviceSettings.DoubleClickInterval @@ -534,46 +543,19 @@ func (em *Events) handlePosEvent(e events.Event) { em.scene.HandleEvent(e) } } - - // we need to handle cursor after all of the events so that - // we get the latest cursor if it changes based on the state - - cursorSet := false - for i := n - 1; i >= 0; i-- { - w := em.mouseInBBox[i] - wb := w.AsWidget() - if !cursorSet && wb.Styles.Cursor != cursors.None { - em.setCursor(wb.Styles.Cursor) - cursorSet = true - } - } + em.setCursorFromStyle() } // updateHovers updates the hovered widgets based on current // widgets in bounding box. func (em *Events) updateHovers(hov, prev []Widget, e events.Event, enter, leave events.Types) []Widget { for _, prv := range prev { - stillIn := false - for _, cur := range hov { - if prv == cur { - stillIn = true - break - } - } - if !stillIn && !tree.IsNil(prv) { + if !slices.Contains(hov, prv) && !tree.IsNil(prv) { prv.AsWidget().Send(leave, e) } } - for _, cur := range hov { - wasIn := false - for _, prv := range prev { - if prv == cur { - wasIn = true - break - } - } - if !wasIn { + if !slices.Contains(prev, cur) { cur.AsWidget().Send(enter, e) } } @@ -581,6 +563,22 @@ func (em *Events) updateHovers(hov, prev []Widget, e events.Event, enter, leave return hov } +// updateSpriteHovers updates the hovered sprites based on current +// sprites in bounding box. +func (em *Events) updateSpriteHovers(hov, prev []*Sprite, e events.Event, enter, leave events.Types) []*Sprite { + for _, prv := range prev { + if !slices.Contains(hov, prv) { + prv.send(leave, e) + } + } + for _, cur := range hov { + if !slices.Contains(prev, cur) { + cur.send(enter, e) + } + } + return hov +} + // topLongHover returns the top-most LongHoverable widget among the Hovers func (em *Events) topLongHover() Widget { var deep Widget @@ -793,9 +791,9 @@ func (em *Events) DragStart(w Widget, data any, e events.Event) { } em.drag = w em.dragData = data - sp := NewSprite(dragSpriteName, image.Point{}, e.WindowPos()) - sp.grabRenderFrom(w) // TODO: maybe show the number of items being dragged - sp.Pixels = clone.AsRGBA(gradient.ApplyOpacity(sp.Pixels, 0.5)) + // TODO: maybe show the number of items being dragged + img := clone.AsRGBA(gradient.ApplyOpacity(grabRenderFrom(w), 0.5)) + sp := NewImageSprite(dragSpriteName, e.WindowPos(), img) sp.Active = true ms.Sprites.Add(sp) } @@ -807,11 +805,11 @@ func (em *Events) dragMove(e events.Event) { return } sp, ok := ms.Sprites.SpriteByName(dragSpriteName) - if !ok { + if !ok || sp == nil { fmt.Println("Drag sprite not found") return } - sp.Geom.Pos = e.WindowPos() + sp.SetPos(e.WindowPos()) for _, w := range em.dragHovers { w.AsWidget().ScrollToThis() } @@ -823,7 +821,7 @@ func (em *Events) dragClearSprite() { if ms == nil { return } - ms.Sprites.InactivateSprite(dragSpriteName) + ms.Sprites.Delete(dragSpriteName) } // DragMenuAddModText adds info about key modifiers for a drag drop menu. @@ -907,6 +905,21 @@ func (em *Events) setCursor(cur cursors.Cursor) { errors.Log(system.TheApp.Cursor(win.SystemWindow).Set(cur)) } +// setCursorFromStyle sets the window cursor to the cursor of the bottom-most +// widget in the mouseInBBox stack that has a Styles.Cursor set. +// This should be called after every event pass. +func (em *Events) setCursorFromStyle() { + n := len(em.mouseInBBox) + for i := n - 1; i >= 0; i-- { + w := em.mouseInBBox[i] + wb := w.AsWidget() + if wb.Styles.Cursor != cursors.None { + em.setCursor(wb.Styles.Cursor) + break + } + } +} + // focusClear saves current focus to FocusPrev func (em *Events) focusClear() bool { if !tree.IsNil(em.focus) { @@ -1302,19 +1315,19 @@ func (em *Events) triggerShortcut(chord key.Chord) bool { func (em *Events) getSpriteInBBox(sc *Scene, pos image.Point) { st := sc.Stage - for _, kv := range st.Sprites.Order { - sp := kv.Value - if !sp.Active { - continue - } - if sp.listeners == nil { - continue - } - r := sp.Geom.Bounds() - if pos.In(r) { - em.spriteInBBox = append(em.spriteInBBox, sp) + st.Sprites.Do(func(sl *SpriteList) { + for _, sp := range sl.Values { + if sp == nil || !sp.Active { + continue + } + if sp.listeners == nil { + continue + } + if pos.In(sp.EventBBox) { + em.spriteInBBox = append(em.spriteInBBox, sp) + } } - } + }) } // handleSpriteEvent handles the given event with sprites @@ -1348,5 +1361,6 @@ loop: } } } + em.spriteHovers = em.updateSpriteHovers(em.spriteInBBox, em.spriteHovers, e, events.MouseEnter, events.MouseLeave) return e.IsHandled() } diff --git a/core/funcbutton.go b/core/funcbutton.go index dbc83d3993..b7630c9354 100644 --- a/core/funcbutton.go +++ b/core/funcbutton.go @@ -17,6 +17,7 @@ import ( "cogentcore.org/core/styles" "cogentcore.org/core/styles/abilities" "cogentcore.org/core/styles/states" + "cogentcore.org/core/tree" "cogentcore.org/core/types" ) @@ -357,12 +358,21 @@ func (fb *FuncButton) CallFunc() { // If there is a single value button, automatically // open its dialog instead of this one if len(fb.Args) == 1 { + curWin := currentRenderWindow sv.UpdateWidget() // need to update first bt := AsButton(sv.Child(1)) if bt != nil { bt.OnFinal(events.Change, func(e events.Event) { // the dialog for the argument has been accepted, so we call the function + async := false + if !tree.IsNil(ctx) && currentRenderWindow != curWin { // calling from another window, must lock + async = true + ctx.AsWidget().AsyncLock() + } accept() + if async { + ctx.AsWidget().AsyncUnlock() + } }) bt.Scene = fb.Scene // we must use this scene for context bt.Send(events.Click) diff --git a/core/icon.go b/core/icon.go index b9ef5572d3..558b988480 100644 --- a/core/icon.go +++ b/core/icon.go @@ -14,7 +14,6 @@ import ( "cogentcore.org/core/colors/gradient" "cogentcore.org/core/icons" "cogentcore.org/core/styles" - "cogentcore.org/core/styles/units" "cogentcore.org/core/svg" "golang.org/x/image/draw" ) @@ -46,8 +45,8 @@ func (ic *Icon) WidgetValue() any { return &ic.Icon } func (ic *Icon) Init() { ic.WidgetBase.Init() - ic.Styler(func(s *styles.Style) { - s.Min.Set(units.Em(1)) + ic.FinalStyler(func(s *styles.Style) { + s.Min = s.IconSize }) } diff --git a/core/layout.go b/core/layout.go index ef50ba20d1..89c5bcd6d5 100644 --- a/core/layout.go +++ b/core/layout.go @@ -16,68 +16,6 @@ import ( "cogentcore.org/core/tree" ) -// Layout uses 3 Size passes, 2 Position passes: -// -// SizeUp: (bottom-up) gathers Actual sizes from our Children & Parts, -// based on Styles.Min / Max sizes and actual content sizing -// (e.g., text size). Flexible elements (e.g., [Text], Flex Wrap, -// [Toolbar]) should reserve the _minimum_ size possible at this stage, -// and then Grow based on SizeDown allocation. - -// SizeDown: (top-down, multiple iterations possible) provides top-down -// size allocations based initially on Scene available size and -// the SizeUp Actual sizes. If there is extra space available, it is -// allocated according to the Grow factors. -// Flexible elements (e.g., Flex Wrap layouts and Text with word wrap) -// update their Actual size based on available Alloc size (re-wrap), -// to fit the allocated shape vs. the initial bottom-up guess. -// However, do NOT grow the Actual size to match Alloc at this stage, -// as Actual sizes must always represent the minimums (see Position). -// Returns true if any change in Actual size occurred. - -// SizeFinal: (bottom-up) similar to SizeUp but done at the end of the -// Sizing phase: first grows widget Actual sizes based on their Grow -// factors, up to their Alloc sizes. Then gathers this updated final -// actual Size information for layouts to register their actual sizes -// prior to positioning, which requires accurate Actual vs. Alloc -// sizes to perform correct alignment calculations. - -// Position: uses the final sizes to set relative positions within layouts -// according to alignment settings, and Grow elements to their actual -// Alloc size per Styles settings and widget-specific behavior. - -// ScenePos: computes scene-based absolute positions and final BBox -// bounding boxes for rendering, based on relative positions from -// Position step and parents accumulated position and scroll offset. -// This is the only step needed when scrolling (very fast). - -// (Text) Wrapping key principles: -// * Using a heuristic initial box based on expected text area from length -// of Text and aspect ratio based on styled size to get initial layout size. -// This avoids extremes of all horizontal or all vertical initial layouts. -// * Use full Alloc for SizeDown to allocate for what has been reserved. -// * Set Actual to what you actually use (key: start with only styled -// so you don't get hysterisis) -// * Layout always re-gets the actuals for accurate Actual sizing - -// Scroll areas are similar: don't request anything more than Min reservation -// and then expand to Alloc in Final. - -// Note that it is critical to not actually change any bottom-up Actual -// sizing based on the Alloc, during the SizeDown process, as this will -// introduce false constraints on the process: only work with minimum -// Actual "hard" constraints to make sure those are satisfied. Text -// and Wrap elements resize only enough to fit within the Alloc space -// to the extent possible, but do not Grow. -// -// The separate SizeFinal step then finally allows elements to grow -// into their final Alloc space, once all the constraints are satisfied. -// -// This overall top-down / bottom-up logic is used in Flutter: -// https://docs.flutter.dev/resources/architectural-overview#rendering-and-layout -// Here's more links to other layout algorithms: -// https://stackoverflow.com/questions/53911631/gui-layout-algorithms-overview - // LayoutPasses is used for the SizeFromChildren method, // which can potentially compute different sizes for different passes. type LayoutPasses int32 //enums:enum diff --git a/core/render.go b/core/render.go index cac3fdc3ce..d9692dcb90 100644 --- a/core/render.go +++ b/core/render.go @@ -287,7 +287,7 @@ func (sc *Scene) applyStyleScene() { // should be used by Widgets to rebuild things that are otherwise // cached (e.g., Icon, TextCursor). func (sc *Scene) doRebuild() { - sc.Stage.Sprites.Reset() + sc.Stage.Sprites.reset() sc.updateScene() sc.applyStyleScene() sc.layoutRenderScene() diff --git a/core/render_js.go b/core/render_js.go index 277c513686..f6a598efd3 100644 --- a/core/render_js.go +++ b/core/render_js.go @@ -7,11 +7,38 @@ package core import ( + "image" + "cogentcore.org/core/colors" + "cogentcore.org/core/math32" + "cogentcore.org/core/paint" "cogentcore.org/core/paint/renderers/htmlcanvas" "cogentcore.org/core/system/composer" + "golang.org/x/image/draw" ) +// grabRenderFrom grabs the rendered image from the given widget. +// If it returns nil, then the image could not be fetched. +func grabRenderFrom(w Widget) *image.RGBA { + wb := w.AsWidget() + if wb.Geom.TotalBBox.Empty() { // the widget is offscreen + return nil + } + // todo: grab region from canvas! + imgRend := paint.NewImageRenderer(math32.FromPoint(wb.Scene.SceneGeom.Size)) + wb.RenderWidget() + rend := wb.Scene.Painter.RenderDone() + imgRend.Render(rend) + scimg := imgRend.Image() + if scimg == nil { + return nil + } + sz := wb.Geom.TotalBBox.Size() + img := image.NewRGBA(image.Rectangle{Max: sz}) + draw.Draw(img, img.Bounds(), scimg, wb.Geom.TotalBBox.Min, draw.Src) + return img +} + func (ps *paintSource) Draw(c composer.Composer) { cw := c.(*composer.ComposerWeb) rd := ps.renderer.(*htmlcanvas.Renderer) @@ -31,20 +58,6 @@ func (ss *scrimSource) Draw(c composer.Composer) { elem.Get("style").Set("backgroundColor", colors.AsHex(clr)) } -func (ss *spritesSource) Draw(c composer.Composer) { - cw := c.(*composer.ComposerWeb) - for _, sr := range ss.sprites { - elem := cw.Element(ss, "div") // TODO: support full images - if !sr.active { - elem.Get("style").Set("display", "none") - continue - } - elem.Get("style").Set("display", "initial") - cw.SetElementGeom(elem, sr.drawPos, sr.pixels.Bounds().Size()) - elem.Get("style").Set("backgroundColor", colors.AsHex(colors.ToUniform(sr.pixels))) - } -} - func (w *renderWindow) fillInsets(c composer.Composer) { // no-op } diff --git a/core/render_notjs.go b/core/render_notjs.go index 43ad6533e1..e159d98c23 100644 --- a/core/render_notjs.go +++ b/core/render_notjs.go @@ -16,6 +16,23 @@ import ( "golang.org/x/image/draw" ) +// grabRenderFrom grabs the rendered image from the given widget. +// 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() + if scimg == nil { + return nil + } + if wb.Geom.TotalBBox.Empty() { // the widget is offscreen + return nil + } + sz := wb.Geom.TotalBBox.Size() + img := image.NewRGBA(image.Rectangle{Max: sz}) + draw.Draw(img, img.Bounds(), scimg, wb.Geom.TotalBBox.Min, draw.Src) + return img +} + func (ps *paintSource) Draw(c composer.Composer) { cd := c.(*composer.ComposerDrawer) rd := ps.renderer.(*rasterx.Renderer) @@ -34,17 +51,7 @@ func (ss *scrimSource) Draw(c composer.Composer) { cd.Drawer.Copy(image.Point{}, clr, ss.bbox, draw.Over, composer.Unchanged) } -func (ss *spritesSource) Draw(c composer.Composer) { - cd := c.(*composer.ComposerDrawer) - for _, sr := range ss.sprites { - if !sr.active { - continue - } - cd.Drawer.Copy(sr.drawPos, sr.pixels, sr.pixels.Bounds(), draw.Over, composer.Unchanged) - } -} - -//////// fillInsets +//////// fillInsets // fillInsetsSource is a [composer.Source] implementation for fillInsets. type fillInsetsSource struct { diff --git a/core/rendersource.go b/core/rendersource.go index c2a4ca1958..42943e7500 100644 --- a/core/rendersource.go +++ b/core/rendersource.go @@ -7,6 +7,9 @@ package core import ( "image" + "cogentcore.org/core/colors" + "cogentcore.org/core/math32" + "cogentcore.org/core/paint" "cogentcore.org/core/paint/render" "cogentcore.org/core/system/composer" "golang.org/x/image/draw" @@ -57,29 +60,28 @@ type scrimSource struct { //////// Sprites // SpritesSource returns a [composer.Source] for rendering [Sprites]. -func SpritesSource(sprites *Sprites, scpos image.Point) composer.Source { - sprites.Lock() - defer sprites.Unlock() - ss := &spritesSource{} - ss.sprites = make([]spriteRender, len(sprites.Order)) - for i, kv := range sprites.Order { - sp := kv.Value - // note: may need to copy pixels but hoping not.. - sr := spriteRender{drawPos: sp.Geom.Pos.Add(scpos), pixels: sp.Pixels, active: sp.Active} - ss.sprites[i] = sr +func SpritesSource(stage *Stage, mainScene *Scene) composer.Source { + stage.Sprites.Lock() + defer stage.Sprites.Unlock() + + sz := math32.FromPoint(mainScene.SceneGeom.Size) + if stage.spritePainter == nil || stage.spritePainter.State.Size != sz { + stage.spritePainter = paint.NewPainter(sz) + stage.spritePainter.Paint.UnitContext = mainScene.Styles.UnitContext + stage.spriteRenderer = paint.NewSourceRenderer(sz) } - sprites.modified = false - return ss -} - -// spritesSource is a [composer.Source] implementation for [Sprites]. -type spritesSource struct { - sprites []spriteRender -} - -// spriteRender holds info sufficient for rendering a sprite. -type spriteRender struct { - drawPos image.Point - pixels *image.RGBA - active bool + pc := stage.spritePainter + pc.Fill.Color = colors.Uniform(colors.Transparent) + pc.Clear() + stage.Sprites.Do(func(sl *SpriteList) { + for _, sp := range sl.Values { + if sp == nil || !sp.Active || sp.Draw == nil { + continue + } + sp.Draw(stage.spritePainter) + } + }) + stage.Sprites.modified = false + render := stage.spritePainter.RenderDone() + return &paintSource{render: render, renderer: stage.spriteRenderer, drawOp: draw.Over, drawPos: image.Point{}} } diff --git a/core/renderwindow.go b/core/renderwindow.go index f8f11e0533..796d8f2ab5 100644 --- a/core/renderwindow.go +++ b/core/renderwindow.go @@ -9,6 +9,7 @@ import ( "image" "log" "sync" + "sync/atomic" "time" "cogentcore.org/core/base/errors" @@ -101,6 +102,15 @@ type renderWindow struct { // lastResize is the time stamp of last resize event -- used for efficient updating. lastResize time.Time + + lastSpriteDraw time.Time + + // winRenderCounter is maintained under atomic locking to coordinate + // the launching of renderAsync functions and when those functions + // actually complete. Each time one is launched, the counter is incremented + // and each time one completes, it is decremented. This ensures + // everything is synchronized. + winRenderCounter int32 } // newRenderWindow creates a new window with given internal name handle, @@ -129,6 +139,7 @@ func newRenderWindow(name, title string, opts *system.NewWindowOptions) *renderW } if !kv.Value.Scene.Close() { w.flags.SetFlag(false, winClosing) + rc.Unlock() return } } @@ -640,7 +651,7 @@ func (w *renderWindow) renderContext() *renderContext { // there is a moment for other goroutines to acquire the lock and get necessary // updates through (such as in offscreen testing). func (w *renderWindow) renderWindow() { - if w.flags.HasFlag(winIsRendering) { // still doing the last one + if atomic.LoadInt32(&w.winRenderCounter) > 0 { // still working w.flags.SetFlag(true, winRenderSkipped) if DebugSettings.WindowRenderTrace { log.Printf("RenderWindow: still rendering, skipped: %v\n", w.name) @@ -674,6 +685,14 @@ func (w *renderWindow) renderWindow() { } spriteMods := top.Sprites.IsModified() + spriteUpdateTime := SystemSettings.CursorBlinkTime + if spriteUpdateTime == 0 { + spriteUpdateTime = 500 * time.Millisecond + } + if time.Since(w.lastSpriteDraw) > spriteUpdateTime { + spriteMods = true + } + if !spriteMods && !rebuild && !stageMods && !sceneMods { // nothing to do! if w.flags.HasFlag(winRenderSkipped) { w.flags.SetFlag(false, winRenderSkipped) @@ -747,15 +766,12 @@ func (w *renderWindow) renderWindow() { log.Println("GatherScenes: popup:", st.String()) } } - scpos := winScene.SceneGeom.Pos - if TheApp.Platform().IsMobile() { - scpos = image.Point{} - } - cp.Add(SpritesSource(&top.Sprites, scpos), &top.Sprites) + cp.Add(SpritesSource(top, winScene), &top.Sprites) + w.lastSpriteDraw = time.Now() w.SystemWindow.Unlock() if offscreen || w.flags.HasFlag(winResize) || sinceResize < 500*time.Millisecond { - w.flags.SetFlag(true, winIsRendering) + atomic.AddInt32(&w.winRenderCounter, 1) w.renderAsync(cp) if w.flags.HasFlag(winResize) { w.lastResize = time.Now() @@ -765,17 +781,17 @@ func (w *renderWindow) renderWindow() { // note: it is critical to set *before* going into loop // because otherwise we can lose an entire pass before the goroutine starts! // function will turn flag off when it finishes. - w.flags.SetFlag(true, winIsRendering) + atomic.AddInt32(&w.winRenderCounter, 1) go w.renderAsync(cp) } } // renderAsync is the implementation of the main render pass, -// which must be called in a goroutine. It relies on the platform-specific -// [renderWindow.doRender]. +// which is usually called in a goroutine. +// It calls the Compose function on the given composer. func (w *renderWindow) renderAsync(cp composer.Composer) { if !w.SystemWindow.Lock() { - w.flags.SetFlag(false, winIsRendering) // note: comes in with flag set + atomic.AddInt32(&w.winRenderCounter, -1) // fmt.Println("renderAsync SystemWindow lock fail") return } @@ -783,8 +799,8 @@ func (w *renderWindow) renderAsync(cp composer.Composer) { // fmt.Println("start compose") cp.Compose() // pr.End() - w.flags.SetFlag(false, winIsRendering) // note: comes in with flag set w.SystemWindow.Unlock() + atomic.AddInt32(&w.winRenderCounter, -1) } // RenderSource returns the [render.Render] state from the [Scene.Painter]. @@ -798,15 +814,8 @@ func (sc *Scene) RenderSource(op draw.Op) composer.Source { type renderWindowFlags int64 //enums:bitflag -trim-prefix win const ( - // winIsRendering indicates that the renderAsync function is running. - winIsRendering renderWindowFlags = iota - - // winRenderSkipped indicates that a render update was skipped, so - // another update will be run to ensure full updating. - winRenderSkipped - // winResize indicates that the window was just resized. - winResize + winResize renderWindowFlags = iota // winStopEventLoop indicates that the event loop should be stopped. winStopEventLoop @@ -816,4 +825,8 @@ const ( // winGotFocus indicates that have we received focus. winGotFocus + + // winRenderSkipped indicates that a render update was skipped, so + // another update will be run to ensure full updating. + winRenderSkipped ) diff --git a/core/scene.go b/core/scene.go index 0cc01c09c0..5a5fb36a87 100644 --- a/core/scene.go +++ b/core/scene.go @@ -58,13 +58,13 @@ type Scene struct { //core:no-new // Size and position relative to overall rendering context. SceneGeom math32.Geom2DInt `edit:"-" set:"-"` - // painter for rendering + // painter for rendering all widgets in the scene. Painter paint.Painter `copier:"-" json:"-" xml:"-" display:"-" set:"-"` - // event manager for this scene + // event manager for this scene. Events Events `copier:"-" json:"-" xml:"-" set:"-"` - // current stage in which this Scene is set + // current stage in which this Scene is set. Stage *Stage `copier:"-" json:"-" xml:"-" set:"-"` // Animations are the currently active [Animation]s in this scene. diff --git a/core/slider.go b/core/slider.go index d2a5ee0c94..fac365afc0 100644 --- a/core/slider.go +++ b/core/slider.go @@ -158,6 +158,7 @@ func (sr *Slider) Init() { sr.Precision = 9 sr.ThumbSize.Set(1, 1) sr.TrackSize = 0.5 + sr.lastValue = -math32.MaxFloat32 sr.Styler(func(s *styles.Style) { s.SetAbilities(true, abilities.Activatable, abilities.Focusable, abilities.Hoverable, abilities.Slideable) @@ -211,6 +212,7 @@ func (sr *Slider) Init() { sr.On(events.SlideStart, func(e events.Event) { pos := sr.pointToRelPos(e.Pos()) sr.setSliderPosEvent(pos) + sr.lastValue = -math32.MaxFloat32 sr.slideStartPos = sr.pos }) sr.On(events.SlideMove, func(e events.Event) { diff --git a/core/splits.go b/core/splits.go index 68760d4d28..74a4c71862 100644 --- a/core/splits.go +++ b/core/splits.go @@ -187,6 +187,7 @@ func (sl *Splits) Init() { tree.AddAt(p, "handle-"+strconv.Itoa(hidx), func(w *Handle) { w.OnChange(func(e events.Event) { sl.setHandlePos(w.IndexInParent(), w.Value()) + sl.SendChange() }) w.Styler(func(s *styles.Style) { ix := w.IndexInParent() diff --git a/core/sprite.go b/core/sprite.go index ce59b43a00..07b9a5695d 100644 --- a/core/sprite.go +++ b/core/sprite.go @@ -8,18 +8,26 @@ import ( "image" "sync" - "cogentcore.org/core/base/ordmap" + "cogentcore.org/core/base/errors" + "cogentcore.org/core/base/keylist" + "cogentcore.org/core/base/tiered" "cogentcore.org/core/events" - "cogentcore.org/core/math32" + "cogentcore.org/core/paint" "golang.org/x/image/draw" ) -// A Sprite is just an image (with optional background) that can be drawn onto -// the OverTex overlay texture of a window. Sprites are used for text cursors/carets -// and for dynamic editing / interactive GUI elements (e.g., drag-n-drop elements) +// A Sprite is a top-level rendering element that paints onto a transparent +// layer that is cleared every render pass. Sprites are used for text cursors/carets +// and for dynamic editing / interactive GUI elements (e.g., drag-n-drop elements). +// To support cursor sprites and other animations, the sprites are redrawn at a +// minimum update rate that is at least as fast as CursorBlinkTime. +// Sprites can also receive mouse events, within their event bounding box. +// It is basically like a [Canvas] element over the entire screen, with no constraints +// on where you can draw. type Sprite struct { // Active is whether this sprite is Active now or not. + // Active sprites Draw and can receive events. Active bool // Name is the unique name of the sprite. @@ -28,11 +36,13 @@ type Sprite struct { // properties for sprite, which allow for user-extensible data Properties map[string]any - // position and size of the image within the RenderWindow - Geom math32.Geom2DInt + // Draw is the function that is called for Active sprites on every render pass + // to draw the sprite onto the top-level transparent layer. + Draw func(pc *paint.Painter) - // pixels to render, which should be the same size as [Sprite.Geom.Size] - Pixels *image.RGBA + // EventBBox is the bounding box for this sprite to receive mouse events. + // Typically this is the region in which it renders. + EventBBox image.Rectangle // listeners are event listener functions for processing events on this widget. // They are called in sequential descending order (so the last added listener @@ -44,54 +54,23 @@ type Sprite struct { // NewSprite returns a new [Sprite] with the given name, which must remain // invariant and unique among all sprites in use, and is used for all access; // prefix with package and type name to ensure uniqueness. Starts out in -// inactive state; must call ActivateSprite. If size is 0, no image is made. -func NewSprite(name string, sz image.Point, pos image.Point) *Sprite { - sp := &Sprite{Name: name} - sp.SetSize(sz) - sp.Geom.Pos = pos +// inactive state; must call ActivateSprite. +func NewSprite(name string, draw func(pc *paint.Painter)) *Sprite { + sp := &Sprite{Name: name, Draw: draw} return sp } -// SetSize sets sprite image to given size; makes a new image (does not resize) -// returns true if a new image was set -func (sp *Sprite) SetSize(nwsz image.Point) bool { - if nwsz.X == 0 || nwsz.Y == 0 { - return false - } - sp.Geom.Size = nwsz // always make sure - if sp.Pixels != nil && sp.Pixels.Bounds().Size() == nwsz { - return false +// InitProperties ensures that the Properties map exists. +func (sp *Sprite) InitProperties() { + if sp.Properties != nil { + return } - sp.Pixels = image.NewRGBA(image.Rectangle{Max: nwsz}) - return true + sp.Properties = make(map[string]any) } -// grabRenderFrom grabs the rendered image from the given widget. -func (sp *Sprite) grabRenderFrom(w Widget) { - img := grabRenderFrom(w) - if img != nil { - sp.Pixels = img - sp.Geom.Size = sp.Pixels.Bounds().Size() - } else { - sp.SetSize(image.Pt(10, 10)) // just a blank placeholder - } -} - -// grabRenderFrom grabs the rendered image from the given widget. -// 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() // todo: need to make this real on JS - if scimg == nil { - return nil - } - if wb.Geom.TotalBBox.Empty() { // the widget is offscreen - return nil - } - sz := wb.Geom.TotalBBox.Size() - img := image.NewRGBA(image.Rectangle{Max: sz}) - draw.Draw(img, img.Bounds(), scimg, wb.Geom.TotalBBox.Min, draw.Src) - return img +// SetPos sets the position of the sprite EventBBox, keeping the same size. +func (sp *Sprite) SetPos(pos image.Point) { + sp.EventBBox = sp.EventBBox.Add(pos.Sub(sp.EventBBox.Min)) } // On adds the given event handler to the sprite's Listeners for the given event type. @@ -144,9 +123,33 @@ func (sp *Sprite) send(typ events.Types, original ...events.Event) { sp.handleEvent(e) } -// Sprites manages a collection of Sprites, with unique name ids. +// NewImageSprite returns a new Sprite that draws the given image +// in the given location, which is stored in the Min of the EventBBox. +// Move the EventBBox to move the render location. +// The image is stored as "image" in Properties. +func NewImageSprite(name string, pos image.Point, img image.Image) *Sprite { + sp := &Sprite{Name: name} + sp.InitProperties() + sp.Properties["image"] = img + sp.EventBBox = img.Bounds().Add(pos) + sp.Draw = func(pc *paint.Painter) { + pc.DrawImage(img, sp.EventBBox, image.Point{}, draw.Over) + } + return sp +} + +//////// Sprites + +type SpriteList = keylist.List[string, *Sprite] + +// Sprites manages a collection of Sprites, with unique name ids within each +// of three priority lists: Normal, First and Final. The convenience API adds to +// the Normal list, while First and Final are available for more advanced cases +// where rendering order needs to be controlled. First items are rendered first +// (so they can be overwritten) and processed last for event handling, +// and vice-versa for Final. type Sprites struct { - ordmap.Map[string, *Sprite] + tiered.Tiered[SpriteList] // set to true if sprites have been modified since last config modified bool @@ -154,73 +157,146 @@ type Sprites struct { sync.Mutex } -// Add adds sprite to list, and returns the image index and -// layer index within that for given sprite. If name already -// exists on list, then it is returned, with size allocation -// updated as needed. -func (ss *Sprites) Add(sp *Sprite) { +// SetModified sets the sprite modified flag, which will +// drive a render to reflect the updated sprite. +// This version locks the sprites: see also [Sprites.SetModifiedLocked]. +func (ss *Sprites) SetModified() { ss.Lock() - ss.Init() - ss.Map.Add(sp.Name, sp) ss.modified = true ss.Unlock() } -// Delete deletes sprite by name, returning indexes where it was located. -// All sprite images must be updated when this occurs, as indexes may have shifted. -func (ss *Sprites) Delete(sp *Sprite) { - ss.Lock() - ss.DeleteKey(sp.Name) +// SetModifiedLocked sets the sprite modified flag, which will +// drive a render to reflect the updated sprite. +// This version assumes Sprites are already locked, which is better for +// doing multiple coordinated updates at the same time. +func (ss *Sprites) SetModifiedLocked() { ss.modified = true +} + +// IsModified returns whether the sprites have been modified, under lock. +func (ss *Sprites) IsModified() bool { + ss.Lock() + defer ss.Unlock() + return ss.modified +} + +// Add adds sprite to the Normal list of sprites, updating if already there. +// This version locks the sprites: see also [Sprites.AddLocked]. +func (ss *Sprites) Add(sp *Sprite) { + ss.Lock() + ss.AddLocked(sp) ss.Unlock() } -// SpriteByName returns the sprite by name +// AddLocked adds sprite to the Normal list of sprites, updating if already there. +// This version assumes Sprites are already locked, which is better for +// doing multiple coordinated updates at the same time. +func (ss *Sprites) AddLocked(sp *Sprite) { + errors.Log(ss.Normal.Add(sp.Name, sp)) + ss.modified = true +} + +// Delete deletes given sprite by name, returning true if found and deleted. +// This version locks the sprites: see also [Sprites.DeleteLocked]. +func (ss *Sprites) Delete(name string) bool { + ss.Lock() + defer ss.Unlock() + return ss.DeleteLocked(name) +} + +// DeleteLocked deletes given sprite by name, returning true if found and deleted. +// This version assumes Sprites are already locked, which is better for +// doing multiple coordinated updates at the same time. +func (ss *Sprites) DeleteLocked(name string) bool { + got := false + ss.Do(func(sl *SpriteList) { + d := sl.DeleteByKey(name) + if d { + got = true + } + }) + if got { + ss.modified = true + } + return got +} + +// SpriteByName returns the sprite by name. +// This version locks the sprites: see also [Sprites.SpriteByNameLocked]. func (ss *Sprites) SpriteByName(name string) (*Sprite, bool) { ss.Lock() defer ss.Unlock() - return ss.ValueByKeyTry(name) + return ss.SpriteByNameLocked(name) +} + +// SpriteByNameLocked returns the sprite by name. +// This version assumes Sprites are already locked, which is better for +// doing multiple coordinated updates at the same time. +func (ss *Sprites) SpriteByNameLocked(name string) (sp *Sprite, ok bool) { + ss.Do(func(sl *SpriteList) { + if ok { + return + } + sp, ok = sl.AtTry(name) + if ok { + return + } + }) + return } -// reset removes all sprites +// reset removes all sprites. func (ss *Sprites) reset() { ss.Lock() - ss.Reset() + ss.Do(func(sl *SpriteList) { + sl.Reset() + }) ss.modified = true ss.Unlock() } -// ActivateSprite flags the sprite as active, setting Modified if wasn't before. -func (ss *Sprites) ActivateSprite(name string) { - sp, ok := ss.SpriteByName(name) - if !ok { - return // not worth bothering about errs -- use a consistent string var! - } +// ActivateSprite flags sprite(s) as active, setting Modified if wasn't before. +// This version locks the sprites: see also [Sprites.ActivateSpriteLocked]. +func (ss *Sprites) ActivateSprite(name ...string) { ss.Lock() - if !sp.Active { - sp.Active = true - ss.modified = true - } + ss.ActivateSpriteLocked(name...) ss.Unlock() } -// InactivateSprite flags the sprite as inactive, setting Modified if wasn't before. -func (ss *Sprites) InactivateSprite(name string) { - sp, ok := ss.SpriteByName(name) - if !ok { - return // not worth bothering about errs -- use a consistent string var! +// ActivateSpriteLocked flags the sprite(s) as active, +// setting Modified if wasn't before. +// This version assumes Sprites are already locked, which is better for +// doing multiple coordinated updates at the same time. +func (ss *Sprites) ActivateSpriteLocked(name ...string) { + for _, nm := range name { + sp, ok := ss.SpriteByNameLocked(nm) + if ok && !sp.Active { + sp.Active = true + ss.modified = true + } } +} + +// InactivateSprite flags the Normal sprite(s) as inactive, +// setting Modified if wasn't before. +// This version locks the sprites: see also [Sprites.InactivateSpriteLocked]. +func (ss *Sprites) InactivateSprite(name ...string) { ss.Lock() - if sp.Active { - sp.Active = false - ss.modified = true - } + ss.InactivateSpriteLocked(name...) ss.Unlock() } -// IsModified returns whether the sprites have been modified. -func (ss *Sprites) IsModified() bool { - ss.Lock() - defer ss.Unlock() - return ss.modified +// InactivateSpriteLocked flags the Normal sprite(s) as inactive, +// setting Modified if wasn't before. +// This version assumes Sprites are already locked, which is better for +// doing multiple coordinated updates at the same time. +func (ss *Sprites) InactivateSpriteLocked(name ...string) { + for _, nm := range name { + sp, ok := ss.SpriteByNameLocked(nm) + if ok && sp.Active { + sp.Active = false + ss.modified = true + } + } } diff --git a/core/stage.go b/core/stage.go index e186017d7b..22eff0bbdb 100644 --- a/core/stage.go +++ b/core/stage.go @@ -11,6 +11,8 @@ import ( "time" "cogentcore.org/core/base/option" + "cogentcore.org/core/paint" + "cogentcore.org/core/paint/render" "cogentcore.org/core/system" ) @@ -202,6 +204,12 @@ type Stage struct { //types:add -setters // Sprites are named images that are rendered last overlaying everything else. Sprites Sprites `json:"-" xml:"-" set:"-"` + + // spritePainter is the painter for sprite drawing. + spritePainter *paint.Painter + + // spriteRenderer is the renderer for sprite drawing. + spriteRenderer render.Renderer } func (st *Stage) String() string { @@ -374,6 +382,7 @@ func (st *Stage) delete() { if st.Type.isMain() && st.popups != nil { st.popups.deleteAll() st.Sprites.reset() + st.spriteRenderer = nil } if st.Scene != nil { st.Scene.DeleteChildren() diff --git a/core/style.go b/core/style.go index fed5987fd9..9324418223 100644 --- a/core/style.go +++ b/core/style.go @@ -98,8 +98,8 @@ func (wb *WidgetBase) resetStyleWidget() { // runStylers runs the [WidgetBase.Stylers]. func (wb *WidgetBase) runStylers() { - wb.Stylers.Do(func(s []func(s *styles.Style)) { - for _, f := range s { + wb.Stylers.Do(func(s *[]func(s *styles.Style)) { + for _, f := range *s { f(&wb.Styles) } }) @@ -217,6 +217,8 @@ func styleFromTags(w Widget, tags reflect.StructTag) { setFromTag(tags, "max-height", s.Max.Y.Em) setFromTag(tags, "grow", func(v float32) { s.Grow.X = v }) setFromTag(tags, "grow-y", func(v float32) { s.Grow.Y = v }) + setFromTag(tags, "icon-width", s.IconSize.X.Em) + setFromTag(tags, "icon-height", s.IconSize.Y.Em) }) if tags.Get("new-window") == "+" { w.AsWidget().setFlag(true, widgetValueNewWindow) diff --git a/core/svg.go b/core/svg.go index ba13cac736..76a857416e 100644 --- a/core/svg.go +++ b/core/svg.go @@ -77,9 +77,7 @@ func (sv *SVG) Init() { return } e.SetHandled() - del := e.PrevDelta() - sv.SVG.Translate.X += float32(del.X) - sv.SVG.Translate.Y += float32(del.Y) + sv.SVG.Translate.SetAdd(math32.FromPoint(e.PrevDelta())) sv.NeedsRender() }) sv.On(events.Scroll, func(e events.Event) { @@ -88,10 +86,7 @@ func (sv *SVG) Init() { } e.SetHandled() se := e.(*events.MouseScroll) - sv.SVG.Scale += float32(se.Delta.Y) / 100 - if sv.SVG.Scale <= 0.0000001 { - sv.SVG.Scale = 0.01 - } + sv.SVG.ZoomAtScroll(se.Delta.Y, se.Pos()) sv.NeedsRender() }) } diff --git a/core/switch.go b/core/switch.go index 3559d9838e..22c6d1c432 100644 --- a/core/switch.go +++ b/core/switch.go @@ -98,16 +98,17 @@ func (sw *Switch) Init() { s.Border.Radius = styles.BorderRadiusSmall s.Gap.Zero() s.CenterAll() + s.IconSize.Set(units.Em(1.5)) - if sw.Type == SwitchChip { + switch sw.Type { + case SwitchChip: if s.Is(states.Checked) { s.Background = colors.Scheme.SurfaceVariant s.Color = colors.Scheme.OnSurfaceVariant } else if !s.Is(states.Focused) { s.Border.Width.Set(units.Dp(1)) } - } - if sw.Type == SwitchSegmentedButton { + case SwitchSegmentedButton: if !s.Is(states.Focused) { s.Border.Width.Set(units.Dp(1)) } @@ -115,6 +116,8 @@ func (sw *Switch) Init() { s.Background = colors.Scheme.SurfaceVariant s.Color = colors.Scheme.OnSurfaceVariant } + case SwitchSwitch: + s.IconSize.Set(units.Em(2), units.Em(1.5)) } if s.Is(states.Selected) { @@ -162,11 +165,6 @@ func (sw *Switch) Init() { s.Color = colors.Scheme.Primary.Base } // switches need to be bigger - if sw.Type == SwitchSwitch { - s.Min.Set(units.Em(2), units.Em(1.5)) - } else { - s.Min.Set(units.Em(1.5)) - } }) w.Updater(func() { w.SetIcon(sw.IconOn) @@ -176,12 +174,8 @@ func (sw *Switch) Init() { iconStyle := func(s *styles.Style) { switch { case sw.Type == SwitchSwitch: - // switches need to be bigger - s.Min.Set(units.Em(2), units.Em(1.5)) case sw.IconOff == icons.None && sw.IconIndeterminate == icons.None: - s.Min.Zero() // nothing to render - default: - s.Min.Set(units.Em(1.5)) + s.IconSize.Zero() // nothing to render } } tree.AddAt(p, "icon-off", func(w *Icon) { diff --git a/core/table.go b/core/table.go index 2fc4363483..e95ff0db7c 100644 --- a/core/table.go +++ b/core/table.go @@ -305,7 +305,7 @@ func (tb *Table) MakeRow(p *tree.Plan, i int) { valnm := fmt.Sprintf("value-%d-%s-%s", fli, itxt, reflectx.ShortTypeName(field.Type)) tags := field.Tag if uv.Kind() == reflect.Slice || uv.Kind() == reflect.Map { - ni := reflect.StructTag(`display:"no-inline"`) + ni := reflect.StructTag(`display:"no-inline" new-window:"+"`) if tags == "" { tags += " " + ni } else { diff --git a/core/textfield.go b/core/textfield.go index ebdb302c9f..ac742e3ee2 100644 --- a/core/textfield.go +++ b/core/textfield.go @@ -22,18 +22,17 @@ import ( "cogentcore.org/core/icons" "cogentcore.org/core/keymap" "cogentcore.org/core/math32" + "cogentcore.org/core/paint" "cogentcore.org/core/styles" "cogentcore.org/core/styles/abilities" "cogentcore.org/core/styles/states" "cogentcore.org/core/styles/units" - "cogentcore.org/core/system" "cogentcore.org/core/text/parse/complete" "cogentcore.org/core/text/rich" "cogentcore.org/core/text/shaped" "cogentcore.org/core/text/text" "cogentcore.org/core/text/textpos" "cogentcore.org/core/tree" - "golang.org/x/image/draw" ) // TextField is a widget for editing a line of text. @@ -160,9 +159,6 @@ type TextField struct { //core:embedder // lineHeight is the line height cached during styling. lineHeight float32 - // blinkOn oscillates between on and off for blinking. - blinkOn bool - // cursorMu is the mutex for updating the cursor between blinker and field. cursorMu sync.Mutex @@ -532,7 +528,7 @@ func (tf *TextField) editDone() { } } tf.clearSelected() - tf.clearCursor() + tf.stopCursor() } // revert aborts editing and reverts to the last saved text. @@ -595,6 +591,9 @@ func (tf *TextField) WidgetTooltip(pos image.Point) (string, image.Point) { //////// Cursor Navigation func (tf *TextField) updateLinePos() { + if tf.renderAll == nil { + return + } tf.cursorLine = tf.renderAll.RuneToLinePos(tf.cursorPos).Line } @@ -1097,7 +1096,7 @@ func (tf *TextField) offerComplete() { return } s := string(tf.editText[0:tf.cursorPos]) - cpos := tf.charRenderPos(tf.cursorPos, true).ToPoint() + cpos := tf.charRenderPos(tf.cursorPos).ToPoint() cpos.X += 5 cpos.Y = tf.Geom.TotalBBox.Max.Y tf.complete.SrcLn = 0 @@ -1160,47 +1159,19 @@ func (tf *TextField) relCharPos(st, ed int) math32.Vector2 { // charRenderPos returns the starting render coords for the given character // position in string -- makes no attempt to rationalize that pos (i.e., if // not in visible range, position will be out of range too). -// if wincoords is true, then adds window box offset -- for cursor, popups -func (tf *TextField) charRenderPos(charidx int, wincoords bool) math32.Vector2 { +func (tf *TextField) charRenderPos(charidx int) math32.Vector2 { pos := tf.effPos - if wincoords { - sc := tf.Scene - pos = pos.Add(math32.FromPoint(sc.SceneGeom.Pos)) - } + sc := tf.Scene + pos = pos.Add(math32.FromPoint(sc.SceneGeom.Pos)) cpos := tf.relCharPos(tf.dispRange.Start, charidx) return pos.Add(cpos) } var ( - // textFieldBlinker manages cursor blinking - textFieldBlinker = Blinker{} - // textFieldSpriteName is the name of the window sprite used for the cursor textFieldSpriteName = "TextField.Cursor" ) -func init() { - TheApp.AddQuitCleanFunc(textFieldBlinker.QuitClean) - textFieldBlinker.Func = func() { - w := textFieldBlinker.Widget - textFieldBlinker.Unlock() // comes in locked - if w == nil { - return - } - tf := AsTextField(w) - if !tf.StateIs(states.Focused) || !tf.IsVisible() { - tf.blinkOn = false - tf.renderCursor(false) - } else { - // Need consistent test results on offscreen. - if TheApp.Platform() != system.Offscreen { - tf.blinkOn = !tf.blinkOn - } - tf.renderCursor(tf.blinkOn) - } - } -} - // startCursor starts the cursor blinking and renders it func (tf *TextField) startCursor() { if tf == nil || tf.This == nil { @@ -1209,95 +1180,81 @@ func (tf *TextField) startCursor() { if !tf.IsVisible() { return } - tf.blinkOn = true - tf.renderCursor(true) - if SystemSettings.CursorBlinkTime == 0 { - return - } - textFieldBlinker.SetWidget(tf.This.(Widget)) - textFieldBlinker.Blink(SystemSettings.CursorBlinkTime) -} - -// clearCursor turns off cursor and stops it from blinking -func (tf *TextField) clearCursor() { - if tf.IsReadOnly() { - return - } - tf.stopCursor() - tf.renderCursor(false) + tf.toggleCursor(true) } // stopCursor stops the cursor from blinking func (tf *TextField) stopCursor() { - if tf == nil || tf.This == nil { - return - } - textFieldBlinker.ResetWidget(tf.This.(Widget)) + tf.toggleCursor(false) } -// renderCursor renders the cursor on or off, as a sprite that is either on or off -func (tf *TextField) renderCursor(on bool) { - if tf == nil || tf.This == nil { - return - } - if !on { - if tf.Scene == nil || tf.Scene.Stage == nil { - return - } - ms := tf.Scene.Stage.Main - if ms == nil { - return - } - spnm := fmt.Sprintf("%v-%v", textFieldSpriteName, tf.lineHeight) - ms.Sprites.InactivateSprite(spnm) +// toggleSprite turns on or off the cursor sprite. +func (tf *TextField) toggleCursor(on bool) { + sc := tf.Scene + if sc == nil || sc.Stage == nil { return } - if !tf.IsVisible() { - return + ms := sc.Stage.Main + if ms == nil { + return // only MainStage has sprites } + spnm := textFieldSpriteName + ms.Sprites.Lock() + defer ms.Sprites.Unlock() - tf.cursorMu.Lock() - defer tf.cursorMu.Unlock() + sp, ok := ms.Sprites.SpriteByNameLocked(spnm) - sp := tf.cursorSprite(on) - if sp == nil { - return + activate := func() { + sp.EventBBox.Min = tf.charRenderPos(tf.cursorPos).ToPointFloor() + sp.Active = true + sp.Properties["turnOn"] = true + sp.Properties["on"] = true + sp.Properties["lastSwitch"] = time.Now() } - sp.Geom.Pos = tf.charRenderPos(tf.cursorPos, true).ToPointFloor() -} -// cursorSprite returns the Sprite for the cursor (which is -// only rendered once with a vertical bar, and just activated and inactivated -// depending on render status). On sets the On status of the cursor. -func (tf *TextField) cursorSprite(on bool) *Sprite { - sc := tf.Scene - if sc == nil { - return nil + if ok { + if on { + activate() + } else { + sp.Active = false + } + return } - ms := sc.Stage.Main - if ms == nil { - return nil // only MainStage has sprites + if !on { + return } - spnm := fmt.Sprintf("%v-%v", textFieldSpriteName, tf.lineHeight) - sp, ok := ms.Sprites.SpriteByName(spnm) - // TODO: figure out how to update caret color on color scheme change - if !ok { - bbsz := image.Point{int(math32.Ceil(tf.CursorWidth.Dots)), int(math32.Ceil(tf.lineHeight))} + sp = NewSprite(spnm, func(pc *paint.Painter) { + if !sp.Active { + return + } + turnOn := sp.Properties["turnOn"].(bool) // force on + if !turnOn { + isOn := sp.Properties["on"].(bool) + lastSwitch := sp.Properties["lastSwitch"].(time.Time) + if SystemSettings.CursorBlinkTime > 0 && time.Since(lastSwitch) > SystemSettings.CursorBlinkTime { + isOn = !isOn + sp.Properties["on"] = isOn + sp.Properties["lastSwitch"] = time.Now() + } + if !isOn { + return + } + } + bbsz := math32.Vec2(math32.Ceil(tf.CursorWidth.Dots), math32.Ceil(tf.lineHeight)) if bbsz.X < 2 { // at least 2 bbsz.X = 2 } - sp = NewSprite(spnm, bbsz, image.Point{}) - sp.Active = on - ibox := sp.Pixels.Bounds() - draw.Draw(sp.Pixels, ibox, tf.CursorColor, image.Point{}, draw.Src) - ms.Sprites.Add(sp) - } - if on { - ms.Sprites.ActivateSprite(sp.Name) - } else { - ms.Sprites.InactivateSprite(sp.Name) - } - return sp + sp.Properties["turnOn"] = false + pc.Fill.Color = nil + pc.Stroke.Color = tf.CursorColor + pc.Stroke.Width.Dot(bbsz.X) + pos := math32.FromPoint(sp.EventBBox.Min) + pc.Line(pos.X, pos.Y, pos.X, pos.Y+bbsz.Y) + pc.Draw() + }) + sp.InitProperties() + activate() + ms.Sprites.AddLocked(sp) } // renderSelect renders the selected region, if any, underneath the text diff --git a/core/tree.go b/core/tree.go index 35c5b8f85e..9828d8c82c 100644 --- a/core/tree.go +++ b/core/tree.go @@ -57,6 +57,7 @@ type Treer interface { //types:add Cut() Copy() Paste() + DeleteSelected() DragDrop(e events.Event) DropDeleteSource(e events.Event) } @@ -191,7 +192,7 @@ func (tr *Tree) rootSetViewIndex() int { if tvn != nil { tvn.viewIndex = idx if tvn.Root == nil { - tvn.Root = tr + tvn.Root = tr.This.(Treer) } idx++ } @@ -221,6 +222,7 @@ func (tr *Tree) Init() { s.Padding.SetVertical(units.Dp(4)) s.Padding.Right.Zero() s.Text.Align = text.Start + s.IconSize.Set(units.Em(1.5)) // need to copy over to actual and then clear styles one if s.Is(states.Selected) { @@ -312,7 +314,7 @@ func (tr *Tree) Init() { if !tr.rootIsReadOnly() && !e.IsHandled() { switch kf { case keymap.Delete: - tr.DeleteNode() + tr.DeleteSelected() e.SetHandled() case keymap.Duplicate: tr.Duplicate() @@ -426,6 +428,7 @@ func (tr *Tree) Init() { s.Cursor = cursors.None s.Color = colors.Scheme.Primary.Base s.Padding.Zero() + s.IconSize = tr.Styles.IconSize s.Align.Self = styles.Center if !w.StateIs(states.Indeterminate) { // we amplify any state layer we receiver so that it is clear @@ -459,7 +462,7 @@ func (tr *Tree) Init() { if tr.Icon.IsSet() { tree.AddAt(p, "icon", func(w *Icon) { w.Styler(func(s *styles.Style) { - s.Font.Size.Dp(24) + s.IconSize = tr.Styles.IconSize s.Color = colors.Scheme.Primary.Base s.Align.Self = styles.Center }) @@ -480,6 +483,12 @@ func (tr *Tree) Init() { w.SetText(tr.Label()) }) }) + // note: this causes excessive updates and is not recommended. use Resync() instead. + // tr.Updater(func() { + // if tr.SyncNode != nil { + // tr.syncToSrc(&tr.viewIndex, false, 0) + // } + // }) } func (tr *Tree) OnAdd() { @@ -492,7 +501,7 @@ func (tr *Tree) OnAdd() { tr.IconLeaf = ptv.IconLeaf } else { if tr.Root == nil { - tr.Root = tr + tr.Root = tr.This.(Treer) } } troot := tr.Root.AsCoreTree() @@ -1231,12 +1240,17 @@ func (tr *Tree) ContextMenuPos(e events.Event) (pos image.Point) { func (tr *Tree) contextMenuReadOnly(m *Scene) { tri := tr.This.(Treer) - NewFuncButton(m).SetFunc(tri.Copy).SetKey(keymap.Copy).SetEnabled(tr.HasSelection()) - NewFuncButton(m).SetFunc(tr.editNode).SetText("View").SetIcon(icons.Visibility).SetEnabled(tr.HasSelection()) + + NewFuncButton(m).SetFunc(tri.Copy).SetKey(keymap.Copy). + SetEnabled(tr.HasSelection()) + NewFuncButton(m).SetFunc(tr.EditNode).SetText("View"). + SetIcon(icons.Visibility).SetEnabled(tr.HasSelection()) NewSeparator(m) - NewFuncButton(m).SetFunc(tr.OpenAll).SetIcon(icons.KeyboardArrowDown).SetEnabled(tr.HasSelection()) - NewFuncButton(m).SetFunc(tr.CloseAll).SetIcon(icons.KeyboardArrowRight).SetEnabled(tr.HasSelection()) + NewFuncButton(m).SetFunc(tr.OpenAll).SetIcon(icons.KeyboardArrowDown). + SetEnabled(tr.HasSelection()) + NewFuncButton(m).SetFunc(tr.CloseAll).SetIcon(icons.KeyboardArrowRight). + SetEnabled(tr.HasSelection()) } func (tr *Tree) contextMenu(m *Scene) { @@ -1246,26 +1260,35 @@ func (tr *Tree) contextMenu(m *Scene) { } tri := tr.This.(Treer) NewFuncButton(m).SetFunc(tr.AddChildNode).SetText("Add child").SetIcon(icons.Add).SetEnabled(tr.HasSelection()) - NewFuncButton(m).SetFunc(tr.InsertBefore).SetIcon(icons.Add).SetEnabled(tr.HasSelection()) - NewFuncButton(m).SetFunc(tr.InsertAfter).SetIcon(icons.Add).SetEnabled(tr.HasSelection()) - NewFuncButton(m).SetFunc(tr.Duplicate).SetIcon(icons.ContentCopy).SetEnabled(tr.HasSelection()) - NewFuncButton(m).SetFunc(tr.DeleteNode).SetText("Delete").SetIcon(icons.Delete). + NewFuncButton(m).SetFunc(tr.InsertBefore).SetIcon(icons.Add). + SetEnabled(tr.HasSelection()) + NewFuncButton(m).SetFunc(tr.InsertAfter).SetIcon(icons.Add). + SetEnabled(tr.HasSelection()) + NewFuncButton(m).SetFunc(tr.Duplicate).SetIcon(icons.ContentCopy). + SetEnabled(tr.HasSelection()) + NewFuncButton(m).SetFunc(tr.DeleteSelected).SetText("Delete").SetIcon(icons.Delete). SetEnabled(tr.HasSelection()) NewSeparator(m) - NewFuncButton(m).SetFunc(tri.Copy).SetIcon(icons.Copy).SetKey(keymap.Copy).SetEnabled(tr.HasSelection()) - NewFuncButton(m).SetFunc(tri.Cut).SetIcon(icons.Cut).SetKey(keymap.Cut).SetEnabled(tr.HasSelection()) + NewFuncButton(m).SetFunc(tri.Copy).SetIcon(icons.Copy).SetKey(keymap.Copy). + SetEnabled(tr.HasSelection()) + NewFuncButton(m).SetFunc(tri.Cut).SetIcon(icons.Cut).SetKey(keymap.Cut). + SetEnabled(tr.HasSelection()) paste := NewFuncButton(m).SetFunc(tri.Paste).SetIcon(icons.Paste).SetKey(keymap.Paste) cb := tr.Scene.Events.Clipboard() if cb != nil { paste.SetState(cb.IsEmpty(), states.Disabled) } NewSeparator(m) - NewFuncButton(m).SetFunc(tr.editNode).SetText("Edit").SetIcon(icons.Edit).SetEnabled(tr.HasSelection()) - NewFuncButton(m).SetFunc(tr.inspectNode).SetText("Inspect").SetIcon(icons.EditDocument).SetEnabled(tr.HasSelection()) + NewFuncButton(m).SetFunc(tr.EditNode).SetText("Edit").SetIcon(icons.Edit). + SetEnabled(tr.HasSelection()) + NewFuncButton(m).SetFunc(tr.inspectNode).SetText("Inspect").SetIcon(icons.EditDocument). + SetEnabled(tr.HasSelection()) NewSeparator(m) - NewFuncButton(m).SetFunc(tr.OpenAll).SetIcon(icons.KeyboardArrowDown).SetEnabled(tr.HasSelection()) - NewFuncButton(m).SetFunc(tr.CloseAll).SetIcon(icons.KeyboardArrowRight).SetEnabled(tr.HasSelection()) + NewFuncButton(m).SetFunc(tr.OpenAll).SetIcon(icons.KeyboardArrowDown). + SetEnabled(tr.HasSelection()) + NewFuncButton(m).SetFunc(tr.CloseAll).SetIcon(icons.KeyboardArrowRight). + SetEnabled(tr.HasSelection()) } // IsRoot returns true if given node is the root of the tree, @@ -1282,6 +1305,26 @@ func (tr *Tree) IsRoot(action ...string) bool { //////// Copy / Cut / Paste +// DeleteSelected deletes selected items. +// Must be called from first node in selection. +func (tr *Tree) DeleteSelected() { //types:add + if tr.IsRoot("Delete") { + return + } + if tr.SyncNode != nil { + tr.deleteSync() + return + } + sels := tr.GetSelectedNodes() + rn := tr.Root.AsCoreTree() + tr.UnselectAll() + for _, sn := range sels { + sn.AsTree().Delete() + } + rn.Update() + rn.sendChangeEvent() +} + // MimeData adds mimedata for this node: a text/plain of the Path. func (tr *Tree) MimeData(md *mimedata.Mimes) { if tr.SyncNode != nil { @@ -1319,7 +1362,8 @@ func (tr *Tree) nodesFromMimeData(md mimedata.Mimes) ([]tree.Node, []string) { return sl, pl } -// Copy copies the tree to the clipboard. +// Copy copies the selected items to the clipboard. +// This must be called on the first item in the selected list. func (tr *Tree) Copy() { //types:add sels := tr.GetSelectedNodes() nitms := max(1, len(sels)) @@ -1336,6 +1380,7 @@ func (tr *Tree) Copy() { //types:add } // Cut copies to [system.Clipboard] and deletes selected items. +// This must be called on the first item in the selected list. func (tr *Tree) Cut() { //types:add if tr.IsRoot("Cut") { return @@ -1379,26 +1424,26 @@ func (tr *Tree) pasteMenu(md mimedata.Mimes) { // to take after each optional action. func (tr *Tree) makePasteMenu(m *Scene, md mimedata.Mimes, fun func()) { NewButton(m).SetText("Assign To").OnClick(func(e events.Event) { - tr.pasteAssign(md) + tr.PasteAssign(md) if fun != nil { fun() } }) NewButton(m).SetText("Add to Children").OnClick(func(e events.Event) { - tr.pasteChildren(md, events.DropCopy) + tr.PasteChildren(md, events.DropCopy) if fun != nil { fun() } }) if !tr.IsRoot() { NewButton(m).SetText("Insert Before").OnClick(func(e events.Event) { - tr.pasteBefore(md, events.DropCopy) + tr.PasteBefore(md, events.DropCopy) if fun != nil { fun() } }) NewButton(m).SetText("Insert After").OnClick(func(e events.Event) { - tr.pasteAfter(md, events.DropCopy) + tr.PasteAfter(md, events.DropCopy) if fun != nil { fun() } @@ -1407,8 +1452,8 @@ func (tr *Tree) makePasteMenu(m *Scene, md mimedata.Mimes, fun func()) { NewButton(m).SetText("Cancel") } -// pasteAssign assigns mime data (only the first one!) to this node -func (tr *Tree) pasteAssign(md mimedata.Mimes) { +// PasteAssign assigns mime data (only the first one!) to this node +func (tr *Tree) PasteAssign(md mimedata.Mimes) { if tr.SyncNode != nil { tr.pasteAssignSync(md) return @@ -1424,17 +1469,17 @@ func (tr *Tree) pasteAssign(md mimedata.Mimes) { tr.sendChangeEvent() } -// pasteBefore inserts object(s) from mime data before this node. +// PasteBefore inserts object(s) from mime data before this node. // If another item with the same name already exists, it will // append _Copy on the name of the inserted objects -func (tr *Tree) pasteBefore(md mimedata.Mimes, mod events.DropMods) { +func (tr *Tree) PasteBefore(md mimedata.Mimes, mod events.DropMods) { tr.pasteAt(md, mod, 0, "Paste before") } -// pasteAfter inserts object(s) from mime data after this node. +// PasteAfter inserts object(s) from mime data after this node. // If another item with the same name already exists, it will // append _Copy on the name of the inserted objects -func (tr *Tree) pasteAfter(md mimedata.Mimes, mod events.DropMods) { +func (tr *Tree) PasteAfter(md mimedata.Mimes, mod events.DropMods) { tr.pasteAt(md, mod, 1, "Paste after") } @@ -1470,24 +1515,20 @@ func (tr *Tree) pasteAt(md mimedata.Mimes, mod events.DropMods, rel int, actNm s sz := len(sl) var selTv *Tree for i, ns := range sl { + nst := ns.AsTree() orgpath := pl[i] - if mod != events.DropMove { - if cn := parent.ChildByName(ns.AsTree().Name, 0); cn != nil { - ns.AsTree().SetName(ns.AsTree().Name + "_Copy") - } - } + tree.SetUniqueNameIfDuplicate(parent, ns) parent.InsertChild(ns, myidx+i) nwb := AsWidget(ns) - ntv := AsTree(ns) - ntv.Root = tr.Root + AsTree(ns).Root = tr.Root nwb.setScene(tr.Scene) nwb.Update() // incl children - npath := ns.AsTree().PathFrom(tr.Root) + npath := nst.PathFrom(tr.Root) if mod == events.DropMove && npath == orgpath { // we will be nuked immediately after drag - ns.AsTree().SetName(ns.AsTree().Name + treeTempMovedTag) // special keyword :) + nst.SetName(nst.Name + treeTempMovedTag) // special keyword :) } if i == sz-1 { - selTv = ntv + selTv = AsTree(ns) } } tr.sendChangeEvent() @@ -1497,9 +1538,9 @@ func (tr *Tree) pasteAt(md mimedata.Mimes, mod events.DropMods, rel int, actNm s } } -// pasteChildren inserts object(s) from mime data -// at end of children of this node -func (tr *Tree) pasteChildren(md mimedata.Mimes, mod events.DropMods) { +// PasteChildren inserts object(s) from mime data +// at end of children of this node. +func (tr *Tree) PasteChildren(md mimedata.Mimes, mod events.DropMods) { if tr.SyncNode != nil { tr.pasteChildrenSync(md, mod) return @@ -1507,11 +1548,10 @@ func (tr *Tree) pasteChildren(md mimedata.Mimes, mod events.DropMods) { sl, _ := tr.nodesFromMimeData(md) for _, ns := range sl { + tree.SetUniqueNameIfDuplicate(tr.This, ns) tr.AddChild(ns) - nwb := AsWidget(ns) - ntv := AsTree(ns) - ntv.Root = tr.Root - nwb.setScene(tr.Scene) + AsTree(ns).Root = tr.Root + AsWidget(ns).setScene(tr.Scene) } tr.Update() tr.Open() diff --git a/core/treesync.go b/core/treesync.go index 6eebb22df1..d4d9f09568 100644 --- a/core/treesync.go +++ b/core/treesync.go @@ -274,8 +274,7 @@ func (tr *Tree) AddChildNode() { //types:add // to this view node in the sync tree. // If SyncNode is set, operates on Sync Tree. func (tr *Tree) DeleteNode() { //types:add - ttl := "Delete" - if tr.IsRoot(ttl) { + if tr.IsRoot("Delete") { return } tr.Close() @@ -293,6 +292,16 @@ func (tr *Tree) DeleteNode() { //types:add } } +// deleteSync deletes selected items. +func (tr *Tree) deleteSync() { + sels := tr.selectedSyncNodes() + tr.UnselectAll() + for _, sn := range sels { + sn.AsTree().Delete() + } + tr.sendChangeEventReSync(nil) +} + // Duplicate duplicates the sync node corresponding to this view node in // the tree, and inserts the duplicate after this node (as a new sibling). // If SyncNode is set, operates on Sync Tree. @@ -317,6 +326,7 @@ func (tr *Tree) Duplicate() { //types:add tr.Unselect() nwkid := tr.Clone() nwkid.AsTree().SetName(nm) + tree.SetUniqueNameIfDuplicate(parent, nwkid) ntv := AsTree(nwkid) parent.InsertChild(nwkid, myidx+1) ntv.Update() @@ -340,6 +350,7 @@ func (tr *Tree) duplicateSync() { nm := fmt.Sprintf("%v_Copy", sn.AsTree().Name) nwkid := sn.AsTree().Clone() nwkid.AsTree().SetName(nm) + tree.SetUniqueNameIfDuplicate(parent, nwkid) parent.AsTree().InsertChild(nwkid, myidx+1) tvparent.sendChangeEventReSync(nil) if tvk := tvparent.ChildByName("tv_"+nm, 0); tvk != nil { @@ -348,9 +359,9 @@ func (tr *Tree) duplicateSync() { } } -// editNode pulls up a [Form] dialog for the node. +// EditNode pulls up a [Form] dialog for the node. // If SyncNode is set, operates on Sync Tree. -func (tr *Tree) editNode() { //types:add +func (tr *Tree) EditNode() { //types:add if tr.SyncNode != nil { tynm := tr.SyncNode.AsTree().NodeType().Name d := NewBody(tynm) @@ -436,19 +447,17 @@ func (tr *Tree) pasteAtSync(md mimedata.Mimes, mod events.DropMods, rel int, act } myidx += rel sroot := tr.Root.AsCoreTree().SyncNode + pt := parent.AsTree() sz := len(sl) var seln tree.Node for i, ns := range sl { + nst := ns.AsTree() orgpath := pl[i] - if mod != events.DropMove { - if cn := parent.AsTree().ChildByName(ns.AsTree().Name, 0); cn != nil { - ns.AsTree().SetName(ns.AsTree().Name + "_Copy") - } - } - parent.AsTree().InsertChild(ns, myidx+i) - npath := ns.AsTree().PathFrom(sroot) + tree.SetUniqueNameIfDuplicate(parent, ns) + pt.InsertChild(ns, myidx+i) + npath := nst.PathFrom(sroot) if mod == events.DropMove && npath == orgpath { // we will be nuked immediately after drag - ns.AsTree().SetName(ns.AsTree().Name + treeTempMovedTag) // special keyword :) + nst.SetName(nst.Name + treeTempMovedTag) // special keyword :) } if i == sz-1 { seln = ns @@ -467,9 +476,10 @@ func (tr *Tree) pasteAtSync(md mimedata.Mimes, mod events.DropMods, rel int, act // end of children of this node func (tr *Tree) pasteChildrenSync(md mimedata.Mimes, mod events.DropMods) { sl, _ := tr.nodesFromMimeData(md) - sk := tr.SyncNode + spar := tr.SyncNode for _, ns := range sl { - sk.AsTree().AddChild(ns) + tree.SetUniqueNameIfDuplicate(spar, ns) + spar.AsTree().AddChild(ns) } tr.sendChangeEventReSync(nil) } diff --git a/core/typegen.go b/core/typegen.go index 445e63aee9..06ca9383b7 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"}, {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: "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]. @@ -800,7 +800,7 @@ func (t *Splits) SetTileSplits(v ...float32) *Splits { t.TileSplits = v; return // elements, there are 2 subsets of sub-splits, with 4 total subsplits. func (t *Splits) SetSubSplits(v ...[]float32) *Splits { t.SubSplits = v; return t } -var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Stage", IDName: "stage", Doc: "Stage is a container and manager for displaying a [Scene]\nin different functional ways, defined by [StageTypes].", Directives: []types.Directive{{Tool: "types", Directive: "add", Args: []string{"-setters"}}}, Fields: []types.Field{{Name: "Type", Doc: "Type is the type of [Stage], which determines behavior and styling."}, {Name: "Scene", Doc: "Scene contents of this [Stage] (what it displays)."}, {Name: "Context", Doc: "Context is a widget in another scene that requested this stage to be created\nand provides context."}, {Name: "Name", Doc: "Name is the name of the Stage, which is generally auto-set\nbased on the [Scene.Name]."}, {Name: "Title", Doc: "Title is the title of the Stage, which is generally auto-set\nbased on the [Body.Title]. It used for the title of [WindowStage]\nand [DialogStage] types, and for a [Text] title widget if\n[Stage.DisplayTitle] is true."}, {Name: "Screen", Doc: "Screen specifies the screen number on which a new window is opened\nby default on desktop platforms. It defaults to -1, which indicates\nthat the first window should open on screen 0 (the default primary\nscreen) and any subsequent windows should open on the same screen as\nthe currently active window. Regardless, the automatically saved last\nscreen of a window with the same [Stage.Title] takes precedence if it exists;\nsee the website documentation on window geometry saving for more information.\nUse [TheApp].ScreenByName(\"name\").ScreenNumber to get the screen by name."}, {Name: "Modal", Doc: "Modal, if true, blocks input to all other stages."}, {Name: "Scrim", Doc: "Scrim, if true, places a darkening scrim over other stages."}, {Name: "ClickOff", Doc: "ClickOff, if true, dismisses the [Stage] if the user clicks anywhere\noff of the [Stage]."}, {Name: "ignoreEvents", Doc: "ignoreEvents is whether to send no events to the stage and\njust pass them down to lower stages."}, {Name: "NewWindow", Doc: "NewWindow, if true, opens a [WindowStage] or [DialogStage] in its own\nseparate operating system window ([renderWindow]). This is true by\ndefault for [WindowStage] on non-mobile platforms, otherwise false."}, {Name: "FullWindow", Doc: "FullWindow, if [Stage.NewWindow] is false, makes [DialogStage]s and\n[WindowStage]s take up the entire window they are created in."}, {Name: "Maximized", Doc: "Maximized is whether to make a window take up the entire screen on desktop\nplatforms by default. It is different from [Stage.Fullscreen] in that\nfullscreen makes the window truly fullscreen without decorations\n(such as for a video player), whereas maximized keeps decorations and just\nmakes it fill the available space. The automatically saved user previous\nmaximized state takes precedence."}, {Name: "Fullscreen", Doc: "Fullscreen is whether to make a window fullscreen on desktop platforms.\nIt is different from [Stage.Maximized] in that fullscreen makes\nthe window truly fullscreen without decorations (such as for a video player),\nwhereas maximized keeps decorations and just makes it fill the available space.\nNot to be confused with [Stage.FullWindow], which is for stages contained within\nanother system window. See [Scene.IsFullscreen] and [Scene.SetFullscreen] to\ncheck and update fullscreen state dynamically on desktop and web platforms\n([Stage.SetFullscreen] sets the initial state, whereas [Scene.SetFullscreen]\nsets the current state after the [Stage] is already running)."}, {Name: "UseMinSize", Doc: "UseMinSize uses a minimum size as a function of the total available size\nfor sizing new windows and dialogs. Otherwise, only the content size is used.\nThe saved window position and size takes precedence on multi-window platforms."}, {Name: "Resizable", Doc: "Resizable specifies whether a window on desktop platforms can\nbe resized by the user, and whether a non-full same-window dialog can\nbe resized by the user on any platform. It defaults to true."}, {Name: "Timeout", Doc: "Timeout, if greater than 0, results in a popup stages disappearing\nafter this timeout duration."}, {Name: "BackButton", Doc: "BackButton is whether to add a back button to the top bar that calls\n[Scene.Close] when clicked. If it is unset, is will be treated as true\non non-[system.Offscreen] platforms for [Stage.FullWindow] but not\n[Stage.NewWindow] [Stage]s that are not the first in the stack."}, {Name: "DisplayTitle", Doc: "DisplayTitle is whether to display the [Stage.Title] using a\n[Text] widget in the top bar. It is on by default for [DialogStage]s\nand off for all other stages."}, {Name: "Pos", Doc: "Pos is the default target position for the [Stage] to be placed within\nthe surrounding window or screen in raw pixels. For a new window on desktop\nplatforms, the automatically saved user previous window position takes precedence.\nFor dialogs, this position is the target center position, not the upper-left corner."}, {Name: "Main", Doc: "If a popup stage, this is the main stage that owns it (via its [Stage.popups]).\nIf a main stage, it points to itself."}, {Name: "popups", Doc: "For main stages, this is the stack of the popups within it\n(created specifically for the main stage).\nFor popups, this is the pointer to the popups within the\nmain stage managing it."}, {Name: "Mains", Doc: "For all stages, this is the main [Stages] that lives in a [renderWindow]\nand manages the main stages."}, {Name: "renderContext", Doc: "rendering context which has info about the RenderWindow onto which we render.\nThis should be used instead of the RenderWindow itself for all relevant\nrendering information. This is only available once a Stage is Run,\nand must always be checked for nil."}, {Name: "Sprites", Doc: "Sprites are named images that are rendered last overlaying everything else."}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Stage", IDName: "stage", Doc: "Stage is a container and manager for displaying a [Scene]\nin different functional ways, defined by [StageTypes].", Directives: []types.Directive{{Tool: "types", Directive: "add", Args: []string{"-setters"}}}, Fields: []types.Field{{Name: "Type", Doc: "Type is the type of [Stage], which determines behavior and styling."}, {Name: "Scene", Doc: "Scene contents of this [Stage] (what it displays)."}, {Name: "Context", Doc: "Context is a widget in another scene that requested this stage to be created\nand provides context."}, {Name: "Name", Doc: "Name is the name of the Stage, which is generally auto-set\nbased on the [Scene.Name]."}, {Name: "Title", Doc: "Title is the title of the Stage, which is generally auto-set\nbased on the [Body.Title]. It used for the title of [WindowStage]\nand [DialogStage] types, and for a [Text] title widget if\n[Stage.DisplayTitle] is true."}, {Name: "Screen", Doc: "Screen specifies the screen number on which a new window is opened\nby default on desktop platforms. It defaults to -1, which indicates\nthat the first window should open on screen 0 (the default primary\nscreen) and any subsequent windows should open on the same screen as\nthe currently active window. Regardless, the automatically saved last\nscreen of a window with the same [Stage.Title] takes precedence if it exists;\nsee the website documentation on window geometry saving for more information.\nUse [TheApp].ScreenByName(\"name\").ScreenNumber to get the screen by name."}, {Name: "Modal", Doc: "Modal, if true, blocks input to all other stages."}, {Name: "Scrim", Doc: "Scrim, if true, places a darkening scrim over other stages."}, {Name: "ClickOff", Doc: "ClickOff, if true, dismisses the [Stage] if the user clicks anywhere\noff of the [Stage]."}, {Name: "ignoreEvents", Doc: "ignoreEvents is whether to send no events to the stage and\njust pass them down to lower stages."}, {Name: "NewWindow", Doc: "NewWindow, if true, opens a [WindowStage] or [DialogStage] in its own\nseparate operating system window ([renderWindow]). This is true by\ndefault for [WindowStage] on non-mobile platforms, otherwise false."}, {Name: "FullWindow", Doc: "FullWindow, if [Stage.NewWindow] is false, makes [DialogStage]s and\n[WindowStage]s take up the entire window they are created in."}, {Name: "Maximized", Doc: "Maximized is whether to make a window take up the entire screen on desktop\nplatforms by default. It is different from [Stage.Fullscreen] in that\nfullscreen makes the window truly fullscreen without decorations\n(such as for a video player), whereas maximized keeps decorations and just\nmakes it fill the available space. The automatically saved user previous\nmaximized state takes precedence."}, {Name: "Fullscreen", Doc: "Fullscreen is whether to make a window fullscreen on desktop platforms.\nIt is different from [Stage.Maximized] in that fullscreen makes\nthe window truly fullscreen without decorations (such as for a video player),\nwhereas maximized keeps decorations and just makes it fill the available space.\nNot to be confused with [Stage.FullWindow], which is for stages contained within\nanother system window. See [Scene.IsFullscreen] and [Scene.SetFullscreen] to\ncheck and update fullscreen state dynamically on desktop and web platforms\n([Stage.SetFullscreen] sets the initial state, whereas [Scene.SetFullscreen]\nsets the current state after the [Stage] is already running)."}, {Name: "UseMinSize", Doc: "UseMinSize uses a minimum size as a function of the total available size\nfor sizing new windows and dialogs. Otherwise, only the content size is used.\nThe saved window position and size takes precedence on multi-window platforms."}, {Name: "Resizable", Doc: "Resizable specifies whether a window on desktop platforms can\nbe resized by the user, and whether a non-full same-window dialog can\nbe resized by the user on any platform. It defaults to true."}, {Name: "Timeout", Doc: "Timeout, if greater than 0, results in a popup stages disappearing\nafter this timeout duration."}, {Name: "BackButton", Doc: "BackButton is whether to add a back button to the top bar that calls\n[Scene.Close] when clicked. If it is unset, is will be treated as true\non non-[system.Offscreen] platforms for [Stage.FullWindow] but not\n[Stage.NewWindow] [Stage]s that are not the first in the stack."}, {Name: "DisplayTitle", Doc: "DisplayTitle is whether to display the [Stage.Title] using a\n[Text] widget in the top bar. It is on by default for [DialogStage]s\nand off for all other stages."}, {Name: "Pos", Doc: "Pos is the default target position for the [Stage] to be placed within\nthe surrounding window or screen in raw pixels. For a new window on desktop\nplatforms, the automatically saved user previous window position takes precedence.\nFor dialogs, this position is the target center position, not the upper-left corner."}, {Name: "Main", Doc: "If a popup stage, this is the main stage that owns it (via its [Stage.popups]).\nIf a main stage, it points to itself."}, {Name: "popups", Doc: "For main stages, this is the stack of the popups within it\n(created specifically for the main stage).\nFor popups, this is the pointer to the popups within the\nmain stage managing it."}, {Name: "Mains", Doc: "For all stages, this is the main [Stages] that lives in a [renderWindow]\nand manages the main stages."}, {Name: "renderContext", Doc: "rendering context which has info about the RenderWindow onto which we render.\nThis should be used instead of the RenderWindow itself for all relevant\nrendering information. This is only available once a Stage is Run,\nand must always be checked for nil."}, {Name: "Sprites", Doc: "Sprites are named images that are rendered last overlaying everything else."}, {Name: "spritePainter", Doc: "spritePainter is the painter for sprite drawing."}, {Name: "spriteRenderer", Doc: "spriteRenderer is the renderer for sprite drawing."}}}) // SetContext sets the [Stage.Context]: // Context is a widget in another scene that requested this stage to be created @@ -1045,7 +1045,7 @@ func (t *Text) SetText(v string) *Text { t.Text = v; return t } // It defaults to [TextBodyLarge]. func (t *Text) SetType(v TextTypes) *Text { t.Type = v; return t } -var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.TextField", IDName: "text-field", Doc: "TextField is a widget for editing a line of text.\n\nWith the default [styles.WhiteSpaceNormal] setting,\ntext will wrap onto multiple lines as needed. You can\ncall [styles.Style.SetTextWrap](false) to force everything\nto be rendered on a single line. With multi-line wrapped text,\nthe text is still treated as a single contiguous line of wrapped text.", Directives: []types.Directive{{Tool: "core", Directive: "embedder"}}, Methods: []types.Method{{Name: "cut", Doc: "cut cuts any selected text and adds it to the clipboard.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "copy", Doc: "copy copies any selected text to the clipboard.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "paste", Doc: "paste inserts text from the clipboard at current cursor position; if\ncursor is within a current selection, that selection is replaced.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Type", Doc: "Type is the styling type of the text field."}, {Name: "Placeholder", Doc: "Placeholder is the text that is displayed\nwhen the text field is empty."}, {Name: "Validator", Doc: "Validator is a function used to validate the input\nof the text field. If it returns a non-nil error,\nthen an error color, icon, and tooltip will be displayed."}, {Name: "LeadingIcon", Doc: "LeadingIcon, if specified, indicates to add a button\nat the start of the text field with this icon.\nSee [TextField.SetLeadingIcon]."}, {Name: "LeadingIconOnClick", Doc: "LeadingIconOnClick, if specified, is the function to call when\nthe LeadingIcon is clicked. If this is nil, the leading icon\nwill not be interactive. See [TextField.SetLeadingIcon]."}, {Name: "TrailingIcon", Doc: "TrailingIcon, if specified, indicates to add a button\nat the end of the text field with this icon.\nSee [TextField.SetTrailingIcon]."}, {Name: "TrailingIconOnClick", Doc: "TrailingIconOnClick, if specified, is the function to call when\nthe TrailingIcon is clicked. If this is nil, the trailing icon\nwill not be interactive. See [TextField.SetTrailingIcon]."}, {Name: "NoEcho", Doc: "NoEcho is whether replace displayed characters with bullets\nto conceal text (for example, for a password input). Also\nsee [TextField.SetTypePassword]."}, {Name: "CursorWidth", Doc: "CursorWidth is the width of the text field cursor.\nIt should be set in a Styler like all other style properties.\nBy default, it is 1dp."}, {Name: "CursorColor", Doc: "CursorColor is the color used for the text field cursor (caret).\nIt should be set in a Styler like all other style properties.\nBy default, it is [colors.Scheme.Primary.Base]."}, {Name: "PlaceholderColor", Doc: "PlaceholderColor is the color used for the [TextField.Placeholder] text.\nIt should be set in a Styler like all other style properties.\nBy default, it is [colors.Scheme.OnSurfaceVariant]."}, {Name: "complete", Doc: "complete contains functions and data for text field completion.\nIt must be set using [TextField.SetCompleter]."}, {Name: "text", Doc: "text is the last saved value of the text string being edited."}, {Name: "edited", Doc: "edited is whether the text has been edited relative to the original."}, {Name: "editText", Doc: "editText is the live text string being edited, with the latest modifications."}, {Name: "error", Doc: "error is the current validation error of the text field."}, {Name: "effPos", Doc: "effPos is the effective position with any leading icon space added."}, {Name: "effSize", Doc: "effSize is the effective size, subtracting any leading and trailing icon space."}, {Name: "dispRange", Doc: "dispRange is the range of visible text, for scrolling text case (non-wordwrap)."}, {Name: "cursorPos", Doc: "cursorPos is the current cursor position as rune index into string."}, {Name: "cursorLine", Doc: "cursorLine is the current cursor line position, for word wrap case."}, {Name: "charWidth", Doc: "charWidth is the approximate number of chars that can be\ndisplayed at any time, which is computed from the font size."}, {Name: "selectRange", Doc: "selectRange is the selected range."}, {Name: "selectInit", Doc: "selectInit is the initial selection position (where it started)."}, {Name: "selectMode", Doc: "selectMode is whether to select text as the cursor moves."}, {Name: "selectModeShift", Doc: "selectModeShift is whether selectmode was turned on because of the shift key."}, {Name: "renderAll", Doc: "renderAll is the render version of entire text, for sizing."}, {Name: "renderVisible", Doc: "renderVisible is the render version of just the visible text in dispRange."}, {Name: "renderedRange", Doc: "renderedRange is the dispRange last rendered."}, {Name: "numLines", Doc: "number of lines from last render update, for word-wrap version"}, {Name: "lineHeight", Doc: "lineHeight is the line height cached during styling."}, {Name: "blinkOn", Doc: "blinkOn oscillates between on and off for blinking."}, {Name: "cursorMu", Doc: "cursorMu is the mutex for updating the cursor between blinker and field."}, {Name: "undos", Doc: "undos is the undo manager for the text field."}, {Name: "leadingIconButton"}, {Name: "trailingIconButton"}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.TextField", IDName: "text-field", Doc: "TextField is a widget for editing a line of text.\n\nWith the default [styles.WhiteSpaceNormal] setting,\ntext will wrap onto multiple lines as needed. You can\ncall [styles.Style.SetTextWrap](false) to force everything\nto be rendered on a single line. With multi-line wrapped text,\nthe text is still treated as a single contiguous line of wrapped text.", Directives: []types.Directive{{Tool: "core", Directive: "embedder"}}, Methods: []types.Method{{Name: "cut", Doc: "cut cuts any selected text and adds it to the clipboard.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "copy", Doc: "copy copies any selected text to the clipboard.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "paste", Doc: "paste inserts text from the clipboard at current cursor position; if\ncursor is within a current selection, that selection is replaced.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Type", Doc: "Type is the styling type of the text field."}, {Name: "Placeholder", Doc: "Placeholder is the text that is displayed\nwhen the text field is empty."}, {Name: "Validator", Doc: "Validator is a function used to validate the input\nof the text field. If it returns a non-nil error,\nthen an error color, icon, and tooltip will be displayed."}, {Name: "LeadingIcon", Doc: "LeadingIcon, if specified, indicates to add a button\nat the start of the text field with this icon.\nSee [TextField.SetLeadingIcon]."}, {Name: "LeadingIconOnClick", Doc: "LeadingIconOnClick, if specified, is the function to call when\nthe LeadingIcon is clicked. If this is nil, the leading icon\nwill not be interactive. See [TextField.SetLeadingIcon]."}, {Name: "TrailingIcon", Doc: "TrailingIcon, if specified, indicates to add a button\nat the end of the text field with this icon.\nSee [TextField.SetTrailingIcon]."}, {Name: "TrailingIconOnClick", Doc: "TrailingIconOnClick, if specified, is the function to call when\nthe TrailingIcon is clicked. If this is nil, the trailing icon\nwill not be interactive. See [TextField.SetTrailingIcon]."}, {Name: "NoEcho", Doc: "NoEcho is whether replace displayed characters with bullets\nto conceal text (for example, for a password input). Also\nsee [TextField.SetTypePassword]."}, {Name: "CursorWidth", Doc: "CursorWidth is the width of the text field cursor.\nIt should be set in a Styler like all other style properties.\nBy default, it is 1dp."}, {Name: "CursorColor", Doc: "CursorColor is the color used for the text field cursor (caret).\nIt should be set in a Styler like all other style properties.\nBy default, it is [colors.Scheme.Primary.Base]."}, {Name: "PlaceholderColor", Doc: "PlaceholderColor is the color used for the [TextField.Placeholder] text.\nIt should be set in a Styler like all other style properties.\nBy default, it is [colors.Scheme.OnSurfaceVariant]."}, {Name: "complete", Doc: "complete contains functions and data for text field completion.\nIt must be set using [TextField.SetCompleter]."}, {Name: "text", Doc: "text is the last saved value of the text string being edited."}, {Name: "edited", Doc: "edited is whether the text has been edited relative to the original."}, {Name: "editText", Doc: "editText is the live text string being edited, with the latest modifications."}, {Name: "error", Doc: "error is the current validation error of the text field."}, {Name: "effPos", Doc: "effPos is the effective position with any leading icon space added."}, {Name: "effSize", Doc: "effSize is the effective size, subtracting any leading and trailing icon space."}, {Name: "dispRange", Doc: "dispRange is the range of visible text, for scrolling text case (non-wordwrap)."}, {Name: "cursorPos", Doc: "cursorPos is the current cursor position as rune index into string."}, {Name: "cursorLine", Doc: "cursorLine is the current cursor line position, for word wrap case."}, {Name: "charWidth", Doc: "charWidth is the approximate number of chars that can be\ndisplayed at any time, which is computed from the font size."}, {Name: "selectRange", Doc: "selectRange is the selected range."}, {Name: "selectInit", Doc: "selectInit is the initial selection position (where it started)."}, {Name: "selectMode", Doc: "selectMode is whether to select text as the cursor moves."}, {Name: "selectModeShift", Doc: "selectModeShift is whether selectmode was turned on because of the shift key."}, {Name: "renderAll", Doc: "renderAll is the render version of entire text, for sizing."}, {Name: "renderVisible", Doc: "renderVisible is the render version of just the visible text in dispRange."}, {Name: "renderedRange", Doc: "renderedRange is the dispRange last rendered."}, {Name: "numLines", Doc: "number of lines from last render update, for word-wrap version"}, {Name: "lineHeight", Doc: "lineHeight is the line height cached during styling."}, {Name: "cursorMu", Doc: "cursorMu is the mutex for updating the cursor between blinker and field."}, {Name: "undos", Doc: "undos is the undo manager for the text field."}, {Name: "leadingIconButton"}, {Name: "trailingIconButton"}}}) // NewTextField returns a new [TextField] with the given optional parent: // TextField is a widget for editing a line of text. @@ -1193,9 +1193,9 @@ var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Toolbar", IDNa // https://cogentcore.org/core/toolbar. 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: "DragDrop", Args: []string{"e"}}, {Name: "DropDeleteSource", Args: []string{"e"}}}}) +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: "Copy", Doc: "Copy copies the tree to the clipboard.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Cut", Doc: "Cut copies to [system.Clipboard] and deletes selected items.", 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 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."}}}) // NewTree returns a new [Tree] with the given optional parent: // Tree provides a graphical representation of a tree structure, diff --git a/core/widgetevents.go b/core/widgetevents.go index 5b5be1cf65..f5174a3b60 100644 --- a/core/widgetevents.go +++ b/core/widgetevents.go @@ -204,6 +204,15 @@ func (wb *WidgetBase) UpdateChange(original ...events.Event) { wb.Update() } +// UpdateInput is a helper function that calls [WidgetBase.SendInput] +// and then [WidgetBase.UpdateRender]. That is the correct order, since +// calling [WidgetBase.UpdateRender] first would cause the value of the widget +// to be incorrectly overridden in a [Value] context. +func (wb *WidgetBase) UpdateInput(original ...events.Event) { + wb.Send(events.Input, original...) + wb.UpdateRender() +} + func (wb *WidgetBase) sendKey(kf keymap.Functions, original ...events.Event) { if wb.This == nil { return @@ -248,7 +257,7 @@ func (wb *WidgetBase) HandleEvent(e events.Event) { s := &wb.Styles state := s.State - wb.Listeners.Do(func(l events.Listeners) { + wb.Listeners.Do(func(l *events.Listeners) { l.Call(e, func() bool { return wb.This != nil }) @@ -459,7 +468,8 @@ func (wb *WidgetBase) handleWidgetMagnify() { }) } -// handleValueOnChange adds a handler that calls [WidgetBase.ValueOnChange]. +// handleValueOnChange adds a handler that calls [WidgetBase.ValueOnChange], +// for [events.Change]. This is installed by default by [Bind]. func (wb *WidgetBase) handleValueOnChange() { // need to go before end-user OnChange handlers wb.OnFirst(events.Change, func(e events.Event) { @@ -469,6 +479,19 @@ func (wb *WidgetBase) handleValueOnChange() { }) } +// HandleValueOnInput adds a handler that calls [WidgetBase.ValueOnChange], +// for [events.Input] events. This is not done by default, but can be useful +// if handling input events from a [Slider] for example. +// This is not generally a good idea for a [TextField] or other text-input widget, +// because the input-level partial value is often not parseable. +func (wb *WidgetBase) HandleValueOnInput() { + wb.OnFirst(events.Input, func(e events.Event) { + if wb.ValueOnChange != nil { + wb.ValueOnChange() + } + }) +} + // SendChangeOnInput adds an event handler that does [WidgetBase.SendChange] // in [WidgetBase.OnInput]. This is not done by default, but you can call it // if you want [events.Input] to trigger full change events, such as in a [Bind] diff --git a/cursors/README.md b/cursors/README.md index a377ddd244..33e819a2d4 100644 --- a/cursors/README.md +++ b/cursors/README.md @@ -1,2 +1,4 @@ # cursors + Cursors provides Go constant names for cursors as SVG files. These cursors are sourced from [daviddarnes/mac-cursors](https://github.com/daviddarnes/mac-cursors), which are from macOS and are licensed under the [Apple User Agreement](https://images.apple.com/legal/sla/docs/OSX1011.pdf). + diff --git a/docs/content/app.md b/docs/content/app.md index 79a766ab0c..141a390a0a 100644 --- a/docs/content/app.md +++ b/docs/content/app.md @@ -2,7 +2,7 @@ Categories = ["Concepts"] +++ -The first call in every **app** is [[doc:core.NewBody]]. This creates and returns a new [[doc:core.Body]], which is a container in which app content is placed. This takes an optional name, which is used for the title of the app/window/tab. +The first call in every **app** is [[doc:core.NewBody]]. This creates and returns a new [[body]], which is a container in which app content is placed. This takes an optional name, which is used for the title of the app/window/tab. After calling NewBody, you add [[widget]]s to the body that was returned, which is typically given the local variable name `b` for body. diff --git a/docs/content/body.md b/docs/content/body.md new file mode 100644 index 0000000000..325071c94a --- /dev/null +++ b/docs/content/body.md @@ -0,0 +1,14 @@ ++++ +Categories = ["Widgets"] ++++ + +A **body** is a [[frame]] widget that contains most of the elements constructed within a [[scene]]. + +The `b` widget that shows up in all of the examples in these docs represents the body that you typically add widgets to, as in the [[hello world tutorial]]: + + + +The [[doc:core.Body]] has methods to create different types of [[scene]] stages, such as the `RunMainWindow()` method in the above example, which manage the process of actually showing the body content to the user. + +If you want to customize the behavior, do `b.NewWindow()` to get a new [[doc:core.Stage]], which has various optional parameters that can be configured. + diff --git a/docs/content/button.md b/docs/content/button.md index 881b953210..b21a553b9b 100644 --- a/docs/content/button.md +++ b/docs/content/button.md @@ -98,3 +98,23 @@ Action and menu buttons are the most minimal buttons, and they are typically onl ```Go core.NewButton(b).SetType(core.ButtonAction).SetText("Action") ``` + +## Styles + +You can change the [[styles#font size]] of a button, which changes the size of its [[text]] and/or [[icon]]: + +```Go +bt := core.NewButton(b).SetText("Download").SetIcon(icons.Download) +bt.Styler(func(s *styles.Style) { + s.Font.Size.Dp(20) +}) +``` + +To change only the size of the icon, you can set the [[icon#icon size]] of a button: + +```Go +bt := core.NewButton(b).SetText("Add").SetIcon(icons.Add) +bt.Styler(func(s *styles.Style) { + s.IconSize.Set(units.Dp(30)) +}) +``` diff --git a/docs/content/color-picker.md b/docs/content/color-picker.md index 058428b791..78405875d2 100644 --- a/docs/content/color-picker.md +++ b/docs/content/color-picker.md @@ -23,6 +23,15 @@ cp.OnChange(func(e events.Event) { }) ``` +You can detect when a user changes the color as they slide ([[events#input]]): + +```Go +cp := core.NewColorPicker(b).SetColor(colors.Green) +cp.OnInput(func(e events.Event) { + core.MessageSnackbar(cp, colors.AsHex(cp.Color)) +}) +``` + ## Color button You can make a [[button]] that opens a color picker [[dialog]]: diff --git a/docs/content/color.md b/docs/content/color.md index 44efbd301d..2b88892da3 100644 --- a/docs/content/color.md +++ b/docs/content/color.md @@ -1,5 +1,5 @@ +++ -Categories = ["Concepts"] +Categories = ["Resources"] +++ **Colors** can be specified in several different ways. Colors are used for [[styles#color|styles]], and can be represented with the [[color picker]] widget. @@ -38,7 +38,7 @@ core.NewForm(b).SetStruct(colors.Scheme) ## Named colors -You can also use named colors: +You can also use named colors (see [[#List of named colors]] below): ```Go bt := core.NewButton(b).SetText("Hello") @@ -117,3 +117,36 @@ fr.Styler(func(s *styles.Style) { s.Min.Set(units.Em(5)) }) ``` + +## List of named colors + +```Go +clrs:= core.NewFrame(b) +clrs.Styler(func(s *styles.Style) { + s.Display = styles.Grid + s.Columns = 8 + s.Gap.Set(units.Dp(18)) +}) +clrs.Maker(func(p *tree.Plan) { + for _, cn := range colors.Names { + nm := strcase.ToCamel(cn) + tree.AddAt(p, nm, func(w *core.Frame) { + w.Styler(func(s *styles.Style) { + s.Direction = styles.Column + }) + cp := core.NewFrame(w) + cp.Styler(func(s *styles.Style) { + s.Min.Set(units.Em(4)) + s.Background = colors.Uniform(colors.Map[cn]) + }) + core.NewText(w).SetText(nm).Styler(func(s *styles.Style) { + s.SetTextWrap(false) + s.CenterAll() + }) + }) + } +}) +``` + + + diff --git a/docs/content/cursors.md b/docs/content/cursors.md new file mode 100644 index 0000000000..35be9e22cb --- /dev/null +++ b/docs/content/cursors.md @@ -0,0 +1,46 @@ ++++ +Categories = ["Resources"] ++++ + +The [[doc:cursors]] package contains standard cursors that can be set to indicate the kinds of actions available on a widget. The cursor changes when the mouse enters the space of the widget. + +Set the cursor in a styler: +```Go +core.NewText(b).SetText("Mouse over to see the cursor").Styler(func(s *styles.Style) { + s.Cursor = cursors.Help +}) +``` + +This code shows all the standard cursors available: + +```Go +curs:= core.NewFrame(b) +curs.Styler(func(s *styles.Style) { + s.Display = styles.Grid + s.Columns = 4 + s.Gap.Set(units.Dp(18)) +}) +curs.Maker(func(p *tree.Plan) { + for _, c := range cursors.CursorN.Values() { + nm := strcase.ToCamel(c.String()) + fnm := filepath.Join("svg", c.String() + ".svg") + tree.AddAt(p, nm, func(w *core.Frame) { + w.Styler(func(s *styles.Style) { + s.Direction = styles.Column + s.Cursor = c.(cursors.Cursor) + }) + sv := core.NewSVG(w) + sv.OpenFS(cursors.Cursors, fnm) + sv.Styler(func(s *styles.Style) { + s.Min.Set(units.Em(4)) + s.Cursor = c.(cursors.Cursor) + }) + core.NewText(w).SetText(nm).Styler(func(s *styles.Style) { + s.SetTextWrap(false) + s.CenterAll() + }) + }) + } +}) +``` + diff --git a/docs/content/icon.md b/docs/content/icon.md index be035fae85..9c5aa3ecf1 100644 --- a/docs/content/icon.md +++ b/docs/content/icon.md @@ -2,7 +2,7 @@ Categories = ["Widgets"] +++ -Cogent Core provides more than 2,000 unique **icons** from the [Material Symbols collection](https://fonts.google.com/icons), allowing you to easily represent many things in a concise, visually pleasing, and language-independent way. +Cogent Core provides more than 2,000 unique **icons** from the [Material Symbols collection](https://fonts.google.com/icons), allowing you to easily represent many things in a concise, visually pleasing, and language-independent way. See [[icons]] for more info about this resource, and how to use additional icons in your app. Icons are specified using their named variable in the [[doc:icons]] package, and they are typically used in the context of another [[widget]], like a [[button]]: @@ -22,34 +22,44 @@ You can use the filled version of an icon: core.NewButton(b).SetIcon(icons.HomeFill) ``` -## Custom icons +## Styles -You can add custom icons to your app using icongen, a part of the [[generate]] tool. Custom icons are typically placed in a `cicons` (custom icons) directory. In it, you can add all of your custom SVG icon files and an `icons.go` file with the following code: +### Icon size -```go -package cicons +You can change the size of an icon: -//go:generate core generate -icons . +```Go +ic := core.NewIcon(b).SetIcon(icons.Home) +ic.Styler(func(s *styles.Style) { + s.IconSize.Set(units.Dp(40)) +}) ``` -Then, once you run `go generate`, you can access your icons through your cicons package, where icon names are automatically transformed into CamelCase: +You can specify different icon sizes for each dimension: -```go -core.NewButton(b).SetIcon(cicons.MyIconName) +```Go +ic := core.NewIcon(b).SetIcon(icons.Home) +ic.Styler(func(s *styles.Style) { + s.IconSize.Set(units.Dp(40), units.Dp(20)) +}) ``` -### Image icons - -Although only SVG files are supported for icons, you can easily embed a bitmap image file in an SVG file. Cogent Core provides an `svg` command line tool that can do this for you. To install it, run: +Icon size is an inherited property, so you can set it on a parent widget like a [[button]] and its icon will update accordingly: -```sh -go install cogentcore.org/core/svg/cmd/svg@main +```Go +bt := core.NewButton(b).SetText("Send").SetIcon(icons.Send) +bt.Styler(func(s *styles.Style) { + s.IconSize.Set(units.Dp(30)) +}) ``` -Then, to embed an image into an svg file, run: +You can also use [[styles#font size]], which applies to all children including icons: -```sh -svg embed-image my-image.png +```Go +tf := core.NewTextField(b).SetText("Hello").SetLeadingIcon(icons.Euro).SetTrailingIcon(icons.OpenInNew) +tf.Styler(func(s *styles.Style) { + s.Font.Size.Dp(20) +}) ``` -This will create a file called `my-image.svg` that has the image embedded into it. Then, you can use that SVG file as an icon as described above. + diff --git a/docs/content/icons.md b/docs/content/icons.md new file mode 100644 index 0000000000..44ea558f97 --- /dev/null +++ b/docs/content/icons.md @@ -0,0 +1,42 @@ ++++ +Categories = ["Resources"] ++++ + +The [[doc:icons]] package contains the [Material Design Symbols](https://fonts.google.com/icons), sourced through [marella/material-symbols](https://github.com/marella/material-symbols). + +See [[icon]] for information about the icon widget and how to set icons in stylers. + +Icons are represented directly as an SVG string that draws the icon, and only those icons that your app actually uses are included, to minimize executable size. This also makes it easy to add new icon sets. + +## Custom icons + +You can add custom icons to your app using icongen, a part of the [[generate]] tool. Custom icons are typically placed in a `cicons` (custom icons) directory. In it, you can add all of your custom SVG icon files and an `icons.go` file with the following code: + +```go +package cicons + +//go:generate core generate -icons . +``` + +Then, once you run `go generate`, you can access your icons through your cicons package, where icon names are automatically transformed into CamelCase: + +```go +core.NewButton(b).SetIcon(cicons.MyIconName) +``` + +### Image icons + +Although only SVG files are supported for icons, you can easily embed a bitmap image file in an SVG file. Cogent Core provides an `svg` command line tool that can do this for you. To install it, run: + +```sh +go install cogentcore.org/core/svg/cmd/svg@main +``` + +Then, to embed an image into an svg file, run: + +```sh +svg embed-image my-image.png +``` + +This will create a file called `my-image.svg` that has the image embedded into it. Then, you can use that SVG file as an icon as described above. + diff --git a/docs/content/layout.md b/docs/content/layout.md new file mode 100644 index 0000000000..50ce5e2ad4 --- /dev/null +++ b/docs/content/layout.md @@ -0,0 +1,77 @@ ++++ +Categories = ["Architecture"] ++++ + +**Layout** is the process of sizing and positioning widgets in a [[scene]], according to the [[styles#Layout]] styles on each widget. Each [[frame]] widget performs the layout of its child widgets, recursively, under control of the overall [[scene]]. + +Each [[doc:core.WidgetBase]] has a `Geom` field that contains the results of the layout process for each widget. + +The layout process involves multiple passes up and down the [[doc:tree]] of widgets within the [[scene]], as described below. + +## Sizing + +Determining the proper size of a given widget involves both _bottom-up_ and _top-down_ constraints: + +* Bottom-up constraints come from the `Min` and `Max` [[styles#Layout]] styles, and the size needed by [[text]] elements to fully display the relevant text, styled according to the [[styles]] settings. + +* Top-down constraints come ultimately from the size of the window containing the [[scene]] in which the content is being displayed, and are propagated down from there to all of the [[frame]] elements in the scene. Each frame gets an _allocation_ of space from above, and then allocates that space to its children, on down the line. + +To satisfy these constraints, multiple passes are required as described below. + +The final result of this process is represented in two key variables in the `Geom` state: + +* **Actual** represents the actual pixel size for a given widget. This is the territory it "owns" and can render into. + +* **Alloc** is the amount allocated to the widget from above. + +Critically, the _positioning_ of widget elements within their parent frame is based on their `Alloc` size. For example, a `Grid` display type will allocate each element in the grid a regularly-sized rectangle box, even if it doesn't need the whole space allocated to render (i.e., its `Actual` may be smaller than its `Alloc`). To keep these elements positioned regularly, the alloc sizes are used, not the actual sizes. + +There are also two different values for each size, according to the box model: + +![Box model](media/box-model.png) + +* **Content** represents the inner contents, which is what the style Min and Max values specify, and has the height and width shown in the above figure. + +* **Total** represents the full size including the padding, border and margin spacing. + +The three types of passes are: + +### SizeUp + +This is the _bottom-up_ pass, from terminal leaf widgets up to through their container frames, and sets the `Actual` sizes based on the styling and text "hard" constraints. + +### SizeDown + +This is the _top-down_ pass, which may require multiple iterations. It starts from the size of the [[scene]], and then allocates that to child elements based on their `Actual` size needs from the `SizeUp` pass. + +If there is extra space available, it is allocated according to the `Grow` styling factors. This is what allows widgets to fill up available space instead of only taking their minimal bottom-up needs. + +Flexible elements (e.g., Flex Wrap layouts and Text with word wrap) update their Actual size based on the available Alloc size (re-wrap), to fit the allocated shape vs. the initial bottom-up guess. + +It is critical that no elements actually Grow to fill the Alloc space at this point: the `Actual` must remain as a _minimal_ "hard" requirement throughout this process. Alloc is only used for reshaping, not growing. + +If any of the elements changed their size at this point, e.g., due to re-wrapping, then another iteration is taken. Re-wrapping can also occur if a scrollbar was added or removed from an internal frame element (if it has an `Overflow` setting of `styles.OverflowAuto` for example). + +### SizeFinal + +SizeFinal: is a final bottom-up pass similar to SizeUp, but now with the benefit of allocation-constrained `Actual` sizes. + +For any elements with Grow > 0, the `Actual` sizes can grow up to their `Alloc` space. Frame containers +accumulate these new actual values for use in positioning. + +## Positioning + +The `Position` pass uses the final sizes to set _relative_ positions within their containing widget (`RelPos`) according to the `Align` and `Justify` [[styles]] settings. + +Critically, if `Actual` == `Alloc`, then these settings have no effect! It is only when the actual size is less than the alloc size that there is extra "room" to move an element around within its allocation. + +The final `ApplyScenePos` pass computes scene-based _absolute_ positions and final bounding boxes (`BBox`) for rendering, based on the relative positions. + +This pass also incorporates offsets from scrolling, and is the only layout pass that is required during scrolling, which is good because it is very fast. + +## Links + +* [Flutter](https://docs.flutter.dev/resources/architectural-overview#rendering-and-layout) uses a similar strategy for reference. + +* [stackoverflow](https://stackoverflow.com/questions/53911631/gui-layout-algorithms-overview) has a discussion of layout issues. + diff --git a/docs/content/media/box-model.png b/docs/content/media/box-model.png new file mode 100644 index 0000000000..aebbd16aaa Binary files /dev/null and b/docs/content/media/box-model.png differ diff --git a/docs/content/media/box-model.svg b/docs/content/media/box-model.svg new file mode 100644 index 0000000000..c8304a7b57 --- /dev/null +++ b/docs/content/media/box-model.svg @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Content + + + + Height + + Width + Padding + Margin + Border + + + + + + + diff --git a/docs/content/media/render-overview.png b/docs/content/media/render-overview.png new file mode 100644 index 0000000000..e56cfd3099 Binary files /dev/null and b/docs/content/media/render-overview.png differ diff --git a/docs/content/media/render-overview.svg b/docs/content/media/render-overview.svg new file mode 100644 index 0000000000..337e3209cd --- /dev/null +++ b/docs/content/media/render-overview.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + render.Render + Painter + Source + Canvas + Scene + html Canvas + Image + render.Renderer + SVG + SVG + + + + { + } + diff --git a/docs/content/plan.md b/docs/content/plan.md index 0c4e99aac2..4037324d65 100644 --- a/docs/content/plan.md +++ b/docs/content/plan.md @@ -23,3 +23,48 @@ spinner.OnChange(func(e events.Event) { ``` Plans are a powerful tool that are critical for some widgets such as those that need to dynamically manage hundreds of children in a convenient and performant way. They aren't always necessary, but you will find them being used a lot in complicated apps, and you will see more examples of them in the rest of this documentation. + +## Plan logic + +A `Plan` is a list (slice) of [[doc:tree.PlanItem]]s that specify all of the children for a given widget. + +Each item must have a unique name, specified in the `PlanItem`, which is used for updating the children in an efficient way to ensure that the widget actually has the correct children. If the current children have all of the same names as the `Plan` list, then nothing is done. Otherwise, any missing items are inserted, and any extra ones are removed, and everything is put in the correct order according to the `Plan`. + +The `Init` function(s) in the `PlanItem` are only run _once_ when a new widget element is made, so they should contain all of the initialization steps such as adding [[styles]] `Styler` functions and [[events]] handling functions. + +There are functions in the `tree` package that use generics to make it easy to add plan items. The type of child widget to create is determined by the type in the Init function, for example this code: + +```go +tree.AddAt(p, strconv.Itoa(i), func(w *core.Button) { +``` + +specifies that a `*core.Button` will be created. + +* [[doc:tree.AddAt]] adds a new item to a `Plan` with a specified name (which must be unique!), and an `Init` function. +* [[doc:tree.Add]] does `AddAt` with an automatically-generated unique name based on the location in the code where the function is called. This only works for one-off calls, not in a for-loop where multiple elements are made at the same code line, like the above example. + +## Maker function + +A [[doc:tree.NodeBase.Maker]] function adds items to the Plan for a widget. + +By having multiple such functions, each function can handle a different situation, when there is more complex logic that determines what child elements are needed, and how they should be configured. + +Functions are called in the order they are added, and there are three [[doc:base/tiered.Tiered]] levels of these function lists, to allow more control over the ordering. + +* [[doc:tree.AddChild]] adds a Maker function that calls [[doc:tree.Add]] to add a new child element. This is helpful when adding a few children in the main `Init` function of a given widget, saving the need to nest everything within an explicit `Maker` function. +* [[doc:tree.AddChildAt]] is the `AddAt` version that takes a unique name instead of auto-generating it. + +## Styling sub-elements of widgets + +The [[doc:tree.AddChildInit]] function can be used to modify the styling of elements within another widget. For example, many standard widgets have an optional [[icon]] element (e.g., [[button]], [[chooser]]). If you want to change the size of that icon, you can do something like this: + +```Go +tree.AddChildInit(core.NewButton(b).SetIcon(icons.Download), "icon", func(w *core.Icon) { + w.Styler(func(s *styles.Style) { + s.Min.Set(units.Em(2)) + }) +}) +``` + +`AddChildInit` adds a new `Init` function to an existing `PlanItem`'s list of such functions. You just need to know the name of the children, which can be found using the [[inspector]] (in general they are lower kebab-case names based on the corresponding `Set` function, e.g., `SetIcon` -> `icon`, etc) + diff --git a/docs/content/principles.md b/docs/content/principles.md index f1d04ad970..487b98860f 100644 --- a/docs/content/principles.md +++ b/docs/content/principles.md @@ -12,6 +12,10 @@ The eventual result of this trend is that people end up stuffing entire programm The solution to this is simple: whenever possible, everything should be written in real code, preferably in one language. Therefore, Cogent Core takes this approach: everything, from [[doc:tree]]s to [[widget]]s to [[style]]s to [[enum]]s, is written in real, pure Go code. The only non-Go functional files in a Cogent Core package or app should be TOML files, which are only used for very simple configuration options to commands, and not for any actual code. +### Closures everywhere + +An important implication of the above is that _closure functions_ should be used anywhere that any kind of configuration is needed, instead of trying to parameterize the space of options that might likely be needed. A closure can contain arbitrary code-based logic while capturing arbitrary external context needed to conditionalize this logic. Thus, closure functions are used everywhere in Cogent Core, including `Styler` functions in [[styles]], `Maker` functions in [[plan]]s, [[event]] handlers, etc. + ## Go is the best programming language (for GUIs) There are many programming languages, and each one represents a different set of tradeoffs. We think that Go is the best language (for GUIs and most other use-cases) according to the following list of fundamental characteristics: diff --git a/docs/content/render.md b/docs/content/render.md index 437dcafba5..c938aa8e4e 100644 --- a/docs/content/render.md +++ b/docs/content/render.md @@ -6,9 +6,9 @@ Categories = ["Architecture"] On all platforms except web, the core rendering logic is pure Go, with cgo used for pushing images to the system window. On web, we use HTML canvas, as discussed later. -The overall flow of rendering is: +The overall flow of rendering is shown in this figure: -* Source (SVG, `core.Scene`, etc) -> `Painter` (`render.Render`) -> `render.Renderer` (`image.Image`, SVG, PDF) +![Render overview](media/render-overview.png) All rendering goes through the `Painter` object in the [[doc:paint]] package, which provides a standard set of drawing functions, operating on a `State` that has a stack of `Context` to provide context for these drawing functions (style, transform, bounds, clipping, and mask). Each of these drawing functions is recorded in a `Render` list of render `Item`s (see [[doc:paint/render]]), which provides the intermediate representation of everything that needs to be rendered in a given render update pass. There are three basic types of render `Item`s: @@ -28,10 +28,12 @@ On the web (`js` platform), we take advantage of the standardized, typically GPU ## Core Scene and Widget rendering logic -At the highest level, rendering is made robust by having a completely separate, mutex lock-protected pass where all render-level updating takes place. This render pass is triggered by [[doc:events.WindowPaint]] events that are sent regularly at the monitor's refresh rate. If nothing needs to be updated, nothing happens (which is the typical case for most frames), so it is not a significant additional cost. +At the highest level, rendering is made robust by having a completely separate, mutex lock-protected pass where all render-level updating takes place. This render pass is triggered by [[doc:events.WindowPaint]] events that are sent regularly at the monitor's refresh rate. If nothing needs to be updated, nothing happens (which is the typical case for most frames), so it is not a significant additional cost. The usual processing of events that arise in response to user GUI actions, or any other source of changes, sets flags that determine what kind of updating needs to happen during rendering. These are typically set via [[doc:core.WidgetBase.NeedsRender]] or [[doc:core.WidgetBase.NeedsLayout]] calls. +Critically, most updates involve just one widget (and its children) that has updated its state or style in some way, and the rendering of the [[scene]] is _cumulative_ so that only this one widget needs to be re-rendered, instead of requiring a full redraw of the entire scene for every update. + The first step in the `renderWindow.renderWindow()` rendering function is to call `updateAll()` which ends up calling `doUpdate()` on the [[doc:core.Scene]] elements within a render window, and this function is what checks if a new [layout](layout) pass has been called for, or whether any individual widgets need to be rendered. Most updating of widgets happens in the event processing loop, which is synchronous (one event is processed at a time). @@ -40,6 +42,8 @@ For any updating that happens outside of the normal event loop (e.g., timer-base The result of the `renderWindow()` function for each Scene is a `render.Render` list of rendering commands, which could be just for one widget that needed updating, or for the entire scene if a new layout was needed. +The [[sprite]] system provides an additional top-level rendering layer that is always redrawn and can be used for dynamic content that can be positioned anywhere across the scene window. + ## Composer and sources The [[doc:system/composer]] manages the final rendering to the platform-specific window that you actually see as a user. It maintains a list of `Source` elements that provide platform-specific rendering logic, with one such source for each active Scene in a GUI (e.g., a dialog Scene could be on top of a main window Scene). diff --git a/docs/content/scene.md b/docs/content/scene.md new file mode 100644 index 0000000000..4279e32bb7 --- /dev/null +++ b/docs/content/scene.md @@ -0,0 +1,34 @@ ++++ +Categories = ["Architecture"] ++++ + +The **scene** contains all of the [[widget]] elements for a window or a dialog. + +It is a type of [[frame]] with optional `Bars` elements that can be configured on each side around a central [[body]] element that has the main scene contents. + +The Scene has the [[doc:paint.Painter]] that all widgets use to [[render]], and it manages the [[events]] for all of its widgets. + +## Bars + +The `Bars` on the scene contain functions for configuring optional [[toolbar]]-like elements on any side surrounding the central body content. + +The `Top` bar is most frequently used, typically in this way: + +```go + b.AddTopBar(func(bar *Frame) { + NewToolbar(bar).Maker(w.MakeToolbar) + }) +``` + +Where `w.MakeToolbar` is a [[plan#Maker function]] taking a `*tree.Plan` argument, that configures the [[toolbar]] with the [[button]] and [[func button]] actions for this scene. + +## Stages + +The [[doc:core.Stage]] provides the outer "setting" for a scene, and manages its behavior in relation to other scenes within the overall [[app]]. + +The different [[doc:core.StageTypes]] include things like `Window`, `Dialog`, `Menu`, `Tooltip` etc. + +The `Window` and `Dialog` are _main_ stages, whereas the others are _popup_ stages, with different overall behavior. + + + diff --git a/docs/content/sprite.md b/docs/content/sprite.md new file mode 100644 index 0000000000..49795663e9 --- /dev/null +++ b/docs/content/sprite.md @@ -0,0 +1,14 @@ ++++ +Categories = ["Concepts"] ++++ + +A **sprite** is a is a top-level rendering element similar to a [[canvas]] that paints onto a transparent layer over the entire window that is cleared every render pass. Sprites are used for text cursors in the [[text field]] and [[text editor]] and for [[drag and drop]]. To support cursor sprites and other animations, the sprites are redrawn at a minimum update rate that is at least as fast as CursorBlinkTime. + +Sprites can also receive mouse [[events]], within their event bounding box, so they can be used for dynamic control functions, like the "handles" in a drawing program, as in the [Cogent Canvas](https://github.com/cogentcore/cogent/canvas) app. + +The primary features of sprites are: + +* They are not under control of the [[layout]] system, and can be placed anywhere. + +* The sprite layer is cleared every time and overlaid on top of any other content, whereas the [[scene]] normally accumulates drawing over time so that different widget-regions can be focally updated without having to redraw the entire scene every time. + diff --git a/docs/content/struct-tags.md b/docs/content/struct-tags.md index 6f08953d5d..242ae73dcc 100644 --- a/docs/content/struct-tags.md +++ b/docs/content/struct-tags.md @@ -8,12 +8,15 @@ A cheat sheet of all possible **struct tags** and their meanings. See the linked * `json:"-"`, `xml:"-"`, `toml:"-"` — do not save/load a field with JSON/XML/TOML * `copier:"-"` — do not copy a field when copying/cloning a widget * `min:"{number}"`, `max:"{number}"`, `step:"{number}"` — customize the min/max/step of a [[spinner]] or [[slider]] +* `width:"{number}"`, `height:"{number}"`, `max-width:"{number}"`, `max-height:"{number}"` — specifies the size of the field's value widget in a [[form]] or [[table]], setting the [[styles]] `Min` (no prefix) or `Max` width in `Ch` (chars) or height in `Em` (font height units). +* `grow:"{number}"`, `grow-y:"{number}"` — specifies the [[styles]] `Grow` factor for the field's value widget in the X or Y dimension. +* `new-window:"+"` — causes a struct field in a [[form]] or [[table]] to open in a new popup dialog window by default, instead of requiring a `Shift` key to do so. The default is to open a full window dialog that replaces the current contents. * `display` — customize the appearance of a field in a [[form]] or [[table]] - * `display:"-"` — hide a field - * `display:"add-fields"` — expand subfields in a [[form]] - * `display:"{inline|no-inline}"` — display a slice/map/struct inline or not inline - * `display:"{switch-type}"` — customize the type of a [[switch]] - * `display:"{date|time}"` — only display the date or time of a [[time picker#time input]] + - `display:"-"` — hide a field + - `display:"add-fields"` — expand subfields in a [[form]] + - `display:"{inline|no-inline}"` — display a slice/map/struct inline or not inline + - `display:"{switch-type}"` — customize the type of a [[switch]] + - `display:"{date|time}"` — only display the date or time of a [[time picker#time input]] * `table` — override the value of `display` in a [[table]] - * `table:"-"` — hide a column - * `table:"+"` — show a column + - `table:"-"` — hide a column in a [[table]] + - `table:"+"` — show a column in a [[table]] diff --git a/docs/content/styles.md b/docs/content/styles.md index 24d383328b..8693cc8c2c 100644 --- a/docs/content/styles.md +++ b/docs/content/styles.md @@ -1,3 +1,7 @@ ++++ +Categories = ["Concepts"] ++++ + **Styles** contains explanations of common [[style]] properties. You can also see the API documentation for an [exhaustive list](https://pkg.go.dev/cogentcore.org/core/styles#Style) of style properties. You can experiment with style properties in the [[style playground]]. ## Color @@ -90,6 +94,10 @@ fr.Styler(func(s *styles.Style) { There are many layout properties that customize the positioning and sizing of widgets, typically using [[unit]]s. See the [[layout]] page for a low-level explanation of the layout process. +The standard box model explains the meaning of different size elements: + +![Box model](media/box-model.png) + ### Size You can control the size of a widget through three properties: `Min`, `Max`, and `Grow`. @@ -313,6 +321,19 @@ tx.Styler(func(s *styles.Style) { }) ``` +### Font size + +You can change the size of text: + +```Go +tx := core.NewText(b).SetText("Big text") +tx.Styler(func(s *styles.Style) { + s.Font.Size.Dp(50) +}) +``` + +See also [[icon#icon size]]. + ### Font family You can change the font family category of text: diff --git a/docs/content/toolbar.md b/docs/content/toolbar.md index 2e24accad4..cfadf5c9cd 100644 --- a/docs/content/toolbar.md +++ b/docs/content/toolbar.md @@ -67,7 +67,7 @@ tb.AddOverflowMenu(func(m *core.Scene) { ## Top bar -Toolbars are frequently added in [[doc:core.Body.AddTopBar]]: +Toolbars are frequently added in [[doc:core.Body.AddTopBar]] of a [[scene]]: ```go b.AddTopBar(func(bar *core.Frame) { diff --git a/docs/docs.go b/docs/docs.go index 30ad49f35d..45ff203216 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -239,7 +239,7 @@ func homePage(ctx *htmlcore.Context) bool { initIcon := func(w *core.Icon) *core.Icon { w.Styler(func(s *styles.Style) { - s.Min.Set(units.Dp(256)) + s.IconSize.Set(units.Dp(256)) s.Color = colors.Scheme.Primary.Base }) return w diff --git a/enums/enumgen/generator.go b/enums/enumgen/generator.go index 26405ba79a..87506527c2 100644 --- a/enums/enumgen/generator.go +++ b/enums/enumgen/generator.go @@ -193,7 +193,7 @@ func (g *Generator) Generate() (bool, error) { g.PrefixValueNames(values, typ.Config) - values = SortValues(values) + values = SortValues(values, typ) g.BuildBasicMethods(values, typ) if typ.IsBitFlag { diff --git a/enums/enumgen/type.go b/enums/enumgen/type.go index 5c5a911f79..805c13c51e 100644 --- a/enums/enumgen/type.go +++ b/enums/enumgen/type.go @@ -14,8 +14,10 @@ package enumgen import ( "fmt" "go/ast" + "slices" "sort" "strings" + "unicode" "unicode/utf8" "cogentcore.org/core/base/strcase" @@ -53,7 +55,7 @@ func (v *Value) String() string { // SortValues sorts the values and ensures there // are no duplicates. The input slice is known // to be non-empty. -func SortValues(values []Value) []Value { +func SortValues(values []Value, typ *Type) []Value { // We use stable sort so the lexically first name is chosen for equal elements. sort.Stable(ByValue(values)) // Remove duplicates. Stable sort has put the one we want to print first, @@ -68,7 +70,14 @@ func SortValues(values []Value) []Value { j++ } } - return values[:j] + // for exported types, delete any unexported values + // unexported types are expected to have unexported values. + if len(typ.Name) > 0 && !unicode.IsLower(rune(typ.Name[0])) { + values = slices.DeleteFunc(values[:j], func(v Value) bool { // remove any unexported names + return unicode.IsLower(rune(v.OriginalName[0])) + }) + } + return values } // TrimValueNames removes the prefixes specified diff --git a/events/source.go b/events/source.go index fbd2b364e0..124188a437 100644 --- a/events/source.go +++ b/events/source.go @@ -215,6 +215,12 @@ func (es *Source) WindowResize() { es.Deque.SendFirst(ev) } +func (es *Source) OSOpenFiles(files []string) { + ev := NewOSFiles(OSOpenFiles, files) + ev.Init() + es.Deque.Send(ev) +} + func (es *Source) Custom(data any) { ce := &CustomEvent{} ce.Typ = Custom diff --git a/examples/demo/demo.go b/examples/demo/demo.go index 10f7ed5411..f09283a50a 100644 --- a/examples/demo/demo.go +++ b/examples/demo/demo.go @@ -583,6 +583,20 @@ func dialogs(ts *core.Tabs) { d.RunDialog(td) }) + tdd := core.NewButton(drow).SetText("Editor") + tdd.OnClick(func(e events.Event) { + d := core.NewBody("Editor") + core.NewText(d).SetType(core.TextSupporting).SetText("What is your name?") + ed := textcore.NewEditor(d) + d.AddBottomBar(func(bar *core.Frame) { + d.AddCancel(bar) + d.AddOK(bar).OnClick(func(e events.Event) { + core.MessageSnackbar(td, "Your name is "+ed.Lines.String()) + }) + }) + d.RunDialog(tdd) + }) + fd := core.NewButton(drow).SetText("Full window") u := &core.User{} fd.OnClick(func(e events.Event) { diff --git a/examples/demo/typegen.go b/examples/demo/typegen.go index a2442592f6..c01949e0f4 100644 --- a/examples/demo/typegen.go +++ b/examples/demo/typegen.go @@ -8,7 +8,7 @@ import ( var _ = types.AddType(&types.Type{Name: "main.tableStruct", IDName: "table-struct", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "Icon", Doc: "an icon"}, {Name: "Age", Doc: "an integer field"}, {Name: "Score", Doc: "a float field"}, {Name: "Name", Doc: "a string field"}, {Name: "File", Doc: "a file"}}}) -var _ = types.AddType(&types.Type{Name: "main.inlineStruct", IDName: "inline-struct", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "On", Doc: "click to show next"}, {Name: "ShowMe", Doc: "this is now showing"}, {Name: "Condition", Doc: "a condition"}, {Name: "Condition1", Doc: "if On && Condition == 0"}, {Name: "Condition2", Doc: "if On && Condition <= 1"}, {Name: "Value", Doc: "a value"}}}) +var _ = types.AddType(&types.Type{Name: "main.inlineStruct", IDName: "inline-struct", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "ShowMe", Doc: "this is now showing"}, {Name: "On", Doc: "click to show next"}, {Name: "Condition", Doc: "a condition"}, {Name: "Condition1", Doc: "if On && Condition == 0"}, {Name: "Condition2", Doc: "if On && Condition <= 1"}, {Name: "Value", Doc: "a value"}}}) var _ = types.AddType(&types.Type{Name: "main.testStruct", IDName: "test-struct", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "Enum", Doc: "An enum value"}, {Name: "Name", Doc: "a string"}, {Name: "ShowNext", Doc: "click to show next"}, {Name: "ShowMe", Doc: "this is now showing"}, {Name: "Inline", Doc: "inline struct"}, {Name: "Condition", Doc: "a condition"}, {Name: "Condition1", Doc: "if Condition == 0"}, {Name: "Condition2", Doc: "if Condition >= 0"}, {Name: "Value", Doc: "a value"}, {Name: "Vector", Doc: "a vector"}, {Name: "Table", Doc: "a slice of structs"}, {Name: "List", Doc: "a slice of floats"}, {Name: "File", Doc: "a file"}}}) diff --git a/filetree/node.go b/filetree/node.go index 9b84068c98..e10abb4d0d 100644 --- a/filetree/node.go +++ b/filetree/node.go @@ -73,6 +73,7 @@ func (fn *Node) Init() { fn.ContextMenus = nil // do not include tree fn.AddContextMenu(fn.contextMenu) fn.Styler(func(s *styles.Style) { + s.IconSize.Set(units.Em(1)) fn.styleFromStatus() }) fn.On(events.KeyChord, func(e events.Event) { @@ -126,20 +127,6 @@ func (fn *Node) Init() { fn.This.(Filer).OpenFile() } }) - tree.AddChildInit(fn.Parts, "branch", func(w *core.Switch) { - tree.AddChildInit(w, "stack", func(w *core.Frame) { - f := func(name string) { - tree.AddChildInit(w, name, func(w *core.Icon) { - w.Styler(func(s *styles.Style) { - s.Min.Set(units.Em(1)) - }) - }) - } - f("icon-on") - f("icon-off") - f("icon-indeterminate") - }) - }) tree.AddChildInit(fn.Parts, "text", func(w *core.Text) { w.Styler(func(s *styles.Style) { if fn.IsExec() && !fn.IsDir() { diff --git a/filetree/typegen.go b/filetree/typegen.go index 93c47bee19..394a39f9d4 100644 --- a/filetree/typegen.go +++ b/filetree/typegen.go @@ -12,7 +12,7 @@ import ( var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/filetree.Filer", IDName: "filer", Doc: "Filer is an interface for file tree file actions that all [Node]s satisfy.\nThis allows apps to intervene and apply any additional logic for these actions.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Methods: []types.Method{{Name: "AsFileNode", Doc: "AsFileNode returns the [Node]", Returns: []string{"Node"}}, {Name: "RenameFiles", Doc: "RenameFiles renames any selected files."}, {Name: "DeleteFiles", Doc: "DeleteFiles deletes any selected files."}, {Name: "GetFileInfo", Doc: "GetFileInfo updates the .Info for this file", Returns: []string{"error"}}, {Name: "OpenFile", Doc: "OpenFile opens the file for node. This is called by OpenFilesDefault", Returns: []string{"error"}}}}) -var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/filetree.Node", IDName: "node", Doc: "Node represents a file in the file system, as a [core.Tree] node.\nThe name of the node is the name of the file.\nFolders have children containing further nodes.", Directives: []types.Directive{{Tool: "core", Directive: "embedder"}}, Methods: []types.Method{{Name: "Cut", Doc: "Cut copies the selected files to the clipboard and then deletes them.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Paste", Doc: "Paste inserts files from the clipboard.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "OpenFilesDefault", Doc: "OpenFilesDefault opens selected files with default app for that file type (os defined).\nruns open on Mac, xdg-open on Linux, and start on Windows", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "duplicateFiles", Doc: "duplicateFiles makes a copy of selected files", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "DeleteFiles", Doc: "DeleteFiles deletes any selected files or directories. If any directory is selected,\nall files and subdirectories in that directory are also deleted.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "RenameFiles", Doc: "renames any selected files", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "RenameFile", Doc: "RenameFile renames file to new name", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"newpath"}, Returns: []string{"error"}}, {Name: "newFiles", Doc: "newFiles makes a new file in selected directory", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename", "addToVCS"}}, {Name: "newFile", Doc: "newFile makes a new file in this directory node", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename", "addToVCS"}}, {Name: "newFolders", Doc: "makes a new folder in the given selected directory", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"foldername"}}, {Name: "newFolder", Doc: "newFolder makes a new folder (directory) in this directory node", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"foldername"}}, {Name: "showFileInfo", Doc: "Shows file information about selected file(s)", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "sortBys", Doc: "sortBys determines how to sort the selected files in the directory.\nDefault is alpha by name, optionally can be sorted by modification time.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"modTime"}}, {Name: "openAll", Doc: "openAll opens all directories under this one", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "removeFromExterns", Doc: "removeFromExterns removes file from list of external files", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "addToVCSSelected", Doc: "addToVCSSelected adds selected files to version control system", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "deleteFromVCSSelected", Doc: "deleteFromVCSSelected removes selected files from version control system", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "commitToVCSSelected", Doc: "commitToVCSSelected commits to version control system based on last selected file", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "revertVCSSelected", Doc: "revertVCSSelected removes selected files from version control system", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "diffVCSSelected", Doc: "diffVCSSelected shows the diffs between two versions of selected files, given by the\nrevision specifiers -- if empty, defaults to A = current HEAD, B = current WC file.\n-1, -2 etc also work as universal ways of specifying prior revisions.\nDiffs are shown in a DiffEditorDialog.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"rev_a", "rev_b"}}, {Name: "logVCSSelected", Doc: "logVCSSelected shows the VCS log of commits for selected files.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "blameVCSSelected", Doc: "blameVCSSelected shows the VCS blame report for this file, reporting for each line\nthe revision and author of the last change.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "Tree"}}, Fields: []types.Field{{Name: "Filepath", Doc: "Filepath is the full path to this file."}, {Name: "Info", Doc: "Info is the full standard file info about this file."}, {Name: "FileIsOpen", Doc: "FileIsOpen indicates that this file has been opened, indicated by Italics."}, {Name: "DirRepo", Doc: "DirRepo is the version control system repository for this directory,\nonly non-nil if this is the highest-level directory in the tree under vcs control."}, {Name: "repoFiles", Doc: "repoFiles has the version control system repository file status,\nproviding a much faster way to get file status, vs. the repo.Status\ncall which is exceptionally slow."}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/filetree.Node", IDName: "node", Doc: "Node represents a file in the file system, as a [core.Tree] node.\nThe name of the node is the name of the file.\nFolders have children containing further nodes.", Directives: []types.Directive{{Tool: "core", Directive: "embedder"}}, Methods: []types.Method{{Name: "Cut", Doc: "Cut copies the selected files to the clipboard and then deletes them.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Paste", Doc: "Paste inserts files from the clipboard.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "OpenFilesDefault", Doc: "OpenFilesDefault opens selected files with default app for that file type (os defined).\nruns open on Mac, xdg-open on Linux, and start on Windows", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "duplicateFiles", Doc: "duplicateFiles makes a copy of selected files", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "DeleteFiles", Doc: "DeleteFiles deletes any selected files or directories. If any directory is selected,\nall files and subdirectories in that directory are also deleted.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "RenameFiles", Doc: "renames any selected files", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "RenameFile", Doc: "RenameFile renames file to new name", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"newpath"}, Returns: []string{"error"}}, {Name: "newFiles", Doc: "newFiles makes a new file in selected directory", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename", "addToVCS"}}, {Name: "newFile", Doc: "newFile makes a new file in this directory node", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename", "addToVCS"}}, {Name: "newFolders", Doc: "makes a new folder in the given selected directory", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"foldername"}}, {Name: "newFolder", Doc: "newFolder makes a new folder (directory) in this directory node", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"foldername"}}, {Name: "showFileInfo", Doc: "Shows file information about selected file(s)", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "sortBys", Doc: "sortBys determines how to sort the selected files in the directory.\nDefault is alpha by name, optionally can be sorted by modification time.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"modTime"}}, {Name: "openAll", Doc: "openAll opens all directories under this one", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "removeFromExterns", Doc: "removeFromExterns removes file from list of external files", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "addToVCSSelected", Doc: "addToVCSSelected adds selected files to version control system", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "deleteFromVCSSelected", Doc: "deleteFromVCSSelected removes selected files from version control system", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "commitToVCSSelected", Doc: "commitToVCSSelected commits to version control system based on last selected file", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "revertVCSSelected", Doc: "revertVCSSelected removes selected files from version control system", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "diffVCSSelected", Doc: "diffVCSSelected shows the diffs between two versions of selected files, given by the\nrevision specifiers -- if empty, defaults to A = current HEAD, B = current WC file.\n-1, -2 etc also work as universal ways of specifying prior revisions.\nDiffs are shown in a DiffEditorDialog.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"rev_a", "rev_b"}}, {Name: "logVCSSelected", Doc: "logVCSSelected shows the VCS log of commits for selected files.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "blameVCSSelected", Doc: "blameVCSSelected shows the VCS blame report for this file, reporting for each line\nthe revision and author of the last change.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "Tree"}}, Fields: []types.Field{{Name: "Filepath", Doc: "Filepath is the full path to this file."}, {Name: "Info", Doc: "Info is the full standard file info about this file."}, {Name: "FileIsOpen", Doc: "FileIsOpen indicates that this file has been opened, indicated by Italics."}, {Name: "DirRepo", Doc: "DirRepo is the version control system repository for this directory,\nonly non-nil if this is the highest-level directory in the tree under vcs control."}}}) // NewNode returns a new [Node] with the given optional parent: // Node represents a file in the file system, as a [core.Tree] node. diff --git a/math32/geom2d.go b/math32/geom2d.go index 886682931d..86c028726c 100644 --- a/math32/geom2d.go +++ b/math32/geom2d.go @@ -21,12 +21,17 @@ func (gm *Geom2DInt) Bounds() image.Rectangle { return image.Rect(gm.Pos.X, gm.Pos.Y, gm.Pos.X+gm.Size.X, gm.Pos.Y+gm.Size.Y) } -// SizeRect converts geom to rect version of size at 0 pos +// Box2 returns bounds as a [Box2]. +func (gm *Geom2DInt) Box2() Box2 { + return B2FromRect(gm.Bounds()) +} + +// SizeRect converts geom to rect version of size at 0 pos. func (gm *Geom2DInt) SizeRect() image.Rectangle { return image.Rect(0, 0, gm.Size.X, gm.Size.Y) } -// SetRect sets values from image.Rectangle +// SetRect sets values from image.Rectangle. func (gm *Geom2DInt) SetRect(r image.Rectangle) { gm.Pos = r.Min gm.Size = r.Size() diff --git a/math32/matrix2.go b/math32/matrix2.go index 4e491b1249..34a7e456fa 100644 --- a/math32/matrix2.go +++ b/math32/matrix2.go @@ -148,31 +148,6 @@ func (a Matrix2) MulVector2AsPoint(v Vector2) Vector2 { return Vec2(tx, ty) } -// MulVector2AsPointCenter multiplies the Vector2 as a point relative to given center-point -// including adding translations. -func (a Matrix2) MulVector2AsPointCenter(v, ctr Vector2) Vector2 { - rel := v.Sub(ctr) - tx := ctr.X + a.XX*rel.X + a.XY*rel.Y + a.X0 - ty := ctr.Y + a.YX*rel.X + a.YY*rel.Y + a.Y0 - return Vec2(tx, ty) -} - -// MulCenter multiplies the Matrix2, first subtracting given translation center point -// from the translation components, and then adding it back in. -func (a Matrix2) MulCenter(b Matrix2, ctr Vector2) Matrix2 { - a.X0 -= ctr.X - a.Y0 -= ctr.Y - rv := a.Mul(b) - rv.X0 += ctr.X - rv.Y0 += ctr.Y - return rv -} - -// SetMulCenter sets the matrix to the result of [Matrix2.MulCenter]. -func (a *Matrix2) SetMulCenter(b Matrix2, ctr Vector2) { - *a = a.MulCenter(b, ctr) -} - // MulFixedAsPoint multiplies the fixed point as a point, including adding translations. func (a Matrix2) MulFixedAsPoint(fp fixed.Point26_6) fixed.Point26_6 { x := fixed.Int26_6((float32(fp.X)*a.XX + float32(fp.Y)*a.XY) + a.X0*32) @@ -221,11 +196,6 @@ func (a Matrix2) ExtractRot() float32 { // ExtractXYScale extracts the X and Y scale factors after undoing any // rotation present -- i.e., in the original X, Y coordinates func (a Matrix2) ExtractScale() (scx, scy float32) { - // rot := a.ExtractRot() - // tx := a.Rotate(-rot) - // scxv := tx.MulVector2AsVector(Vec2(1, 0)) - // scyv := tx.MulVector2AsVector(Vec2(0, 1)) - // return scxv.X, scyv.Y _, _, _, scx, scy, _ = a.Decompose() return } diff --git a/paint/ppath/scanner.go b/paint/ppath/scanner.go index 91b536b855..6b3437c407 100644 --- a/paint/ppath/scanner.go +++ b/paint/ppath/scanner.go @@ -24,7 +24,7 @@ func (p Path) ReverseScanner() ReverseScanner { // Scanner scans the path. type Scanner struct { p Path - i int + i int // i is at the end of the current command } // Scan scans a new path segment and should be called before the other methods. @@ -41,6 +41,11 @@ func (s *Scanner) Cmd() float32 { return s.p[s.i] } +// Index returns the index in path of the current command. +func (s *Scanner) Index() int { + return (s.i - CmdLen(s.p[s.i])) + 1 +} + // Values returns the current path segment values. func (s *Scanner) Values() []float32 { return s.p[s.i-CmdLen(s.p[s.i])+2 : s.i] diff --git a/paint/ppath/transform.go b/paint/ppath/transform.go index 0417e38ee5..401921b51d 100644 --- a/paint/ppath/transform.go +++ b/paint/ppath/transform.go @@ -8,6 +8,8 @@ package ppath import ( + "fmt" + "cogentcore.org/core/math32" ) @@ -19,6 +21,10 @@ func (p Path) Transform(m math32.Matrix2) Path { cmd := p[i] switch cmd { case MoveTo, LineTo, Close: + if i+2 >= len(p) { + fmt.Println("path length error:", len(p), i, p) + return p + } end := m.MulVector2AsPoint(math32.Vec2(p[i+1], p[i+2])) p[i+1] = end.X p[i+2] = end.Y diff --git a/paint/renderers/rasterx/renderer.go b/paint/renderers/rasterx/renderer.go index 8df5faff84..b61c3aacd3 100644 --- a/paint/renderers/rasterx/renderer.go +++ b/paint/renderers/rasterx/renderer.go @@ -188,8 +188,7 @@ func (rs *Renderer) StrokeWidth(pt *render.Path) float32 { return dw } sc := MeanScale(pt.Context.Transform) - lw := math32.Max(sc*dw, sty.Stroke.MinWidth.Dots) - return lw + return sc * dw } func capfunc(st ppath.Caps) CapFunc { diff --git a/paint/renderers/rasterx/scan/scan.go b/paint/renderers/rasterx/scan/scan.go index 15d3ec69fb..79e3b2f88d 100644 --- a/paint/renderers/rasterx/scan/scan.go +++ b/paint/renderers/rasterx/scan/scan.go @@ -218,17 +218,19 @@ func (s *Scanner) Scan(yi int, x0, y0f, x1, y1f fixed.Int26_6) { fullRem += q } yRem -= q - for xi != x1i { - yDelta = fullDelta - yRem += fullRem - if yRem >= 0 { - yDelta++ - yRem -= q + if (xiDelta > 0 && xi < x1i) || (xiDelta < 0 && xi > x1i) { + for xi != x1i { + yDelta = fullDelta + yRem += fullRem + if yRem >= 0 { + yDelta++ + yRem -= q + } + s.Area += int(64 * yDelta) + s.Cover += int(yDelta) + xi, y = xi+xiDelta, y+yDelta + s.SetCell(xi, yi) } - s.Area += int(64 * yDelta) - s.Cover += int(yDelta) - xi, y = xi+xiDelta, y+yDelta - s.SetCell(xi, yi) } } // Do the last cell. @@ -285,11 +287,13 @@ func (s *Scanner) Line(b fixed.Point26_6) { // Do all the intermediate pixels. dcover = int(edge1 - edge0) darea = int(x0fTimes2 * dcover) - for yi != y1i { - s.Area += darea - s.Cover += dcover - yi += yiDelta - s.SetCell(x0i, yi) + if (yiDelta > 0 && yi < y1i) || (yiDelta < 0 && yi > y1i) { + for yi != y1i { + s.Area += darea + s.Cover += dcover + yi += yiDelta + s.SetCell(x0i, yi) + } } // Do the last pixel. dcover = int(y1f - edge0) @@ -331,16 +335,18 @@ func (s *Scanner) Line(b fixed.Point26_6) { fullRem += q } xRem -= q - for yi != y1i { - xDelta = fullDelta - xRem += fullRem - if xRem >= 0 { - xDelta++ - xRem -= q + if (yiDelta > 0 && yi < y1i) || (yiDelta < 0 && yi > y1i) { + for yi != y1i { + xDelta = fullDelta + xRem += fullRem + if xRem >= 0 { + xDelta++ + xRem -= q + } + s.Scan(yi, x, edge0, x+xDelta, edge1) + x, yi = x+xDelta, yi+yiDelta + s.SetCell(int(x)/64, yi) } - s.Scan(yi, x, edge0, x+xDelta, edge1) - x, yi = x+xDelta, yi+yiDelta - s.SetCell(int(x)/64, yi) } } // Do the last scanline. diff --git a/paint/renderers/rasterx/stroke.go b/paint/renderers/rasterx/stroke.go index 9618b9bd0e..70831de833 100644 --- a/paint/renderers/rasterx/stroke.go +++ b/paint/renderers/rasterx/stroke.go @@ -265,6 +265,9 @@ func StrokeArc(p Adder, a, s1, s2 fixed.Point26_6, clockwise bool, trimStart, // Joiner is called when two segments of a stroke are joined. it is exposed // so that if can be wrapped to generate callbacks for the join points. func (r *Stroker) Joiner(p C2Point) { + if p.P.X < 0 || p.P.Y < 0 { + return + } crossProd := p.LNorm.X*p.TNorm.Y - p.TNorm.X*p.LNorm.Y // stroke bottom edge, with the reverse of p r.StrokeEdge(C2Point{P: p.P, TNorm: Invert(p.LNorm), LNorm: Invert(p.TNorm), diff --git a/styles/font.go b/styles/font.go index cc4a4f0f54..4f7b701186 100644 --- a/styles/font.go +++ b/styles/font.go @@ -5,6 +5,7 @@ package styles import ( + "image" "log/slog" "cogentcore.org/core/colors" @@ -22,7 +23,7 @@ type Font struct { //types:add // Size of font to render (inherited). // Converted to points when getting font to use. - Size units.Value + Size units.Value `step:"1"` // Family indicates the generic family of typeface to use, where the // specific named values to use for each are provided in the [Settings], @@ -111,19 +112,41 @@ func (fs *Font) SetRich(sty *rich.Style) { sty.Decoration = fs.Decoration } -// SetRichText sets the rich.Style and text.Style properties from the style props. -func (s *Style) SetRichText(sty *rich.Style, tsty *text.Style) { - s.Font.SetRich(sty) - s.Text.SetText(tsty) - tsty.FontSize = s.Font.Size - tsty.CustomFont = s.Font.CustomFont - if s.Color != nil { - clr := colors.ApplyOpacity(colors.ToUniform(s.Color), s.Opacity) - tsty.Color = clr +// SetFromRich sets from the rich.Style +func (fs *Font) SetFromRich(sty *rich.Style) { + fs.Family = sty.Family + fs.Slant = sty.Slant + fs.Weight = sty.Weight + fs.Stretch = sty.Stretch + fs.Decoration = sty.Decoration +} + +// SetRichText sets the rich.Style and text.Style properties from style +// [Font] and [Text] styles +func SetRichText(sty *rich.Style, tsty *text.Style, font *Font, txt *Text, clr image.Image, opacity float32) { + font.SetRich(sty) + txt.SetText(tsty) + tsty.FontSize = font.Size + tsty.CustomFont = font.CustomFont + if clr != nil { + tsty.Color = colors.ApplyOpacity(colors.ToUniform(clr), opacity) } // note: no default background color here } +// SetFromRichText sets the style [Font] and [Text] styles from rich.Style and text.Style. +func SetFromRichText(sty *rich.Style, tsty *text.Style, font *Font, txt *Text) { + font.SetFromRich(sty) + txt.SetFromText(tsty) + font.Size = tsty.FontSize + font.CustomFont = tsty.CustomFont +} + +// SetRichText sets the rich.Style and text.Style properties from the style props. +func (s *Style) SetRichText(sty *rich.Style, tsty *text.Style) { + SetRichText(sty, tsty, &s.Font, &s.Text, s.Color, s.Opacity) +} + // NewRichText sets the rich.Style and text.Style properties from the style props. func (s *Style) NewRichText() (sty *rich.Style, tsty *text.Style) { sty = rich.NewStyle() diff --git a/styles/paint_props.go b/styles/paint_props.go index aa660e337e..dd1a220c59 100644 --- a/styles/paint_props.go +++ b/styles/paint_props.go @@ -121,8 +121,6 @@ var styleStrokeFuncs = map[string]styleprops.Func{ func(obj *Stroke) *float32 { return &(obj.Opacity) }), "stroke-width": styleprops.Units(units.Dp(1), func(obj *Stroke) *units.Value { return &(obj.Width) }), - "stroke-min-width": styleprops.Units(units.Dp(1), - func(obj *Stroke) *units.Value { return &(obj.MinWidth) }), "stroke-dasharray": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Stroke) if inh, init := styleprops.InhInit(val, parent); inh || init { @@ -256,15 +254,12 @@ func (pc *Stroke) toProperties(p map[string]any) { if pc.Width.Unit != units.UnitDp || pc.Width.Value != 1 { p["stroke-width"] = pc.Width.StringCSS() } - if pc.MinWidth.Unit != units.UnitDp || pc.MinWidth.Value != 1 { - p["stroke-min-width"] = pc.MinWidth.StringCSS() - } // todo: dashes if pc.Cap != ppath.CapButt { p["stroke-linecap"] = pc.Cap.String() } if pc.Join != ppath.JoinMiter { - p["stroke-linecap"] = pc.Cap.String() + p["stroke-linejoin"] = pc.Join.String() } } diff --git a/styles/path.go b/styles/path.go index 7317796b64..6cbc1f72ba 100644 --- a/styles/path.go +++ b/styles/path.go @@ -26,7 +26,7 @@ type Path struct { //types:add Display bool // Stroke (line drawing) parameters. - Stroke Stroke + Stroke Stroke `display:"add-fields"` // Fill (region filling) parameters. Fill Fill @@ -35,7 +35,7 @@ type Path struct { //types:add Opacity float32 // Transform has our additions to the transform stack. - Transform math32.Matrix2 + Transform math32.Matrix2 `display:"inline"` // VectorEffect has various rendering special effects settings. VectorEffect ppath.VectorEffects @@ -149,13 +149,6 @@ type Stroke struct { // line width Width units.Value - // MinWidth is the minimum line width used for rendering. - // If width is > 0, then this is the smallest line width. - // This value is NOT subject to transforms so is in absolute - // dot values, and is ignored if vector-effects, non-scaling-stroke - // is used. This is an extension of the SVG / CSS standard - MinWidth units.Value - // Dashes are the dashes of the stroke. Each pair of values specifies // the amount to paint and then the amount to skip. Dashes []float32 @@ -178,7 +171,6 @@ func (ss *Stroke) Defaults() { // stroking is off by default in svg ss.Color = nil ss.Width.Dp(1) - ss.MinWidth.Dot(.5) ss.Cap = ppath.CapButt ss.Join = ppath.JoinMiter ss.MiterLimit = 10.0 @@ -188,7 +180,6 @@ func (ss *Stroke) Defaults() { // ToDots runs ToDots on unit values, to compile down to raw pixels func (ss *Stroke) ToDots(uc *units.Context) { ss.Width.ToDots(uc) - ss.MinWidth.ToDots(uc) } // ApplyBorderStyle applies the given border style to the stroke style. diff --git a/styles/states/enumgen.go b/styles/states/enumgen.go index 7b933edd36..ea292869c2 100644 --- a/styles/states/enumgen.go +++ b/styles/states/enumgen.go @@ -13,7 +13,7 @@ const StatesN States = 15 var _StatesValueMap = map[string]States{`Invisible`: 0, `Disabled`: 1, `ReadOnly`: 2, `Selected`: 3, `Active`: 4, `Dragging`: 5, `Sliding`: 6, `Focused`: 7, `Attended`: 8, `Checked`: 9, `Indeterminate`: 10, `Hovered`: 11, `LongHovered`: 12, `LongPressed`: 13, `DragHovered`: 14} -var _StatesDescMap = map[States]string{0: `Invisible elements are not displayable, and thus do not present a target for GUI events. It is identical to CSS display:none. It is often used for elements such as tabs to hide elements in tabs that are not open. Elements can be made visible by toggling this flag and thus in general should be constructed and styled, but a new layout step must generally be taken after visibility status has changed. See also [cogentcore.org/core/core.WidgetBase.IsDisplayable].`, 1: `Disabled elements cannot be interacted with or selected, but do display.`, 2: `ReadOnly elements cannot be changed, but can be selected. A text input must not be ReadOnly for entering text. A button can be pressed while ReadOnly -- if not ReadOnly then the label on the button can be edited, for example.`, 3: `Selected elements have been marked for clipboard or other such actions.`, 4: `Active elements are currently being interacted with, usually involving a mouse button being pressed in the element. A text field will be active while being clicked on, and this can also result in a [Focused] state. If further movement happens, an element can also end up being Dragged or Sliding.`, 5: `Dragging means this element is currently being dragged by the mouse (i.e., a MouseDown event followed by MouseMove), as part of a drag-n-drop sequence.`, 6: `Sliding means this element is currently being manipulated via mouse to change the slider state, which will continue until the mouse is released, even if it goes off the element. It should also still be [Active].`, 7: `Focused elements receive keyboard input. Only one element can be Focused at a time.`, 8: `Attended elements are the last Activatable elements to be clicked on. Only one element can be Attended at a time. The main effect of Attended is on scrolling events: see [abilities.ScrollableUnattended]`, 9: `Checked is for check boxes or radio buttons or other similar state.`, 10: `Indeterminate indicates that the true state of an item is unknown. For example, [Checked] state items may be in an uncertain state if they represent other checked items, some of which are checked and some of which are not.`, 11: `Hovered indicates that a mouse pointer has entered the space over an element, but it is not [Active] (nor [DragHovered]).`, 12: `LongHovered indicates a Hover event that persists without significant movement for a minimum period of time (e.g., 500 msec), which typically triggers a tooltip popup.`, 13: `LongPressed indicates a MouseDown event that persists without significant movement for a minimum period of time (e.g., 500 msec), which typically triggers a tooltip and/or context menu popup.`, 14: `DragHovered indicates that a mouse pointer has entered the space over an element during a drag-n-drop sequence. This makes it a candidate for a potential drop target.`} +var _StatesDescMap = map[States]string{0: `Invisible elements are not displayable, and thus do not present a target for GUI events. It is identical to CSS display:none. It is often used for elements such as tabs to hide elements in tabs that are not open. Elements can be made visible by toggling this flag and thus in general should be constructed and styled, but a new layout step must generally be taken after visibility status has changed. See also [cogentcore.org/core/core.WidgetBase.IsDisplayable].`, 1: `Disabled elements cannot be interacted with or selected, but do display.`, 2: `ReadOnly elements cannot be changed, but can be selected. A text input must not be ReadOnly for entering text. A button can be pressed while ReadOnly -- if not ReadOnly then the label on the button can be edited, for example.`, 3: `Selected elements have been marked for clipboard or other such actions.`, 4: `Active elements are currently being interacted with, usually involving a mouse button being pressed in the element. A text field will be active while being clicked on, and this can also result in a [Focused] state. If further movement happens, an element can also end up being Dragged or Sliding.`, 5: `Dragging means this element is currently being dragged by the mouse (i.e., a MouseDown event followed by MouseMove), as part of a drag-n-drop sequence.`, 6: `Sliding means this element is currently being manipulated via mouse to change the slider state, which will continue until the mouse is released, even if it goes off the element. It should also still be [Active].`, 7: `The current Focused element receives keyboard input. Only one element can be Focused at a time.`, 8: `Attended is the last Pressable element to be clicked on. Only one element can be Attended at a time. The main effect of Attended is on scrolling events: see [abilities.ScrollableUnattended]`, 9: `Checked is for check boxes or radio buttons or other similar state.`, 10: `Indeterminate indicates that the true state of an item is unknown. For example, [Checked] state items may be in an uncertain state if they represent other checked items, some of which are checked and some of which are not.`, 11: `Hovered indicates that a mouse pointer has entered the space over an element, but it is not [Active] (nor [DragHovered]).`, 12: `LongHovered indicates a Hover event that persists without significant movement for a minimum period of time (e.g., 500 msec), which typically triggers a tooltip popup.`, 13: `LongPressed indicates a MouseDown event that persists without significant movement for a minimum period of time (e.g., 500 msec), which typically triggers a tooltip and/or context menu popup.`, 14: `DragHovered indicates that a mouse pointer has entered the space over an element during a drag-n-drop sequence. This makes it a candidate for a potential drop target.`} var _StatesMap = map[States]string{0: `Invisible`, 1: `Disabled`, 2: `ReadOnly`, 3: `Selected`, 4: `Active`, 5: `Dragging`, 6: `Sliding`, 7: `Focused`, 8: `Attended`, 9: `Checked`, 10: `Indeterminate`, 11: `Hovered`, 12: `LongHovered`, 13: `LongPressed`, 14: `DragHovered`} diff --git a/styles/style.go b/styles/style.go index 8b1686524b..4f34789fa9 100644 --- a/styles/style.go +++ b/styles/style.go @@ -213,6 +213,10 @@ type Style struct { //types:add // to [DefaultScrollbarWidth], and it is inherited. ScrollbarWidth units.Value + // IconSize specifies the size of icons in this widget, if it has icons. + // The default icon size is 1em for X and Y. This is inherited. + IconSize units.XY `display:"inline"` + // Font styling parameters applicable to individual spans of text. Font Font @@ -232,6 +236,7 @@ func (s *Style) Defaults() { s.Opacity = 1 s.RenderBox = true s.FillMargin = true + s.IconSize.Set(units.Em(1)) s.Font.Defaults() s.Text.Defaults() } @@ -341,6 +346,7 @@ func (s *Style) InheritFields(parent *Style) { s.Color = parent.Color s.Opacity = parent.Opacity s.ScrollbarWidth = parent.ScrollbarWidth + s.IconSize = parent.IconSize s.Font.InheritFields(&parent.Font) s.Text.InheritFields(&parent.Text) } diff --git a/styles/styleprops/xml.go b/styles/styleprops/xml.go index ad88bf5b1c..cca6deed16 100644 --- a/styles/styleprops/xml.go +++ b/styles/styleprops/xml.go @@ -17,9 +17,13 @@ func FromXMLString(style string, properties map[string]any) { st := strings.Split(style, ";") for _, s := range st { kv := strings.Split(s, ":") - if len(kv) >= 2 { - k := strings.TrimSpace(strings.ToLower(kv[0])) - v := strings.TrimSpace(kv[1]) + n := len(kv) + if n >= 2 { + k := strings.TrimSpace(strings.ToLower(kv[n-2])) + if n == 3 { // prefixed name + k = strings.TrimSpace(strings.ToLower(kv[0])) + ":" + k + } + v := strings.TrimSpace(kv[n-1]) properties[k] = v } } diff --git a/styles/text.go b/styles/text.go index 53b0211fff..96331e462d 100644 --- a/styles/text.go +++ b/styles/text.go @@ -93,6 +93,18 @@ func (ts *Text) SetText(tsty *text.Style) { tsty.HighlightColor = ts.HighlightColor } +// SetFromText sets from the given [text.Style]. +func (ts *Text) SetFromText(tsty *text.Style) { + ts.Align = tsty.Align + ts.AlignV = tsty.AlignV + ts.LineHeight = tsty.LineHeight + ts.WhiteSpace = tsty.WhiteSpace + ts.Direction = tsty.Direction + ts.TabSize = tsty.TabSize + ts.SelectColor = tsty.SelectColor + ts.HighlightColor = tsty.HighlightColor +} + // LineHeightDots returns the effective line height in dots (actual pixels) // as FontHeight * LineHeight func (s *Style) LineHeightDots() float32 { diff --git a/styles/typegen.go b/styles/typegen.go index de1454017d..0d6a77c4fd 100644 --- a/styles/typegen.go +++ b/styles/typegen.go @@ -18,8 +18,8 @@ var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/styles.Paint", IDNa var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/styles.Path", IDName: "path", Doc: "Path provides the styling parameters for path-level rendering:\nStroke and Fill.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "Off", Doc: "Off indicates that node and everything below it are off, non-rendering.\nThis is auto-updated based on other settings."}, {Name: "Display", Doc: "Display is the user-settable flag that determines if this item\nshould be displayed."}, {Name: "Stroke", Doc: "Stroke (line drawing) parameters."}, {Name: "Fill", Doc: "Fill (region filling) parameters."}, {Name: "Opacity", Doc: "Opacity is a global transparency alpha factor that applies to stroke and fill."}, {Name: "Transform", Doc: "Transform has our additions to the transform stack."}, {Name: "VectorEffect", Doc: "VectorEffect has various rendering special effects settings."}, {Name: "UnitContext", Doc: "UnitContext has parameters necessary for determining unit sizes."}, {Name: "StyleSet", Doc: "StyleSet indicates if the styles already been set."}, {Name: "PropertiesNil"}, {Name: "dotsSet"}, {Name: "lastUnCtxt"}}}) -var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/styles.Style", IDName: "style", Doc: "Style contains all of the style properties used for GUI widgets.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "State", Doc: "State holds style-relevant state flags, for convenient styling access,\ngiven that styles typically depend on element states."}, {Name: "Abilities", Doc: "Abilities specifies the abilities of this element, which determine\nwhich kinds of states the element can express.\nThis is used by the system/events system. Putting this info next\nto the State info makes it easy to configure and manage."}, {Name: "Cursor", Doc: "the cursor to switch to upon hovering over the element (inherited)"}, {Name: "Padding", Doc: "Padding is the transparent space around central content of box,\nwhich is _included_ in the size of the standard box rendering."}, {Name: "Margin", Doc: "Margin is the outer-most transparent space around box element,\nwhich is _excluded_ from standard box rendering."}, {Name: "Display", Doc: "Display controls how items are displayed, in terms of layout"}, {Name: "Direction", Doc: "Direction specifies the way in which elements are laid out, or\nthe dimension on which an element is longer / travels in."}, {Name: "Wrap", Doc: "Wrap causes elements to wrap around in the CrossAxis dimension\nto fit within sizing constraints."}, {Name: "Justify", Doc: "Justify specifies the distribution of elements along the main axis,\ni.e., the same as Direction, for Flex Display. For Grid, the main axis is\ngiven by the writing direction (e.g., Row-wise for latin based languages)."}, {Name: "Align", Doc: "Align specifies the cross-axis alignment of elements, orthogonal to the\nmain Direction axis. For Grid, the cross-axis is orthogonal to the\nwriting direction (e.g., Column-wise for latin based languages)."}, {Name: "Min", Doc: "Min is the minimum size of the actual content, exclusive of additional space\nfrom padding, border, margin; 0 = default is sum of Min for all content\n(which _includes_ space for all sub-elements).\nThis is equivalent to the Basis for the CSS flex styling model."}, {Name: "Max", Doc: "Max is the maximum size of the actual content, exclusive of additional space\nfrom padding, border, margin; 0 = default provides no Max size constraint"}, {Name: "Grow", Doc: "Grow is the proportional amount that the element can grow (stretch)\nif there is more space available. 0 = default = no growth.\nExtra available space is allocated as: Grow / sum (all Grow).\nImportant: grow elements absorb available space and thus are not\nsubject to alignment (Center, End)."}, {Name: "GrowWrap", Doc: "GrowWrap is a special case for Text elements where it grows initially\nin the horizontal axis to allow for longer, word wrapped text to fill\nthe available space, but then it does not grow thereafter, so that alignment\noperations still work (Grow elements do not align because they absorb all\navailable space). Do NOT set this for non-Text elements."}, {Name: "RenderBox", Doc: "RenderBox determines whether to render the standard box model for the element.\nThis is typically necessary for most elements and helps prevent text, border,\nand box shadow from rendering over themselves. Therefore, it should be kept at\nits default value of true in most circumstances, but it can be set to false\nwhen the element is fully managed by something that is guaranteed to render the\nappropriate background color and/or border for the element."}, {Name: "FillMargin", Doc: "FillMargin determines is whether to fill the margin with\nthe surrounding background color before rendering the element itself.\nThis is typically necessary to prevent text, border, and box shadow from\nrendering over themselves. Therefore, it should be kept at its default value\nof true in most circumstances, but it can be set to false when the element\nis fully managed by something that is guaranteed to render the\nappropriate background color for the element. It is irrelevant if RenderBox\nis false."}, {Name: "Overflow", Doc: "Overflow determines how to handle overflowing content in a layout.\nDefault is OverflowVisible. Set to OverflowAuto to enable scrollbars."}, {Name: "Gap", Doc: "For layouts, extra space added between elements in the layout."}, {Name: "Columns", Doc: "For grid layouts, the number of columns to use.\nIf > 0, number of rows is computed as N elements / Columns.\nUsed as a constraint in layout if individual elements\ndo not specify their row, column positions"}, {Name: "ObjectFit", Doc: "If this object is a replaced object (image, video, etc)\nor has a background image, ObjectFit specifies the way\nin which the replaced object should be fit into the element."}, {Name: "ObjectPosition", Doc: "If this object is a replaced object (image, video, etc)\nor has a background image, ObjectPosition specifies the\nX,Y position of the object within the space allocated for\nthe object (see ObjectFit)."}, {Name: "Border", Doc: "Border is a rendered border around the element."}, {Name: "MaxBorder", Doc: "MaxBorder is the largest border that will ever be rendered\naround the element, the size of which is used for computing\nthe effective margin to allocate for the element."}, {Name: "BoxShadow", Doc: "BoxShadow is the box shadows to render around box (can have multiple)"}, {Name: "MaxBoxShadow", Doc: "MaxBoxShadow contains the largest shadows that will ever be rendered\naround the element, the size of which are used for computing the\neffective margin to allocate for the element."}, {Name: "Color", Doc: "Color specifies the text / content color, and it is inherited."}, {Name: "Background", Doc: "Background specifies the background of the element. It is not inherited,\nand it is nil (transparent) by default."}, {Name: "Opacity", Doc: "alpha value between 0 and 1 to apply to the foreground and background\nof this element and all of its children."}, {Name: "StateLayer", Doc: "StateLayer, if above zero, indicates to create a state layer over\nthe element with this much opacity (on a scale of 0-1) and the\ncolor Color (or StateColor if it defined). It is automatically\nset based on State, but can be overridden in stylers."}, {Name: "StateColor", Doc: "StateColor, if not nil, is the color to use for the StateLayer\ninstead of Color. If you want to disable state layers\nfor an element, do not use this; instead, set StateLayer to 0."}, {Name: "ActualBackground", Doc: "ActualBackground is the computed actual background rendered for the element,\ntaking into account its Background, Opacity, StateLayer, and parent\nActualBackground. It is automatically computed and should not be set manually."}, {Name: "VirtualKeyboard", Doc: "VirtualKeyboard is the virtual keyboard to display, if any,\non mobile platforms when this element is focused. It is not\nused if the element is read only."}, {Name: "Pos", Doc: "Pos is used for the position of the widget if the parent frame\nhas [Style.Display] = [Custom]."}, {Name: "ZIndex", Doc: "ordering factor for rendering depth -- lower numbers rendered first.\nSort children according to this factor"}, {Name: "Row", Doc: "specifies the row that this element should appear within a grid layout"}, {Name: "Col", Doc: "specifies the column that this element should appear within a grid layout"}, {Name: "RowSpan", Doc: "specifies the number of sequential rows that this element should occupy\nwithin a grid layout (todo: not currently supported)"}, {Name: "ColSpan", Doc: "specifies the number of sequential columns that this element should occupy\nwithin a grid layout"}, {Name: "ScrollbarWidth", Doc: "ScrollbarWidth is the width of layout scrollbars. It defaults\nto [DefaultScrollbarWidth], and it is inherited."}, {Name: "Font", Doc: "Font styling parameters applicable to individual spans of text."}, {Name: "Text", Doc: "Text styling parameters applicable to a paragraph of text."}, {Name: "UnitContext", Doc: "unit context: parameters necessary for anchoring relative units"}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/styles.Style", IDName: "style", Doc: "Style contains all of the style properties used for GUI widgets.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "State", Doc: "State holds style-relevant state flags, for convenient styling access,\ngiven that styles typically depend on element states."}, {Name: "Abilities", Doc: "Abilities specifies the abilities of this element, which determine\nwhich kinds of states the element can express.\nThis is used by the system/events system. Putting this info next\nto the State info makes it easy to configure and manage."}, {Name: "Cursor", Doc: "the cursor to switch to upon hovering over the element (inherited)"}, {Name: "Padding", Doc: "Padding is the transparent space around central content of box,\nwhich is _included_ in the size of the standard box rendering."}, {Name: "Margin", Doc: "Margin is the outer-most transparent space around box element,\nwhich is _excluded_ from standard box rendering."}, {Name: "Display", Doc: "Display controls how items are displayed, in terms of layout"}, {Name: "Direction", Doc: "Direction specifies the way in which elements are laid out, or\nthe dimension on which an element is longer / travels in."}, {Name: "Wrap", Doc: "Wrap causes elements to wrap around in the CrossAxis dimension\nto fit within sizing constraints."}, {Name: "Justify", Doc: "Justify specifies the distribution of elements along the main axis,\ni.e., the same as Direction, for Flex Display. For Grid, the main axis is\ngiven by the writing direction (e.g., Row-wise for latin based languages)."}, {Name: "Align", Doc: "Align specifies the cross-axis alignment of elements, orthogonal to the\nmain Direction axis. For Grid, the cross-axis is orthogonal to the\nwriting direction (e.g., Column-wise for latin based languages)."}, {Name: "Min", Doc: "Min is the minimum size of the actual content, exclusive of additional space\nfrom padding, border, margin; 0 = default is sum of Min for all content\n(which _includes_ space for all sub-elements).\nThis is equivalent to the Basis for the CSS flex styling model."}, {Name: "Max", Doc: "Max is the maximum size of the actual content, exclusive of additional space\nfrom padding, border, margin; 0 = default provides no Max size constraint"}, {Name: "Grow", Doc: "Grow is the proportional amount that the element can grow (stretch)\nif there is more space available. 0 = default = no growth.\nExtra available space is allocated as: Grow / sum (all Grow).\nImportant: grow elements absorb available space and thus are not\nsubject to alignment (Center, End)."}, {Name: "GrowWrap", Doc: "GrowWrap is a special case for Text elements where it grows initially\nin the horizontal axis to allow for longer, word wrapped text to fill\nthe available space, but then it does not grow thereafter, so that alignment\noperations still work (Grow elements do not align because they absorb all\navailable space). Do NOT set this for non-Text elements."}, {Name: "RenderBox", Doc: "RenderBox determines whether to render the standard box model for the element.\nThis is typically necessary for most elements and helps prevent text, border,\nand box shadow from rendering over themselves. Therefore, it should be kept at\nits default value of true in most circumstances, but it can be set to false\nwhen the element is fully managed by something that is guaranteed to render the\nappropriate background color and/or border for the element."}, {Name: "FillMargin", Doc: "FillMargin determines is whether to fill the margin with\nthe surrounding background color before rendering the element itself.\nThis is typically necessary to prevent text, border, and box shadow from\nrendering over themselves. Therefore, it should be kept at its default value\nof true in most circumstances, but it can be set to false when the element\nis fully managed by something that is guaranteed to render the\nappropriate background color for the element. It is irrelevant if RenderBox\nis false."}, {Name: "Overflow", Doc: "Overflow determines how to handle overflowing content in a layout.\nDefault is OverflowVisible. Set to OverflowAuto to enable scrollbars."}, {Name: "Gap", Doc: "For layouts, extra space added between elements in the layout."}, {Name: "Columns", Doc: "For grid layouts, the number of columns to use.\nIf > 0, number of rows is computed as N elements / Columns.\nUsed as a constraint in layout if individual elements\ndo not specify their row, column positions"}, {Name: "ObjectFit", Doc: "If this object is a replaced object (image, video, etc)\nor has a background image, ObjectFit specifies the way\nin which the replaced object should be fit into the element."}, {Name: "ObjectPosition", Doc: "If this object is a replaced object (image, video, etc)\nor has a background image, ObjectPosition specifies the\nX,Y position of the object within the space allocated for\nthe object (see ObjectFit)."}, {Name: "Border", Doc: "Border is a rendered border around the element."}, {Name: "MaxBorder", Doc: "MaxBorder is the largest border that will ever be rendered\naround the element, the size of which is used for computing\nthe effective margin to allocate for the element."}, {Name: "BoxShadow", Doc: "BoxShadow is the box shadows to render around box (can have multiple)"}, {Name: "MaxBoxShadow", Doc: "MaxBoxShadow contains the largest shadows that will ever be rendered\naround the element, the size of which are used for computing the\neffective margin to allocate for the element."}, {Name: "Color", Doc: "Color specifies the text / content color, and it is inherited."}, {Name: "Background", Doc: "Background specifies the background of the element. It is not inherited,\nand it is nil (transparent) by default."}, {Name: "Opacity", Doc: "alpha value between 0 and 1 to apply to the foreground and background\nof this element and all of its children."}, {Name: "StateLayer", Doc: "StateLayer, if above zero, indicates to create a state layer over\nthe element with this much opacity (on a scale of 0-1) and the\ncolor Color (or StateColor if it defined). It is automatically\nset based on State, but can be overridden in stylers."}, {Name: "StateColor", Doc: "StateColor, if not nil, is the color to use for the StateLayer\ninstead of Color. If you want to disable state layers\nfor an element, do not use this; instead, set StateLayer to 0."}, {Name: "ActualBackground", Doc: "ActualBackground is the computed actual background rendered for the element,\ntaking into account its Background, Opacity, StateLayer, and parent\nActualBackground. It is automatically computed and should not be set manually."}, {Name: "VirtualKeyboard", Doc: "VirtualKeyboard is the virtual keyboard to display, if any,\non mobile platforms when this element is focused. It is not\nused if the element is read only."}, {Name: "Pos", Doc: "Pos is used for the position of the widget if the parent frame\nhas [Style.Display] = [Custom]."}, {Name: "ZIndex", Doc: "ordering factor for rendering depth -- lower numbers rendered first.\nSort children according to this factor"}, {Name: "Row", Doc: "specifies the row that this element should appear within a grid layout"}, {Name: "Col", Doc: "specifies the column that this element should appear within a grid layout"}, {Name: "RowSpan", Doc: "specifies the number of sequential rows that this element should occupy\nwithin a grid layout (todo: not currently supported)"}, {Name: "ColSpan", Doc: "specifies the number of sequential columns that this element should occupy\nwithin a grid layout"}, {Name: "ScrollbarWidth", Doc: "ScrollbarWidth is the width of layout scrollbars. It defaults\nto [DefaultScrollbarWidth], and it is inherited."}, {Name: "IconSize", Doc: "IconSize specifies the size of icons in this widget, if it has icons.\nThe default icon size is 1em for X and Y. This is inherited."}, {Name: "Font", Doc: "Font styling parameters applicable to individual spans of text."}, {Name: "Text", Doc: "Text styling parameters applicable to a paragraph of text."}, {Name: "UnitContext", Doc: "unit context: parameters necessary for anchoring relative units"}}}) -var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/styles.Text", IDName: "text", Doc: "Text has styles for text layout styling.\nMost of these are inherited", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "Align", Doc: "Align specifies how to align text along the default direction (inherited).\nThis *only* applies to the text within its containing element,\nand is relevant only for multi-line text."}, {Name: "AlignV", Doc: "AlignV specifies \"vertical\" (orthogonal to default direction)\nalignment of text (inherited).\nThis *only* applies to the text within its containing element:\nif that element does not have a specified size\nthat is different from the text size, then this has *no effect*."}, {Name: "LineHeight", Doc: "LineHeight is a multiplier on the default font size for spacing between lines.\nIf there are larger font elements within a line, they will be accommodated, with\nthe same amount of total spacing added above that maximum size as if it was all\nthe same height. The default of 1.3 represents standard \"single spaced\" text."}, {Name: "ParaSpacing", Doc: "ParaSpacing is the line spacing between paragraphs (inherited).\nThis will be copied from [Style.Margin] if that is non-zero,\nor can be set directly. Like [LineHeight], this is a multiplier on\nthe default font size."}, {Name: "WhiteSpace", Doc: "WhiteSpace (not inherited) specifies how white space is processed,\nand how lines are wrapped. If set to WhiteSpaceNormal (default) lines are wrapped.\nSee info about interactions with Grow.X setting for this and the NoWrap case."}, {Name: "Direction", Doc: "Direction specifies the default text direction, which can be overridden if the\nunicode text is typically written in a different direction."}, {Name: "Indent", Doc: "Indent specifies how much to indent the first line in a paragraph (inherited)."}, {Name: "TabSize", Doc: "TabSize specifies the tab size, in number of characters (inherited)."}, {Name: "SelectColor", Doc: "SelectColor is the color to use for the background region of selected text (inherited)."}, {Name: "HighlightColor", Doc: "HighlightColor is the color to use for the background region of highlighted text (inherited)."}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/styles.Text", IDName: "text", Doc: "Text has styles for text layout styling.\nMost of these are inherited", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "Align", Doc: "Align specifies how to align text along the default direction (inherited).\nThis *only* applies to the text within its containing element,\nand is relevant only for multi-line text."}, {Name: "AlignV", Doc: "AlignV specifies \"vertical\" (orthogonal to default direction)\nalignment of text (inherited).\nThis *only* applies to the text within its containing element:\nif that element does not have a specified size\nthat is different from the text size, then this has *no effect*."}, {Name: "LineHeight", Doc: "LineHeight is a multiplier on the default font size for spacing between lines.\nIf there are larger font elements within a line, they will be accommodated, with\nthe same amount of total spacing added above that maximum size as if it was all\nthe same height. The default of 1.3 represents standard \"single spaced\" text."}, {Name: "WhiteSpace", Doc: "WhiteSpace (not inherited) specifies how white space is processed,\nand how lines are wrapped. If set to WhiteSpaceNormal (default) lines are wrapped.\nSee info about interactions with Grow.X setting for this and the NoWrap case."}, {Name: "Direction", Doc: "Direction specifies the default text direction, which can be overridden if the\nunicode text is typically written in a different direction."}, {Name: "TabSize", Doc: "TabSize specifies the tab size, in number of characters (inherited)."}, {Name: "SelectColor", Doc: "SelectColor is the color to use for the background region of selected text (inherited)."}, {Name: "HighlightColor", Doc: "HighlightColor is the color to use for the background region of highlighted text (inherited)."}}}) var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/styles.XY", IDName: "xy", Doc: "XY represents X,Y values", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "X", Doc: "X is the horizontal axis value"}, {Name: "Y", Doc: "Y is the vertical axis value"}}}) diff --git a/svg/circle.go b/svg/circle.go index b9a7835701..6c230def5b 100644 --- a/svg/circle.go +++ b/svg/circle.go @@ -5,7 +5,6 @@ package svg import ( - "cogentcore.org/core/base/slicesx" "cogentcore.org/core/math32" ) @@ -56,9 +55,9 @@ func (g *Circle) Render(sv *SVG) { // each node must define this for itself func (g *Circle) ApplyTransform(sv *SVG, xf math32.Matrix2) { rot := xf.ExtractRot() - if rot != 0 || !g.Paint.Transform.IsIdentity() { - g.Paint.Transform.SetMul(xf) // todo: could be backward - g.SetProperty("transform", g.Paint.Transform.String()) + if rot != 0 { + g.Paint.Transform.SetMul(xf) + g.SetTransformProperty() } else { g.Pos = xf.MulVector2AsPoint(g.Pos) scx, scy := xf.ExtractScale() @@ -66,45 +65,3 @@ func (g *Circle) ApplyTransform(sv *SVG, xf math32.Matrix2) { g.GradientApplyTransform(sv, xf) } } - -// ApplyDeltaTransform applies the given 2D delta transforms to the geometry of this node -// relative to given point. Trans translation and point are in top-level coordinates, -// so must be transformed into local coords first. -// Point is upper left corner of selection box that anchors the translation and scaling, -// and for rotation it is the center point around which to rotate -func (g *Circle) ApplyDeltaTransform(sv *SVG, trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2) { - crot := g.Paint.Transform.ExtractRot() - if rot != 0 || crot != 0 { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, false) // exclude self - g.Paint.Transform.SetMulCenter(xf, lpt) - g.SetProperty("transform", g.Paint.Transform.String()) - } else { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, true) // include self - g.Pos = xf.MulVector2AsPointCenter(g.Pos, lpt) - scx, scy := xf.ExtractScale() - g.Radius *= 0.5 * (scx + scy) - g.GradientApplyTransformPt(sv, xf, lpt) - } -} - -// WriteGeom writes the geometry of the node to a slice of floating point numbers -// the length and ordering of which is specific to each node type. -// Slice must be passed and will be resized if not the correct length. -func (g *Circle) WriteGeom(sv *SVG, dat *[]float32) { - *dat = slicesx.SetLength(*dat, 3+6) - (*dat)[0] = g.Pos.X - (*dat)[1] = g.Pos.Y - (*dat)[2] = g.Radius - g.WriteTransform(*dat, 3) - g.GradientWritePts(sv, dat) -} - -// ReadGeom reads the geometry of the node from a slice of floating point numbers -// the length and ordering of which is specific to each node type. -func (g *Circle) ReadGeom(sv *SVG, dat []float32) { - g.Pos.X = dat[0] - g.Pos.Y = dat[1] - g.Radius = dat[2] - g.ReadTransform(dat, 3) - g.GradientReadPts(sv, dat) -} diff --git a/svg/css.go b/svg/css.go index c9f399ea64..f25ce3a595 100644 --- a/svg/css.go +++ b/svg/css.go @@ -11,14 +11,18 @@ import ( "github.com/aymerick/douceur/parser" ) -// StyleSheet is a Node2D node that contains a stylesheet -- property values +// StyleSheet is a node that contains a stylesheet -- property values // contained in this sheet can be transformed into tree.Properties and set in CSS -// field of appropriate node +// field of appropriate node. type StyleSheet struct { NodeBase Sheet *css.Stylesheet `copier:"-"` } +func (ss *StyleSheet) SVGName() string { + return "css" +} + // ParseString parses the string into a StyleSheet of rules, which can then be // used for extracting properties func (ss *StyleSheet) ParseString(str string) error { @@ -61,11 +65,18 @@ func (ss *StyleSheet) CSSProperties() map[string]any { return pr } -//////////////////////////////////////////////////////////////////////////////////////// -// MetaData +//////// MetaData // MetaData is used for holding meta data info type MetaData struct { NodeBase MetaData string } + +func (g *MetaData) SVGName() string { + return "metadata" +} + +func (g *MetaData) EnforceSVGName() bool { + return false +} diff --git a/svg/ellipse.go b/svg/ellipse.go index 672d7cbc41..74826dd920 100644 --- a/svg/ellipse.go +++ b/svg/ellipse.go @@ -5,7 +5,6 @@ package svg import ( - "cogentcore.org/core/base/slicesx" "cogentcore.org/core/math32" ) @@ -58,53 +57,11 @@ func (g *Ellipse) ApplyTransform(sv *SVG, xf math32.Matrix2) { rot := xf.ExtractRot() if rot != 0 || !g.Paint.Transform.IsIdentity() { g.Paint.Transform.SetMul(xf) - g.SetProperty("transform", g.Paint.Transform.String()) + g.SetTransformProperty() } else { + // todo: this is not the correct transform: g.Pos = xf.MulVector2AsPoint(g.Pos) g.Radii = xf.MulVector2AsVector(g.Radii) g.GradientApplyTransform(sv, xf) } } - -// ApplyDeltaTransform applies the given 2D delta transforms to the geometry of this node -// relative to given point. Trans translation and point are in top-level coordinates, -// so must be transformed into local coords first. -// Point is upper left corner of selection box that anchors the translation and scaling, -// and for rotation it is the center point around which to rotate -func (g *Ellipse) ApplyDeltaTransform(sv *SVG, trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2) { - crot := g.Paint.Transform.ExtractRot() - if rot != 0 || crot != 0 { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, false) // exclude self - g.Paint.Transform.SetMulCenter(xf, lpt) - g.SetProperty("transform", g.Paint.Transform.String()) - } else { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, true) // include self - g.Pos = xf.MulVector2AsPointCenter(g.Pos, lpt) - g.Radii = xf.MulVector2AsVector(g.Radii) - g.GradientApplyTransformPt(sv, xf, lpt) - } -} - -// WriteGeom writes the geometry of the node to a slice of floating point numbers -// the length and ordering of which is specific to each node type. -// Slice must be passed and will be resized if not the correct length. -func (g *Ellipse) WriteGeom(sv *SVG, dat *[]float32) { - *dat = slicesx.SetLength(*dat, 4+6) - (*dat)[0] = g.Pos.X - (*dat)[1] = g.Pos.Y - (*dat)[2] = g.Radii.X - (*dat)[3] = g.Radii.Y - g.WriteTransform(*dat, 4) - g.GradientWritePts(sv, dat) -} - -// ReadGeom reads the geometry of the node from a slice of floating point numbers -// the length and ordering of which is specific to each node type. -func (g *Ellipse) ReadGeom(sv *SVG, dat []float32) { - g.Pos.X = dat[0] - g.Pos.Y = dat[1] - g.Radii.X = dat[2] - g.Radii.Y = dat[3] - g.ReadTransform(dat, 4) - g.GradientReadPts(sv, dat) -} diff --git a/svg/gradient.go b/svg/gradient.go index 539423db32..b6c47f9b8d 100644 --- a/svg/gradient.go +++ b/svg/gradient.go @@ -5,15 +5,20 @@ package svg import ( + "fmt" "log" "strings" "cogentcore.org/core/colors/gradient" "cogentcore.org/core/math32" + "cogentcore.org/core/tree" ) -///////////////////////////////////////////////////////////////////////////// -// Gradient +// note: this code provides convenience methods for updating gradient data +// when dynamically updating svg nodes, as in an animation, svg drawing app, etc. +// It is an awkward design that the gradient needs to be updated for each node +// for most sensible use-cases, because the ObjectBoundingBox is too limited, +// so a unique UserSpaceOnUse gradient is typically needed for each node. // Gradient is used for holding a specified color gradient. // The name is the id for lookup in url @@ -27,16 +32,42 @@ type Gradient struct { StopsName string } -// GradientTypeName returns the SVG-style type name of gradient: linearGradient or radialGradient +// IsRadial returns true if given gradient is a raidal type. +func (gr *Gradient) IsRadial() bool { + _, ok := gr.Grad.(*gradient.Radial) + return ok +} + +// GradientTypeName returns the SVG-style type name of gradient: +// linearGradient or radialGradient func (gr *Gradient) GradientTypeName() string { - if _, ok := gr.Grad.(*gradient.Radial); ok { + if gr.IsRadial() { return "radialGradient" } return "linearGradient" } -////////////////////////////////////////////////////////////////////////////// -// SVG gradient management +// fromGeom sets gradient geom for UserSpaceOnUse gradient units +// from given geom. +func (gr *Gradient) fromGeom(xf math32.Matrix2) { // gg *GradientGeom) { + gb := gr.Grad.AsBase() + if gb.Units != gradient.UserSpaceOnUse { + return + } + gb.Transform = xf +} + +// toGeom copies gradient points to given geom +// for UserSpaceOnUse gradient units. +func (gr *Gradient) toGeom(xf *math32.Matrix2) { + gb := gr.Grad.AsBase() + if gb.Units != gradient.UserSpaceOnUse { + return + } + *xf = gb.Transform +} + +//////// SVG gradient management // GradientByName returns the gradient of given name, stored on SVG node func (sv *SVG) GradientByName(n Node, grnm string) *Gradient { @@ -52,129 +83,92 @@ func (sv *SVG) GradientByName(n Node, grnm string) *Gradient { return gr } -// GradientApplyTransform applies the given transform to any gradients for this node, -// that are using specific coordinates (not bounding box which is automatic) +// GradientApplyTransform applies given transform to node's gradient geometry. +// This should ONLY be called when the node's transform is _not_ being updated, +// and instead its geometry values (pos etc) are being transformed directly by +// this transform, because the node's transform will be applied to the gradient +// when it is being rendered. func (g *NodeBase) GradientApplyTransform(sv *SVG, xf math32.Matrix2) { + g.GradientFill = xf.Mul(g.GradientFill) + g.GradientStroke = xf.Mul(g.GradientStroke) + g.GradientUpdateGeom(sv) +} + +// GradientGeomDefault sets the initial default gradient geometry from node. +func (g *NodeBase) GradientGeomDefault(sv *SVG, gr gradient.Gradient) { + gi := g.This.(Node) + if rg, ok := gr.(*gradient.Radial); ok { + lbb := gi.LocalBBox(sv) + ctr := lbb.Center() + sz := lbb.Size() + rad := 0.5 * max(sz.X, sz.Y) + rg.Center = ctr + rg.Focal = ctr + rg.Radius.Set(rad, rad) + return + } + lg := gr.(*gradient.Linear) + lbb := gi.LocalBBox(sv) + lg.Start = lbb.Min + lg.End = math32.Vec2(lbb.Max.X, lbb.Min.Y) // L-R +} + +// GradientUpdateGeom updates the geometry of UserSpaceOnUse gradients +// in use by this node for "fill" and "stroke" by copying its current +// GradientGeom points to those gradients. +func (g *NodeBase) GradientUpdateGeom(sv *SVG) { gi := g.This.(Node) gnm := NodePropURL(gi, "fill") if gnm != "" { gr := sv.GradientByName(gi, gnm) if gr != nil { - gr.Grad.AsBase().Transform.SetMul(xf) // todo: do the Ctr, unscale version? + gr.fromGeom(g.GradientFill) + // fmt.Println("set fill:", g.GradientFill) } } gnm = NodePropURL(gi, "stroke") if gnm != "" { gr := sv.GradientByName(gi, gnm) if gr != nil { - gr.Grad.AsBase().Transform.SetMul(xf) + gr.fromGeom(g.GradientStroke) } } } -// GradientApplyTransformPt applies the given transform with ctr point -// to any gradients for this node, that are using specific coordinates -// (not bounding box which is automatic) -func (g *NodeBase) GradientApplyTransformPt(sv *SVG, xf math32.Matrix2, pt math32.Vector2) { +// GradientFromGradients updates the geometry of UserSpaceOnUse gradients +// in use by this node for "fill" and "stroke" by copying its current +// GradientGeom points to those gradients. +func (g *NodeBase) GradientFromGradients(sv *SVG) { gi := g.This.(Node) gnm := NodePropURL(gi, "fill") if gnm != "" { gr := sv.GradientByName(gi, gnm) if gr != nil { - gr.Grad.AsBase().Transform.SetMulCenter(xf, pt) // todo: ctr off? + gr.toGeom(&g.GradientFill) } } gnm = NodePropURL(gi, "stroke") if gnm != "" { gr := sv.GradientByName(gi, gnm) if gr != nil { - gr.Grad.AsBase().Transform.SetMulCenter(xf, pt) + gr.toGeom(&g.GradientStroke) } } } -// GradientWritePoints writes the gradient points to -// a slice of floating point numbers, appending to end of slice. -func GradientWritePts(gr gradient.Gradient, dat *[]float32) { - // TODO: do we want this, and is this the right way to structure it? - if gr == nil { - return - } - gb := gr.AsBase() - *dat = append(*dat, gb.Transform.XX) - *dat = append(*dat, gb.Transform.YX) - *dat = append(*dat, gb.Transform.XY) - *dat = append(*dat, gb.Transform.YY) - *dat = append(*dat, gb.Transform.X0) - *dat = append(*dat, gb.Transform.Y0) - - *dat = append(*dat, gb.Box.Min.X) - *dat = append(*dat, gb.Box.Min.Y) - *dat = append(*dat, gb.Box.Max.X) - *dat = append(*dat, gb.Box.Max.Y) -} - -// GradientWritePts writes the geometry of the gradients for this node -// to a slice of floating point numbers, appending to end of slice. -func (g *NodeBase) GradientWritePts(sv *SVG, dat *[]float32) { - gnm := NodePropURL(g, "fill") - if gnm != "" { - gr := sv.GradientByName(g, gnm) - if gr != nil { - GradientWritePts(gr.Grad, dat) - } - } - gnm = NodePropURL(g, "stroke") - if gnm != "" { - gr := sv.GradientByName(g, gnm) - if gr != nil { - GradientWritePts(gr.Grad, dat) - } - } +// GradientFromGradients updates the geometry of UserSpaceOnUse gradients +// for all nodes, for "fill" and "stroke" properties, by copying the current +// GradientGeom points to those gradients. This can be done after loading +// for cases where node transforms will be updated dynamically. +func (sv *SVG) GradientFromGradients() { + sv.Root.WalkDown(func(n tree.Node) bool { + nb := n.(Node).AsNodeBase() + nb.GradientFromGradients(sv) + return tree.Continue + }) } -// GradientReadPoints reads the gradient points from -// a slice of floating point numbers, reading from the end. -func GradientReadPts(gr gradient.Gradient, dat []float32) { - if gr == nil { - return - } - gb := gr.AsBase() - sz := len(dat) - gb.Box.Min.X = dat[sz-4] - gb.Box.Min.Y = dat[sz-3] - gb.Box.Max.X = dat[sz-2] - gb.Box.Max.Y = dat[sz-1] - - gb.Transform.XX = dat[sz-10] - gb.Transform.YX = dat[sz-9] - gb.Transform.XY = dat[sz-8] - gb.Transform.YY = dat[sz-7] - gb.Transform.X0 = dat[sz-6] - gb.Transform.Y0 = dat[sz-5] -} - -// GradientReadPts reads the geometry of the gradients for this node -// from a slice of floating point numbers, reading from the end. -func (g *NodeBase) GradientReadPts(sv *SVG, dat []float32) { - gnm := NodePropURL(g, "fill") - if gnm != "" { - gr := sv.GradientByName(g, gnm) - if gr != nil { - GradientReadPts(gr.Grad, dat) - } - } - gnm = NodePropURL(g, "stroke") - if gnm != "" { - gr := sv.GradientByName(g, gnm) - if gr != nil { - GradientReadPts(gr.Grad, dat) - } - } -} - -////////////////////////////////////////////////////////////////////////////// -// Gradient management utilities for creating element-specific grads +//////// Gradient management utilities for creating element-specific grads // GradientUpdateStops copies stops from StopsName gradient if it is set func (sv *SVG) GradientUpdateStops(gr *Gradient) { @@ -201,13 +195,20 @@ func (sv *SVG) GradientDeleteForNode(n Node, grnm string) bool { } // GradientNewForNode adds a new gradient specific to given node -// that points to given stops name. returns the new gradient +// that points to given stops name. Returns the new gradient // and the url that points to it (nil if parent svg cannot be found). -// Initializes gradient to use bounding box of object, but using userSpaceOnUse setting -func (sv *SVG) GradientNewForNode(n Node, radial bool, stops string) (*Gradient, string) { +// Initializes gradient to use current GradientFill or Stroke with UserSpaceOnUse. +func (sv *SVG) GradientNewForNode(n Node, prop string, radial bool, stops string) (*Gradient, string) { gr, url := sv.GradientNew(radial) gr.StopsName = stops - gr.Grad.AsBase().SetBox(n.LocalBBox(sv)) + gr.Grad.AsBase().Units = gradient.UserSpaceOnUse + nb := n.AsNodeBase() + nb.GradientGeomDefault(sv, gr.Grad) + if prop == "fill" { + nb.GradientFill = math32.Identity2() + } else { + nb.GradientStroke = math32.Identity2() + } sv.GradientUpdateStops(gr) return gr, url } @@ -223,7 +224,8 @@ func (sv *SVG) GradientNew(radial bool) (*Gradient, string) { } gr := NewGradient(sv.Defs) id := sv.NewUniqueID() - gr.SetName(NameID(gnm, id)) + gnm = NameID(gnm, id) + gr.SetName(gnm) url := NameToURL(gnm) if radial { gr.Grad = gradient.NewRadial() @@ -233,15 +235,20 @@ func (sv *SVG) GradientNew(radial bool) (*Gradient, string) { return gr, url } -// GradientUpdateNodeProp ensures that node has a gradient property of given type +// GradientUpdateNodeProp ensures that node has a gradient property of given type. func (sv *SVG) GradientUpdateNodeProp(n Node, prop string, radial bool, stops string) (*Gradient, string) { - ps := n.AsTree().Property(prop) + nb := n.AsNodeBase() + ps := nb.Property(prop) if ps == nil { - gr, url := sv.GradientNewForNode(n, radial, stops) - n.AsTree().SetProperty(prop, url) + gr, url := sv.GradientNewForNode(n, prop, radial, stops) + nb.SetProperty(prop, url) + nb.SetProperty(prop+"-opacity", "1") return gr, url } - pstr := ps.(string) + pstr, ok := ps.(string) + if !ok { + pstr = *ps.(*string) + } trgst := "" if radial { trgst = "radialGradient" @@ -251,79 +258,22 @@ func (sv *SVG) GradientUpdateNodeProp(n Node, prop string, radial bool, stops st url := "url(#" + trgst if strings.HasPrefix(pstr, url) { gr := sv.GradientByName(n, pstr) - gr.StopsName = stops - sv.GradientUpdateStops(gr) - return gr, NameToURL(gr.Name) + if gr != nil { + gr.StopsName = stops + sv.GradientUpdateStops(gr) + return gr, NameToURL(gr.Name) + } else { + fmt.Println("not found:", pstr, url) + } } if strings.HasPrefix(pstr, "url(#") { // wrong kind sv.GradientDeleteForNode(n, pstr) } - gr, url := sv.GradientNewForNode(n, radial, stops) - n.AsTree().SetProperty(prop, url) + gr, url := sv.GradientNewForNode(n, prop, radial, stops) + nb.SetProperty(prop, url) return gr, url } -// GradientUpdateNodePoints updates the points for node based on current bbox -func (sv *SVG) GradientUpdateNodePoints(n Node, prop string) { - ps := n.AsTree().Property(prop) - if ps == nil { - return - } - pstr := ps.(string) - url := "url(#" - if !strings.HasPrefix(pstr, url) { - return - } - gr := sv.GradientByName(n, pstr) - if gr == nil { - return - } - gb := gr.Grad.AsBase() - gb.SetBox(n.LocalBBox(sv)) - gb.SetTransform(math32.Identity2()) -} - -// GradientCloneNodeProp creates a new clone of the existing gradient for node -// if set for given property key ("fill" or "stroke"). -// returns new gradient. -func (sv *SVG) GradientCloneNodeProp(n Node, prop string) *Gradient { - ps := n.AsTree().Property(prop) - if ps == nil { - return nil - } - pstr := ps.(string) - radial := false - if strings.HasPrefix(pstr, "url(#radialGradient") { - radial = true - } else if !strings.HasPrefix(pstr, "url(#linearGradient") { - return nil - } - gr := sv.GradientByName(n, pstr) - if gr == nil { - return nil - } - ngr, url := sv.GradientNewForNode(n, radial, gr.StopsName) - n.AsTree().SetProperty(prop, url) - gradient.CopyFrom(ngr.Grad, gr.Grad) - // TODO(kai): should this return ngr or gr? (used to return gr but ngr seems correct) - return ngr -} - -// GradientDeleteNodeProp deletes any existing gradient for node -// if set for given property key ("fill" or "stroke"). -// Returns true if deleted. -func (sv *SVG) GradientDeleteNodeProp(n Node, prop string) bool { - ps := n.AsTree().Property(prop) - if ps == nil { - return false - } - pstr := ps.(string) - if !strings.HasPrefix(pstr, "url(#radialGradient") && !strings.HasPrefix(pstr, "url(#linearGradient") { - return false - } - return sv.GradientDeleteForNode(n, pstr) -} - // GradientUpdateAllStops removes any items from Defs that are not actually referred to // by anything in the current SVG tree. Returns true if items were removed. // Does not remove gradients with StopsName = "" with extant stops -- these diff --git a/svg/group.go b/svg/group.go index 9ed0a64cfa..9ed4e21acf 100644 --- a/svg/group.go +++ b/svg/group.go @@ -5,7 +5,6 @@ package svg import ( - "cogentcore.org/core/base/slicesx" "cogentcore.org/core/math32" ) @@ -24,11 +23,23 @@ func (g *Group) BBoxes(sv *SVG, parTransform math32.Matrix2) { g.BBoxesFromChildren(sv, parTransform) } +func (g *Group) IsVisible(sv *SVG) bool { + if g == nil || g.This == nil || !g.Paint.Display { // does not check g.Paint.Off! + return false + } + nvis := g.VisBBox == math32.Box2{} + if nvis && !g.isDef { + return false + } + return true +} + func (g *Group) Render(sv *SVG) { - if !g.PushContext(sv) { + if !g.IsVisible(sv) { return } pc := g.Painter(sv) + pc.PushContext(&g.Paint, nil) g.RenderChildren(sv) pc.PopContext() } @@ -37,30 +48,5 @@ func (g *Group) Render(sv *SVG) { // each node must define this for itself func (g *Group) ApplyTransform(sv *SVG, xf math32.Matrix2) { g.Paint.Transform.SetMul(xf) - g.SetProperty("transform", g.Paint.Transform.String()) -} - -// ApplyDeltaTransform applies the given 2D delta transforms to the geometry of this node -// relative to given point. Trans translation and point are in top-level coordinates, -// so must be transformed into local coords first. -// Point is upper left corner of selection box that anchors the translation and scaling, -// and for rotation it is the center point around which to rotate -func (g *Group) ApplyDeltaTransform(sv *SVG, trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2) { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, false) // group does NOT include self - g.Paint.Transform.SetMulCenter(xf, lpt) - g.SetProperty("transform", g.Paint.Transform.String()) -} - -// WriteGeom writes the geometry of the node to a slice of floating point numbers -// the length and ordering of which is specific to each node type. -// Slice must be passed and will be resized if not the correct length. -func (g *Group) WriteGeom(sv *SVG, dat *[]float32) { - *dat = slicesx.SetLength(*dat, 6) - g.WriteTransform(*dat, 0) -} - -// ReadGeom reads the geometry of the node from a slice of floating point numbers -// the length and ordering of which is specific to each node type. -func (g *Group) ReadGeom(sv *SVG, dat []float32) { - g.ReadTransform(dat, 0) + g.SetTransformProperty() } diff --git a/svg/image.go b/svg/image.go index 233ca9fbed..38773aacc7 100644 --- a/svg/image.go +++ b/svg/image.go @@ -10,7 +10,6 @@ import ( "log" "cogentcore.org/core/base/iox/imagex" - "cogentcore.org/core/base/slicesx" "cogentcore.org/core/math32" "golang.org/x/image/draw" "golang.org/x/image/math/f64" @@ -126,55 +125,16 @@ func (g *Image) Render(sv *SVG) { // each node must define this for itself func (g *Image) ApplyTransform(sv *SVG, xf math32.Matrix2) { rot := xf.ExtractRot() - if rot != 0 || !g.Paint.Transform.IsIdentity() { + if rot != 0 { g.Paint.Transform.SetMul(xf) - g.SetProperty("transform", g.Paint.Transform.String()) + g.SetTransformProperty() } else { g.Pos = xf.MulVector2AsPoint(g.Pos) g.Size = xf.MulVector2AsVector(g.Size) + g.GradientApplyTransform(sv, xf) } } -// ApplyDeltaTransform applies the given 2D delta transforms to the geometry of this node -// relative to given point. Trans translation and point are in top-level coordinates, -// so must be transformed into local coords first. -// Point is upper left corner of selection box that anchors the translation and scaling, -// and for rotation it is the center point around which to rotate -func (g *Image) ApplyDeltaTransform(sv *SVG, trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2) { - crot := g.Paint.Transform.ExtractRot() - if rot != 0 || crot != 0 { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, false) // exclude self - g.Paint.Transform.SetMulCenter(xf, lpt) - g.SetProperty("transform", g.Paint.Transform.String()) - } else { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, true) // include self - g.Pos = xf.MulVector2AsPointCenter(g.Pos, lpt) - g.Size = xf.MulVector2AsVector(g.Size) - } -} - -// WriteGeom writes the geometry of the node to a slice of floating point numbers -// the length and ordering of which is specific to each node type. -// Slice must be passed and will be resized if not the correct length. -func (g *Image) WriteGeom(sv *SVG, dat *[]float32) { - *dat = slicesx.SetLength(*dat, 4+6) - (*dat)[0] = g.Pos.X - (*dat)[1] = g.Pos.Y - (*dat)[2] = g.Size.X - (*dat)[3] = g.Size.Y - g.WriteTransform(*dat, 4) -} - -// ReadGeom reads the geometry of the node from a slice of floating point numbers -// the length and ordering of which is specific to each node type. -func (g *Image) ReadGeom(sv *SVG, dat []float32) { - g.Pos.X = dat[0] - g.Pos.Y = dat[1] - g.Size.X = dat[2] - g.Size.Y = dat[3] - g.ReadTransform(dat, 4) -} - // OpenImage opens an image for the bitmap, and resizes to the size of the image // or the specified size -- pass 0 for width and/or height to use the actual image size // for that dimension diff --git a/svg/io.go b/svg/io.go index adf9abb9bc..9a28379ca5 100644 --- a/svg/io.go +++ b/svg/io.go @@ -3,15 +3,14 @@ // license that can be found in the LICENSE file. // svg parsing is adapted from github.com/srwiley/oksvg: -// // Copyright 2017 The oksvg Authors. All rights reserved. -// // created: 2/12/2017 by S.R.Wiley package svg import ( "bufio" + "bytes" "encoding/xml" "errors" "fmt" @@ -869,8 +868,14 @@ func (sv *SVG) UnmarshalXML(decoder *xml.Decoder, se xml.StartElement) error { return nil } -//////////////////////////////////////////////////////////////////////////////////// -// Writing +//////// Writing + +// XMLString returns the svg to a XML-encoded file, using WriteXML +func (sv *SVG) XMLString() string { + var b bytes.Buffer + sv.WriteXML(&b, false) + return string(b.Bytes()) +} // SaveXML saves the svg to a XML-encoded file, using WriteXML func (sv *SVG) SaveXML(fname string) error { @@ -933,28 +938,26 @@ func MarshalXML(n tree.Node, enc *XMLEncoder, setName string) string { } text := "" // if non-empty, contains text to render _, issvg := n.(Node) - _, isgp := n.(*Group) + // _, isgp := n.(*Group) _, ismark := n.(*Marker) - if !isgp { - if issvg && !ismark { - sp := styleprops.ToXMLString(properties) - if sp != "" { - XMLAddAttr(&se.Attr, "style", sp) - } - if txp, has := properties["transform"]; has { - XMLAddAttr(&se.Attr, "transform", reflectx.ToString(txp)) - } - } else { - for k, v := range properties { - sv := reflectx.ToString(v) - if _, has := InkscapeProperties[k]; has { - k = "inkscape:" + k - } else if k == "overflow" { - k = "style" - sv = "overflow:" + sv - } - XMLAddAttr(&se.Attr, k, sv) + if issvg && !ismark { + sp := styleprops.ToXMLString(properties) + if sp != "" { + XMLAddAttr(&se.Attr, "style", sp) + } + if txp, has := properties["transform"]; has { + XMLAddAttr(&se.Attr, "transform", reflectx.ToString(txp)) + } + } else { + for k, v := range properties { + sv := reflectx.ToString(v) + if _, has := InkscapeProperties[k]; has { + k = "inkscape:" + k + } else if k == "overflow" { + k = "style" + sv = "overflow:" + sv } + XMLAddAttr(&se.Attr, k, sv) } } var sb strings.Builder @@ -966,8 +969,8 @@ func MarshalXML(n tree.Node, enc *XMLEncoder, setName string) string { XMLAddAttr(&se.Attr, "d", nd.DataStr) case *Group: nm = "g" - if strings.HasPrefix(strings.ToLower(n.AsTree().Name), "layer") { - } + // if strings.HasPrefix(strings.ToLower(n.AsTree().Name), "layer") { + // } for k, v := range properties { sv := reflectx.ToString(v) switch k { @@ -1022,7 +1025,8 @@ func MarshalXML(n tree.Node, enc *XMLEncoder, setName string) string { } XMLAddAttr(&se.Attr, "points", sb.String()) case *Text: - if nd.Text == "" { + _, parIsTxt := nd.Parent.(*Text) + if nd.HasChildren() || !parIsTxt { nm = "text" } else { nm = "tspan" @@ -1096,13 +1100,11 @@ func MarshalXMLGradient(n *Gradient, name string, enc *XMLEncoder) { } if linear { - // must be non-zero to add - if gb.Box != (math32.Box2{}) { - XMLAddAttr(&me.Attr, "x1", fmt.Sprintf("%g", gb.Box.Min.X)) - XMLAddAttr(&me.Attr, "y1", fmt.Sprintf("%g", gb.Box.Min.Y)) - XMLAddAttr(&me.Attr, "x2", fmt.Sprintf("%g", gb.Box.Max.X)) - XMLAddAttr(&me.Attr, "y2", fmt.Sprintf("%g", gb.Box.Max.Y)) - } + l := gr.(*gradient.Linear) + XMLAddAttr(&me.Attr, "x1", fmt.Sprintf("%g", l.Start.X)) + XMLAddAttr(&me.Attr, "y1", fmt.Sprintf("%g", l.Start.Y)) + XMLAddAttr(&me.Attr, "x2", fmt.Sprintf("%g", l.End.X)) + XMLAddAttr(&me.Attr, "y2", fmt.Sprintf("%g", l.End.Y)) } else { r := gr.(*gradient.Radial) // must be non-zero to add @@ -1129,7 +1131,7 @@ func MarshalXMLGradient(n *Gradient, name string, enc *XMLEncoder) { } if n.StopsName != "" { - XMLAddAttr(&me.Attr, "href", "#"+n.StopsName) + XMLAddAttr(&me.Attr, "xlink:href", "#"+n.StopsName) } enc.EncodeToken(me) @@ -1178,37 +1180,61 @@ func MarshalXMLTree(n Node, enc *XMLEncoder, setName string) (string, error) { func (sv *SVG) MarshalXMLx(enc *XMLEncoder, se xml.StartElement) error { me := xml.StartElement{} me.Name.Local = "svg" - // TODO: what makes sense for PhysicalWidth and PhysicalHeight here? - if sv.PhysicalWidth.Value > 0 { - XMLAddAttr(&me.Attr, "width", fmt.Sprintf("%g", sv.PhysicalWidth.Value)) + + hasNamedView := false + if sv.Root.NumChildren() > 0 { + kd := sv.Root.Children[0] + if md, ismd := kd.(*MetaData); ismd { + if strings.HasPrefix(md.Name, "namedview") { + hasNamedView = true + } + } + } + + // PhysicalWidth and PhysicalHeight define the viewport in SVG terminology: + // https://www.w3.org/TR/SVG/coords.html + // these are defined in "abstract" units. + if sv.PhysicalWidth.Dots > 0 { + XMLAddAttr(&me.Attr, "width", fmt.Sprintf("%g", sv.PhysicalWidth.Dots)) } - if sv.PhysicalHeight.Value > 0 { - XMLAddAttr(&me.Attr, "height", fmt.Sprintf("%g", sv.PhysicalHeight.Value)) + if sv.PhysicalHeight.Dots > 0 { + XMLAddAttr(&me.Attr, "height", fmt.Sprintf("%g", sv.PhysicalHeight.Dots)) } XMLAddAttr(&me.Attr, "viewBox", fmt.Sprintf("%g %g %g %g", sv.Root.ViewBox.Min.X, sv.Root.ViewBox.Min.Y, sv.Root.ViewBox.Size.X, sv.Root.ViewBox.Size.Y)) - XMLAddAttr(&me.Attr, "xmlns:inkscape", "http://www.inkscape.org/namespaces/inkscape") - XMLAddAttr(&me.Attr, "xmlns:sodipodi", "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd") + if hasNamedView { + XMLAddAttr(&me.Attr, "xmlns:inkscape", "http://www.inkscape.org/namespaces/inkscape") + XMLAddAttr(&me.Attr, "xmlns:sodipodi", "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd") + } + XMLAddAttr(&me.Attr, "xmlns:xlink", "http://www.w3.org/1999/xlink") XMLAddAttr(&me.Attr, "xmlns", "http://www.w3.org/2000/svg") enc.EncodeToken(me) - dnm, err := MarshalXMLTree(sv.Defs, enc, "defs") - enc.WriteEnd(dnm) + var errs []error - for _, k := range sv.Root.Children { - var knm string - knm, err = MarshalXMLTree(k.(Node), enc, "") - if knm != "" { - enc.WriteEnd(knm) + if sv.Defs.HasChildren() { + dnm, err := MarshalXMLTree(sv.Defs, enc, "defs") + if err != nil { + errs = append(errs, err) + } else { + enc.WriteEnd(dnm) } + } + + for _, k := range sv.Root.Children { + knm, err := MarshalXMLTree(k.(Node), enc, "") if err != nil { + errs = append(errs, err) break } + if knm != "" { + enc.WriteEnd(knm) + } } ed := xml.EndElement{} ed.Name = me.Name enc.EncodeToken(ed) - return err + return errors.Join(errs...) } // SetStandardXMLAttr sets standard attributes of node given XML-style name / @@ -1217,7 +1243,7 @@ func SetStandardXMLAttr(ni Node, name, val string) bool { nb := ni.AsNodeBase() switch name { case "id": - nb.SetName(val) + nb.Name = val return true case "class": nb.Class = val diff --git a/svg/line.go b/svg/line.go index 387c9481b0..9d5ce2a953 100644 --- a/svg/line.go +++ b/svg/line.go @@ -5,7 +5,6 @@ package svg import ( - "cogentcore.org/core/base/slicesx" "cogentcore.org/core/math32" ) @@ -69,55 +68,12 @@ func (g *Line) Render(sv *SVG) { // each node must define this for itself func (g *Line) ApplyTransform(sv *SVG, xf math32.Matrix2) { rot := xf.ExtractRot() - if rot != 0 || !g.Paint.Transform.IsIdentity() { + if rot != 0 { g.Paint.Transform.SetMul(xf) - g.SetProperty("transform", g.Paint.Transform.String()) + g.SetTransformProperty() } else { g.Start = xf.MulVector2AsPoint(g.Start) g.End = xf.MulVector2AsPoint(g.End) g.GradientApplyTransform(sv, xf) } } - -// ApplyDeltaTransform applies the given 2D delta transforms to the geometry of this node -// relative to given point. Trans translation and point are in top-level coordinates, -// so must be transformed into local coords first. -// Point is upper left corner of selection box that anchors the translation and scaling, -// and for rotation it is the center point around which to rotate -func (g *Line) ApplyDeltaTransform(sv *SVG, trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2) { - crot := g.Paint.Transform.ExtractRot() - if rot != 0 || crot != 0 { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, false) // exclude self - g.Paint.Transform.SetMulCenter(xf, lpt) - g.SetProperty("transform", g.Paint.Transform.String()) - } else { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, true) // include self - g.Start = xf.MulVector2AsPointCenter(g.Start, lpt) - g.End = xf.MulVector2AsPointCenter(g.End, lpt) - g.GradientApplyTransformPt(sv, xf, lpt) - } -} - -// WriteGeom writes the geometry of the node to a slice of floating point numbers -// the length and ordering of which is specific to each node type. -// Slice must be passed and will be resized if not the correct length. -func (g *Line) WriteGeom(sv *SVG, dat *[]float32) { - *dat = slicesx.SetLength(*dat, 4+6) - (*dat)[0] = g.Start.X - (*dat)[1] = g.Start.Y - (*dat)[2] = g.End.X - (*dat)[3] = g.End.Y - g.WriteTransform(*dat, 4) - g.GradientWritePts(sv, dat) -} - -// ReadGeom reads the geometry of the node from a slice of floating point numbers -// the length and ordering of which is specific to each node type. -func (g *Line) ReadGeom(sv *SVG, dat []float32) { - g.Start.X = dat[0] - g.Start.Y = dat[1] - g.End.X = dat[2] - g.End.Y = dat[3] - g.ReadTransform(dat, 4) - g.GradientReadPts(sv, dat) -} diff --git a/svg/node.go b/svg/node.go index 7e2fb4a750..ce7294efc1 100644 --- a/svg/node.go +++ b/svg/node.go @@ -6,12 +6,11 @@ package svg import ( "fmt" - "image" "maps" "reflect" "strings" - "cogentcore.org/core/base/errors" + "cogentcore.org/core/base/reflectx" "cogentcore.org/core/base/slicesx" "cogentcore.org/core/colors" "cogentcore.org/core/math32" @@ -48,22 +47,6 @@ type Node interface { // this just does a direct transform multiplication on coordinates. ApplyTransform(sv *SVG, xf math32.Matrix2) - // ApplyDeltaTransform applies the given 2D delta transforms to the geometry of this node - // relative to given point. Trans translation and point are in top-level coordinates, - // so must be transformed into local coords first. - // Point is upper left corner of selection box that anchors the translation and scaling, - // and for rotation it is the center point around which to rotate. - ApplyDeltaTransform(sv *SVG, trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2) - - // WriteGeom writes the geometry of the node to a slice of floating point numbers - // the length and ordering of which is specific to each node type. - // Slice must be passed and will be resized if not the correct length. - WriteGeom(sv *SVG, dat *[]float32) - - // ReadGeom reads the geometry of the node from a slice of floating point numbers - // the length and ordering of which is specific to each node type. - ReadGeom(sv *SVG, dat []float32) - // SVGName returns the SVG element name (e.g., "rect", "path" etc). SVGName() string @@ -96,57 +79,62 @@ type NodeBase struct { // BBox is the bounding box for the node within the SVG Pixels image. // This one can be outside the visible range of the SVG image. // VisBBox is intersected and only shows visible portion. - BBox image.Rectangle `copier:"-" json:"-" xml:"-" set:"-"` + BBox math32.Box2 `copier:"-" json:"-" xml:"-" set:"-"` // VisBBox is the visible bounding box for the node intersected with the SVG image geometry. - VisBBox image.Rectangle `copier:"-" json:"-" xml:"-" set:"-"` + VisBBox math32.Box2 `copier:"-" json:"-" xml:"-" set:"-"` // Paint is the paint style information for this node. Paint styles.Paint `json:"-" xml:"-" set:"-"` - // isDef is whether this is in [SVG.Defs]. - isDef bool -} + // GradientFill contains the fill gradient geometry to use for linear and radial + // gradients of UserSpaceOnUse type applied to this node. + // These values are updated and copied to gradients of the appropriate type to keep + // the gradients sync'd with updates to the node as it is transformed. + GradientFill math32.Matrix2 `json:"-" xml:"-" set:"-" display:"-"` -func (g *NodeBase) AsNodeBase() *NodeBase { - return g -} + // GradientStroke contains the stroke gradient geometry to use for linear and radial + // gradients of UserSpaceOnUse type applied to this node. + // These values are updated and copied to gradients of the appropriate type to keep + // the gradients sync'd with updates to the node as it is transformed. + GradientStroke math32.Matrix2 `json:"-" xml:"-" set:"-" display:"-"` -func (g *NodeBase) SVGName() string { - return "base" -} - -func (g *NodeBase) EnforceSVGName() bool { - return true -} - -func (g *NodeBase) SetPos(pos math32.Vector2) { -} - -func (g *NodeBase) SetSize(sz math32.Vector2) { -} - -func (g *NodeBase) LocalBBox(sv *SVG) math32.Box2 { - bb := math32.Box2{} - return bb -} - -func (n *NodeBase) BaseInterface() reflect.Type { - return reflect.TypeOf((*NodeBase)(nil)).Elem() + // isDef is whether this is in [SVG.Defs]. + isDef bool } -func (g *NodeBase) PaintStyle() *styles.Paint { - return &g.Paint -} +func (g *NodeBase) AsNodeBase() *NodeBase { return g } +func (g *NodeBase) SVGName() string { return "base" } +func (g *NodeBase) EnforceSVGName() bool { return true } +func (g *NodeBase) SetPos(pos math32.Vector2) {} +func (g *NodeBase) SetSize(sz math32.Vector2) {} +func (g *NodeBase) PaintStyle() *styles.Paint { return &g.Paint } +func (g *NodeBase) LocalBBox(sv *SVG) math32.Box2 { return math32.Box2{} } +func (g *NodeBase) BaseInterface() reflect.Type { return reflect.TypeOf((*NodeBase)(nil)).Elem() } +func (g *NodeBase) SetTransformProperty() { g.SetProperty("transform", g.Paint.Transform.String()) } func (g *NodeBase) Init() { g.Paint.Defaults() + g.GradientFill = math32.Identity2() + g.GradientStroke = math32.Identity2() + g.Paint.Stroke.Width.Px(1) // dp is not understood by svg.. } // SetColorProperties sets color property from a string representation. // It breaks color alpha out as opacity. prop is either "stroke" or "fill" func (g *NodeBase) SetColorProperties(prop, color string) { - clr := errors.Log1(colors.FromString(color)) + if NameFromURL(color) != "" { + return + } + if color == "none" || color == "" { + g.SetProperty(prop, "none") + g.DeleteProperty(prop + "-opacity") + if prop == "stroke" { + g.DeleteProperty("stroke-width") + } + return + } + clr, _ := colors.FromString(color) g.SetProperty(prop+"-opacity", fmt.Sprintf("%g", float32(clr.A)/255)) // we have consumed the A via opacity, so we reset it to 255 clr.A = 255 @@ -181,67 +169,18 @@ func (g *NodeBase) ParentTransform(self bool) math32.Matrix2 { return xf } -// ApplyTransform applies the given 2D transform to the geometry of this node -// this just does a direct transform multiplication on coordinates. +// ApplyTransform applies the given 2D transform to the geometry of this node. func (g *NodeBase) ApplyTransform(sv *SVG, xf math32.Matrix2) { } -// DeltaTransform computes the net transform matrix for given delta transform parameters -// and the transformed version of the reference point. If self is true, then -// include the current node self transform, otherwise don't. Groups do not -// but regular rendering nodes do. -func (g *NodeBase) DeltaTransform(trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2, self bool) (math32.Matrix2, math32.Vector2) { - mxi := g.ParentTransform(self) - mxi = mxi.Inverse() +// DeltaTransform computes the net transform matrix for given delta transform parameters, +// 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) - ldel := mxi.MulVector2AsVector(trans) - xf := math32.Scale2D(scale.X, scale.Y).Rotate(rot) - xf.X0 = ldel.X - xf.Y0 = ldel.Y - return xf, lpt -} - -// ApplyDeltaTransform applies the given 2D delta transforms to the geometry of this node -// relative to given point. Trans translation and point are in top-level coordinates, -// so must be transformed into local coords first. -// Point is upper left corner of selection box that anchors the translation and scaling, -// and for rotation it is the center point around which to rotate -func (g *NodeBase) ApplyDeltaTransform(sv *SVG, trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2) { -} - -// WriteTransform writes the node transform to slice at starting index. -// slice must already be allocated sufficiently. -func (g *NodeBase) WriteTransform(dat []float32, idx int) { - dat[idx+0] = g.Paint.Transform.XX - dat[idx+1] = g.Paint.Transform.YX - dat[idx+2] = g.Paint.Transform.XY - dat[idx+3] = g.Paint.Transform.YY - dat[idx+4] = g.Paint.Transform.X0 - dat[idx+5] = g.Paint.Transform.Y0 -} - -// ReadTransform reads the node transform from slice at starting index. -func (g *NodeBase) ReadTransform(dat []float32, idx int) { - g.Paint.Transform.XX = dat[idx+0] - g.Paint.Transform.YX = dat[idx+1] - g.Paint.Transform.XY = dat[idx+2] - g.Paint.Transform.YY = dat[idx+3] - g.Paint.Transform.X0 = dat[idx+4] - g.Paint.Transform.Y0 = dat[idx+5] -} - -// WriteGeom writes the geometry of the node to a slice of floating point numbers -// the length and ordering of which is specific to each node type. -// Slice must be passed and will be resized if not the correct length. -func (g *NodeBase) WriteGeom(sv *SVG, dat *[]float32) { - *dat = slicesx.SetLength(*dat, 6) - g.WriteTransform(*dat, 0) -} - -// ReadGeom reads the geometry of the node from a slice of floating point numbers -// the length and ordering of which is specific to each node type. -func (g *NodeBase) ReadGeom(sv *SVG, dat []float32) { - g.ReadTransform(dat, 0) + ltr := mxi.MulVector2AsVector(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 } // SVGWalkDown does [tree.NodeBase.WalkDown] on given node using given walk function @@ -254,7 +193,7 @@ func SVGWalkDown(n Node, fun func(sn Node, snb *NodeBase) bool) { } // SVGWalkDownNoDefs does [tree.Node.WalkDown] on given node using given walk function -// with SVG Node parameters. Automatically filters Defs nodes (IsDef) and MetaData, +// with SVG Node parameters. Automatically filters Defs nodes (IsDef) and MetaData, // i.e., it only processes concrete graphical nodes. func SVGWalkDownNoDefs(n Node, fun func(sn Node, snb *NodeBase) bool) { n.AsTree().WalkDown(func(cn tree.Node) bool { @@ -282,29 +221,6 @@ func FirstNonGroupNode(n Node) Node { return ngn } -// NodesContainingPoint returns all Nodes with Bounding Box that contains -// given point, optionally only those that are terminal nodes (no leaves). -// Excludes the starting node. -func NodesContainingPoint(n Node, pt image.Point, leavesOnly bool) []Node { - var cn []Node - SVGWalkDown(n, func(sn Node, snb *NodeBase) bool { - if sn == n { - return tree.Continue - } - if leavesOnly && snb.HasChildren() { - return tree.Continue - } - if snb.Paint.Off { - return tree.Break - } - if pt.In(snb.BBox) { - cn = append(cn, sn) - } - return tree.Continue - }) - return cn -} - //////// Standard Node infrastructure // Style styles the Paint values directly from node properties @@ -400,19 +316,19 @@ func (g *NodeBase) BBoxes(sv *SVG, parTransform math32.Matrix2) { xf := parTransform.Mul(g.Paint.Transform) ni := g.This.(Node) lbb := ni.LocalBBox(sv) - g.BBox = lbb.MulMatrix2(xf).ToRect() - g.VisBBox = sv.Geom.SizeRect().Intersect(g.BBox) + g.BBox = lbb.MulMatrix2(xf) + g.VisBBox = sv.Geom.Box2().Intersect(g.BBox) } // IsVisible checks our bounding box and visibility, returning false if // out of bounds. Must be called as first step in Render. func (g *NodeBase) IsVisible(sv *SVG) bool { - if g.Paint.Off || g == nil || g.This == nil { + if g == nil || g.This == nil || g.Paint.Off || !g.Paint.Display { return false } - nvis := g.VisBBox == image.Rectangle{} + nvis := g.VisBBox == (math32.Box2{}) if nvis && !g.isDef { - // fmt.Println("invisible:", g.Name, g.BBox, g.VisBBox) + // fmt.Println("invisible:", g.Name, "bb:", g.BBox, "vbb:", g.VisBBox, "svg:", sv.Geom.Bounds()) return false } return true @@ -437,7 +353,7 @@ func (g *NodeBase) PushContext(sv *SVG) bool { func (g *NodeBase) BBoxesFromChildren(sv *SVG, parTransform math32.Matrix2) { xf := parTransform.Mul(g.Paint.Transform) - var bb image.Rectangle + var bb math32.Box2 for i, kid := range g.Children { ni := kid.(Node) ni.BBoxes(sv, xf) @@ -449,7 +365,7 @@ func (g *NodeBase) BBoxesFromChildren(sv *SVG, parTransform math32.Matrix2) { } } g.BBox = bb - g.VisBBox = sv.Geom.SizeRect().Intersect(g.BBox) + g.VisBBox = sv.Geom.Box2().Intersect(g.BBox) } func (g *NodeBase) RenderChildren(sv *SVG) { @@ -465,3 +381,28 @@ func (g *NodeBase) Render(sv *SVG) { } g.RenderChildren(sv) } + +// BitCloneNode returns a bit-wise copy of just the single svg Node itself +// without any of the children, props or other state being copied, etc. +// Useful for saving and restoring state during animations or other +// manipulations. See also [CopyFrom]. +func BitCloneNode(n Node) Node { + cp := n.AsTree().NewInstance().(Node) + BitCopyFrom(cp, n) + return cp +} + +// BitCopyFrom copies only the direct field bits and other key shape data +// (e.g., [Path.Data]) between nodes. Useful for saving and restoring +// state during animations or other manipulations. +func BitCopyFrom(to, fm any) { + reflectx.Underlying(reflect.ValueOf(to)).Set(reflectx.Underlying(reflect.ValueOf(fm))) + switch x := to.(type) { + case *Path: + x.Data = fm.(*Path).Data.Clone() + case *Polyline: + slicesx.CopyFrom(x.Points, fm.(*Polyline).Points) + case *Polygon: + slicesx.CopyFrom(x.Points, fm.(*Polygon).Points) + } +} diff --git a/svg/path.go b/svg/path.go index 8b266f5da2..309751af34 100644 --- a/svg/path.go +++ b/svg/path.go @@ -5,7 +5,7 @@ package svg import ( - "cogentcore.org/core/base/slicesx" + "cogentcore.org/core/base/errors" "cogentcore.org/core/math32" "cogentcore.org/core/paint/ppath" ) @@ -28,18 +28,24 @@ func (g *Path) SetPos(pos math32.Vector2) { } func (g *Path) SetSize(sz math32.Vector2) { - // todo: scale bbox + bb := g.Data.FastBounds() + csz := bb.Size() + if csz.X == 0 || csz.Y == 0 { + return + } + sc := sz.Div(csz) + g.Data.Transform(math32.Scale2D(sc.X, sc.Y)) } // SetData sets the path data to given string, parsing it into an optimized // form used for rendering func (g *Path) SetData(data string) error { - g.DataStr = data - var err error - g.Data, err = ppath.ParseSVGPath(data) - if err != nil { + d, err := ppath.ParseSVGPath(data) + if errors.Log(err) != nil { return err } + g.DataStr = data + g.Data = d return err } @@ -69,12 +75,14 @@ func (g *Path) Render(sv *SVG) { pos := g.Data.Coords() dir := g.Data.CoordDirections() np := len(pos) - if mrk_start != nil && np > 0 { - ang := ppath.Angle(dir[0]) + if mrk_start != nil && np > 1 { + dir0 := pos[1].Sub(pos[0]) + ang := ppath.Angle(dir0) // dir[0]: has average but last 2 works better mrk_start.RenderMarker(sv, pos[0], ang, g.Paint.Stroke.Width.Dots) } if mrk_end != nil && np > 1 { - ang := ppath.Angle(dir[np-1]) + dirn := pos[np-1].Sub(pos[np-2]) + ang := ppath.Angle(dirn) // dir[np-1]: see above mrk_end.RenderMarker(sv, pos[np-1], ang, g.Paint.Stroke.Width.Dots) } if mrk_mid != nil && np > 2 { @@ -92,57 +100,9 @@ func (g *Path) UpdatePathString() { g.DataStr = g.Data.ToSVG() } -//////// Transforms - // ApplyTransform applies the given 2D transform to the geometry of this node // each node must define this for itself func (g *Path) ApplyTransform(sv *SVG, xf math32.Matrix2) { - // path may have horiz, vert elements -- only gen soln is to transform - g.Paint.Transform.SetMul(xf) - g.SetProperty("transform", g.Paint.Transform.String()) -} - -// ApplyDeltaTransform applies the given 2D delta transforms to the geometry of this node -// relative to given point. Trans translation and point are in top-level coordinates, -// so must be transformed into local coords first. -// Point is upper left corner of selection box that anchors the translation and scaling, -// and for rotation it is the center point around which to rotate -func (g *Path) ApplyDeltaTransform(sv *SVG, trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2) { - crot := g.Paint.Transform.ExtractRot() - if rot != 0 || crot != 0 { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, false) // exclude self - g.Paint.Transform.SetMulCenter(xf, lpt) - g.SetProperty("transform", g.Paint.Transform.String()) - } else { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, true) // include self - g.ApplyTransformImpl(xf, lpt) - g.GradientApplyTransformPt(sv, xf, lpt) - } -} - -// ApplyTransformImpl does the implementation of applying a transform to all points -func (g *Path) ApplyTransformImpl(xf math32.Matrix2, lpt math32.Vector2) { g.Data.Transform(xf) -} - -// WriteGeom writes the geometry of the node to a slice of floating point numbers -// the length and ordering of which is specific to each node type. -// Slice must be passed and will be resized if not the correct length. -func (g *Path) WriteGeom(sv *SVG, dat *[]float32) { - sz := len(g.Data) - *dat = slicesx.SetLength(*dat, sz+6) - for i := range g.Data { - (*dat)[i] = float32(g.Data[i]) - } - g.WriteTransform(*dat, sz) - g.GradientWritePts(sv, dat) -} - -// ReadGeom reads the geometry of the node from a slice of floating point numbers -// the length and ordering of which is specific to each node type. -func (g *Path) ReadGeom(sv *SVG, dat []float32) { - sz := len(g.Data) - g.Data = ppath.Path(dat) - g.ReadTransform(dat, sz) - g.GradientReadPts(sv, dat) + g.GradientApplyTransform(sv, xf) } diff --git a/svg/polyline.go b/svg/polyline.go index 2acf8f2d84..13f6fe1c23 100644 --- a/svg/polyline.go +++ b/svg/polyline.go @@ -5,7 +5,6 @@ package svg import ( - "cogentcore.org/core/base/slicesx" "cogentcore.org/core/math32" ) @@ -76,9 +75,9 @@ func (g *Polyline) Render(sv *SVG) { // each node must define this for itself func (g *Polyline) ApplyTransform(sv *SVG, xf math32.Matrix2) { rot := xf.ExtractRot() - if rot != 0 || !g.Paint.Transform.IsIdentity() { + if rot != 0 { g.Paint.Transform.SetMul(xf) - g.SetProperty("transform", g.Paint.Transform.String()) + g.SetTransformProperty() } else { for i, p := range g.Points { p = xf.MulVector2AsPoint(p) @@ -87,51 +86,3 @@ func (g *Polyline) ApplyTransform(sv *SVG, xf math32.Matrix2) { g.GradientApplyTransform(sv, xf) } } - -// ApplyDeltaTransform applies the given 2D delta transforms to the geometry of this node -// relative to given point. Trans translation and point are in top-level coordinates, -// so must be transformed into local coords first. -// Point is upper left corner of selection box that anchors the translation and scaling, -// and for rotation it is the center point around which to rotate -func (g *Polyline) ApplyDeltaTransform(sv *SVG, trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2) { - crot := g.Paint.Transform.ExtractRot() - if rot != 0 || crot != 0 { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, false) // exclude self - g.Paint.Transform.SetMulCenter(xf, lpt) - g.SetProperty("transform", g.Paint.Transform.String()) - } else { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, true) // include self - for i, p := range g.Points { - p = xf.MulVector2AsPointCenter(p, lpt) - g.Points[i] = p - } - g.GradientApplyTransformPt(sv, xf, lpt) - } -} - -// WriteGeom writes the geometry of the node to a slice of floating point numbers -// the length and ordering of which is specific to each node type. -// Slice must be passed and will be resized if not the correct length. -func (g *Polyline) WriteGeom(sv *SVG, dat *[]float32) { - sz := len(g.Points) * 2 - *dat = slicesx.SetLength(*dat, sz+6) - for i, p := range g.Points { - (*dat)[i*2] = p.X - (*dat)[i*2+1] = p.Y - } - g.WriteTransform(*dat, sz) - g.GradientWritePts(sv, dat) -} - -// ReadGeom reads the geometry of the node from a slice of floating point numbers -// the length and ordering of which is specific to each node type. -func (g *Polyline) ReadGeom(sv *SVG, dat []float32) { - sz := len(g.Points) * 2 - for i, p := range g.Points { - p.X = dat[i*2] - p.Y = dat[i*2+1] - g.Points[i] = p - } - g.ReadTransform(dat, sz) - g.GradientReadPts(sv, dat) -} diff --git a/svg/rect.go b/svg/rect.go index df83e508fd..322027a5ad 100644 --- a/svg/rect.go +++ b/svg/rect.go @@ -5,7 +5,6 @@ package svg import ( - "cogentcore.org/core/base/slicesx" "cogentcore.org/core/math32" "cogentcore.org/core/styles/sides" ) @@ -67,58 +66,12 @@ func (g *Rect) Render(sv *SVG) { // each node must define this for itself func (g *Rect) ApplyTransform(sv *SVG, xf math32.Matrix2) { rot := xf.ExtractRot() - if rot != 0 || !g.Paint.Transform.IsIdentity() { + if rot != 0 { g.Paint.Transform.SetMul(xf) - g.SetProperty("transform", g.Paint.Transform.String()) + g.SetTransformProperty() } else { g.Pos = xf.MulVector2AsPoint(g.Pos) g.Size = xf.MulVector2AsVector(g.Size) g.GradientApplyTransform(sv, xf) } } - -// ApplyDeltaTransform applies the given 2D delta transforms to the geometry of this node -// relative to given point. Trans translation and point are in top-level coordinates, -// so must be transformed into local coords first. -// Point is upper left corner of selection box that anchors the translation and scaling, -// and for rotation it is the center point around which to rotate -func (g *Rect) ApplyDeltaTransform(sv *SVG, trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2) { - crot := g.Paint.Transform.ExtractRot() - if rot != 0 || crot != 0 { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, false) // exclude self - g.Paint.Transform.SetMulCenter(xf, lpt) // todo: this might be backwards for everything - g.SetProperty("transform", g.Paint.Transform.String()) - } else { - // fmt.Println("adt", trans, scale, rot, pt) - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, true) // include self - // opos := g.Pos - g.Pos = xf.MulVector2AsPointCenter(g.Pos, lpt) - // fmt.Println("apply delta trans:", opos, g.Pos, xf) - g.Size = xf.MulVector2AsVector(g.Size) - g.GradientApplyTransformPt(sv, xf, lpt) - } -} - -// WriteGeom writes the geometry of the node to a slice of floating point numbers -// the length and ordering of which is specific to each node type. -// Slice must be passed and will be resized if not the correct length. -func (g *Rect) WriteGeom(sv *SVG, dat *[]float32) { - *dat = slicesx.SetLength(*dat, 4+6) - (*dat)[0] = g.Pos.X - (*dat)[1] = g.Pos.Y - (*dat)[2] = g.Size.X - (*dat)[3] = g.Size.Y - g.WriteTransform(*dat, 4) - g.GradientWritePts(sv, dat) -} - -// ReadGeom reads the geometry of the node from a slice of floating point numbers -// the length and ordering of which is specific to each node type. -func (g *Rect) ReadGeom(sv *SVG, dat []float32) { - g.Pos.X = dat[0] - g.Pos.Y = dat[1] - g.Size.X = dat[2] - g.Size.Y = dat[3] - g.ReadTransform(dat, 4) - g.GradientReadPts(sv, dat) -} diff --git a/svg/svg.go b/svg/svg.go index 96c7a0e133..f71722eecc 100644 --- a/svg/svg.go +++ b/svg/svg.go @@ -63,7 +63,7 @@ type SVG struct { Color image.Image // Size is size of image, Pos is offset within any parent viewport. - // Node bounding boxes are based on 0 Pos offset within RenderImage + // The bounding boxes within the scene _include_ the Pos offset already. Geom math32.Geom2DInt // physical width of the drawing, e.g., when printed. @@ -80,11 +80,11 @@ type SVG struct { InvertY bool // Translate specifies a translation to apply beyond what is specified in the SVG, - // and its ViewBox transform. + // and its ViewBox transform, in top-level rendering units (dots, pixels). Translate math32.Vector2 // Scale specifies a zoom scale factor to apply beyond what is specified in the SVG, - // and its ViewBox transform. + // and its ViewBox transform. See [SVG.ZoomAt] for convenient zooming method. Scale float32 // painter is the current painter being used, which is only valid during rendering. @@ -226,6 +226,7 @@ func (sv *SVG) Render(pc *paint.Painter) *paint.Painter { if pc != nil { sv.painter = pc } else { + sv.UpdateSize() sv.painter = paint.NewPainter(math32.FromPoint(sv.Geom.Size)) pc = sv.painter } @@ -242,8 +243,7 @@ func (sv *SVG) Render(pc *paint.Painter) *paint.Painter { } sv.Style() - sv.SetRootTransform() - sv.Root.BBoxes(sv, math32.Identity2()) + sv.UpdateBBoxes() if sv.Background != nil { sv.FillViewport() @@ -262,7 +262,25 @@ func (sv *SVG) RenderImage() image.Image { // SaveImage renders the SVG to an image and saves it to given filename, // using the filename extension to determine the file type. func (sv *SVG) SaveImage(fname string) error { - return imagex.Save(sv.RenderImage(), fname) + pos := sv.Geom.Pos + sv.Geom.Pos = image.Point{} + err := imagex.Save(sv.RenderImage(), fname) + sv.Geom.Pos = pos + return err +} + +// SaveImageSize renders the SVG to an image and saves it to given filename, +// using the filename extension to determine the file type. +// Specify either width or height of resulting image, or nothing for +// current physical size as set. +func (sv *SVG) SaveImageSize(fname string, width, height float32) error { + sz := sv.Geom + sv.Geom.Pos = image.Point{} + sv.Geom.Size.X = int(width) + sv.Geom.Size.Y = int(height) + err := imagex.Save(sv.RenderImage(), fname) + sv.Geom = sz + return err } func (sv *SVG) FillViewport() { @@ -271,39 +289,14 @@ func (sv *SVG) FillViewport() { pc.FillBox(math32.Vector2{}, math32.FromPoint(sv.Geom.Size), sv.Background) } -// SetRootTransform sets the Root node transform based on ViewBox, Translate, Scale -// parameters set on the SVG object. -func (sv *SVG) SetRootTransform() { - vb := &sv.Root.ViewBox - box := math32.FromPoint(sv.Geom.Size) - if vb.Size.X == 0 { - vb.Size.X = sv.PhysicalWidth.Dots - } - if vb.Size.Y == 0 { - vb.Size.Y = sv.PhysicalHeight.Dots - } - _, trans, scale := vb.Transform(box) - if sv.InvertY { - scale.Y *= -1 - } - trans.SetSub(vb.Min) - trans.SetAdd(sv.Translate) - scale.SetMulScalar(sv.Scale) - pc := &sv.Root.Paint - pc.Transform = pc.Transform.Scale(scale.X, scale.Y).Translate(trans.X, trans.Y) - if sv.InvertY { - pc.Transform.Y0 = -pc.Transform.Y0 - } +// UpdateBBoxes updates the bounding boxes for all nodes +// using current transform settings. +func (sv *SVG) UpdateBBoxes() { + sv.setRootTransform() + sv.Root.BBoxes(sv, math32.Identity2()) } -// SetDPITransform sets a scaling transform to compensate for -// a given LogicalDPI factor. -// svg rendering is done within a 96 DPI context. -func (sv *SVG) SetDPITransform(logicalDPI float32) { - pc := &sv.Root.Paint - dpisc := logicalDPI / 96.0 - pc.Transform = math32.Scale2D(dpisc, dpisc) -} +//////// Root // Root represents the root of an SVG tree. type Root struct { diff --git a/svg/svg_test.go b/svg/svg_test.go index c0c5cbf094..2c5da57d6c 100644 --- a/svg/svg_test.go +++ b/svg/svg_test.go @@ -40,7 +40,7 @@ func TestSVG(t *testing.T) { files := fsx.Filenames(filepath.Join("testdata", dir), ".svg") for _, fn := range files { - // if fn != "fig_vm_as_tug_of_war.svg" { + // if fn != "fig_neuron_as_detect.svg" { // continue // } RunTest(t, 640, 480, dir, fn) diff --git a/svg/testdata/svg/fig_bp_compute_delta.svg b/svg/testdata/svg/fig_bp_compute_delta.svg index 868b171424..4e0bb1eebd 100644 --- a/svg/testdata/svg/fig_bp_compute_delta.svg +++ b/svg/testdata/svg/fig_bp_compute_delta.svg @@ -1,13 +1,13 @@ + id="defs"> + d="M0 0L5 -5L-12.5 0L5 5z" /> + d="M0 0L5 -5L-12.5 0L5 5z" /> + id="linearGradient5361" + x1="0" + y1="0" + x2="1" + y2="0" + gradientUnits="objectBoundingBox"> @@ -126,8 +131,8 @@ xlink:href="#linearGradient5361" /> @@ -135,7 +140,7 @@ id="path7205" style="fill:#b3b3b3;fill-rule:evenodd;marker-start:none;stroke:#b3b3b3;stroke-width:1.0pt;" transform="scale(0.4) rotate(180) translate(10,0)" - d="M 0,0 L 5,-5 L -12.5,0 L 5,5 L 0,0 z " /> + d="M0 0L5 -5L-12.5 0L5 5z" /> + d="M0 0L5 -5L-12.5 0L5 5z" /> + + + + style="stroke-width:1px;fill:none;nodetypes:cc;stroke-linejoin:miter;marker-end:url(#Arrow1Mendr);stroke-opacity:1;display:inline;stroke:#b3b3b3;stroke-linecap:butt;" + d="M110.6117 460.6653L88.6409 433.8963" /> + style="display:inline;stroke:#b3b3b3;nodetypes:cc;stroke-linecap:butt;stroke-width:1px;fill:none;stroke-linejoin:miter;marker-end:url(#Arrow1Mendf);stroke-opacity:1;" + d="M111.995 460.9179L133.9658 434.1488" /> + style="marker-end:url(#Arrow1Mendr);nodetypes:cc;stroke:#b3b3b3;stroke-width:1px;stroke-linecap:butt;fill:none;stroke-linejoin:miter;stroke-opacity:1;display:inline;" + d="M110.6117 460.6653L88.6409 433.8963" /> + style="nodetypes:cc;fill:none;display:inline;stroke:#b3b3b3;stroke-width:1px;stroke-opacity:1;marker-end:url(#Arrow1Mendf);stroke-linecap:butt;stroke-linejoin:miter;" + d="M111.995 460.9179L133.9658 434.1488" /> @@ -281,7 +295,6 @@ @@ -293,7 +306,6 @@ @@ -305,64 +317,66 @@ + d="M104.0457 601.0765C104.0457 606.3765 99.74924 610.673 94.44927 610.673C89.1493 610.673 84.85282 606.3765 84.85282 601.0765C84.85282 595.7766 89.1493 591.4801 94.44927 591.4801C99.74924 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74924 610.673 94.44927 610.673C89.1493 610.673 84.85282 606.3765 84.85282 601.0765C84.85282 595.7766 89.1493 591.4801 94.44927 591.4801C99.74924 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74925 610.673 94.44928 610.673C89.14931 610.673 84.85283 606.3765 84.85283 601.0765C84.85283 595.7766 89.14931 591.4801 94.44928 591.4801C99.74925 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74924 610.673 94.44927 610.673C89.1493 610.673 84.85282 606.3765 84.85282 601.0765C84.85282 595.7766 89.1493 591.4801 94.44927 591.4801C99.74924 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74924 610.673 94.44927 610.673C89.1493 610.673 84.85282 606.3765 84.85282 601.0765C84.85282 595.7766 89.1493 591.4801 94.44927 591.4801C99.74924 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74925 610.673 94.44928 610.673C89.14931 610.673 84.85283 606.3765 84.85283 601.0765C84.85283 595.7766 89.14931 591.4801 94.44928 591.4801C99.74925 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74924 610.673 94.44927 610.673C89.1493 610.673 84.85282 606.3765 84.85282 601.0765C84.85282 595.7766 89.1493 591.4801 94.44927 591.4801C99.74924 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74924 610.673 94.44927 610.673C89.1493 610.673 84.85282 606.3765 84.85282 601.0765C84.85282 595.7766 89.1493 591.4801 94.44927 591.4801C99.74924 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74925 610.673 94.44928 610.673C89.14931 610.673 84.85283 606.3765 84.85283 601.0765C84.85283 595.7766 89.14931 591.4801 94.44928 591.4801C99.74925 591.4801 104.0457 595.7766 104.0457 601.0765z" /> @@ -374,7 +388,6 @@ @@ -386,7 +399,6 @@ @@ -398,7 +410,6 @@ @@ -410,7 +421,6 @@ @@ -422,11 +432,10 @@ + style="nodetypes:cc;fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#Arrow1Mend);" + d="M107.7072 22.75711V37.65686" /> @@ -438,7 +447,6 @@ @@ -450,7 +458,6 @@ @@ -462,7 +469,6 @@ @@ -474,7 +480,6 @@ @@ -486,7 +491,6 @@ @@ -498,7 +502,6 @@ @@ -510,7 +513,6 @@ @@ -522,7 +524,6 @@ @@ -534,7 +535,6 @@ @@ -546,7 +546,6 @@ @@ -558,7 +557,6 @@ @@ -570,7 +568,6 @@ @@ -582,7 +579,6 @@ @@ -594,7 +590,6 @@ @@ -606,7 +601,6 @@ @@ -618,7 +612,6 @@ @@ -630,7 +623,6 @@ @@ -642,7 +634,6 @@ @@ -654,7 +645,6 @@ @@ -666,7 +656,6 @@ @@ -678,7 +667,6 @@ @@ -690,95 +678,99 @@ + style="stroke:#000000;display:inline;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#Arrow1Mend);fill:none;" + d="M81.82236 505.6171L104.5508 478.343" /> + style="stroke-linecap:butt;stroke-linejoin:miter;display:inline;stroke-opacity:1;marker-end:url(#Arrow1Mend);fill:none;stroke:#000000;stroke-width:1px;" + d="M141.3065 506.1222L118.578 478.8481" /> + style="stroke-opacity:1;display:inline;marker-end:url(#Arrow1Mend);fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;" + d="M111.3693 505.6171V480.6158" /> + style="marker-end:url(#Arrow1Mend);fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;" + d="M78.66534 129.5807L101.3938 102.3066" /> + style="stroke-opacity:1;marker-end:url(#Arrow1Mend);fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;" + d="M138.1494 130.0858L115.421 102.8117" /> + style="marker-end:url(#Arrow1Mend);fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;" + d="M108.2123 129.5807V104.5795" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74924 610.673 94.44927 610.673C89.1493 610.673 84.85282 606.3765 84.85282 601.0765C84.85282 595.7766 89.1493 591.4801 94.44927 591.4801C99.74924 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74923 610.673 94.44926 610.673C89.14928 610.673 84.85281 606.3765 84.85281 601.0765C84.85281 595.7766 89.14928 591.4801 94.44926 591.4801C99.74923 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74924 610.673 94.44927 610.673C89.1493 610.673 84.85282 606.3765 84.85282 601.0765C84.85282 595.7766 89.1493 591.4801 94.44927 591.4801C99.74924 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74924 610.673 94.44927 610.673C89.1493 610.673 84.85282 606.3765 84.85282 601.0765C84.85282 595.7766 89.1493 591.4801 94.44927 591.4801C99.74924 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74923 610.673 94.44926 610.673C89.14928 610.673 84.85281 606.3765 84.85281 601.0765C84.85281 595.7766 89.14928 591.4801 94.44926 591.4801C99.74923 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74924 610.673 94.44927 610.673C89.1493 610.673 84.85282 606.3765 84.85282 601.0765C84.85282 595.7766 89.1493 591.4801 94.44927 591.4801C99.74924 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74924 610.673 94.44927 610.673C89.1493 610.673 84.85282 606.3765 84.85282 601.0765C84.85282 595.7766 89.1493 591.4801 94.44927 591.4801C99.74924 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74923 610.673 94.44926 610.673C89.14928 610.673 84.85281 606.3765 84.85281 601.0765C84.85281 595.7766 89.14928 591.4801 94.44926 591.4801C99.74923 591.4801 104.0457 595.7766 104.0457 601.0765z" /> + d="M104.0457 601.0765C104.0457 606.3765 99.74924 610.673 94.44927 610.673C89.1493 610.673 84.85282 606.3765 84.85282 601.0765C84.85282 595.7766 89.1493 591.4801 94.44927 591.4801C99.74924 591.4801 104.0457 595.7766 104.0457 601.0765z" /> @@ -790,11 +782,10 @@ + style="stroke:#000000;stroke-width:1px;display:inline;nodetypes:cc;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#Arrow1Mend);fill:none;" + d="M291.1765 398.7935V413.6932" /> @@ -806,19 +797,18 @@ + style="stroke-linejoin:miter;display:inline;marker-end:none;marker-start:url(#Arrow1Mstart);fill:none;stroke-width:1px;stroke-linecap:butt;nodetypes:cc;stroke:#000000;stroke-opacity:1;" + d="M290.1663 460.1602L268.1955 433.3912" /> + style="stroke-linecap:butt;stroke:#000000;nodetypes:cc;marker-start:url(#Arrow1Mstart);display:inline;stroke-linejoin:miter;stroke-opacity:1;marker-end:none;fill:none;stroke-width:1px;" + d="M293.0649 460.1602L315.0357 433.3912" /> + style="stroke-opacity:1;marker-start:url(#Arrow1Mstart);marker-end:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;fill:none;display:inline;" + d="M291.6816 460.4128V435.4115" /> @@ -830,7 +820,6 @@ @@ -842,20 +831,19 @@ + style="marker-end:url(#Arrow1Mend);stroke:#000000;stroke-linecap:butt;stroke-linejoin:miter;fill-rule:evenodd;stroke-opacity:1;fill:none;stroke-width:1px;display:inline;" + d="M292.1866 505.6171V480.6158" /> + style="fill:none;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Mend);stroke-miterlimit:4;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;" + d="M316.0512 69.22412L273.1197 66.69874" /> + style="stroke-width:0.5;stroke-linecap:butt;marker-end:url(#Arrow1Mend);fill:none;stroke:#000000;stroke-opacity:1;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;" + d="M316.5563 118.9741L273.6248 116.4488" /> diff --git a/svg/testdata/svg/fig_cortex_lobes_v2.svg b/svg/testdata/svg/fig_cortex_lobes_v2.svg new file mode 100644 index 0000000000..e492cba3c3 --- /dev/null +++ b/svg/testdata/svg/fig_cortex_lobes_v2.svg @@ -0,0 +1,536 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frontal + + + Parietal + + + Temporal + + + Occipital + + + vision + + + space + + + action + + + language + + + auditory + + + objects + + + episodes + + + motor + + + + + + soma + + + control + + + planning + + + semantics + + + sequencing + + + selection + + + decisions + + + affect + + + motivation + + + time + + + number + + + relations + + + V1 + + + S1 + + + M1 + + + A1 + + diff --git a/svg/testdata/svg/fig_synapse.svg b/svg/testdata/svg/fig_synapse.svg new file mode 100644 index 0000000000..cc26fa2ac2 --- /dev/null +++ b/svg/testdata/svg/fig_synapse.svg @@ -0,0 +1,1117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Postsynaptic + + + Dendrite + + + Receptors + + + mGlu + + + AMPA + + + NMDA + + + Cleft + + + Presynaptic + + + Axon + + + Microtubule + + + Terminal + Button + + + Vesicles + + + Neuro- + transmitter + (glutamate) + + + Na+ + + + Ca++ + + + Ca++ + + + + PSD + + + + + + Spine + + + + + + + + + + + + + + + + + VGCC + + + Ca++ + + + + + diff --git a/svg/text.go b/svg/text.go index 99a361b3a1..4cb4157f81 100644 --- a/svg/text.go +++ b/svg/text.go @@ -5,7 +5,6 @@ package svg import ( - "cogentcore.org/core/base/slicesx" "cogentcore.org/core/colors" "cogentcore.org/core/math32" "cogentcore.org/core/text/htmltext" @@ -14,17 +13,22 @@ import ( "cogentcore.org/core/text/text" ) +// todo: needs some work to get up to spec: +// https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/tspan +// dx, dy need to be specifiable as relative offsets from parent text, not just glyph +// relative offsets. Also, the literal parsing of example in link of an inline +// element within a broader text shows that a different kind of parsing +// structure is required. + // Text renders SVG text, handling both text and tspan elements. // tspan is nested under a parent text, where text has empty Text string. +// There is no line wrapping on SVG Text: every span is a separate line. type Text struct { NodeBase // position of the left, baseline of the text Pos math32.Vector2 `xml:"{x,y}"` - // width of text to render if using word-wrapping - Width float32 `xml:"width"` - // text string to render Text string `xml:"text"` @@ -75,15 +79,6 @@ func (g *Text) SetNodePos(pos math32.Vector2) { } } -func (g *Text) SetNodeSize(sz math32.Vector2) { - g.Width = sz.X - scx, _ := g.Paint.Transform.ExtractScale() - for _, kii := range g.Children { - kt := kii.(*Text) - kt.Width = g.Width * scx - } -} - // LocalBBox does full text layout, but no transforms func (g *Text) LocalBBox(sv *SVG) math32.Box2 { if g.Text == "" { @@ -96,10 +91,12 @@ func (g *Text) LocalBBox(sv *SVG) math32.Box2 { } tx, _ := htmltext.HTMLToRich([]byte(g.Text), &fs, nil) // fmt.Println(tx) - sz := math32.Vec2(10000, 10000) + sz := math32.Vec2(10000, 10000) // no wrapping!! g.TextShaped = sv.TextShaper.WrapLines(tx, &fs, &pc.Text, &rich.DefaultSettings, sz) - // baseOff := g.TextShaped.Lines[0].Offset + baseOff := g.TextShaped.Lines[0].Offset g.TextShaped.StartAtBaseline() // remove top-left offset + return g.TextShaped.Bounds.Translate(g.Pos.Sub(baseOff)) + // fmt.Println("baseoff:", baseOff) // fmt.Println(pc.Text.FontSize, pc.Text.FontSize.Dots) @@ -142,7 +139,6 @@ func (g *Text) LocalBBox(sv *SVG) math32.Box2 { */ // todo: TextLength, AdjustGlyphs -- also svg2 at least supports word wrapping! // g.TextShaped.UpdateBBox() - return g.TextShaped.Bounds.Translate(g.Pos) } func (g *Text) BBoxes(sv *SVG, parTransform math32.Matrix2) { @@ -153,8 +149,8 @@ func (g *Text) BBoxes(sv *SVG, parTransform math32.Matrix2) { xf := parTransform.Mul(g.Paint.Transform) ni := g.This.(Node) lbb := ni.LocalBBox(sv) - g.BBox = lbb.MulMatrix2(xf).ToRect() - g.VisBBox = sv.Geom.SizeRect().Intersect(g.BBox) + g.BBox = lbb.MulMatrix2(xf) + g.VisBBox = sv.Geom.Box2().Intersect(g.BBox) } func (g *Text) Render(sv *SVG) { @@ -192,104 +188,13 @@ func (g *Text) RenderText(sv *SVG) { // each node must define this for itself func (g *Text) ApplyTransform(sv *SVG, xf math32.Matrix2) { rot := xf.ExtractRot() - if rot != 0 || !g.Paint.Transform.IsIdentity() { + scx, scy := xf.ExtractScale() + if rot != 0 || scx != 1 || scy != 1 || g.IsParText() { + // note: par text requires transform b/c not saving children pos g.Paint.Transform.SetMul(xf) - g.SetProperty("transform", g.Paint.Transform.String()) - } else { - if g.IsParText() { - for _, kii := range g.Children { - kt := kii.(*Text) - kt.ApplyTransform(sv, xf) - } - } else { - g.Pos = xf.MulVector2AsPoint(g.Pos) - scx, _ := xf.ExtractScale() - g.Width *= scx - g.GradientApplyTransform(sv, xf) - } - } -} - -// ApplyDeltaTransform applies the given 2D delta transforms to the geometry of this node -// relative to given point. Trans translation and point are in top-level coordinates, -// so must be transformed into local coords first. -// Point is upper left corner of selection box that anchors the translation and scaling, -// and for rotation it is the center point around which to rotate -func (g *Text) ApplyDeltaTransform(sv *SVG, trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2) { - crot := g.Paint.Transform.ExtractRot() - if rot != 0 || crot != 0 { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, false) // exclude self - g.Paint.Transform.SetMulCenter(xf, lpt) - g.SetProperty("transform", g.Paint.Transform.String()) + g.SetTransformProperty() } else { - if g.IsParText() { - // translation transform - xft, lptt := g.DeltaTransform(trans, scale, rot, pt, true) // include self when not a parent - // transform transform - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, false) - xf.X0 = 0 // negate translation effects - xf.Y0 = 0 - g.Paint.Transform.SetMulCenter(xf, lpt) - g.SetProperty("transform", g.Paint.Transform.String()) - - g.Pos = xft.MulVector2AsPointCenter(g.Pos, lptt) - scx, _ := xft.ExtractScale() - g.Width *= scx - for _, kii := range g.Children { - kt := kii.(*Text) - kt.Pos = xft.MulVector2AsPointCenter(kt.Pos, lptt) - kt.Width *= scx - } - } else { - xf, lpt := g.DeltaTransform(trans, scale, rot, pt, true) // include self when not a parent - g.Pos = xf.MulVector2AsPointCenter(g.Pos, lpt) - scx, _ := xf.ExtractScale() - g.Width *= scx - } - } -} - -// WriteGeom writes the geometry of the node to a slice of floating point numbers -// the length and ordering of which is specific to each node type. -// Slice must be passed and will be resized if not the correct length. -func (g *Text) WriteGeom(sv *SVG, dat *[]float32) { - if g.IsParText() { - npt := 9 + g.NumChildren()*3 - *dat = slicesx.SetLength(*dat, npt) - (*dat)[0] = g.Pos.X - (*dat)[1] = g.Pos.Y - (*dat)[2] = g.Width - g.WriteTransform(*dat, 3) - for i, kii := range g.Children { - kt := kii.(*Text) - off := 9 + i*3 - (*dat)[off+0] = kt.Pos.X - (*dat)[off+1] = kt.Pos.Y - (*dat)[off+2] = kt.Width - } - } else { - *dat = slicesx.SetLength(*dat, 3+6) - (*dat)[0] = g.Pos.X - (*dat)[1] = g.Pos.Y - (*dat)[2] = g.Width - g.WriteTransform(*dat, 3) - } -} - -// ReadGeom reads the geometry of the node from a slice of floating point numbers -// the length and ordering of which is specific to each node type. -func (g *Text) ReadGeom(sv *SVG, dat []float32) { - g.Pos.X = dat[0] - g.Pos.Y = dat[1] - g.Width = dat[2] - g.ReadTransform(dat, 3) - if g.IsParText() { - for i, kii := range g.Children { - kt := kii.(*Text) - off := 9 + i*3 - kt.Pos.X = dat[off+0] - kt.Pos.Y = dat[off+1] - kt.Width = dat[off+2] - } + g.Pos = xf.MulVector2AsPoint(g.Pos) + g.GradientApplyTransform(sv, xf) } } diff --git a/svg/typegen.go b/svg/typegen.go index fbc1e02099..fb59a5f247 100644 --- a/svg/typegen.go +++ b/svg/typegen.go @@ -33,12 +33,12 @@ var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.ClipPath", IDNa // ClipPath is used for holding a path that renders as a clip path func NewClipPath(parent ...tree.Node) *ClipPath { return tree.New[ClipPath](parent...) } -var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.StyleSheet", IDName: "style-sheet", Doc: "StyleSheet is a Node2D node that contains a stylesheet -- property values\ncontained in this sheet can be transformed into tree.Properties and set in CSS\nfield of appropriate node", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Sheet"}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.StyleSheet", IDName: "style-sheet", Doc: "StyleSheet is a node that contains a stylesheet -- property values\ncontained in this sheet can be transformed into tree.Properties and set in CSS\nfield of appropriate node.", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Sheet"}}}) // NewStyleSheet returns a new [StyleSheet] with the given optional parent: -// StyleSheet is a Node2D node that contains a stylesheet -- property values +// StyleSheet is a node that contains a stylesheet -- property values // contained in this sheet can be transformed into tree.Properties and set in CSS -// field of appropriate node +// field of appropriate node. func NewStyleSheet(parent ...tree.Node) *StyleSheet { return tree.New[StyleSheet](parent...) } // SetSheet sets the [StyleSheet.Sheet] @@ -194,7 +194,7 @@ func (t *Marker) SetTransform(v math32.Matrix2) *Marker { t.Transform = v; retur // effective size for actual rendering func (t *Marker) SetEffSize(v math32.Vector2) *Marker { t.EffSize = v; return t } -var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.NodeBase", IDName: "node-base", Doc: "NodeBase is the base type for all elements within an SVG tree.\nIt implements the [Node] interface and contains the core functionality.", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Class", Doc: "Class contains user-defined class name(s) used primarily for attaching\nCSS styles to different display elements.\nMultiple class names can be used to combine properties;\nuse spaces to separate per css standard."}, {Name: "CSS", Doc: "CSS is the cascading style sheet at this level.\nThese styles apply here and to everything below, until superceded.\nUse .class and #name Properties elements to apply entire styles\nto given elements, and type for element type."}, {Name: "CSSAgg", Doc: "CSSAgg is the aggregated css properties from all higher nodes down to this node."}, {Name: "BBox", Doc: "BBox is the bounding box for the node within the SVG Pixels image.\nThis one can be outside the visible range of the SVG image.\nVisBBox is intersected and only shows visible portion."}, {Name: "VisBBox", Doc: "VisBBox is the visible bounding box for the node intersected with the SVG image geometry."}, {Name: "Paint", Doc: "Paint is the paint style information for this node."}, {Name: "isDef", Doc: "isDef is whether this is in [SVG.Defs]."}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.NodeBase", IDName: "node-base", Doc: "NodeBase is the base type for all elements within an SVG tree.\nIt implements the [Node] interface and contains the core functionality.", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Class", Doc: "Class contains user-defined class name(s) used primarily for attaching\nCSS styles to different display elements.\nMultiple class names can be used to combine properties;\nuse spaces to separate per css standard."}, {Name: "CSS", Doc: "CSS is the cascading style sheet at this level.\nThese styles apply here and to everything below, until superceded.\nUse .class and #name Properties elements to apply entire styles\nto given elements, and type for element type."}, {Name: "CSSAgg", Doc: "CSSAgg is the aggregated css properties from all higher nodes down to this node."}, {Name: "BBox", Doc: "BBox is the bounding box for the node within the SVG Pixels image.\nThis one can be outside the visible range of the SVG image.\nVisBBox is intersected and only shows visible portion."}, {Name: "VisBBox", Doc: "VisBBox is the visible bounding box for the node intersected with the SVG image geometry."}, {Name: "Paint", Doc: "Paint is the paint style information for this node."}, {Name: "GradientFill", Doc: "GradientFill contains the fill gradient geometry to use for linear and radial\ngradients of UserSpaceOnUse type applied to this node.\nThese values are updated and copied to gradients of the appropriate type to keep\nthe gradients sync'd with updates to the node as it is transformed."}, {Name: "GradientStroke", Doc: "GradientStroke contains the stroke gradient geometry to use for linear and radial\ngradients of UserSpaceOnUse type applied to this node.\nThese values are updated and copied to gradients of the appropriate type to keep\nthe gradients sync'd with updates to the node as it is transformed."}, {Name: "isDef", Doc: "isDef is whether this is in [SVG.Defs]."}}}) // NewNodeBase returns a new [NodeBase] with the given optional parent: // NodeBase is the base type for all elements within an SVG tree. @@ -264,21 +264,18 @@ func NewRoot(parent ...tree.Node) *Root { return tree.New[Root](parent...) } // for the SVG during rendering. func (t *Root) SetViewBox(v ViewBox) *Root { t.ViewBox = v; return t } -var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Text", IDName: "text", Doc: "Text renders SVG text, handling both text and tspan elements.\ntspan is nested under a parent text, where text has empty Text string.", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Pos", Doc: "position of the left, baseline of the text"}, {Name: "Width", Doc: "width of text to render if using word-wrapping"}, {Name: "Text", Doc: "text string to render"}, {Name: "TextShaped", Doc: "render version of text"}, {Name: "CharPosX", Doc: "character positions along X axis, if specified"}, {Name: "CharPosY", Doc: "character positions along Y axis, if specified"}, {Name: "CharPosDX", Doc: "character delta-positions along X axis, if specified"}, {Name: "CharPosDY", Doc: "character delta-positions along Y axis, if specified"}, {Name: "CharRots", Doc: "character rotations, if specified"}, {Name: "TextLength", Doc: "author's computed text length, if specified -- we attempt to match"}, {Name: "AdjustGlyphs", Doc: "in attempting to match TextLength, should we adjust glyphs in addition to spacing?"}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Text", IDName: "text", Doc: "Text renders SVG text, handling both text and tspan elements.\ntspan is nested under a parent text, where text has empty Text string.\nThere is no line wrapping on SVG Text: every span is a separate line.", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Pos", Doc: "position of the left, baseline of the text"}, {Name: "Text", Doc: "text string to render"}, {Name: "TextShaped", Doc: "render version of text"}, {Name: "CharPosX", Doc: "character positions along X axis, if specified"}, {Name: "CharPosY", Doc: "character positions along Y axis, if specified"}, {Name: "CharPosDX", Doc: "character delta-positions along X axis, if specified"}, {Name: "CharPosDY", Doc: "character delta-positions along Y axis, if specified"}, {Name: "CharRots", Doc: "character rotations, if specified"}, {Name: "TextLength", Doc: "author's computed text length, if specified -- we attempt to match"}, {Name: "AdjustGlyphs", Doc: "in attempting to match TextLength, should we adjust glyphs in addition to spacing?"}}}) // NewText returns a new [Text] with the given optional parent: // Text renders SVG text, handling both text and tspan elements. // tspan is nested under a parent text, where text has empty Text string. +// There is no line wrapping on SVG Text: every span is a separate line. func NewText(parent ...tree.Node) *Text { return tree.New[Text](parent...) } // SetPos sets the [Text.Pos]: // position of the left, baseline of the text func (t *Text) SetPos(v math32.Vector2) *Text { t.Pos = v; return t } -// SetWidth sets the [Text.Width]: -// width of text to render if using word-wrapping -func (t *Text) SetWidth(v float32) *Text { t.Width = v; return t } - // SetText sets the [Text.Text]: // text string to render func (t *Text) SetText(v string) *Text { t.Text = v; return t } diff --git a/svg/urls.go b/svg/urls.go index 539c4f7dd5..df3645c041 100644 --- a/svg/urls.go +++ b/svg/urls.go @@ -241,8 +241,9 @@ func (sv *SVG) RemoveOrphanedDefs() bool { for _, k := range sv.Defs.Children { k.AsTree().SetProperty(refkey, 0) } - sv.Root.WalkDown(func(k tree.Node) bool { - pr := k.AsTree().Properties + sv.Root.WalkDown(func(n tree.Node) bool { + nb := n.(Node).AsNodeBase() + pr := nb.Properties for _, v := range pr { ps := reflectx.ToString(v) if !strings.HasPrefix(ps, "url(#") { @@ -254,15 +255,20 @@ func (sv *SVG) RemoveOrphanedDefs() bool { IncRefCount(el) } } - if gr, isgr := k.(*Gradient); isgr { + return tree.Continue + }) + sv.Defs.WalkDown(func(n tree.Node) bool { + if gr, isgr := n.(*Gradient); isgr { if gr.StopsName != "" { el := sv.FindDefByName(gr.StopsName) if el != nil { IncRefCount(el) + } else { + fmt.Println("stopsname not found:", gr.StopsName) } } else { if gr.Grad != nil && len(gr.Grad.AsBase().Stops) > 0 { - IncRefCount(k) // keep us around + IncRefCount(n) // keep us around } } } @@ -274,7 +280,7 @@ func (sv *SVG) RemoveOrphanedDefs() bool { n := sv.Defs.Children[i] rc := n.AsTree().Property(refkey).(int) if rc == 0 { - fmt.Printf("Deleting unused item: %s\n", n.AsTree().Name) + // fmt.Printf("Deleting unused item: %s\n", n.AsTree().Name) sv.Defs.Children = slices.Delete(sv.Defs.Children, i, i+1) del = true } else { diff --git a/svg/viewbox.go b/svg/viewbox.go index 41647d01a4..c72607cb88 100644 --- a/svg/viewbox.go +++ b/svg/viewbox.go @@ -14,7 +14,7 @@ import ( ) //////////////////////////////////////////////////////////////////////////////////////// -// ViewBox defines the SVG viewbox +//////// ViewBox defines the SVG viewbox // ViewBox is used in SVG to define the coordinate system type ViewBox struct { @@ -37,6 +37,10 @@ func (vb *ViewBox) Defaults() { vb.PreserveAspectRatio.MeetOrSlice = Meet } +func (vb *ViewBox) Max() math32.Vector2 { + return vb.Min.Add(vb.Size) +} + // BoxString returns the string representation of just the viewbox: // "min.X min.Y size.X size.Y" func (vb *ViewBox) BoxString() string { diff --git a/svg/zoom.go b/svg/zoom.go new file mode 100644 index 0000000000..1669618786 --- /dev/null +++ b/svg/zoom.go @@ -0,0 +1,170 @@ +// Copyright (c) 2018, 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 svg + +import ( + "image" + + "cogentcore.org/core/math32" +) + +// ContentBounds returns the bounding box of the contents +// in its natural units, without any Viewbox transformations, etc. +// Can set the Viewbox to this to have the contents fully occupy the space. +func (sv *SVG) ContentBounds() math32.Box2 { + tr := sv.Root.Paint.Transform + sv.Root.Paint.Transform = math32.Identity2() + sv.Root.BBoxes(sv, math32.Identity2()) + sv.Root.Paint.Transform = tr + return sv.Root.BBox +} + +// UpdateSize ensures that the size is valid, using existing ViewBox values +// to set proportions if size is not valid. +func (sv *SVG) UpdateSize() { + vb := &sv.Root.ViewBox + if vb.Size.X == 0 { + if sv.PhysicalWidth.Dots > 0 { + vb.Size.X = sv.PhysicalWidth.Dots + } else { + vb.Size.X = 640 + } + } + if vb.Size.Y == 0 { + if sv.PhysicalHeight.Dots > 0 { + vb.Size.Y = sv.PhysicalHeight.Dots + } else { + vb.Size.Y = 480 + } + } + if sv.Geom.Size.X > 0 && sv.Geom.Size.Y == 0 { + sv.Geom.Size.Y = int(float32(sv.Geom.Size.X) * (float32(vb.Size.Y) / float32(vb.Size.X))) + } else if sv.Geom.Size.Y > 0 && sv.Geom.Size.X == 0 { + sv.Geom.Size.X = int(float32(sv.Geom.Size.Y) * (float32(vb.Size.X) / float32(vb.Size.Y))) + } +} + +// setRootTransform sets the Root node transform based on ViewBox, Translate, Scale +// parameters set on the SVG object. +func (sv *SVG) setRootTransform() { + sv.UpdateSize() + vb := &sv.Root.ViewBox + box := math32.FromPoint(sv.Geom.Size) + tr := math32.Translate2D(float32(sv.Geom.Pos.X)+sv.Translate.X, float32(sv.Geom.Pos.Y)+sv.Translate.Y) + _, trans, scale := vb.Transform(box) + if sv.InvertY { + scale.Y *= -1 + } + trans.SetSub(vb.Min) + scale.SetMulScalar(sv.Scale) + rt := math32.Scale2D(scale.X, scale.Y).Translate(trans.X, trans.Y) + if sv.InvertY { + rt.Y0 = -rt.Y0 + } + sv.Root.Paint.Transform = tr.Mul(rt) +} + +// SetDPITransform sets a scaling transform to compensate for +// a given LogicalDPI factor. +// svg rendering is done within a 96 DPI context. +func (sv *SVG) SetDPITransform(logicalDPI float32) { + pc := &sv.Root.Paint + dpisc := logicalDPI / 96.0 + pc.Transform = math32.Scale2D(dpisc, dpisc) +} + +// ZoomAtScroll calls ZoomAt using the Delta.Y and Pos() parameters +// from an [events.MouseScroll] event, to produce well-behaved zooming behavior, +// for elements of any size. +func (sv *SVG) ZoomAtScroll(deltaY float32, pos image.Point) { + del := 0.01 * deltaY * max(sv.Root.ViewBox.Size.X/1280, 0.1) + del /= max(1, sv.Scale) + del = math32.Clamp(del, -0.1, 0.1) + sv.ZoomAt(pos, del) +} + +// ZoomAt updates the global Scale by given delta value, +// by multiplying the current Scale by 1+delta +// (+ means zoom in; - means zoom out). +// Delta should be < 1 in magnitude, and resulting scale is clamped +// in range 0.01..100. +// The global Translate is updated so that the given render +// coordinate point (dots) corresponds to the same +// underlying svg viewbox coordinate point. +func (sv *SVG) ZoomAt(pt image.Point, delta float32) { + sc := float32(1) + if delta > 1 { + sc += delta + } else { + sc *= (1 - math32.Min(-delta, .5)) + } + nsc := math32.Clamp(sv.Scale*sc, 0.01, 100) + sv.ScaleAt(pt, nsc) +} + +// ScaleAt sets the global Scale parameter and updates the +// global Translate parameter so that the given render coordinate +// point (dots) corresponds to the same underlying svg viewbox +// coordinate point. +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) + sv.Scale = sc + + sv.setRootTransform() + rxf = sv.Root.Paint.Transform + npt := rxf.MulVector2AsPoint(xpt) // original point back to screen + dpt := mpt.Sub(npt) + sv.Translate.SetAdd(dpt) +} + +func (sv *SVG) ZoomReset() { + sv.Translate.Set(0, 0) + sv.Scale = 1 +} + +// ZoomToContents sets the scale to fit the current contents +// into a display of given size. +func (sv *SVG) ZoomToContents(size math32.Vector2) { + sv.ZoomReset() + bb := sv.ContentBounds() + bsz := bb.Size().Max(math32.Vec2(1, 1)) + if bsz == (math32.Vector2{}) { + return + } + sc := size.Div(bsz) + sv.Translate = bb.Min.Negate() + sv.Scale = math32.Min(sc.X, sc.Y) +} + +// ResizeToContents resizes the drawing to just fit the current contents, +// including moving everything to start at upper-left corner. +// The given grid spacing parameter ensures sizes are in units of the grid +// spacing: pass a 1 to just use actual sizes. +func (sv *SVG) ResizeToContents(grid float32) { + sv.ZoomReset() + bb := sv.ContentBounds() + bsz := bb.Size() + if bsz.X <= 0 || bsz.Y <= 0 { + return + } + trans := bb.Min + treff := trans + if grid > 1 { + incr := grid * sv.Scale + treff.X = math32.Floor(trans.X/incr) * incr + treff.Y = math32.Floor(trans.Y/incr) * incr + bsz.SetAdd(trans.Sub(treff)) + bsz.X = math32.Ceil(bsz.X/incr) * incr + bsz.Y = math32.Ceil(bsz.Y/incr) * incr + } + root := sv.Root + root.ViewBox.Min = treff + root.ViewBox.Size = bsz + // sv.PhysicalWidth.Value = bsz.X + // sv.PhysicalHeight.Value = bsz.Y +} diff --git a/system/driver/base/window.go b/system/driver/base/window.go index 48234664f5..39732b0176 100644 --- a/system/driver/base/window.go +++ b/system/driver/base/window.go @@ -82,7 +82,8 @@ func (w *Window[A]) WinLoop() { if fps <= 0 { fps = 60 } - winPaint := time.NewTicker(time.Second / time.Duration(fps)) + dur := time.Second / time.Duration(fps) + winPaint := time.NewTicker(dur) outer: for { select { @@ -91,6 +92,7 @@ outer: break outer case <-winPaint.C: if w.This.IsClosed() { + winPaint.Stop() fmt.Println("win IsClosed in paint:", w.Name()) break outer } diff --git a/system/driver/desktop/desktop_darwin.go b/system/driver/desktop/desktop_darwin.go index d5a752efc1..7851d9dbe9 100644 --- a/system/driver/desktop/desktop_darwin.go +++ b/system/driver/desktop/desktop_darwin.go @@ -174,18 +174,16 @@ func addMimeData(ctyp *C.char, typlen C.int, cdata *C.char, datalen C.int) { *CurMimeData = append(*CurMimeData, &mimedata.Data{typ, data}) } -// TODO(kai): macOpenFile +// note: this does not happen in time for main startup +// due to the delayed initialization of all the gui stuff. //export macOpenFile func macOpenFile(fname *C.char, flen C.int) { - /* - ofn := C.GoString(fname) - // fmt.Printf("open file: %s\n", ofn) - if theApp.NWindows() == 0 { - theApp.openFiles = append(theApp.openFiles, ofn) - } else { - // win := theApp.Window(0) - // win.Events.NewOS(events.OSEvent, []string{ofn}) - } - */ + ofn := C.GoString(fname) + if TheApp.NWindows() == 0 { + TheApp.OpenFls = append(TheApp.OpenFls, ofn) + } else { + win := TheApp.Window(0) + win.Events().OSOpenFiles([]string{ofn}) + } } diff --git a/system/driver/desktop/desktop_darwin.m b/system/driver/desktop/desktop_darwin.m index 47d0f29843..bd0fe38322 100644 --- a/system/driver/desktop/desktop_darwin.m +++ b/system/driver/desktop/desktop_darwin.m @@ -181,6 +181,7 @@ @implementation GLFWCustomDelegate + (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ + Class class = objc_getClass("GLFWApplicationDelegate"); [GLFWCustomDelegate swizzle:class src:@selector(application:openFile:) tgt:@selector(swz_application:openFile:)]; @@ -210,6 +211,7 @@ + (void) swizzle:(Class) original_c src:(SEL)original_s tgt:(SEL)target_s{ } - (BOOL)swz_application:(NSApplication *)sender openFile:(NSString *)filename{ + // NSLog(@"in open file %@\n", filename); const char* utf_fn = filename.UTF8String; int flen = (int)strlen(utf_fn); macOpenFile((char*)utf_fn, flen); @@ -218,12 +220,15 @@ - (BOOL)swz_application:(NSApplication *)sender openFile:(NSString *)filename{ - (void)swz_application:(NSApplication *)sender openFiles:(NSArray *)filenames{ int n = [filenames count]; + // NSLog(@"in open files: %d nfiles\n", n); int i; for (i=0; i B or B <- A), with Undo", Fields: []types.Field{{Name: "A"}, {Name: "B"}, {Name: "Diffs", Doc: "Diffs are the diffs between A and B"}}}) -var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/lines.Lines", IDName: "lines", Doc: "Lines manages multi-line monospaced text with a given line width in runes,\nso that all text wrapping, editing, and navigation logic can be managed\npurely in text space, allowing rendering and GUI layout to be relatively fast.\nThis is suitable for text editing and terminal applications, among others.\nThe text encoded as runes along with a corresponding [rich.Text] markup\nrepresentation with syntax highlighting etc.\nThe markup is updated in a separate goroutine for efficiency.\nEverything is protected by an overall sync.Mutex and is safe to concurrent access,\nand thus nothing is exported and all access is through protected accessor functions.\nIn general, all unexported methods do NOT lock, and all exported methods do.", Methods: []types.Method{{Name: "Open", Doc: "Open loads the given file into the buffer.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename"}, Returns: []string{"error"}}, {Name: "Revert", Doc: "Revert re-opens text from the current file,\nif the filename is set; returns false if not.\nIt uses an optimized diff-based update to preserve\nexisting formatting, making it very fast if not very different.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"bool"}}}, Embeds: []types.Field{{Name: "Mutex", Doc: "use Lock(), Unlock() directly for overall mutex on any content updates"}}, Fields: []types.Field{{Name: "Settings", Doc: "Settings are the options for how text editing and viewing works."}, {Name: "Highlighter", Doc: "Highlighter does the syntax highlighting markup, and contains the\nparameters thereof, such as the language and style."}, {Name: "Autosave", Doc: "Autosave specifies whether an autosave copy of the file should\nbe automatically saved after changes are made."}, {Name: "FileModPromptFunc", Doc: "FileModPromptFunc is called when a file has been modified in the filesystem\nand it is about to be modified through an edit, in the fileModCheck function.\nThe prompt should determine whether the user wants to revert, overwrite, or\nsave current version as a different file. It must block until the user responds,\nand it is called under the mutex lock to prevent other edits."}, {Name: "fontStyle", Doc: "fontStyle is the default font styling to use for markup.\nIs set to use the monospace font."}, {Name: "undos", Doc: "undos is the undo manager."}, {Name: "filename", Doc: "filename is the filename of the file that was last loaded or saved.\nIf this is empty then no file-related functionality is engaged."}, {Name: "readOnly", Doc: "readOnly marks the contents as not editable. This is for the outer GUI\nelements to consult, and is not enforced within Lines itself."}, {Name: "fileInfo", Doc: "fileInfo is the full information about the current file, if one is set."}, {Name: "parseState", Doc: "parseState is the parsing state information for the file."}, {Name: "changed", Doc: "changed indicates whether any changes have been made.\nUse [IsChanged] method to access."}, {Name: "lines", Doc: "lines are the live lines of text being edited, with the latest modifications.\nThey are encoded as runes per line, which is necessary for one-to-one rune/glyph\nrendering correspondence. All textpos positions are in rune indexes."}, {Name: "tags", Doc: "tags are the extra custom tagged regions for each line."}, {Name: "hiTags", Doc: "hiTags are the syntax highlighting tags, which are auto-generated."}, {Name: "markup", Doc: "markup is the [rich.Text] encoded marked-up version of the text lines,\nwith the results of syntax highlighting. It just has the raw markup without\nadditional layout for a specific line width, which goes in a [view]."}, {Name: "views", Doc: "views are the distinct views of the lines, accessed via a unique view handle,\nwhich is the key in the map. Each view can have its own width, and thus its own\nmarkup and layout."}, {Name: "lineColors", Doc: "lineColors associate a color with a given line number (key of map),\ne.g., for a breakpoint or other such function."}, {Name: "markupEdits", Doc: "markupEdits are the edits that were made during the time it takes to generate\nthe new markup tags. this is rare but it does happen."}, {Name: "markupDelayTimer", Doc: "markupDelayTimer is the markup delay timer."}, {Name: "markupDelayMu", Doc: "markupDelayMu is the mutex for updating the markup delay timer."}, {Name: "posHistory", Doc: "posHistory is the history of cursor positions.\nIt can be used to move back through them."}, {Name: "batchUpdating", Doc: "batchUpdating indicates that a batch update is under way,\nso Input signals are not sent until the end."}, {Name: "autoSaving", Doc: "autoSaving is used in atomically safe way to protect autosaving"}, {Name: "notSaved", Doc: "notSaved indicates if the text has been changed (edited) relative to the\noriginal, since last Save. This can be true even when changed flag is\nfalse, because changed is cleared on EditDone, e.g., when texteditor\nis being monitored for OnChange and user does Control+Enter.\nUse IsNotSaved() method to query state."}, {Name: "fileModOK", Doc: "fileModOK have already asked about fact that file has changed since being\nopened, user is ok"}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/lines.Lines", IDName: "lines", Doc: "Lines manages multi-line monospaced text with a given line width in runes,\nso that all text wrapping, editing, and navigation logic can be managed\npurely in text space, allowing rendering and GUI layout to be relatively fast.\nThis is suitable for text editing and terminal applications, among others.\nThe text encoded as runes along with a corresponding [rich.Text] markup\nrepresentation with syntax highlighting etc.\nThe markup is updated in a separate goroutine for efficiency.\nEverything is protected by an overall sync.Mutex and is safe to concurrent access,\nand thus nothing is exported and all access is through protected accessor functions.\nIn general, all unexported methods do NOT lock, and all exported methods do.", Methods: []types.Method{{Name: "Open", Doc: "Open loads the given file into the buffer.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename"}, Returns: []string{"error"}}, {Name: "Revert", Doc: "Revert re-opens text from the current file,\nif the filename is set; returns false if not.\nIt uses an optimized diff-based update to preserve\nexisting formatting, making it very fast if not very different.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"bool"}}}, Embeds: []types.Field{{Name: "Mutex", Doc: "use Lock(), Unlock() directly for overall mutex on any content updates"}}, Fields: []types.Field{{Name: "Settings", Doc: "Settings are the options for how text editing and viewing works."}, {Name: "Highlighter", Doc: "Highlighter does the syntax highlighting markup, and contains the\nparameters thereof, such as the language and style."}, {Name: "Autosave", Doc: "Autosave specifies whether an autosave copy of the file should\nbe automatically saved after changes are made."}, {Name: "FileModPromptFunc", Doc: "FileModPromptFunc is called when a file has been modified in the filesystem\nand it is about to be modified through an edit, in the fileModCheck function.\nThe prompt should determine whether the user wants to revert, overwrite, or\nsave current version as a different file. It must block until the user responds."}, {Name: "Meta", Doc: "Meta can be used to maintain misc metadata associated with the Lines text,\nwhich allows the Lines object to be the primary data type for applications\ndealing with text data, if there are just a few additional data elements needed.\nUse standard Go camel-case key names, standards in [metadata]."}, {Name: "fontStyle", Doc: "fontStyle is the default font styling to use for markup.\nIs set to use the monospace font."}, {Name: "undos", Doc: "undos is the undo manager."}, {Name: "filename", Doc: "filename is the filename of the file that was last loaded or saved.\nIf this is empty then no file-related functionality is engaged."}, {Name: "readOnly", Doc: "readOnly marks the contents as not editable. This is for the outer GUI\nelements to consult, and is not enforced within Lines itself."}, {Name: "fileInfo", Doc: "fileInfo is the full information about the current file, if one is set."}, {Name: "parseState", Doc: "parseState is the parsing state information for the file."}, {Name: "changed", Doc: "changed indicates whether any changes have been made.\nUse [IsChanged] method to access."}, {Name: "lines", Doc: "lines are the live lines of text being edited, with the latest modifications.\nThey are encoded as runes per line, which is necessary for one-to-one rune/glyph\nrendering correspondence. All textpos positions are in rune indexes."}, {Name: "tags", Doc: "tags are the extra custom tagged regions for each line."}, {Name: "hiTags", Doc: "hiTags are the syntax highlighting tags, which are auto-generated."}, {Name: "markup", Doc: "markup is the [rich.Text] encoded marked-up version of the text lines,\nwith the results of syntax highlighting. It just has the raw markup without\nadditional layout for a specific line width, which goes in a [view]."}, {Name: "views", Doc: "views are the distinct views of the lines, accessed via a unique view handle,\nwhich is the key in the map. Each view can have its own width, and thus its own\nmarkup and layout."}, {Name: "lineColors", Doc: "lineColors associate a color with a given line number (key of map),\ne.g., for a breakpoint or other such function."}, {Name: "markupEdits", Doc: "markupEdits are the edits that were made during the time it takes to generate\nthe new markup tags. this is rare but it does happen."}, {Name: "markupDelayTimer", Doc: "markupDelayTimer is the markup delay timer."}, {Name: "markupDelayMu", Doc: "markupDelayMu is the mutex for updating the markup delay timer."}, {Name: "posHistory", Doc: "posHistory is the history of cursor positions.\nIt can be used to move back through them."}, {Name: "links", Doc: "links is the collection of all hyperlinks within the markup source,\nindexed by the markup source line.\nonly updated at the full markup sweep."}, {Name: "batchUpdating", Doc: "batchUpdating indicates that a batch update is under way,\nso Input signals are not sent until the end."}, {Name: "autoSaving", Doc: "autoSaving is used in atomically safe way to protect autosaving"}, {Name: "notSaved", Doc: "notSaved indicates if the text has been changed (edited) relative to the\noriginal, since last Save. This can be true even when changed flag is\nfalse, because changed is cleared on EditDone, e.g., when texteditor\nis being monitored for OnChange and user does Control+Enter.\nUse IsNotSaved() method to query state."}, {Name: "fileModOK", Doc: "fileModOK have already asked about fact that file has changed since being\nopened, user is ok"}}}) var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/lines.Settings", IDName: "settings", Doc: "Settings contains settings for editing text lines.", Embeds: []types.Field{{Name: "EditorSettings"}}, Fields: []types.Field{{Name: "CommentLine", Doc: "CommentLine are character(s) that start a single-line comment;\nif empty then multi-line comment syntax will be used."}, {Name: "CommentStart", Doc: "CommentStart are character(s) that start a multi-line comment\nor one that requires both start and end."}, {Name: "CommentEnd", Doc: "Commentend are character(s) that end a multi-line comment\nor one that requires both start and end."}}}) var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/lines.Undo", IDName: "undo", Doc: "Undo is the textview.Buf undo manager", Fields: []types.Field{{Name: "Off", Doc: "if true, saving and using undos is turned off (e.g., inactive buffers)"}, {Name: "Stack", Doc: "undo stack of edits"}, {Name: "UndoStack", Doc: "undo stack of *undo* edits -- added to whenever an Undo is done -- for emacs-style undo"}, {Name: "Pos", Doc: "undo position in stack"}, {Name: "Group", Doc: "group counter"}, {Name: "Mu", Doc: "mutex protecting all updates"}}}) -var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/lines.view", IDName: "view", Doc: "view provides a view onto a shared [Lines] text buffer,\nwith a representation of view lines that are the wrapped versions of\nthe original [Lines.lines] source lines, with wrapping according to\nthe view width. Views are managed by the Lines.", Fields: []types.Field{{Name: "width", Doc: "width is the current line width in rune characters, used for line wrapping."}, {Name: "viewLines", Doc: "viewLines is the total number of line-wrapped lines."}, {Name: "vlineStarts", Doc: "vlineStarts are the positions in the original [Lines.lines] source for\nthe start of each view line. This slice is viewLines in length."}, {Name: "markup", Doc: "markup is the view-specific version of the [Lines.markup] markup for\neach view line (len = viewLines)."}, {Name: "lineToVline", Doc: "lineToVline maps the source [Lines.lines] indexes to the wrapped\nviewLines. Each slice value contains the index into the viewLines space,\nsuch that vlineStarts of that index is the start of the original source line.\nAny subsequent vlineStarts with the same Line and Char > 0 following this\nstarting line represent additional wrapped content from the same source line."}, {Name: "listeners", Doc: "listeners is used for sending Change and Input events"}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/lines.view", IDName: "view", Doc: "view provides a view onto a shared [Lines] text buffer,\nwith a representation of view lines that are the wrapped versions of\nthe original [Lines.lines] source lines, with wrapping according to\nthe view width. Views are managed by the Lines.", Fields: []types.Field{{Name: "width", Doc: "width is the current line width in rune characters, used for line wrapping."}, {Name: "viewLines", Doc: "viewLines is the total number of line-wrapped lines."}, {Name: "vlineStarts", Doc: "vlineStarts are the positions in the original [Lines.lines] source for\nthe start of each view line. This slice is viewLines in length."}, {Name: "markup", Doc: "markup is the view-specific version of the [Lines.markup] markup for\neach view line (len = viewLines)."}, {Name: "lineToVline", Doc: "lineToVline maps the source [Lines.lines] indexes to the wrapped\nviewLines. Each slice value contains the index into the viewLines space,\nsuch that vlineStarts of that index is the start of the original source line.\nAny subsequent vlineStarts with the same Line and Char > 0 following this\nstarting line represent additional wrapped content from the same source line."}, {Name: "listeners", Doc: "listeners is used for sending Change, Input, and Close events to views."}}}) diff --git a/text/rich/enumgen.go b/text/rich/enumgen.go index 0087320785..5042d76da8 100644 --- a/text/rich/enumgen.go +++ b/text/rich/enumgen.go @@ -166,16 +166,16 @@ func (i Stretch) MarshalText() ([]byte, error) { return []byte(i.String()), nil // UnmarshalText implements the [encoding.TextUnmarshaler] interface. func (i *Stretch) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Stretch") } -var _DecorationsValues = []Decorations{0, 1, 2, 3, 4, 5, 6, 7} +var _DecorationsValues = []Decorations{0, 1, 2, 3, 4} // DecorationsN is the highest valid value for type Decorations, plus one. -const DecorationsN Decorations = 8 +const DecorationsN Decorations = 5 -var _DecorationsValueMap = map[string]Decorations{`underline`: 0, `overline`: 1, `line-through`: 2, `dotted-underline`: 3, `paragraph-start`: 4, `fill-color`: 5, `stroke-color`: 6, `background`: 7} +var _DecorationsValueMap = map[string]Decorations{`underline`: 0, `overline`: 1, `line-through`: 2, `dotted-underline`: 3, `paragraph-start`: 4} -var _DecorationsDescMap = map[Decorations]string{0: `Underline indicates to place a line below text.`, 1: `Overline indicates to place a line above text.`, 2: `LineThrough indicates to place a line through text.`, 3: `DottedUnderline is used for abbr tag.`, 4: `ParagraphStart indicates that this text is the start of a paragraph, and therefore may be indented according to [text.Style] settings.`, 5: `FillColor means that the fill color of the glyph is set. The standard font rendering uses this fill color (compare to StrokeColor).`, 6: `StrokeColor means that the stroke color of the glyph is set. This is normally not rendered: it looks like an outline of the glyph at larger font sizes, and will make smaller font sizes look significantly thicker.`, 7: `Background means that the background region behind the text is colored. The background is not normally colored so it renders over any background.`} +var _DecorationsDescMap = map[Decorations]string{0: `Underline indicates to place a line below text.`, 1: `Overline indicates to place a line above text.`, 2: `LineThrough indicates to place a line through text.`, 3: `DottedUnderline is used for abbr tag.`, 4: `ParagraphStart indicates that this text is the start of a paragraph, and therefore may be indented according to [text.Style] settings.`} -var _DecorationsMap = map[Decorations]string{0: `underline`, 1: `overline`, 2: `line-through`, 3: `dotted-underline`, 4: `paragraph-start`, 5: `fill-color`, 6: `stroke-color`, 7: `background`} +var _DecorationsMap = map[Decorations]string{0: `underline`, 1: `overline`, 2: `line-through`, 3: `dotted-underline`, 4: `paragraph-start`} // String returns the string representation of this Decorations value. func (i Decorations) String() string { return enums.BitFlagString(i, _DecorationsValues) } diff --git a/text/rich/props.go b/text/rich/props.go index 8cef3a51af..b5c64ab88e 100644 --- a/text/rich/props.go +++ b/text/rich/props.go @@ -82,27 +82,29 @@ var styleFuncs = map[string]styleprops.Func{ func(obj *Style) enums.EnumSetter { return &obj.Stretch }), "text-decoration": func(obj any, key string, val any, parent any, cc colors.Context) { fs := obj.(*Style) + cflags := fs.Decoration & colorFlagsMask if inh, init := styleprops.InhInit(val, parent); inh || init { if inh { fs.Decoration = parent.(*Style).Decoration } else if init { - fs.Decoration = 0 + fs.Decoration = cflags } return } switch vt := val.(type) { case string: if vt == "none" { - fs.Decoration = 0 + fs.Decoration = cflags } else { fs.Decoration.SetString(vt) + fs.Decoration |= cflags } case Decorations: - fs.Decoration = vt + fs.Decoration = cflags | vt default: iv, err := reflectx.ToInt(val) if err == nil { - fs.Decoration = Decorations(iv) + fs.Decoration = cflags | Decorations(iv) } else { styleprops.SetError(key, val, err) } diff --git a/text/rich/srune.go b/text/rich/srune.go index 5cc4cc4700..812829e1c0 100644 --- a/text/rich/srune.go +++ b/text/rich/srune.go @@ -57,15 +57,15 @@ func (s *Style) FromRunes(rs []rune) []rune { RuneToStyle(s, rs[0]) s.Size = math.Float32frombits(uint32(rs[1])) ci := 2 - if s.Decoration.HasFlag(FillColor) { + if s.Decoration.HasFlag(fillColor) { s.fillColor = ColorFromRune(rs[ci]) ci++ } - if s.Decoration.HasFlag(StrokeColor) { + if s.Decoration.HasFlag(strokeColor) { s.strokeColor = ColorFromRune(rs[ci]) ci++ } - if s.Decoration.HasFlag(Background) { + if s.Decoration.HasFlag(background) { s.background = ColorFromRune(rs[ci]) ci++ } @@ -90,13 +90,13 @@ func (s *Style) ToRunes() []rune { if s.Decoration.NumColors() == 0 { return rs } - if s.Decoration.HasFlag(FillColor) { + if s.Decoration.HasFlag(fillColor) { rs = append(rs, ColorToRune(s.fillColor)) } - if s.Decoration.HasFlag(StrokeColor) { + if s.Decoration.HasFlag(strokeColor) { rs = append(rs, ColorToRune(s.strokeColor)) } - if s.Decoration.HasFlag(Background) { + if s.Decoration.HasFlag(background) { rs = append(rs, ColorToRune(s.background)) } if s.Special == Link { diff --git a/text/rich/style.go b/text/rich/style.go index 60d7e77828..430ba8ccea 100644 --- a/text/rich/style.go +++ b/text/rich/style.go @@ -58,17 +58,17 @@ type Style struct { //types:add -setters Direction Directions // fillColor is the color to use for glyph fill (i.e., the standard "ink" color). - // Must use SetFillColor to set Decoration FillColor flag. + // Must use SetFillColor to set Decoration fillColor flag. // This will be encoded in a uint32 following the style rune, in rich.Text spans. fillColor color.Color // strokeColor is the color to use for glyph outline stroking. - // Must use SetStrokeColor to set Decoration StrokeColor flag. + // Must use SetStrokeColor to set Decoration strokeColor flag. // This will be encoded in a uint32 following the style rune, in rich.Text spans. strokeColor color.Color // background is the color to use for the background region. - // Must use SetBackground to set the Decoration Background flag. + // Must use SetBackground to set the Decoration background flag. // This will be encoded in a uint32 following the style rune, in rich.Text spans. background color.Color `set:"-"` @@ -298,30 +298,33 @@ const ( // and therefore may be indented according to [text.Style] settings. ParagraphStart - // FillColor means that the fill color of the glyph is set. + // fillColor means that the fill color of the glyph is set. // The standard font rendering uses this fill color (compare to StrokeColor). - FillColor + fillColor - // StrokeColor means that the stroke color of the glyph is set. + // strokeColor means that the stroke color of the glyph is set. // This is normally not rendered: it looks like an outline of the glyph at // larger font sizes, and will make smaller font sizes look significantly thicker. - StrokeColor + strokeColor - // Background means that the background region behind the text is colored. + // background means that the background region behind the text is colored. // The background is not normally colored so it renders over any background. - Background + background + + // colorFlagsMask is a mask for the color flags, to exclude them as needed. + colorFlagsMask = 1< len(stx) { + rng.End -= (ri + rng.Len()) - len(stx) + // fmt.Println("shape range err:", tx, string(stx), len(stx), si, sn, ri, rng) + // return + } rtx := stx[ri : ri+rng.Len()] SetFontStyle(ctx, fnt, tsty, 0) diff --git a/text/spell/spell.go b/text/spell/spell.go index 5f5e3aa008..f87c85c0d1 100644 --- a/text/spell/spell.go +++ b/text/spell/spell.go @@ -143,8 +143,8 @@ func (sp *SpellData) SaveUserIfLearn() error { // CheckWord checks a single word and returns suggestions if word is unknown. // bool is true if word is in the dictionary, false otherwise. func (sp *SpellData) CheckWord(word string) ([]string, bool) { - if sp.model == nil { - log.Println("spell.CheckWord: programmer error -- Spelling not initialized!") + if sp == nil || sp.model == nil { + // log.Println("spell.CheckWord: programmer error -- Spelling not initialized!") return nil, false } w := lexer.FirstWordApostrophe(word) // only lookup words diff --git a/text/text/props.go b/text/text/props.go index 03ba113bef..4a4845ca1f 100644 --- a/text/text/props.go +++ b/text/text/props.go @@ -109,8 +109,10 @@ func (s *Style) ToProperties(sty *rich.Style, p map[string]any) { if sty.Stretch != rich.StretchNormal { p["font-stretch"] = sty.Stretch.String() } - if sty.Decoration != 0 { - p["text-decoration"] = sty.Decoration.String() + if dstr := sty.Decoration.String(); dstr != "" { + p["text-decoration"] = dstr + } else { + p["text-decoration"] = "none" } if s.Align != Start { p["text-align"] = s.Align.String() @@ -118,7 +120,7 @@ func (s *Style) ToProperties(sty *rich.Style, p map[string]any) { if s.AlignV != Start { p["text-vertical-align"] = s.AlignV.String() } - if s.LineHeight != 1.2 { + if s.LineHeight != 1.3 { p["line-height"] = reflectx.ToString(s.LineHeight) } if s.WhiteSpace != WrapAsNeeded { @@ -130,10 +132,9 @@ func (s *Style) ToProperties(sty *rich.Style, p map[string]any) { if s.TabSize != 4 { p["tab-size"] = reflectx.ToString(s.TabSize) } - if sty.Decoration.HasFlag(rich.FillColor) { - p["fill"] = colors.AsHex(sty.FillColor()) - } else { - p["fill"] = colors.AsHex(s.Color) + p["fill"] = colors.AsHex(s.FillColor(sty)) + if sc := sty.StrokeColor(); sc != nil { + p["stroke-color"] = colors.AsHex(sc) } if s.SelectColor != nil { p["select-color"] = colors.AsHex(colors.ToUniform(s.SelectColor)) diff --git a/text/text/style.go b/text/text/style.go index b8a2765968..b72845749b 100644 --- a/text/text/style.go +++ b/text/text/style.go @@ -132,6 +132,17 @@ func (ts *Style) FontHeight(sty *rich.Style) float32 { return math32.Round(ts.FontSize.Dots * sty.Size) } +// FillColor returns the effective text fill color (main color) +// using any special setting in the given [rich.Style], falling back +// on the Color setting on this text style. +func (ts *Style) FillColor(sty *rich.Style) color.Color { + fc := sty.FillColor() + if fc != nil { + return fc + } + return ts.Color +} + // LineHeightDots returns the effective line height in dots (actual pixels) // as FontHeight * LineHeight func (ts *Style) LineHeightDots(sty *rich.Style) float32 { diff --git a/text/textcore/base.go b/text/textcore/base.go index f66b9e3d8a..755ece000d 100644 --- a/text/textcore/base.go +++ b/text/textcore/base.go @@ -266,7 +266,6 @@ func (ed *Base) editDone() { ed.Lines.EditDone() // sends the change event } ed.clearSelected() - ed.clearCursor() } // reMarkup triggers a complete re-markup of the entire text -- diff --git a/text/textcore/cursor.go b/text/textcore/cursor.go index 4915dfbd9c..cafeed7d73 100644 --- a/text/textcore/cursor.go +++ b/text/textcore/cursor.go @@ -5,48 +5,22 @@ package textcore import ( - "fmt" "image" - "image/draw" + "time" "cogentcore.org/core/core" "cogentcore.org/core/math32" - "cogentcore.org/core/styles/states" - "cogentcore.org/core/system" + "cogentcore.org/core/paint" "cogentcore.org/core/text/textpos" ) var ( - // blinker manages cursor blinking - blinker = core.Blinker{} - // blinkerSpriteName is the name of the window sprite used for the cursor blinkerSpriteName = "textcore.Base.Cursor" ) -func init() { - core.TheApp.AddQuitCleanFunc(blinker.QuitClean) - blinker.Func = func() { - w := blinker.Widget - blinker.Unlock() // comes in locked - if w == nil { - return - } - ed := AsBase(w) - if !ed.StateIs(states.Focused) || !ed.IsVisible() { - ed.blinkOn = false - ed.renderCursor(false) - } else { - // Need consistent test results on offscreen. - if core.TheApp.Platform() != system.Offscreen { - ed.blinkOn = !ed.blinkOn - } - ed.renderCursor(ed.blinkOn) - } - } -} - -// startCursor starts the cursor blinking and renders it +// startCursor starts the cursor blinking and renders it. +// This must be called to update the cursor position -- is called in render. func (ed *Base) startCursor() { if ed == nil || ed.This == nil { return @@ -54,105 +28,90 @@ func (ed *Base) startCursor() { if !ed.IsVisible() { return } - ed.blinkOn = true - ed.renderCursor(true) - if core.SystemSettings.CursorBlinkTime == 0 { - return - } - blinker.SetWidget(ed.This.(core.Widget)) - blinker.Blink(core.SystemSettings.CursorBlinkTime) -} - -// clearCursor turns off cursor and stops it from blinking -func (ed *Base) clearCursor() { - ed.stopCursor() - ed.renderCursor(false) + ed.toggleCursor(true) } // stopCursor stops the cursor from blinking func (ed *Base) stopCursor() { - if ed == nil || ed.This == nil { - return - } - blinker.ResetWidget(ed.This.(core.Widget)) + ed.toggleCursor(false) } -// cursorBBox returns a bounding-box for a cursor at given position -func (ed *Base) cursorBBox(pos textpos.Pos) image.Rectangle { - cpos := ed.charStartPos(pos) - cbmin := cpos.SubScalar(ed.CursorWidth.Dots) - cbmax := cpos.AddScalar(ed.CursorWidth.Dots) - cbmax.Y += ed.charSize.Y - curBBox := image.Rectangle{cbmin.ToPointFloor(), cbmax.ToPointCeil()} - return curBBox -} - -// renderCursor renders the cursor on or off, as a sprite that is either on or off -func (ed *Base) renderCursor(on bool) { - if ed == nil || ed.This == nil { - return - } - if ed.Scene == nil || ed.Scene.Stage == nil || ed.Scene.Stage.Main == nil { - return - } - ms := ed.Scene.Stage.Main - if !on { - spnm := ed.cursorSpriteName() - ms.Sprites.InactivateSprite(spnm) - return - } - if !ed.IsVisible() { +// toggleSprite turns on or off the cursor sprite. +func (ed *Base) toggleCursor(on bool) { + sc := ed.Scene + if sc == nil || sc.Stage == nil { return } - if !ed.posIsVisible(ed.CursorPos) { - return + ms := sc.Stage.Main + if ms == nil { + return // only MainStage has sprites } - ed.cursorMu.Lock() - defer ed.cursorMu.Unlock() + spnm := blinkerSpriteName + ms.Sprites.Lock() + defer ms.Sprites.Unlock() - sp := ed.cursorSprite(on) - if sp == nil { - return - } - sp.Geom.Pos = ed.charStartPos(ed.CursorPos).ToPointFloor() -} + sp, ok := ms.Sprites.SpriteByNameLocked(spnm) -// cursorSpriteName returns the name of the cursor sprite -func (ed *Base) cursorSpriteName() string { - spnm := fmt.Sprintf("%v-%v", blinkerSpriteName, ed.charSize.Y) - return spnm -} + activate := func() { + sp.EventBBox.Min = ed.charStartPos(ed.CursorPos).ToPointFloor() + sp.Active = true + sp.Properties["turnOn"] = true + sp.Properties["on"] = true + sp.Properties["lastSwitch"] = time.Now() + } -// cursorSprite returns the sprite for the cursor, which is -// only rendered once with a vertical bar, and just activated and inactivated -// depending on render status. -func (ed *Base) cursorSprite(on bool) *core.Sprite { - sc := ed.Scene - if sc == nil || sc.Stage == nil || sc.Stage.Main == nil { - return nil + if ok { + if on { + activate() + } else { + sp.Active = false + } + return } - ms := sc.Stage.Main - if ms == nil { - return nil // only MainStage has sprites + if !on { + return } - spnm := ed.cursorSpriteName() - sp, ok := ms.Sprites.SpriteByName(spnm) - if !ok { - bbsz := image.Point{int(math32.Ceil(ed.CursorWidth.Dots)), int(math32.Ceil(ed.charSize.Y))} + sp = core.NewSprite(spnm, func(pc *paint.Painter) { + if !sp.Active { + return + } + turnOn := sp.Properties["turnOn"].(bool) // force on + if !turnOn { + isOn := sp.Properties["on"].(bool) + lastSwitch := sp.Properties["lastSwitch"].(time.Time) + if time.Since(lastSwitch) > core.SystemSettings.CursorBlinkTime { + isOn = !isOn + sp.Properties["on"] = isOn + sp.Properties["lastSwitch"] = time.Now() + } + if !isOn { + return + } + } + bbsz := math32.Vec2(math32.Ceil(ed.CursorWidth.Dots), math32.Ceil(ed.charSize.Y)) if bbsz.X < 2 { // at least 2 bbsz.X = 2 } - sp = core.NewSprite(spnm, bbsz, image.Point{}) - if ed.CursorColor != nil { - ibox := sp.Pixels.Bounds() - draw.Draw(sp.Pixels, ibox, ed.CursorColor, image.Point{}, draw.Src) - ms.Sprites.Add(sp) - } - } - if on { - ms.Sprites.ActivateSprite(sp.Name) - } else { - ms.Sprites.InactivateSprite(sp.Name) - } - return sp + sp.Properties["turnOn"] = false + pc.Fill.Color = nil + pc.Stroke.Color = ed.CursorColor + pc.Stroke.Dashes = nil + pc.Stroke.Width.Dot(bbsz.X) + pos := math32.FromPoint(sp.EventBBox.Min) + pc.Line(pos.X, pos.Y, pos.X, pos.Y+bbsz.Y) + pc.Draw() + }) + sp.InitProperties() + activate() + ms.Sprites.AddLocked(sp) +} + +// cursorBBox returns a bounding-box for a cursor at given position +func (ed *Base) cursorBBox(pos textpos.Pos) image.Rectangle { + cpos := ed.charStartPos(pos) + cbmin := cpos.SubScalar(ed.CursorWidth.Dots) + cbmax := cpos.AddScalar(ed.CursorWidth.Dots) + cbmax.Y += ed.charSize.Y + curBBox := image.Rectangle{cbmin.ToPointFloor(), cbmax.ToPointCeil()} + return curBBox } diff --git a/text/textcore/editor.go b/text/textcore/editor.go index cfce706dbf..d13f423b58 100644 --- a/text/textcore/editor.go +++ b/text/textcore/editor.go @@ -130,7 +130,6 @@ func (ed *Editor) Save() error { //types:add // in an OnChange event handler, unlike [Editor.Save]. func (ed *Editor) SaveQuiet() error { //types:add ed.clearSelected() - ed.clearCursor() return Save(ed.Scene, ed.Lines) } @@ -143,7 +142,7 @@ func (ed *Editor) Close(afterFunc func(canceled bool)) bool { func (ed *Editor) handleFocus() { ed.OnFocusLost(func(e events.Event) { if ed.IsReadOnly() { - ed.clearCursor() + ed.stopCursor() return } if ed.AbilityIs(abilities.Focusable) { @@ -151,6 +150,11 @@ func (ed *Editor) handleFocus() { ed.SetState(false, states.Focused) } }) + ed.OnFocus(func(e events.Event) { + if !ed.IsReadOnly() && ed.AbilityIs(abilities.Focusable) { + ed.startCursor() + } + }) } func (ed *Editor) handleKeyChord() { @@ -478,7 +482,7 @@ func (ed *Editor) keyInput(e events.Event) { if !lasttab && ed.CursorPos.Char == 0 && ed.Lines.Settings.AutoIndent { _, _, cpos := ed.Lines.AutoIndent(ed.CursorPos.Line) ed.CursorPos.Char = cpos - ed.renderCursor(true) + ed.startCursor() gotTabAI = true } else { ed.InsertAtCursor(indent.Bytes(ed.Lines.Settings.IndentChar(), 1, ed.Styles.Text.TabSize)) @@ -567,41 +571,44 @@ func (ed *Editor) keyInputInsertRune(kt events.Event) { if ed.ISearch.On { ed.CancelComplete() ed.iSearchKeyInput(kt) - } else if ed.QReplace.On { + return + } + if ed.QReplace.On { ed.CancelComplete() ed.qReplaceKeyInput(kt) - } else { - if kt.KeyRune() == '{' || kt.KeyRune() == '(' || kt.KeyRune() == '[' { - ed.keyInputInsertBracket(kt) - } else if kt.KeyRune() == '}' && ed.Lines.Settings.AutoIndent && ed.CursorPos.Char == ed.Lines.LineLen(ed.CursorPos.Line) { + return + } + switch { + case kt.KeyRune() == '{' || kt.KeyRune() == '(' || kt.KeyRune() == '[': + ed.keyInputInsertBracket(kt) + case kt.KeyRune() == '}' && ed.Lines.Settings.AutoIndent && ed.CursorPos.Char == ed.Lines.LineLen(ed.CursorPos.Line): + ed.CancelComplete() + ed.lastAutoInsert = 0 + ed.InsertAtCursor([]byte(string(kt.KeyRune()))) + tbe, _, cpos := ed.Lines.AutoIndent(ed.CursorPos.Line) + if tbe != nil { + ed.SetCursorShow(textpos.Pos{Line: tbe.Region.End.Line, Char: cpos}) + } + case ed.lastAutoInsert == kt.KeyRune(): // if we type what we just inserted, just move past + ed.CursorPos.Char++ + ed.SetCursorShow(ed.CursorPos) + ed.lastAutoInsert = 0 + default: + ed.lastAutoInsert = 0 + ed.InsertAtCursor([]byte(string(kt.KeyRune()))) + if kt.KeyRune() == ' ' { ed.CancelComplete() - ed.lastAutoInsert = 0 - ed.InsertAtCursor([]byte(string(kt.KeyRune()))) - tbe, _, cpos := ed.Lines.AutoIndent(ed.CursorPos.Line) - if tbe != nil { - ed.SetCursorShow(textpos.Pos{Line: tbe.Region.End.Line, Char: cpos}) - } - } else if ed.lastAutoInsert == kt.KeyRune() { // if we type what we just inserted, just move past - ed.CursorPos.Char++ - ed.SetCursorShow(ed.CursorPos) - ed.lastAutoInsert = 0 } else { - ed.lastAutoInsert = 0 - ed.InsertAtCursor([]byte(string(kt.KeyRune()))) - if kt.KeyRune() == ' ' { - ed.CancelComplete() - } else { - ed.offerComplete() - } + ed.offerComplete() } - if kt.KeyRune() == '}' || kt.KeyRune() == ')' || kt.KeyRune() == ']' { - cp := ed.CursorPos - np := cp - np.Char-- - tp, found := ed.Lines.BraceMatchRune(kt.KeyRune(), np) - if found { - ed.addScopelights(np, tp) - } + } + if kt.KeyRune() == '}' || kt.KeyRune() == ')' || kt.KeyRune() == ']' { + cp := ed.CursorPos + np := cp + np.Char-- + tp, found := ed.Lines.BraceMatchRune(kt.KeyRune(), np) + if found { + ed.addScopelights(np, tp) } } } @@ -665,6 +672,7 @@ func (ed *Editor) handleMouse() { ed.SetState(true, states.Sliding) ed.isScrolling = true pt := ed.PointToRelPos(e.Pos()) + pt.X -= int(0.8 * ed.charSize.X) // favor inclusive positioning on left newPos := ed.PixelToCursor(pt) if ed.selectMode || e.SelectMode() != events.SelectOne { // extend existing select ed.setCursorFromMouse(pt, newPos, e.SelectMode()) diff --git a/text/textcore/find.go b/text/textcore/find.go index b65a337441..d44f48c66a 100644 --- a/text/textcore/find.go +++ b/text/textcore/find.go @@ -339,6 +339,7 @@ func (ed *Editor) QReplaceStart(find, repl string, lexItems bool) { ed.QReplace.pos, _ = ed.matchFromPos(ed.QReplace.Matches, ed.CursorPos) ed.qReplaceSelectMatch(ed.QReplace.pos) ed.qReplaceEvent() + ed.NeedsRender() } // qReplaceMatches finds QReplace matches -- returns true if there are any diff --git a/text/textcore/nav.go b/text/textcore/nav.go index c266056944..bc1015326e 100644 --- a/text/textcore/nav.go +++ b/text/textcore/nav.go @@ -32,6 +32,7 @@ func (ed *Base) setCursor(pos textpos.Pos) { } ed.scopelightsReset() ed.CursorPos = ed.Lines.ValidPos(pos) + ed.startCursor() bm, has := ed.Lines.BraceMatch(pos) if has { ed.addScopelights(pos, bm) @@ -44,7 +45,6 @@ func (ed *Base) setCursor(pos textpos.Pos) { func (ed *Base) SetCursorShow(pos textpos.Pos) { ed.setCursor(pos) ed.scrollCursorToCenterIfHidden() - ed.renderCursor(true) } // SetCursorTarget sets a new cursor target position, ensures that it is visible. @@ -97,7 +97,7 @@ func (ed *Base) CursorToHistoryPrev() bool { ed.posHistoryIndex-- } ed.scrollCursorToCenterIfHidden() - ed.renderCursor(true) + ed.startCursor() ed.SendInput() return true } @@ -120,7 +120,7 @@ func (ed *Base) CursorToHistoryNext() bool { pos, _ := ed.Lines.PosHistoryAt(ed.posHistoryIndex) ed.CursorPos = ed.Lines.ValidPos(pos) ed.scrollCursorToCenterIfHidden() - ed.renderCursor(true) + ed.startCursor() ed.SendInput() return true } @@ -206,7 +206,7 @@ func (ed *Base) cursorPageDown(steps int) { } ed.setCursor(ed.CursorPos) ed.scrollCursorToTop() - ed.renderCursor(true) + ed.startCursor() ed.cursorSelect(org) ed.SendInput() ed.NeedsRender() @@ -231,7 +231,7 @@ func (ed *Base) cursorPageUp(steps int) { } ed.setCursor(ed.CursorPos) ed.scrollCursorToBottom() - ed.renderCursor(true) + ed.startCursor() ed.cursorSelect(org) ed.SendInput() ed.NeedsRender() @@ -310,7 +310,7 @@ func (ed *Base) cursorBackspace(steps int) { // note: no update b/c signal from buf will drive update ed.cursorBackward(steps) ed.scrollCursorToCenterIfHidden() - ed.renderCursor(true) + ed.startCursor() ed.Lines.DeleteText(ed.CursorPos, org) ed.NeedsRender() } @@ -339,7 +339,7 @@ func (ed *Base) cursorBackspaceWord(steps int) { } ed.cursorBackwardWord(steps) ed.scrollCursorToCenterIfHidden() - ed.renderCursor(true) + ed.startCursor() ed.Lines.DeleteText(ed.CursorPos, org) ed.NeedsRender() } @@ -405,7 +405,7 @@ func (ed *Base) setCursorFromMouse(pt image.Point, newPos textpos.Pos, selMode e } ed.setCursor(newPos) ed.selectRegionUpdate(ed.CursorPos) - ed.renderCursor(true) + ed.startCursor() return } diff --git a/text/textcore/render.go b/text/textcore/render.go index c13d7d6930..d38f06782d 100644 --- a/text/textcore/render.go +++ b/text/textcore/render.go @@ -56,16 +56,12 @@ func (ed *Base) RenderWidget() { } ed.PositionScrolls() ed.renderLines() - if ed.StateIs(states.Focused) { - ed.startCursor() - } else { - ed.stopCursor() - } ed.RenderChildren() ed.RenderScrolls() + if !ed.IsReadOnly() && ed.StateIs(states.Focused) { + ed.startCursor() // needed to update position + } ed.EndRender() - } else { - ed.stopCursor() } } @@ -444,8 +440,12 @@ func (ed *Base) charStartPos(pos textpos.Pos) math32.Vector2 { if ed.Lines == nil { return math32.Vector2{} } + scpos := image.Point{} + if ed.Scene != nil { + scpos = ed.Scene.SceneGeom.Pos + } vpos := ed.Lines.PosToView(ed.viewId, pos) - spos := ed.Geom.Pos.Content + spos := ed.Geom.Pos.Content.Add(math32.FromPoint(scpos)) spos.X += ed.LineNumberPixels() - ed.Geom.Scroll.X spos.Y += (float32(vpos.Line) - ed.scrollPos) * ed.charSize.Y tx := ed.Lines.ViewMarkupLine(ed.viewId, vpos.Line) diff --git a/tree/admin.go b/tree/admin.go index 2defac2e05..4585868a5a 100644 --- a/tree/admin.go +++ b/tree/admin.go @@ -174,3 +174,19 @@ func setUniqueName(n Node, addIfSet bool) { nb.SetName(nb.Name + id) } } + +// SetUniqueNameIfDuplicate adds a unique name id to the given new child +// of given parent node if there is already a child by that name. +// Returns true if renamed. This is slow, and should not be used in +// performance-critical code (ensure names are unique in advance). +func SetUniqueNameIfDuplicate(par, child Node) bool { + ct := child.AsTree() + if IndexByName(par.AsTree().Children, ct.Name) < 0 { + return false + } + opar := ct.Parent + ct.Parent = par // unique name relies on parent + setUniqueName(child, true) + ct.Parent = opar + return true +} diff --git a/tree/plan.go b/tree/plan.go index 9c42c14b16..de9ecde5a1 100644 --- a/tree/plan.go +++ b/tree/plan.go @@ -92,8 +92,8 @@ func (nb *NodeBase) Make(p *Plan) { if len(nb.Makers.First) > 0 || len(nb.Makers.Normal) > 0 || len(nb.Makers.Final) > 0 { p.EnforceEmpty = true } - nb.Makers.Do(func(makers []func(p *Plan)) { - for _, maker := range makers { + nb.Makers.Do(func(makers *[]func(p *Plan)) { + for _, maker := range *makers { maker(p) } }) @@ -110,9 +110,9 @@ func (nb *NodeBase) UpdateFromMake() { // It is called in [cogentcore.org/core/core.WidgetBase.UpdateWidget] and other places // such as in xyz to update the node. func (nb *NodeBase) RunUpdaters() { - nb.Updaters.Do(func(updaters []func()) { - for i := len(updaters) - 1; i >= 0; i-- { - updaters[i]() + nb.Updaters.Do(func(updaters *[]func()) { + for i := len(*updaters) - 1; i >= 0; i-- { + (*updaters)[i]() } }) } diff --git a/xyz/text2d.go b/xyz/text2d.go index 0c04a49e04..e7d9dfaf31 100644 --- a/xyz/text2d.go +++ b/xyz/text2d.go @@ -103,11 +103,12 @@ func (txt *Text2D) RenderText() { } // TODO(kai): do we need to set unit context sizes? (units.Context.SetSizes) st := &txt.Styles - if !st.Font.Decoration.HasFlag(rich.FillColor) { - txt.usesDefaultColor = true - } + st.ToDots() sty, tsty := st.NewRichText() + if sty.FillColor() == nil { + txt.usesDefaultColor = true + } 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) diff --git a/xyz/typegen.go b/xyz/typegen.go index 66e8ac870c..33b1ca1c07 100644 --- a/xyz/typegen.go +++ b/xyz/typegen.go @@ -8,6 +8,7 @@ import ( "cogentcore.org/core/gpu/phong" "cogentcore.org/core/math32" + "cogentcore.org/core/text/shaped" "cogentcore.org/core/tree" "cogentcore.org/core/types" ) @@ -139,11 +140,12 @@ var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/xyz.Pose", IDName: var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/xyz.RenderClasses", IDName: "render-classes", Doc: "RenderClasses define the different classes of rendering"}) -var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/xyz.Scene", IDName: "scene", Doc: "Scene is the overall scenegraph containing nodes as children.\nIt renders to its own gpu.RenderTexture.\nThe Image of this Frame is usable directly or, via xyzcore.Scene,\nwhere it is copied into an overall core.Scene image.\n\nThere is default navigation event processing (disabled by setting NoNav)\nwhere mouse drag events Orbit the camera (Shift = Pan, Alt = PanTarget)\nand arrow keys do Orbit, Pan, PanTarget with same key modifiers.\nSpacebar restores original \"default\" camera, and numbers save (1st time)\nor restore (subsequently) camera views (Control = always save)\n\nA Group at the top-level named \"TrackCamera\" will automatically track\nthe camera (i.e., its Pose is copied) -- Solids in that group can\nset their relative Pos etc to display relative to the camera, to achieve\n\"first person\" effects.", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Background", Doc: "Background is the background of the scene,\nwhich is used directly as a solid color in Vulkan."}, {Name: "NeedsUpdate", Doc: "NeedsUpdate means that Node Pose has changed and an update pass\nis required to update matrix and bounding boxes."}, {Name: "NeedsRender", Doc: "NeedsRender means that something has been updated (minimally the\nCamera pose) and a new Render is required."}, {Name: "Geom", Doc: "Viewport-level viewbox within any parent Viewport2D"}, {Name: "MultiSample", Doc: "number of samples in multisampling. Default of 4 produces smooth\nrendering."}, {Name: "Wireframe", Doc: "render using wireframe instead of filled polygons.\nThis must be set prior to configuring the Phong rendering\nsystem (i.e., just after Scene is made).\nnote: not currently working in WebGPU."}, {Name: "Camera", Doc: "camera determines view onto scene"}, {Name: "Lights", Doc: "all lights used in the scene"}, {Name: "Meshes", Doc: "meshes"}, {Name: "Textures", Doc: "textures"}, {Name: "Library", Doc: "library of objects that can be used in the scene"}, {Name: "NoNav", Doc: "don't activate the standard navigation keyboard and mouse\nevent processing to move around the camera in the scene."}, {Name: "SavedCams", Doc: "saved cameras, can Save and Set these to view the scene\nfrom different angles"}, {Name: "Phong", Doc: "the phong rendering system"}, {Name: "Frame", Doc: "the gpu render frame holding the rendered scene"}, {Name: "imgCopy", Doc: "image used to hold a copy of the Frame image, for ImageCopy() call.\nThis is re-used across calls to avoid large memory allocations,\nso it will automatically update after every ImageCopy call.\nIf a persistent image is required, call [iox/imagex.CloneAsRGBA]."}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/xyz.Scene", IDName: "scene", Doc: "Scene is the overall scenegraph containing nodes as children.\nIt can render offscreen to its own gpu.RenderTexture, or to an\nonscreen surface.\nThe Image of this Frame is usable directly or, via xyzcore.Scene,\nwhere it is copied into an overall core.Scene image.\n\nThere is default navigation event processing (disabled by setting NoNav)\nwhere mouse drag events Orbit the camera (Shift = Pan, Alt = PanTarget)\nand arrow keys do Orbit, Pan, PanTarget with same key modifiers.\nSpacebar restores original \"default\" camera, and numbers save (1st time)\nor restore (subsequently) camera views (Control = always save)\n\nA Group at the top-level named \"TrackCamera\" will automatically track\nthe camera (i.e., its Pose is copied) -- Solids in that group can\nset their relative Pos etc to display relative to the camera, to achieve\n\"first person\" effects.", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Background", Doc: "Background is the background of the scene,\nwhich is used directly as a solid color in Vulkan."}, {Name: "NeedsUpdate", Doc: "NeedsUpdate means that Node Pose has changed and an update pass\nis required to update matrix and bounding boxes."}, {Name: "NeedsRender", Doc: "NeedsRender means that something has been updated (minimally the\nCamera pose) and a new Render is required."}, {Name: "Geom", Doc: "Viewport-level viewbox within any parent Viewport2D"}, {Name: "MultiSample", Doc: "number of samples in multisampling. Default of 4 produces smooth\nrendering."}, {Name: "Wireframe", Doc: "render using wireframe instead of filled polygons.\nThis must be set prior to configuring the Phong rendering\nsystem (i.e., just after Scene is made).\nnote: not currently working in WebGPU."}, {Name: "Camera", Doc: "camera determines view onto scene"}, {Name: "Lights", Doc: "all lights used in the scene"}, {Name: "Meshes", Doc: "meshes"}, {Name: "Textures", Doc: "textures"}, {Name: "Library", Doc: "library of objects that can be used in the scene"}, {Name: "NoNav", Doc: "don't activate the standard navigation keyboard and mouse\nevent processing to move around the camera in the scene."}, {Name: "SavedCams", Doc: "saved cameras, can Save and Set these to view the scene\nfrom different angles"}, {Name: "Phong", Doc: "the phong rendering system"}, {Name: "Frame", Doc: "the gpu render frame holding the rendered scene"}, {Name: "TextShaper", Doc: "TextShaper is the text shaping system for this scene, for doing text layout."}, {Name: "imgCopy", Doc: "image used to hold a copy of the Frame image, for ImageCopy() call.\nThis is re-used across calls to avoid large memory allocations,\nso it will automatically update after every ImageCopy call.\nIf a persistent image is required, call [iox/imagex.CloneAsRGBA]."}}}) // NewScene returns a new [Scene] with the given optional parent: // Scene is the overall scenegraph containing nodes as children. -// It renders to its own gpu.RenderTexture. +// It can render offscreen to its own gpu.RenderTexture, or to an +// onscreen surface. // The Image of this Frame is usable directly or, via xyzcore.Scene, // where it is copied into an overall core.Scene image. // @@ -181,6 +183,10 @@ func (t *Scene) SetWireframe(v bool) *Scene { t.Wireframe = v; return t } // event processing to move around the camera in the scene. func (t *Scene) SetNoNav(v bool) *Scene { t.NoNav = v; return t } +// SetTextShaper sets the [Scene.TextShaper]: +// TextShaper is the text shaping system for this scene, for doing text layout. +func (t *Scene) SetTextShaper(v shaped.Shaper) *Scene { t.TextShaper = v; return t } + var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/xyz.Plane", IDName: "plane", Doc: "Plane is a flat 2D plane, which can be oriented along any\naxis facing either positive or negative", Directives: []types.Directive{{Tool: "types", Directive: "add", Args: []string{"-setters"}}}, Embeds: []types.Field{{Name: "MeshBase"}}, Fields: []types.Field{{Name: "NormAxis", Doc: "axis along which the normal perpendicular to the plane points. E.g., if the Y axis is specified, then it is a standard X-Z ground plane -- see also NormalNeg for whether it is facing in the positive or negative of the given axis."}, {Name: "NormalNeg", Doc: "if false, the plane normal facing in the positive direction along specified NormAxis, otherwise it faces in the negative if true"}, {Name: "Size", Doc: "2D size of plane"}, {Name: "Segs", Doc: "number of segments to divide plane into (enforced to be at least 1) -- may potentially increase rendering quality to have > 1"}, {Name: "Offset", Doc: "offset from origin along direction of normal to the plane"}}}) // SetNormAxis sets the [Plane.NormAxis]: @@ -353,11 +359,11 @@ func NewSolid(parent ...tree.Node) *Solid { return tree.New[Solid](parent...) } // Material contains the material properties of the surface (color, shininess, texture, etc). func (t *Solid) SetMaterial(v Material) *Solid { t.Material = v; return t } -var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/xyz.Text2D", IDName: "text2-d", Doc: "Text2D presents 2D rendered text on a vertically oriented plane, using a texture.\nCall SetText() which calls RenderText to update fortext changes (re-renders texture).\nThe native scale is such that a unit height value is the height of the default font\nset by the font-size property, and the X axis is scaled proportionally based on the\nrendered text size to maintain the aspect ratio. Further scaling can be applied on\ntop of that by setting the Pose.Scale values as usual.\nStandard styling properties can be set on the node to set font size, family,\nand text alignment relative to the Pose.Pos position (e.g., Left, Top puts the\nupper-left corner of text at Pos).\nNote that higher quality is achieved by using a larger font size (36 default).\nThe margin property creates blank margin of the background color around the text\n(2 px default) and the background-color defaults to transparent\nbut can be set to any color.", Embeds: []types.Field{{Name: "Solid"}}, Fields: []types.Field{{Name: "Text", Doc: "the text string to display"}, {Name: "Styles", Doc: "styling settings for the text"}, {Name: "TextPos", Doc: "position offset of start of text rendering relative to upper-left corner"}, {Name: "TextRender", Doc: "render data for text label"}, {Name: "RenderState", Doc: "render state for rendering text"}, {Name: "usesDefaultColor", Doc: "automatically set to true if the font render color is the default\ncolors.Scheme.OnSurface. If so, it is automatically updated if the default\nchanges, e.g., in light mode vs dark mode switching."}}}) +var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/xyz.Text2D", IDName: "text2-d", Doc: "Text2D presents 2D rendered text on a vertically oriented plane, using a texture.\nCall SetText() which calls RenderText to update for text changes (re-renders texture).\nThe native scale is such that a unit height value is the height of the default font\nset by the font-size property, and the X axis is scaled proportionally based on the\nrendered text size to maintain the aspect ratio. Further scaling can be applied on\ntop of that by setting the Pose.Scale values as usual.\nStandard styling properties can be set on the node to set font size, family,\nand text alignment relative to the Pose.Pos position (e.g., Left, Top puts the\nupper-left corner of text at Pos).\nNote that higher quality is achieved by using a larger font size (36 default).\nThe margin property creates blank margin of the background color around the text\n(2 px default) and the background-color defaults to transparent\nbut can be set to any color.", Embeds: []types.Field{{Name: "Solid"}}, Fields: []types.Field{{Name: "Text", Doc: "the text string to display"}, {Name: "Styles", Doc: "styling settings for the text"}, {Name: "TextPos", Doc: "position offset of start of text rendering relative to upper-left corner"}, {Name: "richText", Doc: "richText is the conversion of the HTML text source."}, {Name: "textRender", Doc: "render data for text label"}, {Name: "usesDefaultColor", Doc: "automatically set to true if the font render color is the default\ncolors.Scheme.OnSurface. If so, it is automatically updated if the default\nchanges, e.g., in light mode vs dark mode switching."}}}) // NewText2D returns a new [Text2D] with the given optional parent: // Text2D presents 2D rendered text on a vertically oriented plane, using a texture. -// Call SetText() which calls RenderText to update fortext changes (re-renders texture). +// Call SetText() which calls RenderText to update for text changes (re-renders texture). // The native scale is such that a unit height value is the height of the default font // set by the font-size property, and the X axis is scaled proportionally based on the // rendered text size to maintain the aspect ratio. Further scaling can be applied on diff --git a/yaegicore/coresymbols/cogentcore_org-core-colors.go b/yaegicore/coresymbols/cogentcore_org-core-colors.go index e5a86c2907..49951d2c2e 100644 --- a/yaegicore/coresymbols/cogentcore_org-core-colors.go +++ b/yaegicore/coresymbols/cogentcore_org-core-colors.go @@ -42,6 +42,7 @@ func init() { "Chartreuse": reflect.ValueOf(&colors.Chartreuse).Elem(), "Chocolate": reflect.ValueOf(&colors.Chocolate).Elem(), "Clearer": reflect.ValueOf(colors.Clearer), + "CloneUniform": reflect.ValueOf(colors.CloneUniform), "Coral": reflect.ValueOf(&colors.Coral).Elem(), "Cornflowerblue": reflect.ValueOf(&colors.Cornflowerblue).Elem(), "Cornsilk": reflect.ValueOf(&colors.Cornsilk).Elem(), diff --git a/yaegicore/coresymbols/cogentcore_org-core-core.go b/yaegicore/coresymbols/cogentcore_org-core-core.go index 32ecda6c35..0034c0bff9 100644 --- a/yaegicore/coresymbols/cogentcore_org-core-core.go +++ b/yaegicore/coresymbols/cogentcore_org-core-core.go @@ -102,6 +102,7 @@ func init() { "NewIcon": reflect.ValueOf(core.NewIcon), "NewIconButton": reflect.ValueOf(core.NewIconButton), "NewImage": reflect.ValueOf(core.NewImage), + "NewImageSprite": reflect.ValueOf(core.NewImageSprite), "NewInlineList": reflect.ValueOf(core.NewInlineList), "NewInspector": reflect.ValueOf(core.NewInspector), "NewKeyChordButton": reflect.ValueOf(core.NewKeyChordButton), @@ -227,7 +228,6 @@ func init() { "App": reflect.ValueOf((*core.App)(nil)), "AppearanceSettingsData": reflect.ValueOf((*core.AppearanceSettingsData)(nil)), "BarFuncs": reflect.ValueOf((*core.BarFuncs)(nil)), - "Blinker": reflect.ValueOf((*core.Blinker)(nil)), "Body": reflect.ValueOf((*core.Body)(nil)), "Button": reflect.ValueOf((*core.Button)(nil)), "ButtonEmbedder": reflect.ValueOf((*core.ButtonEmbedder)(nil)), @@ -647,6 +647,7 @@ type _cogentcore_org_core_core_Treer struct { WCopy func() WCopyFieldsFrom func(from tree.Node) WCut func() + WDeleteSelected func() WDestroy func() WDragDrop func(e events.Event) WDropDeleteSource func(e events.Event) @@ -684,6 +685,7 @@ func (W _cogentcore_org_core_core_Treer) ContextMenuPos(e events.Event) image.Po func (W _cogentcore_org_core_core_Treer) Copy() { W.WCopy() } func (W _cogentcore_org_core_core_Treer) CopyFieldsFrom(from tree.Node) { W.WCopyFieldsFrom(from) } func (W _cogentcore_org_core_core_Treer) Cut() { W.WCut() } +func (W _cogentcore_org_core_core_Treer) DeleteSelected() { W.WDeleteSelected() } func (W _cogentcore_org_core_core_Treer) Destroy() { W.WDestroy() } func (W _cogentcore_org_core_core_Treer) DragDrop(e events.Event) { W.WDragDrop(e) } func (W _cogentcore_org_core_core_Treer) DropDeleteSource(e events.Event) { W.WDropDeleteSource(e) } diff --git a/yaegicore/coresymbols/cogentcore_org-core-cursors.go b/yaegicore/coresymbols/cogentcore_org-core-cursors.go new file mode 100644 index 0000000000..be445300fb --- /dev/null +++ b/yaegicore/coresymbols/cogentcore_org-core-cursors.go @@ -0,0 +1,61 @@ +// Code generated by 'yaegi extract cogentcore.org/core/cursors'. DO NOT EDIT. + +package coresymbols + +import ( + "cogentcore.org/core/cursors" + "reflect" +) + +func init() { + Symbols["cogentcore.org/core/cursors/cursors"] = map[string]reflect.Value{ + // function, constant and variable definitions + "Alias": reflect.ValueOf(cursors.Alias), + "Arrow": reflect.ValueOf(cursors.Arrow), + "Cell": reflect.ValueOf(cursors.Cell), + "ContextMenu": reflect.ValueOf(cursors.ContextMenu), + "Copy": reflect.ValueOf(cursors.Copy), + "Crosshair": reflect.ValueOf(cursors.Crosshair), + "CursorN": reflect.ValueOf(cursors.CursorN), + "CursorValues": reflect.ValueOf(cursors.CursorValues), + "Cursors": reflect.ValueOf(&cursors.Cursors).Elem(), + "Grab": reflect.ValueOf(cursors.Grab), + "Grabbing": reflect.ValueOf(cursors.Grabbing), + "Help": reflect.ValueOf(cursors.Help), + "Hotspots": reflect.ValueOf(&cursors.Hotspots).Elem(), + "Move": reflect.ValueOf(cursors.Move), + "None": reflect.ValueOf(cursors.None), + "NotAllowed": reflect.ValueOf(cursors.NotAllowed), + "Pointer": reflect.ValueOf(cursors.Pointer), + "Poof": reflect.ValueOf(cursors.Poof), + "Progress": reflect.ValueOf(cursors.Progress), + "ResizeCol": reflect.ValueOf(cursors.ResizeCol), + "ResizeDown": reflect.ValueOf(cursors.ResizeDown), + "ResizeE": reflect.ValueOf(cursors.ResizeE), + "ResizeEW": reflect.ValueOf(cursors.ResizeEW), + "ResizeLeft": reflect.ValueOf(cursors.ResizeLeft), + "ResizeN": reflect.ValueOf(cursors.ResizeN), + "ResizeNE": reflect.ValueOf(cursors.ResizeNE), + "ResizeNESW": reflect.ValueOf(cursors.ResizeNESW), + "ResizeNS": reflect.ValueOf(cursors.ResizeNS), + "ResizeNW": reflect.ValueOf(cursors.ResizeNW), + "ResizeNWSE": reflect.ValueOf(cursors.ResizeNWSE), + "ResizeRight": reflect.ValueOf(cursors.ResizeRight), + "ResizeRow": reflect.ValueOf(cursors.ResizeRow), + "ResizeS": reflect.ValueOf(cursors.ResizeS), + "ResizeSE": reflect.ValueOf(cursors.ResizeSE), + "ResizeSW": reflect.ValueOf(cursors.ResizeSW), + "ResizeUp": reflect.ValueOf(cursors.ResizeUp), + "ResizeW": reflect.ValueOf(cursors.ResizeW), + "ScreenshotSelection": reflect.ValueOf(cursors.ScreenshotSelection), + "ScreenshotWindow": reflect.ValueOf(cursors.ScreenshotWindow), + "Text": reflect.ValueOf(cursors.Text), + "VerticalText": reflect.ValueOf(cursors.VerticalText), + "Wait": reflect.ValueOf(cursors.Wait), + "ZoomIn": reflect.ValueOf(cursors.ZoomIn), + "ZoomOut": reflect.ValueOf(cursors.ZoomOut), + + // type definitions + "Cursor": reflect.ValueOf((*cursors.Cursor)(nil)), + } +} diff --git a/yaegicore/coresymbols/cogentcore_org-core-filetree.go b/yaegicore/coresymbols/cogentcore_org-core-filetree.go index da602647dd..82df223641 100644 --- a/yaegicore/coresymbols/cogentcore_org-core-filetree.go +++ b/yaegicore/coresymbols/cogentcore_org-core-filetree.go @@ -57,6 +57,7 @@ type _cogentcore_org_core_filetree_Filer struct { WCopyFieldsFrom func(from tree.Node) WCut func() WDeleteFiles func() + WDeleteSelected func() WDestroy func() WDragDrop func(e events.Event) WDropDeleteSource func(e events.Event) @@ -99,6 +100,7 @@ func (W _cogentcore_org_core_filetree_Filer) Copy() { func (W _cogentcore_org_core_filetree_Filer) CopyFieldsFrom(from tree.Node) { W.WCopyFieldsFrom(from) } func (W _cogentcore_org_core_filetree_Filer) Cut() { W.WCut() } func (W _cogentcore_org_core_filetree_Filer) DeleteFiles() { W.WDeleteFiles() } +func (W _cogentcore_org_core_filetree_Filer) DeleteSelected() { W.WDeleteSelected() } func (W _cogentcore_org_core_filetree_Filer) Destroy() { W.WDestroy() } func (W _cogentcore_org_core_filetree_Filer) DragDrop(e events.Event) { W.WDragDrop(e) } func (W _cogentcore_org_core_filetree_Filer) DropDeleteSource(e events.Event) { W.WDropDeleteSource(e) } diff --git a/yaegicore/coresymbols/cogentcore_org-core-styles.go b/yaegicore/coresymbols/cogentcore_org-core-styles.go index 9c24764dce..99b7a53d4a 100644 --- a/yaegicore/coresymbols/cogentcore_org-core-styles.go +++ b/yaegicore/coresymbols/cogentcore_org-core-styles.go @@ -92,6 +92,8 @@ func init() { "SetClampMaxVector": reflect.ValueOf(styles.SetClampMaxVector), "SetClampMin": reflect.ValueOf(styles.SetClampMin), "SetClampMinVector": reflect.ValueOf(styles.SetClampMinVector), + "SetFromRichText": reflect.ValueOf(styles.SetFromRichText), + "SetRichText": reflect.ValueOf(styles.SetRichText), "SpaceAround": reflect.ValueOf(styles.SpaceAround), "SpaceBetween": reflect.ValueOf(styles.SpaceBetween), "SpaceEvenly": reflect.ValueOf(styles.SpaceEvenly), diff --git a/yaegicore/coresymbols/cogentcore_org-core-text-rich.go b/yaegicore/coresymbols/cogentcore_org-core-text-rich.go index 9e4e4827a3..0e6aacb400 100644 --- a/yaegicore/coresymbols/cogentcore_org-core-text-rich.go +++ b/yaegicore/coresymbols/cogentcore_org-core-text-rich.go @@ -14,7 +14,6 @@ func init() { // function, constant and variable definitions "AddFamily": reflect.ValueOf(rich.AddFamily), "BTT": reflect.ValueOf(rich.BTT), - "Background": reflect.ValueOf(rich.Background), "Black": reflect.ValueOf(rich.Black), "Bold": reflect.ValueOf(rich.Bold), "ColorFromRune": reflect.ValueOf(rich.ColorFromRune), @@ -47,7 +46,6 @@ func init() { "FamilyValues": reflect.ValueOf(rich.FamilyValues), "Fangsong": reflect.ValueOf(rich.Fangsong), "Fantasy": reflect.ValueOf(rich.Fantasy), - "FillColor": reflect.ValueOf(rich.FillColor), "FontSizes": reflect.ValueOf(&rich.FontSizes).Elem(), "Italic": reflect.ValueOf(rich.Italic), "Join": reflect.ValueOf(rich.Join), @@ -106,7 +104,6 @@ func init() { "StretchNormal": reflect.ValueOf(rich.StretchNormal), "StretchStart": reflect.ValueOf(constant.MakeFromLiteral("16", token.INT, 0)), "StretchValues": reflect.ValueOf(rich.StretchValues), - "StrokeColor": reflect.ValueOf(rich.StrokeColor), "Sub": reflect.ValueOf(rich.Sub), "Super": reflect.ValueOf(rich.Super), "TTB": reflect.ValueOf(rich.TTB), diff --git a/yaegicore/coresymbols/cogentcore_org-core-tree.go b/yaegicore/coresymbols/cogentcore_org-core-tree.go index 50843e4e0b..d90cdf2986 100644 --- a/yaegicore/coresymbols/cogentcore_org-core-tree.go +++ b/yaegicore/coresymbols/cogentcore_org-core-tree.go @@ -11,38 +11,39 @@ import ( func init() { Symbols["cogentcore.org/core/tree/tree"] = map[string]reflect.Value{ // function, constant and variable definitions - "Add": reflect.ValueOf(interp.GenericFunc("func Add[T NodeValue](p *Plan, init func(w *T)) { //yaegi:add\n\tAddAt(p, AutoPlanName(2), init)\n}")), - "AddAt": reflect.ValueOf(interp.GenericFunc("func AddAt[T NodeValue](p *Plan, name string, init func(w *T)) { //yaegi:add\n\tp.Add(name, func() Node {\n\t\treturn any(New[T]()).(Node)\n\t}, func(n Node) {\n\t\tinit(any(n).(*T))\n\t})\n}")), - "AddChild": reflect.ValueOf(interp.GenericFunc("func AddChild[T NodeValue](parent Node, init func(w *T)) { //yaegi:add\n\tname := AutoPlanName(2) // must get here to get correct name\n\tparent.AsTree().Maker(func(p *Plan) {\n\t\tAddAt(p, name, init)\n\t})\n}")), - "AddChildAt": reflect.ValueOf(interp.GenericFunc("func AddChildAt[T NodeValue](parent Node, name string, init func(w *T)) { //yaegi:add\n\tparent.AsTree().Maker(func(p *Plan) {\n\t\tAddAt(p, name, init)\n\t})\n}")), - "AddChildInit": reflect.ValueOf(interp.GenericFunc("func AddChildInit[T NodeValue](parent Node, name string, init func(w *T)) { //yaegi:add\n\tparent.AsTree().Maker(func(p *Plan) {\n\t\tAddInit(p, name, init)\n\t})\n}")), - "AddInit": reflect.ValueOf(interp.GenericFunc("func AddInit[T NodeValue](p *Plan, name string, init func(w *T)) { //yaegi:add\n\tfor _, child := range p.Children {\n\t\tif child.Name == name {\n\t\t\tchild.Init = append(child.Init, func(n Node) {\n\t\t\t\tinit(any(n).(*T))\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t}\n\tslog.Error(\"AddInit: child not found\", \"name\", name)\n}")), - "AddNew": reflect.ValueOf(interp.GenericFunc("func AddNew[T Node](p *Plan, name string, new func() T, init func(w T)) { //yaegi:add\n\tp.Add(name, func() Node {\n\t\treturn new()\n\t}, func(n Node) {\n\t\tinit(n.(T))\n\t})\n}")), - "AutoPlanName": reflect.ValueOf(tree.AutoPlanName), - "Break": reflect.ValueOf(tree.Break), - "Continue": reflect.ValueOf(tree.Continue), - "EscapePathName": reflect.ValueOf(tree.EscapePathName), - "IndexByName": reflect.ValueOf(tree.IndexByName), - "IndexOf": reflect.ValueOf(tree.IndexOf), - "InitNode": reflect.ValueOf(tree.InitNode), - "IsNil": reflect.ValueOf(tree.IsNil), - "IsNode": reflect.ValueOf(tree.IsNode), - "IsRoot": reflect.ValueOf(tree.IsRoot), - "Last": reflect.ValueOf(tree.Last), - "MoveToParent": reflect.ValueOf(tree.MoveToParent), - "New": reflect.ValueOf(interp.GenericFunc("func New[T NodeValue](parent ...Node) *T { //yaegi:add\n\tn := new(T)\n\tni := any(n).(Node)\n\tInitNode(ni)\n\tif len(parent) == 0 {\n\t\tni.AsTree().SetName(ni.AsTree().NodeType().IDName)\n\t\treturn n\n\t}\n\tp := parent[0]\n\tp.AsTree().Children = append(p.AsTree().Children, ni)\n\tSetParent(ni, p)\n\treturn n\n}")), - "NewNodeBase": reflect.ValueOf(tree.NewNodeBase), - "NewOfType": reflect.ValueOf(tree.NewOfType), - "Next": reflect.ValueOf(tree.Next), - "NextSibling": reflect.ValueOf(tree.NextSibling), - "Previous": reflect.ValueOf(tree.Previous), - "Root": reflect.ValueOf(tree.Root), - "SetParent": reflect.ValueOf(tree.SetParent), - "SetUniqueName": reflect.ValueOf(tree.SetUniqueName), - "UnescapePathName": reflect.ValueOf(tree.UnescapePathName), - "UnmarshalRootJSON": reflect.ValueOf(tree.UnmarshalRootJSON), - "Update": reflect.ValueOf(tree.Update), - "UpdateSlice": reflect.ValueOf(tree.UpdateSlice), + "Add": reflect.ValueOf(interp.GenericFunc("func Add[T NodeValue](p *Plan, init func(w *T)) { //yaegi:add\n\tAddAt(p, AutoPlanName(2), init)\n}")), + "AddAt": reflect.ValueOf(interp.GenericFunc("func AddAt[T NodeValue](p *Plan, name string, init func(w *T)) { //yaegi:add\n\tp.Add(name, func() Node {\n\t\treturn any(New[T]()).(Node)\n\t}, func(n Node) {\n\t\tinit(any(n).(*T))\n\t})\n}")), + "AddChild": reflect.ValueOf(interp.GenericFunc("func AddChild[T NodeValue](parent Node, init func(w *T)) { //yaegi:add\n\tname := AutoPlanName(2) // must get here to get correct name\n\tparent.AsTree().Maker(func(p *Plan) {\n\t\tAddAt(p, name, init)\n\t})\n}")), + "AddChildAt": reflect.ValueOf(interp.GenericFunc("func AddChildAt[T NodeValue](parent Node, name string, init func(w *T)) { //yaegi:add\n\tparent.AsTree().Maker(func(p *Plan) {\n\t\tAddAt(p, name, init)\n\t})\n}")), + "AddChildInit": reflect.ValueOf(interp.GenericFunc("func AddChildInit[T NodeValue](parent Node, name string, init func(w *T)) { //yaegi:add\n\tparent.AsTree().Maker(func(p *Plan) {\n\t\tAddInit(p, name, init)\n\t})\n}")), + "AddInit": reflect.ValueOf(interp.GenericFunc("func AddInit[T NodeValue](p *Plan, name string, init func(w *T)) { //yaegi:add\n\tfor _, child := range p.Children {\n\t\tif child.Name == name {\n\t\t\tchild.Init = append(child.Init, func(n Node) {\n\t\t\t\tinit(any(n).(*T))\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t}\n\tslog.Error(\"AddInit: child not found\", \"name\", name)\n}")), + "AddNew": reflect.ValueOf(interp.GenericFunc("func AddNew[T Node](p *Plan, name string, new func() T, init func(w T)) { //yaegi:add\n\tp.Add(name, func() Node {\n\t\treturn new()\n\t}, func(n Node) {\n\t\tinit(n.(T))\n\t})\n}")), + "AutoPlanName": reflect.ValueOf(tree.AutoPlanName), + "Break": reflect.ValueOf(tree.Break), + "Continue": reflect.ValueOf(tree.Continue), + "EscapePathName": reflect.ValueOf(tree.EscapePathName), + "IndexByName": reflect.ValueOf(tree.IndexByName), + "IndexOf": reflect.ValueOf(tree.IndexOf), + "InitNode": reflect.ValueOf(tree.InitNode), + "IsNil": reflect.ValueOf(tree.IsNil), + "IsNode": reflect.ValueOf(tree.IsNode), + "IsRoot": reflect.ValueOf(tree.IsRoot), + "Last": reflect.ValueOf(tree.Last), + "MoveToParent": reflect.ValueOf(tree.MoveToParent), + "New": reflect.ValueOf(interp.GenericFunc("func New[T NodeValue](parent ...Node) *T { //yaegi:add\n\tn := new(T)\n\tni := any(n).(Node)\n\tInitNode(ni)\n\tif len(parent) == 0 {\n\t\tni.AsTree().SetName(ni.AsTree().NodeType().IDName)\n\t\treturn n\n\t}\n\tp := parent[0]\n\tp.AsTree().Children = append(p.AsTree().Children, ni)\n\tSetParent(ni, p)\n\treturn n\n}")), + "NewNodeBase": reflect.ValueOf(tree.NewNodeBase), + "NewOfType": reflect.ValueOf(tree.NewOfType), + "Next": reflect.ValueOf(tree.Next), + "NextSibling": reflect.ValueOf(tree.NextSibling), + "Previous": reflect.ValueOf(tree.Previous), + "Root": reflect.ValueOf(tree.Root), + "SetParent": reflect.ValueOf(tree.SetParent), + "SetUniqueName": reflect.ValueOf(tree.SetUniqueName), + "SetUniqueNameIfDuplicate": reflect.ValueOf(tree.SetUniqueNameIfDuplicate), + "UnescapePathName": reflect.ValueOf(tree.UnescapePathName), + "UnmarshalRootJSON": reflect.ValueOf(tree.UnmarshalRootJSON), + "Update": reflect.ValueOf(tree.Update), + "UpdateSlice": reflect.ValueOf(tree.UpdateSlice), // type definitions "Node": reflect.ValueOf((*tree.Node)(nil)), diff --git a/yaegicore/coresymbols/make b/yaegicore/coresymbols/make index a3d6890bdd..a340b9ba9a 100755 --- a/yaegicore/coresymbols/make +++ b/yaegicore/coresymbols/make @@ -8,5 +8,5 @@ command extract { yaegi extract image image/color image/draw -extract core events styles styles/states styles/abilities styles/units tree keymap colors colors/gradient filetree text/textcore text/rich text/lines text/text htmlcore content paint base/iox/imagex +extract core events styles styles/states styles/abilities styles/units tree keymap colors colors/gradient filetree text/textcore text/rich text/lines text/text htmlcore content paint base/iox/imagex cursors