// 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 atomiccounter implements a basic atomic int64 counter.
package atomiccounter
import (
"sync/atomic"
)
// Counter implements a basic atomic int64 counter.
type Counter int64
// Add adds to counter.
func (a *Counter) Add(inc int64) int64 {
return atomic.AddInt64((*int64)(a), inc)
}
// Sub subtracts from counter.
func (a *Counter) Sub(dec int64) int64 {
return atomic.AddInt64((*int64)(a), -dec)
}
// Inc increments by 1.
func (a *Counter) Inc() int64 {
return atomic.AddInt64((*int64)(a), 1)
}
// Dec decrements by 1.
func (a *Counter) Dec() int64 {
return atomic.AddInt64((*int64)(a), -1)
}
// Value returns the current value.
func (a *Counter) Value() int64 {
return atomic.LoadInt64((*int64)(a))
}
// Set sets the counter to a new value.
func (a *Counter) Set(val int64) {
atomic.StoreInt64((*int64)(a), val)
}
// Swap swaps a new value in and returns the old value.
func (a *Counter) Swap(val int64) int64 {
return atomic.SwapInt64((*int64)(a), val)
}
// 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 auth provides a system for identifying and authenticating
// users through third party cloud systems in Cogent Core apps.
package auth
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io/fs"
"log/slog"
"net/http"
"os"
"path/filepath"
"cogentcore.org/core/base/iox/jsonx"
"cogentcore.org/core/core"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
// AuthConfig is the configuration information passed to [Auth].
type AuthConfig struct {
// Ctx is the context to use. It is [context.TODO] if unspecified.
Ctx context.Context
// ProviderName is the name of the provider to authenticate with (eg: "google")
ProviderName string
// ProviderURL is the URL of the provider (eg: "https://accounts.google.com")
ProviderURL string
// ClientID is the client ID for the app, which is typically obtained through a developer oauth
// portal (eg: the Credentials section of https://console.developers.google.com/).
ClientID string
// ClientSecret is the client secret for the app, which is typically obtained through a developer oauth
// portal (eg: the Credentials section of https://console.developers.google.com/).
ClientSecret string
// TokenFile is an optional function that returns the filename at which the token for the given user will be stored as JSON.
// If it is nil or it returns "", the token is not stored. Also, if it is non-nil, Auth skips the user-facing authentication
// step if it finds a valid token at the file (ie: remember me). It checks all [AuthConfig.Accounts] until it finds one
// that works for that step. If [AuthConfig.Accounts] is nil, it checks with a blank ("") email account.
TokenFile func(email string) string
// Accounts are optional accounts to check for the remember me feature described in [AuthConfig.TokenFile].
// If it is nil and TokenFile is not, it defaults to contain one blank ("") element.
Accounts []string
// Scopes are additional scopes to request beyond the default "openid", "profile", and "email" scopes
Scopes []string
}
// Auth authenticates the user using the given configuration information and returns the
// resulting oauth token and user info. See [AuthConfig] for more information on the
// configuration options.
func Auth(c *AuthConfig) (*oauth2.Token, *oidc.UserInfo, error) {
if c.Ctx == nil {
c.Ctx = context.TODO()
}
if c.ClientID == "" || c.ClientSecret == "" {
slog.Warn("got empty client id and/or client secret; did you forgot to set env variables?")
}
provider, err := oidc.NewProvider(c.Ctx, c.ProviderURL)
if err != nil {
return nil, nil, err
}
config := oauth2.Config{
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
RedirectURL: "http://127.0.0.1:5556/auth/" + c.ProviderName + "/callback",
Endpoint: provider.Endpoint(),
Scopes: append([]string{oidc.ScopeOpenID, "profile", "email"}, c.Scopes...),
}
var token *oauth2.Token
if c.TokenFile != nil {
if c.Accounts == nil {
c.Accounts = []string{""}
}
for _, account := range c.Accounts {
tf := c.TokenFile(account)
if tf != "" {
err := jsonx.Open(&token, tf)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, nil, err
}
break
}
}
}
// if we didn't get it through remember me, we have to get it manually
if token == nil {
b := make([]byte, 16)
rand.Read(b)
state := base64.RawURLEncoding.EncodeToString(b)
code := make(chan string)
sm := http.NewServeMux()
sm.HandleFunc("/auth/"+c.ProviderName+"/callback", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("state") != state {
http.Error(w, "state did not match", http.StatusBadRequest)
return
}
code <- r.URL.Query().Get("code")
w.Write([]byte("<h1>Signed in</h1><p>You can return to the app</p>"))
})
// TODO(kai/auth): more graceful closing / error handling
go http.ListenAndServe("127.0.0.1:5556", sm)
core.TheApp.OpenURL(config.AuthCodeURL(state))
cs := <-code
token, err = config.Exchange(c.Ctx, cs)
if err != nil {
return nil, nil, fmt.Errorf("failed to exchange token: %w", err)
}
}
tokenSource := config.TokenSource(c.Ctx, token)
// the access token could have changed
newToken, err := tokenSource.Token()
if err != nil {
return nil, nil, err
}
userInfo, err := provider.UserInfo(c.Ctx, tokenSource)
if err != nil {
return nil, nil, fmt.Errorf("failed to get user info: %w", err)
}
if c.TokenFile != nil {
tf := c.TokenFile(userInfo.Email)
if tf != "" {
err := os.MkdirAll(filepath.Dir(tf), 0700)
if err != nil {
return nil, nil, err
}
// TODO(kai/auth): more secure saving of token file
err = jsonx.Save(token, tf)
if err != nil {
return nil, nil, err
}
}
}
return newToken, userInfo, nil
}
// 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 auth
import (
"context"
"cogentcore.org/core/base/auth/cicons"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/colors"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/styles"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
// ButtonsConfig is the configuration information passed to [Buttons].
type ButtonsConfig struct {
// SuccessFunc, if non-nil, is the function called after the user successfully
// authenticates. It is passed the user's authentication token and info.
SuccessFunc func(token *oauth2.Token, userInfo *oidc.UserInfo)
// TokenFile, if non-nil, is the function used to determine what token file function is
// used for [AuthConfig.TokenFile]. It is passed the provider being used (eg: "google") and the
// email address of the user authenticating.
TokenFile func(provider, email string) string
// Accounts are optional accounts to check for the remember me feature described in [AuthConfig.TokenFile].
// See [AuthConfig.Accounts] for more information. If it is nil and TokenFile is not, it defaults to contain
// one blank ("") element.
Accounts []string
// Scopes, if non-nil, is a map of scopes to pass to [Auth], keyed by the
// provider being used (eg: "google").
Scopes map[string][]string
}
// Buttons adds a new vertical layout to the given parent with authentication
// buttons for major platforms, using the given configuration options. See
// [ButtonsConfig] for more information on the configuration options. The
// configuration options can be nil, in which case default values will be used.
func Buttons(par core.Widget, c *ButtonsConfig) *core.Frame {
ly := core.NewFrame(par)
ly.Styler(func(s *styles.Style) {
s.Direction = styles.Column
})
GoogleButton(ly, c)
return ly
}
// Button makes a new button for signing in with the provider
// that has the given name and auth func. It should not typically
// be used by end users; instead, use [Buttons] or the platform-specific
// functions (eg: [Google]). The configuration options can be nil, in
// which case default values will be used.
func Button(par core.Widget, c *ButtonsConfig, provider string, authFunc func(c *AuthConfig) (*oauth2.Token, *oidc.UserInfo, error)) *core.Button {
if c == nil {
c = &ButtonsConfig{}
}
if c.SuccessFunc == nil {
c.SuccessFunc = func(token *oauth2.Token, userInfo *oidc.UserInfo) {}
}
if c.Scopes == nil {
c.Scopes = map[string][]string{}
}
bt := core.NewButton(par).SetText("Sign in")
tf := func(email string) string {
if c.TokenFile != nil {
return c.TokenFile(provider, email)
}
return ""
}
ac := &AuthConfig{
Ctx: context.TODO(),
ProviderName: provider,
TokenFile: tf,
Accounts: c.Accounts,
Scopes: c.Scopes[provider],
}
auth := func() {
token, userInfo, err := authFunc(ac)
if err != nil {
core.ErrorDialog(bt, err, "Error signing in with "+strcase.ToSentence(provider))
return
}
c.SuccessFunc(token, userInfo)
}
bt.OnClick(func(e events.Event) {
auth()
})
// if we have a valid token file, we auth immediately without the user clicking on the button
if c.TokenFile != nil {
if c.Accounts == nil {
c.Accounts = []string{""}
}
for _, account := range c.Accounts {
tf := c.TokenFile(provider, account)
if tf != "" {
exists, err := fsx.FileExists(tf)
if err != nil {
core.ErrorDialog(bt, err, "Error searching for saved "+strcase.ToSentence(provider)+" auth token file")
return bt
}
if exists {
// have to wait until the scene is shown in case any dialogs are created
bt.OnShow(func(e events.Event) {
auth()
})
}
}
}
}
return bt
}
// GoogleButton adds a new button for signing in with Google
// to the given parent using the given configuration information.
func GoogleButton(par core.Widget, c *ButtonsConfig) *core.Button {
bt := Button(par, c, "google", Google).SetType(core.ButtonOutlined).
SetText("Sign in with Google").SetIcon(cicons.SignInWithGoogle)
bt.Styler(func(s *styles.Style) {
s.Color = colors.Scheme.OnSurface
})
return bt
}
// 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 main
import (
"cogentcore.org/core/base/auth"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/core"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
func main() {
b := core.NewBody("Auth basic example")
fun := func(token *oauth2.Token, userInfo *oidc.UserInfo) {
d := core.NewBody("User info")
core.NewText(d).SetType(core.TextHeadlineMedium).SetText("Basic info")
core.NewForm(d).SetStruct(userInfo)
core.NewText(d).SetType(core.TextHeadlineMedium).SetText("Detailed info")
claims := map[string]any{}
errors.Log(userInfo.Claims(&claims))
core.NewKeyedList(d).SetMap(&claims)
d.AddOKOnly().RunFullDialog(b)
}
auth.Buttons(b, &auth.ButtonsConfig{SuccessFunc: fun})
b.RunMainWindow()
}
// 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 main
import (
"path/filepath"
"cogentcore.org/core/base/auth"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/core"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
func main() {
b := core.NewBody("Auth scopes and token file example")
fun := func(token *oauth2.Token, userInfo *oidc.UserInfo) {
d := core.NewBody()
core.NewText(d).SetType(core.TextHeadlineMedium).SetText("Basic info")
core.NewForm(d).SetStruct(userInfo)
core.NewText(d).SetType(core.TextHeadlineMedium).SetText("Detailed info")
claims := map[string]any{}
errors.Log(userInfo.Claims(&claims))
core.NewKeyedList(d).SetMap(&claims)
d.AddOKOnly().RunFullDialog(b)
}
auth.Buttons(b, &auth.ButtonsConfig{
SuccessFunc: fun,
TokenFile: func(provider, email string) string {
return filepath.Join(core.TheApp.AppDataDir(), provider+"-token.json")
},
Scopes: map[string][]string{
"google": {"https://mail.google.com/"},
},
})
b.RunMainWindow()
}
// 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 auth
import (
"os"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
// Google authenticates the user with Google using [Auth] and the given configuration
// information and returns the resulting oauth token and user info. It sets the values
// of [AuthConfig.ProviderName], [AuthConfig.ProviderURL], [AuthConfig.ClientID], and
// [AuthConfig.ClientSecret] if they are not already set.
func Google(c *AuthConfig) (*oauth2.Token, *oidc.UserInfo, error) {
if c.ProviderName == "" {
c.ProviderName = "google"
}
if c.ProviderURL == "" {
c.ProviderURL = "https://accounts.google.com"
}
if c.ClientID == "" {
c.ClientID = os.Getenv("GOOGLE_OAUTH2_CLIENT_ID")
}
if c.ClientSecret == "" {
c.ClientSecret = os.Getenv("GOOGLE_OAUTH2_CLIENT_SECRET")
}
return Auth(c)
}
// 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.
// Based on https://github.com/c2h5oh/datasize
// Copyright (c) 2016 Maciej Lisiewski
// Package datasize provides a data size type and constants.
package datasize
import (
"errors"
"fmt"
"strconv"
"strings"
)
// Size represents a data size.
type Size uint64
const (
B Size = 1
KB = B << 10
MB = KB << 10
GB = MB << 10
TB = GB << 10
PB = TB << 10
EB = PB << 10
fnUnmarshalText string = "UnmarshalText"
maxUint64 uint64 = (1 << 64) - 1
cutoff uint64 = maxUint64 / 10
)
var ErrBits = errors.New("unit with capital unit prefix and lower case unit (b) - bits, not bytes")
func (b Size) Bytes() uint64 {
return uint64(b)
}
func (b Size) KBytes() float64 {
v := b / KB
r := b % KB
return float64(v) + float64(r)/float64(KB)
}
func (b Size) MBytes() float64 {
v := b / MB
r := b % MB
return float64(v) + float64(r)/float64(MB)
}
func (b Size) GBytes() float64 {
v := b / GB
r := b % GB
return float64(v) + float64(r)/float64(GB)
}
func (b Size) TBytes() float64 {
v := b / TB
r := b % TB
return float64(v) + float64(r)/float64(TB)
}
func (b Size) PBytes() float64 {
v := b / PB
r := b % PB
return float64(v) + float64(r)/float64(PB)
}
func (b Size) EBytes() float64 {
v := b / EB
r := b % EB
return float64(v) + float64(r)/float64(EB)
}
// String returns a human-readable representation of the data size.
func (b Size) String() string {
switch {
case b > EB:
return fmt.Sprintf("%.1f EB", b.EBytes())
case b > PB:
return fmt.Sprintf("%.1f PB", b.PBytes())
case b > TB:
return fmt.Sprintf("%.1f TB", b.TBytes())
case b > GB:
return fmt.Sprintf("%.1f GB", b.GBytes())
case b > MB:
return fmt.Sprintf("%.1f MB", b.MBytes())
case b > KB:
return fmt.Sprintf("%.1f KB", b.KBytes())
default:
return fmt.Sprintf("%d B", b)
}
}
// MachineString returns a machine-friendly representation of the data size.
func (b Size) MachineString() string {
switch {
case b == 0:
return "0B"
case b%EB == 0:
return fmt.Sprintf("%dEB", b/EB)
case b%PB == 0:
return fmt.Sprintf("%dPB", b/PB)
case b%TB == 0:
return fmt.Sprintf("%dTB", b/TB)
case b%GB == 0:
return fmt.Sprintf("%dGB", b/GB)
case b%MB == 0:
return fmt.Sprintf("%dMB", b/MB)
case b%KB == 0:
return fmt.Sprintf("%dKB", b/KB)
default:
return fmt.Sprintf("%dB", b)
}
}
func (b Size) MarshalText() ([]byte, error) {
return []byte(b.MachineString()), nil
}
func (b *Size) UnmarshalText(t []byte) error {
var val uint64
var unit string
// copy for error message
t0 := t
var c byte
var i int
ParseLoop:
for i < len(t) {
c = t[i]
switch {
case '0' <= c && c <= '9':
if val > cutoff {
goto Overflow
}
c = c - '0'
val *= 10
if val > val+uint64(c) {
// val+v overflows
goto Overflow
}
val += uint64(c)
i++
default:
if i == 0 {
goto SyntaxError
}
break ParseLoop
}
}
unit = strings.TrimSpace(string(t[i:]))
switch unit {
case "Kb", "Mb", "Gb", "Tb", "Pb", "Eb":
goto BitsError
}
unit = strings.ToLower(unit)
switch unit {
case "", "b", "byte":
// do nothing - already in bytes
case "k", "kb", "kilo", "kilobyte", "kilobytes":
if val > maxUint64/uint64(KB) {
goto Overflow
}
val *= uint64(KB)
case "m", "mb", "mega", "megabyte", "megabytes":
if val > maxUint64/uint64(MB) {
goto Overflow
}
val *= uint64(MB)
case "g", "gb", "giga", "gigabyte", "gigabytes":
if val > maxUint64/uint64(GB) {
goto Overflow
}
val *= uint64(GB)
case "t", "tb", "tera", "terabyte", "terabytes":
if val > maxUint64/uint64(TB) {
goto Overflow
}
val *= uint64(TB)
case "p", "pb", "peta", "petabyte", "petabytes":
if val > maxUint64/uint64(PB) {
goto Overflow
}
val *= uint64(PB)
case "E", "EB", "e", "eb", "eB":
if val > maxUint64/uint64(EB) {
goto Overflow
}
val *= uint64(EB)
default:
goto SyntaxError
}
*b = Size(val)
return nil
Overflow:
*b = Size(maxUint64)
return &strconv.NumError{fnUnmarshalText, string(t0), strconv.ErrRange}
SyntaxError:
*b = 0
return &strconv.NumError{fnUnmarshalText, string(t0), strconv.ErrSyntax}
BitsError:
*b = 0
return &strconv.NumError{fnUnmarshalText, string(t0), ErrBits}
}
func Parse(t []byte) (Size, error) {
var v Size
err := v.UnmarshalText(t)
return v, err
}
func MustParse(t []byte) Size {
v, err := Parse(t)
if err != nil {
panic(err)
}
return v
}
func ParseString(s string) (Size, error) {
return Parse([]byte(s))
}
func MustParseString(s string) Size {
return MustParse([]byte(s))
}
// 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 elide provides basic text eliding functions.
package elide
import "strings"
// End elides from the end of the string if it is longer than given
// size parameter. The resulting string will not exceed sz in length,
// with space reserved for … at the end.
func End(s string, sz int) string {
n := len(s)
if n <= sz {
return s
}
return s[:sz-1] + "…"
}
// Middle elides from the middle of the string if it is longer than given
// size parameter. The resulting string will not exceed sz in length,
// with space reserved for … in the middle
func Middle(s string, sz int) string {
n := len(s)
if n <= sz {
return s
}
en := sz - 1
mid := en / 2
rest := en - mid
return s[:mid] + "…" + s[n-rest:]
}
// AppName elides the given app name to be twelve characters or less
// by removing word(s) from the middle of the string if necessary and possible.
func AppName(s string) string {
if len(s) <= 12 {
return s
}
words := strings.Fields(s)
if len(words) < 3 {
return s
}
return words[0] + " " + words[len(words)-1]
}
// 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 errors provides a set of error handling helpers,
// extending the standard library errors package.
package errors
import (
"log/slog"
"runtime"
"strconv"
)
// Log takes the given error and logs it if it is non-nil.
// The intended usage is:
//
// errors.Log(MyFunc(v))
// // or
// return errors.Log(MyFunc(v))
func Log(err error) error {
if err != nil {
slog.Error(err.Error() + " | " + CallerInfo())
}
return err
}
// Log1 takes the given value and error and returns the value if
// the error is nil, and logs the error and returns a zero value
// if the error is non-nil. The intended usage is:
//
// a := errors.Log1(MyFunc(v))
func Log1[T any](v T, err error) T { //yaegi:add
if err != nil {
slog.Error(err.Error() + " | " + CallerInfo())
}
return v
}
// Log2 takes the given two values and error and returns the values if
// the error is nil, and logs the error and returns zero values
// if the error is non-nil. The intended usage is:
//
// a, b := errors.Log2(MyFunc(v))
func Log2[T1, T2 any](v1 T1, v2 T2, err error) (T1, T2) {
if err != nil {
slog.Error(err.Error() + " | " + CallerInfo())
}
return v1, v2
}
// Must takes the given error and panics if it is non-nil.
// The intended usage is:
//
// errors.Must(MyFunc(v))
func Must(err error) {
if err != nil {
panic(err)
}
}
// Must1 takes the given value and error and returns the value if
// the error is nil, and panics if the error is non-nil. The intended usage is:
//
// a := errors.Must1(MyFunc(v))
func Must1[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
// Must2 takes the given two values and error and returns the values if
// the error is nil, and panics if the error is non-nil. The intended usage is:
//
// a, b := errors.Must2(MyFunc(v))
func Must2[T1, T2 any](v1 T1, v2 T2, err error) (T1, T2) {
if err != nil {
panic(err)
}
return v1, v2
}
// Ignore1 ignores an error return value for a function returning
// a value and an error, allowing direct usage of the value.
// The intended usage is:
//
// a := errors.Ignore1(MyFunc(v))
func Ignore1[T any](v T, err error) T {
return v
}
// Ignore2 ignores an error return value for a function returning
// two values and an error, allowing direct usage of the values.
// The intended usage is:
//
// a, b := errors.Ignore2(MyFunc(v))
func Ignore2[T1, T2 any](v1 T1, v2 T2, err error) (T1, T2) {
return v1, v2
}
// CallerInfo returns string information about the caller
// of the function that called CallerInfo.
func CallerInfo() string {
pc, file, line, _ := runtime.Caller(2)
return runtime.FuncForPC(pc).Name() + " " + file + ":" + strconv.Itoa(line)
}
// 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 errors
import "errors"
// Aliases for standard library errors package:
// ErrUnsupported indicates that a requested operation cannot be performed,
// because it is unsupported. For example, a call to [os.Link] when using a
// file system that does not support hard links.
//
// Functions and methods should not return this error but should instead
// return an error including appropriate context that satisfies
//
// errors.Is(err, errors.ErrUnsupported)
//
// either by directly wrapping ErrUnsupported or by implementing an [Is] method.
//
// Functions and methods should document the cases in which an error
// wrapping this will be returned.
var ErrUnsupported = errors.ErrUnsupported
// As finds the first error in err's tree that matches target, and if one is found, sets
// target to that error value and returns true. Otherwise, it returns false.
//
// The tree consists of err itself, followed by the errors obtained by repeatedly
// calling its Unwrap() error or Unwrap() []error method. When err wraps multiple
// errors, As examines err followed by a depth-first traversal of its children.
//
// An error matches target if the error's concrete value is assignable to the value
// pointed to by target, or if the error has a method As(interface{}) bool such that
// As(target) returns true. In the latter case, the As method is responsible for
// setting target.
//
// An error type might provide an As method so it can be treated as if it were a
// different error type.
//
// As panics if target is not a non-nil pointer to either a type that implements
// error, or to any interface type.
func As(err error, target any) bool { return errors.As(err, target) }
// Is reports whether any error in err's tree matches target.
//
// The tree consists of err itself, followed by the errors obtained by repeatedly
// calling its Unwrap() error or Unwrap() []error method. When err wraps multiple
// errors, Is examines err followed by a depth-first traversal of its children.
//
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
//
// An error type might provide an Is method so it can be treated as equivalent
// to an existing error. For example, if MyError defines
//
// func (m MyError) Is(target error) bool { return target == fs.ErrExist }
//
// then Is(MyError{}, fs.ErrExist) returns true. See [syscall.Errno.Is] for
// an example in the standard library. An Is method should only shallowly
// compare err and the target and not call [Unwrap] on either.
func Is(err, target error) bool { return errors.Is(err, target) }
// Join returns an error that wraps the given errors.
// Any nil error values are discarded.
// Join returns nil if every value in errs is nil.
// The error formats as the concatenation of the strings obtained
// by calling the Error method of each element of errs, with a newline
// between each string.
//
// A non-nil error returned by Join implements the Unwrap() []error method.
func Join(errs ...error) error { return errors.Join(errs...) }
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error { return errors.New(text) }
// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
//
// Unwrap only calls a method of the form "Unwrap() error".
// In particular Unwrap does not unwrap errors returned by [Join].
func Unwrap(err error) error { return errors.Unwrap(err) }
// Copyright (c) 2024, 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 exec
import (
"bytes"
"fmt"
"os/exec"
"strings"
)
// Cmd is a type alias for [exec.Cmd].
type Cmd = exec.Cmd
// CmdIO maintains an [exec.Cmd] pointer and IO state saved for the command
type CmdIO struct {
StdIOState
// Cmd is the [exec.Cmd]
Cmd *exec.Cmd
}
func (c *CmdIO) String() string {
if c.Cmd == nil {
return "<nil>"
}
str := ""
if c.Cmd.ProcessState != nil {
str = c.Cmd.ProcessState.String()
} else if c.Cmd.Process != nil {
str = fmt.Sprintf("%d ", c.Cmd.Process.Pid)
} else {
str = "no process info"
}
str += " " + c.Cmd.String()
return str
}
// NewCmdIO returns a new [CmdIO] initialized with StdIO settings from given Config
func NewCmdIO(c *Config) *CmdIO {
cio := &CmdIO{}
cio.StdIO = c.StdIO
return cio
}
// RunIO runs the given command using the given
// configuration information and arguments,
// waiting for it to complete before returning.
// This IO version of [Run] uses specified stdio and sets the
// command in it as well, for easier management of
// dynamically updated IO routing.
func (c *Config) RunIO(cio *CmdIO, cmd string, args ...string) error {
cm, _, err := c.exec(&cio.StdIO, false, cmd, args...)
cio.Cmd = cm
return err
}
// StartIO starts the given command using the given
// configuration information and arguments,
// just starting the command but not waiting for it to finish.
// This IO version of [Start] uses specified stdio and sets the
// command in it as well, which should be used to Wait for the
// command to finish (in a separate goroutine).
// For dynamic IO routing uses, call [CmdIO.StackStart] prior to
// setting the IO values using Push commands, and then call
// [PopToStart] after Wait finishes, to close any open IO and reset.
func (c *Config) StartIO(cio *CmdIO, cmd string, args ...string) error {
cm, _, err := c.exec(&cio.StdIO, true, cmd, args...)
cio.Cmd = cm
return err
}
// OutputIO runs the command and returns the text from stdout.
func (c *Config) OutputIO(cio *CmdIO, cmd string, args ...string) (string, error) {
// need to use buf to capture output
sio := cio.StdIO // copy
buf := &bytes.Buffer{}
sio.Out = buf
_, _, err := c.exec(&sio, false, cmd, args...)
if cio.Out != nil {
cio.Out.Write(buf.Bytes())
}
return strings.TrimSuffix(buf.String(), "\n"), err
}
// 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 exec provides an easy way to execute commands,
// improving the ease-of-use and error handling of the
// standard library os/exec package. For example:
//
// err := exec.Run("git", "commit", "-am")
// // or
// err := exec.RunSh("git commit -am")
// // or
// err := exec.Verbose().Run("git", "commit", "-am")
package exec
//go:generate core generate
import (
"io"
"os"
"log/slog"
"cogentcore.org/core/base/logx"
)
// Config contains the configuration information that
// controls the behavior of exec. It is passed to most
// high-level functions, and a default version of it
// can be easily constructed using [DefaultConfig].
type Config struct { //types:add -setters
// Buffer is whether to buffer the output of Stdout and Stderr,
// which is necessary for the correct printing of commands and output
// when there is an error with a command, and for correct coloring
// on Windows. Therefore, it should be kept at the default value of
// true in most cases, except for when a command will run for a log
// time and print output throughout (eg: a log command).
Buffer bool
// PrintOnly is whether to only print commands that would be run and
// not actually run them. It can be used, for example, for safely testing
// an app.
PrintOnly bool
// The directory to execute commands in. If it is unset,
// commands are run in the current directory.
Dir string
// Env contains any additional environment variables specified.
// The current environment variables will also be passed to the
// command, but they will be overridden by any variables here
// if there are conflicts.
Env map[string]string `set:"-"`
// Echo is the writer for echoing the command string to.
// It can be set to nil to disable echoing.
Echo io.Writer
// Standard Input / Output management
StdIO StdIO
}
// SetStdout sets the standard output
func (c *Config) SetStdout(w io.Writer) *Config {
c.StdIO.Out = w
return c
}
// SetStderr sets the standard error
func (c *Config) SetStderr(w io.Writer) *Config {
c.StdIO.Err = w
return c
}
// SetStdin sets the standard input
func (c *Config) SetStdin(r io.Reader) *Config {
c.StdIO.In = r
return c
}
// major is the config object for [Major] specified through [SetMajor]
var major *Config
// Major returns the default [Config] object for a major command,
// based on [logx.UserLevel]. It should be used for commands that
// are central to an app's logic and are more important for the user
// to know about and be able to see the output of. It results in
// commands and output being printed with a [logx.UserLevel] of
// [slog.LevelInfo] or below, whereas [Minor] results in that when
// it is [slog.LevelDebug] or below. Most commands in a typical use
// case should be Major, which is why the global helper functions
// operate on it. The object returned by Major is guaranteed to be
// unique, so it can be modified directly.
func Major() *Config {
if major != nil {
// need to make a new copy so people can't modify the underlying
res := *major
return &res
}
if logx.UserLevel <= slog.LevelInfo {
c := &Config{
Buffer: true,
Env: map[string]string{},
Echo: os.Stdout,
}
c.StdIO.SetFromOS()
return c
}
c := &Config{
Buffer: true,
Env: map[string]string{},
}
c.StdIO.SetFromOS()
c.StdIO.Out = nil
return c
}
// SetMajor sets the config object that [Major] returns. It should
// be used sparingly, and only in cases where there is a clear property
// that should be set for all commands. If the given config object is
// nil, [Major] will go back to returning its default value.
func SetMajor(c *Config) {
major = c
}
// minor is the config object for [Minor] specified through [SetMinor]
var minor *Config
// Minor returns the default [Config] object for a minor command,
// based on [logx.UserLevel]. It should be used for commands that
// support an app behind the scenes and are less important for the
// user to know about and be able to see the output of. It results in
// commands and output being printed with a [logx.UserLevel] of
// [slog.LevelDebug] or below, whereas [Major] results in that when
// it is [slog.LevelInfo] or below. The object returned by Minor is
// guaranteed to be unique, so it can be modified directly.
func Minor() *Config {
if minor != nil {
// need to make a new copy so people can't modify the underlying
res := *minor
return &res
}
if logx.UserLevel <= slog.LevelDebug {
c := &Config{
Buffer: true,
Env: map[string]string{},
Echo: os.Stdout,
}
c.StdIO.SetFromOS()
return c
}
c := &Config{
Buffer: true,
Env: map[string]string{},
}
c.StdIO.SetFromOS()
c.StdIO.Out = nil
return c
}
// SetMinor sets the config object that [Minor] returns. It should
// be used sparingly, and only in cases where there is a clear property
// that should be set for all commands. If the given config object is
// nil, [Minor] will go back to returning its default value.
func SetMinor(c *Config) {
minor = c
}
// verbose is the config object for [Verbose] specified through [SetVerbose]
var verbose *Config
// Verbose returns the default [Config] object for a verbose command,
// based on [logx.UserLevel]. It should be used for commands
// whose output are central to an application; for example, for a
// logger or app runner. It results in commands and output being
// printed with a [logx.UserLevel] of [slog.LevelWarn] or below,
// whereas [Major] and [Minor] result in that when it is [slog.LevelInfo]
// and [slog.levelDebug] or below, respectively. The object returned by
// Verbose is guaranteed to be unique, so it can be modified directly.
func Verbose() *Config {
if verbose != nil {
// need to make a new copy so people can't modify the underlying
res := *verbose
return &res
}
if logx.UserLevel <= slog.LevelWarn {
c := &Config{
Buffer: true,
Env: map[string]string{},
Echo: os.Stdout,
}
c.StdIO.SetFromOS()
return c
}
c := &Config{
Buffer: true,
Env: map[string]string{},
}
c.StdIO.SetFromOS()
c.StdIO.Out = nil
return c
}
// SetVerbose sets the config object that [Verbose] returns. It should
// be used sparingly, and only in cases where there is a clear property
// that should be set for all commands. If the given config object is
// nil, [Verbose] will go back to returning its default value.
func SetVerbose(c *Config) {
verbose = c
}
// silent is the config object for [Silent] specified through [SetSilent]
var silent *Config
// Silent returns the default [Config] object for a silent command,
// based on [logx.UserLevel]. It should be used for commands that
// whose output/input is private and needs to be always hidden from
// the user; for example, for a command that involves passwords.
// It results in commands and output never being printed. The object
// returned by Silent is guaranteed to be unique, so it can be modified directly.
func Silent() *Config {
if silent != nil {
// need to make a new copy so people can't modify the underlying
res := *silent
return &res
}
c := &Config{
Buffer: true,
Env: map[string]string{},
}
c.StdIO.In = os.Stdin
return c
}
// SetSilent sets the config object that [Silent] returns. It should
// be used sparingly, and only in cases where there is a clear property
// that should be set for all commands. If the given config object is
// nil, [Silent] will go back to returning its default value.
func SetSilent(c *Config) {
silent = c
}
// GetWriter returns the appropriate writer to use based on the given writer and error.
// If the given error is non-nil, the returned writer is guaranteed to be non-nil,
// with [Config.Stderr] used as a backup. Otherwise, the returned writer will only
// be non-nil if the passed one is.
func (c *Config) GetWriter(w io.Writer, err error) io.Writer {
res := w
if res == nil && err != nil {
res = c.StdIO.Err
}
return res
}
// PrintCmd uses [GetWriter] to print the given command to [Config.Echo]
// or [Config.Stderr], based on the given error and the config settings.
// A newline is automatically inserted.
func (c *Config) PrintCmd(cmd string, err error) {
cmds := c.GetWriter(c.Echo, err)
if cmds != nil {
if c.Dir != "" {
cmds.Write([]byte(logx.SuccessColor(c.Dir) + ": "))
}
cmds.Write([]byte(logx.CmdColor(cmd) + "\n"))
}
}
// PrintCmd calls [Config.PrintCmd] on [Major]
func PrintCmd(cmd string, err error) {
Major().PrintCmd(cmd, err)
}
// SetEnv sets the given environment variable.
func (c *Config) SetEnv(key, val string) *Config {
c.Env[key] = val
return c
}
// 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.
// Adapted in part from: https://github.com/magefile/mage
// Copyright presumably by Nate Finch, primary contributor
// Apache License, Version 2.0, January 2004
package exec
import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"
"cogentcore.org/core/base/logx"
)
// exec executes the command, piping its stdout and stderr to the config
// writers. If start, uses cmd.Start, else.Run.
// If the command fails, it will return an error with the command output.
// The given cmd and args may include references
// to environment variables in $FOO format, in which case these will be
// expanded before the command is run.
//
// Ran reports if the command ran (rather than was not found or not executable).
// Code reports the exit code the command returned if it ran. If err == nil, ran
// is always true and code is always 0.
func (c *Config) exec(sio *StdIO, start bool, cmd string, args ...string) (excmd *exec.Cmd, ran bool, err error) {
expand := func(s string) string {
s2, ok := c.Env[s]
if ok {
return s2
}
return os.Getenv(s)
}
cmd = os.Expand(cmd, expand)
for i := range args {
args[i] = os.Expand(args[i], expand)
}
excmd, ran, code, err := c.run(sio, start, cmd, args...)
_ = code
if err == nil {
return excmd, true, nil
}
return excmd, ran, fmt.Errorf(`failed to run "%s %s: %v"`, cmd, strings.Join(args, " "), err)
}
func (c *Config) run(sio *StdIO, start bool, cmd string, args ...string) (excmd *exec.Cmd, ran bool, code int, err error) {
cm := exec.Command(cmd, args...)
cm.Env = os.Environ()
for k, v := range c.Env {
cm.Env = append(cm.Env, k+"="+v)
}
// need to store in buffer so we can color and print commands and stdout correctly
// (need to declare regardless even if we aren't using so that it is accessible)
ebuf := &bytes.Buffer{}
obuf := &bytes.Buffer{}
if !start && c.Buffer {
cm.Stderr = ebuf
cm.Stdout = obuf
} else {
cm.Stderr = sio.Err
cm.Stdout = sio.Out
}
// need to do now because we aren't buffering, or we are guaranteed to print them
// regardless of whether there is an error anyway, so we should print it now so
// people can see it earlier (especially important if it runs for a long time).
if !start || !c.Buffer || c.Echo != nil {
c.PrintCmd(cmd+" "+strings.Join(args, " "), err)
}
cm.Stdin = sio.In
cm.Dir = c.Dir
if !c.PrintOnly {
if start {
err = cm.Start()
excmd = cm
} else {
err = cm.Run()
}
// we must call InitColor after calling a system command
// TODO(kai): maybe figure out a better solution to this
// or expand this list
if cmd == "cp" || cmd == "ls" || cmd == "mv" {
logx.InitColor()
}
}
if !start && c.Buffer {
// if we have an error, we print the commands and stdout regardless of the config info
// (however, we don't print the command if we are guaranteed to print it regardless, as
// we already printed it above in that case)
if c.Echo == nil {
c.PrintCmd(cmd+" "+strings.Join(args, " "), err)
}
sout := c.GetWriter(sio.Out, err)
if sout != nil {
sout.Write(obuf.Bytes())
}
estr := ebuf.String()
if estr != "" && sio.Err != nil {
sio.Err.Write([]byte(logx.ErrorColor(estr)))
}
}
return excmd, CmdRan(err), ExitStatus(err), err
}
// CmdRan examines the error to determine if it was generated as a result of a
// command running via os/exec.Command. If the error is nil, or the command ran
// (even if it exited with a non-zero exit code), CmdRan reports true. If the
// error is an unrecognized type, or it is an error from exec.Command that says
// the command failed to run (usually due to the command not existing or not
// being executable), it reports false.
func CmdRan(err error) bool {
if err == nil {
return true
}
ee, ok := err.(*exec.ExitError)
if ok {
return ee.Exited()
}
return false
}
type exitStatus interface {
ExitStatus() int
}
// ExitStatus returns the exit status of the error if it is an exec.ExitError
// or if it implements ExitStatus() int.
// 0 if it is nil or 1 if it is a different error.
func ExitStatus(err error) int {
if err == nil {
return 0
}
if e, ok := err.(exitStatus); ok {
return e.ExitStatus()
}
if e, ok := err.(*exec.ExitError); ok {
if ex, ok := e.Sys().(exitStatus); ok {
return ex.ExitStatus()
}
}
return 1
}
// 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 exec
import (
"fmt"
"os"
"os/exec"
)
// LookPath searches for an executable named file in the
// directories named by the PATH environment variable.
// If file contains a slash, it is tried directly and the PATH is not consulted.
// Otherwise, on success, the result is an absolute path.
//
// In older versions of Go, LookPath could return a path relative to the current directory.
// As of Go 1.19, LookPath will instead return that path along with an error satisfying
// errors.Is(err, ErrDot). See the package documentation for more details.
func LookPath(file string) (string, error) { return exec.LookPath(file) }
// RemoveAll is a helper function that calls [os.RemoveAll] and [Config.PrintCmd].
func (c *Config) RemoveAll(path string) error {
var err error
if !c.PrintOnly {
err = os.RemoveAll(path)
}
c.PrintCmd(fmt.Sprintf("rm -rf %q", path), err)
return err
}
// RemoveAll calls [Config.RemoveAll] on [Major]
func RemoveAll(path string) error {
return Major().RemoveAll(path)
}
// MkdirAll is a helper function that calls [os.MkdirAll] and [Config.PrintCmd].
func (c *Config) MkdirAll(path string, perm os.FileMode) error {
var err error
if !c.PrintOnly {
err = os.MkdirAll(path, perm)
}
c.PrintCmd(fmt.Sprintf("mkdir -p %q", path), err)
return err
}
// MkdirAll calls [Config.MkdirAll] on [Major]
func MkdirAll(path string, perm os.FileMode) error {
return Major().MkdirAll(path, perm)
}
// 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.
// Adapted in part from: https://github.com/magefile/mage
// Copyright presumably by Nate Finch, primary contributor
// Apache License, Version 2.0, January 2004
package exec
import (
"bytes"
"os/exec"
"strings"
)
// Run runs the given command using the given
// configuration information and arguments,
// waiting for it to complete before returning.
func (c *Config) Run(cmd string, args ...string) error {
_, _, err := c.exec(&c.StdIO, false, cmd, args...)
return err
}
// Start starts the given command using the given
// configuration information and arguments,
// just starting the command but not waiting for it to finish.
// Returns the [exec.Cmd] command on which you can Wait for
// the command to finish (in a separate goroutine).
// See [StartIO] for a version that manages dynamic IO routing for you.
func (c *Config) Start(cmd string, args ...string) (*exec.Cmd, error) {
excmd, _, err := c.exec(&c.StdIO, true, cmd, args...)
return excmd, err
}
// Output runs the command and returns the text from stdout.
func (c *Config) Output(cmd string, args ...string) (string, error) {
// need to use buf to capture output
buf := &bytes.Buffer{}
sio := c.StdIO // copy
sio.Out = buf
_, _, err := c.exec(&sio, false, cmd, args...)
if c.StdIO.Out != nil {
c.StdIO.Out.Write(buf.Bytes())
}
return strings.TrimSuffix(buf.String(), "\n"), err
}
// Run calls [Config.Run] on [Major]
func Run(cmd string, args ...string) error {
return Major().Run(cmd, args...)
}
// Start calls [Config.Start] on [Major]
func Start(cmd string, args ...string) (*exec.Cmd, error) {
return Major().Start(cmd, args...)
}
// Output calls [Config.Output] on [Major]
func Output(cmd string, args ...string) (string, error) {
return Major().Output(cmd, args...)
}
// Copyright (c) 2024, 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 exec
import (
"fmt"
"io"
"io/fs"
"log/slog"
"os"
"cogentcore.org/core/base/stack"
)
// StdIO contains one set of standard input / output Reader / Writers.
type StdIO struct {
// Out is the writer to write the standard output of called commands to.
// It can be set to nil to disable the writing of the standard output.
Out io.Writer
// Err is the writer to write the standard error of called commands to.
// It can be set to nil to disable the writing of the standard error.
Err io.Writer
// In is the reader to use as the standard input.
In io.Reader
}
// SetFromOS sets all our IO to current os.Std*
func (st *StdIO) SetFromOS() {
st.Out, st.Err, st.In = os.Stdout, os.Stderr, os.Stdin
}
// SetAll sets all our IO from given args
func (st *StdIO) SetAll(out, err io.Writer, in io.Reader) {
st.Out, st.Err, st.In = out, err, in
}
// Set sets our values from other StdIO, returning
// the current values at time of call, to restore later.
func (st *StdIO) Set(o *StdIO) *StdIO {
cur := *st
*st = *o
return &cur
}
// SetToOS sets the current IO to os.Std*, returning
// a StdIO with the current IO settings prior to this call,
// which can be used to restore.
// Note: os.Std* are *os.File types, and this function will panic
// if the current IO are not actually *os.Files.
// The results of a prior SetToOS call will do the right thing for
// saving and restoring the os state.
func (st *StdIO) SetToOS() *StdIO {
cur := &StdIO{}
cur.SetFromOS()
os.Stdout = st.Out.(*os.File)
os.Stderr = st.Err.(*os.File)
os.Stdin = st.In.(*os.File)
return cur
}
// Print prints to the [StdIO.Out]
func (st *StdIO) Print(v ...any) {
fmt.Fprint(st.Out, v...)
}
// Println prints to the [StdIO.Out]
func (st *StdIO) Println(v ...any) {
fmt.Fprintln(st.Out, v...)
}
// Printf prints to the [StdIO.Out]
func (st *StdIO) Printf(f string, v ...any) {
fmt.Fprintf(st.Out, f, v...)
}
// ErrPrint prints to the [StdIO.Err]
func (st *StdIO) ErrPrint(v ...any) {
fmt.Fprint(st.Err, v...)
}
// ErrPrintln prints to the [StdIO.Err]
func (st *StdIO) ErrPrintln(v ...any) {
fmt.Fprintln(st.Err, v...)
}
// ErrPrintf prints to the [StdIO.Err]
func (st *StdIO) ErrPrintf(f string, v ...any) {
fmt.Fprintf(st.Err, f, v...)
}
// IsPipe returns true if the given object is an os.File corresponding to a Pipe,
// which is also not the same as the current os.Stdout, in case that is a Pipe.
func IsPipe(rw any) bool {
if rw == nil {
return false
}
w, ok := rw.(io.Writer)
if !ok {
return false
}
if w == os.Stdout {
return false
}
of, ok := rw.(*os.File)
if !ok {
return false
}
st, err := of.Stat()
if err != nil {
return false
}
md := st.Mode()
if md&fs.ModeNamedPipe != 0 {
return true
}
return md&fs.ModeCharDevice == 0
}
// OutIsPipe returns true if current Out is a Pipe
func (st *StdIO) OutIsPipe() bool { return IsPipe(st.Out) }
// StdIOState maintains a stack of StdIO settings for easier management
// of dynamic IO routing. Call [StackStart] prior to
// setting the IO values using Push commands, and then call
// [PopToStart] when done to close any open IO and reset.
type StdIOState struct {
StdIO
// OutStack is stack of out
OutStack stack.Stack[io.Writer]
// ErrStack is stack of err
ErrStack stack.Stack[io.Writer]
// InStack is stack of in
InStack stack.Stack[io.Reader]
// PipeIn is a stack of the os.File to use for reading from the Out,
// when Out is a Pipe, created by [PushOutPipe].
// Use [OutIsPipe] function to determine if the current output is a Pipe
// in order to determine whether to use the current [PipeIn.Peek()].
// These will be automatically closed during [PopToStart] whenever the
// corresponding Out is a Pipe.
PipeIn stack.Stack[*os.File]
// Starting depths of the respective stacks, for unwinding the stack
// to a defined starting point.
OutStart, ErrStart, InStart int
}
// PushOut pushes a new io.Writer as the current [Out],
// saving the current one on a stack, which can be restored using [PopOut].
func (st *StdIOState) PushOut(out io.Writer) {
st.OutStack.Push(st.Out)
st.Out = out
}
// PushOutPipe calls os.Pipe() and pushes the writer side
// as the new [Out], and pushes the Reader side to [PipeIn]
// which should then be used as the [In] for any other relevant process.
// Call [OutIsPipe] to determine if the current Out is a Pipe, to know
// whether to use the PipeIn.Peek() value.
func (st *StdIOState) PushOutPipe() {
r, w, err := os.Pipe()
if err != nil {
slog.Error(err.Error())
}
st.PushOut(w)
st.PipeIn.Push(r)
}
// PopOut restores previous io.Writer as [Out] from the stack,
// saved during [PushOut], returning the current Out at time of call.
// Pops and closes corresponding PipeIn if current Out is a Pipe.
// This does NOT close the current one, in case you need to use it before closing,
// so that is your responsibility (see [PopToStart] that does this for you).
func (st *StdIOState) PopOut() io.Writer {
if st.OutIsPipe() && len(st.PipeIn) > 0 {
CloseReader(st.PipeIn.Pop())
}
cur := st.Out
st.Out = st.OutStack.Pop()
return cur
}
// PushErr pushes a new io.Writer as the current [Err],
// saving the current one on a stack, which can be restored using [PopErr].
func (st *StdIOState) PushErr(err io.Writer) {
st.ErrStack.Push(st.Err)
st.Err = err
}
// PopErr restores previous io.Writer as [Err] from the stack,
// saved during [PushErr], returning the current Err at time of call.
// This does NOT close the current one, in case you need to use it before closing,
// so that is your responsibility (see [PopToStart] that does this for you).
// Note that Err is often the same as Out, in which case only Out should be closed.
func (st *StdIOState) PopErr() io.Writer {
cur := st.Err
st.Err = st.ErrStack.Pop()
return cur
}
// PushIn pushes a new [io.Reader] as the current [In],
// saving the current one on a stack, which can be restored using [PopIn].
func (st *StdIOState) PushIn(in io.Reader) {
st.InStack.Push(st.In)
st.In = in
}
// PopIn restores previous io.Reader as [In] from the stack,
// saved during [PushIn], returning the current In at time of call.
// This does NOT close the current one, in case you need to use it before closing,
// so that is your responsibility (see [PopToStart] that does this for you).
func (st *StdIOState) PopIn() io.Reader {
cur := st.In
st.In = st.InStack.Pop()
return cur
}
// StackStart records the starting depths of the IO stacks
func (st *StdIOState) StackStart() {
st.OutStart = len(st.OutStack)
st.ErrStart = len(st.ErrStack)
st.InStart = len(st.InStack)
}
// PopToStart unwinds the IO stacks to the depths recorded at [StackStart],
// automatically closing the ones that are popped.
// It automatically checks if any of the Err items are the same as Out
// and does not redundantly close those.
func (st *StdIOState) PopToStart() {
for len(st.ErrStack) > st.ErrStart {
er := st.PopErr()
if !st.ErrIsInOut(er) {
fmt.Println("close err")
CloseWriter(er)
}
}
for len(st.OutStack) > st.OutStart {
CloseWriter(st.PopOut())
}
for len(st.InStack) > st.InStart {
st.PopIn()
}
}
// ErrIsInOut returns true if the given Err writer is also present
// within the active (> [OutStart]) stack of Outs.
// If this is true, then Err should not be closed, as it will be closed
// when the Out is closed.
func (st *StdIOState) ErrIsInOut(er io.Writer) bool {
for i := st.OutStart; i < len(st.OutStack); i++ {
if st.OutStack[i] == er {
return true
}
}
return false
}
// CloseWriter closes given Writer, if it has a Close() method
func CloseWriter(w io.Writer) {
if st, ok := w.(io.Closer); ok {
st.Close()
}
}
// CloseReader closes given Reader, if it has a Close() method
func CloseReader(r io.Reader) {
if st, ok := r.(io.Closer); ok {
st.Close()
}
}
// WriteWrapper is an io.Writer that wraps another io.Writer
type WriteWrapper struct {
io.Writer
}
// ReadWrapper is an io.Reader that wraps another io.Reader
type ReadWrapper struct {
io.Reader
}
// NewWrappers initializes this StdIO with wrappers around given StdIO
func (st *StdIO) NewWrappers(o *StdIO) {
st.Out = &WriteWrapper{Writer: o.Out}
st.Err = &WriteWrapper{Writer: o.Err}
st.In = &ReadWrapper{Reader: o.In}
}
// SetWrappers sets the wrappers to current values from given StdIO,
// returning a copy of the wrapped values previously in place at start of call,
// which can be used in restoring state later.
// The wrappers must have been created using NewWrappers initially.
func (st *StdIO) SetWrappers(o *StdIO) *StdIO {
cur := st.GetWrapped()
st.Out.(*WriteWrapper).Writer = o.Out
st.Err.(*WriteWrapper).Writer = o.Err
st.In.(*ReadWrapper).Reader = o.In
return cur
}
// SetWrappedOut sets the wrapped Out to given writer.
// The wrappers must have been created using NewWrappers initially.
func (st *StdIO) SetWrappedOut(w io.Writer) {
st.Out.(*WriteWrapper).Writer = w
}
// SetWrappedErr sets the wrapped Err to given writer.
// The wrappers must have been created using NewWrappers initially.
func (st *StdIO) SetWrappedErr(w io.Writer) {
st.Err.(*WriteWrapper).Writer = w
}
// SetWrappedIn sets the wrapped In to given reader.
// The wrappers must have been created using NewWrappers initially.
func (st *StdIO) SetWrappedIn(r io.Reader) {
st.In.(*ReadWrapper).Reader = r
}
// GetWrapped returns the current wrapped values as a StdIO.
// The wrappers must have been created using NewWrappers initially.
func (st *StdIO) GetWrapped() *StdIO {
o := &StdIO{}
o.Out = st.Out.(*WriteWrapper).Writer
o.Err = st.Err.(*WriteWrapper).Writer
o.In = st.In.(*ReadWrapper).Reader
return o
}
// Code generated by "core generate"; DO NOT EDIT.
package exec
import (
"io"
"cogentcore.org/core/types"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/base/exec.Config", IDName: "config", Doc: "Config contains the configuration information that\ncontrols the behavior of exec. It is passed to most\nhigh-level functions, and a default version of it\ncan be easily constructed using [DefaultConfig].", Directives: []types.Directive{{Tool: "types", Directive: "add", Args: []string{"-setters"}}}, Fields: []types.Field{{Name: "Buffer", Doc: "Buffer is whether to buffer the output of Stdout and Stderr,\nwhich is necessary for the correct printing of commands and output\nwhen there is an error with a command, and for correct coloring\non Windows. Therefore, it should be kept at the default value of\ntrue in most cases, except for when a command will run for a log\ntime and print output throughout (eg: a log command)."}, {Name: "PrintOnly", Doc: "PrintOnly is whether to only print commands that would be run and\nnot actually run them. It can be used, for example, for safely testing\nan app."}, {Name: "Dir", Doc: "The directory to execute commands in. If it is unset,\ncommands are run in the current directory."}, {Name: "Env", Doc: "Env contains any additional environment variables specified.\nThe current environment variables will also be passed to the\ncommand, but they will be overridden by any variables here\nif there are conflicts."}, {Name: "Echo", Doc: "Echo is the writer for echoing the command string to.\nIt can be set to nil to disable echoing."}, {Name: "StdIO", Doc: "Standard Input / Output management"}}})
// SetBuffer sets the [Config.Buffer]:
// Buffer is whether to buffer the output of Stdout and Stderr,
// which is necessary for the correct printing of commands and output
// when there is an error with a command, and for correct coloring
// on Windows. Therefore, it should be kept at the default value of
// true in most cases, except for when a command will run for a log
// time and print output throughout (eg: a log command).
func (t *Config) SetBuffer(v bool) *Config { t.Buffer = v; return t }
// SetPrintOnly sets the [Config.PrintOnly]:
// PrintOnly is whether to only print commands that would be run and
// not actually run them. It can be used, for example, for safely testing
// an app.
func (t *Config) SetPrintOnly(v bool) *Config { t.PrintOnly = v; return t }
// SetDir sets the [Config.Dir]:
// The directory to execute commands in. If it is unset,
// commands are run in the current directory.
func (t *Config) SetDir(v string) *Config { t.Dir = v; return t }
// SetEcho sets the [Config.Echo]:
// Echo is the writer for echoing the command string to.
// It can be set to nil to disable echoing.
func (t *Config) SetEcho(v io.Writer) *Config { t.Echo = v; return t }
// SetStdIO sets the [Config.StdIO]:
// Standard Input / Output management
func (t *Config) SetStdIO(v StdIO) *Config { t.StdIO = v; return t }
// Code generated by "core generate"; DO NOT EDIT.
package fileinfo
import (
"cogentcore.org/core/enums"
)
var _CategoriesValues = []Categories{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
// CategoriesN is the highest valid value for type Categories, plus one.
const CategoriesN Categories = 16
var _CategoriesValueMap = map[string]Categories{`UnknownCategory`: 0, `Folder`: 1, `Archive`: 2, `Backup`: 3, `Code`: 4, `Doc`: 5, `Sheet`: 6, `Data`: 7, `Text`: 8, `Image`: 9, `Model`: 10, `Audio`: 11, `Video`: 12, `Font`: 13, `Exe`: 14, `Bin`: 15}
var _CategoriesDescMap = map[Categories]string{0: `UnknownCategory is an unknown file category`, 1: `Folder is a folder / directory`, 2: `Archive is a collection of files, e.g., zip tar`, 3: `Backup is a backup file (# ~ etc)`, 4: `Code is a programming language file`, 5: `Doc is an editable word processing file including latex, markdown, html, css, etc`, 6: `Sheet is a spreadsheet file (.xls etc)`, 7: `Data is some kind of data format (csv, json, database, etc)`, 8: `Text is some other kind of text file`, 9: `Image is an image (jpeg, png, svg, etc) *including* PDF`, 10: `Model is a 3D model`, 11: `Audio is an audio file`, 12: `Video is a video file`, 13: `Font is a font file`, 14: `Exe is a binary executable file (scripts go in Code)`, 15: `Bin is some other type of binary (object files, libraries, etc)`}
var _CategoriesMap = map[Categories]string{0: `UnknownCategory`, 1: `Folder`, 2: `Archive`, 3: `Backup`, 4: `Code`, 5: `Doc`, 6: `Sheet`, 7: `Data`, 8: `Text`, 9: `Image`, 10: `Model`, 11: `Audio`, 12: `Video`, 13: `Font`, 14: `Exe`, 15: `Bin`}
// String returns the string representation of this Categories value.
func (i Categories) String() string { return enums.String(i, _CategoriesMap) }
// SetString sets the Categories value from its string representation,
// and returns an error if the string is invalid.
func (i *Categories) SetString(s string) error {
return enums.SetString(i, s, _CategoriesValueMap, "Categories")
}
// Int64 returns the Categories value as an int64.
func (i Categories) Int64() int64 { return int64(i) }
// SetInt64 sets the Categories value from an int64.
func (i *Categories) SetInt64(in int64) { *i = Categories(in) }
// Desc returns the description of the Categories value.
func (i Categories) Desc() string { return enums.Desc(i, _CategoriesDescMap) }
// CategoriesValues returns all possible values for the type Categories.
func CategoriesValues() []Categories { return _CategoriesValues }
// Values returns all possible values for the type Categories.
func (i Categories) Values() []enums.Enum { return enums.Values(_CategoriesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Categories) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Categories) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "Categories")
}
var _KnownValues = []Known{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130}
// KnownN is the highest valid value for type Known, plus one.
const KnownN Known = 131
var _KnownValueMap = map[string]Known{`Unknown`: 0, `Any`: 1, `AnyKnown`: 2, `AnyFolder`: 3, `AnyArchive`: 4, `Multipart`: 5, `Tar`: 6, `Zip`: 7, `GZip`: 8, `SevenZ`: 9, `Xz`: 10, `BZip`: 11, `Dmg`: 12, `Shar`: 13, `AnyBackup`: 14, `Trash`: 15, `AnyCode`: 16, `Ada`: 17, `Bash`: 18, `Cosh`: 19, `Csh`: 20, `C`: 21, `CSharp`: 22, `D`: 23, `Diff`: 24, `Eiffel`: 25, `Erlang`: 26, `Forth`: 27, `Fortran`: 28, `FSharp`: 29, `Go`: 30, `Haskell`: 31, `Java`: 32, `JavaScript`: 33, `Lisp`: 34, `Lua`: 35, `Makefile`: 36, `Mathematica`: 37, `Matlab`: 38, `ObjC`: 39, `OCaml`: 40, `Pascal`: 41, `Perl`: 42, `Php`: 43, `Prolog`: 44, `Python`: 45, `R`: 46, `Ruby`: 47, `Rust`: 48, `Scala`: 49, `Tcl`: 50, `AnyDoc`: 51, `BibTeX`: 52, `TeX`: 53, `Texinfo`: 54, `Troff`: 55, `Html`: 56, `Css`: 57, `Markdown`: 58, `Rtf`: 59, `MSWord`: 60, `OpenText`: 61, `OpenPres`: 62, `MSPowerpoint`: 63, `EBook`: 64, `EPub`: 65, `AnySheet`: 66, `MSExcel`: 67, `OpenSheet`: 68, `AnyData`: 69, `Csv`: 70, `Json`: 71, `Xml`: 72, `Protobuf`: 73, `Ini`: 74, `Tsv`: 75, `Uri`: 76, `Color`: 77, `Yaml`: 78, `Toml`: 79, `Number`: 80, `String`: 81, `Tensor`: 82, `Table`: 83, `AnyText`: 84, `PlainText`: 85, `ICal`: 86, `VCal`: 87, `VCard`: 88, `AnyImage`: 89, `Pdf`: 90, `Postscript`: 91, `Gimp`: 92, `GraphVis`: 93, `Gif`: 94, `Jpeg`: 95, `Png`: 96, `Svg`: 97, `Tiff`: 98, `Pnm`: 99, `Pbm`: 100, `Pgm`: 101, `Ppm`: 102, `Xbm`: 103, `Xpm`: 104, `Bmp`: 105, `Heic`: 106, `Heif`: 107, `AnyModel`: 108, `Vrml`: 109, `X3d`: 110, `Obj`: 111, `AnyAudio`: 112, `Aac`: 113, `Flac`: 114, `Mp3`: 115, `Ogg`: 116, `Midi`: 117, `Wav`: 118, `AnyVideo`: 119, `Mpeg`: 120, `Mp4`: 121, `Mov`: 122, `Ogv`: 123, `Wmv`: 124, `Avi`: 125, `AnyFont`: 126, `TrueType`: 127, `WebOpenFont`: 128, `AnyExe`: 129, `AnyBin`: 130}
var _KnownDescMap = map[Known]string{0: `Unknown = a non-known file type`, 1: `Any is used when selecting a file type, if any type is OK (including Unknown) see also AnyKnown and the Any options for each category`, 2: `AnyKnown is used when selecting a file type, if any Known file type is OK (excludes Unknown) -- see Any and Any options for each category`, 3: `Folder is a folder / directory`, 4: `Archive is a collection of files, e.g., zip tar`, 5: ``, 6: ``, 7: ``, 8: ``, 9: ``, 10: ``, 11: ``, 12: ``, 13: ``, 14: `Backup files`, 15: ``, 16: `Code is a programming language file`, 17: ``, 18: ``, 19: ``, 20: ``, 21: ``, 22: ``, 23: ``, 24: ``, 25: ``, 26: ``, 27: ``, 28: ``, 29: ``, 30: ``, 31: ``, 32: ``, 33: ``, 34: ``, 35: ``, 36: ``, 37: ``, 38: ``, 39: ``, 40: ``, 41: ``, 42: ``, 43: ``, 44: ``, 45: ``, 46: ``, 47: ``, 48: ``, 49: ``, 50: ``, 51: `Doc is an editable word processing file including latex, markdown, html, css, etc`, 52: ``, 53: ``, 54: ``, 55: ``, 56: ``, 57: ``, 58: ``, 59: ``, 60: ``, 61: ``, 62: ``, 63: ``, 64: ``, 65: ``, 66: `Sheet is a spreadsheet file (.xls etc)`, 67: ``, 68: ``, 69: `Data is some kind of data format (csv, json, database, etc)`, 70: ``, 71: ``, 72: ``, 73: ``, 74: ``, 75: ``, 76: ``, 77: ``, 78: ``, 79: ``, 80: `special support for data fs`, 81: ``, 82: ``, 83: ``, 84: `Text is some other kind of text file`, 85: ``, 86: ``, 87: ``, 88: ``, 89: `Image is an image (jpeg, png, svg, etc) *including* PDF`, 90: ``, 91: ``, 92: ``, 93: ``, 94: ``, 95: ``, 96: ``, 97: ``, 98: ``, 99: ``, 100: ``, 101: ``, 102: ``, 103: ``, 104: ``, 105: ``, 106: ``, 107: ``, 108: `Model is a 3D model`, 109: ``, 110: ``, 111: ``, 112: `Audio is an audio file`, 113: ``, 114: ``, 115: ``, 116: ``, 117: ``, 118: ``, 119: `Video is a video file`, 120: ``, 121: ``, 122: ``, 123: ``, 124: ``, 125: ``, 126: `Font is a font file`, 127: ``, 128: ``, 129: `Exe is a binary executable file`, 130: `Bin is some other unrecognized binary type`}
var _KnownMap = map[Known]string{0: `Unknown`, 1: `Any`, 2: `AnyKnown`, 3: `AnyFolder`, 4: `AnyArchive`, 5: `Multipart`, 6: `Tar`, 7: `Zip`, 8: `GZip`, 9: `SevenZ`, 10: `Xz`, 11: `BZip`, 12: `Dmg`, 13: `Shar`, 14: `AnyBackup`, 15: `Trash`, 16: `AnyCode`, 17: `Ada`, 18: `Bash`, 19: `Cosh`, 20: `Csh`, 21: `C`, 22: `CSharp`, 23: `D`, 24: `Diff`, 25: `Eiffel`, 26: `Erlang`, 27: `Forth`, 28: `Fortran`, 29: `FSharp`, 30: `Go`, 31: `Haskell`, 32: `Java`, 33: `JavaScript`, 34: `Lisp`, 35: `Lua`, 36: `Makefile`, 37: `Mathematica`, 38: `Matlab`, 39: `ObjC`, 40: `OCaml`, 41: `Pascal`, 42: `Perl`, 43: `Php`, 44: `Prolog`, 45: `Python`, 46: `R`, 47: `Ruby`, 48: `Rust`, 49: `Scala`, 50: `Tcl`, 51: `AnyDoc`, 52: `BibTeX`, 53: `TeX`, 54: `Texinfo`, 55: `Troff`, 56: `Html`, 57: `Css`, 58: `Markdown`, 59: `Rtf`, 60: `MSWord`, 61: `OpenText`, 62: `OpenPres`, 63: `MSPowerpoint`, 64: `EBook`, 65: `EPub`, 66: `AnySheet`, 67: `MSExcel`, 68: `OpenSheet`, 69: `AnyData`, 70: `Csv`, 71: `Json`, 72: `Xml`, 73: `Protobuf`, 74: `Ini`, 75: `Tsv`, 76: `Uri`, 77: `Color`, 78: `Yaml`, 79: `Toml`, 80: `Number`, 81: `String`, 82: `Tensor`, 83: `Table`, 84: `AnyText`, 85: `PlainText`, 86: `ICal`, 87: `VCal`, 88: `VCard`, 89: `AnyImage`, 90: `Pdf`, 91: `Postscript`, 92: `Gimp`, 93: `GraphVis`, 94: `Gif`, 95: `Jpeg`, 96: `Png`, 97: `Svg`, 98: `Tiff`, 99: `Pnm`, 100: `Pbm`, 101: `Pgm`, 102: `Ppm`, 103: `Xbm`, 104: `Xpm`, 105: `Bmp`, 106: `Heic`, 107: `Heif`, 108: `AnyModel`, 109: `Vrml`, 110: `X3d`, 111: `Obj`, 112: `AnyAudio`, 113: `Aac`, 114: `Flac`, 115: `Mp3`, 116: `Ogg`, 117: `Midi`, 118: `Wav`, 119: `AnyVideo`, 120: `Mpeg`, 121: `Mp4`, 122: `Mov`, 123: `Ogv`, 124: `Wmv`, 125: `Avi`, 126: `AnyFont`, 127: `TrueType`, 128: `WebOpenFont`, 129: `AnyExe`, 130: `AnyBin`}
// String returns the string representation of this Known value.
func (i Known) String() string { return enums.String(i, _KnownMap) }
// SetString sets the Known value from its string representation,
// and returns an error if the string is invalid.
func (i *Known) SetString(s string) error { return enums.SetString(i, s, _KnownValueMap, "Known") }
// Int64 returns the Known value as an int64.
func (i Known) Int64() int64 { return int64(i) }
// SetInt64 sets the Known value from an int64.
func (i *Known) SetInt64(in int64) { *i = Known(in) }
// Desc returns the description of the Known value.
func (i Known) Desc() string { return enums.Desc(i, _KnownDescMap) }
// KnownValues returns all possible values for the type Known.
func KnownValues() []Known { return _KnownValues }
// Values returns all possible values for the type Known.
func (i Known) Values() []enums.Enum { return enums.Values(_KnownValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Known) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Known) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Known") }
// 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 fileinfo
// Categories is a functional category for files; a broad functional
// categorization that can help decide what to do with the file.
//
// It is computed in part from the mime type, but some types require
// other information.
//
// No single categorization scheme is perfect, so any given use
// may require examination of the full mime type etc, but this
// provides a useful broad-scope categorization of file types.
type Categories int32 //enums:enum
const (
// UnknownCategory is an unknown file category
UnknownCategory Categories = iota
// Folder is a folder / directory
Folder
// Archive is a collection of files, e.g., zip tar
Archive
// Backup is a backup file (# ~ etc)
Backup
// Code is a programming language file
Code
// Doc is an editable word processing file including latex, markdown, html, css, etc
Doc
// Sheet is a spreadsheet file (.xls etc)
Sheet
// Data is some kind of data format (csv, json, database, etc)
Data
// Text is some other kind of text file
Text
// Image is an image (jpeg, png, svg, etc) *including* PDF
Image
// Model is a 3D model
Model
// Audio is an audio file
Audio
// Video is a video file
Video
// Font is a font file
Font
// Exe is a binary executable file (scripts go in Code)
Exe
// Bin is some other type of binary (object files, libraries, etc)
Bin
)
// CategoryFromMime returns the file category based on the mime type;
// not all Categories can be inferred from file types
func CategoryFromMime(mime string) Categories {
if mime == "" {
return UnknownCategory
}
mime = MimeNoChar(mime)
if mt, has := AvailableMimes[mime]; has {
return mt.Cat // must be set!
}
// try from type:
ms := MimeTop(mime)
if ms == "" {
return UnknownCategory
}
switch ms {
case "image":
return Image
case "audio":
return Audio
case "video":
return Video
case "font":
return Font
case "model":
return Model
}
if ms == "text" {
return Text
}
return UnknownCategory
}
// 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 fileinfo manages file information and categorizes file types;
// it is the single, consolidated place where file info, mimetypes, and
// filetypes are managed in Cogent Core.
//
// This whole space is a bit of a heterogenous mess; most file types
// we care about are not registered on the official iana registry, which
// seems mainly to have paid registrations in application/ category,
// and doesn't have any of the main programming languages etc.
//
// The official Go std library support depends on different platform
// libraries and mac doesn't have one, so it has very limited support
//
// So we sucked it up and made a full list of all the major file types
// that we really care about and also provide a broader category-level organization
// that is useful for functionally organizing / operating on files.
//
// As fallback, we are this Go package:
// github.com/h2non/filetype
package fileinfo
import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"os"
"path"
"path/filepath"
"strings"
"time"
"cogentcore.org/core/base/datasize"
"cogentcore.org/core/base/vcs"
"cogentcore.org/core/icons"
"github.com/Bios-Marcel/wastebasket"
)
// FileInfo represents the information about a given file / directory,
// including icon, mimetype, etc
type FileInfo struct { //types:add
// icon for file
Ic icons.Icon `table:"no-header"`
// name of the file, without any path
Name string `width:"40"`
// size of the file
Size datasize.Size
// type of file / directory; shorter, more user-friendly
// version of mime type, based on category
Kind string `width:"20" max-width:"20"`
// full official mime type of the contents
Mime string `table:"-"`
// functional category of the file, based on mime data etc
Cat Categories `table:"-"`
// known file type
Known Known `table:"-"`
// file mode bits
Mode fs.FileMode `table:"-"`
// time that contents (only) were last modified
ModTime time.Time `label:"Last modified"`
// version control system status, when enabled
VCS vcs.FileStatus `table:"-"`
// full path to file, including name; for file functions
Path string `table:"-"`
}
// NewFileInfo returns a new FileInfo for given file.
func NewFileInfo(fname string) (*FileInfo, error) {
fi := &FileInfo{}
err := fi.InitFile(fname)
return fi, err
}
// NewFileInfoType returns a new FileInfo representing the given file type.
func NewFileInfoType(ftyp Known) *FileInfo {
fi := &FileInfo{}
fi.SetType(ftyp)
return fi
}
// InitFile initializes a FileInfo for os file based on a filename,
// which is updated to full path using filepath.Abs.
// Returns error from filepath.Abs and / or fs.Stat error on the given file,
// but file info will be updated based on the filename even if
// the file does not exist.
func (fi *FileInfo) InitFile(fname string) error {
var errs []error
path, err := filepath.Abs(fname)
if err == nil {
fi.Path = path
} else {
fi.Path = fname
}
_, fi.Name = filepath.Split(path)
fi.SetMimeInfo()
info, err := os.Stat(fi.Path)
if err != nil {
errs = append(errs, err)
} else {
fi.SetFileInfo(info)
}
return errors.Join(errs...)
}
// InitFileFS initializes a FileInfo based on filename in given fs.FS.
// Returns error from fs.Stat error on the given file,
// but file info will be updated based on the filename even if
// the file does not exist.
func (fi *FileInfo) InitFileFS(fsys fs.FS, fname string) error {
var errs []error
fi.Path = fname
_, fi.Name = path.Split(fname)
fi.SetMimeInfo()
info, err := fs.Stat(fsys, fi.Path)
if err != nil {
errs = append(errs, err)
} else {
fi.SetFileInfo(info)
}
return errors.Join(errs...)
}
// SetMimeInfo parses the file name to set mime type,
// which then drives Kind and Icon.
func (fi *FileInfo) SetMimeInfo() error {
if fi.Path == "" || fi.Path == "." || fi.IsDir() {
return nil
}
fi.Cat = UnknownCategory
fi.Known = Unknown
fi.Kind = ""
mtyp, _, err := MimeFromFile(fi.Path)
if err != nil {
return err
}
fi.Mime = mtyp
fi.Cat = CategoryFromMime(fi.Mime)
fi.Known = MimeKnown(fi.Mime)
if fi.Cat != UnknownCategory {
fi.Kind = fi.Cat.String() + ": "
}
if fi.Known != Unknown {
fi.Kind += fi.Known.String()
} else {
fi.Kind += MimeSub(fi.Mime)
}
return nil
}
// SetFileInfo updates from given [fs.FileInfo]
func (fi *FileInfo) SetFileInfo(info fs.FileInfo) {
fi.Size = datasize.Size(info.Size())
fi.Mode = info.Mode()
fi.ModTime = info.ModTime()
if info.IsDir() {
fi.Kind = "Folder"
fi.Cat = Folder
fi.Known = AnyFolder
} else {
if fi.Cat == UnknownCategory {
if fi.IsExec() {
fi.Cat = Exe
fi.Known = AnyExe
}
}
}
icn, _ := fi.FindIcon()
fi.Ic = icn
}
// SetType sets file type information for given Known file type
func (fi *FileInfo) SetType(ftyp Known) {
mt := MimeFromKnown(ftyp)
fi.Mime = mt.Mime
fi.Cat = mt.Cat
fi.Known = mt.Known
if fi.Name == "" && len(mt.Exts) > 0 {
fi.Name = "_fake" + mt.Exts[0]
fi.Path = fi.Name
}
fi.Kind = fi.Cat.String() + ": "
if fi.Known != Unknown {
fi.Kind += fi.Known.String()
}
}
// IsDir returns true if file is a directory (folder)
func (fi *FileInfo) IsDir() bool {
return fi.Mode.IsDir()
}
// IsExec returns true if file is an executable file
func (fi *FileInfo) IsExec() bool {
if fi.Mode&0111 != 0 {
return true
}
ext := filepath.Ext(fi.Path)
return ext == ".exe"
}
// IsSymLink returns true if file is a symbolic link
func (fi *FileInfo) IsSymlink() bool {
return fi.Mode&os.ModeSymlink != 0
}
// IsHidden returns true if file name starts with . or _ which are typically hidden
func (fi *FileInfo) IsHidden() bool {
return fi.Name == "" || fi.Name[0] == '.' || fi.Name[0] == '_'
}
//////////////////////////////////////////////////////////////////////////////
// File ops
// Duplicate creates a copy of given file -- only works for regular files, not
// directories.
func (fi *FileInfo) Duplicate() (string, error) { //types:add
if fi.IsDir() {
err := fmt.Errorf("core.Duplicate: cannot copy directory: %v", fi.Path)
log.Println(err)
return "", err
}
ext := filepath.Ext(fi.Path)
noext := strings.TrimSuffix(fi.Path, ext)
dst := noext + "_Copy" + ext
cpcnt := 0
for {
if _, err := os.Stat(dst); !os.IsNotExist(err) {
cpcnt++
dst = noext + fmt.Sprintf("_Copy%d", cpcnt) + ext
} else {
break
}
}
return dst, CopyFile(dst, fi.Path, fi.Mode)
}
// Delete moves the file to the trash / recycling bin.
// On mobile and web, it deletes it directly.
func (fi *FileInfo) Delete() error { //types:add
err := wastebasket.Trash(fi.Path)
if errors.Is(err, wastebasket.ErrPlatformNotSupported) {
return os.RemoveAll(fi.Path)
}
return err
}
// Filenames recursively adds fullpath filenames within the starting directory to the "names" slice.
// Directory names within the starting directory are not added.
func Filenames(d os.File, names *[]string) (err error) {
nms, err := d.Readdirnames(-1)
if err != nil {
return err
}
for _, n := range nms {
fp := filepath.Join(d.Name(), n)
ffi, ferr := os.Stat(fp)
if ferr != nil {
return ferr
}
if ffi.IsDir() {
dd, err := os.Open(fp)
if err != nil {
return err
}
defer dd.Close()
Filenames(*dd, names)
} else {
*names = append(*names, fp)
}
}
return nil
}
// Filenames returns a slice of file names from the starting directory and its subdirectories
func (fi *FileInfo) Filenames(names *[]string) (err error) {
if !fi.IsDir() {
err = errors.New("not a directory: Filenames returns a list of files within a directory")
return err
}
path := fi.Path
d, err := os.Open(path)
if err != nil {
return err
}
defer d.Close()
err = Filenames(*d, names)
return err
}
// RenamePath returns the proposed path or the new full path.
// Does not actually do the renaming -- see Rename method.
func (fi *FileInfo) RenamePath(path string) (newpath string, err error) {
if path == "" {
err = fmt.Errorf("core.Rename: new name is empty")
log.Println(err)
return path, err
}
if path == fi.Path {
return "", nil
}
ndir, np := filepath.Split(path)
if ndir == "" {
if np == fi.Name {
return path, nil
}
dir, _ := filepath.Split(fi.Path)
newpath = filepath.Join(dir, np)
}
return newpath, nil
}
// Rename renames (moves) this file to given new path name.
// Updates the FileInfo setting to the new name, although it might
// be out of scope if it moved into a new path
func (fi *FileInfo) Rename(path string) (newpath string, err error) { //types:add
orgpath := fi.Path
newpath, err = fi.RenamePath(path)
if err != nil {
return
}
err = os.Rename(string(orgpath), newpath)
if err == nil {
fi.Path = newpath
_, fi.Name = filepath.Split(newpath)
}
return
}
// FindIcon uses file info to find an appropriate icon for this file -- uses
// Kind string first to find a correspondingly named icon, and then tries the
// extension. Returns true on success.
func (fi *FileInfo) FindIcon() (icons.Icon, bool) {
if fi.IsDir() {
return icons.Folder, true
}
return Icons[fi.Known], true
}
// Note: can get all the detailed birth, access, change times from this package
// "github.com/djherbis/times"
//////////////////////////////////////////////////////////////////////////////
// CopyFile
// here's all the discussion about why CopyFile is not in std lib:
// https://old.reddit.com/r/golang/comments/3lfqoh/why_golang_does_not_provide_a_copy_file_func/
// https://github.com/golang/go/issues/8868
// CopyFile copies the contents from src to dst atomically.
// If dst does not exist, CopyFile creates it with permissions perm.
// If the copy fails, CopyFile aborts and dst is preserved.
func CopyFile(dst, src string, perm os.FileMode) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
tmp, err := os.CreateTemp(filepath.Dir(dst), "")
if err != nil {
return err
}
_, err = io.Copy(tmp, in)
if err != nil {
tmp.Close()
os.Remove(tmp.Name())
return err
}
if err = tmp.Close(); err != nil {
os.Remove(tmp.Name())
return err
}
if err = os.Chmod(tmp.Name(), perm); err != nil {
os.Remove(tmp.Name())
return err
}
return os.Rename(tmp.Name(), dst)
}
// 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 fileinfo
//go:generate core generate
import (
"fmt"
)
// Known is an enumerated list of known file types, for which
// appropriate actions can be taken etc.
type Known int32 //enums:enum
// KnownMimes maps from the known type into the MimeType info for each
// known file type; the known MimeType may be just one of
// multiple that correspond to the known type; it should be first in list
// and have extensions defined
var KnownMimes map[Known]MimeType
// MimeString gives the string representation of the canonical mime type
// associated with given known mime type.
func MimeString(kn Known) string {
mt, has := KnownMimes[kn]
if !has {
// log.Printf("fileinfo.MimeString called with unrecognized 'Known' type: %v\n", sup)
return ""
}
return mt.Mime
}
// Cat returns the Cat category for given known file type
func (kn Known) Cat() Categories {
if kn == Unknown {
return UnknownCategory
}
mt, has := KnownMimes[kn]
if !has {
// log.Printf("fileinfo.KnownCat called with unrecognized 'Known' type: %v\n", sup)
return UnknownCategory
}
return mt.Cat
}
// IsMatch returns true if given file type matches target type,
// which could be any of the Any options
func IsMatch(targ, typ Known) bool {
if targ == Any {
return true
}
if targ == AnyKnown {
return typ != Unknown
}
if targ == typ {
return true
}
cat := typ.Cat()
switch targ {
case AnyFolder:
return cat == Folder
case AnyArchive:
return cat == Archive
case AnyBackup:
return cat == Backup
case AnyCode:
return cat == Code
case AnyDoc:
return cat == Doc
case AnySheet:
return cat == Sheet
case AnyData:
return cat == Data
case AnyText:
return cat == Text
case AnyImage:
return cat == Image
case AnyModel:
return cat == Model
case AnyAudio:
return cat == Audio
case AnyVideo:
return cat == Video
case AnyFont:
return cat == Font
case AnyExe:
return cat == Exe
case AnyBin:
return cat == Bin
}
return false
}
// IsMatchList returns true if given file type matches any of a list of targets
// if list is empty, then always returns true
func IsMatchList(targs []Known, typ Known) bool {
if len(targs) == 0 {
return true
}
for _, trg := range targs {
if IsMatch(trg, typ) {
return true
}
}
return false
}
// KnownByName looks up known file type by caps or lowercase name
func KnownByName(name string) (Known, error) {
var kn Known
err := kn.SetString(name)
if err != nil {
err = fmt.Errorf("fileinfo.KnownByName: doesn't look like that is a known file type: %v", name)
return kn, err
}
return kn, nil
}
// These are the super high-frequency used mime types, to have very quick const level support
const (
TextPlain = "text/plain"
DataJson = "application/json"
DataXml = "application/xml"
DataCsv = "text/csv"
)
// These are the known file types, organized by category
const (
// Unknown = a non-known file type
Unknown Known = iota
// Any is used when selecting a file type, if any type is OK (including Unknown)
// see also AnyKnown and the Any options for each category
Any
// AnyKnown is used when selecting a file type, if any Known
// file type is OK (excludes Unknown) -- see Any and Any options for each category
AnyKnown
// Folder is a folder / directory
AnyFolder
// Archive is a collection of files, e.g., zip tar
AnyArchive
Multipart
Tar
Zip
GZip
SevenZ
Xz
BZip
Dmg
Shar
// Backup files
AnyBackup
Trash
// Code is a programming language file
AnyCode
Ada
Bash
Cosh
Csh
C // includes C++
CSharp
D
Diff
Eiffel
Erlang
Forth
Fortran
FSharp
Go
Haskell
Java
JavaScript
Lisp
Lua
Makefile
Mathematica
Matlab
ObjC
OCaml
Pascal
Perl
Php
Prolog
Python
R
Ruby
Rust
Scala
Tcl
// Doc is an editable word processing file including latex, markdown, html, css, etc
AnyDoc
BibTeX
TeX
Texinfo
Troff
Html
Css
Markdown
Rtf
MSWord
OpenText
OpenPres
MSPowerpoint
EBook
EPub
// Sheet is a spreadsheet file (.xls etc)
AnySheet
MSExcel
OpenSheet
// Data is some kind of data format (csv, json, database, etc)
AnyData
Csv
Json
Xml
Protobuf
Ini
Tsv
Uri
Color
Yaml
Toml
// special support for data fs
Number
String
Tensor
Table
// Text is some other kind of text file
AnyText
PlainText // text/plain mimetype -- used for clipboard
ICal
VCal
VCard
// Image is an image (jpeg, png, svg, etc) *including* PDF
AnyImage
Pdf
Postscript
Gimp
GraphVis
Gif
Jpeg
Png
Svg
Tiff
Pnm
Pbm
Pgm
Ppm
Xbm
Xpm
Bmp
Heic
Heif
// Model is a 3D model
AnyModel
Vrml
X3d
Obj
// Audio is an audio file
AnyAudio
Aac
Flac
Mp3
Ogg
Midi
Wav
// Video is a video file
AnyVideo
Mpeg
Mp4
Mov
Ogv
Wmv
Avi
// Font is a font file
AnyFont
TrueType
WebOpenFont
// Exe is a binary executable file
AnyExe
// Bin is some other unrecognized binary type
AnyBin
)
// 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 mimedata defines MIME data support used in clipboard and
// drag-and-drop functions in the Cogent Core GUI. mimedata.Data struct contains
// format and []byte data, and multiple representations of the same data are
// encoded in mimedata.Mimes which is just a []mimedata.Data slice -- it can
// be encoded / decoded from mime multipart.
//
// See the fileinfo package for known mimetypes.
package mimedata
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"log"
"mime"
"mime/multipart"
"net/textproto"
"strings"
)
const (
MIMEVersion1 = "MIME-Version: 1.0"
ContentType = "Content-Type"
ContentTransferEncoding = "Content-Transfer-Encoding"
TextPlain = "text/plain"
DataJson = "application/json"
DataXml = "application/xml"
)
var (
MIMEVersion1B = ([]byte)(MIMEVersion1)
ContentTypeB = ([]byte)(ContentType)
ContentTransferEncodingB = ([]byte)(ContentTransferEncoding)
)
// Data represents one element of MIME data as a type string and byte slice
type Data struct {
// MIME Type string representing the data, e.g., text/plain, text/html, text/xml, text/uri-list, image/jpg, png etc
Type string
// Data for the item
Data []byte
}
// NewTextData returns a Data representation of the string -- good idea to
// always have a text/plain representation of everything on clipboard /
// drag-n-drop
func NewTextData(text string) *Data {
return &Data{TextPlain, []byte(text)}
}
// NewTextDataBytes returns a Data representation of the bytes string
func NewTextDataBytes(text []byte) *Data {
return &Data{TextPlain, text}
}
// IsText returns true if type is any of the text/ types (literally looks for that
// at start of Type) or is another known text type (e.g., AppJSON, XML)
func IsText(typ string) bool {
if strings.HasPrefix(typ, "text/") {
return true
}
return typ == DataJson || typ == DataXml
}
////////////////////////////////////////////////////////////////////////////////
// Mimes
// Mimes is a slice of mime data, potentially encoding the same data in
// different formats -- this is used for all system APIs for maximum
// flexibility
type Mimes []*Data
// NewMimes returns a new Mimes slice of given length and capacity
func NewMimes(ln, cp int) Mimes {
return make(Mimes, ln, cp)
}
// NewText returns a Mimes representation of the string as a single text/plain Data
func NewText(text string) Mimes {
md := NewTextData(text)
mi := make(Mimes, 1)
mi[0] = md
return mi
}
// NewTextBytes returns a Mimes representation of the bytes string as a single text/plain Data
func NewTextBytes(text []byte) Mimes {
md := NewTextDataBytes(text)
mi := make(Mimes, 1)
mi[0] = md
return mi
}
// NewTextPlus returns a Mimes representation of an item as a text string plus
// a more specific type
func NewTextPlus(text, typ string, data []byte) Mimes {
md := NewTextData(text)
mi := make(Mimes, 2)
mi[0] = md
mi[1] = &Data{typ, data}
return mi
}
// NewMime returns a Mimes representation of one element
func NewMime(typ string, data []byte) Mimes {
mi := make(Mimes, 1)
mi[0] = &Data{typ, data}
return mi
}
// HasType returns true if Mimes has given type of data available
func (mi Mimes) HasType(typ string) bool {
for _, d := range mi {
if d.Type == typ {
return true
}
}
return false
}
// TypeData returns data associated with given MIME type
func (mi Mimes) TypeData(typ string) []byte {
for _, d := range mi {
if d.Type == typ {
return d.Data
}
}
return nil
}
// Text extracts all the text elements of given type as a string
func (mi Mimes) Text(typ string) string {
str := ""
for _, d := range mi {
if d.Type == typ {
str = str + string(d.Data)
}
}
return str
}
// ToMultipart produces a MIME multipart representation of multiple data
// elements present in the stream -- this should be used in system.Clipboard
// whenever there are multiple elements to be pasted, because windows doesn't
// support multiple clip elements, and linux isn't very convenient either
func (mi Mimes) ToMultipart() []byte {
var b bytes.Buffer
mpw := multipart.NewWriter(io.Writer(&b))
hdr := fmt.Sprintf("%v\n%v: multipart/mixed; boundary=%v\n", MIMEVersion1, ContentType, mpw.Boundary())
b.Write(([]byte)(hdr))
for _, d := range mi {
mh := textproto.MIMEHeader{ContentType: {d.Type}}
bin := false
if !IsText(d.Type) {
mh.Add(ContentTransferEncoding, "base64")
bin = true
}
wp, _ := mpw.CreatePart(mh)
if bin {
eb := make([]byte, base64.StdEncoding.EncodedLen(len(d.Data)))
base64.StdEncoding.Encode(eb, d.Data)
wp.Write(eb)
} else {
wp.Write(d.Data)
}
}
mpw.Close()
return b.Bytes()
}
// IsMultipart examines data bytes to see if it has a MIME-Version: 1.0
// ContentType: multipart/* header -- returns the actual multipart media type,
// body of the data string after the header (assumed to be a single \n
// terminated line at start of string, and the boundary separating multipart
// elements (all from mime.ParseMediaType) -- mediaType is the mediaType if it
// is another MIME type -- can check that for non-empty string
func IsMultipart(str []byte) (isMulti bool, mediaType, boundary string, body []byte) {
isMulti = false
mediaType = ""
boundary = ""
body = ([]byte)("")
var pars map[string]string
var err error
if bytes.HasPrefix(str, MIMEVersion1B) {
cri := bytes.IndexByte(str, '\n')
if cri < 0 { // shouldn't happen
return
}
ctln := str[cri+1:]
if bytes.HasPrefix(ctln, ContentTypeB) { // should
cri2 := bytes.IndexByte(ctln, '\n')
if cri2 < 0 { // shouldn't happen
return
}
hdr := ctln[len(ContentTypeB)+1 : cri2]
mediaType, pars, err = mime.ParseMediaType(string(hdr))
if err != nil { // shouldn't happen
log.Printf("mimedata.IsMultipart: malformed MIME header: %v\n", err)
return
}
if strings.HasPrefix(mediaType, "multipart/") {
isMulti = true
body = str[cri2+1:]
boundary = pars["boundary"]
}
}
}
return
}
// FromMultipart parses a MIME multipart representation of multiple data
// elements into corresponding mime data components
func FromMultipart(body []byte, boundary string) Mimes {
mi := make(Mimes, 0, 10)
sr := bytes.NewReader(body)
mr := multipart.NewReader(sr, boundary)
for {
p, err := mr.NextPart()
if err == io.EOF {
return mi
}
if err != nil {
log.Printf("mimedata.IsMultipart: malformed multipart MIME: %v\n", err)
return mi
}
b, err := io.ReadAll(p)
if err != nil {
log.Printf("mimedata.IsMultipart: bad ReadAll of multipart MIME: %v\n", err)
return mi
}
d := Data{}
d.Type = p.Header.Get(ContentType)
cte := p.Header.Get(ContentTransferEncoding)
if cte != "" {
switch cte {
case "base64":
eb := make([]byte, base64.StdEncoding.DecodedLen(len(b)))
base64.StdEncoding.Decode(eb, b)
b = eb
}
}
d.Data = b
mi = append(mi, &d)
}
}
// todo: image, etc extractors
// 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 fileinfo
import (
"fmt"
"mime"
"path/filepath"
"strings"
"github.com/h2non/filetype"
)
// MimeNoChar returns the mime string without any charset
// encoding information, or anything else after a ;
func MimeNoChar(mime string) string {
if sidx := strings.Index(mime, ";"); sidx > 0 {
return strings.TrimSpace(mime[:sidx])
}
return mime
}
// MimeTop returns the top-level main type category from mime type
// i.e., the thing before the / -- returns empty if no /
func MimeTop(mime string) string {
if sidx := strings.Index(mime, "/"); sidx > 0 {
return mime[:sidx]
}
return ""
}
// MimeSub returns the sub-level subtype category from mime type
// i.e., the thing after the / -- returns empty if no /
// also trims off any charset encoding stuff
func MimeSub(mime string) string {
if sidx := strings.Index(MimeNoChar(mime), "/"); sidx > 0 {
return mime[sidx+1:]
}
return ""
}
// MimeFromFile gets mime type from file, using Gabriel Vasile's mimetype
// package, mime.TypeByExtension, the chroma syntax highlighter,
// CustomExtMimeMap, and finally FileExtMimeMap. Use the mimetype package's
// extension mechanism to add further content-based matchers as needed, and
// set CustomExtMimeMap to your own map or call AddCustomExtMime for
// extension-based ones.
func MimeFromFile(fname string) (mtype, ext string, err error) {
ext = strings.ToLower(filepath.Ext(fname))
if mtyp, has := ExtMimeMap[ext]; has { // use our map first: very fast!
return mtyp, ext, nil
}
_, fn := filepath.Split(fname)
var fc, lc byte
if len(fn) > 0 {
fc = fn[0]
lc = fn[len(fn)-1]
}
if fc == '~' || fc == '%' || fc == '#' || lc == '~' || lc == '%' || lc == '#' {
return MimeString(Trash), ext, nil
}
mtypt, err := filetype.MatchFile(fn) // h2non next -- has good coverage
ptyp := ""
isplain := false
if err == nil {
mtyp := mtypt.MIME.Value
ext = mtypt.Extension
if strings.HasPrefix(mtyp, "text/plain") {
isplain = true
ptyp = mtyp
} else {
return mtyp, ext, nil
}
}
mtyp := mime.TypeByExtension(ext)
if mtyp != "" {
return mtyp, ext, nil
}
// TODO(kai/binsize): figure out how to do this without dragging in chroma dependency
// lexer := lexers.Match(fn) // todo: could get start of file and pass to
// // Analyze, but might be too slow..
// if lexer != nil {
// config := lexer.Config()
// if len(config.MimeTypes) > 0 {
// mtyp = config.MimeTypes[0]
// return mtyp, ext, nil
// }
// mtyp := "application/" + strings.ToLower(config.Name)
// return mtyp, ext, nil
// }
if isplain {
return ptyp, ext, nil
}
if strings.ToLower(fn) == "makefile" {
return MimeString(Makefile), ext, nil
}
return "", ext, fmt.Errorf("fileinfo.MimeFromFile could not find mime type for ext: %v file: %v", ext, fn)
}
// todo: use this to check against mime types!
// MimeToKindMapInit makes sure the MimeToKindMap is initialized from
// InitMimeToKindMap plus chroma lexer types.
// func MimeToKindMapInit() {
// if MimeToKindMap != nil {
// return
// }
// MimeToKindMap = InitMimeToKindMap
// for _, l := range lexers.Registry.Lexers {
// config := l.Config()
// nm := strings.ToLower(config.Name)
// if len(config.MimeTypes) > 0 {
// mtyp := config.MimeTypes[0]
// MimeToKindMap[mtyp] = nm
// } else {
// MimeToKindMap["application/"+nm] = nm
// }
// }
// }
//////////////////////////////////////////////////////////////////////////////
// Mime types
// ExtMimeMap is the map from extension to mime type, built from AvailMimes
var ExtMimeMap = map[string]string{}
// MimeType contains all the information associated with a given mime type
type MimeType struct {
// mimetype string: type/subtype
Mime string
// file extensions associated with this file type
Exts []string
// category of file
Cat Categories
// if known, the name of the known file type, else NoSupporUnknown
Known Known
}
// CustomMimes can be set by other apps to contain custom mime types that
// go beyond what is in the standard ones, and can also redefine and
// override the standard one
var CustomMimes []MimeType
// AvailableMimes is the full list (as a map from mimetype) of available defined mime types
// built from StdMimes (compiled in) and then CustomMimes can override
var AvailableMimes map[string]MimeType
// MimeKnown returns the known type for given mime key,
// or Unknown if not found or not a known file type
func MimeKnown(mime string) Known {
mt, has := AvailableMimes[MimeNoChar(mime)]
if !has {
return Unknown
}
return mt.Known
}
// ExtKnown returns the known type for given file extension,
// or Unknown if not found or not a known file type
func ExtKnown(ext string) Known {
mime, has := ExtMimeMap[ext]
if !has {
return Unknown
}
mt, has := AvailableMimes[mime]
if !has {
return Unknown
}
return mt.Known
}
// KnownFromFile returns the known type for given file,
// or Unknown if not found or not a known file type
func KnownFromFile(fname string) Known {
mtyp, _, err := MimeFromFile(fname)
if err != nil {
return Unknown
}
return MimeKnown(mtyp)
}
// MimeFromKnown returns MimeType info for given known file type.
func MimeFromKnown(ftyp Known) MimeType {
for _, mt := range AvailableMimes {
if mt.Known == ftyp {
return mt
}
}
return MimeType{}
}
// MergeAvailableMimes merges the StdMimes and CustomMimes into AvailMimes
// if CustomMimes is updated, then this should be called -- initially
// it just has StdMimes.
// It also builds the ExtMimeMap to map from extension to mime type
// and KnownMimes map of known file types onto their full
// mime type entry
func MergeAvailableMimes() {
AvailableMimes = make(map[string]MimeType, len(StandardMimes)+len(CustomMimes))
for _, mt := range StandardMimes {
AvailableMimes[mt.Mime] = mt
}
for _, mt := range CustomMimes {
AvailableMimes[mt.Mime] = mt // overwrite automatically
}
ExtMimeMap = make(map[string]string) // start over
KnownMimes = make(map[Known]MimeType)
for _, mt := range AvailableMimes {
if len(mt.Exts) > 0 { // first pass add only ext guys to support
for _, ex := range mt.Exts {
if ex[0] != '.' {
fmt.Printf("fileinfo.MergeAvailMimes: ext: %v does not start with a . in type: %v\n", ex, mt.Mime)
}
if pmt, has := ExtMimeMap[ex]; has {
fmt.Printf("fileinfo.MergeAvailMimes: non-unique ext: %v assigned to mime type: %v AND %v\n", ex, pmt, mt.Mime)
} else {
ExtMimeMap[ex] = mt.Mime
}
}
if mt.Known != Unknown {
if hsp, has := KnownMimes[mt.Known]; has {
fmt.Printf("fileinfo.MergeAvailMimes: more-than-one mimetype has extensions for same known file type: %v -- one: %v other %v\n", mt.Known, hsp.Mime, mt.Mime)
} else {
KnownMimes[mt.Known] = mt
}
}
}
}
// second pass to get any known guys that don't have exts
for _, mt := range AvailableMimes {
if mt.Known != Unknown {
if _, has := KnownMimes[mt.Known]; !has {
KnownMimes[mt.Known] = mt
}
}
}
}
func init() {
MergeAvailableMimes()
}
// http://www.iana.org/assignments/media-types/media-types.xhtml
// https://github.com/mirage/ocaml-magic-mime/blob/master/x-mime.types
// https://www.apt-browse.org/browse/debian/stretch/main/all/mime-support/3.60/file/etc/mime.types
// https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types
// StandardMimes is the full list of standard mime types compiled into our code
// various other maps etc are constructed from it.
// When there are multiple types associated with the same real type, pick one
// to be the canonical one and give it, and *only* it, the extensions!
var StandardMimes = []MimeType{
// Folder
{"text/directory", nil, Folder, Unknown},
// Archive
{"multipart/mixed", nil, Archive, Multipart},
{"application/tar", []string{".tar", ".tar.gz", ".tgz", ".taz", ".taZ", ".tar.bz2", ".tz2", ".tbz2", ".tbz", ".tar.lz", ".tar.lzma", ".tlz", ".tar.lzop", ".tar.xz"}, Archive, Tar},
{"application/x-gtar", nil, Archive, Tar},
{"application/x-gtar-compressed", nil, Archive, Tar},
{"application/x-tar", nil, Archive, Tar},
{"application/zip", []string{".zip"}, Archive, Zip},
{"application/gzip", []string{".gz"}, Archive, GZip},
{"application/x-7z-compressed", []string{".7z"}, Archive, SevenZ},
{"application/x-xz", []string{".xz"}, Archive, Xz},
{"application/x-bzip", []string{".bz", ".bz2"}, Archive, BZip},
{"application/x-bzip2", nil, Archive, BZip},
{"application/x-apple-diskimage", []string{".dmg"}, Archive, Dmg},
{"application/x-shar", []string{".shar"}, Archive, Shar},
{"application/x-bittorrent", []string{".torrent"}, Archive, Unknown},
{"application/rar", []string{".rar"}, Archive, Unknown},
{"application/x-stuffit", []string{".sit", ".sitx"}, Archive, Unknown},
{"application/vnd.android.package-archive", []string{".apk"}, Archive, Unknown},
{"application/vnd.debian.binary-package", []string{".deb", ".ddeb", ".udeb"}, Archive, Unknown},
{"application/x-debian-package", nil, Archive, Unknown},
{"application/x-redhat-package-manager", []string{".rpm"}, Archive, Unknown},
{"text/x-rpm-spec", nil, Archive, Unknown},
// Backup
{"application/x-trash", []string{".bak", ".old", ".sik"}, Backup, Trash}, // also "~", "%", "#",
// Code -- use text/ as main instead of application as there are more text
{"text/x-ada", []string{".adb", ".ads", ".ada"}, Code, Ada},
{"text/x-asp", []string{".aspx", ".asax", ".ascx", ".ashx", ".asmx", ".axd"}, Code, Unknown},
{"text/x-sh", []string{".bash", ".sh"}, Code, Bash},
{"application/x-sh", nil, Code, Bash},
{"text/x-csrc", []string{".c", ".C", ".c++", ".cpp", ".cxx", ".cc", ".h", ".h++", ".hpp", ".hxx", ".hh", ".hlsl", ".gsl", ".frag", ".vert", ".mm"}, Code, C}, // this is apparently the main one now
{"text/x-chdr", nil, Code, C},
{"text/x-c", nil, Code, C},
{"text/x-c++hdr", nil, Code, C},
{"text/x-c++src", nil, Code, C},
{"text/x-chdr", nil, Code, C},
{"text/x-cpp", nil, Code, C},
{"text/x-csh", []string{".csh"}, Code, Csh},
{"application/x-csh", nil, Code, Csh},
{"text/x-csharp", []string{".cs"}, Code, CSharp},
{"text/x-dsrc", []string{".d"}, Code, D},
{"text/x-diff", []string{".diff", ".patch"}, Code, Diff},
{"text/x-eiffel", []string{".e"}, Code, Eiffel},
{"text/x-erlang", []string{".erl", ".hrl", ".escript"}, Code, Erlang}, // note: ".es" conflicts with ecmascript
{"text/x-forth", []string{".frt"}, Code, Forth}, // note: ".fs" conflicts with fsharp
{"text/x-fortran", []string{".f", ".F"}, Code, Fortran},
{"text/x-fsharp", []string{".fs", ".fsi"}, Code, FSharp},
{"text/x-gosrc", []string{".go", ".mod", ".work", ".cosh"}, Code, Go},
{"text/x-haskell", []string{".hs", ".lhs"}, Code, Haskell},
{"text/x-literate-haskell", nil, Code, Haskell}, // todo: not sure if same or not
{"text/x-java", []string{".java", ".jar"}, Code, Java},
{"application/java-archive", nil, Code, Java},
{"application/javascript", []string{".js"}, Code, JavaScript},
{"application/ecmascript", []string{".es"}, Code, Unknown},
{"text/x-common-lisp", []string{".lisp", ".cl", ".el"}, Code, Lisp},
{"text/elisp", nil, Code, Lisp},
{"text/x-elisp", nil, Code, Lisp},
{"application/emacs-lisp", nil, Code, Lisp},
{"text/x-lua", []string{".lua", ".wlua"}, Code, Lua},
{"text/x-makefile", nil, Code, Makefile},
{"text/x-autoconf", nil, Code, Makefile},
{"text/x-moc", []string{".moc"}, Code, Unknown},
{"application/mathematica", []string{".nb", ".nbp"}, Code, Mathematica},
{"text/x-matlab", []string{".m"}, Code, Matlab},
{"text/matlab", nil, Code, Matlab},
{"text/octave", nil, Code, Matlab},
{"text/scilab", []string{".sci", ".sce", ".tst"}, Code, Unknown},
{"text/x-modelica", []string{".mo"}, Code, Unknown},
{"text/x-nemerle", []string{".n"}, Code, Unknown},
{"text/x-objcsrc", nil, Code, ObjC}, // doesn't have chroma support -- use C instead
{"text/x-objective-j", nil, Code, Unknown},
{"text/x-ocaml", []string{".ml", ".mli", ".mll", ".mly"}, Code, OCaml},
{"text/x-pascal", []string{".p", ".pas"}, Code, Pascal},
{"text/x-perl", []string{".pl", ".pm"}, Code, Perl},
{"text/x-php", []string{".php", ".php3", ".php4", ".php5", ".inc"}, Code, Php},
{"text/x-prolog", []string{".ecl", ".prolog", ".pro"}, Code, Prolog}, // note: ".pl" conflicts
{"text/x-python", []string{".py", ".pyc", ".pyo", ".pyw"}, Code, Python},
{"application/x-python-code", nil, Code, Python},
{"text/x-rust", []string{".rs"}, Code, Rust},
{"text/rust", nil, Code, Rust},
{"text/x-r", []string{".r", ".S", ".R", ".Rhistory", ".Rprofile", ".Renviron"}, Code, R},
{"text/x-R", nil, Code, R},
{"text/S-Plus", nil, Code, R},
{"text/S", nil, Code, R},
{"text/x-r-source", nil, Code, R},
{"text/x-r-history", nil, Code, R},
{"text/x-r-profile", nil, Code, R},
{"text/x-ruby", []string{".rb"}, Code, Ruby},
{"application/x-ruby", nil, Code, Ruby},
{"text/x-scala", []string{".scala"}, Code, Scala},
{"text/x-tcl", []string{".tcl", ".tk"}, Code, Tcl},
{"application/x-tcl", nil, Code, Tcl},
// Doc
{"text/x-bibtex", []string{".bib"}, Doc, BibTeX},
{"text/x-tex", []string{".tex", ".ltx", ".sty", ".cls", ".latex"}, Doc, TeX},
{"application/x-latex", nil, Doc, TeX},
{"application/x-texinfo", []string{".texinfo", ".texi"}, Doc, Texinfo},
{"application/x-troff", []string{".t", ".tr", ".roff", ".man", ".me", ".ms"}, Doc, Troff},
{"application/x-troff-man", nil, Doc, Troff},
{"application/x-troff-me", nil, Doc, Troff},
{"application/x-troff-ms", nil, Doc, Troff},
{"text/html", []string{".html", ".htm", ".shtml", ".xhtml", ".xht"}, Doc, Html},
{"application/xhtml+xml", nil, Doc, Html},
{"text/mathml", []string{".mml"}, Doc, Unknown},
{"text/css", []string{".css"}, Doc, Css},
{"text/markdown", []string{".md", ".markdown"}, Doc, Markdown},
{"text/x-markdown", nil, Doc, Markdown},
{"application/rtf", []string{".rtf"}, Doc, Rtf},
{"text/richtext", []string{".rtx"}, Doc, Unknown},
{"application/mbox", []string{".mbox"}, Doc, Unknown},
{"application/x-rss+xml", []string{".rss"}, Doc, Unknown},
{"application/msword", []string{".doc", ".dot", ".docx", ".dotx"}, Doc, MSWord},
{"application/vnd.ms-word", nil, Doc, MSWord},
{"application/vnd.openxmlformats-officedocument.wordprocessingml.document", nil, Doc, MSWord},
{"application/vnd.openxmlformats-officedocument.wordprocessingml.template", nil, Doc, MSWord},
{"application/vnd.oasis.opendocument.text", []string{".odt", ".odm", ".ott", ".oth", ".sxw", ".sxg", ".stw", ".sxm"}, Doc, OpenText},
{"application/vnd.oasis.opendocument.text-master", nil, Doc, OpenText},
{"application/vnd.oasis.opendocument.text-template", nil, Doc, OpenText},
{"application/vnd.oasis.opendocument.text-web", nil, Doc, OpenText},
{"application/vnd.sun.xml.writer", nil, Doc, OpenText},
{"application/vnd.sun.xml.writer.global", nil, Doc, OpenText},
{"application/vnd.sun.xml.writer.template", nil, Doc, OpenText},
{"application/vnd.sun.xml.math", nil, Doc, OpenText},
{"application/vnd.oasis.opendocument.presentation", []string{".odp", ".otp", ".sxi", ".sti"}, Doc, OpenPres},
{"application/vnd.oasis.opendocument.presentation-template", nil, Doc, OpenPres},
{"application/vnd.sun.xml.impress", nil, Doc, OpenPres},
{"application/vnd.sun.xml.impress.template", nil, Doc, OpenPres},
{"application/vnd.ms-powerpoint", []string{".ppt", ".pps", ".pptx", ".sldx", ".ppsx", ".potx"}, Doc, MSPowerpoint},
{"application/vnd.openxmlformats-officedocument.presentationml.presentation", nil, Doc, MSPowerpoint},
{"application/vnd.openxmlformats-officedocument.presentationml.slide", nil, Doc, MSPowerpoint},
{"application/vnd.openxmlformats-officedocument.presentationml.slideshow", nil, Doc, MSPowerpoint},
{"application/vnd.openxmlformats-officedocument.presentationml.template", nil, Doc, MSPowerpoint},
{"application/ms-tnef", nil, Doc, Unknown},
{"application/vnd.ms-tnef", nil, Doc, Unknown},
{"application/onenote", []string{".one", ".onetoc2", ".onetmp", ".onepkg"}, Doc, Unknown},
{"application/pgp-encrypted", []string{".pgp"}, Doc, Unknown},
{"application/pgp-keys", []string{".key"}, Doc, Unknown},
{"application/pgp-signature", []string{".sig"}, Doc, Unknown},
{"application/vnd.amazon.ebook", []string{".azw"}, Doc, EBook},
{"application/epub+zip", []string{".epub"}, Doc, EPub},
// Sheet
{"application/vnd.ms-excel", []string{".xls", ".xlb", ".xlt", ".xlsx", ".xltx"}, Sheet, MSExcel},
{"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", nil, Sheet, MSExcel},
{"application/vnd.openxmlformats-officedocument.spreadsheetml.template", nil, Sheet, MSExcel},
{"application/vnd.oasis.opendocument.spreadsheet", []string{".ods", ".ots", ".sxc", ".stc", ".odf"}, Sheet, OpenSheet},
{"application/vnd.oasis.opendocument.spreadsheet-template", nil, Sheet, OpenSheet},
{"application/vnd.oasis.opendocument.formula", nil, Sheet, OpenSheet}, // todo: could be separate
{"application/vnd.sun.xml.calc", nil, Sheet, OpenSheet},
{"application/vnd.sun.xml.calc.template", nil, Sheet, OpenSheet},
// Data
{"text/csv", []string{".csv"}, Data, Csv},
{"application/json", []string{".json"}, Data, Json},
{"application/xml", []string{".xml", ".xsd"}, Data, Xml},
{"text/xml", nil, Data, Xml},
{"text/x-protobuf", []string{".proto"}, Data, Protobuf},
{"text/x-ini", []string{".ini", ".cfg", ".inf"}, Data, Ini},
{"text/x-ini-file", nil, Data, Ini},
{"text/uri-list", nil, Data, Uri},
{"application/x-color", nil, Data, Color},
{"text/toml", []string{".toml"}, Data, Toml},
{"application/toml", nil, Data, Toml},
{"application/yaml", []string{".yaml"}, Data, Yaml},
{"application/rdf+xml", []string{".rdf"}, Data, Unknown},
{"application/msaccess", []string{".mdb"}, Data, Unknown},
{"application/vnd.oasis.opendocument.database", []string{".odb"}, Data, Unknown},
{"text/tab-separated-values", []string{".tsv"}, Data, Tsv},
{"application/vnd.google-earth.kml+xml", []string{".kml", ".kmz"}, Data, Unknown},
{"application/vnd.google-earth.kmz", nil, Data, Unknown},
{"application/x-sql", []string{".sql"}, Data, Unknown},
// Text
{"text/plain", []string{".asc", ".txt", ".text", ".pot", ".brf", ".srt"}, Text, PlainText},
{"text/cache-manifest", []string{".appcache"}, Text, Unknown},
{"text/calendar", []string{".ics", ".icz"}, Text, ICal},
{"text/x-vcalendar", []string{".vcs"}, Text, VCal},
{"text/vcard", []string{".vcf", ".vcard"}, Text, VCard},
// Image
{"application/pdf", []string{".pdf"}, Image, Pdf},
{"application/postscript", []string{".ps", ".ai", ".eps", ".epsi", ".epsf", ".eps2", ".eps3"}, Image, Postscript},
{"application/vnd.oasis.opendocument.graphics", []string{".odc", ".odg", ".otg", ".odi", ".sxd", ".std"}, Image, Unknown},
{"application/vnd.oasis.opendocument.chart", nil, Image, Unknown},
{"application/vnd.oasis.opendocument.graphics-template", nil, Image, Unknown},
{"application/vnd.oasis.opendocument.image", nil, Image, Unknown},
{"application/vnd.sun.xml.draw", nil, Image, Unknown},
{"application/vnd.sun.xml.draw.template", nil, Image, Unknown},
{"application/x-xfig", []string{".fig"}, Image, Unknown},
{"application/x-xcf", []string{".xcf"}, Image, Gimp},
{"text/vnd.graphviz", []string{".gv"}, Image, GraphVis},
{"image/gif", []string{".gif"}, Image, Gif},
{"image/ief", []string{".ief"}, Image, Unknown},
{"image/jp2", []string{".jp2", ".jpg2"}, Image, Unknown},
{"image/jpeg", []string{".jpeg", ".jpg", ".jpe"}, Image, Jpeg},
{"image/jpm", []string{".jpm"}, Image, Unknown},
{"image/jpx", []string{".jpx", ".jpf"}, Image, Unknown},
{"image/pcx", []string{".pcx"}, Image, Unknown},
{"image/png", []string{".png"}, Image, Png},
{"image/heic", []string{".heic"}, Image, Heic},
{"image/heif", []string{".heif"}, Image, Heif},
{"image/svg+xml", []string{".svg", ".svgz"}, Image, Svg},
{"image/tiff", []string{".tiff", ".tif"}, Image, Tiff},
{"image/vnd.djvu", []string{".djvu", ".djv"}, Image, Unknown},
{"image/vnd.microsoft.icon", []string{".ico"}, Image, Unknown},
{"image/vnd.wap.wbmp", []string{".wbmp"}, Image, Unknown},
{"image/x-canon-cr2", []string{".cr2"}, Image, Unknown},
{"image/x-canon-crw", []string{".crw"}, Image, Unknown},
{"image/x-cmu-raster", []string{".ras"}, Image, Unknown},
{"image/x-coreldraw", []string{".cdr", ".pat", ".cdt", ".cpt"}, Image, Unknown},
{"image/x-coreldrawpattern", nil, Image, Unknown},
{"image/x-coreldrawtemplate", nil, Image, Unknown},
{"image/x-corelphotopaint", nil, Image, Unknown},
{"image/x-epson-erf", []string{".erf"}, Image, Unknown},
{"image/x-jg", []string{".art"}, Image, Unknown},
{"image/x-jng", []string{".jng"}, Image, Unknown},
{"image/x-ms-bmp", []string{".bmp"}, Image, Bmp},
{"image/x-nikon-nef", []string{".nef"}, Image, Unknown},
{"image/x-olympus-orf", []string{".orf"}, Image, Unknown},
{"image/x-photoshop", []string{".psd"}, Image, Unknown},
{"image/x-portable-anymap", []string{".pnm"}, Image, Pnm},
{"image/x-portable-bitmap", []string{".pbm"}, Image, Pbm},
{"image/x-portable-graymap", []string{".pgm"}, Image, Pgm},
{"image/x-portable-pixmap", []string{".ppm"}, Image, Ppm},
{"image/x-rgb", []string{".rgb"}, Image, Unknown},
{"image/x-xbitmap", []string{".xbm"}, Image, Xbm},
{"image/x-xpixmap", []string{".xpm"}, Image, Xpm},
{"image/x-xwindowdump", []string{".xwd"}, Image, Unknown},
// Model
{"model/iges", []string{".igs", ".iges"}, Model, Unknown},
{"model/mesh", []string{".msh", ".mesh", ".silo"}, Model, Unknown},
{"model/vrml", []string{".wrl", ".vrml", ".vrm"}, Model, Vrml},
{"x-world/x-vrml", nil, Model, Vrml},
{"model/x3d+xml", []string{".x3dv", ".x3d", ".x3db"}, Model, X3d},
{"model/x3d+vrml", nil, Model, X3d},
{"model/x3d+binary", nil, Model, X3d},
{"application/object", []string{".obj", ".mtl"}, Model, Obj},
// Audio
{"audio/aac", []string{".aac"}, Audio, Aac},
{"audio/flac", []string{".flac"}, Audio, Flac},
{"audio/mpeg", []string{".mpga", ".mpega", ".mp2", ".mp3", ".m4a"}, Audio, Mp3},
{"audio/mpegurl", []string{".m3u"}, Audio, Unknown},
{"audio/x-mpegurl", nil, Audio, Unknown},
{"audio/ogg", []string{".oga", ".ogg", ".opus", ".spx"}, Audio, Ogg},
{"audio/amr", []string{".amr"}, Audio, Unknown},
{"audio/amr-wb", []string{".awb"}, Audio, Unknown},
{"audio/annodex", []string{".axa"}, Audio, Unknown},
{"audio/basic", []string{".au", ".snd"}, Audio, Unknown},
{"audio/csound", []string{".csd", ".orc", ".sco"}, Audio, Unknown},
{"audio/midi", []string{".mid", ".midi", ".kar"}, Audio, Midi},
{"audio/prs.sid", []string{".sid"}, Audio, Unknown},
{"audio/x-aiff", []string{".aif", ".aiff", ".aifc"}, Audio, Unknown},
{"audio/x-gsm", []string{".gsm"}, Audio, Unknown},
{"audio/x-ms-wma", []string{".wma"}, Audio, Unknown},
{"audio/x-ms-wax", []string{".wax"}, Audio, Unknown},
{"audio/x-pn-realaudio", []string{".ra", ".rm", ".ram"}, Audio, Unknown},
{"audio/x-realaudio", nil, Audio, Unknown},
{"audio/x-scpls", []string{".pls"}, Audio, Unknown},
{"audio/x-sd2", []string{".sd2"}, Audio, Unknown},
{"audio/x-wav", []string{".wav"}, Audio, Wav},
// Video
{"video/3gpp", []string{".3gp"}, Video, Unknown},
{"video/annodex", []string{".axv"}, Video, Unknown},
{"video/dl", []string{".dl"}, Video, Unknown},
{"video/dv", []string{".dif", ".dv"}, Video, Unknown},
{"video/fli", []string{".fli"}, Video, Unknown},
{"video/gl", []string{".gl"}, Video, Unknown},
{"video/h264", nil, Video, Unknown},
{"video/mpeg", []string{".mpeg", ".mpg", ".mpe"}, Video, Mpeg},
{"video/MP2T", []string{".ts"}, Video, Unknown},
{"video/mp4", []string{".mp4"}, Video, Mp4},
{"video/quicktime", []string{".qt", ".mov"}, Video, Mov},
{"video/ogg", []string{".ogv"}, Video, Ogv},
{"video/webm", []string{".webm"}, Video, Unknown},
{"video/vnd.mpegurl", []string{".mxu"}, Video, Unknown},
{"video/x-flv", []string{".flv"}, Video, Unknown},
{"video/x-la-asf", []string{".lsf", ".lsx"}, Video, Unknown},
{"video/x-mng", []string{".mng"}, Video, Unknown},
{"video/x-ms-asf", []string{".asf", ".asx"}, Video, Unknown},
{"video/x-ms-wm", []string{".wm"}, Video, Unknown},
{"video/x-ms-wmv", []string{".wmv"}, Video, Wmv},
{"video/x-ms-wmx", []string{".wmx"}, Video, Unknown},
{"video/x-ms-wvx", []string{".wvx"}, Video, Unknown},
{"video/x-msvideo", []string{".avi"}, Video, Avi},
{"video/x-sgi-movie", []string{".movie"}, Video, Unknown},
{"video/x-matroska", []string{".mpv", ".mkv"}, Video, Unknown},
{"application/x-shockwave-flash", []string{".swf"}, Video, Unknown},
// Font
{"font/ttf", []string{".otf", ".ttf", ".ttc"}, Font, TrueType},
{"font/otf", nil, Font, TrueType},
{"application/font-sfnt", nil, Font, TrueType},
{"application/x-font-ttf", nil, Font, TrueType},
{"application/x-font", []string{".pfa", ".pfb", ".gsf", ".pcf", ".pcf.Z"}, Font, Unknown},
{"application/x-font-pcf", nil, Font, Unknown},
{"application/vnd.ms-fontobject", []string{".eot"}, Font, Unknown},
{"font/woff", []string{".woff", ".woff2"}, Font, WebOpenFont},
{"font/woff2", nil, Font, WebOpenFont},
{"application/font-woff", nil, Font, WebOpenFont},
// Exe
{"application/x-executable", nil, Exe, Unknown},
{"application/x-msdos-program", []string{".com", ".exe", ".bat", ".dll"}, Exe, Unknown},
// Binary
{"application/octet-stream", []string{".bin"}, Bin, Unknown},
{"application/x-object", []string{".o"}, Bin, Unknown},
{"text/x-libtool", nil, Bin, Unknown},
}
// below are entries from official /etc/mime.types that we don't recognize
// or consider to be old / obsolete / not relevant -- please file an issue
// or a pull-request to add to main list or add yourself in your own app
// application/activemessage
// application/andrew-insetez
// application/annodexanx
// application/applefile
// application/atom+xmlatom
// application/atomcat+xmlatomcat
// application/atomicmail
// application/atomserv+xmlatomsrv
// application/batch-SMTP
// application/bbolinlin
// application/beep+xml
// application/cals-1840
// application/commonground
// application/cu-seemecu
// application/cybercash
// application/davmount+xmldavmount
// application/dca-rft
// application/dec-dx
// application/dicomdcm
// application/docbook+xml
// application/dsptypetsp
// application/dvcs
// application/edi-consent
// application/edi-x12
// application/edifact
// application/eshop
// application/font-tdpfrpfr
// application/futuresplashspl
// application/ghostview
// application/htahta
// application/http
// application/hyperstudio
// application/iges
// application/index
// application/index.cmd
// application/index.obj
// application/index.response
// application/index.vnd
// application/iotp
// application/ipp
// application/isup
// application/java-serialized-objectser
// application/java-vmclass
// application/m3gm3g
// application/mac-binhex40hqx
// application/mac-compactprocpt
// application/macwriteii
// application/marc
// application/mxfmxf
// application/news-message-id
// application/news-transmission
// application/ocsp-request
// application/ocsp-response
// application/octet-streambin deploy msu msp
// application/odaoda
// application/oebps-package+xmlopf
// application/oggogx
// application/parityfec
// application/pics-rulesprf
// application/pkcs10
// application/pkcs7-mime
// application/pkcs7-signature
// application/pkix-cert
// application/pkix-crl
// application/pkixcmp
// application/prs.alvestrand.titrax-sheet
// application/prs.cww
// application/prs.nprend
// application/qsig
// application/remote-printing
// application/riscos
// application/sdp
// application/set-payment
// application/set-payment-initiation
// application/set-registration
// application/set-registration-initiation
// application/sgml
// application/sgml-open-catalog
// application/sieve
// application/slastl
// application/slate
// application/smil+xmlsmi smil
// application/timestamp-query
// application/timestamp-reply
// application/vemmi
// application/whoispp-query
// application/whoispp-response
// application/wita
// application/x400-bp
// application/xml-dtd
// application/xml-external-parsed-entity
// application/xslt+xmlxsl xslt
// application/xspf+xmlxspf
// application/vnd.3M.Post-it-Notes
// application/vnd.accpac.simply.aso
// application/vnd.accpac.simply.imp
// application/vnd.acucobol
// application/vnd.aether.imp
// application/vnd.anser-web-certificate-issue-initiation
// application/vnd.anser-web-funds-transfer-initiation
// application/vnd.audiograph
// application/vnd.bmi
// application/vnd.businessobjects
// application/vnd.canon-cpdl
// application/vnd.canon-lips
// application/vnd.cinderellacdy
// application/vnd.claymore
// application/vnd.commerce-battelle
// application/vnd.commonspace
// application/vnd.comsocaller
// application/vnd.contact.cmsg
// application/vnd.cosmocaller
// application/vnd.ctc-posml
// application/vnd.cups-postscript
// application/vnd.cups-raster
// application/vnd.cups-raw
// application/vnd.cybank
// application/vnd.dna
// application/vnd.dpgraph
// application/vnd.dxr
// application/vnd.ecdis-update
// application/vnd.ecowin.chart
// application/vnd.ecowin.filerequest
// application/vnd.ecowin.fileupdate
// application/vnd.ecowin.series
// application/vnd.ecowin.seriesrequest
// application/vnd.ecowin.seriesupdate
// application/vnd.enliven
// application/vnd.epson.esf
// application/vnd.epson.msf
// application/vnd.epson.quickanime
// application/vnd.epson.salt
// application/vnd.epson.ssf
// application/vnd.ericsson.quickcall
// application/vnd.eudora.data
// application/vnd.fdf
// application/vnd.ffsns
// application/vnd.flographit
// application/vnd.font-fontforge-sfdsfd
// application/vnd.framemaker
// application/vnd.fsc.weblaunch
// application/vnd.fujitsu.oasys
// application/vnd.fujitsu.oasys2
// application/vnd.fujitsu.oasys3
// application/vnd.fujitsu.oasysgp
// application/vnd.fujitsu.oasysprs
// application/vnd.fujixerox.ddd
// application/vnd.fujixerox.docuworks
// application/vnd.fujixerox.docuworks.binder
// application/vnd.fut-misnet
// application/vnd.grafeq
// application/vnd.groove-account
// application/vnd.groove-identity-message
// application/vnd.groove-injector
// application/vnd.groove-tool-message
// application/vnd.groove-tool-template
// application/vnd.groove-vcard
// application/vnd.hhe.lesson-player
// application/vnd.hp-HPGL
// application/vnd.hp-PCL
// application/vnd.hp-PCLXL
// application/vnd.hp-hpid
// application/vnd.hp-hps
// application/vnd.httphone
// application/vnd.hzn-3d-crossword
// application/vnd.ibm.MiniPay
// application/vnd.ibm.afplinedata
// application/vnd.ibm.modcap
// application/vnd.informix-visionary
// application/vnd.intercon.formnet
// application/vnd.intertrust.digibox
// application/vnd.intertrust.nncp
// application/vnd.intu.qbo
// application/vnd.intu.qfx
// application/vnd.irepository.package+xml
// application/vnd.is-xpr
// application/vnd.japannet-directory-service
// application/vnd.japannet-jpnstore-wakeup
// application/vnd.japannet-payment-wakeup
// application/vnd.japannet-registration
// application/vnd.japannet-registration-wakeup
// application/vnd.japannet-setstore-wakeup
// application/vnd.japannet-verification
// application/vnd.japannet-verification-wakeup
// application/vnd.koan
// application/vnd.lotus-1-2-3
// application/vnd.lotus-approach
// application/vnd.lotus-freelance
// application/vnd.lotus-notes
// application/vnd.lotus-organizer
// application/vnd.lotus-screencam
// application/vnd.lotus-wordpro
// application/vnd.mcd
// application/vnd.mediastation.cdkey
// application/vnd.meridian-slingshot
// application/vnd.mif
// application/vnd.minisoft-hp3000-save
// application/vnd.mitsubishi.misty-guard.trustweb
// application/vnd.mobius.daf
// application/vnd.mobius.dis
// application/vnd.mobius.msl
// application/vnd.mobius.plc
// application/vnd.mobius.txf
// application/vnd.motorola.flexsuite
// application/vnd.motorola.flexsuite.adsi
// application/vnd.motorola.flexsuite.fis
// application/vnd.motorola.flexsuite.gotap
// application/vnd.motorola.flexsuite.kmr
// application/vnd.motorola.flexsuite.ttc
// application/vnd.motorola.flexsuite.wem
// application/vnd.mozilla.xul+xmlxul
// application/vnd.ms-artgalry
// application/vnd.ms-asf
// application/vnd.ms-excel.addin.macroEnabled.12xlam
// application/vnd.ms-excel.sheet.binary.macroEnabled.12xlsb
// application/vnd.ms-excel.sheet.macroEnabled.12xlsm
// application/vnd.ms-excel.template.macroEnabled.12xltm
// application/vnd.ms-fontobjecteot
// application/vnd.ms-lrm
// application/vnd.ms-officethemethmx
// application/vnd.ms-pki.seccatcat
// #application/vnd.ms-pki.stlstl
// application/vnd.ms-powerpoint.addin.macroEnabled.12ppam
// application/vnd.ms-powerpoint.presentation.macroEnabled.12pptm
// application/vnd.ms-powerpoint.slide.macroEnabled.12sldm
// application/vnd.ms-powerpoint.slideshow.macroEnabled.12ppsm
// application/vnd.ms-powerpoint.template.macroEnabled.12potm
// application/vnd.ms-project
// application/vnd.ms-word.document.macroEnabled.12docm
// application/vnd.ms-word.template.macroEnabled.12dotm
// application/vnd.ms-works
// application/vnd.mseq
// application/vnd.msign
// application/vnd.music-niff
// application/vnd.musician
// application/vnd.netfpx
// application/vnd.noblenet-directory
// application/vnd.noblenet-sealer
// application/vnd.noblenet-web
// application/vnd.novadigm.EDM
// application/vnd.novadigm.EDX
// application/vnd.novadigm.EXT
// application/vnd.osa.netdeploy
// application/vnd.palm
// application/vnd.pg.format
// application/vnd.pg.osasli
// application/vnd.powerbuilder6
// application/vnd.powerbuilder6-s
// application/vnd.powerbuilder7
// application/vnd.powerbuilder7-s
// application/vnd.powerbuilder75
// application/vnd.powerbuilder75-s
// application/vnd.previewsystems.box
// application/vnd.publishare-delta-tree
// application/vnd.pvi.ptid1
// application/vnd.pwg-xhtml-print+xml
// application/vnd.rapid
// application/vnd.rim.codcod
// application/vnd.s3sms
// application/vnd.seemail
// application/vnd.shana.informed.formdata
// application/vnd.shana.informed.formtemplate
// application/vnd.shana.informed.interchange
// application/vnd.shana.informed.package
// application/vnd.smafmmf
// application/vnd.sss-cod
// application/vnd.sss-dtf
// application/vnd.sss-ntf
// application/vnd.stardivision.calcsdc
// application/vnd.stardivision.chartsds
// application/vnd.stardivision.drawsda
// application/vnd.stardivision.impresssdd
// application/vnd.stardivision.mathsdf
// application/vnd.stardivision.writersdw
// application/vnd.stardivision.writer-globalsgl
// application/vnd.street-stream
// application/vnd.svd
// application/vnd.swiftview-ics
// application/vnd.symbian.installsis
// application/vnd.tcpdump.pcapcap pcap
// application/vnd.triscape.mxs
// application/vnd.trueapp
// application/vnd.truedoc
// application/vnd.tve-trigger
// application/vnd.ufdl
// application/vnd.uplanet.alert
// application/vnd.uplanet.alert-wbxml
// application/vnd.uplanet.bearer-choice
// application/vnd.uplanet.bearer-choice-wbxml
// application/vnd.uplanet.cacheop
// application/vnd.uplanet.cacheop-wbxml
// application/vnd.uplanet.channel
// application/vnd.uplanet.channel-wbxml
// application/vnd.uplanet.list
// application/vnd.uplanet.list-wbxml
// application/vnd.uplanet.listcmd
// application/vnd.uplanet.listcmd-wbxml
// application/vnd.uplanet.signal
// application/vnd.vcx
// application/vnd.vectorworks
// application/vnd.vidsoft.vidconference
// application/vnd.visiovsd vst vsw vss
// application/vnd.vividence.scriptfile
// application/vnd.wap.sic
// application/vnd.wap.slc
// application/vnd.wap.wbxmlwbxml
// application/vnd.wap.wmlcwmlc
// application/vnd.wap.wmlscriptcwmlsc
// application/vnd.webturbo
// application/vnd.wordperfectwpd
// application/vnd.wordperfect5.1wp5
// application/vnd.wrq-hp3000-labelled
// application/vnd.wt.stf
// application/vnd.xara
// application/vnd.xfdl
// application/vnd.yellowriver-custom-menu
// application/zlib
// application/x-123wk
// application/x-abiwordabw
// application/x-bcpiobcpio
// application/x-cabcab
// application/x-cbrcbr
// application/x-cbzcbz
// application/x-cdfcdf cda
// application/x-cdlinkvcd
// application/x-chess-pgnpgn
// application/x-comsolmph
// application/x-core
// application/x-cpiocpio
// application/x-directordcr dir dxr
// application/x-dmsdms
// application/x-doomwad
// application/x-dvidvi
// application/x-freemindmm
// application/x-futuresplashspl
// application/x-ganttprojectgan
// application/x-gnumericgnumeric
// application/x-go-sgfsgf
// application/x-graphing-calculatorgcf
// application/x-hdfhdf
// #application/x-httpd-erubyrhtml
// #application/x-httpd-phpphtml pht php
// #application/x-httpd-php-sourcephps
// #application/x-httpd-php3php3
// #application/x-httpd-php3-preprocessedphp3p
// #application/x-httpd-php4php4
// #application/x-httpd-php5php5
// application/x-hwphwp
// application/x-icaica
// application/x-infoinfo
// application/x-internet-signupins isp
// application/x-iphoneiii
// application/x-iso9660-imageiso
// application/x-jamjam
// application/x-java-applet
// application/x-java-bean
// application/x-java-jnlp-filejnlp
// application/x-jmoljmz
// application/x-kchartchrt
// application/x-kdelnk
// application/x-killustratorkil
// application/x-koanskp skd skt skm
// application/x-kpresenterkpr kpt
// application/x-kspreadksp
// application/x-kwordkwd kwt
// application/x-lhalha
// application/x-lyxlyx
// application/x-lzhlzh
// application/x-lzxlzx
// application/x-makerfrm maker frame fm fb book fbdoc
// application/x-mifmif
// application/x-mpegURLm3u8
// application/x-ms-applicationapplication
// application/x-ms-manifestmanifest
// application/x-ms-wmdwmd
// application/x-ms-wmzwmz
// application/x-msimsi
// application/x-netcdfnc
// application/x-ns-proxy-autoconfigpac
// application/x-nwcnwc
// application/x-oz-applicationoza
// application/x-pkcs7-certreqrespp7r
// application/x-pkcs7-crlcrl
// application/x-qgisqgs shp shx
// application/x-quicktimeplayerqtl
// application/x-rdprdp
// application/x-rx
// application/x-scilabsci sce
// application/x-scilab-xcosxcos
// application/x-shellscript
// application/x-shockwave-flashswf swfl
// application/x-silverlightscr
// application/x-sv4cpiosv4cpio
// application/x-sv4crcsv4crc
// application/x-tex-gfgf
// application/x-tex-pkpk
// application/x-ustarustar
// application/x-videolan
// application/x-wais-sourcesrc
// application/x-wingzwz
// application/x-x509-ca-certcrt
// application/x-xpinstallxpi
// audio/32kadpcm
// audio/3gpp
// audio/g.722.1
// audio/l16
// audio/mp4a-latm
// audio/mpa-robust
// audio/parityfec
// audio/telephone-event
// audio/tone
// audio/vnd.cisco.nse
// audio/vnd.cns.anp1
// audio/vnd.cns.inf1
// audio/vnd.digital-winds
// audio/vnd.everad.plj
// audio/vnd.lucent.voice
// audio/vnd.nortel.vbk
// audio/vnd.nuera.ecelp4800
// audio/vnd.nuera.ecelp7470
// audio/vnd.nuera.ecelp9600
// audio/vnd.octel.sbc
// audio/vnd.qcelp
// audio/vnd.rhetorex.32kadpcm
// audio/vnd.vmx.cvsd
// chemical/x-alchemyalc
// chemical/x-cachecac cache
// chemical/x-cache-csfcsf
// chemical/x-cactvs-binarycbin cascii ctab
// chemical/x-cdxcdx
// chemical/x-ceriuscer
// chemical/x-chem3dc3d
// chemical/x-chemdrawchm
// chemical/x-cifcif
// chemical/x-cmdfcmdf
// chemical/x-cmlcml
// chemical/x-compasscpa
// chemical/x-crossfirebsd
// chemical/x-csmlcsml csm
// chemical/x-ctxctx
// chemical/x-cxfcxf cef
// #chemical/x-daylight-smilessmi
// chemical/x-embl-dl-nucleotideemb embl
// chemical/x-galactic-spcspc
// chemical/x-gamess-inputinp gam gamin
// chemical/x-gaussian-checkpointfch fchk
// chemical/x-gaussian-cubecub
// chemical/x-gaussian-inputgau gjc gjf
// chemical/x-gaussian-loggal
// chemical/x-gcg8-sequencegcg
// chemical/x-genbankgen
// chemical/x-hinhin
// chemical/x-isostaristr ist
// chemical/x-jcamp-dxjdx dx
// chemical/x-kinemagekin
// chemical/x-macmoleculemcm
// chemical/x-macromodel-inputmmd mmod
// chemical/x-mdl-molfilemol
// chemical/x-mdl-rdfilerd
// chemical/x-mdl-rxnfilerxn
// chemical/x-mdl-sdfilesd sdf
// chemical/x-mdl-tgftgf
// #chemical/x-mifmif
// chemical/x-mmcifmcif
// chemical/x-mol2mol2
// chemical/x-molconn-Zb
// chemical/x-mopac-graphgpt
// chemical/x-mopac-inputmop mopcrt mpc zmt
// chemical/x-mopac-outmoo
// chemical/x-mopac-vibmvb
// chemical/x-ncbi-asn1asn
// chemical/x-ncbi-asn1-asciiprt ent
// chemical/x-ncbi-asn1-binaryval aso
// chemical/x-ncbi-asn1-specasn
// chemical/x-pdbpdb ent
// chemical/x-rosdalros
// chemical/x-swissprotsw
// chemical/x-vamas-iso14976vms
// chemical/x-vmdvmd
// chemical/x-xtelxtel
// chemical/x-xyzxyz
// image/cgm
// image/g3fax
// image/naplps
// image/prs.btif
// image/prs.pti
// image/vnd.cns.inf2
// image/vnd.dwg
// image/vnd.dxf
// image/vnd.fastbidsheet
// image/vnd.fpx
// image/vnd.fst
// image/vnd.fujixerox.edmics-mmr
// image/vnd.fujixerox.edmics-rlc
// image/vnd.mix
// image/vnd.net-fpx
// image/vnd.svf
// image/vnd.xiff
// image/x-icon
// inode/chardevice
// inode/blockdevice
// inode/directory-locked
// inode/directory
// inode/fifo
// inode/socket
// message/delivery-status
// message/disposition-notification
// message/external-body
// message/http
// message/s-http
// message/news
// message/partial
// message/rfc822eml
// model/vnd.dwf
// model/vnd.flatland.3dml
// model/vnd.gdl
// model/vnd.gs-gdl
// model/vnd.gtw
// model/vnd.mts
// model/vnd.vtu
// multipart/alternative
// multipart/appledouble
// multipart/byteranges
// multipart/digest
// multipart/encrypted
// multipart/form-data
// multipart/header-set
// multipart/mixed
// multipart/parallel
// multipart/related
// multipart/report
// multipart/signed
// multipart/voice-message
// text/english
// text/enriched
// {"text/x-gap",
// {"text/x-gtkrc",
// text/h323323
// text/iulsuls
//{"text/x-idl",
//{"text/x-netrexx",
//{"text/x-ocl",
//{"text/x-dtd",
// {"text/x-gettext-translation",
// {"text/x-gettext-translation-template",
// text/parityfec
// text/prs.lines.tag
// text/rfc822-headers
// text/scriptletsct wsc
// text/t140
// text/texmacstm
// text/turtlettl
// text/vnd.abc
// text/vnd.curl
// text/vnd.debian.copyright
// text/vnd.DMClientScript
// text/vnd.flatland.3dml
// text/vnd.fly
// text/vnd.fmi.flexstor
// text/vnd.in3d.3dml
// text/vnd.in3d.spot
// text/vnd.IPTC.NewsML
// text/vnd.IPTC.NITF
// text/vnd.latex-z
// text/vnd.motorola.reflex
// text/vnd.ms-mediapackage
// text/vnd.sun.j2me.app-descriptorjad
// text/vnd.wap.si
// text/vnd.wap.sl
// text/vnd.wap.wmlwml
// text/vnd.wap.wmlscriptwmls
// text/x-booboo
// text/x-componenthtc
// text/x-crontab
// text/x-lilypondly
// text/x-pcs-gcdgcd
// text/x-setextetx
// text/x-sfvsfv
// video/mp4v-es
// video/parityfec
// video/pointer
// video/vnd.fvt
// video/vnd.motorola.video
// video/vnd.motorola.videop
// video/vnd.mts
// video/vnd.nokia.interleaved-multimedia
// video/vnd.vivo
// x-conference/x-cooltalkice
//
// x-epoc/x-sisx-appsisx
// 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 fsx
import (
"io/fs"
"os"
"path/filepath"
"strings"
"cogentcore.org/core/base/errors"
)
// Sub returns [fs.Sub] with any error automatically logged
// for cases where the directory is hardcoded and there is
// no chance of error.
func Sub(fsys fs.FS, dir string) fs.FS {
return errors.Log1(fs.Sub(fsys, dir))
}
// DirFS returns the directory part of given file path as an os.DirFS
// and the filename as a string. These can then be used to access the file
// using the FS-based interface, consistent with embed and other use-cases.
func DirFS(fpath string) (fs.FS, string, error) {
fabs, err := filepath.Abs(fpath)
if err != nil {
return nil, "", err
}
dir, fname := filepath.Split(fabs)
dfs := os.DirFS(dir)
return dfs, fname, nil
}
// FileExistsFS checks whether given file exists, returning true if so,
// false if not, and error if there is an error in accessing the file.
func FileExistsFS(fsys fs.FS, filePath string) (bool, error) {
if fsys, ok := fsys.(fs.StatFS); ok {
fileInfo, err := fsys.Stat(filePath)
if err == nil {
return !fileInfo.IsDir(), nil
}
if errors.Is(err, fs.ErrNotExist) {
return false, nil
}
return false, err
}
fp, err := fsys.Open(filePath)
if err == nil {
fp.Close()
return true, nil
}
if errors.Is(err, fs.ErrNotExist) {
return false, nil
}
return false, err
}
// SplitRootPathFS returns a split of the given FS path (only / path separators)
// into the root element and everything after that point.
// Examples:
// - "/a/b/c" returns "/", "a/b/c"
// - "a/b/c" returns "a", "b/c" (note removal of intervening "/")
// - "a" returns "a", ""
// - "a/" returns "a", "" (note removal of trailing "/")
func SplitRootPathFS(path string) (root, rest string) {
pi := strings.IndexByte(path, '/')
if pi < 0 {
return path, ""
}
if pi == 0 {
return "/", path[1:]
}
if pi < len(path)-1 {
return path[:pi], path[pi+1:]
}
return path[:pi], ""
}
// 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 fsx provides various utility functions for dealing with filesystems.
package fsx
import (
"errors"
"fmt"
"go/build"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
// GoSrcDir tries to locate dir in GOPATH/src/ or GOROOT/src/pkg/ and returns its
// full path. GOPATH may contain a list of paths. From Robin Elkind github.com/mewkiz/pkg.
func GoSrcDir(dir string) (absDir string, err error) {
for _, srcDir := range build.Default.SrcDirs() {
absDir = filepath.Join(srcDir, dir)
finfo, err := os.Stat(absDir)
if err == nil && finfo.IsDir() {
return absDir, nil
}
}
return "", fmt.Errorf("fsx.GoSrcDir: unable to locate directory (%q) in GOPATH/src/ (%q) or GOROOT/src/pkg/ (%q)", dir, os.Getenv("GOPATH"), os.Getenv("GOROOT"))
}
// Files returns all the FileInfo's for files with given extension(s) in directory
// in sorted order (if extensions are empty then all files are returned).
// In case of error, returns nil.
func Files(path string, extensions ...string) []fs.DirEntry {
files, err := os.ReadDir(path)
if err != nil {
return nil
}
if len(extensions) == 0 {
return files
}
sz := len(files)
if sz == 0 {
return nil
}
for i := sz - 1; i >= 0; i-- {
fn := files[i]
ext := filepath.Ext(fn.Name())
keep := false
for _, ex := range extensions {
if strings.EqualFold(ext, ex) {
keep = true
break
}
}
if !keep {
files = append(files[:i], files[i+1:]...)
}
}
return files
}
// Filenames returns all the file names with given extension(s) in directory
// in sorted order (if extensions is empty then all files are returned)
func Filenames(path string, extensions ...string) []string {
f, err := os.Open(path)
if err != nil {
return nil
}
files, err := f.Readdirnames(-1)
f.Close()
if err != nil {
return nil
}
if len(extensions) == 0 {
sort.StringSlice(files).Sort()
return files
}
sz := len(files)
if sz == 0 {
return nil
}
for i := sz - 1; i >= 0; i-- {
fn := files[i]
ext := filepath.Ext(fn)
keep := false
for _, ex := range extensions {
if strings.EqualFold(ext, ex) {
keep = true
break
}
}
if !keep {
files = append(files[:i], files[i+1:]...)
}
}
sort.StringSlice(files).Sort()
return files
}
// Dirs returns a slice of all the directories within a given directory
func Dirs(path string) []string {
files, err := os.ReadDir(path)
if err != nil {
return nil
}
var fnms []string
for _, fi := range files {
if fi.IsDir() {
fnms = append(fnms, fi.Name())
}
}
return fnms
}
// LatestMod returns the latest (most recent) modification time for any of the
// files in the directory (optionally filtered by extension(s) if exts != nil)
// if no files or error, returns zero time value
func LatestMod(path string, exts ...string) time.Time {
tm := time.Time{}
files := Files(path, exts...)
if len(files) == 0 {
return tm
}
for _, de := range files {
fi, err := de.Info()
if err == nil {
if fi.ModTime().After(tm) {
tm = fi.ModTime()
}
}
}
return tm
}
// HasFile returns true if given directory has given file (exact match)
func HasFile(path, file string) bool {
files, err := os.ReadDir(path)
if err != nil {
return false
}
for _, fn := range files {
if fn.Name() == file {
return true
}
}
return false
}
// FindFilesOnPaths attempts to locate given file(s) on given list of paths,
// returning the full Abs path to each file found (nil if none)
func FindFilesOnPaths(paths []string, files ...string) []string {
var res []string
for _, path := range paths {
for _, fn := range files {
fp := filepath.Join(path, fn)
ok, _ := FileExists(fp)
if ok {
res = append(res, fp)
}
}
}
return res
}
// FileExists checks whether given file exists, returning true if so,
// false if not, and error if there is an error in accessing the file.
func FileExists(filePath string) (bool, error) {
fileInfo, err := os.Stat(filePath)
if err == nil {
return !fileInfo.IsDir(), nil
}
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
return false, err
}
// DirAndFile returns the final dir and file name.
func DirAndFile(file string) string {
dir, fnm := filepath.Split(file)
return filepath.Join(filepath.Base(dir), fnm)
}
// RelativeFilePath returns the file name relative to given root file path, if it is
// under that root; otherwise it returns the final dir and file name.
func RelativeFilePath(file, root string) string {
rp, err := filepath.Rel(root, file)
if err == nil && !strings.HasPrefix(rp, "..") {
return rp
}
return DirAndFile(file)
}
// ExtSplit returns the split between the extension and name before
// the extension, for the given file name. Any path elements in the
// file name are preserved; pass [filepath.Base](file) to extract only the
// last element of the file path if that is what is desired.
func ExtSplit(file string) (base, ext string) {
ext = filepath.Ext(file)
base = strings.TrimSuffix(file, ext)
return
}
// 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 generate provides utilities for building code generators in Go.
// The standard path for a code generator is: [Load] -> [PrintHeader] -> [Inspect] -> [Write].
package generate
import (
"fmt"
"go/ast"
"io"
"os"
"path/filepath"
"strings"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/imports"
)
// Load loads and returns the Go packages named by the given patterns.
// Load calls [packages.Load] and ensures that there is at least one
// package; this means that, if there is a nil error, the length
// of the resulting packages is guaranteed to be greater than zero.
func Load(cfg *packages.Config, patterns ...string) ([]*packages.Package, error) {
pkgs, err := packages.Load(cfg, patterns...)
if err != nil {
return nil, err
}
if len(pkgs) == 0 {
return nil, fmt.Errorf("expected at least one package but got %d", len(pkgs))
}
return pkgs, nil
}
// PrintHeader prints a header to the given writer for a generated
// file in the given package with the given imports. Imports do not
// need to be set if you are running [Format] on the code later,
// but they should be set for any external packages that many not
// be found correctly by goimports.
func PrintHeader(w io.Writer, pkg string, imports ...string) {
cmdstr := strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
if len(os.Args) > 1 {
cmdstr += " " + strings.Join(os.Args[1:], " ")
}
fmt.Fprintf(w, "// Code generated by \"%s\"; DO NOT EDIT.\n\n", cmdstr)
fmt.Fprintf(w, "package %s\n", pkg)
if len(imports) > 0 {
fmt.Fprint(w, "import (\n")
for _, imp := range imports {
fmt.Fprintf(w, "\t%q\n", imp)
}
fmt.Fprint(w, ")\n")
}
}
// Inspect goes through all of the files in the given package
// and calls the given function on each node in files that
// are not generated. The bool return value from the given function
// indicates whether to continue traversing down the AST tree
// of that node and look at its children. If a non-nil error value
// is returned by the given function, the traversal of the tree is
// stopped and the error value is returned.
func Inspect(pkg *packages.Package, f func(n ast.Node) (bool, error)) error {
for _, file := range pkg.Syntax {
if ast.IsGenerated(file) {
continue
}
var terr error
var terrNode ast.Node
ast.Inspect(file, func(n ast.Node) bool {
if terr != nil {
return false
}
cont, err := f(n)
if err != nil {
terr = err
terrNode = n
}
return cont
})
if terr != nil {
return fmt.Errorf("generate.Inspect: error while calling inspect function for node %v: %w", terrNode, terr)
}
}
return nil
}
// Filepath returns the filepath of a file in the given
// package with the given filename relative to the package.
func Filepath(pkg *packages.Package, filename string) string {
dir := "."
if len(pkg.Syntax) > 0 {
dir = filepath.Dir(pkg.Fset.Position(pkg.Syntax[0].FileStart).Filename)
}
return filepath.Join(dir, filename)
}
// Write writes the given bytes to the given filename after
// applying goimports using the given options.
func Write(filename string, src []byte, opt *imports.Options) error {
b, ferr := Format(filename, src, opt)
// we still write file even if formatting failed, as it is still useful
// then we handle error later
werr := os.WriteFile(filename, b, 0666)
if werr != nil {
return fmt.Errorf("generate.Write: error writing file: %w", werr)
}
if ferr != nil {
return fmt.Errorf("generate.Write: error formatting code: %w", ferr)
}
return nil
}
// Format returns the given bytes with goimports applied.
// It wraps [imports.Process] by wrapping any error with
// additional context.
func Format(filename string, src []byte, opt *imports.Options) ([]byte, error) {
b, err := imports.Process(filename, src, opt)
if err != nil {
// Should never happen, but can arise when developing code.
// The user can compile the output to see the error.
return src, fmt.Errorf("internal/programmer error: generate.Format: invalid Go generated: %w; compile the package to analyze the error", err)
}
return b, nil
}
// Code generated by "core generate"; DO NOT EDIT.
package indent
import (
"cogentcore.org/core/enums"
)
var _CharacterValues = []Character{0, 1}
// CharacterN is the highest valid value for type Character, plus one.
const CharacterN Character = 2
var _CharacterValueMap = map[string]Character{`Tab`: 0, `Space`: 1}
var _CharacterDescMap = map[Character]string{0: `Tab indicates to use tabs for indentation.`, 1: `Space indicates to use spaces for indentation.`}
var _CharacterMap = map[Character]string{0: `Tab`, 1: `Space`}
// String returns the string representation of this Character value.
func (i Character) String() string { return enums.String(i, _CharacterMap) }
// SetString sets the Character value from its string representation,
// and returns an error if the string is invalid.
func (i *Character) SetString(s string) error {
return enums.SetString(i, s, _CharacterValueMap, "Character")
}
// Int64 returns the Character value as an int64.
func (i Character) Int64() int64 { return int64(i) }
// SetInt64 sets the Character value from an int64.
func (i *Character) SetInt64(in int64) { *i = Character(in) }
// Desc returns the description of the Character value.
func (i Character) Desc() string { return enums.Desc(i, _CharacterDescMap) }
// CharacterValues returns all possible values for the type Character.
func CharacterValues() []Character { return _CharacterValues }
// Values returns all possible values for the type Character.
func (i Character) Values() []enums.Enum { return enums.Values(_CharacterValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Character) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Character) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "Character")
}
// 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 indent provides indentation generation methods.
package indent
//go:generate core generate
import (
"bytes"
"strings"
)
// Character is the type of indentation character to use.
type Character int32 //enums:enum
const (
// Tab indicates to use tabs for indentation.
Tab Character = iota
// Space indicates to use spaces for indentation.
Space
)
// Tabs returns a string of n tabs.
func Tabs(n int) string {
return strings.Repeat("\t", n)
}
// TabBytes returns []byte of n tabs.
func TabBytes(n int) []byte {
return bytes.Repeat([]byte("\t"), n)
}
// Spaces returns a string of n*width spaces.
func Spaces(n, width int) string {
return strings.Repeat(" ", n*width)
}
// SpaceBytes returns a []byte of n*width spaces.
func SpaceBytes(n, width int) []byte {
return bytes.Repeat([]byte(" "), n*width)
}
// String returns a string of n tabs or n*width spaces depending on the indent character.
func String(ich Character, n, width int) string {
if ich == Tab {
return Tabs(n)
}
return Spaces(n, width)
}
// Bytes returns []byte of n tabs or n*width spaces depending on the indent character.
func Bytes(ich Character, n, width int) []byte {
if ich == Tab {
return TabBytes(n)
}
return SpaceBytes(n, width)
}
// Len returns the length of the indent string given indent character and indent level.
func Len(ich Character, n, width int) int {
if ich == Tab {
return n
}
return n * width
}
// 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 iox provides boilerplate wrapper functions for the Go standard
// io functions to Read, Open, Write, and Save, with implementations for
// commonly used encoding formats.
package iox
import (
"bufio"
"bytes"
"io"
"io/fs"
"os"
)
// Decoder is an interface for standard decoder types
type Decoder interface {
// Decode decodes from io.Reader specified at creation
Decode(v any) error
}
// DecoderFunc is a function that creates a new Decoder for given reader
type DecoderFunc func(r io.Reader) Decoder
// NewDecoderFunc returns a DecoderFunc for a specific Decoder type
func NewDecoderFunc[T Decoder](f func(r io.Reader) T) DecoderFunc {
return func(r io.Reader) Decoder { return f(r) }
}
// Open reads the given object from the given filename using the given [DecoderFunc]
func Open(v any, filename string, f DecoderFunc) error {
fp, err := os.Open(filename)
if err != nil {
return err
}
defer fp.Close()
return Read(v, bufio.NewReader(fp), f)
}
// OpenFiles reads the given object from the given filenames using the given [DecoderFunc]
func OpenFiles(v any, filenames []string, f DecoderFunc) error {
for _, file := range filenames {
err := Open(v, file, f)
if err != nil {
return err
}
}
return nil
}
// OpenFS reads the given object from the given filename using the given [DecoderFunc],
// using the given [fs.FS] filesystem (e.g., for embed files)
func OpenFS(v any, fsys fs.FS, filename string, f DecoderFunc) error {
fp, err := fsys.Open(filename)
if err != nil {
return err
}
defer fp.Close()
return Read(v, bufio.NewReader(fp), f)
}
// OpenFilesFS reads the given object from the given filenames using the given [DecoderFunc],
// using the given [fs.FS] filesystem (e.g., for embed files)
func OpenFilesFS(v any, fsys fs.FS, filenames []string, f DecoderFunc) error {
for _, file := range filenames {
err := OpenFS(v, fsys, file, f)
if err != nil {
return err
}
}
return nil
}
// Read reads the given object from the given reader,
// using the given [DecoderFunc]
func Read(v any, reader io.Reader, f DecoderFunc) error {
d := f(reader)
return d.Decode(v)
}
// ReadBytes reads the given object from the given bytes,
// using the given [DecoderFunc]
func ReadBytes(v any, data []byte, f DecoderFunc) error {
b := bytes.NewBuffer(data)
return Read(v, b, f)
}
// 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 iox
import (
"bufio"
"bytes"
"io"
"os"
)
// Encoder is an interface for standard encoder types
type Encoder interface {
// Encode encodes to io.Writer specified at creation
Encode(v any) error
}
// EncoderFunc is a function that creates a new Encoder for given writer
type EncoderFunc func(w io.Writer) Encoder
// NewEncoderFunc returns a EncoderFunc for a specific Encoder type
func NewEncoderFunc[T Encoder](f func(w io.Writer) T) EncoderFunc {
return func(w io.Writer) Encoder { return f(w) }
}
// Save writes the given object to the given filename using the given [EncoderFunc]
func Save(v any, filename string, f EncoderFunc) error {
fp, err := os.Create(filename)
if err != nil {
return err
}
defer fp.Close()
bw := bufio.NewWriter(fp)
err = Write(v, bw, f)
if err != nil {
return err
}
return bw.Flush()
}
// Write writes the given object using the given [EncoderFunc]
func Write(v any, writer io.Writer, f EncoderFunc) error {
e := f(writer)
return e.Encode(v)
}
// WriteBytes writes the given object, returning bytes of the encoding,
// using the given [EncoderFunc]
func WriteBytes(v any, f EncoderFunc) ([]byte, error) {
var b bytes.Buffer
e := f(&b)
err := e.Encode(v)
if err != nil {
return nil, err
}
return b.Bytes(), nil
}
// 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 imagex
import (
"bytes"
"encoding/base64"
"errors"
"image"
"image/jpeg"
"image/png"
"log"
"strings"
)
// ToBase64PNG returns bytes of image encoded as a PNG in Base64 format
// with "image/png" mimetype returned
func ToBase64PNG(img image.Image) ([]byte, string) {
ibuf := &bytes.Buffer{}
png.Encode(ibuf, img)
ib := ibuf.Bytes()
eb := make([]byte, base64.StdEncoding.EncodedLen(len(ib)))
base64.StdEncoding.Encode(eb, ib)
return eb, "image/png"
}
// ToBase64JPG returns bytes image encoded as a JPG in Base64 format
// with "image/jpeg" mimetype returned
func ToBase64JPG(img image.Image) ([]byte, string) {
ibuf := &bytes.Buffer{}
jpeg.Encode(ibuf, img, &jpeg.Options{Quality: 90})
ib := ibuf.Bytes()
eb := make([]byte, base64.StdEncoding.EncodedLen(len(ib)))
base64.StdEncoding.Encode(eb, ib)
return eb, "image/jpeg"
}
// Base64SplitLines splits the encoded Base64 bytes into standard lines of 76
// chars each. The last line also ends in a newline
func Base64SplitLines(b []byte) []byte {
ll := 76
sz := len(b)
nl := (sz / ll)
rb := make([]byte, sz+nl+1)
for i := 0; i < nl; i++ {
st := ll * i
rst := ll*i + i
copy(rb[rst:rst+ll], b[st:st+ll])
rb[rst+ll] = '\n'
}
st := ll * nl
rst := ll*nl + nl
ln := sz - st
copy(rb[rst:rst+ln], b[st:st+ln])
rb[rst+ln] = '\n'
return rb
}
// FromBase64PNG returns image from Base64-encoded bytes in PNG format
func FromBase64PNG(eb []byte) (image.Image, error) {
if eb[76] == ' ' {
eb = bytes.ReplaceAll(eb, []byte(" "), []byte("\n"))
}
db := make([]byte, base64.StdEncoding.DecodedLen(len(eb)))
_, err := base64.StdEncoding.Decode(db, eb)
if err != nil {
log.Println(err)
return nil, err
}
rb := bytes.NewReader(db)
return png.Decode(rb)
}
// FromBase64JPG returns image from Base64-encoded bytes in PNG format
func FromBase64JPG(eb []byte) (image.Image, error) {
if eb[76] == ' ' {
eb = bytes.ReplaceAll(eb, []byte(" "), []byte("\n"))
}
db := make([]byte, base64.StdEncoding.DecodedLen(len(eb)))
_, err := base64.StdEncoding.Decode(db, eb)
if err != nil {
log.Println(err)
return nil, err
}
rb := bytes.NewReader(db)
return jpeg.Decode(rb)
}
// FromBase64 returns image from Base64-encoded bytes in either PNG or JPEG format
// based on fmt which must end in either png, jpg, or jpeg
func FromBase64(fmt string, eb []byte) (image.Image, error) {
if strings.HasSuffix(fmt, "png") {
return FromBase64PNG(eb)
}
if strings.HasSuffix(fmt, "jpg") || strings.HasSuffix(fmt, "jpeg") {
return FromBase64JPG(eb)
}
return nil, errors.New("image format must be either png or jpeg")
}
// Code generated by "core generate"; DO NOT EDIT.
package imagex
import (
"cogentcore.org/core/enums"
)
var _FormatsValues = []Formats{0, 1, 2, 3, 4, 5, 6}
// FormatsN is the highest valid value for type Formats, plus one.
const FormatsN Formats = 7
var _FormatsValueMap = map[string]Formats{`None`: 0, `PNG`: 1, `JPEG`: 2, `GIF`: 3, `TIFF`: 4, `BMP`: 5, `WebP`: 6}
var _FormatsDescMap = map[Formats]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``, 5: ``, 6: ``}
var _FormatsMap = map[Formats]string{0: `None`, 1: `PNG`, 2: `JPEG`, 3: `GIF`, 4: `TIFF`, 5: `BMP`, 6: `WebP`}
// String returns the string representation of this Formats value.
func (i Formats) String() string { return enums.String(i, _FormatsMap) }
// SetString sets the Formats value from its string representation,
// and returns an error if the string is invalid.
func (i *Formats) SetString(s string) error {
return enums.SetString(i, s, _FormatsValueMap, "Formats")
}
// Int64 returns the Formats value as an int64.
func (i Formats) Int64() int64 { return int64(i) }
// SetInt64 sets the Formats value from an int64.
func (i *Formats) SetInt64(in int64) { *i = Formats(in) }
// Desc returns the description of the Formats value.
func (i Formats) Desc() string { return enums.Desc(i, _FormatsDescMap) }
// FormatsValues returns all possible values for the type Formats.
func FormatsValues() []Formats { return _FormatsValues }
// Values returns all possible values for the type Formats.
func (i Formats) Values() []enums.Enum { return enums.Values(_FormatsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Formats) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Formats) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Formats") }
// 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 imagex
//go:generate core generate
import (
"bufio"
"fmt"
"image"
"image/draw"
"image/gif"
"image/jpeg"
"image/png"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"golang.org/x/image/bmp"
"golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
)
// Formats are the supported image encoding / decoding formats
type Formats int32 //enums:enum
// The supported image encoding formats
const (
None Formats = iota
PNG
JPEG
GIF
TIFF
BMP
WebP
)
// ExtToFormat returns a Format based on a filename extension,
// which can start with a . or not
func ExtToFormat(ext string) (Formats, error) {
if len(ext) == 0 {
return None, fmt.Errorf("ExtToFormat: ext is empty")
}
if ext[0] == '.' {
ext = ext[1:]
}
ext = strings.ToLower(ext)
switch ext {
case "png":
return PNG, nil
case "jpg", "jpeg":
return JPEG, nil
case "gif":
return GIF, nil
case "tif", "tiff":
return TIFF, nil
case "bmp":
return BMP, nil
case "webp":
return WebP, nil
}
return None, fmt.Errorf("ExtToFormat: extension %q not recognized", ext)
}
// Open opens an image from the given filename.
// The format is inferred automatically,
// and is returned using the Formats enum.
// png, jpeg, gif, tiff, bmp, and webp are supported.
func Open(filename string) (image.Image, Formats, error) {
file, err := os.Open(filename)
if err != nil {
return nil, None, err
}
defer file.Close()
return Read(file)
}
// OpenFS opens an image from the given filename
// using the given [fs.FS] filesystem (e.g., for embed files).
// The format is inferred automatically,
// and is returned using the Formats enum.
// png, jpeg, gif, tiff, bmp, and webp are supported.
func OpenFS(fsys fs.FS, filename string) (image.Image, Formats, error) {
file, err := fsys.Open(filename)
if err != nil {
return nil, None, err
}
defer file.Close()
return Read(file)
}
// Read reads an image to the given reader,
// The format is inferred automatically,
// and is returned using the Formats enum.
// png, jpeg, gif, tiff, bmp, and webp are supported.
func Read(r io.Reader) (image.Image, Formats, error) {
im, ext, err := image.Decode(r)
if err != nil {
return im, None, err
}
f, err := ExtToFormat(ext)
return im, f, err
}
// Save saves the image to the given filename,
// with the format inferred from the filename.
// png, jpeg, gif, tiff, and bmp are supported.
func Save(im image.Image, filename string) error {
ext := filepath.Ext(filename)
f, err := ExtToFormat(ext)
if err != nil {
return err
}
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
bw := bufio.NewWriter(file)
defer bw.Flush()
return Write(im, file, f)
}
// Write writes the image to the given writer using the given foramt.
// png, jpeg, gif, tiff, and bmp are supported.
func Write(im image.Image, w io.Writer, f Formats) error {
switch f {
case PNG:
return png.Encode(w, im)
case JPEG:
return jpeg.Encode(w, im, &jpeg.Options{Quality: 90})
case GIF:
return gif.Encode(w, im, nil)
case TIFF:
return tiff.Encode(w, im, nil)
case BMP:
return bmp.Encode(w, im)
default:
return fmt.Errorf("iox/imagex.Save: format %q not valid", f)
}
}
// CloneAsRGBA returns an RGBA copy of the supplied image.
func CloneAsRGBA(src image.Image) *image.RGBA {
bounds := src.Bounds()
img := image.NewRGBA(bounds)
draw.Draw(img, bounds, src, bounds.Min, draw.Src)
return img
}
// Copyright 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 imagex
import (
"errors"
"image"
"image/color"
"io/fs"
"os"
"path/filepath"
"strings"
)
// TestingT is an interface wrapper around *testing.T
type TestingT interface {
Errorf(format string, args ...any)
}
// UpdateTestImages indicates whether to update currently saved test
// images in [AssertImage] instead of comparing against them.
// It is automatically set if the build tag "update" is specified,
// and it should typically only be set through that. It should only be
// set when behavior has been updated that causes test images to change,
// and it should only be set once and then turned back off.
var UpdateTestImages = updateTestImages
// CompareUint8 returns true if two numbers are more different than tol
func CompareUint8(cc, ic uint8, tol int) bool {
d := int(cc) - int(ic)
if d < -tol {
return false
}
if d > tol {
return false
}
return true
}
// CompareColors returns true if two colors are more different than tol
func CompareColors(cc, ic color.RGBA, tol int) bool {
if !CompareUint8(cc.R, ic.R, tol) {
return false
}
if !CompareUint8(cc.G, ic.G, tol) {
return false
}
if !CompareUint8(cc.B, ic.B, tol) {
return false
}
if !CompareUint8(cc.A, ic.A, tol) {
return false
}
return true
}
// Assert asserts that the given image is equivalent
// to the image stored at the given filename in the testdata directory,
// with ".png" added to the filename if there is no extension
// (eg: "button" becomes "testdata/button.png"). Forward slashes are
// automatically replaced with backslashes on Windows.
// If it is not, it fails the test with an error, but continues its
// execution. If there is no image at the given filename in the testdata
// directory, it creates the image.
func Assert(t TestingT, img image.Image, filename string) {
filename = filepath.Join("testdata", filename)
if filepath.Ext(filename) == "" {
filename += ".png"
}
err := os.MkdirAll(filepath.Dir(filename), 0750)
if err != nil {
t.Errorf("error making testdata directory: %v", err)
}
ext := filepath.Ext(filename)
failFilename := strings.TrimSuffix(filename, ext) + ".fail" + ext
if UpdateTestImages {
err := Save(img, filename)
if err != nil {
t.Errorf("AssertImage: error saving updated image: %v", err)
}
err = os.RemoveAll(failFilename)
if err != nil {
t.Errorf("AssertImage: error removing old fail image: %v", err)
}
return
}
fimg, _, err := Open(filename)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("AssertImage: error opening saved image: %v", err)
return
}
// we don't have the file yet, so we make it
err := Save(img, filename)
if err != nil {
t.Errorf("AssertImage: error saving new image: %v", err)
}
return
}
failed := false
ibounds := img.Bounds()
fbounds := fimg.Bounds()
if ibounds != fbounds {
t.Errorf("AssertImage: expected bounds %v for image for %s, but got bounds %v; see %s", fbounds, filename, ibounds, failFilename)
failed = true
} else {
for y := ibounds.Min.Y; y < ibounds.Max.Y; y++ {
for x := ibounds.Min.X; x < ibounds.Max.X; x++ {
cc := color.RGBAModel.Convert(img.At(x, y)).(color.RGBA)
ic := color.RGBAModel.Convert(fimg.At(x, y)).(color.RGBA)
if !CompareColors(cc, ic, 1) {
t.Errorf("AssertImage: image for %s is not the same as expected; see %s; expected color %v at (%d, %d), but got %v", filename, failFilename, ic, x, y, cc)
failed = true
break
}
}
if failed {
break
}
}
}
if failed {
err := Save(img, failFilename)
if err != nil {
t.Errorf("AssertImage: error saving fail image: %v", err)
}
} else {
err := os.RemoveAll(failFilename)
if err != nil {
t.Errorf("AssertImage: error removing old fail image: %v", err)
}
}
}
// 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 jsonx
import (
"encoding/json"
"io"
"io/fs"
"cogentcore.org/core/base/iox"
)
// Open reads the given object from the given filename using JSON encoding
func Open(v any, filename string) error {
return iox.Open(v, filename, iox.NewDecoderFunc(json.NewDecoder))
}
// OpenFiles reads the given object from the given filenames using JSON encoding
func OpenFiles(v any, filenames ...string) error {
return iox.OpenFiles(v, filenames, iox.NewDecoderFunc(json.NewDecoder))
}
// OpenFS reads the given object from the given filename using JSON encoding,
// using the given [fs.FS] filesystem (e.g., for embed files)
func OpenFS(v any, fsys fs.FS, filename string) error {
return iox.OpenFS(v, fsys, filename, iox.NewDecoderFunc(json.NewDecoder))
}
// OpenFilesFS reads the given object from the given filenames using JSON encoding,
// using the given [fs.FS] filesystem (e.g., for embed files)
func OpenFilesFS(v any, fsys fs.FS, filenames ...string) error {
return iox.OpenFilesFS(v, fsys, filenames, iox.NewDecoderFunc(json.NewDecoder))
}
// Read reads the given object from the given reader,
// using JSON encoding
func Read(v any, reader io.Reader) error {
return iox.Read(v, reader, iox.NewDecoderFunc(json.NewDecoder))
}
// ReadBytes reads the given object from the given bytes,
// using JSON encoding
func ReadBytes(v any, data []byte) error {
return iox.ReadBytes(v, data, iox.NewDecoderFunc(json.NewDecoder))
}
// Save writes the given object to the given filename using JSON encoding
func Save(v any, filename string) error {
return iox.Save(v, filename, iox.NewEncoderFunc(json.NewEncoder))
}
// Write writes the given object using JSON encoding
func Write(v any, writer io.Writer) error {
return iox.Write(v, writer, iox.NewEncoderFunc(json.NewEncoder))
}
// WriteBytes writes the given object, returning bytes of the encoding,
// using JSON encoding
func WriteBytes(v any) ([]byte, error) {
return iox.WriteBytes(v, iox.NewEncoderFunc(json.NewEncoder))
}
// IndentEncoderFunc is a [iox.EncoderFunc] that sets indentation
var IndentEncoderFunc = func(w io.Writer) iox.Encoder {
e := json.NewEncoder(w)
e.SetIndent("", "\t")
return e
}
// SaveIndent writes the given object to the given filename using JSON encoding, with indentation
func SaveIndent(v any, filename string) error {
return iox.Save(v, filename, IndentEncoderFunc)
}
// WriteIndent writes the given object using JSON encoding, with indentation
func WriteIndent(v any, writer io.Writer) error {
return iox.Write(v, writer, IndentEncoderFunc)
}
// WriteBytesIndent writes the given object, returning bytes of the encoding,
// using JSON encoding, with indentation
func WriteBytesIndent(v any) ([]byte, error) {
return iox.WriteBytes(v, IndentEncoderFunc)
}
// 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 tomlx
import (
"fmt"
"io"
"io/fs"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/iox"
"github.com/pelletier/go-toml/v2"
)
// NewDecoder returns a new [iox.Decoder]
func NewDecoder(r io.Reader) iox.Decoder { return toml.NewDecoder(r) }
// Open reads the given object from the given filename using TOML encoding
func Open(v any, filename string) error {
return iox.Open(v, filename, NewDecoder)
}
// OpenFiles reads the given object from the given filenames using TOML encoding
func OpenFiles(v any, filenames ...string) error {
return iox.OpenFiles(v, filenames, NewDecoder)
}
// OpenFS reads the given object from the given filename using TOML encoding,
// using the given [fs.FS] filesystem (e.g., for embed files)
func OpenFS(v any, fsys fs.FS, filename string) error {
return iox.OpenFS(v, fsys, filename, NewDecoder)
}
// OpenFilesFS reads the given object from the given filenames using TOML encoding,
// using the given [fs.FS] filesystem (e.g., for embed files)
func OpenFilesFS(v any, fsys fs.FS, filenames ...string) error {
return iox.OpenFilesFS(v, fsys, filenames, NewDecoder)
}
// Read reads the given object from the given reader,
// using TOML encoding
func Read(v any, reader io.Reader) error {
return iox.Read(v, reader, NewDecoder)
}
// ReadBytes reads the given object from the given bytes,
// using TOML encoding
func ReadBytes(v any, data []byte) error {
return iox.ReadBytes(v, data, NewDecoder)
}
// NewEncoder returns a new [iox.Encoder]
func NewEncoder(w io.Writer) iox.Encoder {
return toml.NewEncoder(w).SetIndentTables(true).SetArraysMultiline(true)
}
// Save writes the given object to the given filename using TOML encoding
func Save(v any, filename string) error {
return iox.Save(v, filename, NewEncoder)
}
// Write writes the given object using TOML encoding
func Write(v any, writer io.Writer) error {
return iox.Write(v, writer, NewEncoder)
}
// WriteBytes writes the given object, returning bytes of the encoding,
// using TOML encoding
func WriteBytes(v any) ([]byte, error) {
return iox.WriteBytes(v, NewEncoder)
}
// OpenFromPaths reads the given object from the given TOML file,
// looking on paths for the file.
func OpenFromPaths(v any, file string, paths ...string) error {
filenames := fsx.FindFilesOnPaths(paths, file)
if len(filenames) == 0 {
return fmt.Errorf("OpenFromPaths: no files found")
}
return Open(v, filenames[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 xmlx
import (
"encoding/xml"
"io"
"io/fs"
"cogentcore.org/core/base/iox"
)
// Open reads the given object from the given filename using XML encoding
func Open(v any, filename string) error {
return iox.Open(v, filename, iox.NewDecoderFunc(xml.NewDecoder))
}
// OpenFiles reads the given object from the given filenames using XML encoding
func OpenFiles(v any, filenames ...string) error {
return iox.OpenFiles(v, filenames, iox.NewDecoderFunc(xml.NewDecoder))
}
// OpenFS reads the given object from the given filename using XML encoding,
// using the given [fs.FS] filesystem (e.g., for embed files)
func OpenFS(v any, fsys fs.FS, filename string) error {
return iox.OpenFS(v, fsys, filename, iox.NewDecoderFunc(xml.NewDecoder))
}
// OpenFilesFS reads the given object from the given filenames using XML encoding,
// using the given [fs.FS] filesystem (e.g., for embed files)
func OpenFilesFS(v any, fsys fs.FS, filenames ...string) error {
return iox.OpenFilesFS(v, fsys, filenames, iox.NewDecoderFunc(xml.NewDecoder))
}
// Read reads the given object from the given reader,
// using XML encoding
func Read(v any, reader io.Reader) error {
return iox.Read(v, reader, iox.NewDecoderFunc(xml.NewDecoder))
}
// ReadBytes reads the given object from the given bytes,
// using XML encoding
func ReadBytes(v any, data []byte) error {
return iox.ReadBytes(v, data, iox.NewDecoderFunc(xml.NewDecoder))
}
// Save writes the given object to the given filename using XML encoding
func Save(v any, filename string) error {
return iox.Save(v, filename, iox.NewEncoderFunc(xml.NewEncoder))
}
// Write writes the given object using XML encoding
func Write(v any, writer io.Writer) error {
return iox.Write(v, writer, iox.NewEncoderFunc(xml.NewEncoder))
}
// WriteBytes writes the given object, returning bytes of the encoding,
// using XML encoding
func WriteBytes(v any) ([]byte, error) {
return iox.WriteBytes(v, iox.NewEncoderFunc(xml.NewEncoder))
}
// IndentEncoderFunc is a [iox.EncoderFunc] that sets indentation
var IndentEncoderFunc = func(w io.Writer) iox.Encoder {
e := xml.NewEncoder(w)
e.Indent("", "\t")
return e
}
// SaveIndent writes the given object to the given filename using XML encoding, with indentation
func SaveIndent(v any, filename string) error {
return iox.Save(v, filename, IndentEncoderFunc)
}
// WriteIndent writes the given object using XML encoding, with indentation
func WriteIndent(v any, writer io.Writer) error {
return iox.Write(v, writer, IndentEncoderFunc)
}
// WriteBytesIndent writes the given object, returning bytes of the encoding,
// using XML encoding, with indentation
func WriteBytesIndent(v any) ([]byte, error) {
return iox.WriteBytes(v, IndentEncoderFunc)
}
// 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 yamlx
import (
"io"
"io/fs"
"cogentcore.org/core/base/iox"
"gopkg.in/yaml.v3"
)
// Open reads the given object from the given filename using YAML encoding
func Open(v any, filename string) error {
return iox.Open(v, filename, iox.NewDecoderFunc(yaml.NewDecoder))
}
// OpenFiles reads the given object from the given filenames using YAML encoding
func OpenFiles(v any, filenames ...string) error {
return iox.OpenFiles(v, filenames, iox.NewDecoderFunc(yaml.NewDecoder))
}
// OpenFS reads the given object from the given filename using YAML encoding,
// using the given [fs.FS] filesystem (e.g., for embed files)
func OpenFS(v any, fsys fs.FS, filename string) error {
return iox.OpenFS(v, fsys, filename, iox.NewDecoderFunc(yaml.NewDecoder))
}
// OpenFilesFS reads the given object from the given filenames using YAML encoding,
// using the given [fs.FS] filesystem (e.g., for embed files)
func OpenFilesFS(v any, fsys fs.FS, filenames ...string) error {
return iox.OpenFilesFS(v, fsys, filenames, iox.NewDecoderFunc(yaml.NewDecoder))
}
// Read reads the given object from the given reader,
// using YAML encoding
func Read(v any, reader io.Reader) error {
return iox.Read(v, reader, iox.NewDecoderFunc(yaml.NewDecoder))
}
// ReadBytes reads the given object from the given bytes,
// using YAML encoding
func ReadBytes(v any, data []byte) error {
return iox.ReadBytes(v, data, iox.NewDecoderFunc(yaml.NewDecoder))
}
// Save writes the given object to the given filename using YAML encoding
func Save(v any, filename string) error {
return iox.Save(v, filename, iox.NewEncoderFunc(yaml.NewEncoder))
}
// Write writes the given object using YAML encoding
func Write(v any, writer io.Writer) error {
return iox.Write(v, writer, iox.NewEncoderFunc(yaml.NewEncoder))
}
// WriteBytes writes the given object, returning bytes of the encoding,
// using YAML encoding
func WriteBytes(v any) ([]byte, error) {
return iox.WriteBytes(v, iox.NewEncoderFunc(yaml.NewEncoder))
}
// Copyright (c) 2024, 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 labels
import (
"fmt"
"reflect"
"strings"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/base/strcase"
)
// FriendlyTypeName returns a user-friendly version of the name of the given type.
// It transforms it into sentence case, excludes the package, and converts various
// builtin types into more friendly forms (eg: "int" to "Number").
func FriendlyTypeName(typ reflect.Type) string {
nptyp := reflectx.NonPointerType(typ)
if nptyp == nil {
return "None"
}
nm := nptyp.Name()
// if it is named, we use that
if nm != "" {
switch nm {
case "string":
return "Text"
case "float32", "float64", "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr":
return "Number"
}
return strcase.ToSentence(nm)
}
// otherwise, we fall back on Kind
switch nptyp.Kind() {
case reflect.Slice, reflect.Array, reflect.Map:
bnm := FriendlyTypeName(nptyp.Elem())
if strings.HasSuffix(bnm, "s") {
return "List of " + bnm
} else if strings.Contains(bnm, "Function of") {
return strings.ReplaceAll(bnm, "Function of", "Functions of") + "s"
} else {
return bnm + "s"
}
case reflect.Func:
str := "Function"
ni := nptyp.NumIn()
if ni > 0 {
str += " of"
}
for i := 0; i < ni; i++ {
str += " " + FriendlyTypeName(nptyp.In(i))
if ni == 2 && i == 0 {
str += " and"
} else if i == ni-2 {
str += ", and"
} else if i < ni-1 {
str += ","
}
}
return str
}
if nptyp.String() == "interface {}" {
return "Value"
}
return nptyp.String()
}
// FriendlyStructLabel returns a user-friendly label for the given struct value.
func FriendlyStructLabel(v reflect.Value) string {
npv := reflectx.NonPointerValue(v)
if !v.IsValid() || v.IsZero() {
return "None"
}
opv := reflectx.UnderlyingPointer(v)
if lbler, ok := opv.Interface().(Labeler); ok {
return lbler.Label()
}
return FriendlyTypeName(npv.Type())
}
// FriendlySliceLabel returns a user-friendly label for the given slice value.
func FriendlySliceLabel(v reflect.Value) string {
uv := reflectx.Underlying(v)
label := ""
if !uv.IsValid() {
label = "None"
} else {
if uv.Kind() == reflect.Array || !uv.IsNil() {
bnm := FriendlyTypeName(reflectx.SliceElementType(v.Interface()))
if strings.HasSuffix(bnm, "s") {
label = strcase.ToSentence(fmt.Sprintf("%d lists of %s", uv.Len(), bnm))
} else {
label = strcase.ToSentence(fmt.Sprintf("%d %ss", uv.Len(), bnm))
}
} else {
label = "None"
}
}
return label
}
// FriendlyMapLabel returns a user-friendly label for the given map value.
func FriendlyMapLabel(v reflect.Value) string {
uv := reflectx.Underlying(v)
mpi := v.Interface()
label := ""
if !uv.IsValid() || uv.IsNil() {
label = "None"
} else {
bnm := FriendlyTypeName(reflectx.MapValueType(mpi))
if strings.HasSuffix(bnm, "s") {
label = strcase.ToSentence(fmt.Sprintf("%d lists of %s", uv.Len(), bnm))
} else {
label = strcase.ToSentence(fmt.Sprintf("%d %ss", uv.Len(), bnm))
}
}
return label
}
// Copyright (c) 2024, 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 labels
import (
"cogentcore.org/core/base/reflectx"
)
// Labeler interface provides a GUI-appropriate label for an item,
// via a Label string method. See [ToLabel] and [ToLabeler].
type Labeler interface {
// Label returns a GUI-appropriate label for item
Label() string
}
// ToLabel returns the GUI-appropriate label for an item, using the Labeler
// interface if it is defined, and falling back on [reflectx.ToString] converter
// otherwise.
func ToLabel(v any) string {
if lb, ok := v.(Labeler); ok {
return lb.Label()
}
return reflectx.ToString(v)
}
// ToLabeler returns the Labeler label, true if it was defined, else "", false
func ToLabeler(v any) (string, bool) {
if lb, ok := v.(Labeler); ok {
return lb.Label(), true
}
return "", false
}
// SliceLabeler interface provides a GUI-appropriate label
// for a slice item, given an index into the slice.
type SliceLabeler interface {
// ElemLabel returns a GUI-appropriate label for slice element at given index.
ElemLabel(idx int) string
}
// 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 logx
import (
"image/color"
"log/slog"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/matcolor"
"github.com/muesli/termenv"
)
var (
// UseColor is whether to use color in log messages. It is on by default.
UseColor = true
// ColorSchemeIsDark is whether the color scheme of the current terminal is dark-themed.
// Its primary use is in [ColorScheme], and it should typically only be accessed via that.
ColorSchemeIsDark = true
)
// ColorScheme returns the appropriate appropriate color scheme
// for terminal colors. It should be used instead of [colors.Scheme]
// for terminal colors because the theme (dark vs light) of the terminal
// could be different than that of the main app.
func ColorScheme() *matcolor.Scheme {
if ColorSchemeIsDark {
return &colors.Schemes.Dark
}
return &colors.Schemes.Light
}
// colorProfile is the termenv color profile, stored globally for convenience.
// It is set by [SetDefaultLogger] to [termenv.ColorProfile] if [UseColor] is true.
var colorProfile termenv.Profile
// InitColor sets up the terminal environment for color output. It is called automatically
// in an init function if UseColor is set to true. However, if you call a system command
// (ls, cp, etc), you need to call this function again.
func InitColor() {
restoreFunc, err := termenv.EnableVirtualTerminalProcessing(termenv.DefaultOutput())
if err != nil {
slog.Warn("error enabling virtual terminal processing for colored output on Windows: %w", err)
}
_ = restoreFunc // TODO: figure out how to call this at the end of the program
colorProfile = termenv.ColorProfile()
ColorSchemeIsDark = termenv.HasDarkBackground()
}
// ApplyColor applies the given color to the given string
// and returns the resulting string. If [UseColor] is set
// to false, it just returns the string it was passed.
func ApplyColor(clr color.Color, str string) string {
if !UseColor {
return str
}
return termenv.String(str).Foreground(colorProfile.FromColor(clr)).String()
}
// LevelColor applies the color associated with the given level to the
// given string and returns the resulting string. If [UseColor] is set
// to false, it just returns the string it was passed.
func LevelColor(level slog.Level, str string) string {
var clr color.RGBA
switch level {
case slog.LevelDebug:
return DebugColor(str)
case slog.LevelInfo:
return InfoColor(str)
case slog.LevelWarn:
return WarnColor(str)
case slog.LevelError:
return ErrorColor(str)
}
return ApplyColor(clr, str)
}
// DebugColor applies the color associated with the debug level to
// the given string and returns the resulting string. If [UseColor] is set
// to false, it just returns the string it was passed.
func DebugColor(str string) string {
return ApplyColor(colors.ToUniform(ColorScheme().Tertiary.Base), str)
}
// InfoColor applies the color associated with the info level to
// the given string and returns the resulting string. Because the
// color associated with the info level is just white/black, it just
// returns the given string, but it exists for API consistency.
func InfoColor(str string) string {
return str
}
// WarnColor applies the color associated with the warn level to
// the given string and returns the resulting string. If [UseColor] is set
// to false, it just returns the string it was passed.
func WarnColor(str string) string {
return ApplyColor(colors.ToUniform(ColorScheme().Warn.Base), str)
}
// ErrorColor applies the color associated with the error level to
// the given string and returns the resulting string. If [UseColor] is set
// to false, it just returns the string it was passed.
func ErrorColor(str string) string {
return ApplyColor(colors.ToUniform(ColorScheme().Error.Base), str)
}
// SuccessColor applies the color associated with success to the
// given string and returns the resulting string. If [UseColor] is set
// to false, it just returns the string it was passed.
func SuccessColor(str string) string {
return ApplyColor(colors.ToUniform(ColorScheme().Success.Base), str)
}
// CmdColor applies the color associated with terminal commands and arguments
// to the given string and returns the resulting string. If [UseColor] is set
// to false, it just returns the string it was passed.
func CmdColor(str string) string {
return ApplyColor(colors.ToUniform(ColorScheme().Primary.Base), str)
}
// TitleColor applies the color associated with titles and section headers
// to the given string and returns the resulting string. If [UseColor] is set
// to false, it just returns the string it was passed.
func TitleColor(str string) string {
return ApplyColor(colors.ToUniform(ColorScheme().Warn.Base), str)
}
// 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 logx
import "log/slog"
// UserLevel is the verbosity [slog.Level] that the user has selected for
// what logging and printing messages should be shown. Messages at
// levels at or above this level will be shown. It should typically
// be set through exec to the end user's preference. The default user
// verbosity level is [slog.LevelInfo]. If the build tag "debug" is
// specified, it is [slog.LevelDebug]. If the build tag "release" is
// specified, it is [slog.levelWarn]. Any updates to this value will
// be automatically reflected in the behavior of the logx default logger.
var UserLevel = defaultUserLevel
// LevelFromFlags returns the [slog.Level] object corresponding to the given
// user flag options. The flags correspond to the following values:
// - vv: [slog.LevelDebug]
// - v: [slog.LevelInfo]
// - q: [slog.LevelError]
// - (default: [slog.LevelWarn])
// The flags are evaluated in that order, so, for example, if both
// vv and q are specified, it will still return [Debug].
func LevelFromFlags(vv, v, q bool) slog.Level {
switch {
case vv:
return slog.LevelDebug
case v:
return slog.LevelInfo
case q:
return slog.LevelError
default:
return slog.LevelWarn
}
}
// 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.
// This code is based on https://github.com/jba/slog/blob/main/handlers/loghandler/log_handler.go
// Copyright (c) 2022, Jonathan Amsterdam. All rights reserved. (BSD 3-Clause License)
// Package logx implements structured log handling and provides
// global log and print verbosity and color options.
package logx
import (
"context"
"fmt"
"io"
"log/slog"
"os"
"runtime"
"strconv"
"sync"
)
// Handler is a [slog.Handler] whose output resembles that of [log.Logger].
// Use [NewHandler] to make a new [Handler] from a writer and options.
type Handler struct {
Opts slog.HandlerOptions
Prefix string // preformatted group names followed by a dot
Preformat string // preformatted Attrs, with an initial space
Mu sync.Mutex
W io.Writer
}
var _ slog.Handler = &Handler{}
// SetDefaultLogger sets the default logger to be a [Handler] with the
// level set to track [UserLevel]. It is called on program start
// automatically, so it should not need to be called by end user code
// in almost all circumstances.
func SetDefaultLogger() {
slog.SetDefault(slog.New(NewHandler(os.Stderr, &slog.HandlerOptions{
Level: &UserLevel,
})))
if UseColor {
InitColor()
}
}
func init() {
SetDefaultLogger()
}
// NewHandler makes a new [Handler] for the given writer with the given options.
func NewHandler(w io.Writer, opts *slog.HandlerOptions) *Handler {
h := &Handler{W: w}
if opts != nil {
h.Opts = *opts
}
if h.Opts.ReplaceAttr == nil {
h.Opts.ReplaceAttr = func(_ []string, a slog.Attr) slog.Attr { return a }
}
return h
}
// Enabled returns whether the handler should log a mesage with the given
// level in the given context.
func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool {
minLevel := slog.LevelInfo
if h.Opts.Level != nil {
minLevel = h.Opts.Level.Level()
}
return level >= minLevel
}
func (h *Handler) WithGroup(name string) slog.Handler {
return &Handler{
W: h.W,
Opts: h.Opts,
Preformat: h.Preformat,
Prefix: h.Prefix + name + ".",
}
}
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
var buf []byte
for _, a := range attrs {
buf = h.AppendAttr(buf, h.Prefix, a)
}
return &Handler{
W: h.W,
Opts: h.Opts,
Prefix: h.Prefix,
Preformat: h.Preformat + string(buf),
}
}
func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
var buf []byte
if !r.Time.IsZero() {
buf = r.Time.AppendFormat(buf, "2006/01/02 15:04:05")
buf = append(buf, ' ')
}
buf = append(buf, r.Level.String()...)
buf = append(buf, ' ')
if h.Opts.AddSource && r.PC != 0 {
fs := runtime.CallersFrames([]uintptr{r.PC})
f, _ := fs.Next()
buf = append(buf, f.File...)
buf = append(buf, ':')
buf = strconv.AppendInt(buf, int64(f.Line), 10)
buf = append(buf, ' ')
}
buf = append(buf, r.Message...)
buf = append(buf, h.Preformat...)
r.Attrs(func(a slog.Attr) bool {
buf = h.AppendAttr(buf, h.Prefix, a)
return true
})
buf = append(buf, '\n')
h.Mu.Lock()
defer h.Mu.Unlock()
if UseColor {
_, err := h.W.Write([]byte(LevelColor(r.Level, string(buf))))
return err
}
_, err := h.W.Write(buf)
return err
}
func (h *Handler) AppendAttr(buf []byte, prefix string, a slog.Attr) []byte {
if a.Equal(slog.Attr{}) {
return buf
}
if a.Value.Kind() != slog.KindGroup {
buf = append(buf, ' ')
buf = append(buf, prefix...)
buf = append(buf, a.Key...)
buf = append(buf, '=')
return fmt.Appendf(buf, "%v", a.Value.Any())
}
// Group
if a.Key != "" {
prefix += a.Key + "."
}
for _, a := range a.Value.Group() {
buf = h.AppendAttr(buf, prefix, a)
}
return buf
}
// 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 logx
import (
"fmt"
"log/slog"
)
// Print is equivalent to [fmt.Print], but with color based on the given level.
// Also, if [UserLevel] is above the given level, it does not print anything.
func Print(level slog.Level, a ...any) (n int, err error) {
if UserLevel > level {
return 0, nil
}
return fmt.Print(LevelColor(level, fmt.Sprint(a...)))
}
// PrintDebug is equivalent to [Print] with level [slog.LevelDebug].
func PrintDebug(a ...any) (n int, err error) {
return Print(slog.LevelDebug, a...)
}
// PrintInfo is equivalent to [Print] with level [slog.LevelInfo].
func PrintInfo(a ...any) (n int, err error) {
return Print(slog.LevelInfo, a...)
}
// PrintWarn is equivalent to [Print] with level [slog.LevelWarn].
func PrintWarn(a ...any) (n int, err error) {
return Print(slog.LevelWarn, a...)
}
// PrintError is equivalent to [Print] with level [slog.LevelError].
func PrintError(a ...any) (n int, err error) {
return Print(slog.LevelError, a...)
}
// Println is equivalent to [fmt.Println], but with color based on the given level.
// Also, if [UserLevel] is above the given level, it does not print anything.
func Println(level slog.Level, a ...any) (n int, err error) {
if UserLevel > level {
return 0, nil
}
return fmt.Println(LevelColor(level, fmt.Sprint(a...)))
}
// PrintlnDebug is equivalent to [Println] with level [slog.LevelDebug].
func PrintlnDebug(a ...any) (n int, err error) {
return Println(slog.LevelDebug, a...)
}
// PrintlnInfo is equivalent to [Println] with level [slog.LevelInfo].
func PrintlnInfo(a ...any) (n int, err error) {
return Println(slog.LevelInfo, a...)
}
// PrintlnWarn is equivalent to [Println] with level [slog.LevelWarn].
func PrintlnWarn(a ...any) (n int, err error) {
return Println(slog.LevelWarn, a...)
}
// PrintlnError is equivalent to [Println] with level [slog.LevelError].
func PrintlnError(a ...any) (n int, err error) {
return Println(slog.LevelError, a...)
}
// Printf is equivalent to [fmt.Printf], but with color based on the given level.
// Also, if [UserLevel] is above the given level, it does not print anything.
func Printf(level slog.Level, format string, a ...any) (n int, err error) {
if UserLevel > level {
return 0, nil
}
return fmt.Println(LevelColor(level, fmt.Sprintf(format, a...)))
}
// PrintfDebug is equivalent to [Printf] with level [slog.LevelDebug].
func PrintfDebug(format string, a ...any) (n int, err error) {
return Printf(slog.LevelDebug, format, a...)
}
// PrintfInfo is equivalent to [Printf] with level [slog.LevelInfo].
func PrintfInfo(format string, a ...any) (n int, err error) {
return Printf(slog.LevelInfo, format, a...)
}
// PrintfWarn is equivalent to [Printf] with level [slog.LevelWarn].
func PrintfWarn(format string, a ...any) (n int, err error) {
return Printf(slog.LevelWarn, format, a...)
}
// PrintfError is equivalent to [Printf] with level [slog.LevelError].
func PrintfError(format string, a ...any) (n int, err error) {
return Printf(slog.LevelError, format, a...)
}
// Copyright (c) 2024, 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.
// Based on https://github.com/laher/mergefs
// Copyright (c) 2021 Amir Laher under the MIT License
// Package mergefs provides support for merging multiple filesystems together.
package mergefs
import (
"errors"
"io/fs"
"os"
)
// Merge merges the given filesystems together,
func Merge(filesystems ...fs.FS) fs.FS {
return MergedFS{filesystems: filesystems}
}
// MergedFS combines filesystems. Each filesystem can serve different paths.
// The first FS takes precedence
type MergedFS struct {
filesystems []fs.FS
}
// Open opens the named file.
func (mfs MergedFS) Open(name string) (fs.File, error) {
for _, fs := range mfs.filesystems {
file, err := fs.Open(name)
if err == nil { // TODO should we return early when it's not an os.ErrNotExist? Should we offer options to decide this behaviour?
return file, nil
}
}
return nil, os.ErrNotExist
}
// ReadDir reads from the directory, and produces a DirEntry array of different
// directories.
//
// It iterates through all different filesystems that exist in the mfs MergeFS
// filesystem slice and it identifies overlapping directories that exist in different
// filesystems
func (mfs MergedFS) ReadDir(name string) ([]fs.DirEntry, error) {
dirsMap := make(map[string]fs.DirEntry)
notExistCount := 0
for _, filesystem := range mfs.filesystems {
dir, err := fs.ReadDir(filesystem, name)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
notExistCount++
continue
}
return nil, err
}
for _, v := range dir {
if _, ok := dirsMap[v.Name()]; !ok {
dirsMap[v.Name()] = v
}
}
continue
}
if len(mfs.filesystems) == notExistCount {
return nil, fs.ErrNotExist
}
dirs := make([]fs.DirEntry, 0, len(dirsMap))
for _, value := range dirsMap {
dirs = append(dirs, value)
}
return dirs, nil
}
// Copyright (c) 2024, 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 metadata
import (
"fmt"
"maps"
)
// Data is metadata as a map of named any elements
// with generic support for type-safe Get and nil-safe Set.
type Data map[string]any
func (md *Data) init() {
if *md == nil {
*md = make(map[string]any)
}
}
// Set sets key to given value, ensuring that
// the map is created if not previously.
func (md *Data) Set(key string, value any) {
md.init()
(*md)[key] = value
}
// Get gets metadata value of given type.
// returns error if not present or item is a different type.
func Get[T any](md Data, key string) (T, error) {
var z T
x, ok := md[key]
if !ok {
return z, fmt.Errorf("key %q not found in metadata", key)
}
v, ok := x.(T)
if !ok {
return z, fmt.Errorf("key %q has a different type than expected %T: is %T", key, z, x)
}
return v, nil
}
// Copy does a shallow copy of metadata from source.
// Any pointer-based values will still point to the same
// underlying data as the source, but the two maps remain
// distinct. It uses [maps.Copy].
func (md *Data) Copy(src Data) {
if src == nil {
return
}
md.init()
maps.Copy(*md, src)
}
// Code generated by dummy.gen.go.tmpl. DO NOT EDIT.
// Copyright (c) 2020, Cogent Core. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !mpi && !mpich
package mpi
// SendF64 sends values to toProc, using given unique tag identifier.
// This is Blocking. Must have a corresponding Recv call with same tag on toProc, from this proc
func (cm *Comm) SendF64(toProc int, tag int, vals []float64) error {
return nil
}
// Recv64F64 receives values from proc fmProc, using given unique tag identifier
// This is Blocking. Must have a corresponding Send call with same tag on fmProc, to this proc
func (cm *Comm) RecvF64(fmProc int, tag int, vals []float64) error {
return nil
}
// BcastF64 broadcasts slice from fmProc to all other procs.
// All nodes have the same vals after this call, copied from fmProc.
func (cm *Comm) BcastF64(fmProc int, vals []float64) error {
return nil
}
// ReduceF64 reduces all values across procs to toProc in orig to dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ReduceF64(toProc int, op Op, dest, orig []float64) error {
return nil
}
// AllReduceF64 reduces all values across procs to all procs from orig into dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllReduceF64(op Op, dest, orig []float64) error {
return nil
}
// GatherF64 gathers values from all procs into toProc proc, tiled into dest of size np * len(orig).
// This is inverse of Scatter.
// IMPORTANT: orig and dest must be different slices.
func (cm *Comm) GatherF64(toProc int, dest, orig []float64) error {
return nil
}
// AllGatherF64 gathers values from all procs into all procs,
// tiled by proc into dest of size np * len(orig).
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllGatherF64(dest, orig []float64) error {
return nil
}
// ScatterF64 scatters values from fmProc to all procs, distributing len(dest) size chunks to
// each proc from orig slice, which must be of size np * len(dest). This is inverse of Gather.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ScatterF64(fmProc int, dest, orig []float64) error {
return nil
}
// SendF32 sends values to toProc, using given unique tag identifier.
// This is Blocking. Must have a corresponding Recv call with same tag on toProc, from this proc
func (cm *Comm) SendF32(toProc int, tag int, vals []float32) error {
return nil
}
// Recv64F32 receives values from proc fmProc, using given unique tag identifier
// This is Blocking. Must have a corresponding Send call with same tag on fmProc, to this proc
func (cm *Comm) RecvF32(fmProc int, tag int, vals []float32) error {
return nil
}
// BcastF32 broadcasts slice from fmProc to all other procs.
// All nodes have the same vals after this call, copied from fmProc.
func (cm *Comm) BcastF32(fmProc int, vals []float32) error {
return nil
}
// ReduceF32 reduces all values across procs to toProc in orig to dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ReduceF32(toProc int, op Op, dest, orig []float32) error {
return nil
}
// AllReduceF32 reduces all values across procs to all procs from orig into dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllReduceF32(op Op, dest, orig []float32) error {
return nil
}
// GatherF32 gathers values from all procs into toProc proc, tiled into dest of size np * len(orig).
// This is inverse of Scatter.
// IMPORTANT: orig and dest must be different slices.
func (cm *Comm) GatherF32(toProc int, dest, orig []float32) error {
return nil
}
// AllGatherF32 gathers values from all procs into all procs,
// tiled by proc into dest of size np * len(orig).
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllGatherF32(dest, orig []float32) error {
return nil
}
// ScatterF32 scatters values from fmProc to all procs, distributing len(dest) size chunks to
// each proc from orig slice, which must be of size np * len(dest). This is inverse of Gather.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ScatterF32(fmProc int, dest, orig []float32) error {
return nil
}
// SendInt sends values to toProc, using given unique tag identifier.
// This is Blocking. Must have a corresponding Recv call with same tag on toProc, from this proc
func (cm *Comm) SendInt(toProc int, tag int, vals []int) error {
return nil
}
// Recv64Int receives values from proc fmProc, using given unique tag identifier
// This is Blocking. Must have a corresponding Send call with same tag on fmProc, to this proc
func (cm *Comm) RecvInt(fmProc int, tag int, vals []int) error {
return nil
}
// BcastInt broadcasts slice from fmProc to all other procs.
// All nodes have the same vals after this call, copied from fmProc.
func (cm *Comm) BcastInt(fmProc int, vals []int) error {
return nil
}
// ReduceInt reduces all values across procs to toProc in orig to dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ReduceInt(toProc int, op Op, dest, orig []int) error {
return nil
}
// AllReduceInt reduces all values across procs to all procs from orig into dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllReduceInt(op Op, dest, orig []int) error {
return nil
}
// GatherInt gathers values from all procs into toProc proc, tiled into dest of size np * len(orig).
// This is inverse of Scatter.
// IMPORTANT: orig and dest must be different slices.
func (cm *Comm) GatherInt(toProc int, dest, orig []int) error {
return nil
}
// AllGatherInt gathers values from all procs into all procs,
// tiled by proc into dest of size np * len(orig).
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllGatherInt(dest, orig []int) error {
return nil
}
// ScatterInt scatters values from fmProc to all procs, distributing len(dest) size chunks to
// each proc from orig slice, which must be of size np * len(dest). This is inverse of Gather.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ScatterInt(fmProc int, dest, orig []int) error {
return nil
}
// SendI64 sends values to toProc, using given unique tag identifier.
// This is Blocking. Must have a corresponding Recv call with same tag on toProc, from this proc
func (cm *Comm) SendI64(toProc int, tag int, vals []int64) error {
return nil
}
// Recv64I64 receives values from proc fmProc, using given unique tag identifier
// This is Blocking. Must have a corresponding Send call with same tag on fmProc, to this proc
func (cm *Comm) RecvI64(fmProc int, tag int, vals []int64) error {
return nil
}
// BcastI64 broadcasts slice from fmProc to all other procs.
// All nodes have the same vals after this call, copied from fmProc.
func (cm *Comm) BcastI64(fmProc int, vals []int64) error {
return nil
}
// ReduceI64 reduces all values across procs to toProc in orig to dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ReduceI64(toProc int, op Op, dest, orig []int64) error {
return nil
}
// AllReduceI64 reduces all values across procs to all procs from orig into dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllReduceI64(op Op, dest, orig []int64) error {
return nil
}
// GatherI64 gathers values from all procs into toProc proc, tiled into dest of size np * len(orig).
// This is inverse of Scatter.
// IMPORTANT: orig and dest must be different slices.
func (cm *Comm) GatherI64(toProc int, dest, orig []int64) error {
return nil
}
// AllGatherI64 gathers values from all procs into all procs,
// tiled by proc into dest of size np * len(orig).
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllGatherI64(dest, orig []int64) error {
return nil
}
// ScatterI64 scatters values from fmProc to all procs, distributing len(dest) size chunks to
// each proc from orig slice, which must be of size np * len(dest). This is inverse of Gather.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ScatterI64(fmProc int, dest, orig []int64) error {
return nil
}
// SendU64 sends values to toProc, using given unique tag identifier.
// This is Blocking. Must have a corresponding Recv call with same tag on toProc, from this proc
func (cm *Comm) SendU64(toProc int, tag int, vals []uint64) error {
return nil
}
// Recv64U64 receives values from proc fmProc, using given unique tag identifier
// This is Blocking. Must have a corresponding Send call with same tag on fmProc, to this proc
func (cm *Comm) RecvU64(fmProc int, tag int, vals []uint64) error {
return nil
}
// BcastU64 broadcasts slice from fmProc to all other procs.
// All nodes have the same vals after this call, copied from fmProc.
func (cm *Comm) BcastU64(fmProc int, vals []uint64) error {
return nil
}
// ReduceU64 reduces all values across procs to toProc in orig to dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ReduceU64(toProc int, op Op, dest, orig []uint64) error {
return nil
}
// AllReduceU64 reduces all values across procs to all procs from orig into dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllReduceU64(op Op, dest, orig []uint64) error {
return nil
}
// GatherU64 gathers values from all procs into toProc proc, tiled into dest of size np * len(orig).
// This is inverse of Scatter.
// IMPORTANT: orig and dest must be different slices.
func (cm *Comm) GatherU64(toProc int, dest, orig []uint64) error {
return nil
}
// AllGatherU64 gathers values from all procs into all procs,
// tiled by proc into dest of size np * len(orig).
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllGatherU64(dest, orig []uint64) error {
return nil
}
// ScatterU64 scatters values from fmProc to all procs, distributing len(dest) size chunks to
// each proc from orig slice, which must be of size np * len(dest). This is inverse of Gather.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ScatterU64(fmProc int, dest, orig []uint64) error {
return nil
}
// SendI32 sends values to toProc, using given unique tag identifier.
// This is Blocking. Must have a corresponding Recv call with same tag on toProc, from this proc
func (cm *Comm) SendI32(toProc int, tag int, vals []int32) error {
return nil
}
// Recv64I32 receives values from proc fmProc, using given unique tag identifier
// This is Blocking. Must have a corresponding Send call with same tag on fmProc, to this proc
func (cm *Comm) RecvI32(fmProc int, tag int, vals []int32) error {
return nil
}
// BcastI32 broadcasts slice from fmProc to all other procs.
// All nodes have the same vals after this call, copied from fmProc.
func (cm *Comm) BcastI32(fmProc int, vals []int32) error {
return nil
}
// ReduceI32 reduces all values across procs to toProc in orig to dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ReduceI32(toProc int, op Op, dest, orig []int32) error {
return nil
}
// AllReduceI32 reduces all values across procs to all procs from orig into dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllReduceI32(op Op, dest, orig []int32) error {
return nil
}
// GatherI32 gathers values from all procs into toProc proc, tiled into dest of size np * len(orig).
// This is inverse of Scatter.
// IMPORTANT: orig and dest must be different slices.
func (cm *Comm) GatherI32(toProc int, dest, orig []int32) error {
return nil
}
// AllGatherI32 gathers values from all procs into all procs,
// tiled by proc into dest of size np * len(orig).
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllGatherI32(dest, orig []int32) error {
return nil
}
// ScatterI32 scatters values from fmProc to all procs, distributing len(dest) size chunks to
// each proc from orig slice, which must be of size np * len(dest). This is inverse of Gather.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ScatterI32(fmProc int, dest, orig []int32) error {
return nil
}
// SendU32 sends values to toProc, using given unique tag identifier.
// This is Blocking. Must have a corresponding Recv call with same tag on toProc, from this proc
func (cm *Comm) SendU32(toProc int, tag int, vals []uint32) error {
return nil
}
// Recv64U32 receives values from proc fmProc, using given unique tag identifier
// This is Blocking. Must have a corresponding Send call with same tag on fmProc, to this proc
func (cm *Comm) RecvU32(fmProc int, tag int, vals []uint32) error {
return nil
}
// BcastU32 broadcasts slice from fmProc to all other procs.
// All nodes have the same vals after this call, copied from fmProc.
func (cm *Comm) BcastU32(fmProc int, vals []uint32) error {
return nil
}
// ReduceU32 reduces all values across procs to toProc in orig to dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ReduceU32(toProc int, op Op, dest, orig []uint32) error {
return nil
}
// AllReduceU32 reduces all values across procs to all procs from orig into dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllReduceU32(op Op, dest, orig []uint32) error {
return nil
}
// GatherU32 gathers values from all procs into toProc proc, tiled into dest of size np * len(orig).
// This is inverse of Scatter.
// IMPORTANT: orig and dest must be different slices.
func (cm *Comm) GatherU32(toProc int, dest, orig []uint32) error {
return nil
}
// AllGatherU32 gathers values from all procs into all procs,
// tiled by proc into dest of size np * len(orig).
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllGatherU32(dest, orig []uint32) error {
return nil
}
// ScatterU32 scatters values from fmProc to all procs, distributing len(dest) size chunks to
// each proc from orig slice, which must be of size np * len(dest). This is inverse of Gather.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ScatterU32(fmProc int, dest, orig []uint32) error {
return nil
}
// SendI16 sends values to toProc, using given unique tag identifier.
// This is Blocking. Must have a corresponding Recv call with same tag on toProc, from this proc
func (cm *Comm) SendI16(toProc int, tag int, vals []int16) error {
return nil
}
// Recv64I16 receives values from proc fmProc, using given unique tag identifier
// This is Blocking. Must have a corresponding Send call with same tag on fmProc, to this proc
func (cm *Comm) RecvI16(fmProc int, tag int, vals []int16) error {
return nil
}
// BcastI16 broadcasts slice from fmProc to all other procs.
// All nodes have the same vals after this call, copied from fmProc.
func (cm *Comm) BcastI16(fmProc int, vals []int16) error {
return nil
}
// ReduceI16 reduces all values across procs to toProc in orig to dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ReduceI16(toProc int, op Op, dest, orig []int16) error {
return nil
}
// AllReduceI16 reduces all values across procs to all procs from orig into dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllReduceI16(op Op, dest, orig []int16) error {
return nil
}
// GatherI16 gathers values from all procs into toProc proc, tiled into dest of size np * len(orig).
// This is inverse of Scatter.
// IMPORTANT: orig and dest must be different slices.
func (cm *Comm) GatherI16(toProc int, dest, orig []int16) error {
return nil
}
// AllGatherI16 gathers values from all procs into all procs,
// tiled by proc into dest of size np * len(orig).
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllGatherI16(dest, orig []int16) error {
return nil
}
// ScatterI16 scatters values from fmProc to all procs, distributing len(dest) size chunks to
// each proc from orig slice, which must be of size np * len(dest). This is inverse of Gather.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ScatterI16(fmProc int, dest, orig []int16) error {
return nil
}
// SendU16 sends values to toProc, using given unique tag identifier.
// This is Blocking. Must have a corresponding Recv call with same tag on toProc, from this proc
func (cm *Comm) SendU16(toProc int, tag int, vals []uint16) error {
return nil
}
// Recv64U16 receives values from proc fmProc, using given unique tag identifier
// This is Blocking. Must have a corresponding Send call with same tag on fmProc, to this proc
func (cm *Comm) RecvU16(fmProc int, tag int, vals []uint16) error {
return nil
}
// BcastU16 broadcasts slice from fmProc to all other procs.
// All nodes have the same vals after this call, copied from fmProc.
func (cm *Comm) BcastU16(fmProc int, vals []uint16) error {
return nil
}
// ReduceU16 reduces all values across procs to toProc in orig to dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ReduceU16(toProc int, op Op, dest, orig []uint16) error {
return nil
}
// AllReduceU16 reduces all values across procs to all procs from orig into dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllReduceU16(op Op, dest, orig []uint16) error {
return nil
}
// GatherU16 gathers values from all procs into toProc proc, tiled into dest of size np * len(orig).
// This is inverse of Scatter.
// IMPORTANT: orig and dest must be different slices.
func (cm *Comm) GatherU16(toProc int, dest, orig []uint16) error {
return nil
}
// AllGatherU16 gathers values from all procs into all procs,
// tiled by proc into dest of size np * len(orig).
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllGatherU16(dest, orig []uint16) error {
return nil
}
// ScatterU16 scatters values from fmProc to all procs, distributing len(dest) size chunks to
// each proc from orig slice, which must be of size np * len(dest). This is inverse of Gather.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ScatterU16(fmProc int, dest, orig []uint16) error {
return nil
}
// SendI8 sends values to toProc, using given unique tag identifier.
// This is Blocking. Must have a corresponding Recv call with same tag on toProc, from this proc
func (cm *Comm) SendI8(toProc int, tag int, vals []int8) error {
return nil
}
// Recv64I8 receives values from proc fmProc, using given unique tag identifier
// This is Blocking. Must have a corresponding Send call with same tag on fmProc, to this proc
func (cm *Comm) RecvI8(fmProc int, tag int, vals []int8) error {
return nil
}
// BcastI8 broadcasts slice from fmProc to all other procs.
// All nodes have the same vals after this call, copied from fmProc.
func (cm *Comm) BcastI8(fmProc int, vals []int8) error {
return nil
}
// ReduceI8 reduces all values across procs to toProc in orig to dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ReduceI8(toProc int, op Op, dest, orig []int8) error {
return nil
}
// AllReduceI8 reduces all values across procs to all procs from orig into dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllReduceI8(op Op, dest, orig []int8) error {
return nil
}
// GatherI8 gathers values from all procs into toProc proc, tiled into dest of size np * len(orig).
// This is inverse of Scatter.
// IMPORTANT: orig and dest must be different slices.
func (cm *Comm) GatherI8(toProc int, dest, orig []int8) error {
return nil
}
// AllGatherI8 gathers values from all procs into all procs,
// tiled by proc into dest of size np * len(orig).
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllGatherI8(dest, orig []int8) error {
return nil
}
// ScatterI8 scatters values from fmProc to all procs, distributing len(dest) size chunks to
// each proc from orig slice, which must be of size np * len(dest). This is inverse of Gather.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ScatterI8(fmProc int, dest, orig []int8) error {
return nil
}
// SendU8 sends values to toProc, using given unique tag identifier.
// This is Blocking. Must have a corresponding Recv call with same tag on toProc, from this proc
func (cm *Comm) SendU8(toProc int, tag int, vals []uint8) error {
return nil
}
// Recv64U8 receives values from proc fmProc, using given unique tag identifier
// This is Blocking. Must have a corresponding Send call with same tag on fmProc, to this proc
func (cm *Comm) RecvU8(fmProc int, tag int, vals []uint8) error {
return nil
}
// BcastU8 broadcasts slice from fmProc to all other procs.
// All nodes have the same vals after this call, copied from fmProc.
func (cm *Comm) BcastU8(fmProc int, vals []uint8) error {
return nil
}
// ReduceU8 reduces all values across procs to toProc in orig to dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ReduceU8(toProc int, op Op, dest, orig []uint8) error {
return nil
}
// AllReduceU8 reduces all values across procs to all procs from orig into dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllReduceU8(op Op, dest, orig []uint8) error {
return nil
}
// GatherU8 gathers values from all procs into toProc proc, tiled into dest of size np * len(orig).
// This is inverse of Scatter.
// IMPORTANT: orig and dest must be different slices.
func (cm *Comm) GatherU8(toProc int, dest, orig []uint8) error {
return nil
}
// AllGatherU8 gathers values from all procs into all procs,
// tiled by proc into dest of size np * len(orig).
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllGatherU8(dest, orig []uint8) error {
return nil
}
// ScatterU8 scatters values from fmProc to all procs, distributing len(dest) size chunks to
// each proc from orig slice, which must be of size np * len(dest). This is inverse of Gather.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ScatterU8(fmProc int, dest, orig []uint8) error {
return nil
}
// SendC128 sends values to toProc, using given unique tag identifier.
// This is Blocking. Must have a corresponding Recv call with same tag on toProc, from this proc
func (cm *Comm) SendC128(toProc int, tag int, vals []complex128) error {
return nil
}
// Recv64C128 receives values from proc fmProc, using given unique tag identifier
// This is Blocking. Must have a corresponding Send call with same tag on fmProc, to this proc
func (cm *Comm) RecvC128(fmProc int, tag int, vals []complex128) error {
return nil
}
// BcastC128 broadcasts slice from fmProc to all other procs.
// All nodes have the same vals after this call, copied from fmProc.
func (cm *Comm) BcastC128(fmProc int, vals []complex128) error {
return nil
}
// ReduceC128 reduces all values across procs to toProc in orig to dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ReduceC128(toProc int, op Op, dest, orig []complex128) error {
return nil
}
// AllReduceC128 reduces all values across procs to all procs from orig into dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllReduceC128(op Op, dest, orig []complex128) error {
return nil
}
// GatherC128 gathers values from all procs into toProc proc, tiled into dest of size np * len(orig).
// This is inverse of Scatter.
// IMPORTANT: orig and dest must be different slices.
func (cm *Comm) GatherC128(toProc int, dest, orig []complex128) error {
return nil
}
// AllGatherC128 gathers values from all procs into all procs,
// tiled by proc into dest of size np * len(orig).
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllGatherC128(dest, orig []complex128) error {
return nil
}
// ScatterC128 scatters values from fmProc to all procs, distributing len(dest) size chunks to
// each proc from orig slice, which must be of size np * len(dest). This is inverse of Gather.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ScatterC128(fmProc int, dest, orig []complex128) error {
return nil
}
// SendC64 sends values to toProc, using given unique tag identifier.
// This is Blocking. Must have a corresponding Recv call with same tag on toProc, from this proc
func (cm *Comm) SendC64(toProc int, tag int, vals []complex64) error {
return nil
}
// Recv64C64 receives values from proc fmProc, using given unique tag identifier
// This is Blocking. Must have a corresponding Send call with same tag on fmProc, to this proc
func (cm *Comm) RecvC64(fmProc int, tag int, vals []complex64) error {
return nil
}
// BcastC64 broadcasts slice from fmProc to all other procs.
// All nodes have the same vals after this call, copied from fmProc.
func (cm *Comm) BcastC64(fmProc int, vals []complex64) error {
return nil
}
// ReduceC64 reduces all values across procs to toProc in orig to dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ReduceC64(toProc int, op Op, dest, orig []complex64) error {
return nil
}
// AllReduceC64 reduces all values across procs to all procs from orig into dest using given operation.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllReduceC64(op Op, dest, orig []complex64) error {
return nil
}
// GatherC64 gathers values from all procs into toProc proc, tiled into dest of size np * len(orig).
// This is inverse of Scatter.
// IMPORTANT: orig and dest must be different slices.
func (cm *Comm) GatherC64(toProc int, dest, orig []complex64) error {
return nil
}
// AllGatherC64 gathers values from all procs into all procs,
// tiled by proc into dest of size np * len(orig).
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) AllGatherC64(dest, orig []complex64) error {
return nil
}
// ScatterC64 scatters values from fmProc to all procs, distributing len(dest) size chunks to
// each proc from orig slice, which must be of size np * len(dest). This is inverse of Gather.
// IMPORTANT: orig and dest must be different slices
func (cm *Comm) ScatterC64(fmProc int, dest, orig []complex64) error {
return nil
}
// Copyright (c) 2020, Cogent Core. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !mpi && !mpich
package mpi
// this file provides dummy versions, built by default, so mpi can be included
// generically without incurring additional complexity.
// set LogErrors to control whether MPI errors are automatically logged or not
var LogErrors = true
// Op is an aggregation operation: Sum, Min, Max, etc
type Op int
const (
OpSum Op = iota
OpMax
OpMin
OpProd
OpLAND // logical AND
OpLOR // logical OR
OpBAND // bitwise AND
OpBOR // bitwise OR
)
const (
// Root is the rank 0 node -- it is more semantic to use this
Root int = 0
)
// IsOn tells whether MPI is on or not
//
// NOTE: this returns true even after Stop
func IsOn() bool {
return false
}
// Init initialises MPI
func Init() {
}
// InitThreadSafe initialises MPI thread safe
func InitThreadSafe() error {
return nil
}
// Finalize finalises MPI (frees resources, shuts it down)
func Finalize() {
}
// WorldRank returns this proc's rank/ID within the World communicator.
// Returns 0 if not yet initialized, so it is always safe to call.
func WorldRank() (rank int) {
return 0
}
// WorldSize returns the number of procs in the World communicator.
// Returns 1 if not yet initialized, so it is always safe to call.
func WorldSize() (size int) {
return 1
}
// Comm is the MPI communicator -- all MPI communication operates as methods
// on this struct. It holds the MPI_Comm communicator and MPI_Group for
// sub-World group communication.
type Comm struct {
}
// NewComm creates a new communicator.
// if ranks is nil, communicator is for World (all active procs).
// otherwise, defined a group-level commuicator for given ranks.
func NewComm(ranks []int) (*Comm, error) {
cm := &Comm{}
return cm, nil
}
// Rank returns the rank/ID for this proc
func (cm *Comm) Rank() (rank int) {
return 0
}
// Size returns the number of procs in this communicator
func (cm *Comm) Size() (size int) {
return 1
}
// Abort aborts MPI
func (cm *Comm) Abort() error {
return nil
}
// Barrier forces synchronisation
func (cm *Comm) Barrier() error {
return nil
}
// Copyright (c) 2020, 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 mpi
import "fmt"
// PrintAllProcs causes mpi.Printf to print on all processors -- otherwise just 0
var PrintAllProcs = false
// Printf does fmt.Printf only on the 0 rank node (see also AllPrintf to do all)
// and PrintAllProcs var to override for debugging, and print all
func Printf(fs string, pars ...any) {
if !PrintAllProcs && WorldRank() > 0 {
return
}
if WorldRank() > 0 {
AllPrintf(fs, pars...)
} else {
fmt.Printf(fs, pars...)
}
}
// AllPrintf does fmt.Printf on all nodes, with node rank printed first
// This is best for debugging MPI itself.
func AllPrintf(fs string, pars ...any) {
fs = fmt.Sprintf("P%d: ", WorldRank()) + fs
fmt.Printf(fs, pars...)
}
// Println does fmt.Println only on the 0 rank node (see also AllPrintln to do all)
// and PrintAllProcs var to override for debugging, and print all
func Println(fs ...any) {
if !PrintAllProcs && WorldRank() > 0 {
return
}
if WorldRank() > 0 {
AllPrintln(fs...)
} else {
fmt.Println(fs...)
}
}
// AllPrintln does fmt.Println on all nodes, with node rank printed first
// This is best for debugging MPI itself.
func AllPrintln(fs ...any) {
fsa := make([]any, len(fs))
copy(fsa[1:], fs)
fsa[0] = fmt.Sprintf("P%d: ", WorldRank())
fmt.Println(fsa...)
}
// 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 nptime provides a non-pointer version of the time.Time struct
that does not have the location pointer information that time.Time has,
which is more efficient from a memory management perspective, in cases
where you have a lot of time values being kept: https://segment.com/blog/allocation-efficiency-in-high-performance-go-services/
*/
package nptime
import "time"
// Time represents the value of time.Time without using any pointers for the
// location information, so it is more memory efficient when lots of time
// values are being stored.
type Time struct {
// [time.Time.Unix] seconds since 1970
Sec int64
// [time.Time.Nanosecond]; nanosecond offset within second, *not* UnixNano
NSec uint32
}
// IsZero returns true if the time is zero and has not been initialized.
func (t Time) IsZero() bool {
return t == Time{}
}
// Time returns the [time.Time] value for this [Time] value.
func (t Time) Time() time.Time {
return time.Unix(t.Sec, int64(t.NSec))
}
// SetTime sets the [Time] value based on the [time.Time]. value
func (t *Time) SetTime(tt time.Time) {
t.Sec = tt.Unix()
t.NSec = uint32(tt.Nanosecond())
}
// Now sets the time value to [time.Now].
func (t *Time) Now() {
t.SetTime(time.Now())
}
// 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 num
// Abs returns the absolute value of the given value.
func Abs[T Signed | Float](x T) T {
if x < 0 {
return -x
}
return x
}
// 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 num
// see: https://github.com/golang/go/issues/61915
// ToBool returns a bool true if the given number is not zero,
// and false if it is zero, providing a direct way to convert
// numbers to bools as is done automatically in C and other languages.
func ToBool[T Number](v T) bool {
return v != 0
}
// FromBool returns a 1 if the bool is true and a 0 for false.
// Typically the type parameter cannot be inferred and must be provided.
// See SetFromBool for a version that uses a pointer to the destination
// which avoids the need to specify the type parameter.
func FromBool[T Number](v bool) T {
if v {
return 1
}
return 0
}
// SetFromBool converts a bool into a number, using generics,
// setting the pointer to the dst destination value to a 1 if bool is true,
// and 0 otherwise.
// This version of FromBool does not require type parameters typically.
func SetFromBool[T Number](dst *T, b bool) {
if b {
*dst = 1
}
*dst = 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 option provides optional (nullable) types.
package option
// Option represents an optional (nullable) type. If Valid is true, Option
// represents Value. Otherwise, it represents a null/unset/invalid value.
type Option[T any] struct {
Valid bool `label:"Set"`
Value T
}
// New returns a new [Option] set to the given value.
func New[T any](v T) *Option[T] {
o := &Option[T]{}
o.Set(v)
return o
}
// Set sets the value to the given value.
func (o *Option[T]) Set(v T) *Option[T] {
o.Value = v
o.Valid = true
return o
}
// Clear marks the value as null/unset/invalid.
func (o *Option[T]) Clear() *Option[T] {
o.Valid = false
return o
}
// Or returns the value of the option if it is not marked
// as null/unset/invalid, and otherwise it returns the given value.
func (o *Option[T]) Or(or T) T {
if o.Valid {
return o.Value
}
return or
}
func (o *Option[T]) ShouldSave() bool {
return o.Valid
}
func (o *Option[T]) ShouldDisplay(field string) bool {
switch field {
case "Value":
return o.Valid
}
return true
}
// Copyright (c) 2022, 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 ordmap implements an ordered map that retains the order of items
added to a slice, while also providing fast key-based map lookup of items,
using the Go 1.18 generics system.
The implementation is fully visible and the API provides a minimal
subset of methods, compared to other implementations that are heavier,
so that additional functionality can be added as needed.
The slice structure holds the Key and Value for items as they are added,
enabling direct updating of the corresponding map, which holds the
index into the slice. Adding and access are fast, while deleting
and inserting are relatively slow, requiring updating of the index map,
but these are already slow due to the slice updating.
*/
package ordmap
import (
"fmt"
"slices"
)
// KeyValue represents a key-value pair.
type KeyValue[K comparable, V any] struct {
Key K
Value V
}
// Map is a generic ordered map that combines the order of a slice
// and the fast key lookup of a map. A map stores an index
// into a slice that has the value and key associated with the value.
type Map[K comparable, V any] struct {
// Order is an ordered list of values and associated keys, in the order added.
Order []KeyValue[K, V]
// Map is the key to index mapping.
Map map[K]int `display:"-"`
}
// New returns a new ordered map.
func New[K comparable, V any]() *Map[K, V] {
return &Map[K, V]{
Map: make(map[K]int),
}
}
// Make constructs a new ordered map with the given key-value pairs
func Make[K comparable, V any](vals []KeyValue[K, V]) *Map[K, V] {
om := &Map[K, V]{
Order: vals,
Map: make(map[K]int, len(vals)),
}
for i, v := range om.Order {
om.Map[v.Key] = i
}
return om
}
// Init initializes the map if it isn't already.
func (om *Map[K, V]) Init() {
if om.Map == nil {
om.Map = make(map[K]int)
}
}
// Reset resets the map, removing any existing elements.
func (om *Map[K, V]) Reset() {
om.Map = nil
om.Order = nil
}
// Add adds a new value for given key.
// If key already exists in map, it replaces the item at that existing index,
// otherwise it is added to the end.
func (om *Map[K, V]) Add(key K, val V) {
om.Init()
if idx, has := om.Map[key]; has {
om.Map[key] = idx
om.Order[idx] = KeyValue[K, V]{Key: key, Value: val}
} else {
om.Map[key] = len(om.Order)
om.Order = append(om.Order, KeyValue[K, V]{Key: key, Value: val})
}
}
// ReplaceIndex replaces the value at the given index
// with the given new item with the given key.
func (om *Map[K, V]) ReplaceIndex(idx int, key K, val V) {
old := om.Order[idx]
if key != old.Key {
delete(om.Map, old.Key)
om.Map[key] = idx
}
om.Order[idx] = KeyValue[K, V]{Key: key, Value: val}
}
// InsertAtIndex inserts the given value with the given key at the given index.
// This is relatively slow because it needs to renumber the index map above
// the inserted value. It will panic if the key already exists because
// the behavior is undefined in that situation.
func (om *Map[K, V]) InsertAtIndex(idx int, key K, val V) {
if _, has := om.Map[key]; has {
panic("key already exists")
}
om.Init()
sz := len(om.Order)
for o := idx; o < sz; o++ {
om.Map[om.Order[o].Key] = o + 1
}
om.Map[key] = idx
om.Order = slices.Insert(om.Order, idx, KeyValue[K, V]{Key: key, Value: val})
}
// ValueByKey returns the value corresponding to the given key,
// with a zero value returned for a missing key. See [Map.ValueByKeyTry]
// for one that returns a bool for missing keys.
func (om *Map[K, V]) ValueByKey(key K) V {
idx, ok := om.Map[key]
if ok {
return om.Order[idx].Value
}
var zv V
return zv
}
// ValueByKeyTry returns the value corresponding to the given key,
// with false returned for a missing key.
func (om *Map[K, V]) ValueByKeyTry(key K) (V, bool) {
idx, ok := om.Map[key]
if ok {
return om.Order[idx].Value, ok
}
var zv V
return zv, false
}
// IndexIsValid returns an error if the given index is invalid
func (om *Map[K, V]) IndexIsValid(idx int) error {
if idx >= len(om.Order) || idx < 0 {
return fmt.Errorf("ordmap.Map: IndexIsValid: index %d is out of range of a map of length %d", idx, len(om.Order))
}
return nil
}
// IndexByKey returns the index of the given key, with a -1 for missing key.
// See [Map.IndexByKeyTry] for a version returning a bool for missing key.
func (om *Map[K, V]) IndexByKey(key K) int {
idx, ok := om.Map[key]
if !ok {
return -1
}
return idx
}
// IndexByKeyTry returns the index of the given key, with false for a missing key.
func (om *Map[K, V]) IndexByKeyTry(key K) (int, bool) {
idx, ok := om.Map[key]
return idx, ok
}
// ValueByIndex returns the value at the given index in the ordered slice.
func (om *Map[K, V]) ValueByIndex(idx int) V {
return om.Order[idx].Value
}
// KeyByIndex returns the key for the given index in the ordered slice.
func (om *Map[K, V]) KeyByIndex(idx int) K {
return om.Order[idx].Key
}
// Len returns the number of items in the map.
func (om *Map[K, V]) Len() int {
if om == nil {
return 0
}
return len(om.Order)
}
// DeleteIndex deletes item(s) within the index range [i:j].
// This is relatively slow because it needs to renumber the
// index map above the deleted range.
func (om *Map[K, V]) DeleteIndex(i, j int) {
sz := len(om.Order)
ndel := j - i
if ndel <= 0 {
panic("index range is <= 0")
}
for o := j; o < sz; o++ {
om.Map[om.Order[o].Key] = o - ndel
}
for o := i; o < j; o++ {
delete(om.Map, om.Order[o].Key)
}
om.Order = slices.Delete(om.Order, i, j)
}
// DeleteKey deletes the item with the given key, returning false if it does not find it.
func (om *Map[K, V]) DeleteKey(key K) bool {
idx, ok := om.Map[key]
if !ok {
return false
}
om.DeleteIndex(idx, idx+1)
return true
}
// Keys returns a slice of the keys in order.
func (om *Map[K, V]) Keys() []K {
kl := make([]K, om.Len())
for i, kv := range om.Order {
kl[i] = kv.Key
}
return kl
}
// Values returns a slice of the values in order.
func (om *Map[K, V]) Values() []V {
vl := make([]V, om.Len())
for i, kv := range om.Order {
vl[i] = kv.Value
}
return vl
}
// Copy copies all of the entries from the given ordered map
// into this ordered map. It keeps existing entries in this
// map unless they also exist in the given map, in which case
// they are overwritten.
func (om *Map[K, V]) Copy(from *Map[K, V]) {
for _, kv := range from.Order {
om.Add(kv.Key, kv.Value)
}
}
// String returns a string representation of the map.
func (om *Map[K, V]) String() string {
return fmt.Sprintf("%v", om.Order)
}
// GoString returns the map as Go code.
func (om *Map[K, V]) GoString() string {
var zk K
var zv V
res := fmt.Sprintf("ordmap.Make([]ordmap.KeyVal[%T, %T]{\n", zk, zv)
for _, kv := range om.Order {
res += fmt.Sprintf("{%#v, %#v},\n", kv.Key, kv.Value)
}
res += "})"
return res
}
// Copyright (c) 2024, 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 plan provides an efficient mechanism for updating a slice
// to contain a target list of elements, generating minimal edits to
// modify the current slice contents to match the target.
// The mechanism depends on the use of unique name string identifiers
// to determine whether an element is currently configured correctly.
// These could be algorithmically generated hash strings or any other
// such unique identifier.
package plan
import (
"slices"
"cogentcore.org/core/base/slicesx"
)
// Namer is an interface that types can implement to specify their name in a plan context.
type Namer interface {
// PlanName returns the name of the object in a plan context.
PlanName() string
}
// Update ensures that the elements of the given slice contain
// the elements according to the plan specified by the given arguments.
// The argument n specifies the total number of items in the target plan.
// The elements have unique names specified by the given name function.
// If a new item is needed, the given new function is called to create it
// for the given name at the given index position. After a new element is
// created, it is added to the slice, and if the given optional init function
// is non-nil, it is called with the new element and its index. If the
// given destroy function is not-nil, then it is called on any element
// that is being deleted from the slice. Update returns whether any changes
// were made. The given slice must be a pointer so that it can be modified
// live, which is required for init functions to run when the slice is
// correctly updated to the current state.
func Update[T Namer](s *[]T, n int, name func(i int) string, new func(name string, i int) T, init func(e T, i int), destroy func(e T)) bool {
changed := false
// first make a map for looking up the indexes of the target names
names := make([]string, n)
nmap := make(map[string]int, n)
smap := make(map[string]int, n)
for i := range n {
nm := name(i)
names[i] = nm
if _, has := nmap[nm]; has {
panic("plan.Update: duplicate name: " + nm) // no way to recover
}
nmap[nm] = i
}
// first remove anything we don't want
sn := len(*s)
for i := sn - 1; i >= 0; i-- {
nm := (*s)[i].PlanName()
if _, ok := nmap[nm]; !ok {
changed = true
if destroy != nil {
destroy((*s)[i])
}
*s = slices.Delete(*s, i, i+1)
}
smap[nm] = i
}
// next add and move items as needed; in order so guaranteed
for i, tn := range names {
ci := slicesx.Search(*s, func(e T) bool { return e.PlanName() == tn }, smap[tn])
if ci < 0 { // item not currently on the list
changed = true
ne := new(tn, i)
*s = slices.Insert(*s, i, ne)
if init != nil {
init(ne, i)
}
} else { // on the list; is it in the right place?
if ci != i {
changed = true
e := (*s)[ci]
*s = slices.Delete(*s, ci, ci+1)
*s = slices.Insert(*s, i, e)
}
}
}
return changed
}
// 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 profile provides basic but effective profiling of targeted
// functions or code sections, which can often be more informative than
// generic cpu profiling.
//
// Here's how you use it:
//
// // somewhere near start of program (e.g., using flag package)
// profileFlag := flag.Bool("profile", false, "turn on targeted profiling")
// ...
// flag.Parse()
// profile.Profiling = *profileFlag
// ...
// // surrounding the code of interest:
// pr := profile.Start()
// ... code
// pr.End()
// ...
// // at the end or whenever you've got enough data:
// profile.Report(time.Millisecond) // or time.Second or whatever
package profile
import (
"cmp"
"fmt"
"runtime"
"slices"
"strings"
"sync"
"time"
"cogentcore.org/core/base/errors"
)
// Main User API:
// Start starts profiling and returns a Profile struct that must have [Profile.End]
// called on it when done timing. It will be nil if not the first to start
// timing on this function; it assumes nested inner / outer loop structure for
// calls to the same method. It uses the short, package-qualified name of the
// calling function as the name of the profile struct. Extra information can be
// passed to Start, which will be added at the end of the name in a dash-delimited
// format. See [StartName] for a version that supports a custom name.
func Start(info ...string) *Profile {
if !Profiling {
return nil
}
name := ""
pc, _, _, ok := runtime.Caller(1)
if ok {
name = runtime.FuncForPC(pc).Name()
// get rid of everything before the package
if li := strings.LastIndex(name, "/"); li >= 0 {
name = name[li+1:]
}
} else {
err := "profile.Start: unexpected error: unable to get caller"
errors.Log(errors.New(err))
name = "!(" + err + ")"
}
if len(info) > 0 {
name += "-" + strings.Join(info, "-")
}
return TheProfiler.Start(name)
}
// StartName starts profiling and returns a Profile struct that must have
// [Profile.End] called on it when done timing. It will be nil if not the first
// to start timing on this function; it assumes nested inner / outer loop structure
// for calls to the same method. It uses the given name as the name of the profile
// struct. Extra information can be passed to StartName, which will be added at
// the end of the name in a dash-delimited format. See [Start] for a version that
// automatically determines the name from the name of the calling function.
func StartName(name string, info ...string) *Profile {
if len(info) > 0 {
name += "-" + strings.Join(info, "-")
}
return TheProfiler.Start(name)
}
// Report generates a report of all the profile data collected.
func Report(units time.Duration) {
TheProfiler.Report(units)
}
// Reset resets all of the profiling data.
func Reset() {
TheProfiler.Reset()
}
// Profiling is whether profiling is currently enabled.
var Profiling = false
// TheProfiler is the global instance of [Profiler].
var TheProfiler = Profiler{}
// Profile represents one profiled function.
type Profile struct {
Name string
Total time.Duration
N int64
Avg float64
St time.Time
Timing bool
}
func (p *Profile) Start() *Profile {
if !p.Timing {
p.St = time.Now()
p.Timing = true
return p
}
return nil
}
func (p *Profile) End() {
if p == nil || !Profiling {
return
}
dur := time.Since(p.St)
p.Total += dur
p.N++
p.Avg = float64(p.Total) / float64(p.N)
p.Timing = false
}
func (p *Profile) Report(tot float64, units time.Duration) {
us := strings.TrimPrefix(units.String(), "1")
fmt.Printf("%-60sTotal:%8.2f %s\tAvg:%6.2f\tN:%6d\tPct:%6.2f\n",
p.Name, float64(p.Total)/float64(units), us, p.Avg/float64(units), p.N, 100.0*float64(p.Total)/tot)
}
// Profiler manages a map of profiled functions.
type Profiler struct {
Profiles map[string]*Profile
mu sync.Mutex
}
// Start starts profiling and returns a Profile struct that must have .End()
// called on it when done timing
func (p *Profiler) Start(name string) *Profile {
if !Profiling {
return nil
}
p.mu.Lock()
if p.Profiles == nil {
p.Profiles = make(map[string]*Profile, 0)
}
pr, ok := p.Profiles[name]
if !ok {
pr = &Profile{Name: name}
p.Profiles[name] = pr
}
prval := pr.Start()
p.mu.Unlock()
return prval
}
// Report generates a report of all the profile data collected
func (p *Profiler) Report(units time.Duration) {
if !Profiling {
return
}
list := make([]*Profile, len(p.Profiles))
tot := 0.0
idx := 0
for _, pr := range p.Profiles {
tot += float64(pr.Total)
list[idx] = pr
idx++
}
slices.SortFunc(list, func(a, b *Profile) int {
return cmp.Compare(b.Total, a.Total)
})
for _, pr := range list {
pr.Report(tot, units)
}
}
func (p *Profiler) Reset() {
p.Profiles = make(map[string]*Profile, 0)
}
// Copyright (c) 2019, 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 randx
// BoolP is a simple method to generate a true value with given probability
// (else false). It is just rand.Float64() < p but this is more readable
// and explicit.
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func BoolP(p float64, randOpt ...Rand) bool {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
return rnd.Float64() < p
}
// BoolP32 is a simple method to generate a true value with given probability
// (else false). It is just rand.Float32() < p but this is more readable
// and explicit.
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func BoolP32(p float32, randOpt ...Rand) bool {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
return rnd.Float32() < p
}
// 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 randx
import (
"math"
)
// note: this file contains random distribution functions
// from gonum.org/v1/gonum/stat/distuv
// which we modified only to use the randx.Rand interface.
// BinomialGen returns binomial with n trials (par) each of probability p (var)
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func BinomialGen(n, p float64, randOpt ...Rand) float64 {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
// NUMERICAL RECIPES IN C: THE ART OF SCIENTIFIC COMPUTING (ISBN 0-521-43108-5)
// p. 295-6
// http://www.aip.de/groups/soe/local/numres/bookcpdf/c7-3.pdf
porg := p
if p > 0.5 {
p = 1 - p
}
am := n * p
if n < 25 {
// Use direct method.
bnl := 0.0
for i := 0; i < int(n); i++ {
if rnd.Float64() < p {
bnl++
}
}
if p != porg {
return n - bnl
}
return bnl
}
if am < 1 {
// Use rejection method with Poisson proposal.
const logM = 2.6e-2 // constant for rejection sampling (https://en.wikipedia.org/wiki/Rejection_sampling)
var bnl float64
z := -p
pclog := (1 + 0.5*z) * z / (1 + (1+1.0/6*z)*z) // Padé approximant of log(1 + x)
for {
bnl = 0.0
t := 0.0
for i := 0; i < int(n); i++ {
t += rnd.ExpFloat64()
if t >= am {
break
}
bnl++
}
bnlc := n - bnl
z = -bnl / n
log1p := (1 + 0.5*z) * z / (1 + (1+1.0/6*z)*z)
t = (bnlc+0.5)*log1p + bnl - bnlc*pclog + 1/(12*bnlc) - am + logM // Uses Stirling's expansion of log(n!)
if rnd.ExpFloat64() >= t {
break
}
}
if p != porg {
return n - bnl
}
return bnl
}
// Original algorithm samples from a Poisson distribution with the
// appropriate expected value. However, the Poisson approximation is
// asymptotic such that the absolute deviation in probability is O(1/n).
// Rejection sampling produces exact variates with at worst less than 3%
// rejection with miminal additional computation.
// Use rejection method with Cauchy proposal.
g, _ := math.Lgamma(n + 1)
plog := math.Log(p)
pclog := math.Log1p(-p)
sq := math.Sqrt(2 * am * (1 - p))
for {
var em, y float64
for {
y = math.Tan(math.Pi * rnd.Float64())
em = sq*y + am
if em >= 0 && em < n+1 {
break
}
}
em = math.Floor(em)
lg1, _ := math.Lgamma(em + 1)
lg2, _ := math.Lgamma(n - em + 1)
t := 1.2 * sq * (1 + y*y) * math.Exp(g-lg1-lg2+em*plog+(n-em)*pclog)
if rnd.Float64() <= t {
if p != porg {
return n - em
}
return em
}
}
}
// PoissonGen returns poisson variable, as number of events in interval,
// with event rate (lmb = Var) plus mean
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func PoissonGen(lambda float64, randOpt ...Rand) float64 {
// NUMERICAL RECIPES IN C: THE ART OF SCIENTIFIC COMPUTING (ISBN 0-521-43108-5)
// p. 294
// <http://www.aip.de/groups/soe/local/numres/bookcpdf/c7-3.pdf>
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
if lambda < 10.0 {
// Use direct method.
var em float64
t := 0.0
for {
t += rnd.ExpFloat64()
if t >= lambda {
break
}
em++
}
return em
}
// Generate using:
// W. Hörmann. "The transformed rejection method for generating Poisson
// random variables." Insurance: Mathematics and Economics
// 12.1 (1993): 39-45.
b := 0.931 + 2.53*math.Sqrt(lambda)
a := -0.059 + 0.02483*b
invalpha := 1.1239 + 1.1328/(b-3.4)
vr := 0.9277 - 3.6224/(b-2)
for {
U := rnd.Float64() - 0.5
V := rnd.Float64()
us := 0.5 - math.Abs(U)
k := math.Floor((2*a/us+b)*U + lambda + 0.43)
if us >= 0.07 && V <= vr {
return k
}
if k <= 0 || (us < 0.013 && V > us) {
continue
}
lg, _ := math.Lgamma(k + 1)
if math.Log(V*invalpha/(a/(us*us)+b)) <= k*math.Log(lambda)-lambda-lg {
return k
}
}
}
// GammaGen represents maximum entropy distribution with two parameters:
// a shape parameter (Alpha, Par in RandParams),
// and a scaling parameter (Beta, Var in RandParams).
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func GammaGen(alpha, beta float64, randOpt ...Rand) float64 {
const (
// The 0.2 threshold is from https://www4.stat.ncsu.edu/~rmartin/Codes/rgamss.R
// described in detail in https://arxiv.org/abs/1302.1884.
smallAlphaThresh = 0.2
)
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
if beta <= 0 {
panic("GammaGen: beta <= 0")
}
a := alpha
b := beta
switch {
case a <= 0:
panic("gamma: alpha <= 0")
case a == 1:
// Generate from exponential
return rnd.ExpFloat64() / b
case a < smallAlphaThresh:
// Generate using
// Liu, Chuanhai, Martin, Ryan and Syring, Nick. "Simulating from a
// gamma distribution with small shape parameter"
// https://arxiv.org/abs/1302.1884
// use this reference: http://link.springer.com/article/10.1007/s00180-016-0692-0
// Algorithm adjusted to work in log space as much as possible.
lambda := 1/a - 1
lr := -math.Log1p(1 / lambda / math.E)
for {
e := rnd.ExpFloat64()
var z float64
if e >= -lr {
z = e + lr
} else {
z = -rnd.ExpFloat64() / lambda
}
eza := math.Exp(-z / a)
lh := -z - eza
var lEta float64
if z >= 0 {
lEta = -z
} else {
lEta = -1 + lambda*z
}
if lh-lEta > -rnd.ExpFloat64() {
return eza / b
}
}
case a >= smallAlphaThresh:
// Generate using:
// Marsaglia, George, and Wai Wan Tsang. "A simple method for generating
// gamma variables." ACM Transactions on Mathematical Software (TOMS)
// 26.3 (2000): 363-372.
d := a - 1.0/3
m := 1.0
if a < 1 {
d += 1.0
m = math.Pow(rnd.Float64(), 1/a)
}
c := 1 / (3 * math.Sqrt(d))
for {
x := rnd.NormFloat64()
v := 1 + x*c
if v <= 0.0 {
continue
}
v = v * v * v
u := rnd.Float64()
if u < 1.0-0.0331*(x*x)*(x*x) {
return m * d * v / b
}
if math.Log(u) < 0.5*x*x+d*(1-v+math.Log(v)) {
return m * d * v / b
}
}
}
panic("unreachable")
}
// GaussianGen returns gaussian (normal) random number with given
// mean and sigma standard deviation.
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func GaussianGen(mean, sigma float64, randOpt ...Rand) float64 {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
return mean + sigma*rnd.NormFloat64()
}
// BetaGen returns beta random number with two shape parameters
// alpha > 0 and beta > 0
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func BetaGen(alpha, beta float64, randOpt ...Rand) float64 {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
ga := GammaGen(alpha, 1, rnd)
gb := GammaGen(beta, 1, rnd)
return ga / (ga + gb)
}
// Code generated by "core generate -add-types"; DO NOT EDIT.
package randx
import (
"cogentcore.org/core/enums"
)
var _RandDistsValues = []RandDists{0, 1, 2, 3, 4, 5, 6}
// RandDistsN is the highest valid value for type RandDists, plus one.
const RandDistsN RandDists = 7
var _RandDistsValueMap = map[string]RandDists{`Uniform`: 0, `Binomial`: 1, `Poisson`: 2, `Gamma`: 3, `Gaussian`: 4, `Beta`: 5, `Mean`: 6}
var _RandDistsDescMap = map[RandDists]string{0: `Uniform has a uniform probability distribution over Var = range on either side of the Mean`, 1: `Binomial represents number of 1's in n (Par) random (Bernouli) trials of probability p (Var)`, 2: `Poisson represents number of events in interval, with event rate (lambda = Var) plus Mean`, 3: `Gamma represents maximum entropy distribution with two parameters: scaling parameter (Var) and shape parameter k (Par) plus Mean`, 4: `Gaussian normal with Var = stddev plus Mean`, 5: `Beta with Var = alpha and Par = beta shape parameters`, 6: `Mean is just the constant Mean, no randomness`}
var _RandDistsMap = map[RandDists]string{0: `Uniform`, 1: `Binomial`, 2: `Poisson`, 3: `Gamma`, 4: `Gaussian`, 5: `Beta`, 6: `Mean`}
// String returns the string representation of this RandDists value.
func (i RandDists) String() string { return enums.String(i, _RandDistsMap) }
// SetString sets the RandDists value from its string representation,
// and returns an error if the string is invalid.
func (i *RandDists) SetString(s string) error {
return enums.SetString(i, s, _RandDistsValueMap, "RandDists")
}
// Int64 returns the RandDists value as an int64.
func (i RandDists) Int64() int64 { return int64(i) }
// SetInt64 sets the RandDists value from an int64.
func (i *RandDists) SetInt64(in int64) { *i = RandDists(in) }
// Desc returns the description of the RandDists value.
func (i RandDists) Desc() string { return enums.Desc(i, _RandDistsDescMap) }
// RandDistsValues returns all possible values for the type RandDists.
func RandDistsValues() []RandDists { return _RandDistsValues }
// Values returns all possible values for the type RandDists.
func (i RandDists) Values() []enums.Enum { return enums.Values(_RandDistsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i RandDists) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *RandDists) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "RandDists")
}
// Copyright (c) 2019, 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 randx
// PChoose32 chooses an index in given slice of float32's at random according
// to the probilities of each item (must be normalized to sum to 1).
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func PChoose32(ps []float32, randOpt ...Rand) int {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
pv := rnd.Float32()
sum := float32(0)
for i, p := range ps {
sum += p
if pv < sum { // note: lower values already excluded
return i
}
}
return len(ps) - 1
}
// PChoose64 chooses an index in given slice of float64's at random according
// to the probilities of each item (must be normalized to sum to 1)
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func PChoose64(ps []float64, randOpt ...Rand) int {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
pv := rnd.Float64()
sum := float64(0)
for i, p := range ps {
sum += p
if pv < sum { // note: lower values already excluded
return i
}
}
return len(ps) - 1
}
// Copyright (c) 2019, 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 randx
// SequentialInts initializes slice of ints to sequential start..start+N-1
// numbers -- for cases where permuting the order is optional.
func SequentialInts(ins []int, start int) {
for i := range ins {
ins[i] = start + i
}
}
// PermuteInts permutes (shuffles) the order of elements in the given int slice
// using the standard Fisher-Yates shuffle
// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
// So you don't have to remember how to call rand.Shuffle.
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func PermuteInts(ins []int, randOpt ...Rand) {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
rnd.Shuffle(len(ins), func(i, j int) {
ins[i], ins[j] = ins[j], ins[i]
})
}
// PermuteStrings permutes (shuffles) the order of elements in the given string slice
// using the standard Fisher-Yates shuffle
// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
// So you don't have to remember how to call rand.Shuffle
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func PermuteStrings(ins []string, randOpt ...Rand) {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
rnd.Shuffle(len(ins), func(i, j int) {
ins[i], ins[j] = ins[j], ins[i]
})
}
// PermuteFloat32s permutes (shuffles) the order of elements in the given float32 slice
// using the standard Fisher-Yates shuffle
// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
// So you don't have to remember how to call rand.Shuffle
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func PermuteFloat32s(ins []float32, randOpt ...Rand) {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
rnd.Shuffle(len(ins), func(i, j int) {
ins[i], ins[j] = ins[j], ins[i]
})
}
// PermuteFloat64s permutes (shuffles) the order of elements in the given float64 slice
// using the standard Fisher-Yates shuffle
// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
// So you don't have to remember how to call rand.Shuffle
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func PermuteFloat64s(ins []float64, randOpt ...Rand) {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
rnd.Shuffle(len(ins), func(i, j int) {
ins[i], ins[j] = ins[j], ins[i]
})
}
// 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 randx
//go:generate core generate -add-types
import "math/rand"
// Rand provides an interface with most of the standard
// rand.Rand methods, to support the use of either the
// global rand generator or a separate Rand source.
type Rand interface {
// Seed uses the provided seed value to initialize the generator to a deterministic state.
// Seed should not be called concurrently with any other Rand method.
Seed(seed int64)
// Int63 returns a non-negative pseudo-random 63-bit integer as an int64.
Int63() int64
// Uint32 returns a pseudo-random 32-bit value as a uint32.
Uint32() uint32
// Uint64 returns a pseudo-random 64-bit value as a uint64.
Uint64() uint64
// Int31 returns a non-negative pseudo-random 31-bit integer as an int32.
Int31() int32
// Int returns a non-negative pseudo-random int.
Int() int
// Int63n returns, as an int64, a non-negative pseudo-random number in the half-open interval [0,n).
// It panics if n <= 0.
Int63n(n int64) int64
// Int31n returns, as an int32, a non-negative pseudo-random number in the half-open interval [0,n).
// It panics if n <= 0.
Int31n(n int32) int32
// Intn returns, as an int, a non-negative pseudo-random number in the half-open interval [0,n).
// It panics if n <= 0.
Intn(n int) int
// Float64 returns, as a float64, a pseudo-random number in the half-open interval [0.0,1.0).
Float64() float64
// Float32 returns, as a float32, a pseudo-random number in the half-open interval [0.0,1.0).
Float32() float32
// NormFloat64 returns a normally distributed float64 in the range
// [-math.MaxFloat64, +math.MaxFloat64] with
// standard normal distribution (mean = 0, stddev = 1)
// from the default Source.
// To produce a different normal distribution, callers can
// adjust the output using:
//
// sample = NormFloat64() * desiredStdDev + desiredMean
NormFloat64() float64
// ExpFloat64 returns an exponentially distributed float64 in the range
// (0, +math.MaxFloat64] with an exponential distribution whose rate parameter
// (lambda) is 1 and whose mean is 1/lambda (1) from the default Source.
// To produce a distribution with a different rate parameter,
// callers can adjust the output using:
//
// sample = ExpFloat64() / desiredRateParameter
ExpFloat64() float64
// Perm returns, as a slice of n ints, a pseudo-random permutation of the integers
// in the half-open interval [0,n).
Perm(n int) []int
// Shuffle pseudo-randomizes the order of elements.
// n is the number of elements. Shuffle panics if n < 0.
// swap swaps the elements with indexes i and j.
Shuffle(n int, swap func(i, j int))
}
// SysRand supports the system random number generator
// for either a separate rand.Rand source, or, if that
// is nil, the global rand stream.
type SysRand struct {
// if non-nil, use this random number source instead of the global default one
Rand *rand.Rand `display:"-"`
}
// NewGlobalRand returns a new SysRand that implements the
// randx.Rand interface, with the system global rand source.
func NewGlobalRand() *SysRand {
r := &SysRand{}
return r
}
// NewSysRand returns a new SysRand with a new
// rand.Rand random source with given initial seed.
func NewSysRand(seed int64) *SysRand {
r := &SysRand{}
r.NewRand(seed)
return r
}
// NewRand sets Rand to a new rand.Rand source using given seed.
func (r *SysRand) NewRand(seed int64) {
r.Rand = rand.New(rand.NewSource(seed))
}
// Seed uses the provided seed value to initialize the generator to a deterministic state.
// Seed should not be called concurrently with any other Rand method.
func (r *SysRand) Seed(seed int64) {
if r.Rand == nil {
rand.Seed(seed)
return
}
r.Rand.Seed(seed)
}
// Int63 returns a non-negative pseudo-random 63-bit integer as an int64.
func (r *SysRand) Int63() int64 {
if r.Rand == nil {
return rand.Int63()
}
return r.Rand.Int63()
}
// Uint32 returns a pseudo-random 32-bit value as a uint32.
func (r *SysRand) Uint32() uint32 {
if r.Rand == nil {
return rand.Uint32()
}
return r.Rand.Uint32()
}
// Uint64 returns a pseudo-random 64-bit value as a uint64.
func (r *SysRand) Uint64() uint64 {
if r.Rand == nil {
return rand.Uint64()
}
return r.Rand.Uint64()
}
// Int31 returns a non-negative pseudo-random 31-bit integer as an int32.
func (r *SysRand) Int31() int32 {
if r.Rand == nil {
return rand.Int31()
}
return r.Rand.Int31()
}
// Int returns a non-negative pseudo-random int.
func (r *SysRand) Int() int {
if r.Rand == nil {
return rand.Int()
}
return r.Rand.Int()
}
// Int63n returns, as an int64, a non-negative pseudo-random number in the half-open interval [0,n).
// It panics if n <= 0.
func (r *SysRand) Int63n(n int64) int64 {
if r.Rand == nil {
return rand.Int63n(n)
}
return r.Rand.Int63n(n)
}
// Int31n returns, as an int32, a non-negative pseudo-random number in the half-open interval [0,n).
// It panics if n <= 0.
func (r *SysRand) Int31n(n int32) int32 {
if r.Rand == nil {
return rand.Int31n(n)
}
return r.Rand.Int31n(n)
}
// Intn returns, as an int, a non-negative pseudo-random number in the half-open interval [0,n).
// It panics if n <= 0.
func (r *SysRand) Intn(n int) int {
if r.Rand == nil {
return rand.Intn(n)
}
return r.Rand.Intn(n)
}
// Float64 returns, as a float64, a pseudo-random number in the half-open interval [0.0,1.0).
func (r *SysRand) Float64() float64 {
if r.Rand == nil {
return rand.Float64()
}
return r.Rand.Float64()
}
// Float32 returns, as a float32, a pseudo-random number in the half-open interval [0.0,1.0).
func (r *SysRand) Float32() float32 {
if r.Rand == nil {
return rand.Float32()
}
return r.Rand.Float32()
}
// NormFloat64 returns a normally distributed float64 in the range
// [-math.MaxFloat64, +math.MaxFloat64] with
// standard normal distribution (mean = 0, stddev = 1)
// from the default Source.
// To produce a different normal distribution, callers can
// adjust the output using:
//
// sample = NormFloat64() * desiredStdDev + desiredMean
func (r *SysRand) NormFloat64() float64 {
if r.Rand == nil {
return rand.NormFloat64()
}
return r.Rand.NormFloat64()
}
// ExpFloat64 returns an exponentially distributed float64 in the range
// (0, +math.MaxFloat64] with an exponential distribution whose rate parameter
// (lambda) is 1 and whose mean is 1/lambda (1) from the default Source.
// To produce a distribution with a different rate parameter,
// callers can adjust the output using:
//
// sample = ExpFloat64() / desiredRateParameter
func (r *SysRand) ExpFloat64() float64 {
if r.Rand == nil {
return rand.ExpFloat64()
}
return r.Rand.ExpFloat64()
}
// Perm returns, as a slice of n ints, a pseudo-random permutation of the integers
// in the half-open interval [0,n).
func (r *SysRand) Perm(n int) []int {
if r.Rand == nil {
return rand.Perm(n)
}
return r.Rand.Perm(n)
}
// Shuffle pseudo-randomizes the order of elements.
// n is the number of elements. Shuffle panics if n < 0.
// swap swaps the elements with indexes i and j.
func (r *SysRand) Shuffle(n int, swap func(i, j int)) {
if r.Rand == nil {
rand.Shuffle(n, swap)
return
}
r.Rand.Shuffle(n, swap)
}
// Copyright (c) 2019, 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 randx
// RandParams provides parameterized random number generation according to different distributions
// and variance, mean params
type RandParams struct { //git:add
// distribution to generate random numbers from
Dist RandDists
// mean of random distribution -- typically added to generated random variants
Mean float64
// variability parameter for the random numbers (gauss = standard deviation, not variance; uniform = half-range, others as noted in RandDists)
Var float64
// extra parameter for distribution (depends on each one)
Par float64
}
func (rp *RandParams) Defaults() {
rp.Var = 1
rp.Par = 1
}
func (rp *RandParams) ShouldDisplay(field string) bool {
switch field {
case "Par":
return rp.Dist == Gamma || rp.Dist == Binomial || rp.Dist == Beta
}
return true
}
// Gen generates a random variable according to current parameters.
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func (rp *RandParams) Gen(randOpt ...Rand) float64 {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
switch rp.Dist {
case Uniform:
return UniformMeanRange(rp.Mean, rp.Var, rnd)
case Binomial:
return rp.Mean + BinomialGen(rp.Par, rp.Var, rnd)
case Poisson:
return rp.Mean + PoissonGen(rp.Var, rnd)
case Gamma:
return rp.Mean + GammaGen(rp.Par, rp.Var, rnd)
case Gaussian:
return GaussianGen(rp.Mean, rp.Var, rnd)
case Beta:
return rp.Mean + BetaGen(rp.Var, rp.Par, rnd)
}
return rp.Mean
}
// RandDists are different random number distributions
type RandDists int32 //enums:enum
// The random number distributions
const (
// Uniform has a uniform probability distribution over Var = range on either side of the Mean
Uniform RandDists = iota
// Binomial represents number of 1's in n (Par) random (Bernouli) trials of probability p (Var)
Binomial
// Poisson represents number of events in interval, with event rate (lambda = Var) plus Mean
Poisson
// Gamma represents maximum entropy distribution with two parameters: scaling parameter (Var)
// and shape parameter k (Par) plus Mean
Gamma
// Gaussian normal with Var = stddev plus Mean
Gaussian
// Beta with Var = alpha and Par = beta shape parameters
Beta
// Mean is just the constant Mean, no randomness
Mean
)
// IntZeroN returns uniform random integer in the range between 0 and n, exclusive of n: [0,n).
// Thr is an optional parallel thread index (-1 0 to ignore).
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func IntZeroN(n int64, randOpt ...Rand) int64 {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
return rnd.Int63n(n)
}
// IntMinMax returns uniform random integer in range between min and max, exclusive of max: [min,max).
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func IntMinMax(min, max int64, randOpt ...Rand) int64 {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
return min + rnd.Int63n(max-min)
}
// IntMeanRange returns uniform random integer with given range on either side of the mean:
// [mean - range, mean + range]
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func IntMeanRange(mean, rnge int64, randOpt ...Rand) int64 {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
return mean + (rnd.Int63n(2*rnge+1) - rnge)
}
// ZeroOne returns a uniform random number between zero and one (exclusive of 1)
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func ZeroOne(randOpt ...Rand) float64 {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
return rnd.Float64()
}
// UniformMinMax returns uniform random number between min and max values inclusive
// (Do not use for generating integers - will not include max!)
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func UniformMinMax(min, max float64, randOpt ...Rand) float64 {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
return min + (max-min)*rnd.Float64()
}
// UniformMeanRange returns uniform random number with given range on either size of the mean:
// [mean - range, mean + range]
// Optionally can pass a single Rand interface to use --
// otherwise uses system global Rand source.
func UniformMeanRange(mean, rnge float64, randOpt ...Rand) float64 {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
return mean + rnge*2.0*(rnd.Float64()-0.5)
}
// Copyright (c) 2019, 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 randx
import (
"time"
)
// Seeds is a set of random seeds, typically used one per Run
type Seeds []int64
// Init allocates given number of seeds and initializes them to
// sequential numbers 1..n
func (rs *Seeds) Init(n int) {
*rs = make([]int64, n)
for i := range *rs {
(*rs)[i] = int64(i) + 1
}
}
// Set sets the given seed to either the single Rand
// interface passed, or the system global Rand source.
func (rs *Seeds) Set(idx int, randOpt ...Rand) {
var rnd Rand
if len(randOpt) == 0 {
rnd = NewGlobalRand()
} else {
rnd = randOpt[0]
}
rnd.Seed((*rs)[idx])
}
// NewSeeds sets a new set of random seeds based on current time
func (rs *Seeds) NewSeeds() {
rn := time.Now().UnixNano()
for i := range *rs {
(*rs)[i] = rn + int64(i)
}
}
// 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 reflectx
import (
"fmt"
"log"
"reflect"
"sort"
"strings"
"time"
"cogentcore.org/core/base/errors"
)
// This file contains helpful functions for dealing with maps
// in the reflect system
// MapValueType returns the type of the value for the given map (which can be
// a pointer to a map or a direct map); just Elem() of map type, but using
// this function makes it more explicit what is going on.
func MapValueType(mp any) reflect.Type {
return NonPointerType(reflect.TypeOf(mp)).Elem()
}
// MapKeyType returns the type of the key for the given map (which can be a
// pointer to a map or a direct map); just Key() of map type, but using
// this function makes it more explicit what is going on.
func MapKeyType(mp any) reflect.Type {
return NonPointerType(reflect.TypeOf(mp)).Key()
}
// MapAdd adds a new blank entry to the map.
func MapAdd(mv any) {
mpv := reflect.ValueOf(mv)
mpvnp := Underlying(mpv)
mvtyp := mpvnp.Type()
valtyp := MapValueType(mv)
if valtyp.Kind() == reflect.Interface && valtyp.String() == "interface {}" {
valtyp = reflect.TypeOf("")
}
nkey := reflect.New(MapKeyType(mv))
nval := reflect.New(valtyp)
if mpvnp.IsNil() { // make a new map
mpv.Elem().Set(reflect.MakeMap(mvtyp))
mpvnp = Underlying(mpv)
}
mpvnp.SetMapIndex(nkey.Elem(), nval.Elem())
}
// MapDelete deletes the given key from the given map.
func MapDelete(mv any, key reflect.Value) {
mpv := reflect.ValueOf(mv)
mpvnp := Underlying(mpv)
mpvnp.SetMapIndex(key, reflect.Value{}) // delete
}
// MapDeleteAll deletes everything from the given map.
func MapDeleteAll(mv any) {
mpv := reflect.ValueOf(mv)
mpvnp := Underlying(mpv)
if mpvnp.Len() == 0 {
return
}
itr := mpvnp.MapRange()
for itr.Next() {
mpvnp.SetMapIndex(itr.Key(), reflect.Value{}) // delete
}
}
// MapSort sorts the keys of the map either by key or by value,
// and returns those keys as a slice of [reflect.Value]s.
func MapSort(mp any, byKey, ascending bool) []reflect.Value {
mpv := reflect.ValueOf(mp)
mpvnp := Underlying(mpv)
keys := mpvnp.MapKeys() // note: this is a slice of reflect.Value!
if byKey {
ValueSliceSort(keys, ascending)
} else {
MapValueSort(mpvnp, keys, ascending)
}
return keys
}
// MapValueSort sorts the keys of the given map by their values.
func MapValueSort(mpvnp reflect.Value, keys []reflect.Value, ascending bool) error {
if len(keys) == 0 {
return nil
}
keyval := keys[0]
felval := mpvnp.MapIndex(keyval)
eltyp := felval.Type()
elnptyp := NonPointerType(eltyp)
vk := elnptyp.Kind()
elval := OnePointerValue(felval)
elif := elval.Interface()
// try all the numeric types first!
switch {
case vk >= reflect.Int && vk <= reflect.Int64:
sort.Slice(keys, func(i, j int) bool {
iv := Underlying(mpvnp.MapIndex(keys[i])).Int()
jv := Underlying(mpvnp.MapIndex(keys[j])).Int()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
case vk >= reflect.Uint && vk <= reflect.Uint64:
sort.Slice(keys, func(i, j int) bool {
iv := Underlying(mpvnp.MapIndex(keys[i])).Uint()
jv := Underlying(mpvnp.MapIndex(keys[j])).Uint()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
case vk >= reflect.Float32 && vk <= reflect.Float64:
sort.Slice(keys, func(i, j int) bool {
iv := Underlying(mpvnp.MapIndex(keys[i])).Float()
jv := Underlying(mpvnp.MapIndex(keys[j])).Float()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
case vk == reflect.Struct && ShortTypeName(elnptyp) == "time.Time":
sort.Slice(keys, func(i, j int) bool {
iv := Underlying(mpvnp.MapIndex(keys[i])).Interface().(time.Time)
jv := Underlying(mpvnp.MapIndex(keys[j])).Interface().(time.Time)
if ascending {
return iv.Before(jv)
}
return jv.Before(iv)
})
}
// this stringer case will likely pick up most of the rest
switch elif.(type) {
case fmt.Stringer:
sort.Slice(keys, func(i, j int) bool {
iv := Underlying(mpvnp.MapIndex(keys[i])).Interface().(fmt.Stringer).String()
jv := Underlying(mpvnp.MapIndex(keys[j])).Interface().(fmt.Stringer).String()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
}
// last resort!
switch {
case vk == reflect.String:
sort.Slice(keys, func(i, j int) bool {
iv := Underlying(mpvnp.MapIndex(keys[i])).String()
jv := Underlying(mpvnp.MapIndex(keys[j])).String()
if ascending {
return strings.ToLower(iv) < strings.ToLower(jv)
}
return strings.ToLower(iv) > strings.ToLower(jv)
})
return nil
}
err := fmt.Errorf("MapValueSort: unable to sort elements of type: %v", eltyp.String())
log.Println(err)
return err
}
// SetMapRobust robustly sets a map value using [reflect.Value]
// representations of the map, key, and value elements, ensuring that the
// proper types are used for the key and value elements using sensible
// conversions.
func SetMapRobust(mp, ky, val reflect.Value) bool {
mtyp := mp.Type()
if mtyp.Kind() != reflect.Map {
log.Printf("reflectx.SetMapRobust: map arg is not map, is: %v\n", mtyp.String())
return false
}
if !mp.CanSet() {
log.Printf("reflectx.SetMapRobust: map arg is not settable: %v\n", mtyp.String())
return false
}
ktyp := mtyp.Key()
etyp := mtyp.Elem()
if etyp.Kind() == val.Kind() && ky.Kind() == ktyp.Kind() {
mp.SetMapIndex(ky, val)
return true
}
if ky.Kind() == ktyp.Kind() {
mp.SetMapIndex(ky, val.Convert(etyp))
return true
}
if etyp.Kind() == val.Kind() {
mp.SetMapIndex(ky.Convert(ktyp), val)
return true
}
mp.SetMapIndex(ky.Convert(ktyp), val.Convert(etyp))
return true
}
// CopyMapRobust robustly copies maps.
func CopyMapRobust(to, from any) error {
tov := reflect.ValueOf(to)
fmv := reflect.ValueOf(from)
tonp := Underlying(tov)
fmnp := Underlying(fmv)
totyp := tonp.Type()
if totyp.Kind() != reflect.Map {
err := fmt.Errorf("reflectx.CopyMapRobust: 'to' is not map, is: %v", totyp)
return errors.Log(err)
}
fmtyp := fmnp.Type()
if fmtyp.Kind() != reflect.Map {
err := fmt.Errorf("reflectx.CopyMapRobust: 'from' is not map, is: %v", fmtyp)
return errors.Log(err)
}
if tonp.IsNil() {
OnePointerValue(tov).Elem().Set(reflect.MakeMap(totyp))
} else {
MapDeleteAll(to)
}
if fmnp.Len() == 0 {
return nil
}
eltyp := SliceElementType(to)
itr := fmnp.MapRange()
for itr.Next() {
tonp.SetMapIndex(itr.Key(), CloneToType(eltyp, itr.Value().Interface()).Elem())
}
return nil
}
// 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 reflectx
import (
"reflect"
)
// NonPointerType returns a non-pointer version of the given type.
func NonPointerType(typ reflect.Type) reflect.Type {
if typ == nil {
return typ
}
for typ.Kind() == reflect.Pointer {
typ = typ.Elem()
}
return typ
}
// NonPointerValue returns a non-pointer version of the given value.
// If it encounters a nil pointer, it returns the nil pointer instead
// of an invalid value.
func NonPointerValue(v reflect.Value) reflect.Value {
for v.Kind() == reflect.Pointer {
new := v.Elem()
if !new.IsValid() {
return v
}
v = new
}
return v
}
// PointerValue returns a pointer to the given value if it is not already
// a pointer.
func PointerValue(v reflect.Value) reflect.Value {
if !v.IsValid() {
return v
}
if v.Kind() == reflect.Pointer {
return v
}
if v.CanAddr() {
return v.Addr()
}
pv := reflect.New(v.Type())
pv.Elem().Set(v)
return pv
}
// OnePointerValue returns a value that is exactly one pointer away
// from a non-pointer value.
func OnePointerValue(v reflect.Value) reflect.Value {
if !v.IsValid() {
return v
}
if v.Kind() != reflect.Pointer {
if v.CanAddr() {
return v.Addr()
}
// slog.Error("reflectx.OnePointerValue: cannot take address of value", "value", v)
pv := reflect.New(v.Type())
pv.Elem().Set(v)
return pv
} else {
for v.Elem().Kind() == reflect.Pointer {
v = v.Elem()
}
}
return v
}
// Underlying returns the actual underlying version of the given value,
// going through any pointers and interfaces. If it encounters a nil
// pointer or interface, it returns the nil pointer or interface instead of
// an invalid value.
func Underlying(v reflect.Value) reflect.Value {
if !v.IsValid() {
return v
}
for v.Type().Kind() == reflect.Interface || v.Type().Kind() == reflect.Pointer {
new := v.Elem()
if !new.IsValid() {
return v
}
v = new
}
return v
}
// UnderlyingPointer returns a pointer to the actual underlying version of the
// given value, going through any pointers and interfaces. It is equivalent to
// [OnePointerValue] of [Underlying], so if it encounters a nil pointer or
// interface, it stops at the nil pointer or interface instead of returning
// an invalid value.
func UnderlyingPointer(v reflect.Value) reflect.Value {
if !v.IsValid() {
return v
}
uv := Underlying(v)
if !uv.IsValid() {
return v
}
return OnePointerValue(uv)
}
// NonNilNew has the same overall behavior as [reflect.New] except that
// it traverses through any pointers such that a new zero non-pointer value
// will be created in the end, so any pointers in the original type will not
// be nil. For example, in pseudo-code, NonNilNew(**int) will return
// &(&(&(0))).
func NonNilNew(typ reflect.Type) reflect.Value {
n := 0
for typ.Kind() == reflect.Pointer {
n++
typ = typ.Elem()
}
v := reflect.New(typ)
for range n {
pv := reflect.New(v.Type())
pv.Elem().Set(v)
v = pv
}
return v
}
// 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 reflectx
import (
"fmt"
"reflect"
"sort"
"strings"
"time"
"cogentcore.org/core/base/errors"
)
// This file contains helpful functions for dealing with slices
// in the reflect system
// SliceElementType returns the type of the elements of the given slice (which can be
// a pointer to a slice or a direct slice); just [reflect.Type.Elem] of slice type, but
// using this function bypasses any pointer issues and makes it more explicit what is going on.
func SliceElementType(sl any) reflect.Type {
return Underlying(reflect.ValueOf(sl)).Type().Elem()
}
// SliceElementValue returns a new [reflect.Value] of the [SliceElementType].
func SliceElementValue(sl any) reflect.Value {
return NonNilNew(SliceElementType(sl)).Elem()
}
// SliceNewAt inserts a new blank element at the given index in the given slice.
// -1 means the end.
func SliceNewAt(sl any, idx int) {
up := UnderlyingPointer(reflect.ValueOf(sl))
np := up.Elem()
val := SliceElementValue(sl)
sz := np.Len()
np = reflect.Append(np, val)
if idx >= 0 && idx < sz {
reflect.Copy(np.Slice(idx+1, sz+1), np.Slice(idx, sz))
np.Index(idx).Set(val)
}
up.Elem().Set(np)
}
// SliceDeleteAt deletes the element at the given index in the given slice.
func SliceDeleteAt(sl any, idx int) {
svl := OnePointerValue(reflect.ValueOf(sl))
svnp := NonPointerValue(svl)
svtyp := svnp.Type()
nval := reflect.New(svtyp.Elem())
sz := svnp.Len()
reflect.Copy(svnp.Slice(idx, sz-1), svnp.Slice(idx+1, sz))
svnp.Index(sz - 1).Set(nval.Elem())
svl.Elem().Set(svnp.Slice(0, sz-1))
}
// SliceSort sorts a slice of basic values (see [StructSliceSort] for sorting a
// slice-of-struct using a specific field), using float, int, string, and [time.Time]
// conversions.
func SliceSort(sl any, ascending bool) error {
sv := reflect.ValueOf(sl)
svnp := NonPointerValue(sv)
if svnp.Len() == 0 {
return nil
}
eltyp := SliceElementType(sl)
elnptyp := NonPointerType(eltyp)
vk := elnptyp.Kind()
elval := OnePointerValue(svnp.Index(0))
elif := elval.Interface()
// try all the numeric types first!
switch {
case vk >= reflect.Int && vk <= reflect.Int64:
sort.Slice(svnp.Interface(), func(i, j int) bool {
iv := NonPointerValue(svnp.Index(i)).Int()
jv := NonPointerValue(svnp.Index(j)).Int()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
case vk >= reflect.Uint && vk <= reflect.Uint64:
sort.Slice(svnp.Interface(), func(i, j int) bool {
iv := NonPointerValue(svnp.Index(i)).Uint()
jv := NonPointerValue(svnp.Index(j)).Uint()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
case vk >= reflect.Float32 && vk <= reflect.Float64:
sort.Slice(svnp.Interface(), func(i, j int) bool {
iv := NonPointerValue(svnp.Index(i)).Float()
jv := NonPointerValue(svnp.Index(j)).Float()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
case vk == reflect.Struct && ShortTypeName(elnptyp) == "time.Time":
sort.Slice(svnp.Interface(), func(i, j int) bool {
iv := NonPointerValue(svnp.Index(i)).Interface().(time.Time)
jv := NonPointerValue(svnp.Index(j)).Interface().(time.Time)
if ascending {
return iv.Before(jv)
}
return jv.Before(iv)
})
}
// this stringer case will likely pick up most of the rest
switch elif.(type) {
case fmt.Stringer:
sort.Slice(svnp.Interface(), func(i, j int) bool {
iv := NonPointerValue(svnp.Index(i)).Interface().(fmt.Stringer).String()
jv := NonPointerValue(svnp.Index(j)).Interface().(fmt.Stringer).String()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
}
// last resort!
switch {
case vk == reflect.String:
sort.Slice(svnp.Interface(), func(i, j int) bool {
iv := NonPointerValue(svnp.Index(i)).String()
jv := NonPointerValue(svnp.Index(j)).String()
if ascending {
return strings.ToLower(iv) < strings.ToLower(jv)
}
return strings.ToLower(iv) > strings.ToLower(jv)
})
return nil
}
err := fmt.Errorf("SortSlice: unable to sort elements of type: %v", eltyp.String())
return errors.Log(err)
}
// StructSliceSort sorts the given slice of structs according to the given field
// indexes and sort direction, using float, int, string, and [time.Time] conversions.
// It will panic if the field indexes are invalid.
func StructSliceSort(structSlice any, fieldIndex []int, ascending bool) error {
sv := reflect.ValueOf(structSlice)
svnp := NonPointerValue(sv)
if svnp.Len() == 0 {
return nil
}
structTyp := SliceElementType(structSlice)
structNptyp := NonPointerType(structTyp)
fld := structNptyp.FieldByIndex(fieldIndex) // not easy to check.
vk := fld.Type.Kind()
structVal := OnePointerValue(svnp.Index(0))
fieldVal := structVal.Elem().FieldByIndex(fieldIndex)
fieldIf := fieldVal.Interface()
// try all the numeric types first!
switch {
case vk >= reflect.Int && vk <= reflect.Int64:
sort.Slice(svnp.Interface(), func(i, j int) bool {
ival := OnePointerValue(svnp.Index(i))
iv := ival.Elem().FieldByIndex(fieldIndex).Int()
jval := OnePointerValue(svnp.Index(j))
jv := jval.Elem().FieldByIndex(fieldIndex).Int()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
case vk >= reflect.Uint && vk <= reflect.Uint64:
sort.Slice(svnp.Interface(), func(i, j int) bool {
ival := OnePointerValue(svnp.Index(i))
iv := ival.Elem().FieldByIndex(fieldIndex).Uint()
jval := OnePointerValue(svnp.Index(j))
jv := jval.Elem().FieldByIndex(fieldIndex).Uint()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
case vk >= reflect.Float32 && vk <= reflect.Float64:
sort.Slice(svnp.Interface(), func(i, j int) bool {
ival := OnePointerValue(svnp.Index(i))
iv := ival.Elem().FieldByIndex(fieldIndex).Float()
jval := OnePointerValue(svnp.Index(j))
jv := jval.Elem().FieldByIndex(fieldIndex).Float()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
case vk == reflect.Struct && ShortTypeName(fld.Type) == "time.Time":
sort.Slice(svnp.Interface(), func(i, j int) bool {
ival := OnePointerValue(svnp.Index(i))
iv := ival.Elem().FieldByIndex(fieldIndex).Interface().(time.Time)
jval := OnePointerValue(svnp.Index(j))
jv := jval.Elem().FieldByIndex(fieldIndex).Interface().(time.Time)
if ascending {
return iv.Before(jv)
}
return jv.Before(iv)
})
}
// this stringer case will likely pick up most of the rest
switch fieldIf.(type) {
case fmt.Stringer:
sort.Slice(svnp.Interface(), func(i, j int) bool {
ival := OnePointerValue(svnp.Index(i))
iv := ival.Elem().FieldByIndex(fieldIndex).Interface().(fmt.Stringer).String()
jval := OnePointerValue(svnp.Index(j))
jv := jval.Elem().FieldByIndex(fieldIndex).Interface().(fmt.Stringer).String()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
}
// last resort!
switch {
case vk == reflect.String:
sort.Slice(svnp.Interface(), func(i, j int) bool {
ival := OnePointerValue(svnp.Index(i))
iv := ival.Elem().FieldByIndex(fieldIndex).String()
jval := OnePointerValue(svnp.Index(j))
jv := jval.Elem().FieldByIndex(fieldIndex).String()
if ascending {
return strings.ToLower(iv) < strings.ToLower(jv)
}
return strings.ToLower(iv) > strings.ToLower(jv)
})
return nil
}
err := fmt.Errorf("SortStructSlice: unable to sort on field of type: %v", fld.Type.String())
return errors.Log(err)
}
// ValueSliceSort sorts a slice of [reflect.Value]s using basic types where possible.
func ValueSliceSort(sl []reflect.Value, ascending bool) error {
if len(sl) == 0 {
return nil
}
felval := sl[0] // reflect.Value
eltyp := felval.Type()
elnptyp := NonPointerType(eltyp)
vk := elnptyp.Kind()
elval := OnePointerValue(felval)
elif := elval.Interface()
// try all the numeric types first!
switch {
case vk >= reflect.Int && vk <= reflect.Int64:
sort.Slice(sl, func(i, j int) bool {
iv := NonPointerValue(sl[i]).Int()
jv := NonPointerValue(sl[j]).Int()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
case vk >= reflect.Uint && vk <= reflect.Uint64:
sort.Slice(sl, func(i, j int) bool {
iv := NonPointerValue(sl[i]).Uint()
jv := NonPointerValue(sl[j]).Uint()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
case vk >= reflect.Float32 && vk <= reflect.Float64:
sort.Slice(sl, func(i, j int) bool {
iv := NonPointerValue(sl[i]).Float()
jv := NonPointerValue(sl[j]).Float()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
case vk == reflect.Struct && ShortTypeName(elnptyp) == "time.Time":
sort.Slice(sl, func(i, j int) bool {
iv := NonPointerValue(sl[i]).Interface().(time.Time)
jv := NonPointerValue(sl[j]).Interface().(time.Time)
if ascending {
return iv.Before(jv)
}
return jv.Before(iv)
})
}
// this stringer case will likely pick up most of the rest
switch elif.(type) {
case fmt.Stringer:
sort.Slice(sl, func(i, j int) bool {
iv := NonPointerValue(sl[i]).Interface().(fmt.Stringer).String()
jv := NonPointerValue(sl[j]).Interface().(fmt.Stringer).String()
if ascending {
return iv < jv
}
return iv > jv
})
return nil
}
// last resort!
switch {
case vk == reflect.String:
sort.Slice(sl, func(i, j int) bool {
iv := NonPointerValue(sl[i]).String()
jv := NonPointerValue(sl[j]).String()
if ascending {
return strings.ToLower(iv) < strings.ToLower(jv)
}
return strings.ToLower(iv) > strings.ToLower(jv)
})
return nil
}
err := fmt.Errorf("ValueSliceSort: unable to sort elements of type: %v", eltyp.String())
return errors.Log(err)
}
// CopySliceRobust robustly copies slices.
func CopySliceRobust(to, from any) error {
tov := reflect.ValueOf(to)
fmv := reflect.ValueOf(from)
tonp := Underlying(tov)
fmnp := Underlying(fmv)
totyp := tonp.Type()
if totyp.Kind() != reflect.Slice {
err := fmt.Errorf("reflectx.CopySliceRobust: 'to' is not slice, is: %v", totyp.String())
return errors.Log(err)
}
fmtyp := fmnp.Type()
if fmtyp.Kind() != reflect.Slice {
err := fmt.Errorf("reflectx.CopySliceRobust: 'from' is not slice, is: %v", fmtyp.String())
return errors.Log(err)
}
fmlen := fmnp.Len()
if tonp.IsNil() {
tonp.Set(reflect.MakeSlice(totyp, fmlen, fmlen))
} else {
if tonp.Len() > fmlen {
tonp.SetLen(fmlen)
}
}
for i := 0; i < fmlen; i++ {
tolen := tonp.Len()
if i >= tolen {
SliceNewAt(to, i)
}
SetRobust(PointerValue(tonp.Index(i)).Interface(), fmnp.Index(i).Interface())
}
return nil
}
// 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 reflectx
import (
"fmt"
"log/slog"
"reflect"
"strconv"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/iox/jsonx"
)
// WalkFields calls the given walk function on all the exported primary fields of the
// given parent struct value, including those on anonymous embedded
// structs that this struct has. It effectively flattens all of the embedded fields
// of the struct.
//
// It passes the current parent struct, current [reflect.StructField],
// and current field value to the given should and walk functions.
//
// The given should function is called on every struct field (including
// on embedded structs themselves) to determine whether that field and any fields
// it has embedded should be handled (a return value of true indicates to continue
// down and a value of false indicates to not).
func WalkFields(parent reflect.Value, should func(parent reflect.Value, field reflect.StructField, value reflect.Value) bool, walk func(parent reflect.Value, parentField *reflect.StructField, field reflect.StructField, value reflect.Value)) {
walkFields(parent, nil, should, walk)
}
func walkFields(parent reflect.Value, parentField *reflect.StructField, should func(parent reflect.Value, field reflect.StructField, value reflect.Value) bool, walk func(parent reflect.Value, parentField *reflect.StructField, field reflect.StructField, value reflect.Value)) {
typ := parent.Type()
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if !field.IsExported() {
continue
}
value := parent.Field(i)
if !should(parent, field, value) {
continue
}
if field.Type.Kind() == reflect.Struct && field.Anonymous {
walkFields(value, &field, should, walk)
} else {
walk(parent, parentField, field, value)
}
}
}
// NumAllFields returns the number of elemental fields in the given struct type
// using [WalkFields].
func NumAllFields(parent reflect.Value) int {
n := 0
WalkFields(parent,
func(parent reflect.Value, field reflect.StructField, value reflect.Value) bool {
return true
}, func(parent reflect.Value, parentField *reflect.StructField, field reflect.StructField, value reflect.Value) {
n++
})
return n
}
// ValueIsDefault returns whether the given value is equivalent to the
// given string representation used in a field default tag.
func ValueIsDefault(fv reflect.Value, def string) bool {
kind := fv.Kind()
if kind >= reflect.Int && kind <= reflect.Complex128 && strings.Contains(def, ":") {
dtags := strings.Split(def, ":")
lo, _ := strconv.ParseFloat(dtags[0], 64)
hi, _ := strconv.ParseFloat(dtags[1], 64)
vf, err := ToFloat(fv.Interface())
if err != nil {
slog.Error("reflectx.ValueIsDefault: error parsing struct field numerical range def tag", "def", def, "err", err)
return true
}
return lo <= vf && vf <= hi
}
dtags := strings.Split(def, ",")
if strings.ContainsAny(def, "{[") { // complex type, so don't split on commas
dtags = []string{def}
}
for _, df := range dtags {
df = FormatDefault(df)
if df == "" {
return fv.IsZero()
}
dv := reflect.New(fv.Type())
err := SetRobust(dv.Interface(), df)
if err != nil {
slog.Error("reflectx.ValueIsDefault: error getting value from default struct tag", "defaultStructTag", df, "value", fv, "err", err)
return false
}
if reflect.DeepEqual(fv.Interface(), dv.Elem().Interface()) {
return true
}
}
return false
}
// SetFromDefaultTags sets the values of fields in the given struct based on
// `default:` default value struct field tags.
func SetFromDefaultTags(v any) error {
ov := reflect.ValueOf(v)
if IsNil(ov) {
return nil
}
val := NonPointerValue(ov)
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
if !f.IsExported() {
continue
}
fv := val.Field(i)
def := f.Tag.Get("default")
if NonPointerType(f.Type).Kind() == reflect.Struct && def == "" {
SetFromDefaultTags(PointerValue(fv).Interface())
continue
}
err := SetFromDefaultTag(fv, def)
if err != nil {
return fmt.Errorf("reflectx.SetFromDefaultTags: error setting field %q in object of type %q from val %q: %w", f.Name, typ.Name(), def, err)
}
}
return nil
}
// SetFromDefaultTag sets the given value from the given default tag.
func SetFromDefaultTag(v reflect.Value, def string) error {
def = FormatDefault(def)
if def == "" {
return nil
}
return SetRobust(UnderlyingPointer(v).Interface(), def)
}
// ShouldSaver is an interface that types can implement to specify
// whether a value should be included in the result of [NonDefaultFields].
type ShouldSaver interface {
// ShouldSave returns whether this value should be included in the
// result of [NonDefaultFields].
ShouldSave() bool
}
// TODO: this needs to return an ordmap or struct of the fields
// NonDefaultFields returns a map representing all of the fields of the given
// struct (or pointer to a struct) that have values different than their default
// values as specified by the `default:` struct tag. The resulting map is then typically
// saved using something like JSON or TOML. If a value has no default value, it
// checks whether its value is non-zero. If a field has a `save:"-"` tag, it wil
// not be included in the resulting map. If a field implements [ShouldSaver] and
// returns false, it will not be included in the resulting map.
func NonDefaultFields(v any) map[string]any {
res := map[string]any{}
rv := Underlying(reflect.ValueOf(v))
if IsNil(rv) {
return nil
}
rt := rv.Type()
nf := rt.NumField()
for i := 0; i < nf; i++ {
fv := rv.Field(i)
ft := rt.Field(i)
if ft.Tag.Get("save") == "-" {
continue
}
if ss, ok := UnderlyingPointer(fv).Interface().(ShouldSaver); ok {
if !ss.ShouldSave() {
continue
}
}
def := ft.Tag.Get("default")
if NonPointerType(ft.Type).Kind() == reflect.Struct && def == "" {
sfm := NonDefaultFields(fv.Interface())
if len(sfm) > 0 {
res[ft.Name] = sfm
}
continue
}
if !ValueIsDefault(fv, def) {
res[ft.Name] = fv.Interface()
}
}
return res
}
// FormatDefault converts the given `default:` struct tag string into a format suitable
// for being used as a value in [SetRobust]. If it returns "", the default value
// should not be used.
func FormatDefault(def string) string {
if def == "" {
return ""
}
if strings.ContainsAny(def, "{[") { // complex type, so don't split on commas and colons
return strings.ReplaceAll(def, `'`, `"`) // allow single quote to work as double quote for JSON format
}
// we split on commas and colons so we get the first item of lists and ranges
def = strings.Split(def, ",")[0]
def = strings.Split(def, ":")[0]
return def
}
// StructTags returns a map[string]string of the tag string from a [reflect.StructTag] value.
func StructTags(tags reflect.StructTag) map[string]string {
if len(tags) == 0 {
return nil
}
flds := strings.Fields(string(tags))
smap := make(map[string]string, len(flds))
for _, fld := range flds {
cli := strings.Index(fld, ":")
if cli < 0 || len(fld) < cli+3 {
continue
}
vl := strings.TrimSuffix(fld[cli+2:], `"`)
smap[fld[:cli]] = vl
}
return smap
}
// StringJSON returns an indented JSON string representation
// of the given value for printing/debugging.
func StringJSON(v any) string {
return string(errors.Log1(jsonx.WriteBytesIndent(v)))
}
// 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 reflectx
import (
"path"
"reflect"
)
// LongTypeName returns the long, full package-path qualified type name.
// This is guaranteed to be unique and used for internal storage of
// several maps to avoid any conflicts. It is also very quick to compute.
func LongTypeName(typ reflect.Type) string {
nptyp := NonPointerType(typ)
nm := nptyp.Name()
if nm != "" {
p := nptyp.PkgPath()
if p != "" {
return p + "." + nm
}
return nm
}
return typ.String()
}
// ShortTypeName returns the short version of a package-qualified type name
// which just has the last element of the path. This is what is used in
// standard Go programming, and is is used for the key to lookup reflect.Type
// names -- i.e., this is what you should save in a JSON file.
// The potential naming conflict is worth the brevity, and typically a given
// file will only contain mutually compatible, non-conflicting types.
// This is cached in ShortNames because the path.Base computation is apparently
// a bit slow.
func ShortTypeName(typ reflect.Type) string {
nptyp := NonPointerType(typ)
nm := nptyp.Name()
if nm != "" {
p := nptyp.PkgPath()
if p != "" {
return path.Base(p) + "." + nm
}
return nm
}
return typ.String()
}
// CloneToType creates a new pointer to the given type
// and uses [SetRobust] to copy an existing value
// (of potentially another type) to it.
func CloneToType(typ reflect.Type, val any) reflect.Value {
vn := reflect.New(typ)
evi := vn.Interface()
SetRobust(evi, val)
return vn
}
// 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 reflectx provides a collection of helpers for the reflect
// package in the Go standard library.
package reflectx
import (
"encoding/json"
"fmt"
"image"
"image/color"
"reflect"
"strconv"
"time"
"cogentcore.org/core/base/bools"
"cogentcore.org/core/base/elide"
"cogentcore.org/core/colors"
"cogentcore.org/core/enums"
)
// IsNil returns whether the given value is nil or invalid.
// If it is a non-nillable type, it does not check whether
// it is nil to avoid panics.
func IsNil(v reflect.Value) bool {
if !v.IsValid() {
return true
}
switch v.Kind() {
case reflect.Pointer, reflect.Interface, reflect.Map, reflect.Slice, reflect.Func, reflect.Chan:
return v.IsNil()
}
return false
}
// KindIsBasic returns whether the given [reflect.Kind] is a basic,
// elemental type such as Int, Float, etc.
func KindIsBasic(vk reflect.Kind) bool {
return vk >= reflect.Bool && vk <= reflect.Complex128
}
// KindIsNumber returns whether the given [reflect.Kind] is a numeric
// type such as Int, Float, etc.
func KindIsNumber(vk reflect.Kind) bool {
return vk >= reflect.Int && vk <= reflect.Complex128
}
// ToBool robustly converts to a bool any basic elemental type
// (including pointers to such) using a big type switch organized
// for greatest efficiency. It tries the [bools.Booler]
// interface if not a bool type. It falls back on reflection when all
// else fails.
func ToBool(v any) (bool, error) {
switch vt := v.(type) {
case bool:
return vt, nil
case *bool:
if vt == nil {
return false, fmt.Errorf("got nil *bool")
}
return *vt, nil
}
if br, ok := v.(bools.Booler); ok {
return br.Bool(), nil
}
switch vt := v.(type) {
case int:
return vt != 0, nil
case *int:
if vt == nil {
return false, fmt.Errorf("got nil *int")
}
return *vt != 0, nil
case int32:
return vt != 0, nil
case *int32:
if vt == nil {
return false, fmt.Errorf("got nil *int32")
}
return *vt != 0, nil
case int64:
return vt != 0, nil
case *int64:
if vt == nil {
return false, fmt.Errorf("got nil *int64")
}
return *vt != 0, nil
case uint8:
return vt != 0, nil
case *uint8:
if vt == nil {
return false, fmt.Errorf("got nil *uint8")
}
return *vt != 0, nil
case float64:
return vt != 0, nil
case *float64:
if vt == nil {
return false, fmt.Errorf("got nil *float64")
}
return *vt != 0, nil
case float32:
return vt != 0, nil
case *float32:
if vt == nil {
return false, fmt.Errorf("got nil *float32")
}
return *vt != 0, nil
case string:
r, err := strconv.ParseBool(vt)
if err != nil {
return false, err
}
return r, nil
case *string:
if vt == nil {
return false, fmt.Errorf("got nil *string")
}
r, err := strconv.ParseBool(*vt)
if err != nil {
return false, err
}
return r, nil
case int8:
return vt != 0, nil
case *int8:
if vt == nil {
return false, fmt.Errorf("got nil *int8")
}
return *vt != 0, nil
case int16:
return vt != 0, nil
case *int16:
if vt == nil {
return false, fmt.Errorf("got nil *int16")
}
return *vt != 0, nil
case uint:
return vt != 0, nil
case *uint:
if vt == nil {
return false, fmt.Errorf("got nil *uint")
}
return *vt != 0, nil
case uint16:
return vt != 0, nil
case *uint16:
if vt == nil {
return false, fmt.Errorf("got nil *uint16")
}
return *vt != 0, nil
case uint32:
return vt != 0, nil
case *uint32:
if vt == nil {
return false, fmt.Errorf("got nil *uint32")
}
return *vt != 0, nil
case uint64:
return vt != 0, nil
case *uint64:
if vt == nil {
return false, fmt.Errorf("got nil *uint64")
}
return *vt != 0, nil
case uintptr:
return vt != 0, nil
case *uintptr:
if vt == nil {
return false, fmt.Errorf("got nil *uintptr")
}
return *vt != 0, nil
}
// then fall back on reflection
uv := Underlying(reflect.ValueOf(v))
if IsNil(uv) {
return false, fmt.Errorf("got nil value of type %T", v)
}
vk := uv.Kind()
switch {
case vk >= reflect.Int && vk <= reflect.Int64:
return (uv.Int() != 0), nil
case vk >= reflect.Uint && vk <= reflect.Uint64:
return (uv.Uint() != 0), nil
case vk == reflect.Bool:
return uv.Bool(), nil
case vk >= reflect.Float32 && vk <= reflect.Float64:
return (uv.Float() != 0.0), nil
case vk >= reflect.Complex64 && vk <= reflect.Complex128:
return (real(uv.Complex()) != 0.0), nil
case vk == reflect.String:
r, err := strconv.ParseBool(uv.String())
if err != nil {
return false, err
}
return r, nil
default:
return false, fmt.Errorf("got value %v of unsupported type %T", v, v)
}
}
// ToInt robustly converts to an int64 any basic elemental type
// (including pointers to such) using a big type switch organized
// for greatest efficiency, only falling back on reflection when all
// else fails.
func ToInt(v any) (int64, error) {
switch vt := v.(type) {
case int:
return int64(vt), nil
case *int:
if vt == nil {
return 0, fmt.Errorf("got nil *int")
}
return int64(*vt), nil
case int32:
return int64(vt), nil
case *int32:
if vt == nil {
return 0, fmt.Errorf("got nil *int32")
}
return int64(*vt), nil
case int64:
return vt, nil
case *int64:
if vt == nil {
return 0, fmt.Errorf("got nil *int64")
}
return *vt, nil
case uint8:
return int64(vt), nil
case *uint8:
if vt == nil {
return 0, fmt.Errorf("got nil *uint8")
}
return int64(*vt), nil
case float64:
return int64(vt), nil
case *float64:
if vt == nil {
return 0, fmt.Errorf("got nil *float64")
}
return int64(*vt), nil
case float32:
return int64(vt), nil
case *float32:
if vt == nil {
return 0, fmt.Errorf("got nil *float32")
}
return int64(*vt), nil
case bool:
if vt {
return 1, nil
}
return 0, nil
case *bool:
if vt == nil {
return 0, fmt.Errorf("got nil *bool")
}
if *vt {
return 1, nil
}
return 0, nil
case string:
r, err := strconv.ParseInt(vt, 0, 64)
if err != nil {
return 0, err
}
return r, nil
case *string:
if vt == nil {
return 0, fmt.Errorf("got nil *string")
}
r, err := strconv.ParseInt(*vt, 0, 64)
if err != nil {
return 0, err
}
return r, nil
case enums.Enum:
return vt.Int64(), nil
case int8:
return int64(vt), nil
case *int8:
if vt == nil {
return 0, fmt.Errorf("got nil *int8")
}
return int64(*vt), nil
case int16:
return int64(vt), nil
case *int16:
if vt == nil {
return 0, fmt.Errorf("got nil *int16")
}
return int64(*vt), nil
case uint:
return int64(vt), nil
case *uint:
if vt == nil {
return 0, fmt.Errorf("got nil *uint")
}
return int64(*vt), nil
case uint16:
return int64(vt), nil
case *uint16:
if vt == nil {
return 0, fmt.Errorf("got nil *uint16")
}
return int64(*vt), nil
case uint32:
return int64(vt), nil
case *uint32:
if vt == nil {
return 0, fmt.Errorf("got nil *uint32")
}
return int64(*vt), nil
case uint64:
return int64(vt), nil
case *uint64:
if vt == nil {
return 0, fmt.Errorf("got nil *uint64")
}
return int64(*vt), nil
case uintptr:
return int64(vt), nil
case *uintptr:
if vt == nil {
return 0, fmt.Errorf("got nil *uintptr")
}
return int64(*vt), nil
}
// then fall back on reflection
uv := Underlying(reflect.ValueOf(v))
if IsNil(uv) {
return 0, fmt.Errorf("got nil value of type %T", v)
}
vk := uv.Kind()
switch {
case vk >= reflect.Int && vk <= reflect.Int64:
return uv.Int(), nil
case vk >= reflect.Uint && vk <= reflect.Uint64:
return int64(uv.Uint()), nil
case vk == reflect.Bool:
if uv.Bool() {
return 1, nil
}
return 0, nil
case vk >= reflect.Float32 && vk <= reflect.Float64:
return int64(uv.Float()), nil
case vk >= reflect.Complex64 && vk <= reflect.Complex128:
return int64(real(uv.Complex())), nil
case vk == reflect.String:
r, err := strconv.ParseInt(uv.String(), 0, 64)
if err != nil {
return 0, err
}
return r, nil
default:
return 0, fmt.Errorf("got value %v of unsupported type %T", v, v)
}
}
// ToFloat robustly converts to a float64 any basic elemental type
// (including pointers to such) using a big type switch organized for
// greatest efficiency, only falling back on reflection when all else fails.
func ToFloat(v any) (float64, error) {
switch vt := v.(type) {
case float64:
return vt, nil
case *float64:
if vt == nil {
return 0, fmt.Errorf("got nil *float64")
}
return *vt, nil
case float32:
return float64(vt), nil
case *float32:
if vt == nil {
return 0, fmt.Errorf("got nil *float32")
}
return float64(*vt), nil
case int:
return float64(vt), nil
case *int:
if vt == nil {
return 0, fmt.Errorf("got nil *int")
}
return float64(*vt), nil
case int32:
return float64(vt), nil
case *int32:
if vt == nil {
return 0, fmt.Errorf("got nil *int32")
}
return float64(*vt), nil
case int64:
return float64(vt), nil
case *int64:
if vt == nil {
return 0, fmt.Errorf("got nil *int64")
}
return float64(*vt), nil
case uint8:
return float64(vt), nil
case *uint8:
if vt == nil {
return 0, fmt.Errorf("got nil *uint8")
}
return float64(*vt), nil
case bool:
if vt {
return 1, nil
}
return 0, nil
case *bool:
if vt == nil {
return 0, fmt.Errorf("got nil *bool")
}
if *vt {
return 1, nil
}
return 0, nil
case string:
r, err := strconv.ParseFloat(vt, 64)
if err != nil {
return 0.0, err
}
return r, nil
case *string:
if vt == nil {
return 0, fmt.Errorf("got nil *string")
}
r, err := strconv.ParseFloat(*vt, 64)
if err != nil {
return 0.0, err
}
return r, nil
case int8:
return float64(vt), nil
case *int8:
if vt == nil {
return 0, fmt.Errorf("got nil *int8")
}
return float64(*vt), nil
case int16:
return float64(vt), nil
case *int16:
if vt == nil {
return 0, fmt.Errorf("got nil *int16")
}
return float64(*vt), nil
case uint:
return float64(vt), nil
case *uint:
if vt == nil {
return 0, fmt.Errorf("got nil *uint")
}
return float64(*vt), nil
case uint16:
return float64(vt), nil
case *uint16:
if vt == nil {
return 0, fmt.Errorf("got nil *uint16")
}
return float64(*vt), nil
case uint32:
return float64(vt), nil
case *uint32:
if vt == nil {
return 0, fmt.Errorf("got nil *uint32")
}
return float64(*vt), nil
case uint64:
return float64(vt), nil
case *uint64:
if vt == nil {
return 0, fmt.Errorf("got nil *uint64")
}
return float64(*vt), nil
case uintptr:
return float64(vt), nil
case *uintptr:
if vt == nil {
return 0, fmt.Errorf("got nil *uintptr")
}
return float64(*vt), nil
}
// then fall back on reflection
uv := Underlying(reflect.ValueOf(v))
if IsNil(uv) {
return 0, fmt.Errorf("got nil value of type %T", v)
}
vk := uv.Kind()
switch {
case vk >= reflect.Int && vk <= reflect.Int64:
return float64(uv.Int()), nil
case vk >= reflect.Uint && vk <= reflect.Uint64:
return float64(uv.Uint()), nil
case vk == reflect.Bool:
if uv.Bool() {
return 1, nil
}
return 0, nil
case vk >= reflect.Float32 && vk <= reflect.Float64:
return uv.Float(), nil
case vk >= reflect.Complex64 && vk <= reflect.Complex128:
return real(uv.Complex()), nil
case vk == reflect.String:
r, err := strconv.ParseFloat(uv.String(), 64)
if err != nil {
return 0, err
}
return r, nil
default:
return 0, fmt.Errorf("got value %v of unsupported type %T", v, v)
}
}
// ToFloat32 robustly converts to a float32 any basic elemental type
// (including pointers to such) using a big type switch organized for
// greatest efficiency, only falling back on reflection when all else fails.
func ToFloat32(v any) (float32, error) {
switch vt := v.(type) {
case float32:
return vt, nil
case *float32:
if vt == nil {
return 0, fmt.Errorf("got nil *float32")
}
return *vt, nil
case float64:
return float32(vt), nil
case *float64:
if vt == nil {
return 0, fmt.Errorf("got nil *float64")
}
return float32(*vt), nil
case int:
return float32(vt), nil
case *int:
if vt == nil {
return 0, fmt.Errorf("got nil *int")
}
return float32(*vt), nil
case int32:
return float32(vt), nil
case *int32:
if vt == nil {
return 0, fmt.Errorf("got nil *int32")
}
return float32(*vt), nil
case int64:
return float32(vt), nil
case *int64:
if vt == nil {
return 0, fmt.Errorf("got nil *int64")
}
return float32(*vt), nil
case uint8:
return float32(vt), nil
case *uint8:
if vt == nil {
return 0, fmt.Errorf("got nil *uint8")
}
return float32(*vt), nil
case bool:
if vt {
return 1, nil
}
return 0, nil
case *bool:
if vt == nil {
return 0, fmt.Errorf("got nil *bool")
}
if *vt {
return 1, nil
}
return 0, nil
case string:
r, err := strconv.ParseFloat(vt, 32)
if err != nil {
return 0, err
}
return float32(r), nil
case *string:
if vt == nil {
return 0, fmt.Errorf("got nil *string")
}
r, err := strconv.ParseFloat(*vt, 32)
if err != nil {
return 0, err
}
return float32(r), nil
case int8:
return float32(vt), nil
case *int8:
if vt == nil {
return 0, fmt.Errorf("got nil *int8")
}
return float32(*vt), nil
case int16:
return float32(vt), nil
case *int16:
if vt == nil {
return 0, fmt.Errorf("got nil *int8")
}
return float32(*vt), nil
case uint:
return float32(vt), nil
case *uint:
if vt == nil {
return 0, fmt.Errorf("got nil *uint")
}
return float32(*vt), nil
case uint16:
return float32(vt), nil
case *uint16:
if vt == nil {
return 0, fmt.Errorf("got nil *uint16")
}
return float32(*vt), nil
case uint32:
return float32(vt), nil
case *uint32:
if vt == nil {
return 0, fmt.Errorf("got nil *uint32")
}
return float32(*vt), nil
case uint64:
return float32(vt), nil
case *uint64:
if vt == nil {
return 0, fmt.Errorf("got nil *uint64")
}
return float32(*vt), nil
case uintptr:
return float32(vt), nil
case *uintptr:
if vt == nil {
return 0, fmt.Errorf("got nil *uintptr")
}
return float32(*vt), nil
}
// then fall back on reflection
uv := Underlying(reflect.ValueOf(v))
if IsNil(uv) {
return 0, fmt.Errorf("got nil value of type %T", v)
}
vk := uv.Kind()
switch {
case vk >= reflect.Int && vk <= reflect.Int64:
return float32(uv.Int()), nil
case vk >= reflect.Uint && vk <= reflect.Uint64:
return float32(uv.Uint()), nil
case vk == reflect.Bool:
if uv.Bool() {
return 1, nil
}
return 0, nil
case vk >= reflect.Float32 && vk <= reflect.Float64:
return float32(uv.Float()), nil
case vk >= reflect.Complex64 && vk <= reflect.Complex128:
return float32(real(uv.Complex())), nil
case vk == reflect.String:
r, err := strconv.ParseFloat(uv.String(), 32)
if err != nil {
return 0, err
}
return float32(r), nil
default:
return 0, fmt.Errorf("got value %v of unsupported type %T", v, v)
}
}
// ToString robustly converts anything to a String
// using a big type switch organized for greatest efficiency.
// First checks for string or []byte and returns that immediately,
// then checks for the Stringer interface as the preferred conversion
// (e.g., for enums), and then falls back on strconv calls for numeric types.
// If everything else fails, it uses fmt.Sprintf("%v") which always works,
// so there is no need for an error return value. It returns "nil" for any nil
// pointers, and byte is converted as string(byte), not the decimal representation.
func ToString(v any) string {
nilstr := "nil"
// TODO: this reflection is unideal for performance, but we need it to prevent panics,
// so this whole "greatest efficiency" type switch is kind of pointless.
rv := reflect.ValueOf(v)
if IsNil(rv) {
return nilstr
}
switch vt := v.(type) {
case string:
return vt
case *string:
if vt == nil {
return nilstr
}
return *vt
case []byte:
return string(vt)
case *[]byte:
if vt == nil {
return nilstr
}
return string(*vt)
}
if stringer, ok := v.(fmt.Stringer); ok {
return stringer.String()
}
switch vt := v.(type) {
case bool:
if vt {
return "true"
}
return "false"
case *bool:
if vt == nil {
return nilstr
}
if *vt {
return "true"
}
return "false"
case int:
return strconv.FormatInt(int64(vt), 10)
case *int:
if vt == nil {
return nilstr
}
return strconv.FormatInt(int64(*vt), 10)
case int32:
return strconv.FormatInt(int64(vt), 10)
case *int32:
if vt == nil {
return nilstr
}
return strconv.FormatInt(int64(*vt), 10)
case int64:
return strconv.FormatInt(vt, 10)
case *int64:
if vt == nil {
return nilstr
}
return strconv.FormatInt(*vt, 10)
case uint8: // byte, converts as string char
return string(vt)
case *uint8:
if vt == nil {
return nilstr
}
return string(*vt)
case float64:
return strconv.FormatFloat(vt, 'G', -1, 64)
case *float64:
if vt == nil {
return nilstr
}
return strconv.FormatFloat(*vt, 'G', -1, 64)
case float32:
return strconv.FormatFloat(float64(vt), 'G', -1, 32)
case *float32:
if vt == nil {
return nilstr
}
return strconv.FormatFloat(float64(*vt), 'G', -1, 32)
case uintptr:
return fmt.Sprintf("%#x", uintptr(vt))
case *uintptr:
if vt == nil {
return nilstr
}
return fmt.Sprintf("%#x", uintptr(*vt))
case int8:
return strconv.FormatInt(int64(vt), 10)
case *int8:
if vt == nil {
return nilstr
}
return strconv.FormatInt(int64(*vt), 10)
case int16:
return strconv.FormatInt(int64(vt), 10)
case *int16:
if vt == nil {
return nilstr
}
return strconv.FormatInt(int64(*vt), 10)
case uint:
return strconv.FormatInt(int64(vt), 10)
case *uint:
if vt == nil {
return nilstr
}
return strconv.FormatInt(int64(*vt), 10)
case uint16:
return strconv.FormatInt(int64(vt), 10)
case *uint16:
if vt == nil {
return nilstr
}
return strconv.FormatInt(int64(*vt), 10)
case uint32:
return strconv.FormatInt(int64(vt), 10)
case *uint32:
if vt == nil {
return nilstr
}
return strconv.FormatInt(int64(*vt), 10)
case uint64:
return strconv.FormatInt(int64(vt), 10)
case *uint64:
if vt == nil {
return nilstr
}
return strconv.FormatInt(int64(*vt), 10)
case complex64:
return strconv.FormatFloat(float64(real(vt)), 'G', -1, 32) + "," + strconv.FormatFloat(float64(imag(vt)), 'G', -1, 32)
case *complex64:
if vt == nil {
return nilstr
}
return strconv.FormatFloat(float64(real(*vt)), 'G', -1, 32) + "," + strconv.FormatFloat(float64(imag(*vt)), 'G', -1, 32)
case complex128:
return strconv.FormatFloat(real(vt), 'G', -1, 64) + "," + strconv.FormatFloat(imag(vt), 'G', -1, 64)
case *complex128:
if vt == nil {
return nilstr
}
return strconv.FormatFloat(real(*vt), 'G', -1, 64) + "," + strconv.FormatFloat(imag(*vt), 'G', -1, 64)
}
// then fall back on reflection
uv := Underlying(rv)
if IsNil(uv) {
return nilstr
}
vk := uv.Kind()
switch {
case vk >= reflect.Int && vk <= reflect.Int64:
return strconv.FormatInt(uv.Int(), 10)
case vk >= reflect.Uint && vk <= reflect.Uint64:
return strconv.FormatUint(uv.Uint(), 10)
case vk == reflect.Bool:
return strconv.FormatBool(uv.Bool())
case vk >= reflect.Float32 && vk <= reflect.Float64:
return strconv.FormatFloat(uv.Float(), 'G', -1, 64)
case vk >= reflect.Complex64 && vk <= reflect.Complex128:
cv := uv.Complex()
rv := strconv.FormatFloat(real(cv), 'G', -1, 64) + "," + strconv.FormatFloat(imag(cv), 'G', -1, 64)
return rv
case vk == reflect.String:
return uv.String()
case vk == reflect.Slice:
eltyp := SliceElementType(v)
if eltyp.Kind() == reflect.Uint8 { // []byte
return string(v.([]byte))
}
fallthrough
default:
return fmt.Sprintf("%v", v)
}
}
// ToStringPrec robustly converts anything to a String using given precision
// for converting floating values; using a value like 6 truncates the
// nuisance random imprecision of actual floating point values due to the
// fact that they are represented with binary bits.
// Otherwise is identical to ToString for any other cases.
func ToStringPrec(v any, prec int) string {
nilstr := "nil"
switch vt := v.(type) {
case string:
return vt
case *string:
if vt == nil {
return nilstr
}
return *vt
case []byte:
return string(vt)
case *[]byte:
if vt == nil {
return nilstr
}
return string(*vt)
}
if stringer, ok := v.(fmt.Stringer); ok {
return stringer.String()
}
switch vt := v.(type) {
case float64:
return strconv.FormatFloat(vt, 'G', prec, 64)
case *float64:
if vt == nil {
return nilstr
}
return strconv.FormatFloat(*vt, 'G', prec, 64)
case float32:
return strconv.FormatFloat(float64(vt), 'G', prec, 32)
case *float32:
if vt == nil {
return nilstr
}
return strconv.FormatFloat(float64(*vt), 'G', prec, 32)
case complex64:
return strconv.FormatFloat(float64(real(vt)), 'G', prec, 32) + "," + strconv.FormatFloat(float64(imag(vt)), 'G', prec, 32)
case *complex64:
if vt == nil {
return nilstr
}
return strconv.FormatFloat(float64(real(*vt)), 'G', prec, 32) + "," + strconv.FormatFloat(float64(imag(*vt)), 'G', prec, 32)
case complex128:
return strconv.FormatFloat(real(vt), 'G', prec, 64) + "," + strconv.FormatFloat(imag(vt), 'G', prec, 64)
case *complex128:
if vt == nil {
return nilstr
}
return strconv.FormatFloat(real(*vt), 'G', prec, 64) + "," + strconv.FormatFloat(imag(*vt), 'G', prec, 64)
}
return ToString(v)
}
// SetRobust robustly sets the 'to' value from the 'from' value.
// The 'to' value must be a pointer. It copies slices and maps robustly,
// and it can set a struct, slice, or map from a JSON-formatted string
// value. It also handles many other cases, so it is unlikely to fail.
//
// Note that maps are not reset prior to setting, whereas slices are
// set to be fully equivalent to the source slice.
func SetRobust(to, from any) error {
rto := reflect.ValueOf(to)
pto := UnderlyingPointer(rto)
if IsNil(pto) {
return fmt.Errorf("got nil destination value")
}
pito := pto.Interface()
totyp := pto.Elem().Type()
tokind := totyp.Kind()
if !pto.Elem().CanSet() {
return fmt.Errorf("destination value cannot be set; it must be a variable or field, not a const or tmp or other value that cannot be set (value: %v of type %T)", pto, pto)
}
// first we do the generic AssignableTo case
if rto.Kind() == reflect.Pointer {
fv := reflect.ValueOf(from)
if fv.IsValid() {
if fv.Type().AssignableTo(totyp) {
pto.Elem().Set(fv)
return nil
}
ufvp := UnderlyingPointer(fv)
if ufvp.IsValid() && ufvp.Type().AssignableTo(totyp) {
pto.Elem().Set(ufvp)
return nil
}
ufv := ufvp.Elem()
if ufv.IsValid() && ufv.Type().AssignableTo(totyp) {
pto.Elem().Set(ufv)
return nil
}
} else {
return nil
}
}
if sa, ok := pito.(SetAnyer); ok {
err := sa.SetAny(from)
if err != nil {
return err
}
return nil
}
if ss, ok := pito.(SetStringer); ok {
if s, ok := from.(string); ok {
err := ss.SetString(s)
if err != nil {
return err
}
return nil
}
}
if es, ok := pito.(enums.EnumSetter); ok {
if en, ok := from.(enums.Enum); ok {
es.SetInt64(en.Int64())
return nil
}
if str, ok := from.(string); ok {
return es.SetString(str)
}
fm, err := ToInt(from)
if err != nil {
return err
}
es.SetInt64(fm)
return nil
}
if bv, ok := pito.(bools.BoolSetter); ok {
fb, err := ToBool(from)
if err != nil {
return err
}
bv.SetBool(fb)
return nil
}
if td, ok := pito.(*time.Duration); ok {
if fs, ok := from.(string); ok {
fd, err := time.ParseDuration(fs)
if err != nil {
return err
}
*td = fd
return nil
}
}
if fc, err := colors.FromAny(from); err == nil {
switch c := pito.(type) {
case *color.RGBA:
*c = fc
return nil
case *image.Uniform:
c.C = fc
return nil
case SetColorer:
c.SetColor(fc)
return nil
case *image.Image:
*c = colors.Uniform(fc)
return nil
}
}
ftyp := NonPointerType(reflect.TypeOf(from))
switch {
case tokind >= reflect.Int && tokind <= reflect.Int64:
fm, err := ToInt(from)
if err != nil {
return err
}
pto.Elem().Set(reflect.ValueOf(fm).Convert(totyp))
return nil
case tokind >= reflect.Uint && tokind <= reflect.Uint64:
fm, err := ToInt(from)
if err != nil {
return err
}
pto.Elem().Set(reflect.ValueOf(fm).Convert(totyp))
return nil
case tokind == reflect.Bool:
fm, err := ToBool(from)
if err != nil {
return err
}
pto.Elem().Set(reflect.ValueOf(fm).Convert(totyp))
return nil
case tokind >= reflect.Float32 && tokind <= reflect.Float64:
fm, err := ToFloat(from)
if err != nil {
return err
}
pto.Elem().Set(reflect.ValueOf(fm).Convert(totyp))
return nil
case tokind == reflect.String:
fm := ToString(from)
pto.Elem().Set(reflect.ValueOf(fm).Convert(totyp))
return nil
case tokind == reflect.Struct:
if ftyp.Kind() == reflect.String {
err := json.Unmarshal([]byte(ToString(from)), to) // todo: this is not working -- see what marshal says, etc
if err != nil {
marsh, _ := json.Marshal(to)
return fmt.Errorf("error setting struct from string: %w (example format for string: %s)", err, string(marsh))
}
return nil
}
case tokind == reflect.Slice:
if ftyp.Kind() == reflect.String {
err := json.Unmarshal([]byte(ToString(from)), to)
if err != nil {
marsh, _ := json.Marshal(to)
return fmt.Errorf("error setting slice from string: %w (example format for string: %s)", err, string(marsh))
}
return nil
}
return CopySliceRobust(to, from)
case tokind == reflect.Map:
if ftyp.Kind() == reflect.String {
err := json.Unmarshal([]byte(ToString(from)), to)
if err != nil {
marsh, _ := json.Marshal(to)
return fmt.Errorf("error setting map from string: %w (example format for string: %s)", err, string(marsh))
}
return nil
}
return CopyMapRobust(to, from)
}
tos := elide.End(fmt.Sprintf("%v", to), 40)
fms := elide.End(fmt.Sprintf("%v", from), 40)
return fmt.Errorf("unable to set value %s of type %T (using underlying type: %s) from value %s of type %T (using underlying type: %s): not a supported type pair and direct assigning is not possible", tos, to, totyp.String(), fms, from, LongTypeName(Underlying(reflect.ValueOf(from)).Type()))
}
// 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 runes provides a small subset of functions for rune slices that are found in the
strings and bytes standard packages. For rendering and other logic, it is best to
keep raw data in runes, and not having to convert back and forth to bytes or strings
is more efficient.
These are largely copied from the strings or bytes packages.
*/
package runes
import (
"unicode"
"unicode/utf8"
"cogentcore.org/core/base/slicesx"
)
// EqualFold reports whether s and t are equal under Unicode case-folding.
// copied from strings.EqualFold
func EqualFold(s, t []rune) bool {
for len(s) > 0 && len(t) > 0 {
// Extract first rune from each string.
var sr, tr rune
sr, s = s[0], s[1:]
tr, t = t[0], t[1:]
// If they match, keep going; if not, return false.
// Easy case.
if tr == sr {
continue
}
// Make sr < tr to simplify what follows.
if tr < sr {
tr, sr = sr, tr
}
// Fast check for ASCII.
if tr < utf8.RuneSelf {
// ASCII only, sr/tr must be upper/lower case
if 'A' <= sr && sr <= 'Z' && tr == sr+'a'-'A' {
continue
}
return false
}
// General case. SimpleFold(x) returns the next equivalent rune > x
// or wraps around to smaller values.
r := unicode.SimpleFold(sr)
for r != sr && r < tr {
r = unicode.SimpleFold(r)
}
if r == tr {
continue
}
return false
}
// One string is empty. Are both?
return len(s) == len(t)
}
// Index returns the index of given rune string in the text, returning -1 if not found.
func Index(txt, find []rune) int {
fsz := len(find)
if fsz == 0 {
return -1
}
tsz := len(txt)
if tsz < fsz {
return -1
}
mn := tsz - fsz
for i := 0; i <= mn; i++ {
found := true
for j := range find {
if txt[i+j] != find[j] {
found = false
break
}
}
if found {
return i
}
}
return -1
}
// IndexFold returns the index of given rune string in the text, using case folding
// (i.e., case insensitive matching). Returns -1 if not found.
func IndexFold(txt, find []rune) int {
fsz := len(find)
if fsz == 0 {
return -1
}
tsz := len(txt)
if tsz < fsz {
return -1
}
mn := tsz - fsz
for i := 0; i <= mn; i++ {
if EqualFold(txt[i:i+fsz], find) {
return i
}
}
return -1
}
// Repeat returns a new rune slice consisting of count copies of b.
//
// It panics if count is negative or if
// the result of (len(b) * count) overflows.
func Repeat(r []rune, count int) []rune {
if count == 0 {
return []rune{}
}
// Since we cannot return an error on overflow,
// we should panic if the repeat will generate
// an overflow.
// See Issue golang.org/issue/16237.
if count < 0 {
panic("runes: negative Repeat count")
} else if len(r)*count/count != len(r) {
panic("runes: Repeat count causes overflow")
}
nb := make([]rune, len(r)*count)
bp := copy(nb, r)
for bp < len(nb) {
copy(nb[bp:], nb[:bp])
bp *= 2
}
return nb
}
// SetFromBytes sets slice of runes from given slice of bytes,
// using efficient memory reallocation of existing slice.
// returns potentially modified slice: use assign to update.
func SetFromBytes(rs []rune, s []byte) []rune {
n := utf8.RuneCount(s)
rs = slicesx.SetLength(rs, n)
i := 0
for len(s) > 0 {
r, l := utf8.DecodeRune(s)
rs[i] = r
i++
s = s[l:]
}
return rs
}
// Copyright (c) 2024, 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 slicesx provides additional slice functions
// beyond those in the standard [slices] package.
package slicesx
import (
"slices"
"unsafe"
)
// GrowTo increases the slice's capacity, if necessary,
// so that it can hold at least n elements.
func GrowTo[S ~[]E, E any](s S, n int) S {
if n < 0 {
panic("cannot be negative")
}
if n -= cap(s); n > 0 {
s = append(s[:cap(s)], make([]E, n)...)[:len(s)]
}
return s
}
// SetLength sets the length of the given slice,
// re-using and preserving existing values to the extent possible.
func SetLength[E any](s []E, n int) []E {
if len(s) == n {
return s
}
if s == nil {
return make([]E, n)
}
if cap(s) < n {
s = GrowTo(s, n)
}
s = s[:n]
return s
}
// CopyFrom efficiently copies from src into dest, using SetLength
// to ensure the destination has sufficient capacity, and returns
// the destination (which may have changed location as a result).
func CopyFrom[E any](dest []E, src []E) []E {
dest = SetLength(dest, len(src))
copy(dest, src)
return dest
}
// Move moves the element in the given slice at the given
// old position to the given new position and returns the
// resulting slice.
func Move[E any](s []E, from, to int) []E {
temp := s[from]
s = slices.Delete(s, from, from+1)
s = slices.Insert(s, to, temp)
return s
}
// Swap swaps the elements at the given two indices in the given slice.
func Swap[E any](s []E, i, j int) {
s[i], s[j] = s[j], s[i]
}
// As converts a slice of the given type to a slice of the other given type.
// The underlying types of the slice elements must be equivalent.
func As[F, T any](s []F) []T {
as := make([]T, len(s))
for i, v := range s {
as[i] = any(v).(T)
}
return as
}
// Search returns the index of the item in the given slice that matches the target
// according to the given match function, using the given optional starting index
// to optimize the search by searching bidirectionally outward from given index.
// This is much faster when you have some idea about where the item might be.
// If no start index is given, it starts in the middle, which is a good default.
// It returns -1 if no item matching the match function is found.
func Search[E any](slice []E, match func(e E) bool, startIndex ...int) int {
n := len(slice)
if n == 0 {
return -1
}
si := -1
if len(startIndex) > 0 {
si = startIndex[0]
}
if si < 0 {
si = n / 2
}
if si == 0 {
for idx, e := range slice {
if match(e) {
return idx
}
}
} else {
if si >= n {
si = n - 1
}
ui := si + 1
di := si
upo := false
for {
if !upo && ui < n {
if match(slice[ui]) {
return ui
}
ui++
} else {
upo = true
}
if di >= 0 {
if match(slice[di]) {
return di
}
di--
} else if upo {
break
}
}
}
return -1
}
// ToBytes returns the underlying bytes of given slice.
// for items not in a slice, make one of length 1.
// This is copied from webgpu.
func ToBytes[E any](src []E) []byte {
l := uintptr(len(src))
if l == 0 {
return nil
}
elmSize := unsafe.Sizeof(src[0])
return unsafe.Slice((*byte)(unsafe.Pointer(&src[0])), l*elmSize)
}
// Copyright (c) 2024, 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 sshclient
import (
"bytes"
"fmt"
"log"
"log/slog"
"os"
"path/filepath"
"strings"
"github.com/bramvdbogaerde/go-scp"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
)
// Client represents a persistent connection to an ssh host.
// Commands are run by creating [ssh.Session]s from this client.
type Client struct {
Config
// ssh client is present (non-nil) after a successful Connect
Client *ssh.Client
// keeps track of sessions that are being waited upon
Sessions map[string]*ssh.Session
// sessionCounter increments number of Sessions added
// over the lifetime of the Client.
sessionCounter int
// scpClient manages scp file copying
scpClient *scp.Client
}
// NewClient returns a new Client using given [Config] configuration
// parameters.
func NewClient(cfg *Config) *Client {
cl := &Client{Config: *cfg}
return cl
}
// Close terminates any open Sessions and then closes
// the Client connection.
func (cl *Client) Close() {
cl.CloseSessions()
if cl.Client != nil {
cl.Client.Close()
}
cl.scpClient = nil
cl.Client = nil
}
// CloseSessions terminates any open Sessions that are
// still Waiting for the associated process to finish.
func (cl *Client) CloseSessions() {
if cl.Sessions == nil {
return
}
for _, ses := range cl.Sessions {
ses.Close()
}
cl.Sessions = nil
}
// Connect connects to given host, which can either be just the host
// or user@host. If host is empty, the Config default host will be used
// if non-empty, or an error is returned.
// If successful, creates a Client that can be used for
// future sessions. Otherwise, returns error.
// This updates the Host (and User) fields in the config, for future
// reference.
func (cl *Client) Connect(host string) error {
if host == "" {
if cl.Host == "" {
return fmt.Errorf("ssh: Connect host is empty and no default host set in Config")
}
host = cl.Host
}
atidx := strings.Index(host, "@")
if atidx > 0 {
cl.User.User = host[:atidx]
cl.Host = host[atidx+1:]
host = cl.Host
} else {
cl.Host = host
}
if cl.User.KeyPath == "" {
return fmt.Errorf("ssh: key path (%q) is empty -- must be set", cl.User.KeyPath)
}
fn := filepath.Join(cl.User.KeyPath, cl.User.KeyFile)
key, err := os.ReadFile(fn)
if err != nil {
return fmt.Errorf("ssh: unable to read private key from: %q %v", fn, err)
}
// Create the Signer for this private key.
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return fmt.Errorf("ssh: unable to parse private key from: %q %v", fn, err)
}
// more info: https://gist.github.com/Skarlso/34321a230cf0245018288686c9e70b2d
hostKeyCallback, err := knownhosts.New(filepath.Join(cl.User.KeyPath, "known_hosts"))
if err != nil {
log.Fatal("ssh: could not create hostkeycallback function: ", err)
}
config := &ssh.ClientConfig{
User: cl.User.User,
Auth: []ssh.AuthMethod{
// Use the PublicKeys method for remote authentication.
ssh.PublicKeys(signer),
},
HostKeyCallback: hostKeyCallback,
}
// Connect to the remote server and perform the SSH handshake.
client, err := ssh.Dial("tcp", host+":22", config)
if err != nil {
err = fmt.Errorf("ssh: unable to connect to %s as user %s: %v", host, cl.User, err)
return err
}
cl.Sessions = make(map[string]*ssh.Session)
cl.Client = client
cl.GetHomeDir()
return nil
}
// NewSession creates a new session, sets its input / outputs based on
// config. Only works if Client already connected.
func (cl *Client) NewSession(interact bool) (*ssh.Session, error) {
if cl.Client == nil {
return nil, fmt.Errorf("ssh: no client, must Connect first")
}
ses, err := cl.Client.NewSession()
if err != nil {
return nil, err
}
ses.Stdout = cl.StdIO.Out
ses.Stderr = cl.StdIO.Err
// ses.Stdin = nil // cl.StdIO.In // todo: cannot set this like this!
if interact {
modes := ssh.TerminalModes{
ssh.ECHO: 0, // disable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
err := ses.RequestPty("xterm", 40, 80, modes)
if err != nil {
slog.Error(err.Error())
}
}
return ses, nil
}
// WaitSession adds the session to list of open sessions,
// and calls Wait on it.
// It should be called in a goroutine, and will only return
// when the command is completed or terminated.
// The given name is used to save the session
// in a map, for later reference. If left blank,
// the name will be a number that increases with each
// such session created.
func (cl *Client) WaitSession(name string, ses *ssh.Session) error {
if name == "" {
name = fmt.Sprintf("%d", cl.sessionCounter)
}
cl.Sessions[name] = ses
cl.sessionCounter++
return ses.Wait()
}
// GetHomeDir runs "pwd" on the host to get the users home dir,
// called right after connecting.
func (cl *Client) GetHomeDir() error {
ses, err := cl.NewSession(false)
if err != nil {
return err
}
defer ses.Close()
buf := &bytes.Buffer{}
ses.Stdout = buf
err = ses.Run("pwd")
if err != nil {
return fmt.Errorf("ssh: unable to get home directory through pwd: %v", err)
}
cl.HomeDir = strings.TrimSpace(buf.String())
cl.Dir = cl.HomeDir
fmt.Println("home directory:", cl.HomeDir)
return nil
}
// Copyright (c) 2024, 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 sshclient
import (
"os/user"
"path/filepath"
"cogentcore.org/core/base/exec"
)
// User holds user-specific ssh connection configuration settings,
// including Key info.
type User struct {
// user name to connect with
User string
// path to ssh keys: ~/.ssh by default
KeyPath string
// name of ssh key file in KeyPath: .pub is appended for public key
KeyFile string `default:"id_rsa"`
}
func (us *User) Defaults() {
us.KeyFile = "id_rsa"
usr, err := user.Current()
if err == nil {
us.User = usr.Username
us.KeyPath = filepath.Join(usr.HomeDir, ".ssh")
}
}
// Config contains the configuration information that controls
// the behavior of ssh connections and commands. It is used
// to establish a Client connection to a remote host.
// It builds on the shared settings in [exec.Config]
type Config struct {
exec.Config
// user name and ssh key info
User User
// host name / ip address to connect to. can be blank, in which
// case it must be specified in the Client.Connect call.
Host string
// home directory of user on remote host,
// which is captured at initial connection time.
HomeDir string
// ScpPath is the path to the `scp` executable on the host,
// for copying files between local and remote host.
// Defaults to /usr/bin/scp
ScpPath string `default:"/usr/bin/scp"`
}
// NewConfig returns a new ssh Config based on given
// [exec.Config] parameters.
func NewConfig(cfg *exec.Config) *Config {
c := &Config{Config: *cfg}
c.User.Defaults()
c.Dir = "" // start empty until we get homedir
c.ScpPath = "/usr/bin/scp"
return c
}
// Copyright (c) 2024, 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 sshclient
import (
"bytes"
"os"
"path/filepath"
"strings"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/base/logx"
"golang.org/x/crypto/ssh"
)
// Exec executes the command, piping its stdout and stderr to the config
// writers. If the command fails, it will return an error with the command output.
// The given cmd and args may include references
// to environment variables in $FOO format, in which case these will be
// expanded before the command is run.
//
// Ran reports if the command ran (rather than was not found or not executable).
// Code reports the exit code the command returned if it ran. If err == nil, ran
// is always true and code is always 0.
func (cl *Client) Exec(sio *exec.StdIOState, start, output bool, cmd string, args ...string) (string, error) {
ses, err := cl.NewSession(true)
if err != nil {
return "", err
}
defer ses.Close()
expand := func(s string) string {
s2, ok := cl.Env[s]
if ok {
return s2
}
return os.Getenv(s)
}
// todo: what does this do?
cmd = os.Expand(cmd, expand)
for i := range args {
args[i] = os.Expand(args[i], expand)
}
return cl.run(ses, sio, start, output, cmd, args...)
}
func (cl *Client) run(ses *ssh.Session, sio *exec.StdIOState, start, output bool, cmd string, args ...string) (string, error) {
// todo: env is established on connection!
// for k, v := range cl.Env {
// ses.Env = append(ses.Env, k+"="+v)
// }
var err error
out := ""
ses.Stderr = sio.Err // note: no need to save previous b/c not retained
ses.Stdout = sio.Out
if cl.Echo != nil {
cl.PrintCmd(cmd+" "+strings.Join(args, " "), err)
}
if exec.IsPipe(sio.In) {
ses.Stdin = sio.In
}
cdto := ""
cmds := cmd + " " + strings.Join(args, " ")
if cl.Dir != "" {
if cmd == "cd" {
if len(args) > 0 {
cdto = args[0]
} else {
cdto = cl.HomeDir
}
}
cmds = `cd '` + cl.Dir + `'; ` + cmds
}
if !cl.PrintOnly {
switch {
case start:
err = ses.Start(cmds)
go func() {
ses.Wait()
sio.PopToStart()
}()
case output:
buf := &bytes.Buffer{}
ses.Stdout = buf
err = ses.Run(cmds)
if sio.Out != nil {
sio.Out.Write(buf.Bytes())
}
out = strings.TrimSuffix(buf.String(), "\n")
default:
err = ses.Run(cmds)
}
// we must call InitColor after calling a system command
// TODO(kai): maybe figure out a better solution to this
// or expand this list
if cmd == "cp" || cmd == "ls" || cmd == "mv" {
logx.InitColor()
}
if cdto != "" {
if filepath.IsAbs(cdto) {
cl.Dir = filepath.Clean(cdto)
} else {
cl.Dir = filepath.Clean(filepath.Join(cl.Dir, cdto))
}
}
}
return out, err
}
// Copyright (c) 2024, 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 sshclient
import (
"cogentcore.org/core/base/exec"
)
// Run runs given command, using config input / outputs.
// Must have already made a successful Connect.
func (cl *Client) Run(sio *exec.StdIOState, cmd string, args ...string) error {
_, err := cl.Exec(sio, false, false, cmd, args...)
return err
}
// Start starts the given command with arguments.
func (cl *Client) Start(sio *exec.StdIOState, cmd string, args ...string) error {
_, err := cl.Exec(sio, true, false, cmd, args...)
return err
}
// Output runs the command and returns the text from stdout.
func (cl *Client) Output(sio *exec.StdIOState, cmd string, args ...string) (string, error) {
return cl.Exec(sio, false, true, cmd, args...)
}
// Copyright (c) 2024, 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 sshclient
import (
"context"
"io"
"log/slog"
"os"
"path/filepath"
"github.com/bramvdbogaerde/go-scp"
)
// CopyLocalFileToHost copies given local file to given host file
// on the already-connected remote host, using the 'scp' protocol.
// See ScpPath in Config for path to scp on remote host.
// If the host filename is not absolute (i.e, doesn't start with /)
// then the current Dir path on the Client is prepended to the target path.
// Use context.Background() for a basic context if none otherwise in use.
func (cl *Client) CopyLocalFileToHost(ctx context.Context, localFilename, hostFilename string) error {
f, err := os.Open(localFilename)
if err != nil {
return err
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
return err
}
return cl.CopyLocalToHost(ctx, f, stat.Size(), hostFilename)
}
// CopyLocalToHost copies given io.Reader source data to given filename
// on the already-connected remote host, using the 'scp' protocol.
// See ScpPath in Config for path to scp on remote host.
// The size must be given in advance for the scp protocol.
// If the host filename is not absolute (i.e, doesn't start with /)
// then the current Dir path on the Client is prepended to the target path.
// Use context.Background() for a basic context if none otherwise in use.
func (cl *Client) CopyLocalToHost(ctx context.Context, r io.Reader, size int64, hostFilename string) error {
if err := cl.mustScpClient(); err != nil {
return err
}
if !filepath.IsAbs(hostFilename) {
hostFilename = filepath.Join(cl.Dir, hostFilename)
}
return cl.scpClient.CopyPassThru(ctx, r, hostFilename, "0666", size, nil)
}
// CopyHostToLocalFile copies given filename on the already-connected remote host,
// to the local file using the 'scp' protocol.
// See ScpPath in Config for path to scp on remote host.
// If the host filename is not absolute (i.e, doesn't start with /)
// then the current Dir path on the Client is prepended to the target path.
// Use context.Background() for a basic context if none otherwise in use.
func (cl *Client) CopyHostToLocalFile(ctx context.Context, hostFilename, localFilename string) error {
f, err := os.Create(localFilename)
if err != nil {
return err
}
defer f.Close()
return cl.CopyHostToLocal(ctx, hostFilename, f)
}
// CopyHostToLocal copies given filename on the already-connected remote host,
// to the local io.Writer using the 'scp' protocol.
// See ScpPath in Config for path to scp on remote host.
// If the host filename is not absolute (i.e, doesn't start with /)
// then the current Dir path on the Client is prepended to the target path.
// Use context.Background() for a basic context if none otherwise in use.
func (cl *Client) CopyHostToLocal(ctx context.Context, hostFilename string, w io.Writer) error {
if err := cl.mustScpClient(); err != nil {
return err
}
if !filepath.IsAbs(hostFilename) {
hostFilename = filepath.Join(cl.Dir, hostFilename)
}
return cl.scpClient.CopyFromRemotePassThru(ctx, w, hostFilename, nil)
}
func (cl *Client) mustScpClient() error {
if cl.scpClient != nil {
return nil
}
scl, err := scp.NewClientBySSH(cl.Client)
if err != nil {
slog.Error(err.Error())
} else {
cl.scpClient = &scl
}
return err
}
// Copyright (c) 2024, 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 stack provides a generic stack implementation.
package stack
// Stack provides a generic stack using a slice.
type Stack[T any] []T
// Push pushes item(s) onto the stack.
func (st *Stack[T]) Push(it ...T) {
*st = append(*st, it...)
}
// Pop pops the top item off the stack.
// Returns nil / zero value if stack is empty.
func (st *Stack[T]) Pop() T {
n := len(*st)
if n == 0 {
var zv T
return zv
}
li := (*st)[n-1]
*st = (*st)[:n-1]
return li
}
// Peek returns the last element on the stack.
// Returns nil / zero value if stack is empty.
func (st *Stack[T]) Peek() T {
n := len(*st)
if n == 0 {
var zv T
return zv
}
return (*st)[n-1]
}
// Copyright (c) 2024, 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.
// Based on https://github.com/ettle/strcase
// Copyright (c) 2020 Liyan David Chang under the MIT License
package strcase
import (
"strings"
)
// Cases is an enum with all of the different string cases.
type Cases int32 //enums:enum
const (
// LowerCase is all lower case
LowerCase Cases = iota
// UpperCase is all UPPER CASE
UpperCase
// SnakeCase is lower_case_words_with_underscores
SnakeCase
// SNAKECase is UPPER_CASE_WORDS_WITH_UNDERSCORES
SNAKECase
// KebabCase is lower-case-words-with-dashes
KebabCase
// KEBABCase is UPPER-CASE-WORDS-WITH-DASHES
KEBABCase
// CamelCase is CapitalizedWordsConcatenatedTogether
CamelCase
// LowerCamelCase is capitalizedWordsConcatenatedTogether, with the first word lower case
LowerCamelCase
// TitleCase is Captitalized Words With Spaces
TitleCase
// SentenceCase is Lower case words with spaces, with the first word capitalized
SentenceCase
)
// To converts the given string to the given case.
func To(s string, c Cases) string {
switch c {
case LowerCase:
return strings.ToLower(s)
case UpperCase:
return strings.ToUpper(s)
case SnakeCase:
return ToSnake(s)
case SNAKECase:
return ToSNAKE(s)
case KebabCase:
return ToKebab(s)
case KEBABCase:
return ToKEBAB(s)
case CamelCase:
return ToCamel(s)
case LowerCamelCase:
return ToLowerCamel(s)
case TitleCase:
return ToTitle(s)
case SentenceCase:
return ToSentence(s)
}
return s
}
// Copyright (c) 2024, 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.
// Based on https://github.com/ettle/strcase
// Copyright (c) 2020 Liyan David Chang under the MIT License
package strcase
import (
"strings"
"unicode"
)
// WordCases is an enumeration of the ways to format a word.
type WordCases int32 //enums:enum -trim-prefix Word
const (
// WordOriginal indicates to preserve the original input case.
WordOriginal WordCases = iota
// WordLowerCase indicates to make all letters lower case (example).
WordLowerCase
// WordUpperCase indicates to make all letters upper case (EXAMPLE).
WordUpperCase
// WordTitleCase indicates to make only the first letter upper case (Example).
WordTitleCase
// WordCamelCase indicates to make only the first letter upper case, except
// in the first word, in which all letters are lower case (exampleText).
WordCamelCase
// WordSentenceCase indicates to make only the first letter upper case, and
// only for the first word (all other words have fully lower case letters).
WordSentenceCase
)
// ToWordCase converts the given input string to the given word case with the given delimiter.
// Pass 0 for delimeter to use no delimiter.
//
//nolint:gocyclo
func ToWordCase(input string, wordCase WordCases, delimiter rune) string {
input = strings.TrimSpace(input)
runes := []rune(input)
if len(runes) == 0 {
return ""
}
var b strings.Builder
b.Grow(len(input) + 4) // In case we need to write delimiters where they weren't before
firstWord := true
var skipIndexes []int
addWord := func(start, end int) {
// If you have nothing good to say, say nothing at all
if start == end || len(skipIndexes) == end-start {
skipIndexes = nil
return
}
// If you have something to say, start with a delimiter
if !firstWord && delimiter != 0 {
b.WriteRune(delimiter)
}
// Check to see if the entire word is an initialism for preserving initialism.
// Note we don't support preserving initialisms if they are followed
// by a number and we're not spliting before numbers.
if wordCase == WordTitleCase || wordCase == WordSentenceCase || (wordCase == WordCamelCase && !firstWord) {
allCaps := true
for i := start; i < end; i++ {
allCaps = allCaps && (isUpper(runes[i]) || !unicode.IsLetter(runes[i]))
}
if allCaps {
b.WriteString(string(runes[start:end]))
firstWord = false
return
}
}
skipIndex := 0
for i := start; i < end; i++ {
if len(skipIndexes) > 0 && skipIndex < len(skipIndexes) && i == skipIndexes[skipIndex] {
skipIndex++
continue
}
r := runes[i]
switch wordCase {
case WordUpperCase:
b.WriteRune(toUpper(r))
case WordLowerCase:
b.WriteRune(toLower(r))
case WordTitleCase:
if i == start {
b.WriteRune(toUpper(r))
} else {
b.WriteRune(toLower(r))
}
case WordCamelCase:
if !firstWord && i == start {
b.WriteRune(toUpper(r))
} else {
b.WriteRune(toLower(r))
}
case WordSentenceCase:
if firstWord && i == start {
b.WriteRune(toUpper(r))
} else {
b.WriteRune(toLower(r))
}
default:
b.WriteRune(r)
}
}
firstWord = false
skipIndexes = nil
}
var prev, curr rune
next := runes[0] // 0 length will have already returned so safe to index
wordStart := 0
for i := 0; i < len(runes); i++ {
prev = curr
curr = next
if i+1 == len(runes) {
next = 0
} else {
next = runes[i+1]
}
switch defaultSplitFn(prev, curr, next) {
case Skip:
skipIndexes = append(skipIndexes, i)
case Split:
addWord(wordStart, i)
wordStart = i
case SkipSplit:
addWord(wordStart, i)
wordStart = i + 1
}
}
if wordStart != len(runes) {
addWord(wordStart, len(runes))
}
return b.String()
}
// Code generated by "core generate"; DO NOT EDIT.
package strcase
import (
"cogentcore.org/core/enums"
)
var _CasesValues = []Cases{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// CasesN is the highest valid value for type Cases, plus one.
const CasesN Cases = 10
var _CasesValueMap = map[string]Cases{`LowerCase`: 0, `UpperCase`: 1, `SnakeCase`: 2, `SNAKECase`: 3, `KebabCase`: 4, `KEBABCase`: 5, `CamelCase`: 6, `LowerCamelCase`: 7, `TitleCase`: 8, `SentenceCase`: 9}
var _CasesDescMap = map[Cases]string{0: `LowerCase is all lower case`, 1: `UpperCase is all UPPER CASE`, 2: `SnakeCase is lower_case_words_with_underscores`, 3: `SNAKECase is UPPER_CASE_WORDS_WITH_UNDERSCORES`, 4: `KebabCase is lower-case-words-with-dashes`, 5: `KEBABCase is UPPER-CASE-WORDS-WITH-DASHES`, 6: `CamelCase is CapitalizedWordsConcatenatedTogether`, 7: `LowerCamelCase is capitalizedWordsConcatenatedTogether, with the first word lower case`, 8: `TitleCase is Captitalized Words With Spaces`, 9: `SentenceCase is Lower case words with spaces, with the first word capitalized`}
var _CasesMap = map[Cases]string{0: `LowerCase`, 1: `UpperCase`, 2: `SnakeCase`, 3: `SNAKECase`, 4: `KebabCase`, 5: `KEBABCase`, 6: `CamelCase`, 7: `LowerCamelCase`, 8: `TitleCase`, 9: `SentenceCase`}
// String returns the string representation of this Cases value.
func (i Cases) String() string { return enums.String(i, _CasesMap) }
// SetString sets the Cases value from its string representation,
// and returns an error if the string is invalid.
func (i *Cases) SetString(s string) error { return enums.SetString(i, s, _CasesValueMap, "Cases") }
// Int64 returns the Cases value as an int64.
func (i Cases) Int64() int64 { return int64(i) }
// SetInt64 sets the Cases value from an int64.
func (i *Cases) SetInt64(in int64) { *i = Cases(in) }
// Desc returns the description of the Cases value.
func (i Cases) Desc() string { return enums.Desc(i, _CasesDescMap) }
// CasesValues returns all possible values for the type Cases.
func CasesValues() []Cases { return _CasesValues }
// Values returns all possible values for the type Cases.
func (i Cases) Values() []enums.Enum { return enums.Values(_CasesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Cases) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Cases) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Cases") }
var _WordCasesValues = []WordCases{0, 1, 2, 3, 4, 5}
// WordCasesN is the highest valid value for type WordCases, plus one.
const WordCasesN WordCases = 6
var _WordCasesValueMap = map[string]WordCases{`Original`: 0, `LowerCase`: 1, `UpperCase`: 2, `TitleCase`: 3, `CamelCase`: 4, `SentenceCase`: 5}
var _WordCasesDescMap = map[WordCases]string{0: `WordOriginal indicates to preserve the original input case.`, 1: `WordLowerCase indicates to make all letters lower case (example).`, 2: `WordUpperCase indicates to make all letters upper case (EXAMPLE).`, 3: `WordTitleCase indicates to make only the first letter upper case (Example).`, 4: `WordCamelCase indicates to make only the first letter upper case, except in the first word, in which all letters are lower case (exampleText).`, 5: `WordSentenceCase indicates to make only the first letter upper case, and only for the first word (all other words have fully lower case letters).`}
var _WordCasesMap = map[WordCases]string{0: `Original`, 1: `LowerCase`, 2: `UpperCase`, 3: `TitleCase`, 4: `CamelCase`, 5: `SentenceCase`}
// String returns the string representation of this WordCases value.
func (i WordCases) String() string { return enums.String(i, _WordCasesMap) }
// SetString sets the WordCases value from its string representation,
// and returns an error if the string is invalid.
func (i *WordCases) SetString(s string) error {
return enums.SetString(i, s, _WordCasesValueMap, "WordCases")
}
// Int64 returns the WordCases value as an int64.
func (i WordCases) Int64() int64 { return int64(i) }
// SetInt64 sets the WordCases value from an int64.
func (i *WordCases) SetInt64(in int64) { *i = WordCases(in) }
// Desc returns the description of the WordCases value.
func (i WordCases) Desc() string { return enums.Desc(i, _WordCasesDescMap) }
// WordCasesValues returns all possible values for the type WordCases.
func WordCasesValues() []WordCases { return _WordCasesValues }
// Values returns all possible values for the type WordCases.
func (i WordCases) Values() []enums.Enum { return enums.Values(_WordCasesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i WordCases) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *WordCases) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "WordCases")
}
// 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 strcase
// FormatList returns a formatted version of the given list of items following these rules:
// - nil => ""
// - "Go" => "Go"
// - "Go", "Python" => "Go and Python"
// - "Go", "Python", "JavaScript" => "Go, Python, and JavaScript"
// - "Go", "Python", "JavaScript", "C" => "Go, Python, JavaScript, and C"
func FormatList(items ...string) string {
switch len(items) {
case 0:
return ""
case 1:
return items[0]
case 2:
return items[0] + " and " + items[1]
}
res := ""
for i, match := range items {
res += match
if i == len(items)-1 {
// last one, so do nothing
} else if i == len(items)-2 {
res += ", and "
} else {
res += ", "
}
}
return res
}
// Copyright (c) 2024, 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.
// Based on https://github.com/ettle/strcase
// Copyright (c) 2020 Liyan David Chang under the MIT License
package strcase
import "unicode"
// SplitAction defines if and how to split a string
type SplitAction int
const (
// Noop - Continue to next character
Noop SplitAction = iota
// Split - Split between words
// e.g. to split between wordsWithoutDelimiters
Split
// SkipSplit - Split the word and drop the character
// e.g. to split words with delimiters
SkipSplit
// Skip - Remove the character completely
Skip
)
//nolint:gocyclo
func defaultSplitFn(prev, curr, next rune) SplitAction {
// The most common case will be that it's just a letter so let lowercase letters return early since we know what they should do
if isLower(curr) {
return Noop
}
// Delimiters are _, -, ., and unicode spaces
// Handle . lower down as it needs to happen after number exceptions
if curr == '_' || curr == '-' || isSpace(curr) {
return SkipSplit
}
if isUpper(curr) {
if isLower(prev) {
// fooBar
return Split
} else if isUpper(prev) && isLower(next) {
// FOOBar
return Split
}
}
// Do numeric exceptions last to avoid perf penalty
if unicode.IsNumber(prev) {
// v4.3 is not split
if (curr == '.' || curr == ',') && unicode.IsNumber(next) {
return Noop
}
if !unicode.IsNumber(curr) && curr != '.' {
return Split
}
}
// While period is a default delimiter, keep it down here to avoid
// penalty for other delimiters
if curr == '.' {
return SkipSplit
}
return Noop
}
// Copyright (c) 2024, 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.
// Based on https://github.com/ettle/strcase
// Copyright (c) 2020 Liyan David Chang under the MIT License
// Package strcase provides functions for manipulating the case of strings (CamelCase, kebab-case,
// snake_case, Sentence case, etc). It is based on https://github.com/ettle/strcase, which is Copyright
// (c) 2020 Liyan David Chang under the MIT License. Its principle difference from other strcase packages
// is that it preserves acronyms in input text for CamelCase. Therefore, you must call [strings.ToLower]
// on any SCREAMING_INPUT_STRINGS before passing them to [ToCamel], [ToLowerCamel], [ToTitle], and [ToSentence].
package strcase
//go:generate core generate
// ToSnake returns words in snake_case (lower case words with underscores).
func ToSnake(s string) string {
return ToWordCase(s, WordLowerCase, '_')
}
// ToSNAKE returns words in SNAKE_CASE (upper case words with underscores).
// Also known as SCREAMING_SNAKE_CASE or UPPER_CASE.
func ToSNAKE(s string) string {
return ToWordCase(s, WordUpperCase, '_')
}
// ToKebab returns words in kebab-case (lower case words with dashes).
// Also known as dash-case.
func ToKebab(s string) string {
return ToWordCase(s, WordLowerCase, '-')
}
// ToKEBAB returns words in KEBAB-CASE (upper case words with dashes).
// Also known as SCREAMING-KEBAB-CASE or SCREAMING-DASH-CASE.
func ToKEBAB(s string) string {
return ToWordCase(s, WordUpperCase, '-')
}
// ToCamel returns words in CamelCase (capitalized words concatenated together).
// Also known as UpperCamelCase.
func ToCamel(s string) string {
return ToWordCase(s, WordTitleCase, 0)
}
// ToLowerCamel returns words in lowerCamelCase (capitalized words concatenated together,
// with first word lower case). Also known as camelCase or mixedCase.
func ToLowerCamel(s string) string {
return ToWordCase(s, WordCamelCase, 0)
}
// ToTitle returns words in Title Case (capitalized words with spaces).
func ToTitle(s string) string {
return ToWordCase(s, WordTitleCase, ' ')
}
// ToSentence returns words in Sentence case (lower case words with spaces, with the first word capitalized).
func ToSentence(s string) string {
return ToWordCase(s, WordSentenceCase, ' ')
}
// Copyright (c) 2024, 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.
// Based on https://github.com/ettle/strcase
// Copyright (c) 2020 Liyan David Chang under the MIT License
package strcase
import "unicode"
// Unicode functions, optimized for the common case of ascii
// No performance lost by wrapping since these functions get inlined by the compiler
func isUpper(r rune) bool {
return unicode.IsUpper(r)
}
func isLower(r rune) bool {
return unicode.IsLower(r)
}
func isNumber(r rune) bool {
if r >= '0' && r <= '9' {
return true
}
return unicode.IsNumber(r)
}
func isSpace(r rune) bool {
if r == ' ' || r == '\t' || r == '\n' || r == '\r' {
return true
} else if r < 128 {
return false
}
return unicode.IsSpace(r)
}
func toUpper(r rune) rune {
if r >= 'a' && r <= 'z' {
return r - 32
} else if r < 128 {
return r
}
return unicode.ToUpper(r)
}
func toLower(r rune) rune {
if r >= 'A' && r <= 'Z' {
return r + 32
} else if r < 128 {
return r
}
return unicode.ToLower(r)
}
// Copyright (c) 2024, 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 stringsx provides additional string functions
// beyond those in the standard [strings] package.
package stringsx
import (
"bytes"
"strings"
)
// TrimCR returns the string without any trailing \r carriage return
func TrimCR(s string) string {
n := len(s)
if n == 0 {
return s
}
if s[n-1] == '\r' {
return s[:n-1]
}
return s
}
// ByteTrimCR returns the byte string without any trailing \r carriage return
func ByteTrimCR(s []byte) []byte {
n := len(s)
if n == 0 {
return s
}
if s[n-1] == '\r' {
return s[:n-1]
}
return s
}
// SplitLines is a windows-safe version of [strings.Split](s, "\n")
// that removes any trailing \r carriage returns from the split lines.
func SplitLines(s string) []string {
ls := strings.Split(s, "\n")
for i, l := range ls {
ls[i] = TrimCR(l)
}
return ls
}
// ByteSplitLines is a windows-safe version of [bytes.Split](s, "\n")
// that removes any trailing \r carriage returns from the split lines.
func ByteSplitLines(s []byte) [][]byte {
ls := bytes.Split(s, []byte("\n"))
for i, l := range ls {
ls[i] = ByteTrimCR(l)
}
return ls
}
// InsertFirstUnique inserts the given string at the start of the given string slice
// while keeping the overall length to the given max value. If the item is already on
// the list, then it is moved to the top and not re-added (unique items only). This is
// useful for a list of recent items.
func InsertFirstUnique(strs *[]string, str string, max int) {
if *strs == nil {
*strs = make([]string, 0, max)
}
sz := len(*strs)
if sz > max {
*strs = (*strs)[:max]
}
for i, s := range *strs {
if s == str {
if i == 0 {
return
}
copy((*strs)[1:i+1], (*strs)[0:i])
(*strs)[0] = str
return
}
}
if sz >= max {
copy((*strs)[1:max], (*strs)[0:max-1])
(*strs)[0] = str
} else {
*strs = append(*strs, "")
if sz > 0 {
copy((*strs)[1:], (*strs)[0:sz])
}
(*strs)[0] = str
}
}
// Copyright (c) 2024, 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 tiered provides a type for a tiered set of objects.
package tiered
// Tiered represents a tiered set of objects of the same type.
// For example, this is frequently used to represent slices of
// functions that can be run at the first, normal, or final time.
type Tiered[T any] struct {
// First is the object that will be used first,
// before [Tiered.Normal] and [Tiered.Final].
First T
// Normal is the object that will be used at the normal
// time, after [Tiered.First] and before [Tiered.Final].
Normal T
// Final is the object that will be used last,
// after [Tiered.First] and [Tiered.Normal].
Final T
}
// 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)
}
// DoWith calls the given function with each tier of this tiered
// set and the other given tiered set, going through first, then
// normal, then final.
func (t *Tiered[T]) DoWith(other *Tiered[T], f func(*T, *T)) {
f(&t.First, &other.First)
f(&t.Normal, &other.Normal)
f(&t.Final, &other.Final)
}
// Copyright (c) 2024, 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 timer provides a simple wall-clock duration timer based on standard
// [time]. It accumulates total and average over multiple Start / Stop intervals.
package timer
import "time"
// Time manages the timer accumulated time and count
type Time struct {
// the most recent starting time
St time.Time
// the total accumulated time
Total time.Duration
// the number of start/stops
N int
}
// Reset resets the overall accumulated Total and N counters and start time to zero
func (t *Time) Reset() {
t.St = time.Time{}
t.Total = 0
t.N = 0
}
// Start starts the timer
func (t *Time) Start() {
t.St = time.Now()
}
// ResetStart reset then start the timer
func (t *Time) ResetStart() {
t.Reset()
t.Start()
}
// Stop stops the timer and accumulates the latest start - stop interval, and also returns it
func (t *Time) Stop() time.Duration {
if t.St.IsZero() {
t.Total = 0
t.N = 0
return 0
}
iv := time.Since(t.St)
t.Total += iv
t.N++
return iv
}
// Avg returns the average start / stop interval (assumes each was measuring the same thing).
func (t *Time) Avg() time.Duration {
if t.N == 0 {
return 0
}
return t.Total / time.Duration(t.N)
}
// Copyright (c) 2024, 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 tolassert provides functions for asserting the equality of numbers
// with tolerance (in other words, it checks whether numbers are about equal).
package tolassert
import (
"fmt"
"cogentcore.org/core/base/num"
"github.com/stretchr/testify/assert"
)
// Equal asserts that the given two numbers are about equal to each other,
// using a default tolerance of 0.001.
func Equal[T num.Float](t assert.TestingT, expected T, actual T, msgAndArgs ...any) bool {
if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
return EqualTol(t, expected, actual, 0.001, msgAndArgs...)
}
// EqualTol asserts that the given two numbers are about equal to each other,
// using the given tolerance value.
func EqualTol[T num.Float](t assert.TestingT, expected T, actual, tolerance T, msgAndArgs ...any) bool {
if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
if num.Abs(actual-expected) > tolerance {
return assert.Equal(t, expected, actual, msgAndArgs...)
}
return true
}
// EqualTolSlice asserts that the given two slices of numbers are about equal to each other,
// using the given tolerance value.
func EqualTolSlice[T num.Float](t assert.TestingT, expected, actual []T, tolerance T, msgAndArgs ...any) bool {
if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
errs := false
for i, ex := range expected {
a := actual[i]
if num.Abs(a-ex) > tolerance {
assert.Equal(t, expected, actual, fmt.Sprintf("index: %d", i))
errs = true
}
}
return errs
}
// Code generated by "core generate"; DO NOT EDIT.
package vcs
import (
"cogentcore.org/core/enums"
)
var _FileStatusValues = []FileStatus{0, 1, 2, 3, 4, 5, 6}
// FileStatusN is the highest valid value for type FileStatus, plus one.
const FileStatusN FileStatus = 7
var _FileStatusValueMap = map[string]FileStatus{`Untracked`: 0, `Stored`: 1, `Modified`: 2, `Added`: 3, `Deleted`: 4, `Conflicted`: 5, `Updated`: 6}
var _FileStatusDescMap = map[FileStatus]string{0: `Untracked means file is not under VCS control`, 1: `Stored means file is stored under VCS control, and has not been modified in working copy`, 2: `Modified means file is under VCS control, and has been modified in working copy`, 3: `Added means file has just been added to VCS but is not yet committed`, 4: `Deleted means file has been deleted from VCS`, 5: `Conflicted means file is in conflict -- has not been merged`, 6: `Updated means file has been updated in the remote but not locally`}
var _FileStatusMap = map[FileStatus]string{0: `Untracked`, 1: `Stored`, 2: `Modified`, 3: `Added`, 4: `Deleted`, 5: `Conflicted`, 6: `Updated`}
// String returns the string representation of this FileStatus value.
func (i FileStatus) String() string { return enums.String(i, _FileStatusMap) }
// SetString sets the FileStatus value from its string representation,
// and returns an error if the string is invalid.
func (i *FileStatus) SetString(s string) error {
return enums.SetString(i, s, _FileStatusValueMap, "FileStatus")
}
// Int64 returns the FileStatus value as an int64.
func (i FileStatus) Int64() int64 { return int64(i) }
// SetInt64 sets the FileStatus value from an int64.
func (i *FileStatus) SetInt64(in int64) { *i = FileStatus(in) }
// Desc returns the description of the FileStatus value.
func (i FileStatus) Desc() string { return enums.Desc(i, _FileStatusDescMap) }
// FileStatusValues returns all possible values for the type FileStatus.
func FileStatusValues() []FileStatus { return _FileStatusValues }
// Values returns all possible values for the type FileStatus.
func (i FileStatus) Values() []enums.Enum { return enums.Values(_FileStatusValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i FileStatus) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *FileStatus) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "FileStatus")
}
var _TypesValues = []Types{0, 1, 2, 3, 4}
// TypesN is the highest valid value for type Types, plus one.
const TypesN Types = 5
var _TypesValueMap = map[string]Types{`NoVCS`: 0, `novcs`: 0, `Git`: 1, `git`: 1, `Svn`: 2, `svn`: 2, `Bzr`: 3, `bzr`: 3, `Hg`: 4, `hg`: 4}
var _TypesDescMap = map[Types]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``}
var _TypesMap = map[Types]string{0: `NoVCS`, 1: `Git`, 2: `Svn`, 3: `Bzr`, 4: `Hg`}
// String returns the string representation of this Types value.
func (i Types) String() string { return enums.String(i, _TypesMap) }
// SetString sets the Types value from its string representation,
// and returns an error if the string is invalid.
func (i *Types) SetString(s string) error { return enums.SetStringLower(i, s, _TypesValueMap, "Types") }
// Int64 returns the Types value as an int64.
func (i Types) Int64() int64 { return int64(i) }
// SetInt64 sets the Types value from an int64.
func (i *Types) SetInt64(in int64) { *i = Types(in) }
// Desc returns the description of the Types value.
func (i Types) Desc() string { return enums.Desc(i, _TypesDescMap) }
// TypesValues returns all possible values for the type Types.
func TypesValues() []Types { return _TypesValues }
// Values returns all possible values for the type Types.
func (i Types) Values() []enums.Enum { return enums.Values(_TypesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Types) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Types) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Types") }
// Copyright (c) 2020, The Cogent Core Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package vcs
import (
"os"
"path/filepath"
)
// Files is a map used for storing files in a repository along with their status
type Files map[string]FileStatus
// Status returns the VCS file status associated with given filename,
// returning Untracked if not found and safe to empty map.
func (fl *Files) Status(repo Repo, fname string) FileStatus {
if *fl == nil || len(*fl) == 0 {
return Untracked
}
st, ok := (*fl)[relPath(repo, fname)]
if !ok {
return Untracked
}
return st
}
// allFiles returns a slice of all the files, recursively, within a given directory
func allFiles(path string) ([]string, error) {
var fnms []string
er := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
fnms = append(fnms, path)
return nil
})
return fnms, er
}
// FileStatus indicates the status of files in the repository
type FileStatus int32 //enums:enum
const (
// Untracked means file is not under VCS control
Untracked FileStatus = iota
// Stored means file is stored under VCS control, and has not been modified in working copy
Stored
// Modified means file is under VCS control, and has been modified in working copy
Modified
// Added means file has just been added to VCS but is not yet committed
Added
// Deleted means file has been deleted from VCS
Deleted
// Conflicted means file is in conflict -- has not been merged
Conflicted
// Updated means file has been updated in the remote but not locally
Updated
)
// Copyright (c) 2019, 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 vcs
import (
"bufio"
"bytes"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/Masterminds/vcs"
)
type GitRepo struct {
vcs.GitRepo
}
func (gr *GitRepo) Type() Types {
return Git
}
func (gr *GitRepo) Files() (Files, error) {
f := make(Files)
out, err := gr.RunFromDir("git", "ls-files", "-o") // other -- untracked
if err != nil {
return nil, err
}
scan := bufio.NewScanner(bytes.NewReader(out))
for scan.Scan() {
fn := filepath.FromSlash(string(scan.Bytes()))
f[fn] = Untracked
}
out, err = gr.RunFromDir("git", "ls-files", "-c") // cached = all in repo
if err != nil {
return nil, err
}
scan = bufio.NewScanner(bytes.NewReader(out))
for scan.Scan() {
fn := filepath.FromSlash(string(scan.Bytes()))
f[fn] = Stored
}
out, err = gr.RunFromDir("git", "ls-files", "-m") // modified
if err != nil {
return nil, err
}
scan = bufio.NewScanner(bytes.NewReader(out))
for scan.Scan() {
fn := filepath.FromSlash(string(scan.Bytes()))
f[fn] = Modified
}
out, err = gr.RunFromDir("git", "ls-files", "-d") // deleted
if err != nil {
return nil, err
}
scan = bufio.NewScanner(bytes.NewReader(out))
for scan.Scan() {
fn := filepath.FromSlash(string(scan.Bytes()))
f[fn] = Deleted
}
out, err = gr.RunFromDir("git", "ls-files", "-u") // unmerged
if err != nil {
return nil, err
}
scan = bufio.NewScanner(bytes.NewReader(out))
for scan.Scan() {
fn := filepath.FromSlash(string(scan.Bytes()))
f[fn] = Conflicted
}
out, err = gr.RunFromDir("git", "diff", "--name-only", "--diff-filter=A", "HEAD") // deleted
if err != nil {
return nil, err
}
scan = bufio.NewScanner(bytes.NewReader(out))
for scan.Scan() {
fn := filepath.FromSlash(string(scan.Bytes()))
f[fn] = Added
}
return f, nil
}
func (gr *GitRepo) charToStat(stat byte) FileStatus {
switch stat {
case 'M':
return Modified
case 'A':
return Added
case 'D':
return Deleted
case 'U':
return Conflicted
case '?', '!':
return Untracked
}
return Untracked
}
// Status returns status of given file; returns Untracked on any error
func (gr *GitRepo) Status(fname string) (FileStatus, string) {
out, err := gr.RunFromDir("git", "status", "--porcelain", relPath(gr, fname))
if err != nil {
return Untracked, err.Error()
}
ostr := string(out)
if ostr == "" {
return Stored, ""
}
sf := strings.Fields(ostr)
if len(sf) < 2 {
return Stored, ostr
}
stat := sf[0][0]
return gr.charToStat(stat), ostr
}
// Add adds the file to the repo
func (gr *GitRepo) Add(fname string) error {
out, err := gr.RunFromDir("git", "add", relPath(gr, fname))
if err != nil {
log.Println(string(out))
return err
}
return nil
}
// Move moves updates the repo with the rename
func (gr *GitRepo) Move(oldpath, newpath string) error {
out, err := gr.RunFromDir("git", "mv", relPath(gr, oldpath), relPath(gr, newpath))
if err != nil {
log.Println(string(out))
return err
}
out, err = gr.RunFromDir("git", "add", relPath(gr, newpath))
if err != nil {
log.Println(string(out))
return err
}
return nil
}
// Delete removes the file from the repo; uses "force" option to ensure deletion
func (gr *GitRepo) Delete(fname string) error {
out, err := gr.RunFromDir("git", "rm", "-f", relPath(gr, fname))
if err != nil {
log.Println(string(out))
fmt.Printf("%s\n", out)
return err
}
return nil
}
// Delete removes the file from the repo
func (gr *GitRepo) DeleteRemote(fname string) error {
out, err := gr.RunFromDir("git", "rm", "--cached", relPath(gr, fname))
if err != nil {
log.Println(string(out))
return err
}
return nil
}
// CommitFile commits single file to repo staging
func (gr *GitRepo) CommitFile(fname string, message string) error {
out, err := gr.RunFromDir("git", "commit", relPath(gr, fname), "-m", message)
if err != nil {
log.Println(string(out))
return err
}
return nil
}
// RevertFile reverts a single file to last commit of master
func (gr *GitRepo) RevertFile(fname string) error {
out, err := gr.RunFromDir("git", "checkout", relPath(gr, fname))
if err != nil {
log.Println(string(out))
return err
}
return nil
}
// UpdateVersion sets the version of a package currently checked out via Git.
func (s *GitRepo) UpdateVersion(version string) error {
out, err := s.RunFromDir("git", "switch", "--detach", version)
if err != nil {
return vcs.NewLocalError("Unable to update checked out version", err, string(out))
}
return nil
}
// FileContents returns the contents of given file, as a []byte array
// at given revision specifier. -1, -2 etc also work as universal
// ways of specifying prior revisions.
func (gr *GitRepo) FileContents(fname string, rev string) ([]byte, error) {
if rev == "" {
out, err := os.ReadFile(fname)
if err != nil {
log.Println(err.Error())
}
return out, err
} else if rev[0] == '-' {
rsp, err := strconv.Atoi(rev)
if err == nil && rsp < 0 {
rev = fmt.Sprintf("HEAD~%d:", -rsp)
}
} else {
rev += ":"
}
fspec := rev + relPath(gr, fname)
out, err := gr.RunFromDir("git", "show", fspec)
if err != nil {
log.Println(string(out))
return nil, err
}
return out, nil
}
// fieldsThroughDelim gets the concatenated byte through to point where
// field ends with given delimiter, starting at given index
func fieldsThroughDelim(flds [][]byte, delim byte, idx int) (int, string) {
ln := len(flds)
for i := idx; i < ln; i++ {
fld := flds[i]
fsz := len(fld)
if fld[fsz-1] == delim {
str := string(bytes.Join(flds[idx:i+1], []byte(" ")))
return i + 1, str[:len(str)-1]
}
}
return ln, string(bytes.Join(flds[idx:ln], []byte(" ")))
}
// Log returns the log history of commits for given filename
// (or all files if empty). If since is non-empty, it should be
// a date-like expression that the VCS will understand, such as
// 1/1/2020, yesterday, last year, etc
func (gr *GitRepo) Log(fname string, since string) (Log, error) {
args := []string{"log", "--all"}
if since != "" {
args = append(args, `--since="`+since+`"`)
}
args = append(args, `--pretty=format:%h %ad} %an} %ae} %s`)
if fname != "" {
args = append(args, fname)
}
out, err := gr.RunFromDir("git", args...)
if err != nil {
return nil, err
}
var lg Log
scan := bufio.NewScanner(bytes.NewReader(out))
for scan.Scan() {
ln := scan.Bytes()
flds := bytes.Fields(ln)
if len(flds) < 4 {
continue
}
rev := string(flds[0])
ni, date := fieldsThroughDelim(flds, '}', 1)
ni, author := fieldsThroughDelim(flds, '}', ni)
ni, email := fieldsThroughDelim(flds, '}', ni)
msg := string(bytes.Join(flds[ni:], []byte(" ")))
lg.Add(rev, date, author, email, msg)
}
return lg, nil
}
// CommitDesc returns the full textual description of the given commit,
// if rev is empty, defaults to current HEAD, -1, -2 etc also work as universal
// ways of specifying prior revisions.
// Optionally includes diffs for the changes (otherwise just a list of files
// with modification status).
func (gr *GitRepo) CommitDesc(rev string, diffs bool) ([]byte, error) {
if rev == "" {
rev = "HEAD"
} else if rev[0] == '-' {
rsp, err := strconv.Atoi(rev)
if err == nil && rsp < 0 {
rev = fmt.Sprintf("HEAD~%d", -rsp)
}
}
var out []byte
var err error
if diffs {
out, err = gr.RunFromDir("git", "show", rev)
} else {
out, err = gr.RunFromDir("git", "show", "--name-status", rev)
}
if err != nil {
log.Println(string(out))
return nil, err
}
return out, nil
}
// FilesChanged returns the list of files changed and their statuses,
// between two revisions.
// If revA is empty, defaults to current HEAD; revB defaults to HEAD-1.
// -1, -2 etc also work as universal ways of specifying prior revisions.
// Optionally includes diffs for the changes.
func (gr *GitRepo) FilesChanged(revA, revB string, diffs bool) ([]byte, error) {
if revA == "" {
revA = "HEAD"
} else if revA[0] == '-' {
rsp, err := strconv.Atoi(revA)
if err == nil && rsp < 0 {
revA = fmt.Sprintf("HEAD~%d", -rsp)
}
}
if revB != "" && revB[0] == '-' {
rsp, err := strconv.Atoi(revB)
if err == nil && rsp < 0 {
revB = fmt.Sprintf("HEAD~%d", -rsp)
}
}
var out []byte
var err error
if diffs {
out, err = gr.RunFromDir("git", "diff", "-u", revA, revB)
} else {
if revB == "" {
out, err = gr.RunFromDir("git", "diff", "--name-status", revA)
} else {
out, err = gr.RunFromDir("git", "diff", "--name-status", revA, revB)
}
}
if err != nil {
log.Println(string(out))
return nil, err
}
return out, nil
}
// Blame returns an annotated report about the file, showing which revision last
// modified each line.
func (gr *GitRepo) Blame(fname string) ([]byte, error) {
out, err := gr.RunFromDir("git", "blame", fname)
if err != nil {
log.Println(string(out))
return nil, err
}
return out, nil
}
// Copyright (c) 2018, The Cogent Core Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package vcs
// Commit is one VCS commit entry, as returned in a [Log].
type Commit struct {
// revision number / hash code / unique id
Rev string
// date (author's time) when committed
Date string
// author's name
Author string
// author's email
Email string
// message / subject line for commit
Message string `width:"100"`
}
// Log is a listing of commits.
type Log []*Commit
// Add adds a new [Commit] to the [Log], returning the [Commit].
func (lg *Log) Add(rev, date, author, email, message string) *Commit {
cm := &Commit{Rev: rev, Date: date, Author: author, Email: email, Message: message}
*lg = append(*lg, cm)
return cm
}
// Copyright (c) 2019, 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 vcs
import (
"bufio"
"bytes"
"fmt"
"log"
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/Masterminds/vcs"
)
type SvnRepo struct {
vcs.SvnRepo
}
func (gr *SvnRepo) Type() Types {
return Svn
}
func (gr *SvnRepo) CharToStat(stat byte) FileStatus {
switch stat {
case 'M', 'R':
return Modified
case 'A':
return Added
case 'D', '!':
return Deleted
case 'C':
return Conflicted
case '?', 'I':
return Untracked
case '*':
return Updated
default:
return Stored
}
}
func (gr *SvnRepo) Files() (Files, error) {
f := make(Files)
lpath := gr.LocalPath()
allfs, err := allFiles(lpath) // much faster than svn list --recursive
if err != nil {
return nil, err
}
for _, fn := range allfs {
rpath, _ := filepath.Rel(lpath, fn)
f[rpath] = Stored
}
out, err := gr.RunFromDir("svn", "status", "-u")
if err != nil {
return nil, err
}
scan := bufio.NewScanner(bytes.NewReader(out))
for scan.Scan() {
ln := string(scan.Bytes())
flds := strings.Fields(ln)
if len(flds) < 2 {
continue // shouldn't happend
}
stat := flds[0][0]
fn := flds[len(flds)-1]
f[fn] = gr.CharToStat(stat)
}
return f, nil
}
// Status returns status of given file; returns Untracked on any error
func (gr *SvnRepo) Status(fname string) (FileStatus, string) {
out, err := gr.RunFromDir("svn", "status", relPath(gr, fname))
if err != nil {
return Untracked, err.Error()
}
ostr := string(out)
if ostr == "" {
return Stored, ""
}
sf := strings.Fields(ostr)
if len(sf) < 2 {
return Stored, ostr
}
stat := sf[0][0]
return gr.CharToStat(stat), ostr
}
// Add adds the file to the repo
func (gr *SvnRepo) Add(fname string) error {
oscmd := exec.Command("svn", "add", relPath(gr, fname))
stdoutStderr, err := oscmd.CombinedOutput()
if err != nil {
log.Println(string(stdoutStderr))
return err
}
return nil
}
// Move moves updates the repo with the rename
func (gr *SvnRepo) Move(oldpath, newpath string) error {
oscmd := exec.Command("svn", "mv", oldpath, newpath)
stdoutStderr, err := oscmd.CombinedOutput()
if err != nil {
log.Println(string(stdoutStderr))
return err
}
return nil
}
// Delete removes the file from the repo -- uses "force" option to ensure deletion
func (gr *SvnRepo) Delete(fname string) error {
oscmd := exec.Command("svn", "rm", "-f", relPath(gr, fname))
stdoutStderr, err := oscmd.CombinedOutput()
if err != nil {
log.Println(string(stdoutStderr))
return err
}
return nil
}
// DeleteRemote removes the file from the repo, but keeps local copy
func (gr *SvnRepo) DeleteRemote(fname string) error {
oscmd := exec.Command("svn", "delete", "--keep-local", relPath(gr, fname))
stdoutStderr, err := oscmd.CombinedOutput()
if err != nil {
log.Println(string(stdoutStderr))
return err
}
return nil
}
// CommitFile commits single file to repo staging
func (gr *SvnRepo) CommitFile(fname string, message string) error {
oscmd := exec.Command("svn", "commit", relPath(gr, fname), "-m", message)
stdoutStderr, err := oscmd.CombinedOutput()
if err != nil {
log.Println(string(stdoutStderr))
return err
}
return nil
}
// RevertFile reverts a single file to last commit of master
func (gr *SvnRepo) RevertFile(fname string) error {
oscmd := exec.Command("svn", "revert", relPath(gr, fname))
stdoutStderr, err := oscmd.CombinedOutput()
if err != nil {
log.Println(string(stdoutStderr))
return err
}
return nil
}
// FileContents returns the contents of given file, as a []byte array
// at given revision specifier (if empty, defaults to current HEAD).
// -1, -2 etc also work as universal ways of specifying prior revisions.
func (gr *SvnRepo) FileContents(fname string, rev string) ([]byte, error) {
if rev == "" {
rev = "HEAD"
// } else if rev[0] == '-' { // no support at this point..
// rsp, err := strconv.Atoi(rev)
// if err == nil && rsp < 0 {
// rev = fmt.Sprintf("HEAD~%d:", rsp)
// }
}
out, err := gr.RunFromDir("svn", "-r", "rev", "cat", relPath(gr, fname))
if err != nil {
log.Println(string(out))
return nil, err
}
return out, nil
}
// Log returns the log history of commits for given filename
// (or all files if empty). If since is non-empty, it is the
// maximum number of entries to return (a number).
func (gr *SvnRepo) Log(fname string, since string) (Log, error) {
// todo: parse -- requires parsing over multiple lines..
args := []string{"log"}
if since != "" {
args = append(args, `--limit=`+since)
}
if fname != "" {
args = append(args, fname)
}
out, err := gr.RunFromDir("svn", args...)
if err != nil {
return nil, err
}
var lg Log
rev := ""
date := ""
author := ""
email := ""
msg := ""
newStart := false
scan := bufio.NewScanner(bytes.NewReader(out))
for scan.Scan() {
ln := scan.Bytes()
if string(ln[:10]) == "----------" {
if rev != "" {
lg.Add(rev, date, author, email, msg)
}
newStart = true
msg = ""
continue
}
if newStart {
flds := bytes.Split(ln, []byte("|"))
if len(flds) < 4 {
continue
}
rev = strings.TrimSpace(string(flds[0]))
author = strings.TrimSpace(string(flds[1]))
date = strings.TrimSpace(string(flds[2]))
msg = ""
newStart = false
} else {
nosp := bytes.TrimSpace(ln)
if msg == "" && len(nosp) == 0 {
continue
}
msg += string(ln) + "\n"
}
}
return lg, nil
}
// CommitDesc returns the full textual description of the given commit,
// if rev is empty, defaults to current HEAD, -1, -2 etc also work as universal
// ways of specifying prior revisions.
// Optionally includes diffs for the changes (otherwise just a list of files
// with modification status).
func (gr *SvnRepo) CommitDesc(rev string, diffs bool) ([]byte, error) {
if rev == "" {
rev = "HEAD"
} else if rev[0] == '-' {
rsp, err := strconv.Atoi(rev)
if err == nil && rsp < 0 {
rev = fmt.Sprintf("HEAD~%d", -rsp)
}
}
var out []byte
var err error
if diffs {
out, err = gr.RunFromDir("svn", "log", "-v", "--diff", "-r", rev)
} else {
out, err = gr.RunFromDir("svn", "log", "-v", "-r", rev)
}
if err != nil {
log.Println(string(out))
return nil, err
}
return out, err
}
func (gr *SvnRepo) FilesChanged(revA, revB string, diffs bool) ([]byte, error) {
return nil, nil // todo:
}
// Blame returns an annotated report about the file, showing which revision last
// modified each line.
func (gr *SvnRepo) Blame(fname string) ([]byte, error) {
out, err := gr.RunFromDir("svn", "blame", fname)
if err != nil {
log.Println(string(out))
return nil, err
}
return out, nil
}
// Copyright (c) 2018, The Cogent Core Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package vcs provides a more complete version control system (ex: git)
// interface, building on https://github.com/Masterminds/vcs.
package vcs
//go:generate core generate
import (
"fmt"
"path/filepath"
"cogentcore.org/core/base/fsx"
"github.com/Masterminds/vcs"
)
type Types int32 //enums:enum -accept-lower
const (
NoVCS Types = iota
Git
Svn
Bzr
Hg
)
// Repo provides an interface extending [vcs.Repo]
// (https://github.com/Masterminds/vcs)
// with support for file status information and operations.
type Repo interface {
vcs.Repo
// Type returns the type of repo we are using
Type() Types
// Files returns a map of the current files and their status.
Files() (Files, error)
// Status returns status of given file -- returns Untracked and error
// message on any error. FileStatus is a summary status category,
// and string return value is more detailed status information formatted
// according to standard conventions of given VCS.
Status(fname string) (FileStatus, string)
// Add adds the file to the repo
Add(fname string) error
// Move moves the file using VCS command to keep it updated
Move(oldpath, newpath string) error
// Delete removes the file from the repo and working copy.
// Uses "force" option to ensure deletion.
Delete(fname string) error
// DeleteRemote removes the file from the repo but keeps the local file itself
DeleteRemote(fname string) error
// CommitFile commits a single file
CommitFile(fname string, message string) error
// RevertFile reverts a single file to the version that it was last in VCS,
// losing any local changes (destructive!)
RevertFile(fname string) error
// FileContents returns the contents of given file, as a []byte array
// at given revision specifier (if empty, defaults to current HEAD).
// -1, -2 etc also work as universal ways of specifying prior revisions.
FileContents(fname string, rev string) ([]byte, error)
// Log returns the log history of commits for given filename
// (or all files if empty). If since is non-empty, it should be
// a date-like expression that the VCS will understand, such as
// 1/1/2020, yesterday, last year, etc. SVN only understands a
// number as a maximum number of items to return.
Log(fname string, since string) (Log, error)
// CommitDesc returns the full textual description of the given commit,
// if rev is empty, defaults to current HEAD, -1, -2 etc also work as universal
// ways of specifying prior revisions.
// Optionally includes diffs for the changes (otherwise just a list of files
// with modification status).
CommitDesc(rev string, diffs bool) ([]byte, error)
// FilesChanged returns the list of files changed and their statuses,
// between two revisions.
// If revA is empty, defaults to current HEAD; revB defaults to HEAD-1.
// -1, -2 etc also work as universal ways of specifying prior revisions.
// Optionally includes diffs for the changes.
FilesChanged(revA, revB string, diffs bool) ([]byte, error)
// Blame returns an annotated report about the file, showing which revision last
// modified each line.
Blame(fname string) ([]byte, error)
}
func NewRepo(remote, local string) (Repo, error) {
repo, err := vcs.NewRepo(remote, local)
if err == nil {
switch repo.Vcs() {
case vcs.Git:
r := &GitRepo{}
r.GitRepo = *(repo.(*vcs.GitRepo))
return r, err
case vcs.Svn:
r := &SvnRepo{}
r.SvnRepo = *(repo.(*vcs.SvnRepo))
return r, err
case vcs.Hg:
err = fmt.Errorf("hg version control not yet supported")
case vcs.Bzr:
err = fmt.Errorf("bzr version control not yet supported")
}
}
return nil, err
}
// DetectRepo attempts to detect the presence of a repository at the given
// directory path -- returns type of repository if found, else NoVCS.
// Very quickly just looks for signature file name:
// .git for git
// .svn for svn -- but note that this will find any subdir in svn rep.o
func DetectRepo(path string) Types {
if fsx.HasFile(path, ".git") {
return Git
}
if fsx.HasFile(path, ".svn") {
return Svn
}
// todo: rest later..
return NoVCS
}
// relPath return the path relative to the repository LocalPath()
func relPath(repo Repo, path string) string {
relpath, _ := filepath.Rel(repo.LocalPath(), path)
return relpath
}
// 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.
// note: parsing code adapted from pflag package https://github.com/spf13/pflag
// Copyright (c) 2012 Alex Ogier. All rights reserved.
// Copyright (c) 2012 The Go Authors. All rights reserved.
package cli
import (
"fmt"
"reflect"
"strconv"
"strings"
"maps"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/base/strcase"
)
const (
// ErrNotFound can be passed to [SetFromArgs] and [ParseFlags]
// to indicate that they should return an error for a flag that
// is set but not found in the configuration struct.
ErrNotFound = true
// ErrNotFound can be passed to [SetFromArgs] and [ParseFlags]
// to indicate that they should NOT return an error for a flag that
// is set but not found in the configuration struct.
NoErrNotFound = false
)
// SetFromArgs sets config values on the given config object from
// the given from command-line args, based on the field names in
// the config struct and the given list of available commands.
// It returns the command, if any, that was passed in the arguments,
// and any error than occurs during the parsing and setting process.
// If errNotFound is set to true, it is assumed that all flags
// (arguments starting with a "-") must refer to fields in the
// config struct, so any that fail to match trigger an error.
// It is recommended that the [ErrNotFound] and [NoErrNotFound]
// constants be used for the value of errNotFound for clearer code.
func SetFromArgs[T any](cfg T, args []string, errNotFound bool, cmds ...*Cmd[T]) (string, error) {
bf := boolFlags(cfg)
// if we are not already a meta config object, we have to add
// all of the bool flags of the meta config object so that we
// correctly handle the short (no value) versions of things like
// verbose and quiet.
if _, ok := any(cfg).(*metaConfig); !ok {
mcf := boolFlags(&metaConfigFields{})
maps.Copy(bf, mcf)
}
nfargs, flags, err := getArgs(args, bf)
if err != nil {
return "", err
}
cmd, allFlags, err := parseArgs(cfg, nfargs, flags, cmds...)
if err != nil {
return "", err
}
err = parseFlags(flags, allFlags, errNotFound)
if err != nil {
return "", err
}
return cmd, nil
}
// boolFlags returns a map with a true value for every flag name
// that maps to a boolean field. This is needed so that bool
// flags can be properly set with their shorthand syntax.
func boolFlags(obj any) map[string]bool {
fields := &fields{}
addFields(obj, fields, addAllFields)
res := map[string]bool{}
for _, kv := range fields.Order {
f := kv.Value
if f.Field.Type.Kind() != reflect.Bool { // we only care about bools here
continue
}
// we need all cases of both normal and "no" version for all names
for _, name := range f.Names {
for _, cnms := range allCases(name) {
res[cnms] = true
}
for _, cnms := range allCases("No" + name) {
res[cnms] = true
}
}
}
return res
}
// getArgs processes the given args using the given map of bool flags,
// which should be obtained through [boolFlags]. It returns the leftover
// (positional) args, the flags, and any error.
func getArgs(args []string, boolFlags map[string]bool) ([]string, map[string]string, error) {
var nonFlags []string
flags := map[string]string{}
for len(args) > 0 {
s := args[0]
args = args[1:]
if len(s) == 0 || s[0] != '-' || len(s) == 1 { // if we are not a flag, just add to non-flags
nonFlags = append(nonFlags, s)
continue
}
if s[1] == '-' && len(s) == 2 { // "--" terminates the flags
// f.argsLenAtDash = len(f.args)
nonFlags = append(nonFlags, args...)
break
}
name, value, nargs, err := getFlag(s, args, boolFlags)
if err != nil {
return nonFlags, flags, err
}
// we need to updated remaining args with latest
args = nargs
if name != "" { // we ignore no-names so that we can skip things like test args
flags[name] = value
}
}
return nonFlags, flags, nil
}
// getFlag parses the given flag arg string in the context of the given
// remaining arguments and bool flags. It returns the name of the flag,
// the value of the flag, the remaining arguments updated with any changes
// caused by getting this flag, and any error.
func getFlag(s string, args []string, boolFlags map[string]bool) (name, value string, a []string, err error) {
// we start out with the remaining args we were passed
a = args
// we know the first character is a dash, so we can trim it directly
name = s[1:]
// then we trim double dash if there is one
name = strings.TrimPrefix(name, "-")
// we can't start with a dash or equal, as those are reserved characters
if len(name) == 0 || name[0] == '-' || name[0] == '=' {
err = fmt.Errorf("bad flag syntax: %q", s)
return
}
// go test passes args, so we ignore them
if strings.HasPrefix(name, "test.") {
name = ""
return
}
// split on equal (we could be in the form flag=value)
split := strings.SplitN(name, "=", 2)
name = split[0]
if len(split) == 2 {
// if we are in the form flag=value, we are done
value = split[1]
} else if len(a) > 0 && !boolFlags[name] { // otherwise, if we still have more remaining args and are not a bool, our value could be the next arg (if we are a bool, we don't care about the next value)
value = a[0]
// if the next arg starts with a dash, it can't be our value, so we are just a bool arg and we exit with an empty value
if strings.HasPrefix(value, "-") {
value = ""
return
} else {
// if it doesn't start with a dash, it is our value, so we remove it from the remaining args (we have already set value to it above)
a = a[1:]
return
}
}
return
}
// parseArgs parses the given non-flag arguments in the context of the given
// configuration struct, flags, and commands. The non-flag arguments and flags
// should be gotten through [getArgs] first. It returns the command specified by
// the arguments, an ordered map of all of the flag names and their associated
// field objects, and any error.
func parseArgs[T any](cfg T, args []string, flags map[string]string, cmds ...*Cmd[T]) (cmd string, allFlags *fields, err error) {
newArgs, newCmd, err := parseArgsImpl(cfg, args, "", cmds...)
if err != nil {
return newCmd, allFlags, err
}
// if the command is blank, then it is the root command
if newCmd == "" {
for _, c := range cmds {
if c.Root && c.Name != "help" {
newCmd = c.Name
break
}
}
}
allFields := &fields{}
addMetaConfigFields(allFields)
addFields(cfg, allFields, newCmd)
allFlags = &fields{}
newArgs, err = addFlags(allFields, allFlags, newArgs, flags)
if err != nil {
return newCmd, allFields, err
}
if len(newArgs) > 0 {
return newCmd, allFields, fmt.Errorf("got unused arguments: %v", newArgs)
}
return newCmd, allFlags, nil
}
// parseArgsImpl is the underlying implementation of [parseArgs] that is called
// recursively and takes most of what [parseArgs] does, plus the current command state,
// and returns most of what [parseArgs] does, plus the args state.
func parseArgsImpl[T any](cfg T, baseArgs []string, baseCmd string, cmds ...*Cmd[T]) (args []string, cmd string, err error) {
// we start with our base args and command
args = baseArgs
cmd = baseCmd
// if we have no additional args, we have nothing to do
if len(args) == 0 {
return
}
// we only care about one arg at a time (everything else is handled recursively)
arg := args[0]
// get all of the (sub)commands in our base command
baseCmdStrs := strings.Fields(baseCmd)
for _, c := range cmds {
// get all of the (sub)commands in this command
cmdStrs := strings.Fields(c.Name)
// find the (sub)commands that our base command shares with the command we are checking
gotTo := 0
hasMismatch := false
for i, cstr := range cmdStrs {
// if we have no more (sub)commands on our base, mark our location and break
if i >= len(baseCmdStrs) {
gotTo = i
break
}
// if we have a different thing than our base, it is a mismatch
if baseCmdStrs[i] != cstr {
hasMismatch = true
break
}
}
// if we have a different sub(command) for something, this isn't the right command
if hasMismatch {
continue
}
// if the thing after we ran out of (sub)commands on our base isn't our next arg, this isn't the right command
if gotTo >= len(cmdStrs) || arg != cmdStrs[gotTo] {
continue
}
// otherwise, it is the right command, and our new command is our base plus our next arg
cmd = arg
if baseCmd != "" {
cmd = baseCmd + " " + arg
}
// we have consumed our next arg, so we get rid of it
args = args[1:]
// then, we recursively parse again with our new command as context
oargs, ocmd, err := parseArgsImpl(cfg, args, cmd, cmds...)
if err != nil {
return nil, "", err
}
// our new args and command are now whatever the recursive call returned, building upon what we passed it
args = oargs
cmd = ocmd
// we got the command we wanted, so we can break
break
}
return
}
// parseFlags parses the given flags using the given ordered map of all of the
// available flags, setting the values from that map accordingly.
// Setting errNotFound to true causes flags that are not in allFlags to
// trigger an error; otherwise, it just skips those. It is recommended
// that the [ErrNotFound] and [NoErrNotFound] constants be used for the
// value of errNotFound for clearer code. Also, the flags should be
// gotten through [getArgs] first, and the map of available flags should
// be gotten through [parseArgs] first.
func parseFlags(flags map[string]string, allFlags *fields, errNotFound bool) error {
for name, value := range flags {
err := parseFlag(name, value, allFlags, errNotFound)
if err != nil {
return err
}
}
return nil
}
// parseFlag parses the flag with the given name and the given value
// using the given map of all of the available flags, setting the value
// in that map corresponding to the flag name accordingly. Setting
// errNotFound = true causes passing a flag name that is not in allFlags
// to trigger an error; otherwise, it just does nothing and returns no error.
// It is recommended that the [ErrNotFound] and [NoErrNotFound]
// constants be used for the value of errNotFound for clearer code.
func parseFlag(name string, value string, allFlags *fields, errNotFound bool) error {
f, exists := allFlags.ValueByKeyTry(name)
if !exists {
if errNotFound {
return fmt.Errorf("flag name %q not recognized", name)
}
return nil
}
isBool := reflectx.NonPointerValue(f.Value).Kind() == reflect.Bool
if isBool {
// check if we have a "no" prefix and set negate based on that
lcnm := strings.ToLower(name)
negate := false
if len(lcnm) > 3 {
if lcnm[:3] == "no_" || lcnm[:3] == "no-" {
negate = true
} else if lcnm[:2] == "no" {
if _, has := allFlags.ValueByKeyTry(lcnm[2:]); has { // e.g., nogui and gui is on list
negate = true
}
}
}
// the value could be explicitly set to a bool value,
// so we check that; if it is not set, it is true
b := true
if value != "" {
var err error
b, err = strconv.ParseBool(value)
if err != nil {
return fmt.Errorf("error parsing bool flag %q: %w", name, err)
}
}
// if we are negating and true (ex: -no-something), or not negating
// and false (ex: -something=false), we are false
if negate && b || !negate && !b {
value = "false"
} else { // otherwise, we are true
value = "true"
}
}
if value == "" {
// got '--flag' but arg was required
return fmt.Errorf("flag %q needs an argument", name)
}
return setFieldValue(f, value)
}
// setFieldValue sets the value of the given configuration field
// to the given string argument value.
func setFieldValue(f *field, value string) error {
nptyp := reflectx.NonPointerType(f.Value.Type())
vk := nptyp.Kind()
switch {
// TODO: more robust parsing of maps and slices
case vk == reflect.Map:
strs := strings.Split(value, ",")
mval := map[string]string{}
for _, str := range strs {
k, v, found := strings.Cut(str, "=")
if !found {
return fmt.Errorf("missing key-value pair for setting map flag %q from flag value %q (element %q has no %q)", f.Names[0], value, str, "=")
}
mval[k] = v
}
err := reflectx.CopyMapRobust(f.Value.Interface(), mval)
if err != nil {
return fmt.Errorf("unable to set map flag %q from flag value %q: %w", f.Names[0], value, err)
}
case vk == reflect.Slice:
err := reflectx.CopySliceRobust(f.Value.Interface(), strings.Split(value, ","))
if err != nil {
return fmt.Errorf("unable to set slice flag %q from flag value %q: %w", f.Names[0], value, err)
}
default:
// initialize nil fields to prevent panics
// (don't do for maps and slices, as new doesn't work for them)
if f.Value.IsNil() {
f.Value.Set(reflect.New(nptyp))
}
err := reflectx.SetRobust(f.Value.Interface(), value) // overkill but whatever
if err != nil {
return fmt.Errorf("error setting set flag %q from flag value %q: %w", f.Names[0], value, err)
}
}
return nil
}
// addAllCases adds all string cases (kebab-case, snake_case, etc)
// of the given field with the given name to the given set of flags.
func addAllCases(nm string, field *field, allFlags *fields) {
if nm == "Includes" {
return // skip Includes
}
for _, nm := range allCases(nm) {
allFlags.Add(nm, field)
}
}
// allCases returns all of the string cases (kebab-case,
// snake_case, etc) of the given name.
func allCases(nm string) []string {
return []string{nm, strings.ToLower(nm), strcase.ToKebab(nm), strcase.ToSnake(nm), strcase.ToSNAKE(nm)}
}
// addFlags adds to given the given ordered flags map all of the different ways
// all of the given fields can can be specified as flags. It also uses the given
// positional arguments to set the values of the object based on any posarg struct
// tags that fields have. The posarg struct tag must either be "all", "leftover",
// or a valid uint. Finally, it also uses the given map of flags passed to the
// command as context.
func addFlags(allFields *fields, allFlags *fields, args []string, flags map[string]string) ([]string, error) {
consumed := map[int]bool{} // which args we have consumed via pos args
var leftoverField *field
for _, kv := range allFields.Order {
v := kv.Value
f := v.Field
for _, name := range v.Names {
addAllCases(name, v, allFlags)
if f.Type.Kind() == reflect.Bool {
addAllCases("No"+name, v, allFlags)
}
}
// set based on pos arg
posArgTag, ok := f.Tag.Lookup("posarg")
if ok {
switch posArgTag {
case "all":
err := reflectx.SetRobust(v.Value.Interface(), args)
if err != nil {
return nil, fmt.Errorf("error setting field %q to all positional arguments: %v: %w", f.Name, args, err)
}
// everybody has been consumed
for i := range args {
consumed[i] = true
}
case "leftover":
leftoverField = v // must be handled later once we have all of the leftovers
default:
ui, err := strconv.ParseUint(posArgTag, 10, 64)
if err != nil {
return nil, fmt.Errorf("programmer error: invalid value %q for posarg struct tag on field %q: %w", posArgTag, f.Name, err)
}
// if this is true, the pos arg is missing
if ui >= uint64(len(args)) {
// if it isn't required, it doesn't matter if it's missing
req, has := f.Tag.Lookup("required")
if req != "+" && req != "true" && has { // default is required, so !has => required
continue
}
// check if we have set this pos arg as a flag; if we have,
// it makes up for the missing pos arg and there is no error,
// but otherwise there is an error
got := false
for _, fnm := range v.Names { // TODO: is there a more efficient way to do this?
for _, cnm := range allCases(fnm) {
_, ok := flags[cnm]
if ok {
got = true
break
}
}
if got {
break
}
}
if got {
continue // if we got the pos arg through the flag, we skip the rest of the pos arg stuff and go onto the next field
} else {
return nil, fmt.Errorf("missing positional argument %d (%s)", ui, strcase.ToKebab(v.Names[0]))
}
}
err = setFieldValue(v, args[ui]) // must be pointer to be settable
if err != nil {
return nil, fmt.Errorf("error setting field %q to positional argument %d (%q): %w", f.Name, ui, args[ui], err)
}
consumed[int(ui)] = true // we have consumed this argument
}
}
}
// get leftovers based on who was consumed
leftovers := []string{}
for i, a := range args {
if !consumed[i] {
leftovers = append(leftovers, a)
}
}
if leftoverField != nil {
err := reflectx.SetRobust(leftoverField.Value.Interface(), leftovers)
if err != nil {
return nil, fmt.Errorf("error setting field %q to all leftover arguments: %v: %w", leftoverField.Name, leftovers, err)
}
return nil, nil // no more leftovers
}
return leftovers, nil
}
// 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 cli generates powerful CLIs from Go struct types and functions.
// See package clicore to create a GUI representation of a CLI.
package cli
//go:generate core generate
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"cogentcore.org/core/base/logx"
)
// Run runs an app with the given options, configuration struct,
// and commands. It does not run the GUI; see [cogentcore.org/core/cli/clicore.Run]
// for that. The configuration struct should be passed as a pointer, and
// configuration options should be defined as fields on the configuration
// struct. The commands can be specified as either functions or struct
// objects; the functions are more concise but require using [types].
// In addition to the given commands, Run adds a "help" command that
// prints the result of [usage], which will also be the root command if
// no other root command is specified. Also, it adds the fields in
// [metaConfig] as configuration options. If [Options.Fatal] is set to
// true, the error result of Run does not need to be handled. Run uses
// [os.Args] for its arguments.
func Run[T any, C CmdOrFunc[T]](opts *Options, cfg T, cmds ...C) error {
cs, err := CmdsFromCmdOrFuncs[T, C](cmds)
if err != nil {
err := fmt.Errorf("internal/programmer error: error getting commands from given commands: %w", err)
if opts.Fatal {
logx.PrintError(err)
os.Exit(1)
}
return err
}
cmd, err := config(opts, cfg, cs...)
if err != nil {
if opts.Fatal {
logx.PrintlnError("error: ", err)
os.Exit(1)
}
return err
}
err = runCmd(opts, cfg, cmd, cs...)
if err != nil {
if opts.Fatal {
fmt.Println(logx.CmdColor(cmdString(cmd)) + logx.ErrorColor(" failed: "+err.Error()))
os.Exit(1)
}
return fmt.Errorf("%s failed: %w", cmdName()+" "+cmd, err)
}
// if the user sets level to error (via -q), we don't show the success message
if opts.PrintSuccess && logx.UserLevel <= slog.LevelWarn {
fmt.Println(logx.CmdColor(cmdString(cmd)) + logx.SuccessColor(" succeeded"))
}
return nil
}
// runCmd runs the command with the given name using the given options,
// configuration information, and available commands. If the given
// command name is "", it runs the root command.
func runCmd[T any](opts *Options, cfg T, cmd string, cmds ...*Cmd[T]) error {
for _, c := range cmds {
if c.Name == cmd || c.Root && cmd == "" {
err := c.Func(cfg)
if err != nil {
return err
}
return nil
}
}
if cmd == "" { // if we couldn't find the command and we are looking for the root command, we fall back on help
fmt.Println(usage(opts, cfg, cmd, cmds...))
os.Exit(0)
}
return fmt.Errorf("command %q not found", cmd)
}
// cmdName returns the name of the command currently being run.
func cmdName() string {
base := filepath.Base(os.Args[0])
return strings.TrimSuffix(base, filepath.Ext(base))
}
// cmdString is a simple helper function that returns a string
// with [cmdName] and the given command name string combined
// to form a string representing the complete command being run.
func cmdString(cmd string) string {
if cmd == "" {
return cmdName()
}
return cmdName() + " " + cmd
}
// 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 clicore extends package cli by generating Cogent Core GUIs.
package clicore
import (
"fmt"
"os"
"cogentcore.org/core/base/logx"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/cli"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/tree"
)
// Run runs the given app with the given default
// configuration file paths. It is similar to
// [cli.Run], but it also runs the GUI if no
// arguments were provided. The app should be
// a pointer, and configuration options should
// be defined as fields on the app type. Also,
// commands should be defined as methods on the
// app type with the suffix "Cmd"; for example,
// for a command named "build", there should be
// the method:
//
// func (a *App) BuildCmd() error
//
// Run uses [os.Args] for its arguments.
func Run[T any, C cli.CmdOrFunc[T]](opts *cli.Options, cfg T, cmds ...C) error {
cs, err := cli.CmdsFromCmdOrFuncs[T, C](cmds)
if err != nil {
err := fmt.Errorf("error getting commands from given commands: %w", err)
if opts.Fatal {
logx.PrintlnError(err)
os.Exit(1)
}
return err
}
cs = cli.AddCmd(cs, &cli.Cmd[T]{
Func: func(t T) error {
gui(opts, t, cs...)
return nil
},
Name: "gui",
Doc: "gui runs the GUI version of the " + opts.AppName + " tool",
Root: true, // if root isn't already taken, we take it
})
return cli.Run(opts, cfg, cs...)
}
// gui starts the gui for the given cli app, which must be passed as
// a pointer.
func gui[T any](opts *cli.Options, cfg T, cmds ...*cli.Cmd[T]) {
b := core.NewBody(opts.AppName)
b.AddTopBar(func(bar *core.Frame) {
core.NewToolbar(bar).Maker(func(p *tree.Plan) {
for _, cmd := range cmds {
if cmd.Name == "gui" { // we are already in GUI so that command is irrelevant
continue
}
tree.AddAt(p, cmd.Name, func(w *core.Button) {
w.SetText(strcase.ToSentence(cmd.Name)).SetTooltip(cmd.Doc).
OnClick(func(e events.Event) {
err := cmd.Func(cfg)
if err != nil {
// TODO: snackbar
logx.PrintlnError(err)
}
})
})
}
})
})
sv := core.NewForm(b)
sv.SetStruct(cfg)
b.RunMainWindow()
}
// 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 main
import (
"fmt"
"cogentcore.org/core/cli"
"cogentcore.org/core/cli/clicore"
)
//go:generate core generate -add-types -add-methods
type Config struct {
// the name of the user
Name string
// the age of the user
Age int
// whether the user likes Go
LikesGo bool
// the target platform to build for
BuildTarget string
}
// Build builds the app for the config build target.
func Build(c *Config) error {
fmt.Println("Building for platform", c.BuildTarget)
return nil
}
// Run runs the app for the user with the config name.
func Run(c *Config) error {
fmt.Println("Running for user", c.Name)
return nil
}
func main() { //types:skip
opts := cli.DefaultOptions("Basic", "Basic is a basic example application made with clicore.")
clicore.Run(opts, &Config{}, Build, Run)
}
// 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 cli
import (
"fmt"
"strings"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/types"
)
// Cmd represents a runnable command with configuration options.
// The type constraint is the type of the configuration
// information passed to the command.
type Cmd[T any] struct {
// Func is the actual function that runs the command.
// It takes configuration information and returns an error.
Func func(T) error
// Name is the name of the command.
Name string
// Doc is the documentation for the command.
Doc string
// Root is whether the command is the root command
// (what is called when no subcommands are passed)
Root bool
// Icon is the icon of the command in the tool bar
// when running in the GUI via clicore
Icon string
// SepBefore is whether to add a separator before the
// command in the tool bar when running in the GUI via clicore
SepBefore bool
// SepAfter is whether to add a separator after the
// command in the tool bar when running in the GUI via clicore
SepAfter bool
}
// CmdOrFunc is a generic type constraint that represents either
// a [*Cmd] with the given config type or a command function that
// takes the given config type and returns an error.
type CmdOrFunc[T any] interface {
*Cmd[T] | func(T) error
}
// cmdFromFunc returns a new [Cmd] object from the given function
// and any information specified on it using comment directives,
// which requires the use of [types].
func cmdFromFunc[T any](fun func(T) error) (*Cmd[T], error) {
cmd := &Cmd[T]{
Func: fun,
}
fn := types.FuncName(fun)
// we need to get rid of package name and then convert to kebab
strs := strings.Split(fn, ".")
cfn := strs[len(strs)-1] // camel function name
cmd.Name = strcase.ToKebab(cfn)
if f := types.FuncByName(fn); f != nil {
cmd.Doc = f.Doc
for _, dir := range f.Directives {
if dir.Tool != "cli" {
continue
}
if dir.Directive != "cmd" {
return cmd, fmt.Errorf("unrecognized comment directive %q (from comment %q)", dir.Directive, dir.String())
}
_, err := SetFromArgs(cmd, dir.Args, ErrNotFound)
if err != nil {
return cmd, fmt.Errorf("error setting command from directive arguments (from comment %q): %w", dir.String(), err)
}
}
// we format the doc after the directives so that we have the up-to-date documentation and name
cmd.Doc = types.FormatDoc(cmd.Doc, cfn, strcase.ToSentence(cmd.Name))
}
return cmd, nil
}
// cmdFromCmdOrFunc returns a new [Cmd] object from the given
// [CmdOrFunc] object, using [cmdFromFunc] if it is a function.
func cmdFromCmdOrFunc[T any, C CmdOrFunc[T]](cmd C) (*Cmd[T], error) {
switch c := any(cmd).(type) {
case *Cmd[T]:
return c, nil
case func(T) error:
return cmdFromFunc(c)
default:
panic(fmt.Errorf("internal/programmer error: cli.CmdFromCmdOrFunc: impossible type %T for command %v", cmd, cmd))
}
}
// CmdsFromCmdOrFuncs is a helper function that returns a slice
// of command objects from the given slice of [CmdOrFunc] objects,
// using [cmdFromCmdOrFunc].
func CmdsFromCmdOrFuncs[T any, C CmdOrFunc[T]](cmds []C) ([]*Cmd[T], error) {
res := make([]*Cmd[T], len(cmds))
for i, cmd := range cmds {
cmd, err := cmdFromCmdOrFunc[T, C](cmd)
if err != nil {
return nil, err
}
res[i] = cmd
}
return res, nil
}
// AddCmd adds the given command to the given set of commands
// if there is not already a command with the same name in the
// set of commands. Also, if [Cmd.Root] is set to true on the
// passed command, and there are no other root commands in the
// given set of commands, the passed command will be made the
// root command; otherwise, it will be made not the root command.
func AddCmd[T any](cmds []*Cmd[T], cmd *Cmd[T]) []*Cmd[T] {
hasCmd := false
hasRoot := false
for _, c := range cmds {
if c.Name == cmd.Name {
hasCmd = true
}
if c.Root {
hasRoot = true
}
}
if hasCmd {
return cmds
}
cmd.Root = cmd.Root && !hasRoot // we must both want root and be able to take root
cmds = append(cmds, cmd)
return cmds
}
// 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 cli
import (
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"cogentcore.org/core/base/logx"
)
// IMPORTANT: all changes to [metaConfig] must be updated in [metaConfigFields]
// metaConfig contains meta configuration information specified
// via command line arguments that controls the initial behavior
// of cli for all apps before anything else is loaded. Its
// main purpose is to support the help command and flag and
// the specification of custom config files on the command line.
type metaConfig struct {
// the file name of the config file to load
Config string `flag:"cfg,config"`
// whether to display a help message
Help bool `flag:"h,help"`
// the name of the command to display
// help information for. It is only applicable to the
// help command, but it is enabled for all commands so
// that it can consume all positional arguments to prevent
// errors about unused arguments.
HelpCmd string `posarg:"all"`
// whether to run the command in verbose mode
// and print more information
Verbose bool `flag:"v,verbose"`
// whether to run the command in very verbose mode
// and print as much information as possible
VeryVerbose bool `flag:"vv,very-verbose"`
// whether to run the command in quiet mode
// and print less information
Quiet bool `flag:"q,quiet"`
}
// metaConfigFields is the struct used for the implementation
// of [addMetaConfigFields], and for the usage information for
// meta configuration options in [usage].
// NOTE: we could do this through [metaConfig], but that
// causes problems with the HelpCmd field capturing
// everything, so it easier to just add through a separate struct.
// TODO: maybe improve the structure of this.
// TODO: can we get HelpCmd to display correctly in usage?
type metaConfigFields struct { //types:add
// the file name of the config file to load
Config string `flag:"cfg,config"`
// whether to display a help message
Help bool `flag:"h,help"`
// the name of the command to display
// help information for.
HelpCmd string `cmd:"help" posarg:"all"`
// whether to run the command in verbose mode
// and print more information
Verbose bool `flag:"v,verbose"`
// whether to run the command in very verbose mode
// and print as much information as possible
VeryVerbose bool `flag:"vv,very-verbose"`
// whether to run the command in quiet mode
// and print less information
Quiet bool `flag:"q,quiet"`
}
// addMetaConfigFields adds meta fields that control the config process
// to the given map of fields. These fields have no actual effect and
// map to a placeholder value because they are handled elsewhere, but
// they must be set to prevent errors about missing flags. The flags
// that it adds are those in [metaConfig].
func addMetaConfigFields(allFields *fields) {
addFields(&metaConfigFields{}, allFields, "")
}
// metaCmds is a set of commands based on [metaConfig] that
// contains a shell implementation of the help command.
var metaCmds = []*Cmd[*metaConfig]{
{
Func: func(mc *metaConfig) error { return nil }, // this gets handled seperately in [Config], so we don't actually need to do anything here
Name: "help",
Doc: "show usage information for a command",
Root: true,
},
}
// OnConfigurer represents a configuration object that specifies a method to
// be called at the end of the [config] function, with the command that has
// been parsed as an argument.
type OnConfigurer interface {
OnConfig(cmd string) error
}
// config is the main, high-level configuration setting function,
// processing config files and command-line arguments in the following order:
// - Apply any `default:` field tag default values.
// - Look for `--config`, `--cfg`, or `-c` arg, specifying a config file on the command line.
// - Fall back on default config file name passed to `config` function, if arg not found.
// - Read any `Include[s]` files in config file in deepest-first (natural) order,
// then the specified config file last.
// - If multiple config files are found, then they are applied in reverse order, meaning
// that the first specified file takes the highest precedence.
// - Process command-line args based on config field names.
// - Boolean flags are set on with plain -flag; use No prefix to turn off
// (or explicitly set values to true or false).
//
// config also processes -help and -h by printing the [usage] and quitting immediately.
// It takes [Options] that control its behavior, the configuration struct, which is
// what it sets, and the commands, which it uses for context. Also, it uses [os.Args]
// for its command-line arguments. It returns the command, if any, that was passed in
// [os.Args], and any error that ocurred during the configuration process.
func config[T any](opts *Options, cfg T, cmds ...*Cmd[T]) (string, error) {
var errs []error
err := SetFromDefaults(cfg)
if err != nil {
errs = append(errs, err)
}
args := os.Args[1:]
// first, we do a pass to get the meta command flags
// (help and config), which we need to know before
// we can do other configuration.
mc := &metaConfig{}
// we ignore not found flags in meta config, because we only care about meta config and not anything else being passed to the command
cmd, err := SetFromArgs(mc, args, NoErrNotFound, metaCmds...)
if err != nil {
// if we can't do first set for meta flags, we return immediately (we only do AllErrors for more specific errors)
return cmd, fmt.Errorf("error doing meta configuration: %w", err)
}
logx.UserLevel = logx.LevelFromFlags(mc.VeryVerbose, mc.Verbose, mc.Quiet)
// both flag and command trigger help
if mc.Help || cmd == "help" {
// string version of args slice has [] on the side, so need to get rid of them
mc.HelpCmd = strings.TrimPrefix(strings.TrimSuffix(mc.HelpCmd, "]"), "[")
// if flag and no posargs, will be nil
if mc.HelpCmd == "nil" {
mc.HelpCmd = ""
}
fmt.Println(usage(opts, cfg, mc.HelpCmd, cmds...))
os.Exit(0)
}
var cfgFiles []string
if mc.Config != "" {
cfgFiles = append(cfgFiles, mc.Config)
}
cfgFiles = append(cfgFiles, opts.DefaultFiles...)
if opts.SearchUp {
wd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("error getting current directory: %w", err)
}
pwd := wd
for {
pwd = wd
wd = filepath.Dir(pwd)
if wd == pwd { // if there is no change, we have reached the root of the filesystem
break
}
opts.IncludePaths = append(opts.IncludePaths, wd)
}
}
if opts.NeedConfigFile && len(cfgFiles) == 0 {
return "", errors.New("cli.Config: no config file or default files specified")
}
slices.Reverse(opts.IncludePaths)
gotAny := false
for _, fn := range cfgFiles {
err = openWithIncludes(opts, cfg, fn)
if err == nil {
gotAny = true
}
}
if !gotAny && opts.NeedConfigFile {
return "", errors.New("cli.Config: no config files found")
}
cmd, err = SetFromArgs(cfg, args, ErrNotFound, cmds...)
if err != nil {
errs = append(errs, err)
}
if cfer, ok := any(cfg).(OnConfigurer); ok {
err := cfer.OnConfig(cmd)
if err != nil {
errs = append(errs, err)
}
}
return cmd, errors.Join(errs...)
}
// 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 cli
import (
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/reflectx"
)
// SetFromDefaults sets the values of the given config object
// from `default:` struct field tag values. Errors are automatically
// logged in addition to being returned.
func SetFromDefaults(cfg any) error {
return errors.Log(reflectx.SetFromDefaultTags(cfg))
}
// 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 cli
import (
"fmt"
"strings"
"unicode"
"cogentcore.org/core/types"
"github.com/mattn/go-shellwords"
)
// ParseDirective parses and returns a comment directive from
// the given comment string. The returned directive will be nil
// if there is no directive contained in the given comment.
// Directives are of the following form (the slashes are optional):
//
// //tool:directive args...
func ParseDirective(comment string) (*types.Directive, error) {
comment = strings.TrimPrefix(comment, "//")
rs := []rune(comment)
if len(rs) == 0 || unicode.IsSpace(rs[0]) { // directives must not have whitespace as their first character
return nil, nil
}
// directives can not have newlines
if strings.Contains(comment, "\n") {
return nil, nil
}
before, after, found := strings.Cut(comment, ":")
if !found {
return nil, nil
}
directive := &types.Directive{}
directive.Tool = before
args, err := shellwords.Parse(after)
if err != nil {
return nil, fmt.Errorf("error parsing args: %w", err)
}
directive.Args = args
if len(args) > 0 {
directive.Directive = directive.Args[0]
directive.Args = directive.Args[1:]
}
if len(directive.Args) == 0 {
directive.Args = nil
}
return directive, nil
}
// 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 main
import (
"fmt"
"cogentcore.org/core/base/logx"
"cogentcore.org/core/cli"
)
//go:generate core generate -add-types -add-funcs
type Config struct {
// the name of the user
Name string `flag:"name,nm,n"`
// the age of the user
Age int
// whether the user likes Go
LikesGo bool
Build BuildConfig `cmd:"build"`
Server Server
Client Client
// the directory to build in
Dir string
}
type BuildConfig struct {
// the target platform to build for
Target string `flag:"target,build-target" posarg:"0"`
// the platform to build the executable for
Platform string `posarg:"1" required:"-"`
}
type Server struct {
// the server platform
Platform string
}
type Client struct {
// the client platform
Platform string `nest:"-"`
}
// Build builds the app for the config platform and target. It builds apps
// across platforms using the GOOS and GOARCH environment variables and a
// suitable C compiler located on the system.
//
// It is the main command used during a local development workflow, and
// it serves as a direct replacement for go build when building Cogent Core
// apps. In addition to the basic capacities of go build, Build supports
// cross-compiling CGO applications with ease. Also, it handles the
// bundling of icons and fonts into the executable.
//
// Build also uses GoMobile to support the building of .apk and .app
// files for Android and iOS mobile platforms, respectively. Its simple,
// unified, and configurable API for building applications makes it
// the best way to build applications, whether for local debug versions
// or production releases.
func Build(c *Config) error {
fmt.Println("Building for target", c.Build.Target, "and platform", c.Build.Platform, "- user likes go:", c.LikesGo)
return nil
}
// Run runs the app for the given user.
func Run(c *Config) error {
fmt.Println("Running for user", c.Name, "- likes go:", c.LikesGo, "- user level:", logx.UserLevel)
return nil
}
// Mod configures module information.
func Mod(c *Config) error {
fmt.Println("running mod")
return nil
}
// ModTidy tidies module information.
//
//cli:cmd -name "mod tidy"
func ModTidy(c *Config) error {
fmt.Println("running mod tidy")
return nil
}
// ModTidyRemote tidies module information for the remote.
//
//cli:cmd -name "mod tidy remote"
func ModTidyRemote(c *Config) error {
fmt.Println("running mod tidy remote")
return nil
}
// ModTidyRemoteSetURL tidies module information for the remote
// and sets its URL.
//
//cli:cmd -name "mod tidy remote set-url"
func ModTidyRemoteSetURL(c *Config) error {
fmt.Println("running mod tidy remote set-url")
return nil
}
func main() { //types:skip
opts := cli.DefaultOptions("Basic", "Basic is a basic example application made with cli.")
cli.Run(opts, &Config{}, Build, Run, Mod, ModTidy, ModTidyRemote, ModTidyRemoteSetURL)
}
// 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 cli
import (
"log/slog"
"os"
"reflect"
"strings"
"slices"
"cogentcore.org/core/base/ordmap"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/base/strcase"
)
// field represents a struct field in a configuration struct.
type field struct {
// Field is the reflect struct field object for this field
Field reflect.StructField
// Value is the reflect value of the settable pointer to this field
Value reflect.Value
// Struct is the parent struct that contains this field
Struct reflect.Value
// Name is the fully qualified, nested name of this field (eg: A.B.C).
// It is as it appears in code, and is NOT transformed something like kebab-case.
Name string
// Names contains all of the possible end-user names for this field as a flag.
// It defaults to the name of the field, but custom names can be specified via
// the cli struct tag.
Names []string
}
// fields is a simple type alias for an ordered map of [field] objects.
type fields = ordmap.Map[string, *field]
// addAllFields, when passed as the command to [addFields], indicates
// to add all fields, regardless of their command association.
const addAllFields = "*"
// addFields adds to the given fields map all of the fields of the given
// object, in the context of the given command name. A value of [addAllFields]
// for cmd indicates to add all fields, regardless of their command association.
func addFields(obj any, allFields *fields, cmd string) {
addFieldsImpl(obj, "", "", allFields, map[string]*field{}, cmd)
}
// addFieldsImpl is the underlying implementation of [addFields].
// The path is the current path state, the cmdPath is the
// current path state without command-associated names,
// and usedNames is a map keyed by used CamelCase names with values
// of their associated fields, used to track naming conflicts. The
// [field.Name]s of the fields are set based on the path, whereas the
// names of the flags are set based on the command path. The difference
// between the two is that the path is always fully qualified, whereas the
// command path omits the names of structs associated with commands via
// the "cmd" struct tag, as the user already knows what command they are
// running, so they do not need that duplicated specificity for every flag.
func addFieldsImpl(obj any, path string, cmdPath string, allFields *fields, usedNames map[string]*field, cmd string) {
ov := reflect.ValueOf(obj)
if reflectx.IsNil(ov) {
return
}
val := reflectx.NonPointerValue(ov)
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
fv := val.Field(i)
pval := reflectx.PointerValue(fv)
cmdTag, hct := f.Tag.Lookup("cmd")
cmds := strings.Split(cmdTag, ",")
if hct && !slices.Contains(cmds, cmd) && !slices.Contains(cmds, addAllFields) { // if we are associated with a different command, skip
continue
}
if reflectx.NonPointerType(f.Type).Kind() == reflect.Struct {
nwPath := f.Name
if path != "" {
nwPath = path + "." + nwPath
}
nwCmdPath := f.Name
// if we have a command tag, we don't scope our command path with our name,
// as we don't need it because we ran that command and know we are in it
if hct {
nwCmdPath = ""
}
if cmdPath != "" {
nwCmdPath = cmdPath + "." + nwCmdPath
}
addFieldsImpl(reflectx.PointerValue(fv).Interface(), nwPath, nwCmdPath, allFields, usedNames, cmd)
// we still add ourself if we are a struct, so we keep going,
// unless we are associated with a command, in which case there
// is no point in adding ourself
if hct {
continue
}
}
// we first add our unqualified command name, which is the best case scenario
name := f.Name
names := []string{name}
// then, we set our future [Field.Name] to the fully path scoped version (but we don't add it as a command name)
if path != "" {
name = path + "." + name
}
// then, we set add our command path scoped name as a command name
if cmdPath != "" {
names = append(names, cmdPath+"."+f.Name)
}
flagTag, ok := f.Tag.Lookup("flag")
if ok {
names = strings.Split(flagTag, ",")
if len(names) == 0 {
slog.Error("programmer error: expected at least one name in flag struct tag, but got none")
}
}
nf := &field{
Field: f,
Value: pval,
Struct: ov,
Name: name,
Names: names,
}
for i, name := range nf.Names {
// duplicate deletion can cause us to get out of range
if i >= len(nf.Names) {
break
}
name := strcase.ToCamel(name) // everybody is in camel for naming conflict check
if of, has := usedNames[name]; has { // we have a conflict
// if we have a naming conflict between two fields with the same base
// (in the same parent struct), then there is no nesting and they have
// been directly given conflicting names, so there is a simple programmer error
nbase := ""
nli := strings.LastIndex(nf.Name, ".")
if nli >= 0 {
nbase = nf.Name[:nli]
}
obase := ""
oli := strings.LastIndex(of.Name, ".")
if oli >= 0 {
obase = of.Name[:oli]
}
if nbase == obase {
slog.Error("programmer error: cli: two fields were assigned the same name", "name", name, "field0", of.Name, "field1", nf.Name)
os.Exit(1)
}
// if that isn't the case, they are in different parent structs and
// it is a nesting problem, so we use the nest tags to resolve the conflict.
// the basic rule is that whoever specifies the nest:"-" tag gets to
// be non-nested, and if no one specifies it, everyone is nested.
// if both want to be non-nested, that is a programmer error.
// nest field tag values for new and other
nfns := nf.Field.Tag.Get("nest")
ofns := of.Field.Tag.Get("nest")
// whether new and other get to have non-nested version
nfn := nfns == "-" || nfns == "false"
ofn := ofns == "-" || ofns == "false"
if nfn && ofn {
slog.Error(`programmer error: cli: nest:"-" specified on two config fields with the same name; keep nest:"-" on the field you want to be able to access without nesting and remove it from the other one`, "name", name, "field0", of.Name, "field1", nf.Name, "exampleFlagWithoutNesting", "-"+name, "exampleFlagWithNesting", "-"+strcase.ToKebab(nf.Name))
os.Exit(1)
} else if !nfn && !ofn {
// neither one gets it, so we replace both with fully qualified name
applyShortestUniqueName(nf, i, usedNames)
for i, on := range of.Names {
if on == name {
applyShortestUniqueName(of, i, usedNames)
}
}
} else if nfn && !ofn {
// we get it, so we keep ours as is and replace them with fully qualified name
for i, on := range of.Names {
if on == name {
applyShortestUniqueName(of, i, usedNames)
}
}
// we also need to update the field for our name to us
usedNames[name] = nf
} else if !nfn && ofn {
// they get it, so we replace ours with fully qualified name
applyShortestUniqueName(nf, i, usedNames)
}
} else {
// if no conflict, we get the name
usedNames[name] = nf
}
}
allFields.Add(name, nf)
}
}
// applyShortestUniqueName uses [shortestUniqueName] to apply the shortest
// unique name for the given field, in the context of the given
// used names, at the given index.
func applyShortestUniqueName(field *field, idx int, usedNames map[string]*field) {
nm := shortestUniqueName(field.Name, usedNames)
// if we already have this name, we don't need to add it, so we just delete this entry
if slices.Contains(field.Names, nm) {
field.Names = slices.Delete(field.Names, idx, idx+1)
} else {
field.Names[idx] = nm
usedNames[nm] = field
}
}
// shortestUniqueName returns the shortest unique camel-case name for
// the given fully qualified nest name of a field, using the given
// map of used names. It works backwards, so, for example, if given "A.B.C.D",
// it would check "D", then "C.D", then "B.C.D", and finally "A.B.C.D".
func shortestUniqueName(name string, usedNames map[string]*field) string {
strs := strings.Split(name, ".")
cur := ""
for i := len(strs) - 1; i >= 0; i-- {
if cur == "" {
cur = strs[i]
} else {
cur = strs[i] + "." + cur
}
if _, has := usedNames[cur]; !has {
return cur
}
}
return cur // TODO: this should never happen, but if it does, we might want to print an error
}
// 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.
// note: FindFileOnPaths adapted from viper package https://github.com/spf13/viper
// Copyright (c) 2014 Steve Francia
package cli
import (
"errors"
"reflect"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/iox/tomlx"
"cogentcore.org/core/base/reflectx"
)
// TODO(kai): this seems bad
// includer is an interface that facilitates processing
// include files in configuration objects.
type includer interface {
// IncludesPtr returns a pointer to the "Includes []string"
// field containing file(s) to include before processing
// the current config file.
IncludesPtr() *[]string
}
// includeStack returns the stack of include files in the natural
// order in which they are encountered (nil if none).
// Files should then be read in reverse order of the slice.
// Returns an error if any of the include files cannot be found on IncludePath.
// Does not alter cfg.
func includeStack(opts *Options, cfg includer) ([]string, error) {
clone := reflect.New(reflectx.NonPointerType(reflect.TypeOf(cfg))).Interface().(includer)
*clone.IncludesPtr() = *cfg.IncludesPtr()
return includeStackImpl(opts, clone, nil)
}
// includeStackImpl implements IncludeStack, operating on cloned cfg
// todo: could use a more efficient method to just extract the include field..
func includeStackImpl(opts *Options, clone includer, includes []string) ([]string, error) {
incs := *clone.IncludesPtr()
ni := len(incs)
if ni == 0 {
return includes, nil
}
for i := ni - 1; i >= 0; i-- {
includes = append(includes, incs[i]) // reverse order so later overwrite earlier
}
var errs []error
for _, inc := range incs {
*clone.IncludesPtr() = nil
err := tomlx.OpenFiles(clone, fsx.FindFilesOnPaths(opts.IncludePaths, inc)...)
if err == nil {
includes, err = includeStackImpl(opts, clone, includes)
if err != nil {
errs = append(errs, err)
}
} else {
errs = append(errs, err)
}
}
return includes, errors.Join(errs...)
}
// 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 cli
import (
"fmt"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/iox/tomlx"
)
// openWithIncludes reads the config struct from the given config file
// using the given options, looking on [Options.IncludePaths] for the file.
// It opens any Includes specified in the given config file in the natural
// include order so that includers overwrite included settings.
// Is equivalent to Open if there are no Includes. It returns an error if
// any of the include files cannot be found on [Options.IncludePaths].
func openWithIncludes(opts *Options, cfg any, file string) error {
files := fsx.FindFilesOnPaths(opts.IncludePaths, file)
if len(files) == 0 {
return fmt.Errorf("OpenWithIncludes: no files found for %q", file)
}
err := tomlx.OpenFiles(cfg, files...)
if err != nil {
return err
}
incfg, ok := cfg.(includer)
if !ok {
return err
}
incs, err := includeStack(opts, incfg)
ni := len(incs)
if ni == 0 {
return err
}
for i := ni - 1; i >= 0; i-- {
inc := incs[i]
err = tomlx.OpenFiles(cfg, fsx.FindFilesOnPaths(opts.IncludePaths, inc)...)
if err != nil {
fmt.Println(err)
}
}
// reopen original
err = tomlx.OpenFiles(cfg, fsx.FindFilesOnPaths(opts.IncludePaths, file)...)
if err != nil {
return err
}
*incfg.IncludesPtr() = incs
return err
}
// 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 cli
import "cogentcore.org/core/base/strcase"
// Options contains the options passed to cli
// that control its behavior.
type Options struct {
// AppName is the name of the cli app.
AppName string
// AppAbout is the description of the cli app.
AppAbout string
// Fatal is whether to, if there is an error in [Run],
// print it and fatally exit the program through [os.Exit]
// with an exit code of 1.
Fatal bool
// PrintSuccess is whether to print a message indicating
// that a command was successful after it is run, unless
// the user passes -q or -quiet to the command, in which
// case the success message will always not be printed.
PrintSuccess bool
// DefaultEncoding is the default encoding format for config files.
// currently toml is the only supported format, but others could be added
// if needed.
DefaultEncoding string
// DefaultFiles are the default configuration file paths
DefaultFiles []string
// IncludePaths is a list of file paths to try for finding config files
// specified in Include field or via the command line --config --cfg or -c args.
// Set this prior to calling Config; default is current directory '.' and 'configs'.
// The include paths are searched in reverse order such that first specified include
// paths get the highest precedence (config files found in earlier include paths
// override those found in later ones).
IncludePaths []string
// SearchUp indicates whether to search up the filesystem
// for the default config file by checking the provided default
// config file location relative to each directory up the tree
SearchUp bool
// NeedConfigFile indicates whether a configuration file
// must be provided for the command to run
NeedConfigFile bool
}
// DefaultOptions returns a new [Options] value
// with standard default values, based on the given
// app name and optional app about info.
func DefaultOptions(name string, about ...string) *Options {
abt := ""
if len(about) > 0 {
abt = about[0]
}
return &Options{
AppName: name,
AppAbout: abt,
Fatal: true,
PrintSuccess: true,
DefaultEncoding: "toml",
DefaultFiles: []string{strcase.ToKebab(name) + ".toml"},
IncludePaths: []string{".", "configs"},
}
}
// 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 cli
import (
"fmt"
"log/slog"
"os"
"runtime/debug"
"slices"
"strconv"
"strings"
"cogentcore.org/core/base/logx"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/types"
)
// indent is the value used for indentation in [usage].
var indent = " "
// usage returns a usage string based on the given options,
// configuration struct, current command, and available commands.
// It contains [AppAbout], a list of commands and their descriptions,
// and a list of flags and their descriptions, scoped based on the
// current command and its associated commands and configuration.
// The resulting string contains color escape codes.
func usage[T any](opts *Options, cfg T, cmd string, cmds ...*Cmd[T]) string {
var b strings.Builder
if cmd == "" {
if opts.AppAbout != "" {
b.WriteString("\n" + opts.AppAbout + "\n\n")
}
} else {
gotCmd := false
for _, c := range cmds {
if c.Name == cmd {
if c.Doc != "" {
b.WriteString("\n" + c.Doc + "\n\n")
}
gotCmd = true
break
}
}
if !gotCmd {
fmt.Println(logx.CmdColor(cmdName()+" help") + logx.ErrorColor(fmt.Sprintf(" failed: command %q not found", cmd)))
os.Exit(1)
}
}
if bi, ok := debug.ReadBuildInfo(); ok {
revision, time := "dev", "unknown"
for _, set := range bi.Settings {
if set.Key == "vcs.revision" {
revision = set.Value
}
if set.Key == "vcs.time" {
time = set.Value
}
}
b.WriteString(logx.TitleColor("Version: ") + fmt.Sprintf("%s (%s)\n\n", revision, time))
}
fs := &fields{}
addFields(cfg, fs, cmd)
cmdName := cmdName()
if cmd != "" {
cmdName += " " + cmd
}
b.WriteString(logx.TitleColor("Usage:\n") + indent + logx.CmdColor(cmdName+" "))
posArgStrs := []string{}
for _, kv := range fs.Order {
v := kv.Value
f := v.Field
posArgTag, ok := f.Tag.Lookup("posarg")
if ok {
ui := uint64(0)
if posArgTag == "all" || posArgTag == "leftover" {
ui = uint64(len(posArgStrs))
} else {
var err error
ui, err = strconv.ParseUint(posArgTag, 10, 64)
if err != nil {
slog.Error("programmer error: invalid value for posarg struct tag", "field", f.Name, "posArgTag", posArgTag, "err", err)
}
}
// if the slice isn't big enough, grow it to fit this posarg
if ui >= uint64(len(posArgStrs)) {
posArgStrs = slices.Grow(posArgStrs, len(posArgStrs)-int(ui)+1) // increase capacity
posArgStrs = posArgStrs[:ui+1] // extend to capacity
}
nm := strcase.ToKebab(v.Names[0])
req, has := f.Tag.Lookup("required")
if req == "+" || req == "true" || !has { // default is required, so !has => required
posArgStrs[ui] = logx.CmdColor("<" + nm + ">")
} else {
posArgStrs[ui] = logx.SuccessColor("[" + nm + "]")
}
}
}
b.WriteString(strings.Join(posArgStrs, " "))
if len(posArgStrs) > 0 {
b.WriteString(" ")
}
b.WriteString(logx.SuccessColor("[flags]\n"))
commandUsage(&b, cmdName, cmd, cmds...)
b.WriteString(logx.TitleColor("\nFlags:\n") + indent + logx.TitleColor("Flags are case-insensitive, can be in kebab-case, snake_case,\n"))
b.WriteString(indent + logx.TitleColor("or CamelCase, and can have one or two leading dashes. Use a\n"))
b.WriteString(indent + logx.TitleColor("\"no\" prefix to turn off a bool flag.\n\n"))
// add meta ones (help, config, verbose, etc) first
mcfields := &fields{}
addMetaConfigFields(mcfields)
flagUsage(mcfields, &b)
flagUsage(fs, &b)
return b.String()
}
// commandUsage adds the command usage info for the given commands to the
// given [strings.Builder].
// It also takes the full name of our command as it appears in the terminal (cmdName),
// (eg: "core build"), and the name of the command we are running (eg: "build").
//
// To be a command that is included in the usage, we must be one command
// nesting depth (subcommand) deeper than the current command (ie, if we
// are on "x", we can see usage for commands of the form "x y"), and all
// of our commands must be consistent with the current command. For example,
// "" could generate usage for "help", "build", and "run", and "mod" could
// generate usage for "mod init", "mod tidy", and "mod edit". This ensures
// that only relevant commands are shown in the usage.
func commandUsage[T any](b *strings.Builder, cmdName string, cmd string, cmds ...*Cmd[T]) {
acmds := []*Cmd[T]{} // actual commands we care about
var rcmd *Cmd[T] // root command
cmdstrs := strings.Fields(cmd) // subcommand strings in passed command
// need this label so that we can continue outer loop when we have non-matching cmdstr
outer:
for _, c := range cmds {
cstrs := strings.Fields(c.Name) // subcommand strings in command we are checking
if len(cstrs) != len(cmdstrs)+1 { // we must be one deeper
continue
}
for i, cmdstr := range cmdstrs {
if cmdstr != cstrs[i] { // every subcommand so far must match
continue outer
}
}
if c.Root {
rcmd = c
} else if c.Name != cmd { // if it is the same subcommand we are already on, we handle it above in main Usage
acmds = append(acmds, c)
}
}
if len(acmds) != 0 {
b.WriteString(indent + logx.CmdColor(cmdName+" <subcommand> ") + logx.SuccessColor("[flags]\n"))
}
if rcmd != nil {
b.WriteString(logx.TitleColor("\nDefault command:\n"))
b.WriteString(indent + logx.CmdColor(rcmd.Name) + "\n" + indent + indent + strings.ReplaceAll(rcmd.Doc, "\n", "\n"+indent+indent) + "\n") // need to put two indents on every newline for formatting
}
if len(acmds) == 0 && cmd != "" { // nothing to do
return
}
b.WriteString(logx.TitleColor("\nSubcommands:\n"))
// if we are in root, we also add help
if cmd == "" {
b.WriteString(indent + logx.CmdColor("help") + "\n" + indent + indent + "Help shows usage information for a command\n")
}
for _, c := range acmds {
b.WriteString(indent + logx.CmdColor(c.Name))
if c.Doc != "" {
// we only want the first paragraph of text for subcommand usage; after that is where more specific details can go
doc, _, _ := strings.Cut(c.Doc, "\n\n")
b.WriteString("\n" + indent + indent + strings.ReplaceAll(doc, "\n", "\n"+indent+indent)) // need to put two indents on every newline for formatting
}
b.WriteString("\n")
}
}
// flagUsage adds the flag usage info for the given fields
// to the given [strings.Builder].
func flagUsage(fields *fields, b *strings.Builder) {
for _, kv := range fields.Order {
f := kv.Value
b.WriteString(indent)
for i, name := range f.Names {
b.WriteString(logx.CmdColor("-" + strcase.ToKebab(name)))
if i != len(f.Names)-1 {
b.WriteString(", ")
}
}
b.WriteString(" " + logx.SuccessColor(f.Field.Type.String()))
b.WriteString("\n")
field := types.GetField(f.Struct, f.Field.Name)
if field != nil {
b.WriteString(indent + indent + strings.ReplaceAll(field.Doc, "\n", "\n"+indent+indent)) // need to put two indents on every newline for formatting
}
def, ok := f.Field.Tag.Lookup("default")
if ok && def != "" {
b.WriteString(fmt.Sprintf(" (default: %s)", def))
}
b.WriteString("\n")
}
}
// 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 cmd provides utilities for managing
// apps and packages that use the Cogent Core framework.
package cmd
//go:generate core generate
import (
"errors"
"fmt"
"os"
"path/filepath"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/cmd/core/config"
"cogentcore.org/core/cmd/core/mobile"
"cogentcore.org/core/cmd/core/web"
)
// Build builds an executable for the package
// at the config path for the config platforms.
func Build(c *config.Config) error { //types:add
if len(c.Build.Target) == 0 {
return errors.New("build: expected at least 1 platform")
}
for _, platform := range c.Build.Target {
err := config.OSSupported(platform.OS)
if err != nil {
return err
}
if platform.Arch != "*" {
err := config.ArchSupported(platform.Arch)
if err != nil {
return err
}
}
if platform.OS == "android" || platform.OS == "ios" {
return mobile.Build(c)
}
if platform.OS == "web" {
err := os.MkdirAll(c.Build.Output, 0777)
if err != nil {
return err
}
return web.Build(c)
}
err = buildDesktop(c, platform)
if err != nil {
return fmt.Errorf("build: %w", err)
}
}
return nil
}
// buildDesktop builds an executable for the config package for the given desktop platform.
func buildDesktop(c *config.Config, platform config.Platform) error {
xc := exec.Major()
xc.Env["GOOS"] = platform.OS
xc.Env["GOARCH"] = platform.Arch
args := []string{"build"}
if c.Build.Debug {
args = append(args, "-tags", "debug")
}
if c.Build.Trimpath {
args = append(args, "-trimpath")
}
ldflags := ""
output := filepath.Base(c.Build.Output)
if platform.OS == "windows" {
output += ".exe"
// see https://stackoverflow.com/questions/23250505/how-do-i-create-an-executable-from-golang-that-doesnt-open-a-console-window-whe
if c.Build.Windowsgui {
ldflags += " -H=windowsgui"
}
}
ldflags += " " + config.LinkerFlags(c)
args = append(args, "-ldflags", ldflags, "-o", filepath.Join(c.Build.Output, output))
err := xc.Run("go", args...)
if err != nil {
return fmt.Errorf("error building for platform %s/%s: %w", platform.OS, platform.Arch, err)
}
return nil
}
// 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 cmd
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"sync"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/base/logx"
"cogentcore.org/core/cmd/core/config"
)
// Changed concurrently prints all of the repositories within this directory
// that have been changed and need to be updated in Git.
func Changed(c *config.Config) error { //types:add
wg := sync.WaitGroup{}
errs := []error{}
fs.WalkDir(os.DirFS("."), ".", func(path string, d fs.DirEntry, err error) error {
wg.Add(1)
go func() {
defer wg.Done()
if d.Name() != ".git" {
return
}
dir := filepath.Dir(path)
out, err := exec.Major().SetDir(dir).Output("git", "diff")
if err != nil {
errs = append(errs, fmt.Errorf("error getting diff of %q: %w", dir, err))
return
}
if out != "" { // if we have a diff, we have been changed
fmt.Println(logx.CmdColor(dir))
return
}
// if we don't have a diff, we also check to make sure we aren't ahead of the remote
out, err = exec.Minor().SetDir(dir).Output("git", "status")
if err != nil {
errs = append(errs, fmt.Errorf("error getting status of %q: %w", dir, err))
return
}
if strings.Contains(out, "Your branch is ahead") { // if we are ahead, we have been changed
fmt.Println(logx.CmdColor(dir))
}
}()
return nil
})
wg.Wait()
fmt.Println("")
return errors.Join(errs...)
}
// 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 cmd
import (
"fmt"
"path/filepath"
"runtime"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/cmd/core/config"
"cogentcore.org/core/cmd/core/mobile"
)
// Install installs the package on the local system.
// It uses the same config info as build.
func Install(c *config.Config) error { //types:add
for i, p := range c.Build.Target {
err := config.OSSupported(p.OS)
if err != nil {
return fmt.Errorf("install: %w", err)
}
if p.Arch == "*" {
if p.OS == "android" || p.OS == "ios" {
p.Arch = "arm64"
} else {
p.Arch = runtime.GOARCH
}
c.Build.Target[i] = p
}
switch p.OS {
case "android", "ios":
err := Build(c)
if err != nil {
return fmt.Errorf("error building: %w", err)
}
// we only want this target for install
ot := c.Build.Target
c.Build.Target = []config.Platform{p}
err = mobile.Install(c)
c.Build.Target = ot
if err != nil {
return fmt.Errorf("install: %w", err)
}
case "web":
return fmt.Errorf("can not install on platform web; use build or run instead")
case "darwin":
c.Pack.DMG = false
err := Pack(c)
if err != nil {
return err
}
return exec.Run("cp", "-a", filepath.Join(c.Build.Output, c.Name+".app"), "/Applications")
default:
return exec.Major().SetEnv("GOOS", p.OS).SetEnv("GOARCH", runtime.GOARCH).Run("go", "install")
}
}
return nil
}
// 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 cmd
import (
"fmt"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/cmd/core/config"
)
// Log prints the logs from your app running on Android to the terminal.
// Android is the only supported platform for log; use the -debug flag on
// run for other platforms.
func Log(c *config.Config) error { //types:add
if c.Log.Target != "android" {
return fmt.Errorf("only android is supported for log; use the -debug flag on run for other platforms")
}
if !c.Log.Keep {
err := exec.Run("adb", "logcat", "-c")
if err != nil {
return fmt.Errorf("error clearing logs: %w", err)
}
}
// we are logging continiously so we can't buffer, and we must be verbose
err := exec.Verbose().SetBuffer(false).Run("adb", "logcat", "*:"+c.Log.All, "Go:D", "GoLog:D", "GoLogWGPU:D")
if err != nil {
return fmt.Errorf("erroring getting logs: %w", err)
}
return nil
}
// 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 cmd
import (
_ "embed"
"os"
"path/filepath"
"strings"
"text/template"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/cmd/core/config"
"cogentcore.org/core/cmd/core/rendericon"
"github.com/jackmordaunt/icns/v2"
)
// Pack builds and packages the app for the target platform.
// For android, ios, and web, it is equivalent to build.
func Pack(c *config.Config) error { //types:add
err := Build(c)
if err != nil {
return err
}
for _, platform := range c.Build.Target {
switch platform.OS {
case "android", "ios", "web": // build already packages
continue
case "linux":
err := packLinux(c)
if err != nil {
return err
}
case "darwin":
err := packDarwin(c)
if err != nil {
return err
}
case "windows":
err := packWindows(c)
if err != nil {
return err
}
}
}
return nil
}
// packLinux packages the app for Linux by generating a .deb file.
func packLinux(c *config.Config) error {
// based on https://ubuntuforums.org/showthread.php?t=910717
anm := strcase.ToKebab(c.Name)
vnm := strings.TrimPrefix(c.Version, "v")
avnm := anm + "_" + vnm
bpath := c.Build.Output
apath := filepath.Join(bpath, avnm)
ulbpath := filepath.Join(apath, "usr", "local", "bin")
usipath := filepath.Join(apath, "usr", "share", "icons", "hicolor")
usapath := filepath.Join(apath, "usr", "share", "applications")
dpath := filepath.Join(apath, "DEBIAN")
err := os.MkdirAll(ulbpath, 0777)
if err != nil {
return err
}
err = exec.Run("cp", "-p", filepath.Base(c.Build.Output), filepath.Join(ulbpath, anm))
if err != nil {
return err
}
// see https://martin.hoppenheit.info/blog/2016/where-to-put-application-icons-on-linux/
// TODO(kai): consider rendering more icon sizes and/or an XPM icon
ic, err := rendericon.Render(48)
if err != nil {
return err
}
i48path := filepath.Join(usipath, "48x48", "apps")
err = os.MkdirAll(i48path, 0777)
if err != nil {
return err
}
err = imagex.Save(ic, filepath.Join(i48path, anm+".png"))
if err != nil {
return err
}
iscpath := filepath.Join(usipath, "scalable", "apps")
err = os.MkdirAll(iscpath, 0777)
if err != nil {
return err
}
err = exec.Run("cp", "icon.svg", filepath.Join(iscpath, anm+".svg"))
if err != nil {
return err
}
// we need a description
if c.About == "" {
c.About = c.Name
}
err = os.MkdirAll(usapath, 0777)
if err != nil {
return err
}
fapp, err := os.Create(filepath.Join(usapath, anm+".desktop"))
if err != nil {
return err
}
defer fapp.Close()
dfd := &desktopFileData{
Name: c.Name,
Desc: c.About,
Exec: anm,
}
err = desktopFileTmpl.Execute(fapp, dfd)
if err != nil {
return err
}
err = os.MkdirAll(dpath, 0777)
if err != nil {
return err
}
fctrl, err := os.Create(filepath.Join(dpath, "control"))
if err != nil {
return err
}
defer fctrl.Close()
dcd := &debianControlData{
Name: anm,
Version: vnm,
Desc: c.About,
}
err = debianControlTmpl.Execute(fctrl, dcd)
if err != nil {
return err
}
return exec.Run("dpkg-deb", "--build", apath)
}
// desktopFileData is the data passed to [desktopFileTmpl]
type desktopFileData struct {
Name string
Desc string
Exec string
}
// TODO(kai): project website
// desktopFileTmpl is the template for the Linux .desktop file
var desktopFileTmpl = template.Must(template.New("desktopFileTmpl").Parse(
`[Desktop Entry]
Type=Application
Version=1.0
Name={{.Name}}
Comment={{.Desc}}
Exec={{.Exec}}
Icon={{.Exec}}
Terminal=false
`))
// debianControlData is the data passed to [debianControlTmpl]
type debianControlData struct {
Name string
Version string
Desc string
}
// TODO(kai): architecture, maintainer, dependencies
// debianControlTmpl is the template for the Linux DEBIAN/control file
var debianControlTmpl = template.Must(template.New("debianControlTmpl").Parse(
`Package: {{.Name}}
Version: {{.Version}}
Section: base
Priority: optional
Architecture: all
Maintainer: Your Name <you@email.com>
Description: {{.Desc}}
`))
// packDarwin packages the app for macOS by generating a .app and .dmg file.
func packDarwin(c *config.Config) error {
// based on https://github.com/machinebox/appify
anm := c.Name + ".app"
bpath := c.Build.Output
apath := filepath.Join(bpath, anm)
cpath := filepath.Join(apath, "Contents")
mpath := filepath.Join(cpath, "MacOS")
rpath := filepath.Join(cpath, "Resources")
err := os.MkdirAll(mpath, 0777)
if err != nil {
return err
}
err = os.MkdirAll(rpath, 0777)
if err != nil {
return err
}
err = exec.Run("cp", "-p", filepath.Base(c.Build.Output), filepath.Join(mpath, anm))
if err != nil {
return err
}
err = exec.Run("chmod", "+x", mpath)
if err != nil {
return err
}
inm := filepath.Join(rpath, "icon.icns")
fdsi, err := os.Create(inm)
if err != nil {
return err
}
defer fdsi.Close()
// 1024x1024 is the largest icon size on macOS
sic, err := rendericon.Render(1024)
if err != nil {
return err
}
err = icns.Encode(fdsi, sic)
if err != nil {
return err
}
fplist, err := os.Create(filepath.Join(cpath, "Info.plist"))
if err != nil {
return err
}
defer fplist.Close()
ipd := &infoPlistData{
Name: c.Name,
Executable: filepath.Join("MacOS", anm),
Identifier: c.ID,
Version: c.Version,
InfoString: c.About,
ShortVersionString: c.Version,
IconFile: filepath.Join("Contents", "Resources", "icon.icns"),
}
err = infoPlistTmpl.Execute(fplist, ipd)
if err != nil {
return err
}
if !c.Pack.DMG {
return nil
}
// install dmgbuild if we don't already have it
if _, err := exec.LookPath("dmgbuild"); err != nil {
err = exec.Verbose().SetBuffer(false).Run("pip", "install", "dmgbuild")
if err != nil {
return err
}
}
dmgsnm := filepath.Join(bpath, ".tempDmgBuildSettings.py")
fdmg, err := os.Create(dmgsnm)
if err != nil {
return err
}
defer fdmg.Close()
dmgbd := &dmgBuildData{
AppPath: apath,
AppName: anm,
IconPath: inm,
}
err = dmgBuildTmpl.Execute(fdmg, dmgbd)
if err != nil {
return err
}
err = exec.Run("dmgbuild",
"-s", dmgsnm,
c.Name, filepath.Join(bpath, c.Name+".dmg"))
if err != nil {
return err
}
return os.Remove(dmgsnm)
}
// infoPlistData is the data passed to [infoPlistTmpl]
type infoPlistData struct {
Name string
Executable string
Identifier string
Version string
InfoString string
ShortVersionString string
IconFile string
}
// infoPlistTmpl is the template for the macOS .app Info.plist
var infoPlistTmpl = template.Must(template.New("infoPlistTmpl").Parse(
`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>{{ .Name }}</string>
<key>CFBundleExecutable</key>
<string>{{ .Executable }}</string>
<key>CFBundleIdentifier</key>
<string>{{ .Identifier }}</string>
<key>CFBundleVersion</key>
<string>{{ .Version }}</string>
<key>CFBundleGetInfoString</key>
<string>{{ .InfoString }}</string>
<key>CFBundleShortVersionString</key>
<string>{{ .ShortVersionString }}</string>
<key>CFBundleIconFile</key>
<string>{{ .IconFile }}</string>
</dict>
</plist>
`))
// dmgBuildData is the data passed to [dmgBuildTmpl]
type dmgBuildData struct {
AppPath string
AppName string
IconPath string
}
// dmgBuildTmpl is the template for the dmgbuild python settings file
var dmgBuildTmpl = template.Must(template.New("dmgBuildTmpl").Parse(
`files = ['{{.AppPath}}']
symlinks = {"Applications": "/Applications"}
icon = '{{.IconPath}}'
icon_locations = {'{{.AppName}}': (140, 120), "Applications": (500, 120)}
background = "builtin-arrow"
`))
// packWindows packages the app for Windows by generating a .msi file.
func packWindows(c *config.Config) error {
opath := c.Build.Output
ipath := filepath.Join(opath, "tempWindowsInstaller")
gpath := filepath.Join(ipath, "installer.go")
epath := filepath.Join(opath, c.Name+" Installer.exe")
err := os.MkdirAll(ipath, 0777)
if err != nil {
return err
}
fman, err := os.Create(gpath)
if err != nil {
return err
}
wmd := &windowsInstallerData{
Name: c.Name,
Desc: c.About,
}
err = windowsInstallerTmpl.Execute(fman, wmd)
fman.Close()
if err != nil {
return err
}
err = exec.Run("cp", filepath.Base(c.Build.Output)+".exe", filepath.Join(ipath, "app.exe"))
if err != nil {
return err
}
err = exec.Run("cp", "icon.svg", filepath.Join(ipath, "icon.svg"))
if err != nil {
return err
}
err = exec.Run("go", "build", "-o", epath, gpath)
if err != nil {
return err
}
return os.RemoveAll(ipath)
}
// windowsInstallerData is the data passed to [windowsInstallerTmpl]
type windowsInstallerData struct {
Name string
Desc string
}
//go:embed windowsinstaller.go.tmpl
var windowsInstallerTmplString string
// windowsInstallerTmpl is the template for the Windows installer Go file
var windowsInstallerTmpl = template.Must(template.New("windowsInstallerTmpl").Parse(windowsInstallerTmplString))
// 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 cmd
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sync"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/cmd/core/config"
)
// Pull concurrently pulls all of the Git repositories within the current directory.
func Pull(c *config.Config) error { //types:add
wg := sync.WaitGroup{}
errs := []error{}
fs.WalkDir(os.DirFS("."), ".", func(path string, d fs.DirEntry, err error) error {
wg.Add(1)
go func() {
defer wg.Done()
if d.Name() != ".git" {
return
}
dir := filepath.Dir(path)
err := exec.Major().SetDir(dir).Run("git", "pull")
if err != nil {
errs = append(errs, fmt.Errorf("error pulling %q: %w", dir, err))
}
}()
return nil
})
wg.Wait()
return errors.Join(errs...)
}
//go:build !windows
package cmd
func windowsRegistryAddPath(path string) error {
return nil // no-op
}
// 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 cmd
import (
"fmt"
"strings"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/cmd/core/config"
"github.com/Masterminds/semver/v3"
)
// Release releases the project with the specified git version tag.
func Release(c *config.Config) error { //types:add
err := exec.Run("git", "tag", "-a", c.Version, "-m", c.Version+" release")
if err != nil {
return fmt.Errorf("error tagging release: %w", err)
}
err = exec.Run("git", "push", "origin", "--tags")
if err != nil {
return fmt.Errorf("error pushing tags: %w", err)
}
return nil
}
// NextRelease releases the project with the current git version
// tag incremented by one patch version.
func NextRelease(c *config.Config) error { //types:add
ver, err := nextVersion(c)
if err != nil {
return err
}
c.Version = ver
return Release(c)
}
// nextVersion returns the version of the project
// incremented by one patch version.
func nextVersion(c *config.Config) (string, error) {
cur, err := exec.Output("git", "describe", "--tags", "--abbrev=0")
if err != nil {
return "", err
}
ver, err := semver.NewVersion(cur)
if err != nil {
return "", fmt.Errorf("error getting semver version from version %q: %w", c.Version, err)
}
if !strings.HasPrefix(ver.Prerelease(), "dev") { // if no dev pre-release, we can just do standard increment
*ver = ver.IncPatch()
} else { // otherwise, we have to increment pre-release version instead
pvn := strings.TrimPrefix(ver.Prerelease(), "dev")
pver, err := semver.NewVersion(pvn)
if err != nil {
return "", fmt.Errorf("error parsing dev version %q from version %q: %w", pvn, c.Version, err)
}
*pver = pver.IncPatch()
// apply incremented pre-release version to main version
nv, err := ver.SetPrerelease("dev" + pver.String())
if err != nil {
return "", fmt.Errorf("error setting pre-release of new version to %q from repository version %q: %w", "dev"+pver.String(), c.Version, err)
}
*ver = nv
}
return "v" + ver.String(), nil
}
// 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 cmd
import (
"fmt"
"path/filepath"
"runtime"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/base/logx"
"cogentcore.org/core/cmd/core/config"
"cogentcore.org/core/cmd/core/mobile"
"cogentcore.org/core/cmd/core/web"
)
// Run builds and runs the config package. It also displays the logs generated
// by the app. It uses the same config info as build.
func Run(c *config.Config) error { //types:add
if len(c.Build.Target) != 1 {
return fmt.Errorf("expected 1 target platform, but got %d (%v)", len(c.Build.Target), c.Build.Target)
}
t := c.Build.Target[0]
if t.Arch == "*" {
if t.OS == "android" || t.OS == "ios" {
t.Arch = "arm64"
} else {
t.Arch = runtime.GOARCH
}
c.Build.Target[0] = t
}
if t.OS == "ios" && !c.Build.Debug {
// TODO: is there a way to launch without running the debugger?
logx.PrintlnWarn("warning: only installing, not running, because there is no effective way to just launch an app on iOS from the terminal without debugging; pass the -d flag to run and debug")
}
if t.OS == "web" {
// needed for changes to show during local development
c.Web.RandomVersion = true
}
err := Build(c)
if err != nil {
return fmt.Errorf("error building app: %w", err)
}
// Build may have added iossimulator, so we get rid of it for
// the running stage (we already confirmed we were passed 1 up above)
if len(c.Build.Target) > 1 {
c.Build.Target = []config.Platform{t}
}
switch t.OS {
case "darwin", "windows", "linux":
return exec.Verbose().SetBuffer(false).Run(filepath.Join(c.Build.Output, filepath.Base(c.Build.Output)))
case "android":
err := exec.Run("adb", "install", "-r", filepath.Join(c.Build.Output, c.Name+".apk"))
if err != nil {
return fmt.Errorf("error installing app: %w", err)
}
// see https://stackoverflow.com/a/4567928
args := []string{"shell", "am", "start", "-n", c.ID + "/org.golang.app.GoNativeActivity"}
// TODO: get adb am debug on Android working
// if c.Build.Debug {
// args = append(args, "-D")
// }
err = exec.Run("adb", args...)
if err != nil {
return fmt.Errorf("error starting app: %w", err)
}
if c.Build.Debug {
return Log(c)
}
return nil
case "ios":
if !c.Build.Debug {
return mobile.Install(c)
}
return exec.Verbose().SetBuffer(false).Run("ios-deploy", "-b", filepath.Join(c.Build.Output, c.Name+".app"), "-d")
case "web":
return web.Serve(c)
}
return nil
}
// Copyright (c) 2024, 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 cmd
import (
"fmt"
"path/filepath"
"runtime"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/base/logx"
"cogentcore.org/core/cmd/core/config"
)
// Setup installs platform-specific dependencies for the current platform.
// It only needs to be called once per system.
func Setup(c *config.Config) error { //types:add
vc := exec.Verbose().SetBuffer(false)
switch runtime.GOOS {
case "darwin":
p, err := exec.Output("xcode-select", "-p")
if err != nil || p == "" {
err := vc.Run("xcode-select", "--install")
if err != nil {
return err
}
} else {
logx.PrintlnWarn("xcode tools already installed")
}
return nil
case "linux":
// Based on https://docs.fyne.io/started
if _, err := exec.LookPath("apt-get"); err == nil {
err := vc.Run("sudo", "apt-get", "update")
if err != nil {
return err
}
return vc.Run("sudo", "apt-get", "install", "-f", "-y", "gcc", "libgl1-mesa-dev", "libegl1-mesa-dev", "mesa-vulkan-drivers", "xorg-dev")
}
if _, err := exec.LookPath("dnf"); err == nil {
return vc.Run("sudo", "dnf", "install", "gcc", "libX11-devel", "libXcursor-devel", "libXrandr-devel", "libXinerama-devel", "mesa-libGL-devel", "libXi-devel", "libXxf86vm-devel")
}
if _, err := exec.LookPath("pacman"); err == nil {
return vc.Run("sudo", "pacman", "-S", "xorg-server-devel", "libxcursor", "libxrandr", "libxinerama", "libxi")
}
if _, err := exec.LookPath("eopkg"); err == nil {
return vc.Run("sudo", "eopkg", "it", "-c", "system.devel", "mesalib-devel", "libxrandr-devel", "libxcursor-devel", "libxi-devel", "libxinerama-devel")
}
if _, err := exec.LookPath("zypper"); err == nil {
return vc.Run("sudo", "zypper", "install", "gcc", "libXcursor-devel", "libXrandr-devel", "Mesa-libGL-devel", "libXi-devel", "libXinerama-devel", "libXxf86vm-devel")
}
if _, err := exec.LookPath("xbps-install"); err == nil {
return vc.Run("sudo", "xbps-install", "-S", "base-devel", "xorg-server-devel", "libXrandr-devel", "libXcursor-devel", "libXinerama-devel")
}
if _, err := exec.LookPath("apk"); err == nil {
return vc.Run("sudo", "apk", "add", "gcc", "libxcursor-dev", "libxrandr-dev", "libxinerama-dev", "libxi-dev", "linux-headers", "mesa-dev")
}
if _, err := exec.LookPath("nix-shell"); err == nil {
return vc.Run("nix-shell", "-p", "libGL", "pkg-config", "xorg.libX11.dev", "xorg.libXcursor", "xorg.libXi", "xorg.libXinerama", "xorg.libXrandr", "xorg.libXxf86vm")
}
return fmt.Errorf("unknown Linux distro; please file a bug report at https://github.com/cogentcore/core/issues")
case "windows":
if _, err := exec.LookPath("gcc"); err != nil {
err := vc.Run("curl", "-OL", "https://github.com/skeeto/w64devkit/releases/download/v2.0.0/w64devkit-x64-2.0.0.exe")
if err != nil {
return err
}
path, err := filepath.Abs("w64devkit-x64-2.0.0.exe")
if err != nil {
return err
}
err = vc.Run(path, "x", "-oC:", "-aoa")
if err != nil {
return err
}
err = windowsRegistryAddPath(`C:\w64devkit\bin`)
if err != nil {
return err
}
} else {
logx.PrintlnWarn("gcc already installed")
}
if _, err := exec.LookPath("git"); err != nil {
err := vc.Run("curl", "-OL", "https://github.com/git-for-windows/git/releases/download/v2.45.2.windows.1/Git-2.45.2-64-bit.exe")
if err != nil {
return err
}
path, err := filepath.Abs("Git-2.45.2-64-bit.exe")
if err != nil {
return err
}
err = vc.Run(path)
if err != nil {
return err
}
} else {
logx.PrintlnWarn("git already installed")
}
return nil
}
return fmt.Errorf("platform %q not supported for core setup", runtime.GOOS)
}
// 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 config contains the configuration
// structs for the Cogent Core tool.
package config
//go:generate core generate
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"unicode"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/base/iox/tomlx"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/enums/enumgen"
"cogentcore.org/core/types/typegen"
)
// Config is the main config struct that contains all of the configuration
// options for the Cogent Core command line tool.
type Config struct { //types:add
// Name is the user-friendly name of the project.
// The default is based on the current directory name.
Name string
// NamePrefix is the prefix to add to the default name of the project
// and any projects nested below it. A separating space is automatically included.
NamePrefix string
// ID is the bundle / package ID to use for the project
// (required for building for mobile platforms and packaging
// for desktop platforms). It is typically in the format com.org.app
// (eg: com.cogent.mail). It defaults to com.parentDirectory.currentDirectory.
ID string
// About is the about information for the project, which can be viewed via
// the "About" button in the app bar. It is also used when packaging the app.
About string
// the version of the project to release
Version string `cmd:"release" posarg:"0" save:"-"`
// Pages, if specified, indicates that the app has core
// pages located at this directory. If so, markdown code blocks with
// language Go (must be uppercase, as that indicates that is an
// "exported" example) will be collected and stored at pagegen.go, and
// a directory tree will be made for all of the pages when building
// for platform web. This defaults to "content" when building an app
// for platform web that imports pages.
Pages string
// the configuration options for the build, install, run, and pack commands
Build Build `cmd:"build,install,run,pack"`
// the configuration information for the pack command
Pack Pack `cmd:"pack"`
// the configuration information for web
Web Web `cmd:"build,install,run,pack"`
// the configuration options for the log and run commands
Log Log `cmd:"log,run"`
// the configuration options for the generate command
Generate Generate `cmd:"generate"`
}
type Build struct { //types:add
// the target platforms to build executables for
Target []Platform `flag:"t,target" posarg:"0" required:"-" save:"-"`
// Dir is the directory to build the app from.
// It defaults to the current directory.
Dir string
// Output is the directory to output the built app to.
// It defaults to the current directory for desktop executables
// and "bin/{platform}" for all other platforms and command "pack".
Output string `flag:"o,output"`
// Debug is whether to build/run the app in debug mode, which sets
// the "debug" tag when building and prevents the default stripping
// of debug symbols. On iOS and Android, this also prints the program output.
Debug bool `flag:"d,debug"`
// Ldflags are optional additional linker flags to pass to go build commands.
Ldflags string
// Trimpath is whether to replace file system paths with module paths
// in the resulting executable. It is on by default for commands other
// than core run.
Trimpath bool `default:"true"`
// Windowsgui is whether to make this a "Windows GUI" application that
// opens without a terminal window on Windows. It is on by default for
// commands other than core run.
Windowsgui bool `default:"true"`
// the minimum version of the iOS SDK to compile against
IOSVersion string `default:"13.0"`
// the minimum supported Android SDK (uses-sdk/android:minSdkVersion in AndroidManifest.xml)
AndroidMinSDK int `default:"23" min:"23"`
// the target Android SDK version (uses-sdk/android:targetSdkVersion in AndroidManifest.xml)
AndroidTargetSDK int `default:"29"`
}
type Pack struct { //types:add
// whether to build a .dmg file on macOS in addition to a .app file.
// This is automatically disabled for the install command.
DMG bool `default:"true"`
}
type Log struct { //types:add
// the target platform to view the logs for (ios or android)
Target string `default:"android"`
// whether to keep the previous log messages or clear them
Keep bool `default:"false"`
// messages not generated from your app equal to or above this log level will be shown
All string `default:"F"`
}
type Generate struct { //types:add
// the enum generation configuration options passed to enumgen
Enumgen enumgen.Config
// the generation configuration options passed to typegen
Typegen typegen.Config
// the source directory to run generate on (can be multiple through ./...)
Dir string `default:"." posarg:"0" required:"-" nest:"-"`
// Icons, if specified, indicates to generate an icongen.go file with
// icon variables for the icon svg files in the specified folder.
Icons string
}
func (c *Config) OnConfig(cmd string) error {
// if we have no target, we assume it is our current platform,
// unless we are in init, in which case we do not want to set
// the config file to be specific to our platform
if len(c.Build.Target) == 0 && cmd != "init" {
c.Build.Target = []Platform{{OS: runtime.GOOS, Arch: runtime.GOARCH}}
}
if c.Build.Output == "" && len(c.Build.Target) > 0 {
t := c.Build.Target[0]
if cmd == "pack" || t.OS == "web" || t.OS == "android" || t.OS == "ios" {
c.Build.Output = filepath.Join("bin", t.OS)
}
}
// we must make the output dir absolute before changing the current directory
out, err := filepath.Abs(c.Build.Output)
if err != nil {
return err
}
c.Build.Output = out
if c.Build.Dir != "" {
err := os.Chdir(c.Build.Dir)
if err != nil {
return err
}
// re-read the config file from the new location if it exists
err = tomlx.Open(c, "core.toml")
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
}
// we must do auto-naming after we apply any directory change above
if c.Name == "" || c.ID == "" {
cdir, err := os.Getwd()
if err != nil {
return fmt.Errorf("error finding current directory: %w", err)
}
base := filepath.Base(cdir)
if c.Name == "" {
c.Name = strcase.ToTitle(base)
if c.NamePrefix != "" {
c.Name = c.NamePrefix + " " + c.Name
}
}
if c.ID == "" {
dir := filepath.Dir(cdir)
// if our directory starts with a v and then has only digits, it is a version directory
// so we go up another directory to get to the actual directory
if len(dir) > 1 && dir[0] == 'v' && !strings.ContainsFunc(dir[1:], func(r rune) bool {
return !unicode.IsDigit(r)
}) {
dir = filepath.Dir(dir)
}
dir = filepath.Base(dir)
// we ignore anything after any dot in the directory name
dir, _, _ = strings.Cut(dir, ".")
// the default ID is "com.dir.base", which is relatively likely
// to be close to "com.org.app", the intended format
c.ID = "com." + dir + "." + base
}
}
if cmd == "run" {
c.Build.Trimpath = false
c.Build.Windowsgui = false
}
return nil
}
// LinkerFlags returns the ld linker flags that specify the app and core version,
// the app about information, the app icon, and the optional [Build.Ldflags].
func LinkerFlags(c *Config) string {
res := ""
if c.Build.Ldflags != "" {
res += c.Build.Ldflags + " "
}
if !c.Build.Debug {
// See https://stackoverflow.com/questions/30005878/avoid-debugging-information-on-golang and go.dev/issues/25148.
res += "-s -w "
}
if c.About != "" {
res += "-X 'cogentcore.org/core/core.AppAbout=" + strings.ReplaceAll(c.About, "'", `\'`) + "' "
}
b, err := os.ReadFile("icon.svg")
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
errors.Log(err)
}
} else {
res += "-X 'cogentcore.org/core/core.AppIcon=" + strings.ReplaceAll(string(b), "'", `\'`) + "' "
}
// TODO: maybe replace this linker flag version injection logic with
// [debug.ReadBuildInfo] at some point; we currently support it as a
// backup in system/app.go, but it is less reliable and formats worse,
// so we won't use it as a full replacement yet (see
// https://github.com/cogentcore/core/issues/1370).
av, err := exec.Silent().Output("git", "describe", "--tags")
if err == nil {
res += "-X cogentcore.org/core/system.AppVersion=" + av + " "
}
// workspaces can interfere with getting the right version
cv, err := exec.Silent().SetEnv("GOWORK", "off").Output("go", "list", "-m", "-f", "{{.Version}}", "cogentcore.org/core")
if err == nil {
// we must be in core itself if it is blank
if cv == "" {
cv = av
}
res += "-X cogentcore.org/core/system.CoreVersion=" + cv
}
return res
}
// 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 config
import (
"fmt"
"strings"
)
// Note: the maps in this file are derived from https://github.com/golang/go/blob/master/src/go/build/syslist.go
// Platform is a platform with an operating system and an architecture
type Platform struct {
OS string
Arch string
}
// String returns the platform as a string in the form "os/arch"
func (p Platform) String() string {
return p.OS + "/" + p.Arch
}
// OSSupported determines whether the given operating system is supported by Cogent Core. If it is, it returns nil.
// If it isn't, it returns an error detailing the issue with the operating system (not found or not supported).
func OSSupported(os string) error {
supported, ok := supportedOS[os]
if !ok {
return fmt.Errorf("could not find operating system %s; please check that you spelled it correctly", os)
}
if !supported {
return fmt.Errorf("operating system %s exists but is not supported by Cogent Core", os)
}
return nil
}
// ArchSupported determines whether the given architecture is supported by Cogent Core. If it is, it returns nil.
// If it isn't, it also returns an error detailing the issue with the architecture (not found or not supported).
func ArchSupported(arch string) error {
supported, ok := supportedArch[arch]
if !ok {
return fmt.Errorf("could not find architecture %s; please check that you spelled it correctly", arch)
}
if !supported {
return fmt.Errorf("architecture %s exists but is not supported by Cogent Core", arch)
}
return nil
}
// SetString sets the platform from the given string of format os[/arch]
func (p *Platform) SetString(platform string) error {
before, after, found := strings.Cut(platform, "/")
p.OS = before
err := OSSupported(before)
if err != nil {
return fmt.Errorf("error parsing platform: %w", err)
}
if !found {
p.Arch = "*"
return nil
}
p.Arch = after
err = ArchSupported(after)
if err != nil {
return fmt.Errorf("error parsing platform: %w", err)
}
return nil
}
// ArchsForOS returns contains all of the architectures supported for
// each operating system.
var ArchsForOS = map[string][]string{
"darwin": {"386", "amd64", "arm", "arm64"},
"windows": {"386", "amd64", "arm", "arm64"},
"linux": {"386", "amd64", "arm", "arm64"},
"android": {"386", "amd64", "arm", "arm64"},
"ios": {"arm64"},
}
// supportedOS is a map containing all operating systems and whether they are supported by Cogent Core.
var supportedOS = map[string]bool{
"aix": false,
"android": true,
"darwin": true,
"dragonfly": false,
"freebsd": false,
"hurd": false,
"illumos": false,
"ios": true,
"web": true,
"linux": true,
"nacl": false,
"netbsd": false,
"openbsd": false,
"plan9": false,
"solaris": false,
"wasip1": false,
"windows": true,
"zos": false,
}
// supportedArch is a map containing all computer architectures and whether they are supported by Cogent Core.
var supportedArch = map[string]bool{
"386": true,
"amd64": true,
"amd64p32": true,
"arm": true,
"armbe": true,
"arm64": true,
"arm64be": true,
"loong64": false,
"mips": false,
"mipsle": false,
"mips64": false,
"mips64le": false,
"mips64p32": false,
"mips64p32le": false,
"ppc": false,
"ppc64": false,
"ppc64le": false,
"riscv": false,
"riscv64": false,
"s390": false,
"s390x": false,
"sparc": false,
"sparc64": false,
"wasm": true,
}
// 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.
// Command core provides command line tools for developing apps
// and libraries using the Cogent Core framework.
package main
import (
"cogentcore.org/core/cli"
"cogentcore.org/core/cmd/core/cmd"
"cogentcore.org/core/cmd/core/config"
"cogentcore.org/core/cmd/core/generate"
)
func main() {
opts := cli.DefaultOptions("Cogent Core", "Command line tools for developing apps and libraries using the Cogent Core framework.")
opts.DefaultFiles = []string{"core.toml"}
opts.SearchUp = true
cli.Run(opts, &config.Config{}, cmd.Setup, cmd.Build, cmd.Run, cmd.Pack, cmd.Install, generate.Generate, cmd.Changed, cmd.Pull, cmd.Log, cmd.Release, cmd.NextRelease)
}
// 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 generate provides the generation
// of useful methods, variables, and constants
// for Cogent Core code.
package generate
//go:generate core generate
import (
"fmt"
"slices"
"text/template"
"cogentcore.org/core/base/generate"
"cogentcore.org/core/base/ordmap"
"cogentcore.org/core/cmd/core/config"
"cogentcore.org/core/enums/enumgen"
"cogentcore.org/core/types"
"cogentcore.org/core/types/typegen"
"golang.org/x/tools/go/packages"
)
// treeMethodsTmpl is a template that contains the methods
// and functions specific to [tree.Node] types.
var treeMethodsTmpl = template.Must(template.New("TreeMethods").
Funcs(template.FuncMap{
"HasEmbedDirective": hasEmbedDirective,
"HasNoNewDirective": hasNoNewDirective,
"DocToComment": typegen.DocToComment,
"TreePkg": treePkg,
}).Parse(
`
{{if not (HasNoNewDirective .)}}
// New{{.LocalName}} returns a new [{{.LocalName}}] with the given optional parent:
{{DocToComment .Doc}}
func New{{.LocalName}}(parent ...{{TreePkg .}}Node) *{{.LocalName}} { return {{TreePkg .}}New[{{.LocalName}}](parent...) }
{{end}}
{{if HasEmbedDirective .}}
// {{.LocalName}}Embedder is an interface that all types that embed {{.LocalName}} satisfy
type {{.LocalName}}Embedder interface {
As{{.LocalName}}() *{{.LocalName}}
}
// As{{.LocalName}} returns the given value as a value of type {{.LocalName}} if the type
// of the given value embeds {{.LocalName}}, or nil otherwise
func As{{.LocalName}}(n {{TreePkg .}}Node) *{{.LocalName}} {
if t, ok := n.({{.LocalName}}Embedder); ok {
return t.As{{.LocalName}}()
}
return nil
}
// As{{.LocalName}} satisfies the [{{.LocalName}}Embedder] interface
func (t *{{.LocalName}}) As{{.LocalName}}() *{{.LocalName}} { return t }
{{end}}
`,
))
// treePkg returns the package identifier for the tree package in
// the context of the given type ("" if it is already in the tree
// package, and "tree." otherwise)
func treePkg(typ *typegen.Type) string {
if typ.Pkg == "tree" { // we are already in tree
return ""
}
return "tree."
}
// hasEmbedDirective returns whether the given [typegen.Type] has a "core:embedder"
// comment directive. This function is used in [treeMethodsTmpl].
func hasEmbedDirective(typ *typegen.Type) bool {
return slices.ContainsFunc(typ.Directives, func(d types.Directive) bool {
return d.Tool == "core" && d.Directive == "embedder"
})
}
// hasNoNewDirective returns whether the given [typegen.Type] has a "core:no-new"
// comment directive. This function is used in [treeMethodsTmpl].
func hasNoNewDirective(typ *typegen.Type) bool {
return slices.ContainsFunc(typ.Directives, func(d types.Directive) bool {
return d.Tool == "core" && d.Directive == "no-new"
})
}
// Generate is the main entry point to code generation
// that does all of the generation according to the
// given config info. It overrides the
// [config.Config.Generate.Typegen.InterfaceConfigs] info.
func Generate(c *config.Config) error { //types:add
c.Generate.Typegen.InterfaceConfigs = ºap.Map[string, *typegen.Config]{}
c.Generate.Typegen.InterfaceConfigs.Add("cogentcore.org/core/tree.Node", &typegen.Config{
AddTypes: true,
Setters: true,
Templates: []*template.Template{treeMethodsTmpl},
})
pkgs, err := parsePackages(c)
if err != nil {
return fmt.Errorf("Generate: error parsing package: %w", err)
}
err = enumgen.GeneratePkgs(&c.Generate.Enumgen, pkgs)
if err != nil {
return fmt.Errorf("error running enumgen: %w", err)
}
err = typegen.GeneratePkgs(&c.Generate.Typegen, pkgs)
if err != nil {
return fmt.Errorf("error running typegen: %w", err)
}
err = Pages(c)
if err != nil {
return fmt.Errorf("error running pagegen: %w", err)
}
err = Icons(c)
if err != nil {
return fmt.Errorf("error running icongen: %w", err)
}
return nil
}
// parsePackages parses the package(s) based on the given config info.
func parsePackages(cfg *config.Config) ([]*packages.Package, error) {
pcfg := &packages.Config{
Mode: enumgen.PackageModes() | typegen.PackageModes(&cfg.Generate.Typegen), // need superset of both
// TODO: Need to think about constants in test files. Maybe write type_string_test.go
// in a separate pass? For later.
Tests: false,
}
return generate.Load(pcfg, cfg.Generate.Dir)
}
// Copyright (c) 2024, 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 generate
import (
"bytes"
"io/fs"
"os"
"path/filepath"
"strings"
"text/template"
"unicode"
"cogentcore.org/core/base/generate"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/cmd/core/config"
)
// iconData contains the data for an icon
type iconData struct {
Dir string // Dir is the directory in which the icon is contained
Snake string // Snake is the snake_case name of the icon
Camel string // Camel is the CamelCase name of the icon
IconsPackage string // IconsPackage is "icons." or ""
}
var iconTmpl = template.Must(template.New("icon").Parse(
`
//go:embed {{.Dir}}{{.Snake}}.svg
{{.Camel}} {{.IconsPackage}}Icon`,
))
// Icons does any necessary generation for icons.
func Icons(c *config.Config) error {
if c.Generate.Icons == "" {
return nil
}
b := &bytes.Buffer{}
wd, err := os.Getwd()
if err != nil {
return err
}
generate.PrintHeader(b, filepath.Base(wd))
b.WriteString(`import _ "embed"
var (`)
fs.WalkDir(os.DirFS(c.Generate.Icons), ".", func(path string, d fs.DirEntry, err error) error {
if d.IsDir() || filepath.Ext(path) != ".svg" {
return nil
}
name := strings.TrimSuffix(path, ".svg")
// ignore blank icon, as we define the constant for that separately
if name == "blank" {
return nil
}
camel := strcase.ToCamel(name)
// identifier names can't start with a digit
if unicode.IsDigit([]rune(camel)[0]) {
camel = "X" + camel
}
data := iconData{
Snake: name,
Camel: camel,
}
data.Dir = c.Generate.Icons + "/"
if data.Dir == "./" {
data.Dir = ""
}
if !strings.HasSuffix(wd, filepath.Join("core", "icons")) {
data.IconsPackage = "icons."
}
return iconTmpl.Execute(b, data)
})
b.WriteString("\n)\n")
return generate.Write("icongen.go", b.Bytes(), nil)
}
// Copyright (c) 2024, 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 generate
import (
"bufio"
"bytes"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"cogentcore.org/core/base/generate"
"cogentcore.org/core/base/ordmap"
"cogentcore.org/core/cmd/core/config"
"cogentcore.org/core/pages/ppath"
)
// Pages does any necessary generation for pages.
func Pages(c *config.Config) error {
if c.Pages == "" {
return nil
}
examples, err := getPagesExamples(c)
if err != nil {
return err
}
return writePagegen(examples)
}
// getPagesExamples collects and returns all of the pages examples.
func getPagesExamples(c *config.Config) (ordmap.Map[string, []byte], error) {
var examples ordmap.Map[string, []byte]
err := filepath.WalkDir(c.Pages, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if filepath.Ext(path) != ".md" {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
sc := bufio.NewScanner(f)
var curExample [][]byte
inExample := false
gotNewBody := false
gotMain := false
numExamples := 0
for sc.Scan() {
b := sc.Bytes()
if !inExample {
if bytes.HasPrefix(b, []byte("```Go")) {
inExample = true
}
continue
}
if bytes.HasPrefix(b, []byte("func main() {")) {
gotMain = true
}
// core.NewBody in a main function counts as a new start so that full examples work
if gotMain && !gotNewBody && bytes.Contains(b, []byte("core.NewBody(")) {
gotNewBody = true
curExample = nil
curExample = append(curExample, []byte("b := parent"))
continue
}
// RunMainWindow() counts as a quasi-end so that full examples work
if string(b) == "```" || bytes.Contains(b, []byte("RunMainWindow()")) {
if curExample == nil {
continue
}
rel, err := filepath.Rel(c.Pages, path)
if err != nil {
return err
}
rel = strings.ReplaceAll(rel, `\`, "/")
rel = strings.TrimSuffix(rel, filepath.Ext(rel))
rel = strings.TrimSuffix(rel, "/index")
rel = ppath.Format(rel)
id := rel + "-" + strconv.Itoa(numExamples)
examples.Add(id, bytes.Join(curExample, []byte{'\n'}))
curExample = nil
inExample = false
gotNewBody = false
numExamples++
continue
}
curExample = append(curExample, b)
}
return nil
})
return examples, err
}
// writePagegen constructs the pagegen.go file from the given examples.
func writePagegen(examples ordmap.Map[string, []byte]) error {
b := &bytes.Buffer{}
generate.PrintHeader(b, "main")
b.WriteString(`func init() {
maps.Copy(pages.Examples, PagesExamples)
}
// PagesExamples are the compiled pages examples for this app.
var PagesExamples = map[string]func(b core.Widget){`)
for _, kv := range examples.Order {
fmt.Fprintf(b, `
%q: func(b core.Widget){
%s
},`, kv.Key, kv.Value)
}
b.WriteString("\n}")
return generate.Write("pagegen.go", b.Bytes(), nil)
}
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate go run genarsc.go
//go:generate stringer -output binres_string.go -type ResType,DataType
// Package binres implements encoding and decoding of android binary resources.
//
// Binary resource structs support unmarshalling the binary output of aapt.
// Implementations of marshalling for each struct must produce the exact input
// sent to unmarshalling. This allows tests to validate each struct representation
// of the binary format as follows:
//
// - unmarshal the output of aapt
// - marshal the struct representation
// - perform byte-to-byte comparison with aapt output per chunk header and body
//
// This process should strive to make structs idiomatic to make parsing xml text
// into structs trivial.
//
// Once the struct representation is validated, tests for parsing xml text
// into structs can become self-referential as the following holds true:
//
// - the unmarshalled input of aapt output is the only valid target
// - the unmarshalled input of xml text may be compared to the unmarshalled
// input of aapt output to identify errors, e.g. text-trims, wrong flags, etc
//
// This provides validation, byte-for-byte, for producing binary xml resources.
//
// It should be made clear that unmarshalling binary resources is currently only
// in scope for proving that the BinaryMarshaler works correctly. Any other use
// is currently out of scope.
//
// A simple view of binary xml document structure:
//
// XML
// Pool
// Map
// Namespace
// [...node]
//
// Additional resources:
// https://android.googlesource.com/platform/frameworks/base/+/master/libs/androidfw/include/androidfw/ResourceTypes.h
// https://justanapplication.wordpress.com/2011/09/13/ (a series of articles, increment date)
package binres
import (
"encoding"
"encoding/binary"
"encoding/xml"
"fmt"
"io"
"sort"
"strconv"
"strings"
"unicode"
)
func errWrongType(have ResType, want ...ResType) error {
return fmt.Errorf("wrong resource type %s, want one of %v", have, want)
}
type ResType uint16
func (t ResType) IsSupported() bool {
return t != ResNull
}
// explicitly defined for clarity and resolvability with apt source
const (
ResNull ResType = 0x0000
ResStringPool ResType = 0x0001
ResTable ResType = 0x0002
ResXML ResType = 0x0003
ResXMLStartNamespace ResType = 0x0100
ResXMLEndNamespace ResType = 0x0101
ResXMLStartElement ResType = 0x0102
ResXMLEndElement ResType = 0x0103
ResXMLCharData ResType = 0x0104
ResXMLResourceMap ResType = 0x0180
ResTablePackage ResType = 0x0200
ResTableType ResType = 0x0201
ResTableTypeSpec ResType = 0x0202
ResTableLibrary ResType = 0x0203
ResTableOverlayable ResType = 0x0204
ResTableOverlayablePolicy ResType = 0x0205
ResTableStagedAlias ResType = 0x0206
)
var (
btou16 = binary.LittleEndian.Uint16
btou32 = binary.LittleEndian.Uint32
putu16 = binary.LittleEndian.PutUint16
putu32 = binary.LittleEndian.PutUint32
)
// unmarshaler wraps BinaryUnmarshaler to provide byte size of decoded chunks.
type unmarshaler interface {
encoding.BinaryUnmarshaler
// size returns the byte size unmarshalled after a call to
// UnmarshalBinary, or otherwise zero.
size() int
}
// chunkHeader appears at the front of every data chunk in a resource.
type chunkHeader struct {
// Type of data that follows this header.
typ ResType
// Advance slice index by this value to find its associated data, if any.
headerByteSize uint16
// This is the header size plus the size of any data associated with the chunk.
// Advance slice index by this value to completely skip its contents, including
// any child chunks. If this value is the same as headerByteSize, there is
// no data associated with the chunk.
byteSize uint32
}
// size implements unmarshaler.
func (hdr chunkHeader) size() int { return int(hdr.byteSize) }
func (hdr *chunkHeader) UnmarshalBinary(bin []byte) error {
hdr.typ = ResType(btou16(bin))
if !hdr.typ.IsSupported() {
return fmt.Errorf("%s not supported", hdr.typ)
}
hdr.headerByteSize = btou16(bin[2:])
hdr.byteSize = btou32(bin[4:])
if len(bin) < int(hdr.byteSize) {
return fmt.Errorf("too few bytes to unmarshal chunk body, have %v, need at-least %v", len(bin), hdr.byteSize)
}
return nil
}
func (hdr chunkHeader) MarshalBinary() ([]byte, error) {
if !hdr.typ.IsSupported() {
return nil, fmt.Errorf("%s not supported", hdr.typ)
}
bin := make([]byte, 8)
putu16(bin, uint16(hdr.typ))
putu16(bin[2:], hdr.headerByteSize)
putu32(bin[4:], hdr.byteSize)
return bin, nil
}
type XML struct {
chunkHeader
Pool *Pool
Map *Map
Namespace *Namespace
Children []*Element
// tmp field used when unmarshalling binary
stack []*Element
}
// RawValueByName returns the original raw string value of first matching element attribute, or error if not exists.
// Given <manifest package="VAL" ...> then RawValueByName("manifest", xml.Name{Local: "package"}) returns "VAL".
func (bx *XML) RawValueByName(elname string, attrname xml.Name) (string, error) {
elref, err := bx.Pool.RefByName(elname)
if err != nil {
return "", err
}
nref, err := bx.Pool.RefByName(attrname.Local)
if err != nil {
return "", err
}
nsref := PoolRef(NoEntry)
if attrname.Space != "" {
nsref, err = bx.Pool.RefByName(attrname.Space)
if err != nil {
return "", err
}
}
for el := range bx.iterElements() {
if el.Name == elref {
for _, attr := range el.attrs {
// TODO enforce TypedValue DataString constraint?
if nsref == attr.NS && nref == attr.Name {
return bx.Pool.strings[int(attr.RawValue)], nil
}
}
}
}
return "", fmt.Errorf("no matching element %q for attribute %+v found", elname, attrname)
}
const (
androidSchema = "http://schemas.android.com/apk/res/android"
toolsSchema = "http://schemas.android.com/tools"
)
// skipSynthesize is set true for tests to avoid synthesis of additional nodes and attributes.
var skipSynthesize bool
// UnmarshalXML decodes an AndroidManifest.xml document returning type XML
// containing decoded resources with the given minimum and target Android SDK / API version.
func UnmarshalXML(r io.Reader, withIcon bool, minSdkVersion, targetSdkVersion int) (*XML, error) {
tbl, err := OpenTable()
if err != nil {
return nil, err
}
lr := &lineReader{r: r}
dec := xml.NewDecoder(lr)
bx := new(XML)
// temporary pool to resolve real poolref later
pool := new(Pool)
type ltoken struct {
xml.Token
line int
}
var q []ltoken
for {
line := lr.line(dec.InputOffset())
tkn, err := dec.Token()
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
tkn = xml.CopyToken(tkn)
switch tkn := tkn.(type) {
case xml.StartElement:
switch tkn.Name.Local {
default:
q = append(q, ltoken{tkn, line})
case "uses-sdk":
return nil, fmt.Errorf("manual declaration of uses-sdk in AndroidManifest.xml not supported")
case "manifest":
// synthesize additional attributes and nodes for use during encode.
tkn.Attr = append(tkn.Attr, xml.Attr{
Name: xml.Name{
Space: "",
Local: "platformBuildVersionCode",
},
Value: strconv.Itoa(targetSdkVersion),
},
xml.Attr{
Name: xml.Name{
Space: "",
Local: "platformBuildVersionName",
},
Value: "4.1.2-1425332",
},
)
q = append(q, ltoken{tkn, line})
if !skipSynthesize {
s := xml.StartElement{
Name: xml.Name{
Space: "",
Local: "uses-sdk",
},
Attr: []xml.Attr{
{
Name: xml.Name{
Space: androidSchema,
Local: "minSdkVersion",
},
Value: strconv.Itoa(minSdkVersion),
},
{
Name: xml.Name{
Space: androidSchema,
Local: "targetSdkVersion",
},
Value: strconv.Itoa(targetSdkVersion),
},
},
}
e := xml.EndElement{Name: xml.Name{Local: "uses-sdk"}}
q = append(q, ltoken{s, line}, ltoken{e, line})
}
case "application":
if !skipSynthesize {
for _, attr := range tkn.Attr {
if attr.Name.Space == androidSchema && attr.Name.Local == "icon" {
return nil, fmt.Errorf("manual declaration of android:icon in AndroidManifest.xml not supported")
}
}
if withIcon {
tkn.Attr = append(tkn.Attr,
xml.Attr{
Name: xml.Name{
Space: androidSchema,
Local: "icon",
},
Value: "@mipmap/icon",
})
}
}
q = append(q, ltoken{tkn, line})
case "activity", "intent-filter", "action", "category", "service", "meta-data":
// need android:exported="true" for activities in android sdk version 31 and above (still not working so testing with other things also set to exported)
if !skipSynthesize && targetSdkVersion >= 31 {
tkn.Attr = append(tkn.Attr,
xml.Attr{
Name: xml.Name{
Space: androidSchema,
Local: "exported",
},
Value: "true",
},
)
}
q = append(q, ltoken{tkn, line})
}
default:
q = append(q, ltoken{tkn, line})
}
}
for _, ltkn := range q {
tkn, line := ltkn.Token, ltkn.line
switch tkn := tkn.(type) {
case xml.StartElement:
el := &Element{
NodeHeader: NodeHeader{
LineNumber: uint32(line),
Comment: 0xFFFFFFFF,
},
NS: NoEntry,
Name: pool.ref(tkn.Name.Local),
}
if len(bx.stack) == 0 {
bx.Children = append(bx.Children, el)
} else {
n := len(bx.stack)
var p *Element
p, bx.stack = bx.stack[n-1], bx.stack[:n-1]
p.Children = append(p.Children, el)
bx.stack = append(bx.stack, p)
}
bx.stack = append(bx.stack, el)
for _, attr := range tkn.Attr {
if (attr.Name.Space == "xmlns" && attr.Name.Local == "tools") || attr.Name.Space == toolsSchema {
continue // TODO can tbl be queried for schemas to determine validity instead?
}
if attr.Name.Space == "xmlns" && attr.Name.Local == "android" {
if bx.Namespace != nil {
return nil, fmt.Errorf("multiple declarations of xmlns:android encountered")
}
bx.Namespace = &Namespace{
NodeHeader: NodeHeader{
LineNumber: uint32(line),
Comment: NoEntry,
},
prefix: 0,
uri: 0,
}
continue
}
nattr := &Attribute{
NS: pool.ref(attr.Name.Space),
Name: pool.ref(attr.Name.Local),
RawValue: NoEntry,
}
el.attrs = append(el.attrs, nattr)
if attr.Name.Space == "" {
nattr.NS = NoEntry
// TODO it's unclear how to query these
switch attr.Name.Local {
case "platformBuildVersionCode":
nattr.TypedValue.Type = DataIntDec
i, err := strconv.Atoi(attr.Value)
if err != nil {
return nil, err
}
nattr.TypedValue.Value = uint32(i)
default: // "package", "platformBuildVersionName", and any invalid
nattr.RawValue = pool.ref(attr.Value)
nattr.TypedValue.Type = DataString
}
} else {
// get type spec and value data type
ref, err := tbl.RefByName("attr/" + attr.Name.Local)
if err != nil {
return nil, err
}
nt, err := ref.Resolve(tbl)
if err != nil {
return nil, err
}
if len(nt.values) == 0 {
panic("encountered empty values slice")
}
if len(nt.values) == 1 {
val := nt.values[0]
if val.data.Type != DataIntDec {
panic("TODO only know how to handle DataIntDec type here")
}
t := DataType(val.data.Value)
switch t {
case DataString, DataAttribute, DataType(0x3e):
// TODO identify 0x3e, in bootstrap.xml this is the native lib name
nattr.RawValue = pool.ref(attr.Value)
nattr.TypedValue.Type = DataString
nattr.TypedValue.Value = uint32(nattr.RawValue)
case DataIntBool, DataType(0x08):
nattr.TypedValue.Type = DataIntBool
switch attr.Value {
case "true":
nattr.TypedValue.Value = 0xFFFFFFFF
case "false":
nattr.TypedValue.Value = 0
default:
return nil, fmt.Errorf("invalid bool value %q", attr.Value)
}
case DataIntDec, DataFloat, DataFraction:
// TODO DataFraction needs it's own case statement. minSdkVersion identifies as DataFraction
// but has accepted input in the past such as android:minSdkVersion="L"
// Other use-cases for DataFraction are currently unknown as applicable to manifest generation
// but this provides minimum support for writing out minSdkVersion="15" correctly.
nattr.TypedValue.Type = DataIntDec
i, err := strconv.Atoi(attr.Value)
if err != nil {
return nil, err
}
nattr.TypedValue.Value = uint32(i)
case DataReference:
nattr.TypedValue.Type = DataReference
dref, err := tbl.RefByName(attr.Value)
if err != nil {
if strings.HasPrefix(attr.Value, "@mipmap") {
// firstDrawableId is a TableRef matching first entry of mipmap spec initialized by NewMipmapTable.
// 7f is default package, 02 is mipmap spec, 0000 is first entry; e.g. R.drawable.icon
// TODO resource table should generate ids as required.
const firstDrawableId = 0x7f020000
nattr.TypedValue.Value = firstDrawableId
continue
}
return nil, err
}
nattr.TypedValue.Value = uint32(dref)
default:
return nil, fmt.Errorf("unhandled data type %0#2x: %s", uint8(t), t)
}
} else {
// 0x01000000 is an unknown ref that doesn't point to anything, typically
// located at the start of entry value lists, peek at last value to determine type.
t := nt.values[len(nt.values)-1].data.Type
switch t {
case DataIntDec:
for _, val := range nt.values {
if val.name == 0x01000000 {
continue
}
nr, err := val.name.Resolve(tbl)
if err != nil {
return nil, err
}
if attr.Value == nr.key.Resolve(tbl.pkgs[0].keyPool) { // TODO hard-coded pkg ref
nattr.TypedValue = *val.data
break
}
}
case DataIntHex:
nattr.TypedValue.Type = t
for _, x := range strings.Split(attr.Value, "|") {
for _, val := range nt.values {
if val.name == 0x01000000 {
continue
}
nr, err := val.name.Resolve(tbl)
if err != nil {
return nil, err
}
if x == nr.key.Resolve(tbl.pkgs[0].keyPool) { // TODO hard-coded pkg ref
nattr.TypedValue.Value |= val.data.Value
break
}
}
}
default:
return nil, fmt.Errorf("unhandled data type for configuration %0#2x: %s", uint8(t), t)
}
}
}
}
case xml.CharData:
if s := poolTrim(string(tkn)); s != "" {
cdt := &CharData{
NodeHeader: NodeHeader{
LineNumber: uint32(line),
Comment: NoEntry,
},
RawData: pool.ref(s),
}
el := bx.stack[len(bx.stack)-1]
if el.head == nil {
el.head = cdt
} else if el.tail == nil {
el.tail = cdt
} else {
return nil, fmt.Errorf("element head and tail already contain chardata")
}
}
case xml.EndElement:
if tkn.Name.Local == "manifest" {
bx.Namespace.end = &Namespace{
NodeHeader: NodeHeader{
LineNumber: uint32(line),
Comment: NoEntry,
},
prefix: 0,
uri: 0,
}
}
n := len(bx.stack)
var el *Element
el, bx.stack = bx.stack[n-1], bx.stack[:n-1]
if el.end != nil {
return nil, fmt.Errorf("element end already exists")
}
el.end = &ElementEnd{
NodeHeader: NodeHeader{
LineNumber: uint32(line),
Comment: NoEntry,
},
NS: el.NS,
Name: el.Name,
}
case xml.Comment, xml.ProcInst:
// discard
default:
panic(fmt.Errorf("unhandled token type: %T %+v", tkn, tkn))
}
}
// pools appear to be sorted as follows:
// * attribute names prefixed with android:
// * "android", [schema-url], [empty-string]
// * for each node:
// * attribute names with no prefix
// * node name
// * attribute value if data type of name is DataString, DataAttribute, or 0x3e (an unknown)
bx.Pool = new(Pool)
var arecurse func(*Element)
arecurse = func(el *Element) {
for _, attr := range el.attrs {
if attr.NS == NoEntry {
continue
}
if attr.NS.Resolve(pool) == androidSchema {
bx.Pool.strings = append(bx.Pool.strings, attr.Name.Resolve(pool))
}
}
for _, child := range el.Children {
arecurse(child)
}
}
for _, el := range bx.Children {
arecurse(el)
}
// TODO encoding/xml does not enforce namespace prefix and manifest encoding in aapt
// appears to ignore all other prefixes. Inserting this manually is not strictly correct
// for the general case, but the effort to do otherwise currently offers nothing.
bx.Pool.strings = append(bx.Pool.strings, "android", androidSchema)
// there always appears to be an empty string located after schema, even if one is
// not present in manifest.
bx.Pool.strings = append(bx.Pool.strings, "")
var brecurse func(*Element)
brecurse = func(el *Element) {
for _, attr := range el.attrs {
if attr.NS == NoEntry {
bx.Pool.strings = append(bx.Pool.strings, attr.Name.Resolve(pool))
}
}
bx.Pool.strings = append(bx.Pool.strings, el.Name.Resolve(pool))
for _, attr := range el.attrs {
if attr.RawValue != NoEntry {
bx.Pool.strings = append(bx.Pool.strings, attr.RawValue.Resolve(pool))
} else if attr.NS == NoEntry {
bx.Pool.strings = append(bx.Pool.strings, fmt.Sprintf("%+v", attr.TypedValue.Value))
}
}
if el.head != nil {
bx.Pool.strings = append(bx.Pool.strings, el.head.RawData.Resolve(pool))
}
if el.tail != nil {
bx.Pool.strings = append(bx.Pool.strings, el.tail.RawData.Resolve(pool))
}
for _, child := range el.Children {
brecurse(child)
}
}
for _, el := range bx.Children {
brecurse(el)
}
// do not eliminate duplicates until the entire slice has been composed.
// consider <activity android:label="label" .../>
// all attribute names come first followed by values; in such a case, the value "label"
// would be a reference to the same "android:label" in the string pool which will occur
// within the beginning of the pool where other attr names are located.
bx.Pool.strings = asSet(bx.Pool.strings)
// TODO consider cases of multiple declarations of the same attr name that should return error
// before ever reaching this point.
bx.Map = new(Map)
for _, s := range bx.Pool.strings {
ref, err := tbl.RefByName("attr/" + s)
if err != nil {
break // break after first non-ref as all strings after are also non-refs.
}
bx.Map.rs = append(bx.Map.rs, ref)
}
// resolve tmp pool refs to final pool refs
// TODO drop this in favor of sort directly on Table
var resolve func(el *Element)
resolve = func(el *Element) {
if el.NS != NoEntry {
el.NS = bx.Pool.ref(el.NS.Resolve(pool))
el.end.NS = el.NS
}
el.Name = bx.Pool.ref(el.Name.Resolve(pool))
el.end.Name = el.Name
for _, attr := range el.attrs {
if attr.NS != NoEntry {
attr.NS = bx.Pool.ref(attr.NS.Resolve(pool))
}
attr.Name = bx.Pool.ref(attr.Name.Resolve(pool))
if attr.RawValue != NoEntry {
attr.RawValue = bx.Pool.ref(attr.RawValue.Resolve(pool))
if attr.TypedValue.Type == DataString {
attr.TypedValue.Value = uint32(attr.RawValue)
}
}
}
for _, child := range el.Children {
resolve(child)
}
}
for _, el := range bx.Children {
resolve(el)
}
var asort func(*Element)
asort = func(el *Element) {
sort.Sort(byType(el.attrs))
sort.Sort(byNamespace(el.attrs))
sort.Sort(byName(el.attrs))
for _, child := range el.Children {
asort(child)
}
}
for _, el := range bx.Children {
asort(el)
}
for i, s := range bx.Pool.strings {
switch s {
case androidSchema:
bx.Namespace.uri = PoolRef(i)
bx.Namespace.end.uri = PoolRef(i)
case "android":
bx.Namespace.prefix = PoolRef(i)
bx.Namespace.end.prefix = PoolRef(i)
}
}
return bx, nil
}
// UnmarshalBinary decodes all resource chunks in buf returning any error encountered.
func (bx *XML) UnmarshalBinary(buf []byte) error {
if err := (&bx.chunkHeader).UnmarshalBinary(buf); err != nil {
return err
}
buf = buf[8:]
for len(buf) > 0 {
k, err := bx.unmarshalBinaryKind(buf)
if err != nil {
return err
}
buf = buf[k.size():]
}
return nil
}
// unmarshalBinaryKind decodes and stores the first resource chunk of bin.
// It returns the unmarshaler interface and any error encountered.
// If k.size() < len(bin), subsequent chunks can be decoded at bin[k.size():].
func (bx *XML) unmarshalBinaryKind(bin []byte) (k unmarshaler, err error) {
k, err = bx.kind(ResType(btou16(bin)))
if err != nil {
return nil, err
}
if err = k.UnmarshalBinary(bin); err != nil {
return nil, err
}
return k, nil
}
func (bx *XML) kind(t ResType) (unmarshaler, error) {
switch t {
case ResStringPool:
if bx.Pool != nil {
return nil, fmt.Errorf("pool already exists")
}
bx.Pool = new(Pool)
return bx.Pool, nil
case ResXMLResourceMap:
if bx.Map != nil {
return nil, fmt.Errorf("resource map already exists")
}
bx.Map = new(Map)
return bx.Map, nil
case ResXMLStartNamespace:
if bx.Namespace != nil {
return nil, fmt.Errorf("namespace start already exists")
}
bx.Namespace = new(Namespace)
return bx.Namespace, nil
case ResXMLEndNamespace:
if bx.Namespace.end != nil {
return nil, fmt.Errorf("namespace end already exists")
}
bx.Namespace.end = new(Namespace)
return bx.Namespace.end, nil
case ResXMLStartElement:
el := new(Element)
if len(bx.stack) == 0 {
bx.Children = append(bx.Children, el)
} else {
n := len(bx.stack)
var p *Element
p, bx.stack = bx.stack[n-1], bx.stack[:n-1]
p.Children = append(p.Children, el)
bx.stack = append(bx.stack, p)
}
bx.stack = append(bx.stack, el)
return el, nil
case ResXMLEndElement:
n := len(bx.stack)
var el *Element
el, bx.stack = bx.stack[n-1], bx.stack[:n-1]
if el.end != nil {
return nil, fmt.Errorf("element end already exists")
}
el.end = new(ElementEnd)
return el.end, nil
case ResXMLCharData: // TODO assure correctness
cdt := new(CharData)
el := bx.stack[len(bx.stack)-1]
if el.head == nil {
el.head = cdt
} else if el.tail == nil {
el.tail = cdt
} else {
return nil, fmt.Errorf("element head and tail already contain chardata")
}
return cdt, nil
default:
return nil, fmt.Errorf("unexpected type %s", t)
}
}
func (bx *XML) MarshalBinary() ([]byte, error) {
bx.typ = ResXML
bx.headerByteSize = 8
var (
bin, b []byte
err error
)
b, err = bx.chunkHeader.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
b, err = bx.Pool.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
b, err = bx.Map.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
b, err = bx.Namespace.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
for _, child := range bx.Children {
if err := marshalRecurse(child, &bin); err != nil {
return nil, err
}
}
b, err = bx.Namespace.end.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
putu32(bin[4:], uint32(len(bin)))
return bin, nil
}
func marshalRecurse(el *Element, bin *[]byte) error {
b, err := el.MarshalBinary()
if err != nil {
return err
}
*bin = append(*bin, b...)
if el.head != nil {
b, err := el.head.MarshalBinary()
if err != nil {
return err
}
*bin = append(*bin, b...)
}
for _, child := range el.Children {
if err := marshalRecurse(child, bin); err != nil {
return err
}
}
b, err = el.end.MarshalBinary()
if err != nil {
return err
}
*bin = append(*bin, b...)
return nil
}
func (bx *XML) iterElements() <-chan *Element {
ch := make(chan *Element, 1)
go func() {
for _, el := range bx.Children {
iterElementsRecurse(el, ch)
}
close(ch)
}()
return ch
}
func iterElementsRecurse(el *Element, ch chan *Element) {
ch <- el
for _, e := range el.Children {
iterElementsRecurse(e, ch)
}
}
// asSet returns a set from a slice of strings.
func asSet(xs []string) []string {
m := make(map[string]bool)
fo := xs[:0]
for _, x := range xs {
if !m[x] {
m[x] = true
fo = append(fo, x)
}
}
return fo
}
// poolTrim trims all but immediately surrounding space.
// \n\t\tfoobar\n\t\t becomes \tfoobar\n
func poolTrim(s string) string {
var start, end int
for i, r := range s {
if !unicode.IsSpace(r) {
if i != 0 {
start = i - 1 // preserve preceding space
}
break
}
}
for i := len(s) - 1; i >= 0; i-- {
r := rune(s[i])
if !unicode.IsSpace(r) {
if i != len(s)-1 {
end = i + 2
}
break
}
}
if start == 0 && end == 0 {
return "" // every char was a space
}
return s[start:end]
}
// byNamespace sorts attributes based on string pool position of namespace.
// Given that "android" always proceeds "" in the pool, this results in the
// correct ordering of attributes.
type byNamespace []*Attribute
func (a byNamespace) Len() int { return len(a) }
func (a byNamespace) Less(i, j int) bool {
return a[i].NS < a[j].NS
}
func (a byNamespace) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
// byType sorts attributes by the uint8 value of the type.
type byType []*Attribute
func (a byType) Len() int { return len(a) }
func (a byType) Less(i, j int) bool {
return a[i].TypedValue.Type < a[j].TypedValue.Type
}
func (a byType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
// byName sorts attributes that have matching types based on string pool position of name.
type byName []*Attribute
func (a byName) Len() int { return len(a) }
func (a byName) Less(i, j int) bool {
return (a[i].TypedValue.Type == DataString || a[i].TypedValue.Type == DataIntDec) &&
(a[j].TypedValue.Type == DataString || a[j].TypedValue.Type == DataIntDec) &&
a[i].Name < a[j].Name
}
func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type lineReader struct {
off int64
lines []int64
r io.Reader
}
func (r *lineReader) Read(p []byte) (n int, err error) {
n, err = r.r.Read(p)
for i := 0; i < n; i++ {
if p[i] == '\n' {
r.lines = append(r.lines, r.off+int64(i))
}
}
r.off += int64(n)
return n, err
}
func (r *lineReader) line(pos int64) int {
return sort.Search(len(r.lines), func(i int) bool {
return pos < r.lines[i]
}) + 1
}
// Code generated by "stringer -output binres_string.go -type ResType,DataType"; DO NOT EDIT.
package binres
import "strconv"
const (
_ResType_name_0 = "ResNullResStringPoolResTableResXML"
_ResType_name_1 = "ResXMLStartNamespaceResXMLEndNamespaceResXMLStartElementResXMLEndElementResXMLCharData"
_ResType_name_2 = "ResXMLResourceMap"
_ResType_name_3 = "ResTablePackageResTableTypeResTableTypeSpecResTableLibrary"
)
var (
_ResType_index_0 = [...]uint8{0, 7, 20, 28, 34}
_ResType_index_1 = [...]uint8{0, 20, 38, 56, 72, 86}
_ResType_index_3 = [...]uint8{0, 15, 27, 43, 58}
)
func (i ResType) String() string {
switch {
case 0 <= i && i <= 3:
return _ResType_name_0[_ResType_index_0[i]:_ResType_index_0[i+1]]
case 256 <= i && i <= 260:
i -= 256
return _ResType_name_1[_ResType_index_1[i]:_ResType_index_1[i+1]]
case i == 384:
return _ResType_name_2
case 512 <= i && i <= 515:
i -= 512
return _ResType_name_3[_ResType_index_3[i]:_ResType_index_3[i+1]]
default:
return "ResType(" + strconv.FormatInt(int64(i), 10) + ")"
}
}
const (
_DataType_name_0 = "DataNullDataReferenceDataAttributeDataStringDataFloatDataDimensionDataFractionDataDynamicReference"
_DataType_name_1 = "DataIntDecDataIntHexDataIntBool"
_DataType_name_2 = "DataIntColorARGB8DataIntColorRGB8DataIntColorARGB4DataIntColorRGB4"
)
var (
_DataType_index_0 = [...]uint8{0, 8, 21, 34, 44, 53, 66, 78, 98}
_DataType_index_1 = [...]uint8{0, 10, 20, 31}
_DataType_index_2 = [...]uint8{0, 17, 33, 50, 66}
)
func (i DataType) String() string {
switch {
case 0 <= i && i <= 7:
return _DataType_name_0[_DataType_index_0[i]:_DataType_index_0[i+1]]
case 16 <= i && i <= 18:
i -= 16
return _DataType_name_1[_DataType_index_1[i]:_DataType_index_1[i+1]]
case 28 <= i && i <= 31:
i -= 28
return _DataType_name_2[_DataType_index_2[i]:_DataType_index_2[i+1]]
default:
return "DataType(" + strconv.FormatInt(int64(i), 10) + ")"
}
}
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package binres
// NodeHeader is header all xml node types have, providing additional
// information regarding an xml node over binChunkHeader.
type NodeHeader struct {
chunkHeader
LineNumber uint32 // line number in source file this element appears
Comment PoolRef // optional xml comment associated with element, MaxUint32 if none
}
func (hdr *NodeHeader) UnmarshalBinary(bin []byte) error {
if err := (&hdr.chunkHeader).UnmarshalBinary(bin); err != nil {
return err
}
hdr.LineNumber = btou32(bin[8:])
hdr.Comment = PoolRef(btou32(bin[12:]))
return nil
}
func (hdr *NodeHeader) MarshalBinary() ([]byte, error) {
bin := make([]byte, 16)
b, err := hdr.chunkHeader.MarshalBinary()
if err != nil {
return nil, err
}
copy(bin, b)
putu32(bin[8:], hdr.LineNumber)
putu32(bin[12:], uint32(hdr.Comment))
return bin, nil
}
type Namespace struct {
NodeHeader
prefix PoolRef
uri PoolRef
end *Namespace // TODO don't let this type be recursive
}
func (ns *Namespace) UnmarshalBinary(bin []byte) error {
if err := (&ns.NodeHeader).UnmarshalBinary(bin); err != nil {
return err
}
buf := bin[ns.headerByteSize:]
ns.prefix = PoolRef(btou32(buf))
ns.uri = PoolRef(btou32(buf[4:]))
return nil
}
func (ns *Namespace) MarshalBinary() ([]byte, error) {
if ns.end == nil {
ns.typ = ResXMLEndNamespace
} else {
ns.typ = ResXMLStartNamespace
}
ns.headerByteSize = 16
ns.byteSize = 24
bin := make([]byte, ns.byteSize)
b, err := ns.NodeHeader.MarshalBinary()
if err != nil {
return nil, err
}
copy(bin, b)
putu32(bin[16:], uint32(ns.prefix))
putu32(bin[20:], uint32(ns.uri))
return bin, nil
}
type Element struct {
NodeHeader
NS PoolRef
Name PoolRef // name of node if element, otherwise chardata if CDATA
AttributeStart uint16 // byte offset where attrs start
AttributeSize uint16 // byte size of attrs
AttributeCount uint16 // length of attrs
IdIndex uint16 // Index (1-based) of the "id" attribute. 0 if none.
ClassIndex uint16 // Index (1-based) of the "class" attribute. 0 if none.
StyleIndex uint16 // Index (1-based) of the "style" attribute. 0 if none.
attrs []*Attribute
Children []*Element
end *ElementEnd
head, tail *CharData
}
func (el *Element) UnmarshalBinary(buf []byte) error {
if err := (&el.NodeHeader).UnmarshalBinary(buf); err != nil {
return err
}
buf = buf[el.headerByteSize:]
el.NS = PoolRef(btou32(buf))
el.Name = PoolRef(btou32(buf[4:]))
el.AttributeStart = btou16(buf[8:])
el.AttributeSize = btou16(buf[10:])
el.AttributeCount = btou16(buf[12:])
el.IdIndex = btou16(buf[14:])
el.ClassIndex = btou16(buf[16:])
el.StyleIndex = btou16(buf[18:])
buf = buf[el.AttributeStart:]
el.attrs = make([]*Attribute, int(el.AttributeCount))
for i := range el.attrs {
attr := new(Attribute)
if err := attr.UnmarshalBinary(buf); err != nil {
return err
}
el.attrs[i] = attr
buf = buf[el.AttributeSize:]
}
return nil
}
func (el *Element) MarshalBinary() ([]byte, error) {
el.typ = ResXMLStartElement
el.headerByteSize = 16
el.AttributeSize = 20
el.AttributeStart = 20
el.AttributeCount = uint16(len(el.attrs))
el.IdIndex = 0
el.ClassIndex = 0
el.StyleIndex = 0
el.byteSize = uint32(el.headerByteSize) + uint32(el.AttributeStart) + uint32(len(el.attrs)*int(el.AttributeSize))
bin := make([]byte, el.byteSize)
b, err := el.NodeHeader.MarshalBinary()
if err != nil {
return nil, err
}
copy(bin, b)
putu32(bin[16:], uint32(el.NS))
putu32(bin[20:], uint32(el.Name))
putu16(bin[24:], el.AttributeStart)
putu16(bin[26:], el.AttributeSize)
putu16(bin[28:], el.AttributeCount)
putu16(bin[30:], el.IdIndex)
putu16(bin[32:], el.ClassIndex)
putu16(bin[34:], el.StyleIndex)
buf := bin[36:]
for _, attr := range el.attrs {
b, err := attr.MarshalBinary()
if err != nil {
return nil, err
}
copy(buf, b)
buf = buf[int(el.AttributeSize):]
}
return bin, nil
}
// ElementEnd marks the end of an element node, either Element or CharData.
type ElementEnd struct {
NodeHeader
NS PoolRef
Name PoolRef // name of node if binElement, raw chardata if binCharData
}
func (el *ElementEnd) UnmarshalBinary(bin []byte) error {
(&el.NodeHeader).UnmarshalBinary(bin)
buf := bin[el.headerByteSize:]
el.NS = PoolRef(btou32(buf))
el.Name = PoolRef(btou32(buf[4:]))
return nil
}
func (el *ElementEnd) MarshalBinary() ([]byte, error) {
el.typ = ResXMLEndElement
el.headerByteSize = 16
el.byteSize = 24
bin := make([]byte, 24)
b, err := el.NodeHeader.MarshalBinary()
if err != nil {
return nil, err
}
copy(bin, b)
putu32(bin[16:], uint32(el.NS))
putu32(bin[20:], uint32(el.Name))
return bin, nil
}
type Attribute struct {
NS PoolRef
Name PoolRef
RawValue PoolRef // The original raw string value of this attribute.
TypedValue Data // Processesd typed value of this attribute.
}
func (attr *Attribute) UnmarshalBinary(bin []byte) error {
attr.NS = PoolRef(btou32(bin))
attr.Name = PoolRef(btou32(bin[4:]))
attr.RawValue = PoolRef(btou32(bin[8:]))
return (&attr.TypedValue).UnmarshalBinary(bin[12:])
}
func (attr *Attribute) MarshalBinary() ([]byte, error) {
bin := make([]byte, 20)
putu32(bin, uint32(attr.NS))
putu32(bin[4:], uint32(attr.Name))
putu32(bin[8:], uint32(attr.RawValue))
b, err := attr.TypedValue.MarshalBinary()
if err != nil {
return nil, err
}
copy(bin[12:], b)
return bin, nil
}
// CharData represents a CDATA node and includes ref to node's text value.
type CharData struct {
NodeHeader
RawData PoolRef // raw character data
TypedData Data // typed value of character data
}
func (cdt *CharData) UnmarshalBinary(bin []byte) error {
if err := (&cdt.NodeHeader).UnmarshalBinary(bin); err != nil {
return err
}
buf := bin[cdt.headerByteSize:]
cdt.RawData = PoolRef(btou32(buf))
return (&cdt.TypedData).UnmarshalBinary(buf[4:])
}
func (cdt *CharData) MarshalBinary() ([]byte, error) {
cdt.typ = ResXMLCharData
cdt.headerByteSize = 16
cdt.byteSize = 28
bin := make([]byte, cdt.byteSize)
b, err := cdt.NodeHeader.MarshalBinary()
if err != nil {
return nil, err
}
copy(bin, b)
putu32(bin[16:], uint32(cdt.RawData))
b, err = cdt.TypedData.MarshalBinary()
if err != nil {
return nil, err
}
copy(bin[20:], b)
return bin, nil
}
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package binres
import (
"fmt"
"unicode/utf16"
)
const (
SortedFlag uint32 = 1 << 0
UTF8Flag = 1 << 8
)
// PoolRef is the i'th string in a pool.
type PoolRef uint32
// Resolve returns the string entry of PoolRef in pl.
func (ref PoolRef) Resolve(pl *Pool) string {
return pl.strings[ref]
}
// Pool is a container for string and style span collections.
//
// Pool has the following structure marshalled:
//
// chunkHeader
// uint32 number of strings in this pool
// uint32 number of style spans in pool
// uint32 SortedFlag, UTF8Flag
// uint32 index of string data from header
// uint32 index of style data from header
// []uint32 string indices starting at zero
// []uint16 or []uint8 concatenation of string entries
//
// UTF-16 entries are as follows:
//
// uint16 string length, exclusive
// uint16 [optional] low word if high bit of length set
// [n]byte data
// uint16 0x0000 terminator
//
// UTF-8 entries are as follows:
//
// uint8 character length, exclusive
// uint8 [optional] low word if high bit of character length set
// uint8 byte length, exclusive
// uint8 [optional] low word if high bit of byte length set
// [n]byte data
// uint8 0x00 terminator
type Pool struct {
chunkHeader
strings []string
styles []*Span
flags uint32 // SortedFlag, UTF8Flag
}
// ref returns the PoolRef of s, inserting s if it doesn't exist.
func (pl *Pool) ref(s string) PoolRef {
for i, x := range pl.strings {
if s == x {
return PoolRef(i)
}
}
pl.strings = append(pl.strings, s)
return PoolRef(len(pl.strings) - 1)
}
// RefByName returns the PoolRef of s, or error if not exists.
func (pl *Pool) RefByName(s string) (PoolRef, error) {
for i, x := range pl.strings {
if s == x {
return PoolRef(i), nil
}
}
return 0, fmt.Errorf("PoolRef by name %q does not exist", s)
}
func (pl *Pool) IsSorted() bool { return pl.flags&SortedFlag == SortedFlag }
func (pl *Pool) IsUTF8() bool { return pl.flags&UTF8Flag == UTF8Flag }
func (pl *Pool) UnmarshalBinary(bin []byte) error {
if err := (&pl.chunkHeader).UnmarshalBinary(bin); err != nil {
return err
}
if pl.typ != ResStringPool {
return fmt.Errorf("have type %s, want %s", pl.typ, ResStringPool)
}
pl.strings = make([]string, btou32(bin[8:]))
pl.styles = make([]*Span, btou32(bin[12:]))
pl.flags = btou32(bin[16:])
offstrings := btou32(bin[20:])
offstyle := btou32(bin[24:])
hdrlen := 28
if pl.IsUTF8() {
for i := range pl.strings {
offset := int(offstrings + btou32(bin[hdrlen+i*4:]))
// if leading bit set for nchars and nbytes,
// treat first byte as 7-bit high word and next as low word.
nchars := int(bin[offset])
offset++
if nchars&(1<<7) != 0 {
n0 := nchars ^ (1 << 7) // high word
n1 := int(bin[offset]) // low word
nchars = n0*(1<<8) + n1
offset++
}
// TODO(d) At least one case in android.jar (api-10) resource table has only
// highbit set, making 7-bit highword zero. The reason for this is currently
// unknown but would make tests that unmarshal-marshal to match bytes impossible.
// The values noted were high-word: 0 (after highbit unset), low-word: 141
// The size may be treated as an int8 triggering the use of two bytes to store size
// even though the implementation uses uint8.
nbytes := int(bin[offset])
offset++
if nbytes&(1<<7) != 0 {
n0 := nbytes ^ (1 << 7) // high word
n1 := int(bin[offset]) // low word
nbytes = n0*(1<<8) + n1
offset++
}
data := bin[offset : offset+nbytes]
if x := uint8(bin[offset+nbytes]); x != 0 {
return fmt.Errorf("expected zero terminator, got 0x%02X for nchars=%v nbytes=%v data=%q", x, nchars, nbytes, data)
}
pl.strings[i] = string(data)
}
} else {
for i := range pl.strings {
offset := int(offstrings + btou32(bin[hdrlen+i*4:])) // read index of string
// if leading bit set for nchars, treat first byte as 7-bit high word and next as low word.
nchars := int(btou16(bin[offset:]))
offset += 2
if nchars&(1<<15) != 0 { // TODO(d) this is untested
n0 := nchars ^ (1 << 15) // high word
n1 := int(btou16(bin[offset:])) // low word
nchars = n0*(1<<16) + n1
offset += 2
}
data := make([]uint16, nchars)
for i := range data {
data[i] = btou16(bin[offset+2*i:])
}
if x := btou16(bin[offset+nchars*2:]); x != 0 {
return fmt.Errorf("expected zero terminator, got 0x%04X for nchars=%v data=%q", x, nchars, data)
}
pl.strings[i] = string(utf16.Decode(data))
}
}
// TODO decode styles
_ = offstyle
return nil
}
func (pl *Pool) MarshalBinary() ([]byte, error) {
if pl.IsUTF8() {
return nil, fmt.Errorf("encode utf8 not supported")
}
var (
hdrlen = 28
// indices of string indices
iis = make([]uint32, len(pl.strings))
iislen = len(iis) * 4
// utf16 encoded strings concatenated together
strs []uint16
)
for i, x := range pl.strings {
if len(x)>>16 > 0 {
panic(fmt.Errorf("string lengths over 1<<15 not yet supported, got len %d", len(x)))
}
p := utf16.Encode([]rune(x))
if len(p) == 0 {
strs = append(strs, 0x0000, 0x0000)
} else {
strs = append(strs, uint16(len(p))) // string length (implicitly includes zero terminator to follow)
strs = append(strs, p...)
strs = append(strs, 0) // zero terminated
}
// indices start at zero
if i+1 != len(iis) {
iis[i+1] = uint32(len(strs) * 2) // utf16 byte index
}
}
// check strings is 4-byte aligned, pad with zeros if not.
for x := (len(strs) * 2) % 4; x != 0; x -= 2 {
strs = append(strs, 0x0000)
}
strslen := len(strs) * 2
hdr := chunkHeader{
typ: ResStringPool,
headerByteSize: 28,
byteSize: uint32(28 + iislen + strslen),
}
bin := make([]byte, hdr.byteSize)
hdrbin, err := hdr.MarshalBinary()
if err != nil {
return nil, err
}
copy(bin, hdrbin)
putu32(bin[8:], uint32(len(pl.strings)))
putu32(bin[12:], uint32(len(pl.styles)))
putu32(bin[16:], pl.flags)
putu32(bin[20:], uint32(hdrlen+iislen))
putu32(bin[24:], 0) // index of styles start, is 0 when styles length is 0
buf := bin[28:]
for _, x := range iis {
putu32(buf, x)
buf = buf[4:]
}
for _, x := range strs {
putu16(buf, x)
buf = buf[2:]
}
if len(buf) != 0 {
panic(fmt.Errorf("failed to fill allocated buffer, %v bytes left over", len(buf)))
}
return bin, nil
}
type Span struct {
name PoolRef
firstChar, lastChar uint32
}
func (spn *Span) UnmarshalBinary(bin []byte) error {
const end = 0xFFFFFFFF
spn.name = PoolRef(btou32(bin))
if spn.name == end {
return nil
}
spn.firstChar = btou32(bin[4:])
spn.lastChar = btou32(bin[8:])
return nil
}
// Map contains a uint32 slice mapping strings in the string
// pool back to resource identifiers. The i'th element of the slice
// is also the same i'th element of the string pool.
type Map struct {
chunkHeader
rs []TableRef
}
func (m *Map) UnmarshalBinary(bin []byte) error {
(&m.chunkHeader).UnmarshalBinary(bin)
buf := bin[m.headerByteSize:m.byteSize]
m.rs = make([]TableRef, len(buf)/4)
for i := range m.rs {
m.rs[i] = TableRef(btou32(buf[i*4:]))
}
return nil
}
func (m *Map) MarshalBinary() ([]byte, error) {
m.typ = ResXMLResourceMap
m.headerByteSize = 8
m.byteSize = uint32(m.headerByteSize) + uint32(len(m.rs)*4)
bin := make([]byte, m.byteSize)
b, err := m.chunkHeader.MarshalBinary()
if err != nil {
return nil, err
}
copy(bin, b)
for i, r := range m.rs {
putu32(bin[8+i*4:], uint32(r))
}
return bin, nil
}
package binres
import (
"archive/zip"
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"cogentcore.org/core/cmd/core/mobile/sdkpath"
)
// MinSDK is the targeted sdk version for support by package binres.
const MinSDK = 16
func apiResources() ([]byte, error) {
apiResPath, err := apiResourcesPath()
if err != nil {
return nil, err
}
zr, err := zip.OpenReader(apiResPath)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf(`%v; consider installing with "android update sdk --all --no-ui --filter android-%d"`, err, MinSDK)
}
return nil, err
}
defer zr.Close()
buf := new(bytes.Buffer)
for _, f := range zr.File {
if f.Name == "resources.arsc" {
rc, err := f.Open()
if err != nil {
return nil, err
}
_, err = io.Copy(buf, rc)
if err != nil {
return nil, err
}
rc.Close()
break
}
}
if buf.Len() == 0 {
return nil, fmt.Errorf("failed to read resources.arsc")
}
return buf.Bytes(), nil
}
func apiResourcesPath() (string, error) {
platformDir, err := sdkpath.AndroidAPIPath(MinSDK)
if err != nil {
return "", err
}
return filepath.Join(platformDir, "android.jar"), nil
}
// PackResources produces a stripped down gzip version of the resources.arsc from api jar.
func PackResources() ([]byte, error) {
tbl, err := OpenSDKTable()
if err != nil {
return nil, err
}
tbl.pool.strings = []string{} // should not be needed
pkg := tbl.pkgs[0]
// drop language string entries
for _, typ := range pkg.specs[3].types {
if typ.config.locale.language != 0 {
for j, nt := range typ.entries {
if nt == nil { // NoEntry
continue
}
pkg.keyPool.strings[nt.key] = ""
typ.indices[j] = NoEntry
typ.entries[j] = nil
}
}
}
// drop strings from pool for specs to be dropped
for _, spec := range pkg.specs[4:] {
for _, typ := range spec.types {
for _, nt := range typ.entries {
if nt == nil { // NoEntry
continue
}
// don't drop if there's a collision
var collision bool
for _, xspec := range pkg.specs[:4] {
for _, xtyp := range xspec.types {
for _, xnt := range xtyp.entries {
if xnt == nil {
continue
}
if collision = nt.key == xnt.key; collision {
break
}
}
}
}
if !collision {
pkg.keyPool.strings[nt.key] = ""
}
}
}
}
// entries are densely packed but probably safe to drop nil entries off the end
for _, spec := range pkg.specs[:4] {
for _, typ := range spec.types {
var last int
for i, nt := range typ.entries {
if nt != nil {
last = i
}
}
typ.entries = typ.entries[:last+1]
typ.indices = typ.indices[:last+1]
}
}
// keeping 0:attr, 1:id, 2:style, 3:string
pkg.typePool.strings = pkg.typePool.strings[:4]
pkg.specs = pkg.specs[:4]
bin, err := tbl.MarshalBinary()
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
zw := gzip.NewWriter(buf)
if _, err := zw.Write(bin); err != nil {
return nil, err
}
if err := zw.Flush(); err != nil {
return nil, err
}
if err := zw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package binres
import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"strings"
"unicode/utf16"
)
const NoEntry = 0xFFFFFFFF
// TableRef uniquely identifies entries within a resource table.
type TableRef uint32
// Resolve returns the Entry of TableRef in the given table.
//
// A TableRef is structured as follows:
//
// 0xpptteeee
// pp: package index
// tt: type spec index in package
// eeee: entry index in type spec
//
// The package and type spec values start at 1 for the first item,
// to help catch cases where they have not been supplied.
func (ref TableRef) Resolve(tbl *Table) (*Entry, error) {
pkg := tbl.pkgs[uint8(ref>>24)-1]
spec := pkg.specs[uint8(ref>>16)-1]
idx := uint16(ref)
for _, typ := range spec.types {
if idx < uint16(len(typ.entries)) {
nt := typ.entries[idx]
if nt == nil {
return nil, errors.New("nil entry match")
}
return nt, nil
}
}
return nil, errors.New("failed to resolve table reference")
}
// Table is a container for packaged resources. Resource values within a package
// are obtained through pool while resource names and identifiers are obtained
// through each package's type and key pools respectively.
type Table struct {
chunkHeader
pool *Pool
pkgs []*Package
}
// NewMipmapTable returns a resource table initialized for a single xxxhdpi mipmap resource
// and the path to write resource data to.
func NewMipmapTable(pkgname string) (*Table, string) {
pkg := &Package{id: 127, name: pkgname, typePool: &Pool{}, keyPool: &Pool{}}
attr := pkg.typePool.ref("attr")
mipmap := pkg.typePool.ref("mipmap")
icon := pkg.keyPool.ref("icon")
nt := &Entry{values: []*Value{{data: &Data{Type: DataString}}}}
typ := &Type{id: 2, indices: []uint32{0}, entries: []*Entry{nt}}
typ.config.screenType.density = 640
typ.config.version.sdk = 4
pkg.specs = append(pkg.specs,
&TypeSpec{
id: uint8(attr) + 1, //1,
},
&TypeSpec{
id: uint8(mipmap) + 1, //2,
entryCount: 1,
entries: []uint32{uint32(icon)}, // {0}
types: []*Type{typ},
})
pkg.lastPublicType = uint32(len(pkg.typePool.strings)) // 2
pkg.lastPublicKey = uint32(len(pkg.keyPool.strings)) // 1
name := "res/mipmap-xxxhdpi-v4/icon.png"
tbl := &Table{pool: &Pool{}, pkgs: []*Package{pkg}}
tbl.pool.ref(name)
return tbl, name
}
// OpenSDKTable decodes resources.arsc from sdk platform jar.
func OpenSDKTable() (*Table, error) {
bin, err := apiResources()
if err != nil {
return nil, err
}
tbl := new(Table)
if err := tbl.UnmarshalBinary(bin); err != nil {
return nil, err
}
return tbl, nil
}
// OpenTable decodes the prepacked resources.arsc for the supported sdk platform.
func OpenTable() (*Table, error) {
zr, err := gzip.NewReader(bytes.NewReader(arsc))
if err != nil {
return nil, fmt.Errorf("gzip: %v", err)
}
defer zr.Close()
var buf bytes.Buffer
if _, err := io.Copy(&buf, zr); err != nil {
return nil, fmt.Errorf("io: %v", err)
}
tbl := new(Table)
if err := tbl.UnmarshalBinary(buf.Bytes()); err != nil {
return nil, err
}
return tbl, nil
}
// SpecByName parses the spec name from an entry string if necessary and returns
// the Package and TypeSpec associated with that name along with their respective
// indices.
//
// For example:
//
// tbl.SpecByName("@android:style/Theme.NoTitleBar")
// tbl.SpecByName("style")
//
// Both locate the spec by name "style".
func (tbl *Table) SpecByName(name string) (int, *Package, int, *TypeSpec, error) {
n := strings.TrimPrefix(name, "@android:")
n = strings.Split(n, "/")[0]
for pp, pkg := range tbl.pkgs {
for tt, spec := range pkg.specs {
if n == pkg.typePool.strings[spec.id-1] {
return pp, pkg, tt, spec, nil
}
}
}
return 0, nil, 0, nil, fmt.Errorf("spec by name not found: %q", name)
}
// RefByName returns the TableRef by a given name. The ref may be used to resolve the
// associated Entry and is used for the generation of binary manifest files.
func (tbl *Table) RefByName(name string) (TableRef, error) {
pp, pkg, tt, spec, err := tbl.SpecByName(name)
if err != nil {
return 0, err
}
q := strings.Split(name, "/")
if len(q) != 2 {
return 0, fmt.Errorf("invalid entry format, missing forward-slash: %q", name)
}
n := q[1]
for _, t := range spec.types {
for eeee, nt := range t.entries {
if nt == nil { // NoEntry
continue
}
if n == pkg.keyPool.strings[nt.key] {
return TableRef(uint32(eeee) | uint32(tt+1)<<16 | uint32(pp+1)<<24), nil
}
}
}
return 0, fmt.Errorf("failed to find table ref by %q", name)
}
func (tbl *Table) UnmarshalBinary(bin []byte) error {
if err := (&tbl.chunkHeader).UnmarshalBinary(bin); err != nil {
return err
}
if tbl.typ != ResTable {
return fmt.Errorf("unexpected resource type %s, want %s", tbl.typ, ResTable)
}
npkgs := btou32(bin[8:])
tbl.pkgs = make([]*Package, npkgs)
buf := bin[tbl.headerByteSize:]
tbl.pool = new(Pool)
if err := tbl.pool.UnmarshalBinary(buf); err != nil {
return err
}
buf = buf[tbl.pool.size():]
for i := range tbl.pkgs {
pkg := new(Package)
if err := pkg.UnmarshalBinary(buf); err != nil {
return err
}
tbl.pkgs[i] = pkg
buf = buf[pkg.byteSize:]
}
return nil
}
func (tbl *Table) MarshalBinary() ([]byte, error) {
bin := make([]byte, 12)
putu16(bin, uint16(ResTable))
putu16(bin[2:], 12)
putu32(bin[8:], uint32(len(tbl.pkgs)))
if tbl.pool.IsUTF8() {
tbl.pool.flags ^= UTF8Flag
defer func() {
tbl.pool.flags |= UTF8Flag
}()
}
b, err := tbl.pool.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
for _, pkg := range tbl.pkgs {
b, err = pkg.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
}
putu32(bin[4:], uint32(len(bin)))
return bin, nil
}
// Package contains a collection of resource data types.
type Package struct {
chunkHeader
id uint32
name string
lastPublicType uint32 // last index into typePool that is for public use
lastPublicKey uint32 // last index into keyPool that is for public use
typePool *Pool // type names; e.g. theme
keyPool *Pool // resource names; e.g. Theme.NoTitleBar
aliases []*StagedAlias
specs []*TypeSpec
}
func (pkg *Package) UnmarshalBinary(bin []byte) error {
if err := (&pkg.chunkHeader).UnmarshalBinary(bin); err != nil {
return err
}
if pkg.typ != ResTablePackage {
return errWrongType(pkg.typ, ResTablePackage)
}
pkg.id = btou32(bin[8:])
var name []uint16
for i := 0; i < 128; i++ {
x := btou16(bin[12+i*2:])
if x == 0 {
break
}
name = append(name, x)
}
pkg.name = string(utf16.Decode(name))
typeOffset := btou32(bin[268:]) // 0 if inheriting from another package
pkg.lastPublicType = btou32(bin[272:])
keyOffset := btou32(bin[276:]) // 0 if inheriting from another package
pkg.lastPublicKey = btou32(bin[280:])
var idOffset uint32 // value determined by either typePool or keyPool below
if typeOffset != 0 {
pkg.typePool = new(Pool)
if err := pkg.typePool.UnmarshalBinary(bin[typeOffset:]); err != nil {
return err
}
idOffset = typeOffset + pkg.typePool.byteSize
}
if keyOffset != 0 {
pkg.keyPool = new(Pool)
if err := pkg.keyPool.UnmarshalBinary(bin[keyOffset:]); err != nil {
return err
}
idOffset = keyOffset + pkg.keyPool.byteSize
}
if idOffset == 0 {
return nil
}
buf := bin[idOffset:pkg.byteSize]
for len(buf) > 0 {
t := ResType(btou16(buf))
switch t {
case ResTableTypeSpec:
spec := new(TypeSpec)
if err := spec.UnmarshalBinary(buf); err != nil {
return err
}
pkg.specs = append(pkg.specs, spec)
buf = buf[spec.byteSize:]
case ResTableType:
typ := new(Type)
if err := typ.UnmarshalBinary(buf); err != nil {
return err
}
last := pkg.specs[len(pkg.specs)-1]
last.types = append(last.types, typ)
buf = buf[typ.byteSize:]
case ResTableStagedAlias:
alias := new(StagedAlias)
if err := alias.UnmarshalBinary(buf); err != nil {
return err
}
pkg.aliases = append(pkg.aliases, alias)
buf = buf[alias.byteSize:]
default:
return errWrongType(t, ResTableTypeSpec, ResTableType, ResTableStagedAlias)
}
}
return nil
}
func (pkg *Package) MarshalBinary() ([]byte, error) {
// Package header size is determined by C++ struct ResTable_package
// see frameworks/base/include/ResourceTypes.h
bin := make([]byte, 288)
putu16(bin, uint16(ResTablePackage))
putu16(bin[2:], 288)
putu32(bin[8:], pkg.id)
p := utf16.Encode([]rune(pkg.name))
for i, x := range p {
putu16(bin[12+i*2:], x)
}
if pkg.typePool != nil {
if pkg.typePool.IsUTF8() {
pkg.typePool.flags ^= UTF8Flag
defer func() {
pkg.typePool.flags |= UTF8Flag
}()
}
b, err := pkg.typePool.MarshalBinary()
if err != nil {
return nil, err
}
putu32(bin[268:], uint32(len(bin)))
putu32(bin[272:], pkg.lastPublicType)
bin = append(bin, b...)
}
if pkg.keyPool != nil {
if pkg.keyPool.IsUTF8() {
pkg.keyPool.flags ^= UTF8Flag
defer func() {
pkg.keyPool.flags |= UTF8Flag
}()
}
b, err := pkg.keyPool.MarshalBinary()
if err != nil {
return nil, err
}
putu32(bin[276:], uint32(len(bin)))
putu32(bin[280:], pkg.lastPublicKey)
bin = append(bin, b...)
}
for _, alias := range pkg.aliases {
b, err := alias.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
}
for _, spec := range pkg.specs {
b, err := spec.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
}
putu32(bin[4:], uint32(len(bin)))
return bin, nil
}
// TypeSpec provides a specification for the resources defined by a particular type.
type TypeSpec struct {
chunkHeader
id uint8 // id-1 is name index in Package.typePool
res0 uint8 // must be 0
res1 uint16 // must be 0
entryCount uint32 // number of uint32 entry configuration masks that follow
entries []uint32 // entry configuration masks
types []*Type
}
func (spec *TypeSpec) UnmarshalBinary(bin []byte) error {
if err := (&spec.chunkHeader).UnmarshalBinary(bin); err != nil {
return err
}
if spec.typ != ResTableTypeSpec {
return errWrongType(spec.typ, ResTableTypeSpec)
}
spec.id = uint8(bin[8])
spec.res0 = uint8(bin[9])
spec.res1 = btou16(bin[10:])
spec.entryCount = btou32(bin[12:])
spec.entries = make([]uint32, spec.entryCount)
for i := range spec.entries {
spec.entries[i] = btou32(bin[16+i*4:])
}
return nil
}
func (spec *TypeSpec) MarshalBinary() ([]byte, error) {
bin := make([]byte, 16+len(spec.entries)*4)
putu16(bin, uint16(ResTableTypeSpec))
putu16(bin[2:], 16)
putu32(bin[4:], uint32(len(bin)))
bin[8] = byte(spec.id)
// [9] = 0
// [10:12] = 0
putu32(bin[12:], uint32(len(spec.entries)))
for i, x := range spec.entries {
putu32(bin[16+i*4:], x)
}
for _, typ := range spec.types {
b, err := typ.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
}
return bin, nil
}
// Type provides a collection of entries for a specific device configuration.
type Type struct {
chunkHeader
id uint8
res0 uint8 // must be 0
res1 uint16 // must be 0
entryCount uint32 // number of uint32 entry configuration masks that follow
entriesStart uint32 // offset from header where Entry data starts
// configuration this collection of entries is designed for
config struct {
size uint32
imsi struct {
mcc uint16 // mobile country code
mnc uint16 // mobile network code
}
locale struct {
language uint16
country uint16
}
screenType struct {
orientation uint8
touchscreen uint8
density uint16
}
input struct {
keyboard uint8
navigation uint8
inputFlags uint8
inputPad0 uint8
}
screenSize struct {
width uint16
height uint16
}
version struct {
sdk uint16
minor uint16 // always 0
}
screenConfig struct {
layout uint8
uiMode uint8
smallestWidthDP uint16
}
screenSizeDP struct {
width uint16
height uint16
}
}
indices []uint32 // values that map to typePool
entries []*Entry
}
func (typ *Type) UnmarshalBinary(bin []byte) error {
if err := (&typ.chunkHeader).UnmarshalBinary(bin); err != nil {
return err
}
if typ.typ != ResTableType {
return errWrongType(typ.typ, ResTableType)
}
typ.id = uint8(bin[8])
typ.res0 = uint8(bin[9])
typ.res1 = btou16(bin[10:])
typ.entryCount = btou32(bin[12:])
typ.entriesStart = btou32(bin[16:])
if typ.res0 != 0 || typ.res1 != 0 {
return fmt.Errorf("res0 res1 not zero")
}
typ.config.size = btou32(bin[20:])
typ.config.imsi.mcc = btou16(bin[24:])
typ.config.imsi.mnc = btou16(bin[26:])
typ.config.locale.language = btou16(bin[28:])
typ.config.locale.country = btou16(bin[30:])
typ.config.screenType.orientation = uint8(bin[32])
typ.config.screenType.touchscreen = uint8(bin[33])
typ.config.screenType.density = btou16(bin[34:])
typ.config.input.keyboard = uint8(bin[36])
typ.config.input.navigation = uint8(bin[37])
typ.config.input.inputFlags = uint8(bin[38])
typ.config.input.inputPad0 = uint8(bin[39])
typ.config.screenSize.width = btou16(bin[40:])
typ.config.screenSize.height = btou16(bin[42:])
typ.config.version.sdk = btou16(bin[44:])
typ.config.version.minor = btou16(bin[46:])
typ.config.screenConfig.layout = uint8(bin[48])
typ.config.screenConfig.uiMode = uint8(bin[49])
typ.config.screenConfig.smallestWidthDP = btou16(bin[50:])
typ.config.screenSizeDP.width = btou16(bin[52:])
typ.config.screenSizeDP.height = btou16(bin[54:])
// fmt.Println("language/country:", u16tos(typ.config.locale.language), u16tos(typ.config.locale.country))
buf := bin[typ.headerByteSize:typ.entriesStart]
if len(buf) != 4*int(typ.entryCount) {
return fmt.Errorf("index buffer len[%v] doesn't match entryCount[%v]", len(buf), typ.entryCount)
}
typ.indices = make([]uint32, typ.entryCount)
for i := range typ.indices {
typ.indices[i] = btou32(buf[i*4:])
}
typ.entries = make([]*Entry, typ.entryCount)
for i, x := range typ.indices {
if x == NoEntry {
continue
}
nt := &Entry{}
if err := nt.UnmarshalBinary(bin[typ.entriesStart+x:]); err != nil {
return err
}
typ.entries[i] = nt
}
return nil
}
func (typ *Type) MarshalBinary() ([]byte, error) {
bin := make([]byte, 56+len(typ.entries)*4)
putu16(bin, uint16(ResTableType))
putu16(bin[2:], 56)
bin[8] = byte(typ.id)
// [9] = 0
// [10:12] = 0
putu32(bin[12:], uint32(len(typ.entries)))
putu32(bin[16:], uint32(56+len(typ.entries)*4))
// assure typ.config.size is always written as 36; extended configuration beyond supported
// API level is not supported by this marshal implementation but will be forward-compatible.
putu32(bin[20:], 36)
putu16(bin[24:], typ.config.imsi.mcc)
putu16(bin[26:], typ.config.imsi.mnc)
putu16(bin[28:], typ.config.locale.language)
putu16(bin[30:], typ.config.locale.country)
bin[32] = typ.config.screenType.orientation
bin[33] = typ.config.screenType.touchscreen
putu16(bin[34:], typ.config.screenType.density)
bin[36] = typ.config.input.keyboard
bin[37] = typ.config.input.navigation
bin[38] = typ.config.input.inputFlags
bin[39] = typ.config.input.inputPad0
putu16(bin[40:], typ.config.screenSize.width)
putu16(bin[42:], typ.config.screenSize.height)
putu16(bin[44:], typ.config.version.sdk)
putu16(bin[46:], typ.config.version.minor)
bin[48] = typ.config.screenConfig.layout
bin[49] = typ.config.screenConfig.uiMode
putu16(bin[50:], typ.config.screenConfig.smallestWidthDP)
putu16(bin[52:], typ.config.screenSizeDP.width)
putu16(bin[54:], typ.config.screenSizeDP.height)
var ntbin []byte
for i, nt := range typ.entries {
if nt == nil { // NoEntry
putu32(bin[56+i*4:], NoEntry)
continue
}
putu32(bin[56+i*4:], uint32(len(ntbin)))
b, err := nt.MarshalBinary()
if err != nil {
return nil, err
}
ntbin = append(ntbin, b...)
}
bin = append(bin, ntbin...)
putu32(bin[4:], uint32(len(bin)))
return bin, nil
}
type StagedAliasEntry struct {
stagedID uint32
finalizedID uint32
}
func (ae *StagedAliasEntry) MarshalBinary() ([]byte, error) {
bin := make([]byte, 8)
putu32(bin, ae.stagedID)
putu32(bin[4:], ae.finalizedID)
return bin, nil
}
func (ae *StagedAliasEntry) UnmarshalBinary(bin []byte) error {
ae.stagedID = btou32(bin)
ae.finalizedID = btou32(bin[4:])
return nil
}
type StagedAlias struct {
chunkHeader
count uint32
entries []StagedAliasEntry
}
func (a *StagedAlias) UnmarshalBinary(bin []byte) error {
if err := (&a.chunkHeader).UnmarshalBinary(bin); err != nil {
return err
}
if a.typ != ResTableStagedAlias {
return errWrongType(a.typ, ResTableStagedAlias)
}
a.count = btou32(bin[8:])
a.entries = make([]StagedAliasEntry, a.count)
for i := range a.entries {
if err := a.entries[i].UnmarshalBinary(bin[12+i*8:]); err != nil {
return err
}
}
return nil
}
func (a *StagedAlias) MarshalBinary() ([]byte, error) {
chunkHeaderBin, err := a.chunkHeader.MarshalBinary()
if err != nil {
return nil, err
}
countBin := make([]byte, 4)
putu32(countBin, a.count)
bin := append(chunkHeaderBin, countBin...)
for _, entry := range a.entries {
entryBin, err := entry.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, entryBin...)
}
return bin, nil
}
// Entry is a resource key typically followed by a value or resource map.
type Entry struct {
size uint16
flags uint16
key PoolRef // ref into key pool
// only filled if this is a map entry; when size is 16
parent TableRef // id of parent mapping or zero if none
count uint32 // name and value pairs that follow for FlagComplex
values []*Value
}
func (nt *Entry) UnmarshalBinary(bin []byte) error {
nt.size = btou16(bin)
nt.flags = btou16(bin[2:])
nt.key = PoolRef(btou32(bin[4:]))
if nt.size == 16 {
nt.parent = TableRef(btou32(bin[8:]))
nt.count = btou32(bin[12:])
nt.values = make([]*Value, nt.count)
for i := range nt.values {
val := &Value{}
if err := val.UnmarshalBinary(bin[16+i*12:]); err != nil {
return err
}
nt.values[i] = val
}
} else {
data := &Data{}
if err := data.UnmarshalBinary(bin[8:]); err != nil {
return err
}
// TODO boxing data not strictly correct as binary repr isn't of Value.
nt.values = append(nt.values, &Value{0, data})
}
return nil
}
func (nt *Entry) MarshalBinary() ([]byte, error) {
bin := make([]byte, 8)
sz := nt.size
if sz == 0 {
sz = 8
}
putu16(bin, sz)
putu16(bin[2:], nt.flags)
putu32(bin[4:], uint32(nt.key))
if sz == 16 {
bin = append(bin, make([]byte, 8+len(nt.values)*12)...)
putu32(bin[8:], uint32(nt.parent))
putu32(bin[12:], uint32(len(nt.values)))
for i, val := range nt.values {
b, err := val.MarshalBinary()
if err != nil {
return nil, err
}
copy(bin[16+i*12:], b)
}
} else {
b, err := nt.values[0].data.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
}
return bin, nil
}
type Value struct {
name TableRef
data *Data
}
func (val *Value) UnmarshalBinary(bin []byte) error {
val.name = TableRef(btou32(bin))
val.data = &Data{}
return val.data.UnmarshalBinary(bin[4:])
}
func (val *Value) MarshalBinary() ([]byte, error) {
bin := make([]byte, 12)
putu32(bin, uint32(val.name))
b, err := val.data.MarshalBinary()
if err != nil {
return nil, err
}
copy(bin[4:], b)
return bin, nil
}
type DataType uint8
// explicitly defined for clarity and resolvability with apt source
const (
DataNull DataType = 0x00 // either 0 or 1 for resource undefined or empty
DataReference DataType = 0x01 // ResTable_ref, a reference to another resource table entry
DataAttribute DataType = 0x02 // attribute resource identifier
DataString DataType = 0x03 // index into the containing resource table's global value string pool
DataFloat DataType = 0x04 // single-precision floating point number
DataDimension DataType = 0x05 // complex number encoding a dimension value, such as "100in"
DataFraction DataType = 0x06 // complex number encoding a fraction of a container
DataDynamicReference DataType = 0x07 // dynamic ResTable_ref, which needs to be resolved before it can be used like a TYPE_REFERENCE.
DataIntDec DataType = 0x10 // raw integer value of the form n..n
DataIntHex DataType = 0x11 // raw integer value of the form 0xn..n
DataIntBool DataType = 0x12 // either 0 or 1, for input "false" or "true"
DataIntColorARGB8 DataType = 0x1c // raw integer value of the form #aarrggbb
DataIntColorRGB8 DataType = 0x1d // raw integer value of the form #rrggbb
DataIntColorARGB4 DataType = 0x1e // raw integer value of the form #argb
DataIntColorRGB4 DataType = 0x1f // raw integer value of the form #rgb
)
type Data struct {
ByteSize uint16
Res0 uint8 // always 0, useful for debugging bad read offsets
Type DataType
Value uint32
}
func (d *Data) UnmarshalBinary(bin []byte) error {
d.ByteSize = btou16(bin)
d.Res0 = uint8(bin[2])
d.Type = DataType(bin[3])
d.Value = btou32(bin[4:])
return nil
}
func (d *Data) MarshalBinary() ([]byte, error) {
bin := make([]byte, 8)
putu16(bin, 8)
bin[2] = byte(d.Res0)
bin[3] = byte(d.Type)
putu32(bin[4:], d.Value)
return bin, nil
}
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package mobile provides functions for building Cogent Core apps for mobile devices.
package mobile
//go:generate go run gendex.go -o dex.go
import (
"bufio"
"errors"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"slices"
"log/slog"
"maps"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/base/logx"
"cogentcore.org/core/cmd/core/config"
"golang.org/x/tools/go/packages"
)
var tmpDir string
// Build compiles and encodes the app named by the import path.
//
// The named package must define a main function.
//
// The -target flag takes either android (the default), or one or more
// comma-delimited Apple platforms (TODO: apple platforms list).
//
// For -target android, if an AndroidManifest.xml is defined in the
// package directory, it is added to the APK output. Otherwise, a default
// manifest is generated. By default, this builds a fat APK for all supported
// instruction sets (arm, 386, amd64, arm64). A subset of instruction sets can
// be selected by specifying target type with the architecture name. E.g.
// -target=android/arm,android/386.
//
// For Apple -target platforms, gomobile must be run on an OS X machine with
// Xcode installed.
//
// By default, -target ios will generate an XCFramework for both ios
// and iossimulator. Multiple Apple targets can be specified, creating a "fat"
// XCFramework with each slice. To generate a fat XCFramework that supports
// iOS, macOS, and macCatalyst for all supportec architectures (amd64 and arm64),
// specify -target ios,macos,maccatalyst. A subset of instruction sets can be
// selectged by specifying the platform with an architecture name. E.g.
// -target=ios/arm64,maccatalyst/arm64.
//
// If the package directory contains an assets subdirectory, its contents
// are copied into the output.
func Build(c *config.Config) error {
_, err := buildImpl(c)
return err
}
// buildImpl builds a package for mobiles based on the given config info.
// buildImpl returns a built package information and an error if exists.
func buildImpl(c *config.Config) (*packages.Package, error) {
cleanup, err := buildEnvInit(c)
if err != nil {
return nil, err
}
defer cleanup()
for _, platform := range c.Build.Target {
if platform.Arch == "*" {
archs := config.ArchsForOS[platform.OS]
c.Build.Target = make([]config.Platform, len(archs))
for i, arch := range archs {
c.Build.Target[i] = config.Platform{OS: platform.OS, Arch: arch}
}
}
}
// Special case to add iossimulator if we don't already have it and we have ios
hasIOSSimulator := slices.ContainsFunc(c.Build.Target,
func(p config.Platform) bool { return p.OS == "iossimulator" })
hasIOS := slices.ContainsFunc(c.Build.Target,
func(p config.Platform) bool { return p.OS == "ios" })
if !hasIOSSimulator && hasIOS {
c.Build.Target = append(c.Build.Target, config.Platform{OS: "iossimulator", Arch: "arm64"}) // TODO: set arch better here
}
// TODO(ydnar): this should work, unless build tags affect loading a single package.
// Should we try to import packages with different build tags per platform?
pkgs, err := packages.Load(packagesConfig(&c.Build.Target[0]), ".")
if err != nil {
return nil, err
}
// len(pkgs) can be more than 1 e.g., when the specified path includes `...`.
if len(pkgs) != 1 {
return nil, fmt.Errorf("expected 1 package but got %d", len(pkgs))
}
pkg := pkgs[0]
if pkg.Name != "main" {
return nil, fmt.Errorf("cannot build non-main package")
}
if c.ID == "" {
return nil, fmt.Errorf("id must be set when building for mobile")
}
switch {
case isAndroidPlatform(c.Build.Target[0].OS):
if pkg.Name != "main" {
for _, t := range c.Build.Target {
if err := goBuild(c, pkg.PkgPath, androidEnv[t.Arch]); err != nil {
return nil, err
}
}
return pkg, nil
}
_, err = goAndroidBuild(c, pkg, c.Build.Target)
if err != nil {
return nil, err
}
case isApplePlatform(c.Build.Target[0].OS):
if !xCodeAvailable() {
return nil, fmt.Errorf("-target=%s requires XCode", c.Build.Target)
}
if pkg.Name != "main" {
for _, t := range c.Build.Target {
// Catalyst support requires iOS 13+
v, _ := strconv.ParseFloat(c.Build.IOSVersion, 64)
if t.OS == "maccatalyst" && v < 13.0 {
return nil, errors.New("catalyst requires -iosversion=13 or higher")
}
if err := goBuild(c, pkg.PkgPath, appleEnv[t.String()]); err != nil {
return nil, err
}
}
return pkg, nil
}
_, err = goAppleBuild(c, pkg, c.Build.Target)
if err != nil {
return nil, err
}
}
return pkg, nil
}
var nmRE = regexp.MustCompile(`[0-9a-f]{8} t _?(?:.*/vendor/)?(golang.org/x.*/[^.]*)`)
func extractPkgs(nm string, path string) (map[string]bool, error) {
r, w := io.Pipe()
nmpkgs := make(map[string]bool)
errc := make(chan error, 1)
go func() {
s := bufio.NewScanner(r)
for s.Scan() {
if res := nmRE.FindStringSubmatch(s.Text()); res != nil {
nmpkgs[res[1]] = true
}
}
errc <- s.Err()
}()
err := exec.Major().SetStdout(w).Run(nm, path)
w.Close()
if err != nil {
return nil, fmt.Errorf("%s %s: %v", nm, path, err)
}
if err := <-errc; err != nil {
return nil, fmt.Errorf("%s %s: %v", nm, path, err)
}
return nmpkgs, nil
}
func goBuild(c *config.Config, src string, env map[string]string, args ...string) error {
return goCmd(c, "build", []string{src}, env, args...)
}
func goCmd(c *config.Config, subcmd string, srcs []string, env map[string]string, args ...string) error {
return goCmdAt(c, "", subcmd, srcs, env, args...)
}
func goCmdAt(c *config.Config, at string, subcmd string, srcs []string, env map[string]string, args ...string) error {
cargs := []string{subcmd}
// cmd := exec.Command("go", subcmd)
var tags []string
if c.Build.Debug {
tags = append(tags, "debug")
}
if len(tags) > 0 {
cargs = append(cargs, "-tags", strings.Join(tags, ","))
}
if logx.UserLevel <= slog.LevelInfo {
cargs = append(cargs, "-v")
}
cargs = append(cargs, args...)
cargs = append(cargs, srcs...)
xc := exec.Major().SetDir(at)
maps.Copy(xc.Env, env)
// Specify GOMODCACHE explicitly. The default cache path is GOPATH[0]/pkg/mod,
// but the path varies when GOPATH is specified at env, which results in cold cache.
if gmc, err := goModCachePath(); err == nil {
xc.SetEnv("GOMODCACHE", gmc)
}
return xc.Run("go", cargs...)
}
func goModCachePath() (string, error) {
out, err := exec.Output("go", "env", "GOMODCACHE")
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mobile
import (
"bytes"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"encoding/xml"
"errors"
"fmt"
"image/png"
"io"
"log"
"os"
"path/filepath"
"cogentcore.org/core/base/elide"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/base/logx"
"cogentcore.org/core/cmd/core/config"
"cogentcore.org/core/cmd/core/mobile/binres"
"cogentcore.org/core/cmd/core/rendericon"
"golang.org/x/tools/go/packages"
)
const (
minAndroidSDK = 23
defaultAndroidTargetSDK = 29
)
// goAndroidBuild builds the given package for the given Android targets.
func goAndroidBuild(c *config.Config, pkg *packages.Package, targets []config.Platform) (map[string]bool, error) {
ndkRoot, err := ndkRoot(c, targets...)
if err != nil {
return nil, err
}
libName := androidPkgName(c.Name)
// TODO(hajimehoshi): This works only with Go tools that assume all source files are in one directory.
// Fix this to work with other Go tools.
dir := filepath.Dir(pkg.GoFiles[0])
manifestPath := filepath.Join(dir, "AndroidManifest.xml")
manifestData, err := os.ReadFile(manifestPath)
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
buf := new(bytes.Buffer)
buf.WriteString(`<?xml version="1.0" encoding="utf-8"?>`)
err := manifestTmpl.Execute(buf, manifestTmplData{
JavaPkgPath: c.ID,
Name: elide.AppName(c.Name),
LibName: libName,
})
if err != nil {
return nil, err
}
manifestData = buf.Bytes()
logx.PrintfDebug("generated AndroidManifest.xml:\n%s\n", manifestData)
} else {
libName, err = manifestLibName(manifestData)
if err != nil {
return nil, fmt.Errorf("error parsing %s: %v", manifestPath, err)
}
}
libFiles := []string{}
nmpkgs := make(map[string]map[string]bool) // map: arch -> extractPkgs' output
for _, t := range targets {
toolchain := ndk.toolchain(t.Arch)
libPath := "lib/" + toolchain.ABI + "/lib" + libName + ".so"
libAbsPath := filepath.Join(tmpDir, libPath)
if err := exec.MkdirAll(filepath.Dir(libAbsPath), 0755); err != nil {
return nil, err
}
args := []string{
"-buildmode=c-shared",
"-ldflags", config.LinkerFlags(c),
"-o", libAbsPath,
}
if c.Build.Trimpath {
args = append(args, "-trimpath")
}
err = goBuild(
c,
pkg.PkgPath,
androidEnv[t.Arch],
args...,
)
if err != nil {
return nil, err
}
nmpkgs[t.Arch], err = extractPkgs(toolchain.path(c, ndkRoot, "nm"), libAbsPath)
if err != nil {
return nil, err
}
libFiles = append(libFiles, libPath)
}
block, _ := pem.Decode([]byte(debugCert))
if block == nil {
return nil, errors.New("no debug cert")
}
privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
err = os.MkdirAll(c.Build.Output, 0777)
if err != nil {
return nil, err
}
var out io.Writer
f, err := os.Create(filepath.Join(c.Build.Output, c.Name+".apk"))
if err != nil {
return nil, err
}
defer func() {
if cerr := f.Close(); err == nil {
err = cerr
}
}()
out = f
apkw := newWriter(out, privKey)
apkwCreate := func(name string) (io.Writer, error) {
logx.PrintfInfo("apk: %s\n", name)
return apkw.Create(name)
}
apkwWriteFile := func(dst, src string) error {
w, err := apkwCreate(dst)
if err != nil {
return err
}
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(w, f); err != nil {
return err
}
return nil
}
// TODO: do we need this writer stuff?
w, err := apkwCreate("classes.dex")
if err != nil {
return nil, err
}
dexData, err := base64.StdEncoding.DecodeString(dexStr)
if err != nil {
log.Fatalf("internal error: bad dexStr: %v", err)
}
if _, err := w.Write(dexData); err != nil {
return nil, err
}
for _, libFile := range libFiles {
if err := apkwWriteFile(libFile, filepath.Join(tmpDir, libFile)); err != nil {
return nil, err
}
}
// TODO: what should we do about OpenAL?
for _, t := range targets {
toolchain := ndk.toolchain(t.Arch)
if nmpkgs[t.Arch]["cogentcore.org/core/mobile/exp/audio/al"] {
dst := "lib/" + toolchain.ABI + "/libopenal.so"
src := filepath.Join(goMobilePath, dst)
if _, err := os.Stat(src); err != nil {
return nil, errors.New("the Android requires the golang.org/x/mobile/exp/audio/al, but the OpenAL libraries was not found. Please run gomobile init with the -openal flag pointing to an OpenAL source directory")
}
if err := apkwWriteFile(dst, src); err != nil {
return nil, err
}
}
}
// Add the icon. 512 is the largest icon size on Android
// (for the Google Play Store icon).
ic, err := rendericon.Render(512)
if err != nil {
return nil, err
}
bxml, err := binres.UnmarshalXML(bytes.NewReader(manifestData), true, c.Build.AndroidMinSDK, c.Build.AndroidTargetSDK)
if err != nil {
return nil, err
}
// generate resources.arsc identifying single xxxhdpi icon resource.
pkgname, err := bxml.RawValueByName("manifest", xml.Name{Local: "package"})
if err != nil {
return nil, err
}
tbl, name := binres.NewMipmapTable(pkgname)
iw, err := apkwCreate(name)
if err != nil {
return nil, err
}
err = png.Encode(iw, ic)
if err != nil {
return nil, err
}
resw, err := apkwCreate("resources.arsc")
if err != nil {
return nil, err
}
rbin, err := tbl.MarshalBinary()
if err != nil {
return nil, err
}
if _, err := resw.Write(rbin); err != nil {
return nil, err
}
w, err = apkwCreate("AndroidManifest.xml")
if err != nil {
return nil, err
}
bin, err := bxml.MarshalBinary()
if err != nil {
return nil, err
}
if _, err := w.Write(bin); err != nil {
return nil, err
}
// TODO: add gdbserver to apk?
if err := apkw.Close(); err != nil {
return nil, err
}
// TODO: return nmpkgs
return nmpkgs[targets[0].Arch], nil
}
// androidPkgName sanitizes the go package name to be acceptable as a android
// package name part. The android package name convention is similar to the
// java package name convention described in
// https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.5.3.1
// but not exactly same.
func androidPkgName(name string) string {
var res []rune
for _, r := range name {
switch {
case 'a' <= r && r <= 'z', 'A' <= r && r <= 'Z', '0' <= r && r <= '9':
res = append(res, r)
default:
res = append(res, '_')
}
}
if len(res) == 0 || res[0] == '_' || ('0' <= res[0] && res[0] <= '9') {
// Android does not seem to allow the package part starting with _.
res = append([]rune{'g', 'o'}, res...)
}
s := string(res)
// Look for Java keywords that are not Go keywords, and avoid using
// them as a package name.
//
// This is not a problem for normal Go identifiers as we only expose
// exported symbols. The upper case first letter saves everything
// from accidentally matching except for the package name.
//
// Note that basic type names (like int) are not keywords in Go.
switch s {
case "abstract", "assert", "boolean", "byte", "catch", "char", "class",
"do", "double", "enum", "extends", "final", "finally", "float",
"implements", "instanceof", "int", "long", "native", "private",
"protected", "public", "short", "static", "strictfp", "super",
"synchronized", "this", "throw", "throws", "transient", "try",
"void", "volatile", "while":
s += "_"
}
return s
}
// A random uninteresting private key.
// Must be consistent across builds so newer app versions can be installed.
const debugCert = `
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAy6ItnWZJ8DpX9R5FdWbS9Kr1U8Z7mKgqNByGU7No99JUnmyu
NQ6Uy6Nj0Gz3o3c0BXESECblOC13WdzjsH1Pi7/L9QV8jXOXX8cvkG5SJAyj6hcO
LOapjDiN89NXjXtyv206JWYvRtpexyVrmHJgRAw3fiFI+m4g4Qop1CxcIF/EgYh7
rYrqh4wbCM1OGaCleQWaOCXxZGm+J5YNKQcWpjZRrDrb35IZmlT0bK46CXUKvCqK
x7YXHgfhC8ZsXCtsScKJVHs7gEsNxz7A0XoibFw6DoxtjKzUCktnT0w3wxdY7OTj
9AR8mobFlM9W3yirX8TtwekWhDNTYEu8dwwykwIDAQABAoIBAA2hjpIhvcNR9H9Z
BmdEecydAQ0ZlT5zy1dvrWI++UDVmIp+Ve8BSd6T0mOqV61elmHi3sWsBN4M1Rdz
3N38lW2SajG9q0fAvBpSOBHgAKmfGv3Ziz5gNmtHgeEXfZ3f7J95zVGhlHqWtY95
JsmuplkHxFMyITN6WcMWrhQg4A3enKLhJLlaGLJf9PeBrvVxHR1/txrfENd2iJBH
FmxVGILL09fIIktJvoScbzVOneeWXj5vJGzWVhB17DHBbANGvVPdD5f+k/s5aooh
hWAy/yLKocr294C4J+gkO5h2zjjjSGcmVHfrhlXQoEPX+iW1TGoF8BMtl4Llc+jw
lKWKfpECgYEA9C428Z6CvAn+KJ2yhbAtuRo41kkOVoiQPtlPeRYs91Pq4+NBlfKO
2nWLkyavVrLx4YQeCeaEU2Xoieo9msfLZGTVxgRlztylOUR+zz2FzDBYGicuUD3s
EqC0Wv7tiX6dumpWyOcVVLmR9aKlOUzA9xemzIsWUwL3PpyONhKSq7kCgYEA1X2F
f2jKjoOVzglhtuX4/SP9GxS4gRf9rOQ1Q8DzZhyH2LZ6Dnb1uEQvGhiqJTU8CXxb
7odI0fgyNXq425Nlxc1Tu0G38TtJhwrx7HWHuFcbI/QpRtDYLWil8Zr7Q3BT9rdh
moo4m937hLMvqOG9pyIbyjOEPK2WBCtKW5yabqsCgYEAu9DkUBr1Qf+Jr+IEU9I8
iRkDSMeusJ6gHMd32pJVCfRRQvIlG1oTyTMKpafmzBAd/rFpjYHynFdRcutqcShm
aJUq3QG68U9EAvWNeIhA5tr0mUEz3WKTt4xGzYsyWES8u4tZr3QXMzD9dOuinJ1N
+4EEumXtSPKKDG3M8Qh+KnkCgYBUEVSTYmF5EynXc2xOCGsuy5AsrNEmzJqxDUBI
SN/P0uZPmTOhJIkIIZlmrlW5xye4GIde+1jajeC/nG7U0EsgRAV31J4pWQ5QJigz
0+g419wxIUFryGuIHhBSfpP472+w1G+T2mAGSLh1fdYDq7jx6oWE7xpghn5vb9id
EKLjdwKBgBtz9mzbzutIfAW0Y8F23T60nKvQ0gibE92rnUbjPnw8HjL3AZLU05N+
cSL5bhq0N5XHK77sscxW9vXjG0LJMXmFZPp9F6aV6ejkMIXyJ/Yz/EqeaJFwilTq
Mc6xR47qkdzu0dQ1aPm4XD7AWDtIvPo/GG2DKOucLBbQc2cOWtKS
-----END RSA PRIVATE KEY-----
`
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mobile
import (
"bytes"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"text/template"
"cogentcore.org/core/base/elide"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/cmd/core/config"
"cogentcore.org/core/cmd/core/rendericon"
"github.com/jackmordaunt/icns/v2"
"golang.org/x/tools/go/packages"
)
// goAppleBuild builds the given package with the given bundle ID for the given iOS targets.
func goAppleBuild(c *config.Config, pkg *packages.Package, targets []config.Platform) (map[string]bool, error) {
src := pkg.PkgPath
infoplist := new(bytes.Buffer)
if err := infoPlistTmpl.Execute(infoplist, infoPlistTmplData{
BundleID: c.ID,
Name: elide.AppName(c.Name),
Version: c.Version,
InfoString: c.About,
ShortVersionString: c.Version,
IconFile: "icon.icns",
}); err != nil {
return nil, err
}
// Detect the team ID
teamID, err := detectTeamID()
if err != nil {
return nil, err
}
projPbxproj := new(bytes.Buffer)
if err := projPbxprojTmpl.Execute(projPbxproj, projPbxprojTmplData{
TeamID: teamID,
}); err != nil {
return nil, err
}
files := []struct {
name string
contents []byte
}{
{tmpDir + "/main.xcodeproj/project.pbxproj", projPbxproj.Bytes()},
{tmpDir + "/main/Info.plist", infoplist.Bytes()},
{tmpDir + "/main/Images.xcassets/AppIcon.appiconset/Contents.json", []byte(contentsJSON)},
}
for _, file := range files {
if err := exec.MkdirAll(filepath.Dir(file.name), 0755); err != nil {
return nil, err
}
exec.PrintCmd(fmt.Sprintf("echo \"%s\" > %s", file.contents, file.name), nil)
if err := os.WriteFile(file.name, file.contents, 0644); err != nil {
return nil, err
}
}
// We are using lipo tool to build multiarchitecture binaries.
args := []string{"lipo", "-o", filepath.Join(tmpDir, "main/main"), "-create"}
var nmpkgs map[string]bool
builtArch := map[string]bool{}
for _, t := range targets {
// Only one binary per arch allowed
// e.g. ios/arm64 + iossimulator/amd64
if builtArch[t.Arch] {
continue
}
builtArch[t.Arch] = true
path := filepath.Join(tmpDir, t.OS, t.Arch)
buildArgs := []string{
"-ldflags", config.LinkerFlags(c), "-o=" + path,
}
if c.Build.Trimpath {
buildArgs = append(buildArgs, "-trimpath")
}
if err := goBuild(c, src, appleEnv[t.String()], buildArgs...); err != nil {
return nil, err
}
if nmpkgs == nil {
var err error
nmpkgs, err = extractPkgs(appleNM, path)
if err != nil {
return nil, err
}
}
args = append(args, path)
}
if err := exec.Run("xcrun", args...); err != nil {
return nil, err
}
if err := appleCopyAssets(tmpDir); err != nil {
return nil, err
}
// Build and move the release build to the output directory.
err = exec.Run("xcrun", "xcodebuild",
"-configuration", "Release",
"-project", tmpDir+"/main.xcodeproj",
"-allowProvisioningUpdates",
"DEVELOPMENT_TEAM="+teamID)
if err != nil {
return nil, err
}
inm := filepath.Join(tmpDir+"/build/Release-iphoneos/main.app", "icon.icns")
fdsi, err := os.Create(inm)
if err != nil {
return nil, err
}
defer fdsi.Close()
// 1024x1024 is the largest icon size on iOS
// (for the App Store)
sic, err := rendericon.Render(1024)
if err != nil {
return nil, err
}
err = icns.Encode(fdsi, sic)
if err != nil {
return nil, err
}
// TODO(jbd): Fallback to copying if renaming fails.
err = os.MkdirAll(c.Build.Output, 0777)
if err != nil {
return nil, err
}
output := filepath.Join(c.Build.Output, c.Name+".app")
exec.PrintCmd(fmt.Sprintf("mv %s %s", tmpDir+"/build/Release-iphoneos/main.app", output), nil)
// if output already exists, remove.
if err := exec.RemoveAll(output); err != nil {
return nil, err
}
if err := os.Rename(tmpDir+"/build/Release-iphoneos/main.app", output); err != nil {
return nil, err
}
return nmpkgs, nil
}
// detectTeamID determines the Apple Development Team ID on the system.
func detectTeamID() (string, error) {
// Grabs the first certificate for "Apple Development"; will not work if there
// are multiple certificates and the first is not desired.
pemString, err := exec.Output(
"security", "find-certificate",
"-c", "Apple Development", "-p",
)
if err != nil {
err = fmt.Errorf("failed to pull the signing certificate to determine your team ID: %v", err)
return "", err
}
block, _ := pem.Decode([]byte(pemString))
if block == nil {
err = fmt.Errorf("failed to decode the PEM to determine your team ID: %s", pemString)
return "", err
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
err = fmt.Errorf("failed to parse your signing certificate to determine your team ID: %v", err)
return "", err
}
if len(cert.Subject.OrganizationalUnit) == 0 {
err = fmt.Errorf("the signing certificate has no organizational unit (team ID)")
return "", err
}
return cert.Subject.OrganizationalUnit[0], nil
}
func appleCopyAssets(xcodeProjDir string) error {
dstAssets := xcodeProjDir + "/main/assets"
return exec.MkdirAll(dstAssets, 0755)
}
type infoPlistTmplData struct {
BundleID string
Name string
Version string
InfoString string
ShortVersionString string
IconFile string
}
var infoPlistTmpl = template.Must(template.New("infoPlist").Parse(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>main</string>
<key>CFBundleIdentifier</key>
<string>{{.BundleID}}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>{{.Name}}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>{{ .Version }}</string>
<key>CFBundleGetInfoString</key>
<string>{{ .InfoString }}</string>
<key>CFBundleShortVersionString</key>
<string>{{ .ShortVersionString }}</string>
<key>CFBundleIconFile</key>
<string>{{ .IconFile }}</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
`))
type projPbxprojTmplData struct {
TeamID string
}
var projPbxprojTmpl = template.Must(template.New("projPbxproj").Parse(`// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
254BB84F1B1FD08900C56DE9 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 254BB84E1B1FD08900C56DE9 /* Images.xcassets */; };
254BB8681B1FD16500C56DE9 /* main in Resources */ = {isa = PBXBuildFile; fileRef = 254BB8671B1FD16500C56DE9 /* main */; };
25FB30331B30FDEE0005924C /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 25FB30321B30FDEE0005924C /* assets */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
254BB83E1B1FD08900C56DE9 /* main.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = main.app; sourceTree = BUILT_PRODUCTS_DIR; };
254BB8421B1FD08900C56DE9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
254BB84E1B1FD08900C56DE9 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
254BB8671B1FD16500C56DE9 /* main */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = main; sourceTree = "<group>"; };
25FB30321B30FDEE0005924C /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = main/assets; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
254BB8351B1FD08900C56DE9 = {
isa = PBXGroup;
children = (
25FB30321B30FDEE0005924C /* assets */,
254BB8401B1FD08900C56DE9 /* main */,
254BB83F1B1FD08900C56DE9 /* Products */,
);
sourceTree = "<group>";
usesTabs = 0;
};
254BB83F1B1FD08900C56DE9 /* Products */ = {
isa = PBXGroup;
children = (
254BB83E1B1FD08900C56DE9 /* main.app */,
);
name = Products;
sourceTree = "<group>";
};
254BB8401B1FD08900C56DE9 /* main */ = {
isa = PBXGroup;
children = (
254BB8671B1FD16500C56DE9 /* main */,
254BB84E1B1FD08900C56DE9 /* Images.xcassets */,
254BB8411B1FD08900C56DE9 /* Supporting Files */,
);
path = main;
sourceTree = "<group>";
};
254BB8411B1FD08900C56DE9 /* Supporting Files */ = {
isa = PBXGroup;
children = (
254BB8421B1FD08900C56DE9 /* Info.plist */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
254BB83D1B1FD08900C56DE9 /* main */ = {
isa = PBXNativeTarget;
buildConfigurationList = 254BB8611B1FD08900C56DE9 /* Build configuration list for PBXNativeTarget "main" */;
buildPhases = (
254BB83C1B1FD08900C56DE9 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = main;
productName = main;
productReference = 254BB83E1B1FD08900C56DE9 /* main.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
254BB8361B1FD08900C56DE9 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0630;
ORGANIZATIONNAME = Developer;
TargetAttributes = {
254BB83D1B1FD08900C56DE9 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = {{.TeamID}};
};
};
};
buildConfigurationList = 254BB8391B1FD08900C56DE9 /* Build configuration list for PBXProject "main" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 254BB8351B1FD08900C56DE9;
productRefGroup = 254BB83F1B1FD08900C56DE9 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
254BB83D1B1FD08900C56DE9 /* main */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
254BB83C1B1FD08900C56DE9 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
25FB30331B30FDEE0005924C /* assets in Resources */,
254BB8681B1FD16500C56DE9 /* main in Resources */,
254BB84F1B1FD08900C56DE9 /* Images.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
254BB8601B1FD08900C56DE9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
ENABLE_BITCODE = NO;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
};
name = Release;
};
254BB8631B1FD08900C56DE9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = main/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
254BB8391B1FD08900C56DE9 /* Build configuration list for PBXProject "main" */ = {
isa = XCConfigurationList;
buildConfigurations = (
254BB8601B1FD08900C56DE9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
254BB8611B1FD08900C56DE9 /* Build configuration list for PBXNativeTarget "main" */ = {
isa = XCConfigurationList;
buildConfigurations = (
254BB8631B1FD08900C56DE9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 254BB8361B1FD08900C56DE9 /* Project object */;
}
`))
const contentsJSON = `{
"images" : [
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
`
// rfc1034Label sanitizes the name to be usable in a uniform type identifier.
// The sanitization is similar to xcode's rfc1034identifier macro that
// replaces illegal characters (not conforming the rfc1034 label rule) with '-'.
func rfc1034Label(name string) string {
// * Uniform type identifier:
//
// According to
// https://developer.apple.com/library/ios/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_conc/understand_utis_conc.html
//
// A uniform type identifier is a Unicode string that usually contains characters
// in the ASCII character set. However, only a subset of the ASCII characters are
// permitted. You may use the Roman alphabet in upper and lower case (A–Z, a–z),
// the digits 0 through 9, the dot (“.”), and the hyphen (“-”). This restriction
// is based on DNS name restrictions, set forth in RFC 1035.
//
// Uniform type identifiers may also contain any of the Unicode characters greater
// than U+007F.
//
// Note: the actual implementation of xcode does not allow some unicode characters
// greater than U+007f. In this implementation, we just replace everything non
// alphanumeric with "-" like the rfc1034identifier macro.
//
// * RFC1034 Label
//
// <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
// <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
// <let-dig-hyp> ::= <let-dig> | "-"
// <let-dig> ::= <letter> | <digit>
const surrSelf = 0x10000
begin := false
var res []rune
for i, r := range name {
if r == '.' && !begin {
continue
}
begin = true
switch {
case 'a' <= r && r <= 'z', 'A' <= r && r <= 'Z':
res = append(res, r)
case '0' <= r && r <= '9':
if i == 0 {
res = append(res, '-')
} else {
res = append(res, r)
}
default:
if r < surrSelf {
res = append(res, '-')
} else {
res = append(res, '-', '-')
}
}
}
return string(res)
}
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mobile
import (
"crypto"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"io"
"math/big"
"time"
)
// signPKCS7 does the minimal amount of work necessary to embed an RSA
// signature into a PKCS#7 certificate.
//
// We prepare the certificate using the x509 package, read it back in
// to our custom data type and then write it back out with the signature.
func signPKCS7(rand io.Reader, priv *rsa.PrivateKey, msg []byte) ([]byte, error) {
const serialNumber = 0x5462c4dd // arbitrary
name := pkix.Name{CommonName: "gomobile"}
template := &x509.Certificate{
SerialNumber: big.NewInt(serialNumber),
SignatureAlgorithm: x509.SHA1WithRSA,
Subject: name,
}
b, err := x509.CreateCertificate(rand, template, template, priv.Public(), priv)
if err != nil {
return nil, err
}
c := certificate{}
if _, err := asn1.Unmarshal(b, &c); err != nil {
return nil, err
}
h := sha1.New()
h.Write(msg)
hashed := h.Sum(nil)
signed, err := rsa.SignPKCS1v15(rand, priv, crypto.SHA1, hashed)
if err != nil {
return nil, err
}
content := pkcs7SignedData{
ContentType: oidSignedData,
Content: signedData{
Version: 1,
DigestAlgorithms: []pkix.AlgorithmIdentifier{{
Algorithm: oidSHA1,
Parameters: asn1.RawValue{Tag: 5},
}},
ContentInfo: contentInfo{Type: oidData},
Certificates: c,
SignerInfos: []signerInfo{{
Version: 1,
IssuerAndSerialNumber: issuerAndSerialNumber{
Issuer: name.ToRDNSequence(),
SerialNumber: serialNumber,
},
DigestAlgorithm: pkix.AlgorithmIdentifier{
Algorithm: oidSHA1,
Parameters: asn1.RawValue{Tag: 5},
},
DigestEncryptionAlgorithm: pkix.AlgorithmIdentifier{
Algorithm: oidRSAEncryption,
Parameters: asn1.RawValue{Tag: 5},
},
EncryptedDigest: signed,
}},
},
}
return asn1.Marshal(content)
}
type pkcs7SignedData struct {
ContentType asn1.ObjectIdentifier
Content signedData `asn1:"tag:0,explicit"`
}
// signedData is defined in rfc2315, section 9.1.
type signedData struct {
Version int
DigestAlgorithms []pkix.AlgorithmIdentifier `asn1:"set"`
ContentInfo contentInfo
Certificates certificate `asn1:"tag0,explicit"`
SignerInfos []signerInfo `asn1:"set"`
}
type contentInfo struct {
Type asn1.ObjectIdentifier
// Content is optional in PKCS#7 and not provided here.
}
// certificate is defined in rfc2459, section 4.1.
type certificate struct {
TBSCertificate tbsCertificate
SignatureAlgorithm pkix.AlgorithmIdentifier
SignatureValue asn1.BitString
}
// tbsCertificate is defined in rfc2459, section 4.1.
type tbsCertificate struct {
Version int `asn1:"tag:0,default:2,explicit"`
SerialNumber int
Signature pkix.AlgorithmIdentifier
Issuer pkix.RDNSequence // pkix.Name
Validity validity
Subject pkix.RDNSequence // pkix.Name
SubjectPKI subjectPublicKeyInfo
}
// validity is defined in rfc2459, section 4.1.
type validity struct {
NotBefore time.Time
NotAfter time.Time
}
// subjectPublicKeyInfo is defined in rfc2459, section 4.1.
type subjectPublicKeyInfo struct {
Algorithm pkix.AlgorithmIdentifier
SubjectPublicKey asn1.BitString
}
type signerInfo struct {
Version int
IssuerAndSerialNumber issuerAndSerialNumber
DigestAlgorithm pkix.AlgorithmIdentifier
DigestEncryptionAlgorithm pkix.AlgorithmIdentifier
EncryptedDigest []byte
}
type issuerAndSerialNumber struct {
Issuer pkix.RDNSequence // pkix.Name
SerialNumber int
}
// Various ASN.1 Object Identifies, mostly from rfc3852.
var (
// oidPKCS7 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7}
oidData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1}
oidSignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}
oidSHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26}
oidRSAEncryption = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}
)
package mobile
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"slices"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/base/logx"
"cogentcore.org/core/cmd/core/config"
"cogentcore.org/core/cmd/core/mobile/sdkpath"
)
// General mobile build environment. Initialized by envInit.
var (
goMobilePath string // $GOPATH/pkg/gomobile
androidEnv map[string]map[string]string // android arch -> map[string]string
appleEnv map[string]map[string]string // platform/arch -> map[string]string
appleNM string
)
func isAndroidPlatform(platform string) bool {
return platform == "android"
}
func isApplePlatform(platform string) bool {
return slices.Contains(applePlatforms, platform)
}
var applePlatforms = []string{"ios", "iossimulator", "macos", "maccatalyst"}
func platformArchs(platform string) []string {
switch platform {
case "ios":
return []string{"arm64"}
case "iossimulator":
return []string{"arm64", "amd64"}
case "macos", "maccatalyst":
return []string{"arm64", "amd64"}
case "android":
return []string{"arm", "arm64", "386", "amd64"}
default:
panic(fmt.Sprintf("unexpected platform: %s", platform))
}
}
// platformOS returns the correct GOOS value for platform.
func platformOS(platform string) string {
switch platform {
case "android":
return "android"
case "ios", "iossimulator":
return "ios"
case "macos", "maccatalyst":
// For "maccatalyst", Go packages should be built with GOOS=darwin,
// not GOOS=ios, since the underlying OS (and kernel, runtime) is macOS.
// We also apply a "macos" or "maccatalyst" build tag, respectively.
// See below for additional context.
return "darwin"
default:
panic(fmt.Sprintf("unexpected platform: %s", platform))
}
}
func platformTags(platform string) []string {
switch platform {
case "android":
return []string{"android"}
case "ios", "iossimulator":
return []string{"ios"}
case "macos":
return []string{"macos"}
case "maccatalyst":
// Mac Catalyst is a subset of iOS APIs made available on macOS
// designed to ease porting apps developed for iPad to macOS.
// See https://developer.apple.com/mac-catalyst/.
// Because of this, when building a Go package targeting maccatalyst,
// GOOS=darwin (not ios). To bridge the gap and enable maccatalyst
// packages to be compiled, we also specify the "ios" build tag.
// To help discriminate between darwin, ios, macos, and maccatalyst
// targets, there is also a "maccatalyst" tag.
// Some additional context on this can be found here:
// https://stackoverflow.com/questions/12132933/preprocessor-macro-for-os-x-targets/49560690#49560690
// TODO(ydnar): remove tag "ios" when cgo supports Catalyst
// See golang.org/issues/47228
return []string{"ios", "macos", "maccatalyst"}
default:
panic(fmt.Sprintf("unexpected platform: %s", platform))
}
}
func buildEnvInit(c *config.Config) (cleanup func(), err error) {
// Find gomobilepath.
gopath := goEnv("GOPATH")
for _, p := range filepath.SplitList(gopath) {
goMobilePath = filepath.Join(p, "pkg", "gomobile")
if _, err := os.Stat(goMobilePath); err == nil {
break
}
}
logx.PrintlnInfo("GOMOBILE=" + goMobilePath)
// Check the toolchain is in a good state.
// Pick a temporary directory for assembling an apk/app.
if goMobilePath == "" {
return nil, errors.New("toolchain not installed, run `gomobile init`")
}
cleanupFn := func() {
exec.RemoveAll(tmpDir)
}
tmpDir, err = os.MkdirTemp("", "gomobile-work-")
if err != nil {
return nil, err
}
logx.PrintlnInfo("WORK=" + tmpDir)
if err := envInit(c); err != nil {
return nil, err
}
return cleanupFn, nil
}
func envInit(c *config.Config) (err error) {
// Setup the cross-compiler environments.
if ndkRoot, err := ndkRoot(c); err == nil {
androidEnv = make(map[string]map[string]string)
if c.Build.AndroidMinSDK < minAndroidSDK {
return fmt.Errorf("gomobile requires Android API level >= %d", minAndroidSDK)
}
for arch, toolchain := range ndk {
clang := toolchain.path(c, ndkRoot, "clang")
clangpp := toolchain.path(c, ndkRoot, "clang++")
tools := []string{clang, clangpp}
if runtime.GOOS == "windows" {
// Because of https://github.com/android-ndk/ndk/issues/920,
// we require r19c, not just r19b. Fortunately, the clang++.cmd
// script only exists in r19c.
tools = append(tools, clangpp+".cmd")
}
for _, tool := range tools {
_, err = os.Stat(tool)
if err != nil {
return fmt.Errorf("no compiler for %s was found in the NDK (tried %s). Make sure your NDK version is >= r19c. Use `sdkmanager --update` to update it", arch, tool)
}
}
androidEnv[arch] = map[string]string{
"GOOS": "android",
"GOARCH": arch,
"CC": clang,
"CXX": clangpp,
"CGO_ENABLED": "1",
}
if arch == "arm" {
androidEnv[arch]["GOARM"] = "7"
}
}
}
if !xCodeAvailable() {
return nil
}
appleNM = "nm"
appleEnv = make(map[string]map[string]string)
for _, platform := range applePlatforms {
for _, arch := range platformArchs(platform) {
var goos, sdk, clang, cflags string
var err error
switch platform {
case "ios":
goos = "ios"
sdk = "iphoneos"
clang, cflags, err = envClang(sdk)
cflags += " -mios-version-min=" + c.Build.IOSVersion
case "iossimulator":
goos = "ios"
sdk = "iphonesimulator"
clang, cflags, err = envClang(sdk)
cflags += " -mios-simulator-version-min=" + c.Build.IOSVersion
case "maccatalyst":
// Mac Catalyst is a subset of iOS APIs made available on macOS
// designed to ease porting apps developed for iPad to macOS.
// See https://developer.apple.com/mac-catalyst/.
// Because of this, when building a Go package targeting maccatalyst,
// GOOS=darwin (not ios). To bridge the gap and enable maccatalyst
// packages to be compiled, we also specify the "ios" build tag.
// To help discriminate between darwin, ios, macos, and maccatalyst
// targets, there is also a "maccatalyst" tag.
// Some additional context on this can be found here:
// https://stackoverflow.com/questions/12132933/preprocessor-macro-for-os-x-targets/49560690#49560690
goos = "darwin"
sdk = "macosx"
clang, cflags, err = envClang(sdk)
// TODO(ydnar): the following 3 lines MAY be needed to compile
// packages or apps for maccatalyst. Commenting them out now in case
// it turns out they are necessary. Currently none of the example
// apps will build for macos or maccatalyst because they have a
// GLKit dependency, which is deprecated on all Apple platforms, and
// broken on maccatalyst (GLKView isn’t available).
// sysroot := strings.SplitN(cflags, " ", 2)[1]
// cflags += " -isystem " + sysroot + "/System/iOSSupport/usr/include"
// cflags += " -iframework " + sysroot + "/System/iOSSupport/System/Library/Frameworks"
switch arch {
case "amd64":
cflags += " -target x86_64-apple-ios" + c.Build.IOSVersion + "-macabi"
case "arm64":
cflags += " -target arm64-apple-ios" + c.Build.IOSVersion + "-macabi"
}
case "macos":
goos = "darwin"
sdk = "macosx" // Note: the SDK is called "macosx", not "macos"
clang, cflags, err = envClang(sdk)
default:
panic(fmt.Errorf("unknown Apple target: %s/%s", platform, arch))
}
if err != nil {
return err
}
appleEnv[platform+"/"+arch] = map[string]string{
"GOOS": goos,
"GOARCH": arch,
"GOFLAGS": "-tags=" + strings.Join(platformTags(platform), ","),
"CC": clang,
"CXX": clang + "++",
"CGO_CFLAGS": cflags + " -arch " + archClang(arch),
"CGO_CXXFLAGS": cflags + " -arch " + archClang(arch),
"CGO_LDFLAGS": cflags + " -arch " + archClang(arch),
"CGO_ENABLED": "1",
"DARWIN_SDK": sdk,
}
}
}
return nil
}
// abi maps GOARCH values to Android abi strings.
// See https://developer.android.com/ndk/guides/abis
func abi(goarch string) string {
switch goarch {
case "arm":
return "armeabi-v7a"
case "arm64":
return "arm64-v8a"
case "386":
return "x86"
case "amd64":
return "x86_64"
default:
return ""
}
}
// checkNDKRoot returns nil if the NDK in `ndkRoot` supports the current configured
// API version and all the specified Android targets.
func checkNDKRoot(c *config.Config, ndkRoot string, targets []config.Platform) error {
platformsJson, err := os.Open(filepath.Join(ndkRoot, "meta", "platforms.json"))
if err != nil {
return err
}
defer platformsJson.Close()
decoder := json.NewDecoder(platformsJson)
supportedVersions := struct {
Min int
Max int
}{}
if err := decoder.Decode(&supportedVersions); err != nil {
return err
}
if supportedVersions.Min > c.Build.AndroidMinSDK ||
supportedVersions.Max < c.Build.AndroidMinSDK {
return fmt.Errorf("unsupported API version %d (not in %d..%d)", c.Build.AndroidMinSDK, supportedVersions.Min, supportedVersions.Max)
}
abisJson, err := os.Open(filepath.Join(ndkRoot, "meta", "abis.json"))
if err != nil {
return err
}
defer abisJson.Close()
decoder = json.NewDecoder(abisJson)
abis := make(map[string]struct{})
if err := decoder.Decode(&abis); err != nil {
return err
}
for _, target := range targets {
if !isAndroidPlatform(target.OS) {
continue
}
if _, found := abis[abi(target.Arch)]; !found {
return fmt.Errorf("ndk does not support %s", target.OS)
}
}
return nil
}
// compatibleNDKRoots searches the side-by-side NDK dirs for compatible SDKs.
func compatibleNDKRoots(c *config.Config, ndkForest string, targets []config.Platform) ([]string, error) {
ndkDirs, err := os.ReadDir(ndkForest)
if err != nil {
return nil, err
}
compatibleNDKRoots := []string{}
var lastErr error
for _, dirent := range ndkDirs {
ndkRoot := filepath.Join(ndkForest, dirent.Name())
lastErr = checkNDKRoot(c, ndkRoot, targets)
if lastErr == nil {
compatibleNDKRoots = append(compatibleNDKRoots, ndkRoot)
}
}
if len(compatibleNDKRoots) > 0 {
return compatibleNDKRoots, nil
}
return nil, lastErr
}
// ndkVersion returns the full version number of an installed copy of the NDK,
// or "" if it cannot be determined.
func ndkVersion(ndkRoot string) string {
properties, err := os.Open(filepath.Join(ndkRoot, "source.properties"))
if err != nil {
return ""
}
defer properties.Close()
// Parse the version number out of the .properties file.
// See https://en.wikipedia.org/wiki/.properties
scanner := bufio.NewScanner(properties)
for scanner.Scan() {
line := scanner.Text()
tokens := strings.SplitN(line, "=", 2)
if len(tokens) != 2 {
continue
}
if strings.TrimSpace(tokens[0]) == "Pkg.Revision" {
return strings.TrimSpace(tokens[1])
}
}
return ""
}
// ndkRoot returns the root path of an installed NDK that supports all the
// specified Android targets. For details of NDK locations, see
// https://github.com/android/ndk-samples/wiki/Configure-NDK-Path
func ndkRoot(c *config.Config, targets ...config.Platform) (string, error) {
// Try the ANDROID_NDK_HOME variable. This approach is deprecated, but it
// has the highest priority because it represents an explicit user choice.
if ndkRoot := os.Getenv("ANDROID_NDK_HOME"); ndkRoot != "" {
if err := checkNDKRoot(c, ndkRoot, targets); err != nil {
return "", fmt.Errorf("ANDROID_NDK_HOME specifies %s, which is unusable: %w", ndkRoot, err)
}
return ndkRoot, nil
}
androidHome, err := sdkpath.AndroidHome()
if err != nil {
return "", fmt.Errorf("could not locate Android SDK: %w", err)
}
// Use the newest compatible NDK under the side-by-side path arrangement.
ndkForest := filepath.Join(androidHome, "ndk")
ndkRoots, sideBySideErr := compatibleNDKRoots(c, ndkForest, targets)
if len(ndkRoots) != 0 {
// Choose the latest version that supports the build configuration.
// NDKs whose version cannot be determined will be least preferred.
// In the event of a tie, the later ndkRoot will win.
maxVersion := ""
var selected string
for _, ndkRoot := range ndkRoots {
version := ndkVersion(ndkRoot)
if version >= maxVersion {
maxVersion = version
selected = ndkRoot
}
}
return selected, nil
}
// Try the deprecated NDK location.
ndkRoot := filepath.Join(androidHome, "ndk-bundle")
if legacyErr := checkNDKRoot(c, ndkRoot, targets); legacyErr != nil {
return "", fmt.Errorf("no usable NDK in %s: %w, %v", androidHome, sideBySideErr, legacyErr)
}
return ndkRoot, nil
}
func envClang(sdkName string) (clang, cflags string, err error) {
out, err := exec.Minor().Output("xcrun", "--sdk", sdkName, "--find", "clang")
if err != nil {
return "", "", fmt.Errorf("xcrun --find: %v\n%s", err, out)
}
clang = strings.TrimSpace(string(out))
out, err = exec.Minor().Output("xcrun", "--sdk", sdkName, "--show-sdk-path")
if err != nil {
return "", "", fmt.Errorf("xcrun --show-sdk-path: %v\n%s", err, out)
}
sdk := strings.TrimSpace(string(out))
return clang, "-isysroot " + sdk, nil
}
func archClang(goarch string) string {
switch goarch {
case "arm":
return "armv7"
case "arm64":
return "arm64"
case "386":
return "i386"
case "amd64":
return "x86_64"
default:
panic(fmt.Sprintf("unknown GOARCH: %q", goarch))
}
}
func archNDK() string {
if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
return "windows"
} else {
var arch string
switch runtime.GOARCH {
case "386":
arch = "x86"
case "amd64":
arch = "x86_64"
case "arm64":
// Android NDK does not contain arm64 toolchains (until and
// including NDK 23), use use x86_64 instead. See:
// https://github.com/android/ndk/issues/1299
if runtime.GOOS == "darwin" {
arch = "x86_64"
break
}
if runtime.GOOS == "android" { // termux
return "linux-aarch64"
}
fallthrough
default:
panic("unsupported GOARCH: " + runtime.GOARCH)
}
return runtime.GOOS + "-" + arch
}
}
type ndkToolchain struct {
Arch string
ABI string
MinAPI int
ToolPrefix string
ClangPrefixVal string // ClangPrefix is taken by a method
}
func (tc *ndkToolchain) clangPrefix(c *config.Config) string {
if c.Build.AndroidMinSDK < tc.MinAPI {
return fmt.Sprintf("%s%d", tc.ClangPrefixVal, tc.MinAPI)
}
return fmt.Sprintf("%s%d", tc.ClangPrefixVal, c.Build.AndroidMinSDK)
}
func (tc *ndkToolchain) path(c *config.Config, ndkRoot, toolName string) string {
cmdFromPref := func(pref string) string {
return filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", archNDK(), "bin", pref+"-"+toolName)
}
var cmd string
switch toolName {
case "clang", "clang++":
cmd = cmdFromPref(tc.clangPrefix(c))
default:
cmd = cmdFromPref(tc.ToolPrefix)
// Starting from NDK 23, GNU binutils are fully migrated to LLVM binutils.
// See https://android.googlesource.com/platform/ndk/+/master/docs/Roadmap.md#ndk-r23
if _, err := os.Stat(cmd); errors.Is(err, fs.ErrNotExist) {
cmd = cmdFromPref("llvm")
}
}
return cmd
}
type ndkConfig map[string]ndkToolchain // map: GOOS->androidConfig.
func (nc ndkConfig) toolchain(arch string) ndkToolchain {
tc, ok := nc[arch]
if !ok {
panic(`unsupported architecture: ` + arch)
}
return tc
}
var ndk = ndkConfig{
"arm": {
Arch: "arm",
ABI: "armeabi-v7a",
MinAPI: 16,
ToolPrefix: "arm-linux-androideabi",
ClangPrefixVal: "armv7a-linux-androideabi",
},
"arm64": {
Arch: "arm64",
ABI: "arm64-v8a",
MinAPI: 21,
ToolPrefix: "aarch64-linux-android",
ClangPrefixVal: "aarch64-linux-android",
},
"386": {
Arch: "x86",
ABI: "x86",
MinAPI: 16,
ToolPrefix: "i686-linux-android",
ClangPrefixVal: "i686-linux-android",
},
"amd64": {
Arch: "x86_64",
ABI: "x86_64",
MinAPI: 21,
ToolPrefix: "x86_64-linux-android",
ClangPrefixVal: "x86_64-linux-android",
},
}
func xCodeAvailable() bool {
err := exec.Run("xcrun", "xcodebuild", "-version")
return err == nil
}
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mobile
import (
"fmt"
"path/filepath"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/cmd/core/config"
)
// Install installs the app named by the import path on the attached mobile device.
// It assumes that it has already been built.
//
// On Android, the 'adb' tool must be on the PATH.
func Install(c *config.Config) error {
if len(c.Build.Target) != 1 {
return fmt.Errorf("expected 1 target platform, but got %d (%v)", len(c.Build.Target), c.Build.Target)
}
t := c.Build.Target[0]
switch t.OS {
case "android":
return exec.Run("adb", "install", "-r", filepath.Join(c.Build.Output, c.Name+".apk"))
case "ios":
return exec.Major().SetBuffer(false).Run("ios-deploy", "-b", filepath.Join(c.Build.Output, c.Name+".app"))
default:
return fmt.Errorf("mobile.Install only supports target platforms android and ios, but got %q", t.OS)
}
}
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mobile
import (
"encoding/xml"
"errors"
"fmt"
"html/template"
)
type manifestXML struct {
Activity activityXML `xml:"application>activity"`
}
type activityXML struct {
Name string `xml:"name,attr"`
MetaData []metaDataXML `xml:"meta-data"`
}
type metaDataXML struct {
Name string `xml:"name,attr"`
Value string `xml:"value,attr"`
}
// manifestLibName parses the AndroidManifest.xml and finds the library
// name of the NativeActivity.
func manifestLibName(data []byte) (string, error) {
manifest := new(manifestXML)
if err := xml.Unmarshal(data, manifest); err != nil {
return "", err
}
if manifest.Activity.Name != "org.golang.app.GoNativeActivity" {
return "", fmt.Errorf("can only build an .apk for GoNativeActivity, not %q", manifest.Activity.Name)
}
libName := ""
for _, md := range manifest.Activity.MetaData {
if md.Name == "android.app.lib_name" {
libName = md.Value
break
}
}
if libName == "" {
return "", errors.New("AndroidManifest.xml missing meta-data android.app.lib_name")
}
return libName, nil
}
type manifestTmplData struct {
JavaPkgPath string
Name string
LibName string
}
var manifestTmpl = template.Must(template.New("manifest").Parse(`
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="{{.JavaPkgPath}}"
android:versionCode="1"
android:versionName="1.0">
<application android:label="{{.Name}}" android:debuggable="true">
<activity android:name="org.golang.app.GoNativeActivity"
android:label="{{.Name}}"
android:configChanges="orientation|screenSize|keyboardHidden">
<meta-data android:name="android.app.lib_name" android:value="{{.LibName}}" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>`))
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package sdkpath provides functions for locating the Android SDK.
// These functions respect the ANDROID_HOME environment variable, and
// otherwise use the default SDK location.
package sdkpath
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
)
// AndroidHome returns the absolute path of the selected Android SDK,
// if one can be found.
func AndroidHome() (string, error) {
androidHome := os.Getenv("ANDROID_HOME")
if androidHome == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
switch runtime.GOOS {
case "windows":
// See https://android.googlesource.com/platform/tools/adt/idea/+/85b4bfb7a10ad858a30ffa4003085b54f9424087/native/installer/win/setup_android_studio.nsi#100
androidHome = filepath.Join(home, "AppData", "Local", "Android", "sdk")
case "darwin":
// See https://android.googlesource.com/platform/tools/asuite/+/67e0cd9604379e9663df57f16a318d76423c0aa8/aidegen/lib/ide_util.py#88
androidHome = filepath.Join(home, "Library", "Android", "sdk")
default: // Linux, BSDs, etc.
// See LINUX_ANDROID_SDK_PATH in ide_util.py above.
androidHome = filepath.Join(home, "Android", "Sdk")
}
}
if info, err := os.Stat(androidHome); err != nil {
return "", fmt.Errorf("%w; Android SDK was not found at %s", err, androidHome)
} else if !info.IsDir() {
return "", fmt.Errorf("%s is not a directory", androidHome)
}
return androidHome, nil
}
// AndroidAPIPath returns an android SDK platform directory within the configured SDK.
// If there are multiple platforms that satisfy the minimum version requirement,
// AndroidAPIPath returns the latest one among them.
func AndroidAPIPath(api int) (string, error) {
sdk, err := AndroidHome()
if err != nil {
return "", err
}
sdkDir, err := os.Open(filepath.Join(sdk, "platforms"))
if err != nil {
return "", fmt.Errorf("failed to find android SDK platform: %w", err)
}
defer sdkDir.Close()
fis, err := sdkDir.Readdir(-1)
if err != nil {
return "", fmt.Errorf("failed to find android SDK platform (API level: %d): %w", api, err)
}
var apiPath string
var apiVer int
for _, fi := range fis {
name := fi.Name()
if !strings.HasPrefix(name, "android-") {
continue
}
n, err := strconv.Atoi(name[len("android-"):])
if err != nil || n < api {
continue
}
p := filepath.Join(sdkDir.Name(), name)
_, err = os.Stat(filepath.Join(p, "android.jar"))
if err == nil && apiVer < n {
apiPath = p
apiVer = n
}
}
if apiVer == 0 {
return "", fmt.Errorf("failed to find android SDK platform (API level: %d) in %s",
api, sdkDir.Name())
}
return apiPath, nil
}
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mobile
import (
"os"
"runtime"
"strings"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/cmd/core/config"
"golang.org/x/tools/go/packages"
)
var (
goos = runtime.GOOS
goarch = runtime.GOARCH
)
func packagesConfig(t *config.Platform) *packages.Config {
config := &packages.Config{}
// Add CGO_ENABLED=1 explicitly since Cgo is disabled when GOOS is different from host OS.
config.Env = append(os.Environ(), "GOARCH="+t.Arch, "GOOS="+platformOS(t.OS), "CGO_ENABLED=1")
tags := platformTags(t.OS)
if len(tags) > 0 {
config.BuildFlags = []string{"-tags=" + strings.Join(tags, ",")}
}
return config
}
func goEnv(name string) string {
if val := os.Getenv(name); val != "" {
return val
}
val, err := exec.Minor().Output("go", "env", name)
if err != nil {
panic(err) // the Go tool was tested to work earlier
}
return strings.TrimSpace(string(val))
}
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mobile
// TODO: do we need this writer stuff?
// APK is the archival format used for Android apps. It is a ZIP archive with
// three extra files:
//
// META-INF/MANIFEST.MF
// META-INF/CERT.SF
// META-INF/CERT.RSA
//
// The MANIFEST.MF comes from the Java JAR archive format. It is a list of
// files included in the archive along with a SHA1 hash, for example:
//
// Name: lib/armeabi/libbasic.so
// SHA1-Digest: ntLSc1eLCS2Tq1oB4Vw6jvkranw=
//
// For debugging, the equivalent SHA1-Digest can be generated with OpenSSL:
//
// cat lib/armeabi/libbasic.so | openssl sha1 -binary | openssl base64
//
// CERT.SF is a similar manifest. It begins with a SHA1 digest of the entire
// manifest file:
//
// Signature-Version: 1.0
// Created-By: 1.0 (Android)
// SHA1-Digest-Manifest: aJw+u+10C3Enbg8XRCN6jepluYA=
//
// Then for each entry in the manifest it has a SHA1 digest of the manfiest's
// hash combined with the file name:
//
// Name: lib/armeabi/libbasic.so
// SHA1-Digest: Q7NAS6uzrJr6WjePXSGT+vvmdiw=
//
// This can also be generated with openssl:
//
// echo -en "Name: lib/armeabi/libbasic.so\r\nSHA1-Digest: ntLSc1eLCS2Tq1oB4Vw6jvkranw=\r\n\r\n" | openssl sha1 -binary | openssl base64
//
// Note the \r\n line breaks.
//
// CERT.RSA is an RSA signature block made of CERT.SF. Verify it with:
//
// openssl smime -verify -in CERT.RSA -inform DER -content CERT.SF cert.pem
//
// The APK format imposes two extra restrictions on the ZIP format. First,
// it is uncompressed. Second, each contained file is 4-byte aligned. This
// allows the Android OS to mmap contents without unpacking the archive.
// Note: to make life a little harder, Android Studio stores the RSA key used
// for signing in an Oracle Java proprietary keystore format, JKS. For example,
// the generated debug key is in ~/.android/debug.keystore, and can be
// extracted using the JDK's keytool utility:
//
// keytool -importkeystore -srckeystore ~/.android/debug.keystore -destkeystore ~/.android/debug.p12 -deststoretype PKCS12
//
// Once in standard PKCS12, the key can be converted to PEM for use in the
// Go crypto packages:
//
// openssl pkcs12 -in ~/.android/debug.p12 -nocerts -nodes -out ~/.android/debug.pem
//
// Fortunately for debug builds, all that matters is that the APK is signed.
// The choice of key is unimportant, so we can generate one for normal builds.
// For production builds, we can ask users to provide a PEM file.
import (
"archive/zip"
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"encoding/base64"
"fmt"
"hash"
"io"
)
// newWriter returns a new Writer writing an APK file to w.
// The APK will be signed with key.
func newWriter(w io.Writer, priv *rsa.PrivateKey) *writer {
apkw := &writer{priv: priv}
apkw.w = zip.NewWriter(&countWriter{apkw: apkw, w: w})
return apkw
}
// writer implements an APK file writer.
type writer struct {
offset int
w *zip.Writer
priv *rsa.PrivateKey
manifest []manifestEntry
cur *fileWriter
}
// Create adds a file to the APK archive using the provided name.
//
// The name must be a relative path. The file's contents must be written to
// the returned io.Writer before the next call to Create or Close.
func (w *writer) Create(name string) (io.Writer, error) {
if err := w.clearCur(); err != nil {
return nil, fmt.Errorf("apk: Create(%s): %v", name, err)
}
res, err := w.create(name)
if err != nil {
return nil, fmt.Errorf("apk: Create(%s): %v", name, err)
}
return res, nil
}
func (w *writer) create(name string) (io.Writer, error) {
// Align start of file contents by using Extra as padding.
if err := w.w.Flush(); err != nil { // for exact offset
return nil, err
}
const fileHeaderLen = 30 // + filename + extra
start := w.offset + fileHeaderLen + len(name)
extra := (-start) & 3
zipfw, err := w.w.CreateHeader(&zip.FileHeader{
Name: name,
Extra: make([]byte, extra),
})
if err != nil {
return nil, err
}
w.cur = &fileWriter{
name: name,
w: zipfw,
sha1: sha1.New(),
}
return w.cur, nil
}
// Close finishes writing the APK. This includes writing the manifest and
// signing the archive, and writing the ZIP central directory.
//
// It does not close the underlying writer.
func (w *writer) Close() error {
if err := w.clearCur(); err != nil {
return fmt.Errorf("apk: %v", err)
}
hasDex := false
for _, entry := range w.manifest {
if entry.name == "classes.dex" {
hasDex = true
break
}
}
manifest := new(bytes.Buffer)
if hasDex {
fmt.Fprint(manifest, manifestDexHeader)
} else {
fmt.Fprint(manifest, manifestHeader)
}
certBody := new(bytes.Buffer)
for _, entry := range w.manifest {
n := entry.name
h := base64.StdEncoding.EncodeToString(entry.sha1.Sum(nil))
fmt.Fprintf(manifest, "Name: %s\nSHA1-Digest: %s\n\n", n, h)
cHash := sha1.New()
fmt.Fprintf(cHash, "Name: %s\r\nSHA1-Digest: %s\r\n\r\n", n, h)
ch := base64.StdEncoding.EncodeToString(cHash.Sum(nil))
fmt.Fprintf(certBody, "Name: %s\nSHA1-Digest: %s\n\n", n, ch)
}
mHash := sha1.New()
mHash.Write(manifest.Bytes())
cert := new(bytes.Buffer)
fmt.Fprint(cert, certHeader)
fmt.Fprintf(cert, "SHA1-Digest-Manifest: %s\n\n", base64.StdEncoding.EncodeToString(mHash.Sum(nil)))
cert.Write(certBody.Bytes())
mw, err := w.Create("META-INF/MANIFEST.MF")
if err != nil {
return err
}
if _, err := mw.Write(manifest.Bytes()); err != nil {
return err
}
cw, err := w.Create("META-INF/CERT.SF")
if err != nil {
return err
}
if _, err := cw.Write(cert.Bytes()); err != nil {
return err
}
rsa, err := signPKCS7(rand.Reader, w.priv, cert.Bytes())
if err != nil {
return fmt.Errorf("apk: %v", err)
}
rw, err := w.Create("META-INF/CERT.RSA")
if err != nil {
return err
}
if _, err := rw.Write(rsa); err != nil {
return err
}
return w.w.Close()
}
const manifestHeader = `Manifest-Version: 1.0
Created-By: 1.0 (Go)
`
const manifestDexHeader = `Manifest-Version: 1.0
Dex-Location: classes.dex
Created-By: 1.0 (Go)
`
const certHeader = `Signature-Version: 1.0
Created-By: 1.0 (Go)
`
func (w *writer) clearCur() error {
if w.cur == nil {
return nil
}
w.manifest = append(w.manifest, manifestEntry{
name: w.cur.name,
sha1: w.cur.sha1,
})
w.cur.closed = true
w.cur = nil
return nil
}
type manifestEntry struct {
name string
sha1 hash.Hash
}
type countWriter struct {
apkw *writer
w io.Writer
}
func (c *countWriter) Write(p []byte) (n int, err error) {
n, err = c.w.Write(p)
c.apkw.offset += n
return n, err
}
type fileWriter struct {
name string
w io.Writer
sha1 hash.Hash
closed bool
}
func (w *fileWriter) Write(p []byte) (n int, err error) {
if w.closed {
return 0, fmt.Errorf("apk: write to closed file %q", w.name)
}
w.sha1.Write(p)
n, err = w.w.Write(p)
if err != nil {
err = fmt.Errorf("apk: %v", err)
}
return n, err
}
// 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 rendericon
import (
"errors"
"fmt"
"image"
"io/fs"
"os"
"strings"
"cogentcore.org/core/icons"
"cogentcore.org/core/paint"
"cogentcore.org/core/svg"
)
// Render renders the icon located at icon.svg at the given size.
// If no such icon exists, it sets it to a placeholder icon, [icons.DefaultAppIcon].
func Render(size int) (*image.RGBA, error) {
paint.FontLibrary.InitFontPaths(paint.FontPaths...)
sv := svg.NewSVG(size, size)
spath := "icon.svg"
err := sv.OpenXML(spath)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return nil, fmt.Errorf("error opening svg icon file: %w", err)
}
err = os.WriteFile(spath, []byte(icons.CogentCore), 0666)
if err != nil {
return nil, err
}
err = sv.ReadXML(strings.NewReader(string(icons.CogentCore)))
if err != nil {
return nil, err
}
}
sv.Render()
return sv.Pixels, nil
}
// 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 web provides functions for building Cogent Core apps for the web.
package web
import (
"crypto/sha1"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/base/iox/jsonx"
"cogentcore.org/core/cmd/core/config"
"cogentcore.org/core/cmd/core/rendericon"
"cogentcore.org/core/pages/ppath"
strip "github.com/grokify/html-strip-tags-go"
)
// Build builds an app for web using the given configuration information.
func Build(c *config.Config) error {
output := filepath.Join(c.Build.Output, "app.wasm")
opath := output
if c.Web.Gzip {
opath += ".orig"
}
args := []string{"build", "-o", opath, "-ldflags", config.LinkerFlags(c)}
if c.Build.Trimpath {
args = append(args, "-trimpath")
}
err := exec.Major().SetEnv("GOOS", "js").SetEnv("GOARCH", "wasm").Run("go", args...)
if err != nil {
return err
}
if c.Web.Gzip {
err = exec.RemoveAll(output + ".orig.gz")
if err != nil {
return err
}
err = exec.Run("gzip", output+".orig")
if err != nil {
return err
}
err = os.Rename(output+".orig.gz", output)
if err != nil {
return err
}
}
return makeFiles(c)
}
// makeFiles makes the necessary static web files based on the given configuration information.
func makeFiles(c *config.Config) error {
odir := c.Build.Output
if c.Web.RandomVersion {
t := time.Now().UTC().String()
c.Version = fmt.Sprintf(`%x`, sha1.Sum([]byte(t)))
}
// The about text may contain HTML, which we need to get rid of.
// It is trusted, so we do not need a more advanced sanitizer.
c.About = strip.StripTags(c.About)
wej := []byte(wasmExecJS)
err := os.WriteFile(filepath.Join(odir, "wasm_exec.js"), wej, 0666)
if err != nil {
return err
}
ajs, err := makeAppJS(c)
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(odir, "app.js"), ajs, 0666)
if err != nil {
return err
}
awjs, err := makeAppWorkerJS(c)
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(odir, "app-worker.js"), awjs, 0666)
if err != nil {
return err
}
man, err := makeManifestJSON(c)
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(odir, "manifest.webmanifest"), man, 0666)
if err != nil {
return err
}
acs := []byte(appCSS)
err = os.WriteFile(filepath.Join(odir, "app.css"), acs, 0666)
if err != nil {
return err
}
preRenderHTML := ""
if c.Web.GenerateHTML {
preRenderHTML, err = exec.Output("go", "run", "-tags", "offscreen,generatehtml", ".")
if err != nil {
return err
}
}
preRenderHTMLIndex := preRenderHTML
preRenderDescriptionIndex := ""
pagesPreRenderData := &ppath.PreRenderData{}
if strings.HasPrefix(preRenderHTML, "{") {
err := jsonx.Read(pagesPreRenderData, strings.NewReader(preRenderHTML))
if err != nil {
return err
}
preRenderHTMLIndex = pagesPreRenderData.HTML[""]
if c.About == "" {
preRenderDescriptionIndex = pagesPreRenderData.Description[""]
}
if c.Pages == "" {
c.Pages = "content"
}
}
iht, err := makeIndexHTML(c, "", "", preRenderDescriptionIndex, preRenderHTMLIndex)
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(odir, "index.html"), iht, 0666)
if err != nil {
return err
}
if c.Pages != "" {
err := makePages(c, pagesPreRenderData)
if err != nil {
return err
}
}
err = os.MkdirAll(filepath.Join(odir, "icons"), 0777)
if err != nil {
return err
}
sizes := []int{32, 192, 512}
for _, size := range sizes {
ic, err := rendericon.Render(size)
if err != nil {
return err
}
err = imagex.Save(ic, filepath.Join(odir, "icons", strconv.Itoa(size)+".png"))
if err != nil {
return err
}
}
err = exec.Run("cp", "icon.svg", filepath.Join(odir, "icons", "svg.svg"))
if err != nil {
return err
}
return nil
}
// makePages makes a directory structure of pages for
// the core pages located at [config.Config.Pages].
func makePages(c *config.Config, preRenderData *ppath.PreRenderData) error {
return filepath.WalkDir(c.Pages, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if filepath.Ext(path) != ".md" {
return nil
}
path = strings.ReplaceAll(path, `\`, "/")
path = strings.TrimSuffix(path, "index.md")
path = strings.TrimSuffix(path, ".md")
path = strings.TrimPrefix(path, c.Pages)
path = strings.TrimPrefix(path, "/")
path = strings.TrimSuffix(path, "/")
if ppath.Draft(path) {
return nil
}
path = ppath.Format(path)
if path == "" { // exclude root index
return nil
}
opath := filepath.Join(c.Build.Output, path)
err = os.MkdirAll(opath, 0777)
if err != nil {
return err
}
title := ppath.Label(path, c.Name)
if title != c.Name {
title += " • " + c.Name
}
b, err := makeIndexHTML(c, ppath.BasePath(path), title, preRenderData.Description[path], preRenderData.HTML[path])
if err != nil {
return err
}
return os.WriteFile(filepath.Join(opath, "index.html"), b, 0666)
})
}
// 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 web
import (
"net/http"
"cogentcore.org/core/base/logx"
"cogentcore.org/core/cmd/core/config"
)
// Serve serves the build output directory on the default network address at the config port.
func Serve(c *config.Config) error {
fs := http.FileServer(http.Dir(c.Build.Output))
http.Handle("/", fs)
http.HandleFunc("/app.wasm", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/wasm")
if c.Web.Gzip {
w.Header().Set("Content-Encoding", "gzip")
}
fs.ServeHTTP(w, r)
})
logx.PrintlnWarn("Serving at http://localhost:" + c.Web.Port)
return http.ListenAndServe(":"+c.Web.Port, nil)
}
// 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 web
import (
"bytes"
"encoding/json"
"log/slog"
"os"
"text/template"
"cogentcore.org/core/base/elide"
"cogentcore.org/core/cmd/core/config"
strip "github.com/grokify/html-strip-tags-go"
)
// appJSTmpl is the template used in [makeAppJS] to build the app.js file
var appJSTmpl = template.Must(template.New("app.js").Parse(appJS))
// appJSData is the data passed to [appJSTmpl]
type appJSData struct {
Env string
WasmContentLengthHeader string
AutoUpdateInterval int64
}
// makeAppJS exectues [appJSTmpl] based on the given configuration information.
func makeAppJS(c *config.Config) ([]byte, error) {
if c.Web.Env == nil {
c.Web.Env = make(map[string]string)
}
c.Web.Env["GOAPP_STATIC_RESOURCES_URL"] = "/"
c.Web.Env["GOAPP_ROOT_PREFIX"] = "."
for k, v := range c.Web.Env {
if err := os.Setenv(k, v); err != nil {
slog.Error("setting app env variable failed", "name", k, "value", v, "err", err)
}
}
wenv, err := json.Marshal(c.Web.Env)
if err != nil {
return nil, err
}
d := appJSData{
Env: string(wenv),
WasmContentLengthHeader: c.Web.WasmContentLengthHeader,
AutoUpdateInterval: c.Web.AutoUpdateInterval.Milliseconds(),
}
b := &bytes.Buffer{}
err = appJSTmpl.Execute(b, d)
if err != nil {
return nil, err
}
return b.Bytes(), nil
}
// appWorkerJSData is the data passed to [config.Config.Web.ServiceWorkerTemplate]
type appWorkerJSData struct {
Version string
ResourcesToCache string
}
// makeAppWorkerJS executes [config.Config.Web.ServiceWorkerTemplate]. If it empty, it
// sets it to [appWorkerJS].
func makeAppWorkerJS(c *config.Config) ([]byte, error) {
resources := []string{
"app.css",
"app.js",
"app.wasm",
"manifest.webmanifest",
"wasm_exec.js",
"index.html",
}
tmpl, err := template.New("app-worker.js").Parse(appWorkerJS)
if err != nil {
return nil, err
}
rstr, err := json.Marshal(resources)
if err != nil {
return nil, err
}
d := appWorkerJSData{
Version: c.Version,
ResourcesToCache: string(rstr),
}
b := &bytes.Buffer{}
err = tmpl.Execute(b, d)
if err != nil {
return nil, err
}
return b.Bytes(), nil
}
// manifestJSONTmpl is the template used in [makeManifestJSON] to build the mainfest.webmanifest file
var manifestJSONTmpl = template.Must(template.New("manifest.webmanifest").Parse(manifestJSON))
// manifestJSONData is the data passed to [manifestJSONTmpl]
type manifestJSONData struct {
ShortName string
Name string
Description string
}
// makeManifestJSON exectues [manifestJSONTmpl] based on the given configuration information.
func makeManifestJSON(c *config.Config) ([]byte, error) {
d := manifestJSONData{
ShortName: elide.AppName(c.Name),
Name: c.Name,
Description: c.About,
}
b := &bytes.Buffer{}
err := manifestJSONTmpl.Execute(b, d)
if err != nil {
return nil, err
}
return b.Bytes(), nil
}
// indexHTMLTmpl is the template used in [makeIndexHTML] to build the index.html file
var indexHTMLTmpl = template.Must(template.New("index.html").Parse(indexHTML))
// indexHTMLData is the data passed to [indexHTMLTmpl]
type indexHTMLData struct {
BasePath string
Author string
Description string
Keywords []string
Title string
SiteName string
Image string
VanityURL string
GithubVanityRepository string
PreRenderHTML string
}
// makeIndexHTML exectues [indexHTMLTmpl] based on the given configuration information,
// base path for app resources (used in [makePages]), optional title (used in [makePages],
// defaults to [config.Config.Name] otherwise), optional page-specific description (used
// in [makePages], defaults to [config.Config.About]), and pre-render HTML representation
// of app content.
func makeIndexHTML(c *config.Config, basePath, title, description, preRenderHTML string) ([]byte, error) {
if title == "" {
title = c.Name
}
if description == "" {
description = c.About
} else {
// c.About is already stripped earlier, so only necessary
// for page-specific description here.
description = strip.StripTags(description)
}
d := indexHTMLData{
BasePath: basePath,
Author: c.Web.Author,
Description: description,
Keywords: c.Web.Keywords,
Title: title,
SiteName: c.Name,
Image: c.Web.Image,
VanityURL: c.Web.VanityURL,
GithubVanityRepository: c.Web.GithubVanityRepository,
PreRenderHTML: preRenderHTML,
}
b := &bytes.Buffer{}
err := indexHTMLTmpl.Execute(b, d)
if err != nil {
return nil, err
}
return b.Bytes(), nil
}
// Copyright (c) 2024, 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 colors
import (
"image/color"
"cogentcore.org/core/colors/cam/hct"
"cogentcore.org/core/colors/matcolor"
)
// Based on matcolor/accent.go
// ToBase returns the base accent color for the given color
// based on the current scheme (light or dark), which is
// typically used for high emphasis objects or text.
func ToBase(c color.Color) color.RGBA {
if matcolor.SchemeIsDark {
return hct.FromColor(c).WithTone(80).AsRGBA()
}
return hct.FromColor(c).WithTone(40).AsRGBA()
}
// ToOn returns the accent color for the given color
// that should be placed on top of [ToBase] based on
// the current scheme (light or dark).
func ToOn(c color.Color) color.RGBA {
if matcolor.SchemeIsDark {
return hct.FromColor(c).WithTone(20).AsRGBA()
}
return hct.FromColor(c).WithTone(100).AsRGBA()
}
// ToContainer returns the container accent color for the given color
// based on the current scheme (light or dark), which is
// typically used for lower emphasis content.
func ToContainer(c color.Color) color.RGBA {
if matcolor.SchemeIsDark {
return hct.FromColor(c).WithTone(30).AsRGBA()
}
return hct.FromColor(c).WithTone(90).AsRGBA()
}
// ToOnContainer returns the accent color for the given color
// that should be placed on top of [ToContainer] based on
// the current scheme (light or dark).
func ToOnContainer(c color.Color) color.RGBA {
if matcolor.SchemeIsDark {
return hct.FromColor(c).WithTone(90).AsRGBA()
}
return hct.FromColor(c).WithTone(10).AsRGBA()
}
// 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 colors
import (
"image/color"
"log/slog"
"cogentcore.org/core/colors/cam/cam16"
"cogentcore.org/core/colors/cam/hct"
"cogentcore.org/core/math32"
)
// BlendTypes are different algorithms (colorspaces) to use for blending
// the color stop values in generating the gradients.
type BlendTypes int32 //enums:enum
const (
// HCT uses the hue, chroma, and tone space and generally produces the best results,
// but at a slight performance cost.
HCT BlendTypes = iota
// RGB uses raw RGB space, which is the standard space that most other programs use.
// It produces decent results with maximum performance.
RGB
// CAM16 is an alternative colorspace, similar to HCT, but not quite as good.
CAM16
)
// Blend returns a color that is the given proportion between the first
// and second color. For example, 0.1 indicates to blend 10% of the first
// color and 90% of the second. Blending is done using the given blending
// algorithm.
func Blend(bt BlendTypes, p float32, x, y color.Color) color.RGBA {
switch bt {
case HCT:
return hct.Blend(p, x, y)
case RGB:
return BlendRGB(p, x, y)
case CAM16:
return cam16.Blend(p, x, y)
}
slog.Error("got unexpected blend type", "type", bt)
return color.RGBA{}
}
// BlendRGB returns a color that is the given proportion between the first
// and second color in RGB colorspace. For example, 0.1 indicates to blend
// 10% of the first color and 90% of the second. Blending is done directly
// on non-premultiplied
// RGB values, and a correctly premultiplied color is returned.
func BlendRGB(pct float32, x, y color.Color) color.RGBA {
fx := NRGBAF32Model.Convert(x).(NRGBAF32)
fy := NRGBAF32Model.Convert(y).(NRGBAF32)
pct = math32.Clamp(pct, 0, 100.0)
px := pct / 100
py := 1.0 - px
fx.R = px*fx.R + py*fy.R
fx.G = px*fx.G + py*fy.G
fx.B = px*fx.B + py*fy.B
fx.A = px*fx.A + py*fy.A
return AsRGBA(fx)
}
// m is the maximum color value returned by [image.Color.RGBA]
const m = 1<<16 - 1
// AlphaBlend blends the two colors, handling alpha blending correctly.
// The source color is figuratively placed "on top of" the destination color.
func AlphaBlend(dst, src color.Color) color.RGBA {
res := color.RGBA{}
dr, dg, db, da := dst.RGBA()
sr, sg, sb, sa := src.RGBA()
a := (m - sa)
res.R = uint8((uint32(dr)*a/m + sr) >> 8)
res.G = uint8((uint32(dg)*a/m + sg) >> 8)
res.B = uint8((uint32(db)*a/m + sb) >> 8)
res.A = uint8((uint32(da)*a/m + sa) >> 8)
return res
}
// Copyright (c) 2021, 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 cam02
import (
"cogentcore.org/core/colors/cam/cie"
"cogentcore.org/core/math32"
)
// XYZToLMS converts XYZ to Long, Medium, Short cone-based responses,
// using the CAT02 transform from CIECAM02 color appearance model
// (MoroneyFairchildHuntEtAl02)
func XYZToLMS(x, y, z float32) (l, m, s float32) {
l = 0.7328*x + 0.4296*y + -0.1624*z
m = -0.7036*x + 1.6975*y + 0.0061*z
s = 0.0030*x + 0.0136*y + 0.9834*z
return
}
// SRGBLinToLMS converts sRGB linear to Long, Medium, Short
// cone-based responses, using the CAT02 transform from CIECAM02
// color appearance model (MoroneyFairchildHuntEtAl02)
// this is good for representing adaptation but NOT apparently
// good for representing appearances
func SRGBLinToLMS(rl, gl, bl float32) (l, m, s float32) {
l = 0.3904054*rl + 0.54994122*gl + 0.00892632*bl
m = 0.0708416*rl + 0.96317176*gl + 0.00135775*bl
s = 0.0491304*rl + 0.21556128*gl + 0.9450824*bl
return
}
// SRGBToLMS converts sRGB to Long, Medium, Short cone-based responses,
// using the CAT02 transform from CIECAM02 color appearance model
// (MoroneyFairchildHuntEtAl02)
func SRGBToLMS(r, g, b float32) (l, m, s float32) {
rl, gl, bl := cie.SRGBToLinear(r, g, b)
l, m, s = SRGBLinToLMS(rl, gl, bl)
return
}
/*
// convert Long, Medium, Short cone-based responses to XYZ, using the CAT02 transform from CIECAM02 color appearance model (MoroneyFairchildHuntEtAl02)
func LMSToXYZ(l, m, s float32) (x, y, z float32) {
x = 1.096124 * l + 0.4296f * Y + -0.1624f * Z;
y = -0.7036f * X + 1.6975f * Y + 0.0061f * Z;
z = 0.0030f * X + 0.0136f * Y + 0.9834 * Z;
}
*/
///////////////////////////////////
// HPE versions
// LuminanceAdapt implements the luminance adaptation function
// equals 1 at background luminance of 200 so we generally ignore it..
// bgLum is background luminance -- 200 default.
func LuminanceAdapt(bgLum float32) float32 {
lum5 := 5.0 * bgLum
k := 1.0 / (lum5 + 1)
k4 := k * k * k * k
k4m1 := 1 - k4
fl := 0.2*k4*lum5 + .1*k4m1*k4m1*math32.Pow(lum5, 1.0/3.0)
return fl
}
// ResponseCompression takes a 0-1 normalized LMS value
// and performs hyperbolic response compression.
// val must ALREADY have the luminance adaptation applied to it
// using the luminance adaptation function, which is 1 at a
// background luminance level of 200 = 2, so you can skip that
// step if you assume that level of background.
func ResponseCompression(val float32) float32 {
pval := math32.Pow(val, 0.42)
rc := 0.1 + 4.0*pval/(27.13+pval)
return rc
}
// LMSToResp converts Long, Medium, Short cone-based values to
// values that more closely reflect neural responses,
// including a combined long-medium (yellow) channel (lmc).
// Uses the CIECAM02 color appearance model (MoroneyFairchildHuntEtAl02)
// https://en.wikipedia.org/wiki/CIECAM02
func LMSToResp(l, m, s float32) (lc, mc, sc, lmc, grey float32) {
lA := ResponseCompression(l)
mA := ResponseCompression(m)
sA := ResponseCompression(s)
// subtract min and mult by 6 gets values roughly into 1-0 range for L,M
lc = 6 * ((lA + (float32(1)/11)*sA) - 0.109091)
mc = 6 * (((float32(12) / 11) * mA) - 0.109091)
sc = 6 * (((float32(2) / 9) * sA) - 0.0222222)
lmc = 6 * (((float32(1) / 9) * (lA + mA)) - 0.0222222)
grey = (1 / 0.431787) * (2*lA + mA + .05*sA - 0.305)
// note: last term should be: 0.725 * (1/5)^-0.2 = grey background assumption (Yb/Yw = 1/5) = 1
return
}
// SRGBToLMSResp converts sRGB to LMS neural response cone values,
// that more closely reflect neural responses,
// including a combined long-medium (yellow) channel (lmc).
// Uses the CIECAM02 color appearance model (MoroneyFairchildHuntEtAl02)
// https://en.wikipedia.org/wiki/CIECAM02
func SRGBToLMSResp(r, g, b float32) (lc, mc, sc, lmc, grey float32) {
l, m, s := SRGBToLMS(r, g, b)
lc, mc, sc, lmc, grey = LMSToResp(l, m, s)
return
}
// 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.
// Adapted from https://github.com/material-foundation/material-color-utilities
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cam16
import (
"image/color"
"cogentcore.org/core/base/num"
"cogentcore.org/core/colors/cam/cie"
"cogentcore.org/core/math32"
)
// CAM represents a point in the cam16 color model along 6 dimensions
// representing the perceived hue, colorfulness, and brightness,
// similar to HSL but much more well-calibrated to actual human subjective judgments.
type CAM struct {
// hue (h) is the spectral identity of the color (red, green, blue etc) in degrees (0-360)
Hue float32
// chroma (C) is the colorfulness or saturation of the color -- greyscale colors have no chroma, and fully saturated ones have high chroma
Chroma float32
// colorfulness (M) is the absolute chromatic intensity
Colorfulness float32
// saturation (s) is the colorfulness relative to brightness
Saturation float32
// brightness (Q) is the apparent amount of light from the color, which is not a simple function of actual light energy emitted
Brightness float32
// lightness (J) is the brightness relative to a reference white, which varies as a function of chroma and hue
Lightness float32
}
// RGBA implements the color.Color interface.
func (cam *CAM) RGBA() (r, g, b, a uint32) {
x, y, z := cam.XYZ()
rf, gf, bf := cie.XYZ100ToSRGB(x, y, z)
return cie.SRGBFloatToUint32(rf, gf, bf, 1)
}
// AsRGBA returns the color as a [color.RGBA].
func (cam *CAM) AsRGBA() color.RGBA {
x, y, z := cam.XYZ()
rf, gf, bf := cie.XYZ100ToSRGB(x, y, z)
r, g, b, a := cie.SRGBFloatToUint8(rf, gf, bf, 1)
return color.RGBA{r, g, b, a}
}
// UCS returns the CAM16-UCS components based on the the CAM values
func (cam *CAM) UCS() (j, m, a, b float32) {
j = (1 + 100*0.007) * cam.Lightness / (1 + 0.007*cam.Lightness)
m = math32.Log(1+0.0228*cam.Colorfulness) / 0.0228
hr := math32.DegToRad(cam.Hue)
a = m * math32.Cos(hr)
b = m * math32.Sin(hr)
return
}
// FromUCS returns CAM values from the given CAM16-UCS coordinates
// (jstar, astar, and bstar), under standard viewing conditions
func FromUCS(j, a, b float32) *CAM {
return FromUCSView(j, a, b, NewStdView())
}
// FromUCS returns CAM values from the given CAM16-UCS coordinates
// (jstar, astar, and bstar), using the given viewing conditions
func FromUCSView(j, a, b float32, vw *View) *CAM {
m := math32.Sqrt(a*a + b*b)
M := (math32.Exp(m*0.0228) - 1) / 0.0228
c := M / vw.FLRoot
h := math32.RadToDeg(math32.Atan2(b, a))
if h < 0 {
h += 360
}
j /= 1 - (j-100)*0.007
return FromJCHView(j, c, h, vw)
}
// FromJCH returns CAM values from the given lightness (j), chroma (c),
// and hue (h) values under standard viewing condition
func FromJCH(j, c, h float32) *CAM {
return FromJCHView(j, c, h, NewStdView())
}
// FromJCHView returns CAM values from the given lightness (j), chroma (c),
// and hue (h) values under the given viewing conditions
func FromJCHView(j, c, h float32, vw *View) *CAM {
cam := &CAM{Lightness: j, Chroma: c, Hue: h}
cam.Brightness = (4 / vw.C) *
math32.Sqrt(cam.Lightness/100) *
(vw.AW + 4) *
(vw.FLRoot)
cam.Colorfulness = cam.Chroma * vw.FLRoot
alpha := cam.Chroma / math32.Sqrt(cam.Lightness/100)
cam.Saturation = 50 * math32.Sqrt((alpha*vw.C)/(vw.AW+4))
return cam
}
// FromSRGB returns CAM values from given SRGB color coordinates,
// under standard viewing conditions. The RGB value range is 0-1,
// and RGB values have gamma correction.
func FromSRGB(r, g, b float32) *CAM {
return FromXYZ(cie.SRGBToXYZ100(r, g, b))
}
// FromXYZ returns CAM values from given XYZ color coordinate,
// under standard viewing conditions
func FromXYZ(x, y, z float32) *CAM {
return FromXYZView(x, y, z, NewStdView())
}
// FromXYZView returns CAM values from given XYZ color coordinate,
// under given viewing conditions. Requires 100-base XYZ coordinates.
func FromXYZView(x, y, z float32, vw *View) *CAM {
l, m, s := XYZToLMS(x, y, z)
redVgreen, yellowVblue, grey, greyNorm := LMSToOps(l, m, s, vw)
hue := SanitizeDegrees(math32.RadToDeg(math32.Atan2(yellowVblue, redVgreen)))
// achromatic response to color
ac := grey * vw.NBB
// CAM16 lightness and brightness
J := 100 * math32.Pow(ac/vw.AW, vw.C*vw.Z)
Q := (4 / vw.C) * math32.Sqrt(J/100) * (vw.AW + 4) * (vw.FLRoot)
huePrime := hue
if hue < 20.14 {
huePrime += 360
}
eHue := 0.25 * (math32.Cos(huePrime*math32.Pi/180+2) + 3.8)
p1 := 50000 / 13 * eHue * vw.NC * vw.NCB
t := p1 * math32.Sqrt(redVgreen*redVgreen+yellowVblue*yellowVblue) / (greyNorm + 0.305)
alpha := math32.Pow(t, 0.9) * math32.Pow(1.64-math32.Pow(0.29, vw.BgYToWhiteY), 0.73)
// CAM16 chroma, colorfulness, chroma
C := alpha * math32.Sqrt(J/100)
M := C * vw.FLRoot
s = 50 * math32.Sqrt((alpha*vw.C)/(vw.AW+4))
return &CAM{Hue: hue, Chroma: C, Colorfulness: M, Saturation: s, Brightness: Q, Lightness: J}
}
// XYZ returns the CAM color as XYZ coordinates
// under standard viewing conditions.
// Returns 100-base XYZ coordinates.
func (cam *CAM) XYZ() (x, y, z float32) {
return cam.XYZView(NewStdView())
}
// XYZ returns the CAM color as XYZ coordinates
// under the given viewing conditions.
// Returns 100-base XYZ coordinates.
func (cam *CAM) XYZView(vw *View) (x, y, z float32) {
alpha := float32(0)
if cam.Chroma != 0 || cam.Lightness != 0 {
alpha = cam.Chroma / math32.Sqrt(cam.Lightness/100)
}
t := math32.Pow(
alpha/
math32.Pow(
1.64-
math32.Pow(0.29, vw.BgYToWhiteY),
0.73),
1.0/0.9)
hRad := math32.DegToRad(cam.Hue)
eHue := 0.25 * (math32.Cos(hRad+2) + 3.8)
ac := vw.AW * math32.Pow(cam.Lightness/100, 1/vw.C/vw.Z)
p1 := eHue * (50000 / 13) * vw.NC * vw.NCB
p2 := ac / vw.NBB
hSin := math32.Sin(hRad)
hCos := math32.Cos(hRad)
gamma := 23 *
(p2 + 0.305) *
t /
(23*p1 + 11*t*hCos + 108*t*hSin)
a := gamma * hCos
b := gamma * hSin
rA := (460*p2 + 451*a + 288*b) / 1403
gA := (460*p2 - 891*a - 261*b) / 1403
bA := (460*p2 - 220*a - 6300*b) / 1403
rCBase := max(0, (27.13*num.Abs(rA))/(400-num.Abs(rA)))
// TODO(kai): their sign function returns 0 for 0, but we return 1, so this might break
rC := math32.Sign(rA) *
(100 / vw.FL) *
math32.Pow(rCBase, 1/0.42)
gCBase := max(0, (27.13*num.Abs(gA))/(400-num.Abs(gA)))
gC := math32.Sign(gA) *
(100 / vw.FL) *
math32.Pow(gCBase, 1/0.42)
bCBase := max(0, (27.13*num.Abs(bA))/(400-num.Abs(bA)))
bC := math32.Sign(bA) *
(100 / vw.FL) *
math32.Pow(bCBase, 1/0.42)
rF := rC / vw.RGBD.X
gF := gC / vw.RGBD.Y
bF := bC / vw.RGBD.Z
x = 1.86206786*rF - 1.01125463*gF + 0.14918677*bF
y = 0.38752654*rF + 0.62144744*gF - 0.00897398*bF
z = -0.01584150*rF - 0.03412294*gF + 1.04996444*bF
return
}
// 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 cam16
import (
"cogentcore.org/core/math32"
)
// XYZToLMS converts XYZ to Long, Medium, Short cone-based responses,
// using the CAT16 transform from CIECAM16 color appearance model
// (LiLiWangEtAl17)
func XYZToLMS(x, y, z float32) (l, m, s float32) {
l = x*0.401288 + y*0.650173 + z*-0.051461
m = x*-0.250268 + y*1.204414 + z*0.045854
s = x*-0.002079 + y*0.048952 + z*0.953127
return
}
// LMSToXYZ converts Long, Medium, Short cone-based responses to XYZ
// using the CAT16 transform from CIECAM16 color appearance model
// (LiLiWangEtAl17)
func LMSToXYZ(l, m, s float32) (x, y, z float32) {
x = l*1.86206787 + m*-1.0112563 + s*0.14918667
y = l*0.38752654 + m*0.62144744 + s*-0.00897398
z = l*-0.01584150 + m*-0.03412294 + s*1.04996444
return
}
// LuminanceAdaptComp performs luminance adaptation
// and response compression according to the CAM16 model,
// on one component, using equations from HuntLiLuo03
// d = discount factor
// fl = luminance adaptation factor
func LuminanceAdaptComp(v, d, fl float32) float32 {
vd := v * d
f := math32.Pow((fl*math32.Abs(vd))/100, 0.42)
return (math32.Sign(vd) * 400 * f) / (f + 27.13)
}
func InverseChromaticAdapt(adapted float32) float32 {
adaptedAbs := math32.Abs(adapted)
base := math32.Max(0, 27.13*adaptedAbs/(400.0-adaptedAbs))
return math32.Sign(adapted) * math32.Pow(base, 1.0/0.42)
}
// LuminanceAdapt performs luminance adaptation
// and response compression according to the CAM16 model,
// on given r,g,b components, using equations from HuntLiLuo03
// and parameters on given viewing conditions
func LuminanceAdapt(l, m, s float32, vw *View) (lA, mA, sA float32) {
lA = LuminanceAdaptComp(l, vw.RGBD.X, vw.FL)
mA = LuminanceAdaptComp(m, vw.RGBD.Y, vw.FL)
sA = LuminanceAdaptComp(s, vw.RGBD.Z, vw.FL)
return
}
// LMSToOps converts Long, Medium, Short cone-based values to
// opponent redVgreen (a) and yellowVblue (b), and grey (achromatic) values,
// that more closely reflect neural responses.
// greyNorm is a normalizing grey factor used in the CAM16 model.
// l, m, s values must be in 100-base units.
// Uses the CIECAM16 color appearance model.
func LMSToOps(l, m, s float32, vw *View) (redVgreen, yellowVblue, grey, greyNorm float32) {
// Discount illuminant and adapt
lA, mA, sA := LuminanceAdapt(l, m, s, vw)
redVgreen = (11*lA + -12*mA + sA) / 11
yellowVblue = (lA + mA - 2*sA) / 9
// auxiliary components
grey = (40*lA + 20*mA + sA) / 20 // achromatic response, multiplied * view.NBB
greyNorm = (20*lA + 20*mA + 21*sA) / 20 // normalizing factor
return
}
// 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.
// Adapted from https://github.com/material-foundation/material-color-utilities
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cam16
import "cogentcore.org/core/math32"
// SanitizeDegrees ensures that degrees is in [0-360) range
func SanitizeDegrees(deg float32) float32 {
if deg < 0 {
return math32.Mod(deg, 360) + 360
} else if deg >= 360 {
return math32.Mod(deg, 360)
} else {
return deg
}
}
// SanitizeRadians sanitizes a small enough angle in radians.
// Takes an angle in radians; must not deviate too much from 0,
// and returns a coterminal angle between 0 and 2pi.
func SanitizeRadians(angle float32) float32 {
return math32.Mod(angle+math32.Pi*8, math32.Pi*2)
}
// InCyclicOrder returns true a, b, c are in order around a circle
func InCyclicOrder(a, b, c float32) bool {
delta_a_b := SanitizeRadians(b - a)
delta_a_c := SanitizeRadians(c - a)
return delta_a_b < delta_a_c
}
// 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 cam16
import (
"image/color"
"cogentcore.org/core/colors/cam/cie"
"cogentcore.org/core/math32"
)
// Blend returns a color that is the given percent blend between the first
// and second color; 10 = 10% of the first and 90% of the second, etc;
// blending is done directly on non-premultiplied CAM16-UCS values, and
// a correctly premultiplied color is returned.
func Blend(pct float32, x, y color.Color) color.RGBA {
pct = math32.Clamp(pct, 0, 100)
amt := pct / 100
xsr, xsg, xsb, _ := cie.SRGBUint32ToFloat(x.RGBA())
ysr, ysg, ysb, _ := cie.SRGBUint32ToFloat(y.RGBA())
cx := FromSRGB(xsr, xsg, xsb)
cy := FromSRGB(ysr, ysg, ysb)
xj, _, xa, xb := cx.UCS()
yj, _, ya, yb := cy.UCS()
j := yj + (xj-yj)*amt
a := ya + (xa-ya)*amt
b := yb + (xb-yb)*amt
cam := FromUCS(j, a, b)
return cam.AsRGBA()
}
// 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.
// Adapted from https://github.com/material-foundation/material-color-utilities
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cam16
import (
"cogentcore.org/core/colors/cam/cie"
"cogentcore.org/core/math32"
)
// View represents viewing conditions under which a color is being perceived,
// which greatly affects the subjective perception. Defaults represent the
// standard defined such conditions, under which the CAM16 computations operate.
type View struct {
// white point illumination -- typically cie.WhiteD65
WhitePoint math32.Vector3
// the ambient light strength in lux
Luminance float32 `default:"200"`
// the average luminance of 10 degrees around the color in question
BgLuminance float32 `default:"50"`
// the brightness of the entire environment
Surround float32 `default:"2"`
// whether the person's eyes have adapted to the lighting
Adapted bool `default:"false"`
// computed from Luminance
AdaptingLuminance float32 `display:"-"`
//
BgYToWhiteY float32 `display:"-"`
//
AW float32 `display:"-"`
// luminance level induction factor
NBB float32 `display:"-"`
// luminance level induction factor
NCB float32 `display:"-"`
// exponential nonlinearity
C float32 `display:"-"`
// chromatic induction factor
NC float32 `display:"-"`
// luminance-level adaptation factor, based on the HuntLiLuo03 equations
FL float32 `display:"-"`
// FL to the 1/4 power
FLRoot float32 `display:"-"`
// base exponential nonlinearity
Z float32 `display:"-"`
// inverse of the RGBD factors
DRGBInverse math32.Vector3 `display:"-"`
// cone responses to white point, adjusted for discounting
RGBD math32.Vector3 `display:"-"`
}
// NewView returns a new view with all parameters initialized based on given major params
func NewView(whitePoint math32.Vector3, lum, bgLum, surround float32, adapt bool) *View {
vw := &View{WhitePoint: whitePoint, Luminance: lum, BgLuminance: bgLum, Surround: surround, Adapted: adapt}
vw.Update()
return vw
}
// TheStdView is the standard viewing conditions view
// returned by NewStdView if already created.
var TheStdView *View
// NewStdView returns a new standard viewing conditions model
// returns TheStdView if already created
func NewStdView() *View {
if TheStdView != nil {
return TheStdView
}
TheStdView = NewView(cie.WhiteD65, 200, 50, 2, false)
return TheStdView
}
// Update updates all the computed values based on main parameters
func (vw *View) Update() {
vw.AdaptingLuminance = (vw.Luminance / math32.Pi) * (cie.LToY(50) / 100)
// A background of pure black is non-physical and leads to infinities that
// represent the idea that any color viewed in pure black can't be seen.
vw.BgLuminance = math32.Max(0.1, vw.BgLuminance)
// Transform test illuminant white in XYZ to 'cone'/'rgb' responses
rW, gW, bW := XYZToLMS(vw.WhitePoint.X, vw.WhitePoint.Y, vw.WhitePoint.Z)
// Scale input surround, domain (0, 2), to CAM16 surround, domain (0.8, 1.0)
vw.Surround = math32.Clamp(vw.Surround, 0, 2)
f := 0.8 + (vw.Surround / 10)
// "Exponential non-linearity"
if f >= 0.9 {
vw.C = math32.Lerp(0.59, 0.69, ((f - 0.9) * 10))
} else {
vw.C = math32.Lerp(0.525, 0.59, ((f - 0.8) * 10))
}
// Calculate degree of adaptation to illuminant
d := float32(1)
if !vw.Adapted {
d = f * (1 - ((1 / 3.6) * math32.Exp((-vw.AdaptingLuminance-42)/92)))
}
// Per Li et al, if D is greater than 1 or less than 0, set it to 1 or 0.
d = math32.Clamp(d, 0, 1)
// chromatic induction factor
vw.NC = f
// Cone responses to the whitePoint, r/g/b/W, adjusted for discounting.
//
// Why use 100 instead of the white point's relative luminance?
//
// Some papers and implementations, for both CAM02 and CAM16, use the Y
// value of the reference white instead of 100. Fairchild's Color Appearance
// Models (3rd edition) notes that this is in error: it was included in the
// CIE 2004a report on CIECAM02, but, later parts of the conversion process
// account for scaling of appearance relative to the white point relative
// luminance. This part should simply use 100 as luminance.
vw.RGBD.X = d*(100/rW) + 1 - d
vw.RGBD.Y = d*(100/gW) + 1 - d
vw.RGBD.Z = d*(100/bW) + 1 - d
// Factor used in calculating meaningful factors
k := 1 / (5*vw.AdaptingLuminance + 1)
k4 := k * k * k * k
k4F := 1 - k4
// Luminance-level adaptation factor
vw.FL = (k4 * vw.AdaptingLuminance) +
(0.1 * k4F * k4F * math32.Pow(5*vw.AdaptingLuminance, 1.0/3.0))
vw.FLRoot = math32.Pow(vw.FL, 0.25)
// Intermediate factor, ratio of background relative luminance to white relative luminance
n := cie.LToY(vw.BgLuminance) / vw.WhitePoint.Y
vw.BgYToWhiteY = n
// Base exponential nonlinearity
// note Schlomer 2018 has a typo and uses 1.58, the correct factor is 1.48
vw.Z = 1.48 + math32.Sqrt(n)
// Luminance-level induction factors
vw.NBB = 0.725 / math32.Pow(n, 0.2)
vw.NCB = vw.NBB
// Discounted cone responses to the white point, adjusted for post-saturation
// adaptation perceptual nonlinearities.
rA, gA, bA := LuminanceAdapt(rW, gW, bW, vw)
vw.AW = ((40*rA + 20*gA + bA) / 20) * vw.NBB
}
// 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 cie
import "cogentcore.org/core/math32"
// LABCompress does cube-root compression of the X, Y, Z components
// prior to performing the LAB conversion
func LABCompress(t float32) float32 {
e := float32(216.0 / 24389.0)
if t > e {
return math32.Pow(t, 1.0/3.0)
}
kappa := float32(24389.0 / 27.0)
return (kappa*t + 16) / 116
}
func LABUncompress(ft float32) float32 {
e := float32(216.0 / 24389.0)
ft3 := ft * ft * ft
if ft3 > e {
return ft3
}
kappa := float32(24389.0 / 27.0)
return (116*ft - 16) / kappa
}
// XYZToLAB converts a color from XYZ to L*a*b* coordinates
// using the standard D65 illuminant
func XYZToLAB(x, y, z float32) (l, a, b float32) {
x, y, z = XYZNormD65(x, y, z)
fx := LABCompress(x)
fy := LABCompress(y)
fz := LABCompress(z)
l = 116*fy - 16
a = 500 * (fx - fy)
b = 200 * (fy - fz)
return
}
// LABToXYZ converts a color from L*a*b* to XYZ coordinates
// using the standard D65 illuminant
func LABToXYZ(l, a, b float32) (x, y, z float32) {
fy := (l + 16) / 116
fx := a/500 + fy
fz := fy - b/200
x = LABUncompress(fx)
y = LABUncompress(fy)
z = LABUncompress(fz)
x, y, z = XYZDenormD65(x, y, z)
return
}
// LToY Converts an L* value to a Y value.
// L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
// L* measures perceptual luminance, a linear scale. Y in XYZ
// measures relative luminance, a logarithmic scale.
func LToY(l float32) float32 {
return 100 * LABUncompress((l+16)/116)
}
// YToL Converts a Y value to an L* value.
// L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
// L* measures perceptual luminance, a linear scale. Y in XYZ
// measures relative luminance, a logarithmic scale.
func YToL(y float32) float32 {
return LABCompress(y/100)*116 - 16
}
// Copyright (c) 2021, 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 cie
import "cogentcore.org/core/math32"
// SRGBToLinearComp converts an sRGB rgb component to linear space (removes gamma).
// Used in converting from sRGB to XYZ colors.
func SRGBToLinearComp(srgb float32) float32 {
if srgb <= 0.04045 {
return srgb / 12.92
}
return math32.Pow((srgb+0.055)/1.055, 2.4)
}
// SRGBFromLinearComp converts an sRGB rgb linear component
// to non-linear (gamma corrected) sRGB value
// Used in converting from XYZ to sRGB.
func SRGBFromLinearComp(lin float32) float32 {
var gv float32
if lin <= 0.0031308 {
gv = 12.92 * lin
} else {
gv = (1.055*math32.Pow(lin, 1.0/2.4) - 0.055)
}
return math32.Clamp(gv, 0, 1)
}
// SRGBToLinear converts set of sRGB components to linear values,
// removing gamma correction.
func SRGBToLinear(r, g, b float32) (rl, gl, bl float32) {
rl = SRGBToLinearComp(r)
gl = SRGBToLinearComp(g)
bl = SRGBToLinearComp(b)
return
}
// SRGB100ToLinear converts set of sRGB components to linear values,
// removing gamma correction. returns 100-base RGB values
func SRGB100ToLinear(r, g, b float32) (rl, gl, bl float32) {
rl = 100 * SRGBToLinearComp(r)
gl = 100 * SRGBToLinearComp(g)
bl = 100 * SRGBToLinearComp(b)
return
}
// SRGBFromLinear converts set of sRGB components from linear values,
// adding gamma correction.
func SRGBFromLinear(rl, gl, bl float32) (r, g, b float32) {
r = SRGBFromLinearComp(rl)
g = SRGBFromLinearComp(gl)
b = SRGBFromLinearComp(bl)
return
}
// SRGBFromLinear100 converts set of sRGB components from linear values in 0-100 range,
// adding gamma correction.
func SRGBFromLinear100(rl, gl, bl float32) (r, g, b float32) {
r = SRGBFromLinearComp(rl / 100)
g = SRGBFromLinearComp(gl / 100)
b = SRGBFromLinearComp(bl / 100)
return
}
// SRGBFloatToUint8 converts the given non-alpha-premuntiplied sRGB float32
// values to alpha-premultiplied sRGB uint8 values.
func SRGBFloatToUint8(rf, gf, bf, af float32) (r, g, b, a uint8) {
r = uint8(rf*af*255 + 0.5)
g = uint8(gf*af*255 + 0.5)
b = uint8(bf*af*255 + 0.5)
a = uint8(af*255 + 0.5)
return
}
// SRGBFloatToUint32 converts the given non-alpha-premuntiplied sRGB float32
// values to alpha-premultiplied sRGB uint32 values.
func SRGBFloatToUint32(rf, gf, bf, af float32) (r, g, b, a uint32) {
r = uint32(rf*af*65535 + 0.5)
g = uint32(gf*af*65535 + 0.5)
b = uint32(bf*af*65535 + 0.5)
a = uint32(af*65535 + 0.5)
return
}
// SRGBUint8ToFloat converts the given alpha-premultiplied sRGB uint8 values
// to non-alpha-premuntiplied sRGB float32 values.
func SRGBUint8ToFloat(r, g, b, a uint8) (fr, fg, fb, fa float32) {
fa = float32(a) / 255
fr = (float32(r) / 255) / fa
fg = (float32(g) / 255) / fa
fb = (float32(b) / 255) / fa
return
}
// SRGBUint32ToFloat converts the given alpha-premultiplied sRGB uint32 values
// to non-alpha-premuntiplied sRGB float32 values.
func SRGBUint32ToFloat(r, g, b, a uint32) (fr, fg, fb, fa float32) {
fa = float32(a) / 65535
fr = (float32(r) / 65535) / fa
fg = (float32(g) / 65535) / fa
fb = (float32(b) / 65535) / fa
return
}
// Copyright (c) 2021, 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 cie
// SRGBLinToXYZ converts sRGB linear into XYZ CIE standard color space
func SRGBLinToXYZ(rl, gl, bl float32) (x, y, z float32) {
x = 0.41233895*rl + 0.35762064*gl + 0.18051042*bl
y = 0.2126*rl + 0.7152*gl + 0.0722*bl
z = 0.01932141*rl + 0.11916382*gl + 0.95034478*bl
return
}
// XYZToSRGBLin converts XYZ CIE standard color space to sRGB linear
func XYZToSRGBLin(x, y, z float32) (rl, gl, bl float32) {
rl = 3.2406*x + -1.5372*y + -0.4986*z
gl = -0.9689*x + 1.8758*y + 0.0415*z
bl = 0.0557*x + -0.2040*y + 1.0570*z
return
}
// SRGBToXYZ converts sRGB into XYZ CIE standard color space
func SRGBToXYZ(r, g, b float32) (x, y, z float32) {
rl, gl, bl := SRGBToLinear(r, g, b)
x, y, z = SRGBLinToXYZ(rl, gl, bl)
return
}
// SRGBToXYZ100 converts sRGB into XYZ CIE standard color space
// with 100-base sRGB values -- used for CAM16 but not CAM02
func SRGBToXYZ100(r, g, b float32) (x, y, z float32) {
rl, gl, bl := SRGB100ToLinear(r, g, b)
x, y, z = SRGBLinToXYZ(rl, gl, bl)
return
}
// XYZToSRGB converts XYZ CIE standard color space into sRGB
func XYZToSRGB(x, y, z float32) (r, g, b float32) {
rl, bl, gl := XYZToSRGBLin(x, y, z)
r, g, b = SRGBFromLinear(rl, bl, gl)
return
}
// XYZ100ToSRGB converts XYZ CIE standard color space, 100 base units,
// into sRGB
func XYZ100ToSRGB(x, y, z float32) (r, g, b float32) {
rl, bl, gl := XYZToSRGBLin(x/100, y/100, z/100)
r, g, b = SRGBFromLinear(rl, bl, gl)
return
}
// XYZNormD65 normalizes XZY values relative to the D65 outdoor white light values
func XYZNormD65(x, y, z float32) (xr, yr, zr float32) {
xr = x / 0.95047
zr = z / 1.08883
yr = y
return
}
// XYZDenormD65 de-normalizes XZY values relative to the D65 outdoor white light values
func XYZDenormD65(x, y, z float32) (xr, yr, zr float32) {
xr = x * 0.95047
zr = z * 1.08883
yr = y
return
}
// 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.
// Adapted from https://github.com/material-foundation/material-color-utilities
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hct
import (
"cogentcore.org/core/base/num"
"cogentcore.org/core/colors/cam/cam16"
"cogentcore.org/core/math32"
)
// double ChromaticAdaptation(double component) {
// double af = pow(abs(component), 0.42);
// return Signum(component) * 400.0 * af / (af + 27.13);
// }
func MatMul(v math32.Vector3, mat [3][3]float32) math32.Vector3 {
x := v.X*mat[0][0] + v.Y*mat[0][1] + v.Z*mat[0][2]
y := v.X*mat[1][0] + v.Y*mat[1][1] + v.Z*mat[1][2]
z := v.X*mat[2][0] + v.Y*mat[2][1] + v.Z*mat[2][2]
return math32.Vec3(x, y, z)
}
// HueOf Returns the hue of a linear RGB color in CAM16.
func HueOf(linrgb math32.Vector3) float32 {
sd := MatMul(linrgb, kScaledDiscountFromLinrgb)
rA := cam16.LuminanceAdaptComp(sd.X, 1, 1)
gA := cam16.LuminanceAdaptComp(sd.Y, 1, 1)
bA := cam16.LuminanceAdaptComp(sd.Z, 1, 1)
// redness-greenness
a := (11*rA + -12*gA + bA) / 11
// yellowness-blueness
b := (rA + gA - 2*bA) / 9
return math32.Atan2(b, a)
}
// Solves the lerp equation.
// @param source The starting number.
// @param mid The number in the middle.
// @param target The ending number.
// @return A number t such that lerp(source, target, t) = mid.
func Intercept(source, mid, target float32) float32 {
return (mid - source) / (target - source)
}
// GetAxis returns value along axis 0,1,2 -- result is divided by 100
// so that resulting numbers are in 0-1 range.
func GetAxis(v math32.Vector3, axis int) float32 {
switch axis {
case 0:
return v.X
case 1:
return v.Y
case 2:
return v.Z
default:
return -1
}
}
/**
* Intersects a segment with a plane.
*
* @param source The coordinates of point A.
* @param coordinate The R-, G-, or B-coordinate of the plane.
* @param target The coordinates of point B.
* @param axis The axis the plane is perpendicular with. (0: R, 1: G, 2: B)
* @return The intersection point of the segment AB with the plane R=coordinate,
* G=coordinate, or B=coordinate
*/
func SetCoordinate(source, target math32.Vector3, coord float32, axis int) math32.Vector3 {
t := Intercept(GetAxis(source, axis), coord, GetAxis(target, axis))
return source.Lerp(target, t)
}
func IsBounded(x float32) bool {
return 0 <= x && x <= 100
}
// Returns the nth possible vertex of the polygonal intersection.
// @param y The Y value of the plane.
// @param n The zero-based index of the point. 0 <= n <= 11.
// @return The nth possible vertex of the polygonal intersection of the y plane
// and the RGB cube, in linear RGB coordinates, if it exists. If this possible
// vertex lies outside of the cube,
//
// [-1.0, -1.0, -1.0] is returned.
func NthVertex(y float32, n int) math32.Vector3 {
k_r := kYFromLinrgb[0]
k_g := kYFromLinrgb[1]
k_b := kYFromLinrgb[2]
coord_a := float32(0)
if n%4 > 1 {
coord_a = 100
}
coord_b := float32(0)
if n%2 != 0 {
coord_b = 100
}
if n < 4 {
g := coord_a
b := coord_b
r := (y - g*k_g - b*k_b) / k_r
if IsBounded(r) {
return math32.Vec3(r, g, b)
} else {
return math32.Vec3(-1.0, -1.0, -1.0)
}
} else if n < 8 {
b := coord_a
r := coord_b
g := (y - r*k_r - b*k_b) / k_g
if IsBounded(g) {
return math32.Vec3(r, g, b)
} else {
return math32.Vec3(-1.0, -1.0, -1.0)
}
} else {
r := coord_a
g := coord_b
b := (y - r*k_r - g*k_g) / k_b
if IsBounded(b) {
return math32.Vec3(r, g, b)
} else {
return math32.Vec3(-1.0, -1.0, -1.0)
}
}
}
// Finds the segment containing the desired color.
// @param y The Y value of the color.
// @param target_hue The hue of the color.
// @return A list of two sets of linear RGB coordinates, each corresponding to
// an endpoint of the segment containing the desired color.
func BisectToSegment(y, target_hue float32) [2]math32.Vector3 {
left := math32.Vec3(-1.0, -1.0, -1.0)
right := left
left_hue := float32(0.0)
right_hue := float32(0.0)
initialized := false
uncut := true
for n := 0; n < 12; n++ {
mid := NthVertex(y, n)
if mid.X < 0 {
continue
}
mid_hue := HueOf(mid)
if !initialized {
left = mid
right = mid
left_hue = mid_hue
right_hue = mid_hue
initialized = true
continue
}
if uncut || cam16.InCyclicOrder(left_hue, mid_hue, right_hue) {
uncut = false
if cam16.InCyclicOrder(left_hue, target_hue, mid_hue) {
right = mid
right_hue = mid_hue
} else {
left = mid
left_hue = mid_hue
}
}
}
var out [2]math32.Vector3
out[0] = left
out[1] = right
return out
}
func Midpoint(a, b math32.Vector3) math32.Vector3 {
return math32.Vec3((a.X+b.X)/2, (a.Y+b.Y)/2, (a.Z+b.Z)/2)
}
func CriticalPlaneBelow(x float32) int { return int(math32.Floor(x - 0.5)) }
func CriticalPlaneAbove(x float32) int { return int(math32.Ceil(x - 0.5)) }
// Delinearizes an RGB component, returning a floating-point number.
// @param rgb_component 0.0 <= rgb_component <= 100.0, represents linear R/G/B
// channel
// @return 0.0 <= output <= 255.0, color channel converted to regular RGB space
func TrueDelinearized(comp float32) float32 {
normalized := comp / 100
delinearized := float32(0.0)
if normalized <= 0.0031308 {
delinearized = normalized * 12.92
} else {
delinearized = 1.055*math32.Pow(normalized, 1.0/2.4) - 0.055
}
return delinearized * 255
}
// Finds a color with the given Y and hue on the boundary of the cube.
// @param y The Y value of the color.
// @param target_hue The hue of the color.
// @return The desired color, in linear RGB coordinates.
func BisectToLimit(y, target_hue float32) math32.Vector3 {
segment := BisectToSegment(y, target_hue)
left := segment[0]
left_hue := HueOf(left)
right := segment[1]
for axis := 0; axis < 3; axis++ {
if GetAxis(left, axis) != GetAxis(right, axis) {
l_plane := -1
r_plane := 255
if GetAxis(left, axis) < GetAxis(right, axis) {
l_plane = CriticalPlaneBelow(TrueDelinearized(GetAxis(left, axis)))
r_plane = CriticalPlaneAbove(TrueDelinearized(GetAxis(right, axis)))
} else {
l_plane = CriticalPlaneAbove(TrueDelinearized(GetAxis(left, axis)))
r_plane = CriticalPlaneBelow(TrueDelinearized(GetAxis(right, axis)))
}
for i := 0; i < 8; i++ {
if num.Abs(r_plane-l_plane) <= 1 {
break
} else {
m_plane := int(math32.Floor(float32(l_plane+r_plane) / 2.0))
mid_plane_coordinate := kCriticalPlanes[m_plane]
mid := SetCoordinate(left, right, mid_plane_coordinate, axis)
mid_hue := HueOf(mid)
if cam16.InCyclicOrder(left_hue, target_hue, mid_hue) {
right = mid
r_plane = m_plane
} else {
left = mid
left_hue = mid_hue
l_plane = m_plane
}
}
}
}
}
return Midpoint(left, right)
}
/////////////////////////////////////////////
var kScaledDiscountFromLinrgb = [3][3]float32{
{
0.001200833568784504,
0.002389694492170889,
0.0002795742885861124,
},
{
0.0005891086651375999,
0.0029785502573438758,
0.0003270666104008398,
},
{
0.00010146692491640572,
0.0005364214359186694,
0.0032979401770712076,
},
}
var kLinrgbFromScaledDiscount = [3][3]float32{
{
1373.2198709594231,
-1100.4251190754821,
-7.278681089101213,
},
{
-271.815969077903,
559.6580465940733,
-32.46047482791194,
},
{
1.9622899599665666,
-57.173814538844006,
308.7233197812385,
},
}
var kYFromLinrgb = [3]float32{0.2126, 0.7152, 0.0722}
var kCriticalPlanes = [255]float32{
0.015176349177441876, 0.045529047532325624, 0.07588174588720938,
0.10623444424209313, 0.13658714259697685, 0.16693984095186062,
0.19729253930674434, 0.2276452376616281, 0.2579979360165119,
0.28835063437139563, 0.3188300904430532, 0.350925934958123,
0.3848314933096426, 0.42057480301049466, 0.458183274052838,
0.4976837250274023, 0.5391024159806381, 0.5824650784040898,
0.6277969426914107, 0.6751227633498623, 0.7244668422128921,
0.775853049866786, 0.829304845476233, 0.8848452951698498,
0.942497089126609, 1.0022825574869039, 1.0642236851973577,
1.1283421258858297, 1.1946592148522128, 1.2631959812511864,
1.3339731595349034, 1.407011200216447, 1.4823302800086415,
1.5599503113873272, 1.6398909516233677, 1.7221716113234105,
1.8068114625156377, 1.8938294463134073, 1.9832442801866852,
2.075074464868551, 2.1693382909216234, 2.2660538449872063,
2.36523901573795, 2.4669114995532007, 2.5710888059345764,
2.6777882626779785, 2.7870270208169257, 2.898822059350997,
3.0131901897720907, 3.1301480604002863, 3.2497121605402226,
3.3718988244681087, 3.4967242352587946, 3.624204428461639,
3.754355295633311, 3.887192587735158, 4.022731918402185,
4.160988767090289, 4.301978482107941, 4.445716283538092,
4.592217266055746, 4.741496401646282, 4.893568542229298,
5.048448422192488, 5.20615066083972, 5.3666897647573375,
5.5300801301023865, 5.696336044816294, 5.865471690767354,
6.037501145825082, 6.212438385869475, 6.390297286737924,
6.571091626112461, 6.7548350853498045, 6.941541251256611,
7.131223617812143, 7.323895587840543, 7.5195704746346665,
7.7182615035334345, 7.919981813454504, 8.124744458384042,
8.332562408825165, 8.543448553206703, 8.757415699253682,
8.974476575321063, 9.194643831691977, 9.417930041841839,
9.644347703669503, 9.873909240696694, 10.106627003236781,
10.342513269534024, 10.58158024687427, 10.8238400726681,
11.069304815507364, 11.317986476196008, 11.569896988756009,
11.825048221409341, 12.083451977536606, 12.345119996613247,
12.610063955123938, 12.878295467455942, 13.149826086772048,
13.42466730586372, 13.702830557985108, 13.984327217668513,
14.269168601521828, 14.55736596900856, 14.848930523210871,
15.143873411576273, 15.44220572664832, 15.743938506781891,
16.04908273684337, 16.35764934889634, 16.66964922287304,
16.985093187232053, 17.30399201960269, 17.62635644741625,
17.95219714852476, 18.281524751807332, 18.614349837764564,
18.95068293910138, 19.290534541298456, 19.633915083172692,
19.98083495742689, 20.331304511189067, 20.685334046541502,
21.042933821039977, 21.404114048223256, 21.76888489811322,
22.137256497705877, 22.50923893145328, 22.884842241736916,
23.264076429332462, 23.6469514538663, 24.033477234264016,
24.42366364919083, 24.817520537484558, 25.21505769858089,
25.61628489293138, 26.021211842414342, 26.429848230738664,
26.842203703840827, 27.258287870275353, 27.678110301598522,
28.10168053274597, 28.529008062403893, 28.96010235337422,
29.39497283293396, 29.83362889318845, 30.276079891419332,
30.722335150426627, 31.172403958865512, 31.62629557157785,
32.08401920991837, 32.54558406207592, 33.010999283389665,
33.4802739966603, 33.953417292456834, 34.430438229418264,
34.911345834551085, 35.39614910352207, 35.88485700094671,
36.37747846067349, 36.87402238606382, 37.37449765026789,
37.87891309649659, 38.38727753828926, 38.89959975977785,
39.41588851594697, 39.93615253289054, 40.460400508064545,
40.98864111053629, 41.520882981230194, 42.05713473317016,
42.597404951718396, 43.141702194811224, 43.6900349931913,
44.24241185063697, 44.798841244188324, 45.35933162437017,
45.92389141541209, 46.49252901546552, 47.065252796817916,
47.64207110610409, 48.22299226451468, 48.808024568002054,
49.3971762874833, 49.9904556690408, 50.587870934119984,
51.189430279724725, 51.79514187861014, 52.40501387947288,
53.0190544071392, 53.637271562750364, 54.259673423945976,
54.88626804504493, 55.517063457223934, 56.15206766869424,
56.79128866487574, 57.43473440856916, 58.08241284012621,
58.734331877617365, 59.39049941699807, 60.05092333227251,
60.715611475655585, 61.38457167773311, 62.057811747619894,
62.7353394731159, 63.417162620860914, 64.10328893648692,
64.79372614476921, 65.48848194977529, 66.18756403501224,
66.89098006357258, 67.59873767827808, 68.31084450182222,
69.02730813691093, 69.74813616640164, 70.47333615344107,
71.20291564160104, 71.93688215501312, 72.67524319850172,
73.41800625771542, 74.16517879925733, 74.9167682708136,
75.67278210128072, 76.43322770089146, 77.1981124613393,
77.96744375590167, 78.74122893956174, 79.51947534912904,
80.30219030335869, 81.08938110306934, 81.88105503125999,
82.67721935322541, 83.4778813166706, 84.28304815182372,
85.09272707154808, 85.90692527145302, 86.72564993000343,
87.54890820862819, 88.3767072518277, 89.2090541872801,
90.04595612594655, 90.88742016217518, 91.73345337380438,
92.58406282226491, 93.43925555268066, 94.29903859396902,
95.16341895893969, 96.03240364439274, 96.9059996312159,
97.78421388448044, 98.6670533535366, 99.55452497210776,
}
// 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.
// Adapted from https://github.com/material-foundation/material-color-utilities
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hct
import (
"image/color"
"cogentcore.org/core/colors/cam/cie"
"cogentcore.org/core/math32"
)
const (
// ContrastAA is the contrast ratio required by WCAG AA for body text
ContrastAA float32 = 4.5
// ContrastLargeAA is the contrast ratio required by WCAG AA for large text
// (at least 120-150% larger than the body text)
ContrastLargeAA float32 = 3
// ContrastGraphicsAA is the contrast ratio required by WCAG AA for graphical objects
// and active user interface components like graphs, icons, and form input borders
ContrastGraphicsAA float32 = 3
// ContrastAAA is the contrast ratio required by WCAG AAA for body text
ContrastAAA float32 = 7
// ContrastLargeAAA is the contrast ratio required by WCAG AAA for large text
// (at least 120-150% larger than the body text)
ContrastLargeAAA float32 = 4.5
)
// ContrastRatio returns the contrast ratio between the given two colors.
// The contrast ratio will be between 1 and 21.
func ContrastRatio(a, b color.Color) float32 {
ah := FromColor(a)
bh := FromColor(b)
return ToneContrastRatio(ah.Tone, bh.Tone)
}
// ToneContrastRatio returns the contrast ratio between the given two tones.
// The contrast ratio will be between 1 and 21, and the tones should be
// between 0 and 100 and will be clamped to such.
func ToneContrastRatio(a, b float32) float32 {
a = math32.Clamp(a, 0, 100)
b = math32.Clamp(b, 0, 100)
return ContrastRatioOfYs(cie.LToY(a), cie.LToY(b))
}
// ContrastColor returns the color that will ensure that the given contrast ratio
// between the given color and the resulting color is met. If the given ratio can
// not be achieved with the given color, it returns the color that would result in
// the highest contrast ratio. The ratio must be between 1 and 21. If the tone of
// the given color is greater than 50, it tries darker tones first, and otherwise
// it tries lighter tones first.
func ContrastColor(c color.Color, ratio float32) color.RGBA {
h := FromColor(c)
ct := ContrastTone(h.Tone, ratio)
return h.WithTone(ct).AsRGBA()
}
// ContrastColorTry returns the color that will ensure that the given contrast ratio
// between the given color and the resulting color is met. It returns color.RGBA{}, false if
// the given ratio can not be achieved with the given color. The ratio must be between
// 1 and 21. If the tone of the given color is greater than 50, it tries darker tones first,
// and otherwise it tries lighter tones first.
func ContrastColorTry(c color.Color, ratio float32) (color.RGBA, bool) {
h := FromColor(c)
ct, ok := ContrastToneTry(h.Tone, ratio)
if !ok {
return color.RGBA{}, false
}
return h.WithTone(ct).AsRGBA(), true
}
// ContrastTone returns the tone that will ensure that the given contrast ratio
// between the given tone and the resulting tone is met. If the given ratio can
// not be achieved with the given tone, it returns the tone that would result in
// the highest contrast ratio. The tone must be between 0 and 100 and the ratio must be
// between 1 and 21. If the given tone is greater than 50, it tries darker tones first,
// and otherwise it tries lighter tones first.
func ContrastTone(tone, ratio float32) float32 {
ct, ok := ContrastToneTry(tone, ratio)
if ok {
return ct
}
dcr := ToneContrastRatio(tone, 0)
lcr := ToneContrastRatio(tone, 100)
if dcr > lcr {
return 0
}
return 100
}
// ContrastToneTry returns the tone that will ensure that the given contrast ratio
// between the given tone and the resulting tone is met. It returns -1, false if
// the given ratio can not be achieved with the given tone. The tone must be between 0
// and 100 and the ratio must be between 1 and 21. If the given tone is greater than 50,
// it tries darker tones first, and otherwise it tries lighter tones first.
func ContrastToneTry(tone, ratio float32) (float32, bool) {
if tone > 50 {
d, ok := ContrastToneDarkerTry(tone, ratio)
if ok {
return d, true
}
l, ok := ContrastToneLighterTry(tone, ratio)
if ok {
return l, true
}
return -1, false
}
l, ok := ContrastToneLighterTry(tone, ratio)
if ok {
return l, true
}
d, ok := ContrastToneDarkerTry(tone, ratio)
if ok {
return d, true
}
return -1, false
}
// ContrastToneLighter returns a tone greater than or equal to the given tone
// that ensures that given contrast ratio between the two tones is met.
// It returns 100 if the given ratio can not be achieved with the
// given tone. The tone must be between 0 and 100 and the ratio must be
// between 1 and 21.
func ContrastToneLighter(tone, ratio float32) float32 {
safe, ok := ContrastToneLighterTry(tone, ratio)
if ok {
return safe
}
return 100
}
// ContrastToneDarker returns a tone less than or equal to the given tone
// that ensures that given contrast ratio between the two tones is met.
// It returns 0 if the given ratio can not be achieved with the
// given tone. The tone must be between 0 and 100 and the ratio must be
// between 1 and 21.
func ContrastToneDarker(tone, ratio float32) float32 {
safe, ok := ContrastToneDarkerTry(tone, ratio)
if ok {
return safe
}
return 0
}
// ContrastToneLighterTry returns a tone greater than or equal to the given tone
// that ensures that given contrast ratio between the two tones is met.
// It returns -1, false if the given ratio can not be achieved with the
// given tone. The tone must be between 0 and 100 and the ratio must be
// between 1 and 21.
func ContrastToneLighterTry(tone, ratio float32) (float32, bool) {
if tone < 0 || tone > 100 {
return -1, false
}
darkY := cie.LToY(tone)
lightY := ratio*(darkY+5) - 5
realContrast := ContrastRatioOfYs(lightY, darkY)
delta := math32.Abs(realContrast - ratio)
if realContrast < ratio && delta > 0.04 {
return -1, false
}
// TODO(kai/cam): this +0.4 explained by the comment below only seems to cause problems
// Ensure gamut mapping, which requires a 'range' on tone, will still result
// the correct ratio by darkening slightly.
ret := cie.YToL(lightY) // + 0.4
if ret < 0 || ret > 100 {
return -1, false
}
return ret, true
}
// ContrastToneDarkerTry returns a tone less than or equal to the given tone
// that ensures that given contrast ratio between the two tones is met.
// It returns -1, false if the given ratio can not be achieved with the
// given tone. The tone must be between 0 and 100 and the ratio must be
// between 1 and 21.
func ContrastToneDarkerTry(tone, ratio float32) (float32, bool) {
if tone < 0 || tone > 100 {
return -1, false
}
lightY := cie.LToY(tone)
darkY := ((lightY + 5) / ratio) - 5
realContrast := ContrastRatioOfYs(lightY, darkY)
delta := math32.Abs(realContrast - ratio)
if realContrast < ratio && delta > 0.04 {
return -1, false
}
// TODO(kai/cam): this -0.4 explained by the comment below only seems to cause problems
// Ensure gamut mapping, which requires a 'range' on tone, will still result
// the correct ratio by darkening slightly.
ret := cie.YToL(darkY) // - 0.4
if ret < 0 || ret > 100 {
return -1, false
}
return ret, true
}
// ContrastRatioOfYs returns the contrast ratio of two XYZ Y values.
func ContrastRatioOfYs(a, b float32) float32 {
lighter := max(a, b)
darker := min(a, b)
return (lighter + 5) / (darker + 5)
}
// 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.
// Adapted from https://github.com/material-foundation/material-color-utilities
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hct
import (
"fmt"
"image/color"
"cogentcore.org/core/colors/cam/cam16"
"cogentcore.org/core/colors/cam/cie"
)
// HCT represents a color as hue, chroma, and tone. HCT is a color system
// that provides a perceptually accurate color measurement system that can
// also accurately render what colors will appear as in different lighting
// environments. Directly setting the values of the HCT and RGB fields will
// have no effect on the underlying color; instead, use the Set methods
// ([HCT.SetHue], etc). The A field (transparency) can be set directly.
type HCT struct {
// Hue (h) is the spectral identity of the color
// (red, green, blue etc) in degrees (0-360)
Hue float32 `min:"0" max:"360"`
// Chroma (C) is the colorfulness/saturation of the color.
// Grayscale colors have no chroma, and fully saturated ones
// have high chroma. The maximum varies as a function of hue
// and tone, but 120 is a general upper bound (see
// [HCT.MaximumChroma] to get a specific value).
Chroma float32 `min:"0" max:"120"`
// Tone is the L* component from the LAB (L*a*b*) color system,
// which is linear in human perception of lightness.
// It ranges from 0 to 100.
Tone float32 `min:"0" max:"100"`
// sRGB standard gamma-corrected 0-1 normalized RGB representation
// of the color. Critically, components are not premultiplied by alpha.
R, G, B, A float32
}
// New returns a new HCT representation for given parameters:
// hue = 0..360
// chroma = 0..? depends on other params
// tone = 0..100
// also computes and sets the sRGB normalized, gamma corrected RGB values
// while keeping the sRGB representation within its gamut,
// which may cause the chroma to decrease until it is inside the gamut.
func New(hue, chroma, tone float32) HCT {
r, g, b := SolveToRGB(hue, chroma, tone)
return SRGBToHCT(r, g, b)
}
// FromColor constructs a new HCT color from a standard [color.Color].
func FromColor(c color.Color) HCT {
return Uint32ToHCT(c.RGBA())
}
// SetColor sets the HCT color from a standard [color.Color].
func (h *HCT) SetColor(c color.Color) {
*h = FromColor(c)
}
// Model is the standard [color.Model] that converts colors to HCT.
var Model = color.ModelFunc(model)
func model(c color.Color) color.Color {
if h, ok := c.(HCT); ok {
return h
}
return FromColor(c)
}
// RGBA implements the color.Color interface.
// Performs the premultiplication of the RGB components by alpha at this point.
func (h HCT) RGBA() (r, g, b, a uint32) {
return cie.SRGBFloatToUint32(h.R, h.G, h.B, h.A)
}
// AsRGBA returns a standard color.RGBA type
func (h HCT) AsRGBA() color.RGBA {
r, g, b, a := cie.SRGBFloatToUint8(h.R, h.G, h.B, h.A)
return color.RGBA{r, g, b, a}
}
// SetUint32 sets components from unsigned 32bit integers (alpha-premultiplied)
func (h *HCT) SetUint32(r, g, b, a uint32) {
fr, fg, fb, fa := cie.SRGBUint32ToFloat(r, g, b, a)
*h = SRGBAToHCT(fr, fg, fb, fa)
}
// SetHue sets the hue of this color. Chroma may decrease because chroma has a
// different maximum for any given hue and tone.
// 0 <= hue < 360; invalid values are corrected.
func (h *HCT) SetHue(hue float32) *HCT {
r, g, b := SolveToRGB(hue, h.Chroma, h.Tone)
*h = SRGBAToHCT(r, g, b, h.A)
return h
}
// WithHue is like [SetHue] except it returns a new color
// instead of setting the existing one.
func (h HCT) WithHue(hue float32) HCT {
r, g, b := SolveToRGB(hue, h.Chroma, h.Tone)
return SRGBAToHCT(r, g, b, h.A)
}
// SetChroma sets the chroma of this color (0 to max that depends on other params),
// while keeping the sRGB representation within its gamut,
// which may cause the chroma to decrease until it is inside the gamut.
func (h *HCT) SetChroma(chroma float32) *HCT {
r, g, b := SolveToRGB(h.Hue, chroma, h.Tone)
*h = SRGBAToHCT(r, g, b, h.A)
return h
}
// WithChroma is like [SetChroma] except it returns a new color
// instead of setting the existing one.
func (h HCT) WithChroma(chroma float32) HCT {
r, g, b := SolveToRGB(h.Hue, chroma, h.Tone)
return SRGBAToHCT(r, g, b, h.A)
}
// SetTone sets the tone of this color (0 < tone < 100),
// while keeping the sRGB representation within its gamut,
// which may cause the chroma to decrease until it is inside the gamut.
func (h *HCT) SetTone(tone float32) *HCT {
r, g, b := SolveToRGB(h.Hue, h.Chroma, tone)
*h = SRGBAToHCT(r, g, b, h.A)
return h
}
// WithTone is like [SetTone] except it returns a new color
// instead of setting the existing one.
func (h HCT) WithTone(tone float32) HCT {
r, g, b := SolveToRGB(h.Hue, h.Chroma, tone)
return SRGBAToHCT(r, g, b, h.A)
}
// MaximumChroma returns the maximum [HCT.Chroma] value for the hue
// and tone of this color. This will always be between 0 and 120.
func (h HCT) MaximumChroma() float32 {
// WithChroma does a round trip, so the resultant chroma will only
// be as high as the maximum chroma.
return h.WithChroma(120).Chroma
}
// SRGBAToHCT returns an HCT from the given SRGBA color coordinates
// under standard viewing conditions. The RGB value range is 0-1,
// and RGB values have gamma correction. The RGB values must not be
// premultiplied by the given alpha value. See [SRGBToHCT] for
// a version that does not take the alpha value.
func SRGBAToHCT(r, g, b, a float32) HCT {
h := SRGBToHCT(r, g, b)
h.A = a
return h
}
// SRGBToHCT returns an HCT from the given SRGB color coordinates
// under standard viewing conditions. The RGB value range is 0-1,
// and RGB values have gamma correction. Alpha is always 1; see
// [SRGBAToHCT] for a version that takes the alpha value.
func SRGBToHCT(r, g, b float32) HCT {
x, y, z := cie.SRGBToXYZ(r, g, b)
cam := cam16.FromXYZ(100*x, 100*y, 100*z)
l, _, _ := cie.XYZToLAB(x, y, z)
return HCT{Hue: cam.Hue, Chroma: cam.Chroma, Tone: l, R: r, G: g, B: b, A: 1}
}
// Uint32ToHCT returns an HCT from given SRGBA uint32 color coordinates,
// which are used for interchange among image.Color types.
// Uses standard viewing conditions, and RGB values already have gamma correction
// (i.e., they are SRGB values).
func Uint32ToHCT(r, g, b, a uint32) HCT {
h := HCT{}
h.SetUint32(r, g, b, a)
return h
}
func (h HCT) String() string {
return fmt.Sprintf("hct(%g, %g, %g)", h.Hue, h.Chroma, h.Tone)
}
/*
// Translate a color into different [ViewingConditions].
//
// Colors change appearance. They look different with lights on versus off,
// the same color, as in hex code, on white looks different when on black.
// This is called color relativity, most famously explicated by Josef Albers
// in Interaction of Color.
//
// In color science, color appearance models can account for this and
// calculate the appearance of a color in different settings. HCT is based on
// CAM16, a color appearance model, and uses it to make these calculations.
//
// See [ViewingConditions.make] for parameters affecting color appearance.
Hct inViewingConditions(ViewingConditions vc) {
// 1. Use CAM16 to find XYZ coordinates of color in specified VC.
final cam16 = Cam16.fromInt(toInt());
final viewedInVc = cam16.xyzInViewingConditions(vc);
// 2. Create CAM16 of those XYZ coordinates in default VC.
final recastInVc = Cam16.fromXyzInViewingConditions(
viewedInVc[0],
viewedInVc[1],
viewedInVc[2],
ViewingConditions.make(),
);
// 3. Create HCT from:
// - CAM16 using default VC with XYZ coordinates in specified VC.
// - L* converted from Y in XYZ coordinates in specified VC.
final recastHct = Hct.from(
recastInVc.hue,
recastInVc.chroma,
ColorUtils.lstarFromY(viewedInVc[1]),
);
return recastHct;
}
}
*/
// 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.
// Adapted from https://github.com/material-foundation/material-color-utilities
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hct
import (
"cogentcore.org/core/colors/cam/cam16"
"cogentcore.org/core/colors/cam/cie"
"cogentcore.org/core/math32"
)
// SolveToRGBLin Finds an sRGB linear color (represented by math32.Vector3, 0-100 range)
// with the given hue, chroma, and tone, if possible.
// if not possible to represent the target values, the hue and tone will be
// sufficiently close, and chroma will be maximized.
func SolveToRGBLin(hue, chroma, tone float32) math32.Vector3 {
if chroma < 0.0001 || tone < 0.0001 || tone > 99.9999 {
y := cie.LToY(tone)
return math32.Vec3(y, y, y)
}
tone = math32.Clamp(tone, 0, 100)
hue_deg := cam16.SanitizeDegrees(hue)
hue_rad := math32.DegToRad(hue_deg)
y := cie.LToY(tone)
exact := FindResultByJ(hue_rad, chroma, y)
if exact != nil {
return *exact
}
return BisectToLimit(y, hue_rad)
}
// SolveToRGB Finds an sRGB (gamma corrected, 0-1 range) color
// with the given hue, chroma, and tone, if possible.
// if not possible to represent the target values, the hue and tone will be
// sufficiently close, and chroma will be maximized.
func SolveToRGB(hue, chroma, tone float32) (r, g, b float32) {
lin := SolveToRGBLin(hue, chroma, tone)
r, g, b = cie.SRGBFromLinear100(lin.X, lin.Y, lin.Z)
return
}
// Finds a color with the given hue, chroma, and Y.
// @param hue_radians The desired hue in radians.
// @param chroma The desired chroma.
// @param y The desired Y.
// @return The desired color as linear sRGB values.
func FindResultByJ(hue_rad, chroma, y float32) *math32.Vector3 {
// Initial estimate of j.
j := math32.Sqrt(y) * 11
// ===========================================================
// Operations inlined from Cam16 to avoid repeated calculation
// ===========================================================
vw := cam16.NewStdView()
t_inner_coeff := 1 / math32.Pow(1.64-math32.Pow(0.29, vw.BgYToWhiteY), 0.73)
e_hue := 0.25 * (math32.Cos(hue_rad+2) + 3.8)
p1 := e_hue * (50000 / 13) * vw.NC * vw.NCB
h_sin := math32.Sin(hue_rad)
h_cos := math32.Cos(hue_rad)
for itr := 0; itr < 5; itr++ {
j_norm := j / 100
alpha := float32(0)
if !(chroma == 0 || j == 0) {
alpha = chroma / math32.Sqrt(j_norm)
}
t := math32.Pow(alpha*t_inner_coeff, 1/0.9)
ac := vw.AW * math32.Pow(j_norm, 1/vw.C/vw.Z)
p2 := ac / vw.NBB
gamma := 23 * (p2 + 0.305) * t / (23*p1 + 11*t*h_cos + 108*t*h_sin)
a := gamma * h_cos
b := gamma * h_sin
r_a := (460*p2 + 451*a + 288*b) / 1403
g_a := (460*p2 - 891*a - 261*b) / 1403
b_a := (460*p2 - 220*a - 6300*b) / 1403
r_c_scaled := cam16.InverseChromaticAdapt(r_a)
g_c_scaled := cam16.InverseChromaticAdapt(g_a)
b_c_scaled := cam16.InverseChromaticAdapt(b_a)
scaled := math32.Vec3(r_c_scaled, g_c_scaled, b_c_scaled)
linrgb := MatMul(scaled, kLinrgbFromScaledDiscount)
if linrgb.X < 0 || linrgb.Y < 0 || linrgb.Z < 0 {
return nil
}
k_r := kYFromLinrgb[0]
k_g := kYFromLinrgb[1]
k_b := kYFromLinrgb[2]
fnj := k_r*linrgb.X + k_g*linrgb.Y + k_b*linrgb.Z
if fnj <= 0 {
return nil
}
if itr == 4 || math32.Abs(fnj-y) < 0.002 {
if linrgb.X > 100.01 || linrgb.Y > 100.01 || linrgb.Z > 100.01 {
return nil
}
return &linrgb
}
// Iterates with Newton method,
// Using 2 * fn(j) / j as the approximation of fn'(j)
j = j - (fnj-y)*j/(2*fnj)
}
return nil
}
// 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 hct
import (
"image/color"
"cogentcore.org/core/math32"
)
// Lighten returns a color that is lighter by the
// given absolute HCT tone amount (0-100, ranges enforced)
func Lighten(c color.Color, amount float32) color.RGBA {
h := FromColor(c)
h.SetTone(h.Tone + amount)
return h.AsRGBA()
}
// Darken returns a color that is darker by the
// given absolute HCT tone amount (0-100, ranges enforced)
func Darken(c color.Color, amount float32) color.RGBA {
h := FromColor(c)
h.SetTone(h.Tone - amount)
return h.AsRGBA()
}
// Highlight returns a color that is lighter or darker by the
// given absolute HCT tone amount (0-100, ranges enforced),
// making the color darker if it is light (tone >= 50) and
// lighter otherwise. It is the opposite of [Samelight].
func Highlight(c color.Color, amount float32) color.RGBA {
h := FromColor(c)
if h.Tone >= 50 {
h.SetTone(h.Tone - amount)
} else {
h.SetTone(h.Tone + amount)
}
return h.AsRGBA()
}
// Samelight returns a color that is lighter or darker by the
// given absolute HCT tone amount (0-100, ranges enforced),
// making the color lighter if it is light (tone >= 50) and
// darker otherwise. It is the opposite of [Highlight].
func Samelight(c color.Color, amount float32) color.RGBA {
h := FromColor(c)
if h.Tone >= 50 {
h.SetTone(h.Tone + amount)
} else {
h.SetTone(h.Tone - amount)
}
return h.AsRGBA()
}
// Saturate returns a color that is more saturated by the
// given absolute HCT chroma amount (0-max that depends
// on other params but is around 150, ranges enforced)
func Saturate(c color.Color, amount float32) color.RGBA {
h := FromColor(c)
h.SetChroma(h.Chroma + amount)
return h.AsRGBA()
}
// Desaturate returns a color that is less saturated by the
// given absolute HCT chroma amount (0-max that depends
// on other params but is around 150, ranges enforced)
func Desaturate(c color.Color, amount float32) color.RGBA {
h := FromColor(c)
h.SetChroma(h.Chroma - amount)
return h.AsRGBA()
}
// Spin returns a color that has a different hue by the
// given absolute HCT hue amount (±0-360, ranges enforced)
func Spin(c color.Color, amount float32) color.RGBA {
h := FromColor(c)
h.SetHue(h.Hue + amount)
return h.AsRGBA()
}
// MinHueDistance finds the minimum distance between two hues.
// A positive number means add to a to get to b.
// A negative number means subtract from a to get to b.
func MinHueDistance(a, b float32) float32 {
d1 := b - a
d2 := (b + 360) - a
d3 := (b - (a + 360))
d1a := math32.Abs(d1)
d2a := math32.Abs(d2)
d3a := math32.Abs(d3)
if d1a < d2a && d1a < d3a {
return d1
}
if d2a < d1a && d2a < d3a {
return d2
}
return d3
}
// Blend returns a color that is the given percent blend between the first
// and second color; 10 = 10% of the first and 90% of the second, etc;
// blending is done directly on non-premultiplied HCT values, and
// a correctly premultiplied color is returned.
func Blend(pct float32, x, y color.Color) color.RGBA {
hx := FromColor(x)
hy := FromColor(y)
pct = math32.Clamp(pct, 0, 100)
px := pct / 100
py := 1 - px
dhue := MinHueDistance(hx.Hue, hy.Hue)
// weight as a function of chroma strength: if near grey, hue is unreliable
cpy := py * hy.Chroma / (px*hx.Chroma + py*hy.Chroma)
hue := hx.Hue + cpy*dhue
chroma := px*hx.Chroma + py*hy.Chroma
tone := px*hx.Tone + py*hy.Tone
hr := New(hue, chroma, tone)
hr.A = px*hx.A + py*hy.A
return hr.AsRGBA()
}
// IsLight returns whether the given color is light
// (has an HCT tone greater than or equal to 50)
func IsLight(c color.Color) bool {
h := FromColor(c)
return h.Tone >= 50
}
// IsDark returns whether the given color is dark
// (has an HCT tone less than 50)
func IsDark(c color.Color) bool {
h := FromColor(c)
return h.Tone < 50
}
// Copyright (c) 2021, 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 hpe
import "cogentcore.org/core/colors/cam/cie"
// XYZToLMS convert XYZ to Long, Medium, Short cone-based responses,
// using the Hunt-Pointer-Estevez transform.
// This is closer to the actual response functions of the L,M,S cones apparently.
func XYZToLMS(x, y, z float32) (l, m, s float32) {
l = 0.38971*x + 0.68898*y + -0.07868*z
m = -0.22981*x + 1.18340*y + 0.04641*z
s = z
return
}
// SRGBLinToLMS converts sRGB linear to Long, Medium, Short cone-based responses,
// using the Hunt-Pointer-Estevez transform.
// This is closer to the actual response functions of the L,M,S cones apparently.
func SRGBLinToLMS(rl, gl, bl float32) (l, m, s float32) {
l = 0.30567503*rl + 0.62274014*gl + 0.04530167*bl
m = 0.15771291*rl + 0.7697197*gl + 0.08807348*bl
s = 0.0193*rl + 0.1192*gl + 0.9505*bl
return
}
// SRGBToLMS converts sRGB to Long, Medium, Short cone-based responses,
// using the Hunt-Pointer-Estevez transform.
// This is closer to the actual response functions of the L,M,S cones apparently.
func SRGBToLMS(r, g, b float32) (l, m, s float32) {
rl, gl, bl := cie.SRGBToLinear(r, g, b)
l, m, s = SRGBLinToLMS(rl, gl, bl)
return
}
/*
func LMStoXYZ(float& X, float& Y, float& Z,
L, M, S) {
X = 1.096124f * L + 0.4296f * Y + -0.1624f * Z;
Y = -0.7036f * X + 1.6975f * Y + 0.0061f * Z;
Z = 0.0030f * X + 0.0136f * Y + 0.9834 * Z;
}
// convert Long, Medium, Short cone-based responses to XYZ, using the Hunt-Pointer-Estevez transform -- this is closer to the actual response functions of the L,M,S cones apparently
*/
// 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 hsl
import (
"fmt"
"image/color"
"cogentcore.org/core/math32"
)
// HSL represents the Hue [0..360], Saturation [0..1], and Luminance
// (lightness) [0..1] of the color using float32 values
// In general the Alpha channel is not used for HSL but is maintained
// so it can be used to fully represent an RGBA color value.
// When converting to RGBA, alpha multiplies the RGB components.
type HSL struct {
// the hue of the color
H float32 `min:"0" max:"360" step:"5"`
// the saturation of the color
S float32 `min:"0" max:"1" step:"0.05"`
// the luminance (lightness) of the color
L float32 `min:"0" max:"1" step:"0.05"`
// the transparency of the color
A float32 `min:"0" max:"1" step:"0.05"`
}
// New returns a new HSL representation for given parameters:
// hue = 0..360
// saturation = 0..1
// lightness = 0..1
// A is automatically set to 1
func New(hue, saturation, lightness float32) HSL {
return HSL{hue, saturation, lightness, 1}
}
// FromColor constructs a new HSL color from a standard [color.Color]
func FromColor(c color.Color) HSL {
h := HSL{}
h.SetColor(c)
return h
}
// Model is the standard [color.Model] that converts colors to HSL.
var Model = color.ModelFunc(model)
func model(c color.Color) color.Color {
if h, ok := c.(HSL); ok {
return h
}
return FromColor(c)
}
// Implements the [color.Color] interface
// Performs the premultiplication of the RGB components by alpha at this point.
func (h HSL) RGBA() (r, g, b, a uint32) {
fr, fg, fb := HSLtoRGBF32(h.H, h.S, h.L)
r = uint32(fr*h.A*65535.0 + 0.5)
g = uint32(fg*h.A*65535.0 + 0.5)
b = uint32(fb*h.A*65535.0 + 0.5)
a = uint32(h.A*65535.0 + 0.5)
return
}
// AsRGBA returns a standard color.RGBA type
func (h HSL) AsRGBA() color.RGBA {
fr, fg, fb := HSLtoRGBF32(h.H, h.S, h.L)
return color.RGBA{uint8(fr*h.A*255.0 + 0.5), uint8(fg*h.A*255.0 + 0.5), uint8(fb*h.A*255.0 + 0.5), uint8(h.A*255.0 + 0.5)}
}
// SetUint32 sets components from unsigned 32bit integers (alpha-premultiplied)
func (h *HSL) SetUint32(r, g, b, a uint32) {
fa := float32(a) / 65535.0
fr := (float32(r) / 65535.0) / fa
fg := (float32(g) / 65535.0) / fa
fb := (float32(b) / 65535.0) / fa
h.H, h.S, h.L = RGBtoHSLF32(fr, fg, fb)
h.A = fa
}
// SetColor sets from a standard color.Color
func (h *HSL) SetColor(ci color.Color) {
if ci == nil {
*h = HSL{}
return
}
r, g, b, a := ci.RGBA()
h.SetUint32(r, g, b, a)
}
// HSLtoRGBF32 converts HSL values to RGB float32 0..1 values (non alpha-premultiplied) -- based on https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion, https://www.w3.org/TR/css-color-3/ and github.com/lucasb-eyer/go-colorful
func HSLtoRGBF32(h, s, l float32) (r, g, b float32) {
if s == 0 {
r = l
g = l
b = l
return
}
h = h / 360.0 // convert to normalized 0-1 h
var q float32
if l < 0.5 {
q = l * (1.0 + s)
} else {
q = l + s - l*s
}
p := 2.0*l - q
r = HueToRGBF32(p, q, h+1.0/3.0)
g = HueToRGBF32(p, q, h)
b = HueToRGBF32(p, q, h-1.0/3.0)
return
}
func HueToRGBF32(p, q, t float32) float32 {
if t < 0 {
t++
}
if t > 1 {
t--
}
if t < 1.0/6.0 {
return p + (q-p)*6.0*t
}
if t < .5 {
return q
}
if t < 2.0/3.0 {
return p + (q-p)*(2.0/3.0-t)*6.0
}
return p
}
// RGBtoHSLF32 converts RGB 0..1 values (non alpha-premultiplied) to HSL -- based on https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion, https://www.w3.org/TR/css-color-3/ and github.com/lucasb-eyer/go-colorful
func RGBtoHSLF32(r, g, b float32) (h, s, l float32) {
min := math32.Min(math32.Min(r, g), b)
max := math32.Max(math32.Max(r, g), b)
l = (max + min) / 2.0
if min == max {
s = 0
h = 0
} else {
d := max - min
if l > 0.5 {
s = d / (2.0 - max - min)
} else {
s = d / (max + min)
}
switch max {
case r:
h = (g - b) / d
if g < b {
h += 6.0
}
case g:
h = 2.0 + (b-r)/d
case b:
h = 4.0 + (r-g)/d
}
h *= 60
if h < 0 {
h += 360
}
}
return
}
func (h HSL) String() string {
return fmt.Sprintf("hsl(%g, %g, %g)", h.H, h.S, h.L)
}
// 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 hsl
import (
"image/color"
"cogentcore.org/core/math32"
)
// Lighten returns a color that is lighter by the
// given absolute HSL lightness amount (0-100, ranges enforced)
func Lighten(c color.Color, amount float32) color.RGBA {
h := FromColor(c)
h.L += amount / 100
h.L = math32.Clamp(h.L, 0, 1)
return h.AsRGBA()
}
// Darken returns a color that is darker by the
// given absolute HSL lightness amount (0-100, ranges enforced)
func Darken(c color.Color, amount float32) color.RGBA {
h := FromColor(c)
h.L -= amount / 100
h.L = math32.Clamp(h.L, 0, 1)
return h.AsRGBA()
}
// Highlight returns a color that is lighter or darker by the
// given absolute HSL lightness amount (0-100, ranges enforced),
// making the color darker if it is light (tone >= 0.5) and
// lighter otherwise. It is the opposite of [Samelight].
func Highlight(c color.Color, amount float32) color.RGBA {
h := FromColor(c)
if h.L >= 0.5 {
h.L -= amount / 100
} else {
h.L += amount / 100
}
h.L = math32.Clamp(h.L, 0, 1)
return h.AsRGBA()
}
// Samelight returns a color that is lighter or darker by the
// given absolute HSL lightness amount (0-100, ranges enforced),
// making the color lighter if it is light (tone >= 0.5) and
// darker otherwise. It is the opposite of [Highlight].
func Samelight(c color.Color, amount float32) color.RGBA {
h := FromColor(c)
if h.L >= 0.5 {
h.L += amount / 100
} else {
h.L -= amount / 100
}
h.L = math32.Clamp(h.L, 0, 1)
return h.AsRGBA()
}
// Saturate returns a color that is more saturated by the
// given absolute HSL saturation amount (0-100, ranges enforced)
func Saturate(c color.Color, amount float32) color.RGBA {
h := FromColor(c)
h.S += amount / 100
h.S = math32.Clamp(h.S, 0, 1)
return h.AsRGBA()
}
// Desaturate returns a color that is less saturated by the
// given absolute HSL saturation amount (0-100, ranges enforced)
func Desaturate(c color.Color, amount float32) color.RGBA {
h := FromColor(c)
h.S -= amount / 100
h.S = math32.Clamp(h.S, 0, 1)
return h.AsRGBA()
}
// Spin returns a color that has a different hue by the
// given absolute HSL hue amount (0-360, ranges enforced)
func Spin(c color.Color, amount float32) color.RGBA {
h := FromColor(c)
h.H += amount
h.H = math32.Clamp(h.H, 0, 360)
return h.AsRGBA()
}
// IsLight returns whether the given color is light
// (has an HSL lightness greater than or equal to 0.6)
func IsLight(c color.Color) bool {
h := FromColor(c)
return h.L >= 0.6
}
// IsDark returns whether the given color is dark
// (has an HSL lightness less than 0.6)
func IsDark(c color.Color) bool {
h := FromColor(c)
return h.L < 0.6
}
// ContrastColor returns the color that should
// be used to contrast this color (white or black),
// based on the result of [IsLight].
func ContrastColor(c color.Color) color.RGBA {
if IsLight(c) {
return color.RGBA{0, 0, 0, 255}
}
return color.RGBA{255, 255, 255, 255}
}
// Code generated by "core generate"; DO NOT EDIT.
package lms
import (
"cogentcore.org/core/enums"
)
var _OpponentsValues = []Opponents{0, 1, 2}
// OpponentsN is the highest valid value for type Opponents, plus one.
const OpponentsN Opponents = 3
var _OpponentsValueMap = map[string]Opponents{`WhiteBlack`: 0, `RedGreen`: 1, `BlueYellow`: 2}
var _OpponentsDescMap = map[Opponents]string{0: `White vs. Black greyscale`, 1: `Red vs. Green`, 2: `Blue vs. Yellow`}
var _OpponentsMap = map[Opponents]string{0: `WhiteBlack`, 1: `RedGreen`, 2: `BlueYellow`}
// String returns the string representation of this Opponents value.
func (i Opponents) String() string { return enums.String(i, _OpponentsMap) }
// SetString sets the Opponents value from its string representation,
// and returns an error if the string is invalid.
func (i *Opponents) SetString(s string) error {
return enums.SetString(i, s, _OpponentsValueMap, "Opponents")
}
// Int64 returns the Opponents value as an int64.
func (i Opponents) Int64() int64 { return int64(i) }
// SetInt64 sets the Opponents value from an int64.
func (i *Opponents) SetInt64(in int64) { *i = Opponents(in) }
// Desc returns the description of the Opponents value.
func (i Opponents) Desc() string { return enums.Desc(i, _OpponentsDescMap) }
// OpponentsValues returns all possible values for the type Opponents.
func OpponentsValues() []Opponents { return _OpponentsValues }
// Values returns all possible values for the type Opponents.
func (i Opponents) Values() []enums.Enum { return enums.Values(_OpponentsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Opponents) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Opponents) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "Opponents")
}
// Copyright (c) 2021, 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 lms
//go:generate core generate
// OpponentValues holds color opponent values based on cone-like L,M,S inputs
// These values are useful for generating inputs to vision models that
// simulate color opponency representations in the brain.
type OpponentValues struct {
// red vs. green (long vs. medium)
RedGreen float32
// blue vs. yellow (short vs. avg(long, medium))
BlueYellow float32
// greyscale luminance channel -- typically use L* from LAB as best
Grey float32
}
// NewOpponentValues returns a new [OpponentValues] from values representing
// the LMS long, medium, short cone responses, and an overall grey value.
func NewOpponentValues(l, m, s, lm, grey float32) OpponentValues {
return OpponentValues{RedGreen: l - m, BlueYellow: s - lm, Grey: grey}
}
// Opponents enumerates the three primary opponency channels:
// [WhiteBlack], [RedGreen], and [BlueYellow] using colloquial
// "everyday" terms.
type Opponents int32 //enums:enum
const (
// White vs. Black greyscale
WhiteBlack Opponents = iota
// Red vs. Green
RedGreen
// Blue vs. Yellow
BlueYellow
)
// Copyright (c) 2019, 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 colormap
import (
"image/color"
"maps"
"sort"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
)
// Map maps a value onto a color by interpolating between a list of colors
// defining a spectrum, or optionally as an indexed list of colors.
type Map struct {
// Name is the name of the color map
Name string
// if true, this map should be used as an indexed list instead of interpolating a normalized floating point value: requires caller to check this flag and pass int indexes instead of normalized values to MapIndex
Indexed bool
// the colorspace algorithm to use for blending colors
Blend colors.BlendTypes
// color to display for invalid numbers (e.g., NaN)
NoColor color.RGBA
// list of colors to interpolate between
Colors []color.RGBA
}
func (cm *Map) String() string {
return cm.Name
}
// Map returns color for normalized value in range 0-1. NaN returns NoColor
// which can be used to indicate missing values.
func (cm *Map) Map(val float32) color.RGBA {
nc := len(cm.Colors)
if nc == 0 {
return color.RGBA{}
}
if nc == 1 {
return cm.Colors[0]
}
if math32.IsNaN(val) {
return cm.NoColor
}
if val <= 0 {
return cm.Colors[0]
} else if val >= 1 {
return cm.Colors[nc-1]
}
ival := val * float32(nc-1)
lidx := math32.Floor(ival)
uidx := math32.Ceil(ival)
if lidx == uidx {
return cm.Colors[int(lidx)]
}
cmix := 100 * (1 - (ival - lidx))
lclr := cm.Colors[int(lidx)]
uclr := cm.Colors[int(uidx)]
return colors.Blend(cm.Blend, cmix, lclr, uclr)
}
// MapIndex returns color for given index, for scale in Indexed mode.
// NoColor is returned for values out of range of available colors.
// It is responsibility of the caller to use this method instead of Map
// based on the Indexed flag.
func (cm *Map) MapIndex(val int) color.RGBA {
nc := len(cm.Colors)
if val < 0 || val >= nc {
return cm.NoColor
}
return cm.Colors[val]
}
// see https://matplotlib.org/tutorials/colors/colormap-manipulation.html
// for how to read out matplotlib scales -- still don't understand segmented ones!
// StandardMaps is a list of standard color maps
var StandardMaps = map[string]*Map{
"ColdHot": {
Name: "ColdHot",
NoColor: colors.FromRGB(200, 200, 200),
Colors: []color.RGBA{
{0, 255, 255, 255},
{0, 0, 255, 255},
{127, 127, 127, 255},
{255, 0, 0, 255},
{255, 255, 0, 255},
},
},
"Jet": {
Name: "Jet",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{0, 0, 127, 255},
{0, 0, 255, 255},
{0, 127, 255, 255},
{0, 255, 255, 255},
{127, 255, 127, 255},
{255, 255, 0, 255},
{255, 127, 0, 255},
{255, 0, 0, 255},
{127, 0, 0, 255},
},
},
"JetMuted": {
Name: "JetMuted",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{25, 25, 153, 255},
{25, 102, 230, 255},
{0, 230, 230, 255},
{0, 179, 0, 255},
{230, 230, 0, 255},
{230, 102, 25, 255},
{153, 25, 25, 255},
},
},
"Viridis": {
Name: "Viridis",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{72, 33, 114, 255},
{67, 62, 133, 255},
{56, 87, 140, 255},
{45, 111, 142, 255},
{36, 133, 142, 255},
{30, 155, 138, 255},
{42, 176, 127, 255},
{81, 197, 105, 255},
{134, 212, 73, 255},
{194, 223, 35, 255},
{253, 231, 37, 255},
},
},
"Plasma": {
Name: "Plasma",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{61, 4, 155, 255},
{99, 0, 167, 255},
{133, 6, 166, 255},
{166, 32, 152, 255},
{192, 58, 131, 255},
{213, 84, 110, 255},
{231, 111, 90, 255},
{246, 141, 69, 255},
{253, 174, 50, 255},
{252, 210, 36, 255},
{240, 248, 33, 255},
},
},
"Inferno": {
Name: "Inferno",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{37, 12, 3, 255},
{19, 11, 52, 255},
{57, 9, 99, 255},
{95, 19, 110, 255},
{133, 33, 107, 255},
{169, 46, 94, 255},
{203, 65, 73, 255},
{230, 93, 47, 255},
{247, 131, 17, 255},
{252, 174, 19, 255},
{245, 219, 76, 255},
{252, 254, 164, 255},
},
},
"BlueRed": {
Name: "BlueRed",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{0, 0, 255, 255},
{255, 0, 0, 255},
},
},
"BlueBlackRed": {
Name: "BlueBlackRed",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{0, 0, 255, 255},
{76, 76, 76, 255},
{255, 0, 0, 255},
},
},
"BlueGreyRed": {
Name: "BlueGreyRed",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{0, 0, 255, 255},
{127, 127, 127, 255},
{255, 0, 0, 255},
},
},
"BlueWhiteRed": {
Name: "BlueWhiteRed",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{0, 0, 255, 255},
{230, 230, 230, 255},
{255, 0, 0, 255},
},
},
"BlueGreenRed": {
Name: "BlueGreenRed",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{0, 0, 255, 255},
{0, 230, 0, 255},
{255, 0, 0, 255},
},
},
"Rainbow": {
Name: "Rainbow",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{255, 0, 255, 255},
{0, 0, 255, 255},
{0, 255, 0, 255},
{255, 255, 0, 255},
{255, 0, 0, 255},
},
},
"ROYGBIV": {
Name: "ROYGBIV",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{255, 0, 255, 255},
{0, 0, 127, 255},
{0, 0, 255, 255},
{0, 255, 0, 255},
{255, 255, 0, 255},
{255, 0, 0, 255},
},
},
"DarkLight": {
Name: "DarkLight",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{0, 0, 0, 255},
{250, 250, 250, 255},
},
},
"DarkLightDark": {
Name: "DarkLightDark",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{0, 0, 0, 255},
{250, 250, 250, 255},
{0, 0, 0, 255},
},
},
"LightDarkLight": {
Name: "DarkLightDark",
NoColor: color.RGBA{200, 200, 200, 255},
Colors: []color.RGBA{
{250, 250, 250, 255},
{0, 0, 0, 255},
{250, 250, 250, 255},
},
},
}
// AvailableMaps is the list of all available color maps
var AvailableMaps = map[string]*Map{}
func init() {
maps.Copy(AvailableMaps, StandardMaps)
}
// AvailableMapsList returns a sorted list of color map names, e.g., for choosers
func AvailableMapsList() []string {
sl := make([]string, len(AvailableMaps))
ctr := 0
for k := range AvailableMaps {
sl[ctr] = k
ctr++
}
sort.Strings(sl)
return sl
}
// 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 colors provides named colors, utilities for manipulating colors,
// and Material Design 3 color schemes, palettes, and keys in Go.
package colors
//go:generate core generate
import (
"errors"
"fmt"
"image"
"image/color"
"strconv"
"strings"
"cogentcore.org/core/colors/cam/hct"
"cogentcore.org/core/colors/cam/hsl"
"cogentcore.org/core/math32"
)
// IsNil returns whether the color is the nil initial default color
func IsNil(c color.Color) bool {
return AsRGBA(c) == color.RGBA{}
}
// FromRGB makes a new RGBA color from the given
// RGB uint8 values, using 255 for A.
func FromRGB(r, g, b uint8) color.RGBA {
return color.RGBA{r, g, b, 255}
}
// FromNRGBA makes a new RGBA color from the given
// non-alpha-premultiplied RGBA uint8 values.
func FromNRGBA(r, g, b, a uint8) color.RGBA {
return AsRGBA(color.NRGBA{r, g, b, a})
}
// AsRGBA returns the given color as an RGBA color
func AsRGBA(c color.Color) color.RGBA {
if c == nil {
return color.RGBA{}
}
return color.RGBAModel.Convert(c).(color.RGBA)
}
// FromFloat64 makes a new RGBA color from the given 0-1
// normalized floating point numbers (alpha-premultiplied)
func FromFloat64(r, g, b, a float64) color.RGBA {
return color.RGBA{uint8(r * 255), uint8(g * 255), uint8(b * 255), uint8(a * 255)}
}
// FromFloat32 makes a new RGBA color from the given 0-1
// normalized floating point numbers (alpha-premultiplied)
func FromFloat32(r, g, b, a float32) color.RGBA {
return color.RGBA{uint8(r * 255), uint8(g * 255), uint8(b * 255), uint8(a * 255)}
}
// ToFloat32 returns 0-1 normalized floating point numbers from given color
// (alpha-premultiplied)
func ToFloat32(c color.Color) (r, g, b, a float32) {
f := NRGBAF32Model.Convert(c).(NRGBAF32)
r = f.R
g = f.G
b = f.B
a = f.A
return
}
// ToFloat64 returns 0-1 normalized floating point numbers from given color
// (alpha-premultiplied)
func ToFloat64(c color.Color) (r, g, b, a float64) {
f := NRGBAF32Model.Convert(c).(NRGBAF32)
r = float64(f.R)
g = float64(f.G)
b = float64(f.B)
a = float64(f.A)
return
}
// AsString returns the given color as a string,
// using its String method if it exists, and formatting
// it as rgba(r, g, b, a) otherwise.
func AsString(c color.Color) string {
if s, ok := c.(fmt.Stringer); ok {
return s.String()
}
r := AsRGBA(c)
return fmt.Sprintf("rgba(%d, %d, %d, %d)", r.R, r.G, r.B, r.A)
}
// FromName returns the color value specified
// by the given CSS standard color name.
func FromName(name string) (color.RGBA, error) {
c, ok := Map[name]
if !ok {
return color.RGBA{}, errors.New("colors.FromName: name not found: " + name)
}
return c, nil
}
// FromString returns a color value from the given string.
// FromString accepts the following types of strings: standard
// color names, hex, rgb, rgba, hsl, hsla, hct, and hcta values,
// "none" or "off", or any of the transformations listed below.
// The transformations use the given single base color as their starting
// point; if you do not provide a base color, they will use [Transparent]
// as their starting point. The transformations are:
//
// - currentcolor = base color
// - inverse = inverse of base color
// - lighten-VAL or darken-VAL: VAL is amount to lighten or darken (using HCT), e.g., lighter-10 is 10 higher tone
// - saturate-VAL or desaturate-VAL: manipulates the chroma level in HCT by VAL
// - spin-VAL: manipulates the hue level in HCT by VAL
// - clearer-VAL or opaquer-VAL: manipulates the alpha level by VAL
// - blend-VAL-color: blends given percent of given color relative to base in RGB space
func FromString(str string, base ...color.Color) (color.RGBA, error) {
if len(str) == 0 { // consider it null
return color.RGBA{}, nil
}
lstr := strings.ToLower(str)
switch {
case lstr[0] == '#':
return FromHex(str)
case strings.HasPrefix(lstr, "rgb("), strings.HasPrefix(lstr, "rgba("):
val := lstr[strings.Index(lstr, "(")+1:]
val = strings.TrimRight(val, ")")
val = strings.Trim(val, "%")
var r, g, b, a int
a = 255
if strings.Count(val, ",") == 3 {
format := "%d,%d,%d,%d"
fmt.Sscanf(val, format, &r, &g, &b, &a)
} else {
format := "%d,%d,%d"
fmt.Sscanf(val, format, &r, &g, &b)
}
return FromNRGBA(uint8(r), uint8(g), uint8(b), uint8(a)), nil
case strings.HasPrefix(lstr, "hsl("), strings.HasPrefix(lstr, "hsla("):
val := lstr[strings.Index(lstr, "(")+1:]
val = strings.TrimRight(val, ")")
val = strings.Trim(val, "%")
var h, s, l, a int
a = 255
if strings.Count(val, ",") == 3 {
format := "%d,%d,%d,%d"
fmt.Sscanf(val, format, &h, &s, &l, &a)
} else {
format := "%d,%d,%d"
fmt.Sscanf(val, format, &h, &s, &l)
}
return WithA(hsl.New(float32(h), float32(s)/100.0, float32(l)/100.0), uint8(a)), nil
case strings.HasPrefix(lstr, "hct("), strings.HasPrefix(lstr, "hcta("):
val := lstr[strings.Index(lstr, "(")+1:]
val = strings.TrimRight(val, ")")
val = strings.Trim(val, "%")
var h, c, t, a int
a = 255
if strings.Count(val, ",") == 3 {
format := "%d,%d,%d,%d"
fmt.Sscanf(val, format, &h, &c, &t, &a)
} else {
format := "%d,%d,%d"
fmt.Sscanf(val, format, &h, &c, &t)
}
return WithA(hct.New(float32(h), float32(c), float32(t)), uint8(a)), nil
default:
var bc color.Color = Transparent
if len(base) > 0 {
bc = base[0]
}
if hidx := strings.Index(lstr, "-"); hidx > 0 {
cmd := lstr[:hidx]
valstr := lstr[hidx+1:]
val64, err := strconv.ParseFloat(valstr, 32)
if err != nil && cmd != "blend" { // blend handles separately
return color.RGBA{}, fmt.Errorf("colors.FromString: error getting numeric value from %q: %w", valstr, err)
}
val := float32(val64)
switch cmd {
case "lighten":
return hct.Lighten(bc, val), nil
case "darken":
return hct.Darken(bc, val), nil
case "highlight":
return hct.Highlight(bc, val), nil
case "samelight":
return hct.Samelight(bc, val), nil
case "saturate":
return hct.Saturate(bc, val), nil
case "desaturate":
return hct.Desaturate(bc, val), nil
case "spin":
return hct.Spin(bc, val), nil
case "clearer":
return Clearer(bc, val), nil
case "opaquer":
return Opaquer(bc, val), nil
case "blend":
clridx := strings.Index(valstr, "-")
if clridx < 0 {
return color.RGBA{}, fmt.Errorf("colors.FromString: blend color spec not found; format is: blend-PCT-color, got: %v; PCT-color is: %v", lstr, valstr)
}
bvalstr := valstr[:clridx]
val64, err := strconv.ParseFloat(bvalstr, 32)
if err != nil {
return color.RGBA{}, fmt.Errorf("colors.FromString: error getting numeric value from %q: %w", bvalstr, err)
}
val := float32(val64)
clrstr := valstr[clridx+1:]
othc, err := FromString(clrstr, bc)
return BlendRGB(val, bc, othc), err
}
}
switch lstr {
case "none", "off":
return color.RGBA{}, nil
case "transparent":
return Transparent, nil
case "currentcolor":
return AsRGBA(bc), nil
case "inverse":
return Inverse(bc), nil
default:
return FromName(lstr)
}
}
}
// FromAny returns a color from the given value of any type.
// It handles values of types string, [color.Color], [*color.Color],
// [image.Image], and [*image.Image]. It takes an optional base color
// for relative transformations
// (see [FromString]).
func FromAny(val any, base ...color.Color) (color.RGBA, error) {
switch vv := val.(type) {
case string:
return FromString(vv, base...)
case color.Color:
return AsRGBA(vv), nil
case *color.Color:
return AsRGBA(*vv), nil
case image.Image:
return ToUniform(vv), nil
case *image.Image:
return ToUniform(*vv), nil
default:
return color.RGBA{}, fmt.Errorf("colors.FromAny: could not get color from value %v of type %T", val, val)
}
}
// FromHex parses the given non-alpha-premultiplied hex color string
// and returns the resulting alpha-premultiplied color.
func FromHex(hex string) (color.RGBA, error) {
hex = strings.TrimPrefix(hex, "#")
var r, g, b, a int
a = 255
if len(hex) == 3 {
format := "%1x%1x%1x"
fmt.Sscanf(hex, format, &r, &g, &b)
r |= r << 4
g |= g << 4
b |= b << 4
} else if len(hex) == 6 {
format := "%02x%02x%02x"
fmt.Sscanf(hex, format, &r, &g, &b)
} else if len(hex) == 8 {
format := "%02x%02x%02x%02x"
fmt.Sscanf(hex, format, &r, &g, &b, &a)
} else {
return color.RGBA{}, fmt.Errorf("colors.FromHex: could not process %q", hex)
}
return AsRGBA(color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)}), nil
}
// AsHex returns the color as a standard 2-hexadecimal-digits-per-component
// non-alpha-premultiplied hex color string.
func AsHex(c color.Color) string {
if c == nil {
return "nil"
}
r := color.NRGBAModel.Convert(c).(color.NRGBA)
if r.A == 255 {
return fmt.Sprintf("#%02X%02X%02X", r.R, r.G, r.B)
}
return fmt.Sprintf("#%02X%02X%02X%02X", r.R, r.G, r.B, r.A)
}
// WithR returns the given color with the red
// component (R) set to the given alpha-premultiplied value
func WithR(c color.Color, r uint8) color.RGBA {
rc := AsRGBA(c)
rc.R = r
return rc
}
// WithG returns the given color with the green
// component (G) set to the given alpha-premultiplied value
func WithG(c color.Color, g uint8) color.RGBA {
rc := AsRGBA(c)
rc.G = g
return rc
}
// WithB returns the given color with the blue
// component (B) set to the given alpha-premultiplied value
func WithB(c color.Color, b uint8) color.RGBA {
rc := AsRGBA(c)
rc.B = b
return rc
}
// WithA returns the given color with the
// transparency (A) set to the given value,
// with the color premultiplication updated.
func WithA(c color.Color, a uint8) color.RGBA {
n := color.NRGBAModel.Convert(c).(color.NRGBA)
n.A = a
return AsRGBA(n)
}
// WithAF32 returns the given color with the
// transparency (A) set to the given float32 value
// between 0 and 1, with the color premultiplication updated.
func WithAF32(c color.Color, a float32) color.RGBA {
n := color.NRGBAModel.Convert(c).(color.NRGBA)
a = math32.Clamp(a, 0, 1)
n.A = uint8(a * 255)
return AsRGBA(n)
}
// ApplyOpacity applies the given opacity (0-1) to the given color
// and returns the result. It is different from [WithAF32] in that it
// sets the transparency (A) value of the color to the current value
// times the given value instead of just directly overriding it.
func ApplyOpacity(c color.Color, opacity float32) color.RGBA {
r := AsRGBA(c)
if opacity >= 1 {
return r
}
a := r.A
// new A is current A times opacity
return WithA(c, uint8(float32(a)*opacity))
}
// ApplyOpacityNRGBA applies the given opacity (0-1) to the given color
// and returns the result. It is different from [WithAF32] in that it
// sets the transparency (A) value of the color to the current value
// times the given value instead of just directly overriding it.
// It is the [color.NRGBA] version of [ApplyOpacity].
func ApplyOpacityNRGBA(c color.Color, opacity float32) color.NRGBA {
r := color.NRGBAModel.Convert(c).(color.NRGBA)
if opacity >= 1 {
return r
}
a := r.A
// new A is current A times opacity
return color.NRGBA{r.R, r.G, r.B, uint8(float32(a) * opacity)}
}
// Clearer returns a color that is the given amount
// more transparent (lower alpha value) in terms of
// RGBA absolute alpha from 0 to 100, with the color
// premultiplication updated.
func Clearer(c color.Color, amount float32) color.RGBA {
f32 := NRGBAF32Model.Convert(c).(NRGBAF32)
f32.A -= amount / 100
f32.A = math32.Clamp(f32.A, 0, 1)
return AsRGBA(f32)
}
// Opaquer returns a color that is the given amount
// more opaque (higher alpha value) in terms of
// RGBA absolute alpha from 0 to 100,
// with the color premultiplication updated.
func Opaquer(c color.Color, amount float32) color.RGBA {
f32 := NRGBAF32Model.Convert(c).(NRGBAF32)
f32.A += amount / 100
f32.A = math32.Clamp(f32.A, 0, 1)
return AsRGBA(f32)
}
// Inverse returns the inverse of the given color
// (255 - each component). It does not change the
// alpha channel.
func Inverse(c color.Color) color.RGBA {
r := AsRGBA(c)
return color.RGBA{255 - r.R, 255 - r.G, 255 - r.B, r.A}
}
// Add adds given color deltas to this color, safely avoiding overflow > 255
func Add(c, dc color.Color) color.RGBA {
r, g, b, a := c.RGBA() // uint32
dr, dg, db, da := dc.RGBA() // uint32
r = (r + dr) >> 8
g = (g + dg) >> 8
b = (b + db) >> 8
a = (a + da) >> 8
if r > 255 {
r = 255
}
if g > 255 {
g = 255
}
if b > 255 {
b = 255
}
if a > 255 {
a = 255
}
return color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)}
}
// Sub subtracts given color deltas from this color, safely avoiding underflow < 0
func Sub(c, dc color.Color) color.RGBA {
r, g, b, a := c.RGBA() // uint32
dr, dg, db, da := dc.RGBA() // uint32
r = (r - dr) >> 8
g = (g - dg) >> 8
b = (b - db) >> 8
a = (a - da) >> 8
if r > 255 { // overflow
r = 0
}
if g > 255 {
g = 0
}
if b > 255 {
b = 0
}
if a > 255 {
a = 0
}
return color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)}
}
// 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 colors
import (
"image"
"image/color"
)
// Context contains information about the context in which color parsing occurs.
type Context interface {
// Base returns the base color that the color parsing is relative top
Base() color.RGBA
// ImageByURL returns the [image.Image] associated with the given URL.
// Typical URL formats are HTTP URLs like "https://example.com" and node
// URLs like "#name". If it returns nil, that indicats that there is no
// [image.Image] color associated with the given URL.
ImageByURL(url string) image.Image
}
// BaseContext returns a basic [Context] based on the given base color.
func BaseContext(base color.RGBA) Context {
return &baseContext{base}
}
type baseContext struct {
base color.RGBA
}
func (bc *baseContext) Base() color.RGBA {
return bc.base
}
func (bc *baseContext) ImageByURL(url string) image.Image {
return nil
}
// Code generated by "core generate"; DO NOT EDIT.
package colors
import (
"cogentcore.org/core/enums"
)
var _BlendTypesValues = []BlendTypes{0, 1, 2}
// BlendTypesN is the highest valid value for type BlendTypes, plus one.
const BlendTypesN BlendTypes = 3
var _BlendTypesValueMap = map[string]BlendTypes{`HCT`: 0, `RGB`: 1, `CAM16`: 2}
var _BlendTypesDescMap = map[BlendTypes]string{0: `HCT uses the hue, chroma, and tone space and generally produces the best results, but at a slight performance cost.`, 1: `RGB uses raw RGB space, which is the standard space that most other programs use. It produces decent results with maximum performance.`, 2: `CAM16 is an alternative colorspace, similar to HCT, but not quite as good.`}
var _BlendTypesMap = map[BlendTypes]string{0: `HCT`, 1: `RGB`, 2: `CAM16`}
// String returns the string representation of this BlendTypes value.
func (i BlendTypes) String() string { return enums.String(i, _BlendTypesMap) }
// SetString sets the BlendTypes value from its string representation,
// and returns an error if the string is invalid.
func (i *BlendTypes) SetString(s string) error {
return enums.SetString(i, s, _BlendTypesValueMap, "BlendTypes")
}
// Int64 returns the BlendTypes value as an int64.
func (i BlendTypes) Int64() int64 { return int64(i) }
// SetInt64 sets the BlendTypes value from an int64.
func (i *BlendTypes) SetInt64(in int64) { *i = BlendTypes(in) }
// Desc returns the description of the BlendTypes value.
func (i BlendTypes) Desc() string { return enums.Desc(i, _BlendTypesDescMap) }
// BlendTypesValues returns all possible values for the type BlendTypes.
func BlendTypesValues() []BlendTypes { return _BlendTypesValues }
// Values returns all possible values for the type BlendTypes.
func (i BlendTypes) Values() []enums.Enum { return enums.Values(_BlendTypesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i BlendTypes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *BlendTypes) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "BlendTypes")
}
// 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 gradient
import (
"image"
"image/color"
"cogentcore.org/core/colors"
)
// ApplyFunc is a function that transforms input color to an output color.
type ApplyFunc func(c color.Color) color.Color
// ApplyFuncs is a slice of ApplyFunc color functions applied in order
type ApplyFuncs []ApplyFunc
// Add adds a new function
func (af *ApplyFuncs) Add(fun ApplyFunc) {
*af = append(*af, fun)
}
// Apply applies all functions in order to given input color
func (af ApplyFuncs) Apply(c color.Color) color.Color {
for _, f := range af {
c = f(c)
}
return c
}
func (af ApplyFuncs) Clone() ApplyFuncs {
n := len(af)
if n == 0 {
return nil
}
c := make(ApplyFuncs, n)
copy(c, af)
return c
}
// Applier is an image.Image wrapper that applies a color transformation
// to the output of a source image, using the given ApplyFunc
type Applier struct {
image.Image
Func ApplyFunc
}
// NewApplier returns a new applier for given image and apply function
func NewApplier(img image.Image, fun func(c color.Color) color.Color) *Applier {
return &Applier{Image: img, Func: fun}
}
func (ap *Applier) At(x, y int) color.Color {
return ap.Func(ap.Image.At(x, y))
}
// Apply returns a copy of the given image with the given color function
// applied to each pixel of the image, handling special cases:
// [image.Uniform] is optimized and must be preserved as such: color is directly updated.
// [gradient.Gradient] must have Update called prior to rendering, with
// the current bounding box.
func Apply(img image.Image, f ApplyFunc) image.Image {
if img == nil {
return nil
}
switch im := img.(type) {
case *image.Uniform:
return image.NewUniform(f(colors.AsRGBA(im)))
case Gradient:
cp := CopyOf(im)
cp.AsBase().ApplyFuncs.Add(f)
return cp
default:
return NewApplier(img, f)
}
}
// ApplyOpacity applies the given opacity (0-1) to the given image,
// handling the following special cases, and using an Applier for the general case.
// [image.Uniform] is optimized and must be preserved as such: color is directly updated.
// [gradient.Gradient] must have Update called prior to rendering, with
// the current bounding box. Multiplies the opacity of the stops.
func ApplyOpacity(img image.Image, opacity float32) image.Image {
if img == nil {
return nil
}
if opacity == 1 {
return img
}
switch im := img.(type) {
case *image.Uniform:
return image.NewUniform(colors.ApplyOpacity(colors.AsRGBA(im), opacity))
case Gradient:
cp := CopyOf(im)
cp.AsBase().ApplyOpacityToStops(opacity)
return cp
default:
return NewApplier(img, func(c color.Color) color.Color {
return colors.ApplyOpacity(c, opacity)
})
}
}
// Code generated by "core generate"; DO NOT EDIT.
package gradient
import (
"cogentcore.org/core/enums"
)
var _SpreadsValues = []Spreads{0, 1, 2}
// SpreadsN is the highest valid value for type Spreads, plus one.
const SpreadsN Spreads = 3
var _SpreadsValueMap = map[string]Spreads{`pad`: 0, `reflect`: 1, `repeat`: 2}
var _SpreadsDescMap = map[Spreads]string{0: `Pad indicates to have the final color of the gradient fill the object beyond the end of the gradient.`, 1: `Reflect indicates to have a gradient repeat in reverse order (offset 1 to 0) to fully fill an object beyond the end of the gradient.`, 2: `Repeat indicates to have a gradient continue in its original order (offset 0 to 1) by jumping back to the start to fully fill an object beyond the end of the gradient.`}
var _SpreadsMap = map[Spreads]string{0: `pad`, 1: `reflect`, 2: `repeat`}
// String returns the string representation of this Spreads value.
func (i Spreads) String() string { return enums.String(i, _SpreadsMap) }
// SetString sets the Spreads value from its string representation,
// and returns an error if the string is invalid.
func (i *Spreads) SetString(s string) error {
return enums.SetString(i, s, _SpreadsValueMap, "Spreads")
}
// Int64 returns the Spreads value as an int64.
func (i Spreads) Int64() int64 { return int64(i) }
// SetInt64 sets the Spreads value from an int64.
func (i *Spreads) SetInt64(in int64) { *i = Spreads(in) }
// Desc returns the description of the Spreads value.
func (i Spreads) Desc() string { return enums.Desc(i, _SpreadsDescMap) }
// SpreadsValues returns all possible values for the type Spreads.
func SpreadsValues() []Spreads { return _SpreadsValues }
// Values returns all possible values for the type Spreads.
func (i Spreads) Values() []enums.Enum { return enums.Values(_SpreadsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Spreads) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Spreads) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Spreads") }
var _UnitsValues = []Units{0, 1}
// UnitsN is the highest valid value for type Units, plus one.
const UnitsN Units = 2
var _UnitsValueMap = map[string]Units{`objectBoundingBox`: 0, `userSpaceOnUse`: 1}
var _UnitsDescMap = map[Units]string{0: `ObjectBoundingBox indicates that coordinate values are scaled relative to the size of the object and are specified in the normalized range of 0 to 1.`, 1: `UserSpaceOnUse indicates that coordinate values are specified in the current user coordinate system when the gradient is used (ie: actual SVG/core coordinates).`}
var _UnitsMap = map[Units]string{0: `objectBoundingBox`, 1: `userSpaceOnUse`}
// String returns the string representation of this Units value.
func (i Units) String() string { return enums.String(i, _UnitsMap) }
// SetString sets the Units value from its string representation,
// and returns an error if the string is invalid.
func (i *Units) SetString(s string) error { return enums.SetString(i, s, _UnitsValueMap, "Units") }
// Int64 returns the Units value as an int64.
func (i Units) Int64() int64 { return int64(i) }
// SetInt64 sets the Units value from an int64.
func (i *Units) SetInt64(in int64) { *i = Units(in) }
// Desc returns the description of the Units value.
func (i Units) Desc() string { return enums.Desc(i, _UnitsDescMap) }
// UnitsValues returns all possible values for the type Units.
func UnitsValues() []Units { return _UnitsValues }
// Values returns all possible values for the type Units.
func (i Units) Values() []enums.Enum { return enums.Values(_UnitsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Units) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Units) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Units") }
// 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.
// Based on https://github.com/srwiley/rasterx:
// Copyright 2018 by the rasterx Authors. All rights reserved.
// Created 2018 by S.R.Wiley
// Package gradient provides linear, radial, and conic color gradients.
package gradient
//go:generate core generate
import (
"image"
"image/color"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
)
// Gradient is the interface that all gradient types satisfy.
type Gradient interface {
image.Image
// AsBase returns the [Base] of the gradient
AsBase() *Base
// Update updates the computed fields of the gradient, using
// the given object opacity, current bounding box, and additional
// object-level transform (i.e., the current painting transform),
// which is applied in addition to the gradient's own Transform.
// This must be called before rendering the gradient, and it should only be called then.
Update(opacity float32, box math32.Box2, objTransform math32.Matrix2)
}
// Base contains the data and logic common to all gradient types.
type Base struct { //types:add -setters
// the stops for the gradient; use AddStop to add stops
Stops []Stop `set:"-"`
// the spread method used for the gradient if it stops before the end
Spread Spreads
// the colorspace algorithm to use for blending colors
Blend colors.BlendTypes
// the units to use for the gradient
Units Units
// the bounding box of the object with the gradient; this is used when rendering
// gradients with [Units] of [ObjectBoundingBox].
Box math32.Box2
// Transform is the gradient's own transformation matrix applied to the gradient's points.
// This is a property of the Gradient itself.
Transform math32.Matrix2
// Opacity is the overall object opacity multiplier, applied in conjunction with the
// stop-level opacity blending.
Opacity float32
// ApplyFuncs contains functions that are applied to the color after gradient color is generated.
// This allows for efficient StateLayer and other post-processing effects
// to be applied. The Applier handles other cases, but gradients always
// must have the Update function called at render time, so they must
// remain Gradient types.
ApplyFuncs ApplyFuncs `set:"-"`
// boxTransform is the Transform applied to the bounding Box,
// only for [Units] == [ObjectBoundingBox].
boxTransform math32.Matrix2 `set:"-"`
// stopsRGB are the computed RGB stops for blend types other than RGB
stopsRGB []Stop `set:"-"`
// stopsRGBSrc are the source Stops when StopsRGB were last computed
stopsRGBSrc []Stop `set:"-"`
}
// Stop represents a single stop in a gradient
type Stop struct {
// the 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
// the position of the stop between 0 and 1
Pos float32
// Opacity is the 0-1 level of opacity for this stop
Opacity float32
}
// OpacityColor returns the stop color with its opacity applied,
// along with a global opacity multiplier
func (st *Stop) OpacityColor(opacity float32, apply ApplyFuncs) color.Color {
return apply.Apply(colors.ApplyOpacity(st.Color, st.Opacity*opacity))
}
// Spreads are the spread methods used when a gradient reaches
// its end but the object isn't yet fully filled.
type Spreads int32 //enums:enum -transform lower
const (
// Pad indicates to have the final color of the gradient fill
// the object beyond the end of the gradient.
Pad Spreads = iota
// Reflect indicates to have a gradient repeat in reverse order
// (offset 1 to 0) to fully fill an object beyond the end of the gradient.
Reflect
// Repeat indicates to have a gradient continue in its original order
// (offset 0 to 1) by jumping back to the start to fully fill an object beyond
// the end of the gradient.
Repeat
)
// Units are the types of units used for gradient coordinate values
type Units int32 //enums:enum -transform lower-camel
const (
// ObjectBoundingBox indicates that coordinate values are scaled
// relative to the size of the object and are specified in the
// normalized range of 0 to 1.
ObjectBoundingBox Units = iota
// UserSpaceOnUse indicates that coordinate values are specified
// in the current user coordinate system when the gradient is used
// (ie: actual SVG/core coordinates).
UserSpaceOnUse
)
// AddStop adds a new stop with the given color, position, and
// optional opacity to the gradient.
func (b *Base) AddStop(color color.RGBA, pos float32, opacity ...float32) *Base {
op := float32(1)
if len(opacity) > 0 {
op = opacity[0]
}
b.Stops = append(b.Stops, Stop{Color: color, Pos: pos, Opacity: op})
return b
}
// AsBase returns the [Base] of the gradient
func (b *Base) AsBase() *Base {
return b
}
// NewBase returns a new [Base] with default values. It should
// only be used in the New functions of gradient types.
func NewBase() Base {
return Base{
Blend: colors.RGB,
Box: math32.B2(0, 0, 100, 100),
Opacity: 1,
Transform: math32.Identity2(),
}
}
// ColorModel returns the color model used by the gradient image, which is [color.RGBAModel]
func (b *Base) ColorModel() color.Model {
return color.RGBAModel
}
// Bounds returns the bounds of the gradient image, which are infinite.
func (b *Base) Bounds() image.Rectangle {
return image.Rect(-1e9, -1e9, 1e9, 1e9)
}
// CopyFrom copies from the given gradient (cp) onto this gradient (g),
// making new copies of the stops instead of re-using pointers.
// It assumes the gradients are of the same type.
func CopyFrom(g Gradient, cp Gradient) {
switch g := g.(type) {
case *Linear:
*g = *cp.(*Linear)
case *Radial:
*g = *cp.(*Radial)
}
cb := cp.AsBase()
gb := g.AsBase()
gb.CopyStopsFrom(cb)
gb.ApplyFuncs = cb.ApplyFuncs.Clone()
}
// CopyOf returns a copy of the given gradient, making copies of the stops
// instead of re-using pointers.
func CopyOf(g Gradient) Gradient {
var res Gradient
switch g := g.(type) {
case *Linear:
res = &Linear{}
CopyFrom(res, g)
case *Radial:
res = &Radial{}
CopyFrom(res, g)
}
return res
}
// CopyStopsFrom copies the base gradient stops from the given base gradient
func (b *Base) CopyStopsFrom(cp *Base) {
b.Stops = make([]Stop, len(cp.Stops))
copy(b.Stops, cp.Stops)
if cp.stopsRGB == nil {
b.stopsRGB = nil
b.stopsRGBSrc = nil
} else {
b.stopsRGB = make([]Stop, len(cp.stopsRGB))
copy(b.stopsRGB, cp.stopsRGB)
b.stopsRGBSrc = make([]Stop, len(cp.stopsRGBSrc))
copy(b.stopsRGBSrc, cp.stopsRGBSrc)
}
}
// ApplyOpacityToStops multiplies all stop opacities by the given opacity.
func (b *Base) ApplyOpacityToStops(opacity float32) {
for _, s := range b.Stops {
s.Opacity *= opacity
}
for _, s := range b.stopsRGB {
s.Opacity *= opacity
}
for _, s := range b.stopsRGBSrc {
s.Opacity *= opacity
}
}
// updateBase updates the computed fields of the base gradient. It should only be called
// by other gradient types in their [Gradient.Update] functions. It is named updateBase
// to avoid people accidentally calling it instead of [Gradient.Update].
func (b *Base) updateBase() {
b.computeObjectMatrix()
b.updateRGBStops()
}
// computeObjectMatrix computes the effective object transformation
// matrix for a gradient with [Units] of [ObjectBoundingBox], setting
// [Base.boxTransform].
func (b *Base) computeObjectMatrix() {
w, h := b.Box.Size().X, b.Box.Size().Y
oriX, oriY := b.Box.Min.X, b.Box.Min.Y
b.boxTransform = math32.Identity2().Translate(oriX, oriY).Scale(w, h).Mul(b.Transform).
Scale(1/w, 1/h).Translate(-oriX, -oriY).Inverse()
}
// getColor returns the color at the given normalized position along the
// gradient's stops using its spread method and blend algorithm.
func (b *Base) getColor(pos float32) color.Color {
if b.Blend == colors.RGB {
return b.getColorImpl(pos, b.Stops)
}
return b.getColorImpl(pos, b.stopsRGB)
}
// getColorImpl implements [Base.getColor] with given stops
func (b *Base) getColorImpl(pos float32, stops []Stop) color.Color {
d := len(stops)
// These cases can be taken care of early on
if b.Spread == Pad {
if pos >= 1 {
return stops[d-1].OpacityColor(b.Opacity, b.ApplyFuncs)
}
if pos <= 0 {
return stops[0].OpacityColor(b.Opacity, b.ApplyFuncs)
}
}
modRange := float32(1)
if b.Spread == Reflect {
modRange = 2
}
mod := math32.Mod(pos, modRange)
if mod < 0 {
mod += modRange
}
place := 0 // Advance to place where mod is greater than the indicated stop
for place != len(stops) && mod > stops[place].Pos {
place++
}
switch b.Spread {
case Repeat:
var s1, s2 Stop
switch place {
case 0, d:
s1, s2 = stops[d-1], stops[0]
default:
s1, s2 = stops[place-1], stops[place]
}
return b.blendStops(mod, s1, s2, false)
case Reflect:
switch place {
case 0:
return stops[0].OpacityColor(b.Opacity, b.ApplyFuncs)
case d:
// Advance to place where mod-1 is greater than the stop indicated by place in reverse of the stop slice.
// Since this is the reflect b.Spread mode, the mod interval is two, allowing the stop list to be
// iterated in reverse before repeating the sequence.
for place != d*2 && mod-1 > (1-stops[d*2-place-1].Pos) {
place++
}
switch place {
case d:
return stops[d-1].OpacityColor(b.Opacity, b.ApplyFuncs)
case d * 2:
return stops[0].OpacityColor(b.Opacity, b.ApplyFuncs)
default:
return b.blendStops(mod-1, stops[d*2-place], stops[d*2-place-1], true)
}
default:
return b.blendStops(mod, stops[place-1], stops[place], false)
}
default: // PadSpread
switch place {
case 0:
return stops[0].OpacityColor(b.Opacity, b.ApplyFuncs)
case d:
return stops[d-1].OpacityColor(b.Opacity, b.ApplyFuncs)
default:
return b.blendStops(mod, stops[place-1], stops[place], false)
}
}
}
// blendStops blends the given two gradient stops together based on the given position,
// using the gradient's blending algorithm. If flip is true, it flips the given position.
func (b *Base) blendStops(pos float32, s1, s2 Stop, flip bool) color.Color {
s1off := s1.Pos
if s1.Pos > s2.Pos && !flip { // happens in repeat spread mode
s1off--
if pos > 1 {
pos--
}
}
if s2.Pos == s1off {
return s2.OpacityColor(b.Opacity, b.ApplyFuncs)
}
if flip {
pos = 1 - pos
}
tp := (pos - s1off) / (s2.Pos - s1off)
opacity := (s1.Opacity*(1-tp) + s2.Opacity*tp) * b.Opacity
return b.ApplyFuncs.Apply(colors.ApplyOpacity(colors.Blend(colors.RGB, 100*(1-tp), s1.Color, s2.Color), opacity))
}
// updateRGBStops updates stopsRGB from original Stops, for other blend types
func (b *Base) updateRGBStops() {
if b.Blend == colors.RGB || len(b.Stops) == 0 {
b.stopsRGB = nil
b.stopsRGBSrc = nil
return
}
n := len(b.Stops)
lenEq := false
if len(b.stopsRGBSrc) == n {
lenEq = true
equal := true
for i := range b.Stops {
if b.Stops[i] != b.stopsRGBSrc[i] {
equal = false
break
}
}
if equal {
return
}
}
if !lenEq {
b.stopsRGBSrc = make([]Stop, n)
}
copy(b.stopsRGBSrc, b.Stops)
b.stopsRGB = make([]Stop, 0, n*4)
tdp := float32(0.05)
b.stopsRGB = append(b.stopsRGB, b.Stops[0])
for i := 0; i < n-1; i++ {
sp := b.Stops[i]
s := b.Stops[i+1]
dp := s.Pos - sp.Pos
np := int(math32.Ceil(dp / tdp))
if np == 1 {
b.stopsRGB = append(b.stopsRGB, s)
continue
}
pct := float32(1) / float32(np)
dopa := s.Opacity - sp.Opacity
for j := 0; j < np; j++ {
p := pct * float32(j)
c := colors.Blend(colors.RGB, 100*p, s.Color, sp.Color)
pos := sp.Pos + p*dp
opa := sp.Opacity + p*dopa
b.stopsRGB = append(b.stopsRGB, Stop{Color: c, Pos: pos, Opacity: opa})
}
b.stopsRGB = append(b.stopsRGB, s)
}
}
// 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.
// Based on https://github.com/srwiley/rasterx:
// Copyright 2018 by the rasterx Authors. All rights reserved.
// Created 2018 by S.R.Wiley
package gradient
import (
"image/color"
"cogentcore.org/core/math32"
)
// Linear represents a linear gradient. It implements the [image.Image] interface.
type Linear struct { //types:add -setters
Base
// the starting point of the gradient (x1 and y1 in SVG)
Start math32.Vector2
// the ending point of the gradient (x2 and y2 in SVG)
End math32.Vector2
// computed current render versions transformed by object matrix
rStart math32.Vector2
rEnd math32.Vector2
distance math32.Vector2
distanceLengthSquared float32
}
var _ Gradient = &Linear{}
// NewLinear returns a new left-to-right [Linear] gradient.
func NewLinear() *Linear {
return &Linear{
Base: NewBase(),
// default in SVG is LTR
End: math32.Vec2(1, 0),
}
}
// AddStop adds a new stop with the given color, position, and
// optional opacity to the gradient.
func (l *Linear) AddStop(color color.RGBA, pos float32, opacity ...float32) *Linear {
l.Base.AddStop(color, pos, opacity...)
return l
}
// Update updates the computed fields of the gradient, using
// the given current bounding box, and additional
// object-level transform (i.e., the current painting transform),
// which is applied in addition to the gradient's own Transform.
// This must be called before rendering the gradient, and it should only be called then.
func (l *Linear) Update(opacity float32, box math32.Box2, objTransform math32.Matrix2) {
l.Box = box
l.Opacity = opacity
l.updateBase()
if l.Units == ObjectBoundingBox {
sz := l.Box.Size()
l.rStart = l.Box.Min.Add(sz.Mul(l.Start))
l.rEnd = l.Box.Min.Add(sz.Mul(l.End))
} else {
l.rStart = l.Transform.MulVector2AsPoint(l.Start)
l.rEnd = l.Transform.MulVector2AsPoint(l.End)
l.rStart = objTransform.MulVector2AsPoint(l.rStart)
l.rEnd = objTransform.MulVector2AsPoint(l.rEnd)
}
l.distance = l.rEnd.Sub(l.rStart)
l.distanceLengthSquared = l.distance.LengthSquared()
}
// At returns the color of the linear gradient at the given point
func (l *Linear) At(x, y int) color.Color {
switch len(l.Stops) {
case 0:
return color.RGBA{}
case 1:
return l.Stops[0].OpacityColor(l.Opacity, l.ApplyFuncs)
}
pt := math32.Vec2(float32(x)+0.5, float32(y)+0.5)
if l.Units == ObjectBoundingBox {
pt = l.boxTransform.MulVector2AsPoint(pt)
}
df := pt.Sub(l.rStart)
pos := (l.distance.X*df.X + l.distance.Y*df.Y) / l.distanceLengthSquared
return l.getColor(pos)
}
// 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.
// color 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 gradient
import (
"encoding/xml"
"fmt"
"image"
"image/color"
"io"
"strconv"
"strings"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"golang.org/x/net/html/charset"
)
// XMLAttr searches for given attribute in slice of xml attributes;
// returns "" if not found.
func XMLAttr(name string, attrs []xml.Attr) string {
for _, attr := range attrs {
if attr.Name.Local == name {
return attr.Value
}
}
return ""
}
// Cache is a cache of the [image.Image] results of [FromString] calls
// for each string passed to [FromString].
var Cache map[string]image.Image
// FromString parses the given CSS image/gradient/color string and returns the resulting image.
// FromString is based on https://www.w3schools.com/css/css3_gradients.asp.
// See [UnmarshalXML] for an XML-based version. If no Context is
// provied, FromString uses [BaseContext] with [Transparent].
func FromString(str string, ctx ...colors.Context) (image.Image, error) {
var cc colors.Context
if len(ctx) > 0 && ctx[0] != nil {
cc = ctx[0]
} else {
cc = colors.BaseContext(colors.Transparent)
}
if Cache == nil {
Cache = make(map[string]image.Image)
}
cnm := str
if img, ok := Cache[cnm]; ok {
// TODO(kai): do we need to clone?
return img, nil
}
str = strings.TrimSpace(str)
if strings.HasPrefix(str, "url(") {
img := cc.ImageByURL(str)
if img == nil {
return nil, fmt.Errorf("unable to find url %q", str)
}
return img, nil
}
str = strings.ToLower(str)
grad := "-gradient"
gidx := strings.Index(str, grad)
if gidx <= 0 {
s, err := colors.FromString(str, cc.Base())
if err != nil {
return nil, err
}
return colors.Uniform(s), nil
}
gtyp := str[:gidx]
rmdr := str[gidx+len(grad):]
pidx := strings.IndexByte(rmdr, '(')
if pidx < 0 {
return nil, fmt.Errorf("gradient specified but parameters not found in string %q", str)
}
pars := rmdr[pidx+1:]
pars = strings.TrimSuffix(pars, ");")
pars = strings.TrimSuffix(pars, ")")
switch gtyp {
case "linear", "repeating-linear":
l := NewLinear()
if gtyp == "repeating-linear" {
l.SetSpread(Repeat)
}
err := l.SetString(pars)
if err != nil {
return nil, err
}
fixGradientStops(l.Stops)
Cache[cnm] = l
return l, nil
case "radial", "repeating-radial":
r := NewRadial()
if gtyp == "repeating-radial" {
r.SetSpread(Repeat)
}
err := r.SetString(pars)
if err != nil {
return nil, err
}
fixGradientStops(r.Stops)
Cache[cnm] = r
return r, nil
}
return nil, fmt.Errorf("got unknown gradient type %q", gtyp)
}
// FromAny returns the color image specified by the given value of any type in the
// given Context. It handles values of types [color.Color], [image.Image], and string.
// If no Context is provided, it uses [BaseContext] with [Transparent].
func FromAny(val any, ctx ...colors.Context) (image.Image, error) {
switch v := val.(type) {
case color.Color:
return colors.Uniform(v), nil
case image.Image:
return v, nil
case string:
return FromString(v, ctx...)
}
return nil, fmt.Errorf("gradient.FromAny: got unsupported type %T", val)
}
// gradientDegToSides maps gradient degree notation to side notation
var gradientDegToSides = map[string]string{
"0deg": "top",
"360deg": "top",
"45deg": "top right",
"-315deg": "top right",
"90deg": "right",
"-270deg": "right",
"135deg": "bottom right",
"-225deg": "bottom right",
"180deg": "bottom",
"-180deg": "bottom",
"225deg": "bottom left",
"-135deg": "bottom left",
"270deg": "left",
"-90deg": "left",
"315deg": "top left",
"-45deg": "top left",
}
// SetString sets the linear gradient from the given CSS linear gradient string
// (only the part inside of "linear-gradient(...)") (see
// https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient)
func (l *Linear) SetString(str string) error {
// TODO(kai): not fully following spec yet
plist := strings.Split(str, ", ")
var prevColor color.Color
stopIndex := 0
outer:
for pidx := 0; pidx < len(plist); pidx++ {
par := strings.TrimRight(strings.TrimSpace(plist[pidx]), ",")
origPar := par
switch {
case strings.Contains(par, "deg"):
// TODO(kai): this is not true and should be fixed to use trig
// can't use trig, b/c need to be full 1, 0 values -- just use map
var ok bool
par, ok = gradientDegToSides[par]
if !ok {
return fmt.Errorf("invalid gradient angle %q: must be at 45 degree increments", origPar)
}
par = "to " + par
fallthrough
case strings.HasPrefix(par, "to "):
sides := strings.Split(par[3:], " ")
l.Start, l.End = math32.Vector2{}, math32.Vector2{}
for _, side := range sides {
switch side {
case "bottom":
l.Start.Y = 0
l.End.Y = 1
case "top":
l.Start.Y = 1
l.End.Y = 0
case "right":
l.Start.X = 0
l.End.X = 1
case "left":
l.Start.X = 1
l.End.X = 0
}
}
case strings.HasPrefix(par, ")"):
break outer
default: // must be a color stop
var stop *Stop
if len(l.Stops) > stopIndex {
stop = &(l.Stops[stopIndex])
} else {
stop = &Stop{Opacity: 1}
}
err := parseColorStop(stop, prevColor, par)
if err != nil {
return err
}
if len(l.Stops) <= stopIndex {
l.Stops = append(l.Stops, *stop)
}
prevColor = stop.Color
stopIndex++
}
}
if len(l.Stops) > stopIndex {
l.Stops = l.Stops[:stopIndex]
}
return nil
}
// SetString sets the radial gradient from the given CSS radial gradient string
// (only the part inside of "radial-gradient(...)") (see
// https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/radial-gradient)
func (r *Radial) SetString(str string) error {
// TODO(kai): not fully following spec yet
plist := strings.Split(str, ", ")
var prevColor color.Color
stopIndex := 0
outer:
for pidx := 0; pidx < len(plist); pidx++ {
par := strings.TrimRight(strings.TrimSpace(plist[pidx]), ",")
// currently we just ignore circle and ellipse, but we should handle them at some point
par = strings.TrimPrefix(par, "circle")
par = strings.TrimPrefix(par, "ellipse")
par = strings.TrimLeft(par, " ")
switch {
case strings.HasPrefix(par, "at "):
sides := strings.Split(par[3:], " ")
for _, side := range sides {
switch side {
case "bottom":
r.Center.Set(0.5, 1)
case "top":
r.Center.Set(0.5, 0)
case "right":
r.Center.Set(1, 0.5)
case "left":
r.Center.Set(0, 0.5)
case "center":
r.Center.Set(0.5, 0.5)
}
r.Focal = r.Center
}
case strings.HasPrefix(par, ")"):
break outer
default: // must be a color stop
var stop *Stop
if len(r.Stops) > stopIndex {
stop = &r.Stops[stopIndex]
} else {
stop = &Stop{Opacity: 1}
}
err := parseColorStop(stop, prevColor, par)
if err != nil {
return err
}
if len(r.Stops) <= stopIndex {
r.Stops = append(r.Stops, *stop)
}
prevColor = stop.Color
stopIndex++
}
}
if len(r.Stops) > stopIndex {
r.Stops = r.Stops[:stopIndex]
}
return nil
}
// parseColorStop parses the given color stop based on the given previous color
// and parent gradient string.
func parseColorStop(stop *Stop, prev color.Color, par string) error {
cnm := par
if spcidx := strings.Index(par, " "); spcidx > 0 {
cnm = par[:spcidx]
offs := strings.TrimSpace(par[spcidx+1:])
off, err := readFraction(offs)
if err != nil {
return fmt.Errorf("invalid offset %q: %w", offs, err)
}
stop.Pos = off
}
clr, err := colors.FromString(cnm, prev)
if err != nil {
return fmt.Errorf("got invalid color string %q: %w", cnm, err)
}
stop.Color = clr
return nil
}
// NOTE: XML marshalling functionality is at [cogentcore.org/core/svg.MarshalXMLGradient] instead of here
// because it uses a lot of SVG and XML infrastructure defined there.
// ReadXML reads an XML-formatted gradient color from the given io.Reader and
// sets the properties of the given gradient accordingly.
func ReadXML(g *Gradient, reader io.Reader) error {
decoder := xml.NewDecoder(reader)
decoder.CharsetReader = charset.NewReaderLabel
for {
t, err := decoder.Token()
if err != nil {
if err == io.EOF {
break
}
return fmt.Errorf("error parsing color xml: %w", err)
}
switch se := t.(type) {
case xml.StartElement:
return UnmarshalXML(g, decoder, se)
// todo: ignore rest?
}
}
return nil
}
// UnmarshalXML parses the given XML gradient color data and sets the properties
// of the given gradient accordingly.
func UnmarshalXML(g *Gradient, decoder *xml.Decoder, se xml.StartElement) error {
start := &se
for {
var t xml.Token
var err error
if start != nil {
t = *start
start = nil
} else {
t, err = decoder.Token()
}
if err != nil {
if err == io.EOF {
break
}
return fmt.Errorf("error parsing color: %w", err)
}
switch se := t.(type) {
case xml.StartElement:
switch se.Name.Local {
case "linearGradient":
l := NewLinear().SetEnd(math32.Vec2(1, 0)) // SVG is LTR by default
// if we don't already have a gradient, we use this one
if *g == nil {
*g = l
} else if pl, ok := (*g).(*Linear); ok {
// if our previous gradient is also linear, we build on it
l = pl
}
// fmt.Printf("lingrad %v\n", cs.Gradient)
for _, attr := range se.Attr {
// fmt.Printf("attr: %v val: %v\n", attr.Name.Local, attr.Value)
switch attr.Name.Local {
// note: id not processed here - must be done externally
case "x1":
l.Start.X, err = readFraction(attr.Value)
case "y1":
l.Start.Y, err = readFraction(attr.Value)
case "x2":
l.End.X, err = readFraction(attr.Value)
case "y2":
l.End.Y, err = readFraction(attr.Value)
default:
err = readGradAttr(*g, attr)
}
if err != nil {
return fmt.Errorf("error parsing linear gradient: %w", err)
}
}
case "radialGradient":
r := NewRadial()
// if we don't already have a gradient, we use this one
if *g == nil {
*g = r
} else if pr, ok := (*g).(*Radial); ok {
// if our previous gradient is also radial, we build on it
r = pr
}
var setFx, setFy bool
for _, attr := range se.Attr {
switch attr.Name.Local {
// note: id not processed here - must be done externally
case "r":
var radius float32
radius, err = readFraction(attr.Value)
r.Radius.SetScalar(radius)
case "cx":
r.Center.X, err = readFraction(attr.Value)
case "cy":
r.Center.Y, err = readFraction(attr.Value)
case "fx":
setFx = true
r.Focal.X, err = readFraction(attr.Value)
case "fy":
setFy = true
r.Focal.Y, err = readFraction(attr.Value)
default:
err = readGradAttr(*g, attr)
}
if err != nil {
return fmt.Errorf("error parsing radial gradient: %w", err)
}
}
if !setFx { // set fx to cx by default
r.Focal.X = r.Center.X
}
if !setFy { // set fy to cy by default
r.Focal.Y = r.Center.Y
}
case "stop":
stop := Stop{Color: colors.Black, Opacity: 1}
ats := se.Attr
sty := XMLAttr("style", ats)
if sty != "" {
spl := strings.Split(sty, ";")
for _, s := range spl {
s := strings.TrimSpace(s)
ci := strings.IndexByte(s, ':')
if ci < 0 {
continue
}
a := xml.Attr{}
a.Name.Local = s[:ci]
a.Value = s[ci+1:]
ats = append(ats, a)
}
}
for _, attr := range ats {
switch attr.Name.Local {
case "offset":
stop.Pos, err = readFraction(attr.Value)
if err != nil {
return err
}
case "stop-color":
clr, err := colors.FromString(attr.Value)
if err != nil {
return fmt.Errorf("invalid color string: %w", err)
}
stop.Color = clr
case "stop-opacity":
opacity, err := readFraction(attr.Value)
if err != nil {
return fmt.Errorf("invalid stop opacity: %w", err)
}
stop.Opacity = opacity
}
}
if g == nil {
return fmt.Errorf("got stop outside of gradient: %v", stop)
} else {
gb := (*g).AsBase()
gb.Stops = append(gb.Stops, stop)
}
default:
return fmt.Errorf("cannot process svg element %q", se.Name.Local)
}
case xml.EndElement:
if se.Name.Local == "linearGradient" || se.Name.Local == "radialGradient" {
return nil
}
if se.Name.Local != "stop" {
return fmt.Errorf("got unexpected end element: %v", se.Name.Local)
}
case xml.CharData:
}
}
return nil
}
// readFraction reads a decimal value from the given string.
func readFraction(v string) (float32, error) {
v = strings.TrimSpace(v)
d := float32(1)
if strings.HasSuffix(v, "%") {
d = 100
v = strings.TrimSuffix(v, "%")
}
f64, err := strconv.ParseFloat(v, 32)
if err != nil {
return 0, err
}
f := float32(f64)
f /= d
if f < 0 {
f = 0
}
return f, nil
}
// readGradAttr reads the given xml attribute onto the given gradient.
func readGradAttr(g Gradient, attr xml.Attr) error {
gb := g.AsBase()
switch attr.Name.Local {
case "gradientTransform":
err := gb.Transform.SetString(attr.Value)
if err != nil {
return err
}
case "gradientUnits":
return gb.Units.SetString(strings.TrimSpace(attr.Value))
case "spreadMethod":
return gb.Spread.SetString(strings.TrimSpace(attr.Value))
}
return nil
}
// fixGradientStops applies the CSS rules to regularize the given gradient stops:
// https://www.w3.org/TR/css3-images/#color-stop-syntax
func fixGradientStops(stops []Stop) {
sz := len(stops)
if sz == 0 {
return
}
splitSt := -1
last := float32(0)
for i := 0; i < sz; i++ {
st := &stops[i]
if i == sz-1 && st.Pos == 0 {
if last < 1.0 {
st.Pos = 1.0
} else {
st.Pos = last
}
}
if i > 0 && st.Pos == 0 && splitSt < 0 {
splitSt = i
st.Pos = last
continue
}
if splitSt > 0 {
start := stops[splitSt].Pos
end := st.Pos
per := (end - start) / float32(1+(i-splitSt))
cur := start + per
for j := splitSt; j < i; j++ {
stops[j].Pos = cur
cur += per
}
}
last = st.Pos
}
}
// 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.
// Based on https://github.com/srwiley/rasterx:
// Copyright 2018 by the rasterx Authors. All rights reserved.
// Created 2018 by S.R.Wiley
package gradient
import (
"image/color"
"cogentcore.org/core/math32"
)
// Radial represents a radial gradient. It implements the [image.Image] interface.
type Radial struct { //types:add -setters
Base
// the center point of the gradient (cx and cy in SVG)
Center math32.Vector2
// the focal point of the gradient (fx and fy in SVG)
Focal math32.Vector2
// the radius of the gradient (rx and ry in SVG)
Radius math32.Vector2
// current render version -- transformed by object matrix
rCenter math32.Vector2 `set:"-"`
// current render version -- transformed by object matrix
rFocal math32.Vector2 `set:"-"`
// current render version -- transformed by object matrix
rRadius math32.Vector2 `set:"-"`
}
var _ Gradient = &Radial{}
// NewRadial returns a new centered [Radial] gradient.
func NewRadial() *Radial {
return &Radial{
Base: NewBase(),
// default is fully centered
Center: math32.Vector2Scalar(0.5),
Focal: math32.Vector2Scalar(0.5),
Radius: math32.Vector2Scalar(0.5),
}
}
// AddStop adds a new stop with the given color, position, and
// optional opacity to the gradient.
func (r *Radial) AddStop(color color.RGBA, pos float32, opacity ...float32) *Radial {
r.Base.AddStop(color, pos, opacity...)
return r
}
// Update updates the computed fields of the gradient, using
// the given current bounding box, and additional
// object-level transform (i.e., the current painting transform),
// which is applied in addition to the gradient's own Transform.
// This must be called before rendering the gradient, and it should only be called then.
func (r *Radial) Update(opacity float32, box math32.Box2, objTransform math32.Matrix2) {
r.Box = box
r.Opacity = opacity
r.updateBase()
c, f, rs := r.Center, r.Focal, r.Radius
sz := r.Box.Size()
if r.Units == ObjectBoundingBox {
c = r.Box.Min.Add(sz.Mul(c))
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)
}
if c != f {
f.SetDiv(rs)
c.SetDiv(rs)
df := f.Sub(c)
if df.X*df.X+df.Y*df.Y > 1 { // Focus outside of circle; use intersection
// point of line from center to focus and circle as per SVG specs.
nf, intersects := rayCircleIntersectionF(f, c, c, 1-epsilonF)
f = nf
if !intersects {
f.Set(0, 0)
}
}
}
r.rCenter, r.rFocal, r.rRadius = c, f, rs
}
const epsilonF = 1e-5
// At returns the color of the radial gradient at the given point
func (r *Radial) At(x, y int) color.Color {
switch len(r.Stops) {
case 0:
return color.RGBA{}
case 1:
return r.Stops[0].Color
}
if r.rCenter == r.rFocal {
// When the center and focal are the same things are much simpler;
// pos is just distance from center scaled by radius
pt := math32.Vec2(float32(x)+0.5, float32(y)+0.5)
if r.Units == ObjectBoundingBox {
pt = r.boxTransform.MulVector2AsPoint(pt)
}
d := 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)
}
if r.rFocal == math32.Vec2(0, 0) {
return color.RGBA{} // should not happen
}
pt := math32.Vec2(float32(x)+0.5, float32(y)+0.5)
if r.Units == ObjectBoundingBox {
pt = r.boxTransform.MulVector2AsPoint(pt)
}
e := pt.Div(r.rRadius)
t1, intersects := rayCircleIntersectionF(e, r.rFocal, r.rCenter, 1)
if !intersects { // In this case, use the last stop color
s := r.Stops[len(r.Stops)-1]
return s.Color
}
td := t1.Sub(r.rFocal)
d := e.Sub(r.rFocal)
if td.X*td.X+td.Y*td.Y < epsilonF {
s := r.Stops[len(r.Stops)-1]
return s.Color
}
pos := math32.Sqrt(d.X*d.X+d.Y*d.Y) / math32.Sqrt(td.X*td.X+td.Y*td.Y)
return r.getColor(pos)
}
// rayCircleIntersectionF calculates in floating point the points of intersection of
// a ray starting at s2 passing through s1 and a circle in fixed point.
// Returns intersects == false if no solution is possible. If two
// solutions are possible, the point closest to s2 is returned.
func rayCircleIntersectionF(s1, s2, c math32.Vector2, r float32) (pt math32.Vector2, intersects bool) {
n := s2.X - c.X // Calculating using 64* rather than divide
m := s2.Y - c.Y
e := s2.X - s1.X
d := s2.Y - s1.Y
// Quadratic normal form coefficients
A, B, C := e*e+d*d, -2*(e*n+m*d), n*n+m*m-r*r
D := B*B - 4*A*C
if D <= 0 {
return // No intersection or is tangent
}
D = math32.Sqrt(D)
t1, t2 := (-B+D)/(2*A), (-B-D)/(2*A)
p1OnSide := t1 > 0
p2OnSide := t2 > 0
switch {
case p1OnSide && p2OnSide:
if t2 < t1 { // both on ray, use closest to s2
t1 = t2
}
case p2OnSide: // Only p2 on ray
t1 = t2
case p1OnSide: // only p1 on ray
default: // Neither solution is on the ray
return
}
return math32.Vec2((n-e*t1)+c.X, (m-d*t1)+c.Y), true
}
// Code generated by "core generate"; DO NOT EDIT.
package gradient
import (
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/types"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/colors/gradient.Base", IDName: "base", Doc: "Base contains the data and logic common to all gradient types.", Directives: []types.Directive{{Tool: "types", Directive: "add", Args: []string{"-setters"}}}, Fields: []types.Field{{Name: "Stops", Doc: "the stops for the gradient; use AddStop to add stops"}, {Name: "Spread", Doc: "the spread method used for the gradient if it stops before the end"}, {Name: "Blend", Doc: "the colorspace algorithm to use for blending colors"}, {Name: "Units", Doc: "the units to use for the gradient"}, {Name: "Box", Doc: "the bounding box of the object with the gradient; this is used when rendering\ngradients with [Units] of [ObjectBoundingBox]."}, {Name: "Transform", Doc: "Transform is the gradient's own transformation matrix applied to the gradient's points.\nThis is a property of the Gradient itself."}, {Name: "Opacity", Doc: "Opacity is the overall object opacity multiplier, applied in conjunction with the\nstop-level opacity blending."}, {Name: "ApplyFuncs", Doc: "ApplyFuncs contains functions that are applied to the color after gradient color is generated.\nThis allows for efficient StateLayer and other post-processing effects\nto be applied. The Applier handles other cases, but gradients always\nmust have the Update function called at render time, so they must\nremain Gradient types."}, {Name: "boxTransform", Doc: "boxTransform is the Transform applied to the bounding Box,\nonly for [Units] == [ObjectBoundingBox]."}, {Name: "stopsRGB", Doc: "stopsRGB are the computed RGB stops for blend types other than RGB"}, {Name: "stopsRGBSrc", Doc: "stopsRGBSrc are the source Stops when StopsRGB were last computed"}}})
// SetSpread sets the [Base.Spread]:
// the spread method used for the gradient if it stops before the end
func (t *Base) SetSpread(v Spreads) *Base { t.Spread = v; return t }
// SetBlend sets the [Base.Blend]:
// the colorspace algorithm to use for blending colors
func (t *Base) SetBlend(v colors.BlendTypes) *Base { t.Blend = v; return t }
// SetUnits sets the [Base.Units]:
// the units to use for the gradient
func (t *Base) SetUnits(v Units) *Base { t.Units = v; return t }
// SetBox sets the [Base.Box]:
// the bounding box of the object with the gradient; this is used when rendering
// gradients with [Units] of [ObjectBoundingBox].
func (t *Base) SetBox(v math32.Box2) *Base { t.Box = v; return t }
// SetTransform sets the [Base.Transform]:
// Transform is the gradient's own transformation matrix applied to the gradient's points.
// This is a property of the Gradient itself.
func (t *Base) SetTransform(v math32.Matrix2) *Base { t.Transform = v; return t }
// SetOpacity sets the [Base.Opacity]:
// Opacity is the overall object opacity multiplier, applied in conjunction with the
// stop-level opacity blending.
func (t *Base) SetOpacity(v float32) *Base { t.Opacity = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/colors/gradient.Linear", IDName: "linear", Doc: "Linear represents a linear 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: "Start", Doc: "the starting point of the gradient (x1 and y1 in SVG)"}, {Name: "End", Doc: "the ending point of the gradient (x2 and y2 in SVG)"}, {Name: "rStart", Doc: "computed current render versions transformed by object matrix"}, {Name: "rEnd"}, {Name: "distance"}, {Name: "distanceLengthSquared"}}})
// SetStart sets the [Linear.Start]:
// the starting point of the gradient (x1 and y1 in SVG)
func (t *Linear) SetStart(v math32.Vector2) *Linear { t.Start = v; return t }
// SetEnd sets the [Linear.End]:
// 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"}}})
// SetCenter sets the [Radial.Center]:
// the center point of the gradient (cx and cy in SVG)
func (t *Radial) SetCenter(v math32.Vector2) *Radial { t.Center = v; return t }
// SetFocal sets the [Radial.Focal]:
// the focal point of the gradient (fx and fy in SVG)
func (t *Radial) SetFocal(v math32.Vector2) *Radial { t.Focal = v; return t }
// SetRadius sets the [Radial.Radius]:
// the radius of the gradient (rx and ry in SVG)
func (t *Radial) SetRadius(v math32.Vector2) *Radial { t.Radius = v; return t }
// 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 colors
import (
"image"
"image/color"
)
// Uniform returns a new [image.Uniform] filled completely with the given color.
func Uniform(c color.Color) image.Image {
return image.NewUniform(c)
}
// ToUniform converts the given image to a uniform [color.RGBA] color.
func ToUniform(img image.Image) color.RGBA {
if img == nil {
return color.RGBA{}
}
return AsRGBA(img.At(0, 0))
}
// 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}
}
type pattern struct {
f func(x, y int) color.Color
}
func (p *pattern) ColorModel() color.Model {
return color.RGBAModel
}
func (p *pattern) Bounds() image.Rectangle {
return image.Rect(-1e9, -1e9, 1e9, 1e9)
}
func (p *pattern) At(x, y int) color.Color {
return p.f(x, y)
}
// 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 colors
import (
"image/color"
"cogentcore.org/core/colors/matcolor"
)
// Palette contains the main, global [MatPalette]. It can
// be used by end-user code for accessing tonal palette values,
// although [Scheme] is a more typical way to access the color
// scheme values. It defaults to a palette based around a
// primary color of Google Blue (#4285f4)
var Palette = matcolor.NewPalette(matcolor.KeyFromPrimary(color.RGBA{66, 133, 244, 255}))
// Schemes are the main global Material Design 3 color schemes.
// They should not be used for accessing the current color scheme;
// see [Scheme] for that. Instead, they should be set if you want
// to define your own custom color schemes for your app. The recommended
// way to set the Schemes is through the [SetSchemes] function.
var Schemes = matcolor.NewSchemes(Palette)
// Scheme is the main currently active global Material Design 3
// color scheme. It is the main way that end-user code should
// access the color scheme; ideally, almost all color values should
// be set to something in here. For more specific tones of colors,
// see [Palette]. For setting the color schemes of your app, see
// [Schemes] and [SetSchemes]. For setting this scheme to
// be light or dark, see [SetScheme].
var Scheme = &Schemes.Light
// SetSchemes sets [Schemes], [Scheme], and [Palette] based on the
// given primary color. It is the main way that end-user code should
// set the color schemes to something custom. For more specific control,
// see [SetSchemesFromKey].
func SetSchemes(primary color.RGBA) {
SetSchemesFromKey(matcolor.KeyFromPrimary(primary))
}
// SetSchemes sets [Schemes], [Scheme], and [Palette] based on the
// given [matcolor.Key]. It should be used instead of [SetSchemes]
// if you want more specific control over the color scheme.
func SetSchemesFromKey(key *matcolor.Key) {
Palette = matcolor.NewPalette(key)
Schemes = matcolor.NewSchemes(Palette)
SetScheme(matcolor.SchemeIsDark)
}
// SetScheme sets the value of [Scheme] to either [Schemes.Dark]
// or [Schemes.Light], based on the given value of whether the
// color scheme should be dark. It also sets the value of
// [matcolor.SchemeIsDark].
func SetScheme(isDark bool) {
matcolor.SchemeIsDark = isDark
if isDark {
Scheme = &Schemes.Dark
} else {
Scheme = &Schemes.Light
}
}
// 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 matcolor
import (
"image"
)
// Accent contains the four standard variations of a base accent color.
type Accent struct {
// Base is the base color for typically high-emphasis content.
Base image.Image
// On is the color applied to content on top of [Accent.Base].
On image.Image
// Container is the color applied to elements with less emphasis than [Accent.Base].
Container image.Image
// OnContainer is the color applied to content on top of [Accent.Container].
OnContainer image.Image
}
// NewAccentLight returns a new light theme [Accent] from the given [Tones].
func NewAccentLight(tones Tones) Accent {
return Accent{
Base: tones.AbsToneUniform(40),
On: tones.AbsToneUniform(100),
Container: tones.AbsToneUniform(90),
OnContainer: tones.AbsToneUniform(10),
}
}
// NewAccentDark returns a new dark theme [Accent] from the given [Tones].
func NewAccentDark(tones Tones) Accent {
return Accent{
Base: tones.AbsToneUniform(80),
On: tones.AbsToneUniform(20),
Container: tones.AbsToneUniform(30),
OnContainer: tones.AbsToneUniform(90),
}
}
// 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.
// Based on https://github.com/material-foundation/material-color-utilities/blob/main/dart/lib/palettes/core_palette.dart
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package matcolor
import (
"image/color"
"cogentcore.org/core/colors/cam/hct"
)
// Key contains the set of key colors used to generate
// a [Scheme] and [Palette]
type Key struct {
// the primary accent key color
Primary color.RGBA
// the secondary accent key color
Secondary color.RGBA
// the tertiary accent key color
Tertiary color.RGBA
// the select accent key color
Select color.RGBA
// the error accent key color
Error color.RGBA
// the success accent key color
Success color.RGBA
// the warn accent key color
Warn color.RGBA
// the neutral key color used to generate surface and surface container colors
Neutral color.RGBA
// the neutral variant key color used to generate surface variant and outline colors
NeutralVariant color.RGBA
// an optional map of custom accent key colors
Custom map[string]color.RGBA
}
// Key returns a new [Key] from the given primary accent key color.
func KeyFromPrimary(primary color.RGBA) *Key {
k := &Key{}
p := hct.FromColor(primary)
p.SetTone(40)
k.Primary = p.WithChroma(max(p.Chroma, 48)).AsRGBA()
k.Secondary = p.WithChroma(16).AsRGBA()
// Material adds 60, but we subtract 60 to get green instead of pink when specifying
// blue (TODO: is this a good idea, or should we just follow Material?)
k.Tertiary = p.WithHue(p.Hue - 60).WithChroma(24).AsRGBA()
k.Select = p.WithChroma(24).AsRGBA()
k.Error = color.RGBA{179, 38, 30, 255} // #B3261E (Material default error color)
k.Success = color.RGBA{50, 168, 50, 255} // #32a832 (arbitrarily chosen; TODO: maybe come up with a better default success color)
k.Warn = color.RGBA{168, 143, 50, 255} // #a88f32 (arbitrarily chosen; TODO: maybe come up with a better default warn color)
k.Neutral = p.WithChroma(4).AsRGBA()
k.NeutralVariant = p.WithChroma(8).AsRGBA()
return k
}
// 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 matcolor
// Palette contains a tonal palette with tonal values
// for each of the standard colors and any custom colors.
// Use [NewPalette] to create a new palette.
type Palette struct {
// the tones for the primary key color
Primary Tones
// the tones for the secondary key color
Secondary Tones
// the tones for the tertiary key color
Tertiary Tones
// the tones for the select key color
Select Tones
// the tones for the error key color
Error Tones
// the tones for the success key color
Success Tones
// the tones for the warn key color
Warn Tones
// the tones for the neutral key color
Neutral Tones
// the tones for the neutral variant key color
NeutralVariant Tones
// an optional map of tones for custom accent key colors
Custom map[string]Tones
}
// NewPalette creates a new [Palette] from the given key colors.
func NewPalette(key *Key) *Palette {
p := &Palette{
Primary: NewTones(key.Primary),
Secondary: NewTones(key.Secondary),
Tertiary: NewTones(key.Tertiary),
Select: NewTones(key.Select),
Error: NewTones(key.Error),
Success: NewTones(key.Success),
Warn: NewTones(key.Warn),
Neutral: NewTones(key.Neutral),
NeutralVariant: NewTones(key.NeutralVariant),
}
for name, c := range key.Custom {
p.Custom[name] = NewTones(c)
}
return p
}
// 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 matcolor
import (
"image"
"image/color"
)
//go:generate core generate
// Scheme contains the colors for one color scheme (ex: light or dark).
// To generate a scheme, use [NewScheme].
type Scheme struct {
// Primary is the primary color applied to important elements
Primary Accent
// Secondary is the secondary color applied to less important elements
Secondary Accent
// Tertiary is the tertiary color applied as an accent to highlight elements and create contrast between other colors
Tertiary Accent
// Select is the selection color applied to selected or highlighted elements and text
Select Accent
// Error is the error color applied to elements that indicate an error or danger
Error Accent
// Success is the color applied to elements that indicate success
Success Accent
// Warn is the color applied to elements that indicate a warning
Warn Accent
// an optional map of custom accent colors
Custom map[string]Accent
// SurfaceDim is the color applied to elements that will always have the dimmest surface color (see Surface for more information)
SurfaceDim image.Image
// Surface is the color applied to contained areas, like the background of an app
Surface image.Image
// SurfaceBright is the color applied to elements that will always have the brightest surface color (see Surface for more information)
SurfaceBright image.Image
// SurfaceContainerLowest is the color applied to surface container elements that have the lowest emphasis (see SurfaceContainer for more information)
SurfaceContainerLowest image.Image
// SurfaceContainerLow is the color applied to surface container elements that have lower emphasis (see SurfaceContainer for more information)
SurfaceContainerLow image.Image
// SurfaceContainer is the color applied to container elements that contrast elements with the surface color
SurfaceContainer image.Image
// SurfaceContainerHigh is the color applied to surface container elements that have higher emphasis (see SurfaceContainer for more information)
SurfaceContainerHigh image.Image
// SurfaceContainerHighest is the color applied to surface container elements that have the highest emphasis (see SurfaceContainer for more information)
SurfaceContainerHighest image.Image
// SurfaceVariant is the color applied to contained areas that contrast standard Surface elements
SurfaceVariant image.Image
// OnSurface is the color applied to content on top of Surface elements
OnSurface image.Image
// OnSurfaceVariant is the color applied to content on top of SurfaceVariant elements
OnSurfaceVariant image.Image
// InverseSurface is the color applied to elements to make them the reverse color of the surrounding elements and create a contrasting effect
InverseSurface image.Image
// InverseOnSurface is the color applied to content on top of InverseSurface
InverseOnSurface image.Image
// InversePrimary is the color applied to interactive elements on top of InverseSurface
InversePrimary image.Image
// Background is the color applied to the background of the app and other low-emphasis areas
Background image.Image
// OnBackground is the color applied to content on top of Background
OnBackground image.Image
// Outline is the color applied to borders to create emphasized boundaries that need to have sufficient contrast
Outline image.Image
// OutlineVariant is the color applied to create decorative boundaries
OutlineVariant image.Image
// Shadow is the color applied to shadows
Shadow image.Image
// SurfaceTint is the color applied to tint surfaces
SurfaceTint image.Image
// Scrim is the color applied to scrims (semi-transparent overlays)
Scrim image.Image
}
// NewLightScheme returns a new light-themed [Scheme]
// based on the given [Palette].
func NewLightScheme(p *Palette) Scheme {
s := Scheme{
Primary: NewAccentLight(p.Primary),
Secondary: NewAccentLight(p.Secondary),
Tertiary: NewAccentLight(p.Tertiary),
Select: NewAccentLight(p.Select),
Error: NewAccentLight(p.Error),
Success: NewAccentLight(p.Success),
Warn: NewAccentLight(p.Warn),
Custom: map[string]Accent{},
SurfaceDim: p.Neutral.AbsToneUniform(87),
Surface: p.Neutral.AbsToneUniform(98),
SurfaceBright: p.Neutral.AbsToneUniform(98),
SurfaceContainerLowest: p.Neutral.AbsToneUniform(100),
SurfaceContainerLow: p.Neutral.AbsToneUniform(96),
SurfaceContainer: p.Neutral.AbsToneUniform(94),
SurfaceContainerHigh: p.Neutral.AbsToneUniform(92),
SurfaceContainerHighest: p.Neutral.AbsToneUniform(90),
SurfaceVariant: p.NeutralVariant.AbsToneUniform(90),
OnSurface: p.NeutralVariant.AbsToneUniform(10),
OnSurfaceVariant: p.NeutralVariant.AbsToneUniform(30),
InverseSurface: p.Neutral.AbsToneUniform(20),
InverseOnSurface: p.Neutral.AbsToneUniform(95),
InversePrimary: p.Primary.AbsToneUniform(80),
Background: p.Neutral.AbsToneUniform(98),
OnBackground: p.Neutral.AbsToneUniform(10),
Outline: p.NeutralVariant.AbsToneUniform(50),
OutlineVariant: p.NeutralVariant.AbsToneUniform(80),
Shadow: p.Neutral.AbsToneUniform(0),
SurfaceTint: p.Primary.AbsToneUniform(40),
Scrim: p.Neutral.AbsToneUniform(0),
}
for nm, c := range p.Custom {
s.Custom[nm] = NewAccentLight(c)
}
return s
// TODO: maybe fixed colors
}
// NewDarkScheme returns a new dark-themed [Scheme]
// based on the given [Palette].
func NewDarkScheme(p *Palette) Scheme {
s := Scheme{
Primary: NewAccentDark(p.Primary),
Secondary: NewAccentDark(p.Secondary),
Tertiary: NewAccentDark(p.Tertiary),
Select: NewAccentDark(p.Select),
Error: NewAccentDark(p.Error),
Success: NewAccentDark(p.Success),
Warn: NewAccentDark(p.Warn),
Custom: map[string]Accent{},
SurfaceDim: p.Neutral.AbsToneUniform(6),
Surface: p.Neutral.AbsToneUniform(6),
SurfaceBright: p.Neutral.AbsToneUniform(24),
SurfaceContainerLowest: p.Neutral.AbsToneUniform(4),
SurfaceContainerLow: p.Neutral.AbsToneUniform(10),
SurfaceContainer: p.Neutral.AbsToneUniform(12),
SurfaceContainerHigh: p.Neutral.AbsToneUniform(17),
SurfaceContainerHighest: p.Neutral.AbsToneUniform(22),
SurfaceVariant: p.NeutralVariant.AbsToneUniform(30),
OnSurface: p.NeutralVariant.AbsToneUniform(90),
OnSurfaceVariant: p.NeutralVariant.AbsToneUniform(80),
InverseSurface: p.Neutral.AbsToneUniform(90),
InverseOnSurface: p.Neutral.AbsToneUniform(20),
InversePrimary: p.Primary.AbsToneUniform(40),
Background: p.Neutral.AbsToneUniform(6),
OnBackground: p.Neutral.AbsToneUniform(90),
Outline: p.NeutralVariant.AbsToneUniform(60),
OutlineVariant: p.NeutralVariant.AbsToneUniform(30),
// We want some visible "glow" shadow, but not too much
Shadow: image.NewUniform(color.RGBA{127, 127, 127, 127}),
SurfaceTint: p.Primary.AbsToneUniform(80),
Scrim: p.Neutral.AbsToneUniform(0),
}
for nm, c := range p.Custom {
s.Custom[nm] = NewAccentDark(c)
}
return s
// TODO: custom and fixed colors?
}
// SchemeIsDark is whether the currently active color scheme
// is a dark-themed or light-themed color scheme. In almost
// all cases, it should be set via [cogentcore.org/core/colors.SetScheme],
// not directly.
var SchemeIsDark = false
// 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 matcolor
// Schemes contains multiple color schemes
// (light, dark, and any custom ones).
type Schemes struct {
Light Scheme
Dark Scheme
// TODO: maybe custom schemes?
}
// NewSchemes returns new [Schemes] for the given
// [Palette] containing both light and dark schemes.
func NewSchemes(p *Palette) *Schemes {
return &Schemes{
Light: NewLightScheme(p),
Dark: NewDarkScheme(p),
}
}
// 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 matcolor
import (
"image"
"image/color"
"cogentcore.org/core/colors/cam/hct"
)
// Tones contains cached color values for each tone
// of a seed color. To get a tonal value, use [Tones.Tone].
type Tones struct {
// the key color used to generate these tones
Key color.RGBA
// the cached map of tonal color values
Tones map[int]color.RGBA
}
// NewTones returns a new set of [Tones]
// for the given color.
func NewTones(c color.RGBA) Tones {
return Tones{
Key: c,
Tones: map[int]color.RGBA{},
}
}
// AbsTone returns the color at the given absolute
// tone on a scale of 0 to 100. It uses the cached
// value if it exists, and it caches the value if
// it is not already.
func (t *Tones) AbsTone(tone int) color.RGBA {
if c, ok := t.Tones[tone]; ok {
return c
}
c := hct.FromColor(t.Key)
c.SetTone(float32(tone))
r := c.AsRGBA()
t.Tones[tone] = r
return r
}
// AbsToneUniform returns [image.Uniform] of [Tones.AbsTone].
func (t *Tones) AbsToneUniform(tone int) *image.Uniform {
return image.NewUniform(t.AbsTone(tone))
}
// Tone returns the color at the given tone, relative to the "0" tone
// for the current color scheme (0 for light-themed schemes and 100 for
// dark-themed schemes).
func (t *Tones) Tone(tone int) color.RGBA {
if SchemeIsDark {
return t.AbsTone(100 - tone)
}
return t.AbsTone(tone)
}
// ToneUniform returns [image.Uniform] of [Tones.Tone].
func (t *Tones) ToneUniform(tone int) *image.Uniform {
return image.NewUniform(t.Tone(tone))
}
// 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 colors
import "image/color"
// RGBAF32 stores alpha-premultiplied RGBA values in a float32 0 to 1
// normalized format, which is more useful for converting to other spaces
type RGBAF32 struct {
R, G, B, A float32
}
// RGBA implements the color.Color interface
func (c RGBAF32) RGBA() (r, g, b, a uint32) {
r = uint32(c.R*65535.0 + 0.5)
g = uint32(c.G*65535.0 + 0.5)
b = uint32(c.B*65535.0 + 0.5)
a = uint32(c.A*65535.0 + 0.5)
return
}
// FromRGBAF32 returns the color specified by the given float32
// alpha-premultiplied RGBA values in the range 0 to 1
func FromRGBAF32(r, g, b, a float32) color.RGBA {
return AsRGBA(RGBAF32{r, g, b, a})
}
// NRGBAF32 stores non-alpha-premultiplied RGBA values in a float32 0 to 1
// normalized format, which is more useful for converting to other spaces
type NRGBAF32 struct {
R, G, B, A float32
}
// RGBA implements the color.Color interface
func (c NRGBAF32) RGBA() (r, g, b, a uint32) {
r = uint32(c.R*c.A*65535.0 + 0.5)
g = uint32(c.G*c.A*65535.0 + 0.5)
b = uint32(c.B*c.A*65535.0 + 0.5)
a = uint32(c.A*65535.0 + 0.5)
return
}
// FromNRGBAF32 returns the color specified by the given float32
// non alpha-premultiplied RGBA values in the range 0 to 1
func FromNRGBAF32(r, g, b, a float32) color.RGBA {
return AsRGBA(NRGBAF32{r, g, b, a})
}
var (
// RGBAF32Model is the model for converting colors to [RGBAF32] colors
RGBAF32Model color.Model = color.ModelFunc(rgbaf32Model)
// NRGBAF32Model is the model for converting colors to [NRGBAF32] colors
NRGBAF32Model color.Model = color.ModelFunc(nrgbaf32Model)
)
func rgbaf32Model(c color.Color) color.Color {
if _, ok := c.(RGBAF32); ok {
return c
}
r, g, b, a := c.RGBA()
return RGBAF32{float32(r) / 65535.0, float32(g) / 65535.0, float32(b) / 65535.0, float32(a) / 65535.0}
}
func nrgbaf32Model(c color.Color) color.Color {
if _, ok := c.(NRGBAF32); ok {
return c
}
r, g, b, a := c.RGBA()
if a > 0 {
// Since color.Color is alpha pre-multiplied, we need to divide the
// RGB values by alpha again in order to get back the original RGB.
r *= 0xffff
r /= a
g *= 0xffff
g /= a
b *= 0xffff
b /= a
}
return NRGBAF32{float32(r) / 65535.0, float32(g) / 65535.0, float32(b) / 65535.0, float32(a) / 65535.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 colors
import (
"image/color"
"cogentcore.org/core/colors/cam/hct"
"cogentcore.org/core/colors/matcolor"
)
// Spaced returns a maximally widely spaced sequence of colors
// for progressive values of the index, using the HCT space.
// This is useful, for example, for assigning colors in graphs.
func Spaced(idx int) color.RGBA {
if matcolor.SchemeIsDark {
return spacedDark(idx)
}
return spacedLight(idx)
}
// spacedLight is the light mode version of [Spaced].
func spacedLight(idx int) color.RGBA {
// red, blue, green, yellow, violet, aqua, orange, blueviolet
// hues := []float32{30, 280, 140, 110, 330, 200, 70, 305}
hues := []float32{25, 255, 150, 105, 340, 210, 60, 300}
// even 45: 30, 75, 120, 165, 210, 255, 300, 345,
toffs := []float32{0, -10, 0, 5, 0, 0, 5, 0}
tones := []float32{65, 80, 45, 65, 80}
chromas := []float32{90, 90, 90, 20, 20}
ncats := len(hues)
ntc := len(tones)
hi := idx % ncats
hr := idx / ncats
tci := hr % ntc
hue := hues[hi]
tone := toffs[hi] + tones[tci]
chroma := chromas[tci]
return hct.New(hue, float32(chroma), tone).AsRGBA()
}
// spacedDark is the dark mode version of [Spaced].
func spacedDark(idx int) color.RGBA {
// red, blue, green, yellow, violet, aqua, orange, blueviolet
// hues := []float32{30, 280, 140, 110, 330, 200, 70, 305}
hues := []float32{25, 255, 150, 105, 340, 210, 60, 300}
// even 45: 30, 75, 120, 165, 210, 255, 300, 345,
toffs := []float32{0, -10, 0, 10, 0, 0, 5, 0}
tones := []float32{65, 80, 45, 65, 80}
chromas := []float32{90, 90, 90, 20, 20}
ncats := len(hues)
ntc := len(tones)
hi := idx % ncats
hr := idx / ncats
tci := hr % ntc
hue := hues[hi]
tone := toffs[hi] + tones[tci]
chroma := chromas[tci]
return hct.New(hue, float32(chroma), tone).AsRGBA()
}
// 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 (
"image"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/colors"
"cogentcore.org/core/icons"
"cogentcore.org/core/svg"
"cogentcore.org/core/system"
)
var (
// TheApp is the current [App]; only one is ever in effect.
TheApp = &App{App: system.TheApp}
// AppAbout is the about information for the current app.
// It is set by a linker flag in the core command line tool.
AppAbout string
// AppIcon is the svg icon for the current app.
// It is set by a linker flag in the core command line tool.
// It defaults to [icons.CogentCore] otherwise.
AppIcon string = string(icons.CogentCore)
)
// App represents a Cogent Core app. It extends [system.App] to provide both system-level
// and high-level data and functions to do with the currently running application. The
// single instance of it is [TheApp], which embeds [system.TheApp].
type App struct { //types:add -setters
system.App `set:"-"`
// SceneInit is a function called on every newly created [Scene].
// This can be used to set global configuration and styling for all
// widgets in conjunction with [Scene.WidgetInit].
SceneInit func(sc *Scene) `edit:"-"`
}
// appIconImagesCache is a cached version of [appIconImages].
var appIconImagesCache []image.Image
// appIconImages returns a slice of images of sizes 16x16, 32x32, and 48x48
// rendered from [AppIcon]. It returns nil if [AppIcon] is "" or if there is
// an error. It automatically logs any errors. It caches the result for future
// calls.
func appIconImages() []image.Image {
if appIconImagesCache != nil {
return appIconImagesCache
}
if AppIcon == "" {
return nil
}
res := make([]image.Image, 3)
sv := svg.NewSVG(16, 16)
sv.Color = colors.Uniform(colors.FromRGB(66, 133, 244)) // Google Blue (#4285f4)
err := sv.ReadXML(strings.NewReader(AppIcon))
if errors.Log(err) != nil {
return nil
}
sv.Render()
res[0] = sv.Pixels
sv.Resize(image.Pt(32, 32))
sv.Render()
res[1] = sv.Pixels
sv.Resize(image.Pt(48, 48))
sv.Render()
res[2] = sv.Pixels
appIconImagesCache = res
return res
}
// 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 (
"slices"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/styles"
"cogentcore.org/core/system"
)
// BarFuncs are functions for creating control bars,
// attached to different sides of a [Scene]. Functions
// are called in forward order so first added are called first.
type BarFuncs []func(bar *Frame)
// Add adds the given function for configuring a control bar
func (bf *BarFuncs) Add(fun func(bar *Frame)) *BarFuncs {
*bf = append(*bf, fun)
return bf
}
// call calls all the functions for configuring given widget
func (bf *BarFuncs) call(bar *Frame) {
for _, fun := range *bf {
fun(bar)
}
}
// isEmpty returns true if there are no functions added
func (bf *BarFuncs) isEmpty() bool {
return len(*bf) == 0
}
// makeSceneBars configures the side control bars, for main scenes.
func (sc *Scene) makeSceneBars() {
sc.addDefaultBars()
if !sc.Bars.Top.isEmpty() {
head := NewFrame(sc)
head.SetName("top-bar")
head.Styler(func(s *styles.Style) {
s.Align.Items = styles.Center
s.Grow.Set(1, 0)
})
sc.Bars.Top.call(head)
}
if !sc.Bars.Left.isEmpty() || !sc.Bars.Right.isEmpty() {
mid := NewFrame(sc)
mid.SetName("body-area")
if !sc.Bars.Left.isEmpty() {
left := NewFrame(mid)
left.SetName("left-bar")
left.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Align.Items = styles.Center
s.Grow.Set(0, 1)
})
sc.Bars.Left.call(left)
}
if sc.Body != nil {
mid.AddChild(sc.Body)
}
if !sc.Bars.Right.isEmpty() {
right := NewFrame(mid)
right.SetName("right-bar")
right.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Align.Items = styles.Center
s.Grow.Set(0, 1)
})
sc.Bars.Right.call(right)
}
} else {
if sc.Body != nil {
sc.AddChild(sc.Body)
}
}
if !sc.Bars.Bottom.isEmpty() {
foot := NewFrame(sc)
foot.SetName("bottom-bar")
foot.Styler(func(s *styles.Style) {
s.Justify.Content = styles.End
s.Align.Items = styles.Center
s.Grow.Set(1, 0)
})
sc.Bars.Bottom.call(foot)
}
}
func (sc *Scene) addDefaultBars() {
st := sc.Stage
addBack := st.BackButton.Or(st.FullWindow && !st.NewWindow && !(st.Mains != nil && st.Mains.stack.Len() == 0) && TheApp.Platform() != system.Offscreen)
if addBack || st.DisplayTitle {
sc.Bars.Top = slices.Insert(sc.Bars.Top, 0, func(bar *Frame) {
if addBack {
back := NewButton(bar).SetIcon(icons.ArrowBack).SetKey(keymap.HistPrev)
back.SetType(ButtonAction).SetTooltip("Back")
back.OnClick(func(e events.Event) {
sc.Close()
})
}
if st.DisplayTitle {
title := NewText(bar).SetType(TextHeadlineSmall)
title.Updater(func() {
title.SetText(sc.Body.Title)
})
}
})
}
}
//////////////////////////////////////////////////////////////
// Scene wrappers
// AddTopBar adds the given function for configuring a control bar
// at the top of the window
func (bd *Body) AddTopBar(fun func(bar *Frame)) {
bd.Scene.Bars.Top.Add(fun)
}
// AddLeftBar adds the given function for configuring a control bar
// on the left of the window
func (bd *Body) AddLeftBar(fun func(bar *Frame)) {
bd.Scene.Bars.Left.Add(fun)
}
// AddRightBar adds the given function for configuring a control bar
// on the right of the window
func (bd *Body) AddRightBar(fun func(bar *Frame)) {
bd.Scene.Bars.Right.Add(fun)
}
// AddBottomBar adds the given function for configuring a control bar
// at the bottom of the window
func (bd *Body) AddBottomBar(fun func(bar *Frame)) {
bd.Scene.Bars.Bottom.Add(fun)
}
// 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()
}
}
// 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 (
"cogentcore.org/core/base/errors"
"cogentcore.org/core/styles"
"cogentcore.org/core/tree"
)
// Body holds the primary content of a [Scene].
// It is the main container for app content.
type Body struct { //core:no-new
Frame
// Title is the title of the body, which is also
// used for the window title where relevant.
Title string `set:"-"`
}
// NewBody creates a new [Body] that will serve as the content of a [Scene]
// (e.g., a Window, Dialog, etc). [Body] forms the central region
// of a [Scene], and has [styles.OverflowAuto] scrollbars by default.
// It will create its own parent [Scene] at this point, and has wrapper
// functions to transparently manage everything that the [Scene]
// typically manages during configuration, so you can usually avoid
// having to access the [Scene] directly. If a name is given, it will
// be used for the name of the window, and a title widget will be created
// with that text if [Stage.DisplayTitle] is true. Also, if the name of
// [TheApp] is unset, it sets it to the given name.
func NewBody(name ...string) *Body {
bd := tree.New[Body]()
nm := "body"
if len(name) > 0 {
nm = name[0]
}
if TheApp.Name() == "" {
if len(name) == 0 {
nm = "Cogent Core" // first one is called Cogent Core by default
}
TheApp.SetName(nm)
}
if AppearanceSettings.Zoom == 0 {
// we load the settings in NewBody so that people can
// add their own settings to AllSettings first
errors.Log(LoadAllSettings())
}
bd.SetName(nm)
bd.Title = nm
bd.Scene = newBodyScene(bd)
return bd
}
func (bd *Body) Init() {
bd.Frame.Init()
bd.Styler(func(s *styles.Style) {
s.Overflow.Set(styles.OverflowAuto)
s.Direction = styles.Column
s.Grow.Set(1, 1)
})
}
// SetTitle sets the title in the [Body], [Scene], [Stage], [renderWindow],
// and title widget. This is the one place to change the title for everything.
func (bd *Body) SetTitle(title string) *Body {
bd.Name = title
bd.Title = title
bd.Scene.Name = title
if bd.Scene.Stage != nil {
bd.Scene.Stage.Title = title
win := bd.Scene.RenderWindow()
if win != nil {
win.setName(title)
win.setTitle(title)
}
}
// title widget is contained within the top bar
if tb, ok := bd.Scene.ChildByName("top-bar").(Widget); ok {
tb.AsWidget().Update()
}
return bd
}
// SetData sets the [Body]'s [Scene.Data].
func (bd *Body) SetData(data any) *Body {
bd.Scene.SetData(data)
return bd
}
// 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 core
import (
"image"
"log/slog"
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/events/key"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"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/tree"
)
// Button is an interactive button with text, an icon, an indicator, a shortcut,
// and/or a menu. The standard behavior is to register a click event handler with
// [WidgetBase.OnClick].
type Button struct { //core:embedder
Frame
// Type is the type of button.
Type ButtonTypes
// Text is the text for the button.
// If it is blank, no text is shown.
Text string
// Icon is the icon for the button.
// If it is "" or [icons.None], no icon is shown.
Icon icons.Icon
// Indicator is the menu indicator icon to present.
// If it is "" or [icons.None],, no indicator is shown.
// It is automatically set to [icons.KeyboardArrowDown]
// when there is a Menu elements present unless it is
// set to [icons.None].
Indicator icons.Icon
// Shortcut is an optional shortcut keyboard chord to trigger this button,
// active in window-wide scope. Avoid conflicts with other shortcuts
// (a log message will be emitted if so). Shortcuts are processed after
// all other processing of keyboard input. Command is automatically translated
// into Meta on macOS and Control on all other platforms. Also see [Button.SetKey].
Shortcut key.Chord
// Menu is a menu constructor function used to build and display
// a menu whenever the button is clicked. There will be no menu
// if it is nil. The constructor function should add buttons
// to the Scene that it is passed.
Menu func(m *Scene) `json:"-" xml:"-"`
}
// ButtonTypes is an enum containing the
// different possible types of buttons.
type ButtonTypes int32 //enums:enum -trim-prefix Button
const (
// ButtonFilled is a filled button with a
// contrasting background color. It should be
// used for prominent actions, typically those
// that are the final in a sequence. It is equivalent
// to Material Design's filled button.
ButtonFilled ButtonTypes = iota
// ButtonTonal is a filled button, similar
// to [ButtonFilled]. It is used for the same purposes,
// but it has a lighter background color and less emphasis.
// It is equivalent to Material Design's filled tonal button.
ButtonTonal
// ButtonElevated is an elevated button with
// a light background color and a shadow.
// It is equivalent to Material Design's elevated button.
ButtonElevated
// ButtonOutlined is an outlined button that is
// used for secondary actions that are still important.
// It is equivalent to Material Design's outlined button.
ButtonOutlined
// ButtonText is a low-importance button with no border,
// background color, or shadow when not being interacted with.
// It renders primary-colored text, and it renders a background
// color and shadow when hovered/focused/active.
// It should only be used for low emphasis
// actions, and you must ensure it stands out from the
// surrounding context sufficiently. It is equivalent
// to Material Design's text button, but it can also
// contain icons and other things.
ButtonText
// ButtonAction is a simple button that typically serves
// as a simple action among a series of other buttons
// (eg: in a toolbar), or as a part of another widget,
// like a spinner or snackbar. It has no border, background color,
// or shadow when not being interacted with. It inherits the text
// color of its parent, and it renders a background when
// hovered/focused/active. You must ensure it stands out from the
// surrounding context sufficiently. It is equivalent to Material Design's
// icon button, but it can also contain text and other things (and frequently does).
ButtonAction
// ButtonMenu is similar to [ButtonAction], but it is designed
// for buttons located in popup menus.
ButtonMenu
)
func (bt *Button) Init() {
bt.Frame.Init()
bt.Styler(func(s *styles.Style) {
s.SetAbilities(true, abilities.Activatable, abilities.Focusable, abilities.Hoverable, abilities.DoubleClickable, abilities.TripleClickable)
if !bt.IsDisabled() {
s.Cursor = cursors.Pointer
}
s.Border.Radius = styles.BorderRadiusFull
s.Padding.Set(units.Dp(10), units.Dp(24))
if bt.Icon.IsSet() {
s.Padding.Left.Dp(16)
if bt.Text == "" {
s.Padding.Right.Dp(16)
}
}
s.Font.Size.Dp(14) // Button font size is used for text font size
s.Gap.Zero()
s.CenterAll()
s.MaxBoxShadow = styles.BoxShadow1()
switch bt.Type {
case ButtonFilled:
s.Background = colors.Scheme.Primary.Base
s.Color = colors.Scheme.Primary.On
s.Border.Offset.Set(units.Dp(2))
case ButtonTonal:
s.Background = colors.Scheme.Secondary.Container
s.Color = colors.Scheme.Secondary.OnContainer
case ButtonElevated:
s.Background = colors.Scheme.SurfaceContainerLow
s.Color = colors.Scheme.Primary.Base
s.MaxBoxShadow = styles.BoxShadow2()
s.BoxShadow = styles.BoxShadow1()
case ButtonOutlined:
s.Color = colors.Scheme.Primary.Base
s.Border.Style.Set(styles.BorderSolid)
s.Border.Width.Set(units.Dp(1))
case ButtonText:
s.Color = colors.Scheme.Primary.Base
case ButtonAction:
s.MaxBoxShadow = styles.BoxShadow0()
case ButtonMenu:
s.Grow.Set(1, 0) // need to go to edge of menu
s.Justify.Content = styles.Start
s.Border.Radius.Zero()
s.Padding.Set(units.Dp(6), units.Dp(12))
s.MaxBoxShadow = styles.BoxShadow0()
}
if s.Is(states.Hovered) {
s.BoxShadow = s.MaxBoxShadow
}
if bt.IsDisabled() {
s.MaxBoxShadow = styles.BoxShadow0()
s.BoxShadow = s.MaxBoxShadow
}
})
bt.HandleClickOnEnterSpace()
bt.OnClick(func(e events.Event) {
if bt.openMenu(e) {
e.SetHandled()
}
})
bt.OnDoubleClick(func(e events.Event) {
bt.Send(events.Click, e)
})
bt.On(events.TripleClick, func(e events.Event) {
bt.Send(events.Click, e)
})
bt.Updater(func() {
// We must get the shortcuts every time since buttons
// may be added or removed dynamically.
bt.Events().getShortcutsIn(bt)
})
bt.Maker(func(p *tree.Plan) {
// we check if the icons are unset, not if they are nil, so
// that people can manually set it to [icons.None]
if bt.HasMenu() {
if bt.Type == ButtonMenu {
if bt.Indicator == "" {
bt.Indicator = icons.KeyboardArrowRight
}
} else if bt.Text != "" {
if bt.Indicator == "" {
bt.Indicator = icons.KeyboardArrowDown
}
} else {
if bt.Icon == "" {
bt.Icon = icons.Menu
}
}
}
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)
})
})
if bt.Text != "" {
tree.AddAt(p, "space", func(w *Space) {})
}
}
if bt.Text != "" {
tree.AddAt(p, "text", func(w *Text) {
w.Styler(func(s *styles.Style) {
s.SetNonSelectable()
s.SetTextWrap(false)
s.FillMargin = false
s.Font.Size = bt.Styles.Font.Size // Directly inherit to override the [Text.Type]-based default
})
w.Updater(func() {
if bt.Type == ButtonMenu {
w.SetType(TextBodyMedium)
} else {
w.SetType(TextLabelLarge)
}
w.SetText(bt.Text)
})
})
}
if bt.Indicator.IsSet() {
tree.AddAt(p, "indicator-stretch", func(w *Stretch) {
w.Styler(func(s *styles.Style) {
s.Min.Set(units.Em(0.2))
if bt.Type == ButtonMenu {
s.Grow.Set(1, 0)
} else {
s.Grow.Set(0, 0)
}
})
})
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()
})
w.Updater(func() {
w.SetIcon(bt.Indicator)
})
})
}
if bt.Type == ButtonMenu && (!TheApp.SystemPlatform().IsMobile() || TheApp.Platform() == system.Offscreen) {
if !bt.Indicator.IsSet() && bt.Shortcut != "" {
tree.AddAt(p, "shortcut-stretch", func(w *Stretch) {})
tree.AddAt(p, "shortcut", func(w *Text) {
w.Styler(func(s *styles.Style) {
s.SetNonSelectable()
s.SetTextWrap(false)
s.Color = colors.Scheme.OnSurfaceVariant
})
w.Updater(func() {
if bt.Type == ButtonMenu {
w.SetType(TextBodyMedium)
} else {
w.SetType(TextLabelLarge)
}
w.SetText(bt.Shortcut.Label())
})
})
} else if bt.Shortcut != "" {
slog.Error("programmer error: Button: shortcut cannot be used on a sub-menu for", "button", bt)
}
}
})
}
// SetKey sets the shortcut of the button from the given [keymap.Functions].
func (bt *Button) SetKey(kf keymap.Functions) *Button {
bt.SetShortcut(kf.Chord())
return bt
}
// Label returns the text of the button if it is set; otherwise it returns the name.
func (bt *Button) Label() string {
if bt.Text != "" {
return bt.Text
}
return bt.Name
}
// HasMenu returns true if the button has a menu that pops up when it is clicked
// (not that it is in a menu itself; see [ButtonMenu])
func (bt *Button) HasMenu() bool {
return bt.Menu != nil
}
// openMenu opens any menu associated with this element.
// It returns whether any menu was opened.
func (bt *Button) openMenu(e events.Event) bool {
if !bt.HasMenu() {
return false
}
pos := bt.ContextMenuPos(e)
if indic := bt.ChildByName("indicator", 3); indic != nil {
pos = indic.(Widget).ContextMenuPos(nil) // use the pos
}
m := NewMenu(bt.Menu, bt.This.(Widget), pos)
if m == nil {
return false
}
m.Run()
return true
}
func (bt *Button) handleClickDismissMenu() {
// note: must be called last so widgets aren't deleted when the click arrives
bt.OnFinal(events.Click, func(e events.Event) {
bt.Scene.Stage.closePopupAndBelow()
})
}
func (bt *Button) WidgetTooltip(pos image.Point) (string, image.Point) {
res := bt.Tooltip
if bt.Shortcut != "" && (!TheApp.SystemPlatform().IsMobile() || TheApp.Platform() == system.Offscreen) {
res = "[" + bt.Shortcut.Label() + "]"
if bt.Tooltip != "" {
res += " " + bt.Tooltip
}
}
return res, bt.DefaultTooltipPos()
}
// 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 (
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"golang.org/x/image/draw"
)
// Canvas is a widget that can be arbitrarily drawn to by setting
// its Draw function using [Canvas.SetDraw].
type Canvas struct {
WidgetBase
// Draw is the function used to draw the content of the
// canvas every time that it is rendered. The paint context
// is automatically normalized to the size of the canvas,
// so you should specify points on a 0-1 scale.
Draw func(pc *paint.Context)
// context is the paint context used for drawing.
context *paint.Context
}
func (c *Canvas) Init() {
c.WidgetBase.Init()
c.Styler(func(s *styles.Style) {
s.Min.Set(units.Dp(256))
})
}
func (c *Canvas) Render() {
c.WidgetBase.Render()
sz := c.Geom.Size.Actual.Content
szp := c.Geom.Size.Actual.Content.ToPoint()
c.context = paint.NewContext(szp.X, szp.Y)
c.context.UnitContext = c.Styles.UnitContext
c.context.ToDots()
c.context.PushTransform(math32.Scale2D(sz.X, sz.Y))
c.context.VectorEffect = styles.VectorEffectNonScalingStroke
c.Draw(c.context)
draw.Draw(c.Scene.Pixels, c.Geom.ContentBBox, c.context.Image, c.Geom.ScrollOffset(), draw.Over)
}
// 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 core
import (
"errors"
"fmt"
"image"
"log/slog"
"reflect"
"slices"
"strings"
"unicode"
"cogentcore.org/core/base/labels"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
"cogentcore.org/core/enums"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/parse/complete"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
// Chooser is a dropdown selection widget that allows users to choose
// one option among a list of items.
type Chooser struct {
Frame
// Type is the styling type of the chooser.
Type ChooserTypes
// Items are the chooser items available for selection.
Items []ChooserItem
// Icon is an optional icon displayed on the left side of the chooser.
Icon icons.Icon
// Indicator is the icon to use for the indicator displayed on the
// right side of the chooser.
Indicator icons.Icon
// Editable is whether provide a text field for editing the value,
// or just a button for selecting items.
Editable bool
// AllowNew is whether to allow the user to add new items to the
// chooser through the editable textfield (if Editable is set to
// true) and a button at the end of the chooser menu. See also [DefaultNew].
AllowNew bool
// DefaultNew configures the chooser to accept new items, as in
// [AllowNew], and also turns off completion popups and always
// adds new items to the list of items, without prompting.
// Use this for cases where the typical use-case is to enter new values,
// but the history of prior values can also be useful.
DefaultNew bool
// placeholder, if Editable is set to true, is the text that is
// displayed in the text field when it is empty. It must be set
// using [Chooser.SetPlaceholder].
placeholder string `set:"-"`
// ItemsFuncs is a slice of functions to call before showing the items
// of the chooser, which is typically used to configure them
// (eg: if they are based on dynamic data). The functions are called
// in ascending order such that the items added in the first function
// will appear before those added in the last function. Use
// [Chooser.AddItemsFunc] to add a new items function. If at least
// one ItemsFunc is specified, the items of the chooser will be
// cleared before calling the functions.
ItemsFuncs []func() `copier:"-" json:"-" xml:"-" set:"-"`
// CurrentItem is the currently selected item.
CurrentItem ChooserItem `json:"-" xml:"-" set:"-"`
// CurrentIndex is the index of the currently selected item
// in [Chooser.Items].
CurrentIndex int `json:"-" xml:"-" set:"-"`
text *Text
textField *TextField
}
// ChooserItem is an item that can be used in a [Chooser].
type ChooserItem struct {
// Value is the underlying value the chooser item represents.
Value any
// Text is the text displayed to the user for this item.
// If it is empty, then [labels.ToLabel] of [ChooserItem.Value]
// is used instead.
Text string
// Icon is the icon displayed to the user for this item.
Icon icons.Icon
// Tooltip is the tooltip displayed to the user for this item.
Tooltip string
// Func, if non-nil, is a function to call whenever this
// item is selected as the current value of the chooser.
Func func() `json:"-" xml:"-"`
// SeparatorBefore is whether to add a separator before
// this item in the chooser menu.
SeparatorBefore bool
}
// GetText returns the effective text for this chooser item.
// If [ChooserItem.Text] is set, it returns that. Otherwise,
// it returns [labels.ToLabel] of [ChooserItem.Value].
func (ci *ChooserItem) GetText() string {
if ci.Text != "" {
return ci.Text
}
if ci.Value == nil {
return ""
}
return labels.ToLabel(ci.Value)
}
// ChooserTypes is an enum containing the
// different possible types of combo boxes
type ChooserTypes int32 //enums:enum -trim-prefix Chooser
const (
// ChooserFilled represents a filled
// Chooser with a background color
// and a bottom border
ChooserFilled ChooserTypes = iota
// ChooserOutlined represents an outlined
// Chooser with a border on all sides
// and no background color
ChooserOutlined
)
func (ch *Chooser) WidgetValue() any { return ch.CurrentItem.Value }
func (ch *Chooser) SetWidgetValue(value any) error {
rv := reflect.ValueOf(value)
// If the first item is a pointer, we assume that our value should
// be a pointer. Otherwise, it should be a non-pointer value.
if len(ch.Items) > 0 && reflect.TypeOf(ch.Items[0].Value).Kind() == reflect.Pointer {
rv = reflectx.UnderlyingPointer(rv)
} else {
rv = reflectx.Underlying(rv)
}
ch.SetCurrentValue(rv.Interface())
return nil
}
func (ch *Chooser) OnBind(value any, tags reflect.StructTag) {
if e, ok := value.(enums.Enum); ok {
ch.SetEnum(e)
}
}
func (ch *Chooser) Init() {
ch.Frame.Init()
ch.SetIcon(icons.None).SetIndicator(icons.KeyboardArrowDown)
ch.CurrentIndex = -1
ch.Styler(func(s *styles.Style) {
if !s.IsReadOnly() {
s.SetAbilities(true, abilities.Activatable, abilities.Hoverable, abilities.LongHoverable)
if !ch.Editable {
s.SetAbilities(true, abilities.Focusable)
}
}
s.Text.Align = styles.Center
s.Border.Radius = styles.BorderRadiusSmall
s.Padding.Set(units.Dp(8), units.Dp(16))
s.CenterAll()
if !s.IsReadOnly() {
s.Cursor = cursors.Pointer
switch ch.Type {
case ChooserFilled:
s.Background = colors.Scheme.Secondary.Container
s.Color = colors.Scheme.Secondary.OnContainer
case ChooserOutlined:
if !s.Is(states.Focused) {
s.Border.Style.Set(styles.BorderSolid)
s.Border.Width.Set(units.Dp(1))
s.Border.Color.Set(colors.Scheme.OnSurfaceVariant)
}
}
}
// textfield handles everything
if ch.Editable {
s.RenderBox = false
s.Border = styles.Border{}
s.MaxBorder = s.Border
s.Background = nil
s.StateLayer = 0
s.Padding.Zero()
s.Border.Radius.Zero()
}
})
ch.OnClick(func(e events.Event) {
if ch.IsReadOnly() {
return
}
if ch.openMenu(e) {
e.SetHandled()
}
})
ch.OnChange(func(e events.Event) {
if ch.CurrentItem.Func != nil {
ch.CurrentItem.Func()
}
})
ch.OnFinal(events.KeyChord, func(e events.Event) {
tf := ch.textField
kf := keymap.Of(e.KeyChord())
if DebugSettings.KeyEventTrace {
slog.Info("Chooser KeyChordEvent", "widget", ch, "keyFunction", kf)
}
switch {
case kf == keymap.MoveUp:
e.SetHandled()
if len(ch.Items) > 0 {
index := ch.CurrentIndex - 1
if index < 0 {
index += len(ch.Items)
}
ch.selectItemEvent(index)
}
case kf == keymap.MoveDown:
e.SetHandled()
if len(ch.Items) > 0 {
index := ch.CurrentIndex + 1
if index >= len(ch.Items) {
index -= len(ch.Items)
}
ch.selectItemEvent(index)
}
case kf == keymap.PageUp:
e.SetHandled()
if len(ch.Items) > 10 {
index := ch.CurrentIndex - 10
for index < 0 {
index += len(ch.Items)
}
ch.selectItemEvent(index)
}
case kf == keymap.PageDown:
e.SetHandled()
if len(ch.Items) > 10 {
index := ch.CurrentIndex + 10
for index >= len(ch.Items) {
index -= len(ch.Items)
}
ch.selectItemEvent(index)
}
case kf == keymap.Enter || (!ch.Editable && e.KeyRune() == ' '):
// if !(kt.Rune == ' ' && chb.Sc.Type == ScCompleter) {
e.SetHandled()
ch.Send(events.Click, e)
// }
default:
if tf == nil {
break
}
// if we don't have anything special to do,
// we just give our key event to our textfield
tf.HandleEvent(e)
}
})
ch.Maker(func(p *tree.Plan) {
// automatically select the first item if we have nothing selected and no placeholder
if !ch.Editable && ch.CurrentIndex < 0 && ch.CurrentItem.Text == "" {
ch.SetCurrentIndex(0)
}
// editable handles through TextField
if ch.Icon.IsSet() && !ch.Editable {
tree.AddAt(p, "icon", func(w *Icon) {
w.Updater(func() {
w.SetIcon(ch.Icon)
})
})
}
if ch.Editable {
tree.AddAt(p, "text-field", func(w *TextField) {
ch.textField = w
ch.text = nil
w.SetPlaceholder(ch.placeholder)
w.Styler(func(s *styles.Style) {
s.Grow = ch.Styles.Grow // we grow like our parent
s.Max.X.Zero() // constrained by parent
s.SetTextWrap(false)
})
w.SetValidator(func() error {
err := ch.setCurrentText(w.Text())
if err == nil {
ch.SendChange()
}
return err
})
w.OnFocus(func(e events.Event) {
if ch.IsReadOnly() {
return
}
ch.CallItemsFuncs()
})
w.OnClick(func(e events.Event) {
ch.CallItemsFuncs()
w.offerComplete()
})
w.OnKeyChord(func(e events.Event) {
kf := keymap.Of(e.KeyChord())
if kf == keymap.Abort {
if w.error != nil {
w.clear()
w.clearError()
e.SetHandled()
}
}
})
w.Updater(func() {
if w.error != nil {
return // don't override anything when we have an invalid value
}
w.SetText(ch.CurrentItem.GetText()).SetLeadingIcon(ch.Icon).
SetTrailingIcon(ch.Indicator, func(e events.Event) {
ch.openMenu(e)
})
if ch.Type == ChooserFilled {
w.SetType(TextFieldFilled)
} else {
w.SetType(TextFieldOutlined)
}
if ch.DefaultNew && w.complete != nil {
w.complete = nil
} else if !ch.DefaultNew && w.complete == nil {
w.SetCompleter(w, ch.completeMatch, ch.completeEdit)
}
})
w.Maker(func(p *tree.Plan) {
tree.AddInit(p, "trail-icon", func(w *Button) {
w.Styler(func(s *styles.Style) {
// indicator does not need to be focused
s.SetAbilities(false, abilities.Focusable)
})
})
})
})
} else {
tree.AddAt(p, "text", func(w *Text) {
ch.text = w
ch.textField = nil
w.Styler(func(s *styles.Style) {
s.SetNonSelectable()
s.SetTextWrap(false)
})
w.Updater(func() {
w.SetText(ch.CurrentItem.GetText())
})
})
}
if ch.Indicator == "" {
ch.Indicator = icons.KeyboardArrowRight
}
// editable handles through TextField
if !ch.Editable && !ch.IsReadOnly() {
tree.AddAt(p, "indicator", func(w *Icon) {
w.Styler(func(s *styles.Style) {
s.Justify.Self = styles.End
})
w.Updater(func() {
w.SetIcon(ch.Indicator)
})
})
}
})
}
// AddItemsFunc adds the given function to [Chooser.ItemsFuncs].
// These functions are called before showing the items of the chooser,
// and they are typically used to configure them (eg: if they are based
// on dynamic data). The functions are called in ascending order such
// that the items added in the first function will appear before those
// added in the last function. If at least one ItemsFunc is specified,
// the items of the chooser will be cleared before calling the functions.
func (ch *Chooser) AddItemsFunc(f func()) *Chooser {
ch.ItemsFuncs = append(ch.ItemsFuncs, f)
return ch
}
// CallItemsFuncs calls [Chooser.ItemsFuncs].
func (ch *Chooser) CallItemsFuncs() {
if len(ch.ItemsFuncs) == 0 {
return
}
ch.Items = nil
for _, f := range ch.ItemsFuncs {
f()
}
}
// SetTypes sets the [Chooser.Items] from the given types.
func (ch *Chooser) SetTypes(ts ...*types.Type) *Chooser {
ch.Items = make([]ChooserItem, len(ts))
for i, typ := range ts {
ch.Items[i] = ChooserItem{Value: typ}
}
return ch
}
// SetStrings sets the [Chooser.Items] from the given strings.
func (ch *Chooser) SetStrings(ss ...string) *Chooser {
ch.Items = make([]ChooserItem, len(ss))
for i, s := range ss {
ch.Items[i] = ChooserItem{Value: s}
}
return ch
}
// SetEnums sets the [Chooser.Items] from the given enums.
func (ch *Chooser) SetEnums(es ...enums.Enum) *Chooser {
ch.Items = make([]ChooserItem, len(es))
for i, enum := range es {
str := enum.String()
lbl := strcase.ToSentence(str)
desc := enum.Desc()
// If the documentation does not start with the transformed name, but it does
// start with an uppercase letter, then we assume that the first word of the
// documentation is the correct untransformed name. This fixes
// https://github.com/cogentcore/core/issues/774 (also for Switches).
if !strings.HasPrefix(desc, str) && len(desc) > 0 && unicode.IsUpper(rune(desc[0])) {
str, _, _ = strings.Cut(desc, " ")
}
tip := types.FormatDoc(desc, str, lbl)
ch.Items[i] = ChooserItem{Value: enum, Text: lbl, Tooltip: tip}
}
return ch
}
// SetEnum sets the [Chooser.Items] from the [enums.Enum.Values] of the given enum.
func (ch *Chooser) SetEnum(enum enums.Enum) *Chooser {
return ch.SetEnums(enum.Values()...)
}
// findItem finds the given item value on the list of items and returns its index.
func (ch *Chooser) findItem(it any) int {
for i, v := range ch.Items {
if it == v.Value {
return i
}
}
return -1
}
// SetPlaceholder sets the given placeholder text and
// indicates that nothing has been selected.
func (ch *Chooser) SetPlaceholder(text string) *Chooser {
ch.placeholder = text
if !ch.Editable {
ch.CurrentItem.Text = text
ch.showCurrentItem()
}
ch.CurrentIndex = -1
return ch
}
// SetCurrentValue sets the current item and index to those associated with the given value.
// If the given item is not found, it adds it to the items list if it is not "". It also
// sets the text of the chooser to the label of the item.
func (ch *Chooser) SetCurrentValue(value any) *Chooser {
ch.CurrentIndex = ch.findItem(value)
if value != "" && ch.CurrentIndex < 0 { // add to list if not found
ch.CurrentIndex = len(ch.Items)
ch.Items = append(ch.Items, ChooserItem{Value: value})
}
if ch.CurrentIndex >= 0 {
ch.CurrentItem = ch.Items[ch.CurrentIndex]
}
ch.showCurrentItem()
return ch
}
// SetCurrentIndex sets the current index and the item associated with it.
func (ch *Chooser) SetCurrentIndex(index int) *Chooser {
if index < 0 || index >= len(ch.Items) {
return ch
}
ch.CurrentIndex = index
ch.CurrentItem = ch.Items[index]
ch.showCurrentItem()
return ch
}
// setCurrentText sets the current index and item based on the given text string.
// It can only be used for editable choosers.
func (ch *Chooser) setCurrentText(text string) error {
for i, item := range ch.Items {
if text == item.GetText() {
ch.SetCurrentIndex(i)
return nil
}
}
if !(ch.AllowNew || ch.DefaultNew) {
return errors.New("unknown value")
}
ch.Items = append(ch.Items, ChooserItem{Value: text})
ch.SetCurrentIndex(len(ch.Items) - 1)
return nil
}
// showCurrentItem updates the display to present the current item.
func (ch *Chooser) showCurrentItem() *Chooser {
if ch.Editable {
tf := ch.textField
if tf != nil {
tf.SetText(ch.CurrentItem.GetText())
}
} else {
text := ch.text
if text != nil {
text.SetText(ch.CurrentItem.GetText()).UpdateWidget()
}
}
if ch.CurrentItem.Icon.IsSet() {
picon := ch.Icon
ch.SetIcon(ch.CurrentItem.Icon)
if ch.Icon != picon {
ch.Update()
}
}
ch.NeedsRender()
return ch
}
// selectItem selects the item at the given index and updates the chooser to display it.
func (ch *Chooser) selectItem(index int) *Chooser {
if ch.This == nil {
return ch
}
ch.SetCurrentIndex(index)
ch.NeedsLayout()
return ch
}
// selectItemEvent selects the item at the given index and updates the chooser to display it.
// It also sends an [events.Change] event to indicate that the value has changed.
func (ch *Chooser) selectItemEvent(index int) *Chooser {
if ch.This == nil {
return ch
}
ch.selectItem(index)
if ch.textField != nil {
ch.textField.validate()
}
ch.SendChange()
return ch
}
// ClearError clears any existing validation error for an editable chooser.
func (ch *Chooser) ClearError() {
tf := ch.textField
if tf == nil {
return
}
tf.clearError()
}
// makeItemsMenu constructs a menu of all the items.
// It is used when the chooser is clicked.
func (ch *Chooser) makeItemsMenu(m *Scene) {
ch.CallItemsFuncs()
for i, it := range ch.Items {
if it.SeparatorBefore {
NewSeparator(m)
}
bt := NewButton(m).SetText(it.GetText()).SetIcon(it.Icon).SetTooltip(it.Tooltip)
bt.SetSelected(i == ch.CurrentIndex)
bt.OnClick(func(e events.Event) {
ch.selectItemEvent(i)
})
}
if ch.AllowNew {
NewSeparator(m)
NewButton(m).SetText("New item").SetIcon(icons.Add).
SetTooltip("Add a new item to the chooser").
OnClick(func(e events.Event) {
d := NewBody("New item")
NewText(d).SetType(TextSupporting).SetText("Add a new item to the chooser")
tf := NewTextField(d)
d.AddBottomBar(func(bar *Frame) {
d.AddCancel(bar)
d.AddOK(bar).SetText("Add").SetIcon(icons.Add).OnClick(func(e events.Event) {
ch.Items = append(ch.Items, ChooserItem{Value: tf.Text()})
ch.selectItemEvent(len(ch.Items) - 1)
})
})
d.RunDialog(ch)
})
}
}
// openMenu opens the chooser menu that displays all of the items.
// It returns false if there are no items.
func (ch *Chooser) openMenu(e events.Event) bool {
pos := ch.ContextMenuPos(e)
if indicator, ok := ch.ChildByName("indicator").(Widget); ok {
pos = indicator.ContextMenuPos(nil) // use the pos
}
m := NewMenu(ch.makeItemsMenu, ch.This.(Widget), pos)
if m == nil {
return false
}
m.Run()
return true
}
func (ch *Chooser) WidgetTooltip(pos image.Point) (string, image.Point) {
if ch.CurrentItem.Tooltip != "" {
return ch.CurrentItem.Tooltip, ch.DefaultTooltipPos()
}
return ch.Tooltip, ch.DefaultTooltipPos()
}
// completeMatch is the [complete.MatchFunc] used for the
// editable text field part of the Chooser (if it exists).
func (ch *Chooser) completeMatch(data any, text string, posLine, posChar int) (md complete.Matches) {
md.Seed = text
comps := make(complete.Completions, len(ch.Items))
for i, item := range ch.Items {
comps[i] = complete.Completion{
Text: item.GetText(),
Desc: item.Tooltip,
Icon: item.Icon,
}
}
md.Matches = complete.MatchSeedCompletion(comps, md.Seed)
if ch.AllowNew && text != "" && !slices.ContainsFunc(md.Matches, func(c complete.Completion) bool {
return c.Text == text
}) {
md.Matches = append(md.Matches, complete.Completion{
Text: text,
Label: "Add " + text,
Icon: icons.Add,
Desc: fmt.Sprintf("Add %q to the chooser", text),
})
}
return md
}
// completeEdit is the [complete.EditFunc] used for the
// editable textfield part of the Chooser (if it exists).
func (ch *Chooser) completeEdit(data any, text string, cursorPos int, completion complete.Completion, seed string) (ed complete.Edit) {
return complete.Edit{
NewText: completion.Text,
ForwardDelete: len([]rune(text)),
}
}
// Copyright (c) 2019, 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 (
"log/slog"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/colormap"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/events"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
)
// ColorMapName represents the name of a [colormap.Map],
// which can be edited using a [ColorMapButton].
type ColorMapName string
func (cm ColorMapName) Value() Value { return NewColorMapButton() }
// ColorMapButton displays a [colormap.Map] and can be clicked on
// to display a dialog for selecting different color map options.
// It represents a [ColorMapName] value.
type ColorMapButton struct {
Button
MapName string
}
func (cm *ColorMapButton) WidgetValue() any { return &cm.MapName }
func (cm *ColorMapButton) Init() {
cm.Button.Init()
cm.Styler(func(s *styles.Style) {
s.Padding.Zero()
s.Min.Set(units.Em(10), units.Em(2))
if cm.MapName == "" {
s.Background = colors.Scheme.OutlineVariant
return
}
cm, ok := colormap.AvailableMaps[cm.MapName]
if !ok {
slog.Error("got invalid color map name", "name", cm.Name)
s.Background = colors.Scheme.OutlineVariant
return
}
g := gradient.NewLinear()
for i := float32(0); i < 1; i += 0.01 {
gc := cm.Map(i)
g.AddStop(gc, i)
}
s.Background = g
})
InitValueButton(cm, false, func(d *Body) {
d.SetTitle("Select a color map")
sl := colormap.AvailableMapsList()
si := 0
ls := NewList(d).SetSlice(&sl).SetSelectedValue(cm.MapName).BindSelect(&si)
ls.OnChange(func(e events.Event) {
cm.MapName = sl[si]
})
})
}
// 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 core
import (
"image/color"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/cam/hct"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
)
// ColorPicker represents a color value with an interactive color picker
// composed of history buttons, a hex input, three HCT sliders, and standard
// named color buttons.
type ColorPicker struct {
Frame
// Color is the current color.
Color hct.HCT `set:"-"`
}
func (cp *ColorPicker) WidgetValue() any { return &cp.Color }
// SetColor sets the color of the color picker.
func (cp *ColorPicker) SetColor(c color.Color) *ColorPicker {
cp.Color = hct.FromColor(c)
return cp
}
var namedColors = []string{"red", "orange", "yellow", "green", "blue", "violet", "sienna"}
func (cp *ColorPicker) Init() {
cp.Frame.Init()
cp.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Grow.Set(1, 0)
})
colorButton := func(w *Button, c color.Color) {
w.Styler(func(s *styles.Style) {
s.Background = colors.Uniform(c)
s.Padding.Set(units.Dp(ConstantSpacing(16)))
})
w.OnClick(func(e events.Event) {
cp.SetColor(c).UpdateChange()
})
}
tree.AddChild(cp, func(w *Frame) {
tree.AddChild(w, func(w *Button) {
w.SetTooltip("Current color")
colorButton(w, &cp.Color) // a pointer so it updates
})
tree.AddChild(w, func(w *Button) {
w.SetTooltip("Previous color")
colorButton(w, cp.Color) // not a pointer so it does not update
})
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)
})
w.Updater(func() {
w.SetText(colors.AsHex(cp.Color))
})
w.SetValidator(func() error {
c, err := colors.FromHex(w.Text())
if err != nil {
return err
}
cp.SetColor(c).UpdateChange()
return nil
})
})
})
sf := func(s *styles.Style) {
s.Min.Y.Em(2)
s.Min.X.Em(6)
s.Max.X.Em(40)
s.Grow.Set(1, 0)
}
tree.AddChild(cp, func(w *Slider) {
Bind(&cp.Color.Hue, w)
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.UpdateChange()
})
w.Styler(func(s *styles.Style) {
w.ValueColor = nil
w.ThumbColor = colors.Uniform(cp.Color)
g := gradient.NewLinear()
for h := float32(0); h <= 360; h += 5 {
gc := cp.Color.WithHue(h)
g.AddStop(gc.AsRGBA(), h/360)
}
s.Background = g
})
w.FinalStyler(sf)
})
tree.AddChild(cp, func(w *Slider) {
Bind(&cp.Color.Chroma, w)
w.SetMin(0).SetMax(120)
w.SetTooltip("The chroma, which is the colorfulness/saturation of the color")
w.Updater(func() {
w.SetMax(cp.Color.MaximumChroma())
})
w.OnInput(func(e events.Event) {
cp.Color.SetChroma(w.Value)
cp.UpdateChange()
})
w.Styler(func(s *styles.Style) {
w.ValueColor = nil
w.ThumbColor = colors.Uniform(cp.Color)
g := gradient.NewLinear()
for c := float32(0); c <= w.Max; c += 5 {
gc := cp.Color.WithChroma(c)
g.AddStop(gc.AsRGBA(), c/w.Max)
}
s.Background = g
})
w.FinalStyler(sf)
})
tree.AddChild(cp, func(w *Slider) {
Bind(&cp.Color.Tone, w)
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.UpdateChange()
})
w.Styler(func(s *styles.Style) {
w.ValueColor = nil
w.ThumbColor = colors.Uniform(cp.Color)
g := gradient.NewLinear()
for c := float32(0); c <= 100; c += 5 {
gc := cp.Color.WithTone(c)
g.AddStop(gc.AsRGBA(), c/100)
}
s.Background = g
})
w.FinalStyler(sf)
})
tree.AddChild(cp, func(w *Slider) {
Bind(&cp.Color.A, w)
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.UpdateChange()
})
w.Styler(func(s *styles.Style) {
w.ValueColor = nil
w.ThumbColor = colors.Uniform(cp.Color)
g := gradient.NewLinear()
for c := float32(0); c <= 1; c += 0.05 {
gc := colors.WithAF32(cp.Color, c)
g.AddStop(gc, c)
}
s.Background = g
})
w.FinalStyler(sf)
})
tree.AddChild(cp, func(w *Frame) {
for _, name := range namedColors {
c := colors.Map[name]
tree.AddChildAt(w, name, func(w *Button) {
w.SetTooltip(strcase.ToSentence(name))
colorButton(w, c)
})
}
})
}
// ColorButton represents a color value with a button that opens a [ColorPicker].
type ColorButton struct {
Button
Color color.RGBA
}
func (cb *ColorButton) WidgetValue() any { return &cb.Color }
func (cb *ColorButton) Init() {
cb.Button.Init()
cb.SetType(ButtonTonal).SetText("Edit color").SetIcon(icons.Colors)
cb.Styler(func(s *styles.Style) {
// we need to display button as non-transparent
// so that it can be seen
dclr := colors.WithAF32(cb.Color, 1)
s.Background = colors.Uniform(dclr)
s.Color = colors.Uniform(hct.ContrastColor(dclr, hct.ContrastAAA))
})
InitValueButton(cb, false, func(d *Body) {
d.SetTitle("Edit color")
cp := NewColorPicker(d).SetColor(cb.Color)
cp.OnChange(func(e events.Event) {
cb.Color = cp.Color.AsRGBA()
})
})
}
// 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 core
import (
"image"
"sync"
"time"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/parse/complete"
)
// Complete holds the current completion data and functions to call for building
// the list of possible completions and for editing text after a completion is selected.
// It also holds the popup [Stage] associated with it.
type Complete struct { //types:add -setters
// function to get the list of possible completions
MatchFunc complete.MatchFunc
// function to get the text to show for lookup
LookupFunc complete.LookupFunc
// function to edit text using the selected completion
EditFunc complete.EditFunc
// the context object that implements the completion functions
Context any
// line number in source that completion is operating on, if relevant
SrcLn int
// character position in source that completion is operating on
SrcCh int
// the list of potential completions
completions complete.Completions
// current completion seed
Seed string
// the user's completion selection
Completion string
// the event listeners for the completer (it sends [events.Select] events)
listeners events.Listeners
// stage is the popup [Stage] associated with the [Complete].
stage *Stage
delayTimer *time.Timer
delayMu sync.Mutex
showMu sync.Mutex
}
// NewComplete returns a new [Complete] object. It does not show it; see [Complete.Show].
func NewComplete() *Complete {
return &Complete{}
}
// IsAboutToShow returns true if the DelayTimer is started for
// preparing to show a completion. note: don't really need to lock
func (c *Complete) IsAboutToShow() bool {
c.delayMu.Lock()
defer c.delayMu.Unlock()
return c.delayTimer != nil
}
// Show is the main call for listing completions.
// Has a builtin delay timer so completions are only shown after
// a delay, which resets every time it is called.
// After delay, Calls ShowNow, which calls MatchFunc
// to get a list of completions and builds the completion popup menu
func (c *Complete) Show(ctx Widget, pos image.Point, text string) {
if c.MatchFunc == nil {
return
}
wait := SystemSettings.CompleteWaitDuration
if c.stage != nil {
c.Cancel()
}
if wait == 0 {
c.showNow(ctx, pos, text)
return
}
c.delayMu.Lock()
if c.delayTimer != nil {
c.delayTimer.Stop()
}
c.delayTimer = time.AfterFunc(wait,
func() {
c.showNowAsync(ctx, pos, text)
c.delayMu.Lock()
c.delayTimer = nil
c.delayMu.Unlock()
})
c.delayMu.Unlock()
}
// showNow actually calls MatchFunc to get a list of completions and builds the
// completion popup menu. This is the sync version called from
func (c *Complete) showNow(ctx Widget, pos image.Point, text string) {
if c.stage != nil {
c.Cancel()
}
c.showMu.Lock()
defer c.showMu.Unlock()
if c.showNowImpl(ctx, pos, text) {
c.stage.runPopup()
}
}
// showNowAsync actually calls MatchFunc to get a list of completions and builds the
// completion popup menu. This is the Async version for delayed AfterFunc call.
func (c *Complete) showNowAsync(ctx Widget, pos image.Point, text string) {
if c.stage != nil {
c.cancelAsync()
}
c.showMu.Lock()
defer c.showMu.Unlock()
if c.showNowImpl(ctx, pos, text) {
c.stage.runPopupAsync()
}
}
// showNowImpl is the implementation of ShowNow, presenting completions.
// Returns false if nothing to show.
func (c *Complete) showNowImpl(ctx Widget, pos image.Point, text string) bool {
md := c.MatchFunc(c.Context, text, c.SrcLn, c.SrcCh)
c.completions = md.Matches
c.Seed = md.Seed
if len(c.completions) == 0 {
return false
}
if len(c.completions) > SystemSettings.CompleteMaxItems {
c.completions = c.completions[0:SystemSettings.CompleteMaxItems]
}
sc := NewScene(ctx.AsTree().Name + "-complete")
StyleMenuScene(sc)
c.stage = NewPopupStage(CompleterStage, sc, ctx).SetPos(pos)
// we forward our key events to the context object
// so that you can keep typing while in a completer
// sc.OnKeyChord(ctx.HandleEvent)
for i := 0; i < len(c.completions); i++ {
cmp := &c.completions[i]
text := cmp.Text
if cmp.Label != "" {
text = cmp.Label
}
icon := cmp.Icon
mi := NewButton(sc).SetText(text).SetIcon(icons.Icon(icon))
mi.SetTooltip(cmp.Desc)
mi.OnClick(func(e events.Event) {
c.complete(cmp.Text)
})
mi.OnKeyChord(func(e events.Event) {
kf := keymap.Of(e.KeyChord())
if e.KeyRune() == ' ' {
ctx.AsWidget().HandleEvent(e) // bypass button handler!
}
if kf == keymap.Enter {
e.SetHandled()
c.complete(cmp.Text)
}
})
if i == 0 {
sc.Events.SetStartFocus(mi)
}
}
return true
}
// Cancel cancels any existing or pending completion.
// Call when new events nullify prior completions.
// Returns true if canceled.
func (c *Complete) Cancel() bool {
if c.stage == nil {
return false
}
st := c.stage
c.stage = nil
st.ClosePopup()
return true
}
// cancelAsync cancels any existing *or* pending completion,
// inside a delayed callback function (Async)
// Call when new events nullify prior completions.
// Returns true if canceled.
func (c *Complete) cancelAsync() bool {
if c.stage == nil {
return false
}
st := c.stage
c.stage = nil
st.closePopupAsync()
return true
}
// Lookup is the main call for doing lookups.
func (c *Complete) Lookup(text string, posLine, posChar int, sc *Scene, pt image.Point) {
if c.LookupFunc == nil || sc == nil {
return
}
// c.Sc = nil
c.LookupFunc(c.Context, text, posLine, posChar) // this processes result directly
}
// complete sends Select event to listeners, indicating that the user has made a
// selection from the list of possible completions.
// This is called inside the main event loop.
func (c *Complete) complete(s string) {
c.Cancel()
c.Completion = s
c.listeners.Call(&events.Base{Typ: events.Select})
}
// OnSelect registers given listener function for [events.Select] events on Value.
// This is the primary notification event for all [Complete] elements.
func (c *Complete) OnSelect(fun func(e events.Event)) {
c.On(events.Select, fun)
}
// On adds an event listener function for the given event type.
func (c *Complete) On(etype events.Types, fun func(e events.Event)) {
c.listeners.Add(etype, fun)
}
// GetCompletion returns the completion with the given text.
func (c *Complete) GetCompletion(s string) complete.Completion {
for _, cc := range c.completions {
if s == cc.Text {
return cc
}
}
return complete.Completion{}
}
// CompleteEditText is a chance to modify the completion selection before it is inserted.
func CompleteEditText(text string, cp int, completion string, seed string) (ed complete.Edit) {
ed.NewText = completion
return ed
}
// 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 core
import (
"log/slog"
"reflect"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/colors"
"cogentcore.org/core/events"
"cogentcore.org/core/keymap"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
)
// RunDialog returns and runs a new [DialogStage] that does not take up
// the full window it is created in, in the context of the given widget.
// See [Body.NewDialog] to make a new dialog without running it.
func (bd *Body) RunDialog(ctx Widget) *Stage {
return bd.NewDialog(ctx).Run()
}
// NewDialog returns a new [DialogStage] that does not take up the
// full window it is created in, in the context of the given widget.
// You must call [Stage.Run] to run the dialog; see [Body.RunDialog]
// for a version that automatically runs it.
func (bd *Body) NewDialog(ctx Widget) *Stage {
ctx = nonNilContext(ctx)
bd.dialogStyles()
bd.Scene.Stage = newMainStage(DialogStage, bd.Scene)
bd.Scene.Stage.SetModal(true)
bd.Scene.Stage.SetContext(ctx)
bd.Scene.Stage.Pos = ctx.ContextMenuPos(nil)
return bd.Scene.Stage
}
// RunFullDialog returns and runs a new [DialogStage] that takes up the full
// window it is created in, in the context of the given widget.
// See [Body.NewFullDialog] to make a full dialog without running it.
func (bd *Body) RunFullDialog(ctx Widget) *Stage {
return bd.NewFullDialog(ctx).Run()
}
// NewFullDialog returns a new [DialogStage] that takes up the full
// window it is created in, in the context of the given widget.
// You must call [Stage.Run] to run the dialog; see [Body.RunFullDialog]
// for a version that automatically runs it.
func (bd *Body) NewFullDialog(ctx Widget) *Stage {
bd.dialogStyles()
bd.Scene.Stage = newMainStage(DialogStage, bd.Scene)
bd.Scene.Stage.SetModal(true)
bd.Scene.Stage.SetContext(ctx)
bd.Scene.Stage.SetFullWindow(true)
return bd.Scene.Stage
}
// RunWindowDialog returns and runs a new [DialogStage] that is placed in
// a new system window on multi-window platforms, in the context of the given widget.
// See [Body.NewWindowDialog] to make a dialog window without running it.
func (bd *Body) RunWindowDialog(ctx Widget) *Stage {
return bd.NewWindowDialog(ctx).Run()
}
// NewWindowDialog returns a new [DialogStage] that is placed in
// a new system window on multi-window platforms, in the context of the given widget.
// You must call [Stage.Run] to run the dialog; see [Body.RunWindowDialog]
// for a version that automatically runs it.
func (bd *Body) NewWindowDialog(ctx Widget) *Stage {
bd.NewFullDialog(ctx)
bd.Scene.Stage.SetNewWindow(true)
return bd.Scene.Stage
}
// RecycleDialog looks for a dialog with the given data. If it
// finds it, it shows it and returns true. Otherwise, it returns false.
// See [RecycleMainWindow] for a non-dialog window version.
func RecycleDialog(data any) bool {
rw, got := dialogRenderWindows.findData(data)
if !got {
return false
}
rw.Raise()
return true
}
// MessageDialog opens a new Dialog displaying the given message
// in the context of the given widget. An optional title can be provided.
func MessageDialog(ctx Widget, message string, title ...string) {
ttl := ""
if len(title) > 0 {
ttl = title[0]
}
d := NewBody(ttl)
NewText(d).SetType(TextSupporting).SetText(message)
d.AddOKOnly().RunDialog(ctx)
}
// ErrorDialog opens a new dialog displaying the given error
// in the context of the given widget. An optional title can
// be provided; if it is not, the title will default to
// "There was an error". If the given error is nil, no dialog
// is created.
func ErrorDialog(ctx Widget, err error, title ...string) {
if err == nil {
return
}
ttl := "There was an error"
if len(title) > 0 {
ttl = title[0]
}
// we need to get [errors.CallerInfo] at this level
slog.Error(ttl + ": " + err.Error() + " | " + errors.CallerInfo())
d := NewBody(ttl)
NewText(d).SetType(TextSupporting).SetText(err.Error())
d.AddOKOnly().RunDialog(ctx)
}
// AddOK adds an OK button to the given parent widget (typically in
// [Body.AddBottomBar]), connecting to [keymap.Accept]. Clicking on
// the OK button automatically results in the dialog being closed;
// you can add your own [WidgetBase.OnClick] listener to do things
// beyond that. Also see [Body.AddOKOnly].
func (bd *Body) AddOK(parent Widget) *Button {
bt := NewButton(parent).SetText("OK")
bt.OnFinal(events.Click, func(e events.Event) { // then close
e.SetHandled() // otherwise propagates to dead elements
bd.Close()
})
bd.Scene.OnFirst(events.KeyChord, func(e events.Event) {
kf := keymap.Of(e.KeyChord())
if kf == keymap.Accept {
e.SetHandled()
bt.Send(events.Click, e)
}
})
return bt
}
// AddOKOnly adds an OK button to the bottom bar of the [Body] through
// [Body.AddBottomBar], connecting to [keymap.Accept]. Clicking on the
// OK button automatically results in the dialog being closed. Also see
// [Body.AddOK].
func (bd *Body) AddOKOnly() *Body {
bd.AddBottomBar(func(bar *Frame) { bd.AddOK(bar) })
return bd
}
// AddCancel adds a cancel button to the given parent widget
// (typically in [Body.AddBottomBar]), connecting to [keymap.Abort].
// Clicking on the cancel button automatically results in the dialog
// being closed; you can add your own [WidgetBase.OnClick] listener
// to do things beyond that.
func (bd *Body) AddCancel(parent Widget) *Button {
bt := NewButton(parent).SetType(ButtonOutlined).SetText("Cancel")
bt.OnClick(func(e events.Event) {
e.SetHandled() // otherwise propagates to dead elements
bd.Close()
})
abort := func(e events.Event) {
kf := keymap.Of(e.KeyChord())
if kf == keymap.Abort {
e.SetHandled()
bt.Send(events.Click, e)
bd.Close()
}
}
bd.OnFirst(events.KeyChord, abort)
bt.OnFirst(events.KeyChord, abort)
return bt
}
// Close closes the [Stage] associated with this [Body] (typically for dialogs).
func (bd *Body) Close() {
bd.Scene.Close()
}
// dialogStyles sets default stylers for dialog bodies.
// It is automatically called in [Body.NewDialog].
func (bd *Body) dialogStyles() {
bd.Scene.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Color = colors.Scheme.OnSurface
if !bd.Scene.Stage.NewWindow && !bd.Scene.Stage.FullWindow {
s.Padding.Set(units.Dp(24))
s.Border.Radius = styles.BorderRadiusLarge
s.BoxShadow = styles.BoxShadow3()
s.Background = colors.Scheme.SurfaceContainerLow
}
})
}
// nonNilContext returns a non-nil context widget, falling back on the top
// scene of the current window.
func nonNilContext(ctx Widget) Widget {
if !reflectx.IsNil(reflect.ValueOf(ctx)) {
return ctx
}
return currentRenderWindow.mains.top().Scene
}
// Code generated by "core generate"; DO NOT EDIT.
package core
import (
"cogentcore.org/core/enums"
)
var _ButtonTypesValues = []ButtonTypes{0, 1, 2, 3, 4, 5, 6}
// ButtonTypesN is the highest valid value for type ButtonTypes, plus one.
const ButtonTypesN ButtonTypes = 7
var _ButtonTypesValueMap = map[string]ButtonTypes{`Filled`: 0, `Tonal`: 1, `Elevated`: 2, `Outlined`: 3, `Text`: 4, `Action`: 5, `Menu`: 6}
var _ButtonTypesDescMap = map[ButtonTypes]string{0: `ButtonFilled is a filled button with a contrasting background color. It should be used for prominent actions, typically those that are the final in a sequence. It is equivalent to Material Design's filled button.`, 1: `ButtonTonal is a filled button, similar to [ButtonFilled]. It is used for the same purposes, but it has a lighter background color and less emphasis. It is equivalent to Material Design's filled tonal button.`, 2: `ButtonElevated is an elevated button with a light background color and a shadow. It is equivalent to Material Design's elevated button.`, 3: `ButtonOutlined is an outlined button that is used for secondary actions that are still important. It is equivalent to Material Design's outlined button.`, 4: `ButtonText is a low-importance button with no border, background color, or shadow when not being interacted with. It renders primary-colored text, and it renders a background color and shadow when hovered/focused/active. It should only be used for low emphasis actions, and you must ensure it stands out from the surrounding context sufficiently. It is equivalent to Material Design's text button, but it can also contain icons and other things.`, 5: `ButtonAction is a simple button that typically serves as a simple action among a series of other buttons (eg: in a toolbar), or as a part of another widget, like a spinner or snackbar. It has no border, background color, or shadow when not being interacted with. It inherits the text color of its parent, and it renders a background when hovered/focused/active. You must ensure it stands out from the surrounding context sufficiently. It is equivalent to Material Design's icon button, but it can also contain text and other things (and frequently does).`, 6: `ButtonMenu is similar to [ButtonAction], but it is designed for buttons located in popup menus.`}
var _ButtonTypesMap = map[ButtonTypes]string{0: `Filled`, 1: `Tonal`, 2: `Elevated`, 3: `Outlined`, 4: `Text`, 5: `Action`, 6: `Menu`}
// String returns the string representation of this ButtonTypes value.
func (i ButtonTypes) String() string { return enums.String(i, _ButtonTypesMap) }
// SetString sets the ButtonTypes value from its string representation,
// and returns an error if the string is invalid.
func (i *ButtonTypes) SetString(s string) error {
return enums.SetString(i, s, _ButtonTypesValueMap, "ButtonTypes")
}
// Int64 returns the ButtonTypes value as an int64.
func (i ButtonTypes) Int64() int64 { return int64(i) }
// SetInt64 sets the ButtonTypes value from an int64.
func (i *ButtonTypes) SetInt64(in int64) { *i = ButtonTypes(in) }
// Desc returns the description of the ButtonTypes value.
func (i ButtonTypes) Desc() string { return enums.Desc(i, _ButtonTypesDescMap) }
// ButtonTypesValues returns all possible values for the type ButtonTypes.
func ButtonTypesValues() []ButtonTypes { return _ButtonTypesValues }
// Values returns all possible values for the type ButtonTypes.
func (i ButtonTypes) Values() []enums.Enum { return enums.Values(_ButtonTypesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i ButtonTypes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *ButtonTypes) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "ButtonTypes")
}
var _ChooserTypesValues = []ChooserTypes{0, 1}
// ChooserTypesN is the highest valid value for type ChooserTypes, plus one.
const ChooserTypesN ChooserTypes = 2
var _ChooserTypesValueMap = map[string]ChooserTypes{`Filled`: 0, `Outlined`: 1}
var _ChooserTypesDescMap = map[ChooserTypes]string{0: `ChooserFilled represents a filled Chooser with a background color and a bottom border`, 1: `ChooserOutlined represents an outlined Chooser with a border on all sides and no background color`}
var _ChooserTypesMap = map[ChooserTypes]string{0: `Filled`, 1: `Outlined`}
// String returns the string representation of this ChooserTypes value.
func (i ChooserTypes) String() string { return enums.String(i, _ChooserTypesMap) }
// SetString sets the ChooserTypes value from its string representation,
// and returns an error if the string is invalid.
func (i *ChooserTypes) SetString(s string) error {
return enums.SetString(i, s, _ChooserTypesValueMap, "ChooserTypes")
}
// Int64 returns the ChooserTypes value as an int64.
func (i ChooserTypes) Int64() int64 { return int64(i) }
// SetInt64 sets the ChooserTypes value from an int64.
func (i *ChooserTypes) SetInt64(in int64) { *i = ChooserTypes(in) }
// Desc returns the description of the ChooserTypes value.
func (i ChooserTypes) Desc() string { return enums.Desc(i, _ChooserTypesDescMap) }
// ChooserTypesValues returns all possible values for the type ChooserTypes.
func ChooserTypesValues() []ChooserTypes { return _ChooserTypesValues }
// Values returns all possible values for the type ChooserTypes.
func (i ChooserTypes) Values() []enums.Enum { return enums.Values(_ChooserTypesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i ChooserTypes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *ChooserTypes) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "ChooserTypes")
}
var _LayoutPassesValues = []LayoutPasses{0, 1, 2}
// LayoutPassesN is the highest valid value for type LayoutPasses, plus one.
const LayoutPassesN LayoutPasses = 3
var _LayoutPassesValueMap = map[string]LayoutPasses{`SizeUpPass`: 0, `SizeDownPass`: 1, `SizeFinalPass`: 2}
var _LayoutPassesDescMap = map[LayoutPasses]string{0: ``, 1: ``, 2: ``}
var _LayoutPassesMap = map[LayoutPasses]string{0: `SizeUpPass`, 1: `SizeDownPass`, 2: `SizeFinalPass`}
// String returns the string representation of this LayoutPasses value.
func (i LayoutPasses) String() string { return enums.String(i, _LayoutPassesMap) }
// SetString sets the LayoutPasses value from its string representation,
// and returns an error if the string is invalid.
func (i *LayoutPasses) SetString(s string) error {
return enums.SetString(i, s, _LayoutPassesValueMap, "LayoutPasses")
}
// Int64 returns the LayoutPasses value as an int64.
func (i LayoutPasses) Int64() int64 { return int64(i) }
// SetInt64 sets the LayoutPasses value from an int64.
func (i *LayoutPasses) SetInt64(in int64) { *i = LayoutPasses(in) }
// Desc returns the description of the LayoutPasses value.
func (i LayoutPasses) Desc() string { return enums.Desc(i, _LayoutPassesDescMap) }
// LayoutPassesValues returns all possible values for the type LayoutPasses.
func LayoutPassesValues() []LayoutPasses { return _LayoutPassesValues }
// Values returns all possible values for the type LayoutPasses.
func (i LayoutPasses) Values() []enums.Enum { return enums.Values(_LayoutPassesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i LayoutPasses) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *LayoutPasses) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "LayoutPasses")
}
var _MeterTypesValues = []MeterTypes{0, 1, 2}
// MeterTypesN is the highest valid value for type MeterTypes, plus one.
const MeterTypesN MeterTypes = 3
var _MeterTypesValueMap = map[string]MeterTypes{`Linear`: 0, `Circle`: 1, `Semicircle`: 2}
var _MeterTypesDescMap = map[MeterTypes]string{0: `MeterLinear indicates to render a meter that goes in a straight, linear direction, either horizontal or vertical, as specified by [styles.Style.Direction].`, 1: `MeterCircle indicates to render the meter as a circle.`, 2: `MeterSemicircle indicates to render the meter as a semicircle.`}
var _MeterTypesMap = map[MeterTypes]string{0: `Linear`, 1: `Circle`, 2: `Semicircle`}
// String returns the string representation of this MeterTypes value.
func (i MeterTypes) String() string { return enums.String(i, _MeterTypesMap) }
// SetString sets the MeterTypes value from its string representation,
// and returns an error if the string is invalid.
func (i *MeterTypes) SetString(s string) error {
return enums.SetString(i, s, _MeterTypesValueMap, "MeterTypes")
}
// Int64 returns the MeterTypes value as an int64.
func (i MeterTypes) Int64() int64 { return int64(i) }
// SetInt64 sets the MeterTypes value from an int64.
func (i *MeterTypes) SetInt64(in int64) { *i = MeterTypes(in) }
// Desc returns the description of the MeterTypes value.
func (i MeterTypes) Desc() string { return enums.Desc(i, _MeterTypesDescMap) }
// MeterTypesValues returns all possible values for the type MeterTypes.
func MeterTypesValues() []MeterTypes { return _MeterTypesValues }
// Values returns all possible values for the type MeterTypes.
func (i MeterTypes) Values() []enums.Enum { return enums.Values(_MeterTypesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i MeterTypes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *MeterTypes) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "MeterTypes")
}
var _sceneFlagsValues = []sceneFlags{0, 1, 2, 3, 4, 5, 6}
// sceneFlagsN is the highest valid value for type sceneFlags, plus one.
const sceneFlagsN sceneFlags = 7
var _sceneFlagsValueMap = map[string]sceneFlags{`HasShown`: 0, `Updating`: 1, `NeedsRender`: 2, `NeedsLayout`: 3, `HasDeferred`: 4, `ImageUpdated`: 5, `ContentSizing`: 6}
var _sceneFlagsDescMap = map[sceneFlags]string{0: `sceneHasShown is whether this scene has been shown. This is used to ensure that [events.Show] is only sent once.`, 1: `sceneUpdating means the Scene is in the process of sceneUpdating. It is set for any kind of tree-level update. Skip any further update passes until it goes off.`, 2: `sceneNeedsRender is whether anything in the Scene needs to be re-rendered (but not necessarily the whole scene itself).`, 3: `sceneNeedsLayout is whether the Scene needs a new layout pass.`, 4: `sceneHasDeferred is whether the Scene has elements with Deferred functions.`, 5: `sceneImageUpdated indicates that the Scene's image has been updated e.g., due to a render or a resize. This is reset by the global [RenderWindow] rendering pass, so it knows whether it needs to copy the image up to the GPU or not.`, 6: `sceneContentSizing means that this scene is currently doing a contentSize computation to compute the size of the scene (for sizing window for example). Affects layout size computation.`}
var _sceneFlagsMap = map[sceneFlags]string{0: `HasShown`, 1: `Updating`, 2: `NeedsRender`, 3: `NeedsLayout`, 4: `HasDeferred`, 5: `ImageUpdated`, 6: `ContentSizing`}
// String returns the string representation of this sceneFlags value.
func (i sceneFlags) String() string { return enums.BitFlagString(i, _sceneFlagsValues) }
// BitIndexString returns the string representation of this sceneFlags value
// if it is a bit index value (typically an enum constant), and
// not an actual bit flag value.
func (i sceneFlags) BitIndexString() string { return enums.String(i, _sceneFlagsMap) }
// SetString sets the sceneFlags value from its string representation,
// and returns an error if the string is invalid.
func (i *sceneFlags) SetString(s string) error { *i = 0; return i.SetStringOr(s) }
// SetStringOr sets the sceneFlags value from its string representation
// while preserving any bit flags already set, and returns an
// error if the string is invalid.
func (i *sceneFlags) SetStringOr(s string) error {
return enums.SetStringOr(i, s, _sceneFlagsValueMap, "sceneFlags")
}
// Int64 returns the sceneFlags value as an int64.
func (i sceneFlags) Int64() int64 { return int64(i) }
// SetInt64 sets the sceneFlags value from an int64.
func (i *sceneFlags) SetInt64(in int64) { *i = sceneFlags(in) }
// Desc returns the description of the sceneFlags value.
func (i sceneFlags) Desc() string { return enums.Desc(i, _sceneFlagsDescMap) }
// sceneFlagsValues returns all possible values for the type sceneFlags.
func sceneFlagsValues() []sceneFlags { return _sceneFlagsValues }
// Values returns all possible values for the type sceneFlags.
func (i sceneFlags) Values() []enums.Enum { return enums.Values(_sceneFlagsValues) }
// HasFlag returns whether these bit flags have the given bit flag set.
func (i *sceneFlags) HasFlag(f enums.BitFlag) bool { return enums.HasFlag((*int64)(i), f) }
// SetFlag sets the value of the given flags in these flags to the given value.
func (i *sceneFlags) SetFlag(on bool, f ...enums.BitFlag) { enums.SetFlag((*int64)(i), on, f...) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i sceneFlags) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *sceneFlags) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "sceneFlags")
}
var _ThemesValues = []Themes{0, 1, 2}
// ThemesN is the highest valid value for type Themes, plus one.
const ThemesN Themes = 3
var _ThemesValueMap = map[string]Themes{`Auto`: 0, `Light`: 1, `Dark`: 2}
var _ThemesDescMap = map[Themes]string{0: `ThemeAuto indicates to use the theme specified by the operating system`, 1: `ThemeLight indicates to use a light theme`, 2: `ThemeDark indicates to use a dark theme`}
var _ThemesMap = map[Themes]string{0: `Auto`, 1: `Light`, 2: `Dark`}
// String returns the string representation of this Themes value.
func (i Themes) String() string { return enums.String(i, _ThemesMap) }
// SetString sets the Themes value from its string representation,
// and returns an error if the string is invalid.
func (i *Themes) SetString(s string) error { return enums.SetString(i, s, _ThemesValueMap, "Themes") }
// Int64 returns the Themes value as an int64.
func (i Themes) Int64() int64 { return int64(i) }
// SetInt64 sets the Themes value from an int64.
func (i *Themes) SetInt64(in int64) { *i = Themes(in) }
// Desc returns the description of the Themes value.
func (i Themes) Desc() string { return enums.Desc(i, _ThemesDescMap) }
// ThemesValues returns all possible values for the type Themes.
func ThemesValues() []Themes { return _ThemesValues }
// Values returns all possible values for the type Themes.
func (i Themes) Values() []enums.Enum { return enums.Values(_ThemesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Themes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Themes) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Themes") }
var _SizeClassesValues = []SizeClasses{0, 1, 2}
// SizeClassesN is the highest valid value for type SizeClasses, plus one.
const SizeClassesN SizeClasses = 3
var _SizeClassesValueMap = map[string]SizeClasses{`Compact`: 0, `Medium`: 1, `Expanded`: 2}
var _SizeClassesDescMap = map[SizeClasses]string{0: `SizeCompact is the size class for windows with a width less than 600dp, which typically happens on phones.`, 1: `SizeMedium is the size class for windows with a width between 600dp and 840dp inclusive, which typically happens on tablets.`, 2: `SizeExpanded is the size class for windows with a width greater than 840dp, which typically happens on desktop and laptop computers.`}
var _SizeClassesMap = map[SizeClasses]string{0: `Compact`, 1: `Medium`, 2: `Expanded`}
// String returns the string representation of this SizeClasses value.
func (i SizeClasses) String() string { return enums.String(i, _SizeClassesMap) }
// SetString sets the SizeClasses value from its string representation,
// and returns an error if the string is invalid.
func (i *SizeClasses) SetString(s string) error {
return enums.SetString(i, s, _SizeClassesValueMap, "SizeClasses")
}
// Int64 returns the SizeClasses value as an int64.
func (i SizeClasses) Int64() int64 { return int64(i) }
// SetInt64 sets the SizeClasses value from an int64.
func (i *SizeClasses) SetInt64(in int64) { *i = SizeClasses(in) }
// Desc returns the description of the SizeClasses value.
func (i SizeClasses) Desc() string { return enums.Desc(i, _SizeClassesDescMap) }
// SizeClassesValues returns all possible values for the type SizeClasses.
func SizeClassesValues() []SizeClasses { return _SizeClassesValues }
// Values returns all possible values for the type SizeClasses.
func (i SizeClasses) Values() []enums.Enum { return enums.Values(_SizeClassesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i SizeClasses) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *SizeClasses) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "SizeClasses")
}
var _SliderTypesValues = []SliderTypes{0, 1}
// SliderTypesN is the highest valid value for type SliderTypes, plus one.
const SliderTypesN SliderTypes = 2
var _SliderTypesValueMap = map[string]SliderTypes{`Slider`: 0, `Scrollbar`: 1}
var _SliderTypesDescMap = map[SliderTypes]string{0: `SliderSlider indicates a standard, user-controllable slider for setting a numeric value.`, 1: `SliderScrollbar indicates a slider acting as a scrollbar for content. It has a [Slider.visiblePercent] factor that specifies the percent of the content currently visible, which determines the size of the thumb, and thus the range of motion remaining for the thumb Value ([Slider.visiblePercent] = 1 means thumb is full size, and no remaining range of motion). The content size (inside the margin and padding) determines the outer bounds of the rendered area.`}
var _SliderTypesMap = map[SliderTypes]string{0: `Slider`, 1: `Scrollbar`}
// String returns the string representation of this SliderTypes value.
func (i SliderTypes) String() string { return enums.String(i, _SliderTypesMap) }
// SetString sets the SliderTypes value from its string representation,
// and returns an error if the string is invalid.
func (i *SliderTypes) SetString(s string) error {
return enums.SetString(i, s, _SliderTypesValueMap, "SliderTypes")
}
// Int64 returns the SliderTypes value as an int64.
func (i SliderTypes) Int64() int64 { return int64(i) }
// SetInt64 sets the SliderTypes value from an int64.
func (i *SliderTypes) SetInt64(in int64) { *i = SliderTypes(in) }
// Desc returns the description of the SliderTypes value.
func (i SliderTypes) Desc() string { return enums.Desc(i, _SliderTypesDescMap) }
// SliderTypesValues returns all possible values for the type SliderTypes.
func SliderTypesValues() []SliderTypes { return _SliderTypesValues }
// Values returns all possible values for the type SliderTypes.
func (i SliderTypes) Values() []enums.Enum { return enums.Values(_SliderTypesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i SliderTypes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *SliderTypes) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "SliderTypes")
}
var _SplitsTilesValues = []SplitsTiles{0, 1, 2, 3, 4}
// SplitsTilesN is the highest valid value for type SplitsTiles, plus one.
const SplitsTilesN SplitsTiles = 5
var _SplitsTilesValueMap = map[string]SplitsTiles{`Span`: 0, `Split`: 1, `FirstLong`: 2, `SecondLong`: 3, `Plus`: 4}
var _SplitsTilesDescMap = map[SplitsTiles]string{0: `Span has a single element spanning the cross dimension, i.e., a vertical span for a horizontal main axis, or a horizontal span for a vertical main axis. It is the only valid value for 1D Splits, where it specifies a single element per split. If all tiles are Span, then a 1D line is generated.`, 1: `Split has a split between elements along the cross dimension, with the first of 2 elements in the first main axis line and the second in the second line.`, 2: `FirstLong has a long span of first element along the first main axis line and a split between the next two elements along the second line, with a split between the two lines. Visually, the splits form a T shape for a horizontal main axis.`, 3: `SecondLong has the first two elements split along the first line, and the third with a long span along the second main axis line, with a split between the two lines. Visually, the splits form an inverted T shape for a horizontal main axis.`, 4: `Plus is arranged like a plus sign + with the main split along the main axis line, and then individual cross-axis splits between the first two and next two elements.`}
var _SplitsTilesMap = map[SplitsTiles]string{0: `Span`, 1: `Split`, 2: `FirstLong`, 3: `SecondLong`, 4: `Plus`}
// String returns the string representation of this SplitsTiles value.
func (i SplitsTiles) String() string { return enums.String(i, _SplitsTilesMap) }
// SetString sets the SplitsTiles value from its string representation,
// and returns an error if the string is invalid.
func (i *SplitsTiles) SetString(s string) error {
return enums.SetString(i, s, _SplitsTilesValueMap, "SplitsTiles")
}
// Int64 returns the SplitsTiles value as an int64.
func (i SplitsTiles) Int64() int64 { return int64(i) }
// SetInt64 sets the SplitsTiles value from an int64.
func (i *SplitsTiles) SetInt64(in int64) { *i = SplitsTiles(in) }
// Desc returns the description of the SplitsTiles value.
func (i SplitsTiles) Desc() string { return enums.Desc(i, _SplitsTilesDescMap) }
// SplitsTilesValues returns all possible values for the type SplitsTiles.
func SplitsTilesValues() []SplitsTiles { return _SplitsTilesValues }
// Values returns all possible values for the type SplitsTiles.
func (i SplitsTiles) Values() []enums.Enum { return enums.Values(_SplitsTilesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i SplitsTiles) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *SplitsTiles) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "SplitsTiles")
}
var _StageTypesValues = []StageTypes{0, 1, 2, 3, 4, 5}
// StageTypesN is the highest valid value for type StageTypes, plus one.
const StageTypesN StageTypes = 6
var _StageTypesValueMap = map[string]StageTypes{`WindowStage`: 0, `DialogStage`: 1, `MenuStage`: 2, `TooltipStage`: 3, `SnackbarStage`: 4, `CompleterStage`: 5}
var _StageTypesDescMap = map[StageTypes]string{0: `WindowStage is a MainStage that displays a [Scene] in a full window. One of these must be created first, as the primary app content, and it typically persists throughout. It fills the [renderWindow]. Additional windows can be created either within the same [renderWindow] on all platforms or in separate [renderWindow]s on desktop platforms.`, 1: `DialogStage is a MainStage that displays a [Scene] in a smaller dialog window on top of a [WindowStage], or in a full or separate window. It can be [Stage.Modal] or not.`, 2: `MenuStage is a PopupStage that displays a [Scene] typically containing [Button]s overlaid on a MainStage. It is typically [Stage.Modal] and [Stage.ClickOff], and closes when an button is clicked.`, 3: `TooltipStage is a PopupStage that displays a [Scene] with extra text info for a widget overlaid on a MainStage. It is typically [Stage.ClickOff] and not [Stage.Modal].`, 4: `SnackbarStage is a PopupStage that displays a [Scene] with text info and an optional additional button. It is displayed at the bottom of the screen. It is typically not [Stage.ClickOff] or [Stage.Modal], but has a [Stage.Timeout].`, 5: `CompleterStage is a PopupStage that displays a [Scene] with text completion options, spelling corrections, or other such dynamic info. It is typically [Stage.ClickOff], not [Stage.Modal], dynamically updating, and closes when something is selected or typing renders it no longer relevant.`}
var _StageTypesMap = map[StageTypes]string{0: `WindowStage`, 1: `DialogStage`, 2: `MenuStage`, 3: `TooltipStage`, 4: `SnackbarStage`, 5: `CompleterStage`}
// String returns the string representation of this StageTypes value.
func (i StageTypes) String() string { return enums.String(i, _StageTypesMap) }
// SetString sets the StageTypes value from its string representation,
// and returns an error if the string is invalid.
func (i *StageTypes) SetString(s string) error {
return enums.SetString(i, s, _StageTypesValueMap, "StageTypes")
}
// Int64 returns the StageTypes value as an int64.
func (i StageTypes) Int64() int64 { return int64(i) }
// SetInt64 sets the StageTypes value from an int64.
func (i *StageTypes) SetInt64(in int64) { *i = StageTypes(in) }
// Desc returns the description of the StageTypes value.
func (i StageTypes) Desc() string { return enums.Desc(i, _StageTypesDescMap) }
// StageTypesValues returns all possible values for the type StageTypes.
func StageTypesValues() []StageTypes { return _StageTypesValues }
// Values returns all possible values for the type StageTypes.
func (i StageTypes) Values() []enums.Enum { return enums.Values(_StageTypesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i StageTypes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *StageTypes) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "StageTypes")
}
var _SwitchTypesValues = []SwitchTypes{0, 1, 2, 3, 4}
// SwitchTypesN is the highest valid value for type SwitchTypes, plus one.
const SwitchTypesN SwitchTypes = 5
var _SwitchTypesValueMap = map[string]SwitchTypes{`switch`: 0, `chip`: 1, `checkbox`: 2, `radio-button`: 3, `segmented-button`: 4}
var _SwitchTypesDescMap = map[SwitchTypes]string{0: `SwitchSwitch indicates to display a switch as a switch (toggle slider).`, 1: `SwitchChip indicates to display a switch as chip (like Material Design's filter chip), which is typically only used in the context of [Switches].`, 2: `SwitchCheckbox indicates to display a switch as a checkbox.`, 3: `SwitchRadioButton indicates to display a switch as a radio button.`, 4: `SwitchSegmentedButton indicates to display a segmented button, which is typically only used in the context of [Switches].`}
var _SwitchTypesMap = map[SwitchTypes]string{0: `switch`, 1: `chip`, 2: `checkbox`, 3: `radio-button`, 4: `segmented-button`}
// String returns the string representation of this SwitchTypes value.
func (i SwitchTypes) String() string { return enums.String(i, _SwitchTypesMap) }
// SetString sets the SwitchTypes value from its string representation,
// and returns an error if the string is invalid.
func (i *SwitchTypes) SetString(s string) error {
return enums.SetString(i, s, _SwitchTypesValueMap, "SwitchTypes")
}
// Int64 returns the SwitchTypes value as an int64.
func (i SwitchTypes) Int64() int64 { return int64(i) }
// SetInt64 sets the SwitchTypes value from an int64.
func (i *SwitchTypes) SetInt64(in int64) { *i = SwitchTypes(in) }
// Desc returns the description of the SwitchTypes value.
func (i SwitchTypes) Desc() string { return enums.Desc(i, _SwitchTypesDescMap) }
// SwitchTypesValues returns all possible values for the type SwitchTypes.
func SwitchTypesValues() []SwitchTypes { return _SwitchTypesValues }
// Values returns all possible values for the type SwitchTypes.
func (i SwitchTypes) Values() []enums.Enum { return enums.Values(_SwitchTypesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i SwitchTypes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *SwitchTypes) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "SwitchTypes")
}
var _TabTypesValues = []TabTypes{0, 1, 2, 3, 4}
// TabTypesN is the highest valid value for type TabTypes, plus one.
const TabTypesN TabTypes = 5
var _TabTypesValueMap = map[string]TabTypes{`StandardTabs`: 0, `FunctionalTabs`: 1, `NavigationAuto`: 2, `NavigationBar`: 3, `NavigationDrawer`: 4}
var _TabTypesDescMap = map[TabTypes]string{0: `StandardTabs indicates to render the standard type of Material Design style tabs.`, 1: `FunctionalTabs indicates to render functional tabs like those in Google Chrome. These tabs take up less space and are the only kind that can be closed. They will also support being moved at some point.`, 2: `NavigationAuto indicates to render the tabs as either [NavigationBar] or [NavigationDrawer] if [WidgetBase.SizeClass] is [SizeCompact] or not, respectively. NavigationAuto should typically be used instead of one of the specific navigation types for better cross-platform compatability.`, 3: `NavigationBar indicates to render the tabs as a bottom navigation bar with text and icons.`, 4: `NavigationDrawer indicates to render the tabs as a side navigation drawer with text and icons.`}
var _TabTypesMap = map[TabTypes]string{0: `StandardTabs`, 1: `FunctionalTabs`, 2: `NavigationAuto`, 3: `NavigationBar`, 4: `NavigationDrawer`}
// String returns the string representation of this TabTypes value.
func (i TabTypes) String() string { return enums.String(i, _TabTypesMap) }
// SetString sets the TabTypes value from its string representation,
// and returns an error if the string is invalid.
func (i *TabTypes) SetString(s string) error {
return enums.SetString(i, s, _TabTypesValueMap, "TabTypes")
}
// Int64 returns the TabTypes value as an int64.
func (i TabTypes) Int64() int64 { return int64(i) }
// SetInt64 sets the TabTypes value from an int64.
func (i *TabTypes) SetInt64(in int64) { *i = TabTypes(in) }
// Desc returns the description of the TabTypes value.
func (i TabTypes) Desc() string { return enums.Desc(i, _TabTypesDescMap) }
// TabTypesValues returns all possible values for the type TabTypes.
func TabTypesValues() []TabTypes { return _TabTypesValues }
// Values returns all possible values for the type TabTypes.
func (i TabTypes) Values() []enums.Enum { return enums.Values(_TabTypesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i TabTypes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *TabTypes) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "TabTypes") }
var _TextTypesValues = []TextTypes{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
// TextTypesN is the highest valid value for type TextTypes, plus one.
const TextTypesN TextTypes = 16
var _TextTypesValueMap = map[string]TextTypes{`DisplayLarge`: 0, `DisplayMedium`: 1, `DisplaySmall`: 2, `HeadlineLarge`: 3, `HeadlineMedium`: 4, `HeadlineSmall`: 5, `TitleLarge`: 6, `TitleMedium`: 7, `TitleSmall`: 8, `BodyLarge`: 9, `BodyMedium`: 10, `BodySmall`: 11, `LabelLarge`: 12, `LabelMedium`: 13, `LabelSmall`: 14, `Supporting`: 15}
var _TextTypesDescMap = map[TextTypes]string{0: `TextDisplayLarge is large, short, and important display text with a default font size of 57dp.`, 1: `TextDisplayMedium is medium-sized, short, and important display text with a default font size of 45dp.`, 2: `TextDisplaySmall is small, short, and important display text with a default font size of 36dp.`, 3: `TextHeadlineLarge is large, high-emphasis headline text with a default font size of 32dp.`, 4: `TextHeadlineMedium is medium-sized, high-emphasis headline text with a default font size of 28dp.`, 5: `TextHeadlineSmall is small, high-emphasis headline text with a default font size of 24dp.`, 6: `TextTitleLarge is large, medium-emphasis title text with a default font size of 22dp.`, 7: `TextTitleMedium is medium-sized, medium-emphasis title text with a default font size of 16dp.`, 8: `TextTitleSmall is small, medium-emphasis title text with a default font size of 14dp.`, 9: `TextBodyLarge is large body text used for longer passages of text with a default font size of 16dp.`, 10: `TextBodyMedium is medium-sized body text used for longer passages of text with a default font size of 14dp.`, 11: `TextBodySmall is small body text used for longer passages of text with a default font size of 12dp.`, 12: `TextLabelLarge is large text used for label text (like a caption or the text inside a button) with a default font size of 14dp.`, 13: `TextLabelMedium is medium-sized text used for label text (like a caption or the text inside a button) with a default font size of 12dp.`, 14: `TextLabelSmall is small text used for label text (like a caption or the text inside a button) with a default font size of 11dp.`, 15: `TextSupporting is medium-sized supporting text typically used for secondary dialog information below the title. It has a default font size of 14dp and color of [colors.Scheme.OnSurfaceVariant].`}
var _TextTypesMap = map[TextTypes]string{0: `DisplayLarge`, 1: `DisplayMedium`, 2: `DisplaySmall`, 3: `HeadlineLarge`, 4: `HeadlineMedium`, 5: `HeadlineSmall`, 6: `TitleLarge`, 7: `TitleMedium`, 8: `TitleSmall`, 9: `BodyLarge`, 10: `BodyMedium`, 11: `BodySmall`, 12: `LabelLarge`, 13: `LabelMedium`, 14: `LabelSmall`, 15: `Supporting`}
// String returns the string representation of this TextTypes value.
func (i TextTypes) String() string { return enums.String(i, _TextTypesMap) }
// SetString sets the TextTypes value from its string representation,
// and returns an error if the string is invalid.
func (i *TextTypes) SetString(s string) error {
return enums.SetString(i, s, _TextTypesValueMap, "TextTypes")
}
// Int64 returns the TextTypes value as an int64.
func (i TextTypes) Int64() int64 { return int64(i) }
// SetInt64 sets the TextTypes value from an int64.
func (i *TextTypes) SetInt64(in int64) { *i = TextTypes(in) }
// Desc returns the description of the TextTypes value.
func (i TextTypes) Desc() string { return enums.Desc(i, _TextTypesDescMap) }
// TextTypesValues returns all possible values for the type TextTypes.
func TextTypesValues() []TextTypes { return _TextTypesValues }
// Values returns all possible values for the type TextTypes.
func (i TextTypes) Values() []enums.Enum { return enums.Values(_TextTypesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i TextTypes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *TextTypes) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "TextTypes")
}
var _TextFieldTypesValues = []TextFieldTypes{0, 1}
// TextFieldTypesN is the highest valid value for type TextFieldTypes, plus one.
const TextFieldTypesN TextFieldTypes = 2
var _TextFieldTypesValueMap = map[string]TextFieldTypes{`Filled`: 0, `Outlined`: 1}
var _TextFieldTypesDescMap = map[TextFieldTypes]string{0: `TextFieldFilled represents a filled [TextField] with a background color and a bottom border.`, 1: `TextFieldOutlined represents an outlined [TextField] with a border on all sides and no background color.`}
var _TextFieldTypesMap = map[TextFieldTypes]string{0: `Filled`, 1: `Outlined`}
// String returns the string representation of this TextFieldTypes value.
func (i TextFieldTypes) String() string { return enums.String(i, _TextFieldTypesMap) }
// SetString sets the TextFieldTypes value from its string representation,
// and returns an error if the string is invalid.
func (i *TextFieldTypes) SetString(s string) error {
return enums.SetString(i, s, _TextFieldTypesValueMap, "TextFieldTypes")
}
// Int64 returns the TextFieldTypes value as an int64.
func (i TextFieldTypes) Int64() int64 { return int64(i) }
// SetInt64 sets the TextFieldTypes value from an int64.
func (i *TextFieldTypes) SetInt64(in int64) { *i = TextFieldTypes(in) }
// Desc returns the description of the TextFieldTypes value.
func (i TextFieldTypes) Desc() string { return enums.Desc(i, _TextFieldTypesDescMap) }
// TextFieldTypesValues returns all possible values for the type TextFieldTypes.
func TextFieldTypesValues() []TextFieldTypes { return _TextFieldTypesValues }
// Values returns all possible values for the type TextFieldTypes.
func (i TextFieldTypes) Values() []enums.Enum { return enums.Values(_TextFieldTypesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i TextFieldTypes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *TextFieldTypes) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "TextFieldTypes")
}
var _widgetFlagsValues = []widgetFlags{0, 1, 2}
// widgetFlagsN is the highest valid value for type widgetFlags, plus one.
const widgetFlagsN widgetFlags = 3
var _widgetFlagsValueMap = map[string]widgetFlags{`ValueNewWindow`: 0, `NeedsRender`: 1, `FirstRender`: 2}
var _widgetFlagsDescMap = map[widgetFlags]string{0: `widgetValueNewWindow indicates that the dialog of a [Value] should be opened as a new window, instead of a typical full window in the same current window. This is set by [InitValueButton] and handled by [openValueDialog]. This is triggered by holding down the Shift key while clicking on a [Value] button. Certain values such as [FileButton] may set this to true in their [InitValueButton] function.`, 1: `widgetNeedsRender is whether the widget needs to be rendered on the next render iteration.`, 2: `widgetFirstRender indicates that we were the first to render, and pushed our parent's bounds, which then need to be popped.`}
var _widgetFlagsMap = map[widgetFlags]string{0: `ValueNewWindow`, 1: `NeedsRender`, 2: `FirstRender`}
// String returns the string representation of this widgetFlags value.
func (i widgetFlags) String() string { return enums.BitFlagString(i, _widgetFlagsValues) }
// BitIndexString returns the string representation of this widgetFlags value
// if it is a bit index value (typically an enum constant), and
// not an actual bit flag value.
func (i widgetFlags) BitIndexString() string { return enums.String(i, _widgetFlagsMap) }
// SetString sets the widgetFlags value from its string representation,
// and returns an error if the string is invalid.
func (i *widgetFlags) SetString(s string) error { *i = 0; return i.SetStringOr(s) }
// SetStringOr sets the widgetFlags value from its string representation
// while preserving any bit flags already set, and returns an
// error if the string is invalid.
func (i *widgetFlags) SetStringOr(s string) error {
return enums.SetStringOr(i, s, _widgetFlagsValueMap, "widgetFlags")
}
// Int64 returns the widgetFlags value as an int64.
func (i widgetFlags) Int64() int64 { return int64(i) }
// SetInt64 sets the widgetFlags value from an int64.
func (i *widgetFlags) SetInt64(in int64) { *i = widgetFlags(in) }
// Desc returns the description of the widgetFlags value.
func (i widgetFlags) Desc() string { return enums.Desc(i, _widgetFlagsDescMap) }
// widgetFlagsValues returns all possible values for the type widgetFlags.
func widgetFlagsValues() []widgetFlags { return _widgetFlagsValues }
// Values returns all possible values for the type widgetFlags.
func (i widgetFlags) Values() []enums.Enum { return enums.Values(_widgetFlagsValues) }
// HasFlag returns whether these bit flags have the given bit flag set.
func (i *widgetFlags) HasFlag(f enums.BitFlag) bool { return enums.HasFlag((*int64)(i), f) }
// SetFlag sets the value of the given flags in these flags to the given value.
func (i *widgetFlags) SetFlag(on bool, f ...enums.BitFlag) { enums.SetFlag((*int64)(i), on, f...) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i widgetFlags) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *widgetFlags) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "widgetFlags")
}
// 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 core
import (
"fmt"
"image"
"log"
"log/slog"
"path/filepath"
"strings"
"sync"
"time"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/events/key"
"cogentcore.org/core/keymap"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/system"
"cogentcore.org/core/tree"
"github.com/anthonynsimon/bild/clone"
)
// dragSpriteName is the name of the sprite added when dragging an object.
const dragSpriteName = "__DragSprite__"
// note: Events should be in exclusive control of its own state
// and IF we end up needing a mutex, it should be global on main
// entry points (HandleEvent, anything else?)
// Events is an event manager that handles incoming events for a [Scene].
// It creates all the derived event types (Hover, Sliding, Dragging)
// and Focus management for keyboard events.
type Events struct {
// scene is the scene that we manage events for.
scene *Scene
// mutex that protects timer variable updates (e.g., hover AfterFunc's).
timerMu sync.Mutex
// stack of sprites with mouse pointer in BBox, with any listeners present.
spriteInBBox []*Sprite
// currently pressing sprite.
spritePress *Sprite
// currently sliding (dragging) sprite.
spriteSlide *Sprite
// stack of widgets with mouse pointer in BBox, and are not Disabled.
// Last item in the stack is the deepest nested widget (smallest BBox).
mouseInBBox []Widget
// stack of hovered widgets: have mouse pointer in BBox and have Hoverable flag.
hovers []Widget
// lastClickWidget is the last widget that has been clicked on.
lastClickWidget Widget
// lastDoubleClickWidget is the last widget that has been clicked on.
lastDoubleClickWidget Widget
// lastClickTime is the time the last widget was clicked on.
lastClickTime time.Time
// the current candidate for a long hover event.
longHoverWidget Widget
// the position of the mouse at the start of LongHoverTimer.
longHoverPos image.Point
// the timer for the LongHover event, started with time.AfterFunc.
longHoverTimer *time.Timer
// the current candidate for a long press event.
longPressWidget Widget
// the position of the mouse at the start of LongPressTimer.
longPressPos image.Point
// the timer for the LongPress event, started with time.AfterFunc.
longPressTimer *time.Timer
// stack of drag-hovered widgets: have mouse pointer in BBox and have Droppable flag.
dragHovers []Widget
// the deepest widget that was just pressed.
press Widget
// widget receiving mouse dragging events, for drag-n-drop.
drag Widget
// the deepest draggable widget that was just pressed.
dragPress Widget
// widget receiving mouse sliding events.
slide Widget
// the deepest slideable widget that was just pressed.
slidePress Widget
// widget receiving mouse scrolling events, has "scroll focus".
scroll Widget
lastScrollTime time.Time
// widget being held down with RepeatClickable ability.
repeatClick Widget
// the timer for RepeatClickable items.
repeatClickTimer *time.Timer
// widget receiving keyboard events. Use SetFocus, CurFocus.
focus Widget
// widget to focus on at start when no other focus has been
// set yet. Use SetStartFocus.
startFocus Widget
// if StartFocus not set, activate starting focus on first element
startFocusFirst bool
// previously focused widget. Was in Focus when FocusClear is called.
prevFocus Widget
// Currently active shortcuts for this window (shortcuts are always window-wide.
// Use widget key event processing for more local key functions)
shortcuts shortcuts
// source data from DragStart event.
dragData any
}
// mains returns the stack of main stages for our scene.
func (em *Events) mains() *stages {
if em.scene == nil {
return nil
}
return em.scene.Stage.Mains
}
// RenderWindow returns the overall render window in which we reside,
// which could be nil.
func (em *Events) RenderWindow() *renderWindow {
mgr := em.mains()
if mgr == nil {
return nil
}
return mgr.renderWindow
}
func (em *Events) handleEvent(e events.Event) {
if e.IsHandled() {
return
}
switch {
case e.HasPos():
em.handlePosEvent(e)
case e.NeedsFocus():
em.handleFocusEvent(e)
}
}
func (em *Events) handleFocusEvent(e events.Event) {
// key down and key up can not give active focus, only key chord
if em.focus == nil && e.Type() != events.KeyDown && e.Type() != events.KeyUp {
switch {
case em.startFocus != nil:
if DebugSettings.FocusTrace {
fmt.Println(em.scene, "StartFocus:", em.startFocus)
}
em.setFocus(em.startFocus)
case em.prevFocus != nil:
if DebugSettings.FocusTrace {
fmt.Println(em.scene, "PrevFocus:", em.prevFocus)
}
em.setFocus(em.prevFocus)
em.prevFocus = nil
}
}
if em.focus != nil {
em.focus.AsTree().WalkUpParent(func(k tree.Node) bool {
wb := AsWidget(k)
if !wb.IsVisible() {
return tree.Break
}
wb.firstHandleEvent(e)
return !e.IsHandled()
})
if !e.IsHandled() {
em.focus.AsWidget().HandleEvent(e)
}
if !e.IsHandled() {
em.focus.AsTree().WalkUpParent(func(k tree.Node) bool {
wb := AsWidget(k)
if !wb.IsVisible() {
return tree.Break
}
wb.finalHandleEvent(e)
return !e.IsHandled()
})
}
}
em.managerKeyChordEvents(e)
}
func (em *Events) resetOnMouseDown() {
em.press = nil
em.drag = nil
em.dragPress = nil
em.slide = nil
em.slidePress = nil
em.spriteSlide = nil
em.spritePress = nil
em.cancelRepeatClick()
// if we have sent a long hover start event, we send an end
// event (non-nil widget plus nil timer means we already sent)
if em.longHoverWidget != nil && em.longHoverTimer == nil {
em.longHoverWidget.AsWidget().Send(events.LongHoverEnd)
}
em.longHoverWidget = nil
em.longHoverPos = image.Point{}
if em.longHoverTimer != nil {
em.longHoverTimer.Stop()
em.longHoverTimer = nil
}
}
func (em *Events) handlePosEvent(e events.Event) {
pos := e.Pos()
et := e.Type()
sc := em.scene
switch et {
case events.MouseDown:
em.resetOnMouseDown()
case events.MouseDrag:
if em.spriteSlide != nil {
em.spriteSlide.handleEvent(e)
em.spriteSlide.send(events.SlideMove, e)
e.SetHandled()
return
}
if em.slide != nil {
em.slide.AsWidget().HandleEvent(e)
em.slide.AsWidget().Send(events.SlideMove, e)
e.SetHandled()
return
}
case events.Scroll:
if em.scroll != nil {
scInTime := time.Since(em.lastScrollTime) < DeviceSettings.ScrollFocusTime
if scInTime {
em.scroll.AsWidget().HandleEvent(e)
if e.IsHandled() {
em.lastScrollTime = time.Now()
}
return
} else {
em.scroll = nil
}
}
}
em.spriteInBBox = nil
if et != events.MouseMove {
em.getSpriteInBBox(sc, e.WindowPos())
if len(em.spriteInBBox) > 0 {
if em.handleSpriteEvent(e) {
return
}
}
}
em.mouseInBBox = nil
em.getMouseInBBox(sc, pos)
n := len(em.mouseInBBox)
if n == 0 {
if DebugSettings.EventTrace && et != events.MouseMove {
log.Println("Nothing in bbox:", sc.Geom.TotalBBox, "pos:", pos)
}
return
}
var press, dragPress, slidePress, move, up, repeatClick Widget
for i := n - 1; i >= 0; i-- {
w := em.mouseInBBox[i]
wb := w.AsWidget()
// we need to handle this here and not in [Events.GetMouseInBBox] so that
// we correctly process cursors for disabled elements.
// in ScRenderBBoxes, everyone is effectively enabled
if wb.StateIs(states.Disabled) && !sc.renderBBoxes {
continue
}
w.AsWidget().HandleEvent(e) // everyone gets the primary event who is in scope, deepest first
switch et {
case events.MouseMove:
em.scroll = nil
if move == nil && wb.Styles.Abilities.IsHoverable() {
move = w
}
case events.MouseDown:
em.scroll = nil
// in ScRenderBBoxes, everyone is effectively pressable
if press == nil && (wb.Styles.Abilities.IsPressable() || sc.renderBBoxes) {
press = w
}
if dragPress == nil && wb.Styles.Abilities.Is(abilities.Draggable) {
dragPress = w
}
if slidePress == nil && wb.Styles.Abilities.Is(abilities.Slideable) {
slidePress = w
}
if repeatClick == nil && wb.Styles.Abilities.Is(abilities.RepeatClickable) {
repeatClick = w
}
case events.MouseUp:
em.scroll = nil
// in ScRenderBBoxes, everyone is effectively pressable
if up == nil && (wb.Styles.Abilities.IsPressable() || sc.renderBBoxes) {
up = w
}
case events.Scroll:
if e.IsHandled() {
if em.scroll == nil {
em.scroll = w
em.lastScrollTime = time.Now()
}
}
}
}
switch et {
case events.MouseDown:
if press != nil {
em.press = press
}
if dragPress != nil {
em.dragPress = dragPress
}
if slidePress != nil {
em.slidePress = slidePress
}
if repeatClick != nil {
em.repeatClick = repeatClick
em.startRepeatClickTimer()
}
em.handleLongPress(e)
case events.MouseMove:
hovs := make([]Widget, 0, len(em.mouseInBBox))
for _, w := range em.mouseInBBox { // requires forward iter through em.MouseInBBox
wb := w.AsWidget()
// in ScRenderBBoxes, everyone is effectively hoverable
if wb.Styles.Abilities.IsHoverable() || sc.renderBBoxes {
hovs = append(hovs, w)
}
}
if sc.renderBBoxes {
pselw := sc.selectedWidget
if len(em.hovers) > 0 {
sc.selectedWidget = em.hovers[len(em.hovers)-1]
} else {
sc.selectedWidget = nil
}
if sc.selectedWidget != pselw {
if pselw != nil {
pselw.AsWidget().NeedsRender()
}
if sc.selectedWidget != nil {
sc.selectedWidget.AsWidget().NeedsRender()
}
}
}
em.hovers = em.updateHovers(hovs, em.hovers, e, events.MouseEnter, events.MouseLeave)
em.handleLongHover(e)
case events.MouseDrag:
if em.drag != nil {
hovs := make([]Widget, 0, len(em.mouseInBBox))
for _, w := range em.mouseInBBox { // requires forward iter through em.MouseInBBox
wb := w.AsWidget()
if wb.AbilityIs(abilities.Droppable) {
hovs = append(hovs, w)
}
}
em.dragHovers = em.updateHovers(hovs, em.dragHovers, e, events.DragEnter, events.DragLeave)
em.dragMove(e) // updates sprite position
em.drag.AsWidget().HandleEvent(e) // raw drag
em.drag.AsWidget().Send(events.DragMove, e) // usually ignored
e.SetHandled()
} else {
if em.dragPress != nil && em.dragStartCheck(e, DeviceSettings.DragStartTime, DeviceSettings.DragStartDistance) {
em.cancelRepeatClick()
em.cancelLongPress()
em.dragPress.AsWidget().Send(events.DragStart, e)
e.SetHandled()
} else if em.slidePress != nil && em.dragStartCheck(e, DeviceSettings.SlideStartTime, DeviceSettings.DragStartDistance) {
em.cancelRepeatClick()
em.cancelLongPress()
em.slide = em.slidePress
em.slide.AsWidget().Send(events.SlideStart, e)
e.SetHandled()
}
}
// if we already have a long press widget, we update it based on our dragging movement
if em.longPressWidget != nil {
em.handleLongPress(e)
}
case events.MouseUp:
em.cancelRepeatClick()
if em.slide != nil {
em.slide.AsWidget().Send(events.SlideStop, e)
e.SetHandled()
em.slide = nil
em.press = nil
}
if em.drag != nil {
em.dragDrop(em.drag, e)
em.press = nil
}
// if we have sent a long press start event, we don't send click
// events (non-nil widget plus nil timer means we already sent)
if em.press == up && up != nil && !(em.longPressWidget != nil && em.longPressTimer == nil) {
em.cancelLongPress()
switch e.MouseButton() {
case events.Left:
if sc.selectedWidgetChan != nil {
sc.selectedWidgetChan <- up
return
}
dcInTime := time.Since(em.lastClickTime) < DeviceSettings.DoubleClickInterval
em.lastClickTime = time.Now()
sentMulti := false
switch {
case em.lastDoubleClickWidget == up && dcInTime:
tce := e.NewFromClone(events.TripleClick)
for i := n - 1; i >= 0; i-- {
w := em.mouseInBBox[i]
wb := w.AsWidget()
if !wb.StateIs(states.Disabled) && wb.AbilityIs(abilities.TripleClickable) {
sentMulti = true
w.AsWidget().HandleEvent(tce)
break
}
}
case em.lastClickWidget == up && dcInTime:
dce := e.NewFromClone(events.DoubleClick)
for i := n - 1; i >= 0; i-- {
w := em.mouseInBBox[i]
wb := w.AsWidget()
if !wb.StateIs(states.Disabled) && wb.AbilityIs(abilities.DoubleClickable) {
em.lastDoubleClickWidget = up // not actually who gets the event
sentMulti = true
w.AsWidget().HandleEvent(dce)
break
}
}
}
if !sentMulti {
em.lastDoubleClickWidget = nil
em.lastClickWidget = up
up.AsWidget().Send(events.Click, e)
}
case events.Right: // note: automatically gets Control+Left
up.AsWidget().Send(events.ContextMenu, e)
}
}
// if our original pressed widget is different from the one we are
// going up on, then it has not gotten a mouse up event yet, so
// we need to send it one
if em.press != up && em.press != nil {
em.press.AsWidget().HandleEvent(e)
}
em.press = nil
em.cancelLongPress()
// a mouse up event acts also acts as a mouse leave
// event on mobile, as that is needed to clear any
// hovered state
if up != nil && TheApp.Platform().IsMobile() {
up.AsWidget().Send(events.MouseLeave, e)
}
case events.Scroll:
switch {
case em.slide != nil:
em.slide.AsWidget().HandleEvent(e)
case em.drag != nil:
em.drag.AsWidget().HandleEvent(e)
case em.press != nil:
em.press.AsWidget().HandleEvent(e)
default:
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
}
}
}
// 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 && prv.AsTree().This != nil {
prv.AsWidget().Send(leave, e)
}
}
for _, cur := range hov {
wasIn := false
for _, prv := range prev {
if prv == cur {
wasIn = true
break
}
}
if !wasIn {
cur.AsWidget().Send(enter, e)
}
}
// todo: detect change in top one, use to update cursor
return hov
}
// topLongHover returns the top-most LongHoverable widget among the Hovers
func (em *Events) topLongHover() Widget {
var deep Widget
for i := len(em.hovers) - 1; i >= 0; i-- {
h := em.hovers[i]
if h.AsWidget().AbilityIs(abilities.LongHoverable) {
deep = h
break
}
}
return deep
}
// handleLongHover handles long hover events
func (em *Events) handleLongHover(e events.Event) {
em.handleLong(e, em.topLongHover(), &em.longHoverWidget, &em.longHoverPos, &em.longHoverTimer, events.LongHoverStart, events.LongHoverEnd, DeviceSettings.LongHoverTime, DeviceSettings.LongHoverStopDistance)
}
// handleLongPress handles long press events
func (em *Events) handleLongPress(e events.Event) {
em.handleLong(e, em.press, &em.longPressWidget, &em.longPressPos, &em.longPressTimer, events.LongPressStart, events.LongPressEnd, DeviceSettings.LongPressTime, DeviceSettings.LongPressStopDistance)
}
// handleLong is the implementation of [Events.handleLongHover] and
// [EventManger.HandleLongPress]. It handles the logic to do with tracking
// long events using the given pointers to event manager fields and
// constant type, time, and distance properties. It should not need to
// be called by anything except for the aforementioned functions.
func (em *Events) handleLong(e events.Event, deep Widget, w *Widget, pos *image.Point, t **time.Timer, styp, etyp events.Types, stime time.Duration, sdist int) {
em.timerMu.Lock()
defer em.timerMu.Unlock()
// fmt.Println("em:", em.Scene.Name)
clearLong := func() {
if *t != nil {
(*t).Stop() // TODO: do we need to close this?
*t = nil
}
*w = nil
*pos = image.Point{}
// fmt.Println("cleared hover")
}
cpos := e.WindowPos()
dst := int(math32.Hypot(float32(pos.X-cpos.X), float32(pos.Y-cpos.Y)))
// fmt.Println("dist:", dst)
// we have no long hovers, so we must be done
if deep == nil {
// fmt.Println("no deep")
if *w == nil {
// fmt.Println("no lhw")
return
}
// if we have already finished the timer, then we have already
// sent the start event, so we have to send the end one
if *t == nil {
(*w).AsWidget().Send(etyp, e)
}
clearLong()
// fmt.Println("cleared")
return
}
// we still have the current one, so there is nothing to do
// but make sure our position hasn't changed too much
if deep == *w {
// if we haven't gone too far, we have nothing to do
if dst <= sdist {
// fmt.Println("bail on dist:", dst)
return
}
// If we have gone too far, we are done with the long hover and
// we must clear it. However, critically, we do not return, as
// we must make a new tooltip immediately; otherwise, we may end
// up not getting another mouse move event, so we will be on the
// element with no tooltip, which is a bug. Not returning here is
// the solution to https://github.com/cogentcore/core/issues/553
(*w).AsWidget().Send(etyp, e)
clearLong()
// fmt.Println("fallthrough after clear")
}
// if we have changed and still have the timer, we never
// sent a start event, so we just bail
if *t != nil {
clearLong()
// fmt.Println("timer non-nil, cleared")
return
}
// we now know we don't have the timer and thus sent the start
// event already, so we need to send a end event
if *w != nil {
(*w).AsWidget().Send(etyp, e)
clearLong()
// fmt.Println("lhw, send end, cleared")
return
}
// now we can set it to our new widget
*w = deep
// fmt.Println("setting new:", deep)
*pos = e.WindowPos()
*t = time.AfterFunc(stime, func() {
win := em.RenderWindow()
if win == nil {
return
}
rc := win.renderContext() // have to get this one first
rc.lock()
defer rc.unlock()
em.timerMu.Lock() // then can get this
defer em.timerMu.Unlock()
if *w == nil {
return
}
(*w).AsWidget().Send(styp, e)
// we are done with the timer, and this indicates that
// we have sent a start event
*t = nil
})
}
func (em *Events) getMouseInBBox(w Widget, pos image.Point) {
wb := w.AsWidget()
wb.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
// we do not handle disabled here so that
// we correctly process cursors for disabled elements.
// it needs to be handled downstream by anyone who needs it.
if !cwb.IsVisible() {
return tree.Break
}
if !cwb.posInScBBox(pos) {
return tree.Break
}
em.mouseInBBox = append(em.mouseInBBox, cw)
if cwb.Parts != nil {
em.getMouseInBBox(cwb.Parts, pos)
}
if ly := AsFrame(cw); ly != nil {
for d := math32.X; d <= math32.Y; d++ {
if ly.HasScroll[d] {
sb := ly.scrolls[d]
em.getMouseInBBox(sb, pos)
}
}
}
return tree.Continue
})
}
func (em *Events) cancelLongPress() {
// if we have sent a long press start event, we send an end
// event (non-nil widget plus nil timer means we already sent)
if em.longPressWidget != nil && em.longPressTimer == nil {
em.longPressWidget.AsWidget().Send(events.LongPressEnd)
}
em.longPressWidget = nil
em.longPressPos = image.Point{}
if em.longPressTimer != nil {
em.longPressTimer.Stop()
em.longPressTimer = nil
}
}
func (em *Events) cancelRepeatClick() {
em.repeatClick = nil
if em.repeatClickTimer != nil {
em.repeatClickTimer.Stop()
em.repeatClickTimer = nil
}
}
func (em *Events) startRepeatClickTimer() {
if em.repeatClick == nil || !em.repeatClick.AsWidget().IsVisible() {
return
}
delay := DeviceSettings.RepeatClickTime
if em.repeatClickTimer == nil {
delay *= 8
}
em.repeatClickTimer = time.AfterFunc(delay, func() {
if em.repeatClick == nil || !em.repeatClick.AsWidget().IsVisible() {
return
}
em.repeatClick.AsWidget().Send(events.Click)
em.startRepeatClickTimer()
})
}
func (em *Events) dragStartCheck(e events.Event, dur time.Duration, dist int) bool {
since := e.SinceStart()
if since < dur {
return false
}
dst := int(math32.FromPoint(e.StartDelta()).Length())
return dst >= dist
}
// dragStart starts a drag event, capturing a sprite image of the given widget
// and storing the data for later use during Drop.
// A drag does not officially start until this is called.
func (em *Events) dragStart(w Widget, data any, e events.Event) {
ms := em.scene.Stage.Main
if ms == nil {
return
}
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))
sp.Active = true
ms.Sprites.Add(sp)
}
// dragMove is generally handled entirely by the event manager
func (em *Events) dragMove(e events.Event) {
ms := em.scene.Stage.Main
if ms == nil {
return
}
sp, ok := ms.Sprites.SpriteByName(dragSpriteName)
if !ok {
fmt.Println("Drag sprite not found")
return
}
sp.Geom.Pos = e.WindowPos()
for _, w := range em.dragHovers {
w.AsWidget().ScrollToThis()
}
em.scene.NeedsRender()
}
func (em *Events) dragClearSprite() {
ms := em.scene.Stage.Main
if ms == nil {
return
}
ms.Sprites.InactivateSprite(dragSpriteName)
}
func (em *Events) dragMenuAddModText(m *Scene, mod events.DropMods) {
text := ""
switch mod {
case events.DropCopy:
text = "Copy (use Shift to move):"
case events.DropMove:
text = "Move:"
}
NewText(m).SetType(TextLabelLarge).SetText(text).Styler(func(s *styles.Style) {
s.Margin.Set(units.Em(0.5))
})
}
// dragDrop sends the [events.Drop] event to the top of the DragHovers stack.
// clearing the current dragging sprite before doing anything.
// It is up to the target to call
func (em *Events) dragDrop(drag Widget, e events.Event) {
em.dragClearSprite()
data := em.dragData
em.drag = nil
if len(em.dragHovers) == 0 {
if DebugSettings.EventTrace {
fmt.Println(drag, "Drop has no target")
}
return
}
for _, dwi := range em.dragHovers {
dwi.AsWidget().SetState(false, states.DragHovered)
}
targ := em.dragHovers[len(em.dragHovers)-1]
de := events.NewDragDrop(events.Drop, e.(*events.Mouse)) // gets the actual mod at this point
de.Data = data
de.Source = drag
de.Target = targ
if DebugSettings.EventTrace {
fmt.Println(targ, "Drop with mod:", de.DropMod, "source:", de.Source)
}
targ.AsWidget().HandleEvent(de)
}
// dropFinalize should be called as the last step in the Drop event processing,
// to send the DropDeleteSource event to the source in case of DropMod == DropMove.
// Otherwise, nothing actually happens.
func (em *Events) dropFinalize(de *events.DragDrop) {
if de.DropMod != events.DropMove {
return
}
de.Typ = events.DropDeleteSource
de.ClearHandled()
de.Source.(Widget).AsWidget().HandleEvent(de)
}
// Clipboard returns the [system.Clipboard], supplying the window context
// if available.
func (em *Events) Clipboard() system.Clipboard {
var gwin system.Window
if win := em.RenderWindow(); win != nil {
gwin = win.SystemWindow
}
return system.TheApp.Clipboard(gwin)
}
// setCursor sets the window cursor to the given [cursors.Cursor].
func (em *Events) setCursor(cur cursors.Cursor) {
win := em.RenderWindow()
if win == nil {
return
}
if !win.isVisible() {
return
}
errors.Log(system.TheApp.Cursor(win.SystemWindow).Set(cur))
}
// focusClear saves current focus to FocusPrev
func (em *Events) focusClear() bool {
if em.focus != nil {
if DebugSettings.FocusTrace {
fmt.Println(em.scene, "FocusClear:", em.focus)
}
em.prevFocus = em.focus
}
return em.setFocus(nil)
}
// setFocusQuiet sets focus to given item, and returns true if focus changed.
// If item is nil, then nothing has focus.
// This does NOT send the [events.Focus] event to the widget.
// See [Events.setFocus] for version that does send an event.
func (em *Events) setFocusQuiet(w Widget) bool {
if DebugSettings.FocusTrace {
fmt.Println(em.scene, "SetFocus:", w)
}
got := em.setFocusImpl(w, false) // no event
if !got {
if DebugSettings.FocusTrace {
fmt.Println(em.scene, "SetFocus: Failed", w)
}
return false
}
if w != nil {
w.AsWidget().ScrollToThis()
}
return got
}
// setFocus sets focus to given item, and returns true if focus changed.
// If item is nil, then nothing has focus.
// This sends the [events.Focus] event to the widget.
// See [Events.setFocusQuiet] for a version that does not.
func (em *Events) setFocus(w Widget) bool {
if DebugSettings.FocusTrace {
fmt.Println(em.scene, "SetFocusEvent:", w)
}
got := em.setFocusImpl(w, true) // sends event
if !got {
if DebugSettings.FocusTrace {
fmt.Println(em.scene, "SetFocusEvent: Failed", w)
}
return false
}
if w != nil {
w.AsWidget().ScrollToThis()
}
return got
}
// setFocusImpl sets focus to given item -- returns true if focus changed.
// If item is nil, then nothing has focus.
// sendEvent determines whether the events.Focus event is sent to the focused item.
func (em *Events) setFocusImpl(w Widget, sendEvent bool) bool {
cfoc := em.focus
if cfoc == nil {
em.focus = nil
cfoc = nil
}
if cfoc != nil && w != nil && cfoc == w {
if DebugSettings.FocusTrace {
fmt.Println(em.scene, "Already Focus:", cfoc)
}
// if sendEvent { // still send event
// w.Send(events.Focus)
// }
return false
}
if cfoc != nil {
if DebugSettings.FocusTrace {
fmt.Println(em.scene, "Losing focus:", cfoc)
}
cfoc.AsWidget().Send(events.FocusLost)
}
em.focus = w
if sendEvent && w != nil {
w.AsWidget().Send(events.Focus)
}
return true
}
// focusNext sets the focus on the next item
// that can accept focus after the current Focus item.
// returns true if a focus item found.
func (em *Events) focusNext() bool {
if em.focus == nil {
return em.focusFirst()
}
return em.FocusNextFrom(em.focus)
}
// FocusNextFrom sets the focus on the next item
// that can accept focus after the given item.
// It returns true if a focus item is found.
func (em *Events) FocusNextFrom(from Widget) bool {
next := widgetNextFunc(from, func(w Widget) bool {
wb := w.AsWidget()
return wb.IsVisible() && !wb.StateIs(states.Disabled) && wb.AbilityIs(abilities.Focusable)
})
em.setFocus(next)
return next != nil
}
// focusOnOrNext sets the focus on the given item, or the next one that can
// accept focus; returns true if a new focus item is found.
func (em *Events) focusOnOrNext(foc Widget) bool {
cfoc := em.focus
if cfoc == foc {
return true
}
wb := AsWidget(foc)
if !wb.IsVisible() {
return false
}
if wb.AbilityIs(abilities.Focusable) {
em.setFocus(foc)
return true
}
return em.FocusNextFrom(foc)
}
// focusOnOrPrev sets the focus on the given item, or the previous one that can
// accept focus; returns true if a new focus item is found.
func (em *Events) focusOnOrPrev(foc Widget) bool {
cfoc := em.focus
if cfoc == foc {
return true
}
wb := AsWidget(foc)
if !wb.IsVisible() {
return false
}
if wb.AbilityIs(abilities.Focusable) {
em.setFocus(foc)
return true
}
return em.focusPrevFrom(foc)
}
// focusPrev sets the focus on the previous item before the
// current focus item.
func (em *Events) focusPrev() bool {
if em.focus == nil {
return em.focusLast()
}
return em.focusPrevFrom(em.focus)
}
// focusPrevFrom sets the focus on the previous item before the given item
// (can be nil).
func (em *Events) focusPrevFrom(from Widget) bool {
prev := widgetPrevFunc(from, func(w Widget) bool {
wb := w.AsWidget()
return wb.IsVisible() && !wb.StateIs(states.Disabled) && wb.AbilityIs(abilities.Focusable)
})
em.setFocus(prev)
return prev != nil
}
// focusFirst sets the focus on the first focusable item in the tree.
// returns true if a focusable item was found.
func (em *Events) focusFirst() bool {
return em.FocusNextFrom(em.scene.This.(Widget))
}
// focusLast sets the focus on the last focusable item in the tree.
// returns true if a focusable item was found.
func (em *Events) focusLast() bool {
return em.focusLastFrom(em.scene)
}
// focusLastFrom sets the focus on the last focusable item in the given tree.
// returns true if a focusable item was found.
func (em *Events) focusLastFrom(from Widget) bool {
last := tree.Last(from).(Widget)
return em.focusOnOrPrev(last)
}
// SetStartFocus sets the given item to be the first focus when the window opens.
func (em *Events) SetStartFocus(k Widget) {
em.startFocus = k
}
// activateStartFocus activates start focus if there is no current focus
// and StartFocus is set -- returns true if activated
func (em *Events) activateStartFocus() bool {
if em.startFocus == nil && !em.startFocusFirst {
// fmt.Println("no start focus")
return false
}
sf := em.startFocus
em.startFocus = nil
if sf == nil {
em.focusFirst()
} else {
// fmt.Println("start focus on:", sf)
em.setFocus(sf)
}
return true
}
// MangerKeyChordEvents handles lower-priority manager-level key events.
// Mainly tab, shift-tab, and Inspector and Settings.
// event will be marked as processed if handled here.
func (em *Events) managerKeyChordEvents(e events.Event) {
if e.IsHandled() {
return
}
if e.Type() != events.KeyChord {
return
}
win := em.RenderWindow()
if win == nil {
return
}
sc := em.scene
cs := e.KeyChord()
kf := keymap.Of(cs)
switch kf {
case keymap.FocusNext: // tab
if em.focusNext() {
e.SetHandled()
}
case keymap.FocusPrev: // shift-tab
if em.focusPrev() {
e.SetHandled()
}
case keymap.WinSnapshot:
dstr := time.Now().Format(time.DateOnly + "-" + "15-04-05")
fnm := filepath.Join(TheApp.AppDataDir(), "screenshot-"+sc.Name+"-"+dstr+".png")
if errors.Log(imagex.Save(sc.Pixels, fnm)) == nil {
fmt.Println("Saved screenshot to", strings.ReplaceAll(fnm, " ", `\ `))
}
e.SetHandled()
case keymap.ZoomIn:
win.stepZoom(1)
e.SetHandled()
case keymap.ZoomOut:
win.stepZoom(-1)
e.SetHandled()
case keymap.Refresh:
e.SetHandled()
system.TheApp.GetScreens()
UpdateAll()
theWindowGeometrySaver.restoreAll()
// w.FocusInactivate()
// w.FullReRender()
// sz := w.SystemWin.Size()
// w.SetSize(sz)
case keymap.WinFocusNext:
e.SetHandled()
AllRenderWindows.focusNext()
}
if !e.IsHandled() {
em.triggerShortcut(cs)
}
}
// getShortcutsIn gathers all [Button]s in the given parent widget with
// a shortcut specified. It recursively navigates [Button.Menu]s.
func (em *Events) getShortcutsIn(parent Widget) {
parent.AsWidget().WidgetWalkDown(func(w Widget, wb *WidgetBase) bool {
bt := AsButton(w)
if bt == nil {
return tree.Continue
}
if bt.Shortcut != "" {
em.addShortcut(bt.Shortcut.PlatformChord(), bt)
}
if bt.HasMenu() {
tmps := NewScene()
bt.Menu(tmps)
em.getShortcutsIn(tmps)
}
return tree.Break // there are no buttons in buttons
})
}
// shortcuts is a map between a key chord and a specific Button that can be
// triggered. This mapping must be unique, in that each chord has unique
// Button, and generally each Button only has a single chord as well, though
// this is not strictly enforced. shortcuts are evaluated *after* the
// standard KeyMap event processing, so any conflicts are resolved in favor of
// the local widget's key event processing, with the shortcut only operating
// when no conflicting widgets are in focus. shortcuts are always window-wide
// and are intended for global window / toolbar buttons. Widget-specific key
// functions should be handled directly within widget key event
// processing.
type shortcuts map[key.Chord]*Button
// addShortcut adds the given shortcut for the given button.
func (em *Events) addShortcut(chord key.Chord, bt *Button) {
if chord == "" {
return
}
if em.shortcuts == nil {
em.shortcuts = shortcuts{}
}
chords := strings.Split(string(chord), "\n")
for _, c := range chords {
cc := key.Chord(c)
if DebugSettings.KeyEventTrace {
old, exists := em.shortcuts[cc]
if exists && old != bt {
slog.Error("Events.AddShortcut: overwriting duplicate shortcut", "shortcut", cc, "originalButton", old, "newButton", bt)
}
}
em.shortcuts[cc] = bt
}
}
// triggerShortcut attempts to trigger a shortcut, returning true if one was
// triggered, and false otherwise. Also eliminates any shortcuts with deleted
// buttons, and does not trigger for Disabled buttons.
func (em *Events) triggerShortcut(chord key.Chord) bool {
if DebugSettings.KeyEventTrace {
fmt.Printf("Shortcut chord: %v -- looking for button\n", chord)
}
if em.shortcuts == nil {
return false
}
sa, exists := em.shortcuts[chord]
if !exists {
return false
}
if sa == nil || sa.This == nil {
delete(em.shortcuts, chord)
return false
}
if sa.IsDisabled() {
if DebugSettings.KeyEventTrace {
fmt.Printf("Shortcut chord: %v, button: %v -- is inactive, not fired\n", chord, sa.Text)
}
return false
}
if DebugSettings.KeyEventTrace {
fmt.Printf("Shortcut chord: %v, button: %v triggered\n", chord, sa.Text)
}
sa.Send(events.Click)
return true
}
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)
}
}
}
// handleSpriteEvent handles the given event with sprites
// returns true if event was handled
func (em *Events) handleSpriteEvent(e events.Event) bool {
et := e.Type()
loop:
for _, sp := range em.spriteInBBox {
if e.IsHandled() {
break
}
sp.listeners.Call(e) // everyone gets the primary event who is in scope, deepest first
switch et {
case events.MouseDown:
if sp.listeners.HandlesEventType(events.SlideMove) {
e.SetHandled()
em.spriteSlide = sp
em.spriteSlide.send(events.SlideStart, e)
}
if sp.listeners.HandlesEventType(events.Click) {
em.spritePress = sp
}
break loop
case events.MouseUp:
sp.handleEvent(e)
if em.spriteSlide == sp {
sp.send(events.SlideStop, e)
}
if em.spritePress == sp {
sp.send(events.Click, e)
}
}
}
return e.IsHandled()
}
// 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 core
import (
"fmt"
"image"
"io/fs"
"log"
"log/slog"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/fsnotify/fsnotify"
"github.com/mitchellh/go-homedir"
"cogentcore.org/core/base/elide"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/parse/complete"
"cogentcore.org/core/styles"
"cogentcore.org/core/system"
"cogentcore.org/core/tree"
)
// todo:
// * search: use highlighting, not filtering -- < > arrows etc
// * also simple search-while typing in grid?
// * filepicker selector DND is a file:/// url
// FilePicker is a widget for selecting files.
type FilePicker struct {
Frame
// Filterer is an optional filtering function for which files to display.
Filterer FilePickerFilterer `display:"-" json:"-" xml:"-"`
// directory is the absolute path to the directory of files to display.
directory string
// selectedFilename is the name of the currently selected file,
// not including the directory. See [FilePicker.SelectedFile]
// for the full path.
selectedFilename string
// extensions is a list of the target file extensions.
// If there are multiple, they must be comma separated.
// The extensions must include the dot (".") at the start.
// They must be set using [FilePicker.SetExtensions].
extensions string
// extensionMap is a map of lower-cased extensions from Extensions.
// It used for highlighting files with one of these extensions;
// maps onto original Extensions value.
extensionMap map[string]string
// files for current directory
files []*fileinfo.FileInfo
// index of currently selected file in Files list (-1 if none)
selectedIndex int
// change notify for current dir
watcher *fsnotify.Watcher
// channel to close watcher watcher
doneWatcher chan bool
// Previous path that was processed via UpdateFiles
prevPath string
favoritesTable, filesTable *Table
selectField, extensionField *TextField
}
func (fp *FilePicker) Init() {
fp.Frame.Init()
fp.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Grow.Set(1, 1)
})
fp.OnKeyChord(func(e events.Event) {
kf := keymap.Of(e.KeyChord())
if DebugSettings.KeyEventTrace {
slog.Info("FilePicker KeyInput", "widget", fp, "keyFunction", kf)
}
switch kf {
case keymap.Jump, keymap.WordLeft:
e.SetHandled()
fp.directoryUp()
case keymap.Insert, keymap.InsertAfter, keymap.Open, keymap.SelectMode:
e.SetHandled()
if fp.selectFile() {
fp.Send(events.DoubleClick, e) // will close dialog
}
case keymap.Search:
e.SetHandled()
sf := fp.selectField
sf.SetFocus()
}
})
fp.Maker(func(p *tree.Plan) {
if fp.directory == "" {
fp.SetFilename("") // default to current directory
}
if len(recentPaths) == 0 {
openRecentPaths()
}
recentPaths.AddPath(fp.directory, SystemSettings.SavedPathsMax)
saveRecentPaths()
fp.readFiles()
if fp.prevPath != fp.directory {
// TODO(#424): disable for all platforms for now; causing issues
if false && TheApp.Platform() != system.MacOS {
// mac is not supported in a high-capacity fashion at this point
if fp.prevPath == "" {
fp.configWatcher()
} else {
fp.watcher.Remove(fp.prevPath)
}
fp.watcher.Add(fp.directory)
if fp.prevPath == "" {
fp.watchWatcher()
}
}
fp.prevPath = fp.directory
}
tree.AddAt(p, "path", func(w *Chooser) {
Bind(&fp.directory, w)
w.SetEditable(true).SetDefaultNew(true)
w.AddItemsFunc(func() {
fp.addRecentPathItems(&w.Items)
})
w.Styler(func(s *styles.Style) {
s.Grow.Set(1, 0)
})
w.OnChange(func(e events.Event) {
fp.updateFilesEvent()
})
})
tree.AddAt(p, "files", func(w *Frame) {
w.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
})
w.Maker(fp.makeFilesRow)
})
tree.AddAt(p, "selected", func(w *Frame) {
w.Styler(func(s *styles.Style) {
s.Grow.Set(1, 0)
s.Gap.X.Dp(4)
})
w.Maker(fp.makeSelectedRow)
})
})
}
func (fp *FilePicker) Destroy() {
fp.Frame.Destroy()
if fp.watcher != nil {
fp.watcher.Close()
fp.watcher = nil
}
if fp.doneWatcher != nil {
fp.doneWatcher <- true
close(fp.doneWatcher)
fp.doneWatcher = nil
}
}
// FilePickerFilterer is a filtering function for files; returns true if the
// file should be visible in the picker, and false if not
type FilePickerFilterer func(fp *FilePicker, fi *fileinfo.FileInfo) bool
// FilePickerDirOnlyFilter is a [FilePickerFilterer] that only shows directories (folders).
func FilePickerDirOnlyFilter(fp *FilePicker, fi *fileinfo.FileInfo) bool {
return fi.IsDir()
}
// FilePickerExtensionOnlyFilter is a [FilePickerFilterer] that only shows files that
// match the target extensions, and directories.
func FilePickerExtensionOnlyFilter(fp *FilePicker, fi *fileinfo.FileInfo) bool {
if fi.IsDir() {
return true
}
ext := strings.ToLower(filepath.Ext(fi.Name))
_, has := fp.extensionMap[ext]
return has
}
// SetFilename sets the directory and filename of the file picker
// from the given filepath.
func (fp *FilePicker) SetFilename(filename string) *FilePicker {
fp.directory, fp.selectedFilename = filepath.Split(filename)
fp.directory = errors.Log1(filepath.Abs(fp.directory))
return fp
}
// SelectedFile returns the full path to the currently selected file.
func (fp *FilePicker) SelectedFile() string {
sf := fp.selectField
sf.editDone()
return filepath.Join(fp.directory, fp.selectedFilename)
}
// SelectedFileInfo returns the currently selected [fileinfo.FileInfo] or nil.
func (fp *FilePicker) SelectedFileInfo() *fileinfo.FileInfo {
if fp.selectedIndex < 0 || fp.selectedIndex >= len(fp.files) {
return nil
}
return fp.files[fp.selectedIndex]
}
// selectFile selects the current file as the selection.
// if a directory it opens the directory and returns false.
// if a file it selects the file and returns true.
// if no selection, returns false.
func (fp *FilePicker) selectFile() bool {
if fi := fp.SelectedFileInfo(); fi != nil {
if fi.IsDir() {
fp.directory = filepath.Join(fp.directory, fi.Name)
fp.selectedFilename = ""
fp.selectedIndex = -1
fp.updateFilesEvent()
return false
}
return true
}
return false
}
func (fp *FilePicker) MakeToolbar(p *tree.Plan) {
tree.Add(p, func(w *FuncButton) {
w.SetFunc(fp.directoryUp).SetIcon(icons.ArrowUpward).SetKey(keymap.Jump).SetText("Up")
})
tree.Add(p, func(w *FuncButton) {
w.SetFunc(fp.addPathToFavorites).SetIcon(icons.Favorite).SetText("Favorite")
})
tree.Add(p, func(w *FuncButton) {
w.SetFunc(fp.updateFilesEvent).SetIcon(icons.Refresh).SetText("Update")
})
tree.Add(p, func(w *FuncButton) {
w.SetFunc(fp.newFolder).SetIcon(icons.CreateNewFolder)
})
}
func (fp *FilePicker) addRecentPathItems(items *[]ChooserItem) {
for _, sp := range recentPaths {
*items = append(*items, ChooserItem{
Value: sp,
})
}
// TODO: file picker reset and edit recent paths buttons not working
// *items = append(*items, ChooserItem{
// Value: "Reset recent paths",
// Icon: icons.Refresh,
// SeparatorBefore: true,
// Func: func() {
// recentPaths = make(FilePaths, 1, SystemSettings.SavedPathsMax)
// recentPaths[0] = fp.directory
// fp.Update()
// },
// })
// *items = append(*items, ChooserItem{
// Value: "Edit recent paths",
// Icon: icons.Edit,
// Func: func() {
// fp.editRecentPaths()
// },
// })
}
func (fp *FilePicker) makeFilesRow(p *tree.Plan) {
tree.AddAt(p, "favorites", func(w *Table) {
fp.favoritesTable = w
w.SelectedIndex = -1
w.SetReadOnly(true)
w.ReadOnlyKeyNav = false // keys must go to files, not favorites
w.Styler(func(s *styles.Style) {
s.Grow.Set(0, 1)
s.Min.X.Ch(25)
s.Overflow.X = styles.OverflowHidden
})
w.SetSlice(&SystemSettings.FavPaths)
w.OnSelect(func(e events.Event) {
fp.favoritesSelect(w.SelectedIndex)
})
w.Updater(func() {
w.ResetSelectedIndexes()
})
})
tree.AddAt(p, "files", func(w *Table) {
fp.filesTable = w
w.SetReadOnly(true)
w.SetSlice(&fp.files)
w.SelectedField = "Name"
w.SelectedValue = fp.selectedFilename
if SystemSettings.FilePickerSort != "" {
w.setSortFieldName(SystemSettings.FilePickerSort)
}
w.TableStyler = func(w Widget, s *styles.Style, row, col int) {
fn := fp.files[row].Name
ext := strings.ToLower(filepath.Ext(fn))
if _, has := fp.extensionMap[ext]; has {
s.Color = colors.Scheme.Primary.Base
} else {
s.Color = colors.Scheme.OnSurface
}
}
w.Styler(func(s *styles.Style) {
s.Cursor = cursors.Pointer
})
w.OnSelect(func(e events.Event) {
fp.fileSelect(w.SelectedIndex)
})
w.OnDoubleClick(func(e events.Event) {
if w.clickSelectEvent(e) {
if !fp.selectFile() {
e.SetHandled() // don't pass along; keep dialog open
} else {
fp.Scene.sendKey(keymap.Accept, e) // activates Ok button code
}
}
})
w.ContextMenus = nil
w.AddContextMenu(func(m *Scene) {
open := NewButton(m).SetText("Open").SetIcon(icons.Open)
open.SetTooltip("Open the selected file using the default app")
open.OnClick(func(e events.Event) {
TheApp.OpenURL("file://" + fp.SelectedFile())
})
if TheApp.Platform() == system.Web {
open.SetText("Download").SetIcon(icons.Download).SetTooltip("Download this file to your device")
}
NewSeparator(m)
NewButton(m).SetText("Duplicate").SetIcon(icons.FileCopy).
SetTooltip("Make a copy of the selected file").
OnClick(func(e events.Event) {
fn := fp.files[w.SelectedIndex]
fn.Duplicate()
fp.updateFilesEvent()
})
tip := "Delete moves the selected file to the trash / recycling bin"
if TheApp.Platform().IsMobile() {
tip = "Delete deletes the selected file"
}
NewButton(m).SetText("Delete").SetIcon(icons.Delete).
SetTooltip(tip).
OnClick(func(e events.Event) {
fn := fp.files[w.SelectedIndex]
fb := NewSoloFuncButton(w).SetFunc(fn.Delete).SetConfirm(true).SetAfterFunc(fp.updateFilesEvent)
fb.SetTooltip(tip)
fb.CallFunc()
})
NewButton(m).SetText("Rename").SetIcon(icons.EditNote).
SetTooltip("Rename the selected file").
OnClick(func(e events.Event) {
fn := fp.files[w.SelectedIndex]
NewSoloFuncButton(w).SetFunc(fn.Rename).SetAfterFunc(fp.updateFilesEvent).CallFunc()
})
NewButton(m).SetText("Info").SetIcon(icons.Info).
SetTooltip("View information about the selected file").
OnClick(func(e events.Event) {
fn := fp.files[w.SelectedIndex]
d := NewBody("Info: " + fn.Name)
NewForm(d).SetStruct(&fn).SetReadOnly(true)
d.AddOKOnly().RunFullDialog(w)
})
NewSeparator(m)
NewFuncButton(m).SetFunc(fp.newFolder).SetIcon(icons.CreateNewFolder)
})
// w.Updater(func() {})
})
}
func (fp *FilePicker) makeSelectedRow(selected *tree.Plan) {
tree.AddAt(selected, "file-text", func(w *Text) {
w.SetText("File: ")
w.SetTooltip("Enter file name here (or select from list above)")
w.Styler(func(s *styles.Style) {
s.SetTextWrap(false)
})
})
tree.AddAt(selected, "file", func(w *TextField) {
fp.selectField = w
w.SetText(fp.selectedFilename)
w.SetTooltip(fmt.Sprintf("Enter the file name. Special keys: up/down to move selection; %s or %s to go up to parent folder; %s or %s or %s or %s to select current file (if directory, goes into it, if file, selects and closes); %s or %s for prev / next history item; %s return to this field", keymap.WordLeft.Label(), keymap.Jump.Label(), keymap.SelectMode.Label(), keymap.Insert.Label(), keymap.InsertAfter.Label(), keymap.Open.Label(), keymap.HistPrev.Label(), keymap.HistNext.Label(), keymap.Search.Label()))
w.SetCompleter(fp, fp.fileComplete, fp.fileCompleteEdit)
w.Styler(func(s *styles.Style) {
s.Min.X.Ch(60)
s.Max.X.Zero()
s.Grow.Set(1, 0)
})
w.OnChange(func(e events.Event) {
fp.setSelectedFile(w.Text())
})
w.OnKeyChord(func(e events.Event) {
kf := keymap.Of(e.KeyChord())
if kf == keymap.Accept {
fp.setSelectedFile(w.Text())
}
})
w.StartFocus()
w.Updater(func() {
w.SetText(fp.selectedFilename)
})
})
tree.AddAt(selected, "extension-text", func(w *Text) {
w.SetText("Extension(s):").SetTooltip("target extension(s) to highlight; if multiple, separate with commas, and include the . at the start")
w.Styler(func(s *styles.Style) {
s.SetTextWrap(false)
})
})
tree.AddAt(selected, "extension", func(w *TextField) {
fp.extensionField = w
w.SetText(fp.extensions)
w.OnChange(func(e events.Event) {
fp.SetExtensions(w.Text()).Update()
})
})
}
func (fp *FilePicker) configWatcher() error {
if fp.watcher != nil {
return nil
}
var err error
fp.watcher, err = fsnotify.NewWatcher()
return err
}
func (fp *FilePicker) watchWatcher() {
if fp.watcher == nil || fp.watcher.Events == nil {
return
}
if fp.doneWatcher != nil {
return
}
fp.doneWatcher = make(chan bool)
go func() {
watch := fp.watcher
done := fp.doneWatcher
for {
select {
case <-done:
return
case event := <-watch.Events:
switch {
case event.Op&fsnotify.Create == fsnotify.Create ||
event.Op&fsnotify.Remove == fsnotify.Remove ||
event.Op&fsnotify.Rename == fsnotify.Rename:
fp.Update()
}
case err := <-watch.Errors:
_ = err
}
}
}()
}
// updateFilesEvent updates the list of files and other views for the current path.
func (fp *FilePicker) updateFilesEvent() { //types:add
fp.readFiles()
fp.Update()
// sf := fv.SelectField()
// sf.SetFocusEvent()
}
func (fp *FilePicker) readFiles() {
effpath, err := filepath.EvalSymlinks(fp.directory)
if err != nil {
log.Printf("FilePicker Path: %v could not be opened -- error: %v\n", effpath, err)
return
}
_, err = os.Lstat(effpath)
if err != nil {
log.Printf("FilePicker Path: %v could not be opened -- error: %v\n", effpath, err)
return
}
fp.files = make([]*fileinfo.FileInfo, 0, 1000)
filepath.Walk(effpath, func(path string, info fs.FileInfo, err error) error {
if err != nil {
emsg := fmt.Sprintf("Path %q: Error: %v", effpath, err)
// if fv.Scene != nil {
// PromptDialog(fv, DlgOpts{Title: "FilePicker UpdateFiles", emsg, Ok: true, Cancel: false}, nil)
// } else {
log.Printf("FilePicker error: %v\n", emsg)
// }
return nil // ignore
}
if path == effpath { // proceed..
return nil
}
fi, ferr := fileinfo.NewFileInfo(path)
keep := ferr == nil
if fp.Filterer != nil {
keep = fp.Filterer(fp, fi)
}
if keep {
fp.files = append(fp.files, fi)
}
if info.IsDir() {
return filepath.SkipDir
}
return nil
})
}
// updateFavorites updates list of files and other views for current path
func (fp *FilePicker) updateFavorites() {
sv := fp.favoritesTable
sv.Update()
}
// addPathToFavorites adds the current path to favorites
func (fp *FilePicker) addPathToFavorites() { //types:add
dp := fp.directory
if dp == "" {
return
}
_, fnm := filepath.Split(dp)
hd, _ := homedir.Dir()
hd += string(filepath.Separator)
if strings.HasPrefix(dp, hd) {
dp = filepath.Join("~", strings.TrimPrefix(dp, hd))
}
if fnm == "" {
fnm = dp
}
if _, found := SystemSettings.FavPaths.findPath(dp); found {
MessageSnackbar(fp, "Error: path is already on the favorites list")
return
}
fi := favoritePathItem{"folder", fnm, dp}
SystemSettings.FavPaths = append(SystemSettings.FavPaths, fi)
ErrorSnackbar(fp, SaveSettings(SystemSettings), "Error saving settings")
// fv.FileSig.Emit(fv.This, int64(FilePickerFavAdded), fi)
fp.updateFavorites()
}
// directoryUp moves up one directory in the path
func (fp *FilePicker) directoryUp() { //types:add
pdr := filepath.Dir(fp.directory)
if pdr == "" {
return
}
fp.directory = pdr
fp.updateFilesEvent()
}
// newFolder creates a new folder with the given name in the current directory.
func (fp *FilePicker) newFolder(name string) error { //types:add
dp := fp.directory
if dp == "" {
return nil
}
np := filepath.Join(dp, name)
err := os.MkdirAll(np, 0775)
if err != nil {
return err
}
fp.updateFilesEvent()
return nil
}
// setSelectedFile sets the currently selected file to the given name, sends
// a selection event, and updates the selection in the table.
func (fp *FilePicker) setSelectedFile(file string) {
fp.selectedFilename = file
sv := fp.filesTable
ef := fp.extensionField
exts := ef.Text()
if !sv.selectFieldValue("Name", fp.selectedFilename) { // not found
extl := strings.Split(exts, ",")
if len(extl) == 1 {
if !strings.HasSuffix(fp.selectedFilename, extl[0]) {
fp.selectedFilename += extl[0]
}
}
}
fp.selectedIndex = sv.SelectedIndex
sf := fp.selectField
sf.SetText(fp.selectedFilename) // make sure
fp.Send(events.Select) // receiver needs to get selectedFile
}
// fileSelect updates the selection with the given selected file index and
// sends a select event.
func (fp *FilePicker) fileSelect(idx int) {
if idx < 0 {
return
}
fp.saveSortSettings()
fi := fp.files[idx]
fp.selectedIndex = idx
fp.selectedFilename = fi.Name
sf := fp.selectField
sf.SetText(fp.selectedFilename)
fp.Send(events.Select)
}
// SetExtensions sets the [FilePicker.Extensions] to the given comma separated
// list of file extensions, which each must start with a dot (".").
func (fp *FilePicker) SetExtensions(ext string) *FilePicker {
if ext == "" {
if fp.selectedFilename != "" {
ext = strings.ToLower(filepath.Ext(fp.selectedFilename))
}
}
fp.extensions = ext
exts := strings.Split(fp.extensions, ",")
fp.extensionMap = make(map[string]string, len(exts))
for _, ex := range exts {
ex = strings.TrimSpace(ex)
if len(ex) == 0 {
continue
}
if ex[0] != '.' {
ex = "." + ex
}
fp.extensionMap[strings.ToLower(ex)] = ex
}
return fp
}
// favoritesSelect selects a favorite path and goes there
func (fp *FilePicker) favoritesSelect(idx int) {
if idx < 0 || idx >= len(SystemSettings.FavPaths) {
return
}
fi := SystemSettings.FavPaths[idx]
fp.directory, _ = homedir.Expand(fi.Path)
fp.updateFilesEvent()
}
// saveSortSettings saves current sorting preferences
func (fp *FilePicker) saveSortSettings() {
sv := fp.filesTable
if sv == nil {
return
}
SystemSettings.FilePickerSort = sv.sortFieldName()
// fmt.Printf("sort: %v\n", Settings.FilePickerSort)
ErrorSnackbar(fp, SaveSettings(SystemSettings), "Error saving settings")
}
// fileComplete finds the possible completions for the file field
func (fp *FilePicker) fileComplete(data any, text string, posLine, posChar int) (md complete.Matches) {
md.Seed = complete.SeedPath(text)
var files = []string{}
for _, f := range fp.files {
files = append(files, f.Name)
}
if len(md.Seed) > 0 { // return all directories
files = complete.MatchSeedString(files, md.Seed)
}
for _, d := range files {
m := complete.Completion{Text: d}
md.Matches = append(md.Matches, m)
}
return md
}
// fileCompleteEdit is the editing function called when inserting the completion selection in the file field
func (fp *FilePicker) fileCompleteEdit(data any, text string, cursorPos int, c complete.Completion, seed string) (ed complete.Edit) {
ed = complete.EditWord(text, cursorPos, c.Text, seed)
return ed
}
// editRecentPaths displays a dialog allowing the user to
// edit the recent paths list.
func (fp *FilePicker) editRecentPaths() {
d := NewBody("Recent file paths")
NewText(d).SetType(TextSupporting).SetText("You can delete paths you no longer use")
NewList(d).SetSlice(&recentPaths)
d.AddBottomBar(func(bar *Frame) {
d.AddCancel(bar)
d.AddOK(bar).OnClick(func(e events.Event) {
saveRecentPaths()
fp.Update()
})
})
d.RunDialog(fp)
}
// Filename is used to specify an file path.
// It results in a [FileButton] [Value].
type Filename string
// FileButton represents a [Filename] value with a button
// that opens a [FilePicker].
type FileButton struct {
Button
Filename string
// Extensions are the target file extensions for the file picker.
Extensions string
}
func (fb *FileButton) WidgetValue() any { return &fb.Filename }
func (fb *FileButton) OnBind(value any, tags reflect.StructTag) {
if ext, ok := tags.Lookup("extension"); ok {
fb.SetExtensions(ext)
}
}
func (fb *FileButton) Init() {
fb.Button.Init()
fb.SetType(ButtonTonal).SetIcon(icons.File)
fb.Updater(func() {
if fb.Filename == "" {
fb.SetText("Select file")
} else {
fb.SetText(elide.Middle(fb.Filename, 38))
}
})
var fp *FilePicker
InitValueButton(fb, false, func(d *Body) {
d.Title = "Select file"
d.DeleteChildByName("body-title") // file picker has its own title
fp = NewFilePicker(d).SetFilename(fb.Filename).SetExtensions(fb.Extensions)
fb.setFlag(true, widgetValueNewWindow)
d.AddTopBar(func(bar *Frame) {
NewToolbar(bar).Maker(fp.MakeToolbar)
})
}, func() {
fb.Filename = fp.SelectedFile()
})
}
func (fb *FileButton) WidgetTooltip(pos image.Point) (string, image.Point) {
if fb.Filename == "" {
return fb.Tooltip, fb.DefaultTooltipPos()
}
fnm := "(" + fb.Filename + ")"
if fb.Tooltip == "" {
return fnm, fb.DefaultTooltipPos()
}
return fnm + " " + fb.Tooltip, fb.DefaultTooltipPos()
}
// 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 (
"cogentcore.org/core/enums"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/tree"
)
// StateIs returns whether the widget has the given [states.States] flag set.
func (wb *WidgetBase) StateIs(state states.States) bool {
return wb.Styles.State.HasFlag(state)
}
// AbilityIs returns whether the widget has the given [abilities.Abilities] flag set.
func (wb *WidgetBase) AbilityIs(able abilities.Abilities) bool {
return wb.Styles.Abilities.HasFlag(able)
}
// SetState sets the given [states.State] flags to the given value.
func (wb *WidgetBase) SetState(on bool, state ...states.States) *WidgetBase {
bfs := make([]enums.BitFlag, len(state))
for i, st := range state {
bfs[i] = st
}
wb.Styles.State.SetFlag(on, bfs...)
return wb
}
// SetSelected sets the [states.Selected] flag to given value for the entire Widget
// and calls [WidgetBase.Restyle] to apply any resultant style changes.
func (wb *WidgetBase) SetSelected(sel bool) *WidgetBase {
wb.SetState(sel, states.Selected)
wb.Restyle()
return wb
}
// CanFocus returns whether this node can receive keyboard focus.
func (wb *WidgetBase) CanFocus() bool {
return wb.Styles.Abilities.HasFlag(abilities.Focusable)
}
// SetEnabled sets the [states.Disabled] flag to the opposite of the given value.
func (wb *WidgetBase) SetEnabled(enabled bool) *WidgetBase {
return wb.SetState(!enabled, states.Disabled)
}
// IsDisabled returns whether this node is flagged as [states.Disabled].
// If so, behave and style appropriately.
func (wb *WidgetBase) IsDisabled() bool {
return wb.StateIs(states.Disabled)
}
// IsReadOnly returns whether this widget is flagged as either [states.ReadOnly] or [states.Disabled].
func (wb *WidgetBase) IsReadOnly() bool {
return wb.Styles.IsReadOnly()
}
// SetReadOnly sets the [states.ReadOnly] flag to the given value.
func (wb *WidgetBase) SetReadOnly(ro bool) *WidgetBase {
return wb.SetState(ro, states.ReadOnly)
}
// HasStateWithin returns whether this widget or any
// of its children have the given state flag.
func (wb *WidgetBase) HasStateWithin(state states.States) bool {
got := false
wb.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
if cwb.StateIs(state) {
got = true
return tree.Break
}
return tree.Continue
})
return got
}
// 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 core
import (
"fmt"
"reflect"
"slices"
"strings"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/styles"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
// Form represents a struct with rows of field names and editable values.
type Form struct {
Frame
// Struct is the pointer to the struct that we are viewing.
Struct any
// Inline is whether to display the form in one line.
Inline bool
// structFields are the fields of the current struct.
structFields []*structField
// isShouldDisplayer is whether the struct implements [ShouldDisplayer], which results
// in additional updating being done at certain points.
isShouldDisplayer bool
}
// structField represents the values of one struct field being viewed.
type structField struct {
path string
field reflect.StructField
value, parent reflect.Value
}
// NoSentenceCaseFor indicates to not transform field names in
// [Form]s into "Sentence case" for types whose full,
// package-path-qualified name contains any of these strings.
// For example, this can be used to disable sentence casing
// for types with scientific abbreviations in field names,
// which are more readable when not sentence cased. However,
// this should not be needed in most circumstances.
var NoSentenceCaseFor []string
// noSentenceCaseForType returns whether the given fully
// package-path-qualified name contains anything in the
// [NoSentenceCaseFor] list.
func noSentenceCaseForType(tnm string) bool {
return slices.ContainsFunc(NoSentenceCaseFor, func(s string) bool {
return strings.Contains(tnm, s)
})
}
// ShouldDisplayer is an interface that determines whether a named field
// should be displayed in [Form].
type ShouldDisplayer interface {
// ShouldDisplay returns whether the given named field should be displayed.
ShouldDisplay(field string) bool
}
func (fm *Form) WidgetValue() any { return &fm.Struct }
func (fm *Form) getStructFields() {
var fields []*structField
shouldShow := func(parent reflect.Value, field reflect.StructField) bool {
if field.Tag.Get("display") == "-" {
return false
}
if ss, ok := reflectx.UnderlyingPointer(parent).Interface().(ShouldDisplayer); ok {
fm.isShouldDisplayer = true
if !ss.ShouldDisplay(field.Name) {
return false
}
}
return true
}
reflectx.WalkFields(reflectx.Underlying(reflect.ValueOf(fm.Struct)),
func(parent reflect.Value, field reflect.StructField, value reflect.Value) bool {
return shouldShow(parent, field)
},
func(parent reflect.Value, parentField *reflect.StructField, field reflect.StructField, value reflect.Value) {
if field.Tag.Get("display") == "add-fields" && field.Type.Kind() == reflect.Struct {
reflectx.WalkFields(value,
func(parent reflect.Value, sfield reflect.StructField, value reflect.Value) bool {
return shouldShow(parent, sfield)
},
func(parent reflect.Value, parentField *reflect.StructField, sfield reflect.StructField, value reflect.Value) {
// if our parent field is read only, we must also be
if field.Tag.Get("edit") == "-" && sfield.Tag.Get("edit") == "" {
sfield.Tag += ` edit:"-"`
}
fields = append(fields, &structField{path: field.Name + " • " + sfield.Name, field: sfield, value: value, parent: parent})
})
} else {
fields = append(fields, &structField{path: field.Name, field: field, value: value, parent: parent})
}
})
fm.structFields = fields
}
func (fm *Form) Init() {
fm.Frame.Init()
fm.Styler(func(s *styles.Style) {
s.Align.Items = styles.Center
if fm.Inline {
return
}
s.Display = styles.Grid
if fm.SizeClass() == SizeCompact {
s.Columns = 1
} else {
s.Columns = 2
}
})
fm.Maker(func(p *tree.Plan) {
if reflectx.IsNil(reflect.ValueOf(fm.Struct)) {
return
}
fm.getStructFields()
sc := true
if len(NoSentenceCaseFor) > 0 {
sc = !noSentenceCaseForType(types.TypeNameValue(fm.Struct))
}
for i, f := range fm.structFields {
label := f.path
if sc {
label = strcase.ToSentence(label)
}
if lt, ok := f.field.Tag.Lookup("label"); ok {
label = lt
}
labnm := fmt.Sprintf("label-%s", f.path)
// we must have a different name for different types
// so that the widget can be re-made for a new type
typnm := reflectx.ShortTypeName(f.field.Type)
// we must have a different name for invalid values
// so that the widget can be re-made for valid values
if !reflectx.Underlying(f.value).IsValid() {
typnm = "invalid"
}
// We must have a different name for different indexes so that the index
// is always guaranteed to be accurate, which is required since we use it
// as the ground truth everywhere. The index could otherwise become invalid,
// such as when a ShouldDisplayer condition is newly satisfied
// (see https://github.com/cogentcore/core/issues/1096).
valnm := fmt.Sprintf("value-%s-%s-%d", f.path, typnm, i)
readOnlyTag := f.field.Tag.Get("edit") == "-"
def, hasDef := f.field.Tag.Lookup("default")
var labelWidget *Text
var valueWidget Value
tree.AddAt(p, labnm, func(w *Text) {
labelWidget = w
w.Styler(func(s *styles.Style) {
s.SetTextWrap(false)
})
// TODO: technically we should recompute doc, readOnlyTag,
// def, hasDef, etc every time, as this is not fully robust
// (see https://github.com/cogentcore/core/issues/1098).
doc, _ := types.GetDoc(f.value, f.parent, f.field, label)
w.SetTooltip(doc)
if hasDef {
w.SetTooltip("(Default: " + def + ") " + w.Tooltip)
var isDef bool
w.Styler(func(s *styles.Style) {
f := fm.structFields[i]
isDef = reflectx.ValueIsDefault(f.value, def)
dcr := "(Double click to reset to default) "
if !isDef {
s.Color = colors.Scheme.Primary.Base
s.Cursor = cursors.Poof
if !strings.HasPrefix(w.Tooltip, dcr) {
w.SetTooltip(dcr + w.Tooltip)
}
} else {
w.SetTooltip(strings.TrimPrefix(w.Tooltip, dcr))
}
})
w.OnDoubleClick(func(e events.Event) {
f := fm.structFields[i]
if isDef {
return
}
e.SetHandled()
err := reflectx.SetFromDefaultTag(f.value, def)
if err != nil {
ErrorSnackbar(w, err, "Error setting default value")
} else {
w.Update()
valueWidget.AsWidget().Update()
valueWidget.AsWidget().SendChange(e)
}
})
}
w.Updater(func() {
w.SetText(label)
})
})
tree.AddNew(p, valnm, func() Value {
return NewValue(reflectx.UnderlyingPointer(f.value).Interface(), f.field.Tag)
}, func(w Value) {
valueWidget = w
wb := w.AsWidget()
doc, _ := types.GetDoc(f.value, f.parent, f.field, label)
// InitValueButton may set starting wb.Tooltip in Init
if wb.Tooltip == "" {
wb.SetTooltip(doc)
} else if doc == "" {
wb.SetTooltip(wb.Tooltip)
} else {
wb.SetTooltip(wb.Tooltip + " " + doc)
}
if hasDef {
wb.SetTooltip("(Default: " + def + ") " + wb.Tooltip)
}
wb.OnInput(func(e events.Event) {
f := fm.structFields[i]
fm.Send(events.Input, e)
if f.field.Tag.Get("immediate") == "+" {
wb.SendChange(e)
}
})
if !fm.IsReadOnly() && !readOnlyTag {
wb.OnChange(func(e events.Event) {
fm.SendChange(e)
if hasDef {
labelWidget.Update()
}
if fm.isShouldDisplayer {
fm.Update()
}
})
}
wb.Updater(func() {
wb.SetReadOnly(fm.IsReadOnly() || readOnlyTag)
if i < len(fm.structFields) {
f := fm.structFields[i]
Bind(reflectx.UnderlyingPointer(f.value).Interface(), w)
vc := joinValueTitle(fm.ValueTitle, label)
if vc != wb.ValueTitle {
wb.ValueTitle = vc + " (" + wb.ValueTitle + ")"
}
}
})
})
}
})
}
// 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 core
import (
"log/slog"
"time"
"unicode"
"cogentcore.org/core/base/labels"
"cogentcore.org/core/events"
"cogentcore.org/core/keymap"
"cogentcore.org/core/math32"
"cogentcore.org/core/parse/complete"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/tree"
)
// Frame is the primary node type responsible for organizing the sizes
// and positions of child widgets. It also renders the standard box model.
// All collections of widgets should generally be contained within a [Frame];
// otherwise, the parent widget must take over responsibility for positioning.
// Frames automatically can add scrollbars depending on the [styles.Style.Overflow].
//
// For a [styles.Grid] frame, the [styles.Style.Columns] property should
// generally be set to the desired number of columns, from which the number of rows
// is computed; otherwise, it uses the square root of number of
// elements.
type Frame struct {
WidgetBase
// StackTop, for a [styles.Stacked] frame, is the index of the node to use
// as the top of the stack. Only the node at this index is rendered; if it is
// not a valid index, nothing is rendered.
StackTop int
// LayoutStackTopOnly is whether to only layout the top widget
// (specified by [Frame.StackTop]) for a [styles.Stacked] frame.
// This is appropriate for widgets such as [Tabs], which do a full
// redraw on stack changes, but not for widgets such as [Switch]es
// which don't.
LayoutStackTopOnly bool
// layout contains implementation state info for doing layout
layout layoutState
// HasScroll is whether scrollbars exist for each dimension.
HasScroll [2]bool `edit:"-" copier:"-" json:"-" xml:"-" set:"-"`
// scrolls are the scroll bars, which are fully managed as needed.
scrolls [2]*Slider
// accumulated name to search for when keys are typed
focusName string
// time of last focus name event; for timeout
focusNameTime time.Time
// last element focused on; used as a starting point if name is the same
focusNameLast tree.Node
}
func (fr *Frame) Init() {
fr.WidgetBase.Init()
fr.FinalStyler(func(s *styles.Style) {
// we only enable, not disable, since some other widget like Slider may want to enable
if s.Overflow.X == styles.OverflowAuto || s.Overflow.Y == styles.OverflowAuto {
s.SetAbilities(true, abilities.Scrollable, abilities.Slideable)
}
})
fr.OnFinal(events.KeyChord, func(e events.Event) {
kf := keymap.Of(e.KeyChord())
if DebugSettings.KeyEventTrace {
slog.Info("Layout KeyInput", "widget", fr, "keyFunction", kf)
}
if kf == keymap.Abort {
if fr.Scene.Stage.closePopupAndBelow() {
e.SetHandled()
}
return
}
em := fr.Events()
if em == nil {
return
}
grid := fr.Styles.Display == styles.Grid
if fr.Styles.Direction == styles.Row || grid {
switch kf {
case keymap.MoveRight:
if fr.focusNextChild(false) {
e.SetHandled()
}
return
case keymap.MoveLeft:
if fr.focusPreviousChild(false) {
e.SetHandled()
}
return
}
}
if fr.Styles.Direction == styles.Column || grid {
switch kf {
case keymap.MoveDown:
if fr.focusNextChild(true) {
e.SetHandled()
}
return
case keymap.MoveUp:
if fr.focusPreviousChild(true) {
e.SetHandled()
}
return
case keymap.PageDown:
proc := false
for st := 0; st < SystemSettings.LayoutPageSteps; st++ {
if !fr.focusNextChild(true) {
break
}
proc = true
}
if proc {
e.SetHandled()
}
return
case keymap.PageUp:
proc := false
for st := 0; st < SystemSettings.LayoutPageSteps; st++ {
if !fr.focusPreviousChild(true) {
break
}
proc = true
}
if proc {
e.SetHandled()
}
return
}
}
fr.focusOnName(e)
})
fr.On(events.Scroll, func(e events.Event) {
fr.scrollDelta(e)
})
// We treat slide events on frames as scroll events.
fr.On(events.SlideMove, func(e events.Event) {
// We must negate the delta for "natural" scrolling behavior.
del := math32.FromPoint(e.PrevDelta()).MulScalar(-0.034)
fr.scrollDelta(events.NewScroll(e.WindowPos(), del, e.Modifiers()))
})
fr.On(events.SlideStop, func(e events.Event) {
// If we have enough velocity, we continue scrolling over the
// next second in a goroutine while slowly decelerating for a
// smoother experience.
vel := math32.FromPoint(e.StartDelta()).DivScalar(1.5 * float32(e.SinceStart().Milliseconds())).Negate()
if math32.Abs(vel.X) < 1 && math32.Abs(vel.Y) < 1 {
return
}
go func() {
i := 0
tick := time.NewTicker(time.Second / 60)
for range tick.C {
fr.AsyncLock()
fr.scrollDelta(events.NewScroll(e.WindowPos(), vel, e.Modifiers()))
fr.AsyncUnlock()
vel.SetMulScalar(0.95)
i++
if i > 120 {
tick.Stop()
break
}
}
}()
})
}
func (fr *Frame) Style() {
fr.WidgetBase.Style()
for d := math32.X; d <= math32.Y; d++ {
if fr.HasScroll[d] && fr.scrolls[d] != nil {
fr.scrolls[d].Style()
}
}
}
func (fr *Frame) Destroy() {
for d := math32.X; d <= math32.Y; d++ {
fr.deleteScroll(d)
}
fr.WidgetBase.Destroy()
}
// deleteScroll deletes scrollbar along given dimesion.
func (fr *Frame) deleteScroll(d math32.Dims) {
if fr.scrolls[d] == nil {
return
}
sb := fr.scrolls[d]
sb.This.Destroy()
fr.scrolls[d] = nil
}
func (fr *Frame) RenderChildren() {
if fr.Styles.Display == styles.Stacked {
wb := fr.StackTopWidget()
if wb != nil {
wb.This.(Widget).RenderWidget()
}
return
}
fr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cw.RenderWidget()
return tree.Continue
})
}
func (fr *Frame) RenderWidget() {
if fr.PushBounds() {
fr.This.(Widget).Render()
fr.RenderChildren()
fr.renderParts()
fr.RenderScrolls()
fr.PopBounds()
}
}
// childWithFocus returns a direct child of this layout that either is the
// current window focus item, or contains that focus item (along with its
// index) -- nil, -1 if none.
func (fr *Frame) childWithFocus() (Widget, int) {
em := fr.Events()
if em == nil {
return nil, -1
}
var foc Widget
focIndex := -1
fr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
if cwb.ContainsFocus() {
foc = cw
focIndex = i
return tree.Break
}
return tree.Continue
})
return foc, focIndex
}
// focusNextChild attempts to move the focus into the next layout child
// (with wraparound to start); returns true if successful.
// if updn is true, then for Grid layouts, it moves down to next row
// instead of just the sequentially next item.
func (fr *Frame) focusNextChild(updn bool) bool {
sz := len(fr.Children)
if sz <= 1 {
return false
}
foc, idx := fr.childWithFocus()
if foc == nil {
// fmt.Println("no child foc")
return false
}
em := fr.Events()
if em == nil {
return false
}
cur := em.focus
nxti := idx + 1
if fr.Styles.Display == styles.Grid && updn {
nxti = idx + fr.Styles.Columns
}
did := false
if nxti < sz {
nx := fr.Child(nxti).(Widget)
did = em.focusOnOrNext(nx)
} else {
nx := fr.Child(0).(Widget)
did = em.focusOnOrNext(nx)
}
if !did || em.focus == cur {
return false
}
return true
}
// focusPreviousChild attempts to move the focus into the previous layout child
// (with wraparound to end); returns true if successful.
// If updn is true, then for Grid layouts, it moves up to next row
// instead of just the sequentially next item.
func (fr *Frame) focusPreviousChild(updn bool) bool {
sz := len(fr.Children)
if sz <= 1 {
return false
}
foc, idx := fr.childWithFocus()
if foc == nil {
return false
}
em := fr.Events()
if em == nil {
return false
}
cur := em.focus
nxti := idx - 1
if fr.Styles.Display == styles.Grid && updn {
nxti = idx - fr.Styles.Columns
}
did := false
if nxti >= 0 {
did = em.focusOnOrPrev(fr.Child(nxti).(Widget))
} else {
did = em.focusOnOrPrev(fr.Child(sz - 1).(Widget))
}
if !did || em.focus == cur {
return false
}
return true
}
// focusOnName processes key events to look for an element starting with given name
func (fr *Frame) focusOnName(e events.Event) bool {
kf := keymap.Of(e.KeyChord())
if DebugSettings.KeyEventTrace {
slog.Info("Layout FocusOnName", "widget", fr, "keyFunction", kf)
}
delay := e.Time().Sub(fr.focusNameTime)
fr.focusNameTime = e.Time()
if kf == keymap.FocusNext { // tab means go to next match -- don't worry about time
if fr.focusName == "" || delay > SystemSettings.LayoutFocusNameTabTime {
fr.focusName = ""
fr.focusNameLast = nil
return false
}
} else {
if delay > SystemSettings.LayoutFocusNameTimeout {
fr.focusName = ""
}
if !unicode.IsPrint(e.KeyRune()) || e.Modifiers() != 0 {
return false
}
sr := string(e.KeyRune())
if fr.focusName == sr {
// re-search same letter
} else {
fr.focusName += sr
fr.focusNameLast = nil // only use last if tabbing
}
}
// e.SetHandled()
// fmt.Printf("searching for: %v last: %v\n", ly.FocusName, ly.FocusNameLast)
focel := childByLabelCanFocus(fr, fr.focusName, fr.focusNameLast)
if focel != nil {
em := fr.Events()
if em != nil {
em.setFocus(focel.(Widget)) // this will also scroll by default!
}
fr.focusNameLast = focel
return true
} else {
if fr.focusNameLast == nil {
fr.focusName = "" // nothing being found
}
fr.focusNameLast = nil // start over
}
return false
}
// childByLabelCanFocus uses breadth-first search to find
// the first focusable element within the layout whose Label (using
// [ToLabel]) matches the given name using [complete.IsSeedMatching].
// If after is non-nil, it only finds after that element.
func childByLabelCanFocus(fr *Frame, name string, after tree.Node) tree.Node {
gotAfter := false
completions := []complete.Completion{}
fr.WalkDownBreadth(func(n tree.Node) bool {
if n == fr.This { // skip us
return tree.Continue
}
wb := AsWidget(n)
if wb == nil || !wb.CanFocus() { // don't go any further
return tree.Continue
}
if after != nil && !gotAfter {
if n == after {
gotAfter = true
}
return tree.Continue // skip to next
}
completions = append(completions, complete.Completion{
Text: labels.ToLabel(n),
Desc: n.AsTree().PathFrom(fr),
})
return tree.Continue
})
matches := complete.MatchSeedCompletion(completions, name)
if len(matches) > 0 {
return fr.FindPath(matches[0].Desc)
}
return nil
}
// Stretch and Space: spacing elements
// Stretch adds a stretchy element that grows to fill all
// available space. You can set [styles.Style.Grow] to change
// how much it grows relative to other growing elements.
// It does not render anything.
type Stretch struct {
WidgetBase
}
func (st *Stretch) Init() {
st.WidgetBase.Init()
st.Styler(func(s *styles.Style) {
s.RenderBox = false
s.Min.X.Ch(1)
s.Min.Y.Em(1)
s.Grow.Set(1, 1)
})
}
// Space is a fixed size blank space, with
// a default width of 1ch and a height of 1em.
// You can set [styles.Style.Min] to change its size.
// It does not render anything.
type Space struct {
WidgetBase
}
func (sp *Space) Init() {
sp.WidgetBase.Init()
sp.Styler(func(s *styles.Style) {
s.RenderBox = false
s.Min.X.Ch(1)
s.Min.Y.Em(1)
s.Padding.Zero()
s.Margin.Zero()
s.MaxBorder.Width.Zero()
s.Border.Width.Zero()
})
}
// 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 (
"log/slog"
"reflect"
"strings"
"unicode"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/types"
)
// CallFunc calls the given function in the context of the given widget,
// popping up a dialog to prompt for any arguments and show the return
// values of the function. It is a helper function that uses [NewSoloFuncButton]
// under the hood.
func CallFunc(ctx Widget, fun any) {
NewSoloFuncButton(ctx).SetFunc(fun).CallFunc()
}
// NewSoloFuncButton returns a standalone [FuncButton] with a fake parent
// with the given context for popping up any dialogs.
func NewSoloFuncButton(ctx Widget) *FuncButton {
return NewFuncButton(NewWidgetBase()).SetContext(ctx)
}
// FuncButton is a button that is set up to call a function when it
// is pressed, using a dialog to prompt the user for any arguments.
// Also, it automatically sets various properties of the button like
// the text and tooltip based on the properties of the function,
// using [reflect] and [types]. The function must be registered
// with [types] to get documentation information, but that is not
// required; add a `//types:add` comment directive and run `core generate`
// if you want tooltips. If the function is a method, both the method and
// its receiver type must be added to [types] to get documentation.
// The main function to call first is [FuncButton.SetFunc].
type FuncButton struct {
Button
// typesFunc is the [types.Func] associated with this button.
// This function can also be a method, but it must be
// converted to a [types.Func] first. It should typically
// be set using [FuncButton.SetFunc].
typesFunc *types.Func
// reflectFunc is the [reflect.Value] of the function or
// method associated with this button. It should typically
// bet set using [FuncButton.SetFunc].
reflectFunc reflect.Value
// Args are the [FuncArg] objects associated with the
// arguments of the function. They are automatically set in
// [FuncButton.SetFunc], but they can be customized to configure
// default values and other options.
Args []FuncArg `set:"-"`
// Returns are the [FuncArg] objects associated with the
// return values of the function. They are automatically
// set in [FuncButton.SetFunc], but they can be customized
// to configure options. The [FuncArg.Value]s are not set until
// the function is called, and are thus not typically applicable
// to access.
Returns []FuncArg `set:"-"`
// Confirm is whether to prompt the user for confirmation
// before calling the function.
Confirm bool
// ShowReturn is whether to display the return values of
// the function (and a success message if there are none).
// The way that the return values are shown is determined
// by ShowReturnAsDialog. Non-nil error return values will
// always be shown, even if ShowReturn is set to false.
ShowReturn bool
// ShowReturnAsDialog, if and only if ShowReturn is true,
// indicates to show the return values of the function in
// a dialog, instead of in a snackbar, as they are by default.
// If there are multiple return values from the function, or if
// one of them is a complex type (pointer, struct, slice,
// array, map), then ShowReturnAsDialog will
// automatically be set to true.
ShowReturnAsDialog bool
// NewWindow makes the return value dialog a NewWindow dialog.
NewWindow bool
// WarnUnadded is whether to log warnings when a function that
// has not been added to [types] is used. It is on by default and
// must be set before [FuncButton.SetFunc] is called for it to
// have any effect. Warnings are never logged for anonymous functions.
WarnUnadded bool `default:"true"`
// Context is used for opening dialogs if non-nil.
Context Widget
// AfterFunc is an optional function called after the func button
// function is executed.
AfterFunc func()
}
// FuncArg represents one argument or return value of a function
// in the context of a [FuncButton].
type FuncArg struct { //types:add -setters
// Name is the name of the argument or return value.
Name string
// Tag contains any tags associated with the argument or return value,
// which can be added programmatically to customize [Value] behavior.
Tag reflect.StructTag
// Value is the actual value of the function argument or return value.
// It can be modified when creating a [FuncButton] to set a default value.
Value any
}
func (fb *FuncButton) WidgetValue() any {
if !fb.reflectFunc.IsValid() {
return nil
}
return fb.reflectFunc.Interface()
}
func (fb *FuncButton) SetWidgetValue(value any) error {
fb.SetFunc(reflectx.Underlying(reflect.ValueOf(value)).Interface())
return nil
}
func (fb *FuncButton) OnBind(value any, tags reflect.StructTag) {
// If someone is viewing a function value, there is a good chance
// that it is not added to types (and that is out of their control)
// (eg: in the inspector), so we do not warn on unadded functions.
fb.SetWarnUnadded(false).SetType(ButtonTonal)
}
func (fb *FuncButton) Init() {
fb.Button.Init()
fb.WarnUnadded = true
fb.Styler(func(s *styles.Style) {
// If Disabled, these steps are unnecessary and we want the default NotAllowed cursor, so only check for ReadOnly.
if s.Is(states.ReadOnly) {
s.SetAbilities(false, abilities.Hoverable, abilities.Clickable, abilities.Activatable)
s.Cursor = cursors.None
}
})
fb.OnClick(func(e events.Event) {
if !fb.IsReadOnly() {
fb.CallFunc()
}
})
}
// SetText sets the [FuncButton.Text] and updates the tooltip to
// correspond to the new name.
func (fb *FuncButton) SetText(v string) *FuncButton {
ptext := fb.Text
fb.Text = v
if fb.typesFunc != nil && fb.Text != ptext && ptext != "" {
fb.typesFunc.Doc = types.FormatDoc(fb.typesFunc.Doc, ptext, fb.Text)
fb.SetTooltip(fb.typesFunc.Doc)
}
return fb
}
// SetFunc sets the function associated with the FuncButton to the
// given function or method value. For documentation information for
// the function to be obtained, it must be added to [types].
func (fb *FuncButton) SetFunc(fun any) *FuncButton {
fnm := types.FuncName(fun)
if fnm == "" {
return fb.SetText("None")
}
fnm = strings.ReplaceAll(fnm, "[...]", "") // remove any labeling for generics
// the "-fm" suffix indicates that it is a method
if strings.HasSuffix(fnm, "-fm") {
fnm = strings.TrimSuffix(fnm, "-fm")
// the last dot separates the function name
li := strings.LastIndex(fnm, ".")
metnm := fnm[li+1:]
typnm := fnm[:li]
// get rid of any parentheses and pointer receivers
// that may surround the type name
typnm = strings.ReplaceAll(typnm, "(*", "")
typnm = strings.TrimSuffix(typnm, ")")
gtyp := types.TypeByName(typnm)
var met *types.Method
if gtyp == nil {
if fb.WarnUnadded {
slog.Warn("core.FuncButton.SetFunc called with a method whose receiver type has not been added to types", "function", fnm)
}
met = &types.Method{Name: metnm}
} else {
for _, m := range gtyp.Methods {
if m.Name == metnm {
met = &m
break
}
}
if met == nil {
if fb.WarnUnadded {
slog.Warn("core.FuncButton.SetFunc called with a method that has not been added to types (even though the receiver type was, you still need to add the method itself)", "function", fnm)
}
met = &types.Method{Name: metnm}
}
}
return fb.setMethodImpl(met, reflect.ValueOf(fun))
}
if isAnonymousFunction(fnm) {
f := &types.Func{Name: fnm, Doc: "Anonymous function " + fnm}
return fb.setFuncImpl(f, reflect.ValueOf(fun))
}
f := types.FuncByName(fnm)
if f == nil {
if fb.WarnUnadded {
slog.Warn("core.FuncButton.SetFunc called with a function that has not been added to types", "function", fnm)
}
f = &types.Func{Name: fnm}
}
return fb.setFuncImpl(f, reflect.ValueOf(fun))
}
func isAnonymousFunction(fnm string) bool {
// FuncName.funcN indicates that a function was defined anonymously
funcN := len(fnm) > 0 && unicode.IsDigit(rune(fnm[len(fnm)-1])) && strings.Contains(fnm, ".func")
return funcN || fnm == "reflect.makeFuncStub" // used for anonymous functions in yaegi
}
// setFuncImpl is the underlying implementation of [FuncButton.SetFunc].
// It should typically not be used by end-user code.
func (fb *FuncButton) setFuncImpl(gfun *types.Func, rfun reflect.Value) *FuncButton {
fb.typesFunc = gfun
fb.reflectFunc = rfun
fb.setArgs()
fb.setReturns()
snm := fb.typesFunc.Name
// get name without package
li := strings.LastIndex(snm, ".")
isAnonymous := isAnonymousFunction(snm)
if snm == "reflect.makeFuncStub" { // used for anonymous functions in yaegi
snm = "Anonymous function"
li = -1
} else if isAnonymous {
snm = strings.TrimRightFunc(snm, func(r rune) bool {
return unicode.IsDigit(r) || r == '.'
})
snm = strings.TrimSuffix(snm, ".func")
// we cut at the second to last period (we want to keep the
// receiver / package for anonymous functions)
li = strings.LastIndex(snm[:strings.LastIndex(snm, ".")], ".")
}
if li >= 0 {
snm = snm[li+1:] // must also get rid of "."
// if we are a global function, we may have gone too far with the second to last period,
// so we go after the last slash if there still is one
if strings.Contains(snm, "/") {
snm = snm[strings.LastIndex(snm, "/")+1:]
}
}
snm = strings.Map(func(r rune) rune {
if r == '(' || r == ')' || r == '*' {
return -1
}
return r
}, snm)
txt := strcase.ToSentence(snm)
fb.SetText(txt)
// doc formatting interferes with anonymous functions
if !isAnonymous {
fb.typesFunc.Doc = types.FormatDoc(fb.typesFunc.Doc, snm, txt)
}
fb.SetTooltip(fb.typesFunc.Doc)
return fb
}
func (fb *FuncButton) goodContext() Widget {
ctx := fb.Context
if fb.Context == nil {
if fb.This == nil {
return nil
}
ctx = fb.This.(Widget)
}
return ctx
}
func (fb *FuncButton) callFuncShowReturns() {
if fb.AfterFunc != nil {
defer fb.AfterFunc()
}
if len(fb.Args) == 0 {
rets := fb.reflectFunc.Call(nil)
fb.showReturnsDialog(rets)
return
}
rargs := make([]reflect.Value, len(fb.Args))
for i, arg := range fb.Args {
rargs[i] = reflect.ValueOf(arg.Value)
}
rets := fb.reflectFunc.Call(rargs)
fb.showReturnsDialog(rets)
}
// confirmDialog runs the confirm dialog.
func (fb *FuncButton) confirmDialog() {
ctx := fb.goodContext()
d := NewBody(fb.Text + "?")
NewText(d).SetType(TextSupporting).SetText("Are you sure you want to run " + fb.Text + "? " + fb.Tooltip)
d.AddBottomBar(func(bar *Frame) {
d.AddCancel(bar)
d.AddOK(bar).SetText(fb.Text).OnClick(func(e events.Event) {
fb.callFuncShowReturns()
})
})
d.RunDialog(ctx)
}
// CallFunc calls the function associated with this button,
// prompting the user for any arguments.
func (fb *FuncButton) CallFunc() {
if !fb.reflectFunc.IsValid() {
return
}
ctx := fb.goodContext()
if len(fb.Args) == 0 {
if !fb.Confirm {
fb.callFuncShowReturns()
return
}
fb.confirmDialog()
return
}
d := NewBody(fb.Text)
NewText(d).SetType(TextSupporting).SetText(fb.Tooltip)
str := funcArgsToStruct(fb.Args)
sv := NewForm(d).SetStruct(str.Addr().Interface())
accept := func() {
for i := range fb.Args {
fb.Args[i].Value = str.Field(i).Interface()
}
fb.callFuncShowReturns()
}
// If there is a single value button, automatically
// open its dialog instead of this one
if len(fb.Args) == 1 {
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
accept()
})
bt.Scene = fb.Scene // we must use this scene for context
bt.Send(events.Click)
return
}
}
d.AddBottomBar(func(bar *Frame) {
d.AddCancel(bar)
d.AddOK(bar).SetText(fb.Text).OnClick(func(e events.Event) {
d.Close() // note: the other Close event happens too late!
accept()
})
})
d.RunDialog(ctx)
}
// funcArgsToStruct converts a slice of [FuncArg] objects
// to a new non-pointer struct [reflect.Value].
func funcArgsToStruct(args []FuncArg) reflect.Value {
fields := make([]reflect.StructField, len(args))
for i, arg := range args {
fields[i] = reflect.StructField{
Name: strcase.ToCamel(arg.Name),
Type: reflect.TypeOf(arg.Value),
Tag: arg.Tag,
}
}
typ := reflect.StructOf(fields)
value := reflect.New(typ).Elem()
for i, arg := range args {
value.Field(i).Set(reflect.ValueOf(arg.Value))
}
return value
}
// setMethodImpl is the underlying implementation of [FuncButton.SetFunc] for methods.
// It should typically not be used by end-user code.
func (fb *FuncButton) setMethodImpl(gmet *types.Method, rmet reflect.Value) *FuncButton {
return fb.setFuncImpl(&types.Func{
Name: gmet.Name,
Doc: gmet.Doc,
Directives: gmet.Directives,
Args: gmet.Args,
Returns: gmet.Returns,
}, rmet)
}
// showReturnsDialog runs a dialog displaying the given function return
// values for the function associated with the function button. It does
// nothing if [FuncButton.ShowReturn] is false.
func (fb *FuncButton) showReturnsDialog(rets []reflect.Value) {
if !fb.ShowReturn {
for _, ret := range rets {
if err, ok := ret.Interface().(error); ok && err != nil {
ErrorSnackbar(fb, err, fb.Text+" failed")
return
}
}
return
}
ctx := fb.goodContext()
if ctx == nil {
return
}
for i, ret := range rets {
fb.Returns[i].Value = ret.Interface()
}
main := "Result of " + fb.Text
if len(rets) == 0 {
main = fb.Text + " succeeded"
}
if !fb.ShowReturnAsDialog {
txt := main
if len(fb.Returns) > 0 {
txt += ": "
for i, ret := range fb.Returns {
txt += reflectx.ToString(ret.Value)
if i < len(fb.Returns)-1 {
txt += ", "
}
}
}
MessageSnackbar(ctx, txt)
return
}
d := NewBody(main)
NewText(d).SetType(TextSupporting).SetText(fb.Tooltip)
d.AddOKOnly()
str := funcArgsToStruct(fb.Returns)
sv := NewForm(d).SetStruct(str.Addr().Interface()).SetReadOnly(true)
// If there is a single value button, automatically
// open its dialog instead of this one
if len(fb.Returns) == 1 {
sv.UpdateWidget() // need to update first
bt := AsButton(sv.Child(1))
if bt != nil {
bt.Scene = fb.Scene // we must use this scene for context
bt.Send(events.Click)
return
}
}
if fb.NewWindow {
d.RunWindowDialog(ctx)
} else {
d.RunDialog(ctx)
}
}
// setArgs sets the appropriate [Value] objects for the
// arguments of the function associated with the function button.
func (fb *FuncButton) setArgs() {
narg := fb.reflectFunc.Type().NumIn()
fb.Args = make([]FuncArg, narg)
for i := range fb.Args {
typ := fb.reflectFunc.Type().In(i)
name := ""
if fb.typesFunc.Args != nil && len(fb.typesFunc.Args) > i {
name = fb.typesFunc.Args[i]
} else {
name = reflectx.NonPointerType(typ).Name()
}
fb.Args[i] = FuncArg{
Name: name,
Value: reflect.New(typ).Elem().Interface(),
}
}
}
// setReturns sets the appropriate [Value] objects for the
// return values of the function associated with the function
// button.
func (fb *FuncButton) setReturns() {
nret := fb.reflectFunc.Type().NumOut()
fb.Returns = make([]FuncArg, nret)
hasComplex := false
for i := range fb.Returns {
typ := fb.reflectFunc.Type().Out(i)
if !hasComplex {
k := typ.Kind()
if k == reflect.Pointer || k == reflect.Struct || k == reflect.Slice || k == reflect.Array || k == reflect.Map {
hasComplex = true
}
}
name := ""
if fb.typesFunc.Returns != nil && len(fb.typesFunc.Returns) > i {
name = fb.typesFunc.Returns[i]
} else {
name = reflectx.NonPointerType(typ).Name()
}
fb.Returns[i] = FuncArg{
Name: name,
Value: reflect.New(typ).Elem().Interface(),
}
}
if nret > 1 || hasComplex {
fb.ShowReturnAsDialog = true
}
}
// 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 (
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/units"
)
// Handle represents a draggable handle that can be used to
// control the size of an element. The [styles.Style.Direction]
// controls the direction in which the handle moves.
type Handle struct {
WidgetBase
// Min is the minimum value that the handle can go to
// (typically the lower bound of the dialog/splits)
Min float32
// Max is the maximum value that the handle can go to
// (typically the upper bound of the dialog/splits)
Max float32
// Pos is the current position of the handle on the
// scale of [Handle.Min] to [Handle.Max].
Pos float32
}
func (hl *Handle) Init() {
hl.WidgetBase.Init()
hl.Styler(func(s *styles.Style) {
s.SetAbilities(true, abilities.Clickable, abilities.Focusable, abilities.Hoverable, abilities.Slideable)
s.Border.Radius = styles.BorderRadiusFull
s.Background = colors.Scheme.OutlineVariant
})
hl.FinalStyler(func(s *styles.Style) {
if s.Direction == styles.Row {
s.Min.X.Dp(6)
s.Min.Y.Em(2)
s.Margin.SetHorizontal(units.Dp(6))
} else {
s.Min.X.Em(2)
s.Min.Y.Dp(6)
s.Margin.SetVertical(units.Dp(6))
}
if !hl.IsReadOnly() {
if s.Direction == styles.Row {
s.Cursor = cursors.ResizeEW
} else {
s.Cursor = cursors.ResizeNS
}
}
})
hl.On(events.SlideMove, func(e events.Event) {
e.SetHandled()
pos := hl.parentWidget().PointToRelPos(e.Pos())
hl.Pos = math32.FromPoint(pos).Dim(hl.Styles.Direction.Dim())
hl.SendChange(e)
})
}
// Value returns the value on a normalized scale of 0-1,
// based on [Handle.Pos], [Handle.Min], and [Handle.Max].
func (hl *Handle) Value() float32 {
return (hl.Pos - hl.Min) / (hl.Max - hl.Min)
}
// Copyright (c) 2024, 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 (
"bytes"
"encoding/xml"
"fmt"
"io"
"reflect"
"strconv"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/styles"
"cogentcore.org/core/svg"
"cogentcore.org/core/tree"
)
// ToHTML converts the given widget and all of its children to HTML.
// This is not guaranteed to be perfect HTML, and it should not be used as a
// replacement for a Cogent Core app. However, it is good enough to be used as
// a preview or for SEO purposes (see generatehtml.go).
func ToHTML(w Widget) ([]byte, error) {
b := &bytes.Buffer{}
e := xml.NewEncoder(b)
err := toHTML(w, e, b)
if err != nil {
return nil, err
}
return b.Bytes(), nil
}
// htmlElementNames is a map from widget [types.Type.IDName]s to HTML element
// names for cases in which those differ.
var htmlElementNames = map[string]string{
"body": "main", // we are typically placed in a different outer body
"frame": "div",
"text": "p",
"image": "img",
"icon": "svg",
"space": "div",
"separator": "hr",
"text-field": "input",
"spinner": "input",
"slider": "input",
"meter": "progress",
"chooser": "select",
"editor": "textarea",
"pre": "textarea",
"switches": "div",
"switch": "input",
"splits": "div",
"tabs": "div",
"tab": "button",
"tree": "div",
"page": "main",
}
func addAttr(se *xml.StartElement, name, value string) {
if value == "" {
return
}
se.Attr = append(se.Attr, xml.Attr{Name: xml.Name{Local: name}, Value: value})
}
// toHTML is the recursive implementation of [ToHTML].
func toHTML(w Widget, e *xml.Encoder, b *bytes.Buffer) error {
wb := w.AsWidget()
se := &xml.StartElement{}
typ := wb.NodeType()
idName := typ.IDName
se.Name.Local = idName
if tag, ok := wb.Property("tag").(string); ok {
se.Name.Local = tag
}
if se.Name.Local == "tree" { // trees not supported yet
return nil
}
if en, ok := htmlElementNames[se.Name.Local]; ok {
se.Name.Local = en
}
switch typ.Name {
case "cogentcore.org/cogent/canvas.Canvas", "cogentcore.org/cogent/code.Code":
se.Name.Local = "div"
}
if se.Name.Local == "textarea" {
wb.Styles.Min.X.Pw(95)
}
addAttr(se, "id", wb.Name)
if se.Name.Local != "img" { // images don't render yet
addAttr(se, "style", styles.ToCSS(&wb.Styles, idName, se.Name.Local))
}
handleChildren := true
switch w := w.(type) {
case *TextField:
addAttr(se, "type", "text")
addAttr(se, "value", w.text)
handleChildren = false
case *Spinner:
addAttr(se, "type", "number")
addAttr(se, "value", fmt.Sprintf("%g", w.Value))
handleChildren = false
case *Slider:
addAttr(se, "type", "range")
addAttr(se, "value", fmt.Sprintf("%g", w.Value))
handleChildren = false
case *Switch:
addAttr(se, "type", "checkbox")
addAttr(se, "value", strconv.FormatBool(w.IsChecked()))
}
if se.Name.Local == "textarea" {
addAttr(se, "rows", "10")
addAttr(se, "cols", "30")
}
err := e.EncodeToken(*se)
if err != nil {
return err
}
err = e.Flush()
if err != nil {
return err
}
writeSVG := func(s *svg.SVG) error {
s.PhysicalWidth = wb.Styles.Min.X
s.PhysicalHeight = wb.Styles.Min.Y
sb := &bytes.Buffer{}
err := s.WriteXML(sb, false)
if err != nil {
return err
}
io.Copy(b, sb)
return nil
}
switch w := w.(type) {
case *Text:
// We don't want any escaping of HTML-formatted text, so we write directly.
b.WriteString(w.Text)
case *Icon:
w.Styles.Min.Zero() // do not specify any size for the inner svg object
err := writeSVG(&w.svg)
if err != nil {
return err
}
case *SVG:
err := writeSVG(w.SVG)
if err != nil {
return err
}
}
if se.Name.Local == "textarea" && idName == "editor" {
b.WriteString(reflectx.Underlying(reflect.ValueOf(w)).FieldByName("Buffer").Interface().(fmt.Stringer).String())
}
if handleChildren {
wb.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
if idName == "switch" && cwb.Name == "stack" {
return tree.Continue
}
err = toHTML(cw, e, b)
if err != nil {
return tree.Break
}
return tree.Continue
})
if err != nil {
return err
}
}
err = e.EncodeToken(xml.EndElement{se.Name})
if err != nil {
return err
}
return e.Flush()
}
// 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 core
import (
"image"
"strings"
"cogentcore.org/core/base/errors"
"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"
)
// Icon renders an [icons.Icon].
// The rendered version is cached for the current size.
// Icons do not render a background or border independent of their SVG object.
// The size of an Icon is determined by the [styles.Font.Size] property.
type Icon struct {
WidgetBase
// Icon is the [icons.Icon] used to render the [Icon].
Icon icons.Icon
// prevIcon is the previously rendered icon.
prevIcon icons.Icon
// svg drawing of the icon
svg svg.SVG
}
func (ic *Icon) WidgetValue() any { return &ic.Icon }
func (ic *Icon) Init() {
ic.WidgetBase.Init()
ic.svg.Scale = 1
ic.Updater(ic.readIcon)
ic.Styler(func(s *styles.Style) {
s.Min.Set(units.Em(1))
})
ic.FinalStyler(func(s *styles.Style) {
if ic.svg.Root != nil {
ic.svg.Root.ViewBox.PreserveAspectRatio.SetFromStyle(s)
}
})
}
// readIcon reads the [Icon.Icon] if necessary.
func (ic *Icon) readIcon() {
if ic.Icon == ic.prevIcon {
// if nothing has changed, we don't need to read it
return
}
if !ic.Icon.IsSet() {
ic.svg.Pixels = nil
ic.svg.DeleteAll()
ic.prevIcon = ic.Icon
return
}
ic.svg.Config(2, 2)
err := ic.svg.ReadXML(strings.NewReader(string(ic.Icon)))
if errors.Log(err) != nil {
return
}
icons.Used[ic.Icon] = true
ic.prevIcon = ic.Icon
}
// renderSVG renders the [Icon.svg] if necessary.
func (ic *Icon) renderSVG() {
if ic.svg.Root == nil || !ic.svg.Root.HasChildren() {
return
}
sv := &ic.svg
sz := ic.Geom.Size.Actual.Content.ToPoint()
clr := gradient.ApplyOpacity(ic.Styles.Color, ic.Styles.Opacity)
if !ic.NeedsRebuild() && sv.Pixels != nil { // if rebuilding then rebuild
isz := sv.Pixels.Bounds().Size()
// if nothing has changed, we don't need to re-render
if isz == sz && sv.Name == string(ic.Icon) && sv.Color == clr {
return
}
}
if sz == (image.Point{}) {
return
}
// ensure that we have new pixels to render to in order to prevent
// us from rendering over ourself
sv.Pixels = image.NewRGBA(image.Rectangle{Max: sz})
sv.RenderState.Init(sz.X, sz.Y, sv.Pixels)
sv.Geom.Size = sz // make sure
sv.Resize(sz) // does Config if needed
sv.Color = clr
sv.Scale = 1
sv.Render()
sv.Name = string(ic.Icon)
}
func (ic *Icon) Render() {
ic.renderSVG()
if ic.svg.Pixels == nil {
return
}
r := ic.Geom.ContentBBox
sp := ic.Geom.ScrollOffset()
draw.Draw(ic.Scene.Pixels, r, ic.svg.Pixels, sp, draw.Over)
}
// 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 core
import (
"image"
"io/fs"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/icons"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
"golang.org/x/image/draw"
)
// Image is a widget that renders an [image.Image].
// See [styles.Style.ObjectFit] to control the image rendering within
// the allocated size. The default minimum requested size is the pixel
// size in [units.Dp] units (1/160th of an inch).
type Image struct {
WidgetBase
// Image is the [image.Image].
Image image.Image `xml:"-" json:"-"`
// prevImage is the cached last [Image.Image].
prevImage image.Image
// prevRenderImage is the cached last rendered image with any transformations applied.
prevRenderImage image.Image
// prevObjectFit is the cached [styles.Style.ObjectFit] of the last rendered image.
prevObjectFit styles.ObjectFits
// prevSize is the cached allocated size for the last rendered image.
prevSize math32.Vector2
}
func (im *Image) WidgetValue() any { return &im.Image }
func (im *Image) Init() {
im.WidgetBase.Init()
im.Styler(func(s *styles.Style) {
s.ObjectFit = styles.FitContain
if im.Image != nil {
sz := im.Image.Bounds().Size()
s.Min.X.SetCustom(func(uc *units.Context) float32 {
return min(uc.Dp(float32(sz.X)), uc.Pw(95))
})
s.Min.Y.Dp(float32(sz.Y))
}
})
}
// Open sets the image to the image located at the given filename.
func (im *Image) Open(filename Filename) error { //types:add
img, _, err := imagex.Open(string(filename))
if err != nil {
return err
}
im.SetImage(img)
return nil
}
// OpenFS sets the image to the image located at the given filename in the given fs.
func (im *Image) OpenFS(fsys fs.FS, filename string) error {
img, _, err := imagex.OpenFS(fsys, filename)
if err != nil {
return err
}
im.SetImage(img)
return nil
}
func (im *Image) SizeUp() {
im.WidgetBase.SizeUp()
if im.Image != nil {
sz := &im.Geom.Size
obj := math32.FromPoint(im.Image.Bounds().Size())
osz := styles.ObjectSizeFromFit(im.Styles.ObjectFit, obj, sz.Actual.Content)
sz.Actual.Content = osz
sz.setTotalFromContent(&sz.Actual)
}
}
func (im *Image) Render() {
im.WidgetBase.Render()
if im.Image == nil {
return
}
r := im.Geom.ContentBBox
sp := im.Geom.ScrollOffset()
var rimg image.Image
if im.prevImage == im.Image && im.Styles.ObjectFit == im.prevObjectFit && im.Geom.Size.Actual.Content == im.prevSize {
rimg = im.prevRenderImage
} else {
im.prevImage = im.Image
im.prevObjectFit = im.Styles.ObjectFit
im.prevSize = im.Geom.Size.Actual.Content
rimg = im.Styles.ResizeImage(im.Image, im.Geom.Size.Actual.Content)
im.prevRenderImage = rimg
}
draw.Draw(im.Scene.Pixels, r, rimg, sp, draw.Over)
}
func (im *Image) MakeToolbar(p *tree.Plan) {
tree.Add(p, func(w *FuncButton) {
w.SetFunc(im.Open).SetIcon(icons.Open)
})
}
// 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 (
"testing"
"cogentcore.org/core/styles"
"cogentcore.org/core/system"
)
func init() {
system.HandleRecover = handleRecover
system.InitScreenLogicalDPIFunc = AppearanceSettings.applyDPI // called when screens are initialized
TheApp.CogentCoreDataDir() // ensure it exists
theWindowGeometrySaver.needToReload() // gets time stamp associated with open, so it doesn't re-open
theWindowGeometrySaver.open()
styles.SettingsFont = (*string)(&AppearanceSettings.Font)
styles.SettingsMonoFont = (*string)(&AppearanceSettings.MonoFont)
if testing.Testing() {
// needed to prevent app from quitting prematurely
NewBody().RunWindow()
}
}
// 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 core
import (
"reflect"
"strconv"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/tree"
)
// InlineList represents a slice within a single line of value widgets.
// This is typically used for smaller slices.
type InlineList struct {
Frame
// Slice is the slice that we are viewing.
Slice any `set:"-"`
// isArray is whether the slice is actually an array.
isArray bool
}
func (il *InlineList) WidgetValue() any { return &il.Slice }
func (il *InlineList) Init() {
il.Frame.Init()
il.Maker(func(p *tree.Plan) {
sl := reflectx.Underlying(reflect.ValueOf(il.Slice))
sz := min(sl.Len(), SystemSettings.SliceInlineLength)
for i := 0; i < sz; i++ {
itxt := strconv.Itoa(i)
tree.AddNew(p, "value-"+itxt, func() Value {
val := reflectx.UnderlyingPointer(sl.Index(i))
return NewValue(val.Interface(), "")
}, func(w Value) {
wb := w.AsWidget()
wb.OnChange(func(e events.Event) { il.SendChange() })
wb.OnInput(func(e events.Event) {
il.Send(events.Input, e)
})
if il.IsReadOnly() {
wb.SetReadOnly(true)
} else {
wb.AddContextMenu(func(m *Scene) {
il.contextMenu(m, i)
})
}
wb.Updater(func() {
// We need to get the current value each time:
sl := reflectx.Underlying(reflect.ValueOf(il.Slice))
val := reflectx.UnderlyingPointer(sl.Index(i))
Bind(val.Interface(), w)
wb.SetReadOnly(il.IsReadOnly())
})
})
}
if !il.isArray && !il.IsReadOnly() {
tree.AddAt(p, "add-button", func(w *Button) {
w.SetIcon(icons.Add).SetType(ButtonTonal)
w.Tooltip = "Add an element to the list"
w.OnClick(func(e events.Event) {
il.NewAt(-1)
})
})
}
})
}
// SetSlice sets the source slice that we are viewing.
// It rebuilds the children to represent this slice.
func (il *InlineList) SetSlice(sl any) *InlineList {
if reflectx.IsNil(reflect.ValueOf(sl)) {
il.Slice = nil
return il
}
newslc := false
if reflect.TypeOf(sl).Kind() != reflect.Pointer { // prevent crash on non-comparable
newslc = true
} else {
newslc = il.Slice != sl
}
if newslc {
il.Slice = sl
il.isArray = reflectx.NonPointerType(reflect.TypeOf(sl)).Kind() == reflect.Array
il.Update()
}
return il
}
// NewAt inserts a new blank element at the given index in the slice.
// -1 indicates to insert the element at the end.
func (il *InlineList) NewAt(idx int) {
if il.isArray {
return
}
reflectx.SliceNewAt(il.Slice, idx)
il.UpdateChange()
}
// DeleteAt deletes the element at the given index from the slice.
func (il *InlineList) DeleteAt(idx int) {
if il.isArray {
return
}
reflectx.SliceDeleteAt(il.Slice, idx)
il.UpdateChange()
}
func (il *InlineList) contextMenu(m *Scene, idx int) {
if il.IsReadOnly() || il.isArray {
return
}
NewButton(m).SetText("Add").SetIcon(icons.Add).OnClick(func(e events.Event) {
il.NewAt(idx)
})
NewButton(m).SetText("Delete").SetIcon(icons.Delete).OnClick(func(e events.Event) {
il.DeleteAt(idx)
})
NewButton(m).SetText("Open in dialog").SetIcon(icons.OpenInNew).OnClick(func(e events.Event) {
d := NewBody(il.ValueTitle)
NewText(d).SetType(TextSupporting).SetText(il.Tooltip)
NewList(d).SetSlice(il.Slice).SetValueTitle(il.ValueTitle).SetReadOnly(il.IsReadOnly())
d.OnClose(func(e events.Event) {
il.UpdateChange()
})
d.RunFullDialog(il)
})
}
// 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 core
import (
"fmt"
"reflect"
"cogentcore.org/core/base/iox/jsonx"
"cogentcore.org/core/base/labels"
"cogentcore.org/core/colors"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/styles"
"cogentcore.org/core/tree"
)
// Inspector represents a [tree.Node] with a [Tree] and a [Form].
type Inspector struct {
Frame
// Root is the root of the tree being edited.
Root tree.Node
// currentNode is the currently selected node in the tree.
currentNode tree.Node
// filename is the current filename for saving / loading
filename Filename
treeWidget *Tree
}
func (is *Inspector) Init() {
is.Frame.Init()
is.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
s.Direction = styles.Column
})
var titleWidget *Text
tree.AddChildAt(is, "title", func(w *Text) {
titleWidget = w
is.currentNode = is.Root
w.SetType(TextHeadlineSmall)
w.Styler(func(s *styles.Style) {
s.Grow.Set(1, 0)
s.Align.Self = styles.Center
})
w.Updater(func() {
w.SetText(fmt.Sprintf("Inspector of %s (%s)", is.currentNode.AsTree().Name, labels.FriendlyTypeName(reflect.TypeOf(is.currentNode))))
})
})
renderRebuild := func() {
sc, ok := is.Root.(*Scene)
if !ok {
return
}
sc.renderContext().rebuild = true // trigger full rebuild
}
tree.AddChildAt(is, "splits", func(w *Splits) {
w.SetSplits(.3, .7)
var form *Form
tree.AddChildAt(w, "tree-frame", func(w *Frame) {
w.Styler(func(s *styles.Style) {
s.Background = colors.Scheme.SurfaceContainerLow
s.Direction = styles.Column
s.Overflow.Set(styles.OverflowAuto)
s.Gap.Zero()
})
tree.AddChildAt(w, "tree", func(w *Tree) {
is.treeWidget = w
w.SetTreeInit(func(tr *Tree) {
tr.Styler(func(s *styles.Style) {
s.Max.X.Em(20)
})
})
w.OnSelect(func(e events.Event) {
if len(w.SelectedNodes) == 0 {
return
}
sn := w.SelectedNodes[0].AsCoreTree().SyncNode
is.currentNode = sn
// Note: doing Update on the entire inspector reverts all tree expansion,
// so we only want to update the title and form
titleWidget.Update()
form.SetStruct(sn).Update()
sc, ok := is.Root.(*Scene)
if !ok {
return
}
if wb := AsWidget(sn); wb != nil {
pselw := sc.selectedWidget
sc.selectedWidget = sn.(Widget)
wb.NeedsRender()
if pselw != nil {
pselw.AsWidget().NeedsRender()
}
}
})
w.OnChange(func(e events.Event) {
renderRebuild()
})
w.SyncTree(is.Root)
})
})
tree.AddChildAt(w, "struct", func(w *Form) {
form = w
w.OnChange(func(e events.Event) {
renderRebuild()
})
w.OnClose(func(e events.Event) {
sc, ok := is.Root.(*Scene)
if !ok {
return
}
if sc.renderBBoxes {
is.toggleSelectionMode()
}
pselw := sc.selectedWidget
sc.selectedWidget = nil
if pselw != nil {
pselw.AsWidget().NeedsRender()
}
})
w.Updater(func() {
w.SetStruct(is.currentNode)
})
})
})
}
// save saves the tree to current filename, in a standard JSON-formatted file.
func (is *Inspector) save() error { //types:add
if is.Root == nil {
return nil
}
if is.filename == "" {
return nil
}
err := jsonx.Save(is.Root, string(is.filename))
if err != nil {
return err
}
return nil
}
// saveAs saves tree to given filename, in a standard JSON-formatted file
func (is *Inspector) saveAs(filename Filename) error { //types:add
if is.Root == nil {
return nil
}
err := jsonx.Save(is.Root, string(filename))
if err != nil {
return err
}
is.filename = filename
is.NeedsRender() // notify our editor
return nil
}
// open opens tree from given filename, in a standard JSON-formatted file
func (is *Inspector) open(filename Filename) error { //types:add
if is.Root == nil {
return nil
}
err := jsonx.Open(is.Root, string(filename))
if err != nil {
return err
}
is.filename = filename
is.NeedsRender() // notify our editor
return nil
}
// toggleSelectionMode toggles the editor between selection mode or not.
// In selection mode, bounding boxes are rendered around each Widget,
// and clicking on a Widget pulls it up in the inspector.
func (is *Inspector) toggleSelectionMode() { //types:add
sc, ok := is.Root.(*Scene)
if !ok {
return
}
sc.renderBBoxes = !sc.renderBBoxes
if sc.renderBBoxes {
sc.selectedWidgetChan = make(chan Widget)
go is.selectionMonitor()
} else {
if sc.selectedWidgetChan != nil {
close(sc.selectedWidgetChan)
}
sc.selectedWidgetChan = nil
}
sc.NeedsLayout()
}
// selectionMonitor monitors for the selected widget
func (is *Inspector) selectionMonitor() {
sc, ok := is.Root.(*Scene)
if !ok {
return
}
sc.Stage.raise()
sw, ok := <-sc.selectedWidgetChan
if !ok || sw == nil {
return
}
tv := is.treeWidget.FindSyncNode(sw)
if tv == nil {
// if we can't be found, we are probably a part,
// so we keep going up until we find somebody in
// the tree
sw.AsTree().WalkUpParent(func(k tree.Node) bool {
tv = is.treeWidget.FindSyncNode(k)
if tv != nil {
return tree.Break
}
return tree.Continue
})
if tv == nil {
MessageSnackbar(is, fmt.Sprintf("Inspector: tree node missing: %v", sw))
return
}
}
is.AsyncLock() // coming from other tree
tv.OpenParents()
tv.SelectEvent(events.SelectOne)
tv.ScrollToThis()
is.AsyncUnlock()
is.Scene.Stage.raise()
sc.AsyncLock()
sc.renderBBoxes = false
if sc.selectedWidgetChan != nil {
close(sc.selectedWidgetChan)
}
sc.selectedWidgetChan = nil
sc.NeedsRender()
sc.AsyncUnlock()
}
// inspectApp displays [TheApp].
func (is *Inspector) inspectApp() { //types:add
d := NewBody("Inspect app")
NewForm(d).SetStruct(TheApp).SetReadOnly(true)
d.RunFullDialog(is)
}
func (is *Inspector) MakeToolbar(p *tree.Plan) {
tree.Add(p, func(w *FuncButton) {
w.SetFunc(is.toggleSelectionMode).SetText("Select element").SetIcon(icons.ArrowSelectorTool)
w.Updater(func() {
_, ok := is.Root.(*Scene)
w.SetEnabled(ok)
})
})
tree.Add(p, func(w *Separator) {})
tree.Add(p, func(w *FuncButton) {
w.SetFunc(is.open).SetIcon(icons.Open).SetKey(keymap.Open)
w.Args[0].SetValue(is.filename).SetTag(`extension:".json"`)
})
tree.Add(p, func(w *FuncButton) {
w.SetFunc(is.save).SetIcon(icons.Save).SetKey(keymap.Save)
w.Updater(func() {
w.SetEnabled(is.filename != "")
})
})
tree.Add(p, func(w *FuncButton) {
w.SetFunc(is.saveAs).SetIcon(icons.SaveAs).SetKey(keymap.SaveAs)
w.Args[0].SetValue(is.filename).SetTag(`extension:".json"`)
})
tree.Add(p, func(w *Separator) {})
tree.Add(p, func(w *FuncButton) {
w.SetFunc(is.inspectApp).SetIcon(icons.Devices)
})
}
// InspectorWindow opens an interactive editor of the given tree
// in a new window.
func InspectorWindow(n tree.Node) {
if RecycleMainWindow(n) {
return
}
d := NewBody("Inspector")
makeInspector(d, n)
d.RunWindow()
}
// makeInspector configures the given body to have an interactive inspector
// of the given tree.
func makeInspector(b *Body, n tree.Node) {
b.SetTitle("Inspector").SetData(n)
if n != nil {
b.Name += "-" + n.AsTree().Name
b.Title += ": " + n.AsTree().Name
}
is := NewInspector(b)
is.SetRoot(n)
b.AddTopBar(func(bar *Frame) {
NewToolbar(bar).Maker(is.MakeToolbar)
})
}
// 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 core
import (
"cogentcore.org/core/events"
"cogentcore.org/core/events/key"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/styles/states"
)
// KeyMapButton represents a [keymap.MapName] value with a button.
type KeyMapButton struct {
Button
MapName keymap.MapName
}
func (km *KeyMapButton) WidgetValue() any { return &km.MapName }
func (km *KeyMapButton) Init() {
km.Button.Init()
km.SetType(ButtonTonal)
km.Updater(func() {
km.SetText(string(km.MapName))
})
InitValueButton(km, false, func(d *Body) {
d.SetTitle("Select a key map")
si := 0
_, curRow, _ := keymap.AvailableMaps.MapByName(km.MapName)
tv := NewTable(d).SetSlice(&keymap.AvailableMaps).SetSelectedIndex(curRow).BindSelect(&si)
tv.OnChange(func(e events.Event) {
name := keymap.AvailableMaps[si]
km.MapName = keymap.MapName(name.Name)
})
})
}
// KeyChordButton represents a [key.Chord] value with a button.
type KeyChordButton struct {
Button
Chord key.Chord
}
func (kc *KeyChordButton) WidgetValue() any { return &kc.Chord }
func (kc *KeyChordButton) Init() {
kc.Button.Init()
kc.SetType(ButtonTonal)
kc.OnKeyChord(func(e events.Event) {
if !kc.StateIs(states.Focused) {
return
}
kc.Chord = e.KeyChord()
e.SetHandled()
kc.UpdateChange()
})
kc.Updater(func() {
kc.SetText(kc.Chord.Label())
})
kc.AddContextMenu(func(m *Scene) {
NewButton(m).SetText("Clear").SetIcon(icons.ClearAll).OnClick(func(e events.Event) {
kc.Chord = ""
kc.UpdateChange()
})
})
}
// 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 core
import (
"fmt"
"reflect"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
// KeyedList represents a map value using two columns of editable key and value widgets.
type KeyedList struct {
Frame
// Map is the pointer to the map that we are viewing.
Map any
// Inline is whether to display the map in one line.
Inline bool
// SortByValues is whether to sort by values instead of keys.
SortByValues bool
// ncols is the number of columns to display if the keyed list is not inline.
ncols int
}
func (kl *KeyedList) WidgetValue() any { return &kl.Map }
func (kl *KeyedList) Init() {
kl.Frame.Init()
kl.Styler(func(s *styles.Style) {
if kl.Inline {
return
}
s.Display = styles.Grid
s.Columns = kl.ncols
s.Overflow.Set(styles.OverflowAuto)
s.Grow.Set(1, 1)
s.Min.X.Em(20)
s.Min.Y.Em(10)
})
kl.Maker(func(p *tree.Plan) {
mapv := reflectx.Underlying(reflect.ValueOf(kl.Map))
if reflectx.IsNil(mapv) {
return
}
kl.ncols = 2
typeAny := false
valueType := mapv.Type().Elem()
if valueType.String() == "interface {}" {
kl.ncols = 3
typeAny = true
}
builtinTypes := types.BuiltinTypes()
keys := reflectx.MapSort(kl.Map, !kl.SortByValues, true)
for _, key := range keys {
keytxt := reflectx.ToString(key.Interface())
keynm := "key-" + keytxt
valnm := "value-" + keytxt
tree.AddNew(p, keynm, func() Value {
return toValue(key.Interface(), "")
}, func(w Value) {
bindMapKey(mapv, key, w)
wb := w.AsWidget()
wb.SetReadOnly(kl.IsReadOnly())
wb.Styler(func(s *styles.Style) {
s.SetReadOnly(kl.IsReadOnly())
s.SetTextWrap(false)
})
wb.OnChange(func(e events.Event) {
kl.UpdateChange(e)
})
wb.SetReadOnly(kl.IsReadOnly())
wb.OnInput(kl.HandleEvent)
if !kl.IsReadOnly() {
wb.AddContextMenu(func(m *Scene) {
kl.contextMenu(m, key)
})
}
wb.Updater(func() {
bindMapKey(mapv, key, w)
wb.SetReadOnly(kl.IsReadOnly())
})
})
tree.AddNew(p, valnm, func() Value {
val := mapv.MapIndex(key).Interface()
w := toValue(val, "")
return bindMapValue(mapv, key, w)
}, func(w Value) {
wb := w.AsWidget()
wb.SetReadOnly(kl.IsReadOnly())
wb.OnChange(func(e events.Event) { kl.SendChange(e) })
wb.OnInput(kl.HandleEvent)
wb.Styler(func(s *styles.Style) {
s.SetReadOnly(kl.IsReadOnly())
s.SetTextWrap(false)
})
if !kl.IsReadOnly() {
wb.AddContextMenu(func(m *Scene) {
kl.contextMenu(m, key)
})
}
wb.Updater(func() {
bindMapValue(mapv, key, w)
wb.SetReadOnly(kl.IsReadOnly())
})
})
if typeAny {
typnm := "type-" + keytxt
tree.AddAt(p, typnm, func(w *Chooser) {
w.SetTypes(builtinTypes...)
w.OnChange(func(e events.Event) {
typ := reflect.TypeOf(w.CurrentItem.Value.(*types.Type).Instance)
newVal := reflect.New(typ)
// try our best to convert the existing value to the new type
reflectx.SetRobust(newVal.Interface(), mapv.MapIndex(key).Interface())
mapv.SetMapIndex(key, newVal.Elem())
kl.DeleteChildByName(valnm) // force it to be updated
kl.Update()
})
w.Updater(func() {
w.SetReadOnly(kl.IsReadOnly())
vtyp := types.TypeByValue(mapv.MapIndex(key).Interface())
if vtyp == nil {
vtyp = types.TypeByName("string") // default to string
}
w.SetCurrentValue(vtyp)
})
})
}
}
if kl.Inline && !kl.IsReadOnly() {
tree.AddAt(p, "add-button", func(w *Button) {
w.SetIcon(icons.Add).SetType(ButtonTonal)
w.Tooltip = "Add an element"
w.OnClick(func(e events.Event) {
kl.AddItem()
})
})
}
})
}
func (kl *KeyedList) contextMenu(m *Scene, keyv reflect.Value) {
if kl.IsReadOnly() {
return
}
NewButton(m).SetText("Add").SetIcon(icons.Add).OnClick(func(e events.Event) {
kl.AddItem()
})
NewButton(m).SetText("Delete").SetIcon(icons.Delete).OnClick(func(e events.Event) {
kl.DeleteItem(keyv)
})
if kl.Inline {
NewButton(m).SetText("Open in dialog").SetIcon(icons.OpenInNew).OnClick(func(e events.Event) {
d := NewBody(kl.ValueTitle)
NewText(d).SetType(TextSupporting).SetText(kl.Tooltip)
NewKeyedList(d).SetMap(kl.Map).SetValueTitle(kl.ValueTitle).SetReadOnly(kl.IsReadOnly())
d.OnClose(func(e events.Event) {
kl.UpdateChange(e)
})
d.RunFullDialog(kl)
})
}
}
// toggleSort toggles sorting by values vs. keys
func (kl *KeyedList) toggleSort() {
kl.SortByValues = !kl.SortByValues
kl.Update()
}
// AddItem adds a new key-value item to the map.
func (kl *KeyedList) AddItem() {
if reflectx.IsNil(reflect.ValueOf(kl.Map)) {
return
}
reflectx.MapAdd(kl.Map)
kl.UpdateChange()
}
// DeleteItem deletes a key-value item from the map.
func (kl *KeyedList) DeleteItem(key reflect.Value) {
if reflectx.IsNil(reflect.ValueOf(kl.Map)) {
return
}
reflectx.MapDelete(kl.Map, reflectx.NonPointerValue(key))
kl.UpdateChange()
}
func (kl *KeyedList) MakeToolbar(p *tree.Plan) {
if reflectx.IsNil(reflect.ValueOf(kl.Map)) {
return
}
tree.Add(p, func(w *Button) {
w.SetText("Sort").SetIcon(icons.Sort).SetTooltip("Switch between sorting by the keys and the values").
OnClick(func(e events.Event) {
kl.toggleSort()
})
})
if !kl.IsReadOnly() {
tree.Add(p, func(w *Button) {
w.SetText("Add").SetIcon(icons.Add).SetTooltip("Add a new element to the map").
OnClick(func(e events.Event) {
kl.AddItem()
})
})
}
}
// bindMapKey is a version of [Bind] that works for keys in a map.
func bindMapKey[T Value](mapv reflect.Value, key reflect.Value, vw T) T {
wb := vw.AsWidget()
alreadyBound := wb.ValueUpdate != nil
wb.ValueUpdate = func() {
if vws, ok := any(vw).(ValueSetter); ok {
ErrorSnackbar(vw, vws.SetWidgetValue(key.Interface()))
} else {
ErrorSnackbar(vw, reflectx.SetRobust(vw.WidgetValue(), key.Interface()))
}
}
wb.ValueOnChange = func() {
newKey := reflect.New(key.Type())
ErrorSnackbar(vw, reflectx.SetRobust(newKey.Interface(), vw.WidgetValue()))
newKey = newKey.Elem()
if !mapv.MapIndex(newKey).IsValid() { // not already taken
mapv.SetMapIndex(newKey, mapv.MapIndex(key))
mapv.SetMapIndex(key, reflect.Value{})
return
}
d := NewBody("Key already exists")
NewText(d).SetType(TextSupporting).SetText(fmt.Sprintf("The key %q already exists", reflectx.ToString(newKey.Interface())))
d.AddBottomBar(func(bar *Frame) {
d.AddCancel(bar)
d.AddOK(bar).SetText("Overwrite").OnClick(func(e events.Event) {
mapv.SetMapIndex(newKey, mapv.MapIndex(key))
mapv.SetMapIndex(key, reflect.Value{})
wb.SendChange()
})
})
d.RunDialog(vw)
}
if alreadyBound {
ResetWidgetValue(vw)
}
if ob, ok := any(vw).(OnBinder); ok {
ob.OnBind(key.Interface(), "")
}
wb.ValueUpdate() // we update it with the initial value immediately
return vw
}
// bindMapValue is a version of [Bind] that works for values in a map.
func bindMapValue[T Value](mapv reflect.Value, key reflect.Value, vw T) T {
wb := vw.AsWidget()
alreadyBound := wb.ValueUpdate != nil
wb.ValueUpdate = func() {
value := mapv.MapIndex(key).Interface()
if vws, ok := any(vw).(ValueSetter); ok {
ErrorSnackbar(vw, vws.SetWidgetValue(value))
} else {
ErrorSnackbar(vw, reflectx.SetRobust(vw.WidgetValue(), value))
}
}
wb.ValueOnChange = func() {
value := reflectx.NonNilNew(mapv.Type().Elem())
err := reflectx.SetRobust(value.Interface(), vw.WidgetValue())
if err != nil {
ErrorSnackbar(vw, err)
return
}
mapv.SetMapIndex(key, value.Elem())
}
if alreadyBound {
ResetWidgetValue(vw)
}
if ob, ok := any(vw).(OnBinder); ok {
value := mapv.MapIndex(key).Interface()
ob.OnBind(value, "")
}
wb.ValueUpdate() // we update it with the initial value immediately
return vw
}
// 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 (
"fmt"
"image"
"log/slog"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"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
const (
SizeUpPass LayoutPasses = iota
SizeDownPass
SizeFinalPass
)
// Layouter is an interface containing layout functions
// implemented by all types embedding [Frame].
type Layouter interface {
Widget
// AsFrame returns the Layouter as a [Frame].
AsFrame() *Frame
// LayoutSpace sets our Space based on Styles, Scroll, and Gap Spacing.
// Other layout types can change this if they want to.
LayoutSpace()
// SizeFromChildren gathers Actual size from kids into our Actual.Content size.
// Different Layout types can alter this to present different Content
// sizes for the layout process, e.g., if Content is sized to fit allocation,
// as in the [Toolbar] and [List] types.
SizeFromChildren(iter int, pass LayoutPasses) math32.Vector2
// SizeDownSetAllocs is the key SizeDown step that sets the allocations
// in the children, based on our allocation. In the default implementation
// this calls SizeDownGrow if there is extra space to grow, or
// SizeDownAllocActual to set the allocations as they currently are.
SizeDownSetAllocs(iter int)
// ManageOverflow uses overflow settings to determine if scrollbars
// are needed, based on difference between ActualOverflow (full actual size)
// and Alloc allocation. Returns true if size changes as a result.
// If updateSize is false, then the Actual and Alloc sizes are NOT
// updated as a result of change from adding scrollbars
// (generally should be true, but some cases not)
ManageOverflow(iter int, updateSize bool) bool
// ScrollChanged is called in the OnInput event handler for updating
// when the scrollbar value has changed, for given dimension
ScrollChanged(d math32.Dims, sb *Slider)
// ScrollValues returns the maximum size that could be scrolled,
// the visible size (which could be less than the max size, in which
// case no scrollbar is needed), and visSize / maxSize as the VisiblePct.
// This is used in updating the scrollbar and determining whether one is
// needed in the first place
ScrollValues(d math32.Dims) (maxSize, visSize, visPct float32)
// ScrollGeom returns the target position and size for scrollbars
ScrollGeom(d math32.Dims) (pos, sz math32.Vector2)
// SetScrollParams sets scrollbar parameters. Must set Step and PageStep,
// but can also set others as needed.
// Max and VisiblePct are automatically set based on ScrollValues maxSize, visPct.
SetScrollParams(d math32.Dims, sb *Slider)
}
// AsFrame returns the given value as a value of type [Frame] if the type
// of the given value embeds [Frame], or nil otherwise.
func AsFrame(n tree.Node) *Frame {
if t, ok := n.(Layouter); ok {
return t.AsFrame()
}
return nil
}
// AsFrame satisfies the [Layouter] interface.
func (t *Frame) AsFrame() *Frame {
return t
}
var _ Layouter = &Frame{}
// geomCT has core layout elements: Content and Total
type geomCT struct {
// Content is for the contents (children, parts) of the widget,
// excluding the Space (margin, padding, scrollbars).
// This content includes the InnerSpace factor (Gaps in Layout)
// which must therefore be subtracted when allocating down to children.
Content math32.Vector2
// Total is for the total exterior of the widget: Content + Space
Total math32.Vector2
}
func (ct geomCT) String() string {
return fmt.Sprintf("Content: %v, \tTotal: %v", ct.Content, ct.Total)
}
// geomSize has all of the relevant Layout sizes
type geomSize struct {
// Actual is the actual size for the purposes of rendering, representing
// the "external" demands of the widget for space from its parent.
// This is initially the bottom-up constraint computed by SizeUp,
// and only changes during SizeDown when wrapping elements are reshaped
// based on allocated size, or when scrollbars are added.
// For elements with scrollbars (OverflowAuto), the Actual size remains
// at the initial style minimums, "absorbing" is internal size,
// while Internal records the true size of the contents.
// For SizeFinal, Actual size can Grow up to the final Alloc size,
// while Internal records the actual bottom-up contents size.
Actual geomCT `display:"inline"`
// Alloc is the top-down allocated size, based on available visible space,
// starting from the Scene geometry and working downward, attempting to
// accommodate the Actual contents, and allocating extra space based on
// Grow factors. When Actual < Alloc, alignment factors determine positioning
// within the allocated space.
Alloc geomCT `display:"inline"`
// Internal is the internal size representing the true size of all contents
// of the widget. This can be less than Actual.Content if widget has Grow
// factors but its internal contents did not grow accordingly, or it can
// be more than Actual.Content if it has scrollbars (OverflowAuto).
// Note that this includes InnerSpace (Gap).
Internal math32.Vector2
// Space is the padding, total effective margin (border, shadow, etc),
// and scrollbars that subtracts from Total size to get Content size.
Space math32.Vector2
// InnerSpace is total extra space that is included within the Content Size region
// and must be subtracted from Content when passing sizes down to children.
InnerSpace math32.Vector2
// Min is the Styles.Min.Dots() (Ceil int) that constrains the Actual.Content size
Min math32.Vector2
// Max is the Styles.Max.Dots() (Ceil int) that constrains the Actual.Content size
Max math32.Vector2
}
func (ls geomSize) String() string {
return fmt.Sprintf("Actual: %v, \tAlloc: %v", ls.Actual, ls.Alloc)
}
// setInitContentMin sets initial Actual.Content size from given Styles.Min,
// further subject to the current Max constraint.
func (ls *geomSize) setInitContentMin(styMin math32.Vector2) {
csz := &ls.Actual.Content
*csz = styMin
styles.SetClampMaxVector(csz, ls.Max)
}
// FitSizeMax increases given size to fit given fm value, subject to Max constraints
func (ls *geomSize) FitSizeMax(to *math32.Vector2, fm math32.Vector2) {
styles.SetClampMinVector(to, fm)
styles.SetClampMaxVector(to, ls.Max)
}
// setTotalFromContent sets the Total size as Content plus Space
func (ls *geomSize) setTotalFromContent(ct *geomCT) {
ct.Total = ct.Content.Add(ls.Space)
}
// setContentFromTotal sets the Content from Total size,
// subtracting Space
func (ls *geomSize) setContentFromTotal(ct *geomCT) {
ct.Content = ct.Total.Sub(ls.Space)
}
// geomState contains the the layout geometry state for each widget.
// Set by the parent Layout during the Layout process.
type geomState struct {
// Size has sizing data for the widget: use Actual for rendering.
// Alloc shows the potentially larger space top-down allocated.
Size geomSize `display:"add-fields"`
// Pos is position within the overall Scene that we render into,
// including effects of scroll offset, for both Total outer dimension
// and inner Content dimension.
Pos geomCT `display:"inline" edit:"-" copier:"-" json:"-" xml:"-" set:"-"`
// Cell is the logical X, Y index coordinates (col, row) of element
// within its parent layout
Cell image.Point
// RelPos is top, left position relative to parent Content size space
RelPos math32.Vector2
// Scroll is additional scrolling offset within our parent layout
Scroll math32.Vector2
// 2D bounding box for Actual.Total size occupied within parent Scene
// that we render onto, starting at Pos.Total and ending at Pos.Total + Size.Total.
// These are the pixels we can draw into, intersected with parent bounding boxes
// (empty for invisible). Used for render Bounds clipping.
// This includes all space (margin, padding etc).
TotalBBox image.Rectangle `edit:"-" copier:"-" json:"-" xml:"-" set:"-"`
// 2D bounding box for our Content, which excludes our padding, margin, etc.
// starting at Pos.Content and ending at Pos.Content + Size.Content.
// It is intersected with parent bounding boxes.
ContentBBox image.Rectangle `edit:"-" copier:"-" json:"-" xml:"-" set:"-"`
}
func (ls *geomState) String() string {
return "Size: " + ls.Size.String() + "\nPos: " + ls.Pos.String() + "\tCell: " + ls.Cell.String() +
"\tRelPos: " + ls.RelPos.String() + "\tScroll: " + ls.Scroll.String()
}
// contentRangeDim returns the Content bounding box min, max
// along given dimension
func (ls *geomState) contentRangeDim(d math32.Dims) (cmin, cmax float32) {
cmin = float32(math32.PointDim(ls.ContentBBox.Min, d))
cmax = float32(math32.PointDim(ls.ContentBBox.Max, d))
return
}
// totalRect returns Pos.Total -- Size.Actual.Total
// as an image.Rectangle, e.g., for bounding box
func (ls *geomState) totalRect() image.Rectangle {
return math32.RectFromPosSizeMax(ls.Pos.Total, ls.Size.Actual.Total)
}
// contentRect returns Pos.Content, Size.Actual.Content
// as an image.Rectangle, e.g., for bounding box.
func (ls *geomState) contentRect() image.Rectangle {
return math32.RectFromPosSizeMax(ls.Pos.Content, ls.Size.Actual.Content)
}
// ScrollOffset computes the net scrolling offset as a function of
// the difference between the allocated position and the actual
// content position according to the clipped bounding box.
func (ls *geomState) ScrollOffset() image.Point {
return ls.ContentBBox.Min.Sub(ls.Pos.Content.ToPoint())
}
// layoutCell holds the layout implementation data for col, row Cells
type layoutCell struct {
// Size has the Actual size of elements (not Alloc)
Size math32.Vector2
// Grow has the Grow factors
Grow math32.Vector2
}
func (ls *layoutCell) String() string {
return fmt.Sprintf("Size: %v, \tGrow: %g", ls.Size, ls.Grow)
}
func (ls *layoutCell) reset() {
ls.Size.SetZero()
ls.Grow.SetZero()
}
// layoutCells holds one set of LayoutCell cell elements for rows, cols.
// There can be multiple of these for Wrap case.
type layoutCells struct {
// Shape is number of cells along each dimension for our ColRow cells,
Shape image.Point `edit:"-"`
// ColRow has the data for the columns in [0] and rows in [1]:
// col Size.X = max(X over rows) (cross axis), .Y = sum(Y over rows) (main axis for col)
// row Size.X = sum(X over cols) (main axis for row), .Y = max(Y over cols) (cross axis)
// see: https://docs.google.com/spreadsheets/d/1eimUOIJLyj60so94qUr4Buzruj2ulpG5o6QwG2nyxRw/edit?usp=sharing
ColRow [2][]layoutCell `edit:"-"`
}
// cell returns the cell for given dimension and index along that
// dimension (X = Cols, idx = col, Y = Rows, idx = row)
func (lc *layoutCells) cell(d math32.Dims, idx int) *layoutCell {
if len(lc.ColRow[d]) <= idx {
return nil
}
return &(lc.ColRow[d][idx])
}
// init initializes Cells for given shape
func (lc *layoutCells) init(shape image.Point) {
lc.Shape = shape
for d := math32.X; d <= math32.Y; d++ {
n := math32.PointDim(lc.Shape, d)
if len(lc.ColRow[d]) != n {
lc.ColRow[d] = make([]layoutCell, n)
}
for i := 0; i < n; i++ {
lc.ColRow[d][i].reset()
}
}
}
// cellsSize returns the total Size represented by the current Cells,
// which is the Sum of the Max values along each dimension.
func (lc *layoutCells) cellsSize() math32.Vector2 {
var ksz math32.Vector2
for ma := math32.X; ma <= math32.Y; ma++ { // main axis = X then Y
n := math32.PointDim(lc.Shape, ma) // cols, rows
sum := float32(0)
for mi := 0; mi < n; mi++ {
md := lc.cell(ma, mi) // X, Y
mx := md.Size.Dim(ma)
sum += mx // sum of maxes
}
ksz.SetDim(ma, sum)
}
return ksz.Ceil()
}
// gapSizeDim returns the gap size for given dimension, based on Shape and given gap size
func (lc *layoutCells) gapSizeDim(d math32.Dims, gap float32) float32 {
n := math32.PointDim(lc.Shape, d)
return float32(n-1) * gap
}
func (lc *layoutCells) String() string {
s := ""
n := lc.Shape.X
for i := 0; i < n; i++ {
col := lc.cell(math32.X, i)
s += fmt.Sprintln("col:", i, "\tmax X:", col.Size.X, "\tsum Y:", col.Size.Y, "\tmax grX:", col.Grow.X, "\tsum grY:", col.Grow.Y)
}
n = lc.Shape.Y
for i := 0; i < n; i++ {
row := lc.cell(math32.Y, i)
s += fmt.Sprintln("row:", i, "\tsum X:", row.Size.X, "\tmax Y:", row.Size.Y, "\tsum grX:", row.Grow.X, "\tmax grY:", row.Grow.Y)
}
return s
}
// layoutState has internal state for implementing layout
type layoutState struct {
// Shape is number of cells along each dimension,
// computed for each layout type:
// For Grid: Max Col, Row.
// For Flex no Wrap: Cols,1 (X) or 1,Rows (Y).
// For Flex Wrap: Cols,Max(Rows) or Max(Cols),Rows
// For Stacked: 1,1
Shape image.Point `edit:"-"`
// MainAxis cached here from Styles, to enable Wrap-based access.
MainAxis math32.Dims
// Wraps is the number of actual items in each Wrap for Wrap case:
// MainAxis X: Len = Rows, Val = Cols; MainAxis Y: Len = Cols, Val = Rows.
// This should be nil for non-Wrap case.
Wraps []int
// Cells has the Actual size and Grow factor data for each of the child elements,
// organized according to the Shape and Display type.
// For non-Wrap, has one element in slice, with cells based on Shape.
// For Wrap, slice is number of CrossAxis wraps allocated:
// MainAxis X = Rows; MainAxis Y = Cols, and Cells are MainAxis layout x 1 CrossAxis.
Cells []layoutCells `edit:"-"`
// ScrollSize has the scrollbar sizes (widths) for each dim, which adds to Space.
// If there is a vertical scrollbar, X has width; if horizontal, Y has "width" = height
ScrollSize math32.Vector2
// Gap is the Styles.Gap size
Gap math32.Vector2
// GapSize has the total extra gap sizing between elements, which adds to Space.
// This depends on cell layout so it can vary for Wrap case.
// For SizeUp / Down Gap contributes to Space like other cases,
// but for BoundingBox rendering and Alignment, it does NOT, and must be
// subtracted. This happens in the Position phase.
GapSize math32.Vector2
}
// initCells initializes the Cells based on Shape, MainAxis, and Wraps
// which must be set before calling.
func (ls *layoutState) initCells() {
if ls.Wraps == nil {
if len(ls.Cells) != 1 {
ls.Cells = make([]layoutCells, 1)
}
ls.Cells[0].init(ls.Shape)
return
}
ma := ls.MainAxis
ca := ma.Other()
nw := len(ls.Wraps)
if len(ls.Cells) != nw {
ls.Cells = make([]layoutCells, nw)
}
for wi, wn := range ls.Wraps {
var shp image.Point
math32.SetPointDim(&shp, ma, wn)
math32.SetPointDim(&shp, ca, 1)
ls.Cells[wi].init(shp)
}
}
func (ls *layoutState) shapeCheck(w Widget, phase string) bool {
if w.AsTree().HasChildren() && (ls.Shape == (image.Point{}) || len(ls.Cells) == 0) {
// fmt.Println(w, "Shape is nil in:", phase) // TODO: plan for this?
return false
}
return true
}
// cell returns the cell for given dimension and index along that
// dimension, and given other-dimension axis which is ignored for non-Wrap cases.
// Does no range checking and will crash if out of bounds.
func (ls *layoutState) cell(d math32.Dims, dIndex, odIndex int) *layoutCell {
if ls.Wraps == nil {
return ls.Cells[0].cell(d, dIndex)
}
if ls.MainAxis == d {
return ls.Cells[odIndex].cell(d, dIndex)
}
return ls.Cells[dIndex].cell(d, 0)
}
// wrapIndexToCoord returns the X,Y coordinates in Wrap case for given sequential idx
func (ls *layoutState) wrapIndexToCoord(idx int) image.Point {
y := 0
x := 0
sum := 0
if ls.MainAxis == math32.X {
for _, nx := range ls.Wraps {
if idx >= sum && idx < sum+nx {
x = idx - sum
break
}
sum += nx
y++
}
} else {
for _, ny := range ls.Wraps {
if idx >= sum && idx < sum+ny {
y = idx - sum
break
}
sum += ny
x++
}
}
return image.Point{x, y}
}
// cellsSize returns the total Size represented by the current Cells,
// which is the Sum of the Max values along each dimension within each Cell,
// Maxed over cross-axis dimension for Wrap case, plus GapSize.
func (ls *layoutState) cellsSize() math32.Vector2 {
if ls.Wraps == nil {
return ls.Cells[0].cellsSize().Add(ls.GapSize)
}
var ksz math32.Vector2
d := ls.MainAxis
od := d.Other()
gap := ls.Gap.Dim(d)
for wi := range ls.Wraps {
wsz := ls.Cells[wi].cellsSize()
wsz.SetDim(d, wsz.Dim(d)+ls.Cells[wi].gapSizeDim(d, gap))
if wi == 0 {
ksz = wsz
} else {
ksz.SetDim(d, max(ksz.Dim(d), wsz.Dim(d)))
ksz.SetDim(od, ksz.Dim(od)+wsz.Dim(od))
}
}
ksz.SetDim(od, ksz.Dim(od)+ls.GapSize.Dim(od))
return ksz.Ceil()
}
// colWidth returns the width of given column for given row index
// (ignored for non-Wrap), with full bounds checking.
// Returns error if out of range.
func (ls *layoutState) colWidth(row, col int) (float32, error) {
if ls.Wraps == nil {
n := math32.PointDim(ls.Shape, math32.X)
if col >= n {
return 0, fmt.Errorf("Layout.ColWidth: col: %d > number of columns: %d", col, n)
}
return ls.cell(math32.X, col, 0).Size.X, nil
}
nw := len(ls.Wraps)
if ls.MainAxis == math32.X {
if row >= nw {
return 0, fmt.Errorf("Layout.ColWidth: row: %d > number of rows: %d", row, nw)
}
wn := ls.Wraps[row]
if col >= wn {
return 0, fmt.Errorf("Layout.ColWidth: col: %d > number of columns: %d", col, wn)
}
return ls.cell(math32.X, col, row).Size.X, nil
}
if col >= nw {
return 0, fmt.Errorf("Layout.ColWidth: col: %d > number of columns: %d", col, nw)
}
wn := ls.Wraps[col]
if row >= wn {
return 0, fmt.Errorf("Layout.ColWidth: row: %d > number of rows: %d", row, wn)
}
return ls.cell(math32.X, col, row).Size.X, nil
}
// rowHeight returns the height of given row for given
// column (ignored for non-Wrap), with full bounds checking.
// Returns error if out of range.
func (ls *layoutState) rowHeight(row, col int) (float32, error) {
if ls.Wraps == nil {
n := math32.PointDim(ls.Shape, math32.Y)
if row >= n {
return 0, fmt.Errorf("Layout.RowHeight: row: %d > number of rows: %d", row, n)
}
return ls.cell(math32.Y, 0, row).Size.Y, nil
}
nw := len(ls.Wraps)
if ls.MainAxis == math32.Y {
if col >= nw {
return 0, fmt.Errorf("Layout.RowHeight: col: %d > number of columns: %d", col, nw)
}
wn := ls.Wraps[row]
if col >= wn {
return 0, fmt.Errorf("Layout.RowHeight: row: %d > number of rows: %d", row, wn)
}
return ls.cell(math32.Y, col, row).Size.Y, nil
}
if row >= nw {
return 0, fmt.Errorf("Layout.RowHeight: row: %d > number of rows: %d", row, nw)
}
wn := ls.Wraps[col]
if col >= wn {
return 0, fmt.Errorf("Layout.RowHeight: col: %d > number of columns: %d", col, wn)
}
return ls.cell(math32.Y, row, col).Size.Y, nil
}
func (ls *layoutState) String() string {
if ls.Wraps == nil {
return ls.Cells[0].String()
}
s := ""
ods := ls.MainAxis.Other().String()
for wi := range ls.Wraps {
s += fmt.Sprintf("%s: %d Shape: %v\n", ods, wi, ls.Cells[wi].Shape) + ls.Cells[wi].String()
}
return s
}
// StackTopWidget returns the [Frame.StackTop] element as a [WidgetBase].
func (fr *Frame) StackTopWidget() *WidgetBase {
n := fr.Child(fr.StackTop)
return AsWidget(n)
}
// laySetContentFitOverflow sets Internal and Actual.Content size to fit given
// new content size, depending on the Styles Overflow: Auto and Scroll types do NOT
// expand Actual and remain at their current styled actual values,
// absorbing the extra content size within their own scrolling zone
// (full size recorded in Internal).
func (fr *Frame) laySetContentFitOverflow(nsz math32.Vector2, pass LayoutPasses) {
sz := &fr.Geom.Size
asz := &sz.Actual.Content
isz := &sz.Internal
sz.setInitContentMin(sz.Min) // start from style
*isz = nsz // internal is always accurate!
oflow := &fr.Styles.Overflow
nosz := pass == SizeUpPass && fr.Styles.IsFlexWrap()
mx := sz.Max
for d := math32.X; d <= math32.Y; d++ {
if nosz {
continue
}
if !(fr.Scene != nil && fr.Scene.hasFlag(sceneContentSizing)) && oflow.Dim(d) >= styles.OverflowAuto && fr.Parent != nil {
if mx.Dim(d) > 0 {
asz.SetDim(d, styles.ClampMax(styles.ClampMin(asz.Dim(d), nsz.Dim(d)), mx.Dim(d)))
}
} else {
asz.SetDim(d, styles.ClampMin(asz.Dim(d), nsz.Dim(d)))
}
}
styles.SetClampMaxVector(asz, mx)
sz.setTotalFromContent(&sz.Actual)
}
// 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.
func (wb *WidgetBase) SizeUp() {
wb.SizeUpWidget()
}
// SizeUpWidget is the standard Widget SizeUp pass
func (wb *WidgetBase) SizeUpWidget() {
wb.sizeFromStyle()
wb.sizeUpParts()
sz := &wb.Geom.Size
sz.setTotalFromContent(&sz.Actual)
}
// spaceFromStyle sets the Space based on Style BoxSpace().Size()
func (wb *WidgetBase) spaceFromStyle() {
wb.Geom.Size.Space = wb.Styles.BoxSpace().Size().Ceil()
}
// sizeFromStyle sets the initial Actual Sizes from Style.Min, Max.
// Required first call in SizeUp.
func (wb *WidgetBase) sizeFromStyle() {
sz := &wb.Geom.Size
s := &wb.Styles
wb.spaceFromStyle()
wb.Geom.Size.InnerSpace.SetZero()
sz.Min = s.Min.Dots().Ceil()
sz.Max = s.Max.Dots().Ceil()
if s.Min.X.Unit == units.UnitPw || s.Min.X.Unit == units.UnitPh {
sz.Min.X = 0
}
if s.Min.Y.Unit == units.UnitPw || s.Min.Y.Unit == units.UnitPh {
sz.Min.Y = 0
}
if s.Max.X.Unit == units.UnitPw || s.Max.X.Unit == units.UnitPh {
sz.Max.X = 0
}
if s.Max.Y.Unit == units.UnitPw || s.Max.Y.Unit == units.UnitPh {
sz.Max.Y = 0
}
sz.Internal.SetZero()
sz.setInitContentMin(sz.Min)
sz.setTotalFromContent(&sz.Actual)
if DebugSettings.LayoutTrace && (sz.Actual.Content.X > 0 || sz.Actual.Content.Y > 0) {
fmt.Println(wb, "SizeUp from Style:", sz.Actual.Content.String())
}
}
// updateParentRelSizes updates any parent-relative Min, Max size values
// based on current actual parent sizes.
func (wb *WidgetBase) updateParentRelSizes() bool {
pwb := wb.parentWidget()
if pwb == nil {
return false
}
sz := &wb.Geom.Size
s := &wb.Styles
psz := pwb.Geom.Size.Alloc.Content.Sub(pwb.Geom.Size.InnerSpace)
got := false
for d := math32.X; d <= math32.Y; d++ {
if s.Min.Dim(d).Unit == units.UnitPw {
got = true
sz.Min.SetDim(d, psz.X*0.01*s.Min.Dim(d).Value)
}
if s.Min.Dim(d).Unit == units.UnitPh {
got = true
sz.Min.SetDim(d, psz.Y*0.01*s.Min.Dim(d).Value)
}
if s.Max.Dim(d).Unit == units.UnitPw {
got = true
sz.Max.SetDim(d, psz.X*0.01*s.Max.Dim(d).Value)
}
if s.Max.Dim(d).Unit == units.UnitPh {
got = true
sz.Max.SetDim(d, psz.Y*0.01*s.Max.Dim(d).Value)
}
}
if got {
sz.FitSizeMax(&sz.Actual.Total, sz.Min)
sz.FitSizeMax(&sz.Alloc.Total, sz.Min)
sz.setContentFromTotal(&sz.Actual)
sz.setContentFromTotal(&sz.Alloc)
}
return got
}
// sizeUpParts adjusts the Content size to hold the Parts layout if present
func (wb *WidgetBase) sizeUpParts() {
if wb.Parts == nil {
return
}
wb.Parts.SizeUp()
sz := &wb.Geom.Size
sz.FitSizeMax(&sz.Actual.Content, wb.Parts.Geom.Size.Actual.Total)
}
func (fr *Frame) SizeUp() {
if fr.Styles.Display == styles.Custom {
fr.SizeUpWidget()
fr.sizeUpChildren()
return
}
if !fr.HasChildren() {
fr.SizeUpWidget() // behave like a widget
return
}
fr.sizeFromStyle()
fr.layout.ScrollSize.SetZero() // we don't know yet
fr.setInitCells()
fr.This.(Layouter).LayoutSpace()
fr.sizeUpChildren() // kids do their own thing
fr.sizeFromChildrenFit(0, SizeUpPass)
if fr.Parts != nil {
fr.Parts.SizeUp() // just to get sizes -- no std role in layout
}
}
// LayoutSpace sets our Space based on Styles and Scroll.
// Other layout types can change this if they want to.
func (fr *Frame) LayoutSpace() {
fr.spaceFromStyle()
fr.Geom.Size.Space.SetAdd(fr.layout.ScrollSize)
}
// sizeUpChildren calls SizeUp on all the children of this node
func (fr *Frame) sizeUpChildren() {
if fr.Styles.Display == styles.Stacked && !fr.LayoutStackTopOnly {
fr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cw.SizeUp()
return tree.Continue
})
return
}
fr.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cw.SizeUp()
return tree.Continue
})
}
// setInitCells sets the initial default assignment of cell indexes
// to each widget, based on layout type.
func (fr *Frame) setInitCells() {
switch {
case fr.Styles.Display == styles.Flex:
if fr.Styles.Wrap {
fr.setInitCellsWrap()
} else {
fr.setInitCellsFlex()
}
case fr.Styles.Display == styles.Stacked:
fr.setInitCellsStacked()
case fr.Styles.Display == styles.Grid:
fr.setInitCellsGrid()
default:
fr.setInitCellsStacked() // whatever
}
fr.layout.initCells()
fr.setGapSizeFromCells()
fr.layout.shapeCheck(fr, "SizeUp")
// fmt.Println(ly, "SzUp Init", ly.Layout.Shape)
}
func (fr *Frame) setGapSizeFromCells() {
li := &fr.layout
li.Gap = fr.Styles.Gap.Dots().Floor()
// note: this is not accurate for flex
li.GapSize.X = max(float32(li.Shape.X-1)*li.Gap.X, 0)
li.GapSize.Y = max(float32(li.Shape.Y-1)*li.Gap.Y, 0)
fr.Geom.Size.InnerSpace = li.GapSize
}
func (fr *Frame) setInitCellsFlex() {
li := &fr.layout
li.MainAxis = math32.Dims(fr.Styles.Direction)
ca := li.MainAxis.Other()
li.Wraps = nil
idx := 0
fr.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
math32.SetPointDim(&cwb.Geom.Cell, li.MainAxis, idx)
math32.SetPointDim(&cwb.Geom.Cell, ca, 0)
idx++
return tree.Continue
})
if idx == 0 {
if DebugSettings.LayoutTrace {
fmt.Println(fr, "no items:", idx)
}
}
math32.SetPointDim(&li.Shape, li.MainAxis, max(idx, 1)) // must be at least 1
math32.SetPointDim(&li.Shape, ca, 1)
}
func (fr *Frame) setInitCellsWrap() {
li := &fr.layout
li.MainAxis = math32.Dims(fr.Styles.Direction)
ni := 0
fr.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
ni++
return tree.Continue
})
if ni == 0 {
li.Shape = image.Point{1, 1}
li.Wraps = nil
li.GapSize.SetZero()
fr.Geom.Size.InnerSpace.SetZero()
if DebugSettings.LayoutTrace {
fmt.Println(fr, "no items:", ni)
}
return
}
nm := max(int(math32.Sqrt(float32(ni))), 1)
nc := max(ni/nm, 1)
for nm*nc < ni {
nm++
}
li.Wraps = make([]int, nc)
sum := 0
for i := range li.Wraps {
n := min(ni-sum, nm)
li.Wraps[i] = n
sum += n
}
fr.setWrapIndexes()
}
// setWrapIndexes sets indexes for Wrap case
func (fr *Frame) setWrapIndexes() {
li := &fr.layout
idx := 0
var maxc image.Point
fr.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
ic := li.wrapIndexToCoord(idx)
cwb.Geom.Cell = ic
if ic.X > maxc.X {
maxc.X = ic.X
}
if ic.Y > maxc.Y {
maxc.Y = ic.Y
}
idx++
return tree.Continue
})
maxc.X++
maxc.Y++
li.Shape = maxc
}
// UpdateStackedVisibility updates the visibility for Stacked layouts
// so the StackTop widget is visible, and others are Invisible.
func (fr *Frame) UpdateStackedVisibility() {
fr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cwb.SetState(i != fr.StackTop, states.Invisible)
cwb.Geom.Cell = image.Point{0, 0}
return tree.Continue
})
}
func (fr *Frame) setInitCellsStacked() {
fr.UpdateStackedVisibility()
fr.layout.Shape = image.Point{1, 1}
}
func (fr *Frame) setInitCellsGrid() {
n := len(fr.Children)
cols := fr.Styles.Columns
if cols == 0 {
cols = int(math32.Sqrt(float32(n)))
}
rows := n / cols
for rows*cols < n {
rows++
}
if rows == 0 || cols == 0 {
fmt.Println(fr, "no rows or cols:", rows, cols)
}
fr.layout.Shape = image.Point{max(cols, 1), max(rows, 1)}
ci := 0
ri := 0
fr.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cwb.Geom.Cell = image.Point{ci, ri}
ci++
cs := cwb.Styles.ColSpan
if cs > 1 {
ci += cs - 1
}
if ci >= cols {
ci = 0
ri++
}
return tree.Continue
})
}
// sizeFromChildrenFit gathers Actual size from kids, and calls LaySetContentFitOverflow
// to update Actual and Internal size based on this.
func (fr *Frame) sizeFromChildrenFit(iter int, pass LayoutPasses) {
ksz := fr.This.(Layouter).SizeFromChildren(iter, SizeDownPass)
fr.laySetContentFitOverflow(ksz, pass)
if DebugSettings.LayoutTrace {
sz := &fr.Geom.Size
fmt.Println(fr, pass, "FromChildren:", ksz, "Content:", sz.Actual.Content, "Internal:", sz.Internal)
}
}
// SizeFromChildren gathers Actual size from kids.
// Different Layout types can alter this to present different Content
// sizes for the layout process, e.g., if Content is sized to fit allocation,
// as in the [Toolbar] and [List] types.
func (fr *Frame) SizeFromChildren(iter int, pass LayoutPasses) math32.Vector2 {
var ksz math32.Vector2
if fr.Styles.Display == styles.Stacked {
ksz = fr.sizeFromChildrenStacked()
} else {
ksz = fr.sizeFromChildrenCells(iter, pass)
}
return ksz
}
// sizeFromChildrenCells for Flex, Grid
func (fr *Frame) sizeFromChildrenCells(iter int, pass LayoutPasses) math32.Vector2 {
// r 0 1 col X = max(X over rows), Y = sum(Y over rows)
// +--+--+
// 0 | | | row X = sum(X over cols), Y = max(Y over cols)
// +--+--+
// 1 | | |
// +--+--+
li := &fr.layout
li.initCells()
fr.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cidx := cwb.Geom.Cell
sz := cwb.Geom.Size.Actual.Total
grw := cwb.Styles.Grow
if pass <= SizeDownPass && iter == 0 && cwb.Styles.GrowWrap {
grw.Set(1, 0)
}
if DebugSettings.LayoutTraceDetail {
fmt.Println("SzUp i:", i, cwb, "cidx:", cidx, "sz:", sz, "grw:", grw)
}
for ma := math32.X; ma <= math32.Y; ma++ { // main axis = X then Y
ca := ma.Other() // cross axis = Y then X
mi := math32.PointDim(cidx, ma) // X, Y
ci := math32.PointDim(cidx, ca) // Y, X
md := li.cell(ma, mi, ci) // X, Y
cd := li.cell(ca, ci, mi) // Y, X
if md == nil || cd == nil {
break
}
msz := sz.Dim(ma) // main axis size dim: X, Y
mx := md.Size.Dim(ma)
mx = max(mx, msz) // Col, max widths of all elements; Row, max heights of all elements
md.Size.SetDim(ma, mx)
sm := cd.Size.Dim(ma)
sm += msz
cd.Size.SetDim(ma, sm) // Row, sum widths of all elements; Col, sum heights of all elements
gsz := grw.Dim(ma)
mx = md.Grow.Dim(ma)
mx = max(mx, gsz)
md.Grow.SetDim(ma, mx)
sm = cd.Grow.Dim(ma)
sm += gsz
cd.Grow.SetDim(ma, sm)
}
return tree.Continue
})
if DebugSettings.LayoutTraceDetail {
fmt.Println(fr, "SizeFromChildren")
fmt.Println(li.String())
}
ksz := li.cellsSize()
return ksz
}
// sizeFromChildrenStacked for stacked case
func (fr *Frame) sizeFromChildrenStacked() math32.Vector2 {
fr.layout.initCells()
kwb := fr.StackTopWidget()
li := &fr.layout
var ksz math32.Vector2
if kwb != nil {
ksz = kwb.Geom.Size.Actual.Total
kgrw := kwb.Styles.Grow
for ma := math32.X; ma <= math32.Y; ma++ { // main axis = X then Y
md := li.cell(ma, 0, 0)
md.Size = ksz
md.Grow = kgrw
}
}
return ksz
}
// 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.
func (wb *WidgetBase) SizeDown(iter int) bool {
prel := wb.updateParentRelSizes()
redo := wb.sizeDownParts(iter)
return prel || redo
}
func (wb *WidgetBase) sizeDownParts(iter int) bool {
if wb.Parts == nil {
return false
}
sz := &wb.Geom.Size
psz := &wb.Parts.Geom.Size
pgrow, _ := wb.growToAllocSize(sz.Actual.Content, sz.Alloc.Content)
psz.Alloc.Total = pgrow // parts = content
psz.setContentFromTotal(&psz.Alloc)
redo := wb.Parts.SizeDown(iter)
if redo && DebugSettings.LayoutTrace {
fmt.Println(wb, "Parts triggered redo")
}
return redo
}
// sizeDownChildren calls SizeDown on the Children.
// The kids must have their Size.Alloc set prior to this, which
// is what Layout type does. Other special widget types can
// do custom layout and call this too.
func (wb *WidgetBase) sizeDownChildren(iter int) bool {
redo := false
wb.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
re := cw.SizeDown(iter)
if re && DebugSettings.LayoutTrace {
fmt.Println(wb, "SizeDownChildren child:", cwb.Name, "triggered redo")
}
redo = redo || re
return tree.Continue
})
return redo
}
// sizeDownChildren calls SizeDown on the Children.
// The kids must have their Size.Alloc set prior to this, which
// is what Layout type does. Other special widget types can
// do custom layout and call this too.
func (fr *Frame) sizeDownChildren(iter int) bool {
if fr.Styles.Display == styles.Stacked && !fr.LayoutStackTopOnly {
redo := false
fr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
re := cw.SizeDown(iter)
if i == fr.StackTop {
redo = redo || re
}
return tree.Continue
})
return redo
}
return fr.WidgetBase.sizeDownChildren(iter)
}
// growToAllocSize returns the potential size that widget could grow,
// for any dimension with a non-zero Grow factor.
// If Grow is < 1, then the size is increased in proportion, but
// any factor > 1 produces a full fill along that dimension.
// Returns true if this resulted in a change.
func (wb *WidgetBase) growToAllocSize(act, alloc math32.Vector2) (math32.Vector2, bool) {
change := false
for d := math32.X; d <= math32.Y; d++ {
grw := wb.Styles.Grow.Dim(d)
allocd := alloc.Dim(d)
actd := act.Dim(d)
if grw > 0 && allocd > actd {
grw := min(1, grw)
nsz := math32.Ceil(actd + grw*(allocd-actd))
styles.SetClampMax(&nsz, wb.Geom.Size.Max.Dim(d))
if nsz != actd {
change = true
}
act.SetDim(d, nsz)
}
}
return act.Ceil(), change
}
func (fr *Frame) SizeDown(iter int) bool {
redo := fr.sizeDownFrame(iter)
if redo && DebugSettings.LayoutTrace {
fmt.Println(fr, "SizeDown redo")
}
return redo
}
// sizeDownFrame is the [Frame] standard SizeDown pass, returning true if another
// iteration is required. It allocates sizes to fit given parent-allocated
// total size.
func (fr *Frame) sizeDownFrame(iter int) bool {
if fr.Styles.Display == styles.Custom {
return fr.sizeDownCustom(iter)
}
if !fr.HasChildren() || !fr.layout.shapeCheck(fr, "SizeDown") {
return fr.WidgetBase.SizeDown(iter) // behave like a widget
}
prel := fr.updateParentRelSizes()
sz := &fr.Geom.Size
styles.SetClampMaxVector(&sz.Alloc.Content, sz.Max) // can't be more than max..
sz.setTotalFromContent(&sz.Alloc)
if DebugSettings.LayoutTrace {
fmt.Println(fr, "Managing Alloc:", sz.Alloc.Content)
}
chg := fr.This.(Layouter).ManageOverflow(iter, true) // this must go first.
wrapped := false
if iter <= 1 && fr.Styles.IsFlexWrap() {
wrapped = fr.sizeDownWrap(iter) // first recompute wrap
if iter == 0 {
wrapped = true // always update
}
}
fr.This.(Layouter).SizeDownSetAllocs(iter)
redo := fr.sizeDownChildren(iter)
if prel || redo || wrapped {
fr.sizeFromChildrenFit(iter, SizeDownPass)
}
fr.sizeDownParts(iter) // no std role, just get sizes
return chg || wrapped || redo || prel
}
// SizeDownSetAllocs is the key SizeDown step that sets the allocations
// in the children, based on our allocation. In the default implementation
// this calls SizeDownGrow if there is extra space to grow, or
// SizeDownAllocActual to set the allocations as they currrently are.
func (fr *Frame) SizeDownSetAllocs(iter int) {
sz := &fr.Geom.Size
extra := sz.Alloc.Content.Sub(sz.Internal) // note: critical to use internal to be accurate
if extra.X > 0 || extra.Y > 0 {
if DebugSettings.LayoutTrace {
fmt.Println(fr, "SizeDown extra:", extra, "Internal:", sz.Internal, "Alloc:", sz.Alloc.Content)
}
fr.sizeDownGrow(iter, extra)
} else {
fr.sizeDownAllocActual(iter) // set allocations as is
}
}
// ManageOverflow uses overflow settings to determine if scrollbars
// are needed (Internal > Alloc). Returns true if size changes as a result.
// If updateSize is false, then the Actual and Alloc sizes are NOT
// updated as a result of change from adding scrollbars
// (generally should be true, but some cases not)
func (fr *Frame) ManageOverflow(iter int, updateSize bool) bool {
sz := &fr.Geom.Size
sbw := math32.Ceil(fr.Styles.ScrollbarWidth.Dots)
change := false
if iter == 0 {
fr.layout.ScrollSize.SetZero()
fr.setScrollsOff()
for d := math32.X; d <= math32.Y; d++ {
if fr.Styles.Overflow.Dim(d) == styles.OverflowScroll {
if !fr.HasScroll[d] {
change = true
}
fr.HasScroll[d] = true
fr.layout.ScrollSize.SetDim(d.Other(), sbw)
}
}
}
for d := math32.X; d <= math32.Y; d++ {
maxSize, visSize, _ := fr.This.(Layouter).ScrollValues(d)
ofd := maxSize - visSize
switch fr.Styles.Overflow.Dim(d) {
// case styles.OverflowVisible:
// note: this shouldn't happen -- just have this in here for monitoring
// fmt.Println(ly, "OverflowVisible ERROR -- shouldn't have overflow:", d, ofd)
case styles.OverflowAuto:
if ofd <= 1 {
if fr.HasScroll[d] {
if DebugSettings.LayoutTrace {
fmt.Println(fr, "turned off scroll", d)
}
change = true
fr.HasScroll[d] = false
fr.layout.ScrollSize.SetDim(d.Other(), 0)
}
continue
}
if !fr.HasScroll[d] {
change = true
}
fr.HasScroll[d] = true
fr.layout.ScrollSize.SetDim(d.Other(), sbw)
if change && DebugSettings.LayoutTrace {
fmt.Println(fr, "OverflowAuto enabling scrollbars for dim for overflow:", d, ofd, "alloc:", sz.Alloc.Content.Dim(d), "internal:", sz.Internal.Dim(d))
}
}
}
fr.This.(Layouter).LayoutSpace() // adds the scroll space
if updateSize {
sz.setTotalFromContent(&sz.Actual)
sz.setContentFromTotal(&sz.Alloc) // alloc is *decreased* from any increase in space
}
if change && DebugSettings.LayoutTrace {
fmt.Println(fr, "ManageOverflow changed")
}
return change
}
// sizeDownGrow grows the element sizes based on total extra and Grow
func (fr *Frame) sizeDownGrow(iter int, extra math32.Vector2) bool {
redo := false
if fr.Styles.Display == styles.Stacked {
redo = fr.sizeDownGrowStacked(iter, extra)
} else {
redo = fr.sizeDownGrowCells(iter, extra)
}
return redo
}
func (fr *Frame) sizeDownGrowCells(iter int, extra math32.Vector2) bool {
redo := false
sz := &fr.Geom.Size
alloc := sz.Alloc.Content.Sub(sz.InnerSpace) // inner is fixed
// todo: use max growth values instead of individual ones to ensure consistency!
li := &fr.layout
if len(li.Cells) == 0 {
slog.Error("unexpected error: layout has not been initialized", "layout", fr.String())
return false
}
fr.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cidx := cwb.Geom.Cell
ksz := &cwb.Geom.Size
grw := cwb.Styles.Grow
if iter == 0 && cwb.Styles.GrowWrap {
grw.Set(1, 0)
}
// if DebugSettings.LayoutTrace {
// fmt.Println("szdn i:", i, kwb, "cidx:", cidx, "sz:", sz, "grw:", grw)
// }
for ma := math32.X; ma <= math32.Y; ma++ { // main axis = X then Y
gr := grw.Dim(ma)
ca := ma.Other() // cross axis = Y then X
exd := extra.Dim(ma) // row.X = extra width for cols; col.Y = extra height for rows in this col
if exd < 0 {
exd = 0
}
mi := math32.PointDim(cidx, ma) // X, Y
ci := math32.PointDim(cidx, ca) // Y, X
md := li.cell(ma, mi, ci) // X, Y
cd := li.cell(ca, ci, mi) // Y, X
if md == nil || cd == nil {
break
}
mx := md.Size.Dim(ma)
asz := mx
gsum := cd.Grow.Dim(ma)
if gsum > 0 && exd > 0 {
if gr > gsum {
fmt.Println(fr, "SizeDownGrowCells error: grow > grow sum:", gr, gsum)
gr = gsum
}
redo = true
asz = math32.Round(mx + exd*(gr/gsum))
styles.SetClampMax(&asz, ksz.Max.Dim(ma))
if asz > math32.Ceil(alloc.Dim(ma))+1 { // bug!
if DebugSettings.LayoutTrace {
fmt.Println(fr, "SizeDownGrowCells error: sub alloc > total to alloc:", asz, alloc.Dim(ma))
fmt.Println("ma:", ma, "mi:", mi, "ci:", ci, "mx:", mx, "gsum:", gsum, "gr:", gr, "ex:", exd, "par act:", sz.Actual.Content.Dim(ma))
fmt.Println(fr.layout.String())
fmt.Println(fr.layout.cellsSize())
}
}
}
if DebugSettings.LayoutTraceDetail {
fmt.Println(cwb, ma, "alloc:", asz, "was act:", sz.Actual.Total.Dim(ma), "mx:", mx, "gsum:", gsum, "gr:", gr, "ex:", exd)
}
ksz.Alloc.Total.SetDim(ma, asz)
}
ksz.setContentFromTotal(&ksz.Alloc)
return tree.Continue
})
return redo
}
func (fr *Frame) sizeDownWrap(iter int) bool {
wrapped := false
li := &fr.layout
sz := &fr.Geom.Size
d := li.MainAxis
alloc := sz.Alloc.Content
gap := li.Gap.Dim(d)
fit := alloc.Dim(d)
if DebugSettings.LayoutTrace {
fmt.Println(fr, "SizeDownWrap fitting into:", d, fit)
}
first := true
var sum float32
var n int
var wraps []int
fr.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
ksz := cwb.Geom.Size.Actual.Total
if first {
n = 1
sum = ksz.Dim(d) + gap
first = false
return tree.Continue
}
if sum+ksz.Dim(d)+gap >= fit {
if DebugSettings.LayoutTraceDetail {
fmt.Println(fr, "wrapped:", i, sum, ksz.Dim(d), fit)
}
wraps = append(wraps, n)
sum = ksz.Dim(d)
n = 1 // this guy is on next line
} else {
sum += ksz.Dim(d) + gap
n++
}
return tree.Continue
})
if n > 0 {
wraps = append(wraps, n)
}
wrapped = false
if len(wraps) != len(li.Wraps) {
wrapped = true
} else {
for i := range wraps {
if wraps[i] != li.Wraps[i] {
wrapped = true
break
}
}
}
if !wrapped {
return false
}
if DebugSettings.LayoutTrace {
fmt.Println(fr, "wrapped:", wraps)
}
li.Wraps = wraps
fr.setWrapIndexes()
li.initCells()
fr.setGapSizeFromCells()
fr.sizeFromChildrenCells(iter, SizeDownPass)
return wrapped
}
func (fr *Frame) sizeDownGrowStacked(iter int, extra math32.Vector2) bool {
// stack just gets everything from us
chg := false
asz := fr.Geom.Size.Alloc.Content
// todo: could actually use the grow factors to decide if growing here?
if fr.LayoutStackTopOnly {
kwb := fr.StackTopWidget()
if kwb != nil {
ksz := &kwb.Geom.Size
if ksz.Alloc.Total != asz {
chg = true
}
ksz.Alloc.Total = asz
ksz.setContentFromTotal(&ksz.Alloc)
}
return chg
}
// note: allocate everyone in case they are flipped to top
// need a new layout if size is actually different
fr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
ksz := &cwb.Geom.Size
if ksz.Alloc.Total != asz {
chg = true
}
ksz.Alloc.Total = asz
ksz.setContentFromTotal(&ksz.Alloc)
return tree.Continue
})
return chg
}
// sizeDownAllocActual sets Alloc to Actual for no-extra case.
func (fr *Frame) sizeDownAllocActual(iter int) {
if fr.Styles.Display == styles.Stacked {
fr.sizeDownAllocActualStacked(iter)
return
}
// todo: wrap needs special case
fr.sizeDownAllocActualCells(iter)
}
// sizeDownAllocActualCells sets Alloc to Actual for no-extra case.
// Note however that due to max sizing for row / column,
// this size can actually be different than original actual.
func (fr *Frame) sizeDownAllocActualCells(iter int) {
fr.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
ksz := &cwb.Geom.Size
cidx := cwb.Geom.Cell
for ma := math32.X; ma <= math32.Y; ma++ { // main axis = X then Y
ca := ma.Other() // cross axis = Y then X
mi := math32.PointDim(cidx, ma) // X, Y
ci := math32.PointDim(cidx, ca) // Y, X
md := fr.layout.cell(ma, mi, ci) // X, Y
asz := md.Size.Dim(ma)
ksz.Alloc.Total.SetDim(ma, asz)
}
ksz.setContentFromTotal(&ksz.Alloc)
return tree.Continue
})
}
func (fr *Frame) sizeDownAllocActualStacked(iter int) {
// stack just gets everything from us
asz := fr.Geom.Size.Actual.Content
// todo: could actually use the grow factors to decide if growing here?
if fr.LayoutStackTopOnly {
kwb := fr.StackTopWidget()
if kwb != nil {
ksz := &kwb.Geom.Size
ksz.Alloc.Total = asz
ksz.setContentFromTotal(&ksz.Alloc)
}
return
}
// note: allocate everyone in case they are flipped to top
// need a new layout if size is actually different
fr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
ksz := &cwb.Geom.Size
ksz.Alloc.Total = asz
ksz.setContentFromTotal(&ksz.Alloc)
return tree.Continue
})
}
func (fr *Frame) sizeDownCustom(iter int) bool {
prel := fr.updateParentRelSizes()
fr.growToAlloc()
sz := &fr.Geom.Size
if DebugSettings.LayoutTrace {
fmt.Println(fr, "Custom Managing Alloc:", sz.Alloc.Content)
}
styles.SetClampMaxVector(&sz.Alloc.Content, sz.Max) // can't be more than max..
// this allocates our full size to each child, same as ActualStacked all case
asz := sz.Actual.Content
fr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
ksz := &cwb.Geom.Size
ksz.Alloc.Total = asz
ksz.setContentFromTotal(&ksz.Alloc)
return tree.Continue
})
redo := fr.sizeDownChildren(iter)
fr.sizeDownParts(iter) // no std role, just get sizes
return prel || redo
}
// sizeFinalUpdateChildrenSizes can optionally be called for layouts
// that dynamically create child elements based on final layout size.
// It ensures that the children are properly sized.
func (fr *Frame) sizeFinalUpdateChildrenSizes() {
fr.SizeUp()
iter := 3 // late stage..
fr.This.(Layouter).SizeDownSetAllocs(iter)
fr.sizeDownChildren(iter)
fr.sizeDownParts(iter) // no std role, just get sizes
}
// 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.
func (wb *WidgetBase) SizeFinal() {
wb.Geom.RelPos.SetZero()
sz := &wb.Geom.Size
sz.Internal = sz.Actual.Content // keep it before we grow
wb.growToAlloc()
wb.styleSizeUpdate() // now that sizes are stable, ensure styling based on size is updated
wb.sizeFinalParts()
sz.setTotalFromContent(&sz.Actual)
}
// growToAlloc grows our Actual size up to current Alloc size
// for any dimension with a non-zero Grow factor.
// If Grow is < 1, then the size is increased in proportion, but
// any factor > 1 produces a full fill along that dimension.
// Returns true if this resulted in a change in our Total size.
func (wb *WidgetBase) growToAlloc() bool {
if (wb.Scene != nil && wb.Scene.hasFlag(sceneContentSizing)) || wb.Styles.GrowWrap {
return false
}
sz := &wb.Geom.Size
act, change := wb.growToAllocSize(sz.Actual.Total, sz.Alloc.Total)
if change {
if DebugSettings.LayoutTrace {
fmt.Println(wb, "GrowToAlloc:", sz.Alloc.Total, "from actual:", sz.Actual.Total)
}
sz.Actual.Total = act // already has max constraint
sz.setContentFromTotal(&sz.Actual)
}
return change
}
// sizeFinalParts adjusts the Content size to hold the Parts Final sizes
func (wb *WidgetBase) sizeFinalParts() {
if wb.Parts == nil {
return
}
wb.Parts.SizeFinal()
sz := &wb.Geom.Size
sz.FitSizeMax(&sz.Actual.Content, wb.Parts.Geom.Size.Actual.Total)
}
func (fr *Frame) SizeFinal() {
if fr.Styles.Display == styles.Custom {
fr.WidgetBase.SizeFinal() // behave like a widget
fr.WidgetBase.sizeFinalChildren()
return
}
if !fr.HasChildren() || !fr.layout.shapeCheck(fr, "SizeFinal") {
fr.WidgetBase.SizeFinal() // behave like a widget
return
}
fr.Geom.RelPos.SetZero()
fr.sizeFinalChildren() // kids do their own thing
fr.sizeFromChildrenFit(0, SizeFinalPass)
fr.growToAlloc()
fr.styleSizeUpdate() // now that sizes are stable, ensure styling based on size is updated
fr.sizeFinalParts()
}
// sizeFinalChildren calls SizeFinal on all the children of this node
func (wb *WidgetBase) sizeFinalChildren() {
wb.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cw.SizeFinal()
return tree.Continue
})
}
// sizeFinalChildren calls SizeFinal on all the children of this node
func (fr *Frame) sizeFinalChildren() {
if fr.Styles.Display == styles.Stacked && !fr.LayoutStackTopOnly {
fr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cw.SizeFinal()
return tree.Continue
})
return
}
fr.WidgetBase.sizeFinalChildren()
}
// styleSizeUpdate updates styling size values for widget and its parent,
// which should be called after these are updated. Returns true if any changed.
func (wb *WidgetBase) styleSizeUpdate() bool {
pwb := wb.parentWidget()
if pwb == nil {
return false
}
if !wb.updateParentRelSizes() {
return false
}
scsz := wb.Scene.SceneGeom.Size
sz := wb.Geom.Size.Alloc.Content
psz := pwb.Geom.Size.Alloc.Content
chg := wb.Styles.UnitContext.SetSizes(float32(scsz.X), float32(scsz.Y), sz.X, sz.Y, psz.X, psz.Y)
if chg {
wb.Styles.ToDots()
}
return chg
}
// Position uses the final sizes to set relative positions within layouts
// according to alignment settings.
func (wb *WidgetBase) Position() {
wb.positionParts()
}
func (wb *WidgetBase) positionWithinAllocMainX(pos math32.Vector2, parJustify, parAlign styles.Aligns) {
sz := &wb.Geom.Size
pos.X += styles.AlignPos(styles.ItemAlign(parJustify, wb.Styles.Justify.Self), sz.Actual.Total.X, sz.Alloc.Total.X)
pos.Y += styles.AlignPos(styles.ItemAlign(parAlign, wb.Styles.Align.Self), sz.Actual.Total.Y, sz.Alloc.Total.Y)
wb.Geom.RelPos = pos
if DebugSettings.LayoutTrace {
fmt.Println(wb, "Position within Main=X:", pos)
}
}
func (wb *WidgetBase) positionWithinAllocMainY(pos math32.Vector2, parJustify, parAlign styles.Aligns) {
sz := &wb.Geom.Size
pos.Y += styles.AlignPos(styles.ItemAlign(parJustify, wb.Styles.Justify.Self), sz.Actual.Total.Y, sz.Alloc.Total.Y)
pos.X += styles.AlignPos(styles.ItemAlign(parAlign, wb.Styles.Align.Self), sz.Actual.Total.X, sz.Alloc.Total.X)
wb.Geom.RelPos = pos
if DebugSettings.LayoutTrace {
fmt.Println(wb, "Position within Main=Y:", pos)
}
}
func (wb *WidgetBase) positionParts() {
if wb.Parts == nil {
return
}
sz := &wb.Geom.Size
pgm := &wb.Parts.Geom
pgm.RelPos.X = styles.AlignPos(wb.Parts.Styles.Justify.Content, pgm.Size.Actual.Total.X, sz.Actual.Content.X)
pgm.RelPos.Y = styles.AlignPos(wb.Parts.Styles.Align.Content, pgm.Size.Actual.Total.Y, sz.Actual.Content.Y)
if DebugSettings.LayoutTrace {
fmt.Println(wb.Parts, "parts align pos:", pgm.RelPos)
}
wb.Parts.This.(Widget).Position()
}
// positionChildren runs Position on the children
func (wb *WidgetBase) positionChildren() {
wb.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cw.Position()
return tree.Continue
})
}
// Position: uses the final sizes to position everything within layouts
// according to alignment settings.
func (fr *Frame) Position() {
if fr.Styles.Display == styles.Custom {
fr.positionFromPos()
fr.positionChildren()
return
}
if !fr.HasChildren() || !fr.layout.shapeCheck(fr, "Position") {
fr.WidgetBase.Position() // behave like a widget
return
}
if fr.Parent == nil {
fr.positionWithinAllocMainY(math32.Vector2{}, fr.Styles.Justify.Items, fr.Styles.Align.Items)
}
fr.ConfigScrolls() // and configure the scrolls
if fr.Styles.Display == styles.Stacked {
fr.positionStacked()
} else {
fr.positionCells()
fr.positionChildren()
}
fr.positionParts()
}
func (fr *Frame) positionCells() {
if fr.Styles.Display == styles.Flex && fr.Styles.Direction == styles.Column {
fr.positionCellsMainY()
return
}
fr.positionCellsMainX()
}
// Main axis = X
func (fr *Frame) positionCellsMainX() {
// todo: can break apart further into Flex rows
gap := fr.layout.Gap
sz := &fr.Geom.Size
if DebugSettings.LayoutTraceDetail {
fmt.Println(fr, "PositionCells Main X, actual:", sz.Actual.Content, "internal:", sz.Internal)
}
var stPos math32.Vector2
stPos.X = styles.AlignPos(fr.Styles.Justify.Content, sz.Internal.X, sz.Actual.Content.X)
stPos.Y = styles.AlignPos(fr.Styles.Align.Content, sz.Internal.Y, sz.Actual.Content.Y)
pos := stPos
var lastSz math32.Vector2
idx := 0
fr.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cidx := cwb.Geom.Cell
if cidx.X == 0 && idx > 0 {
pos.X = stPos.X
pos.Y += lastSz.Y + gap.Y
}
cwb.positionWithinAllocMainX(pos, fr.Styles.Justify.Items, fr.Styles.Align.Items)
alloc := cwb.Geom.Size.Alloc.Total
pos.X += alloc.X + gap.X
lastSz = alloc
idx++
return tree.Continue
})
}
// Main axis = Y
func (fr *Frame) positionCellsMainY() {
gap := fr.layout.Gap
sz := &fr.Geom.Size
if DebugSettings.LayoutTraceDetail {
fmt.Println(fr, "PositionCells, actual", sz.Actual.Content, "internal:", sz.Internal)
}
var lastSz math32.Vector2
var stPos math32.Vector2
stPos.Y = styles.AlignPos(fr.Styles.Justify.Content, sz.Internal.Y, sz.Actual.Content.Y)
stPos.X = styles.AlignPos(fr.Styles.Align.Content, sz.Internal.X, sz.Actual.Content.X)
pos := stPos
idx := 0
fr.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cidx := cwb.Geom.Cell
if cidx.Y == 0 && idx > 0 {
pos.Y = stPos.Y
pos.X += lastSz.X + gap.X
}
cwb.positionWithinAllocMainY(pos, fr.Styles.Justify.Items, fr.Styles.Align.Items)
alloc := cwb.Geom.Size.Alloc.Total
pos.Y += alloc.Y + gap.Y
lastSz = alloc
idx++
return tree.Continue
})
}
func (fr *Frame) positionStacked() {
fr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cwb.Geom.RelPos.SetZero()
if !fr.LayoutStackTopOnly || i == fr.StackTop {
cw.Position()
}
return tree.Continue
})
}
// ApplyScenePos 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).
func (wb *WidgetBase) ApplyScenePos() {
wb.setPosFromParent()
wb.setBBoxes()
}
// setContentPosFromPos sets the Pos.Content position based on current Pos
// plus the BoxSpace position offset.
func (wb *WidgetBase) setContentPosFromPos() {
off := wb.Styles.BoxSpace().Pos().Floor()
wb.Geom.Pos.Content = wb.Geom.Pos.Total.Add(off)
}
func (wb *WidgetBase) setPosFromParent() {
pwb := wb.parentWidget()
var parPos math32.Vector2
if pwb != nil {
parPos = pwb.Geom.Pos.Content.Add(pwb.Geom.Scroll) // critical that parent adds here but not to self
}
wb.Geom.Pos.Total = wb.Geom.RelPos.Add(parPos)
wb.setContentPosFromPos()
if DebugSettings.LayoutTrace {
fmt.Println(wb, "pos:", wb.Geom.Pos.Total, "parPos:", parPos)
}
}
// setBBoxesFromAllocs sets BBox and ContentBBox from Geom.Pos and .Size
// This does NOT intersect with parent content BBox, which is done in SetBBoxes.
// Use this for elements that are dynamically positioned outside of parent BBox.
func (wb *WidgetBase) setBBoxesFromAllocs() {
wb.Geom.TotalBBox = wb.Geom.totalRect()
wb.Geom.ContentBBox = wb.Geom.contentRect()
}
func (wb *WidgetBase) setBBoxes() {
pwb := wb.parentWidget()
var parBB image.Rectangle
if pwb == nil { // scene
sz := &wb.Geom.Size
wb.Geom.TotalBBox = math32.RectFromPosSizeMax(math32.Vector2{}, sz.Alloc.Total)
off := wb.Styles.BoxSpace().Pos().Floor()
wb.Geom.ContentBBox = math32.RectFromPosSizeMax(off, sz.Alloc.Content)
if DebugSettings.LayoutTrace {
fmt.Println(wb, "Total BBox:", wb.Geom.TotalBBox)
fmt.Println(wb, "Content BBox:", wb.Geom.ContentBBox)
}
} else {
parBB = pwb.Geom.ContentBBox
bb := wb.Geom.totalRect()
wb.Geom.TotalBBox = parBB.Intersect(bb)
if DebugSettings.LayoutTrace {
fmt.Println(wb, "Total BBox:", bb, "parBB:", parBB, "BBox:", wb.Geom.TotalBBox)
}
cbb := wb.Geom.contentRect()
wb.Geom.ContentBBox = parBB.Intersect(cbb)
if DebugSettings.LayoutTrace {
fmt.Println(wb, "Content BBox:", cbb, "parBB:", parBB, "BBox:", wb.Geom.ContentBBox)
}
}
wb.applyScenePosParts()
}
func (wb *WidgetBase) applyScenePosParts() {
if wb.Parts == nil {
return
}
wb.Parts.ApplyScenePos()
}
// applyScenePosChildren runs ApplyScenePos on the children
func (wb *WidgetBase) applyScenePosChildren() {
wb.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cw.ApplyScenePos()
return tree.Continue
})
}
// applyScenePosChildren runs ScenePos on the children
func (fr *Frame) applyScenePosChildren() {
if fr.Styles.Display == styles.Stacked && !fr.LayoutStackTopOnly {
fr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cw.ApplyScenePos()
return tree.Continue
})
return
}
fr.WidgetBase.applyScenePosChildren()
}
// ApplyScenePos: scene-based position and final BBox is computed based on
// parents accumulated position and scrollbar position.
// This step can be performed when scrolling after updating Scroll.
func (fr *Frame) ApplyScenePos() {
fr.scrollResetIfNone()
if fr.Styles.Display == styles.Custom {
fr.WidgetBase.ApplyScenePos()
fr.applyScenePosChildren()
fr.PositionScrolls()
fr.applyScenePosParts() // in case they fit inside parent
return
}
// note: ly.Geom.Scroll has the X, Y scrolling offsets, set by Layouter.ScrollChanged function
if !fr.HasChildren() || !fr.layout.shapeCheck(fr, "ScenePos") {
fr.WidgetBase.ApplyScenePos() // behave like a widget
return
}
fr.WidgetBase.ApplyScenePos()
fr.applyScenePosChildren()
fr.PositionScrolls()
fr.applyScenePosParts() // in case they fit inside parent
// otherwise handle separately like scrolls on layout
}
// scrollResetIfNone resets the scroll offsets if there are no scrollbars
func (fr *Frame) scrollResetIfNone() {
for d := math32.X; d <= math32.Y; d++ {
if !fr.HasScroll[d] {
fr.Geom.Scroll.SetDim(d, 0)
}
}
}
// positionFromPos does Custom positioning from style positions.
func (fr *Frame) positionFromPos() {
fr.forVisibleChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cwb.Geom.RelPos.X = cwb.Styles.Pos.X.Dots
cwb.Geom.RelPos.Y = cwb.Styles.Pos.Y.Dots
return tree.Continue
})
}
// DirectRenderDrawBBoxes returns the destination and source bounding boxes
// for RenderDraw call for widgets that do direct rendering.
// The destBBox.Min point can be passed as the dp destination point for Draw
// function, and srcBBox is the source region. Empty flag indicates if either
// of the srcBBox dimensions are <= 0.
func (wb *WidgetBase) DirectRenderDrawBBoxes(srcFullBBox image.Rectangle) (destBBox, srcBBox image.Rectangle, empty bool) {
tbb := wb.Geom.TotalBBox
destBBox = tbb.Add(wb.Scene.SceneGeom.Pos)
srcBBox = srcFullBBox
pos := wb.Geom.Pos.Total.ToPoint()
if pos.X < tbb.Min.X { // scrolled off left
srcBBox.Min.X = tbb.Min.X - pos.X
}
if pos.Y < tbb.Min.Y {
srcBBox.Min.X = tbb.Min.Y - pos.X
}
sz := srcBBox.Size()
if sz.X <= 0 || sz.Y <= 0 {
empty = true
}
return
}
// 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 core
import (
"encoding/json"
"fmt"
"image"
"image/color"
"log"
"log/slog"
"reflect"
"sort"
"strconv"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
)
// List represents a slice value with a list of value widgets and optional index widgets.
// Use [ListBase.BindSelect] to make the list designed for item selection.
type List struct {
ListBase
// ListStyler is an optional styler for list items.
ListStyler ListStyler `copier:"-" json:"-" xml:"-"`
}
// ListStyler is a styling function for custom styling and
// configuration of elements in the list.
type ListStyler func(w Widget, s *styles.Style, row int)
func (ls *List) HasStyler() bool {
return ls.ListStyler != nil
}
func (ls *List) StyleRow(w Widget, idx, fidx int) {
if ls.ListStyler != nil {
ls.ListStyler(w, &w.AsWidget().Styles, idx)
}
}
// note on implementation:
// * ListGrid handles all the layout logic to start with a minimum number of
// rows and then computes the total number visible based on allocated size.
const (
// ListRowProperty is the tree property name for the row of a list element.
ListRowProperty = "ls-row"
// ListColProperty is the tree property name for the column of a list element.
ListColProperty = "ls-col"
)
// Lister is the interface used by [ListBase] to
// support any abstractions needed for different types of lists.
type Lister interface {
tree.Node
// AsListBase returns the base for direct access to relevant fields etc
AsListBase() *ListBase
// RowWidgetNs returns number of widgets per row and
// offset for index label
RowWidgetNs() (nWidgPerRow, idxOff int)
// UpdateSliceSize updates the current size of the slice
// and sets SliceSize if changed.
UpdateSliceSize() int
// UpdateMaxWidths updates the maximum widths per column based
// on estimates from length of strings (for string values)
UpdateMaxWidths()
// SliceIndex returns the logical slice index: si = i + StartIndex,
// the actual value index vi into the slice value (typically = si),
// which can be different if there is an index indirection as in
// tensorcore table.IndexView), and a bool that is true if the
// index is beyond the available data and is thus invisible,
// given the row index provided.
SliceIndex(i int) (si, vi int, invis bool)
// MakeRow adds config for one row at given widget row index.
// Plan must be the StructGrid Plan.
MakeRow(p *tree.Plan, i int)
// StyleValue performs additional value widget styling
StyleValue(w Widget, s *styles.Style, row, col int)
// HasStyler returns whether there is a custom style function.
HasStyler() bool
// StyleRow calls a custom style function on given row (and field)
StyleRow(w Widget, idx, fidx int)
// RowGrabFocus grabs the focus for the first focusable
// widget in given row.
// returns that element or nil if not successful
// note: grid must have already rendered for focus to be grabbed!
RowGrabFocus(row int) *WidgetBase
// NewAt inserts a new blank element at the given index in the slice.
// -1 indicates to insert the element at the end.
NewAt(idx int)
// DeleteAt deletes the element at the given index from the slice.
DeleteAt(idx int)
// MimeDataType returns the data type for mime clipboard
// (copy / paste) data e.g., fileinfo.DataJson
MimeDataType() string
// CopySelectToMime copies selected rows to mime data
CopySelectToMime() mimedata.Mimes
// PasteAssign assigns mime data (only the first one!) to this idx
PasteAssign(md mimedata.Mimes, idx int)
// PasteAtIndex inserts object(s) from mime data at
// (before) given slice index
PasteAtIndex(md mimedata.Mimes, idx int)
}
var _ Lister = &List{}
// ListBase is the base for [List] and [Table] and any other displays
// of array-like data. It automatically computes the number of rows that fit
// within its allocated space, and manages the offset view window into the full
// list of items, and supports row selection, copy / paste, Drag-n-Drop, etc.
// Use [ListBase.BindSelect] to make the list designed for item selection.
type ListBase struct { //core:no-new
Frame
// Slice is the pointer to the slice that we are viewing.
Slice any `set:"-"`
// ShowIndexes is whether to show the indexes of rows or not (default false).
ShowIndexes bool
// MinRows specifies the minimum number of rows to display, to ensure
// at least this amount is displayed.
MinRows int `default:"4"`
// SelectedValue is the current selection value.
// If it is set, it is used as the initially selected value.
SelectedValue any `copier:"-" display:"-" json:"-" xml:"-"`
// SelectedIndex is the index of the currently selected item.
SelectedIndex int `copier:"-" json:"-" xml:"-"`
// InitSelectedIndex is the index of the row to select at the start.
InitSelectedIndex int `copier:"-" json:"-" xml:"-"`
// SelectedIndexes is a list of currently selected slice indexes.
SelectedIndexes map[int]struct{} `set:"-" copier:"-"`
// lastClick is the last row that has been clicked on.
// This is used to prevent erroneous double click events
// from being sent when the user clicks on multiple different
// rows in quick succession.
lastClick int
// normalCursor is the cached cursor to display when there
// is no row being hovered.
normalCursor cursors.Cursor
// currentCursor is the cached cursor that should currently be
// displayed.
currentCursor cursors.Cursor
// sliceUnderlying is the underlying slice value.
sliceUnderlying reflect.Value
// currently hovered row
hoverRow int
// list of currently dragged indexes
draggedIndexes []int
// VisibleRows is the total number of rows visible in allocated display size.
VisibleRows int `set:"-" edit:"-" copier:"-" json:"-" xml:"-"`
// StartIndex is the starting slice index of visible rows.
StartIndex int `set:"-" edit:"-" copier:"-" json:"-" xml:"-"`
// SliceSize is the size of the slice.
SliceSize int `set:"-" edit:"-" copier:"-" json:"-" xml:"-"`
// MakeIter is the iteration through the configuration process,
// which is reset when a new slice type is set.
MakeIter int `set:"-" edit:"-" copier:"-" json:"-" xml:"-"`
// temp idx state for e.g., dnd
tmpIndex int
// elementValue is a [reflect.Value] representation of the underlying element type
// which is used whenever there are no slice elements available
elementValue reflect.Value
// maximum width of value column in chars, if string
maxWidth int
// ReadOnlyKeyNav is whether support key navigation when ReadOnly (default true).
// It uses a capture of up / down events to manipulate selection, not focus.
ReadOnlyKeyNav bool `default:"true"`
// SelectMode is whether to be in select rows mode or editing mode.
SelectMode bool `set:"-" copier:"-" json:"-" xml:"-"`
// ReadOnlyMultiSelect: if list is ReadOnly, default selection mode is to
// choose one row only. If this is true, standard multiple selection logic
// with modifier keys is instead supported.
ReadOnlyMultiSelect bool
// InFocusGrab is a guard for recursive focus grabbing.
InFocusGrab bool `set:"-" edit:"-" copier:"-" json:"-" xml:"-"`
// isArray is whether the slice is actually an array.
isArray bool
// ListGrid is the [ListGrid] widget.
ListGrid *ListGrid `set:"-" edit:"-" copier:"-" json:"-" xml:"-"`
}
func (lb *ListBase) WidgetValue() any { return &lb.Slice }
func (lb *ListBase) Init() {
lb.Frame.Init()
lb.AddContextMenu(lb.contextMenu)
lb.InitSelectedIndex = -1
lb.hoverRow = -1
lb.MinRows = 4
lb.ReadOnlyKeyNav = true
lb.Styler(func(s *styles.Style) {
s.SetAbilities(true, abilities.Clickable, abilities.DoubleClickable, abilities.TripleClickable)
s.SetAbilities(!lb.IsReadOnly(), abilities.Draggable, abilities.Droppable)
s.Cursor = lb.currentCursor
s.Direction = styles.Column
// absorb horizontal here, vertical in view
s.Overflow.X = styles.OverflowAuto
s.Grow.Set(1, 1)
})
if !lb.IsReadOnly() {
lb.On(events.DragStart, func(e events.Event) {
lb.dragStart(e)
})
lb.On(events.DragEnter, func(e events.Event) {
e.SetHandled()
})
lb.On(events.DragLeave, func(e events.Event) {
e.SetHandled()
})
lb.On(events.Drop, func(e events.Event) {
lb.dragDrop(e)
})
lb.On(events.DropDeleteSource, func(e events.Event) {
lb.dropDeleteSource(e)
})
}
lb.FinalStyler(func(s *styles.Style) {
lb.normalCursor = s.Cursor
})
lb.OnFinal(events.KeyChord, func(e events.Event) {
if lb.IsReadOnly() {
if lb.ReadOnlyKeyNav {
lb.keyInputReadOnly(e)
}
} else {
lb.keyInputEditable(e)
}
})
lb.On(events.MouseMove, func(e events.Event) {
row, _, isValid := lb.rowFromEventPos(e)
prevHoverRow := lb.hoverRow
if !isValid {
lb.hoverRow = -1
lb.Styles.Cursor = lb.normalCursor
} else {
lb.hoverRow = row
lb.Styles.Cursor = cursors.Pointer
}
lb.currentCursor = lb.Styles.Cursor
if lb.hoverRow != prevHoverRow {
lb.NeedsRender()
}
})
lb.On(events.MouseDrag, func(e events.Event) {
row, idx, isValid := lb.rowFromEventPos(e)
if !isValid {
return
}
lb.ListGrid.AutoScroll(math32.Vec2(0, float32(idx)))
prevHoverRow := lb.hoverRow
if !isValid {
lb.hoverRow = -1
lb.Styles.Cursor = lb.normalCursor
} else {
lb.hoverRow = row
lb.Styles.Cursor = cursors.Pointer
}
lb.currentCursor = lb.Styles.Cursor
if lb.hoverRow != prevHoverRow {
lb.NeedsRender()
}
})
lb.OnFirst(events.DoubleClick, func(e events.Event) {
row, _, isValid := lb.rowFromEventPos(e)
if !isValid {
return
}
if lb.lastClick != row+lb.StartIndex {
lb.ListGrid.Send(events.Click, e)
e.SetHandled()
}
})
// we must interpret triple click events as double click
// events for rapid cross-row double clicking to work correctly
lb.OnFirst(events.TripleClick, func(e events.Event) {
lb.Send(events.DoubleClick, e)
})
lb.Maker(func(p *tree.Plan) {
ls := lb.This.(Lister)
ls.UpdateSliceSize()
scrollTo := -1
if lb.SelectedValue != nil {
idx, ok := sliceIndexByValue(lb.Slice, lb.SelectedValue)
if ok {
lb.SelectedIndex = idx
scrollTo = lb.SelectedIndex
}
lb.SelectedValue = nil
lb.InitSelectedIndex = -1
} else if lb.InitSelectedIndex >= 0 {
lb.SelectedIndex = lb.InitSelectedIndex
lb.InitSelectedIndex = -1
scrollTo = lb.SelectedIndex
}
if scrollTo >= 0 {
lb.ScrollToIndex(scrollTo)
}
lb.Updater(func() {
lb.UpdateStartIndex()
})
lb.MakeGrid(p, func(p *tree.Plan) {
for i := 0; i < lb.VisibleRows; i++ {
ls.MakeRow(p, i)
}
})
})
}
func (lb *ListBase) SliceIndex(i int) (si, vi int, invis bool) {
si = lb.StartIndex + i
vi = si
invis = si >= lb.SliceSize
return
}
// StyleValue performs additional value widget styling
func (lb *ListBase) StyleValue(w Widget, s *styles.Style, row, col int) {
if lb.maxWidth > 0 {
hv := units.Ch(float32(lb.maxWidth))
s.Min.X.Value = max(s.Min.X.Value, hv.Convert(s.Min.X.Unit, &s.UnitContext).Value)
}
s.SetTextWrap(false)
}
func (lb *ListBase) AsListBase() *ListBase {
return lb
}
func (lb *ListBase) SetSliceBase() {
lb.SelectMode = false
lb.MakeIter = 0
lb.StartIndex = 0
lb.VisibleRows = lb.MinRows
if !lb.IsReadOnly() {
lb.SelectedIndex = -1
}
lb.ResetSelectedIndexes()
lb.This.(Lister).UpdateMaxWidths()
}
// SetSlice sets the source slice that we are viewing.
// This ReMakes the view for this slice if different.
// Note: it is important to at least set an empty slice of
// the desired type at the start to enable initial configuration.
func (lb *ListBase) SetSlice(sl any) *ListBase {
if reflectx.IsNil(reflect.ValueOf(sl)) {
lb.Slice = nil
return lb
}
// TODO: a lot of this garbage needs to be cleaned up.
// New is not working!
newslc := false
if reflect.TypeOf(sl).Kind() != reflect.Pointer { // prevent crash on non-comparable
newslc = true
} else {
newslc = lb.Slice != sl
}
if !newslc {
lb.MakeIter = 0
return lb
}
lb.Slice = sl
lb.sliceUnderlying = reflectx.Underlying(reflect.ValueOf(lb.Slice))
lb.isArray = reflectx.NonPointerType(reflect.TypeOf(sl)).Kind() == reflect.Array
lb.elementValue = reflectx.Underlying(reflectx.SliceElementValue(sl))
lb.SetSliceBase()
return lb
}
// rowFromEventPos returns the widget row, slice index, and
// whether the index is in slice range, for given event position.
func (lb *ListBase) rowFromEventPos(e events.Event) (row, idx int, isValid bool) {
sg := lb.ListGrid
row, _, isValid = sg.indexFromPixel(e.Pos())
if !isValid {
return
}
idx = row + lb.StartIndex
if row < 0 || idx >= lb.SliceSize {
isValid = false
}
return
}
// clickSelectEvent is a helper for processing selection events
// based on a mouse click, which could be a double or triple
// in addition to a regular click.
// Returns false if no further processing should occur,
// because the user clicked outside the range of active rows.
func (lb *ListBase) clickSelectEvent(e events.Event) bool {
row, _, isValid := lb.rowFromEventPos(e)
if !isValid {
e.SetHandled()
} else {
lb.updateSelectRow(row, e.SelectMode())
}
return isValid
}
// BindSelect makes the list a read-only selection list and then
// binds its events to its scene and its current selection index to the given value.
// It will send an [events.Change] event when the user changes the selection row.
func (lb *ListBase) BindSelect(val *int) *ListBase {
lb.SetReadOnly(true)
lb.OnSelect(func(e events.Event) {
*val = lb.SelectedIndex
lb.SendChange(e)
})
lb.OnDoubleClick(func(e events.Event) {
if lb.clickSelectEvent(e) {
*val = lb.SelectedIndex
lb.Scene.sendKey(keymap.Accept, e) // activate OK button
if lb.Scene.Stage.Type == DialogStage {
lb.Scene.Close() // also directly close dialog for value dialogs without OK button
}
}
})
return lb
}
func (lb *ListBase) UpdateMaxWidths() {
lb.maxWidth = 0
ev := lb.elementValue
isString := ev.Type().Kind() == reflect.String && ev.Type() != reflect.TypeFor[icons.Icon]()
if !isString || lb.SliceSize == 0 {
return
}
mxw := 0
for rw := 0; rw < lb.SliceSize; rw++ {
str := reflectx.ToString(lb.sliceElementValue(rw).Interface())
mxw = max(mxw, len(str))
}
lb.maxWidth = mxw
}
// sliceElementValue returns an underlying non-pointer [reflect.Value]
// of slice element at given index or ElementValue if out of range.
func (lb *ListBase) sliceElementValue(si int) reflect.Value {
var val reflect.Value
if si < lb.SliceSize {
val = reflectx.Underlying(lb.sliceUnderlying.Index(si)) // deal with pointer lists
} else {
val = lb.elementValue
}
if !val.IsValid() {
val = lb.elementValue
}
return val
}
func (lb *ListBase) MakeGrid(p *tree.Plan, maker func(p *tree.Plan)) {
tree.AddAt(p, "grid", func(w *ListGrid) {
lb.ListGrid = w
w.Styler(func(s *styles.Style) {
nWidgPerRow, _ := lb.This.(Lister).RowWidgetNs()
w.minRows = lb.MinRows
s.Display = styles.Grid
s.Columns = nWidgPerRow
s.Grow.Set(1, 1)
s.Overflow.Y = styles.OverflowAuto
s.Gap.Set(units.Em(0.5)) // note: match header
s.Align.Items = styles.Center
// baseline mins:
s.Min.X.Ch(20)
s.Min.Y.Em(6)
})
oc := func(e events.Event) {
lb.SetFocus()
row, _, isValid := w.indexFromPixel(e.Pos())
if isValid {
lb.updateSelectRow(row, e.SelectMode())
lb.lastClick = row + lb.StartIndex
}
}
w.OnClick(oc)
w.On(events.ContextMenu, func(e events.Event) {
// we must select the row on right click so that the context menu
// corresponds to the right row
oc(e)
lb.HandleEvent(e)
})
w.Updater(func() {
nWidgPerRow, _ := lb.This.(Lister).RowWidgetNs()
w.Styles.Columns = nWidgPerRow
})
w.Maker(maker)
})
}
func (lb *ListBase) MakeValue(w Value, i int) {
ls := lb.This.(Lister)
wb := w.AsWidget()
wb.SetProperty(ListRowProperty, i)
wb.Styler(func(s *styles.Style) {
if lb.IsReadOnly() {
s.SetAbilities(true, abilities.DoubleClickable)
s.SetAbilities(false, abilities.Hoverable, abilities.Focusable, abilities.Activatable, abilities.TripleClickable)
s.SetReadOnly(true)
}
row, col := lb.widgetIndex(w)
row += lb.StartIndex
ls.StyleValue(w, s, row, col)
if row < lb.SliceSize {
ls.StyleRow(w, row, col)
}
})
wb.OnSelect(func(e events.Event) {
e.SetHandled()
row, _ := lb.widgetIndex(w)
lb.updateSelectRow(row, e.SelectMode())
lb.lastClick = row + lb.StartIndex
})
wb.OnDoubleClick(lb.HandleEvent)
wb.On(events.ContextMenu, lb.HandleEvent)
wb.OnFirst(events.ContextMenu, func(e events.Event) {
wb.Send(events.Select, e) // we must select the row for context menu actions
})
if !lb.IsReadOnly() {
wb.OnInput(lb.HandleEvent)
}
}
func (lb *ListBase) MakeRow(p *tree.Plan, i int) {
ls := lb.This.(Lister)
si, vi, invis := ls.SliceIndex(i)
itxt := strconv.Itoa(i)
val := lb.sliceElementValue(vi)
if lb.ShowIndexes {
lb.MakeGridIndex(p, i, si, itxt, invis)
}
valnm := fmt.Sprintf("value-%s-%s", itxt, reflectx.ShortTypeName(lb.elementValue.Type()))
tree.AddNew(p, valnm, func() Value {
return NewValue(val.Addr().Interface(), "")
}, func(w Value) {
wb := w.AsWidget()
lb.MakeValue(w, i)
if !lb.IsReadOnly() {
wb.OnChange(func(e events.Event) {
lb.This.(Lister).UpdateMaxWidths()
lb.SendChange(e)
})
}
wb.Updater(func() {
wb := w.AsWidget()
_, vi, invis := ls.SliceIndex(i)
val := lb.sliceElementValue(vi)
Bind(val.Addr().Interface(), w)
wb.SetReadOnly(lb.IsReadOnly())
wb.SetState(invis, states.Invisible)
if lb.This.(Lister).HasStyler() {
w.Style()
}
if invis {
wb.SetSelected(false)
}
})
})
}
func (lb *ListBase) MakeGridIndex(p *tree.Plan, i, si int, itxt string, invis bool) {
ls := lb.This.(Lister)
tree.AddAt(p, "index-"+itxt, func(w *Text) {
w.SetProperty(ListRowProperty, i)
w.Styler(func(s *styles.Style) {
s.SetAbilities(true, abilities.DoubleClickable)
s.SetAbilities(!lb.IsReadOnly(), abilities.Draggable, abilities.Droppable)
s.Cursor = cursors.None
nd := math32.Log10(float32(lb.SliceSize))
nd = max(nd, 3)
s.Min.X.Ch(nd + 2)
s.Padding.Right.Dp(4)
s.Text.Align = styles.End
s.Min.Y.Em(1)
s.GrowWrap = false
})
w.OnSelect(func(e events.Event) {
e.SetHandled()
lb.updateSelectRow(i, e.SelectMode())
lb.lastClick = si
})
w.OnDoubleClick(lb.HandleEvent)
w.On(events.ContextMenu, lb.HandleEvent)
if !lb.IsReadOnly() {
w.On(events.DragStart, func(e events.Event) {
lb.dragStart(e)
})
w.On(events.DragEnter, func(e events.Event) {
e.SetHandled()
})
w.On(events.DragLeave, func(e events.Event) {
e.SetHandled()
})
w.On(events.Drop, func(e events.Event) {
lb.dragDrop(e)
})
w.On(events.DropDeleteSource, func(e events.Event) {
lb.dropDeleteSource(e)
})
}
w.Updater(func() {
si, _, invis := ls.SliceIndex(i)
sitxt := strconv.Itoa(si)
w.SetText(sitxt)
w.SetReadOnly(lb.IsReadOnly())
w.SetState(invis, states.Invisible)
if invis {
w.SetSelected(false)
}
})
})
}
// RowWidgetNs returns number of widgets per row and offset for index label
func (lb *ListBase) RowWidgetNs() (nWidgPerRow, idxOff int) {
nWidgPerRow = 2
idxOff = 1
if !lb.ShowIndexes {
nWidgPerRow -= 1
idxOff = 0
}
return
}
// UpdateSliceSize updates and returns the size of the slice
// and sets SliceSize
func (lb *ListBase) UpdateSliceSize() int {
sz := lb.sliceUnderlying.Len()
lb.SliceSize = sz
return sz
}
// widgetIndex returns the row and column indexes for given widget,
// from the properties set during construction.
func (lb *ListBase) widgetIndex(w Widget) (row, col int) {
if rwi := w.AsTree().Property(ListRowProperty); rwi != nil {
row = rwi.(int)
}
if cli := w.AsTree().Property(ListColProperty); cli != nil {
col = cli.(int)
}
return
}
// UpdateStartIndex updates StartIndex to fit current view
func (lb *ListBase) UpdateStartIndex() {
sz := lb.This.(Lister).UpdateSliceSize()
if sz > lb.VisibleRows {
lastSt := sz - lb.VisibleRows
lb.StartIndex = min(lastSt, lb.StartIndex)
lb.StartIndex = max(0, lb.StartIndex)
} else {
lb.StartIndex = 0
}
}
// updateScroll updates the scroll value
func (lb *ListBase) updateScroll() {
sg := lb.ListGrid
if sg == nil {
return
}
sg.updateScroll(lb.StartIndex)
}
// newAtRow inserts a new blank element at the given display row.
func (lb *ListBase) newAtRow(row int) {
lb.This.(Lister).NewAt(lb.StartIndex + row)
}
// NewAt inserts a new blank element at the given index in the slice.
// -1 indicates to insert the element at the end.
func (lb *ListBase) NewAt(idx int) {
if lb.isArray {
return
}
lb.NewAtSelect(idx)
reflectx.SliceNewAt(lb.Slice, idx)
if idx < 0 {
idx = lb.SliceSize
}
lb.This.(Lister).UpdateSliceSize()
lb.SelectIndexEvent(idx, events.SelectOne)
lb.UpdateChange()
lb.IndexGrabFocus(idx)
}
// deleteAtRow deletes the element at the given display row.
func (lb *ListBase) deleteAtRow(row int) {
lb.This.(Lister).DeleteAt(lb.StartIndex + row)
}
// NewAtSelect updates the selected rows based on
// inserting a new element at the given index.
func (lb *ListBase) NewAtSelect(i int) {
sl := lb.SelectedIndexesList(false) // ascending
lb.ResetSelectedIndexes()
for _, ix := range sl {
if ix >= i {
ix++
}
lb.SelectedIndexes[ix] = struct{}{}
}
}
// DeleteAtSelect updates the selected rows based on
// deleting the element at the given index.
func (lb *ListBase) DeleteAtSelect(i int) {
sl := lb.SelectedIndexesList(true) // desscending
lb.ResetSelectedIndexes()
for _, ix := range sl {
switch {
case ix == i:
continue
case ix > i:
ix--
}
lb.SelectedIndexes[ix] = struct{}{}
}
}
// DeleteAt deletes the element at the given index from the slice.
func (lb *ListBase) DeleteAt(i int) {
if lb.isArray {
return
}
if i < 0 || i >= lb.SliceSize {
return
}
lb.DeleteAtSelect(i)
reflectx.SliceDeleteAt(lb.Slice, i)
lb.This.(Lister).UpdateSliceSize()
lb.UpdateChange()
}
func (lb *ListBase) MakeToolbar(p *tree.Plan) {
if reflectx.IsNil(reflect.ValueOf(lb.Slice)) {
return
}
if lb.isArray || lb.IsReadOnly() {
return
}
tree.Add(p, func(w *Button) {
w.SetText("Add").SetIcon(icons.Add).SetTooltip("add a new element to the slice").
OnClick(func(e events.Event) {
lb.This.(Lister).NewAt(-1)
})
})
}
////////////////////////////////////////////////////////////
// Row access methods
// NOTE: row = physical GUI display row, idx = slice index
// not the same!
// sliceValue returns value interface at given slice index.
func (lb *ListBase) sliceValue(idx int) any {
if idx < 0 || idx >= lb.SliceSize {
fmt.Printf("core.ListBase: slice index out of range: %v\n", idx)
return nil
}
val := reflectx.UnderlyingPointer(lb.sliceUnderlying.Index(idx)) // deal with pointer lists
vali := val.Interface()
return vali
}
// IsRowInBounds returns true if disp row is in bounds
func (lb *ListBase) IsRowInBounds(row int) bool {
return row >= 0 && row < lb.VisibleRows
}
// rowFirstWidget returns the first widget for given row (could be index or
// not) -- false if out of range
func (lb *ListBase) rowFirstWidget(row int) (*WidgetBase, bool) {
if !lb.ShowIndexes {
return nil, false
}
if !lb.IsRowInBounds(row) {
return nil, false
}
nWidgPerRow, _ := lb.This.(Lister).RowWidgetNs()
sg := lb.ListGrid
w := sg.Children[row*nWidgPerRow].(Widget).AsWidget()
return w, true
}
// RowGrabFocus grabs the focus for the first focusable widget
// in given row. returns that element or nil if not successful
// note: grid must have already rendered for focus to be grabbed!
func (lb *ListBase) RowGrabFocus(row int) *WidgetBase {
if !lb.IsRowInBounds(row) || lb.InFocusGrab { // range check
return nil
}
nWidgPerRow, idxOff := lb.This.(Lister).RowWidgetNs()
ridx := nWidgPerRow * row
sg := lb.ListGrid
w := sg.Child(ridx + idxOff).(Widget).AsWidget()
if w.StateIs(states.Focused) {
return w
}
lb.InFocusGrab = true
w.SetFocus()
lb.InFocusGrab = false
return w
}
// IndexGrabFocus grabs the focus for the first focusable widget
// in given idx. returns that element or nil if not successful.
func (lb *ListBase) IndexGrabFocus(idx int) *WidgetBase {
lb.ScrollToIndex(idx)
return lb.This.(Lister).RowGrabFocus(idx - lb.StartIndex)
}
// indexPos returns center of window position of index label for idx (ContextMenuPos)
func (lb *ListBase) indexPos(idx int) image.Point {
row := idx - lb.StartIndex
if row < 0 {
row = 0
}
if row > lb.VisibleRows-1 {
row = lb.VisibleRows - 1
}
var pos image.Point
w, ok := lb.rowFirstWidget(row)
if ok {
pos = w.ContextMenuPos(nil)
}
return pos
}
// rowFromPos returns the row that contains given vertical position, false if not found
func (lb *ListBase) rowFromPos(posY int) (int, bool) {
// todo: could optimize search to approx loc, and search up / down from there
for rw := 0; rw < lb.VisibleRows; rw++ {
w, ok := lb.rowFirstWidget(rw)
if ok {
if w.Geom.TotalBBox.Min.Y < posY && posY < w.Geom.TotalBBox.Max.Y {
return rw, true
}
}
}
return -1, false
}
// indexFromPos returns the idx that contains given vertical position, false if not found
func (lb *ListBase) indexFromPos(posY int) (int, bool) {
row, ok := lb.rowFromPos(posY)
if !ok {
return -1, false
}
return row + lb.StartIndex, true
}
// ScrollToIndexNoUpdate ensures that given slice idx is visible
// by scrolling display as needed.
// This version does not update the slicegrid.
// Just computes the StartIndex and updates the scrollbar
func (lb *ListBase) ScrollToIndexNoUpdate(idx int) bool {
if lb.VisibleRows == 0 {
return false
}
if idx < lb.StartIndex {
lb.StartIndex = idx
lb.StartIndex = max(0, lb.StartIndex)
lb.updateScroll()
return true
}
if idx >= lb.StartIndex+lb.VisibleRows {
lb.StartIndex = idx - (lb.VisibleRows - 4)
lb.StartIndex = max(0, lb.StartIndex)
lb.updateScroll()
return true
}
return false
}
// ScrollToIndex ensures that given slice idx is visible
// by scrolling display as needed.
func (lb *ListBase) ScrollToIndex(idx int) bool {
update := lb.ScrollToIndexNoUpdate(idx)
if update {
lb.Update()
}
return update
}
// sliceIndexByValue searches for first index that contains given value in slice;
// returns false if not found
func sliceIndexByValue(slc any, fldVal any) (int, bool) {
svnp := reflectx.NonPointerValue(reflect.ValueOf(slc))
sz := svnp.Len()
for idx := 0; idx < sz; idx++ {
rval := reflectx.NonPointerValue(svnp.Index(idx))
if rval.Interface() == fldVal {
return idx, true
}
}
return -1, false
}
// moveDown moves the selection down to next row, using given select mode
// (from keyboard modifiers) -- returns newly selected row or -1 if failed
func (lb *ListBase) moveDown(selMode events.SelectModes) int {
if lb.SelectedIndex >= lb.SliceSize-1 {
lb.SelectedIndex = lb.SliceSize - 1
return -1
}
lb.SelectedIndex++
lb.SelectIndexEvent(lb.SelectedIndex, selMode)
return lb.SelectedIndex
}
// moveDownEvent moves the selection down to next row, using given select
// mode (from keyboard modifiers) -- and emits select event for newly selected
// row
func (lb *ListBase) moveDownEvent(selMode events.SelectModes) int {
nidx := lb.moveDown(selMode)
if nidx >= 0 {
lb.ScrollToIndex(nidx)
lb.Send(events.Select) // todo: need to do this for the item?
}
return nidx
}
// moveUp moves the selection up to previous idx, using given select mode
// (from keyboard modifiers) -- returns newly selected idx or -1 if failed
func (lb *ListBase) moveUp(selMode events.SelectModes) int {
if lb.SelectedIndex <= 0 {
lb.SelectedIndex = 0
return -1
}
lb.SelectedIndex--
lb.SelectIndexEvent(lb.SelectedIndex, selMode)
return lb.SelectedIndex
}
// moveUpEvent moves the selection up to previous idx, using given select
// mode (from keyboard modifiers) -- and emits select event for newly selected idx
func (lb *ListBase) moveUpEvent(selMode events.SelectModes) int {
nidx := lb.moveUp(selMode)
if nidx >= 0 {
lb.ScrollToIndex(nidx)
lb.Send(events.Select)
}
return nidx
}
// movePageDown moves the selection down to next page, using given select mode
// (from keyboard modifiers) -- returns newly selected idx or -1 if failed
func (lb *ListBase) movePageDown(selMode events.SelectModes) int {
if lb.SelectedIndex >= lb.SliceSize-1 {
lb.SelectedIndex = lb.SliceSize - 1
return -1
}
lb.SelectedIndex += lb.VisibleRows
lb.SelectedIndex = min(lb.SelectedIndex, lb.SliceSize-1)
lb.SelectIndexEvent(lb.SelectedIndex, selMode)
return lb.SelectedIndex
}
// movePageDownEvent moves the selection down to next page, using given select
// mode (from keyboard modifiers) -- and emits select event for newly selected idx
func (lb *ListBase) movePageDownEvent(selMode events.SelectModes) int {
nidx := lb.movePageDown(selMode)
if nidx >= 0 {
lb.ScrollToIndex(nidx)
lb.Send(events.Select)
}
return nidx
}
// movePageUp moves the selection up to previous page, using given select mode
// (from keyboard modifiers) -- returns newly selected idx or -1 if failed
func (lb *ListBase) movePageUp(selMode events.SelectModes) int {
if lb.SelectedIndex <= 0 {
lb.SelectedIndex = 0
return -1
}
lb.SelectedIndex -= lb.VisibleRows
lb.SelectedIndex = max(0, lb.SelectedIndex)
lb.SelectIndexEvent(lb.SelectedIndex, selMode)
return lb.SelectedIndex
}
// movePageUpEvent moves the selection up to previous page, using given select
// mode (from keyboard modifiers) -- and emits select event for newly selected idx
func (lb *ListBase) movePageUpEvent(selMode events.SelectModes) int {
nidx := lb.movePageUp(selMode)
if nidx >= 0 {
lb.ScrollToIndex(nidx)
lb.Send(events.Select)
}
return nidx
}
//////////////////////////////////////////////////////////
// Selection: user operates on the index labels
// updateSelectRow updates the selection for the given row
func (lb *ListBase) updateSelectRow(row int, selMode events.SelectModes) {
idx := row + lb.StartIndex
if row < 0 || idx >= lb.SliceSize {
return
}
sel := !lb.indexIsSelected(idx)
lb.updateSelectIndex(idx, sel, selMode)
}
// updateSelectIndex updates the selection for the given index
func (lb *ListBase) updateSelectIndex(idx int, sel bool, selMode events.SelectModes) {
if lb.IsReadOnly() && !lb.ReadOnlyMultiSelect {
lb.unselectAllIndexes()
if sel || lb.SelectedIndex == idx {
lb.SelectedIndex = idx
lb.SelectIndex(idx)
}
lb.Send(events.Select)
lb.Restyle()
} else {
lb.SelectIndexEvent(idx, selMode)
}
}
// indexIsSelected returns the selected status of given slice index
func (lb *ListBase) indexIsSelected(idx int) bool {
if lb.IsReadOnly() && !lb.ReadOnlyMultiSelect {
return idx == lb.SelectedIndex
}
_, ok := lb.SelectedIndexes[idx]
return ok
}
func (lb *ListBase) ResetSelectedIndexes() {
lb.SelectedIndexes = make(map[int]struct{})
}
// SelectedIndexesList returns list of selected indexes,
// sorted either ascending or descending
func (lb *ListBase) SelectedIndexesList(descendingSort bool) []int {
rws := make([]int, len(lb.SelectedIndexes))
i := 0
for r := range lb.SelectedIndexes {
if r >= lb.SliceSize { // double safety check at this point
delete(lb.SelectedIndexes, r)
rws = rws[:len(rws)-1]
continue
}
rws[i] = r
i++
}
if descendingSort {
sort.Slice(rws, func(i, j int) bool {
return rws[i] > rws[j]
})
} else {
sort.Slice(rws, func(i, j int) bool {
return rws[i] < rws[j]
})
}
return rws
}
// SelectIndex selects given idx (if not already selected) -- updates select
// status of index label
func (lb *ListBase) SelectIndex(idx int) {
lb.SelectedIndexes[idx] = struct{}{}
}
// unselectIndex unselects given idx (if selected)
func (lb *ListBase) unselectIndex(idx int) {
if lb.indexIsSelected(idx) {
delete(lb.SelectedIndexes, idx)
}
}
// unselectAllIndexes unselects all selected idxs
func (lb *ListBase) unselectAllIndexes() {
lb.ResetSelectedIndexes()
}
// selectAllIndexes selects all idxs
func (lb *ListBase) selectAllIndexes() {
lb.unselectAllIndexes()
lb.SelectedIndexes = make(map[int]struct{}, lb.SliceSize)
for idx := 0; idx < lb.SliceSize; idx++ {
lb.SelectedIndexes[idx] = struct{}{}
}
lb.NeedsRender()
}
// SelectIndexEvent is called when a select event has been received (e.g., a
// mouse click) -- translates into selection updates -- gets selection mode
// from mouse event (ExtendContinuous, ExtendOne)
func (lb *ListBase) SelectIndexEvent(idx int, mode events.SelectModes) {
if mode == events.NoSelect {
return
}
idx = min(idx, lb.SliceSize-1)
if idx < 0 {
lb.ResetSelectedIndexes()
return
}
// row := idx - sv.StartIndex // note: could be out of bounds
switch mode {
case events.SelectOne:
if lb.indexIsSelected(idx) {
if len(lb.SelectedIndexes) > 1 {
lb.unselectAllIndexes()
}
lb.SelectedIndex = idx
lb.SelectIndex(idx)
lb.IndexGrabFocus(idx)
} else {
lb.unselectAllIndexes()
lb.SelectedIndex = idx
lb.SelectIndex(idx)
lb.IndexGrabFocus(idx)
}
lb.Send(events.Select) // sv.SelectedIndex)
case events.ExtendContinuous:
if len(lb.SelectedIndexes) == 0 {
lb.SelectedIndex = idx
lb.SelectIndex(idx)
lb.IndexGrabFocus(idx)
lb.Send(events.Select) // sv.SelectedIndex)
} else {
minIndex := -1
maxIndex := 0
for r := range lb.SelectedIndexes {
if minIndex < 0 {
minIndex = r
} else {
minIndex = min(minIndex, r)
}
maxIndex = max(maxIndex, r)
}
cidx := idx
lb.SelectedIndex = idx
lb.SelectIndex(idx)
if idx < minIndex {
for cidx < minIndex {
r := lb.moveDown(events.SelectQuiet) // just select
cidx = r
}
} else if idx > maxIndex {
for cidx > maxIndex {
r := lb.moveUp(events.SelectQuiet) // just select
cidx = r
}
}
lb.IndexGrabFocus(idx)
lb.Send(events.Select) // sv.SelectedIndex)
}
case events.ExtendOne:
if lb.indexIsSelected(idx) {
lb.unselectIndexEvent(idx)
lb.Send(events.Select) // sv.SelectedIndex)
} else {
lb.SelectedIndex = idx
lb.SelectIndex(idx)
lb.IndexGrabFocus(idx)
lb.Send(events.Select) // sv.SelectedIndex)
}
case events.Unselect:
lb.SelectedIndex = idx
lb.unselectIndexEvent(idx)
case events.SelectQuiet:
lb.SelectedIndex = idx
lb.SelectIndex(idx)
case events.UnselectQuiet:
lb.SelectedIndex = idx
lb.unselectIndex(idx)
}
lb.Restyle()
}
// unselectIndexEvent unselects this idx (if selected) -- and emits a signal
func (lb *ListBase) unselectIndexEvent(idx int) {
if lb.indexIsSelected(idx) {
lb.unselectIndex(idx)
}
}
///////////////////////////////////////////////////
// Copy / Cut / Paste
// mimeDataIndex adds mimedata for given idx: an application/json of the struct
func (lb *ListBase) mimeDataIndex(md *mimedata.Mimes, idx int) {
val := lb.sliceValue(idx)
b, err := json.MarshalIndent(val, "", " ")
if err == nil {
*md = append(*md, &mimedata.Data{Type: fileinfo.DataJson, Data: b})
} else {
log.Printf("ListBase MimeData JSON Marshall error: %v\n", err)
}
}
// fromMimeData creates a slice of structs from mime data
func (lb *ListBase) fromMimeData(md mimedata.Mimes) []any {
svtyp := lb.sliceUnderlying.Type()
sl := make([]any, 0, len(md))
for _, d := range md {
if d.Type == fileinfo.DataJson {
nval := reflect.New(svtyp.Elem()).Interface()
err := json.Unmarshal(d.Data, nval)
if err == nil {
sl = append(sl, nval)
} else {
log.Printf("ListBase FromMimeData: JSON load error: %v\n", err)
}
}
}
return sl
}
// MimeDataType returns the data type for mime clipboard (copy / paste) data
// e.g., fileinfo.DataJson
func (lb *ListBase) MimeDataType() string {
return fileinfo.DataJson
}
// CopySelectToMime copies selected rows to mime data
func (lb *ListBase) CopySelectToMime() mimedata.Mimes {
nitms := len(lb.SelectedIndexes)
if nitms == 0 {
return nil
}
ixs := lb.SelectedIndexesList(false) // ascending
md := make(mimedata.Mimes, 0, nitms)
for _, i := range ixs {
lb.mimeDataIndex(&md, i)
}
return md
}
// copyIndexes copies selected idxs to system.Clipboard, optionally resetting the selection
func (lb *ListBase) copyIndexes(reset bool) { //types:add
nitms := len(lb.SelectedIndexes)
if nitms == 0 {
return
}
md := lb.This.(Lister).CopySelectToMime()
if md != nil {
lb.Clipboard().Write(md)
}
if reset {
lb.unselectAllIndexes()
}
}
// cutIndexes copies selected indexes to system.Clipboard and deletes selected indexes
func (lb *ListBase) cutIndexes() { //types:add
if len(lb.SelectedIndexes) == 0 {
return
}
lb.copyIndexes(false)
ixs := lb.SelectedIndexesList(true) // descending sort
idx := ixs[0]
lb.unselectAllIndexes()
for _, i := range ixs {
lb.This.(Lister).DeleteAt(i)
}
lb.SendChange()
lb.SelectIndexEvent(idx, events.SelectOne)
lb.Update()
}
// pasteIndex pastes clipboard at given idx
func (lb *ListBase) pasteIndex(idx int) { //types:add
lb.tmpIndex = idx
dt := lb.This.(Lister).MimeDataType()
md := lb.Clipboard().Read([]string{dt})
if md != nil {
lb.pasteMenu(md, lb.tmpIndex)
}
}
// makePasteMenu makes the menu of options for paste events
func (lb *ListBase) makePasteMenu(m *Scene, md mimedata.Mimes, idx int, mod events.DropMods, fun func()) {
ls := lb.This.(Lister)
if mod == events.DropCopy {
NewButton(m).SetText("Assign to").OnClick(func(e events.Event) {
ls.PasteAssign(md, idx)
if fun != nil {
fun()
}
})
}
NewButton(m).SetText("Insert before").OnClick(func(e events.Event) {
ls.PasteAtIndex(md, idx)
if fun != nil {
fun()
}
})
NewButton(m).SetText("Insert after").OnClick(func(e events.Event) {
ls.PasteAtIndex(md, idx+1)
if fun != nil {
fun()
}
})
NewButton(m).SetText("Cancel")
}
// pasteMenu performs a paste from the clipboard using given data -- pops up
// a menu to determine what specifically to do
func (lb *ListBase) pasteMenu(md mimedata.Mimes, idx int) {
lb.unselectAllIndexes()
mf := func(m *Scene) {
lb.makePasteMenu(m, md, idx, events.DropCopy, nil)
}
pos := lb.indexPos(idx)
NewMenu(mf, lb.This.(Widget), pos).Run()
}
// PasteAssign assigns mime data (only the first one!) to this idx
func (lb *ListBase) PasteAssign(md mimedata.Mimes, idx int) {
sl := lb.fromMimeData(md)
if len(sl) == 0 {
return
}
ns := sl[0]
lb.sliceUnderlying.Index(idx).Set(reflect.ValueOf(ns).Elem())
lb.UpdateChange()
}
// PasteAtIndex inserts object(s) from mime data at (before) given slice index
func (lb *ListBase) PasteAtIndex(md mimedata.Mimes, idx int) {
sl := lb.fromMimeData(md)
if len(sl) == 0 {
return
}
svl := reflect.ValueOf(lb.Slice)
svnp := lb.sliceUnderlying
for _, ns := range sl {
sz := svnp.Len()
svnp = reflect.Append(svnp, reflect.ValueOf(ns).Elem())
svl.Elem().Set(svnp)
if idx >= 0 && idx < sz {
reflect.Copy(svnp.Slice(idx+1, sz+1), svnp.Slice(idx, sz))
svnp.Index(idx).Set(reflect.ValueOf(ns).Elem())
svl.Elem().Set(svnp)
}
idx++
}
lb.sliceUnderlying = reflectx.NonPointerValue(reflect.ValueOf(lb.Slice)) // need to update after changes
lb.SendChange()
lb.SelectIndexEvent(idx, events.SelectOne)
lb.Update()
}
// duplicate copies selected items and inserts them after current selection --
// return idx of start of duplicates if successful, else -1
func (lb *ListBase) duplicate() int { //types:add
nitms := len(lb.SelectedIndexes)
if nitms == 0 {
return -1
}
ixs := lb.SelectedIndexesList(true) // descending sort -- last first
pasteAt := ixs[0]
lb.copyIndexes(true)
dt := lb.This.(Lister).MimeDataType()
md := lb.Clipboard().Read([]string{dt})
lb.This.(Lister).PasteAtIndex(md, pasteAt)
return pasteAt
}
//////////////////////////////////////////////////////////////////////////////
// Drag-n-Drop
// selectRowIfNone selects the row the mouse is on if there
// are no currently selected items. Returns false if no valid mouse row.
func (lb *ListBase) selectRowIfNone(e events.Event) bool {
nitms := len(lb.SelectedIndexes)
if nitms > 0 {
return true
}
row, _, isValid := lb.ListGrid.indexFromPixel(e.Pos())
if !isValid {
return false
}
lb.updateSelectRow(row, e.SelectMode())
return true
}
// mousePosInGrid returns true if the event mouse position is
// located within the slicegrid.
func (lb *ListBase) mousePosInGrid(e events.Event) bool {
return lb.ListGrid.mousePosInGrid(e.Pos())
}
func (lb *ListBase) dragStart(e events.Event) {
if !lb.selectRowIfNone(e) || !lb.mousePosInGrid(e) {
return
}
ixs := lb.SelectedIndexesList(false) // ascending
if len(ixs) == 0 {
return
}
md := lb.This.(Lister).CopySelectToMime()
w, ok := lb.rowFirstWidget(ixs[0] - lb.StartIndex)
if ok {
lb.Scene.Events.dragStart(w, md, e)
e.SetHandled()
// } else {
// fmt.Println("List DND programmer error")
}
}
func (lb *ListBase) dragDrop(e events.Event) {
de := e.(*events.DragDrop)
if de.Data == nil {
return
}
pos := de.Pos()
idx, ok := lb.indexFromPos(pos.Y)
if ok {
// sv.DraggedIndexes = nil
lb.tmpIndex = idx
lb.saveDraggedIndexes(idx)
md := de.Data.(mimedata.Mimes)
mf := func(m *Scene) {
lb.Scene.Events.dragMenuAddModText(m, de.DropMod)
lb.makePasteMenu(m, md, idx, de.DropMod, func() {
lb.dropFinalize(de)
})
}
pos := lb.indexPos(lb.tmpIndex)
NewMenu(mf, lb.This.(Widget), pos).Run()
}
}
// dropFinalize is called to finalize Drop actions on the Source node.
// Only relevant for DropMod == DropMove.
func (lb *ListBase) dropFinalize(de *events.DragDrop) {
lb.NeedsLayout()
lb.unselectAllIndexes()
lb.Scene.Events.dropFinalize(de) // sends DropDeleteSource to Source
}
// dropDeleteSource handles delete source event for DropMove case
func (lb *ListBase) dropDeleteSource(e events.Event) {
sort.Slice(lb.draggedIndexes, func(i, j int) bool {
return lb.draggedIndexes[i] > lb.draggedIndexes[j]
})
idx := lb.draggedIndexes[0]
for _, i := range lb.draggedIndexes {
lb.This.(Lister).DeleteAt(i)
}
lb.draggedIndexes = nil
lb.SelectIndexEvent(idx, events.SelectOne)
}
// saveDraggedIndexes saves selectedindexes into dragged indexes
// taking into account insertion at idx
func (lb *ListBase) saveDraggedIndexes(idx int) {
sz := len(lb.SelectedIndexes)
if sz == 0 {
lb.draggedIndexes = nil
return
}
ixs := lb.SelectedIndexesList(false) // ascending
lb.draggedIndexes = make([]int, len(ixs))
for i, ix := range ixs {
if ix > idx {
lb.draggedIndexes[i] = ix + sz // make room for insertion
} else {
lb.draggedIndexes[i] = ix
}
}
}
func (lb *ListBase) contextMenu(m *Scene) {
if lb.IsReadOnly() || lb.isArray {
NewButton(m).SetText("Copy").SetIcon(icons.Copy).OnClick(func(e events.Event) {
lb.copyIndexes(true)
})
NewSeparator(m)
NewButton(m).SetText("Toggle indexes").SetIcon(icons.Numbers).OnClick(func(e events.Event) {
lb.ShowIndexes = !lb.ShowIndexes
lb.Update()
})
return
}
NewButton(m).SetText("Add row").SetIcon(icons.Add).OnClick(func(e events.Event) {
lb.newAtRow((lb.SelectedIndex - lb.StartIndex) + 1)
})
NewButton(m).SetText("Delete row").SetIcon(icons.Delete).OnClick(func(e events.Event) {
lb.deleteAtRow(lb.SelectedIndex - lb.StartIndex)
})
NewSeparator(m)
NewButton(m).SetText("Copy").SetIcon(icons.Copy).OnClick(func(e events.Event) {
lb.copyIndexes(true)
})
NewButton(m).SetText("Cut").SetIcon(icons.Cut).OnClick(func(e events.Event) {
lb.cutIndexes()
})
NewButton(m).SetText("Paste").SetIcon(icons.Paste).OnClick(func(e events.Event) {
lb.pasteIndex(lb.SelectedIndex)
})
NewButton(m).SetText("Duplicate").SetIcon(icons.Copy).OnClick(func(e events.Event) {
lb.duplicate()
})
NewSeparator(m)
NewButton(m).SetText("Toggle indexes").SetIcon(icons.Numbers).OnClick(func(e events.Event) {
lb.ShowIndexes = !lb.ShowIndexes
lb.Update()
})
}
// keyInputNav supports multiple selection navigation keys
func (lb *ListBase) keyInputNav(kt events.Event) {
kf := keymap.Of(kt.KeyChord())
selMode := events.SelectModeBits(kt.Modifiers())
if selMode == events.SelectOne {
if lb.SelectMode {
selMode = events.ExtendContinuous
}
}
switch kf {
case keymap.CancelSelect:
lb.unselectAllIndexes()
lb.SelectMode = false
kt.SetHandled()
case keymap.MoveDown:
lb.moveDownEvent(selMode)
kt.SetHandled()
case keymap.MoveUp:
lb.moveUpEvent(selMode)
kt.SetHandled()
case keymap.PageDown:
lb.movePageDownEvent(selMode)
kt.SetHandled()
case keymap.PageUp:
lb.movePageUpEvent(selMode)
kt.SetHandled()
case keymap.SelectMode:
lb.SelectMode = !lb.SelectMode
kt.SetHandled()
case keymap.SelectAll:
lb.selectAllIndexes()
lb.SelectMode = false
kt.SetHandled()
}
}
func (lb *ListBase) keyInputEditable(kt events.Event) {
lb.keyInputNav(kt)
if kt.IsHandled() {
return
}
idx := lb.SelectedIndex
kf := keymap.Of(kt.KeyChord())
if DebugSettings.KeyEventTrace {
slog.Info("ListBase KeyInput", "widget", lb, "keyFunction", kf)
}
switch kf {
// case keymap.Delete: // too dangerous
// sv.This.(Lister).SliceDeleteAt(sv.SelectedIndex)
// sv.SelectMode = false
// sv.SelectIndexEvent(idx, events.SelectOne)
// kt.SetHandled()
case keymap.Duplicate:
nidx := lb.duplicate()
lb.SelectMode = false
if nidx >= 0 {
lb.SelectIndexEvent(nidx, events.SelectOne)
}
kt.SetHandled()
case keymap.Insert:
lb.This.(Lister).NewAt(idx)
lb.SelectMode = false
lb.SelectIndexEvent(idx+1, events.SelectOne) // todo: somehow nidx not working
kt.SetHandled()
case keymap.InsertAfter:
lb.This.(Lister).NewAt(idx + 1)
lb.SelectMode = false
lb.SelectIndexEvent(idx+1, events.SelectOne)
kt.SetHandled()
case keymap.Copy:
lb.copyIndexes(true)
lb.SelectMode = false
lb.SelectIndexEvent(idx, events.SelectOne)
kt.SetHandled()
case keymap.Cut:
lb.cutIndexes()
lb.SelectMode = false
kt.SetHandled()
case keymap.Paste:
lb.pasteIndex(lb.SelectedIndex)
lb.SelectMode = false
kt.SetHandled()
}
}
func (lb *ListBase) keyInputReadOnly(kt events.Event) {
if lb.ReadOnlyMultiSelect {
lb.keyInputNav(kt)
if kt.IsHandled() {
return
}
}
selMode := kt.SelectMode()
if lb.SelectMode {
selMode = events.ExtendOne
}
kf := keymap.Of(kt.KeyChord())
if DebugSettings.KeyEventTrace {
slog.Info("ListBase ReadOnly KeyInput", "widget", lb, "keyFunction", kf)
}
idx := lb.SelectedIndex
switch {
case kf == keymap.MoveDown:
ni := idx + 1
if ni < lb.SliceSize {
lb.ScrollToIndex(ni)
lb.updateSelectIndex(ni, true, selMode)
kt.SetHandled()
}
case kf == keymap.MoveUp:
ni := idx - 1
if ni >= 0 {
lb.ScrollToIndex(ni)
lb.updateSelectIndex(ni, true, selMode)
kt.SetHandled()
}
case kf == keymap.PageDown:
ni := min(idx+lb.VisibleRows-1, lb.SliceSize-1)
lb.ScrollToIndex(ni)
lb.updateSelectIndex(ni, true, selMode)
kt.SetHandled()
case kf == keymap.PageUp:
ni := max(idx-(lb.VisibleRows-1), 0)
lb.ScrollToIndex(ni)
lb.updateSelectIndex(ni, true, selMode)
kt.SetHandled()
case kf == keymap.Enter || kf == keymap.Accept || kt.KeyRune() == ' ':
lb.Send(events.DoubleClick, kt)
kt.SetHandled()
}
}
func (lb *ListBase) SizeFinal() {
sg := lb.ListGrid
if sg == nil {
lb.Frame.SizeFinal()
return
}
localIter := 0
for (lb.MakeIter < 2 || lb.VisibleRows != sg.visibleRows) && localIter < 2 {
if lb.VisibleRows != sg.visibleRows {
lb.VisibleRows = sg.visibleRows
lb.Update()
} else {
sg.StyleTree()
}
sg.sizeFinalUpdateChildrenSizes()
lb.MakeIter++
localIter++
}
lb.Frame.SizeFinal()
}
// ListGrid handles the resizing logic for all [Lister]s.
type ListGrid struct { //core:no-new
Frame
// minRows is set from parent [List]
minRows int
// height of a single row, computed during layout
rowHeight float32
// total number of rows visible in allocated display size
visibleRows int
// Various computed backgrounds
bgStripe, bgSelect, bgSelectStripe, bgHover, bgHoverStripe, bgHoverSelect, bgHoverSelectStripe image.Image
// lastBackground is the background for which modified
// backgrounds were computed -- don't update if same
lastBackground image.Image
}
func (lg *ListGrid) Init() {
lg.Frame.Init()
lg.Styler(func(s *styles.Style) {
s.Display = styles.Grid
})
}
func (lg *ListGrid) SizeFromChildren(iter int, pass LayoutPasses) math32.Vector2 {
csz := lg.Frame.SizeFromChildren(iter, pass)
rht, err := lg.layout.rowHeight(0, 0)
if err != nil {
// fmt.Println("ListGrid Sizing Error:", err)
lg.rowHeight = 42
}
if lg.NeedsRebuild() { // rebuilding = reset
lg.rowHeight = rht
} else {
lg.rowHeight = rht // max(lg.rowHeight, rht) // todo: we are currently testing not having this.
}
if lg.rowHeight == 0 {
// fmt.Println("ListGrid Sizing Error: RowHeight should not be 0!", sg)
lg.rowHeight = 42
}
allocHt := lg.Geom.Size.Alloc.Content.Y - lg.Geom.Size.InnerSpace.Y
if allocHt > lg.rowHeight {
lg.visibleRows = int(math32.Floor(allocHt / lg.rowHeight))
}
lg.visibleRows = max(lg.visibleRows, lg.minRows)
minHt := lg.rowHeight * float32(lg.minRows)
// fmt.Println("VisRows:", sg.VisRows, "rh:", sg.RowHeight, "ht:", minHt)
// visHt := sg.RowHeight * float32(sg.VisRows)
csz.Y = minHt
return csz
}
func (lg *ListGrid) SetScrollParams(d math32.Dims, sb *Slider) {
if d == math32.X {
lg.Frame.SetScrollParams(d, sb)
return
}
sb.Min = 0
sb.Step = 1
if lg.visibleRows > 0 {
sb.PageStep = float32(lg.visibleRows)
} else {
sb.PageStep = 10
}
sb.InputThreshold = sb.Step
}
func (lg *ListGrid) list() (Lister, *ListBase) {
ls := tree.ParentByType[Lister](lg)
if ls == nil {
return nil, nil
}
return ls, ls.AsListBase()
}
func (lg *ListGrid) ScrollChanged(d math32.Dims, sb *Slider) {
if d == math32.X {
lg.Frame.ScrollChanged(d, sb)
return
}
_, sv := lg.list()
if sv == nil {
return
}
sv.StartIndex = int(math32.Round(sb.Value))
sv.Update()
}
func (lg *ListGrid) ScrollValues(d math32.Dims) (maxSize, visSize, visPct float32) {
if d == math32.X {
return lg.Frame.ScrollValues(d)
}
_, sv := lg.list()
if sv == nil {
return
}
maxSize = float32(max(sv.SliceSize, 1))
visSize = float32(lg.visibleRows)
visPct = visSize / maxSize
return
}
func (lg *ListGrid) updateScroll(idx int) {
if !lg.HasScroll[math32.Y] || lg.scrolls[math32.Y] == nil {
return
}
sb := lg.scrolls[math32.Y]
sb.SetValue(float32(idx))
}
func (lg *ListGrid) updateBackgrounds() {
bg := lg.Styles.ActualBackground
if lg.lastBackground == bg {
return
}
lg.lastBackground = bg
// we take our zebra intensity applied foreground color and then overlay it onto our background color
zclr := colors.WithAF32(colors.ToUniform(lg.Styles.Color), AppearanceSettings.ZebraStripesWeight())
lg.bgStripe = gradient.Apply(bg, func(c color.Color) color.Color {
return colors.AlphaBlend(c, zclr)
})
hclr := colors.WithAF32(colors.ToUniform(lg.Styles.Color), 0.08)
lg.bgHover = gradient.Apply(bg, func(c color.Color) color.Color {
return colors.AlphaBlend(c, hclr)
})
zhclr := colors.WithAF32(colors.ToUniform(lg.Styles.Color), AppearanceSettings.ZebraStripesWeight()+0.08)
lg.bgHoverStripe = gradient.Apply(bg, func(c color.Color) color.Color {
return colors.AlphaBlend(c, zhclr)
})
lg.bgSelect = colors.Scheme.Select.Container
lg.bgSelectStripe = colors.Uniform(colors.AlphaBlend(colors.ToUniform(colors.Scheme.Select.Container), zclr))
lg.bgHoverSelect = colors.Uniform(colors.AlphaBlend(colors.ToUniform(colors.Scheme.Select.Container), hclr))
lg.bgHoverSelectStripe = colors.Uniform(colors.AlphaBlend(colors.ToUniform(colors.Scheme.Select.Container), zhclr))
}
func (lg *ListGrid) rowBackground(sel, stripe, hover bool) image.Image {
switch {
case sel && stripe && hover:
return lg.bgHoverSelectStripe
case sel && stripe:
return lg.bgSelectStripe
case sel && hover:
return lg.bgHoverSelect
case sel:
return lg.bgSelect
case stripe && hover:
return lg.bgHoverStripe
case stripe:
return lg.bgStripe
case hover:
return lg.bgHover
default:
return lg.Styles.ActualBackground
}
}
func (lg *ListGrid) ChildBackground(child Widget) image.Image {
bg := lg.Styles.ActualBackground
_, sv := lg.list()
if sv == nil {
return bg
}
lg.updateBackgrounds()
row, _ := sv.widgetIndex(child)
si := row + sv.StartIndex
return lg.rowBackground(sv.indexIsSelected(si), si%2 == 1, row == sv.hoverRow)
}
func (lg *ListGrid) renderStripes() {
pos := lg.Geom.Pos.Content
sz := lg.Geom.Size.Actual.Content
if lg.visibleRows == 0 || sz.Y == 0 {
return
}
lg.updateBackgrounds()
pc := &lg.Scene.PaintContext
rows := lg.layout.Shape.Y
cols := lg.layout.Shape.X
st := pos
offset := 0
_, sv := lg.list()
startIndex := 0
if sv != nil {
startIndex = sv.StartIndex
offset = startIndex % 2
}
for r := 0; r < rows; r++ {
si := r + startIndex
ht, _ := lg.layout.rowHeight(r, 0)
miny := st.Y
for c := 0; c < cols; c++ {
ki := r*cols + c
if ki < lg.NumChildren() {
kw := lg.Child(ki).(Widget).AsWidget()
pyi := math32.Floor(kw.Geom.Pos.Total.Y)
if pyi < miny {
miny = pyi
}
}
}
st.Y = miny
ssz := sz
ssz.Y = ht
stripe := (r+offset)%2 == 1
sbg := lg.rowBackground(sv.indexIsSelected(si), stripe, r == sv.hoverRow)
pc.BlitBox(st, ssz, sbg)
st.Y += ht + lg.layout.Gap.Y
}
}
// mousePosInGrid returns true if the event mouse position is
// located within the slicegrid.
func (lg *ListGrid) mousePosInGrid(pt image.Point) bool {
ptrel := lg.PointToRelPos(pt)
sz := lg.Geom.ContentBBox.Size()
if lg.visibleRows == 0 || sz.Y == 0 {
return false
}
if ptrel.Y < 0 || ptrel.Y >= sz.Y || ptrel.X < 0 || ptrel.X >= sz.X-50 { // leave margin on rhs around scroll
return false
}
return true
}
// indexFromPixel returns the row, column indexes of given pixel point within grid.
// Takes a scene-level position.
func (lg *ListGrid) indexFromPixel(pt image.Point) (row, col int, isValid bool) {
if !lg.mousePosInGrid(pt) {
return
}
ptf := math32.FromPoint(lg.PointToRelPos(pt))
sz := math32.FromPoint(lg.Geom.ContentBBox.Size())
isValid = true
rows := lg.layout.Shape.Y
cols := lg.layout.Shape.X
st := math32.Vector2{}
got := false
for r := 0; r < rows; r++ {
ht, _ := lg.layout.rowHeight(r, 0)
ht += lg.layout.Gap.Y
miny := st.Y
if r > 0 {
for c := 0; c < cols; c++ {
kw := lg.Child(r*cols + c).(Widget).AsWidget()
pyi := math32.Floor(kw.Geom.Pos.Total.Y)
if pyi < miny {
miny = pyi
}
}
}
st.Y = miny
ssz := sz
ssz.Y = ht
if ptf.Y >= st.Y && ptf.Y < st.Y+ssz.Y {
row = r
got = true
break
// todo: col
}
st.Y += ht
}
if !got {
row = rows - 1
}
return
}
func (lg *ListGrid) Render() {
lg.WidgetBase.Render()
lg.renderStripes()
}
// 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 (
"fmt"
"image"
"log/slog"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/system"
"cogentcore.org/core/tree"
)
// newMainStage returns a new MainStage with given type and scene contents.
// Make further configuration choices using Set* methods, which
// can be chained directly after the newMainStage call.
// Use an appropriate Run call at the end to start the Stage running.
func newMainStage(typ StageTypes, sc *Scene) *Stage {
st := &Stage{}
st.setType(typ)
st.setScene(sc)
st.popups = &stages{}
st.popups.main = st
st.Main = st
return st
}
// RunMainWindow creates a new main window from the body,
// runs it, starts the app's main loop, and waits for all windows
// to close. It should typically be called once by every app at
// the end of their main function. It can not be called more than
// once for one app. For secondary windows, see [Body.RunWindow].
// If you need to configure the [Stage] further, use [Body.NewWindow]
// and then [Stage.RunMain] on the resulting [Stage].
func (bd *Body) RunMainWindow() {
if ExternalParent != nil {
bd.handleExternalParent()
return
}
bd.RunWindow()
Wait()
}
// RunMain runs the stage, starts the app's main loop,
// and waits for all windows to close. It can be called instead
// of [Body.RunMainWindow] if extra configuration steps are necessary
// on the [Stage]. It can not be called more than once for one app.
// For secondary stages, see [Stage.Run].
func (st *Stage) RunMain() {
if ExternalParent != nil {
st.Scene.Body.handleExternalParent()
return
}
st.Run()
Wait()
}
// ExternalParent is a parent widget external to this program.
// If it is set, calls to [Body.RunWindow] before [Wait] and
// calls to [Body.RunMainWindow] and [Stage.RunMain] will add the [Body] to this
// parent instead of creating a new window. It should typically not be
// used by end users; it is used in yaegicore and for pre-rendering apps
// as HTML that can be used as a preview and for SEO purposes.
var ExternalParent Widget
// waitCalled is whether [Wait] has been called. It is used for
// [ExternalParent] logic in [Body.RunWindow].
var waitCalled bool
// RunWindow returns and runs a new [WindowStage] that is placed in
// a new system window on multi-window platforms.
// See [Body.NewWindow] to make a window without running it.
// For the first window of your app, you should typically call
// [Body.RunMainWindow] instead.
func (bd *Body) RunWindow() *Stage {
if ExternalParent != nil && !waitCalled {
bd.handleExternalParent()
return nil
}
return bd.NewWindow().Run()
}
// handleExternalParent handles [ExternalParent] logic for
// [Body.RunWindow] and [Body.RunMainWindow].
func (bd *Body) handleExternalParent() {
ExternalParent.AsWidget().AddChild(bd)
// we must set the correct scene for each node
bd.WalkDown(func(n tree.Node) bool {
n.(Widget).AsWidget().Scene = bd.Scene
return tree.Continue
})
// we must not get additional scrollbars here
bd.Styler(func(s *styles.Style) {
s.Overflow.Set(styles.OverflowVisible)
})
}
// NewWindow returns a new [WindowStage] that is placed in
// a new system window on multi-window platforms.
// You must call [Stage.Run] or [Stage.RunMain] to run the window;
// see [Body.RunWindow] and [Body.RunMainWindow] for versions that
// automatically do so.
func (bd *Body) NewWindow() *Stage {
ms := newMainStage(WindowStage, bd.Scene)
ms.SetNewWindow(true)
return ms
}
func (st *Stage) addSceneParts() {
if st.Type != DialogStage || st.FullWindow || st.NewWindow {
return
}
// TODO: convert to use [Scene.Bars] instead of parts
sc := st.Scene
parts := sc.newParts()
parts.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Grow.Set(0, 1)
s.Gap.Zero()
})
mv := NewHandle(parts)
mv.Styler(func(s *styles.Style) {
s.Direction = styles.Column
})
mv.FinalStyler(func(s *styles.Style) {
s.Cursor = cursors.Move
})
mv.SetName("move")
mv.OnChange(func(e events.Event) {
e.SetHandled()
pd := e.PrevDelta()
np := sc.SceneGeom.Pos.Add(pd)
np.X = max(np.X, 0)
np.Y = max(np.Y, 0)
rw := sc.RenderWindow()
sz := rw.SystemWindow.Size()
mx := sz.X - int(sc.SceneGeom.Size.X)
my := sz.Y - int(sc.SceneGeom.Size.Y)
np.X = min(np.X, mx)
np.Y = min(np.Y, my)
sc.SceneGeom.Pos = np
sc.NeedsRender()
})
if st.Resizable {
rsz := NewHandle(parts)
rsz.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.FillMargin = false
})
rsz.FinalStyler(func(s *styles.Style) {
s.Cursor = cursors.ResizeNWSE
s.Min.Set(units.Em(1))
})
rsz.SetName("resize")
rsz.OnChange(func(e events.Event) {
e.SetHandled()
pd := e.PrevDelta()
np := sc.SceneGeom.Size.Add(pd)
minsz := 100
np.X = max(np.X, minsz)
np.Y = max(np.Y, minsz)
ng := sc.SceneGeom
ng.Size = np
sc.resize(ng)
})
}
}
// firstWindowStages creates a temporary [stages] for the first window
// to be able to get sizing information prior to having a RenderWindow,
// based on the system App Screen Size. Only adds a RenderContext.
func (st *Stage) firstWindowStages() *stages {
ms := &stages{}
ms.renderContext = newRenderContext()
return ms
}
// targetScreen returns the screen to use for opening a new window
// based on Screen field, currentRenderWindow's screen, and a fallback
// default of Screen 0.
func (st *Stage) targetScreen() *system.Screen {
if st.Screen >= 0 && st.Screen < TheApp.NScreens() {
return TheApp.Screen(st.Screen)
}
if currentRenderWindow != nil {
return currentRenderWindow.SystemWindow.Screen()
}
return TheApp.Screen(0)
}
// configMainStage does main-stage configuration steps
func (st *Stage) configMainStage() {
sc := st.Scene
if st.NewWindow {
st.FullWindow = true
}
if TheApp.Platform().IsMobile() {
// If we are a new window dialog on a large single-window platform,
// we use a modeless dialog as a substitute.
if st.NewWindow && st.Type == DialogStage && st.Context != nil && st.Context.AsWidget().SizeClass() != SizeCompact {
st.FullWindow = false
st.Modal = false
st.Scrim = false
// Default is to add back button in this situation.
if !st.BackButton.Valid {
st.SetBackButton(true)
}
}
// If we are on mobile, we can never have new windows.
st.NewWindow = false
}
if st.FullWindow || st.NewWindow {
st.Scrim = false
}
sc.makeSceneBars()
sc.updateScene()
}
// runWindow runs a Window with current settings.
func (st *Stage) runWindow() *Stage {
sc := st.Scene
if currentRenderWindow == nil {
// If we have no current render window, we need to be in a new window,
// and we need a *temporary* Mains to get initial pref size
st.setMains(st.firstWindowStages())
} else {
st.setMains(¤tRenderWindow.mains)
}
st.configMainStage()
st.addSceneParts()
sz := st.renderContext.geom.Size
// offscreen windows always consider pref size because
// they must be unbounded by any previous window sizes
// non-offscreen mobile windows must take up the whole window
// and thus don't consider pref size
// desktop new windows and non-full windows can pref size
if TheApp.Platform() == system.Offscreen ||
(!TheApp.Platform().IsMobile() &&
(st.NewWindow || !st.FullWindow || currentRenderWindow == nil)) {
sz = sc.contentSize(sz)
// on offscreen, we don't want any extra space, as we want the smallest
// possible representation of the content
// also, on offscreen, if the new size is bigger than the current size,
// we need to resize the window
if TheApp.Platform() == system.Offscreen {
if currentRenderWindow != nil {
csz := currentRenderWindow.SystemWindow.Size()
nsz := csz
if sz.X > csz.X {
nsz.X = sz.X
}
if sz.Y > csz.Y {
nsz.Y = sz.Y
}
if nsz != csz {
currentRenderWindow.SystemWindow.SetSize(nsz)
TheApp.GetScreens()
}
}
} else {
// on other platforms, we want extra space and a minimum window size
sz = sz.Add(image.Pt(20, 20))
screen := st.targetScreen()
if screen != nil {
st.SetScreen(screen.ScreenNumber)
if st.NewWindow && st.UseMinSize {
// we require windows to be at least 60% and no more than 80% of the
// screen size by default
scsz := screen.PixelSize
sz = image.Pt(max(sz.X, scsz.X*6/10), max(sz.Y, scsz.Y*6/10))
sz = image.Pt(min(sz.X, scsz.X*8/10), min(sz.Y, scsz.Y*8/10))
}
}
}
}
st.Mains = nil // reset
if DebugSettings.WindowRenderTrace {
fmt.Println("MainStage.RunWindow: Window Size:", sz)
}
if st.NewWindow || currentRenderWindow == nil {
sc.resize(math32.Geom2DInt{st.renderContext.geom.Pos, sz})
win := st.newRenderWindow()
mainRenderWindows.add(win)
setCurrentRenderWindow(win)
win.goStartEventLoop()
return st
}
if st.Context != nil {
ms := st.Context.AsWidget().Scene.Stage.Mains
msc := ms.top().Scene
sc.SceneGeom.Size = sz
sc.fitInWindow(msc.SceneGeom) // does resize
ms.push(st)
st.setMains(ms)
} else {
ms := ¤tRenderWindow.mains
msc := ms.top().Scene
sc.SceneGeom.Size = sz
sc.fitInWindow(msc.SceneGeom) // does resize
ms.push(st)
st.setMains(ms)
}
return st
}
// getValidContext ensures that the Context is non-nil and has a valid
// Scene pointer, using CurrentRenderWindow if the current Context is not valid.
// If CurrentRenderWindow is nil (should not happen), then it returns false and
// the calling function must bail.
func (st *Stage) getValidContext() bool {
if st.Context == nil || st.Context.AsTree().This == nil || st.Context.AsWidget().Scene == nil {
if currentRenderWindow == nil {
slog.Error("Stage.Run: Context is nil and CurrentRenderWindow is nil, so cannot Run", "Name", st.Name, "Title", st.Title)
return false
}
st.Context = currentRenderWindow.mains.top().Scene
}
return true
}
// runDialog runs a Dialog with current settings.
func (st *Stage) runDialog() *Stage {
if !st.getValidContext() {
return st
}
ctx := st.Context.AsWidget()
// if our main stages are nil, we wait until our context is shown and then try again
if ctx.Scene.Stage == nil || ctx.Scene.Stage.Mains == nil {
ctx.Defer(func() {
st.runDialog()
})
return st
}
ms := ctx.Scene.Stage.Mains
sc := st.Scene
st.configMainStage()
st.addSceneParts()
sc.SceneGeom.Pos = st.Pos
st.setMains(ms) // temporary for prefs
sz := ms.renderContext.geom.Size
if !st.FullWindow || st.NewWindow {
sz = sc.contentSize(sz)
sz = sz.Add(image.Pt(50, 50))
if st.UseMinSize {
// dialogs must be at least 400dp wide by default
minx := int(ctx.Scene.Styles.UnitContext.Dp(400))
sz.X = max(sz.X, minx)
}
sc.Events.startFocusFirst = true // popup dialogs always need focus
screen := st.targetScreen()
if screen != nil {
st.SetScreen(screen.ScreenNumber)
}
}
if DebugSettings.WindowRenderTrace {
slog.Info("MainStage.RunDialog", "size", sz)
}
if st.NewWindow {
st.Mains = nil
sc.resize(math32.Geom2DInt{st.renderContext.geom.Pos, sz})
st.Type = WindowStage // critical: now is its own window!
sc.SceneGeom.Pos = image.Point{} // ignore pos
win := st.newRenderWindow()
dialogRenderWindows.add(win)
setCurrentRenderWindow(win)
win.goStartEventLoop()
return st
}
sc.SceneGeom.Size = sz
sc.fitInWindow(st.renderContext.geom) // does resize
ms.push(st)
// st.SetMains(ms) // already set
return st
}
func (st *Stage) newRenderWindow() *renderWindow {
name := st.Name
title := st.Title
opts := &system.NewWindowOptions{
Title: title,
Icon: appIconImages(),
Size: st.Scene.SceneGeom.Size,
Pos: st.Pos,
StdPixels: false,
Screen: st.Screen,
}
opts.Flags.SetFlag(!st.Resizable, system.FixedSize)
opts.Flags.SetFlag(st.Maximized, system.Maximized)
opts.Flags.SetFlag(st.Fullscreen, system.Fullscreen)
screen := st.targetScreen()
screenName := ""
if screen != nil {
screenName = screen.Name
}
var wgp *windowGeometry
wgp, screen = theWindowGeometrySaver.get(title, screenName)
if wgp != nil {
theWindowGeometrySaver.settingStart()
opts.Screen = screen.ScreenNumber
opts.Size = wgp.Size
opts.Pos = wgp.Pos
opts.StdPixels = false
if w := AllRenderWindows.FindName(name); w != nil { // offset from existing
opts.Pos.X += 20
opts.Pos.Y += 20
}
opts.Flags.SetFlag(wgp.Max, system.Maximized)
}
win := newRenderWindow(name, title, opts)
theWindowGeometrySaver.settingEnd()
if win == nil {
return nil
}
AllRenderWindows.add(win)
// initialize Mains
win.mains.renderWindow = win
win.mains.renderContext = newRenderContext() // sets defaults according to Screen
// note: win is not yet created by the OS and we don't yet know its actual size
// or dpi.
win.mains.push(st)
st.setMains(&win.mains)
return win
}
// mainHandleEvent handles main stage events
func (st *Stage) mainHandleEvent(e events.Event) {
if st.Scene == nil {
return
}
st.popups.popupHandleEvent(e)
if e.IsHandled() || (st.popups != nil && st.popups.topIsModal()) || st.Scene == nil {
if DebugSettings.EventTrace && e.Type() != events.MouseMove {
fmt.Println("Event handled by popup:", e)
}
return
}
e.SetLocalOff(st.Scene.SceneGeom.Pos)
st.Scene.Events.handleEvent(e)
}
// mainHandleEvent calls mainHandleEvent on relevant stages in reverse order.
func (sm *stages) mainHandleEvent(e events.Event) {
n := sm.stack.Len()
for i := n - 1; i >= 0; i-- {
st := sm.stack.ValueByIndex(i)
st.mainHandleEvent(e)
if e.IsHandled() || st.Modal || st.FullWindow {
break
}
if st.Type == DialogStage { // modeless dialog, by definition
if e.HasPos() && st.Scene != nil {
b := st.Scene.SceneGeom.Bounds()
if e.WindowPos().In(b) { // don't propagate
break
}
}
}
}
}
// 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 (
"image"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/colors"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/system"
"cogentcore.org/core/tree"
)
// StyleMenuScene configures the default styles
// for the given pop-up menu frame with the given parent.
// It should be called on menu frames when they are created.
func StyleMenuScene(msc *Scene) {
msc.Styler(func(s *styles.Style) {
s.Grow.Set(0, 0)
s.Padding.Set(units.Dp(2))
s.Border.Radius = styles.BorderRadiusExtraSmall
s.Background = colors.Scheme.SurfaceContainer
s.BoxShadow = styles.BoxShadow2()
s.Gap.Zero()
})
msc.SetOnChildAdded(func(n tree.Node) {
if bt := AsButton(n); bt != nil {
bt.Type = ButtonMenu
bt.OnKeyChord(func(e events.Event) {
kf := keymap.Of(e.KeyChord())
switch kf {
case keymap.MoveRight:
if bt.openMenu(e) {
e.SetHandled()
}
case keymap.MoveLeft:
// need to be able to use arrow keys to navigate in completer
if msc.Stage.Type != CompleterStage {
msc.Stage.ClosePopup()
e.SetHandled()
}
}
})
return
}
if sp, ok := n.(*Separator); ok {
sp.Styler(func(s *styles.Style) {
s.Direction = styles.Row
})
}
})
}
// newMenuScene constructs a [Scene] for displaying a menu, using the
// given menu constructor function. If no name is provided, it defaults
// to "menu". If no menu items added, returns nil.
func newMenuScene(menu func(m *Scene), name ...string) *Scene {
nm := "menu"
if len(name) > 0 {
nm = name[0] + "-menu"
}
msc := NewScene(nm)
StyleMenuScene(msc)
menu(msc)
if !msc.HasChildren() {
return nil
}
hasSelected := false
msc.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
if cw == msc {
return tree.Continue
}
if bt := AsButton(cw); bt != nil {
if bt.Menu == nil {
bt.handleClickDismissMenu()
}
}
if !hasSelected && cwb.StateIs(states.Selected) {
// fmt.Println("start focus sel:", cwb)
msc.Events.SetStartFocus(cwb)
hasSelected = true
}
return tree.Continue
})
if !hasSelected && msc.HasChildren() {
// fmt.Println("start focus first:", msc.Child(0).(Widget))
msc.Events.SetStartFocus(msc.Child(0).(Widget))
}
return msc
}
// NewMenuStage returns a new Menu stage with given scene contents,
// in connection with given widget, which provides key context
// for constructing the menu, at given RenderWindow position
// (e.g., use ContextMenuPos or WinPos method on ctx Widget).
// Make further configuration choices using Set* methods, which
// can be chained directly after the New call.
// Use Run call at the end to start the Stage running.
func NewMenuStage(sc *Scene, ctx Widget, pos image.Point) *Stage {
if sc == nil || !sc.HasChildren() {
return nil
}
st := NewPopupStage(MenuStage, sc, ctx)
if pos != (image.Point{}) {
st.Pos = pos
}
return st
}
// NewMenu returns a new menu stage based on the given menu constructor
// function, in connection with given widget, which provides key context
// for constructing the menu at given RenderWindow position
// (e.g., use ContextMenuPos or WinPos method on ctx Widget).
// Make further configuration choices using Set* methods, which
// can be chained directly after the New call.
// Use Run call at the end to start the Stage running.
func NewMenu(menu func(m *Scene), ctx Widget, pos image.Point) *Stage {
return NewMenuStage(newMenuScene(menu, ctx.AsTree().Name), ctx, pos)
}
// AddContextMenu adds the given context menu to [WidgetBase.ContextMenus].
// It is the main way that code should modify a widget's context menus.
// Context menu functions are run in reverse order, and separators are
// automatically added between each context menu function. [Scene.ContextMenus]
// apply to all widgets in the scene.
func (wb *WidgetBase) AddContextMenu(menu func(m *Scene)) {
wb.ContextMenus = append(wb.ContextMenus, menu)
}
// applyContextMenus adds the [WidgetBase.ContextMenus] and [Scene.ContextMenus]
// to the given menu scene in reverse order. It also adds separators between each
// context menu function.
func (wb *WidgetBase) applyContextMenus(m *Scene) {
do := func(cms []func(m *Scene)) {
for i := len(cms) - 1; i >= 0; i-- {
if m.NumChildren() > 0 {
NewSeparator(m)
}
cms[i](m)
}
}
do(wb.ContextMenus)
if wb.This != wb.Scene {
do(wb.Scene.ContextMenus)
}
}
// ContextMenuPos returns the default position for the context menu
// upper left corner. The event will be from a mouse ContextMenu
// event if non-nil: should handle both cases.
func (wb *WidgetBase) ContextMenuPos(e events.Event) image.Point {
if e != nil {
return e.WindowPos()
}
return wb.winPos(.5, .5) // center
}
func (wb *WidgetBase) handleWidgetContextMenu() {
wb.On(events.ContextMenu, func(e events.Event) {
wi := wb.This.(Widget)
wi.ShowContextMenu(e)
})
}
func (wb *WidgetBase) ShowContextMenu(e events.Event) {
e.SetHandled() // always
wi := wb.This.(Widget)
nm := NewMenu(wi.AsWidget().applyContextMenus, wi, wi.ContextMenuPos(e))
if nm == nil { // no items
return
}
nm.Run()
}
// NewMenuFromStrings constructs a new menu from given list of strings,
// calling the given function with the index of the selected string.
// if string == sel, that menu item is selected initially.
func NewMenuFromStrings(strs []string, sel string, fun func(idx int)) *Scene {
return newMenuScene(func(m *Scene) {
for i, s := range strs {
b := NewButton(m).SetText(s)
b.OnClick(func(e events.Event) {
fun(i)
})
if s == sel {
b.SetSelected(true)
}
}
})
}
var (
// webCanInstall is whether the app can be installed on the web platform
webCanInstall bool
// webInstall installs the app on the web platform
webInstall func()
)
// MenuSearcher is an interface that [Widget]s can implement
// to customize the items of the menu search chooser created
// by the default [Scene] context menu.
type MenuSearcher interface {
MenuSearch(items *[]ChooserItem)
}
// standardContextMenu adds standard context menu items for the [Scene].
func (sc *Scene) standardContextMenu(m *Scene) { //types:add
msdesc := "Search for menu buttons and other app actions"
NewButton(m).SetText("Menu search").SetIcon(icons.Search).SetKey(keymap.Menu).SetTooltip(msdesc).OnClick(func(e events.Event) {
d := NewBody("Menu search")
NewText(d).SetType(TextSupporting).SetText(msdesc)
w := NewChooser(d).SetEditable(true).SetIcon(icons.Search)
w.Styler(func(s *styles.Style) {
s.Grow.Set(1, 0)
})
w.AddItemsFunc(func() {
for _, rw := range AllRenderWindows {
for _, kv := range rw.mains.stack.Order {
st := kv.Value
// we do not include ourself
if st == sc.Stage || st == w.Scene.Stage {
continue
}
w.Items = append(w.Items, ChooserItem{
Text: st.Title,
Icon: icons.Toolbar,
Tooltip: "Show " + st.Title,
Func: st.raise,
})
}
}
})
w.AddItemsFunc(func() {
addButtonItems(&w.Items, sc, "")
tmps := NewScene()
sc.applyContextMenus(tmps)
addButtonItems(&w.Items, tmps, "")
})
w.OnFinal(events.Change, func(e events.Event) {
d.Close()
})
d.AddBottomBar(func(bar *Frame) {
d.AddCancel(bar)
})
d.RunDialog(sc)
})
NewButton(m).SetText("About").SetIcon(icons.Info).OnClick(func(e events.Event) {
d := NewBody(TheApp.Name())
d.Styler(func(s *styles.Style) {
s.CenterAll()
})
NewText(d).SetType(TextHeadlineLarge).SetText(TheApp.Name())
if AppIcon != "" {
errors.Log(NewSVG(d).ReadString(AppIcon))
}
if AppAbout != "" {
NewText(d).SetText(AppAbout)
}
NewText(d).SetText("App version: " + system.AppVersion)
NewText(d).SetText("Core version: " + system.CoreVersion)
d.AddOKOnly().NewDialog(sc).SetDisplayTitle(false).Run()
})
NewFuncButton(m).SetFunc(SettingsWindow).SetText("Settings").SetIcon(icons.Settings).SetShortcut("Command+,")
if webCanInstall {
icon := icons.InstallDesktop
if TheApp.SystemPlatform().IsMobile() {
icon = icons.InstallMobile
}
NewFuncButton(m).SetFunc(webInstall).SetText("Install").SetIcon(icon).SetTooltip("Install this app to your device as a Progressive Web App (PWA)")
}
NewButton(m).SetText("Inspect").SetIcon(icons.Edit).SetShortcut("Command+Shift+I").
SetTooltip("Developer tools for inspecting the content of the app").
OnClick(func(e events.Event) {
InspectorWindow(sc)
})
// No window menu on mobile platforms
if TheApp.Platform().IsMobile() && TheApp.Platform() != system.Web {
return
}
NewButton(m).SetText("Window").SetMenu(func(m *Scene) {
if sc.IsFullscreen() {
NewButton(m).SetText("Exit fullscreen").SetIcon(icons.Fullscreen).OnClick(func(e events.Event) {
sc.SetFullscreen(false)
})
} else {
NewButton(m).SetText("Fullscreen").SetIcon(icons.Fullscreen).OnClick(func(e events.Event) {
sc.SetFullscreen(true)
})
}
// Only do fullscreen on web
if TheApp.Platform() == system.Web {
return
}
NewButton(m).SetText("Focus next").SetIcon(icons.CenterFocusStrong).
SetKey(keymap.WinFocusNext).OnClick(func(e events.Event) {
AllRenderWindows.focusNext()
})
NewButton(m).SetText("Minimize").SetIcon(icons.Minimize).
OnClick(func(e events.Event) {
win := sc.RenderWindow()
if win != nil {
win.minimize()
}
})
NewSeparator(m)
NewButton(m).SetText("Close window").SetIcon(icons.Close).SetKey(keymap.WinClose).
OnClick(func(e events.Event) {
win := sc.RenderWindow()
if win != nil {
win.closeReq()
}
})
quit := NewButton(m).SetText("Quit").SetIcon(icons.Close).SetShortcut("Command+Q")
quit.OnClick(func(e events.Event) {
go TheApp.QuitReq()
})
})
}
// addButtonItems adds to the given items all of the buttons under
// the given parent. It navigates through button menus to find other
// buttons using a recursive approach that updates path with context
// about the original button menu. Consumers of this function should
// typically set path to "".
func addButtonItems(items *[]ChooserItem, parent tree.Node, path string) {
parent.AsTree().WalkDown(func(n tree.Node) bool {
if ms, ok := n.(MenuSearcher); ok {
ms.MenuSearch(items)
}
bt := AsButton(n)
if bt == nil || bt.IsDisabled() {
return tree.Continue
}
_, isTb := bt.Parent.(*Toolbar)
_, isSc := bt.Parent.(*Scene)
if !isTb && !isSc {
return tree.Continue
}
if bt.Text == "Menu search" {
return tree.Continue
}
label := bt.Text
if label == "" {
label = bt.Tooltip
}
if bt.HasMenu() {
tmps := NewScene()
bt.Menu(tmps)
npath := path
if npath != "" {
npath += " > "
}
if bt.Name != "overflow-menu" {
npath += label
}
addButtonItems(items, tmps, npath)
return tree.Continue
}
if path != "" {
label = path + " > " + label
}
*items = append(*items, ChooserItem{
Text: label,
Icon: bt.Icon,
Tooltip: bt.Tooltip,
Func: func() {
bt.Send(events.Click)
},
})
return tree.Continue
})
}
// 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 (
"fmt"
"image"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
)
// Meter is a widget that renders a current value on as a filled
// bar/circle/semicircle relative to a minimum and maximum potential
// value.
type Meter struct {
WidgetBase
// Type is the styling type of the meter.
Type MeterTypes
// Value is the current value of the meter.
// It defaults to 0.5.
Value float32
// Min is the minimum possible value of the meter.
// It defaults to 0.
Min float32
// Max is the maximum possible value of the meter.
// It defaults to 1.
Max float32
// Text, for [MeterCircle] and [MeterSemicircle], is the
// text to render inside of the circle/semicircle.
Text string
// ValueColor is the image color that will be used to
// render the filled value bar. It should be set in a Styler.
ValueColor image.Image
// Width, for [MeterCircle] and [MeterSemicircle], is the
// width of the circle/semicircle. It should be set in a Styler.
Width units.Value
}
// MeterTypes are the different styling types of [Meter]s.
type MeterTypes int32 //enums:enum -trim-prefix Meter
const (
// MeterLinear indicates to render a meter that goes in a straight,
// linear direction, either horizontal or vertical, as specified by
// [styles.Style.Direction].
MeterLinear MeterTypes = iota
// MeterCircle indicates to render the meter as a circle.
MeterCircle
// MeterSemicircle indicates to render the meter as a semicircle.
MeterSemicircle
)
func (m *Meter) WidgetValue() any { return &m.Value }
func (m *Meter) Init() {
m.WidgetBase.Init()
m.Value = 0.5
m.Max = 1
m.Styler(func(s *styles.Style) {
m.ValueColor = colors.Scheme.Primary.Base
s.Background = colors.Scheme.SurfaceVariant
s.Border.Radius = styles.BorderRadiusFull
})
m.FinalStyler(func(s *styles.Style) {
switch m.Type {
case MeterLinear:
if s.Direction == styles.Row {
s.Min.Set(units.Dp(320), units.Dp(8))
} else {
s.Min.Set(units.Dp(8), units.Dp(320))
}
case MeterCircle:
s.Min.Set(units.Dp(128))
m.Width.Dp(8)
s.Font.Size.Dp(32)
s.Text.LineHeight.Em(40.0 / 32)
s.Text.Align = styles.Center
s.Text.AlignV = styles.Center
case MeterSemicircle:
s.Min.Set(units.Dp(112), units.Dp(64))
m.Width.Dp(16)
s.Font.Size.Dp(22)
s.Text.LineHeight.Em(28.0 / 22)
s.Text.Align = styles.Center
s.Text.AlignV = styles.Center
}
})
}
func (m *Meter) Style() {
m.WidgetBase.Style()
m.Width.ToDots(&m.Styles.UnitContext)
}
func (m *Meter) WidgetTooltip(pos image.Point) (string, image.Point) {
res := m.Tooltip
if res != "" {
res += " "
}
res += fmt.Sprintf("(value: %.4g, minimum: %g, maximum: %g)", m.Value, m.Min, m.Max)
return res, m.DefaultTooltipPos()
}
func (m *Meter) Render() {
pc := &m.Scene.PaintContext
st := &m.Styles
prop := (m.Value - m.Min) / (m.Max - m.Min)
if m.Type == MeterLinear {
m.RenderStandardBox()
if m.ValueColor != nil {
dim := m.Styles.Direction.Dim()
size := m.Geom.Size.Actual.Content.MulDim(dim, prop)
pc.FillStyle.Color = m.ValueColor
m.RenderBoxGeom(m.Geom.Pos.Content, size, st.Border)
}
return
}
pc.StrokeStyle.Width = m.Width
sw := pc.StrokeWidth()
pos := m.Geom.Pos.Content.AddScalar(sw / 2)
size := m.Geom.Size.Actual.Content.SubScalar(sw)
var txt *paint.Text
var toff math32.Vector2
if m.Text != "" {
txt = &paint.Text{}
txt.SetHTML(m.Text, st.FontRender(), &st.Text, &st.UnitContext, nil)
tsz := txt.Layout(&st.Text, st.FontRender(), &st.UnitContext, size)
toff = tsz.DivScalar(2)
}
if m.Type == MeterCircle {
r := size.DivScalar(2)
c := pos.Add(r)
pc.DrawEllipticalArc(c.X, c.Y, r.X, r.Y, 0, 2*math32.Pi)
pc.StrokeStyle.Color = st.Background
pc.Stroke()
if m.ValueColor != nil {
pc.DrawEllipticalArc(c.X, c.Y, r.X, r.Y, -math32.Pi/2, prop*2*math32.Pi-math32.Pi/2)
pc.StrokeStyle.Color = m.ValueColor
pc.Stroke()
}
if txt != nil {
txt.Render(pc, c.Sub(toff))
}
return
}
r := size.Mul(math32.Vec2(0.5, 1))
c := pos.Add(r)
pc.DrawEllipticalArc(c.X, c.Y, r.X, r.Y, math32.Pi, 2*math32.Pi)
pc.StrokeStyle.Color = st.Background
pc.Stroke()
if m.ValueColor != nil {
pc.DrawEllipticalArc(c.X, c.Y, r.X, r.Y, math32.Pi, (1+prop)*math32.Pi)
pc.StrokeStyle.Color = m.ValueColor
pc.Stroke()
}
if txt != nil {
txt.Render(pc, c.Sub(size.Mul(math32.Vec2(0, 0.3))).Sub(toff))
}
}
// Copyright (c) 2024, 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 (
"fmt"
"cogentcore.org/core/styles"
)
// Pages is a frame that can easily swap its content between that of
// different possible pages.
type Pages struct {
Frame
// Page is the currently open page.
Page string
// Pages is a map of page names to functions that configure a page.
Pages map[string]func(pg *Pages) `set:"-"`
// page is the currently rendered page.
page string
}
func (pg *Pages) Init() {
pg.Frame.Init()
pg.Pages = map[string]func(pg *Pages){}
pg.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Grow.Set(1, 1)
})
pg.Updater(func() {
if len(pg.Pages) == 0 {
return
}
if pg.page == pg.Page {
return
}
pg.DeleteChildren()
fun, ok := pg.Pages[pg.Page]
if !ok {
ErrorSnackbar(pg, fmt.Errorf("page %q not found", pg.Page))
return
}
pg.page = pg.Page
fun(pg)
pg.DeferShown()
})
}
// AddPage adds a page with the given name and configuration function.
// If [Pages.Page] is currently unset, it will be set to the given name.
func (pg *Pages) AddPage(name string, f func(pg *Pages)) {
pg.Pages[name] = f
if pg.Page == "" {
pg.Page = name
}
}
// Open sets the current page to the given name and updates the display.
func (pg *Pages) Open(name string) {
pg.SetPage(name)
pg.Update()
}
// 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 (
"time"
"cogentcore.org/core/events"
)
// NewPopupStage returns a new PopupStage with given type and scene contents.
// The given context widget must be non-nil.
// Make further configuration choices using Set* methods, which
// can be chained directly after the NewPopupStage call.
// Use Run call at the end to start the Stage running.
func NewPopupStage(typ StageTypes, sc *Scene, ctx Widget) *Stage {
ctx = nonNilContext(ctx)
st := &Stage{}
st.setType(typ)
st.setScene(sc)
st.Context = ctx
st.Pos = ctx.ContextMenuPos(nil)
sc.Stage = st
// note: not setting all the connections until run
return st
}
// runPopupAsync runs a popup-style Stage in context widget's popups.
// This version is for Asynchronous usage outside the main event loop,
// for example in a delayed callback AfterFunc etc.
func (st *Stage) runPopupAsync() *Stage {
ctx := st.Context.AsWidget()
if ctx.Scene.Stage == nil {
return st.runPopup()
}
ms := ctx.Scene.Stage.Main
rc := ms.renderContext
rc.lock()
defer rc.unlock()
return st.runPopup()
}
// runPopup runs a popup-style Stage in context widget's popups.
func (st *Stage) runPopup() *Stage {
if !st.getValidContext() { // doesn't even have a scene
return st
}
ctx := st.Context.AsWidget()
// if our context stage is nil, we wait until
// our context is shown and then try again
if ctx.Scene.Stage == nil {
ctx.Defer(func() {
st.runPopup()
})
return st
}
if st.Type == SnackbarStage {
st.Scene.makeSceneBars()
}
st.Scene.updateScene()
sc := st.Scene
ms := ctx.Scene.Stage.Main
msc := ms.Scene
if st.Type == SnackbarStage {
// only one snackbar can exist
ms.popups.popDeleteType(SnackbarStage)
}
ms.popups.push(st)
st.setPopups(ms) // sets all pointers
maxGeom := msc.SceneGeom
winst := ms.Mains.windowStage()
usingWinGeom := false
if winst != nil && winst.Scene != nil && winst.Scene != msc {
usingWinGeom = true
maxGeom = winst.Scene.SceneGeom // use the full window if possible
}
// original size and position, which is that of the context widget / location for a tooltip
osz := sc.SceneGeom.Size
opos := sc.SceneGeom.Pos
sc.SceneGeom.Size = maxGeom.Size
sc.SceneGeom.Pos = st.Pos
sz := sc.contentSize(maxGeom.Size)
bigPopup := false
if usingWinGeom && 4*sz.X*sz.Y > 3*msc.SceneGeom.Size.X*msc.SceneGeom.Size.Y { // reasonable fraction
bigPopup = true
}
scrollWd := int(sc.Styles.ScrollbarWidth.Dots)
fontHt := 16
if sc.Styles.Font.Face != nil {
fontHt = int(sc.Styles.Font.Face.Metrics.Height)
}
switch st.Type {
case MenuStage:
sz.X += scrollWd * 2
maxht := int(SystemSettings.MenuMaxHeight * fontHt)
sz.Y = min(maxht, sz.Y)
case SnackbarStage:
b := msc.SceneGeom.Bounds()
// Go in the middle [(max - min) / 2], and then subtract
// half of the size because we are specifying starting point,
// not the center. This results in us being centered.
sc.SceneGeom.Pos.X = (b.Max.X - b.Min.X - sz.X) / 2
// get enough space to fit plus 10 extra pixels of margin
sc.SceneGeom.Pos.Y = b.Max.Y - sz.Y - 10
case TooltipStage:
sc.SceneGeom.Pos.X = opos.X
// default to tooltip above element
ypos := opos.Y - sz.Y - 10
if ypos < 0 {
ypos = 0
}
// however, if we are within 10 pixels of the element,
// we put the tooltip below it instead of above it
maxy := ypos + sz.Y
if maxy > opos.Y-10 {
ypos = opos.Add(osz).Y + 10
}
sc.SceneGeom.Pos.Y = ypos
}
sc.SceneGeom.Size = sz
if bigPopup { // we have a big popup -- make it not cover the original window;
sc.fitInWindow(maxGeom) // does resize
// reposition to be as close to top-right of main scene as possible
tpos := msc.SceneGeom.Pos
tpos.X += msc.SceneGeom.Size.X
if tpos.X+sc.SceneGeom.Size.X > maxGeom.Size.X { // favor left side instead
tpos.X = max(msc.SceneGeom.Pos.X-sc.SceneGeom.Size.X, 0)
}
bpos := tpos.Add(sc.SceneGeom.Size)
if bpos.X > maxGeom.Size.X {
tpos.X -= bpos.X - maxGeom.Size.X
}
if bpos.Y > maxGeom.Size.Y {
tpos.Y -= bpos.Y - maxGeom.Size.Y
}
if tpos.X < 0 {
tpos.X = 0
}
if tpos.Y < 0 {
tpos.Y = 0
}
sc.SceneGeom.Pos = tpos
} else {
sc.fitInWindow(msc.SceneGeom)
}
sc.showIter = 0
if st.Timeout > 0 {
time.AfterFunc(st.Timeout, func() {
if st.Main == nil {
return
}
st.popups.deleteStage(st)
})
}
return st
}
// closePopupAsync closes this stage as a popup.
// This version is for Asynchronous usage outside the main event loop,
// for example in a delayed callback AfterFunc etc.
func (st *Stage) closePopupAsync() {
rc := st.Mains.renderContext
rc.lock()
defer rc.unlock()
st.ClosePopup()
}
// ClosePopup closes this stage as a popup, returning whether it was closed.
func (st *Stage) ClosePopup() bool {
// NOTE: this is critical for Completer to not crash due to async closing
if st.Main == nil || st.popups == nil || st.Mains == nil {
return false
}
return st.popups.deleteStage(st)
}
// closePopupAndBelow closes this stage as a popup,
// and all those immediately below it of the same type.
// It returns whether it successfully closed popups.
func (st *Stage) closePopupAndBelow() bool {
// NOTE: this is critical for Completer to not crash due to async closing
if st.Main == nil || st.popups == nil || st.Mains == nil {
return false
}
return st.popups.deleteStageAndBelow(st)
}
func (st *Stage) popupHandleEvent(e events.Event) {
if st.Scene == nil {
return
}
if e.IsHandled() {
return
}
e.SetLocalOff(st.Scene.SceneGeom.Pos)
// fmt.Println("pos:", evi.Pos(), "local:", evi.LocalPos())
st.Scene.Events.handleEvent(e)
}
// topIsModal returns true if there is a Top PopupStage and it is Modal.
func (pm *stages) topIsModal() bool {
top := pm.top()
if top == nil {
return false
}
return top.Modal
}
// popupHandleEvent processes Popup events.
// requires outer RenderContext mutex.
func (pm *stages) popupHandleEvent(e events.Event) {
top := pm.top()
if top == nil {
return
}
ts := top.Scene
// we must get the top stage that does not ignore events
if top.ignoreEvents {
var ntop *Stage
for i := pm.stack.Len() - 1; i >= 0; i-- {
s := pm.stack.ValueByIndex(i)
if !s.ignoreEvents {
ntop = s
break
}
}
if ntop == nil {
return
}
top = ntop
ts = top.Scene
}
if e.HasPos() {
pos := e.WindowPos()
// fmt.Println("pos:", pos, "top geom:", ts.SceneGeom)
if pos.In(ts.SceneGeom.Bounds()) {
top.popupHandleEvent(e)
e.SetHandled()
return
}
if top.ClickOff && e.Type() == events.MouseUp {
top.closePopupAndBelow()
}
if top.Modal { // absorb any other events!
e.SetHandled()
return
}
// otherwise not Handled, so pass on to first lower stage
// that accepts events and is in bounds
for i := pm.stack.Len() - 1; i >= 0; i-- {
s := pm.stack.ValueByIndex(i)
ss := s.Scene
if !s.ignoreEvents && pos.In(ss.SceneGeom.Bounds()) {
s.popupHandleEvent(e)
e.SetHandled()
return
}
}
} else { // typically focus, so handle even if not in bounds
top.popupHandleEvent(e) // could be set as Handled or not
}
}
// 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 (
"fmt"
"path/filepath"
"runtime/debug"
"strings"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/system"
)
// timesCrashed is the number of times that the program has
// crashed. It is used to prevent an infinite crash loop
// when rendering the crash window.
var timesCrashed int
// webCrashDialog is the function used to display the crash dialog on web.
// It cannot be displayed normally due to threading and single-window issues.
var webCrashDialog func(title, text, body string)
// handleRecover is the core value of [system.HandleRecover]. If r is not nil,
// it makes a window displaying information about the panic. [system.HandleRecover]
// is initialized to this in init.
func handleRecover(r any) {
if r == nil {
return
}
timesCrashed++
system.HandleRecoverBase(r)
if timesCrashed > 1 {
return
}
stack := string(debug.Stack())
// we have to handle the quit button indirectly so that it has the
// right stack for debugging when panicking
quit := make(chan struct{})
title := TheApp.Name() + " stopped unexpectedly"
text := "There was an unexpected error and " + TheApp.Name() + " stopped running."
clpath := filepath.Join(TheApp.AppDataDir(), "crash-logs")
clpath = strings.ReplaceAll(clpath, " ", `\ `) // escape spaces
body := fmt.Sprintf("Crash log saved in %s\n\n%s", clpath, system.CrashLogText(r, stack))
if webCrashDialog != nil {
webCrashDialog(title, text, body)
return
}
b := NewBody(title)
NewText(b).SetText(title).SetType(TextHeadlineSmall)
NewText(b).SetType(TextSupporting).SetText(text)
b.AddBottomBar(func(bar *Frame) {
NewButton(bar).SetText("Details").SetType(ButtonOutlined).OnClick(func(e events.Event) {
d := NewBody("Crash details")
NewText(d).SetText(body).Styler(func(s *styles.Style) {
s.SetMono(true)
s.Text.WhiteSpace = styles.WhiteSpacePreWrap
})
d.AddBottomBar(func(bar *Frame) {
NewButton(bar).SetText("Copy").SetIcon(icons.Copy).SetType(ButtonOutlined).
OnClick(func(e events.Event) {
d.Clipboard().Write(mimedata.NewText(body))
})
d.AddOK(bar)
})
d.RunFullDialog(b)
})
NewButton(bar).SetText("Quit").OnClick(func(e events.Event) {
quit <- struct{}{}
})
})
b.RunWindow()
<-quit
panic(r)
}
// 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 (
"fmt"
"image"
"os"
"path/filepath"
"runtime"
"runtime/pprof"
"time"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/profile"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/cam/hct"
"cogentcore.org/core/events"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/tree"
)
// AsyncLock must be called before making any updates in a separate goroutine
// outside of the main configuration, rendering, and event handling structure.
// It must have a matching [WidgetBase.AsyncUnlock] after it.
func (wb *WidgetBase) AsyncLock() {
rc := wb.Scene.renderContext()
if rc == nil {
// if there is no render context, we are probably
// being deleted, so we just block forever
select {}
}
rc.lock()
if wb.This == nil {
rc.unlock()
select {}
}
wb.Scene.setFlag(true, sceneUpdating)
}
// AsyncUnlock must be called after making any updates in a separate goroutine
// outside of the main configuration, rendering, and event handling structure.
// It must have a matching [WidgetBase.AsyncLock] before it.
func (wb *WidgetBase) AsyncUnlock() {
rc := wb.Scene.renderContext()
if rc == nil {
return
}
if wb.Scene != nil {
wb.Scene.setFlag(false, sceneUpdating)
}
rc.unlock()
}
// NeedsRender specifies that the widget needs to be rendered.
func (wb *WidgetBase) NeedsRender() {
if DebugSettings.UpdateTrace {
fmt.Println("\tDebugSettings.UpdateTrace: NeedsRender:", wb)
}
wb.setFlag(true, widgetNeedsRender)
if wb.Scene != nil {
wb.Scene.setFlag(true, sceneNeedsRender)
}
}
// NeedsLayout specifies that the widget's scene needs to do a layout.
// This needs to be called after any changes that affect the structure
// and/or size of elements.
func (wb *WidgetBase) NeedsLayout() {
if DebugSettings.UpdateTrace {
fmt.Println("\tDebugSettings.UpdateTrace: NeedsLayout:", wb)
}
if wb.Scene != nil {
wb.Scene.setFlag(true, sceneNeedsLayout)
}
}
// NeedsRebuild returns whether the [renderContext] indicates
// a full rebuild is needed. This is typically used to detect
// when the settings have been changed, such as when the color
// scheme or zoom is changed.
func (wb *WidgetBase) NeedsRebuild() bool {
if wb.This == nil || wb.Scene == nil || wb.Scene.Stage == nil {
return false
}
rc := wb.Scene.renderContext()
if rc == nil {
return false
}
return rc.rebuild
}
// layoutScene does a layout of the scene: Size, Position
func (sc *Scene) layoutScene() {
if DebugSettings.LayoutTrace {
fmt.Println("\n############################\nLayoutScene SizeUp start:", sc)
}
sc.SizeUp()
sz := &sc.Geom.Size
sz.Alloc.Total.SetPoint(sc.SceneGeom.Size)
sz.setContentFromTotal(&sz.Alloc)
// sz.Actual = sz.Alloc // todo: is this needed??
if DebugSettings.LayoutTrace {
fmt.Println("\n############################\nSizeDown start:", sc)
}
maxIter := 3
for iter := 0; iter < maxIter; iter++ { // 3 > 2; 4 same as 3
redo := sc.SizeDown(iter)
if redo && iter < maxIter-1 {
if DebugSettings.LayoutTrace {
fmt.Println("\n############################\nSizeDown redo:", sc, "iter:", iter+1)
}
} else {
break
}
}
if DebugSettings.LayoutTrace {
fmt.Println("\n############################\nSizeFinal start:", sc)
}
sc.SizeFinal()
if DebugSettings.LayoutTrace {
fmt.Println("\n############################\nPosition start:", sc)
}
sc.Position()
if DebugSettings.LayoutTrace {
fmt.Println("\n############################\nScenePos start:", sc)
}
sc.ApplyScenePos()
}
// layoutRenderScene does a layout and render of the tree:
// GetSize, DoLayout, Render. Needed after Config.
func (sc *Scene) layoutRenderScene() {
sc.layoutScene()
sc.RenderWidget()
}
// doNeedsRender calls Render on tree from me for nodes
// with NeedsRender flags set
func (wb *WidgetBase) doNeedsRender() {
if wb.This == nil {
return
}
wb.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
if cwb.hasFlag(widgetNeedsRender) {
cw.RenderWidget()
return tree.Break // don't go any deeper
}
if ly := AsFrame(cw); ly != nil {
for d := math32.X; d <= math32.Y; d++ {
if ly.HasScroll[d] && ly.scrolls[d] != nil {
ly.scrolls[d].doNeedsRender()
}
}
}
return tree.Continue
})
}
//////////////////////////////////////////////////////////////////
// Scene
var sceneShowIters = 2
// doUpdate checks scene Needs flags to do whatever updating is required.
// returns false if already updating.
// This is the main update call made by the RenderWindow at FPS frequency.
func (sc *Scene) doUpdate() bool {
if sc.hasFlag(sceneUpdating) {
return false
}
sc.setFlag(true, sceneUpdating) // prevent rendering
defer func() { sc.setFlag(false, sceneUpdating) }()
rc := sc.renderContext()
if sc.showIter < sceneShowIters {
sc.setFlag(true, sceneNeedsLayout)
sc.showIter++
}
switch {
case rc.rebuild:
pr := profile.Start("rebuild")
sc.doRebuild()
sc.setFlag(false, sceneNeedsLayout, sceneNeedsRender)
sc.setFlag(true, sceneImageUpdated)
pr.End()
case sc.lastRender.needsRestyle(rc):
pr := profile.Start("restyle")
sc.applyStyleScene()
sc.layoutRenderScene()
sc.setFlag(false, sceneNeedsLayout, sceneNeedsRender)
sc.setFlag(true, sceneImageUpdated)
sc.lastRender.saveRender(rc)
pr.End()
case sc.hasFlag(sceneNeedsLayout):
pr := profile.Start("layout")
sc.layoutRenderScene()
sc.setFlag(false, sceneNeedsLayout, sceneNeedsRender)
sc.setFlag(true, sceneImageUpdated)
pr.End()
case sc.hasFlag(sceneNeedsRender):
pr := profile.Start("render")
sc.doNeedsRender()
sc.setFlag(false, sceneNeedsRender)
sc.setFlag(true, sceneImageUpdated)
pr.End()
default:
return false
}
if sc.showIter == sceneShowIters { // end of first pass
sc.showIter++ // just go 1 past the iters cutoff
if !sc.hasFlag(sceneContentSizing) {
sc.Events.activateStartFocus()
}
}
return true
}
// updateScene calls UpdateTree on the Scene, which calls
// UpdateWidget on all widgets in the Scene. This will set
// NeedsLayout to drive subsequent layout and render.
// This is a top-level call, typically only done when the window
// is first drawn or resized, or during rebuild,
// once the full sizing information is available.
func (sc *Scene) updateScene() {
sc.setFlag(true, sceneUpdating) // prevent rendering
defer func() { sc.setFlag(false, sceneUpdating) }()
sc.UpdateTree()
}
// applyStyleScene calls ApplyStyle on all widgets in the Scene,
// This is needed whenever the window geometry, DPI,
// etc is updated, which affects styling.
func (sc *Scene) applyStyleScene() {
sc.setFlag(true, sceneUpdating) // prevent rendering
defer func() { sc.setFlag(false, sceneUpdating) }()
sc.StyleTree()
sc.setFlag(true, sceneNeedsLayout)
}
// doRebuild does the full re-render and RenderContext Rebuild flag
// 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.updateScene()
sc.applyStyleScene()
sc.layoutRenderScene()
}
// contentSize computes the size of the scene based on current content.
// initSz is the initial size, e.g., size of screen.
// Used for auto-sizing windows when created, and in [Scene.ResizeToContent].
func (sc *Scene) contentSize(initSz image.Point) image.Point {
sc.setFlag(true, sceneUpdating) // prevent rendering
defer func() { sc.setFlag(false, sceneUpdating) }()
sc.setFlag(true, sceneContentSizing)
sc.updateScene()
sc.applyStyleScene()
sc.layoutScene()
sz := &sc.Geom.Size
psz := sz.Actual.Total
sc.setFlag(false, sceneContentSizing)
sc.showIter = 0
return psz.ToPointFloor()
}
//////////////////////////////////////////////////////////////////
// Widget local rendering
// PushBounds pushes our bounding box bounds onto the bounds stack
// if they are non-empty. This automatically limits our drawing to
// our own bounding box. This must be called as the first step in
// Render implementations. It returns whether the new bounds are
// empty or not; if they are empty, then don't render.
func (wb *WidgetBase) PushBounds() bool {
if wb == nil || wb.This == nil {
return false
}
wb.setFlag(false, widgetNeedsRender) // done!
if !wb.IsVisible() { // checks deleted etc
return false
}
if wb.Geom.TotalBBox.Empty() {
if DebugSettings.RenderTrace {
fmt.Printf("Render empty bbox: %v at %v\n", wb.Path(), wb.Geom.TotalBBox)
}
return false
}
wb.Styles.ComputeActualBackground(wb.parentActualBackground())
pc := &wb.Scene.PaintContext
if pc.State == nil || pc.Image == nil {
return false
}
if len(pc.BoundsStack) == 0 && wb.Parent != nil {
wb.setFlag(true, widgetFirstRender)
// push our parent's bounds if we are the first to render
pw := wb.parentWidget()
pc.PushBoundsGeom(pw.Geom.TotalBBox, pw.Styles.Border.Radius.Dots())
} else {
wb.setFlag(false, widgetFirstRender)
}
pc.PushBoundsGeom(wb.Geom.TotalBBox, wb.Styles.Border.Radius.Dots())
pc.Defaults() // start with default values
if DebugSettings.RenderTrace {
fmt.Printf("Render: %v at %v\n", wb.Path(), wb.Geom.TotalBBox)
}
return true
}
// PopBounds pops our bounding box bounds. This is the last step
// in Render implementations after rendering children.
func (wb *WidgetBase) PopBounds() {
if wb == nil || wb.This == nil {
return
}
pc := &wb.Scene.PaintContext
isSelw := wb.Scene.selectedWidget == wb.This
if wb.Scene.renderBBoxes || isSelw {
pos := math32.FromPoint(wb.Geom.TotalBBox.Min)
sz := math32.FromPoint(wb.Geom.TotalBBox.Size())
// node: we won't necc. get a push prior to next update, so saving these.
pcsw := pc.StrokeStyle.Width
pcsc := pc.StrokeStyle.Color
pcfc := pc.FillStyle.Color
pcop := pc.FillStyle.Opacity
pc.StrokeStyle.Width.Dot(1)
pc.StrokeStyle.Color = colors.Uniform(hct.New(wb.Scene.renderBBoxHue, 100, 50))
pc.FillStyle.Color = nil
if isSelw {
fc := pc.StrokeStyle.Color
pc.FillStyle.Color = fc
pc.FillStyle.Opacity = 0.2
}
pc.DrawRectangle(pos.X, pos.Y, sz.X, sz.Y)
pc.FillStrokeClear()
// restore
pc.FillStyle.Opacity = pcop
pc.FillStyle.Color = pcfc
pc.StrokeStyle.Width = pcsw
pc.StrokeStyle.Color = pcsc
wb.Scene.renderBBoxHue += 10
if wb.Scene.renderBBoxHue > 360 {
rmdr := (int(wb.Scene.renderBBoxHue-360) + 1) % 9
wb.Scene.renderBBoxHue = float32(rmdr)
}
}
pc.PopBounds()
if wb.hasFlag(widgetFirstRender) {
pc.PopBounds()
wb.setFlag(false, widgetFirstRender)
}
}
// Render is the method that widgets should implement to define their
// custom rendering steps. It should not typically be called outside of
// [Widget.RenderWidget], which also does other steps applicable
// for all widgets. The base [WidgetBase.Render] implementation
// renders the standard box model.
func (wb *WidgetBase) Render() {
wb.RenderStandardBox()
}
// RenderWidget renders the widget and any parts and children that it has.
// It does not render if the widget is invisible. It calls Widget.Render]
// for widget-specific rendering.
func (wb *WidgetBase) RenderWidget() {
if wb.PushBounds() {
wb.This.(Widget).Render()
wb.renderChildren()
wb.renderParts()
wb.PopBounds()
}
}
func (wb *WidgetBase) renderParts() {
if wb.Parts != nil {
wb.Parts.RenderWidget()
}
}
// renderChildren renders all of the widget's children.
func (wb *WidgetBase) renderChildren() {
wb.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cw.RenderWidget()
return tree.Continue
})
}
//////// Defer
// Defer adds a function to [WidgetBase.Deferred] that will be called after the next
// [Scene] update/render, including on the initial Scene render. After the function
// is called, it is removed and not called again. In the function, sending events
// etc will work as expected.
func (wb *WidgetBase) Defer(fun func()) {
wb.Deferred = append(wb.Deferred, fun)
if wb.Scene != nil {
wb.Scene.setFlag(true, sceneHasDeferred)
}
}
// runDeferred runs deferred functions on all widgets in the scene.
func (sc *Scene) runDeferred() {
sc.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
for _, f := range cwb.Deferred {
f()
}
cwb.Deferred = nil
return tree.Continue
})
}
// DeferShown adds a [WidgetBase.Defer] function to call [WidgetBase.Shown]
// and activate [WidgetBase.StartFocus]. For example, this is called in [Tabs]
// and [Pages] when a tab/page is newly shown, so that elements can perform
// [WidgetBase.OnShow] updating as needed.
func (wb *WidgetBase) DeferShown() {
wb.Defer(func() {
wb.Shown()
wb.Scene.Events.activateStartFocus()
})
}
// Shown sends [events.Show] to all widgets from this one down. Also see
// [WidgetBase.DeferShown].
func (wb *WidgetBase) Shown() {
wb.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
cwb.Send(events.Show)
return tree.Continue
})
}
//////// Standard Box Model rendering
// RenderBoxGeom renders a box with the given geometry.
func (wb *WidgetBase) RenderBoxGeom(pos math32.Vector2, sz math32.Vector2, bs styles.Border) {
wb.Scene.PaintContext.DrawBorder(pos.X, pos.Y, sz.X, sz.Y, bs)
}
// RenderStandardBox renders the standard box model.
func (wb *WidgetBase) RenderStandardBox() {
pos := wb.Geom.Pos.Total
sz := wb.Geom.Size.Actual.Total
wb.Scene.PaintContext.DrawStandardBox(&wb.Styles, pos, sz, wb.parentActualBackground())
}
//////// Widget position functions
// PointToRelPos translates a point in Scene pixel coords
// into relative position within node, based on the Content BBox
func (wb *WidgetBase) PointToRelPos(pt image.Point) image.Point {
return pt.Sub(wb.Geom.ContentBBox.Min)
}
// winBBox returns the RenderWindow based bounding box for the widget
// by adding the Scene position to the ScBBox
func (wb *WidgetBase) winBBox() image.Rectangle {
bb := wb.Geom.TotalBBox
if wb.Scene != nil {
return bb.Add(wb.Scene.SceneGeom.Pos)
}
return bb
}
// winPos returns the RenderWindow based position within the
// bounding box of the widget, where the x, y coordinates
// are the proportion across the bounding box to use:
// 0 = left / top, 1 = right / bottom
func (wb *WidgetBase) winPos(x, y float32) image.Point {
bb := wb.winBBox()
sz := bb.Size()
var pt image.Point
pt.X = bb.Min.X + int(math32.Round(float32(sz.X)*x))
pt.Y = bb.Min.Y + int(math32.Round(float32(sz.Y)*y))
return pt
}
//////// Profiling and Benchmarking, controlled by settings app bar
// ProfileToggle turns profiling on or off, which does both
// targeted profiling and global CPU and memory profiling.
func ProfileToggle() { //types:add
if profile.Profiling {
endTargetedProfile()
endCPUMemoryProfile()
} else {
startTargetedProfile()
startCPUMemoryProfile()
}
}
var (
// cpuProfileDir is the directory where the profile started
cpuProfileDir string
// cpuProfileFile is the file created by [startCPUMemoryProfile],
// which needs to be stored so that it can be closed in [endCPUMemoryProfile].
cpuProfileFile *os.File
)
// startCPUMemoryProfile starts the standard Go cpu and memory profiling.
func startCPUMemoryProfile() {
cpuProfileDir, _ = os.Getwd()
cpufnm := filepath.Join(cpuProfileDir, "cpu.prof")
fmt.Println("Starting standard cpu and memory profiling to:", cpufnm)
f, err := os.Create(cpufnm)
if errors.Log(err) == nil {
cpuProfileFile = f
errors.Log(pprof.StartCPUProfile(f))
}
}
// endCPUMemoryProfile ends the standard Go cpu and memory profiling.
func endCPUMemoryProfile() {
memfnm := filepath.Join(cpuProfileDir, "mem.prof")
fmt.Println("Ending standard cpu and memory profiling to:", memfnm)
pprof.StopCPUProfile()
errors.Log(cpuProfileFile.Close())
f, err := os.Create(memfnm)
if errors.Log(err) == nil {
runtime.GC() // get up-to-date statistics
errors.Log(pprof.WriteHeapProfile(f))
errors.Log(f.Close())
}
}
// startTargetedProfile starts targeted profiling using the [profile] package.
func startTargetedProfile() {
fmt.Println("Starting targeted profiling")
profile.Reset()
profile.Profiling = true
}
// endTargetedProfile ends targeted profiling and prints the report.
func endTargetedProfile() {
profile.Report(time.Millisecond)
profile.Profiling = false
}
// 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 core
import (
"fmt"
"image"
"log"
"sync"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/matcolor"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/math32"
"cogentcore.org/core/system"
"golang.org/x/image/draw"
)
// windowWait is a wait group for waiting for all the open window event
// loops to finish. It is incremented by [renderWindow.GoStartEventLoop]
// and decremented when the event loop terminates.
var windowWait sync.WaitGroup
// Wait waits for all windows to close and runs the main app loop.
// This should be put at the end of the main function if
// [Body.RunMainWindow] is not used.
func Wait() {
waitCalled = true
defer func() { system.HandleRecover(recover()) }()
go func() {
defer func() { system.HandleRecover(recover()) }()
windowWait.Wait()
system.TheApp.Quit()
}()
system.TheApp.MainLoop()
}
var (
// currentRenderWindow is the current [renderWindow].
// On single window platforms (mobile, web, and offscreen),
// this is the only render window.
currentRenderWindow *renderWindow
// renderWindowGlobalMu is a mutex for any global state associated with windows
renderWindowGlobalMu sync.Mutex
)
func setCurrentRenderWindow(w *renderWindow) {
renderWindowGlobalMu.Lock()
currentRenderWindow = w
renderWindowGlobalMu.Unlock()
}
// renderWindow provides an outer "actual" window where everything is rendered,
// and is the point of entry for all events coming in from user actions.
//
// renderWindow contents are all managed by the [stages] stack that
// handles main [Stage] elements such as [WindowStage] and [DialogStage], which in
// turn manage their own stack of popup stage elements such as menus and tooltips.
// The contents of each Stage is provided by a Scene, containing Widgets,
// and the Stage Pixels image is drawn to the renderWindow in the renderWindow method.
//
// Rendering is handled by the [system.Drawer]. It is akin to a window manager overlaying Go image bitmaps
// on top of each other in the proper order, based on the [stages] stacking order.
// Sprites are managed by the main stage, as layered textures of the same size,
// to enable unlimited number packed into a few descriptors for standard sizes.
type renderWindow struct {
// name is the name of the window.
name string
// title is the displayed name of window, for window manager etc.
// Window object name is the internal handle and is used for tracking property info etc
title string
// SystemWindow is the OS-specific window interface, which handles
// all the os-specific functions, including delivering events etc
SystemWindow system.Window `json:"-" xml:"-"`
// mains is the stack of main stages in this render window.
// The [RenderContext] in this manager is the original source for all Stages.
mains stages
// noEventsChan is a channel on which a signal is sent when there are
// no events left in the window [events.Deque]. It is used internally
// for event handling in tests.
noEventsChan chan struct{}
// closing is whether the window is closing.
closing bool
// gotFocus indicates that have we received focus.
gotFocus bool
// stopEventLoop indicates that the event loop should be stopped.
stopEventLoop bool
}
// newRenderWindow creates a new window with given internal name handle,
// display name, and options. This is called by Stage.newRenderWindow
// which handles setting the opts and other infrastructure.
func newRenderWindow(name, title string, opts *system.NewWindowOptions) *renderWindow {
w := &renderWindow{}
w.name = name
w.title = title
var err error
w.SystemWindow, err = system.TheApp.NewWindow(opts)
if err != nil {
fmt.Printf("Cogent Core NewRenderWindow error: %v \n", err)
return nil
}
w.SystemWindow.SetName(title)
w.SystemWindow.SetTitleBarIsDark(matcolor.SchemeIsDark)
w.SystemWindow.SetCloseReqFunc(func(win system.Window) {
rc := w.renderContext()
rc.lock()
w.closing = true
// ensure that everyone is closed first
for _, kv := range w.mains.stack.Order {
if kv.Value == nil || kv.Value.Scene == nil || kv.Value.Scene.This == nil {
continue
}
if !kv.Value.Scene.Close() {
w.closing = false
return
}
}
rc.unlock()
win.Close()
})
return w
}
// MainScene returns the current [renderWindow.mains] top Scene,
// which is the current window or full window dialog occupying the RenderWindow.
func (w *renderWindow) MainScene() *Scene {
top := w.mains.top()
if top == nil {
return nil
}
return top.Scene
}
// RecycleMainWindow looks for an existing non-dialog window with the given Data.
// If it finds it, it shows it and returns true. Otherwise, it returns false.
// See [RecycleDialog] for a dialog version.
func RecycleMainWindow(data any) bool {
if data == nil {
return false
}
ew, got := mainRenderWindows.findData(data)
if !got {
return false
}
if DebugSettings.WindowEventTrace {
fmt.Printf("Win: %v getting recycled based on data match\n", ew.name)
}
ew.Raise()
return true
}
// setName sets name of this window and also the RenderWindow, and applies any window
// geometry settings associated with the new name if it is different from before
func (w *renderWindow) setName(name string) {
curnm := w.name
isdif := curnm != name
w.name = name
if w.SystemWindow != nil {
w.SystemWindow.SetName(name)
}
if isdif && w.SystemWindow != nil && !w.SystemWindow.Is(system.Fullscreen) {
wgp, sc := theWindowGeometrySaver.get(w.title, "")
if wgp != nil {
theWindowGeometrySaver.settingStart()
if w.SystemWindow.Size() != wgp.Size || w.SystemWindow.Position(sc) != wgp.Pos {
if DebugSettings.WindowGeometryTrace {
log.Printf("WindowGeometry: SetName setting geom for window: %v pos: %v size: %v\n", w.name, wgp.Pos, wgp.Size)
}
w.SystemWindow.SetGeometry(false, wgp.Pos, wgp.Size, sc)
system.TheApp.SendEmptyEvent()
}
theWindowGeometrySaver.settingEnd()
}
}
}
// setTitle sets title of this window and its underlying SystemWin.
func (w *renderWindow) setTitle(title string) {
w.title = title
if w.SystemWindow != nil {
w.SystemWindow.SetTitle(title)
}
}
// SetStageTitle sets the title of the underlying [system.Window] to the given stage title
// combined with the [renderWindow] title.
func (w *renderWindow) SetStageTitle(title string) {
if title != w.title {
title = title + " • " + w.title
}
w.SystemWindow.SetTitle(title)
}
// logicalDPI returns the current logical dots-per-inch resolution of the
// window, which should be used for most conversion of standard units --
// physical DPI can be found in the Screen
func (w *renderWindow) logicalDPI() float32 {
if w.SystemWindow == nil {
sc := system.TheApp.Screen(0)
if sc == nil {
return 160 // null default
}
return sc.LogicalDPI
}
return w.SystemWindow.LogicalDPI()
}
// stepZoom calls [SetZoom] with the current zoom plus 10 times the given number of steps.
func (w *renderWindow) stepZoom(steps float32) {
sc := w.SystemWindow.Screen()
curZoom := AppearanceSettings.Zoom
screenName := ""
sset, ok := AppearanceSettings.Screens[sc.Name]
if ok {
screenName = sc.Name
curZoom = sset.Zoom
}
w.setZoom(curZoom+10*steps, screenName)
}
// setZoom sets [AppearanceSettingsData.Zoom] to the given value and then triggers
// necessary updating and makes a snackbar. If screenName is non-empty, then the
// zoom is set on the screen-specific settings, instead of the global.
func (w *renderWindow) setZoom(zoom float32, screenName string) {
zoom = math32.Clamp(zoom, 10, 500)
if screenName != "" {
sset := AppearanceSettings.Screens[screenName]
sset.Zoom = zoom
AppearanceSettings.Screens[screenName] = sset
} else {
AppearanceSettings.Zoom = zoom
}
AppearanceSettings.Apply()
UpdateAll()
errors.Log(SaveSettings(AppearanceSettings))
if ms := w.MainScene(); ms != nil {
b := NewBody().AddSnackbarText(fmt.Sprintf("%.f%%", zoom))
NewStretch(b)
b.AddSnackbarIcon(icons.Remove, func(e events.Event) {
w.stepZoom(-1)
})
b.AddSnackbarIcon(icons.Add, func(e events.Event) {
w.stepZoom(1)
})
b.AddSnackbarButton("Reset", func(e events.Event) {
w.setZoom(100, screenName)
})
b.DeleteChildByName("stretch")
b.RunSnackbar(ms)
}
}
// resized updates internal buffers after a window has been resized.
func (w *renderWindow) resized() {
rc := w.renderContext()
if !w.isVisible() {
rc.visible = false
return
}
// drw := w.SystemWindow.Drawer()
w.SystemWindow.Lock()
rg := w.SystemWindow.RenderGeom()
w.SystemWindow.Unlock()
curRg := rc.geom
rc.logicalDPI = w.logicalDPI() // always update
if curRg == rg {
if DebugSettings.WindowEventTrace {
fmt.Printf("Win: %v skipped same-size Resized: %v\n", w.name, curRg)
}
// still need to apply style even if size is same
for _, kv := range w.mains.stack.Order {
st := kv.Value
sc := st.Scene
if st.FullWindow && sc.SceneGeom.Size != rg.Size { // double-check: can be off in fullscreen init
st.Sprites.reset()
sc.resize(rg)
}
sc.applyStyleScene()
}
return
}
// w.FocusInactivate()
if !w.isVisible() {
rc.visible = false
if DebugSettings.WindowEventTrace {
fmt.Printf("Win: %v Resized already closed\n", w.name)
}
return
}
if DebugSettings.WindowEventTrace {
fmt.Printf("Win: %v Resized from: %v to: %v\n", w.name, curRg, rg)
}
rc.geom = rg
rc.visible = true
// fmt.Printf("resize dpi: %v\n", w.LogicalDPI())
w.mains.resize(rg)
if DebugSettings.WindowGeometryTrace {
log.Printf("WindowGeometry: recording from Resize\n")
}
theWindowGeometrySaver.record(w)
}
// Raise requests that the window be at the top of the stack of windows,
// and receive focus. If it is minimized, it will be un-minimized. This
// is the only supported mechanism for un-minimizing. This also sets
// [currentRenderWindow] to the window.
func (w *renderWindow) Raise() {
w.SystemWindow.Raise()
setCurrentRenderWindow(w)
}
// minimize requests that the window be minimized, making it no longer
// visible or active; rendering should not occur for minimized windows.
func (w *renderWindow) minimize() {
w.SystemWindow.Minimize()
}
// closeReq requests that the window be closed, which could be rejected.
// It firsts unlocks and then locks the [renderContext] to prevent deadlocks.
// If this is called asynchronously outside of the main event loop,
// [renderWindow.SystemWin.closeReq] should be called directly instead.
func (w *renderWindow) closeReq() {
rc := w.renderContext()
rc.unlock()
w.SystemWindow.CloseReq()
rc.lock()
}
// closed frees any resources after the window has been closed.
func (w *renderWindow) closed() {
AllRenderWindows.delete(w)
mainRenderWindows.delete(w)
dialogRenderWindows.delete(w)
if DebugSettings.WindowEventTrace {
fmt.Printf("Win: %v Closed\n", w.name)
}
if len(AllRenderWindows) > 0 {
pfw := AllRenderWindows[len(AllRenderWindows)-1]
if DebugSettings.WindowEventTrace {
fmt.Printf("Win: %v getting restored focus after: %v closed\n", pfw.name, w.name)
}
pfw.Raise()
}
}
// isClosed reports if the window has been closed
func (w *renderWindow) isClosed() bool {
return w.SystemWindow.IsClosed() || w.mains.stack.Len() == 0
}
// isVisible is the main visibility check; don't do any window updates if not visible!
func (w *renderWindow) isVisible() bool {
if w == nil || w.SystemWindow == nil || w.isClosed() || w.closing || !w.SystemWindow.IsVisible() {
return false
}
return true
}
// goStartEventLoop starts the event processing loop for this window in a new
// goroutine, and returns immediately. Adds to WindowWait wait group so a main
// thread can wait on that for all windows to close.
func (w *renderWindow) goStartEventLoop() {
windowWait.Add(1)
go w.eventLoop()
}
// todo: fix or remove
// sendWinFocusEvent sends the RenderWinFocusEvent to widgets
func (w *renderWindow) sendWinFocusEvent(act events.WinActions) {
// se := window.NewEvent(act)
// se.Init()
// w.Mains.HandleEvent(se)
}
// eventLoop runs the event processing loop for the RenderWindow -- grabs system
// events for the window and dispatches them to receiving nodes, and manages
// other state etc (popups, etc).
func (w *renderWindow) eventLoop() {
defer func() { system.HandleRecover(recover()) }()
d := &w.SystemWindow.Events().Deque
for {
if w.stopEventLoop {
w.stopEventLoop = false
break
}
e := d.NextEvent()
if w.stopEventLoop {
w.stopEventLoop = false
break
}
w.handleEvent(e)
if w.noEventsChan != nil && len(d.Back) == 0 && len(d.Front) == 0 {
w.noEventsChan <- struct{}{}
}
}
if DebugSettings.WindowEventTrace {
fmt.Printf("Win: %v out of event loop\n", w.name)
}
windowWait.Done()
// our last act must be self destruction!
w.mains.deleteAll()
}
// handleEvent processes given events.Event.
// All event processing operates under a RenderContext.Lock
// so that no rendering update can occur during event-driven updates.
// Because rendering itself is event driven, this extra level of safety
// is redundant in this case, but other non-event-driven updates require
// the lock protection.
func (w *renderWindow) handleEvent(e events.Event) {
rc := w.renderContext()
rc.lock()
// we manually handle Unlock's in this function instead of deferring
// it to avoid a cryptic "sync: can't unlock an already unlocked Mutex"
// error when panicking in the rendering goroutine. This is critical for
// debugging on Android. TODO: maybe figure out a more sustainable approach to this.
et := e.Type()
if DebugSettings.EventTrace && et != events.WindowPaint && et != events.MouseMove {
fmt.Println("Window got event", e)
}
if et >= events.Window && et <= events.WindowPaint {
w.handleWindowEvents(e)
rc.unlock()
return
}
// fmt.Printf("got event type: %v: %v\n", et.BitIndexString(), evi)
w.mains.mainHandleEvent(e)
rc.unlock()
}
func (w *renderWindow) handleWindowEvents(e events.Event) {
et := e.Type()
switch et {
case events.WindowPaint:
e.SetHandled()
rc := w.renderContext()
rc.unlock() // one case where we need to break lock
w.renderWindow()
rc.lock()
w.mains.runDeferred() // note: must be outside of locks in renderWindow
case events.WindowResize:
e.SetHandled()
w.resized()
case events.Window:
ev := e.(*events.WindowEvent)
switch ev.Action {
case events.WinClose:
// fmt.Printf("got close event for window %v \n", w.Name)
e.SetHandled()
w.stopEventLoop = true
w.closed()
case events.WinMinimize:
e.SetHandled()
// on mobile platforms, we need to set the size to 0 so that it detects a size difference
// and lets the size event go through when we come back later
// if Platform().IsMobile() {
// w.Scene.Geom.Size = image.Point{}
// }
case events.WinShow:
e.SetHandled()
// note that this is sent delayed by driver
if DebugSettings.WindowEventTrace {
fmt.Printf("Win: %v got show event\n", w.name)
}
case events.WinMove:
e.SetHandled()
// fmt.Printf("win move: %v\n", w.SystemWin.Position())
if DebugSettings.WindowGeometryTrace {
log.Printf("WindowGeometry: recording from Move\n")
}
w.SystemWindow.ConstrainFrame(true) // top only
theWindowGeometrySaver.record(w)
case events.WinFocus:
// if we are not already the last in AllRenderWins, we go there,
// as this allows focus to be restored to us in the future
if len(AllRenderWindows) > 0 && AllRenderWindows[len(AllRenderWindows)-1] != w {
AllRenderWindows.delete(w)
AllRenderWindows.add(w)
}
if !w.gotFocus {
w.gotFocus = true
w.sendWinFocusEvent(events.WinFocus)
if DebugSettings.WindowEventTrace {
fmt.Printf("Win: %v got focus\n", w.name)
}
} else {
if DebugSettings.WindowEventTrace {
fmt.Printf("Win: %v got extra focus\n", w.name)
}
}
setCurrentRenderWindow(w)
case events.WinFocusLost:
if DebugSettings.WindowEventTrace {
fmt.Printf("Win: %v lost focus\n", w.name)
}
w.gotFocus = false
w.sendWinFocusEvent(events.WinFocusLost)
case events.ScreenUpdate:
if DebugSettings.WindowEventTrace {
log.Println("Win: ScreenUpdate", w.name, screenConfig())
}
if !TheApp.Platform().IsMobile() { // native desktop
if TheApp.NScreens() > 0 {
AppearanceSettings.Apply()
UpdateAll()
theWindowGeometrySaver.restoreAll()
}
} else {
w.resized()
}
}
}
}
//////// Rendering
// renderParams are the key [renderWindow] params that determine if
// a scene needs to be restyled since last render, if these params change.
type renderParams struct {
// logicalDPI is the current logical dots-per-inch resolution of the
// window, which should be used for most conversion of standard units.
logicalDPI float32
// Geometry of the rendering window, in actual "dot" pixels used for rendering.
geom math32.Geom2DInt
}
// needsRestyle returns true if the current render context
// params differ from those used in last render.
func (rp *renderParams) needsRestyle(rc *renderContext) bool {
return rp.logicalDPI != rc.logicalDPI || rp.geom != rc.geom
}
// saveRender grabs current render context params
func (rp *renderParams) saveRender(rc *renderContext) {
rp.logicalDPI = rc.logicalDPI
rp.geom = rc.geom
}
// renderContext provides rendering context from outer RenderWindow
// window to Stage and Scene elements to inform styling, layout
// and rendering. It also has the main Mutex for any updates
// to the window contents: use Lock for anything updating.
type renderContext struct {
// logicalDPI is the current logical dots-per-inch resolution of the
// window, which should be used for most conversion of standard units.
logicalDPI float32
// Geometry of the rendering window, in actual "dot" pixels used for rendering.
geom math32.Geom2DInt
// mu is mutex for locking out rendering and any destructive updates.
// It is locked at the RenderWindow level during rendering and
// event processing to provide exclusive blocking of external updates.
// Use AsyncLock from any outside routine to grab the lock before
// doing modifications.
mu sync.Mutex
// visible is whether the window is visible and should be rendered to.
visible bool
// rebuild is whether to force a rebuild of all Scene elements.
rebuild bool
}
// newRenderContext returns a new [renderContext] initialized according to
// the main Screen size and LogicalDPI as initial defaults.
// The actual window size is set during Resized method, which is typically
// called after the window is created by the OS.
func newRenderContext() *renderContext {
rc := &renderContext{}
scr := system.TheApp.Screen(0)
if scr != nil {
rc.geom.SetRect(image.Rectangle{Max: scr.PixelSize})
rc.logicalDPI = scr.LogicalDPI
} else {
rc.geom = math32.Geom2DInt{Size: image.Pt(1080, 720)}
rc.logicalDPI = 160
}
rc.visible = true
return rc
}
// lock is called by RenderWindow during RenderWindow and HandleEvent
// when updating all widgets and rendering the screen.
// Any outside access to window contents / scene must acquire this
// lock first. In general, use AsyncLock to do this.
func (rc *renderContext) lock() {
rc.mu.Lock()
}
// unlock must be called for each Lock, when done.
func (rc *renderContext) unlock() {
rc.mu.Unlock()
}
func (rc *renderContext) String() string {
str := fmt.Sprintf("Geom: %s Visible: %v", rc.geom, rc.visible)
return str
}
func (sc *Scene) RenderDraw(drw system.Drawer, op draw.Op) {
unchanged := !sc.hasFlag(sceneImageUpdated) || sc.hasFlag(sceneUpdating)
drw.Copy(sc.SceneGeom.Pos, sc.Pixels, sc.Pixels.Bounds(), op, unchanged)
sc.setFlag(false, sceneImageUpdated)
}
func (w *renderWindow) renderContext() *renderContext {
return w.mains.renderContext
}
// renderWindow performs all rendering based on current Stages config.
// It sets the Write lock on RenderContext Mutex, so nothing else can update
// during this time. All other updates are done with a Read lock so they
// won't interfere with each other.
func (w *renderWindow) renderWindow() {
rc := w.renderContext()
rc.lock()
defer func() {
rc.rebuild = false
rc.unlock()
}()
rebuild := rc.rebuild
stageMods, sceneMods := w.mains.updateAll() // handles all Scene / Widget updates!
top := w.mains.top()
if top == nil || w.mains.stack.Len() == 0 {
return
}
if !top.Sprites.Modified && !rebuild && !stageMods && !sceneMods { // nothing to do!
// fmt.Println("no mods") // note: get a ton of these..
return
}
if !w.isVisible() || w.SystemWindow.Is(system.Minimized) {
if DebugSettings.WindowRenderTrace {
fmt.Printf("RenderWindow: skipping update on inactive / minimized window: %v\n", w.name)
}
return
}
if DebugSettings.WindowRenderTrace {
fmt.Println("RenderWindow: doing render:", w.name)
fmt.Println("rebuild:", rebuild, "stageMods:", stageMods, "sceneMods:", sceneMods)
}
if !w.SystemWindow.Lock() {
if DebugSettings.WindowRenderTrace {
fmt.Printf("RenderWindow: window was closed: %v\n", w.name)
}
return
}
defer w.SystemWindow.Unlock()
// pr := profile.Start("win.DrawScenes")
drw := w.SystemWindow.Drawer()
drw.Start()
w.fillInsets()
sm := &w.mains
n := sm.stack.Len()
// first, find the top-level window:
winIndex := 0
var winScene *Scene
for i := n - 1; i >= 0; i-- {
st := sm.stack.ValueByIndex(i)
if st.Type == WindowStage {
if DebugSettings.WindowRenderTrace {
fmt.Println("GatherScenes: main Window:", st.String())
}
winScene = st.Scene
winScene.RenderDraw(drw, draw.Src) // first window blits
winIndex = i
for _, w := range st.Scene.directRenders {
w.RenderDraw(drw, draw.Over)
}
break
}
}
// then add everyone above that
for i := winIndex + 1; i < n; i++ {
st := sm.stack.ValueByIndex(i)
if st.Scrim && i == n-1 {
clr := colors.Uniform(colors.ApplyOpacity(colors.ToUniform(colors.Scheme.Scrim), 0.5))
drw.Copy(image.Point{}, clr, winScene.Geom.TotalBBox, draw.Over, system.Unchanged)
}
st.Scene.RenderDraw(drw, draw.Over)
if DebugSettings.WindowRenderTrace {
fmt.Println("GatherScenes: overlay Stage:", st.String())
}
}
// then add the popups for the top main stage
for _, kv := range top.popups.stack.Order {
st := kv.Value
st.Scene.RenderDraw(drw, draw.Over)
if DebugSettings.WindowRenderTrace {
fmt.Println("GatherScenes: popup:", st.String())
}
}
top.Sprites.drawSprites(drw, winScene.SceneGeom.Pos)
drw.End()
}
// fillInsets fills the window insets, if any, with [colors.Scheme.Background].
// called within the overall drawer.Start() render pass.
func (w *renderWindow) fillInsets() {
// render geom and window geom
rg := w.SystemWindow.RenderGeom()
wg := math32.Geom2DInt{Size: w.SystemWindow.Size()}
// if our window geom is the same as our render geom, we have no
// window insets to fill
if wg == rg {
return
}
drw := w.SystemWindow.Drawer()
fill := func(x0, y0, x1, y1 int) {
r := image.Rect(x0, y0, x1, y1)
if r.Dx() == 0 || r.Dy() == 0 {
return
}
drw.Copy(image.Point{}, colors.Scheme.Background, r, draw.Src, system.Unchanged)
}
rb := rg.Bounds()
wb := wg.Bounds()
fill(0, 0, wb.Max.X, rb.Min.Y) // top
fill(0, rb.Max.Y, wb.Max.X, wb.Max.Y) // bottom
fill(rb.Max.X, 0, wb.Max.X, wb.Max.Y) // right
fill(0, 0, rb.Min.X, wb.Max.Y) // left
}
// 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 (
"image"
"slices"
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
"cogentcore.org/core/enums"
"cogentcore.org/core/events"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/system"
"cogentcore.org/core/tree"
)
// Scene contains a [Widget] tree, rooted in an embedded [Frame] layout,
// which renders into its [Scene.Pixels] image. The [Scene] is set in a
// [Stage], which the [Scene] has a pointer to.
//
// Each [Scene] contains state specific to its particular usage
// within a given [Stage] and overall rendering context, representing the unit
// of rendering in the Cogent Core framework.
type Scene struct { //core:no-new
Frame
// Body provides the main contents of scenes that use control Bars
// to allow the main window contents to be specified separately
// from that dynamic control content. When constructing scenes using
// a [Body], you can operate directly on the [Body], which has wrappers
// for most major Scene functions.
Body *Body `json:"-" xml:"-" set:"-"`
// WidgetInit is a function called on every newly created [Widget].
// This can be used to set global configuration and styling for all
// widgets in conjunction with [App.SceneInit].
WidgetInit func(w Widget) `json:"-" xml:"-" edit:"-"`
// Bars are functions for creating control bars,
// attached to different sides of a [Scene]. Functions
// are called in forward order so first added are called first.
Bars styles.Sides[BarFuncs] `json:"-" xml:"-" set:"-"`
// Data is the optional data value being represented by this scene.
// Used e.g., for recycling views of a given item instead of creating new one.
Data any
// Size and position relative to overall rendering context.
SceneGeom math32.Geom2DInt `edit:"-" set:"-"`
// paint context for rendering
PaintContext paint.Context `copier:"-" json:"-" xml:"-" display:"-" set:"-"`
// live pixels that we render into
Pixels *image.RGBA `copier:"-" json:"-" xml:"-" display:"-" set:"-"`
// event manager for this scene
Events Events `copier:"-" json:"-" xml:"-" set:"-"`
// current stage in which this Scene is set
Stage *Stage `copier:"-" json:"-" xml:"-" set:"-"`
// renderBBoxes indicates to render colored bounding boxes for all of the widgets
// in the scene. This is enabled by the [Inspector] in select element mode.
renderBBoxes bool
// renderBBoxHue is current hue for rendering bounding box in [Scene.RenderBBoxes] mode.
renderBBoxHue float32
// selectedWidget is the currently selected/hovered widget through the [Inspector] selection mode
// that should be highlighted with a background color.
selectedWidget Widget
// selectedWidgetChan is the channel on which the selected widget through the inspect editor
// selection mode is transmitted to the inspect editor after the user is done selecting.
selectedWidgetChan chan Widget `json:"-" xml:"-"`
// lastRender captures key params from last render.
// If different then a new ApplyStyleScene is needed.
lastRender renderParams
// showIter counts up at start of showing a Scene
// to trigger Show event and other steps at start of first show
showIter int
// directRenders are widgets that render directly to the [RenderWindow]
// instead of rendering into the Scene Pixels image.
directRenders []Widget
// flags are atomic bit flags for [Scene] state.
flags sceneFlags
}
// sceneFlags are atomic bit flags for [Scene] state.
// They must be atomic to prevent race conditions.
type sceneFlags int64 //enums:bitflag -trim-prefix scene
const (
// sceneHasShown is whether this scene has been shown.
// This is used to ensure that [events.Show] is only sent once.
sceneHasShown sceneFlags = iota
// sceneUpdating means the Scene is in the process of sceneUpdating.
// It is set for any kind of tree-level update.
// Skip any further update passes until it goes off.
sceneUpdating
// sceneNeedsRender is whether anything in the Scene needs to be re-rendered
// (but not necessarily the whole scene itself).
sceneNeedsRender
// sceneNeedsLayout is whether the Scene needs a new layout pass.
sceneNeedsLayout
// sceneHasDeferred is whether the Scene has elements with Deferred functions.
sceneHasDeferred
// sceneImageUpdated indicates that the Scene's image has been updated
// e.g., due to a render or a resize. This is reset by the
// global [RenderWindow] rendering pass, so it knows whether it needs to
// copy the image up to the GPU or not.
sceneImageUpdated
// sceneContentSizing means that this scene is currently doing a
// contentSize computation to compute the size of the scene
// (for sizing window for example). Affects layout size computation.
sceneContentSizing
)
// hasFlag returns whether the given flag is set.
func (sc *Scene) hasFlag(f sceneFlags) bool {
return sc.flags.HasFlag(f)
}
// setFlag sets the given flags to the given value.
func (sc *Scene) setFlag(on bool, f ...enums.BitFlag) {
sc.flags.SetFlag(on, f...)
}
// newBodyScene creates a new Scene for use with an associated Body that
// contains the main content of the Scene (e.g., a Window, Dialog, etc).
// It will be constructed from the Bars-configured control bars on each
// side, with the given Body as the central content.
func newBodyScene(body *Body) *Scene {
sc := NewScene(body.Name + " scene")
sc.Body = body
// need to set parent immediately so that SceneInit works,
// but can not add it yet because it may go elsewhere due
// to app bars
tree.SetParent(body, sc)
return sc
}
// NewScene creates a new [Scene] object without a [Body], e.g., for use
// in a Menu, Tooltip or other such simple popups or non-control-bar Scenes.
func NewScene(name ...string) *Scene {
sc := tree.New[Scene]()
if len(name) > 0 {
sc.SetName(name[0])
}
sc.Events.scene = sc
return sc
}
func (sc *Scene) Init() {
sc.Scene = sc
sc.Frame.Init()
sc.AddContextMenu(sc.standardContextMenu)
sc.Styler(func(s *styles.Style) {
s.Cursor = cursors.Arrow
s.Background = colors.Scheme.Background
s.Color = colors.Scheme.OnBackground
// we never want borders on scenes
s.MaxBorder = styles.Border{}
s.Direction = styles.Column
s.Overflow.Set(styles.OverflowAuto) // screen is always scroller of last resort
// insets and minimum window padding
if sc.Stage == nil {
return
}
if sc.Stage.Type.isPopup() || (sc.Stage.Type == DialogStage && !sc.Stage.FullWindow) {
return
}
s.Padding.Set(units.Dp(8))
})
sc.OnShow(func(e events.Event) {
currentRenderWindow.SetStageTitle(sc.Stage.Title)
})
sc.OnClose(func(e events.Event) {
sm := sc.Stage.Mains
if sm == nil {
return
}
sm.mu.RLock()
defer sm.mu.RUnlock()
if sm.stack.Len() < 2 {
return
}
// the stage that will be visible next
st := sm.stack.ValueByIndex(sm.stack.Len() - 2)
currentRenderWindow.SetStageTitle(st.Title)
})
sc.Updater(func() {
// At the scene level, we reset the shortcuts and add our context menu
// shortcuts every time. This clears the way for buttons to add their
// shortcuts in their own Updaters. We must get the shortcuts every time
// since buttons may be added or removed dynamically.
sc.Events.shortcuts = nil
tmps := NewScene()
sc.applyContextMenus(tmps)
sc.Events.getShortcutsIn(tmps)
})
if TheApp.SceneInit != nil {
TheApp.SceneInit(sc)
}
}
// renderContext returns the current render context.
// This will be nil prior to actual rendering.
func (sc *Scene) renderContext() *renderContext {
if sc.Stage == nil {
return nil
}
sm := sc.Stage.Mains
if sm == nil {
return nil
}
return sm.renderContext
}
// RenderWindow returns the current render window for this scene.
// In general it is best to go through [renderContext] instead of the window.
// This will be nil prior to actual rendering.
func (sc *Scene) RenderWindow() *renderWindow {
if sc.Stage == nil {
return nil
}
sm := sc.Stage.Mains
if sm == nil {
return nil
}
return sm.renderWindow
}
// fitInWindow fits Scene geometry (pos, size) into given window geom.
// Calls resize for the new size.
func (sc *Scene) fitInWindow(winGeom math32.Geom2DInt) {
geom := sc.SceneGeom
// full offscreen windows ignore any window geometry constraints
// because they must be unbounded by any previous window sizes
if TheApp.Platform() != system.Offscreen || !sc.Stage.FullWindow {
geom = geom.FitInWindow(winGeom)
}
sc.resize(geom)
sc.SceneGeom.Pos = geom.Pos
// fmt.Println("win", winGeom, "geom", geom)
}
// resize resizes the scene, creating a new image; updates Geom
func (sc *Scene) resize(geom math32.Geom2DInt) {
if geom.Size.X <= 0 || geom.Size.Y <= 0 {
return
}
if sc.PaintContext.State == nil {
sc.PaintContext.State = &paint.State{}
}
if sc.PaintContext.Paint == nil {
sc.PaintContext.Paint = &styles.Paint{}
}
sc.SceneGeom.Pos = geom.Pos
if sc.Pixels == nil || sc.Pixels.Bounds().Size() != geom.Size {
sc.Pixels = image.NewRGBA(image.Rectangle{Max: geom.Size})
}
sc.PaintContext.Init(geom.Size.X, geom.Size.Y, sc.Pixels)
sc.SceneGeom.Size = geom.Size // make sure
sc.updateScene()
sc.applyStyleScene()
// restart the multi-render updating after resize, to get windows to update correctly while
// resizing on Windows (OS) and Linux (see https://github.com/cogentcore/core/issues/584), to get
// windows on Windows (OS) to update after a window snap (see https://github.com/cogentcore/core/issues/497),
// and to get FillInsets to overwrite mysterious black bars that otherwise are rendered on both iOS
// and Android in different contexts.
// TODO(kai): is there a more efficient way to do this, and do we need to do this on all platforms?
sc.showIter = 0
sc.NeedsLayout()
}
// ResizeToContent resizes the scene so it fits the current content.
// Only applicable to desktop systems where windows can be resized.
// Optional extra size is added to the amount computed to hold the contents,
// which is needed in cases with wrapped text elements, which don't
// always size accurately. See [Scene.SetGeometry] for a more general way
// to set all window geometry properties.
func (sc *Scene) ResizeToContent(extra ...image.Point) {
if TheApp.Platform().IsMobile() { // not resizable
return
}
win := sc.RenderWindow()
if win == nil {
return
}
go func() {
scsz := system.TheApp.Screen(0).PixelSize
sz := sc.contentSize(scsz)
if len(extra) == 1 {
sz = sz.Add(extra[0])
}
win.SystemWindow.SetSize(sz)
}()
}
// SetGeometry uses [system.Window.SetGeometry] to set all window geometry properties,
// with pos in operating system window manager units and size in raw pixels.
// If pos and/or size is not specified, it defaults to the current value.
// If fullscreen is true, pos and size are ignored, and screen indicates the number
// of the screen on which to fullscreen the window. If fullscreen is false, the
// window is moved to the given pos and size on the given screen. If screen is -1,
// the current screen the window is on is used, and fullscreen/pos/size are all
// relative to that screen. It is only applicable on desktop and web platforms,
// with only fullscreen supported on web. See [Scene.SetFullscreen] for a simpler way
// to set only the fullscreen state. See [Scene.ResizeToContent] to resize the window
// to fit the current content.
func (sc *Scene) SetGeometry(fullscreen bool, pos image.Point, size image.Point, screen int) {
rw := sc.RenderWindow()
if rw == nil {
return
}
scr := TheApp.Screen(screen)
if screen < 0 {
scr = rw.SystemWindow.Screen()
}
rw.SystemWindow.SetGeometry(fullscreen, pos, size, scr)
}
// IsFullscreen returns whether the window associated with this [Scene]
// is in fullscreen mode (true) or window mode (false). This is implemented
// on desktop and web platforms. See [Scene.SetFullscreen] to update the
// current fullscreen state and [Stage.SetFullscreen] to set the initial state.
func (sc *Scene) IsFullscreen() bool {
rw := sc.RenderWindow()
if rw == nil {
return false
}
return rw.SystemWindow.Is(system.Fullscreen)
}
// SetFullscreen requests that the window associated with this [Scene]
// be updated to either fullscreen mode (true) or window mode (false).
// This is implemented on desktop and web platforms. See [Scene.IsFullscreen]
// to get the current fullscreen state and [Stage.SetFullscreen] to set the
// initial state. ([Stage.SetFullscreen] sets the initial state, whereas
// this function sets the current state after the [Stage] is already running).
// See [Scene.SetGeometry] for a more general way to set all window
// geometry properties.
func (sc *Scene) SetFullscreen(fullscreen bool) {
rw := sc.RenderWindow()
if rw == nil {
return
}
wgp, screen := theWindowGeometrySaver.get(rw.title, "")
if wgp != nil {
rw.SystemWindow.SetGeometry(fullscreen, wgp.Pos, wgp.Size, screen)
} else {
rw.SystemWindow.SetGeometry(fullscreen, image.Point{}, image.Point{}, rw.SystemWindow.Screen())
}
}
// Close closes the [Stage] associated with this [Scene].
// This only works for main stages (windows and dialogs).
// It returns whether the [Stage] was successfully closed.
func (sc *Scene) Close() bool {
if sc == nil {
return true
}
e := &events.Base{Typ: events.Close}
e.Init()
sc.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
cw.AsWidget().HandleEvent(e)
return tree.Continue
})
// if they set the event as handled, we do not close the scene
if e.IsHandled() {
return false
}
mm := sc.Stage.Mains
if mm == nil {
return false // todo: needed, but not sure why
}
mm.deleteStage(sc.Stage)
if sc.Stage.NewWindow && !TheApp.Platform().IsMobile() && !mm.renderWindow.closing && !mm.renderWindow.stopEventLoop && !TheApp.IsQuitting() {
mm.renderWindow.closeReq()
}
return true
}
func (sc *Scene) ApplyScenePos() {
sc.Frame.ApplyScenePos()
if sc.Parts == nil {
return
}
mvi := sc.Parts.ChildByName("move", 1)
if mvi == nil {
return
}
mv := mvi.(Widget).AsWidget()
sc.Parts.Geom.Pos.Total.Y = math32.Ceil(0.5 * mv.Geom.Size.Actual.Total.Y)
sc.Parts.Geom.Size.Actual = sc.Geom.Size.Actual
sc.Parts.setContentPosFromPos()
sc.Parts.setBBoxesFromAllocs()
sc.Parts.applyScenePosChildren()
psz := sc.Parts.Geom.Size.Actual.Content
mv.Geom.RelPos.X = 0.5*psz.X - 0.5*mv.Geom.Size.Actual.Total.X
mv.Geom.RelPos.Y = 0
mv.setPosFromParent()
mv.setBBoxesFromAllocs()
rszi := sc.Parts.ChildByName("resize", 1)
if rszi == nil {
return
}
rsz := rszi.(Widget).AsWidget()
rsz.Geom.RelPos.X = psz.X // - 0.5*rsz.Geom.Size.Actual.Total.X
rsz.Geom.RelPos.Y = psz.Y // - 0.5*rsz.Geom.Size.Actual.Total.Y
rsz.setPosFromParent()
rsz.setBBoxesFromAllocs()
}
func (sc *Scene) AddDirectRender(w Widget) {
sc.directRenders = append(sc.directRenders, w)
}
func (sc *Scene) DeleteDirectRender(w Widget) {
idx := slices.Index(sc.directRenders, w)
if idx >= 0 {
sc.directRenders = slices.Delete(sc.directRenders, idx, idx+1)
}
}
// 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 (
"image"
"time"
"cogentcore.org/core/events"
"cogentcore.org/core/events/key"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
)
// autoScrollRate determines the rate of auto-scrolling of layouts
var autoScrollRate = float32(1)
// hasAnyScroll returns true if the frame has any scrollbars.
func (fr *Frame) hasAnyScroll() bool {
return fr.HasScroll[math32.X] || fr.HasScroll[math32.Y]
}
// ScrollGeom returns the target position and size for scrollbars
func (fr *Frame) ScrollGeom(d math32.Dims) (pos, sz math32.Vector2) {
sbw := math32.Ceil(fr.Styles.ScrollbarWidth.Dots)
od := d.Other()
bbmin := math32.FromPoint(fr.Geom.ContentBBox.Min)
bbmax := math32.FromPoint(fr.Geom.ContentBBox.Max)
if fr.This != fr.Scene.This { // if not the scene, keep inside the scene
bbmin.SetMax(math32.FromPoint(fr.Scene.Geom.ContentBBox.Min))
bbmax.SetMin(math32.FromPoint(fr.Scene.Geom.ContentBBox.Max).SubScalar(sbw))
}
pos.SetDim(d, bbmin.Dim(d))
pos.SetDim(od, bbmax.Dim(od))
bbsz := bbmax.Sub(bbmin)
sz.SetDim(d, bbsz.Dim(d)-4)
sz.SetDim(od, sbw)
sz = sz.Ceil()
return
}
// ConfigScrolls configures any scrollbars that have been enabled
// during the Layout process. This is called during Position, once
// the sizing and need for scrollbars has been established.
// The final position of the scrollbars is set during ScenePos in
// PositionScrolls. Scrolls are kept around in general.
func (fr *Frame) ConfigScrolls() {
for d := math32.X; d <= math32.Y; d++ {
if fr.HasScroll[d] {
fr.configScroll(d)
}
}
}
// configScroll configures scroll for given dimension
func (fr *Frame) configScroll(d math32.Dims) {
if fr.scrolls[d] != nil {
return
}
fr.scrolls[d] = NewSlider()
sb := fr.scrolls[d]
tree.SetParent(sb, fr)
// sr.SetFlag(true, tree.Field) // note: do not turn on -- breaks pos
sb.SetType(SliderScrollbar)
sb.InputThreshold = 1
sb.Min = 0.0
sb.Styler(func(s *styles.Style) {
s.Direction = styles.Directions(d)
s.Padding.Zero()
s.Margin.Zero()
s.MaxBorder.Width.Zero()
s.Border.Width.Zero()
s.FillMargin = false
})
sb.FinalStyler(func(s *styles.Style) {
od := d.Other()
_, sz := fr.This.(Layouter).ScrollGeom(d)
if sz.X > 0 && sz.Y > 0 {
s.SetState(false, states.Invisible)
s.Min.SetDim(d, units.Dot(sz.Dim(d)))
s.Min.SetDim(od, units.Dot(sz.Dim(od)))
} else {
s.SetState(true, states.Invisible)
}
s.Max = s.Min
})
sb.OnInput(func(e events.Event) {
e.SetHandled()
fr.This.(Layouter).ScrollChanged(d, sb)
})
sb.Update()
}
// ScrollChanged is called in the OnInput event handler for updating,
// when the scrollbar value has changed, for given dimension.
// This is part of the Layouter interface.
func (fr *Frame) ScrollChanged(d math32.Dims, sb *Slider) {
fr.Geom.Scroll.SetDim(d, -sb.Value)
fr.This.(Layouter).ApplyScenePos() // computes updated positions
fr.NeedsRender()
}
// ScrollUpdateFromGeom updates the scrollbar for given dimension
// based on the current Geom.Scroll value for that dimension.
// This can be used to programatically update the scroll value.
func (fr *Frame) ScrollUpdateFromGeom(d math32.Dims) {
if !fr.HasScroll[d] || fr.scrolls[d] == nil {
return
}
sb := fr.scrolls[d]
cv := fr.Geom.Scroll.Dim(d)
sb.setValueEvent(-cv)
fr.This.(Layouter).ApplyScenePos() // computes updated positions
fr.NeedsRender()
}
// ScrollValues returns the maximum size that could be scrolled,
// the visible size (which could be less than the max size, in which
// case no scrollbar is needed), and visSize / maxSize as the VisiblePct.
// This is used in updating the scrollbar and determining whether one is
// needed in the first place
func (fr *Frame) ScrollValues(d math32.Dims) (maxSize, visSize, visPct float32) {
sz := &fr.Geom.Size
maxSize = sz.Internal.Dim(d)
visSize = sz.Alloc.Content.Dim(d)
visPct = visSize / maxSize
return
}
// SetScrollParams sets scrollbar parameters. Must set Step and PageStep,
// but can also set others as needed.
// Max and VisiblePct are automatically set based on ScrollValues maxSize, visPct.
func (fr *Frame) SetScrollParams(d math32.Dims, sb *Slider) {
sb.Step = fr.Styles.Font.Size.Dots // step by lines
sb.PageStep = 10.0 * sb.Step // todo: more dynamic
}
// PositionScrolls arranges scrollbars
func (fr *Frame) PositionScrolls() {
for d := math32.X; d <= math32.Y; d++ {
if fr.HasScroll[d] && fr.scrolls[d] != nil {
fr.positionScroll(d)
} else {
fr.Geom.Scroll.SetDim(d, 0)
}
}
}
func (fr *Frame) positionScroll(d math32.Dims) {
sb := fr.scrolls[d]
pos, ssz := fr.This.(Layouter).ScrollGeom(d)
maxSize, _, visPct := fr.This.(Layouter).ScrollValues(d)
if sb.Geom.Pos.Total == pos && sb.Geom.Size.Actual.Content == ssz && sb.visiblePercent == visPct {
return
}
if ssz.X <= 0 || ssz.Y <= 0 {
sb.SetState(true, states.Invisible)
return
}
sb.SetState(false, states.Invisible)
sb.Max = maxSize
sb.setVisiblePercent(visPct)
// fmt.Println(ly, d, "vis pct:", asz/csz)
sb.SetValue(sb.Value) // keep in range
fr.This.(Layouter).SetScrollParams(d, sb)
sb.Restyle() // applies style
sb.SizeUp()
sb.Geom.Size.Alloc = fr.Geom.Size.Actual
sb.SizeDown(0)
sb.Geom.Pos.Total = pos
sb.setContentPosFromPos()
// note: usually these are intersected with parent *content* bbox,
// but scrolls are specifically outside of that.
sb.setBBoxesFromAllocs()
}
// RenderScrolls renders the scrollbars.
func (fr *Frame) RenderScrolls() {
for d := math32.X; d <= math32.Y; d++ {
if fr.HasScroll[d] && fr.scrolls[d] != nil {
fr.scrolls[d].RenderWidget()
}
}
}
// setScrollsOff turns off the scrollbars.
func (fr *Frame) setScrollsOff() {
for d := math32.X; d <= math32.Y; d++ {
fr.HasScroll[d] = false
}
}
// scrollActionDelta moves the scrollbar in given dimension by given delta.
// returns whether actually scrolled.
func (fr *Frame) scrollActionDelta(d math32.Dims, delta float32) bool {
if fr.HasScroll[d] && fr.scrolls[d] != nil {
sb := fr.scrolls[d]
nval := sb.Value + sb.scrollScale(delta)
chg := sb.setValueEvent(nval)
if chg {
fr.NeedsRender() // only render needed -- scroll updates pos
}
return chg
}
return false
}
// scrollDelta processes a scroll event. If only one dimension is processed,
// and there is a non-zero in other, then the consumed dimension is reset to 0
// and the event is left unprocessed, so a higher level can consume the
// remainder.
func (fr *Frame) scrollDelta(e events.Event) {
se := e.(*events.MouseScroll)
fdel := se.Delta
hasShift := e.HasAnyModifier(key.Shift, key.Alt) // shift or alt indicates to scroll horizontally
if hasShift {
if !fr.HasScroll[math32.X] { // if we have shift, we can only horizontal scroll
return
}
if fr.scrollActionDelta(math32.X, fdel.Y) {
e.SetHandled()
}
return
}
if fr.HasScroll[math32.Y] && fr.HasScroll[math32.X] {
ch1 := fr.scrollActionDelta(math32.Y, fdel.Y)
ch2 := fr.scrollActionDelta(math32.X, fdel.X)
if ch1 || ch2 {
e.SetHandled()
}
} else if fr.HasScroll[math32.Y] {
if fr.scrollActionDelta(math32.Y, fdel.Y) {
e.SetHandled()
}
} else if fr.HasScroll[math32.X] {
if se.Delta.X != 0 {
if fr.scrollActionDelta(math32.X, fdel.X) {
e.SetHandled()
}
} else if se.Delta.Y != 0 {
if fr.scrollActionDelta(math32.X, fdel.Y) {
e.SetHandled()
}
}
}
}
// parentScrollFrame returns the first parent frame that has active scrollbars.
func (wb *WidgetBase) parentScrollFrame() *Frame {
ly := tree.ParentByType[Layouter](wb)
if ly == nil {
return nil
}
fr := ly.AsFrame()
if fr.hasAnyScroll() {
return fr
}
return fr.parentScrollFrame()
}
// ScrollToThis tells this widget's parent frame to scroll to keep
// this widget in view. It returns whether any scrolling was done.
func (wb *WidgetBase) ScrollToThis() bool {
if wb.This == nil {
return false
}
fr := wb.parentScrollFrame()
if fr == nil {
return false
}
return fr.scrollToWidget(wb.This.(Widget))
}
// scrollToWidget scrolls the layout to ensure that the given widget is in view.
// It returns whether scrolling was needed.
func (fr *Frame) scrollToWidget(w Widget) bool {
// note: critical to NOT use BBox b/c it is zero for invisible items!
box := w.AsWidget().Geom.totalRect()
if box.Size() == (image.Point{}) {
return false
}
return fr.ScrollToBox(box)
}
// autoScrollDim auto-scrolls along one dimension, based on the current
// position value, which is in the current scroll value range.
func (fr *Frame) autoScrollDim(d math32.Dims, pos float32) bool {
if !fr.HasScroll[d] || fr.scrolls[d] == nil {
return false
}
sb := fr.scrolls[d]
smax := sb.effectiveMax()
ssz := sb.scrollThumbValue()
dst := sb.Step * autoScrollRate
mind := max(0, (pos - sb.Value))
maxd := max(0, (sb.Value+ssz)-pos)
if mind <= maxd {
pct := mind / ssz
if pct < .1 && sb.Value > 0 {
dst = min(dst, sb.Value)
sb.setValueEvent(sb.Value - dst)
return true
}
} else {
pct := maxd / ssz
if pct < .1 && sb.Value < smax {
dst = min(dst, (smax - sb.Value))
sb.setValueEvent(sb.Value + dst)
return true
}
}
return false
}
var lastAutoScroll time.Time
// AutoScroll scrolls the layout based on given position in scroll
// coordinates (i.e., already subtracing the BBox Min for a mouse event).
func (fr *Frame) AutoScroll(pos math32.Vector2) bool {
now := time.Now()
lag := now.Sub(lastAutoScroll)
if lag < SystemSettings.LayoutAutoScrollDelay {
return false
}
did := false
if fr.HasScroll[math32.Y] && fr.HasScroll[math32.X] {
did = fr.autoScrollDim(math32.Y, pos.Y)
did = did || fr.autoScrollDim(math32.X, pos.X)
} else if fr.HasScroll[math32.Y] {
did = fr.autoScrollDim(math32.Y, pos.Y)
} else if fr.HasScroll[math32.X] {
did = fr.autoScrollDim(math32.X, pos.X)
}
if did {
lastAutoScroll = time.Now()
}
return did
}
// scrollToBoxDim scrolls to ensure that given target [min..max] range
// along one dimension is in view. Returns true if scrolling was needed
func (fr *Frame) scrollToBoxDim(d math32.Dims, tmini, tmaxi int) bool {
if !fr.HasScroll[d] || fr.scrolls[d] == nil {
return false
}
sb := fr.scrolls[d]
if sb == nil || sb.This == nil {
return false
}
tmin, tmax := float32(tmini), float32(tmaxi)
cmin, cmax := fr.Geom.contentRangeDim(d)
if tmin >= cmin && tmax <= cmax {
return false
}
h := fr.Styles.Font.Size.Dots
if tmin < cmin { // favors scrolling to start
trg := sb.Value + tmin - cmin - h
if trg < 0 {
trg = 0
}
sb.setValueEvent(trg)
return true
} else {
if (tmax - tmin) < sb.scrollThumbValue() { // only if whole thing fits
trg := sb.Value + float32(tmax-cmax) + h
sb.setValueEvent(trg)
return true
}
}
return false
}
// ScrollToBox scrolls the layout to ensure that given rect box is in view.
// Returns true if scrolling was needed
func (fr *Frame) ScrollToBox(box image.Rectangle) bool {
did := false
if fr.HasScroll[math32.Y] && fr.HasScroll[math32.X] {
did = fr.scrollToBoxDim(math32.Y, box.Min.Y, box.Max.Y)
did = did || fr.scrollToBoxDim(math32.X, box.Min.X, box.Max.X)
} else if fr.HasScroll[math32.Y] {
did = fr.scrollToBoxDim(math32.Y, box.Min.Y, box.Max.Y)
} else if fr.HasScroll[math32.X] {
did = fr.scrollToBoxDim(math32.X, box.Min.X, box.Max.X)
}
if did {
fr.NeedsRender()
}
return did
}
// ScrollDimToStart scrolls to put the given child coordinate position (eg.,
// top / left of a view box) at the start (top / left) of our scroll area, to
// the extent possible. Returns true if scrolling was needed.
func (fr *Frame) ScrollDimToStart(d math32.Dims, posi int) bool {
if !fr.HasScroll[d] {
return false
}
pos := float32(posi)
cmin, _ := fr.Geom.contentRangeDim(d)
if pos == cmin {
return false
}
sb := fr.scrolls[d]
trg := math32.Clamp(sb.Value+(pos-cmin), 0, sb.effectiveMax())
sb.setValueEvent(trg)
return true
}
// ScrollDimToContentStart is a helper function that scrolls the layout to the
// start of its content (ie: moves the scrollbar to the very start).
func (fr *Frame) ScrollDimToContentStart(d math32.Dims) bool {
if !fr.HasScroll[d] || fr.scrolls[d] == nil {
return false
}
sb := fr.scrolls[d]
sb.setValueEvent(0)
return true
}
// ScrollDimToEnd scrolls to put the given child coordinate position (eg.,
// bottom / right of a view box) at the end (bottom / right) of our scroll
// area, to the extent possible. Returns true if scrolling was needed.
func (fr *Frame) ScrollDimToEnd(d math32.Dims, posi int) bool {
if !fr.HasScroll[d] || fr.scrolls[d] == nil {
return false
}
pos := float32(posi)
_, cmax := fr.Geom.contentRangeDim(d)
if pos == cmax {
return false
}
sb := fr.scrolls[d]
trg := math32.Clamp(sb.Value+(pos-cmax), 0, sb.effectiveMax())
sb.setValueEvent(trg)
return true
}
// ScrollDimToContentEnd is a helper function that scrolls the layout to the
// end of its content (ie: moves the scrollbar to the very end).
func (fr *Frame) ScrollDimToContentEnd(d math32.Dims) bool {
if !fr.HasScroll[d] || fr.scrolls[d] == nil {
return false
}
sb := fr.scrolls[d]
sb.setValueEvent(sb.effectiveMax())
return true
}
// ScrollDimToCenter scrolls to put the given child coordinate position (eg.,
// middle of a view box) at the center of our scroll area, to the extent
// possible. Returns true if scrolling was needed.
func (fr *Frame) ScrollDimToCenter(d math32.Dims, posi int) bool {
if !fr.HasScroll[d] || fr.scrolls[d] == nil {
return false
}
pos := float32(posi)
cmin, cmax := fr.Geom.contentRangeDim(d)
mid := 0.5 * (cmin + cmax)
if pos == mid {
return false
}
sb := fr.scrolls[d]
trg := math32.Clamp(sb.Value+(pos-mid), 0, sb.effectiveMax())
sb.setValueEvent(trg)
return true
}
// 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 (
"cogentcore.org/core/colors"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
)
// Separator draws a separator line. It goes in the direction
// specified by [styles.Style.Direction].
type Separator struct {
WidgetBase
}
func (sp *Separator) Init() {
sp.WidgetBase.Init()
sp.Styler(func(s *styles.Style) {
s.Align.Self = styles.Center
s.Justify.Self = styles.Center
s.Background = colors.Scheme.OutlineVariant
})
sp.FinalStyler(func(s *styles.Style) {
if s.Direction == styles.Row {
s.Grow.Set(1, 0)
s.Min.Y.Dp(1)
s.Margin.SetHorizontal(units.Dp(6))
} else {
s.Grow.Set(0, 1)
s.Min.X.Dp(1)
s.Margin.SetVertical(units.Dp(6))
}
})
}
// 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 core
import (
"image/color"
"io/fs"
"os"
"os/user"
"path/filepath"
"reflect"
"time"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/iox/jsonx"
"cogentcore.org/core/base/iox/tomlx"
"cogentcore.org/core/base/option"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/base/stringsx"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/colors/matcolor"
"cogentcore.org/core/cursors/cursorimg"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/paint"
"cogentcore.org/core/system"
"cogentcore.org/core/tree"
)
// AllSettings is a global slice containing all of the user [Settings]
// that the user will see in the settings window. It contains the base Cogent Core
// settings by default and should be modified by other apps to add their
// app settings.
var AllSettings = []Settings{AppearanceSettings, SystemSettings, DeviceSettings, DebugSettings}
// Settings is the interface that describes the functionality common
// to all settings data types.
type Settings interface {
// Label returns the label text for the settings.
Label() string
// Filename returns the full filename/filepath at which the settings are stored.
Filename() string
// Defaults sets the default values for all of the settings.
Defaults()
// Apply does anything necessary to apply the settings to the app.
Apply()
// MakeToolbar is an optional method that settings objects can implement in order to
// configure the settings view toolbar with settings-related actions that the user can
// perform.
MakeToolbar(p *tree.Plan)
}
// SettingsOpener is an optional additional interface that
// [Settings] can satisfy to customize the behavior of [openSettings].
type SettingsOpener interface {
Settings
// Open opens the settings
Open() error
}
// SettingsSaver is an optional additional interface that
// [Settings] can satisfy to customize the behavior of [SaveSettings].
type SettingsSaver interface {
Settings
// Save saves the settings
Save() error
}
// SettingsBase contains base settings logic that other settings data types can extend.
type SettingsBase struct {
// Name is the name of the settings.
Name string `display:"-" save:"-"`
// File is the full filename/filepath at which the settings are stored.
File string `display:"-" save:"-"`
}
// Label returns the label text for the settings.
func (sb *SettingsBase) Label() string {
return sb.Name
}
// Filename returns the full filename/filepath at which the settings are stored.
func (sb *SettingsBase) Filename() string {
return sb.File
}
// Defaults does nothing by default and can be extended by other settings data types.
func (sb *SettingsBase) Defaults() {}
// Apply does nothing by default and can be extended by other settings data types.
func (sb *SettingsBase) Apply() {}
// MakeToolbar does nothing by default and can be extended by other settings data types.
func (sb *SettingsBase) MakeToolbar(p *tree.Plan) {}
// openSettings opens the given settings from their [Settings.Filename].
// The settings are assumed to be in TOML unless they have a .json file
// extension. If they satisfy the [SettingsOpener] interface,
// [SettingsOpener.Open] will be used instead.
func openSettings(se Settings) error {
if so, ok := se.(SettingsOpener); ok {
return so.Open()
}
fnm := se.Filename()
if filepath.Ext(fnm) == ".json" {
return jsonx.Open(se, fnm)
}
return tomlx.Open(se, fnm)
}
// SaveSettings saves the given settings to their [Settings.Filename].
// The settings will be encoded in TOML unless they have a .json file
// extension. If they satisfy the [SettingsSaver] interface,
// [SettingsSaver.Save] will be used instead. Any non default
// fields are not saved, following [reflectx.NonDefaultFields].
func SaveSettings(se Settings) error {
if ss, ok := se.(SettingsSaver); ok {
return ss.Save()
}
fnm := se.Filename()
ndf := reflectx.NonDefaultFields(se)
if filepath.Ext(fnm) == ".json" {
return jsonx.Save(ndf, fnm)
}
return tomlx.Save(ndf, fnm)
}
// resetSettings resets the given settings to their default values.
func resetSettings(se Settings) error {
err := os.RemoveAll(se.Filename())
if err != nil {
return err
}
npv := reflectx.NonPointerValue(reflect.ValueOf(se))
// we only reset the non-default fields to avoid removing the base
// information (name, filename, etc)
ndf := reflectx.NonDefaultFields(se)
for f := range ndf {
rf := npv.FieldByName(f)
rf.Set(reflect.Zero(rf.Type()))
}
return loadSettings(se)
}
// resetAllSettings resets all of the settings to their default values.
func resetAllSettings() error { //types:add
for _, se := range AllSettings {
err := resetSettings(se)
if err != nil {
return err
}
}
UpdateAll()
return nil
}
// loadSettings sets the defaults of, opens, and applies the given settings.
// If they are not already saved, it saves them. It process their `default:` struct
// tags in addition to calling their [Settings.Default] method.
func loadSettings(se Settings) error {
errors.Log(reflectx.SetFromDefaultTags(se))
se.Defaults()
err := openSettings(se)
// we always apply the settings even if we can't open them
// to apply at least the default values
se.Apply()
if errors.Is(err, fs.ErrNotExist) {
return nil // it is okay for settings to not be saved
}
return err
}
// LoadAllSettings sets the defaults of, opens, and applies [AllSettings].
func LoadAllSettings() error {
errs := []error{}
for _, se := range AllSettings {
err := loadSettings(se)
if err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
// UpdateSettings applies and saves the given settings in the context of the given
// widget and then updates all windows and triggers a full render rebuild.
func UpdateSettings(ctx Widget, se Settings) {
se.Apply()
ErrorSnackbar(ctx, SaveSettings(se), "Error saving "+se.Label()+" settings")
UpdateAll()
}
// UpdateAll updates all windows and triggers a full render rebuild.
// It is typically called when user settings are changed.
func UpdateAll() { //types:add
// Some caches are invalid now:
clear(gradient.Cache)
clear(cursorimg.Cursors)
for _, w := range AllRenderWindows {
rc := w.mains.renderContext
rc.logicalDPI = w.logicalDPI()
rc.rebuild = true // trigger full rebuild
}
}
// AppearanceSettings are the currently active global Cogent Core appearance settings.
var AppearanceSettings = &AppearanceSettingsData{
SettingsBase: SettingsBase{
Name: "Appearance",
File: filepath.Join(TheApp.CogentCoreDataDir(), "appearance-settings.toml"),
},
}
// AppearanceSettingsData is the data type for the global Cogent Core appearance settings.
type AppearanceSettingsData struct { //types:add
SettingsBase
// the color theme.
Theme Themes `default:"Auto"`
// the primary color used to generate the color scheme.
Color color.RGBA `default:"#4285f4"`
// overall zoom factor as a percentage of the default zoom.
// Use Control +/- keyboard shortcut to change zoom level anytime.
// Screen-specific zoom factor will be used if present, see 'Screens' field.
Zoom float32 `default:"100" min:"10" max:"500" step:"10" format:"%g%%"`
// the overall spacing factor as a percentage of the default amount of spacing
// (higher numbers lead to more space and lower numbers lead to higher density).
Spacing float32 `default:"100" min:"10" max:"500" step:"10" format:"%g%%"`
// the overall font size factor applied to all text as a percentage
// of the default font size (higher numbers lead to larger text).
FontSize float32 `default:"100" min:"10" max:"500" step:"10" format:"%g%%"`
// the amount that alternating rows are highlighted when showing
// tabular data (set to 0 to disable zebra striping).
ZebraStripes float32 `default:"0" min:"0" max:"100" step:"10" format:"%g%%"`
// screen-specific settings, which will override overall defaults if set,
// so different screens can use different zoom levels.
// Use 'Save screen zoom' in the toolbar to save the current zoom for the current
// screen, and Control +/- keyboard shortcut to change this zoom level anytime.
Screens map[string]ScreenSettings `edit:"-"`
// text highlighting style / theme.
Highlighting HighlightingName `default:"emacs"`
// Font is the default font family to use.
Font FontName `default:"Roboto"`
// MonoFont is the default mono-spaced font family to use.
MonoFont FontName `default:"Roboto Mono"`
}
// ConstantSpacing returns a spacing value (padding, margin, gap)
// that will remain constant regardless of changes in the
// [AppearanceSettings.Spacing] setting.
func ConstantSpacing(value float32) float32 {
return value * 100 / AppearanceSettings.Spacing
}
// Themes are the different possible themes that a user can select in their settings.
type Themes int32 //enums:enum -trim-prefix Theme
const (
// ThemeAuto indicates to use the theme specified by the operating system
ThemeAuto Themes = iota
// ThemeLight indicates to use a light theme
ThemeLight
// ThemeDark indicates to use a dark theme
ThemeDark
)
func (as *AppearanceSettingsData) ShouldDisplay(field string) bool {
switch field {
case "Color":
return !ForceAppColor
}
return true
}
// AppColor is the default primary color used to generate the color
// scheme. The user can still change the primary color used to generate
// the color scheme through [AppearanceSettingsData.Color] unless
// [ForceAppColor] is set to true, but this value will always take
// effect if the settings color is the default value. It defaults to
// Google Blue (#4285f4).
var AppColor = color.RGBA{66, 133, 244, 255}
// ForceAppColor is whether to prevent the user from changing the color
// scheme and make it always based on [AppColor].
var ForceAppColor bool
func (as *AppearanceSettingsData) Apply() { //types:add
if ForceAppColor || (as.Color == color.RGBA{66, 133, 244, 255}) {
colors.SetSchemes(AppColor)
} else {
colors.SetSchemes(as.Color)
}
switch as.Theme {
case ThemeLight:
colors.SetScheme(false)
case ThemeDark:
colors.SetScheme(true)
case ThemeAuto:
colors.SetScheme(system.TheApp.IsDark())
}
if as.Highlighting == "" {
as.Highlighting = "emacs" // todo: need light / dark versions
}
// TODO(kai): move HiStyle to a separate text editor settings
// if TheViewInterface != nil {
// TheViewInterface.SetHiStyleDefault(as.HiStyle)
// }
as.applyDPI()
}
// applyDPI updates the screen LogicalDPI values according to current
// settings and zoom factor, and then updates all open windows as well.
func (as *AppearanceSettingsData) applyDPI() {
// zoom is percentage, but LogicalDPIScale is multiplier
system.LogicalDPIScale = as.Zoom / 100
// fmt.Println("system ldpi:", system.LogicalDPIScale)
n := system.TheApp.NScreens()
for i := 0; i < n; i++ {
sc := system.TheApp.Screen(i)
if sc == nil {
continue
}
if scp, ok := as.Screens[sc.Name]; ok {
// zoom is percentage, but LogicalDPIScale is multiplier
system.SetLogicalDPIScale(sc.Name, scp.Zoom/100)
}
sc.UpdateLogicalDPI()
}
for _, w := range AllRenderWindows {
w.SystemWindow.SetLogicalDPI(w.SystemWindow.Screen().LogicalDPI)
// this isn't DPI-related, but this is the most efficient place to do it
w.SystemWindow.SetTitleBarIsDark(matcolor.SchemeIsDark)
}
}
// deleteSavedWindowGeometries deletes the file that saves the position and size of
// each window, by screen, and clear current in-memory cache. You shouldn't generally
// need to do this, but sometimes it is useful for testing or windows that are
// showing up in bad places that you can't recover from.
func (as *AppearanceSettingsData) deleteSavedWindowGeometries() { //types:add
theWindowGeometrySaver.deleteAll()
}
// ZebraStripesWeight returns a 0 to 0.2 alpha opacity factor to use in computing
// a zebra stripe color.
func (as *AppearanceSettingsData) ZebraStripesWeight() float32 {
return as.ZebraStripes * 0.002
}
// DeviceSettings are the global device settings.
var DeviceSettings = &DeviceSettingsData{
SettingsBase: SettingsBase{
Name: "Device",
File: filepath.Join(TheApp.CogentCoreDataDir(), "device-settings.toml"),
},
}
// SaveScreenZoom saves the current zoom factor for the current screen,
// which will then be used for this screen instead of overall default.
// Use the Control +/- keyboard shortcut to modify the screen zoom level.
func (as *AppearanceSettingsData) SaveScreenZoom() { //types:add
sc := system.TheApp.Screen(0)
sp, ok := as.Screens[sc.Name]
if !ok {
sp = ScreenSettings{}
}
sp.Zoom = as.Zoom
if as.Screens == nil {
as.Screens = make(map[string]ScreenSettings)
}
as.Screens[sc.Name] = sp
errors.Log(SaveSettings(as))
}
// DeviceSettingsData is the data type for the device settings.
type DeviceSettingsData struct { //types:add
SettingsBase
// The keyboard shortcut map to use
KeyMap keymap.MapName
// The keyboard shortcut maps available as options for Key map.
// If you do not want to have custom key maps, you should leave
// this unset so that you always have the latest standard key maps.
KeyMaps option.Option[keymap.Maps]
// The maximum time interval between button press events to count as a double-click
DoubleClickInterval time.Duration `default:"500ms" min:"100ms" step:"50ms"`
// How fast the scroll wheel moves, which is typically pixels per wheel step
// but units can be arbitrary. It is generally impossible to standardize speed
// and variable across devices, and we don't have access to the system settings,
// so unfortunately you have to set it here.
ScrollWheelSpeed float32 `default:"1" min:"0.01" step:"1"`
// The duration over which the current scroll widget retains scroll focus,
// such that subsequent scroll events are sent to it.
ScrollFocusTime time.Duration `default:"1s" min:"100ms" step:"50ms"`
// The amount of time to wait before initiating a slide event
// (as opposed to a basic press event)
SlideStartTime time.Duration `default:"50ms" min:"5ms" max:"1s" step:"5ms"`
// The amount of time to wait before initiating a drag (drag and drop) event
// (as opposed to a basic press or slide event)
DragStartTime time.Duration `default:"150ms" min:"5ms" max:"1s" step:"5ms"`
// The amount of time to wait between each repeat click event,
// when the mouse is pressed down. The first click is 8x this.
RepeatClickTime time.Duration `default:"100ms" min:"5ms" max:"1s" step:"5ms"`
// The number of pixels that must be moved before initiating a slide/drag
// event (as opposed to a basic press event)
DragStartDistance int `default:"4" min:"0" max:"100" step:"1"`
// The amount of time to wait before initiating a long hover event (e.g., for opening a tooltip)
LongHoverTime time.Duration `default:"500ms" min:"10ms" max:"10s" step:"10ms"`
// The maximum number of pixels that mouse can move and still register a long hover event
LongHoverStopDistance int `default:"5" min:"0" max:"1000" step:"1"`
// The amount of time to wait before initiating a long press event (e.g., for opening a tooltip)
LongPressTime time.Duration `default:"500ms" min:"10ms" max:"10s" step:"10ms"`
// The maximum number of pixels that mouse/finger can move and still register a long press event
LongPressStopDistance int `default:"50" min:"0" max:"1000" step:"1"`
}
func (ds *DeviceSettingsData) Defaults() {
ds.KeyMap = keymap.DefaultMap
ds.KeyMaps.Value = keymap.AvailableMaps
}
func (ds *DeviceSettingsData) Apply() {
if ds.KeyMaps.Valid {
keymap.AvailableMaps = ds.KeyMaps.Value
}
if ds.KeyMap != "" {
keymap.SetActiveMapName(ds.KeyMap)
}
events.ScrollWheelSpeed = ds.ScrollWheelSpeed
}
// ScreenSettings are per-screen settings that override the global settings.
type ScreenSettings struct { //types:add
// overall zoom factor as a percentage of the default zoom
Zoom float32 `default:"100" min:"10" max:"1000" step:"10" format:"%g%%"`
}
// SystemSettings are the currently active Cogent Core system settings.
var SystemSettings = &SystemSettingsData{
SettingsBase: SettingsBase{
Name: "System",
File: filepath.Join(TheApp.CogentCoreDataDir(), "system-settings.toml"),
},
}
// SystemSettingsData is the data type of the global Cogent Core settings.
type SystemSettingsData struct { //types:add
SettingsBase
// text editor settings
Editor EditorSettings
// whether to use a 24-hour clock (instead of AM and PM)
Clock24 bool `label:"24-hour clock"`
// SnackbarTimeout is the default amount of time until snackbars
// disappear (snackbars show short updates about app processes
// at the bottom of the screen)
SnackbarTimeout time.Duration `default:"5s"`
// only support closing the currently selected active tab; if this is set to true, pressing the close button on other tabs will take you to that tab, from which you can close it
OnlyCloseActiveTab bool `default:"false"`
// the limit of file size, above which user will be prompted before opening / copying, etc.
BigFileSize int `default:"10000000"`
// maximum number of saved paths to save in FilePicker
SavedPathsMax int `default:"50"`
// extra font paths, beyond system defaults -- searched first
FontPaths []string
// user info, which is partially filled-out automatically if empty when settings are first created
User User
// favorite paths, shown in FilePickerer and also editable there
FavPaths favoritePaths
// column to sort by in FilePicker, and :up or :down for direction -- updated automatically via FilePicker
FilePickerSort string `display:"-"`
// the maximum height of any menu popup panel in units of font height;
// scroll bars are enforced beyond that size.
MenuMaxHeight int `default:"30" min:"5" step:"1"`
// the amount of time to wait before offering completions
CompleteWaitDuration time.Duration `default:"0ms" min:"0ms" max:"10s" step:"10ms"`
// the maximum number of completions offered in popup
CompleteMaxItems int `default:"25" min:"5" step:"1"`
// time interval for cursor blinking on and off -- set to 0 to disable blinking
CursorBlinkTime time.Duration `default:"500ms" min:"0ms" max:"1s" step:"5ms"`
// The amount of time to wait before trying to autoscroll again
LayoutAutoScrollDelay time.Duration `default:"25ms" min:"1ms" step:"5ms"`
// number of steps to take in PageUp / Down events in terms of number of items
LayoutPageSteps int `default:"10" min:"1" step:"1"`
// the amount of time between keypresses to combine characters into name to search for within layout -- starts over after this delay
LayoutFocusNameTimeout time.Duration `default:"500ms" min:"0ms" max:"5s" step:"20ms"`
// the amount of time since last focus name event to allow tab to focus on next element with same name.
LayoutFocusNameTabTime time.Duration `default:"2s" min:"10ms" max:"10s" step:"100ms"`
// the number of map elements at or below which an inline representation
// of the map will be presented, which is more convenient for small #'s of properties
MapInlineLength int `default:"2" min:"1" step:"1"`
// the number of elemental struct fields at or below which an inline representation
// of the struct will be presented, which is more convenient for small structs
StructInlineLength int `default:"4" min:"2" step:"1"`
// the number of slice elements below which inline will be used
SliceInlineLength int `default:"4" min:"2" step:"1"`
}
func (ss *SystemSettingsData) Defaults() {
ss.FavPaths.setToDefaults()
ss.updateUser()
}
// Apply detailed settings to all the relevant settings.
func (ss *SystemSettingsData) Apply() { //types:add
if ss.FontPaths != nil {
paths := append(ss.FontPaths, paint.FontPaths...)
paint.FontLibrary.InitFontPaths(paths...)
} else {
paint.FontLibrary.InitFontPaths(paint.FontPaths...)
}
np := len(ss.FavPaths)
for i := 0; i < np; i++ {
if ss.FavPaths[i].Icon == "" || ss.FavPaths[i].Icon == "folder" {
ss.FavPaths[i].Icon = icons.Folder
}
}
}
func (ss *SystemSettingsData) Open() error {
fnm := ss.Filename()
err := tomlx.Open(ss, fnm)
if len(ss.FavPaths) == 0 {
ss.FavPaths.setToDefaults()
}
return err
}
// TimeFormat returns the Go time format layout string that should
// be used for displaying times to the user, based on the value of
// [SystemSettingsData.Clock24].
func (ss *SystemSettingsData) TimeFormat() string {
if ss.Clock24 {
return "15:04"
}
return "3:04 PM"
}
// updateUser gets the user info from the OS
func (ss *SystemSettingsData) updateUser() {
usr, err := user.Current()
if err == nil {
ss.User.User = *usr
}
}
// User basic user information that might be needed for different apps
type User struct { //types:add
user.User
// default email address -- e.g., for recording changes in a version control system
Email string
}
// EditorSettings contains text editor settings.
type EditorSettings struct { //types:add
// size of a tab, in chars; also determines indent level for space indent
TabSize int `default:"4"`
// use spaces for indentation, otherwise tabs
SpaceIndent bool
// wrap lines at word boundaries; otherwise long lines scroll off the end
WordWrap bool `default:"true"`
// whether to show line numbers
LineNumbers bool `default:"true"`
// use the completion system to suggest options while typing
Completion bool `default:"true"`
// suggest corrections for unknown words while typing
SpellCorrect bool `default:"true"`
// automatically indent lines when enter, tab, }, etc pressed
AutoIndent bool `default:"true"`
// use emacs-style undo, where after a non-undo command, all the current undo actions are added to the undo stack, such that a subsequent undo is actually a redo
EmacsUndo bool
// colorize the background according to nesting depth
DepthColor bool `default:"true"`
}
//////////////////////////////////////////////////////////////////
// FavoritePaths
// favoritePathItem represents one item in a favorite path list, for display of
// favorites. Is an ordered list instead of a map because user can organize
// in order
type favoritePathItem struct { //types:add
// icon for item
Icon icons.Icon
// name of the favorite item
Name string `width:"20"`
// the path of the favorite item
Path string `table:"-select"`
}
// Label satisfies the Labeler interface
func (fi favoritePathItem) Label() string {
return fi.Name
}
// favoritePaths is a list (slice) of favorite path items
type favoritePaths []favoritePathItem
// setToDefaults sets the paths to default values
func (pf *favoritePaths) setToDefaults() {
*pf = make(favoritePaths, len(defaultPaths))
copy(*pf, defaultPaths)
}
// findPath returns index of path on list, or -1, false if not found
func (pf *favoritePaths) findPath(path string) (int, bool) {
for i, fi := range *pf {
if fi.Path == path {
return i, true
}
}
return -1, false
}
// defaultPaths are default favorite paths
var defaultPaths = favoritePaths{
{icons.Home, "home", "~"},
{icons.DesktopMac, "Desktop", "~/Desktop"},
{icons.Document, "Documents", "~/Documents"},
{icons.Download, "Downloads", "~/Downloads"},
{icons.Computer, "root", "/"},
}
//////////////////////////////////////////////////////////////////
// FilePaths
// FilePaths represents a set of file paths.
type FilePaths []string
// recentPaths are the recently opened paths in the file picker.
var recentPaths FilePaths
// Open file paths from a json-formatted file.
func (fp *FilePaths) Open(filename string) error { //types:add
err := jsonx.Open(fp, filename)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
errors.Log(err)
}
return err
}
// Save file paths to a json-formatted file.
func (fp *FilePaths) Save(filename string) error { //types:add
return errors.Log(jsonx.Save(fp, filename))
}
// AddPath inserts a path to the file paths (at the start), subject to max
// length -- if path is already on the list then it is moved to the start.
func (fp *FilePaths) AddPath(path string, max int) {
stringsx.InsertFirstUnique((*[]string)(fp), path, max)
}
// savedPathsFilename is the name of the saved file paths file in
// the Cogent Core data directory.
const savedPathsFilename = "saved-paths.json"
// saveRecentPaths saves the active RecentPaths to data dir
func saveRecentPaths() {
pdir := TheApp.CogentCoreDataDir()
pnm := filepath.Join(pdir, savedPathsFilename)
errors.Log(recentPaths.Save(pnm))
}
// openRecentPaths loads the active RecentPaths from data dir
func openRecentPaths() {
pdir := TheApp.CogentCoreDataDir()
pnm := filepath.Join(pdir, savedPathsFilename)
err := recentPaths.Open(pnm)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
errors.Log(err)
}
}
//////////////////////////////////////////////////////////////////
// DebugSettings
// DebugSettings are the currently active debugging settings
var DebugSettings = &DebugSettingsData{
SettingsBase: SettingsBase{
Name: "Debug",
File: filepath.Join(TheApp.CogentCoreDataDir(), "debug-settings.toml"),
},
}
// DebugSettingsData is the data type for debugging settings.
type DebugSettingsData struct { //types:add
SettingsBase
// Print a trace of updates that trigger re-rendering
UpdateTrace bool
// Print a trace of the nodes rendering
RenderTrace bool
// Print a trace of all layouts
LayoutTrace bool
// Print more detailed info about the underlying layout computations
LayoutTraceDetail bool
// Print a trace of window events
WindowEventTrace bool
// Print the stack trace leading up to win publish events
// which are expensive
WindowRenderTrace bool
// Print a trace of window geometry saving / loading functions
WindowGeometryTrace bool
// Print a trace of keyboard events
KeyEventTrace bool
// Print a trace of event handling
EventTrace bool
// Print a trace of focus changes
FocusTrace bool
// Print a trace of DND event handling
DNDTrace bool
// DisableWindowGeometrySaver disables the saving and loading of window geometry
// data to allow for easier testing of window manipulation code.
DisableWindowGeometrySaver bool
// Print a trace of Go language completion and lookup process
GoCompleteTrace bool
// Print a trace of Go language type parsing and inference process
GoTypeTrace bool
}
func (db *DebugSettingsData) Defaults() {
// TODO(kai/binsize): figure out how to do this without dragging in parse langs dependency
// db.GoCompleteTrace = golang.CompleteTrace
// db.GoTypeTrace = golang.TraceTypes
}
func (db *DebugSettingsData) Apply() {
// golang.CompleteTrace = db.GoCompleteTrace
// golang.TraceTypes = db.GoTypeTrace
}
// 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 core
import (
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/tree"
)
// settingsEditorToolbarBase is the base toolbar configuration
// function used in [SettingsEditor].
func settingsEditorToolbarBase(p *tree.Plan) {
tree.Add(p, func(w *FuncButton) {
w.SetFunc(AppearanceSettings.SaveScreenZoom).SetIcon(icons.ZoomIn)
w.SetAfterFunc(func() {
AppearanceSettings.Apply()
UpdateAll()
})
})
}
// SettingsWindow opens a window for editing user settings.
func SettingsWindow() { //types:add
if RecycleMainWindow(&AllSettings) {
return
}
d := NewBody("Settings").SetData(&AllSettings)
SettingsEditor(d)
d.RunWindow()
}
// SettingsEditor adds to the given body an editor of user settings.
func SettingsEditor(b *Body) {
b.AddTopBar(func(bar *Frame) {
tb := NewToolbar(bar)
tb.Maker(settingsEditorToolbarBase)
for _, se := range AllSettings {
tb.Maker(se.MakeToolbar)
}
tb.AddOverflowMenu(func(m *Scene) {
NewFuncButton(m).SetFunc(resetAllSettings).SetConfirm(true).SetText("Reset settings").SetIcon(icons.Delete)
NewFuncButton(m).SetFunc(AppearanceSettings.deleteSavedWindowGeometries).SetConfirm(true).SetIcon(icons.Delete)
NewFuncButton(m).SetFunc(ProfileToggle).SetShortcut("Control+Alt+R").SetText("Profile performance").SetIcon(icons.Analytics)
})
})
tabs := NewTabs(b)
for _, se := range AllSettings {
fr, _ := tabs.NewTab(se.Label())
NewForm(fr).SetStruct(se).OnChange(func(e events.Event) {
UpdateSettings(fr, se)
})
}
}
// 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 "cogentcore.org/core/math32"
// SizeClasses are the different size classes that a window can have.
type SizeClasses int32 //enums:enum -trim-prefix Size
const (
// SizeCompact is the size class for windows with a width less than
// 600dp, which typically happens on phones.
SizeCompact SizeClasses = iota
// SizeMedium is the size class for windows with a width between 600dp
// and 840dp inclusive, which typically happens on tablets.
SizeMedium
// SizeExpanded is the size class for windows with a width greater than
// 840dp, which typically happens on desktop and laptop computers.
SizeExpanded
)
// SceneSize returns the effective size of the scene in which the widget is contained
// in terms of dp (density-independent pixels).
func (wb *WidgetBase) SceneSize() math32.Vector2 {
dots := math32.FromPoint(wb.Scene.SceneGeom.Size)
if wb.Scene.hasFlag(sceneContentSizing) {
if currentRenderWindow != nil {
rg := currentRenderWindow.SystemWindow.RenderGeom()
dots = math32.FromPoint(rg.Size)
}
}
dpd := wb.Scene.Styles.UnitContext.Dp(1) // dots per dp
dp := dots.DivScalar(dpd) // dots / (dots / dp) = dots * (dp / dots) = dp
return dp
}
// SizeClass returns the size class of the scene in which the widget is contained
// based on [WidgetBase.SceneSize].
func (wb *WidgetBase) SizeClass() SizeClasses {
dp := wb.SceneSize().X
switch {
case dp < 600:
return SizeCompact
case dp > 840:
return SizeExpanded
default:
return SizeMedium
}
}
// 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 core
import (
"fmt"
"image"
"log/slog"
"reflect"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
)
// Slider is a slideable widget that provides slider functionality with a draggable
// thumb and a clickable track. The [styles.Style.Direction] determines the direction
// in which the slider slides.
type Slider struct {
Frame
// Type is the type of the slider, which determines its visual
// and functional properties. The default type, [SliderSlider],
// should work for most end-user use cases.
Type SliderTypes
// Value is the current value, represented by the position of the thumb.
// It defaults to 0.5.
Value float32 `set:"-"`
// Min is the minimum possible value.
// It defaults to 0.
Min float32
// Max is the maximum value supported.
// It defaults to 1.
Max float32
// Step is the amount that the arrow keys increment/decrement the value by.
// It defaults to 0.1.
Step float32
// EnforceStep is whether to ensure that the value is always
// a multiple of [Slider.Step].
EnforceStep bool
// PageStep is the amount that the PageUp and PageDown keys
// increment/decrement the value by.
// It defaults to 0.2, and will be at least as big as [Slider.Step].
PageStep float32
// Icon is an optional icon to use for the dragging thumb.
Icon icons.Icon
// For Scrollbar type only: proportion (1 max) of the full range of scrolled data
// that is currently visible. This determines the thumb size and range of motion:
// if 1, full slider is the thumb and no motion is possible.
visiblePercent float32 `set:"-"`
// ThumbSize is the size of the thumb as a proportion of the slider thickness,
// which is the content size (inside the padding).
ThumbSize math32.Vector2
// TrackSize is the proportion of slider thickness for the visible track
// for the [SliderSlider] type. It is often thinner than the thumb, achieved
// by values less than 1 (0.5 default).
TrackSize float32 `default:"0.5"`
// InputThreshold is the threshold for the amount of change in scroll
// value before emitting an input event.
InputThreshold float32
// Precision specifies the precision of decimal places (total, not after the decimal
// point) to use in representing the number. This helps to truncate small weird
// floating point values.
Precision int
// ValueColor is the background color that is used for styling the selected value
// section of the slider. It should be set in a Styler, just like the main style
// object is. If it is set to transparent, no value is rendered, so the value
// section of the slider just looks like the rest of the slider.
ValueColor image.Image
// ThumbColor is the background color that is used for styling the thumb (handle)
// of the slider. It should be set in a Styler, just like the main style object is.
// If it is set to transparent, no thumb is rendered, so the thumb section of the
// slider just looks like the rest of the slider.
ThumbColor image.Image
// StayInView is whether to keep the slider (typically a [SliderScrollbar]) within
// the parent [Scene] bounding box, if the parent is in view. This is the default
// behavior for [Frame] scrollbars, and setting this flag replicates that behavior
// in other scrollbars.
StayInView bool
// Computed values below:
// logical position of the slider relative to Size
pos float32
// previous Change event emitted value; don't re-emit Change if it is the same
lastValue float32
// previous sliding value (for computing the Input change)
prevSlide float32
// underlying drag position of slider; not subject to snapping
slideStartPos float32
}
// SliderTypes are the different types of sliders.
type SliderTypes int32 //enums:enum -trim-prefix Slider
const (
// SliderSlider indicates a standard, user-controllable slider
// for setting a numeric value.
SliderSlider SliderTypes = iota
// SliderScrollbar indicates a slider acting as a scrollbar for content.
// It has a [Slider.visiblePercent] factor that specifies the percent of the content
// currently visible, which determines the size of the thumb, and thus the range
// of motion remaining for the thumb Value ([Slider.visiblePercent] = 1 means thumb
// is full size, and no remaining range of motion). The content size (inside the
// margin and padding) determines the outer bounds of the rendered area.
SliderScrollbar
)
func (sr *Slider) WidgetValue() any { return &sr.Value }
func (sr *Slider) OnBind(value any, tags reflect.StructTag) {
kind := reflectx.NonPointerType(reflect.TypeOf(value)).Kind()
if kind >= reflect.Int && kind <= reflect.Uintptr {
sr.SetStep(1).SetEnforceStep(true).SetMax(100)
}
setFromTag(tags, "min", func(v float32) { sr.SetMin(v) })
setFromTag(tags, "max", func(v float32) { sr.SetMax(v) })
setFromTag(tags, "step", func(v float32) { sr.SetStep(v) })
}
func (sr *Slider) Init() {
sr.Frame.Init()
sr.Value = 0.5
sr.Max = 1
sr.visiblePercent = 1
sr.Step = 0.1
sr.PageStep = 0.2
sr.Precision = 9
sr.ThumbSize.Set(1, 1)
sr.TrackSize = 0.5
sr.Styler(func(s *styles.Style) {
s.SetAbilities(true, abilities.Activatable, abilities.Focusable, abilities.Hoverable, abilities.Slideable)
// we use a different color for the thumb and value color
// (compared to the background color) so that they get the
// correct state layer
s.Color = colors.Scheme.Primary.On
if sr.Type == SliderSlider {
sr.ValueColor = colors.Scheme.Primary.Base
sr.ThumbColor = colors.Scheme.Primary.Base
s.Padding.Set(units.Dp(8))
s.Background = colors.Scheme.SurfaceVariant
} else {
sr.ValueColor = colors.Scheme.OutlineVariant
sr.ThumbColor = colors.Scheme.OutlineVariant
s.Background = colors.Scheme.SurfaceContainerLow
}
// sr.ValueColor = s.StateBackgroundColor(sr.ValueColor)
// sr.ThumbColor = s.StateBackgroundColor(sr.ThumbColor)
s.Color = colors.Scheme.OnSurface
s.Border.Radius = styles.BorderRadiusFull
if !sr.IsReadOnly() {
s.Cursor = cursors.Grab
switch {
case s.Is(states.Sliding):
s.Cursor = cursors.Grabbing
case s.Is(states.Active):
s.Cursor = cursors.Grabbing
}
}
})
sr.FinalStyler(func(s *styles.Style) {
if s.Direction == styles.Row {
s.Min.X.Em(20)
s.Min.Y.Em(1)
} else {
s.Min.Y.Em(20)
s.Min.X.Em(1)
}
if sr.Type == SliderScrollbar {
if s.Direction == styles.Row {
s.Min.Y = s.ScrollbarWidth
} else {
s.Min.X = s.ScrollbarWidth
}
}
})
sr.On(events.MouseDown, func(e events.Event) {
pos := sr.pointToRelPos(e.Pos())
sr.setSliderPosEvent(pos)
sr.slideStartPos = sr.pos
})
// note: not doing anything in particular on SlideStart
sr.On(events.SlideMove, func(e events.Event) {
del := e.StartDelta()
if sr.Styles.Direction == styles.Row {
sr.setSliderPosEvent(sr.slideStartPos + float32(del.X))
} else {
sr.setSliderPosEvent(sr.slideStartPos + float32(del.Y))
}
})
// we need to send change events for both SlideStop and Click
// to handle the no-slide click case
change := func(e events.Event) {
pos := sr.pointToRelPos(e.Pos())
sr.setSliderPosEvent(pos)
sr.sendChange()
}
sr.On(events.SlideStop, change)
sr.On(events.Click, change)
sr.On(events.Scroll, func(e events.Event) {
se := e.(*events.MouseScroll)
se.SetHandled()
var del float32
// if we are scrolling in the y direction on an x slider,
// we still count it
if sr.Styles.Direction == styles.Row && se.Delta.X != 0 {
del = se.Delta.X
} else {
del = se.Delta.Y
}
if sr.Type == SliderScrollbar {
del = -del // invert for "natural" scroll
}
edel := sr.scrollScale(del)
sr.setValueEvent(sr.Value + edel)
sr.sendChange()
})
sr.OnKeyChord(func(e events.Event) {
kf := keymap.Of(e.KeyChord())
if DebugSettings.KeyEventTrace {
slog.Info("SliderBase KeyInput", "widget", sr, "keyFunction", kf)
}
switch kf {
case keymap.MoveUp:
sr.setValueEvent(sr.Value - sr.Step)
e.SetHandled()
case keymap.MoveLeft:
sr.setValueEvent(sr.Value - sr.Step)
e.SetHandled()
case keymap.MoveDown:
sr.setValueEvent(sr.Value + sr.Step)
e.SetHandled()
case keymap.MoveRight:
sr.setValueEvent(sr.Value + sr.Step)
e.SetHandled()
case keymap.PageUp:
if sr.PageStep < sr.Step {
sr.PageStep = 2 * sr.Step
}
sr.setValueEvent(sr.Value - sr.PageStep)
e.SetHandled()
case keymap.PageDown:
if sr.PageStep < sr.Step {
sr.PageStep = 2 * sr.Step
}
sr.setValueEvent(sr.Value + sr.PageStep)
e.SetHandled()
case keymap.Home:
sr.setValueEvent(sr.Min)
e.SetHandled()
case keymap.End:
sr.setValueEvent(sr.Max)
e.SetHandled()
}
})
sr.Maker(func(p *tree.Plan) {
if !sr.Icon.IsSet() {
return
}
tree.AddAt(p, "icon", func(w *Icon) {
w.Styler(func(s *styles.Style) {
s.Font.Size.Dp(24)
s.Color = sr.ThumbColor
})
w.Updater(func() {
w.SetIcon(sr.Icon)
})
})
})
}
// snapValue snaps the value to [Slider.Step] if [Slider.EnforceStep] is on.
func (sr *Slider) snapValue() {
if !sr.EnforceStep {
return
}
// round to the nearest step
sr.Value = sr.Step * math32.Round(sr.Value/sr.Step)
}
// sendChange calls [WidgetBase.SendChange] if the current value
// is different from the last value.
func (sr *Slider) sendChange(e ...events.Event) bool {
if sr.Value == sr.lastValue {
return false
}
sr.lastValue = sr.Value
sr.SendChange(e...)
return true
}
// sliderSize returns the size available for sliding, based on allocation
func (sr *Slider) sliderSize() float32 {
sz := sr.Geom.Size.Actual.Content.Dim(sr.Styles.Direction.Dim())
if sr.Type != SliderScrollbar {
thsz := sr.thumbSizeDots()
sz -= thsz.Dim(sr.Styles.Direction.Dim()) // half on each size
}
return sz
}
// sliderThickness returns the thickness of the slider: Content size in other dim.
func (sr *Slider) sliderThickness() float32 {
return sr.Geom.Size.Actual.Content.Dim(sr.Styles.Direction.Dim().Other())
}
// thumbSizeDots returns the thumb size in dots, based on ThumbSize
// and the content thickness
func (sr *Slider) thumbSizeDots() math32.Vector2 {
return sr.ThumbSize.MulScalar(sr.sliderThickness())
}
// slideThumbSize returns thumb size, based on type
func (sr *Slider) slideThumbSize() float32 {
if sr.Type == SliderScrollbar {
minsz := sr.sliderThickness()
return max(math32.Clamp(sr.visiblePercent, 0, 1)*sr.sliderSize(), minsz)
}
return sr.thumbSizeDots().Dim(sr.Styles.Direction.Dim())
}
// effectiveMax returns the effective maximum value represented.
// For the Slider type, it it is just Max.
// for the Scrollbar type, it is Max - Value of thumb size
func (sr *Slider) effectiveMax() float32 {
if sr.Type == SliderScrollbar {
return sr.Max - math32.Clamp(sr.visiblePercent, 0, 1)*(sr.Max-sr.Min)
}
return sr.Max
}
// scrollThumbValue returns the current scroll VisiblePct
// in terms of the Min - Max range of values.
func (sr *Slider) scrollThumbValue() float32 {
return math32.Clamp(sr.visiblePercent, 0, 1) * (sr.Max - sr.Min)
}
// setSliderPos sets the position of the slider at the given
// relative position within the usable Content sliding range,
// in pixels, and updates the corresponding Value based on that position.
func (sr *Slider) setSliderPos(pos float32) {
sz := sr.Geom.Size.Actual.Content.Dim(sr.Styles.Direction.Dim())
if sz <= 0 {
return
}
thsz := sr.slideThumbSize()
thszh := .5 * thsz
sr.pos = math32.Clamp(pos, thszh, sz-thszh)
prel := (sr.pos - thszh) / (sz - thsz)
effmax := sr.effectiveMax()
val := math32.Truncate(sr.Min+prel*(effmax-sr.Min), sr.Precision)
val = math32.Clamp(val, sr.Min, effmax)
// fmt.Println(pos, thsz, prel, val)
sr.Value = val
sr.snapValue()
sr.setPosFromValue(sr.Value) // go back the other way to be fully consistent
sr.NeedsRender()
}
// setSliderPosEvent sets the position of the slider at the given position in pixels,
// and updates the corresponding Value based on that position.
// This version sends input events.
func (sr *Slider) setSliderPosEvent(pos float32) {
sr.setSliderPos(pos)
if math32.Abs(sr.prevSlide-sr.Value) > sr.InputThreshold {
sr.prevSlide = sr.Value
sr.Send(events.Input)
}
}
// setPosFromValue sets the slider position based on the given value
// (typically rs.Value)
func (sr *Slider) setPosFromValue(val float32) {
sz := sr.Geom.Size.Actual.Content.Dim(sr.Styles.Direction.Dim())
if sz <= 0 {
return
}
effmax := sr.effectiveMax()
val = math32.Clamp(val, sr.Min, effmax)
prel := (val - sr.Min) / (effmax - sr.Min) // relative position 0-1
thsz := sr.slideThumbSize()
thszh := .5 * thsz
sr.pos = 0.5*thsz + prel*(sz-thsz)
sr.pos = math32.Clamp(sr.pos, thszh, sz-thszh)
sr.NeedsRender()
}
// setVisiblePercent sets the [Slider.visiblePercent] value for a [SliderScrollbar].
func (sr *Slider) setVisiblePercent(val float32) *Slider {
sr.visiblePercent = math32.Clamp(val, 0, 1)
return sr
}
// SetValue sets the value and updates the slider position,
// but does not send an [events.Change] event.
func (sr *Slider) SetValue(value float32) *Slider {
effmax := sr.effectiveMax()
value = math32.Clamp(value, sr.Min, effmax)
if sr.Value != value {
sr.Value = value
sr.snapValue()
sr.setPosFromValue(value)
}
sr.NeedsRender()
return sr
}
// setValueEvent sets the value and updates the slider representation, and
// emits an input and change event. Returns true if value actually changed.
func (sr *Slider) setValueEvent(val float32) bool {
if sr.Value == val {
return false
}
curVal := sr.Value
sr.SetValue(val)
sr.Send(events.Input)
sr.SendChange()
return curVal != sr.Value
}
func (sr *Slider) WidgetTooltip(pos image.Point) (string, image.Point) {
res := sr.Tooltip
if sr.Type == SliderScrollbar {
return res, sr.DefaultTooltipPos()
}
if res != "" {
res += " "
}
res += fmt.Sprintf("(value: %.4g, minimum: %.4g, maximum: %.4g)", sr.Value, sr.Min, sr.Max)
return res, sr.DefaultTooltipPos()
}
// pointToRelPos translates a point in scene local pixel coords into relative
// position within the slider content range
func (sr *Slider) pointToRelPos(pt image.Point) float32 {
ptf := math32.FromPoint(pt).Dim(sr.Styles.Direction.Dim())
return ptf - sr.Geom.Pos.Content.Dim(sr.Styles.Direction.Dim())
}
// scrollScale returns scaled value of scroll delta
// as a function of the step size.
func (sr *Slider) scrollScale(del float32) float32 {
return del * sr.Step
}
func (sr *Slider) Render() {
sr.setPosFromValue(sr.Value)
pc := &sr.Scene.PaintContext
st := &sr.Styles
dim := sr.Styles.Direction.Dim()
od := dim.Other()
sz := sr.Geom.Size.Actual.Content
pos := sr.Geom.Pos.Content
pabg := sr.parentActualBackground()
if sr.Type == SliderScrollbar {
pc.DrawStandardBox(st, pos, sz, pabg) // track
if sr.ValueColor != nil {
thsz := sr.slideThumbSize()
osz := sr.thumbSizeDots().Dim(od)
tpos := pos
tpos = tpos.AddDim(dim, sr.pos)
tpos = tpos.SubDim(dim, thsz*.5)
tsz := sz
tsz.SetDim(dim, thsz)
origsz := sz.Dim(od)
tsz.SetDim(od, osz)
tpos = tpos.AddDim(od, 0.5*(osz-origsz))
vabg := sr.Styles.ComputeActualBackgroundFor(sr.ValueColor, pabg)
pc.FillStyle.Color = vabg
sr.RenderBoxGeom(tpos, tsz, styles.Border{Radius: st.Border.Radius}) // thumb
}
} else {
prevbg := st.Background
prevsl := st.StateLayer
// use surrounding background with no state layer for surrounding box
st.Background = pabg
st.StateLayer = 0
st.ComputeActualBackground(pabg)
// surrounding box (needed to prevent it from rendering over itself)
sr.RenderStandardBox()
st.Background = prevbg
st.StateLayer = prevsl
st.ComputeActualBackground(pabg)
trsz := sz.Dim(od) * sr.TrackSize
bsz := sz
bsz.SetDim(od, trsz)
bpos := pos
bpos = bpos.AddDim(od, .5*(sz.Dim(od)-trsz))
pc.FillStyle.Color = st.ActualBackground
sr.RenderBoxGeom(bpos, bsz, styles.Border{Radius: st.Border.Radius}) // track
if sr.ValueColor != nil {
bsz.SetDim(dim, sr.pos)
vabg := sr.Styles.ComputeActualBackgroundFor(sr.ValueColor, pabg)
pc.FillStyle.Color = vabg
sr.RenderBoxGeom(bpos, bsz, styles.Border{Radius: st.Border.Radius})
}
thsz := sr.thumbSizeDots()
tpos := pos
tpos.SetDim(dim, pos.Dim(dim)+sr.pos)
tpos = tpos.AddDim(od, 0.5*sz.Dim(od)) // ctr
// render thumb as icon or box
if sr.Icon.IsSet() && sr.HasChildren() {
ic := sr.Child(0).(*Icon)
tpos.SetSub(thsz.MulScalar(.5))
ic.Geom.Pos.Total = tpos
ic.setContentPosFromPos()
ic.setBBoxes()
} else {
tabg := sr.Styles.ComputeActualBackgroundFor(sr.ThumbColor, pabg)
pc.FillStyle.Color = tabg
tpos.SetSub(thsz.MulScalar(0.5))
sr.RenderBoxGeom(tpos, thsz, styles.Border{Radius: st.Border.Radius})
}
}
}
func (sr *Slider) ApplyScenePos() {
sr.WidgetBase.ApplyScenePos()
if !sr.StayInView {
return
}
pwb := sr.parentWidget()
zr := image.Rectangle{}
if !pwb.IsVisible() || pwb.Geom.TotalBBox == zr {
return
}
sbw := math32.Ceil(sr.Styles.ScrollbarWidth.Dots)
scmax := math32.FromPoint(sr.Scene.Geom.ContentBBox.Max).SubScalar(sbw)
sr.Geom.Pos.Total.SetMin(scmax)
sr.setContentPosFromPos()
sr.setBBoxesFromAllocs()
}
// 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 (
"log/slog"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/colors"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
)
// RunSnackbar returns and runs a new [SnackbarStage] in the context
// of the given widget. See [Body.NewSnackbar] to make a snackbar without running it.
func (bd *Body) RunSnackbar(ctx Widget) *Stage {
return bd.NewSnackbar(ctx).Run()
}
// NewSnackbar returns a new [SnackbarStage] in the context
// of the given widget. You must call [Stage.Run] to run the
// snackbar; see [Body.RunSnackbar] for a version that
// automatically runs it.
func (bd *Body) NewSnackbar(ctx Widget) *Stage {
ctx = nonNilContext(ctx)
bd.snackbarStyles()
bd.Scene.Stage = NewPopupStage(SnackbarStage, bd.Scene, ctx).
SetTimeout(SystemSettings.SnackbarTimeout)
return bd.Scene.Stage
}
// MessageSnackbar opens a [SnackbarStage] displaying the given message
// in the context of the given widget.
func MessageSnackbar(ctx Widget, message string) {
NewBody().AddSnackbarText(message).RunSnackbar(ctx)
}
// ErrorSnackbar opens a [SnackbarStage] displaying the given error
// in the context of the given widget. Optional label text can be
// provided; if it is not, the label text will default to "Error".
// If the given error is nil, no snackbar is created.
func ErrorSnackbar(ctx Widget, err error, label ...string) {
if err == nil {
return
}
lbl := "Error"
if len(label) > 0 {
lbl = label[0]
}
text := lbl + ": " + err.Error()
// we need to get [errors.CallerInfo] at this level
slog.Error(text + " | " + errors.CallerInfo())
MessageSnackbar(ctx, text)
}
// snackbarStyles sets default stylers for snackbar bodies.
// It is automatically called in [Body.NewSnackbar].
func (bd *Body) snackbarStyles() {
bd.Styler(func(s *styles.Style) {
s.Direction = styles.Row
s.Overflow.Set(styles.OverflowVisible) // key for avoiding sizing errors when re-rendering with small pref size
s.Border.Radius = styles.BorderRadiusExtraSmall
s.Padding.SetHorizontal(units.Dp(16))
s.Background = colors.Scheme.InverseSurface
s.Color = colors.Scheme.InverseOnSurface
// we go on top of things so we want no margin background
s.FillMargin = false
s.Align.Content = styles.Center
s.Align.Items = styles.Center
s.Gap.X.Dp(12)
s.Grow.Set(1, 0)
s.Min.Y.Dp(48)
s.Min.X.SetCustom(func(uc *units.Context) float32 {
return min(uc.Em(20), uc.Vw(70))
})
})
bd.Scene.Styler(func(s *styles.Style) {
s.Background = nil
s.Border.Radius = styles.BorderRadiusExtraSmall
s.BoxShadow = styles.BoxShadow3()
})
}
// AddSnackbarText adds a snackbar [Text] with the given text.
func (bd *Body) AddSnackbarText(text string) *Body {
tx := NewText(bd).SetText(text).SetType(TextBodyMedium)
tx.Styler(func(s *styles.Style) {
s.SetTextWrap(false)
if s.Is(states.Selected) {
s.Color = colors.Scheme.Select.OnContainer
}
})
return bd
}
// AddSnackbarButton adds a snackbar button with the given text and optional OnClick
// event handler. Only the first of the given event handlers is used, and the
// snackbar is automatically closed when the button is clicked regardless of
// whether there is an event handler passed.
func (bd *Body) AddSnackbarButton(text string, onClick ...func(e events.Event)) *Body {
NewStretch(bd)
bt := NewButton(bd).SetType(ButtonText).SetText(text)
bt.Styler(func(s *styles.Style) {
s.Color = colors.Scheme.InversePrimary
})
bt.OnClick(func(e events.Event) {
if len(onClick) > 0 {
onClick[0](e)
}
bd.Scene.Stage.ClosePopup()
})
return bd
}
// AddSnackbarIcon adds a snackbar icon button with the given icon and optional
// OnClick event handler. Only the first of the given event handlers is used, and the
// snackbar is automatically closed when the button is clicked regardless of whether
// there is an event handler passed.
func (bd *Body) AddSnackbarIcon(icon icons.Icon, onClick ...func(e events.Event)) *Body {
ic := NewButton(bd).SetType(ButtonAction).SetIcon(icon)
ic.Styler(func(s *styles.Style) {
s.Color = colors.Scheme.InverseOnSurface
})
ic.OnClick(func(e events.Event) {
if len(onClick) > 0 {
onClick[0](e)
}
bd.Scene.Stage.ClosePopup()
})
return bd
}
// 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 core
import (
"fmt"
"image"
"log/slog"
"reflect"
"strconv"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/tree"
)
// Spinner is a [TextField] for editing numerical values. It comes with
// fields, methods, buttons, and shortcuts to enhance numerical value editing.
type Spinner struct {
TextField
// Value is the current value.
Value float32 `set:"-"`
// HasMin is whether there is a minimum value to enforce.
// It should be set using [Spinner.SetMin].
HasMin bool `set:"-"`
// Min, if [Spinner.HasMin] is true, is the the minimum value in range.
// It should be set using [Spinner.SetMin].
Min float32 `set:"-"`
// HaxMax is whether there is a maximum value to enforce.
// It should be set using [Spinner.SetMax].
HasMax bool `set:"-"`
// Max, if [Spinner.HasMax] is true, is the maximum value in range.
// It should be set using [Spinner.SetMax].
Max float32 `set:"-"`
// Step is the amount that the up and down buttons and arrow keys
// increment/decrement the value by. It defaults to 0.1.
Step float32
// EnforceStep is whether to ensure that the value of the spinner
// is always a multiple of [Spinner.Step].
EnforceStep bool
// PageStep is the amount that the PageUp and PageDown keys
// increment/decrement the value by.
// It defaults to 0.2, and will be at least as big as [Spinner.Step].
PageStep float32
// Precision specifies the precision of decimal places
// (total, not after the decimal point) to use in
// representing the number. This helps to truncate
// small weird floating point values.
Precision int
// Format is the format string to use for printing the value.
// If it unset, %g is used. If it is decimal based
// (ends in d, b, c, o, O, q, x, X, or U) then the value is
// converted to decimal prior to printing.
Format string
}
func (sp *Spinner) WidgetValue() any { return &sp.Value }
func (sp *Spinner) SetWidgetValue(value any) error {
f, err := reflectx.ToFloat32(value)
if err != nil {
return err
}
sp.SetValue(f)
return nil
}
func (sp *Spinner) OnBind(value any, tags reflect.StructTag) {
kind := reflectx.NonPointerType(reflect.TypeOf(value)).Kind()
if kind >= reflect.Int && kind <= reflect.Uintptr {
sp.SetStep(1).SetEnforceStep(true)
if kind >= reflect.Uint {
sp.SetMin(0)
}
}
if f, ok := tags.Lookup("format"); ok {
sp.SetFormat(f)
}
setFromTag(tags, "min", func(v float32) { sp.SetMin(v) })
setFromTag(tags, "max", func(v float32) { sp.SetMax(v) })
setFromTag(tags, "step", func(v float32) { sp.SetStep(v) })
}
func (sp *Spinner) Init() {
sp.TextField.Init()
sp.SetStep(0.1).SetPageStep(0.2).SetPrecision(6).SetFormat("%g")
sp.SetLeadingIcon(icons.Remove, func(e events.Event) {
sp.incrementValue(-1)
}).SetTrailingIcon(icons.Add, func(e events.Event) {
sp.incrementValue(1)
})
sp.Updater(sp.setTextToValue)
sp.Styler(func(s *styles.Style) {
s.VirtualKeyboard = styles.KeyboardNumber
if sp.IsReadOnly() {
s.Min.X.Ch(6)
s.Max.X.Ch(14)
} else {
s.Min.X.Ch(14)
s.Max.X.Ch(22)
}
// s.Text.Align = styles.End // this doesn't work
})
sp.On(events.Scroll, func(e events.Event) {
if sp.IsReadOnly() || !sp.StateIs(states.Focused) {
return
}
se := e.(*events.MouseScroll)
se.SetHandled()
sp.incrementValue(float32(se.Delta.Y))
})
sp.SetValidator(func() error {
text := sp.Text()
val, err := sp.stringToValue(text)
if err != nil {
return err
}
sp.SetValue(val)
return nil
})
sp.OnKeyChord(func(e events.Event) {
if sp.IsReadOnly() {
return
}
kf := keymap.Of(e.KeyChord())
if DebugSettings.KeyEventTrace {
slog.Info("Spinner KeyChordEvent", "widget", sp, "keyFunction", kf)
}
switch {
case kf == keymap.MoveUp:
e.SetHandled()
sp.incrementValue(1)
case kf == keymap.MoveDown:
e.SetHandled()
sp.incrementValue(-1)
case kf == keymap.PageUp:
e.SetHandled()
sp.pageIncrementValue(1)
case kf == keymap.PageDown:
e.SetHandled()
sp.pageIncrementValue(-1)
}
})
i := func(w *Button) {
w.Styler(func(s *styles.Style) {
// icons do not get separate focus, as people can
// use the arrow keys to get the same effect
s.SetAbilities(false, abilities.Focusable)
s.SetAbilities(true, abilities.RepeatClickable)
})
}
sp.Maker(func(p *tree.Plan) {
if sp.IsReadOnly() {
return
}
tree.AddInit(p, "lead-icon", i)
tree.AddInit(p, "trail-icon", i)
})
}
func (sp *Spinner) setTextToValue() {
sp.SetText(sp.valueToString(sp.Value))
}
// SetMin sets the minimum bound on the value.
func (sp *Spinner) SetMin(min float32) *Spinner {
sp.HasMin = true
sp.Min = min
return sp
}
// SetMax sets the maximum bound on the value.
func (sp *Spinner) SetMax(max float32) *Spinner {
sp.HasMax = true
sp.Max = max
return sp
}
// SetValue sets the value, enforcing any limits, and updates the display.
func (sp *Spinner) SetValue(val float32) *Spinner {
sp.Value = val
if sp.HasMax && sp.Value > sp.Max {
sp.Value = sp.Max
} else if sp.HasMin && sp.Value < sp.Min {
sp.Value = sp.Min
}
sp.Value = math32.Truncate(sp.Value, sp.Precision)
if sp.EnforceStep {
// round to the nearest step
sp.Value = sp.Step * math32.Round(sp.Value/sp.Step)
}
sp.setTextToValue()
sp.NeedsRender()
return sp
}
// setValueEvent calls SetValue and also sends a change event.
func (sp *Spinner) setValueEvent(val float32) *Spinner {
sp.SetValue(val)
sp.SendChange()
return sp
}
// incrementValue increments the value by given number of steps (+ or -),
// and enforces it to be an even multiple of the step size (snap-to-value),
// and sends a change event.
func (sp *Spinner) incrementValue(steps float32) *Spinner {
if sp.IsReadOnly() {
return sp
}
val := sp.Value + steps*sp.Step
val = sp.wrapAround(val)
return sp.setValueEvent(val)
}
// pageIncrementValue increments the value by given number of page steps (+ or -),
// and enforces it to be an even multiple of the step size (snap-to-value),
// and sends a change event.
func (sp *Spinner) pageIncrementValue(steps float32) *Spinner {
if sp.IsReadOnly() {
return sp
}
if sp.PageStep < sp.Step {
sp.PageStep = 2 * sp.Step
}
val := sp.Value + steps*sp.PageStep
val = sp.wrapAround(val)
return sp.setValueEvent(val)
}
// wrapAround, if the spinner has a min and a max, converts values less
// than min to max and values greater than max to min.
func (sp *Spinner) wrapAround(val float32) float32 {
if !sp.HasMin || !sp.HasMax {
return val
}
if val < sp.Min {
return sp.Max
}
if val > sp.Max {
return sp.Min
}
return val
}
// formatIsInt returns true if the format string requires an integer value
func (sp *Spinner) formatIsInt() bool {
if sp.Format == "" {
return false
}
fc := sp.Format[len(sp.Format)-1]
switch fc {
case 'd', 'b', 'c', 'o', 'O', 'q', 'x', 'X', 'U':
return true
}
return false
}
// valueToString converts the value to the string representation thereof
func (sp *Spinner) valueToString(val float32) string {
if sp.formatIsInt() {
return fmt.Sprintf(sp.Format, int64(val))
}
return fmt.Sprintf(sp.Format, val)
}
// stringToValue converts the string field back to float value
func (sp *Spinner) stringToValue(str string) (float32, error) {
if sp.Format == "" {
f64, err := strconv.ParseFloat(str, 32)
return float32(f64), err
}
var err error
if sp.formatIsInt() {
var ival int
_, err = fmt.Sscanf(str, sp.Format, &ival)
if err == nil {
return float32(ival), nil
}
} else {
var fval float32
_, err = fmt.Sscanf(str, sp.Format, &fval)
if err == nil {
return fval, nil
}
}
// if we have an error using the formatted version,
// we try using a pure parse
f64, ferr := strconv.ParseFloat(str, 32)
if ferr == nil {
return float32(f64), nil
}
// if everything fails, we return the error for the
// formatted version
return 0, err
}
func (sp *Spinner) WidgetTooltip(pos image.Point) (string, image.Point) {
res, rpos := sp.TextField.WidgetTooltip(pos)
if sp.error != nil {
return res, rpos
}
if sp.HasMin {
if res != "" {
res += " "
}
res += "(minimum: " + sp.valueToString(sp.Min)
if !sp.HasMax {
res += ")"
}
}
if sp.HasMax {
if sp.HasMin {
res += ", "
} else if res != "" {
res += " ("
} else {
res += "("
}
res += "maximum: " + sp.valueToString(sp.Max) + ")"
}
return res, rpos
}
// 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 core
import (
"log/slog"
"strconv"
"strings"
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/events"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/system"
"cogentcore.org/core/tree"
)
// SplitsTiles specifies 2D tiles for organizing elements within the [Splits] Widget.
// The [styles.Style.Direction] defines the main axis, and the cross axis is orthogonal
// to that, which is organized into chunks of 2 cross-axis "rows". In the case of a
// 1D pattern, only Span is relevant, indicating a single element per split.
type SplitsTiles int32 //enums:enum -trim-prefix Tile
const (
// Span has a single element spanning the cross dimension, i.e.,
// a vertical span for a horizontal main axis, or a horizontal
// span for a vertical main axis. It is the only valid value
// for 1D Splits, where it specifies a single element per split.
// If all tiles are Span, then a 1D line is generated.
TileSpan SplitsTiles = iota
// Split has a split between elements along the cross dimension,
// with the first of 2 elements in the first main axis line and
// the second in the second line.
TileSplit
// FirstLong has a long span of first element along the first
// main axis line and a split between the next two elements
// along the second line, with a split between the two lines.
// Visually, the splits form a T shape for a horizontal main axis.
TileFirstLong
// SecondLong has the first two elements split along the first line,
// and the third with a long span along the second main axis line,
// with a split between the two lines. Visually, the splits form
// an inverted T shape for a horizontal main axis.
TileSecondLong
// Plus is arranged like a plus sign + with the main split along
// the main axis line, and then individual cross-axis splits
// between the first two and next two elements.
TilePlus
)
var (
// tileNumElements is the number of elements per tile.
// the number of splitter handles is n-1.
tileNumElements = map[SplitsTiles]int{TileSpan: 1, TileSplit: 2, TileFirstLong: 3, TileSecondLong: 3, TilePlus: 4}
// tileNumSubSplits is the number of SubSplits proportions per tile.
// The Long cases require 2 pairs, first for the split along the cross axis
// and second for the split along the main axis; Plus requires 3 pairs.
tileNumSubSplits = map[SplitsTiles]int{TileSpan: 1, TileSplit: 2, TileFirstLong: 4, TileSecondLong: 4, TilePlus: 6}
)
// Splits allocates a certain proportion of its space to each of its children,
// organized along [styles.Style.Direction] as the main axis, and supporting
// [SplitsTiles] of 2D splits configurations along the cross axis.
// There is always a split between each Tile segment along the main axis,
// with the proportion of the total main axis space per Tile allocated
// according to normalized Splits factors.
// If all Tiles are Span then a 1D line is generated. Children are allocated
// in order along the main axis, according to each of the Tiles,
// which consume 1 to 4 elements, and have 0 to 3 splits internally.
// The internal split proportion are stored separately in SubSplits.
// A [Handle] widget is added to the Parts for each split, allowing the user
// to drag the relative size of each splits region.
// If more complex geometries are required, use nested Splits.
type Splits struct {
Frame
// Tiles specifies the 2D layout of elements along the [styles.Style.Direction]
// main axis and the orthogonal cross axis. If all Tiles are TileSpan, then
// a 1D line is generated. There is always a split between each Tile segment,
// and different tiles consume different numbers of elements in order, and
// have different numbers of SubSplits. Because each Tile can represent a
// different number of elements, care must be taken to ensure that the full
// set of tiles corresponds to the actual number of children. A default
// 1D configuration will be imposed if there is a mismatch.
Tiles []SplitsTiles
// TileSplits is the proportion (0-1 normalized, enforced) of space
// allocated to each Tile element along the main axis.
// 0 indicates that an element should be completely collapsed.
// By default, each element gets the same amount of space.
TileSplits []float32
// SubSplits contains splits proportions for each Tile element, with
// a variable number depending on the Tile. For the First and Second Long
// elements, there are 2 subsets of sub-splits, with 4 total subsplits.
SubSplits [][]float32
// savedSplits is a saved version of the Splits that can be restored
// for dynamic collapse/expand operations.
savedSplits []float32
// savedSubSplits is a saved version of the SubSplits that can be restored
// for dynamic collapse/expand operations.
savedSubSplits [][]float32
// handleDirs contains the target directions for each of the handles.
// this is set by parent split in its style function, and consumed
// by each handle in its own style function.
handleDirs []styles.Directions
}
func (sl *Splits) Init() {
sl.Frame.Init()
sl.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
s.Margin.Zero()
s.Padding.Zero()
s.Min.Y.Em(10)
if sl.SizeClass() == SizeCompact {
s.Direction = styles.Column
} else {
s.Direction = styles.Row
}
})
sl.FinalStyler(func(s *styles.Style) {
sl.styleSplits()
})
sl.SetOnChildAdded(func(n tree.Node) {
if n != sl.Parts {
AsWidget(n).Styler(func(s *styles.Style) {
// splits elements must scroll independently and grow
s.Overflow.Set(styles.OverflowAuto)
s.Grow.Set(1, 1)
})
}
})
sl.OnKeyChord(func(e events.Event) {
kc := string(e.KeyChord())
mod := "Control+"
if TheApp.Platform() == system.MacOS {
mod = "Meta+"
}
if !strings.HasPrefix(kc, mod) {
return
}
kns := kc[len(mod):]
knc, err := strconv.Atoi(kns)
if err != nil {
return
}
kn := int(knc)
if kn == 0 {
e.SetHandled()
sl.evenSplits(sl.TileSplits)
sl.NeedsLayout()
} else if kn <= len(sl.Children) {
e.SetHandled()
if sl.TileSplits[kn-1] <= 0.01 {
sl.restoreChild(kn - 1)
} else {
sl.collapseSplit(true, kn-1)
}
}
})
sl.Updater(func() {
sl.updateSplits()
})
parts := sl.newParts()
parts.Maker(func(p *tree.Plan) {
// handles are organized first between tiles, then within tiles.
sl.styleSplits()
addHand := func(hidx int) {
tree.AddAt(p, "handle-"+strconv.Itoa(hidx), func(w *Handle) {
w.OnChange(func(e events.Event) {
sl.setHandlePos(w.IndexInParent(), w.Value())
})
w.Styler(func(s *styles.Style) {
ix := w.IndexInParent()
if len(sl.handleDirs) > ix {
s.Direction = sl.handleDirs[ix]
}
})
})
}
nt := len(sl.Tiles)
for i := range nt - 1 {
addHand(i)
}
hi := nt - 1
for _, t := range sl.Tiles {
switch t {
case TileSpan:
case TileSplit:
addHand(hi)
hi++
case TileFirstLong, TileSecondLong:
addHand(hi) // long
addHand(hi + 1) // sub
hi += 2
case TilePlus:
addHand(hi) // long
addHand(hi + 1) // sub1
addHand(hi + 2) // sub2
hi += 3
}
}
})
}
// SetSplits sets the split proportions for the children.
// In general you should pass the same number of args
// as there are children, though fewer could be passed.
func (sl *Splits) SetSplits(splits ...float32) *Splits {
sl.updateSplits()
ntc, _ := sl.tilesTotal()
if ntc == 0 {
nc := len(splits)
sl.TileSplits = slicesx.SetLength(sl.TileSplits, nc)
copy(sl.TileSplits, splits)
sl.Tiles = slicesx.SetLength(sl.Tiles, nc)
for i := range nc {
sl.Tiles[i] = TileSpan
}
sl.updateSplits()
return sl
}
for i, sp := range splits {
sl.SetSplit(i, sp)
}
return sl
}
// SetSplit sets the split proportion of relevant display width
// specific to given child index. Also updates other split values
// in proportion.
func (sl *Splits) SetSplit(idx int, val float32) {
ci := 0
for i, t := range sl.Tiles {
tn := tileNumElements[t]
if idx < ci || idx >= ci+tn {
ci += tn
continue
}
ri := idx - ci
switch t {
case TileSpan:
sl.TileSplits[i] = val
sl.normOtherSplits(i, sl.TileSplits)
case TileSplit:
sl.SubSplits[i][ri] = val
sl.normOtherSplits(ri, sl.SubSplits[i])
case TileFirstLong:
if ri == 0 {
sl.SubSplits[i][0] = val
sl.normOtherSplits(0, sl.SubSplits[i][:2])
} else {
sl.SubSplits[i][1+ri] = val
sl.normOtherSplits(ri-1, sl.SubSplits[i][2:])
}
case TileSecondLong:
if ri == 2 {
sl.SubSplits[i][1] = val
sl.normOtherSplits(1, sl.SubSplits[i][:2])
} else {
sl.SubSplits[i][2+ri] = val
sl.normOtherSplits(ri, sl.SubSplits[i][2:])
}
case TilePlus:
si := 2 + ri
gi := (si / 2) * 2
oi := 1 - (si % 2)
sl.SubSplits[i][si] = val
sl.normOtherSplits(oi, sl.SubSplits[i][gi:gi+2])
}
ci += tn
}
}
// Splits returns the split proportion for each child element.
func (sl *Splits) Splits() []float32 {
nc := len(sl.Children)
sv := make([]float32, nc)
for i := range nc {
sv[i] = sl.Split(i)
}
return sv
}
// Split returns the split proportion for given child index
func (sl *Splits) Split(idx int) float32 {
ci := 0
for i, t := range sl.Tiles {
tn := tileNumElements[t]
if idx < ci || idx >= ci+tn {
ci += tn
continue
}
ri := idx - ci
switch t {
case TileSpan:
return sl.TileSplits[i]
case TileSplit:
return sl.SubSplits[i][ri]
case TileFirstLong:
if ri == 0 {
return sl.SubSplits[i][0]
} else {
return sl.SubSplits[i][1+ri]
}
case TileSecondLong:
if ri == 2 {
return sl.SubSplits[i][1]
} else {
return sl.SubSplits[i][2+ri]
}
case TilePlus:
si := 2 + ri
return sl.SubSplits[i][si]
}
ci += tn
}
return 0
}
// ChildIsCollapsed returns true if the split proportion
// for given child index is 0. Also checks the overall tile
// splits for the child.
func (sl *Splits) ChildIsCollapsed(idx int) bool {
if sl.Split(idx) < 0.01 {
return true
}
ci := 0
for i, t := range sl.Tiles {
tn := tileNumElements[t]
if idx < ci || idx >= ci+tn {
ci += tn
continue
}
ri := idx - ci
if sl.TileSplits[i] < 0.01 {
return true
}
// extra consideration for long split onto subs:
switch t {
case TileFirstLong:
if ri > 0 && sl.SubSplits[i][1] < 0.01 {
return true
}
case TileSecondLong:
if ri < 2 && sl.SubSplits[i][0] < 0.01 {
return true
}
case TilePlus:
if ri < 2 {
return sl.SubSplits[i][0] < 0.01
} else {
return sl.SubSplits[i][1] < 0.01
}
}
return false
}
return false
}
// tilesTotal returns the total number of child elements associated
// with the current set of Tiles elements, and whether there are any
// non-TileSpan elements, which has implications for error handling
// if the total does not match the actual number of children in the Splits.
func (sl *Splits) tilesTotal() (total int, hasNonSpans bool) {
for _, t := range sl.Tiles {
total += tileNumElements[t]
if t != TileSpan {
hasNonSpans = true
}
}
return
}
// updateSplits ensures the Tiles, TileSplits and SubSplits
// are all configured properly, given the number of children.
func (sl *Splits) updateSplits() *Splits {
nc := len(sl.Children)
ntc, hasNonSpans := sl.tilesTotal()
if nc == 0 && ntc == 0 {
return sl
}
if nc > 0 && ntc != nc {
if ntc != 0 && hasNonSpans {
slog.Error("core.Splits: number of children for current Tiles != number of actual children, reverting to 1D", "children", nc, "tiles", ntc)
}
sl.Tiles = slicesx.SetLength(sl.Tiles, nc)
for i := range nc {
sl.Tiles[i] = TileSpan
}
}
nt := len(sl.Tiles)
sl.TileSplits = slicesx.SetLength(sl.TileSplits, nt)
sl.normSplits(sl.TileSplits)
sl.SubSplits = slicesx.SetLength(sl.SubSplits, nt)
for i, t := range sl.Tiles {
ssn := tileNumSubSplits[t]
ss := sl.SubSplits[i]
ss = slicesx.SetLength(ss, ssn)
switch t {
case TileSpan:
ss[0] = 1
case TileSplit:
sl.normSplits(ss)
case TileFirstLong, TileSecondLong:
sl.normSplits(ss[:2]) // first is cross-axis
sl.normSplits(ss[2:])
case TilePlus:
for j := range 3 {
sl.normSplits(ss[2*j : 2*j+2])
}
}
sl.SubSplits[i] = ss
}
return sl
}
// normSplits normalizes the given splits proportions,
// using evenSplits if all zero
func (sl *Splits) normSplits(s []float32) {
sum := float32(0)
for _, sp := range s {
sum += sp
}
if sum == 0 { // set default even splits
sl.evenSplits(s)
return
}
norm := 1 / sum
for i := range s {
s[i] *= norm
}
}
// normOtherSplits normalizes the given splits proportions,
// while keeping the one at the given index at its current value.
func (sl *Splits) normOtherSplits(idx int, s []float32) {
n := len(s)
if n == 1 {
return
}
val := s[idx]
sum := float32(0)
even := (1 - val) / float32(n-1)
for i, sp := range s {
if i != idx {
if sp == 0 {
s[i], sp = even, even
}
sum += sp
}
}
norm := (1 - val) / sum
nsum := float32(0)
for i := range s {
if i != idx {
s[i] *= norm
}
nsum += s[i]
}
}
// evenSplits splits space evenly across all elements
func (sl *Splits) evenSplits(s []float32) {
n := len(s)
if n == 0 {
return
}
even := 1.0 / float32(n)
for i := range s {
s[i] = even
}
}
// saveSplits saves the current set of splits in SavedSplits, for a later RestoreSplits
func (sl *Splits) saveSplits() {
n := len(sl.TileSplits)
if n == 0 {
return
}
sl.savedSplits = slicesx.SetLength(sl.savedSplits, n)
copy(sl.savedSplits, sl.TileSplits)
sl.savedSubSplits = slicesx.SetLength(sl.savedSubSplits, n)
for i, ss := range sl.SubSplits {
sv := sl.savedSubSplits[i]
sv = slicesx.SetLength(sv, len(ss))
copy(sv, ss)
sl.savedSubSplits[i] = sv
}
}
// restoreSplits restores a previously saved set of splits (if it exists), does an update
func (sl *Splits) restoreSplits() {
if len(sl.savedSplits) != len(sl.TileSplits) {
return
}
sl.SetSplits(sl.savedSplits...)
for i, ss := range sl.SubSplits {
sv := sl.savedSubSplits[i]
if len(sv) == len(ss) {
copy(ss, sv)
}
}
sl.NeedsLayout()
}
// setSplitIndex sets given proportional "Splits" space to given value.
// Splits are indexed first by Tiles (major splits) and then
// within tiles, where TileSplit has 2 and the Long cases,
// have the long element first followed by the two smaller ones.
// Calls updateSplits after to ensure renormalization and
// NeedsLayout to ensure layout is updated.
func (sl *Splits) setSplitIndex(idx int, val float32) {
nt := len(sl.Tiles)
if nt == 0 {
return
}
if idx < nt {
sl.TileSplits[idx] = val
return
}
ci := nt
for i, t := range sl.Tiles {
tn := tileNumElements[t]
ri := idx - ci
if ri < 0 {
break
}
switch t {
case TileSpan:
case TileSplit:
if ri < 2 {
sl.SubSplits[i][ri] = val
}
case TileFirstLong, TileSecondLong:
if ri == 0 {
sl.SubSplits[i][ri] = val
} else {
sl.SubSplits[i][2+ri-1] = val
}
case TilePlus:
sl.SubSplits[i][2+ri] = val
}
ci += tn
}
sl.updateSplits()
sl.NeedsLayout()
}
// collapseSplit collapses the splitter region(s) at given index(es),
// by setting splits value to 0.
// optionally saving the prior splits for later Restore function.
func (sl *Splits) collapseSplit(save bool, idxs ...int) {
if save {
sl.saveSplits()
}
for _, idx := range idxs {
sl.setSplitIndex(idx, 0)
}
}
// setHandlePos sets given splits handle position to given 0-1 normalized value.
// Handles are indexed 0..Tiles-1 for main tiles handles, then sequentially
// for any additional child sub-splits depending on tile config.
// Calls updateSplits after to ensure renormalization and
// NeedsLayout to ensure layout is updated.
func (sl *Splits) setHandlePos(idx int, val float32) {
val = math32.Clamp(val, 0, 1)
update := func(idx int, nw float32, s []float32) {
n := len(s)
old := s[idx]
sumTo := float32(0)
for i := range idx + 1 {
sumTo += s[i]
}
delta := nw - sumTo
uval := old + delta
if uval < 0 {
uval = 0
delta = -old
nw = sumTo + delta
}
rmdr := 1 - nw
oldrmdr := 1 - sumTo
if oldrmdr <= 0 {
if rmdr > 0 {
dper := rmdr / float32((n-1)-idx)
for i := idx + 1; i < n; i++ {
s[i] = dper
}
}
} else {
for i := idx + 1; i < n; i++ {
cur := s[i]
s[i] = rmdr * (cur / oldrmdr) // proportional
}
}
s[idx] = uval
}
nt := len(sl.Tiles)
if idx < nt-1 {
update(idx, val, sl.TileSplits)
sl.updateSplits()
sl.NeedsLayout()
return
}
ci := nt - 1
for i, t := range sl.Tiles {
tn := tileNumElements[t] - 1
if tn == 0 {
continue
}
if idx < ci || idx >= ci+tn {
ci += tn
continue
}
ri := idx - ci
switch t {
case TileSplit:
update(0, val, sl.SubSplits[i])
case TileFirstLong, TileSecondLong:
if ri == 0 {
update(0, val, sl.SubSplits[i][:2])
} else {
update(0, val, sl.SubSplits[i][2:])
}
case TilePlus:
if ri == 0 {
update(0, val, sl.SubSplits[i][:2])
} else {
gi := ri * 2
update(0, val, sl.SubSplits[i][gi:gi+2])
}
}
ci += tn
}
sl.updateSplits()
sl.NeedsLayout()
}
// restoreChild restores given child(ren)
// todo: not clear if this makes sense anymore
func (sl *Splits) restoreChild(idxs ...int) {
n := len(sl.Children)
for _, idx := range idxs {
if idx >= 0 && idx < n {
sl.TileSplits[idx] = 1.0 / float32(n)
}
}
sl.updateSplits()
sl.NeedsLayout()
}
func (sl *Splits) styleSplits() {
nt := len(sl.Tiles)
nh := nt - 1
for _, t := range sl.Tiles {
nh += tileNumElements[t] - 1
}
sl.handleDirs = slicesx.SetLength(sl.handleDirs, nh)
dir := sl.Styles.Direction
odir := dir.Other()
hi := nt - 1 // extra handles
for i, t := range sl.Tiles {
if i > 0 {
sl.handleDirs[i-1] = dir
}
switch t {
case TileSpan:
case TileSplit:
sl.handleDirs[hi] = odir
hi++
case TileFirstLong, TileSecondLong:
sl.handleDirs[hi] = odir
sl.handleDirs[hi+1] = dir
hi += 2
case TilePlus:
sl.handleDirs[hi] = odir
sl.handleDirs[hi+1] = dir
sl.handleDirs[hi+2] = dir
hi += 3
}
}
}
func (sl *Splits) SizeDownSetAllocs(iter int) {
if sl.NumChildren() <= 1 {
return
}
sl.updateSplits()
sz := &sl.Geom.Size
// note: InnerSpace is computed based on n children -- not accurate!
csz := sz.Alloc.Content
dim := sl.Styles.Direction.Dim()
odim := dim.Other()
cszd := csz.Dim(dim)
cszo := csz.Dim(odim)
gap := sl.Styles.Gap.Dots().Floor()
gapd := gap.Dim(dim)
gapo := gap.Dim(odim)
hand := sl.Parts.Child(0).(*Handle)
hwd := hand.Geom.Size.Actual.Total.Dim(dim)
cszd -= float32(len(sl.TileSplits)-1) * (hwd + gapd)
setCsz := func(idx int, szm, szc float32) {
cwb := AsWidget(sl.Child(idx))
ksz := &cwb.Geom.Size
ksz.Alloc.Total.SetDim(dim, szm)
ksz.Alloc.Total.SetDim(odim, szc)
ksz.setContentFromTotal(&ksz.Alloc)
}
ci := 0
for i, t := range sl.Tiles {
szt := math32.Round(sl.TileSplits[i] * cszd) // tile size, main axis
szcs := cszo - hwd - gapo // cross axis spilt
szs := szt - hwd - gapd
tn := tileNumElements[t]
switch t {
case TileSpan:
setCsz(ci, szt, cszo)
case TileSplit:
setCsz(ci, szt, math32.Round(szcs*sl.SubSplits[i][0]))
setCsz(ci+1, szt, math32.Round(szcs*sl.SubSplits[i][1]))
case TileFirstLong:
fcht := math32.Round(szcs * sl.SubSplits[i][0])
scht := math32.Round(szcs * sl.SubSplits[i][1])
setCsz(ci, szt, fcht)
setCsz(ci+1, math32.Round(szs*sl.SubSplits[i][2]), scht)
setCsz(ci+2, math32.Round(szs*sl.SubSplits[i][3]), scht)
case TileSecondLong:
fcht := math32.Round(szcs * sl.SubSplits[i][1])
scht := math32.Round(szcs * sl.SubSplits[i][0])
setCsz(ci, math32.Round(szs*sl.SubSplits[i][2]), scht)
setCsz(ci+1, math32.Round(szs*sl.SubSplits[i][3]), scht)
setCsz(ci+2, szt, fcht)
case TilePlus:
fcht := math32.Round(szcs * sl.SubSplits[i][0])
scht := math32.Round(szcs * sl.SubSplits[i][1])
setCsz(ci, math32.Round(szs*sl.SubSplits[i][2]), fcht)
setCsz(ci+1, math32.Round(szs*sl.SubSplits[i][3]), fcht)
setCsz(ci+2, math32.Round(szs*sl.SubSplits[i][4]), scht)
setCsz(ci+3, math32.Round(szs*sl.SubSplits[i][5]), scht)
}
ci += tn
}
}
func (sl *Splits) positionSplits() {
if sl.NumChildren() <= 1 {
return
}
if sl.Parts != nil {
sl.Parts.Geom.Size = sl.Geom.Size // inherit: allows bbox to include handle
}
sz := &sl.Geom.Size
dim := sl.Styles.Direction.Dim()
odim := dim.Other()
csz := sz.Alloc.Content
cszd := csz.Dim(dim)
cszo := csz.Dim(odim)
gap := sl.Styles.Gap.Dots().Floor()
gapd := gap.Dim(dim)
gapo := gap.Dim(odim)
hand := sl.Parts.Child(0).(*Handle)
hwd := hand.Geom.Size.Actual.Total.Dim(dim)
hht := hand.Geom.Size.Actual.Total.Dim(odim)
cszd -= float32(len(sl.TileSplits)-1) * (hwd + gapd)
hwdg := hwd + 0.5*gapd
setChildPos := func(idx int, dpos, opos float32) {
cwb := AsWidget(sl.Child(idx))
cwb.Geom.RelPos.SetDim(dim, dpos)
cwb.Geom.RelPos.SetDim(odim, opos)
}
setHandlePos := func(idx int, dpos, opos, lpos, mn, mx float32) {
hl := sl.Parts.Child(idx).(*Handle)
hl.Geom.RelPos.SetDim(dim, dpos)
hl.Geom.RelPos.SetDim(odim, opos)
hl.Pos = lpos
hl.Min = mn
hl.Max = mx
}
tpos := float32(0) // tile position
ci := 0
nt := len(sl.Tiles)
hi := nt - 1 // extra handles
for i, t := range sl.Tiles {
szt := math32.Round(sl.TileSplits[i] * cszd) // tile size, main axis
szcs := cszo - hwd - gapo // cross axis spilt
szs := szt - hwd - gapd
tn := tileNumElements[t]
if i > 0 {
setHandlePos(i-1, tpos-hwdg, .5*(cszo-hht), tpos, 0, cszd)
}
switch t {
case TileSpan:
setChildPos(ci, tpos, 0)
case TileSplit:
fcht := math32.Round(szcs * sl.SubSplits[i][0])
setHandlePos(hi, tpos+.5*(szt-hht), fcht+0.5*gapo, fcht, 0, szcs)
hi++
setChildPos(ci, tpos, 0)
setChildPos(ci+1, tpos, fcht+hwd+gapo)
case TileFirstLong, TileSecondLong:
fcht := math32.Round(szcs * sl.SubSplits[i][0])
scht := math32.Round(szcs * sl.SubSplits[i][1])
swd := math32.Round(szs * sl.SubSplits[i][2])
bot := fcht + hwd + gapo
setHandlePos(hi, tpos+.5*(szt-hht), fcht+0.5*gapo, fcht, 0, szcs) // long
if t == TileFirstLong {
setHandlePos(hi+1, tpos+swd+0.5*gapd, bot+0.5*(scht-hht), tpos+swd, tpos, tpos+szs)
setChildPos(ci, tpos, 0)
setChildPos(ci+1, tpos, bot)
setChildPos(ci+2, tpos+swd+hwd+gapd, bot)
} else {
setHandlePos(hi+1, tpos+swd+0.5*gapd, 0.5*(fcht-hht), tpos+swd, tpos, tpos+szs)
setChildPos(ci, tpos, 0)
setChildPos(ci+1, tpos+swd+hwd+gapd, 0)
setChildPos(ci+2, tpos, bot)
}
hi += 2
case TilePlus:
fcht := math32.Round(szcs * sl.SubSplits[i][0])
scht := math32.Round(szcs * sl.SubSplits[i][1])
bot := fcht + hwd + gapo
setHandlePos(hi, tpos+.5*(szt-hht), fcht+0.5*gapo, fcht, 0, szcs) // long
swd1 := math32.Round(szs * sl.SubSplits[i][2])
swd2 := math32.Round(szs * sl.SubSplits[i][4])
setHandlePos(hi+1, tpos+swd1+0.5*gapd, 0.5*(fcht-hht), tpos+swd1, tpos, tpos+szs)
setHandlePos(hi+2, tpos+swd2+0.5*gapd, bot+0.5*(scht-hht), tpos+swd2, tpos, tpos+szs)
setChildPos(ci, tpos, 0)
setChildPos(ci+1, tpos+swd1+hwd+gapd, 0)
setChildPos(ci+2, tpos, bot)
setChildPos(ci+3, tpos+swd2+hwd+gapd, bot)
hi += 3
}
ci += tn
tpos += szt + hwd + gapd
}
}
func (sl *Splits) Position() {
if !sl.HasChildren() {
sl.Frame.Position()
return
}
sl.updateSplits()
sl.ConfigScrolls()
sl.positionSplits()
sl.positionChildren()
}
func (sl *Splits) RenderWidget() {
if sl.PushBounds() {
sl.ForWidgetChildren(func(i int, kwi Widget, cwb *WidgetBase) bool {
cwb.SetState(sl.ChildIsCollapsed(i), states.Invisible)
kwi.RenderWidget()
return tree.Continue
})
sl.renderParts()
sl.PopBounds()
}
}
// 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 core
import (
"image"
"cogentcore.org/core/base/ordmap"
"cogentcore.org/core/events"
"cogentcore.org/core/math32"
"cogentcore.org/core/system"
"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)
type Sprite struct {
// Active is whether this sprite is Active now or not.
Active bool
// Name is the unique name of the sprite.
Name string
// 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
// pixels to render, which should be the same size as [Sprite.Geom.Size]
Pixels *image.RGBA
// listeners are event listener functions for processing events on this widget.
// They are called in sequential descending order (so the last added listener
// is called first). They should be added using the On function. FirstListeners
// and FinalListeners are called before and after these listeners, respectively.
listeners events.Listeners `copier:"-" json:"-" xml:"-" set:"-"`
}
// 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
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
}
sp.Pixels = image.NewRGBA(image.Rectangle{Max: nwsz})
return true
}
// 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()
if wb.Scene.Pixels == 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(), wb.Scene.Pixels, wb.Geom.TotalBBox.Min, draw.Src)
return img
}
// On adds the given event handler to the sprite's Listeners for the given event type.
// Listeners are called in sequential descending order, so this listener will be called
// before all of the ones added before it.
func (sp *Sprite) On(etype events.Types, fun func(e events.Event)) *Sprite {
sp.listeners.Add(etype, fun)
return sp
}
// OnClick adds an event listener function for [events.Click] events
func (sp *Sprite) OnClick(fun func(e events.Event)) *Sprite {
return sp.On(events.Click, fun)
}
// OnSlideStart adds an event listener function for [events.SlideStart] events
func (sp *Sprite) OnSlideStart(fun func(e events.Event)) *Sprite {
return sp.On(events.SlideStart, fun)
}
// OnSlideMove adds an event listener function for [events.SlideMove] events
func (sp *Sprite) OnSlideMove(fun func(e events.Event)) *Sprite {
return sp.On(events.SlideMove, fun)
}
// OnSlideStop adds an event listener function for [events.SlideStop] events
func (sp *Sprite) OnSlideStop(fun func(e events.Event)) *Sprite {
return sp.On(events.SlideStop, fun)
}
// HandleEvent sends the given event to all listeners for that event type.
func (sp *Sprite) handleEvent(e events.Event) {
sp.listeners.Call(e)
}
// send sends an new event of the given type to this sprite,
// optionally starting from values in the given original event
// (recommended to include where possible).
// Do not send an existing event using this method if you
// want the Handled state to persist throughout the call chain;
// call [Sprite.handleEvent] directly for any existing events.
func (sp *Sprite) send(typ events.Types, original ...events.Event) {
var e events.Event
if len(original) > 0 && original[0] != nil {
e = original[0].NewFromClone(typ)
} else {
e = &events.Base{Typ: typ}
e.Init()
}
sp.handleEvent(e)
}
// Sprites manages a collection of Sprites, with unique name ids.
type Sprites struct {
ordmap.Map[string, *Sprite]
// set to true if sprites have been modified since last config
Modified bool
}
// 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) {
ss.Init()
ss.Map.Add(sp.Name, sp)
ss.Modified = true
}
// 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.DeleteKey(sp.Name)
ss.Modified = true
}
// SpriteByName returns the sprite by name
func (ss *Sprites) SpriteByName(name string) (*Sprite, bool) {
return ss.ValueByKeyTry(name)
}
// reset removes all sprites
func (ss *Sprites) reset() {
ss.Reset()
ss.Modified = true
}
// 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!
}
if !sp.Active {
sp.Active = true
ss.Modified = true
}
}
// 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!
}
if sp.Active {
sp.Active = false
ss.Modified = true
}
}
// drawSprites draws sprites
func (ss *Sprites) drawSprites(drw system.Drawer, scpos image.Point) {
for _, kv := range ss.Order {
sp := kv.Value
if !sp.Active {
continue
}
// note: in general we assume sprites are static, so Unchanged.
// if needed, could add a "dynamic" flag or something.
drw.Copy(sp.Geom.Pos.Add(scpos), sp.Pixels, sp.Pixels.Bounds(), draw.Over, system.Unchanged)
}
ss.Modified = false
}
// 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 (
"fmt"
"image"
"strings"
"time"
"cogentcore.org/core/base/option"
"cogentcore.org/core/system"
)
// StageTypes are the types of [Stage] containers.
// There are two main categories: MainStage and PopupStage.
// MainStages are [WindowStage] and [DialogStage], which are
// large and potentially complex [Scene]s that persist until
// dismissed. PopupStages are [MenuStage], [TooltipStage],
// [SnackbarStage], and [CompleterStage], which are transitory
// and simple, without additional decorations. MainStages live
// in a [stages] associated with a [renderWindow] and manage
// their own set of PopupStages via another [stages].
type StageTypes int32 //enums:enum
const (
// WindowStage is a MainStage that displays a [Scene] in a full window.
// One of these must be created first, as the primary app content,
// and it typically persists throughout. It fills the [renderWindow].
// Additional windows can be created either within the same [renderWindow]
// on all platforms or in separate [renderWindow]s on desktop platforms.
WindowStage StageTypes = iota
// DialogStage is a MainStage that displays a [Scene] in a smaller dialog
// window on top of a [WindowStage], or in a full or separate window.
// It can be [Stage.Modal] or not.
DialogStage
// MenuStage is a PopupStage that displays a [Scene] typically containing
// [Button]s overlaid on a MainStage. It is typically [Stage.Modal] and
// [Stage.ClickOff], and closes when an button is clicked.
MenuStage
// TooltipStage is a PopupStage that displays a [Scene] with extra text
// info for a widget overlaid on a MainStage. It is typically [Stage.ClickOff]
// and not [Stage.Modal].
TooltipStage
// SnackbarStage is a PopupStage that displays a [Scene] with text info
// and an optional additional button. It is displayed at the bottom of the
// screen. It is typically not [Stage.ClickOff] or [Stage.Modal], but has a
// [Stage.Timeout].
SnackbarStage
// CompleterStage is a PopupStage that displays a [Scene] with text completion
// options, spelling corrections, or other such dynamic info. It is typically
// [Stage.ClickOff], not [Stage.Modal], dynamically updating, and closes when
// something is selected or typing renders it no longer relevant.
CompleterStage
)
// isMain returns true if this type of Stage is a Main stage that manages
// its own set of popups
func (st StageTypes) isMain() bool {
return st <= DialogStage
}
// isPopup returns true if this type of Stage is a Popup, managed by another
// Main stage.
func (st StageTypes) isPopup() bool {
return !st.isMain()
}
// Stage is a container and manager for displaying a [Scene]
// in different functional ways, defined by [StageTypes].
type Stage struct { //types:add -setters
// Type is the type of [Stage], which determines behavior and styling.
Type StageTypes `set:"-"`
// Scene contents of this [Stage] (what it displays).
Scene *Scene `set:"-"`
// Context is a widget in another scene that requested this stage to be created
// and provides context.
Context Widget
// Name is the name of the Stage, which is generally auto-set
// based on the [Scene.Name].
Name string
// Title is the title of the Stage, which is generally auto-set
// based on the [Body.Title]. It used for the title of [WindowStage]
// and [DialogStage] types, and for a [Text] title widget if
// [Stage.DisplayTitle] is true.
Title string
// Screen specifies the screen number on which a new window is opened
// by default on desktop platforms. It defaults to -1, which indicates
// that the first window should open on screen 0 (the default primary
// screen) and any subsequent windows should open on the same screen as
// the currently active window. Regardless, the automatically saved last
// screen of a window with the same [Stage.Title] takes precedence if it exists;
// see the website documentation on window geometry saving for more information.
// Use [TheApp].ScreenByName("name").ScreenNumber to get the screen by name.
Screen int
// Modal, if true, blocks input to all other stages.
Modal bool `set:"-"`
// Scrim, if true, places a darkening scrim over other stages.
Scrim bool
// ClickOff, if true, dismisses the [Stage] if the user clicks anywhere
// off of the [Stage].
ClickOff bool
// ignoreEvents is whether to send no events to the stage and
// just pass them down to lower stages.
ignoreEvents bool
// NewWindow, if true, opens a [WindowStage] or [DialogStage] in its own
// separate operating system window ([renderWindow]). This is true by
// default for [WindowStage] on non-mobile platforms, otherwise false.
NewWindow bool
// FullWindow, if [Stage.NewWindow] is false, makes [DialogStage]s and
// [WindowStage]s take up the entire window they are created in.
FullWindow bool
// Maximized is whether to make a window take up the entire screen on desktop
// platforms by default. It is different from [Stage.Fullscreen] in that
// fullscreen makes the window truly fullscreen without decorations
// (such as for a video player), whereas maximized keeps decorations and just
// makes it fill the available space. The automatically saved user previous
// maximized state takes precedence.
Maximized bool
// Fullscreen is whether to make a window fullscreen on desktop platforms.
// It is different from [Stage.Maximized] in that fullscreen makes
// the window truly fullscreen without decorations (such as for a video player),
// whereas maximized keeps decorations and just makes it fill the available space.
// Not to be confused with [Stage.FullWindow], which is for stages contained within
// another system window. See [Scene.IsFullscreen] and [Scene.SetFullscreen] to
// check and update fullscreen state dynamically on desktop and web platforms
// ([Stage.SetFullscreen] sets the initial state, whereas [Scene.SetFullscreen]
// sets the current state after the [Stage] is already running).
Fullscreen bool
// UseMinSize uses a minimum size as a function of the total available size
// for sizing new windows and dialogs. Otherwise, only the content size is used.
// The saved window position and size takes precedence on multi-window platforms.
UseMinSize bool
// Resizable specifies whether a window on desktop platforms can
// be resized by the user, and whether a non-full same-window dialog can
// be resized by the user on any platform. It defaults to true.
Resizable bool
// Timeout, if greater than 0, results in a popup stages disappearing
// after this timeout duration.
Timeout time.Duration
// BackButton is whether to add a back button to the top bar that calls
// [Scene.Close] when clicked. If it is unset, is will be treated as true
// on non-[system.Offscreen] platforms for [Stage.FullWindow] but not
// [Stage.NewWindow] [Stage]s that are not the first in the stack.
BackButton option.Option[bool] `set:"-"`
// DisplayTitle is whether to display the [Stage.Title] using a
// [Text] widget in the top bar. It is on by default for [DialogStage]s
// and off for all other stages.
DisplayTitle bool
// Pos is the default target position for the [Stage] to be placed within
// the surrounding window or screen in raw pixels. For a new window on desktop
// platforms, the automatically saved user previous window position takes precedence.
Pos image.Point
// If a popup stage, this is the main stage that owns it (via its [Stage.popups]).
// If a main stage, it points to itself.
Main *Stage `set:"-"`
// For main stages, this is the stack of the popups within it
// (created specifically for the main stage).
// For popups, this is the pointer to the popups within the
// main stage managing it.
popups *stages
// For all stages, this is the main [Stages] that lives in a [renderWindow]
// and manages the main stages.
Mains *stages `set:"-"`
// rendering context which has info about the RenderWindow onto which we render.
// This should be used instead of the RenderWindow itself for all relevant
// rendering information. This is only available once a Stage is Run,
// and must always be checked for nil.
renderContext *renderContext
// Sprites are named images that are rendered last overlaying everything else.
Sprites Sprites `json:"-" xml:"-" set:"-"`
}
func (st *Stage) String() string {
str := fmt.Sprintf("%s Type: %s", st.Name, st.Type)
if st.Scene != nil {
str += " Scene: " + st.Scene.Name
}
rc := st.renderContext
if rc != nil {
str += " Rc: " + rc.String()
}
return str
}
// SetBackButton sets [Stage.BackButton] using [option.Option.Set].
func (st *Stage) SetBackButton(b bool) *Stage {
st.BackButton.Set(b)
return st
}
// setNameFromScene sets the name of this Stage based on existing
// Scene and Type settings.
func (st *Stage) setNameFromScene() *Stage {
if st.Scene == nil {
return nil
}
sc := st.Scene
st.Name = sc.Name + "-" + strings.ToLower(st.Type.String())
if sc.Body != nil {
st.Title = sc.Body.Title
}
return st
}
func (st *Stage) setScene(sc *Scene) *Stage {
st.Scene = sc
if sc != nil {
sc.Stage = st
st.setNameFromScene()
}
return st
}
// setMains sets the [Stage.Mains] to the given stack of main stages,
// and also sets the RenderContext from that.
func (st *Stage) setMains(sm *stages) *Stage {
st.Mains = sm
st.renderContext = sm.renderContext
return st
}
// setPopups sets the [Stage.Popups] and [Stage.Mains] from the given main
// stage to which this popup stage belongs.
func (st *Stage) setPopups(mainSt *Stage) *Stage {
st.Main = mainSt
st.Mains = mainSt.Mains
st.popups = mainSt.popups
st.renderContext = st.Mains.renderContext
return st
}
// setType sets the type and also sets default parameters based on that type
func (st *Stage) setType(typ StageTypes) *Stage {
st.Type = typ
st.UseMinSize = true
st.Resizable = true
st.Screen = -1
switch st.Type {
case WindowStage:
if !TheApp.Platform().IsMobile() {
st.NewWindow = true
}
st.FullWindow = true
st.Modal = true // note: there is no global modal option between RenderWindow windows
case DialogStage:
st.Modal = true
st.Scrim = true
st.ClickOff = true
st.DisplayTitle = true
case MenuStage:
st.Modal = true
st.Scrim = false
st.ClickOff = true
case TooltipStage:
st.Modal = false
st.ClickOff = true
st.Scrim = false
st.ignoreEvents = true
case SnackbarStage:
st.Modal = false
case CompleterStage:
st.Modal = false
st.Scrim = false
st.ClickOff = true
}
return st
}
// SetModal sets modal flag for blocking other input (for dialogs).
// Also updates [Stage.Scrim] accordingly if not modal.
func (st *Stage) SetModal(modal bool) *Stage {
st.Modal = modal
if !st.Modal {
st.Scrim = false
}
return st
}
// Run runs the stage using the default run behavior based on the type of stage.
func (st *Stage) Run() *Stage {
if system.OnSystemWindowCreated == nil {
return st.run()
}
// need to prevent premature quitting by ensuring
// that WinWait is not done until we run the Stage
windowWait.Add(1)
go func() {
<-system.OnSystemWindowCreated
system.OnSystemWindowCreated = nil // no longer applicable
st.run()
// now that we have run the Stage, WinWait is accurate and
// we no longer need to prevent it from being done
windowWait.Done()
}()
return st
}
// run is the implementation of [Stage.Run].
func (st *Stage) run() *Stage {
defer func() { system.HandleRecover(recover()) }()
switch st.Type {
case WindowStage:
return st.runWindow()
case DialogStage:
return st.runDialog()
default:
return st.runPopup()
}
}
// doUpdate calls doUpdate on our Scene and UpdateAll on our Popups for Main types.
// returns stageMods = true if any Popup Stages have been modified
// and sceneMods = true if any Scenes have been modified.
func (st *Stage) doUpdate() (stageMods, sceneMods bool) {
if st.Scene == nil {
return
}
if st.Type.isMain() && st.popups != nil {
stageMods, sceneMods = st.popups.updateAll()
}
scMods := st.Scene.doUpdate()
sceneMods = sceneMods || scMods
// if scMods {
// fmt.Println("scene mod", st.Scene.Name)
// }
return
}
// raise moves the Stage to the top of its main [stages]
// and raises the [renderWindow] it is in if necessary.
func (st *Stage) raise() {
if st.Mains.renderWindow != currentRenderWindow {
st.Mains.renderWindow.Raise()
}
st.Mains.moveToTop(st)
currentRenderWindow.SetStageTitle(st.Title)
}
func (st *Stage) delete() {
if st.Type.isMain() && st.popups != nil {
st.popups.deleteAll()
st.Sprites.reset()
}
if st.Scene != nil {
st.Scene.DeleteChildren()
}
st.Scene = nil
st.Main = nil
st.popups = nil
st.Mains = nil
st.renderContext = nil
}
// 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 (
"fmt"
"sync"
"cogentcore.org/core/base/ordmap"
"cogentcore.org/core/math32"
)
// stages manages a stack of [Stage]s.
type stages struct {
// stack is the stack of stages managed by this stage manager.
stack ordmap.Map[string, *Stage]
// modified is set to true whenever the stack has been modified.
// This is cleared by the RenderWindow each render cycle.
modified bool
// rendering context provides key rendering information and locking
// for the RenderWindow in which the stages are running.
renderContext *renderContext
// render window to which we are rendering.
// rely on the RenderContext wherever possible.
renderWindow *renderWindow
// main is the main stage that owns this [Stages].
// This is only set for popup stages.
main *Stage
// mutex protecting reading / updating of the Stack.
// Destructive stack updating gets a Write lock, else Read.
mu sync.RWMutex
}
// top returns the top-most Stage in the Stack, under Read Lock
func (sm *stages) top() *Stage {
sm.mu.RLock()
defer sm.mu.RUnlock()
sz := sm.stack.Len()
if sz == 0 {
return nil
}
return sm.stack.ValueByIndex(sz - 1)
}
// uniqueName returns unique name for given item
func (sm *stages) uniqueName(nm string) string {
ctr := 0
for _, kv := range sm.stack.Order {
if kv.Key == nm {
ctr++
}
}
if ctr > 0 {
return fmt.Sprintf("%s-%d", nm, len(sm.stack.Order))
}
return nm
}
// push pushes a new Stage to top, under Write lock
func (sm *stages) push(st *Stage) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.modified = true
sm.stack.Add(sm.uniqueName(st.Name), st)
}
// deleteStage deletes given stage (removing from stack, calling Delete
// on Stage), returning true if found.
// It runs under Write lock.
func (sm *stages) deleteStage(st *Stage) bool {
sm.mu.Lock()
defer sm.mu.Unlock()
l := sm.stack.Len()
for i := l - 1; i >= 0; i-- {
s := sm.stack.ValueByIndex(i)
if st == s {
sm.modified = true
sm.stack.DeleteIndex(i, i+1)
st.delete()
return true
}
}
return false
}
// deleteStageAndBelow deletes given stage (removing from stack,
// calling Delete on Stage), returning true if found.
// And also deletes all stages of the same type immediately below it.
// It runs under Write lock.
func (sm *stages) deleteStageAndBelow(st *Stage) bool {
sm.mu.Lock()
defer sm.mu.Unlock()
styp := st.Type
l := sm.stack.Len()
got := false
for i := l - 1; i >= 0; i-- {
s := sm.stack.ValueByIndex(i)
if !got {
if st == s {
sm.modified = true
sm.stack.DeleteIndex(i, i+1)
st.delete()
got = true
}
} else {
if s.Type == styp {
sm.stack.DeleteIndex(i, i+1)
st.delete()
}
}
}
return got
}
// moveToTop moves the given stage to the top of the stack,
// returning true if found. It runs under Write lock.
func (sm *stages) moveToTop(st *Stage) bool {
sm.mu.Lock()
defer sm.mu.Unlock()
l := sm.stack.Len()
for i := l - 1; i >= 0; i-- {
s := sm.stack.ValueByIndex(i)
if st == s {
k := sm.stack.KeyByIndex(i)
sm.modified = true
sm.stack.DeleteIndex(i, i+1)
sm.stack.InsertAtIndex(sm.stack.Len(), k, s)
return true
}
}
return false
}
// popType pops the top-most Stage of the given type of the stack,
// returning it or nil if none. It runs under Write lock.
func (sm *stages) popType(typ StageTypes) *Stage {
sm.mu.Lock()
defer sm.mu.Unlock()
l := sm.stack.Len()
for i := l - 1; i >= 0; i-- {
st := sm.stack.ValueByIndex(i)
if st.Type == typ {
sm.modified = true
sm.stack.DeleteIndex(i, i+1)
return st
}
}
return nil
}
// popDeleteType pops the top-most Stage of the given type off the stack
// and calls Delete on it.
func (sm *stages) popDeleteType(typ StageTypes) {
st := sm.popType(typ)
if st != nil {
st.delete()
}
}
// deleteAll deletes all of the stages.
// For when Stage with Popups is Deleted, or when a RenderWindow is closed.
// requires outer RenderContext mutex!
func (sm *stages) deleteAll() {
sm.mu.Lock()
defer sm.mu.Unlock()
sz := sm.stack.Len()
if sz == 0 {
return
}
sm.modified = true
for i := sz - 1; i >= 0; i-- {
st := sm.stack.ValueByIndex(i)
st.delete()
sm.stack.DeleteIndex(i, i+1)
}
}
// resize calls resize on all stages within based on the given
// window render geom.
func (sm *stages) resize(rg math32.Geom2DInt) {
for _, kv := range sm.stack.Order {
st := kv.Value
if st.FullWindow {
st.Sprites.reset()
st.Scene.resize(rg)
} else {
st.Scene.fitInWindow(rg)
}
}
}
// updateAll iterates through all Stages and calls DoUpdate on them.
// returns stageMods = true if any Stages have been modified (Main or Popup),
// and sceneMods = true if any Scenes have been modified.
// Stage calls DoUpdate on its Scene, ensuring everything is updated at the
// Widget level. If nothing is needed, nothing is done.
// This is called only during RenderWindow.RenderWindow,
// under the global RenderContext.Mu Write lock so nothing else can happen.
func (sm *stages) updateAll() (stageMods, sceneMods bool) {
sm.mu.Lock()
defer sm.mu.Unlock()
stageMods = sm.modified
sm.modified = false
sz := sm.stack.Len()
if sz == 0 {
return
}
for _, kv := range sm.stack.Order {
st := kv.Value
stMod, scMod := st.doUpdate()
stageMods = stageMods || stMod
sceneMods = sceneMods || scMod
}
return
}
// windowStage returns the highest level WindowStage (i.e., full window)
func (sm *stages) windowStage() *Stage {
n := sm.stack.Len()
for i := n - 1; i >= 0; i-- {
st := sm.stack.ValueByIndex(i)
if st.Type == WindowStage {
return st
}
}
return nil
}
func (sm *stages) runDeferred() {
for _, kv := range sm.stack.Order {
st := kv.Value
if st.Scene == nil {
continue
}
sc := st.Scene
if sc.hasFlag(sceneContentSizing) {
continue
}
if sc.hasFlag(sceneHasDeferred) {
sc.setFlag(false, sceneHasDeferred)
sc.runDeferred()
}
if sc.showIter == sceneShowIters+1 {
sc.showIter++
if !sc.hasFlag(sceneHasShown) {
sc.setFlag(true, sceneHasShown)
sc.Shown()
}
}
}
}
// 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 core
import (
"image"
"reflect"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/tree"
)
// Styler adds the given function for setting the style properties of the widget
// to [WidgetBase.Stylers.Normal]. It is one of the main ways to specify the styles of
// a widget, in addition to FirstStyler and FinalStyler, which add stylers that
// are called before and after the stylers added by this function, respectively.
func (wb *WidgetBase) Styler(s func(s *styles.Style)) {
wb.Stylers.Normal = append(wb.Stylers.Normal, s)
}
// FirstStyler adds the given function for setting the style properties of the widget
// to [WidgetBase.Stylers.First]. It is one of the main ways to specify the styles of
// a widget, in addition to Styler and FinalStyler, which add stylers that
// are called after the stylers added by this function.
func (wb *WidgetBase) FirstStyler(s func(s *styles.Style)) {
wb.Stylers.First = append(wb.Stylers.First, s)
}
// FinalStyler adds the given function for setting the style properties of the widget
// to [WidgetBase.Stylers.Final]. It is one of the main ways to specify the styles of
// a widget, in addition to FirstStyler and Styler, which add stylers that are called
// before the stylers added by this function.
func (wb *WidgetBase) FinalStyler(s func(s *styles.Style)) {
wb.Stylers.Final = append(wb.Stylers.Final, s)
}
// Style updates the style properties of the widget based on [WidgetBase.Stylers].
// To specify the style properties of a widget, use [WidgetBase.Styler].
func (wb *WidgetBase) Style() {
if wb.This == nil {
return
}
pw := wb.parentWidget()
// we do these things even if we are overriding the style
defer func() {
// note: this does not un-set the Invisible if not None, because all kinds of things
// can turn invisible to off.
if wb.Styles.Display == styles.DisplayNone {
wb.SetState(true, states.Invisible)
}
psz := math32.Vector2{}
if pw != nil {
psz = pw.Geom.Size.Alloc.Content
}
setUnitContext(&wb.Styles, wb.Scene, wb.Geom.Size.Alloc.Content, psz)
}()
if wb.OverrideStyle {
return
}
wb.resetStyleWidget()
if pw != nil {
wb.Styles.InheritFields(&pw.Styles)
}
wb.resetStyleSettings()
wb.runStylers()
wb.styleSettings()
}
// resetStyleWidget resets the widget styles and applies the basic
// default styles specified in [styles.Style.Defaults].
func (wb *WidgetBase) resetStyleWidget() {
s := &wb.Styles
// need to persist state
state := s.State
*s = styles.Style{}
s.Defaults()
s.State = state
// default to state layer associated with the state,
// which the developer can override in their stylers
// wb.Transition(&s.StateLayer, s.State.StateLayer(), 200*time.Millisecond, LinearTransition)
s.StateLayer = s.State.StateLayer()
s.SetMono(false)
}
// runStylers runs the [WidgetBase.Stylers].
func (wb *WidgetBase) runStylers() {
wb.Stylers.Do(func(s []func(s *styles.Style)) {
for _, f := range s {
f(&wb.Styles)
}
})
}
// resetStyleSettings reverses the effects of [WidgetBase.styleSettings]
// for the widget's font size so that it does not create cascading
// inhereted font size values. It only does this for non-root elements,
// as the root element must receive the larger font size so that
// all other widgets inherit it. It must be called before
// [WidgetBase.runStylers] and [WidgetBase.styleSettings].
func (wb *WidgetBase) resetStyleSettings() {
if tree.IsRoot(wb) {
return
}
fsz := AppearanceSettings.FontSize / 100
wb.Styles.Font.Size.Value /= fsz
}
// styleSettings applies [AppearanceSettingsData.Spacing]
// and [AppearanceSettingsData.FontSize] to the style values for the widget.
func (wb *WidgetBase) styleSettings() {
s := &wb.Styles
spc := AppearanceSettings.Spacing / 100
s.Margin.Top.Value *= spc
s.Margin.Right.Value *= spc
s.Margin.Bottom.Value *= spc
s.Margin.Left.Value *= spc
s.Padding.Top.Value *= spc
s.Padding.Right.Value *= spc
s.Padding.Bottom.Value *= spc
s.Padding.Left.Value *= spc
s.Gap.X.Value *= spc
s.Gap.Y.Value *= spc
fsz := AppearanceSettings.FontSize / 100
s.Font.Size.Value *= fsz
}
// StyleTree calls [WidgetBase.Style] on every widget in tree
// underneath and including this widget.
func (wb *WidgetBase) StyleTree() {
wb.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
cw.Style()
return tree.Continue
})
}
// Restyle ensures that the styling of the widget and all of its children
// is updated and rendered by calling [WidgetBase.StyleTree] and
// [WidgetBase.NeedsRender]. It does not trigger a new update or layout
// pass, so it should only be used for non-structural styling changes.
func (wb *WidgetBase) Restyle() {
wb.StyleTree()
wb.NeedsRender()
}
// setUnitContext sets the unit context based on size of scene, element, and parent
// element (from bbox) and then caches everything out in terms of raw pixel
// dots for rendering.
// Zero values for element and parent size are ignored.
func setUnitContext(st *styles.Style, sc *Scene, el, parent math32.Vector2) {
rebuild := false
var rc *renderContext
sz := image.Point{1920, 1080}
if sc != nil {
rebuild = sc.NeedsRebuild()
rc = sc.renderContext()
sz = sc.SceneGeom.Size
}
if rc != nil {
st.UnitContext.DPI = rc.logicalDPI
} else {
st.UnitContext.DPI = 160
}
st.UnitContext.SetSizes(float32(sz.X), float32(sz.Y), el.X, el.Y, parent.X, parent.Y)
if st.Font.Face == nil || rebuild {
st.Font = paint.OpenFont(st.FontRender(), &st.UnitContext) // calls SetUnContext after updating metrics
}
st.ToDots()
}
// ChildBackground returns the background color (Image) for given child Widget.
// By default, this is just our [Styles.Actualbackground] but it can be computed
// specifically for the child (e.g., for zebra stripes in [ListGrid])
func (wb *WidgetBase) ChildBackground(child Widget) image.Image {
return wb.Styles.ActualBackground
}
// parentActualBackground returns the actual background of
// the parent of the widget. If it has no parent, it returns nil.
func (wb *WidgetBase) parentActualBackground() image.Image {
pwb := wb.parentWidget()
if pwb == nil {
return nil
}
return pwb.This.(Widget).ChildBackground(wb.This.(Widget))
}
// setFromTag uses the given tags to call the given set function for the given tag.
func setFromTag(tags reflect.StructTag, tag string, set func(v float32)) {
if v, ok := tags.Lookup(tag); ok {
f, err := reflectx.ToFloat32(v)
if errors.Log(err) == nil {
set(f)
}
}
}
// styleFromTags adds a [WidgetBase.Styler] to the given widget
// to set its style properties based on the given [reflect.StructTag].
// Width, height, and grow properties are supported.
func styleFromTags(w Widget, tags reflect.StructTag) {
w.AsWidget().Styler(func(s *styles.Style) {
setFromTag(tags, "width", s.Min.X.Ch)
setFromTag(tags, "max-width", s.Max.X.Ch)
setFromTag(tags, "height", s.Min.Y.Em)
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 })
})
if tags.Get("new-window") == "+" {
w.AsWidget().setFlag(true, widgetValueNewWindow)
}
}
// 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 (
"image"
"io"
"io/fs"
"strings"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/svg"
"cogentcore.org/core/tree"
"golang.org/x/image/draw"
)
// SVG is a Widget that renders an [svg.SVG] object.
// If it is not [states.ReadOnly], the user can pan and zoom the display.
// By default, it is [states.ReadOnly].
type SVG struct {
WidgetBase
// SVG is the SVG drawing to display.
SVG *svg.SVG `set:"-"`
// prevSize is the cached allocated size for the last rendered image.
prevSize image.Point `xml:"-" json:"-" set:"-"`
}
func (sv *SVG) Init() {
sv.WidgetBase.Init()
sv.SVG = svg.NewSVG(10, 10)
sv.SetReadOnly(true)
sv.Styler(func(s *styles.Style) {
s.Min.Set(units.Dp(256))
ro := sv.IsReadOnly()
s.SetAbilities(!ro, abilities.Slideable, abilities.Activatable, abilities.Scrollable)
if !ro {
if s.Is(states.Active) {
s.Cursor = cursors.Grabbing
s.StateLayer = 0
} else {
s.Cursor = cursors.Grab
}
}
})
sv.FinalStyler(func(s *styles.Style) {
sv.SVG.Root.ViewBox.PreserveAspectRatio.SetFromStyle(s)
})
sv.On(events.SlideMove, func(e events.Event) {
if sv.IsReadOnly() {
return
}
e.SetHandled()
del := e.PrevDelta()
sv.SVG.Translate.X += float32(del.X)
sv.SVG.Translate.Y += float32(del.Y)
sv.NeedsRender()
})
sv.On(events.Scroll, func(e events.Event) {
if sv.IsReadOnly() {
return
}
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.NeedsRender()
})
}
// Open opens an XML-formatted SVG file
func (sv *SVG) Open(filename Filename) error { //types:add
return sv.SVG.OpenXML(string(filename))
}
// OpenSVG opens an XML-formatted SVG file from the given fs.
func (sv *SVG) OpenFS(fsys fs.FS, filename string) error {
return sv.SVG.OpenFS(fsys, filename)
}
// Read reads an XML-formatted SVG file from the given reader.
func (sv *SVG) Read(r io.Reader) error {
return sv.SVG.ReadXML(r)
}
// ReadString reads an XML-formatted SVG file from the given string.
func (sv *SVG) ReadString(s string) error {
return sv.SVG.ReadXML(strings.NewReader(s))
}
// SaveSVG saves the current SVG to an XML-encoded standard SVG file.
func (sv *SVG) SaveSVG(filename Filename) error { //types:add
return sv.SVG.SaveXML(string(filename))
}
// SavePNG saves the current rendered SVG image to an PNG image file.
func (sv *SVG) SavePNG(filename Filename) error { //types:add
return sv.SVG.SavePNG(string(filename))
}
func (sv *SVG) SizeFinal() {
sv.WidgetBase.SizeFinal()
sv.SVG.Resize(sv.Geom.Size.Actual.Content.ToPoint())
}
// renderSVG renders the SVG
func (sv *SVG) renderSVG() {
if sv.SVG == nil {
return
}
// need to make the image again to prevent it from
// rendering over itself
sv.SVG.Pixels = image.NewRGBA(sv.SVG.Pixels.Rect)
sv.SVG.RenderState.Init(sv.SVG.Pixels.Rect.Dx(), sv.SVG.Pixels.Rect.Dy(), sv.SVG.Pixels)
sv.SVG.Render()
sv.prevSize = sv.SVG.Pixels.Rect.Size()
}
func (sv *SVG) Render() {
sv.WidgetBase.Render()
if sv.SVG == nil {
return
}
needsRender := !sv.IsReadOnly()
if !needsRender {
if sv.SVG.Pixels == nil {
needsRender = true
} else {
sz := sv.SVG.Pixels.Bounds().Size()
if sz != sv.prevSize || sz == (image.Point{}) {
needsRender = true
}
}
}
if needsRender {
sv.renderSVG()
}
r := sv.Geom.ContentBBox
sp := sv.Geom.ScrollOffset()
draw.Draw(sv.Scene.Pixels, r, sv.SVG.Pixels, sp, draw.Over)
}
func (sv *SVG) MakeToolbar(p *tree.Plan) {
tree.Add(p, func(w *Button) {
w.SetText("Pan").SetIcon(icons.PanTool)
w.SetTooltip("Toggle the ability to zoom and pan")
w.OnClick(func(e events.Event) {
sv.SetReadOnly(!sv.IsReadOnly())
sv.Restyle()
})
})
tree.Add(p, func(w *Separator) {})
tree.Add(p, func(w *FuncButton) {
w.SetFunc(sv.Open).SetIcon(icons.Open)
})
tree.Add(p, func(w *FuncButton) {
w.SetFunc(sv.SaveSVG).SetIcon(icons.Save)
})
tree.Add(p, func(w *FuncButton) {
w.SetFunc(sv.SavePNG).SetIcon(icons.Save)
})
}
// 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 (
"reflect"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
)
// Switch is a widget that can toggle between an on and off state.
// It can be displayed as a switch, chip, checkbox, radio button,
// or segmented button.
type Switch struct {
Frame
// Type is the styling type of switch.
// It must be set using [Switch.SetType].
Type SwitchTypes `set:"-"`
// Text is the optional text of the switch.
Text string
// IconOn is the icon to use for the on, checked state of the switch.
IconOn icons.Icon
// Iconoff is the icon to use for the off, unchecked state of the switch.
IconOff icons.Icon
// IconIndeterminate is the icon to use for the indeterminate (unknown) state.
IconIndeterminate icons.Icon
}
// SwitchTypes contains the different types of [Switch]es.
type SwitchTypes int32 //enums:enum -trim-prefix Switch -transform kebab
const (
// SwitchSwitch indicates to display a switch as a switch (toggle slider).
SwitchSwitch SwitchTypes = iota
// SwitchChip indicates to display a switch as chip (like Material Design's
// filter chip), which is typically only used in the context of [Switches].
SwitchChip
// SwitchCheckbox indicates to display a switch as a checkbox.
SwitchCheckbox
// SwitchRadioButton indicates to display a switch as a radio button.
SwitchRadioButton
// SwitchSegmentedButton indicates to display a segmented button, which is
// typically only used in the context of [Switches].
SwitchSegmentedButton
)
func (sw *Switch) WidgetValue() any { return sw.IsChecked() }
func (sw *Switch) SetWidgetValue(value any) error {
b, err := reflectx.ToBool(value)
if err != nil {
return err
}
sw.SetChecked(b)
return nil
}
func (sw *Switch) OnBind(value any, tags reflect.StructTag) {
if d, ok := tags.Lookup("display"); ok {
errors.Log(sw.Type.SetString(d))
sw.SetType(sw.Type)
}
}
func (sw *Switch) Init() {
sw.Frame.Init()
sw.Styler(func(s *styles.Style) {
s.SetAbilities(true, abilities.Activatable, abilities.Focusable, abilities.Hoverable, abilities.Checkable)
if !sw.IsReadOnly() {
s.Cursor = cursors.Pointer
}
s.Text.Align = styles.Start
s.Text.AlignV = styles.Center
s.Padding.SetVertical(units.Dp(4))
s.Padding.SetHorizontal(units.Dp(ConstantSpacing(4))) // needed for layout issues
s.Border.Radius = styles.BorderRadiusSmall
s.Gap.Zero()
s.CenterAll()
if sw.Type == 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 {
if !s.Is(states.Focused) {
s.Border.Width.Set(units.Dp(1))
}
if s.Is(states.Checked) {
s.Background = colors.Scheme.SurfaceVariant
s.Color = colors.Scheme.OnSurfaceVariant
}
}
if s.Is(states.Selected) {
s.Background = colors.Scheme.Select.Container
}
})
sw.HandleClickOnEnterSpace()
sw.OnFinal(events.Click, func(e events.Event) {
sw.SetChecked(sw.IsChecked())
if sw.Type == SwitchChip || sw.Type == SwitchSegmentedButton {
sw.updateStackTop() // must update here
sw.NeedsLayout()
} else {
sw.NeedsRender()
}
sw.SendChange(e)
})
sw.Maker(func(p *tree.Plan) {
if sw.IconOn == "" {
sw.IconOn = icons.ToggleOnFill // fallback
}
if sw.IconOff == "" {
sw.IconOff = icons.ToggleOff // fallback
}
tree.AddAt(p, "stack", func(w *Frame) {
w.Styler(func(s *styles.Style) {
s.Display = styles.Stacked
s.Gap.Zero()
})
w.Updater(func() {
sw.updateStackTop() // need to update here
})
w.Maker(func(p *tree.Plan) {
tree.AddAt(p, "icon-on", func(w *Icon) {
w.Styler(func(s *styles.Style) {
if sw.Type == SwitchChip {
s.Color = colors.Scheme.OnSurfaceVariant
} else {
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)
})
})
// same styles for off and indeterminate
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))
}
}
tree.AddAt(p, "icon-off", func(w *Icon) {
w.Styler(iconStyle)
w.Updater(func() {
w.SetIcon(sw.IconOff)
})
})
tree.AddAt(p, "icon-indeterminate", func(w *Icon) {
w.Styler(iconStyle)
w.Updater(func() {
w.SetIcon(sw.IconIndeterminate)
})
})
})
})
if sw.Text != "" {
tree.AddAt(p, "space", func(w *Space) {
w.Styler(func(s *styles.Style) {
s.Min.X.Ch(0.1)
})
})
tree.AddAt(p, "text", func(w *Text) {
w.Styler(func(s *styles.Style) {
s.SetNonSelectable()
s.SetTextWrap(false)
s.FillMargin = false
})
w.Updater(func() {
w.SetText(sw.Text)
})
})
}
})
}
// IsChecked returns whether the switch is checked.
func (sw *Switch) IsChecked() bool {
return sw.StateIs(states.Checked)
}
// SetChecked sets whether the switch it checked.
func (sw *Switch) SetChecked(on bool) *Switch {
sw.SetState(on, states.Checked)
sw.SetState(false, states.Indeterminate)
return sw
}
// updateStackTop updates the [Frame.StackTop] of the stack in the switch
// according to the current icon. It is called automatically to keep the
// switch up-to-date.
func (sw *Switch) updateStackTop() {
st, ok := sw.ChildByName("stack", 0).(*Frame)
if !ok {
return
}
switch {
case sw.StateIs(states.Indeterminate):
st.StackTop = 2
case sw.IsChecked():
st.StackTop = 0
default:
if sw.Type == SwitchChip {
// chips render no icon when off
st.StackTop = -1
return
}
st.StackTop = 1
}
}
// SetType sets the styling type of the switch.
func (sw *Switch) SetType(typ SwitchTypes) *Switch {
sw.Type = typ
sw.IconIndeterminate = icons.Blank
switch sw.Type {
case SwitchSwitch:
// TODO: material has more advanced switches with a checkmark
// if they are turned on; we could implement that at some point
sw.IconOn = icons.ToggleOnFill
sw.IconOff = icons.ToggleOff
sw.IconIndeterminate = icons.ToggleMid
case SwitchChip, SwitchSegmentedButton:
sw.IconOn = icons.Check
sw.IconOff = icons.None
sw.IconIndeterminate = icons.None
case SwitchCheckbox:
sw.IconOn = icons.CheckBoxFill
sw.IconOff = icons.CheckBoxOutlineBlank
sw.IconIndeterminate = icons.IndeterminateCheckBox
case SwitchRadioButton:
sw.IconOn = icons.RadioButtonChecked
sw.IconOff = icons.RadioButtonUnchecked
sw.IconIndeterminate = icons.RadioButtonPartial
}
return sw
}
func (sw *Switch) Render() {
sw.updateStackTop() // important: make sure we're always up-to-date on render
st, ok := sw.ChildByName("stack", 0).(*Frame)
if ok {
st.UpdateStackedVisibility()
}
sw.WidgetBase.Render()
}
// Copyright (c) 2019, 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 (
"fmt"
"reflect"
"slices"
"strconv"
"strings"
"unicode"
"cogentcore.org/core/base/labels"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/enums"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
// Switches is a widget for containing a set of [Switch]es.
// It can optionally enforce mutual exclusivity (ie: radio buttons)
// through the [Switches.Mutex] field. It supports binding to
// [enums.Enum] and [enums.BitFlag] values with appropriate properties
// automatically set.
type Switches struct {
Frame
// Type is the type of switches that will be made.
Type SwitchTypes
// Items are the items displayed to the user.
Items []SwitchItem
// Mutex is whether to make the items mutually exclusive
// (checking one turns off all the others).
Mutex bool
// AllowNone is whether to allow the user to deselect all items.
// It is on by default.
AllowNone bool `default:"true"`
// selectedIndexes are the indexes in [Switches.Items] of the currently
// selected switch items.
selectedIndexes []int
// bitFlagValue is the associated bit flag value if non-nil (for [Value]).
bitFlagValue enums.BitFlagSetter
}
// SwitchItem contains the properties of one item in a [Switches].
type SwitchItem struct {
// Value is the underlying value the switch item represents.
Value any
// Text is the text displayed to the user for this item.
// If it is empty, then [labels.ToLabel] of [SwitchItem.Value]
// is used instead.
Text string
// Tooltip is the tooltip displayed to the user for this item.
Tooltip string
}
// getText returns the effective text for this switch item.
// If [SwitchItem.Text] is set, it returns that. Otherwise,
// it returns [labels.ToLabel] of [SwitchItem.Value].
func (si *SwitchItem) getText() string {
if si.Text != "" {
return si.Text
}
if si.Value == nil {
return ""
}
return labels.ToLabel(si.Value)
}
func (sw *Switches) WidgetValue() any {
if sw.bitFlagValue != nil {
sw.bitFlagFromSelected(sw.bitFlagValue)
// We must return a non-pointer value to prevent [ResetWidgetValue]
// from clearing the bit flag value (since we only ever have one
// total pointer to it, so it is uniquely vulnerable to being destroyed).
return reflectx.Underlying(reflect.ValueOf(sw.bitFlagValue)).Interface()
}
item := sw.SelectedItem()
if item == nil {
return nil
}
return item.Value
}
func (sw *Switches) SetWidgetValue(value any) error {
up := reflectx.UnderlyingPointer(reflect.ValueOf(value))
if bf, ok := up.Interface().(enums.BitFlagSetter); ok {
sw.selectFromBitFlag(bf)
return nil
}
return sw.SelectValue(up.Elem().Interface())
}
func (sw *Switches) OnBind(value any, tags reflect.StructTag) {
if e, ok := value.(enums.Enum); ok {
sw.SetEnum(e).SetType(SwitchSegmentedButton).SetMutex(true)
}
if bf, ok := value.(enums.BitFlagSetter); ok {
sw.bitFlagValue = bf
sw.SetType(SwitchChip).SetMutex(false)
} else {
sw.bitFlagValue = nil
sw.AllowNone = false
}
}
func (sw *Switches) Init() {
sw.Frame.Init()
sw.AllowNone = true
sw.Styler(func(s *styles.Style) {
s.Padding.Set(units.Dp(ConstantSpacing(2)))
s.Margin.Set(units.Dp(ConstantSpacing(2)))
if sw.Type == SwitchSegmentedButton {
s.Gap.Zero()
} else {
s.Wrap = true
}
})
sw.FinalStyler(func(s *styles.Style) {
if s.Direction != styles.Row {
// if we wrap, it just goes in the x direction
s.Wrap = false
}
})
sw.Maker(func(p *tree.Plan) {
for i, item := range sw.Items {
tree.AddAt(p, strconv.Itoa(i), func(w *Switch) {
w.OnChange(func(e events.Event) {
if w.IsChecked() {
if sw.Mutex {
sw.selectedIndexes = []int{i}
} else {
sw.selectedIndexes = append(sw.selectedIndexes, i)
}
} else if sw.AllowNone || len(sw.selectedIndexes) > 1 {
sw.selectedIndexes = slices.DeleteFunc(sw.selectedIndexes, func(v int) bool { return v == i })
}
sw.SendChange(e)
sw.UpdateRender()
})
w.Styler(func(s *styles.Style) {
if sw.Type != SwitchSegmentedButton {
return
}
ip := w.IndexInParent()
brf := styles.BorderRadiusFull.Top
ps := &sw.Styles
if ip == 0 {
if ps.Direction == styles.Row {
s.Border.Radius.Set(brf, units.Zero(), units.Zero(), brf)
} else {
s.Border.Radius.Set(brf, brf, units.Zero(), units.Zero())
}
} else if ip == sw.NumChildren()-1 {
if ps.Direction == styles.Row {
if !s.Is(states.Focused) {
s.Border.Width.SetLeft(units.Zero())
s.MaxBorder.Width = s.Border.Width
}
s.Border.Radius.Set(units.Zero(), brf, brf, units.Zero())
} else {
if !s.Is(states.Focused) {
s.Border.Width.SetTop(units.Zero())
s.MaxBorder.Width = s.Border.Width
}
s.Border.Radius.Set(units.Zero(), units.Zero(), brf, brf)
}
} else {
if !s.Is(states.Focused) {
if ps.Direction == styles.Row {
s.Border.Width.SetLeft(units.Zero())
} else {
s.Border.Width.SetTop(units.Zero())
}
s.MaxBorder.Width = s.Border.Width
}
s.Border.Radius.Zero()
}
})
w.Updater(func() {
w.SetType(sw.Type).SetText(item.getText()).SetTooltip(item.Tooltip)
if sw.Type == SwitchSegmentedButton && sw.Styles.Direction == styles.Column {
// need a blank icon to create a cohesive segmented button
w.SetIconOff(icons.Blank).SetIconIndeterminate(icons.Blank)
}
if !w.StateIs(states.Indeterminate) {
w.SetChecked(slices.Contains(sw.selectedIndexes, i))
}
})
})
}
})
}
// SelectedItem returns the first selected (checked) switch item. It is only
// useful when [Switches.Mutex] is true; if it is not, use [Switches.SelectedItems].
// If no switches are selected, it returns nil.
func (sw *Switches) SelectedItem() *SwitchItem {
if len(sw.selectedIndexes) == 0 {
return nil
}
return &sw.Items[sw.selectedIndexes[0]]
}
// SelectedItems returns all of the currently selected (checked) switch items.
// If [Switches.Mutex] is true, you should use [Switches.SelectedItem] instead.
func (sw *Switches) SelectedItems() []SwitchItem {
res := []SwitchItem{}
for _, i := range sw.selectedIndexes {
res = append(res, sw.Items[i])
}
return res
}
// SelectValue sets the item with the given [SwitchItem.Value]
// to be the only selected item.
func (sw *Switches) SelectValue(value any) error {
for i, item := range sw.Items {
if item.Value == value {
sw.selectedIndexes = []int{i}
return nil
}
}
return fmt.Errorf("Switches.SelectValue: item not found: (value: %v, items: %v)", value, sw.Items)
}
// SetStrings sets the [Switches.Items] from the given strings.
func (sw *Switches) SetStrings(ss ...string) *Switches {
sw.Items = make([]SwitchItem, len(ss))
for i, s := range ss {
sw.Items[i] = SwitchItem{Value: s}
}
return sw
}
// SetEnums sets the [Switches.Items] from the given enums.
func (sw *Switches) SetEnums(es ...enums.Enum) *Switches {
sw.Items = make([]SwitchItem, len(es))
for i, enum := range es {
str := ""
if bf, ok := enum.(enums.BitFlag); ok {
str = bf.BitIndexString()
} else {
str = enum.String()
}
lbl := strcase.ToSentence(str)
desc := enum.Desc()
// If the documentation does not start with the transformed name, but it does
// start with an uppercase letter, then we assume that the first word of the
// documentation is the correct untransformed name. This fixes
// https://github.com/cogentcore/core/issues/774 (also for Chooser).
if !strings.HasPrefix(desc, str) && len(desc) > 0 && unicode.IsUpper(rune(desc[0])) {
str, _, _ = strings.Cut(desc, " ")
}
tip := types.FormatDoc(desc, str, lbl)
sw.Items[i] = SwitchItem{Value: enum, Text: lbl, Tooltip: tip}
}
return sw
}
// SetEnum sets the [Switches.Items] from the [enums.Enum.Values] of the given enum.
func (sw *Switches) SetEnum(enum enums.Enum) *Switches {
return sw.SetEnums(enum.Values()...)
}
// selectFromBitFlag sets which switches are selected based on the given bit flag value.
func (sw *Switches) selectFromBitFlag(bitflag enums.BitFlagSetter) {
values := bitflag.Values()
sw.selectedIndexes = []int{}
for i, value := range values {
if bitflag.HasFlag(value.(enums.BitFlag)) {
sw.selectedIndexes = append(sw.selectedIndexes, i)
}
}
}
// bitFlagFromSelected sets the given bit flag value based on which switches are selected.
func (sw *Switches) bitFlagFromSelected(bitflag enums.BitFlagSetter) {
bitflag.SetInt64(0)
values := bitflag.Values()
for _, i := range sw.selectedIndexes {
bitflag.SetFlag(true, values[i].(enums.BitFlag))
}
}
// 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 core
import (
"fmt"
"log/slog"
"reflect"
"strconv"
"strings"
"cogentcore.org/core/base/labels"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
// todo:
// * search option, both as a search field and as simple type-to-search
// Table represents a slice of structs as a table, where the fields are
// the columns and the elements are the rows. It is a full-featured editor with
// multiple-selection, cut-and-paste, and drag-and-drop.
// Use [ListBase.BindSelect] to make the table designed for item selection.
type Table struct {
ListBase
// TableStyler is an optional styling function for table items.
TableStyler TableStyler `copier:"-" json:"-" xml:"-"`
// SelectedField is the current selection field; initially select value in this field.
SelectedField string `copier:"-" display:"-" json:"-" xml:"-"`
// sortIndex is the current sort index.
sortIndex int
// sortDescending is whether the current sort order is descending.
sortDescending bool
// visibleFields are the visible fields.
visibleFields []reflect.StructField
// numVisibleFields is the number of visible fields.
numVisibleFields int
// headerWidths has the number of characters in each header, per visibleFields.
headerWidths []int
// colMaxWidths records maximum width in chars of string type fields.
colMaxWidths []int
header *Frame
}
// TableStyler is a styling function for custom styling and
// configuration of elements in the table.
type TableStyler func(w Widget, s *styles.Style, row, col int)
func (tb *Table) Init() {
tb.ListBase.Init()
tb.AddContextMenu(tb.contextMenu)
tb.sortIndex = -1
tb.Makers.Normal[0] = func(p *tree.Plan) { // TODO: reduce redundancy with ListBase Maker
svi := tb.This.(Lister)
svi.UpdateSliceSize()
tb.SortSlice()
scrollTo := -1
if tb.SelectedField != "" && tb.SelectedValue != nil {
tb.SelectedIndex, _ = structSliceIndexByValue(tb.Slice, tb.SelectedField, tb.SelectedValue)
tb.SelectedField = ""
tb.SelectedValue = nil
tb.InitSelectedIndex = -1
scrollTo = tb.SelectedIndex
} else if tb.InitSelectedIndex >= 0 {
tb.SelectedIndex = tb.InitSelectedIndex
tb.InitSelectedIndex = -1
scrollTo = tb.SelectedIndex
}
if scrollTo >= 0 {
tb.ScrollToIndex(scrollTo)
}
tb.UpdateStartIndex()
tb.Updater(func() {
tb.UpdateStartIndex()
})
tb.makeHeader(p)
tb.MakeGrid(p, func(p *tree.Plan) {
for i := 0; i < tb.VisibleRows; i++ {
svi.MakeRow(p, i)
}
})
}
}
// StyleValue performs additional value widget styling
func (tb *Table) StyleValue(w Widget, s *styles.Style, row, col int) {
hw := float32(tb.headerWidths[col])
if col == tb.sortIndex {
hw += 6
}
if len(tb.colMaxWidths) > col {
hw = max(float32(tb.colMaxWidths[col]), hw)
}
hv := units.Ch(hw)
s.Min.X.Value = max(s.Min.X.Value, hv.Convert(s.Min.X.Unit, &s.UnitContext).Value)
s.SetTextWrap(false)
}
// SetSlice sets the source slice that we are viewing.
func (tb *Table) SetSlice(sl any) *Table {
if reflectx.IsNil(reflect.ValueOf(sl)) {
tb.Slice = nil
return tb
}
if tb.Slice == sl {
tb.MakeIter = 0
return tb
}
slpTyp := reflect.TypeOf(sl)
if slpTyp.Kind() != reflect.Pointer {
slog.Error("Table requires that you pass a pointer to a slice of struct elements, but type is not a Ptr", "type", slpTyp)
return tb
}
if slpTyp.Elem().Kind() != reflect.Slice {
slog.Error("Table requires that you pass a pointer to a slice of struct elements, but ptr doesn't point to a slice", "type", slpTyp.Elem())
return tb
}
eltyp := reflectx.NonPointerType(reflectx.SliceElementType(sl))
if eltyp.Kind() != reflect.Struct {
slog.Error("Table requires that you pass a slice of struct elements, but type is not a Struct", "type", eltyp.String())
return tb
}
tb.Slice = sl
tb.sliceUnderlying = reflectx.Underlying(reflect.ValueOf(tb.Slice))
tb.elementValue = reflectx.Underlying(reflectx.SliceElementValue(sl))
tb.SetSliceBase()
tb.cacheVisibleFields()
return tb
}
// cacheVisibleFields caches the visible struct fields.
func (tb *Table) cacheVisibleFields() {
tb.visibleFields = make([]reflect.StructField, 0)
shouldShow := func(field reflect.StructField) bool {
tvtag := field.Tag.Get("table")
switch {
case tvtag == "+":
return true
case tvtag == "-":
return false
case tvtag == "-select" && tb.IsReadOnly():
return false
case tvtag == "-edit" && !tb.IsReadOnly():
return false
default:
return field.Tag.Get("display") != "-"
}
}
reflectx.WalkFields(tb.elementValue,
func(parent reflect.Value, field reflect.StructField, value reflect.Value) bool {
return shouldShow(field)
},
func(parent reflect.Value, parentField *reflect.StructField, field reflect.StructField, value reflect.Value) {
if parentField != nil {
field.Index = append(parentField.Index, field.Index...)
}
tb.visibleFields = append(tb.visibleFields, field)
})
tb.numVisibleFields = len(tb.visibleFields)
tb.headerWidths = make([]int, tb.numVisibleFields)
tb.colMaxWidths = make([]int, tb.numVisibleFields)
}
func (tb *Table) UpdateMaxWidths() {
if tb.SliceSize == 0 {
return
}
for fli := 0; fli < tb.numVisibleFields; fli++ {
field := tb.visibleFields[fli]
tb.colMaxWidths[fli] = 0
val := tb.sliceElementValue(0)
fval := val.FieldByIndex(field.Index)
isString := fval.Type().Kind() == reflect.String && fval.Type() != reflect.TypeFor[icons.Icon]()
if !isString {
continue
}
mxw := 0
for rw := 0; rw < tb.SliceSize; rw++ {
val := tb.sliceElementValue(rw)
str := reflectx.ToString(val.FieldByIndex(field.Index).Interface())
mxw = max(mxw, len(str))
}
tb.colMaxWidths[fli] = mxw
}
}
func (tb *Table) makeHeader(p *tree.Plan) {
tree.AddAt(p, "header", func(w *Frame) {
tb.header = w
ToolbarStyles(w)
w.FinalStyler(func(s *styles.Style) {
s.Padding.Zero()
s.Grow.Set(0, 0)
s.Gap.Set(units.Em(0.5)) // matches grid default
})
w.Maker(func(p *tree.Plan) {
if tb.ShowIndexes {
tree.AddAt(p, "_head-index", func(w *Text) {
w.SetType(TextBodyMedium)
w.Styler(func(s *styles.Style) {
s.Align.Self = styles.Center
})
w.SetText("Index")
})
}
for fli := 0; fli < tb.numVisibleFields; fli++ {
field := tb.visibleFields[fli]
tree.AddAt(p, "head-"+field.Name, func(w *Button) {
w.SetType(ButtonAction)
w.Styler(func(s *styles.Style) {
s.Justify.Content = styles.Start
})
w.OnClick(func(e events.Event) {
tb.SortColumn(fli)
})
w.Updater(func() {
htxt := ""
if lbl, ok := field.Tag.Lookup("label"); ok {
htxt = lbl
} else {
htxt = strcase.ToSentence(field.Name)
}
w.SetText(htxt)
w.Tooltip = htxt + " (click to sort by)"
doc, ok := types.GetDoc(reflect.Value{}, tb.elementValue, field, htxt)
if ok && doc != "" {
w.Tooltip += ": " + doc
}
tb.headerWidths[fli] = len(htxt)
if fli == tb.sortIndex {
if tb.sortDescending {
w.SetIndicator(icons.KeyboardArrowDown)
} else {
w.SetIndicator(icons.KeyboardArrowUp)
}
} else {
w.SetIndicator(icons.Blank)
}
})
})
}
})
})
}
// RowWidgetNs returns number of widgets per row and offset for index label
func (tb *Table) RowWidgetNs() (nWidgPerRow, idxOff int) {
nWidgPerRow = 1 + tb.numVisibleFields
idxOff = 1
if !tb.ShowIndexes {
nWidgPerRow -= 1
idxOff = 0
}
return
}
func (tb *Table) MakeRow(p *tree.Plan, i int) {
svi := tb.This.(Lister)
si, _, invis := svi.SliceIndex(i)
itxt := strconv.Itoa(i)
val := tb.sliceElementValue(si)
// stru := val.Interface()
if tb.ShowIndexes {
tb.MakeGridIndex(p, i, si, itxt, invis)
}
for fli := 0; fli < tb.numVisibleFields; fli++ {
field := tb.visibleFields[fli]
uvp := reflectx.UnderlyingPointer(val.FieldByIndex(field.Index))
uv := uvp.Elem()
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"`)
if tags == "" {
tags += " " + ni
} else {
tags = ni
}
}
readOnlyTag := tags.Get("edit") == "-"
tree.AddNew(p, valnm, func() Value {
return NewValue(uvp.Interface(), tags)
}, func(w Value) {
wb := w.AsWidget()
tb.MakeValue(w, i)
w.AsTree().SetProperty(ListColProperty, fli)
if !tb.IsReadOnly() && !readOnlyTag {
wb.OnChange(func(e events.Event) {
tb.This.(Lister).UpdateMaxWidths()
tb.SendChange()
})
}
wb.Updater(func() {
si, vi, invis := svi.SliceIndex(i)
val := tb.sliceElementValue(vi)
upv := reflectx.UnderlyingPointer(val.FieldByIndex(field.Index))
Bind(upv.Interface(), w)
vc := tb.ValueTitle + "[" + strconv.Itoa(si) + "]"
if !invis {
if lblr, ok := tb.Slice.(labels.SliceLabeler); ok {
slbl := lblr.ElemLabel(si)
if slbl != "" {
vc = joinValueTitle(tb.ValueTitle, slbl)
}
}
}
wb.ValueTitle = vc + " (" + wb.ValueTitle + ")"
wb.SetReadOnly(tb.IsReadOnly() || readOnlyTag)
wb.SetState(invis, states.Invisible)
if svi.HasStyler() {
w.Style()
}
if invis {
wb.SetSelected(false)
}
})
})
}
}
func (tb *Table) HasStyler() bool {
return tb.TableStyler != nil
}
func (tb *Table) StyleRow(w Widget, idx, fidx int) {
if tb.TableStyler != nil {
tb.TableStyler(w, &w.AsWidget().Styles, idx, fidx)
}
}
// NewAt inserts a new blank element at the given index in the slice.
// -1 indicates to insert the element at the end.
func (tb *Table) NewAt(idx int) {
tb.NewAtSelect(idx)
reflectx.SliceNewAt(tb.Slice, idx)
if idx < 0 {
idx = tb.SliceSize
}
tb.This.(Lister).UpdateSliceSize()
tb.SelectIndexEvent(idx, events.SelectOne)
tb.UpdateChange()
tb.IndexGrabFocus(idx)
}
// DeleteAt deletes the element at the given index from the slice.
func (tb *Table) DeleteAt(idx int) {
if idx < 0 || idx >= tb.SliceSize {
return
}
tb.DeleteAtSelect(idx)
reflectx.SliceDeleteAt(tb.Slice, idx)
tb.This.(Lister).UpdateSliceSize()
tb.UpdateChange()
}
// SortSlice sorts the slice according to current settings.
func (tb *Table) SortSlice() {
if tb.sortIndex < 0 || tb.sortIndex >= len(tb.visibleFields) {
return
}
rawIndex := tb.visibleFields[tb.sortIndex].Index
reflectx.StructSliceSort(tb.Slice, rawIndex, !tb.sortDescending)
}
// SortColumn sorts the slice for the given field index.
// It toggles between ascending and descending if already
// sorting on this field.
func (tb *Table) SortColumn(fieldIndex int) {
sgh := tb.header
_, idxOff := tb.RowWidgetNs()
for fli := 0; fli < tb.numVisibleFields; fli++ {
hdr := sgh.Child(idxOff + fli).(*Button)
hdr.SetType(ButtonAction)
if fli == fieldIndex {
if tb.sortIndex == fli {
tb.sortDescending = !tb.sortDescending
} else {
tb.sortDescending = false
}
}
}
tb.sortIndex = fieldIndex
tb.SortSlice()
tb.Update()
}
// sortFieldName returns the name of the field being sorted, along with :up or
// :down depending on ascending or descending sorting.
func (tb *Table) sortFieldName() string {
if tb.sortIndex >= 0 && tb.sortIndex < tb.numVisibleFields {
nm := tb.visibleFields[tb.sortIndex].Name
if tb.sortDescending {
nm += ":down"
} else {
nm += ":up"
}
return nm
}
return ""
}
// setSortFieldName sets sorting to happen on given field and direction
// see [Table.sortFieldName] for details.
func (tb *Table) setSortFieldName(nm string) {
if nm == "" {
return
}
spnm := strings.Split(nm, ":")
got := false
for fli := 0; fli < tb.numVisibleFields; fli++ {
fld := tb.visibleFields[fli]
if fld.Name == spnm[0] {
got = true
// fmt.Println("sorting on:", fld.Name, fli, "from:", nm)
tb.sortIndex = fli
}
}
if len(spnm) == 2 {
if spnm[1] == "down" {
tb.sortDescending = true
} else {
tb.sortDescending = false
}
}
if got {
tb.SortSlice()
}
}
// RowGrabFocus grabs the focus for the first focusable widget in given row;
// returns that element or nil if not successful. Note: grid must have
// already rendered for focus to be grabbed!
func (tb *Table) RowGrabFocus(row int) *WidgetBase {
if !tb.IsRowInBounds(row) || tb.InFocusGrab { // range check
return nil
}
nWidgPerRow, idxOff := tb.RowWidgetNs()
ridx := nWidgPerRow * row
lg := tb.ListGrid
// first check if we already have focus
for fli := 0; fli < tb.numVisibleFields; fli++ {
w := lg.Child(ridx + idxOff + fli).(Widget).AsWidget()
if w.StateIs(states.Focused) || w.ContainsFocus() {
return w
}
}
tb.InFocusGrab = true
defer func() { tb.InFocusGrab = false }()
for fli := 0; fli < tb.numVisibleFields; fli++ {
w := lg.Child(ridx + idxOff + fli).(Widget).AsWidget()
if w.CanFocus() {
w.SetFocus()
return w
}
}
return nil
}
// selectFieldValue sets SelectedField and SelectedValue and attempts to find
// corresponding row, setting SelectedIndex and selecting row if found; returns
// true if found, false otherwise.
func (tb *Table) selectFieldValue(fld, val string) bool {
tb.SelectedField = fld
tb.SelectedValue = val
if tb.SelectedField != "" && tb.SelectedValue != nil {
idx, _ := structSliceIndexByValue(tb.Slice, tb.SelectedField, tb.SelectedValue)
if idx >= 0 {
tb.ScrollToIndex(idx)
tb.updateSelectIndex(idx, true, events.SelectOne)
return true
}
}
return false
}
// structSliceIndexByValue searches for first index that contains given value in field of
// given name.
func structSliceIndexByValue(structSlice any, fieldName string, fieldValue any) (int, error) {
svnp := reflectx.NonPointerValue(reflect.ValueOf(structSlice))
sz := svnp.Len()
struTyp := reflectx.NonPointerType(reflect.TypeOf(structSlice).Elem().Elem())
fld, ok := struTyp.FieldByName(fieldName)
if !ok {
err := fmt.Errorf("StructSliceRowByValue: field name: %v not found", fieldName)
slog.Error(err.Error())
return -1, err
}
fldIndex := fld.Index
for idx := 0; idx < sz; idx++ {
rval := reflectx.UnderlyingPointer(svnp.Index(idx))
fval := rval.Elem().FieldByIndex(fldIndex)
if !fval.IsValid() {
continue
}
if fval.Interface() == fieldValue {
return idx, nil
}
}
return -1, nil
}
func (tb *Table) editIndex(idx int) {
if idx < 0 || idx >= tb.sliceUnderlying.Len() {
return
}
val := reflectx.UnderlyingPointer(tb.sliceUnderlying.Index(idx))
stru := val.Interface()
tynm := reflectx.NonPointerType(val.Type()).Name()
lbl := labels.ToLabel(stru)
if lbl != "" {
tynm += ": " + lbl
}
d := NewBody(tynm)
NewForm(d).SetStruct(stru).SetReadOnly(tb.IsReadOnly())
d.AddBottomBar(func(bar *Frame) {
d.AddCancel(bar)
d.AddOK(bar)
})
d.RunFullDialog(tb)
}
func (tb *Table) contextMenu(m *Scene) {
e := NewButton(m)
if tb.IsReadOnly() {
e.SetText("View").SetIcon(icons.Visibility)
} else {
e.SetText("Edit").SetIcon(icons.Edit)
}
e.OnClick(func(e events.Event) {
tb.editIndex(tb.SelectedIndex)
})
}
// Header layout:
func (tb *Table) SizeFinal() {
tb.ListBase.SizeFinal()
sg := tb.ListGrid
if sg == nil {
return
}
sh := tb.header
sh.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
sgb := AsWidget(sg.Child(i))
gsz := &sgb.Geom.Size
ksz := &cwb.Geom.Size
ksz.Actual.Total.X = gsz.Actual.Total.X
ksz.Actual.Content.X = gsz.Actual.Content.X
ksz.Alloc.Total.X = gsz.Alloc.Total.X
ksz.Alloc.Content.X = gsz.Alloc.Content.X
return tree.Continue
})
gsz := &sg.Geom.Size
ksz := &sh.Geom.Size
ksz.Actual.Total.X = gsz.Actual.Total.X
ksz.Actual.Content.X = gsz.Actual.Content.X
ksz.Alloc.Total.X = gsz.Alloc.Total.X
ksz.Alloc.Content.X = gsz.Alloc.Content.X
}
// 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 core
import (
"log/slog"
"sync"
"cogentcore.org/core/base/elide"
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
)
// Tabs divide widgets into logical groups and give users the ability
// to freely navigate between them using tab buttons.
type Tabs struct {
Frame
// Type is the styling type of the tabs. If it is changed after
// the tabs are first configured, Update needs to be called on
// the tabs.
Type TabTypes
// NewTabButton is whether to show a new tab button at the end of the list of tabs.
NewTabButton bool
// maxChars is the maximum number of characters to include in the tab text.
// It elides text that are longer than that.
maxChars int
// CloseIcon is the icon used for tab close buttons.
// If it is "" or [icons.None], the tab is not closeable.
// The default value is [icons.Close].
// Only [FunctionalTabs] can be closed; all other types of
// tabs will not render a close button and can not be closed.
CloseIcon icons.Icon
// mu is a mutex protecting updates to tabs. Tabs can be driven
// programmatically and via user input so need extra protection.
mu sync.Mutex
tabs, frame *Frame
}
// TabTypes are the different styling types of tabs.
type TabTypes int32 //enums:enum
const (
// StandardTabs indicates to render the standard type
// of Material Design style tabs.
StandardTabs TabTypes = iota
// FunctionalTabs indicates to render functional tabs
// like those in Google Chrome. These tabs take up less
// space and are the only kind that can be closed.
// They will also support being moved at some point.
FunctionalTabs
// NavigationAuto indicates to render the tabs as either
// [NavigationBar] or [NavigationDrawer] if
// [WidgetBase.SizeClass] is [SizeCompact] or not, respectively.
// NavigationAuto should typically be used instead of one of the
// specific navigation types for better cross-platform compatability.
NavigationAuto
// NavigationBar indicates to render the tabs as a
// bottom navigation bar with text and icons.
NavigationBar
// NavigationDrawer indicates to render the tabs as a
// side navigation drawer with text and icons.
NavigationDrawer
)
// effective returns the effective tab type in the context
// of the given widget, handling [NavigationAuto] based on
// [WidgetBase.SizeClass].
func (tt TabTypes) effective(w Widget) TabTypes {
if tt != NavigationAuto {
return tt
}
switch w.AsWidget().SizeClass() {
case SizeCompact:
return NavigationBar
default:
return NavigationDrawer
}
}
// isColumn returns whether the tabs should be arranged in a column.
func (tt TabTypes) isColumn() bool {
return tt == NavigationDrawer
}
func (ts *Tabs) Init() {
ts.Frame.Init()
ts.maxChars = 16
ts.CloseIcon = icons.Close
ts.Styler(func(s *styles.Style) {
s.Color = colors.Scheme.OnBackground
s.Grow.Set(1, 1)
if ts.Type.effective(ts).isColumn() {
s.Direction = styles.Row
} else {
s.Direction = styles.Column
}
})
ts.Maker(func(p *tree.Plan) {
tree.AddAt(p, "tabs", func(w *Frame) {
ts.tabs = w
w.Styler(func(s *styles.Style) {
s.Overflow.Set(styles.OverflowHidden) // no scrollbars!
s.Gap.Set(units.Dp(4))
if ts.Type.effective(ts).isColumn() {
s.Direction = styles.Column
s.Grow.Set(0, 1)
} else {
s.Direction = styles.Row
s.Grow.Set(1, 0)
s.Wrap = true
}
})
w.Updater(func() {
if !ts.NewTabButton {
w.DeleteChildByName("new-tab-button")
return
}
if w.ChildByName("new-tab-button") != nil {
return
}
ntb := NewButton(w).SetType(ButtonAction).SetIcon(icons.Add)
ntb.SetTooltip("Add a new tab").SetName("new-tab-button")
ntb.OnClick(func(e events.Event) {
ts.NewTab("New tab")
ts.SelectTabIndex(ts.NumTabs() - 1)
})
})
})
tree.AddAt(p, "frame", func(w *Frame) {
ts.frame = w
w.LayoutStackTopOnly = true // key for allowing each tab to have its own size
w.Styler(func(s *styles.Style) {
s.Display = styles.Stacked
s.Min.Set(units.Dp(160), units.Dp(96))
s.Grow.Set(1, 1)
})
w.SetOnChildAdded(func(n tree.Node) {
AsWidget(n).Styler(func(s *styles.Style) {
// tab frames must scroll independently and grow
s.Overflow.Set(styles.OverflowAuto)
s.Grow.Set(1, 1)
})
})
})
// frame comes before tabs in bottom navigation bar
if ts.Type.effective(ts) == NavigationBar {
p.Children[0], p.Children[1] = p.Children[1], p.Children[0]
}
})
}
// NumTabs returns the number of tabs.
func (ts *Tabs) NumTabs() int {
fr := ts.getFrame()
if fr == nil {
return 0
}
return len(fr.Children)
}
// CurrentTab returns currently selected tab and its index; returns nil if none.
func (ts *Tabs) CurrentTab() (Widget, int) {
if ts.NumTabs() == 0 {
return nil, -1
}
ts.mu.Lock()
defer ts.mu.Unlock()
fr := ts.getFrame()
if fr.StackTop < 0 {
return nil, -1
}
w := fr.Child(fr.StackTop).(Widget)
return w, fr.StackTop
}
// NewTab adds a new tab with the given label and returns the resulting tab frame
// and associated tab button, which can be further customized as needed.
// It is the main end-user API for creating new tabs.
func (ts *Tabs) NewTab(label string) (*Frame, *Tab) {
fr := ts.getFrame()
idx := len(fr.Children)
return ts.insertNewTab(label, idx)
}
// insertNewTab inserts a new tab with the given label at the given index position
// within the list of tabs and returns the resulting tab frame and button.
func (ts *Tabs) insertNewTab(label string, idx int) (*Frame, *Tab) {
tfr := ts.getFrame()
alreadyExists := tfr.ChildByName(label) != nil
frame := NewFrame()
tfr.InsertChild(frame, idx)
frame.SetName(label)
frame.Styler(func(s *styles.Style) {
s.Direction = styles.Column
})
button := ts.insertTabButtonAt(label, idx)
if alreadyExists {
tree.SetUniqueName(frame) // prevent duplicate names
button.SetName(frame.Name) // must be the same name
}
ts.Update()
return frame, button
}
// insertTabButtonAt inserts just the tab button at given index, after the panel has
// already been added to the frame; assumed to be wrapped in update. Generally
// for internal use only.
func (ts *Tabs) insertTabButtonAt(label string, idx int) *Tab {
tb := ts.getTabs()
tab := tree.New[Tab]()
tb.InsertChild(tab, idx)
tab.SetName(label)
tab.SetText(label).SetType(ts.Type).SetCloseIcon(ts.CloseIcon).SetTooltip(label)
tab.maxChars = ts.maxChars
tab.OnClick(func(e events.Event) {
ts.SelectTabByName(tab.Name)
})
fr := ts.getFrame()
if len(fr.Children) == 1 {
fr.StackTop = 0
tab.SetSelected(true)
// } else {
// frame.SetState(true, states.Invisible) // new tab is invisible until selected
}
return tab
}
// tabAtIndex returns content frame and tab button at given index, nil if
// index out of range (emits log message).
func (ts *Tabs) tabAtIndex(idx int) (*Frame, *Tab) {
ts.mu.Lock()
defer ts.mu.Unlock()
fr := ts.getFrame()
tb := ts.getTabs()
sz := len(fr.Children)
if idx < 0 || idx >= sz {
slog.Error("Tabs: index out of range for number of tabs", "index", idx, "numTabs", sz)
return nil, nil
}
tab := tb.Child(idx).(*Tab)
frame := fr.Child(idx).(*Frame)
return frame, tab
}
// SelectTabIndex selects the tab at the given index, returning it or nil.
// This is the final tab selection path.
func (ts *Tabs) SelectTabIndex(idx int) *Frame {
frame, tab := ts.tabAtIndex(idx)
if frame == nil {
return nil
}
fr := ts.getFrame()
if fr.StackTop == idx {
return frame
}
ts.mu.Lock()
ts.unselectOtherTabs(idx)
tab.SetSelected(true)
fr.StackTop = idx
fr.Update()
frame.DeferShown()
ts.mu.Unlock()
return frame
}
// TabByName returns the tab [Frame] with the given widget name
// (nil if not found). The widget name is the original full tab label,
// prior to any eliding.
func (ts *Tabs) TabByName(name string) *Frame {
ts.mu.Lock()
defer ts.mu.Unlock()
fr := ts.getFrame()
frame, _ := fr.ChildByName(name).(*Frame)
return frame
}
// tabIndexByName returns the tab index for the given tab widget name
// and -1 if it can not be found.
// The widget name is the original full tab label, prior to any eliding.
func (ts *Tabs) tabIndexByName(name string) int {
ts.mu.Lock()
defer ts.mu.Unlock()
tb := ts.getTabs()
tab := tb.ChildByName(name)
if tab == nil {
return -1
}
return tab.AsTree().IndexInParent()
}
// SelectTabByName selects the tab by widget name, returning it.
// The widget name is the original full tab label, prior to any eliding.
func (ts *Tabs) SelectTabByName(name string) *Frame {
idx := ts.tabIndexByName(name)
if idx < 0 {
return nil
}
ts.SelectTabIndex(idx)
fr := ts.getFrame()
return fr.Child(idx).(*Frame)
}
// RecycleTab returns a tab with the given name, first by looking for an existing one,
// and if not found, making a new one. It returns the frame for the tab.
func (ts *Tabs) RecycleTab(name string) *Frame {
frame := ts.TabByName(name)
if frame == nil {
frame, _ = ts.NewTab(name)
}
ts.SelectTabByName(name)
return frame
}
// RecycleTabWidget returns a tab with the given widget type in the tab frame,
// first by looking for an existing one with the given name, and if not found,
// making and configuring a new one. It returns the resulting widget.
func RecycleTabWidget[T tree.NodeValue](ts *Tabs, name string) *T {
fr := ts.RecycleTab(name)
if fr.HasChildren() {
return any(fr.Child(0)).(*T)
}
w := tree.New[T](fr)
any(w).(Widget).AsWidget().UpdateWidget()
return w
}
// deleteTabIndex deletes the tab at the given index, returning whether it was successful.
func (ts *Tabs) deleteTabIndex(idx int) bool {
frame, _ := ts.tabAtIndex(idx)
if frame == nil {
return false
}
ts.mu.Lock()
fr := ts.getFrame()
sz := len(fr.Children)
tb := ts.getTabs()
nidx := -1
if fr.StackTop == idx {
if idx > 0 {
nidx = idx - 1
} else if idx < sz-1 {
nidx = idx
}
}
// if we didn't delete the current tab and have at least one
// other tab, we go to the next tab over
if nidx < 0 && ts.NumTabs() > 1 {
nidx = max(idx-1, 0)
}
fr.DeleteChildAt(idx)
tb.DeleteChildAt(idx)
ts.mu.Unlock()
if nidx >= 0 {
ts.SelectTabIndex(nidx)
}
ts.NeedsLayout()
return true
}
// getTabs returns the [Frame] containing the tabs (the first element within us).
// It configures the [Tabs] if necessary.
func (ts *Tabs) getTabs() *Frame {
if ts.tabs == nil {
ts.UpdateWidget()
}
return ts.tabs
}
// Frame returns the stacked [Frame] (the second element within us).
// It configures the Tabs if necessary.
func (ts *Tabs) getFrame() *Frame {
if ts.frame == nil {
ts.UpdateWidget()
}
return ts.frame
}
// unselectOtherTabs turns off all the tabs except given one
func (ts *Tabs) unselectOtherTabs(idx int) {
sz := ts.NumTabs()
tbs := ts.getTabs()
for i := 0; i < sz; i++ {
if i == idx {
continue
}
tb := tbs.Child(i).(*Tab)
if tb.StateIs(states.Selected) {
tb.SetSelected(false)
}
}
}
// Tab is a tab button that contains one or more of a label, an icon,
// and a close icon. Tabs should be made using the [Tabs.NewTab] function.
type Tab struct { //core:no-new
Frame
// Type is the styling type of the tab. This property
// must be set on the parent [Tabs] for it to work correctly.
Type TabTypes
// Text is the text for the tab. If it is blank, no text is shown.
// Text is never shown for [NavigationRail] tabs.
Text string
// Icon is the icon for the tab.
// If it is "" or [icons.None], no icon is shown.
Icon icons.Icon
// CloseIcon is the icon used as a close button for the tab.
// If it is "" or [icons.None], the tab is not closeable.
// The default value is [icons.Close].
// Only [FunctionalTabs] can be closed; all other types of
// tabs will not render a close button and can not be closed.
CloseIcon icons.Icon
// TODO(kai): replace this with general text overflow property (#778)
// maxChars is the maximum number of characters to include in tab text.
// It elides text that is longer than that.
maxChars int
}
func (tb *Tab) Init() {
tb.Frame.Init()
tb.maxChars = 16
tb.CloseIcon = icons.Close
tb.Styler(func(s *styles.Style) {
s.SetAbilities(true, abilities.Activatable, abilities.Focusable, abilities.Hoverable)
if !tb.IsReadOnly() {
s.Cursor = cursors.Pointer
}
if tb.Type.effective(tb).isColumn() {
s.Grow.X = 1
s.Border.Radius = styles.BorderRadiusFull
s.Padding.Set(units.Dp(16))
} else {
s.Border.Radius = styles.BorderRadiusSmall
s.Padding.Set(units.Dp(10))
}
s.Gap.Zero()
s.Align.Content = styles.Center
s.Align.Items = styles.Center
if tb.StateIs(states.Selected) {
s.Color = colors.Scheme.Select.OnContainer
} else {
s.Color = colors.Scheme.OnSurfaceVariant
if tb.Type.effective(tb) == FunctionalTabs {
s.Background = colors.Scheme.SurfaceContainer
}
}
})
tb.HandleClickOnEnterSpace()
tb.Maker(func(p *tree.Plan) {
if tb.maxChars > 0 { // TODO: find a better time to do this?
tb.Text = elide.Middle(tb.Text, tb.maxChars)
}
if tb.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(tb.Icon)
})
})
if tb.Text != "" {
tree.AddAt(p, "space", func(w *Space) {})
}
}
if tb.Text != "" {
tree.AddAt(p, "text", func(w *Text) {
w.Styler(func(s *styles.Style) {
s.SetNonSelectable()
s.SetTextWrap(false)
})
w.Updater(func() {
if tb.Type.effective(tb) == FunctionalTabs {
w.SetType(TextBodyMedium)
} else {
w.SetType(TextLabelLarge)
}
w.SetText(tb.Text)
})
})
}
if tb.Type.effective(tb) == FunctionalTabs && tb.CloseIcon.IsSet() {
tree.AddAt(p, "close-space", func(w *Space) {})
tree.AddAt(p, "close", func(w *Button) {
w.SetType(ButtonAction)
w.Styler(func(s *styles.Style) {
s.Padding.Zero()
s.Border.Radius = styles.BorderRadiusFull
})
w.OnClick(func(e events.Event) {
ts := tb.tabs()
idx := ts.tabIndexByName(tb.Name)
// if OnlyCloseActiveTab is on, only process delete when already selected
if SystemSettings.OnlyCloseActiveTab && !tb.StateIs(states.Selected) {
ts.SelectTabIndex(idx)
} else {
ts.deleteTabIndex(idx)
}
})
w.Updater(func() {
w.SetIcon(tb.CloseIcon)
})
})
}
})
}
// tabs returns the parent [Tabs] of this [Tab].
func (tb *Tab) tabs() *Tabs {
return tb.Parent.AsTree().Parent.(*Tabs)
}
// Copyright 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 (
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/events"
"cogentcore.org/core/system"
)
// AssertRender makes a new window from the body, waits until it is shown
// and all events have been handled, does any necessary re-rendering,
// asserts that its rendered image is the same as that stored at the given
// filename, saving the image to that filename if it does not already exist,
// and then closes the window. It does not return until all of those steps
// are completed. Each (optional) function passed is called after the
// window is shown, and all system events are handled before proessing continues.
// A testdata directory and png file extension are automatically added to
// the the filename, and forward slashes are automatically replaced with
// backslashes on Windows. See [Body.AssertRenderScreen] for a version
// that asserts the rendered image of the entire screen, not just this body.
func (b *Body) AssertRender(t imagex.TestingT, filename string, fun ...func()) {
b.runAndShowNewWindow()
for i := 0; i < len(fun); i++ {
fun[i]()
b.waitNoEvents()
}
if len(fun) == 0 {
// we didn't get it above
b.waitNoEvents()
}
b.Scene.assertPixels(t, filename)
b.Close()
}
// AssertRenderScreen is the same as [Body.AssertRender] except that it asserts the
// rendered image of the entire screen, not just this body. It should be used for
// multi-scene tests like those of snackbars and dialogs.
func (b *Body) AssertRenderScreen(t imagex.TestingT, filename string, fun ...func()) {
b.runAndShowNewWindow()
for i := 0; i < len(fun); i++ {
fun[i]()
b.waitNoEvents()
}
if len(fun) == 0 {
// we didn't get it above
b.waitNoEvents()
}
system.AssertCapture(t, filename)
b.Close()
}
// runAndShowNewWindow runs a new window and waits for it to be shown.
func (b *Body) runAndShowNewWindow() {
showed := make(chan struct{})
b.OnFinal(events.Show, func(e events.Event) {
showed <- struct{}{}
})
b.RunWindow()
<-showed
}
// waitNoEvents waits for all events to be handled and does any rendering
// of the body necessary.
func (b *Body) waitNoEvents() {
rw := b.Scene.RenderWindow()
rw.noEventsChan = make(chan struct{})
<-rw.noEventsChan
rw.noEventsChan = nil
b.AsyncLock()
b.doNeedsRender()
b.AsyncUnlock()
}
// assertPixels asserts that [Scene.Pixels] is equivalent
// to the image stored at the given filename in the testdata directory,
// with ".png" added to the filename if there is no extension
// (eg: "button" becomes "testdata/button.png"). Forward slashes are
// automatically replaced with backslashes on Windows.
// If it is not, it fails the test with an error, but continues its
// execution. If there is no image at the given filename in the testdata
// directory, it creates the image.
func (sc *Scene) assertPixels(t imagex.TestingT, filename string) {
imagex.Assert(t, sc.Pixels, filename)
}
// 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 core
import (
"fmt"
"image"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"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/system"
)
// Text is a widget for rendering text. It supports full HTML styling,
// including links. By default, text wraps and collapses whitespace, although
// you can change this by changing [styles.Text.WhiteSpace].
type Text struct {
WidgetBase
// Text is the text to display.
Text string
// Type is the styling type of text to use.
// It defaults to [TextBodyLarge].
Type TextTypes
// paintText is the [paint.Text] for the text.
paintText paint.Text
// normalCursor is the cached cursor to display when there
// is no link being hovered.
normalCursor cursors.Cursor
}
// TextTypes is an enum containing the different
// possible styling types of [Text] widgets.
type TextTypes int32 //enums:enum -trim-prefix Text
const (
// TextDisplayLarge is large, short, and important
// display text with a default font size of 57dp.
TextDisplayLarge TextTypes = iota
// TextDisplayMedium is medium-sized, short, and important
// display text with a default font size of 45dp.
TextDisplayMedium
// TextDisplaySmall is small, short, and important
// display text with a default font size of 36dp.
TextDisplaySmall
// TextHeadlineLarge is large, high-emphasis
// headline text with a default font size of 32dp.
TextHeadlineLarge
// TextHeadlineMedium is medium-sized, high-emphasis
// headline text with a default font size of 28dp.
TextHeadlineMedium
// TextHeadlineSmall is small, high-emphasis
// headline text with a default font size of 24dp.
TextHeadlineSmall
// TextTitleLarge is large, medium-emphasis
// title text with a default font size of 22dp.
TextTitleLarge
// TextTitleMedium is medium-sized, medium-emphasis
// title text with a default font size of 16dp.
TextTitleMedium
// TextTitleSmall is small, medium-emphasis
// title text with a default font size of 14dp.
TextTitleSmall
// TextBodyLarge is large body text used for longer
// passages of text with a default font size of 16dp.
TextBodyLarge
// TextBodyMedium is medium-sized body text used for longer
// passages of text with a default font size of 14dp.
TextBodyMedium
// TextBodySmall is small body text used for longer
// passages of text with a default font size of 12dp.
TextBodySmall
// TextLabelLarge is large text used for label text (like a caption
// or the text inside a button) with a default font size of 14dp.
TextLabelLarge
// TextLabelMedium is medium-sized text used for label text (like a caption
// or the text inside a button) with a default font size of 12dp.
TextLabelMedium
// TextLabelSmall is small text used for label text (like a caption
// or the text inside a button) with a default font size of 11dp.
TextLabelSmall
// TextSupporting is medium-sized supporting text typically used for
// secondary dialog information below the title. It has a default font
// size of 14dp and color of [colors.Scheme.OnSurfaceVariant].
TextSupporting
)
func (tx *Text) WidgetValue() any { return &tx.Text }
func (tx *Text) Init() {
tx.WidgetBase.Init()
tx.SetType(TextBodyLarge)
tx.Styler(func(s *styles.Style) {
s.SetAbilities(true, abilities.Selectable, abilities.DoubleClickable)
if len(tx.paintText.Links) > 0 {
s.SetAbilities(true, abilities.Clickable, abilities.LongHoverable, abilities.LongPressable)
}
if !tx.IsReadOnly() {
s.Cursor = cursors.Text
}
s.GrowWrap = true
// Text styles based on https://m3.material.io/styles/typography/type-scale-tokens
// We use Em for line height so that it scales properly with font size changes.
switch tx.Type {
case TextLabelLarge:
s.Text.LineHeight.Em(20.0 / 14)
s.Font.Size.Dp(14)
s.Text.LetterSpacing.Dp(0.1)
s.Font.Weight = styles.WeightMedium
case TextLabelMedium:
s.Text.LineHeight.Em(16.0 / 12)
s.Font.Size.Dp(12)
s.Text.LetterSpacing.Dp(0.5)
s.Font.Weight = styles.WeightMedium
case TextLabelSmall:
s.Text.LineHeight.Em(16.0 / 11)
s.Font.Size.Dp(11)
s.Text.LetterSpacing.Dp(0.5)
s.Font.Weight = styles.WeightMedium
case TextBodyLarge:
s.Text.LineHeight.Em(24.0 / 16)
s.Font.Size.Dp(16)
s.Text.LetterSpacing.Dp(0.5)
s.Font.Weight = styles.WeightNormal
case TextSupporting:
s.Color = colors.Scheme.OnSurfaceVariant
fallthrough
case TextBodyMedium:
s.Text.LineHeight.Em(20.0 / 14)
s.Font.Size.Dp(14)
s.Text.LetterSpacing.Dp(0.25)
s.Font.Weight = styles.WeightNormal
case TextBodySmall:
s.Text.LineHeight.Em(16.0 / 12)
s.Font.Size.Dp(12)
s.Text.LetterSpacing.Dp(0.4)
s.Font.Weight = styles.WeightNormal
case TextTitleLarge:
s.Text.LineHeight.Em(28.0 / 22)
s.Font.Size.Dp(22)
s.Text.LetterSpacing.Zero()
s.Font.Weight = styles.WeightNormal
case TextTitleMedium:
s.Text.LineHeight.Em(24.0 / 16)
s.Font.Size.Dp(16)
s.Text.LetterSpacing.Dp(0.15)
s.Font.Weight = styles.WeightMedium
case TextTitleSmall:
s.Text.LineHeight.Em(20.0 / 14)
s.Font.Size.Dp(14)
s.Text.LetterSpacing.Dp(0.1)
s.Font.Weight = styles.WeightMedium
case TextHeadlineLarge:
s.Text.LineHeight.Em(40.0 / 32)
s.Font.Size.Dp(32)
s.Text.LetterSpacing.Zero()
s.Font.Weight = styles.WeightNormal
case TextHeadlineMedium:
s.Text.LineHeight.Em(36.0 / 28)
s.Font.Size.Dp(28)
s.Text.LetterSpacing.Zero()
s.Font.Weight = styles.WeightNormal
case TextHeadlineSmall:
s.Text.LineHeight.Em(32.0 / 24)
s.Font.Size.Dp(24)
s.Text.LetterSpacing.Zero()
s.Font.Weight = styles.WeightNormal
case TextDisplayLarge:
s.Text.LineHeight.Em(64.0 / 57)
s.Font.Size.Dp(57)
s.Text.LetterSpacing.Dp(-0.25)
s.Font.Weight = styles.WeightNormal
case TextDisplayMedium:
s.Text.LineHeight.Em(52.0 / 45)
s.Font.Size.Dp(45)
s.Text.LetterSpacing.Zero()
s.Font.Weight = styles.WeightNormal
case TextDisplaySmall:
s.Text.LineHeight.Em(44.0 / 36)
s.Font.Size.Dp(36)
s.Text.LetterSpacing.Zero()
s.Font.Weight = styles.WeightNormal
}
})
tx.FinalStyler(func(s *styles.Style) {
tx.normalCursor = s.Cursor
tx.paintText.UpdateColors(s.FontRender())
})
tx.HandleTextClick(func(tl *paint.TextLink) {
system.TheApp.OpenURL(tl.URL)
})
tx.OnDoubleClick(func(e events.Event) {
tx.SetSelected(true)
tx.SetFocusQuiet()
})
tx.OnFocusLost(func(e events.Event) {
tx.SetSelected(false)
})
tx.OnKeyChord(func(e events.Event) {
if !tx.StateIs(states.Selected) {
return
}
kf := keymap.Of(e.KeyChord())
if kf == keymap.Copy {
e.SetHandled()
tx.copy()
}
})
tx.On(events.MouseMove, func(e events.Event) {
tl, _ := tx.findLink(e.Pos())
if tl != nil {
tx.Styles.Cursor = cursors.Pointer
} else {
tx.Styles.Cursor = tx.normalCursor
}
})
// todo: ideally it would be possible to only call SetHTML once during config
// and then do the layout only during sizing. However, layout starts with
// existing line breaks (which could come from <br> and <p> in HTML),
// so that is never able to undo initial word wrapping from constrained sizes.
tx.Updater(func() {
tx.configTextSize(tx.Geom.Size.Actual.Content)
})
}
// findLink finds the text link at the given scene-local position. If it
// finds it, it returns it and its bounds; otherwise, it returns nil.
func (tx *Text) findLink(pos image.Point) (*paint.TextLink, image.Rectangle) {
for _, tl := range tx.paintText.Links {
// TODO(kai/link): is there a better way to be safe here?
if tl.Label == "" {
continue
}
tlb := tl.Bounds(&tx.paintText, tx.Geom.Pos.Content)
if pos.In(tlb) {
return &tl, tlb
}
}
return nil, image.Rectangle{}
}
// HandleTextClick handles click events such that the given function will be called
// on any links that are clicked on.
func (tx *Text) HandleTextClick(openLink func(tl *paint.TextLink)) {
tx.OnClick(func(e events.Event) {
tl, _ := tx.findLink(e.Pos())
if tl == nil {
return
}
openLink(tl)
e.SetHandled()
})
}
func (tx *Text) WidgetTooltip(pos image.Point) (string, image.Point) {
if pos == image.Pt(-1, -1) {
return tx.Tooltip, image.Point{}
}
tl, bounds := tx.findLink(pos)
if tl == nil {
return tx.Tooltip, tx.DefaultTooltipPos()
}
return tl.URL, bounds.Min
}
func (tx *Text) copy() {
md := mimedata.NewText(tx.Text)
em := tx.Events()
if em != nil {
em.Clipboard().Write(md)
}
}
func (tx *Text) Label() string {
if tx.Text != "" {
return tx.Text
}
return tx.Name
}
// configTextSize does the HTML and Layout in paintText for text,
// using given size to constrain layout.
func (tx *Text) configTextSize(sz math32.Vector2) {
// todo: last arg is CSSAgg. Can synthesize that some other way?
fs := tx.Styles.FontRender()
txs := &tx.Styles.Text
tx.paintText.SetHTML(tx.Text, fs, txs, &tx.Styles.UnitContext, nil)
tx.paintText.Layout(txs, fs, &tx.Styles.UnitContext, sz)
}
// configTextAlloc is used for determining how much space the text
// takes, using given size (typically Alloc).
// In this case, alignment factors are turned off,
// because they otherwise can absorb much more space, which should
// instead be controlled by the base Align X,Y factors.
func (tx *Text) configTextAlloc(sz math32.Vector2) math32.Vector2 {
// todo: last arg is CSSAgg. Can synthesize that some other way?
fs := tx.Styles.FontRender()
txs := &tx.Styles.Text
align, alignV := txs.Align, txs.AlignV
txs.Align, txs.AlignV = styles.Start, styles.Start
tx.paintText.SetHTML(tx.Text, fs, txs, &tx.Styles.UnitContext, nil)
tx.paintText.Layout(txs, fs, &tx.Styles.UnitContext, sz)
rsz := tx.paintText.BBox.Size().Ceil()
txs.Align, txs.AlignV = align, alignV
tx.paintText.Layout(txs, fs, &tx.Styles.UnitContext, rsz)
return rsz
}
func (tx *Text) SizeUp() {
tx.WidgetBase.SizeUp() // sets Actual size based on styles
sz := &tx.Geom.Size
if tx.Styles.Text.HasWordWrap() {
// note: using a narrow ratio of .5 to allow text to squeeze into narrow space
tx.configTextSize(paint.TextWrapSizeEstimate(tx.Geom.Size.Actual.Content, len(tx.Text), 0.5, &tx.Styles.Font))
} else {
tx.configTextSize(sz.Actual.Content)
}
rsz := tx.paintText.BBox.Size().Ceil()
sz.FitSizeMax(&sz.Actual.Content, rsz)
sz.setTotalFromContent(&sz.Actual)
if DebugSettings.LayoutTrace {
fmt.Println(tx, "Text SizeUp:", rsz, "Actual:", sz.Actual.Content)
}
}
func (tx *Text) SizeDown(iter int) bool {
if !tx.Styles.Text.HasWordWrap() || iter > 1 {
return false
}
sz := &tx.Geom.Size
rsz := tx.configTextAlloc(sz.Alloc.Content) // use allocation
prevContent := sz.Actual.Content
// start over so we don't reflect hysteresis of prior guess
sz.setInitContentMin(tx.Styles.Min.Dots().Ceil())
sz.FitSizeMax(&sz.Actual.Content, rsz)
sz.setTotalFromContent(&sz.Actual)
chg := prevContent != sz.Actual.Content
if chg {
if DebugSettings.LayoutTrace {
fmt.Println(tx, "Label Size Changed:", sz.Actual.Content, "was:", prevContent)
}
}
return chg
}
func (tx *Text) Render() {
tx.WidgetBase.Render()
tx.paintText.Render(&tx.Scene.PaintContext, tx.Geom.Pos.Content)
}
// 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 core
import (
"fmt"
"image"
"log/slog"
"reflect"
"slices"
"sync"
"time"
"unicode"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/events/key"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/parse/complete"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
"golang.org/x/image/draw"
)
// TextField is a widget for editing a line of text.
//
// With the default [styles.WhiteSpaceNormal] setting,
// text will wrap onto multiple lines as needed. You can
// call [styles.Style.SetTextWrap](false) to force everything
// to be rendered on a single line. With multi-line wrapped text,
// the text is still treated as a single contiguous line of wrapped text.
type TextField struct { //core:embedder
Frame
// Type is the styling type of the text field.
Type TextFieldTypes
// Placeholder is the text that is displayed
// when the text field is empty.
Placeholder string
// Validator is a function used to validate the input
// of the text field. If it returns a non-nil error,
// then an error color, icon, and tooltip will be displayed.
Validator func() error `json:"-" xml:"-"`
// LeadingIcon, if specified, indicates to add a button
// at the start of the text field with this icon.
// See [TextField.SetLeadingIcon].
LeadingIcon icons.Icon `set:"-"`
// LeadingIconOnClick, if specified, is the function to call when
// the LeadingIcon is clicked. If this is nil, the leading icon
// will not be interactive. See [TextField.SetLeadingIcon].
LeadingIconOnClick func(e events.Event) `json:"-" xml:"-"`
// TrailingIcon, if specified, indicates to add a button
// at the end of the text field with this icon.
// See [TextField.SetTrailingIcon].
TrailingIcon icons.Icon `set:"-"`
// TrailingIconOnClick, if specified, is the function to call when
// the TrailingIcon is clicked. If this is nil, the trailing icon
// will not be interactive. See [TextField.SetTrailingIcon].
TrailingIconOnClick func(e events.Event) `json:"-" xml:"-"`
// NoEcho is whether replace displayed characters with bullets
// to conceal text (for example, for a password input). Also
// see [TextField.SetTypePassword].
NoEcho bool
// CursorWidth is the width of the text field cursor.
// It should be set in a Styler like all other style properties.
// By default, it is 1dp.
CursorWidth units.Value
// CursorColor is the color used for the text field cursor (caret).
// It should be set in a Styler like all other style properties.
// By default, it is [colors.Scheme.Primary.Base].
CursorColor image.Image
// PlaceholderColor is the color used for the [TextField.Placeholder] text.
// It should be set in a Styler like all other style properties.
// By default, it is [colors.Scheme.OnSurfaceVariant].
PlaceholderColor image.Image
// SelectColor is the color used for the text selection background color.
// It should be set in a Styler like all other style properties.
// By default, it is [colors.Scheme.Select.Container].
SelectColor image.Image
// complete contains functions and data for text field completion.
// It must be set using [TextField.SetCompleter].
complete *Complete
// text is the last saved value of the text string being edited.
text string
// edited is whether the text has been edited relative to the original.
edited bool
// editText is the live text string being edited, with the latest modifications.
editText []rune
// error is the current validation error of the text field.
error error
// effPos is the effective position with any leading icon space added.
effPos math32.Vector2
// effSize is the effective size, subtracting any leading and trailing icon space.
effSize math32.Vector2
// startPos is the starting display position in the string.
startPos int
// endPos is the ending display position in the string.
endPos int
// cursorPos is the current cursor position.
cursorPos int
// cursorLine is the current cursor line position.
cursorLine int
// charWidth is the approximate number of chars that can be
// displayed at any time, which is computed from the font size.
charWidth int
// selectStart is the starting position of selection in the string.
selectStart int
// selectEnd is the ending position of selection in the string.
selectEnd int
// selectInit is the initial selection position (where it started).
selectInit int
// selectMode is whether to select text as the cursor moves.
selectMode bool
// selectModeShift is whether selectmode was turned on because of the shift key.
selectModeShift bool
// renderAll is the render version of entire text, for sizing.
renderAll paint.Text
// renderVisible is the render version of just the visible text.
renderVisible paint.Text
// number of lines from last render update, for word-wrap version
numLines int
// fontHeight is the font height cached during styling.
fontHeight 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
// undos is the undo manager for the text field.
undos textFieldUndos
leadingIconButton, trailingIconButton *Button
}
// TextFieldTypes is an enum containing the
// different possible types of text fields.
type TextFieldTypes int32 //enums:enum -trim-prefix TextField
const (
// TextFieldFilled represents a filled
// [TextField] with a background color
// and a bottom border.
TextFieldFilled TextFieldTypes = iota
// TextFieldOutlined represents an outlined
// [TextField] with a border on all sides
// and no background color.
TextFieldOutlined
)
// Validator is an interface for types to provide a Validate method
// that is used to validate string [Value]s using [TextField.Validator].
type Validator interface {
// Validate returns an error if the value is invalid.
Validate() error
}
func (tf *TextField) WidgetValue() any { return &tf.text }
func (tf *TextField) OnBind(value any, tags reflect.StructTag) {
if vd, ok := value.(Validator); ok {
tf.Validator = vd.Validate
}
}
func (tf *TextField) Init() {
tf.Frame.Init()
tf.AddContextMenu(tf.contextMenu)
tf.Styler(func(s *styles.Style) {
s.SetAbilities(true, abilities.Activatable, abilities.Focusable, abilities.Hoverable, abilities.Slideable, abilities.DoubleClickable, abilities.TripleClickable)
tf.CursorWidth.Dp(1)
tf.SelectColor = colors.Scheme.Select.Container
tf.PlaceholderColor = colors.Scheme.OnSurfaceVariant
tf.CursorColor = colors.Scheme.Primary.Base
s.Cursor = cursors.Text
s.VirtualKeyboard = styles.KeyboardSingleLine
s.GrowWrap = false // note: doesn't work with Grow
s.Grow.Set(1, 0)
s.Min.Y.Em(1.1)
s.Min.X.Ch(20)
s.Max.X.Ch(40)
s.Gap.Zero()
s.Padding.Set(units.Dp(8), units.Dp(8))
if tf.LeadingIcon.IsSet() {
s.Padding.Left.Dp(12)
}
if tf.TrailingIcon.IsSet() {
s.Padding.Right.Dp(12)
}
s.Text.Align = styles.Start
s.Align.Items = styles.Center
s.Color = colors.Scheme.OnSurface
switch tf.Type {
case TextFieldFilled:
s.Border.Style.Set(styles.BorderNone)
s.Border.Style.Bottom = styles.BorderSolid
s.Border.Width.Zero()
s.Border.Color.Zero()
s.Border.Radius = styles.BorderRadiusExtraSmallTop
s.Background = colors.Scheme.SurfaceContainer
s.MaxBorder = s.Border
s.MaxBorder.Width.Bottom = units.Dp(2)
s.MaxBorder.Color.Bottom = colors.Scheme.Primary.Base
s.Border.Width.Bottom = units.Dp(1)
s.Border.Color.Bottom = colors.Scheme.OnSurfaceVariant
if tf.error != nil {
s.Border.Color.Bottom = colors.Scheme.Error.Base
}
case TextFieldOutlined:
s.Border.Style.Set(styles.BorderSolid)
s.Border.Radius = styles.BorderRadiusExtraSmall
s.MaxBorder = s.Border
s.MaxBorder.Width.Set(units.Dp(2))
s.MaxBorder.Color.Set(colors.Scheme.Primary.Base)
s.Border.Width.Set(units.Dp(1))
if tf.error != nil {
s.Border.Color.Set(colors.Scheme.Error.Base)
}
}
if tf.IsReadOnly() {
s.Border.Color.Zero()
s.Border.Width.Zero()
s.Border.Radius.Zero()
s.MaxBorder = s.Border
s.Background = nil
}
if s.Is(states.Selected) {
s.Background = colors.Scheme.Select.Container
}
})
tf.FinalStyler(func(s *styles.Style) {
s.SetAbilities(!tf.IsReadOnly(), abilities.Focusable)
})
tf.handleKeyEvents()
tf.OnFirst(events.Change, func(e events.Event) {
tf.validate()
if tf.error != nil {
e.SetHandled()
}
})
tf.On(events.MouseDown, func(e events.Event) {
if !tf.StateIs(states.Focused) {
tf.SetFocus() // always grab, even if read only..
}
if tf.IsReadOnly() {
return
}
e.SetHandled()
switch e.MouseButton() {
case events.Left:
tf.setCursorFromPixel(e.Pos(), e.SelectMode())
case events.Middle:
e.SetHandled()
tf.setCursorFromPixel(e.Pos(), e.SelectMode())
tf.paste()
}
})
tf.OnClick(func(e events.Event) {
if tf.IsReadOnly() {
return
}
tf.SetFocus()
})
tf.On(events.DoubleClick, func(e events.Event) {
if tf.IsReadOnly() {
return
}
if !tf.IsReadOnly() && !tf.StateIs(states.Focused) {
tf.SetFocus()
}
e.SetHandled()
tf.selectWord()
})
tf.On(events.TripleClick, func(e events.Event) {
if tf.IsReadOnly() {
return
}
if !tf.IsReadOnly() && !tf.StateIs(states.Focused) {
tf.SetFocus()
}
e.SetHandled()
tf.selectAll()
})
tf.On(events.SlideMove, func(e events.Event) {
if tf.IsReadOnly() {
return
}
e.SetHandled()
if !tf.selectMode {
tf.selectModeToggle()
}
tf.setCursorFromPixel(e.Pos(), events.SelectOne)
})
tf.OnClose(func(e events.Event) {
tf.editDone() // todo: this must be protected against something else, for race detector
})
tf.Maker(func(p *tree.Plan) {
tf.editText = []rune(tf.text)
tf.edited = false
if tf.IsReadOnly() {
return
}
if tf.LeadingIcon.IsSet() {
tree.AddAt(p, "lead-icon", func(w *Button) {
tf.leadingIconButton = w
w.SetType(ButtonAction)
w.Styler(func(s *styles.Style) {
s.Padding.Zero()
s.Color = colors.Scheme.OnSurfaceVariant
s.Margin.SetRight(units.Dp(8))
if tf.LeadingIconOnClick == nil {
s.SetAbilities(false, abilities.Activatable, abilities.Focusable, abilities.Hoverable)
s.Cursor = cursors.None
}
// If we are responsible for a positive (non-disabled) state layer
// (instead of our parent), then we amplify it so that it is clear
// that we ourself are receiving a state layer amplifying event.
// Otherwise, we set our state color to that of our parent
// so that it does not appear as if we are getting interaction ourself;
// instead, we are a part of our parent and render a background color no
// different than them.
if s.Is(states.Hovered) || s.Is(states.Focused) || s.Is(states.Active) {
s.StateLayer *= 3
} else {
s.StateColor = tf.Styles.Color
}
})
w.OnClick(func(e events.Event) {
if tf.LeadingIconOnClick != nil {
tf.LeadingIconOnClick(e)
}
})
w.Updater(func() {
w.SetIcon(tf.LeadingIcon)
})
})
} else {
tf.leadingIconButton = nil
}
if tf.TrailingIcon.IsSet() || tf.error != nil {
tree.AddAt(p, "trail-icon-stretch", func(w *Stretch) {
w.Styler(func(s *styles.Style) {
s.Grow.Set(1, 0)
})
})
tree.AddAt(p, "trail-icon", func(w *Button) {
tf.trailingIconButton = w
w.SetType(ButtonAction)
w.Styler(func(s *styles.Style) {
s.Padding.Zero()
s.Color = colors.Scheme.OnSurfaceVariant
if tf.error != nil {
s.Color = colors.Scheme.Error.Base
}
s.Margin.SetLeft(units.Dp(8))
if tf.TrailingIconOnClick == nil || tf.error != nil {
s.SetAbilities(false, abilities.Activatable, abilities.Focusable, abilities.Hoverable)
s.Cursor = cursors.None
// need to clear state in case it was set when there
// was no error
s.State = 0
}
// same reasoning as for leading icon
if s.Is(states.Hovered) || s.Is(states.Focused) || s.Is(states.Active) {
s.StateLayer *= 3
} else {
s.StateColor = tf.Styles.Color
}
})
w.OnClick(func(e events.Event) {
if tf.TrailingIconOnClick != nil {
tf.TrailingIconOnClick(e)
}
})
w.Updater(func() {
w.SetIcon(tf.TrailingIcon)
if tf.error != nil {
w.SetIcon(icons.Error)
}
})
})
} else {
tf.trailingIconButton = nil
}
})
}
func (tf *TextField) Destroy() {
tf.stopCursor()
tf.Frame.Destroy()
}
// Text returns the current text of the text field. It applies any unapplied changes
// first, and sends an [events.Change] event if applicable. This is the main end-user
// method to get the current value of the text field.
func (tf *TextField) Text() string {
tf.editDone()
return tf.text
}
// SetText sets the text of the text field and reverts any current edits
// to reflect this new text.
func (tf *TextField) SetText(text string) *TextField {
if tf.text == text && !tf.edited {
return tf
}
tf.text = text
tf.revert()
return tf
}
// SetLeadingIcon sets the [TextField.LeadingIcon] to the given icon. If an
// on click function is specified, it also sets the [TextField.LeadingIconOnClick]
// to that function. If no function is specified, it does not override any already
// set function.
func (tf *TextField) SetLeadingIcon(icon icons.Icon, onClick ...func(e events.Event)) *TextField {
tf.LeadingIcon = icon
if len(onClick) > 0 {
tf.LeadingIconOnClick = onClick[0]
}
return tf
}
// SetTrailingIcon sets the [TextField.TrailingIcon] to the given icon. If an
// on click function is specified, it also sets the [TextField.TrailingIconOnClick]
// to that function. If no function is specified, it does not override any already
// set function.
func (tf *TextField) SetTrailingIcon(icon icons.Icon, onClick ...func(e events.Event)) *TextField {
tf.TrailingIcon = icon
if len(onClick) > 0 {
tf.TrailingIconOnClick = onClick[0]
}
return tf
}
// AddClearButton adds a trailing icon button at the end
// of the text field that clears the text in the text field
// when it is clicked.
func (tf *TextField) AddClearButton() *TextField {
return tf.SetTrailingIcon(icons.Close, func(e events.Event) {
tf.clear()
})
}
// SetTypePassword enables [TextField.NoEcho] and adds a trailing
// icon button at the end of the textfield that toggles [TextField.NoEcho].
// It also sets [styles.Style.VirtualKeyboard] to [styles.KeyboardPassword].
func (tf *TextField) SetTypePassword() *TextField {
tf.SetNoEcho(true).SetTrailingIcon(icons.Visibility, func(e events.Event) {
tf.NoEcho = !tf.NoEcho
if tf.NoEcho {
tf.TrailingIcon = icons.Visibility
} else {
tf.TrailingIcon = icons.VisibilityOff
}
if icon := tf.trailingIconButton; icon != nil {
icon.SetIcon(tf.TrailingIcon).Update()
}
}).Styler(func(s *styles.Style) {
s.VirtualKeyboard = styles.KeyboardPassword
})
return tf
}
// editDone completes editing and copies the active edited text to the [TextField.text].
// It is called when the return key is pressed or the text field goes out of focus.
func (tf *TextField) editDone() {
if tf.edited {
tf.edited = false
tf.text = string(tf.editText)
tf.SendChange()
// widget can be killed after SendChange
if tf.This == nil {
return
}
}
tf.clearSelected()
tf.clearCursor()
}
// revert aborts editing and reverts to the last saved text.
func (tf *TextField) revert() {
tf.editText = []rune(tf.text)
tf.edited = false
tf.startPos = 0
tf.endPos = tf.charWidth
tf.selectReset()
tf.NeedsRender()
}
// clear clears any existing text.
func (tf *TextField) clear() {
tf.edited = true
tf.editText = tf.editText[:0]
tf.startPos = 0
tf.endPos = 0
tf.selectReset()
tf.SetFocus() // this is essential for ensuring that the clear applies after focus is lost..
tf.NeedsRender()
}
// clearError clears any existing validation error.
func (tf *TextField) clearError() {
if tf.error == nil {
return
}
tf.error = nil
tf.Update()
tf.Send(events.LongHoverEnd) // get rid of any validation tooltip
}
// validate runs [TextField.Validator] and takes any necessary actions
// as a result of that.
func (tf *TextField) validate() {
if tf.Validator == nil {
return
}
err := tf.Validator()
if err == nil {
tf.clearError()
return
}
tf.error = err
tf.Update()
// show the error tooltip immediately
tf.Send(events.LongHoverStart)
}
func (tf *TextField) WidgetTooltip(pos image.Point) (string, image.Point) {
if tf.error == nil {
return tf.Tooltip, tf.DefaultTooltipPos()
}
return tf.error.Error(), tf.DefaultTooltipPos()
}
//////////////////////////////////////////////////////////////////////////////////////////
// Cursor Navigation
// cursorForward moves the cursor forward
func (tf *TextField) cursorForward(steps int) {
tf.cursorPos += steps
if tf.cursorPos > len(tf.editText) {
tf.cursorPos = len(tf.editText)
}
if tf.cursorPos > tf.endPos {
inc := tf.cursorPos - tf.endPos
tf.endPos += inc
}
tf.cursorLine, _, _ = tf.renderAll.RuneSpanPos(tf.cursorPos)
if tf.selectMode {
tf.selectRegionUpdate(tf.cursorPos)
}
tf.NeedsRender()
}
// cursorForwardWord moves the cursor forward by words
func (tf *TextField) cursorForwardWord(steps int) {
for i := 0; i < steps; i++ {
sz := len(tf.editText)
if sz > 0 && tf.cursorPos < sz {
ch := tf.cursorPos
var done = false
for ch < sz && !done { // if on a wb, go past
r1 := tf.editText[ch]
r2 := rune(-1)
if ch < sz-1 {
r2 = tf.editText[ch+1]
}
if IsWordBreak(r1, r2) {
ch++
} else {
done = true
}
}
done = false
for ch < sz && !done {
r1 := tf.editText[ch]
r2 := rune(-1)
if ch < sz-1 {
r2 = tf.editText[ch+1]
}
if !IsWordBreak(r1, r2) {
ch++
} else {
done = true
}
}
tf.cursorPos = ch
} else {
tf.cursorPos = sz
}
}
if tf.cursorPos > len(tf.editText) {
tf.cursorPos = len(tf.editText)
}
if tf.cursorPos > tf.endPos {
inc := tf.cursorPos - tf.endPos
tf.endPos += inc
}
tf.cursorLine, _, _ = tf.renderAll.RuneSpanPos(tf.cursorPos)
if tf.selectMode {
tf.selectRegionUpdate(tf.cursorPos)
}
tf.NeedsRender()
}
// cursorBackward moves the cursor backward
func (tf *TextField) cursorBackward(steps int) {
tf.cursorPos -= steps
if tf.cursorPos < 0 {
tf.cursorPos = 0
}
if tf.cursorPos <= tf.startPos {
dec := min(tf.startPos, 8)
tf.startPos -= dec
}
tf.cursorLine, _, _ = tf.renderAll.RuneSpanPos(tf.cursorPos)
if tf.selectMode {
tf.selectRegionUpdate(tf.cursorPos)
}
tf.NeedsRender()
}
// cursorBackwardWord moves the cursor backward by words
func (tf *TextField) cursorBackwardWord(steps int) {
for i := 0; i < steps; i++ {
sz := len(tf.editText)
if sz > 0 && tf.cursorPos > 0 {
ch := min(tf.cursorPos, sz-1)
var done = false
for ch < sz && !done { // if on a wb, go past
r1 := tf.editText[ch]
r2 := rune(-1)
if ch > 0 {
r2 = tf.editText[ch-1]
}
if IsWordBreak(r1, r2) {
ch--
if ch == -1 {
done = true
}
} else {
done = true
}
}
done = false
for ch < sz && ch >= 0 && !done {
r1 := tf.editText[ch]
r2 := rune(-1)
if ch > 0 {
r2 = tf.editText[ch-1]
}
if !IsWordBreak(r1, r2) {
ch--
} else {
done = true
}
}
tf.cursorPos = ch
} else {
tf.cursorPos = 0
}
}
if tf.cursorPos < 0 {
tf.cursorPos = 0
}
if tf.cursorPos <= tf.startPos {
dec := min(tf.startPos, 8)
tf.startPos -= dec
}
tf.cursorLine, _, _ = tf.renderAll.RuneSpanPos(tf.cursorPos)
if tf.selectMode {
tf.selectRegionUpdate(tf.cursorPos)
}
tf.NeedsRender()
}
// cursorDown moves the cursor down
func (tf *TextField) cursorDown(steps int) {
if tf.numLines <= 1 {
return
}
if tf.cursorLine >= tf.numLines-1 {
return
}
_, ri, _ := tf.renderAll.RuneSpanPos(tf.cursorPos)
tf.cursorLine = min(tf.cursorLine+steps, tf.numLines-1)
tf.cursorPos, _ = tf.renderAll.SpanPosToRuneIndex(tf.cursorLine, ri)
if tf.selectMode {
tf.selectRegionUpdate(tf.cursorPos)
}
tf.NeedsRender()
}
// cursorUp moves the cursor up
func (tf *TextField) cursorUp(steps int) {
if tf.numLines <= 1 {
return
}
if tf.cursorLine <= 0 {
return
}
_, ri, _ := tf.renderAll.RuneSpanPos(tf.cursorPos)
tf.cursorLine = max(tf.cursorLine-steps, 0)
tf.cursorPos, _ = tf.renderAll.SpanPosToRuneIndex(tf.cursorLine, ri)
if tf.selectMode {
tf.selectRegionUpdate(tf.cursorPos)
}
tf.NeedsRender()
}
// cursorStart moves the cursor to the start of the text, updating selection
// if select mode is active.
func (tf *TextField) cursorStart() {
tf.cursorPos = 0
tf.startPos = 0
tf.endPos = min(len(tf.editText), tf.startPos+tf.charWidth)
if tf.selectMode {
tf.selectRegionUpdate(tf.cursorPos)
}
tf.NeedsRender()
}
// cursorEnd moves the cursor to the end of the text, updating selection
// if select mode is active.
func (tf *TextField) cursorEnd() {
ed := len(tf.editText)
tf.cursorPos = ed
tf.endPos = len(tf.editText) // try -- display will adjust
tf.startPos = max(0, tf.endPos-tf.charWidth)
if tf.selectMode {
tf.selectRegionUpdate(tf.cursorPos)
}
tf.NeedsRender()
}
// cursorBackspace deletes character(s) immediately before cursor
func (tf *TextField) cursorBackspace(steps int) {
if tf.hasSelection() {
tf.deleteSelection()
return
}
if tf.cursorPos < steps {
steps = tf.cursorPos
}
if steps <= 0 {
return
}
tf.edited = true
tf.editText = append(tf.editText[:tf.cursorPos-steps], tf.editText[tf.cursorPos:]...)
tf.cursorBackward(steps)
tf.NeedsRender()
}
// cursorDelete deletes character(s) immediately after the cursor
func (tf *TextField) cursorDelete(steps int) {
if tf.hasSelection() {
tf.deleteSelection()
return
}
if tf.cursorPos+steps > len(tf.editText) {
steps = len(tf.editText) - tf.cursorPos
}
if steps <= 0 {
return
}
tf.edited = true
tf.editText = append(tf.editText[:tf.cursorPos], tf.editText[tf.cursorPos+steps:]...)
tf.NeedsRender()
}
// cursorBackspaceWord deletes words(s) immediately before cursor
func (tf *TextField) cursorBackspaceWord(steps int) {
if tf.hasSelection() {
tf.deleteSelection()
return
}
org := tf.cursorPos
tf.cursorBackwardWord(steps)
tf.edited = true
tf.editText = append(tf.editText[:tf.cursorPos], tf.editText[org:]...)
tf.NeedsRender()
}
// cursorDeleteWord deletes word(s) immediately after the cursor
func (tf *TextField) cursorDeleteWord(steps int) {
if tf.hasSelection() {
tf.deleteSelection()
return
}
// note: no update b/c signal from buf will drive update
org := tf.cursorPos
tf.cursorForwardWord(steps)
tf.edited = true
tf.editText = append(tf.editText[:tf.cursorPos], tf.editText[org:]...)
tf.NeedsRender()
}
// cursorKill deletes text from cursor to end of text
func (tf *TextField) cursorKill() {
steps := len(tf.editText) - tf.cursorPos
tf.cursorDelete(steps)
}
///////////////////////////////////////////////////////////////////////////////
// Selection
// clearSelected resets both the global selected flag and any current selection
func (tf *TextField) clearSelected() {
tf.SetState(false, states.Selected)
tf.selectReset()
}
// hasSelection returns whether there is a selected region of text
func (tf *TextField) hasSelection() bool {
tf.selectUpdate()
return tf.selectStart < tf.selectEnd
}
// selection returns the currently selected text
func (tf *TextField) selection() string {
if tf.hasSelection() {
return string(tf.editText[tf.selectStart:tf.selectEnd])
}
return ""
}
// selectModeToggle toggles the SelectMode, updating selection with cursor movement
func (tf *TextField) selectModeToggle() {
if tf.selectMode {
tf.selectMode = false
} else {
tf.selectMode = true
tf.selectInit = tf.cursorPos
tf.selectStart = tf.cursorPos
tf.selectEnd = tf.selectStart
}
}
// shiftSelect sets the selection start if the shift key is down but wasn't previously.
// If the shift key has been released, the selection info is cleared.
func (tf *TextField) shiftSelect(e events.Event) {
hasShift := e.HasAnyModifier(key.Shift)
if hasShift && !tf.selectMode {
tf.selectModeToggle()
tf.selectModeShift = true
}
if !hasShift && tf.selectMode && tf.selectModeShift {
tf.selectReset()
tf.selectModeShift = false
}
}
// selectRegionUpdate updates current select region based on given cursor position
// relative to SelectStart position
func (tf *TextField) selectRegionUpdate(pos int) {
if pos < tf.selectInit {
tf.selectStart = pos
tf.selectEnd = tf.selectInit
} else {
tf.selectStart = tf.selectInit
tf.selectEnd = pos
}
tf.selectUpdate()
}
// selectAll selects all the text
func (tf *TextField) selectAll() {
tf.selectStart = 0
tf.selectInit = 0
tf.selectEnd = len(tf.editText)
if TheApp.SystemPlatform().IsMobile() {
tf.Send(events.ContextMenu)
}
tf.NeedsRender()
}
// isWordBreak defines what counts as a word break for the purposes of selecting words
func (tf *TextField) isWordBreak(r rune) bool {
return unicode.IsSpace(r) || unicode.IsSymbol(r) || unicode.IsPunct(r)
}
// selectWord selects the word (whitespace delimited) that the cursor is on
func (tf *TextField) selectWord() {
sz := len(tf.editText)
if sz <= 3 {
tf.selectAll()
return
}
tf.selectStart = tf.cursorPos
if tf.selectStart >= sz {
tf.selectStart = sz - 2
}
if !tf.isWordBreak(tf.editText[tf.selectStart]) {
for tf.selectStart > 0 {
if tf.isWordBreak(tf.editText[tf.selectStart-1]) {
break
}
tf.selectStart--
}
tf.selectEnd = tf.cursorPos + 1
for tf.selectEnd < sz {
if tf.isWordBreak(tf.editText[tf.selectEnd]) {
break
}
tf.selectEnd++
}
} else { // keep the space start -- go to next space..
tf.selectEnd = tf.cursorPos + 1
for tf.selectEnd < sz {
if !tf.isWordBreak(tf.editText[tf.selectEnd]) {
break
}
tf.selectEnd++
}
for tf.selectEnd < sz { // include all trailing spaces
if tf.isWordBreak(tf.editText[tf.selectEnd]) {
break
}
tf.selectEnd++
}
}
tf.selectInit = tf.selectStart
if TheApp.SystemPlatform().IsMobile() {
tf.Send(events.ContextMenu)
}
tf.NeedsRender()
}
// selectReset resets the selection
func (tf *TextField) selectReset() {
tf.selectMode = false
if tf.selectStart == 0 && tf.selectEnd == 0 {
return
}
tf.selectStart = 0
tf.selectEnd = 0
tf.NeedsRender()
}
// selectUpdate updates the select region after any change to the text, to keep it in range
func (tf *TextField) selectUpdate() {
if tf.selectStart < tf.selectEnd {
ed := len(tf.editText)
if tf.selectStart < 0 {
tf.selectStart = 0
}
if tf.selectEnd > ed {
tf.selectEnd = ed
}
} else {
tf.selectReset()
}
}
// cut cuts any selected text and adds it to the clipboard.
func (tf *TextField) cut() { //types:add
if tf.NoEcho {
return
}
cut := tf.deleteSelection()
if cut != "" {
em := tf.Events()
if em != nil {
em.Clipboard().Write(mimedata.NewText(cut))
}
}
}
// deleteSelection deletes any selected text, without adding to clipboard --
// returns text deleted
func (tf *TextField) deleteSelection() string {
tf.selectUpdate()
if !tf.hasSelection() {
return ""
}
cut := tf.selection()
tf.edited = true
tf.editText = append(tf.editText[:tf.selectStart], tf.editText[tf.selectEnd:]...)
if tf.cursorPos > tf.selectStart {
if tf.cursorPos < tf.selectEnd {
tf.cursorPos = tf.selectStart
} else {
tf.cursorPos -= tf.selectEnd - tf.selectStart
}
}
tf.selectReset()
tf.NeedsRender()
return cut
}
// copy copies any selected text to the clipboard.
func (tf *TextField) copy() { //types:add
if tf.NoEcho {
return
}
tf.selectUpdate()
if !tf.hasSelection() {
return
}
md := mimedata.NewText(tf.Text())
tf.Clipboard().Write(md)
}
// paste inserts text from the clipboard at current cursor position; if
// cursor is within a current selection, that selection is replaced.
func (tf *TextField) paste() { //types:add
data := tf.Clipboard().Read([]string{mimedata.TextPlain})
if data != nil {
if tf.cursorPos >= tf.selectStart && tf.cursorPos < tf.selectEnd {
tf.deleteSelection()
}
tf.insertAtCursor(data.Text(mimedata.TextPlain))
}
}
// insertAtCursor inserts the given text at current cursor position.
func (tf *TextField) insertAtCursor(str string) {
if tf.hasSelection() {
tf.cut()
}
tf.edited = true
rs := []rune(str)
rsl := len(rs)
nt := append(tf.editText, rs...) // first append to end
copy(nt[tf.cursorPos+rsl:], nt[tf.cursorPos:]) // move stuff to end
copy(nt[tf.cursorPos:], rs) // copy into position
tf.editText = nt
tf.endPos += rsl
tf.cursorForward(rsl)
tf.NeedsRender()
}
func (tf *TextField) contextMenu(m *Scene) {
NewFuncButton(m).SetFunc(tf.copy).SetIcon(icons.Copy).SetKey(keymap.Copy).SetState(tf.NoEcho || !tf.hasSelection(), states.Disabled)
if !tf.IsReadOnly() {
NewFuncButton(m).SetFunc(tf.cut).SetIcon(icons.Cut).SetKey(keymap.Cut).SetState(tf.NoEcho || !tf.hasSelection(), states.Disabled)
paste := NewFuncButton(m).SetFunc(tf.paste).SetIcon(icons.Paste).SetKey(keymap.Paste)
cb := tf.Scene.Events.Clipboard()
if cb != nil {
paste.SetState(cb.IsEmpty(), states.Disabled)
}
}
}
///////////////////////////////////////////////////////////////////////////////
// Undo
// textFieldUndoRecord holds one undo record
type textFieldUndoRecord struct {
text []rune
cursorPos int
}
func (ur *textFieldUndoRecord) set(txt []rune, curpos int) {
ur.text = slices.Clone(txt)
ur.cursorPos = curpos
}
// textFieldUndos manages everything about the undo process for a [TextField].
type textFieldUndos struct {
// stack of undo records
stack []textFieldUndoRecord
// position within the undo stack
pos int
// last time undo was saved, for grouping
lastSave time.Time
}
func (us *textFieldUndos) saveUndo(txt []rune, curpos int) {
n := len(us.stack)
now := time.Now()
ts := now.Sub(us.lastSave)
if n > 0 && ts < 250*time.Millisecond {
r := us.stack[n-1]
r.set(txt, curpos)
us.stack[n-1] = r
return
}
r := textFieldUndoRecord{}
r.set(txt, curpos)
us.stack = append(us.stack, r)
us.pos = len(us.stack)
us.lastSave = now
}
func (tf *TextField) saveUndo() {
tf.undos.saveUndo(tf.editText, tf.cursorPos)
}
func (us *textFieldUndos) undo(txt []rune, curpos int) *textFieldUndoRecord {
n := len(us.stack)
if us.pos <= 0 || n == 0 {
return &textFieldUndoRecord{}
}
if us.pos == n {
us.lastSave = time.Time{}
us.saveUndo(txt, curpos)
us.pos--
}
us.pos--
us.lastSave = time.Time{} // prevent any merging
r := &us.stack[us.pos]
return r
}
func (tf *TextField) undo() {
r := tf.undos.undo(tf.editText, tf.cursorPos)
if r != nil {
tf.editText = r.text
tf.cursorPos = r.cursorPos
tf.NeedsRender()
}
}
func (us *textFieldUndos) redo() *textFieldUndoRecord {
n := len(us.stack)
if us.pos >= n-1 {
return nil
}
us.lastSave = time.Time{} // prevent any merging
us.pos++
return &us.stack[us.pos]
}
func (tf *TextField) redo() {
r := tf.undos.redo()
if r != nil {
tf.editText = r.text
tf.cursorPos = r.cursorPos
tf.NeedsRender()
}
}
///////////////////////////////////////////////////////////////////////////////
// Complete
// SetCompleter sets completion functions so that completions will
// automatically be offered as the user types.
func (tf *TextField) SetCompleter(data any, matchFun complete.MatchFunc, editFun complete.EditFunc) {
if matchFun == nil || editFun == nil {
tf.complete = nil
return
}
tf.complete = NewComplete().SetContext(data).SetMatchFunc(matchFun).SetEditFunc(editFun)
tf.complete.OnSelect(func(e events.Event) {
tf.completeText(tf.complete.Completion)
})
}
// offerComplete pops up a menu of possible completions
func (tf *TextField) offerComplete() {
if tf.complete == nil {
return
}
s := string(tf.editText[0:tf.cursorPos])
cpos := tf.charRenderPos(tf.cursorPos, true).ToPoint()
cpos.X += 5
cpos.Y = tf.Geom.TotalBBox.Max.Y
tf.complete.SrcLn = 0
tf.complete.SrcCh = tf.cursorPos
tf.complete.Show(tf, cpos, s)
}
// cancelComplete cancels any pending completion -- call this when new events
// have moved beyond any prior completion scenario
func (tf *TextField) cancelComplete() {
if tf.complete == nil {
return
}
tf.complete.Cancel()
}
// completeText edits the text field using the string chosen from the completion menu
func (tf *TextField) completeText(s string) {
txt := string(tf.editText) // Reminder: do NOT call tf.Text() in an active editing context!
c := tf.complete.GetCompletion(s)
ed := tf.complete.EditFunc(tf.complete.Context, txt, tf.cursorPos, c, tf.complete.Seed)
st := tf.cursorPos - len(tf.complete.Seed)
tf.cursorPos = st
tf.cursorDelete(ed.ForwardDelete)
tf.insertAtCursor(ed.NewText)
tf.editDone()
}
///////////////////////////////////////////////////////////////////////////////
// Rendering
// hasWordWrap returns true if the layout is multi-line word wrapping
func (tf *TextField) hasWordWrap() bool {
return tf.Styles.Text.HasWordWrap()
}
// charPos returns the relative starting position of the given rune,
// in the overall RenderAll of all the text.
// These positions can be out of visible range: see CharRenderPos
func (tf *TextField) charPos(idx int) math32.Vector2 {
if idx <= 0 || len(tf.renderAll.Spans) == 0 {
return math32.Vector2{}
}
pos, _, _, _ := tf.renderAll.RuneRelPos(idx)
pos.Y -= tf.renderAll.Spans[0].RelPos.Y
return pos
}
// relCharPos returns the text width in dots between the two text string
// positions (ed is exclusive -- +1 beyond actual char).
func (tf *TextField) relCharPos(st, ed int) math32.Vector2 {
return tf.charPos(ed).Sub(tf.charPos(st))
}
// 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 {
pos := tf.effPos
if wincoords {
sc := tf.Scene
pos = pos.Add(math32.FromPoint(sc.SceneGeom.Pos))
}
cpos := tf.relCharPos(tf.startPos, 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)
tf.AsyncLock()
if !w.AsWidget().StateIs(states.Focused) || !w.AsWidget().IsVisible() {
tf.blinkOn = false
tf.renderCursor(false)
} else {
tf.blinkOn = !tf.blinkOn
tf.renderCursor(tf.blinkOn)
}
tf.AsyncUnlock()
}
}
// startCursor starts the cursor blinking and renders it
func (tf *TextField) startCursor() {
if tf == nil || tf.This == nil {
return
}
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)
}
// stopCursor stops the cursor from blinking
func (tf *TextField) stopCursor() {
if tf == nil || tf.This == nil {
return
}
textFieldBlinker.ResetWidget(tf.This.(Widget))
}
// 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 {
return
}
ms := tf.Scene.Stage.Main
if ms == nil {
return
}
spnm := fmt.Sprintf("%v-%v", textFieldSpriteName, tf.fontHeight)
ms.Sprites.InactivateSprite(spnm)
return
}
if !tf.IsVisible() {
return
}
tf.cursorMu.Lock()
defer tf.cursorMu.Unlock()
sp := tf.cursorSprite(on)
if sp == nil {
return
}
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
}
ms := sc.Stage.Main
if ms == nil {
return nil // only MainStage has sprites
}
spnm := fmt.Sprintf("%v-%v", textFieldSpriteName, tf.fontHeight)
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.fontHeight))}
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
}
// renderSelect renders the selected region, if any, underneath the text
func (tf *TextField) renderSelect() {
if !tf.hasSelection() {
return
}
effst := max(tf.startPos, tf.selectStart)
if effst >= tf.endPos {
return
}
effed := min(tf.endPos, tf.selectEnd)
if effed < tf.startPos {
return
}
if effed <= effst {
return
}
spos := tf.charRenderPos(effst, false)
pc := &tf.Scene.PaintContext
tsz := tf.relCharPos(effst, effed)
if !tf.hasWordWrap() || tsz.Y == 0 {
pc.FillBox(spos, math32.Vec2(tsz.X, tf.fontHeight), tf.SelectColor)
return
}
ex := float32(tf.Geom.ContentBBox.Max.X)
sx := float32(tf.Geom.ContentBBox.Min.X)
ssi, _, _ := tf.renderAll.RuneSpanPos(effst)
esi, _, _ := tf.renderAll.RuneSpanPos(effed)
ep := tf.charRenderPos(effed, false)
pc.FillBox(spos, math32.Vec2(ex-spos.X, tf.fontHeight), tf.SelectColor)
spos.X = sx
spos.Y += tf.renderAll.Spans[ssi+1].RelPos.Y - tf.renderAll.Spans[ssi].RelPos.Y
for si := ssi + 1; si <= esi; si++ {
if si < esi {
pc.FillBox(spos, math32.Vec2(ex-spos.X, tf.fontHeight), tf.SelectColor)
} else {
pc.FillBox(spos, math32.Vec2(ep.X-spos.X, tf.fontHeight), tf.SelectColor)
}
spos.Y += tf.renderAll.Spans[si].RelPos.Y - tf.renderAll.Spans[si-1].RelPos.Y
}
}
// autoScroll scrolls the starting position to keep the cursor visible
func (tf *TextField) autoScroll() {
sz := &tf.Geom.Size
icsz := tf.iconsSize()
availSz := sz.Actual.Content.Sub(icsz)
tf.configTextSize(availSz)
n := len(tf.editText)
tf.cursorPos = math32.ClampInt(tf.cursorPos, 0, n)
if tf.hasWordWrap() { // does not scroll
tf.startPos = 0
tf.endPos = n
if len(tf.renderAll.Spans) != tf.numLines {
tf.NeedsLayout()
}
return
}
st := &tf.Styles
if n == 0 || tf.Geom.Size.Actual.Content.X <= 0 {
tf.cursorPos = 0
tf.endPos = 0
tf.startPos = 0
return
}
maxw := tf.effSize.X
if maxw < 0 {
return
}
tf.charWidth = int(maxw / st.UnitContext.Dots(units.UnitCh)) // rough guess in chars
if tf.charWidth < 1 {
tf.charWidth = 1
}
// first rationalize all the values
if tf.endPos == 0 || tf.endPos > n { // not init
tf.endPos = n
}
if tf.startPos >= tf.endPos {
tf.startPos = max(0, tf.endPos-tf.charWidth)
}
inc := int(math32.Ceil(.1 * float32(tf.charWidth)))
inc = max(4, inc)
// keep cursor in view with buffer
startIsAnchor := true
if tf.cursorPos < (tf.startPos + inc) {
tf.startPos -= inc
tf.startPos = max(tf.startPos, 0)
tf.endPos = tf.startPos + tf.charWidth
tf.endPos = min(n, tf.endPos)
} else if tf.cursorPos > (tf.endPos - inc) {
tf.endPos += inc
tf.endPos = min(tf.endPos, n)
tf.startPos = tf.endPos - tf.charWidth
tf.startPos = max(0, tf.startPos)
startIsAnchor = false
}
if tf.endPos < tf.startPos {
return
}
if startIsAnchor {
gotWidth := false
spos := tf.charPos(tf.startPos).X
for {
w := tf.charPos(tf.endPos).X - spos
if w < maxw {
if tf.endPos == n {
break
}
nw := tf.charPos(tf.endPos+1).X - spos
if nw >= maxw {
gotWidth = true
break
}
tf.endPos++
} else {
tf.endPos--
}
}
if gotWidth || tf.startPos == 0 {
return
}
// otherwise, try getting some more chars by moving up start..
}
// end is now anchor
epos := tf.charPos(tf.endPos).X
for {
w := epos - tf.charPos(tf.startPos).X
if w < maxw {
if tf.startPos == 0 {
break
}
nw := epos - tf.charPos(tf.startPos-1).X
if nw >= maxw {
break
}
tf.startPos--
} else {
tf.startPos++
}
}
}
// pixelToCursor finds the cursor position that corresponds to the given pixel location
func (tf *TextField) pixelToCursor(pt image.Point) int {
ptf := math32.FromPoint(pt)
rpt := ptf.Sub(tf.effPos)
if rpt.X <= 0 || rpt.Y < 0 {
return tf.startPos
}
n := len(tf.editText)
if tf.hasWordWrap() {
si, ri, ok := tf.renderAll.PosToRune(rpt)
if ok {
ix, _ := tf.renderAll.SpanPosToRuneIndex(si, ri)
ix = min(ix, n)
return ix
}
return tf.startPos
}
pr := tf.PointToRelPos(pt)
px := float32(pr.X)
st := &tf.Styles
c := tf.startPos + int(float64(px/st.UnitContext.Dots(units.UnitCh)))
c = min(c, n)
w := tf.relCharPos(tf.startPos, c).X
if w > px {
for w > px {
c--
if c <= tf.startPos {
c = tf.startPos
break
}
w = tf.relCharPos(tf.startPos, c).X
}
} else if w < px {
for c < tf.endPos {
wn := tf.relCharPos(tf.startPos, c+1).X
if wn > px {
break
} else if wn == px {
c++
break
}
c++
}
}
return c
}
// setCursorFromPixel finds cursor location from given scene-relative
// pixel location, and sets current cursor to it, updating selection too.
func (tf *TextField) setCursorFromPixel(pt image.Point, selMode events.SelectModes) {
oldPos := tf.cursorPos
tf.cursorPos = tf.pixelToCursor(pt)
if tf.selectMode || selMode != events.SelectOne {
if !tf.selectMode && selMode != events.SelectOne {
tf.selectStart = oldPos
tf.selectMode = true
}
if !tf.StateIs(states.Sliding) && selMode == events.SelectOne { // && tf.CursorPos >= tf.SelectStart && tf.CursorPos < tf.SelectEnd {
tf.selectReset()
} else {
tf.selectRegionUpdate(tf.cursorPos)
}
tf.selectUpdate()
} else if tf.hasSelection() {
tf.selectReset()
}
tf.NeedsRender()
}
func (tf *TextField) handleKeyEvents() {
tf.OnKeyChord(func(e events.Event) {
kf := keymap.Of(e.KeyChord())
if DebugSettings.KeyEventTrace {
slog.Info("TextField KeyInput", "widget", tf, "keyFunction", kf)
}
if !tf.StateIs(states.Focused) && kf == keymap.Abort {
return
}
// first all the keys that work for both inactive and active
switch kf {
case keymap.MoveRight:
e.SetHandled()
tf.shiftSelect(e)
tf.cursorForward(1)
tf.offerComplete()
case keymap.WordRight:
e.SetHandled()
tf.shiftSelect(e)
tf.cursorForwardWord(1)
tf.offerComplete()
case keymap.MoveLeft:
e.SetHandled()
tf.shiftSelect(e)
tf.cursorBackward(1)
tf.offerComplete()
case keymap.WordLeft:
e.SetHandled()
tf.shiftSelect(e)
tf.cursorBackwardWord(1)
tf.offerComplete()
case keymap.MoveDown:
if tf.numLines > 1 {
e.SetHandled()
tf.shiftSelect(e)
tf.cursorDown(1)
}
case keymap.MoveUp:
if tf.numLines > 1 {
e.SetHandled()
tf.shiftSelect(e)
tf.cursorUp(1)
}
case keymap.Home:
e.SetHandled()
tf.shiftSelect(e)
tf.cancelComplete()
tf.cursorStart()
case keymap.End:
e.SetHandled()
tf.shiftSelect(e)
tf.cancelComplete()
tf.cursorEnd()
case keymap.SelectMode:
e.SetHandled()
tf.cancelComplete()
tf.selectModeToggle()
case keymap.CancelSelect:
e.SetHandled()
tf.cancelComplete()
tf.selectReset()
case keymap.SelectAll:
e.SetHandled()
tf.cancelComplete()
tf.selectAll()
case keymap.Copy:
e.SetHandled()
tf.cancelComplete()
tf.copy()
}
if tf.IsReadOnly() || e.IsHandled() {
return
}
switch kf {
case keymap.Enter:
fallthrough
case keymap.FocusNext: // we process tab to make it EditDone as opposed to other ways of losing focus
e.SetHandled()
tf.cancelComplete()
tf.editDone()
tf.focusNext()
case keymap.Accept: // ctrl+enter
e.SetHandled()
tf.cancelComplete()
tf.editDone()
case keymap.FocusPrev:
e.SetHandled()
tf.cancelComplete()
tf.editDone()
tf.focusPrev()
case keymap.Abort: // esc
e.SetHandled()
tf.cancelComplete()
tf.revert()
// tf.FocusChanged(FocusInactive)
case keymap.Backspace:
e.SetHandled()
tf.saveUndo()
tf.cursorBackspace(1)
tf.offerComplete()
tf.Send(events.Input, e)
case keymap.Kill:
e.SetHandled()
tf.cancelComplete()
tf.cursorKill()
tf.Send(events.Input, e)
case keymap.Delete:
e.SetHandled()
tf.saveUndo()
tf.cursorDelete(1)
tf.offerComplete()
tf.Send(events.Input, e)
case keymap.BackspaceWord:
e.SetHandled()
tf.saveUndo()
tf.cursorBackspaceWord(1)
tf.offerComplete()
tf.Send(events.Input, e)
case keymap.DeleteWord:
e.SetHandled()
tf.saveUndo()
tf.cursorDeleteWord(1)
tf.offerComplete()
tf.Send(events.Input, e)
case keymap.Cut:
e.SetHandled()
tf.saveUndo()
tf.cancelComplete()
tf.cut()
tf.Send(events.Input, e)
case keymap.Paste:
e.SetHandled()
tf.saveUndo()
tf.cancelComplete()
tf.paste()
tf.Send(events.Input, e)
case keymap.Undo:
e.SetHandled()
tf.undo()
case keymap.Redo:
e.SetHandled()
tf.redo()
case keymap.Complete:
e.SetHandled()
tf.offerComplete()
case keymap.None:
if unicode.IsPrint(e.KeyRune()) {
if !e.HasAnyModifier(key.Control, key.Meta) {
e.SetHandled()
tf.saveUndo()
tf.insertAtCursor(string(e.KeyRune()))
if e.KeyRune() == ' ' {
tf.cancelComplete()
} else {
tf.offerComplete()
}
tf.Send(events.Input, e)
}
}
}
})
tf.OnFocus(func(e events.Event) {
if tf.IsReadOnly() {
e.SetHandled()
}
})
tf.OnFocusLost(func(e events.Event) {
if tf.IsReadOnly() {
e.SetHandled()
return
}
tf.editDone()
})
}
func (tf *TextField) Style() {
tf.WidgetBase.Style()
tf.CursorWidth.ToDots(&tf.Styles.UnitContext)
}
func (tf *TextField) configTextSize(sz math32.Vector2) math32.Vector2 {
st := &tf.Styles
txs := &st.Text
fs := st.FontRender()
st.Font = paint.OpenFont(fs, &st.UnitContext)
txt := tf.editText
if tf.NoEcho {
txt = concealDots(len(tf.editText))
}
align, alignV := txs.Align, txs.AlignV
txs.Align, txs.AlignV = styles.Start, styles.Start // only works with this
tf.renderAll.SetRunes(txt, fs, &st.UnitContext, txs, true, 0, 0)
tf.renderAll.Layout(txs, fs, &st.UnitContext, sz)
txs.Align, txs.AlignV = align, alignV
rsz := tf.renderAll.BBox.Size().Ceil()
return rsz
}
func (tf *TextField) iconsSize() math32.Vector2 {
var sz math32.Vector2
if lead := tf.leadingIconButton; lead != nil {
sz.X += lead.Geom.Size.Actual.Total.X
}
if trail := tf.trailingIconButton; trail != nil {
sz.X += trail.Geom.Size.Actual.Total.X
}
return sz
}
func (tf *TextField) SizeUp() {
tf.Frame.SizeUp()
tmptxt := tf.editText
if len(tf.text) == 0 && len(tf.Placeholder) > 0 {
tf.editText = []rune(tf.Placeholder)
} else {
tf.editText = []rune(tf.text)
}
tf.startPos = 0
tf.endPos = len(tf.editText)
sz := &tf.Geom.Size
icsz := tf.iconsSize()
availSz := sz.Actual.Content.Sub(icsz)
var rsz math32.Vector2
if tf.hasWordWrap() {
rsz = tf.configTextSize(availSz) // TextWrapSizeEstimate(availSz, len(tf.EditTxt), &tf.Styles.Font))
} else {
rsz = tf.configTextSize(availSz)
}
rsz.SetAdd(icsz)
sz.FitSizeMax(&sz.Actual.Content, rsz)
sz.setTotalFromContent(&sz.Actual)
tf.fontHeight = tf.Styles.Font.Face.Metrics.Height
tf.editText = tmptxt
if DebugSettings.LayoutTrace {
fmt.Println(tf, "TextField SizeUp:", rsz, "Actual:", sz.Actual.Content)
}
}
func (tf *TextField) SizeDown(iter int) bool {
if !tf.hasWordWrap() {
return tf.Frame.SizeDown(iter)
}
sz := &tf.Geom.Size
pgrow, _ := tf.growToAllocSize(sz.Actual.Content, sz.Alloc.Content) // key to grow
icsz := tf.iconsSize()
prevContent := sz.Actual.Content
availSz := pgrow.Sub(icsz)
rsz := tf.configTextSize(availSz)
rsz.SetAdd(icsz)
// start over so we don't reflect hysteresis of prior guess
sz.setInitContentMin(tf.Styles.Min.Dots().Ceil())
sz.FitSizeMax(&sz.Actual.Content, rsz)
sz.setTotalFromContent(&sz.Actual)
chg := prevContent != sz.Actual.Content
if chg {
if DebugSettings.LayoutTrace {
fmt.Println(tf, "TextField Size Changed:", sz.Actual.Content, "was:", prevContent)
}
}
sdp := tf.Frame.SizeDown(iter)
return chg || sdp
}
func (tf *TextField) ApplyScenePos() {
tf.Frame.ApplyScenePos()
tf.setEffPosAndSize()
}
// setEffPosAndSize sets the effective position and size of
// the textfield based on its base position and size
// and its icons or lack thereof
func (tf *TextField) setEffPosAndSize() {
sz := tf.Geom.Size.Actual.Content
pos := tf.Geom.Pos.Content
if lead := tf.leadingIconButton; lead != nil {
pos.X += lead.Geom.Size.Actual.Total.X
sz.X -= lead.Geom.Size.Actual.Total.X
}
if trail := tf.trailingIconButton; trail != nil {
sz.X -= trail.Geom.Size.Actual.Total.X
}
tf.numLines = len(tf.renderAll.Spans)
if tf.numLines <= 1 {
pos.Y += 0.5 * (sz.Y - tf.fontHeight) // center
}
tf.effSize = sz.Ceil()
tf.effPos = pos.Ceil()
}
func (tf *TextField) Render() {
defer func() {
if tf.IsReadOnly() {
return
}
if tf.StateIs(states.Focused) {
tf.startCursor()
} else {
tf.stopCursor()
}
}()
pc := &tf.Scene.PaintContext
st := &tf.Styles
tf.autoScroll() // inits paint with our style
fs := st.FontRender()
txs := &st.Text
st.Font = paint.OpenFont(fs, &st.UnitContext)
tf.RenderStandardBox()
if tf.startPos < 0 || tf.endPos > len(tf.editText) {
return
}
cur := tf.editText[tf.startPos:tf.endPos]
tf.renderSelect()
pos := tf.effPos
prevColor := st.Color
if len(tf.editText) == 0 && len(tf.Placeholder) > 0 {
st.Color = tf.PlaceholderColor
fs = st.FontRender() // need to update
cur = []rune(tf.Placeholder)
} else if tf.NoEcho {
cur = concealDots(len(cur))
}
sz := &tf.Geom.Size
icsz := tf.iconsSize()
availSz := sz.Actual.Content.Sub(icsz)
tf.renderVisible.SetRunes(cur, fs, &st.UnitContext, &st.Text, true, 0, 0)
tf.renderVisible.Layout(txs, fs, &st.UnitContext, availSz)
tf.renderVisible.Render(pc, pos)
st.Color = prevColor
}
// IsWordBreak defines what counts as a word break for the purposes of selecting words.
// r1 is the rune in question, r2 is the rune past r1 in the direction you are moving.
// Pass -1 for r2 if there is no rune past r1.
func IsWordBreak(r1, r2 rune) bool {
if r2 == -1 {
if unicode.IsSpace(r1) || unicode.IsSymbol(r1) || unicode.IsPunct(r1) {
return true
}
return false
}
if unicode.IsSpace(r1) || unicode.IsSymbol(r1) {
return true
}
if unicode.IsPunct(r1) && r1 != rune('\'') {
return true
}
if unicode.IsPunct(r1) && r1 == rune('\'') {
if unicode.IsSpace(r2) || unicode.IsSymbol(r2) || unicode.IsPunct(r2) {
return true
}
return false
}
return false
}
// concealDots creates an n-length []rune of bullet characters.
func concealDots(n int) []rune {
dots := make([]rune, n)
for i := range dots {
dots[i] = '•'
}
return dots
}
// 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 (
"reflect"
"strconv"
"time"
"cogentcore.org/core/colors"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
)
// TimePicker is a widget for picking a time.
type TimePicker struct {
Frame
// Time is the time that we are viewing.
Time time.Time
// the raw input hour
hour int
// whether we are in pm mode (so we have to add 12h to everything)
pm bool
}
func (tp *TimePicker) WidgetValue() any { return &tp.Time }
func (tp *TimePicker) Init() {
tp.Frame.Init()
spinnerInit := func(w *Spinner) {
w.Styler(func(s *styles.Style) {
s.Font.Size.Dp(57)
s.Min.X.Ch(7)
})
buttonInit := func(w *Button) {
tree.AddChildInit(w, "icon", func(w *Icon) {
w.Styler(func(s *styles.Style) {
s.Font.Size.Dp(32)
})
})
}
tree.AddChildInit(w, "lead-icon", buttonInit)
tree.AddChildInit(w, "trail-icon", buttonInit)
}
tree.AddChild(tp, func(w *Spinner) {
spinnerInit(w)
w.SetStep(1).SetEnforceStep(true)
w.Updater(func() {
if SystemSettings.Clock24 {
tp.hour = tp.Time.Hour()
w.SetMax(24).SetMin(0)
} else {
tp.hour = tp.Time.Hour() % 12
if tp.hour == 0 {
tp.hour = 12
}
w.SetMax(12).SetMin(1)
}
w.SetValue(float32(tp.hour))
})
w.OnChange(func(e events.Event) {
hr := int(w.Value)
if hr == 12 && !SystemSettings.Clock24 {
hr = 0
}
tp.hour = hr
if tp.pm {
// only add to local variable
hr += 12
}
// we set our hour and keep everything else
tt := tp.Time
tp.Time = time.Date(tt.Year(), tt.Month(), tt.Day(), hr, tt.Minute(), tt.Second(), tt.Nanosecond(), tt.Location())
tp.SendChange()
})
})
tree.AddChild(tp, func(w *Text) {
w.SetType(TextDisplayLarge).SetText(":")
w.Styler(func(s *styles.Style) {
s.SetTextWrap(false)
s.Min.X.Ch(1)
})
})
tree.AddChild(tp, func(w *Spinner) {
spinnerInit(w)
w.SetStep(1).SetEnforceStep(true).
SetMin(0).SetMax(60).SetFormat("%02d")
w.Updater(func() {
w.SetValue(float32(tp.Time.Minute()))
})
w.OnChange(func(e events.Event) {
// we set our minute and keep everything else
tt := tp.Time
tp.Time = time.Date(tt.Year(), tt.Month(), tt.Day(), tt.Hour(), int(w.Value), tt.Second(), tt.Nanosecond(), tt.Location())
tp.SendChange()
})
})
tp.Maker(func(p *tree.Plan) {
if !SystemSettings.Clock24 {
tree.Add(p, func(w *Switches) {
w.SetMutex(true).SetAllowNone(false).SetType(SwitchSegmentedButton).SetItems(SwitchItem{Value: "AM"}, SwitchItem{Value: "PM"})
tp.pm = tp.Time.Hour() >= 12
w.Styler(func(s *styles.Style) {
s.Direction = styles.Column
})
w.Updater(func() {
if tp.pm {
w.SelectValue("PM")
} else {
w.SelectValue("AM")
}
})
w.OnChange(func(e events.Event) {
si := w.SelectedItem()
tt := tp.Time
if tp.hour == 12 {
tp.hour = 0
}
switch si.Value {
case "AM":
tp.pm = false
tp.Time = time.Date(tt.Year(), tt.Month(), tt.Day(), tp.hour, tt.Minute(), tt.Second(), tt.Nanosecond(), tt.Location())
case "PM":
tp.pm = true
tp.Time = time.Date(tt.Year(), tt.Month(), tt.Day(), tp.hour+12, tt.Minute(), tt.Second(), tt.Nanosecond(), tt.Location())
}
tp.SendChange()
})
})
}
})
}
var shortMonths = []string{"Jan", "Feb", "Apr", "Mar", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
// DatePicker is a widget for picking a date.
type DatePicker struct {
Frame
// Time is the time that we are viewing.
Time time.Time
// getTime converts the given calendar grid index to its corresponding time.
// We must store this logic in a closure so that it can always be recomputed
// correctly in the inner closures of the grid maker; otherwise, the local
// variables needed would be stale.
getTime func(i int) time.Time
// som is the start of the month (must be set here to avoid stale variables).
som time.Time
}
// setTime sets the source time and updates the picker.
func (dp *DatePicker) setTime(tim time.Time) {
dp.SetTime(tim).UpdateChange()
}
func (dp *DatePicker) Init() {
dp.Frame.Init()
dp.Styler(func(s *styles.Style) {
s.Direction = styles.Column
})
tree.AddChild(dp, func(w *Frame) {
w.Styler(func(s *styles.Style) {
s.Gap.Zero()
})
arrowStyle := func(s *styles.Style) {
s.Padding.SetHorizontal(units.Dp(12))
s.Color = colors.Scheme.OnSurfaceVariant
}
tree.AddChild(w, func(w *Button) {
w.SetType(ButtonAction).SetIcon(icons.NavigateBefore)
w.OnClick(func(e events.Event) {
dp.setTime(dp.Time.AddDate(0, -1, 0))
})
w.Styler(arrowStyle)
})
tree.AddChild(w, func(w *Chooser) {
sms := make([]ChooserItem, len(shortMonths))
for i, sm := range shortMonths {
sms[i] = ChooserItem{Value: sm}
}
w.SetItems(sms...)
w.Updater(func() {
w.SetCurrentIndex(int(dp.Time.Month() - 1))
})
w.OnChange(func(e events.Event) {
// set our month
dp.setTime(dp.Time.AddDate(0, w.CurrentIndex+1-int(dp.Time.Month()), 0))
})
})
tree.AddChild(w, func(w *Button) {
w.SetType(ButtonAction).SetIcon(icons.NavigateNext)
w.OnClick(func(e events.Event) {
dp.setTime(dp.Time.AddDate(0, 1, 0))
})
w.Styler(arrowStyle)
})
tree.AddChild(w, func(w *Button) {
w.SetType(ButtonAction).SetIcon(icons.NavigateBefore)
w.OnClick(func(e events.Event) {
dp.setTime(dp.Time.AddDate(-1, 0, 0))
})
w.Styler(arrowStyle)
})
tree.AddChild(w, func(w *Chooser) {
w.Updater(func() {
yr := dp.Time.Year()
var yrs []ChooserItem
// we go 100 in each direction from the current year
for i := yr - 100; i <= yr+100; i++ {
yrs = append(yrs, ChooserItem{Value: i})
}
w.SetItems(yrs...)
w.SetCurrentValue(yr)
})
w.OnChange(func(e events.Event) {
// we are centered at current year with 100 in each direction
nyr := w.CurrentIndex + dp.Time.Year() - 100
// set our year
dp.setTime(dp.Time.AddDate(nyr-dp.Time.Year(), 0, 0))
})
})
tree.AddChild(w, func(w *Button) {
w.SetType(ButtonAction).SetIcon(icons.NavigateNext)
w.OnClick(func(e events.Event) {
dp.setTime(dp.Time.AddDate(1, 0, 0))
})
w.Styler(arrowStyle)
})
})
tree.AddChild(dp, func(w *Frame) {
w.Styler(func(s *styles.Style) {
s.Display = styles.Grid
s.Columns = 7
})
w.Maker(func(p *tree.Plan) {
// start of the month
som := dp.Time.AddDate(0, 0, -dp.Time.Day()+1)
// end of the month
eom := dp.Time.AddDate(0, 1, -dp.Time.Day())
// start of the week containing the start of the month
somw := som.AddDate(0, 0, -int(som.Weekday()))
// year day of the start of the week containing the start of the month
somwyd := somw.YearDay()
// end of the week containing the end of the month
eomw := eom.AddDate(0, 0, int(6-eom.Weekday()))
// year day of the end of the week containing the end of the month
eomwyd := eomw.YearDay()
// if we have moved up a year (happens in December),
// we add the number of days in this year
if eomw.Year() > somw.Year() {
eomwyd += time.Date(somw.Year(), 13, -1, 0, 0, 0, 0, somw.Location()).YearDay()
}
dp.getTime = func(i int) time.Time {
return somw.AddDate(0, 0, i)
}
dp.som = som
for i := range 1 + eomwyd - somwyd {
tree.AddAt(p, strconv.Itoa(i), func(w *Button) {
w.SetType(ButtonAction)
w.Updater(func() {
w.SetText(strconv.Itoa(dp.getTime(i).Day()))
})
w.OnClick(func(e events.Event) {
dp.setTime(dp.getTime(i))
})
w.Styler(func(s *styles.Style) {
s.CenterAll()
s.Min.Set(units.Dp(32))
s.Padding.Set(units.Dp(6))
dt := dp.getTime(i)
if dt.Month() != dp.som.Month() {
s.Color = colors.Scheme.OnSurfaceVariant
}
if dt.Year() == time.Now().Year() && dt.YearDay() == time.Now().YearDay() {
s.Border.Width.Set(units.Dp(1))
s.Border.Color.Set(colors.Scheme.Primary.Base)
s.Color = colors.Scheme.Primary.Base
}
if dt.Year() == dp.Time.Year() && dt.YearDay() == dp.Time.YearDay() {
s.Background = colors.Scheme.Primary.Base
s.Color = colors.Scheme.Primary.On
}
})
tree.AddChildInit(w, "text", func(w *Text) {
w.FinalUpdater(func() {
w.SetType(TextBodyLarge)
})
})
})
}
})
})
}
// TimeInput presents two text fields for editing a date and time,
// both of which can pull up corresponding picker dialogs.
type TimeInput struct {
Frame
Time time.Time
// DisplayDate is whether the date input is displayed (default true).
DisplayDate bool
// DisplayTime is whether the time input is displayed (default true).
DisplayTime bool
}
func (ti *TimeInput) WidgetValue() any { return &ti.Time }
func (ti *TimeInput) OnBind(value any, tags reflect.StructTag) {
switch tags.Get("display") {
case "date":
ti.DisplayTime = false
case "time":
ti.DisplayDate = false
}
}
func (ti *TimeInput) Init() {
ti.Frame.Init()
ti.DisplayDate = true
ti.DisplayTime = true
style := func(s *styles.Style) {
s.Min.X.Em(8)
s.Max.X.Em(10)
if ti.IsReadOnly() { // must inherit abilities when read only for table
s.Abilities = ti.Styles.Abilities
}
}
ti.Maker(func(p *tree.Plan) {
if ti.DisplayDate {
tree.Add(p, func(w *TextField) {
w.SetTooltip("The date")
w.SetLeadingIcon(icons.CalendarToday, func(e events.Event) {
d := NewBody("Select date")
dp := NewDatePicker(d).SetTime(ti.Time)
d.AddBottomBar(func(bar *Frame) {
d.AddCancel(bar)
d.AddOK(bar).OnClick(func(e events.Event) {
ti.Time = dp.Time
ti.UpdateChange()
})
})
d.RunDialog(w)
})
w.Styler(style)
w.Updater(func() {
w.SetReadOnly(ti.IsReadOnly())
w.SetText(ti.Time.Format("1/2/2006"))
})
w.SetValidator(func() error {
d, err := time.Parse("1/2/2006", w.Text())
if err != nil {
return err
}
// new date and old time
ti.Time = time.Date(d.Year(), d.Month(), d.Day(), ti.Time.Hour(), ti.Time.Minute(), ti.Time.Second(), ti.Time.Nanosecond(), ti.Time.Location())
ti.SendChange()
return nil
})
})
}
if ti.DisplayTime {
tree.Add(p, func(w *TextField) {
w.SetTooltip("The time")
w.SetLeadingIcon(icons.Schedule, func(e events.Event) {
d := NewBody("Edit time")
tp := NewTimePicker(d).SetTime(ti.Time)
d.AddBottomBar(func(bar *Frame) {
d.AddCancel(bar)
d.AddOK(bar).OnClick(func(e events.Event) {
ti.Time = tp.Time
ti.UpdateChange()
})
})
d.RunDialog(w)
})
w.Styler(style)
w.Updater(func() {
w.SetReadOnly(ti.IsReadOnly())
w.SetText(ti.Time.Format(SystemSettings.TimeFormat()))
})
w.SetValidator(func() error {
t, err := time.Parse(SystemSettings.TimeFormat(), w.Text())
if err != nil {
return err
}
// old date and new time
ti.Time = time.Date(ti.Time.Year(), ti.Time.Month(), ti.Time.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), ti.Time.Location())
ti.SendChange()
return nil
})
})
}
})
}
// DurationInput represents a [time.Duration] value with a spinner and unit chooser.
type DurationInput struct {
Frame
Duration time.Duration
// Unit is the unit of time.
Unit string
}
func (di *DurationInput) WidgetValue() any { return &di.Duration }
func (di *DurationInput) Init() {
di.Frame.Init()
tree.AddChild(di, func(w *Spinner) {
w.SetStep(1).SetPageStep(10)
w.SetTooltip("The value of time")
w.Updater(func() {
if di.Unit == "" {
di.setAutoUnit()
}
w.SetValue(float32(di.Duration) / float32(durationUnitsMap[di.Unit]))
w.SetReadOnly(di.IsReadOnly())
})
w.OnChange(func(e events.Event) {
di.Duration = time.Duration(w.Value * float32(durationUnitsMap[di.Unit]))
di.SendChange()
})
})
tree.AddChild(di, func(w *Chooser) {
Bind(&di.Unit, w)
units := make([]ChooserItem, len(durationUnits))
for i, u := range durationUnits {
units[i] = ChooserItem{Value: u}
}
w.SetItems(units...)
w.SetTooltip("The unit of time")
w.Updater(func() {
w.SetReadOnly(di.IsReadOnly())
})
w.OnChange(func(e events.Event) {
di.Update()
})
})
}
// setAutoUnit sets the [DurationInput.Unit] automatically based on the current duration.
func (di *DurationInput) setAutoUnit() {
di.Unit = durationUnits[0]
for _, u := range durationUnits {
if durationUnitsMap[u] > di.Duration {
break
}
di.Unit = u
}
}
var durationUnits = []string{
"nanoseconds",
"microseconds",
"milliseconds",
"seconds",
"minutes",
"hours",
"days",
"weeks",
"months",
"years",
}
var durationUnitsMap = map[string]time.Duration{
"nanoseconds": time.Nanosecond,
"microseconds": time.Microsecond,
"milliseconds": time.Millisecond,
"seconds": time.Second,
"minutes": time.Minute,
"hours": time.Hour,
"days": 24 * time.Hour,
"weeks": 7 * 24 * time.Hour,
"months": 30 * 24 * time.Hour,
"years": 365 * 24 * time.Hour,
}
// 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 (
"cogentcore.org/core/colors"
"cogentcore.org/core/icons"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
)
// Toolbar is a [Frame] that is useful for holding [Button]s that do things.
// It automatically moves items that do not fit into an overflow menu, and
// manages additional items that are always placed onto this overflow menu.
// Toolbars are frequently added in [Body.AddTopBar].
type Toolbar struct {
Frame
// OverflowMenus are functions for configuring the overflow menu of the
// toolbar. You can use [Toolbar.AddOverflowMenu] to add them.
// These are processed in reverse order (last in, first called)
// so that the default items are added last.
OverflowMenus []func(m *Scene) `set:"-" json:"-" xml:"-"`
// allItemsPlan has all the items, during layout sizing
allItemsPlan *tree.Plan
// overflowItems are items moved from the main toolbar that will be
// shown in the overflow menu.
overflowItems []*tree.PlanItem
// overflowButton is the widget to pull up the overflow menu.
overflowButton *Button
}
// ToolbarMaker is an interface that types can implement to make a toolbar plan.
// It is automatically used when making [Value] dialogs.
type ToolbarMaker interface {
MakeToolbar(p *tree.Plan)
}
func (tb *Toolbar) Init() {
tb.Frame.Init()
ToolbarStyles(tb)
tb.FinalMaker(func(p *tree.Plan) { // must go at end
tree.AddAt(p, "overflow-menu", func(w *Button) {
ic := icons.MoreVert
if tb.Styles.Direction != styles.Row {
ic = icons.MoreHoriz
}
w.SetIcon(ic).SetTooltip("Additional menu items")
w.Updater(func() {
tb, ok := w.Parent.(*Toolbar)
if ok {
w.Menu = tb.overflowMenu
}
})
})
})
}
func (tb *Toolbar) SizeUp() {
if tb.Styles.Wrap {
tb.getOverflowButton()
tb.setOverflowMenuVisibility()
tb.Frame.SizeUp()
return
}
tb.allItemsToChildren()
tb.Frame.SizeUp()
}
func (tb *Toolbar) SizeDown(iter int) bool {
if tb.Styles.Wrap {
return tb.Frame.SizeDown(iter)
}
redo := tb.Frame.SizeDown(iter)
if iter == 0 {
return true // ensure a second pass
}
if tb.Scene.showIter > 0 {
tb.moveToOverflow()
}
return redo
}
func (tb *Toolbar) SizeFromChildren(iter int, pass LayoutPasses) math32.Vector2 {
csz := tb.Frame.SizeFromChildren(iter, pass)
if pass == SizeUpPass || (pass == SizeDownPass && iter == 0) {
dim := tb.Styles.Direction.Dim()
ovsz := tb.Styles.UnitContext.FontEm * 2
if tb.overflowButton != nil {
ovsz = tb.overflowButton.Geom.Size.Actual.Total.Dim(dim)
}
csz.SetDim(dim, ovsz) // present the minimum size initially
return csz
}
return csz
}
// allItemsToChildren moves the overflow items back to the children,
// so the full set is considered for the next layout round,
// and ensures the overflow button is made and moves it
// to the end of the list.
func (tb *Toolbar) allItemsToChildren() {
tb.overflowItems = nil
tb.allItemsPlan = &tree.Plan{}
tb.Make(tb.allItemsPlan)
np := len(tb.allItemsPlan.Children)
if tb.NumChildren() != np {
tb.Scene.RenderWidget()
tb.Update() // todo: needs one more redraw here
}
}
func (tb *Toolbar) parentSize() float32 {
ma := tb.Styles.Direction.Dim()
psz := tb.parentWidget().Geom.Size.Alloc.Content.Sub(tb.Geom.Size.Space)
avail := psz.Dim(ma)
return avail
}
func (tb *Toolbar) getOverflowButton() {
tb.overflowButton = nil
li := tb.Children[tb.NumChildren()-1]
if li == nil {
return
}
if ob, ok := li.(*Button); ok {
tb.overflowButton = ob
}
}
// moveToOverflow moves overflow out of children to the OverflowItems list
func (tb *Toolbar) moveToOverflow() {
if !tb.HasChildren() {
return
}
ma := tb.Styles.Direction.Dim()
avail := tb.parentSize()
tb.getOverflowButton()
if tb.overflowButton == nil {
return
}
ovsz := tb.overflowButton.Geom.Size.Actual.Total.Dim(ma)
avsz := avail - ovsz
sz := &tb.Geom.Size
sz.Alloc.Total.SetDim(ma, avail)
sz.setContentFromTotal(&sz.Alloc)
n := len(tb.Children)
pn := len(tb.allItemsPlan.Children)
ovidx := n - 1
hasOv := false
szsum := float32(0)
tb.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
if i >= n-1 {
return tree.Break
}
ksz := cwb.Geom.Size.Alloc.Total.Dim(ma)
szsum += ksz
if szsum > avsz {
if !hasOv {
ovidx = i
hasOv = true
}
pi := tb.allItemsPlan.Children[i]
tb.overflowItems = append(tb.overflowItems, pi)
}
return tree.Continue
})
if hasOv {
p := &tree.Plan{}
p.Children = tb.allItemsPlan.Children[:ovidx]
p.Children = append(p.Children, tb.allItemsPlan.Children[pn-1]) // ovm
p.Update(tb)
}
if len(tb.overflowItems) == 0 && len(tb.OverflowMenus) == 0 {
tb.overflowButton.SetState(true, states.Invisible)
} else {
tb.overflowButton.SetState(false, states.Invisible)
tb.overflowButton.Update()
}
tb.setOverflowMenuVisibility()
}
func (tb *Toolbar) setOverflowMenuVisibility() {
if tb.overflowButton == nil {
return
}
if len(tb.overflowItems) == 0 && len(tb.OverflowMenus) == 0 {
tb.overflowButton.SetState(true, states.Invisible)
} else {
tb.overflowButton.SetState(false, states.Invisible)
tb.overflowButton.Update()
}
}
// overflowMenu adds the overflow menu to the given Scene.
func (tb *Toolbar) overflowMenu(m *Scene) {
nm := len(tb.OverflowMenus)
ni := len(tb.overflowItems)
if ni > 0 {
p := &tree.Plan{}
p.Children = tb.overflowItems
p.Update(m)
if nm > 1 { // default includes sep
NewSeparator(m)
}
}
// reverse order so defaults are last
for i := nm - 1; i >= 0; i-- {
fn := tb.OverflowMenus[i]
fn(m)
}
}
// AddOverflowMenu adds the given menu function to the overflow menu list.
// These functions are called in reverse order such that the last added function
// is called first when constructing the menu.
func (tb *Toolbar) AddOverflowMenu(fun func(m *Scene)) {
tb.OverflowMenus = append(tb.OverflowMenus, fun)
}
// ToolbarStyles styles the given widget to have standard toolbar styling.
func ToolbarStyles(w Widget) {
wb := w.AsWidget()
wb.Styler(func(s *styles.Style) {
s.Border.Radius = styles.BorderRadiusFull
s.Background = colors.Scheme.SurfaceContainer
s.Gap.Zero()
s.Align.Items = styles.Center
if len(wb.Children) == 0 {
// we must not render toolbars with no children
s.Display = styles.DisplayNone
} else {
s.Display = styles.Flex
}
})
wb.FinalStyler(func(s *styles.Style) {
if s.Direction == styles.Row {
s.Grow.Set(1, 0)
s.Padding.SetHorizontal(units.Dp(16))
} else {
s.Grow.Set(0, 1)
s.Padding.SetVertical(units.Dp(16))
}
})
wb.SetOnChildAdded(func(n tree.Node) {
if bt := AsButton(n); bt != nil {
bt.Type = ButtonAction
return
}
if sp, ok := n.(*Separator); ok {
sp.Styler(func(s *styles.Style) {
s.Direction = wb.Styles.Direction.Other()
})
}
})
}
// 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 (
"image"
"cogentcore.org/core/colors"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
)
// DefaultTooltipPos returns the default position for the tooltip
// for this widget in window coordinates using the window bounding box.
func (wb *WidgetBase) DefaultTooltipPos() image.Point {
bb := wb.winBBox()
pos := bb.Min
pos.X += (bb.Max.X - bb.Min.X) / 2 // center on X
// top of Y
return pos
}
// newTooltipFromScene returns a new Tooltip stage with given scene contents,
// in connection with given widget (which provides key context).
// Make further configuration choices using Set* methods, which
// can be chained directly after the New call.
// Use an appropriate Run call at the end to start the Stage running.
func newTooltipFromScene(sc *Scene, ctx Widget) *Stage {
return NewPopupStage(TooltipStage, sc, ctx)
}
// newTooltip returns a new tooltip stage displaying the given tooltip text
// for the given widget based at the given window-level position, with the size
// defaulting to the size of the widget.
func newTooltip(w Widget, tooltip string, pos image.Point) *Stage {
return newTooltipTextSize(w, tooltip, pos, w.AsWidget().winBBox().Size())
}
// newTooltipTextSize returns a new tooltip stage displaying the given tooltip text
// for the given widget at the given window-level position with the given size.
func newTooltipTextSize(w Widget, tooltip string, pos, sz image.Point) *Stage {
return newTooltipFromScene(newTooltipScene(w, tooltip, pos, sz), w)
}
// newTooltipScene returns a new tooltip scene for the given widget with the
// given tooltip based on the given context position and context size.
func newTooltipScene(w Widget, tooltip string, pos, sz image.Point) *Scene {
sc := NewScene(w.AsTree().Name + "-tooltip")
// tooltip positioning uses the original scene geom as the context values
sc.SceneGeom.Pos = pos
sc.SceneGeom.Size = sz // used for positioning if needed
sc.Styler(func(s *styles.Style) {
s.Border.Radius = styles.BorderRadiusExtraSmall
s.Grow.Set(1, 1)
s.Overflow.Set(styles.OverflowVisible) // key for avoiding sizing errors when re-rendering with small pref size
s.Padding.Set(units.Dp(8))
s.Background = colors.Scheme.InverseSurface
s.Color = colors.Scheme.InverseOnSurface
s.BoxShadow = styles.BoxShadow1()
})
NewText(sc).SetType(TextBodyMedium).SetText(tooltip).
Styler(func(s *styles.Style) {
s.SetTextWrap(true)
s.Max.X.Em(20)
})
return sc
}
// 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 core
import (
"bytes"
"fmt"
"image"
"log/slog"
"strings"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/base/iox/jsonx"
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
)
// Treer is an interface for [Tree] types
// providing access to the base [Tree] and
// overridable method hooks for actions taken on the [Tree],
// including OnOpen, OnClose, etc.
type Treer interface { //types:add
Widget
// AsTree returns the base [Tree] for this node.
AsCoreTree() *Tree
// CanOpen returns true if the node is able to open.
// By default it checks HasChildren(), but could check other properties
// to perform lazy building of the tree.
CanOpen() bool
// OnOpen is called when a node is opened.
// The base version does nothing.
OnOpen()
// OnClose is called when a node is closed
// The base version does nothing.
OnClose()
// The following are all tree editing functions:
MimeData(md *mimedata.Mimes)
Cut()
Copy()
Paste()
DragDrop(e events.Event)
DropDeleteSource(e events.Event)
}
// AsTree returns the given value as a value of type [Tree] if the type
// of the given value embeds [Tree], or nil otherwise.
func AsTree(n tree.Node) *Tree {
if t, ok := n.(Treer); ok {
return t.AsCoreTree()
}
return nil
}
// note: see treesync.go for all the SyncNode mode specific
// functions.
// Tree provides a graphical representation of a tree structure,
// providing full navigation and manipulation abilities.
//
// It does not handle layout by itself, so if you want it to scroll
// separately from the rest of the surrounding context, you must
// place it in a [Frame].
//
// If the [Tree.SyncNode] field is non-nil, typically via the
// [Tree.SyncTree] method, then the Tree mirrors another
// tree structure, and tree editing functions apply to
// the source tree first, and then to the Tree by sync.
//
// Otherwise, data can be directly encoded in a Tree
// derived type, to represent any kind of tree structure
// and associated data.
//
// Standard [events.Event]s are sent to any listeners, including
// [events.Select], [events.Change], and [events.DoubleClick].
// The selected nodes are in the root [Tree.SelectedNodes] list.
type Tree struct {
WidgetBase
// SyncNode, if non-nil, is the [tree.Node] that this widget is
// viewing in the tree (the source). It should be set using
// [Tree.SyncTree].
SyncNode tree.Node `set:"-" copier:"-" json:"-" xml:"-"`
// Text is the text to display for the tree item label, which automatically
// defaults to the [tree.Node.Name] of the tree node. It has no effect
// if [Tree.SyncNode] is non-nil.
Text string
// Icon is an optional icon displayed to the the left of the text label.
Icon icons.Icon
// IconOpen is the icon to use for an open (expanded) branch;
// it defaults to [icons.KeyboardArrowDown].
IconOpen icons.Icon
// IconClosed is the icon to use for a closed (collapsed) branch;
// it defaults to [icons.KeyboardArrowRight].
IconClosed icons.Icon
// IconLeaf is the icon to use for a terminal node branch that has no children;
// it defaults to [icons.Blank].
IconLeaf icons.Icon
// TreeInit is a function that can be set on the root node that is called
// with each child tree node when it is initialized. It is only
// called with the root node itself in [Tree.SetTreeInit], so you
// should typically call that instead of setting this directly.
TreeInit func(tr *Tree) `set:"-"`
// Indent is the amount to indent children relative to this node.
// It should be set in a Styler like all other style properties.
Indent units.Value `copier:"-" json:"-" xml:"-"`
// OpenDepth is the depth for nodes be initialized as open (default 4).
// Nodes beyond this depth will be initialized as closed.
OpenDepth int `copier:"-" json:"-" xml:"-"`
// Closed is whether this tree node is currently toggled closed
// (children not visible).
Closed bool
// SelectMode, when set on the root node, determines whether keyboard movements should update selection.
SelectMode bool
// Computed fields:
// linear index of this node within the entire tree.
// updated on full rebuilds and may sometimes be off,
// but close enough for expected uses
viewIndex int
// size of just this node widget.
// our alloc includes all of our children, but we only draw us.
widgetSize math32.Vector2
// root is the cached root of the tree. It is automatically set.
root *Tree
// SelectedNodes holds the currently selected nodes.
// It is only set on the root node. See [Tree.GetSelectedNodes]
// for a version that also works on non-root nodes.
SelectedNodes []Treer `copier:"-" json:"-" xml:"-" edit:"-" set:"-"`
// actStateLayer is the actual state layer of the tree, which
// should be used when rendering it and its parts (but not its children).
// the reason that it exists is so that the children of the tree
// (other trees) do not inherit its stateful background color, as
// that does not look good.
actStateLayer float32
// inOpen is set in the Open method to prevent recursive opening for lazy-open nodes.
inOpen bool
// Branch is the branch widget that is used to open and close the tree node.
Branch *Switch `json:"-" xml:"-" copier:"-" set:"-" display:"-"`
}
// AsCoreTree satisfies the [Treer] interface.
func (tr *Tree) AsCoreTree() *Tree {
return tr
}
// rootSetViewIndex sets the [Tree.root] and [Tree.viewIndex] for all nodes.
// It returns the total number of leaves in the tree.
func (tr *Tree) rootSetViewIndex() int {
idx := 0
tr.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
tvn := AsTree(cw)
if tvn != nil {
tvn.viewIndex = idx
tvn.root = tr
idx++
}
return tree.Continue
})
return idx
}
func (tr *Tree) Init() {
tr.WidgetBase.Init()
tr.AddContextMenu(tr.contextMenu)
tr.IconOpen = icons.KeyboardArrowDown
tr.IconClosed = icons.KeyboardArrowRight
tr.IconLeaf = icons.Blank
tr.OpenDepth = 4
tr.Styler(func(s *styles.Style) {
// our parts are draggable and droppable, not us ourself
s.SetAbilities(true, abilities.Activatable, abilities.Focusable, abilities.Selectable, abilities.Hoverable)
tr.Indent.Em(1)
s.Border.Style.Set(styles.BorderNone)
s.Border.Radius = styles.BorderRadiusFull
s.MaxBorder = s.Border
// s.Border.Width.Left.SetDp(1)
// s.Border.Color.Left = colors.Scheme.OutlineVariant
s.Margin.Zero()
s.Padding.Left.Dp(ConstantSpacing(4))
s.Padding.SetVertical(units.Dp(4))
s.Padding.Right.Zero()
s.Text.Align = styles.Start
// need to copy over to actual and then clear styles one
if s.Is(states.Selected) {
// render handles manually, similar to with actStateLayer
s.Background = nil
} else {
s.Color = colors.Scheme.OnSurface
}
})
tr.FinalStyler(func(s *styles.Style) {
tr.actStateLayer = s.StateLayer
s.StateLayer = 0
})
// We let the parts handle our state
// so that we only get it when we are doing
// something with this tree specifically,
// not with any of our children (see OnChildAdded).
// we only need to handle the starting ones here,
// as the other ones will just set the state to
// false, which it already is.
tr.On(events.MouseEnter, func(e events.Event) { e.SetHandled() })
tr.On(events.MouseLeave, func(e events.Event) { e.SetHandled() })
tr.On(events.MouseDown, func(e events.Event) { e.SetHandled() })
tr.OnClick(func(e events.Event) { e.SetHandled() })
tr.On(events.DragStart, func(e events.Event) { e.SetHandled() })
tr.On(events.DragEnter, func(e events.Event) { e.SetHandled() })
tr.On(events.DragLeave, func(e events.Event) { e.SetHandled() })
tr.On(events.Drop, func(e events.Event) { e.SetHandled() })
tr.On(events.DropDeleteSource, func(e events.Event) { e.SetHandled() })
tr.On(events.KeyChord, func(e events.Event) {
kf := keymap.Of(e.KeyChord())
selMode := events.SelectModeBits(e.Modifiers())
if DebugSettings.KeyEventTrace {
slog.Info("Tree KeyInput", "widget", tr, "keyFunction", kf, "selMode", selMode)
}
if selMode == events.SelectOne {
if tr.SelectMode {
selMode = events.ExtendContinuous
}
}
tri := tr.This.(Treer)
// first all the keys that work for ReadOnly and active
switch kf {
case keymap.CancelSelect:
tr.UnselectAll()
tr.SetSelectMode(false)
e.SetHandled()
case keymap.MoveRight:
tr.Open()
e.SetHandled()
case keymap.MoveLeft:
tr.Close()
e.SetHandled()
case keymap.MoveDown:
tr.moveDownEvent(selMode)
e.SetHandled()
case keymap.MoveUp:
tr.moveUpEvent(selMode)
e.SetHandled()
case keymap.PageUp:
tr.movePageUpEvent(selMode)
e.SetHandled()
case keymap.PageDown:
tr.movePageDownEvent(selMode)
e.SetHandled()
case keymap.Home:
tr.moveHomeEvent(selMode)
e.SetHandled()
case keymap.End:
tr.moveEndEvent(selMode)
e.SetHandled()
case keymap.SelectMode:
tr.SelectMode = !tr.SelectMode
e.SetHandled()
case keymap.SelectAll:
tr.SelectAll()
e.SetHandled()
case keymap.Enter:
tr.ToggleClose()
e.SetHandled()
case keymap.Copy:
tri.Copy()
e.SetHandled()
}
if !tr.rootIsReadOnly() && !e.IsHandled() {
switch kf {
case keymap.Delete:
tr.DeleteNode()
e.SetHandled()
case keymap.Duplicate:
tr.Duplicate()
e.SetHandled()
case keymap.Insert:
tr.InsertBefore()
e.SetHandled()
case keymap.InsertAfter:
tr.InsertAfter()
e.SetHandled()
case keymap.Cut:
tri.Cut()
e.SetHandled()
case keymap.Paste:
tri.Paste()
e.SetHandled()
}
}
})
parts := tr.newParts()
tri := tr.This.(Treer)
parts.Styler(func(s *styles.Style) {
s.Cursor = cursors.Pointer
s.SetAbilities(true, abilities.Activatable, abilities.Focusable, abilities.Selectable, abilities.Hoverable, abilities.DoubleClickable)
s.SetAbilities(!tr.IsReadOnly() && !tr.rootIsReadOnly(), abilities.Draggable, abilities.Droppable)
s.Gap.X.Em(0.1)
s.Padding.Zero()
// we manually inherit our state layer from the tree state
// layer so that the parts get it but not the other trees
s.StateLayer = tr.actStateLayer
})
parts.AsWidget().FinalStyler(func(s *styles.Style) {
s.Grow.Set(1, 0)
})
// we let the parts handle our state
// so that we only get it when we are doing
// something with this tree specifically,
// not with any of our children (see HandleTreeMouse)
parts.On(events.MouseEnter, func(e events.Event) {
tr.SetState(true, states.Hovered)
tr.Style()
tr.NeedsRender()
e.SetHandled()
})
parts.On(events.MouseLeave, func(e events.Event) {
tr.SetState(false, states.Hovered)
tr.Style()
tr.NeedsRender()
e.SetHandled()
})
parts.On(events.MouseDown, func(e events.Event) {
tr.SetState(true, states.Active)
tr.Style()
tr.NeedsRender()
e.SetHandled()
})
parts.On(events.MouseUp, func(e events.Event) {
tr.SetState(false, states.Active)
tr.Style()
tr.NeedsRender()
e.SetHandled()
})
parts.OnClick(func(e events.Event) {
tr.SelectEvent(e.SelectMode())
e.SetHandled()
})
parts.AsWidget().OnDoubleClick(func(e events.Event) {
if tr.HasChildren() {
tr.ToggleClose()
}
})
parts.On(events.DragStart, func(e events.Event) {
tr.dragStart(e)
})
parts.On(events.DragEnter, func(e events.Event) {
tr.SetState(true, states.DragHovered)
tr.Style()
tr.NeedsRender()
e.SetHandled()
})
parts.On(events.DragLeave, func(e events.Event) {
tr.SetState(false, states.DragHovered)
tr.Style()
tr.NeedsRender()
e.SetHandled()
})
parts.On(events.Drop, func(e events.Event) {
tri.DragDrop(e)
})
parts.On(events.DropDeleteSource, func(e events.Event) {
tri.DropDeleteSource(e)
})
// the context menu events will get sent to the parts, so it
// needs to intercept them and send them up
parts.On(events.ContextMenu, func(e events.Event) {
sels := tr.GetSelectedNodes()
if len(sels) == 0 {
tr.SelectEvent(e.SelectMode())
}
tr.ShowContextMenu(e)
})
tree.AddChildAt(parts, "branch", func(w *Switch) {
tr.Branch = w
w.SetType(SwitchCheckbox)
w.SetIconOn(tr.IconOpen).SetIconOff(tr.IconClosed).SetIconIndeterminate(tr.IconLeaf)
w.Styler(func(s *styles.Style) {
s.SetAbilities(false, abilities.Focusable)
// parent will handle our cursor
s.Cursor = cursors.None
s.Color = colors.Scheme.Primary.Base
s.Padding.Zero()
s.Align.Self = styles.Center
if !w.StateIs(states.Indeterminate) {
// we amplify any state layer we receiver so that it is clear
// we are receiving it, not just our parent
s.StateLayer *= 3
} else {
// no abilities and state layer for indeterminate because
// they are not interactive
s.Abilities = 0
s.StateLayer = 0
}
})
w.OnClick(func(e events.Event) {
if w.IsChecked() && !w.StateIs(states.Indeterminate) {
if !tr.Closed {
tr.Close()
}
} else {
if tr.Closed {
tr.Open()
}
}
})
w.Updater(func() {
if tr.This.(Treer).CanOpen() {
tr.setBranchState()
}
})
})
parts.Maker(func(p *tree.Plan) {
if tr.Icon.IsSet() {
tree.AddAt(p, "icon", func(w *Icon) {
w.Styler(func(s *styles.Style) {
s.Font.Size.Dp(24)
s.Color = colors.Scheme.Primary.Base
s.Align.Self = styles.Center
})
w.Updater(func() {
w.SetIcon(tr.Icon)
})
})
}
})
tree.AddChildAt(parts, "text", func(w *Text) {
w.Styler(func(s *styles.Style) {
s.SetNonSelectable()
s.SetTextWrap(false)
s.Min.X.Ch(16)
s.Min.Y.Em(1.2)
})
w.Updater(func() {
w.SetText(tr.Label())
})
})
}
func (tr *Tree) OnAdd() {
tr.WidgetBase.OnAdd()
tr.Text = tr.Name
if ptv := AsTree(tr.Parent); ptv != nil {
tr.root = ptv.root
tr.IconOpen = ptv.IconOpen
tr.IconClosed = ptv.IconClosed
tr.IconLeaf = ptv.IconLeaf
} else {
tr.root = tr
}
if tr.root.TreeInit != nil {
tr.root.TreeInit(tr)
}
}
// SetTreeInit sets the [Tree.TreeInit]:
// TreeInit is a function that can be set on the root node that is called
// with each child tree node when it is initialized. It is only
// called with the root node itself in this function, SetTreeInit, so you
// should typically call this instead of setting it directly.
func (tr *Tree) SetTreeInit(v func(tr *Tree)) *Tree {
tr.TreeInit = v
v(tr)
return tr
}
// rootIsReadOnly returns the ReadOnly status of the root node,
// which is what controls the functional inactivity of the tree
// if individual nodes are ReadOnly that only affects display typically.
func (tr *Tree) rootIsReadOnly() bool {
if tr.root == nil {
return true
}
return tr.root.IsReadOnly()
}
func (tr *Tree) Style() {
if !tr.HasChildren() {
tr.SetClosed(true)
}
tr.WidgetBase.Style()
tr.Indent.ToDots(&tr.Styles.UnitContext)
tr.Indent.Dots = math32.Ceil(tr.Indent.Dots)
}
func (tr *Tree) setBranchState() {
br := tr.Branch
if br == nil {
return
}
switch {
case !tr.This.(Treer).CanOpen():
br.SetState(true, states.Indeterminate)
case tr.Closed:
br.SetState(false, states.Indeterminate)
br.SetState(false, states.Checked)
br.NeedsRender()
default:
br.SetState(false, states.Indeterminate)
br.SetState(true, states.Checked)
br.NeedsRender()
}
}
// Tree is tricky for alloc because it is both a layout
// of its children but has to maintain its own bbox for its own widget.
func (tr *Tree) SizeUp() {
tr.WidgetBase.SizeUp()
tr.widgetSize = tr.Geom.Size.Actual.Total
h := tr.widgetSize.Y
w := tr.widgetSize.X
if tr.root.This == tr.This { // do it every time on root
tr.root.rootSetViewIndex()
}
if !tr.Closed {
// we layout children under us
tr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cw.SizeUp()
h += cwb.Geom.Size.Actual.Total.Y
kw := cwb.Geom.Size.Actual.Total.X
if math32.IsNaN(kw) { // somehow getting a nan
slog.Error("Tree, node width is NaN", "node:", cwb)
} else {
w = max(w, tr.Indent.Dots+kw)
}
// fmt.Println(kwb, w, h)
return tree.Continue
})
}
sz := &tr.Geom.Size
sz.Actual.Content = math32.Vec2(w, h)
sz.setTotalFromContent(&sz.Actual)
sz.Alloc = sz.Actual // need allocation to match!
tr.widgetSize.X = w // stretch
}
func (tr *Tree) SizeDown(iter int) bool {
// note: key to not grab the whole allocation, as widget default does
redo := tr.sizeDownParts(iter) // give our content to parts
re := tr.sizeDownChildren(iter)
return redo || re
}
func (tr *Tree) Position() {
rn := tr.root
if rn == nil {
slog.Error("core.Tree: RootView is nil", "in node:", tr)
return
}
tr.setBranchState()
sz := &tr.Geom.Size
sz.Actual.Total.X = rn.Geom.Size.Actual.Total.X - (tr.Geom.Pos.Total.X - rn.Geom.Pos.Total.X)
sz.Actual.Content.X = sz.Actual.Total.X - sz.Space.X
tr.widgetSize.X = sz.Actual.Total.X
tr.WidgetBase.Position() // just does our parts
if !tr.Closed {
h := tr.widgetSize.Y
tr.ForWidgetChildren(func(i int, cw Widget, cwb *WidgetBase) bool {
cwb.Geom.RelPos.Y = h
cwb.Geom.RelPos.X = tr.Indent.Dots
h += cwb.Geom.Size.Actual.Total.Y
cw.Position()
return tree.Continue
})
}
}
func (tr *Tree) ApplyScenePos() {
sz := &tr.Geom.Size
if sz.Actual.Total == tr.widgetSize {
sz.setTotalFromContent(&sz.Actual) // restore after scrolling
}
tr.WidgetBase.ApplyScenePos()
tr.applyScenePosChildren()
tr.Geom.Size.Actual.Total = tr.widgetSize // key: we revert to just ourselves
}
func (tr *Tree) Render() {
pc := &tr.Scene.PaintContext
st := &tr.Styles
pabg := tr.parentActualBackground()
// must use workaround act values
st.StateLayer = tr.actStateLayer
if st.Is(states.Selected) {
st.Background = colors.Scheme.Select.Container
}
tr.Styles.ComputeActualBackground(pabg)
pc.DrawStandardBox(st, tr.Geom.Pos.Total, tr.Geom.Size.Actual.Total, pabg)
// after we are done rendering, we clear the values so they aren't inherited
st.StateLayer = 0
st.Background = nil
tr.Styles.ComputeActualBackground(pabg)
}
func (tr *Tree) RenderWidget() {
if tr.PushBounds() {
tr.Render()
if tr.Parts != nil {
// we must copy from actual values in parent
tr.Parts.Styles.StateLayer = tr.actStateLayer
if tr.StateIs(states.Selected) {
tr.Parts.Styles.Background = colors.Scheme.Select.Container
}
tr.renderParts()
}
tr.PopBounds()
}
// We have to render our children outside of `if PushBounds`
// since we could be out of scope but they could still be in!
if !tr.Closed {
tr.renderChildren()
}
}
//////////////////////////////////////////////////////////////////////////////
// Selection
// GetSelectedNodes returns a slice of the currently selected
// Trees within the entire tree, using a list maintained
// by the root node.
func (tr *Tree) GetSelectedNodes() []Treer {
if tr.root == nil {
return nil
}
if len(tr.root.SelectedNodes) == 0 {
return tr.root.SelectedNodes
}
return tr.root.SelectedNodes
}
// SetSelectedNodes updates the selected nodes on the root node to the given list.
func (tr *Tree) SetSelectedNodes(sl []Treer) {
if tr.root != nil {
tr.root.SelectedNodes = sl
}
}
// HasSelection returns whether there are currently selected items.
func (tr *Tree) HasSelection() bool {
return len(tr.GetSelectedNodes()) > 0
}
// Select selects this node (if not already selected).
// You must use this method to update global selection list.
func (tr *Tree) Select() {
if !tr.StateIs(states.Selected) {
tr.SetSelected(true)
tr.Style()
sl := tr.GetSelectedNodes()
sl = append(sl, tr.This.(Treer))
tr.SetSelectedNodes(sl)
tr.NeedsRender()
}
}
// Unselect unselects this node (if selected).
// You must use this method to update global selection list.
func (tr *Tree) Unselect() {
if tr.StateIs(states.Selected) {
tr.SetSelected(false)
tr.Style()
sl := tr.GetSelectedNodes()
sz := len(sl)
for i := 0; i < sz; i++ {
if sl[i] == tr {
sl = append(sl[:i], sl[i+1:]...)
break
}
}
tr.SetSelectedNodes(sl)
tr.NeedsRender()
}
}
// UnselectAll unselects all selected items in the tree.
func (tr *Tree) UnselectAll() {
if tr.Scene == nil {
return
}
sl := tr.GetSelectedNodes()
tr.SetSelectedNodes(nil) // clear in advance
for _, v := range sl {
vt := v.AsCoreTree()
if vt == nil || vt.This == nil {
continue
}
vt.SetSelected(false)
v.Style()
vt.NeedsRender()
}
tr.NeedsRender()
}
// SelectAll selects all items in the tree.
func (tr *Tree) SelectAll() {
if tr.Scene == nil {
return
}
tr.UnselectAll()
nn := tr.root
nn.Select()
for nn != nil {
nn = nn.moveDown(events.SelectQuiet)
}
tr.NeedsRender()
}
// selectUpdate updates selection to include this node,
// using selectmode from mouse event (ExtendContinuous, ExtendOne).
// Returns true if this node selected.
func (tr *Tree) selectUpdate(mode events.SelectModes) bool {
if mode == events.NoSelect {
return false
}
sel := false
switch mode {
case events.SelectOne:
if tr.StateIs(states.Selected) {
sl := tr.GetSelectedNodes()
if len(sl) > 1 {
tr.UnselectAll()
tr.Select()
tr.SetFocusQuiet()
sel = true
}
} else {
tr.UnselectAll()
tr.Select()
tr.SetFocusQuiet()
sel = true
}
case events.ExtendContinuous:
sl := tr.GetSelectedNodes()
if len(sl) == 0 {
tr.Select()
tr.SetFocusQuiet()
sel = true
} else {
minIndex := -1
maxIndex := 0
sel = true
for _, v := range sl {
vn := v.AsCoreTree()
if minIndex < 0 {
minIndex = vn.viewIndex
} else {
minIndex = min(minIndex, vn.viewIndex)
}
maxIndex = max(maxIndex, vn.viewIndex)
}
cidx := tr.viewIndex
nn := tr
tr.Select()
if tr.viewIndex < minIndex {
for cidx < minIndex {
nn = nn.moveDown(events.SelectQuiet) // just select
cidx = nn.viewIndex
}
} else if tr.viewIndex > maxIndex {
for cidx > maxIndex {
nn = nn.moveUp(events.SelectQuiet) // just select
cidx = nn.viewIndex
}
}
}
case events.ExtendOne:
if tr.StateIs(states.Selected) {
tr.UnselectEvent()
} else {
tr.Select()
tr.SetFocusQuiet()
sel = true
}
case events.SelectQuiet:
tr.Select()
// not sel -- no signal..
case events.UnselectQuiet:
tr.Unselect()
// not sel -- no signal..
}
tr.NeedsRender()
return sel
}
// sendSelectEvent sends an [events.Select] event on both this node and the root node.
func (tr *Tree) sendSelectEvent(original ...events.Event) {
if tr.This != tr.root.This {
tr.Send(events.Select, original...)
}
tr.root.Send(events.Select, original...)
}
// sendChangeEvent sends an [events.Change] event on both this node and the root node.
func (tr *Tree) sendChangeEvent(original ...events.Event) {
if tr.This != tr.root.This {
tr.SendChange(original...)
}
tr.root.SendChange(original...)
}
// sendChangeEventReSync sends an [events.Change] event on the RootView node.
// If SyncNode != nil, it also does a re-sync from root.
func (tr *Tree) sendChangeEventReSync(original ...events.Event) {
tr.sendChangeEvent(original...)
if tr.root.SyncNode != nil {
tr.root.Resync()
}
}
// SelectEvent updates selection to include this node,
// using selectmode from mouse event (ExtendContinuous, ExtendOne),
// and root sends selection event. Returns true if event sent.
func (tr *Tree) SelectEvent(mode events.SelectModes) bool {
sel := tr.selectUpdate(mode)
if sel {
tr.sendSelectEvent()
}
return sel
}
// UnselectEvent unselects this node (if selected),
// and root sends a selection event.
func (tr *Tree) UnselectEvent() {
if tr.StateIs(states.Selected) {
tr.Unselect()
tr.sendSelectEvent()
}
}
//////////////////////////////////////////////////////////////////////////////
// Moving
// moveDown moves the selection down to next element in the tree,
// using given select mode (from keyboard modifiers).
// Returns newly selected node.
func (tr *Tree) moveDown(selMode events.SelectModes) *Tree {
if tr.Parent == nil {
return nil
}
if tr.Closed || !tr.HasChildren() { // next sibling
return tr.moveDownSibling(selMode)
} else {
if tr.HasChildren() {
nn := AsTree(tr.Child(0))
if nn != nil {
nn.selectUpdate(selMode)
return nn
}
}
}
return nil
}
// moveDownEvent moves the selection down to next element in the tree,
// using given select mode (from keyboard modifiers).
// Sends select event for newly selected item.
func (tr *Tree) moveDownEvent(selMode events.SelectModes) *Tree {
nn := tr.moveDown(selMode)
if nn != nil && nn != tr {
nn.SetFocusQuiet()
nn.ScrollToThis()
tr.sendSelectEvent()
}
return nn
}
// moveDownSibling moves down only to siblings, not down into children,
// using given select mode (from keyboard modifiers)
func (tr *Tree) moveDownSibling(selMode events.SelectModes) *Tree {
if tr.Parent == nil {
return nil
}
if tr == tr.root {
return nil
}
myidx := tr.IndexInParent()
if myidx < len(tr.Parent.AsTree().Children)-1 {
nn := AsTree(tr.Parent.AsTree().Child(myidx + 1))
if nn != nil {
nn.selectUpdate(selMode)
return nn
}
} else {
return AsTree(tr.Parent).moveDownSibling(selMode) // try up
}
return nil
}
// moveUp moves selection up to previous element in the tree,
// using given select mode (from keyboard modifiers).
// Returns newly selected node
func (tr *Tree) moveUp(selMode events.SelectModes) *Tree {
if tr.Parent == nil || tr == tr.root {
return nil
}
myidx := tr.IndexInParent()
if myidx > 0 {
nn := AsTree(tr.Parent.AsTree().Child(myidx - 1))
if nn != nil {
return nn.moveToLastChild(selMode)
}
} else {
if tr.Parent != nil {
nn := AsTree(tr.Parent)
if nn != nil {
nn.selectUpdate(selMode)
return nn
}
}
}
return nil
}
// moveUpEvent moves the selection up to previous element in the tree,
// using given select mode (from keyboard modifiers).
// Sends select event for newly selected item.
func (tr *Tree) moveUpEvent(selMode events.SelectModes) *Tree {
nn := tr.moveUp(selMode)
if nn != nil && nn != tr {
nn.SetFocusQuiet()
nn.ScrollToThis()
tr.sendSelectEvent()
}
return nn
}
// treePageSteps is the number of steps to take in PageUp / Down events
const treePageSteps = 10
// movePageUpEvent moves the selection up to previous
// TreePageSteps elements in the tree,
// using given select mode (from keyboard modifiers).
// Sends select event for newly selected item.
func (tr *Tree) movePageUpEvent(selMode events.SelectModes) *Tree {
mvMode := selMode
if selMode == events.SelectOne {
mvMode = events.NoSelect
} else if selMode == events.ExtendContinuous || selMode == events.ExtendOne {
mvMode = events.SelectQuiet
}
fnn := tr.moveUp(mvMode)
if fnn != nil && fnn != tr {
for i := 1; i < treePageSteps; i++ {
nn := fnn.moveUp(mvMode)
if nn == nil || nn == fnn {
break
}
fnn = nn
}
if selMode == events.SelectOne {
fnn.selectUpdate(selMode)
}
fnn.SetFocusQuiet()
fnn.ScrollToThis()
tr.sendSelectEvent()
}
tr.NeedsRender()
return fnn
}
// movePageDownEvent moves the selection up to
// previous TreePageSteps elements in the tree,
// using given select mode (from keyboard modifiers).
// Sends select event for newly selected item.
func (tr *Tree) movePageDownEvent(selMode events.SelectModes) *Tree {
mvMode := selMode
if selMode == events.SelectOne {
mvMode = events.NoSelect
} else if selMode == events.ExtendContinuous || selMode == events.ExtendOne {
mvMode = events.SelectQuiet
}
fnn := tr.moveDown(mvMode)
if fnn != nil && fnn != tr {
for i := 1; i < treePageSteps; i++ {
nn := fnn.moveDown(mvMode)
if nn == nil || nn == fnn {
break
}
fnn = nn
}
if selMode == events.SelectOne {
fnn.selectUpdate(selMode)
}
fnn.SetFocusQuiet()
fnn.ScrollToThis()
tr.sendSelectEvent()
}
tr.NeedsRender()
return fnn
}
// moveToLastChild moves to the last child under me, using given select mode
// (from keyboard modifiers)
func (tr *Tree) moveToLastChild(selMode events.SelectModes) *Tree {
if tr.Parent == nil || tr == tr.root {
return nil
}
if !tr.Closed && tr.HasChildren() {
nn := AsTree(tr.Child(tr.NumChildren() - 1))
return nn.moveToLastChild(selMode)
} else {
tr.selectUpdate(selMode)
return tr
}
}
// moveHomeEvent moves the selection up to top of the tree,
// using given select mode (from keyboard modifiers)
// and emits select event for newly selected item
func (tr *Tree) moveHomeEvent(selMode events.SelectModes) *Tree {
tr.root.selectUpdate(selMode)
tr.root.SetFocusQuiet()
tr.root.ScrollToThis()
tr.root.sendSelectEvent()
return tr.root
}
// moveEndEvent moves the selection to the very last node in the tree,
// using given select mode (from keyboard modifiers)
// Sends select event for newly selected item.
func (tr *Tree) moveEndEvent(selMode events.SelectModes) *Tree {
mvMode := selMode
if selMode == events.SelectOne {
mvMode = events.NoSelect
} else if selMode == events.ExtendContinuous || selMode == events.ExtendOne {
mvMode = events.SelectQuiet
}
fnn := tr.moveDown(mvMode)
if fnn != nil && fnn != tr {
for {
nn := fnn.moveDown(mvMode)
if nn == nil || nn == fnn {
break
}
fnn = nn
}
if selMode == events.SelectOne {
fnn.selectUpdate(selMode)
}
fnn.SetFocusQuiet()
fnn.ScrollToThis()
tr.sendSelectEvent()
}
return fnn
}
func (tr *Tree) setChildrenVisibility(parentClosed bool) {
for _, c := range tr.Children {
tvn := AsTree(c)
if tvn != nil {
tvn.SetState(parentClosed, states.Invisible)
}
}
}
// OnClose is called when a node is closed.
// The base version does nothing.
func (tr *Tree) OnClose() {}
// Close closes the given node and updates the tree accordingly
// (if it is not already closed). It calls OnClose in the [Treer]
// interface for extensible actions.
func (tr *Tree) Close() {
if tr.Closed {
return
}
tr.SetClosed(true)
tr.setBranchState()
tr.This.(Treer).OnClose()
tr.setChildrenVisibility(true) // parent closed
tr.NeedsLayout()
}
// OnOpen is called when a node is opened.
// The base version does nothing.
func (tr *Tree) OnOpen() {}
// CanOpen returns true if the node is able to open.
// By default it checks HasChildren(), but could check other properties
// to perform lazy building of the tree.
func (tr *Tree) CanOpen() bool {
return tr.HasChildren()
}
// Open opens the given node and updates the tree accordingly
// (if it is not already opened). It calls OnOpen in the [Treer]
// interface for extensible actions.
func (tr *Tree) Open() {
if !tr.Closed || tr.inOpen || tr.This == nil {
return
}
tr.inOpen = true
if tr.This.(Treer).CanOpen() {
tr.SetClosed(false)
tr.setBranchState()
tr.setChildrenVisibility(false)
tr.This.(Treer).OnOpen()
}
tr.inOpen = false
tr.NeedsLayout()
}
// ToggleClose toggles the close / open status: if closed, opens, and vice-versa.
func (tr *Tree) ToggleClose() {
if tr.Closed {
tr.Open()
} else {
tr.Close()
}
}
// OpenAll opens the node and all of its sub-nodes.
func (tr *Tree) OpenAll() { //types:add
tr.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
tvn := AsTree(cw)
if tvn != nil {
tvn.Open()
return tree.Continue
}
return tree.Break
})
tr.NeedsLayout()
}
// CloseAll closes the node and all of its sub-nodes.
func (tr *Tree) CloseAll() { //types:add
tr.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
tvn := AsTree(cw)
if tvn != nil {
tvn.Close()
return tree.Continue
}
return tree.Break
})
tr.NeedsLayout()
}
// OpenParents opens all the parents of this node
// so that it will be visible.
func (tr *Tree) OpenParents() {
tr.WalkUpParent(func(k tree.Node) bool {
tvn := AsTree(k)
if tvn != nil {
tvn.Open()
return tree.Continue
}
return tree.Break
})
tr.NeedsLayout()
}
/////////////////////////////////////////////////////////////
// Modifying Source Tree
func (tr *Tree) ContextMenuPos(e events.Event) (pos image.Point) {
if e != nil {
pos = e.WindowPos()
return
}
pos.X = tr.Geom.TotalBBox.Min.X + int(tr.Indent.Dots)
pos.Y = (tr.Geom.TotalBBox.Min.Y + tr.Geom.TotalBBox.Max.Y) / 2
return
}
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())
NewSeparator(m)
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) {
if tr.IsReadOnly() || tr.rootIsReadOnly() {
tr.contextMenuReadOnly(m)
return
}
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).
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())
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())
NewSeparator(m)
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,
// creating an error snackbar if it is and action is non-empty.
func (tr *Tree) IsRoot(action string) bool {
if tr.This == tr.root.This {
if action != "" {
MessageSnackbar(tr, fmt.Sprintf("Cannot %v the root of the tree", action))
}
return true
}
return false
}
////////////////////////////////////////////////////////////
// Copy / Cut / Paste
// MimeData adds mimedata for this node: a text/plain of the Path.
func (tr *Tree) MimeData(md *mimedata.Mimes) {
if tr.SyncNode != nil {
tr.mimeDataSync(md)
return
}
*md = append(*md, mimedata.NewTextData(tr.PathFrom(tr.root)))
var buf bytes.Buffer
err := jsonx.Write(tr.This, &buf)
if err == nil {
*md = append(*md, &mimedata.Data{Type: fileinfo.DataJson, Data: buf.Bytes()})
} else {
ErrorSnackbar(tr, err, "Error encoding node")
}
}
// nodesFromMimeData returns a slice of tree nodes for
// the Tree nodes and paths from mime data.
func (tr *Tree) nodesFromMimeData(md mimedata.Mimes) ([]tree.Node, []string) {
ni := len(md) / 2
sl := make([]tree.Node, 0, ni)
pl := make([]string, 0, ni)
for _, d := range md {
if d.Type == fileinfo.DataJson {
nn, err := tree.UnmarshalRootJSON(d.Data)
if err == nil {
sl = append(sl, nn)
} else {
ErrorSnackbar(tr, err, "Error loading node")
}
} else if d.Type == fileinfo.TextPlain { // paths
pl = append(pl, string(d.Data))
}
}
return sl, pl
}
// Copy copies the tree to the clipboard.
func (tr *Tree) Copy() { //types:add
sels := tr.GetSelectedNodes()
nitms := max(1, len(sels))
md := make(mimedata.Mimes, 0, 2*nitms)
tr.This.(Treer).MimeData(&md) // source is always first..
if nitms > 1 {
for _, sn := range sels {
if sn != tr.This {
sn.MimeData(&md)
}
}
}
tr.Clipboard().Write(md)
}
// Cut copies to [system.Clipboard] and deletes selected items.
func (tr *Tree) Cut() { //types:add
if tr.IsRoot("Cut") {
return
}
if tr.SyncNode != nil {
tr.cutSync()
return
}
tr.Copy()
sels := tr.GetSelectedNodes()
root := tr.root
tr.UnselectAll()
for _, sn := range sels {
sn.AsTree().Delete()
}
root.Update()
root.sendChangeEvent()
}
// Paste pastes clipboard at given node.
func (tr *Tree) Paste() { //types:add
md := tr.Clipboard().Read([]string{fileinfo.DataJson})
if md != nil {
tr.pasteMenu(md)
}
}
// pasteMenu performs a paste from the clipboard using given data,
// by popping up a menu to determine what specifically to do.
func (tr *Tree) pasteMenu(md mimedata.Mimes) {
tr.UnselectAll()
mf := func(m *Scene) {
tr.makePasteMenu(m, md, nil)
}
pos := tr.ContextMenuPos(nil)
NewMenu(mf, tr.This.(Widget), pos).Run()
}
// makePasteMenu makes the menu of options for paste events
// Optional function is typically the DropFinalize but could also be other actions
// 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)
if fun != nil {
fun()
}
})
NewButton(m).SetText("Add to Children").OnClick(func(e events.Event) {
tr.pasteChildren(md, events.DropCopy)
if fun != nil {
fun()
}
})
if !tr.IsRoot("") && tr.root.This != tr.This {
NewButton(m).SetText("Insert Before").OnClick(func(e events.Event) {
tr.pasteBefore(md, events.DropCopy)
if fun != nil {
fun()
}
})
NewButton(m).SetText("Insert After").OnClick(func(e events.Event) {
tr.pasteAfter(md, events.DropCopy)
if fun != nil {
fun()
}
})
}
NewButton(m).SetText("Cancel")
}
// 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
}
sl, _ := tr.nodesFromMimeData(md)
if len(sl) == 0 {
return
}
tr.CopyFrom(sl[0]) // nodes with data copy here
tr.setScene(tr.Scene) // ensure children have scene
tr.Update() // could have children
tr.Open()
tr.sendChangeEvent()
}
// 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) {
tr.pasteAt(md, mod, 0, "Paste before")
}
// 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) {
tr.pasteAt(md, mod, 1, "Paste after")
}
// treeTempMovedTag is a kind of hack to prevent moved items from being deleted, using DND
const treeTempMovedTag = `_\&MOVED\&`
// todo: these methods require an interface to work for descended
// nodes, based on base code
// pasteAt inserts object(s) from mime data at rel position to 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) pasteAt(md mimedata.Mimes, mod events.DropMods, rel int, actNm string) {
if tr.Parent == nil {
return
}
parent := AsTree(tr.Parent)
if parent == nil {
MessageSnackbar(tr, "Error: cannot insert after the root of the tree")
return
}
if tr.SyncNode != nil {
tr.pasteAtSync(md, mod, rel, actNm)
return
}
sl, pl := tr.nodesFromMimeData(md)
myidx := tr.IndexInParent()
if myidx < 0 {
return
}
myidx += rel
sz := len(sl)
var selTv *Tree
for i, ns := range sl {
orgpath := pl[i]
if mod != events.DropMove {
if cn := parent.ChildByName(ns.AsTree().Name, 0); cn != nil {
ns.AsTree().SetName(ns.AsTree().Name + "_Copy")
}
}
parent.InsertChild(ns, myidx+i)
nwb := AsWidget(ns)
ntv := AsTree(ns)
ntv.root = tr.root
nwb.setScene(tr.Scene)
nwb.Update() // incl children
npath := ns.AsTree().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 :)
}
if i == sz-1 {
selTv = ntv
}
}
tr.sendChangeEvent()
parent.NeedsLayout()
if selTv != nil {
selTv.SelectEvent(events.SelectOne)
}
}
// 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
}
sl, _ := tr.nodesFromMimeData(md)
for _, ns := range sl {
tr.AddChild(ns)
nwb := AsWidget(ns)
ntv := AsTree(ns)
ntv.root = tr.root
nwb.setScene(tr.Scene)
}
tr.Update()
tr.Open()
tr.sendChangeEvent()
}
//////////////////////////////////////////////////////////////////////////////
// Drag-n-Drop
// dragStart starts a drag-n-drop on this node -- it includes any other
// selected nodes as well, each as additional records in mimedata.
func (tr *Tree) dragStart(e events.Event) {
sels := tr.GetSelectedNodes()
nitms := max(1, len(sels))
md := make(mimedata.Mimes, 0, 2*nitms)
tr.This.(Treer).MimeData(&md) // source is always first..
if nitms > 1 {
for _, sn := range sels {
if sn != tr.This {
sn.MimeData(&md)
}
}
}
tr.Scene.Events.dragStart(tr.This.(Widget), md, e)
}
// dropExternal is not handled by base case but could be in derived
func (tr *Tree) dropExternal(md mimedata.Mimes, mod events.DropMods) {
// todo: not yet implemented
}
// dragClearStates clears the drag-drop related states for this widget
func (tr *Tree) dragClearStates() {
tr.SetState(false, states.Active, states.Selected, states.Hovered, states.DragHovered)
tr.Parts.SetState(false, states.Active, states.Selected, states.Hovered, states.DragHovered)
tr.Style()
tr.NeedsRender()
}
// DragDrop handles drag drop event
func (tr *Tree) DragDrop(e events.Event) {
// todo: some kind of validation for source
tr.UnselectAll()
de := e.(*events.DragDrop)
stv := AsTree(de.Source.(Widget))
if stv != nil {
stv.dragClearStates()
}
md := de.Data.(mimedata.Mimes)
mf := func(m *Scene) {
tr.Scene.Events.dragMenuAddModText(m, de.DropMod)
tr.makePasteMenu(m, md, func() {
tr.DropFinalize(de)
})
}
pos := tr.ContextMenuPos(nil)
NewMenu(mf, tr.This.(Widget), pos).Run()
}
// DropFinalize is called to finalize Drop actions on the Source node.
// Only relevant for DropMod == DropMove.
func (tr *Tree) DropFinalize(de *events.DragDrop) {
tr.UnselectAll()
tr.dragClearStates()
tr.Scene.Events.dropFinalize(de) // sends DropDeleteSource to Source
}
// DropDeleteSource handles delete source event for DropMove case
func (tr *Tree) DropDeleteSource(e events.Event) {
de := e.(*events.DragDrop)
tr.UnselectAll()
if tr.SyncNode != nil {
tr.dropDeleteSourceSync(de)
return
}
md := de.Data.(mimedata.Mimes)
root := tr.root
for _, d := range md {
if d.Type != fileinfo.TextPlain { // link
continue
}
path := string(d.Data)
sn := root.FindPath(path)
if sn != nil {
sn.AsTree().Delete()
}
sn = root.FindPath(path + treeTempMovedTag)
if sn != nil {
psplt := strings.Split(path, "/")
orgnm := psplt[len(psplt)-1]
sn.AsTree().SetName(orgnm)
AsWidget(sn).NeedsRender()
}
}
}
// 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 core
import (
"bytes"
"fmt"
"log"
"strings"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/base/iox/jsonx"
"cogentcore.org/core/base/labels"
"cogentcore.org/core/events"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
// note: see this file has all the SyncNode specific
// functions for Tree.
// SyncTree sets the root [Tree.SyncNode] to the root of the given [tree.Node]
// and synchronizes the rest of the tree to match. The source tree must have
// unique names for each child within a given parent.
func (tr *Tree) SyncTree(n tree.Node) *Tree {
if tr.SyncNode != n {
tr.SyncNode = n
}
tvIndex := 0
tr.syncToSrc(&tvIndex, true, 0)
tr.Update()
return tr
}
// setSyncNode sets the sync source node that we are viewing,
// and syncs the view of its tree. It is called routinely
// via SyncToSrc during tree updating.
// It uses tree Config mechanism to perform minimal updates to
// remain in sync.
func (tr *Tree) setSyncNode(sn tree.Node, tvIndex *int, init bool, depth int) {
if tr.SyncNode != sn {
tr.SyncNode = sn
}
tr.syncToSrc(tvIndex, init, depth)
}
// Resync resynchronizes the [Tree] relative to the [Tree.SyncNode]
// underlying nodes and triggers an update.
func (tr *Tree) Resync() {
tvIndex := tr.viewIndex
tr.syncToSrc(&tvIndex, false, 0)
tr.Update()
}
// syncToSrc updates the view tree to match the sync tree, using
// ConfigChildren to maximally preserve existing tree elements.
// init means we are doing initial build, and depth tracks depth
// (only during init).
func (tr *Tree) syncToSrc(tvIndex *int, init bool, depth int) {
sn := tr.SyncNode
// root must keep the same name for continuity with surrounding context
if tr != tr.root {
nm := "tv_" + sn.AsTree().Name
tr.SetName(nm)
}
tr.viewIndex = *tvIndex
*tvIndex++
if init && depth >= tr.root.OpenDepth {
tr.SetClosed(true)
}
skids := sn.AsTree().Children
p := make(tree.TypePlan, 0, len(skids))
typ := tr.NodeType()
for _, skid := range skids {
p.Add(typ, "tv_"+skid.AsTree().Name)
}
tree.Update(tr, p)
idx := 0
for _, skid := range sn.AsTree().Children {
if len(tr.Children) <= idx {
break
}
vk := AsTree(tr.Children[idx])
vk.setSyncNode(skid, tvIndex, init, depth+1)
idx++
}
if !sn.AsTree().HasChildren() {
tr.SetClosed(true)
}
}
// Label returns the display label for this node,
// satisfying the [labels.Labeler] interface.
func (tr *Tree) Label() string {
if tr.SyncNode != nil {
// TODO: make this an option?
if lbl, has := labels.ToLabeler(tr.SyncNode); has {
return lbl
}
return tr.SyncNode.AsTree().Name
}
if tr.Text != "" {
return tr.Text
}
return tr.Name
}
// selectedSyncNodes returns a slice of the currently selected
// sync source nodes in the entire tree
func (tr *Tree) selectedSyncNodes() []tree.Node {
var res []tree.Node
sl := tr.GetSelectedNodes()
for _, v := range sl {
res = append(res, v.AsCoreTree().SyncNode)
}
return res
}
// FindSyncNode returns the [Tree] node for the corresponding given
// source [tree.Node] in [Tree.SyncNode] or nil if not found.
func (tr *Tree) FindSyncNode(n tree.Node) *Tree {
var res *Tree
tr.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
tvn := AsTree(cw)
if tvn != nil {
if tvn.SyncNode == n {
res = tvn
return tree.Break
}
}
return tree.Continue
})
return res
}
// InsertAfter inserts a new node in the tree
// after this node, at the same (sibling) level,
// prompting for the type of node to insert.
// If SyncNode is set, operates on Sync Tree.
func (tr *Tree) InsertAfter() { //types:add
tr.insertAt(1, "Insert after")
}
// InsertBefore inserts a new node in the tree
// before this node, at the same (sibling) level,
// prompting for the type of node to insert
// If SyncNode is set, operates on Sync Tree.
func (tr *Tree) InsertBefore() { //types:add
tr.insertAt(0, "Insert before")
}
func (tr *Tree) addTreeNodes(rel, myidx int, typ *types.Type, n int) {
var stv *Tree
for i := 0; i < n; i++ {
nn := tree.NewOfType(typ)
tr.InsertChild(nn, myidx+i)
nn.AsTree().SetName(fmt.Sprintf("new-%v-%v", typ.IDName, myidx+rel+i))
ntv := AsTree(nn)
ntv.Update()
if i == n-1 {
stv = ntv
}
}
tr.Update()
tr.Open()
tr.sendChangeEvent()
if stv != nil {
stv.SelectEvent(events.SelectOne)
}
}
func (tr *Tree) addSyncNodes(rel, myidx int, typ *types.Type, n int) {
parent := tr.SyncNode
var sn tree.Node
for i := 0; i < n; i++ {
nn := tree.NewOfType(typ)
parent.AsTree().InsertChild(nn, myidx+i)
nn.AsTree().SetName(fmt.Sprintf("new-%v-%v", typ.IDName, myidx+rel+i))
if i == n-1 {
sn = nn
}
}
tr.sendChangeEventReSync(nil)
if sn != nil {
if tvk := tr.ChildByName("tv_"+sn.AsTree().Name, 0); tvk != nil {
stv := AsTree(tvk)
stv.SelectEvent(events.SelectOne)
}
}
}
// newItemsData contains the data necessary to make a certain
// number of items of a certain type, which can be used with a
// [Form] in new item dialogs.
type newItemsData struct {
// Number is the number of elements to create
Number int
// Type is the type of elements to create
Type *types.Type
}
// insertAt inserts a new node in the tree
// at given relative offset from this node,
// at the same (sibling) level,
// prompting for the type of node to insert
// If SyncNode is set, operates on Sync Tree.
func (tr *Tree) insertAt(rel int, actNm string) {
if tr.IsRoot(actNm) {
return
}
myidx := tr.IndexInParent()
if myidx < 0 {
return
}
myidx += rel
var typ *types.Type
if tr.SyncNode == nil {
typ = types.TypeByValue(tr.This)
} else {
typ = types.TypeByValue(tr.SyncNode)
}
d := NewBody(actNm)
NewText(d).SetType(TextSupporting).SetText("Number and type of items to insert:")
nd := &newItemsData{Number: 1, Type: typ}
NewForm(d).SetStruct(nd)
d.AddBottomBar(func(bar *Frame) {
d.AddCancel(bar)
d.AddOK(bar).OnClick(func(e events.Event) {
parent := AsTree(tr.Parent)
if tr.SyncNode != nil {
parent.addSyncNodes(rel, myidx, nd.Type, nd.Number)
} else {
parent.addTreeNodes(rel, myidx, nd.Type, nd.Number)
}
})
})
d.RunDialog(tr)
}
// AddChildNode adds a new child node to this one in the tree,
// prompting the user for the type of node to add
// If SyncNode is set, operates on Sync Tree.
func (tr *Tree) AddChildNode() { //types:add
ttl := "Add child"
var typ *types.Type
if tr.SyncNode == nil {
typ = types.TypeByValue(tr.This)
} else {
typ = types.TypeByValue(tr.SyncNode)
}
d := NewBody(ttl)
NewText(d).SetType(TextSupporting).SetText("Number and type of items to insert:")
nd := &newItemsData{Number: 1, Type: typ}
NewForm(d).SetStruct(nd)
d.AddBottomBar(func(bar *Frame) {
d.AddCancel(bar)
d.AddOK(bar).OnClick(func(e events.Event) {
if tr.SyncNode != nil {
tr.addSyncNodes(0, 0, nd.Type, nd.Number)
} else {
tr.addTreeNodes(0, 0, nd.Type, nd.Number)
}
})
})
d.RunDialog(tr)
}
// DeleteNode deletes the tree node or sync node corresponding
// 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) {
return
}
tr.Close()
if tr.moveDown(events.SelectOne) == nil {
tr.moveUp(events.SelectOne)
}
if tr.SyncNode != nil {
tr.SyncNode.AsTree().Delete()
tr.sendChangeEventReSync(nil)
} else {
parent := AsTree(tr.Parent)
tr.Delete()
parent.Update()
parent.sendChangeEvent()
}
}
// 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.
func (tr *Tree) Duplicate() { //types:add
ttl := "Duplicate"
if tr.IsRoot(ttl) {
return
}
if tr.Parent == nil {
return
}
if tr.SyncNode != nil {
tr.duplicateSync()
return
}
parent := AsTree(tr.Parent)
myidx := tr.IndexInParent()
if myidx < 0 {
return
}
nm := fmt.Sprintf("%v_Copy", tr.Name)
tr.Unselect()
nwkid := tr.Clone()
nwkid.AsTree().SetName(nm)
ntv := AsTree(nwkid)
parent.InsertChild(nwkid, myidx+1)
ntv.Update()
parent.Update()
parent.sendChangeEvent()
// ntv.SelectEvent(events.SelectOne)
}
func (tr *Tree) duplicateSync() {
sn := tr.SyncNode
tvparent := AsTree(tr.Parent)
parent := tvparent.SyncNode
if parent == nil {
log.Printf("Tree %v nil SyncNode in: %v\n", tr, tvparent.Path())
return
}
myidx := sn.AsTree().IndexInParent()
if myidx < 0 {
return
}
nm := fmt.Sprintf("%v_Copy", sn.AsTree().Name)
nwkid := sn.AsTree().Clone()
nwkid.AsTree().SetName(nm)
parent.AsTree().InsertChild(nwkid, myidx+1)
tvparent.sendChangeEventReSync(nil)
if tvk := tvparent.ChildByName("tv_"+nm, 0); tvk != nil {
stv := AsTree(tvk)
stv.SelectEvent(events.SelectOne)
}
}
// editNode pulls up a [Form] dialog for the node.
// If SyncNode is set, operates on Sync Tree.
func (tr *Tree) editNode() { //types:add
if tr.SyncNode != nil {
tynm := tr.SyncNode.AsTree().NodeType().Name
d := NewBody(tynm)
NewForm(d).SetStruct(tr.SyncNode).SetReadOnly(tr.IsReadOnly())
d.RunWindowDialog(tr)
} else {
tynm := tr.NodeType().Name
d := NewBody(tynm)
NewForm(d).SetStruct(tr.This).SetReadOnly(tr.IsReadOnly())
d.RunWindowDialog(tr)
}
}
// inspectNode pulls up a new Inspector window on the node.
// If SyncNode is set, operates on Sync Tree.
func (tr *Tree) inspectNode() { //types:add
if tr.SyncNode != nil {
InspectorWindow(tr.SyncNode)
} else {
InspectorWindow(tr)
}
}
// mimeDataSync adds mimedata for this node: a text/plain of the Path,
// and an application/json of the sync node.
func (tr *Tree) mimeDataSync(md *mimedata.Mimes) {
sroot := tr.root.SyncNode
src := tr.SyncNode
*md = append(*md, mimedata.NewTextData(src.AsTree().PathFrom(sroot)))
var buf bytes.Buffer
err := jsonx.Write(src, &buf)
if err == nil {
*md = append(*md, &mimedata.Data{Type: fileinfo.DataJson, Data: buf.Bytes()})
} else {
ErrorSnackbar(tr, err, "Error encoding node")
}
}
// syncNodesFromMimeData creates a slice of tree node(s)
// from given mime data and also a corresponding slice
// of original paths.
func (tr *Tree) syncNodesFromMimeData(md mimedata.Mimes) ([]tree.Node, []string) {
ni := len(md) / 2
sl := make([]tree.Node, 0, ni)
pl := make([]string, 0, ni)
for _, d := range md {
if d.Type == fileinfo.DataJson {
nn, err := tree.UnmarshalRootJSON(d.Data)
if err == nil {
sl = append(sl, nn)
} else {
ErrorSnackbar(tr, err, "Error loading node")
}
} else if d.Type == fileinfo.TextPlain { // paths
pl = append(pl, string(d.Data))
}
}
return sl, pl
}
// pasteAssignSync assigns mime data (only the first one!) to this node
func (tr *Tree) pasteAssignSync(md mimedata.Mimes) {
sl, _ := tr.syncNodesFromMimeData(md)
if len(sl) == 0 {
return
}
tr.SyncNode.AsTree().CopyFrom(sl[0])
tr.NeedsLayout()
tr.sendChangeEvent()
}
// pasteAtSync inserts object(s) from mime data at rel position to 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) pasteAtSync(md mimedata.Mimes, mod events.DropMods, rel int, actNm string) {
sn := tr.SyncNode
sl, pl := tr.nodesFromMimeData(md)
tvparent := AsTree(tr.Parent)
parent := sn.AsTree().Parent
myidx := sn.AsTree().IndexInParent()
if myidx < 0 {
return
}
myidx += rel
sroot := tr.root.SyncNode
sz := len(sl)
var seln tree.Node
for i, ns := range sl {
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)
if mod == events.DropMove && npath == orgpath { // we will be nuked immediately after drag
ns.AsTree().SetName(ns.AsTree().Name + treeTempMovedTag) // special keyword :)
}
if i == sz-1 {
seln = ns
}
}
tvparent.sendChangeEventReSync(nil)
if seln != nil {
if tvk := tvparent.ChildByName("tv_"+seln.AsTree().Name, myidx); tvk != nil {
stv := AsTree(tvk)
stv.SelectEvent(events.SelectOne)
}
}
}
// pasteChildrenSync inserts object(s) from mime data at
// end of children of this node
func (tr *Tree) pasteChildrenSync(md mimedata.Mimes, mod events.DropMods) {
sl, _ := tr.nodesFromMimeData(md)
sk := tr.SyncNode
for _, ns := range sl {
sk.AsTree().AddChild(ns)
}
tr.sendChangeEventReSync(nil)
}
// cutSync copies to system.Clipboard and deletes selected items.
func (tr *Tree) cutSync() {
tr.Copy()
sels := tr.selectedSyncNodes()
tr.UnselectAll()
for _, sn := range sels {
sn.AsTree().Delete()
}
tr.sendChangeEventReSync(nil)
}
// dropDeleteSourceSync handles delete source event for DropMove case, for Sync
func (tr *Tree) dropDeleteSourceSync(de *events.DragDrop) {
md := de.Data.(mimedata.Mimes)
sroot := tr.root.SyncNode
for _, d := range md {
if d.Type != fileinfo.TextPlain { // link
continue
}
path := string(d.Data)
sn := sroot.AsTree().FindPath(path)
if sn != nil {
sn.AsTree().Delete()
}
sn = sroot.AsTree().FindPath(path + treeTempMovedTag)
if sn != nil {
psplt := strings.Split(path, "/")
orgnm := psplt[len(psplt)-1]
sn.AsTree().SetName(orgnm)
AsWidget(sn).NeedsRender()
}
}
tr.sendChangeEventReSync(nil)
}
// Code generated by "core generate"; DO NOT EDIT.
package core
import (
"image"
"image/color"
"reflect"
"time"
"cogentcore.org/core/events"
"cogentcore.org/core/events/key"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/parse/complete"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.App", IDName: "app", Doc: "App represents a Cogent Core app. It extends [system.App] to provide both system-level\nand high-level data and functions to do with the currently running application. The\nsingle instance of it is [TheApp], which embeds [system.TheApp].", Directives: []types.Directive{{Tool: "types", Directive: "add", Args: []string{"-setters"}}}, Embeds: []types.Field{{Name: "App"}}, Fields: []types.Field{{Name: "SceneInit", Doc: "SceneInit is a function called on every newly created [Scene].\nThis can be used to set global configuration and styling for all\nwidgets in conjunction with [Scene.WidgetInit]."}}})
// SetSceneInit sets the [App.SceneInit]:
// SceneInit is a function called on every newly created [Scene].
// This can be used to set global configuration and styling for all
// widgets in conjunction with [Scene.WidgetInit].
func (t *App) SetSceneInit(v func(sc *Scene)) *App { t.SceneInit = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Body", IDName: "body", Doc: "Body holds the primary content of a [Scene].\nIt is the main container for app content.", Directives: []types.Directive{{Tool: "core", Directive: "no-new"}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Title", Doc: "Title is the title of the body, which is also\nused for the window title where relevant."}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Button", IDName: "button", Doc: "Button is an interactive button with text, an icon, an indicator, a shortcut,\nand/or a menu. The standard behavior is to register a click event handler with\n[WidgetBase.OnClick].", Directives: []types.Directive{{Tool: "core", Directive: "embedder"}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Type", Doc: "Type is the type of button."}, {Name: "Text", Doc: "Text is the text for the button.\nIf it is blank, no text is shown."}, {Name: "Icon", Doc: "Icon is the icon for the button.\nIf it is \"\" or [icons.None], no icon is shown."}, {Name: "Indicator", Doc: "Indicator is the menu indicator icon to present.\nIf it is \"\" or [icons.None],, no indicator is shown.\nIt is automatically set to [icons.KeyboardArrowDown]\nwhen there is a Menu elements present unless it is\nset to [icons.None]."}, {Name: "Shortcut", Doc: "Shortcut is an optional shortcut keyboard chord to trigger this button,\nactive in window-wide scope. Avoid conflicts with other shortcuts\n(a log message will be emitted if so). Shortcuts are processed after\nall other processing of keyboard input. Command is automatically translated\ninto Meta on macOS and Control on all other platforms. Also see [Button.SetKey]."}, {Name: "Menu", Doc: "Menu is a menu constructor function used to build and display\na menu whenever the button is clicked. There will be no menu\nif it is nil. The constructor function should add buttons\nto the Scene that it is passed."}}})
// NewButton returns a new [Button] with the given optional parent:
// Button is an interactive button with text, an icon, an indicator, a shortcut,
// and/or a menu. The standard behavior is to register a click event handler with
// [WidgetBase.OnClick].
func NewButton(parent ...tree.Node) *Button { return tree.New[Button](parent...) }
// ButtonEmbedder is an interface that all types that embed Button satisfy
type ButtonEmbedder interface {
AsButton() *Button
}
// AsButton returns the given value as a value of type Button if the type
// of the given value embeds Button, or nil otherwise
func AsButton(n tree.Node) *Button {
if t, ok := n.(ButtonEmbedder); ok {
return t.AsButton()
}
return nil
}
// AsButton satisfies the [ButtonEmbedder] interface
func (t *Button) AsButton() *Button { return t }
// SetType sets the [Button.Type]:
// Type is the type of button.
func (t *Button) SetType(v ButtonTypes) *Button { t.Type = v; return t }
// SetText sets the [Button.Text]:
// Text is the text for the button.
// If it is blank, no text is shown.
func (t *Button) SetText(v string) *Button { t.Text = v; return t }
// SetIcon sets the [Button.Icon]:
// Icon is the icon for the button.
// If it is "" or [icons.None], no icon is shown.
func (t *Button) SetIcon(v icons.Icon) *Button { t.Icon = v; return t }
// SetIndicator sets the [Button.Indicator]:
// Indicator is the menu indicator icon to present.
// If it is "" or [icons.None],, no indicator is shown.
// It is automatically set to [icons.KeyboardArrowDown]
// when there is a Menu elements present unless it is
// set to [icons.None].
func (t *Button) SetIndicator(v icons.Icon) *Button { t.Indicator = v; return t }
// SetShortcut sets the [Button.Shortcut]:
// Shortcut is an optional shortcut keyboard chord to trigger this button,
// active in window-wide scope. Avoid conflicts with other shortcuts
// (a log message will be emitted if so). Shortcuts are processed after
// all other processing of keyboard input. Command is automatically translated
// into Meta on macOS and Control on all other platforms. Also see [Button.SetKey].
func (t *Button) SetShortcut(v key.Chord) *Button { t.Shortcut = v; return t }
// SetMenu sets the [Button.Menu]:
// Menu is a menu constructor function used to build and display
// a menu whenever the button is clicked. There will be no menu
// if it is nil. The constructor function should add buttons
// to the Scene that it is passed.
func (t *Button) SetMenu(v func(m *Scene)) *Button { t.Menu = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Canvas", IDName: "canvas", Doc: "Canvas is a widget that can be arbitrarily drawn to by setting\nits Draw function using [Canvas.SetDraw].", Embeds: []types.Field{{Name: "WidgetBase"}}, Fields: []types.Field{{Name: "Draw", Doc: "Draw is the function used to draw the content of the\ncanvas every time that it is rendered. The paint context\nis automatically normalized to the size of the canvas,\nso you should specify points on a 0-1 scale."}, {Name: "context", Doc: "context is the paint context used for drawing."}}})
// NewCanvas returns a new [Canvas] with the given optional parent:
// Canvas is a widget that can be arbitrarily drawn to by setting
// its Draw function using [Canvas.SetDraw].
func NewCanvas(parent ...tree.Node) *Canvas { return tree.New[Canvas](parent...) }
// SetDraw sets the [Canvas.Draw]:
// Draw is the function used to draw the content of the
// canvas every time that it is rendered. The paint context
// is automatically normalized to the size of the canvas,
// so you should specify points on a 0-1 scale.
func (t *Canvas) SetDraw(v func(pc *paint.Context)) *Canvas { t.Draw = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Chooser", IDName: "chooser", Doc: "Chooser is a dropdown selection widget that allows users to choose\none option among a list of items.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Type", Doc: "Type is the styling type of the chooser."}, {Name: "Items", Doc: "Items are the chooser items available for selection."}, {Name: "Icon", Doc: "Icon is an optional icon displayed on the left side of the chooser."}, {Name: "Indicator", Doc: "Indicator is the icon to use for the indicator displayed on the\nright side of the chooser."}, {Name: "Editable", Doc: "Editable is whether provide a text field for editing the value,\nor just a button for selecting items."}, {Name: "AllowNew", Doc: "AllowNew is whether to allow the user to add new items to the\nchooser through the editable textfield (if Editable is set to\ntrue) and a button at the end of the chooser menu. See also [DefaultNew]."}, {Name: "DefaultNew", Doc: "DefaultNew configures the chooser to accept new items, as in\n[AllowNew], and also turns off completion popups and always\nadds new items to the list of items, without prompting.\nUse this for cases where the typical use-case is to enter new values,\nbut the history of prior values can also be useful."}, {Name: "placeholder", Doc: "placeholder, if Editable is set to true, is the text that is\ndisplayed in the text field when it is empty. It must be set\nusing [Chooser.SetPlaceholder]."}, {Name: "ItemsFuncs", Doc: "ItemsFuncs is a slice of functions to call before showing the items\nof the chooser, which is typically used to configure them\n(eg: if they are based on dynamic data). The functions are called\nin ascending order such that the items added in the first function\nwill appear before those added in the last function. Use\n[Chooser.AddItemsFunc] to add a new items function. If at least\none ItemsFunc is specified, the items of the chooser will be\ncleared before calling the functions."}, {Name: "CurrentItem", Doc: "CurrentItem is the currently selected item."}, {Name: "CurrentIndex", Doc: "CurrentIndex is the index of the currently selected item\nin [Chooser.Items]."}, {Name: "text"}, {Name: "textField"}}})
// NewChooser returns a new [Chooser] with the given optional parent:
// Chooser is a dropdown selection widget that allows users to choose
// one option among a list of items.
func NewChooser(parent ...tree.Node) *Chooser { return tree.New[Chooser](parent...) }
// SetType sets the [Chooser.Type]:
// Type is the styling type of the chooser.
func (t *Chooser) SetType(v ChooserTypes) *Chooser { t.Type = v; return t }
// SetItems sets the [Chooser.Items]:
// Items are the chooser items available for selection.
func (t *Chooser) SetItems(v ...ChooserItem) *Chooser { t.Items = v; return t }
// SetIcon sets the [Chooser.Icon]:
// Icon is an optional icon displayed on the left side of the chooser.
func (t *Chooser) SetIcon(v icons.Icon) *Chooser { t.Icon = v; return t }
// SetIndicator sets the [Chooser.Indicator]:
// Indicator is the icon to use for the indicator displayed on the
// right side of the chooser.
func (t *Chooser) SetIndicator(v icons.Icon) *Chooser { t.Indicator = v; return t }
// SetEditable sets the [Chooser.Editable]:
// Editable is whether provide a text field for editing the value,
// or just a button for selecting items.
func (t *Chooser) SetEditable(v bool) *Chooser { t.Editable = v; return t }
// SetAllowNew sets the [Chooser.AllowNew]:
// AllowNew is whether to allow the user to add new items to the
// chooser through the editable textfield (if Editable is set to
// true) and a button at the end of the chooser menu. See also [DefaultNew].
func (t *Chooser) SetAllowNew(v bool) *Chooser { t.AllowNew = v; return t }
// SetDefaultNew sets the [Chooser.DefaultNew]:
// DefaultNew configures the chooser to accept new items, as in
// [AllowNew], and also turns off completion popups and always
// adds new items to the list of items, without prompting.
// Use this for cases where the typical use-case is to enter new values,
// but the history of prior values can also be useful.
func (t *Chooser) SetDefaultNew(v bool) *Chooser { t.DefaultNew = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.ColorMapButton", IDName: "color-map-button", Doc: "ColorMapButton displays a [colormap.Map] and can be clicked on\nto display a dialog for selecting different color map options.\nIt represents a [ColorMapName] value.", Embeds: []types.Field{{Name: "Button"}}, Fields: []types.Field{{Name: "MapName"}}})
// NewColorMapButton returns a new [ColorMapButton] with the given optional parent:
// ColorMapButton displays a [colormap.Map] and can be clicked on
// to display a dialog for selecting different color map options.
// It represents a [ColorMapName] value.
func NewColorMapButton(parent ...tree.Node) *ColorMapButton {
return tree.New[ColorMapButton](parent...)
}
// SetMapName sets the [ColorMapButton.MapName]
func (t *ColorMapButton) SetMapName(v string) *ColorMapButton { t.MapName = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.ColorPicker", IDName: "color-picker", Doc: "ColorPicker represents a color value with an interactive color picker\ncomposed of history buttons, a hex input, three HCT sliders, and standard\nnamed color buttons.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Color", Doc: "Color is the current color."}}})
// NewColorPicker returns a new [ColorPicker] with the given optional parent:
// ColorPicker represents a color value with an interactive color picker
// composed of history buttons, a hex input, three HCT sliders, and standard
// named color buttons.
func NewColorPicker(parent ...tree.Node) *ColorPicker { return tree.New[ColorPicker](parent...) }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.ColorButton", IDName: "color-button", Doc: "ColorButton represents a color value with a button that opens a [ColorPicker].", Embeds: []types.Field{{Name: "Button"}}, Fields: []types.Field{{Name: "Color"}}})
// NewColorButton returns a new [ColorButton] with the given optional parent:
// ColorButton represents a color value with a button that opens a [ColorPicker].
func NewColorButton(parent ...tree.Node) *ColorButton { return tree.New[ColorButton](parent...) }
// SetColor sets the [ColorButton.Color]
func (t *ColorButton) SetColor(v color.RGBA) *ColorButton { t.Color = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Complete", IDName: "complete", Doc: "Complete holds the current completion data and functions to call for building\nthe list of possible completions and for editing text after a completion is selected.\nIt also holds the popup [Stage] associated with it.", Directives: []types.Directive{{Tool: "types", Directive: "add", Args: []string{"-setters"}}}, Fields: []types.Field{{Name: "MatchFunc", Doc: "function to get the list of possible completions"}, {Name: "LookupFunc", Doc: "function to get the text to show for lookup"}, {Name: "EditFunc", Doc: "function to edit text using the selected completion"}, {Name: "Context", Doc: "the context object that implements the completion functions"}, {Name: "SrcLn", Doc: "line number in source that completion is operating on, if relevant"}, {Name: "SrcCh", Doc: "character position in source that completion is operating on"}, {Name: "completions", Doc: "the list of potential completions"}, {Name: "Seed", Doc: "current completion seed"}, {Name: "Completion", Doc: "the user's completion selection"}, {Name: "listeners", Doc: "the event listeners for the completer (it sends [events.Select] events)"}, {Name: "stage", Doc: "stage is the popup [Stage] associated with the [Complete]."}, {Name: "delayTimer"}, {Name: "delayMu"}, {Name: "showMu"}}})
// SetMatchFunc sets the [Complete.MatchFunc]:
// function to get the list of possible completions
func (t *Complete) SetMatchFunc(v complete.MatchFunc) *Complete { t.MatchFunc = v; return t }
// SetLookupFunc sets the [Complete.LookupFunc]:
// function to get the text to show for lookup
func (t *Complete) SetLookupFunc(v complete.LookupFunc) *Complete { t.LookupFunc = v; return t }
// SetEditFunc sets the [Complete.EditFunc]:
// function to edit text using the selected completion
func (t *Complete) SetEditFunc(v complete.EditFunc) *Complete { t.EditFunc = v; return t }
// SetContext sets the [Complete.Context]:
// the context object that implements the completion functions
func (t *Complete) SetContext(v any) *Complete { t.Context = v; return t }
// SetSrcLn sets the [Complete.SrcLn]:
// line number in source that completion is operating on, if relevant
func (t *Complete) SetSrcLn(v int) *Complete { t.SrcLn = v; return t }
// SetSrcCh sets the [Complete.SrcCh]:
// character position in source that completion is operating on
func (t *Complete) SetSrcCh(v int) *Complete { t.SrcCh = v; return t }
// SetSeed sets the [Complete.Seed]:
// current completion seed
func (t *Complete) SetSeed(v string) *Complete { t.Seed = v; return t }
// SetCompletion sets the [Complete.Completion]:
// the user's completion selection
func (t *Complete) SetCompletion(v string) *Complete { t.Completion = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.FilePicker", IDName: "file-picker", Doc: "FilePicker is a widget for selecting files.", Methods: []types.Method{{Name: "updateFilesEvent", Doc: "updateFilesEvent updates the list of files and other views for the current path.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "addPathToFavorites", Doc: "addPathToFavorites adds the current path to favorites", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "directoryUp", Doc: "directoryUp moves up one directory in the path", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "newFolder", Doc: "newFolder creates a new folder with the given name in the current directory.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"name"}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Filterer", Doc: "Filterer is an optional filtering function for which files to display."}, {Name: "directory", Doc: "directory is the absolute path to the directory of files to display."}, {Name: "selectedFilename", Doc: "selectedFilename is the name of the currently selected file,\nnot including the directory. See [FilePicker.SelectedFile]\nfor the full path."}, {Name: "extensions", Doc: "extensions is a list of the target file extensions.\nIf there are multiple, they must be comma separated.\nThe extensions must include the dot (\".\") at the start.\nThey must be set using [FilePicker.SetExtensions]."}, {Name: "extensionMap", Doc: "extensionMap is a map of lower-cased extensions from Extensions.\nIt used for highlighting files with one of these extensions;\nmaps onto original Extensions value."}, {Name: "files", Doc: "files for current directory"}, {Name: "selectedIndex", Doc: "index of currently selected file in Files list (-1 if none)"}, {Name: "watcher", Doc: "change notify for current dir"}, {Name: "doneWatcher", Doc: "channel to close watcher watcher"}, {Name: "prevPath", Doc: "Previous path that was processed via UpdateFiles"}, {Name: "favoritesTable"}, {Name: "filesTable"}, {Name: "selectField"}, {Name: "extensionField"}}})
// NewFilePicker returns a new [FilePicker] with the given optional parent:
// FilePicker is a widget for selecting files.
func NewFilePicker(parent ...tree.Node) *FilePicker { return tree.New[FilePicker](parent...) }
// SetFilterer sets the [FilePicker.Filterer]:
// Filterer is an optional filtering function for which files to display.
func (t *FilePicker) SetFilterer(v FilePickerFilterer) *FilePicker { t.Filterer = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.FileButton", IDName: "file-button", Doc: "FileButton represents a [Filename] value with a button\nthat opens a [FilePicker].", Embeds: []types.Field{{Name: "Button"}}, Fields: []types.Field{{Name: "Filename"}, {Name: "Extensions", Doc: "Extensions are the target file extensions for the file picker."}}})
// NewFileButton returns a new [FileButton] with the given optional parent:
// FileButton represents a [Filename] value with a button
// that opens a [FilePicker].
func NewFileButton(parent ...tree.Node) *FileButton { return tree.New[FileButton](parent...) }
// SetFilename sets the [FileButton.Filename]
func (t *FileButton) SetFilename(v string) *FileButton { t.Filename = v; return t }
// SetExtensions sets the [FileButton.Extensions]:
// Extensions are the target file extensions for the file picker.
func (t *FileButton) SetExtensions(v string) *FileButton { t.Extensions = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Form", IDName: "form", Doc: "Form represents a struct with rows of field names and editable values.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Struct", Doc: "Struct is the pointer to the struct that we are viewing."}, {Name: "Inline", Doc: "Inline is whether to display the form in one line."}, {Name: "structFields", Doc: "structFields are the fields of the current struct."}, {Name: "isShouldDisplayer", Doc: "isShouldDisplayer is whether the struct implements [ShouldDisplayer], which results\nin additional updating being done at certain points."}}})
// NewForm returns a new [Form] with the given optional parent:
// Form represents a struct with rows of field names and editable values.
func NewForm(parent ...tree.Node) *Form { return tree.New[Form](parent...) }
// SetStruct sets the [Form.Struct]:
// Struct is the pointer to the struct that we are viewing.
func (t *Form) SetStruct(v any) *Form { t.Struct = v; return t }
// SetInline sets the [Form.Inline]:
// Inline is whether to display the form in one line.
func (t *Form) SetInline(v bool) *Form { t.Inline = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Frame", IDName: "frame", Doc: "Frame is the primary node type responsible for organizing the sizes\nand positions of child widgets. It also renders the standard box model.\nAll collections of widgets should generally be contained within a [Frame];\notherwise, the parent widget must take over responsibility for positioning.\nFrames automatically can add scrollbars depending on the [styles.Style.Overflow].\n\nFor a [styles.Grid] frame, the [styles.Style.Columns] property should\ngenerally be set to the desired number of columns, from which the number of rows\nis computed; otherwise, it uses the square root of number of\nelements.", Embeds: []types.Field{{Name: "WidgetBase"}}, Fields: []types.Field{{Name: "StackTop", Doc: "StackTop, for a [styles.Stacked] frame, is the index of the node to use\nas the top of the stack. Only the node at this index is rendered; if it is\nnot a valid index, nothing is rendered."}, {Name: "LayoutStackTopOnly", Doc: "LayoutStackTopOnly is whether to only layout the top widget\n(specified by [Frame.StackTop]) for a [styles.Stacked] frame.\nThis is appropriate for widgets such as [Tabs], which do a full\nredraw on stack changes, but not for widgets such as [Switch]es\nwhich don't."}, {Name: "layout", Doc: "layout contains implementation state info for doing layout"}, {Name: "HasScroll", Doc: "HasScroll is whether scrollbars exist for each dimension."}, {Name: "scrolls", Doc: "scrolls are the scroll bars, which are fully managed as needed."}, {Name: "focusName", Doc: "accumulated name to search for when keys are typed"}, {Name: "focusNameTime", Doc: "time of last focus name event; for timeout"}, {Name: "focusNameLast", Doc: "last element focused on; used as a starting point if name is the same"}}})
// NewFrame returns a new [Frame] with the given optional parent:
// Frame is the primary node type responsible for organizing the sizes
// and positions of child widgets. It also renders the standard box model.
// All collections of widgets should generally be contained within a [Frame];
// otherwise, the parent widget must take over responsibility for positioning.
// Frames automatically can add scrollbars depending on the [styles.Style.Overflow].
//
// For a [styles.Grid] frame, the [styles.Style.Columns] property should
// generally be set to the desired number of columns, from which the number of rows
// is computed; otherwise, it uses the square root of number of
// elements.
func NewFrame(parent ...tree.Node) *Frame { return tree.New[Frame](parent...) }
// SetStackTop sets the [Frame.StackTop]:
// StackTop, for a [styles.Stacked] frame, is the index of the node to use
// as the top of the stack. Only the node at this index is rendered; if it is
// not a valid index, nothing is rendered.
func (t *Frame) SetStackTop(v int) *Frame { t.StackTop = v; return t }
// SetLayoutStackTopOnly sets the [Frame.LayoutStackTopOnly]:
// LayoutStackTopOnly is whether to only layout the top widget
// (specified by [Frame.StackTop]) for a [styles.Stacked] frame.
// This is appropriate for widgets such as [Tabs], which do a full
// redraw on stack changes, but not for widgets such as [Switch]es
// which don't.
func (t *Frame) SetLayoutStackTopOnly(v bool) *Frame { t.LayoutStackTopOnly = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Stretch", IDName: "stretch", Doc: "Stretch adds a stretchy element that grows to fill all\navailable space. You can set [styles.Style.Grow] to change\nhow much it grows relative to other growing elements.\nIt does not render anything.", Embeds: []types.Field{{Name: "WidgetBase"}}})
// NewStretch returns a new [Stretch] with the given optional parent:
// Stretch adds a stretchy element that grows to fill all
// available space. You can set [styles.Style.Grow] to change
// how much it grows relative to other growing elements.
// It does not render anything.
func NewStretch(parent ...tree.Node) *Stretch { return tree.New[Stretch](parent...) }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Space", IDName: "space", Doc: "Space is a fixed size blank space, with\na default width of 1ch and a height of 1em.\nYou can set [styles.Style.Min] to change its size.\nIt does not render anything.", Embeds: []types.Field{{Name: "WidgetBase"}}})
// NewSpace returns a new [Space] with the given optional parent:
// Space is a fixed size blank space, with
// a default width of 1ch and a height of 1em.
// You can set [styles.Style.Min] to change its size.
// It does not render anything.
func NewSpace(parent ...tree.Node) *Space { return tree.New[Space](parent...) }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.FuncButton", IDName: "func-button", Doc: "FuncButton is a button that is set up to call a function when it\nis pressed, using a dialog to prompt the user for any arguments.\nAlso, it automatically sets various properties of the button like\nthe text and tooltip based on the properties of the function,\nusing [reflect] and [types]. The function must be registered\nwith [types] to get documentation information, but that is not\nrequired; add a `//types:add` comment directive and run `core generate`\nif you want tooltips. If the function is a method, both the method and\nits receiver type must be added to [types] to get documentation.\nThe main function to call first is [FuncButton.SetFunc].", Embeds: []types.Field{{Name: "Button"}}, Fields: []types.Field{{Name: "typesFunc", Doc: "typesFunc is the [types.Func] associated with this button.\nThis function can also be a method, but it must be\nconverted to a [types.Func] first. It should typically\nbe set using [FuncButton.SetFunc]."}, {Name: "reflectFunc", Doc: "reflectFunc is the [reflect.Value] of the function or\nmethod associated with this button. It should typically\nbet set using [FuncButton.SetFunc]."}, {Name: "Args", Doc: "Args are the [FuncArg] objects associated with the\narguments of the function. They are automatically set in\n[FuncButton.SetFunc], but they can be customized to configure\ndefault values and other options."}, {Name: "Returns", Doc: "Returns are the [FuncArg] objects associated with the\nreturn values of the function. They are automatically\nset in [FuncButton.SetFunc], but they can be customized\nto configure options. The [FuncArg.Value]s are not set until\nthe function is called, and are thus not typically applicable\nto access."}, {Name: "Confirm", Doc: "Confirm is whether to prompt the user for confirmation\nbefore calling the function."}, {Name: "ShowReturn", Doc: "ShowReturn is whether to display the return values of\nthe function (and a success message if there are none).\nThe way that the return values are shown is determined\nby ShowReturnAsDialog. Non-nil error return values will\nalways be shown, even if ShowReturn is set to false."}, {Name: "ShowReturnAsDialog", Doc: "ShowReturnAsDialog, if and only if ShowReturn is true,\nindicates to show the return values of the function in\na dialog, instead of in a snackbar, as they are by default.\nIf there are multiple return values from the function, or if\none of them is a complex type (pointer, struct, slice,\narray, map), then ShowReturnAsDialog will\nautomatically be set to true."}, {Name: "NewWindow", Doc: "NewWindow makes the return value dialog a NewWindow dialog."}, {Name: "WarnUnadded", Doc: "WarnUnadded is whether to log warnings when a function that\nhas not been added to [types] is used. It is on by default and\nmust be set before [FuncButton.SetFunc] is called for it to\nhave any effect. Warnings are never logged for anonymous functions."}, {Name: "Context", Doc: "Context is used for opening dialogs if non-nil."}, {Name: "AfterFunc", Doc: "AfterFunc is an optional function called after the func button\nfunction is executed."}}})
// NewFuncButton returns a new [FuncButton] with the given optional parent:
// FuncButton is a button that is set up to call a function when it
// is pressed, using a dialog to prompt the user for any arguments.
// Also, it automatically sets various properties of the button like
// the text and tooltip based on the properties of the function,
// using [reflect] and [types]. The function must be registered
// with [types] to get documentation information, but that is not
// required; add a `//types:add` comment directive and run `core generate`
// if you want tooltips. If the function is a method, both the method and
// its receiver type must be added to [types] to get documentation.
// The main function to call first is [FuncButton.SetFunc].
func NewFuncButton(parent ...tree.Node) *FuncButton { return tree.New[FuncButton](parent...) }
// SetConfirm sets the [FuncButton.Confirm]:
// Confirm is whether to prompt the user for confirmation
// before calling the function.
func (t *FuncButton) SetConfirm(v bool) *FuncButton { t.Confirm = v; return t }
// SetShowReturn sets the [FuncButton.ShowReturn]:
// ShowReturn is whether to display the return values of
// the function (and a success message if there are none).
// The way that the return values are shown is determined
// by ShowReturnAsDialog. Non-nil error return values will
// always be shown, even if ShowReturn is set to false.
func (t *FuncButton) SetShowReturn(v bool) *FuncButton { t.ShowReturn = v; return t }
// SetShowReturnAsDialog sets the [FuncButton.ShowReturnAsDialog]:
// ShowReturnAsDialog, if and only if ShowReturn is true,
// indicates to show the return values of the function in
// a dialog, instead of in a snackbar, as they are by default.
// If there are multiple return values from the function, or if
// one of them is a complex type (pointer, struct, slice,
// array, map), then ShowReturnAsDialog will
// automatically be set to true.
func (t *FuncButton) SetShowReturnAsDialog(v bool) *FuncButton { t.ShowReturnAsDialog = v; return t }
// SetNewWindow sets the [FuncButton.NewWindow]:
// NewWindow makes the return value dialog a NewWindow dialog.
func (t *FuncButton) SetNewWindow(v bool) *FuncButton { t.NewWindow = v; return t }
// SetWarnUnadded sets the [FuncButton.WarnUnadded]:
// WarnUnadded is whether to log warnings when a function that
// has not been added to [types] is used. It is on by default and
// must be set before [FuncButton.SetFunc] is called for it to
// have any effect. Warnings are never logged for anonymous functions.
func (t *FuncButton) SetWarnUnadded(v bool) *FuncButton { t.WarnUnadded = v; return t }
// SetContext sets the [FuncButton.Context]:
// Context is used for opening dialogs if non-nil.
func (t *FuncButton) SetContext(v Widget) *FuncButton { t.Context = v; return t }
// SetAfterFunc sets the [FuncButton.AfterFunc]:
// AfterFunc is an optional function called after the func button
// function is executed.
func (t *FuncButton) SetAfterFunc(v func()) *FuncButton { t.AfterFunc = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.FuncArg", IDName: "func-arg", Doc: "FuncArg represents one argument or return value of a function\nin the context of a [FuncButton].", Directives: []types.Directive{{Tool: "types", Directive: "add", Args: []string{"-setters"}}}, Fields: []types.Field{{Name: "Name", Doc: "Name is the name of the argument or return value."}, {Name: "Tag", Doc: "Tag contains any tags associated with the argument or return value,\nwhich can be added programmatically to customize [Value] behavior."}, {Name: "Value", Doc: "Value is the actual value of the function argument or return value.\nIt can be modified when creating a [FuncButton] to set a default value."}}})
// SetName sets the [FuncArg.Name]:
// Name is the name of the argument or return value.
func (t *FuncArg) SetName(v string) *FuncArg { t.Name = v; return t }
// SetTag sets the [FuncArg.Tag]:
// Tag contains any tags associated with the argument or return value,
// which can be added programmatically to customize [Value] behavior.
func (t *FuncArg) SetTag(v reflect.StructTag) *FuncArg { t.Tag = v; return t }
// SetValue sets the [FuncArg.Value]:
// Value is the actual value of the function argument or return value.
// It can be modified when creating a [FuncButton] to set a default value.
func (t *FuncArg) SetValue(v any) *FuncArg { t.Value = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Handle", IDName: "handle", Doc: "Handle represents a draggable handle that can be used to\ncontrol the size of an element. The [styles.Style.Direction]\ncontrols the direction in which the handle moves.", Embeds: []types.Field{{Name: "WidgetBase"}}, Fields: []types.Field{{Name: "Min", Doc: "Min is the minimum value that the handle can go to\n(typically the lower bound of the dialog/splits)"}, {Name: "Max", Doc: "Max is the maximum value that the handle can go to\n(typically the upper bound of the dialog/splits)"}, {Name: "Pos", Doc: "Pos is the current position of the handle on the\nscale of [Handle.Min] to [Handle.Max]."}}})
// NewHandle returns a new [Handle] with the given optional parent:
// Handle represents a draggable handle that can be used to
// control the size of an element. The [styles.Style.Direction]
// controls the direction in which the handle moves.
func NewHandle(parent ...tree.Node) *Handle { return tree.New[Handle](parent...) }
// SetMin sets the [Handle.Min]:
// Min is the minimum value that the handle can go to
// (typically the lower bound of the dialog/splits)
func (t *Handle) SetMin(v float32) *Handle { t.Min = v; return t }
// SetMax sets the [Handle.Max]:
// Max is the maximum value that the handle can go to
// (typically the upper bound of the dialog/splits)
func (t *Handle) SetMax(v float32) *Handle { t.Max = v; return t }
// SetPos sets the [Handle.Pos]:
// Pos is the current position of the handle on the
// scale of [Handle.Min] to [Handle.Max].
func (t *Handle) SetPos(v float32) *Handle { t.Pos = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Icon", IDName: "icon", Doc: "Icon renders an [icons.Icon].\nThe rendered version is cached for the current size.\nIcons do not render a background or border independent of their SVG object.\nThe size of an Icon is determined by the [styles.Font.Size] property.", Embeds: []types.Field{{Name: "WidgetBase"}}, Fields: []types.Field{{Name: "Icon", Doc: "Icon is the [icons.Icon] used to render the [Icon]."}, {Name: "prevIcon", Doc: "prevIcon is the previously rendered icon."}, {Name: "svg", Doc: "svg drawing of the icon"}}})
// NewIcon returns a new [Icon] with the given optional parent:
// Icon renders an [icons.Icon].
// The rendered version is cached for the current size.
// Icons do not render a background or border independent of their SVG object.
// The size of an Icon is determined by the [styles.Font.Size] property.
func NewIcon(parent ...tree.Node) *Icon { return tree.New[Icon](parent...) }
// SetIcon sets the [Icon.Icon]:
// Icon is the [icons.Icon] used to render the [Icon].
func (t *Icon) SetIcon(v icons.Icon) *Icon { t.Icon = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Image", IDName: "image", Doc: "Image is a widget that renders an [image.Image].\nSee [styles.Style.ObjectFit] to control the image rendering within\nthe allocated size. The default minimum requested size is the pixel\nsize in [units.Dp] units (1/160th of an inch).", Methods: []types.Method{{Name: "Open", Doc: "Open sets the image to the image located at the given filename.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename"}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "WidgetBase"}}, Fields: []types.Field{{Name: "Image", Doc: "Image is the [image.Image]."}, {Name: "prevImage", Doc: "prevImage is the cached last [Image.Image]."}, {Name: "prevRenderImage", Doc: "prevRenderImage is the cached last rendered image with any transformations applied."}, {Name: "prevObjectFit", Doc: "prevObjectFit is the cached [styles.Style.ObjectFit] of the last rendered image."}, {Name: "prevSize", Doc: "prevSize is the cached allocated size for the last rendered image."}}})
// NewImage returns a new [Image] with the given optional parent:
// Image is a widget that renders an [image.Image].
// See [styles.Style.ObjectFit] to control the image rendering within
// the allocated size. The default minimum requested size is the pixel
// size in [units.Dp] units (1/160th of an inch).
func NewImage(parent ...tree.Node) *Image { return tree.New[Image](parent...) }
// SetImage sets the [Image.Image]:
// Image is the [image.Image].
func (t *Image) SetImage(v image.Image) *Image { t.Image = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.InlineList", IDName: "inline-list", Doc: "InlineList represents a slice within a single line of value widgets.\nThis is typically used for smaller slices.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Slice", Doc: "Slice is the slice that we are viewing."}, {Name: "isArray", Doc: "isArray is whether the slice is actually an array."}}})
// NewInlineList returns a new [InlineList] with the given optional parent:
// InlineList represents a slice within a single line of value widgets.
// This is typically used for smaller slices.
func NewInlineList(parent ...tree.Node) *InlineList { return tree.New[InlineList](parent...) }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Inspector", IDName: "inspector", Doc: "Inspector represents a [tree.Node] with a [Tree] and a [Form].", Methods: []types.Method{{Name: "save", Doc: "save saves the tree to current filename, in a standard JSON-formatted file.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}, {Name: "saveAs", Doc: "saveAs saves tree to given filename, in a standard JSON-formatted file", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename"}, Returns: []string{"error"}}, {Name: "open", Doc: "open opens tree from given filename, in a standard JSON-formatted file", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename"}, Returns: []string{"error"}}, {Name: "toggleSelectionMode", Doc: "toggleSelectionMode toggles the editor between selection mode or not.\nIn selection mode, bounding boxes are rendered around each Widget,\nand clicking on a Widget pulls it up in the inspector.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "inspectApp", Doc: "inspectApp displays [TheApp].", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Root", Doc: "Root is the root of the tree being edited."}, {Name: "currentNode", Doc: "currentNode is the currently selected node in the tree."}, {Name: "filename", Doc: "filename is the current filename for saving / loading"}, {Name: "treeWidget"}}})
// NewInspector returns a new [Inspector] with the given optional parent:
// Inspector represents a [tree.Node] with a [Tree] and a [Form].
func NewInspector(parent ...tree.Node) *Inspector { return tree.New[Inspector](parent...) }
// SetRoot sets the [Inspector.Root]:
// Root is the root of the tree being edited.
func (t *Inspector) SetRoot(v tree.Node) *Inspector { t.Root = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.KeyMapButton", IDName: "key-map-button", Doc: "KeyMapButton represents a [keymap.MapName] value with a button.", Embeds: []types.Field{{Name: "Button"}}, Fields: []types.Field{{Name: "MapName"}}})
// NewKeyMapButton returns a new [KeyMapButton] with the given optional parent:
// KeyMapButton represents a [keymap.MapName] value with a button.
func NewKeyMapButton(parent ...tree.Node) *KeyMapButton { return tree.New[KeyMapButton](parent...) }
// SetMapName sets the [KeyMapButton.MapName]
func (t *KeyMapButton) SetMapName(v keymap.MapName) *KeyMapButton { t.MapName = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.KeyChordButton", IDName: "key-chord-button", Doc: "KeyChordButton represents a [key.Chord] value with a button.", Embeds: []types.Field{{Name: "Button"}}, Fields: []types.Field{{Name: "Chord"}}})
// NewKeyChordButton returns a new [KeyChordButton] with the given optional parent:
// KeyChordButton represents a [key.Chord] value with a button.
func NewKeyChordButton(parent ...tree.Node) *KeyChordButton {
return tree.New[KeyChordButton](parent...)
}
// SetChord sets the [KeyChordButton.Chord]
func (t *KeyChordButton) SetChord(v key.Chord) *KeyChordButton { t.Chord = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.KeyedList", IDName: "keyed-list", Doc: "KeyedList represents a map value using two columns of editable key and value widgets.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Map", Doc: "Map is the pointer to the map that we are viewing."}, {Name: "Inline", Doc: "Inline is whether to display the map in one line."}, {Name: "SortByValues", Doc: "SortByValues is whether to sort by values instead of keys."}, {Name: "ncols", Doc: "ncols is the number of columns to display if the keyed list is not inline."}}})
// NewKeyedList returns a new [KeyedList] with the given optional parent:
// KeyedList represents a map value using two columns of editable key and value widgets.
func NewKeyedList(parent ...tree.Node) *KeyedList { return tree.New[KeyedList](parent...) }
// SetMap sets the [KeyedList.Map]:
// Map is the pointer to the map that we are viewing.
func (t *KeyedList) SetMap(v any) *KeyedList { t.Map = v; return t }
// SetInline sets the [KeyedList.Inline]:
// Inline is whether to display the map in one line.
func (t *KeyedList) SetInline(v bool) *KeyedList { t.Inline = v; return t }
// SetSortByValues sets the [KeyedList.SortByValues]:
// SortByValues is whether to sort by values instead of keys.
func (t *KeyedList) SetSortByValues(v bool) *KeyedList { t.SortByValues = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.List", IDName: "list", Doc: "List represents a slice value with a list of value widgets and optional index widgets.\nUse [ListBase.BindSelect] to make the list designed for item selection.", Embeds: []types.Field{{Name: "ListBase"}}, Fields: []types.Field{{Name: "ListStyler", Doc: "ListStyler is an optional styler for list items."}}})
// NewList returns a new [List] with the given optional parent:
// List represents a slice value with a list of value widgets and optional index widgets.
// Use [ListBase.BindSelect] to make the list designed for item selection.
func NewList(parent ...tree.Node) *List { return tree.New[List](parent...) }
// SetListStyler sets the [List.ListStyler]:
// ListStyler is an optional styler for list items.
func (t *List) SetListStyler(v ListStyler) *List { t.ListStyler = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.ListBase", IDName: "list-base", Doc: "ListBase is the base for [List] and [Table] and any other displays\nof array-like data. It automatically computes the number of rows that fit\nwithin its allocated space, and manages the offset view window into the full\nlist of items, and supports row selection, copy / paste, Drag-n-Drop, etc.\nUse [ListBase.BindSelect] to make the list designed for item selection.", Directives: []types.Directive{{Tool: "core", Directive: "no-new"}}, Methods: []types.Method{{Name: "copyIndexes", Doc: "copyIndexes copies selected idxs to system.Clipboard, optionally resetting the selection", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"reset"}}, {Name: "cutIndexes", Doc: "cutIndexes copies selected indexes to system.Clipboard and deletes selected indexes", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "pasteIndex", Doc: "pasteIndex pastes clipboard at given idx", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"idx"}}, {Name: "duplicate", Doc: "duplicate copies selected items and inserts them after current selection --\nreturn idx of start of duplicates if successful, else -1", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"int"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Slice", Doc: "Slice is the pointer to the slice that we are viewing."}, {Name: "ShowIndexes", Doc: "ShowIndexes is whether to show the indexes of rows or not (default false)."}, {Name: "MinRows", Doc: "MinRows specifies the minimum number of rows to display, to ensure\nat least this amount is displayed."}, {Name: "SelectedValue", Doc: "SelectedValue is the current selection value.\nIf it is set, it is used as the initially selected value."}, {Name: "SelectedIndex", Doc: "SelectedIndex is the index of the currently selected item."}, {Name: "InitSelectedIndex", Doc: "InitSelectedIndex is the index of the row to select at the start."}, {Name: "SelectedIndexes", Doc: "SelectedIndexes is a list of currently selected slice indexes."}, {Name: "lastClick", Doc: "lastClick is the last row that has been clicked on.\nThis is used to prevent erroneous double click events\nfrom being sent when the user clicks on multiple different\nrows in quick succession."}, {Name: "normalCursor", Doc: "normalCursor is the cached cursor to display when there\nis no row being hovered."}, {Name: "currentCursor", Doc: "currentCursor is the cached cursor that should currently be\ndisplayed."}, {Name: "sliceUnderlying", Doc: "sliceUnderlying is the underlying slice value."}, {Name: "hoverRow", Doc: "currently hovered row"}, {Name: "draggedIndexes", Doc: "list of currently dragged indexes"}, {Name: "VisibleRows", Doc: "VisibleRows is the total number of rows visible in allocated display size."}, {Name: "StartIndex", Doc: "StartIndex is the starting slice index of visible rows."}, {Name: "SliceSize", Doc: "SliceSize is the size of the slice."}, {Name: "MakeIter", Doc: "MakeIter is the iteration through the configuration process,\nwhich is reset when a new slice type is set."}, {Name: "tmpIndex", Doc: "temp idx state for e.g., dnd"}, {Name: "elementValue", Doc: "elementValue is a [reflect.Value] representation of the underlying element type\nwhich is used whenever there are no slice elements available"}, {Name: "maxWidth", Doc: "maximum width of value column in chars, if string"}, {Name: "ReadOnlyKeyNav", Doc: "ReadOnlyKeyNav is whether support key navigation when ReadOnly (default true).\nIt uses a capture of up / down events to manipulate selection, not focus."}, {Name: "SelectMode", Doc: "SelectMode is whether to be in select rows mode or editing mode."}, {Name: "ReadOnlyMultiSelect", Doc: "ReadOnlyMultiSelect: if list is ReadOnly, default selection mode is to\nchoose one row only. If this is true, standard multiple selection logic\nwith modifier keys is instead supported."}, {Name: "InFocusGrab", Doc: "InFocusGrab is a guard for recursive focus grabbing."}, {Name: "isArray", Doc: "isArray is whether the slice is actually an array."}, {Name: "ListGrid", Doc: "ListGrid is the [ListGrid] widget."}}})
// SetShowIndexes sets the [ListBase.ShowIndexes]:
// ShowIndexes is whether to show the indexes of rows or not (default false).
func (t *ListBase) SetShowIndexes(v bool) *ListBase { t.ShowIndexes = v; return t }
// SetMinRows sets the [ListBase.MinRows]:
// MinRows specifies the minimum number of rows to display, to ensure
// at least this amount is displayed.
func (t *ListBase) SetMinRows(v int) *ListBase { t.MinRows = v; return t }
// SetSelectedValue sets the [ListBase.SelectedValue]:
// SelectedValue is the current selection value.
// If it is set, it is used as the initially selected value.
func (t *ListBase) SetSelectedValue(v any) *ListBase { t.SelectedValue = v; return t }
// SetSelectedIndex sets the [ListBase.SelectedIndex]:
// SelectedIndex is the index of the currently selected item.
func (t *ListBase) SetSelectedIndex(v int) *ListBase { t.SelectedIndex = v; return t }
// SetInitSelectedIndex sets the [ListBase.InitSelectedIndex]:
// InitSelectedIndex is the index of the row to select at the start.
func (t *ListBase) SetInitSelectedIndex(v int) *ListBase { t.InitSelectedIndex = v; return t }
// SetReadOnlyKeyNav sets the [ListBase.ReadOnlyKeyNav]:
// ReadOnlyKeyNav is whether support key navigation when ReadOnly (default true).
// It uses a capture of up / down events to manipulate selection, not focus.
func (t *ListBase) SetReadOnlyKeyNav(v bool) *ListBase { t.ReadOnlyKeyNav = v; return t }
// SetReadOnlyMultiSelect sets the [ListBase.ReadOnlyMultiSelect]:
// ReadOnlyMultiSelect: if list is ReadOnly, default selection mode is to
// choose one row only. If this is true, standard multiple selection logic
// with modifier keys is instead supported.
func (t *ListBase) SetReadOnlyMultiSelect(v bool) *ListBase { t.ReadOnlyMultiSelect = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.ListGrid", IDName: "list-grid", Doc: "ListGrid handles the resizing logic for all [Lister]s.", Directives: []types.Directive{{Tool: "core", Directive: "no-new"}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "minRows", Doc: "minRows is set from parent [List]"}, {Name: "rowHeight", Doc: "height of a single row, computed during layout"}, {Name: "visibleRows", Doc: "total number of rows visible in allocated display size"}, {Name: "bgStripe", Doc: "Various computed backgrounds"}, {Name: "bgSelect", Doc: "Various computed backgrounds"}, {Name: "bgSelectStripe", Doc: "Various computed backgrounds"}, {Name: "bgHover", Doc: "Various computed backgrounds"}, {Name: "bgHoverStripe", Doc: "Various computed backgrounds"}, {Name: "bgHoverSelect", Doc: "Various computed backgrounds"}, {Name: "bgHoverSelectStripe", Doc: "Various computed backgrounds"}, {Name: "lastBackground", Doc: "lastBackground is the background for which modified\nbackgrounds were computed -- don't update if same"}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Meter", IDName: "meter", Doc: "Meter is a widget that renders a current value on as a filled\nbar/circle/semicircle relative to a minimum and maximum potential\nvalue.", Embeds: []types.Field{{Name: "WidgetBase"}}, Fields: []types.Field{{Name: "Type", Doc: "Type is the styling type of the meter."}, {Name: "Value", Doc: "Value is the current value of the meter.\nIt defaults to 0.5."}, {Name: "Min", Doc: "Min is the minimum possible value of the meter.\nIt defaults to 0."}, {Name: "Max", Doc: "Max is the maximum possible value of the meter.\nIt defaults to 1."}, {Name: "Text", Doc: "Text, for [MeterCircle] and [MeterSemicircle], is the\ntext to render inside of the circle/semicircle."}, {Name: "ValueColor", Doc: "ValueColor is the image color that will be used to\nrender the filled value bar. It should be set in a Styler."}, {Name: "Width", Doc: "Width, for [MeterCircle] and [MeterSemicircle], is the\nwidth of the circle/semicircle. It should be set in a Styler."}}})
// NewMeter returns a new [Meter] with the given optional parent:
// Meter is a widget that renders a current value on as a filled
// bar/circle/semicircle relative to a minimum and maximum potential
// value.
func NewMeter(parent ...tree.Node) *Meter { return tree.New[Meter](parent...) }
// SetType sets the [Meter.Type]:
// Type is the styling type of the meter.
func (t *Meter) SetType(v MeterTypes) *Meter { t.Type = v; return t }
// SetValue sets the [Meter.Value]:
// Value is the current value of the meter.
// It defaults to 0.5.
func (t *Meter) SetValue(v float32) *Meter { t.Value = v; return t }
// SetMin sets the [Meter.Min]:
// Min is the minimum possible value of the meter.
// It defaults to 0.
func (t *Meter) SetMin(v float32) *Meter { t.Min = v; return t }
// SetMax sets the [Meter.Max]:
// Max is the maximum possible value of the meter.
// It defaults to 1.
func (t *Meter) SetMax(v float32) *Meter { t.Max = v; return t }
// SetText sets the [Meter.Text]:
// Text, for [MeterCircle] and [MeterSemicircle], is the
// text to render inside of the circle/semicircle.
func (t *Meter) SetText(v string) *Meter { t.Text = v; return t }
// SetValueColor sets the [Meter.ValueColor]:
// ValueColor is the image color that will be used to
// render the filled value bar. It should be set in a Styler.
func (t *Meter) SetValueColor(v image.Image) *Meter { t.ValueColor = v; return t }
// SetWidth sets the [Meter.Width]:
// Width, for [MeterCircle] and [MeterSemicircle], is the
// width of the circle/semicircle. It should be set in a Styler.
func (t *Meter) SetWidth(v units.Value) *Meter { t.Width = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Pages", IDName: "pages", Doc: "Pages is a frame that can easily swap its content between that of\ndifferent possible pages.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Page", Doc: "Page is the currently open page."}, {Name: "Pages", Doc: "Pages is a map of page names to functions that configure a page."}, {Name: "page", Doc: "page is the currently rendered page."}}})
// NewPages returns a new [Pages] with the given optional parent:
// Pages is a frame that can easily swap its content between that of
// different possible pages.
func NewPages(parent ...tree.Node) *Pages { return tree.New[Pages](parent...) }
// SetPage sets the [Pages.Page]:
// 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 [Scene.Pixels] image. 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: "PaintContext", Doc: "paint context for rendering"}, {Name: "Pixels", Doc: "live pixels that we render into"}, {Name: "Events", Doc: "event manager for this scene"}, {Name: "Stage", Doc: "current stage in which this Scene is set"}, {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: "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 Pixels image."}, {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].
// This can be used to set global configuration and styling for all
// widgets in conjunction with [App.SceneInit].
func (t *Scene) SetWidgetInit(v func(w Widget)) *Scene { t.WidgetInit = v; return t }
// SetData sets the [Scene.Data]:
// Data is the optional data value being represented by this scene.
// Used e.g., for recycling views of a given item instead of creating new one.
func (t *Scene) SetData(v any) *Scene { t.Data = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Separator", IDName: "separator", Doc: "Separator draws a separator line. It goes in the direction\nspecified by [styles.Style.Direction].", Embeds: []types.Field{{Name: "WidgetBase"}}})
// NewSeparator returns a new [Separator] with the given optional parent:
// Separator draws a separator line. It goes in the direction
// specified by [styles.Style.Direction].
func NewSeparator(parent ...tree.Node) *Separator { return tree.New[Separator](parent...) }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.AppearanceSettingsData", IDName: "appearance-settings-data", Doc: "AppearanceSettingsData is the data type for the global Cogent Core appearance settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Methods: []types.Method{{Name: "Apply", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "deleteSavedWindowGeometries", Doc: "deleteSavedWindowGeometries deletes the file that saves the position and size of\neach window, by screen, and clear current in-memory cache. You shouldn't generally\nneed to do this, but sometimes it is useful for testing or windows that are\nshowing up in bad places that you can't recover from.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "SaveScreenZoom", Doc: "SaveScreenZoom saves the current zoom factor for the current screen,\nwhich will then be used for this screen instead of overall default.\nUse the Control +/- keyboard shortcut to modify the screen zoom level.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "SettingsBase"}}, Fields: []types.Field{{Name: "Theme", Doc: "the color theme."}, {Name: "Color", Doc: "the primary color used to generate the color scheme."}, {Name: "Zoom", Doc: "overall zoom factor as a percentage of the default zoom.\nUse Control +/- keyboard shortcut to change zoom level anytime.\nScreen-specific zoom factor will be used if present, see 'Screens' field."}, {Name: "Spacing", Doc: "the overall spacing factor as a percentage of the default amount of spacing\n(higher numbers lead to more space and lower numbers lead to higher density)."}, {Name: "FontSize", Doc: "the overall font size factor applied to all text as a percentage\nof the default font size (higher numbers lead to larger text)."}, {Name: "ZebraStripes", Doc: "the amount that alternating rows are highlighted when showing\ntabular data (set to 0 to disable zebra striping)."}, {Name: "Screens", Doc: "screen-specific settings, which will override overall defaults if set,\nso different screens can use different zoom levels.\nUse 'Save screen zoom' in the toolbar to save the current zoom for the current\nscreen, and Control +/- keyboard shortcut to change this zoom level anytime."}, {Name: "Highlighting", Doc: "text highlighting style / theme."}, {Name: "Font", Doc: "Font is the default font family to use."}, {Name: "MonoFont", Doc: "MonoFont is the default mono-spaced font family to use."}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.DeviceSettingsData", IDName: "device-settings-data", Doc: "DeviceSettingsData is the data type for the device settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Embeds: []types.Field{{Name: "SettingsBase"}}, Fields: []types.Field{{Name: "KeyMap", Doc: "The keyboard shortcut map to use"}, {Name: "KeyMaps", Doc: "The keyboard shortcut maps available as options for Key map.\nIf you do not want to have custom key maps, you should leave\nthis unset so that you always have the latest standard key maps."}, {Name: "DoubleClickInterval", Doc: "The maximum time interval between button press events to count as a double-click"}, {Name: "ScrollWheelSpeed", Doc: "How fast the scroll wheel moves, which is typically pixels per wheel step\nbut units can be arbitrary. It is generally impossible to standardize speed\nand variable across devices, and we don't have access to the system settings,\nso unfortunately you have to set it here."}, {Name: "ScrollFocusTime", Doc: "The duration over which the current scroll widget retains scroll focus,\nsuch that subsequent scroll events are sent to it."}, {Name: "SlideStartTime", Doc: "The amount of time to wait before initiating a slide event\n(as opposed to a basic press event)"}, {Name: "DragStartTime", Doc: "The amount of time to wait before initiating a drag (drag and drop) event\n(as opposed to a basic press or slide event)"}, {Name: "RepeatClickTime", Doc: "The amount of time to wait between each repeat click event,\nwhen the mouse is pressed down. The first click is 8x this."}, {Name: "DragStartDistance", Doc: "The number of pixels that must be moved before initiating a slide/drag\nevent (as opposed to a basic press event)"}, {Name: "LongHoverTime", Doc: "The amount of time to wait before initiating a long hover event (e.g., for opening a tooltip)"}, {Name: "LongHoverStopDistance", Doc: "The maximum number of pixels that mouse can move and still register a long hover event"}, {Name: "LongPressTime", Doc: "The amount of time to wait before initiating a long press event (e.g., for opening a tooltip)"}, {Name: "LongPressStopDistance", Doc: "The maximum number of pixels that mouse/finger can move and still register a long press event"}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.ScreenSettings", IDName: "screen-settings", Doc: "ScreenSettings are per-screen settings that override the global settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "Zoom", Doc: "overall zoom factor as a percentage of the default zoom"}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.SystemSettingsData", IDName: "system-settings-data", Doc: "SystemSettingsData is the data type of the global Cogent Core settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Methods: []types.Method{{Name: "Apply", Doc: "Apply detailed settings to all the relevant settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "SettingsBase"}}, Fields: []types.Field{{Name: "Editor", Doc: "text editor settings"}, {Name: "Clock24", Doc: "whether to use a 24-hour clock (instead of AM and PM)"}, {Name: "SnackbarTimeout", Doc: "SnackbarTimeout is the default amount of time until snackbars\ndisappear (snackbars show short updates about app processes\nat the bottom of the screen)"}, {Name: "OnlyCloseActiveTab", Doc: "only support closing the currently selected active tab; if this is set to true, pressing the close button on other tabs will take you to that tab, from which you can close it"}, {Name: "BigFileSize", Doc: "the limit of file size, above which user will be prompted before opening / copying, etc."}, {Name: "SavedPathsMax", Doc: "maximum number of saved paths to save in FilePicker"}, {Name: "FontPaths", Doc: "extra font paths, beyond system defaults -- searched first"}, {Name: "User", Doc: "user info, which is partially filled-out automatically if empty when settings are first created"}, {Name: "FavPaths", Doc: "favorite paths, shown in FilePickerer and also editable there"}, {Name: "FilePickerSort", Doc: "column to sort by in FilePicker, and :up or :down for direction -- updated automatically via FilePicker"}, {Name: "MenuMaxHeight", Doc: "the maximum height of any menu popup panel in units of font height;\nscroll bars are enforced beyond that size."}, {Name: "CompleteWaitDuration", Doc: "the amount of time to wait before offering completions"}, {Name: "CompleteMaxItems", Doc: "the maximum number of completions offered in popup"}, {Name: "CursorBlinkTime", Doc: "time interval for cursor blinking on and off -- set to 0 to disable blinking"}, {Name: "LayoutAutoScrollDelay", Doc: "The amount of time to wait before trying to autoscroll again"}, {Name: "LayoutPageSteps", Doc: "number of steps to take in PageUp / Down events in terms of number of items"}, {Name: "LayoutFocusNameTimeout", Doc: "the amount of time between keypresses to combine characters into name to search for within layout -- starts over after this delay"}, {Name: "LayoutFocusNameTabTime", Doc: "the amount of time since last focus name event to allow tab to focus on next element with same name."}, {Name: "MapInlineLength", Doc: "the number of map elements at or below which an inline representation\nof the map will be presented, which is more convenient for small #'s of properties"}, {Name: "StructInlineLength", Doc: "the number of elemental struct fields at or below which an inline representation\nof the struct will be presented, which is more convenient for small structs"}, {Name: "SliceInlineLength", Doc: "the number of slice elements below which inline will be used"}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.User", IDName: "user", Doc: "User basic user information that might be needed for different apps", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Embeds: []types.Field{{Name: "User"}}, Fields: []types.Field{{Name: "Email", Doc: "default email address -- e.g., for recording changes in a version control system"}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.EditorSettings", IDName: "editor-settings", Doc: "EditorSettings contains text editor settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "TabSize", Doc: "size of a tab, in chars; also determines indent level for space indent"}, {Name: "SpaceIndent", Doc: "use spaces for indentation, otherwise tabs"}, {Name: "WordWrap", Doc: "wrap lines at word boundaries; otherwise long lines scroll off the end"}, {Name: "LineNumbers", Doc: "whether to show line numbers"}, {Name: "Completion", Doc: "use the completion system to suggest options while typing"}, {Name: "SpellCorrect", Doc: "suggest corrections for unknown words while typing"}, {Name: "AutoIndent", Doc: "automatically indent lines when enter, tab, }, etc pressed"}, {Name: "EmacsUndo", Doc: "use emacs-style undo, where after a non-undo command, all the current undo actions are added to the undo stack, such that a subsequent undo is actually a redo"}, {Name: "DepthColor", Doc: "colorize the background according to nesting depth"}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.favoritePathItem", IDName: "favorite-path-item", Doc: "favoritePathItem represents one item in a favorite path list, for display of\nfavorites. Is an ordered list instead of a map because user can organize\nin order", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "Icon", Doc: "icon for item"}, {Name: "Name", Doc: "name of the favorite item"}, {Name: "Path", Doc: "the path of the favorite item"}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.DebugSettingsData", IDName: "debug-settings-data", Doc: "DebugSettingsData is the data type for debugging settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Embeds: []types.Field{{Name: "SettingsBase"}}, Fields: []types.Field{{Name: "UpdateTrace", Doc: "Print a trace of updates that trigger re-rendering"}, {Name: "RenderTrace", Doc: "Print a trace of the nodes rendering"}, {Name: "LayoutTrace", Doc: "Print a trace of all layouts"}, {Name: "LayoutTraceDetail", Doc: "Print more detailed info about the underlying layout computations"}, {Name: "WindowEventTrace", Doc: "Print a trace of window events"}, {Name: "WindowRenderTrace", Doc: "Print the stack trace leading up to win publish events\nwhich are expensive"}, {Name: "WindowGeometryTrace", Doc: "Print a trace of window geometry saving / loading functions"}, {Name: "KeyEventTrace", Doc: "Print a trace of keyboard events"}, {Name: "EventTrace", Doc: "Print a trace of event handling"}, {Name: "FocusTrace", Doc: "Print a trace of focus changes"}, {Name: "DNDTrace", Doc: "Print a trace of DND event handling"}, {Name: "DisableWindowGeometrySaver", Doc: "DisableWindowGeometrySaver disables the saving and loading of window geometry\ndata to allow for easier testing of window manipulation code."}, {Name: "GoCompleteTrace", Doc: "Print a trace of Go language completion and lookup process"}, {Name: "GoTypeTrace", Doc: "Print a trace of Go language type parsing and inference process"}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Slider", IDName: "slider", Doc: "Slider is a slideable widget that provides slider functionality with a draggable\nthumb and a clickable track. The [styles.Style.Direction] determines the direction\nin which the slider slides.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Type", Doc: "Type is the type of the slider, which determines its visual\nand functional properties. The default type, [SliderSlider],\nshould work for most end-user use cases."}, {Name: "Value", Doc: "Value is the current value, represented by the position of the thumb.\nIt defaults to 0.5."}, {Name: "Min", Doc: "Min is the minimum possible value.\nIt defaults to 0."}, {Name: "Max", Doc: "Max is the maximum value supported.\nIt defaults to 1."}, {Name: "Step", Doc: "Step is the amount that the arrow keys increment/decrement the value by.\nIt defaults to 0.1."}, {Name: "EnforceStep", Doc: "EnforceStep is whether to ensure that the value is always\na multiple of [Slider.Step]."}, {Name: "PageStep", Doc: "PageStep is the amount that the PageUp and PageDown keys\nincrement/decrement the value by.\nIt defaults to 0.2, and will be at least as big as [Slider.Step]."}, {Name: "Icon", Doc: "Icon is an optional icon to use for the dragging thumb."}, {Name: "visiblePercent", Doc: "For Scrollbar type only: proportion (1 max) of the full range of scrolled data\nthat is currently visible. This determines the thumb size and range of motion:\nif 1, full slider is the thumb and no motion is possible."}, {Name: "ThumbSize", Doc: "ThumbSize is the size of the thumb as a proportion of the slider thickness,\nwhich is the content size (inside the padding)."}, {Name: "TrackSize", Doc: "TrackSize is the proportion of slider thickness for the visible track\nfor the [SliderSlider] type. It is often thinner than the thumb, achieved\nby values less than 1 (0.5 default)."}, {Name: "InputThreshold", Doc: "InputThreshold is the threshold for the amount of change in scroll\nvalue before emitting an input event."}, {Name: "Precision", Doc: "Precision specifies the precision of decimal places (total, not after the decimal\npoint) to use in representing the number. This helps to truncate small weird\nfloating point values."}, {Name: "ValueColor", Doc: "ValueColor is the background color that is used for styling the selected value\nsection of the slider. It should be set in a Styler, just like the main style\nobject is. If it is set to transparent, no value is rendered, so the value\nsection of the slider just looks like the rest of the slider."}, {Name: "ThumbColor", Doc: "ThumbColor is the background color that is used for styling the thumb (handle)\nof the slider. It should be set in a Styler, just like the main style object is.\nIf it is set to transparent, no thumb is rendered, so the thumb section of the\nslider just looks like the rest of the slider."}, {Name: "StayInView", Doc: "StayInView is whether to keep the slider (typically a [SliderScrollbar]) within\nthe parent [Scene] bounding box, if the parent is in view. This is the default\nbehavior for [Frame] scrollbars, and setting this flag replicates that behavior\nin other scrollbars."}, {Name: "pos", Doc: "logical position of the slider relative to Size"}, {Name: "lastValue", Doc: "previous Change event emitted value; don't re-emit Change if it is the same"}, {Name: "prevSlide", Doc: "previous sliding value (for computing the Input change)"}, {Name: "slideStartPos", Doc: "underlying drag position of slider; not subject to snapping"}}})
// NewSlider returns a new [Slider] with the given optional parent:
// Slider is a slideable widget that provides slider functionality with a draggable
// thumb and a clickable track. The [styles.Style.Direction] determines the direction
// in which the slider slides.
func NewSlider(parent ...tree.Node) *Slider { return tree.New[Slider](parent...) }
// SetType sets the [Slider.Type]:
// Type is the type of the slider, which determines its visual
// and functional properties. The default type, [SliderSlider],
// should work for most end-user use cases.
func (t *Slider) SetType(v SliderTypes) *Slider { t.Type = v; return t }
// SetMin sets the [Slider.Min]:
// Min is the minimum possible value.
// It defaults to 0.
func (t *Slider) SetMin(v float32) *Slider { t.Min = v; return t }
// SetMax sets the [Slider.Max]:
// Max is the maximum value supported.
// It defaults to 1.
func (t *Slider) SetMax(v float32) *Slider { t.Max = v; return t }
// SetStep sets the [Slider.Step]:
// Step is the amount that the arrow keys increment/decrement the value by.
// It defaults to 0.1.
func (t *Slider) SetStep(v float32) *Slider { t.Step = v; return t }
// SetEnforceStep sets the [Slider.EnforceStep]:
// EnforceStep is whether to ensure that the value is always
// a multiple of [Slider.Step].
func (t *Slider) SetEnforceStep(v bool) *Slider { t.EnforceStep = v; return t }
// SetPageStep sets the [Slider.PageStep]:
// PageStep is the amount that the PageUp and PageDown keys
// increment/decrement the value by.
// It defaults to 0.2, and will be at least as big as [Slider.Step].
func (t *Slider) SetPageStep(v float32) *Slider { t.PageStep = v; return t }
// SetIcon sets the [Slider.Icon]:
// Icon is an optional icon to use for the dragging thumb.
func (t *Slider) SetIcon(v icons.Icon) *Slider { t.Icon = v; return t }
// SetThumbSize sets the [Slider.ThumbSize]:
// ThumbSize is the size of the thumb as a proportion of the slider thickness,
// which is the content size (inside the padding).
func (t *Slider) SetThumbSize(v math32.Vector2) *Slider { t.ThumbSize = v; return t }
// SetTrackSize sets the [Slider.TrackSize]:
// TrackSize is the proportion of slider thickness for the visible track
// for the [SliderSlider] type. It is often thinner than the thumb, achieved
// by values less than 1 (0.5 default).
func (t *Slider) SetTrackSize(v float32) *Slider { t.TrackSize = v; return t }
// SetInputThreshold sets the [Slider.InputThreshold]:
// InputThreshold is the threshold for the amount of change in scroll
// value before emitting an input event.
func (t *Slider) SetInputThreshold(v float32) *Slider { t.InputThreshold = v; return t }
// SetPrecision sets the [Slider.Precision]:
// Precision specifies the precision of decimal places (total, not after the decimal
// point) to use in representing the number. This helps to truncate small weird
// floating point values.
func (t *Slider) SetPrecision(v int) *Slider { t.Precision = v; return t }
// SetValueColor sets the [Slider.ValueColor]:
// ValueColor is the background color that is used for styling the selected value
// section of the slider. It should be set in a Styler, just like the main style
// object is. If it is set to transparent, no value is rendered, so the value
// section of the slider just looks like the rest of the slider.
func (t *Slider) SetValueColor(v image.Image) *Slider { t.ValueColor = v; return t }
// SetThumbColor sets the [Slider.ThumbColor]:
// ThumbColor is the background color that is used for styling the thumb (handle)
// of the slider. It should be set in a Styler, just like the main style object is.
// If it is set to transparent, no thumb is rendered, so the thumb section of the
// slider just looks like the rest of the slider.
func (t *Slider) SetThumbColor(v image.Image) *Slider { t.ThumbColor = v; return t }
// SetStayInView sets the [Slider.StayInView]:
// StayInView is whether to keep the slider (typically a [SliderScrollbar]) within
// the parent [Scene] bounding box, if the parent is in view. This is the default
// behavior for [Frame] scrollbars, and setting this flag replicates that behavior
// in other scrollbars.
func (t *Slider) SetStayInView(v bool) *Slider { t.StayInView = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Spinner", IDName: "spinner", Doc: "Spinner is a [TextField] for editing numerical values. It comes with\nfields, methods, buttons, and shortcuts to enhance numerical value editing.", Embeds: []types.Field{{Name: "TextField"}}, Fields: []types.Field{{Name: "Value", Doc: "Value is the current value."}, {Name: "HasMin", Doc: "HasMin is whether there is a minimum value to enforce.\nIt should be set using [Spinner.SetMin]."}, {Name: "Min", Doc: "Min, if [Spinner.HasMin] is true, is the the minimum value in range.\nIt should be set using [Spinner.SetMin]."}, {Name: "HasMax", Doc: "HaxMax is whether there is a maximum value to enforce.\nIt should be set using [Spinner.SetMax]."}, {Name: "Max", Doc: "Max, if [Spinner.HasMax] is true, is the maximum value in range.\nIt should be set using [Spinner.SetMax]."}, {Name: "Step", Doc: "Step is the amount that the up and down buttons and arrow keys\nincrement/decrement the value by. It defaults to 0.1."}, {Name: "EnforceStep", Doc: "EnforceStep is whether to ensure that the value of the spinner\nis always a multiple of [Spinner.Step]."}, {Name: "PageStep", Doc: "PageStep is the amount that the PageUp and PageDown keys\nincrement/decrement the value by.\nIt defaults to 0.2, and will be at least as big as [Spinner.Step]."}, {Name: "Precision", Doc: "Precision specifies the precision of decimal places\n(total, not after the decimal point) to use in\nrepresenting the number. This helps to truncate\nsmall weird floating point values."}, {Name: "Format", Doc: "Format is the format string to use for printing the value.\nIf it unset, %g is used. If it is decimal based\n(ends in d, b, c, o, O, q, x, X, or U) then the value is\nconverted to decimal prior to printing."}}})
// NewSpinner returns a new [Spinner] with the given optional parent:
// Spinner is a [TextField] for editing numerical values. It comes with
// fields, methods, buttons, and shortcuts to enhance numerical value editing.
func NewSpinner(parent ...tree.Node) *Spinner { return tree.New[Spinner](parent...) }
// SetStep sets the [Spinner.Step]:
// Step is the amount that the up and down buttons and arrow keys
// increment/decrement the value by. It defaults to 0.1.
func (t *Spinner) SetStep(v float32) *Spinner { t.Step = v; return t }
// SetEnforceStep sets the [Spinner.EnforceStep]:
// EnforceStep is whether to ensure that the value of the spinner
// is always a multiple of [Spinner.Step].
func (t *Spinner) SetEnforceStep(v bool) *Spinner { t.EnforceStep = v; return t }
// SetPageStep sets the [Spinner.PageStep]:
// PageStep is the amount that the PageUp and PageDown keys
// increment/decrement the value by.
// It defaults to 0.2, and will be at least as big as [Spinner.Step].
func (t *Spinner) SetPageStep(v float32) *Spinner { t.PageStep = v; return t }
// SetPrecision sets the [Spinner.Precision]:
// Precision specifies the precision of decimal places
// (total, not after the decimal point) to use in
// representing the number. This helps to truncate
// small weird floating point values.
func (t *Spinner) SetPrecision(v int) *Spinner { t.Precision = v; return t }
// SetFormat sets the [Spinner.Format]:
// Format is the format string to use for printing the value.
// If it unset, %g is used. If it is decimal based
// (ends in d, b, c, o, O, q, x, X, or U) then the value is
// converted to decimal prior to printing.
func (t *Spinner) SetFormat(v string) *Spinner { t.Format = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Splits", IDName: "splits", Doc: "Splits allocates a certain proportion of its space to each of its children,\norganized along [styles.Style.Direction] as the main axis, and supporting\n[SplitsTiles] of 2D splits configurations along the cross axis.\nThere is always a split between each Tile segment along the main axis,\nwith the proportion of the total main axis space per Tile allocated\naccording to normalized Splits factors.\nIf all Tiles are Span then a 1D line is generated. Children are allocated\nin order along the main axis, according to each of the Tiles,\nwhich consume 1 to 4 elements, and have 0 to 3 splits internally.\nThe internal split proportion are stored separately in SubSplits.\nA [Handle] widget is added to the Parts for each split, allowing the user\nto drag the relative size of each splits region.\nIf more complex geometries are required, use nested Splits.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Tiles", Doc: "Tiles specifies the 2D layout of elements along the [styles.Style.Direction]\nmain axis and the orthogonal cross axis. If all Tiles are TileSpan, then\na 1D line is generated. There is always a split between each Tile segment,\nand different tiles consume different numbers of elements in order, and\nhave different numbers of SubSplits. Because each Tile can represent a\ndifferent number of elements, care must be taken to ensure that the full\nset of tiles corresponds to the actual number of children. A default\n1D configuration will be imposed if there is a mismatch."}, {Name: "TileSplits", Doc: "TileSplits is the proportion (0-1 normalized, enforced) of space\nallocated to each Tile element along the main axis.\n0 indicates that an element should be completely collapsed.\nBy default, each element gets the same amount of space."}, {Name: "SubSplits", Doc: "SubSplits contains splits proportions for each Tile element, with\na variable number depending on the Tile. For the First and Second Long\nelements, there are 2 subsets of sub-splits, with 4 total subsplits."}, {Name: "savedSplits", Doc: "savedSplits is a saved version of the Splits that can be restored\nfor dynamic collapse/expand operations."}, {Name: "savedSubSplits", Doc: "savedSubSplits is a saved version of the SubSplits that can be restored\nfor dynamic collapse/expand operations."}, {Name: "handleDirs", Doc: "handleDirs contains the target directions for each of the handles.\nthis is set by parent split in its style function, and consumed\nby each handle in its own style function."}}})
// NewSplits returns a new [Splits] with the given optional parent:
// Splits allocates a certain proportion of its space to each of its children,
// organized along [styles.Style.Direction] as the main axis, and supporting
// [SplitsTiles] of 2D splits configurations along the cross axis.
// There is always a split between each Tile segment along the main axis,
// with the proportion of the total main axis space per Tile allocated
// according to normalized Splits factors.
// If all Tiles are Span then a 1D line is generated. Children are allocated
// in order along the main axis, according to each of the Tiles,
// which consume 1 to 4 elements, and have 0 to 3 splits internally.
// The internal split proportion are stored separately in SubSplits.
// A [Handle] widget is added to the Parts for each split, allowing the user
// to drag the relative size of each splits region.
// If more complex geometries are required, use nested Splits.
func NewSplits(parent ...tree.Node) *Splits { return tree.New[Splits](parent...) }
// SetTiles sets the [Splits.Tiles]:
// Tiles specifies the 2D layout of elements along the [styles.Style.Direction]
// main axis and the orthogonal cross axis. If all Tiles are TileSpan, then
// a 1D line is generated. There is always a split between each Tile segment,
// and different tiles consume different numbers of elements in order, and
// have different numbers of SubSplits. Because each Tile can represent a
// different number of elements, care must be taken to ensure that the full
// set of tiles corresponds to the actual number of children. A default
// 1D configuration will be imposed if there is a mismatch.
func (t *Splits) SetTiles(v ...SplitsTiles) *Splits { t.Tiles = v; return t }
// SetTileSplits sets the [Splits.TileSplits]:
// TileSplits is the proportion (0-1 normalized, enforced) of space
// allocated to each Tile element along the main axis.
// 0 indicates that an element should be completely collapsed.
// By default, each element gets the same amount of space.
func (t *Splits) SetTileSplits(v ...float32) *Splits { t.TileSplits = v; return t }
// SetSubSplits sets the [Splits.SubSplits]:
// SubSplits contains splits proportions for each Tile element, with
// a variable number depending on the Tile. For the First and Second Long
// 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."}, {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."}}})
// SetContext sets the [Stage.Context]:
// Context is a widget in another scene that requested this stage to be created
// and provides context.
func (t *Stage) SetContext(v Widget) *Stage { t.Context = v; return t }
// SetName sets the [Stage.Name]:
// Name is the name of the Stage, which is generally auto-set
// based on the [Scene.Name].
func (t *Stage) SetName(v string) *Stage { t.Name = v; return t }
// SetTitle sets the [Stage.Title]:
// Title is the title of the Stage, which is generally auto-set
// based on the [Body.Title]. It used for the title of [WindowStage]
// and [DialogStage] types, and for a [Text] title widget if
// [Stage.DisplayTitle] is true.
func (t *Stage) SetTitle(v string) *Stage { t.Title = v; return t }
// SetScreen sets the [Stage.Screen]:
// Screen specifies the screen number on which a new window is opened
// by default on desktop platforms. It defaults to -1, which indicates
// that the first window should open on screen 0 (the default primary
// screen) and any subsequent windows should open on the same screen as
// the currently active window. Regardless, the automatically saved last
// screen of a window with the same [Stage.Title] takes precedence if it exists;
// see the website documentation on window geometry saving for more information.
// Use [TheApp].ScreenByName("name").ScreenNumber to get the screen by name.
func (t *Stage) SetScreen(v int) *Stage { t.Screen = v; return t }
// SetScrim sets the [Stage.Scrim]:
// Scrim, if true, places a darkening scrim over other stages.
func (t *Stage) SetScrim(v bool) *Stage { t.Scrim = v; return t }
// SetClickOff sets the [Stage.ClickOff]:
// ClickOff, if true, dismisses the [Stage] if the user clicks anywhere
// off of the [Stage].
func (t *Stage) SetClickOff(v bool) *Stage { t.ClickOff = v; return t }
// SetNewWindow sets the [Stage.NewWindow]:
// NewWindow, if true, opens a [WindowStage] or [DialogStage] in its own
// separate operating system window ([renderWindow]). This is true by
// default for [WindowStage] on non-mobile platforms, otherwise false.
func (t *Stage) SetNewWindow(v bool) *Stage { t.NewWindow = v; return t }
// SetFullWindow sets the [Stage.FullWindow]:
// FullWindow, if [Stage.NewWindow] is false, makes [DialogStage]s and
// [WindowStage]s take up the entire window they are created in.
func (t *Stage) SetFullWindow(v bool) *Stage { t.FullWindow = v; return t }
// SetMaximized sets the [Stage.Maximized]:
// Maximized is whether to make a window take up the entire screen on desktop
// platforms by default. It is different from [Stage.Fullscreen] in that
// fullscreen makes the window truly fullscreen without decorations
// (such as for a video player), whereas maximized keeps decorations and just
// makes it fill the available space. The automatically saved user previous
// maximized state takes precedence.
func (t *Stage) SetMaximized(v bool) *Stage { t.Maximized = v; return t }
// SetFullscreen sets the [Stage.Fullscreen]:
// Fullscreen is whether to make a window fullscreen on desktop platforms.
// It is different from [Stage.Maximized] in that fullscreen makes
// the window truly fullscreen without decorations (such as for a video player),
// whereas maximized keeps decorations and just makes it fill the available space.
// Not to be confused with [Stage.FullWindow], which is for stages contained within
// another system window. See [Scene.IsFullscreen] and [Scene.SetFullscreen] to
// check and update fullscreen state dynamically on desktop and web platforms
// ([Stage.SetFullscreen] sets the initial state, whereas [Scene.SetFullscreen]
// sets the current state after the [Stage] is already running).
func (t *Stage) SetFullscreen(v bool) *Stage { t.Fullscreen = v; return t }
// SetUseMinSize sets the [Stage.UseMinSize]:
// UseMinSize uses a minimum size as a function of the total available size
// for sizing new windows and dialogs. Otherwise, only the content size is used.
// The saved window position and size takes precedence on multi-window platforms.
func (t *Stage) SetUseMinSize(v bool) *Stage { t.UseMinSize = v; return t }
// SetResizable sets the [Stage.Resizable]:
// Resizable specifies whether a window on desktop platforms can
// be resized by the user, and whether a non-full same-window dialog can
// be resized by the user on any platform. It defaults to true.
func (t *Stage) SetResizable(v bool) *Stage { t.Resizable = v; return t }
// SetTimeout sets the [Stage.Timeout]:
// Timeout, if greater than 0, results in a popup stages disappearing
// after this timeout duration.
func (t *Stage) SetTimeout(v time.Duration) *Stage { t.Timeout = v; return t }
// SetDisplayTitle sets the [Stage.DisplayTitle]:
// DisplayTitle is whether to display the [Stage.Title] using a
// [Text] widget in the top bar. It is on by default for [DialogStage]s
// and off for all other stages.
func (t *Stage) SetDisplayTitle(v bool) *Stage { t.DisplayTitle = v; return t }
// SetPos sets the [Stage.Pos]:
// Pos is the default target position for the [Stage] to be placed within
// the surrounding window or screen in raw pixels. For a new window on desktop
// platforms, the automatically saved user previous window position takes precedence.
func (t *Stage) SetPos(v image.Point) *Stage { t.Pos = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.SVG", IDName: "svg", Doc: "SVG is a Widget that renders an [svg.SVG] object.\nIf it is not [states.ReadOnly], the user can pan and zoom the display.\nBy default, it is [states.ReadOnly].", Methods: []types.Method{{Name: "Open", Doc: "Open opens an XML-formatted SVG file", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename"}, Returns: []string{"error"}}, {Name: "SaveSVG", Doc: "SaveSVG saves the current SVG to an XML-encoded standard SVG file.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename"}, Returns: []string{"error"}}, {Name: "SavePNG", Doc: "SavePNG saves the current rendered SVG image to an PNG image file.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename"}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "WidgetBase"}}, Fields: []types.Field{{Name: "SVG", Doc: "SVG is the SVG drawing to display."}, {Name: "prevSize", Doc: "prevSize is the cached allocated size for the last rendered image."}}})
// NewSVG returns a new [SVG] with the given optional parent:
// SVG is a Widget that renders an [svg.SVG] object.
// If it is not [states.ReadOnly], the user can pan and zoom the display.
// By default, it is [states.ReadOnly].
func NewSVG(parent ...tree.Node) *SVG { return tree.New[SVG](parent...) }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Switch", IDName: "switch", Doc: "Switch is a widget that can toggle between an on and off state.\nIt can be displayed as a switch, chip, checkbox, radio button,\nor segmented button.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Type", Doc: "Type is the styling type of switch.\nIt must be set using [Switch.SetType]."}, {Name: "Text", Doc: "Text is the optional text of the switch."}, {Name: "IconOn", Doc: "IconOn is the icon to use for the on, checked state of the switch."}, {Name: "IconOff", Doc: "Iconoff is the icon to use for the off, unchecked state of the switch."}, {Name: "IconIndeterminate", Doc: "IconIndeterminate is the icon to use for the indeterminate (unknown) state."}}})
// NewSwitch returns a new [Switch] with the given optional parent:
// Switch is a widget that can toggle between an on and off state.
// It can be displayed as a switch, chip, checkbox, radio button,
// or segmented button.
func NewSwitch(parent ...tree.Node) *Switch { return tree.New[Switch](parent...) }
// SetText sets the [Switch.Text]:
// Text is the optional text of the switch.
func (t *Switch) SetText(v string) *Switch { t.Text = v; return t }
// SetIconOn sets the [Switch.IconOn]:
// IconOn is the icon to use for the on, checked state of the switch.
func (t *Switch) SetIconOn(v icons.Icon) *Switch { t.IconOn = v; return t }
// SetIconOff sets the [Switch.IconOff]:
// Iconoff is the icon to use for the off, unchecked state of the switch.
func (t *Switch) SetIconOff(v icons.Icon) *Switch { t.IconOff = v; return t }
// SetIconIndeterminate sets the [Switch.IconIndeterminate]:
// IconIndeterminate is the icon to use for the indeterminate (unknown) state.
func (t *Switch) SetIconIndeterminate(v icons.Icon) *Switch { t.IconIndeterminate = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Switches", IDName: "switches", Doc: "Switches is a widget for containing a set of [Switch]es.\nIt can optionally enforce mutual exclusivity (ie: radio buttons)\nthrough the [Switches.Mutex] field. It supports binding to\n[enums.Enum] and [enums.BitFlag] values with appropriate properties\nautomatically set.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Type", Doc: "Type is the type of switches that will be made."}, {Name: "Items", Doc: "Items are the items displayed to the user."}, {Name: "Mutex", Doc: "Mutex is whether to make the items mutually exclusive\n(checking one turns off all the others)."}, {Name: "AllowNone", Doc: "AllowNone is whether to allow the user to deselect all items.\nIt is on by default."}, {Name: "selectedIndexes", Doc: "selectedIndexes are the indexes in [Switches.Items] of the currently\nselected switch items."}, {Name: "bitFlagValue", Doc: "bitFlagValue is the associated bit flag value if non-nil (for [Value])."}}})
// NewSwitches returns a new [Switches] with the given optional parent:
// Switches is a widget for containing a set of [Switch]es.
// It can optionally enforce mutual exclusivity (ie: radio buttons)
// through the [Switches.Mutex] field. It supports binding to
// [enums.Enum] and [enums.BitFlag] values with appropriate properties
// automatically set.
func NewSwitches(parent ...tree.Node) *Switches { return tree.New[Switches](parent...) }
// SetType sets the [Switches.Type]:
// Type is the type of switches that will be made.
func (t *Switches) SetType(v SwitchTypes) *Switches { t.Type = v; return t }
// SetItems sets the [Switches.Items]:
// Items are the items displayed to the user.
func (t *Switches) SetItems(v ...SwitchItem) *Switches { t.Items = v; return t }
// SetMutex sets the [Switches.Mutex]:
// Mutex is whether to make the items mutually exclusive
// (checking one turns off all the others).
func (t *Switches) SetMutex(v bool) *Switches { t.Mutex = v; return t }
// SetAllowNone sets the [Switches.AllowNone]:
// AllowNone is whether to allow the user to deselect all items.
// It is on by default.
func (t *Switches) SetAllowNone(v bool) *Switches { t.AllowNone = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Table", IDName: "table", Doc: "Table represents a slice of structs as a table, where the fields are\nthe columns and the elements are the rows. It is a full-featured editor with\nmultiple-selection, cut-and-paste, and drag-and-drop.\nUse [ListBase.BindSelect] to make the table designed for item selection.", Embeds: []types.Field{{Name: "ListBase"}}, Fields: []types.Field{{Name: "TableStyler", Doc: "TableStyler is an optional styling function for table items."}, {Name: "SelectedField", Doc: "SelectedField is the current selection field; initially select value in this field."}, {Name: "sortIndex", Doc: "sortIndex is the current sort index."}, {Name: "sortDescending", Doc: "sortDescending is whether the current sort order is descending."}, {Name: "visibleFields", Doc: "visibleFields are the visible fields."}, {Name: "numVisibleFields", Doc: "numVisibleFields is the number of visible fields."}, {Name: "headerWidths", Doc: "headerWidths has the number of characters in each header, per visibleFields."}, {Name: "colMaxWidths", Doc: "colMaxWidths records maximum width in chars of string type fields."}, {Name: "header"}}})
// NewTable returns a new [Table] with the given optional parent:
// Table represents a slice of structs as a table, where the fields are
// the columns and the elements are the rows. It is a full-featured editor with
// multiple-selection, cut-and-paste, and drag-and-drop.
// Use [ListBase.BindSelect] to make the table designed for item selection.
func NewTable(parent ...tree.Node) *Table { return tree.New[Table](parent...) }
// SetTableStyler sets the [Table.TableStyler]:
// TableStyler is an optional styling function for table items.
func (t *Table) SetTableStyler(v TableStyler) *Table { t.TableStyler = v; return t }
// SetSelectedField sets the [Table.SelectedField]:
// SelectedField is the current selection field; initially select value in this field.
func (t *Table) SetSelectedField(v string) *Table { t.SelectedField = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Tabs", IDName: "tabs", Doc: "Tabs divide widgets into logical groups and give users the ability\nto freely navigate between them using tab buttons.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Type", Doc: "Type is the styling type of the tabs. If it is changed after\nthe tabs are first configured, Update needs to be called on\nthe tabs."}, {Name: "NewTabButton", Doc: "NewTabButton is whether to show a new tab button at the end of the list of tabs."}, {Name: "maxChars", Doc: "maxChars is the maximum number of characters to include in the tab text.\nIt elides text that are longer than that."}, {Name: "CloseIcon", Doc: "CloseIcon is the icon used for tab close buttons.\nIf it is \"\" or [icons.None], the tab is not closeable.\nThe default value is [icons.Close].\nOnly [FunctionalTabs] can be closed; all other types of\ntabs will not render a close button and can not be closed."}, {Name: "mu", Doc: "mu is a mutex protecting updates to tabs. Tabs can be driven\nprogrammatically and via user input so need extra protection."}, {Name: "tabs"}, {Name: "frame"}}})
// NewTabs returns a new [Tabs] with the given optional parent:
// Tabs divide widgets into logical groups and give users the ability
// to freely navigate between them using tab buttons.
func NewTabs(parent ...tree.Node) *Tabs { return tree.New[Tabs](parent...) }
// SetType sets the [Tabs.Type]:
// Type is the styling type of the tabs. If it is changed after
// the tabs are first configured, Update needs to be called on
// the tabs.
func (t *Tabs) SetType(v TabTypes) *Tabs { t.Type = v; return t }
// SetNewTabButton sets the [Tabs.NewTabButton]:
// NewTabButton is whether to show a new tab button at the end of the list of tabs.
func (t *Tabs) SetNewTabButton(v bool) *Tabs { t.NewTabButton = v; return t }
// SetCloseIcon sets the [Tabs.CloseIcon]:
// CloseIcon is the icon used for tab close buttons.
// If it is "" or [icons.None], the tab is not closeable.
// The default value is [icons.Close].
// Only [FunctionalTabs] can be closed; all other types of
// tabs will not render a close button and can not be closed.
func (t *Tabs) SetCloseIcon(v icons.Icon) *Tabs { t.CloseIcon = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Tab", IDName: "tab", Doc: "Tab is a tab button that contains one or more of a label, an icon,\nand a close icon. Tabs should be made using the [Tabs.NewTab] function.", Directives: []types.Directive{{Tool: "core", Directive: "no-new"}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Type", Doc: "Type is the styling type of the tab. This property\nmust be set on the parent [Tabs] for it to work correctly."}, {Name: "Text", Doc: "Text is the text for the tab. If it is blank, no text is shown.\nText is never shown for [NavigationRail] tabs."}, {Name: "Icon", Doc: "Icon is the icon for the tab.\nIf it is \"\" or [icons.None], no icon is shown."}, {Name: "CloseIcon", Doc: "CloseIcon is the icon used as a close button for the tab.\nIf it is \"\" or [icons.None], the tab is not closeable.\nThe default value is [icons.Close].\nOnly [FunctionalTabs] can be closed; all other types of\ntabs will not render a close button and can not be closed."}, {Name: "maxChars", Doc: "maxChars is the maximum number of characters to include in tab text.\nIt elides text that is longer than that."}}})
// SetType sets the [Tab.Type]:
// Type is the styling type of the tab. This property
// must be set on the parent [Tabs] for it to work correctly.
func (t *Tab) SetType(v TabTypes) *Tab { t.Type = v; return t }
// SetText sets the [Tab.Text]:
// Text is the text for the tab. If it is blank, no text is shown.
// Text is never shown for [NavigationRail] tabs.
func (t *Tab) SetText(v string) *Tab { t.Text = v; return t }
// SetIcon sets the [Tab.Icon]:
// Icon is the icon for the tab.
// If it is "" or [icons.None], no icon is shown.
func (t *Tab) SetIcon(v icons.Icon) *Tab { t.Icon = v; return t }
// SetCloseIcon sets the [Tab.CloseIcon]:
// CloseIcon is the icon used as a close button for the tab.
// If it is "" or [icons.None], the tab is not closeable.
// The default value is [icons.Close].
// Only [FunctionalTabs] can be closed; all other types of
// tabs will not render a close button and can not be closed.
func (t *Tab) SetCloseIcon(v icons.Icon) *Tab { t.CloseIcon = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Text", IDName: "text", Doc: "Text is a widget for rendering text. It supports full HTML styling,\nincluding links. By default, text wraps and collapses whitespace, although\nyou can change this by changing [styles.Text.WhiteSpace].", Embeds: []types.Field{{Name: "WidgetBase"}}, Fields: []types.Field{{Name: "Text", Doc: "Text is the text to display."}, {Name: "Type", Doc: "Type is the styling type of text to use.\nIt defaults to [TextBodyLarge]."}, {Name: "paintText", Doc: "paintText is the [paint.Text] for the text."}, {Name: "normalCursor", Doc: "normalCursor is the cached cursor to display when there\nis no link being hovered."}}})
// NewText returns a new [Text] with the given optional parent:
// Text is a widget for rendering text. It supports full HTML styling,
// including links. By default, text wraps and collapses whitespace, although
// you can change this by changing [styles.Text.WhiteSpace].
func NewText(parent ...tree.Node) *Text { return tree.New[Text](parent...) }
// SetText sets the [Text.Text]:
// Text is the text to display.
func (t *Text) SetText(v string) *Text { t.Text = v; return t }
// SetType sets the [Text.Type]:
// Type is the styling type of text to use.
// 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: "SelectColor", Doc: "SelectColor is the color used for the text selection background color.\nIt should be set in a Styler like all other style properties.\nBy default, it is [colors.Scheme.Select.Container]."}, {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: "startPos", Doc: "startPos is the starting display position in the string."}, {Name: "endPos", Doc: "endPos is the ending display position in the string."}, {Name: "cursorPos", Doc: "cursorPos is the current cursor position."}, {Name: "cursorLine", Doc: "cursorLine is the current cursor line position."}, {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: "selectStart", Doc: "selectStart is the starting position of selection in the string."}, {Name: "selectEnd", Doc: "selectEnd is the ending position of selection in the string."}, {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."}, {Name: "numLines", Doc: "number of lines from last render update, for word-wrap version"}, {Name: "fontHeight", Doc: "fontHeight is the font 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"}}})
// NewTextField returns a new [TextField] with the given optional parent:
// TextField is a widget for editing a line of text.
//
// With the default [styles.WhiteSpaceNormal] setting,
// text will wrap onto multiple lines as needed. You can
// call [styles.Style.SetTextWrap](false) to force everything
// to be rendered on a single line. With multi-line wrapped text,
// the text is still treated as a single contiguous line of wrapped text.
func NewTextField(parent ...tree.Node) *TextField { return tree.New[TextField](parent...) }
// TextFieldEmbedder is an interface that all types that embed TextField satisfy
type TextFieldEmbedder interface {
AsTextField() *TextField
}
// AsTextField returns the given value as a value of type TextField if the type
// of the given value embeds TextField, or nil otherwise
func AsTextField(n tree.Node) *TextField {
if t, ok := n.(TextFieldEmbedder); ok {
return t.AsTextField()
}
return nil
}
// AsTextField satisfies the [TextFieldEmbedder] interface
func (t *TextField) AsTextField() *TextField { return t }
// SetType sets the [TextField.Type]:
// Type is the styling type of the text field.
func (t *TextField) SetType(v TextFieldTypes) *TextField { t.Type = v; return t }
// SetPlaceholder sets the [TextField.Placeholder]:
// Placeholder is the text that is displayed
// when the text field is empty.
func (t *TextField) SetPlaceholder(v string) *TextField { t.Placeholder = v; return t }
// SetValidator sets the [TextField.Validator]:
// Validator is a function used to validate the input
// of the text field. If it returns a non-nil error,
// then an error color, icon, and tooltip will be displayed.
func (t *TextField) SetValidator(v func() error) *TextField { t.Validator = v; return t }
// SetLeadingIconOnClick sets the [TextField.LeadingIconOnClick]:
// LeadingIconOnClick, if specified, is the function to call when
// the LeadingIcon is clicked. If this is nil, the leading icon
// will not be interactive. See [TextField.SetLeadingIcon].
func (t *TextField) SetLeadingIconOnClick(v func(e events.Event)) *TextField {
t.LeadingIconOnClick = v
return t
}
// SetTrailingIconOnClick sets the [TextField.TrailingIconOnClick]:
// TrailingIconOnClick, if specified, is the function to call when
// the TrailingIcon is clicked. If this is nil, the trailing icon
// will not be interactive. See [TextField.SetTrailingIcon].
func (t *TextField) SetTrailingIconOnClick(v func(e events.Event)) *TextField {
t.TrailingIconOnClick = v
return t
}
// SetNoEcho sets the [TextField.NoEcho]:
// NoEcho is whether replace displayed characters with bullets
// to conceal text (for example, for a password input). Also
// see [TextField.SetTypePassword].
func (t *TextField) SetNoEcho(v bool) *TextField { t.NoEcho = v; return t }
// SetCursorWidth sets the [TextField.CursorWidth]:
// CursorWidth is the width of the text field cursor.
// It should be set in a Styler like all other style properties.
// By default, it is 1dp.
func (t *TextField) SetCursorWidth(v units.Value) *TextField { t.CursorWidth = v; return t }
// SetCursorColor sets the [TextField.CursorColor]:
// CursorColor is the color used for the text field cursor (caret).
// It should be set in a Styler like all other style properties.
// By default, it is [colors.Scheme.Primary.Base].
func (t *TextField) SetCursorColor(v image.Image) *TextField { t.CursorColor = v; return t }
// SetPlaceholderColor sets the [TextField.PlaceholderColor]:
// PlaceholderColor is the color used for the [TextField.Placeholder] text.
// It should be set in a Styler like all other style properties.
// By default, it is [colors.Scheme.OnSurfaceVariant].
func (t *TextField) SetPlaceholderColor(v image.Image) *TextField { t.PlaceholderColor = v; return t }
// SetSelectColor sets the [TextField.SelectColor]:
// SelectColor is the color used for the text selection background color.
// It should be set in a Styler like all other style properties.
// By default, it is [colors.Scheme.Select.Container].
func (t *TextField) SetSelectColor(v image.Image) *TextField { t.SelectColor = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.TimePicker", IDName: "time-picker", Doc: "TimePicker is a widget for picking a time.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Time", Doc: "Time is the time that we are viewing."}, {Name: "hour", Doc: "the raw input hour"}, {Name: "pm", Doc: "whether we are in pm mode (so we have to add 12h to everything)"}}})
// NewTimePicker returns a new [TimePicker] with the given optional parent:
// TimePicker is a widget for picking a time.
func NewTimePicker(parent ...tree.Node) *TimePicker { return tree.New[TimePicker](parent...) }
// SetTime sets the [TimePicker.Time]:
// Time is the time that we are viewing.
func (t *TimePicker) SetTime(v time.Time) *TimePicker { t.Time = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.DatePicker", IDName: "date-picker", Doc: "DatePicker is a widget for picking a date.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Time", Doc: "Time is the time that we are viewing."}, {Name: "getTime", Doc: "getTime converts the given calendar grid index to its corresponding time.\nWe must store this logic in a closure so that it can always be recomputed\ncorrectly in the inner closures of the grid maker; otherwise, the local\nvariables needed would be stale."}, {Name: "som", Doc: "som is the start of the month (must be set here to avoid stale variables)."}}})
// NewDatePicker returns a new [DatePicker] with the given optional parent:
// DatePicker is a widget for picking a date.
func NewDatePicker(parent ...tree.Node) *DatePicker { return tree.New[DatePicker](parent...) }
// SetTime sets the [DatePicker.Time]:
// Time is the time that we are viewing.
func (t *DatePicker) SetTime(v time.Time) *DatePicker { t.Time = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.TimeInput", IDName: "time-input", Doc: "TimeInput presents two text fields for editing a date and time,\nboth of which can pull up corresponding picker dialogs.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Time"}, {Name: "DisplayDate", Doc: "DisplayDate is whether the date input is displayed (default true)."}, {Name: "DisplayTime", Doc: "DisplayTime is whether the time input is displayed (default true)."}}})
// NewTimeInput returns a new [TimeInput] with the given optional parent:
// TimeInput presents two text fields for editing a date and time,
// both of which can pull up corresponding picker dialogs.
func NewTimeInput(parent ...tree.Node) *TimeInput { return tree.New[TimeInput](parent...) }
// SetTime sets the [TimeInput.Time]
func (t *TimeInput) SetTime(v time.Time) *TimeInput { t.Time = v; return t }
// SetDisplayDate sets the [TimeInput.DisplayDate]:
// DisplayDate is whether the date input is displayed (default true).
func (t *TimeInput) SetDisplayDate(v bool) *TimeInput { t.DisplayDate = v; return t }
// SetDisplayTime sets the [TimeInput.DisplayTime]:
// DisplayTime is whether the time input is displayed (default true).
func (t *TimeInput) SetDisplayTime(v bool) *TimeInput { t.DisplayTime = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.DurationInput", IDName: "duration-input", Doc: "DurationInput represents a [time.Duration] value with a spinner and unit chooser.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Duration"}, {Name: "Unit", Doc: "Unit is the unit of time."}}})
// NewDurationInput returns a new [DurationInput] with the given optional parent:
// DurationInput represents a [time.Duration] value with a spinner and unit chooser.
func NewDurationInput(parent ...tree.Node) *DurationInput { return tree.New[DurationInput](parent...) }
// SetDuration sets the [DurationInput.Duration]
func (t *DurationInput) SetDuration(v time.Duration) *DurationInput { t.Duration = v; return t }
// SetUnit sets the [DurationInput.Unit]:
// Unit is the unit of time.
func (t *DurationInput) SetUnit(v string) *DurationInput { t.Unit = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.Toolbar", IDName: "toolbar", Doc: "Toolbar is a [Frame] that is useful for holding [Button]s that do things.\nIt automatically moves items that do not fit into an overflow menu, and\nmanages additional items that are always placed onto this overflow menu.\nToolbars are frequently added in [Body.AddTopBar].", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "OverflowMenus", Doc: "OverflowMenus are functions for configuring the overflow menu of the\ntoolbar. You can use [Toolbar.AddOverflowMenu] to add them.\nThese are processed in reverse order (last in, first called)\nso that the default items are added last."}, {Name: "allItemsPlan", Doc: "allItemsPlan has all the items, during layout sizing"}, {Name: "overflowItems", Doc: "overflowItems are items moved from the main toolbar that will be\nshown in the overflow menu."}, {Name: "overflowButton", Doc: "overflowButton is the widget to pull up the overflow menu."}}})
// NewToolbar returns a new [Toolbar] with the given optional parent:
// Toolbar is a [Frame] that is useful for holding [Button]s that do things.
// It automatically moves items that do not fit into an overflow menu, and
// manages additional items that are always placed onto this overflow menu.
// Toolbars are frequently added in [Body.AddTopBar].
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 opened.\nThe base version does nothing."}, {Name: "OnClose", Doc: "OnClose is called when a node is 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.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.", 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."}}})
// NewTree returns a new [Tree] with the given optional parent:
// Tree provides a graphical representation of a tree structure,
// providing full navigation and manipulation abilities.
//
// It does not handle layout by itself, so if you want it to scroll
// separately from the rest of the surrounding context, you must
// place it in a [Frame].
//
// If the [Tree.SyncNode] field is non-nil, typically via the
// [Tree.SyncTree] method, then the Tree mirrors another
// tree structure, and tree editing functions apply to
// the source tree first, and then to the Tree by sync.
//
// Otherwise, data can be directly encoded in a Tree
// derived type, to represent any kind of tree structure
// and associated data.
//
// Standard [events.Event]s are sent to any listeners, including
// [events.Select], [events.Change], and [events.DoubleClick].
// The selected nodes are in the root [Tree.SelectedNodes] list.
func NewTree(parent ...tree.Node) *Tree { return tree.New[Tree](parent...) }
// SetText sets the [Tree.Text]:
// Text is the text to display for the tree item label, which automatically
// defaults to the [tree.Node.Name] of the tree node. It has no effect
// if [Tree.SyncNode] is non-nil.
func (t *Tree) SetText(v string) *Tree { t.Text = v; return t }
// SetIcon sets the [Tree.Icon]:
// Icon is an optional icon displayed to the the left of the text label.
func (t *Tree) SetIcon(v icons.Icon) *Tree { t.Icon = v; return t }
// SetIconOpen sets the [Tree.IconOpen]:
// IconOpen is the icon to use for an open (expanded) branch;
// it defaults to [icons.KeyboardArrowDown].
func (t *Tree) SetIconOpen(v icons.Icon) *Tree { t.IconOpen = v; return t }
// SetIconClosed sets the [Tree.IconClosed]:
// IconClosed is the icon to use for a closed (collapsed) branch;
// it defaults to [icons.KeyboardArrowRight].
func (t *Tree) SetIconClosed(v icons.Icon) *Tree { t.IconClosed = v; return t }
// SetIconLeaf sets the [Tree.IconLeaf]:
// IconLeaf is the icon to use for a terminal node branch that has no children;
// it defaults to [icons.Blank].
func (t *Tree) SetIconLeaf(v icons.Icon) *Tree { t.IconLeaf = v; return t }
// SetIndent sets the [Tree.Indent]:
// Indent is the amount to indent children relative to this node.
// It should be set in a Styler like all other style properties.
func (t *Tree) SetIndent(v units.Value) *Tree { t.Indent = v; return t }
// SetOpenDepth sets the [Tree.OpenDepth]:
// OpenDepth is the depth for nodes be initialized as open (default 4).
// Nodes beyond this depth will be initialized as closed.
func (t *Tree) SetOpenDepth(v int) *Tree { t.OpenDepth = v; return t }
// SetClosed sets the [Tree.Closed]:
// Closed is whether this tree node is currently toggled closed
// (children not visible).
func (t *Tree) SetClosed(v bool) *Tree { t.Closed = v; return t }
// SetSelectMode sets the [Tree.SelectMode]:
// SelectMode, when set on the root node, determines whether keyboard movements should update selection.
func (t *Tree) SetSelectMode(v bool) *Tree { t.SelectMode = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.ListButton", IDName: "list-button", Doc: "ListButton represents a slice or array value with a button that opens a [List].", Embeds: []types.Field{{Name: "Button"}}, Fields: []types.Field{{Name: "Slice"}}})
// NewListButton returns a new [ListButton] with the given optional parent:
// ListButton represents a slice or array value with a button that opens a [List].
func NewListButton(parent ...tree.Node) *ListButton { return tree.New[ListButton](parent...) }
// SetSlice sets the [ListButton.Slice]
func (t *ListButton) SetSlice(v any) *ListButton { t.Slice = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.FormButton", IDName: "form-button", Doc: "FormButton represents a struct value with a button that opens a [Form].", Embeds: []types.Field{{Name: "Button"}}, Fields: []types.Field{{Name: "Struct"}}})
// NewFormButton returns a new [FormButton] with the given optional parent:
// FormButton represents a struct value with a button that opens a [Form].
func NewFormButton(parent ...tree.Node) *FormButton { return tree.New[FormButton](parent...) }
// SetStruct sets the [FormButton.Struct]
func (t *FormButton) SetStruct(v any) *FormButton { t.Struct = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.KeyedListButton", IDName: "keyed-list-button", Doc: "KeyedListButton represents a map value with a button that opens a [KeyedList].", Embeds: []types.Field{{Name: "Button"}}, Fields: []types.Field{{Name: "Map"}}})
// NewKeyedListButton returns a new [KeyedListButton] with the given optional parent:
// KeyedListButton represents a map value with a button that opens a [KeyedList].
func NewKeyedListButton(parent ...tree.Node) *KeyedListButton {
return tree.New[KeyedListButton](parent...)
}
// SetMap sets the [KeyedListButton.Map]
func (t *KeyedListButton) SetMap(v any) *KeyedListButton { t.Map = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.TreeButton", IDName: "tree-button", Doc: "TreeButton represents a [tree.Node] value with a button.", Embeds: []types.Field{{Name: "Button"}}, Fields: []types.Field{{Name: "Tree"}}})
// NewTreeButton returns a new [TreeButton] with the given optional parent:
// TreeButton represents a [tree.Node] value with a button.
func NewTreeButton(parent ...tree.Node) *TreeButton { return tree.New[TreeButton](parent...) }
// SetTree sets the [TreeButton.Tree]
func (t *TreeButton) SetTree(v tree.Node) *TreeButton { t.Tree = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.TypeChooser", IDName: "type-chooser", Doc: "TypeChooser represents a [types.Type] value with a chooser.", Embeds: []types.Field{{Name: "Chooser"}}})
// NewTypeChooser returns a new [TypeChooser] with the given optional parent:
// TypeChooser represents a [types.Type] value with a chooser.
func NewTypeChooser(parent ...tree.Node) *TypeChooser { return tree.New[TypeChooser](parent...) }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.IconButton", IDName: "icon-button", Doc: "IconButton represents an [icons.Icon] with a [Button] that opens\na dialog for selecting the icon.", Embeds: []types.Field{{Name: "Button"}}})
// NewIconButton returns a new [IconButton] with the given optional parent:
// IconButton represents an [icons.Icon] with a [Button] that opens
// a dialog for selecting the icon.
func NewIconButton(parent ...tree.Node) *IconButton { return tree.New[IconButton](parent...) }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.FontButton", IDName: "font-button", Doc: "FontButton represents a [FontName] with a [Button] that opens\na dialog for selecting the font family.", Embeds: []types.Field{{Name: "Button"}}})
// NewFontButton returns a new [FontButton] with the given optional parent:
// FontButton represents a [FontName] with a [Button] that opens
// a dialog for selecting the font family.
func NewFontButton(parent ...tree.Node) *FontButton { return tree.New[FontButton](parent...) }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/core.WidgetBase", IDName: "widget-base", Doc: "WidgetBase implements the [Widget] interface and provides the core functionality\nof a widget. You must use WidgetBase as an embedded struct in all higher-level\nwidget types. It renders the standard box model, but does not layout or render\nany children; see [Frame] for that.", Methods: []types.Method{{Name: "Update", Doc: "Update updates the widget and all of its children by running [WidgetBase.UpdateWidget]\nand [WidgetBase.Style] on each one, and triggering a new layout pass with\n[WidgetBase.NeedsLayout]. It is the main way that end users should trigger widget\nupdates, and it is guaranteed to fully update a widget to the current state.\nFor example, it should be called after making any changes to the core properties\nof a widget, such as the text of [Text], the icon of a [Button], or the slice\nof a [Table].\n\nUpdate differs from [WidgetBase.UpdateWidget] in that it updates the widget and all\nof its children down the tree, whereas [WidgetBase.UpdateWidget] only updates the widget\nitself. Also, Update also calls [WidgetBase.Style] and [WidgetBase.NeedsLayout],\nwhereas [WidgetBase.UpdateWidget] does not. End-user code should typically call Update,\nnot [WidgetBase.UpdateWidget].\n\nIf you are calling this in a separate goroutine outside of the main\nconfiguration, rendering, and event handling structure, you need to\ncall [WidgetBase.AsyncLock] and [WidgetBase.AsyncUnlock] before and\nafter this, respectively.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Tooltip", Doc: "Tooltip is the text for the tooltip for this widget,\nwhich can use HTML formatting."}, {Name: "Parts", Doc: "Parts are a separate tree of sub-widgets that can be used to store\northogonal parts of a widget when necessary to separate them from children.\nFor example, [Tree]s use parts to separate their internal parts from\nthe other child tree nodes. Composite widgets like buttons should\nNOT use parts to store their components; parts should only be used when\nabsolutely necessary. Use [WidgetBase.newParts] to make the parts."}, {Name: "Geom", Doc: "Geom has the full layout geometry for size and position of this widget."}, {Name: "OverrideStyle", Doc: "OverrideStyle, if true, indicates override the computed styles of the widget\nand allow directly editing [WidgetBase.Styles]. It is typically only set in\nthe inspector."}, {Name: "Styles", Doc: "Styles are styling settings for this widget. They are set by\n[WidgetBase.Stylers] in [WidgetBase.Style]."}, {Name: "Stylers", Doc: "Stylers is a tiered set of functions that are called in sequential\nascending order (so the last added styler is called last and\nthus can override all other stylers) to style the element.\nThese should be set using the [WidgetBase.Styler], [WidgetBase.FirstStyler],\nand [WidgetBase.FinalStyler] functions."}, {Name: "Listeners", Doc: "Listeners is a tiered set of event listener functions for processing events on this widget.\nThey are called in sequential descending order (so the last added listener\nis called first). They should be added using the [WidgetBase.On], [WidgetBase.OnFirst],\nand [WidgetBase.OnFinal] functions, or any of the various On{EventType} helper functions."}, {Name: "ContextMenus", Doc: "ContextMenus is a slice of menu functions to call to construct\nthe widget's context menu on an [events.ContextMenu]. The\nfunctions are called in reverse order such that the elements\nadded in the last function are the first in the menu.\nContext menus should be added through [WidgetBase.AddContextMenu].\nSeparators will be added between each context menu function.\n[Scene.ContextMenus] apply to all widgets in the scene."}, {Name: "Deferred", Doc: "Deferred is a slice of functions to call after the next [Scene] update/render.\nIn each function event sending etc will work as expected. Use\n[WidgetBase.Defer] to add a function."}, {Name: "Scene", Doc: "Scene is the overall Scene to which we belong. It is automatically\nby widgets whenever they are added to another widget parent."}, {Name: "ValueUpdate", Doc: "ValueUpdate is a function set by [Bind] that is called in\n[WidgetBase.UpdateWidget] to update the widget's value from the bound value.\nIt should not be accessed by end users."}, {Name: "ValueOnChange", Doc: "ValueOnChange is a function set by [Bind] that is called when\nthe widget receives an [events.Change] event to update the bound value\nfrom the widget's value. It should not be accessed by end users."}, {Name: "ValueTitle", Doc: "ValueTitle is the title to display for a dialog for this [Value]."}, {Name: "flags", Doc: "/ flags are atomic bit flags for [WidgetBase] state."}}})
// NewWidgetBase returns a new [WidgetBase] with the given optional parent:
// WidgetBase implements the [Widget] interface and provides the core functionality
// of a widget. You must use WidgetBase as an embedded struct in all higher-level
// widget types. It renders the standard box model, but does not layout or render
// any children; see [Frame] for that.
func NewWidgetBase(parent ...tree.Node) *WidgetBase { return tree.New[WidgetBase](parent...) }
// SetTooltip sets the [WidgetBase.Tooltip]:
// Tooltip is the text for the tooltip for this widget,
// which can use HTML formatting.
func (t *WidgetBase) SetTooltip(v string) *WidgetBase { t.Tooltip = v; return t }
// SetValueTitle sets the [WidgetBase.ValueTitle]:
// ValueTitle is the title to display for a dialog for this [Value].
func (t *WidgetBase) SetValueTitle(v string) *WidgetBase { t.ValueTitle = v; return t }
var _ = types.AddFunc(&types.Func{Name: "cogentcore.org/core/core.ProfileToggle", Doc: "ProfileToggle turns profiling on or off, which does both\ntargeted profiling and global CPU and memory profiling.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}})
var _ = types.AddFunc(&types.Func{Name: "cogentcore.org/core/core.resetAllSettings", Doc: "resetAllSettings resets all of the settings to their default values.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}})
var _ = types.AddFunc(&types.Func{Name: "cogentcore.org/core/core.UpdateAll", Doc: "UpdateAll updates all windows and triggers a full render rebuild.\nIt is typically called when user settings are changed.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}})
var _ = types.AddFunc(&types.Func{Name: "cogentcore.org/core/core.SettingsWindow", Doc: "SettingsWindow opens a window for editing user settings.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}})
// Copyright (c) 2024, 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 (
"fmt"
"cogentcore.org/core/tree"
)
// UpdateWidget updates the widget by running [WidgetBase.Updaters] in
// sequential descending (reverse) order after calling [WidgetBase.ValueUpdate].
// This includes applying the result of [WidgetBase.Make].
//
// UpdateWidget differs from [WidgetBase.Update] in that it only updates the widget
// itself and not any of its children. Also, it does not restyle the widget or trigger
// a new layout pass, while [WidgetBase.Update] does. End-user code should typically
// call [WidgetBase.Update], not UpdateWidget.
func (wb *WidgetBase) UpdateWidget() *WidgetBase {
if wb.ValueUpdate != nil {
wb.ValueUpdate()
}
wb.RunUpdaters()
return wb
}
// UpdateTree calls [WidgetBase.UpdateWidget] on every widget in the tree
// starting with this one and going down.
func (wb *WidgetBase) UpdateTree() {
wb.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
cwb.UpdateWidget()
return tree.Continue
})
}
// Update updates the widget and all of its children by running [WidgetBase.UpdateWidget]
// and [WidgetBase.Style] on each one, and triggering a new layout pass with
// [WidgetBase.NeedsLayout]. It is the main way that end users should trigger widget
// updates, and it is guaranteed to fully update a widget to the current state.
// For example, it should be called after making any changes to the core properties
// of a widget, such as the text of [Text], the icon of a [Button], or the slice
// of a [Table].
//
// Update differs from [WidgetBase.UpdateWidget] in that it updates the widget and all
// of its children down the tree, whereas [WidgetBase.UpdateWidget] only updates the widget
// itself. Also, Update also calls [WidgetBase.Style] and [WidgetBase.NeedsLayout],
// whereas [WidgetBase.UpdateWidget] does not. End-user code should typically call Update,
// not [WidgetBase.UpdateWidget].
//
// If you are calling this in a separate goroutine outside of the main
// configuration, rendering, and event handling structure, you need to
// call [WidgetBase.AsyncLock] and [WidgetBase.AsyncUnlock] before and
// after this, respectively.
func (wb *WidgetBase) Update() { //types:add
if DebugSettings.UpdateTrace {
fmt.Println("\tDebugSettings.UpdateTrace Update:", wb)
}
wb.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
cwb.UpdateWidget()
cw.Style()
return tree.Continue
})
wb.NeedsLayout()
}
// UpdateRender is the same as [WidgetBase.Update], except that it calls
// [WidgetBase.NeedsRender] instead of [WidgetBase.NeedsLayout].
// This should be called when the changes made to the widget do not
// require a new layout pass (if you change the size, spacing, alignment,
// or other layout properties of the widget, you need a new layout pass
// and should call [WidgetBase.Update] instead).
func (wb *WidgetBase) UpdateRender() {
wb.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
cwb.UpdateWidget()
cw.Style()
return tree.Continue
})
wb.NeedsRender()
}
// Copyright (c) 2024, 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 (
"reflect"
"strings"
"cogentcore.org/core/base/labels"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/events"
"cogentcore.org/core/events/key"
)
// Value is a widget that has an associated value representation.
// It can be bound to a value using [Bind].
type Value interface {
Widget
// WidgetValue returns the pointer to the associated value of the widget.
WidgetValue() any
}
// ValueSetter is an optional interface that [Value]s can implement
// to customize how the associated widget value is set from the given value.
type ValueSetter interface {
// SetWidgetValue sets the associated widget value from the given value.
SetWidgetValue(value any) error
}
// OnBinder is an optional interface that [Value]s can implement to
// do something when the widget is bound to the given value.
type OnBinder interface {
// OnBind is called when the widget is bound to the given value
// with the given optional struct tags.
OnBind(value any, tags reflect.StructTag)
}
// Bind binds the given value to the given [Value] such that the values of
// the two will be linked and updated appropriately after [events.Change] events
// and during [WidgetBase.UpdateWidget]. It returns the widget to enable method chaining.
// It also accepts an optional [reflect.StructTag], which is used to set properties
// of certain value widgets.
func Bind[T Value](value any, vw T, tags ...string) T { //yaegi:add
// TODO: make tags be reflect.StructTag once yaegi is fixed to work with that
wb := vw.AsWidget()
alreadyBound := wb.ValueUpdate != nil
wb.ValueUpdate = func() {
if vws, ok := any(vw).(ValueSetter); ok {
ErrorSnackbar(vw, vws.SetWidgetValue(value))
} else {
ErrorSnackbar(vw, reflectx.SetRobust(vw.WidgetValue(), value))
}
}
wb.ValueOnChange = func() {
ErrorSnackbar(vw, reflectx.SetRobust(value, vw.WidgetValue()))
}
if alreadyBound {
ResetWidgetValue(vw)
}
wb.ValueTitle = labels.FriendlyTypeName(reflectx.NonPointerType(reflect.TypeOf(value)))
if ob, ok := any(vw).(OnBinder); ok {
tag := reflect.StructTag("")
if len(tags) > 0 {
tag = reflect.StructTag(tags[0])
}
ob.OnBind(value, tag)
}
wb.ValueUpdate() // we update it with the initial value immediately
return vw
}
// ResetWidgetValue resets the [Value] if it was already bound to another value previously.
// We first need to reset the widget value to zero to avoid any issues with the pointer
// from the old value persisting and being updated. For example, that issue happened
// with slice and map pointers persisting in forms when a new struct was set.
// It should not be called by end-user code; it must be exported since it is referenced
// in a generic function added to yaegi ([Bind]).
func ResetWidgetValue(vw Value) {
rv := reflect.ValueOf(vw.WidgetValue())
if rv.IsValid() && rv.Type().Kind() == reflect.Pointer {
rv.Elem().SetZero()
}
}
// joinValueTitle returns a [WidgetBase.ValueTitle] string composed
// of two elements, with a • separator, handling the cases where
// either or both can be empty.
func joinValueTitle(a, b string) string {
switch {
case a == "":
return b
case b == "":
return a
default:
return a + " • " + b
}
}
const shiftNewWindow = "[Shift: new window]"
// InitValueButton configures the given [Value] to open a dialog representing
// its value in accordance with the given dialog construction function when clicked.
// It also sets the tooltip of the widget appropriately. If allowReadOnly is false,
// the dialog will not be opened if the widget is read only. It also takes an optional
// function to call after the dialog is accepted.
func InitValueButton(v Value, allowReadOnly bool, make func(d *Body), after ...func()) {
wb := v.AsWidget()
// windows are never new on mobile
if !TheApp.Platform().IsMobile() {
wb.SetTooltip(shiftNewWindow)
}
wb.OnClick(func(e events.Event) {
if allowReadOnly || !wb.IsReadOnly() {
if e.HasAnyModifier(key.Shift) {
wb.setFlag(!wb.hasFlag(widgetValueNewWindow), widgetValueNewWindow)
}
openValueDialog(v, make, after...)
}
})
}
// openValueDialog opens a new value dialog for the given [Value] using the
// given function for constructing the dialog and the optional given function
// to call after the dialog is accepted.
func openValueDialog(v Value, make func(d *Body), after ...func()) {
opv := reflectx.UnderlyingPointer(reflect.ValueOf(v.WidgetValue()))
if !opv.IsValid() {
return
}
obj := opv.Interface()
if RecycleDialog(obj) {
return
}
wb := v.AsWidget()
d := NewBody(wb.ValueTitle)
if text := strings.ReplaceAll(wb.Tooltip, shiftNewWindow, ""); text != "" {
NewText(d).SetType(TextSupporting).SetText(text)
}
make(d)
// if we don't have anything specific for ok events,
// we just register an OnClose event and skip the
// OK and Cancel buttons
if len(after) == 0 {
d.OnClose(func(e events.Event) {
wb.UpdateChange()
})
} else {
// otherwise, we have to make the bottom bar
d.AddBottomBar(func(bar *Frame) {
d.AddCancel(bar)
d.AddOK(bar).OnClick(func(e events.Event) {
after[0]()
wb.UpdateChange()
})
})
}
if wb.hasFlag(widgetValueNewWindow) {
d.RunWindowDialog(v)
} else {
d.RunFullDialog(v)
}
}
// Copyright (c) 2024, 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 (
"fmt"
"image/color"
"reflect"
"time"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/enums"
"cogentcore.org/core/events/key"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
// Valuer is an interface that types can implement to specify the
// [Value] that should be used to represent them in the GUI.
type Valuer interface {
// Value returns the [Value] that should be used to represent
// the value in the GUI. If it returns nil, then [ToValue] will
// fall back onto the next step. This function must NOT call [Bind].
Value() Value
}
// ValueTypes is a map of functions that return a [Value]
// for a value of a certain fully package path qualified type name.
// It is used by [toValue]. If a function returns nil, it falls
// back onto the next step. You can add to this using the [AddValueType]
// helper function. These functions must NOT call [Bind].
var ValueTypes = map[string]func(value any) Value{}
// AddValueType binds the given value type to the given [Value] [tree.NodeValue]
// type, meaning that [toValue] will return a new [Value] of the given type
// when it receives values of the given value type. It uses [ValueTypes].
// This function is called with various standard types automatically.
func AddValueType[T any, W tree.NodeValue]() {
var v T
name := types.TypeNameValue(v)
ValueTypes[name] = func(value any) Value {
return any(tree.New[W]()).(Value)
}
}
// NewValue converts the given value into an appropriate [Value]
// whose associated value is bound to the given value. The given value must
// be a pointer. It uses the given optional struct tags for additional context
// and to determine styling properties via [styleFromTags]. It also adds the
// resulting [Value] to the given optional parent if it specified. The specifics
// on how it determines what type of [Value] to make are further
// documented on [toValue].
func NewValue(value any, tags reflect.StructTag, parent ...tree.Node) Value {
vw := toValue(value, tags)
if tags != "" {
styleFromTags(vw, tags)
}
Bind(value, vw, string(tags))
if len(parent) > 0 {
parent[0].AsTree().AddChild(vw)
}
return vw
}
// toValue converts the given value into an appropriate [Value],
// using the given optional struct tags for additional context.
// The given value should typically be a pointer. It does NOT call [Bind];
// see [NewValue] for a version that does. It first checks the
// [Valuer] interface, then the [ValueTypes], and finally it falls
// back on a set of default bindings. If any step results in nil,
// it falls back on the next step.
func toValue(value any, tags reflect.StructTag) Value {
if vwr, ok := value.(Valuer); ok {
if vw := vwr.Value(); vw != nil {
return vw
}
}
rv := reflect.ValueOf(value)
if !rv.IsValid() {
return NewText()
}
uv := reflectx.Underlying(rv)
if reflectx.IsNil(uv) {
return NewText()
}
typ := uv.Type()
if vwt, ok := ValueTypes[types.TypeName(typ)]; ok {
if vw := vwt(value); vw != nil {
return vw
}
}
// Default bindings:
if _, ok := value.(enums.BitFlag); ok {
return NewSwitches()
}
if enum, ok := value.(enums.Enum); ok {
if len(enum.Values()) < 4 {
return NewSwitches()
}
return NewChooser()
}
if _, ok := value.(color.Color); ok {
return NewColorButton()
}
if _, ok := value.(tree.Node); ok {
return NewTreeButton()
}
inline := tags.Get("display") == "inline"
noInline := tags.Get("display") == "no-inline"
kind := typ.Kind()
switch {
case kind >= reflect.Int && kind <= reflect.Float64:
if _, ok := value.(fmt.Stringer); ok {
return NewTextField()
}
return NewSpinner()
case kind == reflect.Bool:
return NewSwitch()
case kind == reflect.Struct:
num := reflectx.NumAllFields(uv)
if !noInline && (inline || num <= SystemSettings.StructInlineLength) {
return NewForm().SetInline(true)
} else {
return NewFormButton()
}
case kind == reflect.Map:
len := uv.Len()
if !noInline && (inline || len <= SystemSettings.MapInlineLength) {
return NewKeyedList().SetInline(true)
} else {
return NewKeyedListButton()
}
case kind == reflect.Array, kind == reflect.Slice:
sz := uv.Len()
elemType := reflectx.SliceElementType(value)
if _, ok := value.([]byte); ok {
return NewTextField()
}
if _, ok := value.([]rune); ok {
return NewTextField()
}
isStruct := (reflectx.NonPointerType(elemType).Kind() == reflect.Struct)
if !noInline && (inline || (!isStruct && sz <= SystemSettings.SliceInlineLength && !tree.IsNode(elemType))) {
return NewInlineList()
} else {
return NewListButton()
}
case kind == reflect.Func:
return NewFuncButton()
}
return NewTextField() // final fallback
}
func init() {
AddValueType[icons.Icon, IconButton]()
AddValueType[time.Time, TimeInput]()
AddValueType[time.Duration, DurationInput]()
AddValueType[types.Type, TypeChooser]()
AddValueType[Filename, FileButton]()
AddValueType[FontName, FontButton]()
AddValueType[keymap.MapName, KeyMapButton]()
AddValueType[key.Chord, KeyChordButton]()
}
// 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 core
import (
"image"
"reflect"
"cogentcore.org/core/base/labels"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
"golang.org/x/exp/maps"
)
// ListButton represents a slice or array value with a button that opens a [List].
type ListButton struct {
Button
Slice any
}
func (lb *ListButton) WidgetValue() any { return &lb.Slice }
func (lb *ListButton) Init() {
lb.Button.Init()
lb.SetType(ButtonTonal).SetIcon(icons.Edit)
lb.Updater(func() {
lb.SetText(labels.FriendlySliceLabel(reflect.ValueOf(lb.Slice)))
})
InitValueButton(lb, true, func(d *Body) {
up := reflectx.Underlying(reflect.ValueOf(lb.Slice))
if up.Type().Kind() != reflect.Array && reflectx.NonPointerType(reflectx.SliceElementType(lb.Slice)).Kind() == reflect.Struct {
tb := NewTable(d).SetSlice(lb.Slice)
tb.SetValueTitle(lb.ValueTitle).SetReadOnly(lb.IsReadOnly())
d.AddTopBar(func(bar *Frame) {
NewToolbar(bar).Maker(tb.MakeToolbar)
})
} else {
sv := NewList(d).SetSlice(lb.Slice)
sv.SetValueTitle(lb.ValueTitle).SetReadOnly(lb.IsReadOnly())
d.AddTopBar(func(bar *Frame) {
NewToolbar(bar).Maker(sv.MakeToolbar)
})
}
})
}
// FormButton represents a struct value with a button that opens a [Form].
type FormButton struct {
Button
Struct any
}
func (fb *FormButton) WidgetValue() any { return &fb.Struct }
func (fb *FormButton) Init() {
fb.Button.Init()
fb.SetType(ButtonTonal).SetIcon(icons.Edit)
fb.Updater(func() {
fb.SetText(labels.FriendlyStructLabel(reflect.ValueOf(fb.Struct)))
})
InitValueButton(fb, true, func(d *Body) {
fm := NewForm(d).SetStruct(fb.Struct)
fm.SetValueTitle(fb.ValueTitle).SetReadOnly(fb.IsReadOnly())
if tb, ok := fb.Struct.(ToolbarMaker); ok {
d.AddTopBar(func(bar *Frame) {
NewToolbar(bar).Maker(tb.MakeToolbar)
})
}
})
}
// KeyedListButton represents a map value with a button that opens a [KeyedList].
type KeyedListButton struct {
Button
Map any
}
func (kb *KeyedListButton) WidgetValue() any { return &kb.Map }
func (kb *KeyedListButton) Init() {
kb.Button.Init()
kb.SetType(ButtonTonal).SetIcon(icons.Edit)
kb.Updater(func() {
kb.SetText(labels.FriendlyMapLabel(reflect.ValueOf(kb.Map)))
})
InitValueButton(kb, true, func(d *Body) {
kl := NewKeyedList(d).SetMap(kb.Map)
kl.SetValueTitle(kb.ValueTitle).SetReadOnly(kb.IsReadOnly())
d.AddTopBar(func(bar *Frame) {
NewToolbar(bar).Maker(kl.MakeToolbar)
})
})
}
// TreeButton represents a [tree.Node] value with a button.
type TreeButton struct {
Button
Tree tree.Node
}
func (tb *TreeButton) WidgetValue() any { return &tb.Tree }
func (tb *TreeButton) Init() {
tb.Button.Init()
tb.SetType(ButtonTonal).SetIcon(icons.Edit)
tb.Updater(func() {
path := "None"
if tb.Tree != nil {
path = tb.Tree.AsTree().String()
}
tb.SetText(path)
})
InitValueButton(tb, true, func(d *Body) {
makeInspector(d, tb.Tree)
})
}
func (tb *TreeButton) WidgetTooltip(pos image.Point) (string, image.Point) {
if tb.Tree == nil {
return tb.Tooltip, tb.DefaultTooltipPos()
}
tpa := "(" + tb.Tree.AsTree().Path() + ")"
if tb.Tooltip == "" {
return tpa, tb.DefaultTooltipPos()
}
return tpa + " " + tb.Tooltip, tb.DefaultTooltipPos()
}
// TypeChooser represents a [types.Type] value with a chooser.
type TypeChooser struct {
Chooser
}
func (tc *TypeChooser) Init() {
tc.Chooser.Init()
tc.SetTypes(maps.Values(types.Types)...)
}
// IconButton represents an [icons.Icon] with a [Button] that opens
// a dialog for selecting the icon.
type IconButton struct {
Button
}
func (ib *IconButton) WidgetValue() any { return &ib.Icon }
func (ib *IconButton) Init() {
ib.Button.Init()
ib.Updater(func() {
if !ib.Icon.IsSet() {
ib.SetText("Select an icon")
} else {
ib.SetText("")
}
if ib.IsReadOnly() {
ib.SetType(ButtonText)
if !ib.Icon.IsSet() {
ib.SetText("").SetIcon(icons.Blank)
}
} else {
ib.SetType(ButtonTonal)
}
})
InitValueButton(ib, false, func(d *Body) {
d.SetTitle("Select an icon")
si := 0
used := maps.Keys(icons.Used)
ls := NewList(d)
ls.SetSlice(&used).SetSelectedValue(ib.Icon).BindSelect(&si)
ls.OnChange(func(e events.Event) {
ib.Icon = used[si]
})
})
}
// FontName is used to specify a font family name.
// It results in a [FontButton] [Value].
type FontName string
// FontButton represents a [FontName] with a [Button] that opens
// a dialog for selecting the font family.
type FontButton struct {
Button
}
func (fb *FontButton) WidgetValue() any { return &fb.Text }
func (fb *FontButton) Init() {
fb.Button.Init()
fb.SetType(ButtonTonal)
InitValueButton(fb, false, func(d *Body) {
d.SetTitle("Select a font family")
si := 0
fi := paint.FontLibrary.FontInfo
tb := NewTable(d)
tb.SetSlice(&fi).SetSelectedField("Name").SetSelectedValue(fb.Text).BindSelect(&si)
tb.SetTableStyler(func(w Widget, s *styles.Style, row, col int) {
if col != 4 {
return
}
s.Font.Family = fi[row].Name
s.Font.Stretch = fi[row].Stretch
s.Font.Weight = fi[row].Weight
s.Font.Style = fi[row].Style
s.Font.Size.Pt(18)
})
tb.OnChange(func(e events.Event) {
fb.Text = fi[si].Name
})
})
}
// HighlightingName is a highlighting style name.
// TODO: move this to texteditor/highlighting.
type HighlightingName string
// 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 provides the core GUI functionality of Cogent Core.
package core
//go:generate core generate
import (
"image"
"log/slog"
"cogentcore.org/core/base/tiered"
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
"cogentcore.org/core/enums"
"cogentcore.org/core/events"
"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/tree"
"golang.org/x/image/draw"
)
// Widget is the interface that all Cogent Core widgets satisfy.
// The core widget functionality is defined on [WidgetBase],
// and all higher-level widget types must embed it. This
// interface only contains the methods that higher-level
// widget types may need to override. You can call
// [Widget.AsWidget] to get the [WidgetBase] of a Widget
// and access the core widget functionality.
type Widget interface {
tree.Node
// AsWidget returns the [WidgetBase] of this Widget. Most
// core widget functionality is implemented on [WidgetBase].
AsWidget() *WidgetBase
// Style updates the style properties of the widget based on [WidgetBase.Stylers].
// To specify the style properties of a widget, use [WidgetBase.Styler].
// Widgets can implement this method if necessary to add additional styling behavior,
// such as calling [units.Value.ToDots] on a custom [units.Value] field.
Style()
// 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.
SizeUp()
// 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.
SizeDown(iter int) bool
// 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.
SizeFinal()
// 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.
Position()
// ApplyScenePos 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).
ApplyScenePos()
// Render is the method that widgets should implement to define their
// custom rendering steps. It should not typically be called outside of
// [Widget.RenderWidget], which also does other steps applicable
// for all widgets. The base [WidgetBase.Render] implementation
// renders the standard box model.
Render()
// RenderWidget renders the widget and any parts and children that it has.
// It does not render if the widget is invisible. It calls [Widget.Render]
// for widget-specific rendering.
RenderWidget()
// WidgetTooltip returns the tooltip text that should be used for this
// widget, and the window-relative position to use for the upper-left corner
// of the tooltip. The current mouse position in scene-local coordinates
// is passed to the function; if it is {-1, -1}, that indicates that
// WidgetTooltip is being called in a Style function to determine whether
// the widget should be [abilities.LongHoverable] and [abilities.LongPressable]
// (if the return string is not "", then it will have those abilities
// so that the tooltip can be displayed).
//
// By default, WidgetTooltip just returns [WidgetBase.Tooltip]
// and [WidgetBase.DefaultTooltipPos], but widgets can override
// it to do different things. For example, buttons add their
// shortcut to the tooltip here.
WidgetTooltip(pos image.Point) (string, image.Point)
// ContextMenuPos returns the default position for popup menus;
// by default in the middle its Bounding Box, but can be adapted as
// appropriate for different widgets.
ContextMenuPos(e events.Event) image.Point
// ShowContextMenu displays the context menu of various actions
// to perform on a Widget, activated by default on the ShowContextMenu
// event, triggered by a Right mouse click.
// Returns immediately, and actions are all executed directly
// (later) via the action signals. Calls ContextMenu and
// ContextMenuPos.
ShowContextMenu(e events.Event)
// ChildBackground returns the background color (Image) for given child Widget.
// By default, this is just our [styles.Style.ActualBackground] but it can be computed
// specifically for the child (e.g., for zebra stripes in [ListGrid]).
ChildBackground(child Widget) image.Image
// RenderDraw draws the current image onto the RenderWindow window,
// using the [system.Drawer] interface methods, typically [Drawer.Copy].
// The given draw operation is suggested by the RenderWindow, with the
// first main window using draw.Src and the rest using draw.Over.
// Individual draw methods are free to ignore if necessary.
// Optimized direct rendering widgets can register by doing
// [Scene.AddDirectRender] to directly draw into the window texture.
RenderDraw(drw system.Drawer, op draw.Op)
}
// WidgetBase implements the [Widget] interface and provides the core functionality
// of a widget. You must use WidgetBase as an embedded struct in all higher-level
// widget types. It renders the standard box model, but does not layout or render
// any children; see [Frame] for that.
type WidgetBase struct {
tree.NodeBase
// Tooltip is the text for the tooltip for this widget,
// which can use HTML formatting.
Tooltip string `json:",omitempty"`
// Parts are a separate tree of sub-widgets that can be used to store
// orthogonal parts of a widget when necessary to separate them from children.
// For example, [Tree]s use parts to separate their internal parts from
// the other child tree nodes. Composite widgets like buttons should
// NOT use parts to store their components; parts should only be used when
// absolutely necessary. Use [WidgetBase.newParts] to make the parts.
Parts *Frame `copier:"-" json:"-" xml:"-" set:"-"`
// Geom has the full layout geometry for size and position of this widget.
Geom geomState `edit:"-" copier:"-" json:"-" xml:"-" set:"-"`
// OverrideStyle, if true, indicates override the computed styles of the widget
// and allow directly editing [WidgetBase.Styles]. It is typically only set in
// the inspector.
OverrideStyle bool `copier:"-" json:"-" xml:"-" set:"-"`
// Styles are styling settings for this widget. They are set by
// [WidgetBase.Stylers] in [WidgetBase.Style].
Styles styles.Style `json:"-" xml:"-" set:"-"`
// Stylers is a tiered set of functions that are called in sequential
// ascending order (so the last added styler is called last and
// thus can override all other stylers) to style the element.
// These should be set using the [WidgetBase.Styler], [WidgetBase.FirstStyler],
// and [WidgetBase.FinalStyler] functions.
Stylers tiered.Tiered[[]func(s *styles.Style)] `copier:"-" json:"-" xml:"-" set:"-" edit:"-" display:"add-fields"`
// Listeners is a tiered set of event listener functions for processing events on this widget.
// They are called in sequential descending order (so the last added listener
// is called first). They should be added using the [WidgetBase.On], [WidgetBase.OnFirst],
// and [WidgetBase.OnFinal] functions, or any of the various On{EventType} helper functions.
Listeners tiered.Tiered[events.Listeners] `copier:"-" json:"-" xml:"-" set:"-" edit:"-" display:"add-fields"`
// ContextMenus is a slice of menu functions to call to construct
// the widget's context menu on an [events.ContextMenu]. The
// functions are called in reverse order such that the elements
// added in the last function are the first in the menu.
// Context menus should be added through [WidgetBase.AddContextMenu].
// Separators will be added between each context menu function.
// [Scene.ContextMenus] apply to all widgets in the scene.
ContextMenus []func(m *Scene) `copier:"-" json:"-" xml:"-" set:"-" edit:"-"`
// Deferred is a slice of functions to call after the next [Scene] update/render.
// In each function event sending etc will work as expected. Use
// [WidgetBase.Defer] to add a function.
Deferred []func() `copier:"-" json:"-" xml:"-" set:"-" edit:"-"`
// Scene is the overall Scene to which we belong. It is automatically
// by widgets whenever they are added to another widget parent.
Scene *Scene `copier:"-" json:"-" xml:"-" set:"-"`
// ValueUpdate is a function set by [Bind] that is called in
// [WidgetBase.UpdateWidget] to update the widget's value from the bound value.
// It should not be accessed by end users.
ValueUpdate func() `copier:"-" json:"-" xml:"-" set:"-"`
// ValueOnChange is a function set by [Bind] that is called when
// the widget receives an [events.Change] event to update the bound value
// from the widget's value. It should not be accessed by end users.
ValueOnChange func() `copier:"-" json:"-" xml:"-" set:"-"`
// ValueTitle is the title to display for a dialog for this [Value].
ValueTitle string
/// flags are atomic bit flags for [WidgetBase] state.
flags widgetFlags
}
// widgetFlags are atomic bit flags for [WidgetBase] state.
// They must be atomic to prevent race conditions.
type widgetFlags int64 //enums:bitflag -trim-prefix widget
const (
// widgetValueNewWindow indicates that the dialog of a [Value] should be opened
// as a new window, instead of a typical full window in the same current window.
// This is set by [InitValueButton] and handled by [openValueDialog].
// This is triggered by holding down the Shift key while clicking on a
// [Value] button. Certain values such as [FileButton] may set this to true
// in their [InitValueButton] function.
widgetValueNewWindow widgetFlags = iota
// widgetNeedsRender is whether the widget needs to be rendered on the next render iteration.
widgetNeedsRender
// widgetFirstRender indicates that we were the first to render, and pushed our parent's
// bounds, which then need to be popped.
widgetFirstRender
)
// hasFlag returns whether the given flag is set.
func (wb *WidgetBase) hasFlag(f widgetFlags) bool {
return wb.flags.HasFlag(f)
}
// setFlag sets the given flags to the given value.
func (wb *WidgetBase) setFlag(on bool, f ...enums.BitFlag) {
wb.flags.SetFlag(on, f...)
}
// Init should be called by every [Widget] type in its custom
// Init if it has one to establish all the default styling
// and event handling that applies to all widgets.
func (wb *WidgetBase) Init() {
wb.Styler(func(s *styles.Style) {
s.MaxBorder.Style.Set(styles.BorderSolid)
s.MaxBorder.Color.Set(colors.Scheme.Primary.Base)
s.MaxBorder.Width.Set(units.Dp(1))
// if we are disabled, we do not react to any state changes,
// and instead always have the same gray colors
if s.Is(states.Disabled) {
s.Cursor = cursors.NotAllowed
s.Opacity = 0.38
return
}
// TODO(kai): what about context menus on mobile?
tt, _ := wb.This.(Widget).WidgetTooltip(image.Pt(-1, -1))
s.SetAbilities(tt != "", abilities.LongHoverable, abilities.LongPressable)
if s.Is(states.Selected) {
s.Background = colors.Scheme.Select.Container
s.Color = colors.Scheme.Select.OnContainer
}
})
wb.FinalStyler(func(s *styles.Style) {
if s.Is(states.Focused) {
s.Border.Style = s.MaxBorder.Style
s.Border.Color = s.MaxBorder.Color
s.Border.Width = s.MaxBorder.Width
}
if !s.AbilityIs(abilities.Focusable) {
// never need bigger border if not focusable
s.MaxBorder = s.Border
}
})
// TODO(kai): maybe move all of these event handling functions into one function
wb.handleWidgetClick()
wb.handleWidgetStateFromMouse()
wb.handleLongHoverTooltip()
wb.handleWidgetStateFromFocus()
wb.handleWidgetContextMenu()
wb.handleWidgetMagnify()
wb.handleValueOnChange()
wb.Updater(wb.UpdateFromMake)
}
// OnAdd is called when widgets are added to a parent.
// It sets the scene of the widget to its widget parent.
// It should be called by all other OnAdd functions defined
// by widget types.
func (wb *WidgetBase) OnAdd() {
if pwb := wb.parentWidget(); pwb != nil {
wb.Scene = pwb.Scene
}
if wb.Parts != nil {
// the Scene of the Parts may not have been set yet if they were made in Init
wb.Parts.Scene = wb.Scene
}
if wb.Scene != nil && wb.Scene.WidgetInit != nil {
wb.Scene.WidgetInit(wb.This.(Widget))
}
}
// setScene sets the Scene pointer for this widget and all of its children.
// This can be necessary when creating widgets outside the usual New* paradigm,
// e.g., when reading from a JSON file.
func (wb *WidgetBase) setScene(sc *Scene) {
wb.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
cwb.Scene = sc
return tree.Continue
})
}
// AsWidget returns the given [tree.Node] as a [WidgetBase] or nil.
func AsWidget(n tree.Node) *WidgetBase {
if w, ok := n.(Widget); ok {
return w.AsWidget()
}
return nil
}
func (wb *WidgetBase) AsWidget() *WidgetBase {
return wb
}
func (wb *WidgetBase) CopyFieldsFrom(from tree.Node) {
wb.NodeBase.CopyFieldsFrom(from)
frm := AsWidget(from)
n := len(wb.ContextMenus)
if len(frm.ContextMenus) > n {
wb.ContextMenus = append(wb.ContextMenus, frm.ContextMenus[n:]...)
}
wb.Stylers.DoWith(&frm.Stylers, func(to, from *[]func(s *styles.Style)) {
n := len(*to)
if len(*from) > n {
*to = append(*to, (*from)[n:]...)
}
})
wb.Listeners.DoWith(&frm.Listeners, func(to, from *events.Listeners) {
to.CopyFromExtra(*from)
})
}
func (wb *WidgetBase) Destroy() {
wb.deleteParts()
wb.NodeBase.Destroy()
}
// deleteParts deletes the widget's parts (and the children of the parts).
func (wb *WidgetBase) deleteParts() {
if wb.Parts != nil {
wb.Parts.Destroy()
}
wb.Parts = nil
}
// newParts makes the [WidgetBase.Parts] if they don't already exist.
// It returns the parts regardless.
func (wb *WidgetBase) newParts() *Frame {
if wb.Parts != nil {
return wb.Parts
}
wb.Parts = NewFrame()
wb.Parts.SetName("parts")
tree.SetParent(wb.Parts, wb) // don't add to children list
wb.Parts.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
s.RenderBox = false
})
return wb.Parts
}
// parentWidget returns the parent as a [WidgetBase] or nil
// if this is the root and has no parent.
func (wb *WidgetBase) parentWidget() *WidgetBase {
if wb.Parent == nil {
return nil
}
pw, ok := wb.Parent.(Widget)
if ok {
return pw.AsWidget()
}
return nil // the parent may be a non-widget in [tree.UnmarshalRootJSON]
}
// IsVisible returns true if a widget is visible for rendering according
// to the [states.Invisible] flag on it or any of its parents.
// This flag is also set by [styles.DisplayNone] during [WidgetBase.Style].
// This does *not* check for an empty TotalBBox, indicating that the widget
// is out of render range; that is done by [WidgetBase.PushBounds] prior to rendering.
// Non-visible nodes are automatically not rendered and do not get
// window events.
// This call recursively calls the parent, which is typically a short path.
func (wb *WidgetBase) IsVisible() bool {
if wb == nil || wb.This == nil || wb.StateIs(states.Invisible) || wb.Scene == nil {
return false
}
if wb.Parent == nil {
return true
}
return wb.parentWidget().IsVisible()
}
// RenderDraw draws the current image onto the RenderWindow window,
// using the [system.Drawer] interface methods, typically [Drawer.Copy].
// The given draw operation is suggested by the RenderWindow, with the
// first main window using draw.Src and the rest using draw.Over.
// Individual draw methods are free to ignore if necessary.
// Optimized direct rendering widgets can register by doing
// [Scene.AddDirectRender] to directly draw into the window texture.
func (wb *WidgetBase) RenderDraw(drw system.Drawer, op draw.Op) {}
// NodeWalkDown extends [tree.Node.WalkDown] to [WidgetBase.Parts],
// which is key for getting full tree traversal to work when updating,
// configuring, and styling. This implements [tree.Node.NodeWalkDown].
func (wb *WidgetBase) NodeWalkDown(fun func(tree.Node) bool) {
if wb.Parts == nil {
return
}
wb.Parts.WalkDown(fun)
}
// ForWidgetChildren iterates through the children as widgets, calling the given function.
// Return [tree.Continue] (true) to continue, and [tree.Break] (false) to terminate.
func (wb *WidgetBase) ForWidgetChildren(fun func(i int, cw Widget, cwb *WidgetBase) bool) {
for i, c := range wb.Children {
w, cwb := c.(Widget), AsWidget(c)
if !fun(i, w, cwb) {
break
}
}
}
// forVisibleChildren iterates through the children,as widgets, calling the given function,
// excluding any with the *local* states.Invisible flag set (does not check parents).
// This is used e.g., for layout functions to exclude non-visible direct children.
// Return [tree.Continue] (true) to continue, and [tree.Break] (false) to terminate.
func (wb *WidgetBase) forVisibleChildren(fun func(i int, cw Widget, cwb *WidgetBase) bool) {
for i, k := range wb.Children {
w, cwb := k.(Widget), AsWidget(k)
if cwb.StateIs(states.Invisible) {
continue
}
cont := fun(i, w, cwb)
if !cont {
break
}
}
}
// WidgetWalkDown is a version of [tree.NodeBase.WalkDown] that operates on [Widget] types,
// calling the given function on the Widget and all of its children in a depth-first manner.
// Return [tree.Continue] to continue and [tree.Break] to terminate.
func (wb *WidgetBase) WidgetWalkDown(fun func(cw Widget, cwb *WidgetBase) bool) {
wb.WalkDown(func(n tree.Node) bool {
cw, cwb := n.(Widget), AsWidget(n)
return fun(cw, cwb)
})
}
// widgetNext returns the next widget in the tree,
// including Parts, which are considered to come after Children.
// returns nil if no more.
func widgetNext(w Widget) Widget {
wb := w.AsWidget()
if !wb.HasChildren() && wb.Parts == nil {
return widgetNextSibling(w)
}
if wb.HasChildren() {
return wb.Child(0).(Widget)
}
if wb.Parts != nil {
return widgetNext(wb.Parts.This.(Widget))
}
return nil
}
// widgetNextSibling returns next sibling or nil if none,
// including Parts, which are considered to come after Children.
func widgetNextSibling(w Widget) Widget {
wb := w.AsWidget()
if wb.Parent == nil {
return nil
}
parent := wb.Parent.(Widget)
myidx := wb.IndexInParent()
if myidx >= 0 && myidx < wb.Parent.AsTree().NumChildren()-1 {
return parent.AsTree().Child(myidx + 1).(Widget)
}
return widgetNextSibling(parent)
}
// widgetPrev returns the previous widget in the tree,
// including Parts, which are considered to come after Children.
// nil if no more.
func widgetPrev(w Widget) Widget {
wb := w.AsWidget()
if wb.Parent == nil {
return nil
}
parent := wb.Parent.(Widget)
myidx := wb.IndexInParent()
if myidx > 0 {
nn := parent.AsTree().Child(myidx - 1).(Widget)
return widgetLastChildParts(nn) // go to parts
}
// we were children, done
return parent
}
// widgetLastChildParts returns the last child under given node,
// or node itself if no children. Starts with Parts,
func widgetLastChildParts(w Widget) Widget {
wb := w.AsWidget()
if wb.Parts != nil && wb.Parts.HasChildren() {
return widgetLastChildParts(wb.Parts.Child(wb.Parts.NumChildren() - 1).(Widget))
}
if wb.HasChildren() {
return widgetLastChildParts(wb.Child(wb.NumChildren() - 1).(Widget))
}
return w
}
// widgetNextFunc returns the next widget in the tree,
// including Parts, which are considered to come after children,
// continuing until the given function returns true.
// nil if no more.
func widgetNextFunc(w Widget, fun func(w Widget) bool) Widget {
for {
nw := widgetNext(w)
if nw == nil {
return nil
}
if fun(nw) {
return nw
}
if nw == w {
slog.Error("WidgetNextFunc", "start", w, "nw == wi", nw)
return nil
}
w = nw
}
}
// widgetPrevFunc returns the previous widget in the tree,
// including Parts, which are considered to come after children,
// continuing until the given function returns true.
// nil if no more.
func widgetPrevFunc(w Widget, fun func(w Widget) bool) Widget {
for {
pw := widgetPrev(w)
if pw == nil {
return nil
}
if fun(pw) {
return pw
}
if pw == w {
slog.Error("WidgetPrevFunc", "start", w, "pw == wi", pw)
return nil
}
w = pw
}
}
// WidgetTooltip is the base implementation of [Widget.WidgetTooltip],
// which just returns [WidgetBase.Tooltip] and [WidgetBase.DefaultTooltipPos].
func (wb *WidgetBase) WidgetTooltip(pos image.Point) (string, image.Point) {
return wb.Tooltip, wb.DefaultTooltipPos()
}
// 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 (
"fmt"
"image"
"log/slog"
"cogentcore.org/core/events"
"cogentcore.org/core/events/key"
"cogentcore.org/core/keymap"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/system"
"cogentcore.org/core/tree"
)
// Events returns the higher-level core event manager
// for this [Widget]'s [Scene].
func (wb *WidgetBase) Events() *Events {
if wb.Scene == nil {
return nil
}
return &wb.Scene.Events
}
// SystemEvents returns the lower-level system event
// manager for this [Widget]'s [Scene].
func (wb *WidgetBase) SystemEvents() *events.Source {
return wb.Scene.RenderWindow().SystemWindow.Events()
}
// Clipboard returns the clipboard for the [Widget] to use.
func (wb *WidgetBase) Clipboard() system.Clipboard {
return wb.Events().Clipboard()
}
// On adds the given event handler to the [WidgetBase.Listeners.Normal] for the given
// event type. Listeners are called in sequential descending order, so this listener
// will be called before all of the ones added before it. On is one of the main ways
// to add an event handler to a widget, in addition to OnFirst and OnFinal, which add
// event handlers that are called before and after those added by this function,
// respectively.
func (wb *WidgetBase) On(etype events.Types, fun func(e events.Event)) {
wb.Listeners.Normal.Add(etype, fun)
}
// OnFirst adds the given event handler to the [WidgetBase.Listeners.First] for the given
// event type. FirstListeners are called in sequential descending order, so this first
// listener will be called before all of the ones added before it. OnFirst is one of the
// main ways to add an event handler to a widget, in addition to On and OnFinal,
// which add event handlers that are called after those added by this function.
func (wb *WidgetBase) OnFirst(etype events.Types, fun func(e events.Event)) {
wb.Listeners.First.Add(etype, fun)
}
// OnFinal adds the given event handler to the [WidgetBase.Listeners.Final] for the given
// event type. FinalListeners are called in sequential descending order, so this final
// listener will be called before all of the ones added before it. OnFinal is one of the
// main ways to add an event handler to a widget, in addition to OnFirst and On,
// which add event handlers that are called before those added by this function.
func (wb *WidgetBase) OnFinal(etype events.Types, fun func(e events.Event)) {
wb.Listeners.Final.Add(etype, fun)
}
// Helper functions for common event types:
// OnClick adds an event listener function for [events.Click] events.
func (wb *WidgetBase) OnClick(fun func(e events.Event)) {
wb.On(events.Click, fun)
}
// OnDoubleClick adds an event listener function for [events.DoubleClick] events.
func (wb *WidgetBase) OnDoubleClick(fun func(e events.Event)) {
wb.On(events.DoubleClick, fun)
}
// OnChange adds an event listener function for [events.Change] events.
func (wb *WidgetBase) OnChange(fun func(e events.Event)) {
wb.On(events.Change, fun)
}
// OnInput adds an event listener function for [events.Input] events.
func (wb *WidgetBase) OnInput(fun func(e events.Event)) {
wb.On(events.Input, fun)
}
// OnKeyChord adds an event listener function for [events.KeyChord] events.
func (wb *WidgetBase) OnKeyChord(fun func(e events.Event)) {
wb.On(events.KeyChord, fun)
}
// OnFocus adds an event listener function for [events.Focus] events.
func (wb *WidgetBase) OnFocus(fun func(e events.Event)) {
wb.On(events.Focus, fun)
}
// OnFocusLost adds an event listener function for [events.FocusLost] events.
func (wb *WidgetBase) OnFocusLost(fun func(e events.Event)) {
wb.On(events.FocusLost, fun)
}
// OnSelect adds an event listener function for [events.Select] events.
func (wb *WidgetBase) OnSelect(fun func(e events.Event)) {
wb.On(events.Select, fun)
}
// OnShow adds an event listener function for [events.Show] events.
func (wb *WidgetBase) OnShow(fun func(e events.Event)) {
wb.On(events.Show, fun)
}
// OnClose adds an event listener function for [events.Close] events.
func (wb *WidgetBase) OnClose(fun func(e events.Event)) {
wb.On(events.Close, fun)
}
// AddCloseDialog adds a dialog that confirms that the user wants to close the Scene
// associated with this widget when they try to close it. It calls the given config
// function to configure the dialog. It is the responsibility of this config function
// to add the title and close button to the dialog, which is necessary so that the close
// dialog can be fully customized. If this function returns false, it does not make the
// dialog. This can be used to make the dialog conditional on other things, like whether
// something is saved.
func (wb *WidgetBase) AddCloseDialog(config func(d *Body) bool) {
var inClose, canClose bool
wb.OnClose(func(e events.Event) {
if canClose {
return // let it close
}
if inClose {
e.SetHandled()
return
}
inClose = true
d := NewBody()
d.AddBottomBar(func(bar *Frame) {
d.AddCancel(bar).OnClick(func(e events.Event) {
inClose = false
canClose = false
})
bar.AsWidget().SetOnChildAdded(func(n tree.Node) {
if bt := AsButton(n); bt != nil {
bt.OnFirst(events.Click, func(e events.Event) {
// any button click gives us permission to close
canClose = true
})
}
})
})
if !config(d) {
return
}
e.SetHandled()
d.RunDialog(wb)
})
}
// Send sends an new event of the given type to this widget,
// optionally starting from values in the given original event
// (recommended to include where possible).
// Do not send an existing event using this method if you
// want the Handled state to persist throughout the call chain;
// call HandleEvent directly for any existing events.
func (wb *WidgetBase) Send(typ events.Types, original ...events.Event) {
if wb.This == nil {
return
}
if typ == events.Click {
em := wb.Events()
if em != nil && em.focus != wb.This.(Widget) {
// always clear any other focus before the click is processed.
// this causes textfields etc to apply their changes.
em.focusClear()
}
}
var e events.Event
if len(original) > 0 && original[0] != nil {
e = original[0].NewFromClone(typ)
} else {
e = &events.Base{Typ: typ}
e.Init()
}
wb.HandleEvent(e)
}
// SendChange sends a new [events.Change] event, which is widely used to signal
// value changing for most widgets. It takes the event that the new change event
// is derived from, if any.
func (wb *WidgetBase) SendChange(original ...events.Event) {
wb.Send(events.Change, original...)
}
// UpdateChange is a helper function that calls [WidgetBase.SendChange]
// and then [WidgetBase.Update]. That is the correct order, since
// calling [WidgetBase.Update] first would cause the value of the widget
// to be incorrectly overridden in a [Value] context.
func (wb *WidgetBase) UpdateChange(original ...events.Event) {
wb.SendChange(original...)
wb.Update()
}
func (wb *WidgetBase) sendKey(kf keymap.Functions, original ...events.Event) {
if wb.This == nil {
return
}
kc := kf.Chord()
wb.sendKeyChord(kc, original...)
}
func (wb *WidgetBase) sendKeyChord(kc key.Chord, original ...events.Event) {
r, code, mods, err := kc.Decode()
if err != nil {
fmt.Println("SendKeyChord: Decode error:", err)
return
}
wb.sendKeyChordRune(r, code, mods, original...)
}
func (wb *WidgetBase) sendKeyChordRune(r rune, code key.Codes, mods key.Modifiers, original ...events.Event) {
ke := events.NewKey(events.KeyChord, r, code, mods)
if len(original) > 0 && original[0] != nil {
kb := *original[0].AsBase()
ke.GenTime = kb.GenTime
ke.ClearHandled()
} else {
ke.Init()
}
ke.Typ = events.KeyChord
wb.HandleEvent(ke)
}
// HandleEvent sends the given event to all [WidgetBase.Listeners] for that event type.
// It also checks if the State has changed and calls [WidgetBase.Restyle] if so.
func (wb *WidgetBase) HandleEvent(e events.Event) {
if DebugSettings.EventTrace {
if e.Type() != events.MouseMove {
fmt.Println(e, "to", wb)
}
}
if wb == nil || wb.This == nil {
return
}
s := &wb.Styles
state := s.State
wb.Listeners.Do(func(l events.Listeners) {
l.Call(e, func() bool {
return wb.This != nil
})
})
if s.State != state {
wb.Restyle()
}
}
// firstHandleEvent sends the given event to the Listeners.First for that event type.
// Does NOT do any state updating.
func (wb *WidgetBase) firstHandleEvent(e events.Event) {
if DebugSettings.EventTrace {
if e.Type() != events.MouseMove {
fmt.Println(e, "first to", wb)
}
}
wb.Listeners.First.Call(e, func() bool {
return wb.This != nil
})
}
// finalHandleEvent sends the given event to the Listeners.Final for that event type.
// Does NOT do any state updating.
func (wb *WidgetBase) finalHandleEvent(e events.Event) {
if DebugSettings.EventTrace {
if e.Type() != events.MouseMove {
fmt.Println(e, "final to", wb)
}
}
wb.Listeners.Final.Call(e, func() bool {
return wb.This != nil
})
}
// posInScBBox returns true if given position is within
// this node's scene bbox
func (wb *WidgetBase) posInScBBox(pos image.Point) bool {
return pos.In(wb.Geom.TotalBBox)
}
// handleWidgetClick handles the Click event for basic Widget behavior.
// For Left button:
// If Checkable, toggles Checked. if Focusable, Focuses or clears,
// If Selectable, updates state and sends Select, Deselect.
func (wb *WidgetBase) handleWidgetClick() {
wb.OnClick(func(e events.Event) {
if wb.AbilityIs(abilities.Checkable) {
wb.SetState(!wb.StateIs(states.Checked), states.Checked)
}
if wb.AbilityIs(abilities.Focusable) {
wb.SetFocusQuiet()
} else {
wb.focusClear()
}
// note: read only widgets are automatically selectable
if wb.AbilityIs(abilities.Selectable) || wb.IsReadOnly() {
wb.Send(events.Select, e)
}
})
}
// handleWidgetStateFromMouse updates all standard
// State flags based on mouse events,
// such as MouseDown / Up -> Active and MouseEnter / Leave -> Hovered.
// None of these "consume" the event by setting Handled flag, as they are
// designed to work in conjunction with more specific handlers.
// Note that Disabled and Invisible widgets do NOT receive
// these events so it is not necessary to check that.
func (wb *WidgetBase) handleWidgetStateFromMouse() {
wb.On(events.MouseDown, func(e events.Event) {
if wb.AbilityIs(abilities.Activatable) {
wb.SetState(true, states.Active)
}
})
wb.On(events.MouseUp, func(e events.Event) {
if wb.AbilityIs(abilities.Activatable) {
wb.SetState(false, states.Active)
}
})
wb.On(events.LongPressStart, func(e events.Event) {
if wb.AbilityIs(abilities.LongPressable) {
wb.SetState(true, states.LongPressed)
}
})
wb.On(events.LongPressEnd, func(e events.Event) {
if wb.AbilityIs(abilities.LongPressable) {
wb.SetState(false, states.LongPressed)
}
})
wb.On(events.MouseEnter, func(e events.Event) {
if wb.AbilityIs(abilities.Hoverable) {
wb.SetState(true, states.Hovered)
}
})
wb.On(events.MouseLeave, func(e events.Event) {
if wb.AbilityIs(abilities.Hoverable) {
wb.SetState(false, states.Hovered)
}
})
wb.On(events.LongHoverStart, func(e events.Event) {
if wb.AbilityIs(abilities.LongHoverable) {
wb.SetState(true, states.LongHovered)
}
})
wb.On(events.LongHoverEnd, func(e events.Event) {
if wb.AbilityIs(abilities.LongHoverable) {
wb.SetState(false, states.LongHovered)
}
})
wb.On(events.SlideStart, func(e events.Event) {
if wb.AbilityIs(abilities.Slideable) {
wb.SetState(true, states.Sliding)
}
})
wb.On(events.SlideStop, func(e events.Event) {
if wb.AbilityIs(abilities.Slideable) {
wb.SetState(false, states.Sliding, states.Active)
}
})
wb.On(events.DragStart, func(e events.Event) {
if wb.AbilityIs(abilities.Draggable) {
wb.SetState(true, states.Dragging)
}
})
wb.On(events.Drop, func(e events.Event) {
if wb.AbilityIs(abilities.Draggable) {
wb.SetState(false, states.Dragging, states.Active)
}
})
}
// handleLongHoverTooltip listens for LongHover and LongPress events and
// pops up and deletes tooltips based on those. Most widgets should call
// this as part of their event handler methods.
func (wb *WidgetBase) handleLongHoverTooltip() {
wb.On(events.LongHoverStart, func(e events.Event) {
wi := wb.This.(Widget)
tt, pos := wi.WidgetTooltip(e.Pos())
if tt == "" {
return
}
e.SetHandled()
newTooltip(wi, tt, pos).Run()
})
wb.On(events.LongHoverEnd, func(e events.Event) {
if wb.Scene.Stage != nil {
wb.Scene.Stage.popups.popDeleteType(TooltipStage)
}
})
wb.On(events.LongPressStart, func(e events.Event) {
wb.Send(events.ContextMenu, e)
wi := wb.This.(Widget)
tt, pos := wi.WidgetTooltip(e.Pos())
if tt == "" {
return
}
e.SetHandled()
newTooltip(wi, tt, pos).Run()
})
wb.On(events.LongPressEnd, func(e events.Event) {
if wb.Scene.Stage != nil {
wb.Scene.Stage.popups.popDeleteType(TooltipStage)
}
})
}
// handleWidgetStateFromFocus updates standard State flags based on Focus events
func (wb *WidgetBase) handleWidgetStateFromFocus() {
wb.OnFocus(func(e events.Event) {
if wb.AbilityIs(abilities.Focusable) {
wb.ScrollToThis()
wb.SetState(true, states.Focused)
if !wb.IsReadOnly() && wb.Styles.VirtualKeyboard != styles.KeyboardNone {
TheApp.ShowVirtualKeyboard(wb.Styles.VirtualKeyboard)
}
}
})
wb.OnFocusLost(func(e events.Event) {
if wb.AbilityIs(abilities.Focusable) {
wb.SetState(false, states.Focused)
if !wb.IsReadOnly() && wb.Styles.VirtualKeyboard != styles.KeyboardNone {
TheApp.HideVirtualKeyboard()
}
}
})
}
// HandleWidgetMagnifyEvent calls [renderWindow.stepZoom] on [events.Magnify]
func (wb *WidgetBase) handleWidgetMagnify() {
wb.On(events.Magnify, func(e events.Event) {
ev := e.(*events.TouchMagnify)
wb.Events().RenderWindow().stepZoom(ev.ScaleFactor - 1)
})
}
// handleValueOnChange adds a handler that calls [WidgetBase.ValueOnChange].
func (wb *WidgetBase) handleValueOnChange() {
// need to go before end-user OnChange handlers
wb.OnFirst(events.Change, func(e events.Event) {
if wb.ValueOnChange != nil {
wb.ValueOnChange()
}
})
}
// HandleClickOnEnterSpace adds a key event handler for Enter and Space
// keys to generate an [events.Click] event. This is not added by default,
// but is added in [Button] and [Switch] for example.
func (wb *WidgetBase) HandleClickOnEnterSpace() {
wb.OnKeyChord(func(e events.Event) {
kf := keymap.Of(e.KeyChord())
if DebugSettings.KeyEventTrace {
slog.Info("WidgetBase HandleClickOnEnterSpace", "widget", wb, "keyFunction", kf)
}
if kf == keymap.Accept {
wb.Send(events.Click, e) // don't handle
} else if kf == keymap.Enter || e.KeyRune() == ' ' {
e.SetHandled()
wb.Send(events.Click, e)
}
})
}
//////// Focus
// SetFocusQuiet sets the keyboard input focus on this item or the first item
// within it that can be focused (if none, then just sets focus to this widget).
// This does NOT send an [events.Focus] event, so the widget will NOT appear focused;
// it will however receive keyboard input, at which point it will get visible focus.
// See [WidgetBase.SetFocus] for a version that sends an event. Also see
// [WidgetBase.StartFocus].
func (wb *WidgetBase) SetFocusQuiet() {
foc := wb.This.(Widget)
if !wb.AbilityIs(abilities.Focusable) {
foc = wb.focusableInThis()
if foc == nil {
foc = wb.This.(Widget)
}
}
em := wb.Events()
if em != nil {
em.setFocusQuiet(foc) // doesn't send event
}
}
// SetFocus sets the keyboard input focus on this item or the first item within it
// that can be focused (if none, then just sets focus to this widget).
// This sends an [events.Focus] event, which typically results in
// the widget being styled as focused. See [WidgetBase.SetFocusQuiet] for
// a version that does not. Also see [WidgetBase.StartFocus].
func (wb *WidgetBase) SetFocus() {
foc := wb.This.(Widget)
if !wb.AbilityIs(abilities.Focusable) {
foc = wb.focusableInThis()
if foc == nil {
foc = wb.This.(Widget)
}
}
em := wb.Events()
if em != nil {
em.setFocus(foc)
}
}
// focusableInThis returns the first Focusable element within this widget
func (wb *WidgetBase) focusableInThis() Widget {
var foc Widget
wb.WidgetWalkDown(func(cw Widget, cwb *WidgetBase) bool {
if !cwb.AbilityIs(abilities.Focusable) {
return tree.Continue
}
foc = cw
return tree.Break // done
})
return foc
}
// focusNext moves the focus onto the next item
func (wb *WidgetBase) focusNext() {
em := wb.Events()
if em != nil {
em.focusNext()
}
}
// focusPrev moves the focus onto the previous item
func (wb *WidgetBase) focusPrev() {
em := wb.Events()
if em != nil {
em.focusPrev()
}
}
// focusClear resets focus to nil, but keeps the previous focus to pick up next time..
func (wb *WidgetBase) focusClear() {
em := wb.Events()
if em != nil {
em.focusClear()
}
}
// StartFocus specifies that this widget should get focus when the [Scene] is shown,
// or when a major content managing widget (e.g., [Tabs], [Pages]) shows a
// tab/page/element that contains this widget. This is implemented via an
// [events.Show] event.
func (wb *WidgetBase) StartFocus() {
em := wb.Events()
if em != nil {
em.SetStartFocus(wb.This.(Widget))
}
}
// ContainsFocus returns whether this widget contains the current focus widget.
func (wb *WidgetBase) ContainsFocus() bool {
em := wb.Events()
if em == nil {
return false
}
cur := em.focus
if cur == nil {
return false
}
if cur.AsTree().This == wb.This {
return true
}
plev := cur.AsTree().ParentLevel(wb.This)
return plev >= 0
}
// 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 core
import (
"encoding/json"
"fmt"
"image"
"log"
"maps"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"time"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/system"
)
var (
// theWindowGeometrySaver is the manager of window geometry settings
theWindowGeometrySaver = windowGeometrySaver{}
)
// screenConfigGeometries has the window geometry data for different
// screen configurations, where a screen configuration is a specific
// set of available screens, naturally corresponding to the
// home vs. office vs. travel usage of a laptop, for example,
// with different sets of screens available in each location.
// Each such configuration has a different set of saved window geometries,
// which is restored when the set of screens changes, so your windows will
// be restored to their last positions and sizes for each such configuration.
type screenConfigGeometries map[string]map[string]windowGeometries
// screenConfig returns the current screen configuration string,
// which is the alpha-sorted list of current screen names.
func screenConfig() string {
ns := TheApp.NScreens()
if ns == 0 {
return "none"
}
scs := make([]string, ns)
for i := range ns {
scs[i] = TheApp.Screen(i).Name
}
slices.Sort(scs)
return strings.Join(scs, "|")
}
// windowGeometrySaver records window geometries in a persistent file,
// which is then used when opening new windows to restore.
type windowGeometrySaver struct {
// the full set of window geometries
geometries screenConfigGeometries
// temporary cached geometries: saved to geometries after SaveDelay
cache screenConfigGeometries
// base name of the settings file in Cogent Core settings directory
filename string
// when settings were last saved: if we weren't the last to save,
// then we need to re-open before modifying.
lastSave time.Time
// if true, we are setting geometry so don't save;
// Caller must call SettingStart() SettingEnd() to block.
settingNoSave bool
// read-write mutex that protects updating of WindowGeometry
mu sync.RWMutex
// wait time before trying to lock file again
lockSleep time.Duration
// wait time before saving the Cache into Geometries
saveDelay time.Duration
// timer for delayed save
saveTimer *time.Timer
}
// init does initialization if not yet initialized
func (ws *windowGeometrySaver) init() {
if ws.geometries == nil {
ws.geometries = make(screenConfigGeometries)
ws.resetCache()
ws.filename = "window-geometry-0.3.6"
ws.lockSleep = 100 * time.Millisecond
ws.saveDelay = 1 * time.Second
}
}
// shouldSave returns whether the window geometry should be saved based on
// the platform: only for desktop native platforms.
func (ws *windowGeometrySaver) shouldSave() bool {
return !TheApp.Platform().IsMobile() && TheApp.Platform() != system.Offscreen && !DebugSettings.DisableWindowGeometrySaver
}
// resetCache resets the cache; call under mutex
func (ws *windowGeometrySaver) resetCache() {
ws.cache = make(screenConfigGeometries)
}
// lockFile attempts to create the window geometry lock file
func (ws *windowGeometrySaver) lockFile() error {
pdir := TheApp.CogentCoreDataDir()
pnm := filepath.Join(pdir, ws.filename+".lck")
for rep := 0; rep < 10; rep++ {
if _, err := os.Stat(pnm); os.IsNotExist(err) {
b, _ := time.Now().MarshalJSON()
err = os.WriteFile(pnm, b, 0644)
if err == nil {
return nil
}
}
b, err := os.ReadFile(pnm)
if err != nil {
time.Sleep(ws.lockSleep)
continue
}
var lts time.Time
err = lts.UnmarshalJSON(b)
if err != nil {
time.Sleep(ws.lockSleep)
continue
}
if time.Since(lts) > 1*time.Second {
// log.Printf("WindowGeometry: lock file stale: %v\n", lts.String())
os.Remove(pnm)
continue
}
// log.Printf("WindowGeometry: waiting for lock file: %v\n", lts.String())
time.Sleep(ws.lockSleep)
}
return errors.New("WinGeom could not lock lock file")
}
// UnLockFile unlocks the window geometry lock file (just removes it)
func (ws *windowGeometrySaver) unlockFile() {
pdir := TheApp.CogentCoreDataDir()
pnm := filepath.Join(pdir, ws.filename+".lck")
os.Remove(pnm)
}
// needToReload returns true if the last save time of settings file is more recent than
// when we last saved. Called under mutex.
func (ws *windowGeometrySaver) needToReload() bool {
pdir := TheApp.CogentCoreDataDir()
pnm := filepath.Join(pdir, ws.filename+".lst")
if _, err := os.Stat(pnm); os.IsNotExist(err) {
return false
}
var lts time.Time
b, err := os.ReadFile(pnm)
if err != nil {
return false
}
err = lts.UnmarshalJSON(b)
if err != nil {
return false
}
eq := lts.Equal(ws.lastSave)
if !eq {
// fmt.Printf("settings file saved more recently: %v than our last save: %v\n", lts.String(),
// mgr.LastSave.String())
ws.lastSave = lts
}
return !eq
}
// saveLastSave saves timestamp (now) of last save to win geom
func (ws *windowGeometrySaver) saveLastSave() {
pdir := TheApp.CogentCoreDataDir()
pnm := filepath.Join(pdir, ws.filename+".lst")
ws.lastSave = time.Now()
b, _ := ws.lastSave.MarshalJSON()
os.WriteFile(pnm, b, 0644)
}
// open RenderWindow Geom settings from Cogent Core standard settings directory
// called under mutex or at start
func (ws *windowGeometrySaver) open() error {
ws.init()
pdir := TheApp.CogentCoreDataDir()
pnm := filepath.Join(pdir, ws.filename+".json")
b, err := os.ReadFile(pnm)
if err != nil {
return err
}
return json.Unmarshal(b, &ws.geometries)
}
// save RenderWindow Geom Settings to Cogent Core standard prefs directory
// assumed to be under mutex and lock still
func (ws *windowGeometrySaver) save() error {
if ws.geometries == nil {
return nil
}
pdir := TheApp.CogentCoreDataDir()
pnm := filepath.Join(pdir, ws.filename+".json")
b, err := json.Marshal(ws.geometries)
if errors.Log(err) != nil {
return err
}
err = os.WriteFile(pnm, b, 0644)
if errors.Log(err) == nil {
ws.saveLastSave()
}
return err
}
// windowName returns window name before first colon, if exists.
// This is the part of the name used to record settings
func (ws *windowGeometrySaver) windowName(winName string) string {
if ci := strings.Index(winName, ":"); ci > 0 {
return winName[:ci]
}
return winName
}
// settingStart turns on SettingNoSave to prevent subsequent redundant calls to
// save a geometry that was being set from already-saved settings.
// Must call SettingEnd to turn off (safe to call even if Start not called).
func (ws *windowGeometrySaver) settingStart() {
ws.mu.Lock()
ws.resetCache() // get rid of anything just saved prior to this -- sus.
ws.settingNoSave = true
ws.mu.Unlock()
}
// settingEnd turns off SettingNoSave -- safe to call even if Start not called.
func (ws *windowGeometrySaver) settingEnd() {
ws.mu.Lock()
ws.settingNoSave = false
ws.mu.Unlock()
}
// record records current state of window as preference
func (ws *windowGeometrySaver) record(win *renderWindow) {
if !ws.shouldSave() || !win.isVisible() || win.SystemWindow.Is(system.Fullscreen) {
return
}
win.SystemWindow.Lock()
wsz := win.SystemWindow.Size()
win.SystemWindow.Unlock()
if wsz == (image.Point{}) {
if DebugSettings.WindowGeometryTrace {
log.Printf("WindowGeometry: Record: NOT storing null size for win: %v\n", win.name)
}
return
}
sc := win.SystemWindow.Screen()
pos := win.SystemWindow.Position(sc)
if TheApp.Platform() == system.Windows && pos.X == -32000 || pos.Y == -32000 { // windows badness
if DebugSettings.WindowGeometryTrace {
log.Printf("WindowGeometry: Record: NOT storing very negative pos: %v for win: %v\n", pos, win.name)
}
return
}
ws.mu.Lock()
if ws.settingNoSave {
if DebugSettings.WindowGeometryTrace {
log.Printf("WindowGeometry: Record: SettingNoSave so NOT storing for win: %v\n", win.name)
}
ws.mu.Unlock()
return
}
ws.init()
cfg := screenConfig()
winName := ws.windowName(win.title)
wgr := windowGeometry{DPI: win.logicalDPI(), DPR: sc.DevicePixelRatio, Max: win.SystemWindow.Is(system.Maximized)}
wgr.Pos = pos
wgr.Size = wsz
// first get copy of stored data
sgs := ws.geometries[cfg]
if sgs == nil {
sgs = make(map[string]windowGeometries)
}
var wgs windowGeometries
if swgs, ok := sgs[winName]; ok {
wgs.Last = swgs.Last
wgs.Screens = maps.Clone(swgs.Screens)
} else {
wgs.Screens = make(map[string]windowGeometry)
}
// then look for current cache data
sgsc := ws.cache[cfg]
if sgsc == nil {
sgsc = make(map[string]windowGeometries)
}
wgsc, hasCache := sgsc[winName]
if hasCache {
wgs.Last = wgsc.Last
for k, v := range wgsc.Screens {
wgs.Screens[k] = v
}
}
wgs.Screens[sc.Name] = wgr
wgs.Last = sc.Name
sgsc[winName] = wgs
ws.cache[cfg] = sgsc
if DebugSettings.WindowGeometryTrace {
log.Printf("WindowGeometry: Record win: %q screen: %q cfg: %q geom: %s", winName, sc.Name, cfg, wgr.String())
}
if ws.saveTimer == nil {
ws.saveTimer = time.AfterFunc(time.Duration(ws.saveDelay), func() {
ws.mu.Lock()
ws.saveCached()
ws.saveTimer = nil
ws.mu.Unlock()
})
}
ws.mu.Unlock()
}
// saveCached saves the cached prefs -- called after timer delay,
// under the Mu.Lock
func (ws *windowGeometrySaver) saveCached() {
ws.lockFile() // not going to change our behavior if we can't lock!
if ws.needToReload() {
ws.open()
}
if DebugSettings.WindowGeometryTrace {
log.Println("WindowGeometry: saveCached")
}
for cfg, sgsc := range ws.cache {
for winName, wgs := range sgsc {
sg := ws.geometries[cfg]
if sg == nil {
sg = make(map[string]windowGeometries)
}
sg[winName] = wgs
ws.geometries[cfg] = sg
}
}
ws.resetCache()
ws.save()
ws.unlockFile()
}
// get returns saved geometry for given window name, returning
// nil if there is no saved info. The last saved screen is used
// if it is currently available (connected); otherwise the given screen
// name is used if non-empty; otherwise the default screen 0 is used.
// If no saved info is found for any active screen, nil is returned.
// The screen used for the preferences is returned, and should be used
// to set the screen for a new window.
// If the window name has a colon, only the part prior to the colon is used.
func (ws *windowGeometrySaver) get(winName, screenName string) (*windowGeometry, *system.Screen) {
if !ws.shouldSave() {
return nil, nil
}
ws.mu.RLock()
defer ws.mu.RUnlock()
cfg := screenConfig()
winName = ws.windowName(winName)
var wgs windowGeometries
fromMain := false
sgs := ws.cache[cfg]
ok := false
if sgs != nil {
wgs, ok = sgs[winName]
}
if !ok {
sgs, ok = ws.geometries[cfg]
if !ok {
return nil, nil
}
wgs, ok = sgs[winName]
fromMain = true
}
if !ok {
return nil, nil
}
wgr, sc := wgs.getForScreen(screenName)
if wgr != nil {
wgr.constrainGeom(sc)
if DebugSettings.WindowGeometryTrace {
log.Printf("WindowGeometry: Got geom for window: %q screen: %q lastScreen: %q cfg: %q geom: %s fromMain: %v\n", winName, sc.Name, wgs.Last, cfg, wgr.String(), fromMain)
}
return wgr, sc
}
return nil, nil
}
// deleteAll deletes the file that saves the position and size of each window,
// by screen, and clear current in-memory cache. You shouldn't need to use
// this but sometimes useful for testing.
func (ws *windowGeometrySaver) deleteAll() {
ws.mu.Lock()
defer ws.mu.Unlock()
pdir := TheApp.CogentCoreDataDir()
pnm := filepath.Join(pdir, ws.filename+".json")
errors.Log(os.Remove(pnm))
ws.geometries = make(screenConfigGeometries)
}
// restoreAll restores size and position of all windows, for current screen.
// Called when screen changes.
func (ws *windowGeometrySaver) restoreAll() {
if !ws.shouldSave() {
return
}
renderWindowGlobalMu.Lock()
defer renderWindowGlobalMu.Unlock()
if DebugSettings.WindowGeometryTrace {
log.Printf("WindowGeometry: RestoreAll: starting\n")
}
ws.settingStart()
for _, w := range AllRenderWindows {
wgp, sc := ws.get(w.title, "")
if wgp != nil && !w.SystemWindow.Is(system.Fullscreen) {
if DebugSettings.WindowGeometryTrace {
log.Printf("WindowGeometry: RestoreAll: restoring geom for window: %v screen: %s geom: %s\n", w.name, sc.Name, wgp.String())
}
w.SystemWindow.SetGeometry(false, wgp.Pos, wgp.Size, sc)
}
}
ws.settingEnd()
if DebugSettings.WindowGeometryTrace {
log.Printf("WindowGeometry: RestoreAll: done\n")
}
}
// windowGeometries holds the window geometries for a given window
// across different screens, and the last screen used.
type windowGeometries struct {
Last string // Last screen
Screens map[string]windowGeometry // Screen map
}
// getForScreen returns saved geometry for an active (connected) Screen,
// searching in order of: last screen saved, given screen name, and then
// going through the list of available screens in order.
// returns nil if no saved geometry info is available for any active screen.
func (wgs *windowGeometries) getForScreen(screenName string) (*windowGeometry, *system.Screen) {
sc := TheApp.ScreenByName(wgs.Last)
if sc != nil {
wgr := wgs.Screens[wgs.Last]
return &wgr, sc
}
sc = TheApp.ScreenByName(screenName)
if sc != nil {
if wgr, ok := wgs.Screens[screenName]; ok {
return &wgr, sc
}
}
ns := TheApp.NScreens()
for i := range ns {
sc = TheApp.Screen(i)
if wgr, ok := wgs.Screens[sc.Name]; ok {
return &wgr, sc
}
}
return nil, nil
}
// windowGeometry records the geometry settings used for
// a certain screen and window pair.
type windowGeometry struct {
DPI float32
DPR float32 // Device Pixel Ratio
Size image.Point
Pos image.Point
Max bool // Maximized
}
func (wg *windowGeometry) String() string {
return fmt.Sprintf("DPI: %g DPR: %g Size: %v Pos: %v Max: %v", wg.DPI, wg.DPR, wg.Size, wg.Pos, wg.Max)
}
// constrainGeom constrains geometry based on screen params
func (wg *windowGeometry) constrainGeom(sc *system.Screen) {
wg.Pos, wg.Size = sc.ConstrainWindowGeometry(wg.Pos, wg.Size)
}
// 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 core
import (
"fmt"
"reflect"
"slices"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/system"
)
// renderWindowList is a list of [renderWindow]s.
type renderWindowList []*renderWindow
// add adds a window to the list.
func (wl *renderWindowList) add(w *renderWindow) {
renderWindowGlobalMu.Lock()
*wl = append(*wl, w)
renderWindowGlobalMu.Unlock()
}
// delete removes a window from the list.
func (wl *renderWindowList) delete(w *renderWindow) {
renderWindowGlobalMu.Lock()
defer renderWindowGlobalMu.Unlock()
*wl = slices.DeleteFunc(*wl, func(rw *renderWindow) bool {
return rw == w
})
}
// FindName finds the window with the given name on the list (case sensitive).
// It returns the window if found and nil otherwise.
func (wl *renderWindowList) FindName(name string) *renderWindow {
renderWindowGlobalMu.Lock()
defer renderWindowGlobalMu.Unlock()
for _, w := range *wl {
if w.name == name {
return w
}
}
return nil
}
// findData finds window with given Data on list -- returns
// window and true if found, nil, false otherwise.
// data of type string works fine -- does equality comparison on string contents.
func (wl *renderWindowList) findData(data any) (*renderWindow, bool) {
if reflectx.IsNil(reflect.ValueOf(data)) {
return nil, false
}
typ := reflect.TypeOf(data)
if !typ.Comparable() {
fmt.Printf("programmer error in RenderWinList.FindData: Scene.Data type %s not comparable (value: %v)\n", typ.String(), data)
return nil, false
}
renderWindowGlobalMu.Lock()
defer renderWindowGlobalMu.Unlock()
for _, wi := range *wl {
msc := wi.MainScene()
if msc == nil {
continue
}
if msc.Data == data {
return wi, true
}
}
return nil, false
}
// focused returns the (first) window in this list that has the WinGotFocus flag set
// and the index in the list (nil, -1 if not present)
func (wl *renderWindowList) focused() (*renderWindow, int) {
renderWindowGlobalMu.Lock()
defer renderWindowGlobalMu.Unlock()
for i, fw := range *wl {
if fw.gotFocus {
return fw, i
}
}
return nil, -1
}
// focusNext focuses on the next window in the list, after the current Focused() one.
// It skips minimized windows.
func (wl *renderWindowList) focusNext() (*renderWindow, int) {
fw, i := wl.focused()
if fw == nil {
return nil, -1
}
renderWindowGlobalMu.Lock()
defer renderWindowGlobalMu.Unlock()
sz := len(*wl)
if sz == 1 {
return nil, -1
}
for j := 0; j < sz-1; j++ {
if i == sz-1 {
i = 0
} else {
i++
}
fw = (*wl)[i]
if !fw.SystemWindow.Is(system.Minimized) {
fw.SystemWindow.Raise()
break
}
}
return fw, i
}
// AllRenderWindows is the list of all [renderWindow]s that have been created
// (dialogs, main windows, etc).
var AllRenderWindows renderWindowList
// dialogRenderWindows is the list of only dialog [renderWindow]s that
// have been created.
var dialogRenderWindows renderWindowList
// mainRenderWindows is the list of main [renderWindow]s (non-dialogs) that
// have been created.
var mainRenderWindows renderWindowList
// 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 cursorimg provides the cached rendering of SVG cursors to images.
package cursorimg
import (
"bytes"
"fmt"
"image"
"image/draw"
_ "image/png"
"io/fs"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/cursors"
"cogentcore.org/core/enums"
"cogentcore.org/core/paint"
"cogentcore.org/core/svg"
)
// Cursor represents a cached rendered cursor, with the [image.Image]
// of the cursor and its hotspot.
type Cursor struct {
// The cached image of the cursor.
Image image.Image
// The size of the cursor.
Size int
// The hotspot is expressed in terms of raw cursor pixels.
Hotspot image.Point
}
// Cursors contains all of the cached rendered cursors, specified first
// by cursor enum and then by size.
var Cursors = map[enums.Enum]map[int]*Cursor{}
// Get returns the cursor object corresponding to the given cursor enum,
// with the given size. If it is not already cached in [Cursors], it renders and caches it.
//
// It automatically replaces literal colors in svg with appropriate scheme colors as follows:
// - #fff: [colors.Palette].Neutral.ToneUniform(100)
// - #000: [colors.Palette].Neutral.ToneUniform(0)
// - #f00: [colors.Scheme].Error.Base
// - #0f0: [colors.Scheme].Success.Base
// - #ff0: [colors.Scheme].Warn.Base
func Get(cursor enums.Enum, size int) (*Cursor, error) {
sm := Cursors[cursor]
if sm == nil {
sm = map[int]*Cursor{}
Cursors[cursor] = sm
}
if c, ok := sm[size]; ok {
return c, nil
}
name := cursor.String()
hot, ok := cursors.Hotspots[cursor]
if !ok {
hot = image.Pt(128, 128)
}
sv := svg.NewSVG(size, size)
b, err := fs.ReadFile(cursors.Cursors, "svg/"+name+".svg")
if err != nil {
return nil, err
}
b = replaceColors(b)
err = sv.ReadXML(bytes.NewReader(b))
if err != nil {
return nil, fmt.Errorf("error opening SVG file for cursor %q: %w", name, err)
}
sv.Render()
blurRadius := size / 16
bounds := sv.Pixels.Bounds()
// We need to add extra space so that the shadow doesn't get clipped.
bounds.Max = bounds.Max.Add(image.Pt(blurRadius, blurRadius))
shadow := image.NewRGBA(bounds)
draw.DrawMask(shadow, shadow.Bounds(), gradient.ApplyOpacity(colors.Scheme.Shadow, 0.25), image.Point{}, sv.Pixels, image.Point{}, draw.Src)
shadow = paint.GaussianBlur(shadow, float64(blurRadius))
draw.Draw(shadow, shadow.Bounds(), sv.Pixels, image.Point{}, draw.Over)
return &Cursor{
Image: shadow,
Size: size,
Hotspot: hot.Mul(size).Div(256),
}, nil
}
// replaceColors replaces literal cursor colors in the given SVG with scheme colors.
func replaceColors(b []byte) []byte {
m := map[string]image.Image{
"#fff": colors.Palette.Neutral.ToneUniform(100),
"#000": colors.Palette.Neutral.ToneUniform(0),
"#f00": colors.Scheme.Error.Base,
"#0f0": colors.Scheme.Success.Base,
"#ff0": colors.Scheme.Warn.Base,
}
for old, clr := range m {
b = bytes.ReplaceAll(b, []byte(fmt.Sprintf("%q", old)), []byte(fmt.Sprintf("%q", colors.AsHex(colors.ToUniform(clr)))))
}
return b
}
// Code generated by "core generate"; DO NOT EDIT.
package cursors
import (
"cogentcore.org/core/enums"
)
var _CursorValues = []Cursor{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39}
// CursorN is the highest valid value for type Cursor, plus one.
const CursorN Cursor = 40
var _CursorValueMap = map[string]Cursor{`none`: 0, `arrow`: 1, `context-menu`: 2, `help`: 3, `pointer`: 4, `progress`: 5, `wait`: 6, `cell`: 7, `crosshair`: 8, `text`: 9, `vertical-text`: 10, `alias`: 11, `copy`: 12, `move`: 13, `not-allowed`: 14, `grab`: 15, `grabbing`: 16, `resize-col`: 17, `resize-row`: 18, `resize-up`: 19, `resize-right`: 20, `resize-down`: 21, `resize-left`: 22, `resize-n`: 23, `resize-e`: 24, `resize-s`: 25, `resize-w`: 26, `resize-ne`: 27, `resize-nw`: 28, `resize-se`: 29, `resize-sw`: 30, `resize-ew`: 31, `resize-ns`: 32, `resize-nesw`: 33, `resize-nwse`: 34, `zoom-in`: 35, `zoom-out`: 36, `screenshot-selection`: 37, `screenshot-window`: 38, `poof`: 39}
var _CursorDescMap = map[Cursor]string{0: `None indicates no preference for a cursor; will typically be inherited`, 1: `Arrow is a standard arrow cursor, which is the default window cursor`, 2: `ContextMenu indicates that a context menu is available`, 3: `Help indicates that help information is available`, 4: `Pointer is a pointing hand that indicates a link or an interactive element`, 5: `Progress indicates that the app is busy in the background, but can still be interacted with (use [Wait] to indicate that it can't be interacted with)`, 6: `Wait indicates that the app is busy and can not be interacted with (use [Progress] to indicate that it can be interacted with)`, 7: `Cell indicates a table cell, especially one that can be selected`, 8: `Crosshair is a cross cursor that typically indicates precision selection, such as in an image`, 9: `Text is an I-Beam that indicates text that can be selected`, 10: `VerticalText is a sideways I-Beam that indicates vertical text that can be selected`, 11: `Alias indicates that a shortcut or alias will be created`, 12: `Copy indicates that a copy of something will be created`, 13: `Move indicates that something is being moved`, 14: `NotAllowed indicates that something can not be done`, 15: `Grab indicates that something can be grabbed`, 16: `Grabbing indicates that something is actively being grabbed`, 17: `ResizeCol indicates that something can be resized in the horizontal direction`, 18: `ResizeRow indicates that something can be resized in the vertical direction`, 19: `ResizeUp indicates that something can be resized in the upper direction`, 20: `ResizeRight indicates that something can be resized in the right direction`, 21: `ResizeDown indicates that something can be resized in the downward direction`, 22: `ResizeLeft indicates that something can be resized in the left direction`, 23: `ResizeN indicates that something can be resized in the upper direction`, 24: `ResizeE indicates that something can be resized in the right direction`, 25: `ResizeS indicates that something can be resized in the downward direction`, 26: `ResizeW indicates that something can be resized in the left direction`, 27: `ResizeNE indicates that something can be resized in the upper-right direction`, 28: `ResizeNW indicates that something can be resized in the upper-left direction`, 29: `ResizeSE indicates that something can be resized in the lower-right direction`, 30: `ResizeSW indicates that something can be resized in the lower-left direction`, 31: `ResizeEW indicates that something can be resized bidirectionally in the right-left direction`, 32: `ResizeNS indicates that something can be resized bidirectionally in the top-bottom direction`, 33: `ResizeNESW indicates that something can be resized bidirectionally in the top-right to bottom-left direction`, 34: `ResizeNWSE indicates that something can be resized bidirectionally in the top-left to bottom-right direction`, 35: `ZoomIn indicates that something can be zoomed in`, 36: `ZoomOut indicates that something can be zoomed out`, 37: `ScreenshotSelection indicates that a screenshot selection box is being selected`, 38: `ScreenshotWindow indicates that a screenshot is being taken of an entire window`, 39: `Poof indicates that an item will dissapear when it is released`}
var _CursorMap = map[Cursor]string{0: `none`, 1: `arrow`, 2: `context-menu`, 3: `help`, 4: `pointer`, 5: `progress`, 6: `wait`, 7: `cell`, 8: `crosshair`, 9: `text`, 10: `vertical-text`, 11: `alias`, 12: `copy`, 13: `move`, 14: `not-allowed`, 15: `grab`, 16: `grabbing`, 17: `resize-col`, 18: `resize-row`, 19: `resize-up`, 20: `resize-right`, 21: `resize-down`, 22: `resize-left`, 23: `resize-n`, 24: `resize-e`, 25: `resize-s`, 26: `resize-w`, 27: `resize-ne`, 28: `resize-nw`, 29: `resize-se`, 30: `resize-sw`, 31: `resize-ew`, 32: `resize-ns`, 33: `resize-nesw`, 34: `resize-nwse`, 35: `zoom-in`, 36: `zoom-out`, 37: `screenshot-selection`, 38: `screenshot-window`, 39: `poof`}
// String returns the string representation of this Cursor value.
func (i Cursor) String() string { return enums.String(i, _CursorMap) }
// SetString sets the Cursor value from its string representation,
// and returns an error if the string is invalid.
func (i *Cursor) SetString(s string) error { return enums.SetString(i, s, _CursorValueMap, "Cursor") }
// Int64 returns the Cursor value as an int64.
func (i Cursor) Int64() int64 { return int64(i) }
// SetInt64 sets the Cursor value from an int64.
func (i *Cursor) SetInt64(in int64) { *i = Cursor(in) }
// Desc returns the description of the Cursor value.
func (i Cursor) Desc() string { return enums.Desc(i, _CursorDescMap) }
// CursorValues returns all possible values for the type Cursor.
func CursorValues() []Cursor { return _CursorValues }
// Values returns all possible values for the type Cursor.
func (i Cursor) Values() []enums.Enum { return enums.Values(_CursorValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Cursor) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Cursor) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Cursor") }
// Copyright 2024 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.
// Command docs provides documentation of Cogent Core,
// hosted at https://cogentcore.org/core.
package main
import (
"embed"
"io/fs"
"os"
"path/filepath"
"reflect"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/colors"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/htmlcore"
"cogentcore.org/core/icons"
"cogentcore.org/core/math32"
"cogentcore.org/core/pages"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/texteditor"
"cogentcore.org/core/tree"
"cogentcore.org/core/yaegicore"
"cogentcore.org/core/yaegicore/symbols"
)
//go:embed content
var content embed.FS
//go:embed *.svg name.png weld-icon.png
var resources embed.FS
//go:embed image.png
var myImage embed.FS
//go:embed icon.svg
var mySVG embed.FS
//go:embed file.go
var myFile embed.FS
const defaultPlaygroundCode = `package main
func main() {
b := core.NewBody()
core.NewButton(b).SetText("Hello, World!")
b.RunMainWindow()
}`
func main() {
b := core.NewBody("Cogent Core Docs")
pg := pages.NewPage(b).SetContent(content)
htmlcore.WikilinkBaseURL = "cogentcore.org/core"
b.AddTopBar(func(bar *core.Frame) {
tb := core.NewToolbar(bar)
tb.Maker(pg.MakeToolbar)
tb.Maker(func(p *tree.Plan) {
tree.Add(p, func(w *core.Button) {
w.SetText("Playground").SetIcon(icons.PlayCircle)
w.OnClick(func(e events.Event) {
pg.Context.OpenURL("/playground")
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("Videos").SetIcon(icons.VideoLibrary)
w.OnClick(func(e events.Event) {
pg.Context.OpenURL("https://youtube.com/@CogentCore")
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("Blog").SetIcon(icons.RssFeed)
w.OnClick(func(e events.Event) {
pg.Context.OpenURL("https://cogentcore.org/blog")
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("GitHub").SetIcon(icons.GitHub)
w.OnClick(func(e events.Event) {
pg.Context.OpenURL("https://github.com/cogentcore/core")
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("Community").SetIcon(icons.Forum)
w.OnClick(func(e events.Event) {
pg.Context.OpenURL("https://cogentcore.org/community")
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("Sponsor").SetIcon(icons.Favorite)
w.OnClick(func(e events.Event) {
pg.Context.OpenURL("https://github.com/sponsors/cogentcore")
})
})
})
})
symbols.Symbols["."]["content"] = reflect.ValueOf(content)
symbols.Symbols["."]["myImage"] = reflect.ValueOf(myImage)
symbols.Symbols["."]["mySVG"] = reflect.ValueOf(mySVG)
symbols.Symbols["."]["myFile"] = reflect.ValueOf(myFile)
htmlcore.ElementHandlers["home-page"] = homePage
htmlcore.ElementHandlers["core-playground"] = func(ctx *htmlcore.Context) bool {
splits := core.NewSplits(ctx.BlockParent)
ed := texteditor.NewEditor(splits)
playgroundFile := filepath.Join(core.TheApp.AppDataDir(), "playground.go")
err := ed.Buffer.Open(core.Filename(playgroundFile))
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
err := os.WriteFile(playgroundFile, []byte(defaultPlaygroundCode), 0666)
core.ErrorSnackbar(ed, err, "Error creating code file")
if err == nil {
err := ed.Buffer.Open(core.Filename(playgroundFile))
core.ErrorSnackbar(ed, err, "Error loading code")
}
} else {
core.ErrorSnackbar(ed, err, "Error loading code")
}
}
ed.OnChange(func(e events.Event) {
core.ErrorSnackbar(ed, ed.Buffer.Save(), "Error saving code")
})
parent := core.NewFrame(splits)
yaegicore.BindTextEditor(ed, parent)
return true
}
htmlcore.ElementHandlers["style-demo"] = func(ctx *htmlcore.Context) bool {
// same as demo styles tab
sp := core.NewSplits(ctx.BlockParent)
sp.Styler(func(s *styles.Style) {
s.Min.Y.Em(40)
})
fm := core.NewForm(sp)
fr := core.NewFrame(core.NewFrame(sp)) // can not control layout when directly in splits
fr.Styler(func(s *styles.Style) {
s.Background = colors.Scheme.Select.Container
s.Grow.Set(1, 1)
})
fr.Style() // must style immediately to get correct default values
fm.SetStruct(&fr.Styles)
fm.OnChange(func(e events.Event) {
fr.OverrideStyle = true
fr.Update()
})
frameSizes := []math32.Vector2{
{20, 100},
{80, 20},
{60, 80},
{40, 120},
{150, 100},
}
for _, sz := range frameSizes {
core.NewFrame(fr).Styler(func(s *styles.Style) {
s.Min.Set(units.Dp(sz.X), units.Dp(sz.Y))
s.Background = colors.Scheme.Primary.Base
})
}
return true
}
b.RunMainWindow()
}
var home *core.Frame
func makeBlock[T tree.NodeValue](title, text string, graphic func(w *T), url ...string) {
if len(url) > 0 {
title = `<a target="_blank" href="` + url[0] + `">` + title + `</a>`
}
tree.AddChildAt(home, title, func(w *core.Frame) {
w.Styler(func(s *styles.Style) {
s.Gap.Set(units.Em(1))
s.Grow.Set(1, 0)
if home.SizeClass() == core.SizeCompact {
s.Direction = styles.Column
}
})
w.Maker(func(p *tree.Plan) {
graphicFirst := w.IndexInParent()%2 != 0 && w.SizeClass() != core.SizeCompact
if graphicFirst {
tree.Add(p, graphic)
}
tree.Add(p, func(w *core.Frame) {
w.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Text.Align = styles.Start
s.Grow.Set(1, 1)
})
tree.AddChild(w, func(w *core.Text) {
w.SetType(core.TextHeadlineLarge).SetText(title)
w.Styler(func(s *styles.Style) {
s.Font.Weight = styles.WeightBold
s.Color = colors.Scheme.Primary.Base
})
})
tree.AddChild(w, func(w *core.Text) {
w.SetType(core.TextTitleLarge).SetText(text)
})
})
if !graphicFirst {
tree.Add(p, graphic)
}
})
})
}
func homePage(ctx *htmlcore.Context) bool {
home = core.NewFrame(ctx.BlockParent)
home.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Grow.Set(1, 1)
s.CenterAll()
})
home.OnShow(func(e events.Event) {
home.Update() // TODO: temporary workaround for #1037
})
tree.AddChild(home, func(w *core.SVG) {
errors.Log(w.ReadString(core.AppIcon))
})
tree.AddChild(home, func(w *core.Image) {
errors.Log(w.OpenFS(resources, "name.png"))
w.Styler(func(s *styles.Style) {
s.Min.X.SetCustom(func(uc *units.Context) float32 {
return min(uc.Dp(612), uc.Vw(80))
})
})
})
tree.AddChild(home, func(w *core.Text) {
w.SetType(core.TextHeadlineMedium).SetText("A cross-platform framework for building powerful, fast, elegant 2D and 3D apps")
})
tree.AddChild(home, func(w *core.Frame) {
tree.AddChild(w, func(w *core.Button) {
w.SetText("Get started")
w.OnClick(func(e events.Event) {
ctx.OpenURL("/basics")
})
})
tree.AddChild(w, func(w *core.Button) {
w.SetText("Install").SetType(core.ButtonTonal)
w.OnClick(func(e events.Event) {
ctx.OpenURL("/setup/install")
})
})
})
initIcon := func(w *core.Icon) *core.Icon {
w.Styler(func(s *styles.Style) {
s.Min.Set(units.Dp(256))
s.Color = colors.Scheme.Primary.Base
})
return w
}
makeBlock("CODE ONCE, RUN EVERYWHERE (CORE)", "With Cogent Core, you can write your app once and it will instantly run on macOS, Windows, Linux, iOS, Android, and the web, automatically scaling to any screen. Instead of struggling with platform-specific code in a multitude of languages, you can easily write and maintain a single Go codebase.", func(w *core.Icon) {
initIcon(w).SetIcon(icons.Devices)
})
makeBlock("EFFORTLESS ELEGANCE", "Cogent Core is built on Go, a high-level language designed for building elegant, readable, and scalable code with full type safety and a robust design that never gets in your way. Cogent Core makes it easy to get started with cross-platform app development in just two commands and seven lines of simple code.", func(w *texteditor.Editor) {
w.Buffer.SetLanguage(fileinfo.Go).SetString(`b := core.NewBody()
core.NewButton(b).SetText("Hello, World!")
b.RunMainWindow()`)
w.SetReadOnly(true)
w.Buffer.Options.LineNumbers = false
w.Styler(func(s *styles.Style) {
if w.SizeClass() != core.SizeCompact {
s.Min.X.Em(20)
}
})
})
makeBlock("COMPLETELY CUSTOMIZABLE", "Cogent Core allows developers and users to fully customize apps to fit their unique needs and preferences through a robust styling system and a powerful color system that allow developers and users to instantly customize every aspect of the appearance and behavior of an app.", func(w *core.Form) {
w.SetStruct(core.AppearanceSettings)
w.OnChange(func(e events.Event) {
core.UpdateSettings(w, core.AppearanceSettings)
})
})
makeBlock("POWERFUL FEATURES", "Cogent Core comes with a powerful set of advanced features that allow you to make almost anything, including fully featured text editors, video and audio players, interactive 3D graphics, customizable data plots, Markdown and HTML rendering, SVG and canvas vector graphics, and automatic views of any Go data structure for instant data binding and advanced app inspection.", func(w *core.Icon) {
initIcon(w).SetIcon(icons.ScatterPlot)
})
makeBlock("OPTIMIZED EXPERIENCE", "Every part of your development experience is guided by a comprehensive set of editable interactive example-based documentation, in-depth video tutorials, easy-to-use command line tools specialized for Cogent Core, and active support and development from the Cogent Core developers.", func(w *core.Icon) {
initIcon(w).SetIcon(icons.PlayCircle)
})
makeBlock("EXTREMELY FAST", "Cogent Core is powered by WebGPU, a modern, cross-platform, high-performance graphics framework that allows apps to run on all platforms at extremely fast speeds. All Cogent Core apps compile to machine code, allowing them to run without any overhead.", func(w *core.Icon) {
initIcon(w).SetIcon(icons.Bolt)
})
makeBlock("FREE AND OPEN SOURCE", "Cogent Core is completely free and open source under the permissive BSD-3 License, allowing you to use Cogent Core for any purpose, commercially or personally. We believe that software works best when everyone can use it.", func(w *core.Icon) {
initIcon(w).SetIcon(icons.Code)
})
makeBlock("USED AROUND THE WORLD", "Over six years of development, Cogent Core has been used and thoroughly tested by developers and scientists around the world for a wide variety of use cases. Cogent Core is an advanced framework actively used to power everything from end-user apps to scientific research.", func(w *core.Icon) {
initIcon(w).SetIcon(icons.GlobeAsia)
})
tree.AddChild(home, func(w *core.Text) {
w.SetType(core.TextDisplaySmall).SetText("<b>What can Cogent Core do?</b>")
})
makeBlock("COGENT CODE", "Cogent Code is a fully featured Go IDE with support for syntax highlighting, code completion, symbol lookup, building and debugging, version control, keyboard shortcuts, and many other features.", func(w *core.SVG) {
errors.Log(w.OpenFS(resources, "code-icon.svg"))
}, "https://cogentcore.org/cogent/code")
makeBlock("COGENT CANVAS", "Cogent Canvas is a powerful vector graphics editor with complete support for shapes, paths, curves, text, images, gradients, groups, alignment, styling, importing, exporting, undo, redo, and various other features.", func(w *core.SVG) {
errors.Log(w.OpenFS(resources, "canvas-icon.svg"))
}, "https://cogentcore.org/cogent/canvas")
makeBlock("COGENT NUMBERS", "Cogent Numbers is a highly extensible math, data science, and statistics platform that combines the power of programming with the convenience of spreadsheets and graphing calculators.", func(w *core.SVG) {
errors.Log(w.OpenFS(resources, "numbers-icon.svg"))
}, "https://github.com/cogentcore/cogent/tree/main/numbers")
makeBlock("COGENT MAIL", "Cogent Mail is a customizable email client with built-in Markdown support, automatic mail filtering, and an extensive set of keyboard shortcuts for advanced mail filing.", func(w *core.SVG) {
errors.Log(w.OpenFS(resources, "mail-icon.svg"))
}, "https://github.com/cogentcore/cogent/tree/main/mail")
makeBlock("COGENT CRAFT", "Cogent Craft is a powerful 3D modeling app with support for creating, loading, and editing 3D object files using an interactive WYSIWYG editor.", func(w *core.SVG) {
errors.Log(w.OpenFS(resources, "craft-icon.svg"))
}, "https://github.com/cogentcore/cogent/tree/main/craft")
makeBlock("EMERGENT", "Emergent is a collection of biologically based 3D neural network models of the brain that power ongoing research in computational cognitive neuroscience.", func(w *core.SVG) {
errors.Log(w.OpenFS(resources, "emergent-icon.svg"))
}, "https://emersim.org")
// makeBlock("WELD", "WELD is a set of 3D computational models of a new approach to quantum physics based on the de Broglie-Bohm pilot wave theory.", func(w *core.Image) {
// errors.Log(w.OpenFS(resources, "weld-icon.png"))
// w.Styler(func(s *styles.Style) {
// s.Min.Set(units.Dp(256))
// })
// }, "https://github.com/WaveELD/WELDBook/blob/main/textmd/ch01_intro.md")
tree.AddChild(home, func(w *core.Text) {
w.SetType(core.TextDisplaySmall).SetText("<b>Why Cogent Core instead of something else?</b>")
})
makeBlock("THE PROBLEM", "After using other frameworks built on HTML and Qt for years to make apps ranging from simple tools to complex scientific models, we realized that we were spending more time dealing with excessive boilerplate, browser inconsistencies, and dependency management issues than actual app development.", func(w *core.Icon) {
initIcon(w).SetIcon(icons.Problem)
})
makeBlock("THE SOLUTION", "We decided to make a framework that would allow us to focus on app content and logic by providing a consistent and elegant API that automatically handles cross-platform support, user customization, and app packaging and deployment.", func(w *core.Icon) {
initIcon(w).SetIcon(icons.Lightbulb)
})
makeBlock("THE RESULT", "Instead of constantly jumping through hoops to create a consistent, easy-to-use, cross-platform app, you can now take advantage of a powerful featureset on all platforms and vastly simplify your development experience.", func(w *core.Icon) {
initIcon(w).SetIcon(icons.Check)
})
tree.AddChild(home, func(w *core.Button) {
w.SetText("Get started")
w.OnClick(func(e events.Event) {
ctx.OpenURL("basics")
})
})
return true
}
// Copyright 2024 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 main
import "fmt"
func exampleFile() {
fmt.Println("This is an example file.")
}
// 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 main provides the actual command line
// implementation of the enumgen library.
package main
import (
"cogentcore.org/core/cli"
"cogentcore.org/core/enums/enumgen"
)
func main() {
opts := cli.DefaultOptions("Enumgen", "Enumgen generates helpful methods for Go enums.")
opts.DefaultFiles = []string{"enumgen.toml"}
cli.Run(opts, &enumgen.Config{}, enumgen.Generate)
}
// 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.
// Based on http://github.com/dmarkham/enumer and
// golang.org/x/tools/cmd/stringer:
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package enumgen
import "text/template"
// BuildBitFlagMethods builds methods specific to bit flag types.
func (g *Generator) BuildBitFlagMethods(runs []Value, typ *Type) {
g.Printf("\n")
g.ExecTmpl(HasFlagMethodTmpl, typ)
g.ExecTmpl(SetFlagMethodTmpl, typ)
}
var HasFlagMethodTmpl = template.Must(template.New("HasFlagMethod").Parse(
`// HasFlag returns whether these bit flags have the given bit flag set.
func (i *{{.Name}}) HasFlag(f enums.BitFlag) bool { return enums.HasFlag((*int64)(i), f) }
`))
var SetFlagMethodTmpl = template.Must(template.New("SetFlagMethod").Parse(
`// SetFlag sets the value of the given flags in these flags to the given value.
func (i *{{.Name}}) SetFlag(on bool, f ...enums.BitFlag) { enums.SetFlag((*int64)(i), on, f...) }
`))
var StringMethodBitFlagTmpl = template.Must(template.New("StringMethodBitFlag").Parse(
`// String returns the string representation of this {{.Name}} value.
func (i {{.Name}}) String() string {
{{- if eq .Extends ""}} return enums.BitFlagString(i, _{{.Name}}Values)
{{- else}} return enums.BitFlagStringExtended(i, _{{.Name}}Values, {{.Extends}}Values()) {{end}} }
`))
var SetStringMethodBitFlagTmpl = template.Must(template.New("SetStringMethodBitFlag").Parse(
`// SetString sets the {{.Name}} value from its string representation,
// and returns an error if the string is invalid.
func (i *{{.Name}}) SetString(s string) error { *i = 0; return i.SetStringOr(s) }
`))
var SetStringOrMethodBitFlagTmpl = template.Must(template.New("SetStringOrMethodBitFlag").Parse(
`// SetStringOr sets the {{.Name}} value from its string representation
// while preserving any bit flags already set, and returns an
// error if the string is invalid.
func (i *{{.Name}}) SetStringOr(s string) error {
{{- if eq .Extends ""}} return enums.SetStringOr{{if .Config.AcceptLower}}Lower{{end}}(i, s, _{{.Name}}ValueMap, "{{.Name}}")
{{- else}} return enums.SetStringOr{{if .Config.AcceptLower}}Lower{{end}}Extended(i, (*{{.Extends}})(i), s, _{{.Name}}ValueMap) {{end}} }
`))
// 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 enumgen provides functions for generating
// enum methods for enum types.
package enumgen
//go:generate core generate
import (
"fmt"
"cogentcore.org/core/base/generate"
"golang.org/x/tools/go/packages"
)
// ParsePackages parses the package(s) located in the configuration source directory.
func ParsePackages(cfg *Config) ([]*packages.Package, error) {
pcfg := &packages.Config{
Mode: PackageModes(),
// TODO: Need to think about constants in test files. Maybe write enumgen_test.go
// in a separate pass? For later.
Tests: false,
}
pkgs, err := generate.Load(pcfg, cfg.Dir)
if err != nil {
return nil, fmt.Errorf("enumgen: Generate: error parsing package: %w", err)
}
return pkgs, err
}
// Generate generates enum methods, using the
// configuration information, loading the packages from the
// configuration source directory, and writing the result
// to the configuration output file.
//
// It is a simple entry point to enumgen that does all
// of the steps; for more specific functionality, create
// a new [Generator] with [NewGenerator] and call methods on it.
//
//cli:cmd -root
func Generate(cfg *Config) error { //types:add
pkgs, err := ParsePackages(cfg)
if err != nil {
return err
}
return GeneratePkgs(cfg, pkgs)
}
// GeneratePkgs generates enum methods using
// the given configuration object and packages parsed
// from the configuration source directory,
// and writes the result to the config output file.
// It is a simple entry point to enumgen that does all
// of the steps; for more specific functionality, create
// a new [Generator] with [NewGenerator] and call methods on it.
func GeneratePkgs(cfg *Config, pkgs []*packages.Package) error {
g := NewGenerator(cfg, pkgs)
for _, pkg := range g.Pkgs {
g.Pkg = pkg
g.Buf.Reset()
err := g.FindEnumTypes()
if err != nil {
return fmt.Errorf("enumgen: Generate: error finding enum types for package %q: %w", pkg.Name, err)
}
g.PrintHeader()
has, err := g.Generate()
if !has {
continue
}
if err != nil {
return fmt.Errorf("enumgen: Generate: error generating code for package %q: %w", pkg.Name, err)
}
err = g.Write()
if err != nil {
return fmt.Errorf("enumgen: Generate: error writing code for package %q: %w", pkg.Name, err)
}
}
return nil
}
// 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.
// Based on http://github.com/dmarkham/enumer and
// golang.org/x/tools/cmd/stringer:
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package enumgen
import (
"bytes"
"errors"
"fmt"
"go/ast"
"go/constant"
"go/token"
"go/types"
"html"
"log/slog"
"os"
"strings"
"text/template"
"cogentcore.org/core/base/generate"
"cogentcore.org/core/cli"
"golang.org/x/tools/go/packages"
)
// Generator holds the state of the generator.
// It is primarily used to buffer the output.
type Generator struct {
Config *Config // The configuration information
Buf bytes.Buffer // The accumulated output.
Pkgs []*packages.Package // The packages we are scanning.
Pkg *packages.Package // The packages we are currently on.
Types []*Type // The enum types
}
// NewGenerator returns a new generator with the
// given configuration information and parsed packages.
func NewGenerator(config *Config, pkgs []*packages.Package) *Generator {
return &Generator{Config: config, Pkgs: pkgs}
}
// PackageModes returns the package load modes needed for this generator
func PackageModes() packages.LoadMode {
return packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo
}
// Printf prints the formatted string to the
// accumulated output in [Generator.Buf]
func (g *Generator) Printf(format string, args ...any) {
fmt.Fprintf(&g.Buf, format, args...)
}
// PrintHeader prints the header and package clause
// to the accumulated output
func (g *Generator) PrintHeader() {
// we need a manual import of enums because it is
// external, but goimports will handle everything else
generate.PrintHeader(&g.Buf, g.Pkg.Name, "cogentcore.org/core/enums")
}
// FindEnumTypes goes through all of the types in the package
// and finds all integer (signed or unsigned) types labeled with enums:enum
// or enums:bitflag. It stores the resulting types in [Generator.Types].
func (g *Generator) FindEnumTypes() error {
g.Types = []*Type{}
return generate.Inspect(g.Pkg, g.InspectForType)
}
// AllowedEnumTypes are the types that can be used for enums
// that are not bit flags (bit flags can only be int64s).
// It is stored as a map for quick and convenient access.
var AllowedEnumTypes = map[string]bool{"int": true, "int64": true, "int32": true, "int16": true, "int8": true, "uint": true, "uint64": true, "uint32": true, "uint16": true, "uint8": true}
// InspectForType looks at the given AST node and adds it
// to [Generator.Types] if it is marked with an appropriate
// comment directive. It returns whether the AST inspector should
// continue, and an error if there is one. It should only
// be called in [ast.Inspect].
func (g *Generator) InspectForType(n ast.Node) (bool, error) {
ts, ok := n.(*ast.TypeSpec)
if !ok {
return true, nil
}
if ts.Comment == nil {
return true, nil
}
for _, c := range ts.Comment.List {
dir, err := cli.ParseDirective(c.Text)
if err != nil {
return false, fmt.Errorf("error parsing comment directive %q: %w", c.Text, err)
}
if dir == nil {
continue
}
if dir.Tool != "enums" {
continue
}
if dir.Directive != "enum" && dir.Directive != "bitflag" {
return false, fmt.Errorf("unrecognized enums directive %q (from %q)", dir.Directive, c.Text)
}
typnm := types.ExprString(ts.Type)
// ident, ok := ts.Type.(*ast.Ident)
// if !ok {
// return false, fmt.Errorf("type of enum type (%v) is %T, not *ast.Ident (try using a standard [un]signed integer type instead)", ts.Type, ts.Type)
// }
cfg := &Config{}
*cfg = *g.Config
leftovers, err := cli.SetFromArgs(cfg, dir.Args, cli.ErrNotFound)
if err != nil {
return false, fmt.Errorf("error setting config info from comment directive args: %w (from directive %q)", err, c.Text)
}
if len(leftovers) > 0 {
return false, fmt.Errorf("expected 0 positional arguments but got %d (list: %v) (from directive %q)", len(leftovers), leftovers, c.Text)
}
typ := g.Pkg.TypesInfo.Defs[ts.Name].Type()
utyp := typ.Underlying()
tt := &Type{Name: ts.Name.Name, Type: ts, Config: cfg}
// if our direct type isn't the same as our underlying type, we are extending our direct type
if cfg.Extend && typnm != utyp.String() {
tt.Extends = typnm
}
switch dir.Directive {
case "enum":
if !AllowedEnumTypes[utyp.String()] {
return false, fmt.Errorf("enum type %s is not allowed; try using a standard [un]signed integer type instead", typnm)
}
tt.IsBitFlag = false
case "bitflag":
if utyp.String() != "int64" {
return false, fmt.Errorf("bit flag enum type %s is not allowed; bit flag enums must be of type int64", typnm)
}
tt.IsBitFlag = true
}
g.Types = append(g.Types, tt)
}
return true, nil
}
// Generate produces the enum methods for the types
// stored in [Generator.Types] and stores them in
// [Generator.Buf]. It returns whether there were
// any enum types to generate methods for, and
// any error that occurred.
func (g *Generator) Generate() (bool, error) {
if len(g.Types) == 0 {
return false, nil
}
for _, typ := range g.Types {
values := make([]Value, 0, 100)
for _, file := range g.Pkg.Syntax {
if ast.IsGenerated(file) {
continue
}
var terr error
ast.Inspect(file, func(n ast.Node) bool {
if terr != nil {
return false
}
vals, cont, err := g.GenDecl(n, file, typ)
if err != nil {
terr = err
} else {
values = append(values, vals...)
}
return cont
})
if terr != nil {
return true, fmt.Errorf("Generate: error parsing declaration clauses: %w", terr)
}
}
if len(values) == 0 {
return true, errors.New("no values defined for type " + typ.Name)
}
g.TrimValueNames(values, typ.Config)
err := g.TransformValueNames(values, typ.Config)
if err != nil {
return true, fmt.Errorf("error transforming value names: %w", err)
}
g.PrefixValueNames(values, typ.Config)
values = SortValues(values)
g.BuildBasicMethods(values, typ)
if typ.IsBitFlag {
g.BuildBitFlagMethods(values, typ)
}
if typ.Config.Text {
g.BuildTextMethods(values, typ)
}
if typ.Config.SQL {
g.AddValueAndScanMethod(typ)
}
if typ.Config.GQL {
g.BuildGQLMethods(values, typ)
}
}
return true, nil
}
// GenDecl processes one declaration clause.
// It returns whether the AST inspector should continue,
// and an error if there is one. It should only be
// called in [ast.Inspect].
func (g *Generator) GenDecl(node ast.Node, file *ast.File, typ *Type) ([]Value, bool, error) {
decl, ok := node.(*ast.GenDecl)
if !ok || decl.Tok != token.CONST {
// We only care about const declarations.
return nil, true, nil
}
vals := []Value{}
// The name of the type of the constants we are declaring.
// Can change if this is a multi-element declaration.
typName := ""
// Loop over the elements of the declaration. Each element is a ValueSpec:
// a list of names possibly followed by a type, possibly followed by values.
// If the type and value are both missing, we carry down the type (and value,
// but the "go/types" package takes care of that).
for _, spec := range decl.Specs {
vspec := spec.(*ast.ValueSpec) // Guaranteed to succeed as this is CONST.
if vspec.Type == nil && len(vspec.Values) > 0 {
// "X = 1". With no type but a value, the constant is untyped.
// Skip this vspec and reset the remembered type.
typName = ""
continue
}
if vspec.Type != nil {
// "X T". We have a type. Remember it.
ident, ok := vspec.Type.(*ast.Ident)
if !ok {
continue
}
typName = ident.Name
}
if typName != typ.Name {
// This is not the type we're looking for.
continue
}
// We now have a list of names (from one line of source code) all being
// declared with the desired type.
// Grab their names and actual values and store them in f.values.
for _, n := range vspec.Names {
if n.Name == "_" {
continue
}
// This dance lets the type checker find the values for us. It's a
// bit tricky: look up the object declared by the n, find its
// types.Const, and extract its value.
obj, ok := g.Pkg.TypesInfo.Defs[n]
if !ok {
return nil, false, errors.New("no value for constant " + n.String())
}
info := obj.Type().Underlying().(*types.Basic).Info()
if info&types.IsInteger == 0 {
return nil, false, errors.New("can't handle non-integer constant type " + typName)
}
value := obj.(*types.Const).Val() // Guaranteed to succeed as this is CONST.
if value.Kind() != constant.Int {
return nil, false, errors.New("can't happen: constant is not an integer " + n.String())
}
i64, isInt := constant.Int64Val(value)
u64, isUint := constant.Uint64Val(value)
if !isInt && !isUint {
return nil, false, errors.New("internal error: value of " + n.String() + " is not an integer: " + value.String())
}
if !isUint {
i64 = int64(u64)
}
v := Value{
OriginalName: n.Name,
Name: n.Name,
Desc: html.EscapeString(strings.Join(strings.Fields(vspec.Doc.Text()), " ")), // need to collapse whitespace and escape
Value: i64,
Signed: info&types.IsUnsigned == 0,
Str: value.String(),
}
if c := vspec.Comment; typ.Config.LineComment && c != nil && len(c.List) == 1 {
v.Name = strings.TrimSpace(c.Text())
}
vals = append(vals, v)
}
}
return vals, false, nil
}
// ExecTmpl executes the given template with the given type and
// writes the result to [Generator.Buf]. It fatally logs any error.
// All enumgen templates take a [Type] as their data.
func (g *Generator) ExecTmpl(t *template.Template, typ *Type) {
err := t.Execute(&g.Buf, typ)
if err != nil {
slog.Error("programmer error: internal error: error executing template", "err", err)
os.Exit(1)
}
}
// Write formats the data in the the Generator's buffer
// ([Generator.Buf]) and writes it to the file specified by
// [Generator.Config.Output].
func (g *Generator) Write() error {
return generate.Write(generate.Filepath(g.Pkg, g.Config.Output), g.Buf.Bytes(), nil)
}
// 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.
// Based on http://github.com/dmarkham/enumer and
// golang.org/x/tools/cmd/stringer:
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package enumgen
import "text/template"
var GQLMethodsTmpl = template.Must(template.New("GQLMethods").Parse(`
// MarshalGQL implements the [graphql.Marshaler] interface.
func (i {{.Name}}) MarshalGQL(w io.Writer) { w.Write([]byte(strconv.Quote(i.String()))) }
// UnmarshalGQL implements the [graphql.Unmarshaler] interface.
func (i *{{.Name}}) UnmarshalGQL(value any) error { return enums.Scan(i, value, "{{.Name}}") }
`))
func (g *Generator) BuildGQLMethods(runs []Value, typ *Type) {
g.ExecTmpl(GQLMethodsTmpl, typ)
}
// 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.
// Based on http://github.com/dmarkham/enumer and
// golang.org/x/tools/cmd/stringer:
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package enumgen
import "text/template"
var TextMethodsTmpl = template.Must(template.New("TextMethods").Parse(
`
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i {{.Name}}) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *{{.Name}}) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "{{.Name}}") }
`))
func (g *Generator) BuildTextMethods(runs []Value, typ *Type) {
g.ExecTmpl(TextMethodsTmpl, typ)
}
// 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.
// Based on http://github.com/dmarkham/enumer and
// golang.org/x/tools/cmd/stringer:
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package enumgen
import (
"strings"
"text/template"
)
// BuildString builds the string function using a map access approach.
func (g *Generator) BuildString(values []Value, typ *Type) {
g.Printf("\n")
g.Printf("\nvar _%sMap = map[%s]string{", typ.Name, typ.Name)
n := 0
for _, value := range values {
g.Printf("%s: `%s`,", &value, value.Name)
n += len(value.Name)
}
g.Printf("}\n\n")
if typ.IsBitFlag {
g.ExecTmpl(StringMethodBitFlagTmpl, typ)
}
g.ExecTmpl(StringMethodMapTmpl, typ)
}
var StringMethodMapTmpl = template.Must(template.New("StringMethodMap").Parse(
`{{if .IsBitFlag}}
// BitIndexString returns the string representation of this {{.Name}} value
// if it is a bit index value (typically an enum constant), and
// not an actual bit flag value.
{{- else}}
// String returns the string representation of this {{.Name}} value.
{{- end}}
func (i {{.Name}}) {{if .IsBitFlag}} BitIndexString {{else}} String {{end}} () string { return enums.
{{- if eq .Extends ""}}String {{else -}} {{if .IsBitFlag -}} BitIndexStringExtended {{else -}} StringExtended {{- end}}[{{.Name}}, {{.Extends}}]{{- end}}(i, _{{.Name}}Map) }
`))
var NConstantTmpl = template.Must(template.New("StringNConstant").Parse(
`//{{.Name}}N is the highest valid value for type {{.Name}}, plus one.
const {{.Name}}N {{.Name}} = {{.MaxValueP1}}
`))
var SetStringMethodTmpl = template.Must(template.New("SetStringMethod").Parse(
`// SetString sets the {{.Name}} value from its string representation,
// and returns an error if the string is invalid.
func (i *{{.Name}}) SetString(s string) error {
{{- if eq .Extends ""}} return enums.SetString{{if .Config.AcceptLower}}Lower{{end}}(i, s, _{{.Name}}ValueMap, "{{.Name}}")
{{- else}} return enums.SetString{{if .Config.AcceptLower}}Lower{{end}}Extended(i, (*{{.Extends}})(i), s, _{{.Name}}ValueMap) {{end}} }
`))
var Int64MethodTmpl = template.Must(template.New("Int64Method").Parse(
`// Int64 returns the {{.Name}} value as an int64.
func (i {{.Name}}) Int64() int64 { return int64(i) }
`))
var SetInt64MethodTmpl = template.Must(template.New("SetInt64Method").Parse(
`// SetInt64 sets the {{.Name}} value from an int64.
func (i *{{.Name}}) SetInt64(in int64) { *i = {{.Name}}(in) }
`))
var DescMethodTmpl = template.Must(template.New("DescMethod").Parse(`// Desc returns the description of the {{.Name}} value.
func (i {{.Name}}) Desc() string {
{{- if eq .Extends ""}} return enums.Desc(i, _{{.Name}}DescMap)
{{- else}} return enums.DescExtended[{{.Name}}, {{.Extends}}](i, _{{.Name}}DescMap) {{end}} }
`))
var ValuesGlobalTmpl = template.Must(template.New("ValuesGlobal").Parse(
`// {{.Name}}Values returns all possible values for the type {{.Name}}.
func {{.Name}}Values() []{{.Name}} {
{{- if eq .Extends ""}} return _{{.Name}}Values
{{- else}} return enums.ValuesGlobalExtended(_{{.Name}}Values, {{.Extends}}Values())
{{- end}} }
`))
var ValuesMethodTmpl = template.Must(template.New("ValuesMethod").Parse(
`// Values returns all possible values for the type {{.Name}}.
func (i {{.Name}}) Values() []enums.Enum {
{{- if eq .Extends ""}} return enums.Values(_{{.Name}}Values)
{{- else}} return enums.ValuesExtended(_{{.Name}}Values, {{.Extends}}Values())
{{- end}} }
`))
var IsValidMethodMapTmpl = template.Must(template.New("IsValidMethodMap").Parse(
`// IsValid returns whether the value is a valid option for type {{.Name}}.
func (i {{.Name}}) IsValid() bool { _, ok := _{{.Name}}Map[i]; return ok
{{- if ne .Extends ""}} || {{.Extends}}(i).IsValid() {{end}} }
`))
// BuildBasicMethods builds methods common to all types, like Desc and SetString.
func (g *Generator) BuildBasicMethods(values []Value, typ *Type) {
// Print the slice of values
max := int64(0)
g.Printf("\nvar _%sValues = []%s{", typ.Name, typ.Name)
for _, value := range values {
g.Printf("%s, ", &value)
if value.Value > max {
max = value.Value
}
}
g.Printf("}\n\n")
typ.MaxValueP1 = max + 1
g.ExecTmpl(NConstantTmpl, typ)
// Print the map between name and value
g.PrintValueMap(values, typ)
// Print the map of values to descriptions
g.PrintDescMap(values, typ)
g.BuildString(values, typ)
// Print the basic extra methods
if typ.IsBitFlag {
g.ExecTmpl(SetStringMethodBitFlagTmpl, typ)
g.ExecTmpl(SetStringOrMethodBitFlagTmpl, typ)
} else {
g.ExecTmpl(SetStringMethodTmpl, typ)
}
g.ExecTmpl(Int64MethodTmpl, typ)
g.ExecTmpl(SetInt64MethodTmpl, typ)
g.ExecTmpl(DescMethodTmpl, typ)
g.ExecTmpl(ValuesGlobalTmpl, typ)
g.ExecTmpl(ValuesMethodTmpl, typ)
if typ.Config.IsValid {
g.ExecTmpl(IsValidMethodMapTmpl, typ)
}
}
// PrintValueMap prints the map between name and value
func (g *Generator) PrintValueMap(values []Value, typ *Type) {
g.Printf("\nvar _%sValueMap = map[string]%s{", typ.Name, typ.Name)
for _, value := range values {
g.Printf("`%s`: %s,", value.Name, &value)
if typ.Config.AcceptLower {
l := strings.ToLower(value.Name)
if l != value.Name { // avoid duplicate keys
g.Printf("`%s`: %s,", l, &value)
}
}
}
g.Printf("}\n\n")
}
// PrintDescMap prints the map of values to descriptions
func (g *Generator) PrintDescMap(values []Value, typ *Type) {
g.Printf("\n")
g.Printf("\nvar _%sDescMap = map[%s]string{", typ.Name, typ.Name)
i := 0
for _, value := range values {
g.Printf("%s: `%s`,", &value, value.Desc)
i++
}
g.Printf("}\n\n")
}
// 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.
// Based on http://github.com/dmarkham/enumer and
// golang.org/x/tools/cmd/stringer:
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package enumgen
import "text/template"
var ValueMethodTmpl = template.Must(template.New("ValueMethod").Parse(
`// Value implements the [driver.Valuer] interface.
func (i {{.Name}}) Value() (driver.Value, error) { return i.String(), nil }
`))
var ScanMethodTmpl = template.Must(template.New("ScanMethod").Parse(
`// Scan implements the [sql.Scanner] interface.
func (i *{{.Name}}) Scan(value any) error { return enums.Scan(i, value, "{{.Name}}") }
`))
func (g *Generator) AddValueAndScanMethod(typ *Type) {
g.Printf("\n")
g.ExecTmpl(ValueMethodTmpl, typ)
g.Printf("\n\n")
g.ExecTmpl(ScanMethodTmpl, typ)
}
// 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.
// Based on http://github.com/dmarkham/enumer and
// golang.org/x/tools/cmd/stringer:
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package enumgen
import (
"fmt"
"go/ast"
"sort"
"strings"
"unicode/utf8"
"cogentcore.org/core/base/strcase"
)
// Type represents a parsed enum type.
type Type struct {
Name string // The name of the type
Type *ast.TypeSpec // The standard AST type value
IsBitFlag bool // Whether the type is a bit flag type
Extends string // The type that this type extends, if any ("" if it doesn't extend)
MaxValueP1 int64 // the highest defined value for the type, plus one
Config *Config // Configuration information set in the comment directive for the type; is initialized to generator config info first
}
// Value represents a declared constant.
type Value struct {
OriginalName string // The name of the constant before transformation
Name string // The name of the constant after transformation (i.e. camel case => snake case)
Desc string // The comment description of the constant
// The Value is stored as a bit pattern alone. The boolean tells us
// whether to interpret it as an int64 or a uint64; the only place
// this matters is when sorting.
// Much of the time the str field is all we need; it is printed
// by Value.String.
Value int64
Signed bool // Whether the constant is a signed type.
Str string // The string representation given by the "go/constant" package.
}
func (v *Value) String() string {
return v.Str
}
// SortValues sorts the values and ensures there
// are no duplicates. The input slice is known
// to be non-empty.
func SortValues(values []Value) []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,
// so use that one. The String method won't care about which named constant
// was the argument, so the first name for the given value is the only one to keep.
// We need to do this because identical values would cause the switch or map
// to fail to compile.
j := 1
for i := 1; i < len(values); i++ {
if values[i].Value != values[i-1].Value {
values[j] = values[i]
j++
}
}
return values[:j]
}
// TrimValueNames removes the prefixes specified
// in [Config.TrimPrefix] from each name
// of the given values.
func (g *Generator) TrimValueNames(values []Value, c *Config) {
for _, prefix := range strings.Split(c.TrimPrefix, ",") {
for i := range values {
values[i].Name = strings.TrimPrefix(values[i].Name, prefix)
}
}
}
// PrefixValueNames adds the prefix specified in
// [Config.AddPrefix] to each name of
// the given values.
func (g *Generator) PrefixValueNames(values []Value, c *Config) {
for i := range values {
values[i].Name = c.AddPrefix + values[i].Name
}
}
// TransformValueNames transforms the names of the given values according
// to the transform method specified in [Config.Transform]
func (g *Generator) TransformValueNames(values []Value, c *Config) error {
var fn func(src string) string
switch c.Transform {
case "upper":
fn = strings.ToUpper
case "lower":
fn = strings.ToLower
case "snake":
fn = strcase.ToSnake
case "SNAKE":
fn = strcase.ToSNAKE
case "kebab":
fn = strcase.ToKebab
case "KEBAB":
fn = strcase.ToKEBAB
case "camel":
fn = strcase.ToCamel
case "lower-camel":
fn = strcase.ToLowerCamel
case "title":
fn = strcase.ToTitle
case "sentence":
fn = strcase.ToSentence
case "first":
fn = func(s string) string {
r, _ := utf8.DecodeRuneInString(s)
return string(r)
}
case "first-upper":
fn = func(s string) string {
r, _ := utf8.DecodeRuneInString(s)
return strings.ToUpper(string(r))
}
case "first-lower":
fn = func(s string) string {
r, _ := utf8.DecodeRuneInString(s)
return strings.ToLower(string(r))
}
case "":
return nil
default:
return fmt.Errorf("unknown transformation method: %q", c.Transform)
}
for i, v := range values {
after := fn(v.Name)
// If the original one was "" or the one before the transformation
// was "" (most commonly if linecomment defines it as empty) we
// do not care if it's empty.
// But if any of them was not empty before then it means that
// the transformed emptied the value
if v.OriginalName != "" && v.Name != "" && after == "" {
return fmt.Errorf("transformation of %q (%s) got an empty result", v.Name, v.OriginalName)
}
values[i].Name = after
}
return nil
}
// ByValue is a sorting method that sorts the constants into increasing order.
// We take care in the Less method to sort in signed or unsigned order,
// as appropriate.
type ByValue []Value
func (b ByValue) Len() int { return len(b) }
func (b ByValue) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
func (b ByValue) Less(i, j int) bool {
if b[i].Signed {
return int64(b[i].Value) < int64(b[j].Value)
}
return b[i].Value < b[j].Value
}
// Copyright (c) 2024, 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 enums
import (
"errors"
"fmt"
"log/slog"
"strconv"
"strings"
"sync/atomic"
"cogentcore.org/core/base/num"
)
// This file contains implementations of enumgen methods.
// EnumConstraint is the generic type constraint that all enums satisfy.
type EnumConstraint interface {
Enum
num.Integer
}
// BitFlagConstraint is the generic type constraint that all bit flags satisfy.
type BitFlagConstraint interface {
BitFlag
num.Integer
}
// String returns the string representation of the given
// enum value with the given map.
func String[T EnumConstraint](i T, m map[T]string) string {
if str, ok := m[i]; ok {
return str
}
return strconv.FormatInt(int64(i), 10)
}
// StringExtended returns the string representation of the given enum value
// with the given map, with the enum type extending the given other enum type.
func StringExtended[T, E EnumConstraint](i T, m map[T]string) string {
if str, ok := m[i]; ok {
return str
}
return E(i).String()
}
// BitIndexStringExtended returns the string representation of the given bit flag enum
// bit index value with the given map, with the bit flag type extending the given other
// bit flag type.
func BitIndexStringExtended[T, E BitFlagConstraint](i T, m map[T]string) string {
if str, ok := m[i]; ok {
return str
}
return E(i).BitIndexString()
}
// BitFlagString returns the string representation of the given bit flag value
// with the given values available.
func BitFlagString[T BitFlagConstraint](i T, values []T) string {
str := ""
ip := any(&i).(BitFlagSetter)
for _, ie := range values {
if ip.HasFlag(ie) {
ies := ie.BitIndexString()
if str == "" {
str = ies
} else {
str += "|" + ies
}
}
}
return str
}
// BitFlagStringExtended returns the string representation of the given bit flag value
// with the given values available, with the bit flag type extending the other given
// bit flag type that has the given values (extendedValues) available.
func BitFlagStringExtended[T, E BitFlagConstraint](i T, values []T, extendedValues []E) string {
str := ""
ip := any(&i).(BitFlagSetter)
for _, ie := range extendedValues {
if ip.HasFlag(ie) {
ies := ie.BitIndexString()
if str == "" {
str = ies
} else {
str += "|" + ies
}
}
}
for _, ie := range values {
if ip.HasFlag(ie) {
ies := ie.BitIndexString()
if str == "" {
str = ies
} else {
str += "|" + ies
}
}
}
return str
}
// SetString sets the given enum value from its string representation, the map from
// enum names to values, and the name of the enum type, which is used for the error message.
func SetString[T EnumConstraint](i *T, s string, valueMap map[string]T, typeName string) error {
if val, ok := valueMap[s]; ok {
*i = val
return nil
}
return errors.New(s + " is not a valid value for type " + typeName)
}
// SetStringLower sets the given enum value from its string representation, the map from
// enum names to values, and the name of the enum type, which is used for the error message.
// It also tries the lowercase version of the given string if the original version fails.
func SetStringLower[T EnumConstraint](i *T, s string, valueMap map[string]T, typeName string) error {
if val, ok := valueMap[s]; ok {
*i = val
return nil
}
if val, ok := valueMap[strings.ToLower(s)]; ok {
*i = val
return nil
}
return errors.New(s + " is not a valid value for type " + typeName)
}
// SetStringExtended sets the given enum value from its string representation and the map from
// enum names to values, with the enum type extending the other given enum type. It also takes
// the enum value in terms of the extended enum type (ie).
func SetStringExtended[T EnumConstraint, E EnumSetter](i *T, ie E, s string, valueMap map[string]T) error {
if val, ok := valueMap[s]; ok {
*i = val
return nil
}
return ie.SetString(s)
}
// SetStringLowerExtended sets the given enum value from its string representation and the map from
// enum names to values, with the enum type extending the other given enum type. It also takes
// the enum value in terms of the extended enum type (ie). It also tries the lowercase version
// of the given string if the original version fails.
func SetStringLowerExtended[T EnumConstraint, E EnumSetter](i *T, ie E, s string, valueMap map[string]T) error {
if val, ok := valueMap[s]; ok {
*i = val
return nil
}
if val, ok := valueMap[strings.ToLower(s)]; ok {
*i = val
return nil
}
return ie.SetString(s)
}
// SetStringOr sets the given bit flag value from its string representation while
// preserving any bit flags already set.
func SetStringOr[T BitFlagConstraint, S BitFlagSetter](i S, s string, valueMap map[string]T, typeName string) error {
flags := strings.Split(s, "|")
for _, flag := range flags {
if val, ok := valueMap[flag]; ok {
i.SetFlag(true, val)
} else if flag == "" {
continue
} else {
return fmt.Errorf("%q is not a valid value for type %s", flag, typeName)
}
}
return nil
}
// SetStringOrLower sets the given bit flag value from its string representation while
// preserving any bit flags already set.
// It also tries the lowercase version of each flag string if the original version fails.
func SetStringOrLower[T BitFlagConstraint, S BitFlagSetter](i S, s string, valueMap map[string]T, typeName string) error {
flags := strings.Split(s, "|")
for _, flag := range flags {
if val, ok := valueMap[flag]; ok {
i.SetFlag(true, val)
} else if val, ok := valueMap[strings.ToLower(flag)]; ok {
i.SetFlag(true, val)
} else if flag == "" {
continue
} else {
return fmt.Errorf("%q is not a valid value for type %s", flag, typeName)
}
}
return nil
}
// SetStringOrExtended sets the given bit flag value from its string representation while
// preserving any bit flags already set, with the enum type extending the other
// given enum type. It also takes the enum value in terms of the extended enum
// type (ie).
func SetStringOrExtended[T BitFlagConstraint, S BitFlagSetter, E BitFlagSetter](i S, ie E, s string, valueMap map[string]T) error {
flags := strings.Split(s, "|")
for _, flag := range flags {
if val, ok := valueMap[flag]; ok {
i.SetFlag(true, val)
} else if flag == "" {
continue
} else {
err := ie.SetStringOr(flag)
if err != nil {
return err
}
}
}
return nil
}
// SetStringOrLowerExtended sets the given bit flag value from its string representation while
// preserving any bit flags already set, with the enum type extending the other
// given enum type. It also takes the enum value in terms of the extended enum
// type (ie). It also tries the lowercase version of each flag string if the original version fails.
func SetStringOrLowerExtended[T BitFlagConstraint, S BitFlagSetter, E BitFlagSetter](i S, ie E, s string, valueMap map[string]T) error {
flags := strings.Split(s, "|")
for _, flag := range flags {
if val, ok := valueMap[flag]; ok {
i.SetFlag(true, val)
} else if val, ok := valueMap[strings.ToLower(flag)]; ok {
i.SetFlag(true, val)
} else if flag == "" {
continue
} else {
err := ie.SetStringOr(flag)
if err != nil {
return err
}
}
}
return nil
}
// Desc returns the description of the given enum value.
func Desc[T EnumConstraint](i T, descMap map[T]string) string {
if str, ok := descMap[i]; ok {
return str
}
return i.String()
}
// DescExtended returns the description of the given enum value, with
// the enum type extending the other given enum type.
func DescExtended[T, E EnumConstraint](i T, descMap map[T]string) string {
if str, ok := descMap[i]; ok {
return str
}
return E(i).Desc()
}
// ValuesGlobalExtended returns also possible values for the given enum
// type that extends the other given enum type.
func ValuesGlobalExtended[T, E EnumConstraint](values []T, extendedValues []E) []T {
res := make([]T, len(extendedValues))
for i, e := range extendedValues {
res[i] = T(e)
}
res = append(res, values...)
return res
}
// Values returns all possible values for the given enum type.
func Values[T EnumConstraint](values []T) []Enum {
res := make([]Enum, len(values))
for i, d := range values {
res[i] = d
}
return res
}
// ValuesExtended returns all possible values for the given enum type
// that extends the other given enum type.
func ValuesExtended[T, E EnumConstraint](values []T, extendedValues []E) []Enum {
les := len(extendedValues)
res := make([]Enum, les+len(values))
for i, d := range extendedValues {
res[i] = d
}
for i, d := range values {
res[i+les] = d
}
return res
}
// HasFlag returns whether these bit flags have the given bit flag set.
func HasFlag(i *int64, f BitFlag) bool {
return atomic.LoadInt64(i)&(1<<uint32(f.Int64())) != 0
}
// SetFlag sets the value of the given flags in these flags to the given value.
func SetFlag(i *int64, on bool, f ...BitFlag) {
var mask int64
for _, v := range f {
mask |= 1 << v.Int64()
}
in := atomic.LoadInt64(i)
if on {
in |= mask
atomic.StoreInt64(i, in)
} else {
in &^= mask
atomic.StoreInt64(i, in)
}
}
// UnmarshalText loads the enum from the given text.
// It logs any error instead of returning it to prevent
// one modified enum from tanking an entire object loading operation.
func UnmarshalText[T EnumSetter](i T, text []byte, typeName string) error {
if err := i.SetString(string(text)); err != nil {
slog.Error(typeName+".UnmarshalText", "err", err)
}
return nil
}
// Scan loads the enum from the given SQL scanner value.
func Scan[T EnumSetter](i T, value any, typeName string) error {
if value == nil {
return nil
}
var str string
switch v := value.(type) {
case []byte:
str = string(v)
case string:
str = v
case fmt.Stringer:
str = v.String()
default:
return fmt.Errorf("invalid value for type %s: %T(%v)", typeName, value, value)
}
return i.SetString(str)
}
// 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 events
import (
"fmt"
"image"
"time"
"cogentcore.org/core/base/nptime"
"cogentcore.org/core/enums"
"cogentcore.org/core/events/key"
)
// Base is the base type for events.
// It is designed to support most event types so no further subtypes
// are needed.
type Base struct {
// Typ is the type of event, returned as Type()
Typ Types
// Flags records event boolean state, using atomic flag operations
Flags EventFlags
// GenTime records the time when the event was first generated, using more
// efficient nptime struct
GenTime nptime.Time
// Key Modifiers present when event occurred: for Key, Mouse, Touch events
Mods key.Modifiers
// Where is the window-based position in raw display dots
// (pixels) where event took place.
Where image.Point
// Start is the window-based starting position in raw display dots
// (pixels) where event started.
Start image.Point
// Prev is the window-based previous position in raw display dots
// (pixels) -- e.g., for mouse dragging.
Prev image.Point
// StTime is the starting time, using more efficient nptime struct
StTime nptime.Time
// PrvTime is the time of the previous event, using more efficient nptime struct
PrvTime nptime.Time
// LocalOffset is the offset subtracted from original window coordinates
// to compute the local coordinates.
LocalOffset image.Point
// WhereLocal is the local position, which can be adjusted from the window pos
// via SubLocalOffset based on a local top-left coordinate for a region within
// the window.
WhereLocal image.Point
// StartLocal is the local starting position
StartLocal image.Point
// PrevLocal is the local previous position
PrevLocal image.Point
// Button is the mouse button being pressed or released, for relevant events.
Button Buttons
// Rune is the meaning of the key event as determined by the
// operating system. The mapping is determined by system-dependent
// current layout, modifiers, lock-states, etc.
Rune rune
// Code is the identity of the physical key relative to a notional
// "standard" keyboard, independent of current layout, modifiers,
// lock-states, etc
Code key.Codes
// todo: add El info
Data any
}
// SetTime sets the event time to Now
func (ev *Base) SetTime() {
ev.GenTime.Now()
}
func (ev *Base) Init() {
ev.SetTime()
ev.SetLocalOff(image.Point{}) // ensure local is copied
}
func (ev Base) Type() Types {
return ev.Typ
}
func (ev *Base) AsBase() *Base {
return ev
}
func (ev Base) IsSame(oth Event) bool {
return ev.Typ == oth.Type() // basic check. redefine in subtypes
}
func (ev Base) IsUnique() bool {
return ev.Flags.HasFlag(Unique)
}
func (ev *Base) SetUnique() {
ev.Flags.SetFlag(true, Unique)
}
func (ev Base) Time() time.Time {
return ev.GenTime.Time()
}
func (ev Base) StartTime() time.Time {
return ev.StTime.Time()
}
func (ev Base) SinceStart() time.Duration {
return ev.Time().Sub(ev.StartTime())
}
func (ev Base) PrevTime() time.Time {
return ev.PrvTime.Time()
}
func (ev Base) SincePrev() time.Duration {
return ev.Time().Sub(ev.PrevTime())
}
func (ev Base) IsHandled() bool {
return ev.Flags.HasFlag(Handled)
}
func (ev *Base) SetHandled() {
ev.Flags.SetFlag(true, Handled)
}
func (ev *Base) ClearHandled() {
ev.Flags.SetFlag(false, Handled)
}
func (ev Base) String() string {
return fmt.Sprintf("%v{Time: %v, Handled: %v}", ev.Typ, ev.Time().Format("04:05"), ev.IsHandled())
}
func (ev Base) OnWinFocus() bool {
return true
}
// SetModifiers sets the bitflags based on a list of key.Modifiers
func (ev *Base) SetModifiers(mods ...enums.BitFlag) {
ev.Mods.SetFlag(true, mods...)
}
// HasAllModifiers tests whether all of given modifier(s) were set
func (ev Base) HasAllModifiers(mods ...enums.BitFlag) bool {
return key.HasAnyModifier(ev.Mods, mods...)
}
func (ev Base) HasAnyModifier(mods ...enums.BitFlag) bool {
return key.HasAnyModifier(ev.Mods, mods...)
}
func (ev Base) NeedsFocus() bool {
return false
}
func (ev Base) HasPos() bool {
return false
}
func (ev Base) WindowPos() image.Point {
return ev.Where
}
func (ev Base) WindowStartPos() image.Point {
return ev.Start
}
func (ev Base) WindowPrevPos() image.Point {
return ev.Prev
}
func (ev Base) StartDelta() image.Point {
return ev.Pos().Sub(ev.StartPos())
}
func (ev Base) PrevDelta() image.Point {
return ev.Pos().Sub(ev.PrevPos())
}
func (ev *Base) SetLocalOff(off image.Point) {
ev.LocalOffset = off
ev.WhereLocal = ev.Where.Sub(off)
ev.StartLocal = ev.Start.Sub(off)
ev.PrevLocal = ev.Prev.Sub(off)
}
func (ev Base) LocalOff() image.Point {
return ev.LocalOffset
}
func (ev Base) Pos() image.Point {
return ev.WhereLocal
}
func (ev Base) StartPos() image.Point {
return ev.StartLocal
}
func (ev Base) PrevPos() image.Point {
return ev.PrevLocal
}
// SelectMode returns the selection mode based on given modifiers on event
func (ev Base) SelectMode() SelectModes {
return SelectModeBits(ev.Mods)
}
// MouseButton is the mouse button being pressed or released, for relevant events.
func (ev Base) MouseButton() Buttons {
return ev.Button
}
// Modifiers returns the modifier keys present at time of event
func (ev Base) Modifiers() key.Modifiers {
return ev.Mods
}
func (ev Base) KeyRune() rune {
return ev.Rune
}
func (ev Base) KeyCode() key.Codes {
return ev.Code
}
// KeyChord returns a string representation of the keyboard event suitable for
// keyboard function maps, etc. Printable runes are sent directly, and
// non-printable ones are converted to their corresponding code names without
// the "Code" prefix.
func (ev Base) KeyChord() key.Chord {
return key.NewChord(ev.Rune, ev.Code, ev.Mods)
}
func (ev Base) Clone() Event {
nb := &Base{}
*nb = ev
nb.Flags.SetFlag(false, Handled)
return nb
}
func (ev Base) NewFromClone(typ Types) Event {
e := ev.Clone()
eb := e.AsBase()
eb.Typ = typ
eb.ClearHandled()
return e
}
// 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 events
import (
"fmt"
)
// CustomEvent is a user-specified event that can be sent and received
// as needed, and contains a Data field for arbitrary data, and
// optional position and focus parameters
type CustomEvent struct {
Base
// set to true if position is available
PosAvail bool
}
func (ce CustomEvent) String() string {
return fmt.Sprintf("%v{Data: %v, Time: %v}", ce.Type(), ce.Data, ce.Time())
}
func (ce CustomEvent) HasPos() bool {
return ce.PosAvail
}
// Copyright 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.
// based on golang.org/x/exp/shiny:
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package events
import (
"fmt"
"sync"
)
// TraceEventCompression can be set to true to see when events
// are being compressed to eliminate laggy behavior.
var TraceEventCompression = false
// Dequer is an infinitely buffered double-ended queue of events.
// If an event is not marked as Unique, and the last
// event in the queue is of the same type, then the new one
// replaces the last one. This automatically implements
// event compression to manage the common situation where
// event processing is slower than event generation,
// such as with Mouse movement and Paint events.
// The zero value is usable, but a Deque value must not be copied.
type Deque struct {
Back []Event // FIFO.
Front []Event // LIFO.
Mu sync.Mutex
Cond sync.Cond // Cond.L is lazily initialized to &Deque.Mu.
}
func (q *Deque) LockAndInit() {
q.Mu.Lock()
if q.Cond.L == nil {
q.Cond.L = &q.Mu
}
}
// NextEvent returns the next event in the deque.
// It blocks until such an event has been sent.
func (q *Deque) NextEvent() Event {
q.LockAndInit()
defer q.Mu.Unlock()
for {
if n := len(q.Front); n > 0 {
e := q.Front[n-1]
q.Front[n-1] = nil
q.Front = q.Front[:n-1]
return e
}
if n := len(q.Back); n > 0 {
e := q.Back[0]
q.Back[0] = nil
q.Back = q.Back[1:]
return e
}
q.Cond.Wait()
}
}
// PollEvent returns the next event in the deque if available,
// and returns true.
// If none are available, it returns false immediately.
func (q *Deque) PollEvent() (Event, bool) {
q.LockAndInit()
defer q.Mu.Unlock()
if n := len(q.Front); n > 0 {
e := q.Front[n-1]
q.Front[n-1] = nil
q.Front = q.Front[:n-1]
return e, true
}
if n := len(q.Back); n > 0 {
e := q.Back[0]
q.Back[0] = nil
q.Back = q.Back[1:]
return e, true
}
return nil, false
}
// Send adds an event to the end of the deque,
// replacing the last of the same type unless marked
// as Unique.
// They are returned by NextEvent in FIFO order.
func (q *Deque) Send(ev Event) {
q.LockAndInit()
defer q.Mu.Unlock()
n := len(q.Back)
if !ev.IsUnique() && n > 0 {
lev := q.Back[n-1]
if ev.IsSame(lev) {
q.Back[n-1] = ev // replace
switch ev.Type() {
case MouseMove, MouseDrag:
me := ev.(*Mouse)
le := lev.(*Mouse)
me.Prev = le.Prev
me.PrvTime = le.PrvTime
case Scroll:
me := ev.(*MouseScroll)
le := lev.(*MouseScroll)
me.Delta = me.Delta.Add(le.Delta)
}
q.Cond.Signal()
if TraceEventCompression {
fmt.Println("compressed back:", ev)
}
return
}
}
q.Back = append(q.Back, ev)
q.Cond.Signal()
}
// SendFirst adds an event to the start of the deque.
// They are returned by NextEvent in LIFO order,
// and have priority over events sent via Send.
// This is typically reserved for window events.
func (q *Deque) SendFirst(ev Event) {
q.LockAndInit()
defer q.Mu.Unlock()
n := len(q.Front)
if !ev.IsUnique() && n > 0 {
lev := q.Front[n-1]
if ev.IsSame(lev) {
if TraceEventCompression {
fmt.Println("compressed front:", ev)
}
q.Front[n-1] = ev // replace
q.Cond.Signal()
return
}
}
q.Front = append(q.Front, ev)
q.Cond.Signal()
}
// 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 events
import (
"fmt"
"image"
"cogentcore.org/core/events/key"
)
// DragDrop represents the drag-and-drop Drop event
type DragDrop struct {
Base
// When event is received by target, DropMod indicates the suggested modifier
// action associated with the drop (affected by holding down modifier
// keys), suggesting what to do with the dropped item, where appropriate.
// Receivers can ignore or process in their own relevant way as needed,
// BUT it is essential to update the event with the actual type of Mod
// action taken, because the event will be sent back to the source with
// this Mod as set by the receiver. The main consequence is that a
// DropMove requires the drop source to delete itself once the event has
// been received, otherwise it (typically) doesn't do anything, so just
// be careful about that particular case.
DropMod DropMods
// Data contains the data from the Source of the drag,
// typically a mimedata encoded representation.
Data any
// Source of the drop, only available for internal DND actions.
// If it is an external drop, this will be nil.
Source any
// Target of the drop -- receiver of an accepted drop should set this to
// itself, so Source (if internal) can see who got it
Target any
}
func NewDragDrop(typ Types, mdrag *Mouse) *DragDrop {
ev := &DragDrop{}
ev.Base = mdrag.Base
ev.Flags.SetFlag(false, Handled)
ev.Typ = typ
ev.DefaultMod()
return ev
}
func NewExternalDrop(typ Types, but Buttons, where image.Point, mods key.Modifiers, data any) *DragDrop {
ev := &DragDrop{}
ev.Typ = typ
ev.SetUnique()
ev.Button = but
ev.Where = where
ev.Mods = mods
ev.Data = data
return ev
}
func (ev *DragDrop) String() string {
return fmt.Sprintf("%v{Button: %v, Pos: %v, Mods: %v, Time: %v}", ev.Type(), ev.Button, ev.Where, ev.Mods.ModifiersString(), ev.Time().Format("04:05"))
}
func (ev *DragDrop) HasPos() bool {
return true
}
// DropMods indicates the modifier associated with the drop action (affected by
// holding down modifier keys), suggesting what to do with the dropped item,
// where appropriate
type DropMods int32 //enums:enum -trim-prefix Drop
const (
NoDropMod DropMods = iota
// Copy is the default and implies data is just copied -- receiver can do
// with it as they please and source does not need to take any further
// action
DropCopy
// Move is signaled with a Shift or Meta key (by default) and implies that
// the source should delete itself when it receives the DropFromSource event
// action with this Mod value set -- receiver must update the Mod to
// reflect actual action taken, and be particularly careful with this one
DropMove
// Link can be any other kind of alternative action -- link is applicable
// to files (symbolic link)
DropLink
// Ignore means that the receiver chose to not process this drop
DropIgnore
)
// DefaultModBits returns the default DropMod modifier action based on modifier keys
func DefaultModBits(mods key.Modifiers) DropMods {
switch {
case key.HasAnyModifier(mods, key.Control):
return DropCopy
case key.HasAnyModifier(mods, key.Shift, key.Meta):
return DropMove
case key.HasAnyModifier(mods, key.Alt):
return DropLink
default:
return DropCopy
}
}
// DefaultMod sets the default DropMod modifier action based on modifier keys
func (e *DragDrop) DefaultMod() {
e.DropMod = DefaultModBits(e.Mods)
}
// Code generated by "core generate"; DO NOT EDIT.
package events
import (
"cogentcore.org/core/enums"
)
var _DropModsValues = []DropMods{0, 1, 2, 3, 4}
// DropModsN is the highest valid value for type DropMods, plus one.
const DropModsN DropMods = 5
var _DropModsValueMap = map[string]DropMods{`NoDropMod`: 0, `Copy`: 1, `Move`: 2, `Link`: 3, `Ignore`: 4}
var _DropModsDescMap = map[DropMods]string{0: ``, 1: `Copy is the default and implies data is just copied -- receiver can do with it as they please and source does not need to take any further action`, 2: `Move is signaled with a Shift or Meta key (by default) and implies that the source should delete itself when it receives the DropFromSource event action with this Mod value set -- receiver must update the Mod to reflect actual action taken, and be particularly careful with this one`, 3: `Link can be any other kind of alternative action -- link is applicable to files (symbolic link)`, 4: `Ignore means that the receiver chose to not process this drop`}
var _DropModsMap = map[DropMods]string{0: `NoDropMod`, 1: `Copy`, 2: `Move`, 3: `Link`, 4: `Ignore`}
// String returns the string representation of this DropMods value.
func (i DropMods) String() string { return enums.String(i, _DropModsMap) }
// SetString sets the DropMods value from its string representation,
// and returns an error if the string is invalid.
func (i *DropMods) SetString(s string) error {
return enums.SetString(i, s, _DropModsValueMap, "DropMods")
}
// Int64 returns the DropMods value as an int64.
func (i DropMods) Int64() int64 { return int64(i) }
// SetInt64 sets the DropMods value from an int64.
func (i *DropMods) SetInt64(in int64) { *i = DropMods(in) }
// Desc returns the description of the DropMods value.
func (i DropMods) Desc() string { return enums.Desc(i, _DropModsDescMap) }
// DropModsValues returns all possible values for the type DropMods.
func DropModsValues() []DropMods { return _DropModsValues }
// Values returns all possible values for the type DropMods.
func (i DropMods) Values() []enums.Enum { return enums.Values(_DropModsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i DropMods) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *DropMods) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "DropMods") }
var _ButtonsValues = []Buttons{0, 1, 2, 3}
// ButtonsN is the highest valid value for type Buttons, plus one.
const ButtonsN Buttons = 4
var _ButtonsValueMap = map[string]Buttons{`NoButton`: 0, `Left`: 1, `Middle`: 2, `Right`: 3}
var _ButtonsDescMap = map[Buttons]string{0: ``, 1: ``, 2: ``, 3: ``}
var _ButtonsMap = map[Buttons]string{0: `NoButton`, 1: `Left`, 2: `Middle`, 3: `Right`}
// String returns the string representation of this Buttons value.
func (i Buttons) String() string { return enums.String(i, _ButtonsMap) }
// SetString sets the Buttons value from its string representation,
// and returns an error if the string is invalid.
func (i *Buttons) SetString(s string) error {
return enums.SetString(i, s, _ButtonsValueMap, "Buttons")
}
// Int64 returns the Buttons value as an int64.
func (i Buttons) Int64() int64 { return int64(i) }
// SetInt64 sets the Buttons value from an int64.
func (i *Buttons) SetInt64(in int64) { *i = Buttons(in) }
// Desc returns the description of the Buttons value.
func (i Buttons) Desc() string { return enums.Desc(i, _ButtonsDescMap) }
// ButtonsValues returns all possible values for the type Buttons.
func ButtonsValues() []Buttons { return _ButtonsValues }
// Values returns all possible values for the type Buttons.
func (i Buttons) Values() []enums.Enum { return enums.Values(_ButtonsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Buttons) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Buttons) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Buttons") }
var _SelectModesValues = []SelectModes{0, 1, 2, 3, 4, 5, 6}
// SelectModesN is the highest valid value for type SelectModes, plus one.
const SelectModesN SelectModes = 7
var _SelectModesValueMap = map[string]SelectModes{`SelectOne`: 0, `ExtendContinuous`: 1, `ExtendOne`: 2, `NoSelect`: 3, `Unselect`: 4, `SelectQuiet`: 5, `UnselectQuiet`: 6}
var _SelectModesDescMap = map[SelectModes]string{0: `SelectOne selects a single item, and is the default when no modifier key is pressed`, 1: `ExtendContinuous, activated by Shift key, extends the selection to select a continuous region of selected items, with no gaps`, 2: `ExtendOne, activated by Control or Meta / Command, extends the selection by adding the one additional item just clicked on, creating a potentially discontinuous set of selected items`, 3: `NoSelect means do not update selection -- this is used programmatically and not available via modifier key`, 4: `Unselect means unselect items -- this is used programmatically and not available via modifier key -- typically ExtendOne will unselect if already selected`, 5: `SelectQuiet means select without doing other updates or signals -- for bulk updates with a final update at the end -- used programmatically`, 6: `UnselectQuiet means unselect without doing other updates or signals -- for bulk updates with a final update at the end -- used programmatically`}
var _SelectModesMap = map[SelectModes]string{0: `SelectOne`, 1: `ExtendContinuous`, 2: `ExtendOne`, 3: `NoSelect`, 4: `Unselect`, 5: `SelectQuiet`, 6: `UnselectQuiet`}
// String returns the string representation of this SelectModes value.
func (i SelectModes) String() string { return enums.String(i, _SelectModesMap) }
// SetString sets the SelectModes value from its string representation,
// and returns an error if the string is invalid.
func (i *SelectModes) SetString(s string) error {
return enums.SetString(i, s, _SelectModesValueMap, "SelectModes")
}
// Int64 returns the SelectModes value as an int64.
func (i SelectModes) Int64() int64 { return int64(i) }
// SetInt64 sets the SelectModes value from an int64.
func (i *SelectModes) SetInt64(in int64) { *i = SelectModes(in) }
// Desc returns the description of the SelectModes value.
func (i SelectModes) Desc() string { return enums.Desc(i, _SelectModesDescMap) }
// SelectModesValues returns all possible values for the type SelectModes.
func SelectModesValues() []SelectModes { return _SelectModesValues }
// Values returns all possible values for the type SelectModes.
func (i SelectModes) Values() []enums.Enum { return enums.Values(_SelectModesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i SelectModes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *SelectModes) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "SelectModes")
}
var _TypesValues = []Types{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45}
// TypesN is the highest valid value for type Types, plus one.
const TypesN Types = 46
var _TypesValueMap = map[string]Types{`UnknownType`: 0, `MouseDown`: 1, `MouseUp`: 2, `MouseMove`: 3, `MouseDrag`: 4, `Click`: 5, `DoubleClick`: 6, `TripleClick`: 7, `ContextMenu`: 8, `LongPressStart`: 9, `LongPressEnd`: 10, `MouseEnter`: 11, `MouseLeave`: 12, `LongHoverStart`: 13, `LongHoverEnd`: 14, `DragStart`: 15, `DragMove`: 16, `DragEnter`: 17, `DragLeave`: 18, `Drop`: 19, `DropDeleteSource`: 20, `SlideStart`: 21, `SlideMove`: 22, `SlideStop`: 23, `Scroll`: 24, `KeyDown`: 25, `KeyUp`: 26, `KeyChord`: 27, `TouchStart`: 28, `TouchEnd`: 29, `TouchMove`: 30, `Magnify`: 31, `Rotate`: 32, `Select`: 33, `Focus`: 34, `FocusLost`: 35, `Change`: 36, `Input`: 37, `Show`: 38, `Close`: 39, `Window`: 40, `WindowResize`: 41, `WindowPaint`: 42, `OS`: 43, `OSOpenFiles`: 44, `Custom`: 45}
var _TypesDescMap = map[Types]string{0: `zero value is an unknown type`, 1: `MouseDown happens when a mouse button is pressed down. See MouseButton() for which. See Click for a synthetic event representing a MouseDown followed by MouseUp on the same element with Left (primary) mouse button. Often that is the most useful.`, 2: `MouseUp happens when a mouse button is released. See MouseButton() for which.`, 3: `MouseMove is always sent when the mouse is moving but no button is down, even if there might be other higher-level events too. These can be numerous and thus it is typically more efficient to listen to other events derived from this. Not unique, and Prev position is updated during compression.`, 4: `MouseDrag is always sent when the mouse is moving and there is a button down, even if there might be other higher-level events too. The start pos indicates where (and when) button first was pressed. Not unique and Prev position is updated during compression.`, 5: `Click represents a MouseDown followed by MouseUp in sequence on the same element, with the Left (primary) button. This is the typical event for most basic user interaction.`, 6: `DoubleClick represents two Click events in a row in rapid succession.`, 7: `TripleClick represents three Click events in a row in rapid succession.`, 8: `ContextMenu represents a MouseDown/Up event with the Right mouse button (which is also activated by Control key + Left Click).`, 9: `LongPressStart is when the mouse has been relatively stable after MouseDown on an element for a minimum duration (500 msec default).`, 10: `LongPressEnd is sent after LongPressStart when the mouse has gone up, moved sufficiently, left the current element, or another input event has happened.`, 11: `MouseEnter is when the mouse enters the bounding box of a new element. It is used for setting the Hover state, and can trigger cursor changes. See DragEnter for alternative case during Drag events.`, 12: `MouseLeave is when the mouse leaves the bounding box of an element, that previously had a MouseEnter event. Given that elements can have overlapping bounding boxes (e.g., child elements within a container), it is not the case that a MouseEnter on a child triggers a MouseLeave on surrounding containers. See DragLeave for alternative case during Drag events.`, 13: `LongHoverStart is when the mouse has been relatively stable after MouseEnter on an element for a minimum duration (500 msec default). This triggers the LongHover state typically used for Tooltips.`, 14: `LongHoverEnd is after LongHoverStart when the mouse has moved sufficiently, left the current element, or another input event has happened, thereby terminating the LongHover state.`, 15: `DragStart is at the start of a drag-n-drop event sequence, when a Draggable element is Active and a sufficient distance of MouseDrag events has occurred to engage the DragStart event.`, 16: `DragMove is for a MouseDrag event during the drag-n-drop sequence. Usually don't need to listen to this one. MouseDrag is also sent.`, 17: `DragEnter is like MouseEnter but after a DragStart during a drag-n-drop sequence. MouseEnter is not sent in this case.`, 18: `DragLeave is like MouseLeave but after a DragStart during a drag-n-drop sequence. MouseLeave is not sent in this case.`, 19: `Drop is sent when an item being Dragged is dropped on top of a target element. The event struct should be DragDrop.`, 20: `DropDeleteSource is sent to the source Drag element if the Drag-n-Drop event is a Move type, which requires deleting the source element. The event struct should be DragDrop.`, 21: `SlideStart is for a Slideable element when Active and a sufficient distance of MouseDrag events has occurred to engage the SlideStart event. Sets the Sliding state.`, 22: `SlideMove is for a Slideable element after SlideStart is being dragged via MouseDrag events.`, 23: `SlideStop is when the mouse button is released on a Slideable element being dragged via MouseDrag events. This typically also accompanied by a Changed event for the new slider value.`, 24: `Scroll is for scroll wheel or other scrolling events (gestures). These are not unique and Delta is updated during compression.`, 25: `KeyDown is when a key is pressed down. This provides fine-grained data about each key as it happens. KeyChord is recommended for a more complete Key event.`, 26: `KeyUp is when a key is released. This provides fine-grained data about each key as it happens. KeyChord is recommended for a more complete Key event.`, 27: `KeyChord is only generated when a non-modifier key is released, and it also contains a string representation of the full chord, suitable for translation into keyboard commands, emacs-style etc. It can be somewhat delayed relative to the KeyUp.`, 28: `TouchStart is when a touch event starts, for the low-level touch event processing. TouchStart also activates MouseDown, Scroll, Magnify, or Rotate events depending on gesture recognition.`, 29: `TouchEnd is when a touch event ends, for the low-level touch event processing. TouchEnd also activates MouseUp events depending on gesture recognition.`, 30: `TouchMove is when a touch event moves, for the low-level touch event processing. TouchMove also activates MouseMove, Scroll, Magnify, or Rotate events depending on gesture recognition.`, 31: `Magnify is a touch-based magnify event (e.g., pinch)`, 32: `Rotate is a touch-based rotate event.`, 33: `Select is sent for any direction of selection change on (or within if relevant) a Selectable element. Typically need to query the element(s) to determine current selection state.`, 34: `Focus is sent when a Focusable element receives keyboard focus (ie: by tabbing).`, 35: `FocusLost is sent when a Focusable element loses keyboard focus.`, 36: `Change is when a value represented by the element has been changed by the user and committed (for example, someone has typed text in a textfield and then pressed enter). This is *not* triggered when the value has not been committed; see [Input] for that. This is for Editable, Checkable, and Slidable items.`, 37: `Input is when a value represented by the element has changed, but has not necessarily been committed (for example, this triggers each time someone presses a key in a text field). This *is* triggered when the value has not been committed; see [Change] for a version that only occurs when the value is committed. This is for Editable, Checkable, and Slidable items.`, 38: `Show is sent to widgets when their Scene is first shown to the user in its final form, and whenever a major content managing widget (e.g., [core.Tabs], [core.Pages]) shows a new tab/page/element (via [core.WidgetBase.Shown] or DeferShown). This can be used for updates that depend on other elements, or relatively expensive updates that should be only done when actually needed "at show time".`, 39: `Close is sent to widgets when their Scene is being closed. This is an opportunity to save unsaved edits, for example. This is guaranteed to only happen once per widget per Scene.`, 40: `Window reports on changes in the window position, visibility (iconify), focus changes, screen update, and closing. These are only sent once per event (Unique).`, 41: `WindowResize happens when the window has been resized, which can happen continuously during a user resizing episode. These are not Unique events, and are compressed to minimize lag.`, 42: `WindowPaint is sent continuously at FPS frequency (60 frames per second by default) to drive updating check on the window. It is not unique, will be compressed to keep pace with updating.`, 43: `OS is an operating system generated event (app level typically)`, 44: `OSOpenFiles is an event telling app to open given files`, 45: `Custom is a user-defined event with a data any field`}
var _TypesMap = map[Types]string{0: `UnknownType`, 1: `MouseDown`, 2: `MouseUp`, 3: `MouseMove`, 4: `MouseDrag`, 5: `Click`, 6: `DoubleClick`, 7: `TripleClick`, 8: `ContextMenu`, 9: `LongPressStart`, 10: `LongPressEnd`, 11: `MouseEnter`, 12: `MouseLeave`, 13: `LongHoverStart`, 14: `LongHoverEnd`, 15: `DragStart`, 16: `DragMove`, 17: `DragEnter`, 18: `DragLeave`, 19: `Drop`, 20: `DropDeleteSource`, 21: `SlideStart`, 22: `SlideMove`, 23: `SlideStop`, 24: `Scroll`, 25: `KeyDown`, 26: `KeyUp`, 27: `KeyChord`, 28: `TouchStart`, 29: `TouchEnd`, 30: `TouchMove`, 31: `Magnify`, 32: `Rotate`, 33: `Select`, 34: `Focus`, 35: `FocusLost`, 36: `Change`, 37: `Input`, 38: `Show`, 39: `Close`, 40: `Window`, 41: `WindowResize`, 42: `WindowPaint`, 43: `OS`, 44: `OSOpenFiles`, 45: `Custom`}
// String returns the string representation of this Types value.
func (i Types) String() string { return enums.String(i, _TypesMap) }
// SetString sets the Types value from its string representation,
// and returns an error if the string is invalid.
func (i *Types) SetString(s string) error { return enums.SetString(i, s, _TypesValueMap, "Types") }
// Int64 returns the Types value as an int64.
func (i Types) Int64() int64 { return int64(i) }
// SetInt64 sets the Types value from an int64.
func (i *Types) SetInt64(in int64) { *i = Types(in) }
// Desc returns the description of the Types value.
func (i Types) Desc() string { return enums.Desc(i, _TypesDescMap) }
// TypesValues returns all possible values for the type Types.
func TypesValues() []Types { return _TypesValues }
// Values returns all possible values for the type Types.
func (i Types) Values() []enums.Enum { return enums.Values(_TypesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Types) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Types) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Types") }
var _EventFlagsValues = []EventFlags{0, 1}
// EventFlagsN is the highest valid value for type EventFlags, plus one.
const EventFlagsN EventFlags = 2
var _EventFlagsValueMap = map[string]EventFlags{`Handled`: 0, `Unique`: 1}
var _EventFlagsDescMap = map[EventFlags]string{0: `Handled indicates that the event has been handled`, 1: `EventUnique indicates that the event is Unique and not to be compressed with like events.`}
var _EventFlagsMap = map[EventFlags]string{0: `Handled`, 1: `Unique`}
// String returns the string representation of this EventFlags value.
func (i EventFlags) String() string { return enums.BitFlagString(i, _EventFlagsValues) }
// BitIndexString returns the string representation of this EventFlags value
// if it is a bit index value (typically an enum constant), and
// not an actual bit flag value.
func (i EventFlags) BitIndexString() string { return enums.String(i, _EventFlagsMap) }
// SetString sets the EventFlags value from its string representation,
// and returns an error if the string is invalid.
func (i *EventFlags) SetString(s string) error { *i = 0; return i.SetStringOr(s) }
// SetStringOr sets the EventFlags value from its string representation
// while preserving any bit flags already set, and returns an
// error if the string is invalid.
func (i *EventFlags) SetStringOr(s string) error {
return enums.SetStringOr(i, s, _EventFlagsValueMap, "EventFlags")
}
// Int64 returns the EventFlags value as an int64.
func (i EventFlags) Int64() int64 { return int64(i) }
// SetInt64 sets the EventFlags value from an int64.
func (i *EventFlags) SetInt64(in int64) { *i = EventFlags(in) }
// Desc returns the description of the EventFlags value.
func (i EventFlags) Desc() string { return enums.Desc(i, _EventFlagsDescMap) }
// EventFlagsValues returns all possible values for the type EventFlags.
func EventFlagsValues() []EventFlags { return _EventFlagsValues }
// Values returns all possible values for the type EventFlags.
func (i EventFlags) Values() []enums.Enum { return enums.Values(_EventFlagsValues) }
// HasFlag returns whether these bit flags have the given bit flag set.
func (i *EventFlags) HasFlag(f enums.BitFlag) bool { return enums.HasFlag((*int64)(i), f) }
// SetFlag sets the value of the given flags in these flags to the given value.
func (i *EventFlags) SetFlag(on bool, f ...enums.BitFlag) { enums.SetFlag((*int64)(i), on, f...) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i EventFlags) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *EventFlags) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "EventFlags")
}
var _WinActionsValues = []WinActions{0, 1, 2, 3, 4, 5, 6, 7}
// WinActionsN is the highest valid value for type WinActions, plus one.
const WinActionsN WinActions = 8
var _WinActionsValueMap = map[string]WinActions{`NoWinAction`: 0, `Close`: 1, `Minimize`: 2, `Move`: 3, `Focus`: 4, `FocusLost`: 5, `Show`: 6, `ScreenUpdate`: 7}
var _WinActionsDescMap = map[WinActions]string{0: `NoWinAction is the zero value for special types (Resize, Paint)`, 1: `WinClose means that the window is about to close, but has not yet closed.`, 2: `WinMinimize means that the window has been iconified / miniaturized / is no longer visible.`, 3: `WinMove means that the window was moved but NOT resized or changed in any other way -- does not require a redraw, but anything tracking positions will want to update.`, 4: `WinFocus indicates that the window has been activated for receiving user input.`, 5: `WinFocusLost indicates that the window is no longer activated for receiving input.`, 6: `WinShow is for the WindowShow event -- sent by the system shortly after the window has opened, to ensure that full rendering is completed with the proper size, and to trigger one-time actions such as configuring the main menu after the window has opened.`, 7: `ScreenUpdate occurs when any of the screen information is updated This event is sent to the first window on the list of active windows and it should then perform any necessary updating`}
var _WinActionsMap = map[WinActions]string{0: `NoWinAction`, 1: `Close`, 2: `Minimize`, 3: `Move`, 4: `Focus`, 5: `FocusLost`, 6: `Show`, 7: `ScreenUpdate`}
// String returns the string representation of this WinActions value.
func (i WinActions) String() string { return enums.String(i, _WinActionsMap) }
// SetString sets the WinActions value from its string representation,
// and returns an error if the string is invalid.
func (i *WinActions) SetString(s string) error {
return enums.SetString(i, s, _WinActionsValueMap, "WinActions")
}
// Int64 returns the WinActions value as an int64.
func (i WinActions) Int64() int64 { return int64(i) }
// SetInt64 sets the WinActions value from an int64.
func (i *WinActions) SetInt64(in int64) { *i = WinActions(in) }
// Desc returns the description of the WinActions value.
func (i WinActions) Desc() string { return enums.Desc(i, _WinActionsDescMap) }
// WinActionsValues returns all possible values for the type WinActions.
func WinActionsValues() []WinActions { return _WinActionsValues }
// Values returns all possible values for the type WinActions.
func (i WinActions) Values() []enums.Enum { return enums.Values(_WinActionsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i WinActions) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *WinActions) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "WinActions")
}
// 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 events
import (
"fmt"
"cogentcore.org/core/events/key"
)
// events.Key is a low-level immediately generated key event, tracking press
// and release of keys -- suitable for fine-grained tracking of key events --
// see also events.Key for events that are generated only on key press,
// and that include the full chord information about all the modifier keys
// that were present when a non-modifier key was released
type Key struct {
Base
}
func NewKey(typ Types, rn rune, code key.Codes, mods key.Modifiers) *Key {
ev := &Key{}
ev.Typ = typ
ev.SetUnique()
ev.Rune = rn
ev.Code = code
ev.Mods = mods
return ev
}
func (ev *Key) HasPos() bool {
return false
}
func (ev *Key) NeedsFocus() bool {
return true
}
func (ev *Key) String() string {
if ev.Typ == KeyChord {
return fmt.Sprintf("%v{Chord: %v, Rune: %d, Hex: %X, Mods: %v, Time: %v, Handled: %v}", ev.Type(), ev.KeyChord(), ev.Rune, ev.Rune, ev.Mods.ModifiersString(), ev.Time().Format("04:05"), ev.IsHandled())
}
return fmt.Sprintf("%v{Code: %v, Mods: %v, Time: %v, Handled: %v}", ev.Type(), ev.Code, ev.Mods.ModifiersString(), ev.Time().Format("04:05"), ev.IsHandled())
}
// 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 key
import (
"fmt"
"runtime"
"strings"
"unicode"
)
// Chord represents the key chord associated with a given key function; it
// is linked to the [cogentcore.org/core/core.KeyChordValue] so you can just
// type keys to set key chords.
type Chord string
// SystemPlatform is the string version of [cogentcore.org/core/system.App.SystemPlatform],
// which is set by system during initialization so that this package can conditionalize
// shortcut formatting based on the underlying system platform without import cycles.
var SystemPlatform string
// NewChord returns a string representation of the keyboard event suitable for
// keyboard function maps, etc. Printable runes are sent directly, and
// non-printable ones are converted to their corresponding code names without
// the "Code" prefix.
func NewChord(rn rune, code Codes, mods Modifiers) Chord {
modstr := mods.ModifiersString()
if modstr != "" && code == CodeSpacebar { // modified space is not regular space
return Chord(modstr + "Spacebar")
}
if unicode.IsPrint(rn) {
if len(modstr) > 0 {
return Chord(modstr + string(unicode.ToUpper(rn))) // all modded keys are uppercase!
} else {
return Chord(string(rn))
}
}
// now convert code
codestr := strings.TrimPrefix(code.String(), "Code")
return Chord(modstr + codestr)
}
// PlatformChord translates Command into either Control or Meta depending on the platform
func (ch Chord) PlatformChord() Chord {
sc := string(ch)
if SystemPlatform == "MacOS" {
sc = strings.ReplaceAll(sc, "Command+", "Meta+")
} else {
sc = strings.ReplaceAll(sc, "Command+", "Control+")
}
return Chord(sc)
}
// CodeIsModifier returns true if given code is a modifier key
func CodeIsModifier(c Codes) bool {
return c >= CodeLeftControl && c <= CodeRightMeta
}
// IsMulti returns true if the Chord represents a multi-key sequence
func (ch Chord) IsMulti() bool {
return strings.Contains(string(ch), " ")
}
// Chords returns the multiple keys represented in a multi-key sequence
func (ch Chord) Chords() []Chord {
ss := strings.Fields(string(ch))
nc := len(ss)
if nc <= 1 {
return []Chord{ch}
}
cc := make([]Chord, nc)
for i, s := range ss {
cc[i] = Chord(s)
}
return cc
}
// Decode decodes a chord string into rune and modifiers (set as bit flags)
func (ch Chord) Decode() (r rune, code Codes, mods Modifiers, err error) {
cs := string(ch.PlatformChord())
cs, _, _ = strings.Cut(cs, "\n") // we only care about the first chord
mods, cs = ModifiersFromString(cs)
rs := ([]rune)(cs)
if len(rs) == 1 {
r = rs[0]
return
}
cstr := string(cs)
code.SetString(cstr)
if code != CodeUnknown {
r = 0
return
}
err = fmt.Errorf("system/events/key.DecodeChord got more/less than one rune: %v from remaining chord: %v", rs, string(cs))
return
}
// Label transforms the chord string into a short form suitable for display to users.
func (ch Chord) Label() string {
cs := string(ch.PlatformChord())
cs = strings.ReplaceAll(cs, "Control", "Ctrl")
switch SystemPlatform {
case "MacOS":
if runtime.GOOS == "js" { // no font to display symbol on web
cs = strings.ReplaceAll(cs, "Meta+", "Cmd+")
} else {
cs = strings.ReplaceAll(cs, "Meta+", "⌘")
// need to have + after ⌘ when before other modifiers
cs = strings.ReplaceAll(cs, "⌘Alt", "⌘+Alt")
cs = strings.ReplaceAll(cs, "⌘Shift", "⌘+Shift")
}
case "Windows":
cs = strings.ReplaceAll(cs, "Meta+", "Win+")
}
cs = strings.ReplaceAll(cs, "\n", " or ")
return cs
}
// Code generated by "core generate"; DO NOT EDIT.
package key
import (
"cogentcore.org/core/enums"
)
var _CodesValues = []Codes{0, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 117, 127, 128, 129, 224, 225, 226, 227, 228, 229, 230, 231, 65536}
// CodesN is the highest valid value for type Codes, plus one.
const CodesN Codes = 65537
var _CodesValueMap = map[string]Codes{`Unknown`: 0, `A`: 4, `B`: 5, `C`: 6, `D`: 7, `E`: 8, `F`: 9, `G`: 10, `H`: 11, `I`: 12, `J`: 13, `K`: 14, `L`: 15, `M`: 16, `N`: 17, `O`: 18, `P`: 19, `Q`: 20, `R`: 21, `S`: 22, `T`: 23, `U`: 24, `V`: 25, `W`: 26, `X`: 27, `Y`: 28, `Z`: 29, `1`: 30, `2`: 31, `3`: 32, `4`: 33, `5`: 34, `6`: 35, `7`: 36, `8`: 37, `9`: 38, `0`: 39, `ReturnEnter`: 40, `Escape`: 41, `Backspace`: 42, `Tab`: 43, `Spacebar`: 44, `HyphenMinus`: 45, `EqualSign`: 46, `LeftSquareBracket`: 47, `RightSquareBracket`: 48, `Backslash`: 49, `Semicolon`: 51, `Apostrophe`: 52, `GraveAccent`: 53, `Comma`: 54, `FullStop`: 55, `Slash`: 56, `CapsLock`: 57, `F1`: 58, `F2`: 59, `F3`: 60, `F4`: 61, `F5`: 62, `F6`: 63, `F7`: 64, `F8`: 65, `F9`: 66, `F10`: 67, `F11`: 68, `F12`: 69, `Pause`: 72, `Insert`: 73, `Home`: 74, `PageUp`: 75, `Delete`: 76, `End`: 77, `PageDown`: 78, `RightArrow`: 79, `LeftArrow`: 80, `DownArrow`: 81, `UpArrow`: 82, `KeypadNumLock`: 83, `KeypadSlash`: 84, `KeypadAsterisk`: 85, `KeypadHyphenMinus`: 86, `KeypadPlusSign`: 87, `KeypadEnter`: 88, `Keypad1`: 89, `Keypad2`: 90, `Keypad3`: 91, `Keypad4`: 92, `Keypad5`: 93, `Keypad6`: 94, `Keypad7`: 95, `Keypad8`: 96, `Keypad9`: 97, `Keypad0`: 98, `KeypadFullStop`: 99, `KeypadEqualSign`: 103, `F13`: 104, `F14`: 105, `F15`: 106, `F16`: 107, `F17`: 108, `F18`: 109, `F19`: 110, `F20`: 111, `F21`: 112, `F22`: 113, `F23`: 114, `F24`: 115, `Help`: 117, `Mute`: 127, `VolumeUp`: 128, `VolumeDown`: 129, `LeftControl`: 224, `LeftShift`: 225, `LeftAlt`: 226, `LeftMeta`: 227, `RightControl`: 228, `RightShift`: 229, `RightAlt`: 230, `RightMeta`: 231, `Compose`: 65536}
var _CodesDescMap = map[Codes]string{0: ``, 4: ``, 5: ``, 6: ``, 7: ``, 8: ``, 9: ``, 10: ``, 11: ``, 12: ``, 13: ``, 14: ``, 15: ``, 16: ``, 17: ``, 18: ``, 19: ``, 20: ``, 21: ``, 22: ``, 23: ``, 24: ``, 25: ``, 26: ``, 27: ``, 28: ``, 29: ``, 30: ``, 31: ``, 32: ``, 33: ``, 34: ``, 35: ``, 36: ``, 37: ``, 38: ``, 39: ``, 40: ``, 41: ``, 42: ``, 43: ``, 44: ``, 45: ``, 46: ``, 47: ``, 48: ``, 49: ``, 51: ``, 52: ``, 53: ``, 54: ``, 55: ``, 56: ``, 57: ``, 58: ``, 59: ``, 60: ``, 61: ``, 62: ``, 63: ``, 64: ``, 65: ``, 66: ``, 67: ``, 68: ``, 69: ``, 72: ``, 73: ``, 74: ``, 75: ``, 76: ``, 77: ``, 78: ``, 79: ``, 80: ``, 81: ``, 82: ``, 83: ``, 84: ``, 85: ``, 86: ``, 87: ``, 88: ``, 89: ``, 90: ``, 91: ``, 92: ``, 93: ``, 94: ``, 95: ``, 96: ``, 97: ``, 98: ``, 99: ``, 103: ``, 104: ``, 105: ``, 106: ``, 107: ``, 108: ``, 109: ``, 110: ``, 111: ``, 112: ``, 113: ``, 114: ``, 115: ``, 117: ``, 127: ``, 128: ``, 129: ``, 224: ``, 225: ``, 226: ``, 227: ``, 228: ``, 229: ``, 230: ``, 231: ``, 65536: `CodeCompose is the Code for a compose key, sometimes called a multi key, used to input non-ASCII characters such as ñ being composed of n and ~. See https://en.wikipedia.org/wiki/Compose_key`}
var _CodesMap = map[Codes]string{0: `Unknown`, 4: `A`, 5: `B`, 6: `C`, 7: `D`, 8: `E`, 9: `F`, 10: `G`, 11: `H`, 12: `I`, 13: `J`, 14: `K`, 15: `L`, 16: `M`, 17: `N`, 18: `O`, 19: `P`, 20: `Q`, 21: `R`, 22: `S`, 23: `T`, 24: `U`, 25: `V`, 26: `W`, 27: `X`, 28: `Y`, 29: `Z`, 30: `1`, 31: `2`, 32: `3`, 33: `4`, 34: `5`, 35: `6`, 36: `7`, 37: `8`, 38: `9`, 39: `0`, 40: `ReturnEnter`, 41: `Escape`, 42: `Backspace`, 43: `Tab`, 44: `Spacebar`, 45: `HyphenMinus`, 46: `EqualSign`, 47: `LeftSquareBracket`, 48: `RightSquareBracket`, 49: `Backslash`, 51: `Semicolon`, 52: `Apostrophe`, 53: `GraveAccent`, 54: `Comma`, 55: `FullStop`, 56: `Slash`, 57: `CapsLock`, 58: `F1`, 59: `F2`, 60: `F3`, 61: `F4`, 62: `F5`, 63: `F6`, 64: `F7`, 65: `F8`, 66: `F9`, 67: `F10`, 68: `F11`, 69: `F12`, 72: `Pause`, 73: `Insert`, 74: `Home`, 75: `PageUp`, 76: `Delete`, 77: `End`, 78: `PageDown`, 79: `RightArrow`, 80: `LeftArrow`, 81: `DownArrow`, 82: `UpArrow`, 83: `KeypadNumLock`, 84: `KeypadSlash`, 85: `KeypadAsterisk`, 86: `KeypadHyphenMinus`, 87: `KeypadPlusSign`, 88: `KeypadEnter`, 89: `Keypad1`, 90: `Keypad2`, 91: `Keypad3`, 92: `Keypad4`, 93: `Keypad5`, 94: `Keypad6`, 95: `Keypad7`, 96: `Keypad8`, 97: `Keypad9`, 98: `Keypad0`, 99: `KeypadFullStop`, 103: `KeypadEqualSign`, 104: `F13`, 105: `F14`, 106: `F15`, 107: `F16`, 108: `F17`, 109: `F18`, 110: `F19`, 111: `F20`, 112: `F21`, 113: `F22`, 114: `F23`, 115: `F24`, 117: `Help`, 127: `Mute`, 128: `VolumeUp`, 129: `VolumeDown`, 224: `LeftControl`, 225: `LeftShift`, 226: `LeftAlt`, 227: `LeftMeta`, 228: `RightControl`, 229: `RightShift`, 230: `RightAlt`, 231: `RightMeta`, 65536: `Compose`}
// String returns the string representation of this Codes value.
func (i Codes) String() string { return enums.String(i, _CodesMap) }
// SetString sets the Codes value from its string representation,
// and returns an error if the string is invalid.
func (i *Codes) SetString(s string) error { return enums.SetString(i, s, _CodesValueMap, "Codes") }
// Int64 returns the Codes value as an int64.
func (i Codes) Int64() int64 { return int64(i) }
// SetInt64 sets the Codes value from an int64.
func (i *Codes) SetInt64(in int64) { *i = Codes(in) }
// Desc returns the description of the Codes value.
func (i Codes) Desc() string { return enums.Desc(i, _CodesDescMap) }
// CodesValues returns all possible values for the type Codes.
func CodesValues() []Codes { return _CodesValues }
// Values returns all possible values for the type Codes.
func (i Codes) Values() []enums.Enum { return enums.Values(_CodesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Codes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Codes) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Codes") }
var _ModifiersValues = []Modifiers{0, 1, 2, 3}
// ModifiersN is the highest valid value for type Modifiers, plus one.
const ModifiersN Modifiers = 4
var _ModifiersValueMap = map[string]Modifiers{`Control`: 0, `Meta`: 1, `Alt`: 2, `Shift`: 3}
var _ModifiersDescMap = map[Modifiers]string{0: `Control is the "Control" (Ctrl) key.`, 1: `Meta is the system meta key (the "Command" key on macOS and the Windows key on Windows).`, 2: `Alt is the "Alt" ("Option" on macOS) key.`, 3: `Shift is the "Shift" key.`}
var _ModifiersMap = map[Modifiers]string{0: `Control`, 1: `Meta`, 2: `Alt`, 3: `Shift`}
// String returns the string representation of this Modifiers value.
func (i Modifiers) String() string { return enums.BitFlagString(i, _ModifiersValues) }
// BitIndexString returns the string representation of this Modifiers value
// if it is a bit index value (typically an enum constant), and
// not an actual bit flag value.
func (i Modifiers) BitIndexString() string { return enums.String(i, _ModifiersMap) }
// SetString sets the Modifiers value from its string representation,
// and returns an error if the string is invalid.
func (i *Modifiers) SetString(s string) error { *i = 0; return i.SetStringOr(s) }
// SetStringOr sets the Modifiers value from its string representation
// while preserving any bit flags already set, and returns an
// error if the string is invalid.
func (i *Modifiers) SetStringOr(s string) error {
return enums.SetStringOr(i, s, _ModifiersValueMap, "Modifiers")
}
// Int64 returns the Modifiers value as an int64.
func (i Modifiers) Int64() int64 { return int64(i) }
// SetInt64 sets the Modifiers value from an int64.
func (i *Modifiers) SetInt64(in int64) { *i = Modifiers(in) }
// Desc returns the description of the Modifiers value.
func (i Modifiers) Desc() string { return enums.Desc(i, _ModifiersDescMap) }
// ModifiersValues returns all possible values for the type Modifiers.
func ModifiersValues() []Modifiers { return _ModifiersValues }
// Values returns all possible values for the type Modifiers.
func (i Modifiers) Values() []enums.Enum { return enums.Values(_ModifiersValues) }
// HasFlag returns whether these bit flags have the given bit flag set.
func (i *Modifiers) HasFlag(f enums.BitFlag) bool { return enums.HasFlag((*int64)(i), f) }
// SetFlag sets the value of the given flags in these flags to the given value.
func (i *Modifiers) SetFlag(on bool, f ...enums.BitFlag) { enums.SetFlag((*int64)(i), on, f...) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Modifiers) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Modifiers) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "Modifiers")
}
// 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 key
//go:generate core generate
import (
"strings"
"cogentcore.org/core/enums"
)
// Modifiers are used as bitflags representing a set of modifier keys.
type Modifiers int64 //enums:bitflag
const (
// Control is the "Control" (Ctrl) key.
Control Modifiers = iota
// Meta is the system meta key (the "Command" key on macOS
// and the Windows key on Windows).
Meta
// Alt is the "Alt" ("Option" on macOS) key.
Alt
// Shift is the "Shift" key.
Shift
)
// ModifiersString returns the string representation of the modifiers using
// plus symbols as seperators. The order is given by Modifiers order:
// Control, Meta, Alt, Shift.
func (mo Modifiers) ModifiersString() string {
modstr := ""
for _, m := range ModifiersValues() {
if mo.HasFlag(m) {
modstr += m.BitIndexString() + "+"
}
}
return modstr
}
// ModifiersFromString returns the modifiers corresponding to given string
// and the remainder of the string after modifiers have been stripped
func ModifiersFromString(cs string) (Modifiers, string) {
var mods Modifiers
for _, m := range ModifiersValues() {
mstr := m.BitIndexString() + "+"
if strings.HasPrefix(cs, mstr) {
mods.SetFlag(true, m)
cs = strings.TrimPrefix(cs, mstr)
}
}
return mods, cs
}
// HasAnyModifier tests whether any of given modifier(s) were set
func HasAnyModifier(flags Modifiers, mods ...enums.BitFlag) bool {
for _, m := range mods {
if flags.HasFlag(m) {
return true
}
}
return false
}
// HasAllModifiers tests whether all of given modifier(s) were set
func HasAllModifiers(flags Modifiers, mods ...enums.BitFlag) bool {
for _, m := range mods {
if !flags.HasFlag(m) {
return false
}
}
return true
}
// 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 events
// Listeners registers lists of event listener functions
// to receive different event types.
// Listeners are closure methods with all context captured.
// Functions are called in *reverse* order of when they are added:
// First In, Last Called, so that "base" functions are typically
// added first, and then can be overridden by later-added ones.
// Call SetHandled() on the event to stop further propagation.
type Listeners map[Types][]func(ev Event)
// Init ensures that the map is constructed.
func (ls *Listeners) Init() {
if *ls != nil {
return
}
*ls = make(map[Types][]func(Event))
}
// Add adds a listener for the given type to the end of the current stack
// such that it will be called before everything else already on the stack.
func (ls *Listeners) Add(typ Types, fun func(e Event)) {
ls.Init()
ets := (*ls)[typ]
ets = append(ets, fun)
(*ls)[typ] = ets
}
// HandlesEventType returns true if this listener handles the given event type.
func (ls *Listeners) HandlesEventType(typ Types) bool {
if *ls == nil {
return false
}
_, has := (*ls)[typ]
return has
}
// Call calls all functions for given event.
// It goes in reverse order so the last functions added are the first called
// and it stops when the event is marked as Handled. This allows for a natural
// and optional override behavior, as compared to requiring more complex
// priority-based mechanisms. Also, it takes an optional function that
// it calls before each event handler is run, returning if it returns
// false.
func (ls *Listeners) Call(ev Event, shouldContinue ...func() bool) {
if ev.IsHandled() {
return
}
typ := ev.Type()
ets := (*ls)[typ]
n := len(ets)
for i := n - 1; i >= 0; i-- {
if len(shouldContinue) > 0 && !shouldContinue[0]() {
break
}
fun := ets[i]
fun(ev)
if ev.IsHandled() {
break
}
}
}
// CopyFromExtra copies additional listeners from given source
// beyond those present in the receiver.
func (ls *Listeners) CopyFromExtra(fr Listeners) {
for typ, l := range *ls {
fl, has := fr[typ]
if has {
n := len(l)
if len(fl) > n {
l = append(l, fl[n:]...)
(*ls)[typ] = l
}
} else {
(*ls)[typ] = fl
}
}
}
// 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 events
import (
"fmt"
"image"
"cogentcore.org/core/events/key"
"cogentcore.org/core/math32"
)
var (
// ScrollWheelSpeed controls how fast the scroll wheel moves (typically
// interpreted as pixels per wheel step).
// This is also in core.DeviceSettings and updated from there
ScrollWheelSpeed = float32(1)
)
// Buttons is a mouse button.
type Buttons int32 //enums:enum
// TODO: have a separate axis concept for wheel up/down? How does that relate
// to joystick events?
const (
NoButton Buttons = iota
Left
Middle
Right
)
// Mouse is a basic mouse event for all mouse events except Scroll
type Mouse struct {
Base
// TODO: have a field to hold what other buttons are down, for detecting
// drags or button-chords.
// TODO: add a Device ID, for multiple input devices?
}
func NewMouse(typ Types, but Buttons, where image.Point, mods key.Modifiers) *Mouse {
ev := &Mouse{}
ev.Typ = typ
ev.SetUnique()
ev.Button = but
ev.Where = where
ev.Mods = mods
return ev
}
func (ev *Mouse) String() string {
return fmt.Sprintf("%v{Button: %v, Pos: %v, Mods: %v, Time: %v}", ev.Type(), ev.Button, ev.Where, ev.Mods.ModifiersString(), ev.Time().Format("04:05"))
}
func (ev Mouse) HasPos() bool {
return true
}
func NewMouseMove(but Buttons, where, prev image.Point, mods key.Modifiers) *Mouse {
ev := &Mouse{}
ev.Typ = MouseMove
// not unique
ev.Button = but
ev.Where = where
ev.Prev = prev
ev.Mods = mods
return ev
}
func NewMouseDrag(but Buttons, where, prev, start image.Point, mods key.Modifiers) *Mouse {
ev := &Mouse{}
ev.Typ = MouseDrag
// not unique
ev.Button = but
ev.Where = where
ev.Prev = prev
ev.Start = start
ev.Mods = mods
return ev
}
// MouseScroll is for mouse scrolling, recording the delta of the scroll
type MouseScroll struct {
Mouse
// Delta is the amount of scrolling in each axis
Delta math32.Vector2
}
func (ev *MouseScroll) String() string {
return fmt.Sprintf("%v{Delta: %v, Pos: %v, Mods: %v, Time: %v}", ev.Type(), ev.Delta, ev.Where, ev.Mods.ModifiersString(), ev.Time().Format("04:05"))
}
func NewScroll(where image.Point, delta math32.Vector2, mods key.Modifiers) *MouseScroll {
ev := &MouseScroll{}
ev.Typ = Scroll
// not unique, but delta integrated!
ev.Where = where
ev.Delta = delta
ev.Mods = mods
return ev
}
// Copyright (c) 2021 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 events
import (
"fmt"
)
// OSEvent reports an OS level event
type OSEvent struct {
Base
}
func NewOSEvent(typ Types) *OSEvent {
ev := &OSEvent{}
ev.Typ = typ
return ev
}
func (ev *OSEvent) String() string {
return fmt.Sprintf("%v{Time: %v}", ev.Type(), ev.Time().Format("04:05"))
}
// osevent.OpenFilesEvent is for OS open files action to open given files
type OSFiles struct {
OSEvent
// Files are a list of files to open
Files []string
}
func NewOSFiles(typ Types, files []string) *OSFiles {
ev := &OSFiles{}
ev.Typ = typ
ev.Files = files
return ev
}
// 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 events
import (
"cogentcore.org/core/events/key"
)
// SelectModes interprets the modifier keys to determine what type of selection mode to use.
// This is also used for selection actions and has modes not directly activated by
// modifier keys.
type SelectModes int32 //enums:enum
const (
// SelectOne selects a single item, and is the default when no modifier key
// is pressed
SelectOne SelectModes = iota
// ExtendContinuous, activated by Shift key, extends the selection to
// select a continuous region of selected items, with no gaps
ExtendContinuous
// ExtendOne, activated by Control or Meta / Command, extends the
// selection by adding the one additional item just clicked on, creating a
// potentially discontinuous set of selected items
ExtendOne
// NoSelect means do not update selection -- this is used programmatically
// and not available via modifier key
NoSelect
// Unselect means unselect items -- this is used programmatically
// and not available via modifier key -- typically ExtendOne will
// unselect if already selected
Unselect
// SelectQuiet means select without doing other updates or signals -- for
// bulk updates with a final update at the end -- used programmatically
SelectQuiet
// UnselectQuiet means unselect without doing other updates or signals -- for
// bulk updates with a final update at the end -- used programmatically
UnselectQuiet
)
// SelectModeBits returns the selection mode based on given modifiers bitflags
func SelectModeBits(mods key.Modifiers) SelectModes {
if key.HasAnyModifier(mods, key.Shift) {
return ExtendContinuous
}
if key.HasAnyModifier(mods, key.Meta) {
return ExtendOne
}
return SelectOne
}
// 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 events
import (
"fmt"
"image"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/base/nptime"
"cogentcore.org/core/events/key"
"cogentcore.org/core/math32"
)
// TraceWindowPaint prints out a . for each WindowPaint event,
// - for other window events, and * for mouse move events.
// Makes it easier to see what is going on in the overall flow.
var TraceWindowPaint = false
// Source is a source of events that manages the event
// construction and sending process for its parent window.
// It caches state as needed to generate derived events such
// as [MouseDrag].
type Source struct {
// Deque is the event queue
Deque Deque
// flag for ignoring mouse events when disabling mouse movement
ResettingPos bool
// Last has the prior state for key variables
Last SourceState
// PaintCount is used for printing paint events as .
PaintCount int
}
// SourceState tracks basic event state over time
// to enable recognition and full data for generating events.
type SourceState struct {
// last mouse button event type (down or up)
MouseButtonType Types
// last mouse button
MouseButton Buttons
// time of MouseDown
MouseDownTime nptime.Time
// position at MouseDown
MouseDownPos image.Point
// position of mouse from move events
MousePos image.Point
// time of last move
MouseMoveTime nptime.Time
// keyboard modifiers (Shift, Alt, etc)
Mods key.Modifiers
// Key event code
Key key.Codes
}
// SendKey processes a basic key event and sends it
func (es *Source) Key(typ Types, rn rune, code key.Codes, mods key.Modifiers) {
ev := NewKey(typ, rn, code, mods)
es.Last.Mods = mods
es.Last.Key = code
ev.Init()
es.Deque.Send(ev)
_, mapped := key.CodeRuneMap[code]
if typ == KeyDown && ev.Code < key.CodeLeftControl &&
(ev.HasAnyModifier(key.Control, key.Meta) || !mapped || ev.Code == key.CodeTab) {
che := NewKey(KeyChord, rn, code, mods)
che.Init()
es.Deque.Send(che)
}
}
// KeyChord processes a basic KeyChord event and sends it
func (es *Source) KeyChord(rn rune, code key.Codes, mods key.Modifiers) {
ev := NewKey(KeyChord, rn, code, mods)
// no further processing of these
ev.Init()
es.Deque.Send(ev)
}
// MouseButton creates and sends a mouse button event with given values
func (es *Source) MouseButton(typ Types, but Buttons, where image.Point, mods key.Modifiers) {
ev := NewMouse(typ, but, where, mods)
if typ != MouseDown && es.Last.MouseButtonType == MouseDown {
ev.StTime = es.Last.MouseDownTime
ev.PrvTime = es.Last.MouseMoveTime
ev.Start = es.Last.MouseDownPos
ev.Prev = es.Last.MousePos
}
es.Last.Mods = mods
es.Last.MouseButtonType = typ
es.Last.MouseButton = but
es.Last.MousePos = where
ev.Init()
if typ == MouseDown {
es.Last.MouseDownPos = where
es.Last.MouseDownTime = ev.GenTime
es.Last.MouseMoveTime = ev.GenTime
}
es.Deque.Send(ev)
}
// MouseMove creates and sends a mouse move or drag event with given values
func (es *Source) MouseMove(where image.Point) {
lastPos := es.Last.MousePos
var ev *Mouse
if es.Last.MouseButtonType == MouseDown {
ev = NewMouseDrag(es.Last.MouseButton, where, lastPos, es.Last.MouseDownPos, es.Last.Mods)
ev.StTime = es.Last.MouseDownTime
ev.PrvTime = es.Last.MouseMoveTime
} else {
ev = NewMouseMove(es.Last.MouseButton, where, lastPos, es.Last.Mods)
ev.PrvTime = es.Last.MouseMoveTime
}
ev.Init()
es.Last.MouseMoveTime = ev.GenTime
// if em.Win.IsCursorEnabled() {
es.Last.MousePos = where
// }
if TraceWindowPaint {
fmt.Printf("*")
}
es.Deque.Send(ev)
}
// Scroll creates and sends a scroll event with given values
func (es *Source) Scroll(where image.Point, delta math32.Vector2) {
ev := NewScroll(where, delta, es.Last.Mods)
ev.Init()
es.Deque.Send(ev)
}
// DropExternal creates and sends a Drop event with given values
func (es *Source) DropExternal(where image.Point, md mimedata.Mimes) {
ev := NewExternalDrop(Drop, es.Last.MouseButton, where, es.Last.Mods, md)
es.Last.MousePos = where
ev.Init()
es.Deque.Send(ev)
}
// Touch creates and sends a touch event with the given values.
// It also creates and sends a corresponding mouse event.
func (es *Source) Touch(typ Types, seq Sequence, where image.Point) {
ev := NewTouch(typ, seq, where)
ev.Init()
es.Deque.Send(ev)
if typ == TouchStart {
es.MouseButton(MouseDown, Left, where, 0) // TODO: modifiers
} else if typ == TouchEnd {
es.MouseButton(MouseUp, Left, where, 0) // TODO: modifiers
} else {
es.MouseMove(where)
}
}
// Magnify creates and sends a [TouchMagnify] event with the given values.
func (es *Source) Magnify(scaleFactor float32, where image.Point) {
ev := NewMagnify(scaleFactor, where)
ev.Init()
es.Deque.Send(ev)
}
// func (es *Source) DND(act dnd.Actions, where image.Point, data mimedata.Mimes) {
// ev := dnd.NewEvent(act, where, em.Last.Mods)
// ev.Data = data
// ev.Init()
// es.Deque.Send(ev)
// }
func (es *Source) Window(act WinActions) {
ev := NewWindow(act)
ev.Init()
if TraceWindowPaint {
fmt.Printf("-")
}
es.Deque.SendFirst(ev)
}
func (es *Source) WindowPaint() {
ev := NewWindowPaint()
ev.Init()
if TraceWindowPaint {
fmt.Printf(".")
es.PaintCount++
if es.PaintCount > 60 {
fmt.Println("")
es.PaintCount = 0
}
}
es.Deque.SendFirst(ev) // separate channel for window!
}
func (es *Source) WindowResize() {
ev := NewWindowResize()
ev.Init()
if TraceWindowPaint {
fmt.Printf("r")
}
es.Deque.SendFirst(ev)
}
func (es *Source) Custom(data any) {
ce := &CustomEvent{}
ce.Typ = Custom
ce.Data = data
ce.Init()
es.Deque.Send(ce)
}
// 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 events
import (
"fmt"
"image"
)
// The best source on android input events is the NDK: include/android/input.h
//
// iOS event handling guide:
// https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS
// Touch is a touch event.
type Touch struct {
Base
// Sequence is the sequence number. The same number is shared by all events
// in a sequence. A sequence begins with a single Begin, is followed by
// zero or more Moves, and ends with a single End. A Sequence
// distinguishes concurrent sequences but its value is subsequently reused.
Sequence Sequence
}
// Sequence identifies a sequence of touch events.
type Sequence int64
// NewTouch creates a new touch event from the given values.
func NewTouch(typ Types, seq Sequence, where image.Point) *Touch {
ev := &Touch{}
ev.Typ = typ
ev.SetUnique()
ev.Sequence = seq
ev.Where = where
return ev
}
func (ev *Touch) HasPos() bool {
return true
}
func (ev *Touch) String() string {
return fmt.Sprintf("%v{Pos: %v, Sequence: %v, Time: %v}", ev.Type(), ev.Where, ev.Sequence, ev.Time().Format("04:05"))
}
// todo: what about these higher-level abstractions of touch-like events?
// TouchMagnify is a touch magnification (scaling) gesture event.
// It is the event struct corresponding to events of type [Magnify].
type TouchMagnify struct {
Touch
// the multiplicative scale factor relative to the previous
// zoom of the screen
ScaleFactor float32
}
// NewMagnify creates a new [TouchMagnify] event based on
// the given multiplicative scale factor.
func NewMagnify(scaleFactor float32, where image.Point) *TouchMagnify {
ev := &TouchMagnify{}
ev.Typ = Magnify
ev.ScaleFactor = scaleFactor
ev.Where = where
return ev
}
// // check for interface implementation
// var _ Event = &MagnifyEvent{}
// ////////////////////////////////////////////
// // RotateEvent is used to represent a rotation gesture.
// type RotateEvent struct {
// GestureEvent
// Rotation float64 // measured in degrees; positive == clockwise
// }
// func (ev *RotateEvent) EventTypes() EventTypes {
// return RotateEventTypes
// }
// // check for interface implementation
// var _ Event = &RotateEvent{}
// // Scroll Event is used to represent a scrolling gesture.
// type ScrollEvent struct {
// GestureEvent
// Delta image.Point
// }
// func (ev *ScrollEvent) EventTypes() EventTypes {
// return ScrollEventTypes
// }
// // check for interface implementation
// var _ Event = &ScrollEvent{}
// 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 events
//go:generate core generate
// Types determines the type of GUI event, and also the
// level at which one can select which events to listen to.
// The type should include both the source / nature of the event
// and the "action" type of the event (e.g., MouseDown, MouseUp
// are separate event types). The standard
// [JavaScript Event](https://developer.mozilla.org/en-US/docs/Web/Events)
// provide the basis for most of the event type names and categories.
// Most events use the same Base type and only need
// to set relevant fields and the type.
// Unless otherwise noted, all events are marked as Unique,
// meaning they are always sent. Non-Unique events are subject
// to compression, where if the last event added (and not yet
// processed and therefore removed from the queue) is of the same type
// then it is replaced with the new one, instead of adding.
type Types int32 //enums:enum
const (
// zero value is an unknown type
UnknownType Types = iota
// MouseDown happens when a mouse button is pressed down.
// See MouseButton() for which.
// See Click for a synthetic event representing a MouseDown
// followed by MouseUp on the same element with Left (primary)
// mouse button. Often that is the most useful.
MouseDown
// MouseUp happens when a mouse button is released.
// See MouseButton() for which.
MouseUp
// MouseMove is always sent when the mouse is moving but no
// button is down, even if there might be other higher-level events too.
// These can be numerous and thus it is typically more efficient
// to listen to other events derived from this.
// Not unique, and Prev position is updated during compression.
MouseMove
// MouseDrag is always sent when the mouse is moving and there
// is a button down, even if there might be other higher-level
// events too.
// The start pos indicates where (and when) button first was pressed.
// Not unique and Prev position is updated during compression.
MouseDrag
// Click represents a MouseDown followed by MouseUp
// in sequence on the same element, with the Left (primary) button.
// This is the typical event for most basic user interaction.
Click
// DoubleClick represents two Click events in a row in rapid
// succession.
DoubleClick
// TripleClick represents three Click events in a row in rapid
// succession.
TripleClick
// ContextMenu represents a MouseDown/Up event with the
// Right mouse button (which is also activated by
// Control key + Left Click).
ContextMenu
// LongPressStart is when the mouse has been relatively stable
// after MouseDown on an element for a minimum duration (500 msec default).
LongPressStart
// LongPressEnd is sent after LongPressStart when the mouse has
// gone up, moved sufficiently, left the current element,
// or another input event has happened.
LongPressEnd
// MouseEnter is when the mouse enters the bounding box
// of a new element. It is used for setting the Hover state,
// and can trigger cursor changes.
// See DragEnter for alternative case during Drag events.
MouseEnter
// MouseLeave is when the mouse leaves the bounding box
// of an element, that previously had a MouseEnter event.
// Given that elements can have overlapping bounding boxes
// (e.g., child elements within a container), it is not the case
// that a MouseEnter on a child triggers a MouseLeave on
// surrounding containers.
// See DragLeave for alternative case during Drag events.
MouseLeave
// LongHoverStart is when the mouse has been relatively stable
// after MouseEnter on an element for a minimum duration
// (500 msec default).
// This triggers the LongHover state typically used for Tooltips.
LongHoverStart
// LongHoverEnd is after LongHoverStart when the mouse has
// moved sufficiently, left the current element,
// or another input event has happened,
// thereby terminating the LongHover state.
LongHoverEnd
// DragStart is at the start of a drag-n-drop event sequence, when
// a Draggable element is Active and a sufficient distance of
// MouseDrag events has occurred to engage the DragStart event.
DragStart
// DragMove is for a MouseDrag event during the drag-n-drop sequence.
// Usually don't need to listen to this one. MouseDrag is also sent.
DragMove
// DragEnter is like MouseEnter but after a DragStart during a
// drag-n-drop sequence. MouseEnter is not sent in this case.
DragEnter
// DragLeave is like MouseLeave but after a DragStart during a
// drag-n-drop sequence. MouseLeave is not sent in this case.
DragLeave
// Drop is sent when an item being Dragged is dropped on top of a
// target element. The event struct should be DragDrop.
Drop
// DropDeleteSource is sent to the source Drag element if the
// Drag-n-Drop event is a Move type, which requires deleting
// the source element. The event struct should be DragDrop.
DropDeleteSource
// SlideStart is for a Slideable element when Active and a
// sufficient distance of MouseDrag events has occurred to
// engage the SlideStart event. Sets the Sliding state.
SlideStart
// SlideMove is for a Slideable element after SlideStart
// is being dragged via MouseDrag events.
SlideMove
// SlideStop is when the mouse button is released on a Slideable
// element being dragged via MouseDrag events. This typically
// also accompanied by a Changed event for the new slider value.
SlideStop
// Scroll is for scroll wheel or other scrolling events (gestures).
// These are not unique and Delta is updated during compression.
Scroll
// KeyDown is when a key is pressed down.
// This provides fine-grained data about each key as it happens.
// KeyChord is recommended for a more complete Key event.
KeyDown
// KeyUp is when a key is released.
// This provides fine-grained data about each key as it happens.
// KeyChord is recommended for a more complete Key event.
KeyUp
// KeyChord is only generated when a non-modifier key is released,
// and it also contains a string representation of the full chord,
// suitable for translation into keyboard commands, emacs-style etc.
// It can be somewhat delayed relative to the KeyUp.
KeyChord
// TouchStart is when a touch event starts, for the low-level touch
// event processing. TouchStart also activates MouseDown, Scroll,
// Magnify, or Rotate events depending on gesture recognition.
TouchStart
// TouchEnd is when a touch event ends, for the low-level touch
// event processing. TouchEnd also activates MouseUp events
// depending on gesture recognition.
TouchEnd
// TouchMove is when a touch event moves, for the low-level touch
// event processing. TouchMove also activates MouseMove, Scroll,
// Magnify, or Rotate events depending on gesture recognition.
TouchMove
// Magnify is a touch-based magnify event (e.g., pinch)
Magnify
// Rotate is a touch-based rotate event.
Rotate
// Select is sent for any direction of selection change
// on (or within if relevant) a Selectable element.
// Typically need to query the element(s) to determine current
// selection state.
Select
// Focus is sent when a Focusable element receives keyboard focus (ie: by tabbing).
Focus
// FocusLost is sent when a Focusable element loses keyboard focus.
FocusLost
// Change is when a value represented by the element has been changed
// by the user and committed (for example, someone has typed text in a
// textfield and then pressed enter). This is *not* triggered when
// the value has not been committed; see [Input] for that.
// This is for Editable, Checkable, and Slidable items.
Change
// Input is when a value represented by the element has changed, but
// has not necessarily been committed (for example, this triggers each
// time someone presses a key in a text field). This *is* triggered when
// the value has not been committed; see [Change] for a version that only
// occurs when the value is committed.
// This is for Editable, Checkable, and Slidable items.
Input
// Show is sent to widgets when their Scene is first shown to the user
// in its final form, and whenever a major content managing widget
// (e.g., [core.Tabs], [core.Pages]) shows a new tab/page/element (via
// [core.WidgetBase.Shown] or DeferShown). This can be used for updates
// that depend on other elements, or relatively expensive updates that
// should be only done when actually needed "at show time".
Show
// Close is sent to widgets when their Scene is being closed. This is an
// opportunity to save unsaved edits, for example. This is guaranteed to
// only happen once per widget per Scene.
Close
// Window reports on changes in the window position,
// visibility (iconify), focus changes, screen update, and closing.
// These are only sent once per event (Unique).
Window
// WindowResize happens when the window has been resized,
// which can happen continuously during a user resizing
// episode. These are not Unique events, and are compressed
// to minimize lag.
WindowResize
// WindowPaint is sent continuously at FPS frequency
// (60 frames per second by default) to drive updating check
// on the window. It is not unique, will be compressed
// to keep pace with updating.
WindowPaint
// OS is an operating system generated event (app level typically)
OS
// OSOpenFiles is an event telling app to open given files
OSOpenFiles
// Custom is a user-defined event with a data any field
Custom
)
// IsKey returns true if event type is a Key type
func (tp Types) IsKey() bool {
return tp >= KeyDown && tp <= KeyChord
}
// IsMouse returns true if event type is a Mouse type
func (tp Types) IsMouse() bool {
return tp >= MouseDown && tp <= LongHoverEnd
}
// IsTouch returns true if event type is a Touch type
func (tp Types) IsTouch() bool {
return tp >= TouchStart && tp <= Rotate
}
// IsDrag returns true if event type is a Drag type
func (tp Types) IsDrag() bool {
return tp >= DragStart && tp <= DragLeave
}
// IsSlide returns true if event type is a Slide type
func (tp Types) IsSlide() bool {
return tp >= SlideStart && tp <= SlideStop
}
// IsWindow returns true if event type is a Window type
func (tp Types) IsWindow() bool {
return tp >= Window && tp <= WindowPaint
}
// EventFlags encode boolean event properties
type EventFlags int64 //enums:bitflag
const (
// Handled indicates that the event has been handled
Handled EventFlags = iota
// EventUnique indicates that the event is Unique and not
// to be compressed with like events.
Unique
)
// 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 events
import (
"fmt"
)
// WindowEvent reports on actions taken on a window.
// The system.Window Flags and other state information
// will always be updated prior to this event being sent,
// so those should be consulted directly for the new current state.
type WindowEvent struct {
Base
// Action taken on the window -- what has changed.
// Window state fields have current values.
Action WinActions
}
func NewWindow(act WinActions) *WindowEvent {
ev := &WindowEvent{}
ev.Action = act
ev.Typ = Window
ev.SetUnique()
return ev
}
func NewWindowResize() *WindowEvent {
ev := &WindowEvent{}
ev.Typ = WindowResize
// not unique
return ev
}
func NewWindowPaint() *WindowEvent {
ev := &WindowEvent{}
ev.Typ = WindowPaint
// not unique
return ev
}
func (ev *WindowEvent) HasPos() bool {
return false
}
func (ev *WindowEvent) String() string {
return fmt.Sprintf("%v{Action: %v, Time: %v}", ev.Type(), ev.Action, ev.Time().Format("04:05"))
}
// WinActions is the action taken on the window by the user.
type WinActions int32 //enums:enum -trim-prefix Win
const (
// NoWinAction is the zero value for special types (Resize, Paint)
NoWinAction WinActions = iota
// WinClose means that the window is about to close, but has not yet closed.
WinClose
// WinMinimize means that the window has been iconified / miniaturized / is no
// longer visible.
WinMinimize
// WinMove means that the window was moved but NOT resized or changed in any
// other way -- does not require a redraw, but anything tracking positions
// will want to update.
WinMove
// WinFocus indicates that the window has been activated for receiving user
// input.
WinFocus
// WinFocusLost indicates that the window is no longer activated for
// receiving input.
WinFocusLost
// WinShow is for the WindowShow event -- sent by the system shortly
// after the window has opened, to ensure that full rendering
// is completed with the proper size, and to trigger one-time actions such as
// configuring the main menu after the window has opened.
WinShow
// ScreenUpdate occurs when any of the screen information is updated
// This event is sent to the first window on the list of active windows
// and it should then perform any necessary updating
ScreenUpdate
)
// Command async demonstrates async updating in Cogent Core.
package main
import (
"time"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
)
type tableStruct struct {
Icon icons.Icon
IntField int
FloatField float32
StrField string
File core.Filename
}
const rows = 100000
func main() {
table := make([]*tableStruct, 0, rows)
b := core.NewBody("Async Updating")
tv := core.NewTable(b)
tv.SetReadOnly(true)
tv.SetSlice(&table)
b.OnShow(func(e events.Event) {
go func() {
for i := 0; i < rows; i++ {
b.AsyncLock()
table = append(table, &tableStruct{IntField: i, FloatField: float32(i) / 10.0})
tv.Update()
if len(table) > 0 {
tv.ScrollToIndex(len(table) - 1)
}
b.AsyncUnlock()
time.Sleep(1 * time.Millisecond)
}
}()
})
b.RunMainWindow()
}
package main
import "cogentcore.org/core/core"
func main() {
b := core.NewBody()
core.NewButton(b).SetText("Hello, World!")
b.RunMainWindow()
}
// 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 main
//go:generate core generate
import (
"embed"
"fmt"
"image"
"strconv"
"strings"
"time"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/colors"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/texteditor"
"cogentcore.org/core/tree"
)
//go:embed demo.go
var demoFile embed.FS
func main() {
b := core.NewBody("Cogent Core Demo")
ts := core.NewTabs(b)
home(ts)
widgets(ts)
collections(ts)
valueBinding(ts)
makeStyles(ts)
b.RunMainWindow()
}
func home(ts *core.Tabs) {
tab, _ := ts.NewTab("Home")
tab.Styler(func(s *styles.Style) {
s.CenterAll()
})
errors.Log(core.NewSVG(tab).ReadString(core.AppIcon))
core.NewText(tab).SetType(core.TextDisplayLarge).SetText("The Cogent Core Demo")
core.NewText(tab).SetType(core.TextTitleLarge).SetText(`A <b>demonstration</b> of the <i>various</i> features of the <a href="https://cogentcore.org/core">Cogent Core</a> 2D and 3D Go GUI <u>framework</u>`)
}
func widgets(ts *core.Tabs) {
wts := core.NewTabs(ts.NewTab("Widgets"))
text(wts)
buttons(wts)
inputs(wts)
sliders(wts)
dialogs(wts)
textEditors(wts)
}
func text(ts *core.Tabs) {
tab, _ := ts.NewTab("Text")
core.NewText(tab).SetType(core.TextHeadlineLarge).SetText("Text")
core.NewText(tab).SetText("Cogent Core provides fully customizable text elements that can be styled in any way you want. Also, there are pre-configured style types for text that allow you to easily create common text types.")
for _, typ := range core.TextTypesValues() {
s := strcase.ToSentence(typ.String())
core.NewText(tab).SetType(typ).SetText(s)
}
}
func makeRow(parent core.Widget) *core.Frame {
row := core.NewFrame(parent)
row.Styler(func(s *styles.Style) {
s.Wrap = true
s.Align.Items = styles.Center
})
return row
}
func buttons(ts *core.Tabs) {
tab, _ := ts.NewTab("Buttons")
core.NewText(tab).SetType(core.TextHeadlineLarge).SetText("Buttons")
core.NewText(tab).SetText("Cogent Core provides customizable buttons that support various events and can be styled in any way you want. Also, there are pre-configured style types for buttons that allow you to achieve common functionality with ease. All buttons support any combination of text, an icon, and an indicator.")
rowm := makeRow(tab)
rowti := makeRow(tab)
rowt := makeRow(tab)
rowi := makeRow(tab)
menu := func(m *core.Scene) {
m1 := core.NewButton(m).SetText("Menu Item 1").SetIcon(icons.Save).SetShortcut("Control+Shift+1")
m1.SetTooltip("A standard menu item with an icon")
m1.OnClick(func(e events.Event) {
fmt.Println("Clicked on menu item 1")
})
m2 := core.NewButton(m).SetText("Menu Item 2").SetIcon(icons.Open)
m2.SetTooltip("A menu item with an icon and a sub menu")
m2.Menu = func(m *core.Scene) {
sm2 := core.NewButton(m).SetText("Sub Menu Item 2").SetIcon(icons.InstallDesktop).
SetTooltip("A sub menu item with an icon")
sm2.OnClick(func(e events.Event) {
fmt.Println("Clicked on sub menu item 2")
})
}
core.NewSeparator(m)
m3 := core.NewButton(m).SetText("Menu Item 3").SetIcon(icons.Favorite).SetShortcut("Control+3").
SetTooltip("A standard menu item with an icon, below a separator")
m3.OnClick(func(e events.Event) {
fmt.Println("Clicked on menu item 3")
})
}
ics := []icons.Icon{
icons.Search, icons.Home, icons.Close, icons.Done, icons.Favorite, icons.PlayArrow,
icons.Add, icons.Delete, icons.ArrowBack, icons.Info, icons.Refresh, icons.VideoCall,
icons.Menu, icons.Settings, icons.AccountCircle, icons.Download, icons.Sort, icons.DateRange,
}
for _, typ := range core.ButtonTypesValues() {
// not really a real button, so not worth including in demo
if typ == core.ButtonMenu {
continue
}
s := strings.TrimPrefix(typ.String(), "Button")
sl := strings.ToLower(s)
art := "A "
if typ == core.ButtonElevated || typ == core.ButtonOutlined || typ == core.ButtonAction {
art = "An "
}
core.NewButton(rowm).SetType(typ).SetText(s).SetIcon(ics[typ]).SetMenu(menu).
SetTooltip(art + sl + " menu button with text and an icon")
b := core.NewButton(rowti).SetType(typ).SetText(s).SetIcon(ics[typ+6]).
SetTooltip("A " + sl + " button with text and an icon")
b.OnClick(func(e events.Event) {
fmt.Println("Got click event on", b.Name)
})
bt := core.NewButton(rowt).SetType(typ).SetText(s).
SetTooltip("A " + sl + " button with text")
bt.OnClick(func(e events.Event) {
fmt.Println("Got click event on", bt.Name)
})
bi := core.NewButton(rowi).SetType(typ).SetIcon(ics[typ+12]).
SetTooltip("A " + sl + " button with an icon")
bi.OnClick(func(e events.Event) {
fmt.Println("Got click event on", bi.Name)
})
}
}
func inputs(ts *core.Tabs) {
tab, _ := ts.NewTab("Inputs")
core.NewText(tab).SetType(core.TextHeadlineLarge).SetText("Inputs")
core.NewText(tab).SetText("Cogent Core provides various customizable input widgets that cover all common uses. Various events can be bound to inputs, and their data can easily be fetched and used wherever needed. There are also pre-configured style types for most inputs that allow you to easily switch among common styling patterns.")
core.NewTextField(tab).SetPlaceholder("Text field")
core.NewTextField(tab).AddClearButton().SetLeadingIcon(icons.Search)
core.NewTextField(tab).SetType(core.TextFieldOutlined).SetTypePassword().SetPlaceholder("Password")
core.NewTextField(tab).SetText("Text field with relatively long initial text")
spinners := core.NewFrame(tab)
core.NewSpinner(spinners).SetStep(5).SetMin(-50).SetMax(100).SetValue(15)
core.NewSpinner(spinners).SetFormat("%X").SetStep(1).SetMax(255).SetValue(44)
choosers := core.NewFrame(tab)
fruits := []core.ChooserItem{
{Value: "Apple", Tooltip: "A round, edible fruit that typically has red skin"},
{Value: "Apricot", Tooltip: "A stonefruit with a yellow or orange color"},
{Value: "Blueberry", Tooltip: "A small blue or purple berry"},
{Value: "Blackberry", Tooltip: "A small, edible, dark fruit"},
{Value: "Peach", Tooltip: "A fruit with yellow or white flesh and a large seed"},
{Value: "Strawberry", Tooltip: "A widely consumed small, red fruit"},
}
core.NewChooser(choosers).SetPlaceholder("Select a fruit").SetItems(fruits...).SetAllowNew(true)
core.NewChooser(choosers).SetPlaceholder("Select a fruit").SetItems(fruits...).SetType(core.ChooserOutlined)
core.NewChooser(tab).SetEditable(true).SetPlaceholder("Select or type a fruit").SetItems(fruits...).SetAllowNew(true)
core.NewChooser(tab).SetEditable(true).SetPlaceholder("Select or type a fruit").SetItems(fruits...).SetType(core.ChooserOutlined)
core.NewSwitch(tab).SetText("Toggle")
core.NewSwitches(tab).SetItems(
core.SwitchItem{Value: "Switch 1", Tooltip: "The first switch"},
core.SwitchItem{Value: "Switch 2", Tooltip: "The second switch"},
core.SwitchItem{Value: "Switch 3", Tooltip: "The third switch"})
core.NewSwitches(tab).SetType(core.SwitchChip).SetStrings("Chip 1", "Chip 2", "Chip 3")
core.NewSwitches(tab).SetType(core.SwitchCheckbox).SetStrings("Checkbox 1", "Checkbox 2", "Checkbox 3")
cs := core.NewSwitches(tab).SetType(core.SwitchCheckbox).SetStrings("Indeterminate 1", "Indeterminate 2", "Indeterminate 3")
cs.SetOnChildAdded(func(n tree.Node) {
core.AsWidget(n).SetState(true, states.Indeterminate)
})
core.NewSwitches(tab).SetType(core.SwitchRadioButton).SetMutex(true).SetStrings("Radio Button 1", "Radio Button 2", "Radio Button 3")
rs := core.NewSwitches(tab).SetType(core.SwitchRadioButton).SetMutex(true).SetStrings("Indeterminate 1", "Indeterminate 2", "Indeterminate 3")
rs.SetOnChildAdded(func(n tree.Node) {
core.AsWidget(n).SetState(true, states.Indeterminate)
})
core.NewSwitches(tab).SetType(core.SwitchSegmentedButton).SetMutex(true).SetStrings("Segmented Button 1", "Segmented Button 2", "Segmented Button 3")
}
func sliders(ts *core.Tabs) {
tab, _ := ts.NewTab("Sliders")
core.NewText(tab).SetType(core.TextHeadlineLarge).SetText("Sliders and meters")
core.NewText(tab).SetText("Cogent Core provides interactive sliders and customizable meters, allowing you to edit and display bounded numbers.")
core.NewSlider(tab)
core.NewSlider(tab).SetValue(0.7).SetState(true, states.Disabled)
csliders := core.NewFrame(tab)
core.NewSlider(csliders).SetValue(0.3).Styler(func(s *styles.Style) {
s.Direction = styles.Column
})
core.NewSlider(csliders).SetValue(0.2).SetState(true, states.Disabled).Styler(func(s *styles.Style) {
s.Direction = styles.Column
})
core.NewMeter(tab).SetType(core.MeterCircle).SetValue(0.7).SetText("70%")
core.NewMeter(tab).SetType(core.MeterSemicircle).SetValue(0.7).SetText("70%")
core.NewMeter(tab).SetValue(0.7)
core.NewMeter(tab).SetValue(0.7).Styler(func(s *styles.Style) {
s.Direction = styles.Column
})
}
func textEditors(ts *core.Tabs) {
tab, _ := ts.NewTab("Text editors")
core.NewText(tab).SetType(core.TextHeadlineLarge).SetText("Text editors")
core.NewText(tab).SetText("Cogent Core provides powerful text editors that support advanced code editing features, like syntax highlighting, completion, undo and redo, copy and paste, rectangular selection, and word, line, and page based navigation, selection, and deletion.")
sp := core.NewSplits(tab)
errors.Log(texteditor.NewEditor(sp).Buffer.OpenFS(demoFile, "demo.go"))
texteditor.NewEditor(sp).Buffer.SetLanguage(fileinfo.Svg).SetString(core.AppIcon)
}
func valueBinding(ts *core.Tabs) {
tab, _ := ts.NewTab("Value binding")
core.NewText(tab).SetType(core.TextHeadlineLarge).SetText("Value binding")
core.NewText(tab).SetText("Cogent Core provides the value binding system, which allows you to instantly bind Go values to interactive widgets with just a single simple line of code.")
name := "Gopher"
core.Bind(&name, core.NewTextField(tab)).OnChange(func(e events.Event) {
fmt.Println("Your name is now", name)
})
age := 35
core.Bind(&age, core.NewSpinner(tab)).OnChange(func(e events.Event) {
fmt.Println("Your age is now", age)
})
on := true
core.Bind(&on, core.NewSwitch(tab)).OnChange(func(e events.Event) {
fmt.Println("The switch is now", on)
})
theme := core.ThemeLight
core.Bind(&theme, core.NewSwitches(tab)).OnChange(func(e events.Event) {
fmt.Println("The theme is now", theme)
})
var state states.States
state.SetFlag(true, states.Hovered)
state.SetFlag(true, states.Dragging)
core.Bind(&state, core.NewSwitches(tab)).OnChange(func(e events.Event) {
fmt.Println("The state is now", state)
})
color := colors.Orange
core.Bind(&color, core.NewColorButton(tab)).OnChange(func(e events.Event) {
fmt.Println("The color is now", color)
})
colorMap := core.ColorMapName("ColdHot")
core.Bind(&colorMap, core.NewColorMapButton(tab)).OnChange(func(e events.Event) {
fmt.Println("The color map is now", colorMap)
})
t := time.Now()
core.Bind(&t, core.NewTimeInput(tab)).OnChange(func(e events.Event) {
fmt.Println("The time is now", t)
})
duration := 5 * time.Minute
core.Bind(&duration, core.NewDurationInput(tab)).OnChange(func(e events.Event) {
fmt.Println("The duration is now", duration)
})
file := core.Filename("demo.go")
core.Bind(&file, core.NewFileButton(tab)).OnChange(func(e events.Event) {
fmt.Println("The file is now", file)
})
font := core.AppearanceSettings.Font
core.Bind(&font, core.NewFontButton(tab)).OnChange(func(e events.Event) {
fmt.Println("The font is now", font)
})
core.Bind(hello, core.NewFuncButton(tab)).SetShowReturn(true)
core.Bind(styles.NewStyle, core.NewFuncButton(tab)).SetConfirm(true).SetShowReturn(true)
core.NewButton(tab).SetText("Inspector").OnClick(func(e events.Event) {
core.InspectorWindow(ts.Scene)
})
}
// Hello displays a greeting message and an age in weeks based on the given information.
func hello(firstName string, lastName string, age int, likesGo bool) (greeting string, weeksOld int) { //types:add
weeksOld = age * 52
greeting = "Hello, " + firstName + " " + lastName + "! "
if likesGo {
greeting += "I'm glad to hear that you like the best programming language!"
} else {
greeting += "You should reconsider what programming languages you like."
}
return
}
func collections(ts *core.Tabs) {
tab, _ := ts.NewTab("Collections")
core.NewText(tab).SetType(core.TextHeadlineLarge).SetText("Collections")
core.NewText(tab).SetText("Cogent Core provides powerful collection widgets that allow you to easily view and edit complex data types like structs, maps, and slices, allowing you to easily create widgets like lists, tables, and forms.")
vts := core.NewTabs(tab)
str := testStruct{
Name: "Go",
Condition: 2,
Value: 3.1415,
Vector: math32.Vec2(5, 7),
Inline: inlineStruct{Value: 3},
Condition2: tableStruct{
Age: 22,
Score: 44.4,
Name: "foo",
File: "core.go",
},
Table: make([]tableStruct, 2),
List: []float32{0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7},
}
ftab, _ := vts.NewTab("Forms")
core.NewForm(ftab).SetStruct(&str)
sl := make([]string, 50)
for i := 0; i < len(sl); i++ {
sl[i] = fmt.Sprintf("element %d", i)
}
sl[10] = "this is a particularly long value"
ltab, _ := vts.NewTab("Lists")
core.NewList(ltab).SetSlice(&sl)
mp := map[string]string{}
mp["Go"] = "Elegant, fast, and easy-to-use"
mp["Python"] = "Slow and duck-typed"
mp["C++"] = "Hard to use and slow to compile"
ktab, _ := vts.NewTab("Keyed lists")
core.NewKeyedList(ktab).SetMap(&mp)
tbl := make([]*tableStruct, 50)
for i := range tbl {
ts := &tableStruct{Age: i, Score: float32(i) / 10}
tbl[i] = ts
}
tbl[0].Name = "this is a particularly long field"
ttab, _ := vts.NewTab("Tables")
core.NewTable(ttab).SetSlice(&tbl)
sp := core.NewSplits(vts.NewTab("Trees")).SetSplits(0.3, 0.7)
tr := core.NewTree(core.NewFrame(sp)).SetText("Root")
makeTree(tr, 0)
sv := core.NewForm(sp).SetStruct(tr)
tr.OnSelect(func(e events.Event) {
if len(tr.SelectedNodes) > 0 {
sv.SetStruct(tr.SelectedNodes[0]).Update()
}
})
}
func makeTree(tr *core.Tree, round int) {
if round > 2 {
return
}
for i := range 3 {
n := core.NewTree(tr).SetText("Child " + strconv.Itoa(i))
makeTree(n, round+1)
}
}
type tableStruct struct { //types:add
// an icon
Icon icons.Icon
// an integer field
Age int `default:"2"`
// a float field
Score float32
// a string field
Name string
// a file
File core.Filename
}
type inlineStruct struct { //types:add
// click to show next
On bool
// this is now showing
ShowMe string
// a condition
Condition int
// if On && Condition == 0
Condition1 string
// if On && Condition <= 1
Condition2 tableStruct
// a value
Value float32
}
func (il *inlineStruct) ShouldDisplay(field string) bool {
switch field {
case "ShowMe", "Condition":
return il.On
case "Condition1":
return il.On && il.Condition == 0
case "Condition2":
return il.On && il.Condition <= 1
}
return true
}
type testStruct struct { //types:add
// An enum value
Enum core.ButtonTypes
// a string
Name string `default:"Go" width:"50"`
// click to show next
ShowNext bool
// this is now showing
ShowMe string
// inline struct
Inline inlineStruct `display:"inline"`
// a condition
Condition int
// if Condition == 0
Condition1 string
// if Condition >= 0
Condition2 tableStruct
// a value
Value float32
// a vector
Vector math32.Vector2
// a slice of structs
Table []tableStruct
// a slice of floats
List []float32
// a file
File core.Filename
}
func (ts *testStruct) ShouldDisplay(field string) bool {
switch field {
case "Name":
return ts.Enum <= core.ButtonElevated
case "ShowMe":
return ts.ShowNext
case "Condition1":
return ts.Condition == 0
case "Condition2":
return ts.Condition >= 0
}
return true
}
func dialogs(ts *core.Tabs) {
tab, _ := ts.NewTab("Dialogs")
core.NewText(tab).SetType(core.TextHeadlineLarge).SetText("Dialogs, snackbars, and windows")
core.NewText(tab).SetText("Cogent Core provides completely customizable dialogs, snackbars, and windows that allow you to easily display, obtain, and organize information.")
core.NewText(tab).SetType(core.TextHeadlineSmall).SetText("Dialogs")
drow := makeRow(tab)
md := core.NewButton(drow).SetText("Message")
md.OnClick(func(e events.Event) {
core.MessageDialog(md, "Something happened", "Message")
})
ed := core.NewButton(drow).SetText("Error")
ed.OnClick(func(e events.Event) {
core.ErrorDialog(ed, errors.New("invalid encoding format"), "Error loading file")
})
cd := core.NewButton(drow).SetText("Confirm")
cd.OnClick(func(e events.Event) {
d := core.NewBody("Confirm")
core.NewText(d).SetType(core.TextSupporting).SetText("Send message?")
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar).OnClick(func(e events.Event) {
core.MessageSnackbar(cd, "Dialog canceled")
})
d.AddOK(bar).OnClick(func(e events.Event) {
core.MessageSnackbar(cd, "Dialog accepted")
})
})
d.RunDialog(cd)
})
td := core.NewButton(drow).SetText("Input")
td.OnClick(func(e events.Event) {
d := core.NewBody("Input")
core.NewText(d).SetType(core.TextSupporting).SetText("What is your name?")
tf := core.NewTextField(d)
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar)
d.AddOK(bar).OnClick(func(e events.Event) {
core.MessageSnackbar(td, "Your name is "+tf.Text())
})
})
d.RunDialog(td)
})
fd := core.NewButton(drow).SetText("Full window")
u := &core.User{}
fd.OnClick(func(e events.Event) {
d := core.NewBody("Full window dialog")
core.NewText(d).SetType(core.TextSupporting).SetText("Edit your information")
core.NewForm(d).SetStruct(u).OnInput(func(e events.Event) {
fmt.Println("Got input event")
})
d.OnClose(func(e events.Event) {
fmt.Println("Your information is:", u)
})
d.RunFullDialog(td)
})
nd := core.NewButton(drow).SetText("New window")
nd.OnClick(func(e events.Event) {
d := core.NewBody("New window dialog")
core.NewText(d).SetType(core.TextSupporting).SetText("This dialog opens in a new window on multi-window platforms")
d.RunWindowDialog(nd)
})
core.NewText(tab).SetType(core.TextHeadlineSmall).SetText("Snackbars")
srow := makeRow(tab)
ms := core.NewButton(srow).SetText("Message")
ms.OnClick(func(e events.Event) {
core.MessageSnackbar(ms, "New messages loaded")
})
es := core.NewButton(srow).SetText("Error")
es.OnClick(func(e events.Event) {
core.ErrorSnackbar(es, errors.New("file not found"), "Error loading page")
})
cs := core.NewButton(srow).SetText("Custom")
cs.OnClick(func(e events.Event) {
core.NewBody().AddSnackbarText("Files updated").
AddSnackbarButton("Refresh", func(e events.Event) {
core.MessageSnackbar(cs, "Refreshed files")
}).AddSnackbarIcon(icons.Close).RunSnackbar(cs)
})
core.NewText(tab).SetType(core.TextHeadlineSmall).SetText("Windows")
wrow := makeRow(tab)
nw := core.NewButton(wrow).SetText("New window")
nw.OnClick(func(e events.Event) {
d := core.NewBody("New window")
core.NewText(d).SetType(core.TextHeadlineSmall).SetText("New window")
core.NewText(d).SetType(core.TextSupporting).SetText("A standalone window that opens in a new window on multi-window platforms")
d.RunWindow()
})
fw := core.NewButton(wrow).SetText("Full window")
fw.OnClick(func(e events.Event) {
d := core.NewBody("Full window")
core.NewText(d).SetType(core.TextSupporting).SetText("A standalone window that opens in the same system window")
d.NewWindow().SetNewWindow(false).SetDisplayTitle(true).Run()
})
core.NewText(tab).SetType(core.TextHeadlineSmall).SetText("Window manipulations")
mrow := makeRow(tab)
rw := core.NewButton(mrow).SetText("Resize to content")
rw.SetTooltip("Resizes this window to fit the current content on multi-window platforms")
rw.OnClick(func(e events.Event) {
mrow.Scene.ResizeToContent(image.Pt(0, 40)) // note: size is not correct due to wrapping? #1307
})
fs := core.NewButton(mrow).SetText("Fullscreen")
fs.SetTooltip("Toggle fullscreen mode on desktop and web platforms")
fs.OnClick(func(e events.Event) {
mrow.Scene.SetFullscreen(!mrow.Scene.IsFullscreen())
})
sg := core.NewButton(mrow).SetText("Set geometry")
sg.SetTooltip("Move the window to the top-left corner of the second screen and resize it on desktop platforms")
sg.OnClick(func(e events.Event) {
mrow.Scene.SetGeometry(false, image.Pt(30, 100), image.Pt(1000, 1000), 1)
})
}
func makeStyles(ts *core.Tabs) {
tab, _ := ts.NewTab("Styles")
core.NewText(tab).SetType(core.TextHeadlineLarge).SetText("Styles and layouts")
core.NewText(tab).SetText("Cogent Core provides a fully customizable styling and layout system that allows you to easily control the position, size, and appearance of all widgets. You can edit the style properties of the outer frame below.")
// same as docs advanced styling demo
sp := core.NewSplits(tab)
fm := core.NewForm(sp)
fr := core.NewFrame(core.NewFrame(sp)) // can not control layout when directly in splits
fr.Styler(func(s *styles.Style) {
s.Background = colors.Scheme.Select.Container
s.Grow.Set(1, 1)
})
fr.Style() // must style immediately to get correct default values
fm.SetStruct(&fr.Styles)
fm.OnChange(func(e events.Event) {
fr.OverrideStyle = true
fr.Update()
})
frameSizes := []math32.Vector2{
{20, 100},
{80, 20},
{60, 80},
{40, 120},
{150, 100},
}
for _, sz := range frameSizes {
core.NewFrame(fr).Styler(func(s *styles.Style) {
s.Min.Set(units.Dp(sz.X), units.Dp(sz.Y))
s.Background = colors.Scheme.Primary.Base
})
}
}
// Copyright (c) 2024, 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 main
import (
"embed"
"cogentcore.org/core/core"
"cogentcore.org/core/plot/plotcore"
"cogentcore.org/core/tensor/table"
)
//go:embed *.tsv
var tsv embed.FS
func main() {
b := core.NewBody("Plot Example")
epc := table.NewTable("epc")
epc.OpenFS(tsv, "ra25epoch.tsv", table.Tab)
pl := plotcore.NewPlotEditor(b)
pl.Options.Title = "RA25 Epoch Train"
pl.Options.XAxis = "Epoch"
pl.Options.Points = true
pl.SetTable(epc)
pl.ColumnOptions("UnitErr").On = true
b.AddTopBar(func(bar *core.Frame) {
core.NewToolbar(bar).Maker(pl.MakeToolbar)
})
b.RunMainWindow()
}
package main
import (
"cogentcore.org/core/base/errors"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/styles"
"cogentcore.org/core/video"
)
func main() {
b := core.NewBody("Video Example")
v := video.NewVideo(b)
// v.Rotation = -90
v.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
})
errors.Log(v.Open("deer.mp4"))
v.OnShow(func(e events.Event) {
v.Play(0, 0)
})
b.RunMainWindow()
}
// 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 filetree
import (
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/texteditor"
)
// MimeData adds mimedata for this node: a text/plain of the Path,
// text/plain of filename, and text/
func (fn *Node) MimeData(md *mimedata.Mimes) {
froot := fn.FileRoot
path := string(fn.Filepath)
punq := fn.PathFrom(froot) // note: tree paths have . escaped -> \,
*md = append(*md, mimedata.NewTextData(punq))
*md = append(*md, mimedata.NewTextData(path))
if int(fn.Info.Size) < core.SystemSettings.BigFileSize {
in, err := os.Open(path)
if err != nil {
slog.Error(err.Error())
return
}
b, err := io.ReadAll(in)
if err != nil {
slog.Error(err.Error())
return
}
fd := &mimedata.Data{fn.Info.Mime, b}
*md = append(*md, fd)
} else {
*md = append(*md, mimedata.NewTextData("File exceeds BigFileSize"))
}
}
// Cut copies the selected files to the clipboard and then deletes them.
func (fn *Node) Cut() { //types:add
if fn.IsRoot("Cut") {
return
}
fn.Copy()
// todo: move files somewhere temporary, then use those temps for paste..
core.MessageDialog(fn, "File names were copied to clipboard and can be pasted to copy elsewhere, but files are not deleted because contents of files are not placed on the clipboard and thus cannot be pasted as such. Use Delete to delete files.")
}
// Paste inserts files from the clipboard.
func (fn *Node) Paste() { //types:add
md := fn.Clipboard().Read([]string{fileinfo.TextPlain})
if md != nil {
fn.pasteFiles(md, false, nil)
}
}
func (fn *Node) DragDrop(e events.Event) {
de := e.(*events.DragDrop)
md := de.Data.(mimedata.Mimes)
fn.pasteFiles(md, de.Source == nil, func() {
fn.DropFinalize(de)
})
}
// pasteCheckExisting checks for existing files in target node directory if
// that is non-nil (otherwise just uses absolute path), and returns list of existing
// and node for last one if exists.
func (fn *Node) pasteCheckExisting(tfn *Node, md mimedata.Mimes, externalDrop bool) ([]string, *Node) {
froot := fn.FileRoot
tpath := ""
if tfn != nil {
tpath = string(tfn.Filepath)
}
nf := len(md)
if !externalDrop {
nf /= 3
}
var sfn *Node
var existing []string
for i := 0; i < nf; i++ {
var d *mimedata.Data
if !externalDrop {
d = md[i*3+1]
npath := string(md[i*3].Data)
sfni := froot.FindPath(npath)
if sfni != nil {
sfn = AsNode(sfni)
}
} else {
d = md[i] // just a list
}
if d.Type != fileinfo.TextPlain {
continue
}
path := string(d.Data)
path = strings.TrimPrefix(path, "file://")
if tfn != nil {
_, fnm := filepath.Split(path)
path = filepath.Join(tpath, fnm)
}
if errors.Log1(fsx.FileExists(path)) {
existing = append(existing, path)
}
}
return existing, sfn
}
// pasteCopyFiles copies files in given data into given target directory
func (fn *Node) pasteCopyFiles(tdir *Node, md mimedata.Mimes, externalDrop bool) {
froot := fn.FileRoot
nf := len(md)
if !externalDrop {
nf /= 3
}
for i := 0; i < nf; i++ {
var d *mimedata.Data
mode := os.FileMode(0664)
if !externalDrop {
d = md[i*3+1]
npath := string(md[i*3].Data)
sfni := froot.FindPath(npath)
if sfni == nil {
slog.Error("filetree.Node: could not find path", "path", npath)
continue
}
sfn := AsNode(sfni)
mode = sfn.Info.Mode
} else {
d = md[i] // just a list
}
if d.Type != fileinfo.TextPlain {
continue
}
path := string(d.Data)
path = strings.TrimPrefix(path, "file://")
tdir.copyFileToDir(path, mode)
}
}
// pasteCopyFilesCheck copies files into given directory node,
// first checking if any already exist -- if they exist, prompts.
func (fn *Node) pasteCopyFilesCheck(tdir *Node, md mimedata.Mimes, externalDrop bool, dropFinal func()) {
existing, _ := fn.pasteCheckExisting(tdir, md, externalDrop)
if len(existing) == 0 {
fn.pasteCopyFiles(tdir, md, externalDrop)
if dropFinal != nil {
dropFinal()
}
return
}
d := core.NewBody("File(s) Exist in Target Dir, Overwrite?")
core.NewText(d).SetType(core.TextSupporting).SetText(fmt.Sprintf("File(s): %v exist, do you want to overwrite?", existing))
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar)
d.AddOK(bar).SetText("Overwrite").OnClick(func(e events.Event) {
fn.pasteCopyFiles(tdir, md, externalDrop)
if dropFinal != nil {
dropFinal()
}
})
})
d.RunDialog(fn)
}
// pasteFiles applies a paste / drop of mime data onto this node.
// always does a copy of files into / onto target.
// externalDrop is true if this is an externally generated Drop event (from OS)
func (fn *Node) pasteFiles(md mimedata.Mimes, externalDrop bool, dropFinal func()) {
if len(md) == 0 {
return
}
if fn == nil || fn.isExternal() {
return
}
tpath := string(fn.Filepath)
isdir := fn.IsDir()
if isdir {
fn.pasteCopyFilesCheck(fn, md, externalDrop, dropFinal)
return
}
if len(md) > 3 { // multiple files -- automatically goes into parent dir
tdir := AsNode(fn.Parent)
fn.pasteCopyFilesCheck(tdir, md, externalDrop, dropFinal)
return
}
// single file dropped onto a single target file
srcpath := ""
if externalDrop || len(md) < 2 {
srcpath = string(md[0].Data) // just file path
} else {
srcpath = string(md[1].Data) // 1 has file path, 0 = tree path, 2 = file data
}
fname := filepath.Base(srcpath)
tdir := AsNode(fn.Parent)
existing, sfn := fn.pasteCheckExisting(tdir, md, externalDrop)
mode := os.FileMode(0664)
if sfn != nil {
mode = sfn.Info.Mode
}
switch {
case len(existing) == 1 && fname == fn.Name:
d := core.NewBody("Overwrite?")
core.NewText(d).SetType(core.TextSupporting).SetText(fmt.Sprintf("Overwrite target file: %s with source file of same name?, or diff (compare) two files?", fn.Name))
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar)
d.AddOK(bar).SetText("Diff (compare)").OnClick(func(e events.Event) {
texteditor.DiffFiles(fn, tpath, srcpath)
})
d.AddOK(bar).SetText("Overwrite").OnClick(func(e events.Event) {
fileinfo.CopyFile(tpath, srcpath, mode)
if dropFinal != nil {
dropFinal()
}
})
})
d.RunDialog(fn)
case len(existing) > 0:
d := core.NewBody("Overwrite?")
core.NewText(d).SetType(core.TextSupporting).SetText(fmt.Sprintf("Overwrite target file: %s with source file: %s, or overwrite existing file with same name as source file (%s), or diff (compare) files?", fn.Name, fname, fname))
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar)
d.AddOK(bar).SetText("Diff to target").OnClick(func(e events.Event) {
texteditor.DiffFiles(fn, tpath, srcpath)
})
d.AddOK(bar).SetText("Diff to existing").OnClick(func(e events.Event) {
npath := filepath.Join(string(tdir.Filepath), fname)
texteditor.DiffFiles(fn, npath, srcpath)
})
d.AddOK(bar).SetText("Overwrite target").OnClick(func(e events.Event) {
fileinfo.CopyFile(tpath, srcpath, mode)
if dropFinal != nil {
dropFinal()
}
})
d.AddOK(bar).SetText("Overwrite existing").OnClick(func(e events.Event) {
npath := filepath.Join(string(tdir.Filepath), fname)
fileinfo.CopyFile(npath, srcpath, mode)
if dropFinal != nil {
dropFinal()
}
})
})
d.RunDialog(fn)
default:
d := core.NewBody("Overwrite?")
core.NewText(d).SetType(core.TextSupporting).SetText(fmt.Sprintf("Overwrite target file: %s with source file: %s, or copy to: %s in current folder (which doesn't yet exist), or diff (compare) the two files?", fn.Name, fname, fname))
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar)
d.AddOK(bar).SetText("Diff (compare)").OnClick(func(e events.Event) {
texteditor.DiffFiles(fn, tpath, srcpath)
})
d.AddOK(bar).SetText("Overwrite target").OnClick(func(e events.Event) {
fileinfo.CopyFile(tpath, srcpath, mode)
if dropFinal != nil {
dropFinal()
}
})
d.AddOK(bar).SetText("Copy new file").OnClick(func(e events.Event) {
tdir.copyFileToDir(srcpath, mode)
if dropFinal != nil {
dropFinal()
}
})
})
d.RunDialog(fn)
}
}
// Dragged is called after target accepts the drop -- we just remove
// elements that were moved
// satisfies core.DragNDropper interface and can be overridden by subtypes
func (fn *Node) DropDeleteSource(e events.Event) {
de := e.(*events.DragDrop)
froot := fn.FileRoot
if froot == nil || fn.isExternal() {
return
}
md := de.Data.(mimedata.Mimes)
nf := len(md) / 3 // always internal
for i := 0; i < nf; i++ {
npath := string(md[i*3].Data)
sfni := froot.FindPath(npath)
if sfni == nil {
slog.Error("filetree.Node: could not find path", "path", npath)
continue
}
sfn := AsNode(sfni)
if sfn == nil {
continue
}
// fmt.Printf("dnd deleting: %v path: %v\n", sfn.Path(), sfn.FPath)
sfn.deleteFile()
}
}
// 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 filetree
import "sync"
// dirFlags are flags on directories: Open, SortBy, etc.
// These flags are stored in the DirFlagMap for persistence.
// This map is saved to a file, so these flags must be stored
// as bit flags instead of a struct to ensure efficient serialization.
type dirFlags int64 //enums:bitflag -trim-prefix dir
const (
// dirIsOpen means directory is open -- else closed
dirIsOpen dirFlags = iota
// dirSortByName means sort the directory entries by name.
// this overrides SortByModTime default on Tree if set.
dirSortByName
// dirSortByModTime means sort the directory entries by modification time.
dirSortByModTime
)
// DirFlagMap is a map for encoding directories that are open in the file
// tree. The strings are typically relative paths. The bool value is used to
// mark active paths and inactive (unmarked) ones can be removed.
// Map access is protected by Mutex.
type DirFlagMap struct {
// map of paths and associated flags
Map map[string]dirFlags
// mutex for accessing map
mu sync.Mutex
}
// init initializes the map, and sets the Mutex lock -- must unlock manually
func (dm *DirFlagMap) init() {
dm.mu.Lock()
if dm.Map == nil {
dm.Map = make(map[string]dirFlags)
}
}
// isOpen returns true if path has isOpen bit flag set
func (dm *DirFlagMap) isOpen(path string) bool {
dm.init()
defer dm.mu.Unlock()
if df, ok := dm.Map[path]; ok {
return df.HasFlag(dirIsOpen)
}
return false
}
// SetOpenState sets the given directory's open flag
func (dm *DirFlagMap) setOpen(path string, open bool) {
dm.init()
defer dm.mu.Unlock()
df := dm.Map[path]
df.SetFlag(open, dirIsOpen)
dm.Map[path] = df
}
// sortByName returns true if path is sorted by name (default if not in map)
func (dm *DirFlagMap) sortByName(path string) bool {
dm.init()
defer dm.mu.Unlock()
if df, ok := dm.Map[path]; ok {
return df.HasFlag(dirSortByName)
}
return true
}
// sortByModTime returns true if path is sorted by mod time
func (dm *DirFlagMap) sortByModTime(path string) bool {
dm.init()
defer dm.mu.Unlock()
if df, ok := dm.Map[path]; ok {
return df.HasFlag(dirSortByModTime)
}
return false
}
// setSortBy sets the given directory's sort by option
func (dm *DirFlagMap) setSortBy(path string, modTime bool) {
dm.init()
defer dm.mu.Unlock()
df := dm.Map[path]
if modTime {
df.SetFlag(true, dirSortByModTime)
df.SetFlag(false, dirSortByName)
} else {
df.SetFlag(false, dirSortByModTime)
df.SetFlag(true, dirSortByName)
}
dm.Map[path] = df
}
// Code generated by "core generate"; DO NOT EDIT.
package filetree
import (
"cogentcore.org/core/enums"
)
var _dirFlagsValues = []dirFlags{0, 1, 2}
// dirFlagsN is the highest valid value for type dirFlags, plus one.
const dirFlagsN dirFlags = 3
var _dirFlagsValueMap = map[string]dirFlags{`IsOpen`: 0, `SortByName`: 1, `SortByModTime`: 2}
var _dirFlagsDescMap = map[dirFlags]string{0: `dirIsOpen means directory is open -- else closed`, 1: `dirSortByName means sort the directory entries by name. this is mutex with other sorts -- keeping option open for non-binary sort choices.`, 2: `dirSortByModTime means sort the directory entries by modification time`}
var _dirFlagsMap = map[dirFlags]string{0: `IsOpen`, 1: `SortByName`, 2: `SortByModTime`}
// String returns the string representation of this dirFlags value.
func (i dirFlags) String() string { return enums.BitFlagString(i, _dirFlagsValues) }
// BitIndexString returns the string representation of this dirFlags value
// if it is a bit index value (typically an enum constant), and
// not an actual bit flag value.
func (i dirFlags) BitIndexString() string { return enums.String(i, _dirFlagsMap) }
// SetString sets the dirFlags value from its string representation,
// and returns an error if the string is invalid.
func (i *dirFlags) SetString(s string) error { *i = 0; return i.SetStringOr(s) }
// SetStringOr sets the dirFlags value from its string representation
// while preserving any bit flags already set, and returns an
// error if the string is invalid.
func (i *dirFlags) SetStringOr(s string) error {
return enums.SetStringOr(i, s, _dirFlagsValueMap, "dirFlags")
}
// Int64 returns the dirFlags value as an int64.
func (i dirFlags) Int64() int64 { return int64(i) }
// SetInt64 sets the dirFlags value from an int64.
func (i *dirFlags) SetInt64(in int64) { *i = dirFlags(in) }
// Desc returns the description of the dirFlags value.
func (i dirFlags) Desc() string { return enums.Desc(i, _dirFlagsDescMap) }
// dirFlagsValues returns all possible values for the type dirFlags.
func dirFlagsValues() []dirFlags { return _dirFlagsValues }
// Values returns all possible values for the type dirFlags.
func (i dirFlags) Values() []enums.Enum { return enums.Values(_dirFlagsValues) }
// HasFlag returns whether these bit flags have the given bit flag set.
func (i *dirFlags) HasFlag(f enums.BitFlag) bool { return enums.HasFlag((*int64)(i), f) }
// SetFlag sets the value of the given flags in these flags to the given value.
func (i *dirFlags) SetFlag(on bool, f ...enums.BitFlag) { enums.SetFlag((*int64)(i), on, f...) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i dirFlags) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *dirFlags) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "dirFlags") }
var _FindLocationValues = []FindLocation{0, 1, 2, 3, 4}
// FindLocationN is the highest valid value for type FindLocation, plus one.
const FindLocationN FindLocation = 5
var _FindLocationValueMap = map[string]FindLocation{`Open`: 0, `All`: 1, `File`: 2, `Dir`: 3, `NotTop`: 4}
var _FindLocationDescMap = map[FindLocation]string{0: `FindOpen finds in all open folders in the left file browser`, 1: `FindLocationAll finds in all directories under the root path. can be slow for large file trees`, 2: `FindLocationFile only finds in the current active file`, 3: `FindLocationDir only finds in the directory of the current active file`, 4: `FindLocationNotTop finds in all open folders *except* the top-level folder`}
var _FindLocationMap = map[FindLocation]string{0: `Open`, 1: `All`, 2: `File`, 3: `Dir`, 4: `NotTop`}
// String returns the string representation of this FindLocation value.
func (i FindLocation) String() string { return enums.String(i, _FindLocationMap) }
// SetString sets the FindLocation value from its string representation,
// and returns an error if the string is invalid.
func (i *FindLocation) SetString(s string) error {
return enums.SetString(i, s, _FindLocationValueMap, "FindLocation")
}
// Int64 returns the FindLocation value as an int64.
func (i FindLocation) Int64() int64 { return int64(i) }
// SetInt64 sets the FindLocation value from an int64.
func (i *FindLocation) SetInt64(in int64) { *i = FindLocation(in) }
// Desc returns the description of the FindLocation value.
func (i FindLocation) Desc() string { return enums.Desc(i, _FindLocationDescMap) }
// FindLocationValues returns all possible values for the type FindLocation.
func FindLocationValues() []FindLocation { return _FindLocationValues }
// Values returns all possible values for the type FindLocation.
func (i FindLocation) Values() []enums.Enum { return enums.Values(_FindLocationValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i FindLocation) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *FindLocation) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "FindLocation")
}
// 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 filetree
import (
"errors"
"fmt"
"os"
"path/filepath"
"syscall"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/vcs"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
)
// Filer is an interface for file tree file actions that all [Node]s satisfy.
type Filer interface { //types:add
core.Treer
// AsFileNode returns the [Node]
AsFileNode() *Node
// RenameFiles renames any selected files.
RenameFiles()
// GetFileInfo updates the .Info for this file
GetFileInfo() error
// OpenFile opens the file for node. This is called by OpenFilesDefault
OpenFile() error
}
var _ Filer = (*Node)(nil)
// OpenFilesDefault opens selected files with default app for that file type (os defined).
// runs open on Mac, xdg-open on Linux, and start on Windows
func (fn *Node) OpenFilesDefault() { //types:add
fn.SelectedFunc(func(sn *Node) {
sn.This.(Filer).OpenFile()
})
}
// OpenFile just does OpenFileDefault
func (fn *Node) OpenFile() error {
return fn.OpenFileDefault()
}
// OpenFileDefault opens file with default app for that file type (os defined)
// runs open on Mac, xdg-open on Linux, and start on Windows
func (fn *Node) OpenFileDefault() error {
core.TheApp.OpenURL("file://" + string(fn.Filepath))
return nil
}
// duplicateFiles makes a copy of selected files
func (fn *Node) duplicateFiles() { //types:add
fn.FileRoot.NeedsLayout()
fn.SelectedFunc(func(sn *Node) {
sn.duplicateFile()
})
}
// duplicateFile creates a copy of given file -- only works for regular files, not
// directories
func (fn *Node) duplicateFile() error {
_, err := fn.Info.Duplicate()
if err == nil && fn.Parent != nil {
fnp := AsNode(fn.Parent)
fnp.Update()
}
return err
}
// deletes any selected files or directories. If any directory is selected,
// all files and subdirectories in that directory are also deleted.
func (fn *Node) deleteFiles() { //types:add
d := core.NewBody("Delete Files?")
core.NewText(d).SetType(core.TextSupporting).SetText("OK to delete file(s)? This is not undoable and files are not moving to trash / recycle bin. If any selections are directories all files and subdirectories will also be deleted.")
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar)
d.AddOK(bar).SetText("Delete Files").OnClick(func(e events.Event) {
fn.deleteFilesImpl()
})
})
d.RunDialog(fn)
}
// deleteFilesImpl does the actual deletion, no prompts
func (fn *Node) deleteFilesImpl() {
fn.FileRoot.NeedsLayout()
fn.SelectedFunc(func(sn *Node) {
if !sn.Info.IsDir() {
sn.deleteFile()
return
}
var fns []string
sn.Info.Filenames(&fns)
ft := sn.FileRoot
for _, filename := range fns {
sn, ok := ft.FindFile(filename)
if !ok {
continue
}
if sn.Buffer != nil {
sn.closeBuf()
}
}
sn.deleteFile()
})
}
// deleteFile deletes this file
func (fn *Node) deleteFile() error {
if fn.isExternal() {
return nil
}
pari := fn.Parent
var parent *Node
if pari != nil {
parent = AsNode(pari)
}
fn.closeBuf()
repo, _ := fn.Repo()
var err error
if !fn.Info.IsDir() && repo != nil && fn.Info.VCS >= vcs.Stored {
// fmt.Printf("del repo: %v\n", fn.FPath)
err = repo.Delete(string(fn.Filepath))
} else {
// fmt.Printf("del raw: %v\n", fn.FPath)
err = fn.Info.Delete()
}
if err == nil {
fn.Delete()
}
if parent != nil {
parent.Update()
}
return err
}
// renames any selected files
func (fn *Node) RenameFiles() { //types:add
fn.FileRoot.NeedsLayout()
fn.SelectedFunc(func(sn *Node) {
fb := core.NewSoloFuncButton(sn).SetFunc(sn.RenameFile)
fb.Args[0].SetValue(sn.Name)
fb.CallFunc()
})
}
// RenameFile renames file to new name
func (fn *Node) RenameFile(newpath string) error { //types:add
if fn.isExternal() {
return nil
}
root := fn.FileRoot
var err error
fn.closeBuf() // invalid after this point
orgpath := fn.Filepath
newpath, err = fn.Info.Rename(newpath)
if len(newpath) == 0 || err != nil {
return err
}
if fn.IsDir() {
if fn.FileRoot.isDirOpen(orgpath) {
fn.FileRoot.setDirOpen(core.Filename(newpath))
}
}
repo, _ := fn.Repo()
stored := false
if fn.IsDir() && !fn.HasChildren() {
err = os.Rename(string(orgpath), newpath)
} else if repo != nil && fn.Info.VCS >= vcs.Stored {
stored = true
err = repo.Move(string(orgpath), newpath)
} else {
err = os.Rename(string(orgpath), newpath)
if err != nil && errors.Is(err, syscall.ENOENT) { // some kind of bogus error it seems?
err = nil
}
}
if err == nil {
err = fn.Info.InitFile(newpath)
}
if err == nil {
fn.Filepath = core.Filename(fn.Info.Path)
fn.SetName(fn.Info.Name)
fn.SetText(fn.Info.Name)
}
// todo: if you add orgpath here to git, then it will show the rename in status
if stored {
fn.AddToVCS()
}
if root != nil {
root.UpdatePath(string(orgpath))
root.UpdatePath(newpath)
}
return err
}
// newFiles makes a new file in selected directory
func (fn *Node) newFiles(filename string, addToVCS bool) { //types:add
done := false
fn.SelectedFunc(func(sn *Node) {
if !done {
sn.newFile(filename, addToVCS)
done = true
}
})
}
// newFile makes a new file in this directory node
func (fn *Node) newFile(filename string, addToVCS bool) { //types:add
if fn.isExternal() {
return
}
ppath := string(fn.Filepath)
if !fn.IsDir() {
ppath, _ = filepath.Split(ppath)
}
np := filepath.Join(ppath, filename)
_, err := os.Create(np)
if err != nil {
core.ErrorSnackbar(fn, err)
return
}
if addToVCS {
nfn, ok := fn.FileRoot.FindFile(np)
if ok && nfn.This != fn.FileRoot.This && string(nfn.Filepath) == np {
// todo: this is where it is erroneously adding too many files to vcs!
fmt.Println("Adding new file to VCS:", nfn.Filepath)
core.MessageSnackbar(fn, "Adding new file to VCS: "+fsx.DirAndFile(string(nfn.Filepath)))
nfn.AddToVCS()
}
}
fn.FileRoot.UpdatePath(np)
}
// makes a new folder in the given selected directory
func (fn *Node) newFolders(foldername string) { //types:add
done := false
fn.SelectedFunc(func(sn *Node) {
if !done {
sn.newFolder(foldername)
done = true
}
})
}
// newFolder makes a new folder (directory) in this directory node
func (fn *Node) newFolder(foldername string) { //types:add
if fn.isExternal() {
return
}
ppath := string(fn.Filepath)
if !fn.IsDir() {
ppath, _ = filepath.Split(ppath)
}
np := filepath.Join(ppath, foldername)
err := os.MkdirAll(np, 0775)
if err != nil {
core.ErrorSnackbar(fn, err)
return
}
fn.FileRoot.UpdatePath(ppath)
}
// copyFileToDir copies given file path into node that is a directory.
// This does NOT check for overwriting -- that must be done at higher level!
func (fn *Node) copyFileToDir(filename string, perm os.FileMode) {
if fn.isExternal() {
return
}
ppath := string(fn.Filepath)
sfn := filepath.Base(filename)
tpath := filepath.Join(ppath, sfn)
fileinfo.CopyFile(tpath, filename, perm)
fn.FileRoot.UpdatePath(ppath)
ofn, ok := fn.FileRoot.FindFile(filename)
if ok && ofn.Info.VCS >= vcs.Stored {
nfn, ok := fn.FileRoot.FindFile(tpath)
if ok && nfn.This != fn.FileRoot.This {
if string(nfn.Filepath) != tpath {
fmt.Printf("error: nfn.FPath != tpath; %q != %q, see bug #453\n", nfn.Filepath, tpath)
} else {
nfn.AddToVCS() // todo: this sometimes is not just tpath! See bug #453
}
nfn.Update()
}
}
}
// Shows file information about selected file(s)
func (fn *Node) showFileInfo() { //types:add
fn.SelectedFunc(func(sn *Node) {
d := core.NewBody("File info")
core.NewForm(d).SetStruct(&sn.Info).SetReadOnly(true)
d.AddOKOnly().RunFullDialog(sn)
})
}
// 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 filetree
import (
"fmt"
"path/filepath"
"sort"
"strings"
"time"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/core"
"cogentcore.org/core/tree"
)
// findDirNode finds directory node by given path.
// Must be a relative path already rooted at tree, or absolute path within tree.
func (fn *Node) findDirNode(path string) (*Node, error) {
rp := fn.RelativePathFrom(core.Filename(path))
if rp == "" {
return nil, fmt.Errorf("FindDirNode: path: %s is not relative to this node's path: %s", path, fn.Filepath)
}
if rp == "." {
return fn, nil
}
dirs := filepath.SplitList(rp)
dir := dirs[0]
dni := fn.ChildByName(dir, 0)
if dni == nil {
return nil, fmt.Errorf("could not find child %q", dir)
}
dn := AsNode(dni)
if len(dirs) == 1 {
if dn.IsDir() {
return dn, nil
}
return nil, fmt.Errorf("FindDirNode: item at path: %s is not a Directory", path)
}
return dn.findDirNode(filepath.Join(dirs[1:]...))
}
// FindFile finds first node representing given file (false if not found) --
// looks for full path names that have the given string as their suffix, so
// you can include as much of the path (including whole thing) as is relevant
// to disambiguate. See FilesMatching for a list of files that match a given
// string.
func (fn *Node) FindFile(fnm string) (*Node, bool) {
if fnm == "" {
return nil, false
}
fneff := fnm
if len(fneff) > 2 && fneff[:2] == ".." { // relative path -- get rid of it and just look for relative part
dirs := strings.Split(fneff, string(filepath.Separator))
for i, dr := range dirs {
if dr != ".." {
fneff = filepath.Join(dirs[i:]...)
break
}
}
}
if efn, err := fn.FileRoot.externalNodeByPath(fnm); err == nil {
return efn, true
}
if strings.HasPrefix(fneff, string(fn.Filepath)) { // full path
ffn, err := fn.dirsTo(fneff)
if err == nil {
return ffn, true
}
return nil, false
}
var ffn *Node
found := false
fn.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool {
sfn := AsNode(cw)
if sfn == nil {
return tree.Continue
}
if strings.HasSuffix(string(sfn.Filepath), fneff) {
ffn = sfn
found = true
return tree.Break
}
return tree.Continue
})
return ffn, found
}
// NodeNameCount is used to report counts of different string-based things
// in the file tree
type NodeNameCount struct {
Name string
Count int
}
func NodeNameCountSort(ecs []NodeNameCount) {
sort.Slice(ecs, func(i, j int) bool {
return ecs[i].Count > ecs[j].Count
})
}
// FileExtensionCounts returns a count of all the different file extensions, sorted
// from highest to lowest.
// If cat is != fileinfo.Unknown then it only uses files of that type
// (e.g., fileinfo.Code to find any code files)
func (fn *Node) FileExtensionCounts(cat fileinfo.Categories) []NodeNameCount {
cmap := make(map[string]int)
fn.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool {
sfn := AsNode(cw)
if sfn == nil {
return tree.Continue
}
if cat != fileinfo.UnknownCategory {
if sfn.Info.Cat != cat {
return tree.Continue
}
}
ext := strings.ToLower(filepath.Ext(sfn.Name))
if ec, has := cmap[ext]; has {
cmap[ext] = ec + 1
} else {
cmap[ext] = 1
}
return tree.Continue
})
ecs := make([]NodeNameCount, len(cmap))
idx := 0
for key, val := range cmap {
ecs[idx] = NodeNameCount{Name: key, Count: val}
idx++
}
NodeNameCountSort(ecs)
return ecs
}
// LatestFileMod returns the most recent mod time of files in the tree.
// If cat is != fileinfo.Unknown then it only uses files of that type
// (e.g., fileinfo.Code to find any code files)
func (fn *Node) LatestFileMod(cat fileinfo.Categories) time.Time {
tmod := time.Time{}
fn.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool {
sfn := AsNode(cw)
if sfn == nil {
return tree.Continue
}
if cat != fileinfo.UnknownCategory {
if sfn.Info.Cat != cat {
return tree.Continue
}
}
ft := (time.Time)(sfn.Info.ModTime)
if ft.After(tmod) {
tmod = ft
}
return tree.Continue
})
return tmod
}
// 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 filetree
import (
"strings"
"cogentcore.org/core/base/vcs"
"cogentcore.org/core/core"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/system"
)
// vcsLabelFunc gets the appropriate label for removing from version control
func vcsLabelFunc(fn *Node, label string) string {
repo, _ := fn.Repo()
if repo != nil {
label = strings.Replace(label, "VCS", string(repo.Vcs()), 1)
}
return label
}
func (fn *Node) VCSContextMenu(m *core.Scene) {
if fn.FileRoot.FS != nil {
return
}
core.NewFuncButton(m).SetFunc(fn.addToVCSSelected).SetText(vcsLabelFunc(fn, "Add to VCS")).SetIcon(icons.Add).
Styler(func(s *styles.Style) {
s.SetState(!fn.HasSelection() || fn.Info.VCS != vcs.Untracked, states.Disabled)
})
core.NewFuncButton(m).SetFunc(fn.deleteFromVCSSelected).SetText(vcsLabelFunc(fn, "Delete from VCS")).SetIcon(icons.Delete).
Styler(func(s *styles.Style) {
s.SetState(!fn.HasSelection() || fn.Info.VCS == vcs.Untracked, states.Disabled)
})
core.NewFuncButton(m).SetFunc(fn.commitToVCSSelected).SetText(vcsLabelFunc(fn, "Commit to VCS")).SetIcon(icons.Star).
Styler(func(s *styles.Style) {
s.SetState(!fn.HasSelection() || fn.Info.VCS == vcs.Untracked, states.Disabled)
})
core.NewFuncButton(m).SetFunc(fn.revertVCSSelected).SetText(vcsLabelFunc(fn, "Revert from VCS")).SetIcon(icons.Undo).
Styler(func(s *styles.Style) {
s.SetState(!fn.HasSelection() || fn.Info.VCS == vcs.Untracked, states.Disabled)
})
core.NewSeparator(m)
core.NewFuncButton(m).SetFunc(fn.diffVCSSelected).SetText(vcsLabelFunc(fn, "Diff VCS")).SetIcon(icons.Add).
Styler(func(s *styles.Style) {
s.SetState(!fn.HasSelection() || fn.Info.VCS == vcs.Untracked, states.Disabled)
})
core.NewFuncButton(m).SetFunc(fn.logVCSSelected).SetText(vcsLabelFunc(fn, "Log VCS")).SetIcon(icons.List).
Styler(func(s *styles.Style) {
s.SetState(!fn.HasSelection() || fn.Info.VCS == vcs.Untracked, states.Disabled)
})
core.NewFuncButton(m).SetFunc(fn.blameVCSSelected).SetText(vcsLabelFunc(fn, "Blame VCS")).SetIcon(icons.CreditScore).
Styler(func(s *styles.Style) {
s.SetState(!fn.HasSelection() || fn.Info.VCS == vcs.Untracked, states.Disabled)
})
}
func (fn *Node) contextMenu(m *core.Scene) {
core.NewFuncButton(m).SetFunc(fn.showFileInfo).SetText("Info").SetIcon(icons.Info).SetEnabled(fn.HasSelection())
open := core.NewFuncButton(m).SetFunc(fn.OpenFilesDefault).SetText("Open").SetIcon(icons.Open)
open.SetEnabled(fn.HasSelection())
if core.TheApp.Platform() == system.Web {
open.SetText("Download").SetIcon(icons.Download).SetTooltip("Download this file to your device")
}
core.NewSeparator(m)
core.NewFuncButton(m).SetFunc(fn.duplicateFiles).SetText("Duplicate").SetIcon(icons.Copy).SetKey(keymap.Duplicate).SetEnabled(fn.HasSelection())
core.NewFuncButton(m).SetFunc(fn.deleteFiles).SetText("Delete").SetIcon(icons.Delete).SetKey(keymap.Delete).SetEnabled(fn.HasSelection())
core.NewFuncButton(m).SetFunc(fn.This.(Filer).RenameFiles).SetText("Rename").SetIcon(icons.NewLabel).SetEnabled(fn.HasSelection())
core.NewSeparator(m)
core.NewFuncButton(m).SetFunc(fn.openAll).SetText("Open all").SetIcon(icons.KeyboardArrowDown).SetEnabled(fn.HasSelection() && fn.IsDir())
core.NewFuncButton(m).SetFunc(fn.CloseAll).SetIcon(icons.KeyboardArrowRight).SetEnabled(fn.HasSelection() && fn.IsDir())
core.NewFuncButton(m).SetFunc(fn.sortBys).SetText("Sort by").SetIcon(icons.Sort).SetEnabled(fn.HasSelection() && fn.IsDir())
core.NewSeparator(m)
core.NewFuncButton(m).SetFunc(fn.newFiles).SetText("New file").SetIcon(icons.OpenInNew).SetEnabled(fn.HasSelection())
core.NewFuncButton(m).SetFunc(fn.newFolders).SetText("New folder").SetIcon(icons.CreateNewFolder).SetEnabled(fn.HasSelection())
core.NewSeparator(m)
fn.VCSContextMenu(m)
core.NewSeparator(m)
core.NewFuncButton(m).SetFunc(fn.removeFromExterns).SetIcon(icons.Delete).SetEnabled(fn.HasSelection())
core.NewSeparator(m)
core.NewFuncButton(m).SetFunc(fn.Copy).SetIcon(icons.Copy).SetKey(keymap.Copy).SetEnabled(fn.HasSelection())
core.NewFuncButton(m).SetFunc(fn.Cut).SetIcon(icons.Cut).SetKey(keymap.Cut).SetEnabled(fn.HasSelection())
paste := core.NewFuncButton(m).SetFunc(fn.Paste).SetIcon(icons.Paste).SetKey(keymap.Paste).SetEnabled(fn.HasSelection())
cb := fn.Events().Clipboard()
if cb != nil {
paste.SetState(cb.IsEmpty(), states.Disabled)
}
}
// 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 filetree
//go:generate core generate
import (
"fmt"
"io/fs"
"log"
"log/slog"
"os"
"path/filepath"
"slices"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/vcs"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/events/key"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/texteditor"
"cogentcore.org/core/texteditor/highlighting"
"cogentcore.org/core/tree"
)
// NodeHighlighting is the default style for syntax highlighting to use for
// file node buffers
var NodeHighlighting = highlighting.StyleDefault
// Node represents a file in the file system, as a [core.Tree] node.
// The name of the node is the name of the file.
// Folders have children containing further nodes.
type Node struct { //core:embedder
core.Tree
// Filepath is the full path to this file.
Filepath core.Filename `edit:"-" set:"-" json:"-" xml:"-" copier:"-"`
// Info is the full standard file info about this file.
Info fileinfo.FileInfo `edit:"-" set:"-" json:"-" xml:"-" copier:"-"`
// Buffer is the file buffer for editing this file.
Buffer *texteditor.Buffer `edit:"-" set:"-" json:"-" xml:"-" copier:"-"`
// FileRoot is the root [Tree] of the tree, which has global state.
FileRoot *Tree `edit:"-" set:"-" json:"-" xml:"-" copier:"-"`
// DirRepo is the version control system repository for this directory,
// only non-nil if this is the highest-level directory in the tree under vcs control.
DirRepo vcs.Repo `edit:"-" set:"-" json:"-" xml:"-" copier:"-"`
// repoFiles has the version control system repository file status,
// providing a much faster way to get file status, vs. the repo.Status
// call which is exceptionally slow.
repoFiles vcs.Files
}
func (fn *Node) AsFileNode() *Node {
return fn
}
func (fn *Node) Init() {
fn.Tree.Init()
fn.IconOpen = icons.FolderOpen
fn.IconClosed = icons.Folder
fn.ContextMenus = nil // do not include tree
fn.AddContextMenu(fn.contextMenu)
fn.Styler(func(s *styles.Style) {
status := fn.Info.VCS
switch {
case status == vcs.Untracked:
s.Color = errors.Must1(gradient.FromString("#808080"))
case status == vcs.Modified:
s.Color = errors.Must1(gradient.FromString("#4b7fd1"))
case status == vcs.Added:
s.Color = errors.Must1(gradient.FromString("#008800"))
case status == vcs.Deleted:
s.Color = errors.Must1(gradient.FromString("#ff4252"))
case status == vcs.Conflicted:
s.Color = errors.Must1(gradient.FromString("#ce8020"))
case status == vcs.Updated:
s.Color = errors.Must1(gradient.FromString("#008060"))
case status == vcs.Stored:
s.Color = colors.Scheme.OnSurface
}
})
fn.On(events.KeyChord, func(e events.Event) {
if core.DebugSettings.KeyEventTrace {
fmt.Printf("Tree KeyInput: %v\n", fn.Path())
}
kf := keymap.Of(e.KeyChord())
selMode := events.SelectModeBits(e.Modifiers())
if selMode == events.SelectOne {
if fn.SelectMode {
selMode = events.ExtendContinuous
}
}
// first all the keys that work for ReadOnly and active
if !fn.IsReadOnly() && !e.IsHandled() {
switch kf {
case keymap.Delete:
fn.deleteFiles()
e.SetHandled()
case keymap.Backspace:
fn.deleteFiles()
e.SetHandled()
case keymap.Duplicate:
fn.duplicateFiles()
e.SetHandled()
case keymap.Insert: // New File
core.CallFunc(fn, fn.newFile)
e.SetHandled()
case keymap.InsertAfter: // New Folder
core.CallFunc(fn, fn.newFolder)
e.SetHandled()
}
}
})
fn.Parts.Styler(func(s *styles.Style) {
s.Gap.X.Em(0.4)
})
fn.Parts.OnClick(func(e events.Event) {
if !e.HasAnyModifier(key.Control, key.Meta, key.Alt, key.Shift) {
fn.Open()
}
})
fn.Parts.OnDoubleClick(func(e events.Event) {
e.SetHandled()
if fn.IsDir() {
fn.ToggleClose()
} else {
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() {
s.Font.Weight = styles.WeightBold
}
if fn.Buffer != nil {
s.Font.Style = styles.Italic
}
})
})
fn.Updater(func() {
fn.setFileIcon()
if fn.IsDir() {
repo, rnode := fn.Repo()
if repo != nil && rnode.This == fn.This {
go rnode.updateRepoFiles()
}
} else {
fn.This.(Filer).GetFileInfo()
}
fn.Text = fn.Info.Name
})
fn.Maker(func(p *tree.Plan) {
if fn.Filepath == "" {
return
}
if fn.Name == externalFilesName {
files := fn.FileRoot.externalFiles
for _, fi := range files {
tree.AddNew(p, fi, func() Filer {
return tree.NewOfType(fn.FileRoot.FileNodeType).(Filer)
}, func(wf Filer) {
w := wf.AsFileNode()
w.NeedsLayout()
w.FileRoot = fn.FileRoot
w.Filepath = core.Filename(fi)
w.Info.Mode = os.ModeIrregular
w.Info.VCS = vcs.Stored
})
}
return
}
if !fn.IsDir() || fn.IsIrregular() {
return
}
if !((fn.FileRoot.inOpenAll && !fn.Info.IsHidden()) || fn.FileRoot.isDirOpen(fn.Filepath)) {
return
}
repo, _ := fn.Repo()
files := fn.dirFileList()
for _, fi := range files {
fpath := filepath.Join(string(fn.Filepath), fi.Name())
if fn.FileRoot.FilterFunc != nil && !fn.FileRoot.FilterFunc(fpath, fi) {
continue
}
tree.AddNew(p, fi.Name(), func() Filer {
return tree.NewOfType(fn.FileRoot.FileNodeType).(Filer)
}, func(wf Filer) {
w := wf.AsFileNode()
w.NeedsLayout()
w.FileRoot = fn.FileRoot
w.Filepath = core.Filename(fpath)
w.This.(Filer).GetFileInfo()
if w.FileRoot.FS == nil {
if w.IsDir() && repo == nil {
w.detectVCSRepo(true) // update files
}
}
})
}
})
}
// IsDir returns true if file is a directory (folder)
func (fn *Node) IsDir() bool {
return fn.Info.IsDir()
}
// IsIrregular returns true if file is a special "Irregular" node
func (fn *Node) IsIrregular() bool {
return (fn.Info.Mode & os.ModeIrregular) != 0
}
// isExternal returns true if file is external to main file tree
func (fn *Node) isExternal() bool {
isExt := false
fn.WalkUp(func(k tree.Node) bool {
sfn := AsNode(k)
if sfn == nil {
return tree.Break
}
if sfn.IsIrregular() {
isExt = true
return tree.Break
}
return tree.Continue
})
return isExt
}
// IsExec returns true if file is an executable file
func (fn *Node) IsExec() bool {
return fn.Info.IsExec()
}
// isOpen returns true if file is flagged as open
func (fn *Node) isOpen() bool {
return !fn.Closed
}
// IsNotSaved returns true if the file is open and has been changed (edited) since last Save
func (fn *Node) IsNotSaved() bool {
return fn.Buffer != nil && fn.Buffer.IsNotSaved()
}
// isAutoSave returns true if file is an auto-save file (starts and ends with #)
func (fn *Node) isAutoSave() bool {
return strings.HasPrefix(fn.Info.Name, "#") && strings.HasSuffix(fn.Info.Name, "#")
}
// RelativePath returns the relative path from root for this node
func (fn *Node) RelativePath() string {
if fn.IsIrregular() || fn.FileRoot == nil {
return fn.Name
}
return fsx.RelativeFilePath(string(fn.Filepath), string(fn.FileRoot.Filepath))
}
// dirFileList returns the list of files in this directory,
// sorted according to DirsOnTop and SortByModTime options
func (fn *Node) dirFileList() []fs.FileInfo {
path := string(fn.Filepath)
var files []fs.FileInfo
var dirs []fs.FileInfo // for DirsOnTop mode
var di []fs.DirEntry
if fn.FileRoot.FS == nil {
di = errors.Log1(os.ReadDir(path))
} else {
di = errors.Log1(fs.ReadDir(fn.FileRoot.FS, path))
}
for _, d := range di {
info := errors.Log1(d.Info())
if fn.FileRoot.DirsOnTop {
if d.IsDir() {
dirs = append(dirs, info)
} else {
files = append(files, info)
}
} else {
files = append(files, info)
}
}
doModSort := fn.FileRoot.SortByModTime
if doModSort {
doModSort = !fn.FileRoot.dirSortByName(core.Filename(path))
} else {
doModSort = fn.FileRoot.dirSortByModTime(core.Filename(path))
}
if fn.FileRoot.DirsOnTop {
if doModSort {
sortByModTime(dirs)
sortByModTime(files)
}
files = append(dirs, files...)
} else {
if doModSort {
sortByModTime(files)
}
}
return files
}
func sortByModTime(files []fs.FileInfo) {
slices.SortFunc(files, func(a, b fs.FileInfo) int {
return a.ModTime().Compare(b.ModTime())
})
}
func (fn *Node) setFileIcon() {
if fn.Info.Ic == "" {
ic, hasic := fn.Info.FindIcon()
if hasic {
fn.Info.Ic = ic
} else {
fn.Info.Ic = icons.Blank
}
}
fn.IconLeaf = fn.Info.Ic
if br := fn.Branch; br != nil {
if br.IconIndeterminate != fn.IconLeaf {
br.SetIconOn(icons.FolderOpen).SetIconOff(icons.Folder).SetIconIndeterminate(fn.IconLeaf)
br.UpdateTree()
}
}
}
// GetFileInfo is a Filer interface method that can be overwritten
// to do custom file info.
func (fn *Node) GetFileInfo() error {
return fn.InitFileInfo()
}
// InitFileInfo initializes file info
func (fn *Node) InitFileInfo() error {
if fn.Filepath == "" {
return nil
}
var err error
if fn.FileRoot.FS == nil { // deal with symlinks
ls, err := os.Lstat(string(fn.Filepath))
if errors.Log(err) != nil {
return err
}
if ls.Mode()&os.ModeSymlink != 0 {
effpath, err := filepath.EvalSymlinks(string(fn.Filepath))
if err != nil {
// this happens too often for links -- skip
// log.Printf("filetree.Node Path: %v could not be opened -- error: %v\n", fn.Filepath, err)
return err
}
fn.Filepath = core.Filename(effpath)
}
err = fn.Info.InitFile(string(fn.Filepath))
} else {
err = fn.Info.InitFileFS(fn.FileRoot.FS, string(fn.Filepath))
}
if err != nil {
emsg := fmt.Errorf("filetree.Node InitFileInfo Path %q: Error: %v", fn.Filepath, err)
log.Println(emsg)
return emsg
}
repo, rnode := fn.Repo()
if repo != nil {
if fn.IsDir() {
fn.Info.VCS = vcs.Stored // always
} else {
rstat := rnode.repoFiles.Status(repo, string(fn.Filepath))
if rstat != fn.Info.VCS {
fn.Info.VCS = rstat
fn.NeedsRender()
}
}
} else {
fn.Info.VCS = vcs.Stored
}
return nil
}
// SelectedFunc runs the given function on all selected nodes in reverse order.
func (fn *Node) SelectedFunc(fun func(n *Node)) {
sels := fn.GetSelectedNodes()
for i := len(sels) - 1; i >= 0; i-- {
sn := AsNode(sels[i])
if sn == nil {
continue
}
fun(sn)
}
}
func (fn *Node) OnOpen() {
fn.openDir()
}
func (fn *Node) OnClose() {
if !fn.IsDir() {
return
}
fn.FileRoot.setDirClosed(fn.Filepath)
}
func (fn *Node) CanOpen() bool {
return fn.HasChildren() || fn.IsDir()
}
// openDir opens given directory node
func (fn *Node) openDir() {
if !fn.IsDir() {
return
}
fn.FileRoot.setDirOpen(fn.Filepath)
fn.Update()
}
// sortBys determines how to sort the selected files in the directory.
// Default is alpha by name, optionally can be sorted by modification time.
func (fn *Node) sortBys(modTime bool) { //types:add
fn.SelectedFunc(func(sn *Node) {
sn.sortBy(modTime)
})
}
// sortBy determines how to sort the files in the directory -- default is alpha by name,
// optionally can be sorted by modification time.
func (fn *Node) sortBy(modTime bool) {
fn.FileRoot.setDirSortBy(fn.Filepath, modTime)
fn.Update()
}
// openAll opens all directories under this one
func (fn *Node) openAll() { //types:add
fn.FileRoot.inOpenAll = true // causes chaining of opening
fn.Tree.OpenAll()
fn.FileRoot.inOpenAll = false
}
// OpenBuf opens the file in its buffer if it is not already open.
// returns true if file is newly opened
func (fn *Node) OpenBuf() (bool, error) {
if fn.IsDir() {
err := fmt.Errorf("filetree.Node cannot open directory in editor: %v", fn.Filepath)
log.Println(err)
return false, err
}
if fn.Buffer != nil {
if fn.Buffer.Filename == fn.Filepath { // close resets filename
return false, nil
}
} else {
fn.Buffer = texteditor.NewBuffer()
fn.Buffer.OnChange(func(e events.Event) {
if fn.Info.VCS == vcs.Stored {
fn.Info.VCS = vcs.Modified
}
})
}
fn.Buffer.SetHighlighting(NodeHighlighting)
return true, fn.Buffer.Open(fn.Filepath)
}
// removeFromExterns removes file from list of external files
func (fn *Node) removeFromExterns() { //types:add
fn.SelectedFunc(func(sn *Node) {
if !sn.isExternal() {
return
}
sn.FileRoot.removeExternalFile(string(sn.Filepath))
sn.closeBuf()
sn.Delete()
})
}
// closeBuf closes the file in its buffer if it is open.
// returns true if closed.
func (fn *Node) closeBuf() bool {
if fn.Buffer == nil {
return false
}
fn.Buffer.Close(nil)
fn.Buffer = nil
return true
}
// RelativePathFrom returns the relative path from node for given full path
func (fn *Node) RelativePathFrom(fpath core.Filename) string {
return fsx.RelativeFilePath(string(fpath), string(fn.Filepath))
}
// dirsTo opens all the directories above the given filename, and returns the node
// for element at given path (can be a file or directory itself -- not opened -- just returned)
func (fn *Node) dirsTo(path string) (*Node, error) {
pth, err := filepath.Abs(path)
if err != nil {
log.Printf("filetree.Node DirsTo path %v could not be turned into an absolute path: %v\n", path, err)
return nil, err
}
rpath := fn.RelativePathFrom(core.Filename(pth))
if rpath == "." {
return fn, nil
}
dirs := strings.Split(rpath, string(filepath.Separator))
cfn := fn
sz := len(dirs)
for i := 0; i < sz; i++ {
dr := dirs[i]
sfni := cfn.ChildByName(dr, 0)
if sfni == nil {
if i == sz-1 { // ok for terminal -- might not exist yet
return cfn, nil
} else {
err = fmt.Errorf("filetree.Node could not find node %v in: %v, orig: %v, rel: %v", dr, cfn.Filepath, pth, rpath)
// slog.Error(err.Error()) // note: this is expected sometimes
return nil, err
}
}
sfn := AsNode(sfni)
if sfn.IsDir() || i == sz-1 {
if i < sz-1 && !sfn.isOpen() {
sfn.openDir()
} else {
cfn = sfn
}
} else {
err := fmt.Errorf("filetree.Node non-terminal node %v is not a directory in: %v", dr, cfn.Filepath)
slog.Error(err.Error())
return nil, err
}
cfn = sfn
}
return cfn, nil
}
// Copyright (c) 2024, 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 filetree
import (
"fmt"
"io/fs"
"log"
"path/filepath"
"regexp"
"sort"
"strings"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/core"
"cogentcore.org/core/texteditor/text"
"cogentcore.org/core/tree"
)
// FindLocation corresponds to the search scope
type FindLocation int32 //enums:enum -trim-prefix FindLocation
const (
// FindOpen finds in all open folders in the left file browser
FindLocationOpen FindLocation = iota
// FindLocationAll finds in all directories under the root path. can be slow for large file trees
FindLocationAll
// FindLocationFile only finds in the current active file
FindLocationFile
// FindLocationDir only finds in the directory of the current active file
FindLocationDir
// FindLocationNotTop finds in all open folders *except* the top-level folder
FindLocationNotTop
)
// SearchResults is used to report search results
type SearchResults struct {
Node *Node
Count int
Matches []text.Match
}
// Search returns list of all nodes starting at given node of given
// language(s) that contain the given string, sorted in descending order by number
// of occurrences; ignoreCase transforms everything into lowercase
func Search(start *Node, find string, ignoreCase, regExp bool, loc FindLocation, activeDir string, langs []fileinfo.Known, openPath func(path string) *Node) []SearchResults {
fb := []byte(find)
fsz := len(find)
if fsz == 0 {
return nil
}
if loc == FindLocationAll {
return findAll(start, find, ignoreCase, regExp, langs, openPath)
}
var re *regexp.Regexp
var err error
if regExp {
re, err = regexp.Compile(find)
if err != nil {
log.Println(err)
return nil
}
}
mls := make([]SearchResults, 0)
start.WalkDown(func(k tree.Node) bool {
sfn := AsNode(k)
if sfn == nil {
return tree.Continue
}
if sfn.IsDir() && !sfn.isOpen() {
// fmt.Printf("dir: %v closed\n", sfn.FPath)
return tree.Break // don't go down into closed directories!
}
if sfn.IsDir() || sfn.IsExec() || sfn.Info.Kind == "octet-stream" || sfn.isAutoSave() {
// fmt.Printf("dir: %v opened\n", sfn.Nm)
return tree.Continue
}
if int(sfn.Info.Size) > core.SystemSettings.BigFileSize {
return tree.Continue
}
if strings.HasSuffix(sfn.Name, ".code") { // exclude self
return tree.Continue
}
if !fileinfo.IsMatchList(langs, sfn.Info.Known) {
return tree.Continue
}
if loc == FindLocationDir {
cdir, _ := filepath.Split(string(sfn.Filepath))
if activeDir != cdir {
return tree.Continue
}
} else if loc == FindLocationNotTop {
// if level == 1 { // todo
// return tree.Continue
// }
}
var cnt int
var matches []text.Match
if sfn.isOpen() && sfn.Buffer != nil {
if regExp {
cnt, matches = sfn.Buffer.SearchRegexp(re)
} else {
cnt, matches = sfn.Buffer.Search(fb, ignoreCase, false)
}
} else {
if regExp {
cnt, matches = text.SearchFileRegexp(string(sfn.Filepath), re)
} else {
cnt, matches = text.SearchFile(string(sfn.Filepath), fb, ignoreCase)
}
}
if cnt > 0 {
mls = append(mls, SearchResults{sfn, cnt, matches})
}
return tree.Continue
})
sort.Slice(mls, func(i, j int) bool {
return mls[i].Count > mls[j].Count
})
return mls
}
// findAll returns list of all files (regardless of what is currently open)
// starting at given node of given language(s) that contain the given string,
// sorted in descending order by number of occurrences. ignoreCase transforms
// everything into lowercase.
func findAll(start *Node, find string, ignoreCase, regExp bool, langs []fileinfo.Known, openPath func(path string) *Node) []SearchResults {
fb := []byte(find)
fsz := len(find)
if fsz == 0 {
return nil
}
var re *regexp.Regexp
var err error
if regExp {
re, err = regexp.Compile(find)
if err != nil {
log.Println(err)
return nil
}
}
mls := make([]SearchResults, 0)
spath := string(start.Filepath) // note: is already Abs
filepath.Walk(spath, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.Name() == ".git" {
return filepath.SkipDir
}
if info.IsDir() {
return nil
}
if int(info.Size()) > core.SystemSettings.BigFileSize {
return nil
}
if strings.HasSuffix(info.Name(), ".code") { // exclude self
return nil
}
if len(langs) > 0 {
mtyp, _, err := fileinfo.MimeFromFile(path)
if err != nil {
return nil
}
known := fileinfo.MimeKnown(mtyp)
if !fileinfo.IsMatchList(langs, known) {
return nil
}
}
ofn := openPath(path)
var cnt int
var matches []text.Match
if ofn != nil && ofn.Buffer != nil {
if regExp {
cnt, matches = ofn.Buffer.SearchRegexp(re)
} else {
cnt, matches = ofn.Buffer.Search(fb, ignoreCase, false)
}
} else {
if regExp {
cnt, matches = text.SearchFileRegexp(path, re)
} else {
cnt, matches = text.SearchFile(path, fb, ignoreCase)
}
}
if cnt > 0 {
if ofn != nil {
mls = append(mls, SearchResults{ofn, cnt, matches})
} else {
sfn, found := start.FindFile(path)
if found {
mls = append(mls, SearchResults{sfn, cnt, matches})
} else {
fmt.Println("file not found in FindFile:", path)
}
}
}
return nil
})
sort.Slice(mls, func(i, j int) bool {
return mls[i].Count > mls[j].Count
})
return mls
}
// 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 filetree
import (
"fmt"
"io/fs"
"log/slog"
"os"
"path/filepath"
"time"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/vcs"
"cogentcore.org/core/core"
"cogentcore.org/core/system"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
"github.com/fsnotify/fsnotify"
)
const (
// externalFilesName is the name of the node that represents external files
externalFilesName = "[external files]"
)
// Tree is the root widget of a file tree representing files in a given directory
// (and subdirectories thereof), and has some overall management state for how to
// view things.
type Tree struct {
Node
// externalFiles are external files outside the root path of the tree.
// They are stored in terms of their absolute paths. These are shown
// in the first sub-node if present; use [Tree.AddExternalFile] to add one.
externalFiles []string
// Dirs records state of directories within the tree (encoded using paths relative to root),
// e.g., open (have been opened by the user) -- can persist this to restore prior view of a tree
Dirs DirFlagMap `set:"-"`
// DirsOnTop indicates whether all directories are placed at the top of the tree.
// Otherwise everything is mixed. This is the default.
DirsOnTop bool
// SortByModTime causes files to be sorted by modification time by default.
// Otherwise it is a per-directory option.
SortByModTime bool
// FileNodeType is the type of node to create; defaults to [Node] but can use custom node types
FileNodeType *types.Type `display:"-" json:"-" xml:"-"`
// FilterFunc, if set, determines whether to include the given node in the tree.
// return true to include, false to not. This applies to files and directories alike.
FilterFunc func(path string, info fs.FileInfo) bool
// FS is the file system we are browsing, if it is an FS (nil = os filesystem)
FS fs.FS
// inOpenAll indicates whether we are in midst of an OpenAll call; nodes should open all dirs.
inOpenAll bool
// watcher does change notify for all dirs
watcher *fsnotify.Watcher
// doneWatcher is channel to close watcher watcher
doneWatcher chan bool
// watchedPaths is map of paths that have been added to watcher; only active if bool = true
watchedPaths map[string]bool
// lastWatchUpdate is last path updated by watcher
lastWatchUpdate string
// lastWatchTime is timestamp of last update
lastWatchTime time.Time
}
func (ft *Tree) Init() {
ft.Node.Init()
ft.FileRoot = ft
ft.FileNodeType = types.For[Node]()
ft.OpenDepth = 4
ft.DirsOnTop = true
ft.FirstMaker(func(p *tree.Plan) {
tree.AddNew(p, externalFilesName, func() Filer {
return tree.NewOfType(ft.FileNodeType).(Filer)
}, func(wf Filer) {
w := wf.AsFileNode()
w.FileRoot = ft
w.Filepath = externalFilesName
w.Info.Mode = os.ModeDir
w.Info.VCS = vcs.Stored
})
})
}
func (fv *Tree) Destroy() {
if fv.watcher != nil {
fv.watcher.Close()
fv.watcher = nil
}
if fv.doneWatcher != nil {
fv.doneWatcher <- true
close(fv.doneWatcher)
fv.doneWatcher = nil
}
fv.Tree.Destroy()
}
// OpenPath opens the filetree at the given os file system directory path.
// It reads all the files at the given path into this tree.
// Only paths listed in [Tree.Dirs] will be opened.
func (ft *Tree) OpenPath(path string) *Tree {
if ft.FileNodeType == nil {
ft.FileNodeType = types.For[Node]()
}
effpath, err := filepath.EvalSymlinks(path)
if err != nil {
effpath = path
}
abs, err := filepath.Abs(effpath)
if errors.Log(err) != nil {
abs = effpath
}
ft.FS = nil
ft.Filepath = core.Filename(abs)
ft.setDirOpen(core.Filename(abs))
ft.detectVCSRepo(true)
ft.This.(Filer).GetFileInfo()
ft.Open()
ft.Update()
return ft
}
// OpenPathFS opens the filetree at the given [fs] file system directory path.
// It reads all the files at the given path into this tree.
// Only paths listed in [Tree.Dirs] will be opened.
func (ft *Tree) OpenPathFS(fsys fs.FS, path string) *Tree {
if ft.FileNodeType == nil {
ft.FileNodeType = types.For[Node]()
}
ft.FS = fsys
ft.Filepath = core.Filename(path)
ft.setDirOpen(core.Filename(path))
ft.This.(Filer).GetFileInfo()
ft.Open()
ft.Update()
return ft
}
// UpdatePath updates the tree at the directory level for given path
// and everything below it. It flags that it needs render update,
// but if a deletion or insertion happened, then NeedsLayout should also
// be called.
func (ft *Tree) UpdatePath(path string) {
ft.NeedsRender()
path = filepath.Clean(path)
ft.dirsTo(path)
if fn, ok := ft.FindFile(path); ok {
if fn.IsDir() {
fn.Update()
return
}
}
fpath, _ := filepath.Split(path)
if fn, ok := ft.FindFile(fpath); ok {
fn.Update()
return
}
// core.MessageSnackbar(ft, "UpdatePath: path not found in tree: "+path)
}
// configWatcher configures a new watcher for tree
func (ft *Tree) configWatcher() error {
if ft.watcher != nil {
return nil
}
ft.watchedPaths = make(map[string]bool)
var err error
ft.watcher, err = fsnotify.NewWatcher()
return err
}
// watchWatcher monitors the watcher channel for update events.
// It must be called once some paths have been added to watcher --
// safe to call multiple times.
func (ft *Tree) watchWatcher() {
if ft.watcher == nil || ft.watcher.Events == nil {
return
}
if ft.doneWatcher != nil {
return
}
ft.doneWatcher = make(chan bool)
go func() {
watch := ft.watcher
done := ft.doneWatcher
for {
select {
case <-done:
return
case event := <-watch.Events:
switch {
case event.Op&fsnotify.Create == fsnotify.Create ||
event.Op&fsnotify.Remove == fsnotify.Remove ||
event.Op&fsnotify.Rename == fsnotify.Rename:
ft.watchUpdate(event.Name)
}
case err := <-watch.Errors:
_ = err
}
}
}()
}
// watchUpdate does the update for given path
func (ft *Tree) watchUpdate(path string) {
ft.AsyncLock()
defer ft.AsyncUnlock()
// fmt.Println(path)
dir, _ := filepath.Split(path)
rp := ft.RelativePathFrom(core.Filename(dir))
if rp == ft.lastWatchUpdate {
now := time.Now()
lagMs := int(now.Sub(ft.lastWatchTime) / time.Millisecond)
if lagMs < 100 {
// fmt.Printf("skipping update to: %s due to lag: %v\n", rp, lagMs)
return // no update
}
}
fn, err := ft.findDirNode(rp)
if err != nil {
// slog.Error(err.Error())
return
}
ft.lastWatchUpdate = rp
ft.lastWatchTime = time.Now()
if !fn.isOpen() {
// fmt.Printf("warning: watcher updating closed node: %s\n", rp)
return // shouldn't happen
}
fn.Update()
}
// watchPath adds given path to those watched
func (ft *Tree) watchPath(path core.Filename) error {
return nil // TODO(#424): disable for all platforms for now; causing issues
if core.TheApp.Platform() == system.MacOS {
return nil // mac is not supported in a high-capacity fashion at this point
}
rp := ft.RelativePathFrom(path)
on, has := ft.watchedPaths[rp]
if on || has {
return nil
}
ft.configWatcher()
// fmt.Printf("watching path: %s\n", path)
err := ft.watcher.Add(string(path))
if err == nil {
ft.watchedPaths[rp] = true
ft.watchWatcher()
} else {
slog.Error(err.Error())
}
return err
}
// unWatchPath removes given path from those watched
func (ft *Tree) unWatchPath(path core.Filename) {
rp := ft.RelativePathFrom(path)
on, has := ft.watchedPaths[rp]
if !on || !has {
return
}
ft.configWatcher()
ft.watcher.Remove(string(path))
ft.watchedPaths[rp] = false
}
// isDirOpen returns true if given directory path is open (i.e., has been
// opened in the view)
func (ft *Tree) isDirOpen(fpath core.Filename) bool {
if fpath == ft.Filepath { // we are always open
return true
}
return ft.Dirs.isOpen(ft.RelativePathFrom(fpath))
}
// setDirOpen sets the given directory path to be open
func (ft *Tree) setDirOpen(fpath core.Filename) {
rp := ft.RelativePathFrom(fpath)
// fmt.Printf("setdiropen: %s\n", rp)
ft.Dirs.setOpen(rp, true)
ft.watchPath(fpath)
}
// setDirClosed sets the given directory path to be closed
func (ft *Tree) setDirClosed(fpath core.Filename) {
rp := ft.RelativePathFrom(fpath)
ft.Dirs.setOpen(rp, false)
ft.unWatchPath(fpath)
}
// setDirSortBy sets the given directory path sort by option
func (ft *Tree) setDirSortBy(fpath core.Filename, modTime bool) {
ft.Dirs.setSortBy(ft.RelativePathFrom(fpath), modTime)
}
// dirSortByModTime returns true if dir is sorted by mod time
func (ft *Tree) dirSortByModTime(fpath core.Filename) bool {
return ft.Dirs.sortByModTime(ft.RelativePathFrom(fpath))
}
// dirSortByName returns true if dir is sorted by name
func (ft *Tree) dirSortByName(fpath core.Filename) bool {
return ft.Dirs.sortByName(ft.RelativePathFrom(fpath))
}
// AddExternalFile adds an external file outside of root of file tree
// and triggers an update, returning the Node for it, or
// error if [filepath.Abs] fails.
func (ft *Tree) AddExternalFile(fpath string) (*Node, error) {
pth, err := filepath.Abs(fpath)
if err != nil {
return nil, err
}
if _, err := os.Stat(pth); err != nil {
return nil, err
}
if has, _ := ft.hasExternalFile(pth); has {
return ft.externalNodeByPath(pth)
}
ft.externalFiles = append(ft.externalFiles, pth)
ft.Child(0).(Filer).AsFileNode().Update()
return ft.externalNodeByPath(pth)
}
// removeExternalFile removes external file from maintained list; returns true if removed.
func (ft *Tree) removeExternalFile(fpath string) bool {
for i, ef := range ft.externalFiles {
if ef == fpath {
ft.externalFiles = append(ft.externalFiles[:i], ft.externalFiles[i+1:]...)
return true
}
}
return false
}
// hasExternalFile returns true and index if given abs path exists on ExtFiles list.
// false and -1 if not.
func (ft *Tree) hasExternalFile(fpath string) (bool, int) {
for i, f := range ft.externalFiles {
if f == fpath {
return true, i
}
}
return false, -1
}
// externalNodeByPath returns Node for given file path, and true, if it
// exists in the external files list. Otherwise returns nil, false.
func (ft *Tree) externalNodeByPath(fpath string) (*Node, error) {
ehas, i := ft.hasExternalFile(fpath)
if !ehas {
return nil, fmt.Errorf("ExtFile not found on list: %v", fpath)
}
ekid := ft.ChildByName(externalFilesName, 0)
if ekid == nil {
return nil, fmt.Errorf("ExtFile not updated -- no ExtFiles node")
}
if n := ekid.AsTree().Child(i); n != nil {
return AsNode(n), nil
}
return nil, fmt.Errorf("ExtFile not updated; index invalid")
}
// Code generated by "core generate"; DO NOT EDIT.
package filetree
import (
"cogentcore.org/core/base/vcs"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
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.", 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."}}})
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: "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: "Buffer", Doc: "Buffer is the file buffer for editing this file."}, {Name: "FileRoot", Doc: "FileRoot is the root [Tree] of the tree, which has global state."}, {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."}}})
// NewNode returns a new [Node] with the given optional parent:
// Node represents a file in the file system, as a [core.Tree] node.
// The name of the node is the name of the file.
// Folders have children containing further nodes.
func NewNode(parent ...tree.Node) *Node { return tree.New[Node](parent...) }
// NodeEmbedder is an interface that all types that embed Node satisfy
type NodeEmbedder interface {
AsNode() *Node
}
// AsNode returns the given value as a value of type Node if the type
// of the given value embeds Node, or nil otherwise
func AsNode(n tree.Node) *Node {
if t, ok := n.(NodeEmbedder); ok {
return t.AsNode()
}
return nil
}
// AsNode satisfies the [NodeEmbedder] interface
func (t *Node) AsNode() *Node { return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/filetree.Tree", IDName: "tree", Doc: "Tree is the root widget of a file tree representing files in a given directory\n(and subdirectories thereof), and has some overall management state for how to\nview things.", Embeds: []types.Field{{Name: "Node"}}, Fields: []types.Field{{Name: "externalFiles", Doc: "externalFiles are external files outside the root path of the tree.\nThey are stored in terms of their absolute paths. These are shown\nin the first sub-node if present; use [Tree.AddExternalFile] to add one."}, {Name: "Dirs", Doc: "records state of directories within the tree (encoded using paths relative to root),\ne.g., open (have been opened by the user) -- can persist this to restore prior view of a tree"}, {Name: "DirsOnTop", Doc: "if true, then all directories are placed at the top of the tree.\nOtherwise everything is mixed."}, {Name: "FileNodeType", Doc: "type of node to create; defaults to [Node] but can use custom node types"}, {Name: "inOpenAll", Doc: "if true, we are in midst of an OpenAll call; nodes should open all dirs"}, {Name: "watcher", Doc: "change notify for all dirs"}, {Name: "doneWatcher", Doc: "channel to close watcher watcher"}, {Name: "watchedPaths", Doc: "map of paths that have been added to watcher; only active if bool = true"}, {Name: "lastWatchUpdate", Doc: "last path updated by watcher"}, {Name: "lastWatchTime", Doc: "timestamp of last update"}}})
// NewTree returns a new [Tree] with the given optional parent:
// Tree is the root widget of a file tree representing files in a given directory
// (and subdirectories thereof), and has some overall management state for how to
// view things.
func NewTree(parent ...tree.Node) *Tree { return tree.New[Tree](parent...) }
// SetDirsOnTop sets the [Tree.DirsOnTop]:
// if true, then all directories are placed at the top of the tree.
// Otherwise everything is mixed.
func (t *Tree) SetDirsOnTop(v bool) *Tree { t.DirsOnTop = v; return t }
// SetFileNodeType sets the [Tree.FileNodeType]:
// type of node to create; defaults to [Node] but can use custom node types
func (t *Tree) SetFileNodeType(v *types.Type) *Tree { t.FileNodeType = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/filetree.VCSLog", IDName: "vcs-log", Doc: "VCSLog is a widget that represents VCS log data.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Log", Doc: "current log"}, {Name: "File", Doc: "file that this is a log of -- if blank then it is entire repository"}, {Name: "Since", Doc: "date expression for how long ago to include log entries from"}, {Name: "Repo", Doc: "version control system repository"}, {Name: "revisionA", Doc: "revision A -- defaults to HEAD"}, {Name: "revisionB", Doc: "revision B -- blank means current working copy"}, {Name: "setA", Doc: "double-click will set the A revision -- else B"}, {Name: "arev"}, {Name: "brev"}, {Name: "atf"}, {Name: "btf"}}})
// NewVCSLog returns a new [VCSLog] with the given optional parent:
// VCSLog is a widget that represents VCS log data.
func NewVCSLog(parent ...tree.Node) *VCSLog { return tree.New[VCSLog](parent...) }
// SetLog sets the [VCSLog.Log]:
// current log
func (t *VCSLog) SetLog(v vcs.Log) *VCSLog { t.Log = v; return t }
// SetFile sets the [VCSLog.File]:
// file that this is a log of -- if blank then it is entire repository
func (t *VCSLog) SetFile(v string) *VCSLog { t.File = v; return t }
// SetSince sets the [VCSLog.Since]:
// date expression for how long ago to include log entries from
func (t *VCSLog) SetSince(v string) *VCSLog { t.Since = v; return t }
// SetRepo sets the [VCSLog.Repo]:
// version control system repository
func (t *VCSLog) SetRepo(v vcs.Repo) *VCSLog { t.Repo = v; return t }
// 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 filetree
import (
"bytes"
"fmt"
"log/slog"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/vcs"
"cogentcore.org/core/core"
"cogentcore.org/core/styles"
"cogentcore.org/core/texteditor"
"cogentcore.org/core/texteditor/text"
"cogentcore.org/core/tree"
)
// FirstVCS returns the first VCS repository starting from this node and going down.
// also returns the node having that repository
func (fn *Node) FirstVCS() (vcs.Repo, *Node) {
if fn.FileRoot.FS != nil {
return nil, nil
}
var repo vcs.Repo
var rnode *Node
fn.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool {
sfn := AsNode(cw)
if sfn == nil {
return tree.Continue
}
sfn.detectVCSRepo(false)
if sfn.DirRepo != nil {
repo = sfn.DirRepo
rnode = sfn
return tree.Break
}
return tree.Continue
})
return repo, rnode
}
// detectVCSRepo detects and configures DirRepo if this directory is root of
// a VCS repository. if updateFiles is true, gets the files in the dir.
// returns true if a repository was newly found here.
func (fn *Node) detectVCSRepo(updateFiles bool) bool {
repo, _ := fn.Repo()
if repo != nil {
return false
}
path := string(fn.Filepath)
rtyp := vcs.DetectRepo(path)
if rtyp == vcs.NoVCS {
return false
}
var err error
repo, err = vcs.NewRepo("origin", path)
if err != nil {
slog.Error(err.Error())
return false
}
fn.DirRepo = repo
if updateFiles {
fn.updateRepoFiles()
}
return true
}
// Repo returns the version control repository associated with this file,
// and the node for the directory where the repo is based.
// Goes up the tree until a repository is found.
func (fn *Node) Repo() (vcs.Repo, *Node) {
if fn.isExternal() || fn.FileRoot.FS != nil {
return nil, nil
}
if fn.DirRepo != nil {
return fn.DirRepo, fn
}
var repo vcs.Repo
var rnode *Node
fn.WalkUpParent(func(k tree.Node) bool {
sfn := AsNode(k)
if sfn == nil {
return tree.Break
}
if sfn.IsIrregular() {
return tree.Break
}
if sfn.DirRepo != nil {
repo = sfn.DirRepo
rnode = sfn
return tree.Break
}
return tree.Continue
})
return repo, rnode
}
func (fn *Node) updateRepoFiles() {
if fn.DirRepo == nil {
return
}
fn.repoFiles, _ = fn.DirRepo.Files()
}
// addToVCSSelected adds selected files to version control system
func (fn *Node) addToVCSSelected() { //types:add
fn.SelectedFunc(func(sn *Node) {
sn.AddToVCS()
})
}
// AddToVCS adds file to version control
func (fn *Node) AddToVCS() {
repo, _ := fn.Repo()
if repo == nil {
return
}
// fmt.Printf("adding to vcs: %v\n", fn.FPath)
err := repo.Add(string(fn.Filepath))
if errors.Log(err) == nil {
fn.Info.VCS = vcs.Added
fn.Update()
}
}
// deleteFromVCSSelected removes selected files from version control system
func (fn *Node) deleteFromVCSSelected() { //types:add
fn.SelectedFunc(func(sn *Node) {
sn.deleteFromVCS()
})
}
// deleteFromVCS removes file from version control
func (fn *Node) deleteFromVCS() {
repo, _ := fn.Repo()
if repo == nil {
return
}
// fmt.Printf("deleting remote from vcs: %v\n", fn.FPath)
err := repo.DeleteRemote(string(fn.Filepath))
if fn != nil && errors.Log(err) == nil {
fn.Info.VCS = vcs.Deleted
fn.Update()
}
}
// commitToVCSSelected commits to version control system based on last selected file
func (fn *Node) commitToVCSSelected() { //types:add
done := false
fn.SelectedFunc(func(sn *Node) {
if !done {
core.CallFunc(sn, fn.commitToVCS)
done = true
}
})
}
// commitToVCS commits file changes to version control system
func (fn *Node) commitToVCS(message string) (err error) {
repo, _ := fn.Repo()
if repo == nil {
return
}
if fn.Info.VCS == vcs.Untracked {
return errors.New("file not in vcs repo: " + string(fn.Filepath))
}
err = repo.CommitFile(string(fn.Filepath), message)
if err != nil {
return err
}
fn.Info.VCS = vcs.Stored
fn.Update()
return err
}
// revertVCSSelected removes selected files from version control system
func (fn *Node) revertVCSSelected() { //types:add
fn.SelectedFunc(func(sn *Node) {
sn.revertVCS()
})
}
// revertVCS reverts file changes since last commit
func (fn *Node) revertVCS() (err error) {
repo, _ := fn.Repo()
if repo == nil {
return
}
if fn.Info.VCS == vcs.Untracked {
return errors.New("file not in vcs repo: " + string(fn.Filepath))
}
err = repo.RevertFile(string(fn.Filepath))
if err != nil {
return err
}
if fn.Info.VCS == vcs.Modified {
fn.Info.VCS = vcs.Stored
} else if fn.Info.VCS == vcs.Added {
// do nothing - leave in "added" state
}
if fn.Buffer != nil {
fn.Buffer.Revert()
}
fn.Update()
return err
}
// diffVCSSelected shows the diffs between two versions of selected files, given by the
// revision specifiers -- if empty, defaults to A = current HEAD, B = current WC file.
// -1, -2 etc also work as universal ways of specifying prior revisions.
// Diffs are shown in a DiffEditorDialog.
func (fn *Node) diffVCSSelected(rev_a string, rev_b string) { //types:add
fn.SelectedFunc(func(sn *Node) {
sn.diffVCS(rev_a, rev_b)
})
}
// diffVCS shows the diffs between two versions of this file, given by the
// revision specifiers -- if empty, defaults to A = current HEAD, B = current WC file.
// -1, -2 etc also work as universal ways of specifying prior revisions.
// Diffs are shown in a DiffEditorDialog.
func (fn *Node) diffVCS(rev_a, rev_b string) error {
repo, _ := fn.Repo()
if repo == nil {
return errors.New("file not in vcs repo: " + string(fn.Filepath))
}
if fn.Info.VCS == vcs.Untracked {
return errors.New("file not in vcs repo: " + string(fn.Filepath))
}
_, err := texteditor.DiffEditorDialogFromRevs(fn, repo, string(fn.Filepath), fn.Buffer, rev_a, rev_b)
return err
}
// logVCSSelected shows the VCS log of commits for selected files.
func (fn *Node) logVCSSelected() { //types:add
fn.SelectedFunc(func(sn *Node) {
sn.LogVCS(false, "")
})
}
// LogVCS shows the VCS log of commits for this file, optionally with a
// since date qualifier: If since is non-empty, it should be
// a date-like expression that the VCS will understand, such as
// 1/1/2020, yesterday, last year, etc. SVN only understands a
// number as a maximum number of items to return.
// If allFiles is true, then the log will show revisions for all files, not just
// this one.
// Returns the Log and also shows it in a VCSLog which supports further actions.
func (fn *Node) LogVCS(allFiles bool, since string) (vcs.Log, error) {
repo, _ := fn.Repo()
if repo == nil {
return nil, errors.New("file not in vcs repo: " + string(fn.Filepath))
}
if fn.Info.VCS == vcs.Untracked {
return nil, errors.New("file not in vcs repo: " + string(fn.Filepath))
}
fnm := string(fn.Filepath)
if allFiles {
fnm = ""
}
lg, err := repo.Log(fnm, since)
if err != nil {
return lg, err
}
vcsLogDialog(nil, repo, lg, fnm, since)
return lg, nil
}
// blameVCSSelected shows the VCS blame report for this file, reporting for each line
// the revision and author of the last change.
func (fn *Node) blameVCSSelected() { //types:add
fn.SelectedFunc(func(sn *Node) {
sn.blameVCS()
})
}
// blameDialog opens a dialog for displaying VCS blame data using textview.TwinViews.
// blame is the annotated blame code, while fbytes is the original file contents.
func blameDialog(ctx core.Widget, fname string, blame, fbytes []byte) *texteditor.TwinEditors {
title := "VCS Blame: " + fsx.DirAndFile(fname)
d := core.NewBody(title)
tv := texteditor.NewTwinEditors(d)
tv.SetSplits(.3, .7)
tv.SetFiles(fname, fname)
flns := bytes.Split(fbytes, []byte("\n"))
lns := bytes.Split(blame, []byte("\n"))
nln := min(len(lns), len(flns))
blns := make([][]byte, nln)
stidx := 0
for i := 0; i < nln; i++ {
fln := flns[i]
bln := lns[i]
if stidx == 0 {
if len(fln) == 0 {
stidx = len(bln)
} else {
stidx = bytes.LastIndex(bln, fln)
}
}
blns[i] = bln[:stidx]
}
btxt := bytes.Join(blns, []byte("\n")) // makes a copy, so blame is disposable now
tv.BufferA.SetText(btxt)
tv.BufferB.SetText(fbytes)
tv.Update()
tva, tvb := tv.Editors()
tva.Styler(func(s *styles.Style) {
s.Text.WhiteSpace = styles.WhiteSpacePre
s.Min.X.Ch(30)
s.Min.Y.Em(40)
})
tvb.Styler(func(s *styles.Style) {
s.Text.WhiteSpace = styles.WhiteSpacePre
s.Min.X.Ch(80)
s.Min.Y.Em(40)
})
d.AddOKOnly()
d.RunWindowDialog(ctx)
return tv
}
// blameVCS shows the VCS blame report for this file, reporting for each line
// the revision and author of the last change.
func (fn *Node) blameVCS() ([]byte, error) {
repo, _ := fn.Repo()
if repo == nil {
return nil, errors.New("file not in vcs repo: " + string(fn.Filepath))
}
if fn.Info.VCS == vcs.Untracked {
return nil, errors.New("file not in vcs repo: " + string(fn.Filepath))
}
fnm := string(fn.Filepath)
fb, err := text.FileBytes(fnm)
if err != nil {
return nil, err
}
blm, err := repo.Blame(fnm)
if err != nil {
return blm, err
}
blameDialog(nil, fnm, blm, fb)
return blm, nil
}
// UpdateAllVCS does an update on any repositories below this one in file tree
func (fn *Node) UpdateAllVCS() {
fn.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool {
sfn := AsNode(cw)
if sfn == nil {
return tree.Continue
}
if !sfn.IsDir() {
return tree.Continue
}
if sfn.DirRepo == nil {
if !sfn.detectVCSRepo(false) {
return tree.Continue
}
}
repo := sfn.DirRepo
fmt.Printf("Updating %v repository: %s from: %s\n", repo.Vcs(), sfn.RelativePath(), repo.Remote())
err := repo.Update()
if err != nil {
fmt.Printf("error: %v\n", err)
}
return tree.Break
})
}
// Copyright (c) 2020, 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 filetree
import (
"log/slog"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/vcs"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/texteditor"
"cogentcore.org/core/texteditor/diffbrowser"
"cogentcore.org/core/tree"
)
// VCSLog is a widget that represents VCS log data.
type VCSLog struct {
core.Frame
// current log
Log vcs.Log
// file that this is a log of -- if blank then it is entire repository
File string
// date expression for how long ago to include log entries from
Since string
// version control system repository
Repo vcs.Repo `json:"-" xml:"-" copier:"-"`
// revision A -- defaults to HEAD
revisionA string
// revision B -- blank means current working copy
revisionB string
// double-click will set the A revision -- else B
setA bool
arev, brev *core.Switch
atf, btf *core.TextField
}
func (lv *VCSLog) Init() {
lv.Frame.Init()
lv.revisionA = "HEAD"
lv.revisionB = ""
lv.setA = true
lv.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Grow.Set(1, 1)
})
tree.AddChildAt(lv, "toolbar", func(w *core.Toolbar) {
w.Maker(lv.makeToolbar)
})
tree.AddChildAt(lv, "log", func(w *core.Table) {
w.SetReadOnly(true)
w.SetSlice(&lv.Log)
w.AddContextMenu(func(m *core.Scene) {
core.NewButton(m).SetText("Set Revision A").
SetTooltip("Set Buffer A's revision to this").
OnClick(func(e events.Event) {
cmt := lv.Log[w.SelectedIndex]
lv.setRevisionA(cmt.Rev)
})
core.NewButton(m).SetText("Set Revision B").
SetTooltip("Set Buffer B's revision to this").
OnClick(func(e events.Event) {
cmt := lv.Log[w.SelectedIndex]
lv.setRevisionB(cmt.Rev)
})
core.NewButton(m).SetText("Copy Revision ID").
SetTooltip("Copies the revision number / hash for this").
OnClick(func(e events.Event) {
cmt := lv.Log[w.SelectedIndex]
w.Clipboard().Write(mimedata.NewText(cmt.Rev))
})
core.NewButton(m).SetText("View Revision").
SetTooltip("Views the file at this revision").
OnClick(func(e events.Event) {
cmt := lv.Log[w.SelectedIndex]
fileAtRevisionDialog(lv, lv.Repo, lv.File, cmt.Rev)
})
core.NewButton(m).SetText("Checkout Revision").
SetTooltip("Checks out this revision").
OnClick(func(e events.Event) {
cmt := lv.Log[w.SelectedIndex]
errors.Log(lv.Repo.UpdateVersion(cmt.Rev))
})
})
w.OnSelect(func(e events.Event) {
idx := w.SelectedIndex
if idx < 0 || idx >= len(lv.Log) {
return
}
cmt := lv.Log[idx]
if lv.setA {
lv.setRevisionA(cmt.Rev)
} else {
lv.setRevisionB(cmt.Rev)
}
lv.toggleRevision()
})
w.OnDoubleClick(func(e events.Event) {
idx := w.SelectedIndex
if idx < 0 || idx >= len(lv.Log) {
return
}
cmt := lv.Log[idx]
if lv.File != "" {
if lv.setA {
lv.setRevisionA(cmt.Rev)
} else {
lv.setRevisionB(cmt.Rev)
}
lv.toggleRevision()
}
cinfo, err := lv.Repo.CommitDesc(cmt.Rev, false)
if err != nil {
slog.Error(err.Error())
return
}
d := core.NewBody("Commit Info: " + cmt.Rev)
buf := texteditor.NewBuffer()
buf.Filename = core.Filename(lv.File)
buf.Options.LineNumbers = true
buf.Stat()
texteditor.NewEditor(d).SetBuffer(buf).Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
})
buf.SetText(cinfo)
d.AddBottomBar(func(bar *core.Frame) {
core.NewButton(bar).SetText("Copy to clipboard").SetIcon(icons.ContentCopy).
OnClick(func(e events.Event) {
d.Clipboard().Write(mimedata.NewTextBytes(cinfo))
})
d.AddOK(bar)
})
d.RunFullDialog(lv)
})
})
}
// setRevisionA sets the revision to use for buffer A
func (lv *VCSLog) setRevisionA(rev string) {
lv.revisionA = rev
lv.atf.Update()
}
// setRevisionB sets the revision to use for buffer B
func (lv *VCSLog) setRevisionB(rev string) {
lv.revisionB = rev
lv.btf.Update()
}
// toggleRevision switches the active revision to set
func (lv *VCSLog) toggleRevision() {
lv.setA = !lv.setA
lv.arev.UpdateRender()
lv.brev.UpdateRender()
}
func (lv *VCSLog) makeToolbar(p *tree.Plan) {
tree.Add(p, func(w *core.Text) {
w.SetText("File: " + fsx.DirAndFile(lv.File))
})
tree.AddAt(p, "a-rev", func(w *core.Switch) {
lv.arev = w
core.Bind(&lv.setA, w)
w.SetText("A Rev: ")
w.SetTooltip("If selected, clicking in log will set this A Revision to use for Diff")
w.OnChange(func(e events.Event) {
lv.brev.UpdateRender()
})
})
tree.AddAt(p, "a-tf", func(w *core.TextField) {
lv.atf = w
core.Bind(&lv.revisionA, w)
w.SetTooltip("A revision: typically this is the older, base revision to compare")
})
tree.Add(p, func(w *core.Button) {
w.SetText("View A").SetIcon(icons.Document).
SetTooltip("View file at revision A").
OnClick(func(e events.Event) {
fileAtRevisionDialog(lv, lv.Repo, lv.File, lv.revisionA)
})
})
tree.Add(p, func(w *core.Separator) {})
tree.AddAt(p, "b-rev", func(w *core.Switch) {
lv.brev = w
w.SetText("B Rev: ")
w.SetTooltip("If selected, clicking in log will set this B Revision to use for Diff")
w.Updater(func() {
w.SetChecked(!lv.setA)
})
w.OnChange(func(e events.Event) {
lv.setA = !w.IsChecked()
lv.arev.UpdateRender()
})
})
tree.AddAt(p, "b-tf", func(w *core.TextField) {
lv.btf = w
core.Bind(&lv.revisionB, w)
w.SetTooltip("B revision: typically this is the newer revision to compare. Leave blank for the current working directory.")
})
tree.Add(p, func(w *core.Button) {
w.SetText("View B").SetIcon(icons.Document).
SetTooltip("View file at revision B").
OnClick(func(e events.Event) {
fileAtRevisionDialog(lv, lv.Repo, lv.File, lv.revisionB)
})
})
tree.Add(p, func(w *core.Separator) {})
tree.Add(p, func(w *core.Button) {
w.SetText("Diff").SetIcon(icons.Difference).
SetTooltip("Show the diffs between two revisions; if blank, A is current HEAD, and B is current working copy").
OnClick(func(e events.Event) {
if lv.File == "" {
diffbrowser.NewDiffBrowserVCS(lv.Repo, lv.revisionA, lv.revisionB)
} else {
texteditor.DiffEditorDialogFromRevs(lv, lv.Repo, lv.File, nil, lv.revisionA, lv.revisionB)
}
})
})
}
// vcsLogDialog returns a VCS Log View for given repo, log and file (file could be empty)
func vcsLogDialog(ctx core.Widget, repo vcs.Repo, lg vcs.Log, file, since string) *core.Body {
title := "VCS Log: "
if file == "" {
title += "All files"
} else {
title += fsx.DirAndFile(file)
}
if since != "" {
title += " since: " + since
}
d := core.NewBody(title)
lv := NewVCSLog(d)
lv.SetRepo(repo).SetLog(lg).SetFile(file).SetSince(since)
d.RunWindowDialog(ctx)
return d
}
// fileAtRevisionDialog shows a file at a given revision in a new dialog window
func fileAtRevisionDialog(ctx core.Widget, repo vcs.Repo, file, rev string) *core.Body {
fb, err := repo.FileContents(file, rev)
if err != nil {
core.ErrorDialog(ctx, err)
return nil
}
if rev == "" {
rev = "HEAD"
}
title := "File at VCS Revision: " + fsx.DirAndFile(file) + "@" + rev
d := core.NewBody(title)
tb := texteditor.NewBuffer().SetText(fb).SetFilename(file) // file is key for getting lang
texteditor.NewEditor(d).SetBuffer(tb).SetReadOnly(true).Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
})
d.RunWindowDialog(ctx)
tb.ReMarkup() // update markup
return d
}
// 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 htmlcore
import (
"net/http"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/colors"
"cogentcore.org/core/core"
"cogentcore.org/core/styles"
"cogentcore.org/core/system"
"cogentcore.org/core/tree"
"github.com/aymerick/douceur/css"
"github.com/aymerick/douceur/parser"
selcss "github.com/ericchiang/css"
"golang.org/x/net/html"
)
// Context contains context information about the current state of a htmlcore
// reader and its surrounding context. It should be created with [NewContext].
type Context struct {
// Node is the node that is currently being read.
Node *html.Node
// styles are the CSS styling rules for each node.
styles map[*html.Node][]*css.Rule
// NewParent is the current parent widget that children of
// the previously read element should be added to, if any.
NewParent core.Widget
// BlockParent is the current parent widget that non-inline elements
// should be added to.
BlockParent core.Widget
// inlineParent is the current parent widget that inline
// elements should be added to; it must be got through
// [Context.InlineParent], as it may need to be constructed
// on the fly. However, it can be set directly.
inlineParent core.Widget
// PageURL, if not "", is the URL of the current page.
// Otherwise, there is no current page.
PageURL string
// OpenURL is the function used to open URLs,
// which defaults to [system.App.OpenURL].
OpenURL func(url string)
// GetURL is the function used to get resources from URLs,
// which defaults to [http.Get].
GetURL func(url string) (*http.Response, error)
}
// NewContext returns a new [Context] with basic defaults.
func NewContext() *Context {
return &Context{
styles: map[*html.Node][]*css.Rule{},
OpenURL: system.TheApp.OpenURL,
GetURL: http.Get,
}
}
// Parent returns the current parent widget that a widget
// associated with the current node should be added to.
// It may make changes to the widget tree, so the widget
// must be added to the resulting parent immediately.
func (c *Context) Parent() core.Widget {
rules := c.styles[c.Node]
display := ""
for _, rule := range rules {
for _, decl := range rule.Declarations {
if decl.Property == "display" {
display = decl.Value
}
}
}
var parent core.Widget
switch display {
case "inline", "inline-block", "":
parent = c.InlineParent()
default:
parent = c.BlockParent
c.inlineParent = nil
}
return parent
}
// config configures the given widget. It needs to be called
// on all widgets that are not configured through the [New]
// pathway.
func (c *Context) config(w core.Widget) {
wb := w.AsWidget()
for _, attr := range c.Node.Attr {
switch attr.Key {
case "id":
wb.SetName(attr.Val)
case "style":
// our CSS parser is strict about semicolons, but
// they aren't needed in normal inline styles in HTML
if !strings.HasSuffix(attr.Val, ";") {
attr.Val += ";"
}
decls, err := parser.ParseDeclarations(attr.Val)
if errors.Log(err) != nil {
continue
}
rule := &css.Rule{Declarations: decls}
if c.styles == nil {
c.styles = map[*html.Node][]*css.Rule{}
}
c.styles[c.Node] = append(c.styles[c.Node], rule)
default:
wb.SetProperty(attr.Key, attr.Val)
}
}
wb.SetProperty("tag", c.Node.Data)
rules := c.styles[c.Node]
wb.Styler(func(s *styles.Style) {
for _, rule := range rules {
for _, decl := range rule.Declarations {
// TODO(kai/styproperties): parent style and context
s.StyleFromProperty(s, decl.Property, decl.Value, colors.BaseContext(colors.ToUniform(s.Color)))
}
}
})
}
// InlineParent returns the current parent widget that inline
// elements should be added to.
func (c *Context) InlineParent() core.Widget {
if c.inlineParent != nil {
return c.inlineParent
}
c.inlineParent = core.NewFrame(c.BlockParent)
c.inlineParent.AsTree().SetName("inline-container")
tree.SetUniqueName(c.inlineParent)
c.inlineParent.AsWidget().Styler(func(s *styles.Style) {
s.Grow.Set(1, 0)
})
return c.inlineParent
}
// addStyle adds the given CSS style string to the page's compiled styles.
func (c *Context) addStyle(style string) {
ss, err := parser.Parse(style)
if errors.Log(err) != nil {
return
}
root := rootNode(c.Node)
for _, rule := range ss.Rules {
var sel *selcss.Selector
if len(rule.Selectors) > 0 {
s, err := selcss.Parse(strings.Join(rule.Selectors, ","))
if errors.Log(err) != nil {
s = &selcss.Selector{}
}
sel = s
} else {
sel = &selcss.Selector{}
}
matches := sel.Select(root)
for _, match := range matches {
c.styles[match] = append(c.styles[match], rule)
}
}
}
// 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 main
import (
_ "embed"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/core"
"cogentcore.org/core/htmlcore"
)
//go:embed example.html
var content string
func main() {
b := core.NewBody("HTML Example")
errors.Log(htmlcore.ReadHTMLString(htmlcore.NewContext(), b, content))
b.RunMainWindow()
}
// 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 main
import (
_ "embed"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/core"
"cogentcore.org/core/htmlcore"
)
//go:embed example.md
var content string
func main() {
b := core.NewBody("MD Example")
errors.Log(htmlcore.ReadMDString(htmlcore.NewContext(), b, content))
b.RunMainWindow()
}
// 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 htmlcore
import (
"fmt"
"io"
"log/slog"
"net/http"
"slices"
"strconv"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/colors"
"cogentcore.org/core/core"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/texteditor"
"cogentcore.org/core/tree"
"golang.org/x/net/html"
)
// ElementHandlers is a map of handler functions for each HTML element
// type (eg: "button", "input", "p"). It is empty by default, but can be
// used by anyone in need of behavior different than the default behavior
// defined in [handleElement] (for example, for custom elements).
// If the handler for an element returns false, then the default behavior
// for an element is used.
var ElementHandlers = map[string]func(ctx *Context) bool{}
// BindTextEditor is a function set to [cogentcore.org/core/yaegicore.BindTextEditor]
// when importing yaegicore, which provides interactive editing functionality for Go
// code blocks in text editors.
var BindTextEditor func(ed *texteditor.Editor, parent core.Widget)
// New adds a new widget of the given type to the context parent.
// It automatically calls [Context.config] on the resulting widget.
func New[T tree.NodeValue](ctx *Context) *T {
parent := ctx.Parent()
w := tree.New[T](parent)
ctx.config(any(w).(core.Widget)) // TODO: better htmlcore structure with new config paradigm?
return w
}
// handleElement calls the handler in [ElementHandlers] associated with the current node
// using the given context. If there is no handler associated with it, it uses default
// hardcoded configuration code.
func handleElement(ctx *Context) {
tag := ctx.Node.Data
h, ok := ElementHandlers[tag]
if ok {
if h(ctx) {
return
}
}
if slices.Contains(textTags, tag) {
handleTextTag(ctx)
return
}
switch tag {
case "script", "title", "meta":
// we don't render anything
case "link":
rel := getAttr(ctx.Node, "rel")
// TODO(kai/htmlcore): maybe handle preload
if rel == "preload" {
return
}
// TODO(kai/htmlcore): support links other than stylesheets
if rel != "stylesheet" {
return
}
resp, err := Get(ctx, getAttr(ctx.Node, "href"))
if errors.Log(err) != nil {
return
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if errors.Log(err) != nil {
return
}
ctx.addStyle(string(b))
case "style":
ctx.addStyle(ExtractText(ctx))
case "body", "main", "div", "section", "nav", "footer", "header", "ol", "ul", "blockquote":
w := New[core.Frame](ctx)
ctx.NewParent = w
if tag == "body" {
w.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
})
}
if tag == "ol" || tag == "ul" {
w.Styler(func(s *styles.Style) {
s.Grow.Set(1, 0)
})
}
case "button":
New[core.Button](ctx).SetText(ExtractText(ctx))
case "h1":
handleText(ctx).SetType(core.TextDisplaySmall)
case "h2":
handleText(ctx).SetType(core.TextHeadlineMedium)
case "h3":
handleText(ctx).SetType(core.TextTitleLarge)
case "h4":
handleText(ctx).SetType(core.TextTitleMedium)
case "h5":
handleText(ctx).SetType(core.TextTitleSmall)
case "h6":
handleText(ctx).SetType(core.TextLabelSmall)
case "p":
handleText(ctx)
case "pre":
hasCode := ctx.Node.FirstChild != nil && ctx.Node.FirstChild.Data == "code"
if hasCode {
ed := New[texteditor.Editor](ctx)
ctx.Node = ctx.Node.FirstChild // go to the code element
lang := getLanguage(getAttr(ctx.Node, "class"))
if lang != "" {
ed.Buffer.SetFileExt(lang)
}
ed.Buffer.SetString(ExtractText(ctx))
if BindTextEditor != nil && lang == "Go" {
ed.Buffer.SpacesToTabs(0, ed.Buffer.NumLines()) // Go uses tabs
parent := core.NewFrame(ed.Parent)
parent.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Grow.Set(1, 0)
})
// we inherit our Grow.Y from our first child so that
// elements that want to grow can do so
parent.SetOnChildAdded(func(n tree.Node) {
if _, ok := n.(*core.Body); ok { // Body should not grow
return
}
wb := core.AsWidget(n)
if wb.IndexInParent() != 0 {
return
}
wb.FinalStyler(func(s *styles.Style) {
parent.Styles.Grow.Y = s.Grow.Y
})
})
BindTextEditor(ed, parent)
} else {
ed.SetReadOnly(true)
ed.Buffer.Options.LineNumbers = false
ed.Styler(func(s *styles.Style) {
s.Border.Width.Zero()
s.MaxBorder.Width.Zero()
s.StateLayer = 0
s.Background = colors.Scheme.SurfaceContainer
})
}
} else {
handleText(ctx).Styler(func(s *styles.Style) {
s.Text.WhiteSpace = styles.WhiteSpacePreWrap
})
}
case "li":
// if we have a p as our first or second child, which is typical
// for markdown-generated HTML, we use it directly for data extraction
// to prevent double elements and unnecessary line breaks.
if ctx.Node.FirstChild != nil && ctx.Node.FirstChild.Data == "p" {
ctx.Node = ctx.Node.FirstChild
} else if ctx.Node.FirstChild != nil && ctx.Node.FirstChild.NextSibling != nil && ctx.Node.FirstChild.NextSibling.Data == "p" {
ctx.Node = ctx.Node.FirstChild.NextSibling
}
text := handleText(ctx)
start := ""
if pw, ok := text.Parent.(core.Widget); ok {
switch pw.AsTree().Property("tag") {
case "ol":
number := 0
for _, k := range pw.AsTree().Children {
// we only consider text for the number (frames may be
// added for nested lists, interfering with the number)
if _, ok := k.(*core.Text); ok {
number++
}
}
start = strconv.Itoa(number) + ". "
case "ul":
// TODO(kai/htmlcore): have different bullets for different depths
start = "• "
}
}
text.SetText(start + text.Text)
case "img":
img := New[core.Image](ctx)
n := ctx.Node
img.SetTooltip(getAttr(n, "alt"))
go func() {
src := getAttr(n, "src")
resp, err := Get(ctx, src)
if errors.Log(err) != nil {
return
}
defer resp.Body.Close()
if strings.Contains(resp.Header.Get("Content-Type"), "svg") {
// TODO(kai/htmlcore): support svg
} else {
im, _, err := imagex.Read(resp.Body)
if err != nil {
slog.Error("error loading image", "url", src, "err", err)
return
}
img.AsyncLock()
img.SetImage(im)
img.Update()
img.AsyncUnlock()
}
}()
case "input":
ityp := getAttr(ctx.Node, "type")
val := getAttr(ctx.Node, "value")
switch ityp {
case "number":
fval := float32(errors.Log1(strconv.ParseFloat(val, 32)))
New[core.Spinner](ctx).SetValue(fval)
case "checkbox":
New[core.Switch](ctx).SetType(core.SwitchCheckbox).
SetState(hasAttr(ctx.Node, "checked"), states.Checked)
case "radio":
New[core.Switch](ctx).SetType(core.SwitchRadioButton).
SetState(hasAttr(ctx.Node, "checked"), states.Checked)
case "range":
fval := float32(errors.Log1(strconv.ParseFloat(val, 32)))
New[core.Slider](ctx).SetValue(fval)
case "button", "submit":
New[core.Button](ctx).SetText(val)
case "color":
core.Bind(val, New[core.ColorButton](ctx))
case "datetime":
core.Bind(val, New[core.TimeInput](ctx))
case "file":
core.Bind(val, New[core.FileButton](ctx))
default:
New[core.TextField](ctx).SetText(val)
}
case "textarea":
buf := texteditor.NewBuffer()
buf.SetText([]byte(ExtractText(ctx)))
New[texteditor.Editor](ctx).SetBuffer(buf)
default:
ctx.NewParent = ctx.Parent()
}
}
func textStyler(s *styles.Style) {
s.Margin.SetVertical(units.Em(core.ConstantSpacing(0.25)))
// TODO: it would be ideal for htmlcore to automatically save a scale factor
// in general and for each domain, that is applied only to page content
// scale := float32(1.2)
// s.Font.Size.Value *= scale
// s.Text.LineHeight.Value *= scale
// s.Text.LetterSpacing.Value *= scale
}
// handleText creates a new [core.Text] from the given information, setting the text and
// the text click function so that URLs are opened according to [Context.OpenURL].
func handleText(ctx *Context) *core.Text {
tx := New[core.Text](ctx).SetText(ExtractText(ctx))
tx.Styler(textStyler)
tx.HandleTextClick(func(tl *paint.TextLink) {
ctx.OpenURL(tl.URL)
})
return tx
}
// handleTextTag creates a new [core.Text] from the given information, setting the text and
// the text click function so that URLs are opened according to [Context.OpenURL]. Also,
// it wraps the text with the [nodeString] of the given node, meaning that it
// should be used for standalone elements that are meant to only exist in text
// (eg: a, span, b, code, etc).
func handleTextTag(ctx *Context) *core.Text {
start, end := nodeString(ctx.Node)
str := start + ExtractText(ctx) + end
tx := New[core.Text](ctx).SetText(str)
tx.Styler(textStyler)
tx.HandleTextClick(func(tl *paint.TextLink) {
ctx.OpenURL(tl.URL)
})
return tx
}
// getAttr gets the given attribute from the given node, returning ""
// if the attribute is not found.
func getAttr(n *html.Node, attr string) string {
res := ""
for _, a := range n.Attr {
if a.Key == attr {
res = a.Val
}
}
return res
}
// hasAttr returns whether the given node has the given attribute defined.
func hasAttr(n *html.Node, attr string) bool {
return slices.ContainsFunc(n.Attr, func(a html.Attribute) bool {
return a.Key == attr
})
}
// getLanguage returns the 'x' in a `language-x` class from the given
// string of class(es).
func getLanguage(class string) string {
fields := strings.Fields(class)
for _, field := range fields {
if strings.HasPrefix(field, "language-") {
return strings.TrimPrefix(field, "language-")
}
}
return ""
}
// Get is a helper function that calls [Context.GetURL] with the given URL, parsed
// relative to the page URL of the given context. It also checks the status
// code of the response and closes the response body and returns an error if
// it is not [http.StatusOK]. If the error is nil, then the response body is
// not closed and must be closed by the caller.
func Get(ctx *Context, url string) (*http.Response, error) {
u, err := parseRelativeURL(url, ctx.PageURL)
if err != nil {
return nil, err
}
resp, err := ctx.GetURL(u.String())
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return resp, fmt.Errorf("got error status %q (code %d)", resp.Status, resp.StatusCode)
}
return resp, nil
}
// 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 htmlcore converts HTML and MD into Cogent Core widget trees.
package htmlcore
import (
"bytes"
"fmt"
"io"
"strings"
"cogentcore.org/core/core"
"golang.org/x/net/html"
)
// ReadHTML reads HTML from the given [io.Reader] and adds corresponding
// Cogent Core widgets to the given [core.Widget], using the given context.
func ReadHTML(ctx *Context, parent core.Widget, r io.Reader) error {
n, err := html.Parse(r)
if err != nil {
return fmt.Errorf("error parsing HTML: %w", err)
}
return readHTMLNode(ctx, parent, n)
}
// ReadHTMLString reads HTML from the given string and adds corresponding
// Cogent Core widgets to the given [core.Widget], using the given context.
func ReadHTMLString(ctx *Context, parent core.Widget, s string) error {
b := bytes.NewBufferString(s)
return ReadHTML(ctx, parent, b)
}
// readHTMLNode reads HTML from the given [*html.Node] and adds corresponding
// Cogent Core widgets to the given [core.Widget], using the given context.
func readHTMLNode(ctx *Context, parent core.Widget, n *html.Node) error {
// nil parent means we are root, so we add user agent styles here
if n.Parent == nil {
ctx.Node = n
ctx.addStyle(userAgentStyles)
}
switch n.Type {
case html.TextNode:
str := strings.TrimSpace(n.Data)
if str != "" {
New[core.Text](ctx).SetText(str)
}
case html.ElementNode:
ctx.Node = n
ctx.BlockParent = parent
ctx.NewParent = nil
handleElement(ctx)
default:
ctx.NewParent = parent
}
if ctx.NewParent != nil && n.FirstChild != nil {
readHTMLNode(ctx, ctx.NewParent, n.FirstChild)
}
if n.NextSibling != nil {
readHTMLNode(ctx, parent, n.NextSibling)
}
return nil
}
// rootNode returns the root node of the given node.
func rootNode(n *html.Node) *html.Node {
for n.Parent != nil {
n = n.Parent
}
return n
}
// 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 htmlcore
import (
"bytes"
"cogentcore.org/core/core"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
)
func mdToHTML(md []byte) []byte {
// create markdown parser with extensions
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
p := parser.NewWithExtensions(extensions)
prev := p.RegisterInline('[', nil)
p.RegisterInline('[', wikiLink(prev))
doc := p.Parse(md)
// create HTML renderer with extensions
htmlFlags := html.CommonFlags | html.HrefTargetBlank
opts := html.RendererOptions{Flags: htmlFlags}
renderer := html.NewRenderer(opts)
return markdown.Render(doc, renderer)
}
// ReadMD reads MD (markdown) from the given bytes and adds corresponding
// Cogent Core widgets to the given [core.Widget], using the given context.
func ReadMD(ctx *Context, parent core.Widget, b []byte) error {
htm := mdToHTML(b)
buf := bytes.NewBuffer(htm)
return ReadHTML(ctx, parent, buf)
}
// ReadMDString reads MD (markdown) from the given string and adds
// corresponding Cogent Core widgets to the given [core.Widget], using the given context.
func ReadMDString(ctx *Context, parent core.Widget, s string) error {
return ReadMD(ctx, parent, []byte(s))
}
// 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 htmlcore
import (
"slices"
"golang.org/x/net/html"
)
// ExtractText recursively extracts all of the text from the children
// of the given [*html.Node], adding any appropriate inline markup for
// formatted text. It adds any non-text elements to the given [core.Widget]
// using [readHTMLNode]. It should not be called on text nodes themselves;
// for that, you can directly access the [html.Node.Data] field. It uses
// the given page URL for context when resolving URLs, but it can be
// omitted if not available.
func ExtractText(ctx *Context) string {
if ctx.Node.FirstChild == nil {
return ""
}
return extractText(ctx, ctx.Node.FirstChild)
}
func extractText(ctx *Context, n *html.Node) string {
str := ""
if n.Type == html.TextNode {
str += n.Data
}
it := isText(n)
if !it {
readHTMLNode(ctx, ctx.Parent(), n)
}
if it && n.FirstChild != nil {
start, end := nodeString(n)
str = start + extractText(ctx, n.FirstChild) + end
}
if n.NextSibling != nil {
str += extractText(ctx, n.NextSibling)
}
return str
}
// nodeString returns the given node as starting and ending strings in the format:
//
// <tag attr0="value0" attr1="value1">
//
// and
//
// </tag>
//
// It returns "", "" if the given node is not an [html.ElementNode]
func nodeString(n *html.Node) (start, end string) {
if n.Type != html.ElementNode {
return
}
tag := n.Data
start = "<" + tag
for _, a := range n.Attr {
start += " " + a.Key + "=" + `"` + a.Val + `"`
}
start += ">"
end = "</" + tag + ">"
return
}
// textTags are all of the node tags that result in a true return value for [isText].
var textTags = []string{
"a", "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "dfn",
"em", "i", "kbd", "mark", "q", "rp", "rt", "ruby", "s", "samp", "small",
"span", "strong", "sub", "sup", "time", "u", "var", "wbr",
}
// isText returns true if the given node is a [html.TextNode] or
// an [html.ElementNode] designed for holding text (a, span, b, code, etc),
// and false otherwise.
func isText(n *html.Node) bool {
if n.Type == html.TextNode {
return true
}
if n.Type == html.ElementNode {
tag := n.Data
return slices.Contains(textTags, tag)
}
return false
}
// 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 htmlcore
import (
"net/url"
)
// parseRelativeURL parses the given raw URL relative to the given base URL.
func parseRelativeURL(rawURL, base string) (*url.URL, error) {
u, err := url.Parse(rawURL)
if err != nil {
return u, err
}
b, err := url.Parse(base)
if err != nil {
return u, err
}
return b.ResolveReference(u), nil
}
// Copyright (c) 2024, 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 htmlcore
import (
"bytes"
"github.com/gomarkdown/markdown/ast"
"github.com/gomarkdown/markdown/parser"
)
// WikilinkBaseURL is the base URL to use for wiki links
var WikilinkBaseURL = "cogentcore.org/core"
// note: this is from: https://github.com/kensanata/oddmu/blob/main/parser.go
// wikiLink returns an inline parser function. This indirection is
// required because we want to call the previous definition in case
// this is not a wikiLink.
func wikiLink(fn func(p *parser.Parser, data []byte, offset int) (int, ast.Node)) func(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
return func(p *parser.Parser, original []byte, offset int) (int, ast.Node) {
data := original[offset:]
n := len(data)
// minimum: [[X]]
if n < 5 || data[1] != '[' {
return fn(p, original, offset)
}
i := 2
for i+1 < n && data[i] != ']' && data[i+1] != ']' {
i++
}
text := data[2 : i+1]
// pkg.go.dev uses fragments for first dot within package
t := bytes.Replace(text, []byte{'.'}, []byte{'#'}, 1)
dest := append([]byte("https://pkg.go.dev/"+WikilinkBaseURL+"/"), t...)
link := &ast.Link{
Destination: dest,
}
ast.AppendChild(link, &ast.Text{Leaf: ast.Leaf{Literal: text}})
return i + 3, link
}
}
// 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 icons provides Material Design Symbols as SVG icon variables.
package icons
import _ "embed"
//go:generate core generate -icons svg
// Icon represents the SVG data of an icon. It can be
// set to "" or [None] to indicate that no icon should be used.
type Icon string
var (
// None is an icon that indicates to not use an icon.
// It completely prevents the rendering of an icon,
// whereas [Blank] renders a blank icon.
None Icon = "none"
// Blank is a blank icon that can be used as a
// placeholder when no other icon is appropriate.
// It still renders an icon, just a blank one,
// whereas [None] indicates to not render one at all.
//
//go:embed svg/blank.svg
Blank Icon
)
// IsSet returns whether the icon is set to a value other than "" or [None].
func (i Icon) IsSet() bool {
return i != "" && i != None
}
// Used is a map containing all icons that have been used.
// It is added to by [cogentcore.org/core/core.Icon].
var Used = map[Icon]bool{}
// Code generated by "core generate"; DO NOT EDIT.
package keymap
import (
"cogentcore.org/core/enums"
)
var _FunctionsValues = []Functions{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65}
// FunctionsN is the highest valid value for type Functions, plus one.
const FunctionsN Functions = 66
var _FunctionsValueMap = map[string]Functions{`None`: 0, `MoveUp`: 1, `MoveDown`: 2, `MoveRight`: 3, `MoveLeft`: 4, `PageUp`: 5, `PageDown`: 6, `Home`: 7, `End`: 8, `DocHome`: 9, `DocEnd`: 10, `WordRight`: 11, `WordLeft`: 12, `FocusNext`: 13, `FocusPrev`: 14, `Enter`: 15, `Accept`: 16, `CancelSelect`: 17, `SelectMode`: 18, `SelectAll`: 19, `Abort`: 20, `Copy`: 21, `Cut`: 22, `Paste`: 23, `PasteHist`: 24, `Backspace`: 25, `BackspaceWord`: 26, `Delete`: 27, `DeleteWord`: 28, `Kill`: 29, `Duplicate`: 30, `Transpose`: 31, `TransposeWord`: 32, `Undo`: 33, `Redo`: 34, `Insert`: 35, `InsertAfter`: 36, `ZoomOut`: 37, `ZoomIn`: 38, `Refresh`: 39, `Recenter`: 40, `Complete`: 41, `Lookup`: 42, `Search`: 43, `Find`: 44, `Replace`: 45, `Jump`: 46, `HistPrev`: 47, `HistNext`: 48, `Menu`: 49, `WinFocusNext`: 50, `WinClose`: 51, `WinSnapshot`: 52, `New`: 53, `NewAlt1`: 54, `NewAlt2`: 55, `Open`: 56, `OpenAlt1`: 57, `OpenAlt2`: 58, `Save`: 59, `SaveAs`: 60, `SaveAlt`: 61, `CloseAlt1`: 62, `CloseAlt2`: 63, `MultiA`: 64, `MultiB`: 65}
var _FunctionsDescMap = map[Functions]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``, 5: ``, 6: ``, 7: `PageRight PageLeft`, 8: ``, 9: ``, 10: ``, 11: ``, 12: ``, 13: ``, 14: ``, 15: ``, 16: ``, 17: ``, 18: ``, 19: ``, 20: ``, 21: `EditItem`, 22: ``, 23: ``, 24: ``, 25: ``, 26: ``, 27: ``, 28: ``, 29: ``, 30: ``, 31: ``, 32: ``, 33: ``, 34: ``, 35: ``, 36: ``, 37: ``, 38: ``, 39: ``, 40: ``, 41: ``, 42: ``, 43: ``, 44: ``, 45: ``, 46: ``, 47: ``, 48: ``, 49: ``, 50: ``, 51: ``, 52: ``, 53: `Below are menu specific functions -- use these as shortcuts for menu buttons allows uniqueness of mapping and easy customization of all key buttons`, 54: ``, 55: ``, 56: ``, 57: ``, 58: ``, 59: ``, 60: ``, 61: ``, 62: ``, 63: ``, 64: ``, 65: ``}
var _FunctionsMap = map[Functions]string{0: `None`, 1: `MoveUp`, 2: `MoveDown`, 3: `MoveRight`, 4: `MoveLeft`, 5: `PageUp`, 6: `PageDown`, 7: `Home`, 8: `End`, 9: `DocHome`, 10: `DocEnd`, 11: `WordRight`, 12: `WordLeft`, 13: `FocusNext`, 14: `FocusPrev`, 15: `Enter`, 16: `Accept`, 17: `CancelSelect`, 18: `SelectMode`, 19: `SelectAll`, 20: `Abort`, 21: `Copy`, 22: `Cut`, 23: `Paste`, 24: `PasteHist`, 25: `Backspace`, 26: `BackspaceWord`, 27: `Delete`, 28: `DeleteWord`, 29: `Kill`, 30: `Duplicate`, 31: `Transpose`, 32: `TransposeWord`, 33: `Undo`, 34: `Redo`, 35: `Insert`, 36: `InsertAfter`, 37: `ZoomOut`, 38: `ZoomIn`, 39: `Refresh`, 40: `Recenter`, 41: `Complete`, 42: `Lookup`, 43: `Search`, 44: `Find`, 45: `Replace`, 46: `Jump`, 47: `HistPrev`, 48: `HistNext`, 49: `Menu`, 50: `WinFocusNext`, 51: `WinClose`, 52: `WinSnapshot`, 53: `New`, 54: `NewAlt1`, 55: `NewAlt2`, 56: `Open`, 57: `OpenAlt1`, 58: `OpenAlt2`, 59: `Save`, 60: `SaveAs`, 61: `SaveAlt`, 62: `CloseAlt1`, 63: `CloseAlt2`, 64: `MultiA`, 65: `MultiB`}
// String returns the string representation of this Functions value.
func (i Functions) String() string { return enums.String(i, _FunctionsMap) }
// SetString sets the Functions value from its string representation,
// and returns an error if the string is invalid.
func (i *Functions) SetString(s string) error {
return enums.SetString(i, s, _FunctionsValueMap, "Functions")
}
// Int64 returns the Functions value as an int64.
func (i Functions) Int64() int64 { return int64(i) }
// SetInt64 sets the Functions value from an int64.
func (i *Functions) SetInt64(in int64) { *i = Functions(in) }
// Desc returns the description of the Functions value.
func (i Functions) Desc() string { return enums.Desc(i, _FunctionsDescMap) }
// FunctionsValues returns all possible values for the type Functions.
func FunctionsValues() []Functions { return _FunctionsValues }
// Values returns all possible values for the type Functions.
func (i Functions) Values() []enums.Enum { return enums.Values(_FunctionsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Functions) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Functions) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "Functions")
}
// 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 keymap
import (
"cogentcore.org/core/system"
// we have to import system/driver here so that it is initialized
// in time for us to the get the system platform
_ "cogentcore.org/core/system/driver"
)
func init() {
AvailableMaps.CopyFrom(StandardMaps)
switch system.TheApp.SystemPlatform() {
case system.MacOS:
DefaultMap = "MacStandard"
case system.Windows:
DefaultMap = "WindowsStandard"
default:
DefaultMap = "LinuxStandard"
}
SetActiveMapName(DefaultMap)
}
// 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 keymap implements maps from keyboard shortcuts to
// semantic GUI keyboard functions.
package keymap
//go:generate core generate
import (
"encoding/json"
"log/slog"
"slices"
"sort"
"strings"
"cogentcore.org/core/events/key"
)
// https://en.wikipedia.org/wiki/Table_of_keyboard_shortcuts
// https://www.cs.colorado.edu/~main/cs1300/lab/emacs.html
// https://help.ubuntu.com/community/KeyboardShortcuts
// Functions are semantic functions that keyboard events
// can perform in the GUI.
type Functions int32 //enums:enum
const (
None Functions = iota
MoveUp
MoveDown
MoveRight
MoveLeft
PageUp
PageDown
// PageRight
// PageLeft
Home // start-of-line
End // end-of-line
DocHome // start-of-doc -- Control / Alt / Shift +Home
DocEnd // end-of-doc Control / Alt / Shift +End
WordRight
WordLeft
// WordLeft is the final navigation function -- all above also allow Shift+ for selection.
FocusNext // Tab
FocusPrev // Shift-Tab
Enter // Enter / return key -- has various special functions
Accept // Ctrl+Enter = accept any changes and close dialog / move to next
CancelSelect
SelectMode
SelectAll
Abort
// EditItem
Copy
Cut
Paste
PasteHist // from history
Backspace
BackspaceWord
Delete
DeleteWord
Kill
Duplicate
Transpose
TransposeWord
Undo
Redo
Insert
InsertAfter
ZoomOut
ZoomIn
Refresh
Recenter // Ctrl+L in emacs
Complete
Lookup
Search // Ctrl+S in emacs -- more interactive type of search
Find // Command+F full-dialog find
Replace
Jump // jump to line
HistPrev
HistNext
Menu // put focus on menu
WinFocusNext
WinClose
WinSnapshot
// Below are menu specific functions -- use these as shortcuts for menu buttons
// allows uniqueness of mapping and easy customization of all key buttons
New
NewAlt1 // alternative version (e.g., shift)
NewAlt2 // alternative version (e.g., alt)
Open
OpenAlt1 // alternative version (e.g., shift)
OpenAlt2 // alternative version (e.g., alt)
Save
SaveAs
SaveAlt // another alt (e.g., alt)
CloseAlt1 // alternative version (e.g., shift)
CloseAlt2 // alternative version (e.g., alt)
MultiA // multi-key sequence A: Emacs Control+C
MultiB // multi-key sequence B: Emacs Control+X
)
// Map is a map between a key sequence (chord) and a specific key
// function. This mapping must be unique, in that each chord has a
// unique function, but multiple chords can trigger the same function.
type Map map[key.Chord]Functions
// ActiveMap points to the active map -- users can set this to an
// alternative map in Settings
var ActiveMap *Map
// MapName has an associated Value for selecting from the list of
// available key map names, for use in preferences etc.
type MapName string
// ActiveMapName is the name of the active keymap
var ActiveMapName MapName
// SetActiveMap sets the current ActiveKeyMap, calling Update on the map
// prior to setting it to ensure that it is a valid, complete map
func SetActiveMap(km *Map, kmName MapName) {
km.Update(kmName)
ActiveMap = km
ActiveMapName = kmName
}
// SetActiveMapName sets the current ActiveKeyMap by name from those
// defined in AvailKeyMaps, calling Update on the map prior to setting it to
// ensure that it is a valid, complete map
func SetActiveMapName(mapnm MapName) {
km, _, ok := AvailableMaps.MapByName(mapnm)
if ok {
SetActiveMap(km, mapnm)
} else {
slog.Error("keymap.SetActiveKeyMapName: key map named not found, using default", "requested", mapnm, "default", DefaultMap)
km, _, ok = AvailableMaps.MapByName(DefaultMap)
if ok {
SetActiveMap(km, DefaultMap)
} else {
avail := make([]string, len(AvailableMaps))
for i, km := range AvailableMaps {
avail[i] = km.Name
}
slog.Error("keymap.SetActiveKeyMapName: DefaultKeyMap not found either; trying first one", "default", DefaultMap, "available", avail)
if len(AvailableMaps) > 0 {
nkm := AvailableMaps[0]
SetActiveMap(&nkm.Map, MapName(nkm.Name))
}
}
}
}
// Of converts the given [key.Chord] into a keyboard function.
func Of(chord key.Chord) Functions {
f, ok := (*ActiveMap)[chord]
if ok {
return f
}
if strings.Contains(string(chord), "Shift+") {
nsc := key.Chord(strings.ReplaceAll(string(chord), "Shift+", ""))
if f, ok = (*ActiveMap)[nsc]; ok && f <= WordLeft { // automatically allow +Shift for nav
return f
}
}
return None
}
// MapItem records one element of the key map, which is used for organizing the map.
type MapItem struct {
// the key chord that activates a function
Key key.Chord
// the function of that key
Fun Functions
}
// ToSlice copies this keymap to a slice of [MapItem]s.
func (km *Map) ToSlice() []MapItem {
kms := make([]MapItem, len(*km))
idx := 0
for key, fun := range *km {
kms[idx] = MapItem{key, fun}
idx++
}
return kms
}
// ChordFor returns all of the key chord triggers for the given
// key function in the map, separating them with newlines.
func (km *Map) ChordFor(kf Functions) key.Chord {
res := []string{}
for key, fun := range *km {
if fun == kf {
res = append(res, string(key))
}
}
slices.Sort(res)
return key.Chord(strings.Join(res, "\n"))
}
// Chord returns all of the key chord triggers for this
// key function in the current active map, separating them with newlines.
func (kf Functions) Chord() key.Chord {
return ActiveMap.ChordFor(kf)
}
// Label transforms the key function into a string representing
// its underlying key chord(s) in a form suitable for display to users.
func (kf Functions) Label() string {
return kf.Chord().Label()
}
// Update ensures that the given keymap has at least one entry for every
// defined key function, grabbing ones from the default map if not, and
// also eliminates any [None] entries which might reflect out-of-date
// functions.
func (km *Map) Update(kmName MapName) {
for key, val := range *km {
if val == None {
slog.Error("keymap.KeyMap: key function is nil; probably renamed", "key", key)
delete(*km, key)
}
}
kms := km.ToSlice()
addkm := make([]MapItem, 0)
sort.Slice(kms, func(i, j int) bool {
return kms[i].Fun < kms[j].Fun
})
lfun := None
for _, ki := range kms {
fun := ki.Fun
if fun != lfun {
del := fun - lfun
if del > 1 {
for mi := lfun + 1; mi < fun; mi++ {
// slog.Error("keymap.KeyMap: key map is missing a key for a key function", "keyMap", kmName, "function", mi)
s := mi.String()
s = "- Not Set - " + s
nski := MapItem{Key: key.Chord(s), Fun: mi}
addkm = append(addkm, nski)
}
}
lfun = fun
}
}
for _, ai := range addkm {
(*km)[ai.Key] = ai.Fun
}
}
// DefaultMap is the overall default keymap, which is set in init
// depending on the platform
var DefaultMap MapName = "LinuxStandard"
// MapsItem is an entry in a Maps list
type MapsItem struct { //types:add -setters
// name of keymap
Name string `width:"20"`
// description of keymap; good idea to include source it was derived from
Desc string
// to edit key sequence click button and type new key combination; to edit function mapped to key sequence choose from menu
Map Map
}
// Label satisfies the Labeler interface
func (km MapsItem) Label() string {
return km.Name
}
// Maps is a list of [MapsItem]s; users can edit these in their settings.
type Maps []MapsItem //types:add
// AvailableMaps is the current list of available keymaps for use.
// This can be loaded / saved / edited in user settings. This is set
// to [StandardMaps] at startup.
var AvailableMaps Maps
// MapByName returns a [Map] and index by name. It returns false
// and prints an error message if not found.
func (km *Maps) MapByName(name MapName) (*Map, int, bool) {
for i, it := range *km {
if it.Name == string(name) {
return &it.Map, i, true
}
}
slog.Error("keymap.KeyMaps.MapByName: key map not found", "name", name)
return nil, -1, false
}
// CopyFrom copies keymaps from given other map
func (km *Maps) CopyFrom(cp Maps) {
*km = make(Maps, 0, len(cp)) // reset
b, _ := json.Marshal(cp)
json.Unmarshal(b, km)
}
// MergeFrom merges keymaps from given other map
func (km *Maps) MergeFrom(cp Maps) {
for nm, mi := range cp {
tmi := (*km)[nm]
for ch, kf := range mi.Map {
tmi.Map[ch] = kf
}
}
}
// order is: Shift, Control, Alt, Meta
// note: shift and meta modifiers for navigation keys do select + move
// note: where multiple shortcuts exist for a given function, any shortcut
// display of such items in menus will randomly display one of the
// options. This can be considered a feature, not a bug!
// 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 keymap
import (
"strings"
"cogentcore.org/core/events/key"
)
// MarkdownDoc generates a markdown table of all the key mappings
func (km *Maps) MarkdownDoc() string { //types:add
mods := []string{"", "Shift", "Control", "Control+Shift", "Meta", "Meta+Shift", "Alt", "Alt+Shift", "Control+Alt", "Meta+Alt"}
var b strings.Builder
fmap := make([][][]string, len(*km)) // km, kf, ch
for i := range *km {
fmap[i] = make([][]string, FunctionsN)
}
for _, md := range mods {
if md == "" {
b.WriteString("### No Modifiers\n\n")
} else {
b.WriteString("### " + md + "\n\n")
}
b.WriteString("| Key ")
for _, m := range *km {
b.WriteString("| `" + m.Name + "` ")
}
b.WriteString("|\n")
b.WriteString("| ---------------------------- ")
for _, m := range *km {
b.WriteString("| " + strings.Repeat("-", len(m.Name)+2) + " ")
}
b.WriteString("|\n")
for cd := key.CodeA; cd < key.CodesN; cd++ {
ch := key.Chord(md + "+" + cd.String())
if md == "" {
ch = key.Chord(cd.String())
}
has := false
for _, m := range *km {
_, ok := m.Map[ch]
if ok {
has = true
break
}
}
if !has {
continue
}
b.WriteString("| " + string(ch) + " ")
for mi, m := range *km {
kf, ok := m.Map[ch]
if ok {
b.WriteString("| " + kf.String() + " ")
fmap[mi][kf] = append(fmap[mi][kf], string(ch))
} else {
b.WriteString("| " + strings.Repeat(" ", len(m.Name)+2) + " ")
}
}
b.WriteString("|\n")
}
b.WriteString("\n\n")
}
// By function
b.WriteString("### By function\n\n")
b.WriteString("| Function ")
for _, m := range *km {
b.WriteString("| `" + m.Name + "` ")
}
b.WriteString("|\n")
b.WriteString("| ---------------------------- ")
for _, m := range *km {
b.WriteString("| " + strings.Repeat("-", len(m.Name)+2) + " ")
}
b.WriteString("|\n")
for kf := MoveUp; kf < FunctionsN; kf++ {
b.WriteString("| " + kf.String() + " ")
for mi, m := range *km {
f := fmap[mi][kf]
b.WriteString("| ")
if len(f) > 0 {
for fi, fs := range f {
b.WriteString(fs)
if fi < len(f)-1 {
b.WriteString(", ")
} else {
b.WriteString(" ")
}
}
} else {
b.WriteString(strings.Repeat(" ", len(m.Name)+2) + " ")
}
}
b.WriteString("|\n")
}
b.WriteString("\n\n")
return b.String()
}
// Code generated by "core generate"; DO NOT EDIT.
package keymap
import (
"cogentcore.org/core/types"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/keymap.MapsItem", IDName: "maps-item", Doc: "MapsItem is an entry in a Maps list", Directives: []types.Directive{{Tool: "types", Directive: "add", Args: []string{"-setters"}}}, Fields: []types.Field{{Name: "Name", Doc: "name of keymap"}, {Name: "Desc", Doc: "description of keymap; good idea to include source it was derived from"}, {Name: "Map", Doc: "to edit key sequence click button and type new key combination; to edit function mapped to key sequence choose from menu"}}})
// SetName sets the [MapsItem.Name]:
// name of keymap
func (t *MapsItem) SetName(v string) *MapsItem { t.Name = v; return t }
// SetDesc sets the [MapsItem.Desc]:
// description of keymap; good idea to include source it was derived from
func (t *MapsItem) SetDesc(v string) *MapsItem { t.Desc = v; return t }
// SetMap sets the [MapsItem.Map]:
// to edit key sequence click button and type new key combination; to edit function mapped to key sequence choose from menu
func (t *MapsItem) SetMap(v Map) *MapsItem { t.Map = v; return t }
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
import (
"unsafe"
"cogentcore.org/core/base/slicesx"
)
// ArrayF32 is a slice of float32 with additional convenience methods
// for other math32 data types. Use slicesx.SetLength to set length
// efficiently.
type ArrayF32 []float32
// NewArrayF32 creates a returns a new array of floats
// with the specified initial size and capacity
func NewArrayF32(size, capacity int) ArrayF32 {
return make([]float32, size, capacity)
}
// NumBytes returns the size of the array in bytes
func (a *ArrayF32) NumBytes() int {
return len(*a) * int(unsafe.Sizeof(float32(0)))
}
// Append appends any number of values to the array
func (a *ArrayF32) Append(v ...float32) {
*a = append(*a, v...)
}
// AppendVector2 appends any number of Vector2 to the array
func (a *ArrayF32) AppendVector2(v ...Vector2) {
for i := 0; i < len(v); i++ {
*a = append(*a, v[i].X, v[i].Y)
}
}
// AppendVector3 appends any number of Vector3 to the array
func (a *ArrayF32) AppendVector3(v ...Vector3) {
for i := 0; i < len(v); i++ {
*a = append(*a, v[i].X, v[i].Y, v[i].Z)
}
}
// AppendVector4 appends any number of Vector4 to the array
func (a *ArrayF32) AppendVector4(v ...Vector4) {
for i := 0; i < len(v); i++ {
*a = append(*a, v[i].X, v[i].Y, v[i].Z, v[i].W)
}
}
// CopyFloat32s copies a []float32 slice from src into target,
// ensuring that the target is the correct size.
func CopyFloat32s(trg *[]float32, src []float32) {
*trg = slicesx.SetLength(*trg, len(src))
copy(*trg, src)
}
// CopyFloat64s copies a []float64 slice from src into target,
// ensuring that the target is the correct size.
func CopyFloat64s(trg *[]float64, src []float64) {
*trg = slicesx.SetLength(*trg, len(src))
copy(*trg, src)
}
func (a *ArrayF32) CopyFrom(src ArrayF32) {
CopyFloat32s((*[]float32)(a), src)
}
// GetVector2 stores in the specified Vector2 the
// values from the array starting at the specified pos.
func (a ArrayF32) GetVector2(pos int, v *Vector2) {
v.X = a[pos]
v.Y = a[pos+1]
}
// GetVector3 stores in the specified Vector3 the
// values from the array starting at the specified pos.
func (a ArrayF32) GetVector3(pos int, v *Vector3) {
v.X = a[pos]
v.Y = a[pos+1]
v.Z = a[pos+2]
}
// GetVector4 stores in the specified Vector4 the
// values from the array starting at the specified pos.
func (a ArrayF32) GetVector4(pos int, v *Vector4) {
v.X = a[pos]
v.Y = a[pos+1]
v.Z = a[pos+2]
v.W = a[pos+3]
}
// GetMatrix4 stores in the specified Matrix4 the
// values from the array starting at the specified pos.
func (a ArrayF32) GetMatrix4(pos int, m *Matrix4) {
m[0] = a[pos]
m[1] = a[pos+1]
m[2] = a[pos+2]
m[3] = a[pos+3]
m[4] = a[pos+4]
m[5] = a[pos+5]
m[6] = a[pos+6]
m[7] = a[pos+7]
m[8] = a[pos+8]
m[9] = a[pos+9]
m[10] = a[pos+10]
m[11] = a[pos+11]
m[12] = a[pos+12]
m[13] = a[pos+13]
m[14] = a[pos+14]
m[15] = a[pos+15]
}
// Set sets the values of the array starting at the specified pos
// from the specified values
func (a ArrayF32) Set(pos int, v ...float32) {
for i, vv := range v {
a[pos+i] = vv
}
}
// SetVector2 sets the values of the array at the specified pos
// from the XY values of the specified Vector2
func (a ArrayF32) SetVector2(pos int, v Vector2) {
a[pos] = v.X
a[pos+1] = v.Y
}
// SetVector3 sets the values of the array at the specified pos
// from the XYZ values of the specified Vector3
func (a ArrayF32) SetVector3(pos int, v Vector3) {
a[pos] = v.X
a[pos+1] = v.Y
a[pos+2] = v.Z
}
// SetVector4 sets the values of the array at the specified pos
// from the XYZ values of the specified Vector4
func (a ArrayF32) SetVector4(pos int, v Vector4) {
a[pos] = v.X
a[pos+1] = v.Y
a[pos+2] = v.Z
a[pos+3] = v.W
}
/////////////////////////////////////////////////////////////////////////////////////
// ArrayU32
// ArrayU32 is a slice of uint32 with additional convenience methods.
// Use slicesx.SetLength to set length efficiently.
type ArrayU32 []uint32
// NewArrayU32 creates a returns a new array of uint32
// with the specified initial size and capacity
func NewArrayU32(size, capacity int) ArrayU32 {
return make([]uint32, size, capacity)
}
// NumBytes returns the size of the array in bytes
func (a *ArrayU32) NumBytes() int {
return len(*a) * int(unsafe.Sizeof(uint32(0)))
}
// Append appends n elements to the array updating the slice if necessary
func (a *ArrayU32) Append(v ...uint32) {
*a = append(*a, v...)
}
// Set sets the values of the array starting at the specified pos
// from the specified values
func (a ArrayU32) Set(pos int, v ...uint32) {
for i, vv := range v {
a[pos+i] = vv
}
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
import (
"image"
"golang.org/x/image/math/fixed"
)
// Box2 represents a 2D bounding box defined by two points:
// the point with minimum coordinates and the point with maximum coordinates.
type Box2 struct {
Min Vector2
Max Vector2
}
// B2 returns a new [Box2] from the given minimum and maximum x and y coordinates.
func B2(x0, y0, x1, y1 float32) Box2 {
return Box2{Vec2(x0, y0), Vec2(x1, y1)}
}
// B2Empty returns a new [Box2] with empty minimum and maximum values
func B2Empty() Box2 {
bx := Box2{}
bx.SetEmpty()
return bx
}
// B2FromRect returns a new [Box2] from the given [image.Rectangle].
func B2FromRect(rect image.Rectangle) Box2 {
b := Box2{}
b.SetFromRect(rect)
return b
}
// B2FromFixed returns a new [Box2] from the given [fixed.Rectangle26_6].
func B2FromFixed(rect fixed.Rectangle26_6) Box2 {
b := Box2{}
b.Min.SetFixed(rect.Min)
b.Max.SetFixed(rect.Max)
return b
}
// SetEmpty set this bounding box to empty (min / max +/- Infinity)
func (b *Box2) SetEmpty() {
b.Min.SetScalar(Infinity)
b.Max.SetScalar(-Infinity)
}
// IsEmpty returns if this bounding box is empty (max < min on any coord).
func (b *Box2) IsEmpty() bool {
return (b.Max.X < b.Min.X) || (b.Max.Y < b.Min.Y)
}
// Set sets this bounding box minimum and maximum coordinates.
// If either min or max are nil, then corresponding values are set to +/- Infinity.
func (b *Box2) Set(min, max *Vector2) {
if min != nil {
b.Min = *min
} else {
b.Min.SetScalar(Infinity)
}
if max != nil {
b.Max = *max
} else {
b.Max.SetScalar(-Infinity)
}
}
// SetFromPoints set this bounding box from the specified array of points.
func (b *Box2) SetFromPoints(points []Vector2) {
b.SetEmpty()
for i := 0; i < len(points); i++ {
b.ExpandByPoint(points[i])
}
}
// SetFromRect set this bounding box from an image.Rectangle
func (b *Box2) SetFromRect(rect image.Rectangle) {
b.Min = FromPoint(rect.Min)
b.Max = FromPoint(rect.Max)
}
// ToRect returns image.Rectangle version of this bbox, using floor for min
// and Ceil for max.
func (b Box2) ToRect() image.Rectangle {
rect := image.Rectangle{}
rect.Min = b.Min.ToPointFloor()
rect.Max = b.Max.ToPointCeil()
return rect
}
// RectInNotEmpty returns true if rect r is contained within b box
// and r is not empty.
// The existing image.Rectangle.In method returns true if r is empty,
// but we typically expect that case to be false (out of range box)
func RectInNotEmpty(r, b image.Rectangle) bool {
if r.Empty() {
return false
}
return r.In(b)
}
// Canon returns the canonical version of the box.
// The returned rectangle has minimum and maximum coordinates swapped
// if necessary so that it is well-formed.
func (b Box2) Canon() Box2 {
if b.Max.X < b.Min.X {
b.Min.X, b.Max.X = b.Max.X, b.Min.X
}
if b.Max.Y < b.Min.Y {
b.Min.Y, b.Max.Y = b.Max.Y, b.Min.Y
}
return b
}
// ExpandByPoint may expand this bounding box to include the specified point.
func (b *Box2) ExpandByPoint(point Vector2) {
b.Min.SetMin(point)
b.Max.SetMax(point)
}
// ExpandByVector expands this bounding box by the specified vector.
func (b *Box2) ExpandByVector(vector Vector2) {
b.Min.SetSub(vector)
b.Max.SetAdd(vector)
}
// ExpandByScalar expands this bounding box by the specified scalar.
func (b *Box2) ExpandByScalar(scalar float32) {
b.Min.SetSubScalar(scalar)
b.Max.SetAddScalar(scalar)
}
// ExpandByBox may expand this bounding box to include the specified box
func (b *Box2) ExpandByBox(box Box2) {
b.ExpandByPoint(box.Min)
b.ExpandByPoint(box.Max)
}
// MulMatrix2 multiplies the specified matrix to the vertices of this bounding box
// and computes the resulting spanning Box2 of the transformed points
func (b Box2) MulMatrix2(m Matrix2) Box2 {
var cs [4]Vector2
cs[0] = m.MulVector2AsPoint(Vec2(b.Min.X, b.Min.Y))
cs[1] = m.MulVector2AsPoint(Vec2(b.Min.X, b.Max.Y))
cs[2] = m.MulVector2AsPoint(Vec2(b.Max.X, b.Min.Y))
cs[3] = m.MulVector2AsPoint(Vec2(b.Max.X, b.Max.Y))
nb := B2Empty()
for i := 0; i < 4; i++ {
nb.ExpandByPoint(cs[i])
}
return nb
}
// SetFromCenterAndSize set this bounding box from a center point and size.
// Size is a vector from the minimum point to the maximum point.
func (b *Box2) SetFromCenterAndSize(center, size Vector2) {
halfSize := size.MulScalar(0.5)
b.Min = center.Sub(halfSize)
b.Max = center.Add(halfSize)
}
// Center calculates the center point of this bounding box.
func (b Box2) Center() Vector2 {
return b.Min.Add(b.Max).MulScalar(0.5)
}
// Size calculates the size of this bounding box: the vector from
// its minimum point to its maximum point.
func (b Box2) Size() Vector2 {
return b.Max.Sub(b.Min)
}
// ContainsPoint returns if this bounding box contains the specified point.
func (b Box2) ContainsPoint(point Vector2) bool {
if point.X < b.Min.X || point.X > b.Max.X ||
point.Y < b.Min.Y || point.Y > b.Max.Y {
return false
}
return true
}
// ContainsBox returns if this bounding box contains other box.
func (b Box2) ContainsBox(box Box2) bool {
return (b.Min.X <= box.Min.X) && (box.Max.X <= b.Max.X) && (b.Min.Y <= box.Min.Y) && (box.Max.Y <= b.Max.Y)
}
// IntersectsBox returns if other box intersects this one.
func (b Box2) IntersectsBox(other Box2) bool {
if other.Max.X < b.Min.X || other.Min.X > b.Max.X ||
other.Max.Y < b.Min.Y || other.Min.Y > b.Max.Y {
return false
}
return true
}
// ClampPoint calculates a new point which is the specified point clamped inside this box.
func (b Box2) ClampPoint(point Vector2) Vector2 {
point.Clamp(b.Min, b.Max)
return point
}
// DistanceToPoint returns the distance from this box to the specified point.
func (b Box2) DistanceToPoint(point Vector2) float32 {
clamp := b.ClampPoint(point)
return clamp.Sub(point).Length()
}
// Intersect returns the intersection with other box.
func (b Box2) Intersect(other Box2) Box2 {
other.Min.SetMax(b.Min)
other.Max.SetMin(b.Max)
return other
}
// Union returns the union with other box.
func (b Box2) Union(other Box2) Box2 {
other.Min.SetMin(b.Min)
other.Max.SetMax(b.Max)
return other
}
// Translate returns translated position of this box by offset.
func (b Box2) Translate(offset Vector2) Box2 {
nb := Box2{}
nb.Min = b.Min.Add(offset)
nb.Max = b.Max.Add(offset)
return nb
}
// ProjectX projects normalized value along the X dimension of this box
func (b Box2) ProjectX(v float32) float32 {
return b.Min.X + v*(b.Max.X-b.Min.X)
}
// ProjectY projects normalized value along the Y dimension of this box
func (b Box2) ProjectY(v float32) float32 {
return b.Min.Y + v*(b.Max.Y-b.Min.Y)
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
// Box3 represents a 3D bounding box defined by two points:
// the point with minimum coordinates and the point with maximum coordinates.
type Box3 struct {
Min Vector3
Max Vector3
}
// B3 returns a new [Box3] from the given minimum and maximum x, y, and z coordinates.
func B3(x0, y0, z0, x1, y1, z1 float32) Box3 {
return Box3{Vec3(x0, y0, z0), Vec3(x1, y1, z1)}
}
// B3Empty returns a new [Box3] with empty minimum and maximum values.
func B3Empty() Box3 {
bx := Box3{}
bx.SetEmpty()
return bx
}
// SetEmpty set this bounding box to empty (min / max +/- Infinity)
func (b *Box3) SetEmpty() {
b.Min.SetScalar(Infinity)
b.Max.SetScalar(-Infinity)
}
// IsEmpty returns true if this bounding box is empty (max < min on any coord).
func (b Box3) IsEmpty() bool {
return (b.Max.X < b.Min.X) || (b.Max.Y < b.Min.Y) || (b.Max.Z < b.Min.Z)
}
// Set sets this bounding box minimum and maximum coordinates.
// If either min or max are nil, then corresponding values are set to +/- Infinity.
func (b *Box3) Set(min, max *Vector3) {
if min != nil {
b.Min = *min
} else {
b.Min.SetScalar(Infinity)
}
if max != nil {
b.Max = *max
} else {
b.Max.SetScalar(-Infinity)
}
}
// SetFromPoints sets this bounding box from the specified array of points.
func (b *Box3) SetFromPoints(points []Vector3) {
b.SetEmpty()
b.ExpandByPoints(points)
}
// ExpandByPoints may expand this bounding box from the specified array of points.
func (b *Box3) ExpandByPoints(points []Vector3) {
for i := 0; i < len(points); i++ {
b.ExpandByPoint(points[i])
}
}
// ExpandByPoint may expand this bounding box to include the specified point.
func (b *Box3) ExpandByPoint(point Vector3) {
b.Min.SetMin(point)
b.Max.SetMax(point)
}
// ExpandByBox may expand this bounding box to include the specified box
func (b *Box3) ExpandByBox(box Box3) {
b.ExpandByPoint(box.Min)
b.ExpandByPoint(box.Max)
}
// ExpandByVector expands this bounding box by the specified vector
// subtracting from min and adding to max.
func (b *Box3) ExpandByVector(vector Vector3) {
b.Min.SetSub(vector)
b.Max.SetAdd(vector)
}
// ExpandByScalar expands this bounding box by the specified scalar
// subtracting from min and adding to max.
func (b *Box3) ExpandByScalar(scalar float32) {
b.Min.SetSubScalar(scalar)
b.Max.SetAddScalar(scalar)
}
// SetFromCenterAndSize sets this bounding box from a center point and size.
// Size is a vector from the minimum point to the maximum point.
func (b *Box3) SetFromCenterAndSize(center, size Vector3) {
halfSize := size.MulScalar(0.5)
b.Min = center.Sub(halfSize)
b.Max = center.Add(halfSize)
}
// Center returns the center of the bounding box.
func (b Box3) Center() Vector3 {
return b.Min.Add(b.Max).MulScalar(0.5)
}
// Size calculates the size of this bounding box: the vector from
// its minimum point to its maximum point.
func (b Box3) Size() Vector3 {
return b.Max.Sub(b.Min)
}
// ContainsPoint returns if this bounding box contains the specified point.
func (b Box3) ContainsPoint(point Vector3) bool {
if point.X < b.Min.X || point.X > b.Max.X ||
point.Y < b.Min.Y || point.Y > b.Max.Y ||
point.Z < b.Min.Z || point.Z > b.Max.Z {
return false
}
return true
}
// ContainsBox returns if this bounding box contains other box.
func (b Box3) ContainsBox(box Box3) bool {
return (b.Min.X <= box.Max.X) && (box.Max.X <= b.Max.X) &&
(b.Min.Y <= box.Min.Y) && (box.Max.Y <= b.Max.Y) &&
(b.Min.Z <= box.Min.Z) && (box.Max.Z <= b.Max.Z)
}
// IntersectsBox returns if other box intersects this one.
func (b Box3) IntersectsBox(other Box3) bool {
// using 6 splitting planes to rule out intersections.
if other.Max.X < b.Min.X || other.Min.X > b.Max.X ||
other.Max.Y < b.Min.Y || other.Min.Y > b.Max.Y ||
other.Max.Z < b.Min.Z || other.Min.Z > b.Max.Z {
return false
}
return true
}
// ClampPoint returns a new point which is the specified point clamped inside this box.
func (b Box3) ClampPoint(point Vector3) Vector3 {
point.Clamp(b.Min, b.Max)
return point
}
// DistanceToPoint returns the distance from this box to the specified point.
func (b Box3) DistanceToPoint(point Vector3) float32 {
clamp := b.ClampPoint(point)
return clamp.Sub(point).Length()
}
// GetBoundingSphere returns a bounding sphere to this bounding box.
func (b Box3) GetBoundingSphere() Sphere {
return Sphere{b.Center(), b.Size().Length() * 0.5}
}
// Intersect returns the intersection with other box.
func (b Box3) Intersect(other Box3) Box3 {
other.Min.SetMax(b.Min)
other.Max.SetMin(b.Max)
return other
}
// Union returns the union with other box.
func (b Box3) Union(other Box3) Box3 {
other.Min.SetMin(b.Min)
other.Max.SetMax(b.Max)
return other
}
// MulMatrix4 multiplies the specified matrix to the vertices of this bounding box
// and computes the resulting spanning Box3 of the transformed points
func (b Box3) MulMatrix4(m *Matrix4) Box3 {
xax := m[0] * b.Min.X
xay := m[1] * b.Min.X
xaz := m[2] * b.Min.X
xbx := m[0] * b.Max.X
xby := m[1] * b.Max.X
xbz := m[2] * b.Max.X
yax := m[4] * b.Min.Y
yay := m[5] * b.Min.Y
yaz := m[6] * b.Min.Y
ybx := m[4] * b.Max.Y
yby := m[5] * b.Max.Y
ybz := m[6] * b.Max.Y
zax := m[8] * b.Min.Z
zay := m[9] * b.Min.Z
zaz := m[10] * b.Min.Z
zbx := m[8] * b.Max.Z
zby := m[9] * b.Max.Z
zbz := m[10] * b.Max.Z
nb := Box3{}
nb.Min.X = Min(xax, xbx) + Min(yax, ybx) + Min(zax, zbx) + m[12]
nb.Min.Y = Min(xay, xby) + Min(yay, yby) + Min(zay, zby) + m[13]
nb.Min.Z = Min(xaz, xbz) + Min(yaz, ybz) + Min(zaz, zbz) + m[14]
nb.Max.X = Max(xax, xbx) + Max(yax, ybx) + Max(zax, zbx) + m[12]
nb.Max.Y = Max(xay, xby) + Max(yay, yby) + Max(zay, zby) + m[13]
nb.Max.Z = Max(xaz, xbz) + Max(yaz, ybz) + Max(zaz, zbz) + m[14]
return nb
}
// MulQuat multiplies the specified quaternion to the vertices of this bounding box
// and computes the resulting spanning Box3 of the transformed points
func (b Box3) MulQuat(q Quat) Box3 {
var cs [8]Vector3
cs[0] = Vec3(b.Min.X, b.Min.Y, b.Min.Z).MulQuat(q)
cs[1] = Vec3(b.Min.X, b.Min.Y, b.Max.Z).MulQuat(q)
cs[2] = Vec3(b.Min.X, b.Max.Y, b.Min.Z).MulQuat(q)
cs[3] = Vec3(b.Max.X, b.Min.Y, b.Min.Z).MulQuat(q)
cs[4] = Vec3(b.Max.X, b.Max.Y, b.Max.Z).MulQuat(q)
cs[5] = Vec3(b.Max.X, b.Max.Y, b.Min.Z).MulQuat(q)
cs[6] = Vec3(b.Max.X, b.Min.Y, b.Max.Z).MulQuat(q)
cs[7] = Vec3(b.Min.X, b.Max.Y, b.Max.Z).MulQuat(q)
nb := B3Empty()
for i := 0; i < 8; i++ {
nb.ExpandByPoint(cs[i])
}
return nb
}
// Translate returns translated position of this box by offset.
func (b Box3) Translate(offset Vector3) Box3 {
nb := Box3{}
nb.Min = b.Min.Add(offset)
nb.Max = b.Max.Add(offset)
return nb
}
// MVProjToNDC projects bounding box through given MVP model-view-projection Matrix4
// with perspective divide to return normalized display coordinates (NDC).
func (b Box3) MVProjToNDC(m *Matrix4) Box3 {
// all corners: i = min, a = max
var cs [8]Vector3
cs[0] = Vector4{b.Min.X, b.Min.Y, b.Min.Z, 1}.MulMatrix4(m).PerspDiv()
cs[1] = Vector4{b.Min.X, b.Min.Y, b.Max.Z, 1}.MulMatrix4(m).PerspDiv()
cs[2] = Vector4{b.Min.X, b.Max.Y, b.Min.Z, 1}.MulMatrix4(m).PerspDiv()
cs[3] = Vector4{b.Max.X, b.Min.Y, b.Min.Z, 1}.MulMatrix4(m).PerspDiv()
cs[4] = Vector4{b.Max.X, b.Max.Y, b.Max.Z, 1}.MulMatrix4(m).PerspDiv()
cs[5] = Vector4{b.Max.X, b.Max.Y, b.Min.Z, 1}.MulMatrix4(m).PerspDiv()
cs[6] = Vector4{b.Max.X, b.Min.Y, b.Max.Z, 1}.MulMatrix4(m).PerspDiv()
cs[7] = Vector4{b.Min.X, b.Max.Y, b.Max.Z, 1}.MulMatrix4(m).PerspDiv()
nb := B3Empty()
for i := 0; i < 8; i++ {
nb.ExpandByPoint(cs[i])
}
return nb
}
// Copyright 2019 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 math32
import "image/color"
// official correct ones:
// SRGBFromLinear converts a color with linear gamma correction to SRGB standard 2.4 gamma
// with offsets.
func SRGBFromLinear(lin float32) float32 {
if lin <= 0.0031308 {
return lin * 12.92
}
return Pow(lin, 1.0/2.4)*1.055 - 0.055
}
// SRGBToLinear converts a color with SRGB gamma correction to SRGB standard 2.4 gamma
// with offsets
func SRGBToLinear(sr float32) float32 {
if sr <= 0.04045 {
return sr / 12.92
}
return Pow((sr+0.055)/1.055, 2.4)
}
/*
// rough-and-ready approx used in many cases:
func SRGBFromLinear(lin float32) float32 {
return Pow(lin, 1.0/2.2)
}
func SRGBToLinear(sr float32) float32 {
return Pow(sr, 2.2)
}
*/
// NewVector3Color returns a Vector3 from Go standard color.Color
// (R,G,B components only)
func NewVector3Color(clr color.Color) Vector3 {
v3 := Vector3{}
v3.SetColor(clr)
return v3
}
// SetColor sets from Go standard color.Color
// (R,G,B components only)
func (v *Vector3) SetColor(clr color.Color) {
r, g, b, _ := clr.RGBA()
v.X = float32(r) / 0xffff
v.Y = float32(g) / 0xffff
v.Z = float32(b) / 0xffff
}
// NewVector4Color returns a Vector4 from Go standard color.Color
// (full R,G,B,A components)
func NewVector4Color(clr color.Color) Vector4 {
v4 := Vector4{}
v4.SetColor(clr)
return v4
}
// SetColor sets a Vector4 from Go standard color.Color
func (v *Vector4) SetColor(clr color.Color) {
r, g, b, a := clr.RGBA()
v.X = float32(r) / 0xffff
v.Y = float32(g) / 0xffff
v.Z = float32(b) / 0xffff
v.W = float32(a) / 0xffff
}
// SRGBFromLinear returns an SRGB color space value from a linear source
func (v Vector3) SRGBFromLinear() Vector3 {
nv := Vector3{}
nv.X = SRGBFromLinear(v.X)
nv.Y = SRGBFromLinear(v.Y)
nv.Z = SRGBFromLinear(v.Z)
return nv
}
// SRGBToLinear returns a linear color space value from a SRGB source
func (v Vector3) SRGBToLinear() Vector3 {
nv := Vector3{}
nv.X = SRGBToLinear(v.X)
nv.Y = SRGBToLinear(v.Y)
nv.Z = SRGBToLinear(v.Z)
return nv
}
// SRGBFromLinear returns an SRGB color space value from a linear source
func (v Vector4) SRGBFromLinear() Vector4 {
nv := Vector4{}
nv.X = SRGBFromLinear(v.X)
nv.Y = SRGBFromLinear(v.Y)
nv.Z = SRGBFromLinear(v.Z)
nv.W = v.W
return nv
}
// SRGBToLinear returns a linear color space value from a SRGB source
func (v Vector4) SRGBToLinear() Vector4 {
nv := Vector4{}
nv.X = SRGBToLinear(v.X)
nv.Y = SRGBToLinear(v.Y)
nv.Z = SRGBToLinear(v.Z)
nv.W = v.W
return nv
}
// Copyright 2019 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 math32
// Dims is a list of vector dimension (component) names
type Dims int32 //enums:enum
const (
X Dims = iota
Y
Z
W
)
// OtherDim returns the other dimension for 2D X,Y
func OtherDim(d Dims) Dims {
switch d {
case X:
return Y
default:
return X
}
}
func (d Dims) Other() Dims {
return OtherDim(d)
}
// Code generated by "core generate"; DO NOT EDIT.
package math32
import (
"cogentcore.org/core/enums"
)
var _DimsValues = []Dims{0, 1, 2, 3}
// DimsN is the highest valid value for type Dims, plus one.
const DimsN Dims = 4
var _DimsValueMap = map[string]Dims{`X`: 0, `Y`: 1, `Z`: 2, `W`: 3}
var _DimsDescMap = map[Dims]string{0: ``, 1: ``, 2: ``, 3: ``}
var _DimsMap = map[Dims]string{0: `X`, 1: `Y`, 2: `Z`, 3: `W`}
// String returns the string representation of this Dims value.
func (i Dims) String() string { return enums.String(i, _DimsMap) }
// SetString sets the Dims value from its string representation,
// and returns an error if the string is invalid.
func (i *Dims) SetString(s string) error { return enums.SetString(i, s, _DimsValueMap, "Dims") }
// Int64 returns the Dims value as an int64.
func (i Dims) Int64() int64 { return int64(i) }
// SetInt64 sets the Dims value from an int64.
func (i *Dims) SetInt64(in int64) { *i = Dims(in) }
// Desc returns the description of the Dims value.
func (i Dims) Desc() string { return enums.Desc(i, _DimsDescMap) }
// DimsValues returns all possible values for the type Dims.
func DimsValues() []Dims { return _DimsValues }
// Values returns all possible values for the type Dims.
func (i Dims) Values() []enums.Enum { return enums.Values(_DimsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Dims) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Dims) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Dims") }
// Copyright 2021 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 math32
import (
"math"
)
// This is a fast version of the natural exponential function, for highly
// time-sensitive uses where precision is less important.
// based on: N. N. Schraudolph. "A fast, compact approximation of the exponential function." Neural Computation, 11(4), May 1999, pp.853-862.
// as discussed and elaborated here: https://stackoverflow.com/questions/47025373/fastest-implementation-of-the-natural-exponential-function-using-sse
/*
// FastExpBad is the basic original N.N. Schraudolph version
// which has bad error and is no faster than the better cubic
// and quadratic cases.
func FastExpBad(x float32) float32 {
i := int32(1512775*x + 1072632447)
if x <= -88.76731 { // this doesn't add anything and -exp is main use-case anyway
return 0
}
return math.Float32frombits(uint32(i << 32))
}
// FastExp3 is less accurate and no faster than quartic version.
// FastExp3 is a cubic spline approximation to the Exp function, by N.N. Schraudolph
// It does not have any of the sanity checking of a standard method -- returns
// nonsense when arg is out of range. Runs in .24ns vs. 8.7ns for 64bit which is faster
// than math32.Exp actually.
func FastExp3(x float32) float32 {
// const (
// Overflow = 88.43114
// Underflow = -88.76731
// NearZero = 1.0 / (1 << 28) // 2**-28
// )
// special cases
// switch {
// these "sanity check" cases cost about 1 ns
// case IsNaN(x) || IsInf(x, 1): /
// return x
// case IsInf(x, -1):
// return 0
// these cases cost about 4+ ns
// case x >= Overflow:
// return Inf(1)
// case x <= Underflow:
// return 0
// case -NearZero < x && x < NearZero:
// return 1 + x
// }
if x <= -88.76731 { // this doesn't add anything and -exp is main use-case anyway
return 0
}
i := int32(12102203*x) + 127*(1<<23)
m := i >> 7 & 0xFFFF // copy mantissa
i += ((((((((1277 * m) >> 14) + 14825) * m) >> 14) - 79749) * m) >> 11) - 626
return math.Float32frombits(uint32(i))
}
*/
//gosl:start fastexp
// FastExp is a quartic spline approximation to the Exp function, by N.N. Schraudolph
// It does not have any of the sanity checking of a standard method -- returns
// nonsense when arg is out of range. Runs in 2.23ns vs. 6.3ns for 64bit which is faster
// than math32.Exp actually.
func FastExp(x float32) float32 {
if x <= -88.02969 { // this doesn't add anything and -exp is main use-case anyway
return 0.0
}
i := int32(12102203*x) + int32(127)*(int32(1)<<23)
m := i >> 7 & 0xFFFF // copy mantissa
i += (((((((((((3537 * m) >> 16) + 13668) * m) >> 18) + 15817) * m) >> 14) - 80470) * m) >> 11)
return math.Float32frombits(uint32(i))
}
//gosl:end fastexp
// Copyright 2019 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 math32
import "golang.org/x/image/math/fixed"
// ToFixed converts a float32 value to a fixed.Int26_6
func ToFixed(x float32) fixed.Int26_6 {
return fixed.Int26_6(x * 64)
}
// FromFixed converts a fixed.Int26_6 to a float32
func FromFixed(x fixed.Int26_6) float32 {
const shift, mask = 6, 1<<6 - 1
if x >= 0 {
return float32(x>>shift) + float32(x&mask)/64
}
x = -x
if x >= 0 {
return -(float32(x>>shift) + float32(x&mask)/64)
}
return 0
}
// ToFixedPoint converts float32 x,y values to a fixed.Point26_6
func ToFixedPoint(x, y float32) fixed.Point26_6 {
return fixed.Point26_6{X: ToFixed(x), Y: ToFixed(y)}
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
// Frustum represents a frustum
type Frustum struct {
Planes [6]Plane
}
// NewFrustumFromMatrix creates and returns a Frustum based on the provided matrix
func NewFrustumFromMatrix(m *Matrix4) *Frustum {
f := new(Frustum)
f.SetFromMatrix(m)
return f
}
// NewFrustum returns a pointer to a new Frustum object made of 6 explicit planes
func NewFrustum(p0, p1, p2, p3, p4, p5 *Plane) *Frustum {
f := new(Frustum)
f.Set(p0, p1, p2, p3, p4, p5)
return f
}
// Set sets the frustum's planes
func (f *Frustum) Set(p0, p1, p2, p3, p4, p5 *Plane) {
if p0 != nil {
f.Planes[0] = *p0
}
if p1 != nil {
f.Planes[1] = *p1
}
if p2 != nil {
f.Planes[2] = *p2
}
if p3 != nil {
f.Planes[3] = *p3
}
if p4 != nil {
f.Planes[4] = *p4
}
if p5 != nil {
f.Planes[5] = *p5
}
}
// SetFromMatrix sets the frustum's planes based on the specified Matrix4
func (f *Frustum) SetFromMatrix(m *Matrix4) {
me0 := m[0]
me1 := m[1]
me2 := m[2]
me3 := m[3]
me4 := m[4]
me5 := m[5]
me6 := m[6]
me7 := m[7]
me8 := m[8]
me9 := m[9]
me10 := m[10]
me11 := m[11]
me12 := m[12]
me13 := m[13]
me14 := m[14]
me15 := m[15]
f.Planes[0].SetDims(me3-me0, me7-me4, me11-me8, me15-me12)
f.Planes[1].SetDims(me3+me0, me7+me4, me11+me8, me15+me12)
f.Planes[2].SetDims(me3+me1, me7+me5, me11+me9, me15+me13)
f.Planes[3].SetDims(me3-me1, me7-me5, me11-me9, me15-me13)
f.Planes[4].SetDims(me3-me2, me7-me6, me11-me10, me15-me14)
f.Planes[5].SetDims(me3+me2, me7+me6, me11+me10, me15+me14)
for i := 0; i < 6; i++ {
f.Planes[i].Normalize()
}
}
// IntersectsSphere determines whether the specified sphere is intersecting the frustum
func (f *Frustum) IntersectsSphere(sphere Sphere) bool {
negRadius := -sphere.Radius
for i := 0; i < 6; i++ {
dist := f.Planes[i].DistanceToPoint(sphere.Center)
if dist < negRadius {
return false
}
}
return true
}
// IntersectsBox determines whether the specified box is intersecting the frustum
func (f *Frustum) IntersectsBox(box Box3) bool {
var p1 Vector3
var p2 Vector3
for i := 0; i < 6; i++ {
plane := &f.Planes[i]
if plane.Norm.X > 0 {
p1.X = box.Min.X
} else {
p1.X = box.Max.X
}
if plane.Norm.X > 0 {
p2.X = box.Max.X
} else {
p2.X = box.Min.X
}
if plane.Norm.Y > 0 {
p1.Y = box.Min.Y
} else {
p1.Y = box.Max.Y
}
if plane.Norm.Y > 0 {
p2.Y = box.Max.Y
} else {
p2.Y = box.Min.Y
}
if plane.Norm.Z > 0 {
p1.Z = box.Min.Z
} else {
p1.Z = box.Max.Z
}
if plane.Norm.Z > 0 {
p2.Z = box.Max.Z
} else {
p2.Z = box.Min.Z
}
d1 := plane.DistanceToPoint(p1)
d2 := plane.DistanceToPoint(p2)
// if both outside plane, no intersection
if d1 < 0 && d2 < 0 {
return false
}
}
return true
}
// ContainsPoint determines whether the frustum contains the specified point
func (f *Frustum) ContainsPoint(point Vector3) bool {
for i := 0; i < 6; i++ {
if f.Planes[i].DistanceToPoint(point) < 0 {
return false
}
}
return true
}
// 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 math32
import (
"image"
)
// Geom2DInt defines a geometry in 2D dots units (int) -- this is just a more
// convenient format than image.Rectangle for cases where the size and
// position are independently updated (e.g., Viewport)
type Geom2DInt struct {
Pos image.Point
Size image.Point
}
// Bounds converts geom to equivalent image.Rectangle
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
func (gm *Geom2DInt) SizeRect() image.Rectangle {
return image.Rect(0, 0, gm.Size.X, gm.Size.Y)
}
// SetRect sets values from image.Rectangle
func (gm *Geom2DInt) SetRect(r image.Rectangle) {
gm.Pos = r.Min
gm.Size = r.Size()
}
// FitGeomInWindow returns a position and size for a region (sub-window)
// within a larger window geom (pos and size) that fits entirely
// within that window to the extent possible,
// given an initial starting position and size.
// The position is first adjusted to try to fit the size, and then the size
// is adjusted to make it fit if it is still too big.
func FitGeomInWindow(stPos, stSz, winPos, winSz int) (pos, sz int) {
pos = stPos
sz = stSz
// we go through two iterations: one to fix our position and one to fix
// our size. this ensures that we adjust position and not size if we can,
// but we still always end up with valid dimensions by using size as a fallback.
if pos < winPos {
pos = winPos
}
if pos+sz > winPos+winSz { // our max > window max
pos = winPos + winSz - sz // window max - our size
}
if pos < winPos {
pos = winPos
}
if pos+sz > winPos+winSz { // our max > window max
sz = winSz + winPos - pos // window max - our min
}
return
}
// FitInWindow returns a position and size for a region (sub-window)
// within a larger window geom that fits entirely within that window to the
// extent possible, for the initial "ideal" starting position and size.
// The position is first adjusted to try to fit the size, and then the size
// is adjusted to make it fit if it is still too big.
func (gm *Geom2DInt) FitInWindow(win Geom2DInt) Geom2DInt {
var fit Geom2DInt
fit.Pos.X, fit.Size.X = FitGeomInWindow(gm.Pos.X, gm.Size.X, win.Pos.X, win.Size.X)
fit.Pos.Y, fit.Size.Y = FitGeomInWindow(gm.Pos.Y, gm.Size.Y, win.Pos.Y, win.Size.Y)
return fit
}
// Copyright 2024 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 math32
// Line2 represents a 2D line segment defined by a start and an end point.
type Line2 struct {
Start Vector2
End Vector2
}
// NewLine2 creates and returns a new Line2 with the
// specified start and end points.
func NewLine2(start, end Vector2) Line2 {
return Line2{start, end}
}
// Set sets this line segment start and end points.
func (l *Line2) Set(start, end Vector2) {
l.Start = start
l.End = end
}
// Center calculates this line segment center point.
func (l *Line2) Center() Vector2 {
return l.Start.Add(l.End).MulScalar(0.5)
}
// Delta calculates the vector from the start to end point of this line segment.
func (l *Line2) Delta() Vector2 {
return l.End.Sub(l.Start)
}
// LengthSquared returns the square of the distance from the start point to the end point.
func (l *Line2) LengthSquared() float32 {
return l.Start.DistanceToSquared(l.End)
}
// Length returns the length from the start point to the end point.
func (l *Line2) Length() float32 {
return l.Start.DistanceTo(l.End)
}
// note: ClosestPointToPoint is adapted from https://math.stackexchange.com/questions/2193720/find-a-point-on-a-line-segment-which-is-the-closest-to-other-point-not-on-the-li
// ClosestPointToPoint returns the point along the line that is
// closest to the given point.
func (l *Line2) ClosestPointToPoint(point Vector2) Vector2 {
v := l.Delta()
u := point.Sub(l.Start)
vu := v.Dot(u)
ds := v.LengthSquared()
t := vu / ds
switch {
case t <= 0:
return l.Start
case t >= 1:
return l.End
default:
return l.Start.Add(v.MulScalar(t))
}
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
// Line3 represents a 3D line segment defined by a start and an end point.
type Line3 struct {
Start Vector3
End Vector3
}
// NewLine3 creates and returns a new Line3 with the
// specified start and end points.
func NewLine3(start, end Vector3) Line3 {
return Line3{start, end}
}
// Set sets this line segment start and end points.
func (l *Line3) Set(start, end Vector3) {
l.Start = start
l.End = end
}
// Center calculates this line segment center point.
func (l *Line3) Center() Vector3 {
return l.Start.Add(l.End).MulScalar(0.5)
}
// Delta calculates the vector from the start to end point of this line segment.
func (l *Line3) Delta() Vector3 {
return l.End.Sub(l.Start)
}
// DistanceSquared returns the square of the distance from the start point to the end point.
func (l *Line3) DistanceSquared() float32 {
return l.Start.DistanceToSquared(l.End)
}
// Dist returns the distance from the start point to the end point.
func (l *Line3) Dist() float32 {
return l.Start.DistanceTo(l.End)
}
// MulMatrix4 returns specified matrix multiplied to this line segment start and end points.
func (l *Line3) MulMatrix4(mat *Matrix4) Line3 {
return Line3{l.Start.MulMatrix4(mat), l.End.MulMatrix4(mat)}
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
// Package math32 is a float32 based vector, matrix, and math package
// for 2D & 3D graphics.
package math32
//go:generate core generate
import (
"math"
"strconv"
"github.com/chewxy/math32"
)
// These are mostly just wrappers around chewxy/math32, which has
// some optimized implementations.
// Mathematical constants.
const (
E = math.E
Pi = math.Pi
Phi = math.Phi
Sqrt2 = math.Sqrt2
SqrtE = math.SqrtE
SqrtPi = math.SqrtPi
SqrtPhi = math.SqrtPhi
Ln2 = math.Ln2
Log2E = math.Log2E
Ln10 = math.Ln10
Log10E = math.Log10E
)
// Floating-point limit values.
// Max is the largest finite value representable by the type.
// SmallestNonzero is the smallest positive, non-zero value representable by the type.
const (
MaxFloat32 = math.MaxFloat32
SmallestNonzeroFloat32 = math.SmallestNonzeroFloat32
)
const (
// DegToRadFactor is the number of radians per degree.
DegToRadFactor = Pi / 180
// RadToDegFactor is the number of degrees per radian.
RadToDegFactor = 180 / Pi
)
// Infinity is positive infinity.
var Infinity = float32(math.Inf(1))
// DegToRad converts a number from degrees to radians
func DegToRad(degrees float32) float32 {
return degrees * DegToRadFactor
}
// RadToDeg converts a number from radians to degrees
func RadToDeg(radians float32) float32 {
return radians * RadToDegFactor
}
// Abs returns the absolute value of x.
//
// Special cases are:
//
// Abs(±Inf) = +Inf
// Abs(NaN) = NaN
func Abs(x float32) float32 {
return math32.Abs(x)
}
// Sign returns -1 if x < 0 and 1 otherwise.
func Sign(x float32) float32 {
if x < 0 {
return -1
}
return 1
}
// Acos returns the arccosine, in radians, of x.
//
// Special case is:
//
// Acos(x) = NaN if x < -1 or x > 1
func Acos(x float32) float32 {
return math32.Acos(x)
}
// Acosh returns the inverse hyperbolic cosine of x.
//
// Special cases are:
//
// Acosh(+Inf) = +Inf
// Acosh(x) = NaN if x < 1
// Acosh(NaN) = NaN
func Acosh(x float32) float32 {
return math32.Acosh(x)
}
// Asin returns the arcsine, in radians, of x.
//
// Special cases are:
//
// Asin(±0) = ±0
// Asin(x) = NaN if x < -1 or x > 1
func Asin(x float32) float32 {
return math32.Asin(x)
}
// Asinh returns the inverse hyperbolic sine of x.
//
// Special cases are:
//
// Asinh(±0) = ±0
// Asinh(±Inf) = ±Inf
// Asinh(NaN) = NaN
func Asinh(x float32) float32 {
return math32.Asinh(x)
}
// Atan returns the arctangent, in radians, of x.
//
// Special cases are:
//
// Atan(±0) = ±0
// Atan(±Inf) = ±Pi/2
func Atan(x float32) float32 {
return math32.Atan(x)
}
// Atan2 returns the arc tangent of y/x, using the signs of the two to determine the quadrant of the return value.
// Special cases are (in order):
//
// Atan2(y, NaN) = NaN
// Atan2(NaN, x) = NaN
// Atan2(+0, x>=0) = +0
// Atan2(-0, x>=0) = -0
// Atan2(+0, x<=-0) = +Pi
// Atan2(-0, x<=-0) = -Pi
// Atan2(y>0, 0) = +Pi/2
// Atan2(y<0, 0) = -Pi/2
// Atan2(+Inf, +Inf) = +Pi/4
// Atan2(-Inf, +Inf) = -Pi/4
// Atan2(+Inf, -Inf) = 3Pi/4
// Atan2(-Inf, -Inf) = -3Pi/4
// Atan2(y, +Inf) = 0
// Atan2(y>0, -Inf) = +Pi
// Atan2(y<0, -Inf) = -Pi
// Atan2(+Inf, x) = +Pi/2
// Atan2(-Inf, x) = -Pi/2
func Atan2(y, x float32) float32 {
return math32.Atan2(y, x)
}
// Atanh returns the inverse hyperbolic tangent of x.
//
// Special cases are:
//
// Atanh(1) = +Inf
// Atanh(±0) = ±0
// Atanh(-1) = -Inf
// Atanh(x) = NaN if x < -1 or x > 1
// Atanh(NaN) = NaN
func Atanh(x float32) float32 {
return math32.Atanh(x)
}
// Cbrt returns the cube root of x.
//
// Special cases are:
//
// Cbrt(±0) = ±0
// Cbrt(±Inf) = ±Inf
// Cbrt(NaN) = NaN
func Cbrt(x float32) float32 {
return math32.Cbrt(x)
}
// Ceil returns the least integer value greater than or equal to x.
//
// Special cases are:
//
// Ceil(±0) = ±0
// Ceil(±Inf) = ±Inf
// Ceil(NaN) = NaN
func Ceil(x float32) float32 {
return math32.Ceil(x)
}
// Copysign returns a value with the magnitude of f
// and the sign of sign.
func Copysign(f, sign float32) float32 {
return math32.Copysign(f, sign)
}
// Cos returns the cosine of the radian argument x.
//
// Special cases are:
//
// Cos(±Inf) = NaN
// Cos(NaN) = NaN
func Cos(x float32) float32 {
return math32.Cos(x)
}
// Cosh returns the hyperbolic cosine of x.
//
// Special cases are:
//
// Cosh(±0) = 1
// Cosh(±Inf) = +Inf
// Cosh(NaN) = NaN
func Cosh(x float32) float32 {
return math32.Cosh(x)
}
// Dim returns the maximum of x-y or 0.
//
// Special cases are:
//
// Dim(+Inf, +Inf) = NaN
// Dim(-Inf, -Inf) = NaN
// Dim(x, NaN) = Dim(NaN, x) = NaN
func Dim(x, y float32) float32 {
return math32.Dim(x, y)
}
// Erf returns the error function of x.
//
// Special cases are:
//
// Erf(+Inf) = 1
// Erf(-Inf) = -1
// Erf(NaN) = NaN
func Erf(x float32) float32 {
return math32.Erf(x)
}
// Erfc returns the complementary error function of x.
//
// Special cases are:
//
// Erfc(+Inf) = 0
// Erfc(-Inf) = 2
// Erfc(NaN) = NaN
func Erfc(x float32) float32 {
return math32.Erfc(x)
}
// Erfcinv returns the inverse of Erfc(x).
//
// Special cases are:
//
// Erfcinv(0) = +Inf
// Erfcinv(2) = -Inf
// Erfcinv(x) = NaN if x < 0 or x > 2
// Erfcinv(NaN) = NaN
func Erfcinv(x float32) float32 {
return float32(math.Erfcinv(float64(x)))
}
// Erfinv returns the inverse error function of x.
//
// Special cases are:
//
// Erfinv(1) = +Inf
// Erfinv(-1) = -Inf
// Erfinv(x) = NaN if x < -1 or x > 1
// Erfinv(NaN) = NaN
func Erfinv(x float32) float32 {
return float32(math.Erfinv(float64(x)))
}
// Exp returns e**x, the base-e exponential of x.
//
// Special cases are:
//
// Exp(+Inf) = +Inf
// Exp(NaN) = NaN
//
// Very large values overflow to 0 or +Inf.
// Very small values underflow to 1.
func Exp(x float32) float32 {
return math32.Exp(x)
}
// Exp2 returns 2**x, the base-2 exponential of x.
//
// Special cases are the same as Exp.
func Exp2(x float32) float32 {
return math32.Exp2(x)
}
// Expm1 returns e**x - 1, the base-e exponential of x minus 1.
// It is more accurate than Exp(x) - 1 when x is near zero.
//
// Special cases are:
//
// Expm1(+Inf) = +Inf
// Expm1(-Inf) = -1
// Expm1(NaN) = NaN
//
// Very large values overflow to -1 or +Inf.
func Expm1(x float32) float32 {
return math32.Expm1(x)
}
// FMA returns x * y + z, computed with only one rounding.
// (That is, FMA returns the fused multiply add of x, y, and z.)
func FMA(x, y, z float32) float32 {
return float32(math.FMA(float64(x), float64(y), float64(z)))
}
// Floor returns the greatest integer value less than or equal to x.
//
// Special cases are:
//
// Floor(±0) = ±0
// Floor(±Inf) = ±Inf
// Floor(NaN) = NaN
func Floor(x float32) float32 {
return math32.Floor(x)
}
// Frexp breaks f into a normalized fraction
// and an integral power of two.
// It returns frac and exp satisfying f == frac × 2**exp,
// with the absolute value of frac in the interval [½, 1).
//
// Special cases are:
//
// Frexp(±0) = ±0, 0
// Frexp(±Inf) = ±Inf, 0
// Frexp(NaN) = NaN, 0
func Frexp(f float32) (frac float32, exp int) {
return math32.Frexp(f)
}
// Gamma returns the Gamma function of x.
//
// Special cases are:
//
// Gamma(+Inf) = +Inf
// Gamma(+0) = +Inf
// Gamma(-0) = -Inf
// Gamma(x) = NaN for integer x < 0
// Gamma(-Inf) = NaN
// Gamma(NaN) = NaN
func Gamma(x float32) float32 {
return math32.Gamma(x)
}
// Hypot returns Sqrt(p*p + q*q), taking care to avoid
// unnecessary overflow and underflow.
//
// Special cases are:
//
// Hypot(±Inf, q) = +Inf
// Hypot(p, ±Inf) = +Inf
// Hypot(NaN, q) = NaN
// Hypot(p, NaN) = NaN
func Hypot(p, q float32) float32 {
return math32.Hypot(p, q)
}
// Ilogb returns the binary exponent of x as an integer.
//
// Special cases are:
//
// Ilogb(±Inf) = MaxInt32
// Ilogb(0) = MinInt32
// Ilogb(NaN) = MaxInt32
func Ilogb(x float32) float32 {
return float32(math32.Ilogb(x))
}
// Inf returns positive infinity if sign >= 0, negative infinity if sign < 0.
func Inf(sign int) float32 {
return math32.Inf(sign)
}
// IsInf reports whether f is an infinity, according to sign.
// If sign > 0, IsInf reports whether f is positive infinity.
// If sign < 0, IsInf reports whether f is negative infinity.
// If sign == 0, IsInf reports whether f is either infinity.
func IsInf(x float32, sign int) bool {
return math32.IsInf(x, sign)
}
// IsNaN reports whether f is an IEEE 754 “not-a-number” value.
func IsNaN(x float32) bool {
return math32.IsNaN(x)
}
// J0 returns the order-zero Bessel function of the first kind.
//
// Special cases are:
//
// J0(±Inf) = 0
// J0(0) = 1
// J0(NaN) = NaN
func J0(x float32) float32 {
return math32.J0(x)
}
// J1 returns the order-one Bessel function of the first kind.
//
// Special cases are:
//
// J1(±Inf) = 0
// J1(NaN) = NaN
func J1(x float32) float32 {
return math32.J1(x)
}
// Jn returns the order-n Bessel function of the first kind.
//
// Special cases are:
//
// Jn(n, ±Inf) = 0
// Jn(n, NaN) = NaN
func Jn(n int, x float32) float32 {
return math32.Jn(n, x)
}
// Ldexp is the inverse of Frexp.
// It returns frac × 2**exp.
//
// Special cases are:
//
// Ldexp(±0, exp) = ±0
// Ldexp(±Inf, exp) = ±Inf
// Ldexp(NaN, exp) = NaN
func Ldexp(frac float32, exp int) float32 {
return math32.Ldexp(frac, exp)
}
// Lerp returns the linear interpolation between start and stop in proportion to amount
func Lerp(start, stop, amount float32) float32 {
return (1-amount)*start + amount*stop
}
// Lgamma returns the natural logarithm and sign (-1 or +1) of Gamma(x).
//
// Special cases are:
//
// Lgamma(+Inf) = +Inf
// Lgamma(0) = +Inf
// Lgamma(-integer) = +Inf
// Lgamma(-Inf) = -Inf
// Lgamma(NaN) = NaN
func Lgamma(x float32) (lgamma float32, sign int) {
return math32.Lgamma(x)
}
// Log returns the natural logarithm of x.
//
// Special cases are:
//
// Log(+Inf) = +Inf
// Log(0) = -Inf
// Log(x < 0) = NaN
// Log(NaN) = NaN
func Log(x float32) float32 {
return math32.Log(x)
}
// Log10 returns the decimal logarithm of x.
// The special cases are the same as for Log.
func Log10(x float32) float32 {
return math32.Log10(x)
}
// Log1p returns the natural logarithm of 1 plus its argument x.
// It is more accurate than Log(1 + x) when x is near zero.
//
// Special cases are:
//
// Log1p(+Inf) = +Inf
// Log1p(±0) = ±0
// Log1p(-1) = -Inf
// Log1p(x < -1) = NaN
// Log1p(NaN) = NaN
func Log1p(x float32) float32 {
return math32.Log1p(x)
}
// Log2 returns the binary logarithm of x.
// The special cases are the same as for Log.
func Log2(x float32) float32 {
return math32.Log2(x)
}
// Logb returns the binary exponent of x.
//
// Special cases are:
//
// Logb(±Inf) = +Inf
// Logb(0) = -Inf
// Logb(NaN) = NaN
func Logb(x float32) float32 {
return math32.Logb(x)
}
// TODO(kai): should we use builtin max and min?
// Max returns the larger of x or y.
//
// Special cases are:
//
// Max(x, +Inf) = Max(+Inf, x) = +Inf
// Max(x, NaN) = Max(NaN, x) = NaN
// Max(+0, ±0) = Max(±0, +0) = +0
// Max(-0, -0) = -0
//
// Note that this differs from the built-in function max when called
// with NaN and +Inf.
func Max(x, y float32) float32 {
return math32.Max(x, y)
}
// Min returns the smaller of x or y.
//
// Special cases are:
//
// Min(x, -Inf) = Min(-Inf, x) = -Inf
// Min(x, NaN) = Min(NaN, x) = NaN
// Min(-0, ±0) = Min(±0, -0) = -0
//
// Note that this differs from the built-in function min when called
// with NaN and -Inf.
func Min(x, y float32) float32 {
return math32.Min(x, y)
}
// Mod returns the floating-point remainder of x/y.
// The magnitude of the result is less than y and its
// sign agrees with that of x.
//
// Special cases are:
//
// Mod(±Inf, y) = NaN
// Mod(NaN, y) = NaN
// Mod(x, 0) = NaN
// Mod(x, ±Inf) = x
// Mod(x, NaN) = NaN
func Mod(x, y float32) float32 {
return math32.Mod(x, y)
}
// Modf returns integer and fractional floating-point numbers
// that sum to f. Both values have the same sign as f.
//
// Special cases are:
//
// Modf(±Inf) = ±Inf, NaN
// Modf(NaN) = NaN, NaN
func Modf(f float32) (it float32, frac float32) {
return math32.Modf(f)
}
// NaN returns an IEEE 754 “not-a-number” value.
func NaN() float32 {
return math32.NaN()
}
// Nextafter returns the next representable float32 value after x towards y.
//
// Special cases are:
//
// Nextafter32(x, x) = x
// Nextafter32(NaN, y) = NaN
// Nextafter32(x, NaN) = NaN
func Nextafter(x, y float32) float32 {
return math32.Nextafter(x, y)
}
// Pow returns x**y, the base-x exponential of y.
//
// Special cases are (in order):
//
// Pow(x, ±0) = 1 for any x
// Pow(1, y) = 1 for any y
// Pow(x, 1) = x for any x
// Pow(NaN, y) = NaN
// Pow(x, NaN) = NaN
// Pow(±0, y) = ±Inf for y an odd integer < 0
// Pow(±0, -Inf) = +Inf
// Pow(±0, +Inf) = +0
// Pow(±0, y) = +Inf for finite y < 0 and not an odd integer
// Pow(±0, y) = ±0 for y an odd integer > 0
// Pow(±0, y) = +0 for finite y > 0 and not an odd integer
// Pow(-1, ±Inf) = 1
// Pow(x, +Inf) = +Inf for |x| > 1
// Pow(x, -Inf) = +0 for |x| > 1
// Pow(x, +Inf) = +0 for |x| < 1
// Pow(x, -Inf) = +Inf for |x| < 1
// Pow(+Inf, y) = +Inf for y > 0
// Pow(+Inf, y) = +0 for y < 0
// Pow(-Inf, y) = Pow(-0, -y)
// Pow(x, y) = NaN for finite x < 0 and finite non-integer y
func Pow(x, y float32) float32 {
return math32.Pow(x, y)
}
// Pow10 returns 10**n, the base-10 exponential of n.
//
// Special cases are:
//
// Pow10(n) = 0 for n < -323
// Pow10(n) = +Inf for n > 308
func Pow10(n int) float32 {
return math32.Pow10(n)
}
// Remainder returns the IEEE 754 floating-point remainder of x/y.
//
// Special cases are:
//
// Remainder(±Inf, y) = NaN
// Remainder(NaN, y) = NaN
// Remainder(x, 0) = NaN
// Remainder(x, ±Inf) = x
// Remainder(x, NaN) = NaN
func Remainder(x, y float32) float32 {
return math32.Remainder(x, y)
}
// Round returns the nearest integer, rounding half away from zero.
//
// Special cases are:
//
// Round(±0) = ±0
// Round(±Inf) = ±Inf
// Round(NaN) = NaN
func Round(x float32) float32 {
return math32.Round(x)
}
// RoundToEven returns the nearest integer, rounding ties to even.
//
// Special cases are:
//
// RoundToEven(±0) = ±0
// RoundToEven(±Inf) = ±Inf
// RoundToEven(NaN) = NaN
func RoundToEven(x float32) float32 {
return float32(math.RoundToEven(float64(x)))
}
// Signbit returns true if x is negative or negative zero.
func Signbit(x float32) bool {
return math32.Signbit(x)
}
// Sin returns the sine of the radian argument x.
//
// Special cases are:
//
// Sin(±0) = ±0
// Sin(±Inf) = NaN
// Sin(NaN) = NaN
func Sin(x float32) float32 {
return math32.Sin(x)
}
// Sincos returns Sin(x), Cos(x).
//
// Special cases are:
//
// Sincos(±0) = ±0, 1
// Sincos(±Inf) = NaN, NaN
// Sincos(NaN) = NaN, NaN
func Sincos(x float32) (sin, cos float32) {
return math32.Sincos(x)
}
// Sinh returns the hyperbolic sine of x.
//
// Special cases are:
//
// Sinh(±0) = ±0
// Sinh(±Inf) = ±Inf
// Sinh(NaN) = NaN
func Sinh(x float32) float32 {
return math32.Sinh(x)
}
// Sqrt returns the square root of x.
//
// Special cases are:
//
// Sqrt(+Inf) = +Inf
// Sqrt(±0) = ±0
// Sqrt(x < 0) = NaN
// Sqrt(NaN) = NaN
func Sqrt(x float32) float32 {
return math32.Sqrt(x)
}
// Tan returns the tangent of the radian argument x.
//
// Special cases are:
//
// Tan(±0) = ±0
// Tan(±Inf) = NaN
// Tan(NaN) = NaN
func Tan(x float32) float32 {
return math32.Tan(x)
}
// Tanh returns the hyperbolic tangent of x.
//
// Special cases are:
//
// Tanh(±0) = ±0
// Tanh(±Inf) = ±1
// Tanh(NaN) = NaN
func Tanh(x float32) float32 {
return math32.Tanh(x)
}
// Trunc returns the integer value of x.
//
// Special cases are:
//
// Trunc(±0) = ±0
// Trunc(±Inf) = ±Inf
// Trunc(NaN) = NaN
func Trunc(x float32) float32 {
return math32.Trunc(x)
}
// Y0 returns the order-zero Bessel function of the second kind.
//
// Special cases are:
//
// Y0(+Inf) = 0
// Y0(0) = -Inf
// Y0(x < 0) = NaN
// Y0(NaN) = NaN
func Y0(x float32) float32 {
return math32.Y0(x)
}
// Y1 returns the order-one Bessel function of the second kind.
//
// Special cases are:
//
// Y1(+Inf) = 0
// Y1(0) = -Inf
// Y1(x < 0) = NaN
// Y1(NaN) = NaN
func Y1(x float32) float32 {
return math32.Y1(x)
}
// Yn returns the order-n Bessel function of the second kind.
//
// Special cases are:
//
// Yn(n, +Inf) = 0
// Yn(n ≥ 0, 0) = -Inf
// Yn(n < 0, 0) = +Inf if n is odd, -Inf if n is even
// Yn(n, x < 0) = NaN
// Yn(n, NaN) = NaN
func Yn(n int, x float32) float32 {
return math32.Yn(n, x)
}
//////////////////////////////////////////////////////////////
// Special additions to math. functions
// Clamp clamps x to the provided closed interval [a, b]
func Clamp(x, a, b float32) float32 {
if x < a {
return a
}
if x > b {
return b
}
return x
}
// ClampInt clamps x to the provided closed interval [a, b]
func ClampInt(x, a, b int) int {
if x < a {
return a
}
if x > b {
return b
}
return x
}
// MinPos returns the minimum of the two values, excluding any that are <= 0
func MinPos(a, b float32) float32 {
if a > 0 && b > 0 {
return Min(a, b)
} else if a > 0 {
return a
} else if b > 0 {
return b
}
return a
}
// MaxPos returns the minimum of the two values, excluding any that are <= 0
func MaxPos(a, b float32) float32 {
if a > 0 && b > 0 {
return Max(a, b)
} else if a > 0 {
return a
} else if b > 0 {
return b
}
return a
}
// IntMultiple returns the interger multiple of mod closest to given value:
// Round(val / mod) * mod
func IntMultiple(val, mod float32) float32 {
return Round(val/mod) * mod
}
// IntMultipleGE returns the interger multiple of mod >= given value:
// Ceil(val / mod) * mod
func IntMultipleGE(val, mod float32) float32 {
return Ceil(val/mod) * mod
}
// TODO: maybe make these functions faster at some point
// Truncate rounds a float32 number to the given level of precision,
// which the number of significant digits to include in the result.
func Truncate(val float32, prec int) float32 {
frep := strconv.FormatFloat(float64(val), 'g', prec, 32)
tval, _ := strconv.ParseFloat(frep, 32)
return float32(tval)
// note: this unfortunately does not work. also Pow(prec) is not likely to be that much faster ;)
// pow := Pow(10, float32(prec))
// return Round(val*pow) / pow
}
// Truncate64 rounds a float64 number to the given level of precision,
// which the number of significant digits to include in the result.
func Truncate64(val float64, prec int) float64 {
frep := strconv.FormatFloat(val, 'g', prec, 64)
val, _ = strconv.ParseFloat(frep, 64)
return val
// note: this unfortunately does not work. also Pow(prec) is not likely to be that much faster ;)
// pow := math.Pow(10, float64(prec))
// return math.Round(val*pow) / pow
}
// Copyright (c) 2020, 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 math32
import (
"fmt"
"log"
"strconv"
"strings"
"unicode"
"cogentcore.org/core/base/errors"
"golang.org/x/image/math/fixed"
)
/*
This is heavily modified from: https://github.com/fogleman/gg
Copyright (C) 2016 Michael Fogleman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// Matrix2 is a 3x2 matrix.
type Matrix2 struct {
XX, YX, XY, YY, X0, Y0 float32
}
// Identity2 returns a new identity [Matrix2] matrix.
func Identity2() Matrix2 {
return Matrix2{
1, 0,
0, 1,
0, 0,
}
}
func (m Matrix2) IsIdentity() bool {
return m.XX == 1 && m.YX == 0 && m.XY == 0 && m.YY == 1 && m.X0 == 0 && m.Y0 == 0
}
// Translate2D returns a Matrix2 2D matrix with given translations
func Translate2D(x, y float32) Matrix2 {
return Matrix2{
1, 0,
0, 1,
x, y,
}
}
// Scale2D returns a Matrix2 2D matrix with given scaling factors
func Scale2D(x, y float32) Matrix2 {
return Matrix2{
x, 0,
0, y,
0, 0,
}
}
// Rotate2D returns a Matrix2 2D matrix with given rotation, specified in radians
func Rotate2D(angle float32) Matrix2 {
c := float32(Cos(angle))
s := float32(Sin(angle))
return Matrix2{
c, s,
-s, c,
0, 0,
}
}
// Shear2D returns a Matrix2 2D matrix with given shearing
func Shear2D(x, y float32) Matrix2 {
return Matrix2{
1, y,
x, 1,
0, 0,
}
}
// Skew2D returns a Matrix2 2D matrix with given skewing
func Skew2D(x, y float32) Matrix2 {
return Matrix2{
1, Tan(y),
Tan(x), 1,
0, 0,
}
}
// Mul returns a*b
func (a Matrix2) Mul(b Matrix2) Matrix2 {
return Matrix2{
XX: a.XX*b.XX + a.XY*b.YX,
YX: a.YX*b.XX + a.YY*b.YX,
XY: a.XX*b.XY + a.XY*b.YY,
YY: a.YX*b.XY + a.YY*b.YY,
X0: a.XX*b.X0 + a.XY*b.Y0 + a.X0,
Y0: a.YX*b.X0 + a.YY*b.Y0 + a.Y0,
}
}
// SetMul sets a to a*b
func (a *Matrix2) SetMul(b Matrix2) {
*a = a.Mul(b)
}
// MulVector2AsVector multiplies the Vector2 as a vector without adding translations.
// This is for directional vectors and not points.
func (a Matrix2) MulVector2AsVector(v Vector2) Vector2 {
tx := a.XX*v.X + a.XY*v.Y
ty := a.YX*v.X + a.YY*v.Y
return Vec2(tx, ty)
}
// MulVector2AsPoint multiplies the Vector2 as a point, including adding translations.
func (a Matrix2) MulVector2AsPoint(v Vector2) Vector2 {
tx := a.XX*v.X + a.XY*v.Y + a.X0
ty := a.YX*v.X + a.YY*v.Y + a.Y0
return Vec2(tx, ty)
}
// 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)
y := fixed.Int26_6((float32(fp.X)*a.YX + float32(fp.Y)*a.YY) + a.Y0*32)
return fixed.Point26_6{x, y}
}
func (a Matrix2) Translate(x, y float32) Matrix2 {
return a.Mul(Translate2D(x, y))
}
func (a Matrix2) Scale(x, y float32) Matrix2 {
return a.Mul(Scale2D(x, y))
}
func (a Matrix2) Rotate(angle float32) Matrix2 {
return a.Mul(Rotate2D(angle))
}
func (a Matrix2) Shear(x, y float32) Matrix2 {
return a.Mul(Shear2D(x, y))
}
func (a Matrix2) Skew(x, y float32) Matrix2 {
return a.Mul(Skew2D(x, y))
}
// ExtractRot extracts the rotation component from a given matrix
func (a Matrix2) ExtractRot() float32 {
return Atan2(-a.XY, a.XX)
}
// 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
}
// Inverse returns inverse of matrix, for inverting transforms
func (a Matrix2) Inverse() Matrix2 {
// homogenous rep, rc indexes, mapping into Matrix3 code
// XX YX X0 n11 n12 n13 a b x
// XY YY Y0 n21 n22 n23 c d y
// 0 0 1 n31 n32 n33 0 0 1
// t11 := a.YY
// t12 := -a.YX
// t13 := a.Y0*a.YX - a.YY*a.X0
det := a.XX*a.YY - a.XY*a.YX // ad - bc
detInv := 1 / det
b := Matrix2{}
b.XX = a.YY * detInv // a = d
b.XY = -a.XY * detInv // c = -c
b.YX = -a.YX * detInv // b = -b
b.YY = a.XX * detInv // d = a
b.X0 = (a.Y0*a.XY - a.YY*a.X0) * detInv
b.Y0 = (a.X0*a.YX - a.XX*a.Y0) * detInv
return b
}
// ParseFloat32 logs any strconv.ParseFloat errors
func ParseFloat32(pstr string) (float32, error) {
r, err := strconv.ParseFloat(pstr, 32)
if err != nil {
log.Printf("core.ParseFloat32: error parsing float32 number from: %v, %v\n", pstr, err)
return float32(0.0), err
}
return float32(r), nil
}
// ParseAngle32 returns radians angle from string that can specify units (deg,
// grad, rad) -- deg is assumed if not specified
func ParseAngle32(pstr string) (float32, error) {
units := "deg"
lstr := strings.ToLower(pstr)
if strings.Contains(lstr, "deg") {
units = "deg"
lstr = strings.TrimSuffix(lstr, "deg")
} else if strings.Contains(lstr, "grad") {
units = "grad"
lstr = strings.TrimSuffix(lstr, "grad")
} else if strings.Contains(lstr, "rad") {
units = "rad"
lstr = strings.TrimSuffix(lstr, "rad")
}
r, err := strconv.ParseFloat(lstr, 32)
if err != nil {
log.Printf("core.ParseAngle32: error parsing float32 number from: %v, %v\n", lstr, err)
return float32(0.0), err
}
switch units {
case "deg":
return DegToRad(float32(r)), nil
case "grad":
return float32(r) * Pi / 200, nil
case "rad":
return float32(r), nil
}
return float32(r), nil
}
// ReadPoints reads a set of floating point values from a SVG format number
// string -- returns a slice or nil if there was an error
func ReadPoints(pstr string) []float32 {
lastIndex := -1
var pts []float32
lr := ' '
for i, r := range pstr {
if !unicode.IsNumber(r) && r != '.' && !(r == '-' && lr == 'e') && r != 'e' {
if lastIndex != -1 {
s := pstr[lastIndex:i]
p, err := ParseFloat32(s)
if err != nil {
return nil
}
pts = append(pts, p)
}
if r == '-' {
lastIndex = i
} else {
lastIndex = -1
}
} else if lastIndex == -1 {
lastIndex = i
}
lr = r
}
if lastIndex != -1 && lastIndex != len(pstr) {
s := pstr[lastIndex:]
p, err := ParseFloat32(s)
if err != nil {
return nil
}
pts = append(pts, p)
}
return pts
}
// PointsCheckN checks the number of points read and emits an error if not equal to n
func PointsCheckN(pts []float32, n int, errmsg string) error {
if len(pts) != n {
return fmt.Errorf("%v incorrect number of points: %v != %v", errmsg, len(pts), n)
}
return nil
}
// SetString processes the standard SVG-style transform strings
func (a *Matrix2) SetString(str string) error {
errmsg := "math32.Matrix2.SetString:"
str = strings.ToLower(strings.TrimSpace(str))
*a = Identity2()
if str == "none" {
*a = Identity2()
return nil
}
// could have multiple transforms
for {
pidx := strings.IndexByte(str, '(')
if pidx < 0 {
err := fmt.Errorf("%s no params for transform: %v", errmsg, str)
return errors.Log(err)
}
cmd := str[:pidx]
vals := str[pidx+1:]
nxt := ""
eidx := strings.IndexByte(vals, ')')
if eidx > 0 {
nxt = strings.TrimSpace(vals[eidx+1:])
if strings.HasPrefix(nxt, ";") {
nxt = strings.TrimSpace(strings.TrimPrefix(nxt, ";"))
}
vals = vals[:eidx]
}
pts := ReadPoints(vals)
switch cmd {
case "matrix":
if err := PointsCheckN(pts, 6, errmsg); err != nil {
errors.Log(err)
} else {
*a = Matrix2{pts[0], pts[1], pts[2], pts[3], pts[4], pts[5]}
}
case "translate":
if len(pts) == 1 {
*a = a.Translate(pts[0], 0)
} else if len(pts) == 2 {
*a = a.Translate(pts[0], pts[1])
} else {
errors.Log(PointsCheckN(pts, 2, errmsg))
}
case "translatex":
if err := PointsCheckN(pts, 1, errmsg); err != nil {
errors.Log(err)
} else {
*a = a.Translate(pts[0], 0)
}
case "translatey":
if err := PointsCheckN(pts, 1, errmsg); err != nil {
errors.Log(err)
} else {
*a = a.Translate(0, pts[0])
}
case "scale":
if len(pts) == 1 {
*a = a.Scale(pts[0], pts[0])
} else if len(pts) == 2 {
*a = a.Scale(pts[0], pts[1])
} else {
err := fmt.Errorf("%v incorrect number of points: 2 != %v", errmsg, len(pts))
errors.Log(err)
}
case "scalex":
if err := PointsCheckN(pts, 1, errmsg); err != nil {
errors.Log(err)
} else {
*a = a.Scale(pts[0], 1)
}
case "scaley":
if err := PointsCheckN(pts, 1, errmsg); err != nil {
errors.Log(err)
} else {
*a = a.Scale(1, pts[0])
}
case "rotate":
ang := DegToRad(pts[0]) // always in degrees in this form
if len(pts) == 3 {
*a = a.Translate(pts[1], pts[2]).Rotate(ang).Translate(-pts[1], -pts[2])
} else if len(pts) == 1 {
*a = a.Rotate(ang)
} else {
errors.Log(PointsCheckN(pts, 1, errmsg))
}
case "skew":
if err := PointsCheckN(pts, 2, errmsg); err != nil {
errors.Log(err)
} else {
*a = a.Skew(pts[0], pts[1])
}
case "skewx":
if err := PointsCheckN(pts, 1, errmsg); err != nil {
errors.Log(err)
} else {
*a = a.Skew(pts[0], 0)
}
case "skewy":
if err := PointsCheckN(pts, 1, errmsg); err != nil {
errors.Log(err)
} else {
*a = a.Skew(0, pts[0])
}
default:
return fmt.Errorf("unknown command %q", cmd)
}
if nxt == "" {
break
}
if !strings.Contains(nxt, "(") {
break
}
str = nxt
}
return nil
}
// String returns the XML-based string representation of the transform
func (a *Matrix2) String() string {
if a.IsIdentity() {
return "none"
}
if a.YX == 0 && a.XY == 0 { // no rotation, emit scale and translate
str := ""
if a.X0 != 0 || a.Y0 != 0 {
str += fmt.Sprintf("translate(%g,%g)", a.X0, a.Y0)
}
if a.XX != 1 || a.YY != 1 {
if str != "" {
str += " "
}
str += fmt.Sprintf("scale(%g,%g)", a.XX, a.YY)
}
return str
}
// just report the whole matrix
return fmt.Sprintf("matrix(%g,%g,%g,%g,%g,%g)", a.XX, a.YX, a.XY, a.YY, a.X0, a.Y0)
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
import "errors"
// Matrix3 is 3x3 matrix organized internally as column matrix.
type Matrix3 [9]float32
// Identity3 returns a new identity [Matrix3] matrix.
func Identity3() Matrix3 {
m := Matrix3{}
m.SetIdentity()
return m
}
func Matrix3FromMatrix2(m Matrix2) Matrix3 {
nm := Matrix3{}
nm.SetFromMatrix2(m)
return nm
}
func Matrix3FromMatrix4(m *Matrix4) Matrix3 {
nm := Matrix3{}
nm.SetFromMatrix4(m)
return nm
}
// Matrix3Translate2D returns a Matrix3 2D matrix with given translations
func Matrix3Translate2D(x, y float32) Matrix3 {
return Matrix3FromMatrix2(Translate2D(x, y))
}
// Matrix3Scale2D returns a Matrix3 2D matrix with given scaling factors
func Matrix3Scale2D(x, y float32) Matrix3 {
return Matrix3FromMatrix2(Scale2D(x, y))
}
// Rotate2D returns a Matrix2 2D matrix with given rotation, specified in radians
func Matrix3Rotate2D(angle float32) Matrix3 {
return Matrix3FromMatrix2(Rotate2D(angle))
}
// Set sets all the elements of the matrix row by row starting at row1, column1,
// row1, column2, row1, column3 and so forth.
func (m *Matrix3) Set(n11, n12, n13, n21, n22, n23, n31, n32, n33 float32) {
m[0] = n11
m[3] = n12
m[6] = n13
m[1] = n21
m[4] = n22
m[7] = n23
m[2] = n31
m[5] = n32
m[8] = n33
}
// SetFromMatrix4 sets the matrix elements based on a Matrix4.
func (m *Matrix3) SetFromMatrix4(src *Matrix4) {
m.Set(
src[0], src[4], src[8],
src[1], src[5], src[9],
src[2], src[6], src[10],
)
}
// note: following use of [2], [5] for translation works
// exactly as the 2x3 Matrix2 case works. But vulkan and wikipedia
// use [6][7] for translation. Not sure exactly what is going on.
// SetFromMatrix2 sets the matrix elements based on a Matrix2.
func (m *Matrix3) SetFromMatrix2(src Matrix2) {
m.Set(
src.XX, src.YX, src.X0,
src.XY, src.YY, src.Y0,
src.X0, src.Y0, 1,
)
}
// FromArray sets this matrix array starting at offset.
func (m *Matrix3) FromArray(array []float32, offset int) {
copy(m[:], array[offset:])
}
// ToArray copies this matrix to array starting at offset.
func (m Matrix3) ToArray(array []float32, offset int) {
copy(array[offset:], m[:])
}
// SetIdentity sets this matrix as the identity matrix.
func (m *Matrix3) SetIdentity() {
m.Set(
1, 0, 0,
0, 1, 0,
0, 0, 1,
)
}
// SetZero sets this matrix as the zero matrix.
func (m *Matrix3) SetZero() {
m.Set(
0, 0, 0,
0, 0, 0,
0, 0, 0,
)
}
// CopyFrom copies from source matrix into this matrix
// (a regular = assign does not copy data, just the pointer!)
func (m *Matrix3) CopyFrom(src Matrix3) {
copy(m[:], src[:])
}
// MulMatrices sets ths matrix as matrix multiplication a by b (i.e., a*b).
func (m *Matrix3) MulMatrices(a, b Matrix3) {
a11 := a[0]
a12 := a[3]
a13 := a[6]
a21 := a[1]
a22 := a[4]
a23 := a[7]
a31 := a[2]
a32 := a[5]
a33 := a[8]
b11 := b[0]
b12 := b[3]
b13 := b[6]
b21 := b[1]
b22 := b[4]
b23 := b[7]
b31 := b[2]
b32 := b[5]
b33 := b[8]
m[0] = b11*a11 + b12*a21 + b13*a31
m[3] = b11*a12 + b12*a22 + b13*a32
m[6] = b11*a13 + b12*a23 + b13*a33
m[1] = b21*a11 + b22*a21 + b23*a31
m[4] = b21*a12 + b22*a22 + b23*a32
m[7] = b21*a13 + b22*a23 + b23*a33
m[2] = b31*a11 + b32*a21 + b33*a31
m[5] = b31*a12 + b32*a22 + b33*a32
m[8] = b31*a13 + b32*a23 + b33*a33
}
// Mul returns this matrix times other matrix (this matrix is unchanged)
func (m Matrix3) Mul(other Matrix3) Matrix3 {
nm := Matrix3{}
nm.MulMatrices(m, other)
return nm
}
// SetMul sets this matrix to this matrix * other
func (m *Matrix3) SetMul(other Matrix3) {
m.MulMatrices(*m, other)
}
// MulScalar returns each of this matrix's components multiplied by the specified
// scalar, leaving the original matrix unchanged.
func (m Matrix3) MulScalar(s float32) Matrix3 {
m.SetMulScalar(s)
return m
}
// SetMulScalar multiplies each of this matrix's components by the specified scalar.
func (m *Matrix3) SetMulScalar(s float32) {
m[0] *= s
m[3] *= s
m[6] *= s
m[1] *= s
m[4] *= s
m[7] *= s
m[2] *= s
m[5] *= s
m[8] *= s
}
// MulVector2AsVector multiplies the Vector2 as a vector without adding translations.
// This is for directional vectors and not points.
func (a Matrix3) MulVector2AsVector(v Vector2) Vector2 {
tx := a[0]*v.X + a[1]*v.Y
ty := a[3]*v.X + a[4]*v.Y
return Vec2(tx, ty)
}
// MulVector2AsPoint multiplies the Vector2 as a point, including adding translations.
func (a Matrix3) MulVector2AsPoint(v Vector2) Vector2 {
tx := a[0]*v.X + a[1]*v.Y + a[2]
ty := a[3]*v.X + a[4]*v.Y + a[5]
return Vec2(tx, ty)
}
// MulVector3Array multiplies count vectors (i.e., 3 sequential array values per each increment in count)
// in the array starting at start index by this matrix.
func (m *Matrix3) MulVector3Array(array []float32, start, count int) {
var v1 Vector3
j := start
for i := 0; i < count; i++ {
v1.FromSlice(array, j)
mv := v1.MulMatrix3(m)
mv.ToSlice(array, j)
j += 3
}
}
// Determinant calculates and returns the determinant of this matrix.
func (m *Matrix3) Determinant() float32 {
return m[0]*m[4]*m[8] -
m[0]*m[5]*m[7] -
m[1]*m[3]*m[8] +
m[1]*m[5]*m[6] +
m[2]*m[3]*m[7] -
m[2]*m[4]*m[6]
}
// SetInverse sets this matrix to the inverse of the src matrix.
// If the src matrix cannot be inverted returns error and
// sets this matrix to the identity matrix.
func (m *Matrix3) SetInverse(src Matrix3) error {
n11 := src[0]
n21 := src[1]
n31 := src[2]
n12 := src[3]
n22 := src[4]
n32 := src[5]
n13 := src[6]
n23 := src[7]
n33 := src[8]
t11 := n33*n22 - n32*n23
t12 := n32*n13 - n33*n12
t13 := n23*n12 - n22*n13
det := n11*t11 + n21*t12 + n31*t13
// no inverse
if det == 0 {
m.SetIdentity()
return errors.New("cannot invert matrix, determinant is 0")
}
detInv := 1 / det
m[0] = t11 * detInv
m[1] = (n31*n23 - n33*n21) * detInv
m[2] = (n32*n21 - n31*n22) * detInv
m[3] = t12 * detInv
m[4] = (n33*n11 - n31*n13) * detInv
m[5] = (n31*n12 - n32*n11) * detInv
m[6] = t13 * detInv
m[7] = (n21*n13 - n23*n11) * detInv
m[8] = (n22*n11 - n21*n12) * detInv
return nil
}
// Inverse returns the inverse of this matrix.
// If the matrix cannot be inverted it silently
// sets this matrix to the identity matrix.
// See Try version for error.
func (m Matrix3) Inverse() Matrix3 {
nm := Matrix3{}
nm.SetInverse(m)
return nm
}
// InverseTry returns the inverse of this matrix.
// If the matrix cannot be inverted returns error and
// sets this matrix to the identity matrix.
func (m Matrix3) InverseTry() (Matrix3, error) {
nm := Matrix3{}
err := nm.SetInverse(m)
return nm, err
}
// SetTranspose transposes this matrix.
func (m *Matrix3) SetTranspose() {
m[1], m[3] = m[3], m[1]
m[2], m[6] = m[6], m[2]
m[5], m[7] = m[7], m[5]
}
// Transpose returns the transpose of this matrix.
func (m Matrix3) Transpose() Matrix3 {
nm := m
nm.SetTranspose()
return nm
}
// ScaleCols returns matrix with columns multiplied by the vector components.
// This can be used when multiplying this matrix by a diagonal matrix if we store
// the diagonal components as a vector.
func (m *Matrix3) ScaleCols(v Vector3) *Matrix3 {
nm := &Matrix3{}
*nm = *m
nm.SetScaleCols(v)
return nm
}
// SetScaleCols multiplies the matrix columns by the vector components.
// This can be used when multiplying this matrix by a diagonal matrix if we store
// the diagonal components as a vector.
func (m *Matrix3) SetScaleCols(v Vector3) {
m[0] *= v.X
m[1] *= v.X
m[2] *= v.X
m[3] *= v.Y
m[4] *= v.Y
m[5] *= v.Y
m[6] *= v.Z
m[7] *= v.Z
m[8] *= v.Z
}
/////////////////////////////////////////////////////////////////////////////
// Special functions
// SetNormalMatrix set this matrix to the matrix that can transform the normal vectors
// from the src matrix which is used transform the vertices (e.g., a ModelView matrix).
// If the src matrix cannot be inverted returns error.
func (m *Matrix3) SetNormalMatrix(src *Matrix4) error {
var err error
*m, err = Matrix3FromMatrix4(src).InverseTry()
m.SetTranspose()
return err
}
// SetRotationFromQuat sets this matrix as a rotation matrix from the specified [Quat].
func (m *Matrix3) SetRotationFromQuat(q Quat) {
x := q.X
y := q.Y
z := q.Z
w := q.W
x2 := x + x
y2 := y + y
z2 := z + z
xx := x * x2
xy := x * y2
xz := x * z2
yy := y * y2
yz := y * z2
zz := z * z2
wx := w * x2
wy := w * y2
wz := w * z2
m[0] = 1 - (yy + zz)
m[3] = xy - wz
m[6] = xz + wy
m[1] = xy + wz
m[4] = 1 - (xx + zz)
m[7] = yz - wx
m[2] = xz - wy
m[5] = yz + wx
m[8] = 1 - (xx + yy)
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
import "errors"
// Matrix4 is 4x4 matrix organized internally as column matrix.
type Matrix4 [16]float32
// Identity4 returns a new identity [Matrix4] matrix.
func Identity4() *Matrix4 {
m := &Matrix4{}
m.SetIdentity()
return m
}
// Set sets all the elements of this matrix row by row starting at row1, column1,
// row1, column2, row1, column3 and so forth.
func (m *Matrix4) Set(n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 float32) {
m[0] = n11
m[4] = n12
m[8] = n13
m[12] = n14
m[1] = n21
m[5] = n22
m[9] = n23
m[13] = n24
m[2] = n31
m[6] = n32
m[10] = n33
m[14] = n34
m[3] = n41
m[7] = n42
m[11] = n43
m[15] = n44
}
// SetFromMatrix3 sets the matrix elements based on a Matrix3,
// filling in 0's for missing off-diagonal elements,
// and 1 on the diagonal.
func (m *Matrix4) SetFromMatrix3(src *Matrix3) {
m.Set(
src[0], src[3], src[6], 0,
src[1], src[4], src[7], 0,
src[2], src[5], src[8], 0,
0, 0, 0, 1,
)
}
// FromArray set this matrix elements from the array starting at offset.
func (m *Matrix4) FromArray(array []float32, offset int) {
copy(m[:], array[offset:])
}
// ToArray copies this matrix elements to array starting at offset.
func (m *Matrix4) ToArray(array []float32, offset int) {
copy(array[offset:], m[:])
}
// SetIdentity sets this matrix as the identity matrix.
func (m *Matrix4) SetIdentity() {
m.Set(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
)
}
// SetZero sets this matrix as the zero matrix.
func (m *Matrix4) SetZero() {
m.Set(
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
)
}
// CopyFrom copies from source matrix into this matrix
// (a regular = assign does not copy data, just the pointer!)
func (m *Matrix4) CopyFrom(src *Matrix4) {
copy(m[:], src[:])
}
// CopyPos copies the position elements of the src matrix into this one.
func (m *Matrix4) CopyPos(src *Matrix4) {
m[12] = src[12]
m[13] = src[13]
m[14] = src[14]
}
// ExtractBasis returns the x,y,z basis vectors of this matrix.
func (m *Matrix4) ExtractBasis() (xAxis, yAxis, zAxis Vector3) {
xAxis.Set(m[0], m[1], m[2])
yAxis.Set(m[4], m[5], m[6])
zAxis.Set(m[8], m[9], m[10])
return
}
// SetBasis sets this matrix basis vectors from the specified vectors.
func (m *Matrix4) SetBasis(xAxis, yAxis, zAxis Vector3) {
m.Set(
xAxis.X, yAxis.X, zAxis.X, 0,
xAxis.Y, yAxis.Y, zAxis.Y, 0,
xAxis.Z, yAxis.Z, zAxis.Z, 0,
0, 0, 0, 1,
)
}
// MulMatrices sets this matrix as matrix multiplication a by b (i.e. a*b).
func (m *Matrix4) MulMatrices(a, b *Matrix4) {
a11 := a[0]
a12 := a[4]
a13 := a[8]
a14 := a[12]
a21 := a[1]
a22 := a[5]
a23 := a[9]
a24 := a[13]
a31 := a[2]
a32 := a[6]
a33 := a[10]
a34 := a[14]
a41 := a[3]
a42 := a[7]
a43 := a[11]
a44 := a[15]
b11 := b[0]
b12 := b[4]
b13 := b[8]
b14 := b[12]
b21 := b[1]
b22 := b[5]
b23 := b[9]
b24 := b[13]
b31 := b[2]
b32 := b[6]
b33 := b[10]
b34 := b[14]
b41 := b[3]
b42 := b[7]
b43 := b[11]
b44 := b[15]
m[0] = a11*b11 + a12*b21 + a13*b31 + a14*b41
m[4] = a11*b12 + a12*b22 + a13*b32 + a14*b42
m[8] = a11*b13 + a12*b23 + a13*b33 + a14*b43
m[12] = a11*b14 + a12*b24 + a13*b34 + a14*b44
m[1] = a21*b11 + a22*b21 + a23*b31 + a24*b41
m[5] = a21*b12 + a22*b22 + a23*b32 + a24*b42
m[9] = a21*b13 + a22*b23 + a23*b33 + a24*b43
m[13] = a21*b14 + a22*b24 + a23*b34 + a24*b44
m[2] = a31*b11 + a32*b21 + a33*b31 + a34*b41
m[6] = a31*b12 + a32*b22 + a33*b32 + a34*b42
m[10] = a31*b13 + a32*b23 + a33*b33 + a34*b43
m[14] = a31*b14 + a32*b24 + a33*b34 + a34*b44
m[3] = a41*b11 + a42*b21 + a43*b31 + a44*b41
m[7] = a41*b12 + a42*b22 + a43*b32 + a44*b42
m[11] = a41*b13 + a42*b23 + a43*b33 + a44*b43
m[15] = a41*b14 + a42*b24 + a43*b34 + a44*b44
}
// Mul returns this matrix times other matrix (this matrix is unchanged)
func (m *Matrix4) Mul(other *Matrix4) *Matrix4 {
nm := &Matrix4{}
nm.MulMatrices(m, other)
return nm
}
// SetMul sets this matrix to this matrix times other
func (m *Matrix4) SetMul(other *Matrix4) {
m.MulMatrices(m, other)
}
// SetMulScalar multiplies each element of this matrix by the specified scalar.
func (m *Matrix4) MulScalar(s float32) {
m[0] *= s
m[4] *= s
m[8] *= s
m[12] *= s
m[1] *= s
m[5] *= s
m[9] *= s
m[13] *= s
m[2] *= s
m[6] *= s
m[10] *= s
m[14] *= s
m[3] *= s
m[7] *= s
m[11] *= s
m[15] *= s
}
// MulVector3Array multiplies count vectors (i.e., 3 sequential array values per each increment in count)
// in the array starting at start index by this matrix.
func (m *Matrix4) MulVector3Array(array []float32, start, count int) {
var v1 Vector3
j := start
for i := 0; i < count; i++ {
v1.FromSlice(array, j)
mv := v1.MulMatrix4(m)
mv.ToSlice(array, j)
j += 3
}
}
// Determinant calculates and returns the determinant of this matrix.
func (m *Matrix4) Determinant() float32 {
n11 := m[0]
n12 := m[4]
n13 := m[8]
n14 := m[12]
n21 := m[1]
n22 := m[5]
n23 := m[9]
n24 := m[13]
n31 := m[2]
n32 := m[6]
n33 := m[10]
n34 := m[14]
n41 := m[3]
n42 := m[7]
n43 := m[11]
n44 := m[15]
return n41*(+n14*n23*n32-n13*n24*n32-n14*n22*n33+n12*n24*n33+n13*n22*n34-n12*n23*n34) +
n42*(+n11*n23*n34-n11*n24*n33+n14*n21*n33-n13*n21*n34+n13*n24*n31-n14*n23*n31) +
n43*(+n11*n24*n32-n11*n22*n34-n14*n21*n32+n12*n21*n34+n14*n22*n31-n12*n24*n31) +
n44*(-n13*n22*n31-n11*n23*n32+n11*n22*n33+n13*n21*n32-n12*n21*n33+n12*n23*n31)
}
// SetInverse sets this matrix to the inverse of the src matrix.
// If the src matrix cannot be inverted returns error and
// sets this matrix to the identity matrix.
func (m *Matrix4) SetInverse(src *Matrix4) error {
n11 := src[0]
n12 := src[4]
n13 := src[8]
n14 := src[12]
n21 := src[1]
n22 := src[5]
n23 := src[9]
n24 := src[13]
n31 := src[2]
n32 := src[6]
n33 := src[10]
n34 := src[14]
n41 := src[3]
n42 := src[7]
n43 := src[11]
n44 := src[15]
t11 := n23*n34*n42 - n24*n33*n42 + n24*n32*n43 - n22*n34*n43 - n23*n32*n44 + n22*n33*n44
t12 := n14*n33*n42 - n13*n34*n42 - n14*n32*n43 + n12*n34*n43 + n13*n32*n44 - n12*n33*n44
t13 := n13*n24*n42 - n14*n23*n42 + n14*n22*n43 - n12*n24*n43 - n13*n22*n44 + n12*n23*n44
t14 := n14*n23*n32 - n13*n24*n32 - n14*n22*n33 + n12*n24*n33 + n13*n22*n34 - n12*n23*n34
det := n11*t11 + n21*t12 + n31*t13 + n41*t14
if det == 0 {
m.SetIdentity()
return errors.New("cannot invert matrix, determinant is 0")
}
detInv := 1 / det
m[0] = t11 * detInv
m[1] = (n24*n33*n41 - n23*n34*n41 - n24*n31*n43 + n21*n34*n43 + n23*n31*n44 - n21*n33*n44) * detInv
m[2] = (n22*n34*n41 - n24*n32*n41 + n24*n31*n42 - n21*n34*n42 - n22*n31*n44 + n21*n32*n44) * detInv
m[3] = (n23*n32*n41 - n22*n33*n41 - n23*n31*n42 + n21*n33*n42 + n22*n31*n43 - n21*n32*n43) * detInv
m[4] = t12 * detInv
m[5] = (n13*n34*n41 - n14*n33*n41 + n14*n31*n43 - n11*n34*n43 - n13*n31*n44 + n11*n33*n44) * detInv
m[6] = (n14*n32*n41 - n12*n34*n41 - n14*n31*n42 + n11*n34*n42 + n12*n31*n44 - n11*n32*n44) * detInv
m[7] = (n12*n33*n41 - n13*n32*n41 + n13*n31*n42 - n11*n33*n42 - n12*n31*n43 + n11*n32*n43) * detInv
m[8] = t13 * detInv
m[9] = (n14*n23*n41 - n13*n24*n41 - n14*n21*n43 + n11*n24*n43 + n13*n21*n44 - n11*n23*n44) * detInv
m[10] = (n12*n24*n41 - n14*n22*n41 + n14*n21*n42 - n11*n24*n42 - n12*n21*n44 + n11*n22*n44) * detInv
m[11] = (n13*n22*n41 - n12*n23*n41 - n13*n21*n42 + n11*n23*n42 + n12*n21*n43 - n11*n22*n43) * detInv
m[12] = t14 * detInv
m[13] = (n13*n24*n31 - n14*n23*n31 + n14*n21*n33 - n11*n24*n33 - n13*n21*n34 + n11*n23*n34) * detInv
m[14] = (n14*n22*n31 - n12*n24*n31 - n14*n21*n32 + n11*n24*n32 + n12*n21*n34 - n11*n22*n34) * detInv
m[15] = (n12*n23*n31 - n13*n22*n31 + n13*n21*n32 - n11*n23*n32 - n12*n21*n33 + n11*n22*n33) * detInv
return nil
}
// Inverse returns the inverse of this matrix.
// If the matrix cannot be inverted returns error and
// sets this matrix to the identity matrix.
func (m *Matrix4) Inverse() (*Matrix4, error) {
nm := &Matrix4{}
err := nm.SetInverse(m)
return nm, err
}
// SetTranspose transposes this matrix.
func (m *Matrix4) SetTranspose() {
m[1], m[4] = m[4], m[1]
m[2], m[8] = m[8], m[2]
m[6], m[9] = m[9], m[6]
m[3], m[12] = m[12], m[3]
m[7], m[13] = m[13], m[7]
m[11], m[14] = m[14], m[11]
}
// Transpose returns the transpose of this matrix.
func (m *Matrix4) Transpose() *Matrix4 {
nm := *m
nm.SetTranspose()
return &nm
}
/////////////////////////////////////////////////////////////////////////////
// Translation, Rotation, Scaling transform
// ScaleCols returns matrix with first column of this matrix multiplied by the vector X component,
// the second column by the vector Y component and the third column by
// the vector Z component. The matrix fourth column is unchanged.
func (m *Matrix4) ScaleCols(v Vector3) *Matrix4 {
nm := &Matrix4{}
nm.SetScaleCols(v)
return nm
}
// SetScaleCols multiplies the first column of this matrix by the vector X component,
// the second column by the vector Y component and the third column by
// the vector Z component. The matrix fourth column is unchanged.
func (m *Matrix4) SetScaleCols(v Vector3) {
m[0] *= v.X
m[4] *= v.Y
m[8] *= v.Z
m[1] *= v.X
m[5] *= v.Y
m[9] *= v.Z
m[2] *= v.X
m[6] *= v.Y
m[10] *= v.Z
m[3] *= v.X
m[7] *= v.Y
m[11] *= v.Z
}
// GetMaxScaleOnAxis returns the maximum scale value of the 3 axes.
func (m *Matrix4) GetMaxScaleOnAxis() float32 {
scaleXSq := m[0]*m[0] + m[1]*m[1] + m[2]*m[2]
scaleYSq := m[4]*m[4] + m[5]*m[5] + m[6]*m[6]
scaleZSq := m[8]*m[8] + m[9]*m[9] + m[10]*m[10]
return Sqrt(Max(scaleXSq, Max(scaleYSq, scaleZSq)))
}
// SetTranslation sets this matrix to a translation matrix from the specified x, y and z values.
func (m *Matrix4) SetTranslation(x, y, z float32) {
m.Set(
1, 0, 0, x,
0, 1, 0, y,
0, 0, 1, z,
0, 0, 0, 1,
)
}
// SetRotationX sets this matrix to a rotation matrix of angle theta around the X axis.
func (m *Matrix4) SetRotationX(theta float32) {
c := Cos(theta)
s := Sin(theta)
m.Set(
1, 0, 0, 0,
0, c, -s, 0,
0, s, c, 0,
0, 0, 0, 1,
)
}
// SetRotationY sets this matrix to a rotation matrix of angle theta around the Y axis.
func (m *Matrix4) SetRotationY(theta float32) {
c := Cos(theta)
s := Sin(theta)
m.Set(
c, 0, s, 0,
0, 1, 0, 0,
-s, 0, c, 0,
0, 0, 0, 1,
)
}
// SetRotationZ sets this matrix to a rotation matrix of angle theta around the Z axis.
func (m *Matrix4) SetRotationZ(theta float32) {
c := Cos(theta)
s := Sin(theta)
m.Set(
c, -s, 0, 0,
s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
)
}
// SetRotationAxis sets this matrix to a rotation matrix of the specified angle around the specified axis.
func (m *Matrix4) SetRotationAxis(axis *Vector3, angle float32) {
c := Cos(angle)
s := Sin(angle)
t := 1 - c
x := axis.X
y := axis.Y
z := axis.Z
tx := t * x
ty := t * y
m.Set(
tx*x+c, tx*y-s*z, tx*z+s*y, 0,
tx*y+s*z, ty*y+c, ty*z-s*x, 0,
tx*z-s*y, ty*z+s*x, t*z*z+c, 0,
0, 0, 0, 1,
)
}
// SetScale sets this matrix to a scale transformation matrix using the specified x, y and z values.
func (m *Matrix4) SetScale(x, y, z float32) {
m.Set(
x, 0, 0, 0,
0, y, 0, 0,
0, 0, z, 0,
0, 0, 0, 1,
)
}
// SetPos sets this transformation matrix position fields from the specified vector v.
func (m *Matrix4) SetPos(v Vector3) {
m[12] = v.X
m[13] = v.Y
m[14] = v.Z
}
// Pos returns the position component of the matrix
func (m *Matrix4) Pos() Vector3 {
pos := Vector3{}
pos.X = m[12]
pos.Y = m[13]
pos.Z = m[14]
return pos
}
// SetTransform sets this matrix to a transformation matrix for the specified position,
// rotation specified by the quaternion and scale.
func (m *Matrix4) SetTransform(pos Vector3, quat Quat, scale Vector3) {
m.SetRotationFromQuat(quat)
m.SetScaleCols(scale)
m.SetPos(pos)
}
// Decompose updates the position vector, quaternion and scale from this transformation matrix.
func (m *Matrix4) Decompose() (pos Vector3, quat Quat, scale Vector3) {
sx := Vec3(m[0], m[1], m[2]).Length()
sy := Vec3(m[4], m[5], m[6]).Length()
sz := Vec3(m[8], m[9], m[10]).Length()
// If determinant is negative, we need to invert one scale
det := m.Determinant()
if det < 0 {
sx = -sx
}
pos.X = m[12]
pos.Y = m[13]
pos.Z = m[14]
// Scale the rotation part
invSX := 1 / sx
invSY := 1 / sy
invSZ := 1 / sz
mat := *m
mat[0] *= invSX
mat[1] *= invSX
mat[2] *= invSX
mat[4] *= invSY
mat[5] *= invSY
mat[6] *= invSY
mat[8] *= invSZ
mat[9] *= invSZ
mat[10] *= invSZ
quat.SetFromRotationMatrix(&mat)
scale.X = sx
scale.Y = sy
scale.Z = sz
return
}
// ExtractRotation sets this matrix as rotation matrix from the src transformation matrix.
func (m *Matrix4) ExtractRotation(src *Matrix4) {
scaleX := 1 / Vec3(src[0], src[1], src[2]).Length()
scaleY := 1 / Vec3(src[4], src[5], src[6]).Length()
scaleZ := 1 / Vec3(src[8], src[9], src[10]).Length()
m[0] = src[0] * scaleX
m[1] = src[1] * scaleX
m[2] = src[2] * scaleX
m[4] = src[4] * scaleY
m[5] = src[5] * scaleY
m[6] = src[6] * scaleY
m[8] = src[8] * scaleZ
m[9] = src[9] * scaleZ
m[10] = src[10] * scaleZ
}
// SetRotationFromEuler set this a matrix as a rotation matrix from the specified euler angles.
func (m *Matrix4) SetRotationFromEuler(euler Vector3) {
x := euler.X
y := euler.Y
z := euler.Z
a := Cos(x)
b := Sin(x)
c := Cos(y)
d := Sin(y)
e := Cos(z)
f := Sin(z)
ae := a * e
af := a * f
be := b * e
bf := b * f
m[0] = c * e
m[4] = -c * f
m[8] = d
m[1] = af + be*d
m[5] = ae - bf*d
m[9] = -b * c
m[2] = bf - ae*d
m[6] = be + af*d
m[10] = a * c
// Last column
m[3] = 0
m[7] = 0
m[11] = 0
// Bottom row
m[12] = 0
m[13] = 0
m[14] = 0
m[15] = 1
}
// SetRotationFromQuat sets this matrix as a rotation matrix from the specified quaternion.
func (m *Matrix4) SetRotationFromQuat(q Quat) {
x := q.X
y := q.Y
z := q.Z
w := q.W
x2 := x + x
y2 := y + y
z2 := z + z
xx := x * x2
xy := x * y2
xz := x * z2
yy := y * y2
yz := y * z2
zz := z * z2
wx := w * x2
wy := w * y2
wz := w * z2
m[0] = 1 - (yy + zz)
m[4] = xy - wz
m[8] = xz + wy
m[1] = xy + wz
m[5] = 1 - (xx + zz)
m[9] = yz - wx
m[2] = xz - wy
m[6] = yz + wx
m[10] = 1 - (xx + yy)
// last column
m[3] = 0
m[7] = 0
m[11] = 0
// bottom row
m[12] = 0
m[13] = 0
m[14] = 0
m[15] = 1
}
// LookAt sets this matrix as view transform matrix with origin at eye,
// looking at target and using the up vector.
func (m *Matrix4) LookAt(eye, target, up Vector3) {
z := eye.Sub(target)
if z.LengthSquared() == 0 {
// Eye and target are in the same position
z.Z = 1
}
z.SetNormal()
x := up.Cross(z)
if x.LengthSquared() == 0 { // Up and Z are parallel
if Abs(up.Z) == 1 {
z.X += 0.0001
} else {
z.Z += 0.0001
}
z.SetNormal()
x = up.Cross(z)
}
x.SetNormal()
y := z.Cross(x)
m[0] = x.X
m[1] = x.Y
m[2] = x.Z
m[4] = y.X
m[5] = y.Y
m[6] = y.Z
m[8] = z.X
m[9] = z.Y
m[10] = z.Z
}
// NewLookAt returns Matrix4 matrix as view transform matrix with origin at eye,
// looking at target and using the up vector.
func NewLookAt(eye, target, up Vector3) *Matrix4 {
rotMat := &Matrix4{}
rotMat.LookAt(eye, target, up)
return rotMat
}
// SetFrustum sets this matrix to a projection frustum matrix bounded
// by the specified planes.
func (m *Matrix4) SetFrustum(left, right, bottom, top, near, far float32) {
fmn := far - near
m[0] = 2 * near / (right - left)
m[1] = 0
m[2] = 0
m[3] = 0
m[4] = 0
m[5] = 2 * near / (top - bottom)
m[6] = 0
m[7] = 0
m[8] = (right + left) / (right - left)
m[9] = (top + bottom) / (top - bottom)
m[10] = -(far + near) / fmn
m[11] = -1
m[12] = 0
m[13] = 0
m[14] = -(2 * far * near) / fmn
m[15] = 0
}
// SetPerspective sets this matrix to a perspective projection matrix
// with the specified field of view in degrees,
// aspect ratio (width/height) and near and far planes.
func (m *Matrix4) SetPerspective(fov, aspect, near, far float32) {
ymax := near * Tan(DegToRad(fov*0.5))
ymin := -ymax
xmin := ymin * aspect
xmax := ymax * aspect
m.SetFrustum(xmin, xmax, ymin, ymax, near, far)
}
// SetOrthographic sets this matrix to an orthographic projection matrix.
func (m *Matrix4) SetOrthographic(width, height, near, far float32) {
p := far - near
z := (far + near) / p
m[0] = 2 / width
m[4] = 0
m[8] = 0
m[12] = 0
m[1] = 0
m[5] = 2 / height
m[9] = 0
m[13] = 0
m[2] = 0
m[6] = 0
m[10] = -2 / p
m[14] = -z
m[3] = 0
m[7] = 0
m[11] = 0
m[15] = 1
}
// SetVkFrustum sets this matrix to a projection frustum matrix bounded by the specified planes.
// This version is for use with Vulkan, and does the equivalent of GLM_DEPTH_ZERO_ONE in glm
// and also multiplies the Y axis by -1, preserving the original OpenGL Y-up system.
// OpenGL provides a "natural" coordinate system for the physical world
// so it is useful to retain that for the world system and just convert
// on the way out to the render using this projection matrix.
func (m *Matrix4) SetVkFrustum(left, right, bottom, top, near, far float32) {
fmn := far - near
m[0] = 2 * near / (right - left)
m[1] = 0
m[2] = 0
m[3] = 0
m[4] = 0
m[5] = -2 * near / (top - bottom)
m[6] = 0
m[7] = 0
m[8] = (right + left) / (right - left)
m[9] = (top + bottom) / (top - bottom)
m[10] = -far / fmn
m[11] = -1
m[12] = 0
m[13] = 0
m[14] = -(far * near) / fmn
m[15] = 0
}
// SetVkPerspective sets this matrix to a vulkan appropriate perspective
// projection matrix, assuming the use of the OpenGL Y-up
// coordinate system for the geometry points.
// OpenGL provides a "natural" coordinate system for the physical world
// so it is useful to retain that for the world system and just convert
// on the way out to the render using this projection matrix.
// The specified field of view is in degrees,
// aspect ratio (width/height) and near and far planes.
func (m *Matrix4) SetVkPerspective(fov, aspect, near, far float32) {
ymax := near * Tan(DegToRad(fov*0.5))
ymin := -ymax
xmin := ymin * aspect
xmax := ymax * aspect
m.SetVkFrustum(xmin, xmax, ymin, ymax, near, far)
}
// Copyright (c) 2024, 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 minmax
import "fmt"
//gosl:start minmax
const (
MaxFloat32 float32 = 3.402823466e+38
MinFloat32 float32 = 1.175494351e-38
)
// AvgMax holds average and max statistics
type AvgMax32 struct {
Avg float32
Max float32
// sum for computing average
Sum float32
// index of max item
MaxIndex int32
// number of items in sum
N int32
pad, pad1, pad2 int32
}
// Init initializes prior to new updates
func (am *AvgMax32) Init() {
am.Avg = 0
am.Sum = 0
am.N = 0
am.Max = -MaxFloat32
am.MaxIndex = -1
}
// UpdateVal updates stats from given value
func (am *AvgMax32) UpdateValue(val float32, idx int32) {
am.Sum += val
am.N++
if val > am.Max {
am.Max = val
am.MaxIndex = idx
}
}
// UpdateFromOther updates these values from other AvgMax32 values
func (am *AvgMax32) UpdateFromOther(oSum, oMax float32, oN, oMaxIndex int32) {
am.Sum += oSum
am.N += oN
if oMax > am.Max {
am.Max = oMax
am.MaxIndex = oMaxIndex
}
}
// CalcAvg computes the average given the current Sum and N values
func (am *AvgMax32) CalcAvg() {
if am.N > 0 {
am.Avg = am.Sum / float32(am.N)
} else {
am.Avg = am.Sum
am.Max = am.Avg // prevents Max from being -MaxFloat..
}
}
//gosl:end minmax
func (am *AvgMax32) String() string {
return fmt.Sprintf("{Avg: %g, Max: %g, Sum: %g, MaxIndex: %d, N: %d}", am.Avg, am.Max, am.Sum, am.MaxIndex, am.N)
}
// UpdateFrom updates these values from other AvgMax32 values
func (am *AvgMax32) UpdateFrom(oth *AvgMax32) {
am.UpdateFromOther(oth.Sum, oth.Max, oth.N, oth.MaxIndex)
am.Sum += oth.Sum
am.N += oth.N
if oth.Max > am.Max {
am.Max = oth.Max
am.MaxIndex = oth.MaxIndex
}
}
// CopyFrom copies from other AvgMax32
func (am *AvgMax32) CopyFrom(oth *AvgMax32) {
*am = *oth
}
///////////////////////////////////////////////////////////////////////////
// 64
// AvgMax holds average and max statistics
type AvgMax64 struct {
Avg float64
Max float64
// sum for computing average
Sum float64
// index of max item
MaxIndex int32
// number of items in sum
N int32
}
// Init initializes prior to new updates
func (am *AvgMax64) Init() {
am.Avg = 0
am.Sum = 0
am.N = 0
am.Max = -MaxFloat64
am.MaxIndex = -1
}
// UpdateVal updates stats from given value
func (am *AvgMax64) UpdateValue(val float64, idx int) {
am.Sum += val
am.N++
if val > am.Max {
am.Max = val
am.MaxIndex = int32(idx)
}
}
// CalcAvg computes the average given the current Sum and N values
func (am *AvgMax64) CalcAvg() {
if am.N > 0 {
am.Avg = am.Sum / float64(am.N)
} else {
am.Avg = am.Sum
am.Max = am.Avg // prevents Max from being -MaxFloat..
}
}
// UpdateFrom updates these values from other AvgMax64
func (am *AvgMax64) UpdateFrom(oth *AvgMax64) {
am.Sum += oth.Sum
am.N += oth.N
if oth.Max > am.Max {
am.Max = oth.Max
am.MaxIndex = oth.MaxIndex
}
}
// CopyFrom copies from other AvgMax64
func (am *AvgMax64) CopyFrom(oth *AvgMax64) {
am.Avg = oth.Avg
am.Max = oth.Max
am.MaxIndex = oth.MaxIndex
am.Sum = oth.Sum
am.N = oth.N
}
// Copyright (c) 2024, 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 minmax
import "fmt"
//gosl:start minmax
// F32 represents a min / max range for float32 values.
// Supports clipping, renormalizing, etc
type F32 struct {
Min float32
Max float32
pad, pad1 int32 // for gpu use
}
// Set sets the min and max values
func (mr *F32) Set(mn, mx float32) {
mr.Min = mn
mr.Max = mx
}
// SetInfinity sets the Min to +MaxFloat, Max to -MaxFloat -- suitable for
// iteratively calling Fit*InRange
func (mr *F32) SetInfinity() {
mr.Min = MaxFloat32
mr.Max = -MaxFloat32
}
// IsValid returns true if Min <= Max
func (mr *F32) IsValid() bool {
return mr.Min <= mr.Max
}
// InRange tests whether value is within the range (>= Min and <= Max)
func (mr *F32) InRange(val float32) bool {
return ((val >= mr.Min) && (val <= mr.Max))
}
// IsLow tests whether value is lower than the minimum
func (mr *F32) IsLow(val float32) bool {
return (val < mr.Min)
}
// IsHigh tests whether value is higher than the maximum
func (mr *F32) IsHigh(val float32) bool {
return (val > mr.Min)
}
// Range returns Max - Min
func (mr *F32) Range() float32 {
return mr.Max - mr.Min
}
// Scale returns 1 / Range -- if Range = 0 then returns 0
func (mr *F32) Scale() float32 {
r := mr.Range()
if r != 0 {
return 1.0 / r
}
return 0
}
// Midpoint returns point halfway between Min and Max
func (mr *F32) Midpoint() float32 {
return 0.5 * (mr.Max + mr.Min)
}
// FitValInRange adjusts our Min, Max to fit given value within Min, Max range
// returns true if we had to adjust to fit.
func (mr *F32) FitValInRange(val float32) bool {
adj := false
if val < mr.Min {
mr.Min = val
adj = true
}
if val > mr.Max {
mr.Max = val
adj = true
}
return adj
}
// NormVal normalizes value to 0-1 unit range relative to current Min / Max range
// Clips the value within Min-Max range first.
func (mr *F32) NormValue(val float32) float32 {
return (mr.ClipValue(val) - mr.Min) * mr.Scale()
}
// ProjVal projects a 0-1 normalized unit value into current Min / Max range (inverse of NormVal)
func (mr *F32) ProjValue(val float32) float32 {
return mr.Min + (val * mr.Range())
}
// ClipVal clips given value within Min / Max range
// Note: a NaN will remain as a NaN
func (mr *F32) ClipValue(val float32) float32 {
if val < mr.Min {
return mr.Min
}
if val > mr.Max {
return mr.Max
}
return val
}
// ClipNormVal clips then normalizes given value within 0-1
// Note: a NaN will remain as a NaN
func (mr *F32) ClipNormValue(val float32) float32 {
if val < mr.Min {
return 0
}
if val > mr.Max {
return 1
}
return mr.NormValue(val)
}
//gosl:end minmax
func (mr *F32) String() string {
return fmt.Sprintf("{%g %g}", mr.Min, mr.Max)
}
// FitInRange adjusts our Min, Max to fit within those of other F32
// returns true if we had to adjust to fit.
func (mr *F32) FitInRange(oth F32) bool {
adj := false
if oth.Min < mr.Min {
mr.Min = oth.Min
adj = true
}
if oth.Max > mr.Max {
mr.Max = oth.Max
adj = true
}
return adj
}
// Copyright (c) 2024, 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 minmax provides a struct that holds Min and Max values.
package minmax
//go:generate core generate
const (
MaxFloat64 float64 = 1.7976931348623158e+308
MinFloat64 float64 = 2.2250738585072014e-308
)
// F64 represents a min / max range for float64 values.
// Supports clipping, renormalizing, etc
type F64 struct {
Min float64
Max float64
}
// Set sets the min and max values
func (mr *F64) Set(mn, mx float64) {
mr.Min = mn
mr.Max = mx
}
// SetInfinity sets the Min to +MaxFloat, Max to -MaxFloat -- suitable for
// iteratively calling Fit*InRange
func (mr *F64) SetInfinity() {
mr.Min = MaxFloat64
mr.Max = -MaxFloat64
}
// IsValid returns true if Min <= Max
func (mr *F64) IsValid() bool {
return mr.Min <= mr.Max
}
// InRange tests whether value is within the range (>= Min and <= Max)
func (mr *F64) InRange(val float64) bool {
return ((val >= mr.Min) && (val <= mr.Max))
}
// IsLow tests whether value is lower than the minimum
func (mr *F64) IsLow(val float64) bool {
return (val < mr.Min)
}
// IsHigh tests whether value is higher than the maximum
func (mr *F64) IsHigh(val float64) bool {
return (val > mr.Min)
}
// Range returns Max - Min
func (mr *F64) Range() float64 {
return mr.Max - mr.Min
}
// Scale returns 1 / Range -- if Range = 0 then returns 0
func (mr *F64) Scale() float64 {
r := mr.Range()
if r != 0 {
return 1 / r
}
return 0
}
// Midpoint returns point halfway between Min and Max
func (mr *F64) Midpoint() float64 {
return 0.5 * (mr.Max + mr.Min)
}
// FitValInRange adjusts our Min, Max to fit given value within Min, Max range
// returns true if we had to adjust to fit.
func (mr *F64) FitValInRange(val float64) bool {
adj := false
if val < mr.Min {
mr.Min = val
adj = true
}
if val > mr.Max {
mr.Max = val
adj = true
}
return adj
}
// NormVal normalizes value to 0-1 unit range relative to current Min / Max range
// Clips the value within Min-Max range first.
func (mr *F64) NormValue(val float64) float64 {
return (mr.ClipValue(val) - mr.Min) * mr.Scale()
}
// ProjVal projects a 0-1 normalized unit value into current Min / Max range (inverse of NormVal)
func (mr *F64) ProjValue(val float64) float64 {
return mr.Min + (val * mr.Range())
}
// ClipVal clips given value within Min / Max range
// Note: a NaN will remain as a NaN
func (mr *F64) ClipValue(val float64) float64 {
if val < mr.Min {
return mr.Min
}
if val > mr.Max {
return mr.Max
}
return val
}
// ClipNormVal clips then normalizes given value within 0-1
// Note: a NaN will remain as a NaN
func (mr *F64) ClipNormValue(val float64) float64 {
if val < mr.Min {
return 0
}
if val > mr.Max {
return 1
}
return mr.NormValue(val)
}
// FitInRange adjusts our Min, Max to fit within those of other F64
// returns true if we had to adjust to fit.
func (mr *F64) FitInRange(oth F64) bool {
adj := false
if oth.Min < mr.Min {
mr.Min = oth.Min
adj = true
}
if oth.Max > mr.Max {
mr.Max = oth.Max
adj = true
}
return adj
}
// Copyright (c) 2024, 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 minmax
import "math"
// Int represents a min / max range for int values.
// Supports clipping, renormalizing, etc
type Int struct {
Min int
Max int
}
// Set sets the min and max values
func (mr *Int) Set(mn, mx int) {
mr.Min, mr.Max = mn, mx
}
// SetInfinity sets the Min to +MaxFloat, Max to -MaxFloat -- suitable for
// iteratively calling Fit*InRange
func (mr *Int) SetInfinity() {
mr.Min, mr.Max = math.MaxInt, -math.MaxInt
}
// IsValid returns true if Min <= Max
func (mr *Int) IsValid() bool {
return mr.Min <= mr.Max
}
// InRange tests whether value is within the range (>= Min and <= Max)
func (mr *Int) InRange(val int) bool {
return ((val >= mr.Min) && (val <= mr.Max))
}
// IsLow tests whether value is lower than the minimum
func (mr *Int) IsLow(val int) bool {
return (val < mr.Min)
}
// IsHigh tests whether value is higher than the maximum
func (mr *Int) IsHigh(val int) bool {
return (val > mr.Min)
}
// Range returns Max - Min
func (mr *Int) Range() int {
return mr.Max - mr.Min
}
// Scale returns 1 / Range -- if Range = 0 then returns 0
func (mr *Int) Scale() float32 {
r := mr.Range()
if r != 0 {
return 1 / float32(r)
}
return 0
}
// Midpoint returns point halfway between Min and Max
func (mr *Int) Midpoint() float32 {
return 0.5 * float32(mr.Max+mr.Min)
}
// FitInRange adjusts our Min, Max to fit within those of other Int
// returns true if we had to adjust to fit.
func (mr *Int) FitInRange(oth Int) bool {
adj := false
if oth.Min < mr.Min {
mr.Min = oth.Min
adj = true
}
if oth.Max > mr.Max {
mr.Max = oth.Max
adj = true
}
return adj
}
// FitValInRange adjusts our Min, Max to fit given value within Min, Max range
// returns true if we had to adjust to fit.
func (mr *Int) FitValInRange(val int) bool {
adj := false
if val < mr.Min {
mr.Min = val
adj = true
}
if val > mr.Max {
mr.Max = val
adj = true
}
return adj
}
// NormVal normalizes value to 0-1 unit range relative to current Min / Max range
// Clips the value within Min-Max range first.
func (mr *Int) NormValue(val int) float32 {
return float32(mr.ClipValue(val)-mr.Min) * mr.Scale()
}
// ProjVal projects a 0-1 normalized unit value into current Min / Max range (inverse of NormVal)
func (mr *Int) ProjValue(val float32) float32 {
return float32(mr.Min) + (val * float32(mr.Range()))
}
// ClipVal clips given value within Min / Max rangee
func (mr *Int) ClipValue(val int) int {
if val < mr.Min {
return mr.Min
}
if val > mr.Max {
return mr.Max
}
return val
}
// ClipNormVal clips then normalizes given value within 0-1
func (mr *Int) ClipNormValue(val int) float32 {
if val < mr.Min {
return 0
}
if val > mr.Max {
return 1
}
return mr.NormValue(val)
}
// Copyright (c) 2024, 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 minmax
import "math"
// notes: gonum/plot has function labelling: talbotLinHanrahan
// based on this algorithm: http://vis.stanford.edu/files/2010-TickLabels-InfoVis.pdf
// but it is goes beyond this basic functionality, and is not exported in any case..
// but could be accessed using DefaultTicks api.
// NiceRoundNumber returns the closest nice round number either above or below
// the given number, based on the observation that numbers 1, 2, 5
// at any power are "nice".
// This is used for choosing graph labels, and auto-scaling ranges to contain
// a given value.
// if below == true then returned number is strictly less than given number
// otherwise it is strictly larger.
func NiceRoundNumber(x float64, below bool) float64 {
rn := x
neg := false
if x < 0 {
neg = true
below = !below // reverses..
}
abs := math.Abs(x)
exp := int(math.Floor(math.Log10(abs)))
order := math.Pow(10, float64(exp))
f := abs / order // fraction between 1 and 10
if below {
switch {
case f >= 5:
rn = 5
case f >= 2:
rn = 2
default:
rn = 1
}
} else {
switch {
case f <= 1:
rn = 1
case f <= 2:
rn = 2
case f <= 5:
rn = 5
default:
rn = 10
}
}
if neg {
return -rn * order
}
return rn * order
}
// Copyright (c) 2024, 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 minmax
// Range32 represents a range of values for plotting, where the min or max can optionally be fixed
type Range32 struct {
// Min and Max range values
F32
// fix the minimum end of the range
FixMin bool
// fix the maximum end of the range
FixMax bool
}
// SetMin sets a fixed min value
func (rr *Range32) SetMin(mn float32) {
rr.FixMin = true
rr.Min = mn
}
// SetMax sets a fixed max value
func (rr *Range32) SetMax(mx float32) {
rr.FixMax = true
rr.Max = mx
}
// Range returns Max - Min
func (rr *Range32) Range() float32 {
return rr.Max - rr.Min
}
///////////////////////////////////////////////////////////////////////
// 64
// Range64 represents a range of values for plotting, where the min or max can optionally be fixed
type Range64 struct {
// Min and Max range values
F64
// fix the minimum end of the range
FixMin bool
// fix the maximum end of the range
FixMax bool
}
// SetMin sets a fixed min value
func (rr *Range64) SetMin(mn float64) {
rr.FixMin = true
rr.Min = mn
}
// SetMax sets a fixed max value
func (rr *Range64) SetMax(mx float64) {
rr.FixMax = true
rr.Max = mx
}
// Range returns Max - Min
func (rr *Range64) Range() float64 {
return rr.Max - rr.Min
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
import "log"
// Plane represents a plane in 3D space by its normal vector and a constant offset.
// When the the normal vector is the unit vector the offset is the distance from the origin.
type Plane struct {
Norm Vector3
Off float32
}
// NewPlane creates and returns a new plane from a normal vector and a offset.
func NewPlane(normal Vector3, offset float32) *Plane {
p := &Plane{normal, offset}
return p
}
// Set sets this plane normal vector and offset.
func (p *Plane) Set(normal Vector3, offset float32) {
p.Norm = normal
p.Off = offset
}
// SetDims sets this plane normal vector dimensions and offset.
func (p *Plane) SetDims(x, y, z, w float32) {
p.Norm.Set(x, y, z)
p.Off = w
}
// SetFromNormalAndCoplanarPoint sets this plane from a normal vector and a point on the plane.
func (p *Plane) SetFromNormalAndCoplanarPoint(normal Vector3, point Vector3) {
p.Norm = normal
p.Off = -point.Dot(p.Norm)
}
// SetFromCoplanarPoints sets this plane from three coplanar points.
func (p *Plane) SetFromCoplanarPoints(a, b, c Vector3) {
norm := c.Sub(b).Cross(a.Sub(b))
norm.SetNormal()
if norm == (Vector3{}) {
log.Printf("math32.SetFromCoplanarPonts: points not actually coplanar: %v %v %v\n", a, b, c)
}
p.SetFromNormalAndCoplanarPoint(norm, a)
}
// Normalize normalizes this plane normal vector and adjusts the offset.
// Note: will lead to a divide by zero if the plane is invalid.
func (p *Plane) Normalize() {
invLen := 1.0 / p.Norm.Length()
p.Norm.SetMulScalar(invLen)
p.Off *= invLen
}
// Negate negates this plane normal.
func (p *Plane) Negate() {
p.Off *= -1
p.Norm = p.Norm.Negate()
}
// DistanceToPoint returns the distance of this plane from point.
func (p *Plane) DistanceToPoint(point Vector3) float32 {
return p.Norm.Dot(point) + p.Off
}
// DistanceToSphere returns the distance of this place from the sphere.
func (p *Plane) DistanceToSphere(sphere Sphere) float32 {
return p.DistanceToPoint(sphere.Center) - sphere.Radius
}
// IsIntersectionLine returns the line intersects this plane.
func (p *Plane) IsIntersectionLine(line Line3) bool {
startSign := p.DistanceToPoint(line.Start)
endSign := p.DistanceToPoint(line.End)
return (startSign < 0 && endSign > 0) || (endSign < 0 && startSign > 0)
}
// IntersectLine calculates the point in the plane which intersets the specified line.
// Returns false if the line does not intersects the plane.
func (p *Plane) IntersectLine(line Line3) (Vector3, bool) {
dir := line.Delta()
denom := p.Norm.Dot(dir)
if denom == 0 {
// line is coplanar, return origin
if p.DistanceToPoint(line.Start) == 0 {
return line.Start, true
}
// Unsure if this is the correct method to handle this case.
return dir, false
}
var t = -(line.Start.Dot(p.Norm) + p.Off) / denom
if t < 0 || t > 1 {
return dir, false
}
return dir.MulScalar(t).Add(line.Start), true
}
// CoplanarPoint returns a point in the plane that is the closest point from the origin.
func (p *Plane) CoplanarPoint() Vector3 {
return p.Norm.MulScalar(-p.Off)
}
// SetTranslate translates this plane in the direction of its normal by offset.
func (p *Plane) SetTranslate(offset Vector3) {
p.Off -= offset.Dot(p.Norm)
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
import "fmt"
// Quat is quaternion with X,Y,Z and W components.
type Quat struct {
X float32
Y float32
Z float32
W float32
}
// NewQuat returns a new quaternion from the specified components.
func NewQuat(x, y, z, w float32) Quat {
return Quat{X: x, Y: y, Z: z, W: w}
}
// NewQuatAxisAngle returns a new quaternion from given axis and angle rotation (radians).
func NewQuatAxisAngle(axis Vector3, angle float32) Quat {
nq := Quat{}
nq.SetFromAxisAngle(axis, angle)
return nq
}
// NewQuatEuler returns a new quaternion from given Euler angles.
func NewQuatEuler(euler Vector3) Quat {
nq := Quat{}
nq.SetFromEuler(euler)
return nq
}
// Set sets this quaternion's components.
func (q *Quat) Set(x, y, z, w float32) {
q.X = x
q.Y = y
q.Z = z
q.W = w
}
// FromArray sets this quaternion's components from array starting at offset.
func (q *Quat) FromArray(array []float32, offset int) {
q.X = array[offset]
q.Y = array[offset+1]
q.Z = array[offset+2]
q.W = array[offset+3]
}
// ToArray copies this quaternions's components to array starting at offset.
func (q *Quat) ToArray(array []float32, offset int) {
array[offset] = q.X
array[offset+1] = q.Y
array[offset+2] = q.Z
array[offset+3] = q.W
}
// SetIdentity sets this quanternion to the identity quaternion.
func (q *Quat) SetIdentity() {
q.X = 0
q.Y = 0
q.Z = 0
q.W = 1
}
// IsIdentity returns if this is an identity quaternion.
func (q *Quat) IsIdentity() bool {
return q.X == 0 && q.Y == 0 && q.Z == 0 && q.W == 1
}
// IsNil returns true if all values are 0 (uninitialized).
func (q *Quat) IsNil() bool {
return q.X == 0 && q.Y == 0 && q.Z == 0 && q.W == 0
}
func (q Quat) String() string {
return fmt.Sprintf("(%v, %v, %v, %v)", q.X, q.Y, q.Z, q.W)
}
// SetFromEuler sets this quaternion from the specified vector with
// Euler angles for each axis. It is assumed that the Euler angles
// are in XYZ order.
func (q *Quat) SetFromEuler(euler Vector3) {
c1 := Cos(euler.X / 2)
c2 := Cos(euler.Y / 2)
c3 := Cos(euler.Z / 2)
s1 := Sin(euler.X / 2)
s2 := Sin(euler.Y / 2)
s3 := Sin(euler.Z / 2)
q.X = s1*c2*c3 - c1*s2*s3
q.Y = c1*s2*c3 + s1*c2*s3
q.Z = c1*c2*s3 - s1*s2*c3
q.W = c1*c2*c3 + s1*s2*s3
}
// ToEuler returns a Vector3 with components as the Euler angles
// from the given quaternion.
func (q *Quat) ToEuler() Vector3 {
rot := Vector3{}
rot.SetEulerAnglesFromQuat(*q)
return rot
}
// SetFromAxisAngle sets this quaternion with the rotation
// specified by the given axis and angle.
func (q *Quat) SetFromAxisAngle(axis Vector3, angle float32) {
halfAngle := angle / 2
s := Sin(halfAngle)
q.X = axis.X * s
q.Y = axis.Y * s
q.Z = axis.Z * s
q.W = Cos(halfAngle)
}
// ToAxisAngle returns the Vector4 holding axis and angle of this Quaternion
func (q *Quat) ToAxisAngle() Vector4 {
aa := Vector4{}
aa.SetAxisAngleFromQuat(*q)
return aa
}
// GenGoSet returns code to set values in object at given path (var.member etc)
func (q *Quat) GenGoSet(path string) string {
aa := q.ToAxisAngle()
return fmt.Sprintf("%s.SetFromAxisAngle(math32.Vec3(%g, %g, %g), %g)", path, aa.X, aa.Y, aa.Z, aa.W)
}
// GenGoNew returns code to create new
func (q *Quat) GenGoNew() string {
return fmt.Sprintf("math32.Quat{%g, %g, %g, %g}", q.X, q.Y, q.Z, q.W)
}
// SetFromRotationMatrix sets this quaternion from the specified rotation matrix.
func (q *Quat) SetFromRotationMatrix(m *Matrix4) {
m11 := m[0]
m12 := m[4]
m13 := m[8]
m21 := m[1]
m22 := m[5]
m23 := m[9]
m31 := m[2]
m32 := m[6]
m33 := m[10]
trace := m11 + m22 + m33
var s float32
if trace > 0 {
s = 0.5 / Sqrt(trace+1.0)
q.W = 0.25 / s
q.X = (m32 - m23) * s
q.Y = (m13 - m31) * s
q.Z = (m21 - m12) * s
} else if m11 > m22 && m11 > m33 {
s = 2.0 * Sqrt(1.0+m11-m22-m33)
q.W = (m32 - m23) / s
q.X = 0.25 * s
q.Y = (m12 + m21) / s
q.Z = (m13 + m31) / s
} else if m22 > m33 {
s = 2.0 * Sqrt(1.0+m22-m11-m33)
q.W = (m13 - m31) / s
q.X = (m12 + m21) / s
q.Y = 0.25 * s
q.Z = (m23 + m32) / s
} else {
s = 2.0 * Sqrt(1.0+m33-m11-m22)
q.W = (m21 - m12) / s
q.X = (m13 + m31) / s
q.Y = (m23 + m32) / s
q.Z = 0.25 * s
}
}
// SetFromUnitVectors sets this quaternion to the rotation from vector vFrom to vTo.
// The vectors must be normalized.
func (q *Quat) SetFromUnitVectors(vFrom, vTo Vector3) {
var v1 Vector3
var EPS float32 = 0.000001
r := vFrom.Dot(vTo) + 1
if r < EPS {
r = 0
if Abs(vFrom.X) > Abs(vFrom.Z) {
v1.Set(-vFrom.Y, vFrom.X, 0)
} else {
v1.Set(0, -vFrom.Z, vFrom.Y)
}
} else {
v1 = vFrom.Cross(vTo)
}
q.X = v1.X
q.Y = v1.Y
q.Z = v1.Z
q.W = r
q.Normalize()
}
// SetInverse sets this quaternion to its inverse.
func (q *Quat) SetInverse() {
q.SetConjugate()
q.Normalize()
}
// Inverse returns the inverse of this quaternion.
func (q *Quat) Inverse() Quat {
nq := *q
nq.SetInverse()
return nq
}
// SetConjugate sets this quaternion to its conjugate.
func (q *Quat) SetConjugate() {
q.X *= -1
q.Y *= -1
q.Z *= -1
}
// Conjugate returns the conjugate of this quaternion.
func (q *Quat) Conjugate() Quat {
nq := *q
nq.SetConjugate()
return nq
}
// Dot returns the dot products of this quaternion with other.
func (q *Quat) Dot(other Quat) float32 {
return q.X*other.X + q.Y*other.Y + q.Z*other.Z + q.W*other.W
}
// LengthSq returns this quanternion's length squared
func (q Quat) LengthSq() float32 {
return q.X*q.X + q.Y*q.Y + q.Z*q.Z + q.W*q.W
}
// Length returns the length of this quaternion
func (q Quat) Length() float32 {
return Sqrt(q.X*q.X + q.Y*q.Y + q.Z*q.Z + q.W*q.W)
}
// Normalize normalizes this quaternion.
func (q *Quat) Normalize() {
l := q.Length()
if l == 0 {
q.X = 0
q.Y = 0
q.Z = 0
q.W = 1
} else {
l = 1 / l
q.X *= l
q.Y *= l
q.Z *= l
q.W *= l
}
}
// NormalizeFast approximates normalizing this quaternion.
// Works best when the quaternion is already almost-normalized.
func (q *Quat) NormalizeFast() {
f := (3.0 - (q.X*q.X + q.Y*q.Y + q.Z*q.Z + q.W*q.W)) / 2.0
if f == 0 {
q.X = 0
q.Y = 0
q.Z = 0
q.W = 1
} else {
q.X *= f
q.Y *= f
q.Z *= f
q.W *= f
}
}
// MulQuats set this quaternion to the multiplication of a by b.
func (q *Quat) MulQuats(a, b Quat) {
// from http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm
qax := a.X
qay := a.Y
qaz := a.Z
qaw := a.W
qbx := b.X
qby := b.Y
qbz := b.Z
qbw := b.W
q.X = qax*qbw + qaw*qbx + qay*qbz - qaz*qby
q.Y = qay*qbw + qaw*qby + qaz*qbx - qax*qbz
q.Z = qaz*qbw + qaw*qbz + qax*qby - qay*qbx
q.W = qaw*qbw - qax*qbx - qay*qby - qaz*qbz
}
// SetMul sets this quaternion to the multiplication of itself by other.
func (q *Quat) SetMul(other Quat) {
q.MulQuats(*q, other)
}
// Mul returns returns multiplication of this quaternion with other
func (q *Quat) Mul(other Quat) Quat {
nq := *q
nq.SetMul(other)
return nq
}
// Slerp sets this quaternion to another quaternion which is the spherically linear interpolation
// from this quaternion to other using t.
func (q *Quat) Slerp(other Quat, t float32) {
if t == 0 {
return
}
if t == 1 {
*q = other
return
}
x := q.X
y := q.Y
z := q.Z
w := q.W
cosHalfTheta := w*other.W + x*other.X + y*other.Y + z*other.Z
if cosHalfTheta < 0 {
q.W = -other.W
q.X = -other.X
q.Y = -other.Y
q.Z = -other.Z
cosHalfTheta = -cosHalfTheta
} else {
*q = other
}
if cosHalfTheta >= 1.0 {
q.W = w
q.X = x
q.Y = y
q.Z = z
return
}
sqrSinHalfTheta := 1.0 - cosHalfTheta*cosHalfTheta
if sqrSinHalfTheta < 0.001 {
s := 1 - t
q.W = s*w + t*q.W
q.X = s*x + t*q.X
q.Y = s*y + t*q.Y
q.Z = s*z + t*q.Z
q.Normalize()
return
}
sinHalfTheta := Sqrt(sqrSinHalfTheta)
halfTheta := Atan2(sinHalfTheta, cosHalfTheta)
ratioA := Sin((1-t)*halfTheta) / sinHalfTheta
ratioB := Sin(t*halfTheta) / sinHalfTheta
q.W = w*ratioA + q.W*ratioB
q.X = x*ratioA + q.X*ratioB
q.Y = y*ratioA + q.Y*ratioB
q.Z = z*ratioA + q.Z*ratioB
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
// Ray represents an oriented 3D line segment defined by an origin point and a direction vector.
type Ray struct {
Origin Vector3
Dir Vector3
}
// NewRay creates and returns a pointer to a Ray object with
// the specified origin and direction vectors.
// If a nil pointer is supplied for any of the parameters,
// the zero vector will be used.
func NewRay(origin, dir Vector3) *Ray {
return &Ray{origin, dir}
}
// Set sets the origin and direction vectors of this Ray.
func (ray *Ray) Set(origin, dir Vector3) {
ray.Origin = origin
ray.Dir = dir
}
// At calculates the point in the ray which is at the specified t distance from the origin
// along its direction.
func (ray *Ray) At(t float32) Vector3 {
return ray.Dir.MulScalar(t).Add(ray.Origin)
}
// Recast sets the new origin of the ray at the specified distance t
// from its origin along its direction.
func (ray *Ray) Recast(t float32) {
ray.Origin = ray.At(t)
}
// ClosestPointToPoint calculates the point in the ray which is closest to the specified point.
func (ray *Ray) ClosestPointToPoint(point Vector3) Vector3 {
dirDist := point.Sub(ray.Origin).Dot(ray.Dir)
if dirDist < 0 {
return ray.Origin
}
return ray.Dir.MulScalar(dirDist).Add(ray.Origin)
}
// DistanceToPoint returns the smallest distance
// from the ray direction vector to the specified point.
func (ray *Ray) DistanceToPoint(point Vector3) float32 {
return Sqrt(ray.DistanceSquaredToPoint(point))
}
// DistanceSquaredToPoint returns the smallest squared distance
// from the ray direction vector to the specified point.
// If the ray was pointed directly at the point this distance would be 0.
func (ray *Ray) DistanceSquaredToPoint(point Vector3) float32 {
dirDist := point.Sub(ray.Origin).Dot(ray.Dir)
// point behind the ray
if dirDist < 0 {
return ray.Origin.DistanceTo(point)
}
return ray.Dir.MulScalar(dirDist).Add(ray.Origin).DistanceToSquared(point)
}
// DistanceSquaredToSegment returns the smallest squared distance
// from this ray to the line segment from v0 to v1.
// If optPointOnRay Vector3 is not nil,
// it is set with the coordinates of the point on the ray.
// if optPointOnSegment Vector3 is not nil,
// it is set with the coordinates of the point on the segment.
func (ray *Ray) DistanceSquaredToSegment(v0, v1 Vector3, optPointOnRay, optPointOnSegment *Vector3) float32 {
segCenter := v0.Add(v1).MulScalar(0.5)
segDir := v1.Sub(v0).Normal()
diff := ray.Origin.Sub(segCenter)
segExtent := v0.DistanceTo(v1) * 0.5
a01 := -ray.Dir.Dot(segDir)
b0 := diff.Dot(ray.Dir)
b1 := -diff.Dot(segDir)
c := diff.LengthSquared()
det := Abs(1 - a01*a01)
var s0, s1, sqrDist, extDet float32
if det > 0 {
// The ray and segment are not parallel.
s0 = a01*b1 - b0
s1 = a01*b0 - b1
extDet = segExtent * det
if s0 >= 0 {
if s1 >= -extDet {
if s1 <= extDet {
// region 0
// Minimum at interior points of ray and segment.
invDet := 1 / det
s0 *= invDet
s1 *= invDet
sqrDist = s0*(s0+a01*s1+2*b0) + s1*(a01*s0+s1+2*b1) + c
} else {
// region 1
s1 = segExtent
s0 = Max(0, -(a01*s1 + b0))
sqrDist = -s0*s0 + s1*(s1+2*b1) + c
}
} else {
// region 5
s1 = -segExtent
s0 = Max(0, -(a01*s1 + b0))
sqrDist = -s0*s0 + s1*(s1+2*b1) + c
}
} else {
if s1 <= -extDet {
// region 4
s0 = Max(0, -(-a01*segExtent + b0))
if s0 > 0 {
s1 = -segExtent
} else {
s1 = Min(Max(-segExtent, -b1), segExtent)
}
sqrDist = -s0*s0 + s1*(s1+2*b1) + c
} else if s1 <= extDet {
// region 3
s0 = 0
s1 = Min(Max(-segExtent, -b1), segExtent)
sqrDist = s1*(s1+2*b1) + c
} else {
// region 2
s0 = Max(0, -(a01*segExtent + b0))
if s0 > 0 {
s1 = segExtent
} else {
s1 = Min(Max(-segExtent, -b1), segExtent)
}
sqrDist = -s0*s0 + s1*(s1+2*b1) + c
}
}
} else {
// Ray and segment are parallel.
if a01 > 0 {
s1 = -segExtent
} else {
s1 = segExtent
}
s0 = Max(0, -(a01*s1 + b0))
sqrDist = -s0*s0 + s1*(s1+2*b1) + c
}
if optPointOnRay != nil {
*optPointOnRay = ray.Dir.MulScalar(s0).Add(ray.Origin)
}
if optPointOnSegment != nil {
*optPointOnSegment = segDir.MulScalar(s1).Add(segCenter)
}
return sqrDist
}
// IsIntersectionSphere returns if this ray intersects with the specified sphere.
func (ray *Ray) IsIntersectionSphere(sphere Sphere) bool {
return ray.DistanceToPoint(sphere.Center) <= sphere.Radius
}
// IntersectSphere calculates the point which is the intersection of this ray with the specified sphere.
// If no intersection is found it returns false.
func (ray *Ray) IntersectSphere(sphere Sphere) (Vector3, bool) {
v1 := sphere.Center.Sub(ray.Origin)
tca := v1.Dot(ray.Dir)
d2 := v1.Dot(v1) - tca*tca
radius2 := sphere.Radius * sphere.Radius
if d2 > radius2 {
return v1, false
}
thc := Sqrt(radius2 - d2)
// t0 = first intersect point - entrance on front of sphere
t0 := tca - thc
// t1 = second intersect point - exit point on back of sphere
t1 := tca + thc
// test to see if both t0 and t1 are behind the ray - if so, return null
if t0 < 0 && t1 < 0 {
return v1, false
}
// test to see if t0 is behind the ray:
// if it is, the ray is inside the sphere, so return the second exit point scaled by t1,
// in order to always return an intersect point that is in front of the ray.
if t0 < 0 {
return ray.At(t1), true
}
// else t0 is in front of the ray, so return the first collision point scaled by t0
return ray.At(t0), true
}
// IsIntersectPlane returns if this ray intersects the specified plane.
func (ray *Ray) IsIntersectPlane(plane Plane) bool {
distToPoint := plane.DistanceToPoint(ray.Origin)
if distToPoint == 0 {
return true
}
denom := plane.Norm.Dot(ray.Dir)
// if false, ray origin is behind the plane (and is pointing behind it)
return denom*distToPoint < 0
}
// DistanceToPlane returns the distance of this ray origin to its intersection point in the plane.
// If the ray does not intersects the plane, returns NaN.
func (ray *Ray) DistanceToPlane(plane Plane) float32 {
denom := plane.Norm.Dot(ray.Dir)
if denom == 0 {
// line is coplanar, return origin
if plane.DistanceToPoint(ray.Origin) == 0 {
return 0
}
return NaN()
}
t := -(ray.Origin.Dot(plane.Norm) + plane.Off) / denom
// Return if the ray never intersects the plane
if t >= 0 {
return t
}
return NaN()
}
// IntersectPlane calculates the point which is the intersection of this ray with the specified plane.
// If no intersection is found false is returned.
func (ray *Ray) IntersectPlane(plane Plane) (Vector3, bool) {
t := ray.DistanceToPlane(plane)
if t == NaN() {
return ray.Origin, false
}
return ray.At(t), true
}
// IntersectBox calculates the point which is the intersection of this ray with the specified box.
// If no intersection is found false is returned.
func (ray *Ray) IntersectBox(box Box3) (Vector3, bool) {
// http://www.scratchapixel.com/lessons/3d-basic-lessons/lesson-7-intersecting-simple-shapes/ray-box-intersection/
var tmin, tmax, tymin, tymax, tzmin, tzmax float32
invdirx := 1 / ray.Dir.X
invdiry := 1 / ray.Dir.Y
invdirz := 1 / ray.Dir.Z
var origin = ray.Origin
if invdirx >= 0 {
tmin = (box.Min.X - origin.X) * invdirx
tmax = (box.Max.X - origin.X) * invdirx
} else {
tmin = (box.Max.X - origin.X) * invdirx
tmax = (box.Min.X - origin.X) * invdirx
}
if invdiry >= 0 {
tymin = (box.Min.Y - origin.Y) * invdiry
tymax = (box.Max.Y - origin.Y) * invdiry
} else {
tymin = (box.Max.Y - origin.Y) * invdiry
tymax = (box.Min.Y - origin.Y) * invdiry
}
if (tmin > tymax) || (tymin > tmax) {
return ray.Origin, false
}
// These lines also handle the case where tmin or tmax is NaN
// (result of 0 * Infinity). x !== x returns true if x is NaN
if tymin > tmin || tmin != tmin {
tmin = tymin
}
if tymax < tmax || tmax != tmax {
tmax = tymax
}
if invdirz >= 0 {
tzmin = (box.Min.Z - origin.Z) * invdirz
tzmax = (box.Max.Z - origin.Z) * invdirz
} else {
tzmin = (box.Max.Z - origin.Z) * invdirz
tzmax = (box.Min.Z - origin.Z) * invdirz
}
if (tmin > tzmax) || (tzmin > tmax) {
return ray.Origin, false
}
if tzmin > tmin || tmin != tmin {
tmin = tzmin
}
if tzmax < tmax || tmax != tmax {
tmax = tzmax
}
// return point closest to the ray (positive side)
if tmax < 0 {
return ray.Origin, false
}
if tmin >= 0 {
return ray.At(tmin), true
}
return ray.At(tmax), true
}
// IntersectTriangle returns if this ray intersects the triangle with the face
// defined by points a, b, c. Returns true if it intersects and the point
// parameter with the intersected point coordinates.
// If backfaceCulling is false it ignores the intersection if the face is not oriented
// in the ray direction.
func (ray *Ray) IntersectTriangle(a, b, c Vector3, backfaceCulling bool) (Vector3, bool) {
edge1 := b.Sub(a)
edge2 := c.Sub(a)
normal := edge1.Cross(edge2)
// Solve Q + t*D = b1*E1 + b2*E2 (Q = kDiff, D = ray direction,
// E1 = kEdge1, E2 = kEdge2, N = Cross(E1,E2)) by
// |Dot(D,N)|*b1 = sign(Dot(D,N))*Dot(D,Cross(Q,E2))
// |Dot(D,N)|*b2 = sign(Dot(D,N))*Dot(D,Cross(E1,Q))
// |Dot(D,N)|*t = -sign(Dot(D,N))*Dot(Q,N)
DdN := ray.Dir.Dot(normal)
var sign float32
if DdN > 0 {
if backfaceCulling {
return ray.Origin, false
}
sign = 1
} else if DdN < 0 {
sign = -1
DdN = -DdN
} else {
return ray.Origin, false
}
diff := ray.Origin.Sub(a)
DdQxE2 := sign * ray.Dir.Dot(diff.Cross(edge2))
// b1 < 0, no intersection
if DdQxE2 < 0 {
return ray.Origin, false
}
DdE1xQ := sign * ray.Dir.Dot(edge1.Cross(diff))
// b2 < 0, no intersection
if DdE1xQ < 0 {
return ray.Origin, false
}
// b1+b2 > 1, no intersection
if DdQxE2+DdE1xQ > DdN {
return ray.Origin, false
}
// Line intersects triangle, check if ray does.
QdN := -sign * diff.Dot(normal)
// t < 0, no intersection
if QdN < 0 {
return ray.Origin, false
}
// Ray intersects triangle.
return ray.At(QdN / DdN), true
}
// MulMatrix4 multiplies this ray origin and direction
// by the specified matrix4, basically transforming this ray coordinates.
func (ray *Ray) ApplyMatrix4(mat4 *Matrix4) {
ray.Dir = ray.Dir.Add(ray.Origin).MulMatrix4(mat4)
ray.Origin = ray.Origin.MulMatrix4(mat4)
ray.Dir.SetSub(ray.Origin)
ray.Dir.SetNormal()
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
// Sphere represents a 3D sphere defined by its center point and a radius
type Sphere struct {
Center Vector3 // center of the sphere
Radius float32 // radius of the sphere
}
// NewSphere creates and returns a pointer to a new sphere with
// the specified center and radius.
func NewSphere(center Vector3, radius float32) *Sphere {
return &Sphere{center, radius}
}
// Set sets the center and radius of this sphere.
func (s *Sphere) Set(center Vector3, radius float32) {
s.Center = center
s.Radius = radius
}
// SetFromBox sets the center and radius of this sphere to surround given box
func (s *Sphere) SetFromBox(box Box3) {
s.Center = box.Center()
s.Radius = 0.5 * box.Size().Length()
}
// SetFromPoints sets this sphere from the specified points array and optional center.
func (s *Sphere) SetFromPoints(points []Vector3, optCenter *Vector3) {
box := B3Empty()
if optCenter != nil {
s.Center = *optCenter
} else {
box.SetFromPoints(points)
s.Center = box.Center()
}
var maxRadiusSq float32
for i := 0; i < len(points); i++ {
maxRadiusSq = Max(maxRadiusSq, s.Center.DistanceToSquared(points[i]))
}
s.Radius = Sqrt(maxRadiusSq)
}
// IsEmpty checks if this sphere is empty (radius <= 0)
func (s *Sphere) IsEmpty(sphere *Sphere) bool {
return s.Radius <= 0
}
// ContainsPoint returns if this sphere contains the specified point.
func (s *Sphere) ContainsPoint(point Vector3) bool {
return point.DistanceToSquared(s.Center) <= (s.Radius * s.Radius)
}
// DistanceToPoint returns the distance from the sphere surface to the specified point.
func (s *Sphere) DistanceToPoint(point Vector3) float32 {
return point.DistanceTo(s.Center) - s.Radius
}
// IntersectSphere returns if other sphere intersects this one.
func (s *Sphere) IntersectSphere(other Sphere) bool {
radiusSum := s.Radius + other.Radius
return other.Center.DistanceToSquared(s.Center) <= (radiusSum * radiusSum)
}
// ClampPoint clamps the specified point inside the sphere.
// If the specified point is inside the sphere, it is the clamped point.
// Otherwise the clamped point is the the point in the sphere surface in the
// nearest of the specified point.
func (s *Sphere) ClampPoint(point Vector3) Vector3 {
deltaLengthSq := s.Center.DistanceToSquared(point)
rv := point
if deltaLengthSq > (s.Radius * s.Radius) {
rv = point.Sub(s.Center).Normal().MulScalar(s.Radius).Add(s.Center)
}
return rv
}
// GetBoundingBox calculates a [Box3] which bounds this sphere.
func (s *Sphere) GetBoundingBox() Box3 {
box := Box3{s.Center, s.Center}
box.ExpandByScalar(s.Radius)
return box
}
// MulMatrix4 applies the specified matrix transform to this sphere.
func (s *Sphere) MulMatrix4(mat *Matrix4) {
s.Center = s.Center.MulMatrix4(mat)
s.Radius = s.Radius * mat.GetMaxScaleOnAxis()
}
// Translate translates this sphere by the specified offset.
func (s *Sphere) Translate(offset Vector3) {
s.Center.SetAdd(offset)
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
// Triangle represents a triangle made of three vertices.
type Triangle struct {
A Vector3
B Vector3
C Vector3
}
// NewTriangle returns a new Triangle object.
func NewTriangle(a, b, c Vector3) Triangle {
return Triangle{a, b, c}
}
// Normal returns the triangle's normal.
func Normal(a, b, c Vector3) Vector3 {
nv := c.Sub(b).Cross(a.Sub(b))
lenSq := nv.LengthSquared()
if lenSq > 0 {
return nv.MulScalar(1 / Sqrt(lenSq))
}
return Vector3{}
}
// BarycoordFromPoint returns the barycentric coordinates for the specified point.
func BarycoordFromPoint(point, a, b, c Vector3) Vector3 {
v0 := c.Sub(a)
v1 := b.Sub(a)
v2 := point.Sub(a)
dot00 := v0.Dot(v0)
dot01 := v0.Dot(v1)
dot02 := v0.Dot(v2)
dot11 := v1.Dot(v1)
dot12 := v1.Dot(v2)
denom := dot00*dot11 - dot01*dot01
// colinear or singular triangle
if denom == 0 {
// arbitrary location outside of triangle?
// not sure if this is the best idea, maybe should be returning undefined
return Vec3(-2, -1, -1)
}
invDenom := 1 / denom
u := (dot11*dot02 - dot01*dot12) * invDenom
v := (dot00*dot12 - dot01*dot02) * invDenom
// barycoordinates must always sum to 1
return Vec3(1-u-v, v, u)
}
// ContainsPoint returns whether a triangle contains a point.
func ContainsPoint(point, a, b, c Vector3) bool {
rv := BarycoordFromPoint(point, a, b, c)
return (rv.X >= 0) && (rv.Y >= 0) && ((rv.X + rv.Y) <= 1)
}
// Set sets the triangle's three vertices.
func (t *Triangle) Set(a, b, c Vector3) {
t.A = a
t.B = b
t.C = c
}
// SetFromPointsAndIndices sets the triangle's vertices based on the specified points and indices.
func (t *Triangle) SetFromPointsAndIndices(points []Vector3, i0, i1, i2 int) {
t.A = points[i0]
t.B = points[i1]
t.C = points[i2]
}
// Area returns the triangle's area.
func (t *Triangle) Area() float32 {
v0 := t.C.Sub(t.B)
v1 := t.A.Sub(t.B)
return v0.Cross(v1).Length() * 0.5
}
// Midpoint returns the triangle's midpoint.
func (t *Triangle) Midpoint() Vector3 {
return t.A.Add(t.B).Add(t.C).MulScalar(float32(1) / 3)
}
// Normal returns the triangle's normal.
func (t *Triangle) Normal() Vector3 {
return Normal(t.A, t.B, t.C)
}
// Plane returns a Plane object aligned with the triangle.
func (t *Triangle) Plane() Plane {
pv := Plane{}
pv.SetFromCoplanarPoints(t.A, t.B, t.C)
return pv
}
// BarycoordFromPoint returns the barycentric coordinates for the specified point.
func (t *Triangle) BarycoordFromPoint(point Vector3) Vector3 {
return BarycoordFromPoint(point, t.A, t.B, t.C)
}
// ContainsPoint returns whether the triangle contains a point.
func (t *Triangle) ContainsPoint(point Vector3) bool {
return ContainsPoint(point, t.A, t.B, t.C)
}
// Copyright (c) 2019, 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
// Package vecint has vector types for emergent, including Vector2i which is a 2D
// vector with int values, using the API based on math32.Vector2i.
// This is distinct from math32.Vector2i because it uses int instead of int32, and
// the int is significantly easier to deal with for some use-cases.
package vecint
//go:generate core generate
import "cogentcore.org/core/math32"
// Vector2i is a 2D vector/point with X and Y int components.
type Vector2i struct {
X int
Y int
}
// Vec2i returns a new [Vector2i] with the given x and y components.
func Vec2i(x, y int) Vector2i {
return Vector2i{X: x, Y: y}
}
// Vector2iScalar returns a new Vector2i with all components set to scalar.
func Vector2iScalar(scalar int) Vector2i {
return Vector2i{X: scalar, Y: scalar}
}
// Vector2iFromVector2Round converts from floating point math32.Vector2 vector to int, using rounding
func Vector2iFromVector2Round(v math32.Vector2) Vector2i {
return Vector2i{int(math32.Round(v.X)), int(math32.Round(v.Y))}
}
// Vector2iFromVector2Floor converts from floating point math32.Vector2 vector to int, using floor
func Vector2iFromVector2Floor(v math32.Vector2) Vector2i {
return Vector2i{int(math32.Floor(v.X)), int(math32.Floor(v.Y))}
}
// Vector2iFromVector2Ceil converts from floating point math32.Vector2 vector to int, using ceil
func Vector2iFromVector2Ceil(v math32.Vector2) Vector2i {
return Vector2i{X: int(math32.Ceil(v.X)), Y: int(math32.Ceil(v.Y))}
}
// ToVector2 returns floating point [math32.Vector2] from int.
func (v Vector2i) ToVector2() math32.Vector2 {
return math32.Vec2(float32(v.X), float32(v.Y))
}
// Set sets this vector X and Y components.
func (v *Vector2i) Set(x, y int) {
v.X = x
v.Y = y
}
// SetScalar sets all vector components to same scalar value.
func (v *Vector2i) SetScalar(scalar int) {
v.X = scalar
v.Y = scalar
}
// SetDim sets this vector component value by its dimension index
func (v *Vector2i) SetDim(dim math32.Dims, value int) {
switch dim {
case math32.X:
v.X = value
case math32.Y:
v.Y = value
default:
panic("dim is out of range")
}
}
// Dim returns this vector component
func (v Vector2i) Dim(dim math32.Dims) int {
switch dim {
case math32.X:
return v.X
case math32.Y:
return v.Y
default:
panic("dim is out of range")
}
}
// SetZero sets all of the vector's components to zero.
func (v *Vector2i) SetZero() {
v.SetScalar(0)
}
// FromSlice sets this vector's components from the given slice, starting at offset.
func (v *Vector2i) FromSlice(array []int, offset int) {
v.X = array[offset]
v.Y = array[offset+1]
}
// ToSlice copies this vector's components to the given slice, starting at offset.
func (v Vector2i) ToSlice(array []int, offset int) {
array[offset] = v.X
array[offset+1] = v.Y
}
// Basic math operations:
// Add adds the other given vector to this one and returns the result as a new vector.
func (v Vector2i) Add(other Vector2i) Vector2i {
return Vector2i{v.X + other.X, v.Y + other.Y}
}
// AddScalar adds scalar s to each component of this vector and returns new vector.
func (v Vector2i) AddScalar(s int) Vector2i {
return Vector2i{v.X + s, v.Y + s}
}
// SetAdd sets this to addition with other vector (i.e., += or plus-equals).
func (v *Vector2i) SetAdd(other Vector2i) {
v.X += other.X
v.Y += other.Y
}
// SetAddScalar sets this to addition with scalar.
func (v *Vector2i) SetAddScalar(s int) {
v.X += s
v.Y += s
}
// Sub subtracts other vector from this one and returns result in new vector.
func (v Vector2i) Sub(other Vector2i) Vector2i {
return Vector2i{v.X - other.X, v.Y - other.Y}
}
// SubScalar subtracts scalar s from each component of this vector and returns new vector.
func (v Vector2i) SubScalar(s int) Vector2i {
return Vector2i{v.X - s, v.Y - s}
}
// SetSub sets this to subtraction with other vector (i.e., -= or minus-equals).
func (v *Vector2i) SetSub(other Vector2i) {
v.X -= other.X
v.Y -= other.Y
}
// SetSubScalar sets this to subtraction of scalar.
func (v *Vector2i) SetSubScalar(s int) {
v.X -= s
v.Y -= s
}
// Mul multiplies each component of this vector by the corresponding one from other
// and returns resulting vector.
func (v Vector2i) Mul(other Vector2i) Vector2i {
return Vector2i{v.X * other.X, v.Y * other.Y}
}
// MulScalar multiplies each component of this vector by the scalar s and returns resulting vector.
func (v Vector2i) MulScalar(s int) Vector2i {
return Vector2i{v.X * s, v.Y * s}
}
// SetMul sets this to multiplication with other vector (i.e., *= or times-equals).
func (v *Vector2i) SetMul(other Vector2i) {
v.X *= other.X
v.Y *= other.Y
}
// SetMulScalar sets this to multiplication by scalar.
func (v *Vector2i) SetMulScalar(s int) {
v.X *= s
v.Y *= s
}
// Div divides each component of this vector by the corresponding one from other vector
// and returns resulting vector.
func (v Vector2i) Div(other Vector2i) Vector2i {
return Vector2i{v.X / other.X, v.Y / other.Y}
}
// DivScalar divides each component of this vector by the scalar s and returns resulting vector.
// If scalar is zero, returns zero.
func (v Vector2i) DivScalar(scalar int) Vector2i {
if scalar != 0 {
return Vector2i{v.X / scalar, v.Y / scalar}
} else {
return Vector2i{}
}
}
// SetDiv sets this to division by other vector (i.e., /= or divide-equals).
func (v *Vector2i) SetDiv(other Vector2i) {
v.X /= other.X
v.Y /= other.Y
}
// SetDivScalar sets this to division by scalar.
func (v *Vector2i) SetDivScalar(scalar int) {
if scalar != 0 {
v.X /= scalar
v.Y /= scalar
} else {
v.SetZero()
}
}
// Min returns min of this vector components vs. other vector.
func (v Vector2i) Min(other Vector2i) Vector2i {
return Vector2i{min(v.X, other.X), min(v.Y, other.Y)}
}
// SetMin sets this vector components to the minimum values of itself and other vector.
func (v *Vector2i) SetMin(other Vector2i) {
v.X = min(v.X, other.X)
v.Y = min(v.Y, other.Y)
}
// Max returns max of this vector components vs. other vector.
func (v Vector2i) Max(other Vector2i) Vector2i {
return Vector2i{max(v.X, other.X), max(v.Y, other.Y)}
}
// SetMax sets this vector components to the maximum value of itself and other vector.
func (v *Vector2i) SetMax(other Vector2i) {
v.X = max(v.X, other.X)
v.Y = max(v.Y, other.Y)
}
// Clamp sets this vector's components to be no less than the corresponding
// components of min and not greater than the corresponding component of max.
// Assumes min < max; if this assumption isn't true, it will not operate correctly.
func (v *Vector2i) Clamp(min, max Vector2i) {
if v.X < min.X {
v.X = min.X
} else if v.X > max.X {
v.X = max.X
}
if v.Y < min.Y {
v.Y = min.Y
} else if v.Y > max.Y {
v.Y = max.Y
}
}
// Negate returns the vector with each component negated.
func (v Vector2i) Negate() Vector2i {
return Vector2i{-v.X, -v.Y}
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
import (
"fmt"
"image"
"golang.org/x/image/math/fixed"
)
// Vector2 is a 2D vector/point with X and Y components.
type Vector2 struct {
X float32
Y float32
}
// Vec2 returns a new [Vector2] with the given x and y components.
func Vec2(x, y float32) Vector2 {
return Vector2{x, y}
}
// Vector2Scalar returns a new [Vector2] with all components set to the given scalar value.
func Vector2Scalar(scalar float32) Vector2 {
return Vector2{scalar, scalar}
}
// FromPoint returns a new [Vector2] from the given [image.Point].
func FromPoint(pt image.Point) Vector2 {
v := Vector2{}
v.SetPoint(pt)
return v
}
// Vector2FromFixed returns a new [Vector2] from the given [fixed.Point26_6].
func Vector2FromFixed(pt fixed.Point26_6) Vector2 {
v := Vector2{}
v.SetFixed(pt)
return v
}
// Set sets this vector's X and Y components.
func (v *Vector2) Set(x, y float32) {
v.X = x
v.Y = y
}
// SetScalar sets all vector components to the same scalar value.
func (v *Vector2) SetScalar(scalar float32) {
v.X = scalar
v.Y = scalar
}
// SetFromVector2i sets from a [Vector2i] (int32) vector.
func (v *Vector2) SetFromVector2i(vi Vector2i) {
v.X = float32(vi.X)
v.Y = float32(vi.Y)
}
// SetDim sets the given vector component value by its dimension index.
func (v *Vector2) SetDim(dim Dims, value float32) {
switch dim {
case X:
v.X = value
case Y:
v.Y = value
default:
panic("dim is out of range")
}
}
// Dim returns the given vector component.
func (v Vector2) Dim(dim Dims) float32 {
switch dim {
case X:
return v.X
case Y:
return v.Y
default:
panic("dim is out of range")
}
}
// SetPointDim sets the given dimension of the given [image.Point] to the given value.
func SetPointDim(pt *image.Point, dim Dims, value int) {
switch dim {
case X:
pt.X = value
case Y:
pt.Y = value
default:
panic("dim is out of range")
}
}
// PointDim returns the given dimension of the given [image.Point].
func PointDim(pt image.Point, dim Dims) int {
switch dim {
case X:
return pt.X
case Y:
return pt.Y
default:
panic("dim is out of range")
}
}
func (a Vector2) String() string {
return fmt.Sprintf("(%v, %v)", a.X, a.Y)
}
// SetPoint sets the vector from the given [image.Point].
func (a *Vector2) SetPoint(pt image.Point) {
a.X = float32(pt.X)
a.Y = float32(pt.Y)
}
// SetFixed sets the vector from the given [fixed.Point26_6].
func (a *Vector2) SetFixed(pt fixed.Point26_6) {
a.X = FromFixed(pt.X)
a.Y = FromFixed(pt.Y)
}
// ToPoint returns the vector as an [image.Point].
func (a Vector2) ToPoint() image.Point {
return image.Point{int(a.X), int(a.Y)}
}
// ToPointFloor returns the vector as an [image.Point] with all values [Floor]ed.
func (a Vector2) ToPointFloor() image.Point {
return image.Point{int(Floor(a.X)), int(Floor(a.Y))}
}
// ToPointCeil returns the vector as an [image.Point] with all values [Ceil]ed.
func (a Vector2) ToPointCeil() image.Point {
return image.Point{int(Ceil(a.X)), int(Ceil(a.Y))}
}
// ToPointRound returns the vector as an [image.Point] with all values [Round]ed.
func (a Vector2) ToPointRound() image.Point {
return image.Point{int(Round(a.X)), int(Round(a.Y))}
}
// ToFixed returns the vector as a [fixed.Point26_6].
func (a Vector2) ToFixed() fixed.Point26_6 {
return ToFixedPoint(a.X, a.Y)
}
// RectFromPosSizeMax returns an [image.Rectangle] from the floor of pos
// and ceil of size.
func RectFromPosSizeMax(pos, size Vector2) image.Rectangle {
tp := pos.ToPointFloor()
ts := size.ToPointCeil()
return image.Rect(tp.X, tp.Y, tp.X+ts.X, tp.Y+ts.Y)
}
// RectFromPosSizeMin returns an [image.Rectangle] from the ceil of pos
// and floor of size.
func RectFromPosSizeMin(pos, size Vector2) image.Rectangle {
tp := pos.ToPointCeil()
ts := size.ToPointFloor()
return image.Rect(tp.X, tp.Y, tp.X+ts.X, tp.Y+ts.Y)
}
// SetZero sets all of the vector's components to zero.
func (v *Vector2) SetZero() {
v.SetScalar(0)
}
// FromSlice sets this vector's components from the given slice, starting at offset.
func (v *Vector2) FromSlice(slice []float32, offset int) {
v.X = slice[offset]
v.Y = slice[offset+1]
}
// ToSlice copies this vector's components to the given slice, starting at offset.
func (v Vector2) ToSlice(slice []float32, offset int) {
slice[offset] = v.X
slice[offset+1] = v.Y
}
// Basic math operations:
// Add adds the other given vector to this one and returns the result as a new vector.
func (v Vector2) Add(other Vector2) Vector2 {
return Vec2(v.X+other.X, v.Y+other.Y)
}
// AddScalar adds scalar s to each component of this vector and returns new vector.
func (v Vector2) AddScalar(s float32) Vector2 {
return Vec2(v.X+s, v.Y+s)
}
// SetAdd sets this to addition with other vector (i.e., += or plus-equals).
func (v *Vector2) SetAdd(other Vector2) {
v.X += other.X
v.Y += other.Y
}
// SetAddScalar sets this to addition with scalar.
func (v *Vector2) SetAddScalar(s float32) {
v.X += s
v.Y += s
}
// Sub subtracts other vector from this one and returns result in new vector.
func (v Vector2) Sub(other Vector2) Vector2 {
return Vec2(v.X-other.X, v.Y-other.Y)
}
// SubScalar subtracts scalar s from each component of this vector and returns new vector.
func (v Vector2) SubScalar(s float32) Vector2 {
return Vec2(v.X-s, v.Y-s)
}
// SetSub sets this to subtraction with other vector (i.e., -= or minus-equals).
func (v *Vector2) SetSub(other Vector2) {
v.X -= other.X
v.Y -= other.Y
}
// SetSubScalar sets this to subtraction of scalar.
func (v *Vector2) SetSubScalar(s float32) {
v.X -= s
v.Y -= s
}
// Mul multiplies each component of this vector by the corresponding one from other
// and returns resulting vector.
func (v Vector2) Mul(other Vector2) Vector2 {
return Vec2(v.X*other.X, v.Y*other.Y)
}
// MulScalar multiplies each component of this vector by the scalar s and returns resulting vector.
func (v Vector2) MulScalar(s float32) Vector2 {
return Vec2(v.X*s, v.Y*s)
}
// SetMul sets this to multiplication with other vector (i.e., *= or times-equals).
func (v *Vector2) SetMul(other Vector2) {
v.X *= other.X
v.Y *= other.Y
}
// SetMulScalar sets this to multiplication by scalar.
func (v *Vector2) SetMulScalar(s float32) {
v.X *= s
v.Y *= s
}
// Div divides each component of this vector by the corresponding one from other vector
// and returns resulting vector.
func (v Vector2) Div(other Vector2) Vector2 {
return Vec2(v.X/other.X, v.Y/other.Y)
}
// DivScalar divides each component of this vector by the scalar s and returns resulting vector.
// If scalar is zero, returns zero.
func (v Vector2) DivScalar(scalar float32) Vector2 {
if scalar != 0 {
return v.MulScalar(1 / scalar)
} else {
return Vector2{}
}
}
// SetDiv sets this to division by other vector (i.e., /= or divide-equals).
func (v *Vector2) SetDiv(other Vector2) {
v.X /= other.X
v.Y /= other.Y
}
// SetDivScalar sets this to division by scalar.
func (v *Vector2) SetDivScalar(scalar float32) {
if scalar != 0 {
v.SetMulScalar(1 / scalar)
} else {
v.SetZero()
}
}
// Abs returns the vector with [Abs] applied to each component.
func (v Vector2) Abs() Vector2 {
return Vec2(Abs(v.X), Abs(v.Y))
}
// Min returns min of this vector components vs. other vector.
func (v Vector2) Min(other Vector2) Vector2 {
return Vec2(Min(v.X, other.X), Min(v.Y, other.Y))
}
// SetMin sets this vector components to the minimum values of itself and other vector.
func (v *Vector2) SetMin(other Vector2) {
v.X = Min(v.X, other.X)
v.Y = Min(v.Y, other.Y)
}
// Max returns max of this vector components vs. other vector.
func (v Vector2) Max(other Vector2) Vector2 {
return Vec2(Max(v.X, other.X), Max(v.Y, other.Y))
}
// SetMax sets this vector components to the maximum value of itself and other vector.
func (v *Vector2) SetMax(other Vector2) {
v.X = Max(v.X, other.X)
v.Y = Max(v.Y, other.Y)
}
// Clamp sets this vector's components to be no less than the corresponding
// components of min and not greater than the corresponding component of max.
// Assumes min < max; if this assumption isn't true, it will not operate correctly.
func (v *Vector2) Clamp(min, max Vector2) {
if v.X < min.X {
v.X = min.X
} else if v.X > max.X {
v.X = max.X
}
if v.Y < min.Y {
v.Y = min.Y
} else if v.Y > max.Y {
v.Y = max.Y
}
}
// Floor returns this vector with [Floor] applied to each of its components.
func (v Vector2) Floor() Vector2 {
return Vec2(Floor(v.X), Floor(v.Y))
}
// Ceil returns this vector with [Ceil] applied to each of its components.
func (v Vector2) Ceil() Vector2 {
return Vec2(Ceil(v.X), Ceil(v.Y))
}
// Round returns this vector with [Round] applied to each of its components.
func (v Vector2) Round() Vector2 {
return Vec2(Round(v.X), Round(v.Y))
}
// Negate returns the vector with each component negated.
func (v Vector2) Negate() Vector2 {
return Vec2(-v.X, -v.Y)
}
// AddDim returns the vector with the given value added on the given dimension.
func (a Vector2) AddDim(d Dims, value float32) Vector2 {
switch d {
case X:
a.X += value
case Y:
a.Y += value
}
return a
}
// SubDim returns the vector with the given value subtracted on the given dimension.
func (a Vector2) SubDim(d Dims, value float32) Vector2 {
switch d {
case X:
a.X -= value
case Y:
a.Y -= value
}
return a
}
// MulDim returns the vector with the given value multiplied by on the given dimension.
func (a Vector2) MulDim(d Dims, value float32) Vector2 {
switch d {
case X:
a.X *= value
case Y:
a.Y *= value
}
return a
}
// DivDim returns the vector with the given value divided by on the given dimension.
func (a Vector2) DivDim(d Dims, value float32) Vector2 {
switch d {
case X:
a.X /= value
case Y:
a.Y /= value
}
return a
}
// Distance, Normal:
// Dot returns the dot product of this vector with the given other vector.
func (v Vector2) Dot(other Vector2) float32 {
return v.X*other.X + v.Y*other.Y
}
// Length returns the length (magnitude) of this vector.
func (v Vector2) Length() float32 {
return Sqrt(v.LengthSquared())
}
// LengthSquared returns the length squared of this vector.
// LengthSquared can be used to compare the lengths of vectors
// without the need to perform a square root.
func (v Vector2) LengthSquared() float32 {
return v.X*v.X + v.Y*v.Y
}
// Normal returns this vector divided by its length (its unit vector).
func (v Vector2) Normal() Vector2 {
return v.DivScalar(v.Length())
}
// DistanceTo returns the distance between these two vectors as points.
func (v Vector2) DistanceTo(other Vector2) float32 {
return Sqrt(v.DistanceToSquared(other))
}
// DistanceToSquared returns the squared distance between these two vectors as points.
func (v Vector2) DistanceToSquared(other Vector2) float32 {
dx := v.X - other.X
dy := v.Y - other.Y
return dx*dx + dy*dy
}
// Cross returns the cross product of this vector with other.
func (v Vector2) Cross(other Vector2) float32 {
return v.X*other.Y - v.Y*other.X
}
// CosTo returns the cosine (normalized dot product) between this vector and other.
func (v Vector2) CosTo(other Vector2) float32 {
return v.Dot(other) / (v.Length() * other.Length())
}
// AngleTo returns the angle between this vector and other.
// Returns angles in range of -PI to PI (not 0 to 2 PI).
func (v Vector2) AngleTo(other Vector2) float32 {
ang := Acos(Clamp(v.CosTo(other), -1, 1))
cross := v.Cross(other)
if cross > 0 {
ang = -ang
}
return ang
}
// Lerp returns vector with each components as the linear interpolated value of
// alpha between itself and the corresponding other component.
func (v Vector2) Lerp(other Vector2, alpha float32) Vector2 {
return Vec2(v.X+(other.X-v.X)*alpha, v.Y+(other.Y-v.Y)*alpha)
}
// InTriangle returns whether the vector is inside the specified triangle.
func (v Vector2) InTriangle(p0, p1, p2 Vector2) bool {
A := 0.5 * (-p1.Y*p2.X + p0.Y*(-p1.X+p2.X) + p0.X*(p1.Y-p2.Y) + p1.X*p2.Y)
sign := float32(1)
if A < 0 {
sign = float32(-1)
}
s := (p0.Y*p2.X - p0.X*p2.Y + (p2.Y-p0.Y)*v.X + (p0.X-p2.X)*v.Y) * sign
t := (p0.X*p1.Y - p0.Y*p1.X + (p0.Y-p1.Y)*v.X + (p1.X-p0.X)*v.Y) * sign
return s >= 0 && t >= 0 && (s+t) < 2*A*sign
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
// Vector2i is a 2D vector/point with X and Y int32 components.
type Vector2i struct {
X int32
Y int32
}
// Vec2i returns a new [Vector2i] with the given x and y components.
func Vec2i(x, y int32) Vector2i {
return Vector2i{X: x, Y: y}
}
// Vector2iScalar returns a new [Vector2i] with all components set to the given scalar value.
func Vector2iScalar(scalar int32) Vector2i {
return Vector2i{X: scalar, Y: scalar}
}
// Set sets this vector X and Y components.
func (v *Vector2i) Set(x, y int32) {
v.X = x
v.Y = y
}
// SetScalar sets all vector components to the same scalar value.
func (v *Vector2i) SetScalar(scalar int32) {
v.X = scalar
v.Y = scalar
}
// SetFromVector2 sets from a [Vector2] (float32) vector.
func (v *Vector2i) SetFromVector2(vf Vector2) {
v.X = int32(vf.X)
v.Y = int32(vf.Y)
}
// SetDim sets the given vector component value by its dimension index.
func (v *Vector2i) SetDim(dim Dims, value int32) {
switch dim {
case X:
v.X = value
case Y:
v.Y = value
default:
panic("dim is out of range")
}
}
// Dim returns the given vector component.
func (v Vector2i) Dim(dim Dims) int32 {
switch dim {
case X:
return v.X
case Y:
return v.Y
default:
panic("dim is out of range")
}
}
// SetZero sets all of the vector's components to zero.
func (v *Vector2i) SetZero() {
v.SetScalar(0)
}
// FromSlice sets this vector's components from the given slice, starting at offset.
func (v *Vector2i) FromSlice(array []int32, offset int) {
v.X = array[offset]
v.Y = array[offset+1]
}
// ToSlice copies this vector's components to the given slice, starting at offset.
func (v Vector2i) ToSlice(array []int32, offset int) {
array[offset] = v.X
array[offset+1] = v.Y
}
// Basic math operations:
// Add adds the other given vector to this one and returns the result as a new vector.
func (v Vector2i) Add(other Vector2i) Vector2i {
return Vector2i{v.X + other.X, v.Y + other.Y}
}
// AddScalar adds scalar s to each component of this vector and returns new vector.
func (v Vector2i) AddScalar(s int32) Vector2i {
return Vector2i{v.X + s, v.Y + s}
}
// SetAdd sets this to addition with other vector (i.e., += or plus-equals).
func (v *Vector2i) SetAdd(other Vector2i) {
v.X += other.X
v.Y += other.Y
}
// SetAddScalar sets this to addition with scalar.
func (v *Vector2i) SetAddScalar(s int32) {
v.X += s
v.Y += s
}
// Sub subtracts other vector from this one and returns result in new vector.
func (v Vector2i) Sub(other Vector2i) Vector2i {
return Vector2i{v.X - other.X, v.Y - other.Y}
}
// SubScalar subtracts scalar s from each component of this vector and returns new vector.
func (v Vector2i) SubScalar(s int32) Vector2i {
return Vector2i{v.X - s, v.Y - s}
}
// SetSub sets this to subtraction with other vector (i.e., -= or minus-equals).
func (v *Vector2i) SetSub(other Vector2i) {
v.X -= other.X
v.Y -= other.Y
}
// SetSubScalar sets this to subtraction of scalar.
func (v *Vector2i) SetSubScalar(s int32) {
v.X -= s
v.Y -= s
}
// Mul multiplies each component of this vector by the corresponding one from other
// and returns resulting vector.
func (v Vector2i) Mul(other Vector2i) Vector2i {
return Vector2i{v.X * other.X, v.Y * other.Y}
}
// MulScalar multiplies each component of this vector by the scalar s and returns resulting vector.
func (v Vector2i) MulScalar(s int32) Vector2i {
return Vector2i{v.X * s, v.Y * s}
}
// SetMul sets this to multiplication with other vector (i.e., *= or times-equals).
func (v *Vector2i) SetMul(other Vector2i) {
v.X *= other.X
v.Y *= other.Y
}
// SetMulScalar sets this to multiplication by scalar.
func (v *Vector2i) SetMulScalar(s int32) {
v.X *= s
v.Y *= s
}
// Div divides each component of this vector by the corresponding one from other vector
// and returns resulting vector.
func (v Vector2i) Div(other Vector2i) Vector2i {
return Vector2i{v.X / other.X, v.Y / other.Y}
}
// DivScalar divides each component of this vector by the scalar s and returns resulting vector.
// If scalar is zero, returns zero.
func (v Vector2i) DivScalar(scalar int32) Vector2i {
if scalar != 0 {
return Vector2i{v.X / scalar, v.Y / scalar}
} else {
return Vector2i{}
}
}
// SetDiv sets this to division by other vector (i.e., /= or divide-equals).
func (v *Vector2i) SetDiv(other Vector2i) {
v.X /= other.X
v.Y /= other.Y
}
// SetDivScalar sets this to division by scalar.
func (v *Vector2i) SetDivScalar(scalar int32) {
if scalar != 0 {
v.X /= scalar
v.Y /= scalar
} else {
v.SetZero()
}
}
// Min returns min of this vector components vs. other vector.
func (v Vector2i) Min(other Vector2i) Vector2i {
return Vector2i{min(v.X, other.X), min(v.Y, other.Y)}
}
// SetMin sets this vector components to the minimum values of itself and other vector.
func (v *Vector2i) SetMin(other Vector2i) {
v.X = min(v.X, other.X)
v.Y = min(v.Y, other.Y)
}
// Max returns max of this vector components vs. other vector.
func (v Vector2i) Max(other Vector2i) Vector2i {
return Vector2i{max(v.X, other.X), max(v.Y, other.Y)}
}
// SetMax sets this vector components to the maximum value of itself and other vector.
func (v *Vector2i) SetMax(other Vector2i) {
v.X = max(v.X, other.X)
v.Y = max(v.Y, other.Y)
}
// Clamp sets this vector's components to be no less than the corresponding
// components of min and not greater than the corresponding component of max.
// Assumes min < max; if this assumption isn't true, it will not operate correctly.
func (v *Vector2i) Clamp(min, max Vector2i) {
if v.X < min.X {
v.X = min.X
} else if v.X > max.X {
v.X = max.X
}
if v.Y < min.Y {
v.Y = min.Y
} else if v.Y > max.Y {
v.Y = max.Y
}
}
// Negate returns the vector with each component negated.
func (v Vector2i) Negate() Vector2i {
return Vector2i{-v.X, -v.Y}
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
import "fmt"
// Vector3 is a 3D vector/point with X, Y and Z components.
type Vector3 struct {
X float32
Y float32
Z float32
}
// Vec3 returns a new [Vector3] with the given x, y and z components.
func Vec3(x, y, z float32) Vector3 {
return Vector3{x, y, z}
}
// Vector3Scalar returns a new [Vector3] with all components set to the given scalar value.
func Vector3Scalar(scalar float32) Vector3 {
return Vector3{scalar, scalar, scalar}
}
// Vector3FromVector4 returns a new [Vector3] from the given [Vector4].
func Vector3FromVector4(v Vector4) Vector3 {
nv := Vector3{}
nv.SetFromVector4(v)
return nv
}
// Set sets this vector X, Y and Z components.
func (v *Vector3) Set(x, y, z float32) {
v.X = x
v.Y = y
v.Z = z
}
// SetScalar sets all vector components to the same scalar value.
func (v *Vector3) SetScalar(scalar float32) {
v.X = scalar
v.Y = scalar
v.Z = scalar
}
// SetFromVector4 sets this vector from a Vector4
func (v *Vector3) SetFromVector4(other Vector4) {
v.X = other.X
v.Y = other.Y
v.Z = other.Z
}
// SetFromVector3i sets from a Vector3i (int32) vector.
func (v *Vector3) SetFromVector3i(vi Vector3i) {
v.X = float32(vi.X)
v.Y = float32(vi.Y)
v.Z = float32(vi.Z)
}
// SetDim sets this vector component value by dimension index.
func (v *Vector3) SetDim(dim Dims, value float32) {
switch dim {
case X:
v.X = value
case Y:
v.Y = value
case Z:
v.Z = value
default:
panic("dim is out of range: ")
}
}
// Dim returns this vector component
func (v Vector3) Dim(dim Dims) float32 {
switch dim {
case X:
return v.X
case Y:
return v.Y
case Z:
return v.Z
default:
panic("dim is out of range")
}
}
func (a Vector3) String() string {
return fmt.Sprintf("(%v, %v, %v)", a.X, a.Y, a.Z)
}
// GenGoSet returns code to set values in object at given path (var.member etc).
func (v *Vector3) GenGoSet(path string) string {
return fmt.Sprintf("%s.Set(%g, %g, %g)", path, v.X, v.Y, v.Z)
}
// SetZero sets all of the vector's components to zero.
func (v *Vector3) SetZero() {
v.SetScalar(0)
}
// FromSlice sets this vector's components from the given slice, starting at offset.
func (v *Vector3) FromSlice(array []float32, offset int) {
v.X = array[offset]
v.Y = array[offset+1]
v.Z = array[offset+2]
}
// ToSlice copies this vector's components to the given slice, starting at offset.
func (v Vector3) ToSlice(array []float32, offset int) {
array[offset] = v.X
array[offset+1] = v.Y
array[offset+2] = v.Z
}
// Basic math operations:
// Add adds the other given vector to this one and returns the result as a new vector.
func (v Vector3) Add(other Vector3) Vector3 {
return Vec3(v.X+other.X, v.Y+other.Y, v.Z+other.Z)
}
// AddScalar adds scalar s to each component of this vector and returns new vector.
func (v Vector3) AddScalar(s float32) Vector3 {
return Vec3(v.X+s, v.Y+s, v.Z+s)
}
// SetAdd sets this to addition with other vector (i.e., += or plus-equals).
func (v *Vector3) SetAdd(other Vector3) {
v.X += other.X
v.Y += other.Y
v.Z += other.Z
}
// SetAddScalar sets this to addition with scalar.
func (v *Vector3) SetAddScalar(s float32) {
v.X += s
v.Y += s
v.Z += s
}
// Sub subtracts other vector from this one and returns result in new vector.
func (v Vector3) Sub(other Vector3) Vector3 {
return Vec3(v.X-other.X, v.Y-other.Y, v.Z-other.Z)
}
// SubScalar subtracts scalar s from each component of this vector and returns new vector.
func (v Vector3) SubScalar(s float32) Vector3 {
return Vec3(v.X-s, v.Y-s, v.Z-s)
}
// SetSub sets this to subtraction with other vector (i.e., -= or minus-equals).
func (v *Vector3) SetSub(other Vector3) {
v.X -= other.X
v.Y -= other.Y
v.Z -= other.Z
}
// SetSubScalar sets this to subtraction of scalar.
func (v *Vector3) SetSubScalar(s float32) {
v.X -= s
v.Y -= s
v.Z -= s
}
// Mul multiplies each component of this vector by the corresponding one from other
// and returns resulting vector.
func (v Vector3) Mul(other Vector3) Vector3 {
return Vec3(v.X*other.X, v.Y*other.Y, v.Z*other.Z)
}
// MulScalar multiplies each component of this vector by the scalar s and returns resulting vector.
func (v Vector3) MulScalar(s float32) Vector3 {
return Vec3(v.X*s, v.Y*s, v.Z*s)
}
// SetMul sets this to multiplication with other vector (i.e., *= or times-equals).
func (v *Vector3) SetMul(other Vector3) {
v.X *= other.X
v.Y *= other.Y
v.Z *= other.Z
}
// SetMulScalar sets this to multiplication by scalar.
func (v *Vector3) SetMulScalar(s float32) {
v.X *= s
v.Y *= s
v.Z *= s
}
// Div divides each component of this vector by the corresponding one from other vector
// and returns resulting vector.
func (v Vector3) Div(other Vector3) Vector3 {
return Vec3(v.X/other.X, v.Y/other.Y, v.Z/other.Z)
}
// DivScalar divides each component of this vector by the scalar s and returns resulting vector.
// If scalar is zero, returns zero.
func (v Vector3) DivScalar(scalar float32) Vector3 {
if scalar != 0 {
return v.MulScalar(1 / scalar)
} else {
return Vector3{}
}
}
// SetDiv sets this to division by other vector (i.e., /= or divide-equals).
func (v *Vector3) SetDiv(other Vector3) {
v.X /= other.X
v.Y /= other.Y
v.Z /= other.Z
}
// SetDivScalar sets this to division by scalar.
func (v *Vector3) SetDivScalar(scalar float32) {
if scalar != 0 {
v.SetMulScalar(1 / scalar)
} else {
v.SetZero()
}
}
// Min returns min of this vector components vs. other vector.
func (v Vector3) Min(other Vector3) Vector3 {
return Vec3(Min(v.X, other.X), Min(v.Y, other.Y), Min(v.Z, other.Z))
}
// SetMin sets this vector components to the minimum values of itself and other vector.
func (v *Vector3) SetMin(other Vector3) {
v.X = Min(v.X, other.X)
v.Y = Min(v.Y, other.Y)
v.Z = Min(v.Z, other.Z)
}
// Max returns max of this vector components vs. other vector.
func (v Vector3) Max(other Vector3) Vector3 {
return Vec3(Max(v.X, other.X), Max(v.Y, other.Y), Max(v.Z, other.Z))
}
// SetMax sets this vector components to the maximum value of itself and other vector.
func (v *Vector3) SetMax(other Vector3) {
v.X = Max(v.X, other.X)
v.Y = Max(v.Y, other.Y)
v.Z = Max(v.Z, other.Z)
}
// Clamp sets this vector's components to be no less than the corresponding
// components of min and not greater than the corresponding component of max.
// Assumes min < max; if this assumption isn't true, it will not operate correctly.
func (v *Vector3) Clamp(min, max Vector3) {
if v.X < min.X {
v.X = min.X
} else if v.X > max.X {
v.X = max.X
}
if v.Y < min.Y {
v.Y = min.Y
} else if v.Y > max.Y {
v.Y = max.Y
}
if v.Z < min.Z {
v.Z = min.Z
} else if v.Z > max.Z {
v.Z = max.Z
}
}
// Floor returns this vector with [Floor] applied to each of its components.
func (v Vector3) Floor() Vector3 {
return Vec3(Floor(v.X), Floor(v.Y), Floor(v.Z))
}
// Ceil returns this vector with [Ceil] applied to each of its components.
func (v Vector3) Ceil() Vector3 {
return Vec3(Ceil(v.X), Ceil(v.Y), Ceil(v.Z))
}
// Round returns this vector with [Round] applied to each of its components.
func (v Vector3) Round() Vector3 {
return Vec3(Round(v.X), Round(v.Y), Round(v.Z))
}
// Negate returns the vector with each component negated.
func (v Vector3) Negate() Vector3 {
return Vec3(-v.X, -v.Y, -v.Z)
}
// Abs returns the vector with [Abs] applied to each component.
func (v Vector3) Abs() Vector3 {
return Vec3(Abs(v.X), Abs(v.Y), Abs(v.Z))
}
// Distance, Normal:
// Dot returns the dot product of this vector with the given other vector.
func (v Vector3) Dot(other Vector3) float32 {
return v.X*other.X + v.Y*other.Y + v.Z*other.Z
}
// Length returns the length (magnitude) of this vector.
func (v Vector3) Length() float32 {
return Sqrt(v.X*v.X + v.Y*v.Y + v.Z*v.Z)
}
// LengthSquared returns the length squared of this vector.
// LengthSquared can be used to compare the lengths of vectors
// without the need to perform a square root.
func (v Vector3) LengthSquared() float32 {
return v.X*v.X + v.Y*v.Y + v.Z*v.Z
}
// Normal returns this vector divided by its length (its unit vector).
func (v Vector3) Normal() Vector3 {
return v.DivScalar(v.Length())
}
// SetNormal normalizes this vector so its length will be 1.
func (v *Vector3) SetNormal() {
v.SetDivScalar(v.Length())
}
// DistanceTo returns the distance between these two vectors as points.
func (v Vector3) DistanceTo(other Vector3) float32 {
return Sqrt(v.DistanceToSquared(other))
}
// DistanceToSquared returns the squared distance between these two vectors as points.
func (v Vector3) DistanceToSquared(other Vector3) float32 {
dx := v.X - other.X
dy := v.Y - other.Y
dz := v.Z - other.Z
return dx*dx + dy*dy + dz*dz
}
// Lerp returns vector with each components as the linear interpolated value of
// alpha between itself and the corresponding other component.
func (v Vector3) Lerp(other Vector3, alpha float32) Vector3 {
return Vec3(v.X+(other.X-v.X)*alpha, v.Y+(other.Y-v.Y)*alpha, v.Z+(other.Z-v.Z)*alpha)
}
// Matrix operations:
// MulMatrix3 returns the vector multiplied by the given 3x3 matrix.
func (v Vector3) MulMatrix3(m *Matrix3) Vector3 {
return Vector3{m[0]*v.X + m[3]*v.Y + m[6]*v.Z,
m[1]*v.X + m[4]*v.Y + m[7]*v.Z,
m[2]*v.X + m[5]*v.Y + m[8]*v.Z}
}
// MulMatrix4 returns the vector multiplied by the given 4x4 matrix.
func (v Vector3) MulMatrix4(m *Matrix4) Vector3 {
return Vector3{m[0]*v.X + m[4]*v.Y + m[8]*v.Z + m[12],
m[1]*v.X + m[5]*v.Y + m[9]*v.Z + m[13],
m[2]*v.X + m[6]*v.Y + m[10]*v.Z + m[14]}
}
// MulMatrix4AsVector4 returns 3-dim vector multiplied by specified 4x4 matrix
// using a 4-dim vector with given 4th dimensional value, then reduced back to
// a 3-dimensional vector. This is somehow different from just straight
// MulMatrix4 on the 3-dim vector. Use 0 for normals and 1 for positions
// as the 4th dim to set.
func (v Vector3) MulMatrix4AsVector4(m *Matrix4, w float32) Vector3 {
return Vector3FromVector4(Vector4FromVector3(v, w).MulMatrix4(m))
}
// NDCToWindow converts normalized display coordinates (NDC) to window
// (pixel) coordinates, using given window size parameters.
// near, far are 0, 1 by default (glDepthRange defaults).
// flipY if true means flip the Y axis (top = 0 for windows vs. bottom = 0 for 3D coords)
func (v Vector3) NDCToWindow(size, off Vector2, near, far float32, flipY bool) Vector3 {
w := Vector3{}
half := size.MulScalar(0.5)
w.X = half.X*v.X + half.X
w.Y = half.Y*v.Y + half.Y
w.Z = 0.5*(far-near)*v.Z + 0.5*(far+near)
if flipY {
w.Y = size.Y - w.Y
}
w.X += off.X
w.Y += off.Y
return w
}
// WindowToNDC converts window (pixel) coordinates to
// normalized display coordinates (NDC), using given window size parameters.
// The Z depth coordinate (0-1) must be set manually or by reading from framebuffer
// flipY if true means flip the Y axis (top = 0 for windows vs. bottom = 0 for 3D coords)
func (v Vector2) WindowToNDC(size, off Vector2, flipY bool) Vector3 {
n := Vector3{}
half := size.MulScalar(0.5)
n.X = v.X - off.X
n.Y = v.Y - off.Y
if flipY {
n.Y = size.Y - n.Y
}
n.X = n.X/half.X - 1
n.Y = n.Y/half.Y - 1
return n
}
// MulProjection returns vector multiplied by the projection matrix m.
func (v Vector3) MulProjection(m *Matrix4) Vector3 {
d := 1 / (m[3]*v.X + m[7]*v.Y + m[11]*v.Z + m[15]) // perspective divide
return Vector3{(m[0]*v.X + m[4]*v.Y + m[8]*v.Z + m[12]) * d,
(m[1]*v.X + m[5]*v.Y + m[9]*v.Z + m[13]) * d,
(m[2]*v.X + m[6]*v.Y + m[10]*v.Z + m[14]) * d}
}
// MulQuat returns vector multiplied by specified quaternion and
// then by the quaternion inverse.
// It basically applies the rotation encoded in the quaternion to this vector.
func (v Vector3) MulQuat(q Quat) Vector3 {
qx := q.X
qy := q.Y
qz := q.Z
qw := q.W
// calculate quat * vector
ix := qw*v.X + qy*v.Z - qz*v.Y
iy := qw*v.Y + qz*v.X - qx*v.Z
iz := qw*v.Z + qx*v.Y - qy*v.X
iw := -qx*v.X - qy*v.Y - qz*v.Z
// calculate result * inverse quat
return Vector3{ix*qw + iw*-qx + iy*-qz - iz*-qy,
iy*qw + iw*-qy + iz*-qx - ix*-qz,
iz*qw + iw*-qz + ix*-qy - iy*-qx}
}
// Cross returns the cross product of this vector with other.
func (v Vector3) Cross(other Vector3) Vector3 {
return Vec3(v.Y*other.Z-v.Z*other.Y, v.Z*other.X-v.X*other.Z, v.X*other.Y-v.Y*other.X)
}
// ProjectOnVector returns vector projected on other vector.
func (v *Vector3) ProjectOnVector(other Vector3) Vector3 {
on := other.Normal()
return on.MulScalar(v.Dot(on))
}
// ProjectOnPlane returns vector projected on the plane specified by normal vector.
func (v *Vector3) ProjectOnPlane(planeNormal Vector3) Vector3 {
return v.Sub(v.ProjectOnVector(planeNormal))
}
// Reflect returns vector reflected relative to the normal vector (assumed to be
// already normalized).
func (v *Vector3) Reflect(normal Vector3) Vector3 {
return v.Sub(normal.MulScalar(2 * v.Dot(normal)))
}
// CosTo returns the cosine (normalized dot product) between this vector and other.
func (v Vector3) CosTo(other Vector3) float32 {
return v.Dot(other) / (v.Length() * other.Length())
}
// AngleTo returns the angle between this vector and other.
// Returns angles in range of -PI to PI (not 0 to 2 PI).
func (v Vector3) AngleTo(other Vector3) float32 {
ang := Acos(Clamp(v.CosTo(other), -1, 1))
cross := v.Cross(other)
switch {
case Abs(cross.Z) >= Abs(cross.Y) && Abs(cross.Z) >= Abs(cross.X):
if cross.Z > 0 {
ang = -ang
}
case Abs(cross.Y) >= Abs(cross.Z) && Abs(cross.Y) >= Abs(cross.X):
if cross.Y > 0 {
ang = -ang
}
case Abs(cross.X) >= Abs(cross.Z) && Abs(cross.X) >= Abs(cross.Y):
if cross.X > 0 {
ang = -ang
}
}
return ang
}
// SetFromMatrixPos set this vector from the translation coordinates
// in the specified transformation matrix.
func (v *Vector3) SetFromMatrixPos(m *Matrix4) {
v.X = m[12]
v.Y = m[13]
v.Z = m[14]
}
// SetEulerAnglesFromMatrix sets this vector components to the Euler angles
// from the specified pure rotation matrix.
func (v *Vector3) SetEulerAnglesFromMatrix(m *Matrix4) {
m11 := m[0]
m12 := m[4]
m13 := m[8]
m22 := m[5]
m23 := m[9]
m32 := m[6]
m33 := m[10]
v.Y = Asin(Clamp(m13, -1, 1))
if Abs(m13) < 0.99999 {
v.X = Atan2(-m23, m33)
v.Z = Atan2(-m12, m11)
} else {
v.X = Atan2(m32, m22)
v.Z = 0
}
}
// NewEulerAnglesFromMatrix returns a Vector3 with components as the Euler angles
// from the specified pure rotation matrix.
func NewEulerAnglesFromMatrix(m *Matrix4) Vector3 {
rot := Vector3{}
rot.SetEulerAnglesFromMatrix(m)
return rot
}
// SetEulerAnglesFromQuat sets this vector components to the Euler angles
// from the specified quaternion.
func (v *Vector3) SetEulerAnglesFromQuat(q Quat) {
mat := Identity4()
mat.SetRotationFromQuat(q)
v.SetEulerAnglesFromMatrix(mat)
}
// RandomTangents computes and returns two arbitrary tangents to the vector.
func (v *Vector3) RandomTangents() (Vector3, Vector3) {
t1 := Vector3{}
t2 := Vector3{}
length := v.Length()
if length > 0 {
n := v.Normal()
randVec := Vector3{}
if Abs(n.X) < 0.9 {
randVec.X = 1
t1 = n.Cross(randVec)
} else if Abs(n.Y) < 0.9 {
randVec.Y = 1
t1 = n.Cross(randVec)
} else {
randVec.Z = 1
t1 = n.Cross(randVec)
}
t2 = n.Cross(t1)
} else {
t1.X = 1
t2.Y = 1
}
return t1, t2
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
// Vector3i is a 3D vector/point with X, Y and Z int32 components.
type Vector3i struct {
X int32
Y int32
Z int32
}
// Vec3i returns a new [Vector3i] with the given x, y and y components.
func Vec3i(x, y, z int32) Vector3i {
return Vector3i{X: x, Y: y, Z: z}
}
// Vector3iScalar returns a new [Vector3i] with all components set to the given scalar value.
func Vector3iScalar(scalar int32) Vector3i {
return Vector3i{X: scalar, Y: scalar, Z: scalar}
}
// Set sets this vector X, Y and Z components.
func (v *Vector3i) Set(x, y, z int32) {
v.X = x
v.Y = y
v.Z = z
}
// SetScalar sets all vector components to the same scalar value.
func (v *Vector3i) SetScalar(scalar int32) {
v.X = scalar
v.Y = scalar
v.Z = scalar
}
// SetFromVector3 sets from a Vector3 (float32) vector.
func (v *Vector3i) SetFromVector3(vf Vector3) {
v.X = int32(vf.X)
v.Y = int32(vf.Y)
v.Z = int32(vf.Z)
}
// SetDim sets the given vector component value by its dimension index.
func (v *Vector3i) SetDim(dim Dims, value int32) {
switch dim {
case X:
v.X = value
case Y:
v.Y = value
case Z:
v.Z = value
default:
panic("dim is out of range: ")
}
}
// Dim returns the given vector component.
func (v Vector3i) Dim(dim Dims) int32 {
switch dim {
case X:
return v.X
case Y:
return v.Y
case Z:
return v.Z
default:
panic("dim is out of range")
}
}
// SetZero sets all of the vector's components to zero.
func (v *Vector3i) SetZero() {
v.SetScalar(0)
}
// FromSlice sets this vector's components from the given slice, starting at offset.
func (v *Vector3i) FromSlice(array []int32, offset int) {
v.X = array[offset]
v.Y = array[offset+1]
v.Z = array[offset+2]
}
// ToSlice copies this vector's components to the given slice, starting at offset.
func (v Vector3i) ToSlice(array []int32, offset int) {
array[offset] = v.X
array[offset+1] = v.Y
array[offset+2] = v.Z
}
// Basic math operations:
// Add adds the other given vector to this one and returns the result as a new vector.
func (v Vector3i) Add(other Vector3i) Vector3i {
return Vector3i{v.X + other.X, v.Y + other.Y, v.Z + other.Z}
}
// AddScalar adds scalar s to each component of this vector and returns new vector.
func (v Vector3i) AddScalar(s int32) Vector3i {
return Vector3i{v.X + s, v.Y + s, v.Z + s}
}
// SetAdd sets this to addition with other vector (i.e., += or plus-equals).
func (v *Vector3i) SetAdd(other Vector3i) {
v.X += other.X
v.Y += other.Y
v.Z += other.Z
}
// SetAddScalar sets this to addition with scalar.
func (v *Vector3i) SetAddScalar(s int32) {
v.X += s
v.Y += s
v.Z += s
}
// Sub subtracts other vector from this one and returns result in new vector.
func (v Vector3i) Sub(other Vector3i) Vector3i {
return Vector3i{v.X - other.X, v.Y - other.Y, v.Z - other.Z}
}
// SubScalar subtracts scalar s from each component of this vector and returns new vector.
func (v Vector3i) SubScalar(s int32) Vector3i {
return Vector3i{v.X - s, v.Y - s, v.Z - s}
}
// SetSub sets this to subtraction with other vector (i.e., -= or minus-equals).
func (v *Vector3i) SetSub(other Vector3i) {
v.X -= other.X
v.Y -= other.Y
v.Z -= other.Z
}
// SetSubScalar sets this to subtraction of scalar.
func (v *Vector3i) SetSubScalar(s int32) {
v.X -= s
v.Y -= s
v.Z -= s
}
// Mul multiplies each component of this vector by the corresponding one from other
// and returns resulting vector.
func (v Vector3i) Mul(other Vector3i) Vector3i {
return Vector3i{v.X * other.X, v.Y * other.Y, v.Z * other.Z}
}
// MulScalar multiplies each component of this vector by the scalar s and returns resulting vector.
func (v Vector3i) MulScalar(s int32) Vector3i {
return Vector3i{v.X * s, v.Y * s, v.Z * s}
}
// SetMul sets this to multiplication with other vector (i.e., *= or times-equals).
func (v *Vector3i) SetMul(other Vector3i) {
v.X *= other.X
v.Y *= other.Y
v.Z *= other.Z
}
// SetMulScalar sets this to multiplication by scalar.
func (v *Vector3i) SetMulScalar(s int32) {
v.X *= s
v.Y *= s
v.Z *= s
}
// Div divides each component of this vector by the corresponding one from other vector
// and returns resulting vector.
func (v Vector3i) Div(other Vector3i) Vector3i {
return Vector3i{v.X / other.X, v.Y / other.Y, v.Z / other.Z}
}
// DivScalar divides each component of this vector by the scalar s and returns resulting vector.
// If scalar is zero, returns zero.
func (v Vector3i) DivScalar(scalar int32) Vector3i {
if scalar != 0 {
return Vector3i{v.X / scalar, v.Y / scalar, v.Z / scalar}
} else {
return Vector3i{}
}
}
// SetDiv sets this to division by other vector (i.e., /= or divide-equals).
func (v *Vector3i) SetDiv(other Vector3i) {
v.X /= other.X
v.Y /= other.Y
v.Z /= other.Z
}
// SetDivScalar sets this to division by scalar.
func (v *Vector3i) SetDivScalar(scalar int32) {
if scalar != 0 {
v.X /= scalar
v.Y /= scalar
v.Z /= scalar
} else {
v.SetZero()
}
}
// Min returns min of this vector components vs. other vector.
func (v Vector3i) Min(other Vector3i) Vector3i {
return Vector3i{min(v.X, other.X), min(v.Y, other.Y), min(v.Z, other.Z)}
}
// SetMin sets this vector components to the minimum values of itself and other vector.
func (v *Vector3i) SetMin(other Vector3i) {
v.X = min(v.X, other.X)
v.Y = min(v.Y, other.Y)
v.Z = min(v.Z, other.Z)
}
// Max returns max of this vector components vs. other vector.
func (v Vector3i) Max(other Vector3i) Vector3i {
return Vector3i{max(v.X, other.X), max(v.Y, other.Y), max(v.Z, other.Z)}
}
// SetMax sets this vector components to the maximum value of itself and other vector.
func (v *Vector3i) SetMax(other Vector3i) {
v.X = max(v.X, other.X)
v.Y = max(v.Y, other.Y)
v.Z = max(v.Z, other.Z)
}
// Clamp sets this vector's components to be no less than the corresponding
// components of min and not greater than the corresponding component of max.
// Assumes min < max; if this assumption isn't true, it will not operate correctly.
func (v *Vector3i) Clamp(min, max Vector3i) {
if v.X < min.X {
v.X = min.X
} else if v.X > max.X {
v.X = max.X
}
if v.Y < min.Y {
v.Y = min.Y
} else if v.Y > max.Y {
v.Y = max.Y
}
if v.Z < min.Z {
v.Z = min.Z
} else if v.Z > max.Z {
v.Z = max.Z
}
}
// Negate returns the vector with each component negated.
func (v Vector3i) Negate() Vector3i {
return Vector3i{-v.X, -v.Y, -v.Z}
}
// Copyright 2019 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.
// Initially copied from G3N: github.com/g3n/engine/math32
// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// with modifications needed to suit Cogent Core functionality.
package math32
import "fmt"
// Vector4 is a vector/point in homogeneous coordinates with X, Y, Z and W components.
type Vector4 struct {
X float32
Y float32
Z float32
W float32
}
// Vec4 returns a new [Vector4] with the given x, y, z, and w components.
func Vec4(x, y, z, w float32) Vector4 {
return Vector4{X: x, Y: y, Z: z, W: w}
}
// Vector4Scalar returns a new [Vector4] with all components set to the given scalar value.
func Vector4Scalar(scalar float32) Vector4 {
return Vector4{X: scalar, Y: scalar, Z: scalar, W: scalar}
}
// Vector4FromVector3 returns a new [Vector4] from the given [Vector3] and w component.
func Vector4FromVector3(v Vector3, w float32) Vector4 {
nv := Vector4{}
nv.SetFromVector3(v, w)
return nv
}
// Set sets this vector X, Y, Z and W components.
func (v *Vector4) Set(x, y, z, w float32) {
v.X = x
v.Y = y
v.Z = z
v.W = w
}
// SetScalar sets all vector components to the same scalar value.
func (v *Vector4) SetScalar(scalar float32) {
v.X = scalar
v.Y = scalar
v.Z = scalar
v.W = scalar
}
// SetFromVector3 sets this vector from a Vector3 and W
func (v *Vector4) SetFromVector3(other Vector3, w float32) {
v.X = other.X
v.Y = other.Y
v.Z = other.Z
v.W = w
}
// SetFromVector2 sets this vector from a Vector2 with 0,1 for Z,W
func (v *Vector4) SetFromVector2(other Vector2) {
v.X = other.X
v.Y = other.Y
v.Z = 0
v.W = 1
}
// SetDim sets this vector component value by dimension index.
func (v *Vector4) SetDim(dim Dims, value float32) {
switch dim {
case X:
v.X = value
case Y:
v.Y = value
case Z:
v.Z = value
case W:
v.W = value
default:
panic("dim is out of range")
}
}
// Dim returns this vector component.
func (v Vector4) Dim(dim Dims) float32 {
switch dim {
case X:
return v.X
case Y:
return v.Y
case Z:
return v.Z
case W:
return v.W
default:
panic("dim is out of range")
}
}
func (v Vector4) String() string {
return fmt.Sprintf("(%v, %v, %v, %v)", v.X, v.Y, v.Z, v.W)
}
// SetZero sets all of the vector's components to zero,
// except for the W component, which it sets to 1, as is standard.
func (v *Vector4) SetZero() {
v.X = 0
v.Y = 0
v.Z = 0
v.W = 1
}
// FromSlice sets this vector's components from the given slice, starting at offset.
func (v *Vector4) FromSlice(array []float32, offset int) {
v.X = array[offset]
v.Y = array[offset+1]
v.Z = array[offset+2]
v.W = array[offset+3]
}
// ToSlice copies this vector's components to the given slice, starting at offset.
func (v Vector4) ToSlice(array []float32, offset int) {
array[offset] = v.X
array[offset+1] = v.Y
array[offset+2] = v.Z
array[offset+3] = v.W
}
// Basic math operations:
// Add adds the other given vector to this one and returns the result as a new vector.
func (v Vector4) Add(other Vector4) Vector4 {
return Vector4{v.X + other.X, v.Y + other.Y, v.Z + other.Z, v.W + other.W}
}
// AddScalar adds scalar s to each component of this vector and returns new vector.
func (v Vector4) AddScalar(s float32) Vector4 {
return Vector4{v.X + s, v.Y + s, v.Z + s, v.W + s}
}
// SetAdd sets this to addition with other vector (i.e., += or plus-equals).
func (v *Vector4) SetAdd(other Vector4) {
v.X += other.X
v.Y += other.Y
v.Z += other.Z
v.W += other.W
}
// SetAddScalar sets this to addition with scalar.
func (v *Vector4) SetAddScalar(s float32) {
v.X += s
v.Y += s
v.Z += s
v.W += s
}
// Sub subtracts other vector from this one and returns result in new vector.
func (v Vector4) Sub(other Vector4) Vector4 {
return Vector4{v.X - other.X, v.Y - other.Y, v.Z - other.Z, v.W - other.W}
}
// SubScalar subtracts scalar s from each component of this vector and returns new vector.
func (v Vector4) SubScalar(s float32) Vector4 {
return Vector4{v.X - s, v.Y - s, v.Z - s, v.W - s}
}
// SetSub sets this to subtraction with other vector (i.e., -= or minus-equals).
func (v *Vector4) SetSub(other Vector4) {
v.X -= other.X
v.Y -= other.Y
v.Z -= other.Z
v.W -= other.W
}
// SetSubScalar sets this to subtraction of scalar.
func (v *Vector4) SetSubScalar(s float32) {
v.X -= s
v.Y -= s
v.Z -= s
v.W -= s
}
// Mul multiplies each component of this vector by the corresponding one from other
// and returns resulting vector.
func (v Vector4) Mul(other Vector4) Vector4 {
return Vector4{v.X * other.X, v.Y * other.Y, v.Z * other.Z, v.W * other.W}
}
// MulScalar multiplies each component of this vector by the scalar s and returns resulting vector.
func (v Vector4) MulScalar(s float32) Vector4 {
return Vector4{v.X * s, v.Y * s, v.Z * s, v.W * s}
}
// SetMul sets this to multiplication with other vector (i.e., *= or times-equals).
func (v *Vector4) SetMul(other Vector4) {
v.X *= other.X
v.Y *= other.Y
v.Z *= other.Z
v.W *= other.W
}
// SetMulScalar sets this to multiplication by scalar.
func (v *Vector4) SetMulScalar(s float32) {
v.X *= s
v.Y *= s
v.Z *= s
v.W *= s
}
// Div divides each component of this vector by the corresponding one from other vector
// and returns resulting vector.
func (v Vector4) Div(other Vector4) Vector4 {
return Vector4{v.X / other.X, v.Y / other.Y, v.Z / other.Z, v.W / other.W}
}
// DivScalar divides each component of this vector by the scalar s and returns resulting vector.
// If scalar is zero, returns zero.
func (v Vector4) DivScalar(scalar float32) Vector4 {
if scalar != 0 {
return v.MulScalar(1 / scalar)
} else {
return Vector4{}
}
}
// SetDiv sets this to division by other vector (i.e., /= or divide-equals).
func (v *Vector4) SetDiv(other Vector4) {
v.X /= other.X
v.Y /= other.Y
v.Z /= other.Z
v.W /= other.W
}
// SetDivScalar sets this to division by scalar.
func (v *Vector4) SetDivScalar(s float32) {
if s != 0 {
v.SetMulScalar(1 / s)
} else {
v.SetZero()
}
}
// Min returns min of this vector components vs. other vector.
func (v Vector4) Min(other Vector4) Vector4 {
return Vector4{Min(v.X, other.X), Min(v.Y, other.Y), Min(v.Z, other.Z), Min(v.W, other.W)}
}
// SetMin sets this vector components to the minimum values of itself and other vector.
func (v *Vector4) SetMin(other Vector4) {
v.X = Min(v.X, other.X)
v.Y = Min(v.Y, other.Y)
v.Z = Min(v.Z, other.Z)
v.W = Min(v.W, other.W)
}
// Max returns max of this vector components vs. other vector.
func (v Vector4) Max(other Vector4) Vector4 {
return Vector4{Max(v.X, other.X), Max(v.Y, other.Y), Max(v.Z, other.Z), Max(v.W, other.W)}
}
// SetMax sets this vector components to the maximum value of itself and other vector.
func (v *Vector4) SetMax(other Vector4) {
v.X = Max(v.X, other.X)
v.Y = Max(v.Y, other.Y)
v.Z = Max(v.Z, other.Z)
v.W = Max(v.W, other.W)
}
// Clamp sets this vector's components to be no less than the corresponding
// components of min and not greater than the corresponding component of max.
// Assumes min < max; if this assumption isn't true, it will not operate correctly.
func (v *Vector4) Clamp(min, max Vector4) {
if v.X < min.X {
v.X = min.X
} else if v.X > max.X {
v.X = max.X
}
if v.Y < min.Y {
v.Y = min.Y
} else if v.Y > max.Y {
v.Y = max.Y
}
if v.Z < min.Z {
v.Z = min.Z
} else if v.Z > max.Z {
v.Z = max.Z
}
if v.W < min.W {
v.W = min.W
} else if v.W > max.W {
v.W = max.W
}
}
// Floor returns this vector with [Floor] applied to each of its components.
func (v Vector4) Floor() Vector4 {
return Vector4{Floor(v.X), Floor(v.Y), Floor(v.Z), Floor(v.W)}
}
// Ceil returns this vector with [Ceil] applied to each of its components.
func (v Vector4) Ceil() Vector4 {
return Vector4{Ceil(v.X), Ceil(v.Y), Ceil(v.Z), Ceil(v.W)}
}
// Round returns this vector with [Round] applied to each of its components.
func (v Vector4) Round() Vector4 {
return Vector4{Round(v.X), Round(v.Y), Round(v.Z), Round(v.W)}
}
// Negate returns the vector with each component negated.
func (v Vector4) Negate() Vector4 {
return Vector4{-v.X, -v.Y, -v.Z, -v.W}
}
// Distance, Normal:
// Dot returns the dot product of this vector with the given other vector.
func (v Vector4) Dot(other Vector4) float32 {
return v.X*other.X + v.Y*other.Y + v.Z*other.Z + v.W*other.W
}
// Length returns the length (magnitude) of this vector.
func (v Vector4) Length() float32 {
return Sqrt(v.X*v.X + v.Y*v.Y + v.Z*v.Z + v.W*v.W)
}
// LengthSquared returns the length squared of this vector.
// LengthSquared can be used to compare the lengths of vectors
// without the need to perform a square root.
func (v Vector4) LengthSquared() float32 {
return v.X*v.X + v.Y*v.Y + v.Z*v.Z + v.W*v.W
}
// Normal returns this vector divided by its length (its unit vector).
func (v Vector4) Normal() Vector4 {
return v.DivScalar(v.Length())
}
// SetNormal normalizes this vector so its length will be 1.
func (v *Vector4) SetNormal() {
v.SetDivScalar(v.Length())
}
// Lerp returns vector with each components as the linear interpolated value of
// alpha between itself and the corresponding other component.
func (v Vector4) Lerp(other Vector4, alpha float32) Vector4 {
return Vector4{v.X + (other.X-v.X)*alpha, v.Y + (other.Y-v.Y)*alpha, v.Z + (other.Z-v.Z)*alpha,
v.W + (other.W-v.W)*alpha}
}
// Matrix operations:
// MulMatrix4 returns vector multiplied by specified 4x4 matrix.
func (v Vector4) MulMatrix4(m *Matrix4) Vector4 {
return Vector4{m[0]*v.X + m[4]*v.Y + m[8]*v.Z + m[12]*v.W,
m[1]*v.X + m[5]*v.Y + m[9]*v.Z + m[13]*v.W,
m[2]*v.X + m[6]*v.Y + m[10]*v.Z + m[14]*v.W,
m[3]*v.X + m[7]*v.Y + m[11]*v.Z + m[15]*v.W}
}
// SetAxisAngleFromQuat set this vector to be the axis (x, y, z) and angle (w)
// of a rotation specified the quaternion q.
// Assumes q is normalized.
func (v *Vector4) SetAxisAngleFromQuat(q Quat) {
// http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToAngle/index.htm
qw := Clamp(q.W, -1, 1)
v.W = 2 * Acos(qw)
s := Sqrt(1 - qw*qw)
if s < 0.0001 {
v.X = 1
v.Y = 0
v.Z = 0
} else {
v.X = q.X / s
v.Y = q.Y / s
v.Z = q.Z / s
}
}
// PerspDiv returns the 3-vector of normalized display coordinates (NDC) from given 4-vector
// By dividing by the 4th W component
func (v Vector4) PerspDiv() Vector3 {
return Vec3(v.X/v.W, v.Y/v.W, v.Z/v.W)
}
// Copyright (c) 2024, 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 pages
import (
"log/slog"
"strconv"
"cogentcore.org/core/colors"
"cogentcore.org/core/core"
"cogentcore.org/core/htmlcore"
"cogentcore.org/core/styles"
)
// Examples are the different core examples that exist as compiled
// Go code that can be run in pages. The map is keyed
// by ID. Generated pagegen.go files add to this by finding
// all code blocks with language Go (must be uppercase, as that
// indicates that is an "exported" example).
var Examples = map[string]func(b core.Widget){}
func init() {
htmlcore.ElementHandlers["pages-example"] = ExampleHandler
}
// NumExamples has the number of examples per page URL.
var NumExamples = map[string]int{}
// ExampleHandler is the htmlcore handler for <pages-example> HTML elements
// that handles examples.
func ExampleHandler(ctx *htmlcore.Context) bool {
// the node we actually care about is our first child, the <pre> element
ctx.Node = ctx.Node.FirstChild
core.NewText(ctx.Parent()).SetText(htmlcore.ExtractText(ctx)).Styler(func(s *styles.Style) {
s.Text.WhiteSpace = styles.WhiteSpacePreWrap
s.Background = colors.Scheme.SurfaceContainer
s.Border.Radius = styles.BorderRadiusMedium
})
url := ctx.PageURL
if url == "" {
url = "index"
}
id := url + "-" + strconv.Itoa(NumExamples[ctx.PageURL])
NumExamples[ctx.PageURL]++
fn := Examples[id]
if fn == nil {
slog.Error("programmer error: pages example not found in pages.Examples (you probably need to run go generate again)", "id", id)
} else {
fn(ctx.Parent())
}
return true
}
// 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 main
import (
"embed"
"cogentcore.org/core/core"
"cogentcore.org/core/pages"
)
//go:embed content
var content embed.FS
func main() {
b := core.NewBody("Pages Example")
pg := pages.NewPage(b).SetContent(content)
b.AddTopBar(func(bar *core.Frame) {
core.NewToolbar(bar).Maker(pg.MakeToolbar)
})
b.RunMainWindow()
}
// 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 pages provides an easy way to make content-focused
// sites consisting of Markdown, HTML, and Cogent Core.
package pages
//go:generate core generate
import (
"bytes"
"io/fs"
"log/slog"
"net/http"
"net/url"
"path"
"slices"
"strings"
"time"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/iox/tomlx"
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/colors"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/htmlcore"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/math32"
"cogentcore.org/core/pages/ppath"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/system"
"cogentcore.org/core/tree"
)
// Page represents a content page with support for navigating
// to other pages within the same source content.
type Page struct {
core.Frame
// Source is the filesystem in which the content is located.
Source fs.FS
// Context is the page's [htmlcore.Context].
Context *htmlcore.Context `set:"-"`
// The history of URLs that have been visited. The oldest page is first.
history []string
// historyIndex is the current place we are at in the History
historyIndex int
// pagePath is the fs path of the current page in [Page.Source]
pagePath string
// urlToPagePath is a map between user-facing page URLs and underlying
// FS page paths.
urlToPagePath map[string]string
// nav is the navigation tree.
nav *core.Tree
// body is the page body frame.
body *core.Frame
}
var _ tree.Node = (*Page)(nil)
// getWebURL, if non-nil, returns the current relative web URL that should
// be passed to [Page.OpenURL] on startup.
var getWebURL func(p *Page) string
// saveWebURL, if non-nil, saves the given web URL to the user's browser address bar and history.
var saveWebURL func(p *Page, u string)
// needsPath indicates that a URL in [Page.URLToPagePath] needs its path
// to be set to the first valid child path, since its index.md file does
// not exist.
const needsPath = "$__NEEDS_PATH__$"
func (pg *Page) Init() {
pg.Frame.Init()
pg.Context = htmlcore.NewContext()
pg.Context.OpenURL = func(url string) {
pg.OpenURL(url, true)
}
pg.Context.GetURL = func(rawURL string) (*http.Response, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
if u.Scheme != "" {
return http.Get(rawURL)
}
rawURL = strings.TrimPrefix(rawURL, "/")
filename := ""
dirPath, ok := pg.urlToPagePath[path.Dir(rawURL)]
if ok {
filename = path.Join(path.Dir(dirPath), path.Base(rawURL))
} else {
filename = rawURL
}
f, err := pg.Source.Open(filename)
if err != nil {
return nil, err
}
return &http.Response{
Status: "200 OK",
StatusCode: 200,
Body: f,
ContentLength: -1,
}, nil
}
pg.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Grow.Set(1, 1)
})
// must be done after the default title is set elsewhere in normal OnShow
pg.OnFinal(events.Show, func(e events.Event) {
pg.setStageTitle()
})
tree.AddChild(pg, func(w *core.Splits) {
w.SetSplits(0.2, 0.8)
tree.AddChild(w, func(w *core.Frame) {
w.SetProperty("tag", "tree") // ignore in generatehtml
w.Styler(func(s *styles.Style) {
s.Background = colors.Scheme.SurfaceContainerLow
})
tree.AddChild(w, func(w *core.Tree) {
pg.nav = w
w.SetText(core.TheApp.Name())
w.SetReadOnly(true)
w.OnSelect(func(e events.Event) {
if len(w.SelectedNodes) == 0 {
return
}
sn := w.SelectedNodes[0]
url := "/"
if sn != w {
// we need a slash so that it doesn't think it's a relative URL
url = "/" + sn.AsTree().PathFrom(w)
}
pg.OpenURL(url, true)
})
pg.urlToPagePath = map[string]string{"": "index.md"}
errors.Log(fs.WalkDir(pg.Source, ".", func(fpath string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// already handled
if fpath == "" || fpath == "." {
return nil
}
if system.TheApp.Platform() == system.Web && ppath.Draft(fpath) {
return nil
}
p := ppath.Format(fpath)
pdir := path.Dir(p)
base := path.Base(p)
// already handled
if base == "index.md" {
return nil
}
ext := path.Ext(base)
if ext != "" && ext != ".md" {
return nil
}
parent := w
if pdir != "" && pdir != "." {
parent = w.FindPath(pdir).(*core.Tree)
}
nm := strings.TrimSuffix(base, ext)
txt := strcase.ToSentence(nm)
tr := core.NewTree(parent).SetText(txt)
tr.SetName(nm)
// need index.md for page path
if d.IsDir() {
fpath += "/index.md"
}
exists, err := fsx.FileExistsFS(pg.Source, fpath)
if err != nil {
return err
}
if !exists {
fpath = needsPath
tr.SetProperty("no-index", true)
}
u := tr.PathFrom(w)
pg.urlToPagePath[u] = fpath
// All of our parents who needs a path get our path.
for pu, pp := range pg.urlToPagePath {
if pp == needsPath && strings.HasPrefix(u, pu) {
pg.urlToPagePath[pu] = fpath
}
}
return nil
}))
// If we still need a path, we shouldn't exist.
for u, p := range pg.urlToPagePath {
if p == needsPath {
delete(pg.urlToPagePath, u)
if n := w.FindPath(u); n != nil {
n.AsTree().Delete()
}
}
}
// open the default page if there is no currently open page
if pg.pagePath == "" {
if getWebURL != nil {
pg.OpenURL(getWebURL(pg), true)
} else {
pg.OpenURL("/", true)
}
}
})
})
tree.AddChild(w, func(w *core.Frame) {
pg.body = w
w.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Padding.Set(units.Dp(8))
})
})
})
}
// setStageTitle sets the title of the stage based on the current page URL.
func (pg *Page) setStageTitle() {
if rw := pg.Scene.RenderWindow(); rw != nil {
rw.SetStageTitle(ppath.Label(pg.Context.PageURL, core.TheApp.Name()))
}
}
// SetContent is a helper function that calls [Page.SetSource]
// with the "content" subdirectory of the given filesystem.
func (pg *Page) SetContent(content fs.FS) *Page {
return pg.SetSource(fsx.Sub(content, "content"))
}
// OpenURL sets the content of the page from the given url. If the given URL
// has no scheme (eg: "/about"), then it sets the content of the page to the
// file specified by the URL. This is either the "index.md" file in the
// corresponding directory (eg: "/about/index.md") or the corresponding
// md file (eg: "/about.md"). If it has a scheme, (eg: "https://example.com"),
// then it opens it in the user's default browser.
func (pg *Page) OpenURL(rawURL string, addToHistory bool) {
u, err := url.Parse(rawURL)
if err != nil {
core.ErrorSnackbar(pg, err, "Invalid URL")
return
}
if u.Scheme != "" {
system.TheApp.OpenURL(u.String())
return
}
if pg.Source == nil {
core.MessageSnackbar(pg, "Programmer error: page source must not be nil")
return
}
// if we are not rooted, we go relative to our current URL
if !strings.HasPrefix(rawURL, "/") {
current := pg.Context.PageURL
if !strings.HasSuffix(pg.pagePath, "index.md") && !strings.HasSuffix(pg.pagePath, "index.html") {
current = path.Dir(current) // we must go up one if we are not the index page (which is already up one)
}
rawURL = path.Join(current, rawURL)
}
if rawURL == ".." {
rawURL = ""
}
// the paths in the fs are never rooted, so we trim a rooted one
rawURL = strings.TrimPrefix(rawURL, "/")
rawURL = strings.TrimSuffix(rawURL, "/")
pg.pagePath = pg.urlToPagePath[rawURL]
b, err := fs.ReadFile(pg.Source, pg.pagePath)
if err != nil {
core.ErrorSnackbar(pg, err, "Error opening page "+rawURL)
return
}
pg.Context.PageURL = rawURL
if addToHistory {
pg.historyIndex = len(pg.history)
pg.history = append(pg.history, pg.Context.PageURL)
}
if saveWebURL != nil {
saveWebURL(pg, pg.Context.PageURL)
}
var frontMatter map[string]any
btp := []byte("+++")
if bytes.HasPrefix(b, btp) {
b = bytes.TrimPrefix(b, btp)
fmb, content, ok := bytes.Cut(b, btp)
if !ok {
slog.Error("got unclosed front matter")
b = fmb
fmb = nil
} else {
b = content
}
if len(fmb) > 0 {
errors.Log(tomlx.ReadBytes(&frontMatter, fmb))
}
}
// need to reset
NumExamples[pg.Context.PageURL] = 0
pg.nav.UnselectAll()
curNav := pg.nav.FindPath(rawURL).(*core.Tree)
// we must select the first tree that does not have "no-index"
if curNav.Property("no-index") != nil {
got := false
curNav.WalkDown(func(n tree.Node) bool {
if got {
return tree.Break
}
tr, ok := n.(*core.Tree)
if !ok || n.AsTree().Property("no-index") != nil {
return tree.Continue
}
curNav = tr
got = true
return tree.Break
})
}
curNav.Select()
curNav.ScrollToThis()
pg.Context.PageURL = curNav.PathFrom(pg.nav)
pg.setStageTitle()
pg.body.DeleteChildren()
if ppath.Draft(pg.pagePath) {
draft := core.NewText(pg.body).SetType(core.TextDisplayMedium).SetText("DRAFT")
draft.Styler(func(s *styles.Style) {
s.Color = colors.Scheme.Error.Base
s.Font.Weight = styles.WeightBold
})
}
if curNav != pg.nav {
bc := core.NewText(pg.body).SetText(ppath.Breadcrumbs(pg.Context.PageURL, core.TheApp.Name()))
bc.HandleTextClick(func(tl *paint.TextLink) {
pg.Context.OpenURL(tl.URL)
})
core.NewText(pg.body).SetType(core.TextDisplaySmall).SetText(curNav.Text)
}
if author := frontMatter["author"]; author != nil {
author := slicesx.As[any, string](author.([]any))
core.NewText(pg.body).SetType(core.TextTitleLarge).SetText("By " + strcase.FormatList(author...))
}
base := strings.TrimPrefix(path.Base(pg.pagePath), "-")
if len(base) >= 10 {
date := base[:10]
if t, err := time.Parse("2006-01-02", date); err == nil {
core.NewText(pg.body).SetType(core.TextTitleMedium).SetText(t.Format("1/2/2006"))
}
}
err = htmlcore.ReadMD(pg.Context, pg.body, b)
if err != nil {
core.ErrorSnackbar(pg, err, "Error loading page")
return
}
if curNav != pg.nav {
buttons := core.NewFrame(pg.body)
buttons.Styler(func(s *styles.Style) {
s.Align.Items = styles.Center
s.Grow.Set(1, 0)
})
if previous, ok := tree.Previous(curNav).(*core.Tree); ok {
// we must skip over trees with "no-index" to get to a real new page
for previous != nil && previous.Property("no-index") != nil {
previous, _ = tree.Previous(previous).(*core.Tree)
}
if previous != nil {
bt := core.NewButton(buttons).SetText("Previous").SetIcon(icons.ArrowBack).SetType(core.ButtonTonal)
bt.OnClick(func(e events.Event) {
curNav.Unselect()
previous.SelectEvent(events.SelectOne)
})
}
}
if next, ok := tree.Next(curNav).(*core.Tree); ok {
core.NewStretch(buttons)
bt := core.NewButton(buttons).SetText("Next").SetIcon(icons.ArrowForward).SetType(core.ButtonTonal)
bt.OnClick(func(e events.Event) {
curNav.Unselect()
next.SelectEvent(events.SelectOne)
})
}
}
pg.body.Update()
pg.body.ScrollDimToContentStart(math32.Y)
}
func (pg *Page) MakeToolbar(p *tree.Plan) {
if pg.historyIndex > 0 {
tree.Add(p, func(w *core.Button) {
w.SetIcon(icons.ArrowBack).SetKey(keymap.HistPrev)
w.SetTooltip("Back")
w.OnClick(func(e events.Event) {
pg.historyIndex--
// we need a slash so that it doesn't think it's a relative URL
pg.OpenURL("/"+pg.history[pg.historyIndex], false)
e.SetHandled()
})
})
}
}
func (pg *Page) MenuSearch(items *[]core.ChooserItem) {
urls := []string{}
for u := range pg.urlToPagePath {
urls = append(urls, u)
}
slices.Sort(urls)
for _, u := range urls {
*items = append(*items, core.ChooserItem{
Value: u,
Text: ppath.Label(u, core.TheApp.Name()),
Func: func() {
pg.OpenURL("/"+u, true)
},
})
}
}
// Copyright (c) 2024, 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 ppath handles pages paths.
package ppath
import (
"path"
"slices"
"strings"
"unicode"
"cogentcore.org/core/base/strcase"
)
// Draft returns whether the given path is a draft page that
// should be ignored in released builds, which is the case
// if the path starts with a dash.
func Draft(p string) bool {
return strings.HasPrefix(path.Base(p), "-")
}
// Format formats the given path into a correct pages path
// by removing all `{digit(s)}-` prefixes at the start of path
// segments, which are used for ordering files and folders and
// thus should not be displayed.
func Format(path string) string {
parts := strings.Split(path, "/")
for i, part := range parts {
if !strings.Contains(part, "-") {
continue
}
parts[i] = strings.TrimLeftFunc(part, func(r rune) bool {
return unicode.IsDigit(r) || r == '-'
})
}
return strings.Join(parts, "/")
}
// Label returns a user friendly label for the given page URL,
// with the given backup name if the URL is blank.
func Label(u string, backup string) string {
if u == "" {
return backup
}
parts := strings.Split(u, "/")
for i, part := range parts {
parts[i] = strcase.ToSentence(part)
}
slices.Reverse(parts)
return strings.Join(parts, " • ")
}
// BasePath returns a path that will take the given path all the
// way to the root using sequences of "..".
func BasePath(path string) string {
if path == "" {
return ""
}
numNested := strings.Count(path, "/") + 1
basePath := ""
for range numNested {
basePath += "../"
}
return basePath
}
// Breadcrumbs returns breadcrumbs (context about the parent directories
// of the given URL). The breadcrumb parts are links. It also takes the
// given name user-friendly name for the root directory.
func Breadcrumbs(u string, root string) string {
dir := path.Dir(u)
if dir == "" {
return ""
}
if !strings.HasPrefix(dir, ".") {
dir = "./" + dir
}
parts := strings.Split(dir, "/")
for i, part := range parts {
n := len(parts) - i
pageURL := ""
for range n {
pageURL = path.Join(pageURL, "..")
}
pageURL = path.Join(pageURL, part)
s := strcase.ToSentence(part)
if part == "." {
s = root
}
parts[i] = `<a href="` + pageURL + `">` + s + `</a>`
}
return strings.Join(parts, " • ")
}
// PreRenderData contains the data printed in JSON by a pages app
// run with the generatehtml tag.
type PreRenderData struct {
// Description contains the automatic page descriptions for each page URL.
Description map[string]string
// HTML contains the pre-rendered HTML for each page URL.
HTML map[string]string
}
// Code generated by "core generate"; DO NOT EDIT.
package pages
import (
"io/fs"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/pages.Page", IDName: "page", Doc: "Page represents a content page with support for navigating\nto other pages within the same source content.", Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Source", Doc: "Source is the filesystem in which the content is located."}, {Name: "Context", Doc: "Context is the page's [htmlcore.Context]."}, {Name: "history", Doc: "The history of URLs that have been visited. The oldest page is first."}, {Name: "historyIndex", Doc: "historyIndex is the current place we are at in the History"}, {Name: "pagePath", Doc: "pagePath is the fs path of the current page in [Page.Source]"}, {Name: "urlToPagePath", Doc: "urlToPagePath is a map between user-facing page URLs and underlying\nFS page paths."}, {Name: "nav", Doc: "nav is the navigation tree."}, {Name: "body", Doc: "body is the page body frame."}}})
// NewPage returns a new [Page] with the given optional parent:
// Page represents a content page with support for navigating
// to other pages within the same source content.
func NewPage(parent ...tree.Node) *Page { return tree.New[Page](parent...) }
// SetSource sets the [Page.Source]:
// Source is the filesystem in which the content is located.
func (t *Page) SetSource(v fs.FS) *Page { t.Source = v; return t }
// 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 paint
import (
"image"
"math"
"cogentcore.org/core/math32"
"github.com/anthonynsimon/bild/clone"
"github.com/anthonynsimon/bild/convolution"
)
// scipy impl:
// https://github.com/scipy/scipy/blob/4bfc152f6ee1ca48c73c06e27f7ef021d729f496/scipy/ndimage/filters.py#L136
// #L214 has the invocation: radius = Ceil(sigma)
// bild uses:
// math.Exp(-0.5 * (x * x / (2 * radius))
// so sigma = sqrt(radius) / 2
// and radius = sigma * sigma * 2
// GaussianBlurKernel1D returns a 1D Gaussian kernel.
// Sigma is the standard deviation,
// and the radius of the kernel is 4 * sigma.
func GaussianBlurKernel1D(sigma float64) *convolution.Kernel {
sigma2 := sigma * sigma
sfactor := -0.5 / sigma2
radius := math.Ceil(4 * sigma) // truncate = 4 in scipy
length := 2*int(radius) + 1
// Create the 1-d gaussian kernel
k := convolution.NewKernel(length, 1)
for i, x := 0, -radius; i < length; i, x = i+1, x+1 {
k.Matrix[i] = math.Exp(sfactor * (x * x))
}
return k
}
// GaussianBlur returns a smoothly blurred version of the image using
// a Gaussian function. Sigma is the standard deviation of the Gaussian
// function, and a kernel of radius = 4 * Sigma is used.
func GaussianBlur(src image.Image, sigma float64) *image.RGBA {
if sigma <= 0 {
return clone.AsRGBA(src)
}
k := GaussianBlurKernel1D(sigma).Normalized()
// Perform separable convolution
options := convolution.Options{Bias: 0, Wrap: false, KeepAlpha: false}
result := convolution.Convolve(src, k, &options)
result = convolution.Convolve(result, k.Transposed(), &options)
return result
}
// EdgeBlurFactors returns multiplicative factors that replicate the effect
// of a Gaussian kernel applied to a sharp edge transition in the middle of
// a line segment, with a given Gaussian sigma, and radius = sigma * radiusFactor.
// The returned line factors go from -radius to +radius.
// For low-contrast (opacity) cases, radiusFactor = 1 works well,
// because values beyond 1 sigma are effectively invisible, but 2 looks
// better for greater contrast cases.
func EdgeBlurFactors(sigma, radiusFactor float32) []float32 {
radius := math32.Ceil(sigma * radiusFactor)
irad := int(radius)
klen := irad*2 + 1
sfactor := -0.5 / (sigma * sigma)
if klen < 0 {
return []float32{}
}
k := make([]float32, klen)
sum := float32(0)
rstart := -radius + 0.5
for i, x := 0, rstart; i < klen; i, x = i+1, x+1 {
v := math32.FastExp(sfactor * (x * x))
sum += v
k[i] = v
}
for i, v := range k {
k[i] = v / sum
}
line := make([]float32, klen)
for li := range line {
sum := float32(0)
for ki, v := range k {
if ki >= (klen - li) {
break
}
sum += v
}
line[li] = sum
}
return line
}
// 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 paint
import (
"image"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
)
// DrawStandardBox draws the CSS standard box model using the given styling information,
// position, size, and parent actual background. This is used for rendering
// widgets such as buttons, text fields, etc in a GUI.
func (pc *Context) DrawStandardBox(st *styles.Style, pos math32.Vector2, size math32.Vector2, pabg image.Image) {
if !st.RenderBox {
return
}
encroach, pr := pc.boundsEncroachParent(pos, size)
tm := st.TotalMargin().Round()
mpos := pos.Add(tm.Pos())
msize := size.Sub(tm.Size())
radius := st.Border.Radius.Dots()
if encroach { // if we encroach, we must limit ourselves to the parent radius
radius = radius.Max(pr)
}
if st.ActualBackground == nil {
// we need to do this to prevent
// elements from rendering over themselves
// (see https://github.com/cogentcore/core/issues/565)
st.ActualBackground = pabg
}
// note that we always set the fill opacity to 1 because we are already applying
// the opacity of the background color in ComputeActualBackground above
pc.FillStyle.Opacity = 1
if st.FillMargin {
// We need to fill the whole box where the
// box shadows / element can go to prevent growing
// box shadows and borders. We couldn't just
// do this when there are box shadows, as they
// may be removed and then need to be covered up.
// This also fixes https://github.com/cogentcore/core/issues/579.
// This isn't an ideal solution because of performance,
// so TODO: maybe come up with a better solution for this.
// We need to use raw geom data because we need to clear
// any box shadow that may have gone in margin.
if encroach { // if we encroach, we must limit ourselves to the parent radius
pc.FillStyle.Color = pabg
pc.DrawRoundedRectangle(pos.X, pos.Y, size.X, size.Y, radius)
pc.Fill()
} else {
pc.BlitBox(pos, size, pabg)
}
}
pc.StrokeStyle.Opacity = st.Opacity
pc.FontStyle.Opacity = st.Opacity
// first do any shadow
if st.HasBoxShadow() {
// CSS effectively goes in reverse order
for i := len(st.BoxShadow) - 1; i >= 0; i-- {
shadow := st.BoxShadow[i]
pc.StrokeStyle.Color = nil
// note: applying 0.5 here does a reasonable job of matching
// material design shadows, at their specified alpha levels.
pc.FillStyle.Color = gradient.ApplyOpacity(shadow.Color, 0.5)
spos := shadow.BasePos(mpos)
ssz := shadow.BaseSize(msize)
// note: we are using EdgeBlurFactors with radiusFactor = 1
// (sigma == radius), so we divide Blur / 2 relative to the
// CSS standard of sigma = blur / 2 (i.e., our sigma = blur,
// so we divide Blur / 2 to achieve the same effect).
// This works fine for low-opacity blur factors (the edges are
// so transparent that you can't really see beyond 1 sigma,
// if you used radiusFactor = 2).
// If a higher-contrast shadow is used, it would look better
// with radiusFactor = 2, and you'd have to remove this /2 factor.
pc.DrawRoundedShadowBlur(shadow.Blur.Dots/2, 1, spos.X, spos.Y, ssz.X, ssz.Y, radius)
}
}
// then draw the box over top of that.
// we need to draw things twice here because we need to clear
// the whole area with the background color first so the border
// doesn't render weirdly
if styles.SidesAreZero(radius.Sides) {
pc.FillBox(mpos, msize, st.ActualBackground)
} else {
pc.FillStyle.Color = st.ActualBackground
// no border; fill on
pc.DrawRoundedRectangle(mpos.X, mpos.Y, msize.X, msize.Y, radius)
pc.Fill()
}
// now that we have drawn background color
// above, we can draw the border
mpos.SetSub(st.Border.Width.Dots().Pos().MulScalar(0.5))
msize.SetAdd(st.Border.Width.Dots().Size().MulScalar(0.5))
mpos.SetSub(st.Border.Offset.Dots().Pos())
msize.SetAdd(st.Border.Offset.Dots().Size())
pc.FillStyle.Color = nil
pc.DrawBorder(mpos.X, mpos.Y, msize.X, msize.Y, st.Border)
}
// boundsEncroachParent returns whether the current box encroaches on the
// parent bounds, taking into account the parent radius, which is also returned.
func (pc *Context) boundsEncroachParent(pos, size math32.Vector2) (bool, styles.SideFloats) {
if len(pc.BoundsStack) == 0 {
return false, styles.SideFloats{}
}
pr := pc.RadiusStack[len(pc.RadiusStack)-1]
if styles.SidesAreZero(pr.Sides) {
return false, pr
}
pbox := pc.BoundsStack[len(pc.BoundsStack)-1]
psz := math32.FromPoint(pbox.Size())
pr = ClampBorderRadius(pr, psz.X, psz.Y)
rect := math32.Box2{Min: pos, Max: pos.Add(size)}
// logic is currently based on consistent radius for all corners
radius := max(pr.Top, pr.Left, pr.Right, pr.Bottom)
// each of these is how much the element is encroaching into each
// side of the bounding rectangle, within the radius curve.
// if the number is negative, then it isn't encroaching at all and can
// be ignored.
top := radius - (rect.Min.Y - float32(pbox.Min.Y))
left := radius - (rect.Min.X - float32(pbox.Min.X))
right := radius - (float32(pbox.Max.X) - rect.Max.X)
bottom := radius - (float32(pbox.Max.Y) - rect.Max.Y)
return top > 0 || left > 0 || right > 0 || bottom > 0, pr
}
// 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 paint
import (
"log"
"math"
"path/filepath"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/colors"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"github.com/goki/freetype/truetype"
"golang.org/x/image/font/opentype"
)
// OpenFont loads the font specified by the font style from the font library.
// This is the primary method to use for loading fonts, as it uses a robust
// fallback method to finding an appropriate font, and falls back on the
// builtin Go font as a last resort. It returns the font
// style object with Face set to the resulting font.
// The font size is always rounded to nearest integer, to produce
// better-looking results (presumably). The current metrics and given
// unit.Context are updated based on the properties of the font.
func OpenFont(fs *styles.FontRender, uc *units.Context) styles.Font {
fs.Size.ToDots(uc)
facenm := FontFaceName(fs.Family, fs.Stretch, fs.Weight, fs.Style)
intDots := int(math.Round(float64(fs.Size.Dots)))
if intDots == 0 {
// fmt.Printf("FontStyle Error: bad font size: %v or units context: %v\n", fs.Size, *ctxt)
intDots = 12
}
face, err := FontLibrary.Font(facenm, intDots)
if err != nil {
log.Printf("%v\n", err)
if fs.Face == nil {
face = errors.Log1(FontLibrary.Font("Roboto", intDots)) // guaranteed to exist
fs.Face = face
}
} else {
fs.Face = face
}
fs.SetUnitContext(uc)
return fs.Font
}
// OpenFontFace loads a font face from the given font file bytes, with the given
// name and path for context, with given raw size in display dots, and if
// strokeWidth is > 0, the font is drawn in outline form (stroked) instead of
// filled (supported in SVG). loadFontMu must be locked prior to calling.
func OpenFontFace(bytes []byte, name, path string, size int, strokeWidth int) (*styles.FontFace, error) {
ext := strings.ToLower(filepath.Ext(path))
if ext == ".otf" {
// note: this compiles but otf fonts are NOT yet supported apparently
f, err := opentype.Parse(bytes)
if err != nil {
return nil, err
}
face, err := opentype.NewFace(f, &opentype.FaceOptions{
Size: float64(size),
DPI: 72,
// Hinting: font.HintingFull,
})
ff := styles.NewFontFace(name, size, face)
return ff, err
} else {
f, err := truetype.Parse(bytes)
if err != nil {
return nil, err
}
face := truetype.NewFace(f, &truetype.Options{
Size: float64(size),
Stroke: strokeWidth,
// Hinting: font.HintingFull,
// GlyphCacheEntries: 1024, // default is 512 -- todo benchmark
})
ff := styles.NewFontFace(name, size, face)
return ff, nil
}
}
// FontStyleCSS looks for "tag" name properties in cssAgg properties, and applies those to
// style if found, and returns true -- false if no such tag found
func FontStyleCSS(fs *styles.FontRender, tag string, cssAgg map[string]any, unit *units.Context, ctxt colors.Context) bool {
if cssAgg == nil {
return false
}
tp, ok := cssAgg[tag]
if !ok {
return false
}
pmap, ok := tp.(map[string]any) // must be a properties map
if ok {
fs.SetStyleProperties(nil, pmap, ctxt)
OpenFont(fs, unit)
return true
}
return false
}
// 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 paint
import (
"embed"
"fmt"
"io/fs"
"log/slog"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/styles"
)
// loadFontMu protects the font loading calls, which are not concurrent-safe
var loadFontMu sync.RWMutex
// FontInfo contains basic font information for choosing a given font --
// displayed in the font chooser dialog.
type FontInfo struct {
// official regularized name of font
Name string
// stretch: normal, expanded, condensed, etc
Stretch styles.FontStretch
// weight: normal, bold, etc
Weight styles.FontWeights
// style -- normal, italic, etc
Style styles.FontStyles
// example text -- styled according to font params in chooser
Example string
}
// Label satisfies the Labeler interface
func (fi FontInfo) Label() string {
return fi.Name
}
// FontLib holds the fonts available in a font library. The font name is
// regularized so that the base "Regular" font is the root term of a sequence
// of other font names that describe the stretch, weight, and style, e.g.,
// "Arial" as the base name, "Arial Bold", "Arial Bold Italic" etc. Thus,
// each font name specifies a particular font weight and style. When fonts
// are loaded into the library, the names are appropriately regularized.
type FontLib struct {
// An fs containing available fonts, which are typically embedded through go:embed.
// It is initialized to contain of the default fonts located in the fonts directory
// (https://github.com/cogentcore/core/tree/main/paint/fonts), but it can be extended by
// any packages by using a merged fs package.
FontsFS fs.FS
// list of font paths to search for fonts
FontPaths []string
// Map of font name to path to file. If the path starts
// with "fs://", it indicates that it is located in
// [FontLib.FontsFS].
FontsAvail map[string]string
// information about each font -- this list should be used for selecting valid regularized font names
FontInfo []FontInfo
// double-map of cached fonts, by font name and then integer font size within that
Faces map[string]map[int]*styles.FontFace
}
// FontLibrary is the core font library, initialized from fonts available on font paths
var FontLibrary FontLib
// FontAvail determines if a given font name is available (case insensitive)
func (fl *FontLib) FontAvail(fontnm string) bool {
loadFontMu.RLock()
defer loadFontMu.RUnlock()
fontnm = strings.ToLower(fontnm)
_, ok := FontLibrary.FontsAvail[fontnm]
return ok
}
// FontInfoExample is example text to demonstrate fonts -- from Inkscape plus extra
var FontInfoExample = "AaBbCcIiPpQq12369$€¢?.:/()àáâãäåæç日本中国⇧⌘"
// Init initializes the font library if it hasn't been yet
func (fl *FontLib) Init() {
if fl.FontPaths == nil {
loadFontMu.Lock()
// fmt.Printf("Initializing font lib\n")
fl.FontsFS = fsx.Sub(defaultFonts, "fonts")
fl.FontPaths = make([]string, 0)
fl.FontsAvail = make(map[string]string)
fl.FontInfo = make([]FontInfo, 0)
fl.Faces = make(map[string]map[int]*styles.FontFace)
loadFontMu.Unlock()
return // no paths to load from yet
}
loadFontMu.RLock()
sz := len(fl.FontsAvail)
loadFontMu.RUnlock()
if sz == 0 {
// fmt.Printf("updating fonts avail in %v\n", fl.FontPaths)
fl.UpdateFontsAvail()
}
}
// Font gets a particular font, specified by the official regularized font
// name (see FontsAvail list), at given dots size (integer), using a cache of
// loaded fonts.
func (fl *FontLib) Font(fontnm string, size int) (*styles.FontFace, error) {
fontnm = strings.ToLower(fontnm)
fl.Init()
loadFontMu.RLock()
if facemap := fl.Faces[fontnm]; facemap != nil {
if face := facemap[size]; face != nil {
// fmt.Printf("Got font face from cache: %v %v\n", fontnm, size)
loadFontMu.RUnlock()
return face, nil
}
}
path := fl.FontsAvail[fontnm]
if path == "" {
loadFontMu.RUnlock()
return nil, fmt.Errorf("girl/paint.FontLib: Font named: %v not found in list of available fonts; try adding to FontPaths in girl/paint.FontLibrary; searched FontLib.FontsFS and paths: %v", fontnm, fl.FontPaths)
}
var bytes []byte
if strings.HasPrefix(path, "fs://") {
b, err := fs.ReadFile(fl.FontsFS, strings.TrimPrefix(path, "fs://"))
if err != nil {
err = fmt.Errorf("error opening font file for font %q in FontsFS: %w", fontnm, err)
slog.Error(err.Error())
return nil, err
}
bytes = b
} else {
b, err := os.ReadFile(path)
if err != nil {
err = fmt.Errorf("error opening font file for font %q with path %q: %w", fontnm, path, err)
slog.Error(err.Error())
return nil, err
}
bytes = b
}
loadFontMu.RUnlock()
loadFontMu.Lock()
face, err := OpenFontFace(bytes, fontnm, path, size, 0)
if err != nil || face == nil {
if err == nil {
err = fmt.Errorf("girl/paint.FontLib: nil face with no error for: %v", fontnm)
}
slog.Error("girl/paint.FontLib: error loading font, removed from list", "fontName", fontnm)
loadFontMu.Unlock()
fl.DeleteFont(fontnm)
return nil, err
}
facemap := fl.Faces[fontnm]
if facemap == nil {
facemap = make(map[int]*styles.FontFace)
fl.Faces[fontnm] = facemap
}
facemap[size] = face
// fmt.Printf("Opened font face: %v %v\n", fontnm, size)
loadFontMu.Unlock()
return face, nil
}
// DeleteFont removes given font from list of available fonts -- if not supported etc
func (fl *FontLib) DeleteFont(fontnm string) {
loadFontMu.Lock()
defer loadFontMu.Unlock()
delete(fl.FontsAvail, fontnm)
for i, fi := range fl.FontInfo {
if strings.ToLower(fi.Name) == fontnm {
sz := len(fl.FontInfo)
copy(fl.FontInfo[i:], fl.FontInfo[i+1:])
fl.FontInfo = fl.FontInfo[:sz-1]
break
}
}
}
// OpenAllFonts attempts to load all fonts that were found -- call this before
// displaying the font chooser to eliminate any bad fonts.
func (fl *FontLib) OpenAllFonts(size int) {
sz := len(fl.FontInfo)
for i := sz - 1; i > 0; i-- {
fi := fl.FontInfo[i]
fl.Font(strings.ToLower(fi.Name), size)
}
}
// InitFontPaths initializes font paths to system defaults, only if no paths
// have yet been set
func (fl *FontLib) InitFontPaths(paths ...string) {
if len(fl.FontPaths) > 0 {
return
}
fl.AddFontPaths(paths...)
}
func (fl *FontLib) AddFontPaths(paths ...string) bool {
fl.Init()
fl.FontPaths = append(fl.FontPaths, paths...)
return fl.UpdateFontsAvail()
}
// UpdateFontsAvail scans for all fonts we can use on the FontPaths
func (fl *FontLib) UpdateFontsAvail() bool {
if len(fl.FontPaths) == 0 {
slog.Error("girl/paint.FontLib: programmer error: no font paths; need to add some")
}
loadFontMu.Lock()
defer loadFontMu.Unlock()
if len(fl.FontsAvail) > 0 {
fl.FontsAvail = make(map[string]string)
}
err := fl.FontsAvailFromFS(fl.FontsFS, "fs://")
if err != nil {
slog.Error("girl/paint.FontLib: error walking FontLib.FontsFS", "err", err)
}
for _, p := range fl.FontPaths {
// we can ignore missing font paths, since some of them may not work on certain systems
if _, err := os.Stat(p); err != nil && errors.Is(err, fs.ErrNotExist) {
continue
}
err := fl.FontsAvailFromFS(os.DirFS(p), p+string(filepath.Separator))
if err != nil {
slog.Error("girl/paint.FontLib: error walking path", "path", p, "err", err)
}
}
sort.Slice(fl.FontInfo, func(i, j int) bool {
return fl.FontInfo[i].Name < fl.FontInfo[j].Name
})
return len(fl.FontsAvail) > 0
}
// FontsAvailFromPath scans for all fonts we can use on a given fs,
// gathering info into FontsAvail and FontInfo. It adds the given root
// path string to all paths.
func (fl *FontLib) FontsAvailFromFS(fsys fs.FS, root string) error {
return fs.WalkDir(fsys, ".", func(path string, info fs.DirEntry, err error) error {
if err != nil {
slog.Error("girl/paint.FontLib: error accessing path", "path", path, "err", err)
return err
}
ext := strings.ToLower(filepath.Ext(path))
_, ok := FontExts[ext]
if !ok {
return nil
}
_, fn := filepath.Split(path)
fn = fn[:len(fn)-len(ext)]
bfn := fn
bfn = strings.TrimSuffix(fn, "bd")
bfn = strings.TrimSuffix(bfn, "bi")
bfn = strings.TrimSuffix(bfn, "z")
bfn = strings.TrimSuffix(bfn, "b")
if bfn != "calibri" && bfn != "gadugui" && bfn != "segoeui" && bfn != "segui" {
bfn = strings.TrimSuffix(bfn, "i")
}
if afn, ok := altFontMap[bfn]; ok {
sfx := ""
if strings.HasSuffix(fn, "bd") || strings.HasSuffix(fn, "b") {
sfx = " Bold"
} else if strings.HasSuffix(fn, "bi") || strings.HasSuffix(fn, "z") {
sfx = " Bold Italic"
} else if strings.HasSuffix(fn, "i") {
sfx = " Italic"
}
fn = afn + sfx
} else {
fn = strcase.ToTitle(fn)
for sc, rp := range shortFontMods {
if strings.HasSuffix(fn, sc) {
fn = strings.TrimSuffix(fn, sc)
fn += rp
break
}
}
}
fn = styles.FixFontMods(fn)
basefn := strings.ToLower(fn)
if _, ok := fl.FontsAvail[basefn]; !ok {
fl.FontsAvail[basefn] = root + path
fi := FontInfo{Name: fn, Example: FontInfoExample}
_, fi.Stretch, fi.Weight, fi.Style = styles.FontNameToMods(fn)
fl.FontInfo = append(fl.FontInfo, fi)
// fmt.Printf("added font %q at path %q\n", basefn, root+path)
}
return nil
})
}
var FontExts = map[string]struct{}{
".ttf": {},
".ttc": {}, // note: unpack to raw .ttf to use -- otherwise only getting first font
".otf": {}, // not yet supported
}
// shortFontMods corrects annoying short font mod names, found in Unity font
// on linux -- needs space and uppercase to avoid confusion -- checked with
// HasSuffix
var shortFontMods = map[string]string{
" B": " Bold",
" I": " Italic",
" C": " Condensed",
" L": " Light",
" LI": " Light Italic",
" M": " Medium",
" MI": " Medium Italic",
" R": " Regular",
" RI": " Italic",
" BI": " Bold Italic",
}
// altFontMap is an alternative font map that maps file names to more standard
// full names (e.g., Times -> Times New Roman) -- also looks for b,i suffixes
// for these cases -- some are added here just to pick up those suffixes.
// This is needed for Windows only.
var altFontMap = map[string]string{
"arial": "Arial",
"ariblk": "Arial Black",
"candara": "Candara",
"calibri": "Calibri",
"cambria": "Cambria",
"cour": "Courier New",
"constan": "Constantia",
"consola": "Console",
"comic": "Comic Sans MS",
"corbel": "Corbel",
"framd": "Franklin Gothic Medium",
"georgia": "Georgia",
"gadugi": "Gadugi",
"malgun": "Malgun Gothic",
"mmrtex": "Myanmar Text",
"pala": "Palatino",
"segoepr": "Segoe Print",
"segoesc": "Segoe Script",
"segoeui": "Segoe UI",
"segui": "Segoe UI Historic",
"tahoma": "Tahoma",
"taile": "Traditional Arabic",
"times": "Times New Roman",
"trebuc": "Trebuchet",
"verdana": "Verdana",
}
//go:embed fonts/*.ttf
var defaultFonts embed.FS
// FontFallbacks are a list of fallback fonts to try, at the basename level.
// Make sure there are no loops! Include Noto versions of everything in this
// because they have the most stretch options, so they should be in the mix if
// they have been installed, and include "Roboto" options last.
var FontFallbacks = map[string]string{
"serif": "Times New Roman",
"times": "Times New Roman",
"Times New Roman": "Liberation Serif",
"Liberation Serif": "NotoSerif",
"sans-serif": "NotoSans",
"NotoSans": "Roboto",
"courier": "Courier",
"Courier": "Courier New",
"Courier New": "NotoSansMono",
"NotoSansMono": "Roboto Mono",
"monospace": "NotoSansMono",
"cursive": "Comic Sans", // todo: look up more of these
"Comic Sans": "Comic Sans MS",
"fantasy": "Impact",
"Impact": "Impac",
}
func addUniqueFont(fns *[]string, fn string) bool {
sz := len(*fns)
for i := 0; i < sz; i++ {
if (*fns)[i] == fn {
return false
}
}
*fns = append(*fns, fn)
return true
}
func addUniqueFontRobust(fns *[]string, fn string) bool {
if FontLibrary.FontAvail(fn) {
return addUniqueFont(fns, fn)
}
camel := strcase.ToCamel(fn)
if FontLibrary.FontAvail(camel) {
return addUniqueFont(fns, camel)
}
spc := strcase.ToTitle(fn)
if FontLibrary.FontAvail(spc) {
return addUniqueFont(fns, spc)
}
return false
}
// 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 paint
import (
"strings"
"sync"
"cogentcore.org/core/styles"
)
var (
// faceNameCache is a cache for fast lookup of valid font face names given style specs
faceNameCache map[string]string
// faceNameCacheMu protects access to faceNameCache
faceNameCacheMu sync.RWMutex
)
// FontFaceName returns the best full FaceName to use for the given font
// family(ies) (comma separated) and modifier parameters
func FontFaceName(fam string, str styles.FontStretch, wt styles.FontWeights, sty styles.FontStyles) string {
if fam == "" {
fam = styles.PrefFontFamily
}
cacheNm := fam + "|" + str.String() + "|" + wt.String() + "|" + sty.String()
faceNameCacheMu.RLock()
if fc, has := faceNameCache[cacheNm]; has {
faceNameCacheMu.RUnlock()
return fc
}
faceNameCacheMu.RUnlock()
nms := strings.Split(fam, ",")
basenm := ""
if len(nms) > 0 { // start off with any styles implicit in font name
_, fstr, fwt, fsty := styles.FontNameToMods(strings.TrimSpace(nms[0]))
if fstr != styles.FontStrNormal {
str = fstr
}
if fwt != styles.WeightNormal {
wt = fwt
}
if fsty != styles.FontNormal {
sty = fsty
}
}
nms, _, _ = FontAlts(fam) // nms are all base names now
// we try multiple iterations, going through list of alternatives (which
// should be from most specific to least, all of which have an existing
// base name) -- first iter we look for an exact match for given
// modifiers, then we start relaxing things in terms of most likely
// issues..
didItalic := false
didOblique := false
iterloop:
for iter := 0; iter < 10; iter++ {
for _, basenm = range nms {
fn := styles.FontNameFromMods(basenm, str, wt, sty)
if FontLibrary.FontAvail(fn) {
break iterloop
}
}
if str != styles.FontStrNormal {
hasStr := false
for _, basenm = range nms {
fn := styles.FontNameFromMods(basenm, str, styles.WeightNormal, styles.FontNormal)
if FontLibrary.FontAvail(fn) {
hasStr = true
break
}
}
if !hasStr { // if even basic stretch not avail, move on
str = styles.FontStrNormal
continue
}
continue
}
if sty == styles.Italic { // italic is more common, but maybe oblique exists
didItalic = true
if !didOblique {
sty = styles.Oblique
continue
}
sty = styles.FontNormal
continue
}
if sty == styles.Oblique { // by now we've tried both, try nothing
didOblique = true
if !didItalic {
sty = styles.Italic
continue
}
sty = styles.FontNormal
continue
}
if wt != styles.WeightNormal {
if wt < styles.Weight400 {
if wt != styles.WeightLight {
wt = styles.WeightLight
continue
}
} else {
if wt != styles.WeightBold {
wt = styles.WeightBold
continue
}
}
wt = styles.WeightNormal
continue
}
if str != styles.FontStrNormal { // time to give up
str = styles.FontStrNormal
continue
}
break // tried everything
}
fnm := styles.FontNameFromMods(basenm, str, wt, sty)
faceNameCacheMu.Lock()
if faceNameCache == nil {
faceNameCache = make(map[string]string)
}
faceNameCache[cacheNm] = fnm
faceNameCacheMu.Unlock()
return fnm
}
// FontSerifMonoGuess looks at a list of alternative font names and tires to
// guess if the font is a serif (vs sans) or monospaced (vs proportional)
// font.
func FontSerifMonoGuess(fns []string) (serif, mono bool) {
for _, fn := range fns {
lfn := strings.ToLower(fn)
if strings.Contains(lfn, "serif") {
serif = true
}
if strings.Contains(lfn, "mono") || lfn == "menlo" || lfn == "courier" || lfn == "courier new" || strings.Contains(lfn, "typewriter") {
mono = true
}
}
return
}
// FontAlts generates a list of all possible alternative fonts that actually
// exist in font library for a list of font families, and a guess as to
// whether the font is a serif (vs sans) or monospaced (vs proportional) font.
// Only deals with base names.
func FontAlts(fams string) (fns []string, serif, mono bool) {
nms := strings.Split(fams, ",")
if len(nms) == 0 {
fn := styles.PrefFontFamily
if fn == "" {
fns = []string{"Roboto"}
return
}
}
fns = make([]string, 0, 20)
for _, fn := range nms {
fn = strings.TrimSpace(fn)
basenm, _, _, _ := styles.FontNameToMods(fn)
addUniqueFontRobust(&fns, basenm)
altsloop:
for {
altfn, ok := FontFallbacks[basenm]
if !ok {
break altsloop
}
addUniqueFontRobust(&fns, altfn)
basenm = altfn
}
}
serif, mono = FontSerifMonoGuess(fns)
// final baseline backups
if mono {
addUniqueFont(&fns, "NotoSansMono") // has more options
addUniqueFont(&fns, "Roboto Mono") // just as good as liberation mono..
} else if serif {
addUniqueFont(&fns, "Liberation Serif")
addUniqueFont(&fns, "NotoSerif")
addUniqueFont(&fns, "Roboto") // not serif but drop dead backup
} else {
addUniqueFont(&fns, "NotoSans")
addUniqueFont(&fns, "Roboto") // good as anything
}
return
}
// 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 paint
import "runtime"
// FontPaths contains the filepaths in which fonts are stored for the current platform.
var FontPaths []string
func init() {
switch runtime.GOOS {
case "android":
FontPaths = []string{"/system/fonts"}
case "darwin", "ios":
FontPaths = []string{"/System/Library/Fonts", "/Library/Fonts"}
case "js":
FontPaths = []string{"/fonts"}
case "linux":
// different distros have a different path
FontPaths = []string{"/usr/share/fonts/truetype", "/usr/share/fonts/TTF"}
case "windows":
FontPaths = []string{"C:\\Windows\\Fonts"}
}
}
// 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 paint
import (
"image"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
)
// TextLink represents a hyperlink within rendered text
type TextLink struct {
// text label for the link
Label string
// full URL for the link
URL string
// Style for rendering this link, set by the controlling widget
Style styles.FontRender
// additional properties defined for the link, from the parsed HTML attributes
Properties map[string]any
// span index where link starts
StartSpan int
// index in StartSpan where link starts
StartIndex int
// span index where link ends (can be same as EndSpan)
EndSpan int
// index in EndSpan where link ends (index of last rune in label)
EndIndex int
}
// Bounds returns the bounds of the link
func (tl *TextLink) Bounds(tr *Text, pos math32.Vector2) image.Rectangle {
stsp := &tr.Spans[tl.StartSpan]
tpos := pos.Add(stsp.RelPos)
sr := &(stsp.Render[tl.StartIndex])
sp := tpos.Add(sr.RelPos)
sp.Y -= sr.Size.Y
ep := sp
if tl.EndSpan == tl.StartSpan {
er := &(stsp.Render[tl.EndIndex])
ep = tpos.Add(er.RelPos)
ep.X += er.Size.X
} else {
er := &(stsp.Render[len(stsp.Render)-1])
ep = tpos.Add(er.RelPos)
ep.X += er.Size.X
}
return image.Rectangle{Min: sp.ToPointFloor(), Max: ep.ToPointCeil()}
}
// 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 paint
import (
"errors"
"image"
"image/color"
"io"
"math"
"slices"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint/raster"
"cogentcore.org/core/styles"
"github.com/anthonynsimon/bild/clone"
"golang.org/x/image/draw"
"golang.org/x/image/math/f64"
)
/*
This borrows heavily from: https://github.com/fogleman/gg
Copyright (C) 2016 Michael Fogleman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// Context provides the rendering state, styling parameters, and methods for
// painting. It is the main entry point to the paint API; most things are methods
// on Context, although Text rendering is handled separately in TextRender.
// A Context is typically constructed through [NewContext], [NewContextFromImage],
// or [NewContextFromRGBA], although it can also be constructed directly through
// a struct literal when an existing [State] and [styles.Paint] exist.
type Context struct {
*State
*styles.Paint
}
// NewContext returns a new [Context] associated with a new [image.RGBA]
// with the given width and height.
func NewContext(width, height int) *Context {
pc := &Context{&State{}, &styles.Paint{}}
sz := image.Pt(width, height)
img := image.NewRGBA(image.Rectangle{Max: sz})
pc.Init(width, height, img)
pc.Bounds = img.Rect
pc.Defaults()
pc.SetUnitContextExt(sz)
return pc
}
// NewContextFromImage returns a new [Context] associated with an [image.RGBA]
// copy of the given [image.Image]. It does not render directly onto the given
// image; see [NewContextFromRGBA] for a version that renders directly.
func NewContextFromImage(img *image.RGBA) *Context {
pc := &Context{&State{}, &styles.Paint{}}
pc.Init(img.Rect.Dx(), img.Rect.Dy(), img)
pc.Defaults()
pc.SetUnitContextExt(img.Rect.Size())
return pc
}
// NewContextFromRGBA returns a new [Context] associated with the given [image.RGBA].
// It renders directly onto the given image; see [NewContextFromImage] for a version
// that makes a copy.
func NewContextFromRGBA(img image.Image) *Context {
pc := &Context{&State{}, &styles.Paint{}}
r := clone.AsRGBA(img)
pc.Init(r.Rect.Dx(), r.Rect.Dy(), r)
pc.Defaults()
pc.SetUnitContextExt(r.Rect.Size())
return pc
}
// FillStrokeClear is a convenience final stroke and clear draw for shapes when done
func (pc *Context) FillStrokeClear() {
if pc.SVGOut != nil {
io.WriteString(pc.SVGOut, pc.SVGPath())
}
pc.FillPreserve()
pc.StrokePreserve()
pc.ClearPath()
}
//////////////////////////////////////////////////////////////////////////////////
// Path Manipulation
// TransformPoint multiplies the specified point by the current transform matrix,
// returning a transformed position.
func (pc *Context) TransformPoint(x, y float32) math32.Vector2 {
return pc.CurrentTransform.MulVector2AsPoint(math32.Vec2(x, y))
}
// BoundingBox computes the bounding box for an element in pixel int
// coordinates, applying current transform
func (pc *Context) BoundingBox(minX, minY, maxX, maxY float32) image.Rectangle {
sw := float32(0.0)
if pc.StrokeStyle.Color != nil {
sw = 0.5 * pc.StrokeWidth()
}
tmin := pc.CurrentTransform.MulVector2AsPoint(math32.Vec2(minX, minY))
tmax := pc.CurrentTransform.MulVector2AsPoint(math32.Vec2(maxX, maxY))
tp1 := math32.Vec2(tmin.X-sw, tmin.Y-sw).ToPointFloor()
tp2 := math32.Vec2(tmax.X+sw, tmax.Y+sw).ToPointCeil()
return image.Rect(tp1.X, tp1.Y, tp2.X, tp2.Y)
}
// BoundingBoxFromPoints computes the bounding box for a slice of points
func (pc *Context) BoundingBoxFromPoints(points []math32.Vector2) image.Rectangle {
sz := len(points)
if sz == 0 {
return image.Rectangle{}
}
min := points[0]
max := points[1]
for i := 1; i < sz; i++ {
min.SetMin(points[i])
max.SetMax(points[i])
}
return pc.BoundingBox(min.X, min.Y, max.X, max.Y)
}
// MoveTo starts a new subpath within the current path starting at the
// specified point.
func (pc *Context) MoveTo(x, y float32) {
if pc.HasCurrent {
pc.Path.Stop(false) // note: used to add a point to separate FillPath..
}
p := pc.TransformPoint(x, y)
pc.Path.Start(p.ToFixed())
pc.Start = p
pc.Current = p
pc.HasCurrent = true
}
// LineTo adds a line segment to the current path starting at the current
// point. If there is no current point, it is equivalent to MoveTo(x, y)
func (pc *Context) LineTo(x, y float32) {
if !pc.HasCurrent {
pc.MoveTo(x, y)
} else {
p := pc.TransformPoint(x, y)
pc.Path.Line(p.ToFixed())
pc.Current = p
}
}
// QuadraticTo adds a quadratic bezier curve to the current path starting at
// the current point. If there is no current point, it first performs
// MoveTo(x1, y1)
func (pc *Context) QuadraticTo(x1, y1, x2, y2 float32) {
if !pc.HasCurrent {
pc.MoveTo(x1, y1)
}
p1 := pc.TransformPoint(x1, y1)
p2 := pc.TransformPoint(x2, y2)
pc.Path.QuadBezier(p1.ToFixed(), p2.ToFixed())
pc.Current = p2
}
// CubicTo adds a cubic bezier curve to the current path starting at the
// current point. If there is no current point, it first performs
// MoveTo(x1, y1).
func (pc *Context) CubicTo(x1, y1, x2, y2, x3, y3 float32) {
if !pc.HasCurrent {
pc.MoveTo(x1, y1)
}
// x0, y0 := pc.Current.X, pc.Current.Y
b := pc.TransformPoint(x1, y1)
c := pc.TransformPoint(x2, y2)
d := pc.TransformPoint(x3, y3)
pc.Path.CubeBezier(b.ToFixed(), c.ToFixed(), d.ToFixed())
pc.Current = d
}
// ClosePath adds a line segment from the current point to the beginning
// of the current subpath. If there is no current point, this is a no-op.
func (pc *Context) ClosePath() {
if pc.HasCurrent {
pc.Path.Stop(true)
pc.Current = pc.Start
}
}
// ClearPath clears the current path. There is no current point after this
// operation.
func (pc *Context) ClearPath() {
pc.Path.Clear()
pc.HasCurrent = false
}
// NewSubPath starts a new subpath within the current path. There is no current
// point after this operation.
func (pc *Context) NewSubPath() {
// if pc.HasCurrent {
// pc.FillPath.Add1(pc.Start.Fixed())
// }
pc.HasCurrent = false
}
// Path Drawing
func (pc *Context) capfunc() raster.CapFunc {
switch pc.StrokeStyle.Cap {
case styles.LineCapButt:
return raster.ButtCap
case styles.LineCapRound:
return raster.RoundCap
case styles.LineCapSquare:
return raster.SquareCap
case styles.LineCapCubic:
return raster.CubicCap
case styles.LineCapQuadratic:
return raster.QuadraticCap
}
return nil
}
func (pc *Context) joinmode() raster.JoinMode {
switch pc.StrokeStyle.Join {
case styles.LineJoinMiter:
return raster.Miter
case styles.LineJoinMiterClip:
return raster.MiterClip
case styles.LineJoinRound:
return raster.Round
case styles.LineJoinBevel:
return raster.Bevel
case styles.LineJoinArcs:
return raster.Arc
case styles.LineJoinArcsClip:
return raster.ArcClip
}
return raster.Arc
}
// StrokeWidth obtains the current stoke width subject to transform (or not
// depending on VecEffNonScalingStroke)
func (pc *Context) StrokeWidth() float32 {
dw := pc.StrokeStyle.Width.Dots
if dw == 0 {
return dw
}
if pc.VectorEffect == styles.VectorEffectNonScalingStroke {
return dw
}
scx, scy := pc.CurrentTransform.ExtractScale()
sc := 0.5 * (math32.Abs(scx) + math32.Abs(scy))
lw := math32.Max(sc*dw, pc.StrokeStyle.MinWidth.Dots)
return lw
}
// StrokePreserve strokes the current path with the current color, line width,
// line cap, line join and dash settings. The path is preserved after this
// operation.
func (pc *Context) StrokePreserve() {
if pc.Raster == nil || pc.StrokeStyle.Color == nil {
return
}
dash := slices.Clone(pc.StrokeStyle.Dashes)
if dash != nil {
scx, scy := pc.CurrentTransform.ExtractScale()
sc := 0.5 * (math32.Abs(scx) + math32.Abs(scy))
for i := range dash {
dash[i] *= sc
}
}
pc.Raster.SetStroke(
math32.ToFixed(pc.StrokeWidth()),
math32.ToFixed(pc.StrokeStyle.MiterLimit),
pc.capfunc(), nil, nil, pc.joinmode(), // todo: supports leading / trailing caps, and "gaps"
dash, 0)
pc.Scanner.SetClip(pc.Bounds)
pc.Path.AddTo(pc.Raster)
fbox := pc.Raster.Scanner.GetPathExtent()
pc.LastRenderBBox = image.Rectangle{Min: image.Point{fbox.Min.X.Floor(), fbox.Min.Y.Floor()},
Max: image.Point{fbox.Max.X.Ceil(), fbox.Max.Y.Ceil()}}
if g, ok := pc.StrokeStyle.Color.(gradient.Gradient); ok {
g.Update(pc.StrokeStyle.Opacity, math32.B2FromRect(pc.LastRenderBBox), pc.CurrentTransform)
pc.Raster.SetColor(pc.StrokeStyle.Color)
} else {
if pc.StrokeStyle.Opacity < 1 {
pc.Raster.SetColor(gradient.ApplyOpacity(pc.StrokeStyle.Color, pc.StrokeStyle.Opacity))
} else {
pc.Raster.SetColor(pc.StrokeStyle.Color)
}
}
pc.Raster.Draw()
pc.Raster.Clear()
}
// Stroke strokes the current path with the current color, line width,
// line cap, line join and dash settings. The path is cleared after this
// operation.
func (pc *Context) Stroke() {
if pc.SVGOut != nil && pc.StrokeStyle.Color != nil {
io.WriteString(pc.SVGOut, pc.SVGPath())
}
pc.StrokePreserve()
pc.ClearPath()
}
// FillPreserve fills the current path with the current color. Open subpaths
// are implicitly closed. The path is preserved after this operation.
func (pc *Context) FillPreserve() {
if pc.Raster == nil || pc.FillStyle.Color == nil {
return
}
rf := &pc.Raster.Filler
rf.SetWinding(pc.FillStyle.Rule == styles.FillRuleNonZero)
pc.Scanner.SetClip(pc.Bounds)
pc.Path.AddTo(rf)
fbox := pc.Scanner.GetPathExtent()
pc.LastRenderBBox = image.Rectangle{Min: image.Point{fbox.Min.X.Floor(), fbox.Min.Y.Floor()},
Max: image.Point{fbox.Max.X.Ceil(), fbox.Max.Y.Ceil()}}
if g, ok := pc.FillStyle.Color.(gradient.Gradient); ok {
g.Update(pc.FillStyle.Opacity, math32.B2FromRect(pc.LastRenderBBox), pc.CurrentTransform)
rf.SetColor(pc.FillStyle.Color)
} else {
if pc.FillStyle.Opacity < 1 {
rf.SetColor(gradient.ApplyOpacity(pc.FillStyle.Color, pc.FillStyle.Opacity))
} else {
rf.SetColor(pc.FillStyle.Color)
}
}
rf.Draw()
rf.Clear()
}
// Fill fills the current path with the current color. Open subpaths
// are implicitly closed. The path is cleared after this operation.
func (pc *Context) Fill() {
if pc.SVGOut != nil {
io.WriteString(pc.SVGOut, pc.SVGPath())
}
pc.FillPreserve()
pc.ClearPath()
}
// FillBox performs an optimized fill of the given
// rectangular region with the given image.
func (pc *Context) FillBox(pos, size math32.Vector2, img image.Image) {
pc.DrawBox(pos, size, img, draw.Over)
}
// BlitBox performs an optimized overwriting fill (blit) of the given
// rectangular region with the given image.
func (pc *Context) BlitBox(pos, size math32.Vector2, img image.Image) {
pc.DrawBox(pos, size, img, draw.Src)
}
// DrawBox performs an optimized fill/blit of the given rectangular region
// with the given image, using the given draw operation.
func (pc *Context) DrawBox(pos, size math32.Vector2, img image.Image, op draw.Op) {
if img == nil {
img = colors.Uniform(color.RGBA{})
}
pos = pc.CurrentTransform.MulVector2AsPoint(pos)
size = pc.CurrentTransform.MulVector2AsVector(size)
b := pc.Bounds.Intersect(math32.RectFromPosSizeMax(pos, size))
if g, ok := img.(gradient.Gradient); ok {
g.Update(pc.FillStyle.Opacity, math32.B2FromRect(b), pc.CurrentTransform)
} else {
img = gradient.ApplyOpacity(img, pc.FillStyle.Opacity)
}
draw.Draw(pc.Image, b, img, b.Min, op)
}
// BlurBox blurs the given already drawn region with the given blur radius.
// The blur radius passed to this function is the actual Gaussian
// standard deviation (σ). This means that you need to divide a CSS-standard
// blur radius value by two before passing it this function
// (see https://stackoverflow.com/questions/65454183/how-does-blur-radius-value-in-box-shadow-property-affect-the-resulting-blur).
func (pc *Context) BlurBox(pos, size math32.Vector2, blurRadius float32) {
rect := math32.RectFromPosSizeMax(pos, size)
sub := pc.Image.SubImage(rect)
sub = GaussianBlur(sub, float64(blurRadius))
draw.Draw(pc.Image, rect, sub, rect.Min, draw.Src)
}
// ClipPreserve updates the clipping region by intersecting the current
// clipping region with the current path as it would be filled by pc.Fill().
// The path is preserved after this operation.
func (pc *Context) ClipPreserve() {
clip := image.NewAlpha(pc.Image.Bounds())
// painter := raster.NewAlphaOverPainter(clip) // todo!
pc.FillPreserve()
if pc.Mask == nil {
pc.Mask = clip
} else { // todo: this one operation MASSIVELY slows down clip usage -- unclear why
mask := image.NewAlpha(pc.Image.Bounds())
draw.DrawMask(mask, mask.Bounds(), clip, image.Point{}, pc.Mask, image.Point{}, draw.Over)
pc.Mask = mask
}
}
// SetMask allows you to directly set the *image.Alpha to be used as a clipping
// mask. It must be the same size as the context, else an error is returned
// and the mask is unchanged.
func (pc *Context) SetMask(mask *image.Alpha) error {
if mask.Bounds() != pc.Image.Bounds() {
return errors.New("mask size must match context size")
}
pc.Mask = mask
return nil
}
// AsMask returns an *image.Alpha representing the alpha channel of this
// context. This can be useful for advanced clipping operations where you first
// render the mask geometry and then use it as a mask.
func (pc *Context) AsMask() *image.Alpha {
b := pc.Image.Bounds()
mask := image.NewAlpha(b)
draw.Draw(mask, b, pc.Image, image.Point{}, draw.Src)
return mask
}
// Clip updates the clipping region by intersecting the current
// clipping region with the current path as it would be filled by pc.Fill().
// The path is cleared after this operation.
func (pc *Context) Clip() {
pc.ClipPreserve()
pc.ClearPath()
}
// ResetClip clears the clipping region.
func (pc *Context) ResetClip() {
pc.Mask = nil
}
//////////////////////////////////////////////////////////////////////////////////
// Convenient Drawing Functions
// Clear fills the entire image with the current fill color.
func (pc *Context) Clear() {
src := pc.FillStyle.Color
draw.Draw(pc.Image, pc.Image.Bounds(), src, image.Point{}, draw.Src)
}
// SetPixel sets the color of the specified pixel using the current stroke color.
func (pc *Context) SetPixel(x, y int) {
pc.Image.Set(x, y, pc.StrokeStyle.Color.At(x, y))
}
func (pc *Context) DrawLine(x1, y1, x2, y2 float32) {
pc.MoveTo(x1, y1)
pc.LineTo(x2, y2)
}
func (pc *Context) DrawPolyline(points []math32.Vector2) {
sz := len(points)
if sz < 2 {
return
}
pc.MoveTo(points[0].X, points[0].Y)
for i := 1; i < sz; i++ {
pc.LineTo(points[i].X, points[i].Y)
}
}
func (pc *Context) DrawPolylinePxToDots(points []math32.Vector2) {
pu := &pc.UnitContext
sz := len(points)
if sz < 2 {
return
}
pc.MoveTo(pu.PxToDots(points[0].X), pu.PxToDots(points[0].Y))
for i := 1; i < sz; i++ {
pc.LineTo(pu.PxToDots(points[i].X), pu.PxToDots(points[i].Y))
}
}
func (pc *Context) DrawPolygon(points []math32.Vector2) {
pc.DrawPolyline(points)
pc.ClosePath()
}
func (pc *Context) DrawPolygonPxToDots(points []math32.Vector2) {
pc.DrawPolylinePxToDots(points)
pc.ClosePath()
}
// DrawBorder is a higher-level function that draws, strokes, and fills
// an potentially rounded border box with the given position, size, and border styles.
func (pc *Context) DrawBorder(x, y, w, h float32, bs styles.Border) {
r := bs.Radius.Dots()
if styles.SidesAreSame(bs.Style) && styles.SidesAreSame(bs.Color) && styles.SidesAreSame(bs.Width.Dots().Sides) {
// set the color if it is not nil and the stroke style
// is not set to the correct color
if bs.Color.Top != nil && bs.Color.Top != pc.StrokeStyle.Color {
pc.StrokeStyle.Color = bs.Color.Top
}
pc.StrokeStyle.Width = bs.Width.Top
pc.StrokeStyle.ApplyBorderStyle(bs.Style.Top)
if styles.SidesAreZero(r.Sides) {
pc.DrawRectangle(x, y, w, h)
} else {
pc.DrawRoundedRectangle(x, y, w, h, r)
}
pc.FillStrokeClear()
return
}
// use consistent rounded rectangle for fill, and then draw borders side by side
pc.DrawRoundedRectangle(x, y, w, h, r)
pc.Fill()
r = ClampBorderRadius(r, w, h)
// position values
var (
xtl, ytl = x, y // top left
xtli, ytli = x + r.Top, y + r.Top // top left inset
xtr, ytr = x + w, y // top right
xtri, ytri = x + w - r.Right, y + r.Right // top right inset
xbr, ybr = x + w, y + h // bottom right
xbri, ybri = x + w - r.Bottom, y + h - r.Bottom // bottom right inset
xbl, ybl = x, y + h // bottom left
xbli, ybli = x + r.Left, y + h - r.Left // bottom left inset
)
// SidesTODO: need to figure out how to style rounded corners correctly
// (in CSS they are split in the middle between different border side styles)
pc.NewSubPath()
pc.MoveTo(xtli, ytl)
// set the color if it is not the same as the already set color
if bs.Color.Top != pc.StrokeStyle.Color {
pc.StrokeStyle.Color = bs.Color.Top
}
pc.StrokeStyle.Width = bs.Width.Top
pc.LineTo(xtri, ytr)
if r.Right != 0 {
pc.DrawArc(xtri, ytri, r.Right, math32.DegToRad(270), math32.DegToRad(360))
}
// if the color or width is changing for the next one, we have to stroke now
if bs.Color.Top != bs.Color.Right || bs.Width.Top.Dots != bs.Width.Right.Dots {
pc.Stroke()
pc.NewSubPath()
pc.MoveTo(xtr, ytri)
}
if bs.Color.Right != pc.StrokeStyle.Color {
pc.StrokeStyle.Color = bs.Color.Right
}
pc.StrokeStyle.Width = bs.Width.Right
pc.LineTo(xbr, ybri)
if r.Bottom != 0 {
pc.DrawArc(xbri, ybri, r.Bottom, math32.DegToRad(0), math32.DegToRad(90))
}
if bs.Color.Right != bs.Color.Bottom || bs.Width.Right.Dots != bs.Width.Bottom.Dots {
pc.Stroke()
pc.NewSubPath()
pc.MoveTo(xbri, ybr)
}
if bs.Color.Bottom != pc.StrokeStyle.Color {
pc.StrokeStyle.Color = bs.Color.Bottom
}
pc.StrokeStyle.Width = bs.Width.Bottom
pc.LineTo(xbli, ybl)
if r.Left != 0 {
pc.DrawArc(xbli, ybli, r.Left, math32.DegToRad(90), math32.DegToRad(180))
}
if bs.Color.Bottom != bs.Color.Left || bs.Width.Bottom.Dots != bs.Width.Left.Dots {
pc.Stroke()
pc.NewSubPath()
pc.MoveTo(xbl, ybli)
}
if bs.Color.Left != pc.StrokeStyle.Color {
pc.StrokeStyle.Color = bs.Color.Left
}
pc.StrokeStyle.Width = bs.Width.Left
pc.LineTo(xtl, ytli)
if r.Top != 0 {
pc.DrawArc(xtli, ytli, r.Top, math32.DegToRad(180), math32.DegToRad(270))
}
pc.LineTo(xtli, ytl)
pc.Stroke()
}
// ClampBorderRadius returns the given border radius clamped to fit based
// on the given width and height of the object.
func ClampBorderRadius(r styles.SideFloats, w, h float32) styles.SideFloats {
min := math32.Min(w/2, h/2)
r.Top = math32.Clamp(r.Top, 0, min)
r.Right = math32.Clamp(r.Right, 0, min)
r.Bottom = math32.Clamp(r.Bottom, 0, min)
r.Left = math32.Clamp(r.Left, 0, min)
return r
}
// DrawRectangle draws (but does not stroke or fill) a standard rectangle with a consistent border
func (pc *Context) DrawRectangle(x, y, w, h float32) {
pc.NewSubPath()
pc.MoveTo(x, y)
pc.LineTo(x+w, y)
pc.LineTo(x+w, y+h)
pc.LineTo(x, y+h)
pc.ClosePath()
}
// DrawRoundedRectangle draws a standard rounded rectangle
// with a consistent border and with the given x and y position,
// width and height, and border radius for each corner.
func (pc *Context) DrawRoundedRectangle(x, y, w, h float32, r styles.SideFloats) {
// clamp border radius values
min := math32.Min(w/2, h/2)
r.Top = math32.Clamp(r.Top, 0, min)
r.Right = math32.Clamp(r.Right, 0, min)
r.Bottom = math32.Clamp(r.Bottom, 0, min)
r.Left = math32.Clamp(r.Left, 0, min)
// position values; some variables are missing because they are unused
var (
xtl, ytl = x, y // top left
xtli, ytli = x + r.Top, y + r.Top // top left inset
ytr = y // top right
xtri, ytri = x + w - r.Right, y + r.Right // top right inset
xbr = x + w // bottom right
xbri, ybri = x + w - r.Bottom, y + h - r.Bottom // bottom right inset
ybl = y + h // bottom left
xbli, ybli = x + r.Left, y + h - r.Left // bottom left inset
)
// SidesTODO: need to figure out how to style rounded corners correctly
// (in CSS they are split in the middle between different border side styles)
pc.NewSubPath()
pc.MoveTo(xtli, ytl)
pc.LineTo(xtri, ytr)
if r.Right != 0 {
pc.DrawArc(xtri, ytri, r.Right, math32.DegToRad(270), math32.DegToRad(360))
}
pc.LineTo(xbr, ybri)
if r.Bottom != 0 {
pc.DrawArc(xbri, ybri, r.Bottom, math32.DegToRad(0), math32.DegToRad(90))
}
pc.LineTo(xbli, ybl)
if r.Left != 0 {
pc.DrawArc(xbli, ybli, r.Left, math32.DegToRad(90), math32.DegToRad(180))
}
pc.LineTo(xtl, ytli)
if r.Top != 0 {
pc.DrawArc(xtli, ytli, r.Top, math32.DegToRad(180), math32.DegToRad(270))
}
pc.ClosePath()
}
// DrawRoundedShadowBlur draws a standard rounded rectangle
// with a consistent border and with the given x and y position,
// width and height, and border radius for each corner.
// The blurSigma and radiusFactor args add a blurred shadow with
// an effective Gaussian sigma = blurSigma, and radius = radiusFactor * sigma.
// This shadow is rendered around the given box size up to given radius.
// See EdgeBlurFactors for underlying blur factor code.
// Using radiusFactor = 1 works well for weak shadows, where the fringe beyond
// 1 sigma is essentially invisible. To match the CSS standard, you then
// pass blurSigma = blur / 2, radiusFactor = 1. For darker shadows,
// use blurSigma = blur / 2, radiusFactor = 2, and reserve extra space for the full shadow.
// The effective blurRadius is clamped to be <= w-2 and h-2.
func (pc *Context) DrawRoundedShadowBlur(blurSigma, radiusFactor, x, y, w, h float32, r styles.SideFloats) {
if blurSigma <= 0 || radiusFactor <= 0 {
pc.DrawRoundedRectangle(x, y, w, h, r)
return
}
x = math32.Floor(x)
y = math32.Floor(y)
w = math32.Ceil(w)
h = math32.Ceil(h)
br := math32.Ceil(radiusFactor * blurSigma)
br = math32.Clamp(br, 1, w/2-2)
br = math32.Clamp(br, 1, h/2-2)
// radiusFactor = math32.Ceil(br / blurSigma)
radiusFactor = br / blurSigma
blurs := EdgeBlurFactors(blurSigma, radiusFactor)
origStroke := pc.StrokeStyle
origFill := pc.FillStyle
origOpacity := pc.FillStyle.Opacity
pc.StrokeStyle.Color = nil
pc.DrawRoundedRectangle(x+br, y+br, w-2*br, h-2*br, r)
pc.FillStrokeClear()
pc.StrokeStyle.Color = pc.FillStyle.Color
pc.FillStyle.Color = nil
pc.StrokeStyle.Width.Dots = 1.5 // 1.5 is the key number: 1 makes lines very transparent overall
for i, b := range blurs {
bo := br - float32(i)
pc.StrokeStyle.Opacity = b * origOpacity
pc.DrawRoundedRectangle(x+bo, y+bo, w-2*bo, h-2*bo, r)
pc.Stroke()
}
pc.StrokeStyle = origStroke
pc.FillStyle = origFill
}
// DrawEllipticalArc draws arc between angle1 and angle2 (radians)
// along an ellipse. Because the y axis points down, angles are clockwise,
// and the rendering draws segments progressing from angle1 to angle2
// using quadratic bezier curves -- centers of ellipse are at cx, cy with
// radii rx, ry -- see DrawEllipticalArcPath for a version compatible with SVG
// A/a path drawing, which uses previous position instead of two angles
func (pc *Context) DrawEllipticalArc(cx, cy, rx, ry, angle1, angle2 float32) {
const n = 16
for i := 0; i < n; i++ {
p1 := float32(i+0) / n
p2 := float32(i+1) / n
a1 := angle1 + (angle2-angle1)*p1
a2 := angle1 + (angle2-angle1)*p2
x0 := cx + rx*math32.Cos(a1)
y0 := cy + ry*math32.Sin(a1)
x1 := cx + rx*math32.Cos((a1+a2)/2)
y1 := cy + ry*math32.Sin((a1+a2)/2)
x2 := cx + rx*math32.Cos(a2)
y2 := cy + ry*math32.Sin(a2)
ncx := 2*x1 - x0/2 - x2/2
ncy := 2*y1 - y0/2 - y2/2
if i == 0 && !pc.HasCurrent {
pc.MoveTo(x0, y0)
}
pc.QuadraticTo(ncx, ncy, x2, y2)
}
}
// following ellipse path code is all directly from srwiley/oksvg
// MaxDx is the Maximum radians a cubic splice is allowed to span
// in ellipse parametric when approximating an off-axis ellipse.
const MaxDx float32 = math.Pi / 8
// ellipsePrime gives tangent vectors for parameterized ellipse; a, b, radii,
// eta parameter, center cx, cy
func ellipsePrime(a, b, sinTheta, cosTheta, eta, cx, cy float32) (px, py float32) {
bCosEta := b * math32.Cos(eta)
aSinEta := a * math32.Sin(eta)
px = -aSinEta*cosTheta - bCosEta*sinTheta
py = -aSinEta*sinTheta + bCosEta*cosTheta
return
}
// ellipsePointAt gives points for parameterized ellipse; a, b, radii, eta
// parameter, center cx, cy
func ellipsePointAt(a, b, sinTheta, cosTheta, eta, cx, cy float32) (px, py float32) {
aCosEta := a * math32.Cos(eta)
bSinEta := b * math32.Sin(eta)
px = cx + aCosEta*cosTheta - bSinEta*sinTheta
py = cy + aCosEta*sinTheta + bSinEta*cosTheta
return
}
// FindEllipseCenter locates the center of the Ellipse if it exists. If it
// does not exist, the radius values will be increased minimally for a
// solution to be possible while preserving the rx to rb ratio. rx and rb
// arguments are pointers that can be checked after the call to see if the
// values changed. This method uses coordinate transformations to reduce the
// problem to finding the center of a circle that includes the origin and an
// arbitrary point. The center of the circle is then transformed back to the
// original coordinates and returned.
func FindEllipseCenter(rx, ry *float32, rotX, startX, startY, endX, endY float32, sweep, largeArc bool) (cx, cy float32) {
cos, sin := math32.Cos(rotX), math32.Sin(rotX)
// Move origin to start point
nx, ny := endX-startX, endY-startY
// Rotate ellipse x-axis to coordinate x-axis
nx, ny = nx*cos+ny*sin, -nx*sin+ny*cos
// Scale X dimension so that rx = ry
nx *= *ry / *rx // Now the ellipse is a circle radius ry; therefore foci and center coincide
midX, midY := nx/2, ny/2
midlenSq := midX*midX + midY*midY
var hr float32 = 0.0
if *ry**ry < midlenSq {
// Requested ellipse does not exist; scale rx, ry to fit. Length of
// span is greater than max width of ellipse, must scale *rx, *ry
nry := math32.Sqrt(midlenSq)
if *rx == *ry {
*rx = nry // prevents roundoff
} else {
*rx = *rx * nry / *ry
}
*ry = nry
} else {
hr = math32.Sqrt(*ry**ry-midlenSq) / math32.Sqrt(midlenSq)
}
// Notice that if hr is zero, both answers are the same.
if (!sweep && !largeArc) || (sweep && largeArc) {
cx = midX + midY*hr
cy = midY - midX*hr
} else {
cx = midX - midY*hr
cy = midY + midX*hr
}
// reverse scale
cx *= *rx / *ry
// reverse rotate and translate back to original coordinates
return cx*cos - cy*sin + startX, cx*sin + cy*cos + startY
}
// DrawEllipticalArcPath is draws an arc centered at cx,cy with radii rx, ry, through
// given angle, either via the smaller or larger arc, depending on largeArc --
// returns in lx, ly the last points which are then set to the current cx, cy
// for the path drawer
func (pc *Context) DrawEllipticalArcPath(cx, cy, ocx, ocy, pcx, pcy, rx, ry, angle float32, largeArc, sweep bool) (lx, ly float32) {
rotX := angle * math.Pi / 180 // Convert degrees to radians
startAngle := math32.Atan2(pcy-cy, pcx-cx) - rotX
endAngle := math32.Atan2(ocy-cy, ocx-cx) - rotX
deltaTheta := endAngle - startAngle
arcBig := math32.Abs(deltaTheta) > math.Pi
// Approximate ellipse using cubic bezier splines
etaStart := math32.Atan2(math32.Sin(startAngle)/ry, math32.Cos(startAngle)/rx)
etaEnd := math32.Atan2(math32.Sin(endAngle)/ry, math32.Cos(endAngle)/rx)
deltaEta := etaEnd - etaStart
if (arcBig && !largeArc) || (!arcBig && largeArc) { // Go has no boolean XOR
if deltaEta < 0 {
deltaEta += math.Pi * 2
} else {
deltaEta -= math.Pi * 2
}
}
// This check might be needed if the center point of the ellipse is
// at the midpoint of the start and end lines.
if deltaEta < 0 && sweep {
deltaEta += math.Pi * 2
} else if deltaEta >= 0 && !sweep {
deltaEta -= math.Pi * 2
}
// Round up to determine number of cubic splines to approximate bezier curve
segs := int(math32.Abs(deltaEta)/MaxDx) + 1
dEta := deltaEta / float32(segs) // span of each segment
// Approximate the ellipse using a set of cubic bezier curves by the method of
// L. Maisonobe, "Drawing an elliptical arc using polylines, quadratic
// or cubic Bezier curves", 2003
// https://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
tde := math32.Tan(dEta / 2)
alpha := math32.Sin(dEta) * (math32.Sqrt(4+3*tde*tde) - 1) / 3 // Math is fun!
lx, ly = pcx, pcy
sinTheta, cosTheta := math32.Sin(rotX), math32.Cos(rotX)
ldx, ldy := ellipsePrime(rx, ry, sinTheta, cosTheta, etaStart, cx, cy)
for i := 1; i <= segs; i++ {
eta := etaStart + dEta*float32(i)
var px, py float32
if i == segs {
px, py = ocx, ocy // Just makes the end point exact; no roundoff error
} else {
px, py = ellipsePointAt(rx, ry, sinTheta, cosTheta, eta, cx, cy)
}
dx, dy := ellipsePrime(rx, ry, sinTheta, cosTheta, eta, cx, cy)
pc.CubicTo(lx+alpha*ldx, ly+alpha*ldy, px-alpha*dx, py-alpha*dy, px, py)
lx, ly, ldx, ldy = px, py, dx, dy
}
return lx, ly
}
// DrawEllipse draws an ellipse at the given position with the given radii.
func (pc *Context) DrawEllipse(x, y, rx, ry float32) {
pc.NewSubPath()
pc.DrawEllipticalArc(x, y, rx, ry, 0, 2*math32.Pi)
pc.ClosePath()
}
// DrawArc draws an arc at the given position with the given radius
// and angles in radians. Because the y axis points down, angles are clockwise,
// and the rendering draws segments progressing from angle1 to angle2
func (pc *Context) DrawArc(x, y, r, angle1, angle2 float32) {
pc.DrawEllipticalArc(x, y, r, r, angle1, angle2)
}
// DrawCircle draws a circle at the given position with the given radius.
func (pc *Context) DrawCircle(x, y, r float32) {
pc.NewSubPath()
pc.DrawEllipticalArc(x, y, r, r, 0, 2*math32.Pi)
pc.ClosePath()
}
// DrawRegularPolygon draws a regular polygon with the given number of sides
// at the given position with the given rotation.
func (pc *Context) DrawRegularPolygon(n int, x, y, r, rotation float32) {
angle := 2 * math32.Pi / float32(n)
rotation -= math32.Pi / 2
if n%2 == 0 {
rotation += angle / 2
}
pc.NewSubPath()
for i := 0; i < n; i++ {
a := rotation + angle*float32(i)
pc.LineTo(x+r*math32.Cos(a), y+r*math32.Sin(a))
}
pc.ClosePath()
}
// DrawImage draws the specified image at the specified point.
func (pc *Context) DrawImage(fmIm image.Image, x, y float32) {
pc.DrawImageAnchored(fmIm, x, y, 0, 0)
}
// DrawImageAnchored draws the specified image at the specified anchor point.
// The anchor point is x - w * ax, y - h * ay, where w, h is the size of the
// image. Use ax=0.5, ay=0.5 to center the image at the specified point.
func (pc *Context) DrawImageAnchored(fmIm image.Image, x, y, ax, ay float32) {
s := fmIm.Bounds().Size()
x -= ax * float32(s.X)
y -= ay * float32(s.Y)
transformer := draw.BiLinear
m := pc.CurrentTransform.Translate(x, y)
s2d := f64.Aff3{float64(m.XX), float64(m.XY), float64(m.X0), float64(m.YX), float64(m.YY), float64(m.Y0)}
if pc.Mask == nil {
transformer.Transform(pc.Image, s2d, fmIm, fmIm.Bounds(), draw.Over, nil)
} else {
transformer.Transform(pc.Image, s2d, fmIm, fmIm.Bounds(), draw.Over, &draw.Options{
DstMask: pc.Mask,
DstMaskP: image.Point{},
})
}
}
// DrawImageScaled draws the specified image starting at given upper-left point,
// such that the size of the image is rendered as specified by w, h parameters
// (an additional scaling is applied to the transform matrix used in rendering)
func (pc *Context) DrawImageScaled(fmIm image.Image, x, y, w, h float32) {
s := fmIm.Bounds().Size()
isz := math32.FromPoint(s)
isc := math32.Vec2(w, h).Div(isz)
transformer := draw.BiLinear
m := pc.CurrentTransform.Translate(x, y).Scale(isc.X, isc.Y)
s2d := f64.Aff3{float64(m.XX), float64(m.XY), float64(m.X0), float64(m.YX), float64(m.YY), float64(m.Y0)}
if pc.Mask == nil {
transformer.Transform(pc.Image, s2d, fmIm, fmIm.Bounds(), draw.Over, nil)
} else {
transformer.Transform(pc.Image, s2d, fmIm, fmIm.Bounds(), draw.Over, &draw.Options{
DstMask: pc.Mask,
DstMaskP: image.Point{},
})
}
}
//////////////////////////////////////////////////////////////////////////////////
// Transformation Matrix Operations
// Identity resets the current transformation matrix to the identity matrix.
// This results in no translating, scaling, rotating, or shearing.
func (pc *Context) Identity() {
pc.Transform = math32.Identity2()
}
// Translate updates the current matrix with a translation.
func (pc *Context) Translate(x, y float32) {
pc.Transform = pc.Transform.Translate(x, y)
}
// Scale updates the current matrix with a scaling factor.
// Scaling occurs about the origin.
func (pc *Context) Scale(x, y float32) {
pc.Transform = pc.Transform.Scale(x, y)
}
// ScaleAbout updates the current matrix with a scaling factor.
// Scaling occurs about the specified point.
func (pc *Context) ScaleAbout(sx, sy, x, y float32) {
pc.Translate(x, y)
pc.Scale(sx, sy)
pc.Translate(-x, -y)
}
// Rotate updates the current matrix with a clockwise rotation.
// Rotation occurs about the origin. Angle is specified in radians.
func (pc *Context) Rotate(angle float32) {
pc.Transform = pc.Transform.Rotate(angle)
}
// RotateAbout updates the current matrix with a clockwise rotation.
// Rotation occurs about the specified point. Angle is specified in radians.
func (pc *Context) RotateAbout(angle, x, y float32) {
pc.Translate(x, y)
pc.Rotate(angle)
pc.Translate(-x, -y)
}
// Shear updates the current matrix with a shearing angle.
// Shearing occurs about the origin.
func (pc *Context) Shear(x, y float32) {
pc.Transform = pc.Transform.Shear(x, y)
}
// ShearAbout updates the current matrix with a shearing angle.
// Shearing occurs about the specified point.
func (pc *Context) ShearAbout(sx, sy, x, y float32) {
pc.Translate(x, y)
pc.Shear(sx, sy)
pc.Translate(-x, -y)
}
// InvertY flips the Y axis so that Y grows from bottom to top and Y=0 is at
// the bottom of the image.
func (pc *Context) InvertY() {
pc.Translate(0, float32(pc.Image.Bounds().Size().Y))
pc.Scale(1, -1)
}
// 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.
// Based on https://github.com/srwiley/rasterx:
// Copyright 2018 by the rasterx Authors. All rights reserved.
// Created 2018 by S.R.Wiley
package raster
import (
"golang.org/x/image/math/fixed"
)
// Dasher struct extends the Stroker and can draw
// dashed lines with end capping
type Dasher struct {
Stroker
Dashes []fixed.Int26_6
DashPlace int
FirstDashIsGap bool
DashIsGap bool
DeltaDash fixed.Int26_6
DashOffset fixed.Int26_6
// Sgm allows us to switch between dashing
// and non-dashing rasterizers in the SetStroke function.
Sgm Raster
}
// NewDasher returns a Dasher ptr with default values.
// A Dasher has all of the capabilities of a Stroker, Filler, and Scanner, plus the ability
// to stroke curves with solid lines. Use SetStroke to configure with non-default
// values.
func NewDasher(width, height int, scanner Scanner) *Dasher {
r := new(Dasher)
r.Scanner = scanner
r.SetBounds(width, height)
r.SetWinding(true)
r.SetStroke(1*64, 4*64, ButtCap, nil, FlatGap, MiterClip, nil, 0)
r.Sgm = &r.Stroker
return r
}
// JoinF overides stroker JoinF during dashed stroking, because we need to slightly modify
// the the call as below to handle the case of the join being in a dash gap.
func (r *Dasher) JoinF() {
if len(r.Dashes) == 0 || !r.InStroke || !r.DashIsGap {
r.Stroker.JoinF()
}
}
// Start starts a dashed line
func (r *Dasher) Start(a fixed.Point26_6) {
// Advance dashPlace to the dashOffset start point and set deltaDash
if len(r.Dashes) > 0 {
r.DeltaDash = r.DashOffset
r.DashIsGap = false
r.DashPlace = 0
for r.DeltaDash > r.Dashes[r.DashPlace] {
r.DeltaDash -= r.Dashes[r.DashPlace]
r.DashIsGap = !r.DashIsGap
r.DashPlace++
if r.DashPlace == len(r.Dashes) {
r.DashPlace = 0
}
}
r.FirstDashIsGap = r.DashIsGap
}
r.Stroker.Start(a)
}
// LineF overides stroker LineF to modify the the call as below
// while performing the join in a dashed stroke.
func (r *Dasher) LineF(b fixed.Point26_6) {
var bnorm fixed.Point26_6
a := r.A // Copy local a since r.a is going to change during stroke operation
ba := b.Sub(a)
segLen := Length(ba)
var nlt fixed.Int26_6
if b == r.LeadPoint.P { // End of segment
bnorm = r.LeadPoint.TNorm // Use more accurate leadPoint tangent
} else {
bnorm = TurnPort90(ToLength(b.Sub(a), r.U)) // Intra segment normal
}
for segLen+r.DeltaDash > r.Dashes[r.DashPlace] {
nl := r.Dashes[r.DashPlace] - r.DeltaDash
nlt += nl
r.DashLineStrokeBit(a.Add(ToLength(ba, nlt)), bnorm, false)
r.DashIsGap = !r.DashIsGap
segLen -= nl
r.DeltaDash = 0
r.DashPlace++
if r.DashPlace == len(r.Dashes) {
r.DashPlace = 0
}
}
r.DeltaDash += segLen
r.DashLineStrokeBit(b, bnorm, true)
}
// SetStroke set the parameters for stroking a line. width is the width of the line, miterlimit is the miter cutoff
// value for miter, arc, miterclip and arcClip joinModes. CapL and CapT are the capping functions for leading and trailing
// line ends. If one is nil, the other function is used at both ends. gp is the gap function that determines how a
// gap on the convex side of two lines joining is filled. jm is the JoinMode for curve segments. Dashes is the values for
// the dash pattern. Pass in nil or an empty slice for no dashes. dashoffset is the starting offset into the dash array.
func (r *Dasher) SetStroke(width, miterLimit fixed.Int26_6, capL, capT CapFunc, gp GapFunc, jm JoinMode, dashes []float32, dashOffset float32) {
r.Stroker.SetStroke(width, miterLimit, capL, capT, gp, jm)
r.Dashes = r.Dashes[:0] // clear the dash array
if len(dashes) == 0 {
r.Sgm = &r.Stroker // This is just plain stroking
return
}
// Dashed Stroke
// Convert the float dash array and offset to fixed point and attach to the Filler
oneIsPos := false // Check to see if at least one dash is > 0
for _, v := range dashes {
fv := fixed.Int26_6(v * 64)
if fv <= 0 { // Negatives are considered 0s.
fv = 0
} else {
oneIsPos = true
}
r.Dashes = append(r.Dashes, fv)
}
if !oneIsPos {
r.Dashes = r.Dashes[:0]
r.Sgm = &r.Stroker // This is just plain stroking
return
}
r.DashOffset = fixed.Int26_6(dashOffset * 64)
r.Sgm = r // Use the full dasher
}
// Stop terminates a dashed line
func (r *Dasher) Stop(isClosed bool) {
if len(r.Dashes) == 0 {
r.Stroker.Stop(isClosed)
return
}
if !r.InStroke {
return
}
if isClosed && r.A != r.FirstP.P {
r.LineSeg(r.Sgm, r.FirstP.P)
}
ra := &r.Filler
if isClosed && !r.FirstDashIsGap && !r.DashIsGap { // closed connect w/o caps
a := r.A
r.FirstP.TNorm = r.LeadPoint.TNorm
r.FirstP.RT = r.LeadPoint.RT
r.FirstP.TTan = r.LeadPoint.TTan
ra.Start(r.FirstP.P.Sub(r.FirstP.TNorm))
ra.Line(a.Sub(r.Ln))
ra.Start(a.Add(r.Ln))
ra.Line(r.FirstP.P.Add(r.FirstP.TNorm))
r.Joiner(r.FirstP)
r.FirstP.BlackWidowMark(ra)
} else { // Cap open ends
if !r.DashIsGap {
r.CapL(ra, r.LeadPoint.P, r.LeadPoint.TNorm)
}
if !r.FirstDashIsGap {
r.CapT(ra, r.FirstP.P, Invert(r.FirstP.LNorm))
}
}
r.InStroke = false
}
// DashLineStrokeBit is a helper function that reduces code redundancy in the
// LineF function.
func (r *Dasher) DashLineStrokeBit(b, bnorm fixed.Point26_6, dontClose bool) {
if !r.DashIsGap { // Moving from dash to gap
a := r.A
ra := &r.Filler
ra.Start(b.Sub(bnorm))
ra.Line(a.Sub(r.Ln))
ra.Start(a.Add(r.Ln))
ra.Line(b.Add(bnorm))
if !dontClose {
r.CapL(ra, b, bnorm)
}
} else { // Moving from gap to dash
if !dontClose {
ra := &r.Filler
r.CapT(ra, b, Invert(bnorm))
}
}
r.A = b
r.Ln = bnorm
}
// Line for Dasher is here to pass the dasher sgm to LineP
func (r *Dasher) Line(b fixed.Point26_6) {
r.LineSeg(r.Sgm, b)
}
// QuadBezier for dashing
func (r *Dasher) QuadBezier(b, c fixed.Point26_6) {
r.QuadBezierf(r.Sgm, b, c)
}
// CubeBezier starts a stroked cubic bezier.
// It is a low level function exposed for the purposes of callbacks
// and debugging.
func (r *Dasher) CubeBezier(b, c, d fixed.Point26_6) {
r.CubeBezierf(r.Sgm, b, c, d)
}
// Code generated by "core generate"; DO NOT EDIT.
package raster
import (
"cogentcore.org/core/enums"
)
var _PathCommandValues = []PathCommand{0, 1, 2, 3, 4}
// PathCommandN is the highest valid value for type PathCommand, plus one.
const PathCommandN PathCommand = 5
var _PathCommandValueMap = map[string]PathCommand{`PathMoveTo`: 0, `PathLineTo`: 1, `PathQuadTo`: 2, `PathCubicTo`: 3, `PathClose`: 4}
var _PathCommandDescMap = map[PathCommand]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``}
var _PathCommandMap = map[PathCommand]string{0: `PathMoveTo`, 1: `PathLineTo`, 2: `PathQuadTo`, 3: `PathCubicTo`, 4: `PathClose`}
// String returns the string representation of this PathCommand value.
func (i PathCommand) String() string { return enums.String(i, _PathCommandMap) }
// SetString sets the PathCommand value from its string representation,
// and returns an error if the string is invalid.
func (i *PathCommand) SetString(s string) error {
return enums.SetString(i, s, _PathCommandValueMap, "PathCommand")
}
// Int64 returns the PathCommand value as an int64.
func (i PathCommand) Int64() int64 { return int64(i) }
// SetInt64 sets the PathCommand value from an int64.
func (i *PathCommand) SetInt64(in int64) { *i = PathCommand(in) }
// Desc returns the description of the PathCommand value.
func (i PathCommand) Desc() string { return enums.Desc(i, _PathCommandDescMap) }
// PathCommandValues returns all possible values for the type PathCommand.
func PathCommandValues() []PathCommand { return _PathCommandValues }
// Values returns all possible values for the type PathCommand.
func (i PathCommand) Values() []enums.Enum { return enums.Values(_PathCommandValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i PathCommand) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *PathCommand) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "PathCommand")
}
var _JoinModeValues = []JoinMode{0, 1, 2, 3, 4, 5}
// JoinModeN is the highest valid value for type JoinMode, plus one.
const JoinModeN JoinMode = 6
var _JoinModeValueMap = map[string]JoinMode{`Arc`: 0, `ArcClip`: 1, `Miter`: 2, `MiterClip`: 3, `Bevel`: 4, `Round`: 5}
var _JoinModeDescMap = map[JoinMode]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``, 5: ``}
var _JoinModeMap = map[JoinMode]string{0: `Arc`, 1: `ArcClip`, 2: `Miter`, 3: `MiterClip`, 4: `Bevel`, 5: `Round`}
// String returns the string representation of this JoinMode value.
func (i JoinMode) String() string { return enums.String(i, _JoinModeMap) }
// SetString sets the JoinMode value from its string representation,
// and returns an error if the string is invalid.
func (i *JoinMode) SetString(s string) error {
return enums.SetString(i, s, _JoinModeValueMap, "JoinMode")
}
// Int64 returns the JoinMode value as an int64.
func (i JoinMode) Int64() int64 { return int64(i) }
// SetInt64 sets the JoinMode value from an int64.
func (i *JoinMode) SetInt64(in int64) { *i = JoinMode(in) }
// Desc returns the description of the JoinMode value.
func (i JoinMode) Desc() string { return enums.Desc(i, _JoinModeDescMap) }
// JoinModeValues returns all possible values for the type JoinMode.
func JoinModeValues() []JoinMode { return _JoinModeValues }
// Values returns all possible values for the type JoinMode.
func (i JoinMode) Values() []enums.Enum { return enums.Values(_JoinModeValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i JoinMode) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *JoinMode) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "JoinMode") }
// 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.
// Based on https://github.com/srwiley/rasterx:
// Copyright 2018 by the rasterx Authors. All rights reserved.
// Created 2018 by S.R.Wiley
package raster
import (
"cogentcore.org/core/math32"
"golang.org/x/image/math/fixed"
)
// Filler is a filler that implements [Raster].
type Filler struct {
Scanner
A fixed.Point26_6
First fixed.Point26_6
}
// NewFiller returns a Filler ptr with default values.
// A Filler in addition to rasterizing lines like a Scann,
// can also rasterize quadratic and cubic bezier curves.
// If Scanner is nil default scanner ScannerGV is used
func NewFiller(width, height int, scanner Scanner) *Filler {
r := new(Filler)
r.Scanner = scanner
r.SetBounds(width, height)
r.SetWinding(true)
return r
}
// Start starts a new path at the given point.
func (r *Filler) Start(a fixed.Point26_6) {
r.A = a
r.First = a
r.Scanner.Start(a)
}
// Stop sends a path at the given point.
func (r *Filler) Stop(isClosed bool) {
if r.First != r.A {
r.Line(r.First)
}
}
// QuadBezier adds a quadratic segment to the current curve.
func (r *Filler) QuadBezier(b, c fixed.Point26_6) {
r.QuadBezierF(r, b, c)
}
// QuadTo flattens the quadratic Bezier curve into lines through the LineTo func
// This functions is adapted from the version found in
// golang.org/x/image/vector
func QuadTo(ax, ay, bx, by, cx, cy float32, LineTo func(dx, dy float32)) {
devsq := DevSquared(ax, ay, bx, by, cx, cy)
if devsq >= 0.333 {
const tol = 3
n := 1 + int(math32.Sqrt(math32.Sqrt(tol*float32(devsq))))
t, nInv := float32(0), 1/float32(n)
for i := 0; i < n-1; i++ {
t += nInv
mt := 1 - t
t1 := mt * mt
t2 := mt * t * 2
t3 := t * t
LineTo(
ax*t1+bx*t2+cx*t3,
ay*t1+by*t2+cy*t3)
}
}
LineTo(cx, cy)
}
// CubeTo flattens the cubic Bezier curve into lines through the LineTo func
// This functions is adapted from the version found in
// golang.org/x/image/vector
func CubeTo(ax, ay, bx, by, cx, cy, dx, dy float32, LineTo func(ex, ey float32)) {
devsq := DevSquared(ax, ay, bx, by, dx, dy)
if devsqAlt := DevSquared(ax, ay, cx, cy, dx, dy); devsq < devsqAlt {
devsq = devsqAlt
}
if devsq >= 0.333 {
const tol = 3
n := 1 + int(math32.Sqrt(math32.Sqrt(tol*float32(devsq))))
t, nInv := float32(0), 1/float32(n)
for i := 0; i < n-1; i++ {
t += nInv
tsq := t * t
mt := 1 - t
mtsq := mt * mt
t1 := mtsq * mt
t2 := mtsq * t * 3
t3 := mt * tsq * 3
t4 := tsq * t
LineTo(
ax*t1+bx*t2+cx*t3+dx*t4,
ay*t1+by*t2+cy*t3+dy*t4)
}
}
LineTo(dx, dy)
}
// DevSquared returns a measure of how curvy the sequence (ax, ay) to (bx, by)
// to (cx, cy) is. It determines how many line segments will approximate a
// Bézier curve segment. This functions is copied from the version found in
// golang.org/x/image/vector as are the below comments.
//
// http://lists.nongnu.org/archive/html/freetype-devel/2016-08/msg00080.html
// gives the rationale for this evenly spaced heuristic instead of a recursive
// de Casteljau approach:
//
// The reason for the subdivision by n is that I expect the "flatness"
// computation to be semi-expensive (it's done once rather than on each
// potential subdivision) and also because you'll often get fewer subdivisions.
// Taking a circular arc as a simplifying assumption (ie a spherical cow),
// where I get n, a recursive approach would get 2^⌈lg n⌉, which, if I haven't
// made any horrible mistakes, is expected to be 33% more in the limit.
func DevSquared(ax, ay, bx, by, cx, cy float32) float32 {
devx := ax - 2*bx + cx
devy := ay - 2*by + cy
return devx*devx + devy*devy
}
// QuadBezierF adds a quadratic segment to the sgm Rasterizer.
func (r *Filler) QuadBezierF(sgm Raster, b, c fixed.Point26_6) {
// check for degenerate bezier
if r.A == b || b == c {
sgm.Line(c)
return
}
sgm.JoinF()
QuadTo(float32(r.A.X), float32(r.A.Y), // Pts are x64, but does not matter.
float32(b.X), float32(b.Y),
float32(c.X), float32(c.Y),
func(dx, dy float32) {
sgm.LineF(fixed.Point26_6{X: fixed.Int26_6(dx), Y: fixed.Int26_6(dy)})
})
}
// CubeBezier adds a cubic bezier to the curve
func (r *Filler) CubeBezier(b, c, d fixed.Point26_6) {
r.CubeBezierF(r, b, c, d)
}
// JoinF is a no-op for a filling rasterizer. This is used in stroking and dashed
// stroking
func (r *Filler) JoinF() {
}
// Line for a filling rasterizer is just the line call in scan
func (r *Filler) Line(b fixed.Point26_6) {
r.LineF(b)
}
// LineF for a filling rasterizer is just the line call in scan
func (r *Filler) LineF(b fixed.Point26_6) {
r.Scanner.Line(b)
r.A = b
}
// CubeBezierF adds a cubic bezier to the curve. sending the line calls the the
// sgm Rasterizer
func (r *Filler) CubeBezierF(sgm Raster, b, c, d fixed.Point26_6) {
if (r.A == b && c == d) || (r.A == b && b == c) || (c == b && d == c) {
sgm.Line(d)
return
}
sgm.JoinF()
CubeTo(float32(r.A.X), float32(r.A.Y),
float32(b.X), float32(b.Y),
float32(c.X), float32(c.Y),
float32(d.X), float32(d.Y),
func(ex, ey float32) {
sgm.LineF(fixed.Point26_6{X: fixed.Int26_6(ex), Y: fixed.Int26_6(ey)})
})
}
// Clear resets the filler
func (r *Filler) Clear() {
r.A = fixed.Point26_6{}
r.First = r.A
r.Scanner.Clear()
}
// SetBounds sets the maximum width and height of the rasterized image and
// calls Clear. The width and height are in pixels, not fixed.Int26_6 units.
func (r *Filler) SetBounds(width, height int) {
if width < 0 {
width = 0
}
if height < 0 {
height = 0
}
r.Scanner.SetBounds(width, height)
r.Clear()
}
// 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.
// Based on https://github.com/srwiley/rasterx:
// Copyright 2018 by the rasterx Authors. All rights reserved.
// Created 2018 by S.R.Wiley
package raster
import (
"fmt"
"cogentcore.org/core/math32"
"golang.org/x/image/math/fixed"
)
// Invert returns the point inverted around the origin
func Invert(v fixed.Point26_6) fixed.Point26_6 {
return fixed.Point26_6{X: -v.X, Y: -v.Y}
}
// TurnStarboard90 returns the vector 90 degrees starboard (right in direction heading)
func TurnStarboard90(v fixed.Point26_6) fixed.Point26_6 {
return fixed.Point26_6{X: -v.Y, Y: v.X}
}
// TurnPort90 returns the vector 90 degrees port (left in direction heading)
func TurnPort90(v fixed.Point26_6) fixed.Point26_6 {
return fixed.Point26_6{X: v.Y, Y: -v.X}
}
// DotProd returns the inner product of p and q
func DotProd(p fixed.Point26_6, q fixed.Point26_6) fixed.Int52_12 {
return fixed.Int52_12(int64(p.X)*int64(q.X) + int64(p.Y)*int64(q.Y))
}
// Length is the distance from the origin of the point
func Length(v fixed.Point26_6) fixed.Int26_6 {
vx, vy := float32(v.X), float32(v.Y)
return fixed.Int26_6(math32.Sqrt(vx*vx + vy*vy))
}
// PathCommand is the type for the path command token
type PathCommand fixed.Int26_6 //enums:enum -no-extend
// Human readable path command constants
const (
PathMoveTo PathCommand = iota
PathLineTo
PathQuadTo
PathCubicTo
PathClose
)
// A Path starts with a PathCommand value followed by zero to three fixed
// int points.
type Path []fixed.Int26_6
// ToSVGPath returns a string representation of the path
func (p Path) ToSVGPath() string {
s := ""
for i := 0; i < len(p); {
if i != 0 {
s += " "
}
switch PathCommand(p[i]) {
case PathMoveTo:
s += fmt.Sprintf("M%4.3f,%4.3f", float32(p[i+1])/64, float32(p[i+2])/64)
i += 3
case PathLineTo:
s += fmt.Sprintf("L%4.3f,%4.3f", float32(p[i+1])/64, float32(p[i+2])/64)
i += 3
case PathQuadTo:
s += fmt.Sprintf("Q%4.3f,%4.3f,%4.3f,%4.3f", float32(p[i+1])/64, float32(p[i+2])/64,
float32(p[i+3])/64, float32(p[i+4])/64)
i += 5
case PathCubicTo:
s += "C" + fmt.Sprintf("C%4.3f,%4.3f,%4.3f,%4.3f,%4.3f,%4.3f", float32(p[i+1])/64, float32(p[i+2])/64,
float32(p[i+3])/64, float32(p[i+4])/64, float32(p[i+5])/64, float32(p[i+6])/64)
i += 7
case PathClose:
s += "Z"
i++
default:
panic("freetype/rasterx: bad pather")
}
}
return s
}
// String returns a readable representation of a Path.
func (p Path) String() string {
return p.ToSVGPath()
}
// Clear zeros the path slice
func (p *Path) Clear() {
*p = (*p)[:0]
}
// Start starts a new curve at the given point.
func (p *Path) Start(a fixed.Point26_6) {
*p = append(*p, fixed.Int26_6(PathMoveTo), a.X, a.Y)
}
// Line adds a linear segment to the current curve.
func (p *Path) Line(b fixed.Point26_6) {
*p = append(*p, fixed.Int26_6(PathLineTo), b.X, b.Y)
}
// QuadBezier adds a quadratic segment to the current curve.
func (p *Path) QuadBezier(b, c fixed.Point26_6) {
*p = append(*p, fixed.Int26_6(PathQuadTo), b.X, b.Y, c.X, c.Y)
}
// CubeBezier adds a cubic segment to the current curve.
func (p *Path) CubeBezier(b, c, d fixed.Point26_6) {
*p = append(*p, fixed.Int26_6(PathCubicTo), b.X, b.Y, c.X, c.Y, d.X, d.Y)
}
// Stop joins the ends of the path
func (p *Path) Stop(closeLoop bool) {
if closeLoop {
*p = append(*p, fixed.Int26_6(PathClose))
}
}
// AddTo adds the Path p to q.
func (p Path) AddTo(q Adder) {
for i := 0; i < len(p); {
switch PathCommand(p[i]) {
case PathMoveTo:
q.Stop(false) // Fixes issues #1 by described by Djadala; implicit close if currently in path.
q.Start(fixed.Point26_6{X: p[i+1], Y: p[i+2]})
i += 3
case PathLineTo:
q.Line(fixed.Point26_6{X: p[i+1], Y: p[i+2]})
i += 3
case PathQuadTo:
q.QuadBezier(fixed.Point26_6{X: p[i+1], Y: p[i+2]}, fixed.Point26_6{X: p[i+3], Y: p[i+4]})
i += 5
case PathCubicTo:
q.CubeBezier(fixed.Point26_6{X: p[i+1], Y: p[i+2]},
fixed.Point26_6{X: p[i+3], Y: p[i+4]}, fixed.Point26_6{X: p[i+5], Y: p[i+6]})
i += 7
case PathClose:
q.Stop(true)
i++
default:
panic("AddTo: bad path")
}
}
q.Stop(false)
}
// ToLength scales the point to the length indicated by ln
func ToLength(p fixed.Point26_6, ln fixed.Int26_6) (q fixed.Point26_6) {
if ln == 0 || (p.X == 0 && p.Y == 0) {
return
}
pX, pY := float32(p.X), float32(p.Y)
lnF := float32(ln)
pLen := math32.Sqrt(pX*pX + pY*pY)
qX, qY := pX*lnF/pLen, pY*lnF/pLen
q.X, q.Y = fixed.Int26_6(qX), fixed.Int26_6(qY)
return
}
// ClosestPortside returns the closest of p1 or p2 on the port side of the
// line from the bow to the stern. (port means left side of the direction you are heading)
// isIntersecting is just convienice to reduce code, and if false returns false, because p1 and p2 are not valid
func ClosestPortside(bow, stern, p1, p2 fixed.Point26_6, isIntersecting bool) (xt fixed.Point26_6, intersects bool) {
if !isIntersecting {
return
}
dir := bow.Sub(stern)
dp1 := p1.Sub(stern)
dp2 := p2.Sub(stern)
cp1 := dir.X*dp1.Y - dp1.X*dir.Y
cp2 := dir.X*dp2.Y - dp2.X*dir.Y
switch {
case cp1 < 0 && cp2 < 0:
return
case cp1 < 0 && cp2 >= 0:
return p2, true
case cp1 >= 0 && cp2 < 0:
return p1, true
default: // both points on port side
dirdot := DotProd(dir, dir)
// calculate vector rejections of dp1 and dp2 onto dir
h1 := dp1.Sub(dir.Mul(fixed.Int26_6((DotProd(dp1, dir) << 6) / dirdot)))
h2 := dp2.Sub(dir.Mul(fixed.Int26_6((DotProd(dp2, dir) << 6) / dirdot)))
// return point with smallest vector rejection; i.e. closest to dir line
if (h1.X*h1.X + h1.Y*h1.Y) > (h2.X*h2.X + h2.Y*h2.Y) {
return p2, true
}
return p1, true
}
}
// RadCurvature returns the curvature of a Bezier curve end point,
// given an end point, the two adjacent control points and the degree.
// The sign of the value indicates if the center of the osculating circle
// is left or right (port or starboard) of the curve in the forward direction.
func RadCurvature(p0, p1, p2 fixed.Point26_6, dm fixed.Int52_12) fixed.Int26_6 {
a, b := p2.Sub(p1), p1.Sub(p0)
abdot, bbdot := DotProd(a, b), DotProd(b, b)
h := a.Sub(b.Mul(fixed.Int26_6((abdot << 6) / bbdot))) // h is the vector rejection of a onto b
if h.X == 0 && h.Y == 0 { // points are co-linear
return 0
}
radCurve := fixed.Int26_6((fixed.Int52_12(a.X*a.X+a.Y*a.Y) * dm / fixed.Int52_12(Length(h)<<6)) >> 6)
if a.X*b.Y > b.X*a.Y { // xprod sign
return radCurve
}
return -radCurve
}
// CircleCircleIntersection calculates the points of intersection of
// two circles or returns with intersects == false if no such points exist.
func CircleCircleIntersection(ct, cl fixed.Point26_6, rt, rl fixed.Int26_6) (xt1, xt2 fixed.Point26_6, intersects bool) {
dc := cl.Sub(ct)
d := Length(dc)
// Check for solvability.
if d > (rt + rl) {
return // No solution. Circles do not intersect.
}
// check if d < abs(rt-rl)
if da := rt - rl; (da > 0 && d < da) || (da < 0 && d < -da) {
return // No solution. One circle is contained by the other.
}
rlf, rtf, df := float32(rl), float32(rt), float32(d)
af := (rtf*rtf - rlf*rlf + df*df) / df / 2.0
hfd := math32.Sqrt(rtf*rtf-af*af) / df
afd := af / df
rOffx, rOffy := float32(-dc.Y)*hfd, float32(dc.X)*hfd
p2x := float32(ct.X) + float32(dc.X)*afd
p2y := float32(ct.Y) + float32(dc.Y)*afd
xt1x, xt1y := p2x+rOffx, p2y+rOffy
xt2x, xt2y := p2x-rOffx, p2y-rOffy
return fixed.Point26_6{X: fixed.Int26_6(xt1x), Y: fixed.Int26_6(xt1y)},
fixed.Point26_6{X: fixed.Int26_6(xt2x), Y: fixed.Int26_6(xt2y)}, true
}
// CalcIntersect calculates the points of intersection of two fixed point lines
// and panics if the determinate is zero. You have been warned.
func CalcIntersect(a1, a2, b1, b2 fixed.Point26_6) (x fixed.Point26_6) {
da, db, ds := a2.Sub(a1), b2.Sub(b1), a1.Sub(b1)
det := float32(da.X*db.Y - db.X*da.Y) // Determinate
t := float32(ds.Y*db.X-ds.X*db.Y) / det
x = a1.Add(fixed.Point26_6{X: fixed.Int26_6(float32(da.X) * t), Y: fixed.Int26_6(float32(da.Y) * t)})
return
}
// RayCircleIntersection calculates the points of intersection of
// a ray starting at s2 passing through s1 and a circle in fixed point.
// Returns intersects == false if no solution is possible. If two
// solutions are possible, the point closest to s2 is returned
func RayCircleIntersection(s1, s2, c fixed.Point26_6, r fixed.Int26_6) (x fixed.Point26_6, intersects bool) {
fx, fy, intersects := RayCircleIntersectionF(float32(s1.X), float32(s1.Y),
float32(s2.X), float32(s2.Y), float32(c.X), float32(c.Y), float32(r))
return fixed.Point26_6{X: fixed.Int26_6(fx),
Y: fixed.Int26_6(fy)}, intersects
}
// RayCircleIntersectionF calculates in floating point the points of intersection of
// a ray starting at s2 passing through s1 and a circle in fixed point.
// Returns intersects == false if no solution is possible. If two
// solutions are possible, the point closest to s2 is returned
func RayCircleIntersectionF(s1X, s1Y, s2X, s2Y, cX, cY, r float32) (x, y float32, intersects bool) {
n := s2X - cX // Calculating using 64* rather than divide
m := s2Y - cY
e := s2X - s1X
d := s2Y - s1Y
// Quadratic normal form coefficients
A, B, C := e*e+d*d, -2*(e*n+m*d), n*n+m*m-r*r
D := B*B - 4*A*C
if D <= 0 {
return // No intersection or is tangent
}
D = math32.Sqrt(D)
t1, t2 := (-B+D)/(2*A), (-B-D)/(2*A)
p1OnSide := t1 > 0
p2OnSide := t2 > 0
switch {
case p1OnSide && p2OnSide:
if t2 < t1 { // both on ray, use closest to s2
t1 = t2
}
case p2OnSide: // Only p2 on ray
t1 = t2
case p1OnSide: // only p1 on ray
default: // Neither solution is on the ray
return
}
return (n - e*t1) + cX, (m - d*t1) + cY, true
}
// MatrixAdder is an adder that applies matrix M to all points
type MatrixAdder struct {
Adder
M math32.Matrix2
}
// Reset sets the matrix M to identity
func (t *MatrixAdder) Reset() {
t.M = math32.Identity2()
}
// Start starts a new path
func (t *MatrixAdder) Start(a fixed.Point26_6) {
t.Adder.Start(t.M.MulFixedAsPoint(a))
}
// Line adds a linear segment to the current curve.
func (t *MatrixAdder) Line(b fixed.Point26_6) {
t.Adder.Line(t.M.MulFixedAsPoint(b))
}
// QuadBezier adds a quadratic segment to the current curve.
func (t *MatrixAdder) QuadBezier(b, c fixed.Point26_6) {
t.Adder.QuadBezier(t.M.MulFixedAsPoint(b), t.M.MulFixedAsPoint(c))
}
// CubeBezier adds a cubic segment to the current curve.
func (t *MatrixAdder) CubeBezier(b, c, d fixed.Point26_6) {
t.Adder.CubeBezier(t.M.MulFixedAsPoint(b), t.M.MulFixedAsPoint(c), t.M.MulFixedAsPoint(d))
}
// 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.
// Based on https://github.com/srwiley/rasterx:
// Copyright 2018 by the rasterx Authors. All rights reserved.
// Created 2018 by S.R.Wiley
package raster
import (
"cogentcore.org/core/math32"
"golang.org/x/image/math/fixed"
)
// MaxDx is the Maximum radians a cubic splice is allowed to span
// in ellipse parametric when approximating an off-axis ellipse.
const MaxDx float32 = math32.Pi / 8
// ToFixedP converts two floats to a fixed point.
func ToFixedP(x, y float32) (p fixed.Point26_6) {
p.X = fixed.Int26_6(x * 64)
p.Y = fixed.Int26_6(y * 64)
return
}
// AddCircle adds a circle to the Adder p
func AddCircle(cx, cy, r float32, p Adder) {
AddEllipse(cx, cy, r, r, 0, p)
}
// AddEllipse adds an elipse with center at cx,cy, with the indicated
// x and y radius, (rx, ry), rotated around the center by rot degrees.
func AddEllipse(cx, cy, rx, ry, rot float32, p Adder) {
rotRads := rot * math32.Pi / 180
pt := math32.Identity2().Translate(cx, cy).Rotate(rotRads).Translate(-cx, -cy).MulVector2AsPoint(math32.Vec2(cx+rx, cy))
points := []float32{rx, ry, rot, 1.0, 0.0, pt.X, pt.Y}
p.Start(pt.ToFixed())
AddArc(points, cx, cy, pt.X, pt.Y, p)
p.Stop(true)
}
// AddRect adds a rectangle of the indicated size, rotated
// around the center by rot degrees.
func AddRect(minX, minY, maxX, maxY, rot float32, p Adder) {
rot *= math32.Pi / 180
cx, cy := (minX+maxX)/2, (minY+maxY)/2
m := math32.Identity2().Translate(cx, cy).Rotate(rot).Translate(-cx, -cy)
q := &MatrixAdder{M: m, Adder: p}
q.Start(ToFixedP(minX, minY))
q.Line(ToFixedP(maxX, minY))
q.Line(ToFixedP(maxX, maxY))
q.Line(ToFixedP(minX, maxY))
q.Stop(true)
}
// AddRoundRect adds a rectangle of the indicated size, rotated
// around the center by rot degrees with rounded corners of radius
// rx in the x axis and ry in the y axis. gf specifes the shape of the
// filleting function. Valid values are RoundGap, QuadraticGap, CubicGap,
// FlatGap, or nil which defaults to a flat gap.
func AddRoundRect(minX, minY, maxX, maxY, rx, ry, rot float32, gf GapFunc, p Adder) {
if rx <= 0 || ry <= 0 {
AddRect(minX, minY, maxX, maxY, rot, p)
return
}
rot *= math32.Pi / 180
if gf == nil {
gf = FlatGap
}
w := maxX - minX
if w < rx*2 {
rx = w / 2
}
h := maxY - minY
if h < ry*2 {
ry = h / 2
}
stretch := rx / ry
midY := minY + h/2
m := math32.Identity2().Translate(minX+w/2, midY).Rotate(rot).Scale(1, 1/stretch).Translate(-minX-w/2, -minY-h/2)
maxY = midY + h/2*stretch
minY = midY - h/2*stretch
q := &MatrixAdder{M: m, Adder: p}
q.Start(ToFixedP(minX+rx, minY))
q.Line(ToFixedP(maxX-rx, minY))
gf(q, ToFixedP(maxX-rx, minY+rx), ToFixedP(0, -rx), ToFixedP(rx, 0))
q.Line(ToFixedP(maxX, maxY-rx))
gf(q, ToFixedP(maxX-rx, maxY-rx), ToFixedP(rx, 0), ToFixedP(0, rx))
q.Line(ToFixedP(minX+rx, maxY))
gf(q, ToFixedP(minX+rx, maxY-rx), ToFixedP(0, rx), ToFixedP(-rx, 0))
q.Line(ToFixedP(minX, minY+rx))
gf(q, ToFixedP(minX+rx, minY+rx), ToFixedP(-rx, 0), ToFixedP(0, -rx))
q.Stop(true)
}
// AddArc adds an arc to the adder p
func AddArc(points []float32, cx, cy, px, py float32, p Adder) (lx, ly float32) {
rotX := points[2] * math32.Pi / 180 // Convert degress to radians
largeArc := points[3] != 0
sweep := points[4] != 0
startAngle := math32.Atan2(py-cy, px-cx) - rotX
endAngle := math32.Atan2(points[6]-cy, points[5]-cx) - rotX
deltaTheta := endAngle - startAngle
arcBig := math32.Abs(deltaTheta) > math32.Pi
// Approximate ellipse using cubic bezeir splines
etaStart := math32.Atan2(math32.Sin(startAngle)/points[1], math32.Cos(startAngle)/points[0])
etaEnd := math32.Atan2(math32.Sin(endAngle)/points[1], math32.Cos(endAngle)/points[0])
deltaEta := etaEnd - etaStart
if (arcBig && !largeArc) || (!arcBig && largeArc) { // Go has no boolean XOR
if deltaEta < 0 {
deltaEta += math32.Pi * 2
} else {
deltaEta -= math32.Pi * 2
}
}
// This check might be needed if the center point of the ellipse is
// at the midpoint of the start and end lines.
if deltaEta < 0 && sweep {
deltaEta += math32.Pi * 2
} else if deltaEta >= 0 && !sweep {
deltaEta -= math32.Pi * 2
}
// Round up to determine number of cubic splines to approximate bezier curve
segs := int(math32.Abs(deltaEta)/MaxDx) + 1
dEta := deltaEta / float32(segs) // span of each segment
// Approximate the ellipse using a set of cubic bezier curves by the method of
// L. Maisonobe, "Drawing an elliptical arc using polylines, quadratic
// or cubic Bezier curves", 2003
// https://www.spaceroots.org/documents/elllipse/elliptical-arc.pdf
tde := math32.Tan(dEta / 2)
alpha := math32.Sin(dEta) * (math32.Sqrt(4+3*tde*tde) - 1) / 3 // math32 is fun!
lx, ly = px, py
sinTheta, cosTheta := math32.Sin(rotX), math32.Cos(rotX)
ldx, ldy := EllipsePrime(points[0], points[1], sinTheta, cosTheta, etaStart, cx, cy)
for i := 1; i <= segs; i++ {
eta := etaStart + dEta*float32(i)
var px, py float32
if i == segs {
px, py = points[5], points[6] // Just makes the end point exact; no roundoff error
} else {
px, py = EllipsePointAt(points[0], points[1], sinTheta, cosTheta, eta, cx, cy)
}
dx, dy := EllipsePrime(points[0], points[1], sinTheta, cosTheta, eta, cx, cy)
p.CubeBezier(ToFixedP(lx+alpha*ldx, ly+alpha*ldy),
ToFixedP(px-alpha*dx, py-alpha*dy), ToFixedP(px, py))
lx, ly, ldx, ldy = px, py, dx, dy
}
return lx, ly
}
// EllipsePrime gives tangent vectors for parameterized ellipse; a, b, radii, eta parameter, center cx, cy
func EllipsePrime(a, b, sinTheta, cosTheta, eta, cx, cy float32) (px, py float32) {
bCosEta := b * math32.Cos(eta)
aSinEta := a * math32.Sin(eta)
px = -aSinEta*cosTheta - bCosEta*sinTheta
py = -aSinEta*sinTheta + bCosEta*cosTheta
return
}
// EllipsePointAt gives points for parameterized ellipse; a, b, radii, eta parameter, center cx, cy
func EllipsePointAt(a, b, sinTheta, cosTheta, eta, cx, cy float32) (px, py float32) {
aCosEta := a * math32.Cos(eta)
bSinEta := b * math32.Sin(eta)
px = cx + aCosEta*cosTheta - bSinEta*sinTheta
py = cy + aCosEta*sinTheta + bSinEta*cosTheta
return
}
// FindEllipseCenter locates the center of the Ellipse if it exists. If it does not exist,
// the radius values will be increased minimally for a solution to be possible
// while preserving the ra to rb ratio. ra and rb arguments are pointers that can be
// checked after the call to see if the values changed. This method uses coordinate transformations
// to reduce the problem to finding the center of a circle that includes the origin
// and an arbitrary point. The center of the circle is then transformed
// back to the original coordinates and returned.
func FindEllipseCenter(ra, rb *float32, rotX, startX, startY, endX, endY float32, sweep, smallArc bool) (cx, cy float32) {
cos, sin := math32.Cos(rotX), math32.Sin(rotX)
// Move origin to start point
nx, ny := endX-startX, endY-startY
// Rotate ellipse x-axis to coordinate x-axis
nx, ny = nx*cos+ny*sin, -nx*sin+ny*cos
// Scale X dimension so that ra = rb
nx *= *rb / *ra // Now the ellipse is a circle radius rb; therefore foci and center coincide
midX, midY := nx/2, ny/2
midlenSq := midX*midX + midY*midY
var hr float32
if *rb**rb < midlenSq {
// Requested ellipse does not exist; scale ra, rb to fit. Length of
// span is greater than max width of ellipse, must scale *ra, *rb
nrb := math32.Sqrt(midlenSq)
if *ra == *rb {
*ra = nrb // prevents roundoff
} else {
*ra = *ra * nrb / *rb
}
*rb = nrb
} else {
hr = math32.Sqrt(*rb**rb-midlenSq) / math32.Sqrt(midlenSq)
}
// Notice that if hr is zero, both answers are the same.
if (sweep && smallArc) || (!sweep && !smallArc) {
cx = midX + midY*hr
cy = midY - midX*hr
} else {
cx = midX - midY*hr
cy = midY + midX*hr
}
// reverse scale
cx *= *ra / *rb
//Reverse rotate and translate back to original coordinates
return cx*cos - cy*sin + startX, cx*sin + cy*cos + startY
}
// 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.
// Based on https://github.com/srwiley/rasterx:
// Copyright 2018 by the rasterx Authors. All rights reserved.
// Created 2018 by S.R.Wiley
package raster
import (
"cogentcore.org/core/math32"
"golang.org/x/image/math/fixed"
)
// Stroker does everything a [Filler] does, but
// also allows for stroking and dashed stroking in addition to
// filling
type Stroker struct {
Filler
// Trailing cap function
CapT CapFunc
// Leading cpa function
CapL CapFunc
// When gap appears between segments, this function is called
JoinGap GapFunc
// Tracks progress of the stroke
FirstP C2Point
// Tracks progress of the stroke
TrailPoint C2Point
// Tracks progress of the stroke
LeadPoint C2Point
// last normal of intra-seg connection.
Ln fixed.Point26_6
// U is the half-width of the stroke.
U fixed.Int26_6
MLimit fixed.Int26_6
JoinMode JoinMode
InStroke bool
}
// NewStroker returns a ptr to a Stroker with default values.
// A Stroker has all of the capabilities of a Filler and Scanner, plus the ability
// to stroke curves with solid lines. Use SetStroke to configure with non-default
// values.
func NewStroker(width, height int, scanner Scanner) *Stroker {
r := new(Stroker)
r.Scanner = scanner
r.SetBounds(width, height)
//Defaults for stroking
r.SetWinding(true)
r.U = 2 << 6
r.MLimit = 4 << 6
r.JoinMode = MiterClip
r.JoinGap = RoundGap
r.CapL = RoundCap
r.CapT = RoundCap
r.SetStroke(1<<6, 4<<6, ButtCap, nil, FlatGap, MiterClip)
return r
}
// CapFunc defines a function that draws caps on the ends of lines
type CapFunc func(p Adder, a, eNorm fixed.Point26_6)
// GapFunc defines a function to bridge gaps when the miter limit is
// exceeded
type GapFunc func(p Adder, a, tNorm, lNorm fixed.Point26_6)
// C2Point represents a point that connects two stroke segments
// and holds the tangent, normal and radius of curvature
// of the trailing and leading segments in fixed point values.
type C2Point struct {
P, TTan, LTan, TNorm, LNorm fixed.Point26_6
RT, RL fixed.Int26_6
}
// JoinMode type to specify how segments join.
type JoinMode int32 //enums:enum
// JoinMode constants determine how stroke segments bridge the gap at a join
// ArcClip mode is like MiterClip applied to arcs, and is not part of the SVG2.0
// standard.
const (
Arc JoinMode = iota
ArcClip
Miter
MiterClip
Bevel
Round
)
const (
// Number of cubic beziers to approx half a circle
CubicsPerHalfCircle = 8
// 1/4 in fixed point
EpsilonFixed = fixed.Int26_6(16)
// fixed point t parameterization shift factor;
// (2^this)/64 is the max length of t for fixed.Int26_6
TStrokeShift = 14
)
// SetStroke set the parameters for stroking a line. width is the width of the line, miterlimit is the miter cutoff
// value for miter, arc, miterclip and arcClip joinModes. CapL and CapT are the capping functions for leading and trailing
// line ends. If one is nil, the other function is used at both ends. If both are nil, both ends are ButtCapped.
// gp is the gap function that determines how a gap on the convex side of two joining lines is filled. jm is the JoinMode
// for curve segments.
func (r *Stroker) SetStroke(width, miterLimit fixed.Int26_6, capL, capT CapFunc, gp GapFunc, jm JoinMode) {
r.U = width / 2
r.CapL = capL
r.CapT = capT
r.JoinMode = jm
r.JoinGap = gp
r.MLimit = (r.U * miterLimit) >> 6
if r.CapT == nil {
if r.CapL == nil {
r.CapT = ButtCap
} else {
r.CapT = r.CapL
}
}
if r.CapL == nil {
r.CapL = r.CapT
}
if gp == nil {
if r.JoinMode == Round {
r.JoinGap = RoundGap
} else {
r.JoinGap = FlatGap
}
}
}
// GapToCap is a utility that converts a CapFunc to GapFunc
func GapToCap(p Adder, a, eNorm fixed.Point26_6, gf GapFunc) {
p.Start(a.Add(eNorm))
gf(p, a, eNorm, Invert(eNorm))
p.Line(a.Sub(eNorm))
}
var (
// ButtCap caps lines with a straight line
ButtCap CapFunc = func(p Adder, a, eNorm fixed.Point26_6) {
p.Start(a.Add(eNorm))
p.Line(a.Sub(eNorm))
}
// SquareCap caps lines with a square which is slightly longer than ButtCap
SquareCap CapFunc = func(p Adder, a, eNorm fixed.Point26_6) {
tpt := a.Add(TurnStarboard90(eNorm))
p.Start(a.Add(eNorm))
p.Line(tpt.Add(eNorm))
p.Line(tpt.Sub(eNorm))
p.Line(a.Sub(eNorm))
}
// RoundCap caps lines with a half-circle
RoundCap CapFunc = func(p Adder, a, eNorm fixed.Point26_6) {
GapToCap(p, a, eNorm, RoundGap)
}
// CubicCap caps lines with a cubic bezier
CubicCap CapFunc = func(p Adder, a, eNorm fixed.Point26_6) {
GapToCap(p, a, eNorm, CubicGap)
}
// QuadraticCap caps lines with a quadratic bezier
QuadraticCap CapFunc = func(p Adder, a, eNorm fixed.Point26_6) {
GapToCap(p, a, eNorm, QuadraticGap)
}
// Gap functions
// FlatGap bridges miter-limit gaps with a straight line
FlatGap GapFunc = func(p Adder, a, tNorm, lNorm fixed.Point26_6) {
p.Line(a.Add(lNorm))
}
// RoundGap bridges miter-limit gaps with a circular arc
RoundGap GapFunc = func(p Adder, a, tNorm, lNorm fixed.Point26_6) {
StrokeArc(p, a, a.Add(tNorm), a.Add(lNorm), true, 0, 0, p.Line)
p.Line(a.Add(lNorm)) // just to be sure line joins cleanly,
// last pt in stoke arc may not be precisely s2
}
// CubicGap bridges miter-limit gaps with a cubic bezier
CubicGap GapFunc = func(p Adder, a, tNorm, lNorm fixed.Point26_6) {
p.CubeBezier(a.Add(tNorm).Add(TurnStarboard90(tNorm)), a.Add(lNorm).Add(TurnPort90(lNorm)), a.Add(lNorm))
}
// QuadraticGap bridges miter-limit gaps with a quadratic bezier
QuadraticGap GapFunc = func(p Adder, a, tNorm, lNorm fixed.Point26_6) {
c1, c2 := a.Add(tNorm).Add(TurnStarboard90(tNorm)), a.Add(lNorm).Add(TurnPort90(lNorm))
cm := c1.Add(c2).Mul(fixed.Int26_6(1 << 5))
p.QuadBezier(cm, a.Add(lNorm))
}
)
// StrokeArc strokes a circular arc by approximation with bezier curves
func StrokeArc(p Adder, a, s1, s2 fixed.Point26_6, clockwise bool, trimStart,
trimEnd fixed.Int26_6, firstPoint func(p fixed.Point26_6)) (ps1, ds1, ps2, ds2 fixed.Point26_6) {
// Approximate the circular arc using a set of cubic bezier curves by the method of
// L. Maisonobe, "Drawing an elliptical arc using polylines, quadratic
// or cubic Bezier curves", 2003
// https://www.spaceroots.org/documents/elllipse/elliptical-arc.pdf
// The method was simplified for circles.
theta1 := math32.Atan2(float32(s1.Y-a.Y), float32(s1.X-a.X))
theta2 := math32.Atan2(float32(s2.Y-a.Y), float32(s2.X-a.X))
if !clockwise {
for theta1 < theta2 {
theta1 += math32.Pi * 2
}
} else {
for theta2 < theta1 {
theta2 += math32.Pi * 2
}
}
deltaTheta := theta2 - theta1
if trimStart > 0 {
ds := (deltaTheta * float32(trimStart)) / float32(1<<TStrokeShift)
deltaTheta -= ds
theta1 += ds
}
if trimEnd > 0 {
ds := (deltaTheta * float32(trimEnd)) / float32(1<<TStrokeShift)
deltaTheta -= ds
}
segs := int(math32.Abs(deltaTheta)/(math32.Pi/CubicsPerHalfCircle)) + 1
dTheta := deltaTheta / float32(segs)
tde := math32.Tan(dTheta / 2)
alpha := fixed.Int26_6(math32.Sin(dTheta) * (math32.Sqrt(4+3*tde*tde) - 1) * (64.0 / 3.0)) // math32 is fun!
r := float32(Length(s1.Sub(a))) // Note r is *64
ldp := fixed.Point26_6{X: -fixed.Int26_6(r * math32.Sin(theta1)), Y: fixed.Int26_6(r * math32.Cos(theta1))}
ds1 = ldp
ps1 = fixed.Point26_6{X: a.X + ldp.Y, Y: a.Y - ldp.X}
firstPoint(ps1)
s1 = ps1
for i := 1; i <= segs; i++ {
eta := theta1 + dTheta*float32(i)
ds2 = fixed.Point26_6{X: -fixed.Int26_6(r * math32.Sin(eta)), Y: fixed.Int26_6(r * math32.Cos(eta))}
ps2 = fixed.Point26_6{X: a.X + ds2.Y, Y: a.Y - ds2.X} // Using deriviative to calc new pt, because circle
p1 := s1.Add(ldp.Mul(alpha))
p2 := ps2.Sub(ds2.Mul(alpha))
p.CubeBezier(p1, p2, ps2)
s1, ldp = ps2, ds2
}
return
}
// 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) {
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),
TTan: Invert(p.LTan), LTan: Invert(p.TTan), RT: -p.RL, RL: -p.RT}, -crossProd)
// stroke top edge
r.StrokeEdge(p, crossProd)
}
// StrokeEdge reduces code redundancy in the Joiner function by 2x since it handles
// the top and bottom edges. This function encodes most of the logic of how to
// handle joins between the given C2Point point p, and the end of the line.
func (r *Stroker) StrokeEdge(p C2Point, crossProd fixed.Int26_6) {
ra := &r.Filler
s1, s2 := p.P.Add(p.TNorm), p.P.Add(p.LNorm) // Bevel points for top leading and trailing
ra.Start(s1)
if crossProd > -EpsilonFixed*EpsilonFixed { // Almost co-linear or convex
ra.Line(s2)
return // No need to fill any gaps
}
var ct, cl fixed.Point26_6 // Center of curvature trailing, leading
var rt, rl fixed.Int26_6 // Radius of curvature trailing, leading
// Adjust radiuses for stroke width
if r.JoinMode == Arc || r.JoinMode == ArcClip {
// Find centers of radius of curvature and adjust the radius to be drawn
// by half the stroke width.
if p.RT != 0 {
if p.RT > 0 {
ct = p.P.Add(ToLength(TurnPort90(p.TTan), p.RT))
rt = p.RT - r.U
} else {
ct = p.P.Sub(ToLength(TurnPort90(p.TTan), -p.RT))
rt = -p.RT + r.U
}
if rt < 0 {
rt = 0
}
}
if p.RL != 0 {
if p.RL > 0 {
cl = p.P.Add(ToLength(TurnPort90(p.LTan), p.RL))
rl = p.RL - r.U
} else {
cl = p.P.Sub(ToLength(TurnPort90(p.LTan), -p.RL))
rl = -p.RL + r.U
}
if rl < 0 {
rl = 0
}
}
}
if r.JoinMode == MiterClip || r.JoinMode == Miter ||
// Arc or ArcClip with 0 tRadCurve and 0 lRadCurve is treated the same as a
// Miter or MiterClip join, resp.
((r.JoinMode == Arc || r.JoinMode == ArcClip) && (rt == 0 && rl == 0)) {
xt := CalcIntersect(s1.Sub(p.TTan), s1, s2, s2.Sub(p.LTan))
xa := xt.Sub(p.P)
if Length(xa) < r.MLimit { // within miter limit
ra.Line(xt)
ra.Line(s2)
return
}
if r.JoinMode == MiterClip || (r.JoinMode == ArcClip) {
//Projection of tNorm onto xa
tProjP := xa.Mul(fixed.Int26_6((DotProd(xa, p.TNorm) << 6) / DotProd(xa, xa)))
projLen := Length(tProjP)
if r.MLimit > projLen { // the miter limit line is past the bevel point
// t is the fraction shifted by tStrokeShift to scale the vectors from the bevel point
// to the line intersection, so that they abbut the miter limit line.
tiLength := Length(xa)
sx1, sx2 := xt.Sub(s1), xt.Sub(s2)
t := (r.MLimit - projLen) << TStrokeShift / (tiLength - projLen)
tx := ToLength(sx1, t*Length(sx1)>>TStrokeShift)
lx := ToLength(sx2, t*Length(sx2)>>TStrokeShift)
vx := ToLength(xa, t*Length(xa)>>TStrokeShift)
s1p, _, ap := s1.Add(tx), s2.Add(lx), p.P.Add(vx)
gLen := Length(ap.Sub(s1p))
ra.Line(s1p)
r.JoinGap(ra, ap, ToLength(TurnPort90(p.TTan), gLen), ToLength(TurnPort90(p.LTan), gLen))
ra.Line(s2)
return
}
} // Fallthrough
} else if r.JoinMode == Arc || r.JoinMode == ArcClip {
// Test for cases of a bezier meeting line, an line meeting a bezier,
// or a bezier meeting a bezier. (Line meeting line is handled above.)
switch {
case rt == 0: // rl != 0, because one must be non-zero as checked above
xt, intersect := RayCircleIntersection(s1.Add(p.TTan), s1, cl, rl)
if intersect {
ray1, ray2 := xt.Sub(cl), s2.Sub(cl)
clockwise := (ray1.X*ray2.Y > ray1.Y*ray2.X) // Sign of xprod
if Length(p.P.Sub(xt)) < r.MLimit { // within miter limit
StrokeArc(ra, cl, xt, s2, clockwise, 0, 0, ra.Line)
ra.Line(s2)
return
}
// Not within miter limit line
if r.JoinMode == ArcClip { // Scale bevel points towards xt, and call gap func
xa := xt.Sub(p.P)
//Projection of tNorm onto xa
tProjP := xa.Mul(fixed.Int26_6((DotProd(xa, p.TNorm) << 6) / DotProd(xa, xa)))
projLen := Length(tProjP)
if r.MLimit > projLen { // the miter limit line is past the bevel point
// t is the fraction shifted by tStrokeShift to scale the line or arc from the bevel point
// to the line intersection, so that they abbut the miter limit line.
sx1 := xt.Sub(s1) //, xt.Sub(s2)
t := fixed.Int26_6(1<<TStrokeShift) - ((r.MLimit - projLen) << TStrokeShift / (Length(xa) - projLen))
tx := ToLength(sx1, t*Length(sx1)>>TStrokeShift)
s1p := xt.Sub(tx)
ra.Line(s1p)
sp1, ds1, ps2, _ := StrokeArc(ra, cl, xt, s2, clockwise, t, 0, ra.Start)
ra.Start(s1p)
// calc gap center as pt where -tnorm and line perp to midcoord
midP := sp1.Add(s1p).Mul(fixed.Int26_6(1 << 5)) // midpoint
midLine := TurnPort90(midP.Sub(sp1))
if midLine.X*midLine.X+midLine.Y*midLine.Y > EpsilonFixed { // if midline is zero, CalcIntersect is invalid
ap := CalcIntersect(s1p, s1p.Sub(p.TNorm), midLine.Add(midP), midP)
gLen := Length(ap.Sub(s1p))
if clockwise {
ds1 = Invert(ds1)
}
r.JoinGap(ra, ap, ToLength(TurnPort90(p.TTan), gLen), ToLength(TurnStarboard90(ds1), gLen))
}
ra.Line(sp1)
ra.Start(ps2)
ra.Line(s2)
return
}
//Bevel points not past miter limit: fallthrough
}
}
case rl == 0: // rt != 0, because one must be non-zero as checked above
xt, intersect := RayCircleIntersection(s2.Sub(p.LTan), s2, ct, rt)
if intersect {
ray1, ray2 := s1.Sub(ct), xt.Sub(ct)
clockwise := ray1.X*ray2.Y > ray1.Y*ray2.X
if Length(p.P.Sub(xt)) < r.MLimit { // within miter limit
StrokeArc(ra, ct, s1, xt, clockwise, 0, 0, ra.Line)
ra.Line(s2)
return
}
// Not within miter limit line
if r.JoinMode == ArcClip { // Scale bevel points towards xt, and call gap func
xa := xt.Sub(p.P)
//Projection of lNorm onto xa
lProjP := xa.Mul(fixed.Int26_6((DotProd(xa, p.LNorm) << 6) / DotProd(xa, xa)))
projLen := Length(lProjP)
if r.MLimit > projLen { // The miter limit line is past the bevel point,
// t is the fraction to scale the line or arc from the bevel point
// to the line intersection, so that they abbut the miter limit line.
sx2 := xt.Sub(s2)
t := fixed.Int26_6(1<<TStrokeShift) - ((r.MLimit - projLen) << TStrokeShift / (Length(xa) - projLen))
lx := ToLength(sx2, t*Length(sx2)>>TStrokeShift)
s2p := xt.Sub(lx)
_, _, ps2, ds2 := StrokeArc(ra, ct, s1, xt, clockwise, 0, t, ra.Line)
// calc gap center as pt where -lnorm and line perp to midcoord
midP := s2p.Add(ps2).Mul(fixed.Int26_6(1 << 5)) // midpoint
midLine := TurnStarboard90(midP.Sub(ps2))
if midLine.X*midLine.X+midLine.Y*midLine.Y > EpsilonFixed { // if midline is zero, CalcIntersect is invalid
ap := CalcIntersect(midP, midLine.Add(midP), s2p, s2p.Sub(p.LNorm))
gLen := Length(ap.Sub(ps2))
if clockwise {
ds2 = Invert(ds2)
}
r.JoinGap(ra, ap, ToLength(TurnStarboard90(ds2), gLen), ToLength(TurnPort90(p.LTan), gLen))
}
ra.Line(s2)
return
}
//Bevel points not past miter limit: fallthrough
}
}
default: // Both rl != 0 and rt != 0 as checked above
xt1, xt2, gIntersect := CircleCircleIntersection(ct, cl, rt, rl)
xt, intersect := ClosestPortside(s1, s2, xt1, xt2, gIntersect)
if intersect {
ray1, ray2 := s1.Sub(ct), xt.Sub(ct)
clockwiseT := (ray1.X*ray2.Y > ray1.Y*ray2.X)
ray1, ray2 = xt.Sub(cl), s2.Sub(cl)
clockwiseL := ray1.X*ray2.Y > ray1.Y*ray2.X
if Length(p.P.Sub(xt)) < r.MLimit { // within miter limit
StrokeArc(ra, ct, s1, xt, clockwiseT, 0, 0, ra.Line)
StrokeArc(ra, cl, xt, s2, clockwiseL, 0, 0, ra.Line)
ra.Line(s2)
return
}
if r.JoinMode == ArcClip { // Scale bevel points towards xt, and call gap func
xa := xt.Sub(p.P)
//Projection of lNorm onto xa
lProjP := xa.Mul(fixed.Int26_6((DotProd(xa, p.LNorm) << 6) / DotProd(xa, xa)))
projLen := Length(lProjP)
if r.MLimit > projLen { // The miter limit line is past the bevel point,
// t is the fraction to scale the line or arc from the bevel point
// to the line intersection, so that they abbut the miter limit line.
t := fixed.Int26_6(1<<TStrokeShift) - ((r.MLimit - projLen) << TStrokeShift / (Length(xa) - projLen))
_, _, ps1, ds1 := StrokeArc(ra, ct, s1, xt, clockwiseT, 0, t, r.Filler.Line)
ps2, ds2, fs2, _ := StrokeArc(ra, cl, xt, s2, clockwiseL, t, 0, ra.Start)
midP := ps1.Add(ps2).Mul(fixed.Int26_6(1 << 5)) // midpoint
midLine := TurnStarboard90(midP.Sub(ps1))
ra.Start(ps1)
if midLine.X*midLine.X+midLine.Y*midLine.Y > EpsilonFixed { // if midline is zero, CalcIntersect is invalid
if clockwiseT {
ds1 = Invert(ds1)
}
if clockwiseL {
ds2 = Invert(ds2)
}
ap := CalcIntersect(midP, midLine.Add(midP), ps2, ps2.Sub(TurnStarboard90(ds2)))
gLen := Length(ap.Sub(ps2))
r.JoinGap(ra, ap, ToLength(TurnStarboard90(ds1), gLen), ToLength(TurnStarboard90(ds2), gLen))
}
ra.Line(ps2)
ra.Start(fs2)
ra.Line(s2)
return
}
}
}
// fallthrough to final JoinGap
}
}
r.JoinGap(ra, p.P, p.TNorm, p.LNorm)
ra.Line(s2)
}
// Stop a stroked line. The line will close
// is isClosed is true. Otherwise end caps will
// be drawn at both ends.
func (r *Stroker) Stop(isClosed bool) {
if !r.InStroke {
return
}
rf := &r.Filler
if isClosed {
if r.FirstP.P != rf.A {
r.Line(r.FirstP.P)
}
a := rf.A
r.FirstP.TNorm = r.LeadPoint.TNorm
r.FirstP.RT = r.LeadPoint.RT
r.FirstP.TTan = r.LeadPoint.TTan
rf.Start(r.FirstP.P.Sub(r.FirstP.TNorm))
rf.Line(a.Sub(r.Ln))
rf.Start(a.Add(r.Ln))
rf.Line(r.FirstP.P.Add(r.FirstP.TNorm))
r.Joiner(r.FirstP)
r.FirstP.BlackWidowMark(rf)
} else {
a := rf.A
rf.Start(r.LeadPoint.P.Sub(r.LeadPoint.TNorm))
rf.Line(a.Sub(r.Ln))
rf.Start(a.Add(r.Ln))
rf.Line(r.LeadPoint.P.Add(r.LeadPoint.TNorm))
r.CapL(rf, r.LeadPoint.P, r.LeadPoint.TNorm)
r.CapT(rf, r.FirstP.P, Invert(r.FirstP.LNorm))
}
r.InStroke = false
}
// QuadBezier starts a stroked quadratic bezier.
func (r *Stroker) QuadBezier(b, c fixed.Point26_6) {
r.QuadBezierf(r, b, c)
}
// CubeBezier starts a stroked quadratic bezier.
func (r *Stroker) CubeBezier(b, c, d fixed.Point26_6) {
r.CubeBezierf(r, b, c, d)
}
// QuadBezierf calcs end curvature of beziers
func (r *Stroker) QuadBezierf(s Raster, b, c fixed.Point26_6) {
r.TrailPoint = r.LeadPoint
r.CalcEndCurvature(r.A, b, c, c, b, r.A, fixed.Int52_12(2<<12), DoCalcCurvature(s))
r.QuadBezierF(s, b, c)
r.A = c
}
// DoCalcCurvature determines if calculation of the end curvature is required
// depending on the raster type and JoinMode
func DoCalcCurvature(r Raster) bool {
switch q := r.(type) {
case *Filler:
return false // never for filler
case *Stroker:
return (q.JoinMode == Arc || q.JoinMode == ArcClip)
case *Dasher:
return (q.JoinMode == Arc || q.JoinMode == ArcClip)
default:
return true // Better safe than sorry if another raster type is used
}
}
func (r *Stroker) CubeBezierf(sgm Raster, b, c, d fixed.Point26_6) {
if (r.A == b && c == d) || (r.A == b && b == c) || (c == b && d == c) {
sgm.Line(d)
return
}
r.TrailPoint = r.LeadPoint
// Only calculate curvature if stroking or and using arc or arc-clip
doCalcCurve := DoCalcCurvature(sgm)
const dm = fixed.Int52_12((3 << 12) / 2)
switch {
// b != c, and c != d see above
case r.A == b:
r.CalcEndCurvature(b, c, d, d, c, b, dm, doCalcCurve)
// b != a, and b != c, see above
case c == d:
r.CalcEndCurvature(r.A, b, c, c, b, r.A, dm, doCalcCurve)
default:
r.CalcEndCurvature(r.A, b, c, d, c, b, dm, doCalcCurve)
}
r.CubeBezierF(sgm, b, c, d)
r.A = d
}
// Line adds a line segment to the rasterizer
func (r *Stroker) Line(b fixed.Point26_6) {
r.LineSeg(r, b)
}
// LineSeg is called by both the Stroker and Dasher
func (r *Stroker) LineSeg(sgm Raster, b fixed.Point26_6) {
r.TrailPoint = r.LeadPoint
ba := b.Sub(r.A)
if ba.X == 0 && ba.Y == 0 { // a == b, line is degenerate
if r.TrailPoint.TTan.X != 0 || r.TrailPoint.TTan.Y != 0 {
ba = r.TrailPoint.TTan // Use last tangent for seg tangent
} else { // Must be on top of last moveto; set ba to X axis unit vector
ba = fixed.Point26_6{X: 1 << 6, Y: 0}
}
}
bnorm := TurnPort90(ToLength(ba, r.U))
r.TrailPoint.LTan = ba
r.LeadPoint.TTan = ba
r.TrailPoint.LNorm = bnorm
r.LeadPoint.TNorm = bnorm
r.TrailPoint.RL = 0.0
r.LeadPoint.RT = 0.0
r.TrailPoint.P = r.A
r.LeadPoint.P = b
sgm.JoinF()
sgm.LineF(b)
r.A = b
}
// LineF is for intra-curve lines. It is required for the Rasterizer interface
// so that if the line is being stroked or dash stroked, different actions can be
// taken.
func (r *Stroker) LineF(b fixed.Point26_6) {
// b is either an intra-segment value, or
// the end of the segment.
var bnorm fixed.Point26_6
a := r.A // Hold a since r.a is going to change during stroke operation
if b == r.LeadPoint.P { // End of segment
bnorm = r.LeadPoint.TNorm // Use more accurate leadPoint tangent
} else {
bnorm = TurnPort90(ToLength(b.Sub(a), r.U)) // Intra segment normal
}
ra := &r.Filler
ra.Start(b.Sub(bnorm))
ra.Line(a.Sub(r.Ln))
ra.Start(a.Add(r.Ln))
ra.Line(b.Add(bnorm))
r.A = b
r.Ln = bnorm
}
// Start iniitates a stroked path
func (r *Stroker) Start(a fixed.Point26_6) {
r.InStroke = false
r.Filler.Start(a)
}
// CalcEndCurvature calculates the radius of curvature given the control points
// of a bezier curve.
// It is a low level function exposed for the purposes of callbacks
// and debugging.
func (r *Stroker) CalcEndCurvature(p0, p1, p2, q0, q1, q2 fixed.Point26_6,
dm fixed.Int52_12, calcRadCuve bool) {
r.TrailPoint.P = p0
r.LeadPoint.P = q0
r.TrailPoint.LTan = p1.Sub(p0)
r.LeadPoint.TTan = q0.Sub(q1)
r.TrailPoint.LNorm = TurnPort90(ToLength(r.TrailPoint.LTan, r.U))
r.LeadPoint.TNorm = TurnPort90(ToLength(r.LeadPoint.TTan, r.U))
if calcRadCuve {
r.TrailPoint.RL = RadCurvature(p0, p1, p2, dm)
r.LeadPoint.RT = -RadCurvature(q0, q1, q2, dm)
} else {
r.TrailPoint.RL = 0
r.LeadPoint.RT = 0
}
}
func (r *Stroker) JoinF() {
if !r.InStroke {
r.InStroke = true
r.FirstP = r.TrailPoint
} else {
ra := &r.Filler
tl := r.TrailPoint.P.Sub(r.TrailPoint.TNorm)
th := r.TrailPoint.P.Add(r.TrailPoint.TNorm)
if r.A != r.TrailPoint.P || r.Ln != r.TrailPoint.TNorm {
a := r.A
ra.Start(tl)
ra.Line(a.Sub(r.Ln))
ra.Start(a.Add(r.Ln))
ra.Line(th)
}
r.Joiner(r.TrailPoint)
r.TrailPoint.BlackWidowMark(ra)
}
r.Ln = r.TrailPoint.LNorm
r.A = r.TrailPoint.P
}
// BlackWidowMark handles a gap in a stroke that can occur when a line end is too close
// to a segment to segment join point. Although it is only required in those cases,
// at this point, no code has been written to properly detect when it is needed,
// so for now it just draws by default.
func (jp *C2Point) BlackWidowMark(ra Adder) {
xprod := jp.TNorm.X*jp.LNorm.Y - jp.TNorm.Y*jp.LNorm.X
if xprod > EpsilonFixed*EpsilonFixed {
tl := jp.P.Sub(jp.TNorm)
ll := jp.P.Sub(jp.LNorm)
ra.Start(jp.P)
ra.Line(tl)
ra.Line(ll)
ra.Line(jp.P)
} else if xprod < -EpsilonFixed*EpsilonFixed {
th := jp.P.Add(jp.TNorm)
lh := jp.P.Add(jp.LNorm)
ra.Start(jp.P)
ra.Line(lh)
ra.Line(th)
ra.Line(jp.P)
}
}
// 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 paint
import (
"errors"
"image"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"golang.org/x/image/font"
)
// Rune contains fully explicit data needed for rendering a single rune
// -- Face and Color can be nil after first element, in which case the last
// non-nil is used -- likely slightly more efficient to avoid setting all
// those pointers -- float32 values used to support better accuracy when
// transforming points
type Rune struct {
// fully specified font rendering info, includes fully computed font size.
// This is exactly what will be drawn, with no further transforms.
// If nil, previous one is retained.
Face font.Face `json:"-"`
// Color is the color to draw characters in.
// If nil, previous one is retained.
Color image.Image `json:"-"`
// background color to fill background of color, for highlighting,
// <mark> tag, etc. Unlike Face, Color, this must be non-nil for every case
// that uses it, as nil is also used for default transparent background.
Background image.Image `json:"-"`
// dditional decoration to apply: underline, strike-through, etc.
// Also used for encoding a few special layout hints to pass info
// from styling tags to separate layout algorithms (e.g., <P> vs <BR>)
Deco styles.TextDecorations
// relative position from start of Text for the lower-left baseline
// rendering position of the font character
RelPos math32.Vector2
// size of the rune itself, exclusive of spacing that might surround it
Size math32.Vector2
// rotation in radians for this character, relative to its lower-left
// baseline rendering position
RotRad float32
// scaling of the X dimension, in case of non-uniform scaling, 0 = no separate scaling
ScaleX float32
}
// HasNil returns error if any of the key info (face, color) is nil -- only
// the first element must be non-nil
func (rr *Rune) HasNil() error {
if rr.Face == nil {
return errors.New("core.Rune: Face is nil")
}
if rr.Color == nil {
return errors.New("core.Rune: Color is nil")
}
// note: BackgroundColor can be nil -- transparent
return nil
}
// CurFace is convenience for updating current font face if non-nil
func (rr *Rune) CurFace(curFace font.Face) font.Face {
if rr.Face != nil {
return rr.Face
}
return curFace
}
// CurColor is convenience for updating current color if non-nil
func (rr *Rune) CurColor(curColor image.Image) image.Image {
if rr.Color != nil {
return rr.Color
}
return curColor
}
// RelPosAfterLR returns the relative position after given rune for LR order: RelPos.X + Size.X
func (rr *Rune) RelPosAfterLR() float32 {
return rr.RelPos.X + rr.Size.X
}
// RelPosAfterRL returns the relative position after given rune for RL order: RelPos.X - Size.X
func (rr *Rune) RelPosAfterRL() float32 {
return rr.RelPos.X - rr.Size.X
}
// RelPosAfterTB returns the relative position after given rune for TB order: RelPos.Y + Size.Y
func (rr *Rune) RelPosAfterTB() float32 {
return rr.RelPos.Y + rr.Size.Y
}
// 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.
// Based on https://github.com/srwiley/scanx:
// Copyright 2018 by the scanx Authors. All rights reserved.
// Created 2018 by S.R.Wiley
// This is the anti-aliasing algorithm from the golang
// translation of FreeType. It has been adapted for use by the scan package
// which replaces the painter interface with the spanner interface.
// Copyright 2010 The Freetype-Go Authors. All rights reserved.
// Use of this source code is governed by your choice of either the
// FreeType License or the GNU General Public License version 2 (or
// any later version), both of which can be found in the LICENSE file.
// Package scan provides an anti-aliasing 2-D rasterizer, which is
// based on the larger Freetype suite of font-related packages, but the
// raster package is not specific to font rasterization, and can be used
// standalone without any other Freetype package.
// Rasterization is done by the same area/coverage accumulation algorithm as
// the Freetype "smooth" module, and the Anti-Grain Geometry library. A
// description of the area/coverage algorithm is at
// http://projects.tuxee.net/cl-vectors/section-the-cl-aa-algorithm
package scan
import (
"image"
"math"
"golang.org/x/image/math/fixed"
)
// Scanner is a refactored version of the freetype scanner
type Scanner struct {
// If false, the behavior is to use the even-odd winding fill
// rule during Rasterize.
UseNonZeroWinding bool
// The Width of the Rasterizer. The height is implicit in len(cellIndex).
Width int
// The current pen position.
A fixed.Point26_6
// The current cell and its area/coverage being accumulated.
Xi, Yi int
Area int
Cover int
// The clip bounds of the scanner
Clip image.Rectangle
// The saved cells.
Cell []Cell
// Linked list of cells, one per row.
CellIndex []int
// The spanner that we use.
Spanner Spanner
// The bounds.
MinX, MinY, MaxX, MaxY fixed.Int26_6
}
// SpanFunc is the type of a span function
type SpanFunc func(yi, xi0, xi1 int, alpha uint32)
// A Spanner consumes spans as they are created by the Scanner Draw function
type Spanner interface {
// SetColor sets the color used for rendering.
SetColor(color image.Image)
// This returns a function that is efficient given the Spanner parameters.
GetSpanFunc() SpanFunc
}
// Cell is part of a linked list (for a given yi co-ordinate) of accumulated
// area/coverage for the pixel at (xi, yi).
type Cell struct {
Xi int
Area int
Cover int
Next int
}
func (s *Scanner) Set(a fixed.Point26_6) {
if s.MaxX < a.X {
s.MaxX = a.X
}
if s.MaxY < a.Y {
s.MaxY = a.Y
}
if s.MinX > a.X {
s.MinX = a.X
}
if s.MinY > a.Y {
s.MinY = a.Y
}
}
// SetWinding set the winding rule for the polygons
func (s *Scanner) SetWinding(useNonZeroWinding bool) {
s.UseNonZeroWinding = useNonZeroWinding
}
// SetColor sets the color used for rendering.
func (s *Scanner) SetColor(clr image.Image) {
s.Spanner.SetColor(clr)
}
// FindCell returns the index in [Scanner.Cell] for the cell corresponding to
// (r.xi, r.yi). The cell is created if necessary.
func (s *Scanner) FindCell() int {
yi := s.Yi
if yi < 0 || yi >= len(s.CellIndex) {
return -1
}
xi := s.Xi
if xi < 0 {
xi = -1
} else if xi > s.Width {
xi = s.Width
}
i, prev := s.CellIndex[yi], -1
for i != -1 && s.Cell[i].Xi <= xi {
if s.Cell[i].Xi == xi {
return i
}
i, prev = s.Cell[i].Next, i
}
c := len(s.Cell)
s.Cell = append(s.Cell, Cell{xi, 0, 0, i})
if prev == -1 {
s.CellIndex[yi] = c
} else {
s.Cell[prev].Next = c
}
return c
}
// SaveCell saves any accumulated [Scanner.Area] or [Scanner.Cover] for ([Scanner.Xi], [Scanner.Yi]).
func (s *Scanner) SaveCell() {
if s.Area != 0 || s.Cover != 0 {
i := s.FindCell()
if i != -1 {
s.Cell[i].Area += s.Area
s.Cell[i].Cover += s.Cover
}
s.Area = 0
s.Cover = 0
}
}
// SetCell sets the (xi, yi) cell that r is accumulating area/coverage for.
func (s *Scanner) SetCell(xi, yi int) {
if s.Xi != xi || s.Yi != yi {
s.SaveCell()
s.Xi, s.Yi = xi, yi
}
}
// Scan accumulates area/coverage for the yi'th scanline, going from
// x0 to x1 in the horizontal direction (in 26.6 fixed point co-ordinates)
// and from y0f to y1f fractional vertical units within that scanline.
func (s *Scanner) Scan(yi int, x0, y0f, x1, y1f fixed.Int26_6) {
// Break the 26.6 fixed point X co-ordinates into integral and fractional parts.
x0i := int(x0) / 64
x0f := x0 - fixed.Int26_6(64*x0i)
x1i := int(x1) / 64
x1f := x1 - fixed.Int26_6(64*x1i)
// A perfectly horizontal scan.
if y0f == y1f {
s.SetCell(x1i, yi)
return
}
dx, dy := x1-x0, y1f-y0f
// A single cell scan.
if x0i == x1i {
s.Area += int((x0f + x1f) * dy)
s.Cover += int(dy)
return
}
// There are at least two cells. Apart from the first and last cells,
// all intermediate cells go through the full width of the cell,
// or 64 units in 26.6 fixed point format.
var (
p, q, edge0, edge1 fixed.Int26_6
xiDelta int
)
if dx > 0 {
p, q = (64-x0f)*dy, dx
edge0, edge1, xiDelta = 0, 64, 1
} else {
p, q = x0f*dy, -dx
edge0, edge1, xiDelta = 64, 0, -1
}
yDelta, yRem := p/q, p%q
if yRem < 0 {
yDelta--
yRem += q
}
// Do the first cell.
xi, y := x0i, y0f
s.Area += int((x0f + edge1) * yDelta)
s.Cover += int(yDelta)
xi, y = xi+xiDelta, y+yDelta
s.SetCell(xi, yi)
if xi != x1i {
// Do all the intermediate cells.
p = 64 * (y1f - y + yDelta)
fullDelta, fullRem := p/q, p%q
if fullRem < 0 {
fullDelta--
fullRem += q
}
yRem -= q
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)
}
}
// Do the last cell.
yDelta = y1f - y
s.Area += int((edge0 + x1f) * yDelta)
s.Cover += int(yDelta)
}
// Start starts a new path at the given point.
func (s *Scanner) Start(a fixed.Point26_6) {
s.Set(a)
s.SetCell(int(a.X/64), int(a.Y/64))
s.A = a
}
// Line adds a linear segment to the current curve.
func (s *Scanner) Line(b fixed.Point26_6) {
s.Set(b)
x0, y0 := s.A.X, s.A.Y
x1, y1 := b.X, b.Y
dx, dy := x1-x0, y1-y0
// Break the 26.6 fixed point Y co-ordinates into integral and fractional
// parts.
y0i := int(y0) / 64
y0f := y0 - fixed.Int26_6(64*y0i)
y1i := int(y1) / 64
y1f := y1 - fixed.Int26_6(64*y1i)
if y0i == y1i {
// There is only one scanline.
s.Scan(y0i, x0, y0f, x1, y1f)
} else if dx == 0 {
// This is a vertical line segment. We avoid calling r.scan and instead
// manipulate r.area and r.cover directly.
var (
edge0, edge1 fixed.Int26_6
yiDelta int
)
if dy > 0 {
edge0, edge1, yiDelta = 0, 64, 1
} else {
edge0, edge1, yiDelta = 64, 0, -1
}
x0i, yi := int(x0)/64, y0i
x0fTimes2 := (int(x0) - (64 * x0i)) * 2
// Do the first pixel.
dcover := int(edge1 - y0f)
darea := int(x0fTimes2 * dcover)
s.Area += darea
s.Cover += dcover
yi += yiDelta
s.SetCell(x0i, yi)
// 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)
}
// Do the last pixel.
dcover = int(y1f - edge0)
darea = int(x0fTimes2 * dcover)
s.Area += darea
s.Cover += dcover
} else {
// There are at least two scanlines. Apart from the first and last
// scanlines, all intermediate scanlines go through the full height of
// the row, or 64 units in 26.6 fixed point format.
var (
p, q, edge0, edge1 fixed.Int26_6
yiDelta int
)
if dy > 0 {
p, q = (64-y0f)*dx, dy
edge0, edge1, yiDelta = 0, 64, 1
} else {
p, q = y0f*dx, -dy
edge0, edge1, yiDelta = 64, 0, -1
}
xDelta, xRem := p/q, p%q
if xRem < 0 {
xDelta--
xRem += q
}
// Do the first scanline.
x, yi := x0, y0i
s.Scan(yi, x, y0f, x+xDelta, edge1)
x, yi = x+xDelta, yi+yiDelta
s.SetCell(int(x)/64, yi)
if yi != y1i {
// Do all the intermediate scanlines.
p = 64 * dx
fullDelta, fullRem := p/q, p%q
if fullRem < 0 {
fullDelta--
fullRem += q
}
xRem -= q
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)
}
}
// Do the last scanline.
s.Scan(yi, x, edge0, x1, y1f)
}
// The next lineTo starts from b.
s.A = b
}
// AreaToAlpha converts an area value to a uint32 alpha value. A completely
// filled pixel corresponds to an area of 64*64*2, and an alpha of 0xffff. The
// conversion of area values greater than this depends on the winding rule:
// even-odd or non-zero.
func (s *Scanner) AreaToAlpha(area int) uint32 {
// The C Freetype implementation (version 2.3.12) does "alpha := area>>1"
// without the +1. Round-to-nearest gives a more symmetric result than
// round-down. The C implementation also returns 8-bit alpha, not 16-bit
// alpha.
a := (area + 1) >> 1
if a < 0 {
a = -a
}
alpha := uint32(a)
if s.UseNonZeroWinding {
if alpha > 0x0fff {
alpha = 0x0fff
}
} else {
alpha &= 0x1fff
if alpha > 0x1000 {
alpha = 0x2000 - alpha
} else if alpha == 0x1000 {
alpha = 0x0fff
}
}
// alpha is now in the range [0x0000, 0x0fff]. Convert that 12-bit alpha to
// 16-bit alpha.
return alpha<<4 | alpha>>8
}
// Draw converts r's accumulated curves into Spans for p. The Spans passed
// to the spanner are non-overlapping, and sorted by Y and then X. They all have non-zero
// width (and 0 <= X0 < X1 <= r.width) and non-zero A, except for the final
// Span, which has Y, X0, X1 and A all equal to zero.
func (s *Scanner) Draw() {
b := image.Rect(0, 0, s.Width, len(s.CellIndex))
if s.Clip.Dx() != 0 && s.Clip.Dy() != 0 {
b = b.Intersect(s.Clip)
}
s.SaveCell()
span := s.Spanner.GetSpanFunc()
for yi := b.Min.Y; yi < b.Max.Y; yi++ {
xi, cover := 0, 0
for c := s.CellIndex[yi]; c != -1; c = s.Cell[c].Next {
if cover != 0 && s.Cell[c].Xi > xi {
alpha := s.AreaToAlpha(cover * 64 * 2)
if alpha != 0 {
xi0, xi1 := xi, s.Cell[c].Xi
if xi0 < b.Min.X {
xi0 = b.Min.X
}
if xi1 > b.Max.X {
xi1 = b.Max.X
}
if xi0 < xi1 {
span(yi, xi0, xi1, alpha)
}
}
}
cover += s.Cell[c].Cover
alpha := s.AreaToAlpha(cover*64*2 - s.Cell[c].Area)
xi = s.Cell[c].Xi + 1
if alpha != 0 {
xi0, xi1 := s.Cell[c].Xi, xi
if xi0 < b.Min.X {
xi0 = b.Min.X
}
if xi1 > b.Max.X {
xi1 = b.Max.X
}
if xi0 < xi1 {
span(yi, xi0, xi1, alpha)
}
}
}
}
}
// GetPathExtent returns the bounds of the accumulated path extent
func (s *Scanner) GetPathExtent() fixed.Rectangle26_6 {
return fixed.Rectangle26_6{
Min: fixed.Point26_6{X: s.MinX, Y: s.MinY},
Max: fixed.Point26_6{X: s.MaxX, Y: s.MaxY}}
}
// Clear cancels any previous accumulated scans
func (s *Scanner) Clear() {
s.A = fixed.Point26_6{}
s.Xi = 0
s.Yi = 0
s.Area = 0
s.Cover = 0
s.Cell = s.Cell[:0]
for i := 0; i < len(s.CellIndex); i++ {
s.CellIndex[i] = -1
}
const mxfi = fixed.Int26_6(math.MaxInt32)
s.MinX, s.MinY, s.MaxX, s.MaxY = mxfi, mxfi, -mxfi, -mxfi
}
// SetBounds sets the maximum width and height of the rasterized image and
// calls Clear. The width and height are in pixels, not fixed.Int26_6 units.
func (s *Scanner) SetBounds(width, height int) {
if width < 0 {
width = 0
}
if height < 0 {
height = 0
}
s.Width = width
s.Cell = s.Cell[:0]
if height > cap(s.CellIndex) {
s.CellIndex = make([]int, height)
}
// Make sure length of cellIndex = height
s.CellIndex = s.CellIndex[0:height]
s.Width = width
s.Clear()
}
// NewScanner creates a new Scanner with the given bounds.
func NewScanner(xs Spanner, width, height int) (sc *Scanner) {
sc = &Scanner{Spanner: xs, UseNonZeroWinding: true}
sc.SetBounds(width, height)
return
}
// SetClip will not affect accumulation of scans, but it will
// clip drawing of the spans int the Draw func by the clip rectangle.
func (s *Scanner) SetClip(r image.Rectangle) {
s.Clip = r
}
// 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.
// Based on https://github.com/srwiley/scanx:
// Copyright 2018 by the scanx Authors. All rights reserved.
// Created 2018 by S.R.Wiley
package scan
import (
"image"
"image/color"
"image/draw"
"cogentcore.org/core/colors"
)
const (
m = 1<<16 - 1
mp = 0x100 * m
pa uint32 = 0x101
q uint32 = 0xFF00
)
// ImgSpanner is a Spanner that draws Spans onto an [*image.RGBA] image.
// It uses either a color function as a the color source, or a fgColor
// if colFunc is nil.
type ImgSpanner struct {
BaseSpanner
Pix []uint8
Stride int
ColorImage image.Image
}
// LinkListSpanner is a Spanner that draws Spans onto a draw.Image
// interface satisfying struct but it is optimized for [*image.RGBA].
// It uses a solid Color only for fg and bg and does not support a color function
// used by gradients. Spans are accumulated into a set of linked lists, one for
// every horizontal line in the image. After the spans for the image are accumulated,
// use the DrawToImage function to write the spans to an image.
type LinkListSpanner struct {
BaseSpanner
Spans []SpanCell
BgColor color.RGBA
LastY int
LastP int
}
// SpanCell represents a span cell.
type SpanCell struct {
X0 int
X1 int
Next int
Clr color.RGBA
}
// BaseSpanner contains base spanner information extended by [ImgSpanner] and [LinkListSpanner].
type BaseSpanner struct {
// drawing is done with Bounds.Min as the origin
Bounds image.Rectangle
// Op is how pixels are overlayed
Op draw.Op
FgColor color.RGBA
}
// Clear clears the current spans
func (x *LinkListSpanner) Clear() {
x.LastY, x.LastP = 0, 0
x.Spans = x.Spans[0:0]
width := x.Bounds.Dy()
for i := 0; i < width; i++ {
// The first cells are indexed according to the y values
// to create y separate linked lists corresponding to the
// image y length. Since index 0 is used by the first of these sentinel cells
// 0 can and is used for the end of list value by the spanner linked list.
x.Spans = append(x.Spans, SpanCell{})
}
}
func (x *LinkListSpanner) SpansToImage(img draw.Image) {
for y := 0; y < x.Bounds.Dy(); y++ {
p := x.Spans[y].Next
for p != 0 {
spCell := x.Spans[p]
clr := spCell.Clr
x0, x1 := spCell.X0, spCell.X1
for x := x0; x < x1; x++ {
img.Set(y, x, clr)
}
p = spCell.Next
}
}
}
func (x *LinkListSpanner) SpansToPix(pix []uint8, stride int) {
for y := 0; y < x.Bounds.Dy(); y++ {
yo := y * stride
p := x.Spans[y].Next
for p != 0 {
spCell := x.Spans[p]
i0 := yo + spCell.X0*4
i1 := i0 + (spCell.X1-spCell.X0)*4
r, g, b, a := spCell.Clr.R, spCell.Clr.G, spCell.Clr.B, spCell.Clr.A
for i := i0; i < i1; i += 4 {
pix[i+0] = r
pix[i+1] = g
pix[i+2] = b
pix[i+3] = a
}
p = spCell.Next
}
}
}
// DrawToImage draws the accumulated y spans onto the img
func (x *LinkListSpanner) DrawToImage(img image.Image) {
switch img := img.(type) {
case *image.RGBA:
x.SpansToPix(img.Pix, img.Stride)
case draw.Image:
x.SpansToImage(img)
}
}
// SetBounds sets the spanner boundaries
func (x *LinkListSpanner) SetBounds(bounds image.Rectangle) {
x.Bounds = bounds
x.Clear()
}
func (x *LinkListSpanner) BlendColor(under color.RGBA, ma uint32) color.RGBA {
if ma == 0 {
return under
}
rma := uint32(x.FgColor.R) * ma
gma := uint32(x.FgColor.G) * ma
bma := uint32(x.FgColor.B) * ma
ama := uint32(x.FgColor.A) * ma
if x.Op != draw.Over || under.A == 0 || ama == m*0xFF {
return color.RGBA{
uint8(rma / q),
uint8(gma / q),
uint8(bma / q),
uint8(ama / q)}
}
a := m - (ama / (m >> 8))
cc := color.RGBA{
uint8((uint32(under.R)*a + rma) / q),
uint8((uint32(under.G)*a + gma) / q),
uint8((uint32(under.B)*a + bma) / q),
uint8((uint32(under.A)*a + ama) / q)}
return cc
}
func (x *LinkListSpanner) AddLink(x0, x1, next, pp int, underColor color.RGBA, alpha uint32) (p int) {
clr := x.BlendColor(underColor, alpha)
if pp >= x.Bounds.Dy() && x.Spans[pp].X1 >= x0 && ((clr.A == 0 && x.Spans[pp].Clr.A == 0) || clr == x.Spans[pp].Clr) {
// Just extend the prev span; a new one is not required
x.Spans[pp].X1 = x1
return pp
}
x.Spans = append(x.Spans, SpanCell{X0: x0, X1: x1, Next: next, Clr: clr})
p = len(x.Spans) - 1
x.Spans[pp].Next = p
return
}
// GetSpanFunc returns the function that consumes a span described by the parameters.
func (x *LinkListSpanner) GetSpanFunc() SpanFunc {
x.LastY = -1 // x within a y list may no longer be ordered, so this ensures a reset.
return x.SpanOver
}
// SpanOver adds the span into an array of linked lists of spans using the fgColor and Porter-Duff composition
// ma is the accumulated alpha coverage. This function also assumes usage sorted x inputs for each y and so if
// inputs for x in y are not monotonically increasing, then lastY should be set to -1.
func (x *LinkListSpanner) SpanOver(yi, xi0, xi1 int, ma uint32) {
if yi != x.LastY { // If the y place has changed, start at the list beginning
x.LastP = yi
x.LastY = yi
}
// since spans are sorted, we can start from x.lastP
pp := x.LastP
p := x.Spans[pp].Next
for p != 0 && xi0 < xi1 {
sp := x.Spans[p]
if sp.X1 <= xi0 { //sp is before new span
pp = p
p = sp.Next
continue
}
if sp.X0 >= xi1 { //new span is before sp
x.LastP = x.AddLink(xi0, xi1, p, pp, x.BgColor, ma)
return
}
// left span
if xi0 < sp.X0 {
pp = x.AddLink(xi0, sp.X0, p, pp, x.BgColor, ma)
xi0 = sp.X0
} else if xi0 > sp.X0 {
pp = x.AddLink(sp.X0, xi0, p, pp, sp.Clr, 0)
}
clr := x.BlendColor(sp.Clr, ma)
sameClrs := pp >= x.Bounds.Dy() && ((clr.A == 0 && x.Spans[pp].Clr.A == 0) || clr == x.Spans[pp].Clr)
if xi1 < sp.X1 { // span does not go beyond sp
// merge with left span
if x.Spans[pp].X1 >= xi0 && sameClrs {
x.Spans[pp].X1 = xi1
x.Spans[pp].Next = sp.Next
// Suffices not to advance lastP ?!? Testing says NO!
x.LastP = yi // We need to go back, so let's just go to start of the list next time
p = pp
} else {
// middle span; replaces sp
x.Spans[p] = SpanCell{X0: xi0, X1: xi1, Next: sp.Next, Clr: clr}
x.LastP = pp
}
x.AddLink(xi1, sp.X1, sp.Next, p, sp.Clr, 0)
return
}
if x.Spans[pp].X1 >= xi0 && sameClrs { // Extend and merge with previous
x.Spans[pp].X1 = sp.X1
x.Spans[pp].Next = sp.Next
p = sp.Next // clip out the current span from the list
xi0 = sp.X1 // set remaining to start for next loop
continue
}
// Set current span to start of new span and combined color
x.Spans[p] = SpanCell{X0: xi0, X1: sp.X1, Next: sp.Next, Clr: clr}
xi0 = sp.X1 // any remaining span starts at sp.x1
pp = p
p = sp.Next
}
x.LastP = pp
if xi0 < xi1 { // add any remaining span to the end of the chain
x.AddLink(xi0, xi1, 0, pp, x.BgColor, ma)
}
}
// SetBgColor sets the background color for blending to the first pixel of the given color
func (x *LinkListSpanner) SetBgColor(c image.Image) {
x.BgColor = colors.AsRGBA(colors.ToUniform(c))
}
// SetColor sets the color of x to the first pixel of the given color
func (x *LinkListSpanner) SetColor(c image.Image) {
x.FgColor = colors.AsRGBA(colors.ToUniform(c))
}
// NewImgSpanner returns an ImgSpanner set to draw to the given [*image.RGBA].
func NewImgSpanner(img *image.RGBA) (x *ImgSpanner) {
x = &ImgSpanner{}
x.SetImage(img)
return
}
// SetImage set the [*image.RGBA] that the ImgSpanner will draw onto.
func (x *ImgSpanner) SetImage(img *image.RGBA) {
x.Pix = img.Pix
x.Stride = img.Stride
x.Bounds = img.Bounds()
}
// SetColor sets the color of x to the given color image
func (x *ImgSpanner) SetColor(c image.Image) {
if u, ok := c.(*image.Uniform); ok {
x.FgColor = colors.AsRGBA(u.C)
x.ColorImage = nil
return
}
x.FgColor = color.RGBA{}
x.ColorImage = c
}
// GetSpanFunc returns the function that consumes a span described by the parameters.
// The next four func declarations are all slightly different
// but in order to reduce code redundancy, this method is used
// to dispatch the function in the draw method.
func (x *ImgSpanner) GetSpanFunc() SpanFunc {
var (
useColorFunc = x.ColorImage != nil
drawOver = x.Op == draw.Over
)
switch {
case useColorFunc && drawOver:
return x.SpanColorFunc
case useColorFunc && !drawOver:
return x.SpanColorFuncR
case !useColorFunc && !drawOver:
return x.SpanFgColorR
default:
return x.SpanFgColor
}
}
// SpanColorFuncR draw the span using a colorFunc and replaces the previous values.
func (x *ImgSpanner) SpanColorFuncR(yi, xi0, xi1 int, ma uint32) {
i0 := (yi)*x.Stride + (xi0)*4
i1 := i0 + (xi1-xi0)*4
cx := xi0
for i := i0; i < i1; i += 4 {
rcr, rcg, rcb, rca := x.ColorImage.At(cx, yi).RGBA()
cx++
x.Pix[i+0] = uint8(rcr * ma / mp)
x.Pix[i+1] = uint8(rcg * ma / mp)
x.Pix[i+2] = uint8(rcb * ma / mp)
x.Pix[i+3] = uint8(rca * ma / mp)
}
}
// SpanFgColorR draws the span with the fore ground color and replaces the previous values.
func (x *ImgSpanner) SpanFgColorR(yi, xi0, xi1 int, ma uint32) {
i0 := (yi)*x.Stride + (xi0)*4
i1 := i0 + (xi1-xi0)*4
cr, cg, cb, ca := x.FgColor.RGBA()
rma := uint8(cr * ma / mp)
gma := uint8(cg * ma / mp)
bma := uint8(cb * ma / mp)
ama := uint8(ca * ma / mp)
for i := i0; i < i1; i += 4 {
x.Pix[i+0] = rma
x.Pix[i+1] = gma
x.Pix[i+2] = bma
x.Pix[i+3] = ama
}
}
// SpanColorFunc draws the span using a colorFunc and the Porter-Duff composition operator.
func (x *ImgSpanner) SpanColorFunc(yi, xi0, xi1 int, ma uint32) {
i0 := (yi)*x.Stride + (xi0)*4
i1 := i0 + (xi1-xi0)*4
cx := xi0
for i := i0; i < i1; i += 4 {
// uses the Porter-Duff composition operator.
rcr, rcg, rcb, rca := x.ColorImage.At(cx, yi).RGBA()
cx++
a := (m - (rca * ma / m)) * pa
dr := uint32(x.Pix[i+0])
dg := uint32(x.Pix[i+1])
db := uint32(x.Pix[i+2])
da := uint32(x.Pix[i+3])
x.Pix[i+0] = uint8((dr*a + rcr*ma) / mp)
x.Pix[i+1] = uint8((dg*a + rcg*ma) / mp)
x.Pix[i+2] = uint8((db*a + rcb*ma) / mp)
x.Pix[i+3] = uint8((da*a + rca*ma) / mp)
}
}
// SpanFgColor draw the span using the fore ground color and the Porter-Duff composition operator.
func (x *ImgSpanner) SpanFgColor(yi, xi0, xi1 int, ma uint32) {
i0 := (yi)*x.Stride + (xi0)*4
i1 := i0 + (xi1-xi0)*4
// uses the Porter-Duff composition operator.
cr, cg, cb, ca := x.FgColor.RGBA()
ama := ca * ma
if ama == 0xFFFF*0xFFFF { // undercolor is ignored
rmb := uint8(cr * ma / mp)
gmb := uint8(cg * ma / mp)
bmb := uint8(cb * ma / mp)
amb := uint8(ama / mp)
for i := i0; i < i1; i += 4 {
x.Pix[i+0] = rmb
x.Pix[i+1] = gmb
x.Pix[i+2] = bmb
x.Pix[i+3] = amb
}
return
}
rma := cr * ma
gma := cg * ma
bma := cb * ma
a := (m - (ama / m)) * pa
for i := i0; i < i1; i += 4 {
x.Pix[i+0] = uint8((uint32(x.Pix[i+0])*a + rma) / mp)
x.Pix[i+1] = uint8((uint32(x.Pix[i+1])*a + gma) / mp)
x.Pix[i+2] = uint8((uint32(x.Pix[i+2])*a + bma) / mp)
x.Pix[i+3] = uint8((uint32(x.Pix[i+3])*a + ama) / mp)
}
}
// 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 paint
import (
"errors"
"fmt"
"image"
"runtime"
"sync"
"unicode"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"golang.org/x/image/font"
)
// Span contains fully explicit data needed for rendering a span of text
// as a slice of runes, with rune and Rune elements in one-to-one
// correspondence (but any nil values will use prior non-nil value -- first
// rune must have all non-nil). Text can be oriented in any direction -- the
// only constraint is that it starts from a single starting position.
// Typically only text within a span will obey kerning. In standard
// Text context, each span is one line of text -- should not have new
// lines within the span itself. In SVG special cases (e.g., TextPath), it
// can be anything. It is NOT synonymous with the HTML <span> tag, as many
// styling applications of that tag can be accommodated within a larger
// span-as-line. The first Rune RelPos for LR text should be at X=0
// (LastPos = 0 for RL) -- i.e., relpos positions are minimal for given span.
type Span struct {
// text as runes
Text []rune
// render info for each rune in one-to-one correspondence
Render []Rune
// position for start of text relative to an absolute coordinate that is provided at the time of rendering.
// This typically includes the baseline offset to align all rune rendering there.
// Individual rune RelPos are added to this plus the render-time offset to get the final position.
RelPos math32.Vector2
// rune position for further edge of last rune.
// For standard flat strings this is the overall length of the string.
// Used for size / layout computations: you do not add RelPos to this,
// as it is in same Text relative coordinates
LastPos math32.Vector2
// where relevant, this is the (default, dominant) text direction for the span
Dir styles.TextDirections
// mask of decorations that have been set on this span -- optimizes rendering passes
HasDeco styles.TextDecorations
}
func (sr *Span) Len() int {
return len(sr.Render)
}
// Init initializes a new span with given capacity
func (sr *Span) Init(capsz int) {
sr.Text = make([]rune, 0, capsz)
sr.Render = make([]Rune, 0, capsz)
sr.HasDeco = 0
}
// IsValid ensures that at least some text is represented and the sizes of
// Text and Render slices are the same, and that the first render info is non-nil
func (sr *Span) IsValid() error {
if len(sr.Text) == 0 {
return errors.New("core.Text: Text is empty")
}
if len(sr.Text) != len(sr.Render) {
return fmt.Errorf("core.Text: Render length %v != Text length %v for text: %v", len(sr.Render), len(sr.Text), string(sr.Text))
}
return sr.Render[0].HasNil()
}
// SizeHV computes the size of the text span from the first char to the last
// position, which is valid for purely horizontal or vertical text lines --
// either X or Y will be zero depending on orientation
func (sr *Span) SizeHV() math32.Vector2 {
if sr.IsValid() != nil {
return math32.Vector2{}
}
sz := sr.Render[0].RelPos.Sub(sr.LastPos)
if sz.X < 0 {
sz.X = -sz.X
}
if sz.Y < 0 {
sz.Y = -sz.Y
}
return sz
}
// SetBackground sets the BackgroundColor of the Runes to given value,
// if was not previously nil.
func (sr *Span) SetBackground(bg image.Image) {
if len(sr.Render) == 0 {
return
}
for i := range sr.Render {
rr := &sr.Render[i]
if rr.Background != nil {
rr.Background = bg
}
}
}
// RuneRelPos returns the relative (starting) position of the given rune index
// (adds Span RelPos and rune RelPos) -- this is typically the baseline
// position where rendering will start, not the upper left corner. if index >
// length, then uses LastPos
func (sr *Span) RuneRelPos(idx int) math32.Vector2 {
if idx >= len(sr.Render) {
return sr.LastPos
}
return sr.RelPos.Add(sr.Render[idx].RelPos)
}
// RuneEndPos returns the relative ending position of the given rune index
// (adds Span RelPos and rune RelPos + rune Size.X for LR writing). If index >
// length, then uses LastPos
func (sr *Span) RuneEndPos(idx int) math32.Vector2 {
if idx >= len(sr.Render) {
return sr.LastPos
}
spos := sr.RelPos.Add(sr.Render[idx].RelPos)
spos.X += sr.Render[idx].Size.X
return spos
}
// AppendRune adds one rune and associated formatting info
func (sr *Span) HasDecoUpdate(bg image.Image, deco styles.TextDecorations) {
sr.HasDeco |= deco
if bg != nil {
sr.HasDeco.SetFlag(true, styles.DecoBackgroundColor)
}
}
// IsNewPara returns true if this span starts a new paragraph
func (sr *Span) IsNewPara() bool {
if len(sr.Render) == 0 {
return false
}
return sr.Render[0].Deco.HasFlag(styles.DecoParaStart)
}
// SetNewPara sets this as starting a new paragraph
func (sr *Span) SetNewPara() {
if len(sr.Render) > 0 {
sr.Render[0].Deco.SetFlag(true, styles.DecoParaStart)
}
}
// AppendRune adds one rune and associated formatting info
func (sr *Span) AppendRune(r rune, face font.Face, clr image.Image, bg image.Image, deco styles.TextDecorations) {
sr.Text = append(sr.Text, r)
rr := Rune{Face: face, Color: clr, Background: bg, Deco: deco}
sr.Render = append(sr.Render, rr)
sr.HasDecoUpdate(bg, deco)
}
// AppendString adds string and associated formatting info, optimized with
// only first rune having non-nil face and color settings
func (sr *Span) AppendString(str string, face font.Face, clr image.Image, bg image.Image, deco styles.TextDecorations, sty *styles.FontRender, ctxt *units.Context) {
if len(str) == 0 {
return
}
ucfont := &styles.FontRender{}
if runtime.GOOS == "darwin" {
ucfont.Family = "Arial Unicode"
} else {
ucfont.Family = "Arial"
}
ucfont.Size = sty.Size
ucfont.Font = OpenFont(ucfont, ctxt) // note: this is lightweight once loaded in library
TextFontRenderMu.Lock()
defer TextFontRenderMu.Unlock()
nwr := []rune(str)
sz := len(nwr)
sr.Text = append(sr.Text, nwr...)
rr := Rune{Face: face, Color: clr, Background: bg, Deco: deco}
r := nwr[0]
lastUc := false
if _, ok := face.GlyphAdvance(r); !ok {
rr.Face = ucfont.Face.Face
lastUc = true
}
sr.HasDecoUpdate(bg, deco)
sr.Render = append(sr.Render, rr)
for i := 1; i < sz; i++ { // optimize by setting rest to nil for same
rp := Rune{Deco: deco, Background: bg}
r := nwr[i]
if _, ok := face.GlyphAdvance(r); !ok {
if !lastUc {
rp.Face = ucfont.Face.Face
lastUc = true
}
} else {
if lastUc {
rp.Face = face
lastUc = false
}
}
// }
sr.Render = append(sr.Render, rp)
}
}
// SetRenders sets rendering parameters based on style
func (sr *Span) SetRenders(sty *styles.FontRender, uc *units.Context, noBG bool, rot, scalex float32) {
sz := len(sr.Text)
if sz == 0 {
return
}
bgc := sty.Background
ucfont := &styles.FontRender{}
ucfont.Family = "Arial Unicode"
ucfont.Size = sty.Size
ucfont.Font = OpenFont(ucfont, uc)
sr.HasDecoUpdate(bgc, sty.Decoration)
sr.Render = make([]Rune, sz)
if sty.Face == nil {
sr.Render[0].Face = ucfont.Face.Face
} else {
sr.Render[0].Face = sty.Face.Face
}
sr.Render[0].Color = sty.Color
sr.Render[0].Background = bgc
sr.Render[0].RotRad = rot
sr.Render[0].ScaleX = scalex
if bgc != nil {
for i := range sr.Text {
sr.Render[i].Background = bgc
}
}
if rot != 0 || scalex != 0 {
for i := range sr.Text {
sr.Render[i].RotRad = rot
sr.Render[i].ScaleX = scalex
}
}
if sty.Decoration != styles.DecoNone {
for i := range sr.Text {
sr.Render[i].Deco = sty.Decoration
}
}
// use unicode font for all non-ascii symbols
lastUc := false
for i, r := range sr.Text {
if _, ok := sty.Face.Face.GlyphAdvance(r); !ok {
if !lastUc {
sr.Render[i].Face = ucfont.Face.Face
lastUc = true
}
} else {
if lastUc {
sr.Render[i].Face = sty.Face.Face
lastUc = false
}
}
}
}
// SetString initializes to given plain text string, with given default style
// parameters that are set for the first render element -- constructs Render
// slice of same size as Text
func (sr *Span) SetString(str string, sty *styles.FontRender, ctxt *units.Context, noBG bool, rot, scalex float32) {
sr.Text = []rune(str)
sr.SetRenders(sty, ctxt, noBG, rot, scalex)
}
// SetRunes initializes to given plain rune string, with given default style
// parameters that are set for the first render element -- constructs Render
// slice of same size as Text
func (sr *Span) SetRunes(str []rune, sty *styles.FontRender, ctxt *units.Context, noBG bool, rot, scalex float32) {
sr.Text = str
sr.SetRenders(sty, ctxt, noBG, rot, scalex)
}
// UpdateColors sets the font styling colors the first rune
// based on the given font style parameters.
func (sr *Span) UpdateColors(sty *styles.FontRender) {
if len(sr.Render) == 0 {
return
}
r := &sr.Render[0]
if sty.Color != nil {
r.Color = sty.Color
}
if sty.Background != nil {
r.Background = sty.Background
}
}
// TextFontRenderMu mutex is required because multiple different goroutines
// associated with different windows can (and often will be) call font stuff
// at the same time (curFace.GlyphAdvance, rendering font) at the same time, on
// the same font face -- and that turns out not to work!
var TextFontRenderMu sync.Mutex
// SetRunePosLR sets relative positions of each rune using a flat
// left-to-right text layout, based on font size info and additional extra
// letter and word spacing parameters (which can be negative)
func (sr *Span) SetRunePosLR(letterSpace, wordSpace, chsz float32, tabSize int) {
if err := sr.IsValid(); err != nil {
// log.Println(err)
return
}
sr.Dir = styles.LRTB
sz := len(sr.Text)
prevR := rune(-1)
lspc := letterSpace
wspc := wordSpace
if tabSize == 0 {
tabSize = 4
}
var fpos float32
curFace := sr.Render[0].Face
TextFontRenderMu.Lock()
defer TextFontRenderMu.Unlock()
for i, r := range sr.Text {
rr := &(sr.Render[i])
curFace = rr.CurFace(curFace)
fht := math32.FromFixed(curFace.Metrics().Height)
if prevR >= 0 {
fpos += math32.FromFixed(curFace.Kern(prevR, r))
}
rr.RelPos.X = fpos
rr.RelPos.Y = 0
if rr.Deco.HasFlag(styles.DecoSuper) {
rr.RelPos.Y = -0.45 * math32.FromFixed(curFace.Metrics().Ascent)
}
if rr.Deco.HasFlag(styles.DecoSub) {
rr.RelPos.Y = 0.15 * math32.FromFixed(curFace.Metrics().Ascent)
}
// todo: could check for various types of special unicode space chars here
a, _ := curFace.GlyphAdvance(r)
a32 := math32.FromFixed(a)
if a32 == 0 {
a32 = .1 * fht // something..
}
rr.Size = math32.Vec2(a32, fht)
if r == '\t' {
col := int(math32.Ceil(fpos / chsz))
curtab := col / tabSize
curtab++
col = curtab * tabSize
cpos := chsz * float32(col)
if cpos > fpos {
fpos = cpos
}
} else {
fpos += a32
if i < sz-1 {
fpos += lspc
if unicode.IsSpace(r) {
fpos += wspc
}
}
}
prevR = r
}
sr.LastPos.X = fpos
sr.LastPos.Y = 0
}
// SetRunePosTB sets relative positions of each rune using a flat
// top-to-bottom text layout -- i.e., letters are in their normal
// upright orientation, but arranged vertically.
func (sr *Span) SetRunePosTB(letterSpace, wordSpace, chsz float32, tabSize int) {
if err := sr.IsValid(); err != nil {
// log.Println(err)
return
}
sr.Dir = styles.TB
sz := len(sr.Text)
lspc := letterSpace
wspc := wordSpace
if tabSize == 0 {
tabSize = 4
}
var fpos float32
curFace := sr.Render[0].Face
TextFontRenderMu.Lock()
defer TextFontRenderMu.Unlock()
col := 0 // current column position -- todo: does NOT deal with indent
for i, r := range sr.Text {
rr := &(sr.Render[i])
curFace = rr.CurFace(curFace)
fht := math32.FromFixed(curFace.Metrics().Height)
rr.RelPos.X = 0
rr.RelPos.Y = fpos
if rr.Deco.HasFlag(styles.DecoSuper) {
rr.RelPos.Y = -0.45 * math32.FromFixed(curFace.Metrics().Ascent)
}
if rr.Deco.HasFlag(styles.DecoSub) {
rr.RelPos.Y = 0.15 * math32.FromFixed(curFace.Metrics().Ascent)
}
// todo: could check for various types of special unicode space chars here
a, _ := curFace.GlyphAdvance(r)
a32 := math32.FromFixed(a)
if a32 == 0 {
a32 = .1 * fht // something..
}
rr.Size = math32.Vec2(a32, fht)
if r == '\t' {
curtab := col / tabSize
curtab++
col = curtab * tabSize
cpos := chsz * float32(col)
if cpos > fpos {
fpos = cpos
}
} else {
fpos += fht
col++
if i < sz-1 {
fpos += lspc
if unicode.IsSpace(r) {
fpos += wspc
}
}
}
}
sr.LastPos.Y = fpos
sr.LastPos.X = 0
}
// SetRunePosTBRot sets relative positions of each rune using a flat
// top-to-bottom text layout, with characters rotated 90 degress
// based on font size info and additional extra letter and word spacing
// parameters (which can be negative)
func (sr *Span) SetRunePosTBRot(letterSpace, wordSpace, chsz float32, tabSize int) {
if err := sr.IsValid(); err != nil {
// log.Println(err)
return
}
sr.Dir = styles.TB
sz := len(sr.Text)
prevR := rune(-1)
lspc := letterSpace
wspc := wordSpace
if tabSize == 0 {
tabSize = 4
}
var fpos float32
curFace := sr.Render[0].Face
TextFontRenderMu.Lock()
defer TextFontRenderMu.Unlock()
col := 0 // current column position -- todo: does NOT deal with indent
for i, r := range sr.Text {
rr := &(sr.Render[i])
rr.RotRad = math32.Pi / 2
curFace = rr.CurFace(curFace)
fht := math32.FromFixed(curFace.Metrics().Height)
if prevR >= 0 {
fpos += math32.FromFixed(curFace.Kern(prevR, r))
}
rr.RelPos.Y = fpos
rr.RelPos.X = 0
if rr.Deco.HasFlag(styles.DecoSuper) {
rr.RelPos.X = -0.45 * math32.FromFixed(curFace.Metrics().Ascent)
}
if rr.Deco.HasFlag(styles.DecoSub) {
rr.RelPos.X = 0.15 * math32.FromFixed(curFace.Metrics().Ascent)
}
// todo: could check for various types of special unicode space chars here
a, _ := curFace.GlyphAdvance(r)
a32 := math32.FromFixed(a)
if a32 == 0 {
a32 = .1 * fht // something..
}
rr.Size = math32.Vec2(fht, a32)
if r == '\t' {
curtab := col / tabSize
curtab++
col = curtab * tabSize
cpos := chsz * float32(col)
if cpos > fpos {
fpos = cpos
}
} else {
fpos += a32
col++
if i < sz-1 {
fpos += lspc
if unicode.IsSpace(r) {
fpos += wspc
}
}
}
prevR = r
}
sr.LastPos.Y = fpos
sr.LastPos.X = 0
}
// FindWrapPosLR finds a position to do word wrapping to fit within trgSize.
// RelPos positions must have already been set (e.g., SetRunePosLR)
func (sr *Span) FindWrapPosLR(trgSize, curSize float32) int {
sz := len(sr.Text)
if sz == 0 {
return -1
}
idx := int(float32(sz) * (trgSize / curSize))
if idx >= sz {
idx = sz - 1
}
// find starting index that is just within size
csz := sr.RelPos.X + sr.Render[idx].RelPosAfterLR()
if csz > trgSize {
for idx > 0 {
csz = sr.RelPos.X + sr.Render[idx].RelPosAfterLR()
if csz <= trgSize {
break
}
idx--
}
} else {
for idx < sz-1 {
nsz := sr.RelPos.X + sr.Render[idx+1].RelPosAfterLR()
if nsz > trgSize {
break
}
idx++
}
}
fitIdx := idx
if unicode.IsSpace(sr.Text[idx]) {
idx++
for idx < sz && unicode.IsSpace(sr.Text[idx]) { // break at END of whitespace
idx++
}
return idx
}
// find earlier space
for idx > 0 && !unicode.IsSpace(sr.Text[idx-1]) {
idx--
}
if idx > 0 {
return idx
}
// no spaces within size: do a hard break at point
return fitIdx
}
// ZeroPos ensures that the positions start at 0, for LR direction
func (sr *Span) ZeroPosLR() {
sz := len(sr.Text)
if sz == 0 {
return
}
sx := sr.Render[0].RelPos.X
if sx == 0 {
return
}
for i := range sr.Render {
sr.Render[i].RelPos.X -= sx
}
sr.LastPos.X -= sx
}
// TrimSpaceLeft trims leading space elements from span, and updates the
// relative positions accordingly, for LR direction
func (sr *Span) TrimSpaceLeftLR() {
srr0 := sr.Render[0]
for range sr.Text {
if unicode.IsSpace(sr.Text[0]) {
sr.Text = sr.Text[1:]
sr.Render = sr.Render[1:]
if len(sr.Render) > 0 {
if sr.Render[0].Face == nil {
sr.Render[0].Face = srr0.Face
}
if sr.Render[0].Color == nil {
sr.Render[0].Color = srr0.Color
}
}
} else {
break
}
}
sr.ZeroPosLR()
}
// TrimSpaceRight trims trailing space elements from span, and updates the
// relative positions accordingly, for LR direction
func (sr *Span) TrimSpaceRightLR() {
for range sr.Text {
lidx := len(sr.Text) - 1
if unicode.IsSpace(sr.Text[lidx]) {
sr.Text = sr.Text[:lidx]
sr.Render = sr.Render[:lidx]
lidx--
if lidx >= 0 {
sr.LastPos.X = sr.Render[lidx].RelPosAfterLR()
} else {
sr.LastPos.X = sr.Render[0].Size.X
}
} else {
break
}
}
}
// TrimSpace trims leading and trailing space elements from span, and updates
// the relative positions accordingly, for LR direction
func (sr *Span) TrimSpaceLR() {
sr.TrimSpaceLeftLR()
sr.TrimSpaceRightLR()
}
// SplitAt splits current span at given index, returning a new span with
// remainder after index -- space is trimmed from both spans and relative
// positions updated, for LR direction
func (sr *Span) SplitAtLR(idx int) *Span {
if idx <= 0 || idx >= len(sr.Text)-1 { // shouldn't happen
return nil
}
nsr := Span{Text: sr.Text[idx:], Render: sr.Render[idx:], Dir: sr.Dir, HasDeco: sr.HasDeco}
sr.Text = sr.Text[:idx]
sr.Render = sr.Render[:idx]
sr.LastPos.X = sr.Render[idx-1].RelPosAfterLR()
// sr.TrimSpaceLR()
// nsr.TrimSpaceLeftLR() // don't trim right!
// go back and find latest face and color -- each sr must start with valid one
if len(nsr.Render) > 0 {
nrr0 := &(nsr.Render[0])
face, color := sr.LastFont()
if nrr0.Face == nil {
nrr0.Face = face
}
if nrr0.Color == nil {
nrr0.Color = color
}
}
return &nsr
}
// LastFont finds the last font and color from given span
func (sr *Span) LastFont() (face font.Face, color image.Image) {
for i := len(sr.Render) - 1; i >= 0; i-- {
srr := sr.Render[i]
if face == nil && srr.Face != nil {
face = srr.Face
if face != nil && color != nil {
break
}
}
if color == nil && srr.Color != nil {
color = srr.Color
if face != nil && color != nil {
break
}
}
}
return
}
// RenderBg renders the background behind chars
func (sr *Span) RenderBg(pc *Context, tpos math32.Vector2) {
curFace := sr.Render[0].Face
didLast := false
// first := true
for i := range sr.Text {
rr := &(sr.Render[i])
if rr.Background == nil {
if didLast {
pc.Fill()
}
didLast = false
continue
}
curFace = rr.CurFace(curFace)
dsc32 := math32.FromFixed(curFace.Metrics().Descent)
rp := tpos.Add(rr.RelPos)
scx := float32(1)
if rr.ScaleX != 0 {
scx = rr.ScaleX
}
tx := math32.Scale2D(scx, 1).Rotate(rr.RotRad)
ll := rp.Add(tx.MulVector2AsVector(math32.Vec2(0, dsc32)))
ur := ll.Add(tx.MulVector2AsVector(math32.Vec2(rr.Size.X, -rr.Size.Y)))
if int(math32.Floor(ll.X)) > pc.Bounds.Max.X || int(math32.Floor(ur.Y)) > pc.Bounds.Max.Y ||
int(math32.Ceil(ur.X)) < pc.Bounds.Min.X || int(math32.Ceil(ll.Y)) < pc.Bounds.Min.Y {
if didLast {
pc.Fill()
}
didLast = false
continue
}
pc.FillStyle.Color = rr.Background
szt := math32.Vec2(rr.Size.X, -rr.Size.Y)
sp := rp.Add(tx.MulVector2AsVector(math32.Vec2(0, dsc32)))
ul := sp.Add(tx.MulVector2AsVector(math32.Vec2(0, szt.Y)))
lr := sp.Add(tx.MulVector2AsVector(math32.Vec2(szt.X, 0)))
pc.DrawPolygon([]math32.Vector2{sp, ul, ur, lr})
didLast = true
}
if didLast {
pc.Fill()
}
}
// RenderUnderline renders the underline for span -- ensures continuity to do it all at once
func (sr *Span) RenderUnderline(pc *Context, tpos math32.Vector2) {
curFace := sr.Render[0].Face
curColor := sr.Render[0].Color
didLast := false
for i, r := range sr.Text {
if !unicode.IsPrint(r) {
continue
}
rr := &(sr.Render[i])
if !(rr.Deco.HasFlag(styles.Underline) || rr.Deco.HasFlag(styles.DecoDottedUnderline)) {
if didLast {
pc.Stroke()
}
didLast = false
continue
}
curFace = rr.CurFace(curFace)
if rr.Color != nil {
curColor = rr.Color
}
dsc32 := math32.FromFixed(curFace.Metrics().Descent)
rp := tpos.Add(rr.RelPos)
scx := float32(1)
if rr.ScaleX != 0 {
scx = rr.ScaleX
}
tx := math32.Scale2D(scx, 1).Rotate(rr.RotRad)
ll := rp.Add(tx.MulVector2AsVector(math32.Vec2(0, dsc32)))
ur := ll.Add(tx.MulVector2AsVector(math32.Vec2(rr.Size.X, -rr.Size.Y)))
if int(math32.Floor(ll.X)) > pc.Bounds.Max.X || int(math32.Floor(ur.Y)) > pc.Bounds.Max.Y ||
int(math32.Ceil(ur.X)) < pc.Bounds.Min.X || int(math32.Ceil(ll.Y)) < pc.Bounds.Min.Y {
if didLast {
pc.Stroke()
}
continue
}
dw := .05 * rr.Size.Y
if !didLast {
pc.StrokeStyle.Width.Dots = dw
pc.StrokeStyle.Color = curColor
}
if rr.Deco.HasFlag(styles.DecoDottedUnderline) {
pc.StrokeStyle.Dashes = []float32{2, 2}
}
sp := rp.Add(tx.MulVector2AsVector(math32.Vec2(0, 2*dw)))
ep := rp.Add(tx.MulVector2AsVector(math32.Vec2(rr.Size.X, 2*dw)))
if didLast {
pc.LineTo(sp.X, sp.Y)
} else {
pc.NewSubPath()
pc.MoveTo(sp.X, sp.Y)
}
pc.LineTo(ep.X, ep.Y)
didLast = true
}
if didLast {
pc.Stroke()
}
pc.StrokeStyle.Dashes = nil
}
// RenderLine renders overline or line-through -- anything that is a function of ascent
func (sr *Span) RenderLine(pc *Context, tpos math32.Vector2, deco styles.TextDecorations, ascPct float32) {
curFace := sr.Render[0].Face
curColor := sr.Render[0].Color
didLast := false
for i, r := range sr.Text {
if !unicode.IsPrint(r) {
continue
}
rr := &(sr.Render[i])
if !rr.Deco.HasFlag(deco) {
if didLast {
pc.Stroke()
}
didLast = false
continue
}
curFace = rr.CurFace(curFace)
dsc32 := math32.FromFixed(curFace.Metrics().Descent)
asc32 := math32.FromFixed(curFace.Metrics().Ascent)
rp := tpos.Add(rr.RelPos)
scx := float32(1)
if rr.ScaleX != 0 {
scx = rr.ScaleX
}
tx := math32.Scale2D(scx, 1).Rotate(rr.RotRad)
ll := rp.Add(tx.MulVector2AsVector(math32.Vec2(0, dsc32)))
ur := ll.Add(tx.MulVector2AsVector(math32.Vec2(rr.Size.X, -rr.Size.Y)))
if int(math32.Floor(ll.X)) > pc.Bounds.Max.X || int(math32.Floor(ur.Y)) > pc.Bounds.Max.Y ||
int(math32.Ceil(ur.X)) < pc.Bounds.Min.X || int(math32.Ceil(ll.Y)) < pc.Bounds.Min.Y {
if didLast {
pc.Stroke()
}
continue
}
if rr.Color != nil {
curColor = rr.Color
}
dw := 0.05 * rr.Size.Y
if !didLast {
pc.StrokeStyle.Width.Dots = dw
pc.StrokeStyle.Color = curColor
}
yo := ascPct * asc32
sp := rp.Add(tx.MulVector2AsVector(math32.Vec2(0, -yo)))
ep := rp.Add(tx.MulVector2AsVector(math32.Vec2(rr.Size.X, -yo)))
if didLast {
pc.LineTo(sp.X, sp.Y)
} else {
pc.NewSubPath()
pc.MoveTo(sp.X, sp.Y)
}
pc.LineTo(ep.X, ep.Y)
didLast = true
}
if didLast {
pc.Stroke()
}
}
// 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 paint
import (
"image"
"io"
"log/slog"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint/raster"
"cogentcore.org/core/paint/scan"
"cogentcore.org/core/styles"
)
// The State holds all the current rendering state information used
// while painting -- a viewport just has one of these
type State struct {
// current transform
CurrentTransform math32.Matrix2
// current path
Path raster.Path
// rasterizer -- stroke / fill rendering engine from raster
Raster *raster.Dasher
// scan scanner
Scanner *scan.Scanner
// scan spanner
ImgSpanner *scan.ImgSpanner
// starting point, for close path
Start math32.Vector2
// current point
Current math32.Vector2
// is current point current?
HasCurrent bool
// pointer to image to render into
Image *image.RGBA
// current mask
Mask *image.Alpha
// Bounds are the boundaries to restrict drawing to.
// This is much faster than using a clip mask for basic
// square region exclusion.
Bounds image.Rectangle
// bounding box of last object rendered; computed by renderer during Fill or Stroke, grabbed by SVG objects
LastRenderBBox image.Rectangle
// stack of transforms
TransformStack []math32.Matrix2
// BoundsStack is a stack of parent bounds.
// Every render starts with a push onto this stack, and finishes with a pop.
BoundsStack []image.Rectangle
// Radius is the border radius of the element that is currently being rendered.
// This is only relevant when using [State.PushBoundsGeom].
Radius styles.SideFloats
// RadiusStack is a stack of the border radii for the parent elements,
// with each one corresponding to the entry with the same index in
// [State.BoundsStack]. This is only relevant when using [State.PushBoundsGeom].
RadiusStack []styles.SideFloats
// stack of clips, if needed
ClipStack []*image.Alpha
// if non-nil, SVG output of paint commands is sent here
SVGOut io.Writer
}
// Init initializes the [State]. It must be called whenever the image size changes.
func (rs *State) Init(width, height int, img *image.RGBA) {
rs.CurrentTransform = math32.Identity2()
rs.Image = img
rs.ImgSpanner = scan.NewImgSpanner(img)
rs.Scanner = scan.NewScanner(rs.ImgSpanner, width, height)
rs.Raster = raster.NewDasher(width, height, rs.Scanner)
}
// PushTransform pushes current transform onto stack and apply new transform on top of it
// must protect within render mutex lock (see Lock version)
func (rs *State) PushTransform(tf math32.Matrix2) {
if rs.TransformStack == nil {
rs.TransformStack = make([]math32.Matrix2, 0)
}
rs.TransformStack = append(rs.TransformStack, rs.CurrentTransform)
rs.CurrentTransform.SetMul(tf)
}
// PopTransform pops transform off the stack and set to current transform
// must protect within render mutex lock (see Lock version)
func (rs *State) PopTransform() {
sz := len(rs.TransformStack)
if sz == 0 {
slog.Error("programmer error: paint.State.PopTransform: stack is empty")
rs.CurrentTransform = math32.Identity2()
return
}
rs.CurrentTransform = rs.TransformStack[sz-1]
rs.TransformStack = rs.TransformStack[:sz-1]
}
// PushBounds pushes the current bounds onto the stack and sets new bounds.
// This is the essential first step in rendering. See [State.PushBoundsGeom]
// for a version that takes more arguments.
func (rs *State) PushBounds(b image.Rectangle) {
rs.PushBoundsGeom(b, styles.SideFloats{})
}
// PushBoundsGeom pushes the current bounds onto the stack and sets new bounds.
// This is the essential first step in rendering. It also takes the border radius
// of the current element.
func (rs *State) PushBoundsGeom(total image.Rectangle, radius styles.SideFloats) {
if rs.Bounds.Empty() {
rs.Bounds = rs.Image.Bounds()
}
rs.BoundsStack = append(rs.BoundsStack, rs.Bounds)
rs.RadiusStack = append(rs.RadiusStack, rs.Radius)
rs.Bounds = total
rs.Radius = radius
}
// PopBounds pops the bounds off the stack and sets the current bounds.
// This must be equally balanced with corresponding [State.PushBounds] calls.
func (rs *State) PopBounds() {
sz := len(rs.BoundsStack)
if sz == 0 {
slog.Error("programmer error: paint.State.PopBounds: stack is empty")
rs.Bounds = rs.Image.Bounds()
return
}
rs.Bounds = rs.BoundsStack[sz-1]
rs.Radius = rs.RadiusStack[sz-1]
rs.BoundsStack = rs.BoundsStack[:sz-1]
rs.RadiusStack = rs.RadiusStack[:sz-1]
}
// PushClip pushes current Mask onto the clip stack
func (rs *State) PushClip() {
if rs.Mask == nil {
return
}
if rs.ClipStack == nil {
rs.ClipStack = make([]*image.Alpha, 0, 10)
}
rs.ClipStack = append(rs.ClipStack, rs.Mask)
}
// PopClip pops Mask off the clip stack and set to current mask
func (rs *State) PopClip() {
sz := len(rs.ClipStack)
if sz == 0 {
slog.Error("programmer error: paint.State.PopClip: stack is empty")
rs.Mask = nil // implied
return
}
rs.Mask = rs.ClipStack[sz-1]
rs.ClipStack[sz-1] = nil
rs.ClipStack = rs.ClipStack[:sz-1]
}
// Size returns the size of the underlying image as a [math32.Vector2].
func (rs *State) Size() math32.Vector2 {
return math32.FromPoint(rs.Image.Rect.Size())
}
// Copyright (c) 2024, 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 paint
import (
"fmt"
"image"
"cogentcore.org/core/colors"
)
// SVGStart returns the start of an SVG based on the current context state
func (pc *Context) SVGStart() string {
sz := pc.Image.Bounds().Size()
return fmt.Sprintf(`<svg width="%dpx" height="%dpx">\n`, sz.X, sz.Y)
}
// SVGEnd returns the end of an SVG based on the current context state
func (pc *Context) SVGEnd() string {
return "</svg>"
}
// SVGPath generates an SVG path representation of the current Path
func (pc *Context) SVGPath() string {
style := pc.SVGStrokeStyle() + pc.SVGFillStyle()
return `<path style="` + style + `" d="` + pc.Path.ToSVGPath() + `"/>\n`
}
// SVGStrokeStyle returns the style string for current Stroke
func (pc *Context) SVGStrokeStyle() string {
if pc.StrokeStyle.Color == nil {
return "stroke:none;"
}
s := "stroke-width:" + fmt.Sprintf("%g", pc.StrokeWidth()) + ";"
switch im := pc.StrokeStyle.Color.(type) {
case *image.Uniform:
s += "stroke:" + colors.AsHex(colors.AsRGBA(im)) + ";"
}
// todo: dashes, gradients
return s
}
// SVGFillStyle returns the style string for current Fill
func (pc *Context) SVGFillStyle() string {
if pc.FillStyle.Color == nil {
return "fill:none;"
}
s := ""
switch im := pc.FillStyle.Color.(type) {
case *image.Uniform:
s += "fill:" + colors.AsHex(colors.AsRGBA(im)) + ";"
}
// todo: gradients etc
return s
}
// 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 paint
import (
"bytes"
"encoding/xml"
"html"
"image"
"io"
"math"
"strings"
"unicode/utf8"
"unicode"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"golang.org/x/image/draw"
"golang.org/x/image/font"
"golang.org/x/image/math/f64"
"golang.org/x/net/html/charset"
)
// text.go contains all the core text rendering and formatting code -- see
// font.go for basic font-level style and management
//
// Styling, Formatting / Layout, and Rendering are each handled separately as
// three different levels in the stack -- simplifies many things to separate
// in this way, and makes the final render pass maximally efficient and
// high-performance, at the potential cost of some memory redundancy.
// todo: TB, RL cases -- layout is complicated.. with unicode-bidi, direction,
// writing-mode styles all interacting: https://www.w3.org/TR/SVG11/text.html#TextLayout
// Text contains one or more Span elements, typically with each
// representing a separate line of text (but they can be anything).
type Text struct {
Spans []Span
// bounding box for the rendered text. use Size() method to get the size.
BBox math32.Box2
// fontheight computed in last Layout
FontHeight float32
// lineheight computed in last Layout
LineHeight float32
// whether has had overflow in rendering
HasOverflow bool
// where relevant, this is the (default, dominant) text direction for the span
Dir styles.TextDirections
// hyperlinks within rendered text
Links []TextLink
}
// InsertSpan inserts a new span at given index
func (tr *Text) InsertSpan(at int, ns *Span) {
sz := len(tr.Spans)
tr.Spans = append(tr.Spans, Span{})
if at > sz-1 {
tr.Spans[sz] = *ns
return
}
copy(tr.Spans[at+1:], tr.Spans[at:])
tr.Spans[at] = *ns
}
// Render does text rendering into given image, within given bounds, at given
// absolute position offset (specifying position of text baseline) -- any
// applicable transforms (aside from the char-specific rotation in Render)
// must be applied in advance in computing the relative positions of the
// runes, and the overall font size, etc. todo: does not currently support
// stroking, only filling of text -- probably need to grab path from font and
// use paint rendering for stroking.
func (tr *Text) Render(pc *Context, pos math32.Vector2) {
// pr := profile.Start("RenderText")
// defer pr.End()
var ppaint styles.Paint
ppaint.CopyStyleFrom(pc.Paint)
pc.PushTransform(math32.Identity2()) // needed for SVG
defer pc.PopTransform()
pc.CurrentTransform = math32.Identity2()
TextFontRenderMu.Lock()
defer TextFontRenderMu.Unlock()
elipses := '…'
hadOverflow := false
rendOverflow := false
overBoxSet := false
var overStart math32.Vector2
var overBox math32.Box2
var overFace font.Face
var overColor image.Image
for _, sr := range tr.Spans {
if sr.IsValid() != nil {
continue
}
curFace := sr.Render[0].Face
curColor := sr.Render[0].Color
if g, ok := curColor.(gradient.Gradient); ok {
g.Update(pc.FontStyle.Opacity, math32.B2FromRect(pc.LastRenderBBox), pc.CurrentTransform)
} else {
curColor = gradient.ApplyOpacity(curColor, pc.FontStyle.Opacity)
}
tpos := pos.Add(sr.RelPos)
if !overBoxSet {
overWd, _ := curFace.GlyphAdvance(elipses)
overWd32 := math32.FromFixed(overWd)
overEnd := math32.FromPoint(pc.Bounds.Max)
overStart = overEnd.Sub(math32.Vec2(overWd32, 0.1*tr.FontHeight))
overBox = math32.Box2{Min: math32.Vec2(overStart.X, overEnd.Y-tr.FontHeight), Max: overEnd}
overFace = curFace
overColor = curColor
overBoxSet = true
}
d := &font.Drawer{
Dst: pc.Image,
Src: curColor,
Face: curFace,
}
// todo: cache flags if these are actually needed
if sr.HasDeco.HasFlag(styles.DecoBackgroundColor) {
// fmt.Println("rendering background color for span", rs)
sr.RenderBg(pc, tpos)
}
if sr.HasDeco.HasFlag(styles.Underline) || sr.HasDeco.HasFlag(styles.DecoDottedUnderline) {
sr.RenderUnderline(pc, tpos)
}
if sr.HasDeco.HasFlag(styles.Overline) {
sr.RenderLine(pc, tpos, styles.Overline, 1.1)
}
for i, r := range sr.Text {
rr := &(sr.Render[i])
if rr.Color != nil {
curColor := rr.Color
curColor = gradient.ApplyOpacity(curColor, pc.FontStyle.Opacity)
d.Src = curColor
}
curFace = rr.CurFace(curFace)
if !unicode.IsPrint(r) {
continue
}
dsc32 := math32.FromFixed(curFace.Metrics().Descent)
rp := tpos.Add(rr.RelPos)
scx := float32(1)
if rr.ScaleX != 0 {
scx = rr.ScaleX
}
tx := math32.Scale2D(scx, 1).Rotate(rr.RotRad)
ll := rp.Add(tx.MulVector2AsVector(math32.Vec2(0, dsc32)))
ur := ll.Add(tx.MulVector2AsVector(math32.Vec2(rr.Size.X, -rr.Size.Y)))
if int(math32.Ceil(ur.X)) < pc.Bounds.Min.X || int(math32.Ceil(ll.Y)) < pc.Bounds.Min.Y {
continue
}
doingOverflow := false
if tr.HasOverflow {
cmid := ll.Add(math32.Vec2(0.5*rr.Size.X, -0.5*rr.Size.Y))
if overBox.ContainsPoint(cmid) {
doingOverflow = true
r = elipses
}
}
if int(math32.Floor(ll.X)) > pc.Bounds.Max.X+1 || int(math32.Floor(ur.Y)) > pc.Bounds.Max.Y+1 {
hadOverflow = true
if !doingOverflow {
continue
}
}
if rendOverflow { // once you've rendered, no more rendering
continue
}
d.Face = curFace
d.Dot = rp.ToFixed()
dr, mask, maskp, _, ok := d.Face.Glyph(d.Dot, r)
if !ok {
// fmt.Printf("not ok rendering rune: %v\n", string(r))
continue
}
if rr.RotRad == 0 && (rr.ScaleX == 0 || rr.ScaleX == 1) {
idr := dr.Intersect(pc.Bounds)
soff := image.Point{}
if dr.Min.X < pc.Bounds.Min.X {
soff.X = pc.Bounds.Min.X - dr.Min.X
maskp.X += pc.Bounds.Min.X - dr.Min.X
}
if dr.Min.Y < pc.Bounds.Min.Y {
soff.Y = pc.Bounds.Min.Y - dr.Min.Y
maskp.Y += pc.Bounds.Min.Y - dr.Min.Y
}
draw.DrawMask(d.Dst, idr, d.Src, soff, mask, maskp, draw.Over)
} else {
srect := dr.Sub(dr.Min)
dbase := math32.Vec2(rp.X-float32(dr.Min.X), rp.Y-float32(dr.Min.Y))
transformer := draw.BiLinear
fx, fy := float32(dr.Min.X), float32(dr.Min.Y)
m := math32.Translate2D(fx+dbase.X, fy+dbase.Y).Scale(scx, 1).Rotate(rr.RotRad).Translate(-dbase.X, -dbase.Y)
s2d := f64.Aff3{float64(m.XX), float64(m.XY), float64(m.X0), float64(m.YX), float64(m.YY), float64(m.Y0)}
transformer.Transform(d.Dst, s2d, d.Src, srect, draw.Over, &draw.Options{
SrcMask: mask,
SrcMaskP: maskp,
})
}
if doingOverflow {
rendOverflow = true
}
}
if sr.HasDeco.HasFlag(styles.LineThrough) {
sr.RenderLine(pc, tpos, styles.LineThrough, 0.25)
}
}
tr.HasOverflow = hadOverflow
if hadOverflow && !rendOverflow && overBoxSet {
d := &font.Drawer{
Dst: pc.Image,
Src: overColor,
Face: overFace,
Dot: overStart.ToFixed(),
}
dr, mask, maskp, _, _ := d.Face.Glyph(d.Dot, elipses)
idr := dr.Intersect(pc.Bounds)
soff := image.Point{}
draw.DrawMask(d.Dst, idr, d.Src, soff, mask, maskp, draw.Over)
}
pc.Paint.CopyStyleFrom(&ppaint)
}
// RenderTopPos renders at given top position -- uses first font info to
// compute baseline offset and calls overall Render -- convenience for simple
// widget rendering without layouts
func (tr *Text) RenderTopPos(pc *Context, tpos math32.Vector2) {
if len(tr.Spans) == 0 {
return
}
sr := &(tr.Spans[0])
if sr.IsValid() != nil {
return
}
curFace := sr.Render[0].Face
pos := tpos
pos.Y += math32.FromFixed(curFace.Metrics().Ascent)
tr.Render(pc, pos)
}
// SetString is for basic text rendering with a single style of text (see
// SetHTML for tag-formatted text) -- configures a single Span with the
// entire string, and does standard layout (LR currently). rot and scalex are
// general rotation and x-scaling to apply to all chars -- alternatively can
// apply these per character after. Be sure that OpenFont has been run so a
// valid Face is available. noBG ignores any BackgroundColor in font style, and never
// renders background color
func (tr *Text) SetString(str string, fontSty *styles.FontRender, ctxt *units.Context, txtSty *styles.Text, noBG bool, rot, scalex float32) {
if len(tr.Spans) != 1 {
tr.Spans = make([]Span, 1)
}
tr.Links = nil
sr := &(tr.Spans[0])
sr.SetString(str, fontSty, ctxt, noBG, rot, scalex)
sr.SetRunePosLR(txtSty.LetterSpacing.Dots, txtSty.WordSpacing.Dots, fontSty.Face.Metrics.Ch, txtSty.TabSize)
ssz := sr.SizeHV()
vht := fontSty.Face.Face.Metrics().Height
tr.BBox.Min.SetZero()
tr.BBox.Max = math32.Vec2(ssz.X, math32.FromFixed(vht))
}
// SetStringRot90 is for basic text rendering with a single style of text (see
// SetHTML for tag-formatted text) -- configures a single Span with the
// entire string, and does TB rotated layout (-90 deg).
// Be sure that OpenFont has been run so a valid Face is available.
// noBG ignores any BackgroundColor in font style, and never renders background color
func (tr *Text) SetStringRot90(str string, fontSty *styles.FontRender, ctxt *units.Context, txtSty *styles.Text, noBG bool, scalex float32) {
if len(tr.Spans) != 1 {
tr.Spans = make([]Span, 1)
}
tr.Links = nil
sr := &(tr.Spans[0])
rot := float32(math32.Pi / 2)
sr.SetString(str, fontSty, ctxt, noBG, rot, scalex)
sr.SetRunePosTBRot(txtSty.LetterSpacing.Dots, txtSty.WordSpacing.Dots, fontSty.Face.Metrics.Ch, txtSty.TabSize)
ssz := sr.SizeHV()
vht := fontSty.Face.Face.Metrics().Height
tr.BBox.Min.SetZero()
tr.BBox.Max = math32.Vec2(math32.FromFixed(vht), ssz.Y)
}
// SetRunes is for basic text rendering with a single style of text (see
// SetHTML for tag-formatted text) -- configures a single Span with the
// entire string, and does standard layout (LR currently). rot and scalex are
// general rotation and x-scaling to apply to all chars -- alternatively can
// apply these per character after Be sure that OpenFont has been run so a
// valid Face is available. noBG ignores any BackgroundColor in font style, and never
// renders background color
func (tr *Text) SetRunes(str []rune, fontSty *styles.FontRender, ctxt *units.Context, txtSty *styles.Text, noBG bool, rot, scalex float32) {
if len(tr.Spans) != 1 {
tr.Spans = make([]Span, 1)
}
tr.Links = nil
sr := &(tr.Spans[0])
sr.SetRunes(str, fontSty, ctxt, noBG, rot, scalex)
sr.SetRunePosLR(txtSty.LetterSpacing.Dots, txtSty.WordSpacing.Dots, fontSty.Face.Metrics.Ch, txtSty.TabSize)
ssz := sr.SizeHV()
vht := fontSty.Face.Face.Metrics().Height
tr.BBox.Min.SetZero()
tr.BBox.Max = math32.Vec2(ssz.X, math32.FromFixed(vht))
}
// SetHTMLSimpleTag sets the styling parameters for simple html style tags
// that only require updating the given font spec values -- returns true if handled
// https://www.w3schools.com/cssref/css_default_values.asp
func SetHTMLSimpleTag(tag string, fs *styles.FontRender, ctxt *units.Context, cssAgg map[string]any) bool {
did := false
switch tag {
case "b", "strong":
fs.Weight = styles.WeightBold
fs.Font = OpenFont(fs, ctxt)
did = true
case "i", "em", "var", "cite":
fs.Style = styles.Italic
fs.Font = OpenFont(fs, ctxt)
did = true
case "ins":
fallthrough
case "u":
fs.SetDecoration(styles.Underline)
did = true
case "s", "del", "strike":
fs.SetDecoration(styles.LineThrough)
did = true
case "sup":
fs.SetDecoration(styles.DecoSuper)
curpts := math.Round(float64(fs.Size.Convert(units.UnitPt, ctxt).Value))
curpts -= 2
fs.Size = units.Pt(float32(curpts))
fs.Size.ToDots(ctxt)
fs.Font = OpenFont(fs, ctxt)
did = true
case "sub":
fs.SetDecoration(styles.DecoSub)
fallthrough
case "small":
curpts := math.Round(float64(fs.Size.Convert(units.UnitPt, ctxt).Value))
curpts -= 2
fs.Size = units.Pt(float32(curpts))
fs.Size.ToDots(ctxt)
fs.Font = OpenFont(fs, ctxt)
did = true
case "big":
curpts := math.Round(float64(fs.Size.Convert(units.UnitPt, ctxt).Value))
curpts += 2
fs.Size = units.Pt(float32(curpts))
fs.Size.ToDots(ctxt)
fs.Font = OpenFont(fs, ctxt)
did = true
case "xx-small", "x-small", "smallf", "medium", "large", "x-large", "xx-large":
fs.Size = units.Pt(styles.FontSizePoints[tag])
fs.Size.ToDots(ctxt)
fs.Font = OpenFont(fs, ctxt)
did = true
case "mark":
fs.Background = colors.Scheme.Warn.Container
did = true
case "abbr", "acronym":
fs.SetDecoration(styles.DecoDottedUnderline)
did = true
case "tt", "kbd", "samp", "code":
fs.Family = "monospace"
fs.Font = OpenFont(fs, ctxt)
fs.Background = colors.Scheme.SurfaceContainer
did = true
}
return did
}
// SetHTML sets text by decoding all standard inline HTML text style
// formatting tags in the string and sets the per-character font information
// appropriately, using given font style info. <P> and <BR> tags create new
// spans, with <P> marking start of subsequent span with DecoParaStart.
// Critically, it does NOT deal at all with layout (positioning) except in
// breaking lines into different spans, but not with word wrapping -- only
// sets font, color, and decoration info, and strips out the tags it processes
// -- result can then be processed by different layout algorithms as needed.
// cssAgg, if non-nil, should contain CSSAgg properties -- will be tested for
// special css styling of each element.
func (tr *Text) SetHTML(str string, font *styles.FontRender, txtSty *styles.Text, ctxt *units.Context, cssAgg map[string]any) {
if txtSty.HasPre() {
tr.SetHTMLPre([]byte(str), font, txtSty, ctxt, cssAgg)
} else {
tr.SetHTMLNoPre([]byte(str), font, txtSty, ctxt, cssAgg)
}
}
// SetHTMLBytes does SetHTML with bytes as input -- more efficient -- use this
// if already in bytes
func (tr *Text) SetHTMLBytes(str []byte, font *styles.FontRender, txtSty *styles.Text, ctxt *units.Context, cssAgg map[string]any) {
if txtSty.HasPre() {
tr.SetHTMLPre(str, font, txtSty, ctxt, cssAgg)
} else {
tr.SetHTMLNoPre(str, font, txtSty, ctxt, cssAgg)
}
}
// This is the No-Pre parser that uses the golang XML decoder system, which
// strips all whitespace and is thus unsuitable for any Pre case
func (tr *Text) SetHTMLNoPre(str []byte, font *styles.FontRender, txtSty *styles.Text, ctxt *units.Context, cssAgg map[string]any) {
// errstr := "core.Text SetHTML"
sz := len(str)
if sz == 0 {
return
}
tr.Spans = make([]Span, 1)
tr.Links = nil
curSp := &(tr.Spans[0])
initsz := min(sz, 1020)
curSp.Init(initsz)
spcstr := bytes.Join(bytes.Fields(str), []byte(" "))
reader := bytes.NewReader(spcstr)
decoder := xml.NewDecoder(reader)
decoder.Strict = false
decoder.AutoClose = xml.HTMLAutoClose
decoder.Entity = xml.HTMLEntity
decoder.CharsetReader = charset.NewReaderLabel
font.Font = OpenFont(font, ctxt)
// set when a </p> is encountered
nextIsParaStart := false
curLinkIndex := -1 // if currently processing an <a> link element
fstack := make([]*styles.FontRender, 1, 10)
fstack[0] = font
for {
t, err := decoder.Token()
if err != nil {
if err == io.EOF {
break
}
// log.Printf("%v parsing error: %v for string\n%v\n", errstr, err, string(str))
break
}
switch se := t.(type) {
case xml.StartElement:
curf := fstack[len(fstack)-1]
fs := *curf
nm := strings.ToLower(se.Name.Local)
curLinkIndex = -1
if !SetHTMLSimpleTag(nm, &fs, ctxt, cssAgg) {
switch nm {
case "a":
fs.Color = colors.Scheme.Primary.Base
fs.SetDecoration(styles.Underline)
curLinkIndex = len(tr.Links)
tl := &TextLink{StartSpan: len(tr.Spans) - 1, StartIndex: len(curSp.Text)}
sprop := make(map[string]any, len(se.Attr))
tl.Properties = sprop
for _, attr := range se.Attr {
if attr.Name.Local == "href" {
tl.URL = attr.Value
}
sprop[attr.Name.Local] = attr.Value
}
tr.Links = append(tr.Links, *tl)
case "span":
// just uses properties
case "q":
curf := fstack[len(fstack)-1]
atStart := len(curSp.Text) == 0
curSp.AppendRune('“', curf.Face.Face, curf.Color, curf.Background, curf.Decoration)
if nextIsParaStart && atStart {
curSp.SetNewPara()
}
nextIsParaStart = false
case "dfn":
// no default styling
case "bdo":
// bidirectional override..
case "p":
if len(curSp.Text) > 0 {
// fmt.Printf("para start: '%v'\n", string(curSp.Text))
tr.Spans = append(tr.Spans, Span{})
curSp = &(tr.Spans[len(tr.Spans)-1])
}
nextIsParaStart = true
case "br":
default:
// log.Printf("%v tag not recognized: %v for string\n%v\n", errstr, nm, string(str))
}
}
if len(se.Attr) > 0 {
sprop := make(map[string]any, len(se.Attr))
for _, attr := range se.Attr {
switch attr.Name.Local {
case "style":
styles.SetStylePropertiesXML(attr.Value, &sprop)
case "class":
if cssAgg != nil {
clnm := "." + attr.Value
if aggp, ok := styles.SubProperties(cssAgg, clnm); ok {
fs.SetStyleProperties(nil, aggp, nil)
fs.Font = OpenFont(&fs, ctxt)
}
}
default:
sprop[attr.Name.Local] = attr.Value
}
}
fs.SetStyleProperties(nil, sprop, nil)
fs.Font = OpenFont(&fs, ctxt)
}
if cssAgg != nil {
FontStyleCSS(&fs, nm, cssAgg, ctxt, nil)
}
fstack = append(fstack, &fs)
case xml.EndElement:
switch se.Name.Local {
case "p":
tr.Spans = append(tr.Spans, Span{})
curSp = &(tr.Spans[len(tr.Spans)-1])
nextIsParaStart = true
case "br":
tr.Spans = append(tr.Spans, Span{})
curSp = &(tr.Spans[len(tr.Spans)-1])
case "q":
curf := fstack[len(fstack)-1]
curSp.AppendRune('”', curf.Face.Face, curf.Color, curf.Background, curf.Decoration)
case "a":
if curLinkIndex >= 0 && curLinkIndex < len(tr.Links) {
tl := &tr.Links[curLinkIndex]
tl.EndSpan = len(tr.Spans) - 1
tl.EndIndex = len(curSp.Text)
curLinkIndex = -1
}
}
if len(fstack) > 1 {
fstack = fstack[:len(fstack)-1]
}
case xml.CharData:
curf := fstack[len(fstack)-1]
atStart := len(curSp.Text) == 0
sstr := html.UnescapeString(string(se))
if nextIsParaStart && atStart {
sstr = strings.TrimLeftFunc(sstr, func(r rune) bool {
return unicode.IsSpace(r)
})
}
curSp.AppendString(sstr, curf.Face.Face, curf.Color, curf.Background, curf.Decoration, font, ctxt)
if nextIsParaStart && atStart {
curSp.SetNewPara()
}
nextIsParaStart = false
if curLinkIndex >= 0 && curLinkIndex < len(tr.Links) {
tl := &tr.Links[curLinkIndex]
tl.Label = sstr
}
}
}
}
// note: adding print / log statements to following when inside gide will cause
// an infinite loop because the console redirection uses this very same code!
// SetHTMLPre sets preformatted HTML-styled text by decoding all standard
// inline HTML text style formatting tags in the string and sets the
// per-character font information appropriately, using given font style info.
// Only basic styling tags, including <span> elements with style parameters
// (including class names) are decoded. Whitespace is decoded as-is,
// including LF \n etc, except in WhiteSpacePreLine case which only preserves LF's
func (tr *Text) SetHTMLPre(str []byte, font *styles.FontRender, txtSty *styles.Text, ctxt *units.Context, cssAgg map[string]any) {
// errstr := "core.Text SetHTMLPre"
sz := len(str)
tr.Spans = make([]Span, 1)
tr.Links = nil
if sz == 0 {
return
}
curSp := &(tr.Spans[0])
initsz := min(sz, 1020)
curSp.Init(initsz)
font.Font = OpenFont(font, ctxt)
nextIsParaStart := false
curLinkIndex := -1 // if currently processing an <a> link element
fstack := make([]*styles.FontRender, 1, 10)
fstack[0] = font
tagstack := make([]string, 0, 10)
tmpbuf := make([]byte, 0, 1020)
bidx := 0
curTag := ""
for bidx < sz {
cb := str[bidx]
ftag := ""
if cb == '<' && sz > bidx+1 {
eidx := bytes.Index(str[bidx+1:], []byte(">"))
if eidx > 0 {
ftag = string(str[bidx+1 : bidx+1+eidx])
bidx += eidx + 2
} else { // get past <
curf := fstack[len(fstack)-1]
curSp.AppendString(string(str[bidx:bidx+1]), curf.Face.Face, curf.Color, curf.Background, curf.Decoration, font, ctxt)
bidx++
}
}
if ftag != "" {
if ftag[0] == '/' {
etag := strings.ToLower(ftag[1:])
// fmt.Printf("%v etag: %v\n", bidx, etag)
if etag == "pre" {
continue // ignore
}
if etag != curTag {
// log.Printf("%v end tag: %v doesn't match current tag: %v for string\n%v\n", errstr, etag, curTag, string(str))
}
switch etag {
// case "p":
// tr.Spans = append(tr.Spans, Span{})
// curSp = &(tr.Spans[len(tr.Spans)-1])
// nextIsParaStart = true
// case "br":
// tr.Spans = append(tr.Spans, Span{})
// curSp = &(tr.Spans[len(tr.Spans)-1])
case "q":
curf := fstack[len(fstack)-1]
curSp.AppendRune('”', curf.Face.Face, curf.Color, curf.Background, curf.Decoration)
case "a":
if curLinkIndex >= 0 && curLinkIndex < len(tr.Links) {
tl := &tr.Links[curLinkIndex]
tl.EndSpan = len(tr.Spans) - 1
tl.EndIndex = len(curSp.Text)
curLinkIndex = -1
}
}
if len(fstack) > 1 { // pop at end
fstack = fstack[:len(fstack)-1]
}
tslen := len(tagstack)
if tslen > 1 {
curTag = tagstack[tslen-2]
tagstack = tagstack[:tslen-1]
} else if tslen == 1 {
curTag = ""
tagstack = tagstack[:0]
}
} else { // start tag
parts := strings.Split(ftag, " ")
stag := strings.ToLower(strings.TrimSpace(parts[0]))
// fmt.Printf("%v stag: %v\n", bidx, stag)
attrs := parts[1:]
attr := strings.Split(strings.Join(attrs, " "), "=")
nattr := len(attr) / 2
curf := fstack[len(fstack)-1]
fs := *curf
curLinkIndex = -1
if !SetHTMLSimpleTag(stag, &fs, ctxt, cssAgg) {
switch stag {
case "a":
fs.Color = colors.Scheme.Primary.Base
fs.SetDecoration(styles.Underline)
curLinkIndex = len(tr.Links)
tl := &TextLink{StartSpan: len(tr.Spans) - 1, StartIndex: len(curSp.Text)}
if nattr > 0 {
sprop := make(map[string]any, len(parts)-1)
tl.Properties = sprop
for ai := 0; ai < nattr; ai++ {
nm := strings.TrimSpace(attr[ai*2])
vl := strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(attr[ai*2+1]), `"`), `"`)
if nm == "href" {
tl.URL = vl
}
sprop[nm] = vl
}
}
tr.Links = append(tr.Links, *tl)
case "span":
// just uses properties
case "q":
curf := fstack[len(fstack)-1]
atStart := len(curSp.Text) == 0
curSp.AppendRune('“', curf.Face.Face, curf.Color, curf.Background, curf.Decoration)
if nextIsParaStart && atStart {
curSp.SetNewPara()
}
nextIsParaStart = false
case "dfn":
// no default styling
case "bdo":
// bidirectional override..
// case "p":
// if len(curSp.Text) > 0 {
// // fmt.Printf("para start: '%v'\n", string(curSp.Text))
// tr.Spans = append(tr.Spans, Span{})
// curSp = &(tr.Spans[len(tr.Spans)-1])
// }
// nextIsParaStart = true
// case "br":
case "pre":
continue // ignore
default:
// log.Printf("%v tag not recognized: %v for string\n%v\n", errstr, stag, string(str))
// just ignore it and format as is, for pre case!
// todo: need to include
}
}
if nattr > 0 { // attr
sprop := make(map[string]any, nattr)
for ai := 0; ai < nattr; ai++ {
nm := strings.TrimSpace(attr[ai*2])
vl := strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(attr[ai*2+1]), `"`), `"`)
// fmt.Printf("nm: %v val: %v\n", nm, vl)
switch nm {
case "style":
styles.SetStylePropertiesXML(vl, &sprop)
case "class":
if cssAgg != nil {
clnm := "." + vl
if aggp, ok := styles.SubProperties(cssAgg, clnm); ok {
fs.SetStyleProperties(nil, aggp, nil)
fs.Font = OpenFont(&fs, ctxt)
}
}
default:
sprop[nm] = vl
}
}
fs.SetStyleProperties(nil, sprop, nil)
fs.Font = OpenFont(&fs, ctxt)
}
if cssAgg != nil {
FontStyleCSS(&fs, stag, cssAgg, ctxt, nil)
}
fstack = append(fstack, &fs)
curTag = stag
tagstack = append(tagstack, curTag)
}
} else { // raw chars
// todo: deal with WhiteSpacePreLine -- trim out non-LF ws
curf := fstack[len(fstack)-1]
// atStart := len(curSp.Text) == 0
tmpbuf := tmpbuf[0:0]
didNl := false
aggloop:
for ; bidx < sz; bidx++ {
nb := str[bidx] // re-gets cb so it can be processed here..
switch nb {
case '<':
if (bidx > 0 && str[bidx-1] == '<') || sz == bidx+1 {
tmpbuf = append(tmpbuf, nb)
didNl = false
} else {
didNl = false
break aggloop
}
case '\n': // todo absorb other line endings
unestr := html.UnescapeString(string(tmpbuf))
curSp.AppendString(unestr, curf.Face.Face, curf.Color, curf.Background, curf.Decoration, font, ctxt)
tmpbuf = tmpbuf[0:0]
tr.Spans = append(tr.Spans, Span{})
curSp = &(tr.Spans[len(tr.Spans)-1])
didNl = true
default:
didNl = false
tmpbuf = append(tmpbuf, nb)
}
}
if !didNl {
unestr := html.UnescapeString(string(tmpbuf))
// fmt.Printf("%v added: %v\n", bidx, unestr)
curSp.AppendString(unestr, curf.Face.Face, curf.Color, curf.Background, curf.Decoration, font, ctxt)
if curLinkIndex >= 0 && curLinkIndex < len(tr.Links) {
tl := &tr.Links[curLinkIndex]
tl.Label = unestr
}
}
}
}
}
//////////////////////////////////////////////////////////////////////////////////
// Utilities
func (tx *Text) String() string {
s := ""
for i := range tx.Spans {
sr := &tx.Spans[i]
s += string(sr.Text) + "\n"
}
return s
}
// UpdateColors sets the font styling colors the first rune
// based on the given font style parameters.
func (tx *Text) UpdateColors(sty *styles.FontRender) {
for i := range tx.Spans {
sr := &tx.Spans[i]
sr.UpdateColors(sty)
}
}
// SetBackground sets the BackgroundColor of the first Render in each Span
// to given value, if was not nil.
func (tx *Text) SetBackground(bg image.Image) {
for i := range tx.Spans {
sr := &tx.Spans[i]
sr.SetBackground(bg)
}
}
// NextRuneAt returns the next rune starting from given index -- could be at
// that index or some point thereafter -- returns utf8.RuneError if no valid
// rune could be found -- this should be a standard function!
func NextRuneAt(str string, idx int) rune {
r, _ := utf8.DecodeRuneInString(str[idx:])
return r
}
// 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 paint
import (
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"golang.org/x/image/font"
)
// RuneSpanPos returns the position (span, rune index within span) within a
// sequence of spans of a given absolute rune index, starting in the first
// span -- returns false if index is out of range (and returns the last position).
func (tx *Text) RuneSpanPos(idx int) (si, ri int, ok bool) {
if idx < 0 || len(tx.Spans) == 0 {
return 0, 0, false
}
ri = idx
for si = range tx.Spans {
if ri < 0 {
ri = 0
}
sr := &tx.Spans[si]
if ri >= len(sr.Render) {
ri -= len(sr.Render)
continue
}
return si, ri, true
}
si = len(tx.Spans) - 1
ri = len(tx.Spans[si].Render)
return si, ri, false
}
// SpanPosToRuneIndex returns the absolute rune index for a given span, rune
// index position -- i.e., the inverse of RuneSpanPos. Returns false if given
// input position is out of range, and returns last valid index in that case.
func (tx *Text) SpanPosToRuneIndex(si, ri int) (idx int, ok bool) {
idx = 0
for i := range tx.Spans {
sr := &tx.Spans[i]
if si > i {
idx += len(sr.Render)
continue
}
if ri <= len(sr.Render) {
return idx + ri, true
}
return idx + (len(sr.Render)), false
}
return 0, false
}
// RuneRelPos returns the relative (starting) position of the given rune
// index, counting progressively through all spans present (adds Span RelPos
// and rune RelPos) -- this is typically the baseline position where rendering
// will start, not the upper left corner. If index > length, then uses
// LastPos. Returns also the index of the span that holds that char (-1 = no
// spans at all) and the rune index within that span, and false if index is
// out of range.
func (tx *Text) RuneRelPos(idx int) (pos math32.Vector2, si, ri int, ok bool) {
si, ri, ok = tx.RuneSpanPos(idx)
if ok {
sr := &tx.Spans[si]
return sr.RelPos.Add(sr.Render[ri].RelPos), si, ri, true
}
nsp := len(tx.Spans)
if nsp > 0 {
sr := &tx.Spans[nsp-1]
return sr.LastPos, nsp - 1, len(sr.Render), false
}
return math32.Vector2{}, -1, -1, false
}
// RuneEndPos returns the relative ending position of the given rune index,
// counting progressively through all spans present(adds Span RelPos and rune
// RelPos + rune Size.X for LR writing). If index > length, then uses LastPos.
// Returns also the index of the span that holds that char (-1 = no spans at
// all) and the rune index within that span, and false if index is out of
// range.
func (tx *Text) RuneEndPos(idx int) (pos math32.Vector2, si, ri int, ok bool) {
si, ri, ok = tx.RuneSpanPos(idx)
if ok {
sr := &tx.Spans[si]
spos := sr.RelPos.Add(sr.Render[ri].RelPos)
spos.X += sr.Render[ri].Size.X
return spos, si, ri, true
}
nsp := len(tx.Spans)
if nsp > 0 {
sr := &tx.Spans[nsp-1]
return sr.LastPos, nsp - 1, len(sr.Render), false
}
return math32.Vector2{}, -1, -1, false
}
// PosToRune returns the rune span and rune indexes for given relative X,Y
// pixel position, if the pixel position lies within the given text area.
// If not, returns false. It is robust to left-right out-of-range positions,
// returning the first or last rune index respectively.
func (tx *Text) PosToRune(pos math32.Vector2) (si, ri int, ok bool) {
ok = false
if pos.X < 0 || pos.Y < 0 { // note: don't bail on X yet
return
}
sz := tx.BBox.Size()
if pos.Y >= sz.Y {
si = len(tx.Spans) - 1
sr := tx.Spans[si]
ri = len(sr.Render)
ok = true
return
}
if len(tx.Spans) == 0 {
ok = true
return
}
yoff := tx.Spans[0].RelPos.Y // baseline offset applied to everything
for li, sr := range tx.Spans {
st := sr.RelPos
st.Y -= yoff
lp := sr.LastPos
lp.Y += tx.LineHeight - yoff // todo: only for LR
b := math32.Box2{Min: st, Max: lp}
nr := len(sr.Render)
if !b.ContainsPoint(pos) {
if pos.Y >= st.Y && pos.Y < lp.Y {
if pos.X < st.X {
return li, 0, true
}
return li, nr + 1, true
}
continue
}
for j := range sr.Render {
r := &sr.Render[j]
sz := r.Size
sz.Y = tx.LineHeight // todo: only LR
if j < nr-1 {
nxt := &sr.Render[j+1]
sz.X = nxt.RelPos.X - r.RelPos.X
}
ep := st.Add(sz)
b := math32.Box2{Min: st, Max: ep}
if b.ContainsPoint(pos) {
return li, j, true
}
st.X += sz.X // todo: only LR
}
}
return 0, 0, false
}
//////////////////////////////////////////////////////////////////////////////////
// TextStyle-based Layout Routines
// Layout does basic standard layout of text using Text style parameters, assigning
// relative positions to spans and runes according to given styles, and given
// size overall box. Nonzero values used to constrain, with the width used as a
// hard constraint to drive word wrapping (if a word wrap style is present).
// Returns total resulting size box for text, which can be larger than the given
// size, if the text requires more size to fit everything.
// Font face in styles.Font is used for determining line spacing here.
// Other versions can do more expensive calculations of variable line spacing as needed.
func (tr *Text) Layout(txtSty *styles.Text, fontSty *styles.FontRender, ctxt *units.Context, size math32.Vector2) math32.Vector2 {
// todo: switch on layout types once others are supported
return tr.LayoutStdLR(txtSty, fontSty, ctxt, size)
}
// LayoutStdLR does basic standard layout of text in LR direction.
func (tr *Text) LayoutStdLR(txtSty *styles.Text, fontSty *styles.FontRender, ctxt *units.Context, size math32.Vector2) math32.Vector2 {
if len(tr.Spans) == 0 {
return math32.Vector2{}
}
// pr := profile.Start("TextLayout")
// defer pr.End()
//
tr.Dir = styles.LRTB
fontSty.Font = OpenFont(fontSty, ctxt)
fht := fontSty.Face.Metrics.Height
tr.FontHeight = fht
dsc := math32.FromFixed(fontSty.Face.Face.Metrics().Descent)
lspc := txtSty.EffLineHeight(fht)
tr.LineHeight = lspc
lpad := (lspc - fht) / 2 // padding above / below text box for centering in line
maxw := float32(0)
// first pass gets rune positions and wraps text as needed, and gets max width
si := 0
for si < len(tr.Spans) {
sr := &(tr.Spans[si])
if err := sr.IsValid(); err != nil {
si++
continue
}
if sr.LastPos.X == 0 { // don't re-do unless necessary
sr.SetRunePosLR(txtSty.LetterSpacing.Dots, txtSty.WordSpacing.Dots, fontSty.Face.Metrics.Ch, txtSty.TabSize)
}
if sr.IsNewPara() {
sr.RelPos.X = txtSty.Indent.Dots
} else {
sr.RelPos.X = 0
}
ssz := sr.SizeHV()
ssz.X += sr.RelPos.X
if size.X > 0 && ssz.X > size.X && txtSty.HasWordWrap() {
for {
wp := sr.FindWrapPosLR(size.X, ssz.X)
if wp > 0 && wp < len(sr.Text)-1 {
nsr := sr.SplitAtLR(wp)
tr.InsertSpan(si+1, nsr)
ssz = sr.SizeHV()
ssz.X += sr.RelPos.X
if ssz.X > maxw {
maxw = ssz.X
}
si++
if si >= len(tr.Spans) {
break
}
sr = &(tr.Spans[si]) // keep going with nsr
sr.SetRunePosLR(txtSty.LetterSpacing.Dots, txtSty.WordSpacing.Dots, fontSty.Face.Metrics.Ch, txtSty.TabSize)
ssz = sr.SizeHV()
// fixup links
for li := range tr.Links {
tl := &tr.Links[li]
if tl.StartSpan == si-1 {
if tl.StartIndex >= wp {
tl.StartIndex -= wp
tl.StartSpan++
}
} else if tl.StartSpan > si-1 {
tl.StartSpan++
}
if tl.EndSpan == si-1 {
if tl.EndIndex >= wp {
tl.EndIndex -= wp
tl.EndSpan++
}
} else if tl.EndSpan > si-1 {
tl.EndSpan++
}
}
if ssz.X <= size.X {
if ssz.X > maxw {
maxw = ssz.X
}
break
}
} else {
if ssz.X > maxw {
maxw = ssz.X
}
break
}
}
} else {
if ssz.X > maxw {
maxw = ssz.X
}
}
si++
}
// have maxw, can do alignment cases..
// make sure links are still in range
for li := range tr.Links {
tl := &tr.Links[li]
stsp := tr.Spans[tl.StartSpan]
if tl.StartIndex >= len(stsp.Text) {
tl.StartIndex = len(stsp.Text) - 1
}
edsp := tr.Spans[tl.EndSpan]
if tl.EndIndex >= len(edsp.Text) {
tl.EndIndex = len(edsp.Text) - 1
}
}
if maxw > size.X {
size.X = maxw
}
// vertical alignment
nsp := len(tr.Spans)
npara := 0
for si := 1; si < nsp; si++ {
sr := &(tr.Spans[si])
if sr.IsNewPara() {
npara++
}
}
vht := lspc*float32(nsp) + float32(npara)*txtSty.ParaSpacing.Dots
if vht > size.Y {
size.Y = vht
}
tr.BBox.Min.SetZero()
tr.BBox.Max = math32.Vec2(maxw, vht)
vpad := float32(0) // padding at top to achieve vertical alignment
vextra := size.Y - vht
if vextra > 0 {
switch txtSty.AlignV {
case styles.Center:
vpad = vextra / 2
case styles.End:
vpad = vextra
}
}
vbaseoff := lspc - lpad - dsc // offset of baseline within overall line
vpos := vpad + vbaseoff
for si := range tr.Spans {
sr := &(tr.Spans[si])
if si > 0 && sr.IsNewPara() {
vpos += txtSty.ParaSpacing.Dots
}
sr.RelPos.Y = vpos
sr.LastPos.Y = vpos
ssz := sr.SizeHV()
ssz.X += sr.RelPos.X
hextra := size.X - ssz.X
if hextra > 0 {
switch txtSty.Align {
case styles.Center:
sr.RelPos.X += hextra / 2
case styles.End:
sr.RelPos.X += hextra
}
}
vpos += lspc
}
return size
}
// Transform applies given 2D transform matrix to the text character rotations,
// scaling, and positions, so that the text is rendered according to that transform.
// The fontSty is the font style used for specifying the font originally.
func (tr *Text) Transform(mat math32.Matrix2, fontSty *styles.FontRender, ctxt *units.Context) {
orgsz := fontSty.Size
tmpsty := styles.FontRender{}
tmpsty = *fontSty
rot := mat.ExtractRot()
scx, scy := mat.ExtractScale()
scalex := scx / scy
if scalex == 1 {
scalex = 0
}
for si := range tr.Spans {
sr := &(tr.Spans[si])
sr.RelPos = mat.MulVector2AsVector(sr.RelPos)
sr.LastPos = mat.MulVector2AsVector(sr.LastPos)
for i := range sr.Render {
rn := &sr.Render[i]
if rn.Face != nil {
tmpsty.Size = units.Value{Value: orgsz.Value * scy, Unit: orgsz.Unit, Dots: orgsz.Dots * scy} // rescale by y
tmpsty.Font = OpenFont(&tmpsty, ctxt)
rn.Face = tmpsty.Face.Face
}
rn.RelPos = mat.MulVector2AsVector(rn.RelPos)
rn.Size.Y *= scy
rn.Size.X *= scx
rn.RotRad = rot
rn.ScaleX = scalex
}
}
tr.BBox = tr.BBox.MulMatrix2(mat)
}
// UpdateBBox updates the overall text bounding box
// based on actual glyph bounding boxes.
func (tr *Text) UpdateBBox() {
tr.BBox.SetEmpty()
for si := range tr.Spans {
sr := &(tr.Spans[si])
var curfc font.Face
for i := range sr.Render {
r := sr.Text[i]
rn := &sr.Render[i]
if rn.Face != nil {
curfc = rn.Face
}
gbf, _, ok := curfc.GlyphBounds(r)
if ok {
gb := math32.B2FromFixed(gbf)
gb.Translate(rn.RelPos)
tr.BBox.ExpandByBox(gb)
}
}
}
}
// TextWrapSizeEstimate is the size to use for layout during the SizeUp pass,
// for word wrap case, where the sizing actually matters,
// based on trying to fit the given number of characters into the given content size
// with given font height, and ratio of width to height.
// Ratio is used when csz is 0: 1.618 is golden, and smaller numbers to allow
// for narrower, taller text columns.
func TextWrapSizeEstimate(csz math32.Vector2, nChars int, ratio float32, fs *styles.Font) math32.Vector2 {
chars := float32(nChars)
fht := float32(16)
if fs.Face != nil {
fht = fs.Face.Metrics.Height
}
area := chars * fht * fht
if csz.X > 0 && csz.Y > 0 {
ratio = csz.X / csz.Y
// fmt.Println(lb, "content size ratio:", ratio)
}
// w = ratio * h
// w^2 + h^2 = a
// (ratio*h)^2 + h^2 = a
h := math32.Sqrt(area) / math32.Sqrt(ratio+1)
w := ratio * h
if w < csz.X { // must be at least this
w = csz.X
h = area / w
h = max(h, csz.Y)
}
sz := math32.Vec2(w, h)
return sz
}
// 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 main
import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/parse"
_ "cogentcore.org/core/parse/languages"
"cogentcore.org/core/parse/syms"
)
var Excludes []string
func main() {
var path string
var recurse bool
var excl string
parse.LanguageSupport.OpenStandard()
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
fmt.Fprintf(flag.CommandLine.Output(), "\ne.g., on mac, to run all of the std go files except cmd which is large and slow:\npi -r -ex cmd /usr/local/Cellar/go/1.11.3/libexec/src\n\n")
}
// process command args
flag.StringVar(&path, "path", "", "path to open; can be to a directory or a filename within the directory; or just last arg without a flag")
flag.BoolVar(&recurse, "r", false, "recursive; apply to subdirectories")
flag.StringVar(&excl, "ex", "", "comma-separated list of directory names to exclude, for recursive case")
flag.Parse()
if path == "" {
if flag.NArg() > 0 {
path = flag.Arg(0)
} else {
path = "."
}
}
Excludes = strings.Split(excl, ",")
// todo: assuming go for now
if recurse {
DoGoRecursive(path)
} else {
DoGoPath(path)
}
}
func DoGoPath(path string) {
fmt.Printf("Processing path: %v\n", path)
lp, _ := parse.LanguageSupport.Properties(fileinfo.Go)
pr := lp.Lang.Parser()
pr.ReportErrs = true
fs := parse.NewFileState()
pkgsym := lp.Lang.ParseDir(fs, path, parse.LanguageDirOptions{Rebuild: true})
if pkgsym != nil {
syms.SaveSymDoc(pkgsym, fileinfo.Go, path)
}
}
func DoGoRecursive(path string) {
DoGoPath(path)
drs := fsx.Dirs(path)
outer:
for _, dr := range drs {
if dr == "testdata" {
continue
}
for _, ex := range Excludes {
if dr == ex {
continue outer
}
}
sp := filepath.Join(path, dr)
DoGoRecursive(sp)
}
}
// Copyright (c) 2024, 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.
// Command update updates all of the .parse files within
// or beneath the current directory by opening and saving them.
package main
import (
"io/fs"
"path/filepath"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/parse"
)
func main() {
errors.Log(filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if filepath.Ext(path) != ".parse" {
return nil
}
p := parse.NewParser()
err = p.OpenJSON(path)
if err != nil {
return err
}
return p.SaveJSON(path)
}))
}
// 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 complete
import (
"cmp"
"path/filepath"
"slices"
"strings"
"unicode"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/icons"
)
// Completion holds one potential completion
type Completion struct {
// Text is the completion text: what will actually be inserted if selected.
Text string
// Label is the label text to show the user. It is only used if
// non-empty; otherwise Text is used.
Label string
// Icon is the icon to render in core for the completion item.
Icon icons.Icon
// Desc is extra description information used in tooltips in core.
Desc string
}
// Completions is a full list (slice) of completion options
type Completions []Completion
// Matches is used for passing completions around.
// contains seed in addition to completions
type Matches struct {
// the matches based on seed
Matches Completions
// seed is the prefix we use to find possible completions
Seed string
}
// Lookup is used for returning lookup results
type Lookup struct {
// if non-empty, the result is to view this file (full path)
Filename string
// starting line number within file to display
StLine int
// ending line number within file
EdLine int
// if filename is empty, this is raw text to display for lookup result
Text []byte
}
// SetFile sets file info
func (lk *Lookup) SetFile(fname string, st, ed int) {
lk.Filename = fname
lk.StLine = st
lk.EdLine = ed
}
// Edit is returned from completion edit function
// to incorporate the selected completion
type Edit struct {
// completion text after special edits
NewText string
// number of runes, past the cursor, to delete, if any
ForwardDelete int
// cursor adjustment if cursor should be placed in a location other than at end of newText
CursorAdjust int
}
// MatchFunc is the function called to get the list of possible completions
// and also determines the correct seed based on the text
// passed as a parameter of CompletionFunc
type MatchFunc func(data any, text string, posLine, posChar int) Matches
// LookupFunc is the function called to get the lookup results for given
// input test and position.
type LookupFunc func(data any, text string, posLine, posChar int) Lookup
// EditFunc is passed the current text and the selected completion for text editing.
// Allows for other editing, e.g. adding "()" or adding "/", etc.
type EditFunc func(data any, text string, cursorPos int, comp Completion, seed string) Edit
// MatchSeedString returns a list of matches given a list of string
// possibilities and a seed. It checks whether different
// transformations of each possible completion contain a lowercase
// version of the seed. It returns nil if there are no matches.
func MatchSeedString(completions []string, seed string) []string {
if len(seed) == 0 {
// everything matches
return completions
}
var matches []string
lseed := strings.ToLower(seed)
for _, c := range completions {
if IsSeedMatching(lseed, c) {
matches = append(matches, c)
}
}
slices.SortStableFunc(matches, func(a, b string) int {
return cmp.Compare(MatchPrecedence(lseed, a), MatchPrecedence(lseed, b))
})
return matches
}
// MatchSeedCompletion returns a list of matches given a list of
// [Completion] possibilities and a seed. It checks whether different
// transformations of each possible completion contain a lowercase
// version of the seed. It returns nil if there are no matches.
func MatchSeedCompletion(completions []Completion, seed string) []Completion {
if len(seed) == 0 {
// everything matches
return completions
}
var matches []Completion
lseed := strings.ToLower(seed)
for _, c := range completions {
if IsSeedMatching(lseed, c.Text) {
matches = append(matches, c)
}
}
slices.SortStableFunc(matches, func(a, b Completion) int {
return cmp.Compare(MatchPrecedence(lseed, a.Text), MatchPrecedence(lseed, b.Text))
})
return matches
}
// IsSeedMatching returns whether the given lowercase seed matches
// the given completion string. It checks whether different
// transformations of the completion contain the lowercase
// version of the seed.
func IsSeedMatching(lseed string, completion string) bool {
lc := strings.ToLower(completion)
if strings.Contains(lc, lseed) {
return true
}
// stripped version of completion
// (space delimeted with no punctuation and symbols)
cs := strings.Map(func(r rune) rune {
if unicode.IsPunct(r) || unicode.IsSymbol(r) {
return -1
}
return r
}, completion)
cs = strcase.ToWordCase(cs, strcase.WordLowerCase, ' ')
if strings.Contains(cs, lseed) {
return true
}
// the initials (first letters) of every field
ci := ""
csdf := strings.Fields(cs)
for _, f := range csdf {
ci += string(f[0])
}
return strings.Contains(ci, lseed)
}
// MatchPrecedence returns the sorting precedence of the given
// completion relative to the given lowercase seed. The completion
// is assumed to already match the seed by [IsSeedMatching]. A
// lower return value indicates a higher precedence.
func MatchPrecedence(lseed string, completion string) int {
lc := strings.ToLower(completion)
if strings.HasPrefix(lc, lseed) {
return 0
}
if len(lseed) > 0 && strings.HasPrefix(lc, lseed[:1]) {
return 1
}
return 2
}
// SeedSpace returns the text after the last whitespace,
// which is typically used for creating a completion seed string.
func SeedSpace(text string) string {
return SeedAfter(text, func(r rune) bool {
return unicode.IsSpace(r)
})
}
// SeedPath returns the text after the last whitespace and path/filepath
// separator, which is typically used for creating a completion seed string.
func SeedPath(text string) string {
return SeedAfter(text, func(r rune) bool {
return unicode.IsSpace(r) || r == '/' || r == filepath.Separator
})
}
// SeedPath returns the text after the last rune for which the given
// function returns true, which is typically used for creating a completion
// seed string.
func SeedAfter(text string, f func(r rune) bool) string {
seedStart := 0
runes := []rune(text)
for i := len(runes) - 1; i >= 0; i-- {
r := runes[i]
if f(r) {
seedStart = i + 1
break
}
}
return string(runes[seedStart:])
}
// EditWord replaces the completion seed and any text up to the next whitespace with completion
func EditWord(text string, cursorPos int, completion string, seed string) (ed Edit) {
s2 := string(text[cursorPos:])
fd := 0 // number of characters past seed in word to be deleted (forward delete)
r := rune(0)
if len(s2) > 0 {
for fd, r = range s2 {
if unicode.IsSpace(r) {
break
}
}
}
if fd == len(s2)-1 { // last word case
fd += 1
}
ed.NewText = completion
ed.ForwardDelete = fd + len(seed)
ed.CursorAdjust = 0
return ed
}
// Code generated by "core generate -add-types"; DO NOT EDIT.
package parse
import (
"cogentcore.org/core/enums"
)
var _LanguageFlagsValues = []LanguageFlags{0, 1, 2, 3}
// LanguageFlagsN is the highest valid value for type LanguageFlags, plus one.
const LanguageFlagsN LanguageFlags = 4
var _LanguageFlagsValueMap = map[string]LanguageFlags{`NoFlags`: 0, `IndentSpace`: 1, `IndentTab`: 2, `ReAutoIndent`: 3}
var _LanguageFlagsDescMap = map[LanguageFlags]string{0: `NoFlags = nothing special`, 1: `IndentSpace means that spaces must be used for this language`, 2: `IndentTab means that tabs must be used for this language`, 3: `ReAutoIndent causes current line to be re-indented during AutoIndent for Enter (newline) -- this should only be set for strongly indented languages where the previous + current line can tell you exactly what indent the current line should be at.`}
var _LanguageFlagsMap = map[LanguageFlags]string{0: `NoFlags`, 1: `IndentSpace`, 2: `IndentTab`, 3: `ReAutoIndent`}
// String returns the string representation of this LanguageFlags value.
func (i LanguageFlags) String() string { return enums.String(i, _LanguageFlagsMap) }
// SetString sets the LanguageFlags value from its string representation,
// and returns an error if the string is invalid.
func (i *LanguageFlags) SetString(s string) error {
return enums.SetString(i, s, _LanguageFlagsValueMap, "LanguageFlags")
}
// Int64 returns the LanguageFlags value as an int64.
func (i LanguageFlags) Int64() int64 { return int64(i) }
// SetInt64 sets the LanguageFlags value from an int64.
func (i *LanguageFlags) SetInt64(in int64) { *i = LanguageFlags(in) }
// Desc returns the description of the LanguageFlags value.
func (i LanguageFlags) Desc() string { return enums.Desc(i, _LanguageFlagsDescMap) }
// LanguageFlagsValues returns all possible values for the type LanguageFlags.
func LanguageFlagsValues() []LanguageFlags { return _LanguageFlagsValues }
// Values returns all possible values for the type LanguageFlags.
func (i LanguageFlags) Values() []enums.Enum { return enums.Values(_LanguageFlagsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i LanguageFlags) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *LanguageFlags) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "LanguageFlags")
}
// 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 parse
import (
"fmt"
"path/filepath"
"strings"
"sync"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/parser"
"cogentcore.org/core/parse/syms"
)
// FileState contains the full lexing and parsing state information for a given file.
// It is the master state record for everything that happens in parse. One of these
// should be maintained for each file; texteditor.Buf has one as ParseState field.
//
// Separate State structs are maintained for each stage (Lexing, PassTwo, Parsing) and
// the final output of Parsing goes into the AST and Syms fields.
//
// The Src lexer.File field maintains all the info about the source file, and the basic
// tokenized version of the source produced initially by lexing and updated by the
// remaining passes. It has everything that is maintained at a line-by-line level.
type FileState struct {
// the source to be parsed -- also holds the full lexed tokens
Src lexer.File `json:"-" xml:"-"`
// state for lexing
LexState lexer.State `json:"_" xml:"-"`
// state for second pass nesting depth and EOS matching
TwoState lexer.TwoState `json:"-" xml:"-"`
// state for parsing
ParseState parser.State `json:"-" xml:"-"`
// ast output tree from parsing
AST *parser.AST `json:"-" xml:"-"`
// symbols contained within this file -- initialized at start of parsing and created by AddSymbol or PushNewScope actions. These are then processed after parsing by the language-specific code, via Lang interface.
Syms syms.SymMap `json:"-" xml:"-"`
// External symbols that are entirely maintained in a language-specific way by the Lang interface code. These are only here as a convenience and are not accessed in any way by the language-general parse code.
ExtSyms syms.SymMap `json:"-" xml:"-"`
// mutex protecting updates / reading of Syms symbols
SymsMu sync.RWMutex `display:"-" json:"-" xml:"-"`
// waitgroup for coordinating processing of other items
WaitGp sync.WaitGroup `display:"-" json:"-" xml:"-"`
// anonymous counter -- counts up
AnonCtr int `display:"-" json:"-" xml:"-"`
// path mapping cache -- for other files referred to by this file, this stores the full path associated with a logical path (e.g., in go, the logical import path -> local path with actual files) -- protected for access from any thread
PathMap sync.Map `display:"-" json:"-" xml:"-"`
}
// Init initializes the file state
func (fs *FileState) Init() {
// fmt.Println("fs init:", fs.Src.Filename)
fs.AST = parser.NewAST()
fs.LexState.Init()
fs.TwoState.Init()
fs.ParseState.Init(&fs.Src, fs.AST)
fs.SymsMu.Lock()
fs.Syms = make(syms.SymMap)
fs.SymsMu.Unlock()
fs.AnonCtr = 0
}
// NewFileState returns a new initialized file state
func NewFileState() *FileState {
fs := &FileState{}
fs.Init()
return fs
}
func (fs *FileState) ClearAST() {
fs.Syms.ClearAST()
fs.ExtSyms.ClearAST()
}
func (fs *FileState) Destroy() {
fs.ClearAST()
fs.Syms = nil
fs.ExtSyms = nil
fs.AST.Destroy()
fs.LexState.Init()
fs.TwoState.Init()
fs.ParseState.Destroy()
}
// SetSrc sets source to be parsed, and filename it came from, and also the
// base path for project for reporting filenames relative to
// (if empty, path to filename is used)
func (fs *FileState) SetSrc(src [][]rune, fname, basepath string, sup fileinfo.Known) {
fs.Init()
fs.Src.SetSrc(src, fname, basepath, sup)
fs.LexState.Filename = fname
}
// LexAtEnd returns true if lexing state is now at end of source
func (fs *FileState) LexAtEnd() bool {
return fs.LexState.Ln >= fs.Src.NLines()
}
// LexLine returns the lexing output for given line, combining comments and all other tokens
// and allocating new memory using clone
func (fs *FileState) LexLine(ln int) lexer.Line {
return fs.Src.LexLine(ln)
}
// LexLineString returns a string rep of the current lexing output for the current line
func (fs *FileState) LexLineString() string {
return fs.LexState.LineString()
}
// LexNextSrcLine returns the next line of source that the lexer is currently at
func (fs *FileState) LexNextSrcLine() string {
return fs.LexState.NextSrcLine()
}
// LexHasErrs returns true if there were errors from lexing
func (fs *FileState) LexHasErrs() bool {
return len(fs.LexState.Errs) > 0
}
// LexErrReport returns a report of all the lexing errors -- these should only
// occur during development of lexer so we use a detailed report format
func (fs *FileState) LexErrReport() string {
return fs.LexState.Errs.Report(0, fs.Src.BasePath, true, true)
}
// PassTwoHasErrs returns true if there were errors from pass two processing
func (fs *FileState) PassTwoHasErrs() bool {
return len(fs.TwoState.Errs) > 0
}
// PassTwoErrString returns all the pass two errors as a string -- these should
// only occur during development so we use a detailed report format
func (fs *FileState) PassTwoErrReport() string {
return fs.TwoState.Errs.Report(0, fs.Src.BasePath, true, true)
}
// ParseAtEnd returns true if parsing state is now at end of source
func (fs *FileState) ParseAtEnd() bool {
return fs.ParseState.AtEofNext()
}
// ParseNextSrcLine returns the next line of source that the parser is currently at
func (fs *FileState) ParseNextSrcLine() string {
return fs.ParseState.NextSrcLine()
}
// ParseHasErrs returns true if there were errors from parsing
func (fs *FileState) ParseHasErrs() bool {
return len(fs.ParseState.Errs) > 0
}
// ParseErrReport returns at most 10 parsing errors in end-user format, sorted
func (fs *FileState) ParseErrReport() string {
fs.ParseState.Errs.Sort()
return fs.ParseState.Errs.Report(10, fs.Src.BasePath, true, false)
}
// ParseErrReportAll returns all parsing errors in end-user format, sorted
func (fs *FileState) ParseErrReportAll() string {
fs.ParseState.Errs.Sort()
return fs.ParseState.Errs.Report(0, fs.Src.BasePath, true, false)
}
// ParseErrReportDetailed returns at most 10 parsing errors in detailed format, sorted
func (fs *FileState) ParseErrReportDetailed() string {
fs.ParseState.Errs.Sort()
return fs.ParseState.Errs.Report(10, fs.Src.BasePath, true, true)
}
// RuleString returns the rule info for entire source -- if full
// then it includes the full stack at each point -- otherwise just the top
// of stack
func (fs *FileState) ParseRuleString(full bool) string {
return fs.ParseState.RuleString(full)
}
////////////////////////////////////////////////////////////////////////
// Syms symbol processing support
// FindNameScoped looks for given symbol name within given map first
// (if non nil) and then in fs.Syms and ExtSyms maps,
// and any children on those global maps that are of subcategory
// token.NameScope (i.e., namespace, module, package, library)
func (fs *FileState) FindNameScoped(nm string, scope syms.SymMap) (*syms.Symbol, bool) {
var sy *syms.Symbol
has := false
if scope != nil {
sy, has = scope.FindName(nm)
if has {
return sy, true
}
}
sy, has = fs.Syms.FindNameScoped(nm)
if has {
return sy, true
}
sy, has = fs.ExtSyms.FindNameScoped(nm)
if has {
return sy, true
}
return nil, false
}
// FindChildren fills out map with direct children of given symbol
// If seed is non-empty it is used as a prefix for filtering children names.
// Returns false if no children were found.
func (fs *FileState) FindChildren(sym *syms.Symbol, seed string, scope syms.SymMap, kids *syms.SymMap) bool {
if len(sym.Children) == 0 {
if sym.Type != "" {
typ, got := fs.FindNameScoped(sym.NonPtrTypeName(), scope)
if got {
sym = typ
} else {
return false
}
}
}
if seed != "" {
sym.Children.FindNamePrefix(seed, kids)
} else {
kids.CopyFrom(sym.Children, true) // src is newer
}
return len(*kids) > 0
}
// FindAnyChildren fills out map with either direct children of given symbol
// or those of the type of this symbol -- useful for completion.
// If seed is non-empty it is used as a prefix for filtering children names.
// Returns false if no children were found.
func (fs *FileState) FindAnyChildren(sym *syms.Symbol, seed string, scope syms.SymMap, kids *syms.SymMap) bool {
if len(sym.Children) == 0 {
if sym.Type != "" {
typ, got := fs.FindNameScoped(sym.NonPtrTypeName(), scope)
if got {
sym = typ
} else {
return false
}
}
}
if seed != "" {
sym.Children.FindNamePrefixRecursive(seed, kids)
} else {
kids.CopyFrom(sym.Children, true) // src is newer
}
return len(*kids) > 0
}
// FindNamePrefixScoped looks for given symbol name prefix within given map first
// (if non nil) and then in fs.Syms and ExtSyms maps,
// and any children on those global maps that are of subcategory
// token.NameScope (i.e., namespace, module, package, library)
// adds to given matches map (which can be nil), for more efficient recursive use
func (fs *FileState) FindNamePrefixScoped(seed string, scope syms.SymMap, matches *syms.SymMap) {
lm := len(*matches)
if scope != nil {
scope.FindNamePrefixRecursive(seed, matches)
}
if len(*matches) != lm {
return
}
fs.Syms.FindNamePrefixScoped(seed, matches)
if len(*matches) != lm {
return
}
fs.ExtSyms.FindNamePrefixScoped(seed, matches)
}
// NextAnonName returns the next anonymous name for this file, using counter here
// and given context name (e.g., package name)
func (fs *FileState) NextAnonName(ctxt string) string {
fs.AnonCtr++
fn := filepath.Base(fs.Src.Filename)
ext := filepath.Ext(fn)
if ext != "" {
fn = strings.TrimSuffix(fn, ext)
}
return fmt.Sprintf("anon_%s_%d", ctxt, fs.AnonCtr)
}
// PathMapLoad does a mutex-protected load of PathMap for given string,
// returning value and true if found
func (fs *FileState) PathMapLoad(path string) (string, bool) {
fabs, ok := fs.PathMap.Load(path)
fs.PathMap.Load(path)
if ok {
return fabs.(string), ok
}
return "", ok
}
// PathMapStore does a mutex-protected store of abs path for given path key
func (fs *FileState) PathMapStore(path, abs string) {
fs.PathMap.Store(path, abs)
}
// 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 parse
import (
"sync"
"cogentcore.org/core/base/fileinfo"
)
// FileStates contains two FileState's: one is being processed while the
// other is being used externally. The FileStates maintains
// a common set of file information set in each of the FileState items when
// they are used.
type FileStates struct {
// the filename
Filename string
// the known file type, if known (typically only known files are processed)
Known fileinfo.Known
// base path for reporting file names -- this must be set externally e.g., by gide for the project root path
BasePath string
// index of the state that is done
DoneIndex int
// one filestate
FsA FileState
// one filestate
FsB FileState
// mutex locking the switching of Done vs. Proc states
SwitchMu sync.Mutex
// mutex locking the parsing of Proc state -- reading states can happen fine with this locked, but no switching
ProcMu sync.Mutex
// extra meta data associated with this FileStates
Meta map[string]string
}
// NewFileStates returns a new FileStates for given filename, basepath,
// and known file type.
func NewFileStates(fname, basepath string, sup fileinfo.Known) *FileStates {
fs := &FileStates{}
fs.SetSrc(fname, basepath, sup)
return fs
}
// SetSrc sets the source that is processed by this FileStates
// if basepath is empty then it is set to the path for the filename.
func (fs *FileStates) SetSrc(fname, basepath string, sup fileinfo.Known) {
fs.ProcMu.Lock() // make sure processing is done
defer fs.ProcMu.Unlock()
fs.SwitchMu.Lock()
defer fs.SwitchMu.Unlock()
fs.Filename = fname
fs.BasePath = basepath
fs.Known = sup
fs.FsA.SetSrc(nil, fname, basepath, sup)
fs.FsB.SetSrc(nil, fname, basepath, sup)
}
// Done returns the filestate that is done being updated, and is ready for
// use by external clients etc. Proc is the other one which is currently
// being processed by the parser and is not ready to be used externally.
// The state is accessed under a lock, and as long as any use of state is
// fast enough, it should be usable over next two switches (typically true).
func (fs *FileStates) Done() *FileState {
fs.SwitchMu.Lock()
defer fs.SwitchMu.Unlock()
return fs.DoneNoLock()
}
// DoneNoLock returns the filestate that is done being updated, and is ready for
// use by external clients etc. Proc is the other one which is currently
// being processed by the parser and is not ready to be used externally.
// The state is accessed under a lock, and as long as any use of state is
// fast enough, it should be usable over next two switches (typically true).
func (fs *FileStates) DoneNoLock() *FileState {
switch fs.DoneIndex {
case 0:
return &fs.FsA
case 1:
return &fs.FsB
}
return &fs.FsA
}
// Proc returns the filestate that is currently being processed by
// the parser etc and is not ready for external use.
// Access is protected by a lock so it will wait if currently switching.
// The state is accessed under a lock, and as long as any use of state is
// fast enough, it should be usable over next two switches (typically true).
func (fs *FileStates) Proc() *FileState {
fs.SwitchMu.Lock()
defer fs.SwitchMu.Unlock()
return fs.ProcNoLock()
}
// ProcNoLock returns the filestate that is currently being processed by
// the parser etc and is not ready for external use.
// Access is protected by a lock so it will wait if currently switching.
// The state is accessed under a lock, and as long as any use of state is
// fast enough, it should be usable over next two switches (typically true).
func (fs *FileStates) ProcNoLock() *FileState {
switch fs.DoneIndex {
case 0:
return &fs.FsB
case 1:
return &fs.FsA
}
return &fs.FsB
}
// StartProc should be called when starting to process the file, and returns the
// FileState to use for processing. It locks the Proc state, sets the current
// source code, and returns the filestate for subsequent processing.
func (fs *FileStates) StartProc(txt []byte) *FileState {
fs.ProcMu.Lock()
pfs := fs.ProcNoLock()
pfs.Src.BasePath = fs.BasePath
pfs.Src.SetBytes(txt)
return pfs
}
// EndProc is called when primary processing (parsing) has been completed --
// there still may be ongoing updating of symbols after this point but parse
// is done. This calls Switch to move Proc over to done, under cover of ProcMu Lock
func (fs *FileStates) EndProc() {
fs.Switch()
fs.ProcMu.Unlock()
}
// Switch switches so that the current Proc() filestate is now the Done()
// it is assumed to be called under ProcMu.Locking cover, and also
// does the Swtich locking.
func (fs *FileStates) Switch() {
fs.SwitchMu.Lock()
defer fs.SwitchMu.Unlock()
fs.DoneIndex++
fs.DoneIndex = fs.DoneIndex % 2
// fmt.Printf("switched: %v %v\n", fs.DoneIndex, fs.Filename)
}
// MetaData returns given meta data string for given key,
// returns true if present, false if not
func (fs *FileStates) MetaData(key string) (string, bool) {
if fs.Meta == nil {
return "", false
}
md, ok := fs.Meta[key]
return md, ok
}
// SetMetaData sets given meta data record
func (fs *FileStates) SetMetaData(key, value string) {
if fs.Meta == nil {
fs.Meta = make(map[string]string)
}
fs.Meta[key] = value
}
// DeleteMetaData deletes given meta data record
func (fs *FileStates) DeleteMetaData(key string) {
if fs.Meta == nil {
return
}
delete(fs.Meta, key)
}
// Copyright (c) 2020, 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.
// Copied and only lightly modified from:
// https://github.com/nickng/bibtex
// Licensed under an Apache-2.0 license
// and presumably Copyright (c) 2017 by Nick Ng
package bibtex
import (
"bytes"
"fmt"
"slices"
"sort"
"strconv"
"strings"
"text/tabwriter"
)
// BibString is a segment of a bib string.
type BibString interface {
RawString() string // Internal representation.
String() string // Displayed string.
}
// BibVar is a string variable.
type BibVar struct {
Key string // Variable key.
Value BibString // Variable actual value.
}
// RawString is the internal representation of the variable.
func (v *BibVar) RawString() string {
return v.Key
}
func (v *BibVar) String() string {
if v == nil {
return ""
}
return v.Value.String()
}
// BibConst is a string constant.
type BibConst string
// NewBibConst converts a constant string to BibConst.
func NewBibConst(c string) BibConst {
return BibConst(c)
}
// RawString is the internal representation of the constant (i.e. the string).
func (c BibConst) RawString() string {
return fmt.Sprintf("{%s}", string(c))
}
func (c BibConst) String() string {
return string(c)
}
// BibComposite is a composite string, may contain both variable and string.
type BibComposite []BibString
// NewBibComposite creates a new composite with one element.
func NewBibComposite(s BibString) *BibComposite {
comp := &BibComposite{}
return comp.Append(s)
}
// Append adds a BibString to the composite
func (c *BibComposite) Append(s BibString) *BibComposite {
comp := append(*c, s)
return &comp
}
func (c *BibComposite) String() string {
var buf bytes.Buffer
for _, s := range *c {
buf.WriteString(s.String())
}
return buf.String()
}
// RawString returns a raw (bibtex) representation of the composite string.
func (c *BibComposite) RawString() string {
var buf bytes.Buffer
for i, comp := range *c {
if i > 0 {
buf.WriteString(" # ")
}
switch comp := comp.(type) {
case *BibConst:
buf.WriteString(comp.RawString())
case *BibVar:
buf.WriteString(comp.RawString())
case *BibComposite:
buf.WriteString(comp.RawString())
}
}
return buf.String()
}
// BibEntry is a record of BibTeX record.
type BibEntry struct {
Type string
CiteName string
Fields map[string]BibString
}
// NewBibEntry creates a new BibTeX entry.
func NewBibEntry(entryType string, citeName string) *BibEntry {
spaceStripper := strings.NewReplacer(" ", "")
cleanedType := strings.ToLower(spaceStripper.Replace(entryType))
cleanedName := spaceStripper.Replace(citeName)
return &BibEntry{
Type: cleanedType,
CiteName: cleanedName,
Fields: map[string]BibString{},
}
}
// AddField adds a field (key-value) to a BibTeX entry.
func (entry *BibEntry) AddField(name string, value BibString) {
entry.Fields[strings.TrimSpace(name)] = value
}
// BibTex is a list of BibTeX entries.
type BibTex struct {
Preambles []BibString // List of Preambles
Entries []*BibEntry // Items in a bibliography.
KeyMap map[string]*BibEntry // fast key lookup map -- made on demand in Lookup
StringVar map[string]*BibVar // Map from string variable to string.
}
// NewBibTex creates a new BibTex data structure.
func NewBibTex() *BibTex {
return &BibTex{
Preambles: []BibString{},
Entries: []*BibEntry{},
StringVar: make(map[string]*BibVar),
}
}
// AddPreamble adds a preamble to a bibtex.
func (bib *BibTex) AddPreamble(p BibString) {
bib.Preambles = append(bib.Preambles, p)
}
// AddEntry adds an entry to the BibTeX data structure.
func (bib *BibTex) AddEntry(entry *BibEntry) {
bib.Entries = append(bib.Entries, entry)
}
// AddStringVar adds a new string var (if does not exist).
func (bib *BibTex) AddStringVar(key string, val BibString) {
bib.StringVar[key] = &BibVar{Key: key, Value: val}
}
// SortEntries sorts entries by CiteName.
func (bib *BibTex) SortEntries() {
slices.SortFunc(bib.Entries, func(a, b *BibEntry) int {
return strings.Compare(a.CiteName, b.CiteName)
})
}
// GetStringVar looks up a string by its key.
func (bib *BibTex) GetStringVar(key string) *BibVar {
if bv, ok := bib.StringVar[key]; ok {
return bv
}
// at this point, key is usually a month -- just pass through
bib.AddStringVar(key, NewBibConst(key))
return bib.StringVar[key]
}
// String returns a BibTex data structure as a simplified BibTex string.
func (bib *BibTex) String() string {
var bibtex bytes.Buffer
for _, entry := range bib.Entries {
bibtex.WriteString(fmt.Sprintf("@%s{%s,\n", entry.Type, entry.CiteName))
for key, val := range entry.Fields {
if i, err := strconv.Atoi(strings.TrimSpace(val.String())); err == nil {
bibtex.WriteString(fmt.Sprintf(" %s = %d,\n", key, i))
} else {
bibtex.WriteString(fmt.Sprintf(" %s = {%s},\n", key, strings.TrimSpace(val.String())))
}
}
bibtex.Truncate(bibtex.Len() - 2)
bibtex.WriteString(fmt.Sprintf("\n}\n"))
}
return bibtex.String()
}
// RawString returns a BibTex data structure in its internal representation.
func (bib *BibTex) RawString() string {
var bibtex bytes.Buffer
for k, strvar := range bib.StringVar {
bibtex.WriteString(fmt.Sprintf("@string{%s = {%s}}\n", k, strvar.String()))
}
for _, preamble := range bib.Preambles {
bibtex.WriteString(fmt.Sprintf("@preamble{%s}\n", preamble.RawString()))
}
for _, entry := range bib.Entries {
bibtex.WriteString(fmt.Sprintf("@%s{%s,\n", entry.Type, entry.CiteName))
for key, val := range entry.Fields {
if i, err := strconv.Atoi(strings.TrimSpace(val.String())); err == nil {
bibtex.WriteString(fmt.Sprintf(" %s = %d,\n", key, i))
} else {
bibtex.WriteString(fmt.Sprintf(" %s = %s,\n", key, val.RawString()))
}
}
bibtex.Truncate(bibtex.Len() - 2)
bibtex.WriteString(fmt.Sprintf("\n}\n"))
}
return bibtex.String()
}
// PrettyString pretty prints a BibTex.
func (bib *BibTex) PrettyString() string {
var buf bytes.Buffer
for i, entry := range bib.Entries {
if i != 0 {
fmt.Fprint(&buf, "\n")
}
fmt.Fprintf(&buf, "@%s{%s,\n", entry.Type, entry.CiteName)
// Determine key order.
keys := []string{}
for key := range entry.Fields {
keys = append(keys, key)
}
priority := map[string]int{"title": -3, "author": -2, "url": -1}
sort.Slice(keys, func(i, j int) bool {
pi, pj := priority[keys[i]], priority[keys[j]]
return pi < pj || (pi == pj && keys[i] < keys[j])
})
// Write fields.
tw := tabwriter.NewWriter(&buf, 1, 4, 1, ' ', 0)
for _, key := range keys {
value := entry.Fields[key].String()
format := stringformat(value)
fmt.Fprintf(tw, " %s\t=\t"+format+",\n", key, value)
}
tw.Flush()
// Close.
buf.WriteString("}\n")
}
return buf.String()
}
// stringformat determines the correct formatting verb for the given BibTeX field value.
func stringformat(v string) string {
// Numbers may be represented unquoted.
if _, err := strconv.Atoi(v); err == nil {
return "%s"
}
// Strings with certain characters must be brace quoted.
if strings.ContainsAny(v, "\"{}") {
return "{%s}"
}
// Default to quoted string.
return "%q"
}
// MakeKeyMap creates the KeyMap from CiteName to entry
func (bib *BibTex) MakeKeyMap() {
bib.KeyMap = make(map[string]*BibEntry, len(bib.Entries))
for _, be := range bib.Entries {
bib.KeyMap[be.CiteName] = be
}
}
// Lookup finds CiteName in entries, using fast KeyMap (made on demand)
// returns nil, false if not found
func (bib *BibTex) Lookup(cite string) (*BibEntry, bool) {
if bib.KeyMap == nil {
bib.MakeKeyMap()
}
be, has := bib.KeyMap[cite]
return be, has
}
// Copyright (c) 2020, 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.
// Copied and only lightly modified from:
// https://github.com/nickng/bibtex
// Licensed under an Apache-2.0 license
// and presumably Copyright (c) 2017 by Nick Ng
package bibtex
import (
__yyfmt__ "fmt"
"io"
)
type bibTag struct {
key string
val BibString
}
var bib *BibTex // Only for holding current bib
type bibtexSymType struct {
yys int
bibtex *BibTex
strval string
bibentry *BibEntry
bibtag *bibTag
bibtags []*bibTag
strings BibString
}
const COMMENT = 57346
const STRING = 57347
const PREAMBLE = 57348
const ATSIGN = 57349
const COLON = 57350
const EQUAL = 57351
const COMMA = 57352
const POUND = 57353
const LBRACE = 57354
const RBRACE = 57355
const DQUOTE = 57356
const LPAREN = 57357
const RPAREN = 57358
const BAREIDENT = 57359
const IDENT = 57360
var bibtexToknames = [...]string{
"$end",
"error",
"$unk",
"COMMENT",
"STRING",
"PREAMBLE",
"ATSIGN",
"COLON",
"EQUAL",
"COMMA",
"POUND",
"LBRACE",
"RBRACE",
"DQUOTE",
"LPAREN",
"RPAREN",
"BAREIDENT",
"IDENT",
}
var bibtexStatenames = [...]string{}
const bibtexEofCode = 1
const bibtexErrCode = 2
const bibtexInitialStackSize = 16
// Parse is the entry point to the bibtex parser.
func Parse(r io.Reader) (*BibTex, error) {
l := NewLexer(r)
bibtexParse(l)
select {
case err := <-l.Errors:
return nil, err
default:
return bib, nil
}
}
var bibtexExca = [...]int{
-1, 1,
1, -1,
-2, 0,
}
const bibtexPrivate = 57344
const bibtexLast = 61
var bibtexAct = [...]int{
22, 39, 40, 41, 9, 10, 11, 24, 23, 44,
43, 27, 48, 26, 21, 20, 25, 8, 50, 28,
29, 33, 33, 49, 18, 16, 38, 19, 17, 14,
31, 12, 15, 42, 13, 30, 45, 46, 33, 33,
52, 51, 48, 36, 33, 47, 37, 33, 35, 34,
54, 53, 33, 7, 32, 4, 1, 6, 5, 3,
2,
}
var bibtexPact = [...]int{
-1000, -1000, 46, -1000, -1000, -1000, -1000, 0, 19, 17,
13, 12, -2, -3, -10, -10, -4, -6, -10, -10,
25, 20, 41, -1000, -1000, 36, 39, 34, 33, 10,
-14, -14, -1000, -8, -1000, -10, -10, -1000, -1000, 32,
-1000, 14, 2, -1000, -1000, 28, 27, -1000, -14, -10,
-1000, -1000, -1000, -1000, 11,
}
var bibtexPgo = [...]int{
0, 60, 59, 2, 58, 1, 0, 57, 56, 55,
}
var bibtexR1 = [...]int{
0, 8, 1, 1, 1, 1, 1, 2, 2, 9,
9, 4, 4, 7, 7, 6, 6, 6, 6, 3,
3, 5, 5,
}
var bibtexR2 = [...]int{
0, 1, 0, 2, 2, 2, 2, 7, 7, 5,
5, 7, 7, 5, 5, 1, 1, 3, 3, 0,
3, 1, 3,
}
var bibtexChk = [...]int{
-1000, -8, -1, -2, -9, -4, -7, 7, 17, 4,
5, 6, 12, 15, 12, 15, 12, 15, 12, 15,
17, 17, -6, 18, 17, -6, 17, 17, -6, -6,
10, 10, 13, 11, 13, 9, 9, 13, 16, -5,
-3, 17, -5, 18, 17, -6, -6, 13, 10, 9,
16, 13, 13, -3, -6,
}
var bibtexDef = [...]int{
2, -2, 1, 3, 4, 5, 6, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 15, 16, 0, 0, 0, 0, 0,
19, 19, 9, 0, 10, 0, 0, 13, 14, 0,
21, 0, 0, 17, 18, 0, 0, 7, 19, 0,
8, 11, 12, 22, 20,
}
var bibtexToken1 = [...]int{
1,
}
var bibtexToken2 = [...]int{
2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
12, 13, 14, 15, 16, 17, 18,
}
var bibtexTokem3 = [...]int{
0,
}
var bibtexErrorMessages = [...]struct {
state int
token int
msg string
}{}
/* parser for yacc output */
var (
bibtexDebug = 0
bibtexErrorVerbose = false
)
type bibtexLexer interface {
Lex(lval *bibtexSymType) int
Error(s string)
}
type bibtexParser interface {
Parse(bibtexLexer) int
Lookahead() int
}
type bibtexParserImpl struct {
lval bibtexSymType
stack [bibtexInitialStackSize]bibtexSymType
char int
}
func (p *bibtexParserImpl) Lookahead() int {
return p.char
}
func bibtexNewParser() bibtexParser {
return &bibtexParserImpl{}
}
const bibtexFlag = -1000
func bibtexTokname(c int) string {
if c >= 1 && c-1 < len(bibtexToknames) {
if bibtexToknames[c-1] != "" {
return bibtexToknames[c-1]
}
}
return __yyfmt__.Sprintf("tok-%v", c)
}
func bibtexStatname(s int) string {
if s >= 0 && s < len(bibtexStatenames) {
if bibtexStatenames[s] != "" {
return bibtexStatenames[s]
}
}
return __yyfmt__.Sprintf("state-%v", s)
}
func bibtexErrorMessage(state, lookAhead int) string {
const TOKSTART = 4
if !bibtexErrorVerbose {
return "syntax error"
}
for _, e := range bibtexErrorMessages {
if e.state == state && e.token == lookAhead {
return "syntax error: " + e.msg
}
}
res := "syntax error: unexpected " + bibtexTokname(lookAhead)
// To match Bison, suggest at most four expected tokens.
expected := make([]int, 0, 4)
// Look for shiftable tokens.
base := bibtexPact[state]
for tok := TOKSTART; tok-1 < len(bibtexToknames); tok++ {
if n := base + tok; n >= 0 && n < bibtexLast && bibtexChk[bibtexAct[n]] == tok {
if len(expected) == cap(expected) {
return res
}
expected = append(expected, tok)
}
}
if bibtexDef[state] == -2 {
i := 0
for bibtexExca[i] != -1 || bibtexExca[i+1] != state {
i += 2
}
// Look for tokens that we accept or reduce.
for i += 2; bibtexExca[i] >= 0; i += 2 {
tok := bibtexExca[i]
if tok < TOKSTART || bibtexExca[i+1] == 0 {
continue
}
if len(expected) == cap(expected) {
return res
}
expected = append(expected, tok)
}
// If the default action is to accept or reduce, give up.
if bibtexExca[i+1] != 0 {
return res
}
}
for i, tok := range expected {
if i == 0 {
res += ", expecting "
} else {
res += " or "
}
res += bibtexTokname(tok)
}
return res
}
func bibtexlex1(lex bibtexLexer, lval *bibtexSymType) (char, token int) {
token = 0
char = lex.Lex(lval)
if char <= 0 {
token = bibtexToken1[0]
goto out
}
if char < len(bibtexToken1) {
token = bibtexToken1[char]
goto out
}
if char >= bibtexPrivate {
if char < bibtexPrivate+len(bibtexToken2) {
token = bibtexToken2[char-bibtexPrivate]
goto out
}
}
for i := 0; i < len(bibtexTokem3); i += 2 {
token = bibtexTokem3[i+0]
if token == char {
token = bibtexTokem3[i+1]
goto out
}
}
out:
if token == 0 {
token = bibtexToken2[1] /* unknown char */
}
if bibtexDebug >= 3 {
__yyfmt__.Printf("lex %s(%d)\n", bibtexTokname(token), uint(char))
}
return char, token
}
func bibtexParse(bibtexlex bibtexLexer) int {
return bibtexNewParser().Parse(bibtexlex)
}
func (bibtexrcvr *bibtexParserImpl) Parse(bibtexlex bibtexLexer) int {
var bibtexn int
var bibtexVAL bibtexSymType
var bibtexDollar []bibtexSymType
_ = bibtexDollar // silence set and not used
bibtexS := bibtexrcvr.stack[:]
Nerrs := 0 /* number of errors */
Errflag := 0 /* error recovery flag */
bibtexstate := 0
bibtexrcvr.char = -1
bibtextoken := -1 // bibtexrcvr.char translated into internal numbering
defer func() {
// Make sure we report no lookahead when not parsing.
bibtexstate = -1
bibtexrcvr.char = -1
bibtextoken = -1
}()
bibtexp := -1
goto bibtexstack
ret0:
return 0
ret1:
return 1
bibtexstack:
/* put a state and value onto the stack */
if bibtexDebug >= 4 {
__yyfmt__.Printf("char %v in %v\n", bibtexTokname(bibtextoken), bibtexStatname(bibtexstate))
}
bibtexp++
if bibtexp >= len(bibtexS) {
nyys := make([]bibtexSymType, len(bibtexS)*2)
copy(nyys, bibtexS)
bibtexS = nyys
}
bibtexS[bibtexp] = bibtexVAL
bibtexS[bibtexp].yys = bibtexstate
bibtexnewstate:
bibtexn = bibtexPact[bibtexstate]
if bibtexn <= bibtexFlag {
goto bibtexdefault /* simple state */
}
if bibtexrcvr.char < 0 {
bibtexrcvr.char, bibtextoken = bibtexlex1(bibtexlex, &bibtexrcvr.lval)
}
bibtexn += bibtextoken
if bibtexn < 0 || bibtexn >= bibtexLast {
goto bibtexdefault
}
bibtexn = bibtexAct[bibtexn]
if bibtexChk[bibtexn] == bibtextoken { /* valid shift */
bibtexrcvr.char = -1
bibtextoken = -1
bibtexVAL = bibtexrcvr.lval
bibtexstate = bibtexn
if Errflag > 0 {
Errflag--
}
goto bibtexstack
}
bibtexdefault:
/* default state action */
bibtexn = bibtexDef[bibtexstate]
if bibtexn == -2 {
if bibtexrcvr.char < 0 {
bibtexrcvr.char, bibtextoken = bibtexlex1(bibtexlex, &bibtexrcvr.lval)
}
/* look through exception table */
xi := 0
for {
if bibtexExca[xi+0] == -1 && bibtexExca[xi+1] == bibtexstate {
break
}
xi += 2
}
for xi += 2; ; xi += 2 {
bibtexn = bibtexExca[xi+0]
if bibtexn < 0 || bibtexn == bibtextoken {
break
}
}
bibtexn = bibtexExca[xi+1]
if bibtexn < 0 {
goto ret0
}
}
if bibtexn == 0 {
/* error ... attempt to resume parsing */
switch Errflag {
case 0: /* brand new error */
bibtexlex.Error(bibtexErrorMessage(bibtexstate, bibtextoken))
Nerrs++
if bibtexDebug >= 1 {
__yyfmt__.Printf("%s", bibtexStatname(bibtexstate))
__yyfmt__.Printf(" saw %s\n", bibtexTokname(bibtextoken))
}
fallthrough
case 1, 2: /* incompletely recovered error ... try again */
Errflag = 3
/* find a state where "error" is a legal shift action */
for bibtexp >= 0 {
bibtexn = bibtexPact[bibtexS[bibtexp].yys] + bibtexErrCode
if bibtexn >= 0 && bibtexn < bibtexLast {
bibtexstate = bibtexAct[bibtexn] /* simulate a shift of "error" */
if bibtexChk[bibtexstate] == bibtexErrCode {
goto bibtexstack
}
}
/* the current p has no shift on "error", pop stack */
if bibtexDebug >= 2 {
__yyfmt__.Printf("error recovery pops state %d\n", bibtexS[bibtexp].yys)
}
bibtexp--
}
/* there is no state on the stack with an error shift ... abort */
goto ret1
case 3: /* no shift yet; clobber input char */
if bibtexDebug >= 2 {
__yyfmt__.Printf("error recovery discards %s\n", bibtexTokname(bibtextoken))
}
if bibtextoken == bibtexEofCode {
goto ret1
}
bibtexrcvr.char = -1
bibtextoken = -1
goto bibtexnewstate /* try again in the same state */
}
}
/* reduction by production bibtexn */
if bibtexDebug >= 2 {
__yyfmt__.Printf("reduce %v in:\n\t%v\n", bibtexn, bibtexStatname(bibtexstate))
}
bibtexnt := bibtexn
bibtexpt := bibtexp
_ = bibtexpt // guard against "declared and not used"
bibtexp -= bibtexR2[bibtexn]
// bibtexp is now the index of $0. Perform the default action. Iff the
// reduced production is ε, $1 is possibly out of range.
if bibtexp+1 >= len(bibtexS) {
nyys := make([]bibtexSymType, len(bibtexS)*2)
copy(nyys, bibtexS)
bibtexS = nyys
}
bibtexVAL = bibtexS[bibtexp+1]
/* consult goto table to find next state */
bibtexn = bibtexR1[bibtexn]
bibtexg := bibtexPgo[bibtexn]
bibtexj := bibtexg + bibtexS[bibtexp].yys + 1
if bibtexj >= bibtexLast {
bibtexstate = bibtexAct[bibtexg]
} else {
bibtexstate = bibtexAct[bibtexj]
if bibtexChk[bibtexstate] != -bibtexn {
bibtexstate = bibtexAct[bibtexg]
}
}
// dummy call; replaced with literal code
switch bibtexnt {
case 1:
bibtexDollar = bibtexS[bibtexpt-1 : bibtexpt+1]
{
}
case 2:
bibtexDollar = bibtexS[bibtexpt-0 : bibtexpt+1]
{
bibtexVAL.bibtex = NewBibTex()
bib = bibtexVAL.bibtex
}
case 3:
bibtexDollar = bibtexS[bibtexpt-2 : bibtexpt+1]
{
bibtexVAL.bibtex = bibtexDollar[1].bibtex
bibtexVAL.bibtex.AddEntry(bibtexDollar[2].bibentry)
}
case 4:
bibtexDollar = bibtexS[bibtexpt-2 : bibtexpt+1]
{
bibtexVAL.bibtex = bibtexDollar[1].bibtex
}
case 5:
bibtexDollar = bibtexS[bibtexpt-2 : bibtexpt+1]
{
bibtexVAL.bibtex = bibtexDollar[1].bibtex
bibtexVAL.bibtex.AddStringVar(bibtexDollar[2].bibtag.key, bibtexDollar[2].bibtag.val)
}
case 6:
bibtexDollar = bibtexS[bibtexpt-2 : bibtexpt+1]
{
bibtexVAL.bibtex = bibtexDollar[1].bibtex
bibtexVAL.bibtex.AddPreamble(bibtexDollar[2].strings)
}
case 7:
bibtexDollar = bibtexS[bibtexpt-7 : bibtexpt+1]
{
bibtexVAL.bibentry = NewBibEntry(bibtexDollar[2].strval, bibtexDollar[4].strval)
for _, t := range bibtexDollar[6].bibtags {
bibtexVAL.bibentry.AddField(t.key, t.val)
}
}
case 8:
bibtexDollar = bibtexS[bibtexpt-7 : bibtexpt+1]
{
bibtexVAL.bibentry = NewBibEntry(bibtexDollar[2].strval, bibtexDollar[4].strval)
for _, t := range bibtexDollar[6].bibtags {
bibtexVAL.bibentry.AddField(t.key, t.val)
}
}
case 9:
bibtexDollar = bibtexS[bibtexpt-5 : bibtexpt+1]
{
}
case 10:
bibtexDollar = bibtexS[bibtexpt-5 : bibtexpt+1]
{
}
case 11:
bibtexDollar = bibtexS[bibtexpt-7 : bibtexpt+1]
{
bibtexVAL.bibtag = &bibTag{key: bibtexDollar[4].strval, val: bibtexDollar[6].strings}
}
case 12:
bibtexDollar = bibtexS[bibtexpt-7 : bibtexpt+1]
{
bibtexVAL.bibtag = &bibTag{key: bibtexDollar[4].strval, val: bibtexDollar[6].strings}
}
case 13:
bibtexDollar = bibtexS[bibtexpt-5 : bibtexpt+1]
{
bibtexVAL.strings = bibtexDollar[4].strings
}
case 14:
bibtexDollar = bibtexS[bibtexpt-5 : bibtexpt+1]
{
bibtexVAL.strings = bibtexDollar[4].strings
}
case 15:
bibtexDollar = bibtexS[bibtexpt-1 : bibtexpt+1]
{
bibtexVAL.strings = NewBibConst(bibtexDollar[1].strval)
}
case 16:
bibtexDollar = bibtexS[bibtexpt-1 : bibtexpt+1]
{
bibtexVAL.strings = bib.GetStringVar(bibtexDollar[1].strval)
}
case 17:
bibtexDollar = bibtexS[bibtexpt-3 : bibtexpt+1]
{
bibtexVAL.strings = NewBibComposite(bibtexDollar[1].strings)
bibtexVAL.strings.(*BibComposite).Append(NewBibConst(bibtexDollar[3].strval))
}
case 18:
bibtexDollar = bibtexS[bibtexpt-3 : bibtexpt+1]
{
bibtexVAL.strings = NewBibComposite(bibtexDollar[1].strings)
bibtexVAL.strings.(*BibComposite).Append(bib.GetStringVar(bibtexDollar[3].strval))
}
case 19:
bibtexDollar = bibtexS[bibtexpt-0 : bibtexpt+1]
{
}
case 20:
bibtexDollar = bibtexS[bibtexpt-3 : bibtexpt+1]
{
bibtexVAL.bibtag = &bibTag{key: bibtexDollar[1].strval, val: bibtexDollar[3].strings}
}
case 21:
bibtexDollar = bibtexS[bibtexpt-1 : bibtexpt+1]
{
bibtexVAL.bibtags = []*bibTag{bibtexDollar[1].bibtag}
}
case 22:
bibtexDollar = bibtexS[bibtexpt-3 : bibtexpt+1]
{
if bibtexDollar[3].bibtag == nil {
bibtexVAL.bibtags = bibtexDollar[1].bibtags
} else {
bibtexVAL.bibtags = append(bibtexDollar[1].bibtags, bibtexDollar[3].bibtag)
}
}
}
goto bibtexstack /* stack new state and value */
}
// Copyright (c) 2020, 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.
// Copied and only lightly modified from:
// https://github.com/nickng/bibtex
// Licensed under an Apache-2.0 license
// and presumably Copyright (c) 2017 by Nick Ng
package bibtex
import (
"errors"
"fmt"
)
var (
// ErrUnexpectedAtsign is an error for unexpected @ in {}.
ErrUnexpectedAtsign = errors.New("Unexpected @ sign")
// ErrUnknownStringVar is an error for looking up undefined string var.
ErrUnknownStringVar = errors.New("Unknown string variable")
)
// ErrParse is a parse error.
type ErrParse struct {
Pos TokenPos
Err string // Error string returned from parser.
}
func (e *ErrParse) Error() string {
return fmt.Sprintf("Parse failed at %s: %s", e.Pos, e.Err)
}
// 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 bibtex
import (
"fmt"
"os"
"path/filepath"
"time"
)
// File maintains a record for an bibtex file
type File struct {
// file name -- full path
File string
// bibtex data loaded from file
BibTex *BibTex
// mod time for loaded bibfile -- to detect updates
Mod time.Time
}
// FullPath returns full path to given bibtex file,
// looking on standard BIBINPUTS or TEXINPUTS env var paths if not found locally.
func FullPath(fname string) (string, error) {
_, err := os.Stat(fname)
path := fname
nfErr := fmt.Errorf("bibtex file not found, even on BIBINPUTS or TEXINPUTS paths: %s", fname)
if os.IsNotExist(err) {
bin := os.Getenv("BIBINPUTS")
if bin == "" {
bin = os.Getenv("TEXINPUTS")
}
if bin == "" {
return "", nfErr
}
pth := filepath.SplitList(bin)
got := false
for _, p := range pth {
bf := filepath.Join(p, fname)
_, err = os.Stat(bf)
if err == nil {
path = bf
got = true
break
}
}
if !got {
return "", nfErr
}
}
path, err = filepath.Abs(path)
if err != nil {
return path, err
}
return path, nil
}
// Open [re]opens the given filename, looking on standard BIBINPUTS or TEXINPUTS
// env var paths if not found locally. If Mod >= mod timestamp on the file,
// and BibTex is already loaded, then nothing happens (already have it), but
// otherwise it parses the file and puts contents in BibTex field.
func (fl *File) Open(fname string) error {
path := fname
var err error
if fl.File == "" {
path, err = FullPath(fname)
if err != nil {
return err
}
fl.File = path
fl.BibTex = nil
fl.Mod = time.Time{}
// fmt.Printf("first open file: %s path: %s\n", fname, fl.File)
}
st, err := os.Stat(fl.File)
if err != nil {
return err
}
if fl.BibTex != nil && !fl.Mod.Before(st.ModTime()) {
// fmt.Printf("existing file: %v is fine: file mod: %v last mod: %v\n", fl.File, st.ModTime(), fl.Mod)
return nil
}
f, err := os.Open(fl.File)
if err != nil {
return err
}
defer f.Close()
parsed, err := Parse(f)
if err != nil {
err = fmt.Errorf("Bibtex bibliography: %s not loaded due to error(s):\n%v", fl.File, err)
return err
}
fl.BibTex = parsed
fl.Mod = st.ModTime()
// fmt.Printf("(re)loaded bibtex bibliography: %s\n", fl.File)
return nil
}
//////////////////////////////////////////////////////////////////////
// Files
// Files is a map of bibtex files
type Files map[string]*File
// Open [re]opens the given filename, looking on standard BIBINPUTS or TEXINPUTS
// env var paths if not found locally. If Mod >= mod timestamp on the file,
// and BibTex is already loaded, then nothing happens (already have it), but
// otherwise it parses the file and puts contents in BibTex field.
func (fl *Files) Open(fname string) (*File, error) {
if *fl == nil {
*fl = make(Files)
}
fr, has := (*fl)[fname]
if has {
err := fr.Open(fname)
return fr, err
}
fr = &File{}
err := fr.Open(fname)
if err != nil {
return nil, err
}
(*fl)[fname] = fr
return fr, nil
}
// Copyright (c) 2020, 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.
// Copied and only lightly modified from:
// https://github.com/nickng/bibtex
// Licensed under an Apache-2.0 license
// and presumably Copyright (c) 2017 by Nick Ng
//go:generate goyacc -p bibtex -o bibtex.y.go bibtex.y
package bibtex
import "io"
// Lexer for bibtex.
type Lexer struct {
scanner *Scanner
Errors chan error
}
// NewLexer returns a new yacc-compatible lexer.
func NewLexer(r io.Reader) *Lexer {
return &Lexer{scanner: NewScanner(r), Errors: make(chan error, 1)}
}
// Lex is provided for yacc-compatible parser.
func (l *Lexer) Lex(yylval *bibtexSymType) int {
token, strval := l.scanner.Scan()
yylval.strval = strval
return int(token)
}
// Error handles error.
func (l *Lexer) Error(err string) {
l.Errors <- &ErrParse{Err: err, Pos: l.scanner.pos}
}
// Copyright (c) 2020, 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.
// Copied and only lightly modified from:
// https://github.com/nickng/bibtex
// Licensed under an Apache-2.0 license
// and presumably Copyright (c) 2017 by Nick Ng
package bibtex
import (
"bufio"
"bytes"
"io"
"strconv"
"strings"
)
var parseField bool
// Scanner is a lexical scanner
type Scanner struct {
r *bufio.Reader
pos TokenPos
}
// NewScanner returns a new instance of Scanner.
func NewScanner(r io.Reader) *Scanner {
return &Scanner{r: bufio.NewReader(r), pos: TokenPos{Char: 0, Lines: []int{}}}
}
// read reads the next rune from the buffered reader.
// Returns the rune(0) if an error occurs (or io.eof is returned).
func (s *Scanner) read() rune {
ch, _, err := s.r.ReadRune()
if err != nil {
return eof
}
if ch == '\n' {
s.pos.Lines = append(s.pos.Lines, s.pos.Char)
s.pos.Char = 0
} else {
s.pos.Char++
}
return ch
}
// unread places the previously read rune back on the reader.
func (s *Scanner) unread() {
_ = s.r.UnreadRune()
if s.pos.Char == 0 {
s.pos.Char = s.pos.Lines[len(s.pos.Lines)-1]
s.pos.Lines = s.pos.Lines[:len(s.pos.Lines)-1]
} else {
s.pos.Char--
}
}
// Scan returns the next token and literal value.
func (s *Scanner) Scan() (tok Token, lit string) {
ch := s.read()
if isWhitespace(ch) {
s.ignoreWhitespace()
ch = s.read()
}
if isAlphanum(ch) {
s.unread()
return s.scanIdent()
}
switch ch {
case eof:
return 0, ""
case '@':
return ATSIGN, string(ch)
case ':':
return COLON, string(ch)
case ',':
parseField = false // reset parseField if reached end of field.
return COMMA, string(ch)
case '=':
parseField = true // set parseField if = sign outside quoted or ident.
return EQUAL, string(ch)
case '"':
return s.scanQuoted()
case '{':
if parseField {
return s.scanBraced()
}
return LBRACE, string(ch)
case '}':
if parseField { // reset parseField if reached end of entry.
parseField = false
}
return RBRACE, string(ch)
case '#':
return POUND, string(ch)
case ' ':
s.ignoreWhitespace()
}
return ILLEGAL, string(ch)
}
// scanIdent categorises a string to one of three categories.
func (s *Scanner) scanIdent() (tok Token, lit string) {
switch ch := s.read(); ch {
case '"':
return s.scanQuoted()
case '{':
return s.scanBraced()
default:
s.unread() // Not open quote/brace.
return s.scanBare()
}
}
func (s *Scanner) scanBare() (Token, string) {
var buf bytes.Buffer
for {
if ch := s.read(); ch == eof {
break
} else if !isAlphanum(ch) && !isBareSymbol(ch) || isWhitespace(ch) {
s.unread()
break
} else {
_, _ = buf.WriteRune(ch)
}
}
str := buf.String()
if strings.ToLower(str) == "comment" {
return COMMENT, str
} else if strings.ToLower(str) == "preamble" {
return PREAMBLE, str
} else if strings.ToLower(str) == "string" {
return STRING, str
} else if _, err := strconv.Atoi(str); err == nil && parseField { // Special case for numeric
return IDENT, str
}
return BAREIDENT, str
}
// scanBraced parses a braced string, like {this}.
func (s *Scanner) scanBraced() (Token, string) {
var buf bytes.Buffer
var macro bool
brace := 1
for {
if ch := s.read(); ch == eof {
break
} else if ch == '\\' {
_, _ = buf.WriteRune(ch)
macro = true
} else if ch == '{' {
_, _ = buf.WriteRune(ch)
brace++
} else if ch == '}' {
brace--
macro = false
if brace == 0 { // Balances open brace.
return IDENT, buf.String()
}
_, _ = buf.WriteRune(ch)
} else if ch == '@' {
if macro {
_, _ = buf.WriteRune(ch)
// } else {
// log.Printf("%s: %s", ErrUnexpectedAtsign, buf.String())
}
} else if isWhitespace(ch) {
_, _ = buf.WriteRune(ch)
macro = false
} else {
_, _ = buf.WriteRune(ch)
}
}
return ILLEGAL, buf.String()
}
// scanQuoted parses a quoted string, like "this".
func (s *Scanner) scanQuoted() (Token, string) {
var buf bytes.Buffer
brace := 0
for {
if ch := s.read(); ch == eof {
break
} else if ch == '{' {
brace++
} else if ch == '}' {
brace--
} else if ch == '"' {
if brace == 0 { // Matches open quote, unescaped
return IDENT, buf.String()
}
_, _ = buf.WriteRune(ch)
} else {
_, _ = buf.WriteRune(ch)
}
}
return ILLEGAL, buf.String()
}
// ignoreWhitespace consumes the current rune and all contiguous whitespace.
func (s *Scanner) ignoreWhitespace() {
for {
if ch := s.read(); ch == eof {
break
} else if !isWhitespace(ch) {
s.unread()
break
}
}
}
// Copyright (c) 2020, 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.
// Copied and only lightly modified from:
// https://github.com/nickng/bibtex
// Licensed under an Apache-2.0 license
// and presumably Copyright (c) 2017 by Nick Ng
package bibtex
import (
"fmt"
"strings"
)
// Lexer token.
type Token int
const (
// ILLEGAL stands for an invalid token.
ILLEGAL Token = iota
)
var eof = rune(0)
// TokenPos is a pair of coordinate to identify start of token.
type TokenPos struct {
Char int
Lines []int
}
func (p TokenPos) String() string {
return fmt.Sprintf("%d:%d", len(p.Lines)+1, p.Char)
}
func isWhitespace(ch rune) bool {
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
}
func isAlpha(ch rune) bool {
return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z')
}
func isDigit(ch rune) bool {
return ('0' <= ch && ch <= '9')
}
func isAlphanum(ch rune) bool {
return isAlpha(ch) || isDigit(ch)
}
func isBareSymbol(ch rune) bool {
return strings.ContainsRune("-_:./+", ch)
}
// isSymbol returns true if ch is a valid symbol
func isSymbol(ch rune) bool {
return strings.ContainsRune("!?&*+-./:;<>[]^_`|~@", ch)
}
func isOpenQuote(ch rune) bool {
return ch == '{' || ch == '"'
}
// 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 golang
import (
"strings"
"unsafe"
"cogentcore.org/core/icons"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/complete"
"cogentcore.org/core/parse/syms"
)
var BuiltinTypes syms.TypeMap
// InstallBuiltinTypes initializes the BuiltinTypes map
func InstallBuiltinTypes() {
if len(BuiltinTypes) != 0 {
return
}
for _, tk := range BuiltinTypeKind {
ty := syms.NewType(tk.Name, tk.Kind)
ty.Size = []int{tk.Size}
BuiltinTypes.Add(ty)
}
}
func (gl *GoLang) CompleteBuiltins(fs *parse.FileState, seed string, md *complete.Matches) {
for _, tk := range BuiltinTypeKind {
if strings.HasPrefix(tk.Name, seed) {
c := complete.Completion{Text: tk.Name, Label: tk.Name, Icon: icons.Type}
md.Matches = append(md.Matches, c)
}
}
for _, bs := range BuiltinMisc {
if strings.HasPrefix(bs, seed) {
c := complete.Completion{Text: bs, Label: bs, Icon: icons.Variable}
md.Matches = append(md.Matches, c)
}
}
for _, bs := range BuiltinFuncs {
if strings.HasPrefix(bs, seed) {
bs = bs + "()"
c := complete.Completion{Text: bs, Label: bs, Icon: icons.Function}
md.Matches = append(md.Matches, c)
}
}
for _, bs := range BuiltinPackages {
if strings.HasPrefix(bs, seed) {
c := complete.Completion{Text: bs, Label: bs, Icon: icons.Type} // todo: was types
md.Matches = append(md.Matches, c)
}
}
for _, bs := range BuiltinKeywords {
if strings.HasPrefix(bs, seed) {
c := complete.Completion{Text: bs, Label: bs, Icon: icons.Constant}
md.Matches = append(md.Matches, c)
}
}
}
// BuiltinTypeKind are the type names and kinds for builtin Go primitive types
// (i.e., those with names)
var BuiltinTypeKind = []syms.TypeKindSize{
{"int", syms.Int, int(unsafe.Sizeof(int(0)))},
{"int8", syms.Int8, 1},
{"int16", syms.Int16, 2},
{"int32", syms.Int32, 4},
{"int64", syms.Int64, 8},
{"uint", syms.Uint, int(unsafe.Sizeof(uint(0)))},
{"uint8", syms.Uint8, 1},
{"uint16", syms.Uint16, 2},
{"uint32", syms.Uint32, 4},
{"uint64", syms.Uint64, 8},
{"uintptr", syms.Uintptr, 8},
{"byte", syms.Uint8, 1},
{"rune", syms.Int32, 4},
{"float32", syms.Float32, 4},
{"float64", syms.Float64, 8},
{"complex64", syms.Complex64, 8},
{"complex128", syms.Complex128, 16},
{"bool", syms.Bool, 1},
{"string", syms.String, 0},
{"error", syms.Interface, 0},
{"struct{}", syms.Struct, 0},
{"interface{}", syms.Interface, 0},
}
// BuiltinMisc are misc builtin items
var BuiltinMisc = []string{
"true",
"false",
}
// BuiltinFuncs are functions builtin to the Go language
var BuiltinFuncs = []string{
"append",
"copy",
"delete",
"len",
"cap",
"make",
"new",
"complex",
"real",
"imag",
"close",
"panic",
"recover",
}
// BuiltinKeywords are keywords built into Go -- for, range, etc
var BuiltinKeywords = []string{
"break",
"case",
"chan",
"const",
"continue",
"default",
"defer",
"else",
"fallthrough",
"for",
"func",
"go",
"goto",
"if",
"import",
"interface",
"map",
"package",
"range",
"return",
"select",
"struct",
"switch",
"type",
"var",
}
// BuiltinPackages are the standard library packages
var BuiltinPackages = []string{
"bufio",
"bytes",
"context",
"crypto",
"compress",
"encoding",
"errors",
"expvar",
"flag",
"fmt",
"hash",
"html",
"image",
"io",
"log",
"math",
"mime",
"net",
"os",
"path",
"plugin",
"reflect",
"regexp",
"runtime",
"sort",
"strconv",
"strings",
"sync",
"syscall",
"testing",
"time",
"unicode",
"unsafe",
"tar",
"zip",
"bzip2",
"flate",
"gzip",
"lzw",
"zlib",
"heap",
"list",
"ring",
"aes",
"cipher",
"des",
"dsa",
"ecdsa",
"ed25519",
"elliptic",
"hmac",
"md5",
"rc4",
"rsa",
"sha1",
"sha256",
"sha512",
"tls",
"x509",
"sql",
"ascii85",
"asn1",
"base32",
"base64",
"binary",
"csv",
"gob",
"hex",
"json",
"pem",
"xml",
"ast",
"build",
"constant",
"doc",
"format",
"importer",
"parser",
"printer",
"scanner",
"token",
"types",
"adler32",
"crc32",
"crc64",
"fnv",
"template",
"color",
"draw",
"gif",
"jpeg",
"png",
"suffixarray",
"ioutil",
"syslog",
"big",
"bits",
"cmplx",
"rand",
"multipart",
"quotedprintable",
"http",
"cookiejar",
"cgi",
"httptrace",
"httputil",
"pprof",
"socktest",
"mail",
"rpc",
"jsonrpc",
"smtp",
"textproto",
"url",
"exec",
"signal",
"user",
"filepath",
"syntax",
"cgo",
"debug",
"atomic",
"math",
"sys",
"msan",
"race",
"trace",
"atomic",
"js",
"scanner",
"tabwriter",
"template",
"utf16",
"utf8",
}
// 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 golang
import (
"fmt"
"os"
"path/filepath"
"strings"
"unicode"
"cogentcore.org/core/icons"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/complete"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/parser"
"cogentcore.org/core/parse/syms"
"cogentcore.org/core/parse/token"
"cogentcore.org/core/tree"
)
var CompleteTrace = false
// Lookup is the main api called by completion code in giv/complete.go to lookup item
func (gl *GoLang) Lookup(fss *parse.FileStates, str string, pos lexer.Pos) (ld complete.Lookup) {
if str == "" {
return
}
origStr := str
str = lexer.LastScopedString(str)
if len(str) == 0 {
return
}
fs := fss.Done()
if len(fs.ParseState.Scopes) == 0 {
return // need a package
}
fs.SymsMu.RLock()
defer fs.SymsMu.RUnlock()
pr := gl.Parser()
if pr == nil {
return
}
fpath, _ := filepath.Abs(fs.Src.Filename)
if CompleteTrace {
fmt.Printf("lookup str: %v orig: %v\n", str, origStr)
}
lfs := pr.ParseString(str, fpath, fs.Src.Known)
if lfs == nil {
return
}
if CompleteTrace {
lfs.ParseState.AST.WriteTree(os.Stdout, 0)
lfs.LexState.Errs.Report(20, "", true, true)
lfs.ParseState.Errs.Report(20, "", true, true)
}
var scopes syms.SymMap // scope(s) for position, fname
scope := gl.CompletePosScope(fs, pos, fpath, &scopes)
start, last := gl.CompleteASTStart(lfs.ParseState.AST, scope)
if CompleteTrace {
if start == nil {
fmt.Printf("start = nil\n")
return
}
fmt.Printf("\n####################\nlookup start in scope: %v\n", scope)
lfs.ParseState.AST.WriteTree(os.Stdout, 0)
fmt.Printf("Start tree:\n")
start.WriteTree(os.Stdout, 0)
}
pkg := fs.ParseState.Scopes[0]
start.SrcReg.St = pos
if start == last { // single-item
seed := start.Src
if seed != "" {
return gl.LookupString(fs, pkg, scopes, seed)
}
return gl.LookupString(fs, pkg, scopes, str)
}
typ, nxt, got := gl.TypeFromASTExprStart(fs, pkg, pkg, start)
lststr := ""
if nxt != nil {
lststr = nxt.Src
}
if got {
if lststr != "" {
for _, mt := range typ.Meths {
nm := mt.Name
if !strings.HasPrefix(nm, lststr) {
continue
}
if mt.Filename != "" {
ld.SetFile(mt.Filename, mt.Region.St.Ln, mt.Region.Ed.Ln)
return
}
}
}
// fmt.Printf("got lookup type: %v, last str: %v\n", typ.String(), lststr)
ld.SetFile(typ.Filename, typ.Region.St.Ln, typ.Region.Ed.Ln)
return
}
// see if it starts with a package name..
snxt := start.NextAST()
lststr = last.Src
if snxt != nil && snxt.Src != "" {
ststr := snxt.Src
if lststr != "" && lststr != ststr {
ld = gl.LookupString(fs, pkg, nil, ststr+"."+lststr)
} else {
ld = gl.LookupString(fs, pkg, nil, ststr)
}
} else {
ld = gl.LookupString(fs, pkg, scopes, lststr)
}
if ld.Filename == "" { // didn't work
ld = gl.LookupString(fs, pkg, scopes, str)
}
return
}
// CompleteLine is the main api called by completion code in giv/complete.go
func (gl *GoLang) CompleteLine(fss *parse.FileStates, str string, pos lexer.Pos) (md complete.Matches) {
if str == "" {
return
}
origStr := str
str = lexer.LastScopedString(str)
if len(str) > 0 {
lstchr := str[len(str)-1]
mbrace, right := lexer.BracePair(rune(lstchr))
if mbrace != 0 && right { // don't try to match after closing expr
return
}
}
fs := fss.Done()
if len(fs.ParseState.Scopes) == 0 {
return // need a package
}
fs.SymsMu.RLock()
defer fs.SymsMu.RUnlock()
pr := gl.Parser()
if pr == nil {
return
}
fpath, _ := filepath.Abs(fs.Src.Filename)
if CompleteTrace {
fmt.Printf("complete str: %v orig: %v\n", str, origStr)
}
lfs := pr.ParseString(str, fpath, fs.Src.Known)
if lfs == nil {
return
}
if CompleteTrace {
lfs.ParseState.AST.WriteTree(os.Stdout, 0)
lfs.LexState.Errs.Report(20, "", true, true)
lfs.ParseState.Errs.Report(20, "", true, true)
}
var scopes syms.SymMap // scope(s) for position, fname
scope := gl.CompletePosScope(fs, pos, fpath, &scopes)
start, last := gl.CompleteASTStart(lfs.ParseState.AST, scope)
if CompleteTrace {
if start == nil {
fmt.Printf("start = nil\n")
return
}
fmt.Printf("\n####################\ncompletion start in scope: %v\n", scope)
lfs.ParseState.AST.WriteTree(os.Stdout, 0)
fmt.Printf("Start tree:\n")
start.WriteTree(os.Stdout, 0)
}
pkg := fs.ParseState.Scopes[0]
start.SrcReg.St = pos
if start == last { // single-item
seed := start.Src
if CompleteTrace {
fmt.Printf("start == last: %v\n", seed)
}
md.Seed = seed
if start.Name == "TypeNm" {
gl.CompleteTypeName(fs, pkg, seed, &md)
return
}
if len(scopes) > 0 {
syms.AddCompleteSymsPrefix(scopes, "", seed, &md)
}
gl.CompletePkgSyms(fs, pkg, seed, &md)
gl.CompleteBuiltins(fs, seed, &md)
return
}
typ, nxt, got := gl.TypeFromASTExprStart(fs, pkg, pkg, start)
lststr := ""
if nxt != nil {
lststr = nxt.Src
}
if got && typ != nil {
// fmt.Printf("got completion type: %v, last str: %v\n", typ.String(), lststr)
syms.AddCompleteTypeNames(typ, typ.Name, lststr, &md)
} else {
// see if it starts with a package name..
// todo: move this to a function as in lookup
snxt := start.NextAST()
if snxt != nil && snxt.Src != "" {
ststr := snxt.Src
psym, has := gl.PkgSyms(fs, pkg.Children, ststr)
if has {
lststr := last.Src
if lststr != "" && lststr != ststr {
var matches syms.SymMap
psym.Children.FindNamePrefixScoped(lststr, &matches)
syms.AddCompleteSyms(matches, ststr, &md)
md.Seed = lststr
} else {
syms.AddCompleteSyms(psym.Children, ststr, &md)
}
return
}
}
if CompleteTrace {
fmt.Printf("completion type not found\n")
}
}
// if len(md.Matches) == 0 {
// fmt.Printf("complete str: %v orig: %v\n", str, origStr)
// lfs.ParseState.AST.WriteTree(os.Stdout, 0)
// }
return
}
// CompletePosScope returns the scope for given position in given filename,
// and fills in the scoping symbol(s) in scMap
func (gl *GoLang) CompletePosScope(fs *parse.FileState, pos lexer.Pos, fpath string, scopes *syms.SymMap) token.Tokens {
fs.Syms.FindContainsRegion(fpath, pos, 2, token.None, scopes) // None matches any, 2 extra lines to add for new typing
if len(*scopes) == 0 {
return token.None
}
if len(*scopes) == 1 {
for _, sy := range *scopes {
if CompleteTrace {
fmt.Printf("scope: %v reg: %v pos: %v\n", sy.Name, sy.Region, pos)
}
return sy.Kind
}
}
var last *syms.Symbol
for _, sy := range *scopes {
if sy.Kind.SubCat() == token.NameFunction {
return sy.Kind
}
last = sy
}
if CompleteTrace {
fmt.Printf(" > 1 scopes!\n")
scopes.WriteDoc(os.Stdout, 0)
}
return last.Kind
}
// CompletePkgSyms matches all package symbols using seed
func (gl *GoLang) CompletePkgSyms(fs *parse.FileState, pkg *syms.Symbol, seed string, md *complete.Matches) {
md.Seed = seed
var matches syms.SymMap
pkg.Children.FindNamePrefixScoped(seed, &matches)
syms.AddCompleteSyms(matches, "", md)
}
// CompleteTypeName matches builtin and package type names to seed
func (gl *GoLang) CompleteTypeName(fs *parse.FileState, pkg *syms.Symbol, seed string, md *complete.Matches) {
md.Seed = seed
for _, tk := range BuiltinTypeKind {
if strings.HasPrefix(tk.Name, seed) {
c := complete.Completion{Text: tk.Name, Label: tk.Name, Icon: icons.Type}
md.Matches = append(md.Matches, c)
}
}
sfunc := strings.HasPrefix(seed, "func ")
for _, tk := range pkg.Types {
if !sfunc && strings.HasPrefix(tk.Name, "func ") {
continue
}
if strings.HasPrefix(tk.Name, seed) {
c := complete.Completion{Text: tk.Name, Label: tk.Name, Icon: icons.Type}
md.Matches = append(md.Matches, c)
}
}
}
// LookupString attempts to lookup a string, which could be a type name,
// (with package qualifier), could be partial, etc
func (gl *GoLang) LookupString(fs *parse.FileState, pkg *syms.Symbol, scopes syms.SymMap, str string) (ld complete.Lookup) {
str = lexer.TrimLeftToAlpha(str)
pnm, tnm := SplitType(str)
if pnm != "" && tnm != "" {
psym, has := gl.PkgSyms(fs, pkg.Children, pnm)
if has {
tnm = lexer.TrimLeftToAlpha(tnm)
var matches syms.SymMap
psym.Children.FindNamePrefixScoped(tnm, &matches)
if len(matches) == 1 {
var psy *syms.Symbol
for _, sy := range matches {
psy = sy
}
ld.SetFile(psy.Filename, psy.Region.St.Ln, psy.Region.Ed.Ln)
return
}
}
if CompleteTrace {
fmt.Printf("Lookup: package-qualified string not found: %v\n", str)
}
return
}
// try types to str:
var tym *syms.Type
nmatch := 0
for _, tk := range pkg.Types {
if strings.HasPrefix(tk.Name, str) {
tym = tk
nmatch++
}
}
if nmatch == 1 {
ld.SetFile(tym.Filename, tym.Region.St.Ln, tym.Region.Ed.Ln)
return
}
var matches syms.SymMap
if len(scopes) > 0 {
scopes.FindNamePrefixRecursive(str, &matches)
if len(matches) > 0 {
for _, sy := range matches {
ld.SetFile(sy.Filename, sy.Region.St.Ln, sy.Region.Ed.Ln) // take first
return
}
}
}
pkg.Children.FindNamePrefixScoped(str, &matches)
if len(matches) > 0 {
for _, sy := range matches {
ld.SetFile(sy.Filename, sy.Region.St.Ln, sy.Region.Ed.Ln) // take first
return
}
}
if CompleteTrace {
fmt.Printf("Lookup: string not found: %v\n", str)
}
return
}
// CompleteASTStart finds the best starting point in the given current-line AST
// to start completion process, which walks back down from that starting point
func (gl *GoLang) CompleteASTStart(ast *parser.AST, scope token.Tokens) (start, last *parser.AST) {
curi := tree.Last(ast)
if curi == nil {
return
}
cur := curi.(*parser.AST)
last = cur
start = cur
prv := cur
for {
var parent *parser.AST
if cur.Parent != nil {
parent = cur.Parent.(*parser.AST)
}
switch {
case cur.Name == "TypeNm":
return cur, last
case cur.Name == "File":
if prv != last && prv.Src == last.Src {
return last, last // triggers single-item completion
}
return prv, last
case cur.Name == "Selector":
if parent != nil {
if parent.Name[:4] == "Asgn" {
return cur, last
}
if strings.HasSuffix(parent.Name, "Expr") {
return cur, last
}
} else {
flds := strings.Fields(cur.Src)
cur.Src = flds[len(flds)-1] // skip any spaces
return cur, last
}
case cur.Name == "Name":
if cur.Src == "if" { // weird parsing if incomplete
if prv != last && prv.Src == last.Src {
return last, last // triggers single-item completion
}
return prv, last
}
if parent != nil {
if parent.Name[:4] == "Asgn" {
return prv, last
}
if strings.HasSuffix(parent.Name, "Expr") {
return cur, last
}
}
case cur.Name == "ExprStmt":
if scope == token.None {
return prv, last
}
if cur.Src != "(" && cur.Src == prv.Src {
return prv, last
}
if cur.Src != "(" && prv != last {
return prv, last
}
case strings.HasSuffix(cur.Name, "Stmt"):
return prv, last
case cur.Name == "Args":
return prv, last
}
nxt := cur.PrevAST()
if nxt == nil {
return cur, last
}
prv = cur
cur = nxt
}
return cur, last
}
// CompleteEdit returns the completion edit data for integrating the selected completion
// into the source
func (gl *GoLang) CompleteEdit(fss *parse.FileStates, text string, cp int, comp complete.Completion, seed string) (ed complete.Edit) {
// if the original is ChildByName() and the cursor is between d and B and the comp is Children,
// then delete the portion after "Child" and return the new comp and the number or runes past
// the cursor to delete
s2 := text[cp:]
gotParen := false
if len(s2) > 0 && lexer.IsLetterOrDigit(rune(s2[0])) {
for i, c := range s2 {
if c == '(' {
gotParen = true
s2 = s2[:i]
break
}
isalnum := c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c)
if !isalnum {
s2 = s2[:i]
break
}
}
} else {
s2 = ""
}
var nw = comp.Text
if gotParen && strings.HasSuffix(nw, "()") {
nw = nw[:len(nw)-2]
}
// fmt.Printf("text: %v|%v comp: %v s2: %v\n", text[:cp], text[cp:], nw, s2)
ed.NewText = nw
ed.ForwardDelete = len(s2)
return ed
}
// Copyright (c) 2020, 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 golang
import (
"fmt"
"os"
"path/filepath"
"strings"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/parser"
"cogentcore.org/core/parse/syms"
"cogentcore.org/core/parse/token"
)
// TypeFromASTExprStart starts walking the ast expression to find the type.
// This computes the last ast point as the stopping point for processing
// and then calls TypeFromASTExpr.
// It returns the type, any AST node that remained unprocessed at the end, and bool if found.
func (gl *GoLang) TypeFromASTExprStart(fs *parse.FileState, origPkg, pkg *syms.Symbol, tyast *parser.AST) (*syms.Type, *parser.AST, bool) {
last := tyast.NextSiblingAST()
// fmt.Printf("last: %v \n", last.PathUnique())
return gl.TypeFromASTExpr(fs, origPkg, pkg, tyast, last)
}
// TypeFromASTExpr walks the ast expression to find the type.
// It returns the type, any AST node that remained unprocessed at the end, and bool if found.
func (gl *GoLang) TypeFromASTExpr(fs *parse.FileState, origPkg, pkg *syms.Symbol, tyast, last *parser.AST) (*syms.Type, *parser.AST, bool) {
pos := tyast.SrcReg.St
fpath, _ := filepath.Abs(fs.Src.Filename)
// containers of given region -- local scoping
var conts syms.SymMap
fs.Syms.FindContainsRegion(fpath, pos, 2, token.NameFunction, &conts) // 2 extra lines always!
// if TraceTypes && len(conts) == 0 {
// fmt.Printf("TExpr: no conts for fpath: %v pos: %v\n", fpath, pos)
// }
// if TraceTypes {
// tyast.WriteTree(os.Stdout, 0)
// }
tnm := tyast.Name
switch {
case tnm == "FuncCall":
fun := tyast.NextAST()
if fun == nil {
return nil, nil, false
}
funm := fun.Src
sym, got := fs.FindNameScoped(funm, conts)
if got {
if !gl.InferEmptySymbolType(sym, fs, pkg) {
return nil, fun, false
}
if sym.Type == "" {
if TraceTypes {
fmt.Printf("TExpr: FuncCall: function type not set yet: %v\n", funm)
}
gl.InferSymbolType(sym, fs, pkg, true)
}
ftnm := sym.Type
ftyp, _ := gl.FindTypeName(ftnm, fs, pkg)
if ftyp != nil && len(ftyp.Size) == 2 {
return gl.TypeFromFuncCall(fs, origPkg, pkg, tyast, last, ftyp)
}
if TraceTypes {
fmt.Printf("TExpr: FuncCall: could not find function: %v\n", funm)
}
return nil, fun, false
} else {
if funm == "len" || funm == "cap" {
return BuiltinTypes["int"], nil, true
}
if funm == "append" {
farg := fun.NextAST().NextAST()
return gl.TypeFromASTExpr(fs, origPkg, pkg, farg, last)
}
ctyp, _ := gl.FindTypeName(funm, fs, pkg) // conversion
if ctyp != nil {
return ctyp, nil, true
}
if TraceTypes {
fmt.Printf("TExpr: FuncCall: could not find function: %v\n", funm)
}
return nil, fun, false
}
case tnm == "Selector":
if tyast.NumChildren() == 0 { // incomplete
return nil, nil, false
}
tnmA := tyast.ChildAST(0)
if tnmA.Name != "Name" {
if TraceTypes {
fmt.Printf("TExpr: selector start node kid is not a Name: %v, src: %v\n", tnmA.Name, tnmA.Src)
tnmA.WriteTree(os.Stdout, 0)
}
return nil, tnmA, false
}
return gl.TypeFromASTName(fs, origPkg, pkg, tnmA, last, conts)
case tnm == "Slice": // strings.HasPrefix(tnm, "Slice"):
if tyast.NumChildren() == 0 { // incomplete
return nil, nil, false
}
tnmA := tyast.ChildAST(0)
if tnmA.Name != "Name" {
if TraceTypes {
fmt.Printf("TExpr: slice start node kid is not a Name: %v, src: %v\n", tnmA.Name, tnmA.Src)
}
return nil, tnmA, false
}
snm := tnmA.Src
sym, got := fs.FindNameScoped(snm, conts)
if got {
return gl.TypeFromASTSym(fs, origPkg, pkg, tnmA, last, sym)
}
if TraceTypes {
fmt.Printf("TExpr: could not find symbol for slice var name: %v\n", snm)
}
return nil, tnmA, false
case tnm == "Name":
return gl.TypeFromASTName(fs, origPkg, pkg, tyast, last, conts)
case strings.HasPrefix(tnm, "Lit"):
sty, got := gl.TypeFromASTLit(fs, pkg, nil, tyast)
return sty, nil, got
case strings.HasSuffix(tnm, "AutoType"):
sty, got := gl.SubTypeFromAST(fs, pkg, tyast, 0)
return sty, nil, got
case tnm == "CompositeLit":
sty, got := gl.SubTypeFromAST(fs, pkg, tyast, 0)
return sty, nil, got
case tnm == "AddrExpr":
if !tyast.HasChildren() {
return nil, nil, false
}
ch := tyast.ChildAST(0)
snm := tyast.Src[1:] // after &
var sty *syms.Type
switch ch.Name {
case "CompositeLit":
sty, _ = gl.SubTypeFromAST(fs, pkg, ch, 0)
case "Selector":
sty, _ = gl.TypeFromAST(fs, pkg, nil, ch)
case "Name":
sym, got := fs.FindNameScoped(snm, conts)
if got {
sty, _, got = gl.TypeFromASTSym(fs, origPkg, pkg, ch, last, sym)
} else {
if snm == "true" || snm == "false" {
return BuiltinTypes["bool"], nil, true
}
if TraceTypes {
fmt.Printf("TExpr: could not find symbol named: %v\n", snm)
}
}
}
if sty != nil {
ty := &syms.Type{}
ty.Kind = syms.Ptr
tynm := SymTypeNameForPkg(sty, pkg)
ty.Name = "*" + tynm
ty.Els.Add("ptr", tynm)
return ty, nil, true
}
if TraceTypes {
fmt.Printf("TExpr: could not process addr expr:\n")
tyast.WriteTree(os.Stdout, 0)
}
return nil, tyast, false
case tnm == "DePtrExpr":
sty, got := gl.SubTypeFromAST(fs, pkg, tyast, 0) // first child
return sty, nil, got
case strings.HasSuffix(tnm, "Expr"):
// note: could figure out actual numerical type, but in practice we don't care
// for lookup / completion, so ignoring for now.
return BuiltinTypes["float64"], nil, true
case tnm == "TypeAssert":
sty, got := gl.SubTypeFromAST(fs, pkg, tyast, 1) // type is second child
return sty, nil, got
case tnm == "MakeCall":
sty, got := gl.SubTypeFromAST(fs, pkg, tyast, 0)
return sty, nil, got
case strings.Contains(tnm, "Chan"):
sty, got := gl.SubTypeFromAST(fs, pkg, tyast, 0)
return sty, nil, got
default:
if TraceTypes {
fmt.Printf("TExpr: cannot start with: %v\n", tyast.Name)
tyast.WriteTree(os.Stdout, 0)
}
return nil, tyast, false
}
return nil, tyast, false
}
// TypeFromASTSym attempts to get the type from given symbol as part of expression.
// It returns the type, any AST node that remained unprocessed at the end, and bool if found.
func (gl *GoLang) TypeFromASTSym(fs *parse.FileState, origPkg, pkg *syms.Symbol, tyast, last *parser.AST, sym *syms.Symbol) (*syms.Type, *parser.AST, bool) {
// if TraceTypes {
// fmt.Printf("TExpr: sym named: %v kind: %v type: %v\n", sym.Name, sym.Kind, sym.Type)
// }
if sym.Kind.SubCat() == token.NameScope {
// if TraceTypes {
// fmt.Printf("TExpr: symbol has scope type (package) -- will be looked up in a sec\n")
// }
return nil, nil, false // higher-level will catch it
}
if !gl.InferEmptySymbolType(sym, fs, pkg) {
return nil, tyast, false
}
tnm := sym.Type
return gl.TypeFromASTType(fs, origPkg, pkg, tyast, last, tnm)
}
// TypeFromASTType walks the ast expression to find the type, starting from current type name.
// It returns the type, any AST node that remained unprocessed at the end, and bool if found.
func (gl *GoLang) TypeFromASTType(fs *parse.FileState, origPkg, pkg *syms.Symbol, tyast, last *parser.AST, tnm string) (*syms.Type, *parser.AST, bool) {
if tnm[0] == '*' {
tnm = tnm[1:]
}
ttp, npkg := gl.FindTypeName(tnm, fs, pkg)
if ttp == nil {
if TraceTypes {
fmt.Printf("TExpr: error -- couldn't find type name: %v\n", tnm)
}
return nil, tyast, false
}
pkgnm := ""
if pi := strings.Index(ttp.Name, "."); pi > 0 {
pkgnm = ttp.Name[:pi]
}
if npkg != origPkg { // need to make a package-qualified copy of type
if pkgnm == "" {
pkgnm = npkg.Name
qtnm := QualifyType(pkgnm, ttp.Name)
if qtnm != ttp.Name {
if etyp, ok := pkg.Types[qtnm]; ok {
ttp = etyp
} else {
ntyp := &syms.Type{}
*ntyp = *ttp
ntyp.Name = qtnm
origPkg.Types.Add(ntyp)
ttp = ntyp
}
}
}
}
pkg = npkg // update to new context
// if TraceTypes {
// fmt.Printf("TExpr: found type: %v kind: %v\n", ttp.Name, ttp.Kind)
// }
if tyast == nil || tyast == last {
return ttp, tyast, true
}
if tyast.Name == "QualType" && tnm != tyast.Src {
// tyast.Src is new type name
return gl.TypeFromASTType(fs, origPkg, pkg, tyast, last, tyast.Src)
}
nxt := tyast
for {
nxt = nxt.NextAST()
if nxt == nil || nxt == last {
// if TraceTypes {
// fmt.Printf("TExpr: returning terminal type\n")
// }
return ttp, nxt, true
}
brk := false
switch {
case nxt.Name == "Name":
brk = true
case strings.HasPrefix(nxt.Name, "Lit"):
sty, got := gl.TypeFromASTLit(fs, pkg, nil, nxt)
return sty, nil, got
case nxt.Name == "TypeAssert":
sty, got := gl.SubTypeFromAST(fs, origPkg, nxt, 1) // type is second child, switch back to orig pkg
return sty, nil, got
case nxt.Name == "Slice":
continue
case strings.HasPrefix(nxt.Name, "Slice"):
eltyp := ttp.Els.ByName("val")
if eltyp != nil {
elnm := QualifyType(pkgnm, eltyp.Type)
// if TraceTypes {
// fmt.Printf("TExpr: slice/map el type: %v\n", elnm)
// }
return gl.TypeFromASTType(fs, origPkg, pkg, nxt, last, elnm)
}
if ttp.Name == "string" {
return BuiltinTypes["string"], nil, true
}
if TraceTypes {
fmt.Printf("TExpr: slice operator not on slice: %v\n", ttp.Name)
tyast.WriteTree(os.Stdout, 0)
}
case nxt.Name == "FuncCall":
// ttp is the function type name
fun := nxt.NextAST()
if fun == nil || fun == last {
return ttp, fun, true
}
funm := fun.Src
ftyp, got := ttp.Meths[funm]
if got && len(ftyp.Size) == 2 {
return gl.TypeFromFuncCall(fs, origPkg, pkg, nxt, last, ftyp)
} else {
if TraceTypes {
fmt.Printf("TExpr: FuncCall: could not find method: %v in type: %v\n", funm, ttp.Name)
tyast.WriteTree(os.Stdout, 0)
}
return nil, fun, false
}
}
if brk || nxt == nil || nxt == last {
break
}
// if TraceTypes {
// fmt.Printf("TExpr: skipping over %v\n", nxt.Nm)
// }
}
if nxt == nil {
return ttp, nxt, false
}
nm := nxt.Src
stp := ttp.Els.ByName(nm)
if stp != nil {
// if TraceTypes {
// fmt.Printf("TExpr: found Name: %v in type els\n", nm)
// }
return gl.TypeFromASTType(fs, origPkg, pkg, nxt, last, stp.Type)
}
// if TraceTypes {
// fmt.Printf("TExpr: error -- Name: %v not found in type els\n", nm)
// // ttp.WriteDoc(os.Stdout, 0)
// }
return ttp, nxt, true // robust, needed for completion
}
// TypeFromASTFuncCall gets return type of function call as return value, and returns the sibling node to
// continue parsing in, skipping over everything in the function call
func (gl *GoLang) TypeFromFuncCall(fs *parse.FileState, origPkg, pkg *syms.Symbol, tyast, last *parser.AST, ftyp *syms.Type) (*syms.Type, *parser.AST, bool) {
nxt := tyast.NextSiblingAST() // skip over everything within method in ast
if len(ftyp.Size) != 2 {
if TraceTypes {
fmt.Printf("TExpr: FuncCall: %v is not properly initialized with sizes\n", ftyp.Name)
}
return nil, nxt, false
}
npars := ftyp.Size[0] // first size is number of params
nrval := ftyp.Size[1] // second size is number of return values
if nrval == 0 {
if TraceTypes {
fmt.Printf("TExpr: FuncCall: %v has no return value\n", ftyp.Name)
}
return nil, nxt, false // no return -- shouldn't happen
}
rtyp := ftyp.Els[npars] // first return
if nxt != nil && nxt.Name == "Name" { // direct de-ref on function return value -- ASTType assumes nxt is type el
prv := nxt.PrevAST()
if prv != tyast {
nxt = prv
}
}
// if TraceTypes {
// fmt.Printf("got return type: %v\n", rtyp)
// }
return gl.TypeFromASTType(fs, origPkg, pkg, nxt, last, rtyp.Type)
}
// TypeFromASTName gets type from a Name in a given context (conts)
func (gl *GoLang) TypeFromASTName(fs *parse.FileState, origPkg, pkg *syms.Symbol, tyast, last *parser.AST, conts syms.SymMap) (*syms.Type, *parser.AST, bool) {
snm := tyast.Src
sym, got := fs.FindNameScoped(snm, conts)
if got && sym.Kind.SubCat() != token.NameScope {
tsym, nnxt, got := gl.TypeFromASTSym(fs, origPkg, pkg, tyast, last, sym)
if got {
return tsym, nnxt, got
}
if TraceTypes {
fmt.Printf("TExpr: got symbol but could not get type from sym name: %v\n", snm)
// tyast.WriteTree(os.Stdout, 0)
}
}
if snm == "true" || snm == "false" {
return BuiltinTypes["bool"], nil, true
}
// maybe it is a package name
psym, has := gl.PkgSyms(fs, pkg.Children, snm)
if has {
// if TraceTypes {
// fmt.Printf("TExpr: entering package name: %v\n", snm)
// }
nxt := tyast.NextAST()
if nxt != nil {
if nxt.Name == "Selector" {
nxt = nxt.NextAST()
}
return gl.TypeFromASTExpr(fs, origPkg, psym, nxt, last)
}
if TraceTypes {
fmt.Printf("TExpr: package alone not useful\n")
}
return nil, tyast, false // package alone not useful
}
if TraceTypes {
fmt.Printf("TExpr: could not find symbol for name: %v\n", snm)
}
return nil, tyast, false
}
// Copyright (c) 2020, 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 golang
import (
"fmt"
"unicode"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/parser"
"cogentcore.org/core/parse/syms"
"cogentcore.org/core/parse/token"
)
// TypeMeths gathers method types from the type symbol's children
func (gl *GoLang) TypeMeths(fs *parse.FileState, pkg *syms.Symbol, ty *syms.Type) {
_, tnm := SplitType(ty.Name)
tsym, got := pkg.Children.FindNameScoped(tnm)
if !got {
if !unicode.IsLower(rune(tnm[0])) && TraceTypes {
fmt.Printf("TypeMeths: error -- did NOT get type sym: %v in pkg: %v\n", tnm, pkg.Name)
}
return
}
for _, sy := range tsym.Children {
if sy.Kind.SubCat() != token.NameFunction || sy.AST == nil {
continue
}
fty := gl.FuncTypeFromAST(fs, pkg, sy.AST.(*parser.AST), nil)
if fty != nil {
fty.Kind = syms.Method
fty.Name = sy.Name
fty.Filename = sy.Filename
fty.Region = sy.Region
ty.Meths.Add(fty)
// if TraceTypes {
// fmt.Printf("TypeMeths: Added method: %v\n", fty)
// }
} else {
if TraceTypes {
fmt.Printf("TypeMeths: method failed: %v\n", sy.Name)
}
}
}
}
// NamesFromAST returns a slice of name(s) from namelist nodes
func (gl *GoLang) NamesFromAST(fs *parse.FileState, pkg *syms.Symbol, ast *parser.AST, idx int) []string {
sast := ast.ChildAST(idx)
if sast == nil {
if TraceTypes {
fmt.Printf("TraceTypes: could not find child 0 on ast %v", ast)
}
return nil
}
var sary []string
if sast.HasChildren() {
for i := range sast.Children {
sary = append(sary, gl.NamesFromAST(fs, pkg, sast, i)...)
}
} else {
sary = append(sary, sast.Src)
}
return sary
}
// FuncTypeFromAST initializes a function type from ast -- type can either be anon
// or a named type -- if anon then the name is the full type signature without param names
func (gl *GoLang) FuncTypeFromAST(fs *parse.FileState, pkg *syms.Symbol, ast *parser.AST, fty *syms.Type) *syms.Type {
// ast.WriteTree(os.Stdout, 0)
if ast == nil || !ast.HasChildren() {
return nil
}
pars := ast.ChildAST(0)
if pars == nil {
if TraceTypes {
fmt.Printf("TraceTypes: could not find child 0 on ast %v", ast)
}
return nil
}
if fty == nil {
fty = &syms.Type{}
fty.Kind = syms.Func
}
poff := 0
isMeth := false
if pars.Name == "MethRecvName" && len(ast.Children) > 2 {
isMeth = true
rcv := pars.Children[0].(*parser.AST)
rtyp := pars.Children[1].(*parser.AST)
fty.Els.Add(rcv.Src, rtyp.Src)
poff = 2
pars = ast.ChildAST(2)
} else if pars.Name == "Name" && len(ast.Children) > 1 {
poff = 1
pars = ast.ChildAST(1)
}
npars := len(pars.Children)
var sigpars *parser.AST
if npars > 0 && (pars.Name == "SigParams" || pars.Name == "SigParamsResult") {
if ps := pars.ChildAST(0); ps == nil {
sigpars = pars
pars = ps
npars = len(pars.Children)
} else {
npars = 0 // not really
}
}
if npars > 0 {
gl.ParamsFromAST(fs, pkg, pars, fty, "param")
npars = len(fty.Els) // how many we added -- auto-includes receiver for method
} else {
if isMeth {
npars = 1
}
}
nrvals := 0
if sigpars != nil && len(sigpars.Children) >= 2 {
rvals := sigpars.ChildAST(1)
gl.RvalsFromAST(fs, pkg, rvals, fty)
nrvals = len(fty.Els) - npars // how many we added..
} else if poff < 2 && (len(ast.Children) >= poff+2) {
rvals := ast.ChildAST(poff + 1)
gl.RvalsFromAST(fs, pkg, rvals, fty)
nrvals = len(fty.Els) - npars // how many we added..
}
fty.Size = []int{npars, nrvals}
return fty
}
// ParamsFromAST sets params as Els for given function type (also for return types)
func (gl *GoLang) ParamsFromAST(fs *parse.FileState, pkg *syms.Symbol, pars *parser.AST, fty *syms.Type, name string) {
npars := len(pars.Children)
var pnames []string // param names that all share same type
for i := 0; i < npars; i++ {
par := pars.Children[i].(*parser.AST)
psz := len(par.Children)
if par.Name == "ParType" && psz == 1 {
ptypa := par.Children[0].(*parser.AST)
if ptypa.Name == "TypeNm" { // could be multiple args with same type or a separate type-only arg
if ptl, _ := gl.FindTypeName(par.Src, fs, pkg); ptl != nil {
fty.Els.Add(fmt.Sprintf("%s_%v", name, i), par.Src)
continue
}
pnames = append(pnames, par.Src) // add to later type
} else {
ptyp, ok := gl.SubTypeFromAST(fs, pkg, par, 0)
if ok {
pnsz := len(pnames)
if pnsz > 0 {
for _, pn := range pnames {
fty.Els.Add(pn, ptyp.Name)
}
}
fty.Els.Add(fmt.Sprintf("%s_%v", name, i), ptyp.Name)
continue
}
pnames = nil
}
} else if psz == 2 { // ParName
pnm := par.Children[0].(*parser.AST)
ptyp, ok := gl.SubTypeFromAST(fs, pkg, par, 1)
if ok {
pnsz := len(pnames)
if pnsz > 0 {
for _, pn := range pnames {
fty.Els.Add(pn, ptyp.Name)
}
}
fty.Els.Add(pnm.Src, ptyp.Name)
continue
}
pnames = nil
}
}
}
// RvalsFromAST sets return value(s) as Els for given function type
func (gl *GoLang) RvalsFromAST(fs *parse.FileState, pkg *syms.Symbol, rvals *parser.AST, fty *syms.Type) {
if rvals.Name == "Block" { // todo: maybe others
return
}
nrvals := len(rvals.Children)
if nrvals == 1 { // single rval, unnamed, has type directly..
rval := rvals.ChildAST(0)
if rval.Name != "ParName" {
nrvals = 1
rtyp, ok := gl.SubTypeFromAST(fs, pkg, rvals, 0)
if ok {
fty.Els.Add("rval", rtyp.Name)
return
}
}
}
gl.ParamsFromAST(fs, pkg, rvals, fty, "rval")
}
// 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 golang
import (
_ "embed"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"unicode"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/indent"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/languages"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/token"
)
//go:embed go.parse
var parserBytes []byte
// GoLang implements the Lang interface for the Go language
type GoLang struct {
Pr *parse.Parser
}
// TheGoLang is the instance variable providing support for the Go language
var TheGoLang = GoLang{}
func init() {
parse.StandardLanguageProperties[fileinfo.Go].Lang = &TheGoLang
languages.ParserBytes[fileinfo.Go] = parserBytes
}
func (gl *GoLang) Parser() *parse.Parser {
if gl.Pr != nil {
return gl.Pr
}
lp, _ := parse.LanguageSupport.Properties(fileinfo.Go)
if lp.Parser == nil {
parse.LanguageSupport.OpenStandard()
}
gl.Pr = lp.Parser
if gl.Pr == nil {
return nil
}
return gl.Pr
}
// ParseFile is the main point of entry for external calls into the parser
func (gl *GoLang) ParseFile(fss *parse.FileStates, txt []byte) {
pr := gl.Parser()
if pr == nil {
log.Println("ParseFile: no parser; must call parse.LangSupport.OpenStandard() at startup!")
return
}
pfs := fss.StartProc(txt) // current processing one
ext := filepath.Ext(pfs.Src.Filename)
if ext == ".mod" { // note: mod doesn't parse!
fss.EndProc()
return
}
// fmt.Println("\nstarting Parse:", pfs.Src.Filename)
// lprf := profile.Start("LexAll")
pr.LexAll(pfs)
// lprf.End()
// pprf := profile.Start("ParseAll")
pr.ParseAll(pfs)
// pprf.End()
fss.EndProc() // only symbols still need locking, done separately
path, _ := filepath.Split(pfs.Src.Filename)
// fmt.Println("done parse")
if len(pfs.ParseState.Scopes) > 0 { // should be for complete files, not for snippets
pkg := pfs.ParseState.Scopes[0]
pfs.Syms[pkg.Name] = pkg // keep around..
// fmt.Printf("main pkg name: %v\n", pkg.Name)
path = strings.TrimSuffix(path, string([]rune{filepath.Separator}))
pfs.WaitGp.Add(1)
go func() {
gl.AddPathToSyms(pfs, path)
gl.AddImportsToExts(fss, pfs, pkg) // will do ResolveTypes when it finishes
// fmt.Println("done import")
}()
} else {
if TraceTypes {
fmt.Printf("not importing scope for: %v\n", path)
}
pfs.ClearAST()
if pfs.AST.HasChildren() {
pfs.AST.DeleteChildren()
}
// fmt.Println("done no import")
}
}
func (gl *GoLang) LexLine(fs *parse.FileState, line int, txt []rune) lexer.Line {
pr := gl.Parser()
if pr == nil {
return nil
}
return pr.LexLine(fs, line, txt)
}
func (gl *GoLang) ParseLine(fs *parse.FileState, line int) *parse.FileState {
pr := gl.Parser()
if pr == nil {
return nil
}
lfs := pr.ParseLine(fs, line) // should highlight same line?
return lfs
}
func (gl *GoLang) HighlightLine(fss *parse.FileStates, line int, txt []rune) lexer.Line {
pr := gl.Parser()
if pr == nil {
return nil
}
pfs := fss.Done()
ll := pr.LexLine(pfs, line, txt)
lfs := pr.ParseLine(pfs, line)
if lfs != nil {
ll = lfs.Src.Lexs[0]
cml := pfs.Src.Comments[line]
merge := lexer.MergeLines(ll, cml)
mc := merge.Clone()
if len(cml) > 0 {
initDepth := pfs.Src.PrevDepth(line)
pr.PassTwo.NestDepthLine(mc, initDepth)
}
lfs.Syms.WriteDoc(os.Stdout, 0)
lfs.Destroy()
return mc
} else {
return ll
}
}
// IndentLine returns the indentation level for given line based on
// previous line's indentation level, and any delta change based on
// e.g., brackets starting or ending the previous or current line, or
// other language-specific keywords. See lexer.BracketIndentLine for example.
// Indent level is in increments of tabSz for spaces, and tabs for tabs.
// Operates on rune source with markup lex tags per line.
func (gl *GoLang) IndentLine(fs *parse.FileStates, src [][]rune, tags []lexer.Line, ln int, tabSz int) (pInd, delInd, pLn int, ichr indent.Character) {
pInd, pLn, ichr = lexer.PrevLineIndent(src, tags, ln, tabSz)
curUnd, _ := lexer.LineStartEndBracket(src[ln], tags[ln])
_, prvInd := lexer.LineStartEndBracket(src[pLn], tags[pLn])
brackParen := false // true if line only has bracket and paren -- outdent current
if len(tags[pLn]) >= 2 { // allow for comments
pl := tags[pLn][0]
ll := tags[pLn][1]
if ll.Token.Token == token.PunctGpRParen && pl.Token.Token == token.PunctGpRBrace {
brackParen = true
}
}
delInd = 0
if brackParen {
delInd-- // outdent
}
switch {
case prvInd && curUnd:
case prvInd:
delInd++
case curUnd:
delInd--
}
pwrd := lexer.FirstWord(string(src[pLn]))
cwrd := lexer.FirstWord(string(src[ln]))
if cwrd == "case" || cwrd == "default" {
if pwrd == "switch" {
delInd = 0
} else if pwrd == "case" {
delInd = 0
} else {
delInd = -1
}
} else if pwrd == "case" || pwrd == "default" {
delInd = 1
}
if pInd == 0 && delInd < 0 { // error..
delInd = 0
}
return
}
// AutoBracket returns what to do when a user types a starting bracket character
// (bracket, brace, paren) while typing.
// pos = position where bra will be inserted, and curLn is the current line
// match = insert the matching ket, and newLine = insert a new line.
func (gl *GoLang) AutoBracket(fs *parse.FileStates, bra rune, pos lexer.Pos, curLn []rune) (match, newLine bool) {
lnLen := len(curLn)
if bra == '{' {
if pos.Ch == lnLen {
if lnLen == 0 || unicode.IsSpace(curLn[pos.Ch-1]) {
newLine = true
}
match = true
} else {
match = unicode.IsSpace(curLn[pos.Ch])
}
} else {
match = pos.Ch == lnLen || unicode.IsSpace(curLn[pos.Ch]) // at end or if space after
}
return
}
// 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 golang
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"unicode"
"unicode/utf8"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/syms"
"cogentcore.org/core/parse/token"
"golang.org/x/tools/go/packages"
)
// ParseDirLock provides a lock protecting parsing of a package directory
type ParseDirLock struct {
// logical import path
Path string
Processing bool
// mutex protecting processing of this path
Mu sync.Mutex `json:"-" xml:"-"`
}
// ParseDirLocks manages locking for parsing package directories
type ParseDirLocks struct {
// map of paths with processing status
Dirs map[string]*ParseDirLock
// mutex protecting access to Dirs
Mu sync.Mutex `json:"-" xml:"-"`
}
// TheParseDirs is the parse dirs locking manager
var TheParseDirs ParseDirLocks
// ParseDir is how you call ParseDir on a given path in a secure way that is
// managed for multiple accesses. If dir is currently being parsed, then
// the mutex is locked and caller will wait until that is done -- at which point
// the next one should be able to load parsed symbols instead of parsing fresh.
// Once the symbols are returned, then the local FileState SymsMu lock must be
// used when integrating any external symbols back into another filestate.
// As long as all the symbol resolution etc is all happening outside of the
// external syms linking, then it does not need to be protected.
func (pd *ParseDirLocks) ParseDir(gl *GoLang, fs *parse.FileState, path string, opts parse.LanguageDirOptions) *syms.Symbol {
pfld := strings.Fields(path)
if len(pfld) > 1 { // remove first alias
path = pfld[1]
}
pd.Mu.Lock()
if pd.Dirs == nil {
pd.Dirs = make(map[string]*ParseDirLock)
}
ds, has := pd.Dirs[path]
if !has {
ds = &ParseDirLock{Path: path}
pd.Dirs[path] = ds
}
pd.Mu.Unlock()
ds.Mu.Lock()
ds.Processing = true
rsym := gl.ParseDirImpl(fs, path, opts)
ds.Processing = false
ds.Mu.Unlock()
return rsym
}
// ParseDirExcludes are files to exclude in processing directories
// because they take a long time and aren't very useful (data files).
// Any file that contains one of these strings is excluded.
var ParseDirExcludes = []string{
"/image/font/gofont/",
"zerrors_",
"unicode/tables.go",
"filecat/mimetype.go",
"/html/entity.go",
"/draw/impl.go",
"/truetype/hint.go",
"/runtime/proc.go",
}
// ParseDir is the interface call for parsing a directory
func (gl *GoLang) ParseDir(fs *parse.FileState, path string, opts parse.LanguageDirOptions) *syms.Symbol {
if path == "" || path == "C" || path[0] == '_' {
return nil
}
return TheParseDirs.ParseDir(gl, fs, path, opts)
}
// ParseDirImpl does the actual work of parsing a directory.
// Path is assumed to be a package import path or a local file name
func (gl *GoLang) ParseDirImpl(fs *parse.FileState, path string, opts parse.LanguageDirOptions) *syms.Symbol {
var files []string
var pkgPathAbs string
gm := os.Getenv("GO111MODULE")
if filepath.IsAbs(path) {
pkgPathAbs = path
} else {
pkgPathAbs = path
if gm == "off" { // note: using GOPATH manual mechanism as packages.Load is very slow
// fmt.Printf("nomod\n")
_, err := os.Stat(pkgPathAbs)
if os.IsNotExist(err) {
pkgPathAbs, err = fsx.GoSrcDir(pkgPathAbs)
if err != nil {
if TraceTypes {
log.Println(err)
}
return nil
}
} else if err != nil {
log.Println(err.Error())
return nil
}
pkgPathAbs, _ = filepath.Abs(pkgPathAbs)
} else { // modules mode
fabs, has := fs.PathMapLoad(path) // only use cache for modules mode -- GOPATH is fast
if has && !opts.Rebuild { // rebuild always re-paths
pkgPathAbs = fabs
// fmt.Printf("using cached path: %s to: %s\n", path, pkgPathAbs)
} else {
// fmt.Printf("mod: loading package: %s\n", path)
// packages automatically deals with GOPATH vs. modules, etc.
pkgs, err := packages.Load(&packages.Config{Mode: packages.NeedName | packages.NeedFiles}, path)
if err != nil {
// this is too many errors!
// log.Println(err)
return nil
}
if len(pkgs) != 1 {
fmt.Printf("More than one package for path: %v\n", path)
return nil
}
pkg := pkgs[0]
if len(pkg.GoFiles) == 0 {
// fmt.Printf("No Go files found in package: %v\n", path)
return nil
}
// files = pkg.GoFiles
fgo := pkg.GoFiles[0]
pkgPathAbs, _ = filepath.Abs(filepath.Dir(fgo))
// fmt.Printf("mod: %v package: %v PkgPath: %s\n", gm, path, pkgPathAbs)
}
fs.PathMapStore(path, pkgPathAbs) // cache for later
}
// fmt.Printf("Parsing, loading path: %v\n", path)
}
files = fsx.Filenames(pkgPathAbs, ".go")
if len(files) == 0 {
// fmt.Printf("No go files, bailing\n")
return nil
}
for i, pt := range files {
files[i] = filepath.Join(pkgPathAbs, pt)
}
if !opts.Rebuild {
csy, cts, err := syms.OpenSymCache(fileinfo.Go, pkgPathAbs)
if err == nil && csy != nil {
sydir := filepath.Dir(csy.Filename)
diffPath := sydir != pkgPathAbs
// if diffPath {
// fmt.Printf("rebuilding %v because path: %v != cur path: %v\n", path, sydir, pkgPathAbs)
// }
if diffPath || (!gl.Pr.ModTime.IsZero() && cts.Before(gl.Pr.ModTime)) {
// fmt.Printf("rebuilding %v because parser: %v is newer than cache: %v\n", path, gl.Pr.ModTime, cts)
} else {
lstmod := fsx.LatestMod(pkgPathAbs, ".go")
if lstmod.Before(cts) {
// fmt.Printf("loaded cache for: %v from: %v\n", pkgPathAbs, cts)
return csy
}
}
}
}
pr := gl.Parser()
var pkgsym *syms.Symbol
var fss []*parse.FileState // file states for each file
for _, fpath := range files {
fnm := filepath.Base(fpath)
if strings.HasSuffix(fnm, "_test.go") {
continue
}
// avoid processing long slow files that aren't needed anyway:
excl := false
for _, ex := range ParseDirExcludes {
if strings.Contains(fpath, ex) {
excl = true
break
}
}
if excl {
continue
}
fs := parse.NewFileState() // we use a separate fs for each file, so we have full ast
fss = append(fss, fs)
// optional monitoring of parsing
// fs.ParseState.Trace.On = true
// fs.ParseState.Trace.Match = true
// fs.ParseState.Trace.NoMatch = true
// fs.ParseState.Trace.Run = true
// fs.ParseState.Trace.RunAct = true
// fs.ParseState.Trace.StdOut()
err := fs.Src.OpenFile(fpath)
if err != nil {
continue
}
// fmt.Printf("parsing file: %v\n", fnm)
// stt := time.Now()
pr.LexAll(fs)
// lxdur := time.Now().Sub(stt)
pr.ParseAll(fs)
// prdur := time.Now().Sub(stt)
// if prdur > 500*time.Millisecond {
// fmt.Printf("file: %s full parse: %v\n", fpath, prdur)
// }
if len(fs.ParseState.Scopes) > 0 { // should be
pkg := fs.ParseState.Scopes[0]
gl.DeleteUnexported(pkg, pkg.Name)
if pkgsym == nil {
pkgsym = pkg
} else {
pkgsym.CopyFromScope(pkg)
if TraceTypes {
pkgsym.Types.PrintUnknowns()
}
}
// } else {
// fmt.Printf("\tno parse state scopes!\n")
}
}
if pkgsym == nil || len(fss) == 0 {
return nil
}
pfs := fss[0] // parse.NewFileState() // master overall package file state
gl.ResolveTypes(pfs, pkgsym, false) // false = don't include function-internal scope items
gl.DeleteExternalTypes(pkgsym)
if !opts.Nocache {
syms.SaveSymCache(pkgsym, fileinfo.Go, pkgPathAbs)
}
pkgsym.ClearAST() // otherwise memory can be huge -- can comment this out for debugging
for _, fs := range fss {
fs.Destroy()
}
return pkgsym
}
/////////////////////////////////////////////////////////////////////////////
// Go util funcs
// DeleteUnexported deletes lower-case unexported items from map, and
// children of symbols on map
func (gl *GoLang) DeleteUnexported(sy *syms.Symbol, pkgsc string) {
if sy.Kind.SubCat() != token.NameScope { // only for top-level scopes
return
}
for nm, ss := range sy.Children {
if ss == sy {
fmt.Printf("warning: child is self!: %v\n", sy.String())
delete(sy.Children, nm)
continue
}
if ss.Kind.SubCat() != token.NameScope { // typically lowercase
rn, _ := utf8.DecodeRuneInString(nm)
if nm == "" || unicode.IsLower(rn) {
delete(sy.Children, nm)
continue
}
// sc, has := ss.Scopes[token.NamePackage]
// if has && sc != pkgsc {
// fmt.Printf("excluding out-of-scope symbol: %v %v\n", sc, ss.String())
// delete(sy.Children, nm)
// continue
// }
}
if ss.HasChildren() {
gl.DeleteUnexported(ss, pkgsc)
}
}
}
// DeleteExternalTypes deletes types from outside current package scope.
// These can be created during ResolveTypes but should be deleted before
// saving symbol type.
func (gl *GoLang) DeleteExternalTypes(sy *syms.Symbol) {
pkgsc := sy.Name
for nm, ty := range sy.Types {
sc, has := ty.Scopes[token.NamePackage]
if has && sc != pkgsc {
// fmt.Printf("excluding out-of-scope type: %v %v\n", sc, ty.String())
delete(sy.Types, nm)
continue
}
}
}
// ImportPathPkg returns the package (last dir) and base of import path
// from import path string -- removes any quotes around path first.
func (gl *GoLang) ImportPathPkg(im string) (path, base, pkg string) {
sz := len(im)
if sz < 3 {
return
}
path = im
if im[0] == '"' {
path = im[1 : sz-1]
}
base, pkg = filepath.Split(path)
return
}
// PkgSyms attempts to find package symbols for given package name.
// Is also passed any current package symbol context in psyms which might be
// different from default filestate context.
func (gl *GoLang) PkgSyms(fs *parse.FileState, psyms syms.SymMap, pnm string) (*syms.Symbol, bool) {
psym, has := fs.ExtSyms[pnm]
if has {
return psym, has
}
ipsym, has := gl.FindImportPkg(fs, psyms, pnm) // look for import within psyms package symbols
if has {
gl.AddImportToExts(fs, ipsym.Name, false) // no lock
psym, has = fs.ExtSyms[pnm]
}
return psym, has
}
// AddImportsToExts adds imports from given package into parse.FileState.ExtSyms list
// imports are coded as NameLibrary symbols with names = import path
func (gl *GoLang) AddImportsToExts(fss *parse.FileStates, pfs *parse.FileState, pkg *syms.Symbol) {
var imps syms.SymMap
pfs.SymsMu.RLock()
pkg.Children.FindKindScoped(token.NameLibrary, &imps)
pfs.SymsMu.RUnlock()
if len(imps) == 0 {
goto reset
return
}
for _, im := range imps {
if im.Name == "C" {
continue
}
// pfs.WaitGp.Add(1) // note: already under an outer-loop go routine
// with *same* waitgp
gl.AddImportToExts(pfs, im.Name, false) // no lock
}
// pfs.WaitGp.Wait() // each goroutine will do done when done..
// now all the info is in place: parse it
if TraceTypes {
fmt.Printf("\n#####################\nResolving Types now for: %v\n", pfs.Src.Filename)
}
gl.ResolveTypes(pfs, pkg, true) // true = do include function-internal scope items
reset:
pfs.ClearAST()
pkg.ClearAST()
// if pfs.AST.HasChildren() {
// pfs.AST.DeleteChildren()
// }
}
// AddImportToExts adds given import into parse.FileState.ExtSyms list
// assumed to be called as a separate goroutine
func (gl *GoLang) AddImportToExts(fs *parse.FileState, im string, lock bool) {
im, _, pkg := gl.ImportPathPkg(im)
psym := gl.ParseDir(fs, im, parse.LanguageDirOptions{})
if psym != nil {
psym.Name = pkg
if lock {
fs.SymsMu.Lock()
}
gl.AddPkgToExts(fs, psym)
if lock {
fs.SymsMu.Unlock()
}
}
if lock {
fs.WaitGp.Done()
}
}
// AddPathToSyms adds given path into parse.FileState.Syms list
// Is called as a separate goroutine in ParseFile with WaitGp
func (gl *GoLang) AddPathToSyms(fs *parse.FileState, path string) {
psym := gl.ParseDir(fs, path, parse.LanguageDirOptions{})
if psym != nil {
gl.AddPkgToSyms(fs, psym)
}
fs.WaitGp.Done()
}
// AddPkgToSyms adds given package symbol, with children from package
// to parse.FileState.Syms map -- merges with anything already there
// does NOT add imports -- that is an optional second step.
// Returns true if there was an existing entry for this package.
func (gl *GoLang) AddPkgToSyms(fs *parse.FileState, pkg *syms.Symbol) bool {
fs.SymsMu.Lock()
psy, has := fs.Syms[pkg.Name]
if has {
// fmt.Printf("AddPkgToSyms: importing pkg types: %v\n", pkg.Name)
psy.CopyFromScope(pkg)
if TraceTypes {
psy.Types.PrintUnknowns()
}
} else {
fs.Syms[pkg.Name] = pkg
}
fs.SymsMu.Unlock()
return has
}
// AddPathToExts adds given path into parse.FileState.ExtSyms list
// assumed to be called as a separate goroutine
func (gl *GoLang) AddPathToExts(fs *parse.FileState, path string) {
psym := gl.ParseDir(fs, path, parse.LanguageDirOptions{})
if psym != nil {
gl.AddPkgToExts(fs, psym)
}
}
// AddPkgToExts adds given package symbol, with children from package
// to parse.FileState.ExtSyms map -- merges with anything already there
// does NOT add imports -- that is an optional second step.
// Returns true if there was an existing entry for this package.
func (gl *GoLang) AddPkgToExts(fs *parse.FileState, pkg *syms.Symbol) bool {
psy, has := fs.ExtSyms[pkg.Name]
if has {
psy.CopyFromScope(pkg)
pkg = psy
} else {
if fs.ExtSyms == nil {
fs.ExtSyms = make(syms.SymMap)
}
fs.ExtSyms[pkg.Name] = pkg
}
return has
}
// FindImportPkg attempts to find an import package based on symbols in
// an existing package. For indirect loading of packages from other packages
// that we don't direct import.
func (gl *GoLang) FindImportPkg(fs *parse.FileState, psyms syms.SymMap, nm string) (*syms.Symbol, bool) {
for _, sy := range psyms {
if sy.Kind != token.NameLibrary {
continue
}
_, _, pkg := gl.ImportPathPkg(sy.Name)
if pkg == nm {
return sy, true
}
}
return nil, false
}
// 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 golang
import (
"fmt"
"os"
"strings"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/parser"
"cogentcore.org/core/parse/syms"
"cogentcore.org/core/parse/token"
)
// TypeErr indicates is the type name we use to indicate that the type could not be inferred
var TypeErr = "<err>"
// TypeInProcess indicates is the type name we use to indicate that the type
// is currently being processed -- prevents loops
var TypeInProcess = "<in-process>"
// InferSymbolType infers the symbol types for given symbol and all of its children
// funInternal determines whether to include function-internal symbols
// (e.g., variables within function scope -- only for local files).
func (gl *GoLang) InferSymbolType(sy *syms.Symbol, fs *parse.FileState, pkg *syms.Symbol, funInternal bool) {
if sy.Name == "" {
sy.Type = TypeErr
return
}
if sy.Name[0] == '_' {
sy.Type = TypeErr
return
}
if sy.AST != nil {
ast := sy.AST.(*parser.AST)
switch {
case sy.Kind == token.NameField:
stsc, ok := sy.Scopes[token.NameStruct]
if ok {
stty, _ := gl.FindTypeName(stsc, fs, pkg)
if stty != nil {
fldel := stty.Els.ByName(sy.Name)
if fldel != nil {
sy.Type = fldel.Type
// fmt.Printf("set field type: %s\n", sy.Label())
} else {
if TraceTypes {
fmt.Printf("InferSymbolType: field named: %v not found in struct type: %v\n", sy.Name, stty.Name)
}
}
} else {
if TraceTypes {
fmt.Printf("InferSymbolType: field named: %v struct type: %v not found\n", sy.Name, stsc)
}
}
if sy.Type == "" {
sy.Type = stsc + "." + sy.Name
}
} else {
if TraceTypes {
fmt.Printf("InferSymbolType: field named: %v doesn't have NameStruct scope\n", sy.Name)
}
}
case sy.Kind == token.NameVarClass: // method receiver
stsc, ok := sy.Scopes.SubCat(token.NameType)
if ok {
sy.Type = stsc
}
case sy.Kind.SubCat() == token.NameVar:
var astyp *parser.AST
if ast.HasChildren() {
if strings.HasPrefix(ast.Name, "ForRange") {
gl.InferForRangeSymbolType(sy, fs, pkg)
} else {
astyp = ast.ChildAST(len(ast.Children) - 1)
vty, ok := gl.TypeFromAST(fs, pkg, nil, astyp)
if ok {
sy.Type = SymTypeNameForPkg(vty, pkg)
// if TraceTypes {
// fmt.Printf("namevar: %v type: %v from ast\n", sy.Name, sy.Type)
// }
} else {
sy.Type = TypeErr // actively mark as err so not re-processed
if TraceTypes {
fmt.Printf("InferSymbolType: NameVar: %v NOT resolved from ast: %v\n", sy.Name, astyp.Path())
astyp.WriteTree(os.Stdout, 0)
}
}
}
} else {
sy.Type = TypeErr
if TraceTypes {
fmt.Printf("InferSymbolType: NameVar: %v has no children\n", sy.Name)
}
}
case sy.Kind == token.NameConstant:
if !strings.HasPrefix(ast.Name, "ConstSpec") {
if TraceTypes {
fmt.Printf("InferSymbolType: NameConstant: %v not a const: %v\n", sy.Name, ast.Name)
}
return
}
parent := ast.ParentAST()
if parent != nil && parent.HasChildren() {
fc := parent.ChildAST(0)
if fc.HasChildren() {
ffc := fc.ChildAST(0)
if ffc.Name == "Name" {
ffc = ffc.NextAST()
}
var vty *syms.Type
if ffc != nil {
vty, _ = gl.TypeFromAST(fs, pkg, nil, ffc)
}
if vty != nil {
sy.Type = SymTypeNameForPkg(vty, pkg)
} else {
sy.Type = TypeErr
if TraceTypes {
fmt.Printf("InferSymbolType: NameConstant: %v NOT resolved from ast: %v\n", sy.Name, ffc.Path())
ffc.WriteTree(os.Stdout, 1)
}
}
} else {
sy.Type = TypeErr
}
} else {
sy.Type = TypeErr
}
case sy.Kind.SubCat() == token.NameType:
vty, _ := gl.FindTypeName(sy.Name, fs, pkg)
if vty != nil {
sy.Type = SymTypeNameForPkg(vty, pkg)
} else {
// if TraceTypes {
// fmt.Printf("InferSymbolType: NameType: %v\n", sy.Name)
// }
if ast.HasChildren() {
astyp := ast.ChildAST(len(ast.Children) - 1)
if astyp.Name == "FieldTag" {
// ast.WriteTree(os.Stdout, 1)
astyp = ast.ChildAST(len(ast.Children) - 2)
}
vty, ok := gl.TypeFromAST(fs, pkg, nil, astyp)
if ok {
sy.Type = SymTypeNameForPkg(vty, pkg)
// if TraceTypes {
// fmt.Printf("InferSymbolType: NameType: %v type: %v from ast\n", sy.Name, sy.Type)
// }
} else {
sy.Type = TypeErr // actively mark as err so not re-processed
if TraceTypes {
fmt.Printf("InferSymbolType: NameType: %v NOT resolved from ast: %v\n", sy.Name, astyp.Path())
ast.WriteTree(os.Stdout, 1)
}
}
} else {
sy.Type = TypeErr
}
}
case sy.Kind == token.NameFunction:
ftyp := gl.FuncTypeFromAST(fs, pkg, ast, nil)
if ftyp != nil {
ftyp.Name = "func " + sy.Name
ftyp.Filename = sy.Filename
ftyp.Region = sy.Region
sy.Type = ftyp.Name
pkg.Types.Add(ftyp)
sy.Detail = "(" + ftyp.ArgString() + ") " + ftyp.ReturnString()
// if TraceTypes {
// fmt.Printf("InferSymbolType: added function type: %v %v\n", ftyp.Name, ftyp.String())
// }
}
}
}
if !funInternal && sy.Kind.SubCat() == token.NameFunction {
sy.Children = nil // nuke!
} else {
for _, ss := range sy.Children {
if sy != ss {
if false && TraceTypes {
fmt.Printf("InferSymbolType: processing child: %v\n", ss)
}
gl.InferSymbolType(ss, fs, pkg, funInternal)
}
}
}
}
// InferForRangeSymbolType infers the type of a ForRange expr
// gets the container type properly
func (gl *GoLang) InferForRangeSymbolType(sy *syms.Symbol, fs *parse.FileState, pkg *syms.Symbol) {
ast := sy.AST.(*parser.AST)
if ast.NumChildren() < 2 {
sy.Type = TypeErr // actively mark as err so not re-processed
if TraceTypes {
fmt.Printf("InferSymbolType: ForRange NameVar: %v does not have expected 2+ children\n", sy.Name)
ast.WriteTree(os.Stdout, 0)
}
return
}
// vars are in first child, type is in second child, rest of code is on last node
astyp := ast.ChildAST(1)
vty, ok := gl.TypeFromAST(fs, pkg, nil, astyp)
if !ok {
sy.Type = TypeErr // actively mark as err so not re-processed
if TraceTypes {
fmt.Printf("InferSymbolType: NameVar: %v NOT resolved from ForRange ast: %v\n", sy.Name, astyp.Path())
astyp.WriteTree(os.Stdout, 0)
}
return
}
varidx := 1 // which variable are we: first or second?
vast := ast.ChildAST(0)
if vast.NumChildren() <= 1 {
varidx = 0
} else if vast.ChildAST(0).Src == sy.Name {
varidx = 0
}
// vty is the container -- first el should be the type of element
switch vty.Kind {
case syms.Map: // need to know if we are the key or el
if len(vty.Els) > 1 {
tn := vty.Els[varidx].Type
if IsQualifiedType(vty.Name) && !IsQualifiedType(tn) {
pnm, _ := SplitType(vty.Name)
sy.Type = QualifyType(pnm, tn)
} else {
sy.Type = tn
}
} else {
sy.Type = TypeErr
if TraceTypes {
fmt.Printf("InferSymbolType: %s has ForRange over Map on type without an el type: %v\n", sy.Name, vty.Name)
}
}
case syms.Array, syms.List:
if varidx == 0 {
sy.Type = "int"
} else if len(vty.Els) > 0 {
tn := vty.Els[0].Type
if IsQualifiedType(vty.Name) && !IsQualifiedType(tn) {
pnm, _ := SplitType(vty.Name)
sy.Type = QualifyType(pnm, tn)
} else {
sy.Type = tn
}
} else {
sy.Type = TypeErr
if TraceTypes {
fmt.Printf("InferSymbolType: %s has ForRange over Array, List on type without an el type: %v\n", sy.Name, vty.Name)
}
}
case syms.String:
if varidx == 0 {
sy.Type = "int"
} else {
sy.Type = "rune"
}
default:
sy.Type = TypeErr
if TraceTypes {
fmt.Printf("InferSymbolType: %s has ForRange over non-container type: %v kind: %v\n", sy.Name, vty.Name, vty.Kind)
}
}
}
// InferEmptySymbolType ensures that any empty symbol type is resolved during
// processing of other type information -- returns true if was able to resolve
func (gl *GoLang) InferEmptySymbolType(sym *syms.Symbol, fs *parse.FileState, pkg *syms.Symbol) bool {
if sym.Type == "" { // hasn't happened yet
// if TraceTypes {
// fmt.Printf("TExpr: trying to infer type\n")
// }
sym.Type = TypeInProcess
gl.InferSymbolType(sym, fs, pkg, true)
}
if sym.Type == TypeInProcess {
if TraceTypes {
fmt.Printf("TExpr: source symbol is in process -- we have a loop: %v kind: %v\n", sym.Name, sym.Kind)
}
sym.Type = TypeErr
return false
}
if sym.Type == TypeErr {
if TraceTypes {
fmt.Printf("TExpr: source symbol has type err: %v kind: %v\n", sym.Name, sym.Kind)
}
return false
}
if sym.Type == "" { // shouldn't happen
sym.Type = TypeErr
if TraceTypes {
fmt.Printf("TExpr: source symbol has type err (but wasn't marked): %v kind: %v\n", sym.Name, sym.Kind)
}
return false
}
return true
}
func SymTypeNameForPkg(ty *syms.Type, pkg *syms.Symbol) string {
sc, has := ty.Scopes[token.NamePackage]
if has && sc != pkg.Name {
return QualifyType(sc, ty.Name)
}
return ty.Name
}
// 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 golang
import (
"cogentcore.org/core/parse/syms"
"cogentcore.org/core/parse/token"
)
// FuncParams returns the parameters of given function / method symbol,
// in proper order, name type for each param space separated in string
func (gl *GoLang) FuncParams(fsym *syms.Symbol) []string {
var ps []string
for _, cs := range fsym.Children {
if cs.Kind != token.NameVarParam {
continue
}
if len(ps) <= cs.Index {
op := ps
ps = make([]string, cs.Index+1)
copy(ps, op)
}
s := cs.Name + " " + cs.Type
ps[cs.Index] = s
}
return ps
}
// Copyright (c) 2020, 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 golang
import (
"fmt"
"strings"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/parser"
"cogentcore.org/core/parse/syms"
"cogentcore.org/core/parse/token"
)
var TraceTypes = false
// IsQualifiedType returns true if type is qualified by a package prefix
// is sensitive to [] or map[ prefix so it does NOT report as a qualified type in that
// case -- it is a compound local type defined in terms of a qualified type.
func IsQualifiedType(tnm string) bool {
if strings.HasPrefix(tnm, "[]") || strings.HasPrefix(tnm, "map[") {
return false
}
return strings.Index(tnm, ".") > 0
}
// QualifyType returns the type name tnm qualified by pkgnm if it is non-empty
// and only if tnm is not a basic type name
func QualifyType(pkgnm, tnm string) string {
if pkgnm == "" || IsQualifiedType(tnm) {
return tnm
}
if _, btyp := BuiltinTypes[tnm]; btyp {
return tnm
}
return pkgnm + "." + tnm
}
// SplitType returns the package and type names from a potentially qualified
// type name -- if it is not qualified, package name is empty.
// is sensitive to [] prefix so it does NOT split in that case
func SplitType(nm string) (pkgnm, tnm string) {
if !IsQualifiedType(nm) {
return "", nm
}
sci := strings.Index(nm, ".")
return nm[:sci], nm[sci+1:]
}
// PrefixType returns the type name prefixed with given prefix -- keeps any
// package name as the outer scope.
func PrefixType(pfx, nm string) string {
pkgnm, tnm := SplitType(nm)
return QualifyType(pkgnm, pfx+tnm)
}
// FindTypeName finds given type name in pkg and in broader context
// returns new package symbol if type name is in a different package
// else returns pkg arg.
func (gl *GoLang) FindTypeName(tynm string, fs *parse.FileState, pkg *syms.Symbol) (*syms.Type, *syms.Symbol) {
if tynm == "" {
return nil, nil
}
if tynm[0] == '*' {
tynm = tynm[1:]
}
pnm, tnm := SplitType(tynm)
if pnm == "" {
if btyp, ok := BuiltinTypes[tnm]; ok {
return btyp, pkg
}
if gtyp, ok := pkg.Types[tnm]; ok {
return gtyp, pkg
}
// if TraceTypes {
// fmt.Printf("FindTypeName: unqualified type name: %v not found in package: %v\n", tnm, pkg.Name)
// }
return nil, pkg
}
if npkg, ok := gl.PkgSyms(fs, pkg.Children, pnm); ok {
if gtyp, ok := npkg.Types[tnm]; ok {
return gtyp, npkg
}
if TraceTypes {
fmt.Printf("FindTypeName: type name: %v not found in package: %v\n", tnm, pnm)
}
} else {
if TraceTypes {
fmt.Printf("FindTypeName: could not find package: %v\n", pnm)
}
}
if TraceTypes {
fmt.Printf("FindTypeName: type name: %v not found in package: %v\n", tynm, pkg.Name)
}
return nil, pkg
}
// ResolveTypes initializes all user-defined types from AST data
// and then resolves types of symbols. The pkg must be a single
// package symbol i.e., the children there are all the elements of the
// package and the types are all the global types within the package.
// funInternal determines whether to include function-internal symbols
// (e.g., variables within function scope -- only for local files).
func (gl *GoLang) ResolveTypes(fs *parse.FileState, pkg *syms.Symbol, funInternal bool) {
fs.SymsMu.Lock()
gl.TypesFromAST(fs, pkg)
gl.InferSymbolType(pkg, fs, pkg, funInternal)
fs.SymsMu.Unlock()
}
// TypesFromAST initializes the types from their AST parse
func (gl *GoLang) TypesFromAST(fs *parse.FileState, pkg *syms.Symbol) {
InstallBuiltinTypes()
for _, ty := range pkg.Types {
gl.InitTypeFromAST(fs, pkg, ty)
}
}
// InitTypeFromAST initializes given type from ast
func (gl *GoLang) InitTypeFromAST(fs *parse.FileState, pkg *syms.Symbol, ty *syms.Type) {
if ty.AST == nil || len(ty.AST.AsTree().Children) < 2 {
// if TraceTypes {
// fmt.Printf("TypesFromAST: Type has nil AST! %v\n", ty.String())
// }
return
}
tyast := ty.AST.(*parser.AST).ChildAST(1)
if tyast == nil {
if TraceTypes {
fmt.Printf("TypesFromAST: Type has invalid AST! %v missing child 1\n", ty.String())
}
return
}
if ty.Name == "" {
if TraceTypes {
fmt.Printf("TypesFromAST: Type has no name! %v\n", ty.String())
}
return
}
if ty.Initialized {
// if TraceTypes {
// fmt.Printf("Type: %v already initialized\n", ty.Name)
// }
return
}
gl.TypeFromAST(fs, pkg, ty, tyast)
gl.TypeMeths(fs, pkg, ty) // all top-level named types might have methods
ty.Initialized = true
}
// SubTypeFromAST returns a subtype from child ast at given index, nil if failed
func (gl *GoLang) SubTypeFromAST(fs *parse.FileState, pkg *syms.Symbol, ast *parser.AST, idx int) (*syms.Type, bool) {
sast := ast.ChildAST(idx)
if sast == nil {
if TraceTypes {
fmt.Printf("TraceTypes: could not find child %d on ast %v", idx, ast)
}
return nil, false
}
return gl.TypeFromAST(fs, pkg, nil, sast)
}
// TypeToKindMap maps AST type names to syms.Kind basic categories for how we
// treat them for subsequent processing. Basically: Primitive or Composite
var TypeToKindMap = map[string]syms.Kinds{
"BasicType": syms.Primitive,
"TypeNm": syms.Primitive,
"QualType": syms.Primitive,
"PointerType": syms.Primitive,
"MapType": syms.Composite,
"SliceType": syms.Composite,
"ArrayType": syms.Composite,
"StructType": syms.Composite,
"InterfaceType": syms.Composite,
"FuncType": syms.Composite,
"StringDbl": syms.KindsN, // note: Lit is removed by ASTTypeName
"StringTicks": syms.KindsN,
"Rune": syms.KindsN,
"NumInteger": syms.KindsN,
"NumFloat": syms.KindsN,
"NumImag": syms.KindsN,
}
// ASTTypeName returns the effective type name from ast node
// dropping the "Lit" for example.
func (gl *GoLang) ASTTypeName(tyast *parser.AST) string {
tnm := tyast.Name
if strings.HasPrefix(tnm, "Lit") {
tnm = tnm[3:]
}
return tnm
}
// TypeFromAST returns type from AST parse -- returns true if successful.
// This is used both for initialization of global types via TypesFromAST
// and also for online type processing in the course of tracking down
// other types while crawling the AST. In the former case, ty is non-nil
// and the goal is to fill out the type information -- the ty will definitely
// have a name already. In the latter case, the ty will be nil, but the
// tyast node may have a Src name that will first be looked up to determine
// if a previously processed type is already available. The tyast.Name is
// the parser categorization of the type (BasicType, StructType, etc).
func (gl *GoLang) TypeFromAST(fs *parse.FileState, pkg *syms.Symbol, ty *syms.Type, tyast *parser.AST) (*syms.Type, bool) {
tnm := gl.ASTTypeName(tyast)
bkind, ok := TypeToKindMap[tnm]
if !ok { // must be some kind of expression
sty, _, got := gl.TypeFromASTExprStart(fs, pkg, pkg, tyast)
return sty, got
}
switch bkind {
case syms.Primitive:
return gl.TypeFromASTPrim(fs, pkg, ty, tyast)
case syms.Composite:
return gl.TypeFromASTComp(fs, pkg, ty, tyast)
case syms.KindsN:
return gl.TypeFromASTLit(fs, pkg, ty, tyast)
}
return nil, false
}
// TypeFromASTPrim handles primitive (non composite) type processing
func (gl *GoLang) TypeFromASTPrim(fs *parse.FileState, pkg *syms.Symbol, ty *syms.Type, tyast *parser.AST) (*syms.Type, bool) {
tnm := gl.ASTTypeName(tyast)
src := tyast.Src
etyp, tpkg := gl.FindTypeName(src, fs, pkg)
if etyp != nil {
if ty == nil { // if we can find an existing type, and not filling in global, use it
if tpkg != pkg {
pkgnm := tpkg.Name
qtnm := QualifyType(pkgnm, etyp.Name)
if qtnm != etyp.Name {
if letyp, ok := pkg.Types[qtnm]; ok {
etyp = letyp
} else {
ntyp := &syms.Type{}
*ntyp = *etyp
ntyp.Name = qtnm
pkg.Types.Add(ntyp)
etyp = ntyp
}
}
}
return etyp, true
}
} else {
if TraceTypes && src != "" {
fmt.Printf("TypeFromAST: primitive type name: %v not found\n", src)
}
}
switch tnm {
case "BasicType":
if etyp != nil {
ty.Kind = etyp.Kind
ty.Els.Add("par", etyp.Name) // parent type
return ty, true
} else {
return nil, false
}
case "TypeNm", "QualType":
if etyp != nil && etyp != ty {
ty.Kind = etyp.Kind
if ty.Name != etyp.Name {
ty.Els.Add("par", etyp.Name) // parent type
if TraceTypes {
fmt.Printf("TypeFromAST: TypeNm %v defined from parent type: %v\n", ty.Name, etyp.Name)
}
}
return ty, true
} else {
return nil, false
}
case "PointerType":
if ty == nil {
ty = &syms.Type{}
}
ty.Kind = syms.Ptr
if sty, ok := gl.SubTypeFromAST(fs, pkg, tyast, 0); ok {
ty.Els.Add("ptr", sty.Name)
if ty.Name == "" {
ty.Name = "*" + sty.Name
pkg.Types.Add(ty) // add pointers so we don't have to keep redefining
if TraceTypes {
fmt.Printf("TypeFromAST: Adding PointerType: %v\n", ty.String())
}
}
return ty, true
} else {
return nil, false
}
}
return nil, false
}
// TypeFromASTComp handles composite type processing
func (gl *GoLang) TypeFromASTComp(fs *parse.FileState, pkg *syms.Symbol, ty *syms.Type, tyast *parser.AST) (*syms.Type, bool) {
tnm := gl.ASTTypeName(tyast)
newTy := false
if ty == nil {
newTy = true
ty = &syms.Type{}
}
switch tnm {
case "MapType":
ty.Kind = syms.Map
keyty, kok := gl.SubTypeFromAST(fs, pkg, tyast, 0)
valty, vok := gl.SubTypeFromAST(fs, pkg, tyast, 1)
if kok && vok {
ty.Els.Add("key", SymTypeNameForPkg(keyty, pkg))
ty.Els.Add("val", SymTypeNameForPkg(valty, pkg))
if newTy {
ty.Name = "map[" + keyty.Name + "]" + valty.Name
}
} else {
return nil, false
}
case "SliceType":
ty.Kind = syms.List
valty, ok := gl.SubTypeFromAST(fs, pkg, tyast, 0)
if ok {
ty.Els.Add("val", SymTypeNameForPkg(valty, pkg))
if newTy {
ty.Name = "[]" + valty.Name
}
} else {
return nil, false
}
case "ArrayType":
ty.Kind = syms.Array
valty, ok := gl.SubTypeFromAST(fs, pkg, tyast, 1)
if ok {
ty.Els.Add("val", SymTypeNameForPkg(valty, pkg))
if newTy {
ty.Name = "[]" + valty.Name // todo: get size from child0, set to Size
}
} else {
return nil, false
}
case "StructType":
ty.Kind = syms.Struct
nfld := len(tyast.Children)
if nfld == 0 {
return BuiltinTypes["struct{}"], true
}
ty.Size = []int{nfld}
for i := 0; i < nfld; i++ {
fld := tyast.Children[i].(*parser.AST)
fsrc := fld.Src
switch fld.Name {
case "NamedField":
if len(fld.Children) <= 1 { // anonymous, non-qualified
ty.Els.Add(fsrc, fsrc)
gl.StructInheritEls(fs, pkg, ty, fsrc)
continue
}
fldty, ok := gl.SubTypeFromAST(fs, pkg, fld, 1)
if ok {
nms := gl.NamesFromAST(fs, pkg, fld, 0)
for _, nm := range nms {
ty.Els.Add(nm, SymTypeNameForPkg(fldty, pkg))
}
}
case "AnonQualField":
ty.Els.Add(fsrc, fsrc) // anon two are same
gl.StructInheritEls(fs, pkg, ty, fsrc)
}
}
if newTy {
ty.Name = fs.NextAnonName(pkg.Name + "_struct")
}
// if TraceTypes {
// fmt.Printf("TypeFromAST: New struct type defined: %v\n", ty.Name)
// ty.WriteDoc(os.Stdout, 0)
// }
case "InterfaceType":
ty.Kind = syms.Interface
nmth := len(tyast.Children)
if nmth == 0 {
return BuiltinTypes["interface{}"], true
}
ty.Size = []int{nmth}
for i := 0; i < nmth; i++ {
fld := tyast.Children[i].(*parser.AST)
fsrc := fld.Src
switch fld.Name {
case "MethSpecAnonLocal":
fallthrough
case "MethSpecAnonQual":
ty.Els.Add(fsrc, fsrc) // anon two are same
case "MethSpecName":
if nm := fld.ChildAST(0); nm != nil {
mty := syms.NewType(ty.Name+":"+nm.Src, syms.Method)
pkg.Types.Add(mty) // add interface methods as new types..
gl.FuncTypeFromAST(fs, pkg, fld, mty) // todo: this is not working -- debug
ty.Els.Add(nm.Src, mty.Name)
}
}
}
if newTy {
if nmth == 0 {
ty.Name = "interface{}"
} else {
ty.Name = fs.NextAnonName(pkg.Name + "_interface")
}
}
case "FuncType":
ty.Kind = syms.Func
gl.FuncTypeFromAST(fs, pkg, tyast, ty)
if newTy {
if len(ty.Els) == 0 {
ty.Name = "func()"
} else {
ty.Name = fs.NextAnonName(pkg.Name + "_func")
}
}
}
if newTy {
etyp, has := pkg.Types[ty.Name]
if has {
return etyp, true
}
pkg.Types.Add(ty) // add anon composite types
// if TraceTypes {
// fmt.Printf("TypeFromASTComp: Created new anon composite type: %v %s\n", ty.Name, ty.String())
// }
}
return ty, true // fallthrough is true..
}
// TypeFromASTLit gets type from literals
func (gl *GoLang) TypeFromASTLit(fs *parse.FileState, pkg *syms.Symbol, ty *syms.Type, tyast *parser.AST) (*syms.Type, bool) {
tnm := tyast.Name
var bty *syms.Type
switch tnm {
case "LitStringDbl":
bty = BuiltinTypes["string"]
case "LitStringTicks":
bty = BuiltinTypes["string"]
case "LitRune":
bty = BuiltinTypes["rune"]
case "LitNumInteger":
bty = BuiltinTypes["int"]
case "LitNumFloat":
bty = BuiltinTypes["float64"]
case "LitNumImag":
bty = BuiltinTypes["complex128"]
}
if bty == nil {
return nil, false
}
if ty == nil {
return bty, true
}
ty.Kind = bty.Kind
ty.Els.Add("par", bty.Name) // parent type
return ty, true
}
// StructInheritEls inherits struct fields and meths from given embedded type.
// Ensures that copied values are properly qualified if from another package.
func (gl *GoLang) StructInheritEls(fs *parse.FileState, pkg *syms.Symbol, ty *syms.Type, etynm string) {
ety, _ := gl.FindTypeName(etynm, fs, pkg)
if ety == nil {
if TraceTypes {
fmt.Printf("Embedded struct type not found: %v for type: %v\n", etynm, ty.Name)
}
return
}
if !ety.Initialized {
// if TraceTypes {
// fmt.Printf("Embedded struct type not yet initialized, initializing: %v for type: %v\n", ety.Name, ty.Name)
// }
gl.InitTypeFromAST(fs, pkg, ety)
}
pkgnm := pkg.Name
diffPkg := false
epkg, has := ety.Scopes[token.NamePackage]
if has && epkg != pkgnm {
diffPkg = true
}
if diffPkg {
for i := range ety.Els {
nt := ety.Els[i].Clone()
tnm := nt.Type
_, isb := BuiltinTypes[tnm]
if !isb && !IsQualifiedType(tnm) {
tnm = QualifyType(epkg, tnm)
// fmt.Printf("Fixed type: %v to %v\n", ety.Els[i].Type, tnm)
}
nt.Type = tnm
ty.Els = append(ty.Els, *nt)
}
nmt := len(ety.Meths)
if nmt > 0 {
ty.Meths = make(syms.TypeMap, nmt)
for mn, mt := range ety.Meths {
nmt := mt.Clone()
for i := range nmt.Els {
t := &nmt.Els[i]
tnm := t.Type
_, isb := BuiltinTypes[tnm]
if !isb && !IsQualifiedType(tnm) {
tnm = QualifyType(epkg, tnm)
}
t.Type = tnm
}
ty.Meths[mn] = nmt
}
}
} else {
ty.Els.CopyFrom(ety.Els)
ty.Meths.CopyFrom(ety.Meths, false) // dest is newer
}
ty.Size[0] += len(ety.Els)
// if TraceTypes {
// fmt.Printf("Struct Type: %v inheriting from: %v\n", ty.Name, ety.Name)
// }
}
// 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 languages
import (
"fmt"
"cogentcore.org/core/base/fileinfo"
)
var ParserBytes map[fileinfo.Known][]byte = make(map[fileinfo.Known][]byte)
func OpenParser(sl fileinfo.Known) ([]byte, error) {
parserBytes, ok := ParserBytes[sl]
if !ok {
return nil, fmt.Errorf("langs.OpenParser: no parser bytes for %v", sl)
}
return parserBytes, nil
}
// Copyright (c) 2020, 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 markdown
import (
"log"
"strings"
"cogentcore.org/core/icons"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/complete"
"cogentcore.org/core/parse/languages/bibtex"
"cogentcore.org/core/parse/lexer"
)
// CompleteCite does completion on citation
func (ml *MarkdownLang) CompleteCite(fss *parse.FileStates, origStr, str string, pos lexer.Pos) (md complete.Matches) {
bfile, has := fss.MetaData("bibfile")
if !has {
return
}
bf, err := ml.Bibs.Open(bfile)
if err != nil {
return
}
md.Seed = str
for _, be := range bf.BibTex.Entries {
if strings.HasPrefix(be.CiteName, str) {
c := complete.Completion{Text: be.CiteName, Label: be.CiteName, Icon: icons.Field}
md.Matches = append(md.Matches, c)
}
}
return md
}
// LookupCite does lookup on citation
func (ml *MarkdownLang) LookupCite(fss *parse.FileStates, origStr, str string, pos lexer.Pos) (ld complete.Lookup) {
bfile, has := fss.MetaData("bibfile")
if !has {
return
}
bf, err := ml.Bibs.Open(bfile)
if err != nil {
return
}
lkbib := bibtex.NewBibTex()
for _, be := range bf.BibTex.Entries {
if strings.HasPrefix(be.CiteName, str) {
lkbib.Entries = append(lkbib.Entries, be)
}
}
if len(lkbib.Entries) > 0 {
ld.SetFile(fss.Filename, 0, 0)
ld.Text = []byte(lkbib.PrettyString())
}
return ld
}
// OpenBibfile attempts to find the bibliography file, and load it.
// Sets meta data "bibfile" to resulting file if found, and deletes it if not.
func (ml *MarkdownLang) OpenBibfile(fss *parse.FileStates, pfs *parse.FileState) error {
bfile := ml.FindBibliography(pfs)
if bfile == "" {
fss.DeleteMetaData("bibfile")
return nil
}
_, err := ml.Bibs.Open(bfile)
if err != nil {
log.Println(err)
fss.DeleteMetaData("bibfile")
return err
}
fss.SetMetaData("bibfile", bfile)
return nil
}
// FindBibliography looks for yaml metadata at top of markdown file
func (ml *MarkdownLang) FindBibliography(pfs *parse.FileState) string {
nlines := pfs.Src.NLines()
if nlines < 3 {
return ""
}
fln := string(pfs.Src.Lines[0])
if fln != "---" {
return ""
}
trg := `bibfile: `
trgln := len(trg)
mx := min(nlines, 100)
for i := 1; i < mx; i++ {
sln := pfs.Src.Lines[i]
lstr := string(sln)
if lstr == "---" {
return ""
}
lnln := len(sln)
if lnln < trgln {
continue
}
if strings.HasPrefix(lstr, trg) {
fnm := lstr[trgln:lnln]
if !strings.HasSuffix(fnm, ".bib") {
fnm += ".bib"
}
return fnm
}
}
return ""
}
// 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 markdown
import (
_ "embed"
"strings"
"unicode"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/indent"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/complete"
"cogentcore.org/core/parse/languages"
"cogentcore.org/core/parse/languages/bibtex"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/syms"
"cogentcore.org/core/parse/token"
)
//go:embed markdown.parse
var parserBytes []byte
// MarkdownLang implements the Lang interface for the Markdown language
type MarkdownLang struct {
Pr *parse.Parser
// bibliography files that have been loaded, keyed by file path from bibfile metadata stored in filestate
Bibs bibtex.Files
}
// TheMarkdownLang is the instance variable providing support for the Markdown language
var TheMarkdownLang = MarkdownLang{}
func init() {
parse.StandardLanguageProperties[fileinfo.Markdown].Lang = &TheMarkdownLang
languages.ParserBytes[fileinfo.Markdown] = parserBytes
}
func (ml *MarkdownLang) Parser() *parse.Parser {
if ml.Pr != nil {
return ml.Pr
}
lp, _ := parse.LanguageSupport.Properties(fileinfo.Markdown)
if lp.Parser == nil {
parse.LanguageSupport.OpenStandard()
}
ml.Pr = lp.Parser
if ml.Pr == nil {
return nil
}
return ml.Pr
}
func (ml *MarkdownLang) ParseFile(fss *parse.FileStates, txt []byte) {
pr := ml.Parser()
if pr == nil {
return
}
pfs := fss.StartProc(txt) // current processing one
pr.LexAll(pfs)
ml.OpenBibfile(fss, pfs)
fss.EndProc() // now done
// no parser
}
func (ml *MarkdownLang) LexLine(fs *parse.FileState, line int, txt []rune) lexer.Line {
pr := ml.Parser()
if pr == nil {
return nil
}
return pr.LexLine(fs, line, txt)
}
func (ml *MarkdownLang) ParseLine(fs *parse.FileState, line int) *parse.FileState {
// n/a
return nil
}
func (ml *MarkdownLang) HighlightLine(fss *parse.FileStates, line int, txt []rune) lexer.Line {
fs := fss.Done()
return ml.LexLine(fs, line, txt)
}
func (ml *MarkdownLang) CompleteLine(fss *parse.FileStates, str string, pos lexer.Pos) (md complete.Matches) {
origStr := str
lfld := lexer.LastField(str)
str = lexer.InnerBracketScope(lfld, "[", "]")
if len(str) > 1 {
if str[0] == '@' {
return ml.CompleteCite(fss, origStr, str[1:], pos)
}
}
// n/a
return md
}
// Lookup is the main api called by completion code in giv/complete.go to lookup item
func (ml *MarkdownLang) Lookup(fss *parse.FileStates, str string, pos lexer.Pos) (ld complete.Lookup) {
origStr := str
lfld := lexer.LastField(str)
str = lexer.InnerBracketScope(lfld, "[", "]")
if len(str) > 1 {
if str[0] == '@' {
return ml.LookupCite(fss, origStr, str[1:], pos)
}
}
return
}
func (ml *MarkdownLang) CompleteEdit(fs *parse.FileStates, text string, cp int, comp complete.Completion, seed string) (ed complete.Edit) {
// if the original is ChildByName() and the cursor is between d and B and the comp is Children,
// then delete the portion after "Child" and return the new comp and the number or runes past
// the cursor to delete
s2 := text[cp:]
// gotParen := false
if len(s2) > 0 && lexer.IsLetterOrDigit(rune(s2[0])) {
for i, c := range s2 {
if c == '{' {
// gotParen = true
s2 = s2[:i]
break
}
isalnum := c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c)
if !isalnum {
s2 = s2[:i]
break
}
}
} else {
s2 = ""
}
var nw = comp.Text
// if gotParen && strings.HasSuffix(nw, "()") {
// nw = nw[:len(nw)-2]
// }
// fmt.Printf("text: %v|%v comp: %v s2: %v\n", text[:cp], text[cp:], nw, s2)
ed.NewText = nw
ed.ForwardDelete = len(s2)
return ed
}
func (ml *MarkdownLang) ParseDir(fs *parse.FileState, path string, opts parse.LanguageDirOptions) *syms.Symbol {
// n/a
return nil
}
// List keywords (for indent)
var ListKeys = map[string]struct{}{"*": {}, "+": {}, "-": {}}
// IndentLine returns the indentation level for given line based on
// previous line's indentation level, and any delta change based on
// e.g., brackets starting or ending the previous or current line, or
// other language-specific keywords. See lexer.BracketIndentLine for example.
// Indent level is in increments of tabSz for spaces, and tabs for tabs.
// Operates on rune source with markup lex tags per line.
func (ml *MarkdownLang) IndentLine(fs *parse.FileStates, src [][]rune, tags []lexer.Line, ln int, tabSz int) (pInd, delInd, pLn int, ichr indent.Character) {
pInd, pLn, ichr = lexer.PrevLineIndent(src, tags, ln, tabSz)
delInd = 0
ptg := tags[pLn]
ctg := tags[ln]
if len(ptg) == 0 || len(ctg) == 0 {
return
}
fpt := ptg[0]
fct := ctg[0]
if fpt.Token.Token != token.Keyword || fct.Token.Token != token.Keyword {
return
}
pk := strings.TrimSpace(string(fpt.Src(src[pLn])))
ck := strings.TrimSpace(string(fct.Src(src[ln])))
// fmt.Printf("pk: %v ck: %v\n", string(pk), string(ck))
if len(pk) >= 1 && len(ck) >= 1 {
_, pky := ListKeys[pk]
_, cky := ListKeys[ck]
if unicode.IsDigit(rune(pk[0])) {
pk = "1"
pky = true
}
if unicode.IsDigit(rune(ck[0])) {
ck = "1"
cky = true
}
if pky && cky {
if pk != ck {
delInd = 1
return
}
return
}
}
return
}
// AutoBracket returns what to do when a user types a starting bracket character
// (bracket, brace, paren) while typing.
// pos = position where bra will be inserted, and curLn is the current line
// match = insert the matching ket, and newLine = insert a new line.
func (ml *MarkdownLang) AutoBracket(fs *parse.FileStates, bra rune, pos lexer.Pos, curLn []rune) (match, newLine bool) {
lnLen := len(curLn)
match = pos.Ch == lnLen || unicode.IsSpace(curLn[pos.Ch]) // at end or if space after
newLine = false
return
}
// 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 tex
import (
"log"
"strings"
"cogentcore.org/core/icons"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/complete"
"cogentcore.org/core/parse/languages/bibtex"
"cogentcore.org/core/parse/lexer"
)
// CompleteCite does completion on citation
func (tl *TexLang) CompleteCite(fss *parse.FileStates, origStr, str string, pos lexer.Pos) (md complete.Matches) {
bfile, has := fss.MetaData("bibfile")
if !has {
return
}
bf, err := tl.Bibs.Open(bfile)
if err != nil {
return
}
md.Seed = str
for _, be := range bf.BibTex.Entries {
if strings.HasPrefix(be.CiteName, str) {
c := complete.Completion{Text: be.CiteName, Label: be.CiteName, Icon: icons.Field}
md.Matches = append(md.Matches, c)
}
}
return md
}
// LookupCite does lookup on citation
func (tl *TexLang) LookupCite(fss *parse.FileStates, origStr, str string, pos lexer.Pos) (ld complete.Lookup) {
bfile, has := fss.MetaData("bibfile")
if !has {
return
}
bf, err := tl.Bibs.Open(bfile)
if err != nil {
return
}
lkbib := bibtex.NewBibTex()
for _, be := range bf.BibTex.Entries {
if strings.HasPrefix(be.CiteName, str) {
lkbib.Entries = append(lkbib.Entries, be)
}
}
if len(lkbib.Entries) > 0 {
ld.SetFile(fss.Filename, 0, 0)
ld.Text = []byte(lkbib.PrettyString())
}
return ld
}
// OpenBibfile attempts to open the /bibliography file.
// Sets meta data "bibfile" to resulting file if found, and deletes it if not.
func (tl *TexLang) OpenBibfile(fss *parse.FileStates, pfs *parse.FileState) error {
bfile := tl.FindBibliography(pfs)
if bfile == "" {
fss.DeleteMetaData("bibfile")
return nil
}
_, err := tl.Bibs.Open(bfile)
if err != nil {
log.Println(err)
fss.DeleteMetaData("bibfile")
return err
}
fss.SetMetaData("bibfile", bfile)
return nil
}
func (tl *TexLang) FindBibliography(pfs *parse.FileState) string {
nlines := pfs.Src.NLines()
trg := `\bibliography{`
trgln := len(trg)
for i := nlines - 1; i >= 0; i-- {
sln := pfs.Src.Lines[i]
lnln := len(sln)
if lnln == 0 {
continue
}
if sln[0] != '\\' {
continue
}
if lnln > 100 {
continue
}
lstr := string(sln)
if strings.HasPrefix(lstr, trg) {
return lstr[trgln:len(sln)-1] + ".bib"
}
}
return ""
}
// 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 tex
import (
"strings"
"unicode"
"cogentcore.org/core/icons"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/complete"
"cogentcore.org/core/parse/lexer"
)
func (tl *TexLang) CompleteLine(fss *parse.FileStates, str string, pos lexer.Pos) (md complete.Matches) {
origStr := str
lfld := lexer.LastField(str)
str = lexer.LastScopedString(str)
if len(lfld) < 2 {
return md
}
if lfld[0] == '\\' && lfld[1:] == str { // use the /
str = lfld
}
if HasCite(lfld) {
return tl.CompleteCite(fss, origStr, str, pos)
}
md.Seed = str
if len(LaTeXCommandsAll) == 0 {
LaTeXCommandsAll = append(LaTeXCommands, CiteCommands...)
}
for _, ls := range LaTeXCommandsAll {
if strings.HasPrefix(ls, str) {
c := complete.Completion{Text: ls, Label: ls, Icon: icons.Function}
md.Matches = append(md.Matches, c)
}
}
return md
}
// Lookup is the main api called by completion code in giv/complete.go to lookup item
func (tl *TexLang) Lookup(fss *parse.FileStates, str string, pos lexer.Pos) (ld complete.Lookup) {
origStr := str
lfld := lexer.LastField(str)
str = lexer.LastScopedString(str)
if HasCite(lfld) {
return tl.LookupCite(fss, origStr, str, pos)
}
return
}
func (tl *TexLang) CompleteEdit(fss *parse.FileStates, text string, cp int, comp complete.Completion, seed string) (ed complete.Edit) {
// if the original is ChildByName() and the cursor is between d and B and the comp is Children,
// then delete the portion after "Child" and return the new comp and the number or runes past
// the cursor to delete
s2 := text[cp:]
// gotParen := false
if len(s2) > 0 && lexer.IsLetterOrDigit(rune(s2[0])) {
for i, c := range s2 {
if c == '{' {
// gotParen = true
s2 = s2[:i]
break
}
isalnum := c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c)
if !isalnum {
s2 = s2[:i]
break
}
}
} else {
s2 = ""
}
var nw = comp.Text
// if gotParen && strings.HasSuffix(nw, "()") {
// nw = nw[:len(nw)-2]
// }
// fmt.Printf("text: %v|%v comp: %v s2: %v\n", text[:cp], text[cp:], nw, s2)
ed.NewText = nw
ed.ForwardDelete = len(s2)
return ed
}
// CiteCommands is a list of latex citation commands (APA style requires many variations).
// We include all the variations so they show up in completion.
var CiteCommands = []string{`\cite`, `\citep`, `\citet`, `\citeNP`, `\citeyearpar`, `\citeyear`, `\citeauthor`, `\citeA`, `\citealp`, `\citeyearNP`, `\parencite`, `\textcite`, `\nptextcite`, `\incite`, `\nopcite`, `\yrcite`, `\yrnopcite`, `\abbrevcite`, `\abbrevincite`}
// HasCite returns true if string has Prefix in CiteCmds
func HasCite(str string) bool {
for _, cc := range CiteCommands {
if strings.HasPrefix(str, cc) {
return true
}
}
return false
}
// LaTeXCommandsAll concatenates LaTeXCmds and CiteCmds
var LaTeXCommandsAll []string
// LaTeXCommands is a big list of standard commands
var LaTeXCommands = []string{
`\em`,
`\emph`,
`\textbf`,
`\textit`,
`\texttt`,
`\textsf`,
`\textrm`,
`\tiny`,
`\scriptsize`,
`\footnotesize`,
`\small`,
`\normalsize`,
`\large`,
`\Large`,
`\LARGE`,
`\huge`,
`\Huge`,
`\begin`,
`\end`,
`enumerate`,
`itemize`,
`description`,
`\item`,
`figure`,
`table`,
`tabular`,
`array`,
`\hline`,
`\cline`,
`\multicolumn`,
`equation`,
`center`,
`\centering`,
`\verb`,
`verbatim`,
`quote`,
`\section`,
`\subsection`,
`\subsubsection`,
`\paragraph`,
}
// 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 tex
import (
_ "embed"
"strings"
"unicode"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/indent"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/languages"
"cogentcore.org/core/parse/languages/bibtex"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/syms"
)
//go:embed tex.parse
var parserBytes []byte
// TexLang implements the Lang interface for the Tex / LaTeX language
type TexLang struct {
Pr *parse.Parser
// bibliography files that have been loaded, keyed by file path from bibfile metadata stored in filestate
Bibs bibtex.Files
}
// TheTexLang is the instance variable providing support for the Go language
var TheTexLang = TexLang{}
func init() {
parse.StandardLanguageProperties[fileinfo.TeX].Lang = &TheTexLang
languages.ParserBytes[fileinfo.TeX] = parserBytes
}
func (tl *TexLang) Parser() *parse.Parser {
if tl.Pr != nil {
return tl.Pr
}
lp, _ := parse.LanguageSupport.Properties(fileinfo.TeX)
if lp.Parser == nil {
parse.LanguageSupport.OpenStandard()
}
tl.Pr = lp.Parser
if tl.Pr == nil {
return nil
}
return tl.Pr
}
func (tl *TexLang) ParseFile(fss *parse.FileStates, txt []byte) {
pr := tl.Parser()
if pr == nil {
return
}
pfs := fss.StartProc(txt) // current processing one
pr.LexAll(pfs)
tl.OpenBibfile(fss, pfs)
fss.EndProc() // now done
// no parser
}
func (tl *TexLang) LexLine(fs *parse.FileState, line int, txt []rune) lexer.Line {
pr := tl.Parser()
if pr == nil {
return nil
}
return pr.LexLine(fs, line, txt)
}
func (tl *TexLang) ParseLine(fs *parse.FileState, line int) *parse.FileState {
// n/a
return nil
}
func (tl *TexLang) HighlightLine(fss *parse.FileStates, line int, txt []rune) lexer.Line {
fs := fss.Done()
return tl.LexLine(fs, line, txt)
}
func (tl *TexLang) ParseDir(fs *parse.FileState, path string, opts parse.LanguageDirOptions) *syms.Symbol {
// n/a
return nil
}
// IndentLine returns the indentation level for given line based on
// previous line's indentation level, and any delta change based on
// e.g., brackets starting or ending the previous or current line, or
// other language-specific keywords. See lexer.BracketIndentLine for example.
// Indent level is in increments of tabSz for spaces, and tabs for tabs.
// Operates on rune source with markup lex tags per line.
func (tl *TexLang) IndentLine(fs *parse.FileStates, src [][]rune, tags []lexer.Line, ln int, tabSz int) (pInd, delInd, pLn int, ichr indent.Character) {
pInd, pLn, ichr = lexer.PrevLineIndent(src, tags, ln, tabSz)
curUnd, _ := lexer.LineStartEndBracket(src[ln], tags[ln])
_, prvInd := lexer.LineStartEndBracket(src[pLn], tags[pLn])
delInd = 0
switch {
case prvInd && curUnd:
delInd = 0 // offset
case prvInd:
delInd = 1 // indent
case curUnd:
delInd = -1 // undent
}
pst := lexer.FirstNonSpaceRune(src[pLn])
cst := lexer.FirstNonSpaceRune(src[ln])
pbeg := false
if pst >= 0 {
sts := string(src[pLn][pst:])
if strings.HasPrefix(sts, "\\begin{") {
pbeg = true
}
}
cend := false
if cst >= 0 {
sts := string(src[ln][cst:])
if strings.HasPrefix(sts, "\\end{") {
cend = true
}
}
switch {
case pbeg && cend:
delInd = 0
case pbeg:
delInd = 1
case cend:
delInd = -1
}
if pInd == 0 && delInd < 0 { // error..
delInd = 0
}
return
}
// AutoBracket returns what to do when a user types a starting bracket character
// (bracket, brace, paren) while typing.
// pos = position where bra will be inserted, and curLn is the current line
// match = insert the matching ket, and newLine = insert a new line.
func (tl *TexLang) AutoBracket(fs *parse.FileStates, bra rune, pos lexer.Pos, curLn []rune) (match, newLine bool) {
lnLen := len(curLn)
match = pos.Ch == lnLen || unicode.IsSpace(curLn[pos.Ch]) // at end or if space after
newLine = false
return
}
// 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 parse
import (
"fmt"
"time"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/parse/languages"
"cogentcore.org/core/parse/lexer"
)
// LanguageFlags are special properties of a given language
type LanguageFlags int32 //enums:enum
// LangFlags
const (
// NoFlags = nothing special
NoFlags LanguageFlags = iota
// IndentSpace means that spaces must be used for this language
IndentSpace
// IndentTab means that tabs must be used for this language
IndentTab
// ReAutoIndent causes current line to be re-indented during AutoIndent for Enter
// (newline) -- this should only be set for strongly indented languages where
// the previous + current line can tell you exactly what indent the current line
// should be at.
ReAutoIndent
)
// LanguageProperties contains properties of languages supported by the parser
// framework
type LanguageProperties struct {
// known language -- must be a supported one from Known list
Known fileinfo.Known
// character(s) that start a single-line comment -- if empty then multi-line comment syntax will be used
CommentLn string
// character(s) that start a multi-line comment or one that requires both start and end
CommentSt string
// character(s) that end a multi-line comment or one that requires both start and end
CommentEd string
// special properties for this language -- as an explicit list of options to make them easier to see and set in defaults
Flags []LanguageFlags
// Lang interface for this language
Lang Language `json:"-" xml:"-"`
// parser for this language -- initialized in OpenStandard
Parser *Parser `json:"-" xml:"-"`
}
// HasFlag returns true if given flag is set in Flags
func (lp *LanguageProperties) HasFlag(flg LanguageFlags) bool {
for _, f := range lp.Flags {
if f == flg {
return true
}
}
return false
}
// StandardLanguageProperties is the standard compiled-in set of language properties
var StandardLanguageProperties = map[fileinfo.Known]*LanguageProperties{
fileinfo.Ada: {fileinfo.Ada, "--", "", "", nil, nil, nil},
fileinfo.Bash: {fileinfo.Bash, "# ", "", "", nil, nil, nil},
fileinfo.Csh: {fileinfo.Csh, "# ", "", "", nil, nil, nil},
fileinfo.C: {fileinfo.C, "// ", "/* ", " */", nil, nil, nil},
fileinfo.CSharp: {fileinfo.CSharp, "// ", "/* ", " */", nil, nil, nil},
fileinfo.D: {fileinfo.D, "// ", "/* ", " */", nil, nil, nil},
fileinfo.ObjC: {fileinfo.ObjC, "// ", "/* ", " */", nil, nil, nil},
fileinfo.Go: {fileinfo.Go, "// ", "/* ", " */", []LanguageFlags{IndentTab}, nil, nil},
fileinfo.Java: {fileinfo.Java, "// ", "/* ", " */", nil, nil, nil},
fileinfo.JavaScript: {fileinfo.JavaScript, "// ", "/* ", " */", nil, nil, nil},
fileinfo.Eiffel: {fileinfo.Eiffel, "--", "", "", nil, nil, nil},
fileinfo.Haskell: {fileinfo.Haskell, "--", "{- ", "-}", nil, nil, nil},
fileinfo.Lisp: {fileinfo.Lisp, "; ", "", "", nil, nil, nil},
fileinfo.Lua: {fileinfo.Lua, "--", "---[[ ", "--]]", nil, nil, nil},
fileinfo.Makefile: {fileinfo.Makefile, "# ", "", "", []LanguageFlags{IndentTab}, nil, nil},
fileinfo.Matlab: {fileinfo.Matlab, "% ", "%{ ", " %}", nil, nil, nil},
fileinfo.OCaml: {fileinfo.OCaml, "", "(* ", " *)", nil, nil, nil},
fileinfo.Pascal: {fileinfo.Pascal, "// ", " ", " }", nil, nil, nil},
fileinfo.Perl: {fileinfo.Perl, "# ", "", "", nil, nil, nil},
fileinfo.Python: {fileinfo.Python, "# ", "", "", []LanguageFlags{IndentSpace}, nil, nil},
fileinfo.Php: {fileinfo.Php, "// ", "/* ", " */", nil, nil, nil},
fileinfo.R: {fileinfo.R, "# ", "", "", nil, nil, nil},
fileinfo.Ruby: {fileinfo.Ruby, "# ", "", "", nil, nil, nil},
fileinfo.Rust: {fileinfo.Rust, "// ", "/* ", " */", nil, nil, nil},
fileinfo.Scala: {fileinfo.Scala, "// ", "/* ", " */", nil, nil, nil},
fileinfo.Html: {fileinfo.Html, "", "<!-- ", " -->", nil, nil, nil},
fileinfo.TeX: {fileinfo.TeX, "% ", "", "", nil, nil, nil},
fileinfo.Markdown: {fileinfo.Markdown, "", "<!--- ", " -->", []LanguageFlags{IndentSpace}, nil, nil},
fileinfo.Yaml: {fileinfo.Yaml, "#", "", "", []LanguageFlags{IndentSpace}, nil, nil},
}
// LanguageSupporter provides general support for supported languages.
// e.g., looking up lexers and parsers by name.
// Also implements the lexer.LangLexer interface to provide access to other
// Guest Lexers
type LanguageSupporter struct{}
// LanguageSupport is the main language support hub for accessing parse
// support interfaces for each supported language
var LanguageSupport = LanguageSupporter{}
// OpenStandard opens all the standard parsers for languages, from the langs/ directory
func (ll *LanguageSupporter) OpenStandard() error {
lexer.TheLanguageLexer = &LanguageSupport
for sl, lp := range StandardLanguageProperties {
pib, err := languages.OpenParser(sl)
if err != nil {
continue
}
pr := NewParser()
err = pr.ReadJSON(pib)
if err != nil {
return errors.Log(err)
}
pr.ModTime = time.Date(2023, 02, 10, 00, 00, 00, 0, time.UTC)
pr.InitAll()
lp.Parser = pr
}
return nil
}
// Properties looks up language properties by fileinfo.Known const int type
func (ll *LanguageSupporter) Properties(sup fileinfo.Known) (*LanguageProperties, error) {
lp, has := StandardLanguageProperties[sup]
if !has {
err := fmt.Errorf("parse.LangSupport.Properties: no specific support for language: %v", sup)
return nil, err
}
return lp, nil
}
// PropertiesByName looks up language properties by string name of language
// (with case-insensitive fallback). Returns error if not supported.
func (ll *LanguageSupporter) PropertiesByName(lang string) (*LanguageProperties, error) {
sup, err := fileinfo.KnownByName(lang)
if err != nil {
// log.Println(err.Error()) // don't want output during lexing..
return nil, err
}
return ll.Properties(sup)
}
// LexerByName looks up Lexer for given language by name
// (with case-insensitive fallback). Returns nil if not supported.
func (ll *LanguageSupporter) LexerByName(lang string) *lexer.Rule {
lp, err := ll.PropertiesByName(lang)
if err != nil {
return nil
}
if lp.Parser == nil {
// log.Printf("core.LangSupport: no lexer / parser support for language: %v\n", lang)
return nil
}
return lp.Parser.Lexer
}
// Copyright (c) 2020, 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 lexer
import (
"cogentcore.org/core/parse/token"
)
// BracePair returns the matching brace-like punctuation for given rune,
// which must be a left or right brace {}, bracket [] or paren ().
// Also returns true if it is *right*
func BracePair(r rune) (match rune, right bool) {
right = false
switch r {
case '{':
match = '}'
case '}':
right = true
match = '{'
case '(':
match = ')'
case ')':
right = true
match = '('
case '[':
match = ']'
case ']':
right = true
match = '['
}
return
}
// BraceMatch finds the brace, bracket, or paren that is the partner
// of the one passed to function, within maxLns lines of start.
// Operates on rune source with markup lex tags per line (tags exclude comments).
func BraceMatch(src [][]rune, tags []Line, r rune, st Pos, maxLns int) (en Pos, found bool) {
en.Ln = -1
found = false
match, rt := BracePair(r)
var left int
var right int
if rt {
right++
} else {
left++
}
ch := st.Ch
ln := st.Ln
nln := len(src)
mx := min(nln-ln, maxLns)
mn := min(ln, maxLns)
txt := src[ln]
tln := tags[ln]
if left > right {
for l := ln + 1; l < ln+mx; l++ {
for i := ch + 1; i < len(txt); i++ {
if txt[i] == r {
lx, _ := tln.AtPos(i)
if lx == nil || lx.Token.Token.Cat() != token.Comment {
left++
continue
}
}
if txt[i] == match {
lx, _ := tln.AtPos(i)
if lx == nil || lx.Token.Token.Cat() != token.Comment {
right++
if left == right {
en.Ln = l - 1
en.Ch = i
break
}
}
}
}
if en.Ln >= 0 {
found = true
break
}
txt = src[l]
tln = tags[l]
ch = -1
}
} else {
for l := ln - 1; l >= ln-mn; l-- {
ch = min(ch, len(txt))
for i := ch - 1; i >= 0; i-- {
if txt[i] == r {
lx, _ := tln.AtPos(i)
if lx == nil || lx.Token.Token.Cat() != token.Comment {
right++
continue
}
}
if txt[i] == match {
lx, _ := tln.AtPos(i)
if lx == nil || lx.Token.Token.Cat() != token.Comment {
left++
if left == right {
en.Ln = l + 1
en.Ch = i
break
}
}
}
}
if en.Ln >= 0 {
found = true
break
}
txt = src[l]
tln = tags[l]
ch = len(txt)
}
}
return en, found
}
// Code generated by "core generate"; DO NOT EDIT.
package lexer
import (
"cogentcore.org/core/enums"
)
var _ActionsValues = []Actions{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// ActionsN is the highest valid value for type Actions, plus one.
const ActionsN Actions = 11
var _ActionsValueMap = map[string]Actions{`Next`: 0, `Name`: 1, `Number`: 2, `Quoted`: 3, `QuotedRaw`: 4, `EOL`: 5, `ReadUntil`: 6, `PushState`: 7, `PopState`: 8, `SetGuestLex`: 9, `PopGuestLex`: 10}
var _ActionsDescMap = map[Actions]string{0: `Next means advance input position to the next character(s) after the matched characters`, 1: `Name means read in an entire name, which is letters, _ and digits after first letter position will be advanced to just after`, 2: `Number means read in an entire number -- the token type will automatically be set to the actual type of number that was read in, and position advanced to just after`, 3: `Quoted means read in an entire string enclosed in quote delimeter that is present at current position, with proper skipping of escaped. Position advanced to just after`, 4: `QuotedRaw means read in an entire string enclosed in quote delimeter that is present at start position, with proper skipping of escaped. Position advanced to just after. Raw version supports multi-line and includes CR etc at end of lines (e.g., back-tick in various languages)`, 5: `EOL means read till the end of the line (e.g., for single-line comments)`, 6: `ReadUntil reads until string(s) in the Until field are found, or until the EOL if none are found`, 7: `PushState means push the given state value onto the state stack`, 8: `PopState means pop given state value off the state stack`, 9: `SetGuestLex means install the Name (must be a prior action) as the guest lexer -- it will take over lexing until PopGuestLex is called`, 10: `PopGuestLex removes the current guest lexer and returns to the original language lexer`}
var _ActionsMap = map[Actions]string{0: `Next`, 1: `Name`, 2: `Number`, 3: `Quoted`, 4: `QuotedRaw`, 5: `EOL`, 6: `ReadUntil`, 7: `PushState`, 8: `PopState`, 9: `SetGuestLex`, 10: `PopGuestLex`}
// String returns the string representation of this Actions value.
func (i Actions) String() string { return enums.String(i, _ActionsMap) }
// SetString sets the Actions value from its string representation,
// and returns an error if the string is invalid.
func (i *Actions) SetString(s string) error {
return enums.SetString(i, s, _ActionsValueMap, "Actions")
}
// Int64 returns the Actions value as an int64.
func (i Actions) Int64() int64 { return int64(i) }
// SetInt64 sets the Actions value from an int64.
func (i *Actions) SetInt64(in int64) { *i = Actions(in) }
// Desc returns the description of the Actions value.
func (i Actions) Desc() string { return enums.Desc(i, _ActionsDescMap) }
// ActionsValues returns all possible values for the type Actions.
func ActionsValues() []Actions { return _ActionsValues }
// Values returns all possible values for the type Actions.
func (i Actions) Values() []enums.Enum { return enums.Values(_ActionsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Actions) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Actions) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Actions") }
var _MatchesValues = []Matches{0, 1, 2, 3, 4, 5, 6}
// MatchesN is the highest valid value for type Matches, plus one.
const MatchesN Matches = 7
var _MatchesValueMap = map[string]Matches{`String`: 0, `StrName`: 1, `Letter`: 2, `Digit`: 3, `WhiteSpace`: 4, `CurState`: 5, `AnyRune`: 6}
var _MatchesDescMap = map[Matches]string{0: `String means match a specific string as given in the rule Note: this only looks for the string with no constraints on what happens after this string -- use StrName to match entire names`, 1: `StrName means match a specific string that is a complete alpha-numeric string (including underbar _) with some other char at the end must use this for all keyword matches to ensure that it isn't just the start of a longer name`, 2: `Match any letter, including underscore`, 3: `Match digit 0-9`, 4: `Match any white space (space, tab) -- input is already broken into lines`, 5: `CurState means match current state value set by a PushState action, using String value in rule all CurState cases must generally be first in list of rules so they can preempt other rules when the state is active`, 6: `AnyRune means match any rune -- use this as the last condition where other terminators come first!`}
var _MatchesMap = map[Matches]string{0: `String`, 1: `StrName`, 2: `Letter`, 3: `Digit`, 4: `WhiteSpace`, 5: `CurState`, 6: `AnyRune`}
// String returns the string representation of this Matches value.
func (i Matches) String() string { return enums.String(i, _MatchesMap) }
// SetString sets the Matches value from its string representation,
// and returns an error if the string is invalid.
func (i *Matches) SetString(s string) error {
return enums.SetString(i, s, _MatchesValueMap, "Matches")
}
// Int64 returns the Matches value as an int64.
func (i Matches) Int64() int64 { return int64(i) }
// SetInt64 sets the Matches value from an int64.
func (i *Matches) SetInt64(in int64) { *i = Matches(in) }
// Desc returns the description of the Matches value.
func (i Matches) Desc() string { return enums.Desc(i, _MatchesDescMap) }
// MatchesValues returns all possible values for the type Matches.
func MatchesValues() []Matches { return _MatchesValues }
// Values returns all possible values for the type Matches.
func (i Matches) Values() []enums.Enum { return enums.Values(_MatchesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Matches) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Matches) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Matches") }
var _MatchPosValues = []MatchPos{0, 1, 2, 3, 4, 5, 6}
// MatchPosN is the highest valid value for type MatchPos, plus one.
const MatchPosN MatchPos = 7
var _MatchPosValueMap = map[string]MatchPos{`AnyPos`: 0, `StartOfLine`: 1, `EndOfLine`: 2, `MiddleOfLine`: 3, `StartOfWord`: 4, `EndOfWord`: 5, `MiddleOfWord`: 6}
var _MatchPosDescMap = map[MatchPos]string{0: `AnyPos matches at any position`, 1: `StartOfLine matches at start of line`, 2: `EndOfLine matches at end of line`, 3: `MiddleOfLine matches not at the start or end`, 4: `StartOfWord matches at start of word`, 5: `EndOfWord matches at end of word`, 6: `MiddleOfWord matches not at the start or end`}
var _MatchPosMap = map[MatchPos]string{0: `AnyPos`, 1: `StartOfLine`, 2: `EndOfLine`, 3: `MiddleOfLine`, 4: `StartOfWord`, 5: `EndOfWord`, 6: `MiddleOfWord`}
// String returns the string representation of this MatchPos value.
func (i MatchPos) String() string { return enums.String(i, _MatchPosMap) }
// SetString sets the MatchPos value from its string representation,
// and returns an error if the string is invalid.
func (i *MatchPos) SetString(s string) error {
return enums.SetString(i, s, _MatchPosValueMap, "MatchPos")
}
// Int64 returns the MatchPos value as an int64.
func (i MatchPos) Int64() int64 { return int64(i) }
// SetInt64 sets the MatchPos value from an int64.
func (i *MatchPos) SetInt64(in int64) { *i = MatchPos(in) }
// Desc returns the description of the MatchPos value.
func (i MatchPos) Desc() string { return enums.Desc(i, _MatchPosDescMap) }
// MatchPosValues returns all possible values for the type MatchPos.
func MatchPosValues() []MatchPos { return _MatchPosValues }
// Values returns all possible values for the type MatchPos.
func (i MatchPos) Values() []enums.Enum { return enums.Values(_MatchPosValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i MatchPos) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *MatchPos) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "MatchPos") }
// 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.
// Much of this is directly copied from Go's go/scanner package:
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package lexer
import (
"fmt"
"io"
"path/filepath"
"reflect"
"sort"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/tree"
)
// In an ErrorList, an error is represented by an *Error.
// The position Pos, if valid, points to the beginning of
// the offending token, and the error condition is described
// by Msg.
type Error struct {
// position where the error occurred in the source
Pos Pos
// full filename with path
Filename string
// brief error message
Msg string
// line of source where error was
Src string
// lexer or parser rule that emitted the error
Rule tree.Node
}
// Error implements the error interface -- gives the minimal version of error string
func (e Error) Error() string {
if e.Filename != "" {
_, fn := filepath.Split(e.Filename)
return fn + ":" + e.Pos.String() + ": " + e.Msg
}
return e.Pos.String() + ": " + e.Msg
}
// Report provides customizable output options for viewing errors:
// - basepath if non-empty shows filename relative to that path.
// - showSrc shows the source line on a second line -- truncated to 30 chars around err
// - showRule prints the rule name
func (e Error) Report(basepath string, showSrc, showRule bool) string {
var err error
fnm := ""
if e.Filename != "" {
if basepath != "" {
fnm, err = filepath.Rel(basepath, e.Filename)
}
if basepath == "" || err != nil {
_, fnm = filepath.Split(e.Filename)
}
}
str := fnm + ":" + e.Pos.String() + ": " + e.Msg
if showRule && !reflectx.IsNil(reflect.ValueOf(e.Rule)) {
str += fmt.Sprintf(" (rule: %v)", e.Rule.AsTree().Name)
}
ssz := len(e.Src)
if showSrc && ssz > 0 && ssz >= e.Pos.Ch {
str += "<br>\n\t> "
if ssz > e.Pos.Ch+30 {
str += e.Src[e.Pos.Ch : e.Pos.Ch+30]
} else if ssz > e.Pos.Ch {
str += e.Src[e.Pos.Ch:]
}
}
return str
}
// ErrorList is a list of *Errors.
// The zero value for an ErrorList is an empty ErrorList ready to use.
type ErrorList []*Error
// Add adds an Error with given position and error message to an ErrorList.
func (p *ErrorList) Add(pos Pos, fname, msg string, srcln string, rule tree.Node) *Error {
e := &Error{pos, fname, msg, srcln, rule}
*p = append(*p, e)
return e
}
// Reset resets an ErrorList to no errors.
func (p *ErrorList) Reset() { *p = (*p)[0:0] }
// ErrorList implements the sort Interface.
func (p ErrorList) Len() int { return len(p) }
func (p ErrorList) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p ErrorList) Less(i, j int) bool {
e := p[i]
f := p[j]
if e.Filename != f.Filename {
return e.Filename < f.Filename
}
if e.Pos.Ln != f.Pos.Ln {
return e.Pos.Ln < f.Pos.Ln
}
if e.Pos.Ch != f.Pos.Ch {
return e.Pos.Ch < f.Pos.Ch
}
return e.Msg < e.Msg
}
// Sort sorts an ErrorList. *Error entries are sorted by position,
// other errors are sorted by error message, and before any *Error
// entry.
func (p ErrorList) Sort() {
sort.Sort(p)
}
// RemoveMultiples sorts an ErrorList and removes all but the first error per line.
func (p *ErrorList) RemoveMultiples() {
sort.Sort(p)
var last Pos // initial last.Ln is != any legal error line
var lastfn string
i := 0
for _, e := range *p {
if e.Filename != lastfn || e.Pos.Ln != last.Ln {
last = e.Pos
lastfn = e.Filename
(*p)[i] = e
i++
}
}
(*p) = (*p)[0:i]
}
// An ErrorList implements the error interface.
func (p ErrorList) Error() string {
switch len(p) {
case 0:
return "no errors"
case 1:
return p[0].Error()
}
return fmt.Sprintf("%s (and %d more errors)", p[0], len(p)-1)
}
// Err returns an error equivalent to this error list.
// If the list is empty, Err returns nil.
func (p ErrorList) Err() error {
if len(p) == 0 {
return nil
}
return p
}
// Report returns all (or up to maxN if > 0) errors in the list in one string
// with customizable output options for viewing errors:
// - basepath if non-empty shows filename relative to that path.
// - showSrc shows the source line on a second line -- truncated to 30 chars around err
// - showRule prints the rule name
func (p ErrorList) Report(maxN int, basepath string, showSrc, showRule bool) string {
ne := len(p)
if ne == 0 {
return ""
}
str := ""
if maxN == 0 {
maxN = ne
} else {
maxN = min(ne, maxN)
}
cnt := 0
lstln := -1
for ei := 0; ei < ne; ei++ {
er := p[ei]
if er.Pos.Ln == lstln {
continue
}
str += p[ei].Report(basepath, showSrc, showRule) + "<br>\n"
lstln = er.Pos.Ln
cnt++
if cnt > maxN {
break
}
}
if ne > maxN {
str += fmt.Sprintf("... and %v more errors<br>\n", ne-maxN)
}
return str
}
// PrintError is a utility function that prints a list of errors to w,
// one error per line, if the err parameter is an ErrorList. Otherwise
// it prints the err string.
func PrintError(w io.Writer, err error) {
if list, ok := err.(ErrorList); ok {
for _, e := range list {
fmt.Fprintf(w, "%s\n", e)
}
} else if err != nil {
fmt.Fprintf(w, "%s\n", err)
}
}
// 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 lexer
import (
"bytes"
"io"
"log"
"os"
"slices"
"strings"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/parse/token"
)
// File contains the contents of the file being parsed -- all kept in
// memory, and represented by Line as runes, so that positions in
// the file are directly convertible to indexes in Lines structure
type File struct {
// the current file being lex'd
Filename string
// the known file type, if known (typically only known files are processed)
Known fileinfo.Known
// base path for reporting file names -- this must be set externally e.g., by gide for the project root path
BasePath string
// lex'd version of the lines -- allocated to size of Lines
Lexs []Line
// comment tokens are stored separately here, so parser doesn't need to worry about them, but they are available for highlighting and other uses
Comments []Line
// stack present at the end of each line -- needed for contextualizing line-at-time lexing while editing
LastStacks []Stack
// token positions per line for the EOS (end of statement) tokens -- very important for scoping top-down parsing
EosPos []EosPos
// contents of the file as lines of runes
Lines [][]rune
}
// SetSrc sets the source to given content, and alloc Lexs -- if basepath is empty
// then it is set to the path for the filename
func (fl *File) SetSrc(src [][]rune, fname, basepath string, known fileinfo.Known) {
fl.Filename = fname
if basepath != "" {
fl.BasePath = basepath
}
fl.Known = known
fl.Lines = src
fl.AllocLines()
}
// AllocLines allocates the data per line: lex outputs and stack.
// We reset state so stale state is not hanging around.
func (fl *File) AllocLines() {
if fl.Lines == nil {
return
}
nlines := fl.NLines()
fl.Lexs = make([]Line, nlines)
fl.Comments = make([]Line, nlines)
fl.LastStacks = make([]Stack, nlines)
fl.EosPos = make([]EosPos, nlines)
}
// LinesInserted inserts new lines -- called e.g., by core.TextBuf to sync
// the markup with ongoing edits
func (fl *File) LinesInserted(stln, nlns int) {
// Lexs
n := len(fl.Lexs)
if stln > n {
stln = n
}
fl.Lexs = slices.Insert(fl.Lexs, stln, make([]Line, nlns)...)
fl.Comments = slices.Insert(fl.Comments, stln, make([]Line, nlns)...)
fl.LastStacks = slices.Insert(fl.LastStacks, stln, make([]Stack, nlns)...)
fl.EosPos = slices.Insert(fl.EosPos, stln, make([]EosPos, nlns)...)
}
// LinesDeleted deletes lines -- called e.g., by core.TextBuf to sync
// the markup with ongoing edits
func (fl *File) LinesDeleted(stln, edln int) {
edln = min(edln, len(fl.Lexs))
fl.Lexs = append(fl.Lexs[:stln], fl.Lexs[edln:]...)
fl.Comments = append(fl.Comments[:stln], fl.Comments[edln:]...)
fl.LastStacks = append(fl.LastStacks[:stln], fl.LastStacks[edln:]...)
fl.EosPos = append(fl.EosPos[:stln], fl.EosPos[edln:]...)
}
// RunesFromBytes returns the lines of runes from a basic byte array
func RunesFromBytes(b []byte) [][]rune {
lns := bytes.Split(b, []byte("\n"))
nlines := len(lns)
rns := make([][]rune, nlines)
for ln, txt := range lns {
rns[ln] = bytes.Runes(txt)
}
return rns
}
// RunesFromString returns the lines of runes from a string (more efficient
// than converting to bytes)
func RunesFromString(str string) [][]rune {
lns := strings.Split(str, "\n")
nlines := len(lns)
rns := make([][]rune, nlines)
for ln, txt := range lns {
rns[ln] = []rune(txt)
}
return rns
}
// OpenFileBytes returns bytes in given file, and logs any errors as well
func OpenFileBytes(fname string) ([]byte, error) {
fp, err := os.Open(fname)
if err != nil {
log.Println(err.Error())
return nil, err
}
alltxt, err := io.ReadAll(fp)
fp.Close()
if err != nil {
log.Println(err.Error())
return nil, err
}
return alltxt, nil
}
// OpenFile sets source to be parsed from given filename
func (fl *File) OpenFile(fname string) error {
alltxt, err := OpenFileBytes(fname)
if err != nil {
return err
}
rns := RunesFromBytes(alltxt)
known := fileinfo.KnownFromFile(fname)
fl.SetSrc(rns, fname, "", known)
return nil
}
// SetBytes sets source to be parsed from given bytes
func (fl *File) SetBytes(txt []byte) {
if txt == nil {
return
}
fl.Lines = RunesFromBytes(txt)
fl.AllocLines()
}
// SetLineSrc sets source runes from given line of runes.
// Returns false if out of range.
func (fl *File) SetLineSrc(ln int, txt []rune) bool {
nlines := fl.NLines()
if ln >= nlines || ln < 0 || txt == nil {
return false
}
fl.Lines[ln] = slices.Clone(txt)
return true
}
// InitFromLine initializes from one line of source file
func (fl *File) InitFromLine(sfl *File, ln int) bool {
nlines := sfl.NLines()
if ln >= nlines || ln < 0 {
return false
}
src := [][]rune{sfl.Lines[ln], {}} // need extra blank
fl.SetSrc(src, sfl.Filename, sfl.BasePath, sfl.Known)
fl.Lexs = []Line{sfl.Lexs[ln], {}}
fl.Comments = []Line{sfl.Comments[ln], {}}
fl.EosPos = []EosPos{sfl.EosPos[ln], {}}
return true
}
// InitFromString initializes from given string. Returns false if string is empty
func (fl *File) InitFromString(str string, fname string, known fileinfo.Known) bool {
if str == "" {
return false
}
src := RunesFromString(str)
if len(src) == 1 { // need more than 1 line
src = append(src, []rune{})
}
fl.SetSrc(src, fname, "", known)
return true
}
///////////////////////////////////////////////////////////////////////////
// Accessors
// NLines returns the number of lines in source
func (fl *File) NLines() int {
if fl.Lines == nil {
return 0
}
return len(fl.Lines)
}
// SrcLine returns given line of source, as a string, or "" if out of range
func (fl *File) SrcLine(ln int) string {
nlines := fl.NLines()
if ln < 0 || ln >= nlines {
return ""
}
return string(fl.Lines[ln])
}
// SetLine sets the line data from the lexer -- does a clone to keep the copy
func (fl *File) SetLine(ln int, lexs, comments Line, stack Stack) {
if len(fl.Lexs) <= ln {
fl.AllocLines()
}
if len(fl.Lexs) <= ln {
return
}
fl.Lexs[ln] = lexs.Clone()
fl.Comments[ln] = comments.Clone()
fl.LastStacks[ln] = stack.Clone()
fl.EosPos[ln] = nil
}
// LexLine returns the lexing output for given line,
// combining comments and all other tokens
// and allocating new memory using clone
func (fl *File) LexLine(ln int) Line {
if len(fl.Lexs) <= ln {
return nil
}
merge := MergeLines(fl.Lexs[ln], fl.Comments[ln])
return merge.Clone()
}
// NTokens returns number of lex tokens for given line
func (fl *File) NTokens(ln int) int {
if fl == nil || fl.Lexs == nil {
return 0
}
if len(fl.Lexs) <= ln {
return 0
}
return len(fl.Lexs[ln])
}
// IsLexPosValid returns true if given lexical token position is valid
func (fl *File) IsLexPosValid(pos Pos) bool {
if pos.Ln < 0 || pos.Ln >= fl.NLines() {
return false
}
nt := fl.NTokens(pos.Ln)
if pos.Ch < 0 || pos.Ch >= nt {
return false
}
return true
}
// LexAt returns Lex item at given position, with no checking
func (fl *File) LexAt(cp Pos) *Lex {
return &fl.Lexs[cp.Ln][cp.Ch]
}
// LexAtSafe returns the Lex item at given position, or last lex item if beyond end
func (fl *File) LexAtSafe(cp Pos) Lex {
nln := fl.NLines()
if nln == 0 {
return Lex{}
}
if cp.Ln >= nln {
cp.Ln = nln - 1
}
sz := len(fl.Lexs[cp.Ln])
if sz == 0 {
if cp.Ln > 0 {
cp.Ln--
return fl.LexAtSafe(cp)
}
return Lex{}
}
if cp.Ch < 0 {
cp.Ch = 0
}
if cp.Ch >= sz {
cp.Ch = sz - 1
}
return *fl.LexAt(cp)
}
// ValidTokenPos returns the next valid token position starting at given point,
// false if at end of tokens
func (fl *File) ValidTokenPos(pos Pos) (Pos, bool) {
for pos.Ch >= fl.NTokens(pos.Ln) {
pos.Ln++
pos.Ch = 0
if pos.Ln >= fl.NLines() {
pos.Ln = fl.NLines() - 1 // make valid
return pos, false
}
}
return pos, true
}
// NextTokenPos returns the next token position, false if at end of tokens
func (fl *File) NextTokenPos(pos Pos) (Pos, bool) {
pos.Ch++
return fl.ValidTokenPos(pos)
}
// PrevTokenPos returns the previous token position, false if at end of tokens
func (fl *File) PrevTokenPos(pos Pos) (Pos, bool) {
pos.Ch--
if pos.Ch < 0 {
pos.Ln--
if pos.Ln < 0 {
return pos, false
}
for fl.NTokens(pos.Ln) == 0 {
pos.Ln--
if pos.Ln < 0 {
pos.Ln = 0
pos.Ch = 0
return pos, false
}
}
pos.Ch = fl.NTokens(pos.Ln) - 1
}
return pos, true
}
// Token gets lex token at given Pos (Ch = token index)
func (fl *File) Token(pos Pos) token.KeyToken {
return fl.Lexs[pos.Ln][pos.Ch].Token
}
// PrevDepth returns the depth of the token immediately prior to given line
func (fl *File) PrevDepth(ln int) int {
pos := Pos{ln, 0}
pos, ok := fl.PrevTokenPos(pos)
if !ok {
return 0
}
lx := fl.LexAt(pos)
depth := lx.Token.Depth
if lx.Token.Token.IsPunctGpLeft() {
depth++
}
return depth
}
// PrevStack returns the stack from the previous line
func (fl *File) PrevStack(ln int) Stack {
if ln <= 0 {
return nil
}
if len(fl.LastStacks) <= ln {
return nil
}
return fl.LastStacks[ln-1]
}
// TokenMapReg creates a TokenMap of tokens in region, including their
// Cat and SubCat levels -- err's on side of inclusiveness -- used
// for optimizing token matching
func (fl *File) TokenMapReg(reg Reg) TokenMap {
m := make(TokenMap)
cp, ok := fl.ValidTokenPos(reg.St)
for ok && cp.IsLess(reg.Ed) {
tok := fl.Token(cp).Token
m.Set(tok)
subc := tok.SubCat()
if subc != tok {
m.Set(subc)
}
cat := tok.Cat()
if cat != tok {
m.Set(cat)
}
cp, ok = fl.NextTokenPos(cp)
}
return m
}
/////////////////////////////////////////////////////////////////////
// Source access from pos, reg, tok
// TokenSrc gets source runes for given token position
func (fl *File) TokenSrc(pos Pos) []rune {
if !fl.IsLexPosValid(pos) {
return nil
}
lx := fl.Lexs[pos.Ln][pos.Ch]
return fl.Lines[pos.Ln][lx.St:lx.Ed]
}
// TokenSrcPos returns source reg associated with lex token at given token position
func (fl *File) TokenSrcPos(pos Pos) Reg {
if !fl.IsLexPosValid(pos) {
return Reg{}
}
lx := fl.Lexs[pos.Ln][pos.Ch]
return Reg{St: Pos{pos.Ln, lx.St}, Ed: Pos{pos.Ln, lx.Ed}}
}
// TokenSrcReg translates a region of tokens into a region of source
func (fl *File) TokenSrcReg(reg Reg) Reg {
if !fl.IsLexPosValid(reg.St) || reg.IsNil() {
return Reg{}
}
st := fl.Lexs[reg.St.Ln][reg.St.Ch].St
ep, _ := fl.PrevTokenPos(reg.Ed) // ed is exclusive -- go to prev
ed := fl.Lexs[ep.Ln][ep.Ch].Ed
return Reg{St: Pos{reg.St.Ln, st}, Ed: Pos{ep.Ln, ed}}
}
// RegSrc returns the source (as a string) for given region
func (fl *File) RegSrc(reg Reg) string {
if reg.Ed.Ln == reg.St.Ln {
if reg.Ed.Ch > reg.St.Ch {
return string(fl.Lines[reg.Ed.Ln][reg.St.Ch:reg.Ed.Ch])
} else {
return ""
}
}
src := string(fl.Lines[reg.St.Ln][reg.St.Ch:])
nln := reg.Ed.Ln - reg.St.Ln
if nln > 10 {
src += "|>" + string(fl.Lines[reg.St.Ln+1]) + "..."
src += "|>" + string(fl.Lines[reg.Ed.Ln-1])
return src
}
for ln := reg.St.Ln + 1; ln < reg.Ed.Ln; ln++ {
src += "|>" + string(fl.Lines[ln])
}
src += "|>" + string(fl.Lines[reg.Ed.Ln][:reg.Ed.Ch])
return src
}
// TokenRegSrc returns the source code associated with the given token region
func (fl *File) TokenRegSrc(reg Reg) string {
if !fl.IsLexPosValid(reg.St) {
return ""
}
srcreg := fl.TokenSrcReg(reg)
return fl.RegSrc(srcreg)
}
// LexTagSrcLn returns the lex'd tagged source line for given line
func (fl *File) LexTagSrcLn(ln int) string {
return fl.Lexs[ln].TagSrc(fl.Lines[ln])
}
// LexTagSrc returns the lex'd tagged source for entire source
func (fl *File) LexTagSrc() string {
txt := ""
nlines := fl.NLines()
for ln := 0; ln < nlines; ln++ {
txt += fl.LexTagSrcLn(ln) + "\n"
}
return txt
}
/////////////////////////////////////////////////////////////////
// EOS end of statement processing
// InsertEos inserts an EOS just after the given token position
// (e.g., cp = last token in line)
func (fl *File) InsertEos(cp Pos) Pos {
np := Pos{cp.Ln, cp.Ch + 1}
elx := fl.LexAt(cp)
depth := elx.Token.Depth
fl.Lexs[cp.Ln].Insert(np.Ch, Lex{Token: token.KeyToken{Token: token.EOS, Depth: depth}, St: elx.Ed, Ed: elx.Ed})
fl.EosPos[np.Ln] = append(fl.EosPos[np.Ln], np.Ch)
return np
}
// ReplaceEos replaces given token with an EOS
func (fl *File) ReplaceEos(cp Pos) {
clex := fl.LexAt(cp)
clex.Token.Token = token.EOS
fl.EosPos[cp.Ln] = append(fl.EosPos[cp.Ln], cp.Ch)
}
// EnsureFinalEos makes sure that the given line ends with an EOS (if it
// has tokens).
// Used for line-at-time parsing just to make sure it matches even if
// you haven't gotten to the end etc.
func (fl *File) EnsureFinalEos(ln int) {
if ln >= fl.NLines() {
return
}
sz := len(fl.Lexs[ln])
if sz == 0 {
return // can't get depth or anything -- useless
}
ep := Pos{ln, sz - 1}
elx := fl.LexAt(ep)
if elx.Token.Token == token.EOS {
return
}
fl.InsertEos(ep)
}
// NextEos finds the next EOS position at given depth, false if none
func (fl *File) NextEos(stpos Pos, depth int) (Pos, bool) {
// prf := profile.Start("NextEos")
// defer prf.End()
ep := stpos
nlines := fl.NLines()
if stpos.Ln >= nlines {
return ep, false
}
eps := fl.EosPos[stpos.Ln]
for i := range eps {
if eps[i] < stpos.Ch {
continue
}
ep.Ch = eps[i]
lx := fl.LexAt(ep)
if lx.Token.Depth == depth {
return ep, true
}
}
for ep.Ln = stpos.Ln + 1; ep.Ln < nlines; ep.Ln++ {
eps := fl.EosPos[ep.Ln]
sz := len(eps)
if sz == 0 {
continue
}
for i := 0; i < sz; i++ {
ep.Ch = eps[i]
lx := fl.LexAt(ep)
if lx.Token.Depth == depth {
return ep, true
}
}
}
return ep, false
}
// NextEosAnyDepth finds the next EOS at any depth
func (fl *File) NextEosAnyDepth(stpos Pos) (Pos, bool) {
ep := stpos
nlines := fl.NLines()
if stpos.Ln >= nlines {
return ep, false
}
eps := fl.EosPos[stpos.Ln]
if np := eps.FindGtEq(stpos.Ch); np >= 0 {
ep.Ch = np
return ep, true
}
ep.Ch = 0
for ep.Ln = stpos.Ln + 1; ep.Ln < nlines; ep.Ln++ {
sz := len(fl.EosPos[ep.Ln])
if sz == 0 {
continue
}
return ep, true
}
return ep, false
}
// Copyright (c) 2020, 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 lexer
import (
"cogentcore.org/core/base/indent"
"cogentcore.org/core/parse/token"
)
// these functions support indentation algorithms,
// operating on marked-up rune source.
// LineIndent returns the number of tabs or spaces at start of given rune-line,
// based on target tab-size (only relevant for spaces).
// If line starts with tabs, then those are counted, else spaces --
// combinations of tabs and spaces won't produce sensible results.
func LineIndent(src []rune, tabSz int) (ind int, ichr indent.Character) {
ichr = indent.Tab
sz := len(src)
if sz == 0 {
return
}
if src[0] == ' ' {
ichr = indent.Space
ind = 1
} else if src[0] != '\t' {
return
} else {
ind = 1
}
if ichr == indent.Space {
for i := 1; i < sz; i++ {
if src[i] == ' ' {
ind++
} else {
ind /= tabSz
return
}
}
ind /= tabSz
return
} else {
for i := 1; i < sz; i++ {
if src[i] == '\t' {
ind++
} else {
return
}
}
}
return
}
// PrevLineIndent returns indentation level of previous line
// from given line that has indentation -- skips blank lines.
// Returns indent level and previous line number, and indent char.
// indent level is in increments of tabSz for spaces, and tabs for tabs.
// Operates on rune source with markup lex tags per line.
func PrevLineIndent(src [][]rune, tags []Line, ln int, tabSz int) (ind, pln int, ichr indent.Character) {
ln--
for ln >= 0 {
if len(src[ln]) == 0 {
ln--
continue
}
ind, ichr = LineIndent(src[ln], tabSz)
pln = ln
return
}
ind = 0
pln = 0
return
}
// BracketIndentLine returns the indentation level for given line based on
// previous line's indentation level, and any delta change based on
// brackets starting or ending the previous or current line.
// indent level is in increments of tabSz for spaces, and tabs for tabs.
// Operates on rune source with markup lex tags per line.
func BracketIndentLine(src [][]rune, tags []Line, ln int, tabSz int) (pInd, delInd, pLn int, ichr indent.Character) {
pInd, pLn, ichr = PrevLineIndent(src, tags, ln, tabSz)
curUnd, _ := LineStartEndBracket(src[ln], tags[ln])
_, prvInd := LineStartEndBracket(src[pLn], tags[pLn])
delInd = 0
switch {
case prvInd && curUnd:
delInd = 0 // offset
case prvInd:
delInd = 1 // indent
case curUnd:
delInd = -1 // undent
}
if pInd == 0 && delInd < 0 { // error..
delInd = 0
}
return
}
// LastTokenIgnoreComment returns the last token of the tags, ignoring
// any final comment at end
func LastLexIgnoreComment(tags Line) (*Lex, int) {
var ll *Lex
li := -1
nt := len(tags)
for i := nt - 1; i >= 0; i-- {
l := &tags[i]
if l.Token.Token.Cat() == token.Comment || l.Token.Token < token.Keyword {
continue
}
ll = l
li = i
break
}
return ll, li
}
// LineStartEndBracket checks if line starts with a closing bracket
// or ends with an opening bracket. This is used for auto-indent for example.
// Bracket is Paren, Bracket, or Brace.
func LineStartEndBracket(src []rune, tags Line) (start, end bool) {
if len(src) == 0 {
return
}
nt := len(tags)
if nt > 0 {
ftok := tags[0].Token.Token
if ftok.InSubCat(token.PunctGp) {
if ftok.IsPunctGpRight() {
start = true
}
}
ll, _ := LastLexIgnoreComment(tags)
if ll != nil {
ltok := ll.Token.Token
if ltok.InSubCat(token.PunctGp) {
if ltok.IsPunctGpLeft() {
end = true
}
}
}
return
}
// no tags -- do it manually
fi := FirstNonSpaceRune(src)
if fi >= 0 {
bp, rt := BracePair(src[fi])
if bp != 0 && rt {
start = true
}
}
li := LastNonSpaceRune(src)
if li >= 0 {
bp, rt := BracePair(src[li])
if bp != 0 && !rt {
end = true
}
}
return
}
// 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 lexer provides all the lexing functions that transform text
// into lexical tokens, using token types defined in the token package.
// It also has the basic file source and position / region management
// functionality.
package lexer
//go:generate core generate
import (
"fmt"
"cogentcore.org/core/base/nptime"
"cogentcore.org/core/parse/token"
)
// Lex represents a single lexical element, with a token, and start and end rune positions
// within a line of a file. Critically it also contains the nesting depth computed from
// all the parens, brackets, braces. Todo: also support XML < > </ > tag depth.
type Lex struct {
// Token includes cache of keyword for keyword types, and also has nesting depth: starting at 0 at start of file and going up for every increment in bracket / paren / start tag and down for every decrement. Is computed once and used extensively in parsing.
Token token.KeyToken
// start rune index within original source line for this token
St int
// end rune index within original source line for this token (exclusive -- ends one before this)
Ed int
// time when region was set -- used for updating locations in the text based on time stamp (using efficient non-pointer time)
Time nptime.Time
}
func NewLex(tok token.KeyToken, st, ed int) Lex {
lx := Lex{Token: tok, St: st, Ed: ed}
return lx
}
// Src returns the rune source for given lex item (does no validity checking)
func (lx *Lex) Src(src []rune) []rune {
return src[lx.St:lx.Ed]
}
// Now sets the time stamp to now
func (lx *Lex) Now() {
lx.Time.Now()
}
// String satisfies the fmt.Stringer interface
func (lx *Lex) String() string {
return fmt.Sprintf("[+%d:%v:%v:%v]", lx.Token.Depth, lx.St, lx.Ed, lx.Token.String())
}
// ContainsPos returns true if the Lex element contains given character position
func (lx *Lex) ContainsPos(pos int) bool {
return pos >= lx.St && pos < lx.Ed
}
// OverlapsReg returns true if the two regions overlap
func (lx *Lex) OverlapsReg(or Lex) bool {
// start overlaps
if (lx.St >= or.St && lx.St < or.Ed) || (or.St >= lx.St && or.St < lx.Ed) {
return true
}
// end overlaps
return (lx.Ed > or.St && lx.Ed <= or.Ed) || (or.Ed > lx.St && or.Ed <= lx.Ed)
}
// Region returns the region for this lexical element, at given line
func (lx *Lex) Region(ln int) Reg {
return Reg{St: Pos{Ln: ln, Ch: lx.St}, Ed: Pos{Ln: ln, Ch: lx.Ed}}
}
// 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 lexer
import (
"slices"
"sort"
"unicode"
"cogentcore.org/core/parse/token"
)
// Line is one line of Lex'd text
type Line []Lex
// Add adds one element to the lex line (just append)
func (ll *Line) Add(lx Lex) {
*ll = append(*ll, lx)
}
// Add adds one element to the lex line with given params, returns pointer to that new lex
func (ll *Line) AddLex(tok token.KeyToken, st, ed int) *Lex {
lx := NewLex(tok, st, ed)
li := len(*ll)
ll.Add(lx)
return &(*ll)[li]
}
// Insert inserts one element to the lex line at given point
func (ll *Line) Insert(idx int, lx Lex) {
sz := len(*ll)
*ll = append(*ll, lx)
if idx < sz {
copy((*ll)[idx+1:], (*ll)[idx:sz])
(*ll)[idx] = lx
}
}
// AtPos returns the Lex in place for given position, and index, or nil, -1 if none
func (ll *Line) AtPos(pos int) (*Lex, int) {
for i := range *ll {
lx := &((*ll)[i])
if lx.ContainsPos(pos) {
return lx, i
}
}
return nil, -1
}
// Clone returns a new copy of the line
func (ll *Line) Clone() Line {
if len(*ll) == 0 {
return nil
}
cp := make(Line, len(*ll))
for i := range *ll {
cp[i] = (*ll)[i]
}
return cp
}
// AddSort adds a new lex element in sorted order to list, sorted by start
// position, and if at the same start position, then sorted *decreasing*
// by end position -- this allows outer tags to be processed before inner tags
// which fits a stack-based tag markup logic.
func (ll *Line) AddSort(lx Lex) {
for i, t := range *ll {
if t.St < lx.St {
continue
}
if t.St == lx.St && t.Ed >= lx.Ed {
continue
}
*ll = append(*ll, lx)
copy((*ll)[i+1:], (*ll)[i:])
(*ll)[i] = lx
return
}
*ll = append(*ll, lx)
}
// Sort sorts the lex elements by starting pos, and ending pos *decreasing* if a tie
func (ll *Line) Sort() {
sort.Slice((*ll), func(i, j int) bool {
return (*ll)[i].St < (*ll)[j].St || ((*ll)[i].St == (*ll)[j].St && (*ll)[i].Ed > (*ll)[j].Ed)
})
}
// DeleteIndex deletes at given index
func (ll *Line) DeleteIndex(idx int) {
*ll = append((*ll)[:idx], (*ll)[idx+1:]...)
}
// DeleteToken deletes a specific token type from list
func (ll *Line) DeleteToken(tok token.Tokens) {
nt := len(*ll)
for i := nt - 1; i >= 0; i-- { // remove
t := (*ll)[i]
if t.Token.Token == tok {
ll.DeleteIndex(i)
}
}
}
// RuneStrings returns array of strings for Lex regions defined in Line, for
// given rune source string
func (ll *Line) RuneStrings(rstr []rune) []string {
regs := make([]string, len(*ll))
for i, t := range *ll {
regs[i] = string(rstr[t.St:t.Ed])
}
return regs
}
// MergeLines merges the two lines of lex regions into a combined list
// properly ordered by sequence of tags within the line.
func MergeLines(t1, t2 Line) Line {
sz1 := len(t1)
sz2 := len(t2)
if sz1 == 0 {
return t2
}
if sz2 == 0 {
return t1
}
tsz := sz1 + sz2
tl := make(Line, sz1, tsz)
copy(tl, t1)
for i := 0; i < sz2; i++ {
tl.AddSort(t2[i])
}
return tl
}
// String satisfies the fmt.Stringer interface
func (ll *Line) String() string {
str := ""
for _, t := range *ll {
str += t.String() + " "
}
return str
}
// TagSrc returns the token-tagged source
func (ll *Line) TagSrc(src []rune) string {
str := ""
for _, t := range *ll {
s := t.Src(src)
str += t.String() + `"` + string(s) + `"` + " "
}
return str
}
// Strings returns a slice of strings for each of the Lex items in given rune src
// split by Line Lex's. Returns nil if Line empty.
func (ll *Line) Strings(src []rune) []string {
nl := len(*ll)
if nl == 0 {
return nil
}
sa := make([]string, nl)
for i, t := range *ll {
sa[i] = string(t.Src(src))
}
return sa
}
// NonCodeWords returns a Line of white-space separated word tokens in given tagged source
// that ignores token.IsCode token regions -- i.e., the "regular" words
// present in the source line -- this is useful for things like spell checking
// or manual parsing.
func (ll *Line) NonCodeWords(src []rune) Line {
wsrc := slices.Clone(src)
for _, t := range *ll { // blank out code parts first
if t.Token.Token.IsCode() {
for i := t.St; i < t.Ed; i++ {
wsrc[i] = ' '
}
}
}
return RuneFields(wsrc)
}
// RuneFields returns a Line of Lex's defining the non-white-space "fields"
// in the given rune string
func RuneFields(src []rune) Line {
if len(src) == 0 {
return nil
}
var ln Line
cur := Lex{}
pspc := unicode.IsSpace(src[0])
cspc := pspc
for i, r := range src {
cspc = unicode.IsSpace(r)
if pspc {
if !cspc {
cur.St = i
}
} else {
if cspc {
cur.Ed = i
ln.Add(cur)
}
}
pspc = cspc
}
if !pspc {
cur.Ed = len(src)
cur.Now()
ln.Add(cur)
}
return ln
}
// 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 lexer
import (
"fmt"
"strings"
"unicode"
"cogentcore.org/core/parse/token"
)
// These functions provide "manual" lexing support for specific cases, such as completion, where a string must be processed further.
// FirstWord returns the first contiguous sequence of purely [unicode.IsLetter] runes within the given string.
// It skips over any leading non-letters until a letter is found.
// Note that this function does not include numbers. For that, you can use the FirstWordDigits function.
func FirstWord(str string) string {
rstr := ""
for _, s := range str {
if !IsLetter(s) {
if len(rstr) == 0 {
continue
}
break
}
rstr += string(s)
}
return rstr
}
// FirstWordDigits returns the first contiguous sequence of purely [IsLetterOrDigit]
// runes within the given string. It skips over any leading non-letters until a letter
// (not digit) is found.
func FirstWordDigits(str string) string {
rstr := ""
for _, s := range str {
if !IsLetterOrDigit(s) {
if len(rstr) == 0 {
continue
}
break
}
if len(rstr) == 0 && IsDigit(s) { // can't start with digit
continue
}
rstr += string(s)
}
return rstr
}
// FirstWordApostrophe returns the first contiguous sequence of purely
// [unicode.IsLetter] runes that can also contain an apostrophe within
// the word but not at the end.
func FirstWordApostrophe(str string) string {
rstr := ""
for _, s := range str {
if !(IsLetter(s) || s == '\'') {
if len(rstr) == 0 {
continue
}
break
}
if len(rstr) == 0 && s == '\'' { // can't start with '
continue
}
rstr += string(s)
}
rstr = strings.TrimRight(rstr, "'") // get rid of any trailing ones!
return rstr
}
// TrimLeftToAlpha returns string without any leading non-alpha runes.
func TrimLeftToAlpha(nm string) string {
return strings.TrimLeftFunc(nm, func(r rune) bool {
return !unicode.IsLetter(r)
})
}
// FirstNonSpaceRune returns the index of first non-space rune, -1 if not found
func FirstNonSpaceRune(src []rune) int {
for i, s := range src {
if !unicode.IsSpace(s) {
return i
}
}
return -1
}
// LastNonSpaceRune returns the index of last non-space rune, -1 if not found
func LastNonSpaceRune(src []rune) int {
sz := len(src)
if sz == 0 {
return -1
}
for i := sz - 1; i >= 0; i-- {
s := src[i]
if !unicode.IsSpace(s) {
return i
}
}
return -1
}
// InnerBracketScope returns the inner scope for a given bracket type if it is
// imbalanced. It is important to do completion based on just that inner scope
// if that is where the user is at.
func InnerBracketScope(str string, brl, brr string) string {
nlb := strings.Count(str, brl)
nrb := strings.Count(str, brr)
if nlb == nrb {
return str
}
if nlb > nrb {
li := strings.LastIndex(str, brl)
if li == len(str)-1 {
return InnerBracketScope(str[:li], brl, brr) // get rid of open ending and try again
}
str = str[li+1:]
ri := strings.Index(str, brr)
if ri < 0 {
return str
}
return str[:ri]
}
// nrb > nlb -- we're missing the left guys -- go to first rb
ri := strings.Index(str, brr)
if ri == 0 {
return InnerBracketScope(str[1:], brl, brr) // get rid of opening and try again
}
str = str[:ri]
li := strings.Index(str, brl)
if li < 0 {
return str
}
return str[li+1:]
}
// LastField returns the last white-space separated string
func LastField(str string) string {
if str == "" {
return ""
}
flds := strings.Fields(str)
return flds[len(flds)-1]
}
// ObjPathAt returns the starting Lex, before given lex,
// that include sequences of PunctSepPeriod and NameTag
// which are used for object paths (e.g., field.field.field)
func ObjPathAt(line Line, lx *Lex) *Lex {
stlx := lx
if lx.St > 1 {
_, lxidx := line.AtPos(lx.St - 1)
for i := lxidx; i >= 0; i-- {
clx := &line[i]
if clx.Token.Token == token.PunctSepPeriod || clx.Token.Token.InCat(token.Name) {
stlx = clx
} else {
break
}
}
}
return stlx
}
// LastScopedString returns the last white-space separated, and bracket
// enclosed string from given string.
func LastScopedString(str string) string {
str = LastField(str)
bstr := str
str = InnerBracketScope(str, "{", "}")
str = InnerBracketScope(str, "(", ")")
str = InnerBracketScope(str, "[", "]")
if str == "" {
return bstr
}
str = TrimLeftToAlpha(str)
if str == "" {
str = TrimLeftToAlpha(bstr)
if str == "" {
return bstr
}
}
flds := strings.Split(str, ",")
return flds[len(flds)-1]
}
// HasUpperCase returns true if string has an upper-case letter
func HasUpperCase(str string) bool {
for _, r := range str {
if unicode.IsUpper(r) {
return true
}
}
return false
}
// MatchCase uses the source string case (upper / lower) to set corresponding
// case in target string, returning that string.
func MatchCase(src, trg string) string {
rsc := []rune(src)
rtg := []rune(trg)
mx := min(len(rsc), len(rtg))
for i := 0; i < mx; i++ {
t := rtg[i]
if unicode.IsUpper(rsc[i]) {
if !unicode.IsUpper(t) {
rtg[i] = unicode.ToUpper(t)
}
} else {
if !unicode.IsLower(t) {
rtg[i] = unicode.ToLower(t)
}
}
}
return string(rtg)
}
// MarkupPathsAsLinks checks for strings that look like file paths / urls and returns
// the original fields as a byte slice along with a marked-up version of that
// with html link markups for the files (as <a href="file:///...").
// Input is field-parsed already, and maxFlds is the maximum number of fields
// to look for file paths in (e.g., 2 is a reasonable default, to avoid getting
// other false-alarm info later in the text).
// This is mainly used for marking up output from commands, for example.
func MarkupPathsAsLinks(flds []string, maxFlds int) (orig, link []byte) {
mx := min(len(flds), maxFlds)
for i := 0; i < mx; i++ {
ff := flds[i]
if !strings.HasPrefix(ff, "./") && !strings.HasPrefix(ff, "/") && !strings.HasPrefix(ff, "../") {
// todo: use regex instead of this.
if !strings.Contains(ff, "/") && !strings.Contains(ff, ":") {
continue
}
}
fnflds := strings.Split(ff, ":")
fn := string(fnflds[0])
pos := ""
col := ""
if len(fnflds) > 1 {
pos = string(fnflds[1])
col = ""
if len(fnflds) > 2 {
col = string(fnflds[2])
}
}
lstr := ""
if col != "" {
lstr = fmt.Sprintf(`<a href="file:///%v#L%vC%v">%v</a>`, fn, pos, col, string(ff))
} else if pos != "" {
lstr = fmt.Sprintf(`<a href="file:///%v#L%v">%v</a>`, fn, pos, string(ff))
} else {
lstr = fmt.Sprintf(`<a href="file:///%v">%v</a>`, fn, string(ff))
}
orig = []byte(ff)
link = []byte(lstr)
break
}
return
}
// 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 lexer
import (
"unicode"
"unicode/utf8"
)
// Matches are what kind of lexing matches to make
type Matches int32 //enums:enum
// Matching rules
const (
// String means match a specific string as given in the rule
// Note: this only looks for the string with no constraints on
// what happens after this string -- use StrName to match entire names
String Matches = iota
// StrName means match a specific string that is a complete alpha-numeric
// string (including underbar _) with some other char at the end
// must use this for all keyword matches to ensure that it isn't just
// the start of a longer name
StrName
// Match any letter, including underscore
Letter
// Match digit 0-9
Digit
// Match any white space (space, tab) -- input is already broken into lines
WhiteSpace
// CurState means match current state value set by a PushState action, using String value in rule
// all CurState cases must generally be first in list of rules so they can preempt
// other rules when the state is active
CurState
// AnyRune means match any rune -- use this as the last condition where other terminators
// come first!
AnyRune
)
// MatchPos are special positions for a match to occur
type MatchPos int32 //enums:enum
// Matching position rules
const (
// AnyPos matches at any position
AnyPos MatchPos = iota
// StartOfLine matches at start of line
StartOfLine
// EndOfLine matches at end of line
EndOfLine
// MiddleOfLine matches not at the start or end
MiddleOfLine
// StartOfWord matches at start of word
StartOfWord
// EndOfWord matches at end of word
EndOfWord
// MiddleOfWord matches not at the start or end
MiddleOfWord
)
//////////////////////////////////////////////////////////////////////////////
// Match functions -- see also state for more complex ones
func IsLetter(ch rune) bool {
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= utf8.RuneSelf && unicode.IsLetter(ch)
}
func IsDigit(ch rune) bool {
return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch)
}
func IsLetterOrDigit(ch rune) bool {
return IsLetter(ch) || IsDigit(ch)
}
func IsWhiteSpace(ch rune) bool {
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
}
// 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 lexer
import (
"cogentcore.org/core/parse/token"
)
// PassTwo performs second pass(s) through the lexicalized version of the source,
// computing nesting depth for every token once and for all -- this is essential for
// properly matching tokens and also for colorization in syntax highlighting.
// Optionally, a subsequent pass finds end-of-statement (EOS) tokens, which are essential
// for parsing to first break the source down into statement-sized chunks. A separate
// list of EOS token positions is maintained for very fast access.
type PassTwo struct {
// should we perform EOS detection on this type of file?
DoEos bool
// use end-of-line as a default EOS, if nesting depth is same as start of line (python) -- see also EolToks
Eol bool
// replace all semicolons with EOS to keep it consistent (C, Go..)
Semi bool
// use backslash as a line continuer (python)
Backslash bool
// if a right-brace } is detected anywhere in the line, insert an EOS *before* RBrace AND after it (needed for Go) -- do not include RBrace in EolToks in this case
RBraceEos bool
// specific tokens to recognize at the end of a line that trigger an EOS (Go)
EolToks token.KeyTokenList
}
// TwoState is the state maintained for the PassTwo process
type TwoState struct {
// position in lex tokens we're on
Pos Pos
// file that we're operating on
Src *File
// stack of nesting tokens
NestStack []token.Tokens
// any error messages accumulated during lexing specifically
Errs ErrorList
}
// Init initializes state for a new pass -- called at start of NestDepth
func (ts *TwoState) Init() {
ts.Pos = PosZero
ts.NestStack = ts.NestStack[0:0]
}
// SetSrc sets the source we're operating on
func (ts *TwoState) SetSrc(src *File) {
ts.Src = src
}
// NextLine advances to next line
func (ts *TwoState) NextLine() {
ts.Pos.Ln++
ts.Pos.Ch = 0
}
// Error adds an passtwo error at current position
func (ts *TwoState) Error(msg string) {
ppos := ts.Pos
ppos.Ch--
clex := ts.Src.LexAtSafe(ppos)
ts.Errs.Add(Pos{ts.Pos.Ln, clex.St}, ts.Src.Filename, "PassTwo: "+msg, ts.Src.SrcLine(ts.Pos.Ln), nil)
}
// NestStackStr returns the token stack as strings
func (ts *TwoState) NestStackStr() string {
str := ""
for _, tok := range ts.NestStack {
switch tok {
case token.PunctGpLParen:
str += "paren ( "
case token.PunctGpLBrack:
str += "bracket [ "
case token.PunctGpLBrace:
str += "brace { "
}
}
return str
}
/////////////////////////////////////////////////////////////////////
// PassTwo
// Error adds an passtwo error at given position
func (pt *PassTwo) Error(ts *TwoState, msg string) {
ts.Error(msg)
}
// HasErrs reports if there are errors in eosing process
func (pt *PassTwo) HasErrs(ts *TwoState) bool {
return len(ts.Errs) > 0
}
// MismatchError reports a mismatch for given type of parentheses / bracket
func (pt *PassTwo) MismatchError(ts *TwoState, tok token.Tokens) {
switch tok {
case token.PunctGpRParen:
pt.Error(ts, "mismatching parentheses -- right paren ')' without matching left paren '('")
case token.PunctGpRBrack:
pt.Error(ts, "mismatching square brackets -- right bracket ']' without matching left bracket '['")
case token.PunctGpRBrace:
pt.Error(ts, "mismatching curly braces -- right brace '}' without matching left bracket '{'")
}
}
// PushNest pushes a nesting left paren / bracket onto stack
func (pt *PassTwo) PushNest(ts *TwoState, tok token.Tokens) {
ts.NestStack = append(ts.NestStack, tok)
}
// PopNest attempts to pop given token off of nesting stack, generating error if it mismatches
func (pt *PassTwo) PopNest(ts *TwoState, tok token.Tokens) {
sz := len(ts.NestStack)
if sz == 0 {
pt.MismatchError(ts, tok)
return
}
cur := ts.NestStack[sz-1]
ts.NestStack = ts.NestStack[:sz-1] // better to clear than keep even if err
if cur != tok.PunctGpMatch() {
pt.MismatchError(ts, tok)
}
}
// Perform nesting depth computation
func (pt *PassTwo) NestDepth(ts *TwoState) {
ts.Init()
nlines := ts.Src.NLines()
if nlines == 0 {
return
}
// if len(ts.Src.Lexs[nlines-1]) > 0 { // last line ends with tokens -- parser needs empty last line..
// ts.Src.Lexs = append(ts.Src.Lexs, Line{})
// *ts.Src.Lines = append(*ts.Src.Lines, []rune{})
// }
for ts.Pos.Ln < nlines {
sz := len(ts.Src.Lexs[ts.Pos.Ln])
if sz == 0 {
ts.NextLine()
continue
}
lx := ts.Src.LexAt(ts.Pos)
tok := lx.Token.Token
if tok.IsPunctGpLeft() {
lx.Token.Depth = len(ts.NestStack) // depth increments AFTER -- this turns out to be ESSENTIAL!
pt.PushNest(ts, tok)
} else if tok.IsPunctGpRight() {
pt.PopNest(ts, tok)
lx.Token.Depth = len(ts.NestStack) // end has same depth as start, which is same as SURROUND
} else {
lx.Token.Depth = len(ts.NestStack)
}
ts.Pos.Ch++
if ts.Pos.Ch >= sz {
ts.NextLine()
}
}
stsz := len(ts.NestStack)
if stsz > 0 {
pt.Error(ts, "mismatched grouping -- end of file with these left unmatched: "+ts.NestStackStr())
}
}
// Perform nesting depth computation on only one line, starting at
// given initial depth -- updates the given line
func (pt *PassTwo) NestDepthLine(line Line, initDepth int) {
sz := len(line)
if sz == 0 {
return
}
depth := initDepth
for i := 0; i < sz; i++ {
lx := &line[i]
tok := lx.Token.Token
if tok.IsPunctGpLeft() {
lx.Token.Depth = depth
depth++
} else if tok.IsPunctGpRight() {
depth--
lx.Token.Depth = depth
} else {
lx.Token.Depth = depth
}
}
}
// Perform EOS detection
func (pt *PassTwo) EosDetect(ts *TwoState) {
nlines := ts.Src.NLines()
pt.EosDetectPos(ts, PosZero, nlines)
}
// Perform EOS detection at given starting position, for given number of lines
func (pt *PassTwo) EosDetectPos(ts *TwoState, pos Pos, nln int) {
ts.Pos = pos
nlines := ts.Src.NLines()
ok := false
for lc := 0; ts.Pos.Ln < nlines && lc < nln; lc++ {
sz := len(ts.Src.Lexs[ts.Pos.Ln])
if sz == 0 {
ts.NextLine()
continue
}
if pt.Semi {
for ts.Pos.Ch = 0; ts.Pos.Ch < sz; ts.Pos.Ch++ {
lx := ts.Src.LexAt(ts.Pos)
if lx.Token.Token == token.PunctSepSemicolon {
ts.Src.ReplaceEos(ts.Pos)
}
}
}
if pt.RBraceEos {
skip := false
for ci := 0; ci < sz; ci++ {
lx := ts.Src.LexAt(Pos{ts.Pos.Ln, ci})
if lx.Token.Token == token.PunctGpRBrace {
if ci == 0 {
ip := Pos{ts.Pos.Ln, 0}
ip, ok = ts.Src.PrevTokenPos(ip)
if ok {
ilx := ts.Src.LexAt(ip)
if ilx.Token.Token != token.PunctGpLBrace && ilx.Token.Token != token.EOS {
ts.Src.InsertEos(ip)
}
}
} else {
ip := Pos{ts.Pos.Ln, ci - 1}
ilx := ts.Src.LexAt(ip)
if ilx.Token.Token != token.PunctGpLBrace {
ts.Src.InsertEos(ip)
ci++
sz++
}
}
if ci == sz-1 {
ip := Pos{ts.Pos.Ln, ci}
ts.Src.InsertEos(ip)
sz++
skip = true
}
}
}
if skip {
ts.NextLine()
continue
}
}
ep := Pos{ts.Pos.Ln, sz - 1} // end of line token
elx := ts.Src.LexAt(ep)
if pt.Eol {
sp := Pos{ts.Pos.Ln, 0} // start of line token
slx := ts.Src.LexAt(sp)
if slx.Token.Depth == elx.Token.Depth {
ts.Src.InsertEos(ep)
}
}
if len(pt.EolToks) > 0 { // not depth specific
if pt.EolToks.Match(elx.Token) {
ts.Src.InsertEos(ep)
}
}
ts.NextLine()
}
}
// 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 lexer
import (
"fmt"
"strings"
"cogentcore.org/core/parse/token"
)
// Pos is a position within the source file -- it is recorded always in 0, 0
// offset positions, but is converted into 1,1 offset for public consumption
// Ch positions are always in runes, not bytes. Also used for lex token indexes.
type Pos struct {
Ln int
Ch int
}
// String satisfies the fmt.Stringer interferace
func (ps Pos) String() string {
s := fmt.Sprintf("%d", ps.Ln+1)
if ps.Ch != 0 {
s += fmt.Sprintf(":%d", ps.Ch)
}
return s
}
// PosZero is the uninitialized zero text position (which is
// still a valid position)
var PosZero = Pos{}
// PosErr represents an error text position (-1 for both line and char)
// used as a return value for cases where error positions are possible
var PosErr = Pos{-1, -1}
// IsLess returns true if receiver position is less than given comparison
func (ps *Pos) IsLess(cmp Pos) bool {
switch {
case ps.Ln < cmp.Ln:
return true
case ps.Ln == cmp.Ln:
return ps.Ch < cmp.Ch
default:
return false
}
}
// FromString decodes text position from a string representation of form:
// [#]LxxCxx -- used in e.g., URL links -- returns true if successful
func (ps *Pos) FromString(link string) bool {
link = strings.TrimPrefix(link, "#")
lidx := strings.Index(link, "L")
cidx := strings.Index(link, "C")
switch {
case lidx >= 0 && cidx >= 0:
fmt.Sscanf(link, "L%dC%d", &ps.Ln, &ps.Ch)
ps.Ln-- // link is 1-based, we use 0-based
ps.Ch-- // ditto
case lidx >= 0:
fmt.Sscanf(link, "L%d", &ps.Ln)
ps.Ln-- // link is 1-based, we use 0-based
case cidx >= 0:
fmt.Sscanf(link, "C%d", &ps.Ch)
ps.Ch--
default:
// todo: could support other formats
return false
}
return true
}
////////////////////////////////////////////////////////////////////
// Reg
// Reg is a contiguous region within the source file
type Reg struct {
// starting position of region
St Pos
// ending position of region
Ed Pos
}
// RegZero is the zero region
var RegZero = Reg{}
// IsNil checks if the region is empty, because the start is after or equal to the end
func (tr Reg) IsNil() bool {
return !tr.St.IsLess(tr.Ed)
}
// Contains returns true if region contains position
func (tr Reg) Contains(ps Pos) bool {
return ps.IsLess(tr.Ed) && (tr.St == ps || tr.St.IsLess(ps))
}
////////////////////////////////////////////////////////////////////
// EosPos
// EosPos is a line of EOS token positions, always sorted low-to-high
type EosPos []int
// FindGt returns any pos value greater than given token pos, -1 if none
func (ep EosPos) FindGt(ch int) int {
for i := range ep {
if ep[i] > ch {
return ep[i]
}
}
return -1
}
// FindGtEq returns any pos value greater than or equal to given token pos, -1 if none
func (ep EosPos) FindGtEq(ch int) int {
for i := range ep {
if ep[i] >= ch {
return ep[i]
}
}
return -1
}
////////////////////////////////////////////////////////////////////
// TokenMap
// TokenMap is a token map, for optimizing token exclusion
type TokenMap map[token.Tokens]struct{}
// Set sets map for given token
func (tm TokenMap) Set(tok token.Tokens) {
tm[tok] = struct{}{}
}
// Has returns true if given token is in the map
func (tm TokenMap) Has(tok token.Tokens) bool {
_, has := tm[tok]
return has
}
// 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 lexer
import (
"fmt"
"io"
"strings"
"text/tabwriter"
"unicode"
"cogentcore.org/core/base/indent"
"cogentcore.org/core/parse/token"
"cogentcore.org/core/tree"
)
// Trace is whether to print debug trace info.
var Trace = false
// Rule operates on the text input to produce the lexical tokens.
//
// Lexing is done line-by-line -- you must push and pop states to
// coordinate across multiple lines, e.g., for multi-line comments.
//
// There is full access to entire line and you can decide based on future
// (offset) characters.
//
// In general it is best to keep lexing as simple as possible and
// leave the more complex things for the parsing step.
type Rule struct {
tree.NodeBase
// disable this rule -- useful for testing and exploration
Off bool `json:",omitempty"`
// description / comments about this rule
Desc string `json:",omitempty"`
// the token value that this rule generates -- use None for non-terminals
Token token.Tokens
// the lexical match that we look for to engage this rule
Match Matches
// position where match can occur
Pos MatchPos
// if action is LexMatch, this is the string we match
String string
// offset into the input to look for a match: 0 = current char, 1 = next one, etc
Offset int `json:",omitempty"`
// adjusts the size of the region (plus or minus) that is processed for the Next action -- allows broader and narrower matching relative to tagging
SizeAdj int `json:",omitempty"`
// the action(s) to perform, in order, if there is a match -- these are performed prior to iterating over child nodes
Acts []Actions
// string(s) for ReadUntil action -- will read until any of these strings are found -- separate different options with | -- if you need to read until a literal | just put two || in a row and that will show up as a blank, which is interpreted as a literal |
Until string `json:",omitempty"`
// the state to push if our action is PushState -- note that State matching is on String, not this value
PushState string `json:",omitempty"`
// create an optimization map for this rule, which must be a parent with children that all match against a Name string -- this reads the Name and directly activates the associated rule with that String, without having to iterate through them -- use this for keywords etc -- produces a SIGNIFICANT speedup for long lists of keywords.
NameMap bool `json:",omitempty"`
// length of source that matched -- if Next is called, this is what will be skipped to
MatchLen int `display:"-" json:"-" xml:"-"`
// NameMap lookup map -- created during Compile
NmMap map[string]*Rule `edit:"-" json:"-" xml:"-"`
}
// CompileAll is called on the top-level Rule to compile all nodes.
// returns true if everything is ok
func (lr *Rule) CompileAll(ls *State) bool {
allok := false
lr.WalkDown(func(k tree.Node) bool {
lri := k.(*Rule)
ok := lri.Compile(ls)
if !ok {
allok = false
}
return true
})
return allok
}
// Compile performs any one-time compilation steps on the rule
// returns false if there are any problems.
func (lr *Rule) Compile(ls *State) bool {
if lr.Off {
lr.SetProperty("inactive", true)
} else {
lr.DeleteProperty("inactive")
}
valid := true
lr.ComputeMatchLen(ls)
if lr.NameMap {
if !lr.CompileNameMap(ls) {
valid = false
}
}
return valid
}
// CompileNameMap compiles name map -- returns false if there are problems.
func (lr *Rule) CompileNameMap(ls *State) bool {
valid := true
lr.NmMap = make(map[string]*Rule, len(lr.Children))
for _, klri := range lr.Children {
klr := klri.(*Rule)
if !klr.Validate(ls) {
valid = false
}
if klr.String == "" {
ls.Error(0, "CompileNameMap: must have non-empty String to match", lr)
valid = false
continue
}
if _, has := lr.NmMap[klr.String]; has {
ls.Error(0, fmt.Sprintf("CompileNameMap: multiple rules have the same string name: %v -- must be unique!", klr.String), lr)
valid = false
} else {
lr.NmMap[klr.String] = klr
}
}
return valid
}
// Validate checks for any errors in the rules and issues warnings,
// returns true if valid (no err) and false if invalid (errs)
func (lr *Rule) Validate(ls *State) bool {
valid := true
if !tree.IsRoot(lr) {
switch lr.Match {
case StrName:
fallthrough
case String:
if len(lr.String) == 0 {
valid = false
ls.Error(0, "match = String or StrName but String is empty", lr)
}
case CurState:
for _, act := range lr.Acts {
if act == Next {
valid = false
ls.Error(0, "match = CurState cannot have Action = Next -- no src match", lr)
}
}
if len(lr.String) == 0 {
ls.Error(0, "match = CurState must have state to match in String -- is empty", lr)
}
if len(lr.PushState) > 0 {
ls.Error(0, "match = CurState has non-empty PushState -- must have state to match in String instead", lr)
}
}
}
if !lr.HasChildren() && len(lr.Acts) == 0 {
valid = false
ls.Error(0, "rule has no children and no action -- does nothing", lr)
}
hasPos := false
for _, act := range lr.Acts {
if act >= Name && act <= EOL {
hasPos = true
}
if act == Next && hasPos {
valid = false
ls.Error(0, "action = Next incompatible with action that reads item such as Name, Number, Quoted", lr)
}
}
if lr.Token.Cat() == token.Keyword && lr.Match != StrName {
valid = false
ls.Error(0, "Keyword token must use StrName to match entire name", lr)
}
// now we iterate over our kids
for _, klri := range lr.Children {
klr := klri.(*Rule)
if !klr.Validate(ls) {
valid = false
}
}
return valid
}
// ComputeMatchLen computes MatchLen based on match type
func (lr *Rule) ComputeMatchLen(ls *State) {
switch lr.Match {
case String:
sz := len(lr.String)
lr.MatchLen = lr.Offset + sz + lr.SizeAdj
case StrName:
sz := len(lr.String)
lr.MatchLen = lr.Offset + sz + lr.SizeAdj
case Letter:
lr.MatchLen = lr.Offset + 1 + lr.SizeAdj
case Digit:
lr.MatchLen = lr.Offset + 1 + lr.SizeAdj
case WhiteSpace:
lr.MatchLen = lr.Offset + 1 + lr.SizeAdj
case CurState:
lr.MatchLen = 0
case AnyRune:
lr.MatchLen = lr.Offset + 1 + lr.SizeAdj
}
}
// LexStart is called on the top-level lex node to start lexing process for one step
func (lr *Rule) LexStart(ls *State) *Rule {
hasGuest := ls.GuestLex != nil
cpos := ls.Pos
lxsz := len(ls.Lex)
mrule := lr
for _, klri := range lr.Children {
klr := klri.(*Rule)
if mrule = klr.Lex(ls); mrule != nil { // first to match takes it -- order matters!
break
}
}
if hasGuest && ls.GuestLex != nil && lr != ls.GuestLex {
ls.Pos = cpos // backup and undo what the standard rule did, and redo with guest..
// this is necessary to allow main lex to detect when to turn OFF the guest!
if lxsz > 0 {
ls.Lex = ls.Lex[:lxsz]
} else {
ls.Lex = nil
}
mrule = ls.GuestLex.LexStart(ls)
}
if !ls.AtEol() && cpos == ls.Pos {
ls.Error(cpos, "did not advance position -- need more rules to match current input", lr)
return nil
}
return mrule
}
// Lex tries to apply rule to given input state, returns lowest-level rule that matched, nil if none
func (lr *Rule) Lex(ls *State) *Rule {
if lr.Off || !lr.IsMatch(ls) {
return nil
}
st := ls.Pos // starting pos that we're consuming
tok := token.KeyToken{Token: lr.Token}
for _, act := range lr.Acts {
lr.DoAct(ls, act, &tok)
}
ed := ls.Pos // our ending state
if ed > st {
if tok.Token.IsKeyword() {
tok.Key = lr.String // if we matched, this is it
}
ls.Add(tok, st, ed)
if Trace {
fmt.Println("Lex:", lr.Desc, "Added token:", tok, "at:", st, ed)
}
}
if !lr.HasChildren() {
return lr
}
if lr.NameMap && lr.NmMap != nil {
nm := ls.ReadNameTmp(lr.Offset)
klr, ok := lr.NmMap[nm]
if ok {
if mrule := klr.Lex(ls); mrule != nil { // should!
return mrule
}
}
} else {
// now we iterate over our kids
for _, klri := range lr.Children {
klr := klri.(*Rule)
if mrule := klr.Lex(ls); mrule != nil { // first to match takes it -- order matters!
return mrule
}
}
}
// if kids don't match and we don't have any actions, we are just a grouper
// and thus we depend entirely on kids matching
if len(lr.Acts) == 0 {
if Trace {
fmt.Println("Lex:", lr.Desc, "fallthrough")
}
return nil
}
return lr
}
// IsMatch tests if the rule matches for current input state, returns true if so, false if not
func (lr *Rule) IsMatch(ls *State) bool {
if !lr.IsMatchPos(ls) {
return false
}
switch lr.Match {
case String:
sz := len(lr.String)
str, ok := ls.String(lr.Offset, sz)
if !ok {
return false
}
if str != lr.String {
return false
}
return true
case StrName:
nm := ls.ReadNameTmp(lr.Offset)
if nm != lr.String {
return false
}
return true
case Letter:
rn, ok := ls.Rune(lr.Offset)
if !ok {
return false
}
if IsLetter(rn) {
return true
}
return false
case Digit:
rn, ok := ls.Rune(lr.Offset)
if !ok {
return false
}
if IsDigit(rn) {
return true
}
return false
case WhiteSpace:
rn, ok := ls.Rune(lr.Offset)
if !ok {
return false
}
if IsWhiteSpace(rn) {
return true
}
return false
case CurState:
if ls.MatchState(lr.String) {
return true
}
return false
case AnyRune:
_, ok := ls.Rune(lr.Offset)
return ok
}
return false
}
// IsMatchPos tests if the rule matches position
func (lr *Rule) IsMatchPos(ls *State) bool {
lsz := len(ls.Src)
switch lr.Pos {
case AnyPos:
return true
case StartOfLine:
return ls.Pos == 0
case EndOfLine:
tsz := lr.TargetLen(ls)
return ls.Pos == lsz-1-tsz
case MiddleOfLine:
if ls.Pos == 0 {
return false
}
tsz := lr.TargetLen(ls)
return ls.Pos != lsz-1-tsz
case StartOfWord:
return ls.Pos == 0 || unicode.IsSpace(ls.Src[ls.Pos-1])
case EndOfWord:
tsz := lr.TargetLen(ls)
ep := ls.Pos + tsz
return ep == lsz || (ep+1 < lsz && unicode.IsSpace(ls.Src[ep+1]))
case MiddleOfWord:
if ls.Pos == 0 || unicode.IsSpace(ls.Src[ls.Pos-1]) {
return false
}
tsz := lr.TargetLen(ls)
ep := ls.Pos + tsz
if ep == lsz || (ep+1 < lsz && unicode.IsSpace(ls.Src[ep+1])) {
return false
}
return true
}
return true
}
// TargetLen returns the length of the target including offset
func (lr *Rule) TargetLen(ls *State) int {
switch lr.Match {
case StrName:
fallthrough
case String:
sz := len(lr.String)
return lr.Offset + sz
case Letter:
return lr.Offset + 1
case Digit:
return lr.Offset + 1
case WhiteSpace:
return lr.Offset + 1
case AnyRune:
return lr.Offset + 1
case CurState:
return 0
}
return 0
}
// DoAct performs given action
func (lr *Rule) DoAct(ls *State, act Actions, tok *token.KeyToken) {
switch act {
case Next:
ls.Next(lr.MatchLen)
case Name:
ls.ReadName()
case Number:
tok.Token = ls.ReadNumber()
case Quoted:
ls.ReadQuoted()
case QuotedRaw:
ls.ReadQuoted() // todo: raw!
case EOL:
ls.Pos = len(ls.Src)
case ReadUntil:
ls.ReadUntil(lr.Until)
ls.Pos += lr.SizeAdj
case PushState:
ls.PushState(lr.PushState)
case PopState:
ls.PopState()
case SetGuestLex:
if ls.LastName == "" {
ls.Error(ls.Pos, "SetGuestLex action requires prior Name action -- name is empty", lr)
} else {
lx := TheLanguageLexer.LexerByName(ls.LastName)
if lx != nil {
ls.GuestLex = lx
ls.SaveStack = ls.Stack.Clone()
}
}
case PopGuestLex:
if ls.SaveStack != nil {
ls.Stack = ls.SaveStack
ls.SaveStack = nil
}
ls.GuestLex = nil
}
}
///////////////////////////////////////////////////////////////////////
// Non-lexing functions
// Find looks for rules in the tree that contain given string in String or Name fields
func (lr *Rule) Find(find string) []*Rule {
var res []*Rule
lr.WalkDown(func(k tree.Node) bool {
lri := k.(*Rule)
if strings.Contains(lri.String, find) || strings.Contains(lri.Name, find) {
res = append(res, lri)
}
return true
})
return res
}
// WriteGrammar outputs the lexer rules as a formatted grammar in a BNF-like format
// it is called recursively
func (lr *Rule) WriteGrammar(writer io.Writer, depth int) {
if tree.IsRoot(lr) {
for _, k := range lr.Children {
lri := k.(*Rule)
lri.WriteGrammar(writer, depth)
}
} else {
ind := indent.Tabs(depth)
gpstr := ""
if lr.HasChildren() {
gpstr = " {"
}
offstr := ""
if lr.Pos != AnyPos {
offstr += fmt.Sprintf("@%v:", lr.Pos)
}
if lr.Offset > 0 {
offstr += fmt.Sprintf("+%d:", lr.Offset)
}
actstr := ""
if len(lr.Acts) > 0 {
actstr = "\t do: "
for _, ac := range lr.Acts {
actstr += ac.String()
if ac == PushState {
actstr += ": " + lr.PushState
} else if ac == ReadUntil {
actstr += ": \"" + lr.Until + "\""
}
actstr += "; "
}
}
if lr.Desc != "" {
fmt.Fprintf(writer, "%v// %v %v \n", ind, lr.Name, lr.Desc)
}
if (lr.Match >= Letter && lr.Match <= WhiteSpace) || lr.Match == AnyRune {
fmt.Fprintf(writer, "%v%v:\t\t %v\t\t if %v%v%v%v\n", ind, lr.Name, lr.Token, offstr, lr.Match, actstr, gpstr)
} else {
fmt.Fprintf(writer, "%v%v:\t\t %v\t\t if %v%v == \"%v\"%v%v\n", ind, lr.Name, lr.Token, offstr, lr.Match, lr.String, actstr, gpstr)
}
if lr.HasChildren() {
w := tabwriter.NewWriter(writer, 4, 4, 2, ' ', 0)
for _, k := range lr.Children {
lri := k.(*Rule)
lri.WriteGrammar(w, depth+1)
}
w.Flush()
fmt.Fprintf(writer, "%v}\n", ind)
}
}
}
// 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 lexer
// Stack is the stack for states
type Stack []string
// Top returns the state at the top of the stack
func (ss *Stack) Top() string {
sz := len(*ss)
if sz == 0 {
return ""
}
return (*ss)[sz-1]
}
// Push appends state to stack
func (ss *Stack) Push(state string) {
*ss = append(*ss, state)
}
// Pop takes state off the stack and returns it
func (ss *Stack) Pop() string {
sz := len(*ss)
if sz == 0 {
return ""
}
st := (*ss)[sz-1]
*ss = (*ss)[:sz-1]
return st
}
// Clone returns a copy of the stack
func (ss *Stack) Clone() Stack {
sz := len(*ss)
if sz == 0 {
return nil
}
cl := make(Stack, sz)
copy(cl, *ss)
return cl
}
// Reset stack
func (ss *Stack) Reset() {
*ss = nil
}
// 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 lexer
import (
"fmt"
"strings"
"unicode"
"cogentcore.org/core/base/nptime"
"cogentcore.org/core/parse/token"
)
// LanguageLexer looks up lexer for given language; implementation in parent parse package
// so we need the interface
type LanguageLexer interface {
// LexerByName returns the top-level [Rule] for given language (case invariant lookup)
LexerByName(lang string) *Rule
}
// TheLanguageLexer is the instance of LangLexer interface used to lookup lexers
// for languages -- is set in parse/languages.go
var TheLanguageLexer LanguageLexer
// State is the state maintained for lexing
type State struct {
// the current file being lex'd
Filename string
// if true, record whitespace tokens -- else ignore
KeepWS bool
// the current line of source being processed
Src []rune
// the lex output for this line
Lex Line
// the comments output for this line -- kept separately
Comments Line
// the current rune char position within the line
Pos int
// the line within overall source that we're operating on (0 indexed)
Ln int
// the current rune read by NextRune
Ch rune
// state stack
Stack Stack
// the last name that was read
LastName string
// a guest lexer that can be installed for managing a different language type, e.g., quoted text in markdown files
GuestLex *Rule
// copy of stack at point when guest lexer was installed -- restore when popped
SaveStack Stack
// time stamp for lexing -- set at start of new lex process
Time nptime.Time
// any error messages accumulated during lexing specifically
Errs ErrorList
}
// Init initializes the state at start of parsing
func (ls *State) Init() {
ls.GuestLex = nil
ls.Stack.Reset()
ls.Ln = 0
ls.SetLine(nil)
ls.SaveStack = nil
ls.Errs.Reset()
}
// SetLine sets a new line for parsing and initializes the lex output and pos
func (ls *State) SetLine(src []rune) {
ls.Src = src
ls.Lex = nil
ls.Comments = nil
ls.Pos = 0
}
// LineString returns the current lex output as tagged source
func (ls *State) LineString() string {
return fmt.Sprintf("[%v,%v]: %v", ls.Ln, ls.Pos, ls.Lex.TagSrc(ls.Src))
}
// Error adds a lexing error at given position
func (ls *State) Error(pos int, msg string, rule *Rule) {
ls.Errs.Add(Pos{ls.Ln, pos}, ls.Filename, "Lexer: "+msg, string(ls.Src), rule)
}
// AtEol returns true if current position is at end of line
func (ls *State) AtEol() bool {
sz := len(ls.Src)
return ls.Pos >= sz
}
// String gets the string at given offset and length from current position, returns false if out of range
func (ls *State) String(off, sz int) (string, bool) {
idx := ls.Pos + off
ei := idx + sz
if ei > len(ls.Src) {
return "", false
}
return string(ls.Src[idx:ei]), true
}
// Rune gets the rune at given offset from current position, returns false if out of range
func (ls *State) Rune(off int) (rune, bool) {
idx := ls.Pos + off
if idx >= len(ls.Src) {
return 0, false
}
return ls.Src[idx], true
}
// Next moves to next position using given increment in source line -- returns false if at end
func (ls *State) Next(inc int) bool {
sz := len(ls.Src)
ls.Pos += inc
if ls.Pos >= sz {
ls.Pos = sz
return false
}
return true
}
// NextRune reads the next rune into Ch and returns false if at end of line
func (ls *State) NextRune() bool {
sz := len(ls.Src)
ls.Pos++
if ls.Pos >= sz {
ls.Pos = sz
return false
}
ls.Ch = ls.Src[ls.Pos]
return true
}
// CurRune reads the current rune into Ch and returns false if at end of line
func (ls *State) CurRune() bool {
sz := len(ls.Src)
if ls.Pos >= sz {
ls.Pos = sz
return false
}
ls.Ch = ls.Src[ls.Pos]
return true
}
// Add adds a lex token for given region -- merges with prior if same
func (ls *State) Add(tok token.KeyToken, st, ed int) {
if tok.Token == token.TextWhitespace && !ls.KeepWS {
return
}
lxl := &ls.Lex
if tok.Token.Cat() == token.Comment {
lxl = &ls.Comments
}
sz := len(*lxl)
if sz > 0 && tok.Token.CombineRepeats() {
lst := &(*lxl)[sz-1]
if lst.Token.Token == tok.Token && lst.Ed == st {
lst.Ed = ed
return
}
}
lx := (*lxl).AddLex(tok, st, ed)
lx.Time = ls.Time
}
// PushState pushes state onto stack
func (ls *State) PushState(st string) {
ls.Stack.Push(st)
}
// CurState returns the current state
func (ls *State) CurState() string {
return ls.Stack.Top()
}
// PopState pops state off of stack
func (ls *State) PopState() string {
return ls.Stack.Pop()
}
// MatchState returns true if the current state matches the string
func (ls *State) MatchState(st string) bool {
sz := len(ls.Stack)
if sz == 0 {
return false
}
return ls.Stack[sz-1] == st
}
// ReadNameTmp reads a standard alpha-numeric_ name and returns it.
// Does not update the lexing position -- a "lookahead" name read
func (ls *State) ReadNameTmp(off int) string {
cp := ls.Pos
ls.Pos += off
ls.ReadName()
ls.Pos = cp
return ls.LastName
}
// ReadName reads a standard alpha-numeric_ name -- saves in LastName
func (ls *State) ReadName() {
str := ""
sz := len(ls.Src)
for ls.Pos < sz {
rn := ls.Src[ls.Pos]
if IsLetterOrDigit(rn) {
str += string(rn)
ls.Pos++
} else {
break
}
}
ls.LastName = str
}
// NextSrcLine returns the next line of text
func (ls *State) NextSrcLine() string {
if ls.AtEol() {
return "EOL"
}
return string(ls.Src[ls.Pos:])
}
// ReadUntil reads until given string(s) -- does do depth tracking if looking for a bracket
// open / close kind of symbol.
// For multiple "until" string options, separate each by |
// and use empty to match a single | or || in combination with other options.
// Terminates at end of line without error
func (ls *State) ReadUntil(until string) {
ustrs := strings.Split(until, "|")
if len(ustrs) == 0 || (len(ustrs) == 1 && len(ustrs[0]) == 0) {
ustrs = []string{"|"}
}
sz := len(ls.Src)
got := false
depth := 0
match := rune(0)
if len(ustrs) == 1 && len(ustrs[0]) == 1 {
switch ustrs[0][0] {
case '}':
match = '{'
case ')':
match = '('
case ']':
match = '['
}
}
for ls.NextRune() {
if match != 0 {
if ls.Ch == match {
depth++
continue
} else if ls.Ch == rune(ustrs[0][0]) {
if depth > 0 {
depth--
continue
}
}
if depth > 0 {
continue
}
}
for _, un := range ustrs {
usz := len(un)
if usz == 0 { // ||
if ls.Ch == '|' {
ls.NextRune() // move past
break
}
} else {
ep := ls.Pos + usz
if ep < sz && ls.Pos < ep {
sm := string(ls.Src[ls.Pos:ep])
if sm == un {
ls.Pos += usz
got = true
break
}
}
}
}
if got {
break
}
}
}
// ReadNumber reads a number of any sort, returning the type of the number
func (ls *State) ReadNumber() token.Tokens {
offs := ls.Pos
tok := token.LitNumInteger
ls.CurRune()
if ls.Ch == '0' {
// int or float
offs := ls.Pos
ls.NextRune()
if ls.Ch == 'x' || ls.Ch == 'X' {
// hexadecimal int
ls.NextRune()
ls.ScanMantissa(16)
if ls.Pos-offs <= 2 {
// only scanned "0x" or "0X"
ls.Error(offs, "illegal hexadecimal number", nil)
}
} else {
// octal int or float
seenDecimalDigit := false
ls.ScanMantissa(8)
if ls.Ch == '8' || ls.Ch == '9' {
// illegal octal int or float
seenDecimalDigit = true
ls.ScanMantissa(10)
}
if ls.Ch == '.' || ls.Ch == 'e' || ls.Ch == 'E' || ls.Ch == 'i' {
goto fraction
}
// octal int
if seenDecimalDigit {
ls.Error(offs, "illegal octal number", nil)
}
}
goto exit
}
// decimal int or float
ls.ScanMantissa(10)
fraction:
if ls.Ch == '.' {
tok = token.LitNumFloat
ls.NextRune()
ls.ScanMantissa(10)
}
if ls.Ch == 'e' || ls.Ch == 'E' {
tok = token.LitNumFloat
ls.NextRune()
if ls.Ch == '-' || ls.Ch == '+' {
ls.NextRune()
}
if DigitValue(ls.Ch) < 10 {
ls.ScanMantissa(10)
} else {
ls.Error(offs, "illegal floating-point exponent", nil)
}
}
if ls.Ch == 'i' {
tok = token.LitNumImag
ls.NextRune()
}
exit:
return tok
}
func DigitValue(ch rune) int {
switch {
case '0' <= ch && ch <= '9':
return int(ch - '0')
case 'a' <= ch && ch <= 'f':
return int(ch - 'a' + 10)
case 'A' <= ch && ch <= 'F':
return int(ch - 'A' + 10)
}
return 16 // larger than any legal digit val
}
func (ls *State) ScanMantissa(base int) {
for DigitValue(ls.Ch) < base {
if !ls.NextRune() {
break
}
}
}
func (ls *State) ReadQuoted() {
delim, _ := ls.Rune(0)
offs := ls.Pos
ls.NextRune()
for {
ch := ls.Ch
if ch == delim {
ls.NextRune() // move past
break
}
if ch == '\\' {
ls.ReadEscape(delim)
}
if !ls.NextRune() {
ls.Error(offs, "string literal not terminated", nil)
break
}
}
}
// ReadEscape parses an escape sequence where rune is the accepted
// escaped quote. In case of a syntax error, it stops at the offending
// character (without consuming it) and returns false. Otherwise
// it returns true.
func (ls *State) ReadEscape(quote rune) bool {
offs := ls.Pos
var n int
var base, max uint32
switch ls.Ch {
case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', quote:
ls.NextRune()
return true
case '0', '1', '2', '3', '4', '5', '6', '7':
n, base, max = 3, 8, 255
case 'x':
ls.NextRune()
n, base, max = 2, 16, 255
case 'u':
ls.NextRune()
n, base, max = 4, 16, unicode.MaxRune
case 'U':
ls.NextRune()
n, base, max = 8, 16, unicode.MaxRune
default:
msg := "unknown escape sequence"
if ls.Ch < 0 {
msg = "escape sequence not terminated"
}
ls.Error(offs, msg, nil)
return false
}
var x uint32
for n > 0 {
d := uint32(DigitValue(ls.Ch))
if d >= base {
msg := fmt.Sprintf("illegal character %#U in escape sequence", ls.Ch)
if ls.Ch < 0 {
msg = "escape sequence not terminated"
}
ls.Error(ls.Pos, msg, nil)
return false
}
x = x*base + d
ls.NextRune()
n--
}
if x > max || 0xD800 <= x && x < 0xE000 {
ls.Error(ls.Pos, "escape sequence is invalid Unicode code point", nil)
return false
}
return true
}
// Code generated by "core generate"; DO NOT EDIT.
package lexer
import (
"cogentcore.org/core/parse/token"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/parse/lexer.Rule", IDName: "rule", Doc: "Rule operates on the text input to produce the lexical tokens.\n\nLexing is done line-by-line -- you must push and pop states to\ncoordinate across multiple lines, e.g., for multi-line comments.\n\nThere is full access to entire line and you can decide based on future\n(offset) characters.\n\nIn general it is best to keep lexing as simple as possible and\nleave the more complex things for the parsing step.", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Off", Doc: "disable this rule -- useful for testing and exploration"}, {Name: "Desc", Doc: "description / comments about this rule"}, {Name: "Token", Doc: "the token value that this rule generates -- use None for non-terminals"}, {Name: "Match", Doc: "the lexical match that we look for to engage this rule"}, {Name: "Pos", Doc: "position where match can occur"}, {Name: "String", Doc: "if action is LexMatch, this is the string we match"}, {Name: "Offset", Doc: "offset into the input to look for a match: 0 = current char, 1 = next one, etc"}, {Name: "SizeAdj", Doc: "adjusts the size of the region (plus or minus) that is processed for the Next action -- allows broader and narrower matching relative to tagging"}, {Name: "Acts", Doc: "the action(s) to perform, in order, if there is a match -- these are performed prior to iterating over child nodes"}, {Name: "Until", Doc: "string(s) for ReadUntil action -- will read until any of these strings are found -- separate different options with | -- if you need to read until a literal | just put two || in a row and that will show up as a blank, which is interpreted as a literal |"}, {Name: "PushState", Doc: "the state to push if our action is PushState -- note that State matching is on String, not this value"}, {Name: "NameMap", Doc: "create an optimization map for this rule, which must be a parent with children that all match against a Name string -- this reads the Name and directly activates the associated rule with that String, without having to iterate through them -- use this for keywords etc -- produces a SIGNIFICANT speedup for long lists of keywords."}, {Name: "MatchLen", Doc: "length of source that matched -- if Next is called, this is what will be skipped to"}, {Name: "NmMap", Doc: "NameMap lookup map -- created during Compile"}}})
// NewRule returns a new [Rule] with the given optional parent:
// Rule operates on the text input to produce the lexical tokens.
//
// Lexing is done line-by-line -- you must push and pop states to
// coordinate across multiple lines, e.g., for multi-line comments.
//
// There is full access to entire line and you can decide based on future
// (offset) characters.
//
// In general it is best to keep lexing as simple as possible and
// leave the more complex things for the parsing step.
func NewRule(parent ...tree.Node) *Rule { return tree.New[Rule](parent...) }
// SetOff sets the [Rule.Off]:
// disable this rule -- useful for testing and exploration
func (t *Rule) SetOff(v bool) *Rule { t.Off = v; return t }
// SetDesc sets the [Rule.Desc]:
// description / comments about this rule
func (t *Rule) SetDesc(v string) *Rule { t.Desc = v; return t }
// SetToken sets the [Rule.Token]:
// the token value that this rule generates -- use None for non-terminals
func (t *Rule) SetToken(v token.Tokens) *Rule { t.Token = v; return t }
// SetMatch sets the [Rule.Match]:
// the lexical match that we look for to engage this rule
func (t *Rule) SetMatch(v Matches) *Rule { t.Match = v; return t }
// SetPos sets the [Rule.Pos]:
// position where match can occur
func (t *Rule) SetPos(v MatchPos) *Rule { t.Pos = v; return t }
// SetString sets the [Rule.String]:
// if action is LexMatch, this is the string we match
func (t *Rule) SetString(v string) *Rule { t.String = v; return t }
// SetOffset sets the [Rule.Offset]:
// offset into the input to look for a match: 0 = current char, 1 = next one, etc
func (t *Rule) SetOffset(v int) *Rule { t.Offset = v; return t }
// SetSizeAdj sets the [Rule.SizeAdj]:
// adjusts the size of the region (plus or minus) that is processed for the Next action -- allows broader and narrower matching relative to tagging
func (t *Rule) SetSizeAdj(v int) *Rule { t.SizeAdj = v; return t }
// SetActs sets the [Rule.Acts]:
// the action(s) to perform, in order, if there is a match -- these are performed prior to iterating over child nodes
func (t *Rule) SetActs(v ...Actions) *Rule { t.Acts = v; return t }
// SetUntil sets the [Rule.Until]:
// string(s) for ReadUntil action -- will read until any of these strings are found -- separate different options with | -- if you need to read until a literal | just put two || in a row and that will show up as a blank, which is interpreted as a literal |
func (t *Rule) SetUntil(v string) *Rule { t.Until = v; return t }
// SetPushState sets the [Rule.PushState]:
// the state to push if our action is PushState -- note that State matching is on String, not this value
func (t *Rule) SetPushState(v string) *Rule { t.PushState = v; return t }
// SetNameMap sets the [Rule.NameMap]:
// create an optimization map for this rule, which must be a parent with children that all match against a Name string -- this reads the Name and directly activates the associated rule with that String, without having to iterate through them -- use this for keywords etc -- produces a SIGNIFICANT speedup for long lists of keywords.
func (t *Rule) SetNameMap(v bool) *Rule { t.NameMap = v; return t }
// SetMatchLen sets the [Rule.MatchLen]:
// length of source that matched -- if Next is called, this is what will be skipped to
func (t *Rule) SetMatchLen(v int) *Rule { t.MatchLen = v; return t }
// SetNmMap sets the [Rule.NmMap]:
// NameMap lookup map -- created during Compile
func (t *Rule) SetNmMap(v map[string]*Rule) *Rule { t.NmMap = v; return t }
// Code generated by "core generate"; DO NOT EDIT.
package lsp
import (
"cogentcore.org/core/enums"
)
var _CompletionKindValues = []CompletionKind{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25}
// CompletionKindN is the highest valid value for type CompletionKind, plus one.
const CompletionKindN CompletionKind = 26
var _CompletionKindValueMap = map[string]CompletionKind{`None`: 0, `Text`: 1, `Method`: 2, `Function`: 3, `Constructor`: 4, `Field`: 5, `Variable`: 6, `Class`: 7, `Interface`: 8, `Module`: 9, `Property`: 10, `Unit`: 11, `Value`: 12, `Enum`: 13, `Keyword`: 14, `Snippet`: 15, `Color`: 16, `File`: 17, `Reference`: 18, `Folder`: 19, `EnumMember`: 20, `Constant`: 21, `Struct`: 22, `Event`: 23, `Operator`: 24, `TypeParameter`: 25}
var _CompletionKindDescMap = map[CompletionKind]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``, 5: ``, 6: ``, 7: ``, 8: ``, 9: ``, 10: ``, 11: ``, 12: ``, 13: ``, 14: ``, 15: ``, 16: ``, 17: ``, 18: ``, 19: ``, 20: ``, 21: ``, 22: ``, 23: ``, 24: ``, 25: ``}
var _CompletionKindMap = map[CompletionKind]string{0: `None`, 1: `Text`, 2: `Method`, 3: `Function`, 4: `Constructor`, 5: `Field`, 6: `Variable`, 7: `Class`, 8: `Interface`, 9: `Module`, 10: `Property`, 11: `Unit`, 12: `Value`, 13: `Enum`, 14: `Keyword`, 15: `Snippet`, 16: `Color`, 17: `File`, 18: `Reference`, 19: `Folder`, 20: `EnumMember`, 21: `Constant`, 22: `Struct`, 23: `Event`, 24: `Operator`, 25: `TypeParameter`}
// String returns the string representation of this CompletionKind value.
func (i CompletionKind) String() string { return enums.String(i, _CompletionKindMap) }
// SetString sets the CompletionKind value from its string representation,
// and returns an error if the string is invalid.
func (i *CompletionKind) SetString(s string) error {
return enums.SetString(i, s, _CompletionKindValueMap, "CompletionKind")
}
// Int64 returns the CompletionKind value as an int64.
func (i CompletionKind) Int64() int64 { return int64(i) }
// SetInt64 sets the CompletionKind value from an int64.
func (i *CompletionKind) SetInt64(in int64) { *i = CompletionKind(in) }
// Desc returns the description of the CompletionKind value.
func (i CompletionKind) Desc() string { return enums.Desc(i, _CompletionKindDescMap) }
// CompletionKindValues returns all possible values for the type CompletionKind.
func CompletionKindValues() []CompletionKind { return _CompletionKindValues }
// Values returns all possible values for the type CompletionKind.
func (i CompletionKind) Values() []enums.Enum { return enums.Values(_CompletionKindValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i CompletionKind) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *CompletionKind) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "CompletionKind")
}
var _SymbolKindValues = []SymbolKind{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}
// SymbolKindN is the highest valid value for type SymbolKind, plus one.
const SymbolKindN SymbolKind = 27
var _SymbolKindValueMap = map[string]SymbolKind{`NoSymbolKind`: 0, `File`: 1, `Module`: 2, `Namespace`: 3, `Package`: 4, `Class`: 5, `Method`: 6, `Property`: 7, `Field`: 8, `Constructor`: 9, `Enum`: 10, `Interface`: 11, `Function`: 12, `Variable`: 13, `Constant`: 14, `String`: 15, `Number`: 16, `Boolean`: 17, `Array`: 18, `Object`: 19, `Key`: 20, `Null`: 21, `EnumMember`: 22, `Struct`: 23, `Event`: 24, `Operator`: 25, `TypeParameter`: 26}
var _SymbolKindDescMap = map[SymbolKind]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``, 5: ``, 6: ``, 7: ``, 8: ``, 9: ``, 10: ``, 11: ``, 12: ``, 13: ``, 14: ``, 15: ``, 16: ``, 17: ``, 18: ``, 19: ``, 20: ``, 21: ``, 22: ``, 23: ``, 24: ``, 25: ``, 26: ``}
var _SymbolKindMap = map[SymbolKind]string{0: `NoSymbolKind`, 1: `File`, 2: `Module`, 3: `Namespace`, 4: `Package`, 5: `Class`, 6: `Method`, 7: `Property`, 8: `Field`, 9: `Constructor`, 10: `Enum`, 11: `Interface`, 12: `Function`, 13: `Variable`, 14: `Constant`, 15: `String`, 16: `Number`, 17: `Boolean`, 18: `Array`, 19: `Object`, 20: `Key`, 21: `Null`, 22: `EnumMember`, 23: `Struct`, 24: `Event`, 25: `Operator`, 26: `TypeParameter`}
// String returns the string representation of this SymbolKind value.
func (i SymbolKind) String() string { return enums.String(i, _SymbolKindMap) }
// SetString sets the SymbolKind value from its string representation,
// and returns an error if the string is invalid.
func (i *SymbolKind) SetString(s string) error {
return enums.SetString(i, s, _SymbolKindValueMap, "SymbolKind")
}
// Int64 returns the SymbolKind value as an int64.
func (i SymbolKind) Int64() int64 { return int64(i) }
// SetInt64 sets the SymbolKind value from an int64.
func (i *SymbolKind) SetInt64(in int64) { *i = SymbolKind(in) }
// Desc returns the description of the SymbolKind value.
func (i SymbolKind) Desc() string { return enums.Desc(i, _SymbolKindDescMap) }
// SymbolKindValues returns all possible values for the type SymbolKind.
func SymbolKindValues() []SymbolKind { return _SymbolKindValues }
// Values returns all possible values for the type SymbolKind.
func (i SymbolKind) Values() []enums.Enum { return enums.Values(_SymbolKindValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i SymbolKind) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *SymbolKind) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "SymbolKind")
}
// 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 lsp contains types for the Language Server Protocol
// LSP: https://microsoft.github.io/language-server-protocol/specification
// and mappings from these elements into the token.Tokens types
// which are used internally in parse.
package lsp
//go:generate core generate
import (
"cogentcore.org/core/parse/token"
)
// SymbolKind is the Language Server Protocol (LSP) SymbolKind, which
// we map onto the token.Tokens that are used internally.
type SymbolKind int32 //enums:enum
// SymbolKind is the list of SymbolKind items from LSP
const (
NoSymbolKind SymbolKind = iota
File // 1 in LSP
Module
Namespace
Package
Class
Method
Property
Field
Constructor
Enum
Interface
Function
Variable
Constant
String
Number
Boolean
Array
Object
Key
Null
EnumMember
Struct
Event
Operator
TypeParameter // 26 in LSP
)
// SymbolKindTokenMap maps between symbols and token.Tokens
var SymbolKindTokenMap = map[SymbolKind]token.Tokens{
Module: token.NameModule,
Namespace: token.NameNamespace,
Package: token.NamePackage,
Class: token.NameClass,
Method: token.NameMethod,
Property: token.NameProperty,
Field: token.NameField,
Constructor: token.NameConstructor,
Enum: token.NameEnum,
Interface: token.NameInterface,
Function: token.NameFunction,
Variable: token.NameVar,
Constant: token.NameConstant,
String: token.LitStr,
Number: token.LitNum,
Boolean: token.LiteralBool,
Array: token.NameArray,
Object: token.NameObject,
Key: token.NameTag,
Null: token.None,
EnumMember: token.NameEnumMember,
Struct: token.NameStruct,
Event: token.NameEvent,
Operator: token.Operator,
TypeParameter: token.NameTypeParam,
}
// TokenSymbolKindMap maps from tokens to LSP SymbolKind
var TokenSymbolKindMap map[token.Tokens]SymbolKind
func init() {
for s, t := range SymbolKindTokenMap {
TokenSymbolKindMap[t] = s
}
}
// 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 parse
//go:generate core generate -add-types
import (
"encoding/json"
"fmt"
"os"
"time"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/iox/jsonx"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/parser"
)
// Parser is the overall parser for managing the parsing
type Parser struct {
// lexer rules for first pass of lexing file
Lexer *lexer.Rule
// second pass after lexing -- computes nesting depth and EOS finding
PassTwo lexer.PassTwo
// parser rules for parsing lexed tokens
Parser *parser.Rule
// file name for overall parser (not file being parsed!)
Filename string
// if true, reports errors after parsing, to stdout
ReportErrs bool
// when loaded from file, this is the modification time of the parser -- re-processes cache if parser is newer than cached files
ModTime time.Time `json:"-" xml:"-"`
}
// Init initializes the parser -- must be called after creation
func (pr *Parser) Init() {
pr.Lexer = lexer.NewRule()
pr.Parser = parser.NewRule()
}
// NewParser returns a new initialized parser
func NewParser() *Parser {
pr := &Parser{}
pr.Init()
return pr
}
// InitAll initializes everything about the parser -- call this when setting up a new
// parser after it has been loaded etc
func (pr *Parser) InitAll() {
fs := &FileState{} // dummy, for error recording
fs.Init()
pr.Lexer.CompileAll(&fs.LexState)
pr.Lexer.Validate(&fs.LexState)
pr.Parser.CompileAll(&fs.ParseState)
pr.Parser.Validate(&fs.ParseState)
}
// LexInit gets the lexer ready to start lexing
func (pr *Parser) LexInit(fs *FileState) {
fs.LexState.Init()
fs.LexState.Time.Now()
fs.TwoState.Init()
if fs.Src.NLines() > 0 {
fs.LexState.SetLine(fs.Src.Lines[0])
}
}
// LexNext does next step of lexing -- returns lowest-level rule that
// matched, and nil when nomatch err or at end of source input
func (pr *Parser) LexNext(fs *FileState) *lexer.Rule {
if fs.LexState.Ln >= fs.Src.NLines() {
return nil
}
for {
if fs.LexState.AtEol() {
fs.Src.SetLine(fs.LexState.Ln, fs.LexState.Lex, fs.LexState.Comments, fs.LexState.Stack)
fs.LexState.Ln++
if fs.LexState.Ln >= fs.Src.NLines() {
return nil
}
fs.LexState.SetLine(fs.Src.Lines[fs.LexState.Ln])
}
mrule := pr.Lexer.LexStart(&fs.LexState)
if mrule != nil {
return mrule
}
if !fs.LexState.AtEol() { // err
break
}
}
return nil
}
// LexNextLine does next line of lexing -- returns lowest-level rule that
// matched at end, and nil when nomatch err or at end of source input
func (pr *Parser) LexNextLine(fs *FileState) *lexer.Rule {
if fs.LexState.Ln >= fs.Src.NLines() {
return nil
}
var mrule *lexer.Rule
for {
if fs.LexState.AtEol() {
fs.Src.SetLine(fs.LexState.Ln, fs.LexState.Lex, fs.LexState.Comments, fs.LexState.Stack)
fs.LexState.Ln++
if fs.LexState.Ln >= fs.Src.NLines() {
return nil
}
fs.LexState.SetLine(fs.Src.Lines[fs.LexState.Ln])
return mrule
}
mrule = pr.Lexer.LexStart(&fs.LexState)
if mrule == nil {
return nil
}
}
}
// LexRun keeps running LextNext until it stops
func (pr *Parser) LexRun(fs *FileState) {
for {
if pr.LexNext(fs) == nil {
break
}
}
}
// LexLine runs lexer for given single line of source, which is updated
// from the given text (if non-nil)
// Returns merged regular and token comment lines, cloned and ready for use.
func (pr *Parser) LexLine(fs *FileState, ln int, txt []rune) lexer.Line {
nlines := fs.Src.NLines()
if ln >= nlines || ln < 0 {
return nil
}
fs.Src.SetLineSrc(ln, txt)
fs.LexState.SetLine(fs.Src.Lines[ln])
pst := fs.Src.PrevStack(ln)
fs.LexState.Stack = pst.Clone()
for !fs.LexState.AtEol() {
mrule := pr.Lexer.LexStart(&fs.LexState)
if mrule == nil {
break
}
}
initDepth := fs.Src.PrevDepth(ln)
pr.PassTwo.NestDepthLine(fs.LexState.Lex, initDepth) // important to set this one's depth
fs.Src.SetLine(ln, fs.LexState.Lex, fs.LexState.Comments, fs.LexState.Stack) // before saving here
fs.TwoState.SetSrc(&fs.Src)
fs.Src.EosPos[ln] = nil // reset eos
pr.PassTwo.EosDetectPos(&fs.TwoState, lexer.Pos{Ln: ln}, 1)
merge := lexer.MergeLines(fs.LexState.Lex, fs.LexState.Comments)
mc := merge.Clone()
if len(fs.LexState.Comments) > 0 {
pr.PassTwo.NestDepthLine(mc, initDepth)
}
return mc
}
// DoPassTwo does the second pass after lexing
func (pr *Parser) DoPassTwo(fs *FileState) {
fs.TwoState.SetSrc(&fs.Src)
pr.PassTwo.NestDepth(&fs.TwoState)
if pr.PassTwo.DoEos {
pr.PassTwo.EosDetect(&fs.TwoState)
}
}
// LexAll runs a complete pass of the lexer and pass two, on current state
func (pr *Parser) LexAll(fs *FileState) {
pr.LexInit(fs)
// lprf := profile.Start("LexRun") // quite fast now..
pr.LexRun(fs)
// fs.LexErrReport()
// lprf.End()
pr.DoPassTwo(fs) // takes virtually no time
}
// ParserInit initializes the parser prior to running
func (pr *Parser) ParserInit(fs *FileState) bool {
fs.AnonCtr = 0
fs.ParseState.Init(&fs.Src, fs.AST)
return true
}
// ParseNext does next step of parsing -- returns lowest-level rule that matched
// or nil if no match error or at end
func (pr *Parser) ParseNext(fs *FileState) *parser.Rule {
mrule := pr.Parser.StartParse(&fs.ParseState)
return mrule
}
// ParseRun continues running the parser until the end of the file
func (pr *Parser) ParseRun(fs *FileState) {
for {
pr.Parser.StartParse(&fs.ParseState)
if fs.ParseState.AtEofNext() {
break
}
}
}
// ParseAll does full parsing, including ParseInit and ParseRun, assuming LexAll
// has been done already
func (pr *Parser) ParseAll(fs *FileState) {
if !pr.ParserInit(fs) {
return
}
pr.ParseRun(fs)
if pr.ReportErrs {
if fs.ParseHasErrs() {
fmt.Println(fs.ParseErrReport())
}
}
}
// ParseLine runs parser for given single line of source
// does Parsing in a separate FileState and returns that with
// AST etc (or nil if nothing). Assumes LexLine has already
// been run on given line.
func (pr *Parser) ParseLine(fs *FileState, ln int) *FileState {
nlines := fs.Src.NLines()
if ln >= nlines || ln < 0 {
return nil
}
lfs := NewFileState()
lfs.Src.InitFromLine(&fs.Src, ln)
lfs.Src.EnsureFinalEos(0)
lfs.ParseState.Init(&lfs.Src, lfs.AST)
pr.ParseRun(lfs)
return lfs
}
// ParseString runs lexer and parser on given string of text, returning
// FileState of results (can be nil if string is empty or no lexical tokens).
// Also takes supporting contextual info for file / language that this string
// is associated with (only for reference)
func (pr *Parser) ParseString(str string, fname string, sup fileinfo.Known) *FileState {
if str == "" {
return nil
}
lfs := NewFileState()
lfs.Src.InitFromString(str, fname, sup)
// lfs.ParseState.Trace.FullOn()
// lfs.ParseSTate.Trace.StdOut()
lfs.ParseState.Init(&lfs.Src, lfs.AST)
pr.LexAll(lfs)
lxs := lfs.Src.Lexs[0]
if len(lxs) == 0 {
return nil
}
lfs.Src.EnsureFinalEos(0)
pr.ParseAll(lfs)
return lfs
}
// ReadJSON opens lexer and parser rules from Bytes, in a standard JSON-formatted file
func (pr *Parser) ReadJSON(b []byte) error {
err := json.Unmarshal(b, pr)
return errors.Log(err)
}
// OpenJSON opens lexer and parser rules from the given filename, in a standard JSON-formatted file
func (pr *Parser) OpenJSON(filename string) error {
err := jsonx.Open(pr, filename)
return errors.Log(err)
}
// SaveJSON saves lexer and parser rules, in a standard JSON-formatted file
func (pr *Parser) SaveJSON(filename string) error {
err := jsonx.Save(pr, filename)
return errors.Log(err)
}
// SaveGrammar saves lexer and parser grammar rules to BNF-like .parsegrammar file
func (pr *Parser) SaveGrammar(filename string) error {
ofl, err := os.Create(filename)
if err != nil {
return errors.Log(err)
}
fmt.Fprintf(ofl, "// %v Lexer\n\n", filename)
pr.Lexer.WriteGrammar(ofl, 0)
fmt.Fprintf(ofl, "\n\n///////////////////////////////////////////////////\n// %v Parser\n\n", filename)
pr.Parser.WriteGrammar(ofl, 0)
return ofl.Close()
}
// 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 parser
import (
"fmt"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/token"
)
// Actions are parsing actions to perform
type Actions int32 //enums:enum
// The parsing acts
const (
// ChangeToken changes the token to the Tok specified in the Act action
ChangeToken Actions = iota
// AddSymbol means add name as a symbol, using current scoping and token type
// or the token specified in the Act action if != None
AddSymbol
// PushScope means look for an existing symbol of given name
// to push onto current scope -- adding a new one if not found --
// does not add new item to overall symbol list. This is useful
// for e.g., definitions of methods on a type, where this is not
// the definition of the type itself.
PushScope
// PushNewScope means add a new symbol to the list and also push
// onto scope stack, using given token type or the token specified
// in the Act action if != None
PushNewScope
// PopScope means remove the most recently added scope item
PopScope
// PopScopeReg means remove the most recently added scope item, and also
// updates the source region for that item based on final SrcReg from
// corresponding AST node -- for "definitional" scope
PopScopeReg
// AddDetail adds src at given path as detail info for the last-added symbol
// if there is already something there, a space is added for this new addition
AddDetail
// AddType Adds a type with the given name -- sets the AST node for this rule
// and actual type is resolved later in a second language-specific pass
AddType
// PushStack adds name to stack -- provides context-sensitivity option for
// optimizing and ambiguity resolution
PushStack
// PopStack pops the stack
PopStack
)
// Act is one action to perform, operating on the AST output
type Act struct {
// at what point during sequence of sub-rules / tokens should this action be run? -1 = at end, 0 = before first rule, 1 = before second rule, etc -- must be at point when relevant AST nodes have been added, but for scope setting, must be early enough so that scope is present
RunIndex int
// what action to perform
Act Actions
// AST path, relative to current node: empty = current node; specifies a child node by index, and a name specifies it by name -- include name/name for sub-nodes etc -- multiple path options can be specified by | or & and will be tried in order until one succeeds (for |) or all that succeed will be used for &. ... means use all nodes with given name (only for change token) -- for PushStack, this is what to push on the stack
Path string `width:"50"`
// for ChangeToken, the new token type to assign to token at given path
Token token.Tokens
// for ChangeToken, only change if token is this to start with (only if != None))
FromToken token.Tokens
}
// String satisfies fmt.Stringer interface
func (ac Act) String() string {
if ac.FromToken != token.None {
return fmt.Sprintf(`%v:%v:"%v":%v<-%v`, ac.RunIndex, ac.Act, ac.Path, ac.Token, ac.FromToken)
} else {
return fmt.Sprintf(`%v:%v:"%v":%v`, ac.RunIndex, ac.Act, ac.Path, ac.Token)
}
}
// ChangeToken changes the token type, using FromToken logic
func (ac *Act) ChangeToken(lx *lexer.Lex) {
if ac.FromToken == token.None {
lx.Token.Token = ac.Token
return
}
if lx.Token.Token != ac.FromToken {
return
}
lx.Token.Token = ac.Token
}
// Acts are multiple actions
type Acts []Act
// String satisfies fmt.Stringer interface
func (ac Acts) String() string {
if len(ac) == 0 {
return ""
}
str := "{ "
for i := range ac {
str += ac[i].String() + "; "
}
str += "}"
return str
}
// ASTActs are actions to perform on the [AST] nodes
type ASTActs int32 //enums:enum
// The [AST] actions
const (
// NoAST means don't create an AST node for this rule
NoAST ASTActs = iota
// AddAST means create an AST node for this rule, adding it to the current anchor AST.
// Any sub-rules within this rule are *not* added as children of this node -- see
// SubAST and AnchorAST. This is good for token-only terminal nodes and list elements
// that should be added to a list.
AddAST
// SubAST means create an AST node and add all the elements of *this rule* as
// children of this new node (including sub-rules), *except* for the very last rule
// which is assumed to be a recursive rule -- that one goes back up to the parent node.
// This is good for adding more complex elements with sub-rules to a recursive list,
// without creating a new hierarchical depth level for every such element.
SubAST
// AnchorAST means create an AST node and set it as the anchor that subsequent
// sub-nodes are added into. This is for a new hierarchical depth level
// where everything under this rule gets organized.
AnchorAST
// AnchorFirstAST means create an AST node and set it as the anchor that subsequent
// sub-nodes are added into, *only* if this is the first time that this rule has
// matched within the current sequence (i.e., if the parent of this rule is the same
// rule then don't add a new AST node). This is good for starting a new list
// of recursively defined elements, without creating increasing depth levels.
AnchorFirstAST
)
// 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 parse does the parsing stage after lexing
package parser
//go:generate core generate
import (
"fmt"
"io"
"cogentcore.org/core/base/indent"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/syms"
"cogentcore.org/core/tree"
)
// AST is a node in the abstract syntax tree generated by the parsing step
// the name of the node (from tree.NodeBase) is the type of the element
// (e.g., expr, stmt, etc)
// These nodes are generated by the parser.Rule's by matching tokens
type AST struct {
tree.NodeBase
// region in source lexical tokens corresponding to this AST node -- Ch = index in lex lines
TokReg lexer.Reg `set:"-"`
// region in source file corresponding to this AST node
SrcReg lexer.Reg `set:"-"`
// source code corresponding to this AST node
Src string `set:"-"`
// stack of symbols created for this node
Syms syms.SymStack `set:"-"`
}
func (ast *AST) Destroy() {
ast.Syms.ClearAST()
ast.Syms = nil
ast.NodeBase.Destroy()
}
// ChildAST returns the Child at given index as an AST.
// Will panic if index is invalid -- use Try if unsure.
func (ast *AST) ChildAST(idx int) *AST {
return ast.Child(idx).(*AST)
}
// ParentAST returns the Parent as an AST.
func (ast *AST) ParentAST() *AST {
if ast.Parent == nil {
return nil
}
pn := ast.Parent.AsTree().This
if pn == nil {
return nil
}
return pn.(*AST)
}
// NextAST returns the next node in the AST tree, or nil if none
func (ast *AST) NextAST() *AST {
nxti := tree.Next(ast)
if nxti == nil {
return nil
}
return nxti.(*AST)
}
// NextSiblingAST returns the next sibling node in the AST tree, or nil if none
func (ast *AST) NextSiblingAST() *AST {
nxti := tree.NextSibling(ast)
if nxti == nil {
return nil
}
return nxti.(*AST)
}
// PrevAST returns the previous node in the AST tree, or nil if none
func (ast *AST) PrevAST() *AST {
nxti := tree.Previous(ast)
if nxti == nil {
return nil
}
return nxti.(*AST)
}
// SetTokReg sets the token region for this rule to given region
func (ast *AST) SetTokReg(reg lexer.Reg, src *lexer.File) {
ast.TokReg = reg
ast.SrcReg = src.TokenSrcReg(ast.TokReg)
ast.Src = src.RegSrc(ast.SrcReg)
}
// SetTokRegEnd updates the ending token region to given position --
// token regions are typically over-extended and get narrowed as tokens actually match
func (ast *AST) SetTokRegEnd(pos lexer.Pos, src *lexer.File) {
ast.TokReg.Ed = pos
ast.SrcReg = src.TokenSrcReg(ast.TokReg)
ast.Src = src.RegSrc(ast.SrcReg)
}
// WriteTree writes the AST tree data to the writer -- not attempting to re-render
// source code -- just for debugging etc
func (ast *AST) WriteTree(out io.Writer, depth int) {
ind := indent.Tabs(depth)
fmt.Fprintf(out, "%v%v: %v\n", ind, ast.Name, ast.Src)
for _, k := range ast.Children {
ai := k.(*AST)
ai.WriteTree(out, depth+1)
}
}
// Code generated by "core generate"; DO NOT EDIT.
package parser
import (
"cogentcore.org/core/enums"
)
var _ActionsValues = []Actions{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// ActionsN is the highest valid value for type Actions, plus one.
const ActionsN Actions = 10
var _ActionsValueMap = map[string]Actions{`ChangeToken`: 0, `AddSymbol`: 1, `PushScope`: 2, `PushNewScope`: 3, `PopScope`: 4, `PopScopeReg`: 5, `AddDetail`: 6, `AddType`: 7, `PushStack`: 8, `PopStack`: 9}
var _ActionsDescMap = map[Actions]string{0: `ChangeToken changes the token to the Tok specified in the Act action`, 1: `AddSymbol means add name as a symbol, using current scoping and token type or the token specified in the Act action if != None`, 2: `PushScope means look for an existing symbol of given name to push onto current scope -- adding a new one if not found -- does not add new item to overall symbol list. This is useful for e.g., definitions of methods on a type, where this is not the definition of the type itself.`, 3: `PushNewScope means add a new symbol to the list and also push onto scope stack, using given token type or the token specified in the Act action if != None`, 4: `PopScope means remove the most recently added scope item`, 5: `PopScopeReg means remove the most recently added scope item, and also updates the source region for that item based on final SrcReg from corresponding AST node -- for "definitional" scope`, 6: `AddDetail adds src at given path as detail info for the last-added symbol if there is already something there, a space is added for this new addition`, 7: `AddType Adds a type with the given name -- sets the AST node for this rule and actual type is resolved later in a second language-specific pass`, 8: `PushStack adds name to stack -- provides context-sensitivity option for optimizing and ambiguity resolution`, 9: `PopStack pops the stack`}
var _ActionsMap = map[Actions]string{0: `ChangeToken`, 1: `AddSymbol`, 2: `PushScope`, 3: `PushNewScope`, 4: `PopScope`, 5: `PopScopeReg`, 6: `AddDetail`, 7: `AddType`, 8: `PushStack`, 9: `PopStack`}
// String returns the string representation of this Actions value.
func (i Actions) String() string { return enums.String(i, _ActionsMap) }
// SetString sets the Actions value from its string representation,
// and returns an error if the string is invalid.
func (i *Actions) SetString(s string) error {
return enums.SetString(i, s, _ActionsValueMap, "Actions")
}
// Int64 returns the Actions value as an int64.
func (i Actions) Int64() int64 { return int64(i) }
// SetInt64 sets the Actions value from an int64.
func (i *Actions) SetInt64(in int64) { *i = Actions(in) }
// Desc returns the description of the Actions value.
func (i Actions) Desc() string { return enums.Desc(i, _ActionsDescMap) }
// ActionsValues returns all possible values for the type Actions.
func ActionsValues() []Actions { return _ActionsValues }
// Values returns all possible values for the type Actions.
func (i Actions) Values() []enums.Enum { return enums.Values(_ActionsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Actions) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Actions) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Actions") }
var _ASTActsValues = []ASTActs{0, 1, 2, 3, 4}
// ASTActsN is the highest valid value for type ASTActs, plus one.
const ASTActsN ASTActs = 5
var _ASTActsValueMap = map[string]ASTActs{`NoAST`: 0, `AddAST`: 1, `SubAST`: 2, `AnchorAST`: 3, `AnchorFirstAST`: 4}
var _ASTActsDescMap = map[ASTActs]string{0: `NoAST means don't create an AST node for this rule`, 1: `AddAST means create an AST node for this rule, adding it to the current anchor AST. Any sub-rules within this rule are *not* added as children of this node -- see SubAST and AnchorAST. This is good for token-only terminal nodes and list elements that should be added to a list.`, 2: `SubAST means create an AST node and add all the elements of *this rule* as children of this new node (including sub-rules), *except* for the very last rule which is assumed to be a recursive rule -- that one goes back up to the parent node. This is good for adding more complex elements with sub-rules to a recursive list, without creating a new hierarchical depth level for every such element.`, 3: `AnchorAST means create an AST node and set it as the anchor that subsequent sub-nodes are added into. This is for a new hierarchical depth level where everything under this rule gets organized.`, 4: `AnchorFirstAST means create an AST node and set it as the anchor that subsequent sub-nodes are added into, *only* if this is the first time that this rule has matched within the current sequence (i.e., if the parent of this rule is the same rule then don't add a new AST node). This is good for starting a new list of recursively defined elements, without creating increasing depth levels.`}
var _ASTActsMap = map[ASTActs]string{0: `NoAST`, 1: `AddAST`, 2: `SubAST`, 3: `AnchorAST`, 4: `AnchorFirstAST`}
// String returns the string representation of this ASTActs value.
func (i ASTActs) String() string { return enums.String(i, _ASTActsMap) }
// SetString sets the ASTActs value from its string representation,
// and returns an error if the string is invalid.
func (i *ASTActs) SetString(s string) error {
return enums.SetString(i, s, _ASTActsValueMap, "ASTActs")
}
// Int64 returns the ASTActs value as an int64.
func (i ASTActs) Int64() int64 { return int64(i) }
// SetInt64 sets the ASTActs value from an int64.
func (i *ASTActs) SetInt64(in int64) { *i = ASTActs(in) }
// Desc returns the description of the ASTActs value.
func (i ASTActs) Desc() string { return enums.Desc(i, _ASTActsDescMap) }
// ASTActsValues returns all possible values for the type ASTActs.
func ASTActsValues() []ASTActs { return _ASTActsValues }
// Values returns all possible values for the type ASTActs.
func (i ASTActs) Values() []enums.Enum { return enums.Values(_ASTActsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i ASTActs) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *ASTActs) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "ASTActs") }
var _StepsValues = []Steps{0, 1, 2, 3, 4}
// StepsN is the highest valid value for type Steps, plus one.
const StepsN Steps = 5
var _StepsValueMap = map[string]Steps{`Match`: 0, `SubMatch`: 1, `NoMatch`: 2, `Run`: 3, `RunAct`: 4}
var _StepsDescMap = map[Steps]string{0: `Match happens when a rule matches`, 1: `SubMatch is when a sub-rule within a rule matches`, 2: `NoMatch is when the rule fails to match (recorded at first non-match, which terminates matching process`, 3: `Run is when the rule is running and iterating through its sub-rules`, 4: `RunAct is when the rule is running and performing actions`}
var _StepsMap = map[Steps]string{0: `Match`, 1: `SubMatch`, 2: `NoMatch`, 3: `Run`, 4: `RunAct`}
// String returns the string representation of this Steps value.
func (i Steps) String() string { return enums.String(i, _StepsMap) }
// SetString sets the Steps value from its string representation,
// and returns an error if the string is invalid.
func (i *Steps) SetString(s string) error { return enums.SetString(i, s, _StepsValueMap, "Steps") }
// Int64 returns the Steps value as an int64.
func (i Steps) Int64() int64 { return int64(i) }
// SetInt64 sets the Steps value from an int64.
func (i *Steps) SetInt64(in int64) { *i = Steps(in) }
// Desc returns the description of the Steps value.
func (i Steps) Desc() string { return enums.Desc(i, _StepsDescMap) }
// StepsValues returns all possible values for the type Steps.
func StepsValues() []Steps { return _StepsValues }
// Values returns all possible values for the type Steps.
func (i Steps) Values() []enums.Enum { return enums.Values(_StepsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Steps) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Steps) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Steps") }
// 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 parser does the parsing stage after lexing, using a top-down recursive-descent
// (TDRD) strategy, with a special reverse mode to deal with left-associative binary expressions
// which otherwise end up being right-associative for TDRD parsing.
// Higher-level rules provide scope to lower-level ones, with a special EOS end-of-statement
// scope recognized for
package parser
import (
"fmt"
"io"
"strconv"
"strings"
"text/tabwriter"
"cogentcore.org/core/base/indent"
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/syms"
"cogentcore.org/core/parse/token"
"cogentcore.org/core/tree"
)
// Set GUIActive to true if the gui (parseview) is active -- ensures that the
// AST tree is updated when nodes are swapped in reverse mode, and maybe
// other things
var GUIActive = false
// DepthLimit is the infinite recursion prevention cutoff
var DepthLimit = 10000
// parser.Rule operates on the lexically tokenized input, not the raw source.
//
// The overall strategy is pragmatically based on the current known form of
// most languages, which are organized around a sequence of statements having
// a clear scoping defined by the EOS (end of statement), which is identified
// in a first pass through tokenized output in PassTwo.
//
// We use a top-down, recursive-descent style parsing, with flexible lookahead
// based on scoping provided by the EOS tags. Each rule progressively scopes
// down the space, using token matches etc to bracket the space for flexible
// elements.
//
// There are two different rule types:
// 1. Parents with multiple children (i.e. Groups), which are all the different
// variations for satisfying that rule, with precedence encoded directly in the
// ordering of the children. These have empty "Rule" string and Rules.
// 2. Explicit rules specified in the Rule string.
// The first step is matching which searches in order for matches within the
// children of parent nodes, and for explicit rule nodes, it looks first
// through all the explicit tokens in the rule. If there are no explicit tokens
// then matching defers to ONLY the first node listed by default -- you can
// add a @ prefix to indicate a rule that is also essential to match.
//
// After a rule matches, it then proceeds through the rules narrowing the scope
// and calling the sub-nodes..
type Rule struct {
tree.NodeBase
// disable this rule -- useful for testing and exploration
Off bool `json:",omitempty"`
// description / comments about this rule
Desc string `json:",omitempty"`
// the rule as a space-separated list of rule names and token(s) -- use single quotes around 'tokens' (using token.Tokens names or symbols). For keywords use 'key:keyword'. All tokens are matched at the same nesting depth as the start of the scope of this rule, unless they have a +D relative depth value differential before the token. Use @ prefix for a sub-rule to require that rule to match -- by default explicit tokens are used if available, and then only the first sub-rule failing that. Use ! by itself to define start of an exclusionary rule -- doesn't match when those rule elements DO match. Use : prefix for a special group node that matches a single token at start of scope, and then defers to the child rules to perform full match -- this is used for FirstTokenMap when there are multiple versions of a given keyword rule. Use - prefix for tokens anchored by the end (next token) instead of the previous one -- typically just for token prior to 'EOS' but also a block of tokens that need to go backward in the middle of a sequence to avoid ambiguity can be marked with -
Rule string
// if present, this rule only fires if stack has this on it
StackMatch string `json:",omitempty"`
// what action should be take for this node when it matches
AST ASTActs
// actions to perform based on parsed AST tree data, when this rule is done executing
Acts Acts `json:",omitempty"`
// for group-level rules having lots of children and lots of recursiveness, and also of high-frequency, when we first encounter such a rule, make a map of all the tokens in the entire scope, and use that for a first-pass rejection on matching tokens
OptTokenMap bool `json:",omitempty"`
// for group-level rules with a number of rules that match based on first tokens / keywords, build map to directly go to that rule -- must also organize all of these rules sequentially from the start -- if no match, goes directly to first non-lookup case
FirstTokenMap bool `json:",omitempty"`
// rule elements compiled from Rule string
Rules RuleList `json:"-" xml:"-"`
// strategic matching order for matching the rules
Order []int `edit:"-" json:"-" xml:"-"`
// map from first tokens / keywords to rules for FirstTokenMap case
FiTokenMap map[string]*Rule `edit:"-" json:"-" xml:"-" set:"-"`
// for FirstTokenMap, the start of the else cases not covered by the map
FiTokenElseIndex int `edit:"-" json:"-" xml:"-" set:"-"`
// exclusionary key index -- this is the token in Rules that we need to exclude matches for using ExclFwd and ExclRev rules
ExclKeyIndex int `edit:"-" json:"-" xml:"-" set:"-"`
// exclusionary forward-search rule elements compiled from Rule string
ExclFwd RuleList `edit:"-" json:"-" xml:"-" set:"-"`
// exclusionary reverse-search rule elements compiled from Rule string
ExclRev RuleList `edit:"-" json:"-" xml:"-" set:"-"`
// Bool flags:
// setsScope means that this rule sets its own scope, because it ends with EOS
setsScope bool
// reverse means that this rule runs in reverse (starts with - sign) -- for arithmetic
// binary expressions only: this is needed to produce proper associativity result for
// mathematical expressions in the recursive descent parser.
// Only for rules of form: Expr '+' Expr -- two sub-rules with a token operator
// in the middle.
reverse bool
// noTokens means that this rule doesn't have any explicit tokens -- only refers to
// other rules
noTokens bool
// onlyTokens means that this rule only has explicit tokens for matching -- can be
// optimized
onlyTokens bool
// tokenMatchGroup is a group node that also has a single token match, so it can
// be used in a FirstTokenMap to optimize lookup of rules
tokenMatchGroup bool
}
// RuleEl is an element of a parsing rule -- either a pointer to another rule or a token
type RuleEl struct {
// sub-rule for this position -- nil if token
Rule *Rule
// token, None if rule
Token token.KeyToken
// start increment for matching -- this is the number of non-optional, non-match items between (start | last match) and this item -- increments start region for matching
StInc int
// if true, this rule must match for rule to fire -- by default only tokens and, failing that, the first sub-rule is used for matching -- use @ to require a match
Match bool
// this rule is optional -- will absorb tokens if they exist -- indicated with ? prefix
Opt bool
// match this rule working backward from the next token -- triggered by - (minus) prefix and optimizes cases where there can be a lot of tokens going forward but few going from end -- must be anchored by a terminal EOS or other FromNext elements and is ignored if at the very end
FromNext bool
}
func (re RuleEl) IsRule() bool {
return re.Rule != nil
}
func (re RuleEl) IsToken() bool {
return re.Rule == nil
}
// RuleList is a list (slice) of rule elements
type RuleList []RuleEl
// Last returns the last rule -- only used in cases where there are rules
func (rl RuleList) Last() *RuleEl {
return &rl[len(rl)-1]
}
// RuleMap is a map of all the rule names, for quick lookup
var RuleMap map[string]*Rule
// Matches encodes the regions of each match, Err for no match
type Matches []lexer.Reg
// StartEnd returns the first and last non-zero positions in the Matches list as a region
func (mm Matches) StartEnd() lexer.Reg {
reg := lexer.RegZero
for _, mp := range mm {
if mp.St != lexer.PosZero {
if reg.St == lexer.PosZero {
reg.St = mp.St
}
reg.Ed = mp.Ed
}
}
return reg
}
// StartEndExcl returns the first and last non-zero positions in the Matches list as a region
// moves the end to next toke to make it the usual exclusive end pos
func (mm Matches) StartEndExcl(ps *State) lexer.Reg {
reg := mm.StartEnd()
reg.Ed, _ = ps.Src.NextTokenPos(reg.Ed)
return reg
}
///////////////////////////////////////////////////////////////////////
// Rule
// IsGroup returns true if this node is a group, else it should have rules
func (pr *Rule) IsGroup() bool {
return pr.HasChildren()
}
// SetRuleMap is called on the top-level Rule and initializes the RuleMap
func (pr *Rule) SetRuleMap(ps *State) {
RuleMap = map[string]*Rule{}
pr.WalkDown(func(k tree.Node) bool {
pri := k.(*Rule)
if epr, has := RuleMap[pri.Name]; has {
ps.Error(lexer.PosZero, fmt.Sprintf("Parser Compile: multiple rules with same name: %v and %v", pri.Path(), epr.Path()), pri)
} else {
RuleMap[pri.Name] = pri
}
return true
})
}
// CompileAll is called on the top-level Rule to compile all nodes
// it calls SetRuleMap first.
// Returns true if everything is ok, false if there were compile errors
func (pr *Rule) CompileAll(ps *State) bool {
pr.SetRuleMap(ps)
allok := true
pr.WalkDown(func(k tree.Node) bool {
pri := k.(*Rule)
ok := pri.Compile(ps)
if !ok {
allok = false
}
return true
})
return allok
}
// Compile compiles string rules into their runnable elements.
// Returns true if everything is ok, false if there were compile errors.
func (pr *Rule) Compile(ps *State) bool {
if pr.Off {
pr.SetProperty("inactive", true)
} else {
pr.DeleteProperty("inactive")
}
if pr.Rule == "" { // parent
pr.Rules = nil
pr.setsScope = false
return true
}
valid := true
rstr := pr.Rule
if pr.Rule[0] == '-' {
rstr = rstr[1:]
pr.reverse = true
} else {
pr.reverse = false
}
rs := strings.Split(rstr, " ")
nr := len(rs)
pr.Rules = make(RuleList, nr)
pr.ExclFwd = nil
pr.ExclRev = nil
pr.noTokens = false
pr.onlyTokens = true // default is this..
pr.setsScope = false
pr.tokenMatchGroup = false
pr.Order = nil
nmatch := 0
ntok := 0
curStInc := 0
eoses := 0
for ri := range rs {
rn := strings.TrimSpace(rs[ri])
if len(rn) == 0 {
ps.Error(lexer.PosZero, "Compile: Rules has empty string -- make sure there is only one space between rule elements", pr)
valid = false
break
}
if rn == "!" { // exclusionary rule
nr = ri
pr.Rules = pr.Rules[:ri]
pr.CompileExcl(ps, rs, ri+1)
break
}
if rn[0] == ':' {
pr.tokenMatchGroup = true
}
rr := &pr.Rules[ri]
tokst := strings.Index(rn, "'")
if tokst >= 0 {
if rn[0] == '?' {
rr.Opt = true
} else {
rr.StInc = curStInc
rr.Match = true // all tokens match by default
pr.Order = append(pr.Order, ri)
nmatch++
ntok++
curStInc = 0
}
sz := len(rn)
if rn[0] == '+' {
td, _ := strconv.ParseInt(rn[1:tokst], 10, 64)
rr.Token.Depth = int(td)
} else if rn[0] == '-' {
rr.FromNext = true
}
tn := rn[tokst+1 : sz-1]
if len(tn) > 4 && tn[:4] == "key:" {
rr.Token.Token = token.Keyword
rr.Token.Key = tn[4:]
} else {
if pmt, has := token.OpPunctMap[tn]; has {
rr.Token.Token = pmt
} else {
err := rr.Token.Token.SetString(tn)
if err != nil {
ps.Error(lexer.PosZero, fmt.Sprintf("Compile: token convert error: %v", err.Error()), pr)
valid = false
}
}
}
if rr.Token.Token == token.EOS {
eoses++
if ri == nr-1 {
rr.StInc = eoses
pr.setsScope = true
}
}
} else {
st := 0
if rn[:2] == "?@" || rn[:2] == "@?" {
st = 2
rr.Opt = true
rr.Match = true
} else if rn[0] == '?' {
st = 1
rr.Opt = true
} else if rn[0] == '@' {
st = 1
rr.Match = true
pr.onlyTokens = false
pr.Order = append(pr.Order, ri)
nmatch++
} else {
curStInc++
}
rp, ok := RuleMap[rn[st:]]
if !ok {
ps.Error(lexer.PosZero, fmt.Sprintf("Compile: refers to rule %v not found", rn), pr)
valid = false
} else {
rr.Rule = rp
}
}
}
if pr.reverse {
pr.AST = AnchorAST // must be
}
if ntok == 0 && nmatch == 0 {
pr.Rules[0].Match = true
pr.Order = append(pr.Order, 0)
pr.noTokens = true
} else {
pr.OptimizeOrder(ps)
}
return valid
}
// OptimizeOrder optimizes the order of processing rule elements, including:
// * A block of reversed elements that match from next
func (pr *Rule) OptimizeOrder(ps *State) {
osz := len(pr.Order)
if osz == 0 {
return
}
nfmnxt := 0
fmnSt := -1
fmnEd := -1
lastwas := false
for oi := 0; oi < osz; oi++ {
ri := pr.Order[oi]
rr := &pr.Rules[ri]
if rr.FromNext {
nfmnxt++
if fmnSt < 0 {
fmnSt = oi
}
if lastwas {
fmnEd = oi // end of block
}
lastwas = true
} else {
lastwas = false
}
}
if nfmnxt > 1 && fmnEd > 0 {
nword := make([]int, osz)
for oi := 0; oi < fmnSt; oi++ {
nword[oi] = pr.Order[oi]
}
idx := fmnSt
for oi := fmnEd - 1; oi >= fmnSt; oi-- {
nword[idx] = pr.Order[oi]
idx++
}
for oi := fmnEd; oi < osz; oi++ {
nword[oi] = pr.Order[oi]
}
pr.Order = nword
}
}
// CompileTokMap compiles first token map
func (pr *Rule) CompileTokMap(ps *State) bool {
valid := true
pr.FiTokenMap = make(map[string]*Rule, len(pr.Children))
pr.FiTokenElseIndex = len(pr.Children)
for i, kpri := range pr.Children {
kpr := kpri.(*Rule)
if len(kpr.Rules) == 0 || !kpr.Rules[0].IsToken() {
pr.FiTokenElseIndex = i
break
}
fr := kpr.Rules[0]
skey := fr.Token.StringKey()
if _, has := pr.FiTokenMap[skey]; has {
ps.Error(lexer.PosZero, fmt.Sprintf("CompileFirstTokenMap: multiple rules have the same first token: %v -- must be unique -- use a :'tok' group to match that first token and put all the sub-rules as children of that node", fr.Token), pr)
pr.FiTokenElseIndex = 0
valid = false
} else {
pr.FiTokenMap[skey] = kpr
}
}
return valid
}
// CompileExcl compiles exclusionary rules starting at given point
// currently only working for single-token matching rule
func (pr *Rule) CompileExcl(ps *State, rs []string, rist int) bool {
valid := true
nr := len(rs)
var ktok token.KeyToken
ktoki := -1
for ri := 0; ri < rist; ri++ {
rr := &pr.Rules[ri]
if !rr.IsToken() {
continue
}
ktok = rr.Token
ktoki = ri
break
}
if ktoki < 0 {
ps.Error(lexer.PosZero, "CompileExcl: no token found for matching exclusion rules", pr)
return false
}
pr.ExclRev = make(RuleList, nr-rist)
ki := -1
for ri := rist; ri < nr; ri++ {
rn := strings.TrimSpace(rs[ri])
rr := &pr.ExclRev[ri-rist]
if rn[0] == '?' {
rr.Opt = true
}
tokst := strings.Index(rn, "'")
if tokst < 0 {
continue // pure optional
}
if !rr.Opt {
rr.Match = true // all tokens match by default
}
sz := len(rn)
if rn[0] == '+' {
td, _ := strconv.ParseInt(rn[1:tokst], 10, 64)
rr.Token.Depth = int(td)
}
tn := rn[tokst+1 : sz-1]
if len(tn) > 4 && tn[:4] == "key:" {
rr.Token.Token = token.Keyword
rr.Token.Key = tn[4:]
} else {
if pmt, has := token.OpPunctMap[tn]; has {
rr.Token.Token = pmt
} else {
err := rr.Token.Token.SetString(tn)
if err != nil {
ps.Error(lexer.PosZero, fmt.Sprintf("CompileExcl: token convert error: %v", err.Error()), pr)
valid = false
}
}
}
if rr.Token.Equal(ktok) {
ki = ri
}
}
if ki < 0 {
ps.Error(lexer.PosZero, fmt.Sprintf("CompileExcl: key token: %v not found in exclusion rule", ktok), pr)
valid = false
return valid
}
pr.ExclKeyIndex = ktoki
pr.ExclFwd = pr.ExclRev[ki+1-rist:]
pr.ExclRev = pr.ExclRev[:ki-rist]
return valid
}
// Validate checks for any errors in the rules and issues warnings,
// returns true if valid (no err) and false if invalid (errs)
func (pr *Rule) Validate(ps *State) bool {
valid := true
// do this here so everything else is compiled
if len(pr.Rules) == 0 && pr.FirstTokenMap {
pr.CompileTokMap(ps)
}
if len(pr.Rules) == 0 && !pr.HasChildren() && !tree.IsRoot(pr) {
ps.Error(lexer.PosZero, "Validate: rule has no rules and no children", pr)
valid = false
}
if !pr.tokenMatchGroup && len(pr.Rules) > 0 && pr.HasChildren() {
ps.Error(lexer.PosZero, "Validate: rule has both rules and children -- should be either-or", pr)
valid = false
}
if pr.reverse {
if len(pr.Rules) != 3 {
ps.Error(lexer.PosZero, "Validate: a Reverse (-) rule must have 3 children -- for binary operator expressions only", pr)
valid = false
} else {
if !pr.Rules[1].IsToken() {
ps.Error(lexer.PosZero, "Validate: a Reverse (-) rule must have a token to be recognized in the middle of two rules -- for binary operator expressions only", pr)
}
}
}
if len(pr.Rules) > 0 {
if pr.Rules[0].IsRule() && (pr.Rules[0].Rule == pr || pr.ParentLevel(pr.Rules[0].Rule) >= 0) { // left recursive
if pr.Rules[0].Match {
ps.Error(lexer.PosZero, fmt.Sprintf("Validate: rule refers to itself recursively in first sub-rule: %v and that sub-rule is marked as a Match -- this is infinite recursion and is not allowed! Must use distinctive tokens in rule to match this rule, and then left-recursive elements will be filled in when the rule runs, but they cannot be used for matching rule.", pr.Rules[0].Rule.Name), pr)
valid = false
}
ntok := 0
for _, rr := range pr.Rules {
if rr.IsToken() {
ntok++
}
}
if ntok == 0 {
ps.Error(lexer.PosZero, fmt.Sprintf("Validate: rule refers to itself recursively in first sub-rule: %v, and does not have any tokens in the rule -- MUST promote tokens to this rule to disambiguate match, otherwise will just do infinite recursion!", pr.Rules[0].Rule.Name), pr)
valid = false
}
}
}
// now we iterate over our kids
for _, kpri := range pr.Children {
kpr := kpri.(*Rule)
if !kpr.Validate(ps) {
valid = false
}
}
return valid
}
// StartParse is called on the root of the parse rule tree to start the parsing process
func (pr *Rule) StartParse(ps *State) *Rule {
if ps.AtEofNext() || !pr.HasChildren() {
ps.GotoEof()
return nil
}
kpr := pr.Children[0].(*Rule) // first rule is special set of valid top-level matches
var parAST *AST
scope := lexer.Reg{St: ps.Pos}
if ps.AST.HasChildren() {
parAST = ps.AST.ChildAST(0)
} else {
parAST = NewAST(ps.AST)
parAST.SetName(kpr.Name)
ok := false
scope.St, ok = ps.Src.ValidTokenPos(scope.St)
if !ok {
ps.GotoEof()
return nil
}
ps.Pos = scope.St
}
didErr := false
for {
cpos := ps.Pos
mrule := kpr.Parse(ps, pr, parAST, scope, nil, 0)
ps.ResetNonMatches()
if ps.AtEof() {
return nil
}
if cpos == ps.Pos {
if !didErr {
ps.Error(cpos, "did not advance position -- need more rules to match current input -- skipping to next EOS", pr)
didErr = true
}
cp, ok := ps.Src.NextTokenPos(ps.Pos)
if !ok {
ps.GotoEof()
return nil
}
ep, ok := ps.Src.NextEosAnyDepth(cp)
if !ok {
ps.GotoEof()
return nil
}
ps.Pos = ep
} else {
return mrule
}
}
}
// Parse tries to apply rule to given input state, returns rule that matched or nil
// parent is the parent rule that we're being called from.
// parAST is the current ast node that we add to.
// scope is the region to search within, defined by parent or EOS if we have a terminal
// one
func (pr *Rule) Parse(ps *State, parent *Rule, parAST *AST, scope lexer.Reg, optMap lexer.TokenMap, depth int) *Rule {
if pr.Off {
return nil
}
if depth >= DepthLimit {
ps.Error(scope.St, "depth limit exceeded -- parser rules error -- look for recursive cases", pr)
return nil
}
nr := len(pr.Rules)
if !pr.tokenMatchGroup && nr > 0 {
return pr.ParseRules(ps, parent, parAST, scope, optMap, depth)
}
if optMap == nil && pr.OptTokenMap {
optMap = ps.Src.TokenMapReg(scope)
if ps.Trace.On {
ps.Trace.Out(ps, pr, Run, scope.St, scope, parAST, fmt.Sprintf("made optmap of size: %d", len(optMap)))
}
}
// pure group types just iterate over kids
for _, kpri := range pr.Children {
kpr := kpri.(*Rule)
if mrule := kpr.Parse(ps, pr, parAST, scope, optMap, depth+1); mrule != nil {
return mrule
}
}
return nil
}
// ParseRules parses rules and returns this rule if it matches, nil if not
func (pr *Rule) ParseRules(ps *State, parent *Rule, parAST *AST, scope lexer.Reg, optMap lexer.TokenMap, depth int) *Rule {
ok := false
if pr.setsScope {
scope, ok = pr.Scope(ps, parAST, scope)
if !ok {
return nil
}
} else if GUIActive {
if scope == lexer.RegZero {
ps.Error(scope.St, "scope is empty and no EOS in rule -- invalid rules -- starting rules must all have EOS", pr)
return nil
}
}
match, nscope, mpos := pr.Match(ps, parAST, scope, 0, optMap)
if !match {
return nil
}
rparent := parent.Parent.(*Rule)
if parent.AST != NoAST && parent.IsGroup() {
if parAST.Name != parent.Name {
mreg := mpos.StartEndExcl(ps)
newAST := ps.AddAST(parAST, parent.Name, mreg)
if parent.AST == AnchorAST {
parAST = newAST
}
}
} else if parent.IsGroup() && rparent.AST != NoAST && rparent.IsGroup() { // two-level group...
if parAST.Name != rparent.Name {
mreg := mpos.StartEndExcl(ps)
newAST := ps.AddAST(parAST, rparent.Name, mreg)
if rparent.AST == AnchorAST {
parAST = newAST
}
}
}
valid := pr.DoRules(ps, parent, parAST, nscope, mpos, optMap, depth) // returns validity but we don't care once matched..
if !valid {
return nil
}
return pr
}
// Scope finds the potential scope region for looking for tokens -- either from
// EOS position or State ScopeStack pushed from parents.
// Returns new scope and false if no valid scope found.
func (pr *Rule) Scope(ps *State, parAST *AST, scope lexer.Reg) (lexer.Reg, bool) {
// prf := profile.Start("Scope")
// defer prf.End()
nscope := scope
creg := scope
lr := pr.Rules.Last()
for ei := 0; ei < lr.StInc; ei++ {
stlx := ps.Src.LexAt(creg.St)
ep, ok := ps.Src.NextEos(creg.St, stlx.Token.Depth)
if !ok {
// ps.Error(creg.St, "could not find EOS at target nesting depth -- parens / bracket / brace mismatch?", pr)
return nscope, false
}
if scope.Ed != lexer.PosZero && lr.Opt && scope.Ed.IsLess(ep) {
// optional tokens can't take us out of scope
return scope, true
}
if ei == lr.StInc-1 {
nscope.Ed = ep
if ps.Trace.On {
ps.Trace.Out(ps, pr, SubMatch, nscope.St, nscope, parAST, fmt.Sprintf("from EOS: starting scope: %v new scope: %v end pos: %v depth: %v", scope, nscope, ep, stlx.Token.Depth))
}
} else {
creg.St, ok = ps.Src.NextTokenPos(ep) // advance
if !ok {
// ps.Error(scope.St, "end of file looking for EOS tokens -- premature file end?", pr)
return nscope, false
}
}
}
return nscope, true
}
// Match attempts to match the rule, returns true if it matches, and the
// match positions, along with any update to the scope
func (pr *Rule) Match(ps *State, parAST *AST, scope lexer.Reg, depth int, optMap lexer.TokenMap) (bool, lexer.Reg, Matches) {
if pr.Off {
return false, scope, nil
}
if depth > DepthLimit {
ps.Error(scope.St, "depth limit exceeded -- parser rules error -- look for recursive cases", pr)
return false, scope, nil
}
if ps.IsNonMatch(scope, pr) {
return false, scope, nil
}
if pr.StackMatch != "" {
if ps.Stack.Top() != pr.StackMatch {
return false, scope, nil
}
}
// mprf := profile.Start("Match")
// defer mprf.End()
// Note: uncomment the following to see which rules are taking the most
// time -- very helpful for focusing effort on optimizing those rules.
// prf := profile.Start(pr.Nm)
// defer prf.End()
nr := len(pr.Rules)
if pr.tokenMatchGroup || nr == 0 { // Group
return pr.MatchGroup(ps, parAST, scope, depth, optMap)
}
// prf := profile.Start("IsMatch")
if mst, match := ps.IsMatch(pr, scope); match {
// prf.End()
return true, scope, mst.Regs
}
// prf.End()
var mpos Matches
match := false
if pr.noTokens {
match, mpos = pr.MatchNoToks(ps, parAST, scope, depth, optMap)
} else if pr.onlyTokens {
match, mpos = pr.MatchOnlyToks(ps, parAST, scope, depth, optMap)
} else {
match, mpos = pr.MatchMixed(ps, parAST, scope, depth, optMap)
}
if !match {
ps.AddNonMatch(scope, pr)
return false, scope, nil
}
if len(pr.ExclFwd) > 0 || len(pr.ExclRev) > 0 {
ktpos := mpos[pr.ExclKeyIndex]
if pr.MatchExclude(ps, scope, ktpos, depth, optMap) {
if ps.Trace.On {
ps.Trace.Out(ps, pr, NoMatch, ktpos.St, scope, parAST, "Exclude criteria matched")
}
ps.AddNonMatch(scope, pr)
return false, scope, nil
}
}
mreg := mpos.StartEnd()
ps.AddMatch(pr, scope, mpos)
if ps.Trace.On {
ps.Trace.Out(ps, pr, Match, mreg.St, scope, parAST, fmt.Sprintf("Full Match reg: %v", mreg))
}
return true, scope, mpos
}
// MatchOnlyToks matches rules having only tokens
func (pr *Rule) MatchOnlyToks(ps *State, parAST *AST, scope lexer.Reg, depth int, optMap lexer.TokenMap) (bool, Matches) {
nr := len(pr.Rules)
var mpos Matches
scstlx := ps.Src.LexAt(scope.St) // scope starting lex
scstDepth := scstlx.Token.Depth
creg := scope
osz := len(pr.Order)
for oi := 0; oi < osz; oi++ {
ri := pr.Order[oi]
rr := &pr.Rules[ri]
kt := rr.Token
if optMap != nil && !optMap.Has(kt.Token) { // not even a possibility
return false, nil
}
if rr.FromNext {
if mpos == nil {
mpos = make(Matches, nr) // make on demand -- cuts out a lot of allocations!
}
mpos[nr-1] = lexer.Reg{scope.Ed, scope.Ed}
}
kt.Depth += scstDepth // always use starting scope depth
match, tpos := pr.MatchToken(ps, rr, ri, kt, &creg, mpos, parAST, scope, depth, optMap)
if !match {
if ps.Trace.On {
if tpos != lexer.PosZero {
tlx := ps.Src.LexAt(tpos)
ps.Trace.Out(ps, pr, NoMatch, creg.St, creg, parAST, fmt.Sprintf("%v token: %v, was: %v", ri, kt.String(), tlx.String()))
} else {
ps.Trace.Out(ps, pr, NoMatch, creg.St, creg, parAST, fmt.Sprintf("%v token: %v, nil region", ri, kt.String()))
}
}
return false, nil
}
if mpos == nil {
mpos = make(Matches, nr) // make on demand -- cuts out a lot of allocations!
}
mpos[ri] = lexer.Reg{tpos, tpos}
if ps.Trace.On {
ps.Trace.Out(ps, pr, SubMatch, creg.St, creg, parAST, fmt.Sprintf("%v token: %v", ri, kt.String()))
}
}
return true, mpos
}
// MatchToken matches one token sub-rule -- returns true for match and
// false if no match -- and the position where it was / should have been
func (pr *Rule) MatchToken(ps *State, rr *RuleEl, ri int, kt token.KeyToken, creg *lexer.Reg, mpos Matches, parAST *AST, scope lexer.Reg, depth int, optMap lexer.TokenMap) (bool, lexer.Pos) {
nr := len(pr.Rules)
ok := false
matchst := false // match start of creg
matched := false // match end of creg
var tpos lexer.Pos
if ri == 0 {
matchst = true
} else if mpos != nil {
lpos := mpos[ri-1].Ed
if lpos != lexer.PosZero { // previous has matched
matchst = true
} else if ri < nr-1 && rr.FromNext {
lpos := mpos[ri+1].St
if lpos != lexer.PosZero { // previous has matched
creg.Ed, _ = ps.Src.PrevTokenPos(lpos)
matched = true
}
}
}
for stinc := 0; stinc < rr.StInc; stinc++ {
creg.St, _ = ps.Src.NextTokenPos(creg.St)
}
if ri == nr-1 && rr.Token.Token == token.EOS {
return true, scope.Ed
}
if creg.IsNil() && !matched {
return false, tpos
}
if matchst { // start token must be right here
if !ps.MatchToken(kt, creg.St) {
return false, creg.St
}
tpos = creg.St
} else if matched {
if !ps.MatchToken(kt, creg.Ed) {
return false, creg.Ed
}
tpos = creg.Ed
} else {
// prf := profile.Start("FindToken")
if pr.reverse {
tpos, ok = ps.FindTokenReverse(kt, *creg)
} else {
tpos, ok = ps.FindToken(kt, *creg)
}
// prf.End()
if !ok {
return false, tpos
}
}
creg.St, _ = ps.Src.NextTokenPos(tpos) // always ratchet up
return true, tpos
}
// MatchMixed matches mixed tokens and non-tokens
func (pr *Rule) MatchMixed(ps *State, parAST *AST, scope lexer.Reg, depth int, optMap lexer.TokenMap) (bool, Matches) {
nr := len(pr.Rules)
var mpos Matches
scstlx := ps.Src.LexAt(scope.St) // scope starting lex
scstDepth := scstlx.Token.Depth
creg := scope
osz := len(pr.Order)
// first pass filter on tokens
if optMap != nil {
for oi := 0; oi < osz; oi++ {
ri := pr.Order[oi]
rr := &pr.Rules[ri]
if rr.IsToken() {
kt := rr.Token
if !optMap.Has(kt.Token) { // not even a possibility
return false, nil
}
}
}
}
for oi := 0; oi < osz; oi++ {
ri := pr.Order[oi]
rr := &pr.Rules[ri]
/////////////////////////////////////////////
// Token
if rr.IsToken() {
kt := rr.Token
if rr.FromNext {
if mpos == nil {
mpos = make(Matches, nr) // make on demand -- cuts out a lot of allocations!
}
mpos[nr-1] = lexer.Reg{scope.Ed, scope.Ed}
}
kt.Depth += scstDepth // always use starting scope depth
match, tpos := pr.MatchToken(ps, rr, ri, kt, &creg, mpos, parAST, scope, depth, optMap)
if !match {
if ps.Trace.On {
if tpos != lexer.PosZero {
tlx := ps.Src.LexAt(tpos)
ps.Trace.Out(ps, pr, NoMatch, creg.St, creg, parAST, fmt.Sprintf("%v token: %v, was: %v", ri, kt.String(), tlx.String()))
} else {
ps.Trace.Out(ps, pr, NoMatch, creg.St, creg, parAST, fmt.Sprintf("%v token: %v, nil region", ri, kt.String()))
}
}
return false, nil
}
if mpos == nil {
mpos = make(Matches, nr) // make on demand -- cuts out a lot of allocations!
}
mpos[ri] = lexer.Reg{tpos, tpos}
if ps.Trace.On {
ps.Trace.Out(ps, pr, SubMatch, creg.St, creg, parAST, fmt.Sprintf("%v token: %v", ri, kt.String()))
}
continue
}
//////////////////////////////////////////////
// Sub-Rule
if creg.IsNil() {
ps.Trace.Out(ps, pr, NoMatch, creg.St, creg, parAST, fmt.Sprintf("%v sub-rule: %v, nil region", ri, rr.Rule.Name))
return false, nil
}
// first, limit region to same depth or greater as start of region -- prevents
// overflow beyond natural boundaries
stlx := ps.Src.LexAt(creg.St) // scope starting lex
cp, _ := ps.Src.NextTokenPos(creg.St)
stdp := stlx.Token.Depth
for cp.IsLess(creg.Ed) {
lx := ps.Src.LexAt(cp)
if lx.Token.Depth < stdp {
creg.Ed = cp
break
}
cp, _ = ps.Src.NextTokenPos(cp)
}
if ps.Trace.On {
ps.Trace.Out(ps, pr, SubMatch, creg.St, creg, parAST, fmt.Sprintf("%v trying sub-rule: %v", ri, rr.Rule.Name))
}
match, _, smpos := rr.Rule.Match(ps, parAST, creg, depth+1, optMap)
if !match {
if ps.Trace.On {
ps.Trace.Out(ps, pr, NoMatch, creg.St, creg, parAST, fmt.Sprintf("%v sub-rule: %v", ri, rr.Rule.Name))
}
return false, nil
}
creg.Ed = scope.Ed // back to full scope
// look through smpos for last valid position -- use that as last match pos
mreg := smpos.StartEnd()
lmnpos, ok := ps.Src.NextTokenPos(mreg.Ed)
if !ok && !(ri == nr-1 || (ri == nr-2 && pr.setsScope)) {
// if at end, or ends in EOS, then ok..
if ps.Trace.On {
ps.Trace.Out(ps, pr, NoMatch, creg.St, creg, parAST, fmt.Sprintf("%v sub-rule: %v -- not at end and no tokens left", ri, rr.Rule.Name))
}
return false, nil
}
if mpos == nil {
mpos = make(Matches, nr) // make on demand -- cuts out a lot of allocations!
}
mpos[ri] = mreg
creg.St = lmnpos
if ps.Trace.On {
msreg := mreg
msreg.Ed = lmnpos
ps.Trace.Out(ps, pr, SubMatch, mreg.St, msreg, parAST, fmt.Sprintf("%v rule: %v reg: %v", ri, rr.Rule.Name, msreg))
}
}
return true, mpos
}
// MatchNoToks matches NoToks case -- just does single sub-rule match
func (pr *Rule) MatchNoToks(ps *State, parAST *AST, scope lexer.Reg, depth int, optMap lexer.TokenMap) (bool, Matches) {
creg := scope
ri := 0
rr := &pr.Rules[0]
if ps.Trace.On {
ps.Trace.Out(ps, pr, SubMatch, creg.St, creg, parAST, fmt.Sprintf("%v trying sub-rule: %v", ri, rr.Rule.Name))
}
match, _, smpos := rr.Rule.Match(ps, parAST, creg, depth+1, optMap)
if !match {
if ps.Trace.On {
ps.Trace.Out(ps, pr, NoMatch, creg.St, creg, parAST, fmt.Sprintf("%v sub-rule: %v", ri, rr.Rule.Name))
}
return false, nil
}
if ps.Trace.On {
mreg := smpos.StartEnd() // todo: should this include creg start instead?
ps.Trace.Out(ps, pr, SubMatch, mreg.St, mreg, parAST, fmt.Sprintf("%v rule: %v reg: %v", ri, rr.Rule.Name, mreg))
}
return true, smpos
}
// MatchGroup does matching for Group rules
func (pr *Rule) MatchGroup(ps *State, parAST *AST, scope lexer.Reg, depth int, optMap lexer.TokenMap) (bool, lexer.Reg, Matches) {
// prf := profile.Start("SubMatch")
if mst, match := ps.IsMatch(pr, scope); match {
// prf.End()
return true, scope, mst.Regs
}
// prf.End()
sti := 0
nk := len(pr.Children)
if pr.FirstTokenMap {
stlx := ps.Src.LexAt(scope.St)
if kpr, has := pr.FiTokenMap[stlx.Token.StringKey()]; has {
match, nscope, mpos := kpr.Match(ps, parAST, scope, depth+1, optMap)
if match {
if ps.Trace.On {
ps.Trace.Out(ps, pr, SubMatch, scope.St, scope, parAST, fmt.Sprintf("first token group child: %v", kpr.Name))
}
ps.AddMatch(pr, scope, mpos)
return true, nscope, mpos
}
}
sti = pr.FiTokenElseIndex
}
for i := sti; i < nk; i++ {
kpri := pr.Children[i]
kpr := kpri.(*Rule)
match, nscope, mpos := kpr.Match(ps, parAST, scope, depth+1, optMap)
if match {
if ps.Trace.On {
ps.Trace.Out(ps, pr, SubMatch, scope.St, scope, parAST, fmt.Sprintf("group child: %v", kpr.Name))
}
ps.AddMatch(pr, scope, mpos)
return true, nscope, mpos
}
}
ps.AddNonMatch(scope, pr)
return false, scope, nil
}
// MatchExclude looks for matches of exclusion tokens -- if found, they exclude this rule
// return is true if exclude matches and rule should be excluded
func (pr *Rule) MatchExclude(ps *State, scope lexer.Reg, ktpos lexer.Reg, depth int, optMap lexer.TokenMap) bool {
nf := len(pr.ExclFwd)
nr := len(pr.ExclRev)
scstlx := ps.Src.LexAt(scope.St) // scope starting lex
scstDepth := scstlx.Token.Depth
if nf > 0 {
cp, ok := ps.Src.NextTokenPos(ktpos.St)
if !ok {
return false
}
prevAny := false
for ri := 0; ri < nf; ri++ {
rr := pr.ExclFwd[ri]
kt := rr.Token
kt.Depth += scstDepth // always use starting scope depth
if kt.Token == token.None {
prevAny = true // wild card
continue
}
if prevAny {
creg := scope
creg.St = cp
pos, ok := ps.FindToken(kt, creg)
if !ok {
return false
}
cp = pos
} else {
if !ps.MatchToken(kt, cp) {
if !rr.Opt {
return false
}
lx := ps.Src.LexAt(cp)
if lx.Token.Depth != kt.Depth {
break
}
// ok, keep going -- no info..
}
}
cp, ok = ps.Src.NextTokenPos(cp)
if !ok && ri < nf-1 {
return false
}
if scope.Ed == cp || scope.Ed.IsLess(cp) { // out of scope -- if non-opt left, nomatch
ri++
for ; ri < nf; ri++ {
rr := pr.ExclFwd[ri]
if !rr.Opt {
return false
}
}
break
}
prevAny = false
}
}
if nr > 0 {
cp, ok := ps.Src.PrevTokenPos(ktpos.St)
if !ok {
return false
}
prevAny := false
for ri := nr - 1; ri >= 0; ri-- {
rr := pr.ExclRev[ri]
kt := rr.Token
kt.Depth += scstDepth // always use starting scope depth
if kt.Token == token.None {
prevAny = true // wild card
continue
}
if prevAny {
creg := scope
creg.Ed = cp
pos, ok := ps.FindTokenReverse(kt, creg)
if !ok {
return false
}
cp = pos
} else {
if !ps.MatchToken(kt, cp) {
if !rr.Opt {
return false
}
lx := ps.Src.LexAt(cp)
if lx.Token.Depth != kt.Depth {
break
}
// ok, keep going -- no info..
}
}
cp, ok = ps.Src.PrevTokenPos(cp)
if !ok && ri > 0 {
return false
}
if cp.IsLess(scope.St) {
ri--
for ; ri >= 0; ri-- {
rr := pr.ExclRev[ri]
if !rr.Opt {
return false
}
}
break
}
prevAny = false
}
}
return true
}
// DoRules after we have matched, goes through rest of the rules -- returns false if
// there were any issues encountered
func (pr *Rule) DoRules(ps *State, parent *Rule, parentAST *AST, scope lexer.Reg, mpos Matches, optMap lexer.TokenMap, depth int) bool {
trcAST := parentAST
var ourAST *AST
anchorFirst := (pr.AST == AnchorFirstAST && parentAST.Name != pr.Name)
if pr.AST != NoAST {
// prf := profile.Start("AddAST")
ourAST = ps.AddAST(parentAST, pr.Name, scope)
// prf.End()
trcAST = ourAST
if ps.Trace.On {
ps.Trace.Out(ps, pr, Run, scope.St, scope, trcAST, fmt.Sprintf("running with new ast: %v", trcAST.Path()))
}
} else {
if ps.Trace.On {
ps.Trace.Out(ps, pr, Run, scope.St, scope, trcAST, fmt.Sprintf("running with parent ast: %v", trcAST.Path()))
}
}
if pr.reverse {
return pr.DoRulesRevBinExp(ps, parent, parentAST, scope, mpos, ourAST, optMap, depth)
}
nr := len(pr.Rules)
valid := true
creg := scope
for ri := 0; ri < nr; ri++ {
pr.DoActs(ps, ri, parent, ourAST, parentAST)
rr := &pr.Rules[ri]
if rr.IsToken() && !rr.Opt {
mp := mpos[ri].St
if mp == ps.Pos {
ps.Pos, _ = ps.Src.NextTokenPos(ps.Pos) // already matched -- move past
if ps.Trace.On {
ps.Trace.Out(ps, pr, Run, mp, scope, trcAST, fmt.Sprintf("%v: token at expected pos: %v", ri, rr.Token))
}
} else if mp.IsLess(ps.Pos) {
// ps.Pos has moved beyond our expected token -- sub-rule has eaten more than expected!
if rr.Token.Token == token.EOS {
if ps.Trace.On {
ps.Trace.Out(ps, pr, Run, mp, scope, trcAST, fmt.Sprintf("%v: EOS token consumed by sub-rule: %v", ri, rr.Token))
}
} else {
ps.Error(mp, fmt.Sprintf("expected token: %v (at rule index: %v) was consumed by prior sub-rule(s)", rr.Token, ri), pr)
}
} else if ri == nr-1 && rr.Token.Token == token.EOS {
ps.ResetNonMatches() // passed this chunk of inputs -- don't need those nonmatches
} else {
ps.Error(mp, fmt.Sprintf("token: %v (at rule index: %v) has extra preceding input inconsistent with grammar", rr.Token, ri), pr)
ps.Pos, _ = ps.Src.NextTokenPos(mp) // move to token for more robustness
}
if ourAST != nil {
ourAST.SetTokRegEnd(ps.Pos, ps.Src) // update our end to any tokens that match
}
continue
}
creg.St = ps.Pos
creg.Ed = scope.Ed
if !pr.noTokens {
for mi := ri + 1; mi < nr; mi++ {
if mpos[mi].St != lexer.PosZero {
creg.Ed = mpos[mi].St // only look up to point of next matching token
break
}
}
}
if rr.IsToken() { // opt by definition here
if creg.IsNil() { // no tokens left..
if ps.Trace.On {
ps.Trace.Out(ps, pr, Run, creg.St, scope, trcAST, fmt.Sprintf("%v: opt token: %v no more src", ri, rr.Token))
}
continue
}
stlx := ps.Src.LexAt(creg.St)
kt := rr.Token
kt.Depth += stlx.Token.Depth
pos, ok := ps.FindToken(kt, creg)
if !ok {
if ps.Trace.On {
ps.Trace.Out(ps, pr, NoMatch, creg.St, creg, parentAST, fmt.Sprintf("%v token: %v", ri, kt.String()))
}
continue
}
if ps.Trace.On {
ps.Trace.Out(ps, pr, Match, pos, creg, parentAST, fmt.Sprintf("%v token: %v", ri, kt))
}
ps.Pos, _ = ps.Src.NextTokenPos(pos)
continue
}
////////////////////////////////////////////////////
// Below here is a Sub-Rule
if creg.IsNil() { // no tokens left..
if rr.Opt {
if ps.Trace.On {
ps.Trace.Out(ps, pr, Run, creg.St, scope, trcAST, fmt.Sprintf("%v: opt rule: %v no more src", ri, rr.Rule.Name))
}
continue
}
ps.Error(creg.St, fmt.Sprintf("missing expected input for: %v", rr.Rule.Name), pr)
valid = false
break // no point in continuing
}
useAST := parentAST
if pr.AST == AnchorAST || anchorFirst || (pr.AST == SubAST && ri < nr-1) {
useAST = ourAST
}
// NOTE: we can't use anything about the previous match here, because it could have
// come from a sub-sub-rule and in any case is not where you want to start
// because is could have been a token in the middle.
if ps.Trace.On {
ps.Trace.Out(ps, pr, Run, creg.St, creg, trcAST, fmt.Sprintf("%v: trying rule: %v", ri, rr.Rule.Name))
}
subm := rr.Rule.Parse(ps, pr, useAST, creg, optMap, depth+1)
if subm == nil {
if !rr.Opt {
ps.Error(creg.St, fmt.Sprintf("required element: %v did not match input", rr.Rule.Name), pr)
valid = false
break
} else {
if ps.Trace.On {
ps.Trace.Out(ps, pr, Run, creg.St, creg, trcAST, fmt.Sprintf("%v: optional rule: %v failed", ri, rr.Rule.Name))
}
}
}
if !rr.Opt && ourAST != nil {
ourAST.SetTokRegEnd(ps.Pos, ps.Src) // update our end to include non-optional elements
}
}
if valid {
pr.DoActs(ps, -1, parent, ourAST, parentAST)
}
return valid
}
// DoRulesRevBinExp reverse version of do rules for binary expression rule with
// one key token in the middle -- we just pay attention to scoping rest of sub-rules
// relative to that, and don't otherwise adjust scope or position. In particular all
// the position updating taking place in sup-rules is then just ignored and we set the
// position to the end position matched by the "last" rule (which was the first processed)
func (pr *Rule) DoRulesRevBinExp(ps *State, parent *Rule, parentAST *AST, scope lexer.Reg, mpos Matches, ourAST *AST, optMap lexer.TokenMap, depth int) bool {
nr := len(pr.Rules)
valid := true
creg := scope
trcAST := parentAST
if ourAST != nil {
trcAST = ourAST
}
tokpos := mpos[1].St
aftMpos, ok := ps.Src.NextTokenPos(tokpos)
if !ok {
ps.Error(tokpos, "premature end of input", pr)
return false
}
epos := scope.Ed
for i := nr - 1; i >= 0; i-- {
rr := &pr.Rules[i]
if i > 1 {
creg.St = aftMpos // end expr is in region from key token to end of scope
ps.Pos = creg.St // only works for a single rule after key token -- sub-rules not necc reverse
creg.Ed = scope.Ed
} else if i == 1 {
if ps.Trace.On {
ps.Trace.Out(ps, pr, Run, tokpos, scope, trcAST, fmt.Sprintf("%v: key token: %v", i, rr.Token))
}
continue
} else { // start
creg.St = scope.St
ps.Pos = creg.St
creg.Ed = tokpos
}
if rr.IsRule() { // non-key tokens ignored
if creg.IsNil() { // no tokens left..
ps.Error(creg.St, fmt.Sprintf("missing expected input for: %v", rr.Rule.Name), pr)
valid = false
continue
}
useAST := parentAST
if pr.AST == AnchorAST {
useAST = ourAST
}
if ps.Trace.On {
ps.Trace.Out(ps, pr, Run, creg.St, creg, trcAST, fmt.Sprintf("%v: trying rule: %v", i, rr.Rule.Name))
}
subm := rr.Rule.Parse(ps, pr, useAST, creg, optMap, depth+1)
if subm == nil {
if !rr.Opt {
ps.Error(creg.St, fmt.Sprintf("required element: %v did not match input", rr.Rule.Name), pr)
valid = false
}
}
}
}
// our AST is now backwards -- need to swap them
if len(ourAST.Children) == 2 {
slicesx.Swap(ourAST.Children, 0, 1)
// if GuiActive {
// we have a very strange situation here: the tree of the AST will typically
// have two children, named identically (e.g., Expr, Expr) and it will not update
// after our swap. If we could use UniqNames then it would be ok, but that doesn't
// work for tree names.. really need an option that supports uniqname AND reg names
// https://cogentcore.org/core/ki/issues/2
// ourAST.NewChild(ASTType, "Dummy")
// ourAST.DeleteChildAt(2, true)
// }
}
ps.Pos = epos
return valid
}
// DoActs performs actions at given point in rule execution (ri = rule index, is -1 at end)
func (pr *Rule) DoActs(ps *State, ri int, parent *Rule, ourAST, parentAST *AST) bool {
if len(pr.Acts) == 0 {
return false
}
// prf := profile.Start("DoActs")
// defer prf.End()
valid := true
for ai := range pr.Acts {
act := &pr.Acts[ai]
if act.RunIndex != ri {
continue
}
if !pr.DoAct(ps, act, parent, ourAST, parentAST) {
valid = false
}
}
return valid
}
// DoAct performs one action after a rule executes
func (pr *Rule) DoAct(ps *State, act *Act, parent *Rule, ourAST, parAST *AST) bool {
if act.Act == PushStack {
ps.Stack.Push(act.Path)
return true
} else if act.Act == PopStack {
ps.Stack.Pop()
return true
}
useAST := ourAST
if useAST == nil {
useAST = parAST
}
apath := useAST.Path()
var node tree.Node
var adnl []tree.Node // additional nodes
if act.Path == "" {
node = useAST
} else if andidx := strings.Index(act.Path, "&"); andidx >= 0 {
pths := strings.Split(act.Path, "&")
for _, p := range pths {
findAll := false
if strings.HasSuffix(p, "...") {
findAll = true
p = strings.TrimSuffix(p, "...")
}
var nd tree.Node
if p[:3] == "../" {
nd = parAST.FindPath(p[3:])
} else {
nd = useAST.FindPath(p)
}
if nd != nil {
if node == nil {
node = nd
}
if findAll {
pn := nd.AsTree().Parent
for _, pk := range pn.AsTree().Children {
if pk != nd && pk.AsTree().Name == nd.AsTree().Name {
adnl = append(adnl, pk)
}
}
} else if node != nd {
adnl = append(adnl, nd)
}
}
}
} else {
pths := strings.Split(act.Path, "|")
for _, p := range pths {
findAll := false
if strings.HasSuffix(p, "...") {
findAll = true
p = strings.TrimSuffix(p, "...")
}
if p[:3] == "../" {
node = parAST.FindPath(p[3:])
} else {
node = useAST.FindPath(p)
}
if node != nil {
if findAll {
pn := node.AsTree().Parent
for _, pk := range pn.AsTree().Children {
if pk != node && pk.AsTree().Name == node.AsTree().Name {
adnl = append(adnl, pk)
}
}
}
break
}
}
}
if node == nil {
if ps.Trace.On {
ps.Trace.Out(ps, pr, RunAct, ps.Pos, lexer.RegZero, useAST, fmt.Sprintf("Act %v: ERROR: node not found at path(s): %v in node: %v", act.Act, act.Path, apath))
}
return false
}
ast := node.(*AST)
lx := ps.Src.LexAt(ast.TokReg.St)
useTok := lx.Token.Token
if act.Token != token.None {
useTok = act.Token
}
nm := ast.Src
nms := strings.Split(nm, ",")
if len(adnl) > 0 {
for _, pk := range adnl {
nast := pk.(*AST)
if nast != ast {
nms = append(nms, strings.Split(nast.Src, ",")...)
}
}
}
for i := range nms {
nms[i] = strings.TrimSpace(nms[i])
}
switch act.Act {
case ChangeToken:
cp := ast.TokReg.St
for cp.IsLess(ast.TokReg.Ed) {
tlx := ps.Src.LexAt(cp)
act.ChangeToken(tlx)
cp, _ = ps.Src.NextTokenPos(cp)
}
if len(adnl) > 0 {
for _, pk := range adnl {
nast := pk.(*AST)
cp := nast.TokReg.St
for cp.IsLess(nast.TokReg.Ed) {
tlx := ps.Src.LexAt(cp)
act.ChangeToken(tlx)
cp, _ = ps.Src.NextTokenPos(cp)
}
}
}
if ps.Trace.On {
ps.Trace.Out(ps, pr, RunAct, ast.TokReg.St, ast.TokReg, ast, fmt.Sprintf("Act: Token set to: %v from path: %v = %v in node: %v", act.Token, act.Path, nm, apath))
}
return false
case AddSymbol:
for i := range nms {
n := nms[i]
if n == "" || n == "_" { // go special case..
continue
}
sy, has := ps.FindNameTopScope(n) // only look in top scope
added := false
if has {
sy.Region = ast.SrcReg
sy.Kind = useTok
if ps.Trace.On {
ps.Trace.Out(ps, pr, RunAct, ast.TokReg.St, ast.TokReg, ast, fmt.Sprintf("Act: Add sym already exists: %v from path: %v = %v in node: %v", sy.String(), act.Path, n, apath))
}
} else {
sy = syms.NewSymbol(n, useTok, ps.Src.Filename, ast.SrcReg)
added = sy.AddScopesStack(ps.Scopes)
if !added {
ps.Syms.Add(sy)
}
}
useAST.Syms.Push(sy)
sy.AST = useAST.This
if ps.Trace.On {
ps.Trace.Out(ps, pr, RunAct, ast.TokReg.St, ast.TokReg, ast, fmt.Sprintf("Act: Added sym: %v from path: %v = %v in node: %v", sy.String(), act.Path, n, apath))
}
}
case PushScope:
sy, has := ps.FindNameTopScope(nm) // Scoped(nm)
if !has {
sy = syms.NewSymbol(nm, useTok, ps.Src.Filename, ast.SrcReg) // lexer.RegZero) // zero = tmp
added := sy.AddScopesStack(ps.Scopes)
if !added {
ps.Syms.Add(sy)
}
}
ps.Scopes.Push(sy)
useAST.Syms.Push(sy)
if ps.Trace.On {
ps.Trace.Out(ps, pr, RunAct, ast.TokReg.St, ast.TokReg, ast, fmt.Sprintf("Act: Pushed Sym: %v from path: %v = %v in node: %v", sy.String(), act.Path, nm, apath))
}
case PushNewScope:
// add plus push
sy, has := ps.FindNameTopScope(nm) // Scoped(nm)
if has {
if ps.Trace.On {
ps.Trace.Out(ps, pr, RunAct, ast.TokReg.St, ast.TokReg, ast, fmt.Sprintf("Act: Push New sym already exists: %v from path: %v = %v in node: %v", sy.String(), act.Path, nm, apath))
}
} else {
sy = syms.NewSymbol(nm, useTok, ps.Src.Filename, ast.SrcReg)
added := sy.AddScopesStack(ps.Scopes)
if !added {
ps.Syms.Add(sy)
}
}
ps.Scopes.Push(sy) // key diff from add..
useAST.Syms.Push(sy)
sy.AST = useAST.This
if ps.Trace.On {
ps.Trace.Out(ps, pr, RunAct, ast.TokReg.St, ast.TokReg, ast, fmt.Sprintf("Act: Pushed New Sym: %v from path: %v = %v in node: %v", sy.String(), act.Path, nm, apath))
}
case PopScope:
sy := ps.Scopes.Pop()
if ps.Trace.On {
ps.Trace.Out(ps, pr, RunAct, ast.TokReg.St, ast.TokReg, ast, fmt.Sprintf("Act: Popped Sym: %v in node: %v", sy.String(), apath))
}
case PopScopeReg:
sy := ps.Scopes.Pop()
sy.Region = ast.SrcReg // update source region to final -- select remains initial trigger one
if ps.Trace.On {
ps.Trace.Out(ps, pr, RunAct, ast.TokReg.St, ast.TokReg, ast, fmt.Sprintf("Act: Popped Sym: %v in node: %v", sy.String(), apath))
}
case AddDetail:
sy := useAST.Syms.Top()
if sy != nil {
if sy.Detail == "" {
sy.Detail = nm
} else {
sy.Detail += " " + nm
}
if ps.Trace.On {
ps.Trace.Out(ps, pr, RunAct, ast.TokReg.St, ast.TokReg, ast, fmt.Sprintf("Act: Added Detail: %v to Sym: %v in node: %v", nm, sy.String(), apath))
}
} else {
if ps.Trace.On {
ps.Trace.Out(ps, pr, RunAct, ast.TokReg.St, ast.TokReg, ast, fmt.Sprintf("Act: Add Detail: %v ERROR -- symbol not found in node: %v", nm, apath))
}
}
case AddType:
scp := ps.Scopes.Top()
if scp == nil {
ps.Trace.Out(ps, pr, RunAct, ast.TokReg.St, ast.TokReg, ast, fmt.Sprintf("Act: Add Type: %v ERROR -- requires current scope -- none set in node: %v", nm, apath))
return false
}
for i := range nms {
n := nms[i]
if n == "" || n == "_" { // go special case..
continue
}
ty := syms.NewType(n, syms.Unknown)
ty.Filename = ps.Src.Filename
ty.Region = ast.SrcReg
ty.AST = useAST.This
ty.AddScopesStack(ps.Scopes)
scp.Types.Add(ty)
if ps.Trace.On {
ps.Trace.Out(ps, pr, RunAct, ast.TokReg.St, ast.TokReg, ast, fmt.Sprintf("Act: Added type: %v from path: %v = %v in node: %v", ty.String(), act.Path, n, apath))
}
}
}
return true
}
///////////////////////////////////////////////////////////////////////
// Non-parsing functions
// Find looks for rules in the tree that contain given string in Rule or Name fields
func (pr *Rule) Find(find string) []*Rule {
var res []*Rule
pr.WalkDown(func(k tree.Node) bool {
pri := k.(*Rule)
if strings.Contains(pri.Rule, find) || strings.Contains(pri.Name, find) {
res = append(res, pri)
}
return true
})
return res
}
// WriteGrammar outputs the parser rules as a formatted grammar in a BNF-like format
// it is called recursively
func (pr *Rule) WriteGrammar(writer io.Writer, depth int) {
if tree.IsRoot(pr) {
for _, k := range pr.Children {
pri := k.(*Rule)
pri.WriteGrammar(writer, depth)
}
} else {
ind := indent.Tabs(depth)
nmstr := pr.Name
if pr.Off {
nmstr = "// OFF: " + nmstr
}
if pr.Desc != "" {
fmt.Fprintf(writer, "%v// %v %v \n", ind, nmstr, pr.Desc)
}
if pr.IsGroup() {
fmt.Fprintf(writer, "%v%v {\n", ind, nmstr)
w := tabwriter.NewWriter(writer, 4, 4, 2, ' ', 0)
for _, k := range pr.Children {
pri := k.(*Rule)
pri.WriteGrammar(w, depth+1)
}
w.Flush()
fmt.Fprintf(writer, "%v}\n", ind)
} else {
astr := ""
switch pr.AST {
case AddAST:
astr = "+AST"
case SubAST:
astr = "_AST"
case AnchorAST:
astr = ">AST"
case AnchorFirstAST:
astr = ">1AST"
}
fmt.Fprintf(writer, "%v%v:\t%v\t%v\n", ind, nmstr, pr.Rule, astr)
if len(pr.Acts) > 0 {
fmt.Fprintf(writer, "%v--->Acts:%v\n", ind, pr.Acts.String())
}
}
}
}
// 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 parser
import (
"fmt"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/syms"
"cogentcore.org/core/parse/token"
)
// parser.State is the state maintained for parsing
type State struct {
// source and lexed version of source we're parsing
Src *lexer.File `display:"no-inline"`
// tracing for this parser
Trace TraceOptions
// root of the AST abstract syntax tree we're updating
AST *AST
// symbol map that everything gets added to from current file of parsing -- typically best for subsequent management to just have a single outer-most scoping symbol here (e.g., in Go it is the package), and then everything is a child under that
Syms syms.SymMap
// stack of scope(s) added to FileSyms e.g., package, library, module-level elements of which this file is a part -- these are reset at the start and must be added by parsing actions within the file itself
Scopes syms.SymStack
// the current lex token position
Pos lexer.Pos
// any error messages accumulated during parsing specifically
Errs lexer.ErrorList `display:"no-inline"`
// rules that matched and ran at each point, in 1-to-1 correspondence with the Src.Lex tokens for the lines and char pos dims
Matches [][]MatchStack `display:"no-inline"`
// rules that did NOT match -- represented as a map by scope of a RuleSet
NonMatches ScopeRuleSet `display:"no-inline"`
// stack for context-sensitive rules
Stack lexer.Stack `display:"no-inline"`
}
// Init initializes the state at start of parsing
func (ps *State) Init(src *lexer.File, ast *AST) {
// fmt.Println("in init")
// if ps.Src != nil {
// fmt.Println("was:", ps.Src.Filename)
// }
// if src != nil {
// fmt.Println("new:", src.Filename)
// }
ps.Src = src
if ps.AST != nil && ps.AST.This != nil {
// fmt.Println("deleting old ast")
ps.AST.DeleteChildren()
}
ps.AST = ast
if ps.AST != nil && ps.AST.This != nil {
// fmt.Println("deleting new ast")
ps.AST.DeleteChildren()
}
ps.ClearAST()
ps.Syms.Reset()
ps.Scopes.Reset()
ps.Stack.Reset()
if ps.Src != nil {
ps.Pos, _ = ps.Src.ValidTokenPos(lexer.PosZero)
}
ps.Errs.Reset()
ps.Trace.Init()
ps.AllocRules()
}
func (ps *State) ClearAST() {
ps.Syms.ClearAST()
ps.Scopes.ClearAST()
}
func (ps *State) Destroy() {
if ps.AST != nil && ps.AST.This != nil {
ps.AST.DeleteChildren()
}
ps.AST = nil
ps.ClearAST()
ps.Syms.Reset()
ps.Scopes.Reset()
ps.Stack.Reset()
if ps.Src != nil {
ps.Pos, _ = ps.Src.ValidTokenPos(lexer.PosZero)
}
ps.Errs.Reset()
ps.Trace.Init()
}
// AllocRules allocate the match, nonmatch rule state in correspondence with the src state
func (ps *State) AllocRules() {
nlines := ps.Src.NLines()
if nlines == 0 {
return
}
if len(ps.Src.Lexs) != nlines {
return
}
ps.Matches = make([][]MatchStack, nlines)
ntot := 0
for ln := 0; ln < nlines; ln++ {
sz := len(ps.Src.Lexs[ln])
if sz > 0 {
ps.Matches[ln] = make([]MatchStack, sz)
ntot += sz
}
}
ps.NonMatches = make(ScopeRuleSet, ntot*10)
}
// Error adds a parsing error at given lex token position
func (ps *State) Error(pos lexer.Pos, msg string, rule *Rule) {
if pos != lexer.PosZero {
pos = ps.Src.TokenSrcPos(pos).St
}
e := ps.Errs.Add(pos, ps.Src.Filename, msg, ps.Src.SrcLine(pos.Ln), rule)
if GUIActive {
erstr := e.Report(ps.Src.BasePath, true, true)
fmt.Fprintln(ps.Trace.OutWrite, "ERROR: "+erstr)
}
}
// AtEof returns true if current position is at end of file -- this includes
// common situation where it is just at the very last token
func (ps *State) AtEof() bool {
if ps.Pos.Ln >= ps.Src.NLines() {
return true
}
_, ok := ps.Src.ValidTokenPos(ps.Pos)
return !ok
}
// AtEofNext returns true if current OR NEXT position is at end of file -- this includes
// common situation where it is just at the very last token
func (ps *State) AtEofNext() bool {
if ps.AtEof() {
return true
}
return ps.Pos.Ln == ps.Src.NLines()-1
}
// GotoEof sets current position at EOF
func (ps *State) GotoEof() {
ps.Pos.Ln = ps.Src.NLines()
ps.Pos.Ch = 0
}
// NextSrcLine returns the next line of text
func (ps *State) NextSrcLine() string {
sp, ok := ps.Src.ValidTokenPos(ps.Pos)
if !ok {
return ""
}
ep := sp
ep.Ch = ps.Src.NTokens(ep.Ln)
if ep.Ch == sp.Ch+1 { // only one
nep, ok := ps.Src.ValidTokenPos(ep)
if ok {
ep = nep
ep.Ch = ps.Src.NTokens(ep.Ln)
}
}
reg := lexer.Reg{St: sp, Ed: ep}
return ps.Src.TokenRegSrc(reg)
}
// MatchLex is our optimized matcher method, matching tkey depth as well
func (ps *State) MatchLex(lx *lexer.Lex, tkey token.KeyToken, isCat, isSubCat bool, cp lexer.Pos) bool {
if lx.Token.Depth != tkey.Depth {
return false
}
if !(lx.Token.Token == tkey.Token || (isCat && lx.Token.Token.Cat() == tkey.Token) || (isSubCat && lx.Token.Token.SubCat() == tkey.Token)) {
return false
}
if tkey.Key == "" {
return true
}
return tkey.Key == lx.Token.Key
}
// FindToken looks for token in given region, returns position where found, false if not.
// Only matches when depth is same as at reg.St start at the start of the search.
// All positions in token indexes.
func (ps *State) FindToken(tkey token.KeyToken, reg lexer.Reg) (lexer.Pos, bool) {
// prf := profile.Start("FindToken")
// defer prf.End()
cp, ok := ps.Src.ValidTokenPos(reg.St)
if !ok {
return cp, false
}
tok := tkey.Token
isCat := tok.Cat() == tok
isSubCat := tok.SubCat() == tok
for cp.IsLess(reg.Ed) {
lx := ps.Src.LexAt(cp)
if ps.MatchLex(lx, tkey, isCat, isSubCat, cp) {
return cp, true
}
cp, ok = ps.Src.NextTokenPos(cp)
if !ok {
return cp, false
}
}
return cp, false
}
// MatchToken returns true if token matches at given position -- must be
// a valid position!
func (ps *State) MatchToken(tkey token.KeyToken, pos lexer.Pos) bool {
tok := tkey.Token
isCat := tok.Cat() == tok
isSubCat := tok.SubCat() == tok
lx := ps.Src.LexAt(pos)
tkey.Depth = lx.Token.Depth
return ps.MatchLex(lx, tkey, isCat, isSubCat, pos)
}
// FindTokenReverse looks *backwards* for token in given region, with same depth as reg.Ed-1 end
// where the search starts. Returns position where found, false if not.
// Automatically deals with possible confusion with unary operators -- if there are two
// ambiguous operators in a row, automatically gets the first one. This is mainly / only used for
// binary operator expressions (mathematical binary operators).
// All positions are in token indexes.
func (ps *State) FindTokenReverse(tkey token.KeyToken, reg lexer.Reg) (lexer.Pos, bool) {
// prf := profile.Start("FindTokenReverse")
// defer prf.End()
cp, ok := ps.Src.PrevTokenPos(reg.Ed)
if !ok {
return cp, false
}
tok := tkey.Token
isCat := tok.Cat() == tok
isSubCat := tok.SubCat() == tok
isAmbigUnary := tok.IsAmbigUnaryOp()
for reg.St.IsLess(cp) || cp == reg.St {
lx := ps.Src.LexAt(cp)
if ps.MatchLex(lx, tkey, isCat, isSubCat, cp) {
if isAmbigUnary { // make sure immed prior is not also!
pp, ok := ps.Src.PrevTokenPos(cp)
if ok {
pt := ps.Src.Token(pp)
if tok == token.OpMathMul {
if !pt.Token.IsUnaryOp() {
return cp, true
}
} else {
if !pt.Token.IsAmbigUnaryOp() {
return cp, true
}
}
// otherwise we don't match -- cannot match second opr
} else {
return cp, true
}
} else {
return cp, true
}
}
ok := false
cp, ok = ps.Src.PrevTokenPos(cp)
if !ok {
return cp, false
}
}
return cp, false
}
// AddAST adds a child AST node to given parent AST node
func (ps *State) AddAST(parAST *AST, rule string, reg lexer.Reg) *AST {
chAST := NewAST(parAST)
chAST.SetName(rule)
chAST.SetTokReg(reg, ps.Src)
return chAST
}
///////////////////////////////////////////////////////////////////////////
// Match State, Stack
// MatchState holds state info for rules that matched, recorded at starting position of match
type MatchState struct {
// rule that either matched or ran here
Rule *Rule
// scope for match
Scope lexer.Reg
// regions of match for each sub-region
Regs Matches
}
// String is fmt.Stringer
func (rs MatchState) String() string {
if rs.Rule == nil {
return ""
}
return fmt.Sprintf("%v%v", rs.Rule.Name, rs.Scope)
}
// MatchStack is the stack of rules that matched or ran for each token point
type MatchStack []MatchState
// Add given rule to stack
func (rs *MatchStack) Add(pr *Rule, scope lexer.Reg, regs Matches) {
*rs = append(*rs, MatchState{Rule: pr, Scope: scope, Regs: regs})
}
// Find looks for given rule and scope on the stack
func (rs *MatchStack) Find(pr *Rule, scope lexer.Reg) (*MatchState, bool) {
for i := range *rs {
r := &(*rs)[i]
if r.Rule == pr && r.Scope == scope {
return r, true
}
}
return nil, false
}
// AddMatch adds given rule to rule stack at given scope
func (ps *State) AddMatch(pr *Rule, scope lexer.Reg, regs Matches) {
rs := &ps.Matches[scope.St.Ln][scope.St.Ch]
rs.Add(pr, scope, regs)
}
// IsMatch looks for rule at given scope in list of matches, if found
// returns match state info
func (ps *State) IsMatch(pr *Rule, scope lexer.Reg) (*MatchState, bool) {
rs := &ps.Matches[scope.St.Ln][scope.St.Ch]
sz := len(*rs)
if sz == 0 {
return nil, false
}
return rs.Find(pr, scope)
}
// RuleString returns the rule info for entire source -- if full
// then it includes the full stack at each point -- otherwise just the top
// of stack
func (ps *State) RuleString(full bool) string {
txt := ""
nlines := ps.Src.NLines()
for ln := 0; ln < nlines; ln++ {
sz := len(ps.Matches[ln])
if sz == 0 {
txt += "\n"
} else {
for ch := 0; ch < sz; ch++ {
rs := ps.Matches[ln][ch]
sd := len(rs)
txt += ` "` + string(ps.Src.TokenSrc(lexer.Pos{ln, ch})) + `"`
if sd == 0 {
txt += "-"
} else {
if !full {
txt += rs[sd-1].String()
} else {
txt += fmt.Sprintf("[%v: ", sd)
for i := 0; i < sd; i++ {
txt += rs[i].String()
}
txt += "]"
}
}
}
txt += "\n"
}
}
return txt
}
///////////////////////////////////////////////////////////////////////////
// ScopeRuleSet and NonMatch
// ScopeRule is a scope and a rule, for storing matches / nonmatch
type ScopeRule struct {
Scope lexer.Reg
Rule *Rule
}
// ScopeRuleSet is a map by scope of RuleSets, for non-matching rules
type ScopeRuleSet map[ScopeRule]struct{}
// Add a rule to scope set, with auto-alloc
func (rs ScopeRuleSet) Add(scope lexer.Reg, pr *Rule) {
sr := ScopeRule{scope, pr}
rs[sr] = struct{}{}
}
// Has checks if scope rule set has given scope, rule
func (rs ScopeRuleSet) Has(scope lexer.Reg, pr *Rule) bool {
sr := ScopeRule{scope, pr}
_, has := rs[sr]
return has
}
// AddNonMatch adds given rule to non-matching rule set for this scope
func (ps *State) AddNonMatch(scope lexer.Reg, pr *Rule) {
ps.NonMatches.Add(scope, pr)
}
// IsNonMatch looks for rule in nonmatch list at given scope
func (ps *State) IsNonMatch(scope lexer.Reg, pr *Rule) bool {
return ps.NonMatches.Has(scope, pr)
}
// ResetNonMatches resets the non-match map -- do after every EOS
func (ps *State) ResetNonMatches() {
ps.NonMatches = make(ScopeRuleSet)
}
///////////////////////////////////////////////////////////////////////////
// Symbol management
// FindNameScoped searches top-down in the stack for something with the given name
// in symbols that are of subcategory token.NameScope (i.e., namespace, module, package, library)
// also looks in ps.Syms if not found in Scope stack.
func (ps *State) FindNameScoped(nm string) (*syms.Symbol, bool) {
sy, has := ps.Scopes.FindNameScoped(nm)
if has {
return sy, has
}
return ps.Syms.FindNameScoped(nm)
}
// FindNameTopScope searches only in top of current scope for something
//
// with the given name in symbols
//
// also looks in ps.Syms if not found in Scope stack.
func (ps *State) FindNameTopScope(nm string) (*syms.Symbol, bool) {
sy := ps.Scopes.Top()
if sy == nil {
return nil, false
}
chs, has := sy.Children[nm]
return chs, has
}
// 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 parser
import (
"fmt"
"os"
"strings"
"cogentcore.org/core/parse/lexer"
)
// TraceOptions provides options for debugging / monitoring the rule matching and execution process
type TraceOptions struct {
// perform tracing
On bool
// trace specific named rules here (space separated) -- if blank, then all rules are traced
Rules string `width:"50"`
// trace full rule matches -- when a rule fully matches
Match bool
// trace sub-rule matches -- when the parts of each rule match
SubMatch bool
// trace sub-rule non-matches -- why a rule doesn't match -- which terminates the matching process at first non-match (can be a lot of info)
NoMatch bool
// trace progress running through each of the sub-rules when a rule has matched and is 'running'
Run bool
// trace actions performed by running rules
RunAct bool
// if true, shows the full scope source for every trace statement
ScopeSrc bool
// for the ParseOut display, whether to display the full stack of rules at each position, or just the deepest one
FullStackOut bool
// list of rules
RulesList []string `display:"-" json:"-" xml:"-"`
// trace output is written here, connected via os.Pipe to OutRead
OutWrite *os.File `display:"-" json:"-" xml:"-"`
// trace output is read here; can connect this using [texteditor.OutputBuffer] to monitor tracing output
OutRead *os.File `display:"-" json:"-" xml:"-"`
}
// Init intializes tracer after any changes -- opens pipe if not already open
func (pt *TraceOptions) Init() {
if pt.Rules == "" {
pt.RulesList = nil
} else {
pt.RulesList = strings.Split(pt.Rules, " ")
}
}
// FullOn sets all options on
func (pt *TraceOptions) FullOn() {
pt.On = true
pt.Match = true
pt.SubMatch = true
pt.NoMatch = true
pt.Run = true
pt.RunAct = true
pt.ScopeSrc = true
}
// PipeOut sets output to a pipe for monitoring (OutWrite -> OutRead)
func (pt *TraceOptions) PipeOut() {
if pt.OutWrite == nil {
pt.OutRead, pt.OutWrite, _ = os.Pipe() // seriously, does this ever fail?
}
}
// Stdout sets [TraceOptions.OutWrite] to [os.Stdout]
func (pt *TraceOptions) Stdout() {
pt.OutWrite = os.Stdout
}
// CheckRule checks if given rule should be traced
func (pt *TraceOptions) CheckRule(rule string) bool {
if len(pt.RulesList) == 0 {
if pt.Rules != "" {
pt.Init()
if len(pt.RulesList) == 0 {
return true
}
} else {
return true
}
}
for _, rn := range pt.RulesList {
if rn == rule {
return true
}
}
return false
}
// Out outputs a trace message -- returns true if actually output
func (pt *TraceOptions) Out(ps *State, pr *Rule, step Steps, pos lexer.Pos, scope lexer.Reg, ast *AST, msg string) bool {
if !pt.On {
return false
}
if !pt.CheckRule(pr.Name) {
return false
}
switch step {
case Match:
if !pt.Match {
return false
}
case SubMatch:
if !pt.SubMatch {
return false
}
case NoMatch:
if !pt.NoMatch {
return false
}
case Run:
if !pt.Run {
return false
}
case RunAct:
if !pt.RunAct {
return false
}
}
tokSrc := pos.String() + `"` + string(ps.Src.TokenSrc(pos)) + `"`
plev := ast.ParentLevel(ps.AST)
ind := ""
for i := 0; i < plev; i++ {
ind += "\t"
}
fmt.Fprintf(pt.OutWrite, "%v%v:\t %v\t %v\t tok: %v\t scope: %v\t ast: %v\n", ind, pr.Name, step, msg, tokSrc, scope, ast.Path())
if pt.ScopeSrc {
scopeSrc := ps.Src.TokenRegSrc(scope)
fmt.Fprintf(pt.OutWrite, "%v\t%v\n", ind, scopeSrc)
}
return true
}
// CopyOpts copies just the options
func (pt *TraceOptions) CopyOpts(ot *TraceOptions) {
pt.On = ot.On
pt.Rules = ot.Rules
pt.Match = ot.Match
pt.SubMatch = ot.SubMatch
pt.NoMatch = ot.NoMatch
pt.Run = ot.Run
pt.RunAct = ot.RunAct
pt.ScopeSrc = ot.ScopeSrc
}
// Steps are the different steps of the parsing processing
type Steps int32 //enums:enum
// The parsing steps
const (
// Match happens when a rule matches
Match Steps = iota
// SubMatch is when a sub-rule within a rule matches
SubMatch
// NoMatch is when the rule fails to match (recorded at first non-match, which terminates
// matching process
NoMatch
// Run is when the rule is running and iterating through its sub-rules
Run
// RunAct is when the rule is running and performing actions
RunAct
)
// Code generated by "core generate"; DO NOT EDIT.
package parser
import (
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/parse/parser.AST", IDName: "ast", Doc: "AST is a node in the abstract syntax tree generated by the parsing step\nthe name of the node (from tree.NodeBase) is the type of the element\n(e.g., expr, stmt, etc)\nThese nodes are generated by the parser.Rule's by matching tokens", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "TokReg", Doc: "region in source lexical tokens corresponding to this AST node -- Ch = index in lex lines"}, {Name: "SrcReg", Doc: "region in source file corresponding to this AST node"}, {Name: "Src", Doc: "source code corresponding to this AST node"}, {Name: "Syms", Doc: "stack of symbols created for this node"}}})
// NewAST returns a new [AST] with the given optional parent:
// AST is a node in the abstract syntax tree generated by the parsing step
// the name of the node (from tree.NodeBase) is the type of the element
// (e.g., expr, stmt, etc)
// These nodes are generated by the parser.Rule's by matching tokens
func NewAST(parent ...tree.Node) *AST { return tree.New[AST](parent...) }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/parse/parser.Rule", IDName: "rule", Doc: "The first step is matching which searches in order for matches within the\nchildren of parent nodes, and for explicit rule nodes, it looks first\nthrough all the explicit tokens in the rule. If there are no explicit tokens\nthen matching defers to ONLY the first node listed by default -- you can\nadd a @ prefix to indicate a rule that is also essential to match.\n\nAfter a rule matches, it then proceeds through the rules narrowing the scope\nand calling the sub-nodes..", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Off", Doc: "disable this rule -- useful for testing and exploration"}, {Name: "Desc", Doc: "description / comments about this rule"}, {Name: "Rule", Doc: "the rule as a space-separated list of rule names and token(s) -- use single quotes around 'tokens' (using token.Tokens names or symbols). For keywords use 'key:keyword'. All tokens are matched at the same nesting depth as the start of the scope of this rule, unless they have a +D relative depth value differential before the token. Use @ prefix for a sub-rule to require that rule to match -- by default explicit tokens are used if available, and then only the first sub-rule failing that. Use ! by itself to define start of an exclusionary rule -- doesn't match when those rule elements DO match. Use : prefix for a special group node that matches a single token at start of scope, and then defers to the child rules to perform full match -- this is used for FirstTokenMap when there are multiple versions of a given keyword rule. Use - prefix for tokens anchored by the end (next token) instead of the previous one -- typically just for token prior to 'EOS' but also a block of tokens that need to go backward in the middle of a sequence to avoid ambiguity can be marked with -"}, {Name: "StackMatch", Doc: "if present, this rule only fires if stack has this on it"}, {Name: "AST", Doc: "what action should be take for this node when it matches"}, {Name: "Acts", Doc: "actions to perform based on parsed AST tree data, when this rule is done executing"}, {Name: "OptTokenMap", Doc: "for group-level rules having lots of children and lots of recursiveness, and also of high-frequency, when we first encounter such a rule, make a map of all the tokens in the entire scope, and use that for a first-pass rejection on matching tokens"}, {Name: "FirstTokenMap", Doc: "for group-level rules with a number of rules that match based on first tokens / keywords, build map to directly go to that rule -- must also organize all of these rules sequentially from the start -- if no match, goes directly to first non-lookup case"}, {Name: "Rules", Doc: "rule elements compiled from Rule string"}, {Name: "Order", Doc: "strategic matching order for matching the rules"}, {Name: "FiTokenMap", Doc: "map from first tokens / keywords to rules for FirstTokenMap case"}, {Name: "FiTokenElseIndex", Doc: "for FirstTokenMap, the start of the else cases not covered by the map"}, {Name: "ExclKeyIndex", Doc: "exclusionary key index -- this is the token in Rules that we need to exclude matches for using ExclFwd and ExclRev rules"}, {Name: "ExclFwd", Doc: "exclusionary forward-search rule elements compiled from Rule string"}, {Name: "ExclRev", Doc: "exclusionary reverse-search rule elements compiled from Rule string"}, {Name: "setsScope", Doc: "setsScope means that this rule sets its own scope, because it ends with EOS"}, {Name: "reverse", Doc: "reverse means that this rule runs in reverse (starts with - sign) -- for arithmetic\nbinary expressions only: this is needed to produce proper associativity result for\nmathematical expressions in the recursive descent parser.\nOnly for rules of form: Expr '+' Expr -- two sub-rules with a token operator\nin the middle."}, {Name: "noTokens", Doc: "noTokens means that this rule doesn't have any explicit tokens -- only refers to\nother rules"}, {Name: "onlyTokens", Doc: "onlyTokens means that this rule only has explicit tokens for matching -- can be\noptimized"}, {Name: "tokenMatchGroup", Doc: "tokenMatchGroup is a group node that also has a single token match, so it can\nbe used in a FirstTokenMap to optimize lookup of rules"}}})
// NewRule returns a new [Rule] with the given optional parent:
// The first step is matching which searches in order for matches within the
// children of parent nodes, and for explicit rule nodes, it looks first
// through all the explicit tokens in the rule. If there are no explicit tokens
// then matching defers to ONLY the first node listed by default -- you can
// add a @ prefix to indicate a rule that is also essential to match.
//
// After a rule matches, it then proceeds through the rules narrowing the scope
// and calling the sub-nodes..
func NewRule(parent ...tree.Node) *Rule { return tree.New[Rule](parent...) }
// SetOff sets the [Rule.Off]:
// disable this rule -- useful for testing and exploration
func (t *Rule) SetOff(v bool) *Rule { t.Off = v; return t }
// SetDesc sets the [Rule.Desc]:
// description / comments about this rule
func (t *Rule) SetDesc(v string) *Rule { t.Desc = v; return t }
// SetRule sets the [Rule.Rule]:
// the rule as a space-separated list of rule names and token(s) -- use single quotes around 'tokens' (using token.Tokens names or symbols). For keywords use 'key:keyword'. All tokens are matched at the same nesting depth as the start of the scope of this rule, unless they have a +D relative depth value differential before the token. Use @ prefix for a sub-rule to require that rule to match -- by default explicit tokens are used if available, and then only the first sub-rule failing that. Use ! by itself to define start of an exclusionary rule -- doesn't match when those rule elements DO match. Use : prefix for a special group node that matches a single token at start of scope, and then defers to the child rules to perform full match -- this is used for FirstTokenMap when there are multiple versions of a given keyword rule. Use - prefix for tokens anchored by the end (next token) instead of the previous one -- typically just for token prior to 'EOS' but also a block of tokens that need to go backward in the middle of a sequence to avoid ambiguity can be marked with -
func (t *Rule) SetRule(v string) *Rule { t.Rule = v; return t }
// SetStackMatch sets the [Rule.StackMatch]:
// if present, this rule only fires if stack has this on it
func (t *Rule) SetStackMatch(v string) *Rule { t.StackMatch = v; return t }
// SetAST sets the [Rule.AST]:
// what action should be take for this node when it matches
func (t *Rule) SetAST(v ASTActs) *Rule { t.AST = v; return t }
// SetActs sets the [Rule.Acts]:
// actions to perform based on parsed AST tree data, when this rule is done executing
func (t *Rule) SetActs(v Acts) *Rule { t.Acts = v; return t }
// SetOptTokenMap sets the [Rule.OptTokenMap]:
// for group-level rules having lots of children and lots of recursiveness, and also of high-frequency, when we first encounter such a rule, make a map of all the tokens in the entire scope, and use that for a first-pass rejection on matching tokens
func (t *Rule) SetOptTokenMap(v bool) *Rule { t.OptTokenMap = v; return t }
// SetFirstTokenMap sets the [Rule.FirstTokenMap]:
// for group-level rules with a number of rules that match based on first tokens / keywords, build map to directly go to that rule -- must also organize all of these rules sequentially from the start -- if no match, goes directly to first non-lookup case
func (t *Rule) SetFirstTokenMap(v bool) *Rule { t.FirstTokenMap = v; return t }
// SetRules sets the [Rule.Rules]:
// rule elements compiled from Rule string
func (t *Rule) SetRules(v RuleList) *Rule { t.Rules = v; return t }
// SetOrder sets the [Rule.Order]:
// strategic matching order for matching the rules
func (t *Rule) SetOrder(v ...int) *Rule { t.Order = v; return t }
// 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 syms
import (
"go/build"
"log"
"os"
"os/user"
"path/filepath"
"strings"
"time"
"cogentcore.org/core/base/fileinfo"
)
// ParseCacheDir returns the parse cache directory for given language, and ensures that it exists.
func ParseCacheDir(lang fileinfo.Known) (string, error) {
ucdir, err := os.UserCacheDir()
if err != nil {
return "", err
}
cdir := filepath.Join(filepath.Join(ucdir, "Cogent Core", "parse"), lang.String())
err = os.MkdirAll(cdir, 0775)
if err != nil {
log.Printf("ParseCacheDir: cache not available: %v\n", err)
}
return cdir, err
}
// GoRelPath returns the GOPATH or GOROOT relative path for given filename
func GoRelPath(filename string) (string, error) {
absfn, err := filepath.Abs(filename)
if err != nil {
return absfn, err
}
relfn := absfn
got := false
for _, srcDir := range build.Default.SrcDirs() {
if strings.HasPrefix(absfn, srcDir) {
relfn = strings.TrimPrefix(absfn, srcDir)
got = true
break
}
}
if got {
return relfn, nil
}
usr, err := user.Current()
if err != nil {
return relfn, err
}
homedir := usr.HomeDir
if strings.HasPrefix(absfn, homedir) {
relfn = strings.TrimPrefix(absfn, homedir)
}
return relfn, nil
}
// CacheFilename returns the filename to use for cache file for given filename
func CacheFilename(lang fileinfo.Known, filename string) (string, error) {
cdir, err := ParseCacheDir(lang)
if err != nil {
return "", err
}
relfn, err := GoRelPath(filename)
if err != nil {
return "", err
}
path := relfn
if filepath.Ext(path) != "" { // if it has an ext, it is not a dir..
path, _ = filepath.Split(path)
}
path = filepath.Clean(path)
if path[0] == filepath.Separator {
path = path[1:]
}
path = strings.Replace(path, string(filepath.Separator), "%", -1)
path = filepath.Join(cdir, path)
return path, nil
}
// SaveSymCache saves cache of symbols starting with given symbol
// (typically a package, module, library), which is at given
// filename
func SaveSymCache(sy *Symbol, lang fileinfo.Known, filename string) error {
cfile, err := CacheFilename(lang, filename)
if err != nil {
return err
}
return sy.SaveJSON(cfile)
}
// SaveSymDoc saves doc file of syms -- for double-checking contents etc
func SaveSymDoc(sy *Symbol, lang fileinfo.Known, filename string) error {
cfile, err := CacheFilename(lang, filename)
if err != nil {
return err
}
cfile += ".doc"
ofl, err := os.Create(cfile)
if err != nil {
return err
}
sy.WriteDoc(ofl, 0)
return nil
}
// OpenSymCache opens cache of symbols into given symbol
// (typically a package, module, library), which is at given
// filename -- returns time stamp when cache was last saved
func OpenSymCache(lang fileinfo.Known, filename string) (*Symbol, time.Time, error) {
cfile, err := CacheFilename(lang, filename)
if err != nil {
return nil, time.Time{}, err
}
info, err := os.Stat(cfile)
if os.IsNotExist(err) {
return nil, time.Time{}, err
}
sy := &Symbol{}
err = sy.OpenJSON(cfile)
return sy, info.ModTime(), err
}
// 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 syms
import (
"strings"
"cogentcore.org/core/icons"
"cogentcore.org/core/parse/complete"
"cogentcore.org/core/parse/token"
)
// AddCompleteSyms adds given symbols as matches in the given match data
// Scope is e.g., type name (label only)
func AddCompleteSyms(sym SymMap, scope string, md *complete.Matches) {
if len(sym) == 0 {
return
}
sys := sym.Slice(true) // sorted
for _, sy := range sys {
if sy.Name[0] == '_' { // internal / import
continue
}
nm := sy.Name
lbl := sy.Label()
if sy.Kind.SubCat() == token.NameFunction {
nm += "()"
}
if scope != "" {
lbl = lbl + " (" + scope + ".)"
}
c := complete.Completion{Text: nm, Label: lbl, Icon: sy.Kind.Icon(), Desc: sy.Detail}
// fmt.Printf("nm: %v kind: %v icon: %v\n", nm, sy.Kind, c.Icon)
md.Matches = append(md.Matches, c)
}
}
// AddCompleteTypeNames adds names from given type as matches in the given match data
// Scope is e.g., type name (label only), and seed is prefix filter for names
func AddCompleteTypeNames(typ *Type, scope, seed string, md *complete.Matches) {
md.Seed = seed
for _, te := range typ.Els {
nm := te.Name
if seed != "" {
if !strings.HasPrefix(nm, seed) {
continue
}
}
lbl := nm
if scope != "" {
lbl = lbl + " (" + scope + ".)"
}
icon := icons.Field // assume..
c := complete.Completion{Text: nm, Label: lbl, Icon: icon}
// fmt.Printf("nm: %v kind: %v icon: %v\n", nm, sy.Kind, c.Icon)
md.Matches = append(md.Matches, c)
}
for _, mt := range typ.Meths {
nm := mt.Name
if seed != "" {
if !strings.HasPrefix(nm, seed) {
continue
}
}
lbl := nm + "(" + mt.ArgString() + ") " + mt.ReturnString()
if scope != "" {
lbl = lbl + " (" + scope + ".)"
}
nm += "()"
icon := icons.Method // assume..
c := complete.Completion{Text: nm, Label: lbl, Icon: icon}
// fmt.Printf("nm: %v kind: %v icon: %v\n", nm, sy.Kind, c.Icon)
md.Matches = append(md.Matches, c)
}
}
// AddCompleteSymsPrefix adds subset of symbols that match seed prefix to given match data
func AddCompleteSymsPrefix(sym SymMap, scope, seed string, md *complete.Matches) {
matches := &sym
if seed != "" {
matches = &SymMap{}
md.Seed = seed
sym.FindNamePrefixRecursive(seed, matches)
}
AddCompleteSyms(*matches, scope, md)
}
// Code generated by "core generate"; DO NOT EDIT.
package syms
import (
"cogentcore.org/core/enums"
)
var _KindsValues = []Kinds{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50}
// KindsN is the highest valid value for type Kinds, plus one.
const KindsN Kinds = 51
var _KindsValueMap = map[string]Kinds{`Unknown`: 0, `Primitive`: 1, `Numeric`: 2, `Integer`: 3, `Signed`: 4, `Int`: 5, `Int8`: 6, `Int16`: 7, `Int32`: 8, `Int64`: 9, `Unsigned`: 10, `Uint`: 11, `Uint8`: 12, `Uint16`: 13, `Uint32`: 14, `Uint64`: 15, `Uintptr`: 16, `Ptr`: 17, `Ref`: 18, `UnsafePtr`: 19, `Fixed`: 20, `Fixed26_6`: 21, `Fixed16_6`: 22, `Fixed0_32`: 23, `Float`: 24, `Float16`: 25, `Float32`: 26, `Float64`: 27, `Complex`: 28, `Complex64`: 29, `Complex128`: 30, `Bool`: 31, `Composite`: 32, `Tuple`: 33, `Range`: 34, `Array`: 35, `List`: 36, `String`: 37, `Matrix`: 38, `Tensor`: 39, `Map`: 40, `Set`: 41, `FrozenSet`: 42, `Struct`: 43, `Class`: 44, `Object`: 45, `Chan`: 46, `Function`: 47, `Func`: 48, `Method`: 49, `Interface`: 50}
var _KindsDescMap = map[Kinds]string{0: `Unknown is the nil kind -- kinds should be known in general..`, 1: `Category: Primitive, in the strict sense of low-level, atomic, small, fixed size`, 2: `SubCat: Numeric`, 3: `Sub2Cat: Integer`, 4: `Sub3Cat: Signed -- track this using properties in types, not using Sub3 level`, 5: ``, 6: ``, 7: ``, 8: ``, 9: ``, 10: `Sub3Cat: Unsigned`, 11: ``, 12: ``, 13: ``, 14: ``, 15: ``, 16: ``, 17: `Sub3Cat: Ptr, Ref etc -- in Numeric, Integer even though in some languages pointer arithmetic might not be allowed, for some cases, etc`, 18: ``, 19: ``, 20: `Sub2Cat: Fixed point -- could be under integer, but..`, 21: ``, 22: ``, 23: ``, 24: `Sub2Cat: Floating point`, 25: ``, 26: ``, 27: ``, 28: `Sub3Cat: Complex -- under floating point`, 29: ``, 30: ``, 31: `SubCat: Bool`, 32: `Category: Composite -- types composed of above primitive types`, 33: `SubCat: Tuple -- a fixed length 1d collection of elements that can be of any type Type.Els required for each element`, 34: ``, 35: `SubCat: Array -- a fixed length 1d collection of same-type elements Type.Els has one element for type`, 36: `SubCat: List -- a variable-length 1d collection of same-type elements This is Slice for Go Type.Els has one element for type`, 37: ``, 38: `SubCat: Matrix -- a twod collection of same-type elements has two Size values, one for each dimension`, 39: `SubCat: Tensor -- an n-dimensional collection of same-type elements first element of Size is number of dimensions, rest are dimensions`, 40: `SubCat: Map -- an associative array / hash map / dictionary Type.Els first el is key, second is type`, 41: `SubCat: Set -- typically a degenerate form of hash map with no value`, 42: ``, 43: `SubCat: Struct -- like a tuple but with specific semantics in most languages Type.Els are the fields, and if there is an inheritance relationship these are put first with relevant identifiers -- in Go these are unnamed fields`, 44: ``, 45: ``, 46: `Chan: a channel (Go Specific)`, 47: `Category: Function -- types that are functions Type.Els are the params and return values in order, with Size[0] being number of params and Size[1] number of returns`, 48: `SubCat: Func -- a standalone function`, 49: `SubCat: Method -- a function with a specific receiver (e.g., on a Class in C++, or on any type in Go). First Type.Els is receiver param -- included in Size[0]`, 50: `SubCat: Interface -- an abstract definition of a set of methods (in Go) Type.Els are the Methods with the receiver type missing or Unknown`}
var _KindsMap = map[Kinds]string{0: `Unknown`, 1: `Primitive`, 2: `Numeric`, 3: `Integer`, 4: `Signed`, 5: `Int`, 6: `Int8`, 7: `Int16`, 8: `Int32`, 9: `Int64`, 10: `Unsigned`, 11: `Uint`, 12: `Uint8`, 13: `Uint16`, 14: `Uint32`, 15: `Uint64`, 16: `Uintptr`, 17: `Ptr`, 18: `Ref`, 19: `UnsafePtr`, 20: `Fixed`, 21: `Fixed26_6`, 22: `Fixed16_6`, 23: `Fixed0_32`, 24: `Float`, 25: `Float16`, 26: `Float32`, 27: `Float64`, 28: `Complex`, 29: `Complex64`, 30: `Complex128`, 31: `Bool`, 32: `Composite`, 33: `Tuple`, 34: `Range`, 35: `Array`, 36: `List`, 37: `String`, 38: `Matrix`, 39: `Tensor`, 40: `Map`, 41: `Set`, 42: `FrozenSet`, 43: `Struct`, 44: `Class`, 45: `Object`, 46: `Chan`, 47: `Function`, 48: `Func`, 49: `Method`, 50: `Interface`}
// String returns the string representation of this Kinds value.
func (i Kinds) String() string { return enums.String(i, _KindsMap) }
// SetString sets the Kinds value from its string representation,
// and returns an error if the string is invalid.
func (i *Kinds) SetString(s string) error { return enums.SetString(i, s, _KindsValueMap, "Kinds") }
// Int64 returns the Kinds value as an int64.
func (i Kinds) Int64() int64 { return int64(i) }
// SetInt64 sets the Kinds value from an int64.
func (i *Kinds) SetInt64(in int64) { *i = Kinds(in) }
// Desc returns the description of the Kinds value.
func (i Kinds) Desc() string { return enums.Desc(i, _KindsDescMap) }
// KindsValues returns all possible values for the type Kinds.
func KindsValues() []Kinds { return _KindsValues }
// Values returns all possible values for the type Kinds.
func (i Kinds) Values() []enums.Enum { return enums.Values(_KindsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Kinds) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Kinds) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Kinds") }
// 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 syms
//go:generate core generate
import (
"reflect"
)
// Kinds is a complete set of basic type categories and sub(sub..) categories -- these
// describe builtin types -- user-defined types must be some combination / version
// of these builtin types.
//
// See: https://en.wikipedia.org/wiki/List_of_data_structures
type Kinds int32 //enums:enum
// CatMap is the map into the category level for each kind
var CatMap map[Kinds]Kinds
// SubCatMap is the map into the sub-category level for each kind
var SubCatMap map[Kinds]Kinds
// Sub2CatMap is the map into the sub2-category level for each kind
var Sub2CatMap map[Kinds]Kinds
func init() {
InitCatMap()
InitSubCatMap()
InitSub2CatMap()
}
// Cat returns the category that a given kind lives in, using CatMap
func (tk Kinds) Cat() Kinds {
return CatMap[tk]
}
// SubCat returns the sub-category that a given kind lives in, using SubCatMap
func (tk Kinds) SubCat() Kinds {
return SubCatMap[tk]
}
// Sub2Cat returns the sub2-category that a given kind lives in, using Sub2CatMap
func (tk Kinds) Sub2Cat() Kinds {
return Sub2CatMap[tk]
}
// IsCat returns true if this is a category-level kind
func (tk Kinds) IsCat() bool {
return tk.Cat() == tk
}
// IsSubCat returns true if this is a sub-category-level kind
func (tk Kinds) IsSubCat() bool {
return tk.SubCat() == tk
}
// IsSub2Cat returns true if this is a sub2-category-level kind
func (tk Kinds) IsSub2Cat() bool {
return tk.Sub2Cat() == tk
}
func (tk Kinds) InCat(other Kinds) bool {
return tk.Cat() == other.Cat()
}
func (tk Kinds) InSubCat(other Kinds) bool {
return tk.SubCat() == other.SubCat()
}
func (tk Kinds) InSub2Cat(other Kinds) bool {
return tk.Sub2Cat() == other.Sub2Cat()
}
func (tk Kinds) IsPtr() bool {
return tk >= Ptr && tk <= UnsafePtr
}
func (tk Kinds) IsPrimitiveNonPtr() bool {
return tk.Cat() == Primitive && !tk.IsPtr()
}
// The list of Kinds
const (
// Unknown is the nil kind -- kinds should be known in general..
Unknown Kinds = iota
// Category: Primitive, in the strict sense of low-level, atomic, small, fixed size
Primitive
// SubCat: Numeric
Numeric
// Sub2Cat: Integer
Integer
// Sub3Cat: Signed -- track this using properties in types, not using Sub3 level
Signed
Int
Int8
Int16
Int32
Int64
// Sub3Cat: Unsigned
Unsigned
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr // generic raw pointer data value -- see also Ptr, Ref for more semantic cases
// Sub3Cat: Ptr, Ref etc -- in Numeric, Integer even though in some languages
// pointer arithmetic might not be allowed, for some cases, etc
Ptr // pointer -- element is what we point to (kind of a composite type)
Ref // reference -- element is what we refer to
UnsafePtr // for case where these are distinguished from Ptr (Go) -- similar to Uintptr
// Sub2Cat: Fixed point -- could be under integer, but..
Fixed
Fixed26_6
Fixed16_6
Fixed0_32
// Sub2Cat: Floating point
Float
Float16
Float32
Float64
// Sub3Cat: Complex -- under floating point
Complex
Complex64
Complex128
// SubCat: Bool
Bool
// Category: Composite -- types composed of above primitive types
Composite
// SubCat: Tuple -- a fixed length 1d collection of elements that can be of any type
// Type.Els required for each element
Tuple
Range // a special kind of tuple for Python ranges
// SubCat: Array -- a fixed length 1d collection of same-type elements
// Type.Els has one element for type
Array
// SubCat: List -- a variable-length 1d collection of same-type elements
// This is Slice for Go
// Type.Els has one element for type
List
String // List of some type of char rep -- Type.Els is type, as all Lists
// SubCat: Matrix -- a twod collection of same-type elements
// has two Size values, one for each dimension
Matrix
// SubCat: Tensor -- an n-dimensional collection of same-type elements
// first element of Size is number of dimensions, rest are dimensions
Tensor
// SubCat: Map -- an associative array / hash map / dictionary
// Type.Els first el is key, second is type
Map
// SubCat: Set -- typically a degenerate form of hash map with no value
Set
FrozenSet // python's frozen set of fixed values
// SubCat: Struct -- like a tuple but with specific semantics in most languages
// Type.Els are the fields, and if there is an inheritance relationship these
// are put first with relevant identifiers -- in Go these are unnamed fields
Struct
Class
Object
// Chan: a channel (Go Specific)
Chan
// Category: Function -- types that are functions
// Type.Els are the params and return values in order, with Size[0] being number
// of params and Size[1] number of returns
Function
// SubCat: Func -- a standalone function
Func
// SubCat: Method -- a function with a specific receiver (e.g., on a Class in C++,
// or on any type in Go).
// First Type.Els is receiver param -- included in Size[0]
Method
// SubCat: Interface -- an abstract definition of a set of methods (in Go)
// Type.Els are the Methods with the receiver type missing or Unknown
Interface
)
// Categories
var Cats = []Kinds{
Unknown,
Primitive,
Composite,
Function,
KindsN,
}
// Sub-Categories
var SubCats = []Kinds{
Unknown,
Primitive,
Numeric,
Bool,
Composite,
Tuple,
Array,
List,
Matrix,
Tensor,
Map,
Set,
Struct,
Chan,
Function,
Func,
Method,
Interface,
KindsN,
}
// Sub2-Categories
var Sub2Cats = []Kinds{
Unknown,
Primitive,
Numeric,
Integer,
Fixed,
Float,
Bool,
Composite,
Tuple,
Array,
List,
Matrix,
Tensor,
Map,
Set,
Struct,
Chan,
Function,
Func,
Method,
Interface,
KindsN,
}
// InitCatMap initializes the CatMap
func InitCatMap() {
if CatMap != nil {
return
}
CatMap = make(map[Kinds]Kinds, KindsN)
for tk := Unknown; tk < KindsN; tk++ {
for c := 1; c < len(Cats); c++ {
if tk < Cats[c] {
CatMap[tk] = Cats[c-1]
break
}
}
}
}
// InitSubCatMap initializes the SubCatMap
func InitSubCatMap() {
if SubCatMap != nil {
return
}
SubCatMap = make(map[Kinds]Kinds, KindsN)
for tk := Unknown; tk < KindsN; tk++ {
for c := 1; c < len(SubCats); c++ {
if tk < SubCats[c] {
SubCatMap[tk] = SubCats[c-1]
break
}
}
}
}
// InitSub2CatMap initializes the SubCatMap
func InitSub2CatMap() {
if Sub2CatMap != nil {
return
}
Sub2CatMap = make(map[Kinds]Kinds, KindsN)
for tk := Unknown; tk < KindsN; tk++ {
for c := 1; c < len(SubCats); c++ {
if tk < Sub2Cats[c] {
Sub2CatMap[tk] = Sub2Cats[c-1]
break
}
}
}
}
///////////////////////////////////////////////////////////////////////
// Reflect map
// ReflectKindMap maps reflect kinds to syms.Kinds
var ReflectKindMap = map[reflect.Kind]Kinds{
reflect.Invalid: Unknown,
reflect.Int: Int,
reflect.Int8: Int8,
reflect.Int16: Int16,
reflect.Int32: Int32,
reflect.Int64: Int64,
reflect.Uint: Uint,
reflect.Uint8: Uint8,
reflect.Uint16: Uint16,
reflect.Uint32: Uint32,
reflect.Uint64: Uint64,
reflect.Uintptr: Uintptr,
reflect.Float32: Float32,
reflect.Float64: Float64,
reflect.Complex64: Complex64,
reflect.Complex128: Complex128,
reflect.Bool: Bool,
reflect.Array: Array,
reflect.Chan: Chan,
reflect.Func: Func,
reflect.Interface: Interface,
reflect.Map: Map,
reflect.Pointer: Ptr,
reflect.Slice: List,
reflect.String: String,
reflect.Struct: Struct,
reflect.UnsafePointer: Ptr,
}
// 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 syms defines the symbols and their properties that
// are accumulated from a parsed file, and are then used for
// e.g., completion lookup, etc.
//
// We looked at several different standards for formats, types, etc:
//
// LSP: https://microsoft.github.io/language-server-protocol/specification
// useful to enable parse to act as an LSP server at some point.
// additional symbol kinds:
// https://github.com/Microsoft/language-server-protocol/issues/344
//
// See also: github.com/sourcegraph/sourcegraph
// and specifically: /cmd/frontend/graphqlbackend/search_symbols.go
// it seems to use https://github.com/universal-ctags/ctags
// for the raw data..
//
// Other relevant guidance comes from the go compiler system which is
// used extensively in github.com/mdemsky/gocode for example.
// In particular: go/types/scope.go type.go, and package.go contain the
// relevant data structures for how information is organized for
// compiled go packages, which have all this data cached and avail
// to be imported via the go/importer which returns a go/types/Package
// which in turn contains Scope's which in turn contain Objects that
// define the elements of the compiled language.
package syms
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"cogentcore.org/core/base/indent"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/token"
"cogentcore.org/core/tree"
)
// Symbol contains the information for everything about a given
// symbol that is created by parsing, and can be looked up.
// It corresponds to the LSP DocumentSymbol structure, and
// the Go Object type.
type Symbol struct {
// name of the symbol
Name string
// additional detail and specification of the symbol -- e.g. if a function, the signature of the function
Detail string
// lexical kind of symbol, using token.Tokens list
Kind token.Tokens
// Type name for this symbol -- if it is a type, this is its corresponding type representation -- if it is a variable then this is its type
Type string
// index for ordering children within a given scope, e.g., fields in a struct / class
Index int
// full filename / URI of source
Filename string
// region in source encompassing this item -- if = RegZero then this is a temp symbol and children are not added to it
Region lexer.Reg
// region that should be selected when activated, etc
SelectReg lexer.Reg
// relevant scoping / parent symbols, e.g., namespace, package, module, class, function, etc..
Scopes SymNames
// children of this symbol -- this includes e.g., methods and fields of classes / structs / types, and all elements within packages, etc
Children SymMap
// types defined within the scope of this symbol
Types TypeMap
// AST node that created this symbol -- only valid during parsing
AST tree.Node `json:"-" xml:"-"`
}
// NewSymbol returns a new symbol with the basic info filled in -- SelectReg defaults to Region
func NewSymbol(name string, kind token.Tokens, fname string, reg lexer.Reg) *Symbol {
sy := &Symbol{Name: name, Kind: kind, Filename: fname, Region: reg, SelectReg: reg}
return sy
}
// CopyFromSrc copies all the source-related fields from other symbol
// (no Type, Types, or Children). AST is only copied if non-nil.
func (sy *Symbol) CopyFromSrc(cp *Symbol) {
sy.Detail = cp.Detail
sy.Kind = cp.Kind
sy.Index = cp.Index
sy.Filename = cp.Filename
sy.Region = cp.Region
sy.SelectReg = cp.SelectReg
// if cp.AST != nil {
// sy.AST = cp.AST
// }
}
// IsTemp returns true if this is temporary symbol that is used for scoping but is not
// otherwise permanently added to list of symbols. Indicated by Zero Region.
func (sy *Symbol) IsTemp() bool {
return sy.Region == lexer.RegZero
}
// HasChildren returns true if this symbol has children
func (sy *Symbol) HasChildren() bool {
return len(sy.Children) > 0
}
// String satisfies fmt.Stringer interface
func (sy *Symbol) String() string {
return fmt.Sprintf("%v: %v (%v)", sy.Name, sy.Kind, sy.Region)
}
// Label satisfies core.Labeler interface -- nicer presentation label
func (sy *Symbol) Label() string {
lbl := sy.Name
switch {
case sy.Kind.SubCat() == token.NameFunction:
pi := strings.Index(sy.Detail, "(")
if pi >= 0 {
lbl += sy.Detail[pi:]
} else {
lbl += "()"
}
default:
if sy.Type != "" {
lbl += " (" + sy.Type + ")"
}
}
return lbl
}
// Clone returns a clone copy of this symbol.
// Does NOT copy the Children or Types -- caller can decide about that.
func (sy *Symbol) Clone() *Symbol {
nsy := &Symbol{Name: sy.Name, Detail: sy.Detail, Kind: sy.Kind, Type: sy.Type, Index: sy.Index, Filename: sy.Filename, Region: sy.Region, SelectReg: sy.SelectReg}
nsy.Scopes = sy.Scopes.Clone()
// nsy.AST = sy.AST
return nsy
}
// AddChild adds a child symbol, if this parent symbol is not temporary
// returns true if item name was added and NOT already on the map,
// and false if it was already or parent is temp.
// Always adds new symbol in any case.
// If parent symbol is of the NameType subcategory, then index of child is set
// to the size of this child map before adding.
func (sy *Symbol) AddChild(child *Symbol) bool {
// if sy.IsTemp() {
// return false
// }
sy.Children.Alloc()
_, on := sy.Children[child.Name]
idx := len(sy.Children)
child.Index = idx // always record index
sy.Children[child.Name] = child
return !on
}
// AllocScopes allocates scopes map if nil
func (sy *Symbol) AllocScopes() {
if sy.Scopes == nil {
sy.Scopes = make(SymNames)
}
}
// AddScopesMap adds a given scope element(s) from map to this Symbol.
// if add is true, add this symbol to those scopes if they are not temporary.
func (sy *Symbol) AddScopesMap(sm SymMap, add bool) {
if len(sm) == 0 {
return
}
sy.AllocScopes()
for _, s := range sm {
sy.Scopes[s.Kind] = s.Name
if add {
s.AddChild(sy)
}
}
}
// AddScopesStack adds a given scope element(s) from stack to this Symbol.
// Adds this symbol as a child to the top of the scopes if it is not temporary --
// returns true if so added.
func (sy *Symbol) AddScopesStack(ss SymStack) bool {
sz := len(ss)
if sz == 0 {
return false
}
sy.AllocScopes()
added := false
for i := 0; i < sz; i++ {
sc := ss[i]
sy.Scopes[sc.Kind] = sc.Name
if i == sz-1 {
added = sc.AddChild(sy)
}
}
return added
}
// FindAnyChildren finds children of this symbol using either
// direct children if those are present, or the type name if
// present -- used for completion routines. Adds to kids map.
// scope1, scope2 are used for looking up type name.
// If seed is non-empty it is used as a prefix for filtering children names.
// Returns false if no children were found.
func (sy *Symbol) FindAnyChildren(seed string, scope1, scope2 SymMap, kids *SymMap) bool {
sym := sy
if len(sym.Children) == 0 {
if sym.Type != "" {
tynm := sym.NonPtrTypeName()
if typ, got := scope1.FindNameScoped(tynm); got {
sym = typ
} else if typ, got := scope2.FindNameScoped(tynm); got {
sym = typ
} else {
return false
}
}
}
if seed != "" {
sym.Children.FindNamePrefixRecursive(seed, kids)
} else {
kids.CopyFrom(sym.Children, true) // srcIsNewer
}
return len(*kids) > 0
}
// NonPtrTypeName returns the name of the type without any leading * or &
func (sy *Symbol) NonPtrTypeName() string {
return strings.TrimPrefix(strings.TrimPrefix(sy.Type, "&"), "*")
}
// CopyFromScope copies the Children and Types from given other symbol
// for scopes (e.g., Go package), to merge with existing.
func (sy *Symbol) CopyFromScope(src *Symbol) {
sy.Children.CopyFrom(src.Children, false) // src is NOT newer
sy.Types.CopyFrom(src.Types, false) // src is NOT newer
}
// OpenJSON opens from a JSON-formatted file.
func (sy *Symbol) OpenJSON(filename string) error {
b, err := os.ReadFile(filename)
if err != nil {
return err
}
return json.Unmarshal(b, sy)
}
// SaveJSON saves to a JSON-formatted file.
func (sy *Symbol) SaveJSON(filename string) error {
b, err := json.MarshalIndent(sy, "", " ")
if err != nil {
return err
}
err = os.WriteFile(filename, b, 0644)
return err
}
// WriteDoc writes basic doc info
func (sy *Symbol) WriteDoc(out io.Writer, depth int) {
ind := indent.Tabs(depth)
fmt.Fprintf(out, "%v%v: %v", ind, sy.Name, sy.Kind)
if sy.Type != "" {
fmt.Fprintf(out, " (%v)", sy.Type)
}
if len(sy.Types) > 0 {
fmt.Fprint(out, " Types: {\n")
sy.Types.WriteDoc(out, depth+1)
fmt.Fprintf(out, "%v}\n", ind)
}
if sy.HasChildren() {
fmt.Fprint(out, " {\n")
sy.Children.WriteDoc(out, depth+1)
fmt.Fprintf(out, "%v}\n", ind)
} else {
fmt.Fprint(out, "\n")
}
}
// ClearAST sets the AST pointers to nil for all symbols in this one.
// otherwise the AST memory is never freed and can get quite large.
func (sm *Symbol) ClearAST() {
sm.AST = nil
sm.Children.ClearAST()
sm.Types.ClearAST()
}
// ClearAST sets the AST pointers to nil for all symbols.
// otherwise the AST memory is never freed and can get quite large.
func (sm *SymMap) ClearAST() {
for _, ss := range *sm {
ss.ClearAST()
}
}
// ClearAST sets the AST pointers to nil for all symbols in this one.
// otherwise the AST memory is never freed and can get quite large.
func (ty *Type) ClearAST() {
ty.AST = nil
ty.Meths.ClearAST()
}
// ClearAST sets the AST pointers to nil for all symbols.
// otherwise the AST memory is never freed and can get quite large.
func (tm *TypeMap) ClearAST() {
for _, ty := range *tm {
ty.ClearAST()
}
}
//////////////////////////////////////////////////////////////////
// SymNames
// SymNames provides a map-list of symbol names, indexed by their token kinds.
// Used primarily for specifying Scopes
type SymNames map[token.Tokens]string
// SubCat returns a scope with the given SubCat type, or false if not found
func (sn *SymNames) SubCat(sc token.Tokens) (string, bool) {
for tk, nm := range *sn {
if tk.SubCat() == sc {
return nm, true
}
}
return "", false
}
// Clone returns a clone copy of this map (nil if empty)
func (sn *SymNames) Clone() SymNames {
sz := len(*sn)
if sz == 0 {
return nil
}
nsn := make(SymNames, sz)
for tk, nm := range *sn {
nsn[tk] = nm
}
return nsn
}
// 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 syms
import (
"encoding/json"
"io"
"os"
"path/filepath"
"sort"
"strings"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/token"
)
// SymMap is a map between symbol names and their full information.
// A given project will have a top-level SymMap and perhaps local
// maps for individual files, etc. Namespaces / packages can be
// created and elements added to them to create appropriate
// scoping structure etc. Note that we have to use pointers
// for symbols b/c otherwise it is very expensive to re-assign
// values all the time -- https://github.com/golang/go/issues/3117
type SymMap map[string]*Symbol
// Alloc ensures that map is made
func (sm *SymMap) Alloc() {
if *sm == nil {
*sm = make(SymMap)
}
}
// Add adds symbol to map
func (sm *SymMap) Add(sy *Symbol) {
sm.Alloc()
(*sm)[sy.Name] = sy
}
// AddNew adds a new symbol to the map with the basic info
func (sm *SymMap) AddNew(name string, kind token.Tokens, fname string, reg lexer.Reg) *Symbol {
sy := NewSymbol(name, kind, fname, reg)
sm.Alloc()
(*sm)[name] = sy
return sy
}
// Reset resets the symbol map
func (sm *SymMap) Reset() {
*sm = make(SymMap)
}
// CopyFrom copies all the symbols from given source map into this one,
// including merging everything from common elements.
// Symbols with Type resolved are retained when there are duplicates.
// srcIsNewer means that the src map has the newer information to grab for
// updating the symbol region info during the merge.
func (sm *SymMap) CopyFrom(src SymMap, srcIsNewer bool) {
sm.Alloc()
for nm, ssy := range src {
dsy, has := (*sm)[nm]
if !has {
(*sm)[nm] = ssy
continue
}
if srcIsNewer {
dsy.CopyFromSrc(ssy)
} else {
ssy.CopyFromSrc(dsy)
}
if dsy.Type != "" {
// fmt.Printf("dupe sym: %v, using existing with type: %v\n", nm, dsy.Type)
// fmt.Printf("\texisting region: %v new source region: %v\n", dsy.Region, ssy.Region)
dsy.Children.CopyFrom(ssy.Children, srcIsNewer)
} else if ssy.Type != "" {
// fmt.Printf("dupe sym: %v, using new with type: %v\n", nm, ssy.Type)
// fmt.Printf("\texisting region: %v new source region: %v\n", dsy.Region, ssy.Region)
ssy.Children.CopyFrom(dsy.Children, !srcIsNewer)
(*sm)[nm] = ssy
} else {
dsy.Children.CopyFrom(ssy.Children, srcIsNewer)
}
}
}
// First returns the first symbol in the map -- only sensible when there
// is just one such element
func (sm *SymMap) First() *Symbol {
for _, sy := range *sm {
return sy
}
return nil
}
// Slice returns a slice of the elements in the map, optionally sorted by name
func (sm *SymMap) Slice(sorted bool) []*Symbol {
sys := make([]*Symbol, len(*sm))
idx := 0
for _, sy := range *sm {
sys[idx] = sy
idx++
}
if sorted {
sort.Slice(sys, func(i, j int) bool {
return sys[i].Name < sys[j].Name
})
}
return sys
}
// FindName looks for given symbol name within this map and any children on the map
func (sm *SymMap) FindName(nm string) (*Symbol, bool) {
if *sm == nil {
return nil, false
}
sy, has := (*sm)[nm]
if has {
return sy, has
}
for _, ss := range *sm {
if sy, has = ss.Children.FindName(nm); has {
return sy, has
}
}
return nil, false
}
// FindNameScoped looks for given symbol name within this map and any children on the map
// that are of subcategory token.NameScope (i.e., namespace, module, package, library)
func (sm *SymMap) FindNameScoped(nm string) (*Symbol, bool) {
if *sm == nil {
return nil, false
}
sy, has := (*sm)[nm]
if has {
return sy, has
}
for _, ss := range *sm {
if ss.Kind.SubCat() == token.NameScope {
if sy, has = ss.Children.FindNameScoped(nm); has {
return sy, has
}
}
}
return nil, false
}
// FindKind looks for given symbol kind within this map and any children on the map
// Returns all instances found. Uses cat / subcat based token matching -- if you
// specify a category-level or subcategory level token, it will match everything in that group
func (sm *SymMap) FindKind(kind token.Tokens, matches *SymMap) {
if *sm == nil {
return
}
for _, sy := range *sm {
if kind.Match(sy.Kind) {
if *matches == nil {
*matches = make(SymMap)
}
(*matches)[sy.Name] = sy
}
}
for _, ss := range *sm {
ss.Children.FindKind(kind, matches)
}
}
// FindKindScoped looks for given symbol kind within this map and any children on the map
// that are of subcategory token.NameScope (i.e., namespace, module, package, library).
// Returns all instances found. Uses cat / subcat based token matching -- if you
// specify a category-level or subcategory level token, it will match everything in that group
func (sm *SymMap) FindKindScoped(kind token.Tokens, matches *SymMap) {
if *sm == nil {
return
}
for _, sy := range *sm {
if kind.Match(sy.Kind) {
if *matches == nil {
*matches = make(SymMap)
}
(*matches)[sy.Name] = sy
}
}
for _, ss := range *sm {
if ss.Kind.SubCat() == token.NameScope {
ss.Children.FindKindScoped(kind, matches)
}
}
}
// FindContainsRegion looks for given symbol kind that contains the given
// source file path (must be filepath.Abs file path) and position.
// Returns all instances found. Uses cat / subcat based token matching -- if you
// specify a category-level or subcategory level token, it will match everything
// in that group. if you specify kind = token.None then all tokens that contain
// region will be returned. extraLns are extra lines added to the symbol region
// for purposes of matching.
func (sm *SymMap) FindContainsRegion(fpath string, pos lexer.Pos, extraLns int, kind token.Tokens, matches *SymMap) {
if *sm == nil {
return
}
for _, sy := range *sm {
fp, _ := filepath.Abs(sy.Filename)
if fp != fpath {
continue
}
reg := sy.Region
if extraLns > 0 {
reg.Ed.Ln += extraLns
}
if !reg.Contains(pos) {
continue
}
if kind == token.None || kind.Match(sy.Kind) {
if *matches == nil {
*matches = make(SymMap)
}
(*matches)[sy.Name] = sy
}
}
for _, ss := range *sm {
ss.Children.FindContainsRegion(fpath, pos, extraLns, kind, matches)
}
}
// OpenJSON opens from a JSON-formatted file.
func (sm *SymMap) OpenJSON(filename string) error {
b, err := os.ReadFile(filename)
if err != nil {
return err
}
return json.Unmarshal(b, sm)
}
// SaveJSON saves to a JSON-formatted file.
func (sm *SymMap) SaveJSON(filename string) error {
b, err := json.MarshalIndent(sm, "", " ")
if err != nil {
return err
}
err = os.WriteFile(filename, b, 0644)
return err
}
// Names returns a slice of the names in this map, optionally sorted
func (sm *SymMap) Names(sorted bool) []string {
nms := make([]string, len(*sm))
idx := 0
for _, sy := range *sm {
nms[idx] = sy.Name
idx++
}
if sorted {
sort.StringSlice(nms).Sort()
}
return nms
}
// KindNames returns a slice of the kind:names in this map, optionally sorted
func (sm *SymMap) KindNames(sorted bool) []string {
nms := make([]string, len(*sm))
idx := 0
for _, sy := range *sm {
nms[idx] = sy.Kind.String() + ":" + sy.Name
idx++
}
if sorted {
sort.StringSlice(nms).Sort()
}
return nms
}
// WriteDoc writes basic doc info, sorted by kind and name
func (sm *SymMap) WriteDoc(out io.Writer, depth int) {
nms := sm.KindNames(true)
for _, nm := range nms {
ci := strings.Index(nm, ":")
sy := (*sm)[nm[ci+1:]]
sy.WriteDoc(out, depth)
}
}
//////////////////////////////////////////////////////////////////////
// Partial lookups
// FindNamePrefix looks for given symbol name prefix within this map
// adds to given matches map (which can be nil), for more efficient recursive use
func (sm *SymMap) FindNamePrefix(seed string, matches *SymMap) {
noCase := true
if lexer.HasUpperCase(seed) {
noCase = false
}
for _, sy := range *sm {
nm := sy.Name
if noCase {
nm = strings.ToLower(nm)
}
if strings.HasPrefix(nm, seed) {
if *matches == nil {
*matches = make(SymMap)
}
(*matches)[sy.Name] = sy
}
}
}
// FindNamePrefixRecursive looks for given symbol name prefix within this map
// and any children on the map.
// adds to given matches map (which can be nil), for more efficient recursive use
func (sm *SymMap) FindNamePrefixRecursive(seed string, matches *SymMap) {
noCase := true
if lexer.HasUpperCase(seed) {
noCase = false
}
for _, sy := range *sm {
nm := sy.Name
if noCase {
nm = strings.ToLower(nm)
}
if strings.HasPrefix(nm, seed) {
if *matches == nil {
*matches = make(SymMap)
}
(*matches)[sy.Name] = sy
}
}
for _, ss := range *sm {
ss.Children.FindNamePrefixRecursive(seed, matches)
}
}
// FindNamePrefixScoped looks for given symbol name prefix within this map
// and any children on the map that are of subcategory
// token.NameScope (i.e., namespace, module, package, library)
// adds to given matches map (which can be nil), for more efficient recursive use
func (sm *SymMap) FindNamePrefixScoped(seed string, matches *SymMap) {
noCase := true
if lexer.HasUpperCase(seed) {
noCase = false
}
for _, sy := range *sm {
nm := sy.Name
if nm[0] == '"' {
nm = strings.Trim(nm, `"`) // path names may be quoted
nm = filepath.Base(nm) // sorry, this is a bit of a Go-specific hack to look at package names only
sy = sy.Clone()
sy.Name = nm
}
if noCase {
nm = strings.ToLower(nm)
}
if strings.HasPrefix(nm, seed) {
if *matches == nil {
*matches = make(SymMap)
}
(*matches)[nm] = sy
}
}
for _, ss := range *sm {
if ss.Kind.SubCat() == token.NameScope {
ss.Children.FindNamePrefixScoped(seed, matches)
}
}
}
// 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 syms
import (
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/token"
)
// SymStack is a simple stack (slice) of symbols
type SymStack []*Symbol
// Top returns the state at the top of the stack (could be nil)
func (ss *SymStack) Top() *Symbol {
sz := len(*ss)
if sz == 0 {
return nil
}
return (*ss)[sz-1]
}
// Push appends symbol to stack
func (ss *SymStack) Push(sy *Symbol) {
*ss = append(*ss, sy)
}
// PushNew adds a new symbol to the stack with the basic info
func (ss *SymStack) PushNew(name string, kind token.Tokens, fname string, reg lexer.Reg) *Symbol {
sy := NewSymbol(name, kind, fname, reg)
ss.Push(sy)
return sy
}
// Pop takes symbol off the stack and returns it
func (ss *SymStack) Pop() *Symbol {
sz := len(*ss)
if sz == 0 {
return nil
}
sy := (*ss)[sz-1]
*ss = (*ss)[:sz-1]
return sy
}
// Reset resets the stack
func (ss *SymStack) Reset() {
*ss = nil
}
// FindNameScoped searches top-down in the stack for something with the given name
// in symbols that are of subcategory token.NameScope (i.e., namespace, module, package, library)
func (ss *SymStack) FindNameScoped(nm string) (*Symbol, bool) {
sz := len(*ss)
if sz == 0 {
return nil, false
}
for i := sz - 1; i >= 0; i-- {
sy := (*ss)[i]
if sy.Name == nm {
return sy, true
}
ssy, has := sy.Children.FindNameScoped(nm)
if has {
return ssy, true
}
}
return nil, false
}
func (ss *SymStack) ClearAST() {
for _, sy := range *ss {
sy.ClearAST()
}
}
// 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 syms
import (
"fmt"
"io"
"maps"
"slices"
"cogentcore.org/core/base/indent"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/tree"
)
// Type contains all the information about types. Types can be builtin
// or composed of builtin types. Each type can have one or more elements,
// e.g., fields for a struct or class, multiple values for a go function,
// or the two types for a map (key, value), etc..
type Type struct {
// name of the type -- can be the name of a field or the role for a type element
Name string
// kind of type -- overall nature of the type
Kind Kinds
// documentation about this type, extracted from code
Desc string
// set to true after type has been initialized during post-parse processing
Initialized bool `edit:"-"`
// elements of this type -- ordering and meaning varies depending on the Kind of type -- for Primitive types this is the parent type, for Composite types it describes the key elements of the type: Tuple = each element's type; Array = type of elements; Struct = each field, etc (see docs for each in Kinds)
Els TypeEls
// methods defined for this type
Meths TypeMap
// for primitive types, this is the number of bytes, for composite types, it is the number of elements, which can be multi-dimensional (e.g., for functions, number of params is (including receiver param for methods) and return vals is )
Size []int
// full filename / URI of source where type is defined (may be empty for auto types)
Filename string
// region in source encompassing this type
Region lexer.Reg
// relevant scoping / parent symbols, e.g., namespace, package, module, class, function, etc..
Scopes SymNames
// additional type properties, such as const, virtual, static -- these are just recorded textually and not systematized to keep things open-ended -- many of the most important properties can be inferred from the Kind property
Properties map[string]any
// AST node that corresponds to this type -- only valid during parsing
AST tree.Node `json:"-" xml:"-"`
}
// NewType returns a new Type struct initialized with given name and kind
func NewType(name string, kind Kinds) *Type {
ty := &Type{Name: name, Kind: kind}
return ty
}
// AllocScopes allocates scopes map if nil
func (ty *Type) AllocScopes() {
if ty.Scopes == nil {
ty.Scopes = make(SymNames)
}
}
// CopyFromSrc copies source-level data from given other type
func (ty *Type) CopyFromSrc(cp *Type) {
ty.Filename = cp.Filename
ty.Region = cp.Region
if cp.AST != nil {
ty.AST = cp.AST
}
}
// Clone returns a deep copy of this type, cloning / copying all sub-elements
// except the AST and Initialized
func (ty *Type) Clone() *Type {
// note: not copying Initialized
nty := &Type{Name: ty.Name, Kind: ty.Kind, Desc: ty.Desc, Filename: ty.Filename, Region: ty.Region, AST: ty.AST}
nty.Els.CopyFrom(ty.Els)
nty.Meths = ty.Meths.Clone()
nty.Size = slices.Clone(ty.Size)
nty.Scopes = ty.Scopes.Clone()
maps.Copy(nty.Properties, ty.Properties)
return nty
}
// AddScopesStack adds a given scope element(s) from stack to this Type.
func (ty *Type) AddScopesStack(ss SymStack) {
sz := len(ss)
if sz == 0 {
return
}
ty.AllocScopes()
for i := 0; i < sz; i++ {
sc := ss[i]
ty.Scopes[sc.Kind] = sc.Name
}
}
// String() satisfies the fmt.Stringer interface
func (ty *Type) String() string {
switch {
case ty.Kind.Cat() == Function && len(ty.Size) == 2:
str := "func "
npars := ty.Size[0]
if ty.Kind.SubCat() == Method {
str += "(" + ty.Els.StringRange(0, 1) + ") " + ty.Name + "(" + ty.Els.StringRange(1, npars-1) + ")"
} else {
str += ty.Name + "(" + ty.Els.StringRange(0, npars) + ")"
}
nrets := ty.Size[1]
if nrets == 1 {
tel := ty.Els[npars]
str += " " + tel.Type
} else if nrets > 1 {
str += " (" + ty.Els.StringRange(npars, nrets) + ")"
}
return str
case ty.Kind.SubCat() == Map:
return "map[" + ty.Els[0].Type + "]" + ty.Els[1].Type
case ty.Kind == String:
return "string"
case ty.Kind.SubCat() == List:
return "[]" + ty.Els[0].Type
}
return ty.Name + ": " + ty.Kind.String()
}
// ArgString() returns string of args to function if it is a function type
func (ty *Type) ArgString() string {
if ty.Kind.Cat() != Function || len(ty.Size) != 2 {
return ""
}
npars := ty.Size[0]
if ty.Kind.SubCat() == Method {
return ty.Els.StringRange(1, npars-1)
} else {
return ty.Els.StringRange(0, npars)
}
}
// ReturnString() returns string of return vals of function if it is a function type
func (ty *Type) ReturnString() string {
if ty.Kind.Cat() != Function || len(ty.Size) != 2 {
return ""
}
npars := ty.Size[0]
nrets := ty.Size[1]
if nrets == 1 {
tel := ty.Els[npars]
return tel.Type
} else if nrets > 1 {
return "(" + ty.Els.StringRange(npars, nrets) + ")"
}
return ""
}
// NonPtrType returns the non-pointer name of this type, if it is a pointer type
// otherwise just returns Name
func (ty *Type) NonPtrType() string {
if !(ty.Kind == Ptr || ty.Kind == Ref || ty.Kind == UnsafePtr) {
return ty.Name
}
if len(ty.Els) == 1 {
return ty.Els[0].Type
}
return ty.Name // shouldn't happen
}
// WriteDoc writes basic doc info
func (ty *Type) WriteDoc(out io.Writer, depth int) {
ind := indent.Tabs(depth)
fmt.Fprintf(out, "%v%v: %v", ind, ty.Name, ty.Kind)
if len(ty.Size) == 1 {
fmt.Fprintf(out, " Size: %v", ty.Size[0])
} else if len(ty.Size) > 1 {
fmt.Fprint(out, " Size: { ")
for i := range ty.Size {
fmt.Fprintf(out, "%v, ", ty.Size[i])
}
fmt.Fprint(out, " }")
}
if ty.Kind.SubCat() == Struct && len(ty.Els) > 0 {
fmt.Fprint(out, " {\n")
indp := indent.Tabs(depth + 1)
for i := range ty.Els {
fmt.Fprintf(out, "%v%v\n", indp, ty.Els[i].String())
}
fmt.Fprintf(out, "%v}\n", ind)
} else {
fmt.Fprint(out, "\n")
}
if len(ty.Meths) > 0 {
fmt.Fprint(out, "Methods: {\n")
indp := indent.Tabs(depth + 1)
for _, m := range ty.Meths {
fmt.Fprintf(out, "%v%v\n", indp, m.String())
}
fmt.Fprintf(out, "%v}\n", ind)
} else {
fmt.Fprint(out, "\n")
}
}
//////////////////////////////////////////////////////////////////////////////////
// TypeEls
// TypeEl is a type element -- has a name (local to the type, e.g., field name)
// and a type name that can be looked up in a master list of types
type TypeEl struct {
// element name -- e.g., field name for struct, or functional name for other types
Name string
// type name -- looked up on relevant lists -- includes scoping / package / namespace name as appropriate
Type string
}
// String() satisfies the fmt.Stringer interface
func (tel *TypeEl) String() string {
if tel.Name != "" {
return tel.Name + " " + tel.Type
}
return tel.Type
}
// Clone() returns a copy of this el
func (tel *TypeEl) Clone() *TypeEl {
te := &TypeEl{Name: tel.Name, Type: tel.Type}
return te
}
// TypeEls are the type elements for types
type TypeEls []TypeEl
// Add adds a new type element
func (te *TypeEls) Add(nm, typ string) {
(*te) = append(*te, TypeEl{Name: nm, Type: typ})
}
// CopyFrom copies from another list
func (te *TypeEls) CopyFrom(cp TypeEls) {
for _, t := range cp {
(*te) = append(*te, t)
}
}
// ByName returns type el with given name, nil if not there
func (te *TypeEls) ByName(nm string) *TypeEl {
for i := range *te {
el := &(*te)[i]
if el.Name == nm {
return el
}
}
return nil
}
// ByType returns type el with given type, nil if not there
func (te *TypeEls) ByType(typ string) *TypeEl {
for i := range *te {
el := &(*te)[i]
if el.Type == typ {
return el
}
}
return nil
}
// String() satisfies the fmt.Stringer interface
func (te *TypeEls) String() string {
return te.StringRange(0, len(*te))
}
// StringRange() returns a string rep of range of items
func (te *TypeEls) StringRange(st, n int) string {
n = min(n, len(*te))
str := ""
for i := 0; i < n; i++ {
tel := (*te)[st+i]
str += tel.String()
if i < n-1 {
str += ", "
}
}
return str
}
// 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 syms
import (
"fmt"
"io"
"sort"
"strings"
)
// TypeMap is a map of types for quick looking up by name
type TypeMap map[string]*Type
// Alloc ensures that map is made
func (tm *TypeMap) Alloc() {
if *tm == nil {
*tm = make(TypeMap)
}
}
// Add adds a type to the map, handling allocation for nil maps
func (tm *TypeMap) Add(ty *Type) {
tm.Alloc()
(*tm)[ty.Name] = ty
}
// CopyFrom copies all the types from given source map into this one.
// Types that have Kind != Unknown are retained over unknown ones.
// srcIsNewer means that the src type is newer and should be used for
// other data like source
func (tm *TypeMap) CopyFrom(src TypeMap, srcIsNewer bool) {
tm.Alloc()
for nm, sty := range src {
ety, has := (*tm)[nm]
if !has {
(*tm)[nm] = sty
continue
}
if srcIsNewer {
ety.CopyFromSrc(sty)
} else {
sty.CopyFromSrc(ety)
}
if ety.Kind != Unknown {
// if len(sty.Els) > 0 && len(sty.Els) != len(ety.Els) {
// fmt.Printf("TypeMap dupe type: %v existing kind: %v els: %v new els: %v kind: %v\n", nm, ety.Kind, len(ety.Els), len(sty.Els), sty.Kind)
// }
} else if sty.Kind != Unknown {
// if len(ety.Els) > 0 && len(sty.Els) != len(ety.Els) {
// fmt.Printf("TypeMap dupe type: %v new kind: %v els: %v existing els: %v\n", nm, sty.Kind, len(sty.Els), len(ety.Els))
// }
(*tm)[nm] = sty
// } else {
// fmt.Printf("TypeMap dupe type: %v both unknown: existing els: %v new els: %v\n", nm, len(ety.Els), len(sty.Els))
}
}
}
// Clone returns deep copy of this type map -- types are Clone() copies.
// returns nil if this map is empty
func (tm *TypeMap) Clone() TypeMap {
sz := len(*tm)
if sz == 0 {
return nil
}
ntm := make(TypeMap, sz)
for nm, sty := range *tm {
ntm[nm] = sty.Clone()
}
return ntm
}
// Names returns a slice of the names in this map, optionally sorted
func (tm *TypeMap) Names(sorted bool) []string {
nms := make([]string, len(*tm))
idx := 0
for _, ty := range *tm {
nms[idx] = ty.Name
idx++
}
if sorted {
sort.StringSlice(nms).Sort()
}
return nms
}
// KindNames returns a slice of the kind:names in this map, optionally sorted
func (tm *TypeMap) KindNames(sorted bool) []string {
nms := make([]string, len(*tm))
idx := 0
for _, ty := range *tm {
nms[idx] = ty.Kind.String() + ":" + ty.Name
idx++
}
if sorted {
sort.StringSlice(nms).Sort()
}
return nms
}
// PrintUnknowns prints all the types that have a Kind = Unknown
// indicates an error in type resolution
func (tm *TypeMap) PrintUnknowns() {
for _, ty := range *tm {
if ty.Kind != Unknown {
continue
}
fmt.Printf("Type named: %v has Kind = Unknown! From file: %v:%v\n", ty.Name, ty.Filename, ty.Region)
}
}
// WriteDoc writes basic doc info, sorted by kind and name
func (tm *TypeMap) WriteDoc(out io.Writer, depth int) {
nms := tm.KindNames(true)
for _, nm := range nms {
ci := strings.Index(nm, ":")
ty := (*tm)[nm[ci+1:]]
ty.WriteDoc(out, depth)
}
}
// TypeKindSize is used for initialization of builtin typemaps
type TypeKindSize struct {
Name string
Kind Kinds
Size int
}
// Code generated by "core generate"; DO NOT EDIT.
package token
import (
"cogentcore.org/core/enums"
)
var _TokensValues = []Tokens{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176}
// TokensN is the highest valid value for type Tokens, plus one.
const TokensN Tokens = 177
var _TokensValueMap = map[string]Tokens{`None`: 0, `Error`: 1, `EOF`: 2, `EOL`: 3, `EOS`: 4, `Background`: 5, `Keyword`: 6, `KeywordConstant`: 7, `KeywordDeclaration`: 8, `KeywordNamespace`: 9, `KeywordPseudo`: 10, `KeywordReserved`: 11, `KeywordType`: 12, `Name`: 13, `NameBuiltin`: 14, `NameBuiltinPseudo`: 15, `NameOther`: 16, `NamePseudo`: 17, `NameType`: 18, `NameClass`: 19, `NameStruct`: 20, `NameField`: 21, `NameInterface`: 22, `NameConstant`: 23, `NameEnum`: 24, `NameEnumMember`: 25, `NameArray`: 26, `NameMap`: 27, `NameObject`: 28, `NameTypeParam`: 29, `NameFunction`: 30, `NameDecorator`: 31, `NameFunctionMagic`: 32, `NameMethod`: 33, `NameOperator`: 34, `NameConstructor`: 35, `NameException`: 36, `NameLabel`: 37, `NameEvent`: 38, `NameScope`: 39, `NameNamespace`: 40, `NameModule`: 41, `NamePackage`: 42, `NameLibrary`: 43, `NameVar`: 44, `NameVarAnonymous`: 45, `NameVarClass`: 46, `NameVarGlobal`: 47, `NameVarInstance`: 48, `NameVarMagic`: 49, `NameVarParam`: 50, `NameValue`: 51, `NameTag`: 52, `NameProperty`: 53, `NameAttribute`: 54, `NameEntity`: 55, `Literal`: 56, `LiteralDate`: 57, `LiteralOther`: 58, `LiteralBool`: 59, `LitStr`: 60, `LitStrAffix`: 61, `LitStrAtom`: 62, `LitStrBacktick`: 63, `LitStrBoolean`: 64, `LitStrChar`: 65, `LitStrDelimiter`: 66, `LitStrDoc`: 67, `LitStrDouble`: 68, `LitStrEscape`: 69, `LitStrHeredoc`: 70, `LitStrInterpol`: 71, `LitStrName`: 72, `LitStrOther`: 73, `LitStrRegex`: 74, `LitStrSingle`: 75, `LitStrSymbol`: 76, `LitStrFile`: 77, `LitNum`: 78, `LitNumBin`: 79, `LitNumFloat`: 80, `LitNumHex`: 81, `LitNumInteger`: 82, `LitNumIntegerLong`: 83, `LitNumOct`: 84, `LitNumImag`: 85, `Operator`: 86, `OperatorWord`: 87, `OpMath`: 88, `OpMathAdd`: 89, `OpMathSub`: 90, `OpMathMul`: 91, `OpMathDiv`: 92, `OpMathRem`: 93, `OpBit`: 94, `OpBitAnd`: 95, `OpBitOr`: 96, `OpBitNot`: 97, `OpBitXor`: 98, `OpBitShiftLeft`: 99, `OpBitShiftRight`: 100, `OpBitAndNot`: 101, `OpAsgn`: 102, `OpAsgnAssign`: 103, `OpAsgnInc`: 104, `OpAsgnDec`: 105, `OpAsgnArrow`: 106, `OpAsgnDefine`: 107, `OpMathAsgn`: 108, `OpMathAsgnAdd`: 109, `OpMathAsgnSub`: 110, `OpMathAsgnMul`: 111, `OpMathAsgnDiv`: 112, `OpMathAsgnRem`: 113, `OpBitAsgn`: 114, `OpBitAsgnAnd`: 115, `OpBitAsgnOr`: 116, `OpBitAsgnXor`: 117, `OpBitAsgnShiftLeft`: 118, `OpBitAsgnShiftRight`: 119, `OpBitAsgnAndNot`: 120, `OpLog`: 121, `OpLogAnd`: 122, `OpLogOr`: 123, `OpLogNot`: 124, `OpRel`: 125, `OpRelEqual`: 126, `OpRelNotEqual`: 127, `OpRelLess`: 128, `OpRelGreater`: 129, `OpRelLtEq`: 130, `OpRelGtEq`: 131, `OpList`: 132, `OpListEllipsis`: 133, `Punctuation`: 134, `PunctGp`: 135, `PunctGpLParen`: 136, `PunctGpRParen`: 137, `PunctGpLBrack`: 138, `PunctGpRBrack`: 139, `PunctGpLBrace`: 140, `PunctGpRBrace`: 141, `PunctSep`: 142, `PunctSepComma`: 143, `PunctSepPeriod`: 144, `PunctSepSemicolon`: 145, `PunctSepColon`: 146, `PunctStr`: 147, `PunctStrDblQuote`: 148, `PunctStrQuote`: 149, `PunctStrBacktick`: 150, `PunctStrEsc`: 151, `Comment`: 152, `CommentHashbang`: 153, `CommentMultiline`: 154, `CommentSingle`: 155, `CommentSpecial`: 156, `CommentPreproc`: 157, `CommentPreprocFile`: 158, `Text`: 159, `TextWhitespace`: 160, `TextSymbol`: 161, `TextPunctuation`: 162, `TextSpellErr`: 163, `TextStyle`: 164, `TextStyleDeleted`: 165, `TextStyleEmph`: 166, `TextStyleError`: 167, `TextStyleHeading`: 168, `TextStyleInserted`: 169, `TextStyleOutput`: 170, `TextStylePrompt`: 171, `TextStyleStrong`: 172, `TextStyleSubheading`: 173, `TextStyleTraceback`: 174, `TextStyleUnderline`: 175, `TextStyleLink`: 176}
var _TokensDescMap = map[Tokens]string{0: `None is the nil token value -- for non-terminal cases or TBD`, 1: `Error is an input that could not be tokenized due to syntax error etc`, 2: `EOF is end of file`, 3: `EOL is end of line (typically implicit -- used for rule matching)`, 4: `EOS is end of statement -- a key meta-token -- in C it is ;, in Go it is either ; or EOL`, 5: `Background is for syntax highlight styles based on these tokens`, 6: `Cat: Keywords (actual keyword is just the string)`, 7: ``, 8: ``, 9: ``, 10: ``, 11: ``, 12: ``, 13: `Cat: Names.`, 14: ``, 15: ``, 16: ``, 17: ``, 18: `SubCat: Type names`, 19: ``, 20: ``, 21: ``, 22: ``, 23: ``, 24: ``, 25: ``, 26: ``, 27: ``, 28: ``, 29: ``, 30: `SubCat: Function names`, 31: ``, 32: ``, 33: ``, 34: ``, 35: ``, 36: ``, 37: ``, 38: ``, 39: `SubCat: Scoping names`, 40: ``, 41: ``, 42: ``, 43: ``, 44: `SubCat: NameVar -- variable names`, 45: ``, 46: ``, 47: ``, 48: ``, 49: ``, 50: ``, 51: `SubCat: Value -- data-like elements`, 52: ``, 53: ``, 54: ``, 55: ``, 56: `Cat: Literals.`, 57: ``, 58: ``, 59: ``, 60: `SubCat: Literal Strings.`, 61: ``, 62: ``, 63: ``, 64: ``, 65: ``, 66: ``, 67: ``, 68: ``, 69: ``, 70: ``, 71: ``, 72: ``, 73: ``, 74: ``, 75: ``, 76: ``, 77: ``, 78: `SubCat: Literal Numbers.`, 79: ``, 80: ``, 81: ``, 82: ``, 83: ``, 84: ``, 85: ``, 86: `Cat: Operators.`, 87: ``, 88: `SubCat: Math operators`, 89: ``, 90: ``, 91: ``, 92: ``, 93: ``, 94: `SubCat: Bitwise operators`, 95: ``, 96: ``, 97: ``, 98: ``, 99: ``, 100: ``, 101: ``, 102: `SubCat: Assign operators`, 103: ``, 104: ``, 105: ``, 106: ``, 107: ``, 108: `SubCat: Math Assign operators`, 109: ``, 110: ``, 111: ``, 112: ``, 113: ``, 114: `SubCat: Bitwise Assign operators`, 115: ``, 116: ``, 117: ``, 118: ``, 119: ``, 120: ``, 121: `SubCat: Logical operators`, 122: ``, 123: ``, 124: ``, 125: `SubCat: Relational operators`, 126: ``, 127: ``, 128: ``, 129: ``, 130: ``, 131: ``, 132: `SubCat: List operators`, 133: ``, 134: `Cat: Punctuation.`, 135: `SubCat: Grouping punctuation`, 136: ``, 137: ``, 138: ``, 139: ``, 140: ``, 141: ``, 142: `SubCat: Separator punctuation`, 143: ``, 144: ``, 145: ``, 146: ``, 147: `SubCat: String punctuation`, 148: ``, 149: ``, 150: ``, 151: ``, 152: `Cat: Comments.`, 153: ``, 154: ``, 155: ``, 156: ``, 157: `SubCat: Preprocessor "comments".`, 158: ``, 159: `Cat: Text.`, 160: ``, 161: ``, 162: ``, 163: ``, 164: `SubCat: TextStyle (corresponds to Generic in chroma / pygments) todo: look in font deco for more`, 165: ``, 166: ``, 167: ``, 168: ``, 169: ``, 170: ``, 171: ``, 172: ``, 173: ``, 174: ``, 175: ``, 176: ``}
var _TokensMap = map[Tokens]string{0: `None`, 1: `Error`, 2: `EOF`, 3: `EOL`, 4: `EOS`, 5: `Background`, 6: `Keyword`, 7: `KeywordConstant`, 8: `KeywordDeclaration`, 9: `KeywordNamespace`, 10: `KeywordPseudo`, 11: `KeywordReserved`, 12: `KeywordType`, 13: `Name`, 14: `NameBuiltin`, 15: `NameBuiltinPseudo`, 16: `NameOther`, 17: `NamePseudo`, 18: `NameType`, 19: `NameClass`, 20: `NameStruct`, 21: `NameField`, 22: `NameInterface`, 23: `NameConstant`, 24: `NameEnum`, 25: `NameEnumMember`, 26: `NameArray`, 27: `NameMap`, 28: `NameObject`, 29: `NameTypeParam`, 30: `NameFunction`, 31: `NameDecorator`, 32: `NameFunctionMagic`, 33: `NameMethod`, 34: `NameOperator`, 35: `NameConstructor`, 36: `NameException`, 37: `NameLabel`, 38: `NameEvent`, 39: `NameScope`, 40: `NameNamespace`, 41: `NameModule`, 42: `NamePackage`, 43: `NameLibrary`, 44: `NameVar`, 45: `NameVarAnonymous`, 46: `NameVarClass`, 47: `NameVarGlobal`, 48: `NameVarInstance`, 49: `NameVarMagic`, 50: `NameVarParam`, 51: `NameValue`, 52: `NameTag`, 53: `NameProperty`, 54: `NameAttribute`, 55: `NameEntity`, 56: `Literal`, 57: `LiteralDate`, 58: `LiteralOther`, 59: `LiteralBool`, 60: `LitStr`, 61: `LitStrAffix`, 62: `LitStrAtom`, 63: `LitStrBacktick`, 64: `LitStrBoolean`, 65: `LitStrChar`, 66: `LitStrDelimiter`, 67: `LitStrDoc`, 68: `LitStrDouble`, 69: `LitStrEscape`, 70: `LitStrHeredoc`, 71: `LitStrInterpol`, 72: `LitStrName`, 73: `LitStrOther`, 74: `LitStrRegex`, 75: `LitStrSingle`, 76: `LitStrSymbol`, 77: `LitStrFile`, 78: `LitNum`, 79: `LitNumBin`, 80: `LitNumFloat`, 81: `LitNumHex`, 82: `LitNumInteger`, 83: `LitNumIntegerLong`, 84: `LitNumOct`, 85: `LitNumImag`, 86: `Operator`, 87: `OperatorWord`, 88: `OpMath`, 89: `OpMathAdd`, 90: `OpMathSub`, 91: `OpMathMul`, 92: `OpMathDiv`, 93: `OpMathRem`, 94: `OpBit`, 95: `OpBitAnd`, 96: `OpBitOr`, 97: `OpBitNot`, 98: `OpBitXor`, 99: `OpBitShiftLeft`, 100: `OpBitShiftRight`, 101: `OpBitAndNot`, 102: `OpAsgn`, 103: `OpAsgnAssign`, 104: `OpAsgnInc`, 105: `OpAsgnDec`, 106: `OpAsgnArrow`, 107: `OpAsgnDefine`, 108: `OpMathAsgn`, 109: `OpMathAsgnAdd`, 110: `OpMathAsgnSub`, 111: `OpMathAsgnMul`, 112: `OpMathAsgnDiv`, 113: `OpMathAsgnRem`, 114: `OpBitAsgn`, 115: `OpBitAsgnAnd`, 116: `OpBitAsgnOr`, 117: `OpBitAsgnXor`, 118: `OpBitAsgnShiftLeft`, 119: `OpBitAsgnShiftRight`, 120: `OpBitAsgnAndNot`, 121: `OpLog`, 122: `OpLogAnd`, 123: `OpLogOr`, 124: `OpLogNot`, 125: `OpRel`, 126: `OpRelEqual`, 127: `OpRelNotEqual`, 128: `OpRelLess`, 129: `OpRelGreater`, 130: `OpRelLtEq`, 131: `OpRelGtEq`, 132: `OpList`, 133: `OpListEllipsis`, 134: `Punctuation`, 135: `PunctGp`, 136: `PunctGpLParen`, 137: `PunctGpRParen`, 138: `PunctGpLBrack`, 139: `PunctGpRBrack`, 140: `PunctGpLBrace`, 141: `PunctGpRBrace`, 142: `PunctSep`, 143: `PunctSepComma`, 144: `PunctSepPeriod`, 145: `PunctSepSemicolon`, 146: `PunctSepColon`, 147: `PunctStr`, 148: `PunctStrDblQuote`, 149: `PunctStrQuote`, 150: `PunctStrBacktick`, 151: `PunctStrEsc`, 152: `Comment`, 153: `CommentHashbang`, 154: `CommentMultiline`, 155: `CommentSingle`, 156: `CommentSpecial`, 157: `CommentPreproc`, 158: `CommentPreprocFile`, 159: `Text`, 160: `TextWhitespace`, 161: `TextSymbol`, 162: `TextPunctuation`, 163: `TextSpellErr`, 164: `TextStyle`, 165: `TextStyleDeleted`, 166: `TextStyleEmph`, 167: `TextStyleError`, 168: `TextStyleHeading`, 169: `TextStyleInserted`, 170: `TextStyleOutput`, 171: `TextStylePrompt`, 172: `TextStyleStrong`, 173: `TextStyleSubheading`, 174: `TextStyleTraceback`, 175: `TextStyleUnderline`, 176: `TextStyleLink`}
// String returns the string representation of this Tokens value.
func (i Tokens) String() string { return enums.String(i, _TokensMap) }
// SetString sets the Tokens value from its string representation,
// and returns an error if the string is invalid.
func (i *Tokens) SetString(s string) error { return enums.SetString(i, s, _TokensValueMap, "Tokens") }
// Int64 returns the Tokens value as an int64.
func (i Tokens) Int64() int64 { return int64(i) }
// SetInt64 sets the Tokens value from an int64.
func (i *Tokens) SetInt64(in int64) { *i = Tokens(in) }
// Desc returns the description of the Tokens value.
func (i Tokens) Desc() string { return enums.Desc(i, _TokensDescMap) }
// TokensValues returns all possible values for the type Tokens.
func TokensValues() []Tokens { return _TokensValues }
// Values returns all possible values for the type Tokens.
func (i Tokens) Values() []enums.Enum { return enums.Values(_TokensValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Tokens) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Tokens) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Tokens") }
// 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 token defines a complete set of all lexical tokens for any kind of
// language! It is based on the alecthomas/chroma / pygments lexical tokens
// plus all the more detailed tokens needed for actually parsing languages
package token
//go:generate core generate
import (
"fmt"
"cogentcore.org/core/icons"
)
// Tokens is a complete set of lexical tokens that encompasses all programming and text
// markup languages. It includes everything in alecthomas/chroma (pygments) and
// everything needed for Go, C, C++, Python, etc.
//
// There are categories and sub-categories, and methods to get those from a given
// element. The first category is 'None'.
//
// See http://pygments.org/docs/tokens/ for more docs on the different categories
//
// Anything missing should be added via a pull request etc
type Tokens int32 //enums:enum
// CatMap is the map into the category level for each token
var CatMap map[Tokens]Tokens
// SubCatMap is the map into the sub-category level for each token
var SubCatMap map[Tokens]Tokens
func init() {
InitCatMap()
InitSubCatMap()
}
// Cat returns the category that a given token lives in, using CatMap
func (tk Tokens) Cat() Tokens {
return CatMap[tk]
}
// SubCat returns the sub-category that a given token lives in, using SubCatMap
func (tk Tokens) SubCat() Tokens {
return SubCatMap[tk]
}
// IsCat returns true if this is a category-level token
func (tk Tokens) IsCat() bool {
return tk.Cat() == tk
}
// IsSubCat returns true if this is a sub-category-level token
func (tk Tokens) IsSubCat() bool {
return tk.SubCat() == tk
}
// InCat returns true if this is in same category as given token
func (tk Tokens) InCat(other Tokens) bool {
return tk.Cat() == other.Cat()
}
// InCat returns true if this is in same sub-category as given token
func (tk Tokens) InSubCat(other Tokens) bool {
return tk.SubCat() == other.SubCat()
}
// IsCode returns true if this token is in Keyword, Name, Operator, or Punctuation categs.
// these are recognized code (program) elements that can usefully be distinguished from
// other forms of raw text (e.g., for spell checking)
func (tk Tokens) IsCode() bool {
return tk.InCat(Keyword) || tk.InCat(Name) || tk.InCat(Operator) || tk.InCat(Punctuation)
}
// IsKeyword returns true if this in the Keyword category
func (tk Tokens) IsKeyword() bool {
return tk.Cat() == Keyword
}
// Parent returns the closest parent-level of this token (subcat or cat)
func (tk Tokens) Parent() Tokens {
if tk.IsSubCat() {
return tk.Cat()
}
return tk.SubCat()
}
// Match returns true if the two tokens match, in a category / subcategory sensitive manner:
// if receiver token is a category, then it matches other token if it is the same category
// and likewise for subcategory
func (tk Tokens) Match(otk Tokens) bool {
if tk == otk {
return true
}
if tk.IsCat() && otk.Cat() == tk {
return true
}
return tk.IsSubCat() && otk.SubCat() == tk
}
// IsPunctGpLeft returns true if token is a PunctGpL token -- left paren, brace, bracket
func (tk Tokens) IsPunctGpLeft() bool {
return (tk == PunctGpLParen || tk == PunctGpLBrack || tk == PunctGpLBrace)
}
// IsPunctGpRight returns true if token is a PunctGpR token -- right paren, brace, bracket
func (tk Tokens) IsPunctGpRight() bool {
return (tk == PunctGpRParen || tk == PunctGpRBrack || tk == PunctGpRBrace)
}
// PunctGpMatch returns the matching token for given PunctGp token
func (tk Tokens) PunctGpMatch() Tokens {
switch tk {
case PunctGpLParen:
return PunctGpRParen
case PunctGpRParen:
return PunctGpLParen
case PunctGpLBrack:
return PunctGpRBrack
case PunctGpRBrack:
return PunctGpLBrack
case PunctGpLBrace:
return PunctGpRBrace
case PunctGpRBrace:
return PunctGpLBrace
}
return None
}
// IsAmbigUnaryOp returns true if this token is an operator that could either be
// a Unary or Binary operator -- need special matching for this.
// includes * and & which are used for address operations in C-like languages
func (tk Tokens) IsAmbigUnaryOp() bool {
return (tk == OpMathSub || tk == OpMathMul || tk == OpBitAnd || tk == OpMathAdd || tk == OpBitXor)
}
// IsUnaryOp returns true if this token is an operator that is typically used as
// a Unary operator: - + & * ! ^ ! <-
func (tk Tokens) IsUnaryOp() bool {
return (tk == OpMathSub || tk == OpMathMul || tk == OpBitAnd || tk == OpMathAdd ||
tk == OpBitXor || tk == OpLogNot || tk == OpAsgnArrow)
}
// CombineRepeats are token types where repeated tokens of the same type should
// be combined together -- literals, comments, text
func (tk Tokens) CombineRepeats() bool {
cat := tk.Cat()
return (cat == Literal || cat == Comment || cat == Text || cat == Name)
}
// StyleName returns the abbreviated 2-3 letter style name of the tag
func (tk Tokens) StyleName() string {
return Names[tk]
}
// ClassName returns the . prefixed CSS classname of the tag style
// for styling, a CSS property should exist with this name
func (tk Tokens) ClassName() string {
return "." + tk.StyleName()
}
/////////////////////////////////////////////////////////////////////////////
// KeyToken -- keyword + token
// KeyToken combines a token and an optional keyword name for Keyword token types
// if Tok is in Keyword category, then Key string can be used to check for same keyword.
// Also has a Depth for matching against a particular nesting depth
type KeyToken struct {
Token Tokens
Key string
Depth int
}
var KeyTokenZero = KeyToken{}
func (kt KeyToken) String() string {
ds := ""
if kt.Depth != 0 {
ds = fmt.Sprintf("+%d:", kt.Depth)
}
if kt.Key != "" {
return ds + kt.Token.String() + ": " + kt.Key
}
return ds + kt.Token.String()
}
// Equal compares equality of two tokens including keywords if token is in Keyword category.
// See also Match for version that uses category / subcategory matching
func (kt KeyToken) Equal(okt KeyToken) bool {
if kt.Token.IsKeyword() && kt.Key != "" {
return kt.Token == okt.Token && kt.Key == okt.Key
}
return kt.Token == okt.Token
}
// Match compares equality of two tokens including keywords if token is in Keyword category.
// returns true if the two tokens match, in a category / subcategory sensitive manner:
// if receiver token is a category, then it matches other token if it is the same category
// and likewise for subcategory
func (kt KeyToken) Match(okt KeyToken) bool {
if kt.Token.IsKeyword() && kt.Key != "" {
return kt.Token.Match(okt.Token) && kt.Key == okt.Key
}
return kt.Token.Match(okt.Token)
}
// MatchDepth compares equality of two tokens including depth -- see Match for other matching
// criteria
func (kt KeyToken) MatchDepth(okt KeyToken) bool {
if kt.Depth != okt.Depth {
return false
}
return kt.Match(okt)
}
// StringKey encodes token into a string for optimized string-based map key lookup
func (kt KeyToken) StringKey() string {
tstr := string([]byte{byte(kt.Token)})
if kt.Token.IsKeyword() {
return tstr + kt.Key
} else {
return tstr
}
}
// KeyTokenList is a list (slice) of KeyTokens
type KeyTokenList []KeyToken
// Match returns true if given keytoken matches any of the items on the list
func (kl KeyTokenList) Match(okt KeyToken) bool {
for _, kt := range kl {
if kt.Match(okt) {
return true
}
}
return false
}
// Icon returns the appropriate icon for the type of lexical item this is.
func (tk Tokens) Icon() icons.Icon {
switch {
case tk.SubCat() == NameVar:
return icons.Variable
case tk == NameConstant || tk == NameEnum || tk == NameEnumMember:
return icons.Constant
case tk == NameField:
return icons.Field
case tk.SubCat() == NameType:
return icons.Type
case tk == NameMethod:
return icons.Method
case tk.SubCat() == NameFunction:
return icons.Function
}
return ""
}
/////////////////////////////////////////////////////////////////////////////
// Tokens
// The list of tokens
const (
// None is the nil token value -- for non-terminal cases or TBD
None Tokens = iota
// Error is an input that could not be tokenized due to syntax error etc
Error
// EOF is end of file
EOF
// EOL is end of line (typically implicit -- used for rule matching)
EOL
// EOS is end of statement -- a key meta-token -- in C it is ;, in Go it is either ; or EOL
EOS
// Background is for syntax highlight styles based on these tokens
Background
// Cat: Keywords (actual keyword is just the string)
Keyword
KeywordConstant
KeywordDeclaration
KeywordNamespace // incl package, import
KeywordPseudo
KeywordReserved
KeywordType
// Cat: Names.
Name
NameBuiltin // e.g., true, false -- builtin values..
NameBuiltinPseudo // e.g., this, self
NameOther
NamePseudo
// SubCat: Type names
NameType
NameClass
NameStruct
NameField
NameInterface
NameConstant
NameEnum
NameEnumMember
NameArray // includes slice etc
NameMap
NameObject
NameTypeParam // for generics, templates
// SubCat: Function names
NameFunction
NameDecorator // function-like wrappers in python
NameFunctionMagic // e.g., __init__ in python
NameMethod
NameOperator
NameConstructor // includes destructor..
NameException
NameLabel // e.g., goto label
NameEvent // for LSP -- not really sure what it is..
// SubCat: Scoping names
NameScope
NameNamespace
NameModule
NamePackage
NameLibrary
// SubCat: NameVar -- variable names
NameVar
NameVarAnonymous
NameVarClass
NameVarGlobal
NameVarInstance
NameVarMagic
NameVarParam
// SubCat: Value -- data-like elements
NameValue
NameTag // e.g., HTML tag
NameProperty
NameAttribute // e.g., HTML attr
NameEntity // special entities. (e.g. in HTML). seems like other..
// Cat: Literals.
Literal
LiteralDate
LiteralOther
LiteralBool
// SubCat: Literal Strings.
LitStr
LitStrAffix // unicode specifiers etc
LitStrAtom
LitStrBacktick
LitStrBoolean
LitStrChar
LitStrDelimiter
LitStrDoc // doc-specific strings where syntactically noted
LitStrDouble
LitStrEscape // esc sequences within strings
LitStrHeredoc // in ruby, perl
LitStrInterpol // interpolated parts of strings in #{foo} in Ruby
LitStrName
LitStrOther
LitStrRegex
LitStrSingle
LitStrSymbol
LitStrFile // filename
// SubCat: Literal Numbers.
LitNum
LitNumBin
LitNumFloat
LitNumHex
LitNumInteger
LitNumIntegerLong
LitNumOct
LitNumImag
// Cat: Operators.
Operator
OperatorWord
// SubCat: Math operators
OpMath
OpMathAdd // +
OpMathSub // -
OpMathMul // *
OpMathDiv // /
OpMathRem // %
// SubCat: Bitwise operators
OpBit
OpBitAnd // &
OpBitOr // |
OpBitNot // ~
OpBitXor // ^
OpBitShiftLeft // <<
OpBitShiftRight // >>
OpBitAndNot // &^
// SubCat: Assign operators
OpAsgn
OpAsgnAssign // =
OpAsgnInc // ++
OpAsgnDec // --
OpAsgnArrow // <-
OpAsgnDefine // :=
// SubCat: Math Assign operators
OpMathAsgn
OpMathAsgnAdd // +=
OpMathAsgnSub // -=
OpMathAsgnMul // *=
OpMathAsgnDiv // /=
OpMathAsgnRem // %=
// SubCat: Bitwise Assign operators
OpBitAsgn
OpBitAsgnAnd // &=
OpBitAsgnOr // |=
OpBitAsgnXor // ^=
OpBitAsgnShiftLeft // <<=
OpBitAsgnShiftRight // >>=
OpBitAsgnAndNot // &^=
// SubCat: Logical operators
OpLog
OpLogAnd // &&
OpLogOr // ||
OpLogNot // !
// SubCat: Relational operators
OpRel
OpRelEqual // ==
OpRelNotEqual // !=
OpRelLess // <
OpRelGreater // >
OpRelLtEq // <=
OpRelGtEq // >=
// SubCat: List operators
OpList
OpListEllipsis // ...
// Cat: Punctuation.
Punctuation
// SubCat: Grouping punctuation
PunctGp
PunctGpLParen // (
PunctGpRParen // )
PunctGpLBrack // [
PunctGpRBrack // ]
PunctGpLBrace // {
PunctGpRBrace // }
// SubCat: Separator punctuation
PunctSep
PunctSepComma // ,
PunctSepPeriod // .
PunctSepSemicolon // ;
PunctSepColon // :
// SubCat: String punctuation
PunctStr
PunctStrDblQuote // "
PunctStrQuote // '
PunctStrBacktick // `
PunctStrEsc // \
// Cat: Comments.
Comment
CommentHashbang
CommentMultiline
CommentSingle
CommentSpecial
// SubCat: Preprocessor "comments".
CommentPreproc
CommentPreprocFile
// Cat: Text.
Text
TextWhitespace
TextSymbol
TextPunctuation
TextSpellErr
// SubCat: TextStyle (corresponds to Generic in chroma / pygments) todo: look in font deco for more
TextStyle
TextStyleDeleted // strike-through
TextStyleEmph // italics
TextStyleError
TextStyleHeading
TextStyleInserted
TextStyleOutput
TextStylePrompt
TextStyleStrong // bold
TextStyleSubheading
TextStyleTraceback
TextStyleUnderline
TextStyleLink
)
// Categories
var Cats = []Tokens{
None,
Keyword,
Name,
Literal,
Operator,
Punctuation,
Comment,
Text,
TokensN,
}
// Sub-Categories
var SubCats = []Tokens{
None,
Keyword,
Name,
NameType,
NameFunction,
NameScope,
NameVar,
NameValue,
Literal,
LitStr,
LitNum,
Operator,
OpMath,
OpBit,
OpAsgn,
OpMathAsgn,
OpBitAsgn,
OpLog,
OpRel,
OpList,
Punctuation,
PunctGp,
PunctSep,
PunctStr,
Comment,
CommentPreproc,
Text,
TextStyle,
TokensN,
}
// InitCatMap initializes the CatMap
func InitCatMap() {
if CatMap != nil {
return
}
CatMap = make(map[Tokens]Tokens, TokensN)
for tk := None; tk < TokensN; tk++ {
for c := 1; c < len(Cats); c++ {
if tk < Cats[c] {
CatMap[tk] = Cats[c-1]
break
}
}
}
}
// InitSubCatMap initializes the SubCatMap
func InitSubCatMap() {
if SubCatMap != nil {
return
}
SubCatMap = make(map[Tokens]Tokens, TokensN)
for tk := None; tk < TokensN; tk++ {
for c := 1; c < len(SubCats); c++ {
if tk < SubCats[c] {
SubCatMap[tk] = SubCats[c-1]
break
}
}
}
}
// OpPunctMap provides a lookup of operators and punctuation tokens by their usual
// string representation
var OpPunctMap = map[string]Tokens{
"+": OpMathAdd,
"-": OpMathSub,
"*": OpMathMul,
"/": OpMathDiv,
"%": OpMathRem,
"&": OpBitAnd,
"|": OpBitOr,
"~": OpBitNot,
"^": OpBitXor,
"<<": OpBitShiftLeft,
">>": OpBitShiftRight,
"&^": OpBitAndNot,
"=": OpAsgnAssign,
"++": OpAsgnInc,
"--": OpAsgnDec,
"<-": OpAsgnArrow,
":=": OpAsgnDefine,
"+=": OpMathAsgnAdd,
"-=": OpMathAsgnSub,
"*=": OpMathAsgnMul,
"/=": OpMathAsgnDiv,
"%=": OpMathAsgnRem,
"&=": OpBitAsgnAnd,
"|=": OpBitAsgnOr,
"^=": OpBitAsgnXor,
"<<=": OpBitAsgnShiftLeft,
">>=": OpBitAsgnShiftRight,
"&^=": OpBitAsgnAndNot,
"&&": OpLogAnd,
"||": OpLogOr,
"!": OpLogNot,
"==": OpRelEqual,
"!=": OpRelNotEqual,
"<": OpRelLess,
">": OpRelGreater,
"<=": OpRelLtEq,
">=": OpRelGtEq,
"...": OpListEllipsis,
"(": PunctGpLParen,
")": PunctGpRParen,
"[": PunctGpLBrack,
"]": PunctGpRBrack,
"{": PunctGpLBrace,
"}": PunctGpRBrace,
",": PunctSepComma,
".": PunctSepPeriod,
";": PunctSepSemicolon,
":": PunctSepColon,
"\"": PunctStrDblQuote,
"'": PunctStrQuote,
"`": PunctStrBacktick,
"\\": PunctStrEsc,
}
// Names are the short tag names for each token, used e.g., for syntax highlighting
// These are based on alecthomas/chroma / pygments
var Names = map[Tokens]string{
None: "",
Error: "err",
EOF: "EOF",
EOL: "EOL",
EOS: "EOS",
Background: "bg",
Keyword: "k",
KeywordConstant: "kc",
KeywordDeclaration: "kd",
KeywordNamespace: "kn",
KeywordPseudo: "kp",
KeywordReserved: "kr",
KeywordType: "kt",
Name: "n",
NameBuiltin: "nb",
NameBuiltinPseudo: "bp",
NameOther: "nx",
NamePseudo: "pu",
NameType: "nt",
NameClass: "nc",
NameStruct: "ns",
NameField: "nfl",
NameInterface: "nti",
NameConstant: "no",
NameEnum: "nen",
NameEnumMember: "nem",
NameArray: "nr",
NameMap: "nm",
NameObject: "nj",
NameTypeParam: "ntp",
NameFunction: "nf",
NameDecorator: "nd",
NameFunctionMagic: "fm",
NameMethod: "mt",
NameOperator: "np",
NameConstructor: "cr",
NameException: "ne",
NameLabel: "nl",
NameEvent: "ev",
NameScope: "nsc",
NameNamespace: "nn",
NameModule: "md",
NamePackage: "pk",
NameLibrary: "lb",
NameVar: "nv",
NameVarAnonymous: "ay",
NameVarClass: "vc",
NameVarGlobal: "vg",
NameVarInstance: "vi",
NameVarMagic: "vm",
NameVarParam: "vp",
NameValue: "vl",
NameTag: "ng",
NameProperty: "py",
NameAttribute: "na",
NameEntity: "ni",
Literal: "l",
LiteralDate: "ld",
LiteralOther: "lo",
LiteralBool: "bo",
LitStr: "s",
LitStrAffix: "sa",
LitStrAtom: "st",
LitStrBacktick: "sb",
LitStrBoolean: "so",
LitStrChar: "sc",
LitStrDelimiter: "dl",
LitStrDoc: "sd",
LitStrDouble: "s2",
LitStrEscape: "se",
LitStrHeredoc: "sh",
LitStrInterpol: "si",
LitStrName: "sn",
LitStrOther: "sx",
LitStrRegex: "sr",
LitStrSingle: "s1",
LitStrSymbol: "ss",
LitStrFile: "fl",
LitNum: "m",
LitNumBin: "mb",
LitNumFloat: "mf",
LitNumHex: "mh",
LitNumInteger: "mi",
LitNumIntegerLong: "il",
LitNumOct: "mo",
LitNumImag: "mj",
Operator: "o",
OperatorWord: "ow",
// don't really need these -- only have at sub-categ level
OpMath: "om",
OpBit: "ob",
OpAsgn: "oa",
OpMathAsgn: "pa",
OpBitAsgn: "ba",
OpLog: "ol",
OpRel: "or",
OpList: "oi",
Punctuation: "p",
PunctGp: "pg",
PunctSep: "ps",
PunctStr: "pr",
Comment: "c",
CommentHashbang: "ch",
CommentMultiline: "cm",
CommentSingle: "c1",
CommentSpecial: "cs",
CommentPreproc: "cp",
CommentPreprocFile: "cpf",
Text: "",
TextWhitespace: "w",
TextSymbol: "ts",
TextPunctuation: "tp",
TextSpellErr: "te",
TextStyle: "g",
TextStyleDeleted: "gd",
TextStyleEmph: "ge",
TextStyleError: "gr",
TextStyleHeading: "gh",
TextStyleInserted: "gi",
TextStyleOutput: "go",
TextStylePrompt: "gp",
TextStyleStrong: "gs",
TextStyleSubheading: "gu",
TextStyleTraceback: "gt",
TextStyleUnderline: "gl",
TextStyleLink: "ga",
}
// Copyright (c) 2024, 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.
// Adapted initially from gonum/plot:
// Copyright ©2015 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package plot
import (
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
)
// Normalizer rescales values from the data coordinate system to the
// normalized coordinate system.
type Normalizer interface {
// Normalize transforms a value x in the data coordinate system to
// the normalized coordinate system.
Normalize(min, max, x float32) float32
}
// Axis represents either a horizontal or vertical
// axis of a plot.
type Axis struct {
// Min and Max are the minimum and maximum data
// values represented by the axis.
Min, Max float32
// specifies which axis this is: X or Y
Axis math32.Dims
// Label for the axis
Label Text
// Line styling properties for the axis line.
Line LineStyle
// Padding between the axis line and the data. Having
// non-zero padding ensures that the data is never drawn
// on the axis, thus making it easier to see.
Padding units.Value
// has the text style for rendering tick labels, and is shared for actual rendering
TickText Text
// line style for drawing tick lines
TickLine LineStyle
// length of tick lines
TickLength units.Value
// Ticker generates the tick marks. Any tick marks
// returned by the Marker function that are not in
// range of the axis are not drawn.
Ticker Ticker
// Scale transforms a value given in the data coordinate system
// to the normalized coordinate system of the axis—its distance
// along the axis as a fraction of the axis range.
Scale Normalizer
// AutoRescale enables an axis to automatically adapt its minimum
// and maximum boundaries, according to its underlying Ticker.
AutoRescale bool
// cached list of ticks, set in size
ticks []Tick
}
// Sets Defaults, range is (∞, ∞), and thus any finite
// value is less than Min and greater than Max.
func (ax *Axis) Defaults(dim math32.Dims) {
ax.Min = math32.Inf(+1)
ax.Max = math32.Inf(-1)
ax.Axis = dim
ax.Line.Defaults()
ax.Label.Defaults()
ax.Label.Style.Size.Dp(20)
ax.Padding.Pt(5)
ax.TickText.Defaults()
ax.TickText.Style.Size.Dp(16)
ax.TickText.Style.Padding.Dp(2)
ax.TickLine.Defaults()
ax.TickLength.Pt(8)
if dim == math32.Y {
ax.Label.Style.Rotation = -90
ax.TickText.Style.Align = styles.End
}
ax.Scale = LinearScale{}
ax.Ticker = DefaultTicks{}
}
// SanitizeRange ensures that the range of the axis makes sense.
func (ax *Axis) SanitizeRange() {
if math32.IsInf(ax.Min, 0) {
ax.Min = 0
}
if math32.IsInf(ax.Max, 0) {
ax.Max = 0
}
if ax.Min > ax.Max {
ax.Min, ax.Max = ax.Max, ax.Min
}
if ax.Min == ax.Max {
ax.Min--
ax.Max++
}
if ax.AutoRescale {
marks := ax.Ticker.Ticks(ax.Min, ax.Max)
for _, t := range marks {
ax.Min = math32.Min(ax.Min, t.Value)
ax.Max = math32.Max(ax.Max, t.Value)
}
}
}
// LinearScale an be used as the value of an Axis.Scale function to
// set the axis to a standard linear scale.
type LinearScale struct{}
var _ Normalizer = LinearScale{}
// Normalize returns the fractional distance of x between min and max.
func (LinearScale) Normalize(min, max, x float32) float32 {
return (x - min) / (max - min)
}
// LogScale can be used as the value of an Axis.Scale function to
// set the axis to a log scale.
type LogScale struct{}
var _ Normalizer = LogScale{}
// Normalize returns the fractional logarithmic distance of
// x between min and max.
func (LogScale) Normalize(min, max, x float32) float32 {
if min <= 0 || max <= 0 || x <= 0 {
panic("Values must be greater than 0 for a log scale.")
}
logMin := math32.Log(min)
return (math32.Log(x) - logMin) / (math32.Log(max) - logMin)
}
// InvertedScale can be used as the value of an Axis.Scale function to
// invert the axis using any Normalizer.
type InvertedScale struct{ Normalizer }
var _ Normalizer = InvertedScale{}
// Normalize returns a normalized [0, 1] value for the position of x.
func (is InvertedScale) Normalize(min, max, x float32) float32 {
return is.Normalizer.Normalize(max, min, x)
}
// Norm returns the value of x, given in the data coordinate
// system, normalized to its distance as a fraction of the
// range of this axis. For example, if x is a.Min then the return
// value is 0, and if x is a.Max then the return value is 1.
func (ax *Axis) Norm(x float32) float32 {
return ax.Scale.Normalize(ax.Min, ax.Max, x)
}
// Copyright (c) 2024, 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.
// Adapted from github.com/gonum/plot:
// Copyright ©2015 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package plot
import (
"errors"
"cogentcore.org/core/math32"
)
// data defines the main data interfaces for plotting.
// Other more specific types of plots define their own interfaces.
// unlike gonum/plot, NaN values are treated as missing data here.
var (
ErrInfinity = errors.New("plotter: infinite data point")
ErrNoData = errors.New("plotter: no data points")
)
// CheckFloats returns an error if any of the arguments are Infinity.
// or if there are no non-NaN data points available for plotting.
func CheckFloats(fs ...float32) error {
n := 0
for _, f := range fs {
switch {
case math32.IsNaN(f):
case math32.IsInf(f, 0):
return ErrInfinity
default:
n++
}
}
if n == 0 {
return ErrNoData
}
return nil
}
// CheckNaNs returns true if any of the floats are NaN
func CheckNaNs(fs ...float32) bool {
for _, f := range fs {
if math32.IsNaN(f) {
return true
}
}
return false
}
//////////////////////////////////////////////////
// Valuer
// Valuer provides an interface for a list of scalar values
type Valuer interface {
// Len returns the number of values.
Len() int
// Value returns a value.
Value(i int) float32
}
// Range returns the minimum and maximum values.
func Range(vs Valuer) (min, max float32) {
min = math32.Inf(1)
max = math32.Inf(-1)
for i := 0; i < vs.Len(); i++ {
v := vs.Value(i)
if math32.IsNaN(v) {
continue
}
min = math32.Min(min, v)
max = math32.Max(max, v)
}
return
}
// Values implements the Valuer interface.
type Values []float32
func (vs Values) Len() int {
return len(vs)
}
func (vs Values) Value(i int) float32 {
return vs[i]
}
// CopyValues returns a Values that is a copy of the values
// from a Valuer, or an error if there are no values, or if one of
// the copied values is a Infinity.
// NaN values are skipped in the copying process.
func CopyValues(vs Valuer) (Values, error) {
if vs.Len() == 0 {
return nil, ErrNoData
}
cpy := make(Values, 0, vs.Len())
for i := 0; i < vs.Len(); i++ {
v := vs.Value(i)
if math32.IsNaN(v) {
continue
}
if err := CheckFloats(v); err != nil {
return nil, err
}
cpy = append(cpy, v)
}
return cpy, nil
}
//////////////////////////////////////////////////
// XYer
// XYer provides an interface for a list of X,Y data pairs
type XYer interface {
// Len returns the number of x, y pairs.
Len() int
// XY returns an x, y pair.
XY(i int) (x, y float32)
}
// XYRange returns the minimum and maximum
// x and y values.
func XYRange(xys XYer) (xmin, xmax, ymin, ymax float32) {
xmin, xmax = Range(XValues{xys})
ymin, ymax = Range(YValues{xys})
return
}
// XYs implements the XYer interface.
type XYs []math32.Vector2
func (xys XYs) Len() int {
return len(xys)
}
func (xys XYs) XY(i int) (float32, float32) {
return xys[i].X, xys[i].Y
}
// CopyXYs returns an XYs that is a copy of the x and y values from
// an XYer, or an error if one of the data points contains a NaN or
// Infinity.
func CopyXYs(data XYer) (XYs, error) {
if data.Len() == 0 {
return nil, ErrNoData
}
cpy := make(XYs, 0, data.Len())
for i := range data.Len() {
x, y := data.XY(i)
if CheckNaNs(x, y) {
continue
}
if err := CheckFloats(x, y); err != nil {
return nil, err
}
cpy = append(cpy, math32.Vec2(x, y))
}
return cpy, nil
}
// PlotXYs returns plot coordinates for given set of XYs
func PlotXYs(plt *Plot, data XYs) XYs {
ps := make(XYs, len(data))
for i := range data {
ps[i].X, ps[i].Y = plt.PX(data[i].X), plt.PY(data[i].Y)
}
return ps
}
// XValues implements the Valuer interface,
// returning the x value from an XYer.
type XValues struct {
XYer
}
func (xs XValues) Value(i int) float32 {
x, _ := xs.XY(i)
return x
}
// YValues implements the Valuer interface,
// returning the y value from an XYer.
type YValues struct {
XYer
}
func (ys YValues) Value(i int) float32 {
_, y := ys.XY(i)
return y
}
//////////////////////////////////////////////////
// XYer
// XYZer provides an interface for a list of X,Y,Z data triples.
// It also satisfies the XYer interface for the X,Y pairs.
type XYZer interface {
// Len returns the number of x, y, z triples.
Len() int
// XYZ returns an x, y, z triple.
XYZ(i int) (float32, float32, float32)
// XY returns an x, y pair.
XY(i int) (float32, float32)
}
// XYZs implements the XYZer interface using a slice.
type XYZs []XYZ
// XYZ is an x, y and z value.
type XYZ struct{ X, Y, Z float32 }
// Len implements the Len method of the XYZer interface.
func (xyz XYZs) Len() int {
return len(xyz)
}
// XYZ implements the XYZ method of the XYZer interface.
func (xyz XYZs) XYZ(i int) (float32, float32, float32) {
return xyz[i].X, xyz[i].Y, xyz[i].Z
}
// XY implements the XY method of the XYer interface.
func (xyz XYZs) XY(i int) (float32, float32) {
return xyz[i].X, xyz[i].Y
}
// CopyXYZs copies an XYZer.
func CopyXYZs(data XYZer) (XYZs, error) {
if data.Len() == 0 {
return nil, ErrNoData
}
cpy := make(XYZs, 0, data.Len())
for i := range data.Len() {
x, y, z := data.XYZ(i)
if CheckNaNs(x, y, z) {
continue
}
if err := CheckFloats(x, y, z); err != nil {
return nil, err
}
cpy = append(cpy, XYZ{X: x, Y: y, Z: z})
}
return cpy, nil
}
// XYValues implements the XYer interface, returning
// the x and y values from an XYZer.
type XYValues struct{ XYZer }
// XY implements the XY method of the XYer interface.
func (xy XYValues) XY(i int) (float32, float32) {
x, y, _ := xy.XYZ(i)
return x, y
}
//////////////////////////////////////////////////
// Labeler
// Labeler provides an interface for a list of string labels
type Labeler interface {
// Label returns a label.
Label(i int) string
}
// Copyright (c) 2024, 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.
// Adapted from gonum/plot:
// Copyright ©2015 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package plot
import (
"bufio"
"bytes"
"image"
"io"
"os"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
)
// SVGString returns an SVG representation of the plot as a string
func (pt *Plot) SVGString() string {
b := &bytes.Buffer{}
pt.Paint.SVGOut = b
pt.svgDraw()
pt.Paint.SVGOut = nil
return b.String()
}
// svgDraw draws SVGOut writer that must already be set in Paint
func (pt *Plot) svgDraw() {
pt.drawConfig()
io.WriteString(pt.Paint.SVGOut, pt.Paint.SVGStart())
pt.Draw()
io.WriteString(pt.Paint.SVGOut, pt.Paint.SVGEnd())
}
// SVGToFile saves the SVG to given file
func (pt *Plot) SVGToFile(filename string) error {
fp, err := os.Create(filename)
if err != nil {
return err
}
defer fp.Close()
bw := bufio.NewWriter(fp)
pt.Paint.SVGOut = bw
pt.svgDraw()
pt.Paint.SVGOut = nil
return bw.Flush()
}
// drawConfig configures everything for drawing
func (pt *Plot) drawConfig() {
pt.Resize(pt.Size) // ensure
pt.Legend.TextStyle.openFont(pt)
pt.Paint.ToDots()
}
// Draw draws the plot to image.
// Plotters are drawn in the order in which they were
// added to the plot.
func (pt *Plot) Draw() {
pt.drawConfig()
pc := pt.Paint
ptw := float32(pt.Size.X)
pth := float32(pt.Size.X)
ptb := image.Rectangle{Max: pt.Size}
pc.PushBounds(ptb)
if pt.Background != nil {
pc.BlitBox(math32.Vector2{}, math32.FromPoint(pt.Size), pt.Background)
}
if pt.Title.Text != "" {
pt.Title.Config(pt)
pos := pt.Title.PosX(ptw)
pad := pt.Title.Style.Padding.Dots
pos.Y = pad
pt.Title.Draw(pt, pos)
th := pt.Title.PaintText.BBox.Size().Y + 2*pad
pth -= th
ptb.Min.Y += int(math32.Ceil(th))
}
pt.X.SanitizeRange()
pt.Y.SanitizeRange()
ywidth, tickWidth, tpad, bpad := pt.Y.sizeY(pt)
xheight, lpad, rpad := pt.X.sizeX(pt, float32(pt.Size.X-int(ywidth)))
tb := ptb
tb.Min.X += ywidth
pc.PushBounds(tb)
pt.X.drawX(pt, lpad, rpad)
pc.PopBounds()
tb = ptb
tb.Max.Y -= xheight
pc.PushBounds(tb)
pt.Y.drawY(pt, tickWidth, tpad, bpad)
pc.PopBounds()
tb = ptb
tb.Min.X += ywidth + lpad
tb.Max.X -= rpad
tb.Max.Y -= xheight + bpad
tb.Min.Y += tpad
pt.PlotBox.SetFromRect(tb)
// don't cut off lines
tb.Min.X -= 2
tb.Min.Y -= 2
tb.Max.X += 2
tb.Max.Y += 2
pc.PushBounds(tb)
for _, plt := range pt.Plotters {
plt.Plot(pt)
}
pt.Legend.draw(pt)
pc.PopBounds()
pc.PopBounds() // global
}
////////////////////////////////////////////////////////////////
// Axis
// drawTicks returns true if the tick marks should be drawn.
func (ax *Axis) drawTicks() bool {
return ax.TickLine.Width.Value > 0 && ax.TickLength.Value > 0
}
// sizeX returns the total height of the axis, left and right padding
func (ax *Axis) sizeX(pt *Plot, axw float32) (ht, lpad, rpad int) {
pc := pt.Paint
uc := &pc.UnitContext
ax.TickLength.ToDots(uc)
ax.ticks = ax.Ticker.Ticks(ax.Min, ax.Max)
h := float32(0)
if ax.Label.Text != "" { // We assume that the label isn't rotated.
ax.Label.Config(pt)
h += ax.Label.PaintText.BBox.Size().Y
h += ax.Label.Style.Padding.Dots
}
lw := ax.Line.Width.Dots
lpad = int(math32.Ceil(lw)) + 2
rpad = int(math32.Ceil(lw)) + 10
tht := float32(0)
if len(ax.ticks) > 0 {
if ax.drawTicks() {
h += ax.TickLength.Dots
}
ftk := ax.firstTickLabel()
if ftk.Label != "" {
px, _ := ax.tickPosX(pt, ftk, axw)
if px < 0 {
lpad += int(math32.Ceil(-px))
}
tht = max(tht, ax.TickText.PaintText.BBox.Size().Y)
}
ltk := ax.lastTickLabel()
if ltk.Label != "" {
px, wd := ax.tickPosX(pt, ltk, axw)
if px+wd > axw {
rpad += int(math32.Ceil((px + wd) - axw))
}
tht = max(tht, ax.TickText.PaintText.BBox.Size().Y)
}
ax.TickText.Text = ax.longestTickLabel()
if ax.TickText.Text != "" {
ax.TickText.Config(pt)
tht = max(tht, ax.TickText.PaintText.BBox.Size().Y)
}
h += ax.TickText.Style.Padding.Dots
}
h += tht + lw + ax.Padding.Dots
ht = int(math32.Ceil(h))
return
}
// tickLabelPosX returns the relative position and width for given tick along X axis
// for given total axis width
func (ax *Axis) tickPosX(pt *Plot, t Tick, axw float32) (px, wd float32) {
x := axw * float32(ax.Norm(t.Value))
if x < 0 || x > axw {
return
}
ax.TickText.Text = t.Label
ax.TickText.Config(pt)
pos := ax.TickText.PosX(0)
px = pos.X + x
wd = ax.TickText.PaintText.BBox.Size().X
return
}
func (ax *Axis) firstTickLabel() Tick {
for _, tk := range ax.ticks {
if tk.Label != "" {
return tk
}
}
return Tick{}
}
func (ax *Axis) lastTickLabel() Tick {
n := len(ax.ticks)
for i := n - 1; i >= 0; i-- {
tk := ax.ticks[i]
if tk.Label != "" {
return tk
}
}
return Tick{}
}
func (ax *Axis) longestTickLabel() string {
lst := ""
for _, tk := range ax.ticks {
if len(tk.Label) > len(lst) {
lst = tk.Label
}
}
return lst
}
func (ax *Axis) sizeY(pt *Plot) (ywidth, tickWidth, tpad, bpad int) {
pc := pt.Paint
uc := &pc.UnitContext
ax.ticks = ax.Ticker.Ticks(ax.Min, ax.Max)
ax.TickLength.ToDots(uc)
w := float32(0)
if ax.Label.Text != "" {
ax.Label.Config(pt)
w += ax.Label.PaintText.BBox.Size().X
w += ax.Label.Style.Padding.Dots
}
lw := ax.Line.Width.Dots
tpad = int(math32.Ceil(lw)) + 2
bpad = int(math32.Ceil(lw)) + 2
if len(ax.ticks) > 0 {
if ax.drawTicks() {
w += ax.TickLength.Dots
}
ax.TickText.Text = ax.longestTickLabel()
if ax.TickText.Text != "" {
ax.TickText.Config(pt)
tw := ax.TickText.PaintText.BBox.Size().X
w += tw
tickWidth = int(math32.Ceil(tw))
w += ax.TickText.Style.Padding.Dots
tht := int(math32.Ceil(0.5 * ax.TickText.PaintText.BBox.Size().X))
tpad += tht
bpad += tht
}
}
w += lw + ax.Padding.Dots
ywidth = int(math32.Ceil(w))
return
}
// drawX draws the horizontal axis
func (ax *Axis) drawX(pt *Plot, lpad, rpad int) {
ab := pt.Paint.Bounds
ab.Min.X += lpad
ab.Max.X -= rpad
axw := float32(ab.Size().X)
// axh := float32(ab.Size().Y) // height of entire plot
if ax.Label.Text != "" {
ax.Label.Config(pt)
pos := ax.Label.PosX(axw)
pos.X += float32(ab.Min.X)
th := ax.Label.PaintText.BBox.Size().Y
pos.Y = float32(ab.Max.Y) - th
ax.Label.Draw(pt, pos)
ab.Max.Y -= int(math32.Ceil(th + ax.Label.Style.Padding.Dots))
}
tickHt := float32(0)
for _, t := range ax.ticks {
x := axw * float32(ax.Norm(t.Value))
if x < 0 || x > axw || t.IsMinor() {
continue
}
ax.TickText.Text = t.Label
ax.TickText.Config(pt)
pos := ax.TickText.PosX(0)
pos.X += x + float32(ab.Min.X)
tickHt = ax.TickText.PaintText.BBox.Size().Y + ax.TickText.Style.Padding.Dots
pos.Y += float32(ab.Max.Y) - tickHt
ax.TickText.Draw(pt, pos)
}
if len(ax.ticks) > 0 {
ab.Max.Y -= int(math32.Ceil(tickHt))
// } else {
// y += ax.Width / 2
}
if len(ax.ticks) > 0 && ax.drawTicks() {
ln := ax.TickLength.Dots
for _, t := range ax.ticks {
yoff := float32(0)
if t.IsMinor() {
yoff = 0.5 * ln
}
x := axw * float32(ax.Norm(t.Value))
if x < 0 || x > axw {
continue
}
x += float32(ab.Min.X)
ax.TickLine.Draw(pt, math32.Vec2(x, float32(ab.Max.Y)-yoff), math32.Vec2(x, float32(ab.Max.Y)-ln))
}
ab.Max.Y -= int(ln - 0.5*ax.Line.Width.Dots)
}
ax.Line.Draw(pt, math32.Vec2(float32(ab.Min.X), float32(ab.Max.Y)), math32.Vec2(float32(ab.Min.X)+axw, float32(ab.Max.Y)))
}
// drawY draws the Y axis along the left side
func (ax *Axis) drawY(pt *Plot, tickWidth, tpad, bpad int) {
ab := pt.Paint.Bounds
ab.Min.Y += tpad
ab.Max.Y -= bpad
axh := float32(ab.Size().Y)
if ax.Label.Text != "" {
ax.Label.Style.Align = styles.Center
pos := ax.Label.PosY(axh)
tw := ax.Label.PaintText.BBox.Size().X
pos.Y += float32(ab.Min.Y) + ax.Label.PaintText.BBox.Size().Y
pos.X = float32(ab.Min.X)
ax.Label.Draw(pt, pos)
ab.Min.X += int(math32.Ceil(tw + ax.Label.Style.Padding.Dots))
}
tickWd := float32(0)
for _, t := range ax.ticks {
y := axh * (1 - float32(ax.Norm(t.Value)))
if y < 0 || y > axh || t.IsMinor() {
continue
}
ax.TickText.Text = t.Label
ax.TickText.Config(pt)
pos := ax.TickText.PosX(float32(tickWidth))
pos.X += float32(ab.Min.X)
pos.Y = float32(ab.Min.Y) + y - 0.5*ax.TickText.PaintText.BBox.Size().Y
tickWd = max(tickWd, ax.TickText.PaintText.BBox.Size().X+ax.TickText.Style.Padding.Dots)
ax.TickText.Draw(pt, pos)
}
if len(ax.ticks) > 0 {
ab.Min.X += int(math32.Ceil(tickWd))
// } else {
// y += ax.Width / 2
}
if len(ax.ticks) > 0 && ax.drawTicks() {
ln := ax.TickLength.Dots
for _, t := range ax.ticks {
xoff := float32(0)
if t.IsMinor() {
xoff = 0.5 * ln
}
y := axh * (1 - float32(ax.Norm(t.Value)))
if y < 0 || y > axh {
continue
}
y += float32(ab.Min.Y)
ax.TickLine.Draw(pt, math32.Vec2(float32(ab.Min.X)+xoff, y), math32.Vec2(float32(ab.Min.X)+ln, y))
}
ab.Min.X += int(ln + 0.5*ax.Line.Width.Dots)
}
ax.Line.Draw(pt, math32.Vec2(float32(ab.Min.X), float32(ab.Min.Y)), math32.Vec2(float32(ab.Min.X), float32(ab.Max.Y)))
}
////////////////////////////////////////////////
// Legend
// draw draws the legend
func (lg *Legend) draw(pt *Plot) {
pc := pt.Paint
uc := &pc.UnitContext
ptb := pc.Bounds
lg.ThumbnailWidth.ToDots(uc)
lg.TextStyle.ToDots(uc)
lg.Position.XOffs.ToDots(uc)
lg.Position.YOffs.ToDots(uc)
lg.TextStyle.openFont(pt)
em := lg.TextStyle.Font.Face.Metrics.Em
pad := math32.Ceil(lg.TextStyle.Padding.Dots)
var ltxt Text
ltxt.Style = lg.TextStyle
var sz image.Point
maxTht := 0
for _, e := range lg.Entries {
ltxt.Text = e.Text
ltxt.Config(pt)
sz.X = max(sz.X, int(math32.Ceil(ltxt.PaintText.BBox.Size().X)))
tht := int(math32.Ceil(ltxt.PaintText.BBox.Size().Y + pad))
maxTht = max(tht, maxTht)
}
sz.X += int(em)
sz.Y = len(lg.Entries) * maxTht
txsz := sz
sz.X += int(lg.ThumbnailWidth.Dots)
pos := ptb.Min
if lg.Position.Left {
pos.X += int(lg.Position.XOffs.Dots)
} else {
pos.X = ptb.Max.X - sz.X - int(lg.Position.XOffs.Dots)
}
if lg.Position.Top {
pos.Y += int(lg.Position.YOffs.Dots)
} else {
pos.Y = ptb.Max.Y - sz.Y - int(lg.Position.YOffs.Dots)
}
if lg.Fill != nil {
pc.FillBox(math32.FromPoint(pos), math32.FromPoint(sz), lg.Fill)
}
cp := pos
thsz := image.Point{X: int(lg.ThumbnailWidth.Dots), Y: maxTht - 2*int(pad)}
for _, e := range lg.Entries {
tp := cp
tp.X += int(txsz.X)
tp.Y += int(pad)
tb := image.Rectangle{Min: tp, Max: tp.Add(thsz)}
pc.PushBounds(tb)
for _, t := range e.Thumbs {
t.Thumbnail(pt)
}
pc.PopBounds()
ltxt.Text = e.Text
ltxt.Config(pt)
ltxt.Draw(pt, math32.FromPoint(cp))
cp.Y += maxTht
}
}
// Copyright (c) 2024, 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.
// Copied directly from gonum/plot:
// Copyright ©2017 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This is an implementation of the Talbot, Lin and Hanrahan algorithm
// described in doi:10.1109/TVCG.2010.130 with reference to the R
// implementation in the labeling package, ©2014 Justin Talbot (Licensed
// MIT+file LICENSE|Unlimited).
package plot
import "cogentcore.org/core/math32"
const (
// dlamchE is the machine epsilon. For IEEE this is 2^{-53}.
dlamchE = 1.0 / (1 << 53)
// dlamchB is the radix of the machine (the base of the number system).
dlamchB = 2
// dlamchP is base * eps.
dlamchP = dlamchB * dlamchE
)
const (
// free indicates no restriction on label containment.
free = iota
// containData specifies that all the data range lies
// within the interval [label_min, label_max].
containData
// withinData specifies that all labels lie within the
// interval [dMin, dMax].
withinData
)
// talbotLinHanrahan returns an optimal set of approximately want label values
// for the data range [dMin, dMax], and the step and magnitude of the step between values.
// containment is specifies are guarantees for label and data range containment, valid
// values are free, containData and withinData.
// The optional parameters Q, nice numbers, and w, weights, allow tuning of the
// algorithm but by default (when nil) are set to the parameters described in the
// paper.
// The legibility function allows tuning of the legibility assessment for labels.
// By default, when nil, legbility will set the legibility score for each candidate
// labelling scheme to 1.
// See the paper for an explanation of the function of Q, w and legibility.
func talbotLinHanrahan(dMin, dMax float32, want int, containment int, Q []float32, w *weights, legibility func(lMin, lMax, lStep float32) float32) (values []float32, step, q float32, magnitude int) {
const eps = dlamchP * 100
if dMin > dMax {
panic("labelling: invalid data range: min greater than max")
}
if Q == nil {
Q = []float32{1, 5, 2, 2.5, 4, 3}
}
if w == nil {
w = &weights{
simplicity: 0.25,
coverage: 0.2,
density: 0.5,
legibility: 0.05,
}
}
if legibility == nil {
legibility = unitLegibility
}
if r := dMax - dMin; r < eps {
l := make([]float32, want)
step := r / float32(want-1)
for i := range l {
l[i] = dMin + float32(i)*step
}
magnitude = minAbsMag(dMin, dMax)
return l, step, 0, magnitude
}
type selection struct {
// n is the number of labels selected.
n int
// lMin and lMax are the selected min
// and max label values. lq is the q
// chosen.
lMin, lMax, lStep, lq float32
// score is the score for the selection.
score float32
// magnitude is the magnitude of the
// label step distance.
magnitude int
}
best := selection{score: -2}
outer:
for skip := 1; ; skip++ {
for _, q := range Q {
sm := maxSimplicity(q, Q, skip)
if w.score(sm, 1, 1, 1) < best.score {
break outer
}
for have := 2; ; have++ {
dm := maxDensity(have, want)
if w.score(sm, 1, dm, 1) < best.score {
break
}
delta := (dMax - dMin) / float32(have+1) / float32(skip) / q
const maxExp = 309
for mag := int(math32.Ceil(math32.Log10(delta))); mag < maxExp; mag++ {
step := float32(skip) * q * math32.Pow10(mag)
cm := maxCoverage(dMin, dMax, step*float32(have-1))
if w.score(sm, cm, dm, 1) < best.score {
break
}
fracStep := step / float32(skip)
kStep := step * float32(have-1)
minStart := (math32.Floor(dMax/step) - float32(have-1)) * float32(skip)
maxStart := math32.Ceil(dMax/step) * float32(skip)
for start := minStart; start <= maxStart && start != start-1; start++ {
lMin := start * fracStep
lMax := lMin + kStep
switch containment {
case containData:
if dMin < lMin || lMax < dMax {
continue
}
case withinData:
if lMin < dMin || dMax < lMax {
continue
}
case free:
// Free choice.
}
score := w.score(
simplicity(q, Q, skip, lMin, lMax, step),
coverage(dMin, dMax, lMin, lMax),
density(have, want, dMin, dMax, lMin, lMax),
legibility(lMin, lMax, step),
)
if score > best.score {
best = selection{
n: have,
lMin: lMin,
lMax: lMax,
lStep: float32(skip) * q,
lq: q,
score: score,
magnitude: mag,
}
}
}
}
}
}
}
if best.score == -2 {
l := make([]float32, want)
step := (dMax - dMin) / float32(want-1)
for i := range l {
l[i] = dMin + float32(i)*step
}
magnitude = minAbsMag(dMin, dMax)
return l, step, 0, magnitude
}
l := make([]float32, best.n)
step = best.lStep * math32.Pow10(best.magnitude)
for i := range l {
l[i] = best.lMin + float32(i)*step
}
return l, best.lStep, best.lq, best.magnitude
}
// minAbsMag returns the minumum magnitude of the absolute values of a and b.
func minAbsMag(a, b float32) int {
return int(math32.Min(math32.Floor(math32.Log10(math32.Abs(a))), (math32.Floor(math32.Log10(math32.Abs(b))))))
}
// simplicity returns the simplicity score for how will the curent q, lMin, lMax,
// lStep and skip match the given nice numbers, Q.
func simplicity(q float32, Q []float32, skip int, lMin, lMax, lStep float32) float32 {
const eps = dlamchP * 100
for i, v := range Q {
if v == q {
m := math32.Mod(lMin, lStep)
v = 0
if (m < eps || lStep-m < eps) && lMin <= 0 && 0 <= lMax {
v = 1
}
return 1 - float32(i)/(float32(len(Q))-1) - float32(skip) + v
}
}
panic("labelling: invalid q for Q")
}
// maxSimplicity returns the maximum simplicity for q, Q and skip.
func maxSimplicity(q float32, Q []float32, skip int) float32 {
for i, v := range Q {
if v == q {
return 1 - float32(i)/(float32(len(Q))-1) - float32(skip) + 1
}
}
panic("labelling: invalid q for Q")
}
// coverage returns the coverage score for based on the average
// squared distance between the extreme labels, lMin and lMax, and
// the extreme data points, dMin and dMax.
func coverage(dMin, dMax, lMin, lMax float32) float32 {
r := 0.1 * (dMax - dMin)
max := dMax - lMax
min := dMin - lMin
return 1 - 0.5*(max*max+min*min)/(r*r)
}
// maxCoverage returns the maximum coverage achievable for the data
// range.
func maxCoverage(dMin, dMax, span float32) float32 {
r := dMax - dMin
if span <= r {
return 1
}
h := 0.5 * (span - r)
r *= 0.1
return 1 - (h*h)/(r*r)
}
// density returns the density score which measures the goodness of
// the labelling density compared to the user defined target
// based on the want parameter given to talbotLinHanrahan.
func density(have, want int, dMin, dMax, lMin, lMax float32) float32 {
rho := float32(have-1) / (lMax - lMin)
rhot := float32(want-1) / (math32.Max(lMax, dMax) - math32.Min(dMin, lMin))
if d := rho / rhot; d >= 1 {
return 2 - d
}
return 2 - rhot/rho
}
// maxDensity returns the maximum density score achievable for have and want.
func maxDensity(have, want int) float32 {
if have < want {
return 1
}
return 2 - float32(have-1)/float32(want-1)
}
// unitLegibility returns a default legibility score ignoring label
// spacing.
func unitLegibility(_, _, _ float32) float32 {
return 1
}
// weights is a helper type to calcuate the labelling scheme's total score.
type weights struct {
simplicity, coverage, density, legibility float32
}
// score returns the score for a labelling scheme with simplicity, s,
// coverage, c, density, d and legibility l.
func (w *weights) score(s, c, d, l float32) float32 {
return w.simplicity*s + w.coverage*c + w.density*d + w.legibility*l
}
// Copyright (c) 2024, 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 plot
import (
"image"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/styles/units"
)
// LegendPosition specifies where to put the legend
type LegendPosition struct {
// Top and Left specify the location of the legend.
Top, Left bool
// XOffs and YOffs are added to the legend's final position,
// relative to the relevant anchor position
XOffs, YOffs units.Value
}
func (lg *LegendPosition) Defaults() {
lg.Top = true
}
// A Legend gives a description of the meaning of different
// data elements of the plot. Each legend entry has a name
// and a thumbnail, where the thumbnail shows a small
// sample of the display style of the corresponding data.
type Legend struct {
// TextStyle is the style given to the legend entry texts.
TextStyle TextStyle
// position of the legend
Position LegendPosition `display:"inline"`
// ThumbnailWidth is the width of legend thumbnails.
ThumbnailWidth units.Value
// Fill specifies the background fill color for the legend box,
// if non-nil.
Fill image.Image
// Entries are all of the LegendEntries described by this legend.
Entries []LegendEntry
}
func (lg *Legend) Defaults() {
lg.TextStyle.Defaults()
lg.TextStyle.Padding.Dp(2)
lg.TextStyle.Font.Size.Dp(20)
lg.Position.Defaults()
lg.ThumbnailWidth.Pt(20)
lg.Fill = gradient.ApplyOpacity(colors.Scheme.Surface, 0.75)
}
// Add adds an entry to the legend with the given name.
// The entry's thumbnail is drawn as the composite of all of the
// thumbnails.
func (lg *Legend) Add(name string, thumbs ...Thumbnailer) {
lg.Entries = append(lg.Entries, LegendEntry{Text: name, Thumbs: thumbs})
}
// LegendForPlotter returns the legend Text for given plotter,
// if it exists as a Thumbnailer in the legend entries.
// Otherwise returns empty string.
func (lg *Legend) LegendForPlotter(plt Plotter) string {
for _, e := range lg.Entries {
for _, tn := range e.Thumbs {
if tp, isp := tn.(Plotter); isp && tp == plt {
return e.Text
}
}
}
return ""
}
// Thumbnailer wraps the Thumbnail method, which
// draws the small image in a legend representing the
// style of data.
type Thumbnailer interface {
// Thumbnail draws an thumbnail representing
// a legend entry. The thumbnail will usually show
// a smaller representation of the style used
// to plot the corresponding data.
Thumbnail(pt *Plot)
}
// A LegendEntry represents a single line of a legend, it
// has a name and an icon.
type LegendEntry struct {
// text is the text associated with this entry.
Text string
// thumbs is a slice of all of the thumbnails styles
Thumbs []Thumbnailer
}
// Copyright (c) 2024, 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 plot
import (
"image"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles/units"
)
// LineStyle has style properties for line drawing
type LineStyle struct {
// stroke color image specification; stroking is off if nil
Color image.Image
// line width
Width 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
}
func (ls *LineStyle) Defaults() {
ls.Color = colors.Scheme.OnSurface
ls.Width.Pt(1)
}
// SetStroke sets the stroke style in plot paint to current line style.
// returns false if either the Width = 0 or Color is nil
func (ls *LineStyle) SetStroke(pt *Plot) bool {
if ls.Color == nil {
return false
}
pc := pt.Paint
uc := &pc.UnitContext
ls.Width.ToDots(uc)
if ls.Width.Dots == 0 {
return false
}
pc.StrokeStyle.Width = ls.Width
pc.StrokeStyle.Color = ls.Color
pc.StrokeStyle.ToDots(uc)
return true
}
// Draw draws a line between given coordinates, setting the stroke style
// to current parameters. Returns false if either Width = 0 or Color = nil
func (ls *LineStyle) Draw(pt *Plot, start, end math32.Vector2) bool {
if !ls.SetStroke(pt) {
return false
}
pc := pt.Paint
pc.MoveTo(start.X, start.Y)
pc.LineTo(end.X, end.Y)
pc.Stroke()
return true
}
// Copyright (c) 2024, 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.
// Adapted from github.com/gonum/plot:
// Copyright ©2015 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package plot
//go:generate core generate -add-types
import (
"image"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles"
)
// Plot is the basic type representing a plot.
// It renders into its own image.RGBA Pixels image,
// and can also save a corresponding SVG version.
// The Axis ranges are updated automatically when plots
// are added, so setting a fixed range should happen
// after that point. See [UpdateRange] method as well.
type Plot struct {
// Title of the plot
Title Text
// Background is the background of the plot.
// The default is [colors.Scheme.Surface].
Background image.Image
// standard text style with default options
StandardTextStyle styles.Text
// X and Y are the horizontal and vertical axes
// of the plot respectively.
X, Y Axis
// Legend is the plot's legend.
Legend Legend
// plotters are drawn by calling their Plot method
// after the axes are drawn.
Plotters []Plotter
// size is the target size of the image to render to
Size image.Point
// DPI is the dots per inch for rendering the image.
// Larger numbers result in larger scaling of the plot contents
// which is strongly recommended for print (e.g., use 300 for print)
DPI float32 `default:"96,160,300"`
// painter for rendering
Paint *paint.Context
// pixels that we render into
Pixels *image.RGBA `copier:"-" json:"-" xml:"-" edit:"-"`
// Current plot bounding box in image coordinates, for plotting coordinates
PlotBox math32.Box2
}
// Defaults sets defaults
func (pt *Plot) Defaults() {
pt.Title.Defaults()
pt.Title.Style.Size.Dp(24)
pt.Background = colors.Scheme.Surface
pt.X.Defaults(math32.X)
pt.Y.Defaults(math32.Y)
pt.Legend.Defaults()
pt.DPI = 96
pt.Size = image.Point{1280, 1024}
pt.StandardTextStyle.Defaults()
pt.StandardTextStyle.WhiteSpace = styles.WhiteSpaceNowrap
}
// New returns a new plot with some reasonable default settings.
func New() *Plot {
pt := &Plot{}
pt.Defaults()
return pt
}
// Add adds a Plotters to the plot.
//
// If the plotters implements DataRanger then the
// minimum and maximum values of the X and Y
// axes are changed if necessary to fit the range of
// the data.
//
// When drawing the plot, Plotters are drawn in the
// order in which they were added to the plot.
func (pt *Plot) Add(ps ...Plotter) {
pt.Plotters = append(pt.Plotters, ps...)
}
// SetPixels sets the backing pixels image to given image.RGBA
func (pt *Plot) SetPixels(img *image.RGBA) {
pt.Pixels = img
pt.Paint = paint.NewContextFromImage(pt.Pixels)
pt.Paint.UnitContext.DPI = pt.DPI
pt.Size = pt.Pixels.Bounds().Size()
pt.UpdateRange() // needs context, to automatically update for labels
}
// Resize sets the size of the output image to given size.
// Does nothing if already the right size.
func (pt *Plot) Resize(sz image.Point) {
if pt.Pixels != nil {
ib := pt.Pixels.Bounds().Size()
if ib == sz {
pt.Size = sz
pt.Paint.UnitContext.DPI = pt.DPI
return // already good
}
}
pt.SetPixels(image.NewRGBA(image.Rectangle{Max: sz}))
}
func (pt *Plot) SaveImage(filename string) error {
return imagex.Save(pt.Pixels, filename)
}
// NominalX configures the plot to have a nominal X
// axis—an X axis with names instead of numbers. The
// X location corresponding to each name are the integers,
// e.g., the x value 0 is centered above the first name and
// 1 is above the second name, etc. Labels for x values
// that do not end up in range of the X axis will not have
// tick marks.
func (pt *Plot) NominalX(names ...string) {
pt.X.TickLine.Width.Pt(0)
pt.X.TickLength.Pt(0)
pt.X.Line.Width.Pt(0)
// pt.Y.Padding.Pt(pt.X.Tick.Label.Width(names[0]) / 2)
ticks := make([]Tick, len(names))
for i, name := range names {
ticks[i] = Tick{float32(i), name}
}
pt.X.Ticker = ConstantTicks(ticks)
}
// HideX configures the X axis so that it will not be drawn.
func (pt *Plot) HideX() {
pt.X.TickLength.Pt(0)
pt.X.Line.Width.Pt(0)
pt.X.Ticker = ConstantTicks([]Tick{})
}
// HideY configures the Y axis so that it will not be drawn.
func (pt *Plot) HideY() {
pt.Y.TickLength.Pt(0)
pt.Y.Line.Width.Pt(0)
pt.Y.Ticker = ConstantTicks([]Tick{})
}
// HideAxes hides the X and Y axes.
func (pt *Plot) HideAxes() {
pt.HideX()
pt.HideY()
}
// NominalY is like NominalX, but for the Y axis.
func (pt *Plot) NominalY(names ...string) {
pt.Y.TickLine.Width.Pt(0)
pt.Y.TickLength.Pt(0)
pt.Y.Line.Width.Pt(0)
// pt.X.Padding = pt.Y.Tick.Label.Height(names[0]) / 2
ticks := make([]Tick, len(names))
for i, name := range names {
ticks[i] = Tick{float32(i), name}
}
pt.Y.Ticker = ConstantTicks(ticks)
}
// UpdateRange updates the axis range values based on current Plot values.
// This first resets the range so any fixed additional range values should
// be set after this point.
func (pt *Plot) UpdateRange() {
pt.X.Min = math32.Inf(+1)
pt.X.Max = math32.Inf(-1)
pt.Y.Min = math32.Inf(+1)
pt.Y.Max = math32.Inf(-1)
for _, d := range pt.Plotters {
pt.UpdateRangeFromPlotter(d)
}
}
func (pt *Plot) UpdateRangeFromPlotter(d Plotter) {
if x, ok := d.(DataRanger); ok {
xmin, xmax, ymin, ymax := x.DataRange(pt)
pt.X.Min = math32.Min(pt.X.Min, xmin)
pt.X.Max = math32.Max(pt.X.Max, xmax)
pt.Y.Min = math32.Min(pt.Y.Min, ymin)
pt.Y.Max = math32.Max(pt.Y.Max, ymax)
}
}
// PX returns the X-axis plotting coordinate for given raw data value
// using the current plot bounding region
func (pt *Plot) PX(v float32) float32 {
return pt.PlotBox.ProjectX(pt.X.Norm(v))
}
// PY returns the Y-axis plotting coordinate for given raw data value
func (pt *Plot) PY(v float32) float32 {
return pt.PlotBox.ProjectY(1 - pt.Y.Norm(v))
}
// ClosestDataToPixel returns the Plotter data point closest to given pixel point,
// in the Pixels image.
func (pt *Plot) ClosestDataToPixel(px, py int) (plt Plotter, idx int, dist float32, data, pixel math32.Vector2, legend string) {
tp := math32.Vec2(float32(px), float32(py))
dist = float32(math32.MaxFloat32)
for _, p := range pt.Plotters {
dts, pxls := p.XYData()
for i := range pxls.Len() {
ptx, pty := pxls.XY(i)
pxy := math32.Vec2(ptx, pty)
d := pxy.DistanceTo(tp)
if d < dist {
dist = d
pixel = pxy
plt = p
idx = i
dx, dy := dts.XY(i)
data = math32.Vec2(dx, dy)
legend = pt.Legend.LegendForPlotter(p)
}
}
}
return
}
// Copyright (c) 2024, 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 plotcore
import (
"fmt"
"log"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/math32/minmax"
"cogentcore.org/core/plot"
"cogentcore.org/core/plot/plots"
"cogentcore.org/core/tensor/stats/split"
"cogentcore.org/core/tensor/table"
)
// bar plot is on integer positions, with different Y values and / or
// legend values interleaved
// genPlotBar generates a Bar plot, setting GPlot variable
func (pl *PlotEditor) genPlotBar() {
plt := plot.New() // note: not clear how to re-use, due to newtablexynames
if pl.Options.BarWidth > 1 {
pl.Options.BarWidth = .8
}
// process xaxis first
xi, xview, err := pl.plotXAxis(plt, pl.table)
if err != nil {
return
}
xp := pl.Columns[xi]
var lsplit *table.Splits
nleg := 1
if pl.Options.Legend != "" {
_, err = pl.table.Table.ColumnIndex(pl.Options.Legend)
if err != nil {
log.Println("plot.Legend: " + err.Error())
} else {
xview.SortColumnNames([]string{pl.Options.Legend, xp.Column}, table.Ascending) // make it fit!
lsplit = split.GroupBy(xview, pl.Options.Legend)
nleg = max(lsplit.Len(), 1)
}
}
var firstXY *tableXY
var strCols []*ColumnOptions
nys := 0
for _, cp := range pl.Columns {
if !cp.On {
continue
}
if cp.IsString {
strCols = append(strCols, cp)
continue
}
if cp.TensorIndex < 0 {
yc := errors.Log1(pl.table.Table.ColumnByName(cp.Column))
_, sz := yc.RowCellSize()
nys += sz
} else {
nys++
}
}
if nys == 0 {
return
}
stride := nys * nleg
if stride > 1 {
stride += 1 // extra gap
}
yoff := 0
yidx := 0
maxx := 0 // max number of x values
for _, cp := range pl.Columns {
if !cp.On || cp == xp {
continue
}
if cp.IsString {
continue
}
start := yoff
for li := 0; li < nleg; li++ {
lview := xview
leg := ""
if lsplit != nil && len(lsplit.Values) > li {
leg = lsplit.Values[li][0]
lview = lsplit.Splits[li]
}
nidx := 1
stidx := cp.TensorIndex
if cp.TensorIndex < 0 { // do all
yc := errors.Log1(pl.table.Table.ColumnByName(cp.Column))
_, sz := yc.RowCellSize()
nidx = sz
stidx = 0
}
for ii := 0; ii < nidx; ii++ {
idx := stidx + ii
xy, _ := newTableXYName(lview, xi, xp.TensorIndex, cp.Column, idx, cp.Range)
if xy == nil {
continue
}
maxx = max(maxx, lview.Len())
if firstXY == nil {
firstXY = xy
}
lbl := cp.getLabel()
clr := cp.Color
if leg != "" {
lbl = leg + " " + lbl
}
if nleg > 1 {
cidx := yidx*nleg + li
clr = colors.Uniform(colors.Spaced(cidx))
}
if nidx > 1 {
clr = colors.Uniform(colors.Spaced(idx))
lbl = fmt.Sprintf("%s_%02d", lbl, idx)
}
ec := -1
if cp.ErrColumn != "" {
ec, _ = pl.table.Table.ColumnIndex(cp.ErrColumn)
}
var bar *plots.BarChart
if ec >= 0 {
exy, _ := newTableXY(lview, ec, 0, ec, 0, minmax.Range32{})
bar, err = plots.NewBarChart(xy, exy)
if err != nil {
// log.Println(err)
continue
}
} else {
bar, err = plots.NewBarChart(xy, nil)
if err != nil {
// log.Println(err)
continue
}
}
bar.Color = clr
bar.Stride = float32(stride)
bar.Offset = float32(start)
bar.Width = pl.Options.BarWidth
plt.Add(bar)
plt.Legend.Add(lbl, bar)
start++
}
}
yidx++
yoff += nleg
}
mid := (stride - 1) / 2
if stride > 1 {
mid = (stride - 2) / 2
}
if firstXY != nil && len(strCols) > 0 {
firstXY.table = xview
n := xview.Len()
for _, cp := range strCols {
xy, _ := newTableXY(xview, xi, xp.TensorIndex, firstXY.yColumn, cp.TensorIndex, firstXY.yRange)
xy.labelColumn, _ = xview.Table.ColumnIndex(cp.Column)
xy.yIndex = firstXY.yIndex
xyl := plots.XYLabels{}
xyl.XYs = make(plot.XYs, n)
xyl.Labels = make([]string, n)
for i := range xview.Indexes {
y := firstXY.Value(i)
x := float32(mid + (i%maxx)*stride)
xyl.XYs[i] = math32.Vec2(x, y)
xyl.Labels[i] = xy.Label(i)
}
lbls, _ := plots.NewLabels(xyl)
if lbls != nil {
plt.Add(lbls)
}
}
}
netn := pl.table.Len() * stride
xc := pl.table.Table.Columns[xi]
vals := make([]string, netn)
for i, dx := range pl.table.Indexes {
pi := mid + i*stride
if pi < netn && dx < xc.Len() {
vals[pi] = xc.String1D(dx)
}
}
plt.NominalX(vals...)
pl.configPlot(plt)
pl.plot = plt
}
// Code generated by "core generate"; DO NOT EDIT.
package plotcore
import (
"cogentcore.org/core/enums"
)
var _PlotTypesValues = []PlotTypes{0, 1}
// PlotTypesN is the highest valid value for type PlotTypes, plus one.
const PlotTypesN PlotTypes = 2
var _PlotTypesValueMap = map[string]PlotTypes{`XY`: 0, `Bar`: 1}
var _PlotTypesDescMap = map[PlotTypes]string{0: `XY is a standard line / point plot.`, 1: `Bar plots vertical bars.`}
var _PlotTypesMap = map[PlotTypes]string{0: `XY`, 1: `Bar`}
// String returns the string representation of this PlotTypes value.
func (i PlotTypes) String() string { return enums.String(i, _PlotTypesMap) }
// SetString sets the PlotTypes value from its string representation,
// and returns an error if the string is invalid.
func (i *PlotTypes) SetString(s string) error {
return enums.SetString(i, s, _PlotTypesValueMap, "PlotTypes")
}
// Int64 returns the PlotTypes value as an int64.
func (i PlotTypes) Int64() int64 { return int64(i) }
// SetInt64 sets the PlotTypes value from an int64.
func (i *PlotTypes) SetInt64(in int64) { *i = PlotTypes(in) }
// Desc returns the description of the PlotTypes value.
func (i PlotTypes) Desc() string { return enums.Desc(i, _PlotTypesDescMap) }
// PlotTypesValues returns all possible values for the type PlotTypes.
func PlotTypesValues() []PlotTypes { return _PlotTypesValues }
// Values returns all possible values for the type PlotTypes.
func (i PlotTypes) Values() []enums.Enum { return enums.Values(_PlotTypesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i PlotTypes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *PlotTypes) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "PlotTypes")
}
// Copyright (c) 2024, 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 plotcore
import (
"image"
"strings"
"cogentcore.org/core/base/option"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/math32/minmax"
"cogentcore.org/core/plot"
"cogentcore.org/core/plot/plots"
"cogentcore.org/core/tensor/table"
)
// PlotOptions are options for the overall plot.
type PlotOptions struct { //types:add
// optional title at top of plot
Title string
// type of plot to generate. For a Bar plot, items are plotted ordinally by row and the XAxis is optional
Type PlotTypes
// whether to plot lines
Lines bool `default:"true"`
// whether to plot points with symbols
Points bool
// width of lines
LineWidth float32 `default:"1"`
// size of points
PointSize float32 `default:"3"`
// the shape used to draw points
PointShape plots.Shapes
// width of bars for bar plot, as fraction of available space (1 = no gaps)
BarWidth float32 `min:"0.01" max:"1" default:"0.8"`
// if true, draw lines that connect points with a negative X-axis direction;
// otherwise there is a break in the line.
// default is false, so that repeated series of data across the X axis
// are plotted separately.
NegativeXDraw bool
// Scale multiplies the plot DPI value, to change the overall scale
// of the rendered plot. Larger numbers produce larger scaling.
// Typically use larger numbers when generating plots for inclusion in
// documents or other cases where the overall plot size will be small.
Scale float32 `default:"1,2"`
// what column to use for the common X axis. if empty or not found,
// the row number is used. This optional for Bar plots, if present and
// Legend is also present, then an extra space will be put between X values.
XAxis string
// optional column for adding a separate colored / styled line or bar
// according to this value, and acts just like a separate Y variable,
// crossed with Y variables.
Legend string
// position of the Legend
LegendPosition plot.LegendPosition `display:"inline"`
// rotation of the X Axis labels, in degrees
XAxisRotation float32
// optional label to use for XAxis instead of column name
XAxisLabel string
// optional label to use for YAxis -- if empty, first column name is used
YAxisLabel string
}
// defaults sets defaults if unset values are present.
func (po *PlotOptions) defaults() {
if po.LineWidth == 0 {
po.LineWidth = 1
po.Lines = true
po.Points = false
po.PointSize = 3
po.BarWidth = .8
po.LegendPosition.Defaults()
}
if po.Scale == 0 {
po.Scale = 1
}
}
// fromMeta sets plot options from meta data.
func (po *PlotOptions) fromMeta(dt *table.Table) {
po.FromMetaMap(dt.MetaData)
}
// metaMapLower tries meta data access by lower-case version of key too
func metaMapLower(meta map[string]string, key string) (string, bool) {
vl, has := meta[key]
if has {
return vl, has
}
vl, has = meta[strings.ToLower(key)]
return vl, has
}
// FromMetaMap sets plot options from meta data map.
func (po *PlotOptions) FromMetaMap(meta map[string]string) {
if typ, has := metaMapLower(meta, "Type"); has {
po.Type.SetString(typ)
}
if op, has := metaMapLower(meta, "Lines"); has {
if op == "+" || op == "true" {
po.Lines = true
} else {
po.Lines = false
}
}
if op, has := metaMapLower(meta, "Points"); has {
if op == "+" || op == "true" {
po.Points = true
} else {
po.Points = false
}
}
if lw, has := metaMapLower(meta, "LineWidth"); has {
po.LineWidth, _ = reflectx.ToFloat32(lw)
}
if ps, has := metaMapLower(meta, "PointSize"); has {
po.PointSize, _ = reflectx.ToFloat32(ps)
}
if bw, has := metaMapLower(meta, "BarWidth"); has {
po.BarWidth, _ = reflectx.ToFloat32(bw)
}
if op, has := metaMapLower(meta, "NegativeXDraw"); has {
if op == "+" || op == "true" {
po.NegativeXDraw = true
} else {
po.NegativeXDraw = false
}
}
if scl, has := metaMapLower(meta, "Scale"); has {
po.Scale, _ = reflectx.ToFloat32(scl)
}
if xc, has := metaMapLower(meta, "XAxis"); has {
po.XAxis = xc
}
if lc, has := metaMapLower(meta, "Legend"); has {
po.Legend = lc
}
if xrot, has := metaMapLower(meta, "XAxisRotation"); has {
po.XAxisRotation, _ = reflectx.ToFloat32(xrot)
}
if lb, has := metaMapLower(meta, "XAxisLabel"); has {
po.XAxisLabel = lb
}
if lb, has := metaMapLower(meta, "YAxisLabel"); has {
po.YAxisLabel = lb
}
}
// ColumnOptions are options for plotting one column of data.
type ColumnOptions struct { //types:add
// whether to plot this column
On bool
// name of column being plotting
Column string
// whether to plot lines; uses the overall plot option if unset
Lines option.Option[bool]
// whether to plot points with symbols; uses the overall plot option if unset
Points option.Option[bool]
// the width of lines; uses the overall plot option if unset
LineWidth option.Option[float32]
// the size of points; uses the overall plot option if unset
PointSize option.Option[float32]
// the shape used to draw points; uses the overall plot option if unset
PointShape option.Option[plots.Shapes]
// effective range of data to plot -- either end can be fixed
Range minmax.Range32 `display:"inline"`
// full actual range of data -- only valid if specifically computed
FullRange minmax.F32 `display:"inline"`
// color to use when plotting the line / column
Color image.Image
// desired number of ticks
NTicks int
// if specified, this is an alternative label to use when plotting
Label string
// if column has n-dimensional tensor cells in each row, this is the index within each cell to plot -- use -1 to plot *all* indexes as separate lines
TensorIndex int
// specifies a column containing error bars for this column
ErrColumn string
// if true this is a string column -- plots as labels
IsString bool `edit:"-"`
}
// defaults sets defaults if unset values are present.
func (co *ColumnOptions) defaults() {
if co.NTicks == 0 {
co.NTicks = 10
}
}
// getLabel returns the effective label of the column.
func (co *ColumnOptions) getLabel() string {
if co.Label != "" {
return co.Label
}
return co.Column
}
// fromMetaMap sets column options from meta data map.
func (co *ColumnOptions) fromMetaMap(meta map[string]string) {
if op, has := metaMapLower(meta, co.Column+":On"); has {
if op == "+" || op == "true" || op == "" {
co.On = true
} else {
co.On = false
}
}
if op, has := metaMapLower(meta, co.Column+":Off"); has {
if op == "+" || op == "true" || op == "" {
co.On = false
} else {
co.On = true
}
}
if op, has := metaMapLower(meta, co.Column+":FixMin"); has {
if op == "+" || op == "true" {
co.Range.FixMin = true
} else {
co.Range.FixMin = false
}
}
if op, has := metaMapLower(meta, co.Column+":FixMax"); has {
if op == "+" || op == "true" {
co.Range.FixMax = true
} else {
co.Range.FixMax = false
}
}
if op, has := metaMapLower(meta, co.Column+":FloatMin"); has {
if op == "+" || op == "true" {
co.Range.FixMin = false
} else {
co.Range.FixMin = true
}
}
if op, has := metaMapLower(meta, co.Column+":FloatMax"); has {
if op == "+" || op == "true" {
co.Range.FixMax = false
} else {
co.Range.FixMax = true
}
}
if vl, has := metaMapLower(meta, co.Column+":Max"); has {
co.Range.Max, _ = reflectx.ToFloat32(vl)
}
if vl, has := metaMapLower(meta, co.Column+":Min"); has {
co.Range.Min, _ = reflectx.ToFloat32(vl)
}
if lb, has := metaMapLower(meta, co.Column+":Label"); has {
co.Label = lb
}
if lb, has := metaMapLower(meta, co.Column+":ErrColumn"); has {
co.ErrColumn = lb
}
if vl, has := metaMapLower(meta, co.Column+":TensorIndex"); has {
iv, _ := reflectx.ToInt(vl)
co.TensorIndex = int(iv)
}
}
// PlotTypes are different types of plots.
type PlotTypes int32 //enums:enum
const (
// XY is a standard line / point plot.
XY PlotTypes = iota
// Bar plots vertical bars.
Bar
)
// Copyright (c) 2024, 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 plotcore
import (
"fmt"
"image"
"image/draw"
"cogentcore.org/core/colors"
"cogentcore.org/core/core"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/plot"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
)
// Plot is a widget that renders a [plot.Plot] object.
// If it is not [states.ReadOnly], the user can pan and zoom the graph.
// See [PlotEditor] for an interactive interface for selecting columns to view.
type Plot struct {
core.WidgetBase
// Scale multiplies the plot DPI value, to change the overall scale
// of the rendered plot. Larger numbers produce larger scaling.
// Typically use larger numbers when generating plots for inclusion in
// documents or other cases where the overall plot size will be small.
Scale float32
// Plot is the Plot to display in this widget
Plot *plot.Plot `set:"-"`
// SetRangesFunc, if set, is called to adjust the data ranges
// after the point when these ranges are updated based on the plot data.
SetRangesFunc func()
}
// SetPlot sets the plot to given Plot, and calls UpdatePlot to ensure it is
// drawn at the current size of this widget
func (pt *Plot) SetPlot(pl *plot.Plot) {
if pl != nil && pt.Plot != nil && pt.Plot.Pixels != nil {
pl.DPI = pt.Scale * pt.Styles.UnitContext.DPI
pl.SetPixels(pt.Plot.Pixels) // re-use the image!
}
pt.Plot = pl
pt.updatePlot()
}
// updatePlot draws the current plot at the size of the current widget,
// and triggers a Render so the widget will be rendered.
func (pt *Plot) updatePlot() {
if pt.Plot == nil {
pt.NeedsRender()
return
}
sz := pt.Geom.Size.Actual.Content.ToPoint()
if sz == (image.Point{}) {
return
}
pt.Plot.DPI = pt.Scale * pt.Styles.UnitContext.DPI
pt.Plot.Resize(sz)
if pt.SetRangesFunc != nil {
pt.SetRangesFunc()
}
pt.Plot.Draw()
pt.NeedsRender()
}
func (pt *Plot) Init() {
pt.WidgetBase.Init()
pt.Scale = 1
pt.Styler(func(s *styles.Style) {
s.Min.Set(units.Dp(256))
ro := pt.IsReadOnly()
s.SetAbilities(!ro, abilities.Slideable, abilities.Activatable, abilities.Scrollable)
if !ro {
if s.Is(states.Active) {
s.Cursor = cursors.Grabbing
s.StateLayer = 0
} else {
s.Cursor = cursors.Grab
}
}
})
pt.On(events.SlideMove, func(e events.Event) {
e.SetHandled()
if pt.Plot == nil {
return
}
del := e.PrevDelta()
dx := -float32(del.X) * (pt.Plot.X.Max - pt.Plot.X.Min) * 0.0008
dy := float32(del.Y) * (pt.Plot.Y.Max - pt.Plot.Y.Min) * 0.0008
pt.Plot.X.Min += dx
pt.Plot.X.Max += dx
pt.Plot.Y.Min += dy
pt.Plot.Y.Max += dy
pt.updatePlot()
pt.NeedsRender()
})
pt.On(events.Scroll, func(e events.Event) {
e.SetHandled()
if pt.Plot == nil {
return
}
se := e.(*events.MouseScroll)
sc := 1 + (float32(se.Delta.Y) * 0.002)
pt.Plot.X.Min *= sc
pt.Plot.X.Max *= sc
pt.Plot.Y.Min *= sc
pt.Plot.Y.Max *= sc
pt.updatePlot()
pt.NeedsRender()
})
}
func (pt *Plot) WidgetTooltip(pos image.Point) (string, image.Point) {
if pos == image.Pt(-1, -1) {
return "_", image.Point{}
}
if pt.Plot == nil {
return pt.Tooltip, pt.DefaultTooltipPos()
}
wpos := pos.Sub(pt.Geom.ContentBBox.Min)
_, idx, dist, data, _, legend := pt.Plot.ClosestDataToPixel(wpos.X, wpos.Y)
if dist <= 10 {
return fmt.Sprintf("%s[%d]: (%g, %g)", legend, idx, data.X, data.Y), pos
}
return pt.Tooltip, pt.DefaultTooltipPos()
}
func (pt *Plot) SizeFinal() {
pt.WidgetBase.SizeFinal()
pt.updatePlot()
}
func (pt *Plot) Render() {
pt.WidgetBase.Render()
r := pt.Geom.ContentBBox
sp := pt.Geom.ScrollOffset()
if pt.Plot == nil || pt.Plot.Pixels == nil {
draw.Draw(pt.Scene.Pixels, r, colors.Scheme.Surface, sp, draw.Src)
return
}
draw.Draw(pt.Scene.Pixels, r, pt.Plot.Pixels, sp, draw.Src)
}
// Copyright (c) 2024, 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 plotcore provides Cogent Core widgets for viewing and editing plots.
package plotcore
//go:generate core generate
import (
"io/fs"
"log/slog"
"path/filepath"
"reflect"
"strings"
"time"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/colors"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/math32"
"cogentcore.org/core/plot"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/system"
"cogentcore.org/core/tensor/table"
"cogentcore.org/core/tensor/tensorcore"
"cogentcore.org/core/tree"
)
// PlotEditor is a widget that provides an interactive 2D plot
// of selected columns of tabular data, represented by a [table.IndexView] into
// a [table.Table]. Other types of tabular data can be converted into this format.
// The user can change various options for the plot and also modify the underlying data.
type PlotEditor struct { //types:add
core.Frame
// table is the table of data being plotted.
table *table.IndexView
// Options are the overall plot options.
Options PlotOptions
// Columns are the options for each column of the table.
Columns []*ColumnOptions `set:"-"`
// plot is the plot object.
plot *plot.Plot
// current svg file
svgFile core.Filename
// current csv data file
dataFile core.Filename
// currently doing a plot
inPlot bool
columnsFrame *core.Frame
plotWidget *Plot
}
func (pl *PlotEditor) CopyFieldsFrom(frm tree.Node) {
fr := frm.(*PlotEditor)
pl.Frame.CopyFieldsFrom(&fr.Frame)
pl.Options = fr.Options
pl.setIndexView(fr.table)
mx := min(len(pl.Columns), len(fr.Columns))
for i := 0; i < mx; i++ {
*pl.Columns[i] = *fr.Columns[i]
}
}
// NewSubPlot returns a [PlotEditor] with its own separate [core.Toolbar],
// suitable for a tab or other element that is not the main plot.
func NewSubPlot(parent ...tree.Node) *PlotEditor {
fr := core.NewFrame(parent...)
tb := core.NewToolbar(fr)
pl := NewPlotEditor(fr)
fr.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Grow.Set(1, 1)
})
tb.Maker(pl.MakeToolbar)
return pl
}
func (pl *PlotEditor) Init() {
pl.Frame.Init()
pl.Options.defaults()
pl.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
if pl.SizeClass() == core.SizeCompact {
s.Direction = styles.Column
}
})
pl.OnShow(func(e events.Event) {
pl.UpdatePlot()
})
pl.Updater(func() {
if pl.table != nil && pl.table.Table != nil {
pl.Options.fromMeta(pl.table.Table)
}
})
tree.AddChildAt(pl, "columns", func(w *core.Frame) {
pl.columnsFrame = w
w.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Background = colors.Scheme.SurfaceContainerLow
if w.SizeClass() == core.SizeCompact {
s.Grow.Set(1, 0)
} else {
s.Grow.Set(0, 1)
s.Overflow.Y = styles.OverflowAuto
}
})
w.Maker(pl.makeColumns)
})
tree.AddChildAt(pl, "plot", func(w *Plot) {
pl.plotWidget = w
w.Plot = pl.plot
w.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
})
})
}
// setIndexView sets the table to view and does Update
// to update the Column list, which will also trigger a Layout
// and updating of the plot on next render pass.
// This is safe to call from a different goroutine.
func (pl *PlotEditor) setIndexView(tab *table.IndexView) *PlotEditor {
pl.table = tab
pl.Update()
return pl
}
// SetTable sets the table to view and does Update
// to update the Column list, which will also trigger a Layout
// and updating of the plot on next render pass.
// This is safe to call from a different goroutine.
func (pl *PlotEditor) SetTable(tab *table.Table) *PlotEditor {
pl.table = table.NewIndexView(tab)
pl.Update()
return pl
}
// SetSlice sets the table to a [table.NewSliceTable]
// from the given slice.
func (pl *PlotEditor) SetSlice(sl any) *PlotEditor {
return pl.SetTable(errors.Log1(table.NewSliceTable(sl)))
}
// ColumnOptions returns the current column options by name
// (to access by index, just use Columns directly).
func (pl *PlotEditor) ColumnOptions(column string) *ColumnOptions {
for _, co := range pl.Columns {
if co.Column == column {
return co
}
}
return nil
}
// Bool constants for [PlotEditor.SetColumnOptions].
const (
On = true
Off = false
FixMin = true
FloatMin = false
FixMax = true
FloatMax = false
)
// SetColumnOptions sets the main parameters for one column.
func (pl *PlotEditor) SetColumnOptions(column string, on bool, fixMin bool, min float32, fixMax bool, max float32) *ColumnOptions {
co := pl.ColumnOptions(column)
if co == nil {
slog.Error("plotcore.PlotEditor.SetColumnOptions: column not found", "column", column)
return nil
}
co.On = on
co.Range.FixMin = fixMin
if fixMin {
co.Range.Min = min
}
co.Range.FixMax = fixMax
if fixMax {
co.Range.Max = max
}
return co
}
// SaveSVG saves the plot to an svg -- first updates to ensure that plot is current
func (pl *PlotEditor) SaveSVG(fname core.Filename) { //types:add
pl.UpdatePlot()
// TODO: get plot svg saving working
// pc := pl.PlotChild()
// SaveSVGView(string(fname), pl.Plot, sv, 2)
pl.svgFile = fname
}
// SavePNG saves the current plot to a png, capturing current render
func (pl *PlotEditor) SavePNG(fname core.Filename) { //types:add
pl.UpdatePlot()
imagex.Save(pl.plot.Pixels, string(fname))
}
// SaveCSV saves the Table data to a csv (comma-separated values) file with headers (any delim)
func (pl *PlotEditor) SaveCSV(fname core.Filename, delim table.Delims) { //types:add
pl.table.SaveCSV(fname, delim, table.Headers)
pl.dataFile = fname
}
// SaveAll saves the current plot to a png, svg, and the data to a tsv -- full save
// Any extension is removed and appropriate extensions are added
func (pl *PlotEditor) SaveAll(fname core.Filename) { //types:add
fn := string(fname)
fn = strings.TrimSuffix(fn, filepath.Ext(fn))
pl.SaveCSV(core.Filename(fn+".tsv"), table.Tab)
pl.SavePNG(core.Filename(fn + ".png"))
pl.SaveSVG(core.Filename(fn + ".svg"))
}
// OpenCSV opens the Table data from a csv (comma-separated values) file (or any delim)
func (pl *PlotEditor) OpenCSV(filename core.Filename, delim table.Delims) { //types:add
pl.table.Table.OpenCSV(filename, delim)
pl.dataFile = filename
pl.UpdatePlot()
}
// OpenFS opens the Table data from a csv (comma-separated values) file (or any delim)
// from the given filesystem.
func (pl *PlotEditor) OpenFS(fsys fs.FS, filename core.Filename, delim table.Delims) {
pl.table.Table.OpenFS(fsys, string(filename), delim)
pl.dataFile = filename
pl.UpdatePlot()
}
// yLabel returns the Y-axis label
func (pl *PlotEditor) yLabel() string {
if pl.Options.YAxisLabel != "" {
return pl.Options.YAxisLabel
}
for _, cp := range pl.Columns {
if cp.On {
return cp.getLabel()
}
}
return "Y"
}
// xLabel returns the X-axis label
func (pl *PlotEditor) xLabel() string {
if pl.Options.XAxisLabel != "" {
return pl.Options.XAxisLabel
}
if pl.Options.XAxis != "" {
cp := pl.ColumnOptions(pl.Options.XAxis)
if cp != nil {
return cp.getLabel()
}
return pl.Options.XAxis
}
return "X"
}
// GoUpdatePlot updates the display based on current IndexView into table.
// This version can be called from goroutines. It does Sequential() on
// the [table.IndexView], under the assumption that it is used for tracking a
// the latest updates of a running process.
func (pl *PlotEditor) GoUpdatePlot() {
if pl == nil || pl.This == nil {
return
}
if core.TheApp.Platform() == system.Web {
time.Sleep(time.Millisecond) // critical to prevent hanging!
}
if !pl.IsVisible() || pl.table == nil || pl.table.Table == nil || pl.inPlot {
return
}
pl.Scene.AsyncLock()
pl.table.Sequential()
pl.genPlot()
pl.NeedsRender()
pl.Scene.AsyncUnlock()
}
// UpdatePlot updates the display based on current IndexView into table.
// It does not automatically update the [table.IndexView] unless it is
// nil or out date.
func (pl *PlotEditor) UpdatePlot() {
if pl == nil || pl.This == nil {
return
}
if pl.table == nil || pl.table.Table == nil || pl.inPlot {
return
}
if len(pl.Children) != 2 || len(pl.Columns) != pl.table.Table.NumColumns() {
pl.Update()
}
if pl.table.Len() == 0 {
pl.table.Sequential()
}
pl.genPlot()
}
// genPlot generates the plot and renders it to SVG
// It surrounds operation with InPlot true / false to prevent multiple updates
func (pl *PlotEditor) genPlot() {
if pl.inPlot {
slog.Error("plot: in plot already") // note: this never seems to happen -- could probably nuke
return
}
pl.inPlot = true
if pl.table == nil {
pl.inPlot = false
return
}
if len(pl.table.Indexes) == 0 {
pl.table.Sequential()
} else {
lsti := pl.table.Indexes[pl.table.Len()-1]
if lsti >= pl.table.Table.Rows { // out of date
pl.table.Sequential()
}
}
pl.plot = nil
switch pl.Options.Type {
case XY:
pl.genPlotXY()
case Bar:
pl.genPlotBar()
}
pl.plotWidget.Scale = pl.Options.Scale
pl.plotWidget.SetRangesFunc = func() {
plt := pl.plotWidget.Plot
xi, err := pl.table.Table.ColumnIndex(pl.Options.XAxis)
if err == nil {
xp := pl.Columns[xi]
if xp.Range.FixMin {
plt.X.Min = math32.Min(plt.X.Min, float32(xp.Range.Min))
}
if xp.Range.FixMax {
plt.X.Max = math32.Max(plt.X.Max, float32(xp.Range.Max))
}
}
for _, cp := range pl.Columns { // key that this comes at the end, to actually stick
if !cp.On || cp.IsString {
continue
}
if cp.Range.FixMin {
plt.Y.Min = math32.Min(plt.Y.Min, float32(cp.Range.Min))
}
if cp.Range.FixMax {
plt.Y.Max = math32.Max(plt.Y.Max, float32(cp.Range.Max))
}
}
}
pl.plotWidget.SetPlot(pl.plot) // redraws etc
pl.inPlot = false
}
// configPlot configures the given plot based on the plot options.
func (pl *PlotEditor) configPlot(plt *plot.Plot) {
plt.Title.Text = pl.Options.Title
plt.X.Label.Text = pl.xLabel()
plt.Y.Label.Text = pl.yLabel()
plt.Legend.Position = pl.Options.LegendPosition
plt.X.TickText.Style.Rotation = float32(pl.Options.XAxisRotation)
}
// plotXAxis processes the XAxis and returns its index
func (pl *PlotEditor) plotXAxis(plt *plot.Plot, ixvw *table.IndexView) (xi int, xview *table.IndexView, err error) {
xi, err = ixvw.Table.ColumnIndex(pl.Options.XAxis)
if err != nil {
// log.Println("plot.PlotXAxis: " + err.Error())
return
}
xview = ixvw
xc := ixvw.Table.Columns[xi]
xp := pl.Columns[xi]
sz := 1
if xp.Range.FixMin {
plt.X.Min = math32.Min(plt.X.Min, float32(xp.Range.Min))
}
if xp.Range.FixMax {
plt.X.Max = math32.Max(plt.X.Max, float32(xp.Range.Max))
}
if xc.NumDims() > 1 {
sz = xc.Len() / xc.DimSize(0)
if xp.TensorIndex > sz || xp.TensorIndex < 0 {
slog.Error("plotcore.PlotEditor.plotXAxis: TensorIndex invalid -- reset to 0")
xp.TensorIndex = 0
}
}
return
}
const plotColumnsHeaderN = 2
// columnsListUpdate updates the list of columns
func (pl *PlotEditor) columnsListUpdate() {
if pl.table == nil || pl.table.Table == nil {
pl.Columns = nil
return
}
dt := pl.table.Table
nc := dt.NumColumns()
if nc == len(pl.Columns) {
return
}
pl.Columns = make([]*ColumnOptions, nc)
clri := 0
hasOn := false
for ci := range dt.NumColumns() {
cn := dt.ColumnName(ci)
if pl.Options.XAxis == "" && ci == 0 {
pl.Options.XAxis = cn // x-axis defaults to the first column
}
cp := &ColumnOptions{Column: cn}
cp.defaults()
tcol := dt.Columns[ci]
if tcol.IsString() {
cp.IsString = true
} else {
cp.IsString = false
// we enable the first non-string, non-x-axis, non-first column by default
if !hasOn && cn != pl.Options.XAxis && ci != 0 {
cp.On = true
hasOn = true
}
}
cp.fromMetaMap(pl.table.Table.MetaData)
inc := 1
if cn == pl.Options.XAxis || tcol.IsString() || tcol.DataType() == reflect.Int || tcol.DataType() == reflect.Int64 || tcol.DataType() == reflect.Int32 || tcol.DataType() == reflect.Uint8 {
inc = 0
}
cp.Color = colors.Uniform(colors.Spaced(clri))
pl.Columns[ci] = cp
clri += inc
}
}
// ColumnsFromMetaMap updates all the column settings from given meta map
func (pl *PlotEditor) ColumnsFromMetaMap(meta map[string]string) {
for _, cp := range pl.Columns {
cp.fromMetaMap(meta)
}
}
// setAllColumns turns all Columns on or off (except X axis)
func (pl *PlotEditor) setAllColumns(on bool) {
fr := pl.columnsFrame
for i, cli := range fr.Children {
if i < plotColumnsHeaderN {
continue
}
ci := i - plotColumnsHeaderN
cp := pl.Columns[ci]
if cp.Column == pl.Options.XAxis {
continue
}
cp.On = on
cl := cli.(*core.Frame)
sw := cl.Child(0).(*core.Switch)
sw.SetChecked(cp.On)
}
pl.UpdatePlot()
pl.NeedsRender()
}
// setColumnsByName turns columns on or off if their name contains
// the given string.
func (pl *PlotEditor) setColumnsByName(nameContains string, on bool) { //types:add
fr := pl.columnsFrame
for i, cli := range fr.Children {
if i < plotColumnsHeaderN {
continue
}
ci := i - plotColumnsHeaderN
cp := pl.Columns[ci]
if cp.Column == pl.Options.XAxis {
continue
}
if !strings.Contains(cp.Column, nameContains) {
continue
}
cp.On = on
cl := cli.(*core.Frame)
sw := cl.Child(0).(*core.Switch)
sw.SetChecked(cp.On)
}
pl.UpdatePlot()
pl.NeedsRender()
}
// makeColumns makes the Plans for columns
func (pl *PlotEditor) makeColumns(p *tree.Plan) {
pl.columnsListUpdate()
tree.Add(p, func(w *core.Frame) {
tree.AddChild(w, func(w *core.Button) {
w.SetText("Clear").SetIcon(icons.ClearAll).SetType(core.ButtonAction)
w.SetTooltip("Turn all columns off")
w.OnClick(func(e events.Event) {
pl.setAllColumns(false)
})
})
tree.AddChild(w, func(w *core.Button) {
w.SetText("Search").SetIcon(icons.Search).SetType(core.ButtonAction)
w.SetTooltip("Select columns by column name")
w.OnClick(func(e events.Event) {
core.CallFunc(pl, pl.setColumnsByName)
})
})
})
tree.Add(p, func(w *core.Separator) {})
for _, cp := range pl.Columns {
tree.AddAt(p, cp.Column, func(w *core.Frame) {
w.Styler(func(s *styles.Style) {
s.CenterAll()
})
tree.AddChild(w, func(w *core.Switch) {
w.SetType(core.SwitchCheckbox).SetTooltip("Turn this column on or off")
w.OnChange(func(e events.Event) {
cp.On = w.IsChecked()
pl.UpdatePlot()
})
w.Updater(func() {
xaxis := cp.Column == pl.Options.XAxis || cp.Column == pl.Options.Legend
w.SetState(xaxis, states.Disabled, states.Indeterminate)
if xaxis {
cp.On = false
} else {
w.SetChecked(cp.On)
}
})
})
tree.AddChild(w, func(w *core.Button) {
w.SetText(cp.Column).SetType(core.ButtonAction).SetTooltip("Edit column options including setting it as the x-axis or legend")
w.OnClick(func(e events.Event) {
update := func() {
if core.TheApp.Platform().IsMobile() {
pl.Update()
return
}
// we must be async on multi-window platforms since
// it is coming from a separate window
pl.AsyncLock()
pl.Update()
pl.AsyncUnlock()
}
d := core.NewBody("Column options")
core.NewForm(d).SetStruct(cp).
OnChange(func(e events.Event) {
update()
})
d.AddTopBar(func(bar *core.Frame) {
core.NewToolbar(bar).Maker(func(p *tree.Plan) {
tree.Add(p, func(w *core.Button) {
w.SetText("Set x-axis").OnClick(func(e events.Event) {
pl.Options.XAxis = cp.Column
update()
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("Set legend").OnClick(func(e events.Event) {
pl.Options.Legend = cp.Column
update()
})
})
})
})
d.RunWindowDialog(pl)
})
})
})
}
}
func (pl *PlotEditor) MakeToolbar(p *tree.Plan) {
if pl.table == nil {
return
}
tree.Add(p, func(w *core.Button) {
w.SetIcon(icons.PanTool).
SetTooltip("toggle the ability to zoom and pan the view").OnClick(func(e events.Event) {
pw := pl.plotWidget
pw.SetReadOnly(!pw.IsReadOnly())
pw.Restyle()
})
})
// tree.Add(p, func(w *core.Button) {
// w.SetIcon(icons.ArrowForward).
// SetTooltip("turn on select mode for selecting Plot elements").
// OnClick(func(e events.Event) {
// fmt.Println("this will select select mode")
// })
// })
tree.Add(p, func(w *core.Separator) {})
tree.Add(p, func(w *core.Button) {
w.SetText("Update").SetIcon(icons.Update).
SetTooltip("update fully redraws display, reflecting any new settings etc").
OnClick(func(e events.Event) {
pl.UpdateWidget()
pl.UpdatePlot()
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("Options").SetIcon(icons.Settings).
SetTooltip("Options for how the plot is rendered").
OnClick(func(e events.Event) {
d := core.NewBody("Plot options")
core.NewForm(d).SetStruct(&pl.Options).
OnChange(func(e events.Event) {
pl.GoUpdatePlot()
})
d.RunWindowDialog(pl)
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("Table").SetIcon(icons.Edit).
SetTooltip("open a Table window of the data").
OnClick(func(e events.Event) {
d := core.NewBody(pl.Name + " Data")
tv := tensorcore.NewTable(d).SetTable(pl.table.Table)
d.AddTopBar(func(bar *core.Frame) {
core.NewToolbar(bar).Maker(tv.MakeToolbar)
})
d.RunWindowDialog(pl)
})
})
tree.Add(p, func(w *core.Separator) {})
tree.Add(p, func(w *core.Button) {
w.SetText("Save").SetIcon(icons.Save).SetMenu(func(m *core.Scene) {
core.NewFuncButton(m).SetFunc(pl.SaveSVG).SetIcon(icons.Save)
core.NewFuncButton(m).SetFunc(pl.SavePNG).SetIcon(icons.Save)
core.NewFuncButton(m).SetFunc(pl.SaveCSV).SetIcon(icons.Save)
core.NewSeparator(m)
core.NewFuncButton(m).SetFunc(pl.SaveAll).SetIcon(icons.Save)
})
})
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(pl.OpenCSV).SetIcon(icons.Open)
})
tree.Add(p, func(w *core.Separator) {})
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(pl.table.FilterColumnName).SetText("Filter").SetIcon(icons.FilterAlt)
w.SetAfterFunc(pl.UpdatePlot)
})
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(pl.table.Sequential).SetText("Unfilter").SetIcon(icons.FilterAltOff)
w.SetAfterFunc(pl.UpdatePlot)
})
}
func (pt *PlotEditor) SizeFinal() {
pt.Frame.SizeFinal()
pt.UpdatePlot()
}
// Copyright (c) 2024, 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 plotcore
import (
"cogentcore.org/core/base/errors"
"cogentcore.org/core/math32"
"cogentcore.org/core/math32/minmax"
"cogentcore.org/core/plot"
"cogentcore.org/core/plot/plots"
"cogentcore.org/core/tensor/table"
)
// tableXY selects two columns from a [table.Table] data table to plot in a [plot.Plot],
// satisfying the [plot.XYer], [plot.Valuer], [plot.Labeler], and [plots.YErrorer] interfaces.
// For Tensor-valued cells, Index's specify tensor cell.
// Also satisfies the plot/plots.Labeler interface for labels attached to a line, and
// plot/plots.YErrorer for error bars.
type tableXY struct {
// the index view of data table to plot from
table *table.IndexView
// the indexes of the tensor columns to use for the X and Y data, respectively
xColumn, yColumn int
// numer of elements in each row of data -- 1 for scalar, > 1 for multi-dimensional
xRowSize, yRowSize int
// the indexes of the element within each tensor cell if cells are n-dimensional, respectively
xIndex, yIndex int
// the column to use for returning a label using Label interface -- for string cols
labelColumn int
// the column to use for returning errorbars (+/- given value) -- if YColumn is tensor then this must also be a tensor and given YIndex used
errColumn int
// range constraints on Y values
yRange minmax.Range32
}
var _ plot.XYer = &tableXY{}
var _ plot.Valuer = &tableXY{}
var _ plot.Labeler = &tableXY{}
var _ plots.YErrorer = &tableXY{}
// newTableXY returns a new XY plot view onto the given IndexView of table.Table (makes a copy),
// from given column indexes, and tensor indexes within each cell.
// Column indexes are enforced to be valid, with an error message if they are not.
func newTableXY(dt *table.IndexView, xcol, xtsrIndex, ycol, ytsrIndex int, yrng minmax.Range32) (*tableXY, error) {
txy := &tableXY{table: dt.Clone(), xColumn: xcol, yColumn: ycol, xIndex: xtsrIndex, yIndex: ytsrIndex, yRange: yrng}
return txy, txy.validate()
}
// newTableXYName returns a new XY plot view onto the given IndexView of table.Table (makes a copy),
// from given column name and tensor indexes within each cell.
// Column indexes are enforced to be valid, with an error message if they are not.
func newTableXYName(dt *table.IndexView, xi, xtsrIndex int, ycol string, ytsrIndex int, yrng minmax.Range32) (*tableXY, error) {
yi, err := dt.Table.ColumnIndex(ycol)
if errors.Log(err) != nil {
return nil, err
}
txy := &tableXY{table: dt.Clone(), xColumn: xi, yColumn: yi, xIndex: xtsrIndex, yIndex: ytsrIndex, yRange: yrng}
return txy, txy.validate()
}
// validate returns error message if column indexes are invalid, else nil
// it also sets column indexes to 0 so nothing crashes.
func (txy *tableXY) validate() error {
if txy.table == nil {
return errors.New("eplot.TableXY table is nil")
}
nc := txy.table.Table.NumColumns()
if txy.xColumn >= nc || txy.xColumn < 0 {
txy.xColumn = 0
return errors.New("eplot.TableXY XColumn index invalid -- reset to 0")
}
if txy.yColumn >= nc || txy.yColumn < 0 {
txy.yColumn = 0
return errors.New("eplot.TableXY YColumn index invalid -- reset to 0")
}
xc := txy.table.Table.Columns[txy.xColumn]
yc := txy.table.Table.Columns[txy.yColumn]
if xc.NumDims() > 1 {
_, txy.xRowSize = xc.RowCellSize()
// note: index already validated
}
if yc.NumDims() > 1 {
_, txy.yRowSize = yc.RowCellSize()
if txy.yIndex >= txy.yRowSize || txy.yIndex < 0 {
txy.yIndex = 0
return errors.New("eplot.TableXY Y TensorIndex invalid -- reset to 0")
}
}
txy.filterValues()
return nil
}
// filterValues removes items with NaN values, and out of Y range
func (txy *tableXY) filterValues() {
txy.table.Filter(func(et *table.Table, row int) bool {
xv := txy.tRowXValue(row)
yv := txy.tRowValue(row)
if math32.IsNaN(yv) || math32.IsNaN(xv) {
return false
}
if txy.yRange.FixMin && yv < txy.yRange.Min {
return false
}
if txy.yRange.FixMax && yv > txy.yRange.Max {
return false
}
return true
})
}
// Len returns the number of rows in the view of table
func (txy *tableXY) Len() int {
if txy.table == nil || txy.table.Table == nil {
return 0
}
return txy.table.Len()
}
// tRowValue returns the y value at given true table row in table
func (txy *tableXY) tRowValue(row int) float32 {
yc := txy.table.Table.Columns[txy.yColumn]
y := float32(0.0)
switch {
case yc.IsString():
y = float32(row)
case yc.NumDims() > 1:
_, sz := yc.RowCellSize()
if txy.yIndex < sz && txy.yIndex >= 0 {
y = float32(yc.FloatRowCell(row, txy.yIndex))
}
default:
y = float32(yc.Float1D(row))
}
return y
}
// Value returns the y value at given row in table
func (txy *tableXY) Value(row int) float32 {
if txy.table == nil || txy.table.Table == nil || row >= txy.table.Len() {
return 0
}
trow := txy.table.Indexes[row] // true table row
yc := txy.table.Table.Columns[txy.yColumn]
y := float32(0.0)
switch {
case yc.IsString():
y = float32(row)
case yc.NumDims() > 1:
_, sz := yc.RowCellSize()
if txy.yIndex < sz && txy.yIndex >= 0 {
y = float32(yc.FloatRowCell(trow, txy.yIndex))
}
default:
y = float32(yc.Float1D(trow))
}
return y
}
// tRowXValue returns an x value at given actual row in table
func (txy *tableXY) tRowXValue(row int) float32 {
if txy.table == nil || txy.table.Table == nil {
return 0
}
xc := txy.table.Table.Columns[txy.xColumn]
x := float32(0.0)
switch {
case xc.IsString():
x = float32(row)
case xc.NumDims() > 1:
_, sz := xc.RowCellSize()
if txy.xIndex < sz && txy.xIndex >= 0 {
x = float32(xc.FloatRowCell(row, txy.xIndex))
}
default:
x = float32(xc.Float1D(row))
}
return x
}
// xValue returns an x value at given row in table
func (txy *tableXY) xValue(row int) float32 {
if txy.table == nil || txy.table.Table == nil || row >= txy.table.Len() {
return 0
}
trow := txy.table.Indexes[row] // true table row
xc := txy.table.Table.Columns[txy.xColumn]
x := float32(0.0)
switch {
case xc.IsString():
x = float32(row)
case xc.NumDims() > 1:
_, sz := xc.RowCellSize()
if txy.xIndex < sz && txy.xIndex >= 0 {
x = float32(xc.FloatRowCell(trow, txy.xIndex))
}
default:
x = float32(xc.Float1D(trow))
}
return x
}
// XY returns an x, y pair at given row in table
func (txy *tableXY) XY(row int) (x, y float32) {
if txy.table == nil || txy.table.Table == nil {
return 0, 0
}
x = txy.xValue(row)
y = txy.Value(row)
return
}
// Label returns a label for given row in table, implementing [plot.Labeler] interface
func (txy *tableXY) Label(row int) string {
if txy.table == nil || txy.table.Table == nil || row >= txy.table.Len() {
return ""
}
trow := txy.table.Indexes[row] // true table row
return txy.table.Table.Columns[txy.labelColumn].String1D(trow)
}
// YError returns error bars, implementing [plots.YErrorer] interface.
func (txy *tableXY) YError(row int) (float32, float32) {
if txy.table == nil || txy.table.Table == nil || row >= txy.table.Len() {
return 0, 0
}
trow := txy.table.Indexes[row] // true table row
ec := txy.table.Table.Columns[txy.errColumn]
eval := float32(0.0)
switch {
case ec.IsString():
eval = float32(row)
case ec.NumDims() > 1:
_, sz := ec.RowCellSize()
if txy.yIndex < sz && txy.yIndex >= 0 {
eval = float32(ec.FloatRowCell(trow, txy.yIndex))
}
default:
eval = float32(ec.Float1D(trow))
}
return -eval, eval
}
// Code generated by "core generate"; DO NOT EDIT.
package plotcore
import (
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/plot/plotcore.PlotOptions", IDName: "plot-options", Doc: "PlotOptions are options for the overall plot.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "Title", Doc: "optional title at top of plot"}, {Name: "Type", Doc: "type of plot to generate. For a Bar plot, items are plotted ordinally by row and the XAxis is optional"}, {Name: "Lines", Doc: "whether to plot lines"}, {Name: "Points", Doc: "whether to plot points with symbols"}, {Name: "LineWidth", Doc: "width of lines"}, {Name: "PointSize", Doc: "size of points"}, {Name: "PointShape", Doc: "the shape used to draw points"}, {Name: "BarWidth", Doc: "width of bars for bar plot, as fraction of available space (1 = no gaps)"}, {Name: "NegativeXDraw", Doc: "if true, draw lines that connect points with a negative X-axis direction;\notherwise there is a break in the line.\ndefault is false, so that repeated series of data across the X axis\nare plotted separately."}, {Name: "Scale", Doc: "Scale multiplies the plot DPI value, to change the overall scale\nof the rendered plot. Larger numbers produce larger scaling.\nTypically use larger numbers when generating plots for inclusion in\ndocuments or other cases where the overall plot size will be small."}, {Name: "XAxis", Doc: "what column to use for the common X axis. if empty or not found,\nthe row number is used. This optional for Bar plots, if present and\nLegend is also present, then an extra space will be put between X values."}, {Name: "Legend", Doc: "optional column for adding a separate colored / styled line or bar\naccording to this value, and acts just like a separate Y variable,\ncrossed with Y variables."}, {Name: "LegendPosition", Doc: "position of the Legend"}, {Name: "XAxisRotation", Doc: "rotation of the X Axis labels, in degrees"}, {Name: "XAxisLabel", Doc: "optional label to use for XAxis instead of column name"}, {Name: "YAxisLabel", Doc: "optional label to use for YAxis -- if empty, first column name is used"}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/plot/plotcore.ColumnOptions", IDName: "column-options", Doc: "ColumnOptions are options for plotting one column of data.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "On", Doc: "whether to plot this column"}, {Name: "Column", Doc: "name of column being plotting"}, {Name: "Lines", Doc: "whether to plot lines; uses the overall plot option if unset"}, {Name: "Points", Doc: "whether to plot points with symbols; uses the overall plot option if unset"}, {Name: "LineWidth", Doc: "the width of lines; uses the overall plot option if unset"}, {Name: "PointSize", Doc: "the size of points; uses the overall plot option if unset"}, {Name: "PointShape", Doc: "the shape used to draw points; uses the overall plot option if unset"}, {Name: "Range", Doc: "effective range of data to plot -- either end can be fixed"}, {Name: "FullRange", Doc: "full actual range of data -- only valid if specifically computed"}, {Name: "Color", Doc: "color to use when plotting the line / column"}, {Name: "NTicks", Doc: "desired number of ticks"}, {Name: "Label", Doc: "if specified, this is an alternative label to use when plotting"}, {Name: "TensorIndex", Doc: "if column has n-dimensional tensor cells in each row, this is the index within each cell to plot -- use -1 to plot *all* indexes as separate lines"}, {Name: "ErrColumn", Doc: "specifies a column containing error bars for this column"}, {Name: "IsString", Doc: "if true this is a string column -- plots as labels"}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/plot/plotcore.Plot", IDName: "plot", Doc: "Plot is a widget that renders a [plot.Plot] object.\nIf it is not [states.ReadOnly], the user can pan and zoom the graph.\nSee [PlotEditor] for an interactive interface for selecting columns to view.", Embeds: []types.Field{{Name: "WidgetBase"}}, Fields: []types.Field{{Name: "Scale", Doc: "Scale multiplies the plot DPI value, to change the overall scale\nof the rendered plot. Larger numbers produce larger scaling.\nTypically use larger numbers when generating plots for inclusion in\ndocuments or other cases where the overall plot size will be small."}, {Name: "Plot", Doc: "Plot is the Plot to display in this widget"}}})
// NewPlot returns a new [Plot] with the given optional parent:
// Plot is a widget that renders a [plot.Plot] object.
// If it is not [states.ReadOnly], the user can pan and zoom the graph.
// See [PlotEditor] for an interactive interface for selecting columns to view.
func NewPlot(parent ...tree.Node) *Plot { return tree.New[Plot](parent...) }
// SetScale sets the [Plot.Scale]:
// Scale multiplies the plot DPI value, to change the overall scale
// of the rendered plot. Larger numbers produce larger scaling.
// Typically use larger numbers when generating plots for inclusion in
// documents or other cases where the overall plot size will be small.
func (t *Plot) SetScale(v float32) *Plot { t.Scale = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/plot/plotcore.PlotEditor", IDName: "plot-editor", Doc: "PlotEditor is a widget that provides an interactive 2D plot\nof selected columns of tabular data, represented by a [table.IndexView] into\na [table.Table]. Other types of tabular data can be converted into this format.\nThe user can change various options for the plot and also modify the underlying data.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Methods: []types.Method{{Name: "SaveSVG", Doc: "SaveSVG saves the plot to an svg -- first updates to ensure that plot is current", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"fname"}}, {Name: "SavePNG", Doc: "SavePNG saves the current plot to a png, capturing current render", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"fname"}}, {Name: "SaveCSV", Doc: "SaveCSV saves the Table data to a csv (comma-separated values) file with headers (any delim)", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"fname", "delim"}}, {Name: "SaveAll", Doc: "SaveAll saves the current plot to a png, svg, and the data to a tsv -- full save\nAny extension is removed and appropriate extensions are added", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"fname"}}, {Name: "OpenCSV", Doc: "OpenCSV opens the Table data from a csv (comma-separated values) file (or any delim)", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename", "delim"}}, {Name: "setColumnsByName", Doc: "setColumnsByName turns columns on or off if their name contains\nthe given string.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"nameContains", "on"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "table", Doc: "table is the table of data being plotted."}, {Name: "Options", Doc: "Options are the overall plot options."}, {Name: "Columns", Doc: "Columns are the options for each column of the table."}, {Name: "plot", Doc: "plot is the plot object."}, {Name: "svgFile", Doc: "current svg file"}, {Name: "dataFile", Doc: "current csv data file"}, {Name: "inPlot", Doc: "currently doing a plot"}, {Name: "columnsFrame"}, {Name: "plotWidget"}}})
// NewPlotEditor returns a new [PlotEditor] with the given optional parent:
// PlotEditor is a widget that provides an interactive 2D plot
// of selected columns of tabular data, represented by a [table.IndexView] into
// a [table.Table]. Other types of tabular data can be converted into this format.
// The user can change various options for the plot and also modify the underlying data.
func NewPlotEditor(parent ...tree.Node) *PlotEditor { return tree.New[PlotEditor](parent...) }
// SetOptions sets the [PlotEditor.Options]:
// Options are the overall plot options.
func (t *PlotEditor) SetOptions(v PlotOptions) *PlotEditor { t.Options = v; return t }
// Copyright (c) 2024, 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 plotcore
import (
"fmt"
"log/slog"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/colors"
"cogentcore.org/core/plot"
"cogentcore.org/core/plot/plots"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/stats/split"
"cogentcore.org/core/tensor/table"
)
// genPlotXY generates an XY (lines, points) plot, setting Plot variable
func (pl *PlotEditor) genPlotXY() {
plt := plot.New()
// process xaxis first
xi, xview, err := pl.plotXAxis(plt, pl.table)
if err != nil {
return
}
xp := pl.Columns[xi]
var lsplit *table.Splits
nleg := 1
if pl.Options.Legend != "" {
_, err = pl.table.Table.ColumnIndex(pl.Options.Legend)
if err != nil {
slog.Error("plot.Legend", "err", err.Error())
} else {
errors.Log(xview.SortStableColumnNames([]string{pl.Options.Legend, xp.Column}, table.Ascending))
lsplit = split.GroupBy(xview, pl.Options.Legend)
nleg = max(lsplit.Len(), 1)
}
}
var firstXY *tableXY
var strCols []*ColumnOptions
nys := 0
for _, cp := range pl.Columns {
if !cp.On {
continue
}
if cp.IsString {
strCols = append(strCols, cp)
continue
}
if cp.TensorIndex < 0 {
yc := errors.Log1(pl.table.Table.ColumnByName(cp.Column))
_, sz := yc.RowCellSize()
nys += sz
} else {
nys++
}
}
if nys == 0 {
return
}
firstXY = nil
yidx := 0
for _, cp := range pl.Columns {
if !cp.On || cp == xp {
continue
}
if cp.IsString {
continue
}
for li := 0; li < nleg; li++ {
lview := xview
leg := ""
if lsplit != nil && len(lsplit.Values) > li {
leg = lsplit.Values[li][0]
lview = lsplit.Splits[li]
}
nidx := 1
stidx := cp.TensorIndex
if cp.TensorIndex < 0 { // do all
yc := errors.Log1(pl.table.Table.ColumnByName(cp.Column))
_, sz := yc.RowCellSize()
nidx = sz
stidx = 0
}
for ii := 0; ii < nidx; ii++ {
idx := stidx + ii
tix := lview.Clone()
xy, _ := newTableXYName(tix, xi, xp.TensorIndex, cp.Column, idx, cp.Range)
if xy == nil {
continue
}
if firstXY == nil {
firstXY = xy
}
var pts *plots.Scatter
var lns *plots.Line
lbl := cp.getLabel()
clr := cp.Color
if leg != "" {
lbl = leg + " " + lbl
}
if nleg > 1 {
cidx := yidx*nleg + li
clr = colors.Uniform(colors.Spaced(cidx))
}
if nidx > 1 {
clr = colors.Uniform(colors.Spaced(idx))
lbl = fmt.Sprintf("%s_%02d", lbl, idx)
}
if cp.Lines.Or(pl.Options.Lines) && cp.Points.Or(pl.Options.Points) {
lns, pts, _ = plots.NewLinePoints(xy)
} else if cp.Points.Or(pl.Options.Points) {
pts, _ = plots.NewScatter(xy)
} else {
lns, _ = plots.NewLine(xy)
}
if lns != nil {
lns.LineStyle.Width.Pt(float32(cp.LineWidth.Or(pl.Options.LineWidth)))
lns.LineStyle.Color = clr
lns.NegativeXDraw = pl.Options.NegativeXDraw
plt.Add(lns)
if pts != nil {
plt.Legend.Add(lbl, lns, pts)
} else {
plt.Legend.Add(lbl, lns)
}
}
if pts != nil {
pts.LineStyle.Color = clr
pts.LineStyle.Width.Pt(float32(cp.LineWidth.Or(pl.Options.LineWidth)))
pts.PointSize.Pt(float32(cp.PointSize.Or(pl.Options.PointSize)))
pts.PointShape = cp.PointShape.Or(pl.Options.PointShape)
plt.Add(pts)
if lns == nil {
plt.Legend.Add(lbl, pts)
}
}
if cp.ErrColumn != "" {
ec := errors.Log1(pl.table.Table.ColumnIndex(cp.ErrColumn))
if ec >= 0 {
xy.errColumn = ec
eb, _ := plots.NewYErrorBars(xy)
eb.LineStyle.Color = clr
plt.Add(eb)
}
}
}
}
yidx++
}
if firstXY != nil && len(strCols) > 0 {
for _, cp := range strCols {
xy, _ := newTableXY(xview, xi, xp.TensorIndex, firstXY.yColumn, cp.TensorIndex, firstXY.yRange)
xy.labelColumn, _ = xview.Table.ColumnIndex(cp.Column)
xy.yIndex = firstXY.yIndex
lbls, _ := plots.NewLabels(xy)
if lbls != nil {
plt.Add(lbls)
}
}
}
// Use string labels for X axis if X is a string
xc := pl.table.Table.Columns[xi]
if xc.IsString() {
xcs := xc.(*tensor.String)
vals := make([]string, pl.table.Len())
for i, dx := range pl.table.Indexes {
vals[i] = xcs.Values[dx]
}
plt.NominalX(vals...)
}
pl.configPlot(plt)
pl.plot = plt
}
// Copyright (c) 2019, Cogent Core. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This is copied and modified directly from gonum to add better error-bar
// plotting for bar plots, along with multiple groups.
// Copyright ©2015 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package plots
import (
"image"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/plot"
)
// A BarChart presents ordinally-organized data with rectangular bars
// with lengths proportional to the data values, and an optional
// error bar ("handle") at the top of the bar using given error value
// (single value, like a standard deviation etc, not drawn below the bar).
//
// Bars are plotted centered at integer multiples of Stride plus Start offset.
// Full data range also includes Pad value to extend range beyond edge bar centers.
// Bar Width is in data units, e.g., should be <= Stride.
// Defaults provide a unit-spaced plot.
type BarChart struct {
// Values are the plotted values
Values plot.Values
// YErrors is a copy of the Y errors for each point.
Errors plot.Values
// XYs is the actual pixel plotting coordinates for each value.
XYs plot.XYs
// PXYs is the actual pixel plotting coordinates for each value.
PXYs plot.XYs
// Offset is offset added to each X axis value relative to the
// Stride computed value (X = offset + index * Stride)
// Defaults to 1.
Offset float32
// Stride is distance between bars. Defaults to 1.
Stride float32
// Width is the width of the bars, which should be less than
// the Stride to prevent bar overlap.
// Defaults to .8
Width float32
// Pad is additional space at start / end of data range, to keep bars from
// overflowing ends. This amount is subtracted from Offset
// and added to (len(Values)-1)*Stride -- no other accommodation for bar
// width is provided, so that should be built into this value as well.
// Defaults to 1.
Pad float32
// Color is the fill color of the bars.
Color image.Image
// LineStyle is the style of the line connecting the points.
// Use zero width to disable lines.
LineStyle plot.LineStyle
// Horizontal dictates whether the bars should be in the vertical
// (default) or horizontal direction. If Horizontal is true, all
// X locations and distances referred to here will actually be Y
// locations and distances.
Horizontal bool
// stackedOn is the bar chart upon which this bar chart is stacked.
StackedOn *BarChart
}
// NewBarChart returns a new bar chart with a single bar for each value.
// The bars heights correspond to the values and their x locations correspond
// to the index of their value in the Valuer. Optional error-bar values can be
// provided.
func NewBarChart(vs, ers plot.Valuer) (*BarChart, error) {
values, err := plot.CopyValues(vs)
if err != nil {
return nil, err
}
var errs plot.Values
if ers != nil {
errs, err = plot.CopyValues(ers)
if err != nil {
return nil, err
}
}
b := &BarChart{
Values: values,
Errors: errs,
}
b.Defaults()
return b, nil
}
func (b *BarChart) Defaults() {
b.Offset = 1
b.Stride = 1
b.Width = .8
b.Pad = 1
b.Color = colors.Scheme.OnSurface
b.LineStyle.Defaults()
}
func (b *BarChart) XYData() (data plot.XYer, pixels plot.XYer) {
data = b.XYs
pixels = b.PXYs
return
}
// BarHeight returns the maximum y value of the
// ith bar, taking into account any bars upon
// which it is stacked.
func (b *BarChart) BarHeight(i int) float32 {
ht := float32(0.0)
if b == nil {
return 0
}
if i >= 0 && i < len(b.Values) {
ht += b.Values[i]
}
if b.StackedOn != nil {
ht += b.StackedOn.BarHeight(i)
}
return ht
}
// StackOn stacks a bar chart on top of another,
// and sets the bar positioning options to that of the
// chart upon which it is being stacked.
func (b *BarChart) StackOn(on *BarChart) {
b.Offset = on.Offset
b.Stride = on.Stride
b.Pad = on.Pad
b.StackedOn = on
}
// Plot implements the plot.Plotter interface.
func (b *BarChart) Plot(plt *plot.Plot) {
pc := plt.Paint
pc.FillStyle.Color = b.Color
b.LineStyle.SetStroke(plt)
nv := len(b.Values)
b.XYs = make(plot.XYs, nv)
b.PXYs = make(plot.XYs, nv)
hw := 0.5 * b.Width
ew := b.Width / 3
for i, ht := range b.Values {
cat := b.Offset + float32(i)*b.Stride
var bottom, catVal, catMin, catMax, valMin, valMax float32
var box math32.Box2
if b.Horizontal {
catVal = plt.PY(cat)
catMin = plt.PY(cat - hw)
catMax = plt.PY(cat + hw)
bottom = b.StackedOn.BarHeight(i) // nil safe
valMin = plt.PX(bottom)
valMax = plt.PX(bottom + ht)
b.XYs[i] = math32.Vec2(bottom+ht, cat)
b.PXYs[i] = math32.Vec2(valMax, catVal)
box.Min.Set(valMin, catMin)
box.Max.Set(valMax, catMax)
} else {
catVal = plt.PX(cat)
catMin = plt.PX(cat - hw)
catMax = plt.PX(cat + hw)
bottom = b.StackedOn.BarHeight(i) // nil safe
valMin = plt.PY(bottom)
valMax = plt.PY(bottom + ht)
b.XYs[i] = math32.Vec2(cat, bottom+ht)
b.PXYs[i] = math32.Vec2(catVal, valMax)
box.Min.Set(catMin, valMin)
box.Max.Set(catMax, valMax)
}
pc.DrawRectangle(box.Min.X, box.Min.Y, box.Size().X, box.Size().Y)
pc.FillStrokeClear()
if i < len(b.Errors) {
errval := b.Errors[i]
if b.Horizontal {
eVal := plt.PX(bottom + ht + math32.Abs(errval))
pc.MoveTo(valMax, catVal)
pc.LineTo(eVal, catVal)
pc.MoveTo(eVal, plt.PY(cat-ew))
pc.LineTo(eVal, plt.PY(cat+ew))
} else {
eVal := plt.PY(bottom + ht + math32.Abs(errval))
pc.MoveTo(catVal, valMax)
pc.LineTo(catVal, eVal)
pc.MoveTo(plt.PX(cat-ew), eVal)
pc.LineTo(plt.PX(cat+ew), eVal)
}
pc.Stroke()
}
}
}
// DataRange implements the plot.DataRanger interface.
func (b *BarChart) DataRange(plt *plot.Plot) (xmin, xmax, ymin, ymax float32) {
catMin := b.Offset - b.Pad
catMax := b.Offset + float32(len(b.Values)-1)*b.Stride + b.Pad
valMin := math32.Inf(1)
valMax := math32.Inf(-1)
for i, val := range b.Values {
valBot := b.StackedOn.BarHeight(i)
valTop := valBot + val
if i < len(b.Errors) {
valTop += math32.Abs(b.Errors[i])
}
valMin = math32.Min(valMin, math32.Min(valBot, valTop))
valMax = math32.Max(valMax, math32.Max(valBot, valTop))
}
if !b.Horizontal {
return catMin, catMax, valMin, valMax
}
return valMin, valMax, catMin, catMax
}
// Thumbnail fulfills the plot.Thumbnailer interface.
func (b *BarChart) Thumbnail(plt *plot.Plot) {
pc := plt.Paint
pc.FillStyle.Color = b.Color
b.LineStyle.SetStroke(plt)
ptb := pc.Bounds
pc.DrawRectangle(float32(ptb.Min.X), float32(ptb.Min.Y), float32(ptb.Size().X), float32(ptb.Size().Y))
pc.FillStrokeClear()
}
// Code generated by "core generate"; DO NOT EDIT.
package plots
import (
"cogentcore.org/core/enums"
)
var _StepKindValues = []StepKind{0, 1, 2, 3}
// StepKindN is the highest valid value for type StepKind, plus one.
const StepKindN StepKind = 4
var _StepKindValueMap = map[string]StepKind{`NoStep`: 0, `PreStep`: 1, `MidStep`: 2, `PostStep`: 3}
var _StepKindDescMap = map[StepKind]string{0: `NoStep connects two points by simple line`, 1: `PreStep connects two points by following lines: vertical, horizontal.`, 2: `MidStep connects two points by following lines: horizontal, vertical, horizontal. Vertical line is placed in the middle of the interval.`, 3: `PostStep connects two points by following lines: horizontal, vertical.`}
var _StepKindMap = map[StepKind]string{0: `NoStep`, 1: `PreStep`, 2: `MidStep`, 3: `PostStep`}
// String returns the string representation of this StepKind value.
func (i StepKind) String() string { return enums.String(i, _StepKindMap) }
// SetString sets the StepKind value from its string representation,
// and returns an error if the string is invalid.
func (i *StepKind) SetString(s string) error {
return enums.SetString(i, s, _StepKindValueMap, "StepKind")
}
// Int64 returns the StepKind value as an int64.
func (i StepKind) Int64() int64 { return int64(i) }
// SetInt64 sets the StepKind value from an int64.
func (i *StepKind) SetInt64(in int64) { *i = StepKind(in) }
// Desc returns the description of the StepKind value.
func (i StepKind) Desc() string { return enums.Desc(i, _StepKindDescMap) }
// StepKindValues returns all possible values for the type StepKind.
func StepKindValues() []StepKind { return _StepKindValues }
// Values returns all possible values for the type StepKind.
func (i StepKind) Values() []enums.Enum { return enums.Values(_StepKindValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i StepKind) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *StepKind) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "StepKind") }
var _ShapesValues = []Shapes{0, 1, 2, 3, 4, 5, 6, 7}
// ShapesN is the highest valid value for type Shapes, plus one.
const ShapesN Shapes = 8
var _ShapesValueMap = map[string]Shapes{`Ring`: 0, `Circle`: 1, `Square`: 2, `Box`: 3, `Triangle`: 4, `Pyramid`: 5, `Plus`: 6, `Cross`: 7}
var _ShapesDescMap = map[Shapes]string{0: `Ring is the outline of a circle`, 1: `Circle is a solid circle`, 2: `Square is the outline of a square`, 3: `Box is a filled square`, 4: `Triangle is the outline of a triangle`, 5: `Pyramid is a filled triangle`, 6: `Plus is a plus sign`, 7: `Cross is a big X`}
var _ShapesMap = map[Shapes]string{0: `Ring`, 1: `Circle`, 2: `Square`, 3: `Box`, 4: `Triangle`, 5: `Pyramid`, 6: `Plus`, 7: `Cross`}
// String returns the string representation of this Shapes value.
func (i Shapes) String() string { return enums.String(i, _ShapesMap) }
// SetString sets the Shapes value from its string representation,
// and returns an error if the string is invalid.
func (i *Shapes) SetString(s string) error { return enums.SetString(i, s, _ShapesValueMap, "Shapes") }
// Int64 returns the Shapes value as an int64.
func (i Shapes) Int64() int64 { return int64(i) }
// SetInt64 sets the Shapes value from an int64.
func (i *Shapes) SetInt64(in int64) { *i = Shapes(in) }
// Desc returns the description of the Shapes value.
func (i Shapes) Desc() string { return enums.Desc(i, _ShapesDescMap) }
// ShapesValues returns all possible values for the type Shapes.
func ShapesValues() []Shapes { return _ShapesValues }
// Values returns all possible values for the type Shapes.
func (i Shapes) Values() []enums.Enum { return enums.Values(_ShapesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Shapes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Shapes) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Shapes") }
// Copyright (c) 2024, 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 plots
import (
"cogentcore.org/core/math32"
"cogentcore.org/core/plot"
"cogentcore.org/core/styles/units"
)
//////////////////////////////////////////////////
// XErrorer
// XErrorer provides an interface for a list of Low, High error bar values.
// This is used in addition to an XYer interface, if implemented.
type XErrorer interface {
// XError returns Low, High error values for X data.
XError(i int) (low, high float32)
}
// Errors is a slice of low and high error values.
type Errors []struct{ Low, High float32 }
// XErrors implements the XErrorer interface.
type XErrors Errors
func (xe XErrors) XError(i int) (low, high float32) {
return xe[i].Low, xe[i].High
}
// YErrorer provides an interface for YError method.
// This is used in addition to an XYer interface, if implemented.
type YErrorer interface {
// YError returns two error values for Y data.
YError(i int) (float32, float32)
}
// YErrors implements the YErrorer interface.
type YErrors Errors
func (ye YErrors) YError(i int) (float32, float32) {
return ye[i].Low, ye[i].High
}
// YErrorBars implements the plot.Plotter, plot.DataRanger,
// and plot.GlyphBoxer interfaces, drawing vertical error
// bars, denoting error in Y values.
type YErrorBars struct {
// XYs is a copy of the points for this line.
plot.XYs
// YErrors is a copy of the Y errors for each point.
YErrors
// PXYs is the actual pixel plotting coordinates for each XY value,
// representing the high, center value of the error bar.
PXYs plot.XYs
// LineStyle is the style used to draw the error bars.
LineStyle plot.LineStyle
// CapWidth is the width of the caps drawn at the top of each error bar.
CapWidth units.Value
}
func (eb *YErrorBars) Defaults() {
eb.LineStyle.Defaults()
eb.CapWidth.Dp(10)
}
// NewYErrorBars returns a new YErrorBars plotter, or an error on failure.
// The error values from the YErrorer interface are interpreted as relative
// to the corresponding Y value. The errors for a given Y value are computed
// by taking the absolute value of the error returned by the YErrorer
// and subtracting the first and adding the second to the Y value.
func NewYErrorBars(yerrs interface {
plot.XYer
YErrorer
}) (*YErrorBars, error) {
errors := make(YErrors, yerrs.Len())
for i := range errors {
errors[i].Low, errors[i].High = yerrs.YError(i)
if err := plot.CheckFloats(errors[i].Low, errors[i].High); err != nil {
return nil, err
}
}
xys, err := plot.CopyXYs(yerrs)
if err != nil {
return nil, err
}
eb := &YErrorBars{
XYs: xys,
YErrors: errors,
}
eb.Defaults()
return eb, nil
}
func (e *YErrorBars) XYData() (data plot.XYer, pixels plot.XYer) {
data = e.XYs
pixels = e.PXYs
return
}
// Plot implements the Plotter interface, drawing labels.
func (e *YErrorBars) Plot(plt *plot.Plot) {
pc := plt.Paint
uc := &pc.UnitContext
e.CapWidth.ToDots(uc)
cw := 0.5 * e.CapWidth.Dots
nv := len(e.YErrors)
e.PXYs = make(plot.XYs, nv)
e.LineStyle.SetStroke(plt)
for i, err := range e.YErrors {
x := plt.PX(e.XYs[i].X)
ylow := plt.PY(e.XYs[i].Y - math32.Abs(err.Low))
yhigh := plt.PY(e.XYs[i].Y + math32.Abs(err.High))
e.PXYs[i].X = x
e.PXYs[i].Y = yhigh
pc.MoveTo(x, ylow)
pc.LineTo(x, yhigh)
pc.MoveTo(x-cw, ylow)
pc.LineTo(x+cw, ylow)
pc.MoveTo(x-cw, yhigh)
pc.LineTo(x+cw, yhigh)
pc.Stroke()
}
}
// DataRange implements the plot.DataRanger interface.
func (e *YErrorBars) DataRange(plt *plot.Plot) (xmin, xmax, ymin, ymax float32) {
xmin, xmax = plot.Range(plot.XValues{e})
ymin = math32.Inf(1)
ymax = math32.Inf(-1)
for i, err := range e.YErrors {
y := e.XYs[i].Y
ylow := y - math32.Abs(err.Low)
yhigh := y + math32.Abs(err.High)
ymin = math32.Min(math32.Min(math32.Min(ymin, y), ylow), yhigh)
ymax = math32.Max(math32.Max(math32.Max(ymax, y), ylow), yhigh)
}
return
}
// XErrorBars implements the plot.Plotter, plot.DataRanger,
// and plot.GlyphBoxer interfaces, drawing horizontal error
// bars, denoting error in Y values.
type XErrorBars struct {
// XYs is a copy of the points for this line.
plot.XYs
// XErrors is a copy of the X errors for each point.
XErrors
// PXYs is the actual pixel plotting coordinates for each XY value,
// representing the high, center value of the error bar.
PXYs plot.XYs
// LineStyle is the style used to draw the error bars.
LineStyle plot.LineStyle
// CapWidth is the width of the caps drawn at the top
// of each error bar.
CapWidth units.Value
}
// Returns a new XErrorBars plotter, or an error on failure. The error values
// from the XErrorer interface are interpreted as relative to the corresponding
// X value. The errors for a given X value are computed by taking the absolute
// value of the error returned by the XErrorer and subtracting the first and
// adding the second to the X value.
func NewXErrorBars(xerrs interface {
plot.XYer
XErrorer
}) (*XErrorBars, error) {
errors := make(XErrors, xerrs.Len())
for i := range errors {
errors[i].Low, errors[i].High = xerrs.XError(i)
if err := plot.CheckFloats(errors[i].Low, errors[i].High); err != nil {
return nil, err
}
}
xys, err := plot.CopyXYs(xerrs)
if err != nil {
return nil, err
}
eb := &XErrorBars{
XYs: xys,
XErrors: errors,
}
eb.Defaults()
return eb, nil
}
func (eb *XErrorBars) Defaults() {
eb.LineStyle.Defaults()
eb.CapWidth.Dp(10)
}
func (e *XErrorBars) XYData() (data plot.XYer, pixels plot.XYer) {
data = e.XYs
pixels = e.PXYs
return
}
// Plot implements the Plotter interface, drawing labels.
func (e *XErrorBars) Plot(plt *plot.Plot) {
pc := plt.Paint
uc := &pc.UnitContext
e.CapWidth.ToDots(uc)
cw := 0.5 * e.CapWidth.Dots
nv := len(e.XErrors)
e.PXYs = make(plot.XYs, nv)
e.LineStyle.SetStroke(plt)
for i, err := range e.XErrors {
y := plt.PY(e.XYs[i].Y)
xlow := plt.PX(e.XYs[i].X - math32.Abs(err.Low))
xhigh := plt.PX(e.XYs[i].X + math32.Abs(err.High))
e.PXYs[i].X = xhigh
e.PXYs[i].Y = y
pc.MoveTo(xlow, y)
pc.LineTo(xhigh, y)
pc.MoveTo(xlow, y-cw)
pc.LineTo(xlow, y+cw)
pc.MoveTo(xhigh, y-cw)
pc.LineTo(xhigh, y+cw)
pc.Stroke()
}
}
// DataRange implements the plot.DataRanger interface.
func (e *XErrorBars) DataRange(plt *plot.Plot) (xmin, xmax, ymin, ymax float32) {
ymin, ymax = plot.Range(plot.YValues{e})
xmin = math32.Inf(1)
xmax = math32.Inf(-1)
for i, err := range e.XErrors {
x := e.XYs[i].X
xlow := x - math32.Abs(err.Low)
xhigh := x + math32.Abs(err.High)
xmin = math32.Min(math32.Min(math32.Min(xmin, x), xlow), xhigh)
xmax = math32.Max(math32.Max(math32.Max(xmax, x), xlow), xhigh)
}
return
}
// Copyright (c) 2024, 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 plots
import (
"errors"
"image"
"cogentcore.org/core/math32"
"cogentcore.org/core/plot"
"cogentcore.org/core/styles/units"
)
// Labels implements the Plotter interface,
// drawing a set of labels at specified points.
type Labels struct {
// XYs is a copy of the points for labels
plot.XYs
// PXYs is the actual pixel plotting coordinates for each XY value.
PXYs plot.XYs
// Labels is the set of labels corresponding to each point.
Labels []string
// TextStyle is the style of the label text.
// Each label can have a different text style, but
// by default they share a common one (len = 1)
TextStyle []plot.TextStyle
// Offset is added directly to the final label location.
Offset units.XY
// plot size and number of TextStyle when styles last generated -- don't regen
styleSize image.Point
styleN int
}
// NewLabels returns a new Labels using defaults
func NewLabels(d XYLabeler) (*Labels, error) {
xys, err := plot.CopyXYs(d)
if err != nil {
return nil, err
}
if d.Len() != len(xys) {
return nil, errors.New("plotter: number of points does not match the number of labels")
}
strs := make([]string, d.Len())
for i := range strs {
strs[i] = d.Label(i)
}
styles := make([]plot.TextStyle, 1)
for i := range styles {
styles[i].Defaults()
}
return &Labels{
XYs: xys,
Labels: strs,
TextStyle: styles,
}, nil
}
func (l *Labels) XYData() (data plot.XYer, pixels plot.XYer) {
data = l.XYs
pixels = l.PXYs
return
}
// updateStyles updates the text styles and dots.
// returns true if custom styles are used per point
func (l *Labels) updateStyles(plt *plot.Plot) bool {
customStyles := len(l.TextStyle) == len(l.XYs)
if plt.Size == l.styleSize && len(l.TextStyle) == l.styleN {
return customStyles
}
l.styleSize = plt.Size
l.styleN = len(l.TextStyle)
pc := plt.Paint
uc := &pc.UnitContext
l.Offset.ToDots(uc)
for i := range l.TextStyle {
l.TextStyle[i].ToDots(uc)
}
return customStyles
}
// Plot implements the Plotter interface, drawing labels.
func (l *Labels) Plot(plt *plot.Plot) {
ps := plot.PlotXYs(plt, l.XYs)
customStyles := l.updateStyles(plt)
var ltxt plot.Text
for i, label := range l.Labels {
if label == "" {
continue
}
if customStyles {
ltxt.Style = l.TextStyle[i]
} else {
ltxt.Style = l.TextStyle[0]
}
ltxt.Text = label
ltxt.Config(plt)
tht := ltxt.PaintText.BBox.Size().Y
ltxt.Draw(plt, math32.Vec2(ps[i].X+l.Offset.X.Dots, ps[i].Y+l.Offset.Y.Dots-tht))
}
}
// DataRange returns the minimum and maximum X and Y values
func (l *Labels) DataRange(plt *plot.Plot) (xmin, xmax, ymin, ymax float32) {
xmin, xmax, ymin, ymax = plot.XYRange(l) // first get basic numerical range
pxToData := math32.FromPoint(plt.Size)
pxToData.X = (xmax - xmin) / pxToData.X
pxToData.Y = (ymax - ymin) / pxToData.Y
customStyles := l.updateStyles(plt)
var ltxt plot.Text
for i, label := range l.Labels {
if label == "" {
continue
}
if customStyles {
ltxt.Style = l.TextStyle[i]
} else {
ltxt.Style = l.TextStyle[0]
}
ltxt.Text = label
ltxt.Config(plt)
tht := pxToData.Y * ltxt.PaintText.BBox.Size().Y
twd := 1.1 * pxToData.X * ltxt.PaintText.BBox.Size().X
x, y := l.XY(i)
minx := x
maxx := x + pxToData.X*l.Offset.X.Dots + twd
miny := y
maxy := y + pxToData.Y*l.Offset.Y.Dots + tht // y is up here
xmin = min(xmin, minx)
xmax = max(xmax, maxx)
ymin = min(ymin, miny)
ymax = max(ymax, maxy)
}
return
}
// XYLabeler combines the [plot.XYer] and [plot.Labeler] types.
type XYLabeler interface {
plot.XYer
plot.Labeler
}
// XYLabels holds XY data with labels.
// The ith label corresponds to the ith XY.
type XYLabels struct {
plot.XYs
Labels []string
}
// Label returns the label for point index i.
func (l XYLabels) Label(i int) string {
return l.Labels[i]
}
var _ XYLabeler = (*XYLabels)(nil)
// Copyright (c) 2024, 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.
// Adapted from github.com/gonum/plot:
// Copyright ©2015 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package plots
//go:generate core generate
import (
"image"
"cogentcore.org/core/math32"
"cogentcore.org/core/plot"
)
// StepKind specifies a form of a connection of two consecutive points.
type StepKind int32 //enums:enum
const (
// NoStep connects two points by simple line
NoStep StepKind = iota
// PreStep connects two points by following lines: vertical, horizontal.
PreStep
// MidStep connects two points by following lines: horizontal, vertical, horizontal.
// Vertical line is placed in the middle of the interval.
MidStep
// PostStep connects two points by following lines: horizontal, vertical.
PostStep
)
// Line implements the Plotter interface, drawing a line using XYer data.
type Line struct {
// XYs is a copy of the points for this line.
plot.XYs
// PXYs is the actual pixel plotting coordinates for each XY value.
PXYs plot.XYs
// StepStyle is the kind of the step line.
StepStyle StepKind
// LineStyle is the style of the line connecting the points.
// Use zero width to disable lines.
LineStyle plot.LineStyle
// Fill is the color to fill the area below the plot.
// Use nil to disable filling, which is the default.
Fill image.Image
// if true, draw lines that connect points with a negative X-axis direction;
// otherwise there is a break in the line.
// default is false, so that repeated series of data across the X axis
// are plotted separately.
NegativeXDraw bool
}
// NewLine returns a Line that uses the default line style and
// does not draw glyphs.
func NewLine(xys plot.XYer) (*Line, error) {
data, err := plot.CopyXYs(xys)
if err != nil {
return nil, err
}
ln := &Line{XYs: data}
ln.Defaults()
return ln, nil
}
// NewLinePoints returns both a Line and a
// Scatter plot for the given point data.
func NewLinePoints(xys plot.XYer) (*Line, *Scatter, error) {
sc, err := NewScatter(xys)
if err != nil {
return nil, nil, err
}
ln := &Line{XYs: sc.XYs}
ln.Defaults()
return ln, sc, nil
}
func (pts *Line) Defaults() {
pts.LineStyle.Defaults()
}
func (pts *Line) XYData() (data plot.XYer, pixels plot.XYer) {
data = pts.XYs
pixels = pts.PXYs
return
}
// Plot draws the Line, implementing the plot.Plotter interface.
func (pts *Line) Plot(plt *plot.Plot) {
pc := plt.Paint
ps := plot.PlotXYs(plt, pts.XYs)
np := len(ps)
pts.PXYs = ps
if pts.Fill != nil {
pc.FillStyle.Color = pts.Fill
minY := plt.PY(plt.Y.Min)
prev := math32.Vec2(ps[0].X, minY)
pc.MoveTo(prev.X, prev.Y)
for i := range ps {
pt := ps[i]
switch pts.StepStyle {
case NoStep:
if pt.X < prev.X {
pc.LineTo(prev.X, minY)
pc.ClosePath()
pc.MoveTo(pt.X, minY)
}
pc.LineTo(pt.X, pt.Y)
case PreStep:
if i == 0 {
continue
}
if pt.X < prev.X {
pc.LineTo(prev.X, minY)
pc.ClosePath()
pc.MoveTo(pt.X, minY)
} else {
pc.LineTo(prev.X, pt.Y)
}
pc.LineTo(pt.X, pt.Y)
case MidStep:
if pt.X < prev.X {
pc.LineTo(prev.X, minY)
pc.ClosePath()
pc.MoveTo(pt.X, minY)
} else {
pc.LineTo(0.5*(prev.X+pt.X), prev.Y)
pc.LineTo(0.5*(prev.X+pt.X), pt.Y)
}
pc.LineTo(pt.X, pt.Y)
case PostStep:
if pt.X < prev.X {
pc.LineTo(prev.X, minY)
pc.ClosePath()
pc.MoveTo(pt.X, minY)
} else {
pc.LineTo(pt.X, prev.Y)
}
pc.LineTo(pt.X, pt.Y)
}
prev = pt
}
pc.LineTo(prev.X, minY)
pc.ClosePath()
pc.Fill()
}
pc.FillStyle.Color = nil
if !pts.LineStyle.SetStroke(plt) {
return
}
prev := ps[0]
pc.MoveTo(prev.X, prev.Y)
for i := 1; i < np; i++ {
pt := ps[i]
if pts.StepStyle != NoStep {
if pt.X >= prev.X {
switch pts.StepStyle {
case PreStep:
pc.LineTo(prev.X, pt.Y)
case MidStep:
pc.LineTo(0.5*(prev.X+pt.X), prev.Y)
pc.LineTo(0.5*(prev.X+pt.X), pt.Y)
case PostStep:
pc.LineTo(pt.X, prev.Y)
}
} else {
pc.MoveTo(pt.X, pt.Y)
}
}
if !pts.NegativeXDraw && pt.X < prev.X {
pc.MoveTo(pt.X, pt.Y)
} else {
pc.LineTo(pt.X, pt.Y)
}
prev = pt
}
pc.Stroke()
}
// DataRange returns the minimum and maximum
// x and y values, implementing the plot.DataRanger interface.
func (pts *Line) DataRange(plt *plot.Plot) (xmin, xmax, ymin, ymax float32) {
return plot.XYRange(pts)
}
// Thumbnail returns the thumbnail for the LineTo, implementing the plot.Thumbnailer interface.
func (pts *Line) Thumbnail(plt *plot.Plot) {
pc := plt.Paint
ptb := pc.Bounds
midY := 0.5 * float32(ptb.Min.Y+ptb.Max.Y)
if pts.Fill != nil {
tb := ptb
if pts.LineStyle.Width.Value > 0 {
tb.Min.Y = int(midY)
}
pc.FillBox(math32.FromPoint(tb.Min), math32.FromPoint(tb.Size()), pts.Fill)
}
if pts.LineStyle.SetStroke(plt) {
pc.MoveTo(float32(ptb.Min.X), midY)
pc.LineTo(float32(ptb.Max.X), midY)
pc.Stroke()
}
}
// Copyright (c) 2024, 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.
// Adapted from github.com/gonum/plot:
// Copyright ©2015 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package plots
import (
"cogentcore.org/core/math32"
"cogentcore.org/core/plot"
"cogentcore.org/core/styles/units"
)
// Scatter implements the Plotter interface, drawing
// a shape for each point.
type Scatter struct {
// XYs is a copy of the points for this scatter.
plot.XYs
// PXYs is the actual plotting coordinates for each XY value.
PXYs plot.XYs
// size of shape to draw for each point
PointSize units.Value
// shape to draw for each point
PointShape Shapes
// LineStyle is the style of the line connecting the points.
// Use zero width to disable lines.
LineStyle plot.LineStyle
}
// NewScatter returns a Scatter that uses the
// default glyph style.
func NewScatter(xys plot.XYer) (*Scatter, error) {
data, err := plot.CopyXYs(xys)
if err != nil {
return nil, err
}
sc := &Scatter{XYs: data}
sc.LineStyle.Defaults()
sc.PointSize.Pt(4)
return sc, nil
}
func (pts *Scatter) XYData() (data plot.XYer, pixels plot.XYer) {
data = pts.XYs
pixels = pts.PXYs
return
}
// Plot draws the Line, implementing the plot.Plotter interface.
func (pts *Scatter) Plot(plt *plot.Plot) {
pc := plt.Paint
if !pts.LineStyle.SetStroke(plt) {
return
}
pts.PointSize.ToDots(&pc.UnitContext)
pc.FillStyle.Color = pts.LineStyle.Color
ps := plot.PlotXYs(plt, pts.XYs)
for i := range ps {
pt := ps[i]
DrawShape(pc, math32.Vec2(pt.X, pt.Y), pts.PointSize.Dots, pts.PointShape)
}
pc.FillStyle.Color = nil
}
// DataRange returns the minimum and maximum
// x and y values, implementing the plot.DataRanger interface.
func (pts *Scatter) DataRange(plt *plot.Plot) (xmin, xmax, ymin, ymax float32) {
return plot.XYRange(pts)
}
// Thumbnail the thumbnail for the Scatter,
// implementing the plot.Thumbnailer interface.
func (pts *Scatter) Thumbnail(plt *plot.Plot) {
if !pts.LineStyle.SetStroke(plt) {
return
}
pc := plt.Paint
pts.PointSize.ToDots(&pc.UnitContext)
pc.FillStyle.Color = pts.LineStyle.Color
ptb := pc.Bounds
midX := 0.5 * float32(ptb.Min.X+ptb.Max.X)
midY := 0.5 * float32(ptb.Min.Y+ptb.Max.Y)
DrawShape(pc, math32.Vec2(midX, midY), pts.PointSize.Dots, pts.PointShape)
}
// Copyright (c) 2024, 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 plots
import (
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
)
type Shapes int32 //enums:enum
const (
// Ring is the outline of a circle
Ring Shapes = iota
// Circle is a solid circle
Circle
// Square is the outline of a square
Square
// Box is a filled square
Box
// Triangle is the outline of a triangle
Triangle
// Pyramid is a filled triangle
Pyramid
// Plus is a plus sign
Plus
// Cross is a big X
Cross
)
// DrawShape draws the given shape
func DrawShape(pc *paint.Context, pos math32.Vector2, size float32, shape Shapes) {
switch shape {
case Ring:
DrawRing(pc, pos, size)
case Circle:
DrawCircle(pc, pos, size)
case Square:
DrawSquare(pc, pos, size)
case Box:
DrawBox(pc, pos, size)
case Triangle:
DrawTriangle(pc, pos, size)
case Pyramid:
DrawPyramid(pc, pos, size)
case Plus:
DrawPlus(pc, pos, size)
case Cross:
DrawCross(pc, pos, size)
}
}
func DrawRing(pc *paint.Context, pos math32.Vector2, size float32) {
pc.DrawCircle(pos.X, pos.Y, size)
pc.Stroke()
}
func DrawCircle(pc *paint.Context, pos math32.Vector2, size float32) {
pc.DrawCircle(pos.X, pos.Y, size)
pc.FillStrokeClear()
}
func DrawSquare(pc *paint.Context, pos math32.Vector2, size float32) {
x := size * 0.9
pc.MoveTo(pos.X-x, pos.Y-x)
pc.LineTo(pos.X+x, pos.Y-x)
pc.LineTo(pos.X+x, pos.Y+x)
pc.LineTo(pos.X-x, pos.Y+x)
pc.ClosePath()
pc.Stroke()
}
func DrawBox(pc *paint.Context, pos math32.Vector2, size float32) {
x := size * 0.9
pc.MoveTo(pos.X-x, pos.Y-x)
pc.LineTo(pos.X+x, pos.Y-x)
pc.LineTo(pos.X+x, pos.Y+x)
pc.LineTo(pos.X-x, pos.Y+x)
pc.ClosePath()
pc.FillStrokeClear()
}
func DrawTriangle(pc *paint.Context, pos math32.Vector2, size float32) {
x := size * 0.9
pc.MoveTo(pos.X, pos.Y-x)
pc.LineTo(pos.X-x, pos.Y+x)
pc.LineTo(pos.X+x, pos.Y+x)
pc.ClosePath()
pc.Stroke()
}
func DrawPyramid(pc *paint.Context, pos math32.Vector2, size float32) {
x := size * 0.9
pc.MoveTo(pos.X, pos.Y-x)
pc.LineTo(pos.X-x, pos.Y+x)
pc.LineTo(pos.X+x, pos.Y+x)
pc.ClosePath()
pc.FillStrokeClear()
}
func DrawPlus(pc *paint.Context, pos math32.Vector2, size float32) {
x := size * 1.05
pc.MoveTo(pos.X-x, pos.Y)
pc.LineTo(pos.X+x, pos.Y)
pc.MoveTo(pos.X, pos.Y-x)
pc.LineTo(pos.X, pos.Y+x)
pc.ClosePath()
pc.Stroke()
}
func DrawCross(pc *paint.Context, pos math32.Vector2, size float32) {
x := size * 0.9
pc.MoveTo(pos.X-x, pos.Y-x)
pc.LineTo(pos.X+x, pos.Y+x)
pc.MoveTo(pos.X+x, pos.Y-x)
pc.LineTo(pos.X-x, pos.Y+x)
pc.ClosePath()
pc.Stroke()
}
// Copyright (c) 2024, 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 plots
import "cogentcore.org/core/plot"
// Table is an interface for tabular data for plotting,
// with columns of values.
type Table interface {
// number of columns of data
NumColumns() int
// name of given column
ColumnName(i int) string
// number of rows of data
NumRows() int
// PlotData returns the data value at given column and row
PlotData(column, row int) float32
}
func TableColumnIndex(tab Table, name string) int {
for i := range tab.NumColumns() {
if tab.ColumnName(i) == name {
return i
}
}
return -1
}
// TableXYer is an interface for providing XY access to Table data
type TableXYer struct {
Table Table
// the indexes of the tensor columns to use for the X and Y data, respectively
XColumn, YColumn int
}
func NewTableXYer(tab Table, xcolumn, ycolumn int) *TableXYer {
txy := &TableXYer{Table: tab, XColumn: xcolumn, YColumn: ycolumn}
return txy
}
func (dt *TableXYer) Len() int {
return dt.Table.NumRows()
}
func (dt *TableXYer) XY(i int) (x, y float32) {
return dt.Table.PlotData(dt.XColumn, i), dt.Table.PlotData(dt.YColumn, i)
}
// AddTableLine adds Line with given x, y columns from given tabular data
func AddTableLine(plt *plot.Plot, tab Table, xcolumn, ycolumn int) (*Line, error) {
txy := NewTableXYer(tab, xcolumn, ycolumn)
ln, err := NewLine(txy)
if err != nil {
return nil, err
}
plt.Add(ln)
return ln, nil
}
// AddTableLinePoints adds Line w/ Points with given x, y columns from given tabular data
func AddTableLinePoints(plt *plot.Plot, tab Table, xcolumn, ycolumn int) (*Line, *Scatter, error) {
txy := &TableXYer{Table: tab, XColumn: xcolumn, YColumn: ycolumn}
ln, sc, err := NewLinePoints(txy)
if err != nil {
return nil, nil, err
}
plt.Add(ln)
plt.Add(sc)
return ln, sc, nil
}
// Copyright (c) 2024, 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 plot
import (
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
)
// DefaultFontFamily specifies a default font for plotting.
// if not set, the standard Cogent Core default font is used.
var DefaultFontFamily = ""
// TextStyle specifies styling parameters for Text elements
type TextStyle struct {
styles.FontRender
// how to align text along the relevant dimension for the text element
Align styles.Aligns
// Padding is used in a case-dependent manner to add space around text elements
Padding units.Value
// rotation of the text, in Degrees
Rotation float32
}
func (ts *TextStyle) Defaults() {
ts.FontRender.Defaults()
ts.Color = colors.Scheme.OnSurface
ts.Align = styles.Center
if DefaultFontFamily != "" {
ts.FontRender.Family = DefaultFontFamily
}
}
func (ts *TextStyle) openFont(pt *Plot) {
if ts.Font.Face == nil {
paint.OpenFont(&ts.FontRender, &pt.Paint.UnitContext) // calls SetUnContext after updating metrics
}
}
func (ts *TextStyle) ToDots(uc *units.Context) {
ts.FontRender.ToDots(uc)
ts.Padding.ToDots(uc)
}
// Text specifies a single text element in a plot
type Text struct {
// text string, which can use HTML formatting
Text string
// styling for this text element
Style TextStyle
// PaintText is the [paint.Text] for the text.
PaintText paint.Text
}
func (tx *Text) Defaults() {
tx.Style.Defaults()
}
// config is called during the layout of the plot, prior to drawing
func (tx *Text) Config(pt *Plot) {
uc := &pt.Paint.UnitContext
fs := &tx.Style.FontRender
if math32.Abs(tx.Style.Rotation) > 10 {
tx.Style.Align = styles.End
}
fs.ToDots(uc)
tx.Style.Padding.ToDots(uc)
txln := float32(len(tx.Text))
fht := fs.Size.Dots
hsz := float32(12) * txln
txs := &pt.StandardTextStyle
tx.PaintText.SetHTML(tx.Text, fs, txs, uc, nil)
tx.PaintText.Layout(txs, fs, uc, math32.Vector2{X: hsz, Y: fht})
if tx.Style.Rotation != 0 {
rotx := math32.Rotate2D(math32.DegToRad(tx.Style.Rotation))
tx.PaintText.Transform(rotx, fs, uc)
}
}
// PosX returns the starting position for a horizontally-aligned text element,
// based on given width. Text must have been config'd already.
func (tx *Text) PosX(width float32) math32.Vector2 {
pos := math32.Vector2{}
pos.X = styles.AlignFactor(tx.Style.Align) * width
switch tx.Style.Align {
case styles.Center:
pos.X -= 0.5 * tx.PaintText.BBox.Size().X
case styles.End:
pos.X -= tx.PaintText.BBox.Size().X
}
if math32.Abs(tx.Style.Rotation) > 10 {
pos.Y += 0.5 * tx.PaintText.BBox.Size().Y
}
return pos
}
// PosY returns the starting position for a vertically-rotated text element,
// based on given height. Text must have been config'd already.
func (tx *Text) PosY(height float32) math32.Vector2 {
pos := math32.Vector2{}
pos.Y = styles.AlignFactor(tx.Style.Align) * height
switch tx.Style.Align {
case styles.Center:
pos.Y -= 0.5 * tx.PaintText.BBox.Size().Y
case styles.End:
pos.Y -= tx.PaintText.BBox.Size().Y
}
return pos
}
// Draw renders the text at given upper left position
func (tx *Text) Draw(pt *Plot, pos math32.Vector2) {
tx.PaintText.Render(pt.Paint, pos)
}
// Copyright (c) 2024, 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 plot
import (
"strconv"
"time"
"cogentcore.org/core/math32"
)
// A Tick is a single tick mark on an axis.
type Tick struct {
// Value is the data value marked by this Tick.
Value float32
// Label is the text to display at the tick mark.
// If Label is an empty string then this is a minor tick mark.
Label string
}
// IsMinor returns true if this is a minor tick mark.
func (tk *Tick) IsMinor() bool {
return tk.Label == ""
}
// Ticker creates Ticks in a specified range
type Ticker interface {
// Ticks returns Ticks in a specified range
Ticks(min, max float32) []Tick
}
// DefaultTicks is suitable for the Ticker field of an Axis,
// it returns a reasonable default set of tick marks.
type DefaultTicks struct{}
var _ Ticker = DefaultTicks{}
// Ticks returns Ticks in the specified range.
func (DefaultTicks) Ticks(min, max float32) []Tick {
if max <= min {
panic("illegal range")
}
const suggestedTicks = 3
labels, step, q, mag := talbotLinHanrahan(min, max, suggestedTicks, withinData, nil, nil, nil)
majorDelta := step * math32.Pow10(mag)
if q == 0 {
// Simple fall back was chosen, so
// majorDelta is the label distance.
majorDelta = labels[1] - labels[0]
}
// Choose a reasonable, but ad
// hoc formatting for labels.
fc := byte('f')
var off int
if mag < -1 || 6 < mag {
off = 1
fc = 'g'
}
if math32.Trunc(q) != q {
off += 2
}
prec := minInt(6, maxInt(off, -mag))
ticks := make([]Tick, len(labels))
for i, v := range labels {
ticks[i] = Tick{Value: v, Label: strconv.FormatFloat(float64(v), fc, prec, 32)}
}
var minorDelta float32
// See talbotLinHanrahan for the values used here.
switch step {
case 1, 2.5:
minorDelta = majorDelta / 5
case 2, 3, 4, 5:
minorDelta = majorDelta / step
default:
if majorDelta/2 < dlamchP {
return ticks
}
minorDelta = majorDelta / 2
}
// Find the first minor tick not greater
// than the lowest data value.
var i float32
for labels[0]+(i-1)*minorDelta > min {
i--
}
// Add ticks at minorDelta intervals when
// they are not within minorDelta/2 of a
// labelled tick.
for {
val := labels[0] + i*minorDelta
if val > max {
break
}
found := false
for _, t := range ticks {
if math32.Abs(t.Value-val) < minorDelta/2 {
found = true
}
}
if !found {
ticks = append(ticks, Tick{Value: val})
}
i++
}
return ticks
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
// LogTicks is suitable for the Ticker field of an Axis,
// it returns tick marks suitable for a log-scale axis.
type LogTicks struct {
// Prec specifies the precision of tick rendering
// according to the documentation for strconv.FormatFloat.
Prec int
}
var _ Ticker = LogTicks{}
// Ticks returns Ticks in a specified range
func (t LogTicks) Ticks(min, max float32) []Tick {
if min <= 0 || max <= 0 {
panic("Values must be greater than 0 for a log scale.")
}
val := math32.Pow10(int(math32.Log10(min)))
max = math32.Pow10(int(math32.Ceil(math32.Log10(max))))
var ticks []Tick
for val < max {
for i := 1; i < 10; i++ {
if i == 1 {
ticks = append(ticks, Tick{Value: val, Label: formatFloatTick(val, t.Prec)})
}
ticks = append(ticks, Tick{Value: val * float32(i)})
}
val *= 10
}
ticks = append(ticks, Tick{Value: val, Label: formatFloatTick(val, t.Prec)})
return ticks
}
// ConstantTicks is suitable for the Ticker field of an Axis.
// This function returns the given set of ticks.
type ConstantTicks []Tick
var _ Ticker = ConstantTicks{}
// Ticks returns Ticks in a specified range
func (ts ConstantTicks) Ticks(float32, float32) []Tick {
return ts
}
// UnixTimeIn returns a time conversion function for the given location.
func UnixTimeIn(loc *time.Location) func(t float32) time.Time {
return func(t float32) time.Time {
return time.Unix(int64(t), 0).In(loc)
}
}
// UTCUnixTime is the default time conversion for TimeTicks.
var UTCUnixTime = UnixTimeIn(time.UTC)
// TimeTicks is suitable for axes representing time values.
type TimeTicks struct {
// Ticker is used to generate a set of ticks.
// If nil, DefaultTicks will be used.
Ticker Ticker
// Format is the textual representation of the time value.
// If empty, time.RFC3339 will be used
Format string
// Time takes a float32 value and converts it into a time.Time.
// If nil, UTCUnixTime is used.
Time func(t float32) time.Time
}
var _ Ticker = TimeTicks{}
// Ticks implements plot.Ticker.
func (t TimeTicks) Ticks(min, max float32) []Tick {
if t.Ticker == nil {
t.Ticker = DefaultTicks{}
}
if t.Format == "" {
t.Format = time.RFC3339
}
if t.Time == nil {
t.Time = UTCUnixTime
}
ticks := t.Ticker.Ticks(min, max)
for i := range ticks {
tick := &ticks[i]
if tick.Label == "" {
continue
}
tick.Label = t.Time(tick.Value).Format(t.Format)
}
return ticks
}
/*
// lengthOffset returns an offset that should be added to the
// tick mark's line to accout for its length. I.e., the start of
// the line for a minor tick mark must be shifted by half of
// the length.
func (t Tick) lengthOffset(len vg.Length) vg.Length {
if t.IsMinor() {
return len / 2
}
return 0
}
// tickLabelHeight returns height of the tick mark labels.
func tickLabelHeight(sty text.Style, ticks []Tick) vg.Length {
maxHeight := vg.Length(0)
for _, t := range ticks {
if t.IsMinor() {
continue
}
r := sty.Rectangle(t.Label)
h := r.Max.Y - r.Min.Y
if h > maxHeight {
maxHeight = h
}
}
return maxHeight
}
// tickLabelWidth returns the width of the widest tick mark label.
func tickLabelWidth(sty text.Style, ticks []Tick) vg.Length {
maxWidth := vg.Length(0)
for _, t := range ticks {
if t.IsMinor() {
continue
}
r := sty.Rectangle(t.Label)
w := r.Max.X - r.Min.X
if w > maxWidth {
maxWidth = w
}
}
return maxWidth
}
*/
// formatFloatTick returns a g-formated string representation of v
// to the specified precision.
func formatFloatTick(v float32, prec int) string {
return strconv.FormatFloat(float64(v), 'g', prec, 32)
}
// TickerFunc is suitable for the Ticker field of an Axis.
// It is an adapter which allows to quickly setup a Ticker using a function with an appropriate signature.
type TickerFunc func(min, max float32) []Tick
var _ Ticker = TickerFunc(nil)
// Ticks implements plot.Ticker.
func (f TickerFunc) Ticks(min, max float32) []Tick {
return f(min, max)
}
// Copyright (c) 2024, 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 shell
import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"strconv"
"strings"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/base/logx"
"cogentcore.org/core/base/sshclient"
"github.com/mitchellh/go-homedir"
)
// InstallBuiltins adds the builtin shell commands to [Shell.Builtins].
func (sh *Shell) InstallBuiltins() {
sh.Builtins = make(map[string]func(cmdIO *exec.CmdIO, args ...string) error)
sh.Builtins["cd"] = sh.Cd
sh.Builtins["exit"] = sh.Exit
sh.Builtins["jobs"] = sh.JobsCmd
sh.Builtins["kill"] = sh.Kill
sh.Builtins["set"] = sh.Set
sh.Builtins["add-path"] = sh.AddPath
sh.Builtins["which"] = sh.Which
sh.Builtins["source"] = sh.Source
sh.Builtins["cossh"] = sh.CoSSH
sh.Builtins["scp"] = sh.Scp
sh.Builtins["debug"] = sh.Debug
sh.Builtins["history"] = sh.History
}
// Cd changes the current directory.
func (sh *Shell) Cd(cmdIO *exec.CmdIO, args ...string) error {
if len(args) > 1 {
return fmt.Errorf("no more than one argument can be passed to cd")
}
dir := ""
if len(args) == 1 {
dir = args[0]
}
dir, err := homedir.Expand(dir)
if err != nil {
return err
}
if dir == "" {
dir, err = homedir.Dir()
if err != nil {
return err
}
}
dir, err = filepath.Abs(dir)
if err != nil {
return err
}
err = os.Chdir(dir)
if err != nil {
return err
}
sh.Config.Dir = dir
return nil
}
// Exit exits the shell.
func (sh *Shell) Exit(cmdIO *exec.CmdIO, args ...string) error {
os.Exit(0)
return nil
}
// Set sets the given environment variable to the given value.
func (sh *Shell) Set(cmdIO *exec.CmdIO, args ...string) error {
if len(args) != 2 {
return fmt.Errorf("expected two arguments, got %d", len(args))
}
return os.Setenv(args[0], args[1])
}
// JobsCmd is the builtin jobs command
func (sh *Shell) JobsCmd(cmdIO *exec.CmdIO, args ...string) error {
for i, jb := range sh.Jobs {
cmdIO.Printf("[%d] %s\n", i+1, jb.String())
}
return nil
}
// Kill kills a job by job number or PID.
// Just expands the job id expressions %n into PIDs and calls system kill.
func (sh *Shell) Kill(cmdIO *exec.CmdIO, args ...string) error {
if len(args) == 0 {
return fmt.Errorf("cosh kill: expected at least one argument")
}
sh.JobIDExpand(args)
sh.Config.RunIO(cmdIO, "kill", args...)
return nil
}
// Fg foregrounds a job by job number
func (sh *Shell) Fg(cmdIO *exec.CmdIO, args ...string) error {
if len(args) != 1 {
return fmt.Errorf("cosh fg: requires exactly one job id argument")
}
jid := args[0]
exp := sh.JobIDExpand(args)
if exp != 1 {
return fmt.Errorf("cosh fg: argument was not a job id in the form %%n")
}
jno, _ := strconv.Atoi(jid[1:]) // guaranteed good
job := sh.Jobs[jno]
cmdIO.Printf("foregrounding job [%d]\n", jno)
_ = job
// todo: the problem here is we need to change the stdio for running job
// job.Cmd.Wait() // wait
// * probably need to have wrapper StdIO for every exec so we can flexibly redirect for fg, bg commands.
// * likewise, need to run everything effectively as a bg job with our own explicit Wait, which we can then communicate with to move from fg to bg.
return nil
}
// AddPath adds the given path(s) to $PATH.
func (sh *Shell) AddPath(cmdIO *exec.CmdIO, args ...string) error {
if len(args) == 0 {
return fmt.Errorf("cosh add-path expected at least one argument")
}
path := os.Getenv("PATH")
for _, arg := range args {
arg, err := homedir.Expand(arg)
if err != nil {
return err
}
path = path + ":" + arg
}
return os.Setenv("PATH", path)
}
// Which reports the executable associated with the given command.
// Processes builtins and commands, and if not found, then passes on
// to exec which.
func (sh *Shell) Which(cmdIO *exec.CmdIO, args ...string) error {
if len(args) != 1 {
return fmt.Errorf("cosh which: requires one argument")
}
cmd := args[0]
if _, hasCmd := sh.Commands[cmd]; hasCmd {
cmdIO.Println(cmd, "is a user-defined command")
return nil
}
if _, hasBlt := sh.Builtins[cmd]; hasBlt {
cmdIO.Println(cmd, "is a cosh builtin command")
return nil
}
sh.Config.RunIO(cmdIO, "which", args...)
return nil
}
// Source loads and evaluates the given file(s)
func (sh *Shell) Source(cmdIO *exec.CmdIO, args ...string) error {
if len(args) == 0 {
return fmt.Errorf("cosh source: requires at least one argument")
}
for _, fn := range args {
sh.TranspileCodeFromFile(fn)
}
// note that we do not execute the file -- just loads it in
return nil
}
// CoSSH manages SSH connections, which are referenced by the @name
// identifier. It handles the following cases:
// - @name -- switches to using given host for all subsequent commands
// - host [name] -- connects to a server specified in first arg and switches
// to using it, with optional name instead of default sequential number.
// - close -- closes all open connections, or the specified one
func (sh *Shell) CoSSH(cmdIO *exec.CmdIO, args ...string) error {
if len(args) < 1 {
return fmt.Errorf("cossh: requires at least one argument")
}
cmd := args[0]
var err error
host := ""
name := fmt.Sprintf("%d", 1+len(sh.SSHClients))
con := false
switch {
case cmd == "close":
sh.CloseSSH()
return nil
case cmd == "@" && len(args) == 2:
name = args[1]
case len(args) == 2:
con = true
host = args[0]
name = args[1]
default:
con = true
host = args[0]
}
if con {
cl := sshclient.NewClient(sh.SSH)
err = cl.Connect(host)
if err != nil {
return err
}
sh.SSHClients[name] = cl
sh.SSHActive = name
} else {
if name == "0" {
sh.SSHActive = ""
} else {
sh.SSHActive = name
cl := sh.ActiveSSH()
if cl == nil {
err = fmt.Errorf("cosh: ssh connection named: %q not found", name)
}
}
}
return err
}
// Scp performs file copy over SSH connection, with the remote filename
// prefixed with the @name: and the local filename un-prefixed.
// The order is from -> to, as in standard cp.
// The remote filename is automatically relative to the current working
// directory on the remote host.
func (sh *Shell) Scp(cmdIO *exec.CmdIO, args ...string) error {
if len(args) != 2 {
return fmt.Errorf("scp: requires exactly two arguments")
}
var lfn, hfn string
toHost := false
if args[0][0] == '@' {
hfn = args[0]
lfn = args[1]
} else if args[1][0] == '@' {
hfn = args[1]
lfn = args[0]
toHost = true
} else {
return fmt.Errorf("scp: one of the files must a remote host filename, specified by @name:")
}
ci := strings.Index(hfn, ":")
if ci < 0 {
return fmt.Errorf("scp: remote host filename does not contain a : after the host name")
}
host := hfn[1:ci]
hfn = hfn[ci+1:]
cl, err := sh.SSHByHost(host)
if err != nil {
return err
}
ctx := sh.Ctx
if ctx == nil {
ctx = context.Background()
}
if toHost {
err = cl.CopyLocalFileToHost(ctx, lfn, hfn)
} else {
err = cl.CopyHostToLocalFile(ctx, hfn, lfn)
}
return err
}
// Debug changes log level
func (sh *Shell) Debug(cmdIO *exec.CmdIO, args ...string) error {
if len(args) == 0 {
if logx.UserLevel == slog.LevelDebug {
logx.UserLevel = slog.LevelInfo
} else {
logx.UserLevel = slog.LevelDebug
}
}
if len(args) == 1 {
lev := args[0]
if lev == "on" || lev == "true" || lev == "1" {
logx.UserLevel = slog.LevelDebug
} else {
logx.UserLevel = slog.LevelInfo
}
}
return nil
}
// History shows history
func (sh *Shell) History(cmdIO *exec.CmdIO, args ...string) error {
n := len(sh.Hist)
nh := n
if len(args) == 1 {
an, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("history: error parsing number of history items: %q, error: %s", args[0], err.Error())
}
nh = min(n, an)
} else if len(args) > 1 {
return fmt.Errorf("history: uses at most one argument")
}
for i := n - nh; i < n; i++ {
cmdIO.Printf("%d:\t%s\n", i, sh.Hist[i])
}
return nil
}
// Copyright (c) 2024, 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.
// Command cosh is an interactive cli for running and compiling Cogent Shell (cosh).
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/cli"
"cogentcore.org/core/shell"
"cogentcore.org/core/shell/interpreter"
"github.com/cogentcore/yaegi/interp"
)
//go:generate core generate -add-types -add-funcs
// Config is the configuration information for the cosh cli.
type Config struct {
// Input is the input file to run/compile.
// If this is provided as the first argument,
// then the program will exit after running,
// unless the Interactive mode is flagged.
Input string `posarg:"0" required:"-"`
// Expr is an optional expression to evaluate, which can be used
// in addition to the Input file to run, to execute commands
// defined within that file for example, or as a command to run
// prior to starting interactive mode if no Input is specified.
Expr string `flag:"e,expr"`
// Args is an optional list of arguments to pass in the run command.
// These arguments will be turned into an "args" local variable in the shell.
// These are automatically processed from any leftover arguments passed, so
// you should not need to specify this flag manually.
Args []string `cmd:"run" posarg:"leftover" required:"-"`
// Interactive runs the interactive command line after processing any input file.
// Interactive mode is the default mode for the run command unless an input file
// is specified.
Interactive bool `cmd:"run" flag:"i,interactive"`
}
func main() { //types:skip
opts := cli.DefaultOptions("cosh", "An interactive tool for running and compiling Cogent Shell (cosh).")
cli.Run(opts, &Config{}, Run, Build)
}
// Run runs the specified cosh file. If no file is specified,
// it runs an interactive shell that allows the user to input cosh.
func Run(c *Config) error { //cli:cmd -root
in := interpreter.NewInterpreter(interp.Options{})
in.Config()
if len(c.Args) > 0 {
in.Eval("args := cosh.StringsToAnys(" + fmt.Sprintf("%#v)", c.Args))
}
if c.Input == "" {
return Interactive(c, in)
}
code := ""
if errors.Log1(fsx.FileExists(c.Input)) {
b, err := os.ReadFile(c.Input)
if err != nil && c.Expr == "" {
return err
}
code = string(b)
}
if c.Expr != "" {
if code != "" {
code += "\n"
}
code += c.Expr + "\n"
}
_, _, err := in.Eval(code)
if err == nil {
err = in.Shell.DepthError()
}
if c.Interactive {
return Interactive(c, in)
}
return err
}
// Interactive runs an interactive shell that allows the user to input cosh.
func Interactive(c *Config, in *interpreter.Interpreter) error {
if c.Expr != "" {
in.Eval(c.Expr)
}
in.Interactive()
return nil
}
// Build builds the specified input cosh file, or all .cosh files in the current
// directory if no input is specified, to corresponding .go file name(s).
// If the file does not already contain a "package" specification, then
// "package main; func main()..." wrappers are added, which allows the same
// code to be used in interactive and Go compiled modes.
func Build(c *Config) error {
var fns []string
if c.Input != "" {
fns = []string{c.Input}
} else {
fns = fsx.Filenames(".", ".cosh")
}
var errs []error
for _, fn := range fns {
ofn := strings.TrimSuffix(fn, filepath.Ext(fn)) + ".go"
err := shell.NewShell().TranspileFile(fn, ofn)
if err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
// Copyright (c) 2024, 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 shell
import (
"os"
"path/filepath"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/icons"
"cogentcore.org/core/parse/complete"
"github.com/mitchellh/go-homedir"
)
// CompleteMatch is the [complete.MatchFunc] for the shell.
func (sh *Shell) CompleteMatch(data any, text string, posLine, posChar int) (md complete.Matches) {
comps := complete.Completions{}
text = text[:posChar]
md.Seed = complete.SeedPath(text)
fullPath := complete.SeedSpace(text)
fullPath = errors.Log1(homedir.Expand(fullPath))
parent := strings.TrimSuffix(fullPath, md.Seed)
dir := filepath.Join(sh.Config.Dir, parent)
if filepath.IsAbs(parent) {
dir = parent
}
entries := errors.Log1(os.ReadDir(dir))
for _, entry := range entries {
icon := icons.File
if entry.IsDir() {
icon = icons.Folder
}
name := strings.ReplaceAll(entry.Name(), " ", `\ `) // escape spaces
comps = append(comps, complete.Completion{
Text: name,
Icon: icon,
Desc: filepath.Join(sh.Config.Dir, name),
})
}
if parent == "" {
for cmd := range sh.Builtins {
comps = append(comps, complete.Completion{
Text: cmd,
Icon: icons.Terminal,
Desc: "Builtin command: " + cmd,
})
}
for cmd := range sh.Commands {
comps = append(comps, complete.Completion{
Text: cmd,
Icon: icons.Terminal,
Desc: "Command: " + cmd,
})
}
// todo: write something that looks up all files on path -- should cache that per
// path string setting
}
md.Matches = complete.MatchSeedCompletion(comps, md.Seed)
return md
}
// CompleteEdit is the [complete.EditFunc] for the shell.
func (sh *Shell) CompleteEdit(data any, text string, cursorPos int, completion complete.Completion, seed string) (ed complete.Edit) {
return complete.EditWord(text, cursorPos, completion.Text, seed)
}
// ReadlineCompleter implements [github.com/ergochat/readline.AutoCompleter].
type ReadlineCompleter struct {
Shell *Shell
}
func (rc *ReadlineCompleter) Do(line []rune, pos int) (newLine [][]rune, length int) {
text := string(line)
md := rc.Shell.CompleteMatch(nil, text, 0, pos)
res := [][]rune{}
for _, match := range md.Matches {
after := strings.TrimPrefix(match.Text, md.Seed)
if md.Seed != "" && after == match.Text {
continue // no overlap
}
if match.Icon == icons.Folder {
after += string(filepath.Separator)
} else {
after += " "
}
res = append(res, []rune(after))
}
return res, len(md.Seed)
}
// Copyright (c) 2024, 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 cosh defines convenient utility functions for
// use in the cosh shell, available with the cosh prefix.
package cosh
import (
"io/fs"
"os"
"path/filepath"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/base/stringsx"
)
// SplitLines returns a slice of given string split by lines
// with any extra whitespace trimmed for each line entry.
func SplitLines(s string) []string {
sl := stringsx.SplitLines(s)
for i, s := range sl {
sl[i] = strings.TrimSpace(s)
}
return sl
}
// FileExists returns true if given file exists
func FileExists(path string) bool {
ex := errors.Log1(fsx.FileExists(path))
return ex
}
// WriteFile writes string to given file with standard permissions,
// logging any errors.
func WriteFile(filename, str string) error {
err := os.WriteFile(filename, []byte(str), 0666)
if err != nil {
errors.Log(err)
}
return err
}
// ReadFile reads the string from the given file, logging any errors.
func ReadFile(filename string) string {
str, err := os.ReadFile(filename)
if err != nil {
errors.Log(err)
}
return string(str)
}
// ReplaceInFile replaces all occurrences of given string with replacement
// in given file, rewriting the file. Also returns the updated string.
func ReplaceInFile(filename, old, new string) string {
str := ReadFile(filename)
str = strings.ReplaceAll(str, old, new)
WriteFile(filename, str)
return str
}
// StringsToAnys converts a slice of strings to a slice of any,
// using slicesx.ToAny. The interpreter cannot process generics
// yet, so this wrapper is needed. Use for passing args to
// a command, for example.
func StringsToAnys(s []string) []any {
return slicesx.As[string, any](s)
}
// AllFiles returns a list of all files (excluding directories)
// under the given path.
func AllFiles(path string) []string {
var files []string
filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
files = append(files, path)
return nil
})
return files
}
// Copyright (c) 2024, 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 shell
import (
"bytes"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/base/sshclient"
"github.com/mitchellh/go-homedir"
)
// Exec handles command execution for all cases, parameterized by the args.
// It executes the given command string, waiting for the command to finish,
// handling the given arguments appropriately.
// If there is any error, it adds it to the shell, and triggers CancelExecution.
// - errOk = don't call AddError so execution will not stop on error
// - start = calls Start on the command, which then runs asynchronously, with
// a goroutine forked to Wait for it and close its IO
// - output = return the output of the command as a string (otherwise return is "")
func (sh *Shell) Exec(errOk, start, output bool, cmd any, args ...any) string {
out := ""
if !errOk && len(sh.Errors) > 0 {
return out
}
cmdIO := exec.NewCmdIO(&sh.Config)
cmdIO.StackStart()
if start {
cmdIO.PushIn(nil) // no stdin for bg
}
cl, scmd, sargs := sh.ExecArgs(cmdIO, errOk, cmd, args...)
if scmd == "" {
return out
}
var err error
if cl != nil {
switch {
case start:
err = cl.Start(&cmdIO.StdIOState, scmd, sargs...)
case output:
cmdIO.PushOut(nil)
out, err = cl.Output(&cmdIO.StdIOState, scmd, sargs...)
default:
err = cl.Run(&cmdIO.StdIOState, scmd, sargs...)
}
if !errOk {
sh.AddError(err)
}
} else {
ran := false
ran, out = sh.RunBuiltinOrCommand(cmdIO, errOk, output, scmd, sargs...)
if !ran {
sh.isCommand.Push(false)
switch {
case start:
err = sh.Config.StartIO(cmdIO, scmd, sargs...)
sh.Jobs.Push(cmdIO)
go func() {
if !cmdIO.OutIsPipe() {
fmt.Printf("[%d] %s\n", len(sh.Jobs), cmdIO.String())
}
cmdIO.Cmd.Wait()
cmdIO.PopToStart()
sh.DeleteJob(cmdIO)
}()
case output:
cmdIO.PushOut(nil)
out, err = sh.Config.OutputIO(cmdIO, scmd, sargs...)
default:
err = sh.Config.RunIO(cmdIO, scmd, sargs...)
}
if !errOk {
sh.AddError(err)
}
sh.isCommand.Pop()
}
}
if !start {
cmdIO.PopToStart()
}
return out
}
// RunBuiltinOrCommand runs a builtin or a command, returning true if it ran,
// and the output string if running in output mode.
func (sh *Shell) RunBuiltinOrCommand(cmdIO *exec.CmdIO, errOk, output bool, cmd string, args ...string) (bool, string) {
out := ""
cmdFun, hasCmd := sh.Commands[cmd]
bltFun, hasBlt := sh.Builtins[cmd]
if !hasCmd && !hasBlt {
return false, out
}
if hasCmd {
sh.commandArgs.Push(args)
sh.isCommand.Push(true)
}
// note: we need to set both os. and wrapper versions, so it works the same
// in compiled vs. interpreted mode
oldsh := sh.Config.StdIO.Set(&cmdIO.StdIO)
oldwrap := sh.StdIOWrappers.SetWrappers(&cmdIO.StdIO)
oldstd := cmdIO.SetToOS()
if output {
obuf := &bytes.Buffer{}
// os.Stdout = obuf // needs a file
sh.Config.StdIO.Out = obuf
sh.StdIOWrappers.SetWrappedOut(obuf)
cmdIO.PushOut(obuf)
if hasCmd {
cmdFun(args...)
} else {
sh.AddError(bltFun(cmdIO, args...))
}
out = strings.TrimSuffix(obuf.String(), "\n")
} else {
if hasCmd {
cmdFun(args...)
} else {
sh.AddError(bltFun(cmdIO, args...))
}
}
if hasCmd {
sh.isCommand.Pop()
sh.commandArgs.Pop()
}
oldstd.SetToOS()
sh.StdIOWrappers.SetWrappers(oldwrap)
sh.Config.StdIO = *oldsh
return true, out
}
func (sh *Shell) HandleArgErr(errok bool, err error) error {
if err == nil {
return err
}
if errok {
sh.Config.StdIO.ErrPrintln(err.Error())
} else {
sh.AddError(err)
}
return err
}
// ExecArgs processes the args to given exec command,
// handling all of the input / output redirection and
// file globbing, homedir expansion, etc.
func (sh *Shell) ExecArgs(cmdIO *exec.CmdIO, errOk bool, cmd any, args ...any) (*sshclient.Client, string, []string) {
if len(sh.Jobs) > 0 {
jb := sh.Jobs.Peek()
if jb.OutIsPipe() {
cmdIO.PushIn(jb.PipeIn.Peek())
}
}
scmd := reflectx.ToString(cmd)
cl := sh.ActiveSSH()
isCmd := sh.isCommand.Peek()
sargs := make([]string, 0, len(args))
var err error
for _, a := range args {
s := reflectx.ToString(a)
if s == "" {
continue
}
if cl == nil {
s, err = homedir.Expand(s)
sh.HandleArgErr(errOk, err)
// note: handling globbing in a later pass, to not clutter..
} else {
if s[0] == '~' {
s = "$HOME/" + s[1:]
}
}
sargs = append(sargs, s)
}
if scmd[0] == '@' {
newHost := ""
if scmd == "@0" { // local
cl = nil
} else {
hnm := scmd[1:]
if scl, ok := sh.SSHClients[hnm]; ok {
newHost = hnm
cl = scl
} else {
sh.HandleArgErr(errOk, fmt.Errorf("cosh: ssh connection named: %q not found", hnm))
}
}
if len(sargs) > 0 {
scmd = sargs[0]
sargs = sargs[1:]
} else { // just a ssh switch
sh.SSHActive = newHost
return nil, "", nil
}
}
for i := 0; i < len(sargs); i++ { // we modify so no range
s := sargs[i]
switch {
case s[0] == '>':
sargs = sh.OutToFile(cl, cmdIO, errOk, sargs, i)
case s[0] == '|':
sargs = sh.OutToPipe(cl, cmdIO, errOk, sargs, i)
case cl == nil && isCmd && strings.HasPrefix(s, "args"):
sargs = sh.CmdArgs(errOk, sargs, i)
i-- // back up because we consume this one
}
}
// do globbing late here so we don't have to wade through everything.
// only for local.
if cl == nil {
gargs := make([]string, 0, len(sargs))
for _, s := range sargs {
g, err := filepath.Glob(s)
if err != nil || len(g) == 0 { // not valid
gargs = append(gargs, s)
} else {
gargs = append(gargs, g...)
}
}
sargs = gargs
}
return cl, scmd, sargs
}
// OutToFile processes the > arg that sends output to a file
func (sh *Shell) OutToFile(cl *sshclient.Client, cmdIO *exec.CmdIO, errOk bool, sargs []string, i int) []string {
n := len(sargs)
s := sargs[i]
sn := len(s)
fn := ""
narg := 1
if i < n-1 {
fn = sargs[i+1]
narg = 2
}
appn := false
errf := false
switch {
case sn > 1 && s[1] == '>':
appn = true
if sn > 2 && s[2] == '&' {
errf = true
}
case sn > 1 && s[1] == '&':
errf = true
case sn > 1:
fn = s[1:]
narg = 1
}
if fn == "" {
sh.HandleArgErr(errOk, fmt.Errorf("cosh: no output file specified"))
return sargs
}
if cl != nil {
if !strings.HasPrefix(fn, "@0:") {
return sargs
}
fn = fn[3:]
}
sargs = slices.Delete(sargs, i, i+narg)
// todo: process @n: expressions here -- if @0 then it is the same
// if @1, then need to launch an ssh "cat >[>] file" with pipe from command as stdin
var f *os.File
var err error
if appn {
f, err = os.OpenFile(fn, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
} else {
f, err = os.Create(fn)
}
if err == nil {
cmdIO.PushOut(f)
if errf {
cmdIO.PushErr(f)
}
} else {
sh.HandleArgErr(errOk, err)
}
return sargs
}
// OutToPipe processes the | arg that sends output to a pipe
func (sh *Shell) OutToPipe(cl *sshclient.Client, cmdIO *exec.CmdIO, errOk bool, sargs []string, i int) []string {
s := sargs[i]
sn := len(s)
errf := false
if sn > 1 && s[1] == '&' {
errf = true
}
// todo: what to do here?
sargs = slices.Delete(sargs, i, i+1)
cmdIO.PushOutPipe()
if errf {
cmdIO.PushErr(cmdIO.Out)
}
// sh.HandleArgErr(errok, err)
return sargs
}
// CmdArgs processes expressions involving "args" for commands
func (sh *Shell) CmdArgs(errOk bool, sargs []string, i int) []string {
// n := len(sargs)
// s := sargs[i]
// sn := len(s)
args := sh.commandArgs.Peek()
// fmt.Println("command args:", args)
switch {
case sargs[i] == "args...":
sargs = slices.Delete(sargs, i, i+1)
sargs = slices.Insert(sargs, i, args...)
}
return sargs
}
// CancelExecution calls the Cancel() function if set.
func (sh *Shell) CancelExecution() {
if sh.Cancel != nil {
sh.Cancel()
}
}
// Copyright (c) 2024, 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 shell
import (
"fmt"
"strings"
"unicode"
)
func ExecWords(ln string) ([]string, error) {
ln = strings.TrimSpace(ln)
n := len(ln)
if n == 0 {
return nil, nil
}
word := ""
esc := false
dQuote := false
bQuote := false
brace := 0
brack := 0
redir := false
var words []string
addWord := func() {
if brace > 0 { // always accum into one token inside brace
return
}
if len(word) > 0 {
words = append(words, word)
word = ""
}
}
atStart := true
sbrack := (ln[0] == '[')
if sbrack {
word = "["
addWord()
brack++
ln = ln[1:]
atStart = false
}
for _, r := range ln {
quote := dQuote || bQuote
if redir {
redir = false
if r == '&' {
word += string(r)
addWord()
continue
}
if r == '>' {
word += string(r)
redir = true
continue
}
addWord()
}
switch {
case esc:
if brace == 0 && unicode.IsSpace(r) { // we will be quoted later anyway
word = word[:len(word)-1]
}
word += string(r)
esc = false
case r == '\\':
esc = true
word += string(r)
case r == '"':
if !bQuote {
dQuote = !dQuote
}
word += string(r)
case r == '`':
if !dQuote {
bQuote = !bQuote
}
word += string(r)
case quote: // absorbs quote -- no need to check below
word += string(r)
case unicode.IsSpace(r):
addWord()
continue // don't reset at start
case r == '{':
if brace == 0 {
addWord()
word = "{"
addWord()
}
brace++
case r == '}':
brace--
if brace == 0 {
addWord()
word = "}"
addWord()
}
case r == '[':
word += string(r)
if atStart && brack == 0 {
sbrack = true
addWord()
}
brack++
case r == ']':
brack--
if brack == 0 && sbrack { // only point of tracking brack is to get this end guy
addWord()
word = "]"
addWord()
} else {
word += string(r)
}
case r == '<' || r == '>' || r == '|':
addWord()
word += string(r)
redir = true
case r == '&': // known to not be redir
addWord()
word += string(r)
case r == ';':
addWord()
word += string(r)
addWord()
atStart = true
continue // avoid reset
default:
word += string(r)
}
atStart = false
}
addWord()
if dQuote || bQuote || brack > 0 {
return words, fmt.Errorf("cosh: exec command has unterminated quotes (\": %v, `: %v) or brackets [ %v ]", dQuote, bQuote, brack > 0)
}
return words, nil
}
// ExecWordIsCommand returns true if given exec word is a command-like string
// (excluding any paths)
func ExecWordIsCommand(f string) bool {
if strings.Contains(f, "(") || strings.Contains(f, "=") {
return false
}
return true
}
// Code generated by 'yaegi extract cogentcore.org/core/base/datasize'. DO NOT EDIT.
package interpreter
import (
"cogentcore.org/core/base/datasize"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/base/datasize/datasize"] = map[string]reflect.Value{
// function, constant and variable definitions
"B": reflect.ValueOf(datasize.B),
"EB": reflect.ValueOf(datasize.EB),
"ErrBits": reflect.ValueOf(&datasize.ErrBits).Elem(),
"GB": reflect.ValueOf(datasize.GB),
"KB": reflect.ValueOf(datasize.KB),
"MB": reflect.ValueOf(datasize.MB),
"MustParse": reflect.ValueOf(datasize.MustParse),
"MustParseString": reflect.ValueOf(datasize.MustParseString),
"PB": reflect.ValueOf(datasize.PB),
"Parse": reflect.ValueOf(datasize.Parse),
"ParseString": reflect.ValueOf(datasize.ParseString),
"TB": reflect.ValueOf(datasize.TB),
// type definitions
"Size": reflect.ValueOf((*datasize.Size)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/base/elide'. DO NOT EDIT.
package interpreter
import (
"cogentcore.org/core/base/elide"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/base/elide/elide"] = map[string]reflect.Value{
// function, constant and variable definitions
"AppName": reflect.ValueOf(elide.AppName),
"End": reflect.ValueOf(elide.End),
"Middle": reflect.ValueOf(elide.Middle),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/base/errors'. DO NOT EDIT.
package interpreter
import (
"cogentcore.org/core/base/errors"
"github.com/cogentcore/yaegi/interp"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/base/errors/errors"] = map[string]reflect.Value{
// function, constant and variable definitions
"As": reflect.ValueOf(errors.As),
"CallerInfo": reflect.ValueOf(errors.CallerInfo),
"ErrUnsupported": reflect.ValueOf(&errors.ErrUnsupported).Elem(),
"Is": reflect.ValueOf(errors.Is),
"Join": reflect.ValueOf(errors.Join),
"Log": reflect.ValueOf(errors.Log),
"Log1": reflect.ValueOf(interp.GenericFunc("func Log1[T any](v T, err error) T { //yaegi:add\n\tif err != nil {\n\t\tslog.Error(err.Error() + \" | \" + CallerInfo())\n\t}\n\treturn v\n}")),
"Must": reflect.ValueOf(errors.Must),
"New": reflect.ValueOf(errors.New),
"Unwrap": reflect.ValueOf(errors.Unwrap),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/base/fsx'. DO NOT EDIT.
package interpreter
import (
"cogentcore.org/core/base/fsx"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/base/fsx/fsx"] = map[string]reflect.Value{
// function, constant and variable definitions
"DirAndFile": reflect.ValueOf(fsx.DirAndFile),
"DirFS": reflect.ValueOf(fsx.DirFS),
"Dirs": reflect.ValueOf(fsx.Dirs),
"FileExists": reflect.ValueOf(fsx.FileExists),
"FileExistsFS": reflect.ValueOf(fsx.FileExistsFS),
"Filenames": reflect.ValueOf(fsx.Filenames),
"Files": reflect.ValueOf(fsx.Files),
"FindFilesOnPaths": reflect.ValueOf(fsx.FindFilesOnPaths),
"GoSrcDir": reflect.ValueOf(fsx.GoSrcDir),
"HasFile": reflect.ValueOf(fsx.HasFile),
"LatestMod": reflect.ValueOf(fsx.LatestMod),
"RelativeFilePath": reflect.ValueOf(fsx.RelativeFilePath),
"Sub": reflect.ValueOf(fsx.Sub),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/base/strcase'. DO NOT EDIT.
package interpreter
import (
"cogentcore.org/core/base/strcase"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/base/strcase/strcase"] = map[string]reflect.Value{
// function, constant and variable definitions
"CamelCase": reflect.ValueOf(strcase.CamelCase),
"CasesN": reflect.ValueOf(strcase.CasesN),
"CasesValues": reflect.ValueOf(strcase.CasesValues),
"FormatList": reflect.ValueOf(strcase.FormatList),
"KEBABCase": reflect.ValueOf(strcase.KEBABCase),
"KebabCase": reflect.ValueOf(strcase.KebabCase),
"LowerCamelCase": reflect.ValueOf(strcase.LowerCamelCase),
"LowerCase": reflect.ValueOf(strcase.LowerCase),
"Noop": reflect.ValueOf(strcase.Noop),
"SNAKECase": reflect.ValueOf(strcase.SNAKECase),
"SentenceCase": reflect.ValueOf(strcase.SentenceCase),
"Skip": reflect.ValueOf(strcase.Skip),
"SkipSplit": reflect.ValueOf(strcase.SkipSplit),
"SnakeCase": reflect.ValueOf(strcase.SnakeCase),
"Split": reflect.ValueOf(strcase.Split),
"TitleCase": reflect.ValueOf(strcase.TitleCase),
"To": reflect.ValueOf(strcase.To),
"ToCamel": reflect.ValueOf(strcase.ToCamel),
"ToKEBAB": reflect.ValueOf(strcase.ToKEBAB),
"ToKebab": reflect.ValueOf(strcase.ToKebab),
"ToLowerCamel": reflect.ValueOf(strcase.ToLowerCamel),
"ToSNAKE": reflect.ValueOf(strcase.ToSNAKE),
"ToSentence": reflect.ValueOf(strcase.ToSentence),
"ToSnake": reflect.ValueOf(strcase.ToSnake),
"ToTitle": reflect.ValueOf(strcase.ToTitle),
"ToWordCase": reflect.ValueOf(strcase.ToWordCase),
"UpperCase": reflect.ValueOf(strcase.UpperCase),
"WordCamelCase": reflect.ValueOf(strcase.WordCamelCase),
"WordCasesN": reflect.ValueOf(strcase.WordCasesN),
"WordCasesValues": reflect.ValueOf(strcase.WordCasesValues),
"WordLowerCase": reflect.ValueOf(strcase.WordLowerCase),
"WordOriginal": reflect.ValueOf(strcase.WordOriginal),
"WordSentenceCase": reflect.ValueOf(strcase.WordSentenceCase),
"WordTitleCase": reflect.ValueOf(strcase.WordTitleCase),
"WordUpperCase": reflect.ValueOf(strcase.WordUpperCase),
// type definitions
"Cases": reflect.ValueOf((*strcase.Cases)(nil)),
"SplitAction": reflect.ValueOf((*strcase.SplitAction)(nil)),
"WordCases": reflect.ValueOf((*strcase.WordCases)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/base/stringsx'. DO NOT EDIT.
package interpreter
import (
"cogentcore.org/core/base/stringsx"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/base/stringsx/stringsx"] = map[string]reflect.Value{
// function, constant and variable definitions
"ByteSplitLines": reflect.ValueOf(stringsx.ByteSplitLines),
"ByteTrimCR": reflect.ValueOf(stringsx.ByteTrimCR),
"InsertFirstUnique": reflect.ValueOf(stringsx.InsertFirstUnique),
"SplitLines": reflect.ValueOf(stringsx.SplitLines),
"TrimCR": reflect.ValueOf(stringsx.TrimCR),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/shell/cosh'. DO NOT EDIT.
package interpreter
import (
"cogentcore.org/core/shell/cosh"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/shell/cosh/cosh"] = map[string]reflect.Value{
// function, constant and variable definitions
"AllFiles": reflect.ValueOf(cosh.AllFiles),
"FileExists": reflect.ValueOf(cosh.FileExists),
"ReadFile": reflect.ValueOf(cosh.ReadFile),
"ReplaceInFile": reflect.ValueOf(cosh.ReplaceInFile),
"SplitLines": reflect.ValueOf(cosh.SplitLines),
"StringsToAnys": reflect.ValueOf(cosh.StringsToAnys),
"WriteFile": reflect.ValueOf(cosh.WriteFile),
}
}
// Copyright (c) 2024, 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 interpreter
//go:generate ./make
import (
"reflect"
"github.com/cogentcore/yaegi/interp"
)
var Symbols = map[string]map[string]reflect.Value{}
// ImportShell imports special symbols from the shell package.
func (in *Interpreter) ImportShell() {
in.Interp.Use(interp.Exports{
"cogentcore.org/core/shell/shell": map[string]reflect.Value{
"Run": reflect.ValueOf(in.Shell.Run),
"RunErrOK": reflect.ValueOf(in.Shell.RunErrOK),
"Output": reflect.ValueOf(in.Shell.Output),
"OutputErrOK": reflect.ValueOf(in.Shell.OutputErrOK),
"Start": reflect.ValueOf(in.Shell.Start),
"AddCommand": reflect.ValueOf(in.Shell.AddCommand),
"RunCommands": reflect.ValueOf(in.Shell.RunCommands),
},
})
}
// Copyright (c) 2024, 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 interpreter
import (
"context"
"fmt"
"io"
"log"
"os"
"os/signal"
"reflect"
"strconv"
"strings"
"syscall"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/shell"
"github.com/cogentcore/yaegi/interp"
"github.com/cogentcore/yaegi/stdlib"
"github.com/ergochat/readline"
)
// Interpreter represents one running shell context
type Interpreter struct {
// the cosh shell
Shell *shell.Shell
// HistFile is the name of the history file to open / save.
// Defaults to ~/.cosh-history for the default cosh shell.
// Update this prior to running Config() to take effect.
HistFile string
// the yaegi interpreter
Interp *interp.Interpreter
}
func init() {
delete(stdlib.Symbols, "errors/errors") // use our errors package instead
}
// NewInterpreter returns a new [Interpreter] initialized with the given options.
// It automatically imports the standard library and configures necessary shell
// functions. End user app must call [Interp.Config] after importing any additional
// symbols, prior to running the interpreter.
func NewInterpreter(options interp.Options) *Interpreter {
in := &Interpreter{HistFile: "~/.cosh-history"}
in.Shell = shell.NewShell()
if options.Stdin != nil {
in.Shell.Config.StdIO.In = options.Stdin
}
if options.Stdout != nil {
in.Shell.Config.StdIO.Out = options.Stdout
}
if options.Stderr != nil {
in.Shell.Config.StdIO.Err = options.Stderr
}
in.Shell.SaveOrigStdIO()
options.Stdout = in.Shell.StdIOWrappers.Out
options.Stderr = in.Shell.StdIOWrappers.Err
options.Stdin = in.Shell.StdIOWrappers.In
in.Interp = interp.New(options)
errors.Log(in.Interp.Use(stdlib.Symbols))
errors.Log(in.Interp.Use(Symbols))
in.ImportShell()
go in.MonitorSignals()
return in
}
// Prompt returns the appropriate REPL prompt to show the user.
func (in *Interpreter) Prompt() string {
dp := in.Shell.TotalDepth()
if dp == 0 {
return in.Shell.HostAndDir() + " > "
}
res := "> "
for range dp {
res += " " // note: /t confuses readline
}
return res
}
// Eval evaluates (interprets) the given code,
// returning the value returned from the interpreter.
// HasPrint indicates whether the last line of code
// has the string print in it, which is for determining
// whether to print the result in interactive mode.
// It automatically logs any error in addition to returning it.
func (in *Interpreter) Eval(code string) (v reflect.Value, hasPrint bool, err error) {
in.Shell.TranspileCode(code)
source := false
if in.Shell.SSHActive == "" {
source = strings.HasPrefix(code, "source")
}
if in.Shell.TotalDepth() == 0 {
nl := len(in.Shell.Lines)
if nl > 0 {
ln := in.Shell.Lines[nl-1]
if strings.Contains(strings.ToLower(ln), "print") {
hasPrint = true
}
}
v, err = in.RunCode()
in.Shell.Errors = nil
}
if source {
v, err = in.RunCode() // run accumulated code
}
return
}
// RunCode runs the accumulated set of code lines
// and clears the stack of code lines.
// It automatically logs any error in addition to returning it.
func (in *Interpreter) RunCode() (reflect.Value, error) {
if len(in.Shell.Errors) > 0 {
return reflect.Value{}, errors.Join(in.Shell.Errors...)
}
in.Shell.AddChunk()
code := in.Shell.Chunks
in.Shell.ResetCode()
var v reflect.Value
var err error
for _, ch := range code {
ctx := in.Shell.StartContext()
v, err = in.Interp.EvalWithContext(ctx, ch)
in.Shell.EndContext()
if err != nil {
cancelled := errors.Is(err, context.Canceled)
// fmt.Println("cancelled:", cancelled)
in.Shell.RestoreOrigStdIO()
in.Shell.ResetDepth()
if !cancelled {
in.Shell.AddError(err)
} else {
in.Shell.Errors = nil
}
break
}
}
return v, err
}
// RunConfig runs the .cosh startup config file in the user's
// home directory if it exists.
func (in *Interpreter) RunConfig() error {
err := in.Shell.TranspileConfig()
if err != nil {
errors.Log(err)
}
_, err = in.RunCode()
return err
}
// MonitorSignals monitors the operating system signals to appropriately
// stop the interpreter and prevent the shell from closing on Control+C.
// It is called automatically in another goroutine in [NewInterpreter].
func (in *Interpreter) MonitorSignals() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
for {
<-c
in.Shell.CancelExecution()
}
}
// Config performs final configuration after all the imports have been Use'd
func (in *Interpreter) Config() {
in.Interp.ImportUsed()
in.RunConfig()
}
// OpenHistory opens history from the current HistFile
// and loads it into the readline history for given rl instance
func (in *Interpreter) OpenHistory(rl *readline.Instance) error {
err := in.Shell.OpenHistory(in.HistFile)
if err == nil {
for _, h := range in.Shell.Hist {
rl.SaveToHistory(h)
}
}
return err
}
// SaveHistory saves last 500 (or HISTFILESIZE env value) lines of history,
// to the current HistFile.
func (in *Interpreter) SaveHistory() error {
n := 500
if hfs := os.Getenv("HISTFILESIZE"); hfs != "" {
en, err := strconv.Atoi(hfs)
if err != nil {
in.Shell.Config.StdIO.ErrPrintf("SaveHistory: environment variable HISTFILESIZE: %q not a number: %s", hfs, err.Error())
} else {
n = en
}
}
return in.Shell.SaveHistory(n, in.HistFile)
}
// Interactive runs an interactive shell that allows the user to input cosh.
// Must have done in.Config() prior to calling.
func (in *Interpreter) Interactive() error {
rl, err := readline.NewFromConfig(&readline.Config{
AutoComplete: &shell.ReadlineCompleter{Shell: in.Shell},
Undo: true,
})
if err != nil {
return err
}
in.OpenHistory(rl)
defer rl.Close()
log.SetOutput(rl.Stderr()) // redraw the prompt correctly after log output
for {
rl.SetPrompt(in.Prompt())
line, err := rl.ReadLine()
if errors.Is(err, readline.ErrInterrupt) {
continue
}
if errors.Is(err, io.EOF) {
in.SaveHistory()
os.Exit(0)
}
if err != nil {
in.SaveHistory()
return err
}
if len(line) > 0 && line[0] == '!' { // history command
hl, err := strconv.Atoi(line[1:])
nh := len(in.Shell.Hist)
if err != nil {
in.Shell.Config.StdIO.ErrPrintf("history number: %q not a number: %s", line[1:], err.Error())
line = ""
} else if hl >= nh {
in.Shell.Config.StdIO.ErrPrintf("history number: %d not in range: [0:%d]", hl, nh)
line = ""
} else {
line = in.Shell.Hist[hl]
fmt.Printf("h:%d\t%s\n", hl, line)
}
} else if line != "" && !strings.HasPrefix(line, "history") && line != "h" {
in.Shell.AddHistory(line)
}
in.Shell.Errors = nil
v, hasPrint, err := in.Eval(line)
if err == nil && !hasPrint && v.IsValid() && !v.IsZero() && v.Kind() != reflect.Func {
fmt.Println(v.Interface())
}
}
}
// Copyright (c) 2024, 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 shell
import (
"go/token"
)
// ReplaceIdentAt replaces an identifier spanning n tokens
// starting at given index, with a single identifier with given string.
// This is used in Exec mode for dealing with identifiers and paths that are
// separately-parsed by Go.
func (tk Tokens) ReplaceIdentAt(at int, str string, n int) Tokens {
ntk := append(tk[:at], &Token{Tok: token.IDENT, Str: str})
ntk = append(ntk, tk[at+n:]...)
return ntk
}
// Path extracts a standard path or URL expression from the current
// list of tokens (starting at index 0), returning the path string
// and the number of tokens included in the path.
// Restricts processing to contiguous elements with no spaces!
// If it is not a path, returns nil string, 0
func (tk Tokens) Path(idx0 bool) (string, int) {
n := len(tk)
if n == 0 {
return "", 0
}
t0 := tk[0]
ispath := (t0.IsPathDelim() || t0.Tok == token.TILDE)
if n == 1 {
if ispath {
return t0.String(), 1
}
return "", 0
}
str := tk[0].String()
lastEnd := int(tk[0].Pos) + len(str)
ci := 1
if !ispath {
lastEnd = int(tk[0].Pos)
ci = 0
if t0.Tok != token.IDENT {
return "", 0
}
tin := 1
tid := t0.Str
tindelim := tk[tin].IsPathDelim()
if idx0 {
tindelim = tk[tin].Tok == token.QUO
}
if (int(tk[tin].Pos) > lastEnd+len(tid)) || !(tk[tin].Tok == token.COLON || tindelim) {
return "", 0
}
ci += tin + 1
str = tid + tk[tin].String()
lastEnd += len(str)
}
prevWasDelim := true
for {
if ci >= n || int(tk[ci].Pos) > lastEnd {
return str, ci
}
ct := tk[ci]
if ct.IsPathDelim() || ct.IsPathExtraDelim() {
prevWasDelim = true
str += ct.String()
lastEnd += len(ct.String())
ci++
continue
}
if ct.Tok == token.STRING {
prevWasDelim = true
str += EscapeQuotes(ct.String())
lastEnd += len(ct.String())
ci++
continue
}
if !prevWasDelim {
if ct.Tok == token.ILLEGAL && ct.Str == `\` && ci+1 < n && int(tk[ci+1].Pos) == lastEnd+2 {
prevWasDelim = true
str += " "
ci++
lastEnd += 2
continue
}
return str, ci
}
if ct.IsWord() {
prevWasDelim = false
str += ct.String()
lastEnd += len(ct.String())
ci++
continue
}
return str, ci
}
}
func (tk *Token) IsPathDelim() bool {
return tk.Tok == token.PERIOD || tk.Tok == token.QUO
}
func (tk *Token) IsPathExtraDelim() bool {
return tk.Tok == token.SUB || tk.Tok == token.ASSIGN || tk.Tok == token.REM || (tk.Tok == token.ILLEGAL && (tk.Str == "?" || tk.Str == "#"))
}
// IsWord returns true if the token is some kind of word-like entity,
// including IDENT, STRING, CHAR, or one of the Go keywords.
// This is for exec filtering.
func (tk *Token) IsWord() bool {
return tk.Tok == token.IDENT || tk.IsGo() || tk.Tok == token.STRING || tk.Tok == token.CHAR
}
// Copyright (c) 2024, 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 shell
// Run executes the given command string, waiting for the command to finish,
// handling the given arguments appropriately.
// If there is any error, it adds it to the shell, and triggers CancelExecution.
// It forwards output to [exec.Config.Stdout] and [exec.Config.Stderr] appropriately.
func (sh *Shell) Run(cmd any, args ...any) {
sh.Exec(false, false, false, cmd, args...)
}
// RunErrOK executes the given command string, waiting for the command to finish,
// handling the given arguments appropriately.
// It does not stop execution if there is an error.
// If there is any error, it adds it to the shell. It forwards output to
// [exec.Config.Stdout] and [exec.Config.Stderr] appropriately.
func (sh *Shell) RunErrOK(cmd any, args ...any) {
sh.Exec(true, false, false, cmd, args...)
}
// Start starts the given command string for running in the background,
// handling the given arguments appropriately.
// If there is any error, it adds it to the shell. It forwards output to
// [exec.Config.Stdout] and [exec.Config.Stderr] appropriately.
func (sh *Shell) Start(cmd any, args ...any) {
sh.Exec(false, true, false, cmd, args...)
}
// Output executes the given command string, handling the given arguments
// appropriately. If there is any error, it adds it to the shell. It returns
// the stdout as a string and forwards stderr to [exec.Config.Stderr] appropriately.
func (sh *Shell) Output(cmd any, args ...any) string {
return sh.Exec(false, false, true, cmd, args...)
}
// OutputErrOK executes the given command string, handling the given arguments
// appropriately. If there is any error, it adds it to the shell. It returns
// the stdout as a string and forwards stderr to [exec.Config.Stderr] appropriately.
func (sh *Shell) OutputErrOK(cmd any, args ...any) string {
return sh.Exec(true, false, true, cmd, args...)
}
// Copyright (c) 2024, 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 shell provides the Cogent Shell (cosh), which combines the best parts
// of Go and bash to provide an integrated shell experience that allows you to
// easily run terminal commands while using Go for complicated logic.
package shell
import (
"context"
"fmt"
"io/fs"
"log/slog"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/exec"
"cogentcore.org/core/base/logx"
"cogentcore.org/core/base/num"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/base/sshclient"
"cogentcore.org/core/base/stack"
"cogentcore.org/core/base/stringsx"
"github.com/mitchellh/go-homedir"
"golang.org/x/tools/imports"
)
// Shell represents one running shell context.
type Shell struct {
// Config is the [exec.Config] used to run commands.
Config exec.Config
// StdIOWrappers are IO wrappers sent to the interpreter, so we can
// control the IO streams used within the interpreter.
// Call SetWrappers on this with another StdIO object to update settings.
StdIOWrappers exec.StdIO
// ssh connection, configuration
SSH *sshclient.Config
// collection of ssh clients
SSHClients map[string]*sshclient.Client
// SSHActive is the name of the active SSH client
SSHActive string
// depth of delim at the end of the current line. if 0, was complete.
ParenDepth, BraceDepth, BrackDepth, TypeDepth, DeclDepth int
// Chunks of code lines that are accumulated during Transpile,
// each of which should be evaluated separately, to avoid
// issues with contextual effects from import, package etc.
Chunks []string
// current stack of transpiled lines, that are accumulated into
// code Chunks
Lines []string
// stack of runtime errors
Errors []error
// Builtins are all the builtin shell commands
Builtins map[string]func(cmdIO *exec.CmdIO, args ...string) error
// commands that have been defined, which can be run in Exec mode.
Commands map[string]func(args ...string)
// Jobs is a stack of commands running in the background
// (via Start instead of Run)
Jobs stack.Stack[*exec.CmdIO]
// Cancel, while the interpreter is running, can be called
// to stop the code interpreting.
// It is connected to the Ctx context, by StartContext()
// Both can be nil.
Cancel func()
// Ctx is the context used for cancelling current shell running
// a single chunk of code, typically from the interpreter.
// We are not able to pass the context around so it is set here,
// in the StartContext function. Clear when done with ClearContext.
Ctx context.Context
// original standard IO setings, to restore
OrigStdIO exec.StdIO
// Hist is the accumulated list of command-line input,
// which is displayed with the history builtin command,
// and saved / restored from ~/.coshhist file
Hist []string
// FuncToVar translates function definitions into variable definitions,
// which is the default for interactive use of random code fragments
// without the complete go formatting.
// For pure transpiling of a complete codebase with full proper Go formatting
// this should be turned off.
FuncToVar bool
// commandArgs is a stack of args passed to a command, used for simplified
// processing of args expressions.
commandArgs stack.Stack[[]string]
// isCommand is a stack of bools indicating whether the _immediate_ run context
// is a command, which affects the way that args are processed.
isCommand stack.Stack[bool]
// if this is non-empty, it is the name of the last command defined.
// triggers insertion of the AddCommand call to add to list of defined commands.
lastCommand string
}
// NewShell returns a new [Shell] with default options.
func NewShell() *Shell {
sh := &Shell{
Config: exec.Config{
Dir: errors.Log1(os.Getwd()),
Env: map[string]string{},
Buffer: false,
},
}
sh.FuncToVar = true
sh.Config.StdIO.SetFromOS()
sh.SSH = sshclient.NewConfig(&sh.Config)
sh.SSHClients = make(map[string]*sshclient.Client)
sh.Commands = make(map[string]func(args ...string))
sh.InstallBuiltins()
return sh
}
// StartContext starts a processing context,
// setting the Ctx and Cancel Fields.
// Call EndContext when current operation finishes.
func (sh *Shell) StartContext() context.Context {
sh.Ctx, sh.Cancel = context.WithCancel(context.Background())
return sh.Ctx
}
// EndContext ends a processing context, clearing the
// Ctx and Cancel fields.
func (sh *Shell) EndContext() {
sh.Ctx = nil
sh.Cancel = nil
}
// SaveOrigStdIO saves the current Config.StdIO as the original to revert to
// after an error, and sets the StdIOWrappers to use them.
func (sh *Shell) SaveOrigStdIO() {
sh.OrigStdIO = sh.Config.StdIO
sh.StdIOWrappers.NewWrappers(&sh.OrigStdIO)
}
// RestoreOrigStdIO reverts to using the saved OrigStdIO
func (sh *Shell) RestoreOrigStdIO() {
sh.Config.StdIO = sh.OrigStdIO
sh.OrigStdIO.SetToOS()
sh.StdIOWrappers.SetWrappers(&sh.OrigStdIO)
}
// Close closes any resources associated with the shell,
// including terminating any commands that are not running "nohup"
// in the background.
func (sh *Shell) Close() {
sh.CloseSSH()
// todo: kill jobs etc
}
// CloseSSH closes all open ssh client connections
func (sh *Shell) CloseSSH() {
sh.SSHActive = ""
for _, cl := range sh.SSHClients {
cl.Close()
}
sh.SSHClients = make(map[string]*sshclient.Client)
}
// ActiveSSH returns the active ssh client
func (sh *Shell) ActiveSSH() *sshclient.Client {
if sh.SSHActive == "" {
return nil
}
return sh.SSHClients[sh.SSHActive]
}
// Host returns the name we're running commands on,
// which is empty if localhost (default).
func (sh *Shell) Host() string {
cl := sh.ActiveSSH()
if cl == nil {
return ""
}
return "@" + sh.SSHActive + ":" + cl.Host
}
// HostAndDir returns the name we're running commands on,
// which is empty if localhost (default),
// and the current directory on that host.
func (sh *Shell) HostAndDir() string {
host := ""
dir := sh.Config.Dir
home := errors.Log1(homedir.Dir())
cl := sh.ActiveSSH()
if cl != nil {
host = "@" + sh.SSHActive + ":" + cl.Host + ":"
dir = cl.Dir
home = cl.HomeDir
}
rel := errors.Log1(filepath.Rel(home, dir))
// if it has to go back, then it is not in home dir, so no ~
if strings.Contains(rel, "..") {
return host + dir + string(filepath.Separator)
}
return host + filepath.Join("~", rel) + string(filepath.Separator)
}
// SSHByHost returns the SSH client for given host name, with err if not found
func (sh *Shell) SSHByHost(host string) (*sshclient.Client, error) {
if scl, ok := sh.SSHClients[host]; ok {
return scl, nil
}
return nil, fmt.Errorf("ssh connection named: %q not found", host)
}
// TotalDepth returns the sum of any unresolved paren, brace, or bracket depths.
func (sh *Shell) TotalDepth() int {
return num.Abs(sh.ParenDepth) + num.Abs(sh.BraceDepth) + num.Abs(sh.BrackDepth)
}
// ResetCode resets the stack of transpiled code
func (sh *Shell) ResetCode() {
sh.Chunks = nil
sh.Lines = nil
}
// ResetDepth resets the current depths to 0
func (sh *Shell) ResetDepth() {
sh.ParenDepth, sh.BraceDepth, sh.BrackDepth, sh.TypeDepth, sh.DeclDepth = 0, 0, 0, 0, 0
}
// DepthError reports an error if any of the parsing depths are not zero,
// to be called at the end of transpiling a complete block of code.
func (sh *Shell) DepthError() error {
if sh.TotalDepth() == 0 {
return nil
}
str := ""
if sh.ParenDepth != 0 {
str += fmt.Sprintf("Incomplete parentheses (), remaining depth: %d\n", sh.ParenDepth)
}
if sh.BraceDepth != 0 {
str += fmt.Sprintf("Incomplete braces [], remaining depth: %d\n", sh.BraceDepth)
}
if sh.BrackDepth != 0 {
str += fmt.Sprintf("Incomplete brackets {}, remaining depth: %d\n", sh.BrackDepth)
}
if str != "" {
slog.Error(str)
return errors.New(str)
}
return nil
}
// AddLine adds line on the stack
func (sh *Shell) AddLine(ln string) {
sh.Lines = append(sh.Lines, ln)
}
// Code returns the current transpiled lines,
// split into chunks that should be compiled separately.
func (sh *Shell) Code() string {
sh.AddChunk()
if len(sh.Chunks) == 0 {
return ""
}
return strings.Join(sh.Chunks, "\n")
}
// AddChunk adds current lines into a chunk of code
// that should be compiled separately.
func (sh *Shell) AddChunk() {
if len(sh.Lines) == 0 {
return
}
sh.Chunks = append(sh.Chunks, strings.Join(sh.Lines, "\n"))
sh.Lines = nil
}
// TranspileCode processes each line of given code,
// adding the results to the LineStack
func (sh *Shell) TranspileCode(code string) {
lns := strings.Split(code, "\n")
n := len(lns)
if n == 0 {
return
}
for _, ln := range lns {
hasDecl := sh.DeclDepth > 0
tl := sh.TranspileLine(ln)
sh.AddLine(tl)
if sh.BraceDepth == 0 && sh.BrackDepth == 0 && sh.ParenDepth == 1 && sh.lastCommand != "" {
sh.lastCommand = ""
nl := len(sh.Lines)
sh.Lines[nl-1] = sh.Lines[nl-1] + ")"
sh.ParenDepth--
}
if hasDecl && sh.DeclDepth == 0 { // break at decl
sh.AddChunk()
}
}
}
// TranspileCodeFromFile transpiles the code in given file
func (sh *Shell) TranspileCodeFromFile(file string) error {
b, err := os.ReadFile(file)
if err != nil {
return err
}
sh.TranspileCode(string(b))
return nil
}
// TranspileFile transpiles the given input cosh file to the
// given output Go file. If no existing package declaration
// is found, then package main and func main declarations are
// added. This also affects how functions are interpreted.
func (sh *Shell) TranspileFile(in string, out string) error {
b, err := os.ReadFile(in)
if err != nil {
return err
}
code := string(b)
lns := stringsx.SplitLines(code)
hasPackage := false
for _, ln := range lns {
if strings.HasPrefix(ln, "package ") {
hasPackage = true
break
}
}
if hasPackage {
sh.FuncToVar = false // use raw functions
}
sh.TranspileCode(code)
sh.FuncToVar = true
if err != nil {
return err
}
gen := "// Code generated by \"cosh build\"; DO NOT EDIT.\n\n"
if hasPackage {
sh.Lines = slices.Insert(sh.Lines, 0, gen)
} else {
sh.Lines = slices.Insert(sh.Lines, 0, gen, "package main", "", "func main() {", "shell := shell.NewShell()")
sh.Lines = append(sh.Lines, "}")
}
src := []byte(sh.Code())
res, err := imports.Process(out, src, nil)
if err != nil {
res = src
slog.Error(err.Error())
} else {
err = sh.DepthError()
}
werr := os.WriteFile(out, res, 0666)
return errors.Join(err, werr)
}
// AddError adds the given error to the error stack if it is non-nil,
// and calls the Cancel function if set, to stop execution.
// This is the main way that shell errors are handled.
// It also prints the error.
func (sh *Shell) AddError(err error) error {
if err == nil {
return nil
}
sh.Errors = append(sh.Errors, err)
logx.PrintlnError(err)
sh.CancelExecution()
return err
}
// TranspileConfig transpiles the .cosh startup config file in the user's
// home directory if it exists.
func (sh *Shell) TranspileConfig() error {
path, err := homedir.Expand("~/.cosh")
if err != nil {
return err
}
b, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil
}
return err
}
sh.TranspileCode(string(b))
return nil
}
// AddHistory adds given line to the Hist record of commands
func (sh *Shell) AddHistory(line string) {
sh.Hist = append(sh.Hist, line)
}
// SaveHistory saves up to the given number of lines of current history
// to given file, e.g., ~/.coshhist for the default cosh program.
// If n is <= 0 all lines are saved. n is typically 500 by default.
func (sh *Shell) SaveHistory(n int, file string) error {
path, err := homedir.Expand(file)
if err != nil {
return err
}
hn := len(sh.Hist)
sn := hn
if n > 0 {
sn = min(n, hn)
}
lh := strings.Join(sh.Hist[hn-sn:hn], "\n")
err = os.WriteFile(path, []byte(lh), 0666)
if err != nil {
return err
}
return nil
}
// OpenHistory opens Hist history lines from given file,
// e.g., ~/.coshhist
func (sh *Shell) OpenHistory(file string) error {
path, err := homedir.Expand(file)
if err != nil {
return err
}
b, err := os.ReadFile(path)
if err != nil {
return err
}
sh.Hist = strings.Split(string(b), "\n")
return nil
}
// AddCommand adds given command to list of available commands
func (sh *Shell) AddCommand(name string, cmd func(args ...string)) {
sh.Commands[name] = cmd
}
// RunCommands runs the given command(s). This is typically called
// from a Makefile-style cosh script.
func (sh *Shell) RunCommands(cmds []any) error {
for _, cmd := range cmds {
if cmdFun, hasCmd := sh.Commands[reflectx.ToString(cmd)]; hasCmd {
cmdFun()
} else {
return errors.Log(fmt.Errorf("command %q not found", cmd))
}
}
return nil
}
// DeleteJob deletes the given job and returns true if successful,
func (sh *Shell) DeleteJob(cmdIO *exec.CmdIO) bool {
idx := slices.Index(sh.Jobs, cmdIO)
if idx >= 0 {
sh.Jobs = slices.Delete(sh.Jobs, idx, idx+1)
return true
}
return false
}
// JobIDExpand expands %n job id values in args with the full PID
// returns number of PIDs expanded
func (sh *Shell) JobIDExpand(args []string) int {
exp := 0
for i, id := range args {
if id[0] == '%' {
idx, err := strconv.Atoi(id[1:])
if err == nil {
if idx > 0 && idx <= len(sh.Jobs) {
jb := sh.Jobs[idx-1]
if jb.Cmd != nil && jb.Cmd.Process != nil {
args[i] = fmt.Sprintf("%d", jb.Cmd.Process.Pid)
exp++
}
} else {
sh.AddError(fmt.Errorf("cosh: job number out of range: %d", idx))
}
}
}
}
return exp
}
// Copyright (c) 2024, 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 shell
import (
"go/scanner"
"go/token"
"log/slog"
"slices"
"strings"
"cogentcore.org/core/base/logx"
)
// Token provides full data for one token
type Token struct {
// Go token classification
Tok token.Token
// Literal string
Str string
// position in the original string.
// this is only set for the original parse,
// not for transpiled additions.
Pos token.Pos
}
// Tokens is a slice of Token
type Tokens []*Token
// NewToken returns a new token, for generated tokens without Pos
func NewToken(tok token.Token, str ...string) *Token {
tk := &Token{Tok: tok}
if len(str) > 0 {
tk.Str = str[0]
}
return tk
}
// Add adds a new token, for generated tokens without Pos
func (tk *Tokens) Add(tok token.Token, str ...string) *Token {
nt := NewToken(tok, str...)
*tk = append(*tk, nt)
return nt
}
// AddTokens adds given tokens to our list
func (tk *Tokens) AddTokens(toks Tokens) *Tokens {
*tk = append(*tk, toks...)
return tk
}
// Insert inserts a new token at given position
func (tk *Tokens) Insert(i int, tok token.Token, str ...string) *Token {
nt := NewToken(tok, str...)
*tk = slices.Insert(*tk, i, nt)
return nt
}
// Last returns the final token in the list
func (tk Tokens) Last() *Token {
n := len(tk)
if n == 0 {
return nil
}
return tk[n-1]
}
// DeleteLastComma removes any final Comma.
// easier to generate and delete at the end
func (tk *Tokens) DeleteLastComma() {
lt := tk.Last()
if lt == nil {
return
}
if lt.Tok == token.COMMA {
*tk = (*tk)[:len(*tk)-1]
}
}
// String returns the string for the token
func (tk *Token) String() string {
if tk.Str != "" {
return tk.Str
}
return tk.Tok.String()
}
// IsBacktickString returns true if the given STRING uses backticks
func (tk *Token) IsBacktickString() bool {
if tk.Tok != token.STRING {
return false
}
return (tk.Str[0] == '`')
}
// IsGo returns true if the given token is a Go Keyword or Comment
func (tk *Token) IsGo() bool {
if tk.Tok >= token.BREAK && tk.Tok <= token.VAR {
return true
}
if tk.Tok == token.COMMENT {
return true
}
return false
}
// IsValidExecIdent returns true if the given token is a valid component
// of an Exec mode identifier
func (tk *Token) IsValidExecIdent() bool {
return (tk.IsGo() || tk.Tok == token.IDENT || tk.Tok == token.SUB || tk.Tok == token.DEC || tk.Tok == token.INT || tk.Tok == token.FLOAT || tk.Tok == token.ASSIGN)
}
// String is the stringer version which includes the token ID
// in addition to the string literal
func (tk Tokens) String() string {
str := ""
for _, tok := range tk {
str += "[" + tok.Tok.String() + "] "
if tok.Str != "" {
str += tok.Str + " "
}
}
if len(str) == 0 {
return str
}
return str[:len(str)-1] // remove trailing space
}
// Code returns concatenated Str values of the tokens,
// to generate a surface-valid code string.
func (tk Tokens) Code() string {
n := len(tk)
if n == 0 {
return ""
}
str := ""
prvIdent := false
for _, tok := range tk {
switch {
case tok.IsOp():
if tok.Tok == token.INC || tok.Tok == token.DEC {
str += tok.String() + " "
} else if tok.Tok == token.MUL {
str += " " + tok.String()
} else {
str += " " + tok.String() + " "
}
prvIdent = false
case tok.Tok == token.ELLIPSIS:
str += " " + tok.String()
prvIdent = false
case tok.IsBracket() || tok.Tok == token.PERIOD:
if tok.Tok == token.RBRACE || tok.Tok == token.LBRACE {
if len(str) > 0 && str[len(str)-1] != ' ' {
str += " "
}
str += tok.String() + " "
} else {
str += tok.String()
}
prvIdent = false
case tok.Tok == token.COMMA || tok.Tok == token.COLON || tok.Tok == token.SEMICOLON:
str += tok.String() + " "
prvIdent = false
case tok.Tok == token.STRUCT:
str += " " + tok.String() + " "
case tok.Tok == token.FUNC:
if prvIdent {
str += " "
}
str += tok.String()
prvIdent = true
case tok.IsGo():
if prvIdent {
str += " "
}
str += tok.String()
if tok.Tok != token.MAP {
str += " "
}
prvIdent = false
case tok.Tok == token.IDENT || tok.Tok == token.STRING:
if prvIdent {
str += " "
}
str += tok.String()
prvIdent = true
default:
str += tok.String()
prvIdent = false
}
}
if len(str) == 0 {
return str
}
if str[len(str)-1] == ' ' {
return str[:len(str)-1]
}
return str
}
// IsOp returns true if the given token is an operator
func (tk *Token) IsOp() bool {
if tk.Tok >= token.ADD && tk.Tok <= token.DEFINE {
return true
}
return false
}
// Contains returns true if the token string contains any of the given token(s)
func (tk Tokens) Contains(toks ...token.Token) bool {
if len(toks) == 0 {
slog.Error("programmer error: tokens.Contains with no args")
return false
}
for _, t := range tk {
for _, st := range toks {
if t.Tok == st {
return true
}
}
}
return false
}
// EscapeQuotes replaces any " with \"
func EscapeQuotes(str string) string {
return strings.ReplaceAll(str, `"`, `\"`)
}
// AddQuotes surrounds given string with quotes,
// also escaping any contained quotes
func AddQuotes(str string) string {
return `"` + EscapeQuotes(str) + `"`
}
// IsBracket returns true if the given token is a bracket delimiter:
// paren, brace, bracket
func (tk *Token) IsBracket() bool {
if (tk.Tok >= token.LPAREN && tk.Tok <= token.LBRACE) || (tk.Tok >= token.RPAREN && tk.Tok <= token.RBRACE) {
return true
}
return false
}
// RightMatching returns the position (or -1 if not found) for the
// right matching [paren, bracket, brace] given the left one that
// is at the 0 position of the current set of tokens.
func (tk Tokens) RightMatching() int {
n := len(tk)
if n == 0 {
return -1
}
rb := token.RPAREN
lb := tk[0].Tok
switch lb {
case token.LPAREN:
rb = token.RPAREN
case token.LBRACK:
rb = token.RBRACK
case token.LBRACE:
rb = token.RBRACE
}
depth := 0
for i := 1; i < n; i++ {
tok := tk[i].Tok
switch tok {
case rb:
if depth <= 0 {
return i
}
depth--
case lb:
depth++
}
}
return -1
}
// BracketDepths returns the depths for the three bracket delimiters
// [paren, bracket, brace], based on unmatched right versions.
func (tk Tokens) BracketDepths() (paren, brace, brack int) {
n := len(tk)
if n == 0 {
return
}
for i := 0; i < n; i++ {
tok := tk[i].Tok
switch tok {
case token.LPAREN:
paren++
case token.LBRACE:
brace++
case token.LBRACK:
brack++
case token.RPAREN:
paren--
case token.RBRACE:
brace--
case token.RBRACK:
brack--
}
}
return
}
// Tokens converts the string into tokens
func (sh *Shell) Tokens(ln string) Tokens {
fset := token.NewFileSet()
f := fset.AddFile("", fset.Base(), len(ln))
var sc scanner.Scanner
sc.Init(f, []byte(ln), sh.errHandler, scanner.ScanComments|2) // 2 is non-exported dontInsertSemis
// note to Go team: just export this stuff. seriously.
var toks Tokens
for {
pos, tok, lit := sc.Scan()
if tok == token.EOF {
break
}
// logx.PrintfDebug(" token: %s\t%s\t%q\n", fset.Position(pos), tok, lit)
toks = append(toks, &Token{Tok: tok, Pos: pos, Str: lit})
}
return toks
}
func (sh *Shell) errHandler(pos token.Position, msg string) {
logx.PrintlnDebug("Scan Error:", pos, msg)
}
// Copyright (c) 2024, 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 shell
import (
"fmt"
"go/token"
"strings"
"cogentcore.org/core/base/logx"
)
// TranspileLine is the main function for parsing a single line of shell input,
// returning a new transpiled line of code that converts Exec code into corresponding
// Go function calls.
func (sh *Shell) TranspileLine(ln string) string {
if len(ln) == 0 {
return ln
}
if strings.HasPrefix(ln, "#!") {
return ""
}
toks := sh.TranspileLineTokens(ln)
paren, brace, brack := toks.BracketDepths()
sh.ParenDepth += paren
sh.BraceDepth += brace
sh.BrackDepth += brack
if sh.TypeDepth > 0 && sh.BraceDepth == 0 {
sh.TypeDepth = 0
}
if sh.DeclDepth > 0 && sh.ParenDepth == 0 {
sh.DeclDepth = 0
}
// logx.PrintlnDebug("depths: ", sh.ParenDepth, sh.BraceDepth, sh.BrackDepth)
return toks.Code()
}
// TranspileLineTokens returns the tokens for the full line
func (sh *Shell) TranspileLineTokens(ln string) Tokens {
if ln == "" {
return nil
}
toks := sh.Tokens(ln)
n := len(toks)
if n == 0 {
return toks
}
ewords, err := ExecWords(ln)
if err != nil {
sh.AddError(err)
return nil
}
logx.PrintlnDebug("\n########## line:\n", ln, "\nTokens:\n", toks.String(), "\nWords:\n", ewords)
if toks[0].Tok == token.TYPE {
sh.TypeDepth++
}
if toks[0].Tok == token.IMPORT || toks[0].Tok == token.VAR || toks[0].Tok == token.CONST {
sh.DeclDepth++
}
if sh.TypeDepth > 0 || sh.DeclDepth > 0 {
logx.PrintlnDebug("go: type / decl defn")
return sh.TranspileGo(toks)
}
t0 := toks[0]
_, t0pn := toks.Path(true) // true = first position
en := len(ewords)
f0exec := (t0.Tok == token.IDENT && ExecWordIsCommand(ewords[0]))
switch {
case t0.Tok == token.LBRACE:
logx.PrintlnDebug("go: { } line")
return sh.TranspileGo(toks[1 : n-1])
case t0.Tok == token.LBRACK:
logx.PrintlnDebug("exec: [ ] line")
return sh.TranspileExec(ewords, false) // it processes the [ ]
case t0.Tok == token.ILLEGAL:
logx.PrintlnDebug("exec: illegal")
return sh.TranspileExec(ewords, false)
case t0.IsBacktickString():
logx.PrintlnDebug("exec: backquoted string")
exe := sh.TranspileExecString(t0.Str, false)
if n > 1 { // todo: is this an error?
exe.AddTokens(sh.TranspileGo(toks[1:]))
}
return exe
case t0.Tok == token.IDENT && t0.Str == "command":
sh.lastCommand = toks[1].Str // 1 is the name -- triggers AddCommand
toks = toks[2:] // get rid of first
toks.Insert(0, token.IDENT, "shell.AddCommand")
toks.Insert(1, token.LPAREN)
toks.Insert(2, token.STRING, `"`+sh.lastCommand+`"`)
toks.Insert(3, token.COMMA)
toks.Insert(4, token.FUNC)
toks.Insert(5, token.LPAREN)
toks.Insert(6, token.IDENT, "args")
toks.Insert(7, token.ELLIPSIS)
toks.Insert(8, token.IDENT, "string")
toks.Insert(9, token.RPAREN)
toks.AddTokens(sh.TranspileGo(toks[11:]))
case t0.IsGo():
if t0.Tok == token.GO {
if !toks.Contains(token.LPAREN) {
logx.PrintlnDebug("exec: go command")
return sh.TranspileExec(ewords, false)
}
}
logx.PrintlnDebug("go keyword")
return sh.TranspileGo(toks)
case toks[n-1].Tok == token.INC:
return sh.TranspileGo(toks)
case t0pn > 0: // path expr
logx.PrintlnDebug("exec: path...")
return sh.TranspileExec(ewords, false)
case t0.Tok == token.STRING:
logx.PrintlnDebug("exec: string...")
return sh.TranspileExec(ewords, false)
case f0exec && en == 1:
logx.PrintlnDebug("exec: 1 word")
return sh.TranspileExec(ewords, false)
case !f0exec: // exec must be IDENT
logx.PrintlnDebug("go: not ident")
return sh.TranspileGo(toks)
case f0exec && en > 1 && (ewords[1][0] == '=' || ewords[1][0] == ':' || ewords[1][0] == '+' || toks[1].Tok == token.COMMA):
logx.PrintlnDebug("go: assignment or defn")
return sh.TranspileGo(toks)
case f0exec: // now any ident
logx.PrintlnDebug("exec: ident..")
return sh.TranspileExec(ewords, false)
default:
logx.PrintlnDebug("go: default")
return sh.TranspileGo(toks)
}
return toks
}
// TranspileGo returns transpiled tokens assuming Go code.
// Unpacks any backtick encapsulated shell commands.
func (sh *Shell) TranspileGo(toks Tokens) Tokens {
n := len(toks)
if n == 0 {
return toks
}
if sh.FuncToVar && toks[0].Tok == token.FUNC { // reorder as an assignment
if len(toks) > 1 && toks[1].Tok == token.IDENT {
toks[0] = toks[1]
toks.Insert(1, token.DEFINE)
toks[2] = &Token{Tok: token.FUNC}
}
}
gtoks := make(Tokens, 0, len(toks)) // return tokens
for _, tok := range toks {
if sh.TypeDepth == 0 && tok.IsBacktickString() {
gtoks = append(gtoks, sh.TranspileExecString(tok.Str, true)...)
} else {
gtoks = append(gtoks, tok)
}
}
return gtoks
}
// TranspileExecString returns transpiled tokens assuming Exec code,
// from a backtick-encoded string, with the given bool indicating
// whether [Output] is needed.
func (sh *Shell) TranspileExecString(str string, output bool) Tokens {
if len(str) <= 1 {
return nil
}
ewords, err := ExecWords(str[1 : len(str)-1]) // enclosed string
if err != nil {
sh.AddError(err)
}
return sh.TranspileExec(ewords, output)
}
// TranspileExec returns transpiled tokens assuming Exec code,
// with the given bools indicating the type of run to execute.
func (sh *Shell) TranspileExec(ewords []string, output bool) Tokens {
n := len(ewords)
if n == 0 {
return nil
}
etoks := make(Tokens, 0, n+5) // return tokens
var execTok *Token
bgJob := false
noStop := false
if ewords[0] == "[" {
ewords = ewords[1:]
n--
noStop = true
}
startExec := func() {
bgJob = false
etoks.Add(token.IDENT, "shell")
etoks.Add(token.PERIOD)
switch {
case output && noStop:
execTok = etoks.Add(token.IDENT, "OutputErrOK")
case output && !noStop:
execTok = etoks.Add(token.IDENT, "Output")
case !output && noStop:
execTok = etoks.Add(token.IDENT, "RunErrOK")
case !output && !noStop:
execTok = etoks.Add(token.IDENT, "Run")
}
etoks.Add(token.LPAREN)
}
endExec := func() {
if bgJob {
execTok.Str = "Start"
}
etoks.DeleteLastComma()
etoks.Add(token.RPAREN)
}
startExec()
for i := 0; i < n; i++ {
f := ewords[i]
switch {
case f == "{": // embedded go
if n < i+3 {
sh.AddError(fmt.Errorf("cosh: no matching right brace } found in exec command line"))
} else {
gstr := ewords[i+1]
etoks.AddTokens(sh.TranspileGo(sh.Tokens(gstr)))
etoks.Add(token.COMMA)
i += 2
}
case f == "[":
noStop = true
case f == "]": // solo is def end
// just skip
noStop = false
case f == "&":
bgJob = true
case f[0] == '|':
execTok.Str = "Start"
etoks.Add(token.IDENT, AddQuotes(f))
etoks.Add(token.COMMA)
endExec()
etoks.Add(token.SEMICOLON)
etoks.AddTokens(sh.TranspileExec(ewords[i+1:], output))
return etoks
case f == ";":
endExec()
etoks.Add(token.SEMICOLON)
etoks.AddTokens(sh.TranspileExec(ewords[i+1:], output))
return etoks
default:
if f[0] == '"' || f[0] == '`' {
etoks.Add(token.STRING, f)
} else {
etoks.Add(token.IDENT, AddQuotes(f)) // mark as an IDENT but add quotes!
}
etoks.Add(token.COMMA)
}
}
endExec()
return etoks
}
// 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 spell
import (
"strings"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/token"
)
// CheckLexLine returns the Lex regions for any words that are misspelled
// within given line of text with existing Lex tags -- automatically
// excludes any Code token regions (see token.IsCode). Token is set
// to token.TextSpellErr on returned Lex's
func CheckLexLine(src []rune, tags lexer.Line) lexer.Line {
wrds := tags.NonCodeWords(src)
var ser lexer.Line
for _, t := range wrds {
wrd := string(t.Src(src))
lwrd := lexer.FirstWordApostrophe(wrd)
if len(lwrd) <= 2 {
continue
}
_, known := Spell.CheckWord(lwrd)
if !known {
t.Token.Token = token.TextSpellErr
widx := strings.Index(wrd, lwrd)
ld := len(wrd) - len(lwrd)
t.St += widx
t.Ed += widx - ld
t.Now()
ser = append(ser, t)
}
}
return ser
}
// Copyright (c) 2024, 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 spell
import (
"io/fs"
"os"
"slices"
"strings"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/stringsx"
"golang.org/x/exp/maps"
)
type Dict map[string]struct{}
func (d Dict) Add(word string) {
d[word] = struct{}{}
}
func (d Dict) Exists(word string) bool {
_, ex := d[word]
return ex
}
// List returns a list (slice) of words in dictionary
// in alpha-sorted order
func (d Dict) List() []string {
wl := maps.Keys(d)
slices.Sort(wl)
return wl
}
// Save saves a dictionary list of words
// to a simple one-word-per-line list, in alpha order
func (d Dict) Save(fname string) error {
wl := d.List()
ws := strings.Join(wl, "\n")
return os.WriteFile(fname, []byte(ws), 0666)
}
// NewDictFromList makes a new dictionary from given list
// (slice) of words
func NewDictFromList(wl []string) Dict {
d := make(Dict, len(wl))
for _, w := range wl {
d.Add(w)
}
return d
}
// OpenDict opens a dictionary list of words
// from a simple one-word-per-line list
func OpenDict(fname string) (Dict, error) {
dfs, fnm, err := fsx.DirFS(fname)
if err != nil {
return nil, err
}
return OpenDictFS(dfs, fnm)
}
// OpenDictFS opens a dictionary list of words
// from a simple one-word-per-line list, from given filesystem
func OpenDictFS(fsys fs.FS, filename string) (Dict, error) {
f, err := fs.ReadFile(fsys, filename)
if err != nil {
return nil, err
}
wl := stringsx.SplitLines(string(f))
d := NewDictFromList(wl)
return d, nil
}
// Copyright (c) 2024, 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 main
import (
"fmt"
"log/slog"
"cogentcore.org/core/cli"
"cogentcore.org/core/spell"
)
//go:generate core generate -add-types -add-funcs
// Config is the configuration information for the cosh cli.
type Config struct {
// InputA is the first input dictionary file
InputA string `posarg:"0" required:"+"`
// InputB is the second input dictionary file
InputB string `posarg:"1" required:"+"`
// Output is the output file for merge command
Output string `cmd:"merge" posarg:"2" required:"-"`
}
func main() { //types:skip
opts := cli.DefaultOptions("dict", "runs dictionary commands")
cli.Run(opts, &Config{}, Compare, Merge)
}
// Compare compares two dictionaries
func Compare(c *Config) error { //cli:cmd -root
a, err := spell.OpenDict(c.InputA)
if err != nil {
slog.Error(err.Error())
return err
}
b, err := spell.OpenDict(c.InputB)
if err != nil {
slog.Error(err.Error())
return err
}
fmt.Printf("In %s not in %s:\n", c.InputA, c.InputB)
for aw := range a {
if !b.Exists(aw) {
fmt.Println(aw)
}
}
fmt.Printf("\n########################\nIn %s not in %s:\n", c.InputB, c.InputA)
for bw := range b {
if !a.Exists(bw) {
fmt.Println(bw)
}
}
return nil
}
// Merge combines two dictionaries
func Merge(c *Config) error { //cli:cmd
return nil
}
// Copyright (c) 2020, Cogent Core. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// this code is adapted from: https://github.com/sajari/fuzzy
// https://www.sajari.com/
// Most of which seems to have been written by Hamish @sajari
// it does not have a copyright notice in the code itself but does have
// an MIT license file.
//
// key change is to ignore counts and just use a flat Dict dictionary
// list of words.
package spell
import (
"strings"
"sync"
"golang.org/x/exp/maps"
)
// Model is the full data model
type Model struct {
// list of all words, combining Base and User dictionaries
Dict Dict
// user dictionary of additional words
UserDict Dict
// words to ignore for this session
Ignore Dict
// map of misspelled word to potential correct spellings
Suggest map[string][]string
// depth of edits to include in Suggest map (2 is only sensible value)
Depth int
sync.RWMutex
}
// Create and initialise a new model
func NewModel() *Model {
md := new(Model)
return md.Init()
}
func (md *Model) Init() *Model {
md.Suggest = make(map[string][]string)
md.Ignore = make(Dict)
md.Depth = 2
return md
}
func (md *Model) SetDicts(base, user Dict) {
md.Dict = base
md.UserDict = user
maps.Copy(md.Dict, md.UserDict)
go md.addSuggestionsForWords(md.Dict.List())
}
// addSuggestionsForWords
func (md *Model) addSuggestionsForWords(terms []string) {
md.Lock()
// st := time.Now()
for _, term := range terms {
md.createSuggestKeys(term)
}
// fmt.Println("train took:", time.Since(st)) // about 500 msec for 32k words, 5 sec for 235k
md.Unlock()
}
// AddWord adds a new word to user dictionary,
// and generates new suggestions for it
func (md *Model) AddWord(term string) {
md.Lock()
defer md.Unlock()
if md.Dict.Exists(term) {
return
}
md.UserDict.Add(term)
md.Dict.Add(term)
md.createSuggestKeys(term)
}
// Delete removes given word from dictionary -- undoes learning
func (md *Model) Delete(term string) {
md.Lock()
edits := md.EditsMulti(term, 1)
for _, edit := range edits {
sug := md.Suggest[edit]
ns := len(sug)
for i := ns - 1; i >= 0; i-- {
hit := sug[i]
if hit == term {
sug = append(sug[:i], sug[i+1:]...)
}
}
if len(sug) == 0 {
delete(md.Suggest, edit)
} else {
md.Suggest[edit] = sug
}
}
delete(md.Dict, term)
md.Unlock()
}
// For a given term, create the partially deleted lookup keys
func (md *Model) createSuggestKeys(term string) {
edits := md.EditsMulti(term, md.Depth)
for _, edit := range edits {
skip := false
for _, hit := range md.Suggest[edit] {
if hit == term {
// Already know about this one
skip = true
continue
}
}
if !skip && len(edit) > 1 {
md.Suggest[edit] = append(md.Suggest[edit], term)
}
}
}
// Edits at any depth for a given term. The depth of the model is used
func (md *Model) EditsMulti(term string, depth int) []string {
edits := Edits1(term)
for {
depth--
if depth <= 0 {
break
}
for _, edit := range edits {
edits2 := Edits1(edit)
for _, edit2 := range edits2 {
edits = append(edits, edit2)
}
}
}
return edits
}
type Pair struct {
str1 string
str2 string
}
// Edits1 creates a set of terms that are 1 char delete from the input term
func Edits1(word string) []string {
splits := []Pair{}
for i := 0; i <= len(word); i++ {
splits = append(splits, Pair{word[:i], word[i:]})
}
total_set := []string{}
for _, elem := range splits {
//deletion
if len(elem.str2) > 0 {
total_set = append(total_set, elem.str1+elem.str2[1:])
} else {
total_set = append(total_set, elem.str1)
}
}
// Special case ending in "ies" or "ys"
if strings.HasSuffix(word, "ies") {
total_set = append(total_set, word[:len(word)-3]+"ys")
}
if strings.HasSuffix(word, "ys") {
total_set = append(total_set, word[:len(word)-2]+"ies")
}
return total_set
}
// For a given input term, suggest some alternatives.
// if the input is in the dictionary, it will be the only item
// returned.
func (md *Model) suggestPotential(input string) []string {
input = strings.ToLower(input)
// 0 - If this is a dictionary term we're all good, no need to go further
if md.Dict.Exists(input) {
return []string{input}
}
ss := make(Dict)
var sord []string
// 1 - See if the input matches a "suggest" key
if sugg, ok := md.Suggest[input]; ok {
for _, pot := range sugg {
if !ss.Exists(pot) {
sord = append(sord, pot)
ss.Add(pot)
}
}
}
// 2 - See if edit1 matches input
edits := md.EditsMulti(input, md.Depth)
got := false
for _, edit := range edits {
if len(edit) > 2 && md.Dict.Exists(edit) {
got = true
if !ss.Exists(edit) {
sord = append(sord, edit)
ss.Add(edit)
}
}
}
if got {
return sord
}
// 3 - No hits on edit1 distance, look for transposes and replaces
// Note: these are more complex, we need to check the guesses
// more thoroughly, e.g. levals=[valves] in a raw sense, which
// is incorrect
for _, edit := range edits {
if sugg, ok := md.Suggest[edit]; ok {
// Is this a real transpose or replace?
for _, pot := range sugg {
lev := Levenshtein(&input, &pot)
if lev <= md.Depth+1 { // The +1 doesn't seem to impact speed, but has greater coverage when the depth is not sufficient to make suggestions
if !ss.Exists(pot) {
sord = append(sord, pot)
ss.Add(pot)
}
}
}
}
}
return sord
}
// Return the most likely corrections in order from best to worst
func (md *Model) Suggestions(input string, n int) []string {
md.RLock()
suggestions := md.suggestPotential(input)
md.RUnlock()
return suggestions
}
// Calculate the Levenshtein distance between two strings
func Levenshtein(a, b *string) int {
la := len(*a)
lb := len(*b)
d := make([]int, la+1)
var lastdiag, olddiag, temp int
for i := 1; i <= la; i++ {
d[i] = i
}
for i := 1; i <= lb; i++ {
d[0] = i
lastdiag = i - 1
for j := 1; j <= la; j++ {
olddiag = d[j]
min := d[j] + 1
if (d[j-1] + 1) < min {
min = d[j-1] + 1
}
if (*a)[j-1] == (*b)[i-1] {
temp = 0
} else {
temp = 1
}
if (lastdiag + temp) < min {
min = lastdiag + temp
}
d[j] = min
lastdiag = olddiag
}
}
return d[la]
}
// 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 spell
import (
"embed"
"log"
"log/slog"
"os"
"strings"
"sync"
"time"
"cogentcore.org/core/parse/lexer"
)
//go:embed dict_en_us
var embedDict embed.FS
// SaveAfterLearnIntervalSecs is number of seconds since
// dict file has been opened / saved
// above which model is saved after learning.
const SaveAfterLearnIntervalSecs = 20
// Spell is the global shared spell
var Spell *SpellData
type SpellData struct {
// UserFile is path to user's dictionary where learned words go
UserFile string
model *Model
openTime time.Time // ModTime() on file
learnTime time.Time // last time when a Learn function was called -- last mod to model -- zero if not mod
mu sync.RWMutex // we need our own mutex in case of loading a new model
}
// NewSpell opens spell data with given user dictionary file
func NewSpell(userFile string) *SpellData {
d, err := OpenDictFS(embedDict, "dict_en_us")
if err != nil {
slog.Error(err.Error())
return nil
}
sp := &SpellData{UserFile: userFile}
sp.ResetLearnTime()
sp.model = NewModel()
sp.openTime = time.Date(2024, 06, 30, 00, 00, 00, 0, time.UTC)
sp.OpenUser()
sp.model.SetDicts(d, sp.model.UserDict)
return sp
}
// modTime returns the modification time of given file path
func modTime(path string) (time.Time, error) {
info, err := os.Stat(path)
if err != nil {
return time.Time{}, err
}
return info.ModTime(), nil
}
func (sp *SpellData) ResetLearnTime() {
sp.learnTime = time.Time{}
}
// OpenUser opens user dictionary of words
func (sp *SpellData) OpenUser() error {
sp.mu.Lock()
defer sp.mu.Unlock()
d, err := OpenDict(sp.UserFile)
if err != nil {
// slog.Error(err.Error())
sp.model.UserDict = make(Dict)
return err
}
// note: does not have suggestions for new words
// future impl will not precompile suggs so it is not worth it
sp.openTime, err = modTime(sp.UserFile)
sp.model.UserDict = d
return err
}
// OpenUserCheck checks if the current user dict file has been modified
// since last open time and re-opens it if so.
func (sp *SpellData) OpenUserCheck() error {
if sp.UserFile == "" {
return nil
}
sp.mu.Lock()
defer sp.mu.Unlock()
tm, err := modTime(sp.UserFile)
if err != nil {
return err
}
if tm.After(sp.openTime) {
sp.OpenUser()
sp.openTime = tm
// log.Printf("opened newer spell file: %s\n", openTime.String())
}
return err
}
// SaveUser saves the user dictionary
// note: this will overwrite any existing file; be sure to have opened
// the current file before making any changes.
func (sp *SpellData) SaveUser() error {
sp.mu.RLock()
defer sp.mu.RUnlock()
if sp.model == nil {
return nil
}
sp.ResetLearnTime()
err := sp.model.UserDict.Save(sp.UserFile)
if err == nil {
sp.openTime, err = modTime(sp.UserFile)
} else {
log.Printf("spell.Spell: Error saving file %q: %v\n", sp.UserFile, err)
}
return err
}
// SaveUserIfLearn saves the user dictionary
// if learning has occurred since last save / open.
// If no changes also checks if file has been modified and opens it if so.
func (sp *SpellData) SaveUserIfLearn() error {
if sp == nil {
return nil
}
if sp.UserFile == "" {
return nil
}
if sp.learnTime.IsZero() {
return sp.OpenUserCheck()
}
sp.SaveUser()
return nil
}
// 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!")
return nil, false
}
w := lexer.FirstWordApostrophe(word) // only lookup words
orig := w
w = strings.ToLower(w)
sp.mu.RLock()
defer sp.mu.RUnlock()
if sp.model.Ignore.Exists(w) {
return nil, true
}
suggests := sp.model.Suggestions(w, 10)
if suggests == nil { // no sug and not known
return nil, false
}
if len(suggests) == 1 && suggests[0] == w {
return nil, true
}
for i, s := range suggests {
suggests[i] = lexer.MatchCase(orig, s)
}
return suggests, false
}
// AddWord adds given word to the User dictionary
func (sp *SpellData) AddWord(word string) {
if sp.learnTime.IsZero() {
sp.OpenUserCheck() // be sure we have latest before learning!
}
sp.mu.Lock()
lword := strings.ToLower(word)
sp.model.AddWord(lword)
sp.learnTime = time.Now()
sint := sp.learnTime.Sub(sp.openTime) / time.Second
sp.mu.Unlock()
if sp.UserFile != "" && sint > SaveAfterLearnIntervalSecs {
sp.SaveUser()
// log.Printf("spell.LearnWord: saved updated model after %d seconds\n", sint)
}
}
// DeleteWord removes word from dictionary, in case accidentally added
func (sp *SpellData) DeleteWord(word string) {
if sp.learnTime.IsZero() {
sp.OpenUserCheck() // be sure we have latest before learning!
}
sp.mu.Lock()
lword := strings.ToLower(word)
sp.model.Delete(lword)
sp.learnTime = time.Now()
sint := sp.learnTime.Sub(sp.openTime) / time.Second
sp.mu.Unlock()
if sp.UserFile != "" && sint > SaveAfterLearnIntervalSecs {
sp.SaveUser()
}
log.Printf("spell.DeleteWord: %s\n", lword)
}
/*
// Complete finds possible completions based on the prefix s
func (sp *SpellData) Complete(s string) []string {
if model == nil {
log.Println("spell.Complete: programmer error -- Spelling not initialized!")
OpenDefault() // backup
}
sp.mu.RLock()
defer sp.mu.RUnlock()
result, _ := model.Autocomplete(s)
return result
}
*/
// IgnoreWord adds the word to the Ignore list
func (sp *SpellData) IgnoreWord(word string) {
word = strings.ToLower(word)
sp.model.Ignore.Add(word)
}
// 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 abilities
//go:generate core generate
import "cogentcore.org/core/enums"
// Abilities represent abilities of GUI elements to take on different States,
// and are aligned with the States flags. All elements can be disabled.
// These correspond to some of the global attributes in CSS:
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
type Abilities int64 //enums:bitflag
const (
// Selectable means it can be Selected
Selectable Abilities = iota
// Activatable means it can be made Active by pressing down on it,
// which gives it a visible state layer color change.
// This also implies Clickable, receiving Click events when
// the user executes a mouse down and up event on the same element.
Activatable
// Clickable means it can be Clicked, receiving Click events when
// the user executes a mouse down and up event on the same element,
// but otherwise does not change its rendering when pressed
// (as Activatable does). Use this for items that are more passively
// clickable, such as frames or tables, whereas e.g., a Button is
// Activatable.
Clickable
// DoubleClickable indicates that an element does something different
// when it is clicked on twice in a row.
DoubleClickable
// TripleClickable indicates that an element does something different
// when it is clicked on three times in a row.
TripleClickable
// RepeatClickable indicates that an element should receive repeated
// click events when the pointer is held down on it.
RepeatClickable
// LongPressable indicates that an element can be LongPressed
LongPressable
// Draggable means it can be Dragged
Draggable
// Droppable means it can receive DragEnter, DragLeave, and Drop events
// (not specific to current Drag item, just generally)
Droppable
// Slideable means it has a slider element that can be dragged
// to change value. Cannot be both Draggable and Slideable.
Slideable
// Checkable means it can be Checked
Checkable
// Scrollable means it can be Scrolled
Scrollable
// Focusable means it can be Focused: capable of receiving and processing key events directly
// and typically changing the style when focused to indicate this property to the user.
Focusable
// Hoverable means it can be Hovered
Hoverable
// LongHoverable means it can be LongHovered
LongHoverable
)
// Is is a shortcut for HasFlag for Abilities
func (ab *Abilities) Is(flag enums.BitFlag) bool {
return ab.HasFlag(flag)
}
// IsPressable returns true when an element is Selectable, Activatable,
// DoubleClickable, Draggable, Slideable, or Checkable
func (ab *Abilities) IsPressable() bool {
return ab.Is(Selectable) || ab.Is(Activatable) || ab.Is(DoubleClickable) || ab.Is(TripleClickable) || ab.Is(Draggable) || ab.Is(Slideable) || ab.Is(Checkable) || ab.Is(Clickable) || ab.Is(LongPressable)
}
// IsHoverable is true for both Hoverable and LongHoverable
func (ab *Abilities) IsHoverable() bool {
return ab.HasFlag(Hoverable) || ab.HasFlag(LongHoverable)
}
// Code generated by "core generate"; DO NOT EDIT.
package abilities
import (
"cogentcore.org/core/enums"
)
var _AbilitiesValues = []Abilities{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}
// AbilitiesN is the highest valid value for type Abilities, plus one.
const AbilitiesN Abilities = 15
var _AbilitiesValueMap = map[string]Abilities{`Selectable`: 0, `Activatable`: 1, `Clickable`: 2, `DoubleClickable`: 3, `TripleClickable`: 4, `RepeatClickable`: 5, `LongPressable`: 6, `Draggable`: 7, `Droppable`: 8, `Slideable`: 9, `Checkable`: 10, `Scrollable`: 11, `Focusable`: 12, `Hoverable`: 13, `LongHoverable`: 14}
var _AbilitiesDescMap = map[Abilities]string{0: `Selectable means it can be Selected`, 1: `Activatable means it can be made Active by pressing down on it, which gives it a visible state layer color change. This also implies Clickable, receiving Click events when the user executes a mouse down and up event on the same element.`, 2: `Clickable means it can be Clicked, receiving Click events when the user executes a mouse down and up event on the same element, but otherwise does not change its rendering when pressed (as Activatable does). Use this for items that are more passively clickable, such as frames or tables, whereas e.g., a Button is Activatable.`, 3: `DoubleClickable indicates that an element does something different when it is clicked on twice in a row.`, 4: `TripleClickable indicates that an element does something different when it is clicked on three times in a row.`, 5: `RepeatClickable indicates that an element should receive repeated click events when the pointer is held down on it.`, 6: `LongPressable indicates that an element can be LongPressed`, 7: `Draggable means it can be Dragged`, 8: `Droppable means it can receive DragEnter, DragLeave, and Drop events (not specific to current Drag item, just generally)`, 9: `Slideable means it has a slider element that can be dragged to change value. Cannot be both Draggable and Slideable.`, 10: `Checkable means it can be Checked`, 11: `Scrollable means it can be Scrolled`, 12: `Focusable means it can be Focused: capable of receiving and processing key events directly and typically changing the style when focused to indicate this property to the user.`, 13: `Hoverable means it can be Hovered`, 14: `LongHoverable means it can be LongHovered`}
var _AbilitiesMap = map[Abilities]string{0: `Selectable`, 1: `Activatable`, 2: `Clickable`, 3: `DoubleClickable`, 4: `TripleClickable`, 5: `RepeatClickable`, 6: `LongPressable`, 7: `Draggable`, 8: `Droppable`, 9: `Slideable`, 10: `Checkable`, 11: `Scrollable`, 12: `Focusable`, 13: `Hoverable`, 14: `LongHoverable`}
// String returns the string representation of this Abilities value.
func (i Abilities) String() string { return enums.BitFlagString(i, _AbilitiesValues) }
// BitIndexString returns the string representation of this Abilities value
// if it is a bit index value (typically an enum constant), and
// not an actual bit flag value.
func (i Abilities) BitIndexString() string { return enums.String(i, _AbilitiesMap) }
// SetString sets the Abilities value from its string representation,
// and returns an error if the string is invalid.
func (i *Abilities) SetString(s string) error { *i = 0; return i.SetStringOr(s) }
// SetStringOr sets the Abilities value from its string representation
// while preserving any bit flags already set, and returns an
// error if the string is invalid.
func (i *Abilities) SetStringOr(s string) error {
return enums.SetStringOr(i, s, _AbilitiesValueMap, "Abilities")
}
// Int64 returns the Abilities value as an int64.
func (i Abilities) Int64() int64 { return int64(i) }
// SetInt64 sets the Abilities value from an int64.
func (i *Abilities) SetInt64(in int64) { *i = Abilities(in) }
// Desc returns the description of the Abilities value.
func (i Abilities) Desc() string { return enums.Desc(i, _AbilitiesDescMap) }
// AbilitiesValues returns all possible values for the type Abilities.
func AbilitiesValues() []Abilities { return _AbilitiesValues }
// Values returns all possible values for the type Abilities.
func (i Abilities) Values() []enums.Enum { return enums.Values(_AbilitiesValues) }
// HasFlag returns whether these bit flags have the given bit flag set.
func (i *Abilities) HasFlag(f enums.BitFlag) bool { return enums.HasFlag((*int64)(i), f) }
// SetFlag sets the value of the given flags in these flags to the given value.
func (i *Abilities) SetFlag(on bool, f ...enums.BitFlag) { enums.SetFlag((*int64)(i), on, f...) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Abilities) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Abilities) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "Abilities")
}
// 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 styles
import (
"image"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles/units"
)
// note: background-color is in FontStyle as it is needed to make that the
// only style needed for text render styling
// // Background has style parameters for backgrounds
// type Background struct {
// // todo: all the properties not yet implemented -- mostly about images
// // Image is like a PaintServer -- includes gradients etc
// // Attachment -- how the image moves
// // Clip -- how to clip the image
// // Origin
// // Position
// // Repeat
// // Size
// }
// func (b *Background) Defaults() {
// b.Color.SetColor(White)
// }
// BorderStyles determines how to draw the border
type BorderStyles int32 //enums:enum -trim-prefix Border -transform kebab
const (
// BorderSolid indicates to render a solid border.
BorderSolid BorderStyles = iota
// BorderDotted indicates to render a dotted border.
BorderDotted
// BorderDashed indicates to render a dashed border.
BorderDashed
// TODO(kai): maybe implement these at some point if there
// is ever an actual use case for them
// BorderDouble is not currently supported.
BorderDouble
// BorderGroove is not currently supported.
BorderGroove
// BorderRidge is not currently supported.
BorderRidge
// BorderInset is not currently supported.
BorderInset
// BorderOutset is not currently supported.
BorderOutset
// BorderNone indicates to render no border.
BorderNone
)
// IMPORTANT: any changes here must be updated in style_properties.go StyleBorderFuncs
// Border contains style parameters for borders
type Border struct { //types:add
// Style specifies how to draw the border
Style Sides[BorderStyles]
// Width specifies the width of the border
Width SideValues `display:"inline"`
// Radius specifies the radius (rounding) of the corners
Radius SideValues `display:"inline"`
// Offset specifies how much, if any, the border is offset
// from its element. It is only applicable in the standard
// box model, which is used by [paint.Context.DrawStdBox] and
// all standard GUI elements.
Offset SideValues `display:"inline"`
// Color specifies the color of the border
Color Sides[image.Image] `display:"inline"`
}
// ToDots runs ToDots on unit values, to compile down to raw pixels
func (bs *Border) ToDots(uc *units.Context) {
bs.Width.ToDots(uc)
bs.Radius.ToDots(uc)
bs.Offset.ToDots(uc)
}
// Pre-configured border radius values, based on
// https://m3.material.io/styles/shape/shape-scale-tokens
var (
// BorderRadiusExtraSmall indicates to use extra small
// 4dp rounded corners
BorderRadiusExtraSmall = NewSideValues(units.Dp(4))
// BorderRadiusExtraSmallTop indicates to use extra small
// 4dp rounded corners on the top of the element and no
// border radius on the bottom of the element
BorderRadiusExtraSmallTop = NewSideValues(units.Dp(4), units.Dp(4), units.Zero(), units.Zero())
// BorderRadiusSmall indicates to use small
// 8dp rounded corners
BorderRadiusSmall = NewSideValues(units.Dp(8))
// BorderRadiusMedium indicates to use medium
// 12dp rounded corners
BorderRadiusMedium = NewSideValues(units.Dp(12))
// BorderRadiusLarge indicates to use large
// 16dp rounded corners
BorderRadiusLarge = NewSideValues(units.Dp(16))
// BorderRadiusLargeEnd indicates to use large
// 16dp rounded corners on the end (right side)
// of the element and no border radius elsewhere
BorderRadiusLargeEnd = NewSideValues(units.Zero(), units.Dp(16), units.Dp(16), units.Zero())
// BorderRadiusLargeTop indicates to use large
// 16dp rounded corners on the top of the element
// and no border radius on the bottom of the element
BorderRadiusLargeTop = NewSideValues(units.Dp(16), units.Dp(16), units.Zero(), units.Zero())
// BorderRadiusExtraLarge indicates to use extra large
// 28dp rounded corners
BorderRadiusExtraLarge = NewSideValues(units.Dp(28))
// BorderRadiusExtraLargeTop indicates to use extra large
// 28dp rounded corners on the top of the element
// and no border radius on the bottom of the element
BorderRadiusExtraLargeTop = NewSideValues(units.Dp(28), units.Dp(28), units.Zero(), units.Zero())
// BorderRadiusFull indicates to use a full border radius,
// which creates a circular/pill-shaped object.
// It is defined to be a value that the width/height of an object
// will never exceed.
BorderRadiusFull = NewSideValues(units.Dp(1_000_000_000))
)
// IMPORTANT: any changes here must be updated in style_properties.go StyleShadowFuncs
// style parameters for shadows
type Shadow struct { //types:add
// OffsetX is th horizontal offset of the shadow.
// Positive moves it right, negative moves it left.
OffsetX units.Value
// OffsetY is the vertical offset of the shadow.
// Positive moves it down, negative moves it up.
OffsetY units.Value
// Blur specifies the blur radius of the shadow.
// Higher numbers make it more blurry.
Blur units.Value
// Spread specifies the spread radius of the shadow.
// Positive numbers increase the size of the shadow,
// and negative numbers decrease the size.
Spread units.Value
// Color specifies the color of the shadow.
Color image.Image
// Inset specifies whether the shadow is inset within the
// box instead of outset outside of the box.
// TODO: implement.
Inset bool
}
func (s *Shadow) HasShadow() bool {
return s.OffsetX.Dots != 0 || s.OffsetY.Dots != 0 || s.Blur.Dots != 0 || s.Spread.Dots != 0
}
// ToDots runs ToDots on unit values, to compile down to raw pixels
func (s *Shadow) ToDots(uc *units.Context) {
s.OffsetX.ToDots(uc)
s.OffsetY.ToDots(uc)
s.Blur.ToDots(uc)
s.Spread.ToDots(uc)
}
// BasePos returns the position at which the base box shadow
// (the actual solid, unblurred box part) should be rendered
// if the shadow is on an element with the given starting position.
func (s *Shadow) BasePos(startPos math32.Vector2) math32.Vector2 {
// Offset directly affects position.
// We need to subtract spread
// to compensate for size changes and stay centered.
return startPos.Add(math32.Vec2(s.OffsetX.Dots, s.OffsetY.Dots)).SubScalar(s.Spread.Dots)
}
// BaseSize returns the total size the base box shadow
// (the actual solid, unblurred part) should be if
// the shadow is on an element with the given starting size.
func (s *Shadow) BaseSize(startSize math32.Vector2) math32.Vector2 {
// Spread goes on all sides, so need to count twice per dimension.
return startSize.AddScalar(2 * s.Spread.Dots)
}
// Pos returns the position at which the blurred box shadow
// should start if the shadow is on an element
// with the given starting position.
func (s *Shadow) Pos(startPos math32.Vector2) math32.Vector2 {
// We need to subtract half of blur
// to compensate for size changes and stay centered.
return s.BasePos(startPos).SubScalar(s.Blur.Dots / 2)
}
// Size returns the total size occupied by the blurred box shadow
// if the shadow is on an element with the given starting size.
func (s *Shadow) Size(startSize math32.Vector2) math32.Vector2 {
// Blur goes on all sides, but it is rendered as half of actual
// because CSS does the same, so we only count it once.
return s.BaseSize(startSize).AddScalar(s.Blur.Dots)
}
// Margin returns the effective margin created by the
// shadow on each side in terms of raw display dots.
// It should be added to margin for sizing considerations.
func (s *Shadow) Margin() SideFloats {
// Spread benefits every side.
// Offset goes either way, depending on side.
// Every side must be positive.
// note: we are using EdgeBlurFactors with radiusFactor = 1
// (sigma == radius), so we divide Blur / 2 relative to the
// CSS standard of sigma = blur / 2 (i.e., our sigma = blur,
// so we divide Blur / 2 to achieve the same effect).
// This works fine for low-opacity blur factors (the edges are
// so transparent that you can't really see beyond 1 sigma if
// you used radiusFactor = 2).
// If a higher-contrast shadow is used, it would look better
// with radiusFactor = 2, and you'd have to remove this /2 factor.
sdots := float32(0)
if s.Blur.Dots > 0 {
sdots = math32.Ceil(0.5 * s.Blur.Dots)
if sdots < 2 { // for tight dp = 1 case, the render antialiasing requires a min width..
sdots = 2
}
}
return NewSideFloats(
math32.Max(s.Spread.Dots-s.OffsetY.Dots+sdots, 0),
math32.Max(s.Spread.Dots+s.OffsetX.Dots+sdots, 0),
math32.Max(s.Spread.Dots+s.OffsetY.Dots+sdots, 0),
math32.Max(s.Spread.Dots-s.OffsetX.Dots+sdots, 0),
)
}
// AddBoxShadow adds the given box shadows to the style
func (s *Style) AddBoxShadow(shadow ...Shadow) {
if s.BoxShadow == nil {
s.BoxShadow = []Shadow{}
}
s.BoxShadow = append(s.BoxShadow, shadow...)
}
// BoxShadowMargin returns the effective box
// shadow margin of the style, calculated through [Shadow.Margin]
func (s *Style) BoxShadowMargin() SideFloats {
return BoxShadowMargin(s.BoxShadow)
}
// MaxBoxShadowMargin returns the maximum effective box
// shadow margin of the style, calculated through [Shadow.Margin]
func (s *Style) MaxBoxShadowMargin() SideFloats {
return BoxShadowMargin(s.MaxBoxShadow)
}
// BoxShadowMargin returns the maximum effective box shadow margin
// of the given box shadows, calculated through [Shadow.Margin].
func BoxShadowMargin(shadows []Shadow) SideFloats {
max := SideFloats{}
for _, sh := range shadows {
max = max.Max(sh.Margin())
}
return max
}
// BoxShadowToDots runs ToDots on all box shadow
// unit values to compile down to raw pixels
func (s *Style) BoxShadowToDots(uc *units.Context) {
for i := range s.BoxShadow {
s.BoxShadow[i].ToDots(uc)
}
for i := range s.MaxBoxShadow {
s.MaxBoxShadow[i].ToDots(uc)
}
}
// HasBoxShadow returns whether the style has
// any box shadows
func (s *Style) HasBoxShadow() bool {
for _, sh := range s.BoxShadow {
if sh.HasShadow() {
return true
}
}
return false
}
// Pre-configured box shadow values, based on
// those in Material 3.
// BoxShadow0 returns the shadows
// to be used on Elevation 0 elements.
// There are no shadows part of BoxShadow0,
// so applying it is purely semantic.
func BoxShadow0() []Shadow {
return []Shadow{}
}
// BoxShadow1 contains the shadows
// to be used on Elevation 1 elements.
func BoxShadow1() []Shadow {
return []Shadow{
{
OffsetX: units.Zero(),
OffsetY: units.Dp(3),
Blur: units.Dp(1),
Spread: units.Dp(-2),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.2),
},
{
OffsetX: units.Zero(),
OffsetY: units.Dp(2),
Blur: units.Dp(2),
Spread: units.Zero(),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.14),
},
{
OffsetX: units.Zero(),
OffsetY: units.Dp(1),
Blur: units.Dp(5),
Spread: units.Zero(),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.12),
},
}
}
// BoxShadow2 returns the shadows
// to be used on Elevation 2 elements.
func BoxShadow2() []Shadow {
return []Shadow{
{
OffsetX: units.Zero(),
OffsetY: units.Dp(2),
Blur: units.Dp(4),
Spread: units.Dp(-1),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.2),
},
{
OffsetX: units.Zero(),
OffsetY: units.Dp(4),
Blur: units.Dp(5),
Spread: units.Zero(),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.14),
},
{
OffsetX: units.Zero(),
OffsetY: units.Dp(1),
Blur: units.Dp(10),
Spread: units.Zero(),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.12),
},
}
}
// TODO: figure out why 3 and 4 are the same
// BoxShadow3 returns the shadows
// to be used on Elevation 3 elements.
func BoxShadow3() []Shadow {
return []Shadow{
{
OffsetX: units.Zero(),
OffsetY: units.Dp(5),
Blur: units.Dp(5),
Spread: units.Dp(-3),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.2),
},
{
OffsetX: units.Zero(),
OffsetY: units.Dp(8),
Blur: units.Dp(10),
Spread: units.Dp(1),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.14),
},
{
OffsetX: units.Zero(),
OffsetY: units.Dp(3),
Blur: units.Dp(14),
Spread: units.Dp(2),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.12),
},
}
}
// BoxShadow4 returns the shadows
// to be used on Elevation 4 elements.
func BoxShadow4() []Shadow {
return []Shadow{
{
OffsetX: units.Zero(),
OffsetY: units.Dp(5),
Blur: units.Dp(5),
Spread: units.Dp(-3),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.2),
},
{
OffsetX: units.Zero(),
OffsetY: units.Dp(8),
Blur: units.Dp(10),
Spread: units.Dp(1),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.14),
},
{
OffsetX: units.Zero(),
OffsetY: units.Dp(3),
Blur: units.Dp(14),
Spread: units.Dp(2),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.12),
},
}
}
// BoxShadow5 returns the shadows
// to be used on Elevation 5 elements.
func BoxShadow5() []Shadow {
return []Shadow{
{
OffsetX: units.Zero(),
OffsetY: units.Dp(8),
Blur: units.Dp(10),
Spread: units.Dp(-6),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.2),
},
{
OffsetX: units.Zero(),
OffsetY: units.Dp(16),
Blur: units.Dp(24),
Spread: units.Dp(2),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.14),
},
{
OffsetX: units.Zero(),
OffsetY: units.Dp(6),
Blur: units.Dp(30),
Spread: units.Dp(5),
Color: gradient.ApplyOpacity(colors.Scheme.Shadow, 0.12),
},
}
}
// 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 styles
import "cogentcore.org/core/math32"
// ClampMax returns given value, not greater than given max _only if_ max > 0
func ClampMax(v, mx float32) float32 {
if mx <= 0 {
return v
}
return min(v, mx)
}
// ClampMin returns given value, not less than given min _only if_ min > 0
func ClampMin(v, mn float32) float32 {
if mn <= 0 {
return v
}
return max(v, mn)
}
// SetClampMax ensures the given value is not greater than given max _only if_ max > 0
func SetClampMax(v *float32, mx float32) {
if mx <= 0 {
return
}
*v = min(*v, mx)
}
// SetClampMin ensures the given value is not less than given min _only if_ min > 0
func SetClampMin(v *float32, mn float32) {
if mn <= 0 {
return
}
*v = max(*v, mn)
}
// ClampMaxVector returns given Vector2 values, not greater than given max _only if_ max > 0
func ClampMaxVector(v, mx math32.Vector2) math32.Vector2 {
var nv math32.Vector2
nv.X = ClampMax(v.X, mx.X)
nv.Y = ClampMax(v.Y, mx.Y)
return nv
}
// ClampMinVector returns given Vector2 values, not less than given min _only if_ min > 0
func ClampMinVector(v, mn math32.Vector2) math32.Vector2 {
var nv math32.Vector2
nv.X = ClampMin(v.X, mn.X)
nv.Y = ClampMin(v.Y, mn.Y)
return nv
}
// SetClampMaxVector ensures the given Vector2 values are not greater than given max _only if_ max > 0
func SetClampMaxVector(v *math32.Vector2, mx math32.Vector2) {
SetClampMax(&v.X, mx.X)
SetClampMax(&v.Y, mx.Y)
}
// SetClampMinVector ensures the given Vector2 values are not less than given min _only if_ min > 0
func SetClampMinVector(v *math32.Vector2, mn math32.Vector2) {
SetClampMin(&v.X, mn.X)
SetClampMin(&v.Y, mn.Y)
}
// Copyright (c) 2024, 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 styles
import (
"fmt"
"image"
"strconv"
"strings"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
)
// ToCSS converts the given [Style] object to a semicolon-separated CSS string.
// It is not guaranteed to be fully complete or accurate. It also takes the kebab-case
// ID name of the associated widget and the resultant html element name for context.
func ToCSS(s *Style, idName, htmlName string) string {
parts := []string{}
add := func(key, value string) {
if value == "" || value == "0" || value == "0px" || value == "0dot" {
return
}
parts = append(parts, key+":"+value)
}
add("color", colorToCSS(s.Color))
add("background", colorToCSS(s.Background))
if htmlName == "svg" {
add("stroke", colorToCSS(s.Color))
add("fill", colorToCSS(s.Color))
}
if idName != "text" { // text does not have these layout properties
if s.Is(states.Invisible) {
add("display", "none")
} else {
add("display", s.Display.String())
}
add("flex-direction", s.Direction.String())
add("flex-grow", fmt.Sprintf("%g", s.Grow.Y))
add("justify-content", s.Justify.Content.String())
add("align-items", s.Align.Items.String())
add("columns", strconv.Itoa(s.Columns))
add("gap", s.Gap.X.StringCSS())
}
add("min-width", s.Min.X.StringCSS())
add("min-height", s.Min.Y.StringCSS())
add("max-width", s.Max.X.StringCSS())
add("max-height", s.Max.Y.StringCSS())
if s.Grow == (math32.Vector2{}) {
add("width", s.Min.X.StringCSS())
add("height", s.Min.Y.StringCSS())
}
add("padding-top", s.Padding.Top.StringCSS())
add("padding-right", s.Padding.Right.StringCSS())
add("padding-bottom", s.Padding.Bottom.StringCSS())
add("padding-left", s.Padding.Left.StringCSS())
add("margin", s.Margin.Top.StringCSS())
if s.Font.Size.Value != 16 || s.Font.Size.Unit != units.UnitDp {
add("font-size", s.Font.Size.StringCSS())
}
if s.Font.Family != "" && s.Font.Family != "Roboto" {
ff := s.Font.Family
if strings.HasSuffix(ff, "Mono") {
ff += ", monospace"
} else {
ff += ", sans-serif"
}
add("font-family", ff)
}
if s.Font.Weight == WeightMedium {
add("font-weight", "500")
} else {
add("font-weight", s.Font.Weight.String())
}
add("line-height", s.Text.LineHeight.StringCSS())
add("text-align", s.Text.Align.String())
if s.Border.Width.Top.Value > 0 {
add("border-style", s.Border.Style.Top.String())
add("border-width", s.Border.Width.Top.StringCSS())
add("border-color", colorToCSS(s.Border.Color.Top))
}
add("border-radius", s.Border.Radius.Top.StringCSS())
return strings.Join(parts, ";")
}
func colorToCSS(c image.Image) string {
switch c {
case nil:
return ""
case colors.Scheme.Primary.Base:
return "var(--primary-color)"
case colors.Scheme.Primary.On:
return "var(--primary-on-color)"
case colors.Scheme.Secondary.Container:
return "var(--secondary-container-color)"
case colors.Scheme.Secondary.OnContainer:
return "var(--secondary-on-container-color)"
case colors.Scheme.Surface, colors.Scheme.OnSurface, colors.Scheme.Background, colors.Scheme.OnBackground:
return "" // already default
case colors.Scheme.SurfaceContainer, colors.Scheme.SurfaceContainerLowest, colors.Scheme.SurfaceContainerLow, colors.Scheme.SurfaceContainerHigh, colors.Scheme.SurfaceContainerHighest:
return "var(--surface-container-color)" // all of them are close enough for this
default:
return colors.AsHex(colors.ToUniform(c))
}
}
// Code generated by "core generate"; DO NOT EDIT.
package styles
import (
"cogentcore.org/core/enums"
)
var _BorderStylesValues = []BorderStyles{0, 1, 2, 3, 4, 5, 6, 7, 8}
// BorderStylesN is the highest valid value for type BorderStyles, plus one.
const BorderStylesN BorderStyles = 9
var _BorderStylesValueMap = map[string]BorderStyles{`solid`: 0, `dotted`: 1, `dashed`: 2, `double`: 3, `groove`: 4, `ridge`: 5, `inset`: 6, `outset`: 7, `none`: 8}
var _BorderStylesDescMap = map[BorderStyles]string{0: `BorderSolid indicates to render a solid border.`, 1: `BorderDotted indicates to render a dotted border.`, 2: `BorderDashed indicates to render a dashed border.`, 3: `BorderDouble is not currently supported.`, 4: `BorderGroove is not currently supported.`, 5: `BorderRidge is not currently supported.`, 6: `BorderInset is not currently supported.`, 7: `BorderOutset is not currently supported.`, 8: `BorderNone indicates to render no border.`}
var _BorderStylesMap = map[BorderStyles]string{0: `solid`, 1: `dotted`, 2: `dashed`, 3: `double`, 4: `groove`, 5: `ridge`, 6: `inset`, 7: `outset`, 8: `none`}
// String returns the string representation of this BorderStyles value.
func (i BorderStyles) String() string { return enums.String(i, _BorderStylesMap) }
// SetString sets the BorderStyles value from its string representation,
// and returns an error if the string is invalid.
func (i *BorderStyles) SetString(s string) error {
return enums.SetString(i, s, _BorderStylesValueMap, "BorderStyles")
}
// Int64 returns the BorderStyles value as an int64.
func (i BorderStyles) Int64() int64 { return int64(i) }
// SetInt64 sets the BorderStyles value from an int64.
func (i *BorderStyles) SetInt64(in int64) { *i = BorderStyles(in) }
// Desc returns the description of the BorderStyles value.
func (i BorderStyles) Desc() string { return enums.Desc(i, _BorderStylesDescMap) }
// BorderStylesValues returns all possible values for the type BorderStyles.
func BorderStylesValues() []BorderStyles { return _BorderStylesValues }
// Values returns all possible values for the type BorderStyles.
func (i BorderStyles) Values() []enums.Enum { return enums.Values(_BorderStylesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i BorderStyles) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *BorderStyles) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "BorderStyles")
}
var _FontStylesValues = []FontStyles{0, 1, 2}
// FontStylesN is the highest valid value for type FontStyles, plus one.
const FontStylesN FontStyles = 3
var _FontStylesValueMap = map[string]FontStyles{`normal`: 0, `italic`: 1, `oblique`: 2}
var _FontStylesDescMap = map[FontStyles]string{0: ``, 1: `Italic indicates to make font italic`, 2: `Oblique indicates to make font slanted`}
var _FontStylesMap = map[FontStyles]string{0: `normal`, 1: `italic`, 2: `oblique`}
// String returns the string representation of this FontStyles value.
func (i FontStyles) String() string { return enums.String(i, _FontStylesMap) }
// SetString sets the FontStyles value from its string representation,
// and returns an error if the string is invalid.
func (i *FontStyles) SetString(s string) error {
return enums.SetString(i, s, _FontStylesValueMap, "FontStyles")
}
// Int64 returns the FontStyles value as an int64.
func (i FontStyles) Int64() int64 { return int64(i) }
// SetInt64 sets the FontStyles value from an int64.
func (i *FontStyles) SetInt64(in int64) { *i = FontStyles(in) }
// Desc returns the description of the FontStyles value.
func (i FontStyles) Desc() string { return enums.Desc(i, _FontStylesDescMap) }
// FontStylesValues returns all possible values for the type FontStyles.
func FontStylesValues() []FontStyles { return _FontStylesValues }
// Values returns all possible values for the type FontStyles.
func (i FontStyles) Values() []enums.Enum { return enums.Values(_FontStylesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i FontStyles) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *FontStyles) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "FontStyles")
}
var _FontWeightsValues = []FontWeights{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}
// FontWeightsN is the highest valid value for type FontWeights, plus one.
const FontWeightsN FontWeights = 20
var _FontWeightsValueMap = map[string]FontWeights{`normal`: 0, `100`: 1, `thin`: 2, `200`: 3, `extra-light`: 4, `300`: 5, `light`: 6, `400`: 7, `500`: 8, `medium`: 9, `600`: 10, `semi-bold`: 11, `700`: 12, `bold`: 13, `800`: 14, `extra-bold`: 15, `900`: 16, `black`: 17, `bolder`: 18, `lighter`: 19}
var _FontWeightsDescMap = map[FontWeights]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``, 5: ``, 6: ``, 7: ``, 8: ``, 9: ``, 10: ``, 11: ``, 12: ``, 13: ``, 14: ``, 15: ``, 16: ``, 17: ``, 18: ``, 19: ``}
var _FontWeightsMap = map[FontWeights]string{0: `normal`, 1: `100`, 2: `thin`, 3: `200`, 4: `extra-light`, 5: `300`, 6: `light`, 7: `400`, 8: `500`, 9: `medium`, 10: `600`, 11: `semi-bold`, 12: `700`, 13: `bold`, 14: `800`, 15: `extra-bold`, 16: `900`, 17: `black`, 18: `bolder`, 19: `lighter`}
// String returns the string representation of this FontWeights value.
func (i FontWeights) String() string { return enums.String(i, _FontWeightsMap) }
// SetString sets the FontWeights value from its string representation,
// and returns an error if the string is invalid.
func (i *FontWeights) SetString(s string) error {
return enums.SetString(i, s, _FontWeightsValueMap, "FontWeights")
}
// Int64 returns the FontWeights value as an int64.
func (i FontWeights) Int64() int64 { return int64(i) }
// SetInt64 sets the FontWeights value from an int64.
func (i *FontWeights) SetInt64(in int64) { *i = FontWeights(in) }
// Desc returns the description of the FontWeights value.
func (i FontWeights) Desc() string { return enums.Desc(i, _FontWeightsDescMap) }
// FontWeightsValues returns all possible values for the type FontWeights.
func FontWeightsValues() []FontWeights { return _FontWeightsValues }
// Values returns all possible values for the type FontWeights.
func (i FontWeights) Values() []enums.Enum { return enums.Values(_FontWeightsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i FontWeights) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *FontWeights) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "FontWeights")
}
var _FontStretchValues = []FontStretch{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// FontStretchN is the highest valid value for type FontStretch, plus one.
const FontStretchN FontStretch = 11
var _FontStretchValueMap = map[string]FontStretch{`Normal`: 0, `UltraCondensed`: 1, `ExtraCondensed`: 2, `SemiCondensed`: 3, `SemiExpanded`: 4, `ExtraExpanded`: 5, `UltraExpanded`: 6, `Condensed`: 7, `Expanded`: 8, `Narrower`: 9, `Wider`: 10}
var _FontStretchDescMap = map[FontStretch]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``, 5: ``, 6: ``, 7: ``, 8: ``, 9: ``, 10: ``}
var _FontStretchMap = map[FontStretch]string{0: `Normal`, 1: `UltraCondensed`, 2: `ExtraCondensed`, 3: `SemiCondensed`, 4: `SemiExpanded`, 5: `ExtraExpanded`, 6: `UltraExpanded`, 7: `Condensed`, 8: `Expanded`, 9: `Narrower`, 10: `Wider`}
// String returns the string representation of this FontStretch value.
func (i FontStretch) String() string { return enums.String(i, _FontStretchMap) }
// SetString sets the FontStretch value from its string representation,
// and returns an error if the string is invalid.
func (i *FontStretch) SetString(s string) error {
return enums.SetString(i, s, _FontStretchValueMap, "FontStretch")
}
// Int64 returns the FontStretch value as an int64.
func (i FontStretch) Int64() int64 { return int64(i) }
// SetInt64 sets the FontStretch value from an int64.
func (i *FontStretch) SetInt64(in int64) { *i = FontStretch(in) }
// Desc returns the description of the FontStretch value.
func (i FontStretch) Desc() string { return enums.Desc(i, _FontStretchDescMap) }
// FontStretchValues returns all possible values for the type FontStretch.
func FontStretchValues() []FontStretch { return _FontStretchValues }
// Values returns all possible values for the type FontStretch.
func (i FontStretch) Values() []enums.Enum { return enums.Values(_FontStretchValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i FontStretch) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *FontStretch) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "FontStretch")
}
var _TextDecorationsValues = []TextDecorations{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// TextDecorationsN is the highest valid value for type TextDecorations, plus one.
const TextDecorationsN TextDecorations = 10
var _TextDecorationsValueMap = map[string]TextDecorations{`none`: 0, `underline`: 1, `overline`: 2, `line-through`: 3, `blink`: 4, `dotted-underline`: 5, `para-start`: 6, `super`: 7, `sub`: 8, `background-color`: 9}
var _TextDecorationsDescMap = map[TextDecorations]string{0: ``, 1: `Underline indicates to place a line below text`, 2: `Overline indicates to place a line above text`, 3: `LineThrough indicates to place a line through text`, 4: `Blink is not currently supported (and probably a bad idea generally ;)`, 5: `DottedUnderline is used for abbr tag -- otherwise not a standard text-decoration option afaik`, 6: `DecoParaStart at start of a SpanRender indicates that it should be styled as the start of a new paragraph and not just the start of a new line`, 7: `DecoSuper indicates super-scripted text`, 8: `DecoSub indicates sub-scripted text`, 9: `DecoBackgroundColor indicates that a bg color has been set -- for use in optimizing rendering`}
var _TextDecorationsMap = map[TextDecorations]string{0: `none`, 1: `underline`, 2: `overline`, 3: `line-through`, 4: `blink`, 5: `dotted-underline`, 6: `para-start`, 7: `super`, 8: `sub`, 9: `background-color`}
// String returns the string representation of this TextDecorations value.
func (i TextDecorations) String() string { return enums.BitFlagString(i, _TextDecorationsValues) }
// BitIndexString returns the string representation of this TextDecorations value
// if it is a bit index value (typically an enum constant), and
// not an actual bit flag value.
func (i TextDecorations) BitIndexString() string { return enums.String(i, _TextDecorationsMap) }
// SetString sets the TextDecorations value from its string representation,
// and returns an error if the string is invalid.
func (i *TextDecorations) SetString(s string) error { *i = 0; return i.SetStringOr(s) }
// SetStringOr sets the TextDecorations value from its string representation
// while preserving any bit flags already set, and returns an
// error if the string is invalid.
func (i *TextDecorations) SetStringOr(s string) error {
return enums.SetStringOr(i, s, _TextDecorationsValueMap, "TextDecorations")
}
// Int64 returns the TextDecorations value as an int64.
func (i TextDecorations) Int64() int64 { return int64(i) }
// SetInt64 sets the TextDecorations value from an int64.
func (i *TextDecorations) SetInt64(in int64) { *i = TextDecorations(in) }
// Desc returns the description of the TextDecorations value.
func (i TextDecorations) Desc() string { return enums.Desc(i, _TextDecorationsDescMap) }
// TextDecorationsValues returns all possible values for the type TextDecorations.
func TextDecorationsValues() []TextDecorations { return _TextDecorationsValues }
// Values returns all possible values for the type TextDecorations.
func (i TextDecorations) Values() []enums.Enum { return enums.Values(_TextDecorationsValues) }
// HasFlag returns whether these bit flags have the given bit flag set.
func (i *TextDecorations) HasFlag(f enums.BitFlag) bool { return enums.HasFlag((*int64)(i), f) }
// SetFlag sets the value of the given flags in these flags to the given value.
func (i *TextDecorations) SetFlag(on bool, f ...enums.BitFlag) { enums.SetFlag((*int64)(i), on, f...) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i TextDecorations) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *TextDecorations) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "TextDecorations")
}
var _BaselineShiftsValues = []BaselineShifts{0, 1, 2}
// BaselineShiftsN is the highest valid value for type BaselineShifts, plus one.
const BaselineShiftsN BaselineShifts = 3
var _BaselineShiftsValueMap = map[string]BaselineShifts{`baseline`: 0, `super`: 1, `sub`: 2}
var _BaselineShiftsDescMap = map[BaselineShifts]string{0: ``, 1: ``, 2: ``}
var _BaselineShiftsMap = map[BaselineShifts]string{0: `baseline`, 1: `super`, 2: `sub`}
// String returns the string representation of this BaselineShifts value.
func (i BaselineShifts) String() string { return enums.String(i, _BaselineShiftsMap) }
// SetString sets the BaselineShifts value from its string representation,
// and returns an error if the string is invalid.
func (i *BaselineShifts) SetString(s string) error {
return enums.SetString(i, s, _BaselineShiftsValueMap, "BaselineShifts")
}
// Int64 returns the BaselineShifts value as an int64.
func (i BaselineShifts) Int64() int64 { return int64(i) }
// SetInt64 sets the BaselineShifts value from an int64.
func (i *BaselineShifts) SetInt64(in int64) { *i = BaselineShifts(in) }
// Desc returns the description of the BaselineShifts value.
func (i BaselineShifts) Desc() string { return enums.Desc(i, _BaselineShiftsDescMap) }
// BaselineShiftsValues returns all possible values for the type BaselineShifts.
func BaselineShiftsValues() []BaselineShifts { return _BaselineShiftsValues }
// Values returns all possible values for the type BaselineShifts.
func (i BaselineShifts) Values() []enums.Enum { return enums.Values(_BaselineShiftsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i BaselineShifts) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *BaselineShifts) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "BaselineShifts")
}
var _FontVariantsValues = []FontVariants{0, 1}
// FontVariantsN is the highest valid value for type FontVariants, plus one.
const FontVariantsN FontVariants = 2
var _FontVariantsValueMap = map[string]FontVariants{`normal`: 0, `small-caps`: 1}
var _FontVariantsDescMap = map[FontVariants]string{0: ``, 1: ``}
var _FontVariantsMap = map[FontVariants]string{0: `normal`, 1: `small-caps`}
// String returns the string representation of this FontVariants value.
func (i FontVariants) String() string { return enums.String(i, _FontVariantsMap) }
// SetString sets the FontVariants value from its string representation,
// and returns an error if the string is invalid.
func (i *FontVariants) SetString(s string) error {
return enums.SetString(i, s, _FontVariantsValueMap, "FontVariants")
}
// Int64 returns the FontVariants value as an int64.
func (i FontVariants) Int64() int64 { return int64(i) }
// SetInt64 sets the FontVariants value from an int64.
func (i *FontVariants) SetInt64(in int64) { *i = FontVariants(in) }
// Desc returns the description of the FontVariants value.
func (i FontVariants) Desc() string { return enums.Desc(i, _FontVariantsDescMap) }
// FontVariantsValues returns all possible values for the type FontVariants.
func FontVariantsValues() []FontVariants { return _FontVariantsValues }
// Values returns all possible values for the type FontVariants.
func (i FontVariants) Values() []enums.Enum { return enums.Values(_FontVariantsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i FontVariants) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *FontVariants) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "FontVariants")
}
var _DirectionsValues = []Directions{0, 1}
// DirectionsN is the highest valid value for type Directions, plus one.
const DirectionsN Directions = 2
var _DirectionsValueMap = map[string]Directions{`row`: 0, `column`: 1}
var _DirectionsDescMap = map[Directions]string{0: `Row indicates that elements are laid out in a row or that an element is longer / travels in the x dimension.`, 1: `Column indicates that elements are laid out in a column or that an element is longer / travels in the y dimension.`}
var _DirectionsMap = map[Directions]string{0: `row`, 1: `column`}
// String returns the string representation of this Directions value.
func (i Directions) String() string { return enums.String(i, _DirectionsMap) }
// SetString sets the Directions value from its string representation,
// and returns an error if the string is invalid.
func (i *Directions) SetString(s string) error {
return enums.SetString(i, s, _DirectionsValueMap, "Directions")
}
// Int64 returns the Directions value as an int64.
func (i Directions) Int64() int64 { return int64(i) }
// SetInt64 sets the Directions value from an int64.
func (i *Directions) SetInt64(in int64) { *i = Directions(in) }
// Desc returns the description of the Directions value.
func (i Directions) Desc() string { return enums.Desc(i, _DirectionsDescMap) }
// DirectionsValues returns all possible values for the type Directions.
func DirectionsValues() []Directions { return _DirectionsValues }
// Values returns all possible values for the type Directions.
func (i Directions) Values() []enums.Enum { return enums.Values(_DirectionsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Directions) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Directions) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "Directions")
}
var _DisplaysValues = []Displays{0, 1, 2, 3, 4}
// DisplaysN is the highest valid value for type Displays, plus one.
const DisplaysN Displays = 5
var _DisplaysValueMap = map[string]Displays{`flex`: 0, `stacked`: 1, `grid`: 2, `custom`: 3, `none`: 4}
var _DisplaysDescMap = map[Displays]string{0: `Flex is the default layout model, based on a simplified version of the CSS flex layout: uses MainAxis to specify the direction, Wrap for wrapping of elements, and Min, Max, and Grow values on elements to determine sizing.`, 1: `Stacked is a stack of elements, with one on top that is visible`, 2: `Grid is the X, Y grid layout, with Columns specifying the number of elements in the X axis.`, 3: `Custom means that no automatic layout will be applied to elements, which can then be managed via custom code by setting the [Style.Pos] position.`, 4: `None means the item is not displayed: sets the Invisible state`}
var _DisplaysMap = map[Displays]string{0: `flex`, 1: `stacked`, 2: `grid`, 3: `custom`, 4: `none`}
// String returns the string representation of this Displays value.
func (i Displays) String() string { return enums.String(i, _DisplaysMap) }
// SetString sets the Displays value from its string representation,
// and returns an error if the string is invalid.
func (i *Displays) SetString(s string) error {
return enums.SetString(i, s, _DisplaysValueMap, "Displays")
}
// Int64 returns the Displays value as an int64.
func (i Displays) Int64() int64 { return int64(i) }
// SetInt64 sets the Displays value from an int64.
func (i *Displays) SetInt64(in int64) { *i = Displays(in) }
// Desc returns the description of the Displays value.
func (i Displays) Desc() string { return enums.Desc(i, _DisplaysDescMap) }
// DisplaysValues returns all possible values for the type Displays.
func DisplaysValues() []Displays { return _DisplaysValues }
// Values returns all possible values for the type Displays.
func (i Displays) Values() []enums.Enum { return enums.Values(_DisplaysValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Displays) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Displays) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Displays") }
var _AlignsValues = []Aligns{0, 1, 2, 3, 4, 5, 6, 7}
// AlignsN is the highest valid value for type Aligns, plus one.
const AlignsN Aligns = 8
var _AlignsValueMap = map[string]Aligns{`auto`: 0, `start`: 1, `end`: 2, `center`: 3, `baseline`: 4, `space-between`: 5, `space-around`: 6, `space-evenly`: 7}
var _AlignsDescMap = map[Aligns]string{0: `Auto means the item uses the container's AlignItems value`, 1: `Align items to the start (top, left) of layout`, 2: `Align items to the end (bottom, right) of layout`, 3: `Align items centered`, 4: `Align to text baselines`, 5: `First and last are flush, equal space between remaining items`, 6: `First and last have 1/2 space at edges, full space between remaining items`, 7: `Equal space at start, end, and between all items`}
var _AlignsMap = map[Aligns]string{0: `auto`, 1: `start`, 2: `end`, 3: `center`, 4: `baseline`, 5: `space-between`, 6: `space-around`, 7: `space-evenly`}
// String returns the string representation of this Aligns value.
func (i Aligns) String() string { return enums.String(i, _AlignsMap) }
// SetString sets the Aligns value from its string representation,
// and returns an error if the string is invalid.
func (i *Aligns) SetString(s string) error { return enums.SetString(i, s, _AlignsValueMap, "Aligns") }
// Int64 returns the Aligns value as an int64.
func (i Aligns) Int64() int64 { return int64(i) }
// SetInt64 sets the Aligns value from an int64.
func (i *Aligns) SetInt64(in int64) { *i = Aligns(in) }
// Desc returns the description of the Aligns value.
func (i Aligns) Desc() string { return enums.Desc(i, _AlignsDescMap) }
// AlignsValues returns all possible values for the type Aligns.
func AlignsValues() []Aligns { return _AlignsValues }
// Values returns all possible values for the type Aligns.
func (i Aligns) Values() []enums.Enum { return enums.Values(_AlignsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Aligns) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Aligns) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Aligns") }
var _OverflowsValues = []Overflows{0, 1, 2, 3}
// OverflowsN is the highest valid value for type Overflows, plus one.
const OverflowsN Overflows = 4
var _OverflowsValueMap = map[string]Overflows{`visible`: 0, `hidden`: 1, `auto`: 2, `scroll`: 3}
var _OverflowsDescMap = map[Overflows]string{0: `OverflowVisible makes the overflow visible, meaning that the size of the container is always at least the Min size of its contents. No scrollbars are shown.`, 1: `OverflowHidden hides the overflow and doesn't present scrollbars.`, 2: `OverflowAuto automatically determines if scrollbars should be added to show the overflow. Scrollbars are added only if the actual content size is greater than the currently available size.`, 3: `OverflowScroll means that scrollbars are always visible, and is otherwise identical to Auto. However, only during Viewport PrefSize call, the actual content size is used -- otherwise it behaves just like Auto.`}
var _OverflowsMap = map[Overflows]string{0: `visible`, 1: `hidden`, 2: `auto`, 3: `scroll`}
// String returns the string representation of this Overflows value.
func (i Overflows) String() string { return enums.String(i, _OverflowsMap) }
// SetString sets the Overflows value from its string representation,
// and returns an error if the string is invalid.
func (i *Overflows) SetString(s string) error {
return enums.SetString(i, s, _OverflowsValueMap, "Overflows")
}
// Int64 returns the Overflows value as an int64.
func (i Overflows) Int64() int64 { return int64(i) }
// SetInt64 sets the Overflows value from an int64.
func (i *Overflows) SetInt64(in int64) { *i = Overflows(in) }
// Desc returns the description of the Overflows value.
func (i Overflows) Desc() string { return enums.Desc(i, _OverflowsDescMap) }
// OverflowsValues returns all possible values for the type Overflows.
func OverflowsValues() []Overflows { return _OverflowsValues }
// Values returns all possible values for the type Overflows.
func (i Overflows) Values() []enums.Enum { return enums.Values(_OverflowsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Overflows) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Overflows) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "Overflows")
}
var _ObjectFitsValues = []ObjectFits{0, 1, 2, 3, 4}
// ObjectFitsN is the highest valid value for type ObjectFits, plus one.
const ObjectFitsN ObjectFits = 5
var _ObjectFitsValueMap = map[string]ObjectFits{`fill`: 0, `contain`: 1, `cover`: 2, `none`: 3, `scale-down`: 4}
var _ObjectFitsDescMap = map[ObjectFits]string{0: `FitFill indicates that the replaced object will fill the element's entire content box, stretching if necessary.`, 1: `FitContain indicates that the replaced object will resize as large as possible while fully fitting within the element's content box and maintaining its aspect ratio. Therefore, it may not fill the entire element.`, 2: `FitCover indicates that the replaced object will fill the element's entire content box, clipping if necessary.`, 3: `FitNone indicates that the replaced object will not resize.`, 4: `FitScaleDown indicates that the replaced object will size as if [FitNone] or [FitContain] was specified, using whichever will result in a smaller final size.`}
var _ObjectFitsMap = map[ObjectFits]string{0: `fill`, 1: `contain`, 2: `cover`, 3: `none`, 4: `scale-down`}
// String returns the string representation of this ObjectFits value.
func (i ObjectFits) String() string { return enums.String(i, _ObjectFitsMap) }
// SetString sets the ObjectFits value from its string representation,
// and returns an error if the string is invalid.
func (i *ObjectFits) SetString(s string) error {
return enums.SetString(i, s, _ObjectFitsValueMap, "ObjectFits")
}
// Int64 returns the ObjectFits value as an int64.
func (i ObjectFits) Int64() int64 { return int64(i) }
// SetInt64 sets the ObjectFits value from an int64.
func (i *ObjectFits) SetInt64(in int64) { *i = ObjectFits(in) }
// Desc returns the description of the ObjectFits value.
func (i ObjectFits) Desc() string { return enums.Desc(i, _ObjectFitsDescMap) }
// ObjectFitsValues returns all possible values for the type ObjectFits.
func ObjectFitsValues() []ObjectFits { return _ObjectFitsValues }
// Values returns all possible values for the type ObjectFits.
func (i ObjectFits) Values() []enums.Enum { return enums.Values(_ObjectFitsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i ObjectFits) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *ObjectFits) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "ObjectFits")
}
var _FillRulesValues = []FillRules{0, 1}
// FillRulesN is the highest valid value for type FillRules, plus one.
const FillRulesN FillRules = 2
var _FillRulesValueMap = map[string]FillRules{`nonzero`: 0, `evenodd`: 1}
var _FillRulesDescMap = map[FillRules]string{0: ``, 1: ``}
var _FillRulesMap = map[FillRules]string{0: `nonzero`, 1: `evenodd`}
// String returns the string representation of this FillRules value.
func (i FillRules) String() string { return enums.String(i, _FillRulesMap) }
// SetString sets the FillRules value from its string representation,
// and returns an error if the string is invalid.
func (i *FillRules) SetString(s string) error {
return enums.SetString(i, s, _FillRulesValueMap, "FillRules")
}
// Int64 returns the FillRules value as an int64.
func (i FillRules) Int64() int64 { return int64(i) }
// SetInt64 sets the FillRules value from an int64.
func (i *FillRules) SetInt64(in int64) { *i = FillRules(in) }
// Desc returns the description of the FillRules value.
func (i FillRules) Desc() string { return enums.Desc(i, _FillRulesDescMap) }
// FillRulesValues returns all possible values for the type FillRules.
func FillRulesValues() []FillRules { return _FillRulesValues }
// Values returns all possible values for the type FillRules.
func (i FillRules) Values() []enums.Enum { return enums.Values(_FillRulesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i FillRules) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *FillRules) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "FillRules")
}
var _VectorEffectsValues = []VectorEffects{0, 1}
// VectorEffectsN is the highest valid value for type VectorEffects, plus one.
const VectorEffectsN VectorEffects = 2
var _VectorEffectsValueMap = map[string]VectorEffects{`none`: 0, `non-scaling-stroke`: 1}
var _VectorEffectsDescMap = map[VectorEffects]string{0: ``, 1: `VectorEffectNonScalingStroke means that the stroke width is not affected by transform properties`}
var _VectorEffectsMap = map[VectorEffects]string{0: `none`, 1: `non-scaling-stroke`}
// String returns the string representation of this VectorEffects value.
func (i VectorEffects) String() string { return enums.String(i, _VectorEffectsMap) }
// SetString sets the VectorEffects value from its string representation,
// and returns an error if the string is invalid.
func (i *VectorEffects) SetString(s string) error {
return enums.SetString(i, s, _VectorEffectsValueMap, "VectorEffects")
}
// Int64 returns the VectorEffects value as an int64.
func (i VectorEffects) Int64() int64 { return int64(i) }
// SetInt64 sets the VectorEffects value from an int64.
func (i *VectorEffects) SetInt64(in int64) { *i = VectorEffects(in) }
// Desc returns the description of the VectorEffects value.
func (i VectorEffects) Desc() string { return enums.Desc(i, _VectorEffectsDescMap) }
// VectorEffectsValues returns all possible values for the type VectorEffects.
func VectorEffectsValues() []VectorEffects { return _VectorEffectsValues }
// Values returns all possible values for the type VectorEffects.
func (i VectorEffects) Values() []enums.Enum { return enums.Values(_VectorEffectsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i VectorEffects) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *VectorEffects) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "VectorEffects")
}
var _LineCapsValues = []LineCaps{0, 1, 2, 3, 4}
// LineCapsN is the highest valid value for type LineCaps, plus one.
const LineCapsN LineCaps = 5
var _LineCapsValueMap = map[string]LineCaps{`butt`: 0, `round`: 1, `square`: 2, `cubic`: 3, `quadratic`: 4}
var _LineCapsDescMap = map[LineCaps]string{0: `LineCapButt indicates to draw no line caps; it draws a line with the length of the specified length.`, 1: `LineCapRound indicates to draw a semicircle on each line end with a diameter of the stroke width.`, 2: `LineCapSquare indicates to draw a rectangle on each line end with a height of the stroke width and a width of half of the stroke width.`, 3: `LineCapCubic is a rasterx extension`, 4: `LineCapQuadratic is a rasterx extension`}
var _LineCapsMap = map[LineCaps]string{0: `butt`, 1: `round`, 2: `square`, 3: `cubic`, 4: `quadratic`}
// String returns the string representation of this LineCaps value.
func (i LineCaps) String() string { return enums.String(i, _LineCapsMap) }
// SetString sets the LineCaps value from its string representation,
// and returns an error if the string is invalid.
func (i *LineCaps) SetString(s string) error {
return enums.SetString(i, s, _LineCapsValueMap, "LineCaps")
}
// Int64 returns the LineCaps value as an int64.
func (i LineCaps) Int64() int64 { return int64(i) }
// SetInt64 sets the LineCaps value from an int64.
func (i *LineCaps) SetInt64(in int64) { *i = LineCaps(in) }
// Desc returns the description of the LineCaps value.
func (i LineCaps) Desc() string { return enums.Desc(i, _LineCapsDescMap) }
// LineCapsValues returns all possible values for the type LineCaps.
func LineCapsValues() []LineCaps { return _LineCapsValues }
// Values returns all possible values for the type LineCaps.
func (i LineCaps) Values() []enums.Enum { return enums.Values(_LineCapsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i LineCaps) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *LineCaps) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "LineCaps") }
var _LineJoinsValues = []LineJoins{0, 1, 2, 3, 4, 5}
// LineJoinsN is the highest valid value for type LineJoins, plus one.
const LineJoinsN LineJoins = 6
var _LineJoinsValueMap = map[string]LineJoins{`miter`: 0, `miter-clip`: 1, `round`: 2, `bevel`: 3, `arcs`: 4, `arcs-clip`: 5}
var _LineJoinsDescMap = map[LineJoins]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``, 5: `rasterx extension`}
var _LineJoinsMap = map[LineJoins]string{0: `miter`, 1: `miter-clip`, 2: `round`, 3: `bevel`, 4: `arcs`, 5: `arcs-clip`}
// String returns the string representation of this LineJoins value.
func (i LineJoins) String() string { return enums.String(i, _LineJoinsMap) }
// SetString sets the LineJoins value from its string representation,
// and returns an error if the string is invalid.
func (i *LineJoins) SetString(s string) error {
return enums.SetString(i, s, _LineJoinsValueMap, "LineJoins")
}
// Int64 returns the LineJoins value as an int64.
func (i LineJoins) Int64() int64 { return int64(i) }
// SetInt64 sets the LineJoins value from an int64.
func (i *LineJoins) SetInt64(in int64) { *i = LineJoins(in) }
// Desc returns the description of the LineJoins value.
func (i LineJoins) Desc() string { return enums.Desc(i, _LineJoinsDescMap) }
// LineJoinsValues returns all possible values for the type LineJoins.
func LineJoinsValues() []LineJoins { return _LineJoinsValues }
// Values returns all possible values for the type LineJoins.
func (i LineJoins) Values() []enums.Enum { return enums.Values(_LineJoinsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i LineJoins) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *LineJoins) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "LineJoins")
}
var _SideIndexesValues = []SideIndexes{0, 1, 2, 3}
// SideIndexesN is the highest valid value for type SideIndexes, plus one.
const SideIndexesN SideIndexes = 4
var _SideIndexesValueMap = map[string]SideIndexes{`Top`: 0, `Right`: 1, `Bottom`: 2, `Left`: 3}
var _SideIndexesDescMap = map[SideIndexes]string{0: ``, 1: ``, 2: ``, 3: ``}
var _SideIndexesMap = map[SideIndexes]string{0: `Top`, 1: `Right`, 2: `Bottom`, 3: `Left`}
// String returns the string representation of this SideIndexes value.
func (i SideIndexes) String() string { return enums.String(i, _SideIndexesMap) }
// SetString sets the SideIndexes value from its string representation,
// and returns an error if the string is invalid.
func (i *SideIndexes) SetString(s string) error {
return enums.SetString(i, s, _SideIndexesValueMap, "SideIndexes")
}
// Int64 returns the SideIndexes value as an int64.
func (i SideIndexes) Int64() int64 { return int64(i) }
// SetInt64 sets the SideIndexes value from an int64.
func (i *SideIndexes) SetInt64(in int64) { *i = SideIndexes(in) }
// Desc returns the description of the SideIndexes value.
func (i SideIndexes) Desc() string { return enums.Desc(i, _SideIndexesDescMap) }
// SideIndexesValues returns all possible values for the type SideIndexes.
func SideIndexesValues() []SideIndexes { return _SideIndexesValues }
// Values returns all possible values for the type SideIndexes.
func (i SideIndexes) Values() []enums.Enum { return enums.Values(_SideIndexesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i SideIndexes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *SideIndexes) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "SideIndexes")
}
var _VirtualKeyboardsValues = []VirtualKeyboards{0, 1, 2, 3, 4, 5, 6, 7}
// VirtualKeyboardsN is the highest valid value for type VirtualKeyboards, plus one.
const VirtualKeyboardsN VirtualKeyboards = 8
var _VirtualKeyboardsValueMap = map[string]VirtualKeyboards{`none`: 0, `single-line`: 1, `multi-line`: 2, `number`: 3, `password`: 4, `email`: 5, `phone`: 6, `url`: 7}
var _VirtualKeyboardsDescMap = map[VirtualKeyboards]string{0: `KeyboardNone indicates to display no virtual keyboard.`, 1: `KeyboardSingleLine indicates to display a virtual keyboard with a default input style and a "Done" return key.`, 2: `KeyboardMultiLine indicates to display a virtual keyboard with a default input style and a "Return" return key.`, 3: `KeyboardNumber indicates to display a virtual keyboard for inputting a number.`, 4: `KeyboardPassword indicates to display a virtual keyboard for inputting a password.`, 5: `KeyboardEmail indicates to display a virtual keyboard for inputting an email address.`, 6: `KeyboardPhone indicates to display a virtual keyboard for inputting a phone number.`, 7: `KeyboardURL indicates to display a virtual keyboard for inputting a URL / URI / web address.`}
var _VirtualKeyboardsMap = map[VirtualKeyboards]string{0: `none`, 1: `single-line`, 2: `multi-line`, 3: `number`, 4: `password`, 5: `email`, 6: `phone`, 7: `url`}
// String returns the string representation of this VirtualKeyboards value.
func (i VirtualKeyboards) String() string { return enums.String(i, _VirtualKeyboardsMap) }
// SetString sets the VirtualKeyboards value from its string representation,
// and returns an error if the string is invalid.
func (i *VirtualKeyboards) SetString(s string) error {
return enums.SetString(i, s, _VirtualKeyboardsValueMap, "VirtualKeyboards")
}
// Int64 returns the VirtualKeyboards value as an int64.
func (i VirtualKeyboards) Int64() int64 { return int64(i) }
// SetInt64 sets the VirtualKeyboards value from an int64.
func (i *VirtualKeyboards) SetInt64(in int64) { *i = VirtualKeyboards(in) }
// Desc returns the description of the VirtualKeyboards value.
func (i VirtualKeyboards) Desc() string { return enums.Desc(i, _VirtualKeyboardsDescMap) }
// VirtualKeyboardsValues returns all possible values for the type VirtualKeyboards.
func VirtualKeyboardsValues() []VirtualKeyboards { return _VirtualKeyboardsValues }
// Values returns all possible values for the type VirtualKeyboards.
func (i VirtualKeyboards) Values() []enums.Enum { return enums.Values(_VirtualKeyboardsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i VirtualKeyboards) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *VirtualKeyboards) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "VirtualKeyboards")
}
var _UnicodeBidiValues = []UnicodeBidi{0, 1, 2}
// UnicodeBidiN is the highest valid value for type UnicodeBidi, plus one.
const UnicodeBidiN UnicodeBidi = 3
var _UnicodeBidiValueMap = map[string]UnicodeBidi{`normal`: 0, `embed`: 1, `bidi-override`: 2}
var _UnicodeBidiDescMap = map[UnicodeBidi]string{0: ``, 1: ``, 2: ``}
var _UnicodeBidiMap = map[UnicodeBidi]string{0: `normal`, 1: `embed`, 2: `bidi-override`}
// String returns the string representation of this UnicodeBidi value.
func (i UnicodeBidi) String() string { return enums.String(i, _UnicodeBidiMap) }
// SetString sets the UnicodeBidi value from its string representation,
// and returns an error if the string is invalid.
func (i *UnicodeBidi) SetString(s string) error {
return enums.SetString(i, s, _UnicodeBidiValueMap, "UnicodeBidi")
}
// Int64 returns the UnicodeBidi value as an int64.
func (i UnicodeBidi) Int64() int64 { return int64(i) }
// SetInt64 sets the UnicodeBidi value from an int64.
func (i *UnicodeBidi) SetInt64(in int64) { *i = UnicodeBidi(in) }
// Desc returns the description of the UnicodeBidi value.
func (i UnicodeBidi) Desc() string { return enums.Desc(i, _UnicodeBidiDescMap) }
// UnicodeBidiValues returns all possible values for the type UnicodeBidi.
func UnicodeBidiValues() []UnicodeBidi { return _UnicodeBidiValues }
// Values returns all possible values for the type UnicodeBidi.
func (i UnicodeBidi) Values() []enums.Enum { return enums.Values(_UnicodeBidiValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i UnicodeBidi) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *UnicodeBidi) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "UnicodeBidi")
}
var _TextDirectionsValues = []TextDirections{0, 1, 2, 3, 4, 5, 6, 7}
// TextDirectionsN is the highest valid value for type TextDirections, plus one.
const TextDirectionsN TextDirections = 8
var _TextDirectionsValueMap = map[string]TextDirections{`lrtb`: 0, `rltb`: 1, `tbrl`: 2, `lr`: 3, `rl`: 4, `tb`: 5, `ltr`: 6, `rtl`: 7}
var _TextDirectionsDescMap = map[TextDirections]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``, 5: ``, 6: ``, 7: ``}
var _TextDirectionsMap = map[TextDirections]string{0: `lrtb`, 1: `rltb`, 2: `tbrl`, 3: `lr`, 4: `rl`, 5: `tb`, 6: `ltr`, 7: `rtl`}
// String returns the string representation of this TextDirections value.
func (i TextDirections) String() string { return enums.String(i, _TextDirectionsMap) }
// SetString sets the TextDirections value from its string representation,
// and returns an error if the string is invalid.
func (i *TextDirections) SetString(s string) error {
return enums.SetString(i, s, _TextDirectionsValueMap, "TextDirections")
}
// Int64 returns the TextDirections value as an int64.
func (i TextDirections) Int64() int64 { return int64(i) }
// SetInt64 sets the TextDirections value from an int64.
func (i *TextDirections) SetInt64(in int64) { *i = TextDirections(in) }
// Desc returns the description of the TextDirections value.
func (i TextDirections) Desc() string { return enums.Desc(i, _TextDirectionsDescMap) }
// TextDirectionsValues returns all possible values for the type TextDirections.
func TextDirectionsValues() []TextDirections { return _TextDirectionsValues }
// Values returns all possible values for the type TextDirections.
func (i TextDirections) Values() []enums.Enum { return enums.Values(_TextDirectionsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i TextDirections) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *TextDirections) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "TextDirections")
}
var _TextAnchorsValues = []TextAnchors{0, 1, 2}
// TextAnchorsN is the highest valid value for type TextAnchors, plus one.
const TextAnchorsN TextAnchors = 3
var _TextAnchorsValueMap = map[string]TextAnchors{`start`: 0, `middle`: 1, `end`: 2}
var _TextAnchorsDescMap = map[TextAnchors]string{0: ``, 1: ``, 2: ``}
var _TextAnchorsMap = map[TextAnchors]string{0: `start`, 1: `middle`, 2: `end`}
// String returns the string representation of this TextAnchors value.
func (i TextAnchors) String() string { return enums.String(i, _TextAnchorsMap) }
// SetString sets the TextAnchors value from its string representation,
// and returns an error if the string is invalid.
func (i *TextAnchors) SetString(s string) error {
return enums.SetString(i, s, _TextAnchorsValueMap, "TextAnchors")
}
// Int64 returns the TextAnchors value as an int64.
func (i TextAnchors) Int64() int64 { return int64(i) }
// SetInt64 sets the TextAnchors value from an int64.
func (i *TextAnchors) SetInt64(in int64) { *i = TextAnchors(in) }
// Desc returns the description of the TextAnchors value.
func (i TextAnchors) Desc() string { return enums.Desc(i, _TextAnchorsDescMap) }
// TextAnchorsValues returns all possible values for the type TextAnchors.
func TextAnchorsValues() []TextAnchors { return _TextAnchorsValues }
// Values returns all possible values for the type TextAnchors.
func (i TextAnchors) Values() []enums.Enum { return enums.Values(_TextAnchorsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i TextAnchors) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *TextAnchors) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "TextAnchors")
}
var _WhiteSpacesValues = []WhiteSpaces{0, 1, 2, 3, 4}
// WhiteSpacesN is the highest valid value for type WhiteSpaces, plus one.
const WhiteSpacesN WhiteSpaces = 5
var _WhiteSpacesValueMap = map[string]WhiteSpaces{`Normal`: 0, `Nowrap`: 1, `Pre`: 2, `PreLine`: 3, `PreWrap`: 4}
var _WhiteSpacesDescMap = map[WhiteSpaces]string{0: `WhiteSpaceNormal means that all white space is collapsed to a single space, and text wraps when necessary. To get full word wrapping to expand to all available space, you also need to set GrowWrap = true. Use the SetTextWrap convenience method to set both.`, 1: `WhiteSpaceNowrap means that sequences of whitespace will collapse into a single whitespace. Text will never wrap to the next line except if there is an explicit line break via a <br> tag. In general you also don't want simple non-wrapping text labels to Grow (GrowWrap = false). Use the SetTextWrap method to set both.`, 2: `WhiteSpacePre means that whitespace is preserved. Text will only wrap on line breaks. Acts like the <pre> tag in HTML. This invokes a different hand-written parser because the default Go parser automatically throws away whitespace.`, 3: `WhiteSpacePreLine means that sequences of whitespace will collapse into a single whitespace. Text will wrap when necessary, and on line breaks`, 4: `WhiteSpacePreWrap means that whitespace is preserved. Text will wrap when necessary, and on line breaks`}
var _WhiteSpacesMap = map[WhiteSpaces]string{0: `Normal`, 1: `Nowrap`, 2: `Pre`, 3: `PreLine`, 4: `PreWrap`}
// String returns the string representation of this WhiteSpaces value.
func (i WhiteSpaces) String() string { return enums.String(i, _WhiteSpacesMap) }
// SetString sets the WhiteSpaces value from its string representation,
// and returns an error if the string is invalid.
func (i *WhiteSpaces) SetString(s string) error {
return enums.SetString(i, s, _WhiteSpacesValueMap, "WhiteSpaces")
}
// Int64 returns the WhiteSpaces value as an int64.
func (i WhiteSpaces) Int64() int64 { return int64(i) }
// SetInt64 sets the WhiteSpaces value from an int64.
func (i *WhiteSpaces) SetInt64(in int64) { *i = WhiteSpaces(in) }
// Desc returns the description of the WhiteSpaces value.
func (i WhiteSpaces) Desc() string { return enums.Desc(i, _WhiteSpacesDescMap) }
// WhiteSpacesValues returns all possible values for the type WhiteSpaces.
func WhiteSpacesValues() []WhiteSpaces { return _WhiteSpacesValues }
// Values returns all possible values for the type WhiteSpaces.
func (i WhiteSpaces) Values() []enums.Enum { return enums.Values(_WhiteSpacesValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i WhiteSpaces) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *WhiteSpaces) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "WhiteSpaces")
}
// 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 styles
import (
"image"
"log/slog"
"strings"
"cogentcore.org/core/colors"
"cogentcore.org/core/styles/units"
)
// IMPORTANT: any changes here must be updated in style_properties.go StyleFontFuncs
// Font contains all font styling information.
// Most of font information is inherited.
// Font does not include all information needed
// for rendering -- see [FontRender] for that.
type Font struct { //types:add
// size of font to render (inherited); converted to points when getting font to use
Size units.Value
// font family (inherited): ordered list of comma-separated names from more general to more specific to use; use split on , to parse
Family string
// style (inherited): normal, italic, etc
Style FontStyles
// weight (inherited): normal, bold, etc
Weight FontWeights
// font stretch / condense options (inherited)
Stretch FontStretch
// normal or small caps (inherited)
Variant FontVariants
// Decoration contains the bit flag [TextDecorations]
// (underline, line-through, etc). It must be set using
// [Font.SetDecoration] since it contains bit flags.
// It is not inherited.
Decoration TextDecorations
// super / sub script (not inherited)
Shift BaselineShifts
// full font information including enhanced metrics and actual font codes for drawing text; this is a pointer into FontLibrary of loaded fonts
Face *FontFace `display:"-"`
}
func (fs *Font) Defaults() {
fs.Size = units.Dp(16)
}
// InheritFields from parent
func (fs *Font) InheritFields(parent *Font) {
// fs.Color = par.Color
fs.Family = parent.Family
fs.Style = parent.Style
if parent.Size.Value != 0 {
fs.Size = parent.Size
}
fs.Weight = parent.Weight
fs.Stretch = parent.Stretch
fs.Variant = parent.Variant
}
// ToDots runs ToDots on unit values, to compile down to raw pixels
func (fs *Font) ToDots(uc *units.Context) {
if fs.Size.Unit == units.UnitEm || fs.Size.Unit == units.UnitEx || fs.Size.Unit == units.UnitCh {
slog.Error("girl/styles.Font.Size was set to Em, Ex, or Ch; that is recursive and unstable!", "unit", fs.Size.Unit)
fs.Size.Dp(16)
}
fs.Size.ToDots(uc)
}
// SetDecoration sets text decoration (underline, etc),
// which uses bitflags to allow multiple combinations.
func (fs *Font) SetDecoration(deco ...TextDecorations) {
for _, d := range deco {
fs.Decoration.SetFlag(true, d)
}
}
// SetUnitContext sets the font-specific information in the given
// units.Context, based on the currently loaded face.
func (fs *Font) SetUnitContext(uc *units.Context) {
if fs.Face != nil {
uc.SetFont(fs.Face.Metrics.Em, fs.Face.Metrics.Ex, fs.Face.Metrics.Ch, uc.Dp(16))
}
}
func (fs *Font) StyleFromProperties(parent *Font, properties map[string]any, ctxt colors.Context) {
for key, val := range properties {
if len(key) == 0 {
continue
}
if key[0] == '#' || key[0] == '.' || key[0] == ':' || key[0] == '_' {
continue
}
if sfunc, ok := styleFontFuncs[key]; ok {
sfunc(fs, key, val, parent, ctxt)
}
}
}
// SetStyleProperties sets font style values based on given property map (name:
// value pairs), inheriting elements as appropriate from parent, and also
// having a default style for the "initial" setting.
func (fs *Font) SetStyleProperties(parent *Font, properties map[string]any, ctxt colors.Context) {
// direct font styling is used only for special cases -- don't do this:
// if !fs.StyleSet && parent != nil { // first time
// fs.InheritFields(parent)
// }
fs.StyleFromProperties(parent, properties, ctxt)
}
//////////////////////////////////////////////////////////////////////////////////
// Font Style enums
// TODO: should we keep FontSizePoints?
// FontSizePoints maps standard font names to standard point sizes -- we use
// dpi zoom scaling instead of rescaling "medium" font size, so generally use
// these values as-is. smaller and larger relative scaling can move in 2pt increments
var FontSizePoints = map[string]float32{
"xx-small": 7,
"x-small": 7.5,
"small": 10, // small is also "smaller"
"smallf": 10, // smallf = small font size..
"medium": 12,
"large": 14,
"x-large": 18,
"xx-large": 24,
}
// FontStyles styles of font: normal, italic, etc
type FontStyles int32 //enums:enum -trim-prefix Font -transform kebab
const (
FontNormal FontStyles = iota
// Italic indicates to make font italic
Italic
// Oblique indicates to make font slanted
Oblique
)
// FontStyleNames contains the uppercase names of all the valid font styles
// used in the regularized font names. The first name is the baseline default
// and will be omitted from font names.
var FontStyleNames = []string{"Normal", "Italic", "Oblique"}
// FontWeights are the valid names for different weights of font, with both
// the numeric and standard names given. The regularized font names in the
// font library use the names, as those are typically found in the font files.
type FontWeights int32 //enums:enum -trim-prefix Weight -transform kebab
const (
WeightNormal FontWeights = iota
Weight100
WeightThin // (Hairline)
Weight200
WeightExtraLight // (UltraLight)
Weight300
WeightLight
Weight400
Weight500
WeightMedium
Weight600
WeightSemiBold // (DemiBold)
Weight700
WeightBold
Weight800
WeightExtraBold // (UltraBold)
Weight900
WeightBlack
WeightBolder
WeightLighter
)
// FontWeightNames contains the uppercase names of all the valid font weights
// used in the regularized font names. The first name is the baseline default
// and will be omitted from font names. Order must have names that are subsets
// of other names at the end so they only match if the more specific one
// hasn't!
var FontWeightNames = []string{"Normal", "Thin", "ExtraLight", "Light", "Medium", "SemiBold", "ExtraBold", "Bold", "Black"}
// FontWeightNameValues is 1-to-1 index map from FontWeightNames to
// corresponding weight value (using more semantic term instead of numerical
// one)
var FontWeightNameValues = []FontWeights{WeightNormal, WeightThin, WeightExtraLight, WeightLight, WeightMedium, WeightSemiBold, WeightExtraBold, WeightBold, WeightBlack}
// FontWeightToNameMap maps all the style enums to canonical regularized font names
var FontWeightToNameMap = map[FontWeights]string{
Weight100: "Thin",
WeightThin: "Thin",
Weight200: "ExtraLight",
WeightExtraLight: "ExtraLight",
Weight300: "Light",
WeightLight: "Light",
Weight400: "",
WeightNormal: "",
Weight500: "Medium",
WeightMedium: "Medium",
Weight600: "SemiBold",
WeightSemiBold: "SemiBold",
Weight700: "Bold",
WeightBold: "Bold",
Weight800: "ExtraBold",
WeightExtraBold: "ExtraBold",
Weight900: "Black",
WeightBlack: "Black",
WeightBolder: "Medium", // todo: lame but assumes normal and goes one bolder
WeightLighter: "Light", // todo: lame but assumes normal and goes one lighter
}
// FontStretch are different stretch levels of font. These are less typically
// available on most platforms by default.
type FontStretch int32 //enums:enum -trim-prefix FontStr
const (
FontStrNormal FontStretch = iota
FontStrUltraCondensed
FontStrExtraCondensed
FontStrSemiCondensed
FontStrSemiExpanded
FontStrExtraExpanded
FontStrUltraExpanded
FontStrCondensed
FontStrExpanded
FontStrNarrower
FontStrWider
)
// FontStretchNames contains the uppercase names of all the valid font
// stretches used in the regularized font names. The first name is the
// baseline default and will be omitted from font names. Order must have
// names that are subsets of other names at the end so they only match if the
// more specific one hasn't! And also match the FontStretch enum.
var FontStretchNames = []string{"Normal", "UltraCondensed", "ExtraCondensed", "SemiCondensed", "SemiExpanded", "ExtraExpanded", "UltraExpanded", "Condensed", "Expanded", "Condensed", "Expanded"}
// TextDecorations are underline, line-through, etc, as bit flags
// that must be set using [Font.SetDecoration].
// Also used for additional layout hints for RuneRender.
type TextDecorations int64 //enums:bitflag -trim-prefix Deco -transform kebab
const (
DecoNone TextDecorations = iota
// Underline indicates to place a line below text
Underline
// Overline indicates to place a line above text
Overline
// LineThrough indicates to place a line through text
LineThrough
// Blink is not currently supported (and probably a bad idea generally ;)
DecoBlink
// DottedUnderline is used for abbr tag -- otherwise not a standard text-decoration option afaik
DecoDottedUnderline
// following are special case layout hints in RuneRender, to pass
// information from a styling pass to a subsequent layout pass -- they are
// NOT processed during final rendering
// DecoParaStart at start of a SpanRender indicates that it should be
// styled as the start of a new paragraph and not just the start of a new
// line
DecoParaStart
// DecoSuper indicates super-scripted text
DecoSuper
// DecoSub indicates sub-scripted text
DecoSub
// DecoBackgroundColor indicates that a bg color has been set -- for use in optimizing rendering
DecoBackgroundColor
)
// BaselineShifts are for super / sub script
type BaselineShifts int32 //enums:enum -trim-prefix Shift -transform kebab
const (
ShiftBaseline BaselineShifts = iota
ShiftSuper
ShiftSub
)
// FontVariants is just normal vs. small caps. todo: not currently supported
type FontVariants int32 //enums:enum -trim-prefix FontVar -transform kebab
const (
FontVarNormal FontVariants = iota
FontVarSmallCaps
)
// FontNameToMods parses the regularized font name and returns the appropriate
// base name and associated font mods.
func FontNameToMods(fn string) (basenm string, str FontStretch, wt FontWeights, sty FontStyles) {
basenm = fn
for mi, mod := range FontStretchNames {
spmod := " " + mod
if strings.Contains(fn, spmod) {
str = FontStretch(mi)
basenm = strings.Replace(basenm, spmod, "", 1)
break
}
}
for mi, mod := range FontWeightNames {
spmod := " " + mod
if strings.Contains(fn, spmod) {
wt = FontWeightNameValues[mi]
basenm = strings.Replace(basenm, spmod, "", 1)
break
}
}
for mi, mod := range FontStyleNames {
spmod := " " + mod
if strings.Contains(fn, spmod) {
sty = FontStyles(mi)
basenm = strings.Replace(basenm, spmod, "", 1)
break
}
}
return
}
// FontNameFromMods generates the appropriate regularized file name based on
// base name and modifiers
func FontNameFromMods(basenm string, str FontStretch, wt FontWeights, sty FontStyles) string {
fn := basenm
if str != FontStrNormal {
fn += " " + FontStretchNames[str]
}
if wt != WeightNormal && wt != Weight400 {
fn += " " + FontWeightToNameMap[wt]
}
if sty != FontNormal {
fn += " " + FontStyleNames[sty]
}
return fn
}
// FixFontMods ensures that standard font modifiers have a space in front of
// them, and that the default is not in the name -- used for regularizing font
// names.
func FixFontMods(fn string) string {
for mi, mod := range FontStretchNames {
if bi := strings.Index(fn, mod); bi > 0 {
if fn[bi-1] != ' ' {
fn = strings.Replace(fn, mod, " "+mod, 1)
}
if mi == 0 { // default, remove
fn = strings.Replace(fn, " "+mod, "", 1)
}
break // critical to break to prevent subsets from matching
}
}
for mi, mod := range FontWeightNames {
if bi := strings.Index(fn, mod); bi > 0 {
if fn[bi-1] != ' ' {
fn = strings.Replace(fn, mod, " "+mod, 1)
}
if mi == 0 { // default, remove
fn = strings.Replace(fn, " "+mod, "", 1)
}
break // critical to break to prevent subsets from matching
}
}
for mi, mod := range FontStyleNames {
if bi := strings.Index(fn, mod); bi > 0 {
if fn[bi-1] != ' ' {
fn = strings.Replace(fn, mod, " "+mod, 1)
}
if mi == 0 { // default, remove
fn = strings.Replace(fn, " "+mod, "", 1)
}
break // critical to break to prevent subsets from matching
}
}
// also get rid of Regular!
fn = strings.TrimSuffix(fn, " Regular")
fn = strings.TrimSuffix(fn, "Regular")
return fn
}
// FontRender contains all font styling information
// that is needed for SVG text rendering. It is passed to
// Paint and Style functions. It should typically not be
// used by end-user code -- see [Font] for that.
// It stores all values as pointers so that they correspond
// to the values of the style object it was derived from.
type FontRender struct { //types:add
Font
// text color (inherited)
Color image.Image
// background color (not inherited, transparent by default)
Background image.Image
// alpha value between 0 and 1 to apply to the foreground and background of this element and all of its children
Opacity float32
}
// FontRender returns the font-rendering-related
// styles of the style object as a FontRender
func (s *Style) FontRender() *FontRender {
return &FontRender{
Font: s.Font,
Color: s.Color,
// we do NOT set the BackgroundColor because the label renders its own background color
// STYTODO(kai): this might cause problems with inline span styles
Opacity: s.Opacity,
}
}
func (fr *FontRender) Defaults() {
fr.Color = colors.Scheme.OnSurface
fr.Opacity = 1
fr.Font.Defaults()
}
// InheritFields from parent
func (fr *FontRender) InheritFields(parent *FontRender) {
fr.Color = parent.Color
fr.Opacity = parent.Opacity
fr.Font.InheritFields(&parent.Font)
}
// SetStyleProperties sets font style values based on given property map (name:
// value pairs), inheriting elements as appropriate from parent, and also
// having a default style for the "initial" setting.
func (fr *FontRender) SetStyleProperties(parent *FontRender, properties map[string]any, ctxt colors.Context) {
var pfont *Font
if parent != nil {
pfont = &parent.Font
}
fr.Font.StyleFromProperties(pfont, properties, ctxt)
fr.StyleRenderFromProperties(parent, properties, ctxt)
}
func (fs *FontRender) StyleRenderFromProperties(parent *FontRender, properties map[string]any, ctxt colors.Context) {
for key, val := range properties {
if len(key) == 0 {
continue
}
if key[0] == '#' || key[0] == '.' || key[0] == ':' || key[0] == '_' {
continue
}
if sfunc, ok := styleFontRenderFuncs[key]; ok {
sfunc(fs, key, val, parent, ctxt)
}
}
}
// 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 styles
import (
"cogentcore.org/core/math32"
"golang.org/x/image/font"
)
// FontFace is our enhanced Font Face structure which contains the enhanced computed
// metrics in addition to the font.Face face
type FontFace struct { //types:add
// The full FaceName that the font is accessed by
Name string
// The integer font size in raw dots
Size int
// The system image.Font font rendering interface
Face font.Face
// enhanced metric information for the font
Metrics FontMetrics
}
// NewFontFace returns a new font face
func NewFontFace(nm string, sz int, face font.Face) *FontFace {
ff := &FontFace{Name: nm, Size: sz, Face: face}
ff.ComputeMetrics()
return ff
}
// FontMetrics are our enhanced dot-scale font metrics compared to what is available in
// the standard font.Metrics lib, including Ex and Ch being defined in terms of
// the actual letter x and 0
type FontMetrics struct { //types:add
// reference 1.0 spacing line height of font in dots -- computed from font as ascent + descent + lineGap, where lineGap is specified by the font as the recommended line spacing
Height float32
// Em size of font -- this is NOT actually the width of the letter M, but rather the specified point size of the font (in actual display dots, not points) -- it does NOT include the descender and will not fit the entire height of the font
Em float32
// Ex size of font -- this is the actual height of the letter x in the font
Ex float32
// Ch size of font -- this is the actual width of the 0 glyph in the font
Ch float32
}
// ComputeMetrics computes the Height, Em, Ex, Ch and Rem metrics associated
// with current font and overall units context
func (fs *FontFace) ComputeMetrics() {
// apd := fs.Face.Metrics().Ascent + fs.Face.Metrics().Descent
fmet := fs.Face.Metrics()
fs.Metrics.Height = math32.Ceil(math32.FromFixed(fmet.Height))
fs.Metrics.Em = float32(fs.Size) // conventional definition
xb, _, ok := fs.Face.GlyphBounds('x')
if ok {
fs.Metrics.Ex = math32.FromFixed(xb.Max.Y - xb.Min.Y)
// note: metric.Ex is typically 0?
// if fs.Metrics.Ex != metex {
// fmt.Printf("computed Ex: %v metric ex: %v\n", fs.Metrics.Ex, metex)
// }
} else {
metex := math32.FromFixed(fmet.XHeight)
if metex != 0 {
fs.Metrics.Ex = metex
} else {
fs.Metrics.Ex = 0.5 * fs.Metrics.Em
}
}
xb, _, ok = fs.Face.GlyphBounds('0')
if ok {
fs.Metrics.Ch = math32.FromFixed(xb.Max.X - xb.Min.X)
} else {
fs.Metrics.Ch = 0.5 * fs.Metrics.Em
}
}
// 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 styles
import (
"cogentcore.org/core/math32"
"cogentcore.org/core/styles/units"
)
// todo: for style
// Resize: user-resizability
// z-index
// CSS vs. Layout alignment
//
// CSS has align-self, align-items (for a container, provides a default for
// items) and align-content which only applies to lines in a flex layout (akin
// to a flow layout) -- there is a presumed horizontal aspect to these, except
// align-content, so they are subsumed in the AlignH parameter in this style.
// Vertical-align works as expected, and Text.Align uses left/center/right
// IMPORTANT: any changes here must be updated in style_properties.go StyleLayoutFuncs
// DefaultScrollbarWidth is the default [Style.ScrollbarWidth].
var DefaultScrollbarWidth = units.Dp(10)
func (s *Style) LayoutDefaults() {
s.Justify.Defaults()
s.Align.Defaults()
s.Gap.Set(units.Em(0.5))
s.ScrollbarWidth = DefaultScrollbarWidth
}
// ToDots runs ToDots on unit values, to compile down to raw pixels
func (s *Style) LayoutToDots(uc *units.Context) {
s.Pos.ToDots(uc)
s.Min.ToDots(uc)
s.Max.ToDots(uc)
s.Padding.ToDots(uc)
s.Margin.ToDots(uc)
s.Gap.ToDots(uc)
s.ScrollbarWidth.ToDots(uc)
// max must be at least as much as min
if s.Max.X.Dots > 0 {
s.Max.X.Dots = max(s.Max.X.Dots, s.Min.X.Dots)
}
if s.Max.Y.Dots > 0 {
s.Max.Y.Dots = max(s.Max.Y.Dots, s.Min.Y.Dots)
}
}
// AlignPos returns the position offset based on Align.X,Y settings
// for given inner-sized box within given outer-sized container box.
func AlignPos(align Aligns, inner, outer float32) float32 {
extra := outer - inner
var pos float32
if extra > 0 {
pos += AlignFactor(align) * extra
}
return math32.Floor(pos)
}
/////////////////////////////////////////////////////////////////
// Direction specifies the way in which elements are laid out, or
// the dimension on which an element is longer / travels in.
type Directions int32 //enums:enum -transform kebab
const (
// Row indicates that elements are laid out in a row
// or that an element is longer / travels in the x dimension.
Row Directions = iota
// Column indicates that elements are laid out in a column
// or that an element is longer / travels in the y dimension.
Column
)
// Dim returns the corresponding dimension for the direction.
func (d Directions) Dim() math32.Dims {
return math32.Dims(d)
}
// Other returns the opposite (other) direction.
func (d Directions) Other() Directions {
if d == Row {
return Column
}
return Row
}
// Displays determines how items are displayed.
type Displays int32 //enums:enum -trim-prefix Display -transform kebab
const (
// Flex is the default layout model, based on a simplified version of the
// CSS flex layout: uses MainAxis to specify the direction, Wrap for
// wrapping of elements, and Min, Max, and Grow values on elements to
// determine sizing.
Flex Displays = iota
// Stacked is a stack of elements, with one on top that is visible
Stacked
// Grid is the X, Y grid layout, with Columns specifying the number
// of elements in the X axis.
Grid
// Custom means that no automatic layout will be applied to elements,
// which can then be managed via custom code by setting the [Style.Pos] position.
Custom
// None means the item is not displayed: sets the Invisible state
DisplayNone
)
// Aligns has all different types of alignment and justification.
type Aligns int32 //enums:enum -transform kebab
const (
// Auto means the item uses the container's AlignItems value
Auto Aligns = iota
// Align items to the start (top, left) of layout
Start
// Align items to the end (bottom, right) of layout
End
// Align items centered
Center
// Align to text baselines
Baseline
// First and last are flush, equal space between remaining items
SpaceBetween
// First and last have 1/2 space at edges, full space between remaining items
SpaceAround
// Equal space at start, end, and between all items
SpaceEvenly
)
func AlignFactor(al Aligns) float32 {
switch al {
case Start:
return 0
case End:
return 1
case Center:
return 0.5
}
return 0
}
// AlignSet specifies the 3 levels of Justify or Align: Content, Items, and Self
type AlignSet struct { //types:add
// Content specifies the distribution of the entire collection of items within
// any larger amount of space allocated to the container. By contrast, Items
// and Self specify distribution within the individual element's allocated space.
Content Aligns
// Items specifies the distribution within the individual element's allocated space,
// as a default for all items within a collection.
Items Aligns
// Self specifies the distribution within the individual element's allocated space,
// for this specific item. Auto defaults to containers Items setting.
Self Aligns
}
func (as *AlignSet) Defaults() {
as.Content = Start
as.Items = Start
as.Self = Auto
}
// ItemAlign returns the effective Aligns value between parent Items and Self
func ItemAlign(parItems, self Aligns) Aligns {
if self == Auto {
return parItems
}
return self
}
// overflow type -- determines what happens when there is too much stuff in a layout
type Overflows int32 //enums:enum -trim-prefix Overflow -transform kebab
const (
// OverflowVisible makes the overflow visible, meaning that the size
// of the container is always at least the Min size of its contents.
// No scrollbars are shown.
OverflowVisible Overflows = iota
// OverflowHidden hides the overflow and doesn't present scrollbars.
OverflowHidden
// OverflowAuto automatically determines if scrollbars should be added to show
// the overflow. Scrollbars are added only if the actual content size is greater
// than the currently available size.
OverflowAuto
// OverflowScroll means that scrollbars are always visible,
// and is otherwise identical to Auto. However, only during Viewport PrefSize call,
// the actual content size is used -- otherwise it behaves just like Auto.
OverflowScroll
)
// Copyright (c) 2024, 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 styles
import (
"image"
"image/draw"
"cogentcore.org/core/math32"
"github.com/anthonynsimon/bild/transform"
)
// ObjectFits are the different ways in which a replaced element
// (image, video, etc) can be fit into its containing box.
type ObjectFits int32 //enums:enum -trim-prefix Fit -transform kebab
const (
// FitFill indicates that the replaced object will fill
// the element's entire content box, stretching if necessary.
FitFill ObjectFits = iota
// FitContain indicates that the replaced object will resize
// as large as possible while fully fitting within the element's
// content box and maintaining its aspect ratio. Therefore,
// it may not fill the entire element.
FitContain
// FitCover indicates that the replaced object will fill
// the element's entire content box, clipping if necessary.
FitCover
// FitNone indicates that the replaced object will not resize.
FitNone
// FitScaleDown indicates that the replaced object will size
// as if [FitNone] or [FitContain] was specified, using
// whichever will result in a smaller final size.
FitScaleDown
)
// ObjectSizeFromFit returns the target object size based on the given
// ObjectFits setting, original object size, and target box size
// for the object to fit into.
func ObjectSizeFromFit(fit ObjectFits, obj, box math32.Vector2) math32.Vector2 {
oar := obj.X / obj.Y
bar := box.X / box.Y
var sz math32.Vector2
switch fit {
case FitFill:
return box
case FitContain, FitScaleDown:
if oar >= bar {
// if we have a higher x:y than them, x is our limiting size
sz.X = box.X
// and we make our y in proportion to that
sz.Y = obj.Y * (box.X / obj.X)
} else {
// if we have a lower x:y than them, y is our limiting size
sz.Y = box.Y
// and we make our x in proportion to that
sz.X = obj.X * (box.Y / obj.Y)
}
case FitCover:
if oar < bar {
// if we have a lower x:y than them, x is our limiting size
sz.X = box.X
// and we make our y in proportion to that
sz.Y = obj.Y * (box.X / obj.X)
} else {
// if we have a lower x:y than them, y is our limiting size
sz.Y = box.Y
// and we make our x in proportion to that
sz.X = obj.X * (box.Y / obj.Y)
}
}
return sz
}
// ResizeImage resizes the given image according to [Style.ObjectFit]
// in an object of the given box size.
func (s *Style) ResizeImage(img image.Image, box math32.Vector2) image.Image {
obj := math32.FromPoint(img.Bounds().Size())
sz := ObjectSizeFromFit(s.ObjectFit, obj, box)
if s.ObjectFit == FitScaleDown && sz.X >= obj.X {
return img
}
rimg := transform.Resize(img, int(sz.X), int(sz.Y), transform.NearestNeighbor)
if s.ObjectFit != FitCover {
return rimg
}
// but we cap the destination size to the size of the containing object
drect := image.Rect(0, 0, int(min(sz.X, box.X)), int(min(sz.Y, box.Y)))
dst := image.NewRGBA(drect)
draw.Draw(dst, drect, rimg, image.Point{}, draw.Src)
return dst
}
// 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 styles
import (
"image"
"image/color"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles/units"
)
// Paint provides the styling parameters for SVG-style rendering
type Paint struct { //types:add
// prop: display:none -- node and everything below it are off, non-rendering
Off bool
// todo big enum of how to display item -- controls layout etc
Display bool
// stroke (line drawing) parameters
StrokeStyle Stroke
// fill (region filling) parameters
FillStyle Fill
// font also has global opacity setting, along with generic color, background-color settings, which can be copied into stroke / fill as needed
FontStyle FontRender
// TextStyle has the text styling settings.
TextStyle Text
// various rendering special effects settings
VectorEffect VectorEffects
// our additions to transform -- pushed to render state
Transform math32.Matrix2
// unit context -- parameters necessary for anchoring relative units
UnitContext units.Context
// have the styles already been set?
StyleSet bool
PropertiesNil bool
dotsSet bool
lastUnCtxt units.Context
}
func (pc *Paint) Defaults() {
pc.Off = false
pc.Display = true
pc.StyleSet = false
pc.StrokeStyle.Defaults()
pc.FillStyle.Defaults()
pc.FontStyle.Defaults()
pc.TextStyle.Defaults()
pc.Transform = math32.Identity2()
}
// CopyStyleFrom copies styles from another paint
func (pc *Paint) CopyStyleFrom(cp *Paint) {
pc.Off = cp.Off
pc.Display = cp.Display
pc.UnitContext = cp.UnitContext
pc.StrokeStyle = cp.StrokeStyle
pc.FillStyle = cp.FillStyle
pc.FontStyle = cp.FontStyle
pc.TextStyle = cp.TextStyle
pc.VectorEffect = cp.VectorEffect
}
// InheritFields from parent
func (pc *Paint) InheritFields(parent *Paint) {
pc.FontStyle.InheritFields(&parent.FontStyle)
pc.TextStyle.InheritFields(&parent.TextStyle)
}
// SetStyleProperties sets paint values based on given property map (name: value
// pairs), inheriting elements as appropriate from parent, and also having a
// default style for the "initial" setting
func (pc *Paint) SetStyleProperties(parent *Paint, properties map[string]any, ctxt colors.Context) {
if !pc.StyleSet && parent != nil { // first time
pc.InheritFields(parent)
}
pc.styleFromProperties(parent, properties, ctxt)
pc.PropertiesNil = (len(properties) == 0)
pc.StyleSet = true
}
func (pc *Paint) FromStyle(st *Style) {
pc.UnitContext = st.UnitContext
pc.FontStyle = *st.FontRender()
pc.TextStyle = st.Text
}
// ToDotsImpl runs ToDots on unit values, to compile down to raw pixels
func (pc *Paint) ToDotsImpl(uc *units.Context) {
pc.StrokeStyle.ToDots(uc)
pc.FillStyle.ToDots(uc)
pc.FontStyle.ToDots(uc)
pc.TextStyle.ToDots(uc)
}
// SetUnitContextExt sets the unit context for external usage of paint
// outside of Core Scene context, based on overall size of painting canvas.
// caches everything out in terms of raw pixel dots for rendering
// call at start of render.
func (pc *Paint) SetUnitContextExt(size image.Point) {
if pc.UnitContext.DPI == 0 {
pc.UnitContext.Defaults()
}
// TODO: maybe should have different values for these sizes?
pc.UnitContext.SetSizes(float32(size.X), float32(size.Y), float32(size.X), float32(size.Y), float32(size.X), float32(size.Y))
pc.FontStyle.SetUnitContext(&pc.UnitContext)
pc.ToDotsImpl(&pc.UnitContext)
pc.dotsSet = true
}
// ToDots runs ToDots on unit values, to compile down to raw pixels
func (pc *Paint) ToDots() {
if !(pc.dotsSet && pc.UnitContext == pc.lastUnCtxt && pc.PropertiesNil) {
pc.ToDotsImpl(&pc.UnitContext)
pc.dotsSet = true
pc.lastUnCtxt = pc.UnitContext
}
}
type FillRules int32 //enums:enum -trim-prefix FillRule -transform lower
const (
FillRuleNonZero FillRules = iota
FillRuleEvenOdd
)
// VectorEffects contains special effects for rendering
type VectorEffects int32 //enums:enum -trim-prefix VectorEffect -transform kebab
const (
VectorEffectNone VectorEffects = iota
// VectorEffectNonScalingStroke means that the stroke width is not affected by
// transform properties
VectorEffectNonScalingStroke
)
// IMPORTANT: any changes here must be updated below in StyleFillFuncs
// Fill contains all the properties for filling a region
type Fill struct {
// fill color image specification; filling is off if nil
Color image.Image
// global alpha opacity / transparency factor between 0 and 1
Opacity float32
// rule for how to fill more complex shapes with crossing lines
Rule FillRules
}
// Defaults initializes default values for paint fill
func (pf *Fill) Defaults() {
pf.Color = colors.Uniform(color.Black)
pf.Rule = FillRuleNonZero
pf.Opacity = 1.0
}
// ToDots runs ToDots on unit values, to compile down to raw pixels
func (fs *Fill) ToDots(uc *units.Context) {
}
////////////////////////////////////////////////////////////////////////////////////
// Stroke
// end-cap of a line: stroke-linecap property in SVG
type LineCaps int32 //enums:enum -trim-prefix LineCap -transform kebab
const (
// LineCapButt indicates to draw no line caps; it draws a
// line with the length of the specified length.
LineCapButt LineCaps = iota
// LineCapRound indicates to draw a semicircle on each line
// end with a diameter of the stroke width.
LineCapRound
// LineCapSquare indicates to draw a rectangle on each line end
// with a height of the stroke width and a width of half of the
// stroke width.
LineCapSquare
// LineCapCubic is a rasterx extension
LineCapCubic
// LineCapQuadratic is a rasterx extension
LineCapQuadratic
)
// the way in which lines are joined together: stroke-linejoin property in SVG
type LineJoins int32 //enums:enum -trim-prefix LineJoin -transform kebab
const (
LineJoinMiter LineJoins = iota
LineJoinMiterClip
LineJoinRound
LineJoinBevel
LineJoinArcs
// rasterx extension
LineJoinArcsClip
)
// IMPORTANT: any changes here must be updated below in StyleStrokeFuncs
// Stroke contains all the properties for painting a line
type Stroke struct {
// stroke color image specification; stroking is off if nil
Color image.Image
// global alpha opacity / transparency factor between 0 and 1
Opacity float32
// line width
Width units.Value
// 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
// how to draw the end cap of lines
Cap LineCaps
// how to join line segments
Join LineJoins
// limit of how far to miter -- must be 1 or larger
MiterLimit float32 `min:"1"`
}
// Defaults initializes default values for paint stroke
func (ss *Stroke) Defaults() {
// stroking is off by default in svg
ss.Color = nil
ss.Width.Dp(1)
ss.MinWidth.Dot(.5)
ss.Cap = LineCapButt
ss.Join = LineJoinMiter // Miter not yet supported, but that is the default -- falls back on bevel
ss.MiterLimit = 10.0
ss.Opacity = 1.0
}
// 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.
func (ss *Stroke) ApplyBorderStyle(bs BorderStyles) {
switch bs {
case BorderNone:
ss.Color = nil
case BorderDotted:
ss.Dashes = []float32{0, 12}
ss.Cap = LineCapRound
case BorderDashed:
ss.Dashes = []float32{8, 6}
}
}
// 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 styles
import (
"log"
"strconv"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/enums"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles/units"
)
////////////////////////////////////////////////////////////////////////////
// Styling functions for setting from properties
// see style_properties.go for master version
// styleFromProperties sets style field values based on map[string]any properties
func (pc *Paint) styleFromProperties(parent *Paint, properties map[string]any, cc colors.Context) {
for key, val := range properties {
if len(key) == 0 {
continue
}
if key[0] == '#' || key[0] == '.' || key[0] == ':' || key[0] == '_' {
continue
}
if key == "display" {
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
pc.Display = parent.Display
} else if init {
pc.Display = true
}
return
}
sval := reflectx.ToString(val)
switch sval {
case "none":
pc.Display = false
case "inline":
pc.Display = true
default:
pc.Display = true
}
continue
}
if sfunc, ok := styleStrokeFuncs[key]; ok {
if parent != nil {
sfunc(&pc.StrokeStyle, key, val, &parent.StrokeStyle, cc)
} else {
sfunc(&pc.StrokeStyle, key, val, nil, cc)
}
continue
}
if sfunc, ok := styleFillFuncs[key]; ok {
if parent != nil {
sfunc(&pc.FillStyle, key, val, &parent.FillStyle, cc)
} else {
sfunc(&pc.FillStyle, key, val, nil, cc)
}
continue
}
if sfunc, ok := styleFontFuncs[key]; ok {
if parent != nil {
sfunc(&pc.FontStyle.Font, key, val, &parent.FontStyle.Font, cc)
} else {
sfunc(&pc.FontStyle.Font, key, val, nil, cc)
}
continue
}
if sfunc, ok := styleFontRenderFuncs[key]; ok {
if parent != nil {
sfunc(&pc.FontStyle, key, val, &parent.FontStyle, cc)
} else {
sfunc(&pc.FontStyle, key, val, nil, cc)
}
continue
}
if sfunc, ok := styleTextFuncs[key]; ok {
if parent != nil {
sfunc(&pc.TextStyle, key, val, &parent.TextStyle, cc)
} else {
sfunc(&pc.TextStyle, key, val, nil, cc)
}
continue
}
if sfunc, ok := stylePaintFuncs[key]; ok {
sfunc(pc, key, val, parent, cc)
continue
}
}
}
/////////////////////////////////////////////////////////////////////////////////
// Stroke
// styleStrokeFuncs are functions for styling the Stroke object
var styleStrokeFuncs = map[string]styleFunc{
"stroke": func(obj any, key string, val any, parent any, cc colors.Context) {
fs := obj.(*Stroke)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
fs.Color = parent.(*Stroke).Color
} else if init {
fs.Color = colors.Uniform(colors.Black)
}
return
}
fs.Color = errors.Log1(gradient.FromAny(val, cc))
},
"stroke-opacity": styleFuncFloat(float32(1),
func(obj *Stroke) *float32 { return &(obj.Opacity) }),
"stroke-width": styleFuncUnits(units.Dp(1),
func(obj *Stroke) *units.Value { return &(obj.Width) }),
"stroke-min-width": styleFuncUnits(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 := styleInhInit(val, parent); inh || init {
if inh {
fs.Dashes = parent.(*Stroke).Dashes
} else if init {
fs.Dashes = nil
}
return
}
switch vt := val.(type) {
case string:
fs.Dashes = parseDashesString(vt)
case []float32:
math32.CopyFloat32s(&fs.Dashes, vt)
case *[]float32:
math32.CopyFloat32s(&fs.Dashes, *vt)
}
},
"stroke-linecap": styleFuncEnum(LineCapButt,
func(obj *Stroke) enums.EnumSetter { return &(obj.Cap) }),
"stroke-linejoin": styleFuncEnum(LineJoinMiter,
func(obj *Stroke) enums.EnumSetter { return &(obj.Join) }),
"stroke-miterlimit": styleFuncFloat(float32(1),
func(obj *Stroke) *float32 { return &(obj.MiterLimit) }),
}
/////////////////////////////////////////////////////////////////////////////////
// Fill
// styleFillFuncs are functions for styling the Fill object
var styleFillFuncs = map[string]styleFunc{
"fill": func(obj any, key string, val any, parent any, cc colors.Context) {
fs := obj.(*Fill)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
fs.Color = parent.(*Fill).Color
} else if init {
fs.Color = colors.Uniform(colors.Black)
}
return
}
fs.Color = errors.Log1(gradient.FromAny(val, cc))
},
"fill-opacity": styleFuncFloat(float32(1),
func(obj *Fill) *float32 { return &(obj.Opacity) }),
"fill-rule": styleFuncEnum(FillRuleNonZero,
func(obj *Fill) enums.EnumSetter { return &(obj.Rule) }),
}
/////////////////////////////////////////////////////////////////////////////////
// Paint
// stylePaintFuncs are functions for styling the Stroke object
var stylePaintFuncs = map[string]styleFunc{
"vector-effect": styleFuncEnum(VectorEffectNone,
func(obj *Paint) enums.EnumSetter { return &(obj.VectorEffect) }),
"transform": func(obj any, key string, val any, parent any, cc colors.Context) {
pc := obj.(*Paint)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
pc.Transform = parent.(*Paint).Transform
} else if init {
pc.Transform = math32.Identity2()
}
return
}
switch vt := val.(type) {
case string:
pc.Transform.SetString(vt)
case *math32.Matrix2:
pc.Transform = *vt
case math32.Matrix2:
pc.Transform = vt
}
},
}
// parseDashesString gets a dash slice from given string
func parseDashesString(str string) []float32 {
if len(str) == 0 || str == "none" {
return nil
}
ds := strings.Split(str, ",")
dl := make([]float32, len(ds))
for i, dstr := range ds {
d, err := strconv.ParseFloat(strings.TrimSpace(dstr), 32)
if err != nil {
log.Printf("core.ParseDashesString parsing error: %v\n", err)
return nil
}
dl[i] = float32(d)
}
return dl
}
// 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 styles
import (
"fmt"
"image/color"
"strings"
"log/slog"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles/units"
)
// SideIndexes provides names for the Sides in order defined
type SideIndexes int32 //enums:enum
const (
Top SideIndexes = iota
Right
Bottom
Left
)
// Sides contains values for each side or corner of a box.
// If Sides contains sides, the struct field names correspond
// directly to the side values (ie: Top = top side value).
// If Sides contains corners, the struct field names correspond
// to the corners as follows: Top = top left, Right = top right,
// Bottom = bottom right, Left = bottom left.
type Sides[T any] struct { //types:add
// top/top-left value
Top T
// right/top-right value
Right T
// bottom/bottom-right value
Bottom T
// left/bottom-left value
Left T
}
// NewSides is a helper that creates new sides/corners of the given type
// and calls Set on them with the given values.
func NewSides[T any](vals ...T) *Sides[T] {
return (&Sides[T]{}).Set(vals...)
}
// Set sets the values of the sides/corners from the given list of 0 to 4 values.
// If 0 values are provided, all sides/corners are set to the zero value of the type.
// If 1 value is provided, all sides/corners are set to that value.
// If 2 values are provided, the top/top-left and bottom/bottom-right are set to the first value
// and the right/top-right and left/bottom-left are set to the second value.
// If 3 values are provided, the top/top-left is set to the first value,
// the right/top-right and left/bottom-left are set to the second value,
// and the bottom/bottom-right is set to the third value.
// If 4 values are provided, the top/top-left is set to the first value,
// the right/top-right is set to the second value, the bottom/bottom-right is set
// to the third value, and the left/bottom-left is set to the fourth value.
// If more than 4 values are provided, the behavior is the same
// as with 4 values, but Set also logs a programmer error.
// This behavior is based on the CSS multi-side/corner setting syntax,
// like that with padding and border-radius (see https://www.w3schools.com/css/css_padding.asp
// and https://www.w3schools.com/cssref/css3_pr_border-radius.php)
func (s *Sides[T]) Set(vals ...T) *Sides[T] {
switch len(vals) {
case 0:
var zval T
s.SetAll(zval)
case 1:
s.SetAll(vals[0])
case 2:
s.SetVertical(vals[0])
s.SetHorizontal(vals[1])
case 3:
s.Top = vals[0]
s.SetHorizontal(vals[1])
s.Bottom = vals[2]
case 4:
s.Top = vals[0]
s.Right = vals[1]
s.Bottom = vals[2]
s.Left = vals[3]
default:
s.Top = vals[0]
s.Right = vals[1]
s.Bottom = vals[2]
s.Left = vals[3]
slog.Error("programmer error: sides.Set: expected 0 to 4 values, but got", "numValues", len(vals))
}
return s
}
// Zero sets the values of all of the sides to zero.
func (s *Sides[T]) Zero() *Sides[T] {
s.Set()
return s
}
// SetVertical sets the values for the sides/corners in the
// vertical/diagonally descending direction
// (top/top-left and bottom/bottom-right) to the given value
func (s *Sides[T]) SetVertical(val T) *Sides[T] {
s.Top = val
s.Bottom = val
return s
}
// SetHorizontal sets the values for the sides/corners in the
// horizontal/diagonally ascending direction
// (right/top-right and left/bottom-left) to the given value
func (s *Sides[T]) SetHorizontal(val T) *Sides[T] {
s.Right = val
s.Left = val
return s
}
// SetAll sets the values for all of the sides/corners
// to the given value
func (s *Sides[T]) SetAll(val T) *Sides[T] {
s.Top = val
s.Right = val
s.Bottom = val
s.Left = val
return s
}
// SetTop sets the top side to the given value
func (s *Sides[T]) SetTop(top T) *Sides[T] {
s.Top = top
return s
}
// SetRight sets the right side to the given value
func (s *Sides[T]) SetRight(right T) *Sides[T] {
s.Right = right
return s
}
// SetBottom sets the bottom side to the given value
func (s *Sides[T]) SetBottom(bottom T) *Sides[T] {
s.Bottom = bottom
return s
}
// SetLeft sets the left side to the given value
func (s *Sides[T]) SetLeft(left T) *Sides[T] {
s.Left = left
return s
}
// SetAny sets the sides/corners from the given value of any type
func (s *Sides[T]) SetAny(a any) error {
switch val := a.(type) {
case Sides[T]:
*s = val
case *Sides[T]:
*s = *val
case T:
s.SetAll(val)
case *T:
s.SetAll(*val)
case []T:
s.Set(val...)
case *[]T:
s.Set(*val...)
case string:
return s.SetString(val)
default:
return s.SetString(fmt.Sprint(val))
}
return nil
}
// SetString sets the sides/corners from the given string value
func (s *Sides[T]) SetString(str string) error {
fields := strings.Fields(str)
vals := make([]T, len(fields))
for i, field := range fields {
ss, ok := any(&vals[i]).(reflectx.SetStringer)
if !ok {
err := fmt.Errorf("(Sides).SetString('%s'): to set from a string, the sides type (%T) must implement reflectx.SetStringer (needs SetString(str string) error function)", str, s)
slog.Error(err.Error())
return err
}
err := ss.SetString(field)
if err != nil {
nerr := fmt.Errorf("(Sides).SetString('%s'): error setting sides of type %T from string: %w", str, s, err)
slog.Error(nerr.Error())
return nerr
}
}
s.Set(vals...)
return nil
}
// SidesAreSame returns whether all of the sides/corners are the same
func SidesAreSame[T comparable](s Sides[T]) bool {
return s.Right == s.Top && s.Bottom == s.Top && s.Left == s.Top
}
// SidesAreZero returns whether all of the sides/corners are equal to zero
func SidesAreZero[T comparable](s Sides[T]) bool {
var zv T
return s.Top == zv && s.Right == zv && s.Bottom == zv && s.Left == zv
}
// SideValues contains units.Value values for each side/corner of a box
type SideValues struct { //types:add
Sides[units.Value]
}
// NewSideValues is a helper that creates new side/corner values
// and calls Set on them with the given values.
func NewSideValues(vals ...units.Value) SideValues {
sides := Sides[units.Value]{}
sides.Set(vals...)
return SideValues{sides}
}
// ToDots converts the values for each of the sides/corners
// to raw display pixels (dots) and sets the Dots field for each
// of the values. It returns the dot values as a SideFloats.
func (sv *SideValues) ToDots(uc *units.Context) SideFloats {
return NewSideFloats(
sv.Top.ToDots(uc),
sv.Right.ToDots(uc),
sv.Bottom.ToDots(uc),
sv.Left.ToDots(uc),
)
}
// Dots returns the dot values of the sides/corners as a SideFloats.
// It does not compute them; see ToDots for that.
func (sv SideValues) Dots() SideFloats {
return NewSideFloats(
sv.Top.Dots,
sv.Right.Dots,
sv.Bottom.Dots,
sv.Left.Dots,
)
}
// SideFloats contains float32 values for each side/corner of a box
type SideFloats struct { //types:add
Sides[float32]
}
// NewSideFloats is a helper that creates new side/corner floats
// and calls Set on them with the given values.
func NewSideFloats(vals ...float32) SideFloats {
sides := Sides[float32]{}
sides.Set(vals...)
return SideFloats{sides}
}
// Add adds the side floats to the
// other side floats and returns the result
func (sf SideFloats) Add(other SideFloats) SideFloats {
return NewSideFloats(
sf.Top+other.Top,
sf.Right+other.Right,
sf.Bottom+other.Bottom,
sf.Left+other.Left,
)
}
// Sub subtracts the other side floats from
// the side floats and returns the result
func (sf SideFloats) Sub(other SideFloats) SideFloats {
return NewSideFloats(
sf.Top-other.Top,
sf.Right-other.Right,
sf.Bottom-other.Bottom,
sf.Left-other.Left,
)
}
// MulScalar multiplies each side by the given scalar value
// and returns the result.
func (sf SideFloats) MulScalar(s float32) SideFloats {
return NewSideFloats(
sf.Top*s,
sf.Right*s,
sf.Bottom*s,
sf.Left*s,
)
}
// Min returns a new side floats containing the
// minimum values of the two side floats
func (sf SideFloats) Min(other SideFloats) SideFloats {
return NewSideFloats(
math32.Min(sf.Top, other.Top),
math32.Min(sf.Right, other.Right),
math32.Min(sf.Bottom, other.Bottom),
math32.Min(sf.Left, other.Left),
)
}
// Max returns a new side floats containing the
// maximum values of the two side floats
func (sf SideFloats) Max(other SideFloats) SideFloats {
return NewSideFloats(
math32.Max(sf.Top, other.Top),
math32.Max(sf.Right, other.Right),
math32.Max(sf.Bottom, other.Bottom),
math32.Max(sf.Left, other.Left),
)
}
// Round returns a new side floats with each side value
// rounded to the nearest whole number.
func (sf SideFloats) Round() SideFloats {
return NewSideFloats(
math32.Round(sf.Top),
math32.Round(sf.Right),
math32.Round(sf.Bottom),
math32.Round(sf.Left),
)
}
// Pos returns the position offset casued by the side/corner values (Left, Top)
func (sf SideFloats) Pos() math32.Vector2 {
return math32.Vec2(sf.Left, sf.Top)
}
// Size returns the toal size the side/corner values take up (Left + Right, Top + Bottom)
func (sf SideFloats) Size() math32.Vector2 {
return math32.Vec2(sf.Left+sf.Right, sf.Top+sf.Bottom)
}
// ToValues returns the side floats a
// SideValues composed of [units.UnitDot] values
func (sf SideFloats) ToValues() SideValues {
return NewSideValues(
units.Dot(sf.Top),
units.Dot(sf.Right),
units.Dot(sf.Bottom),
units.Dot(sf.Left),
)
}
// SideColors contains color values for each side/corner of a box
type SideColors struct { //types:add
Sides[color.RGBA]
}
// NewSideColors is a helper that creates new side/corner colors
// and calls Set on them with the given values.
// It does not return any error values and just logs them.
func NewSideColors(vals ...color.RGBA) SideColors {
sides := Sides[color.RGBA]{}
sides.Set(vals...)
return SideColors{sides}
}
// SetAny sets the sides/corners from the given value of any type
func (s *SideColors) SetAny(a any, base color.Color) error {
switch val := a.(type) {
case Sides[color.RGBA]:
s.Sides = val
case *Sides[color.RGBA]:
s.Sides = *val
case color.RGBA:
s.SetAll(val)
case *color.RGBA:
s.SetAll(*val)
case []color.RGBA:
s.Set(val...)
case *[]color.RGBA:
s.Set(*val...)
case string:
return s.SetString(val, base)
default:
return s.SetString(fmt.Sprint(val), base)
}
return nil
}
// SetString sets the sides/corners from the given string value
func (s *SideColors) SetString(str string, base color.Color) error {
fields := strings.Fields(str)
vals := make([]color.RGBA, len(fields))
for i, field := range fields {
clr, err := colors.FromString(field, base)
if err != nil {
nerr := fmt.Errorf("(SideColors).SetString('%s'): error setting sides of type %T from string: %w", str, s, err)
slog.Error(nerr.Error())
return nerr
}
vals[i] = clr
}
s.Set(vals...)
return nil
}
// Code generated by "core generate"; DO NOT EDIT.
package states
import (
"cogentcore.org/core/enums"
)
var _StatesValues = []States{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}
// StatesN is the highest valid value for type States, plus one.
const StatesN States = 14
var _StatesValueMap = map[string]States{`Invisible`: 0, `Disabled`: 1, `ReadOnly`: 2, `Selected`: 3, `Active`: 4, `Dragging`: 5, `Sliding`: 6, `Focused`: 7, `Checked`: 8, `Indeterminate`: 9, `Hovered`: 10, `LongHovered`: 11, `LongPressed`: 12, `DragHovered`: 13}
var _StatesDescMap = map[States]string{0: `Invisible elements are not displayed, and thus do not present a target for GUI events. It is identical to css display:none. This can also be set when the item is out of visible display as in scrolling or collapsing elements. 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.`, 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.`, 8: `Checked is for check boxes or radio buttons or other similar state.`, 9: `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.`, 10: `Hovered indicates that a mouse pointer has entered the space over an element, but it is not [Active] (nor [DragHovered]).`, 11: `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.`, 12: `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.`, 13: `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: `Checked`, 9: `Indeterminate`, 10: `Hovered`, 11: `LongHovered`, 12: `LongPressed`, 13: `DragHovered`}
// String returns the string representation of this States value.
func (i States) String() string { return enums.BitFlagString(i, _StatesValues) }
// BitIndexString returns the string representation of this States value
// if it is a bit index value (typically an enum constant), and
// not an actual bit flag value.
func (i States) BitIndexString() string { return enums.String(i, _StatesMap) }
// SetString sets the States value from its string representation,
// and returns an error if the string is invalid.
func (i *States) SetString(s string) error { *i = 0; return i.SetStringOr(s) }
// SetStringOr sets the States value from its string representation
// while preserving any bit flags already set, and returns an
// error if the string is invalid.
func (i *States) SetStringOr(s string) error {
return enums.SetStringOr(i, s, _StatesValueMap, "States")
}
// Int64 returns the States value as an int64.
func (i States) Int64() int64 { return int64(i) }
// SetInt64 sets the States value from an int64.
func (i *States) SetInt64(in int64) { *i = States(in) }
// Desc returns the description of the States value.
func (i States) Desc() string { return enums.Desc(i, _StatesDescMap) }
// StatesValues returns all possible values for the type States.
func StatesValues() []States { return _StatesValues }
// Values returns all possible values for the type States.
func (i States) Values() []enums.Enum { return enums.Values(_StatesValues) }
// HasFlag returns whether these bit flags have the given bit flag set.
func (i *States) HasFlag(f enums.BitFlag) bool { return enums.HasFlag((*int64)(i), f) }
// SetFlag sets the value of the given flags in these flags to the given value.
func (i *States) SetFlag(on bool, f ...enums.BitFlag) { enums.SetFlag((*int64)(i), on, f...) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i States) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *States) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "States") }
// 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 states
//go:generate core generate
import "cogentcore.org/core/enums"
// States are GUI states of elements that are relevant for styling based on
// CSS pseudo-classes (https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes).
type States int64 //enums:bitflag
const (
// Invisible elements are not displayed, and thus do not present
// a target for GUI events. It is identical to css display:none.
// This can also be set when the item is out of visible display
// as in scrolling or collapsing elements.
// 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.
Invisible States = iota
// Disabled elements cannot be interacted with or selected,
// but do display.
Disabled
// 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.
ReadOnly
// Selected elements have been marked for clipboard or other such actions.
Selected
// 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.
Active
// 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.
Dragging
// 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].
Sliding
// Focused elements receive keyboard input.
Focused
// Checked is for check boxes or radio buttons or other similar state.
Checked
// 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.
Indeterminate
// Hovered indicates that a mouse pointer has entered the space over
// an element, but it is not [Active] (nor [DragHovered]).
Hovered
// 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.
LongHovered
// 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.
LongPressed
// 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.
DragHovered
)
// Is is a shortcut for HasFlag for States
func (st States) Is(flag enums.BitFlag) bool {
return st.HasFlag(flag)
}
// StateLayer returns the state layer opacity for the state, appropriate for use
// as the value of [cogentcore.org/core/styles.Style.StateLayer]
func (st States) StateLayer() float32 {
switch {
case st.Is(Disabled):
return 0
case st.Is(Dragging), st.Is(LongPressed):
return 0.12
case st.Is(Active), st.Is(Focused):
return 0.10
case st.Is(Hovered), st.Is(DragHovered):
return 0.08
default:
return 0
}
}
// 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 styles provides style objects containing style properties
// used for GUI widgets and other rendering contexts.
package styles
//go:generate core generate
import (
"fmt"
"image"
"image/color"
"log/slog"
"strings"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/cursors"
"cogentcore.org/core/enums"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
)
// IMPORTANT: any changes here must be updated in style_properties.go StyleStyleFuncs
// and likewise for all sub-styles as fields here.
// Style contains all of the style properties used for GUI widgets.
type Style struct { //types:add
// State holds style-relevant state flags, for convenient styling access,
// given that styles typically depend on element states.
State states.States
// Abilities specifies the abilities of this element, which determine
// which kinds of states the element can express.
// This is used by the system/events system. Putting this info next
// to the State info makes it easy to configure and manage.
Abilities abilities.Abilities
// the cursor to switch to upon hovering over the element (inherited)
Cursor cursors.Cursor
// Padding is the transparent space around central content of box,
// which is _included_ in the size of the standard box rendering.
Padding SideValues `display:"inline"`
// Margin is the outer-most transparent space around box element,
// which is _excluded_ from standard box rendering.
Margin SideValues `display:"inline"`
// Display controls how items are displayed, in terms of layout
Display Displays
// Direction specifies the way in which elements are laid out, or
// the dimension on which an element is longer / travels in.
Direction Directions
// Wrap causes elements to wrap around in the CrossAxis dimension
// to fit within sizing constraints.
Wrap bool
// Justify specifies the distribution of elements along the main axis,
// i.e., the same as Direction, for Flex Display. For Grid, the main axis is
// given by the writing direction (e.g., Row-wise for latin based languages).
Justify AlignSet `display:"inline"`
// Align specifies the cross-axis alignment of elements, orthogonal to the
// main Direction axis. For Grid, the cross-axis is orthogonal to the
// writing direction (e.g., Column-wise for latin based languages).
Align AlignSet `display:"inline"`
// Min is the minimum size of the actual content, exclusive of additional space
// from padding, border, margin; 0 = default is sum of Min for all content
// (which _includes_ space for all sub-elements).
// This is equivalent to the Basis for the CSS flex styling model.
Min units.XY `display:"inline"`
// Max is the maximum size of the actual content, exclusive of additional space
// from padding, border, margin; 0 = default provides no Max size constraint
Max units.XY `display:"inline"`
// Grow is the proportional amount that the element can grow (stretch)
// if there is more space available. 0 = default = no growth.
// Extra available space is allocated as: Grow / sum (all Grow).
// Important: grow elements absorb available space and thus are not
// subject to alignment (Center, End).
Grow math32.Vector2
// GrowWrap is a special case for Text elements where it grows initially
// in the horizontal axis to allow for longer, word wrapped text to fill
// the available space, but then it does not grow thereafter, so that alignment
// operations still work (Grow elements do not align because they absorb all
// available space). Do NOT set this for non-Text elements.
GrowWrap bool
// RenderBox determines whether to render the standard box model for the element.
// This is typically necessary for most elements and helps prevent text, border,
// and box shadow from rendering over themselves. Therefore, it should be kept at
// its default value of true in most circumstances, but it can be set to false
// when the element is fully managed by something that is guaranteed to render the
// appropriate background color and/or border for the element.
RenderBox bool
// FillMargin determines is whether to fill the margin with
// the surrounding background color before rendering the element itself.
// This is typically necessary to prevent text, border, and box shadow from
// rendering over themselves. Therefore, it should be kept at its default value
// of true in most circumstances, but it can be set to false when the element
// is fully managed by something that is guaranteed to render the
// appropriate background color for the element. It is irrelevant if RenderBox
// is false.
FillMargin bool
// Overflow determines how to handle overflowing content in a layout.
// Default is OverflowVisible. Set to OverflowAuto to enable scrollbars.
Overflow XY[Overflows]
// For layouts, extra space added between elements in the layout.
Gap units.XY `display:"inline"`
// For grid layouts, the number of columns to use.
// If > 0, number of rows is computed as N elements / Columns.
// Used as a constraint in layout if individual elements
// do not specify their row, column positions
Columns int
// If this object is a replaced object (image, video, etc)
// or has a background image, ObjectFit specifies the way
// in which the replaced object should be fit into the element.
ObjectFit ObjectFits
// If this object is a replaced object (image, video, etc)
// or has a background image, ObjectPosition specifies the
// X,Y position of the object within the space allocated for
// the object (see ObjectFit).
ObjectPosition units.XY
// Border is a rendered border around the element.
Border Border
// MaxBorder is the largest border that will ever be rendered
// around the element, the size of which is used for computing
// the effective margin to allocate for the element.
MaxBorder Border
// BoxShadow is the box shadows to render around box (can have multiple)
BoxShadow []Shadow
// MaxBoxShadow contains the largest shadows that will ever be rendered
// around the element, the size of which are used for computing the
// effective margin to allocate for the element.
MaxBoxShadow []Shadow
// Color specifies the text / content color, and it is inherited.
Color image.Image
// Background specifies the background of the element. It is not inherited,
// and it is nil (transparent) by default.
Background image.Image
// alpha value between 0 and 1 to apply to the foreground and background of this element and all of its children
Opacity float32
// StateLayer, if above zero, indicates to create a state layer over the element with this much opacity (on a scale of 0-1) and the
// color Color (or StateColor if it defined). It is automatically set based on State, but can be overridden in stylers.
StateLayer float32
// StateColor, if not nil, is the color to use for the StateLayer instead of Color. If you want to disable state layers
// for an element, do not use this; instead, set StateLayer to 0.
StateColor image.Image
// ActualBackground is the computed actual background rendered for the element,
// taking into account its Background, Opacity, StateLayer, and parent
// ActualBackground. It is automatically computed and should not be set manually.
ActualBackground image.Image
// VirtualKeyboard is the virtual keyboard to display, if any,
// on mobile platforms when this element is focused. It is not
// used if the element is read only.
VirtualKeyboard VirtualKeyboards
// Pos is used for the position of the widget if the parent frame
// has [Style.Display] = [Custom].
Pos units.XY `display:"inline"`
// ordering factor for rendering depth -- lower numbers rendered first.
// Sort children according to this factor
ZIndex int
// specifies the row that this element should appear within a grid layout
Row int
// specifies the column that this element should appear within a grid layout
Col int
// specifies the number of sequential rows that this element should occupy
// within a grid layout (todo: not currently supported)
RowSpan int
// specifies the number of sequential columns that this element should occupy
// within a grid layout
ColSpan int
// ScrollbarWidth is the width of layout scrollbars. It defaults
// to [DefaultScrollbarWidth], and it is inherited.
ScrollbarWidth units.Value
// font styling parameters
Font Font
// text styling parameters
Text Text
// unit context: parameters necessary for anchoring relative units
UnitContext units.Context
}
func (s *Style) Defaults() {
// mostly all the defaults are 0 initial values, except these..
s.UnitContext.Defaults()
s.LayoutDefaults()
s.Color = colors.Scheme.OnSurface
s.Border.Color.Set(colors.Scheme.Outline)
s.Opacity = 1
s.RenderBox = true
s.FillMargin = true
s.Font.Defaults()
s.Text.Defaults()
}
// VirtualKeyboards are all of the supported virtual keyboard types
// to display on mobile platforms.
type VirtualKeyboards int32 //enums:enum -trim-prefix Keyboard -transform kebab
const (
// KeyboardNone indicates to display no virtual keyboard.
KeyboardNone VirtualKeyboards = iota
// KeyboardSingleLine indicates to display a virtual keyboard
// with a default input style and a "Done" return key.
KeyboardSingleLine
// KeyboardMultiLine indicates to display a virtual keyboard
// with a default input style and a "Return" return key.
KeyboardMultiLine
// KeyboardNumber indicates to display a virtual keyboard
// for inputting a number.
KeyboardNumber
// KeyboardPassword indicates to display a virtual keyboard
// for inputting a password.
KeyboardPassword
// KeyboardEmail indicates to display a virtual keyboard
// for inputting an email address.
KeyboardEmail
// KeyboardPhone indicates to display a virtual keyboard
// for inputting a phone number.
KeyboardPhone
// KeyboardURL indicates to display a virtual keyboard for
// inputting a URL / URI / web address.
KeyboardURL
)
// todo: Animation
// Clear -- no floating elements
// Clip -- clip images
// column- settings -- lots of those
// List-style for lists
// Object-fit for videos
// visibility -- support more than just hidden
// transition -- animation of hover, etc
// SetStylePropertiesXML sets style properties from XML style string, which contains ';'
// separated name: value pairs
func SetStylePropertiesXML(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])
if *properties == nil {
*properties = make(map[string]any)
}
(*properties)[k] = v
}
}
}
// StylePropertiesXML returns style properties for XML style string, which contains ';'
// separated name: value pairs
func StylePropertiesXML(properties map[string]any) string {
var sb strings.Builder
for k, v := range properties {
if k == "transform" {
continue
}
sb.WriteString(fmt.Sprintf("%s:%s;", k, reflectx.ToString(v)))
}
return sb.String()
}
// NewStyle returns a new [Style] object with default values.
func NewStyle() *Style {
s := &Style{}
s.Defaults()
return s
}
// Is returns whether the given [states.States] flag is set
func (s *Style) Is(st states.States) bool {
return s.State.HasFlag(st)
}
// AbilityIs returns whether the given [abilities.Abilities] flag is set
func (s *Style) AbilityIs(able abilities.Abilities) bool {
return s.Abilities.HasFlag(able)
}
// SetState sets the given [states.States] flags to the given value
func (s *Style) SetState(on bool, state ...states.States) *Style {
bfs := make([]enums.BitFlag, len(state))
for i, st := range state {
bfs[i] = st
}
s.State.SetFlag(on, bfs...)
return s
}
// SetEnabled sets the Disabled State flag according to given bool
func (s *Style) SetEnabled(on bool) *Style {
s.State.SetFlag(!on, states.Disabled)
return s
}
// IsReadOnly returns whether this style object is flagged as either [states.ReadOnly] or [states.Disabled].
func (s *Style) IsReadOnly() bool {
return s.Is(states.ReadOnly) || s.Is(states.Disabled)
}
// SetAbilities sets the given [states.State] flags to the given value
func (s *Style) SetAbilities(on bool, able ...abilities.Abilities) {
bfs := make([]enums.BitFlag, len(able))
for i, st := range able {
bfs[i] = st
}
s.Abilities.SetFlag(on, bfs...)
}
// InheritFields from parent
func (s *Style) InheritFields(parent *Style) {
s.Color = parent.Color
s.Opacity = parent.Opacity
s.ScrollbarWidth = parent.ScrollbarWidth
s.Font.InheritFields(&parent.Font)
s.Text.InheritFields(&parent.Text)
}
// ToDotsImpl runs ToDots on unit values, to compile down to raw pixels
func (s *Style) ToDotsImpl(uc *units.Context) {
s.LayoutToDots(uc)
s.Font.ToDots(uc)
s.Text.ToDots(uc)
s.Border.ToDots(uc)
s.MaxBorder.ToDots(uc)
s.BoxShadowToDots(uc)
}
// ToDots caches all style elements in terms of raw pixel
// dots for rendering.
func (s *Style) ToDots() {
if s.Min.X.Unit == units.UnitEw || s.Min.X.Unit == units.UnitEh ||
s.Min.Y.Unit == units.UnitEw || s.Min.Y.Unit == units.UnitEh ||
s.Max.X.Unit == units.UnitEw || s.Max.X.Unit == units.UnitEh ||
s.Max.Y.Unit == units.UnitEw || s.Max.Y.Unit == units.UnitEh {
slog.Error("styling error: cannot use Ew or Eh for Min size -- that is self-referential!")
}
s.ToDotsImpl(&s.UnitContext)
}
// BoxSpace returns the extra space around the central content in the box model in dots.
// It rounds all of the sides first.
func (s *Style) BoxSpace() SideFloats {
return s.TotalMargin().Add(s.Padding.Dots()).Round()
}
// TotalMargin returns the total effective margin of the element
// holding the style, using the sum of the actual margin, the max
// border width, and the max box shadow effective margin. If the
// values for the max border width / box shadow are unset, the
// current values are used instead, which allows for the omission
// of the max properties when the values do not change.
func (s *Style) TotalMargin() SideFloats {
mbw := s.MaxBorder.Width.Dots()
if SidesAreZero(mbw.Sides) {
mbw = s.Border.Width.Dots()
}
mbo := s.MaxBorder.Offset.Dots()
if SidesAreZero(mbo.Sides) {
mbo = s.Border.Offset.Dots()
}
mbw = mbw.Add(mbo)
if s.Border.Style.Top == BorderNone {
mbw.Top = 0
}
if s.Border.Style.Right == BorderNone {
mbw.Right = 0
}
if s.Border.Style.Bottom == BorderNone {
mbw.Bottom = 0
}
if s.Border.Style.Left == BorderNone {
mbw.Left = 0
}
mbsm := s.MaxBoxShadowMargin()
if SidesAreZero(mbsm.Sides) {
mbsm = s.BoxShadowMargin()
}
return s.Margin.Dots().Add(mbw).Add(mbsm)
}
// SubProperties returns a sub-property map from given prop map for a given styling
// selector (property name) -- e.g., :normal :active :hover etc -- returns
// false if not found
func SubProperties(prp map[string]any, selector string) (map[string]any, bool) {
sp, ok := prp[selector]
if !ok {
return nil, false
}
spm, ok := sp.(map[string]any)
if ok {
return spm, true
}
return nil, false
}
// StyleDefault is default style can be used when property specifies "default"
var StyleDefault Style
// ComputeActualBackground sets [Style.ActualBackground] based on the
// given parent actual background and the properties of the style object.
func (s *Style) ComputeActualBackground(pabg image.Image) {
s.ActualBackground = s.ComputeActualBackgroundFor(s.Background, pabg)
}
// ComputeActualBackgroundFor returns the actual background for
// the given background based on the given parent actual background
// and the properties of the style object.
func (s *Style) ComputeActualBackgroundFor(bg, pabg image.Image) image.Image {
if bg == nil {
bg = pabg
} else if u, ok := bg.(*image.Uniform); ok && colors.IsNil(u.C) {
bg = pabg
}
if s.Opacity >= 1 && s.StateLayer <= 0 {
// we have no transformations to apply
return bg
}
// TODO(kai): maybe improve this function to handle all
// use cases correctly (image parents, image state colors, etc)
upabg := colors.ToUniform(pabg)
if s.Opacity < 1 {
bg = gradient.Apply(bg, func(c color.Color) color.Color {
// we take our opacity-applied background color and then overlay it onto our surrounding color
obg := colors.ApplyOpacity(c, s.Opacity)
return colors.AlphaBlend(upabg, obg)
})
}
if s.StateLayer > 0 {
sc := s.Color
if s.StateColor != nil {
sc = s.StateColor
}
// we take our state-layer-applied state color and then overlay it onto our background color
sclr := colors.WithAF32(colors.ToUniform(sc), s.StateLayer)
bg = gradient.Apply(bg, func(c color.Color) color.Color {
return colors.AlphaBlend(c, sclr)
})
}
return bg
}
// IsFlexWrap returns whether the style is both [Style.Wrap] and [Flex].
func (s *Style) IsFlexWrap() bool {
return s.Wrap && s.Display == Flex
}
// SetReadOnly sets the [states.ReadOnly] flag to the given value.
func (s *Style) SetReadOnly(ro bool) {
s.SetState(ro, states.ReadOnly)
}
// CenterAll sets all of the alignment properties to [Center]
// such that all children are fully centered.
func (s *Style) CenterAll() {
s.Justify.Content = Center
s.Justify.Items = Center
s.Align.Content = Center
s.Align.Items = Center
s.Text.Align = Center
s.Text.AlignV = Center
}
// SettingsFont and SettingsMonoFont are pointers to Font and MonoFont in
// [core.AppearanceSettings], which are used in [Style.SetMono] if non-nil.
// They are set automatically by core, so end users should typically not have
// to interact with them.
var SettingsFont, SettingsMonoFont *string
// SetMono sets whether the font is monospace, using the [SettingsFont]
// and [SettingsMonoFont] pointers if possible, and falling back on "mono"
// and "sans-serif" otherwise.
func (s *Style) SetMono(mono bool) {
if mono {
if SettingsMonoFont != nil {
s.Font.Family = *SettingsMonoFont
return
}
s.Font.Family = "mono"
return
}
if SettingsFont != nil {
s.Font.Family = *SettingsFont
return
}
s.Font.Family = "sans-serif"
}
// SetTextWrap sets the Text.WhiteSpace and GrowWrap properties in
// a coordinated manner. If wrap == true, then WhiteSpaceNormal
// and GrowWrap = true; else WhiteSpaceNowrap and GrowWrap = false, which
// are typically the two desired stylings.
func (s *Style) SetTextWrap(wrap bool) {
if wrap {
s.Text.WhiteSpace = WhiteSpaceNormal
s.GrowWrap = true
} else {
s.Text.WhiteSpace = WhiteSpaceNowrap
s.GrowWrap = false
}
}
// SetNonSelectable turns off the Selectable and DoubleClickable
// abilities and sets the Cursor to None.
func (s *Style) SetNonSelectable() {
s.SetAbilities(false, abilities.Selectable, abilities.DoubleClickable)
s.Cursor = cursors.None
}
// 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 styles
import (
"log/slog"
"reflect"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/num"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/enums"
"cogentcore.org/core/styles/units"
)
// styleInhInit detects the style values of "inherit" and "initial",
// setting the corresponding bool return values
func styleInhInit(val, parent any) (inh, init bool) {
if str, ok := val.(string); ok {
switch str {
case "inherit":
return !reflectx.IsNil(reflect.ValueOf(parent)), false
case "initial":
return false, true
default:
return false, false
}
}
return false, false
}
// styleFuncInt returns a style function for any numerical value
func styleFuncInt[T any, F num.Integer](initVal F, getField func(obj *T) *F) styleFunc {
return func(obj any, key string, val any, parent any, cc colors.Context) {
fp := getField(obj.(*T))
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
*fp = *getField(parent.(*T))
} else if init {
*fp = initVal
}
return
}
fv, _ := reflectx.ToInt(val)
*fp = F(fv)
}
}
// styleFuncFloat returns a style function for any numerical value
func styleFuncFloat[T any, F num.Float](initVal F, getField func(obj *T) *F) styleFunc {
return func(obj any, key string, val any, parent any, cc colors.Context) {
fp := getField(obj.(*T))
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
*fp = *getField(parent.(*T))
} else if init {
*fp = initVal
}
return
}
fv, _ := reflectx.ToFloat(val) // can represent any number, ToFloat is fast type switch
*fp = F(fv)
}
}
// styleFuncBool returns a style function for a bool value
func styleFuncBool[T any](initVal bool, getField func(obj *T) *bool) styleFunc {
return func(obj any, key string, val any, parent any, cc colors.Context) {
fp := getField(obj.(*T))
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
*fp = *getField(parent.(*T))
} else if init {
*fp = initVal
}
return
}
fv, _ := reflectx.ToBool(val)
*fp = fv
}
}
// styleFuncUnits returns a style function for units.Value
func styleFuncUnits[T any](initVal units.Value, getField func(obj *T) *units.Value) styleFunc {
return func(obj any, key string, val any, parent any, cc colors.Context) {
fp := getField(obj.(*T))
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
*fp = *getField(parent.(*T))
} else if init {
*fp = initVal
}
return
}
fp.SetAny(val, key)
}
}
// styleFuncEnum returns a style function for any enum value
func styleFuncEnum[T any](initVal enums.Enum, getField func(obj *T) enums.EnumSetter) styleFunc {
return func(obj any, key string, val any, parent any, cc colors.Context) {
fp := getField(obj.(*T))
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
fp.SetInt64(getField(parent.(*T)).Int64())
} else if init {
fp.SetInt64(initVal.Int64())
}
return
}
if st, ok := val.(string); ok {
fp.SetString(st)
return
}
if en, ok := val.(enums.Enum); ok {
fp.SetInt64(en.Int64())
return
}
iv, _ := reflectx.ToInt(val)
fp.SetInt64(int64(iv))
}
}
// These functions set styles from map[string]any which are used for styling
// styleSetError reports that cannot set property of given key with given value due to given error
func styleSetError(key string, val any, err error) {
slog.Error("styles.Style: error setting value", "key", key, "value", val, "err", err)
}
type styleFunc func(obj any, key string, val any, parent any, cc colors.Context)
// StyleFromProperty sets style field values based on the given property key and value
func (s *Style) StyleFromProperty(parent *Style, key string, val any, cc colors.Context) {
if sfunc, ok := styleLayoutFuncs[key]; ok {
if parent != nil {
sfunc(s, key, val, parent, cc)
} else {
sfunc(s, key, val, nil, cc)
}
return
}
if sfunc, ok := styleFontFuncs[key]; ok {
if parent != nil {
sfunc(&s.Font, key, val, &parent.Font, cc)
} else {
sfunc(&s.Font, key, val, nil, cc)
}
return
}
if sfunc, ok := styleTextFuncs[key]; ok {
if parent != nil {
sfunc(&s.Text, key, val, &parent.Text, cc)
} else {
sfunc(&s.Text, key, val, nil, cc)
}
return
}
if sfunc, ok := styleBorderFuncs[key]; ok {
if parent != nil {
sfunc(&s.Border, key, val, &parent.Border, cc)
} else {
sfunc(&s.Border, key, val, nil, cc)
}
return
}
if sfunc, ok := styleStyleFuncs[key]; ok {
sfunc(s, key, val, parent, cc)
return
}
// doesn't work with multiple shadows
// if sfunc, ok := StyleShadowFuncs[key]; ok {
// if parent != nil {
// sfunc(&s.BoxShadow, key, val, &par.BoxShadow, cc)
// } else {
// sfunc(&s.BoxShadow, key, val, nil, cc)
// }
// return
// }
}
/////////////////////////////////////////////////////////////////////////////////
// Style
// styleStyleFuncs are functions for styling the Style object itself
var styleStyleFuncs = map[string]styleFunc{
"color": func(obj any, key string, val any, parent any, cc colors.Context) {
fs := obj.(*Style)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
fs.Color = parent.(*Style).Color
} else if init {
fs.Color = colors.Scheme.OnSurface
}
return
}
fs.Color = errors.Log1(gradient.FromAny(val, cc))
},
"background-color": func(obj any, key string, val any, parent any, cc colors.Context) {
fs := obj.(*Style)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
fs.Background = parent.(*Style).Background
} else if init {
fs.Background = nil
}
return
}
fs.Background = errors.Log1(gradient.FromAny(val, cc))
},
"opacity": styleFuncFloat(float32(1),
func(obj *Style) *float32 { return &obj.Opacity }),
}
/////////////////////////////////////////////////////////////////////////////////
// Layout
// styleLayoutFuncs are functions for styling the layout
// style properties; they are still stored on the main style object,
// but they are done separately to improve clarity
var styleLayoutFuncs = map[string]styleFunc{
"display": styleFuncEnum(Flex,
func(obj *Style) enums.EnumSetter { return &obj.Display }),
"flex-direction": func(obj any, key string, val, parent any, cc colors.Context) {
s := obj.(*Style)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
s.Direction = parent.(*Style).Direction
} else if init {
s.Direction = Row
}
return
}
str := reflectx.ToString(val)
if str == "row" || str == "row-reverse" {
s.Direction = Row
} else {
s.Direction = Column
}
},
// TODO(kai/styproperties): multi-dim flex-grow
"flex-grow": styleFuncFloat(0, func(obj *Style) *float32 { return &obj.Grow.Y }),
"wrap": styleFuncBool(false,
func(obj *Style) *bool { return &obj.Wrap }),
"justify-content": styleFuncEnum(Start,
func(obj *Style) enums.EnumSetter { return &obj.Justify.Content }),
"justify-items": styleFuncEnum(Start,
func(obj *Style) enums.EnumSetter { return &obj.Justify.Items }),
"justify-self": styleFuncEnum(Auto,
func(obj *Style) enums.EnumSetter { return &obj.Justify.Self }),
"align-content": styleFuncEnum(Start,
func(obj *Style) enums.EnumSetter { return &obj.Align.Content }),
"align-items": styleFuncEnum(Start,
func(obj *Style) enums.EnumSetter { return &obj.Align.Items }),
"align-self": styleFuncEnum(Auto,
func(obj *Style) enums.EnumSetter { return &obj.Align.Self }),
"x": styleFuncUnits(units.Value{},
func(obj *Style) *units.Value { return &obj.Pos.X }),
"y": styleFuncUnits(units.Value{},
func(obj *Style) *units.Value { return &obj.Pos.Y }),
"width": styleFuncUnits(units.Value{},
func(obj *Style) *units.Value { return &obj.Min.X }),
"height": styleFuncUnits(units.Value{},
func(obj *Style) *units.Value { return &obj.Min.Y }),
"max-width": styleFuncUnits(units.Value{},
func(obj *Style) *units.Value { return &obj.Max.X }),
"max-height": styleFuncUnits(units.Value{},
func(obj *Style) *units.Value { return &obj.Max.Y }),
"min-width": styleFuncUnits(units.Dp(2),
func(obj *Style) *units.Value { return &obj.Min.X }),
"min-height": styleFuncUnits(units.Dp(2),
func(obj *Style) *units.Value { return &obj.Min.Y }),
"margin": func(obj any, key string, val any, parent any, cc colors.Context) {
s := obj.(*Style)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
s.Margin = parent.(*Style).Margin
} else if init {
s.Margin.Zero()
}
return
}
s.Margin.SetAny(val)
},
"padding": func(obj any, key string, val any, parent any, cc colors.Context) {
s := obj.(*Style)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
s.Padding = parent.(*Style).Padding
} else if init {
s.Padding.Zero()
}
return
}
s.Padding.SetAny(val)
},
// TODO(kai/styproperties): multi-dim overflow
"overflow": styleFuncEnum(OverflowAuto,
func(obj *Style) enums.EnumSetter { return &obj.Overflow.Y }),
"columns": styleFuncInt(int(0),
func(obj *Style) *int { return &obj.Columns }),
"row": styleFuncInt(int(0),
func(obj *Style) *int { return &obj.Row }),
"col": styleFuncInt(int(0),
func(obj *Style) *int { return &obj.Col }),
"row-span": styleFuncInt(int(0),
func(obj *Style) *int { return &obj.RowSpan }),
"col-span": styleFuncInt(int(0),
func(obj *Style) *int { return &obj.ColSpan }),
"z-index": styleFuncInt(int(0),
func(obj *Style) *int { return &obj.ZIndex }),
"scrollbar-width": styleFuncUnits(units.Value{},
func(obj *Style) *units.Value { return &obj.ScrollbarWidth }),
}
/////////////////////////////////////////////////////////////////////////////////
// Font
// styleFontFuncs are functions for styling the Font object
var styleFontFuncs = map[string]styleFunc{
"font-size": func(obj any, key string, val any, parent any, cc colors.Context) {
fs := obj.(*Font)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
fs.Size = parent.(*Font).Size
} else if init {
fs.Size.Set(12, units.UnitPt)
}
return
}
switch vt := val.(type) {
case string:
if psz, ok := FontSizePoints[vt]; ok {
fs.Size = units.Pt(psz)
} else {
fs.Size.SetAny(val, key) // also processes string
}
default:
fs.Size.SetAny(val, key)
}
},
"font-family": func(obj any, key string, val any, parent any, cc colors.Context) {
fs := obj.(*Font)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
fs.Family = parent.(*Font).Family
} else if init {
fs.Family = "" // font has defaults
}
return
}
fs.Family = reflectx.ToString(val)
},
"font-style": styleFuncEnum(FontNormal,
func(obj *Font) enums.EnumSetter { return &obj.Style }),
"font-weight": styleFuncEnum(WeightNormal,
func(obj *Font) enums.EnumSetter { return &obj.Weight }),
"font-stretch": styleFuncEnum(FontStrNormal,
func(obj *Font) enums.EnumSetter { return &obj.Stretch }),
"font-variant": styleFuncEnum(FontVarNormal,
func(obj *Font) enums.EnumSetter { return &obj.Variant }),
"baseline-shift": styleFuncEnum(ShiftBaseline,
func(obj *Font) enums.EnumSetter { return &obj.Shift }),
"text-decoration": func(obj any, key string, val any, parent any, cc colors.Context) {
fs := obj.(*Font)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
fs.Decoration = parent.(*Font).Decoration
} else if init {
fs.Decoration = DecoNone
}
return
}
switch vt := val.(type) {
case string:
if vt == "none" {
fs.Decoration = DecoNone
} else {
fs.Decoration.SetString(vt)
}
case TextDecorations:
fs.Decoration = vt
default:
iv, err := reflectx.ToInt(val)
if err == nil {
fs.Decoration = TextDecorations(iv)
} else {
styleSetError(key, val, err)
}
}
},
}
// styleFontRenderFuncs are _extra_ functions for styling
// the FontRender object in addition to base Font
var styleFontRenderFuncs = map[string]styleFunc{
"color": func(obj any, key string, val any, parent any, cc colors.Context) {
fs := obj.(*FontRender)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
fs.Color = parent.(*FontRender).Color
} else if init {
fs.Color = colors.Scheme.OnSurface
}
return
}
fs.Color = errors.Log1(gradient.FromAny(val, cc))
},
"background-color": func(obj any, key string, val any, parent any, cc colors.Context) {
fs := obj.(*FontRender)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
fs.Background = parent.(*FontRender).Background
} else if init {
fs.Background = nil
}
return
}
fs.Background = errors.Log1(gradient.FromAny(val, cc))
},
"opacity": styleFuncFloat(float32(1),
func(obj *FontRender) *float32 { return &obj.Opacity }),
}
/////////////////////////////////////////////////////////////////////////////////
// Text
// styleTextFuncs are functions for styling the Text object
var styleTextFuncs = map[string]styleFunc{
"text-align": styleFuncEnum(Start,
func(obj *Text) enums.EnumSetter { return &obj.Align }),
"text-vertical-align": styleFuncEnum(Start,
func(obj *Text) enums.EnumSetter { return &obj.AlignV }),
"text-anchor": styleFuncEnum(AnchorStart,
func(obj *Text) enums.EnumSetter { return &obj.Anchor }),
"letter-spacing": styleFuncUnits(units.Value{},
func(obj *Text) *units.Value { return &obj.LetterSpacing }),
"word-spacing": styleFuncUnits(units.Value{},
func(obj *Text) *units.Value { return &obj.WordSpacing }),
"line-height": styleFuncUnits(LineHeightNormal,
func(obj *Text) *units.Value { return &obj.LineHeight }),
"white-space": styleFuncEnum(WhiteSpaceNormal,
func(obj *Text) enums.EnumSetter { return &obj.WhiteSpace }),
"unicode-bidi": styleFuncEnum(BidiNormal,
func(obj *Text) enums.EnumSetter { return &obj.UnicodeBidi }),
"direction": styleFuncEnum(LRTB,
func(obj *Text) enums.EnumSetter { return &obj.Direction }),
"writing-mode": styleFuncEnum(LRTB,
func(obj *Text) enums.EnumSetter { return &obj.WritingMode }),
"glyph-orientation-vertical": styleFuncFloat(float32(1),
func(obj *Text) *float32 { return &obj.OrientationVert }),
"glyph-orientation-horizontal": styleFuncFloat(float32(1),
func(obj *Text) *float32 { return &obj.OrientationHoriz }),
"text-indent": styleFuncUnits(units.Value{},
func(obj *Text) *units.Value { return &obj.Indent }),
"para-spacing": styleFuncUnits(units.Value{},
func(obj *Text) *units.Value { return &obj.ParaSpacing }),
"tab-size": styleFuncInt(int(4),
func(obj *Text) *int { return &obj.TabSize }),
}
/////////////////////////////////////////////////////////////////////////////////
// Border
// styleBorderFuncs are functions for styling the Border object
var styleBorderFuncs = map[string]styleFunc{
// SidesTODO: need to figure out how to get key and context information for side SetAny calls
// with padding, margin, border, etc
"border-style": func(obj any, key string, val any, parent any, cc colors.Context) {
bs := obj.(*Border)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
bs.Style = parent.(*Border).Style
} else if init {
bs.Style.Set(BorderSolid)
}
return
}
switch vt := val.(type) {
case string:
bs.Style.SetString(vt)
case BorderStyles:
bs.Style.Set(vt)
case []BorderStyles:
bs.Style.Set(vt...)
default:
iv, err := reflectx.ToInt(val)
if err == nil {
bs.Style.Set(BorderStyles(iv))
} else {
styleSetError(key, val, err)
}
}
},
"border-width": func(obj any, key string, val any, parent any, cc colors.Context) {
bs := obj.(*Border)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
bs.Width = parent.(*Border).Width
} else if init {
bs.Width.Zero()
}
return
}
bs.Width.SetAny(val)
},
"border-radius": func(obj any, key string, val any, parent any, cc colors.Context) {
bs := obj.(*Border)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
bs.Radius = parent.(*Border).Radius
} else if init {
bs.Radius.Zero()
}
return
}
bs.Radius.SetAny(val)
},
"border-color": func(obj any, key string, val any, parent any, cc colors.Context) {
bs := obj.(*Border)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
bs.Color = parent.(*Border).Color
} else if init {
bs.Color.Set(colors.Scheme.Outline)
}
return
}
// TODO(kai): support side-specific border colors
bs.Color.Set(errors.Log1(gradient.FromAny(val, cc)))
},
}
/////////////////////////////////////////////////////////////////////////////////
// Outline
// styleOutlineFuncs are functions for styling the OutlineStyle object
var styleOutlineFuncs = map[string]styleFunc{
"outline-style": func(obj any, key string, val any, parent any, cc colors.Context) {
bs := obj.(*Border)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
bs.Style = parent.(*Border).Style
} else if init {
bs.Style.Set(BorderSolid)
}
return
}
switch vt := val.(type) {
case string:
bs.Style.SetString(vt)
case BorderStyles:
bs.Style.Set(vt)
case []BorderStyles:
bs.Style.Set(vt...)
default:
iv, err := reflectx.ToInt(val)
if err == nil {
bs.Style.Set(BorderStyles(iv))
} else {
styleSetError(key, val, err)
}
}
},
"outline-width": func(obj any, key string, val any, parent any, cc colors.Context) {
bs := obj.(*Border)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
bs.Width = parent.(*Border).Width
} else if init {
bs.Width.Zero()
}
return
}
bs.Width.SetAny(val)
},
"outline-radius": func(obj any, key string, val any, parent any, cc colors.Context) {
bs := obj.(*Border)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
bs.Radius = parent.(*Border).Radius
} else if init {
bs.Radius.Zero()
}
return
}
bs.Radius.SetAny(val)
},
"outline-color": func(obj any, key string, val any, parent any, cc colors.Context) {
bs := obj.(*Border)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
bs.Color = parent.(*Border).Color
} else if init {
bs.Color.Set(colors.Scheme.Outline)
}
return
}
// TODO(kai): support side-specific border colors
bs.Color.Set(errors.Log1(gradient.FromAny(val, cc)))
},
}
/////////////////////////////////////////////////////////////////////////////////
// Shadow
// styleShadowFuncs are functions for styling the Shadow object
var styleShadowFuncs = map[string]styleFunc{
"box-shadow.offset-x": styleFuncUnits(units.Value{},
func(obj *Shadow) *units.Value { return &obj.OffsetX }),
"box-shadow.offset-y": styleFuncUnits(units.Value{},
func(obj *Shadow) *units.Value { return &obj.OffsetY }),
"box-shadow.blur": styleFuncUnits(units.Value{},
func(obj *Shadow) *units.Value { return &obj.Blur }),
"box-shadow.spread": styleFuncUnits(units.Value{},
func(obj *Shadow) *units.Value { return &obj.Spread }),
"box-shadow.color": func(obj any, key string, val any, parent any, cc colors.Context) {
ss := obj.(*Shadow)
if inh, init := styleInhInit(val, parent); inh || init {
if inh {
ss.Color = parent.(*Shadow).Color
} else if init {
ss.Color = colors.Scheme.Shadow
}
return
}
ss.Color = errors.Log1(gradient.FromAny(val, cc))
},
"box-shadow.inset": styleFuncBool(false,
func(obj *Shadow) *bool { return &obj.Inset }),
}
// 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 styles
import (
"cogentcore.org/core/styles/units"
)
// IMPORTANT: any changes here must be updated in style_properties.go StyleTextFuncs
// Text is used for layout-level (widget, html-style) text styling --
// FontStyle contains all the lower-level text rendering info used in SVG --
// most of these are inherited
type Text struct { //types:add
// how to align text, horizontally (inherited).
// This *only* applies to the text within its containing element,
// and is typically relevant only for multi-line text:
// for single-line text, if element does not have a specified size
// that is different from the text size, then this has *no effect*.
Align Aligns
// vertical alignment of text (inherited).
// This *only* applies to the text within its containing element:
// if that element does not have a specified size
// that is different from the text size, then this has *no effect*.
AlignV Aligns
// for svg rendering only (inherited):
// determines the alignment relative to text position coordinate.
// For RTL start is right, not left, and start is top for TB
Anchor TextAnchors
// spacing between characters and lines
LetterSpacing units.Value
// extra space to add between words (inherited)
WordSpacing units.Value
// LineHeight is the height of a line of text (inherited).
// Text is centered within the overall line height.
// The standard way to specify line height is in terms of
// [units.Em] so that it scales with the font size.
LineHeight units.Value
// WhiteSpace (not inherited) specifies how white space is processed,
// and how lines are wrapped. If set to WhiteSpaceNormal (default) lines are wrapped.
// See info about interactions with Grow.X setting for this and the NoWrap case.
WhiteSpace WhiteSpaces
// determines how to treat unicode bidirectional information (inherited)
UnicodeBidi UnicodeBidi
// bidi-override or embed -- applies to all text elements (inherited)
Direction TextDirections
// overall writing mode -- only for text elements, not span (inherited)
WritingMode TextDirections
// for TBRL writing mode (only), determines orientation of alphabetic characters (inherited);
// 90 is default (rotated); 0 means keep upright
OrientationVert float32
// for horizontal LR/RL writing mode (only), determines orientation of all characters (inherited);
// 0 is default (upright)
OrientationHoriz float32
// how much to indent the first line in a paragraph (inherited)
Indent units.Value
// extra spacing between paragraphs (inherited); copied from [Style.Margin] per CSS spec
// if that is non-zero, else can be set directly with para-spacing
ParaSpacing units.Value
// tab size, in number of characters (inherited)
TabSize int
}
// LineHeightNormal represents a normal line height,
// equal to the default height of the font being used.
var LineHeightNormal = units.Dp(-1)
func (ts *Text) Defaults() {
ts.LineHeight = LineHeightNormal
ts.Align = Start
ts.AlignV = Baseline
ts.Direction = LTR
ts.OrientationVert = 90
ts.TabSize = 4
}
// ToDots runs ToDots on unit values, to compile down to raw pixels
func (ts *Text) ToDots(uc *units.Context) {
ts.LetterSpacing.ToDots(uc)
ts.WordSpacing.ToDots(uc)
ts.LineHeight.ToDots(uc)
ts.Indent.ToDots(uc)
ts.ParaSpacing.ToDots(uc)
}
// InheritFields from parent
func (ts *Text) InheritFields(parent *Text) {
ts.Align = parent.Align
ts.AlignV = parent.AlignV
ts.Anchor = parent.Anchor
ts.WordSpacing = parent.WordSpacing
ts.LineHeight = parent.LineHeight
// ts.WhiteSpace = par.WhiteSpace // todo: we can't inherit this b/c label base default then gets overwritten
ts.UnicodeBidi = parent.UnicodeBidi
ts.Direction = parent.Direction
ts.WritingMode = parent.WritingMode
ts.OrientationVert = parent.OrientationVert
ts.OrientationHoriz = parent.OrientationHoriz
ts.Indent = parent.Indent
ts.ParaSpacing = parent.ParaSpacing
ts.TabSize = parent.TabSize
}
// EffLineHeight returns the effective line height for the given
// font height, handling the [LineHeightNormal] special case.
func (ts *Text) EffLineHeight(fontHeight float32) float32 {
if ts.LineHeight.Value < 0 {
return fontHeight
}
return ts.LineHeight.Dots
}
// AlignFactors gets basic text alignment factors
func (ts *Text) AlignFactors() (ax, ay float32) {
ax = 0.0
ay = 0.0
hal := ts.Align
switch hal {
case Center:
ax = 0.5 // todo: determine if font is horiz or vert..
case End:
ax = 1.0
}
val := ts.AlignV
switch val {
case Start:
ay = 0.9 // todo: need to find out actual baseline
case Center:
ay = 0.45 // todo: determine if font is horiz or vert..
case End:
ay = -0.1 // todo: need actual baseline
}
return
}
// UnicodeBidi determines the type of bidirectional text support.
// See https://pkg.go.dev/golang.org/x/text/unicode/bidi.
type UnicodeBidi int32 //enums:enum -trim-prefix Bidi -transform kebab
const (
BidiNormal UnicodeBidi = iota
BidiEmbed
BidiBidiOverride
)
// TextDirections are for direction of text writing, used in direction and writing-mode styles
type TextDirections int32 //enums:enum -transform lower
const (
LRTB TextDirections = iota
RLTB
TBRL
LR
RL
TB
LTR
RTL
)
// TextAnchors are for direction of text writing, used in direction and writing-mode styles
type TextAnchors int32 //enums:enum -trim-prefix Anchor -transform kebab
const (
AnchorStart TextAnchors = iota
AnchorMiddle
AnchorEnd
)
// WhiteSpaces determine how white space is processed
type WhiteSpaces int32 //enums:enum -trim-prefix WhiteSpace
const (
// WhiteSpaceNormal means that all white space is collapsed to a single
// space, and text wraps when necessary. To get full word wrapping to
// expand to all available space, you also need to set GrowWrap = true.
// Use the SetTextWrap convenience method to set both.
WhiteSpaceNormal WhiteSpaces = iota
// WhiteSpaceNowrap means that sequences of whitespace will collapse into
// a single whitespace. Text will never wrap to the next line except
// if there is an explicit line break via a <br> tag. In general you
// also don't want simple non-wrapping text labels to Grow (GrowWrap = false).
// Use the SetTextWrap method to set both.
WhiteSpaceNowrap
// WhiteSpacePre means that whitespace is preserved. Text
// will only wrap on line breaks. Acts like the <pre> tag in HTML. This
// invokes a different hand-written parser because the default Go
// parser automatically throws away whitespace.
WhiteSpacePre
// WhiteSpacePreLine means that sequences of whitespace will collapse
// into a single whitespace. Text will wrap when necessary, and on line
// breaks
WhiteSpacePreLine
// WhiteSpacePreWrap means that whitespace is preserved.
// Text will wrap when necessary, and on line breaks
WhiteSpacePreWrap
)
// HasWordWrap returns true if current white space option supports word wrap
func (ts *Text) HasWordWrap() bool {
switch ts.WhiteSpace {
case WhiteSpaceNormal, WhiteSpacePreLine, WhiteSpacePreWrap:
return true
default:
return false
}
}
// HasPre returns true if current white space option preserves existing
// whitespace (or at least requires that parser in case of PreLine, which is
// intermediate)
func (ts *Text) HasPre() bool {
switch ts.WhiteSpace {
case WhiteSpaceNormal, WhiteSpaceNowrap:
return false
default:
return true
}
}
// 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 units
import "cogentcore.org/core/base/reflectx"
// Context specifies everything about the current context necessary for converting the number
// into specific display-dependent pixels
type Context struct {
// DPI is dots-per-inch of the display
DPI float32
// FontEm is the size of the font of the element in raw dots (not points)
FontEm float32
// FontEx is the height x-height of font in points (size of 'x' glyph)
FontEx float32
// FontCh is the ch-size character size of font in points (width of '0' glyph)
FontCh float32
// FontRem is the size of the font of the root element in raw dots (not points)
FontRem float32
// Vpw is viewport width in dots
Vpw float32
// Vph is viewport height in dots
Vph float32
// Elw is width of element in dots
Elw float32
// Elh is height of element in dots
Elh float32
// Paw is width of parent in dots
Paw float32
// Pah is height of parent in dots
Pah float32
}
// Defaults are generic defaults
func (uc *Context) Defaults() {
uc.DPI = DpPerInch
uc.FontEm = 16
uc.FontEx = 8
uc.FontCh = 8
uc.FontRem = 16
uc.Vpw = 800
uc.Vph = 600
uc.Elw = uc.Vpw
uc.Elh = uc.Vph
uc.Paw = uc.Vpw
uc.Pah = uc.Vph
}
func (uc *Context) String() string {
return reflectx.StringJSON(uc)
}
// SetSizes sets the context values for the non-font sizes
// to the given values; the values are ignored if they are zero.
// returns true if any are different.
func (uc *Context) SetSizes(vw, vh, ew, eh, pw, ph float32) bool {
diff := false
if vw != 0 {
if uc.Vpw != vw {
diff = true
}
uc.Vpw = vw
}
if vh != 0 {
if uc.Vph != vh {
diff = true
}
uc.Vph = vh
}
if ew != 0 {
if uc.Elw != ew {
diff = true
}
uc.Elw = ew
}
if eh != 0 {
if uc.Elh != eh {
diff = true
}
uc.Elh = eh
}
if pw != 0 {
if uc.Paw != pw {
diff = true
}
uc.Paw = pw
}
if ph != 0 {
if uc.Pah != ph {
diff = true
}
uc.Pah = ph
}
return diff
}
// SetFont sets the context values for fonts: note these are already in raw
// DPI dots, not points or anything else
func (uc *Context) SetFont(em, ex, ch, rem float32) {
uc.FontEm = em
uc.FontEx = ex
uc.FontCh = ch
uc.FontRem = rem
}
// ToDotsFact returns factor needed to convert given unit into raw pixels (dots in DPI)
func (uc *Context) Dots(un Units) float32 {
if uc.DPI == 0 {
// log.Printf("gi/units Context was not initialized -- falling back on defaults\n")
uc.Defaults()
}
switch un {
case UnitEw:
return 0.01 * uc.Elw
case UnitEh:
return 0.01 * uc.Elh
case UnitPw:
return 0.01 * uc.Paw
case UnitPh:
return 0.01 * uc.Pah
case UnitEm:
return uc.FontEm
case UnitEx:
return uc.FontEx
case UnitCh:
return uc.FontCh
case UnitRem:
return uc.FontRem
case UnitVw:
return 0.01 * uc.Vpw
case UnitVh:
return 0.01 * uc.Vph
case UnitVmin:
return 0.01 * min(uc.Vpw, uc.Vph)
case UnitVmax:
return 0.01 * max(uc.Vpw, uc.Vph)
case UnitCm:
return uc.DPI / CmPerInch
case UnitMm:
return uc.DPI / MmPerInch
case UnitQ:
return uc.DPI / (4.0 * MmPerInch)
case UnitIn:
return uc.DPI
case UnitPc:
return uc.DPI / PcPerInch
case UnitPt:
return uc.DPI / PtPerInch
case UnitPx:
return uc.DPI / PxPerInch
case UnitDp:
return uc.DPI / DpPerInch
case UnitDot:
return 1.0
}
return uc.DPI
}
// ToDots converts value in given units into raw display pixels (dots in DPI)
func (uc *Context) ToDots(val float32, un Units) float32 {
return val * uc.Dots(un)
}
// PxToDots just converts a value from pixels to dots
func (uc *Context) PxToDots(val float32) float32 {
return val * uc.Dots(UnitPx)
}
// DotsToPx just converts a value from dots to pixels
func (uc *Context) DotsToPx(val float32) float32 {
return val / uc.Dots(UnitPx)
}
// Code generated by "core generate"; DO NOT EDIT.
package units
import (
"cogentcore.org/core/enums"
)
var _UnitsValues = []Units{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
// UnitsN is the highest valid value for type Units, plus one.
const UnitsN Units = 21
var _UnitsValueMap = map[string]Units{`dp`: 0, `px`: 1, `ew`: 2, `eh`: 3, `pw`: 4, `ph`: 5, `rem`: 6, `em`: 7, `ex`: 8, `ch`: 9, `vw`: 10, `vh`: 11, `vmin`: 12, `vmax`: 13, `cm`: 14, `mm`: 15, `q`: 16, `in`: 17, `pc`: 18, `pt`: 19, `dot`: 20}
var _UnitsDescMap = map[Units]string{0: `UnitDp represents density-independent pixels. 1dp is 1/160 in. Inches are not necessarily the same as actual physical inches, as they depend on the DPI, so dp values may correspond to different physical sizes on different displays, but they will look correct.`, 1: `UnitPx represents logical pixels. 1px is 1/96 in. These are not raw display pixels, for which you should use dots. Dp is a more common unit for general use.`, 2: `UnitEw represents percentage of element width, which is equivalent to CSS % in some contexts.`, 3: `UnitEh represents percentage of element height, which is equivalent to CSS % in some contexts.`, 4: `UnitPw represents percentage of parent width, which is equivalent to CSS % in some contexts.`, 5: `UnitPh represents percentage of parent height, which is equivalent to CSS % in some contexts.`, 6: `UnitRem represents the font size of the root element, which is always 16dp.`, 7: `UnitEm represents the font size of the element.`, 8: `UnitEx represents x-height of the element's font (size of 'x' glyph). It falls back to a default of 0.5em.`, 9: `UnitCh represents width of the '0' glyph in the element's font. It falls back to a default of 0.5em.`, 10: `UnitVw represents percentage of viewport (Scene) width.`, 11: `UnitVh represents percentage of viewport (Scene) height.`, 12: `UnitVmin represents percentage of the smaller dimension of the viewport (Scene).`, 13: `UnitVmax represents percentage of the larger dimension of the viewport (Scene).`, 14: `UnitCm represents logical centimeters. 1cm is 1/2.54 in.`, 15: `UnitMm represents logical millimeters. 1mm is 1/10 cm.`, 16: `UnitQ represents logical quarter-millimeters. 1q is 1/40 cm.`, 17: `UnitIn represents logical inches. 1in is 2.54cm or 96px. This is similar to CSS inches in that it is not necessarily the same as an actual physical inch; it is dependent on the DPI of the display.`, 18: `UnitPc represents logical picas. 1pc is 1/6 in.`, 19: `UnitPt represents points. 1pt is 1/72 in.`, 20: `UnitDot represents real display pixels. They are generally only used internally.`}
var _UnitsMap = map[Units]string{0: `dp`, 1: `px`, 2: `ew`, 3: `eh`, 4: `pw`, 5: `ph`, 6: `rem`, 7: `em`, 8: `ex`, 9: `ch`, 10: `vw`, 11: `vh`, 12: `vmin`, 13: `vmax`, 14: `cm`, 15: `mm`, 16: `q`, 17: `in`, 18: `pc`, 19: `pt`, 20: `dot`}
// String returns the string representation of this Units value.
func (i Units) String() string { return enums.String(i, _UnitsMap) }
// SetString sets the Units value from its string representation,
// and returns an error if the string is invalid.
func (i *Units) SetString(s string) error { return enums.SetString(i, s, _UnitsValueMap, "Units") }
// Int64 returns the Units value as an int64.
func (i Units) Int64() int64 { return int64(i) }
// SetInt64 sets the Units value from an int64.
func (i *Units) SetInt64(in int64) { *i = Units(in) }
// Desc returns the description of the Units value.
func (i Units) Desc() string { return enums.Desc(i, _UnitsDescMap) }
// UnitsValues returns all possible values for the type Units.
func UnitsValues() []Units { return _UnitsValues }
// Values returns all possible values for the type Units.
func (i Units) Values() []enums.Enum { return enums.Values(_UnitsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Units) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Units) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Units") }
// Code generated by "go run gen.go"; DO NOT EDIT.
// 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 units
// Dp returns a new dp value.
// Dp is density-independent pixels. 1dp is 1/160 in. Inches are not necessarily the same as actual physical inches, as they depend on the DPI, so dp values may correspond to different physical sizes on different displays, but they will look correct.
func Dp(value float32) Value {
return Value{Value: value, Unit: UnitDp}
}
// Dp sets the value in terms of dp.
// Dp is density-independent pixels. 1dp is 1/160 in. Inches are not necessarily the same as actual physical inches, as they depend on the DPI, so dp values may correspond to different physical sizes on different displays, but they will look correct.
func (v *Value) Dp(value float32) {
v.Value = value
v.Unit = UnitDp
}
// Dp converts the given dp value to dots.
// Dp is density-independent pixels. 1dp is 1/160 in. Inches are not necessarily the same as actual physical inches, as they depend on the DPI, so dp values may correspond to different physical sizes on different displays, but they will look correct.
func (uc *Context) Dp(value float32) float32 {
return uc.ToDots(value, UnitDp)
}
// Px returns a new px value.
// Px is logical pixels. 1px is 1/96 in. These are not raw display pixels, for which you should use dots. Dp is a more common unit for general use.
func Px(value float32) Value {
return Value{Value: value, Unit: UnitPx}
}
// Px sets the value in terms of px.
// Px is logical pixels. 1px is 1/96 in. These are not raw display pixels, for which you should use dots. Dp is a more common unit for general use.
func (v *Value) Px(value float32) {
v.Value = value
v.Unit = UnitPx
}
// Px converts the given px value to dots.
// Px is logical pixels. 1px is 1/96 in. These are not raw display pixels, for which you should use dots. Dp is a more common unit for general use.
func (uc *Context) Px(value float32) float32 {
return uc.ToDots(value, UnitPx)
}
// Ew returns a new ew value.
// Ew is percentage of element width, which is equivalent to CSS % in some contexts.
func Ew(value float32) Value {
return Value{Value: value, Unit: UnitEw}
}
// Ew sets the value in terms of ew.
// Ew is percentage of element width, which is equivalent to CSS % in some contexts.
func (v *Value) Ew(value float32) {
v.Value = value
v.Unit = UnitEw
}
// Ew converts the given ew value to dots.
// Ew is percentage of element width, which is equivalent to CSS % in some contexts.
func (uc *Context) Ew(value float32) float32 {
return uc.ToDots(value, UnitEw)
}
// Eh returns a new eh value.
// Eh is percentage of element height, which is equivalent to CSS % in some contexts.
func Eh(value float32) Value {
return Value{Value: value, Unit: UnitEh}
}
// Eh sets the value in terms of eh.
// Eh is percentage of element height, which is equivalent to CSS % in some contexts.
func (v *Value) Eh(value float32) {
v.Value = value
v.Unit = UnitEh
}
// Eh converts the given eh value to dots.
// Eh is percentage of element height, which is equivalent to CSS % in some contexts.
func (uc *Context) Eh(value float32) float32 {
return uc.ToDots(value, UnitEh)
}
// Pw returns a new pw value.
// Pw is percentage of parent width, which is equivalent to CSS % in some contexts.
func Pw(value float32) Value {
return Value{Value: value, Unit: UnitPw}
}
// Pw sets the value in terms of pw.
// Pw is percentage of parent width, which is equivalent to CSS % in some contexts.
func (v *Value) Pw(value float32) {
v.Value = value
v.Unit = UnitPw
}
// Pw converts the given pw value to dots.
// Pw is percentage of parent width, which is equivalent to CSS % in some contexts.
func (uc *Context) Pw(value float32) float32 {
return uc.ToDots(value, UnitPw)
}
// Ph returns a new ph value.
// Ph is percentage of parent height, which is equivalent to CSS % in some contexts.
func Ph(value float32) Value {
return Value{Value: value, Unit: UnitPh}
}
// Ph sets the value in terms of ph.
// Ph is percentage of parent height, which is equivalent to CSS % in some contexts.
func (v *Value) Ph(value float32) {
v.Value = value
v.Unit = UnitPh
}
// Ph converts the given ph value to dots.
// Ph is percentage of parent height, which is equivalent to CSS % in some contexts.
func (uc *Context) Ph(value float32) float32 {
return uc.ToDots(value, UnitPh)
}
// Rem returns a new rem value.
// Rem is the font size of the root element, which is always 16dp.
func Rem(value float32) Value {
return Value{Value: value, Unit: UnitRem}
}
// Rem sets the value in terms of rem.
// Rem is the font size of the root element, which is always 16dp.
func (v *Value) Rem(value float32) {
v.Value = value
v.Unit = UnitRem
}
// Rem converts the given rem value to dots.
// Rem is the font size of the root element, which is always 16dp.
func (uc *Context) Rem(value float32) float32 {
return uc.ToDots(value, UnitRem)
}
// Em returns a new em value.
// Em is the font size of the element.
func Em(value float32) Value {
return Value{Value: value, Unit: UnitEm}
}
// Em sets the value in terms of em.
// Em is the font size of the element.
func (v *Value) Em(value float32) {
v.Value = value
v.Unit = UnitEm
}
// Em converts the given em value to dots.
// Em is the font size of the element.
func (uc *Context) Em(value float32) float32 {
return uc.ToDots(value, UnitEm)
}
// Ex returns a new ex value.
// Ex is x-height of the element's font (size of 'x' glyph). It falls back to a default of 0.5em.
func Ex(value float32) Value {
return Value{Value: value, Unit: UnitEx}
}
// Ex sets the value in terms of ex.
// Ex is x-height of the element's font (size of 'x' glyph). It falls back to a default of 0.5em.
func (v *Value) Ex(value float32) {
v.Value = value
v.Unit = UnitEx
}
// Ex converts the given ex value to dots.
// Ex is x-height of the element's font (size of 'x' glyph). It falls back to a default of 0.5em.
func (uc *Context) Ex(value float32) float32 {
return uc.ToDots(value, UnitEx)
}
// Ch returns a new ch value.
// Ch is width of the '0' glyph in the element's font. It falls back to a default of 0.5em.
func Ch(value float32) Value {
return Value{Value: value, Unit: UnitCh}
}
// Ch sets the value in terms of ch.
// Ch is width of the '0' glyph in the element's font. It falls back to a default of 0.5em.
func (v *Value) Ch(value float32) {
v.Value = value
v.Unit = UnitCh
}
// Ch converts the given ch value to dots.
// Ch is width of the '0' glyph in the element's font. It falls back to a default of 0.5em.
func (uc *Context) Ch(value float32) float32 {
return uc.ToDots(value, UnitCh)
}
// Vw returns a new vw value.
// Vw is percentage of viewport (Scene) width.
func Vw(value float32) Value {
return Value{Value: value, Unit: UnitVw}
}
// Vw sets the value in terms of vw.
// Vw is percentage of viewport (Scene) width.
func (v *Value) Vw(value float32) {
v.Value = value
v.Unit = UnitVw
}
// Vw converts the given vw value to dots.
// Vw is percentage of viewport (Scene) width.
func (uc *Context) Vw(value float32) float32 {
return uc.ToDots(value, UnitVw)
}
// Vh returns a new vh value.
// Vh is percentage of viewport (Scene) height.
func Vh(value float32) Value {
return Value{Value: value, Unit: UnitVh}
}
// Vh sets the value in terms of vh.
// Vh is percentage of viewport (Scene) height.
func (v *Value) Vh(value float32) {
v.Value = value
v.Unit = UnitVh
}
// Vh converts the given vh value to dots.
// Vh is percentage of viewport (Scene) height.
func (uc *Context) Vh(value float32) float32 {
return uc.ToDots(value, UnitVh)
}
// Vmin returns a new vmin value.
// Vmin is percentage of the smaller dimension of the viewport (Scene).
func Vmin(value float32) Value {
return Value{Value: value, Unit: UnitVmin}
}
// Vmin sets the value in terms of vmin.
// Vmin is percentage of the smaller dimension of the viewport (Scene).
func (v *Value) Vmin(value float32) {
v.Value = value
v.Unit = UnitVmin
}
// Vmin converts the given vmin value to dots.
// Vmin is percentage of the smaller dimension of the viewport (Scene).
func (uc *Context) Vmin(value float32) float32 {
return uc.ToDots(value, UnitVmin)
}
// Vmax returns a new vmax value.
// Vmax is percentage of the larger dimension of the viewport (Scene).
func Vmax(value float32) Value {
return Value{Value: value, Unit: UnitVmax}
}
// Vmax sets the value in terms of vmax.
// Vmax is percentage of the larger dimension of the viewport (Scene).
func (v *Value) Vmax(value float32) {
v.Value = value
v.Unit = UnitVmax
}
// Vmax converts the given vmax value to dots.
// Vmax is percentage of the larger dimension of the viewport (Scene).
func (uc *Context) Vmax(value float32) float32 {
return uc.ToDots(value, UnitVmax)
}
// Cm returns a new cm value.
// Cm is logical centimeters. 1cm is 1/2.54 in.
func Cm(value float32) Value {
return Value{Value: value, Unit: UnitCm}
}
// Cm sets the value in terms of cm.
// Cm is logical centimeters. 1cm is 1/2.54 in.
func (v *Value) Cm(value float32) {
v.Value = value
v.Unit = UnitCm
}
// Cm converts the given cm value to dots.
// Cm is logical centimeters. 1cm is 1/2.54 in.
func (uc *Context) Cm(value float32) float32 {
return uc.ToDots(value, UnitCm)
}
// Mm returns a new mm value.
// Mm is logical millimeters. 1mm is 1/10 cm.
func Mm(value float32) Value {
return Value{Value: value, Unit: UnitMm}
}
// Mm sets the value in terms of mm.
// Mm is logical millimeters. 1mm is 1/10 cm.
func (v *Value) Mm(value float32) {
v.Value = value
v.Unit = UnitMm
}
// Mm converts the given mm value to dots.
// Mm is logical millimeters. 1mm is 1/10 cm.
func (uc *Context) Mm(value float32) float32 {
return uc.ToDots(value, UnitMm)
}
// Q returns a new q value.
// Q is logical quarter-millimeters. 1q is 1/40 cm.
func Q(value float32) Value {
return Value{Value: value, Unit: UnitQ}
}
// Q sets the value in terms of q.
// Q is logical quarter-millimeters. 1q is 1/40 cm.
func (v *Value) Q(value float32) {
v.Value = value
v.Unit = UnitQ
}
// Q converts the given q value to dots.
// Q is logical quarter-millimeters. 1q is 1/40 cm.
func (uc *Context) Q(value float32) float32 {
return uc.ToDots(value, UnitQ)
}
// In returns a new in value.
// In is logical inches. 1in is 2.54cm or 96px. This is similar to CSS inches in that it is not necessarily the same as an actual physical inch; it is dependent on the DPI of the display.
func In(value float32) Value {
return Value{Value: value, Unit: UnitIn}
}
// In sets the value in terms of in.
// In is logical inches. 1in is 2.54cm or 96px. This is similar to CSS inches in that it is not necessarily the same as an actual physical inch; it is dependent on the DPI of the display.
func (v *Value) In(value float32) {
v.Value = value
v.Unit = UnitIn
}
// In converts the given in value to dots.
// In is logical inches. 1in is 2.54cm or 96px. This is similar to CSS inches in that it is not necessarily the same as an actual physical inch; it is dependent on the DPI of the display.
func (uc *Context) In(value float32) float32 {
return uc.ToDots(value, UnitIn)
}
// Pc returns a new pc value.
// Pc is logical picas. 1pc is 1/6 in.
func Pc(value float32) Value {
return Value{Value: value, Unit: UnitPc}
}
// Pc sets the value in terms of pc.
// Pc is logical picas. 1pc is 1/6 in.
func (v *Value) Pc(value float32) {
v.Value = value
v.Unit = UnitPc
}
// Pc converts the given pc value to dots.
// Pc is logical picas. 1pc is 1/6 in.
func (uc *Context) Pc(value float32) float32 {
return uc.ToDots(value, UnitPc)
}
// Pt returns a new pt value.
// Pt is points. 1pt is 1/72 in.
func Pt(value float32) Value {
return Value{Value: value, Unit: UnitPt}
}
// Pt sets the value in terms of pt.
// Pt is points. 1pt is 1/72 in.
func (v *Value) Pt(value float32) {
v.Value = value
v.Unit = UnitPt
}
// Pt converts the given pt value to dots.
// Pt is points. 1pt is 1/72 in.
func (uc *Context) Pt(value float32) float32 {
return uc.ToDots(value, UnitPt)
}
// 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 units
import (
"fmt"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/reflectx"
"golang.org/x/image/math/fixed"
)
// NOTE: we have empty labels for value fields, because there is a natural
// flow of the unit values without it. "{{Value}} {{Unit}}" without labels
// makes sense and provides a nicer end-user experience.
// Value and units, and converted value into raw pixels (dots in DPI)
type Value struct { //types:add
// Value is the value in terms of the specified unit
Value float32 `label:""`
// Unit is the unit used for the value
Unit Units `label:""`
// Dots is the computed value in raw pixels (dots in DPI)
Dots float32 `display:"-"`
// Custom is a custom function that returns the dots of the value.
// If it is non-nil, it overrides all other fields.
// Otherwise, the standard ToDots with the other fields is used.
Custom func(uc *Context) float32 `display:"-" json:"-" xml:"-" toml:"-" save:"-"`
}
// New creates a new value with the given unit type
func New(val float32, un Units) Value {
return Value{Value: val, Unit: un}
}
// Set sets the value and units of an existing value
func (v *Value) Set(val float32, un Units) {
v.Value = val
v.Unit = un
}
// Zero returns a new zero (0) value.
func Zero() Value {
return Value{Unit: UnitDot}
}
// Zero sets the value to zero (0).
func (v *Value) Zero() {
v.Value = 0
v.Unit = UnitDot
v.Dots = 0
}
// Dot returns a new dots value.
// Dots are actual real display pixels, which are generally only used internally.
func Dot(val float32) Value {
return Value{Value: val, Unit: UnitDot, Dots: val}
}
// Dot sets the value directly in terms of dots.
// Dots are actual real display pixels, which are generally only used internally.
func (v *Value) Dot(val float32) {
v.Value = val
v.Unit = UnitDot
v.Dots = val
}
// Custom returns a new custom value that has the dots
// of the value returned by the given function.
func Custom(fun func(uc *Context) float32) Value {
return Value{Custom: fun}
}
// SetCustom sets the value to be a custom value that has
// the dots of the value returned by the given function.
func (v *Value) SetCustom(fun func(uc *Context) float32) {
v.Custom = fun
}
// ToDots converts value to raw display pixels (dots as in DPI), setting also
// the Dots field
func (v *Value) ToDots(uc *Context) float32 {
if v.Custom != nil {
v.Dots = v.Custom(uc)
} else {
v.Dots = uc.ToDots(v.Value, v.Unit)
}
return v.Dots
}
// ToDotsFixed converts value to raw display pixels (dots in DPI) in
// fixed-point 26.6 format for rendering
func (v *Value) ToDotsFixed(uc *Context) fixed.Int26_6 {
return fixed.Int26_6(v.ToDots(uc))
}
// Convert converts value to the given units, given unit context
func (v *Value) Convert(to Units, uc *Context) Value {
dots := v.ToDots(uc)
return Value{Value: dots / uc.Dots(to), Unit: to, Dots: dots}
}
// String implements the [fmt.Stringer] interface.
func (v *Value) String() string {
return fmt.Sprintf("%g%s", v.Value, v.Unit.String())
}
// StringCSS returns the value as a string suitable for CSS
// by changing dp to px and using % if applicable.
func (v Value) StringCSS() string {
if v.Unit == UnitDp {
v.Unit = UnitPx // non-pointer so can change directly
}
s := v.String()
if v.Unit == UnitPw || v.Unit == UnitPh || v.Unit == UnitEw || v.Unit == UnitEh {
s = s[:len(s)-2] + "%"
}
return s
}
// SetString sets value from a string
func (v *Value) SetString(str string) error {
trstr := strings.TrimSpace(strings.Replace(str, "%", "pct", -1))
sz := len(trstr)
if sz < 2 {
vc, err := reflectx.ToFloat(str)
if err != nil {
return fmt.Errorf("(units.Value).SetString: unable to convert string value %q into a number: %w", trstr, err)
}
v.Value = float32(vc)
v.Unit = UnitPx
return nil
}
var ends [4]string
ends[0] = strings.ToLower(trstr[sz-1:])
ends[1] = strings.ToLower(trstr[sz-2:])
if sz > 3 {
ends[2] = strings.ToLower(trstr[sz-3:])
}
if sz > 4 {
ends[3] = strings.ToLower(trstr[sz-4:])
}
var numstr string
un := UnitPx // default to pixels
for _, u := range UnitsValues() {
nm := u.String()
unsz := len(nm)
if ends[unsz-1] == nm {
numstr = trstr[:sz-unsz]
un = u
break
}
}
if len(numstr) == 0 { // no units
numstr = trstr
}
var val float32
trspc := strings.TrimSpace(numstr)
n, err := fmt.Sscanf(trspc, "%g", &val)
if err != nil {
return fmt.Errorf("(units.Value).SetString: error scanning string '%s': %w", trspc, err)
}
if n == 0 {
return fmt.Errorf("(units.Value).SetString: no arguments parsed from string '%s'", trspc)
}
v.Set(val, un)
return nil
}
// StringToValue converts a string to a value representation.
func StringToValue(str string) Value {
var v Value
v.SetString(str)
return v
}
// SetAny sets value from an interface value representation as from map[string]any
// key is optional property key for error message -- always logs the error
func (v *Value) SetAny(iface any, key string) error {
switch val := iface.(type) {
case string:
v.SetString(val)
case Value:
*v = val
case *Value:
*v = *val
default: // assume Dp as an implicit default
valflt, err := reflectx.ToFloat(iface)
if err == nil {
v.Dp(float32(valflt))
} else {
err := fmt.Errorf("units.Value: could not set property %q from value: %v of type: %T: %w", key, val, val, err)
return errors.Log(err)
}
}
return nil
}
// 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 units
import (
"cogentcore.org/core/math32"
)
// XY represents unit Value for X and Y dimensions
type XY struct { //types:add
// X is the horizontal axis value
X Value
// Y is the vertical axis value
Y Value
}
// ToDots converts value to raw display pixels (dots as in DPI),
// setting also the Dots field
func (xy *XY) ToDots(uc *Context) {
xy.X.ToDots(uc)
xy.Y.ToDots(uc)
}
// String implements the fmt.Stringer interface.
func (xy *XY) String() string {
return "(" + xy.X.String() + ", " + xy.Y.String() + ")"
}
// Zero sets values to 0
func (xy *XY) Zero() {
xy.X.Zero()
xy.Y.Zero()
}
// Set sets the x and y values according to the given values.
// No values: set both to 0.
// One value: set both to that value.
// Two values: set x to the first value and y to the second value.
func (xy *XY) Set(v ...Value) {
switch len(v) {
case 0:
var zv Value
xy.X = zv
xy.Y = zv
case 1:
xy.X = v[0]
xy.Y = v[0]
default:
xy.X = v[0]
xy.Y = v[1]
}
}
// Dim returns the value for given dimension
func (xy *XY) Dim(d math32.Dims) Value {
switch d {
case math32.X:
return xy.X
case math32.Y:
return xy.Y
default:
panic("units.XY dimension invalid")
}
}
// SetDim sets the value for given dimension
func (xy *XY) SetDim(d math32.Dims, val Value) {
switch d {
case math32.X:
xy.X = val
case math32.Y:
xy.Y = val
default:
panic("units.XY dimension invalid")
}
}
// Dots returns the dots values as a math32.Vector2 vector
func (xy *XY) Dots() math32.Vector2 {
return math32.Vec2(xy.X.Dots, xy.Y.Dots)
}
// 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 styles
import (
"fmt"
"cogentcore.org/core/math32"
)
// XY represents X,Y values
type XY[T any] struct { //types:add
// X is the horizontal axis value
X T
// Y is the vertical axis value
Y T
}
// String implements the fmt.Stringer interface.
func (xy *XY[T]) String() string {
return fmt.Sprintf("(%v, %v)", xy.X, xy.Y)
}
// Set sets the X, Y values according to the given values.
// no values: set to 0.
// 1 value: set both to that value.
// 2 values, set X, Y to the two values respectively.
func (xy *XY[T]) Set(v ...T) {
switch len(v) {
case 0:
var zv T
xy.X = zv
xy.Y = zv
case 1:
xy.X = v[0]
xy.Y = v[0]
default:
xy.X = v[0]
xy.Y = v[1]
}
}
// return the value for given dimension
func (xy *XY[T]) Dim(d math32.Dims) T {
switch d {
case math32.X:
return xy.X
case math32.Y:
return xy.Y
default:
panic("styles.XY dimension invalid")
}
}
// set the value for given dimension
func (xy *XY[T]) SetDim(d math32.Dims, val T) {
switch d {
case math32.X:
xy.X = val
case math32.Y:
xy.Y = val
default:
panic("styles.XY dimension invalid")
}
}
// 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 (
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/math32"
)
// Circle is a SVG circle
type Circle struct {
NodeBase
// position of the center of the circle
Pos math32.Vector2 `xml:"{cx,cy}"`
// radius of the circle
Radius float32 `xml:"r"`
}
func (g *Circle) SVGName() string { return "circle" }
func (g *Circle) Init() {
g.Radius = 1
}
func (g *Circle) SetNodePos(pos math32.Vector2) {
g.Pos = pos.SubScalar(g.Radius)
}
func (g *Circle) SetNodeSize(sz math32.Vector2) {
g.Radius = 0.25 * (sz.X + sz.Y)
}
func (g *Circle) LocalBBox() math32.Box2 {
bb := math32.Box2{}
hlw := 0.5 * g.LocalLineWidth()
bb.Min = g.Pos.SubScalar(g.Radius + hlw)
bb.Max = g.Pos.AddScalar(g.Radius + hlw)
return bb
}
func (g *Circle) Render(sv *SVG) {
vis, pc := g.PushTransform(sv)
if !vis {
return
}
pc.DrawCircle(g.Pos.X, g.Pos.Y, g.Radius)
pc.FillStrokeClear()
g.BBoxes(sv)
g.RenderChildren(sv)
pc.PopTransform()
}
// ApplyTransform applies the given 2D transform to the geometry of this node
// 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())
} else {
g.Pos = xf.MulVector2AsPoint(g.Pos)
scx, scy := xf.ExtractScale()
g.Radius *= 0.5 * (scx + scy)
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)
}
// 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
// todo: needs to be impl
// ClipPath is used for holding a path that renders as a clip path
type ClipPath struct {
NodeBase
}
func (g *ClipPath) SVGName() string { return "clippath" }
// Copyright (c) 2024, 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 main
//go:generate core generate -add-types -add-funcs
import (
"path/filepath"
"strings"
"cogentcore.org/core/cli"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/svg"
)
func main() { //types:skip
opts := cli.DefaultOptions("svg", "Command line tools for rendering and creating svg files")
cli.Run(opts, &Config{}, Render, EmbedImage)
}
type Config struct {
// Input is the filename of the input file
Input string `posarg:"0"`
// Output is the filename of the output file.
// Defaults to input with the extension changed to the output format.
Output string `flag:"o,output"`
// Fill, if specified, indicates to fill the background of
// the svg with the specified color in CSS format.
Fill string
Render RenderConfig `cmd:"render"`
}
type RenderConfig struct {
// Width is the width of the rendered image
Width int `posarg:"1"`
// Height is the height of the rendered image.
// Defaults to width.
Height int `posarg:"2" required:"-"`
}
// Render renders the input svg file to the output image file.
//
//cli:cmd -root
func Render(c *Config) error {
if c.Render.Height == 0 {
c.Render.Height = c.Render.Width
}
sv := svg.NewSVG(c.Render.Width, c.Render.Height)
err := ApplyFill(c, sv)
if err != nil {
return err
}
err = sv.OpenXML(c.Input)
if err != nil {
return err
}
sv.Render()
if c.Output == "" {
c.Output = strings.TrimSuffix(c.Input, filepath.Ext(c.Input)) + ".png"
}
return sv.SavePNG(c.Output)
}
// EmbedImage embeds the input image file into the output svg file.
func EmbedImage(c *Config) error {
sv := svg.NewSVG(0, 0)
err := ApplyFill(c, sv)
if err != nil {
return err
}
img := svg.NewImage(sv.Root)
err = img.OpenImage(c.Input, 0, 0)
if err != nil {
return err
}
sv.Root.ViewBox.Size.SetPoint(img.Pixels.Bounds().Size())
if c.Output == "" {
c.Output = strings.TrimSuffix(c.Input, filepath.Ext(c.Input)) + ".svg"
}
return sv.SaveXML(c.Output)
}
// ApplyFill applies [Config.Fill] to the given [svg.SVG].
func ApplyFill(c *Config, sv *svg.SVG) error { //types:skip
if c.Fill == "" {
return nil
}
bg, err := gradient.FromString(c.Fill)
if err != nil {
return err
}
sv.Background = bg
return nil
}
// 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 (
"log"
"github.com/aymerick/douceur/css"
"github.com/aymerick/douceur/parser"
)
// StyleSheet is a Node2D 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
type StyleSheet struct {
NodeBase
Sheet *css.Stylesheet `copier:"-"`
}
// ParseString parses the string into a StyleSheet of rules, which can then be
// used for extracting properties
func (ss *StyleSheet) ParseString(str string) error {
pss, err := parser.Parse(str)
if err != nil {
log.Printf("styles.StyleSheet ParseString parser error: %v\n", err)
return err
}
ss.Sheet = pss
return nil
}
// CSSProperties returns the properties for each of the rules in this style sheet,
// suitable for setting the CSS value of a node -- returns nil if empty sheet
func (ss *StyleSheet) CSSProperties() map[string]any {
if ss.Sheet == nil {
return nil
}
sz := len(ss.Sheet.Rules)
if sz == 0 {
return nil
}
pr := map[string]any{}
for _, r := range ss.Sheet.Rules {
if r.Kind == css.AtRule {
continue // not supported
}
nd := len(r.Declarations)
if nd == 0 {
continue
}
for _, sel := range r.Selectors {
sp := map[string]any{}
for _, de := range r.Declarations {
sp[de.Property] = de.Value
}
pr[sel] = sp
}
}
return pr
}
////////////////////////////////////////////////////////////////////////////////////////
// MetaData
// MetaData is used for holding meta data info
type MetaData struct {
NodeBase
MetaData string
}
// 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 (
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/math32"
)
// Ellipse is a SVG ellipse
type Ellipse struct {
NodeBase
// position of the center of the ellipse
Pos math32.Vector2 `xml:"{cx,cy}"`
// radii of the ellipse in the horizontal, vertical axes
Radii math32.Vector2 `xml:"{rx,ry}"`
}
func (g *Ellipse) SVGName() string { return "ellipse" }
func (g *Ellipse) Init() {
g.NodeBase.Init()
g.Radii.Set(1, 1)
}
func (g *Ellipse) SetNodePos(pos math32.Vector2) {
g.Pos = pos.Sub(g.Radii)
}
func (g *Ellipse) SetNodeSize(sz math32.Vector2) {
g.Radii = sz.MulScalar(0.5)
}
func (g *Ellipse) LocalBBox() math32.Box2 {
bb := math32.Box2{}
hlw := 0.5 * g.LocalLineWidth()
bb.Min = g.Pos.Sub(g.Radii.AddScalar(hlw))
bb.Max = g.Pos.Add(g.Radii.AddScalar(hlw))
return bb
}
func (g *Ellipse) Render(sv *SVG) {
vis, pc := g.PushTransform(sv)
if !vis {
return
}
pc.DrawEllipse(g.Pos.X, g.Pos.Y, g.Radii.X, g.Radii.Y)
pc.FillStrokeClear()
g.BBoxes(sv)
g.RenderChildren(sv)
pc.PopTransform()
}
// ApplyTransform applies the given 2D transform to the geometry of this node
// each node must define this for itself
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())
} else {
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)
}
// Code generated by "core generate"; DO NOT EDIT.
package svg
import (
"cogentcore.org/core/enums"
)
var _ViewBoxAlignsValues = []ViewBoxAligns{0, 1, 2, 3}
// ViewBoxAlignsN is the highest valid value for type ViewBoxAligns, plus one.
const ViewBoxAlignsN ViewBoxAligns = 4
var _ViewBoxAlignsValueMap = map[string]ViewBoxAligns{`mid`: 0, `none`: 1, `min`: 2, `max`: 3}
var _ViewBoxAlignsDescMap = map[ViewBoxAligns]string{0: `align ViewBox.Min with midpoint of Viewport (default)`, 1: `do not preserve uniform scaling (if either X or Y is None, both are treated as such). In this case, the Meet / Slice value is ignored. This is the same as FitFill from styles.ObjectFits`, 2: `align ViewBox.Min with top / left of Viewport`, 3: `align ViewBox.Min+Size with bottom / right of Viewport`}
var _ViewBoxAlignsMap = map[ViewBoxAligns]string{0: `mid`, 1: `none`, 2: `min`, 3: `max`}
// String returns the string representation of this ViewBoxAligns value.
func (i ViewBoxAligns) String() string { return enums.String(i, _ViewBoxAlignsMap) }
// SetString sets the ViewBoxAligns value from its string representation,
// and returns an error if the string is invalid.
func (i *ViewBoxAligns) SetString(s string) error {
return enums.SetString(i, s, _ViewBoxAlignsValueMap, "ViewBoxAligns")
}
// Int64 returns the ViewBoxAligns value as an int64.
func (i ViewBoxAligns) Int64() int64 { return int64(i) }
// SetInt64 sets the ViewBoxAligns value from an int64.
func (i *ViewBoxAligns) SetInt64(in int64) { *i = ViewBoxAligns(in) }
// Desc returns the description of the ViewBoxAligns value.
func (i ViewBoxAligns) Desc() string { return enums.Desc(i, _ViewBoxAlignsDescMap) }
// ViewBoxAlignsValues returns all possible values for the type ViewBoxAligns.
func ViewBoxAlignsValues() []ViewBoxAligns { return _ViewBoxAlignsValues }
// Values returns all possible values for the type ViewBoxAligns.
func (i ViewBoxAligns) Values() []enums.Enum { return enums.Values(_ViewBoxAlignsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i ViewBoxAligns) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *ViewBoxAligns) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "ViewBoxAligns")
}
var _ViewBoxMeetOrSliceValues = []ViewBoxMeetOrSlice{0, 1}
// ViewBoxMeetOrSliceN is the highest valid value for type ViewBoxMeetOrSlice, plus one.
const ViewBoxMeetOrSliceN ViewBoxMeetOrSlice = 2
var _ViewBoxMeetOrSliceValueMap = map[string]ViewBoxMeetOrSlice{`meet`: 0, `slice`: 1}
var _ViewBoxMeetOrSliceDescMap = map[ViewBoxMeetOrSlice]string{0: `Meet only applies if Align != None (i.e., only for uniform scaling), and means the entire ViewBox is visible within Viewport, and it is scaled up as much as possible to meet the align constraints. This is the same as FitContain from styles.ObjectFits`, 1: `Slice only applies if Align != None (i.e., only for uniform scaling), and means the entire ViewBox is covered by the ViewBox, and the ViewBox is scaled down as much as possible, while still meeting the align constraints. This is the same as FitCover from styles.ObjectFits`}
var _ViewBoxMeetOrSliceMap = map[ViewBoxMeetOrSlice]string{0: `meet`, 1: `slice`}
// String returns the string representation of this ViewBoxMeetOrSlice value.
func (i ViewBoxMeetOrSlice) String() string { return enums.String(i, _ViewBoxMeetOrSliceMap) }
// SetString sets the ViewBoxMeetOrSlice value from its string representation,
// and returns an error if the string is invalid.
func (i *ViewBoxMeetOrSlice) SetString(s string) error {
return enums.SetString(i, s, _ViewBoxMeetOrSliceValueMap, "ViewBoxMeetOrSlice")
}
// Int64 returns the ViewBoxMeetOrSlice value as an int64.
func (i ViewBoxMeetOrSlice) Int64() int64 { return int64(i) }
// SetInt64 sets the ViewBoxMeetOrSlice value from an int64.
func (i *ViewBoxMeetOrSlice) SetInt64(in int64) { *i = ViewBoxMeetOrSlice(in) }
// Desc returns the description of the ViewBoxMeetOrSlice value.
func (i ViewBoxMeetOrSlice) Desc() string { return enums.Desc(i, _ViewBoxMeetOrSliceDescMap) }
// ViewBoxMeetOrSliceValues returns all possible values for the type ViewBoxMeetOrSlice.
func ViewBoxMeetOrSliceValues() []ViewBoxMeetOrSlice { return _ViewBoxMeetOrSliceValues }
// Values returns all possible values for the type ViewBoxMeetOrSlice.
func (i ViewBoxMeetOrSlice) Values() []enums.Enum { return enums.Values(_ViewBoxMeetOrSliceValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i ViewBoxMeetOrSlice) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *ViewBoxMeetOrSlice) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "ViewBoxMeetOrSlice")
}
// 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
// Filter represents SVG filter* elements
type Filter struct {
NodeBase
FilterType string
}
func (g *Filter) SVGName() string { return "filter" }
// 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
// Flow represents SVG flow* elements
type Flow struct {
NodeBase
FlowType string
}
func (g *Flow) SVGName() string { return "flow" }
// 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 (
"log"
"strings"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/math32"
)
/////////////////////////////////////////////////////////////////////////////
// Gradient
// Gradient is used for holding a specified color gradient.
// The name is the id for lookup in url
type Gradient struct {
NodeBase
// the color gradient
Grad gradient.Gradient
// name of another gradient to get stops from
StopsName string
}
// GradientTypeName returns the SVG-style type name of gradient: linearGradient or radialGradient
func (gr *Gradient) GradientTypeName() string {
if _, ok := gr.Grad.(*gradient.Radial); ok {
return "radialGradient"
}
return "linearGradient"
}
//////////////////////////////////////////////////////////////////////////////
// SVG gradient management
// GradientByName returns the gradient of given name, stored on SVG node
func (sv *SVG) GradientByName(n Node, grnm string) *Gradient {
gri := sv.NodeFindURL(n, grnm)
if gri == nil {
return nil
}
gr, ok := gri.(*Gradient)
if !ok {
log.Printf("SVG Found element named: %v but isn't a Gradient type, instead is: %T", grnm, gri)
return nil
}
return gr
}
// GradientApplyTransform applies the given transform to any gradients for this node,
// that are using specific coordinates (not bounding box which is automatic)
func (g *NodeBase) GradientApplyTransform(sv *SVG, xf math32.Matrix2) {
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?
}
}
gnm = NodePropURL(gi, "stroke")
if gnm != "" {
gr := sv.GradientByName(gi, gnm)
if gr != nil {
gr.Grad.AsBase().Transform.SetMul(xf)
}
}
}
// 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) {
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?
}
}
gnm = NodePropURL(gi, "stroke")
if gnm != "" {
gr := sv.GradientByName(gi, gnm)
if gr != nil {
gr.Grad.AsBase().Transform.SetMulCenter(xf, pt)
}
}
}
// 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)
}
}
}
// 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
// GradientUpdateStops copies stops from StopsName gradient if it is set
func (sv *SVG) GradientUpdateStops(gr *Gradient) {
if gr.StopsName == "" {
return
}
sgr := sv.GradientByName(gr, gr.StopsName)
if sgr != nil {
gr.Grad.AsBase().CopyStopsFrom(sgr.Grad.AsBase())
}
}
// GradientDeleteForNode deletes the node-specific gradient on given node
// of given name, which can be a full url(# name or just the bare name.
// Returns true if deleted.
func (sv *SVG) GradientDeleteForNode(n Node, grnm string) bool {
gr := sv.GradientByName(n, grnm)
if gr == nil || gr.StopsName == "" {
return false
}
unm := NameFromURL(grnm)
sv.Defs.DeleteChildByName(unm)
return true
}
// GradientNewForNode adds a new gradient specific to given node
// 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) {
gr, url := sv.GradientNew(radial)
gr.StopsName = stops
gr.Grad.AsBase().SetBox(n.LocalBBox())
sv.GradientUpdateStops(gr)
return gr, url
}
// GradientNew adds a new gradient, either linear or radial,
// with a new unique id
func (sv *SVG) GradientNew(radial bool) (*Gradient, string) {
gnm := ""
if radial {
gnm = "radialGradient"
} else {
gnm = "linearGradient"
}
gr := NewGradient(sv.Defs)
id := sv.NewUniqueID()
gr.SetName(NameID(gnm, id))
url := NameToURL(gnm)
if radial {
gr.Grad = gradient.NewRadial()
} else {
gr.Grad = gradient.NewLinear()
}
return gr, url
}
// 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)
if ps == nil {
gr, url := sv.GradientNewForNode(n, radial, stops)
n.AsTree().SetProperty(prop, url)
return gr, url
}
pstr := ps.(string)
trgst := ""
if radial {
trgst = "radialGradient"
} else {
trgst = "linearGradient"
}
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 strings.HasPrefix(pstr, "url(#") { // wrong kind
sv.GradientDeleteForNode(n, pstr)
}
gr, url := sv.GradientNewForNode(n, radial, stops)
n.AsTree().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())
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
// should be removed manually, as they are not automatically generated.
func (sv *SVG) GradientUpdateAllStops() {
for _, k := range sv.Defs.Children {
gr, ok := k.(*Gradient)
if ok {
sv.GradientUpdateStops(gr)
}
}
}
// 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/base/slicesx"
"cogentcore.org/core/math32"
)
// Group groups together SVG elements.
// Provides a common transform for all group elements
// and shared style properties.
type Group struct {
NodeBase
}
func (g *Group) SVGName() string { return "g" }
func (g *Group) EnforceSVGName() bool { return false }
// BBoxFromChildren sets the Group BBox from children
func BBoxFromChildren(n Node) image.Rectangle {
bb := image.Rectangle{}
for i, kid := range n.AsTree().Children {
kn := kid.(Node)
knb := kn.AsNodeBase()
if i == 0 {
bb = knb.BBox
} else {
bb = bb.Union(knb.BBox)
}
}
return bb
}
func (g *Group) NodeBBox(sv *SVG) image.Rectangle {
bb := BBoxFromChildren(g)
return bb
}
func (g *Group) Render(sv *SVG) {
pc := &g.Paint
rs := &sv.RenderState
if pc.Off || rs == nil {
return
}
rs.PushTransform(pc.Transform)
g.RenderChildren(sv)
g.BBoxes(sv) // must come after render
rs.PopTransform()
}
// ApplyTransform applies the given 2D transform to the geometry of this node
// 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)
}
// Copyright (c) 2021, 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 (
"errors"
"image"
"log"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"golang.org/x/image/draw"
"golang.org/x/image/math/f64"
)
// Image is an SVG image (bitmap)
type Image struct {
NodeBase
// position of the top-left of the image
Pos math32.Vector2 `xml:"{x,y}"`
// rendered size of the image (imposes a scaling on image when it is rendered)
Size math32.Vector2 `xml:"{width,height}"`
// file name of image loaded -- set by OpenImage
Filename string
// how to scale and align the image
ViewBox ViewBox `xml:"viewbox"`
// the image pixels
Pixels *image.RGBA `xml:"-" json:"-" display:"-"`
}
func (g *Image) SVGName() string { return "image" }
func (g *Image) SetNodePos(pos math32.Vector2) {
g.Pos = pos
}
func (g *Image) SetNodeSize(sz math32.Vector2) {
g.Size = sz
}
// SetImageSize sets size of the bitmap image.
// This does not resize any existing image, just makes a new image
// if the size is different
func (g *Image) SetImageSize(nwsz image.Point) {
if nwsz.X == 0 || nwsz.Y == 0 {
return
}
if g.Pixels != nil && g.Pixels.Bounds().Size() == nwsz {
return
}
g.Pixels = image.NewRGBA(image.Rectangle{Max: nwsz})
}
// SetImage sets 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. Copies from given image into internal image for this bitmap.
func (g *Image) SetImage(img image.Image, width, height float32) {
sz := img.Bounds().Size()
if width <= 0 && height <= 0 {
g.SetImageSize(sz)
draw.Draw(g.Pixels, g.Pixels.Bounds(), img, image.Point{}, draw.Src)
if g.Size.X == 0 && g.Size.Y == 0 {
g.Size = math32.FromPoint(sz)
}
} else {
tsz := sz
transformer := draw.BiLinear
scx := float32(1)
scy := float32(1)
if width > 0 {
scx = width / float32(sz.X)
tsz.X = int(width)
}
if height > 0 {
scy = height / float32(sz.Y)
tsz.Y = int(height)
}
g.SetImageSize(tsz)
m := math32.Scale2D(scx, scy)
s2d := f64.Aff3{float64(m.XX), float64(m.XY), float64(m.X0), float64(m.YX), float64(m.YY), float64(m.Y0)}
transformer.Transform(g.Pixels, s2d, img, img.Bounds(), draw.Over, nil)
if g.Size.X == 0 && g.Size.Y == 0 {
g.Size = math32.FromPoint(tsz)
}
}
}
func (g *Image) DrawImage(sv *SVG) {
if g.Pixels == nil {
return
}
pc := &paint.Context{&sv.RenderState, &g.Paint}
pc.DrawImageScaled(g.Pixels, g.Pos.X, g.Pos.Y, g.Size.X, g.Size.Y)
}
func (g *Image) NodeBBox(sv *SVG) image.Rectangle {
rs := &sv.RenderState
pos := rs.CurrentTransform.MulVector2AsPoint(g.Pos)
max := rs.CurrentTransform.MulVector2AsPoint(g.Pos.Add(g.Size))
posi := pos.ToPointCeil()
maxi := max.ToPointCeil()
return image.Rectangle{posi, maxi}.Canon()
}
func (g *Image) LocalBBox() math32.Box2 {
bb := math32.Box2{}
bb.Min = g.Pos
bb.Max = g.Pos.Add(g.Size)
return bb
}
func (g *Image) Render(sv *SVG) {
vis, rs := g.PushTransform(sv)
if !vis {
return
}
g.DrawImage(sv)
g.BBoxes(sv)
g.RenderChildren(sv)
rs.PopTransform()
}
// ApplyTransform applies the given 2D transform to the geometry of this node
// 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() {
g.Paint.Transform.SetMul(xf)
g.SetProperty("transform", g.Paint.Transform.String())
} else {
g.Pos = xf.MulVector2AsPoint(g.Pos)
g.Size = xf.MulVector2AsVector(g.Size)
}
}
// 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
func (g *Image) OpenImage(filename string, width, height float32) error {
img, _, err := imagex.Open(filename)
if err != nil {
log.Printf("svg.OpenImage -- could not open file: %v, err: %v\n", filename, err)
return err
}
g.Filename = filename
g.SetImage(img, width, height)
return nil
}
// SaveImage saves current image to a file
func (g *Image) SaveImage(filename string) error {
if g.Pixels == nil {
return errors.New("svg.SaveImage Pixels is nil")
}
return imagex.Save(g.Pixels, filename)
}
// 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.
// 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"
"encoding/xml"
"errors"
"fmt"
"io"
"io/fs"
"log"
"os"
"strings"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/tree"
"golang.org/x/net/html/charset"
)
// this file contains all the IO-related parsing etc routines
// see https://cogentcore.org/core/ki/wiki/Naming for IO naming conventions
// using standard XML marshal / unmarshal
var (
errParamMismatch = errors.New("SVG Parse: Param mismatch")
errCommandUnknown = errors.New("SVG Parse: Unknown command")
errZeroLengthID = errors.New("SVG Parse: zero length id")
errMissingID = errors.New("SVG Parse: cannot find id")
)
// OpenXML Opens XML-formatted SVG input from given file
func (sv *SVG) OpenXML(fname string) error {
filename := fname
fi, err := os.Stat(filename)
if err != nil {
log.Println(err)
return err
}
if fi.IsDir() {
err := fmt.Errorf("svg.OpenXML: file is a directory: %v", filename)
log.Println(err)
return err
}
fp, err := os.Open(filename)
if err != nil {
log.Println(err)
return err
}
defer fp.Close()
return sv.ReadXML(bufio.NewReader(fp))
}
// OpenFS Opens XML-formatted SVG input from given file, filesystem FS
func (sv *SVG) OpenFS(fsys fs.FS, fname string) error {
fp, err := fsys.Open(fname)
if err != nil {
return err
}
defer fp.Close()
return sv.ReadXML(bufio.NewReader(fp))
}
// ReadXML reads XML-formatted SVG input from io.Reader, and uses
// xml.Decoder to create the SVG scenegraph for corresponding SVG drawing.
// Removes any existing content in SVG first. To process a byte slice, pass:
// bytes.NewReader([]byte(str)) -- all errors are logged and also returned.
func (sv *SVG) ReadXML(reader io.Reader) error {
decoder := xml.NewDecoder(reader)
decoder.Strict = false
decoder.AutoClose = xml.HTMLAutoClose
decoder.Entity = xml.HTMLEntity
decoder.CharsetReader = charset.NewReaderLabel
var err error
outer:
for {
var t xml.Token
t, err = decoder.Token()
if err != nil {
if err == io.EOF {
break
}
log.Printf("SVG parsing error: %v\n", err)
break
}
switch se := t.(type) {
case xml.StartElement:
err = sv.UnmarshalXML(decoder, se)
break outer
// todo: ignore rest?
}
}
if err == io.EOF {
return nil
}
return err
}
// UnmarshalXML unmarshals the svg using xml.Decoder
func (sv *SVG) UnmarshalXML(decoder *xml.Decoder, se xml.StartElement) error {
start := &se
sv.DeleteAll()
curPar := sv.Root.This.(Node) // current parent node into which elements are created
curSvg := sv.Root
inTitle := false
inDesc := false
inDef := false
inCSS := false
var curCSS *StyleSheet
inTxt := false
var curTxt *Text
inTspn := false
var curTspn *Text
var defPrevPar Node // previous parent before a def encountered
for {
var t xml.Token
var err error
if start != nil {
t = *start
start = nil
} else {
t, err = decoder.Token()
}
if err != nil {
if err == io.EOF {
break
}
log.Printf("SVG parsing error: %v\n", err)
return err
}
switch se := t.(type) {
case xml.StartElement:
nm := se.Name.Local
switch {
case nm == "svg":
// if curPar != sv.This {
// curPar = curPar.NewChild(TypeSVG, "svg").(Node)
// }
for _, attr := range se.Attr {
// if SetStdXMLAttr(curSvg, attr.Name.Local, attr.Value) {
// continue
// }
switch attr.Name.Local {
case "viewBox":
pts := math32.ReadPoints(attr.Value)
if len(pts) != 4 {
return errParamMismatch
}
curSvg.ViewBox.Min.X = pts[0]
curSvg.ViewBox.Min.Y = pts[1]
curSvg.ViewBox.Size.X = pts[2]
curSvg.ViewBox.Size.Y = pts[3]
case "width":
sv.PhysicalWidth.SetString(attr.Value)
sv.PhysicalWidth.ToDots(&curSvg.Paint.UnitContext)
case "height":
sv.PhysicalHeight.SetString(attr.Value)
sv.PhysicalHeight.ToDots(&curSvg.Paint.UnitContext)
case "preserveAspectRatio":
curSvg.ViewBox.PreserveAspectRatio.SetString(attr.Value)
default:
curPar.AsTree().SetProperty(attr.Name.Local, attr.Value)
}
}
case nm == "desc":
inDesc = true
case nm == "title":
inTitle = true
case nm == "defs":
inDef = true
defPrevPar = curPar
curPar = sv.Defs
case nm == "g":
curPar = NewGroup(curPar)
for _, attr := range se.Attr {
if SetStandardXMLAttr(curPar.AsNodeBase(), attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
default:
curPar.AsTree().SetProperty(attr.Name.Local, attr.Value)
}
}
case nm == "rect":
rect := NewRect(curPar)
var x, y, w, h, rx, ry float32
for _, attr := range se.Attr {
if SetStandardXMLAttr(rect, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
case "x":
x, err = math32.ParseFloat32(attr.Value)
case "y":
y, err = math32.ParseFloat32(attr.Value)
case "width":
w, err = math32.ParseFloat32(attr.Value)
case "height":
h, err = math32.ParseFloat32(attr.Value)
case "rx":
rx, err = math32.ParseFloat32(attr.Value)
case "ry":
ry, err = math32.ParseFloat32(attr.Value)
default:
rect.SetProperty(attr.Name.Local, attr.Value)
}
if err != nil {
return err
}
}
rect.Pos.Set(x, y)
rect.Size.Set(w, h)
rect.Radius.Set(rx, ry)
case nm == "circle":
circle := NewCircle(curPar)
var cx, cy, r float32
for _, attr := range se.Attr {
if SetStandardXMLAttr(circle, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
case "cx":
cx, err = math32.ParseFloat32(attr.Value)
case "cy":
cy, err = math32.ParseFloat32(attr.Value)
case "r":
r, err = math32.ParseFloat32(attr.Value)
default:
circle.SetProperty(attr.Name.Local, attr.Value)
}
if err != nil {
return err
}
}
circle.Pos.Set(cx, cy)
circle.Radius = r
case nm == "ellipse":
ellipse := NewEllipse(curPar)
var cx, cy, rx, ry float32
for _, attr := range se.Attr {
if SetStandardXMLAttr(ellipse, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
case "cx":
cx, err = math32.ParseFloat32(attr.Value)
case "cy":
cy, err = math32.ParseFloat32(attr.Value)
case "rx":
rx, err = math32.ParseFloat32(attr.Value)
case "ry":
ry, err = math32.ParseFloat32(attr.Value)
default:
ellipse.SetProperty(attr.Name.Local, attr.Value)
}
if err != nil {
return err
}
}
ellipse.Pos.Set(cx, cy)
ellipse.Radii.Set(rx, ry)
case nm == "line":
line := NewLine(curPar)
var x1, x2, y1, y2 float32
for _, attr := range se.Attr {
if SetStandardXMLAttr(line, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
case "x1":
x1, err = math32.ParseFloat32(attr.Value)
case "y1":
y1, err = math32.ParseFloat32(attr.Value)
case "x2":
x2, err = math32.ParseFloat32(attr.Value)
case "y2":
y2, err = math32.ParseFloat32(attr.Value)
default:
line.SetProperty(attr.Name.Local, attr.Value)
}
if err != nil {
return err
}
}
line.Start.Set(x1, y1)
line.End.Set(x2, y2)
case nm == "polygon":
polygon := NewPolygon(curPar)
for _, attr := range se.Attr {
if SetStandardXMLAttr(polygon, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
case "points":
pts := math32.ReadPoints(attr.Value)
if pts != nil {
sz := len(pts)
if sz%2 != 0 {
err = fmt.Errorf("SVG polygon has an odd number of points: %v str: %v", sz, attr.Value)
log.Println(err)
return err
}
pvec := make([]math32.Vector2, sz/2)
for ci := 0; ci < sz/2; ci++ {
pvec[ci].Set(pts[ci*2], pts[ci*2+1])
}
polygon.Points = pvec
}
default:
polygon.SetProperty(attr.Name.Local, attr.Value)
}
if err != nil {
return err
}
}
case nm == "polyline":
polyline := NewPolyline(curPar)
for _, attr := range se.Attr {
if SetStandardXMLAttr(polyline, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
case "points":
pts := math32.ReadPoints(attr.Value)
if pts != nil {
sz := len(pts)
if sz%2 != 0 {
err = fmt.Errorf("SVG polyline has an odd number of points: %v str: %v", sz, attr.Value)
log.Println(err)
return err
}
pvec := make([]math32.Vector2, sz/2)
for ci := 0; ci < sz/2; ci++ {
pvec[ci].Set(pts[ci*2], pts[ci*2+1])
}
polyline.Points = pvec
}
default:
polyline.SetProperty(attr.Name.Local, attr.Value)
}
if err != nil {
return err
}
}
case nm == "path":
path := NewPath(curPar)
for _, attr := range se.Attr {
if attr.Name.Local == "original-d" {
continue
}
if SetStandardXMLAttr(path, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
case "d":
path.SetData(attr.Value)
default:
path.SetProperty(attr.Name.Local, attr.Value)
}
if err != nil {
return err
}
}
case nm == "image":
img := NewImage(curPar)
var x, y, w, h float32
for _, attr := range se.Attr {
if SetStandardXMLAttr(img, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
case "x":
x, err = math32.ParseFloat32(attr.Value)
case "y":
y, err = math32.ParseFloat32(attr.Value)
case "width":
w, err = math32.ParseFloat32(attr.Value)
case "height":
h, err = math32.ParseFloat32(attr.Value)
case "preserveAspectRatio":
img.ViewBox.PreserveAspectRatio.SetString(attr.Value)
case "href":
if len(attr.Value) > 11 && attr.Value[:11] == "data:image/" {
es := attr.Value[11:]
fmti := strings.Index(es, ";")
fm := es[:fmti]
bs64 := es[fmti+1 : fmti+8]
if bs64 != "base64," {
log.Printf("image base64 encoding string not properly formatted: %s\n", bs64)
}
eb := []byte(es[fmti+8:])
im, err := imagex.FromBase64(fm, eb)
if err != nil {
log.Println(err)
} else {
img.SetImage(im, 0, 0)
}
} else { // url
}
default:
img.SetProperty(attr.Name.Local, attr.Value)
}
if err != nil {
return err
}
}
img.Pos.Set(x, y)
img.Size.Set(w, h)
case nm == "tspan":
fallthrough
case nm == "text":
var txt *Text
if se.Name.Local == "text" {
txt = NewText(curPar)
inTxt = true
curTxt = txt
} else {
if (inTxt && curTxt != nil) || curPar == nil {
txt = NewText(curTxt)
tree.SetUniqueName(txt)
txt.Pos = curTxt.Pos
} else if curTxt != nil {
txt = NewText(curPar)
tree.SetUniqueName(txt)
}
inTspn = true
curTspn = txt
}
if txt == nil {
break
}
for _, attr := range se.Attr {
if SetStandardXMLAttr(txt, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
case "x":
pts := math32.ReadPoints(attr.Value)
if len(pts) > 1 {
txt.CharPosX = pts
} else if len(pts) == 1 {
txt.Pos.X = pts[0]
}
case "y":
pts := math32.ReadPoints(attr.Value)
if len(pts) > 1 {
txt.CharPosY = pts
} else if len(pts) == 1 {
txt.Pos.Y = pts[0]
}
case "dx":
pts := math32.ReadPoints(attr.Value)
if len(pts) > 0 {
txt.CharPosDX = pts
}
case "dy":
pts := math32.ReadPoints(attr.Value)
if len(pts) > 0 {
txt.CharPosDY = pts
}
case "rotate":
pts := math32.ReadPoints(attr.Value)
if len(pts) > 0 {
txt.CharRots = pts
}
case "textLength":
tl, err := math32.ParseFloat32(attr.Value)
if err != nil {
txt.TextLength = tl
}
case "lengthAdjust":
if attr.Value == "spacingAndGlyphs" {
txt.AdjustGlyphs = true
} else {
txt.AdjustGlyphs = false
}
default:
txt.SetProperty(attr.Name.Local, attr.Value)
}
if err != nil {
return err
}
}
case nm == "linearGradient":
grad := NewGradient(curPar)
for _, attr := range se.Attr {
if SetStandardXMLAttr(grad, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
case "href":
nm := attr.Value
nm = strings.TrimPrefix(nm, "#")
hr := curPar.AsTree().ChildByName(nm, 0)
if hr != nil {
if hrg, ok := hr.(*Gradient); ok {
grad.StopsName = nm
grad.Grad = gradient.CopyOf(hrg.Grad)
if _, ok := grad.Grad.(*gradient.Linear); !ok {
cp := grad.Grad
grad.Grad = gradient.NewLinear()
*grad.Grad.AsBase() = *cp.AsBase()
}
}
}
}
}
err = gradient.UnmarshalXML(&grad.Grad, decoder, se)
if err != nil {
return err
}
case nm == "radialGradient":
grad := NewGradient(curPar)
for _, attr := range se.Attr {
if SetStandardXMLAttr(grad, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
case "href":
nm := attr.Value
nm = strings.TrimPrefix(nm, "#")
hr := curPar.AsTree().ChildByName(nm, 0)
if hr != nil {
if hrg, ok := hr.(*Gradient); ok {
grad.StopsName = nm
grad.Grad = gradient.CopyOf(hrg.Grad)
if _, ok := grad.Grad.(*gradient.Radial); !ok {
cp := grad.Grad
grad.Grad = gradient.NewRadial()
*grad.Grad.AsBase() = *cp.AsBase()
}
}
}
}
}
err = gradient.UnmarshalXML(&grad.Grad, decoder, se)
if err != nil {
return err
}
case nm == "style":
sty := NewStyleSheet(curPar)
for _, attr := range se.Attr {
if SetStandardXMLAttr(sty, attr.Name.Local, attr.Value) {
continue
}
}
inCSS = true
curCSS = sty
// style code shows up in CharData below
case nm == "clipPath":
curPar = NewClipPath(curPar)
cp := curPar.(*ClipPath)
for _, attr := range se.Attr {
if SetStandardXMLAttr(cp, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
default:
cp.SetProperty(attr.Name.Local, attr.Value)
}
}
case nm == "marker":
curPar = NewMarker(curPar)
mrk := curPar.(*Marker)
var rx, ry float32
szx := float32(3)
szy := float32(3)
for _, attr := range se.Attr {
if SetStandardXMLAttr(mrk, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
case "refX":
rx, err = math32.ParseFloat32(attr.Value)
case "refY":
ry, err = math32.ParseFloat32(attr.Value)
case "markerWidth":
szx, err = math32.ParseFloat32(attr.Value)
case "markerHeight":
szy, err = math32.ParseFloat32(attr.Value)
case "matrixUnits":
if attr.Value == "strokeWidth" {
mrk.Units = StrokeWidth
} else {
mrk.Units = UserSpaceOnUse
}
case "viewBox":
pts := math32.ReadPoints(attr.Value)
if len(pts) != 4 {
return errParamMismatch
}
mrk.ViewBox.Min.X = pts[0]
mrk.ViewBox.Min.Y = pts[1]
mrk.ViewBox.Size.X = pts[2]
mrk.ViewBox.Size.Y = pts[3]
case "orient":
mrk.Orient = attr.Value
default:
mrk.SetProperty(attr.Name.Local, attr.Value)
}
if err != nil {
return err
}
}
mrk.RefPos.Set(rx, ry)
mrk.Size.Set(szx, szy)
case nm == "use":
link := gradient.XMLAttr("href", se.Attr)
itm := sv.FindNamedElement(link)
if itm != nil {
cln := itm.AsTree().Clone().(Node)
if cln != nil {
curPar.AsTree().AddChild(cln)
for _, attr := range se.Attr {
if SetStandardXMLAttr(cln.AsNodeBase(), attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
default:
cln.AsTree().SetProperty(attr.Name.Local, attr.Value)
}
}
}
}
case nm == "Work":
fallthrough
case nm == "RDF":
fallthrough
case nm == "format":
fallthrough
case nm == "type":
fallthrough
case nm == "namedview":
fallthrough
case nm == "perspective":
fallthrough
case nm == "grid":
fallthrough
case nm == "guide":
fallthrough
case nm == "metadata":
curPar = NewMetaData(curPar)
md := curPar.(*MetaData)
md.Class = nm
for _, attr := range se.Attr {
if SetStandardXMLAttr(md, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
default:
curPar.AsTree().SetProperty(attr.Name.Local, attr.Value)
}
}
case strings.HasPrefix(nm, "flow"):
curPar = NewFlow(curPar)
md := curPar.(*Flow)
md.Class = nm
md.FlowType = nm
for _, attr := range se.Attr {
if SetStandardXMLAttr(md, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
default:
curPar.AsTree().SetProperty(attr.Name.Local, attr.Value)
}
}
case strings.HasPrefix(nm, "fe"):
fallthrough
case strings.HasPrefix(nm, "path-effect"):
fallthrough
case strings.HasPrefix(nm, "filter"):
curPar = NewFilter(curPar)
md := curPar.(*Filter)
md.Class = nm
md.FilterType = nm
for _, attr := range se.Attr {
if SetStandardXMLAttr(md, attr.Name.Local, attr.Value) {
continue
}
switch attr.Name.Local {
default:
curPar.AsTree().SetProperty(attr.Name.Local, attr.Value)
}
}
default:
errStr := "SVG Cannot process svg element " + se.Name.Local
log.Println(errStr)
// IconAutoOpen = false
}
case xml.EndElement:
switch se.Name.Local {
case "title":
inTitle = false
case "desc":
inDesc = false
case "style":
inCSS = false
curCSS = nil
case "text":
inTxt = false
curTxt = nil
case "tspan":
inTspn = false
curTspn = nil
case "defs":
if inDef {
inDef = false
curPar = defPrevPar
}
case "rect":
case "circle":
case "ellipse":
case "line":
case "polygon":
case "polyline":
case "path":
case "use":
case "linearGradient":
case "radialGradient":
default:
if curPar == sv.Root.This {
break
}
if curPar.AsTree().Parent == nil {
break
}
curPar = curPar.AsTree().Parent.(Node)
if curPar == sv.Root.This {
break
}
r := tree.ParentByType[*Root](curPar)
if r != nil {
curSvg = r
}
}
case xml.CharData:
// (ok, md := curPar.(*MetaData); ok)
trspc := strings.TrimSpace(string(se))
switch {
// case :
// md.MetaData = string(se)
case inTitle:
sv.Title += trspc
case inDesc:
sv.Desc += trspc
case inTspn && curTspn != nil:
curTspn.Text = trspc
case inTxt && curTxt != nil:
curTxt.Text = trspc
case inCSS && curCSS != nil:
curCSS.ParseString(trspc)
cp := curCSS.CSSProperties()
if cp != nil {
if inDef && defPrevPar != nil {
defPrevPar.AsNodeBase().CSS = cp
} else {
curPar.AsNodeBase().CSS = cp
}
}
}
}
}
return nil
}
////////////////////////////////////////////////////////////////////////////////////
// Writing
// SaveXML saves the svg to a XML-encoded file, using WriteXML
func (sv *SVG) SaveXML(fname string) error {
filename := string(fname)
fp, err := os.Create(filename)
if err != nil {
log.Println(err)
return err
}
defer fp.Close()
bw := bufio.NewWriter(fp)
err = sv.WriteXML(bw, true)
if err != nil {
log.Println(err)
return err
}
err = bw.Flush()
if err != nil {
log.Println(err)
}
return err
}
// WriteXML writes XML-formatted SVG output to io.Writer, and uses
// XMLEncoder
func (sv *SVG) WriteXML(wr io.Writer, indent bool) error {
enc := NewXMLEncoder(wr)
if indent {
enc.Indent("", " ")
}
sv.MarshalXMLx(enc, xml.StartElement{})
enc.Flush()
return nil
}
func XMLAddAttr(attr *[]xml.Attr, name, val string) {
at := xml.Attr{}
at.Name.Local = name
at.Value = val
*attr = append(*attr, at)
}
// InkscapeProperties are property keys that should be prefixed with "inkscape:"
var InkscapeProperties = map[string]bool{
"isstock": true,
"stockid": true,
}
// MarshalXML encodes just the given node under SVG to XML.
// It returns the name of node, for end tag; if empty, then children will not be
// output.
func MarshalXML(n tree.Node, enc *XMLEncoder, setName string) string {
if n == nil || n.AsTree().This == nil {
return ""
}
se := xml.StartElement{}
properties := n.AsTree().Properties
if n.AsTree().Name != "" {
XMLAddAttr(&se.Attr, "id", n.AsTree().Name)
}
text := "" // if non-empty, contains text to render
_, issvg := n.(Node)
_, isgp := n.(*Group)
_, ismark := n.(*Marker)
if !isgp {
if issvg && !ismark {
sp := styles.StylePropertiesXML(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
nm := ""
switch nd := n.(type) {
case *Path:
nm = "path"
nd.DataStr = PathDataString(nd.Data)
XMLAddAttr(&se.Attr, "d", nd.DataStr)
case *Group:
nm = "g"
if strings.HasPrefix(strings.ToLower(n.AsTree().Name), "layer") {
}
for k, v := range properties {
sv := reflectx.ToString(v)
switch k {
case "opacity", "transform":
XMLAddAttr(&se.Attr, k, sv)
case "groupmode":
XMLAddAttr(&se.Attr, "inkscape:groupmode", sv)
if st, has := properties["style"]; has {
XMLAddAttr(&se.Attr, "style", reflectx.ToString(st))
} else {
XMLAddAttr(&se.Attr, "style", "display:inline")
}
case "insensitive":
if sv == "true" {
XMLAddAttr(&se.Attr, "sodipodi:"+k, sv)
}
}
}
case *Rect:
nm = "rect"
XMLAddAttr(&se.Attr, "x", fmt.Sprintf("%g", nd.Pos.X))
XMLAddAttr(&se.Attr, "y", fmt.Sprintf("%g", nd.Pos.Y))
XMLAddAttr(&se.Attr, "width", fmt.Sprintf("%g", nd.Size.X))
XMLAddAttr(&se.Attr, "height", fmt.Sprintf("%g", nd.Size.Y))
case *Circle:
nm = "circle"
XMLAddAttr(&se.Attr, "cx", fmt.Sprintf("%g", nd.Pos.X))
XMLAddAttr(&se.Attr, "cy", fmt.Sprintf("%g", nd.Pos.Y))
XMLAddAttr(&se.Attr, "r", fmt.Sprintf("%g", nd.Radius))
case *Ellipse:
nm = "ellipse"
XMLAddAttr(&se.Attr, "cx", fmt.Sprintf("%g", nd.Pos.X))
XMLAddAttr(&se.Attr, "cy", fmt.Sprintf("%g", nd.Pos.Y))
XMLAddAttr(&se.Attr, "rx", fmt.Sprintf("%g", nd.Radii.X))
XMLAddAttr(&se.Attr, "ry", fmt.Sprintf("%g", nd.Radii.Y))
case *Line:
nm = "line"
XMLAddAttr(&se.Attr, "x1", fmt.Sprintf("%g", nd.Start.X))
XMLAddAttr(&se.Attr, "y1", fmt.Sprintf("%g", nd.Start.Y))
XMLAddAttr(&se.Attr, "x2", fmt.Sprintf("%g", nd.End.X))
XMLAddAttr(&se.Attr, "y2", fmt.Sprintf("%g", nd.End.Y))
case *Polygon:
nm = "polygon"
for _, p := range nd.Points {
sb.WriteString(fmt.Sprintf("%g,%g ", p.X, p.Y))
}
XMLAddAttr(&se.Attr, "points", sb.String())
case *Polyline:
nm = "polyline"
for _, p := range nd.Points {
sb.WriteString(fmt.Sprintf("%g,%g ", p.X, p.Y))
}
XMLAddAttr(&se.Attr, "points", sb.String())
case *Text:
if nd.Text == "" {
nm = "text"
} else {
nm = "tspan"
}
XMLAddAttr(&se.Attr, "x", fmt.Sprintf("%g", nd.Pos.X))
XMLAddAttr(&se.Attr, "y", fmt.Sprintf("%g", nd.Pos.Y))
text = nd.Text
case *Image:
if nd.Pixels == nil {
return ""
}
nm = "image"
XMLAddAttr(&se.Attr, "x", fmt.Sprintf("%g", nd.Pos.X))
XMLAddAttr(&se.Attr, "y", fmt.Sprintf("%g", nd.Pos.Y))
XMLAddAttr(&se.Attr, "width", fmt.Sprintf("%g", nd.Size.X))
XMLAddAttr(&se.Attr, "height", fmt.Sprintf("%g", nd.Size.Y))
XMLAddAttr(&se.Attr, "preserveAspectRatio", nd.ViewBox.PreserveAspectRatio.String())
ib, fmt := imagex.ToBase64PNG(nd.Pixels)
XMLAddAttr(&se.Attr, "href", "data:"+fmt+";base64,"+string(imagex.Base64SplitLines(ib)))
case *MetaData:
if strings.HasPrefix(nd.Name, "namedview") {
nm = "sodipodi:namedview"
} else if strings.HasPrefix(nd.Name, "grid") {
nm = "inkscape:grid"
}
case *Gradient:
MarshalXMLGradient(nd, nd.Name, enc)
return "" // exclude -- already written
case *Marker:
nm = "marker"
XMLAddAttr(&se.Attr, "refX", fmt.Sprintf("%g", nd.RefPos.X))
XMLAddAttr(&se.Attr, "refY", fmt.Sprintf("%g", nd.RefPos.Y))
XMLAddAttr(&se.Attr, "orient", nd.Orient)
case *Filter:
return "" // not yet supported
case *StyleSheet:
nm = "style"
default:
nm = n.AsTree().NodeType().Name
}
se.Name.Local = nm
if setName != "" {
se.Name.Local = setName
}
enc.EncodeToken(se)
if text != "" {
cd := xml.CharData([]byte(text))
enc.EncodeToken(cd)
}
return se.Name.Local
}
// MarshalXMLGradient adds the XML for the given gradient to the given encoder.
// This is not in [cogentcore.org/core/colors/gradient] because it uses a lot of SVG
// and XML infrastructure defined here.
func MarshalXMLGradient(n *Gradient, name string, enc *XMLEncoder) {
gr := n.Grad
if gr == nil {
return
}
gb := gr.AsBase()
me := xml.StartElement{}
XMLAddAttr(&me.Attr, "id", name)
linear := true
if _, ok := gr.(*gradient.Radial); ok {
linear = false
me.Name.Local = "radialGradient"
} else {
me.Name.Local = "linearGradient"
}
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))
}
} else {
r := gr.(*gradient.Radial)
// must be non-zero to add
if r.Center != (math32.Vector2{}) {
XMLAddAttr(&me.Attr, "cx", fmt.Sprintf("%g", r.Center.X))
XMLAddAttr(&me.Attr, "cy", fmt.Sprintf("%g", r.Center.Y))
}
if r.Focal != (math32.Vector2{}) {
XMLAddAttr(&me.Attr, "fx", fmt.Sprintf("%g", r.Focal.X))
XMLAddAttr(&me.Attr, "fy", fmt.Sprintf("%g", r.Focal.Y))
}
if r.Radius != (math32.Vector2{}) {
XMLAddAttr(&me.Attr, "r", fmt.Sprintf("%g", max(r.Radius.X, r.Radius.Y)))
}
}
XMLAddAttr(&me.Attr, "gradientUnits", gb.Units.String())
// pad is default
if gb.Spread != gradient.Pad {
XMLAddAttr(&me.Attr, "spreadMethod", gb.Spread.String())
}
if gb.Transform != math32.Identity2() {
XMLAddAttr(&me.Attr, "gradientTransform", fmt.Sprintf("matrix(%g,%g,%g,%g,%g,%g)", gb.Transform.XX, gb.Transform.YX, gb.Transform.XY, gb.Transform.YY, gb.Transform.X0, gb.Transform.Y0))
}
if n.StopsName != "" {
XMLAddAttr(&me.Attr, "href", "#"+n.StopsName)
}
enc.EncodeToken(me)
if n.StopsName == "" {
for _, gs := range gb.Stops {
se := xml.StartElement{}
se.Name.Local = "stop"
clr := gs.Color
hs := colors.AsHex(clr)[:7] // get rid of transparency
XMLAddAttr(&se.Attr, "style", fmt.Sprintf("stop-color:%s;stop-opacity:%g;", hs, float32(colors.AsRGBA(clr).A)/255))
XMLAddAttr(&se.Attr, "offset", fmt.Sprintf("%g", gs.Pos))
enc.EncodeToken(se)
enc.WriteEnd(se.Name.Local)
}
}
enc.WriteEnd(me.Name.Local)
}
// MarshalXMLTree encodes the given node and any children to XML.
// It returns any error, and name of element that enc.WriteEnd() should be
// called with; allows for extra elements to be added at end of list.
func MarshalXMLTree(n Node, enc *XMLEncoder, setName string) (string, error) {
name := MarshalXML(n, enc, setName)
if name == "" {
return "", nil
}
for _, k := range n.AsTree().Children {
knm, err := MarshalXMLTree(k.(Node), enc, "")
if knm != "" {
enc.WriteEnd(knm)
}
if err != nil {
return name, err
}
}
return name, nil
}
// MarshalXMLx marshals the svg using XMLEncoder
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))
}
if sv.PhysicalHeight.Value > 0 {
XMLAddAttr(&me.Attr, "height", fmt.Sprintf("%g", sv.PhysicalHeight.Value))
}
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")
XMLAddAttr(&me.Attr, "xmlns", "http://www.w3.org/2000/svg")
enc.EncodeToken(me)
dnm, err := MarshalXMLTree(sv.Defs, enc, "defs")
enc.WriteEnd(dnm)
for _, k := range sv.Root.Children {
var knm string
knm, err = MarshalXMLTree(k.(Node), enc, "")
if knm != "" {
enc.WriteEnd(knm)
}
if err != nil {
break
}
}
ed := xml.EndElement{}
ed.Name = me.Name
enc.EncodeToken(ed)
return err
}
// SetStandardXMLAttr sets standard attributes of node given XML-style name /
// attribute values (e.g., from parsing XML / SVG files); returns true if handled.
func SetStandardXMLAttr(ni Node, name, val string) bool {
nb := ni.AsNodeBase()
switch name {
case "id":
nb.SetName(val)
return true
case "class":
nb.Class = val
return true
case "style":
styles.SetStylePropertiesXML(val, (*map[string]any)(&nb.Properties))
return true
}
return false
}
// 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 (
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/math32"
)
// Line is a SVG line
type Line struct {
NodeBase
// position of the start of the line
Start math32.Vector2 `xml:"{x1,y1}"`
// position of the end of the line
End math32.Vector2 `xml:"{x2,y2}"`
}
func (g *Line) SVGName() string { return "line" }
func (g *Line) Init() {
g.NodeBase.Init()
g.End.Set(1, 1)
}
func (g *Line) SetPos(pos math32.Vector2) {
g.Start = pos
}
func (g *Line) SetSize(sz math32.Vector2) {
g.End = g.Start.Add(sz)
}
func (g *Line) LocalBBox() math32.Box2 {
bb := math32.B2Empty()
bb.ExpandByPoint(g.Start)
bb.ExpandByPoint(g.End)
hlw := 0.5 * g.LocalLineWidth()
bb.Min.SetSubScalar(hlw)
bb.Max.SetAddScalar(hlw)
return bb
}
func (g *Line) Render(sv *SVG) {
vis, pc := g.PushTransform(sv)
if !vis {
return
}
pc.DrawLine(g.Start.X, g.Start.Y, g.End.X, g.End.Y)
pc.Stroke()
g.BBoxes(sv)
if mrk := sv.MarkerByName(g, "marker-start"); mrk != nil {
ang := math32.Atan2(g.End.Y-g.Start.Y, g.End.X-g.Start.X)
mrk.RenderMarker(sv, g.Start, ang, g.Paint.StrokeStyle.Width.Dots)
}
if mrk := sv.MarkerByName(g, "marker-end"); mrk != nil {
ang := math32.Atan2(g.End.Y-g.Start.Y, g.End.X-g.Start.X)
mrk.RenderMarker(sv, g.End, ang, g.Paint.StrokeStyle.Width.Dots)
}
g.RenderChildren(sv)
pc.PopTransform()
}
// ApplyTransform applies the given 2D transform to the geometry of this node
// 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() {
g.Paint.Transform.SetMul(xf)
g.SetProperty("transform", g.Paint.Transform.String())
} 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)
}
// 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 (
"log"
"cogentcore.org/core/math32"
)
// Marker represents marker elements that can be drawn along paths (arrow heads, etc)
type Marker struct {
NodeBase
// reference position to align the vertex position with, specified in ViewBox coordinates
RefPos math32.Vector2 `xml:"{refX,refY}"`
// size of marker to render, in Units units
Size math32.Vector2 `xml:"{markerWidth,markerHeight}"`
// units to use
Units MarkerUnits `xml:"markerUnits"`
// viewbox defines the internal coordinate system for the drawing elements within the marker
ViewBox ViewBox
// orientation of the marker -- either 'auto' or an angle
Orient string `xml:"orient"`
// current vertex position
VertexPos math32.Vector2
// current vertex angle in radians
VertexAngle float32
// current stroke width
StrokeWidth float32
// net transform computed from settings and current values -- applied prior to rendering
Transform math32.Matrix2
// effective size for actual rendering
EffSize math32.Vector2
}
func (g *Marker) SVGName() string { return "marker" }
func (g *Marker) EnforceSVGName() bool { return false }
// MarkerUnits specifies units to use for svg marker elements
type MarkerUnits int32 //enum: enum
const (
StrokeWidth MarkerUnits = iota
UserSpaceOnUse
MarkerUnitsN
)
// RenderMarker renders the marker using given vertex position, angle (in
// radians), and stroke width
func (mrk *Marker) RenderMarker(sv *SVG, vertexPos math32.Vector2, vertexAng, strokeWidth float32) {
mrk.VertexPos = vertexPos
mrk.VertexAngle = vertexAng
mrk.StrokeWidth = strokeWidth
if mrk.Units == StrokeWidth {
mrk.EffSize = mrk.Size.MulScalar(strokeWidth)
} else {
mrk.EffSize = mrk.Size
}
ang := vertexAng
if mrk.Orient != "auto" {
ang, _ = math32.ParseAngle32(mrk.Orient)
}
if mrk.ViewBox.Size == (math32.Vector2{}) {
mrk.ViewBox.Size = math32.Vec2(3, 3)
}
mrk.Transform = math32.Rotate2D(ang).Scale(mrk.EffSize.X/mrk.ViewBox.Size.X, mrk.EffSize.Y/mrk.ViewBox.Size.Y).Translate(-mrk.RefPos.X, -mrk.RefPos.Y)
mrk.Transform.X0 += vertexPos.X
mrk.Transform.Y0 += vertexPos.Y
mrk.Paint.Transform = mrk.Transform
// fmt.Println("render marker:", mrk.Name, strokeWidth)
mrk.Render(sv)
}
func (g *Marker) Render(sv *SVG) {
pc := &g.Paint
rs := &sv.RenderState
rs.PushTransform(pc.Transform)
g.RenderChildren(sv)
g.BBoxes(sv) // must come after render
rs.PopTransform()
}
func (g *Marker) BBoxes(sv *SVG) {
if g.This == nil {
return
}
ni := g.This.(Node)
g.BBox = ni.NodeBBox(sv)
g.BBox.Canon()
g.VisBBox = sv.Geom.SizeRect().Intersect(g.BBox)
}
//////////////////////////////////////////////////////////
// SVG marker management
// MarkerByName finds marker property of given name, or generic "marker"
// type, and if set, attempts to find that marker and return it
func (sv *SVG) MarkerByName(n Node, marker string) *Marker {
url := NodePropURL(n, marker)
if url == "" {
url = NodePropURL(n, "marker")
}
if url == "" {
return nil
}
mrkn := sv.NodeFindURL(n, url)
if mrkn == nil {
return nil
}
mrk, ok := mrkn.(*Marker)
if !ok {
log.Printf("SVG Found element named: %v but isn't a Marker type, instead is: %T", url, mrkn)
return nil
}
return mrk
}
// 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 (
"fmt"
"image"
"maps"
"reflect"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles"
"cogentcore.org/core/tree"
)
// Node is the interface for all SVG nodes.
type Node interface {
tree.Node
// AsNodeBase returns the [NodeBase] for our node, which gives
// access to all the base-level data structures and methods
// without requiring interface methods.
AsNodeBase() *NodeBase
// Render draws the node to the svg image.
Render(sv *SVG)
// BBoxes computes BBox and VisBBox during Render.
BBoxes(sv *SVG)
// LocalBBox returns the bounding box of node in local dimensions.
LocalBBox() math32.Box2
// NodeBBox returns the bounding box in image coordinates for this node.
NodeBBox(sv *SVG) image.Rectangle
// SetNodePos sets the upper left effective position of this element, in local dimensions.
SetNodePos(pos math32.Vector2)
// SetNodeSize sets the overall effective size of this element, in local dimensions.
SetNodeSize(sz math32.Vector2)
// ApplyTransform applies the given 2D transform to the geometry of this node
// 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
// EnforceSVGName returns true if in general this element should
// be named with its SVGName plus a unique id.
// Groups and Markers are false.
EnforceSVGName() bool
}
// NodeBase is the base type for all elements within an SVG tree.
// It implements the [Node] interface and contains the core functionality.
type NodeBase struct {
tree.NodeBase
// Class contains user-defined class name(s) used primarily for attaching
// CSS styles to different display elements.
// Multiple class names can be used to combine properties;
// use spaces to separate per css standard.
Class string
// CSS is the cascading style sheet at this level.
// These styles apply here and to everything below, until superceded.
// Use .class and #name Properties elements to apply entire styles
// to given elements, and type for element type.
CSS map[string]any `xml:"css" set:"-"`
// CSSAgg is the aggregated css properties from all higher nodes down to this node.
CSSAgg map[string]any `copier:"-" json:"-" xml:"-" set:"-" display:"no-inline"`
// 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:"-"`
// VisBBox is the visible bounding box for the node intersected with the SVG image geometry.
VisBBox image.Rectangle `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
}
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) LocalBBox() math32.Box2 {
bb := math32.Box2{}
return bb
}
func (n *NodeBase) BaseInterface() reflect.Type {
return reflect.TypeOf((*NodeBase)(nil)).Elem()
}
func (g *NodeBase) PaintStyle() *styles.Paint {
return &g.Paint
}
func (g *NodeBase) Init() {
g.Paint.Defaults()
}
// 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))
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
g.SetProperty(prop, colors.AsHex(clr))
}
// ParTransform returns the full compounded 2D transform matrix for all
// of the parents of this node. If self is true, then include our
// own transform too.
func (g *NodeBase) ParTransform(self bool) math32.Matrix2 {
pars := []Node{}
xf := math32.Identity2()
n := g.This.(Node)
for {
if n.AsTree().Parent == nil {
break
}
n = n.AsTree().Parent.(Node)
pars = append(pars, n)
}
np := len(pars)
if np > 0 {
xf = pars[np-1].AsNodeBase().PaintStyle().Transform
}
for i := np - 2; i >= 0; i-- {
n := pars[i]
xf.SetMul(n.AsNodeBase().PaintStyle().Transform)
}
if self {
xf.SetMul(g.Paint.Transform)
}
return xf
}
// ApplyTransform applies the given 2D transform to the geometry of this node
// this just does a direct transform multiplication on coordinates.
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.ParTransform(self)
mxi = mxi.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)
}
// SVGWalkDown does [tree.NodeBase.WalkDown] on given node using given walk function
// with SVG Node parameters.
func SVGWalkDown(n Node, fun func(sn Node, snb *NodeBase) bool) {
n.AsTree().WalkDown(func(n tree.Node) bool {
sn := n.(Node)
return fun(sn, sn.AsNodeBase())
})
}
// SVGWalkDownNoDefs does [tree.Node.WalkDown] on given node using given walk function
// 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 {
sn := cn.(Node)
snb := sn.AsNodeBase()
_, md := sn.(*MetaData)
if snb.isDef || md {
return tree.Break
}
return fun(sn, snb)
})
}
// FirstNonGroupNode returns the first item that is not a group
// recursing into groups until a non-group item is found.
func FirstNonGroupNode(n Node) Node {
var ngn Node
SVGWalkDownNoDefs(n, func(sn Node, snb *NodeBase) bool {
if _, isgp := sn.(*Group); isgp {
return tree.Continue
}
ngn = sn
return tree.Break
})
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
func (g *NodeBase) Style(sv *SVG) {
pc := &g.Paint
pc.Defaults()
ctxt := colors.Context(sv)
pc.StyleSet = false // this is always first call, restart
var parCSSAgg map[string]any
if g.Parent != nil { // && g.Par != sv.Root.This
pn := g.Parent.(Node)
parCSSAgg = pn.AsNodeBase().CSSAgg
pp := pn.AsNodeBase().PaintStyle()
pc.CopyStyleFrom(pp)
pc.SetStyleProperties(pp, g.Properties, ctxt)
} else {
pc.SetStyleProperties(nil, g.Properties, ctxt)
}
pc.ToDotsImpl(&pc.UnitContext) // we always inherit parent's unit context -- SVG sets it once-and-for-all
if parCSSAgg != nil {
AggCSS(&g.CSSAgg, parCSSAgg)
} else {
g.CSSAgg = nil
}
AggCSS(&g.CSSAgg, g.CSS)
g.StyleCSS(sv, g.CSSAgg)
pc.StrokeStyle.Opacity *= pc.FontStyle.Opacity // applies to all
pc.FillStyle.Opacity *= pc.FontStyle.Opacity
pc.Off = !pc.Display || (pc.StrokeStyle.Color == nil && pc.FillStyle.Color == nil)
}
// AggCSS aggregates css properties
func AggCSS(agg *map[string]any, css map[string]any) {
if *agg == nil {
*agg = make(map[string]any)
}
maps.Copy(*agg, css)
}
// ApplyCSS applies css styles to given node,
// using key to select sub-properties from overall properties list
func (g *NodeBase) ApplyCSS(sv *SVG, key string, css map[string]any) bool {
pp, got := css[key]
if !got {
return false
}
pmap, ok := pp.(map[string]any) // must be a properties map
if !ok {
return false
}
pc := &g.Paint
ctxt := colors.Context(sv)
if g.Parent != sv.Root.This {
pp := g.Parent.(Node).AsNodeBase().PaintStyle()
pc.SetStyleProperties(pp, pmap, ctxt)
} else {
pc.SetStyleProperties(nil, pmap, ctxt)
}
return true
}
// StyleCSS applies css style properties to given SVG node
// parsing out type, .class, and #name selectors
func (g *NodeBase) StyleCSS(sv *SVG, css map[string]any) {
tyn := strings.ToLower(g.NodeType().Name) // type is most general, first
g.ApplyCSS(sv, tyn, css)
cln := "." + strings.ToLower(g.Class) // then class
g.ApplyCSS(sv, cln, css)
idnm := "#" + strings.ToLower(g.Name) // then name
g.ApplyCSS(sv, idnm, css)
}
// LocalBBoxToWin converts a local bounding box to SVG coordinates
func (g *NodeBase) LocalBBoxToWin(bb math32.Box2) image.Rectangle {
mxi := g.ParTransform(true) // include self
return bb.MulMatrix2(mxi).ToRect()
}
func (g *NodeBase) NodeBBox(sv *SVG) image.Rectangle {
rs := &sv.RenderState
return rs.LastRenderBBox
}
func (g *NodeBase) SetNodePos(pos math32.Vector2) {
// no-op by default
}
func (g *NodeBase) SetNodeSize(sz math32.Vector2) {
// no-op by default
}
// LocalLineWidth returns the line width in local coordinates
func (g *NodeBase) LocalLineWidth() float32 {
pc := &g.Paint
if pc.StrokeStyle.Color == nil {
return 0
}
return pc.StrokeStyle.Width.Dots
}
// ComputeBBox is called by default in render to compute bounding boxes for
// gui interaction -- can only be done in rendering because that is when all
// the proper transforms are all in place -- VpBBox is intersected with parent SVG
func (g *NodeBase) BBoxes(sv *SVG) {
if g.This == nil {
return
}
ni := g.This.(Node)
g.BBox = ni.NodeBBox(sv)
g.BBox.Canon()
g.VisBBox = sv.Geom.SizeRect().Intersect(g.BBox)
}
// PushTransform checks our bounding box and visibility, returning false if
// out of bounds. If visible, pushes our transform.
// Must be called as first step in Render.
func (g *NodeBase) PushTransform(sv *SVG) (bool, *paint.Context) {
g.BBox = image.Rectangle{}
if g.Paint.Off || g == nil || g.This == nil {
return false, nil
}
ni := g.This.(Node)
// if g.IsInvisible() { // just the Invisible flag
// return false, nil
// }
lbb := ni.LocalBBox()
g.BBox = g.LocalBBoxToWin(lbb)
g.VisBBox = sv.Geom.SizeRect().Intersect(g.BBox)
nvis := g.VisBBox == image.Rectangle{}
// g.SetInvisibleState(nvis) // don't set
if nvis && !g.isDef {
return false, nil
}
rs := &sv.RenderState
rs.PushTransform(g.Paint.Transform)
pc := &paint.Context{rs, &g.Paint}
return true, pc
}
func (g *NodeBase) RenderChildren(sv *SVG) {
for _, kid := range g.Children {
ni := kid.(Node)
ni.Render(sv)
}
}
func (g *NodeBase) Render(sv *SVG) {
vis, rs := g.PushTransform(sv)
if !vis {
return
}
// pc := &g.Paint
// render path elements, then compute bbox, then fill / stroke
g.BBoxes(sv)
g.RenderChildren(sv)
rs.PopTransform()
}
// 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 (
"fmt"
"log"
"math"
"strconv"
"strings"
"unicode"
"unsafe"
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/tree"
)
// Path renders SVG data sequences that can render just about anything
type Path struct {
NodeBase
// the path data to render -- path commands and numbers are serialized, with each command specifying the number of floating-point coord data points that follow
Data []PathData `xml:"-" set:"-"`
// string version of the path data
DataStr string `xml:"d"`
}
func (g *Path) SVGName() string { return "path" }
func (g *Path) SetPos(pos math32.Vector2) {
// todo: set first point
}
func (g *Path) SetSize(sz math32.Vector2) {
// todo: scale bbox
}
// 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 = PathDataParse(data)
if err != nil {
return err
}
err = PathDataValidate(&g.Data, g.Path())
return err
}
func (g *Path) LocalBBox() math32.Box2 {
bb := PathDataBBox(g.Data)
hlw := 0.5 * g.LocalLineWidth()
bb.Min.SetSubScalar(hlw)
bb.Max.SetAddScalar(hlw)
return bb
}
func (g *Path) Render(sv *SVG) {
sz := len(g.Data)
if sz < 2 {
return
}
vis, pc := g.PushTransform(sv)
if !vis {
return
}
PathDataRender(g.Data, pc)
pc.FillStrokeClear()
g.BBoxes(sv)
if mrk := sv.MarkerByName(g, "marker-start"); mrk != nil {
// todo: could look for close-path at end and find angle from there..
stv, ang := PathDataStart(g.Data)
mrk.RenderMarker(sv, stv, ang, g.Paint.StrokeStyle.Width.Dots)
}
if mrk := sv.MarkerByName(g, "marker-end"); mrk != nil {
env, ang := PathDataEnd(g.Data)
mrk.RenderMarker(sv, env, ang, g.Paint.StrokeStyle.Width.Dots)
}
if mrk := sv.MarkerByName(g, "marker-mid"); mrk != nil {
var ptm2, ptm1, pt math32.Vector2
gotidx := 0
PathDataIterFunc(g.Data, func(idx int, cmd PathCmds, ptIndex int, cp math32.Vector2, ctrls []math32.Vector2) bool {
ptm2 = ptm1
ptm1 = pt
pt = cp
if gotidx < 2 {
gotidx++
return true
}
if idx >= sz-3 { // todo: this is approximate...
return false
}
ang := 0.5 * (math32.Atan2(pt.Y-ptm1.Y, pt.X-ptm1.X) + math32.Atan2(ptm1.Y-ptm2.Y, ptm1.X-ptm2.X))
mrk.RenderMarker(sv, ptm1, ang, g.Paint.StrokeStyle.Width.Dots)
gotidx++
return true
})
}
g.RenderChildren(sv)
pc.PopTransform()
}
// AddPath adds given path command to the PathData
func (g *Path) AddPath(cmd PathCmds, args ...float32) {
na := len(args)
cd := cmd.EncCmd(na)
g.Data = append(g.Data, cd)
if na > 0 {
ad := unsafe.Slice((*PathData)(unsafe.Pointer(&args[0])), na)
g.Data = append(g.Data, ad...)
}
}
// AddPathArc adds an arc command using the simpler Paint.DrawArc parameters
// with center at the current position, and the given radius
// and angles in degrees. Because the y axis points down, angles are clockwise,
// and the rendering draws segments progressing from angle1 to angle2.
func (g *Path) AddPathArc(r, angle1, angle2 float32) {
ra1 := math32.DegToRad(angle1)
ra2 := math32.DegToRad(angle2)
xs := r * math32.Cos(ra1)
ys := r * math32.Sin(ra1)
xe := r * math32.Cos(ra2)
ye := r * math32.Sin(ra2)
longArc := float32(0)
if math32.Abs(angle2-angle1) >= 180 {
longArc = 1
}
sweep := float32(1)
if angle2-angle1 < 0 {
sweep = 0
}
g.AddPath(Pcm, xs, ys)
g.AddPath(Pca, r, r, 0, longArc, sweep, xe-xs, ye-ys)
}
// UpdatePathString sets the path string from the Data
func (g *Path) UpdatePathString() {
g.DataStr = PathDataString(g.Data)
}
// PathCmds are the commands within the path SVG drawing data type
type PathCmds byte //enum: enum
const (
// move pen, abs coords
PcM PathCmds = iota
// move pen, rel coords
Pcm
// lineto, abs
PcL
// lineto, rel
Pcl
// horizontal lineto, abs
PcH
// relative lineto, rel
Pch
// vertical lineto, abs
PcV
// vertical lineto, rel
Pcv
// Bezier curveto, abs
PcC
// Bezier curveto, rel
Pcc
// smooth Bezier curveto, abs
PcS
// smooth Bezier curveto, rel
Pcs
// quadratic Bezier curveto, abs
PcQ
// quadratic Bezier curveto, rel
Pcq
// smooth quadratic Bezier curveto, abs
PcT
// smooth quadratic Bezier curveto, rel
Pct
// elliptical arc, abs
PcA
// elliptical arc, rel
Pca
// close path
PcZ
// close path
Pcz
// error -- invalid command
PcErr
)
// PathData encodes the svg path data, using 32-bit floats which are converted
// into uint32 for path commands, and contain the command as the first 5
// bits, and the remaining 27 bits are the number of data points following the
// path command to interpret as numbers.
type PathData float32
// Cmd decodes path data as a command and a number of subsequent values for that command
func (pd PathData) Cmd() (PathCmds, int) {
iv := uint32(pd)
cmd := PathCmds(iv & 0x1F) // only the lowest 5 bits (31 values) for command
n := int((iv & 0xFFFFFFE0) >> 5) // extract the n from remainder of bits
return cmd, n
}
// EncCmd encodes command and n into PathData
func (pc PathCmds) EncCmd(n int) PathData {
nb := int32(n << 5) // n up-shifted
pd := PathData(int32(pc) | nb)
return pd
}
// PathDataNext gets the next path data point, incrementing the index
func PathDataNext(data []PathData, i *int) float32 {
pd := data[*i]
(*i)++
return float32(pd)
}
// PathDataNextVector gets the next 2 path data points as a vector
func PathDataNextVector(data []PathData, i *int) math32.Vector2 {
v := math32.Vector2{}
v.X = float32(data[*i])
(*i)++
v.Y = float32(data[*i])
(*i)++
return v
}
// PathDataNextRel gets the next 2 path data points as a relative vector
// and returns that relative vector added to current point
func PathDataNextRel(data []PathData, i *int, cp math32.Vector2) math32.Vector2 {
v := math32.Vector2{}
v.X = float32(data[*i])
(*i)++
v.Y = float32(data[*i])
(*i)++
return v.Add(cp)
}
// PathDataNextCmd gets the next path data command, incrementing the index -- ++
// not an expression so its clunky
func PathDataNextCmd(data []PathData, i *int) (PathCmds, int) {
pd := data[*i]
(*i)++
return pd.Cmd()
}
func reflectPt(pt, rp math32.Vector2) math32.Vector2 {
return pt.MulScalar(2).Sub(rp)
}
// PathDataRender traverses the path data and renders it using paint.
// We assume all the data has been validated and that n's are sufficient, etc
func PathDataRender(data []PathData, pc *paint.Context) {
sz := len(data)
if sz == 0 {
return
}
lastCmd := PcErr
var st, cp, xp, ctrl math32.Vector2
for i := 0; i < sz; {
cmd, n := PathDataNextCmd(data, &i)
rel := false
switch cmd {
case PcM:
cp = PathDataNextVector(data, &i)
pc.MoveTo(cp.X, cp.Y)
st = cp
for np := 1; np < n/2; np++ {
cp = PathDataNextVector(data, &i)
pc.LineTo(cp.X, cp.Y)
}
case Pcm:
cp = PathDataNextRel(data, &i, cp)
pc.MoveTo(cp.X, cp.Y)
st = cp
for np := 1; np < n/2; np++ {
cp = PathDataNextRel(data, &i, cp)
pc.LineTo(cp.X, cp.Y)
}
case PcL:
for np := 0; np < n/2; np++ {
cp = PathDataNextVector(data, &i)
pc.LineTo(cp.X, cp.Y)
}
case Pcl:
for np := 0; np < n/2; np++ {
cp = PathDataNextRel(data, &i, cp)
pc.LineTo(cp.X, cp.Y)
}
case PcH:
for np := 0; np < n; np++ {
cp.X = PathDataNext(data, &i)
pc.LineTo(cp.X, cp.Y)
}
case Pch:
for np := 0; np < n; np++ {
cp.X += PathDataNext(data, &i)
pc.LineTo(cp.X, cp.Y)
}
case PcV:
for np := 0; np < n; np++ {
cp.Y = PathDataNext(data, &i)
pc.LineTo(cp.X, cp.Y)
}
case Pcv:
for np := 0; np < n; np++ {
cp.Y += PathDataNext(data, &i)
pc.LineTo(cp.X, cp.Y)
}
case PcC:
for np := 0; np < n/6; np++ {
xp = PathDataNextVector(data, &i)
ctrl = PathDataNextVector(data, &i)
cp = PathDataNextVector(data, &i)
pc.CubicTo(xp.X, xp.Y, ctrl.X, ctrl.Y, cp.X, cp.Y)
}
case Pcc:
for np := 0; np < n/6; np++ {
xp = PathDataNextRel(data, &i, cp)
ctrl = PathDataNextRel(data, &i, cp)
cp = PathDataNextRel(data, &i, cp)
pc.CubicTo(xp.X, xp.Y, ctrl.X, ctrl.Y, cp.X, cp.Y)
}
case Pcs:
rel = true
fallthrough
case PcS:
for np := 0; np < n/4; np++ {
switch lastCmd {
case Pcc, PcC, Pcs, PcS:
ctrl = reflectPt(cp, ctrl)
default:
ctrl = cp
}
if rel {
xp = PathDataNextRel(data, &i, cp)
cp = PathDataNextRel(data, &i, cp)
} else {
xp = PathDataNextVector(data, &i)
cp = PathDataNextVector(data, &i)
}
pc.CubicTo(ctrl.X, ctrl.Y, xp.X, xp.Y, cp.X, cp.Y)
lastCmd = cmd
ctrl = xp
}
case PcQ:
for np := 0; np < n/4; np++ {
ctrl = PathDataNextVector(data, &i)
cp = PathDataNextVector(data, &i)
pc.QuadraticTo(ctrl.X, ctrl.Y, cp.X, cp.Y)
}
case Pcq:
for np := 0; np < n/4; np++ {
ctrl = PathDataNextRel(data, &i, cp)
cp = PathDataNextRel(data, &i, cp)
pc.QuadraticTo(ctrl.X, ctrl.Y, cp.X, cp.Y)
}
case Pct:
rel = true
fallthrough
case PcT:
for np := 0; np < n/2; np++ {
switch lastCmd {
case Pcq, PcQ, PcT, Pct:
ctrl = reflectPt(cp, ctrl)
default:
ctrl = cp
}
if rel {
cp = PathDataNextRel(data, &i, cp)
} else {
cp = PathDataNextVector(data, &i)
}
pc.QuadraticTo(ctrl.X, ctrl.Y, cp.X, cp.Y)
lastCmd = cmd
}
case Pca:
rel = true
fallthrough
case PcA:
for np := 0; np < n/7; np++ {
rad := PathDataNextVector(data, &i)
ang := PathDataNext(data, &i)
largeArc := (PathDataNext(data, &i) != 0)
sweep := (PathDataNext(data, &i) != 0)
prv := cp
if rel {
cp = PathDataNextRel(data, &i, cp)
} else {
cp = PathDataNextVector(data, &i)
}
ncx, ncy := paint.FindEllipseCenter(&rad.X, &rad.Y, ang*math.Pi/180, prv.X, prv.Y, cp.X, cp.Y, sweep, largeArc)
cp.X, cp.Y = pc.DrawEllipticalArcPath(ncx, ncy, cp.X, cp.Y, prv.X, prv.Y, rad.X, rad.Y, ang, largeArc, sweep)
}
case PcZ:
fallthrough
case Pcz:
pc.ClosePath()
cp = st
}
lastCmd = cmd
}
}
// PathDataIterFunc traverses the path data and calls given function on each
// coordinate point, passing overall starting index of coords in data stream,
// command, index of the points within that command, and coord values
// (absolute, not relative, regardless of the command type), including
// special control points for path commands that have them (else nil).
// If function returns false (use [tree.Break] vs. [tree.Continue]) then
// traversal is aborted.
// For Control points, order is in same order as in standard path stream
// when multiple, e.g., C,S.
// For A: order is: nc, prv, rad, math32.Vector2{X: ang}, math32.Vec2(laf, sf)}
func PathDataIterFunc(data []PathData, fun func(idx int, cmd PathCmds, ptIndex int, cp math32.Vector2, ctrls []math32.Vector2) bool) {
sz := len(data)
if sz == 0 {
return
}
lastCmd := PcErr
var st, cp, xp, ctrl, nc math32.Vector2
for i := 0; i < sz; {
cmd, n := PathDataNextCmd(data, &i)
rel := false
switch cmd {
case PcM:
cp = PathDataNextVector(data, &i)
if !fun(i-2, cmd, 0, cp, nil) {
return
}
st = cp
for np := 1; np < n/2; np++ {
cp = PathDataNextVector(data, &i)
if !fun(i-2, cmd, np, cp, nil) {
return
}
}
case Pcm:
cp = PathDataNextRel(data, &i, cp)
if !fun(i-2, cmd, 0, cp, nil) {
return
}
st = cp
for np := 1; np < n/2; np++ {
cp = PathDataNextRel(data, &i, cp)
if !fun(i-2, cmd, np, cp, nil) {
return
}
}
case PcL:
for np := 0; np < n/2; np++ {
cp = PathDataNextVector(data, &i)
if !fun(i-2, cmd, np, cp, nil) {
return
}
}
case Pcl:
for np := 0; np < n/2; np++ {
cp = PathDataNextRel(data, &i, cp)
if !fun(i-2, cmd, np, cp, nil) {
return
}
}
case PcH:
for np := 0; np < n; np++ {
cp.X = PathDataNext(data, &i)
if !fun(i-1, cmd, np, cp, nil) {
return
}
}
case Pch:
for np := 0; np < n; np++ {
cp.X += PathDataNext(data, &i)
if !fun(i-1, cmd, np, cp, nil) {
return
}
}
case PcV:
for np := 0; np < n; np++ {
cp.Y = PathDataNext(data, &i)
if !fun(i-1, cmd, np, cp, nil) {
return
}
}
case Pcv:
for np := 0; np < n; np++ {
cp.Y += PathDataNext(data, &i)
if !fun(i-1, cmd, np, cp, nil) {
return
}
}
case PcC:
for np := 0; np < n/6; np++ {
xp = PathDataNextVector(data, &i)
ctrl = PathDataNextVector(data, &i)
cp = PathDataNextVector(data, &i)
if !fun(i-2, cmd, np, cp, []math32.Vector2{xp, ctrl}) {
return
}
}
case Pcc:
for np := 0; np < n/6; np++ {
xp = PathDataNextRel(data, &i, cp)
ctrl = PathDataNextRel(data, &i, cp)
cp = PathDataNextRel(data, &i, cp)
if !fun(i-2, cmd, np, cp, []math32.Vector2{xp, ctrl}) {
return
}
}
case Pcs:
rel = true
fallthrough
case PcS:
for np := 0; np < n/4; np++ {
switch lastCmd {
case Pcc, PcC, Pcs, PcS:
ctrl = reflectPt(cp, ctrl)
default:
ctrl = cp
}
if rel {
xp = PathDataNextRel(data, &i, cp)
cp = PathDataNextRel(data, &i, cp)
} else {
xp = PathDataNextVector(data, &i)
cp = PathDataNextVector(data, &i)
}
if !fun(i-2, cmd, np, cp, []math32.Vector2{xp, ctrl}) {
return
}
lastCmd = cmd
ctrl = xp
}
case PcQ:
for np := 0; np < n/4; np++ {
ctrl = PathDataNextVector(data, &i)
cp = PathDataNextVector(data, &i)
if !fun(i-2, cmd, np, cp, []math32.Vector2{ctrl}) {
return
}
}
case Pcq:
for np := 0; np < n/4; np++ {
ctrl = PathDataNextRel(data, &i, cp)
cp = PathDataNextRel(data, &i, cp)
if !fun(i-2, cmd, np, cp, []math32.Vector2{ctrl}) {
return
}
}
case Pct:
rel = true
fallthrough
case PcT:
for np := 0; np < n/2; np++ {
switch lastCmd {
case Pcq, PcQ, PcT, Pct:
ctrl = reflectPt(cp, ctrl)
default:
ctrl = cp
}
if rel {
cp = PathDataNextRel(data, &i, cp)
} else {
cp = PathDataNextVector(data, &i)
}
if !fun(i-2, cmd, np, cp, []math32.Vector2{ctrl}) {
return
}
lastCmd = cmd
}
case Pca:
rel = true
fallthrough
case PcA:
for np := 0; np < n/7; np++ {
rad := PathDataNextVector(data, &i)
ang := PathDataNext(data, &i)
laf := PathDataNext(data, &i)
largeArc := (laf != 0)
sf := PathDataNext(data, &i)
sweep := (sf != 0)
prv := cp
if rel {
cp = PathDataNextRel(data, &i, cp)
} else {
cp = PathDataNextVector(data, &i)
}
nc.X, nc.Y = paint.FindEllipseCenter(&rad.X, &rad.Y, ang*math.Pi/180, prv.X, prv.Y, cp.X, cp.Y, sweep, largeArc)
if !fun(i-2, cmd, np, cp, []math32.Vector2{nc, prv, rad, {X: ang}, {laf, sf}}) {
return
}
}
case PcZ:
fallthrough
case Pcz:
cp = st
}
lastCmd = cmd
}
}
// PathDataBBox traverses the path data and extracts the local bounding box
func PathDataBBox(data []PathData) math32.Box2 {
bb := math32.B2Empty()
PathDataIterFunc(data, func(idx int, cmd PathCmds, ptIndex int, cp math32.Vector2, ctrls []math32.Vector2) bool {
bb.ExpandByPoint(cp)
return tree.Continue
})
return bb
}
// PathDataStart gets the starting coords and angle from the path
func PathDataStart(data []PathData) (vec math32.Vector2, ang float32) {
gotSt := false
PathDataIterFunc(data, func(idx int, cmd PathCmds, ptIndex int, cp math32.Vector2, ctrls []math32.Vector2) bool {
if gotSt {
ang = math32.Atan2(cp.Y-vec.Y, cp.X-vec.X)
return tree.Break
}
vec = cp
gotSt = true
return tree.Continue
})
return
}
// PathDataEnd gets the ending coords and angle from the path
func PathDataEnd(data []PathData) (vec math32.Vector2, ang float32) {
gotSome := false
PathDataIterFunc(data, func(idx int, cmd PathCmds, ptIndex int, cp math32.Vector2, ctrls []math32.Vector2) bool {
if gotSome {
ang = math32.Atan2(cp.Y-vec.Y, cp.X-vec.X)
}
vec = cp
gotSome = true
return tree.Continue
})
return
}
// PathCmdNMap gives the number of points per each command
var PathCmdNMap = map[PathCmds]int{
PcM: 2,
Pcm: 2,
PcL: 2,
Pcl: 2,
PcH: 1,
Pch: 1,
PcV: 1,
Pcv: 1,
PcC: 6,
Pcc: 6,
PcS: 4,
Pcs: 4,
PcQ: 4,
Pcq: 4,
PcT: 2,
Pct: 2,
PcA: 7,
Pca: 7,
PcZ: 0,
Pcz: 0,
}
// PathCmdIsRel returns true if the path command is relative, false for absolute
func PathCmdIsRel(pc PathCmds) bool {
return pc%2 == 1 // odd ones are relative
}
// PathDataValidate validates the path data and emits error messages on log
func PathDataValidate(data *[]PathData, errstr string) error {
sz := len(*data)
if sz == 0 {
return nil
}
di := 0
fcmd, _ := PathDataNextCmd(*data, &di)
if !(fcmd == Pcm || fcmd == PcM) {
log.Printf("core.PathDataValidate on %v: doesn't start with M or m -- adding\n", errstr)
ns := make([]PathData, 3, sz+3)
ns[0] = PcM.EncCmd(2)
ns[1], ns[2] = (*data)[1], (*data)[2]
*data = append(ns, *data...)
}
sz = len(*data)
for i := 0; i < sz; {
cmd, n := PathDataNextCmd(*data, &i)
trgn, ok := PathCmdNMap[cmd]
if !ok {
err := fmt.Errorf("core.PathDataValidate on %v: Path Command not valid: %v", errstr, cmd)
log.Println(err)
return err
}
if (trgn == 0 && n > 0) || (trgn > 0 && n%trgn != 0) {
err := fmt.Errorf("core.PathDataValidate on %v: Path Command %v has invalid n: %v -- should be: %v", errstr, cmd, n, trgn)
log.Println(err)
return err
}
for np := 0; np < n; np++ {
PathDataNext(*data, &i)
}
}
return nil
}
// PathRuneToCmd maps rune to path command
var PathRuneToCmd = map[rune]PathCmds{
'M': PcM,
'm': Pcm,
'L': PcL,
'l': Pcl,
'H': PcH,
'h': Pch,
'V': PcV,
'v': Pcv,
'C': PcC,
'c': Pcc,
'S': PcS,
's': Pcs,
'Q': PcQ,
'q': Pcq,
'T': PcT,
't': Pct,
'A': PcA,
'a': Pca,
'Z': PcZ,
'z': Pcz,
}
// PathCmdToRune maps command to rune
var PathCmdToRune = map[PathCmds]rune{}
func init() {
for k, v := range PathRuneToCmd {
PathCmdToRune[v] = k
}
}
// PathDecodeCmd decodes rune into corresponding command
func PathDecodeCmd(r rune) PathCmds {
cmd, ok := PathRuneToCmd[r]
if ok {
return cmd
} else {
// log.Printf("core.PathDecodeCmd unrecognized path command: %v %v\n", string(r), r)
return PcErr
}
}
// PathDataParse parses a string representation of the path data into compiled path data
func PathDataParse(d string) ([]PathData, error) {
var pd []PathData
endi := len(d) - 1
numSt := -1
numGotDec := false // did last number already get a decimal point -- if so, then an additional decimal point now acts as a delimiter -- some crazy paths actually leverage that!
lr := ' '
lstCmd := -1
// first pass: just do the raw parse into commands and numbers
for i, r := range d {
num := unicode.IsNumber(r) || (r == '.' && !numGotDec) || (r == '-' && lr == 'e') || r == 'e'
notn := !num
if i == endi || notn {
if numSt != -1 || (i == endi && !notn) {
if numSt == -1 {
numSt = i
}
nstr := d[numSt:i]
if i == endi && !notn {
nstr = d[numSt : i+1]
}
p, err := strconv.ParseFloat(nstr, 32)
if err != nil {
log.Printf("core.PathDataParse could not parse string: %v into float\n", nstr)
return nil, err
}
pd = append(pd, PathData(p))
}
if r == '-' || r == '.' {
numSt = i
if r == '.' {
numGotDec = true
} else {
numGotDec = false
}
} else {
numSt = -1
numGotDec = false
if lstCmd != -1 { // update number of args for previous command
lcm, _ := pd[lstCmd].Cmd()
n := (len(pd) - lstCmd) - 1
pd[lstCmd] = lcm.EncCmd(n)
}
if !unicode.IsSpace(r) && r != ',' {
cmd := PathDecodeCmd(r)
if cmd == PcErr {
if i != endi {
err := fmt.Errorf("core.PathDataParse invalid command rune: %v", r)
log.Println(err)
return nil, err
}
} else {
pc := cmd.EncCmd(0) // encode with 0 length to start
lstCmd = len(pd)
pd = append(pd, pc) // push on
}
}
}
} else if numSt == -1 { // got start of a number
numSt = i
if r == '.' {
numGotDec = true
} else {
numGotDec = false
}
} else { // inside a number
if r == '.' {
numGotDec = true
}
}
lr = r
}
return pd, nil
// todo: add some error checking..
}
// PathDataString returns the string representation of the path data
func PathDataString(data []PathData) string {
sz := len(data)
if sz == 0 {
return ""
}
var sb strings.Builder
var rp, cp, xp, ctrl math32.Vector2
for i := 0; i < sz; {
cmd, n := PathDataNextCmd(data, &i)
sb.WriteString(fmt.Sprintf("%c ", PathCmdToRune[cmd]))
switch cmd {
case PcM, Pcm:
cp = PathDataNextVector(data, &i)
sb.WriteString(fmt.Sprintf("%g,%g ", cp.X, cp.Y))
for np := 1; np < n/2; np++ {
cp = PathDataNextVector(data, &i)
sb.WriteString(fmt.Sprintf("%g,%g ", cp.X, cp.Y))
}
case PcL, Pcl:
for np := 0; np < n/2; np++ {
rp = PathDataNextVector(data, &i)
sb.WriteString(fmt.Sprintf("%g,%g ", rp.X, rp.Y))
}
case PcH, Pch, PcV, Pcv:
for np := 0; np < n; np++ {
cp.Y = PathDataNext(data, &i)
sb.WriteString(fmt.Sprintf("%g ", cp.Y))
}
case PcC, Pcc:
for np := 0; np < n/6; np++ {
xp = PathDataNextVector(data, &i)
sb.WriteString(fmt.Sprintf("%g,%g ", xp.X, xp.Y))
ctrl = PathDataNextVector(data, &i)
sb.WriteString(fmt.Sprintf("%g,%g ", ctrl.X, ctrl.Y))
cp = PathDataNextVector(data, &i)
sb.WriteString(fmt.Sprintf("%g,%g ", cp.X, cp.Y))
}
case Pcs, PcS:
for np := 0; np < n/4; np++ {
xp = PathDataNextVector(data, &i)
sb.WriteString(fmt.Sprintf("%g,%g ", xp.X, xp.Y))
cp = PathDataNextVector(data, &i)
sb.WriteString(fmt.Sprintf("%g,%g ", cp.X, cp.Y))
}
case PcQ, Pcq:
for np := 0; np < n/4; np++ {
ctrl = PathDataNextVector(data, &i)
sb.WriteString(fmt.Sprintf("%g,%g ", ctrl.X, ctrl.Y))
cp = PathDataNextVector(data, &i)
sb.WriteString(fmt.Sprintf("%g,%g ", cp.X, cp.Y))
}
case PcT, Pct:
for np := 0; np < n/2; np++ {
cp = PathDataNextVector(data, &i)
sb.WriteString(fmt.Sprintf("%g,%g ", cp.X, cp.Y))
}
case PcA, Pca:
for np := 0; np < n/7; np++ {
rad := PathDataNextVector(data, &i)
sb.WriteString(fmt.Sprintf("%g,%g ", rad.X, rad.Y))
ang := PathDataNext(data, &i)
largeArc := PathDataNext(data, &i)
sweep := PathDataNext(data, &i)
sb.WriteString(fmt.Sprintf("%g %g %g ", ang, largeArc, sweep))
cp = PathDataNextVector(data, &i)
sb.WriteString(fmt.Sprintf("%g,%g ", cp.X, cp.Y))
}
case PcZ, Pcz:
}
}
return sb.String()
}
//////////////////////////////////////////////////////////////////////////////////
// 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())
}
// PathDataTransformAbs does the transform of next two data points as absolute coords
func PathDataTransformAbs(data []PathData, i *int, xf math32.Matrix2, lpt math32.Vector2) math32.Vector2 {
cp := PathDataNextVector(data, i)
tc := xf.MulVector2AsPointCenter(cp, lpt)
data[*i-2] = PathData(tc.X)
data[*i-1] = PathData(tc.Y)
return tc
}
// PathDataTransformRel does the transform of next two data points as relative coords
// compared to given cp coordinate. returns new *absolute* coordinate
func PathDataTransformRel(data []PathData, i *int, xf math32.Matrix2, cp math32.Vector2) math32.Vector2 {
rp := PathDataNextVector(data, i)
tc := xf.MulVector2AsVector(rp)
data[*i-2] = PathData(tc.X)
data[*i-1] = PathData(tc.Y)
return cp.Add(tc) // new abs
}
// 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) {
sz := len(g.Data)
data := g.Data
lastCmd := PcErr
var cp, st math32.Vector2
var xp, ctrl, rp math32.Vector2
for i := 0; i < sz; {
cmd, n := PathDataNextCmd(data, &i)
rel := false
switch cmd {
case PcM:
cp = PathDataTransformAbs(data, &i, xf, lpt)
st = cp
for np := 1; np < n/2; np++ {
cp = PathDataTransformAbs(data, &i, xf, lpt)
}
case Pcm:
if i == 1 { // starting
cp = PathDataTransformAbs(data, &i, xf, lpt)
} else {
cp = PathDataTransformRel(data, &i, xf, cp)
}
st = cp
for np := 1; np < n/2; np++ {
cp = PathDataTransformRel(data, &i, xf, cp)
}
case PcL:
for np := 0; np < n/2; np++ {
cp = PathDataTransformAbs(data, &i, xf, lpt)
}
case Pcl:
for np := 0; np < n/2; np++ {
cp = PathDataTransformRel(data, &i, xf, cp)
}
case PcH:
for np := 0; np < n; np++ {
cp.X = PathDataNext(data, &i)
tc := xf.MulVector2AsPointCenter(cp, lpt)
data[i-1] = PathData(tc.X)
}
case Pch:
for np := 0; np < n; np++ {
rp.X = PathDataNext(data, &i)
rp.Y = 0
rp = xf.MulVector2AsVector(rp)
data[i-1] = PathData(rp.X)
cp.SetAdd(rp) // new abs
}
case PcV:
for np := 0; np < n; np++ {
cp.Y = PathDataNext(data, &i)
tc := xf.MulVector2AsPointCenter(cp, lpt)
data[i-1] = PathData(tc.Y)
}
case Pcv:
for np := 0; np < n; np++ {
rp.Y = PathDataNext(data, &i)
rp.X = 0
rp = xf.MulVector2AsVector(rp)
data[i-1] = PathData(rp.Y)
cp.SetAdd(rp) // new abs
}
case PcC:
for np := 0; np < n/6; np++ {
xp = PathDataTransformAbs(data, &i, xf, lpt)
ctrl = PathDataTransformAbs(data, &i, xf, lpt)
cp = PathDataTransformAbs(data, &i, xf, lpt)
}
case Pcc:
for np := 0; np < n/6; np++ {
xp = PathDataTransformRel(data, &i, xf, cp)
ctrl = PathDataTransformRel(data, &i, xf, cp)
cp = PathDataTransformRel(data, &i, xf, cp)
}
case Pcs:
rel = true
fallthrough
case PcS:
for np := 0; np < n/4; np++ {
switch lastCmd {
case Pcc, PcC, Pcs, PcS:
ctrl = reflectPt(cp, ctrl)
default:
ctrl = cp
}
if rel {
xp = PathDataTransformRel(data, &i, xf, cp)
cp = PathDataTransformRel(data, &i, xf, cp)
} else {
xp = PathDataTransformAbs(data, &i, xf, lpt)
cp = PathDataTransformAbs(data, &i, xf, lpt)
}
lastCmd = cmd
ctrl = xp
}
case PcQ:
for np := 0; np < n/4; np++ {
ctrl = PathDataTransformAbs(data, &i, xf, lpt)
cp = PathDataTransformAbs(data, &i, xf, lpt)
}
case Pcq:
for np := 0; np < n/4; np++ {
ctrl = PathDataTransformRel(data, &i, xf, cp)
cp = PathDataTransformRel(data, &i, xf, cp)
}
case Pct:
rel = true
fallthrough
case PcT:
for np := 0; np < n/2; np++ {
switch lastCmd {
case Pcq, PcQ, PcT, Pct:
ctrl = reflectPt(cp, ctrl)
default:
ctrl = cp
}
if rel {
cp = PathDataTransformRel(data, &i, xf, cp)
} else {
cp = PathDataTransformAbs(data, &i, xf, lpt)
}
lastCmd = cmd
}
case Pca:
rel = true
fallthrough
case PcA:
for np := 0; np < n/7; np++ {
rad := PathDataTransformRel(data, &i, xf, math32.Vector2{})
ang := PathDataNext(data, &i)
largeArc := (PathDataNext(data, &i) != 0)
sweep := (PathDataNext(data, &i) != 0)
pc := cp
if rel {
cp = PathDataTransformRel(data, &i, xf, cp)
} else {
cp = PathDataTransformAbs(data, &i, xf, lpt)
}
ncx, ncy := paint.FindEllipseCenter(&rad.X, &rad.Y, ang*math.Pi/180, pc.X, pc.Y, cp.X, cp.Y, sweep, largeArc)
_ = ncx
_ = ncy
}
case PcZ:
fallthrough
case Pcz:
cp = st
}
lastCmd = cmd
}
}
// 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)
for i := range g.Data {
g.Data[i] = PathData(dat[i])
}
g.ReadTransform(dat, sz)
g.GradientReadPts(sv, dat)
}
// 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 (
"cogentcore.org/core/math32"
)
// Polygon is a SVG polygon
type Polygon struct {
Polyline
}
func (g *Polygon) SVGName() string { return "polygon" }
func (g *Polygon) Render(sv *SVG) {
sz := len(g.Points)
if sz < 2 {
return
}
vis, pc := g.PushTransform(sv)
if !vis {
return
}
pc.DrawPolygon(g.Points)
pc.FillStrokeClear()
g.BBoxes(sv)
if mrk := sv.MarkerByName(g, "marker-start"); mrk != nil {
pt := g.Points[0]
ptn := g.Points[1]
ang := math32.Atan2(ptn.Y-pt.Y, ptn.X-pt.X)
mrk.RenderMarker(sv, pt, ang, g.Paint.StrokeStyle.Width.Dots)
}
if mrk := sv.MarkerByName(g, "marker-end"); mrk != nil {
pt := g.Points[sz-1]
ptp := g.Points[sz-2]
ang := math32.Atan2(pt.Y-ptp.Y, pt.X-ptp.X)
mrk.RenderMarker(sv, pt, ang, g.Paint.StrokeStyle.Width.Dots)
}
if mrk := sv.MarkerByName(g, "marker-mid"); mrk != nil {
for i := 1; i < sz-1; i++ {
pt := g.Points[i]
ptp := g.Points[i-1]
ptn := g.Points[i+1]
ang := 0.5 * (math32.Atan2(pt.Y-ptp.Y, pt.X-ptp.X) + math32.Atan2(ptn.Y-pt.Y, ptn.X-pt.X))
mrk.RenderMarker(sv, pt, ang, g.Paint.StrokeStyle.Width.Dots)
}
}
g.RenderChildren(sv)
pc.PopTransform()
}
// 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 (
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/math32"
)
// Polyline is a SVG multi-line shape
type Polyline struct {
NodeBase
// the coordinates to draw -- does a moveto on the first, then lineto for all the rest
Points []math32.Vector2 `xml:"points"`
}
func (g *Polyline) SVGName() string { return "polyline" }
func (g *Polyline) SetPos(pos math32.Vector2) {
// todo: set offset relative to bbox
}
func (g *Polyline) SetSize(sz math32.Vector2) {
// todo: scale bbox
}
func (g *Polyline) LocalBBox() math32.Box2 {
bb := math32.B2Empty()
for _, pt := range g.Points {
bb.ExpandByPoint(pt)
}
hlw := 0.5 * g.LocalLineWidth()
bb.Min.SetSubScalar(hlw)
bb.Max.SetAddScalar(hlw)
return bb
}
func (g *Polyline) Render(sv *SVG) {
sz := len(g.Points)
if sz < 2 {
return
}
vis, pc := g.PushTransform(sv)
if !vis {
return
}
pc.DrawPolyline(g.Points)
pc.FillStrokeClear()
g.BBoxes(sv)
if mrk := sv.MarkerByName(g, "marker-start"); mrk != nil {
pt := g.Points[0]
ptn := g.Points[1]
ang := math32.Atan2(ptn.Y-pt.Y, ptn.X-pt.X)
mrk.RenderMarker(sv, pt, ang, g.Paint.StrokeStyle.Width.Dots)
}
if mrk := sv.MarkerByName(g, "marker-end"); mrk != nil {
pt := g.Points[sz-1]
ptp := g.Points[sz-2]
ang := math32.Atan2(pt.Y-ptp.Y, pt.X-ptp.X)
mrk.RenderMarker(sv, pt, ang, g.Paint.StrokeStyle.Width.Dots)
}
if mrk := sv.MarkerByName(g, "marker-mid"); mrk != nil {
for i := 1; i < sz-1; i++ {
pt := g.Points[i]
ptp := g.Points[i-1]
ptn := g.Points[i+1]
ang := 0.5 * (math32.Atan2(pt.Y-ptp.Y, pt.X-ptp.X) + math32.Atan2(ptn.Y-pt.Y, ptn.X-pt.X))
mrk.RenderMarker(sv, pt, ang, g.Paint.StrokeStyle.Width.Dots)
}
}
g.RenderChildren(sv)
pc.PopTransform()
}
// ApplyTransform applies the given 2D transform to the geometry of this node
// 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() {
g.Paint.Transform.SetMul(xf)
g.SetProperty("transform", g.Paint.Transform.String())
} else {
for i, p := range g.Points {
p = xf.MulVector2AsPoint(p)
g.Points[i] = p
}
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)
}
// 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 (
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
)
// Rect is a SVG rectangle, optionally with rounded corners
type Rect struct {
NodeBase
// position of the top-left of the rectangle
Pos math32.Vector2 `xml:"{x,y}"`
// size of the rectangle
Size math32.Vector2 `xml:"{width,height}"`
// radii for curved corners, as a proportion of width, height
Radius math32.Vector2 `xml:"{rx,ry}"`
}
func (g *Rect) SVGName() string { return "rect" }
func (g *Rect) Init() {
g.NodeBase.Init()
g.Size.Set(1, 1)
}
func (g *Rect) SetNodePos(pos math32.Vector2) {
g.Pos = pos
}
func (g *Rect) SetNodeSize(sz math32.Vector2) {
g.Size = sz
}
func (g *Rect) LocalBBox() math32.Box2 {
bb := math32.Box2{}
hlw := 0.5 * g.LocalLineWidth()
bb.Min = g.Pos.SubScalar(hlw)
bb.Max = g.Pos.Add(g.Size).AddScalar(hlw)
return bb
}
func (g *Rect) Render(sv *SVG) {
vis, pc := g.PushTransform(sv)
if !vis {
return
}
// TODO: figure out a better way to do this
bs := styles.Border{}
bs.Style.Set(styles.BorderSolid)
bs.Width.Set(pc.StrokeStyle.Width)
bs.Color.Set(pc.StrokeStyle.Color)
bs.Radius.Set(units.Dp(g.Radius.X))
if g.Radius.X == 0 && g.Radius.Y == 0 {
pc.DrawRectangle(g.Pos.X, g.Pos.Y, g.Size.X, g.Size.Y)
} else {
// todo: only supports 1 radius right now -- easy to add another
// SidesTODO: also support different radii for each corner
pc.DrawRoundedRectangle(g.Pos.X, g.Pos.Y, g.Size.X, g.Size.Y, styles.NewSideFloats(g.Radius.X))
}
pc.FillStrokeClear()
g.BBoxes(sv)
g.RenderChildren(sv)
pc.PopTransform()
}
// ApplyTransform applies the given 2D transform to the geometry of this node
// 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() {
g.Paint.Transform.SetMul(xf)
g.SetProperty("transform", g.Paint.Transform.String())
} 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)
}
// 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
//go:generate core generate
import (
"image"
"image/color"
"strings"
"sync"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
)
// SVG is an SVG object.
type SVG struct {
// Name is the name of the SVG -- e.g., the filename if loaded
Name string
// the title of the svg
Title string `xml:"title"`
// the description of the svg
Desc string `xml:"desc"`
// Background is the image/color to fill the background with,
// if any.
Background image.Image
// Color can be set to provide a default Fill and Stroke Color value
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 Pixels image
Geom math32.Geom2DInt
// physical width of the drawing, e.g., when printed.
// Does not affect rendering, metadata.
PhysicalWidth units.Value
// physical height of the drawing, e.g., when printed.
// Does not affect rendering, metadata.
PhysicalHeight units.Value
// InvertY, when applying the ViewBox transform, also flip the Y axis so that
// the smallest Y value is at the bottom of the SVG box,
// instead of being at the top as it is by default.
InvertY bool
// Translate specifies a translation to apply beyond what is specified in the SVG,
// and its ViewBox transform.
Translate math32.Vector2
// Scale specifies a zoom scale factor to apply beyond what is specified in the SVG,
// and its ViewBox transform.
Scale float32
// render state for rendering
RenderState paint.State `copier:"-" json:"-" xml:"-" edit:"-"`
// live pixels that we render into
Pixels *image.RGBA `copier:"-" json:"-" xml:"-" edit:"-"`
// all defs defined elements go here (gradients, symbols, etc)
Defs *Group
// Root is the root of the svg tree, which has the top-level viewbox and styles.
Root *Root
// map of def names to index. uses starting index to find element.
// always updated after each search.
DefIndexes map[string]int `display:"-" json:"-" xml:"-"`
// map of unique numeric ids for all elements.
// Used for allocating new unique id numbers, appended to end of elements.
// See NewUniqueID, GatherIDs
UniqueIDs map[int]struct{} `display:"-" json:"-" xml:"-"`
// flag is set when the SVG is rendering
IsRendering bool
// mutex for protecting rendering
RenderMu sync.Mutex `display:"-" json:"-" xml:"-"`
}
// NewSVG creates a SVG with Pixels Image of the specified width and height
func NewSVG(width, height int) *SVG {
sv := &SVG{}
sv.Config(width, height)
return sv
}
// Config configures the SVG, setting image to given size
// and initializing all relevant fields.
func (sv *SVG) Config(width, height int) {
sz := image.Point{width, height}
sv.Geom.Size = sz
sv.Scale = 1
sv.Pixels = image.NewRGBA(image.Rectangle{Max: sz})
sv.RenderState.Init(width, height, sv.Pixels)
sv.Root = NewRoot()
sv.Root.SetName("svg")
sv.Defs = NewGroup()
sv.Defs.SetName("defs")
}
// Resize resizes the viewport, creating a new image -- updates Geom Size
func (sv *SVG) Resize(nwsz image.Point) {
if nwsz.X == 0 || nwsz.Y == 0 {
return
}
if sv.Root == nil || sv.Root.This == nil {
sv.Config(nwsz.X, nwsz.Y)
return
}
if sv.Pixels != nil {
ib := sv.Pixels.Bounds().Size()
if ib == nwsz {
sv.Geom.Size = nwsz // make sure
return // already good
}
}
sv.Pixels = image.NewRGBA(image.Rectangle{Max: nwsz})
sv.RenderState.Init(nwsz.X, nwsz.Y, sv.Pixels)
sv.Geom.Size = nwsz // make sure
}
// DeleteAll deletes any existing elements in this svg
func (sv *SVG) DeleteAll() {
if sv.Root == nil || sv.Root.This == nil {
return
}
sv.Root.Paint.Defaults()
sv.Root.DeleteChildren()
sv.Defs.DeleteChildren()
}
// Base returns the current Color activated in the context.
// Color has support for special color names that are relative to
// this current color.
func (sv *SVG) Base() color.RGBA {
return colors.AsRGBA(colors.ToUniform(sv.Background))
}
// ImageByURL finds a Node by an element name (URL-like path), and
// attempts to convert it to an [image.Image].
// Used for color styling based on url() value.
func (sv *SVG) ImageByURL(url string) image.Image {
// TODO(kai): support taking snapshot of element as image in SVG.ImageByURL
if sv == nil {
return nil
}
val := url[4:]
val = strings.TrimPrefix(strings.TrimSuffix(val, ")"), "#")
def := sv.FindDefByName(val)
if def != nil {
if grad, ok := def.(*Gradient); ok {
return grad.Grad
}
}
ne := sv.FindNamedElement(val)
if grad, ok := ne.(*Gradient); ok {
return grad.Grad
}
return nil
}
func (sv *SVG) Style() {
// set isDef
sv.Defs.WalkDown(func(n tree.Node) bool {
sn := n.(Node)
sn.AsNodeBase().isDef = true
sn.AsNodeBase().Style(sv)
return tree.Continue
})
sv.Root.Paint.Defaults()
if sv.Color != nil {
// TODO(kai): consider handling non-uniform colors here
c := colors.ToUniform(sv.Color)
sv.Root.SetColorProperties("stroke", colors.AsHex(c))
sv.Root.SetColorProperties("fill", colors.AsHex(c))
}
sv.SetUnitContext(&sv.Root.Paint, math32.Vector2{}, math32.Vector2{})
sv.Root.WalkDown(func(k tree.Node) bool {
sn := k.(Node)
sn.AsNodeBase().Style(sv)
return tree.Continue
})
}
func (sv *SVG) Render() {
sv.RenderMu.Lock()
sv.IsRendering = true
sv.Style()
sv.SetRootTransform()
rs := &sv.RenderState
rs.PushBounds(sv.Pixels.Bounds())
if sv.Background != nil {
sv.FillViewport()
}
sv.Root.Render(sv)
rs.PopBounds()
sv.RenderMu.Unlock()
sv.IsRendering = false
}
func (sv *SVG) FillViewport() {
pc := &paint.Context{&sv.RenderState, &sv.Root.Paint}
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
}
}
// 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)
}
// SavePNG saves the Pixels to a PNG file
func (sv *SVG) SavePNG(fname string) error {
return imagex.Save(sv.Pixels, fname)
}
// Root represents the root of an SVG tree.
type Root struct {
Group
// ViewBox defines the coordinate system for the drawing.
// These units are mapped into the screen space allocated
// for the SVG during rendering.
ViewBox ViewBox
}
func (g *Root) SVGName() string { return "svg" }
func (g *Root) EnforceSVGName() bool { return false }
func (g *Root) NodeBBox(sv *SVG) image.Rectangle {
// todo: return viewbox
return sv.Geom.SizeRect()
}
// SetUnitContext sets the unit context based on size of viewport, element,
// and parent element (from bbox) and then caches everything out in terms of raw pixel
// dots for rendering -- call at start of render
func (sv *SVG) SetUnitContext(pc *styles.Paint, el, parent math32.Vector2) {
pc.UnitContext.Defaults()
pc.UnitContext.DPI = 96 // paint (SVG) context is always 96 = 1to1
if sv.RenderState.Image != nil {
sz := sv.RenderState.Image.Bounds().Size()
pc.UnitContext.SetSizes(float32(sz.X), float32(sz.Y), el.X, el.Y, parent.X, parent.Y)
} else {
pc.UnitContext.SetSizes(0, 0, el.X, el.Y, parent.X, parent.Y)
}
pc.FontStyle.SetUnitContext(&pc.UnitContext)
pc.ToDots()
}
// 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/base/slicesx"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles"
)
// Text renders SVG text, handling both text and tspan elements.
// tspan is nested under a parent text -- text has empty Text string.
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"`
// render version of text
TextRender paint.Text `xml:"-" json:"-" copier:"-"`
// character positions along X axis, if specified
CharPosX []float32
// character positions along Y axis, if specified
CharPosY []float32
// character delta-positions along X axis, if specified
CharPosDX []float32
// character delta-positions along Y axis, if specified
CharPosDY []float32
// character rotations, if specified
CharRots []float32
// author's computed text length, if specified -- we attempt to match
TextLength float32
// in attempting to match TextLength, should we adjust glyphs in addition to spacing?
AdjustGlyphs bool
// last text render position -- lower-left baseline of start
LastPos math32.Vector2 `xml:"-" json:"-" copier:"-"`
// last actual bounding box in display units (dots)
LastBBox math32.Box2 `xml:"-" json:"-" copier:"-"`
}
func (g *Text) SVGName() string {
if len(g.Text) == 0 {
return "text"
}
return "tspan"
}
// IsParText returns true if this element serves as a parent text element
// to tspan elements within it. This is true if NumChildren() > 0 and
// Text == ""
func (g *Text) IsParText() bool {
return g.NumChildren() > 0 && g.Text == ""
}
func (g *Text) SetNodePos(pos math32.Vector2) {
g.Pos = pos
for _, kii := range g.Children {
kt := kii.(*Text)
kt.Pos = g.Paint.Transform.MulVector2AsPoint(pos)
}
}
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
}
}
func (g *Text) NodeBBox(sv *SVG) image.Rectangle {
if g.IsParText() {
return BBoxFromChildren(g)
} else {
return image.Rectangle{Min: g.LastBBox.Min.ToPointFloor(), Max: g.LastBBox.Max.ToPointCeil()}
}
}
// TextBBox returns the bounding box in local coordinates
func (g *Text) TextBBox() math32.Box2 {
if g.Text == "" {
return math32.Box2{}
}
g.LayoutText()
pc := &g.Paint
bb := g.TextRender.BBox
bb.Translate(math32.Vec2(0, -0.8*pc.FontStyle.Font.Face.Metrics.Height)) // adjust for baseline
return bb
}
// LayoutText does the full text layout without any transformations,
// including computing the text bounding box.
// This is called in TextBBox because that is called first prior to render.
func (g *Text) LayoutText() {
if g.Text == "" {
return
}
pc := &g.Paint
pc.FontStyle.Font = paint.OpenFont(&pc.FontStyle, &pc.UnitContext) // use original size font
if pc.FillStyle.Color != nil {
pc.FontStyle.Color = pc.FillStyle.Color
}
g.TextRender.SetString(g.Text, &pc.FontStyle, &pc.UnitContext, &pc.TextStyle, true, 0, 1)
sr := &(g.TextRender.Spans[0])
// todo: align styling only affects multi-line text and is about how tspan is arranged within
// the overall text block.
if len(g.CharPosX) > 0 {
mx := min(len(g.CharPosX), len(sr.Render))
for i := 0; i < mx; i++ {
sr.Render[i].RelPos.X = g.CharPosX[i]
}
}
if len(g.CharPosY) > 0 {
mx := min(len(g.CharPosY), len(sr.Render))
for i := 0; i < mx; i++ {
sr.Render[i].RelPos.Y = g.CharPosY[i]
}
}
if len(g.CharPosDX) > 0 {
mx := min(len(g.CharPosDX), len(sr.Render))
for i := 0; i < mx; i++ {
if i > 0 {
sr.Render[i].RelPos.X = sr.Render[i-1].RelPos.X + g.CharPosDX[i]
} else {
sr.Render[i].RelPos.X = g.CharPosDX[i] // todo: not sure this is right
}
}
}
if len(g.CharPosDY) > 0 {
mx := min(len(g.CharPosDY), len(sr.Render))
for i := 0; i < mx; i++ {
if i > 0 {
sr.Render[i].RelPos.Y = sr.Render[i-1].RelPos.Y + g.CharPosDY[i]
} else {
sr.Render[i].RelPos.Y = g.CharPosDY[i] // todo: not sure this is right
}
}
}
// todo: TextLength, AdjustGlyphs -- also svg2 at least supports word wrapping!
g.TextRender.UpdateBBox()
}
func (g *Text) RenderText(sv *SVG) {
pc := &paint.Context{&sv.RenderState, &g.Paint}
mat := &pc.CurrentTransform
// note: layout of text has already been done in LocalBBox above
g.TextRender.Transform(*mat, &pc.FontStyle, &pc.UnitContext)
pos := mat.MulVector2AsPoint(math32.Vec2(g.Pos.X, g.Pos.Y))
if pc.TextStyle.Align == styles.Center || pc.TextStyle.Anchor == styles.AnchorMiddle {
pos.X -= g.TextRender.BBox.Size().X * .5
} else if pc.TextStyle.Align == styles.End || pc.TextStyle.Anchor == styles.AnchorEnd {
pos.X -= g.TextRender.BBox.Size().X
}
g.TextRender.Render(pc, pos)
g.LastPos = pos
bb := g.TextRender.BBox
bb.Translate(math32.Vec2(pos.X, pos.Y-0.8*pc.FontStyle.Font.Face.Metrics.Height)) // adjust for baseline
g.LastBBox = bb
g.BBoxes(sv)
}
func (g *Text) LocalBBox() math32.Box2 {
return g.TextBBox()
}
func (g *Text) Render(sv *SVG) {
if g.IsParText() {
pc := &g.Paint
rs := &sv.RenderState
rs.PushTransform(pc.Transform)
g.RenderChildren(sv)
g.BBoxes(sv) // must come after render
rs.PopTransform()
} else {
vis, rs := g.PushTransform(sv)
if !vis {
return
}
if len(g.Text) > 0 {
g.RenderText(sv)
}
g.RenderChildren(sv)
if g.IsParText() {
g.BBoxes(sv) // after kids have rendered
}
rs.PopTransform()
}
}
// ApplyTransform applies the given 2D transform to the geometry of this node
// 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() {
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())
} 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]
}
}
}
// Code generated by "core generate"; DO NOT EDIT.
package svg
import (
"image"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
"github.com/aymerick/douceur/css"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Circle", IDName: "circle", Doc: "Circle is a SVG circle", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Pos", Doc: "position of the center of the circle"}, {Name: "Radius", Doc: "radius of the circle"}}})
// NewCircle returns a new [Circle] with the given optional parent:
// Circle is a SVG circle
func NewCircle(parent ...tree.Node) *Circle { return tree.New[Circle](parent...) }
// SetPos sets the [Circle.Pos]:
// position of the center of the circle
func (t *Circle) SetPos(v math32.Vector2) *Circle { t.Pos = v; return t }
// SetRadius sets the [Circle.Radius]:
// radius of the circle
func (t *Circle) SetRadius(v float32) *Circle { t.Radius = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.ClipPath", IDName: "clip-path", Doc: "ClipPath is used for holding a path that renders as a clip path", Embeds: []types.Field{{Name: "NodeBase"}}})
// NewClipPath returns a new [ClipPath] with the given optional parent:
// 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"}}})
// NewStyleSheet returns a new [StyleSheet] with the given optional parent:
// StyleSheet is a Node2D 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
func NewStyleSheet(parent ...tree.Node) *StyleSheet { return tree.New[StyleSheet](parent...) }
// SetSheet sets the [StyleSheet.Sheet]
func (t *StyleSheet) SetSheet(v *css.Stylesheet) *StyleSheet { t.Sheet = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.MetaData", IDName: "meta-data", Doc: "MetaData is used for holding meta data info", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "MetaData"}}})
// NewMetaData returns a new [MetaData] with the given optional parent:
// MetaData is used for holding meta data info
func NewMetaData(parent ...tree.Node) *MetaData { return tree.New[MetaData](parent...) }
// SetMetaData sets the [MetaData.MetaData]
func (t *MetaData) SetMetaData(v string) *MetaData { t.MetaData = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Ellipse", IDName: "ellipse", Doc: "Ellipse is a SVG ellipse", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Pos", Doc: "position of the center of the ellipse"}, {Name: "Radii", Doc: "radii of the ellipse in the horizontal, vertical axes"}}})
// NewEllipse returns a new [Ellipse] with the given optional parent:
// Ellipse is a SVG ellipse
func NewEllipse(parent ...tree.Node) *Ellipse { return tree.New[Ellipse](parent...) }
// SetPos sets the [Ellipse.Pos]:
// position of the center of the ellipse
func (t *Ellipse) SetPos(v math32.Vector2) *Ellipse { t.Pos = v; return t }
// SetRadii sets the [Ellipse.Radii]:
// radii of the ellipse in the horizontal, vertical axes
func (t *Ellipse) SetRadii(v math32.Vector2) *Ellipse { t.Radii = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Filter", IDName: "filter", Doc: "Filter represents SVG filter* elements", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "FilterType"}}})
// NewFilter returns a new [Filter] with the given optional parent:
// Filter represents SVG filter* elements
func NewFilter(parent ...tree.Node) *Filter { return tree.New[Filter](parent...) }
// SetFilterType sets the [Filter.FilterType]
func (t *Filter) SetFilterType(v string) *Filter { t.FilterType = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Flow", IDName: "flow", Doc: "Flow represents SVG flow* elements", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "FlowType"}}})
// NewFlow returns a new [Flow] with the given optional parent:
// Flow represents SVG flow* elements
func NewFlow(parent ...tree.Node) *Flow { return tree.New[Flow](parent...) }
// SetFlowType sets the [Flow.FlowType]
func (t *Flow) SetFlowType(v string) *Flow { t.FlowType = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Gradient", IDName: "gradient", Doc: "Gradient is used for holding a specified color gradient.\nThe name is the id for lookup in url", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Grad", Doc: "the color gradient"}, {Name: "StopsName", Doc: "name of another gradient to get stops from"}}})
// NewGradient returns a new [Gradient] with the given optional parent:
// Gradient is used for holding a specified color gradient.
// The name is the id for lookup in url
func NewGradient(parent ...tree.Node) *Gradient { return tree.New[Gradient](parent...) }
// SetGrad sets the [Gradient.Grad]:
// the color gradient
func (t *Gradient) SetGrad(v gradient.Gradient) *Gradient { t.Grad = v; return t }
// SetStopsName sets the [Gradient.StopsName]:
// name of another gradient to get stops from
func (t *Gradient) SetStopsName(v string) *Gradient { t.StopsName = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Group", IDName: "group", Doc: "Group groups together SVG elements.\nProvides a common transform for all group elements\nand shared style properties.", Embeds: []types.Field{{Name: "NodeBase"}}})
// NewGroup returns a new [Group] with the given optional parent:
// Group groups together SVG elements.
// Provides a common transform for all group elements
// and shared style properties.
func NewGroup(parent ...tree.Node) *Group { return tree.New[Group](parent...) }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Image", IDName: "image", Doc: "Image is an SVG image (bitmap)", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Pos", Doc: "position of the top-left of the image"}, {Name: "Size", Doc: "rendered size of the image (imposes a scaling on image when it is rendered)"}, {Name: "Filename", Doc: "file name of image loaded -- set by OpenImage"}, {Name: "ViewBox", Doc: "how to scale and align the image"}, {Name: "Pixels", Doc: "the image pixels"}}})
// NewImage returns a new [Image] with the given optional parent:
// Image is an SVG image (bitmap)
func NewImage(parent ...tree.Node) *Image { return tree.New[Image](parent...) }
// SetPos sets the [Image.Pos]:
// position of the top-left of the image
func (t *Image) SetPos(v math32.Vector2) *Image { t.Pos = v; return t }
// SetSize sets the [Image.Size]:
// rendered size of the image (imposes a scaling on image when it is rendered)
func (t *Image) SetSize(v math32.Vector2) *Image { t.Size = v; return t }
// SetFilename sets the [Image.Filename]:
// file name of image loaded -- set by OpenImage
func (t *Image) SetFilename(v string) *Image { t.Filename = v; return t }
// SetViewBox sets the [Image.ViewBox]:
// how to scale and align the image
func (t *Image) SetViewBox(v ViewBox) *Image { t.ViewBox = v; return t }
// SetPixels sets the [Image.Pixels]:
// the image pixels
func (t *Image) SetPixels(v *image.RGBA) *Image { t.Pixels = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Line", IDName: "line", Doc: "Line is a SVG line", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Start", Doc: "position of the start of the line"}, {Name: "End", Doc: "position of the end of the line"}}})
// NewLine returns a new [Line] with the given optional parent:
// Line is a SVG line
func NewLine(parent ...tree.Node) *Line { return tree.New[Line](parent...) }
// SetStart sets the [Line.Start]:
// position of the start of the line
func (t *Line) SetStart(v math32.Vector2) *Line { t.Start = v; return t }
// SetEnd sets the [Line.End]:
// position of the end of the line
func (t *Line) SetEnd(v math32.Vector2) *Line { t.End = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Marker", IDName: "marker", Doc: "Marker represents marker elements that can be drawn along paths (arrow heads, etc)", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "RefPos", Doc: "reference position to align the vertex position with, specified in ViewBox coordinates"}, {Name: "Size", Doc: "size of marker to render, in Units units"}, {Name: "Units", Doc: "units to use"}, {Name: "ViewBox", Doc: "viewbox defines the internal coordinate system for the drawing elements within the marker"}, {Name: "Orient", Doc: "orientation of the marker -- either 'auto' or an angle"}, {Name: "VertexPos", Doc: "current vertex position"}, {Name: "VertexAngle", Doc: "current vertex angle in radians"}, {Name: "StrokeWidth", Doc: "current stroke width"}, {Name: "Transform", Doc: "net transform computed from settings and current values -- applied prior to rendering"}, {Name: "EffSize", Doc: "effective size for actual rendering"}}})
// NewMarker returns a new [Marker] with the given optional parent:
// Marker represents marker elements that can be drawn along paths (arrow heads, etc)
func NewMarker(parent ...tree.Node) *Marker { return tree.New[Marker](parent...) }
// SetRefPos sets the [Marker.RefPos]:
// reference position to align the vertex position with, specified in ViewBox coordinates
func (t *Marker) SetRefPos(v math32.Vector2) *Marker { t.RefPos = v; return t }
// SetSize sets the [Marker.Size]:
// size of marker to render, in Units units
func (t *Marker) SetSize(v math32.Vector2) *Marker { t.Size = v; return t }
// SetUnits sets the [Marker.Units]:
// units to use
func (t *Marker) SetUnits(v MarkerUnits) *Marker { t.Units = v; return t }
// SetViewBox sets the [Marker.ViewBox]:
// viewbox defines the internal coordinate system for the drawing elements within the marker
func (t *Marker) SetViewBox(v ViewBox) *Marker { t.ViewBox = v; return t }
// SetOrient sets the [Marker.Orient]:
// orientation of the marker -- either 'auto' or an angle
func (t *Marker) SetOrient(v string) *Marker { t.Orient = v; return t }
// SetVertexPos sets the [Marker.VertexPos]:
// current vertex position
func (t *Marker) SetVertexPos(v math32.Vector2) *Marker { t.VertexPos = v; return t }
// SetVertexAngle sets the [Marker.VertexAngle]:
// current vertex angle in radians
func (t *Marker) SetVertexAngle(v float32) *Marker { t.VertexAngle = v; return t }
// SetStrokeWidth sets the [Marker.StrokeWidth]:
// current stroke width
func (t *Marker) SetStrokeWidth(v float32) *Marker { t.StrokeWidth = v; return t }
// SetTransform sets the [Marker.Transform]:
// net transform computed from settings and current values -- applied prior to rendering
func (t *Marker) SetTransform(v math32.Matrix2) *Marker { t.Transform = v; return t }
// SetEffSize sets the [Marker.EffSize]:
// 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]."}}})
// NewNodeBase returns a new [NodeBase] with the given optional parent:
// NodeBase is the base type for all elements within an SVG tree.
// It implements the [Node] interface and contains the core functionality.
func NewNodeBase(parent ...tree.Node) *NodeBase { return tree.New[NodeBase](parent...) }
// SetClass sets the [NodeBase.Class]:
// Class contains user-defined class name(s) used primarily for attaching
// CSS styles to different display elements.
// Multiple class names can be used to combine properties;
// use spaces to separate per css standard.
func (t *NodeBase) SetClass(v string) *NodeBase { t.Class = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Path", IDName: "path", Doc: "Path renders SVG data sequences that can render just about anything", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Data", Doc: "the path data to render -- path commands and numbers are serialized, with each command specifying the number of floating-point coord data points that follow"}, {Name: "DataStr", Doc: "string version of the path data"}}})
// NewPath returns a new [Path] with the given optional parent:
// Path renders SVG data sequences that can render just about anything
func NewPath(parent ...tree.Node) *Path { return tree.New[Path](parent...) }
// SetDataStr sets the [Path.DataStr]:
// string version of the path data
func (t *Path) SetDataStr(v string) *Path { t.DataStr = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Polygon", IDName: "polygon", Doc: "Polygon is a SVG polygon", Embeds: []types.Field{{Name: "Polyline"}}})
// NewPolygon returns a new [Polygon] with the given optional parent:
// Polygon is a SVG polygon
func NewPolygon(parent ...tree.Node) *Polygon { return tree.New[Polygon](parent...) }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Polyline", IDName: "polyline", Doc: "Polyline is a SVG multi-line shape", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Points", Doc: "the coordinates to draw -- does a moveto on the first, then lineto for all the rest"}}})
// NewPolyline returns a new [Polyline] with the given optional parent:
// Polyline is a SVG multi-line shape
func NewPolyline(parent ...tree.Node) *Polyline { return tree.New[Polyline](parent...) }
// SetPoints sets the [Polyline.Points]:
// the coordinates to draw -- does a moveto on the first, then lineto for all the rest
func (t *Polyline) SetPoints(v ...math32.Vector2) *Polyline { t.Points = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Rect", IDName: "rect", Doc: "Rect is a SVG rectangle, optionally with rounded corners", Embeds: []types.Field{{Name: "NodeBase"}}, Fields: []types.Field{{Name: "Pos", Doc: "position of the top-left of the rectangle"}, {Name: "Size", Doc: "size of the rectangle"}, {Name: "Radius", Doc: "radii for curved corners, as a proportion of width, height"}}})
// NewRect returns a new [Rect] with the given optional parent:
// Rect is a SVG rectangle, optionally with rounded corners
func NewRect(parent ...tree.Node) *Rect { return tree.New[Rect](parent...) }
// SetPos sets the [Rect.Pos]:
// position of the top-left of the rectangle
func (t *Rect) SetPos(v math32.Vector2) *Rect { t.Pos = v; return t }
// SetSize sets the [Rect.Size]:
// size of the rectangle
func (t *Rect) SetSize(v math32.Vector2) *Rect { t.Size = v; return t }
// SetRadius sets the [Rect.Radius]:
// radii for curved corners, as a proportion of width, height
func (t *Rect) SetRadius(v math32.Vector2) *Rect { t.Radius = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/svg.Root", IDName: "root", Doc: "Root represents the root of an SVG tree.", Embeds: []types.Field{{Name: "Group"}}, Fields: []types.Field{{Name: "ViewBox", Doc: "ViewBox defines the coordinate system for the drawing.\nThese units are mapped into the screen space allocated\nfor the SVG during rendering."}}})
// NewRoot returns a new [Root] with the given optional parent:
// Root represents the root of an SVG tree.
func NewRoot(parent ...tree.Node) *Root { return tree.New[Root](parent...) }
// SetViewBox sets the [Root.ViewBox]:
// ViewBox defines the coordinate system for the drawing.
// These units are mapped into the screen space allocated
// 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 -- 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: "TextRender", 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?"}, {Name: "LastPos", Doc: "last text render position -- lower-left baseline of start"}, {Name: "LastBBox", Doc: "last actual bounding box in display units (dots)"}}})
// 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 -- text has empty Text string.
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 }
// SetTextRender sets the [Text.TextRender]:
// render version of text
func (t *Text) SetTextRender(v paint.Text) *Text { t.TextRender = v; return t }
// SetCharPosX sets the [Text.CharPosX]:
// character positions along X axis, if specified
func (t *Text) SetCharPosX(v ...float32) *Text { t.CharPosX = v; return t }
// SetCharPosY sets the [Text.CharPosY]:
// character positions along Y axis, if specified
func (t *Text) SetCharPosY(v ...float32) *Text { t.CharPosY = v; return t }
// SetCharPosDX sets the [Text.CharPosDX]:
// character delta-positions along X axis, if specified
func (t *Text) SetCharPosDX(v ...float32) *Text { t.CharPosDX = v; return t }
// SetCharPosDY sets the [Text.CharPosDY]:
// character delta-positions along Y axis, if specified
func (t *Text) SetCharPosDY(v ...float32) *Text { t.CharPosDY = v; return t }
// SetCharRots sets the [Text.CharRots]:
// character rotations, if specified
func (t *Text) SetCharRots(v ...float32) *Text { t.CharRots = v; return t }
// SetTextLength sets the [Text.TextLength]:
// author's computed text length, if specified -- we attempt to match
func (t *Text) SetTextLength(v float32) *Text { t.TextLength = v; return t }
// SetAdjustGlyphs sets the [Text.AdjustGlyphs]:
// in attempting to match TextLength, should we adjust glyphs in addition to spacing?
func (t *Text) SetAdjustGlyphs(v bool) *Text { t.AdjustGlyphs = v; return t }
// SetLastPos sets the [Text.LastPos]:
// last text render position -- lower-left baseline of start
func (t *Text) SetLastPos(v math32.Vector2) *Text { t.LastPos = v; return t }
// SetLastBBox sets the [Text.LastBBox]:
// last actual bounding box in display units (dots)
func (t *Text) SetLastBBox(v math32.Box2) *Text { t.LastBBox = v; return t }
// 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 (
"fmt"
"log"
"math/rand"
"slices"
"strconv"
"strings"
"unicode"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/tree"
)
/////////////////////////////////////////////////////////////////////////////
// Naming elements with unique id's
// SplitNameIDDig splits name into numerical end part and preceding name,
// based on string of digits from end of name.
// If Id == 0 then it was not specified or didn't parse.
// SVG object names are element names + numerical id
func SplitNameIDDig(nm string) (string, int) {
sz := len(nm)
for i := sz - 1; i >= 0; i-- {
c := rune(nm[i])
if !unicode.IsDigit(c) {
if i == sz-1 {
return nm, 0
}
n := nm[:i+1]
id, _ := strconv.Atoi(nm[i+1:])
return n, id
}
}
return nm, 0
}
// SplitNameID splits name after the element name (e.g., 'rect')
// returning true if it starts with element name,
// and numerical id part after that element.
// if numerical id part is 0, then it didn't parse.
// SVG object names are element names + numerical id
func SplitNameID(elnm, nm string) (bool, int) {
if !strings.HasPrefix(nm, elnm) {
// fmt.Printf("not elnm: %s %s\n", nm, elnm)
return false, 0
}
idstr := nm[len(elnm):]
id, _ := strconv.Atoi(idstr)
return true, id
}
// NameID returns the name with given unique id.
// returns plain name if id == 0
func NameID(nm string, id int) string {
if id == 0 {
return nm
}
return fmt.Sprintf("%s%d", nm, id)
}
// GatherIDs gathers all the numeric id suffixes currently in use.
// It automatically renames any that are not unique or empty.
func (sv *SVG) GatherIDs() {
sv.UniqueIDs = make(map[int]struct{})
sv.Root.WalkDown(func(n tree.Node) bool {
sv.NodeEnsureUniqueID(n.(Node))
return tree.Continue
})
}
// NodeEnsureUniqueID ensures that the given node has a unique ID.
// Call this on any newly created nodes.
func (sv *SVG) NodeEnsureUniqueID(n Node) {
elnm := n.SVGName()
if elnm == "" {
return
}
nb := n.AsNodeBase()
elpfx, id := SplitNameID(elnm, nb.Name)
if !elpfx {
if !n.EnforceSVGName() { // if we end in a number, just register it anyway
_, id = SplitNameIDDig(nb.Name)
if id > 0 {
sv.UniqueIDs[id] = struct{}{}
}
return
}
_, id = SplitNameIDDig(nb.Name)
if id > 0 {
nb.SetName(NameID(elnm, id))
}
}
_, exists := sv.UniqueIDs[id]
if id <= 0 || exists {
id = sv.NewUniqueID() // automatically registers it
nb.SetName(NameID(elnm, id))
} else {
sv.UniqueIDs[id] = struct{}{}
}
}
// NewUniqueID returns a new unique numerical id number, for naming an object
func (sv *SVG) NewUniqueID() int {
if sv.UniqueIDs == nil {
sv.GatherIDs()
}
sz := len(sv.UniqueIDs)
var nid int
for {
switch {
case sz >= 10000:
nid = rand.Intn(sz * 100)
case sz >= 1000:
nid = rand.Intn(10000)
default:
nid = rand.Intn(1000)
}
if _, has := sv.UniqueIDs[nid]; has {
continue
}
break
}
sv.UniqueIDs[nid] = struct{}{}
return nid
}
// FindDefByName finds Defs item by name, using cached indexes for speed
func (sv *SVG) FindDefByName(defnm string) Node {
if sv.DefIndexes == nil {
sv.DefIndexes = make(map[string]int)
}
idx, has := sv.DefIndexes[defnm]
if !has {
idx = len(sv.Defs.Children) / 2
}
dn := sv.Defs.ChildByName(defnm, idx)
if dn != nil {
sv.DefIndexes[defnm] = dn.AsTree().IndexInParent()
return dn.(Node)
}
delete(sv.DefIndexes, defnm) // not found, so delete from map
return nil
}
func (sv *SVG) FindNamedElement(name string) Node {
name = strings.TrimPrefix(name, "#")
def := sv.FindDefByName(name)
if def != nil {
return def
}
sv.Root.WalkDown(func(n tree.Node) bool {
if n.AsTree().Name == name {
def = n.(Node)
return tree.Break
}
return tree.Continue
})
if def != nil {
return def
}
log.Printf("SVG FindNamedElement: could not find name: %v\n", name)
return nil
}
// NameFromURL returns just the name referred to in a url(#name)
// if it is not a url(#) format then returns empty string.
func NameFromURL(url string) string {
if len(url) < 7 {
return ""
}
if url[:5] != "url(#" {
return ""
}
ref := url[5:]
sz := len(ref)
if ref[sz-1] == ')' {
ref = ref[:sz-1]
}
return ref
}
// NameToURL returns url as: url(#name)
func NameToURL(nm string) string {
return "url(#" + nm + ")"
}
// NodeFindURL finds a url element in the parent SVG of given node.
// Returns nil if not found.
// Works with full 'url(#Name)' string or plain name or "none"
func (sv *SVG) NodeFindURL(n Node, url string) Node {
if url == "none" {
return nil
}
ref := NameFromURL(url)
if ref == "" {
ref = url
}
if ref == "" {
return nil
}
rv := sv.FindNamedElement(ref)
if rv == nil {
log.Printf("svg.NodeFindURL could not find element named: %s for element: %s\n", url, n.AsTree().Path())
}
return rv
}
// NodePropURL returns a url(#name) url from given prop name on node,
// or empty string if none. Returned value is just the 'name' part
// of the url, not the full string.
func NodePropURL(n Node, prop string) string {
fp := n.AsTree().Property(prop)
fs, iss := fp.(string)
if !iss {
return ""
}
return NameFromURL(fs)
}
const SVGRefCountKey = "SVGRefCount"
func IncRefCount(k tree.Node) {
rc := k.AsTree().Property(SVGRefCountKey).(int)
rc++
k.AsTree().SetProperty(SVGRefCountKey, rc)
}
// RemoveOrphanedDefs 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
// should be removed manually, as they are not automatically generated.
func (sv *SVG) RemoveOrphanedDefs() bool {
refkey := SVGRefCountKey
for _, k := range sv.Defs.Children {
k.AsTree().SetProperty(refkey, 0)
}
sv.Root.WalkDown(func(k tree.Node) bool {
pr := k.AsTree().Properties
for _, v := range pr {
ps := reflectx.ToString(v)
if !strings.HasPrefix(ps, "url(#") {
continue
}
nm := NameFromURL(ps)
el := sv.FindDefByName(nm)
if el != nil {
IncRefCount(el)
}
}
if gr, isgr := k.(*Gradient); isgr {
if gr.StopsName != "" {
el := sv.FindDefByName(gr.StopsName)
if el != nil {
IncRefCount(el)
}
} else {
if gr.Grad != nil && len(gr.Grad.AsBase().Stops) > 0 {
IncRefCount(k) // keep us around
}
}
}
return tree.Continue
})
sz := len(sv.Defs.Children)
del := false
for i := sz - 1; i >= 0; i-- {
n := sv.Defs.Children[i]
rc := n.AsTree().Property(refkey).(int)
if rc == 0 {
fmt.Printf("Deleting unused item: %s\n", n.AsTree().Name)
sv.Defs.Children = slices.Delete(sv.Defs.Children, i, i+1)
del = true
} else {
n.AsTree().DeleteProperty(refkey)
}
}
return del
}
// 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 (
"errors"
"fmt"
"strings"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
)
////////////////////////////////////////////////////////////////////////////////////////
// ViewBox defines the SVG viewbox
// ViewBox is used in SVG to define the coordinate system
type ViewBox struct {
// offset or starting point in parent Viewport2D
Min math32.Vector2
// size of viewbox within parent Viewport2D
Size math32.Vector2
// how to scale the view box within parent
PreserveAspectRatio ViewBoxPreserveAspectRatio
}
// Defaults returns viewbox to defaults
func (vb *ViewBox) Defaults() {
vb.Min = math32.Vector2{}
vb.Size = math32.Vec2(100, 100)
vb.PreserveAspectRatio.Align.Set(AlignMid)
vb.PreserveAspectRatio.MeetOrSlice = Meet
}
// BoxString returns the string representation of just the viewbox:
// "min.X min.Y size.X size.Y"
func (vb *ViewBox) BoxString() string {
return fmt.Sprintf(`viewbox="%g %g %g %g"`, vb.Min.X, vb.Min.Y, vb.Size.X, vb.Size.Y)
}
func (vb *ViewBox) String() string {
return vb.BoxString() + ` preserveAspectRatio="` + vb.PreserveAspectRatio.String() + `"`
}
// Transform returns the transform based on viewbox size relative to given box
// (viewport) size that it will be rendered into
func (vb *ViewBox) Transform(box math32.Vector2) (size, trans, scale math32.Vector2) {
of := styles.FitFill
switch {
case vb.PreserveAspectRatio.Align.X == AlignNone:
of = styles.FitFill
case vb.PreserveAspectRatio.MeetOrSlice == Meet:
of = styles.FitContain
case vb.PreserveAspectRatio.MeetOrSlice == Slice:
of = styles.FitCover
}
if vb.Size.X == 0 || vb.Size.Y == 0 {
vb.Size = math32.Vec2(100, 100)
}
size = styles.ObjectSizeFromFit(of, vb.Size, box)
scale = size.Div(vb.Size)
extra := box.Sub(size)
if extra.X > 0 {
trans.X = extra.X * vb.PreserveAspectRatio.Align.X.AlignFactor()
}
if extra.Y > 0 {
trans.Y = extra.Y * vb.PreserveAspectRatio.Align.Y.AlignFactor()
}
trans.SetDiv(scale)
return
}
// ViewBoxAlign defines values for the PreserveAspectRatio alignment factor
type ViewBoxAligns int32 //enums:enum -trim-prefix Align -transform lower
const (
// align ViewBox.Min with midpoint of Viewport (default)
AlignMid ViewBoxAligns = iota
// do not preserve uniform scaling (if either X or Y is None, both are treated as such).
// In this case, the Meet / Slice value is ignored.
// This is the same as FitFill from styles.ObjectFits
AlignNone
// align ViewBox.Min with top / left of Viewport
AlignMin
// align ViewBox.Min+Size with bottom / right of Viewport
AlignMax
)
// Aligns returns the styles.Aligns version of ViewBoxAligns
func (va ViewBoxAligns) Aligns() styles.Aligns {
switch va {
case AlignNone:
return styles.Start
case AlignMin:
return styles.Start
case AlignMax:
return styles.End
default:
return styles.Center
}
}
// SetFromAligns sets alignment from the styles.Aligns version of ViewBoxAligns
func (va *ViewBoxAligns) SetFromAligns(a styles.Aligns) {
switch a {
case styles.Start:
*va = AlignMin
case styles.End:
*va = AlignMax
case styles.Center:
*va = AlignMid
}
}
// AlignFactor returns the alignment factor for proportion offset
func (va ViewBoxAligns) AlignFactor() float32 {
return styles.AlignFactor(va.Aligns())
}
// ViewBoxMeetOrSlice defines values for the PreserveAspectRatio meet or slice factor
type ViewBoxMeetOrSlice int32 //enums:enum -transform lower
const (
// Meet only applies if Align != None (i.e., only for uniform scaling),
// and means the entire ViewBox is visible within Viewport,
// and it is scaled up as much as possible to meet the align constraints.
// This is the same as FitContain from styles.ObjectFits
Meet ViewBoxMeetOrSlice = iota
// Slice only applies if Align != None (i.e., only for uniform scaling),
// and means the entire ViewBox is covered by the ViewBox, and the
// ViewBox is scaled down as much as possible, while still meeting the
// align constraints.
// This is the same as FitCover from styles.ObjectFits
Slice
)
// ViewBoxPreserveAspectRatio determines how to scale the view box within parent Viewport2D
type ViewBoxPreserveAspectRatio struct {
// how to align X, Y coordinates within viewbox
Align styles.XY[ViewBoxAligns] `xml:"align"`
// how to scale the view box relative to the viewport
MeetOrSlice ViewBoxMeetOrSlice `xml:"meetOrSlice"`
}
func (pa *ViewBoxPreserveAspectRatio) String() string {
if pa.Align.X == AlignNone {
return "none"
}
xs := "xM" + pa.Align.X.String()[1:]
ys := "YM" + pa.Align.Y.String()[1:]
s := xs + ys
if pa.MeetOrSlice != Meet {
s += " slice"
}
return s
}
// SetString sets from a standard svg-formatted string,
// consisting of:
// none | x[Min, Mid, Max]Y[Min, Mid, Max] [ meet | slice]
// e.g., "xMidYMid meet" (default)
// It does not make sense to specify "meet | slice" for "none"
// as they do not apply in that case.
func (pa *ViewBoxPreserveAspectRatio) SetString(s string) error {
s = strings.TrimSpace(s)
if len(s) == 0 {
pa.Align.Set(AlignMid, AlignMid)
pa.MeetOrSlice = Meet
return nil
}
sl := strings.ToLower(s)
f := strings.Fields(sl)
if strings.HasPrefix(f[0], "none") {
pa.Align.Set(AlignNone)
pa.MeetOrSlice = Meet
return nil
}
var errs []error
if len(f) > 1 {
switch f[1] {
case "slice":
pa.MeetOrSlice = Slice
case "meet":
pa.MeetOrSlice = Meet
default:
errs = append(errs, fmt.Errorf("ViewBoxPreserveAspectRatio: 2nd value must be meet or slice, not %q", f[1]))
}
}
yi := strings.Index(f[0], "y")
if yi < 0 {
return fmt.Errorf("ViewBoxPreserveAspectRatio: string %q must contain a 'y'", s)
}
xs := f[0][1:yi]
ys := f[0][yi+1:]
err := pa.Align.X.SetString(xs)
if err != nil {
errs = append(errs, fmt.Errorf("ViewBoxPreserveAspectRatio: X align be min, mid, or max, not %q", xs))
}
err = pa.Align.Y.SetString(ys)
if err != nil {
errs = append(errs, fmt.Errorf("ViewBoxPreserveAspectRatio: Y align be min, mid, or max, not %q", ys))
}
if len(errs) == 0 {
return nil
}
return errors.Join(errs...)
}
// SetFromStyle sets from ObjectFit and Justify (X) and Align (Y) Content
// in given style.
func (pa *ViewBoxPreserveAspectRatio) SetFromStyle(s *styles.Style) {
pa.Align.X.SetFromAligns(s.Justify.Content)
pa.Align.Y.SetFromAligns(s.Align.Content)
// todo: could override with ObjectPosition but maybe not worth it?
switch s.ObjectFit {
case styles.FitFill:
pa.Align.Set(AlignNone)
case styles.FitContain:
pa.MeetOrSlice = Meet
case styles.FitCover, styles.FitScaleDown: // note: FitScaleDown not handled
pa.MeetOrSlice = Slice
}
}
// 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 (
"bufio"
"bytes"
"encoding/xml"
"fmt"
"io"
"unicode/utf8"
)
// XMLEncoder is a minimal XML encoder that formats output with Attr
// each on a new line, using same API as xml.Encoder
type XMLEncoder struct {
Writer io.Writer
DoIndent bool
IndBytes []byte
PreBytes []byte
CurIndent int
CurStart string
NoEndIndent bool
}
func NewXMLEncoder(wr io.Writer) *XMLEncoder {
return &XMLEncoder{Writer: wr}
}
func (xe *XMLEncoder) Indent(prefix, indent string) {
if len(indent) > 0 {
xe.DoIndent = true
}
xe.IndBytes = []byte(indent)
xe.PreBytes = []byte(prefix)
}
func (xe *XMLEncoder) EncodeToken(t xml.Token) error {
switch t := t.(type) {
case xml.StartElement:
if err := xe.WriteStart(&t); err != nil {
return err
}
case xml.EndElement:
if err := xe.WriteEnd(t.Name.Local); err != nil {
return err
}
case xml.CharData:
if xe.CurStart != "" {
xe.WriteString(">")
xe.CurStart = ""
xe.NoEndIndent = true // don't indent the end now
}
EscapeText(xe.Writer, t, false)
}
return nil
}
func (xe *XMLEncoder) WriteString(str string) {
xe.Writer.Write([]byte(str))
}
func (xe *XMLEncoder) WriteIndent() {
xe.Writer.Write(xe.PreBytes)
xe.Writer.Write(bytes.Repeat(xe.IndBytes, xe.CurIndent))
}
func (xe *XMLEncoder) WriteEOL() {
xe.Writer.Write([]byte("\n"))
}
// Decide whether the given rune is in the XML Character Range, per
// the Char production of https://www.xml.com/axml/testaxml.htm,
// Section 2.2 Characters.
func isInCharacterRange(r rune) (inrange bool) {
return r == 0x09 ||
r == 0x0A ||
r == 0x0D ||
r >= 0x20 && r <= 0xD7FF ||
r >= 0xE000 && r <= 0xFFFD ||
r >= 0x10000 && r <= 0x10FFFF
}
var (
escQuot = []byte(""") // shorter than """
escApos = []byte("'") // shorter than "'"
escAmp = []byte("&")
escLT = []byte("<")
escGT = []byte(">")
escTab = []byte("	")
escNL = []byte("
")
escCR = []byte("
")
escFFFD = []byte("\uFFFD") // Unicode replacement character
)
// XMLEscapeText writes to w the properly escaped XML equivalent
// of the plain text data s. If escapeNewline is true, newline
// XMLcharacters will be escaped.
func EscapeText(w io.Writer, s []byte, escapeNewline bool) error {
var esc []byte
last := 0
for i := 0; i < len(s); {
r, width := utf8.DecodeRune(s[i:])
i += width
switch r {
case '"':
esc = escQuot
case '\'':
esc = escApos
case '&':
esc = escAmp
case '<':
esc = escLT
case '>':
esc = escGT
case '\t':
esc = escTab
case '\n':
if !escapeNewline {
continue
}
esc = escNL
case '\r':
esc = escCR
default:
if !isInCharacterRange(r) || (r == 0xFFFD && width == 1) {
esc = escFFFD
break
}
continue
}
if _, err := w.Write(s[last : i-width]); err != nil {
return err
}
if _, err := w.Write(esc); err != nil {
return err
}
last = i
}
_, err := w.Write(s[last:])
return err
}
// EscapeString writes to p the properly escaped XML equivalent
// of the plain text data s.
func (xe *XMLEncoder) EscapeString(s string, escapeNewline bool) {
var esc []byte
last := 0
for i := 0; i < len(s); {
r, width := utf8.DecodeRuneInString(s[i:])
i += width
switch r {
case '"':
esc = escQuot
case '\'':
esc = escApos
case '&':
esc = escAmp
case '<':
esc = escLT
case '>':
esc = escGT
case '\t':
esc = escTab
case '\n':
if !escapeNewline {
continue
}
esc = escNL
case '\r':
esc = escCR
default:
if !isInCharacterRange(r) || (r == 0xFFFD && width == 1) {
esc = escFFFD
break
}
continue
}
xe.WriteString(s[last : i-width])
xe.Writer.Write(esc)
last = i
}
xe.WriteString(s[last:])
}
func (xe *XMLEncoder) WriteStart(start *xml.StartElement) error {
if start.Name.Local == "" {
return fmt.Errorf("xml: start tag with no name")
}
if xe.CurStart != "" {
xe.WriteString(">")
xe.WriteEOL()
}
xe.WriteIndent()
xe.WriteString("<")
xe.WriteString(start.Name.Local)
xe.CurIndent++
xe.CurStart = start.Name.Local
// Attributes
for _, attr := range start.Attr {
name := attr.Name
if name.Local == "" {
continue
}
xe.WriteEOL()
xe.WriteIndent()
xe.WriteString(name.Local)
xe.WriteString(`="`)
xe.EscapeString(attr.Value, false)
xe.WriteString(`"`)
}
return nil
}
func (xe *XMLEncoder) WriteEnd(name string) error {
xe.CurIndent--
if name == "" {
return fmt.Errorf("xml: end tag with no name")
}
if xe.CurStart == name {
xe.WriteString(" />")
xe.WriteEOL()
} else {
if !xe.NoEndIndent {
xe.WriteIndent()
}
xe.NoEndIndent = false
xe.WriteString("</")
xe.WriteString(name)
xe.WriteString(">")
xe.WriteEOL()
}
xe.CurStart = ""
xe.Flush()
return nil
}
func (xe *XMLEncoder) Flush() {
if bw, isb := xe.Writer.(*bufio.Writer); isb {
bw.Flush()
}
}
// Copyright (c) 2024, 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 tensor
import (
"fmt"
"log"
"reflect"
"unsafe"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/base/slicesx"
)
// Base is an n-dim array of float64s.
type Base[T any] struct {
Shp Shape
Values []T
Meta map[string]string
}
// Shape returns a pointer to the shape that fully parametrizes the tensor shape
func (tsr *Base[T]) Shape() *Shape { return &tsr.Shp }
// Len returns the number of elements in the tensor (product of shape dimensions).
func (tsr *Base[T]) Len() int { return tsr.Shp.Len() }
// NumDims returns the total number of dimensions.
func (tsr *Base[T]) NumDims() int { return tsr.Shp.NumDims() }
// DimSize returns size of given dimension
func (tsr *Base[T]) DimSize(dim int) int { return tsr.Shp.DimSize(dim) }
// RowCellSize returns the size of the outer-most Row shape dimension,
// and the size of all the remaining inner dimensions (the "cell" size).
// Used for Tensors that are columns in a data table.
func (tsr *Base[T]) RowCellSize() (rows, cells int) {
return tsr.Shp.RowCellSize()
}
// DataType returns the type of the data elements in the tensor.
// Bool is returned for the Bits tensor type.
func (tsr *Base[T]) DataType() reflect.Kind {
var v T
return reflect.TypeOf(v).Kind()
}
func (tsr *Base[T]) Sizeof() int64 {
var v T
return int64(unsafe.Sizeof(v)) * int64(tsr.Len())
}
func (tsr *Base[T]) Bytes() []byte {
return slicesx.ToBytes(tsr.Values)
}
func (tsr *Base[T]) Value(i []int) T { j := tsr.Shp.Offset(i); return tsr.Values[j] }
func (tsr *Base[T]) Value1D(i int) T { return tsr.Values[i] }
func (tsr *Base[T]) Set(i []int, val T) { j := tsr.Shp.Offset(i); tsr.Values[j] = val }
func (tsr *Base[T]) Set1D(i int, val T) { tsr.Values[i] = val }
// SetShape sets the shape params, resizing backing storage appropriately
func (tsr *Base[T]) SetShape(sizes []int, names ...string) {
tsr.Shp.SetShape(sizes, names...)
nln := tsr.Len()
tsr.Values = slicesx.SetLength(tsr.Values, nln)
}
// SetNumRows sets the number of rows (outer-most dimension) in a RowMajor organized tensor.
func (tsr *Base[T]) SetNumRows(rows int) {
rows = max(1, rows) // must be > 0
_, cells := tsr.Shp.RowCellSize()
nln := rows * cells
tsr.Shp.Sizes[0] = rows
tsr.Values = slicesx.SetLength(tsr.Values, nln)
}
// subSpaceImpl returns a new tensor with innermost subspace at given
// offset(s) in outermost dimension(s) (len(offs) < NumDims).
// The new tensor points to the values of the this tensor (i.e., modifications
// will affect both), as its Values slice is a view onto the original (which
// is why only inner-most contiguous supsaces are supported).
// Use Clone() method to separate the two.
func (tsr *Base[T]) subSpaceImpl(offs []int) *Base[T] {
nd := tsr.NumDims()
od := len(offs)
if od >= nd {
return nil
}
stsr := &Base[T]{}
stsr.SetShape(tsr.Shp.Sizes[od:], tsr.Shp.Names[od:]...)
sti := make([]int, nd)
copy(sti, offs)
stoff := tsr.Shp.Offset(sti)
sln := stsr.Len()
stsr.Values = tsr.Values[stoff : stoff+sln]
return stsr
}
func (tsr *Base[T]) StringValue(i []int) string {
j := tsr.Shp.Offset(i)
return reflectx.ToString(tsr.Values[j])
}
func (tsr *Base[T]) String1D(off int) string { return reflectx.ToString(tsr.Values[off]) }
func (tsr *Base[T]) StringRowCell(row, cell int) string {
_, sz := tsr.Shp.RowCellSize()
return reflectx.ToString(tsr.Values[row*sz+cell])
}
// Label satisfies the core.Labeler interface for a summary description of the tensor
func (tsr *Base[T]) Label() string {
return fmt.Sprintf("Tensor: %s", tsr.Shp.String())
}
// Dims is the gonum/mat.Matrix interface method for returning the dimensionality of the
// 2D Matrix. Assumes Row-major ordering and logs an error if NumDims < 2.
func (tsr *Base[T]) Dims() (r, c int) {
nd := tsr.NumDims()
if nd < 2 {
log.Println("tensor Dims gonum Matrix call made on Tensor with dims < 2")
return 0, 0
}
return tsr.Shp.DimSize(nd - 2), tsr.Shp.DimSize(nd - 1)
}
// Symmetric is the gonum/mat.Matrix interface method for returning the dimensionality of a symmetric
// 2D Matrix.
func (tsr *Base[T]) Symmetric() (r int) {
nd := tsr.NumDims()
if nd < 2 {
log.Println("tensor Symmetric gonum Matrix call made on Tensor with dims < 2")
return 0
}
if tsr.Shp.DimSize(nd-2) != tsr.Shp.DimSize(nd-1) {
log.Println("tensor Symmetric gonum Matrix call made on Tensor that is not symmetric")
return 0
}
return tsr.Shp.DimSize(nd - 1)
}
// SymmetricDim returns the number of rows/columns in the matrix.
func (tsr *Base[T]) SymmetricDim() int {
nd := tsr.NumDims()
if nd < 2 {
log.Println("tensor Symmetric gonum Matrix call made on Tensor with dims < 2")
return 0
}
if tsr.Shp.DimSize(nd-2) != tsr.Shp.DimSize(nd-1) {
log.Println("tensor Symmetric gonum Matrix call made on Tensor that is not symmetric")
return 0
}
return tsr.Shp.DimSize(nd - 1)
}
// SetMetaData sets a key=value meta data (stored as a map[string]string).
// For TensorGrid display: top-zero=+/-, odd-row=+/-, image=+/-,
// min, max set fixed min / max values, background=color
func (tsr *Base[T]) SetMetaData(key, val string) {
if tsr.Meta == nil {
tsr.Meta = make(map[string]string)
}
tsr.Meta[key] = val
}
// MetaData retrieves value of given key, bool = false if not set
func (tsr *Base[T]) MetaData(key string) (string, bool) {
if tsr.Meta == nil {
return "", false
}
val, ok := tsr.Meta[key]
return val, ok
}
// MetaDataMap returns the underlying map used for meta data
func (tsr *Base[T]) MetaDataMap() map[string]string {
return tsr.Meta
}
// CopyMetaData copies meta data from given source tensor
func (tsr *Base[T]) CopyMetaData(frm Tensor) {
fmap := frm.MetaDataMap()
if len(fmap) == 0 {
return
}
if tsr.Meta == nil {
tsr.Meta = make(map[string]string)
}
for k, v := range fmap {
tsr.Meta[k] = v
}
}
// Copyright (c) 2024, 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 tensor
import (
"fmt"
"log/slog"
"reflect"
"strings"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/tensor/bitslice"
"gonum.org/v1/gonum/mat"
)
// Bits is a tensor of bits backed by a bitslice.Slice for efficient storage
// of binary data
type Bits struct {
Shp Shape
Values bitslice.Slice
Meta map[string]string
}
// NewBits returns a new n-dimensional tensor of bit values
// with the given sizes per dimension (shape), and optional dimension names.
func NewBits(sizes []int, names ...string) *Bits {
tsr := &Bits{}
tsr.SetShape(sizes, names...)
tsr.Values = bitslice.Make(tsr.Len(), 0)
return tsr
}
// NewBitsShape returns a new n-dimensional tensor of bit values
// using given shape.
func NewBitsShape(shape *Shape) *Bits {
tsr := &Bits{}
tsr.Shp.CopyShape(shape)
tsr.Values = bitslice.Make(tsr.Len(), 0)
return tsr
}
func Float64ToBool(val float64) bool {
bv := true
if val == 0 {
bv = false
}
return bv
}
func BoolToFloat64(bv bool) float64 {
if bv {
return 1
} else {
return 0
}
}
func (tsr *Bits) IsString() bool {
return false
}
// DataType returns the type of the data elements in the tensor.
// Bool is returned for the Bits tensor type.
func (tsr *Bits) DataType() reflect.Kind {
return reflect.Bool
}
func (tsr *Bits) Sizeof() int64 {
return int64(len(tsr.Values))
}
func (tsr *Bits) Bytes() []byte {
return slicesx.ToBytes(tsr.Values)
}
// Shape returns a pointer to the shape that fully parametrizes the tensor shape
func (tsr *Bits) Shape() *Shape { return &tsr.Shp }
// Len returns the number of elements in the tensor (product of shape dimensions).
func (tsr *Bits) Len() int { return tsr.Shp.Len() }
// NumDims returns the total number of dimensions.
func (tsr *Bits) NumDims() int { return tsr.Shp.NumDims() }
// DimSize returns size of given dimension
func (tsr *Bits) DimSize(dim int) int { return tsr.Shp.DimSize(dim) }
// RowCellSize returns the size of the outer-most Row shape dimension,
// and the size of all the remaining inner dimensions (the "cell" size).
// Used for Tensors that are columns in a data table.
func (tsr *Bits) RowCellSize() (rows, cells int) {
return tsr.Shp.RowCellSize()
}
// Value returns value at given tensor index
func (tsr *Bits) Value(i []int) bool { j := int(tsr.Shp.Offset(i)); return tsr.Values.Index(j) }
// Value1D returns value at given tensor 1D (flat) index
func (tsr *Bits) Value1D(i int) bool { return tsr.Values.Index(i) }
func (tsr *Bits) Set(i []int, val bool) { j := int(tsr.Shp.Offset(i)); tsr.Values.Set(j, val) }
func (tsr *Bits) Set1D(i int, val bool) { tsr.Values.Set(i, val) }
// SetShape sets the shape params, resizing backing storage appropriately
func (tsr *Bits) SetShape(sizes []int, names ...string) {
tsr.Shp.SetShape(sizes, names...)
nln := tsr.Len()
tsr.Values.SetLen(nln)
}
// SetNumRows sets the number of rows (outer-most dimension) in a RowMajor organized tensor.
func (tsr *Bits) SetNumRows(rows int) {
rows = max(1, rows) // must be > 0
_, cells := tsr.Shp.RowCellSize()
nln := rows * cells
tsr.Shp.Sizes[0] = rows
tsr.Values.SetLen(nln)
}
// SubSpace is not possible with Bits
func (tsr *Bits) SubSpace(offs []int) Tensor {
return nil
}
func (tsr *Bits) Float(i []int) float64 {
j := tsr.Shp.Offset(i)
return BoolToFloat64(tsr.Values.Index(j))
}
func (tsr *Bits) SetFloat(i []int, val float64) {
j := tsr.Shp.Offset(i)
tsr.Values.Set(j, Float64ToBool(val))
}
func (tsr *Bits) StringValue(i []int) string {
j := tsr.Shp.Offset(i)
return reflectx.ToString(tsr.Values.Index(j))
}
func (tsr *Bits) SetString(i []int, val string) {
if bv, err := reflectx.ToBool(val); err == nil {
j := tsr.Shp.Offset(i)
tsr.Values.Set(j, bv)
}
}
func (tsr *Bits) Float1D(off int) float64 {
return BoolToFloat64(tsr.Values.Index(off))
}
func (tsr *Bits) SetFloat1D(off int, val float64) {
tsr.Values.Set(off, Float64ToBool(val))
}
func (tsr *Bits) FloatRowCell(row, cell int) float64 {
_, sz := tsr.RowCellSize()
return BoolToFloat64(tsr.Values.Index(row*sz + cell))
}
func (tsr *Bits) SetFloatRowCell(row, cell int, val float64) {
_, sz := tsr.RowCellSize()
tsr.Values.Set(row*sz+cell, Float64ToBool(val))
}
func (tsr *Bits) Floats(flt *[]float64) {
sz := tsr.Len()
*flt = slicesx.SetLength(*flt, sz)
for j := 0; j < sz; j++ {
(*flt)[j] = BoolToFloat64(tsr.Values.Index(j))
}
}
// SetFloats sets tensor values from a []float64 slice (copies values).
func (tsr *Bits) SetFloats(vals []float64) {
sz := min(tsr.Len(), len(vals))
for j := 0; j < sz; j++ {
tsr.Values.Set(j, Float64ToBool(vals[j]))
}
}
func (tsr *Bits) String1D(off int) string {
return reflectx.ToString(tsr.Values.Index(off))
}
func (tsr *Bits) SetString1D(off int, val string) {
if bv, err := reflectx.ToBool(val); err == nil {
tsr.Values.Set(off, bv)
}
}
func (tsr *Bits) StringRowCell(row, cell int) string {
_, sz := tsr.RowCellSize()
return reflectx.ToString(tsr.Values.Index(row*sz + cell))
}
func (tsr *Bits) SetStringRowCell(row, cell int, val string) {
if bv, err := reflectx.ToBool(val); err == nil {
_, sz := tsr.RowCellSize()
tsr.Values.Set(row*sz+cell, bv)
}
}
// Label satisfies the core.Labeler interface for a summary description of the tensor
func (tsr *Bits) Label() string {
return fmt.Sprintf("tensor.Bits: %s", tsr.Shp.String())
}
// SetMetaData sets a key=value meta data (stored as a map[string]string).
// For TensorGrid display: top-zero=+/-, odd-row=+/-, image=+/-,
// min, max set fixed min / max values, background=color
func (tsr *Bits) SetMetaData(key, val string) {
if tsr.Meta == nil {
tsr.Meta = make(map[string]string)
}
tsr.Meta[key] = val
}
// MetaData retrieves value of given key, bool = false if not set
func (tsr *Bits) MetaData(key string) (string, bool) {
if tsr.Meta == nil {
return "", false
}
val, ok := tsr.Meta[key]
return val, ok
}
// MetaDataMap returns the underlying map used for meta data
func (tsr *Bits) MetaDataMap() map[string]string {
return tsr.Meta
}
// CopyMetaData copies meta data from given source tensor
func (tsr *Bits) CopyMetaData(frm Tensor) {
fmap := frm.MetaDataMap()
if len(fmap) == 0 {
return
}
if tsr.Meta == nil {
tsr.Meta = make(map[string]string)
}
for k, v := range fmap {
tsr.Meta[k] = v
}
}
// Range is not applicable to Bits tensor
func (tsr *Bits) Range() (min, max float64, minIndex, maxIndex int) {
minIndex = -1
maxIndex = -1
return
}
// SetZeros is simple convenience function initialize all values to 0
func (tsr *Bits) SetZeros() {
ln := tsr.Len()
for j := 0; j < ln; j++ {
tsr.Values.Set(j, false)
}
}
// Clone clones this tensor, creating a duplicate copy of itself with its
// own separate memory representation of all the values, and returns
// that as a Tensor (which can be converted into the known type as needed).
func (tsr *Bits) Clone() Tensor {
csr := NewBitsShape(&tsr.Shp)
csr.Values = tsr.Values.Clone()
return csr
}
// CopyFrom copies all avail values from other tensor into this tensor, with an
// optimized implementation if the other tensor is of the same type, and
// otherwise it goes through appropriate standard type.
func (tsr *Bits) CopyFrom(frm Tensor) {
if fsm, ok := frm.(*Bits); ok {
copy(tsr.Values, fsm.Values)
return
}
sz := min(len(tsr.Values), frm.Len())
for i := 0; i < sz; i++ {
tsr.Values.Set(i, Float64ToBool(frm.Float1D(i)))
}
}
// CopyShapeFrom copies just the shape from given source tensor
// calling SetShape with the shape params from source (see for more docs).
func (tsr *Bits) CopyShapeFrom(frm Tensor) {
tsr.SetShape(frm.Shape().Sizes, frm.Shape().Names...)
}
// CopyCellsFrom copies given range of values from other tensor into this tensor,
// using flat 1D indexes: to = starting index in this Tensor to start copying into,
// start = starting index on from Tensor to start copying from, and n = number of
// values to copy. Uses an optimized implementation if the other tensor is
// of the same type, and otherwise it goes through appropriate standard type.
func (tsr *Bits) CopyCellsFrom(frm Tensor, to, start, n int) {
if fsm, ok := frm.(*Bits); ok {
for i := 0; i < n; i++ {
tsr.Values.Set(to+i, fsm.Values.Index(start+i))
}
return
}
for i := 0; i < n; i++ {
tsr.Values.Set(to+i, Float64ToBool(frm.Float1D(start+i)))
}
}
// Dims is the gonum/mat.Matrix interface method for returning the dimensionality of the
// 2D Matrix. Not supported for Bits -- do not call!
func (tsr *Bits) Dims() (r, c int) {
slog.Error("tensor Dims gonum Matrix call made on Bits Tensor; not supported")
return 0, 0
}
// At is the gonum/mat.Matrix interface method for returning 2D matrix element at given
// row, column index. Not supported for Bits -- do not call!
func (tsr *Bits) At(i, j int) float64 {
slog.Error("tensor At gonum Matrix call made on Bits Tensor; not supported")
return 0
}
// T is the gonum/mat.Matrix transpose method.
// Not supported for Bits -- do not call!
func (tsr *Bits) T() mat.Matrix {
slog.Error("tensor T gonum Matrix call made on Bits Tensor; not supported")
return mat.Transpose{tsr}
}
// String satisfies the fmt.Stringer interface for string of tensor data
func (tsr *Bits) String() string {
str := tsr.Label()
sz := tsr.Len()
if sz > 1000 {
return str
}
var b strings.Builder
b.WriteString(str)
b.WriteString("\n")
oddRow := true
rows, cols, _, _ := Projection2DShape(&tsr.Shp, oddRow)
for r := 0; r < rows; r++ {
rc, _ := Projection2DCoords(&tsr.Shp, oddRow, r, 0)
b.WriteString(fmt.Sprintf("%v: ", rc))
for c := 0; c < cols; c++ {
vl := Projection2DValue(tsr, oddRow, r, c)
b.WriteString(fmt.Sprintf("%g ", vl))
}
b.WriteString("\n")
}
return b.String()
}
// Copyright (c) 2024, The Cogent Core Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package bitslice implements a simple slice-of-bits using a []byte slice for storage,
// which is used for efficient storage of boolean data, such as projection connectivity patterns.
package bitslice
import "fmt"
// bitslice.Slice is the slice of []byte that holds the bits.
// first byte maintains the number of bits used in the last byte (0-7).
// when 0 then prior byte is all full and a new one must be added for append.
type Slice []byte
// BitIndex returns the byte, bit index of given bit index
func BitIndex(idx int) (byte int, bit uint32) {
return idx / 8, uint32(idx % 8)
}
// Make makes a new bitslice of given length and capacity (optional, pass 0 for default)
// *bits* (rounds up 1 for both).
// also reserves first byte for extra bits value
func Make(ln, cp int) Slice {
by, bi := BitIndex(ln)
bln := by
if bi != 0 {
bln++
}
var sl Slice
if cp > 0 {
sl = make(Slice, bln+1, (cp/8)+2)
} else {
sl = make(Slice, bln+1)
}
sl[0] = byte(bi)
return sl
}
// Len returns the length of the slice in bits
func (bs *Slice) Len() int {
ln := len(*bs)
if ln == 0 {
return 0
}
eb := (*bs)[0]
bln := ln - 1
if eb != 0 {
bln--
}
tln := bln*8 + int(eb)
return tln
}
// Cap returns the capacity of the slice in bits -- always modulo 8
func (bs *Slice) Cap() int {
return (cap(*bs) - 1) * 8
}
// SetLen sets the length of the slice, copying values if a new allocation is required
func (bs *Slice) SetLen(ln int) {
by, bi := BitIndex(ln)
bln := by
if bi != 0 {
bln++
}
if cap(*bs) >= bln+1 {
*bs = (*bs)[0 : bln+1]
(*bs)[0] = byte(bi)
} else {
sl := make(Slice, bln+1)
sl[0] = byte(bi)
copy(sl, *bs)
*bs = sl
}
}
// Set sets value of given bit index -- no extra range checking is performed -- will panic if out of range
func (bs *Slice) Set(idx int, val bool) {
by, bi := BitIndex(idx)
if val {
(*bs)[by+1] |= 1 << bi
} else {
(*bs)[by+1] &^= 1 << bi
}
}
// Index returns bit value at given bit index
func (bs *Slice) Index(idx int) bool {
by, bi := BitIndex(idx)
return ((*bs)[by+1] & (1 << bi)) != 0
}
// Append adds a bit to the slice and returns possibly new slice, possibly old slice..
func (bs *Slice) Append(val bool) Slice {
if len(*bs) == 0 {
*bs = Make(1, 0)
bs.Set(0, val)
return *bs
}
ln := bs.Len()
eb := (*bs)[0]
if eb == 0 {
*bs = append(*bs, 0) // now we add
(*bs)[0] = 1
} else if eb < 7 {
(*bs)[0]++
} else {
(*bs)[0] = 0
}
bs.Set(ln, val)
return *bs
}
// SetAll sets all values to either on or off -- much faster than setting individual bits
func (bs *Slice) SetAll(val bool) {
ln := len(*bs)
for i := 1; i < ln; i++ {
if val {
(*bs)[i] = 0xFF
} else {
(*bs)[i] = 0
}
}
}
// ToBools converts to a []bool slice
func (bs *Slice) ToBools() []bool {
ln := len(*bs)
bb := make([]bool, ln)
for i := 0; i < ln; i++ {
bb[i] = bs.Index(i)
}
return bb
}
// Clone creates a new copy of this bitslice with separate memory
func (bs *Slice) Clone() Slice {
cp := make(Slice, len(*bs))
copy(cp, *bs)
return cp
}
// SubSlice returns a new Slice from given start, end range indexes of this slice
// if end is <= 0 then the length of the source slice is used (equivalent to omitting
// the number after the : in a Go subslice expression)
func (bs *Slice) SubSlice(start, end int) Slice {
ln := bs.Len()
if end <= 0 {
end = ln
}
if end > ln {
panic("bitslice.SubSlice: end index is beyond length of slice")
}
if start > end {
panic("bitslice.SubSlice: start index greater than end index")
}
nln := end - start
if nln <= 0 {
return Slice{}
}
ss := Make(nln, 0)
for i := 0; i < nln; i++ {
ss.Set(i, bs.Index(i+start))
}
return ss
}
// Delete returns a new bit slice with N elements removed starting at given index.
// This must be a copy given the nature of the 8-bit aliasing.
func (bs *Slice) Delete(start, n int) Slice {
ln := bs.Len()
if n <= 0 {
panic("bitslice.Delete: n <= 0")
}
if start >= ln {
panic("bitslice.Delete: start index >= length")
}
end := start + n
if end > ln {
panic("bitslice.Delete: end index greater than length")
}
nln := ln - n
if nln <= 0 {
return Slice{}
}
ss := Make(nln, 0)
for i := 0; i < start; i++ {
ss.Set(i, bs.Index(i))
}
for i := end; i < ln; i++ {
ss.Set(i-n, bs.Index(i))
}
return ss
}
// Insert returns a new bit slice with N false elements inserted starting at given index.
// This must be a copy given the nature of the 8-bit aliasing.
func (bs *Slice) Insert(start, n int) Slice {
ln := bs.Len()
if n <= 0 {
panic("bitslice.Insert: n <= 0")
}
if start > ln {
panic("bitslice.Insert: start index greater than length")
}
nln := ln + n
ss := Make(nln, 0)
for i := 0; i < start; i++ {
ss.Set(i, bs.Index(i))
}
for i := start; i < ln; i++ {
ss.Set(i+n, bs.Index(i))
}
return ss
}
// String satisfies the fmt.Stringer interface
func (bs *Slice) String() string {
ln := bs.Len()
if ln == 0 {
if *bs == nil {
return "nil"
}
return "[]"
}
mx := ln
if mx > 1000 {
mx = 1000
}
str := "["
for i := 0; i < mx; i++ {
val := bs.Index(i)
if val {
str += "1 "
} else {
str += "0 "
}
if (i+1)%80 == 0 {
str += "\n"
}
}
if ln > mx {
str += fmt.Sprintf("...(len=%v)", ln)
}
str += "]"
return str
}
// Copyright (c) 2024, 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 main
import (
"bufio"
"flag"
"fmt"
"os"
"sort"
"strconv"
"cogentcore.org/core/core"
"cogentcore.org/core/tensor/stats/split"
"cogentcore.org/core/tensor/stats/stats"
"cogentcore.org/core/tensor/table"
)
var (
Output string
Col string
OutFile *os.File
OutWriter *bufio.Writer
LF = []byte("\n")
Delete bool
LogPrec = 4
)
func main() {
var help bool
var avg bool
var colavg bool
flag.BoolVar(&help, "help", false, "if true, report usage info")
flag.BoolVar(&avg, "avg", false, "if true, files must have same cols (ideally rows too, though not necessary), outputs average of any float-type columns across files")
flag.BoolVar(&colavg, "colavg", false, "if true, outputs average of any float-type columns aggregated by column")
flag.StringVar(&Col, "col", "", "name of column for colavg")
flag.StringVar(&Output, "output", "", "name of output file -- stdout if not specified")
flag.StringVar(&Output, "o", "", "name of output file -- stdout if not specified")
flag.BoolVar(&Delete, "delete", false, "if true, delete the source files after cat -- careful!")
flag.BoolVar(&Delete, "d", false, "if true, delete the source files after cat -- careful!")
flag.IntVar(&LogPrec, "prec", 4, "precision for number output -- defaults to 4")
flag.Parse()
files := flag.Args()
sort.StringSlice(files).Sort()
if Output != "" {
OutFile, err := os.Create(Output)
if err != nil {
fmt.Println("Error creating output file:", err)
os.Exit(1)
}
defer OutFile.Close()
OutWriter = bufio.NewWriter(OutFile)
} else {
OutWriter = bufio.NewWriter(os.Stdout)
}
switch {
case help || len(files) == 0:
fmt.Printf("\netcat is a data table concatenation utility\n\tassumes all files have header lines, and only retains the header for the first file\n\t(otherwise just use regular cat)\n")
flag.PrintDefaults()
case colavg:
AvgByColumn(files, Col)
case avg:
AvgCat(files)
default:
RawCat(files)
}
OutWriter.Flush()
}
// RawCat concatenates all data in one big file
func RawCat(files []string) {
for fi, fn := range files {
fp, err := os.Open(fn)
if err != nil {
fmt.Println("Error opening file: ", err)
continue
}
scan := bufio.NewScanner(fp)
li := 0
for {
if !scan.Scan() {
break
}
ln := scan.Bytes()
if li == 0 {
if fi == 0 {
OutWriter.Write(ln)
OutWriter.Write(LF)
}
} else {
OutWriter.Write(ln)
OutWriter.Write(LF)
}
li++
}
fp.Close()
if Delete {
os.Remove(fn)
}
}
}
// AvgCat computes average across all runs
func AvgCat(files []string) {
dts := make([]*table.Table, 0, len(files))
for _, fn := range files {
dt := &table.Table{}
err := dt.OpenCSV(core.Filename(fn), table.Tab)
if err != nil {
fmt.Println("Error opening file: ", err)
continue
}
if dt.Rows == 0 {
fmt.Printf("File %v empty\n", fn)
continue
}
dts = append(dts, dt)
}
if len(dts) == 0 {
fmt.Println("No files or files are empty, exiting")
return
}
avgdt := stats.MeanTables(dts)
avgdt.SetMetaData("precision", strconv.Itoa(LogPrec))
avgdt.SaveCSV(core.Filename(Output), table.Tab, table.Headers)
}
// AvgByColumn computes average by given column for given files
// If column is empty, averages across all rows.
func AvgByColumn(files []string, column string) {
for _, fn := range files {
dt := table.NewTable()
err := dt.OpenCSV(core.Filename(fn), table.Tab)
if err != nil {
fmt.Println("Error opening file: ", err)
continue
}
if dt.Rows == 0 {
fmt.Printf("File %v empty\n", fn)
continue
}
ix := table.NewIndexView(dt)
var spl *table.Splits
if column == "" {
spl = split.All(ix)
} else {
spl = split.GroupBy(ix, column)
}
for ci, cl := range dt.Columns {
if cl.IsString() || dt.ColumnNames[ci] == column {
continue
}
split.AggIndex(spl, ci, stats.Mean)
}
avgdt := spl.AggsToTable(table.ColumnNameOnly)
avgdt.SetMetaData("precision", strconv.Itoa(LogPrec))
avgdt.SaveCSV(core.Filename(Output), table.Tab, table.Headers)
}
}
// Copyright (c) 2020, 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 main
import (
"bufio"
"os"
"strings"
"time"
)
// File represents one opened file -- all data is read in and maintained here
type File struct {
// file name (either in same dir or include path)
FName string `desc:"file name (either in same dir or include path)"`
// mod time of file when last read
ModTime time.Time `desc:"mod time of file when last read"`
// delim is commas, not tabs
Commas bool `desc:"delim is commas, not tabs"`
// rows of data == len(Data)
Rows int `desc:"rows of data == len(Data)"`
// width of each column: resized to fit widest element
Widths []int `desc:"width of each column: resized to fit widest element"`
// headers
Heads []string `desc:"headers"`
// data -- rows 1..end
Data [][]string `desc:"data -- rows 1..end"`
}
// Files is a slice of open files
type Files []*File
// TheFiles are the set of open files
var TheFiles Files
// Open opens file, reads it
func (fl *File) Open(fname string) error {
fl.FName = fname
return fl.Read()
}
// CheckUpdate checks if file has been modified -- returns true if so
func (fl *File) CheckUpdate() bool {
st, err := os.Stat(fl.FName)
if err != nil {
return false
}
return st.ModTime().After(fl.ModTime)
}
// Read reads data from file
func (fl *File) Read() error {
st, err := os.Stat(fl.FName)
if err != nil {
return err
}
fl.ModTime = st.ModTime()
f, err := os.Open(fl.FName)
if err != nil {
return err
}
defer f.Close()
if fl.Data != nil {
fl.Data = fl.Data[:0]
}
scan := bufio.NewScanner(f)
ln := 0
for scan.Scan() {
s := string(scan.Bytes())
var fd []string
if fl.Commas {
fd = strings.Split(s, ",")
} else {
fd = strings.Split(s, "\t")
}
if ln == 0 {
if len(fd) == 0 || strings.Count(s, ",") > strings.Count(s, "\t") {
fl.Commas = true
fd = strings.Split(s, ",")
}
fl.Heads = fd
fl.Widths = make([]int, len(fl.Heads))
fl.FitWidths(fd)
ln++
continue
}
fl.Data = append(fl.Data, fd)
fl.FitWidths(fd)
ln++
}
fl.Rows = ln - 1 // skip header
return err
}
// FitWidths expands widths given current set of fields
func (fl *File) FitWidths(fd []string) {
nw := len(fl.Widths)
for i, f := range fd {
if i >= nw {
break
}
w := max(fl.Widths[i], len(f))
fl.Widths[i] = w
}
}
/////////////////////////////////////////////////////////////////
// Files
// Open opens all files
func (fl *Files) Open(fnms []string) {
for _, fn := range fnms {
f := &File{}
err := f.Open(fn)
if err == nil {
*fl = append(*fl, f)
}
}
}
// CheckUpdates check for any updated files, re-read if so -- returns true if so
func (fl *Files) CheckUpdates() bool {
got := false
for _, f := range *fl {
if f.CheckUpdate() {
f.Read()
got = true
}
}
return got
}
// Copyright (c) 2020, 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 main
import "github.com/nsf/termbox-go"
func (tm *Term) Help() {
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
ln := 0
tm.DrawStringDef(0, ln, "Key(s) Function")
ln++
tm.DrawStringDef(0, ln, "--------------------------------------------------------------")
ln++
tm.DrawStringDef(0, ln, "spc,n page down")
ln++
tm.DrawStringDef(0, ln, "p page up")
ln++
tm.DrawStringDef(0, ln, "f scroll right-hand panel to the right")
ln++
tm.DrawStringDef(0, ln, "b scroll right-hand panel to the left")
ln++
tm.DrawStringDef(0, ln, "w widen the left-hand panel of columns")
ln++
tm.DrawStringDef(0, ln, "s shrink the left-hand panel of columns")
ln++
tm.DrawStringDef(0, ln, "t toggle tail-mode (auto updating as file grows) on/off")
ln++
tm.DrawStringDef(0, ln, "a jump to top")
ln++
tm.DrawStringDef(0, ln, "e jump to end")
ln++
tm.DrawStringDef(0, ln, "v rotate down through the list of files (if not all displayed)")
ln++
tm.DrawStringDef(0, ln, "u rotate up through the list of files (if not all displayed)")
ln++
tm.DrawStringDef(0, ln, "m more minimum lines per file -- increase amount shown of each file")
ln++
tm.DrawStringDef(0, ln, "l less minimum lines per file -- decrease amount shown of each file")
ln++
tm.DrawStringDef(0, ln, "d toggle display of file names")
ln++
tm.DrawStringDef(0, ln, "c toggle display of column numbers instead of names")
ln++
tm.DrawStringDef(0, ln, "q quit")
ln++
termbox.Flush()
}
// Copyright (c) 2020, 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 main
import (
"fmt"
"image"
"sync"
termbox "github.com/nsf/termbox-go"
)
// Term represents the terminal display -- has all drawing routines
// and all display data. See Tail for two diff display modes.
type Term struct {
// size of terminal
Size image.Point `desc:"size of terminal"`
// number of fixed (non-scrolling) columns on left
FixCols int `desc:"number of fixed (non-scrolling) columns on left"`
// starting column index -- relative to FixCols
ColSt int `desc:"starting column index -- relative to FixCols"`
// starting row index -- for !Tail mode
RowSt int `desc:"starting row index -- for !Tail mode"`
// row from end -- for Tail mode
RowFromEnd int `desc:"row from end (relative to RowsPer) -- for Tail mode"`
// starting index into files (if too many to display)
FileSt int `desc:"starting index into files (if too many to display)"`
// number of files to display (if too many to display)
NFiles int `desc:"number of files to display (if too many to display)"`
// minimum number of lines per file
MinLines int `desc:"minimum number of lines per file"`
// maximum column width (1/4 of term width)
MaxWd int `desc:"maximum column width (1/4 of term width)"`
// max number of rows across all files
MaxRows int `desc:"max number of rows across all files"`
// number of Y rows per file total: Size.Y / len(TheFiles)
YPer int `desc:"number of Y rows per file total: Size.Y / len(TheFiles)"`
// rows of data per file (subtracting header, filename)
RowsPer int `desc:"rows of data per file (subtracting header, filename)"`
// if true, print filename
ShowFName bool `desc:"if true, print filename"`
// if true, display is synchronized by the last row for each file, and otherwise it is synchronized by the starting row. Tail also checks for file updates
Tail bool `desc:"if true, display is synchronized by the last row for each file, and otherwise it is synchronized by the starting row. Tail also checks for file updates"`
// display column numbers instead of names
ColNums bool `desc:"display column numbers instead of names"`
// draw mutex
Mu sync.Mutex `desc:"draw mutex"`
}
// TheTerm is the terminal instance
var TheTerm Term
// Draw draws the current terminal display
func (tm *Term) Draw() error {
tm.Mu.Lock()
defer tm.Mu.Unlock()
err := termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
if err != nil {
return err
}
w, h := termbox.Size()
tm.Size.X = w
tm.Size.Y = h
tm.MaxWd = tm.Size.X / 4
if tm.MinLines == 0 {
tm.MinLines = min(5, tm.Size.Y-1)
}
nf := len(TheFiles)
if nf == 0 {
return fmt.Errorf("No files")
}
ysz := tm.Size.Y - 1 // status line
tm.YPer = ysz / nf
tm.NFiles = nf
if tm.YPer < tm.MinLines {
tm.NFiles = ysz / tm.MinLines
tm.YPer = tm.MinLines
}
if tm.NFiles+tm.FileSt > nf {
tm.FileSt = max(0, nf-tm.NFiles)
}
tm.RowsPer = tm.YPer - 1
if tm.ShowFName {
tm.RowsPer--
}
sty := 0
mxrows := 0
for fi := 0; fi < tm.NFiles; fi++ {
ffi := tm.FileSt + fi
if ffi >= nf {
break
}
fl := TheFiles[ffi]
tm.DrawFile(fl, sty)
sty += tm.YPer
mxrows = max(mxrows, fl.Rows)
}
tm.MaxRows = mxrows
tm.StatusLine()
termbox.Flush()
return nil
}
// StatusLine renders the status line at bottom
func (tm *Term) StatusLine() {
pos := tm.RowSt
if tm.Tail {
pos = tm.RowFromEnd
}
stat := fmt.Sprintf("Tail: %v\tPos: %d\tMaxRows: %d\tNFile: %d\tFileSt: %d\t h = help [spc,n,p,r,f,l,b,w,s,t,a,e,v,u,m,l,c,q] ", tm.Tail, pos, tm.MaxRows, len(TheFiles), tm.FileSt)
tm.DrawString(0, tm.Size.Y-1, stat, len(stat), termbox.AttrReverse, termbox.AttrReverse)
}
// NextPage moves down a page
func (tm *Term) NextPage() error {
if tm.Tail {
mn := min(-(tm.MaxRows - tm.RowsPer), 0)
tm.RowFromEnd = min(tm.RowFromEnd+tm.RowsPer, 0)
tm.RowFromEnd = max(tm.RowFromEnd, mn)
} else {
tm.RowSt = min(tm.RowSt+tm.RowsPer, tm.MaxRows-tm.RowsPer)
tm.RowSt = max(tm.RowSt, 0)
}
return tm.Draw()
}
// PrevPage moves up a page
func (tm *Term) PrevPage() error {
if tm.Tail {
mn := min(-(tm.MaxRows - tm.RowsPer), 0)
tm.RowFromEnd = min(tm.RowFromEnd-tm.RowsPer, 0)
tm.RowFromEnd = max(tm.RowFromEnd, mn)
} else {
tm.RowSt = max(tm.RowSt-tm.RowsPer, 0)
tm.RowSt = min(tm.RowSt, tm.MaxRows-tm.RowsPer)
}
return tm.Draw()
}
// NextLine moves down a page
func (tm *Term) NextLine() error {
if tm.Tail {
mn := min(-(tm.MaxRows - tm.RowsPer), 0)
tm.RowFromEnd = min(tm.RowFromEnd+1, 0)
tm.RowFromEnd = max(tm.RowFromEnd, mn)
} else {
tm.RowSt = min(tm.RowSt+1, tm.MaxRows-tm.RowsPer)
tm.RowSt = max(tm.RowSt, 0)
}
return tm.Draw()
}
// PrevLine moves up a page
func (tm *Term) PrevLine() error {
if tm.Tail {
mn := min(-(tm.MaxRows - tm.RowsPer), 0)
tm.RowFromEnd = min(tm.RowFromEnd-1, 0)
tm.RowFromEnd = max(tm.RowFromEnd, mn)
} else {
tm.RowSt = max(tm.RowSt-1, 0)
tm.RowSt = min(tm.RowSt, tm.MaxRows-tm.RowsPer)
}
return tm.Draw()
}
// Top moves to starting row = 0
func (tm *Term) Top() error {
mn := min(-(tm.MaxRows - tm.RowsPer), 0)
tm.RowFromEnd = mn
tm.RowSt = 0
return tm.Draw()
}
// End moves row start to last position in longest file
func (tm *Term) End() error {
mx := max(tm.MaxRows-tm.RowsPer, 0)
tm.RowFromEnd = 0
tm.RowSt = mx
return tm.Draw()
}
// ScrollRight scrolls columns to right
func (tm *Term) ScrollRight() error {
tm.ColSt++ // no obvious max
return tm.Draw()
}
// ScrollLeft scrolls columns to left
func (tm *Term) ScrollLeft() error {
tm.ColSt = max(tm.ColSt-1, 0)
return tm.Draw()
}
// FixRight increases number of fixed columns
func (tm *Term) FixRight() error {
tm.FixCols++ // no obvious max
return tm.Draw()
}
// FixLeft decreases number of fixed columns
func (tm *Term) FixLeft() error {
tm.FixCols = max(tm.FixCols-1, 0)
return tm.Draw()
}
// FilesNext moves down in list of files to display
func (tm *Term) FilesNext() error {
nf := len(TheFiles)
tm.FileSt = min(tm.FileSt+1, nf-tm.NFiles)
tm.FileSt = max(tm.FileSt, 0)
return tm.Draw()
}
// FilesPrev moves up in list of files to display
func (tm *Term) FilesPrev() error {
nf := len(TheFiles)
tm.FileSt = max(tm.FileSt-1, 0)
tm.FileSt = min(tm.FileSt, nf-tm.NFiles)
return tm.Draw()
}
// MoreMinLines increases minimum number of lines per file
func (tm *Term) MoreMinLines() error {
tm.MinLines++
return tm.Draw()
}
// LessMinLines decreases minimum number of lines per file
func (tm *Term) LessMinLines() error {
tm.MinLines--
tm.MinLines = max(3, tm.MinLines)
return tm.Draw()
}
// ToggleNames toggles whether file names are shown
func (tm *Term) ToggleNames() error {
tm.ShowFName = !tm.ShowFName
return tm.Draw()
}
// ToggleTail toggles Tail mode
func (tm *Term) ToggleTail() error {
tm.Tail = !tm.Tail
return tm.Draw()
}
// ToggleColNums toggles ColNums mode
func (tm *Term) ToggleColNums() error {
tm.ColNums = !tm.ColNums
return tm.Draw()
}
// TailCheck does tail update check -- returns true if updated
func (tm *Term) TailCheck() bool {
if !tm.Tail {
return false
}
tm.Mu.Lock()
update := TheFiles.CheckUpdates()
tm.Mu.Unlock()
if !update {
return false
}
tm.Draw()
return true
}
// DrawFile draws one file, starting at given y offset
func (tm *Term) DrawFile(fl *File, sty int) {
tdo := (fl.Rows - tm.RowsPer) + tm.RowFromEnd // tail data offset for this file
tdo = max(0, tdo)
rst := min(tm.RowSt, fl.Rows-tm.RowsPer)
rst = max(0, rst)
stx := 0
for ci, hs := range fl.Heads {
if !(ci < tm.FixCols || ci >= tm.FixCols+tm.ColSt) {
continue
}
my := sty
if tm.ShowFName {
tm.DrawString(0, my, fl.FName, tm.Size.X, termbox.AttrReverse, termbox.AttrReverse)
my++
}
wmax := min(fl.Widths[ci], tm.MaxWd)
if tm.ColNums {
hs = fmt.Sprintf("%d", ci)
}
tm.DrawString(stx, my, hs, wmax, termbox.AttrReverse, termbox.AttrReverse)
if ci == tm.FixCols-1 {
tm.DrawString(stx+wmax+1, my, "|", 1, termbox.AttrReverse, termbox.AttrReverse)
}
my++
for ri := 0; ri < tm.RowsPer; ri++ {
var di int
if tm.Tail {
di = tdo + ri
} else {
di = rst + ri
}
if di >= len(fl.Data) || di < 0 {
continue
}
dr := fl.Data[di]
if ci >= len(dr) {
break
}
ds := dr[ci]
tm.DrawString(stx, my+ri, ds, wmax, termbox.ColorDefault, termbox.ColorDefault)
if ci == tm.FixCols-1 {
tm.DrawString(stx+wmax+1, my+ri, "|", 1, termbox.AttrReverse, termbox.AttrReverse)
}
}
stx += wmax + 1
if ci == tm.FixCols-1 {
stx += 2
}
if stx >= tm.Size.X {
break
}
}
}
// DrawStringDef draws string at given position, using default colors
func (tm *Term) DrawStringDef(x, y int, s string) {
tm.DrawString(x, y, s, tm.Size.X, termbox.ColorDefault, termbox.ColorDefault)
}
// DrawString draws string at given position, using given attributes
func (tm *Term) DrawString(x, y int, s string, maxlen int, fg, bg termbox.Attribute) {
if y >= tm.Size.Y || y < 0 {
return
}
for i, r := range s {
if i >= maxlen {
break
}
xp := x + i
if xp >= tm.Size.X || xp < 0 {
continue
}
termbox.SetCell(xp, y, r, fg, bg)
}
}
// Copyright (c) 2020, 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 main
import (
"fmt"
"log"
"os"
"time"
"github.com/nsf/termbox-go"
)
func main() {
err := termbox.Init()
if err != nil {
log.Println(err)
panic(err)
}
defer termbox.Close()
TheFiles.Open(os.Args[1:])
nf := len(TheFiles)
if nf == 0 {
fmt.Printf("usage: etail <filename>... (space separated)\n")
return
}
if nf > 1 {
TheTerm.ShowFName = true
}
err = TheTerm.ToggleTail() // start in tail mode
if err != nil {
log.Println(err)
panic(err)
}
Tailer := time.NewTicker(time.Duration(500) * time.Millisecond)
go func() {
for {
<-Tailer.C
TheTerm.TailCheck()
}
}()
loop:
for {
switch ev := termbox.PollEvent(); ev.Type {
case termbox.EventKey:
switch {
case ev.Key == termbox.KeyEsc || ev.Ch == 'Q' || ev.Ch == 'q':
break loop
case ev.Ch == ' ' || ev.Ch == 'n' || ev.Ch == 'N' || ev.Key == termbox.KeyPgdn || ev.Key == termbox.KeySpace:
TheTerm.NextPage()
case ev.Ch == 'p' || ev.Ch == 'P' || ev.Key == termbox.KeyPgup:
TheTerm.PrevPage()
case ev.Key == termbox.KeyArrowDown:
TheTerm.NextLine()
case ev.Key == termbox.KeyArrowUp:
TheTerm.PrevLine()
case ev.Ch == 'f' || ev.Ch == 'F' || ev.Key == termbox.KeyArrowRight:
TheTerm.ScrollRight()
case ev.Ch == 'b' || ev.Ch == 'B' || ev.Key == termbox.KeyArrowLeft:
TheTerm.ScrollLeft()
case ev.Ch == 'a' || ev.Ch == 'A' || ev.Key == termbox.KeyHome:
TheTerm.Top()
case ev.Ch == 'e' || ev.Ch == 'E' || ev.Key == termbox.KeyEnd:
TheTerm.End()
case ev.Ch == 'w' || ev.Ch == 'W':
TheTerm.FixRight()
case ev.Ch == 's' || ev.Ch == 'S':
TheTerm.FixLeft()
case ev.Ch == 'v' || ev.Ch == 'V':
TheTerm.FilesNext()
case ev.Ch == 'u' || ev.Ch == 'U':
TheTerm.FilesPrev()
case ev.Ch == 'm' || ev.Ch == 'M':
TheTerm.MoreMinLines()
case ev.Ch == 'l' || ev.Ch == 'L':
TheTerm.LessMinLines()
case ev.Ch == 'd' || ev.Ch == 'D':
TheTerm.ToggleNames()
case ev.Ch == 't' || ev.Ch == 'T':
TheTerm.ToggleTail()
case ev.Ch == 'c' || ev.Ch == 'C':
TheTerm.ToggleColNums()
case ev.Ch == 'h' || ev.Ch == 'H':
TheTerm.Help()
}
case termbox.EventResize:
TheTerm.Draw()
}
}
}
// Copyright (c) 2024, 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 databrowser
//go:generate core generate
import (
"io/fs"
"path/filepath"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/filetree"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
// Browser is a data browser, for browsing data either on an os filesystem
// or as a datafs virtual data filesystem.
type Browser struct {
core.Frame
// FS is the filesystem, if browsing an FS
FS fs.FS
// DataRoot is the path to the root of the data to browse
DataRoot string
toolbar *core.Toolbar
splits *core.Splits
files *filetree.Tree
tabs *core.Tabs
}
// Init initializes with the data and script directories
func (br *Browser) Init() {
br.Frame.Init()
br.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
})
br.OnShow(func(e events.Event) {
br.UpdateFiles()
})
tree.AddChildAt(br, "splits", func(w *core.Splits) {
br.splits = w
w.SetSplits(.15, .85)
tree.AddChildAt(w, "fileframe", func(w *core.Frame) {
w.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Overflow.Set(styles.OverflowAuto)
s.Grow.Set(1, 1)
})
tree.AddChildAt(w, "filetree", func(w *filetree.Tree) {
br.files = w
w.FileNodeType = types.For[FileNode]()
// w.OnSelect(func(e events.Event) {
// e.SetHandled()
// sels := w.SelectedViews()
// if sels != nil {
// br.FileNodeSelected(sn)
// }
// })
})
})
tree.AddChildAt(w, "tabs", func(w *core.Tabs) {
br.tabs = w
w.Type = core.FunctionalTabs
})
})
}
// NewBrowserWindow opens a new data Browser for given
// file system (nil for os files) and data directory.
func NewBrowserWindow(fsys fs.FS, dataDir string) *Browser {
b := core.NewBody("Cogent Data Browser: " + fsx.DirAndFile(dataDir))
br := NewBrowser(b)
br.FS = fsys
ddr := dataDir
if fsys == nil {
ddr = errors.Log1(filepath.Abs(dataDir))
}
b.AddTopBar(func(bar *core.Frame) {
tb := core.NewToolbar(bar)
br.toolbar = tb
tb.Maker(br.MakeToolbar)
})
br.SetDataRoot(ddr)
b.RunWindow()
return br
}
// ParentBrowser returns the Browser parent of given node
func ParentBrowser(tn tree.Node) *Browser {
var res *Browser
tn.AsTree().WalkUp(func(n tree.Node) bool {
if c, ok := n.(*Browser); ok {
res = c
return false
}
return true
})
return res
}
// UpdateFiles Updates the files list.
func (br *Browser) UpdateFiles() { //types:add
files := br.files
if br.FS != nil {
files.SortByModTime = true
files.OpenPathFS(br.FS, br.DataRoot)
} else {
files.OpenPath(br.DataRoot)
}
br.Update()
}
func (br *Browser) MakeToolbar(p *tree.Plan) {
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(br.UpdateFiles).SetText("").SetIcon(icons.Refresh).SetShortcut("Command+U")
})
}
// Copyright (c) 2024, 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 databrowser
import (
"cogentcore.org/core/core"
"cogentcore.org/core/plot/plotcore"
"cogentcore.org/core/styles"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/table"
"cogentcore.org/core/tensor/tensorcore"
"cogentcore.org/core/texteditor"
)
// NewTab creates a tab with given label, or returns the existing one
// with given type of widget within it. mkfun function is called to create
// and configure a new widget if not already existing.
func NewTab[T any](br *Browser, label string, mkfun func(tab *core.Frame) T) T {
tab := br.tabs.RecycleTab(label)
if tab.HasChildren() {
return tab.Child(1).(T)
}
w := mkfun(tab)
return w
}
// NewTabTensorTable creates a tab with a tensorcore.Table widget
// to view given table.Table, using its own table.IndexView as tv.Table.
// Use tv.Table.Table to get the underlying *table.Table
// Use tv.Table.Sequential to update the IndexView to view
// all of the rows when done updating the Table, and then call br.Update()
func (br *Browser) NewTabTensorTable(label string, dt *table.Table) *tensorcore.Table {
tv := NewTab[*tensorcore.Table](br, label, func(tab *core.Frame) *tensorcore.Table {
tb := core.NewToolbar(tab)
tv := tensorcore.NewTable(tab)
tb.Maker(tv.MakeToolbar)
return tv
})
tv.SetTable(dt)
br.Update()
return tv
}
// NewTabTensorEditor creates a tab with a tensorcore.TensorEditor widget
// to view given Tensor.
func (br *Browser) NewTabTensorEditor(label string, tsr tensor.Tensor) *tensorcore.TensorEditor {
tv := NewTab[*tensorcore.TensorEditor](br, label, func(tab *core.Frame) *tensorcore.TensorEditor {
tb := core.NewToolbar(tab)
tv := tensorcore.NewTensorEditor(tab)
tb.Maker(tv.MakeToolbar)
return tv
})
tv.SetTensor(tsr)
br.Update()
return tv
}
// NewTabTensorGrid creates a tab with a tensorcore.TensorGrid widget
// to view given Tensor.
func (br *Browser) NewTabTensorGrid(label string, tsr tensor.Tensor) *tensorcore.TensorGrid {
tv := NewTab[*tensorcore.TensorGrid](br, label, func(tab *core.Frame) *tensorcore.TensorGrid {
// tb := core.NewToolbar(tab)
tv := tensorcore.NewTensorGrid(tab)
// tb.Maker(tv.MakeToolbar)
return tv
})
tv.SetTensor(tsr)
br.Update()
return tv
}
// NewTabPlot creates a tab with a Plot of given table.Table.
func (br *Browser) NewTabPlot(label string, dt *table.Table) *plotcore.PlotEditor {
pl := NewTab[*plotcore.PlotEditor](br, label, func(tab *core.Frame) *plotcore.PlotEditor {
return plotcore.NewSubPlot(tab)
})
pl.SetTable(dt)
br.Update()
return pl
}
// NewTabSliceTable creates a tab with a core.Table widget
// to view the given slice of structs.
func (br *Browser) NewTabSliceTable(label string, slc any) *core.Table {
tv := NewTab[*core.Table](br, label, func(tab *core.Frame) *core.Table {
return core.NewTable(tab)
})
tv.SetSlice(slc)
br.Update()
return tv
}
// NewTabEditor opens a texteditor.Editor tab, displaying given string.
func (br *Browser) NewTabEditor(label, content string) *texteditor.Editor {
ed := NewTab[*texteditor.Editor](br, label, func(tab *core.Frame) *texteditor.Editor {
ed := texteditor.NewEditor(tab)
ed.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
})
return ed
})
if content != "" {
ed.Buffer.SetText([]byte(content))
}
br.Update()
return ed
}
// NewTabEditorFile opens an editor tab for given file
func (br *Browser) NewTabEditorFile(label, filename string) *texteditor.Editor {
ed := NewTab[*texteditor.Editor](br, label, func(tab *core.Frame) *texteditor.Editor {
ed := texteditor.NewEditor(tab)
ed.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
})
return ed
})
ed.Buffer.Open(core.Filename(filename))
br.Update()
return ed
}
// Copyright (c) 2024, 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 databrowser
import (
"image"
"log"
"reflect"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/filetree"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/tensor/datafs"
"cogentcore.org/core/tensor/table"
"cogentcore.org/core/texteditor/diffbrowser"
)
// FileNode is databrowser version of FileNode for FileTree
type FileNode struct {
filetree.Node
}
func (fn *FileNode) Init() {
fn.Node.Init()
fn.AddContextMenu(fn.ContextMenu)
}
func (fn *FileNode) WidgetTooltip(pos image.Point) (string, image.Point) {
res := fn.Tooltip
if fn.Info.Cat == fileinfo.Data {
ofn := fn.AsNode()
switch fn.Info.Known {
case fileinfo.Number, fileinfo.String:
dv := DataFS(ofn)
v, _ := dv.AsString()
if res != "" {
res += " "
}
res += v
}
}
return res, fn.DefaultTooltipPos()
}
// DataFS returns the datafs representation of this item.
// returns nil if not a dataFS item.
func DataFS(fn *filetree.Node) *datafs.Data {
dfs, ok := fn.FileRoot.FS.(*datafs.Data)
if !ok {
return nil
}
dfi, err := dfs.Stat(string(fn.Filepath))
if errors.Log(err) != nil {
return nil
}
return dfi.(*datafs.Data)
}
func (fn *FileNode) GetFileInfo() error {
err := fn.InitFileInfo()
if fn.FileRoot.FS == nil {
return err
}
d := DataFS(fn.AsNode())
if d != nil {
fn.Info.Known = d.KnownFileInfo()
fn.Info.Cat = fileinfo.Data
switch fn.Info.Known {
case fileinfo.Tensor:
fn.Info.Ic = icons.BarChart
case fileinfo.Table:
fn.Info.Ic = icons.BarChart4Bars
case fileinfo.Number:
fn.Info.Ic = icons.Tag
case fileinfo.String:
fn.Info.Ic = icons.Title
default:
fn.Info.Ic = icons.BarChart
}
}
return err
}
func (fn *FileNode) OpenFile() error {
ofn := fn.AsNode()
br := ParentBrowser(fn.This)
if br == nil {
return nil
}
df := fsx.DirAndFile(string(fn.Filepath))
switch {
case fn.Info.Cat == fileinfo.Data:
switch fn.Info.Known {
case fileinfo.Tensor:
d := DataFS(ofn)
tsr := d.AsTensor()
if tsr.IsString() || tsr.DataType() < reflect.Float32 {
br.NewTabTensorEditor(df, tsr)
} else {
br.NewTabTensorGrid(df, tsr)
}
case fileinfo.Table:
d := DataFS(ofn)
dt := d.AsTable()
br.NewTabTensorTable(df, dt)
br.Update()
case fileinfo.Number:
dv := DataFS(ofn)
v, _ := dv.AsFloat32()
d := core.NewBody(df)
core.NewText(d).SetType(core.TextSupporting).SetText(df)
sp := core.NewSpinner(d).SetValue(v)
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar)
d.AddOK(bar).OnClick(func(e events.Event) {
dv.SetFloat32(sp.Value)
})
})
d.RunDialog(br)
case fileinfo.String:
dv := DataFS(ofn)
v, _ := dv.AsString()
d := core.NewBody(df)
core.NewText(d).SetType(core.TextSupporting).SetText(df)
tf := core.NewTextField(d).SetText(v)
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar)
d.AddOK(bar).OnClick(func(e events.Event) {
dv.SetString(tf.Text())
})
})
d.RunDialog(br)
default:
dt := table.NewTable()
err := dt.OpenCSV(fn.Filepath, table.Tab) // todo: need more flexible data handling mode
if err != nil {
core.ErrorSnackbar(br, err)
} else {
br.NewTabTensorTable(df, dt)
}
}
case fn.IsExec(): // todo: use exec?
fn.OpenFilesDefault()
case fn.Info.Cat == fileinfo.Video: // todo: use our video viewer
fn.OpenFilesDefault()
case fn.Info.Cat == fileinfo.Audio: // todo: use our audio viewer
fn.OpenFilesDefault()
case fn.Info.Cat == fileinfo.Image: // todo: use our image viewer
fn.OpenFilesDefault()
case fn.Info.Cat == fileinfo.Model: // todo: use xyz
fn.OpenFilesDefault()
case fn.Info.Cat == fileinfo.Sheet: // todo: use our spreadsheet :)
fn.OpenFilesDefault()
case fn.Info.Cat == fileinfo.Bin: // don't edit
fn.OpenFilesDefault()
case fn.Info.Cat == fileinfo.Archive || fn.Info.Cat == fileinfo.Backup: // don't edit
fn.OpenFilesDefault()
default:
br.NewTabEditor(df, string(fn.Filepath))
}
return nil
}
// EditFiles calls EditFile on selected files
func (fn *FileNode) EditFiles() { //types:add
fn.SelectedFunc(func(sn *filetree.Node) {
sn.This.(*FileNode).EditFile()
})
}
// EditFile pulls up this file in a texteditor
func (fn *FileNode) EditFile() {
if fn.IsDir() {
log.Printf("FileNode Edit -- cannot view (edit) directories!\n")
return
}
br := ParentBrowser(fn.This)
if br == nil {
return
}
if fn.Info.Cat == fileinfo.Data {
fn.OpenFile()
return
}
df := fsx.DirAndFile(string(fn.Filepath))
br.NewTabEditor(df, string(fn.Filepath))
}
// PlotFiles calls PlotFile on selected files
func (fn *FileNode) PlotFiles() { //types:add
fn.SelectedFunc(func(sn *filetree.Node) {
if sfn, ok := sn.This.(*FileNode); ok {
sfn.PlotFile()
}
})
}
// PlotFile pulls up this file in a texteditor.
func (fn *FileNode) PlotFile() {
br := ParentBrowser(fn.This)
if br == nil {
return
}
d := DataFS(fn.AsNode())
df := fsx.DirAndFile(string(fn.Filepath))
ptab := df + " Plot"
var dt *table.Table
switch {
case fn.IsDir():
dt = d.DirTable(nil)
case fn.Info.Cat == fileinfo.Data:
switch fn.Info.Known {
case fileinfo.Tensor:
tsr := d.AsTensor()
dt = table.NewTable(df)
dt.Rows = tsr.DimSize(0)
rc := dt.AddIntColumn("Row")
for r := range dt.Rows {
rc.Values[r] = r
}
dt.AddColumn(tsr, fn.Name)
case fileinfo.Table:
dt = d.AsTable()
default:
dt = table.NewTable(df)
err := dt.OpenCSV(fn.Filepath, table.Tab) // todo: need more flexible data handling mode
if err != nil {
core.ErrorSnackbar(br, err)
dt = nil
}
}
}
if dt == nil {
return
}
pl := br.NewTabPlot(ptab, dt)
pl.Options.Title = df
// TODO: apply column and plot level options.
br.Update()
}
// DiffDirs displays a browser with differences between two selected directories
func (fn *FileNode) DiffDirs() { //types:add
var da, db *filetree.Node
fn.SelectedFunc(func(sn *filetree.Node) {
if sn.IsDir() {
if da == nil {
da = sn
} else if db == nil {
db = sn
}
}
})
if da == nil || db == nil {
core.MessageSnackbar(fn, "DiffDirs requires two selected directories")
return
}
NewDiffBrowserDirs(string(da.Filepath), string(db.Filepath))
}
// NewDiffBrowserDirs returns a new diff browser for files that differ
// within the two given directories. Excludes Job and .tsv data files.
func NewDiffBrowserDirs(pathA, pathB string) {
brow, b := diffbrowser.NewBrowserWindow()
brow.DiffDirs(pathA, pathB, func(fname string) bool {
if IsTableFile(fname) {
return true
}
if strings.HasPrefix(fname, "job.") || fname == "dbmeta.toml" {
return true
}
return false
})
b.RunWindow()
}
func IsTableFile(fname string) bool {
return strings.HasSuffix(fname, ".tsv") || strings.HasSuffix(fname, ".csv")
}
func (fn *FileNode) ContextMenu(m *core.Scene) {
core.NewFuncButton(m).SetFunc(fn.EditFiles).SetText("Edit").SetIcon(icons.Edit).
Styler(func(s *styles.Style) {
s.SetState(!fn.HasSelection(), states.Disabled)
})
core.NewFuncButton(m).SetFunc(fn.PlotFiles).SetText("Plot").SetIcon(icons.Edit).
Styler(func(s *styles.Style) {
s.SetState(!fn.HasSelection() || fn.Info.Cat != fileinfo.Data, states.Disabled)
})
core.NewFuncButton(m).SetFunc(fn.DiffDirs).SetText("Diff Dirs").SetIcon(icons.Edit).
Styler(func(s *styles.Style) {
s.SetState(!fn.HasSelection() || !fn.IsDir(), states.Disabled)
})
}
// Code generated by "core generate"; DO NOT EDIT.
package databrowser
import (
"io/fs"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/tensor/databrowser.Browser", IDName: "browser", Doc: "Browser is a data browser, for browsing data either on an os filesystem\nor as a datafs virtual data filesystem.", Methods: []types.Method{{Name: "UpdateFiles", Doc: "UpdateFiles Updates the file picker with current files in DataRoot,", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "FS", Doc: "Filesystem, if browsing an FS"}, {Name: "DataRoot", Doc: "DataRoot is the path to the root of the data to browse"}, {Name: "toolbar"}}})
// NewBrowser returns a new [Browser] with the given optional parent:
// Browser is a data browser, for browsing data either on an os filesystem
// or as a datafs virtual data filesystem.
func NewBrowser(parent ...tree.Node) *Browser { return tree.New[Browser](parent...) }
// SetFS sets the [Browser.FS]:
// Filesystem, if browsing an FS
func (t *Browser) SetFS(v fs.FS) *Browser { t.FS = v; return t }
// SetDataRoot sets the [Browser.DataRoot]:
// DataRoot is the path to the root of the data to browse
func (t *Browser) SetDataRoot(v string) *Browser { t.DataRoot = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/tensor/databrowser.FileNode", IDName: "file-node", Doc: "FileNode is databrowser version of FileNode for FileTree", Methods: []types.Method{{Name: "EditFiles", Doc: "EditFiles calls EditFile on selected files", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "PlotFiles", Doc: "PlotFiles calls PlotFile on selected files", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "DiffDirs", Doc: "DiffDirs displays a browser with differences between two selected directories", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "Node"}}})
// NewFileNode returns a new [FileNode] with the given optional parent:
// FileNode is databrowser version of FileNode for FileTree
func NewFileNode(parent ...tree.Node) *FileNode { return tree.New[FileNode](parent...) }
// Copyright (c) 2024, 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 datafs
import (
"errors"
"reflect"
"time"
"unsafe"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/metadata"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/table"
)
// Data is a single item of data, the "file" or "directory" in the data filesystem.
type Data struct {
// Parent is the parent data directory
Parent *Data
// name is the name of this item. it is not a path.
name string
// modTime tracks time added to directory, used for ordering.
modTime time.Time
// Meta has metadata, including standardized support for
// plotting options, compute functions.
Meta metadata.Data
// Value is the underlying value of data;
// is a map[string]*Data for directories.
Value any
}
// NewData returns a new Data item in given directory Data item,
// which can be nil. If not a directory, an error will be generated.
// The modTime is automatically set to now, and can be used for sorting
// by order created. The name must be unique within parent.
func NewData(dir *Data, name string) (*Data, error) {
d := &Data{Parent: dir, name: name, modTime: time.Now()}
var err error
if dir != nil {
err = dir.Add(d)
}
return d, err
}
// New adds new data item(s) of given basic type to given directory,
// with given name(s) (at least one is required).
// Values are initialized to zero value for type.
// All names must be unique in the directory.
// Returns the first item created, for immediate use of one value.
func New[T any](dir *Data, names ...string) (*Data, error) {
if len(names) == 0 {
err := errors.New("datafs.New requires at least 1 name")
return nil, err
}
var r *Data
var errs []error
for _, nm := range names {
var v T
d, err := NewData(dir, nm)
if err != nil {
errs = append(errs, err)
continue
}
d.Value = v
if r == nil {
r = d
}
}
return r, errors.Join(errs...)
}
// NewTensor returns a new Tensor of given data type, shape sizes,
// and optional dimension names, in given directory Data item.
// The name must be unique in the directory.
func NewTensor[T string | bool | float32 | float64 | int | int32 | byte](dir *Data, name string, sizes []int, names ...string) (tensor.Tensor, error) {
tsr := tensor.New[T](sizes, names...)
d, err := NewData(dir, name)
d.Value = tsr
return tsr, err
}
// NewTable makes new table.Table(s) in given directory,
// for given name(s) (at least one is required).
// All names must be unique in the directory.
// Returns the first table created, for immediate use of one item.
func NewTable(dir *Data, names ...string) (*table.Table, error) {
if len(names) == 0 {
err := errors.New("datafs.New requires at least 1 name")
return nil, err
}
var r *table.Table
var errs []error
for _, nm := range names {
t := table.NewTable(nm)
d, err := NewData(dir, nm)
if err != nil {
errs = append(errs, err)
continue
}
d.Value = t
if r == nil {
r = t
}
}
return r, errors.Join(errs...)
}
///////////////////////////////
// Data Access
// IsNumeric returns true if the [DataType] is a basic scalar
// numerical value, e.g., float32, int, etc.
func (d *Data) IsNumeric() bool {
return reflectx.KindIsNumber(d.DataType())
}
// DataType returns the type of the data elements in the tensor.
// Bool is returned for the Bits tensor type.
func (d *Data) DataType() reflect.Kind {
if d.Value == nil {
return reflect.Invalid
}
return reflect.TypeOf(d.Value).Kind()
}
func (d *Data) KnownFileInfo() fileinfo.Known {
if tsr := d.AsTensor(); tsr != nil {
return fileinfo.Tensor
}
kind := d.DataType()
if reflectx.KindIsNumber(kind) {
return fileinfo.Number
}
if kind == reflect.String {
return fileinfo.String
}
return fileinfo.Unknown
}
// AsTensor returns the data as a tensor if it is one, else nil.
func (d *Data) AsTensor() tensor.Tensor {
tsr, _ := d.Value.(tensor.Tensor)
return tsr
}
// AsTable returns the data as a table if it is one, else nil.
func (d *Data) AsTable() *table.Table {
dt, _ := d.Value.(*table.Table)
return dt
}
// AsFloat64 returns data as a float64 if it is a scalar value
// that can be so converted. Returns false if not.
func (d *Data) AsFloat64() (float64, bool) {
// fast path for actual floats
if f, ok := d.Value.(float64); ok {
return f, true
}
if f, ok := d.Value.(float32); ok {
return float64(f), true
}
if tsr := d.AsTensor(); tsr != nil {
return 0, false
}
if dt := d.AsTable(); dt != nil {
return 0, false
}
v, err := reflectx.ToFloat(d.Value)
if err != nil {
return 0, false
}
return v, true
}
// SetFloat64 sets data from given float64 if it is a scalar value
// that can be so set. Returns false if not.
func (d *Data) SetFloat64(v float64) bool {
// fast path for actual floats
if _, ok := d.Value.(float64); ok {
d.Value = v
return true
}
if _, ok := d.Value.(float32); ok {
d.Value = float32(v)
return true
}
if tsr := d.AsTensor(); tsr != nil {
return false
}
if dt := d.AsTable(); dt != nil {
return false
}
err := reflectx.SetRobust(&d.Value, v)
if err != nil {
return false
}
return true
}
// AsFloat32 returns data as a float32 if it is a scalar value
// that can be so converted. Returns false if not.
func (d *Data) AsFloat32() (float32, bool) {
v, ok := d.AsFloat64()
return float32(v), ok
}
// SetFloat32 sets data from given float32 if it is a scalar value
// that can be so set. Returns false if not.
func (d *Data) SetFloat32(v float32) bool {
return d.SetFloat64(float64(v))
}
// AsString returns data as a string if it is a scalar value
// that can be so converted. Returns false if not.
func (d *Data) AsString() (string, bool) {
// fast path for actual strings
if s, ok := d.Value.(string); ok {
return s, true
}
if tsr := d.AsTensor(); tsr != nil {
return "", false
}
if dt := d.AsTable(); dt != nil {
return "", false
}
s := reflectx.ToString(d.Value)
return s, true
}
// SetString sets data from given string if it is a scalar value
// that can be so set. Returns false if not.
func (d *Data) SetString(v string) bool {
// fast path for actual strings
if _, ok := d.Value.(string); ok {
d.Value = v
return true
}
if tsr := d.AsTensor(); tsr != nil {
return false
}
if dt := d.AsTable(); dt != nil {
return false
}
err := reflectx.SetRobust(&d.Value, v)
if err != nil {
return false
}
return true
}
// AsInt returns data as a int if it is a scalar value
// that can be so converted. Returns false if not.
func (d *Data) AsInt() (int, bool) {
// fast path for actual ints
if f, ok := d.Value.(int); ok {
return f, true
}
if tsr := d.AsTensor(); tsr != nil {
return 0, false
}
if dt := d.AsTable(); dt != nil {
return 0, false
}
v, err := reflectx.ToInt(d.Value)
if err != nil {
return 0, false
}
return int(v), true
}
// SetInt sets data from given int if it is a scalar value
// that can be so set. Returns false if not.
func (d *Data) SetInt(v int) bool {
// fast path for actual ints
if _, ok := d.Value.(int); ok {
d.Value = v
return true
}
if tsr := d.AsTensor(); tsr != nil {
return false
}
if dt := d.AsTable(); dt != nil {
return false
}
err := reflectx.SetRobust(&d.Value, v)
if err != nil {
return false
}
return true
}
// Bytes returns the byte-wise representation of the data Value.
// This is the actual underlying data, so make a copy if it can be
// unintentionally modified or retained more than for immediate use.
func (d *Data) Bytes() []byte {
if tsr := d.AsTensor(); tsr != nil {
return tsr.Bytes()
}
size := d.Size()
switch x := d.Value.(type) {
// todo: other things here?
default:
return unsafe.Slice((*byte)(unsafe.Pointer(&x)), size)
}
}
// Copyright (c) 2024, 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 datafs
import (
"errors"
"fmt"
"io/fs"
"path"
"slices"
"sort"
"golang.org/x/exp/maps"
)
// NewDir returns a new datafs directory with given name.
// if parent != nil and a directory, this dir is added to it.
// if name is empty, then it is set to "root", the root directory.
// Note that "/" is not allowed for the root directory in Go [fs].
// Names must be unique within a directory.
func NewDir(name string, parent ...*Data) (*Data, error) {
if name == "" {
name = "root"
}
var par *Data
if len(parent) == 1 {
par = parent[0]
}
d, err := NewData(par, name)
d.Value = make(map[string]*Data)
return d, err
}
// Item returns data item in given directory by name.
// This is for fast access and direct usage of known
// items, and it will crash if item is not found or
// this data is not a directory.
func (d *Data) Item(name string) *Data {
fm := d.filemap()
return fm[name]
}
// Items returns data items in given directory by name.
// error reports any items not found, or if not a directory.
func (d *Data) Items(names ...string) ([]*Data, error) {
if err := d.mustDir("Items", ""); err != nil {
return nil, err
}
fm := d.filemap()
var errs []error
var its []*Data
for _, nm := range names {
dt := fm[nm]
if dt != nil {
its = append(its, dt)
} else {
err := fmt.Errorf("datafs Dir %q item not found: %q", d.Path(), nm)
errs = append(errs, err)
}
}
return its, errors.Join(errs...)
}
// ItemsFunc returns data items in given directory
// filtered by given function, in alpha order.
// If func is nil, all items are returned.
// Any directories within this directory are returned,
// unless specifically filtered.
func (d *Data) ItemsFunc(fun func(item *Data) bool) []*Data {
if err := d.mustDir("ItemsFunc", ""); err != nil {
return nil
}
fm := d.filemap()
names := d.DirNamesAlpha()
var its []*Data
for _, nm := range names {
dt := fm[nm]
if fun != nil && !fun(dt) {
continue
}
its = append(its, dt)
}
return its
}
// ItemsByTimeFunc returns data items in given directory
// filtered by given function, in time order (i.e., order added).
// If func is nil, all items are returned.
// Any directories within this directory are returned,
// unless specifically filtered.
func (d *Data) ItemsByTimeFunc(fun func(item *Data) bool) []*Data {
if err := d.mustDir("ItemsByTimeFunc", ""); err != nil {
return nil
}
fm := d.filemap()
names := d.DirNamesByTime()
var its []*Data
for _, nm := range names {
dt := fm[nm]
if fun != nil && !fun(dt) {
continue
}
its = append(its, dt)
}
return its
}
// FlatItemsFunc returns all "leaf" (non directory) data items
// in given directory, recursively descending into directories
// to return a flat list of the entire subtree,
// filtered by given function, in alpha order. The function can
// filter out directories to prune the tree.
// If func is nil, all items are returned.
func (d *Data) FlatItemsFunc(fun func(item *Data) bool) []*Data {
if err := d.mustDir("FlatItemsFunc", ""); err != nil {
return nil
}
fm := d.filemap()
names := d.DirNamesAlpha()
var its []*Data
for _, nm := range names {
dt := fm[nm]
if fun != nil && !fun(dt) {
continue
}
if dt.IsDir() {
subs := dt.FlatItemsFunc(fun)
its = append(its, subs...)
} else {
its = append(its, dt)
}
}
return its
}
// FlatItemsByTimeFunc returns all "leaf" (non directory) data items
// in given directory, recursively descending into directories
// to return a flat list of the entire subtree,
// filtered by given function, in time order (i.e., order added).
// The function can filter out directories to prune the tree.
// If func is nil, all items are returned.
func (d *Data) FlatItemsByTimeFunc(fun func(item *Data) bool) []*Data {
if err := d.mustDir("FlatItemsByTimeFunc", ""); err != nil {
return nil
}
fm := d.filemap()
names := d.DirNamesByTime()
var its []*Data
for _, nm := range names {
dt := fm[nm]
if fun != nil && !fun(dt) {
continue
}
if dt.IsDir() {
subs := dt.FlatItemsByTimeFunc(fun)
its = append(its, subs...)
} else {
its = append(its, dt)
}
}
return its
}
// DirAtPath returns directory at given relative path
// from this starting dir.
func (d *Data) DirAtPath(dir string) (*Data, error) {
var err error
dir = path.Clean(dir)
sdf, err := d.Sub(dir) // this ensures that d is a dir
if err != nil {
return nil, err
}
return sdf.(*Data), nil
}
// Path returns the full path to this data item
func (d *Data) Path() string {
pt := d.name
cur := d.Parent
loops := make(map[*Data]struct{})
for {
if cur == nil {
return pt
}
if _, ok := loops[cur]; ok {
return pt
}
pt = path.Join(cur.name, pt)
loops[cur] = struct{}{}
cur = cur.Parent
}
}
// filemap returns the Value as map[string]*Data, or nil if not a dir
func (d *Data) filemap() map[string]*Data {
fm, ok := d.Value.(map[string]*Data)
if !ok {
return nil
}
return fm
}
// DirNamesAlpha returns the names of items in the directory
// sorted alphabetically. Data must be dir by this point.
func (d *Data) DirNamesAlpha() []string {
fm := d.filemap()
names := maps.Keys(fm)
sort.Strings(names)
return names
}
// DirNamesByTime returns the names of items in the directory
// sorted by modTime (order added). Data must be dir by this point.
func (d *Data) DirNamesByTime() []string {
fm := d.filemap()
names := maps.Keys(fm)
slices.SortFunc(names, func(a, b string) int {
return fm[a].ModTime().Compare(fm[b].ModTime())
})
return names
}
// mustDir returns an error for given operation and path
// if this data item is not a directory.
func (d *Data) mustDir(op, path string) error {
if !d.IsDir() {
return &fs.PathError{Op: op, Path: path, Err: errors.New("datafs item is not a directory")}
}
return nil
}
// Add adds an item to this directory data item.
// The only errors are if this item is not a directory,
// or the name already exists.
// Names must be unique within a directory.
func (d *Data) Add(it *Data) error {
if err := d.mustDir("Add", it.name); err != nil {
return err
}
fm := d.filemap()
_, ok := fm[it.name]
if ok {
return &fs.PathError{Op: "add", Path: it.name, Err: errors.New("data item already exists; names must be unique")}
}
fm[it.name] = it
return nil
}
// Mkdir creates a new directory with the specified name.
// The only error is if this item is not a directory.
func (d *Data) Mkdir(name string) (*Data, error) {
if err := d.mustDir("Mkdir", name); err != nil {
return nil, err
}
return NewDir(name, d)
}
// Copyright (c) 2024, 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 datafs
import (
"bytes"
"io"
"io/fs"
)
// File represents a data item for reading, as an [fs.File].
// All io functionality is handled by [bytes.Reader].
type File struct {
bytes.Reader
Data *Data
dirEntries []fs.DirEntry
dirsRead int
}
func (f *File) Stat() (fs.FileInfo, error) {
return f.Data, nil
}
func (f *File) Close() error {
f.Reader.Reset(f.Data.Bytes())
return nil
}
// DirFile represents a directory data item for reading, as fs.ReadDirFile.
type DirFile struct {
File
dirEntries []fs.DirEntry
dirsRead int
}
func (f *DirFile) ReadDir(n int) ([]fs.DirEntry, error) {
if err := f.Data.mustDir("DirFile:ReadDir", ""); err != nil {
return nil, err
}
if f.dirEntries == nil {
f.dirEntries, _ = f.Data.ReadDir(".")
f.dirsRead = 0
}
ne := len(f.dirEntries)
if n <= 0 {
if f.dirsRead >= ne {
return nil, nil
}
re := f.dirEntries[f.dirsRead:]
f.dirsRead = ne
return re, nil
}
if f.dirsRead >= ne {
return nil, io.EOF
}
mx := min(n+f.dirsRead, ne)
re := f.dirEntries[f.dirsRead:mx]
f.dirsRead = mx
return re, nil
}
func (f *DirFile) Close() error {
f.dirsRead = 0
return nil
}
// Copyright (c) 2024, 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 datafs
import (
"bytes"
"errors"
"io/fs"
"path"
"slices"
"sort"
"time"
"unsafe"
"cogentcore.org/core/base/fsx"
"golang.org/x/exp/maps"
)
// fs.go contains all the io/fs interface implementations
// Open opens the given data Value within this datafs filesystem.
func (d *Data) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "open", Path: name, Err: errors.New("invalid name")}
}
dir, file := path.Split(name)
sd, err := d.DirAtPath(dir)
if err != nil {
return nil, err
}
fm := sd.filemap()
itm, ok := fm[file]
if !ok {
if dir == "" && (file == d.name || file == ".") {
return &DirFile{File: File{Reader: *bytes.NewReader(d.Bytes()), Data: d}}, nil
}
return nil, &fs.PathError{Op: "open", Path: name, Err: errors.New("file not found")}
}
if itm.IsDir() {
return &DirFile{File: File{Reader: *bytes.NewReader(itm.Bytes()), Data: itm}}, nil
}
return &File{Reader: *bytes.NewReader(itm.Bytes()), Data: itm}, nil
}
// Stat returns a FileInfo describing the file.
// If there is an error, it should be of type *PathError.
func (d *Data) Stat(name string) (fs.FileInfo, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "open", Path: name, Err: errors.New("invalid name")}
}
dir, file := path.Split(name)
sd, err := d.DirAtPath(dir)
if err != nil {
return nil, err
}
fm := sd.filemap()
itm, ok := fm[file]
if !ok {
if dir == "" && (file == d.name || file == ".") {
return d, nil
}
return nil, &fs.PathError{Op: "stat", Path: name, Err: errors.New("file not found")}
}
return itm, nil
}
// Sub returns a data FS corresponding to the subtree rooted at dir.
func (d *Data) Sub(dir string) (fs.FS, error) {
if err := d.mustDir("sub", dir); err != nil {
return nil, err
}
if !fs.ValidPath(dir) {
return nil, &fs.PathError{Op: "sub", Path: dir, Err: errors.New("invalid name")}
}
if dir == "." || dir == "" || dir == d.name {
return d, nil
}
cd := dir
cur := d
root, rest := fsx.SplitRootPathFS(dir)
if root == "." || root == d.name {
cd = rest
}
for {
if cd == "." || cd == "" {
return cur, nil
}
root, rest := fsx.SplitRootPathFS(cd)
if root == "." && rest == "" {
return cur, nil
}
cd = rest
fm := cur.filemap()
sd, ok := fm[root]
if !ok {
return nil, &fs.PathError{Op: "sub", Path: dir, Err: errors.New("directory not found")}
}
if !sd.IsDir() {
return nil, &fs.PathError{Op: "sub", Path: dir, Err: errors.New("is not a directory")}
}
cur = sd
}
}
// ReadDir returns the contents of the given directory within this filesystem.
// Use "." (or "") to refer to the current directory.
func (d *Data) ReadDir(dir string) ([]fs.DirEntry, error) {
sd, err := d.DirAtPath(dir)
if err != nil {
return nil, err
}
fm := sd.filemap()
names := maps.Keys(fm)
sort.Strings(names)
ents := make([]fs.DirEntry, len(names))
for i, nm := range names {
ents[i] = fm[nm]
}
return ents, nil
}
// ReadFile reads the named file and returns its contents.
// A successful call returns a nil error, not io.EOF.
// (Because ReadFile reads the whole file, the expected EOF
// from the final Read is not treated as an error to be reported.)
//
// The caller is permitted to modify the returned byte slice.
// This method should return a copy of the underlying data.
func (d *Data) ReadFile(name string) ([]byte, error) {
if err := d.mustDir("readFile", name); err != nil {
return nil, err
}
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "readFile", Path: name, Err: errors.New("invalid name")}
}
dir, file := path.Split(name)
sd, err := d.DirAtPath(dir)
if err != nil {
return nil, err
}
fm := sd.filemap()
itm, ok := fm[file]
if !ok {
return nil, &fs.PathError{Op: "readFile", Path: name, Err: errors.New("file not found")}
}
if itm.IsDir() {
return nil, &fs.PathError{Op: "readFile", Path: name, Err: errors.New("Value is a directory")}
}
return slices.Clone(itm.Bytes()), nil
}
///////////////////////////////
// FileInfo interface:
// Sizer is an interface to allow an arbitrary data Value
// to report its size in bytes. Size is automatically computed for
// known basic data Values supported by datafs directly.
type Sizer interface {
Sizeof() int64
}
func (d *Data) Name() string { return d.name }
// Size returns the size of known data Values, or it uses
// the Sizer interface, otherwise returns 0.
func (d *Data) Size() int64 {
if szr, ok := d.Value.(Sizer); ok { // tensor implements Sizer
return szr.Sizeof()
}
switch x := d.Value.(type) {
case float32, int32, uint32:
return 4
case float64, int64:
return 8
case int:
return int64(unsafe.Sizeof(x))
case complex64:
return 16
case complex128:
return 32
}
return 0
}
func (d *Data) IsDir() bool {
_, ok := d.Value.(map[string]*Data)
return ok
}
func (d *Data) ModTime() time.Time {
return d.modTime
}
func (d *Data) Mode() fs.FileMode {
if d.IsDir() {
return 0755 | fs.ModeDir
}
return 0444
}
// Sys returns the metadata for Value
func (d *Data) Sys() any { return d.Meta }
///////////////////////////////
// DirEntry interface
func (d *Data) Type() fs.FileMode {
return d.Mode().Type()
}
func (d *Data) Info() (fs.FileInfo, error) {
return d, nil
}
// Copyright (c) 2024, 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 datafs
import (
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/metadata"
"cogentcore.org/core/plot/plotcore"
"cogentcore.org/core/tensor/table"
)
// This file provides standardized metadata options for frequent
// use cases, using codified key names to eliminate typos.
// SetMetaItems sets given metadata for items in given directory
// with given names. Returns error for any items not found.
func (d *Data) SetMetaItems(key string, value any, names ...string) error {
its, err := d.Items(names...)
for _, it := range its {
it.Meta.Set(key, value)
}
return err
}
// PlotColumnZeroOne returns plot options with a fixed 0-1 range
func PlotColumnZeroOne() *plotcore.ColumnOptions {
opts := &plotcore.ColumnOptions{}
opts.Range.SetMin(0)
opts.Range.SetMax(1)
return opts
}
// SetPlotColumnOptions sets given plotting options for named items
// within this directory (stored in Metadata).
func (d *Data) SetPlotColumnOptions(opts *plotcore.ColumnOptions, names ...string) error {
return d.SetMetaItems("PlotColumnOptions", opts, names...)
}
// PlotColumnOptions returns plotting options if they have been set, else nil.
func (d *Data) PlotColumnOptions() *plotcore.ColumnOptions {
return errors.Ignore1(metadata.Get[*plotcore.ColumnOptions](d.Meta, "PlotColumnOptions"))
}
// SetCalcFunc sets a function to compute an updated Value for this data item.
// Function is stored as CalcFunc in Metadata. Can be called by [Data.Calc] method.
func (d *Data) SetCalcFunc(fun func() error) {
d.Meta.Set("CalcFunc", fun)
}
// Calc calls function set by [Data.SetCalcFunc] to compute an updated Value
// for this data item. Returns an error if func not set, or any error from func itself.
// Function is stored as CalcFunc in Metadata.
func (d *Data) Calc() error {
fun, err := metadata.Get[func() error](d.Meta, "CalcFunc")
if err != nil {
return err
}
return fun()
}
// CalcAll calls function set by [Data.SetCalcFunc] for all items
// in this directory and all of its subdirectories.
// Calls Calc on items from FlatItemsByTimeFunc(nil)
func (d *Data) CalcAll() error {
var errs []error
items := d.FlatItemsByTimeFunc(nil)
for _, it := range items {
err := it.Calc()
if err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
// DirTable returns a table.Table for this directory item, with columns
// as the Tensor elements in the directory and any subdirectories,
// from FlatItemsByTimeFunc using given filter function.
// This is a convenient mechanism for creating a plot of all the data
// in a given directory.
// If such was previously constructed, it is returned from "DirTable"
// Metadata key where the table is stored.
// Row count is updated to current max row.
// Delete that key to reconstruct if items have changed.
func (d *Data) DirTable(fun func(item *Data) bool) *table.Table {
dt, err := metadata.Get[*table.Table](d.Meta, "DirTable")
if err == nil {
var maxRow int
for _, tsr := range dt.Columns {
maxRow = max(maxRow, tsr.DimSize(0))
}
dt.Rows = maxRow
return dt
}
items := d.FlatItemsByTimeFunc(fun)
dt = table.NewTable(fsx.DirAndFile(string(d.Path())))
for _, it := range items {
tsr := it.AsTensor()
if tsr == nil {
continue
}
if dt.Rows == 0 {
dt.Rows = tsr.DimSize(0)
}
nm := it.Name()
if it.Parent != d {
nm = fsx.DirAndFile(string(it.Path()))
}
dt.AddColumn(tsr, nm)
}
d.Meta.Set("DirTable", dt)
return dt
}
// Copyright (c) 2024, 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 main
import (
"math/rand/v2"
"reflect"
"strconv"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/core"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/databrowser"
"cogentcore.org/core/tensor/datafs"
"cogentcore.org/core/tensor/stats/stats"
)
type Sim struct {
Root *datafs.Data
Config *datafs.Data
Stats *datafs.Data
Logs *datafs.Data
}
// ConfigAll configures the sim
func (ss *Sim) ConfigAll() {
ss.Root = errors.Log1(datafs.NewDir("Root"))
ss.Config = errors.Log1(ss.Root.Mkdir("Config"))
errors.Log1(datafs.New[int](ss.Config, "NRun", "NEpoch", "NTrial"))
ss.Config.Item("NRun").SetInt(5)
ss.Config.Item("NEpoch").SetInt(20)
ss.Config.Item("NTrial").SetInt(25)
ss.Stats = ss.ConfigStats(ss.Root)
ss.Logs = ss.ConfigLogs(ss.Root)
}
// ConfigStats adds basic stats that we record for our simulation.
func (ss *Sim) ConfigStats(dir *datafs.Data) *datafs.Data {
stats := errors.Log1(dir.Mkdir("Stats"))
errors.Log1(datafs.New[int](stats, "Run", "Epoch", "Trial")) // counters
errors.Log1(datafs.New[string](stats, "TrialName"))
errors.Log1(datafs.New[float32](stats, "SSE", "AvgSSE", "TrlErr"))
z1 := datafs.PlotColumnZeroOne()
stats.SetPlotColumnOptions(z1, "AvgErr", "TrlErr")
zmax := datafs.PlotColumnZeroOne()
zmax.Range.FixMax = false
stats.SetPlotColumnOptions(z1, "SSE")
return stats
}
// ConfigLogs adds first-level logging of stats into tensors
func (ss *Sim) ConfigLogs(dir *datafs.Data) *datafs.Data {
logd := errors.Log1(dir.Mkdir("Log"))
trial := ss.ConfigTrialLog(logd)
ss.ConfigAggLog(logd, "Epoch", trial, stats.Mean, stats.Sem, stats.Min)
return logd
}
// ConfigTrialLog adds first-level logging of stats into tensors
func (ss *Sim) ConfigTrialLog(dir *datafs.Data) *datafs.Data {
logd := errors.Log1(dir.Mkdir("Trial"))
ntrial, _ := ss.Config.Item("NTrial").AsInt()
sitems := ss.Stats.ItemsByTimeFunc(nil)
for _, st := range sitems {
dt := errors.Log1(datafs.NewData(logd, st.Name()))
tsr := tensor.NewOfType(st.DataType(), []int{ntrial}, "row")
dt.Value = tsr
dt.Meta.Copy(st.Meta) // key affordance: we get meta data from source
dt.SetCalcFunc(func() error {
trl, _ := ss.Stats.Item("Trial").AsInt()
if st.IsNumeric() {
v, _ := st.AsFloat64()
tsr.SetFloat1D(trl, v)
} else {
v, _ := st.AsString()
tsr.SetString1D(trl, v)
}
return nil
})
}
return logd
}
// ConfigAggLog adds a higher-level logging of lower-level into higher-level tensors
func (ss *Sim) ConfigAggLog(dir *datafs.Data, level string, from *datafs.Data, aggs ...stats.Stats) *datafs.Data {
logd := errors.Log1(dir.Mkdir(level))
sitems := ss.Stats.ItemsByTimeFunc(nil)
nctr, _ := ss.Config.Item("N" + level).AsInt()
for _, st := range sitems {
if !st.IsNumeric() {
continue
}
src := from.Item(st.Name()).AsTensor()
if st.DataType() >= reflect.Float32 {
dd := errors.Log1(logd.Mkdir(st.Name()))
for _, ag := range aggs { // key advantage of dir structure: multiple stats per item
dt := errors.Log1(datafs.NewData(dd, ag.String()))
tsr := tensor.NewOfType(st.DataType(), []int{nctr}, "row")
dt.Value = tsr
dt.Meta.Copy(st.Meta)
dt.SetCalcFunc(func() error {
ctr, _ := ss.Stats.Item(level).AsInt()
v := stats.StatTensor(src, ag)
tsr.SetFloat1D(ctr, v)
return nil
})
}
} else {
dt := errors.Log1(datafs.NewData(logd, st.Name()))
tsr := tensor.NewOfType(st.DataType(), []int{nctr}, "row")
// todo: set level counter as default x axis in plot config
dt.Value = tsr
dt.Meta.Copy(st.Meta)
dt.SetCalcFunc(func() error {
ctr, _ := ss.Stats.Item(level).AsInt()
v, _ := st.AsFloat64()
tsr.SetFloat1D(ctr, v)
return nil
})
}
}
return logd
}
func (ss *Sim) Run() {
nepc, _ := ss.Config.Item("NEpoch").AsInt()
ntrl, _ := ss.Config.Item("NTrial").AsInt()
for epc := range nepc {
ss.Stats.Item("Epoch").SetInt(epc)
for trl := range ntrl {
ss.Stats.Item("Trial").SetInt(trl)
ss.RunTrial(trl)
}
ss.EpochDone()
}
}
func (ss *Sim) RunTrial(trl int) {
ss.Stats.Item("TrialName").SetString("Trial_" + strconv.Itoa(trl))
sse := rand.Float32()
avgSSE := rand.Float32()
ss.Stats.Item("SSE").SetFloat32(sse)
ss.Stats.Item("AvgSSE").SetFloat32(avgSSE)
trlErr := float32(1)
if sse < 0.5 {
trlErr = 0
}
ss.Stats.Item("TrlErr").SetFloat32(trlErr)
ss.Logs.Item("Trial").CalcAll()
}
func (ss *Sim) EpochDone() {
ss.Logs.Item("Epoch").CalcAll()
}
func main() {
ss := &Sim{}
ss.ConfigAll()
ss.Run()
databrowser.NewBrowserWindow(ss.Root, "Root")
core.Wait()
}
// Copyright (c) 2024, 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 main
import (
"embed"
"fmt"
"math"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/tensor/stats/split"
"cogentcore.org/core/tensor/stats/stats"
"cogentcore.org/core/tensor/table"
"cogentcore.org/core/tensor/tensorcore"
"cogentcore.org/core/tree"
)
// Planets is raw data
var Planets *table.Table
// PlanetsDesc are descriptive stats of all (non-Null) data
var PlanetsDesc *table.Table
// PlanetsNNDesc are descriptive stats of planets where entire row is non-null
var PlanetsNNDesc *table.Table
// GpMethodOrbit shows the median of orbital period as a function of method
var GpMethodOrbit *table.Table
// GpMethodYear shows all stats of year described by orbit
var GpMethodYear *table.Table
// GpMethodDecade shows number of planets found in each decade by given method
var GpMethodDecade *table.Table
// GpDecade shows number of planets found in each decade
var GpDecade *table.Table
//go:embed *.csv
var csv embed.FS
// AnalyzePlanets analyzes planets.csv data following some of the examples
// given here, using pandas:
//
// https://jakevdp.github.io/PythonDataScienceHandbook/03.08-aggregation-and-grouping.html
func AnalyzePlanets() {
Planets = table.NewTable("planets")
Planets.OpenFS(csv, "planets.csv", table.Comma)
PlanetsAll := table.NewIndexView(Planets) // full original data
PlanetsDesc = stats.DescAll(PlanetsAll) // individually excludes Null values in each col, but not row-wise
PlanetsNNDesc = stats.DescAll(PlanetsAll) // standard descriptive stats for row-wise non-nulls
byMethod := split.GroupBy(PlanetsAll, "method")
split.AggColumn(byMethod, "orbital_period", stats.Median)
GpMethodOrbit = byMethod.AggsToTable(table.AddAggName)
byMethod.DeleteAggs()
split.DescColumn(byMethod, "year") // full desc stats of year
byMethod.Filter(func(idx int) bool {
ag := errors.Log1(byMethod.AggByColumnName("year:Std"))
return ag.Aggs[idx][0] > 0 // exclude results with 0 std
})
GpMethodYear = byMethod.AggsToTable(table.AddAggName)
byMethodDecade := split.GroupByFunc(PlanetsAll, func(row int) []string {
meth := Planets.StringValue("method", row)
yr := Planets.Float("year", row)
decade := math.Floor(yr/10) * 10
return []string{meth, fmt.Sprintf("%gs", decade)}
})
byMethodDecade.SetLevels("method", "decade")
split.AggColumn(byMethodDecade, "number", stats.Sum)
// uncomment this to switch to decade first, then method
// byMethodDecade.ReorderLevels([]int{1, 0})
// byMethodDecade.SortLevels()
decadeOnly := errors.Log1(byMethodDecade.ExtractLevels([]int{1}))
split.AggColumn(decadeOnly, "number", stats.Sum)
GpDecade = decadeOnly.AggsToTable(table.AddAggName)
GpMethodDecade = byMethodDecade.AggsToTable(table.AddAggName) // here to ensure that decadeOnly didn't mess up..
// todo: need unstack -- should be specific to the splits data because we already have the cols and
// groups etc -- the ExtractLevels method provides key starting point.
// todo: pivot table -- neeeds unstack function.
// todo: could have a generic unstack-like method that takes a column for the data to turn into columns
// and another that has the data to put in the cells.
}
func main() {
AnalyzePlanets()
b := core.NewBody("dataproc")
tv := core.NewTabs(b)
nt, _ := tv.NewTab("Planets Data")
tbv := tensorcore.NewTable(nt).SetTable(Planets)
b.AddTopBar(func(bar *core.Frame) {
tb := core.NewToolbar(bar)
tb.Maker(tbv.MakeToolbar)
tb.Maker(func(p *tree.Plan) {
tree.Add(p, func(w *core.Button) {
w.SetText("README").SetIcon(icons.FileMarkdown).
SetTooltip("open README help file").OnClick(func(e events.Event) {
core.TheApp.OpenURL("https://github.com/cogentcore/core/blob/main/tensor/examples/dataproc/README.md")
})
})
})
})
nt, _ = tv.NewTab("Non-Null Rows Desc")
tensorcore.NewTable(nt).SetTable(PlanetsNNDesc)
nt, _ = tv.NewTab("All Desc")
tensorcore.NewTable(nt).SetTable(PlanetsDesc)
nt, _ = tv.NewTab("By Method Orbit")
tensorcore.NewTable(nt).SetTable(GpMethodOrbit)
nt, _ = tv.NewTab("By Method Year")
tensorcore.NewTable(nt).SetTable(GpMethodYear)
nt, _ = tv.NewTab("By Method Decade")
tensorcore.NewTable(nt).SetTable(GpMethodDecade)
nt, _ = tv.NewTab("By Decade")
tensorcore.NewTable(nt).SetTable(GpDecade)
tv.SelectTabIndex(0)
b.RunMainWindow()
}
// Copyright (c) 2024, 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 main
import (
"embed"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/core"
"cogentcore.org/core/tensor/table"
"cogentcore.org/core/tensor/tensorcore"
)
//go:embed *.tsv
var tsv embed.FS
func main() {
pats := table.NewTable("pats")
pats.SetMetaData("name", "TrainPats")
pats.SetMetaData("desc", "Training patterns")
// todo: meta data for grid size
errors.Log(pats.OpenFS(tsv, "random_5x5_25.tsv", table.Tab))
b := core.NewBody("grids")
tv := core.NewTabs(b)
// nt, _ := tv.NewTab("First")
nt, _ := tv.NewTab("Patterns")
etv := tensorcore.NewTable(nt).SetTable(pats)
b.AddTopBar(func(bar *core.Frame) {
core.NewToolbar(bar).Maker(etv.MakeToolbar)
})
b.RunMainWindow()
}
// Copyright (c) 2024, 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 tensor
import (
"encoding/csv"
"io"
"log"
"os"
"strconv"
"cogentcore.org/core/core"
)
// SaveCSV writes a tensor to a comma-separated-values (CSV) file
// (where comma = any delimiter, specified in the delim arg).
// Outer-most dims are rows in the file, and inner-most is column --
// Reading just grabs all values and doesn't care about shape.
func SaveCSV(tsr Tensor, filename core.Filename, delim rune) error {
fp, err := os.Create(string(filename))
defer fp.Close()
if err != nil {
log.Println(err)
return err
}
WriteCSV(tsr, fp, delim)
return nil
}
// OpenCSV reads a tensor from a comma-separated-values (CSV) file
// (where comma = any delimiter, specified in the delim arg),
// using the Go standard encoding/csv reader conforming
// to the official CSV standard.
// Reads all values and assigns as many as fit.
func OpenCSV(tsr Tensor, filename core.Filename, delim rune) error {
fp, err := os.Open(string(filename))
defer fp.Close()
if err != nil {
log.Println(err)
return err
}
return ReadCSV(tsr, fp, delim)
}
//////////////////////////////////////////////////////////////////////////
// WriteCSV
// WriteCSV writes a tensor to a comma-separated-values (CSV) file
// (where comma = any delimiter, specified in the delim arg).
// Outer-most dims are rows in the file, and inner-most is column --
// Reading just grabs all values and doesn't care about shape.
func WriteCSV(tsr Tensor, w io.Writer, delim rune) error {
prec := -1
if ps, ok := tsr.MetaData("precision"); ok {
prec, _ = strconv.Atoi(ps)
}
cw := csv.NewWriter(w)
if delim != 0 {
cw.Comma = delim
}
nrow := tsr.DimSize(0)
nin := tsr.Len() / nrow
rec := make([]string, nin)
str := tsr.IsString()
for ri := 0; ri < nrow; ri++ {
for ci := 0; ci < nin; ci++ {
idx := ri*nin + ci
if str {
rec[ci] = tsr.String1D(idx)
} else {
rec[ci] = strconv.FormatFloat(tsr.Float1D(idx), 'g', prec, 64)
}
}
err := cw.Write(rec)
if err != nil {
log.Println(err)
return err
}
}
cw.Flush()
return nil
}
// ReadCSV reads a tensor from a comma-separated-values (CSV) file
// (where comma = any delimiter, specified in the delim arg),
// using the Go standard encoding/csv reader conforming
// to the official CSV standard.
// Reads all values and assigns as many as fit.
func ReadCSV(tsr Tensor, r io.Reader, delim rune) error {
cr := csv.NewReader(r)
if delim != 0 {
cr.Comma = delim
}
rec, err := cr.ReadAll() // todo: lazy, avoid resizing
if err != nil || len(rec) == 0 {
return err
}
rows := len(rec)
cols := len(rec[0])
sz := tsr.Len()
idx := 0
for ri := 0; ri < rows; ri++ {
for ci := 0; ci < cols; ci++ {
str := rec[ri][ci]
tsr.SetString1D(idx, str)
idx++
if idx >= sz {
goto done
}
}
}
done:
return nil
}
// Copyright (c) 2024, 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 tensor
import (
"fmt"
"log"
"math"
"strconv"
"strings"
"cogentcore.org/core/base/num"
"cogentcore.org/core/base/slicesx"
"gonum.org/v1/gonum/mat"
)
// Number is a tensor of numerical values
type Number[T num.Number] struct {
Base[T]
}
// Float64 is an alias for Number[float64].
type Float64 = Number[float64]
// Float32 is an alias for Number[float32].
type Float32 = Number[float32]
// Int is an alias for Number[int].
type Int = Number[int]
// Int32 is an alias for Number[int32].
type Int32 = Number[int32]
// Byte is an alias for Number[byte].
type Byte = Number[byte]
// NewFloat32 returns a new Float32 tensor
// with the given sizes per dimension (shape), and optional dimension names.
func NewFloat32(sizes []int, names ...string) *Float32 {
return New[float32](sizes, names...).(*Float32)
}
// NewFloat64 returns a new Float64 tensor
// with the given sizes per dimension (shape), and optional dimension names.
func NewFloat64(sizes []int, names ...string) *Float64 {
return New[float64](sizes, names...).(*Float64)
}
// NewInt returns a new Int tensor
// with the given sizes per dimension (shape), and optional dimension names.
func NewInt(sizes []int, names ...string) *Int {
return New[float64](sizes, names...).(*Int)
}
// NewInt32 returns a new Int32 tensor
// with the given sizes per dimension (shape), and optional dimension names.
func NewInt32(sizes []int, names ...string) *Int32 {
return New[float64](sizes, names...).(*Int32)
}
// NewByte returns a new Byte tensor
// with the given sizes per dimension (shape), and optional dimension names.
func NewByte(sizes []int, names ...string) *Byte {
return New[float64](sizes, names...).(*Byte)
}
// NewNumber returns a new n-dimensional tensor of numerical values
// with the given sizes per dimension (shape), and optional dimension names.
func NewNumber[T num.Number](sizes []int, names ...string) *Number[T] {
tsr := &Number[T]{}
tsr.SetShape(sizes, names...)
tsr.Values = make([]T, tsr.Len())
return tsr
}
// NewNumberShape returns a new n-dimensional tensor of numerical values
// using given shape.
func NewNumberShape[T num.Number](shape *Shape) *Number[T] {
tsr := &Number[T]{}
tsr.Shp.CopyShape(shape)
tsr.Values = make([]T, tsr.Len())
return tsr
}
func (tsr *Number[T]) IsString() bool {
return false
}
func (tsr *Number[T]) AddScalar(i []int, val float64) float64 {
j := tsr.Shp.Offset(i)
tsr.Values[j] += T(val)
return float64(tsr.Values[j])
}
func (tsr *Number[T]) MulScalar(i []int, val float64) float64 {
j := tsr.Shp.Offset(i)
tsr.Values[j] *= T(val)
return float64(tsr.Values[j])
}
func (tsr *Number[T]) SetString(i []int, val string) {
if fv, err := strconv.ParseFloat(val, 64); err == nil {
j := tsr.Shp.Offset(i)
tsr.Values[j] = T(fv)
}
}
func (tsr Number[T]) SetString1D(off int, val string) {
if fv, err := strconv.ParseFloat(val, 64); err == nil {
tsr.Values[off] = T(fv)
}
}
func (tsr *Number[T]) SetStringRowCell(row, cell int, val string) {
if fv, err := strconv.ParseFloat(val, 64); err == nil {
_, sz := tsr.Shp.RowCellSize()
tsr.Values[row*sz+cell] = T(fv)
}
}
// String satisfies the fmt.Stringer interface for string of tensor data
func (tsr *Number[T]) String() string {
str := tsr.Label()
sz := len(tsr.Values)
if sz > 1000 {
return str
}
var b strings.Builder
b.WriteString(str)
b.WriteString("\n")
oddRow := true
rows, cols, _, _ := Projection2DShape(&tsr.Shp, oddRow)
for r := 0; r < rows; r++ {
rc, _ := Projection2DCoords(&tsr.Shp, oddRow, r, 0)
b.WriteString(fmt.Sprintf("%v: ", rc))
for c := 0; c < cols; c++ {
vl := Projection2DValue(tsr, oddRow, r, c)
b.WriteString(fmt.Sprintf("%7g ", vl))
}
b.WriteString("\n")
}
return b.String()
}
func (tsr *Number[T]) Float(i []int) float64 {
j := tsr.Shp.Offset(i)
return float64(tsr.Values[j])
}
func (tsr *Number[T]) SetFloat(i []int, val float64) {
j := tsr.Shp.Offset(i)
tsr.Values[j] = T(val)
}
func (tsr *Number[T]) Float1D(i int) float64 {
return float64(tsr.Values[i])
}
func (tsr *Number[T]) SetFloat1D(i int, val float64) {
tsr.Values[i] = T(val)
}
func (tsr *Number[T]) FloatRowCell(row, cell int) float64 {
_, sz := tsr.Shp.RowCellSize()
i := row*sz + cell
return float64(tsr.Values[i])
}
func (tsr *Number[T]) SetFloatRowCell(row, cell int, val float64) {
_, sz := tsr.Shp.RowCellSize()
tsr.Values[row*sz+cell] = T(val)
}
// Floats sets []float64 slice of all elements in the tensor
// (length is ensured to be sufficient).
// This can be used for all of the gonum/floats methods
// for basic math, gonum/stats, etc.
func (tsr *Number[T]) Floats(flt *[]float64) {
*flt = slicesx.SetLength(*flt, len(tsr.Values))
switch vals := any(tsr.Values).(type) {
case []float64:
copy(*flt, vals)
default:
for i, v := range tsr.Values {
(*flt)[i] = float64(v)
}
}
}
// SetFloats sets tensor values from a []float64 slice (copies values).
func (tsr *Number[T]) SetFloats(flt []float64) {
switch vals := any(tsr.Values).(type) {
case []float64:
copy(vals, flt)
default:
for i, v := range flt {
tsr.Values[i] = T(v)
}
}
}
// At is the gonum/mat.Matrix interface method for returning 2D matrix element at given
// row, column index. Assumes Row-major ordering and logs an error if NumDims < 2.
func (tsr *Number[T]) At(i, j int) float64 {
nd := tsr.NumDims()
if nd < 2 {
log.Println("tensor Dims gonum Matrix call made on Tensor with dims < 2")
return 0
} else if nd == 2 {
return tsr.Float([]int{i, j})
} else {
ix := make([]int, nd)
ix[nd-2] = i
ix[nd-1] = j
return tsr.Float(ix)
}
}
// T is the gonum/mat.Matrix transpose method.
// It performs an implicit transpose by returning the receiver inside a Transpose.
func (tsr *Number[T]) T() mat.Matrix {
return mat.Transpose{tsr}
}
// Range returns the min, max (and associated indexes, -1 = no values) for the tensor.
// This is needed for display and is thus in the core api in optimized form
// Other math operations can be done using gonum/floats package.
func (tsr *Number[T]) Range() (min, max float64, minIndex, maxIndex int) {
minIndex = -1
maxIndex = -1
for j, vl := range tsr.Values {
fv := float64(vl)
if math.IsNaN(fv) {
continue
}
if fv < min || minIndex < 0 {
min = fv
minIndex = j
}
if fv > max || maxIndex < 0 {
max = fv
maxIndex = j
}
}
return
}
// SetZeros is simple convenience function initialize all values to 0
func (tsr *Number[T]) SetZeros() {
for j := range tsr.Values {
tsr.Values[j] = 0
}
}
// Clone clones this tensor, creating a duplicate copy of itself with its
// own separate memory representation of all the values, and returns
// that as a Tensor (which can be converted into the known type as needed).
func (tsr *Number[T]) Clone() Tensor {
csr := NewNumberShape[T](&tsr.Shp)
copy(csr.Values, tsr.Values)
return csr
}
// CopyFrom copies all avail values from other tensor into this tensor, with an
// optimized implementation if the other tensor is of the same type, and
// otherwise it goes through appropriate standard type.
func (tsr *Number[T]) CopyFrom(frm Tensor) {
if fsm, ok := frm.(*Number[T]); ok {
copy(tsr.Values, fsm.Values)
return
}
sz := min(len(tsr.Values), frm.Len())
for i := 0; i < sz; i++ {
tsr.Values[i] = T(frm.Float1D(i))
}
}
// CopyShapeFrom copies just the shape from given source tensor
// calling SetShape with the shape params from source (see for more docs).
func (tsr *Number[T]) CopyShapeFrom(frm Tensor) {
tsr.SetShape(frm.Shape().Sizes, frm.Shape().Names...)
}
// CopyCellsFrom copies given range of values from other tensor into this tensor,
// using flat 1D indexes: to = starting index in this Tensor to start copying into,
// start = starting index on from Tensor to start copying from, and n = number of
// values to copy. Uses an optimized implementation if the other tensor is
// of the same type, and otherwise it goes through appropriate standard type.
func (tsr *Number[T]) CopyCellsFrom(frm Tensor, to, start, n int) {
if fsm, ok := frm.(*Number[T]); ok {
for i := 0; i < n; i++ {
tsr.Values[to+i] = fsm.Values[start+i]
}
return
}
for i := 0; i < n; i++ {
tsr.Values[to+i] = T(frm.Float1D(start + i))
}
}
// SubSpace returns a new tensor with innermost subspace at given
// offset(s) in outermost dimension(s) (len(offs) < NumDims).
// The new tensor points to the values of the this tensor (i.e., modifications
// will affect both), as its Values slice is a view onto the original (which
// is why only inner-most contiguous supsaces are supported).
// Use Clone() method to separate the two.
func (tsr *Number[T]) SubSpace(offs []int) Tensor {
b := tsr.subSpaceImpl(offs)
rt := &Number[T]{Base: *b}
return rt
}
// Copyright (c) 2024, 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 tensor
const (
// OddRow is for oddRow arguments to Projection2D functions,
// specifies that the odd dimension goes along the row.
OddRow = true
// OddColumn is for oddRow arguments to Projection2D functions,
// specifies that the odd dimension goes along the column.
OddColumn = false
)
// Projection2DShape returns the size of a 2D projection of the given tensor Shape,
// collapsing higher dimensions down to 2D (and 1D up to 2D).
// For any odd number of dimensions, the remaining outer-most dimension
// can either be multipliexed across the row or column, given the oddRow arg.
// Even multiples of inner-most dimensions are assumed to be row, then column.
// rowEx returns the number of "extra" (higher dimensional) rows
// and colEx returns the number of extra cols
func Projection2DShape(shp *Shape, oddRow bool) (rows, cols, rowEx, colEx int) {
if shp.Len() == 0 {
return 1, 1, 0, 0
}
nd := shp.NumDims()
switch nd {
case 1:
if oddRow {
return shp.DimSize(0), 1, 0, 0
} else {
return 1, shp.DimSize(0), 0, 0
}
case 2:
return shp.DimSize(0), shp.DimSize(1), 0, 0
case 3:
if oddRow {
return shp.DimSize(0) * shp.DimSize(1), shp.DimSize(2), shp.DimSize(0), 0
} else {
return shp.DimSize(1), shp.DimSize(0) * shp.DimSize(2), 0, shp.DimSize(0)
}
case 4:
return shp.DimSize(0) * shp.DimSize(2), shp.DimSize(1) * shp.DimSize(3), shp.DimSize(0), shp.DimSize(1)
case 5:
if oddRow {
return shp.DimSize(0) * shp.DimSize(1) * shp.DimSize(3), shp.DimSize(2) * shp.DimSize(4), shp.DimSize(0) * shp.DimSize(1), 0
} else {
return shp.DimSize(1) * shp.DimSize(3), shp.DimSize(0) * shp.DimSize(2) * shp.DimSize(4), 0, shp.DimSize(0) * shp.DimSize(1)
}
}
return 1, 1, 0, 0
}
// Projection2DIndex returns the flat 1D index for given row, col coords for a 2D projection
// of the given tensor shape, collapsing higher dimensions down to 2D (and 1D up to 2D).
// For any odd number of dimensions, the remaining outer-most dimension
// can either be multipliexed across the row or column, given the oddRow arg.
// Even multiples of inner-most dimensions are assumed to be row, then column.
func Projection2DIndex(shp *Shape, oddRow bool, row, col int) int {
nd := shp.NumDims()
switch nd {
case 1:
if oddRow {
return row
} else {
return col
}
case 2:
return shp.Offset([]int{row, col})
case 3:
if oddRow {
ny := shp.DimSize(1)
yy := row / ny
y := row % ny
return shp.Offset([]int{yy, y, col})
} else {
nx := shp.DimSize(2)
xx := col / nx
x := col % nx
return shp.Offset([]int{xx, row, x})
}
case 4:
ny := shp.DimSize(2)
yy := row / ny
y := row % ny
nx := shp.DimSize(3)
xx := col / nx
x := col % nx
return shp.Offset([]int{yy, xx, y, x})
case 5:
// todo: oddRows version!
nyy := shp.DimSize(1)
ny := shp.DimSize(3)
yyy := row / (nyy * ny)
yy := row % (nyy * ny)
y := yy % ny
yy = yy / ny
nx := shp.DimSize(4)
xx := col / nx
x := col % nx
return shp.Offset([]int{yyy, yy, xx, y, x})
}
return 0
}
// Projection2DCoords returns the corresponding full-dimensional coordinates
// that go into the given row, col coords for a 2D projection of the given tensor,
// collapsing higher dimensions down to 2D (and 1D up to 2D).
func Projection2DCoords(shp *Shape, oddRow bool, row, col int) (rowCoords, colCoords []int) {
idx := Projection2DIndex(shp, oddRow, row, col)
dims := shp.Index(idx)
nd := shp.NumDims()
switch nd {
case 1:
if oddRow {
return dims, []int{0}
} else {
return []int{0}, dims
}
case 2:
return dims[:1], dims[1:]
case 3:
if oddRow {
return dims[:2], dims[2:]
} else {
return dims[:1], dims[1:]
}
case 4:
return []int{dims[0], dims[2]}, []int{dims[1], dims[3]}
case 5:
if oddRow {
return []int{dims[0], dims[1], dims[3]}, []int{dims[2], dims[4]}
} else {
return []int{dims[1], dims[3]}, []int{dims[0], dims[2], dims[4]}
}
}
return nil, nil
}
// Projection2DValue returns the float64 value at given row, col coords for a 2D projection
// of the given tensor, collapsing higher dimensions down to 2D (and 1D up to 2D).
// For any odd number of dimensions, the remaining outer-most dimension
// can either be multipliexed across the row or column, given the oddRow arg.
// Even multiples of inner-most dimensions are assumed to be row, then column.
func Projection2DValue(tsr Tensor, oddRow bool, row, col int) float64 {
idx := Projection2DIndex(tsr.Shape(), oddRow, row, col)
return tsr.Float1D(idx)
}
// Projection2DString returns the string value at given row, col coords for a 2D projection
// of the given tensor, collapsing higher dimensions down to 2D (and 1D up to 2D).
// For any odd number of dimensions, the remaining outer-most dimension
// can either be multipliexed across the row or column, given the oddRow arg.
// Even multiples of inner-most dimensions are assumed to be row, then column.
func Projection2DString(tsr Tensor, oddRow bool, row, col int) string {
idx := Projection2DIndex(tsr.Shape(), oddRow, row, col)
return tsr.String1D(idx)
}
// Projection2DSet sets a float64 value at given row, col coords for a 2D projection
// of the given tensor, collapsing higher dimensions down to 2D (and 1D up to 2D).
// For any odd number of dimensions, the remaining outer-most dimension
// can either be multipliexed across the row or column, given the oddRow arg.
// Even multiples of inner-most dimensions are assumed to be row, then column.
func Projection2DSet(tsr Tensor, oddRow bool, row, col int, val float64) {
idx := Projection2DIndex(tsr.Shape(), oddRow, row, col)
tsr.SetFloat1D(idx, val)
}
// Projection2DSetString sets a string value at given row, col coords for a 2D projection
// of the given tensor, collapsing higher dimensions down to 2D (and 1D up to 2D).
// For any odd number of dimensions, the remaining outer-most dimension
// can either be multipliexed across the row or column, given the oddRow arg.
// Even multiples of inner-most dimensions are assumed to be row, then column.
func Projection2DSetString(tsr Tensor, oddRow bool, row, col int, val string) {
idx := Projection2DIndex(tsr.Shape(), oddRow, row, col)
tsr.SetString1D(idx, val)
}
// Copyright (c) 2024, 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 tensor
import (
"fmt"
"slices"
)
// Shape manages a tensor's shape information, including strides and dimension names
// and can compute the flat index into an underlying 1D data storage array based on an
// n-dimensional index (and vice-versa).
// Per C / Go / Python conventions, indexes are Row-Major, ordered from
// outer to inner left-to-right, so the inner-most is right-most.
type Shape struct {
// size per dimension
Sizes []int
// offsets for each dimension
Strides []int `display:"-"`
// names of each dimension
Names []string `display:"-"`
}
// NewShape returns a new shape with given sizes and optional dimension names.
// RowMajor ordering is used by default.
func NewShape(sizes []int, names ...string) *Shape {
sh := &Shape{}
sh.SetShape(sizes, names...)
return sh
}
// SetShape sets the shape size and optional names
// RowMajor ordering is used by default.
func (sh *Shape) SetShape(sizes []int, names ...string) {
sh.Sizes = slices.Clone(sizes)
sh.Strides = RowMajorStrides(sizes)
sh.Names = make([]string, len(sh.Sizes))
if len(names) == len(sizes) {
copy(sh.Names, names)
}
}
// CopyShape copies the shape parameters from another Shape struct.
// copies the data so it is not accidentally subject to updates.
func (sh *Shape) CopyShape(cp *Shape) {
sh.Sizes = slices.Clone(cp.Sizes)
sh.Strides = slices.Clone(cp.Strides)
sh.Names = slices.Clone(cp.Names)
}
// Len returns the total length of elements in the tensor
// (i.e., the product of the shape sizes)
func (sh *Shape) Len() int {
if len(sh.Sizes) == 0 {
return 0
}
o := int(1)
for _, v := range sh.Sizes {
o *= v
}
return int(o)
}
// NumDims returns the total number of dimensions.
func (sh *Shape) NumDims() int { return len(sh.Sizes) }
// DimSize returns the size of given dimension.
func (sh *Shape) DimSize(i int) int { return sh.Sizes[i] }
// DimName returns the name of given dimension.
func (sh *Shape) DimName(i int) string { return sh.Names[i] }
// DimByName returns the index of the given dimension name.
// returns -1 if not found.
func (sh *Shape) DimByName(name string) int {
for i, nm := range sh.Names {
if nm == name {
return i
}
}
return -1
}
// DimSizeByName returns the size of given dimension, specified by name.
// will crash if name not found.
func (sh *Shape) DimSizeByName(name string) int {
return sh.DimSize(sh.DimByName(name))
}
// IndexIsValid() returns true if given index is valid (within ranges for all dimensions)
func (sh *Shape) IndexIsValid(idx []int) bool {
if len(idx) != sh.NumDims() {
return false
}
for i, v := range sh.Sizes {
if idx[i] < 0 || idx[i] >= v {
return false
}
}
return true
}
// IsEqual returns true if this shape is same as other (does not compare names)
func (sh *Shape) IsEqual(oth *Shape) bool {
if !EqualInts(sh.Sizes, oth.Sizes) {
return false
}
if !EqualInts(sh.Strides, oth.Strides) {
return false
}
return true
}
// RowCellSize returns the size of the outer-most Row shape dimension,
// and the size of all the remaining inner dimensions (the "cell" size).
// Used for Tensors that are columns in a data table.
func (sh *Shape) RowCellSize() (rows, cells int) {
rows = sh.Sizes[0]
if len(sh.Sizes) == 1 {
cells = 1
} else {
cells = sh.Len() / rows
}
return
}
// Offset returns the "flat" 1D array index into an element at the given n-dimensional index.
// No checking is done on the length or size of the index values relative to the shape of the tensor.
func (sh *Shape) Offset(index []int) int {
var offset int
for i, v := range index {
offset += v * sh.Strides[i]
}
return offset
}
// Index returns the n-dimensional index from a "flat" 1D array index.
func (sh *Shape) Index(offset int) []int {
nd := len(sh.Sizes)
index := make([]int, nd)
rem := offset
for i := nd - 1; i >= 0; i-- {
s := sh.Sizes[i]
iv := rem % s
rem /= s
index[i] = iv
}
return index
}
// String satisfies the fmt.Stringer interface
func (sh *Shape) String() string {
str := "["
for i := range sh.Sizes {
nm := sh.Names[i]
if nm != "" {
str += nm + ": "
}
str += fmt.Sprintf("%d", sh.Sizes[i])
if i < len(sh.Sizes)-1 {
str += ", "
}
}
str += "]"
return str
}
// RowMajorStrides returns strides for sizes where the first dimension is outer-most
// and subsequent dimensions are progressively inner.
func RowMajorStrides(sizes []int) []int {
rem := int(1)
for _, v := range sizes {
rem *= v
}
if rem == 0 {
strides := make([]int, len(sizes))
rem := int(1)
for i := range strides {
strides[i] = rem
}
return strides
}
strides := make([]int, len(sizes))
for i, v := range sizes {
rem /= v
strides[i] = rem
}
return strides
}
// ColMajorStrides returns strides for sizes where the first dimension is inner-most
// and subsequent dimensions are progressively outer
func ColMajorStrides(sizes []int) []int {
total := int(1)
for _, v := range sizes {
if v == 0 {
strides := make([]int, len(sizes))
for i := range strides {
strides[i] = total
}
return strides
}
}
strides := make([]int, len(sizes))
for i, v := range sizes {
strides[i] = total
total *= v
}
return strides
}
// EqualInts compares two int slices and returns true if they are equal
func EqualInts(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// AddShapes returns a new shape by adding two shapes one after the other.
func AddShapes(shape1, shape2 *Shape) *Shape {
sh1 := shape1.Sizes
sh2 := shape2.Sizes
nsh := make([]int, len(sh1)+len(sh2))
copy(nsh, sh1)
copy(nsh[len(sh1):], sh2)
nms := make([]string, len(sh1)+len(sh2))
copy(nms, shape1.Names)
copy(nms[len(sh1):], shape2.Names)
return NewShape(nsh, nms...)
}
// Copyright (c) 2024, 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 clust
//go:generate core generate
import (
"fmt"
"math"
"math/rand"
"cogentcore.org/core/base/indent"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/stats/simat"
"cogentcore.org/core/tensor/stats/stats"
)
// Node is one node in the cluster
type Node struct {
// index into original distance matrix -- only valid for for terminal leaves
Index int
// distance for this node -- how far apart were all the kids from each other when this node was created -- is 0 for leaf nodes
Dist float64
// total aggregate distance from parents -- the X axis offset at which our cluster starts
ParDist float64
// y-axis value for this node -- if a parent, it is the average of its kids Y's, otherwise it counts down
Y float64
// child nodes under this one
Kids []*Node
}
// IsLeaf returns true if node is a leaf of the tree with no kids
func (nn *Node) IsLeaf() bool {
return len(nn.Kids) == 0
}
// Sprint prints to string
func (nn *Node) Sprint(smat *simat.SimMat, depth int) string {
if nn.IsLeaf() {
return smat.Rows[nn.Index] + " "
}
sv := fmt.Sprintf("\n%v%v: ", indent.Tabs(depth), nn.Dist)
for _, kn := range nn.Kids {
sv += kn.Sprint(smat, depth+1)
}
return sv
}
// Indexes collects all the indexes in this node
func (nn *Node) Indexes(ix []int, ctr *int) {
if nn.IsLeaf() {
ix[*ctr] = nn.Index
(*ctr)++
} else {
for _, kn := range nn.Kids {
kn.Indexes(ix, ctr)
}
}
}
// NewNode merges two nodes into a new node
func NewNode(na, nb *Node, dst float64) *Node {
nn := &Node{Dist: dst}
nn.Kids = []*Node{na, nb}
return nn
}
// Glom implements basic agglomerative clustering, based on a raw similarity matrix as given.
// This calls GlomInit to initialize the root node with all of the leaves, and the calls
// GlomClust to do the iterative clustering process. If you want to start with pre-defined
// initial clusters, then call GlomClust with a root node so-initialized.
// The smat.Mat matrix must be an tensor.Float64.
func Glom(smat *simat.SimMat, dfunc DistFunc) *Node {
ntot := smat.Mat.DimSize(0) // number of leaves
root := GlomInit(ntot)
return GlomClust(root, smat, dfunc)
}
// GlomStd implements basic agglomerative clustering, based on a raw similarity matrix as given.
// This calls GlomInit to initialize the root node with all of the leaves, and the calls
// GlomClust to do the iterative clustering process. If you want to start with pre-defined
// initial clusters, then call GlomClust with a root node so-initialized.
// The smat.Mat matrix must be an tensor.Float64.
// Std version uses std distance functions
func GlomStd(smat *simat.SimMat, std StdDists) *Node {
return Glom(smat, StdFunc(std))
}
// GlomInit returns a standard root node initialized with all of the leaves
func GlomInit(ntot int) *Node {
root := &Node{}
root.Kids = make([]*Node, ntot)
for i := 0; i < ntot; i++ {
root.Kids[i] = &Node{Index: i}
}
return root
}
// GlomClust does the iterative agglomerative clustering, based on a raw similarity matrix as given,
// using a root node that has already been initialized with the starting clusters (all of the
// leaves by default, but could be anything if you want to start with predefined clusters).
// The smat.Mat matrix must be an tensor.Float64.
func GlomClust(root *Node, smat *simat.SimMat, dfunc DistFunc) *Node {
ntot := smat.Mat.DimSize(0) // number of leaves
smatf := smat.Mat.(*tensor.Float64).Values
maxd := stats.Max64(smatf)
// indexes in each group
aidx := make([]int, ntot)
bidx := make([]int, ntot)
for {
var ma, mb []int
mval := math.MaxFloat64
for ai, ka := range root.Kids {
actr := 0
ka.Indexes(aidx, &actr)
aix := aidx[0:actr]
for bi := 0; bi < ai; bi++ {
kb := root.Kids[bi]
bctr := 0
kb.Indexes(bidx, &bctr)
bix := bidx[0:bctr]
dv := dfunc(aix, bix, ntot, maxd, smatf)
if dv < mval {
mval = dv
ma = []int{ai}
mb = []int{bi}
} else if dv == mval { // do all ties at same time
ma = append(ma, ai)
mb = append(mb, bi)
}
}
}
ni := 0
if len(ma) > 1 {
ni = rand.Intn(len(ma))
}
na := ma[ni]
nb := mb[ni]
// fmt.Printf("merging nodes at dist: %v: %v and %v\nA: %v\nB: %v\n", mval, na, nb, root.Kids[na].Sprint(smat, 0), root.Kids[nb].Sprint(smat, 0))
nn := NewNode(root.Kids[na], root.Kids[nb], mval)
for i := len(root.Kids) - 1; i >= 0; i-- {
if i == na || i == nb {
root.Kids = append(root.Kids[:i], root.Kids[i+1:]...)
}
}
root.Kids = append(root.Kids, nn)
if len(root.Kids) == 1 {
break
}
}
return root
}
// Copyright (c) 2024, 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 clust
import (
"math"
)
// DistFunc is a clustering distance function that evaluates aggregate distance
// between nodes, given the indexes of leaves in a and b clusters
// which are indexs into an ntot x ntot similarity (distance) matrix smat.
// maxd is the maximum distance value in the smat, which is needed by the
// ContrastDist function and perhaps others.
type DistFunc func(aix, bix []int, ntot int, maxd float64, smat []float64) float64
// MinDist is the minimum-distance or single-linkage weighting function for comparing
// two clusters a and b, given by their list of indexes.
// ntot is total number of nodes, and smat is the square similarity matrix [ntot x ntot].
func MinDist(aix, bix []int, ntot int, maxd float64, smat []float64) float64 {
md := math.MaxFloat64
for _, ai := range aix {
for _, bi := range bix {
d := smat[ai*ntot+bi]
if d < md {
md = d
}
}
}
return md
}
// MaxDist is the maximum-distance or complete-linkage weighting function for comparing
// two clusters a and b, given by their list of indexes.
// ntot is total number of nodes, and smat is the square similarity matrix [ntot x ntot].
func MaxDist(aix, bix []int, ntot int, maxd float64, smat []float64) float64 {
md := -math.MaxFloat64
for _, ai := range aix {
for _, bi := range bix {
d := smat[ai*ntot+bi]
if d > md {
md = d
}
}
}
return md
}
// AvgDist is the average-distance or average-linkage weighting function for comparing
// two clusters a and b, given by their list of indexes.
// ntot is total number of nodes, and smat is the square similarity matrix [ntot x ntot].
func AvgDist(aix, bix []int, ntot int, maxd float64, smat []float64) float64 {
md := 0.0
n := 0
for _, ai := range aix {
for _, bi := range bix {
d := smat[ai*ntot+bi]
md += d
n++
}
}
if n > 0 {
md /= float64(n)
}
return md
}
// ContrastDist computes maxd + (average within distance - average between distance)
// for two clusters a and b, given by their list of indexes.
// avg between is average distance between all items in a & b versus all outside that.
// ntot is total number of nodes, and smat is the square similarity matrix [ntot x ntot].
// maxd is the maximum distance and is needed to ensure distances are positive.
func ContrastDist(aix, bix []int, ntot int, maxd float64, smat []float64) float64 {
wd := AvgDist(aix, bix, ntot, maxd, smat)
nab := len(aix) + len(bix)
abix := append(aix, bix...)
abmap := make(map[int]struct{}, ntot-nab)
for _, ix := range abix {
abmap[ix] = struct{}{}
}
oix := make([]int, ntot-nab)
octr := 0
for ix := 0; ix < ntot; ix++ {
if _, has := abmap[ix]; !has {
oix[octr] = ix
octr++
}
}
bd := AvgDist(abix, oix, ntot, maxd, smat)
return maxd + (wd - bd)
}
// StdDists are standard clustering distance functions
type StdDists int32 //enums:enum
const (
// Min is the minimum-distance or single-linkage weighting function
Min StdDists = iota
// Max is the maximum-distance or complete-linkage weighting function
Max
// Avg is the average-distance or average-linkage weighting function
Avg
// Contrast computes maxd + (average within distance - average between distance)
Contrast
)
// StdFunc returns a standard distance function as specified
func StdFunc(std StdDists) DistFunc {
switch std {
case Min:
return MinDist
case Max:
return MaxDist
case Avg:
return AvgDist
case Contrast:
return ContrastDist
}
return nil
}
// Code generated by "core generate"; DO NOT EDIT.
package clust
import (
"cogentcore.org/core/enums"
)
var _StdDistsValues = []StdDists{0, 1, 2, 3}
// StdDistsN is the highest valid value for type StdDists, plus one.
const StdDistsN StdDists = 4
var _StdDistsValueMap = map[string]StdDists{`Min`: 0, `Max`: 1, `Avg`: 2, `Contrast`: 3}
var _StdDistsDescMap = map[StdDists]string{0: `Min is the minimum-distance or single-linkage weighting function`, 1: `Max is the maximum-distance or complete-linkage weighting function`, 2: `Avg is the average-distance or average-linkage weighting function`, 3: `Contrast computes maxd + (average within distance - average between distance)`}
var _StdDistsMap = map[StdDists]string{0: `Min`, 1: `Max`, 2: `Avg`, 3: `Contrast`}
// String returns the string representation of this StdDists value.
func (i StdDists) String() string { return enums.String(i, _StdDistsMap) }
// SetString sets the StdDists value from its string representation,
// and returns an error if the string is invalid.
func (i *StdDists) SetString(s string) error {
return enums.SetString(i, s, _StdDistsValueMap, "StdDists")
}
// Int64 returns the StdDists value as an int64.
func (i StdDists) Int64() int64 { return int64(i) }
// SetInt64 sets the StdDists value from an int64.
func (i *StdDists) SetInt64(in int64) { *i = StdDists(in) }
// Desc returns the description of the StdDists value.
func (i StdDists) Desc() string { return enums.Desc(i, _StdDistsDescMap) }
// StdDistsValues returns all possible values for the type StdDists.
func StdDistsValues() []StdDists { return _StdDistsValues }
// Values returns all possible values for the type StdDists.
func (i StdDists) Values() []enums.Enum { return enums.Values(_StdDistsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i StdDists) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *StdDists) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "StdDists") }
// Copyright (c) 2024, 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 clust
import (
"cogentcore.org/core/tensor/stats/simat"
"cogentcore.org/core/tensor/table"
)
// Plot sets the rows of given data table to trace out lines with labels that
// will render cluster plot starting at root node when plotted with a standard plotting package.
// The lines double-back on themselves to form a continuous line to be plotted.
func Plot(pt *table.Table, root *Node, smat *simat.SimMat) {
pt.DeleteAll()
pt.AddFloat64Column("X")
pt.AddFloat64Column("Y")
pt.AddStringColumn("Label")
nextY := 0.5
root.SetYs(&nextY)
root.SetParDist(0.0)
root.Plot(pt, smat)
}
// Plot sets the rows of given data table to trace out lines with labels that
// will render this node in a cluster plot when plotted with a standard plotting package.
// The lines double-back on themselves to form a continuous line to be plotted.
func (nn *Node) Plot(pt *table.Table, smat *simat.SimMat) {
row := pt.Rows
if nn.IsLeaf() {
pt.SetNumRows(row + 1)
pt.SetFloatIndex(0, row, nn.ParDist)
pt.SetFloatIndex(1, row, nn.Y)
if len(smat.Rows) > nn.Index {
pt.SetStringIndex(2, row, smat.Rows[nn.Index])
}
} else {
for _, kn := range nn.Kids {
pt.SetNumRows(row + 2)
pt.SetFloatIndex(0, row, nn.ParDist)
pt.SetFloatIndex(1, row, kn.Y)
row++
pt.SetFloatIndex(0, row, nn.ParDist+nn.Dist)
pt.SetFloatIndex(1, row, kn.Y)
kn.Plot(pt, smat)
row = pt.Rows
pt.SetNumRows(row + 1)
pt.SetFloatIndex(0, row, nn.ParDist)
pt.SetFloatIndex(1, row, kn.Y)
row++
}
pt.SetNumRows(row + 1)
pt.SetFloatIndex(0, row, nn.ParDist)
pt.SetFloatIndex(1, row, nn.Y)
}
}
// SetYs sets the Y-axis values for the nodes in preparation for plotting.
func (nn *Node) SetYs(nextY *float64) {
if nn.IsLeaf() {
nn.Y = *nextY
(*nextY) += 1.0
} else {
avgy := 0.0
for _, kn := range nn.Kids {
kn.SetYs(nextY)
avgy += kn.Y
}
avgy /= float64(len(nn.Kids))
nn.Y = avgy
}
}
// SetParDist sets the parent distance for the nodes in preparation for plotting.
func (nn *Node) SetParDist(pard float64) {
nn.ParDist = pard
if !nn.IsLeaf() {
pard += nn.Dist
for _, kn := range nn.Kids {
kn.SetParDist(pard)
}
}
}
// Copyright (c) 2024, 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 convolve
//go:generate core generate
import (
"errors"
"cogentcore.org/core/base/slicesx"
)
// Slice32 convolves given kernel with given source slice, putting results in
// destination, which is ensured to be the same size as the source slice,
// using existing capacity if available, and otherwise making a new slice.
// The kernel should be normalized, and odd-sized do it is symmetric about 0.
// Returns an error if sizes are not valid.
// No parallelization is used -- see Slice32Parallel for very large slices.
// Edges are handled separately with renormalized kernels -- they can be
// clipped from dest by excluding the kernel half-width from each end.
func Slice32(dest *[]float32, src []float32, kern []float32) error {
sz := len(src)
ksz := len(kern)
if ksz == 0 || sz == 0 {
return errors.New("convolve.Slice32: kernel or source are empty")
}
if ksz%2 == 0 {
return errors.New("convolve.Slice32: kernel is not odd sized")
}
if sz < ksz {
return errors.New("convolve.Slice32: source must be > kernel in size")
}
khalf := (ksz - 1) / 2
*dest = slicesx.SetLength(*dest, sz)
for i := khalf; i < sz-khalf; i++ {
var sum float32
for j := 0; j < ksz; j++ {
sum += src[(i-khalf)+j] * kern[j]
}
(*dest)[i] = sum
}
for i := 0; i < khalf; i++ {
var sum, ksum float32
for j := 0; j <= khalf+i; j++ {
ki := (j + khalf) - i // 0: 1+kh, 1: etc
si := i + (ki - khalf)
// fmt.Printf("i: %d j: %d ki: %d si: %d\n", i, j, ki, si)
sum += src[si] * kern[ki]
ksum += kern[ki]
}
(*dest)[i] = sum / ksum
}
for i := sz - khalf; i < sz; i++ {
var sum, ksum float32
ei := sz - i - 1
for j := 0; j <= khalf+ei; j++ {
ki := ((ksz - 1) - (j + khalf)) + ei
si := i + (ki - khalf)
// fmt.Printf("i: %d j: %d ki: %d si: %d ei: %d\n", i, j, ki, si, ei)
sum += src[si] * kern[ki]
ksum += kern[ki]
}
(*dest)[i] = sum / ksum
}
return nil
}
// Slice64 convolves given kernel with given source slice, putting results in
// destination, which is ensured to be the same size as the source slice,
// using existing capacity if available, and otherwise making a new slice.
// The kernel should be normalized, and odd-sized do it is symmetric about 0.
// Returns an error if sizes are not valid.
// No parallelization is used -- see Slice64Parallel for very large slices.
// Edges are handled separately with renormalized kernels -- they can be
// clipped from dest by excluding the kernel half-width from each end.
func Slice64(dest *[]float64, src []float64, kern []float64) error {
sz := len(src)
ksz := len(kern)
if ksz == 0 || sz == 0 {
return errors.New("convolve.Slice64: kernel or source are empty")
}
if ksz%2 == 0 {
return errors.New("convolve.Slice64: kernel is not odd sized")
}
if sz < ksz {
return errors.New("convolve.Slice64: source must be > kernel in size")
}
khalf := (ksz - 1) / 2
*dest = slicesx.SetLength(*dest, sz)
for i := khalf; i < sz-khalf; i++ {
var sum float64
for j := 0; j < ksz; j++ {
sum += src[(i-khalf)+j] * kern[j]
}
(*dest)[i] = sum
}
for i := 0; i < khalf; i++ {
var sum, ksum float64
for j := 0; j <= khalf+i; j++ {
ki := (j + khalf) - i // 0: 1+kh, 1: etc
si := i + (ki - khalf)
// fmt.Printf("i: %d j: %d ki: %d si: %d\n", i, j, ki, si)
sum += src[si] * kern[ki]
ksum += kern[ki]
}
(*dest)[i] = sum / ksum
}
for i := sz - khalf; i < sz; i++ {
var sum, ksum float64
ei := sz - i - 1
for j := 0; j <= khalf+ei; j++ {
ki := ((ksz - 1) - (j + khalf)) + ei
si := i + (ki - khalf)
// fmt.Printf("i: %d j: %d ki: %d si: %d ei: %d\n", i, j, ki, si, ei)
sum += src[si] * kern[ki]
ksum += kern[ki]
}
(*dest)[i] = sum / ksum
}
return nil
}
// Copyright (c) 2024, 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 convolve
import (
"math"
"cogentcore.org/core/math32"
)
// GaussianKernel32 returns a normalized gaussian kernel for smoothing
// with given half-width and normalized sigma (actual sigma = khalf * sigma).
// A sigma value of .5 is typical for smaller half-widths for containing
// most of the gaussian efficiently -- anything lower than .33 is inefficient --
// generally just use a lower half-width instead.
func GaussianKernel32(khalf int, sigma float32) []float32 {
ksz := khalf*2 + 1
kern := make([]float32, ksz)
sigdiv := 1 / (sigma * float32(khalf))
var sum float32
for i := 0; i < ksz; i++ {
x := sigdiv * float32(i-khalf)
kv := math32.Exp(-0.5 * x * x)
kern[i] = kv
sum += kv
}
nfac := 1 / sum
for i := 0; i < ksz; i++ {
kern[i] *= nfac
}
return kern
}
// GaussianKernel64 returns a normalized gaussian kernel
// with given half-width and normalized sigma (actual sigma = khalf * sigma)
// A sigma value of .5 is typical for smaller half-widths for containing
// most of the gaussian efficiently -- anything lower than .33 is inefficient --
// generally just use a lower half-width instead.
func GaussianKernel64(khalf int, sigma float64) []float64 {
ksz := khalf*2 + 1
kern := make([]float64, ksz)
sigdiv := 1 / (sigma * float64(khalf))
var sum float64
for i := 0; i < ksz; i++ {
x := sigdiv * float64(i-khalf)
kv := math.Exp(-0.5 * x * x)
kern[i] = kv
sum += kv
}
nfac := 1 / sum
for i := 0; i < ksz; i++ {
kern[i] *= nfac
}
return kern
}
// Copyright (c) 2024, 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 convolve
import (
"reflect"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/table"
)
// SmoothTable returns a cloned table with each of the floating-point
// columns in the source table smoothed over rows.
// khalf is the half-width of the Gaussian smoothing kernel,
// where larger values produce more smoothing. A sigma of .5
// is used for the kernel.
func SmoothTable(src *table.Table, khalf int) *table.Table {
k64 := GaussianKernel64(khalf, .5)
k32 := GaussianKernel32(khalf, .5)
dest := src.Clone()
for ci, sci := range src.Columns {
switch sci.DataType() {
case reflect.Float32:
sc := sci.(*tensor.Float32)
dc := dest.Columns[ci].(*tensor.Float32)
Slice32(&dc.Values, sc.Values, k32)
case reflect.Float64:
sc := sci.(*tensor.Float64)
dc := dest.Columns[ci].(*tensor.Float64)
Slice64(&dc.Values, sc.Values, k64)
}
}
return dest
}
// Copyright (c) 2024, 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 glm
import (
"fmt"
"math"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/table"
)
// todo: add tests
// GLM contains results and parameters for running a general
// linear model, which is a general form of multivariate linear
// regression, supporting multiple independent and dependent
// variables. Make a NewGLM and then do Run() on a tensor
// table.IndexView with the relevant data in columns of the table.
// Batch-mode gradient descent is used and the relevant parameters
// can be altered from defaults before calling Run as needed.
type GLM struct {
// Coeff are the coefficients to map from input independent variables
// to the dependent variables. The first, outer dimension is number of
// dependent variables, and the second, inner dimension is number of
// independent variables plus one for the offset (b) (last element).
Coeff tensor.Float64
// mean squared error of the fitted values relative to data
MSE float64
// R2 is the r^2 total variance accounted for by the linear model,
// for each dependent variable = 1 - (ErrVariance / ObsVariance)
R2 []float64
// Observed variance of each of the dependent variables to be predicted.
ObsVariance []float64
// Variance of the error residuals per dependent variables
ErrVariance []float64
// optional names of the independent variables, for reporting results
IndepNames []string
// optional names of the dependent variables, for reporting results
DepNames []string
///////////////////////////////////////////
// Parameters for the GLM model fitting:
// ZeroOffset restricts the offset of the linear function to 0,
// forcing it to pass through the origin. Otherwise, a constant offset "b"
// is fit during the model fitting process.
ZeroOffset bool
// learning rate parameter, which can be adjusted to reduce iterations based on
// specific properties of the data, but the default is reasonable for most "typical" data.
LRate float64 `default:"0.1"`
// tolerance on difference in mean squared error (MSE) across iterations to stop
// iterating and consider the result to be converged.
StopTolerance float64 `default:"0.0001"`
// Constant cost factor subtracted from weights, for the L1 norm or "Lasso"
// regression. This is good for producing sparse results but can arbitrarily
// select one of multiple correlated independent variables.
L1Cost float64
// Cost factor proportional to the coefficient value, for the L2 norm or "Ridge"
// regression. This is good for generally keeping weights small and equally
// penalizes correlated independent variables.
L2Cost float64
// CostStartIter is the iteration when we start applying the L1, L2 Cost factors.
// It is often a good idea to have a few unconstrained iterations prior to
// applying the cost factors.
CostStartIter int `default:"5"`
// maximum number of iterations to perform
MaxIters int `default:"50"`
///////////////////////////////////////////
// Cached values from the table
// Table of data
Table *table.IndexView
// tensor columns from table with the respective variables
IndepVars, DepVars, PredVars, ErrVars tensor.Tensor
// Number of independent and dependent variables
NIndepVars, NDepVars int
}
func NewGLM() *GLM {
glm := &GLM{}
glm.Defaults()
return glm
}
func (glm *GLM) Defaults() {
glm.LRate = 0.1
glm.StopTolerance = 0.001
glm.MaxIters = 50
glm.CostStartIter = 5
}
func (glm *GLM) init(nIv, nDv int) {
glm.NIndepVars = nIv
glm.NDepVars = nDv
glm.Coeff.SetShape([]int{nDv, nIv + 1}, "DepVars", "IndepVars")
glm.R2 = make([]float64, nDv)
glm.ObsVariance = make([]float64, nDv)
glm.ErrVariance = make([]float64, nDv)
glm.IndepNames = make([]string, nIv)
glm.DepNames = make([]string, nDv)
}
// SetTable sets the data to use from given indexview of table, where
// each of the Vars args specifies a column in the table, which can have either a
// single scalar value for each row, or a tensor cell with multiple values.
// predVars and errVars (predicted values and error values) are optional.
func (glm *GLM) SetTable(ix *table.IndexView, indepVars, depVars, predVars, errVars string) error {
dt := ix.Table
iv, err := dt.ColumnByName(indepVars)
if err != nil {
return err
}
dv, err := dt.ColumnByName(depVars)
if err != nil {
return err
}
var pv, ev tensor.Tensor
if predVars != "" {
pv, err = dt.ColumnByName(predVars)
if err != nil {
return err
}
}
if errVars != "" {
ev, err = dt.ColumnByName(errVars)
if err != nil {
return err
}
}
if pv != nil && !pv.Shape().IsEqual(dv.Shape()) {
return fmt.Errorf("predVars must have same shape as depVars")
}
if ev != nil && !ev.Shape().IsEqual(dv.Shape()) {
return fmt.Errorf("errVars must have same shape as depVars")
}
_, nIv := iv.RowCellSize()
_, nDv := dv.RowCellSize()
glm.init(nIv, nDv)
glm.Table = ix
glm.IndepVars = iv
glm.DepVars = dv
glm.PredVars = pv
glm.ErrVars = ev
return nil
}
// Run performs the multi-variate linear regression using data SetTable function,
// learning linear coefficients and an overall static offset that best
// fits the observed dependent variables as a function of the independent variables.
// Initial values of the coefficients, and other parameters for the regression,
// should be set prior to running.
func (glm *GLM) Run() {
ix := glm.Table
iv := glm.IndepVars
dv := glm.DepVars
pv := glm.PredVars
ev := glm.ErrVars
if pv == nil {
pv = dv.Clone()
}
if ev == nil {
ev = dv.Clone()
}
nDv := glm.NDepVars
nIv := glm.NIndepVars
nCi := nIv + 1
dc := glm.Coeff.Clone().(*tensor.Float64)
lastItr := false
sse := 0.0
prevmse := 0.0
n := ix.Len()
norm := 1.0 / float64(n)
lrate := norm * glm.LRate
for itr := 0; itr < glm.MaxIters; itr++ {
for i := range dc.Values {
dc.Values[i] = 0
}
sse = 0
if (itr+1)%10 == 0 {
lrate *= 0.5
}
for i := 0; i < n; i++ {
row := ix.Indexes[i]
for di := 0; di < nDv; di++ {
pred := 0.0
for ii := 0; ii < nIv; ii++ {
pred += glm.Coeff.Value([]int{di, ii}) * iv.FloatRowCell(row, ii)
}
if !glm.ZeroOffset {
pred += glm.Coeff.Value([]int{di, nIv})
}
targ := dv.FloatRowCell(row, di)
err := targ - pred
sse += err * err
for ii := 0; ii < nIv; ii++ {
dc.Values[di*nCi+ii] += err * iv.FloatRowCell(row, ii)
}
if !glm.ZeroOffset {
dc.Values[di*nCi+nIv] += err
}
if lastItr {
pv.SetFloatRowCell(row, di, pred)
if ev != nil {
ev.SetFloatRowCell(row, di, err)
}
}
}
}
for di := 0; di < nDv; di++ {
for ii := 0; ii <= nIv; ii++ {
if glm.ZeroOffset && ii == nIv {
continue
}
idx := di*(nCi+1) + ii
w := glm.Coeff.Values[idx]
d := dc.Values[idx]
sgn := 1.0
if w < 0 {
sgn = -1.0
} else if w == 0 {
sgn = 0
}
glm.Coeff.Values[idx] += lrate * (d - glm.L1Cost*sgn - glm.L2Cost*w)
}
}
glm.MSE = norm * sse
if lastItr {
break
}
if itr > 0 {
dmse := glm.MSE - prevmse
if math.Abs(dmse) < glm.StopTolerance || itr == glm.MaxIters-2 {
lastItr = true
}
}
fmt.Println(itr, glm.MSE)
prevmse = glm.MSE
}
obsMeans := make([]float64, nDv)
errMeans := make([]float64, nDv)
for i := 0; i < n; i++ {
row := ix.Indexes[i]
for di := 0; di < nDv; di++ {
obsMeans[di] += dv.FloatRowCell(row, di)
errMeans[di] += ev.FloatRowCell(row, di)
}
}
for di := 0; di < nDv; di++ {
obsMeans[di] *= norm
errMeans[di] *= norm
glm.ObsVariance[di] = 0
glm.ErrVariance[di] = 0
}
for i := 0; i < n; i++ {
row := ix.Indexes[i]
for di := 0; di < nDv; di++ {
o := dv.FloatRowCell(row, di) - obsMeans[di]
glm.ObsVariance[di] += o * o
e := ev.FloatRowCell(row, di) - errMeans[di]
glm.ErrVariance[di] += e * e
}
}
for di := 0; di < nDv; di++ {
glm.ObsVariance[di] *= norm
glm.ErrVariance[di] *= norm
glm.R2[di] = 1.0 - (glm.ErrVariance[di] / glm.ObsVariance[di])
}
}
// Variance returns a description of the variance accounted for by the regression
// equation, R^2, for each dependent variable, along with the variances of
// observed and errors (residuals), which are used to compute it.
func (glm *GLM) Variance() string {
str := ""
for di := range glm.R2 {
if len(glm.DepNames) > di && glm.DepNames[di] != "" {
str += glm.DepNames[di]
} else {
str += fmt.Sprintf("DV %d", di)
}
str += fmt.Sprintf("\tR^2: %8.6g\tR: %8.6g\tVar Err: %8.4g\t Obs: %8.4g\n", glm.R2[di], math.Sqrt(glm.R2[di]), glm.ErrVariance[di], glm.ObsVariance[di])
}
return str
}
// Coeffs returns a string describing the coefficients
func (glm *GLM) Coeffs() string {
str := ""
for di := range glm.NDepVars {
if len(glm.DepNames) > di && glm.DepNames[di] != "" {
str += glm.DepNames[di]
} else {
str += fmt.Sprintf("DV %d", di)
}
str += " = "
for ii := 0; ii <= glm.NIndepVars; ii++ {
str += fmt.Sprintf("\t%8.6g", glm.Coeff.Value([]int{di, ii}))
if ii < glm.NIndepVars {
str += " * "
if len(glm.IndepNames) > ii && glm.IndepNames[di] != "" {
str += glm.IndepNames[di]
} else {
str += fmt.Sprintf("IV_%d", ii)
}
str += " + "
}
}
str += "\n"
}
return str
}
// Copyright (c) 2024, 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 histogram
//go:generate core generate
import (
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/math32"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/table"
)
// F64 generates a histogram of counts of values within given
// number of bins and min / max range. hist vals is sized to nBins.
// if value is < min or > max it is ignored.
func F64(hist *[]float64, vals []float64, nBins int, min, max float64) {
*hist = slicesx.SetLength(*hist, nBins)
h := *hist
// 0.1.2.3 = 3-0 = 4 bins
inc := (max - min) / float64(nBins)
for i := 0; i < nBins; i++ {
h[i] = 0
}
for _, v := range vals {
if v < min || v > max {
continue
}
bin := int((v - min) / inc)
if bin >= nBins {
bin = nBins - 1
}
h[bin] += 1
}
}
// F64Table generates an table with a histogram of counts of values within given
// number of bins and min / max range. The table has columns: Value, Count
// if value is < min or > max it is ignored.
// The Value column represents the min value for each bin, with the max being
// the value of the next bin, or the max if at the end.
func F64Table(dt *table.Table, vals []float64, nBins int, min, max float64) {
dt.DeleteAll()
dt.AddFloat64Column("Value")
dt.AddFloat64Column("Count")
dt.SetNumRows(nBins)
ct := dt.Columns[1].(*tensor.Float64)
F64(&ct.Values, vals, nBins, min, max)
inc := (max - min) / float64(nBins)
vls := dt.Columns[0].(*tensor.Float64).Values
for i := 0; i < nBins; i++ {
vls[i] = math32.Truncate64(min+float64(i)*inc, 4)
}
}
//////////////////////////////////////////////////////
// float32
// F32 generates a histogram of counts of values within given
// number of bins and min / max range. hist vals is sized to nBins.
// if value is < min or > max it is ignored.
func F32(hist *[]float32, vals []float32, nBins int, min, max float32) {
*hist = slicesx.SetLength(*hist, nBins)
h := *hist
// 0.1.2.3 = 3-0 = 4 bins
inc := (max - min) / float32(nBins)
for i := 0; i < nBins; i++ {
h[i] = 0
}
for _, v := range vals {
if v < min || v > max {
continue
}
bin := int((v - min) / inc)
if bin >= nBins {
bin = nBins - 1
}
h[bin] += 1
}
}
// F32Table generates an table with a histogram of counts of values within given
// number of bins and min / max range. The table has columns: Value, Count
// if value is < min or > max it is ignored.
// The Value column represents the min value for each bin, with the max being
// the value of the next bin, or the max if at the end.
func F32Table(dt *table.Table, vals []float32, nBins int, min, max float32) {
dt.DeleteAll()
dt.AddFloat32Column("Value")
dt.AddFloat32Column("Count")
dt.SetNumRows(nBins)
ct := dt.Columns[1].(*tensor.Float32)
F32(&ct.Values, vals, nBins, min, max)
inc := (max - min) / float32(nBins)
vls := dt.Columns[0].(*tensor.Float32).Values
for i := 0; i < nBins; i++ {
vls[i] = math32.Truncate(min+float32(i)*inc, 4)
}
}
// Copyright (c) 2024, 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 metric
import (
"math"
"cogentcore.org/core/math32"
)
///////////////////////////////////////////
// Abs
// Abs32 computes the sum of absolute value of differences (L1 Norm).
// Skips NaN's and panics if lengths are not equal.
func Abs32(a, b []float32) float32 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
ss := float32(0)
for i, av := range a {
bv := b[i]
if math32.IsNaN(av) || math32.IsNaN(bv) {
continue
}
ss += math32.Abs(av - bv)
}
return ss
}
// Abs64 computes the sum of absolute value of differences (L1 Norm).
// Skips NaN's and panics if lengths are not equal.
func Abs64(a, b []float64) float64 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
ss := float64(0)
for i, av := range a {
bv := b[i]
if math.IsNaN(av) || math.IsNaN(bv) {
continue
}
ss += math.Abs(av - bv)
}
return ss
}
///////////////////////////////////////////
// Hamming
// Hamming32 computes the sum of 1's for every element that is different
// (city block).
// Skips NaN's and panics if lengths are not equal.
func Hamming32(a, b []float32) float32 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
ss := float32(0)
for i, av := range a {
bv := b[i]
if math32.IsNaN(av) || math32.IsNaN(bv) {
continue
}
if av != bv {
ss += 1
}
}
return ss
}
// Hamming64 computes the sum of absolute value of differences (L1 Norm).
// Skips NaN's and panics if lengths are not equal.
func Hamming64(a, b []float64) float64 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
ss := float64(0)
for i, av := range a {
bv := b[i]
if math.IsNaN(av) || math.IsNaN(bv) {
continue
}
if av != bv {
ss += 1
}
}
return ss
}
// Code generated by "core generate"; DO NOT EDIT.
package metric
import (
"cogentcore.org/core/enums"
)
var _StdMetricsValues = []StdMetrics{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
// StdMetricsN is the highest valid value for type StdMetrics, plus one.
const StdMetricsN StdMetrics = 13
var _StdMetricsValueMap = map[string]StdMetrics{`Euclidean`: 0, `SumSquares`: 1, `Abs`: 2, `Hamming`: 3, `EuclideanBinTol`: 4, `SumSquaresBinTol`: 5, `InvCosine`: 6, `InvCorrelation`: 7, `CrossEntropy`: 8, `InnerProduct`: 9, `Covariance`: 10, `Correlation`: 11, `Cosine`: 12}
var _StdMetricsDescMap = map[StdMetrics]string{0: ``, 1: ``, 2: ``, 3: ``, 4: ``, 5: ``, 6: `InvCosine is 1-Cosine -- useful to convert into an Increasing metric`, 7: `InvCorrelation is 1-Correlation -- useful to convert into an Increasing metric`, 8: ``, 9: `Everything below here is !Increasing -- larger = closer, not farther`, 10: ``, 11: ``, 12: ``}
var _StdMetricsMap = map[StdMetrics]string{0: `Euclidean`, 1: `SumSquares`, 2: `Abs`, 3: `Hamming`, 4: `EuclideanBinTol`, 5: `SumSquaresBinTol`, 6: `InvCosine`, 7: `InvCorrelation`, 8: `CrossEntropy`, 9: `InnerProduct`, 10: `Covariance`, 11: `Correlation`, 12: `Cosine`}
// String returns the string representation of this StdMetrics value.
func (i StdMetrics) String() string { return enums.String(i, _StdMetricsMap) }
// SetString sets the StdMetrics value from its string representation,
// and returns an error if the string is invalid.
func (i *StdMetrics) SetString(s string) error {
return enums.SetString(i, s, _StdMetricsValueMap, "StdMetrics")
}
// Int64 returns the StdMetrics value as an int64.
func (i StdMetrics) Int64() int64 { return int64(i) }
// SetInt64 sets the StdMetrics value from an int64.
func (i *StdMetrics) SetInt64(in int64) { *i = StdMetrics(in) }
// Desc returns the description of the StdMetrics value.
func (i StdMetrics) Desc() string { return enums.Desc(i, _StdMetricsDescMap) }
// StdMetricsValues returns all possible values for the type StdMetrics.
func StdMetricsValues() []StdMetrics { return _StdMetricsValues }
// Values returns all possible values for the type StdMetrics.
func (i StdMetrics) Values() []enums.Enum { return enums.Values(_StdMetricsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i StdMetrics) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *StdMetrics) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "StdMetrics")
}
// Copyright (c) 2024, Cogent Core. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate core generate
package metric
// Func32 is a distance / similarity metric operating on slices of float32 numbers
type Func32 func(a, b []float32) float32
// Func64 is a distance / similarity metric operating on slices of float64 numbers
type Func64 func(a, b []float64) float64
// StdMetrics are standard metric functions
type StdMetrics int32 //enums:enum
const (
Euclidean StdMetrics = iota
SumSquares
Abs
Hamming
EuclideanBinTol
SumSquaresBinTol
// InvCosine is 1-Cosine -- useful to convert into an Increasing metric
InvCosine
// InvCorrelation is 1-Correlation -- useful to convert into an Increasing metric
InvCorrelation
CrossEntropy
// Everything below here is !Increasing -- larger = closer, not farther
InnerProduct
Covariance
Correlation
Cosine
)
// Increasing returns true if the distance metric is such that metric
// values increase as a function of distance (e.g., Euclidean)
// and false if metric values decrease as a function of distance
// (e.g., Cosine, Correlation)
func Increasing(std StdMetrics) bool {
if std >= InnerProduct {
return false
}
return true
}
// StdFunc32 returns a standard metric function as specified
func StdFunc32(std StdMetrics) Func32 {
switch std {
case Euclidean:
return Euclidean32
case SumSquares:
return SumSquares32
case Abs:
return Abs32
case Hamming:
return Hamming32
case EuclideanBinTol:
return EuclideanBinTol32
case SumSquaresBinTol:
return SumSquaresBinTol32
case InvCorrelation:
return InvCorrelation32
case InvCosine:
return InvCosine32
case CrossEntropy:
return CrossEntropy32
case InnerProduct:
return InnerProduct32
case Covariance:
return Covariance32
case Correlation:
return Correlation32
case Cosine:
return Cosine32
}
return nil
}
// StdFunc64 returns a standard metric function as specified
func StdFunc64(std StdMetrics) Func64 {
switch std {
case Euclidean:
return Euclidean64
case SumSquares:
return SumSquares64
case Abs:
return Abs64
case Hamming:
return Hamming64
case EuclideanBinTol:
return EuclideanBinTol64
case SumSquaresBinTol:
return SumSquaresBinTol64
case InvCorrelation:
return InvCorrelation64
case InvCosine:
return InvCosine64
case CrossEntropy:
return CrossEntropy64
case InnerProduct:
return InnerProduct64
case Covariance:
return Covariance64
case Correlation:
return Correlation64
case Cosine:
return Cosine64
}
return nil
}
// Copyright (c) 2024, 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 metric
import (
"math"
"cogentcore.org/core/math32"
)
///////////////////////////////////////////
// CrossEntropy
// CrossEntropy32 computes cross-entropy between the two vectors.
// Skips NaN's and panics if lengths are not equal.
func CrossEntropy32(a, b []float32) float32 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
ss := float32(0)
for i, av := range a {
bv := b[i]
if math32.IsNaN(av) || math32.IsNaN(bv) {
continue
}
bv = math32.Max(bv, 0.000001)
bv = math32.Min(bv, 0.999999)
if av >= 1.0 {
ss += -math32.Log(bv)
} else if av <= 0.0 {
ss += -math32.Log(1.0 - bv)
} else {
ss += av*math32.Log(av/bv) + (1-av)*math32.Log((1-av)/(1-bv))
}
}
return ss
}
// CrossEntropy64 computes the cross-entropy between the two vectors.
// Skips NaN's and panics if lengths are not equal.
func CrossEntropy64(a, b []float64) float64 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
ss := float64(0)
for i, av := range a {
bv := b[i]
if math.IsNaN(av) || math.IsNaN(bv) {
continue
}
bv = math.Max(bv, 0.000001)
bv = math.Min(bv, 0.999999)
if av >= 1.0 {
ss += -math.Log(bv)
} else if av <= 0.0 {
ss += -math.Log(1.0 - bv)
} else {
ss += av*math.Log(av/bv) + (1-av)*math.Log((1-av)/(1-bv))
}
}
return ss
}
// Copyright (c) 2024, 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 metric
import (
"math"
"cogentcore.org/core/math32"
"cogentcore.org/core/tensor/stats/stats"
)
///////////////////////////////////////////
// SumSquares
// SumSquares32 computes the sum-of-squares distance between two vectors.
// Skips NaN's and panics if lengths are not equal.
// Uses optimized algorithm from BLAS that avoids numerical overflow.
func SumSquares32(a, b []float32) float32 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
n := len(a)
if n < 2 {
if n == 1 {
return math32.Abs(a[0] - b[0])
}
return 0
}
var (
scale float32 = 0
sumSquares float32 = 1
)
for i, av := range a {
bv := b[i]
if av == bv || math32.IsNaN(av) || math32.IsNaN(bv) {
continue
}
absxi := math32.Abs(av - bv)
if scale < absxi {
sumSquares = 1 + sumSquares*(scale/absxi)*(scale/absxi)
scale = absxi
} else {
sumSquares = sumSquares + (absxi/scale)*(absxi/scale)
}
}
if math32.IsInf(scale, 1) {
return math32.Inf(1)
}
return scale * scale * sumSquares
}
// SumSquares64 computes the sum-of-squares distance between two vectors.
// Skips NaN's and panics if lengths are not equal.
// Uses optimized algorithm from BLAS that avoids numerical overflow.
func SumSquares64(a, b []float64) float64 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
n := len(a)
if n < 2 {
if n == 1 {
return math.Abs(a[0] - b[0])
}
return 0
}
var (
scale float64 = 0
sumSquares float64 = 1
)
for i, av := range a {
bv := b[i]
if av == bv || math.IsNaN(av) || math.IsNaN(bv) {
continue
}
absxi := math.Abs(av - bv)
if scale < absxi {
sumSquares = 1 + sumSquares*(scale/absxi)*(scale/absxi)
scale = absxi
} else {
sumSquares = sumSquares + (absxi/scale)*(absxi/scale)
}
}
if math.IsInf(scale, 1) {
return math.Inf(1)
}
return scale * scale * sumSquares
}
///////////////////////////////////////////
// SumSquaresBinTol
// SumSquaresBinTol32 computes the sum-of-squares distance between two vectors.
// Skips NaN's and panics if lengths are not equal.
// Uses optimized algorithm from BLAS that avoids numerical overflow.
// BinTol version uses binary tolerance for 0-1 valued-vectors where
// abs diff < .5 counts as 0 error (i.e., closer than not).
func SumSquaresBinTol32(a, b []float32) float32 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
n := len(a)
if n < 2 {
if n == 1 {
return math32.Abs(a[0] - b[0])
}
return 0
}
var (
scale float32 = 0
sumSquares float32 = 1
)
for i, av := range a {
bv := b[i]
if av == bv || math32.IsNaN(av) || math32.IsNaN(bv) {
continue
}
absxi := math32.Abs(av - bv)
if absxi < 0.5 {
continue
}
if scale < absxi {
sumSquares = 1 + sumSquares*(scale/absxi)*(scale/absxi)
scale = absxi
} else {
sumSquares = sumSquares + (absxi/scale)*(absxi/scale)
}
}
if math32.IsInf(scale, 1) {
return math32.Inf(1)
}
return scale * scale * sumSquares
}
// SumSquaresBinTol64 computes the sum-of-squares distance between two vectors.
// Skips NaN's and panics if lengths are not equal.
// Uses optimized algorithm from BLAS that avoids numerical overflow.
// BinTol version uses binary tolerance for 0-1 valued-vectors where
// abs diff < .5 counts as 0 error (i.e., closer than not).
func SumSquaresBinTol64(a, b []float64) float64 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
n := len(a)
if n < 2 {
if n == 1 {
return math.Abs(a[0] - b[0])
}
return 0
}
var (
scale float64 = 0
sumSquares float64 = 1
)
for i, av := range a {
bv := b[i]
if av == bv || math.IsNaN(av) || math.IsNaN(bv) {
continue
}
absxi := math.Abs(av - bv)
if absxi < 0.5 {
continue
}
if scale < absxi {
sumSquares = 1 + sumSquares*(scale/absxi)*(scale/absxi)
scale = absxi
} else {
sumSquares = sumSquares + (absxi/scale)*(absxi/scale)
}
}
if math.IsInf(scale, 1) {
return math.Inf(1)
}
return scale * scale * sumSquares
}
///////////////////////////////////////////
// Euclidean
// Euclidean32 computes the square-root of sum-of-squares distance
// between two vectors (aka the L2 norm).
// Skips NaN's and panics if lengths are not equal.
// Uses optimized algorithm from BLAS that avoids numerical overflow.
func Euclidean32(a, b []float32) float32 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
n := len(a)
if n < 2 {
if n == 1 {
return math32.Abs(a[0] - b[0])
}
return 0
}
var (
scale float32 = 0
sumSquares float32 = 1
)
for i, av := range a {
bv := b[i]
if av == bv || math32.IsNaN(av) || math32.IsNaN(bv) {
continue
}
absxi := math32.Abs(av - bv)
if scale < absxi {
sumSquares = 1 + sumSquares*(scale/absxi)*(scale/absxi)
scale = absxi
} else {
sumSquares = sumSquares + (absxi/scale)*(absxi/scale)
}
}
if math32.IsInf(scale, 1) {
return math32.Inf(1)
}
return scale * math32.Sqrt(sumSquares)
}
// Euclidean64 computes the square-root of sum-of-squares distance
// between two vectors (aka the L2 norm).
// Skips NaN's and panics if lengths are not equal.
// Uses optimized algorithm from BLAS that avoids numerical overflow.
func Euclidean64(a, b []float64) float64 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
n := len(a)
if n < 2 {
if n == 1 {
return math.Abs(a[0] - b[0])
}
return 0
}
var (
scale float64 = 0
sumSquares float64 = 1
)
for i, av := range a {
bv := b[i]
if av == bv || math.IsNaN(av) || math.IsNaN(bv) {
continue
}
absxi := math.Abs(av - bv)
if scale < absxi {
sumSquares = 1 + sumSquares*(scale/absxi)*(scale/absxi)
scale = absxi
} else {
sumSquares = sumSquares + (absxi/scale)*(absxi/scale)
}
}
if math.IsInf(scale, 1) {
return math.Inf(1)
}
return scale * math.Sqrt(sumSquares)
}
///////////////////////////////////////////
// EuclideanBinTol
// EuclideanBinTol32 computes the square-root of sum-of-squares distance
// between two vectors (aka the L2 norm).
// Skips NaN's and panics if lengths are not equal.
// Uses optimized algorithm from BLAS that avoids numerical overflow.
// BinTol version uses binary tolerance for 0-1 valued-vectors where
// abs diff < .5 counts as 0 error (i.e., closer than not).
func EuclideanBinTol32(a, b []float32) float32 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
n := len(a)
if n < 2 {
if n == 1 {
return math32.Abs(a[0] - b[0])
}
return 0
}
var (
scale float32 = 0
sumSquares float32 = 1
)
for i, av := range a {
bv := b[i]
if av == bv || math32.IsNaN(av) || math32.IsNaN(bv) {
continue
}
absxi := math32.Abs(av - bv)
if absxi < 0.5 {
continue
}
if scale < absxi {
sumSquares = 1 + sumSquares*(scale/absxi)*(scale/absxi)
scale = absxi
} else {
sumSquares = sumSquares + (absxi/scale)*(absxi/scale)
}
}
if math32.IsInf(scale, 1) {
return math32.Inf(1)
}
return scale * math32.Sqrt(sumSquares)
}
// EuclideanBinTol64 computes the square-root of sum-of-squares distance
// between two vectors (aka the L2 norm).
// Skips NaN's and panics if lengths are not equal.
// Uses optimized algorithm from BLAS that avoids numerical overflow.
// BinTol version uses binary tolerance for 0-1 valued-vectors where
// abs diff < .5 counts as 0 error (i.e., closer than not).
func EuclideanBinTol64(a, b []float64) float64 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
n := len(a)
if n < 2 {
if n == 1 {
return math.Abs(a[0] - b[0])
}
return 0
}
var (
scale float64 = 0
sumSquares float64 = 1
)
for i, av := range a {
bv := b[i]
if av == bv || math.IsNaN(av) || math.IsNaN(bv) {
continue
}
absxi := math.Abs(av - bv)
if absxi < 0.5 {
continue
}
if scale < absxi {
sumSquares = 1 + sumSquares*(scale/absxi)*(scale/absxi)
scale = absxi
} else {
sumSquares = sumSquares + (absxi/scale)*(absxi/scale)
}
}
if math.IsInf(scale, 1) {
return math.Inf(1)
}
return scale * math.Sqrt(sumSquares)
}
///////////////////////////////////////////
// Covariance
// Covariance32 computes the mean of the co-product of each vector element minus
// the mean of that vector: cov(A,B) = E[(A - E(A))(B - E(B))]
// Skips NaN's and panics if lengths are not equal.
func Covariance32(a, b []float32) float32 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
ss := float32(0)
am := stats.Mean32(a)
bm := stats.Mean32(b)
n := 0
for i, av := range a {
bv := b[i]
if math32.IsNaN(av) || math32.IsNaN(bv) {
continue
}
ss += (av - am) * (bv - bm)
n++
}
if n > 0 {
ss /= float32(n)
}
return ss
}
// Covariance64 computes the mean of the co-product of each vector element minus
// the mean of that vector: cov(A,B) = E[(A - E(A))(B - E(B))]
// Skips NaN's and panics if lengths are not equal.
func Covariance64(a, b []float64) float64 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
ss := float64(0)
am := stats.Mean64(a)
bm := stats.Mean64(b)
n := 0
for i, av := range a {
bv := b[i]
if math.IsNaN(av) || math.IsNaN(bv) {
continue
}
ss += (av - am) * (bv - bm)
n++
}
if n > 0 {
ss /= float64(n)
}
return ss
}
///////////////////////////////////////////
// Correlation
// Correlation32 computes the vector similarity in range (-1..1) as the
// mean of the co-product of each vector element minus the mean of that vector,
// normalized by the product of their standard deviations:
// cor(A,B) = E[(A - E(A))(B - E(B))] / sigma(A) sigma(B).
// (i.e., the standardized covariance) -- equivalent to the cosine of mean-normalized
// vectors.
// Skips NaN's and panics if lengths are not equal.
func Correlation32(a, b []float32) float32 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
ss := float32(0)
am := stats.Mean32(a)
bm := stats.Mean32(b)
var avar, bvar float32
for i, av := range a {
bv := b[i]
if math32.IsNaN(av) || math32.IsNaN(bv) {
continue
}
ad := av - am
bd := bv - bm
ss += ad * bd // between
avar += ad * ad // within
bvar += bd * bd
}
vp := math32.Sqrt(avar * bvar)
if vp > 0 {
ss /= vp
}
return ss
}
// Correlation64 computes the vector similarity in range (-1..1) as the
// mean of the co-product of each vector element minus the mean of that vector,
// normalized by the product of their standard deviations:
// cor(A,B) = E[(A - E(A))(B - E(B))] / sigma(A) sigma(B).
// (i.e., the standardized covariance) -- equivalent to the cosine of mean-normalized
// vectors.
// Skips NaN's and panics if lengths are not equal.
func Correlation64(a, b []float64) float64 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
ss := float64(0)
am := stats.Mean64(a)
bm := stats.Mean64(b)
var avar, bvar float64
for i, av := range a {
bv := b[i]
if math.IsNaN(av) || math.IsNaN(bv) {
continue
}
ad := av - am
bd := bv - bm
ss += ad * bd // between
avar += ad * ad // within
bvar += bd * bd
}
vp := math.Sqrt(avar * bvar)
if vp > 0 {
ss /= vp
}
return ss
}
///////////////////////////////////////////
// InnerProduct
// InnerProduct32 computes the sum of the element-wise product of the two vectors.
// Skips NaN's and panics if lengths are not equal.
func InnerProduct32(a, b []float32) float32 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
ss := float32(0)
for i, av := range a {
bv := b[i]
if math32.IsNaN(av) || math32.IsNaN(bv) {
continue
}
ss += av * bv
}
return ss
}
// InnerProduct64 computes the mean of the co-product of each vector element minus
// the mean of that vector, normalized by the product of their standard deviations:
// cor(A,B) = E[(A - E(A))(B - E(B))] / sigma(A) sigma(B).
// (i.e., the standardized covariance) -- equivalent to the cosine of mean-normalized
// vectors.
// Skips NaN's and panics if lengths are not equal.
func InnerProduct64(a, b []float64) float64 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
ss := float64(0)
for i, av := range a {
bv := b[i]
if math.IsNaN(av) || math.IsNaN(bv) {
continue
}
ss += av * bv
}
return ss
}
///////////////////////////////////////////
// Cosine
// Cosine32 computes the cosine of the angle between two vectors (-1..1),
// as the normalized inner product: inner product / sqrt(ssA * ssB).
// If vectors are mean-normalized = Correlation.
// Skips NaN's and panics if lengths are not equal.
func Cosine32(a, b []float32) float32 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
ss := float32(0)
var ass, bss float32
for i, av := range a {
bv := b[i]
if math32.IsNaN(av) || math32.IsNaN(bv) {
continue
}
ss += av * bv // between
ass += av * av // within
bss += bv * bv
}
vp := math32.Sqrt(ass * bss)
if vp > 0 {
ss /= vp
}
return ss
}
// Cosine32 computes the cosine of the angle between two vectors (-1..1),
// as the normalized inner product: inner product / sqrt(ssA * ssB).
// If vectors are mean-normalized = Correlation.
// Skips NaN's and panics if lengths are not equal.
func Cosine64(a, b []float64) float64 {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
ss := float64(0)
var ass, bss float64
for i, av := range a {
bv := b[i]
if math.IsNaN(av) || math.IsNaN(bv) {
continue
}
ss += av * bv // between
ass += av * av // within
bss += bv * bv
}
vp := math.Sqrt(ass * bss)
if vp > 0 {
ss /= vp
}
return ss
}
///////////////////////////////////////////
// InvCosine
// InvCosine32 computes 1 - cosine of the angle between two vectors (-1..1),
// as the normalized inner product: inner product / sqrt(ssA * ssB).
// If vectors are mean-normalized = Correlation.
// Skips NaN's and panics if lengths are not equal.
func InvCosine32(a, b []float32) float32 {
return 1 - Cosine32(a, b)
}
// InvCosine32 computes 1 - cosine of the angle between two vectors (-1..1),
// as the normalized inner product: inner product / sqrt(ssA * ssB).
// If vectors are mean-normalized = Correlation.
// Skips NaN's and panics if lengths are not equal.
func InvCosine64(a, b []float64) float64 {
return 1 - Cosine64(a, b)
}
///////////////////////////////////////////
// InvCorrelation
// InvCorrelation32 computes 1 - the vector similarity in range (-1..1) as the
// mean of the co-product of each vector element minus the mean of that vector,
// normalized by the product of their standard deviations:
// cor(A,B) = E[(A - E(A))(B - E(B))] / sigma(A) sigma(B).
// (i.e., the standardized covariance) -- equivalent to the cosine of mean-normalized
// vectors.
// Skips NaN's and panics if lengths are not equal.
func InvCorrelation32(a, b []float32) float32 {
return 1 - Correlation32(a, b)
}
// InvCorrelation64 computes 1 - the vector similarity in range (-1..1) as the
// mean of the co-product of each vector element minus the mean of that vector,
// normalized by the product of their standard deviations:
// cor(A,B) = E[(A - E(A))(B - E(B))] / sigma(A) sigma(B).
// (i.e., the standardized covariance) -- equivalent to the cosine of mean-normalized
// vectors.
// Skips NaN's and panics if lengths are not equal.
func InvCorrelation64(a, b []float64) float64 {
return 1 - Correlation64(a, b)
}
// Copyright (c) 2024, 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 metric
import (
"math"
"cogentcore.org/core/tensor"
)
// ClosestRow32 returns the closest fit between probe pattern and patterns in
// an tensor with float32 data where the outer-most dimension is assumed to be a row
// (e.g., as a column in an table), using the given metric function,
// *which must have the Increasing property* -- i.e., larger = further.
// returns the row and metric value for that row.
// Col cell sizes must match size of probe (panics if not).
func ClosestRow32(probe tensor.Tensor, col tensor.Tensor, mfun Func32) (int, float32) {
pr := probe.(*tensor.Float32)
cl := col.(*tensor.Float32)
rows := col.Shape().DimSize(0)
csz := col.Len() / rows
if csz != probe.Len() {
panic("metric.ClosestRow32: probe size != cell size of tensor column!\n")
}
ci := -1
minv := float32(math.MaxFloat32)
for ri := 0; ri < rows; ri++ {
st := ri * csz
rvals := cl.Values[st : st+csz]
v := mfun(pr.Values, rvals)
if v < minv {
ci = ri
minv = v
}
}
return ci, minv
}
// ClosestRow64 returns the closest fit between probe pattern and patterns in
// a tensor with float64 data where the outer-most dimension is assumed to be a row
// (e.g., as a column in an table), using the given metric function,
// *which must have the Increasing property* -- i.e., larger = further.
// returns the row and metric value for that row.
// Col cell sizes must match size of probe (panics if not).
func ClosestRow64(probe tensor.Tensor, col tensor.Tensor, mfun Func64) (int, float64) {
pr := probe.(*tensor.Float64)
cl := col.(*tensor.Float64)
rows := col.DimSize(0)
csz := col.Len() / rows
if csz != probe.Len() {
panic("metric.ClosestRow64: probe size != cell size of tensor column!\n")
}
ci := -1
minv := math.MaxFloat64
for ri := 0; ri < rows; ri++ {
st := ri * csz
rvals := cl.Values[st : st+csz]
v := mfun(pr.Values, rvals)
if v < minv {
ci = ri
minv = v
}
}
return ci, minv
}
// ClosestRow32Py returns the closest fit between probe pattern and patterns in
// an tensor.Float32 where the outer-most dimension is assumed to be a row
// (e.g., as a column in an table), using the given metric function,
// *which must have the Increasing property* -- i.e., larger = further.
// returns the row and metric value for that row.
// Col cell sizes must match size of probe (panics if not).
// Py version is for Python, returns a slice with row, cor, takes std metric
func ClosestRow32Py(probe tensor.Tensor, col tensor.Tensor, std StdMetrics) []float32 {
row, cor := ClosestRow32(probe, col, StdFunc32(std))
return []float32{float32(row), cor}
}
// ClosestRow64Py returns the closest fit between probe pattern and patterns in
// an tensor.Tensor where the outer-most dimension is assumed to be a row
// (e.g., as a column in an table), using the given metric function,
// *which must have the Increasing property* -- i.e., larger = further.
// returns the row and metric value for that row.
// Col cell sizes must match size of probe (panics if not).
// Optimized for tensor.Float64 but works for any tensor.
// Py version is for Python, returns a slice with row, cor, takes std metric
func ClosestRow64Py(probe tensor.Tensor, col tensor.Tensor, std StdMetrics) []float64 {
row, cor := ClosestRow64(probe, col, StdFunc64(std))
return []float64{float64(row), cor}
}
// Copyright (c) 2024, 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 metric
import (
"math"
"cogentcore.org/core/math32"
)
///////////////////////////////////////////
// Tolerance
// Tolerance32 sets a = b for any element where |a-b| <= tol.
// This can be called prior to any metric function.
func Tolerance32(a, b []float32, tol float32) {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
for i, av := range a {
bv := b[i]
if math32.IsNaN(av) || math32.IsNaN(bv) {
continue
}
if math32.Abs(av-bv) <= tol {
a[i] = bv
}
}
}
// Tolerance64 sets a = b for any element where |a-b| <= tol.
// This can be called prior to any metric function.
func Tolerance64(a, b []float64, tol float64) {
if len(a) != len(b) {
panic("metric: slice lengths do not match")
}
for i, av := range a {
bv := b[i]
if math.IsNaN(av) || math.IsNaN(bv) {
continue
}
if math.Abs(av-bv) <= tol {
a[i] = bv
}
}
}
// Copyright (c) 2024, 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 norm
import (
"fmt"
"log/slog"
"math"
"cogentcore.org/core/math32"
"cogentcore.org/core/tensor"
)
// Abs32 applies the Abs function to each element in given slice
func Abs32(a []float32) {
for i, av := range a {
if math32.IsNaN(av) {
continue
}
a[i] = math32.Abs(av)
}
}
// Abs64 applies the Abs function to each element in given slice
func Abs64(a []float64) {
for i, av := range a {
if math.IsNaN(av) {
continue
}
a[i] = math.Abs(av)
}
}
func FloatOnlyError() error {
err := fmt.Errorf("Only float32 or float64 data types supported")
slog.Error(err.Error())
return err
}
// AbsTensor applies the Abs function to each element in given tensor,
// for float32 and float64 data types.
func AbsTensor(a tensor.Tensor) {
switch tsr := a.(type) {
case *tensor.Float32:
Abs32(tsr.Values)
case *tensor.Float64:
Abs64(tsr.Values)
default:
FloatOnlyError()
}
}
// Copyright (c) 2024, 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 norm
//go:generate core generate
import (
"math"
"cogentcore.org/core/math32"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/stats/stats"
)
// FloatFunc applies given functions to float tensor data, which is either Float32 or Float64
func FloatFunc(tsr tensor.Tensor, nfunc32 Func32, nfunc64 Func64, stIdx, nIdx int, ffunc32 func(a []float32, fun Func32), ffunc64 func(a []float64, fun Func64)) {
switch tt := tsr.(type) {
case *tensor.Float32:
vals := tt.Values
if nIdx > 0 {
vals = vals[stIdx : stIdx+nIdx]
}
ffunc32(vals, nfunc32)
case *tensor.Float64:
vals := tt.Values
if nIdx > 0 {
vals = vals[stIdx : stIdx+nIdx]
}
ffunc64(vals, nfunc64)
default:
FloatOnlyError()
}
}
///////////////////////////////////////////
// DivNorm
// DivNorm32 does divisive normalization by given norm function
// i.e., it divides each element by the norm value computed from nfunc.
func DivNorm32(a []float32, nfunc Func32) {
nv := nfunc(a)
if nv != 0 {
MultVector32(a, 1/nv)
}
}
// DivNorm64 does divisive normalization by given norm function
// i.e., it divides each element by the norm value computed from nfunc.
func DivNorm64(a []float64, nfunc Func64) {
nv := nfunc(a)
if nv != 0 {
MultVec64(a, 1/nv)
}
}
///////////////////////////////////////////
// SubNorm
// SubNorm32 does subtractive normalization by given norm function
// i.e., it subtracts norm computed by given function from each element.
func SubNorm32(a []float32, nfunc Func32) {
nv := nfunc(a)
AddVector32(a, -nv)
}
// SubNorm64 does subtractive normalization by given norm function
// i.e., it subtracts norm computed by given function from each element.
func SubNorm64(a []float64, nfunc Func64) {
nv := nfunc(a)
AddVec64(a, -nv)
}
///////////////////////////////////////////
// ZScore
// ZScore32 subtracts the mean and divides by the standard deviation
func ZScore32(a []float32) {
SubNorm32(a, stats.Mean32)
DivNorm32(a, stats.Std32)
}
// ZScore64 subtracts the mean and divides by the standard deviation
func ZScore64(a []float64) {
SubNorm64(a, stats.Mean64)
DivNorm64(a, stats.Std64)
}
///////////////////////////////////////////
// Unit
// Unit32 subtracts the min and divides by the max, so that values are in 0-1 unit range
func Unit32(a []float32) {
SubNorm32(a, stats.Min32)
DivNorm32(a, stats.Max32)
}
// Unit64 subtracts the min and divides by the max, so that values are in 0-1 unit range
func Unit64(a []float64) {
SubNorm64(a, stats.Min64)
DivNorm64(a, stats.Max64)
}
///////////////////////////////////////////
// MultVec
// MultVector32 multiplies vector elements by scalar
func MultVector32(a []float32, val float32) {
for i, av := range a {
if math32.IsNaN(av) {
continue
}
a[i] *= val
}
}
// MultVec64 multiplies vector elements by scalar
func MultVec64(a []float64, val float64) {
for i, av := range a {
if math.IsNaN(av) {
continue
}
a[i] *= val
}
}
///////////////////////////////////////////
// AddVec
// AddVector32 adds scalar to vector
func AddVector32(a []float32, val float32) {
for i, av := range a {
if math32.IsNaN(av) {
continue
}
a[i] += val
}
}
// AddVec64 adds scalar to vector
func AddVec64(a []float64, val float64) {
for i, av := range a {
if math.IsNaN(av) {
continue
}
a[i] += val
}
}
///////////////////////////////////////////
// Thresh
// Thresh32 thresholds the values of the vector -- anything above the high threshold is set
// to the high value, and everything below the low threshold is set to the low value.
func Thresh32(a []float32, hi bool, hiThr float32, lo bool, loThr float32) {
for i, av := range a {
if math32.IsNaN(av) {
continue
}
if hi && av > hiThr {
a[i] = hiThr
}
if lo && av < loThr {
a[i] = loThr
}
}
}
// Thresh64 thresholds the values of the vector -- anything above the high threshold is set
// to the high value, and everything below the low threshold is set to the low value.
func Thresh64(a []float64, hi bool, hiThr float64, lo bool, loThr float64) {
for i, av := range a {
if math.IsNaN(av) {
continue
}
if hi && av > hiThr {
a[i] = hiThr
}
if lo && av < loThr {
a[i] = loThr
}
}
}
///////////////////////////////////////////
// Binarize
// Binarize32 turns vector into binary-valued, by setting anything >= the threshold
// to the high value, and everything below to the low value.
func Binarize32(a []float32, thr, hiVal, loVal float32) {
for i, av := range a {
if math32.IsNaN(av) {
continue
}
if av >= thr {
a[i] = hiVal
} else {
a[i] = loVal
}
}
}
// Binarize64 turns vector into binary-valued, by setting anything >= the threshold
// to the high value, and everything below to the low value.
func Binarize64(a []float64, thr, hiVal, loVal float64) {
for i, av := range a {
if math.IsNaN(av) {
continue
}
if av >= thr {
a[i] = hiVal
} else {
a[i] = loVal
}
}
}
// Func32 is a norm function operating on slice of float32 numbers
type Func32 func(a []float32) float32
// Func64 is a norm function operating on slices of float64 numbers
type Func64 func(a []float64) float64
// Copyright (c) 2024, 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 norm
import (
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/stats/stats"
)
///////////////////////////////////////////
// DivNorm
// TensorDivNorm does divisive normalization by given norm function
// computed on the first ndim dims of the tensor, where 0 = all values,
// 1 = norm each of the sub-dimensions under the first outer-most dimension etc.
// ndim must be < NumDims() if not 0.
func TensorDivNorm(tsr tensor.Tensor, ndim int, nfunc32 Func32, nfunc64 Func64) {
if ndim == 0 {
FloatFunc(tsr, nfunc32, nfunc64, 0, 0, DivNorm32, DivNorm64)
}
if ndim >= tsr.NumDims() {
panic("norm.TensorSubNorm32: number of dims must be < NumDims()")
}
sln := 1
ln := tsr.Len()
for i := 0; i < ndim; i++ {
sln *= tsr.Shape().DimSize(i)
}
dln := ln / sln
for sl := 0; sl < sln; sl++ {
st := sl * dln
FloatFunc(tsr, nfunc32, nfunc64, st, dln, DivNorm32, DivNorm64)
}
}
///////////////////////////////////////////
// SubNorm
// TensorSubNorm does subtractive normalization by given norm function
// computed on the first ndim dims of the tensor, where 0 = all values,
// 1 = norm each of the sub-dimensions under the first outer-most dimension etc.
// ndim must be < NumDims() if not 0 (panics).
func TensorSubNorm(tsr tensor.Tensor, ndim int, nfunc32 Func32, nfunc64 Func64) {
if ndim == 0 {
FloatFunc(tsr, nfunc32, nfunc64, 0, 0, SubNorm32, SubNorm64)
}
if ndim >= tsr.NumDims() {
panic("norm.TensorSubNorm32: number of dims must be < NumDims()")
}
sln := 1
ln := tsr.Len()
for i := 0; i < ndim; i++ {
sln *= tsr.Shape().DimSize(i)
}
dln := ln / sln
for sl := 0; sl < sln; sl++ {
st := sl * dln
FloatFunc(tsr, nfunc32, nfunc64, st, dln, SubNorm32, SubNorm64)
}
}
// TensorZScore subtracts the mean and divides by the standard deviation
// computed on the first ndim dims of the tensor, where 0 = all values,
// 1 = norm each of the sub-dimensions under the first outer-most dimension etc.
// ndim must be < NumDims() if not 0 (panics).
// must be a float32 or float64 tensor
func TensorZScore(tsr tensor.Tensor, ndim int) {
TensorSubNorm(tsr, ndim, stats.Mean32, stats.Mean64)
TensorDivNorm(tsr, ndim, stats.Std32, stats.Std64)
}
// TensorUnit subtracts the min and divides by the max, so that values are in 0-1 unit range
// computed on the first ndim dims of the tensor, where 0 = all values,
// 1 = norm each of the sub-dimensions under the first outer-most dimension etc.
// ndim must be < NumDims() if not 0 (panics).
// must be a float32 or float64 tensor
func TensorUnit(tsr tensor.Tensor, ndim int) {
TensorSubNorm(tsr, ndim, stats.Min32, stats.Min64)
TensorDivNorm(tsr, ndim, stats.Max32, stats.Max64)
}
// Copyright (c) 2024, 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 pca
import (
"fmt"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/stats/metric"
"cogentcore.org/core/tensor/table"
)
// CovarTableColumn generates a covariance matrix from given column name
// in given IndexView of an table.Table, and given metric function
// (typically Covariance or Correlation -- use Covar if vars have similar
// overall scaling, which is typical in neural network models, and use
// Correl if they are on very different scales -- Correl effectively rescales).
// A Covariance matrix computes the *row-wise* vector similarities for each
// pairwise combination of column cells -- i.e., the extent to which each
// cell co-varies in its value with each other cell across the rows of the table.
// This is the input to the PCA eigenvalue decomposition of the resulting
// covariance matrix.
func CovarTableColumn(cmat tensor.Tensor, ix *table.IndexView, column string, mfun metric.Func64) error {
col, err := ix.Table.ColumnByName(column)
if err != nil {
return err
}
rows := ix.Len()
nd := col.NumDims()
if nd < 2 || rows == 0 {
return fmt.Errorf("pca.CovarTableColumn: must have 2 or more dims and rows != 0")
}
ln := col.Len()
sz := ln / col.DimSize(0) // size of cell
cshp := []int{sz, sz}
cmat.SetShape(cshp)
av := make([]float64, rows)
bv := make([]float64, rows)
sdim := []int{0, 0}
for ai := 0; ai < sz; ai++ {
sdim[0] = ai
TableColumnRowsVec(av, ix, col, ai)
for bi := 0; bi <= ai; bi++ { // lower diag
sdim[1] = bi
TableColumnRowsVec(bv, ix, col, bi)
cv := mfun(av, bv)
cmat.SetFloat(sdim, cv)
}
}
// now fill in upper diagonal with values from lower diagonal
// note: assumes symmetric distance function
fdim := []int{0, 0}
for ai := 0; ai < sz; ai++ {
sdim[0] = ai
fdim[1] = ai
for bi := ai + 1; bi < sz; bi++ { // upper diag
fdim[0] = bi
sdim[1] = bi
cv := cmat.Float(fdim)
cmat.SetFloat(sdim, cv)
}
}
if nm, has := ix.Table.MetaData["name"]; has {
cmat.SetMetaData("name", nm+"_"+column)
} else {
cmat.SetMetaData("name", column)
}
if ds, has := ix.Table.MetaData["desc"]; has {
cmat.SetMetaData("desc", ds)
}
return nil
}
// CovarTensor generates a covariance matrix from given tensor.Tensor,
// where the outer-most dimension is rows, and all other dimensions within that
// are covaried against each other, using given metric function
// (typically Covariance or Correlation -- use Covar if vars have similar
// overall scaling, which is typical in neural network models, and use
// Correl if they are on very different scales -- Correl effectively rescales).
// A Covariance matrix computes the *row-wise* vector similarities for each
// pairwise combination of column cells -- i.e., the extent to which each
// cell co-varies in its value with each other cell across the rows of the table.
// This is the input to the PCA eigenvalue decomposition of the resulting
// covariance matrix.
func CovarTensor(cmat tensor.Tensor, tsr tensor.Tensor, mfun metric.Func64) error {
rows := tsr.DimSize(0)
nd := tsr.NumDims()
if nd < 2 || rows == 0 {
return fmt.Errorf("pca.CovarTensor: must have 2 or more dims and rows != 0")
}
ln := tsr.Len()
sz := ln / rows
cshp := []int{sz, sz}
cmat.SetShape(cshp)
av := make([]float64, rows)
bv := make([]float64, rows)
sdim := []int{0, 0}
for ai := 0; ai < sz; ai++ {
sdim[0] = ai
TensorRowsVec(av, tsr, ai)
for bi := 0; bi <= ai; bi++ { // lower diag
sdim[1] = bi
TensorRowsVec(bv, tsr, bi)
cv := mfun(av, bv)
cmat.SetFloat(sdim, cv)
}
}
// now fill in upper diagonal with values from lower diagonal
// note: assumes symmetric distance function
fdim := []int{0, 0}
for ai := 0; ai < sz; ai++ {
sdim[0] = ai
fdim[1] = ai
for bi := ai + 1; bi < sz; bi++ { // upper diag
fdim[0] = bi
sdim[1] = bi
cv := cmat.Float(fdim)
cmat.SetFloat(sdim, cv)
}
}
if nm, has := tsr.MetaData("name"); has {
cmat.SetMetaData("name", nm+"Covar")
} else {
cmat.SetMetaData("name", "Covar")
}
if ds, has := tsr.MetaData("desc"); has {
cmat.SetMetaData("desc", ds)
}
return nil
}
// TableColumnRowsVec extracts row-wise vector from given cell index into vec.
// vec must be of size ix.Len() -- number of rows
func TableColumnRowsVec(vec []float64, ix *table.IndexView, col tensor.Tensor, cidx int) {
rows := ix.Len()
ln := col.Len()
sz := ln / col.DimSize(0) // size of cell
for ri := 0; ri < rows; ri++ {
coff := ix.Indexes[ri]*sz + cidx
vec[ri] = col.Float1D(coff)
}
}
// TensorRowsVec extracts row-wise vector from given cell index into vec.
// vec must be of size tsr.DimSize(0) -- number of rows
func TensorRowsVec(vec []float64, tsr tensor.Tensor, cidx int) {
rows := tsr.DimSize(0)
ln := tsr.Len()
sz := ln / rows
for ri := 0; ri < rows; ri++ {
coff := ri*sz + cidx
vec[ri] = tsr.Float1D(coff)
}
}
// CovarTableColumnStd generates a covariance matrix from given column name
// in given IndexView of an table.Table, and given metric function
// (typically Covariance or Correlation -- use Covar if vars have similar
// overall scaling, which is typical in neural network models, and use
// Correl if they are on very different scales -- Correl effectively rescales).
// A Covariance matrix computes the *row-wise* vector similarities for each
// pairwise combination of column cells -- i.e., the extent to which each
// cell co-varies in its value with each other cell across the rows of the table.
// This is the input to the PCA eigenvalue decomposition of the resulting
// covariance matrix.
// This Std version is usable e.g., in Python where the func cannot be passed.
func CovarTableColumnStd(cmat tensor.Tensor, ix *table.IndexView, column string, met metric.StdMetrics) error {
return CovarTableColumn(cmat, ix, column, metric.StdFunc64(met))
}
// CovarTensorStd generates a covariance matrix from given tensor.Tensor,
// where the outer-most dimension is rows, and all other dimensions within that
// are covaried against each other, using given metric function
// (typically Covariance or Correlation -- use Covar if vars have similar
// overall scaling, which is typical in neural network models, and use
// Correl if they are on very different scales -- Correl effectively rescales).
// A Covariance matrix computes the *row-wise* vector similarities for each
// pairwise combination of column cells -- i.e., the extent to which each
// cell co-varies in its value with each other cell across the rows of the table.
// This is the input to the PCA eigenvalue decomposition of the resulting
// covariance matrix.
// This Std version is usable e.g., in Python where the func cannot be passed.
func CovarTensorStd(cmat tensor.Tensor, tsr tensor.Tensor, met metric.StdMetrics) error {
return CovarTensor(cmat, tsr, metric.StdFunc64(met))
}
// Copyright (c) 2024, 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 pca
//go:generate core generate
import (
"fmt"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/stats/metric"
"cogentcore.org/core/tensor/table"
"gonum.org/v1/gonum/mat"
)
// PCA computes the eigenvalue decomposition of a square similarity matrix,
// typically generated using the correlation metric.
type PCA struct {
// the covariance matrix computed on original data, which is then eigen-factored
Covar tensor.Tensor `display:"no-inline"`
// the eigenvectors, in same size as Covar - each eigenvector is a column in this 2D square matrix, ordered *lowest* to *highest* across the columns -- i.e., maximum eigenvector is the last column
Vectors tensor.Tensor `display:"no-inline"`
// the eigenvalues, ordered *lowest* to *highest*
Values []float64 `display:"no-inline"`
}
func (pa *PCA) Init() {
pa.Covar = &tensor.Float64{}
pa.Vectors = &tensor.Float64{}
pa.Values = nil
}
// TableColumn is a convenience method that computes a covariance matrix
// on given column of table and then performs the PCA on the resulting matrix.
// If no error occurs, the results can be read out from Vectors and Values
// or used in Projection methods.
// mfun is metric function, typically Covariance or Correlation -- use Covar
// if vars have similar overall scaling, which is typical in neural network models,
// and use Correl if they are on very different scales -- Correl effectively rescales).
// A Covariance matrix computes the *row-wise* vector similarities for each
// pairwise combination of column cells -- i.e., the extent to which each
// cell co-varies in its value with each other cell across the rows of the table.
// This is the input to the PCA eigenvalue decomposition of the resulting
// covariance matrix, which extracts the eigenvectors as directions with maximal
// variance in this matrix.
func (pa *PCA) TableColumn(ix *table.IndexView, column string, mfun metric.Func64) error {
if pa.Covar == nil {
pa.Init()
}
err := CovarTableColumn(pa.Covar, ix, column, mfun)
if err != nil {
return err
}
return pa.PCA()
}
// Tensor is a convenience method that computes a covariance matrix
// on given tensor and then performs the PCA on the resulting matrix.
// If no error occurs, the results can be read out from Vectors and Values
// or used in Projection methods.
// mfun is metric function, typically Covariance or Correlation -- use Covar
// if vars have similar overall scaling, which is typical in neural network models,
// and use Correl if they are on very different scales -- Correl effectively rescales).
// A Covariance matrix computes the *row-wise* vector similarities for each
// pairwise combination of column cells -- i.e., the extent to which each
// cell co-varies in its value with each other cell across the rows of the table.
// This is the input to the PCA eigenvalue decomposition of the resulting
// covariance matrix, which extracts the eigenvectors as directions with maximal
// variance in this matrix.
func (pa *PCA) Tensor(tsr tensor.Tensor, mfun metric.Func64) error {
if pa.Covar == nil {
pa.Init()
}
err := CovarTensor(pa.Covar, tsr, mfun)
if err != nil {
return err
}
return pa.PCA()
}
// TableColumnStd is a convenience method that computes a covariance matrix
// on given column of table and then performs the PCA on the resulting matrix.
// If no error occurs, the results can be read out from Vectors and Values
// or used in Projection methods.
// mfun is a Std metric function, typically Covariance or Correlation -- use Covar
// if vars have similar overall scaling, which is typical in neural network models,
// and use Correl if they are on very different scales -- Correl effectively rescales).
// A Covariance matrix computes the *row-wise* vector similarities for each
// pairwise combination of column cells -- i.e., the extent to which each
// cell co-varies in its value with each other cell across the rows of the table.
// This is the input to the PCA eigenvalue decomposition of the resulting
// covariance matrix, which extracts the eigenvectors as directions with maximal
// variance in this matrix.
// This Std version is usable e.g., in Python where the func cannot be passed.
func (pa *PCA) TableColumnStd(ix *table.IndexView, column string, met metric.StdMetrics) error {
return pa.TableColumn(ix, column, metric.StdFunc64(met))
}
// TensorStd is a convenience method that computes a covariance matrix
// on given tensor and then performs the PCA on the resulting matrix.
// If no error occurs, the results can be read out from Vectors and Values
// or used in Projection methods.
// mfun is Std metric function, typically Covariance or Correlation -- use Covar
// if vars have similar overall scaling, which is typical in neural network models,
// and use Correl if they are on very different scales -- Correl effectively rescales).
// A Covariance matrix computes the *row-wise* vector similarities for each
// pairwise combination of column cells -- i.e., the extent to which each
// cell co-varies in its value with each other cell across the rows of the table.
// This is the input to the PCA eigenvalue decomposition of the resulting
// covariance matrix, which extracts the eigenvectors as directions with maximal
// variance in this matrix.
// This Std version is usable e.g., in Python where the func cannot be passed.
func (pa *PCA) TensorStd(tsr tensor.Tensor, met metric.StdMetrics) error {
return pa.Tensor(tsr, metric.StdFunc64(met))
}
// PCA performs the eigen decomposition of the existing Covar matrix.
// Vectors and Values fields contain the results.
func (pa *PCA) PCA() error {
if pa.Covar == nil || pa.Covar.NumDims() != 2 {
return fmt.Errorf("pca.PCA: Covar matrix is nil or not 2D")
}
var eig mat.EigenSym
// note: MUST be a Float64 otherwise doesn't have Symmetric function
ok := eig.Factorize(pa.Covar.(*tensor.Float64), true)
if !ok {
return fmt.Errorf("gonum EigenSym Factorize failed")
}
if pa.Vectors == nil {
pa.Vectors = &tensor.Float64{}
}
var ev mat.Dense
eig.VectorsTo(&ev)
tensor.CopyDense(pa.Vectors, &ev)
nr := pa.Vectors.DimSize(0)
if len(pa.Values) != nr {
pa.Values = make([]float64, nr)
}
eig.Values(pa.Values)
return nil
}
// ProjectColumn projects values from the given column of given table (via IndexView)
// onto the idx'th eigenvector (0 = largest eigenvalue, 1 = next, etc).
// Must have already called PCA() method.
func (pa *PCA) ProjectColumn(vals *[]float64, ix *table.IndexView, column string, idx int) error {
col, err := ix.Table.ColumnByName(column)
if err != nil {
return err
}
if pa.Vectors == nil {
return fmt.Errorf("PCA.ProjectColumn Vectors are nil -- must call PCA first")
}
nr := pa.Vectors.DimSize(0)
if idx >= nr {
return fmt.Errorf("PCA.ProjectColumn eigenvector index > rank of matrix")
}
cvec := make([]float64, nr)
eidx := nr - 1 - idx // eigens in reverse order
vec := pa.Vectors.(*tensor.Float64)
for ri := 0; ri < nr; ri++ {
cvec[ri] = vec.Value([]int{ri, eidx}) // vecs are in columns, reverse magnitude order
}
rows := ix.Len()
if len(*vals) != rows {
*vals = make([]float64, rows)
}
ln := col.Len()
sz := ln / col.DimSize(0) // size of cell
if sz != nr {
return fmt.Errorf("PCA.ProjectColumn column cell size != pca eigenvectors")
}
rdim := []int{0}
for row := 0; row < rows; row++ {
sum := 0.0
rdim[0] = ix.Indexes[row]
rt := col.SubSpace(rdim)
for ci := 0; ci < sz; ci++ {
sum += cvec[ci] * rt.Float1D(ci)
}
(*vals)[row] = sum
}
return nil
}
// ProjectColumnToTable projects values from the given column of given table (via IndexView)
// onto the given set of eigenvectors (idxs, 0 = largest eigenvalue, 1 = next, etc),
// and stores results along with labels from column labNm into results table.
// Must have already called PCA() method.
func (pa *PCA) ProjectColumnToTable(projections *table.Table, ix *table.IndexView, column, labNm string, idxs []int) error {
_, err := ix.Table.ColumnByName(column)
if err != nil {
return err
}
if pa.Vectors == nil {
return fmt.Errorf("PCA.ProjectColumn Vectors are nil -- must call PCA first")
}
rows := ix.Len()
projections.DeleteAll()
pcolSt := 0
if labNm != "" {
projections.AddStringColumn(labNm)
pcolSt = 1
}
for _, idx := range idxs {
projections.AddFloat64Column(fmt.Sprintf("Projection%v", idx))
}
projections.SetNumRows(rows)
for ii, idx := range idxs {
pcol := projections.Columns[pcolSt+ii].(*tensor.Float64)
pa.ProjectColumn(&pcol.Values, ix, column, idx)
}
if labNm != "" {
lcol, err := ix.Table.ColumnByName(labNm)
if err == nil {
plcol := projections.Columns[0]
for row := 0; row < rows; row++ {
plcol.SetString1D(row, lcol.String1D(row))
}
}
}
return nil
}
// Copyright (c) 2024, 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 pca
import (
"fmt"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/stats/metric"
"cogentcore.org/core/tensor/table"
"gonum.org/v1/gonum/mat"
)
// SVD computes the eigenvalue decomposition of a square similarity matrix,
// typically generated using the correlation metric.
type SVD struct {
// type of SVD to run: SVDNone is the most efficient if you only need the values which are always computed. Otherwise, SVDThin is the next most efficient for getting approximate vectors
Kind mat.SVDKind
// condition value -- minimum normalized eigenvalue to return in values
Cond float64 `default:"0.01"`
// the rank (count) of singular values greater than Cond
Rank int
// the covariance matrix computed on original data, which is then eigen-factored
Covar tensor.Tensor `display:"no-inline"`
// the eigenvectors, in same size as Covar - each eigenvector is a column in this 2D square matrix, ordered *lowest* to *highest* across the columns -- i.e., maximum eigenvector is the last column
Vectors tensor.Tensor `display:"no-inline"`
// the eigenvalues, ordered *lowest* to *highest*
Values []float64 `display:"no-inline"`
}
func (svd *SVD) Init() {
svd.Kind = mat.SVDNone
svd.Cond = 0.01
svd.Covar = &tensor.Float64{}
svd.Vectors = &tensor.Float64{}
svd.Values = nil
}
// TableColumn is a convenience method that computes a covariance matrix
// on given column of table and then performs the SVD on the resulting matrix.
// If no error occurs, the results can be read out from Vectors and Values
// or used in Projection methods.
// mfun is metric function, typically Covariance or Correlation -- use Covar
// if vars have similar overall scaling, which is typical in neural network models,
// and use Correl if they are on very different scales -- Correl effectively rescales).
// A Covariance matrix computes the *row-wise* vector similarities for each
// pairwise combination of column cells -- i.e., the extent to which each
// cell co-varies in its value with each other cell across the rows of the table.
// This is the input to the SVD eigenvalue decomposition of the resulting
// covariance matrix, which extracts the eigenvectors as directions with maximal
// variance in this matrix.
func (svd *SVD) TableColumn(ix *table.IndexView, column string, mfun metric.Func64) error {
if svd.Covar == nil {
svd.Init()
}
err := CovarTableColumn(svd.Covar, ix, column, mfun)
if err != nil {
return err
}
return svd.SVD()
}
// Tensor is a convenience method that computes a covariance matrix
// on given tensor and then performs the SVD on the resulting matrix.
// If no error occurs, the results can be read out from Vectors and Values
// or used in Projection methods.
// mfun is metric function, typically Covariance or Correlation -- use Covar
// if vars have similar overall scaling, which is typical in neural network models,
// and use Correl if they are on very different scales -- Correl effectively rescales).
// A Covariance matrix computes the *row-wise* vector similarities for each
// pairwise combination of column cells -- i.e., the extent to which each
// cell co-varies in its value with each other cell across the rows of the table.
// This is the input to the SVD eigenvalue decomposition of the resulting
// covariance matrix, which extracts the eigenvectors as directions with maximal
// variance in this matrix.
func (svd *SVD) Tensor(tsr tensor.Tensor, mfun metric.Func64) error {
if svd.Covar == nil {
svd.Init()
}
err := CovarTensor(svd.Covar, tsr, mfun)
if err != nil {
return err
}
return svd.SVD()
}
// TableColumnStd is a convenience method that computes a covariance matrix
// on given column of table and then performs the SVD on the resulting matrix.
// If no error occurs, the results can be read out from Vectors and Values
// or used in Projection methods.
// mfun is a Std metric function, typically Covariance or Correlation -- use Covar
// if vars have similar overall scaling, which is typical in neural network models,
// and use Correl if they are on very different scales -- Correl effectively rescales).
// A Covariance matrix computes the *row-wise* vector similarities for each
// pairwise combination of column cells -- i.e., the extent to which each
// cell co-varies in its value with each other cell across the rows of the table.
// This is the input to the SVD eigenvalue decomposition of the resulting
// covariance matrix, which extracts the eigenvectors as directions with maximal
// variance in this matrix.
// This Std version is usable e.g., in Python where the func cannot be passed.
func (svd *SVD) TableColumnStd(ix *table.IndexView, column string, met metric.StdMetrics) error {
return svd.TableColumn(ix, column, metric.StdFunc64(met))
}
// TensorStd is a convenience method that computes a covariance matrix
// on given tensor and then performs the SVD on the resulting matrix.
// If no error occurs, the results can be read out from Vectors and Values
// or used in Projection methods.
// mfun is Std metric function, typically Covariance or Correlation -- use Covar
// if vars have similar overall scaling, which is typical in neural network models,
// and use Correl if they are on very different scales -- Correl effectively rescales).
// A Covariance matrix computes the *row-wise* vector similarities for each
// pairwise combination of column cells -- i.e., the extent to which each
// cell co-varies in its value with each other cell across the rows of the table.
// This is the input to the SVD eigenvalue decomposition of the resulting
// covariance matrix, which extracts the eigenvectors as directions with maximal
// variance in this matrix.
// This Std version is usable e.g., in Python where the func cannot be passed.
func (svd *SVD) TensorStd(tsr tensor.Tensor, met metric.StdMetrics) error {
return svd.Tensor(tsr, metric.StdFunc64(met))
}
// SVD performs the eigen decomposition of the existing Covar matrix.
// Vectors and Values fields contain the results.
func (svd *SVD) SVD() error {
if svd.Covar == nil || svd.Covar.NumDims() != 2 {
return fmt.Errorf("svd.SVD: Covar matrix is nil or not 2D")
}
var eig mat.SVD
// note: MUST be a Float64 otherwise doesn't have Symmetric function
ok := eig.Factorize(svd.Covar, svd.Kind)
if !ok {
return fmt.Errorf("gonum SVD Factorize failed")
}
if svd.Kind > mat.SVDNone {
if svd.Vectors == nil {
svd.Vectors = &tensor.Float64{}
}
var ev mat.Dense
eig.UTo(&ev)
tensor.CopyDense(svd.Vectors, &ev)
}
nr := svd.Covar.DimSize(0)
if len(svd.Values) != nr {
svd.Values = make([]float64, nr)
}
eig.Values(svd.Values)
svd.Rank = eig.Rank(svd.Cond)
return nil
}
// ProjectColumn projects values from the given column of given table (via IndexView)
// onto the idx'th eigenvector (0 = largest eigenvalue, 1 = next, etc).
// Must have already called SVD() method.
func (svd *SVD) ProjectColumn(vals *[]float64, ix *table.IndexView, column string, idx int) error {
col, err := ix.Table.ColumnByName(column)
if err != nil {
return err
}
if svd.Vectors == nil || svd.Vectors.Len() == 0 {
return fmt.Errorf("SVD.ProjectColumn Vectors are nil: must call SVD first, with Kind = mat.SVDFull so that the vectors are returned")
}
nr := svd.Vectors.DimSize(0)
if idx >= nr {
return fmt.Errorf("SVD.ProjectColumn eigenvector index > rank of matrix")
}
cvec := make([]float64, nr)
// eidx := nr - 1 - idx // eigens in reverse order
vec := svd.Vectors.(*tensor.Float64)
for ri := 0; ri < nr; ri++ {
cvec[ri] = vec.Value([]int{ri, idx}) // vecs are in columns, reverse magnitude order
}
rows := ix.Len()
if len(*vals) != rows {
*vals = make([]float64, rows)
}
ln := col.Len()
sz := ln / col.DimSize(0) // size of cell
if sz != nr {
return fmt.Errorf("SVD.ProjectColumn column cell size != svd eigenvectors")
}
rdim := []int{0}
for row := 0; row < rows; row++ {
sum := 0.0
rdim[0] = ix.Indexes[row]
rt := col.SubSpace(rdim)
for ci := 0; ci < sz; ci++ {
sum += cvec[ci] * rt.Float1D(ci)
}
(*vals)[row] = sum
}
return nil
}
// ProjectColumnToTable projects values from the given column of given table (via IndexView)
// onto the given set of eigenvectors (idxs, 0 = largest eigenvalue, 1 = next, etc),
// and stores results along with labels from column labNm into results table.
// Must have already called SVD() method.
func (svd *SVD) ProjectColumnToTable(projections *table.Table, ix *table.IndexView, column, labNm string, idxs []int) error {
_, err := ix.Table.ColumnByName(column)
if errors.Log(err) != nil {
return err
}
if svd.Vectors == nil {
return fmt.Errorf("SVD.ProjectColumn Vectors are nil -- must call SVD first")
}
rows := ix.Len()
projections.DeleteAll()
pcolSt := 0
if labNm != "" {
projections.AddStringColumn(labNm)
pcolSt = 1
}
for _, idx := range idxs {
projections.AddFloat64Column(fmt.Sprintf("Projection%v", idx))
}
projections.SetNumRows(rows)
for ii, idx := range idxs {
pcol := projections.Columns[pcolSt+ii].(*tensor.Float64)
svd.ProjectColumn(&pcol.Values, ix, column, idx)
}
if labNm != "" {
lcol, err := ix.Table.ColumnByName(labNm)
if errors.Log(err) == nil {
plcol := projections.Columns[0]
for row := 0; row < rows; row++ {
plcol.SetString1D(row, lcol.String1D(row))
}
}
}
return nil
}
// Copyright (c) 2024, 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 simat
import (
"fmt"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/stats/metric"
"cogentcore.org/core/tensor/table"
)
// SimMat is a similarity / distance matrix with additional row and column
// labels for display purposes.
type SimMat struct {
// the similarity / distance matrix (typically an tensor.Float64)
Mat tensor.Tensor
// labels for the rows -- blank rows trigger generation of grouping lines
Rows []string
// labels for the columns -- blank columns trigger generation of grouping lines
Columns []string
}
// NewSimMat returns a new SimMat similarity matrix
func NewSimMat() *SimMat {
return &SimMat{}
}
// Init initializes SimMat with default Matrix and nil rows, cols
func (smat *SimMat) Init() {
smat.Mat = &tensor.Float64{}
smat.Mat.SetMetaData("grid-fill", "1") // best for sim mats -- can override later if need to
smat.Rows = nil
smat.Columns = nil
}
// TableColumnStd generates a similarity / distance matrix from given column name
// in given IndexView of an table.Table, and given standard metric function.
// if labNm is not empty, uses given column name for labels, which if blankRepeat
// is true are filtered so that any sequentially repeated labels are blank.
// This Std version is usable e.g., in Python where the func cannot be passed.
func (smat *SimMat) TableColumnStd(ix *table.IndexView, column, labNm string, blankRepeat bool, met metric.StdMetrics) error {
return smat.TableColumn(ix, column, labNm, blankRepeat, metric.StdFunc64(met))
}
// TableColumn generates a similarity / distance matrix from given column name
// in given IndexView of an table.Table, and given metric function.
// if labNm is not empty, uses given column name for labels, which if blankRepeat
// is true are filtered so that any sequentially repeated labels are blank.
func (smat *SimMat) TableColumn(ix *table.IndexView, column, labNm string, blankRepeat bool, mfun metric.Func64) error {
col, err := ix.Table.ColumnByName(column)
if err != nil {
return err
}
smat.Init()
sm := smat.Mat
rows := ix.Len()
nd := col.NumDims()
if nd < 2 || rows == 0 {
return fmt.Errorf("simat.Tensor: must have 2 or more dims and rows != 0")
}
ln := col.Len()
sz := ln / col.DimSize(0) // size of cell
sshp := []int{rows, rows}
sm.SetShape(sshp)
av := make([]float64, sz)
bv := make([]float64, sz)
ardim := []int{0}
brdim := []int{0}
sdim := []int{0, 0}
for ai := 0; ai < rows; ai++ {
ardim[0] = ix.Indexes[ai]
sdim[0] = ai
ar := col.SubSpace(ardim)
ar.Floats(&av)
for bi := 0; bi <= ai; bi++ { // lower diag
brdim[0] = ix.Indexes[bi]
sdim[1] = bi
br := col.SubSpace(brdim)
br.Floats(&bv)
sv := mfun(av, bv)
sm.SetFloat(sdim, sv)
}
}
// now fill in upper diagonal with values from lower diagonal
// note: assumes symmetric distance function
fdim := []int{0, 0}
for ai := 0; ai < rows; ai++ {
sdim[0] = ai
fdim[1] = ai
for bi := ai + 1; bi < rows; bi++ { // upper diag
fdim[0] = bi
sdim[1] = bi
sv := sm.Float(fdim)
sm.SetFloat(sdim, sv)
}
}
if nm, has := ix.Table.MetaData["name"]; has {
sm.SetMetaData("name", nm+"_"+column)
} else {
sm.SetMetaData("name", column)
}
if ds, has := ix.Table.MetaData["desc"]; has {
sm.SetMetaData("desc", ds)
}
if labNm == "" {
return nil
}
lc, err := ix.Table.ColumnByName(labNm)
if err != nil {
return err
}
smat.Rows = make([]string, rows)
last := ""
for r := 0; r < rows; r++ {
lbl := lc.String1D(ix.Indexes[r])
if blankRepeat && lbl == last {
continue
}
smat.Rows[r] = lbl
last = lbl
}
smat.Columns = smat.Rows // identical
return nil
}
// BlankRepeat returns string slice with any sequentially repeated strings blanked out
func BlankRepeat(str []string) []string {
sz := len(str)
br := make([]string, sz)
last := ""
for r, s := range str {
if s == last {
continue
}
br[r] = s
last = s
}
return br
}
// Copyright (c) 2024, 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 simat
import (
"fmt"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/stats/metric"
)
// Tensor computes a similarity / distance matrix on tensor
// using given metric function. Outer-most dimension ("rows") is
// used as "indexical" dimension and all other dimensions within that
// are compared.
// Results go in smat which is ensured to have proper square 2D shape
// (rows * rows).
func Tensor(smat tensor.Tensor, a tensor.Tensor, mfun metric.Func64) error {
rows := a.DimSize(0)
nd := a.NumDims()
if nd < 2 || rows == 0 {
return fmt.Errorf("simat.Tensor: must have 2 or more dims and rows != 0")
}
ln := a.Len()
sz := ln / rows
sshp := []int{rows, rows}
smat.SetShape(sshp)
av := make([]float64, sz)
bv := make([]float64, sz)
ardim := []int{0}
brdim := []int{0}
sdim := []int{0, 0}
for ai := 0; ai < rows; ai++ {
ardim[0] = ai
sdim[0] = ai
ar := a.SubSpace(ardim)
ar.Floats(&av)
for bi := 0; bi <= ai; bi++ { // lower diag
brdim[0] = bi
sdim[1] = bi
br := a.SubSpace(brdim)
br.Floats(&bv)
sv := mfun(av, bv)
smat.SetFloat(sdim, sv)
}
}
// now fill in upper diagonal with values from lower diagonal
// note: assumes symmetric distance function
fdim := []int{0, 0}
for ai := 0; ai < rows; ai++ {
sdim[0] = ai
fdim[1] = ai
for bi := ai + 1; bi < rows; bi++ { // upper diag
fdim[0] = bi
sdim[1] = bi
sv := smat.Float(fdim)
smat.SetFloat(sdim, sv)
}
}
return nil
}
// Tensors computes a similarity / distance matrix on two tensors
// using given metric function. Outer-most dimension ("rows") is
// used as "indexical" dimension and all other dimensions within that
// are compared. Resulting reduced 2D shape of two tensors must be
// the same (returns error if not).
// Rows of smat = a, cols = b
func Tensors(smat tensor.Tensor, a, b tensor.Tensor, mfun metric.Func64) error {
arows := a.DimSize(0)
and := a.NumDims()
brows := b.DimSize(0)
bnd := b.NumDims()
if and < 2 || bnd < 2 || arows == 0 || brows == 0 {
return fmt.Errorf("simat.Tensors: must have 2 or more dims and rows != 0")
}
alen := a.Len()
asz := alen / arows
blen := b.Len()
bsz := blen / brows
if asz != bsz {
return fmt.Errorf("simat.Tensors: size of inner dimensions must be same")
}
sshp := []int{arows, brows}
smat.SetShape(sshp, "a", "b")
av := make([]float64, asz)
bv := make([]float64, bsz)
ardim := []int{0}
brdim := []int{0}
sdim := []int{0, 0}
for ai := 0; ai < arows; ai++ {
ardim[0] = ai
sdim[0] = ai
ar := a.SubSpace(ardim)
ar.Floats(&av)
for bi := 0; bi < brows; bi++ {
brdim[0] = bi
sdim[1] = bi
br := b.SubSpace(brdim)
br.Floats(&bv)
sv := mfun(av, bv)
smat.SetFloat(sdim, sv)
}
}
return nil
}
// TensorStd computes a similarity / distance matrix on tensor
// using given Std metric function. Outer-most dimension ("rows") is
// used as "indexical" dimension and all other dimensions within that
// are compared.
// Results go in smat which is ensured to have proper square 2D shape
// (rows * rows).
// This Std version is usable e.g., in Python where the func cannot be passed.
func TensorStd(smat tensor.Tensor, a tensor.Tensor, met metric.StdMetrics) error {
return Tensor(smat, a, metric.StdFunc64(met))
}
// TensorsStd computes a similarity / distance matrix on two tensors
// using given Std metric function. Outer-most dimension ("rows") is
// used as "indexical" dimension and all other dimensions within that
// are compared. Resulting reduced 2D shape of two tensors must be
// the same (returns error if not).
// Rows of smat = a, cols = b
// This Std version is usable e.g., in Python where the func cannot be passed.
func TensorsStd(smat tensor.Tensor, a, b tensor.Tensor, met metric.StdMetrics) error {
return Tensors(smat, a, b, metric.StdFunc64(met))
}
// Copyright (c) 2024, 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 split
import (
"fmt"
"cogentcore.org/core/tensor/stats/stats"
"cogentcore.org/core/tensor/table"
)
// AggIndex performs aggregation using given standard statistic (e.g., Mean) across
// all splits, and returns the SplitAgg container of the results, which are also
// stored in the Splits. Column is specified by index.
func AggIndex(spl *table.Splits, colIndex int, stat stats.Stats) *table.SplitAgg {
ag := spl.AddAgg(stat.String(), colIndex)
for _, sp := range spl.Splits {
agv := stats.StatIndex(sp, colIndex, stat)
ag.Aggs = append(ag.Aggs, agv)
}
return ag
}
// AggColumn performs aggregation using given standard statistic (e.g., Mean) across
// all splits, and returns the SplitAgg container of the results, which are also
// stored in the Splits. Column is specified by name; returns error for bad column name.
func AggColumn(spl *table.Splits, column string, stat stats.Stats) (*table.SplitAgg, error) {
dt := spl.Table()
if dt == nil {
return nil, fmt.Errorf("split.AggTry: No splits to aggregate over")
}
colIndex, err := dt.ColumnIndex(column)
if err != nil {
return nil, err
}
return AggIndex(spl, colIndex, stat), nil
}
// AggAllNumericColumns performs aggregation using given standard aggregation function across
// all splits, for all number-valued columns in the table.
func AggAllNumericColumns(spl *table.Splits, stat stats.Stats) {
dt := spl.Table()
for ci, cl := range dt.Columns {
if cl.IsString() {
continue
}
AggIndex(spl, ci, stat)
}
}
///////////////////////////////////////////////////
// Desc
// DescIndex performs aggregation using standard statistics across
// all splits, and stores results in the Splits. Column is specified by index.
func DescIndex(spl *table.Splits, colIndex int) {
dt := spl.Table()
if dt == nil {
return
}
col := dt.Columns[colIndex]
sts := stats.DescStats
if col.NumDims() > 1 { // nd cannot do qiles
sts = stats.DescStatsND
}
for _, st := range sts {
AggIndex(spl, colIndex, st)
}
}
// DescColumn performs aggregation using standard statistics across
// all splits, and stores results in the Splits.
// Column is specified by name; returns error for bad column name.
func DescColumn(spl *table.Splits, column string) error {
dt := spl.Table()
if dt == nil {
return fmt.Errorf("split.DescTry: No splits to aggregate over")
}
colIndex, err := dt.ColumnIndex(column)
if err != nil {
return err
}
DescIndex(spl, colIndex)
return nil
}
// Copyright (c) 2024, 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 split
//go:generate core generate
import (
"log"
"slices"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/tensor/table"
)
// All returns a single "split" with all of the rows in given view
// useful for leveraging the aggregation management functions in splits
func All(ix *table.IndexView) *table.Splits {
spl := &table.Splits{}
spl.Levels = []string{"All"}
spl.New(ix.Table, []string{"All"}, ix.Indexes...)
return spl
}
// GroupByIndex returns a new Splits set based on the groups of values
// across the given set of column indexes.
// Uses a stable sort on columns, so ordering of other dimensions is preserved.
func GroupByIndex(ix *table.IndexView, colIndexes []int) *table.Splits {
nc := len(colIndexes)
if nc == 0 || ix.Table == nil {
return nil
}
if ix.Table.ColumnNames == nil {
log.Println("split.GroupBy: Table does not have any column names -- will not work")
return nil
}
spl := &table.Splits{}
spl.Levels = make([]string, nc)
for i, ci := range colIndexes {
spl.Levels[i] = ix.Table.ColumnNames[ci]
}
srt := ix.Clone()
srt.SortStableColumns(colIndexes, true) // important for consistency
lstValues := make([]string, nc)
curValues := make([]string, nc)
var curIx *table.IndexView
for _, rw := range srt.Indexes {
diff := false
for i, ci := range colIndexes {
cl := ix.Table.Columns[ci]
cv := cl.String1D(rw)
curValues[i] = cv
if cv != lstValues[i] {
diff = true
}
}
if diff || curIx == nil {
curIx = spl.New(ix.Table, curValues, rw)
copy(lstValues, curValues)
} else {
curIx.AddIndex(rw)
}
}
if spl.Len() == 0 { // prevent crashing from subsequent ops: add an empty split
spl.New(ix.Table, curValues) // no rows added here
}
return spl
}
// GroupBy returns a new Splits set based on the groups of values
// across the given set of column names.
// Uses a stable sort on columns, so ordering of other dimensions is preserved.
func GroupBy(ix *table.IndexView, columns ...string) *table.Splits {
return GroupByIndex(ix, errors.Log1(ix.Table.ColumnIndexesByNames(columns...)))
}
// GroupByFunc returns a new Splits set based on the given function
// which returns value(s) to group on for each row of the table.
// The function should always return the same number of values -- if
// it doesn't behavior is undefined.
// Uses a stable sort on columns, so ordering of other dimensions is preserved.
func GroupByFunc(ix *table.IndexView, fun func(row int) []string) *table.Splits {
if ix.Table == nil {
return nil
}
// save function values
funvals := make(map[int][]string, ix.Len())
nv := 0 // number of valeus
for _, rw := range ix.Indexes {
sv := fun(rw)
if nv == 0 {
nv = len(sv)
}
funvals[rw] = slices.Clone(sv)
}
srt := ix.Clone()
srt.SortStable(func(et *table.Table, i, j int) bool { // sort based on given function values
fvi := funvals[i]
fvj := funvals[j]
for fi := 0; fi < nv; fi++ {
if fvi[fi] < fvj[fi] {
return true
} else if fvi[fi] > fvj[fi] {
return false
}
}
return false
})
// now do our usual grouping operation
spl := &table.Splits{}
lstValues := make([]string, nv)
var curIx *table.IndexView
for _, rw := range srt.Indexes {
curValues := funvals[rw]
diff := (curIx == nil)
if !diff {
for fi := 0; fi < nv; fi++ {
if lstValues[fi] != curValues[fi] {
diff = true
break
}
}
}
if diff {
curIx = spl.New(ix.Table, curValues, rw)
copy(lstValues, curValues)
} else {
curIx.AddIndex(rw)
}
}
return spl
}
// Copyright (c) 2024, 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 split
import (
"fmt"
"math"
"cogentcore.org/core/tensor/table"
"gonum.org/v1/gonum/floats"
)
// Permuted generates permuted random splits of table rows, using given list of probabilities,
// which will be normalized to sum to 1 (error returned if sum = 0)
// names are optional names for each split (e.g., Train, Test) which will be
// used to label the Values of the resulting Splits.
func Permuted(ix *table.IndexView, probs []float64, names []string) (*table.Splits, error) {
if ix == nil || ix.Len() == 0 {
return nil, fmt.Errorf("split.Random table is nil / empty")
}
np := len(probs)
if len(names) > 0 && len(names) != np {
return nil, fmt.Errorf("split.Random names not same len as probs")
}
sum := floats.Sum(probs)
if sum == 0 {
return nil, fmt.Errorf("split.Random probs sum to 0")
}
nr := ix.Len()
ns := make([]int, np)
cum := 0
fnr := float64(nr)
for i, p := range probs {
p /= sum
per := int(math.Round(p * fnr))
if cum+per > nr {
per = nr - cum
if per <= 0 {
break
}
}
ns[i] = per
cum += per
}
spl := &table.Splits{}
perm := ix.Clone()
perm.Permuted()
cum = 0
spl.SetLevels("permuted")
for i, n := range ns {
nm := ""
if names != nil {
nm = names[i]
} else {
nm = fmt.Sprintf("p=%v", probs[i]/sum)
}
spl.New(ix.Table, []string{nm}, perm.Indexes[cum:cum+n]...)
cum += n
}
return spl, nil
}
// Copyright (c) 2024, 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 stats
import (
"cogentcore.org/core/tensor/table"
)
// DescStats are all the standard stats
var DescStats = []Stats{Count, Mean, Std, Sem, Min, Max, Q1, Median, Q3}
// DescStatsND are all the standard stats for n-dimensional (n > 1) data -- cannot do quantiles
var DescStatsND = []Stats{Count, Mean, Std, Sem, Min, Max}
// DescAll returns a table of standard descriptive stats for
// all numeric columns in given table, operating over all non-Null, non-NaN elements
// in each column.
func DescAll(ix *table.IndexView) *table.Table {
st := ix.Table
nAgg := len(DescStats)
dt := table.NewTable().SetNumRows(nAgg)
dt.AddStringColumn("Stat")
for ci := range st.Columns {
col := st.Columns[ci]
if col.IsString() {
continue
}
dt.AddFloat64TensorColumn(st.ColumnNames[ci], col.Shape().Sizes[1:], col.Shape().Names[1:]...)
}
dtnm := dt.Columns[0]
dtci := 1
qs := []float64{.25, .5, .75}
sq := len(DescStatsND)
for ci := range st.Columns {
col := st.Columns[ci]
if col.IsString() {
continue
}
_, csz := col.RowCellSize()
dtst := dt.Columns[dtci]
for i, styp := range DescStatsND {
ag := StatIndex(ix, ci, styp)
si := i * csz
for j := 0; j < csz; j++ {
dtst.SetFloat1D(si+j, ag[j])
}
if dtci == 1 {
dtnm.SetString1D(i, styp.String())
}
}
if col.NumDims() == 1 {
qvs := QuantilesIndex(ix, ci, qs)
for i, qv := range qvs {
dtst.SetFloat1D(sq+i, qv)
dtnm.SetString1D(sq+i, DescStats[sq+i].String())
}
}
dtci++
}
return dt
}
// DescIndex returns a table of standard descriptive aggregates
// of non-Null, non-NaN elements in given IndexView indexed view of an
// table.Table, for given column index.
func DescIndex(ix *table.IndexView, colIndex int) *table.Table {
st := ix.Table
col := st.Columns[colIndex]
stats := DescStats
if col.NumDims() > 1 { // nd cannot do qiles
stats = DescStatsND
}
nAgg := len(stats)
dt := table.NewTable().SetNumRows(nAgg)
dt.AddStringColumn("Stat")
dt.AddFloat64TensorColumn(st.ColumnNames[colIndex], col.Shape().Sizes[1:], col.Shape().Names[1:]...)
dtnm := dt.Columns[0]
dtst := dt.Columns[1]
_, csz := col.RowCellSize()
for i, styp := range DescStatsND {
ag := StatIndex(ix, colIndex, styp)
si := i * csz
for j := 0; j < csz; j++ {
dtst.SetFloat1D(si+j, ag[j])
}
dtnm.SetString1D(i, styp.String())
}
if col.NumDims() == 1 {
sq := len(DescStatsND)
qs := []float64{.25, .5, .75}
qvs := QuantilesIndex(ix, colIndex, qs)
for i, qv := range qvs {
dtst.SetFloat1D(sq+i, qv)
dtnm.SetString1D(sq+i, DescStats[sq+i].String())
}
}
return dt
}
// DescColumn returns a table of standard descriptive stats
// of non-NaN elements in given IndexView indexed view of an
// table.Table, for given column name.
// If name not found, returns error message.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func DescColumn(ix *table.IndexView, column string) (*table.Table, error) {
colIndex, err := ix.Table.ColumnIndex(column)
if err != nil {
return nil, err
}
return DescIndex(ix, colIndex), nil
}
// Code generated by "core generate"; DO NOT EDIT.
package stats
import (
"cogentcore.org/core/enums"
)
var _StatsValues = []Stats{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}
// StatsN is the highest valid value for type Stats, plus one.
const StatsN Stats = 20
var _StatsValueMap = map[string]Stats{`Count`: 0, `Sum`: 1, `Prod`: 2, `Min`: 3, `Max`: 4, `MinAbs`: 5, `MaxAbs`: 6, `Mean`: 7, `Var`: 8, `Std`: 9, `Sem`: 10, `L1Norm`: 11, `SumSq`: 12, `L2Norm`: 13, `VarPop`: 14, `StdPop`: 15, `SemPop`: 16, `Median`: 17, `Q1`: 18, `Q3`: 19}
var _StatsDescMap = map[Stats]string{0: `count of number of elements`, 1: `sum of elements`, 2: `product of elements`, 3: `minimum value`, 4: `max maximum value`, 5: `minimum absolute value`, 6: `maximum absolute value`, 7: `mean mean value`, 8: `sample variance (squared diffs from mean, divided by n-1)`, 9: `sample standard deviation (sqrt of Var)`, 10: `sample standard error of the mean (Std divided by sqrt(n))`, 11: `L1 Norm: sum of absolute values`, 12: `sum of squared values`, 13: `L2 Norm: square-root of sum-of-squares`, 14: `population variance (squared diffs from mean, divided by n)`, 15: `population standard deviation (sqrt of VarPop)`, 16: `population standard error of the mean (StdPop divided by sqrt(n))`, 17: `middle value in sorted ordering`, 18: `Q1 first quartile = 25%ile value = .25 quantile value`, 19: `Q3 third quartile = 75%ile value = .75 quantile value`}
var _StatsMap = map[Stats]string{0: `Count`, 1: `Sum`, 2: `Prod`, 3: `Min`, 4: `Max`, 5: `MinAbs`, 6: `MaxAbs`, 7: `Mean`, 8: `Var`, 9: `Std`, 10: `Sem`, 11: `L1Norm`, 12: `SumSq`, 13: `L2Norm`, 14: `VarPop`, 15: `StdPop`, 16: `SemPop`, 17: `Median`, 18: `Q1`, 19: `Q3`}
// String returns the string representation of this Stats value.
func (i Stats) String() string { return enums.String(i, _StatsMap) }
// SetString sets the Stats value from its string representation,
// and returns an error if the string is invalid.
func (i *Stats) SetString(s string) error { return enums.SetString(i, s, _StatsValueMap, "Stats") }
// Int64 returns the Stats value as an int64.
func (i Stats) Int64() int64 { return int64(i) }
// SetInt64 sets the Stats value from an int64.
func (i *Stats) SetInt64(in int64) { *i = Stats(in) }
// Desc returns the description of the Stats value.
func (i Stats) Desc() string { return enums.Desc(i, _StatsDescMap) }
// StatsValues returns all possible values for the type Stats.
func StatsValues() []Stats { return _StatsValues }
// Values returns all possible values for the type Stats.
func (i Stats) Values() []enums.Enum { return enums.Values(_StatsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Stats) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Stats) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Stats") }
// Copyright (c) 2024, 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 stats
import (
"math"
"cogentcore.org/core/math32"
)
// Stat32 returns statistic according to given Stats type applied
// to all non-NaN elements in given slice of float32
func Stat32(a []float32, stat Stats) float32 {
switch stat {
case Count:
return Count32(a)
case Sum:
return Sum32(a)
case Prod:
return Prod32(a)
case Min:
return Min32(a)
case Max:
return Max32(a)
case MinAbs:
return MinAbs32(a)
case MaxAbs:
return MaxAbs32(a)
case Mean:
return Mean32(a)
case Var:
return Var32(a)
case Std:
return Std32(a)
case Sem:
return Sem32(a)
case L1Norm:
return L1Norm32(a)
case SumSq:
return SumSq32(a)
case L2Norm:
return L2Norm32(a)
case VarPop:
return VarPop32(a)
case StdPop:
return StdPop32(a)
case SemPop:
return SemPop32(a)
// case Median:
// return Median32(a)
// case Q1:
// return Q132(a)
// case Q3:
// return Q332(a)
}
return 0
}
// Stat64 returns statistic according to given Stats type applied
// to all non-NaN elements in given slice of float64
func Stat64(a []float64, stat Stats) float64 {
switch stat {
case Count:
return Count64(a)
case Sum:
return Sum64(a)
case Prod:
return Prod64(a)
case Min:
return Min64(a)
case Max:
return Max64(a)
case MinAbs:
return MinAbs64(a)
case MaxAbs:
return MaxAbs64(a)
case Mean:
return Mean64(a)
case Var:
return Var64(a)
case Std:
return Std64(a)
case Sem:
return Sem64(a)
case L1Norm:
return L1Norm64(a)
case SumSq:
return SumSq64(a)
case L2Norm:
return L2Norm64(a)
case VarPop:
return VarPop64(a)
case StdPop:
return StdPop64(a)
case SemPop:
return SemPop64(a)
// case Median:
// return Median64(a)
// case Q1:
// return Q164(a)
// case Q3:
// return Q364(a)
}
return 0
}
///////////////////////////////////////////
// Count
// Count32 computes the number of non-NaN vector values.
// Skips NaN's
func Count32(a []float32) float32 {
n := 0
for _, av := range a {
if math32.IsNaN(av) {
continue
}
n++
}
return float32(n)
}
// Count64 computes the number of non-NaN vector values.
// Skips NaN's
func Count64(a []float64) float64 {
n := 0
for _, av := range a {
if math.IsNaN(av) {
continue
}
n++
}
return float64(n)
}
///////////////////////////////////////////
// Sum
// Sum32 computes the sum of vector values.
// Skips NaN's
func Sum32(a []float32) float32 {
s := float32(0)
for _, av := range a {
if math32.IsNaN(av) {
continue
}
s += av
}
return s
}
// Sum64 computes the sum of vector values.
// Skips NaN's
func Sum64(a []float64) float64 {
s := float64(0)
for _, av := range a {
if math.IsNaN(av) {
continue
}
s += av
}
return s
}
///////////////////////////////////////////
// Prod
// Prod32 computes the product of vector values.
// Skips NaN's
func Prod32(a []float32) float32 {
s := float32(1)
for _, av := range a {
if math32.IsNaN(av) {
continue
}
s *= av
}
return s
}
// Prod64 computes the product of vector values.
// Skips NaN's
func Prod64(a []float64) float64 {
s := float64(1)
for _, av := range a {
if math.IsNaN(av) {
continue
}
s *= av
}
return s
}
///////////////////////////////////////////
// Min
// Min32 computes the max over vector values.
// Skips NaN's
func Min32(a []float32) float32 {
m := float32(math.MaxFloat32)
for _, av := range a {
if math32.IsNaN(av) {
continue
}
m = math32.Min(m, av)
}
return m
}
// MinIndex32 computes the min over vector values, and returns index of min as well
// Skips NaN's
func MinIndex32(a []float32) (float32, int) {
m := float32(math.MaxFloat32)
mi := -1
for i, av := range a {
if math32.IsNaN(av) {
continue
}
if av < m {
m = av
mi = i
}
}
return m, mi
}
// Min64 computes the max over vector values.
// Skips NaN's
func Min64(a []float64) float64 {
m := float64(math.MaxFloat64)
for _, av := range a {
if math.IsNaN(av) {
continue
}
m = math.Min(m, av)
}
return m
}
// MinIndex64 computes the min over vector values, and returns index of min as well
// Skips NaN's
func MinIndex64(a []float64) (float64, int) {
m := float64(math.MaxFloat64)
mi := -1
for i, av := range a {
if math.IsNaN(av) {
continue
}
if av < m {
m = av
mi = i
}
}
return m, mi
}
///////////////////////////////////////////
// Max
// Max32 computes the max over vector values.
// Skips NaN's
func Max32(a []float32) float32 {
m := float32(-math.MaxFloat32)
for _, av := range a {
if math32.IsNaN(av) {
continue
}
m = math32.Max(m, av)
}
return m
}
// MaxIndex32 computes the max over vector values, and returns index of max as well
// Skips NaN's
func MaxIndex32(a []float32) (float32, int) {
m := float32(-math.MaxFloat32)
mi := -1
for i, av := range a {
if math32.IsNaN(av) {
continue
}
if av > m {
m = av
mi = i
}
}
return m, mi
}
// Max64 computes the max over vector values.
// Skips NaN's
func Max64(a []float64) float64 {
m := float64(-math.MaxFloat64)
for _, av := range a {
if math.IsNaN(av) {
continue
}
m = math.Max(m, av)
}
return m
}
// MaxIndex64 computes the max over vector values, and returns index of max as well
// Skips NaN's
func MaxIndex64(a []float64) (float64, int) {
m := float64(-math.MaxFloat64)
mi := -1
for i, av := range a {
if math.IsNaN(av) {
continue
}
if av > m {
m = av
mi = i
}
}
return m, mi
}
///////////////////////////////////////////
// MinAbs
// MinAbs32 computes the max of absolute value over vector values.
// Skips NaN's
func MinAbs32(a []float32) float32 {
m := float32(math.MaxFloat32)
for _, av := range a {
if math32.IsNaN(av) {
continue
}
m = math32.Min(m, math32.Abs(av))
}
return m
}
// MinAbs64 computes the max over vector values.
// Skips NaN's
func MinAbs64(a []float64) float64 {
m := float64(math.MaxFloat64)
for _, av := range a {
if math.IsNaN(av) {
continue
}
m = math.Min(m, math.Abs(av))
}
return m
}
///////////////////////////////////////////
// MaxAbs
// MaxAbs32 computes the max of absolute value over vector values.
// Skips NaN's
func MaxAbs32(a []float32) float32 {
m := float32(0)
for _, av := range a {
if math32.IsNaN(av) {
continue
}
m = math32.Max(m, math32.Abs(av))
}
return m
}
// MaxAbs64 computes the max over vector values.
// Skips NaN's
func MaxAbs64(a []float64) float64 {
m := float64(0)
for _, av := range a {
if math.IsNaN(av) {
continue
}
m = math.Max(m, math.Abs(av))
}
return m
}
///////////////////////////////////////////
// Mean
// Mean32 computes the mean of the vector (sum / N).
// Skips NaN's
func Mean32(a []float32) float32 {
s := float32(0)
n := 0
for _, av := range a {
if math32.IsNaN(av) {
continue
}
s += av
n++
}
if n > 0 {
s /= float32(n)
}
return s
}
// Mean64 computes the mean of the vector (sum / N).
// Skips NaN's
func Mean64(a []float64) float64 {
s := float64(0)
n := 0
for _, av := range a {
if math.IsNaN(av) {
continue
}
s += av
n++
}
if n > 0 {
s /= float64(n)
}
return s
}
///////////////////////////////////////////
// Var
// Var32 returns the sample variance of non-NaN elements.
func Var32(a []float32) float32 {
mean := Mean32(a)
n := 0
s := float32(0)
for _, av := range a {
if math32.IsNaN(av) {
continue
}
dv := av - mean
s += dv * dv
n++
}
if n > 1 {
s /= float32(n - 1)
}
return s
}
// Var64 returns the sample variance of non-NaN elements.
func Var64(a []float64) float64 {
mean := Mean64(a)
n := 0
s := float64(0)
for _, av := range a {
if math.IsNaN(av) {
continue
}
dv := av - mean
s += dv * dv
n++
}
if n > 1 {
s /= float64(n - 1)
}
return s
}
///////////////////////////////////////////
// Std
// Std32 returns the sample standard deviation of non-NaN elements in vector.
func Std32(a []float32) float32 {
return math32.Sqrt(Var32(a))
}
// Std64 returns the sample standard deviation of non-NaN elements in vector.
func Std64(a []float64) float64 {
return math.Sqrt(Var64(a))
}
///////////////////////////////////////////
// Sem
// Sem32 returns the sample standard error of the mean of non-NaN elements in vector.
func Sem32(a []float32) float32 {
cnt := Count32(a)
if cnt < 2 {
return 0
}
return Std32(a) / math32.Sqrt(cnt)
}
// Sem64 returns the sample standard error of the mean of non-NaN elements in vector.
func Sem64(a []float64) float64 {
cnt := Count64(a)
if cnt < 2 {
return 0
}
return Std64(a) / math.Sqrt(cnt)
}
///////////////////////////////////////////
// L1Norm
// L1Norm32 computes the sum of absolute values (L1 Norm).
// Skips NaN's
func L1Norm32(a []float32) float32 {
ss := float32(0)
for _, av := range a {
if math32.IsNaN(av) {
continue
}
ss += math32.Abs(av)
}
return ss
}
// L1Norm64 computes the sum of absolute values (L1 Norm).
// Skips NaN's
func L1Norm64(a []float64) float64 {
ss := float64(0)
for _, av := range a {
if math.IsNaN(av) {
continue
}
ss += math.Abs(av)
}
return ss
}
///////////////////////////////////////////
// SumSquares
// SumSq32 computes the sum-of-squares of vector.
// Skips NaN's. Uses optimized algorithm from BLAS that avoids numerical overflow.
func SumSq32(a []float32) float32 {
n := len(a)
if n < 2 {
if n == 1 {
return math32.Abs(a[0])
}
return 0
}
var (
scale float32 = 0
sumSquares float32 = 1
)
for _, v := range a {
if v == 0 || math32.IsNaN(v) {
continue
}
absxi := math32.Abs(v)
if scale < absxi {
sumSquares = 1 + sumSquares*(scale/absxi)*(scale/absxi)
scale = absxi
} else {
sumSquares = sumSquares + (absxi/scale)*(absxi/scale)
}
}
if math32.IsInf(scale, 1) {
return math32.Inf(1)
}
return scale * scale * sumSquares
}
// SumSq64 computes the sum-of-squares of vector.
// Skips NaN's. Uses optimized algorithm from BLAS that avoids numerical overflow.
func SumSq64(a []float64) float64 {
n := len(a)
if n < 2 {
if n == 1 {
return math.Abs(a[0])
}
return 0
}
var (
scale float64 = 0
ss float64 = 1
)
for _, v := range a {
if v == 0 || math.IsNaN(v) {
continue
}
absxi := math.Abs(v)
if scale < absxi {
ss = 1 + ss*(scale/absxi)*(scale/absxi)
scale = absxi
} else {
ss = ss + (absxi/scale)*(absxi/scale)
}
}
if math.IsInf(scale, 1) {
return math.Inf(1)
}
return scale * scale * ss
}
///////////////////////////////////////////
// L2Norm
// L2Norm32 computes the square-root of sum-of-squares of vector, i.e., the L2 norm.
// Skips NaN's. Uses optimized algorithm from BLAS that avoids numerical overflow.
func L2Norm32(a []float32) float32 {
n := len(a)
if n < 2 {
if n == 1 {
return math32.Abs(a[0])
}
return 0
}
var (
scale float32 = 0
ss float32 = 1
)
for _, v := range a {
if v == 0 || math32.IsNaN(v) {
continue
}
absxi := math32.Abs(v)
if scale < absxi {
ss = 1 + ss*(scale/absxi)*(scale/absxi)
scale = absxi
} else {
ss = ss + (absxi/scale)*(absxi/scale)
}
}
if math32.IsInf(scale, 1) {
return math32.Inf(1)
}
return scale * math32.Sqrt(ss)
}
// L2Norm64 computes the square-root of sum-of-squares of vector, i.e., the L2 norm.
// Skips NaN's. Uses optimized algorithm from BLAS that avoids numerical overflow.
func L2Norm64(a []float64) float64 {
n := len(a)
if n < 2 {
if n == 1 {
return math.Abs(a[0])
}
return 0
}
var (
scale float64 = 0
ss float64 = 1
)
for _, v := range a {
if v == 0 || math.IsNaN(v) {
continue
}
absxi := math.Abs(v)
if scale < absxi {
ss = 1 + ss*(scale/absxi)*(scale/absxi)
scale = absxi
} else {
ss = ss + (absxi/scale)*(absxi/scale)
}
}
if math.IsInf(scale, 1) {
return math.Inf(1)
}
return scale * math.Sqrt(ss)
}
///////////////////////////////////////////
// VarPop
// VarPop32 returns the population variance of non-NaN elements.
func VarPop32(a []float32) float32 {
mean := Mean32(a)
n := 0
s := float32(0)
for _, av := range a {
if math32.IsNaN(av) {
continue
}
dv := av - mean
s += dv * dv
n++
}
if n > 0 {
s /= float32(n)
}
return s
}
// VarPop64 returns the population variance of non-NaN elements.
func VarPop64(a []float64) float64 {
mean := Mean64(a)
n := 0
s := float64(0)
for _, av := range a {
if math.IsNaN(av) {
continue
}
dv := av - mean
s += dv * dv
n++
}
if n > 0 {
s /= float64(n)
}
return s
}
///////////////////////////////////////////
// StdPop
// StdPop32 returns the population standard deviation of non-NaN elements in vector.
func StdPop32(a []float32) float32 {
return math32.Sqrt(VarPop32(a))
}
// StdPop64 returns the population standard deviation of non-NaN elements in vector.
func StdPop64(a []float64) float64 {
return math.Sqrt(VarPop64(a))
}
///////////////////////////////////////////
// SemPop
// SemPop32 returns the population standard error of the mean of non-NaN elements in vector.
func SemPop32(a []float32) float32 {
cnt := Count32(a)
if cnt < 2 {
return 0
}
return StdPop32(a) / math32.Sqrt(cnt)
}
// SemPop64 returns the population standard error of the mean of non-NaN elements in vector.
func SemPop64(a []float64) float64 {
cnt := Count64(a)
if cnt < 2 {
return 0
}
return StdPop64(a) / math.Sqrt(cnt)
}
// Copyright (c) 2024, 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 stats
import "math"
// These are standard StatFunc functions that can operate on tensor.Tensor
// or table.Table, using float64 values
// StatFunc is an statistic function that incrementally updates agg
// aggregation value from each element in the tensor in turn.
// Returns new agg value that will be passed into next item as agg.
type StatFunc func(idx int, val float64, agg float64) float64
// CountFunc is an StatFunc that computes number of elements (non-Null, non-NaN)
// Use 0 as initial value.
func CountFunc(idx int, val float64, agg float64) float64 {
return agg + 1
}
// SumFunc is an StatFunc that computes a sum aggregate.
// use 0 as initial value.
func SumFunc(idx int, val float64, agg float64) float64 {
return agg + val
}
// Prodfunc is an StatFunc that computes a product aggregate.
// use 1 as initial value.
func ProdFunc(idx int, val float64, agg float64) float64 {
return agg * val
}
// MinFunc is an StatFunc that computes a min aggregate.
// use math.MaxFloat64 for initial agg value.
func MinFunc(idx int, val float64, agg float64) float64 {
return math.Min(agg, val)
}
// MaxFunc is an StatFunc that computes a max aggregate.
// use -math.MaxFloat64 for initial agg value.
func MaxFunc(idx int, val float64, agg float64) float64 {
return math.Max(agg, val)
}
// MinAbsFunc is an StatFunc that computes a min aggregate.
// use math.MaxFloat64 for initial agg value.
func MinAbsFunc(idx int, val float64, agg float64) float64 {
return math.Min(agg, math.Abs(val))
}
// MaxAbsFunc is an StatFunc that computes a max aggregate.
// use -math.MaxFloat64 for initial agg value.
func MaxAbsFunc(idx int, val float64, agg float64) float64 {
return math.Max(agg, math.Abs(val))
}
// L1NormFunc is an StatFunc that computes the L1 norm: sum of absolute values
// use 0 as initial value.
func L1NormFunc(idx int, val float64, agg float64) float64 {
return agg + math.Abs(val)
}
// Note: SumSq is not numerically stable for large N in simple func form.
// Copyright (c) 2024, 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 stats
import (
"cogentcore.org/core/base/errors"
"cogentcore.org/core/tensor/table"
)
// IfFunc is used for the *If aggregators -- counted if it returns true
type IfFunc func(idx int, val float64) bool
///////////////////////////////////////////////////
// CountIf
// CountIfIndex returns the count of true return values for given IfFunc on
// non-NaN elements in given IndexView indexed view of an
// table.Table, for given column index.
// Return value(s) is size of column cell: 1 for scalar 1D columns
// and N for higher-dimensional columns.
func CountIfIndex(ix *table.IndexView, colIndex int, iffun IfFunc) []float64 {
return StatIndexFunc(ix, colIndex, 0, func(idx int, val float64, agg float64) float64 {
if iffun(idx, val) {
return agg + 1
}
return agg
})
}
// CountIfColumn returns the count of true return values for given IfFunc on
// non-NaN elements in given IndexView indexed view of an
// table.Table, for given column name.
// If name not found, nil is returned.
// Return value(s) is size of column cell: 1 for scalar 1D columns
// and N for higher-dimensional columns.
func CountIfColumn(ix *table.IndexView, column string, iffun IfFunc) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return CountIfIndex(ix, colIndex, iffun)
}
///////////////////////////////////////////////////
// PropIf
// PropIfIndex returns the proportion (0-1) of true return values for given IfFunc on
// non-Null, non-NaN elements in given IndexView indexed view of an
// table.Table, for given column index.
// Return value(s) is size of column cell: 1 for scalar 1D columns
// and N for higher-dimensional columns.
func PropIfIndex(ix *table.IndexView, colIndex int, iffun IfFunc) []float64 {
cnt := CountIndex(ix, colIndex)
if cnt == nil {
return nil
}
pif := CountIfIndex(ix, colIndex, iffun)
for i := range pif {
if cnt[i] > 0 {
pif[i] /= cnt[i]
}
}
return pif
}
// PropIfColumn returns the proportion (0-1) of true return values for given IfFunc on
// non-NaN elements in given IndexView indexed view of an
// table.Table, for given column name.
// If name not found, nil is returned -- use Try version for error message.
// Return value(s) is size of column cell: 1 for scalar 1D columns
// and N for higher-dimensional columns.
func PropIfColumn(ix *table.IndexView, column string, iffun IfFunc) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return PropIfIndex(ix, colIndex, iffun)
}
///////////////////////////////////////////////////
// PctIf
// PctIfIndex returns the percentage (0-100) of true return values for given IfFunc on
// non-Null, non-NaN elements in given IndexView indexed view of an
// table.Table, for given column index.
// Return value(s) is size of column cell: 1 for scalar 1D columns
// and N for higher-dimensional columns.
func PctIfIndex(ix *table.IndexView, colIndex int, iffun IfFunc) []float64 {
cnt := CountIndex(ix, colIndex)
if cnt == nil {
return nil
}
pif := CountIfIndex(ix, colIndex, iffun)
for i := range pif {
if cnt[i] > 0 {
pif[i] = 100.0 * (pif[i] / cnt[i])
}
}
return pif
}
// PctIfColumn returns the percentage (0-100) of true return values for given IfFunc on
// non-Null, non-NaN elements in given IndexView indexed view of an
// table.Table, for given column name.
// If name not found, nil is returned -- use Try version for error message.
// Return value(s) is size of column cell: 1 for scalar 1D columns
// and N for higher-dimensional columns.
func PctIfColumn(ix *table.IndexView, column string, iffun IfFunc) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return PctIfIndex(ix, colIndex, iffun)
}
// Copyright (c) 2024, 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 stats
import (
"math"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/tensor/table"
)
// Every IndexView Stats method in this file follows one of these signatures:
// IndexViewFuncIndex is a stats function operating on IndexView, taking a column index arg
type IndexViewFuncIndex func(ix *table.IndexView, colIndex int) []float64
// IndexViewFuncColumn is a stats function operating on IndexView, taking a column name arg
type IndexViewFuncColumn func(ix *table.IndexView, column string) []float64
// StatIndex returns IndexView statistic according to given Stats type applied
// to all non-NaN elements in given IndexView indexed view of
// an table.Table, for given column index.
// Return value(s) is size of column cell: 1 for scalar 1D columns
// and N for higher-dimensional columns.
func StatIndex(ix *table.IndexView, colIndex int, stat Stats) []float64 {
switch stat {
case Count:
return CountIndex(ix, colIndex)
case Sum:
return SumIndex(ix, colIndex)
case Prod:
return ProdIndex(ix, colIndex)
case Min:
return MinIndex(ix, colIndex)
case Max:
return MaxIndex(ix, colIndex)
case MinAbs:
return MinAbsIndex(ix, colIndex)
case MaxAbs:
return MaxAbsIndex(ix, colIndex)
case Mean:
return MeanIndex(ix, colIndex)
case Var:
return VarIndex(ix, colIndex)
case Std:
return StdIndex(ix, colIndex)
case Sem:
return SemIndex(ix, colIndex)
case L1Norm:
return L1NormIndex(ix, colIndex)
case SumSq:
return SumSqIndex(ix, colIndex)
case L2Norm:
return L2NormIndex(ix, colIndex)
case VarPop:
return VarPopIndex(ix, colIndex)
case StdPop:
return StdPopIndex(ix, colIndex)
case SemPop:
return SemPopIndex(ix, colIndex)
case Median:
return MedianIndex(ix, colIndex)
case Q1:
return Q1Index(ix, colIndex)
case Q3:
return Q3Index(ix, colIndex)
}
return nil
}
// StatColumn returns IndexView statistic according to given Stats type applied
// to all non-NaN elements in given IndexView indexed view of
// an table.Table, for given column name.
// If name not found, returns error message.
// Return value(s) is size of column cell: 1 for scalar 1D columns
// and N for higher-dimensional columns.
func StatColumn(ix *table.IndexView, column string, stat Stats) ([]float64, error) {
colIndex, err := ix.Table.ColumnIndex(column)
if err != nil {
return nil, err
}
rv := StatIndex(ix, colIndex, stat)
return rv, nil
}
// StatIndexFunc applies given StatFunc function to each element in the given column,
// using float64 conversions of the values. ini is the initial value for the agg variable.
// Operates independently over each cell on n-dimensional columns and returns the result as a slice
// of values per cell.
func StatIndexFunc(ix *table.IndexView, colIndex int, ini float64, fun StatFunc) []float64 {
cl := ix.Table.Columns[colIndex]
_, csz := cl.RowCellSize()
ag := make([]float64, csz)
for i := range ag {
ag[i] = ini
}
if csz == 1 {
for _, srw := range ix.Indexes {
val := cl.Float1D(srw)
if !math.IsNaN(val) {
ag[0] = fun(srw, val, ag[0])
}
}
} else {
for _, srw := range ix.Indexes {
si := srw * csz
for j := range ag {
val := cl.Float1D(si + j)
if !math.IsNaN(val) {
ag[j] = fun(si+j, val, ag[j])
}
}
}
}
return ag
}
///////////////////////////////////////////////////
// Count
// CountIndex returns the count of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func CountIndex(ix *table.IndexView, colIndex int) []float64 {
return StatIndexFunc(ix, colIndex, 0, CountFunc)
}
// CountColumn returns the count of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func CountColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return CountIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// Sum
// SumIndex returns the sum of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func SumIndex(ix *table.IndexView, colIndex int) []float64 {
return StatIndexFunc(ix, colIndex, 0, SumFunc)
}
// SumColumn returns the sum of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func SumColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return SumIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// Prod
// ProdIndex returns the product of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func ProdIndex(ix *table.IndexView, colIndex int) []float64 {
return StatIndexFunc(ix, colIndex, 1, ProdFunc)
}
// ProdColumn returns the product of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func ProdColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return ProdIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// Min
// MinIndex returns the minimum of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func MinIndex(ix *table.IndexView, colIndex int) []float64 {
return StatIndexFunc(ix, colIndex, math.MaxFloat64, MinFunc)
}
// MinColumn returns the minimum of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func MinColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return MinIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// Max
// MaxIndex returns the maximum of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func MaxIndex(ix *table.IndexView, colIndex int) []float64 {
return StatIndexFunc(ix, colIndex, -math.MaxFloat64, MaxFunc)
}
// MaxColumn returns the maximum of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func MaxColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return MaxIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// MinAbs
// MinAbsIndex returns the minimum of abs of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func MinAbsIndex(ix *table.IndexView, colIndex int) []float64 {
return StatIndexFunc(ix, colIndex, math.MaxFloat64, MinAbsFunc)
}
// MinAbsColumn returns the minimum of abs of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func MinAbsColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return MinAbsIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// MaxAbs
// MaxAbsIndex returns the maximum of abs of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func MaxAbsIndex(ix *table.IndexView, colIndex int) []float64 {
return StatIndexFunc(ix, colIndex, -math.MaxFloat64, MaxAbsFunc)
}
// MaxAbsColumn returns the maximum of abs of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func MaxAbsColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return MaxAbsIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// Mean
// MeanIndex returns the mean of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func MeanIndex(ix *table.IndexView, colIndex int) []float64 {
cnt := CountIndex(ix, colIndex)
if cnt == nil {
return nil
}
mean := SumIndex(ix, colIndex)
for i := range mean {
if cnt[i] > 0 {
mean[i] /= cnt[i]
}
}
return mean
}
// MeanColumn returns the mean of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func MeanColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return MeanIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// Var
// VarIndex returns the sample variance of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Sample variance is normalized by 1/(n-1) -- see VarPop version for 1/n normalization.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func VarIndex(ix *table.IndexView, colIndex int) []float64 {
cnt := CountIndex(ix, colIndex)
if cnt == nil {
return nil
}
mean := SumIndex(ix, colIndex)
for i := range mean {
if cnt[i] > 0 {
mean[i] /= cnt[i]
}
}
col := ix.Table.Columns[colIndex]
_, csz := col.RowCellSize()
vr := StatIndexFunc(ix, colIndex, 0, func(idx int, val float64, agg float64) float64 {
cidx := idx % csz
dv := val - mean[cidx]
return agg + dv*dv
})
for i := range vr {
if cnt[i] > 1 {
vr[i] /= (cnt[i] - 1)
}
}
return vr
}
// VarColumn returns the sample variance of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// Sample variance is normalized by 1/(n-1) -- see VarPop version for 1/n normalization.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func VarColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return VarIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// Std
// StdIndex returns the sample std deviation of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Sample std deviation is normalized by 1/(n-1) -- see StdPop version for 1/n normalization.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func StdIndex(ix *table.IndexView, colIndex int) []float64 {
std := VarIndex(ix, colIndex)
for i := range std {
std[i] = math.Sqrt(std[i])
}
return std
}
// StdColumn returns the sample std deviation of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// Sample std deviation is normalized by 1/(n-1) -- see StdPop version for 1/n normalization.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func StdColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return StdIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// Sem
// SemIndex returns the sample standard error of the mean of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Sample sem is normalized by 1/(n-1) -- see SemPop version for 1/n normalization.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func SemIndex(ix *table.IndexView, colIndex int) []float64 {
cnt := CountIndex(ix, colIndex)
if cnt == nil {
return nil
}
sem := StdIndex(ix, colIndex)
for i := range sem {
if cnt[i] > 0 {
sem[i] /= math.Sqrt(cnt[i])
}
}
return sem
}
// SemColumn returns the sample standard error of the mean of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// Sample sem is normalized by 1/(n-1) -- see SemPop version for 1/n normalization.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func SemColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return SemIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// L1Norm
// L1NormIndex returns the L1 norm (sum abs values) of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func L1NormIndex(ix *table.IndexView, colIndex int) []float64 {
return StatIndexFunc(ix, colIndex, 0, L1NormFunc)
}
// L1NormColumn returns the L1 norm (sum abs values) of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func L1NormColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return L1NormIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// SumSq
// SumSqIndex returns the sum-of-squares of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func SumSqIndex(ix *table.IndexView, colIndex int) []float64 {
cl := ix.Table.Columns[colIndex]
_, csz := cl.RowCellSize()
scale := make([]float64, csz)
ss := make([]float64, csz)
for i := range ss {
ss[i] = 1
}
n := len(ix.Indexes)
if csz == 1 {
if n < 2 {
if n == 1 {
ss[0] = math.Abs(cl.Float1D(ix.Indexes[0]))
return ss
}
return scale // all 0s
}
for _, srw := range ix.Indexes {
v := cl.Float1D(srw)
absxi := math.Abs(v)
if scale[0] < absxi {
ss[0] = 1 + ss[0]*(scale[0]/absxi)*(scale[0]/absxi)
scale[0] = absxi
} else {
ss[0] = ss[0] + (absxi/scale[0])*(absxi/scale[0])
}
}
if math.IsInf(scale[0], 1) {
ss[0] = math.Inf(1)
} else {
ss[0] = scale[0] * scale[0] * ss[0]
}
} else {
if n < 2 {
if n == 1 {
si := csz * ix.Indexes[0]
for j := range csz {
ss[j] = math.Abs(cl.Float1D(si + j))
}
return ss
}
return scale // all 0s
}
for _, srw := range ix.Indexes {
si := srw * csz
for j := range ss {
v := cl.Float1D(si + j)
absxi := math.Abs(v)
if scale[j] < absxi {
ss[j] = 1 + ss[j]*(scale[j]/absxi)*(scale[j]/absxi)
scale[j] = absxi
} else {
ss[j] = ss[j] + (absxi/scale[j])*(absxi/scale[j])
}
}
}
for j := range ss {
if math.IsInf(scale[j], 1) {
ss[j] = math.Inf(1)
} else {
ss[j] = scale[j] * scale[j] * ss[j]
}
}
}
return ss
}
// SumSqColumn returns the sum-of-squares of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func SumSqColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return SumSqIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// L2Norm
// L2NormIndex returns the L2 norm (square root of sum-of-squares) of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func L2NormIndex(ix *table.IndexView, colIndex int) []float64 {
ss := SumSqIndex(ix, colIndex)
for i := range ss {
ss[i] = math.Sqrt(ss[i])
}
return ss
}
// L2NormColumn returns the L2 norm (square root of sum-of-squares) of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func L2NormColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return L2NormIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// VarPop
// VarPopIndex returns the population variance of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// population variance is normalized by 1/n -- see Var version for 1/(n-1) sample normalization.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func VarPopIndex(ix *table.IndexView, colIndex int) []float64 {
cnt := CountIndex(ix, colIndex)
if cnt == nil {
return nil
}
mean := SumIndex(ix, colIndex)
for i := range mean {
if cnt[i] > 0 {
mean[i] /= cnt[i]
}
}
col := ix.Table.Columns[colIndex]
_, csz := col.RowCellSize()
vr := StatIndexFunc(ix, colIndex, 0, func(idx int, val float64, agg float64) float64 {
cidx := idx % csz
dv := val - mean[cidx]
return agg + dv*dv
})
for i := range vr {
if cnt[i] > 0 {
vr[i] /= cnt[i]
}
}
return vr
}
// VarPopColumn returns the population variance of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// population variance is normalized by 1/n -- see Var version for 1/(n-1) sample normalization.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func VarPopColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return VarPopIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// StdPop
// StdPopIndex returns the population std deviation of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// population std dev is normalized by 1/n -- see Var version for 1/(n-1) sample normalization.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func StdPopIndex(ix *table.IndexView, colIndex int) []float64 {
std := VarPopIndex(ix, colIndex)
for i := range std {
std[i] = math.Sqrt(std[i])
}
return std
}
// StdPopColumn returns the population std deviation of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// population std dev is normalized by 1/n -- see Var version for 1/(n-1) sample normalization.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func StdPopColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return StdPopIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// SemPop
// SemPopIndex returns the population standard error of the mean of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// population sem is normalized by 1/n -- see Var version for 1/(n-1) sample normalization.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func SemPopIndex(ix *table.IndexView, colIndex int) []float64 {
cnt := CountIndex(ix, colIndex)
if cnt == nil {
return nil
}
sem := StdPopIndex(ix, colIndex)
for i := range sem {
if cnt[i] > 0 {
sem[i] /= math.Sqrt(cnt[i])
}
}
return sem
}
// SemPopColumn returns the standard error of the mean of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// population sem is normalized by 1/n -- see Var version for 1/(n-1) sample normalization.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func SemPopColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return SemPopIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// Median
// MedianIndex returns the median of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func MedianIndex(ix *table.IndexView, colIndex int) []float64 {
return QuantilesIndex(ix, colIndex, []float64{.5})
}
// MedianColumn returns the median of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func MedianColumn(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return MedianIndex(ix, colIndex)
}
///////////////////////////////////////////////////
// Q1
// Q1Index returns the first quartile of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func Q1Index(ix *table.IndexView, colIndex int) []float64 {
return QuantilesIndex(ix, colIndex, []float64{.25})
}
// Q1Column returns the first quartile of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func Q1Column(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return Q1Index(ix, colIndex)
}
///////////////////////////////////////////////////
// Q3
// Q3Index returns the third quartile of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func Q3Index(ix *table.IndexView, colIndex int) []float64 {
return QuantilesIndex(ix, colIndex, []float64{.75})
}
// Q3Column returns the third quartile of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned.
// Return value is size of each column cell -- 1 for scalar 1D columns
// and N for higher-dimensional columns.
func Q3Column(ix *table.IndexView, column string) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return Q3Index(ix, colIndex)
}
// Copyright (c) 2024, 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 stats
import (
"math"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/tensor/table"
)
// QuantilesIndex returns the given quantile(s) of non-NaN elements in given
// IndexView indexed view of an table.Table, for given column index.
// Column must be a 1d Column -- returns nil for n-dimensional columns.
// qs are 0-1 values, 0 = min, 1 = max, .5 = median, etc. Uses linear interpolation.
// Because this requires a sort, it is more efficient to get as many quantiles
// as needed in one pass.
func QuantilesIndex(ix *table.IndexView, colIndex int, qs []float64) []float64 {
nq := len(qs)
if nq == 0 {
return nil
}
col := ix.Table.Columns[colIndex]
if col.NumDims() > 1 { // only valid for 1D
return nil
}
rvs := make([]float64, nq)
six := ix.Clone() // leave original indexes intact
six.Filter(func(et *table.Table, row int) bool { // get rid of NaNs in this column
if math.IsNaN(col.Float1D(row)) {
return false
}
return true
})
six.SortColumn(colIndex, true)
sz := len(six.Indexes) - 1 // length of our own index list
fsz := float64(sz)
for i, q := range qs {
val := 0.0
qi := q * fsz
lwi := math.Floor(qi)
lwii := int(lwi)
if lwii >= sz {
val = col.Float1D(six.Indexes[sz])
} else if lwii < 0 {
val = col.Float1D(six.Indexes[0])
} else {
phi := qi - lwi
lwv := col.Float1D(six.Indexes[lwii])
hiv := col.Float1D(six.Indexes[lwii+1])
val = (1-phi)*lwv + phi*hiv
}
rvs[i] = val
}
return rvs
}
// Quantiles returns the given quantile(s) of non-Null, non-NaN elements in given
// IndexView indexed view of an table.Table, for given column name.
// If name not found, nil is returned -- use Try version for error message.
// Column must be a 1d Column -- returns nil for n-dimensional columns.
// qs are 0-1 values, 0 = min, 1 = max, .5 = median, etc. Uses linear interpolation.
// Because this requires a sort, it is more efficient to get as many quantiles
// as needed in one pass.
func Quantiles(ix *table.IndexView, column string, qs []float64) []float64 {
colIndex := errors.Log1(ix.Table.ColumnIndex(column))
if colIndex == -1 {
return nil
}
return QuantilesIndex(ix, colIndex, qs)
}
// Copyright (c) 2024, 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 stats
import (
"reflect"
"cogentcore.org/core/tensor/table"
)
// MeanTables returns an table.Table with the mean values across all float
// columns of the input tables, which must have the same columns but not
// necessarily the same number of rows.
func MeanTables(dts []*table.Table) *table.Table {
nt := len(dts)
if nt == 0 {
return nil
}
maxRows := 0
var maxdt *table.Table
for _, dt := range dts {
if dt.Rows > maxRows {
maxRows = dt.Rows
maxdt = dt
}
}
if maxRows == 0 {
return nil
}
ot := maxdt.Clone()
// N samples per row
rns := make([]int, maxRows)
for _, dt := range dts {
dnr := dt.Rows
mx := min(dnr, maxRows)
for ri := 0; ri < mx; ri++ {
rns[ri]++
}
}
for ci, cl := range ot.Columns {
if cl.DataType() != reflect.Float32 && cl.DataType() != reflect.Float64 {
continue
}
_, cells := cl.RowCellSize()
for di, dt := range dts {
if di == 0 {
continue
}
dc := dt.Columns[ci]
dnr := dt.Rows
mx := min(dnr, maxRows)
for ri := 0; ri < mx; ri++ {
si := ri * cells
for j := 0; j < cells; j++ {
ci := si + j
cv := cl.Float1D(ci)
cv += dc.Float1D(ci)
cl.SetFloat1D(ci, cv)
}
}
}
for ri := 0; ri < maxRows; ri++ {
si := ri * cells
for j := 0; j < cells; j++ {
ci := si + j
cv := cl.Float1D(ci)
if rns[ri] > 0 {
cv /= float64(rns[ri])
cl.SetFloat1D(ci, cv)
}
}
}
}
return ot
}
// Copyright (c) 2024, 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 stats
import (
"math"
"cogentcore.org/core/tensor"
)
// StatTensor returns Tensor statistic according to given Stats type applied
// to all non-NaN elements in given Tensor
func StatTensor(tsr tensor.Tensor, stat Stats) float64 {
switch stat {
case Count:
return CountTensor(tsr)
case Sum:
return SumTensor(tsr)
case Prod:
return ProdTensor(tsr)
case Min:
return MinTensor(tsr)
case Max:
return MaxTensor(tsr)
case MinAbs:
return MinAbsTensor(tsr)
case MaxAbs:
return MaxAbsTensor(tsr)
case Mean:
return MeanTensor(tsr)
case Var:
return VarTensor(tsr)
case Std:
return StdTensor(tsr)
case Sem:
return SemTensor(tsr)
case L1Norm:
return L1NormTensor(tsr)
case SumSq:
return SumSqTensor(tsr)
case L2Norm:
return L2NormTensor(tsr)
case VarPop:
return VarPopTensor(tsr)
case StdPop:
return StdPopTensor(tsr)
case SemPop:
return SemPopTensor(tsr)
// case Median:
// return MedianTensor(tsr)
// case Q1:
// return Q1Tensor(tsr)
// case Q3:
// return Q3Tensor(tsr)
}
return 0
}
// TensorStat applies given StatFunc function to each element in the tensor
// (automatically skips NaN elements), using float64 conversions of the values.
// ini is the initial value for the agg variable. returns final aggregate value
func TensorStat(tsr tensor.Tensor, ini float64, fun StatFunc) float64 {
ln := tsr.Len()
agg := ini
for j := 0; j < ln; j++ {
val := tsr.Float1D(j)
if !math.IsNaN(val) {
agg = fun(j, val, agg)
}
}
return agg
}
// CountTensor returns the count of non-NaN elements in given Tensor.
func CountTensor(tsr tensor.Tensor) float64 {
return TensorStat(tsr, 0, CountFunc)
}
// SumTensor returns the sum of non-NaN elements in given Tensor.
func SumTensor(tsr tensor.Tensor) float64 {
return TensorStat(tsr, 0, SumFunc)
}
// ProdTensor returns the product of non-NaN elements in given Tensor.
func ProdTensor(tsr tensor.Tensor) float64 {
return TensorStat(tsr, 1, ProdFunc)
}
// MinTensor returns the minimum of non-NaN elements in given Tensor.
func MinTensor(tsr tensor.Tensor) float64 {
return TensorStat(tsr, math.MaxFloat64, MinFunc)
}
// MaxTensor returns the maximum of non-NaN elements in given Tensor.
func MaxTensor(tsr tensor.Tensor) float64 {
return TensorStat(tsr, -math.MaxFloat64, MaxFunc)
}
// MinAbsTensor returns the minimum of non-NaN elements in given Tensor.
func MinAbsTensor(tsr tensor.Tensor) float64 {
return TensorStat(tsr, math.MaxFloat64, MinAbsFunc)
}
// MaxAbsTensor returns the maximum of non-NaN elements in given Tensor.
func MaxAbsTensor(tsr tensor.Tensor) float64 {
return TensorStat(tsr, -math.MaxFloat64, MaxAbsFunc)
}
// MeanTensor returns the mean of non-NaN elements in given Tensor.
func MeanTensor(tsr tensor.Tensor) float64 {
cnt := CountTensor(tsr)
if cnt == 0 {
return 0
}
return SumTensor(tsr) / cnt
}
// VarTensor returns the sample variance of non-NaN elements in given Tensor.
func VarTensor(tsr tensor.Tensor) float64 {
cnt := CountTensor(tsr)
if cnt < 2 {
return 0
}
mean := SumTensor(tsr) / cnt
vr := TensorStat(tsr, 0, func(idx int, val float64, agg float64) float64 {
dv := val - mean
return agg + dv*dv
})
return vr / (cnt - 1)
}
// StdTensor returns the sample standard deviation of non-NaN elements in given Tensor.
func StdTensor(tsr tensor.Tensor) float64 {
return math.Sqrt(VarTensor(tsr))
}
// SemTensor returns the sample standard error of the mean of non-NaN elements in given Tensor.
func SemTensor(tsr tensor.Tensor) float64 {
cnt := CountTensor(tsr)
if cnt < 2 {
return 0
}
return StdTensor(tsr) / math.Sqrt(cnt)
}
// L1NormTensor returns the L1 norm: sum of absolute values of non-NaN elements in given Tensor.
func L1NormTensor(tsr tensor.Tensor) float64 {
return TensorStat(tsr, 0, L1NormFunc)
}
// SumSqTensor returns the sum-of-squares of non-NaN elements in given Tensor.
func SumSqTensor(tsr tensor.Tensor) float64 {
n := tsr.Len()
if n < 2 {
if n == 1 {
return math.Abs(tsr.Float1D(0))
}
return 0
}
var (
scale float64 = 0
ss float64 = 1
)
for j := 0; j < n; j++ {
v := tsr.Float1D(j)
if v == 0 || math.IsNaN(v) {
continue
}
absxi := math.Abs(v)
if scale < absxi {
ss = 1 + ss*(scale/absxi)*(scale/absxi)
scale = absxi
} else {
ss = ss + (absxi/scale)*(absxi/scale)
}
}
if math.IsInf(scale, 1) {
return math.Inf(1)
}
return scale * scale * ss
}
// L2NormTensor returns the L2 norm: square root of sum-of-squared values of non-NaN elements in given Tensor.
func L2NormTensor(tsr tensor.Tensor) float64 {
return math.Sqrt(SumSqTensor(tsr))
}
// VarPopTensor returns the population variance of non-NaN elements in given Tensor.
func VarPopTensor(tsr tensor.Tensor) float64 {
cnt := CountTensor(tsr)
if cnt < 2 {
return 0
}
mean := SumTensor(tsr) / cnt
vr := TensorStat(tsr, 0, func(idx int, val float64, agg float64) float64 {
dv := val - mean
return agg + dv*dv
})
return vr / cnt
}
// StdPopTensor returns the population standard deviation of non-NaN elements in given Tensor.
func StdPopTensor(tsr tensor.Tensor) float64 {
return math.Sqrt(VarPopTensor(tsr))
}
// SemPopTensor returns the population standard error of the mean of non-NaN elements in given Tensor.
func SemPopTensor(tsr tensor.Tensor) float64 {
cnt := CountTensor(tsr)
if cnt < 2 {
return 0
}
return StdPopTensor(tsr) / math.Sqrt(cnt)
}
// Copyright (c) 2024, 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 tensor
import (
"fmt"
"log"
"math"
"strconv"
"strings"
"cogentcore.org/core/base/slicesx"
"gonum.org/v1/gonum/mat"
)
// String is a tensor of string values
type String struct {
Base[string]
}
// NewString returns a new n-dimensional tensor of string values
// with the given sizes per dimension (shape), and optional dimension names.
func NewString(sizes []int, names ...string) *String {
tsr := &String{}
tsr.SetShape(sizes, names...)
tsr.Values = make([]string, tsr.Len())
return tsr
}
// NewStringShape returns a new n-dimensional tensor of string values
// using given shape.
func NewStringShape(shape *Shape) *String {
tsr := &String{}
tsr.Shp.CopyShape(shape)
tsr.Values = make([]string, tsr.Len())
return tsr
}
// StringToFloat64 converts string value to float64 using strconv,
// returning 0 if any error
func StringToFloat64(str string) float64 {
if fv, err := strconv.ParseFloat(str, 64); err == nil {
return fv
}
return 0
}
// Float64ToString converts float64 to string value using strconv, g format
func Float64ToString(val float64) string {
return strconv.FormatFloat(val, 'g', -1, 64)
}
func (tsr *String) IsString() bool {
return true
}
func (tsr *String) AddScalar(i []int, val float64) float64 {
j := tsr.Shp.Offset(i)
fv := StringToFloat64(tsr.Values[j]) + val
tsr.Values[j] = Float64ToString(fv)
return fv
}
func (tsr *String) MulScalar(i []int, val float64) float64 {
j := tsr.Shp.Offset(i)
fv := StringToFloat64(tsr.Values[j]) * val
tsr.Values[j] = Float64ToString(fv)
return fv
}
func (tsr *String) SetString(i []int, val string) {
j := tsr.Shp.Offset(i)
tsr.Values[j] = val
}
func (tsr String) SetString1D(off int, val string) {
tsr.Values[off] = val
}
func (tsr *String) SetStringRowCell(row, cell int, val string) {
_, sz := tsr.Shp.RowCellSize()
tsr.Values[row*sz+cell] = val
}
// String satisfies the fmt.Stringer interface for string of tensor data
func (tsr *String) String() string {
str := tsr.Label()
sz := len(tsr.Values)
if sz > 1000 {
return str
}
var b strings.Builder
b.WriteString(str)
b.WriteString("\n")
oddRow := true
rows, cols, _, _ := Projection2DShape(&tsr.Shp, oddRow)
for r := 0; r < rows; r++ {
rc, _ := Projection2DCoords(&tsr.Shp, oddRow, r, 0)
b.WriteString(fmt.Sprintf("%v: ", rc))
for c := 0; c < cols; c++ {
idx := Projection2DIndex(tsr.Shape(), oddRow, r, c)
vl := tsr.Values[idx]
b.WriteString(vl)
}
b.WriteString("\n")
}
return b.String()
}
func (tsr *String) Float(i []int) float64 {
j := tsr.Shp.Offset(i)
return StringToFloat64(tsr.Values[j])
}
func (tsr *String) SetFloat(i []int, val float64) {
j := tsr.Shp.Offset(i)
tsr.Values[j] = Float64ToString(val)
}
func (tsr *String) Float1D(off int) float64 {
return StringToFloat64(tsr.Values[off])
}
func (tsr *String) SetFloat1D(off int, val float64) {
tsr.Values[off] = Float64ToString(val)
}
func (tsr *String) FloatRowCell(row, cell int) float64 {
_, sz := tsr.Shp.RowCellSize()
return StringToFloat64(tsr.Values[row*sz+cell])
}
func (tsr *String) SetFloatRowCell(row, cell int, val float64) {
_, sz := tsr.Shp.RowCellSize()
tsr.Values[row*sz+cell] = Float64ToString(val)
}
// Floats sets []float64 slice of all elements in the tensor
// (length is ensured to be sufficient).
// This can be used for all of the gonum/floats methods
// for basic math, gonum/stats, etc.
func (tsr *String) Floats(flt *[]float64) {
*flt = slicesx.SetLength(*flt, len(tsr.Values))
for i, v := range tsr.Values {
(*flt)[i] = StringToFloat64(v)
}
}
// SetFloats sets tensor values from a []float64 slice (copies values).
func (tsr *String) SetFloats(flt []float64) {
for i, v := range flt {
tsr.Values[i] = Float64ToString(v)
}
}
// At is the gonum/mat.Matrix interface method for returning 2D matrix element at given
// row, column index. Assumes Row-major ordering and logs an error if NumDims < 2.
func (tsr *String) At(i, j int) float64 {
nd := tsr.NumDims()
if nd < 2 {
log.Println("tensor Dims gonum Matrix call made on Tensor with dims < 2")
return 0
} else if nd == 2 {
return tsr.Float([]int{i, j})
} else {
ix := make([]int, nd)
ix[nd-2] = i
ix[nd-1] = j
return tsr.Float(ix)
}
}
// T is the gonum/mat.Matrix transpose method.
// It performs an implicit transpose by returning the receiver inside a Transpose.
func (tsr *String) T() mat.Matrix {
return mat.Transpose{tsr}
}
// Range returns the min, max (and associated indexes, -1 = no values) for the tensor.
// This is needed for display and is thus in the core api in optimized form
// Other math operations can be done using gonum/floats package.
func (tsr *String) Range() (min, max float64, minIndex, maxIndex int) {
minIndex = -1
maxIndex = -1
for j, vl := range tsr.Values {
fv := StringToFloat64(vl)
if math.IsNaN(fv) {
continue
}
if fv < min || minIndex < 0 {
min = fv
minIndex = j
}
if fv > max || maxIndex < 0 {
max = fv
maxIndex = j
}
}
return
}
// SetZeros is simple convenience function initialize all values to 0
func (tsr *String) SetZeros() {
for j := range tsr.Values {
tsr.Values[j] = ""
}
}
// Clone clones this tensor, creating a duplicate copy of itself with its
// own separate memory representation of all the values, and returns
// that as a Tensor (which can be converted into the known type as needed).
func (tsr *String) Clone() Tensor {
csr := NewStringShape(&tsr.Shp)
copy(csr.Values, tsr.Values)
return csr
}
// CopyFrom copies all avail values from other tensor into this tensor, with an
// optimized implementation if the other tensor is of the same type, and
// otherwise it goes through appropriate standard type.
func (tsr *String) CopyFrom(frm Tensor) {
if fsm, ok := frm.(*String); ok {
copy(tsr.Values, fsm.Values)
return
}
sz := min(len(tsr.Values), frm.Len())
for i := 0; i < sz; i++ {
tsr.Values[i] = Float64ToString(frm.Float1D(i))
}
}
// CopyShapeFrom copies just the shape from given source tensor
// calling SetShape with the shape params from source (see for more docs).
func (tsr *String) CopyShapeFrom(frm Tensor) {
tsr.SetShape(frm.Shape().Sizes, frm.Shape().Names...)
}
// CopyCellsFrom copies given range of values from other tensor into this tensor,
// using flat 1D indexes: to = starting index in this Tensor to start copying into,
// start = starting index on from Tensor to start copying from, and n = number of
// values to copy. Uses an optimized implementation if the other tensor is
// of the same type, and otherwise it goes through appropriate standard type.
func (tsr *String) CopyCellsFrom(frm Tensor, to, start, n int) {
if fsm, ok := frm.(*String); ok {
for i := 0; i < n; i++ {
tsr.Values[to+i] = fsm.Values[start+i]
}
return
}
for i := 0; i < n; i++ {
tsr.Values[to+i] = Float64ToString(frm.Float1D(start + i))
}
}
// SubSpace returns a new tensor with innermost subspace at given
// offset(s) in outermost dimension(s) (len(offs) < NumDims).
// The new tensor points to the values of the this tensor (i.e., modifications
// will affect both), as its Values slice is a view onto the original (which
// is why only inner-most contiguous supsaces are supported).
// Use Clone() method to separate the two.
func (tsr *String) SubSpace(offs []int) Tensor {
b := tsr.subSpaceImpl(offs)
rt := &String{Base: *b}
return rt
}
// Code generated by "core generate"; DO NOT EDIT.
package table
import (
"cogentcore.org/core/enums"
)
var _DelimsValues = []Delims{0, 1, 2, 3}
// DelimsN is the highest valid value for type Delims, plus one.
const DelimsN Delims = 4
var _DelimsValueMap = map[string]Delims{`Tab`: 0, `Comma`: 1, `Space`: 2, `Detect`: 3}
var _DelimsDescMap = map[Delims]string{0: `Tab is the tab rune delimiter, for TSV tab separated values`, 1: `Comma is the comma rune delimiter, for CSV comma separated values`, 2: `Space is the space rune delimiter, for SSV space separated value`, 3: `Detect is used during reading a file -- reads the first line and detects tabs or commas`}
var _DelimsMap = map[Delims]string{0: `Tab`, 1: `Comma`, 2: `Space`, 3: `Detect`}
// String returns the string representation of this Delims value.
func (i Delims) String() string { return enums.String(i, _DelimsMap) }
// SetString sets the Delims value from its string representation,
// and returns an error if the string is invalid.
func (i *Delims) SetString(s string) error { return enums.SetString(i, s, _DelimsValueMap, "Delims") }
// Int64 returns the Delims value as an int64.
func (i Delims) Int64() int64 { return int64(i) }
// SetInt64 sets the Delims value from an int64.
func (i *Delims) SetInt64(in int64) { *i = Delims(in) }
// Desc returns the description of the Delims value.
func (i Delims) Desc() string { return enums.Desc(i, _DelimsDescMap) }
// DelimsValues returns all possible values for the type Delims.
func DelimsValues() []Delims { return _DelimsValues }
// Values returns all possible values for the type Delims.
func (i Delims) Values() []enums.Enum { return enums.Values(_DelimsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Delims) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Delims) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Delims") }
// Copyright (c) 2024, 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 table
import (
"fmt"
"log"
"math/rand"
"slices"
"sort"
"strings"
)
// LessFunc is a function used for sort comparisons that returns
// true if Table row i is less than Table row j -- these are the
// raw row numbers, which have already been projected through
// indexes when used for sorting via Indexes.
type LessFunc func(et *Table, i, j int) bool
// Filterer is a function used for filtering that returns
// true if Table row should be included in the current filtered
// view of the table, and false if it should be removed.
type Filterer func(et *Table, row int) bool
// IndexView is an indexed wrapper around an table.Table that provides a
// specific view onto the Table defined by the set of indexes.
// This provides an efficient way of sorting and filtering a table by only
// updating the indexes while doing nothing to the Table itself.
// To produce a table that has data actually organized according to the
// indexed order, call the NewTable method.
// IndexView views on a table can also be organized together as Splits
// of the table rows, e.g., by grouping values along a given column.
type IndexView struct { //types:add
// Table that we are an indexed view onto
Table *Table
// current indexes into Table
Indexes []int
// current Less function used in sorting
lessFunc LessFunc `copier:"-" display:"-" xml:"-" json:"-"`
}
// NewIndexView returns a new IndexView based on given table, initialized with sequential idxes
func NewIndexView(et *Table) *IndexView {
ix := &IndexView{}
ix.SetTable(et)
return ix
}
// SetTable sets as indexes into given table with sequential initial indexes
func (ix *IndexView) SetTable(et *Table) {
ix.Table = et
ix.Sequential()
}
// DeleteInvalid deletes all invalid indexes from the list.
// Call this if rows (could) have been deleted from table.
func (ix *IndexView) DeleteInvalid() {
if ix.Table == nil || ix.Table.Rows <= 0 {
ix.Indexes = nil
return
}
ni := ix.Len()
for i := ni - 1; i >= 0; i-- {
if ix.Indexes[i] >= ix.Table.Rows {
ix.Indexes = append(ix.Indexes[:i], ix.Indexes[i+1:]...)
}
}
}
// Sequential sets indexes to sequential row-wise indexes into table
func (ix *IndexView) Sequential() { //types:add
if ix.Table == nil || ix.Table.Rows <= 0 {
ix.Indexes = nil
return
}
ix.Indexes = make([]int, ix.Table.Rows)
for i := range ix.Indexes {
ix.Indexes[i] = i
}
}
// Permuted sets indexes to a permuted order -- if indexes already exist
// then existing list of indexes is permuted, otherwise a new set of
// permuted indexes are generated
func (ix *IndexView) Permuted() {
if ix.Table == nil || ix.Table.Rows <= 0 {
ix.Indexes = nil
return
}
if len(ix.Indexes) == 0 {
ix.Indexes = rand.Perm(ix.Table.Rows)
} else {
rand.Shuffle(len(ix.Indexes), func(i, j int) {
ix.Indexes[i], ix.Indexes[j] = ix.Indexes[j], ix.Indexes[i]
})
}
}
// AddIndex adds a new index to the list
func (ix *IndexView) AddIndex(idx int) {
ix.Indexes = append(ix.Indexes, idx)
}
// Sort sorts the indexes into our Table using given Less function.
// The Less function operates directly on row numbers into the Table
// as these row numbers have already been projected through the indexes.
func (ix *IndexView) Sort(lessFunc func(et *Table, i, j int) bool) {
ix.lessFunc = lessFunc
sort.Sort(ix)
}
// SortIndexes sorts the indexes into our Table directly in
// numerical order, producing the native ordering, while preserving
// any filtering that might have occurred.
func (ix *IndexView) SortIndexes() {
sort.Ints(ix.Indexes)
}
const (
// Ascending specifies an ascending sort direction for table Sort routines
Ascending = true
// Descending specifies a descending sort direction for table Sort routines
Descending = false
)
// SortColumnName sorts the indexes into our Table according to values in
// given column name, using either ascending or descending order.
// Only valid for 1-dimensional columns.
// Returns error if column name not found.
func (ix *IndexView) SortColumnName(column string, ascending bool) error { //types:add
ci, err := ix.Table.ColumnIndex(column)
if err != nil {
log.Println(err)
return err
}
ix.SortColumn(ci, ascending)
return nil
}
// SortColumn sorts the indexes into our Table according to values in
// given column index, using either ascending or descending order.
// Only valid for 1-dimensional columns.
func (ix *IndexView) SortColumn(colIndex int, ascending bool) {
cl := ix.Table.Columns[colIndex]
if cl.IsString() {
ix.Sort(func(et *Table, i, j int) bool {
if ascending {
return cl.String1D(i) < cl.String1D(j)
} else {
return cl.String1D(i) > cl.String1D(j)
}
})
} else {
ix.Sort(func(et *Table, i, j int) bool {
if ascending {
return cl.Float1D(i) < cl.Float1D(j)
} else {
return cl.Float1D(i) > cl.Float1D(j)
}
})
}
}
// SortColumnNames sorts the indexes into our Table according to values in
// given column names, using either ascending or descending order.
// Only valid for 1-dimensional columns.
// Returns error if column name not found.
func (ix *IndexView) SortColumnNames(columns []string, ascending bool) error {
nc := len(columns)
if nc == 0 {
return fmt.Errorf("table.IndexView.SortColumnNames: no column names provided")
}
cis := make([]int, nc)
for i, cn := range columns {
ci, err := ix.Table.ColumnIndex(cn)
if err != nil {
log.Println(err)
return err
}
cis[i] = ci
}
ix.SortColumns(cis, ascending)
return nil
}
// SortColumns sorts the indexes into our Table according to values in
// given list of column indexes, using either ascending or descending order for
// all of the columns. Only valid for 1-dimensional columns.
func (ix *IndexView) SortColumns(colIndexes []int, ascending bool) {
ix.Sort(func(et *Table, i, j int) bool {
for _, ci := range colIndexes {
cl := ix.Table.Columns[ci]
if cl.IsString() {
if ascending {
if cl.String1D(i) < cl.String1D(j) {
return true
} else if cl.String1D(i) > cl.String1D(j) {
return false
} // if equal, fallthrough to next col
} else {
if cl.String1D(i) > cl.String1D(j) {
return true
} else if cl.String1D(i) < cl.String1D(j) {
return false
} // if equal, fallthrough to next col
}
} else {
if ascending {
if cl.Float1D(i) < cl.Float1D(j) {
return true
} else if cl.Float1D(i) > cl.Float1D(j) {
return false
} // if equal, fallthrough to next col
} else {
if cl.Float1D(i) > cl.Float1D(j) {
return true
} else if cl.Float1D(i) < cl.Float1D(j) {
return false
} // if equal, fallthrough to next col
}
}
}
return false
})
}
/////////////////////////////////////////////////////////////////////////
// Stable sorts -- sometimes essential..
// SortStable stably sorts the indexes into our Table using given Less function.
// The Less function operates directly on row numbers into the Table
// as these row numbers have already been projected through the indexes.
// It is *essential* that it always returns false when the two are equal
// for the stable function to actually work.
func (ix *IndexView) SortStable(lessFunc func(et *Table, i, j int) bool) {
ix.lessFunc = lessFunc
sort.Stable(ix)
}
// SortStableColumnName sorts the indexes into our Table according to values in
// given column name, using either ascending or descending order.
// Only valid for 1-dimensional columns.
// Returns error if column name not found.
func (ix *IndexView) SortStableColumnName(column string, ascending bool) error {
ci, err := ix.Table.ColumnIndex(column)
if err != nil {
log.Println(err)
return err
}
ix.SortStableColumn(ci, ascending)
return nil
}
// SortStableColumn sorts the indexes into our Table according to values in
// given column index, using either ascending or descending order.
// Only valid for 1-dimensional columns.
func (ix *IndexView) SortStableColumn(colIndex int, ascending bool) {
cl := ix.Table.Columns[colIndex]
if cl.IsString() {
ix.SortStable(func(et *Table, i, j int) bool {
if ascending {
return cl.String1D(i) < cl.String1D(j)
} else {
return cl.String1D(i) > cl.String1D(j)
}
})
} else {
ix.SortStable(func(et *Table, i, j int) bool {
if ascending {
return cl.Float1D(i) < cl.Float1D(j)
} else {
return cl.Float1D(i) > cl.Float1D(j)
}
})
}
}
// SortStableColumnNames sorts the indexes into our Table according to values in
// given column names, using either ascending or descending order.
// Only valid for 1-dimensional columns.
// Returns error if column name not found.
func (ix *IndexView) SortStableColumnNames(columns []string, ascending bool) error {
nc := len(columns)
if nc == 0 {
return fmt.Errorf("table.IndexView.SortStableColumnNames: no column names provided")
}
cis := make([]int, nc)
for i, cn := range columns {
ci, err := ix.Table.ColumnIndex(cn)
if err != nil {
log.Println(err)
return err
}
cis[i] = ci
}
ix.SortStableColumns(cis, ascending)
return nil
}
// SortStableColumns sorts the indexes into our Table according to values in
// given list of column indexes, using either ascending or descending order for
// all of the columns. Only valid for 1-dimensional columns.
func (ix *IndexView) SortStableColumns(colIndexes []int, ascending bool) {
ix.SortStable(func(et *Table, i, j int) bool {
for _, ci := range colIndexes {
cl := ix.Table.Columns[ci]
if cl.IsString() {
if ascending {
if cl.String1D(i) < cl.String1D(j) {
return true
} else if cl.String1D(i) > cl.String1D(j) {
return false
} // if equal, fallthrough to next col
} else {
if cl.String1D(i) > cl.String1D(j) {
return true
} else if cl.String1D(i) < cl.String1D(j) {
return false
} // if equal, fallthrough to next col
}
} else {
if ascending {
if cl.Float1D(i) < cl.Float1D(j) {
return true
} else if cl.Float1D(i) > cl.Float1D(j) {
return false
} // if equal, fallthrough to next col
} else {
if cl.Float1D(i) > cl.Float1D(j) {
return true
} else if cl.Float1D(i) < cl.Float1D(j) {
return false
} // if equal, fallthrough to next col
}
}
}
return false
})
}
// Filter filters the indexes into our Table using given Filter function.
// The Filter function operates directly on row numbers into the Table
// as these row numbers have already been projected through the indexes.
func (ix *IndexView) Filter(filterer func(et *Table, row int) bool) {
sz := len(ix.Indexes)
for i := sz - 1; i >= 0; i-- { // always go in reverse for filtering
if !filterer(ix.Table, ix.Indexes[i]) { // delete
ix.Indexes = append(ix.Indexes[:i], ix.Indexes[i+1:]...)
}
}
}
// FilterColumnName filters the indexes into our Table according to values in
// given column name, using string representation of column values.
// Includes rows with matching values unless exclude is set.
// If contains, only checks if row contains string; if ignoreCase, ignores case.
// Use named args for greater clarity.
// Only valid for 1-dimensional columns.
// Returns error if column name not found.
func (ix *IndexView) FilterColumnName(column string, str string, exclude, contains, ignoreCase bool) error { //types:add
ci, err := ix.Table.ColumnIndex(column)
if err != nil {
log.Println(err)
return err
}
ix.FilterColumn(ci, str, exclude, contains, ignoreCase)
return nil
}
// FilterColumn sorts the indexes into our Table according to values in
// given column index, using string representation of column values.
// Includes rows with matching values unless exclude is set.
// If contains, only checks if row contains string; if ignoreCase, ignores case.
// Use named args for greater clarity.
// Only valid for 1-dimensional columns.
func (ix *IndexView) FilterColumn(colIndex int, str string, exclude, contains, ignoreCase bool) {
col := ix.Table.Columns[colIndex]
lowstr := strings.ToLower(str)
ix.Filter(func(et *Table, row int) bool {
val := col.String1D(row)
has := false
switch {
case contains && ignoreCase:
has = strings.Contains(strings.ToLower(val), lowstr)
case contains:
has = strings.Contains(val, str)
case ignoreCase:
has = strings.EqualFold(val, str)
default:
has = (val == str)
}
if exclude {
return !has
}
return has
})
}
// NewTable returns a new table with column data organized according to
// the indexes
func (ix *IndexView) NewTable() *Table {
rows := len(ix.Indexes)
nt := ix.Table.Clone()
nt.SetNumRows(rows)
if rows == 0 {
return nt
}
for ci := range nt.Columns {
scl := ix.Table.Columns[ci]
tcl := nt.Columns[ci]
_, csz := tcl.RowCellSize()
for i, srw := range ix.Indexes {
tcl.CopyCellsFrom(scl, i*csz, srw*csz, csz)
}
}
return nt
}
// Clone returns a copy of the current index view with its own index memory
func (ix *IndexView) Clone() *IndexView {
nix := &IndexView{}
nix.CopyFrom(ix)
return nix
}
// CopyFrom copies from given other IndexView (we have our own unique copy of indexes)
func (ix *IndexView) CopyFrom(oix *IndexView) {
ix.Table = oix.Table
ix.Indexes = slices.Clone(oix.Indexes)
}
// AddRows adds n rows to end of underlying Table, and to the indexes in this view
func (ix *IndexView) AddRows(n int) { //types:add
stidx := ix.Table.Rows
ix.Table.SetNumRows(stidx + n)
for i := stidx; i < stidx+n; i++ {
ix.Indexes = append(ix.Indexes, i)
}
}
// InsertRows adds n rows to end of underlying Table, and to the indexes starting at
// given index in this view
func (ix *IndexView) InsertRows(at, n int) {
stidx := ix.Table.Rows
ix.Table.SetNumRows(stidx + n)
nw := make([]int, n, n+len(ix.Indexes)-at)
for i := 0; i < n; i++ {
nw[i] = stidx + i
}
ix.Indexes = append(ix.Indexes[:at], append(nw, ix.Indexes[at:]...)...)
}
// DeleteRows deletes n rows of indexes starting at given index in the list of indexes
func (ix *IndexView) DeleteRows(at, n int) {
ix.Indexes = append(ix.Indexes[:at], ix.Indexes[at+n:]...)
}
// RowsByStringIndex returns the list of *our indexes* whose row in the table has
// given string value in given column index (de-reference our indexes to get actual row).
// if contains, only checks if row contains string; if ignoreCase, ignores case.
// Use named args for greater clarity.
func (ix *IndexView) RowsByStringIndex(colIndex int, str string, contains, ignoreCase bool) []int {
dt := ix.Table
col := dt.Columns[colIndex]
lowstr := strings.ToLower(str)
var idxs []int
for idx, srw := range ix.Indexes {
val := col.String1D(srw)
has := false
switch {
case contains && ignoreCase:
has = strings.Contains(strings.ToLower(val), lowstr)
case contains:
has = strings.Contains(val, str)
case ignoreCase:
has = strings.EqualFold(val, str)
default:
has = (val == str)
}
if has {
idxs = append(idxs, idx)
}
}
return idxs
}
// RowsByString returns the list of *our indexes* whose row in the table has
// given string value in given column name (de-reference our indexes to get actual row).
// if contains, only checks if row contains string; if ignoreCase, ignores case.
// returns error message for invalid column name.
// Use named args for greater clarity.
func (ix *IndexView) RowsByString(column string, str string, contains, ignoreCase bool) ([]int, error) {
dt := ix.Table
ci, err := dt.ColumnIndex(column)
if err != nil {
return nil, err
}
return ix.RowsByStringIndex(ci, str, contains, ignoreCase), nil
}
// Len returns the length of the index list
func (ix *IndexView) Len() int {
return len(ix.Indexes)
}
// Less calls the LessFunc for sorting
func (ix *IndexView) Less(i, j int) bool {
return ix.lessFunc(ix.Table, ix.Indexes[i], ix.Indexes[j])
}
// Swap switches the indexes for i and j
func (ix *IndexView) Swap(i, j int) {
ix.Indexes[i], ix.Indexes[j] = ix.Indexes[j], ix.Indexes[i]
}
// Copyright (c) 2024, 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 table
import (
"bufio"
"encoding/csv"
"fmt"
"io"
"io/fs"
"log"
"math"
"os"
"reflect"
"strconv"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/core"
"cogentcore.org/core/tensor"
)
// Delim are standard CSV delimiter options (Tab, Comma, Space)
type Delims int32 //enums:enum
const (
// Tab is the tab rune delimiter, for TSV tab separated values
Tab Delims = iota
// Comma is the comma rune delimiter, for CSV comma separated values
Comma
// Space is the space rune delimiter, for SSV space separated value
Space
// Detect is used during reading a file -- reads the first line and detects tabs or commas
Detect
)
func (dl Delims) Rune() rune {
switch dl {
case Tab:
return '\t'
case Comma:
return ','
case Space:
return ' '
}
return '\t'
}
const (
// Headers is passed to CSV methods for the headers arg, to use headers
// that capture full type and tensor shape information.
Headers = true
// NoHeaders is passed to CSV methods for the headers arg, to not use headers
NoHeaders = false
)
// SaveCSV writes a table to a comma-separated-values (CSV) file
// (where comma = any delimiter, specified in the delim arg).
// If headers = true then generate column headers that capture the type
// and tensor cell geometry of the columns, enabling full reloading
// of exactly the same table format and data (recommended).
// Otherwise, only the data is written.
func (dt *Table) SaveCSV(filename core.Filename, delim Delims, headers bool) error { //types:add
fp, err := os.Create(string(filename))
defer fp.Close()
if err != nil {
log.Println(err)
return err
}
bw := bufio.NewWriter(fp)
err = dt.WriteCSV(bw, delim, headers)
bw.Flush()
return err
}
// SaveCSV writes a table index view to a comma-separated-values (CSV) file
// (where comma = any delimiter, specified in the delim arg).
// If headers = true then generate column headers that capture the type
// and tensor cell geometry of the columns, enabling full reloading
// of exactly the same table format and data (recommended).
// Otherwise, only the data is written.
func (ix *IndexView) SaveCSV(filename core.Filename, delim Delims, headers bool) error { //types:add
fp, err := os.Create(string(filename))
defer fp.Close()
if err != nil {
log.Println(err)
return err
}
bw := bufio.NewWriter(fp)
err = ix.WriteCSV(bw, delim, headers)
bw.Flush()
return err
}
// OpenCSV reads a table from a comma-separated-values (CSV) file
// (where comma = any delimiter, specified in the delim arg),
// using the Go standard encoding/csv reader conforming to the official CSV standard.
// If the table does not currently have any columns, the first row of the file
// is assumed to be headers, and columns are constructed therefrom.
// If the file was saved from table with headers, then these have full configuration
// information for tensor type and dimensionality.
// If the table DOES have existing columns, then those are used robustly
// for whatever information fits from each row of the file.
func (dt *Table) OpenCSV(filename core.Filename, delim Delims) error { //types:add
fp, err := os.Open(string(filename))
if err != nil {
return errors.Log(err)
}
defer fp.Close()
return dt.ReadCSV(bufio.NewReader(fp), delim)
}
// OpenFS is the version of [Table.OpenCSV] that uses an [fs.FS] filesystem.
func (dt *Table) OpenFS(fsys fs.FS, filename string, delim Delims) error {
fp, err := fsys.Open(filename)
if err != nil {
return errors.Log(err)
}
defer fp.Close()
return dt.ReadCSV(bufio.NewReader(fp), delim)
}
// OpenCSV reads a table idx view from a comma-separated-values (CSV) file
// (where comma = any delimiter, specified in the delim arg),
// using the Go standard encoding/csv reader conforming to the official CSV standard.
// If the table does not currently have any columns, the first row of the file
// is assumed to be headers, and columns are constructed therefrom.
// If the file was saved from table with headers, then these have full configuration
// information for tensor type and dimensionality.
// If the table DOES have existing columns, then those are used robustly
// for whatever information fits from each row of the file.
func (ix *IndexView) OpenCSV(filename core.Filename, delim Delims) error { //types:add
err := ix.Table.OpenCSV(filename, delim)
ix.Sequential()
return err
}
// OpenFS is the version of [IndexView.OpenCSV] that uses an [fs.FS] filesystem.
func (ix *IndexView) OpenFS(fsys fs.FS, filename string, delim Delims) error {
err := ix.Table.OpenFS(fsys, filename, delim)
ix.Sequential()
return err
}
// ReadCSV reads a table from a comma-separated-values (CSV) file
// (where comma = any delimiter, specified in the delim arg),
// using the Go standard encoding/csv reader conforming to the official CSV standard.
// If the table does not currently have any columns, the first row of the file
// is assumed to be headers, and columns are constructed therefrom.
// If the file was saved from table with headers, then these have full configuration
// information for tensor type and dimensionality.
// If the table DOES have existing columns, then those are used robustly
// for whatever information fits from each row of the file.
func (dt *Table) ReadCSV(r io.Reader, delim Delims) error {
cr := csv.NewReader(r)
cr.Comma = delim.Rune()
rec, err := cr.ReadAll() // todo: lazy, avoid resizing
if err != nil || len(rec) == 0 {
return err
}
rows := len(rec)
// cols := len(rec[0])
strow := 0
if dt.NumColumns() == 0 || DetectTableHeaders(rec[0]) {
dt.DeleteAll()
err := ConfigFromHeaders(dt, rec[0], rec)
if err != nil {
log.Println(err.Error())
return err
}
strow++
rows--
}
dt.SetNumRows(rows)
for ri := 0; ri < rows; ri++ {
dt.ReadCSVRow(rec[ri+strow], ri)
}
return nil
}
// ReadCSVRow reads a record of CSV data into given row in table
func (dt *Table) ReadCSVRow(rec []string, row int) {
tc := dt.NumColumns()
ci := 0
if rec[0] == "_D:" { // data row
ci++
}
nan := math.NaN()
for j := 0; j < tc; j++ {
tsr := dt.Columns[j]
_, csz := tsr.RowCellSize()
stoff := row * csz
for cc := 0; cc < csz; cc++ {
str := rec[ci]
if !tsr.IsString() {
if str == "" || str == "NaN" || str == "-NaN" || str == "Inf" || str == "-Inf" {
tsr.SetFloat1D(stoff+cc, nan)
} else {
tsr.SetString1D(stoff+cc, strings.TrimSpace(str))
}
} else {
tsr.SetString1D(stoff+cc, strings.TrimSpace(str))
}
ci++
if ci >= len(rec) {
return
}
}
}
}
// ConfigFromHeaders attempts to configure Table based on the headers.
// for non-table headers, data is examined to determine types.
func ConfigFromHeaders(dt *Table, hdrs []string, rec [][]string) error {
if DetectTableHeaders(hdrs) {
return ConfigFromTableHeaders(dt, hdrs)
}
return ConfigFromDataValues(dt, hdrs, rec)
}
// DetectTableHeaders looks for special header characters -- returns true if found
func DetectTableHeaders(hdrs []string) bool {
for _, hd := range hdrs {
hd = strings.TrimSpace(hd)
if hd == "" {
continue
}
if hd == "_H:" {
return true
}
if _, ok := TableHeaderToType[hd[0]]; !ok { // all must be table
return false
}
}
return true
}
// ConfigFromTableHeaders attempts to configure a Table based on special table headers
func ConfigFromTableHeaders(dt *Table, hdrs []string) error {
for _, hd := range hdrs {
hd = strings.TrimSpace(hd)
if hd == "" || hd == "_H:" {
continue
}
typ, hd := TableColumnType(hd)
dimst := strings.Index(hd, "]<")
if dimst > 0 {
dims := hd[dimst+2 : len(hd)-1]
lbst := strings.Index(hd, "[")
hd = hd[:lbst]
csh := ShapeFromString(dims)
// new tensor starting
dt.AddTensorColumnOfType(typ, hd, csh, "Row")
continue
}
dimst = strings.Index(hd, "[")
if dimst > 0 {
continue
}
dt.AddColumnOfType(typ, hd)
}
return nil
}
// TableHeaderToType maps special header characters to data type
var TableHeaderToType = map[byte]reflect.Kind{
'$': reflect.String,
'%': reflect.Float32,
'#': reflect.Float64,
'|': reflect.Int,
'^': reflect.Bool,
}
// TableHeaderChar returns the special header character based on given data type
func TableHeaderChar(typ reflect.Kind) byte {
switch {
case typ == reflect.Bool:
return '^'
case typ == reflect.Float32:
return '%'
case typ == reflect.Float64:
return '#'
case typ >= reflect.Int && typ <= reflect.Uintptr:
return '|'
default:
return '$'
}
}
// TableColumnType parses the column header for special table type information
func TableColumnType(nm string) (reflect.Kind, string) {
typ, ok := TableHeaderToType[nm[0]]
if ok {
nm = nm[1:]
} else {
typ = reflect.String // most general, default
}
return typ, nm
}
// ShapeFromString parses string representation of shape as N:d,d,..
func ShapeFromString(dims string) []int {
clni := strings.Index(dims, ":")
nd, _ := strconv.Atoi(dims[:clni])
sh := make([]int, nd)
ci := clni + 1
for i := 0; i < nd; i++ {
dstr := ""
if i < nd-1 {
nci := strings.Index(dims[ci:], ",")
dstr = dims[ci : ci+nci]
ci += nci + 1
} else {
dstr = dims[ci:]
}
d, _ := strconv.Atoi(dstr)
sh[i] = d
}
return sh
}
// ConfigFromDataValues configures a Table based on data types inferred
// from the string representation of given records, using header names if present.
func ConfigFromDataValues(dt *Table, hdrs []string, rec [][]string) error {
nr := len(rec)
for ci, hd := range hdrs {
hd = strings.TrimSpace(hd)
if hd == "" {
hd = fmt.Sprintf("col_%d", ci)
}
nmatch := 0
typ := reflect.String
for ri := 1; ri < nr; ri++ {
rv := rec[ri][ci]
if rv == "" {
continue
}
ctyp := InferDataType(rv)
switch {
case ctyp == reflect.String: // definitive
typ = ctyp
break
case typ == ctyp && (nmatch > 1 || ri == nr-1): // good enough
break
case typ == ctyp: // gather more info
nmatch++
case typ == reflect.String: // always upgrade from string default
nmatch = 0
typ = ctyp
case typ == reflect.Int && ctyp == reflect.Float64: // upgrade
nmatch = 0
typ = ctyp
}
}
dt.AddColumnOfType(typ, hd)
}
return nil
}
// InferDataType returns the inferred data type for the given string
// only deals with float64, int, and string types
func InferDataType(str string) reflect.Kind {
if strings.Contains(str, ".") {
_, err := strconv.ParseFloat(str, 64)
if err == nil {
return reflect.Float64
}
}
_, err := strconv.ParseInt(str, 10, 64)
if err == nil {
return reflect.Int
}
// try float again just in case..
_, err = strconv.ParseFloat(str, 64)
if err == nil {
return reflect.Float64
}
return reflect.String
}
//////////////////////////////////////////////////////////////////////////
// WriteCSV
// WriteCSV writes a table to a comma-separated-values (CSV) file
// (where comma = any delimiter, specified in the delim arg).
// If headers = true then generate column headers that capture the type
// and tensor cell geometry of the columns, enabling full reloading
// of exactly the same table format and data (recommended).
// Otherwise, only the data is written.
func (dt *Table) WriteCSV(w io.Writer, delim Delims, headers bool) error {
ncol := 0
var err error
if headers {
ncol, err = dt.WriteCSVHeaders(w, delim)
if err != nil {
log.Println(err)
return err
}
}
cw := csv.NewWriter(w)
cw.Comma = delim.Rune()
for ri := 0; ri < dt.Rows; ri++ {
err = dt.WriteCSVRowWriter(cw, ri, ncol)
if err != nil {
log.Println(err)
return err
}
}
cw.Flush()
return nil
}
// WriteCSV writes only rows in table idx view to a comma-separated-values (CSV) file
// (where comma = any delimiter, specified in the delim arg).
// If headers = true then generate column headers that capture the type
// and tensor cell geometry of the columns, enabling full reloading
// of exactly the same table format and data (recommended).
// Otherwise, only the data is written.
func (ix *IndexView) WriteCSV(w io.Writer, delim Delims, headers bool) error {
ncol := 0
var err error
if headers {
ncol, err = ix.Table.WriteCSVHeaders(w, delim)
if err != nil {
log.Println(err)
return err
}
}
cw := csv.NewWriter(w)
cw.Comma = delim.Rune()
nrow := ix.Len()
for ri := 0; ri < nrow; ri++ {
err = ix.Table.WriteCSVRowWriter(cw, ix.Indexes[ri], ncol)
if err != nil {
log.Println(err)
return err
}
}
cw.Flush()
return nil
}
// WriteCSVHeaders writes headers to a comma-separated-values (CSV) file
// (where comma = any delimiter, specified in the delim arg).
// Returns number of columns in header
func (dt *Table) WriteCSVHeaders(w io.Writer, delim Delims) (int, error) {
cw := csv.NewWriter(w)
cw.Comma = delim.Rune()
hdrs := dt.TableHeaders()
nc := len(hdrs)
err := cw.Write(hdrs)
if err != nil {
return nc, err
}
cw.Flush()
return nc, nil
}
// WriteCSVRow writes given row to a comma-separated-values (CSV) file
// (where comma = any delimiter, specified in the delim arg)
func (dt *Table) WriteCSVRow(w io.Writer, row int, delim Delims) error {
cw := csv.NewWriter(w)
cw.Comma = delim.Rune()
err := dt.WriteCSVRowWriter(cw, row, 0)
cw.Flush()
return err
}
// WriteCSVRowWriter uses csv.Writer to write one row
func (dt *Table) WriteCSVRowWriter(cw *csv.Writer, row int, ncol int) error {
prec := -1
if ps, ok := dt.MetaData["precision"]; ok {
prec, _ = strconv.Atoi(ps)
}
var rec []string
if ncol > 0 {
rec = make([]string, 0, ncol)
} else {
rec = make([]string, 0)
}
rc := 0
for i := range dt.Columns {
tsr := dt.Columns[i]
nd := tsr.NumDims()
if nd == 1 {
vl := ""
if prec <= 0 || tsr.IsString() {
vl = tsr.String1D(row)
} else {
vl = strconv.FormatFloat(tsr.Float1D(row), 'g', prec, 64)
}
if len(rec) <= rc {
rec = append(rec, vl)
} else {
rec[rc] = vl
}
rc++
} else {
csh := tensor.NewShape(tsr.Shape().Sizes[1:]) // cell shape
tc := csh.Len()
for ti := 0; ti < tc; ti++ {
vl := ""
if prec <= 0 || tsr.IsString() {
vl = tsr.String1D(row*tc + ti)
} else {
vl = strconv.FormatFloat(tsr.Float1D(row*tc+ti), 'g', prec, 64)
}
if len(rec) <= rc {
rec = append(rec, vl)
} else {
rec[rc] = vl
}
rc++
}
}
}
err := cw.Write(rec)
return err
}
// TableHeaders generates special header strings from the table
// with full information about type and tensor cell dimensionality.
func (dt *Table) TableHeaders() []string {
hdrs := []string{}
for i := range dt.Columns {
tsr := dt.Columns[i]
nm := dt.ColumnNames[i]
nm = string([]byte{TableHeaderChar(tsr.DataType())}) + nm
if tsr.NumDims() == 1 {
hdrs = append(hdrs, nm)
} else {
csh := tensor.NewShape(tsr.Shape().Sizes[1:]) // cell shape
tc := csh.Len()
nd := csh.NumDims()
fnm := nm + fmt.Sprintf("[%v:", nd)
dn := fmt.Sprintf("<%v:", nd)
ffnm := fnm
for di := 0; di < nd; di++ {
ffnm += "0"
dn += fmt.Sprintf("%v", csh.DimSize(di))
if di < nd-1 {
ffnm += ","
dn += ","
}
}
ffnm += "]" + dn + ">"
hdrs = append(hdrs, ffnm)
for ti := 1; ti < tc; ti++ {
idx := csh.Index(ti)
ffnm := fnm
for di := 0; di < nd; di++ {
ffnm += fmt.Sprintf("%v", idx[di])
if di < nd-1 {
ffnm += ","
}
}
ffnm += "]"
hdrs = append(hdrs, ffnm)
}
}
}
return hdrs
}
// Copyright (c) 2024, 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 table
import (
"fmt"
"reflect"
"cogentcore.org/core/base/reflectx"
)
// NewSliceTable returns a new Table with data from the given slice
// of structs.
func NewSliceTable(st any) (*Table, error) {
npv := reflectx.NonPointerValue(reflect.ValueOf(st))
if npv.Kind() != reflect.Slice {
return nil, fmt.Errorf("NewSliceTable: not a slice")
}
eltyp := reflectx.NonPointerType(npv.Type().Elem())
if eltyp.Kind() != reflect.Struct {
return nil, fmt.Errorf("NewSliceTable: element type is not a struct")
}
dt := NewTable()
for i := 0; i < eltyp.NumField(); i++ {
f := eltyp.Field(i)
switch f.Type.Kind() {
case reflect.Float32:
dt.AddFloat32Column(f.Name)
case reflect.Float64:
dt.AddFloat64Column(f.Name)
case reflect.String:
dt.AddStringColumn(f.Name)
}
}
nr := npv.Len()
dt.SetNumRows(nr)
for ri := 0; ri < nr; ri++ {
for i := 0; i < eltyp.NumField(); i++ {
f := eltyp.Field(i)
switch f.Type.Kind() {
case reflect.Float32:
dt.SetFloat(f.Name, ri, float64(npv.Index(ri).Field(i).Interface().(float32)))
case reflect.Float64:
dt.SetFloat(f.Name, ri, float64(npv.Index(ri).Field(i).Interface().(float64)))
case reflect.String:
dt.SetString(f.Name, ri, npv.Index(ri).Field(i).Interface().(string))
}
}
}
return dt, nil
}
// UpdateSliceTable updates given Table with data from the given slice
// of structs, which must be the same type as used to configure the table
func UpdateSliceTable(st any, dt *Table) {
npv := reflectx.NonPointerValue(reflect.ValueOf(st))
eltyp := reflectx.NonPointerType(npv.Type().Elem())
nr := npv.Len()
dt.SetNumRows(nr)
for ri := 0; ri < nr; ri++ {
for i := 0; i < eltyp.NumField(); i++ {
f := eltyp.Field(i)
switch f.Type.Kind() {
case reflect.Float32:
dt.SetFloat(f.Name, ri, float64(npv.Index(ri).Field(i).Interface().(float32)))
case reflect.Float64:
dt.SetFloat(f.Name, ri, float64(npv.Index(ri).Field(i).Interface().(float64)))
case reflect.String:
dt.SetString(f.Name, ri, npv.Index(ri).Field(i).Interface().(string))
}
}
}
}
// Copyright (c) 2024, 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 table
import (
"fmt"
"slices"
"sort"
"strings"
"cogentcore.org/core/base/errors"
)
// SplitAgg contains aggregation results for splits
type SplitAgg struct {
// the name of the aggregation operation performed, e.g., Sum, Mean, etc
Name string
// column index on which the aggregation was performed -- results will have same shape as cells in this column
ColumnIndex int
// aggregation results -- outer index is length of splits, inner is the length of the cell shape for the column
Aggs [][]float64
}
// Splits is a list of indexed views into a given Table, that represent a particular
// way of splitting up the data, e.g., whenever a given column value changes.
//
// It is functionally equivalent to the MultiIndex in python's pandas: it has multiple
// levels of indexes as listed in the Levels field, which then have corresponding
// Values for each split. These index levels can be re-ordered, and new Splits or
// IndexViews's can be created from subsets of the existing levels. The Values are
// stored simply as string values, as this is the most general type and often
// index values are labels etc.
//
// For Splits created by the splits.GroupBy function for example, each index Level is
// the column name that the data was grouped by, and the Values for each split are then
// the values of those columns. However, any arbitrary set of levels and values can
// be used, e.g., as in the splits.GroupByFunc function.
//
// Conceptually, a given Split always contains the full "outer product" of all the
// index levels -- there is one split for each unique combination of values along each
// index level. Thus, removing one level collapses across those values and moves the
// corresponding indexes into the remaining split indexes.
//
// You can Sort and Filter based on the index values directly, to reorganize the splits
// and drop particular index values, etc.
//
// Splits also maintains Aggs aggregate values for each split, which can be computed using
// standard aggregation methods over data columns, using the split.Agg* functions.
//
// The table code contains the structural methods for managing the Splits data.
// See split package for end-user methods to generate different kinds of splits,
// and perform aggregations, etc.
type Splits struct {
// the list of index views for each split
Splits []*IndexView
// levels of indexes used to organize the splits -- each split contains the full outer product across these index levels. for example, if the split was generated by grouping over column values, then these are the column names in order of grouping. the splits are not automatically sorted hierarchically by these levels but e.g., the GroupBy method produces that result -- use the Sort methods to explicitly sort.
Levels []string
// the values of the index levels associated with each split. The outer dimension is the same length as Splits, and the inner dimension is the levels.
Values [][]string
// aggregate results, one for each aggregation operation performed -- split-level data is contained within each SplitAgg struct -- deleting a split removes these aggs but adding new splits just invalidates all existing aggs (they are automatically deleted).
Aggs []*SplitAgg
// current Less function used in sorting
lessFunc SplitsLessFunc `copier:"-" display:"-" xml:"-" json:"-"`
}
// SplitsLessFunc is a function used for sort comparisons that returns
// true if split i is less than split j
type SplitsLessFunc func(spl *Splits, i, j int) bool
// Len returns number of splits
func (spl *Splits) Len() int {
return len(spl.Splits)
}
// Table returns the table from the first split (should be same for all)
// returns nil if no splits yet
func (spl *Splits) Table() *Table {
if len(spl.Splits) == 0 {
return nil
}
return spl.Splits[0].Table
}
// New adds a new split to the list for given table, and with associated
// values, which are copied before saving into Values list, and any number of rows
// from the table associated with this split (also copied).
// Any existing Aggs are deleted by this.
func (spl *Splits) New(dt *Table, values []string, rows ...int) *IndexView {
spl.Aggs = nil
ix := &IndexView{Table: dt}
spl.Splits = append(spl.Splits, ix)
if len(rows) > 0 {
ix.Indexes = append(ix.Indexes, slices.Clone(rows)...)
}
if len(values) > 0 {
spl.Values = append(spl.Values, slices.Clone(values))
} else {
spl.Values = append(spl.Values, nil)
}
return ix
}
// ByValue finds split indexes by matching to split values, returns nil if not found.
// values are used in order as far as they go and any remaining values are assumed
// to match, and any empty values will match anything. Can use this to access different
// subgroups within overall set of splits.
func (spl *Splits) ByValue(values []string) []int {
var matches []int
for si, sn := range spl.Values {
sz := min(len(sn), len(values))
match := true
for j := 0; j < sz; j++ {
if values[j] == "" {
continue
}
if values[j] != sn[j] {
match = false
break
}
}
if match {
matches = append(matches, si)
}
}
return matches
}
// Delete deletes split at given index -- use this to coordinate deletion
// of Splits, Values, and Aggs values for given split
func (spl *Splits) Delete(idx int) {
spl.Splits = append(spl.Splits[:idx], spl.Splits[idx+1:]...)
spl.Values = append(spl.Values[:idx], spl.Values[idx+1:]...)
for _, ag := range spl.Aggs {
ag.Aggs = append(ag.Aggs[:idx], ag.Aggs[idx+1:]...)
}
}
// Filter removes any split for which given function returns false
func (spl *Splits) Filter(fun func(idx int) bool) {
sz := len(spl.Splits)
for si := sz - 1; si >= 0; si-- {
if !fun(si) {
spl.Delete(si)
}
}
}
// Sort sorts the splits according to the given Less function.
func (spl *Splits) Sort(lessFunc func(spl *Splits, i, j int) bool) {
spl.lessFunc = lessFunc
sort.Sort(spl)
}
// SortLevels sorts the splits according to the current index level ordering of values
// i.e., first index level is outer sort dimension, then within that is the next, etc
func (spl *Splits) SortLevels() {
spl.Sort(func(sl *Splits, i, j int) bool {
vli := sl.Values[i]
vlj := sl.Values[j]
for k := range vli {
if vli[k] < vlj[k] {
return true
} else if vli[k] > vlj[k] {
return false
} // fallthrough
}
return false
})
}
// SortOrder sorts the splits according to the given ordering of index levels
// which can be a subset as well
func (spl *Splits) SortOrder(order []int) error {
if len(order) == 0 || len(order) > len(spl.Levels) {
return fmt.Errorf("table.Splits SortOrder: order length == 0 or > Levels")
}
spl.Sort(func(sl *Splits, i, j int) bool {
vli := sl.Values[i]
vlj := sl.Values[j]
for k := range order {
if vli[order[k]] < vlj[order[k]] {
return true
} else if vli[order[k]] > vlj[order[k]] {
return false
} // fallthrough
}
return false
})
return nil
}
// ReorderLevels re-orders the index levels according to the given new ordering indexes
// e.g., []int{1,0} will move the current level 0 to level 1, and 1 to level 0
// no checking is done to ensure these are sensible beyond basic length test --
// behavior undefined if so. Typically you want to call SortLevels after this.
func (spl *Splits) ReorderLevels(order []int) error {
nlev := len(spl.Levels)
if len(order) != nlev {
return fmt.Errorf("table.Splits ReorderLevels: order length != Levels")
}
old := make([]string, nlev)
copy(old, spl.Levels)
for i := range order {
spl.Levels[order[i]] = old[i]
}
for si := range spl.Values {
copy(old, spl.Values[si])
for i := range order {
spl.Values[si][order[i]] = old[i]
}
}
return nil
}
// ExtractLevels returns a new Splits that only has the given levels of indexes,
// in their given order, with the other levels removed and their corresponding indexes
// merged into the appropriate remaining levels.
// Any existing aggregation data is not retained in the new splits.
func (spl *Splits) ExtractLevels(levels []int) (*Splits, error) {
nlv := len(levels)
if nlv == 0 || nlv >= len(spl.Levels) {
return nil, fmt.Errorf("table.Splits ExtractLevels: levels length == 0 or >= Levels")
}
aggs := spl.Aggs
spl.Aggs = nil
ss := spl.Clone()
spl.Aggs = aggs
ss.SortOrder(levels)
// now just do the grouping by levels values
lstValues := make([]string, nlv)
curValues := make([]string, nlv)
var curIx *IndexView
nsp := len(ss.Splits)
for si := nsp - 1; si >= 0; si-- {
diff := false
for li := range levels {
vl := ss.Values[si][levels[li]]
curValues[li] = vl
if vl != lstValues[li] {
diff = true
}
}
if diff || curIx == nil {
curIx = ss.Splits[si]
copy(lstValues, curValues)
ss.Values[si] = slices.Clone(curValues)
} else {
curIx.Indexes = append(curIx.Indexes, ss.Splits[si].Indexes...) // absorb
ss.Delete(si)
}
}
ss.Levels = make([]string, nlv)
for li := range levels {
ss.Levels[li] = spl.Levels[levels[li]]
}
return ss, nil
}
// Clone returns a cloned copy of our SplitAgg
func (sa *SplitAgg) Clone() *SplitAgg {
nsa := &SplitAgg{}
nsa.CopyFrom(sa)
return nsa
}
// CopyFrom copies from other SplitAgg -- we get our own unique copy of everything
func (sa *SplitAgg) CopyFrom(osa *SplitAgg) {
sa.Name = osa.Name
sa.ColumnIndex = osa.ColumnIndex
nags := len(osa.Aggs)
if nags > 0 {
sa.Aggs = make([][]float64, nags)
for si := range osa.Aggs {
sa.Aggs[si] = slices.Clone(osa.Aggs[si])
}
}
}
// Clone returns a cloned copy of our splits
func (spl *Splits) Clone() *Splits {
nsp := &Splits{}
nsp.CopyFrom(spl)
return nsp
}
// CopyFrom copies from other Splits -- we get our own unique copy of everything
func (spl *Splits) CopyFrom(osp *Splits) {
spl.Splits = make([]*IndexView, len(osp.Splits))
spl.Values = make([][]string, len(osp.Values))
for si := range osp.Splits {
spl.Splits[si] = osp.Splits[si].Clone()
spl.Values[si] = slices.Clone(osp.Values[si])
}
spl.Levels = slices.Clone(osp.Levels)
nag := len(osp.Aggs)
if nag > 0 {
spl.Aggs = make([]*SplitAgg, nag)
for ai := range osp.Aggs {
spl.Aggs[ai] = osp.Aggs[ai].Clone()
}
}
}
// AddAgg adds a new set of aggregation results for the Splits
func (spl *Splits) AddAgg(name string, colIndex int) *SplitAgg {
ag := &SplitAgg{Name: name, ColumnIndex: colIndex}
spl.Aggs = append(spl.Aggs, ag)
return ag
}
// DeleteAggs deletes all existing aggregation data
func (spl *Splits) DeleteAggs() {
spl.Aggs = nil
}
// AggByName returns Agg results for given name, which does NOT include the
// column name, just the name given to the Agg result
// (e.g., Mean for a standard Mean agg).
// Returns error message if not found.
func (spl *Splits) AggByName(name string) (*SplitAgg, error) {
for _, ag := range spl.Aggs {
if ag.Name == name {
return ag, nil
}
}
return nil, fmt.Errorf("table.Splits AggByName: agg results named: %v not found", name)
}
// AggByColumnName returns Agg results for given column name,
// optionally including :Name agg name appended, where Name
// is the name given to the Agg result (e.g., Mean for a standard Mean agg).
// Returns error message if not found.
func (spl *Splits) AggByColumnName(name string) (*SplitAgg, error) {
dt := spl.Table()
if dt == nil {
return nil, fmt.Errorf("table.Splits AggByColumnName: table nil")
}
nmsp := strings.Split(name, ":")
colIndex, err := dt.ColumnIndex(nmsp[0])
if err != nil {
return nil, err
}
for _, ag := range spl.Aggs {
if ag.ColumnIndex != colIndex {
continue
}
if len(nmsp) == 2 && nmsp[1] != ag.Name {
continue
}
return ag, nil
}
return nil, fmt.Errorf("table.Splits AggByColumnName: agg results named: %v not found", name)
}
// SetLevels sets the Levels index names -- must match actual index dimensionality
// of the Values. This is automatically done by e.g., GroupBy, but must be done
// manually if creating custom indexes.
func (spl *Splits) SetLevels(levels ...string) {
spl.Levels = levels
}
// use these for arg to ArgsToTable*
const (
// ColumnNameOnly means resulting agg table just has the original column name, no aggregation name
ColumnNameOnly bool = true
// AddAggName means resulting agg table columns have aggregation name appended
AddAggName = false
)
// AggsToTable returns a Table containing this Splits' aggregate data.
// Must have Levels and Aggs all created as in the split.Agg* methods.
// if colName == ColumnNameOnly, then the name of the columns for the Table
// is just the corresponding agg column name -- otherwise it also includes
// the name of the aggregation function with a : divider (e.g., Name:Mean)
func (spl *Splits) AggsToTable(colName bool) *Table {
nsp := len(spl.Splits)
if nsp == 0 {
return nil
}
dt := spl.Splits[0].Table
st := NewTable().SetNumRows(nsp)
for _, cn := range spl.Levels {
oc, _ := dt.ColumnByName(cn)
if oc != nil {
st.AddColumnOfType(oc.DataType(), cn)
} else {
st.AddStringColumn(cn)
}
}
for _, ag := range spl.Aggs {
col := dt.Columns[ag.ColumnIndex]
an := dt.ColumnNames[ag.ColumnIndex]
if colName == AddAggName {
an += ":" + ag.Name
}
st.AddFloat64TensorColumn(an, col.Shape().Sizes[1:], col.Shape().Names[1:]...)
}
for si := range spl.Splits {
cidx := 0
for ci := range spl.Levels {
col := st.Columns[cidx]
col.SetString1D(si, spl.Values[si][ci])
cidx++
}
for _, ag := range spl.Aggs {
col := st.Columns[cidx]
_, csz := col.RowCellSize()
sti := si * csz
av := ag.Aggs[si]
for j, a := range av {
col.SetFloat1D(sti+j, a)
}
cidx++
}
}
return st
}
// AggsToTableCopy returns a Table containing this Splits' aggregate data
// and a copy of the first row of data for each split for all non-agg cols,
// which is useful for recording other data that goes along with aggregated values.
// Must have Levels and Aggs all created as in the split.Agg* methods.
// if colName == ColumnNameOnly, then the name of the columns for the Table
// is just the corresponding agg column name -- otherwise it also includes
// the name of the aggregation function with a : divider (e.g., Name:Mean)
func (spl *Splits) AggsToTableCopy(colName bool) *Table {
nsp := len(spl.Splits)
if nsp == 0 {
return nil
}
dt := spl.Splits[0].Table
st := NewTable().SetNumRows(nsp)
exmap := make(map[string]struct{})
for _, cn := range spl.Levels {
st.AddStringColumn(cn)
exmap[cn] = struct{}{}
}
for _, ag := range spl.Aggs {
col := dt.Columns[ag.ColumnIndex]
an := dt.ColumnNames[ag.ColumnIndex]
exmap[an] = struct{}{}
if colName == AddAggName {
an += ":" + ag.Name
}
st.AddFloat64TensorColumn(an, col.Shape().Sizes[1:], col.Shape().Names[1:]...)
}
var cpcol []string
for _, cn := range dt.ColumnNames {
if _, ok := exmap[cn]; !ok {
cpcol = append(cpcol, cn)
col := errors.Log1(dt.ColumnByName(cn))
st.AddColumn(col.Clone(), cn)
}
}
for si, sidx := range spl.Splits {
cidx := 0
for ci := range spl.Levels {
col := st.Columns[cidx]
col.SetString1D(si, spl.Values[si][ci])
cidx++
}
for _, ag := range spl.Aggs {
col := st.Columns[cidx]
_, csz := col.RowCellSize()
sti := si * csz
av := ag.Aggs[si]
for j, a := range av {
col.SetFloat1D(sti+j, a)
}
cidx++
}
if len(sidx.Indexes) > 0 {
stidx := sidx.Indexes[0]
for _, cn := range cpcol {
st.CopyCell(cn, si, dt, cn, stidx)
}
}
}
return st
}
// Less calls the LessFunc for sorting
func (spl *Splits) Less(i, j int) bool {
return spl.lessFunc(spl, i, j)
}
// Swap switches the indexes for i and j
func (spl *Splits) Swap(i, j int) {
spl.Splits[i], spl.Splits[j] = spl.Splits[j], spl.Splits[i]
spl.Values[i], spl.Values[j] = spl.Values[j], spl.Values[i]
for _, ag := range spl.Aggs {
ag.Aggs[i], ag.Aggs[j] = ag.Aggs[j], ag.Aggs[i]
}
}
// Copyright (c) 2024, 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 table
//go:generate core generate
import (
"errors"
"fmt"
"log/slog"
"math"
"reflect"
"slices"
"strings"
"cogentcore.org/core/tensor"
)
// Table is a table of data, with columns of tensors,
// each with the same number of Rows (outer-most dimension).
type Table struct { //types:add
// columns of data, as tensor.Tensor tensors
Columns []tensor.Tensor `display:"no-inline"`
// the names of the columns
ColumnNames []string
// number of rows, which is enforced to be the size of the outer-most dimension of the column tensors
Rows int `edit:"-"`
// the map of column names to column numbers
ColumnNameMap map[string]int `display:"-"`
// misc meta data for the table. We use lower-case key names following the struct tag convention: name = name of table; desc = description; read-only = gui is read-only; precision = n for precision to write out floats in csv. For Column-specific data, we look for ColumnName: prefix, specifically ColumnName:desc = description of the column contents, which is shown as tooltip in the tensorcore.Table, and :width for width of a column
MetaData map[string]string
}
func NewTable(name ...string) *Table {
dt := &Table{}
if len(name) > 0 {
dt.SetMetaData("name", name[0])
}
return dt
}
// IsValidRow returns error if the row is invalid
func (dt *Table) IsValidRow(row int) error {
if row < 0 || row >= dt.Rows {
return fmt.Errorf("table.Table IsValidRow: row %d is out of valid range [0..%d]", row, dt.Rows)
}
return nil
}
// NumRows returns the number of rows
func (dt *Table) NumRows() int { return dt.Rows }
// NumColumns returns the number of columns
func (dt *Table) NumColumns() int { return len(dt.Columns) }
// Column returns the tensor at given column index
func (dt *Table) Column(i int) tensor.Tensor { return dt.Columns[i] }
// ColumnByName returns the tensor at given column name, with error message if not found.
// Returns nil if not found
func (dt *Table) ColumnByName(name string) (tensor.Tensor, error) {
i, ok := dt.ColumnNameMap[name]
if !ok {
return nil, fmt.Errorf("table.Table ColumnByNameTry: column named: %v not found", name)
}
return dt.Columns[i], nil
}
// ColumnIndex returns the index of the given column name,
// along with an error if not found.
func (dt *Table) ColumnIndex(name string) (int, error) {
i, ok := dt.ColumnNameMap[name]
if !ok {
return 0, fmt.Errorf("table.Table ColumnIndex: column named: %v not found", name)
}
return i, nil
}
// ColumnIndexesByNames returns the indexes of the given column names.
// idxs have -1 if name not found.
func (dt *Table) ColumnIndexesByNames(names ...string) ([]int, error) {
nc := len(names)
if nc == 0 {
return nil, nil
}
var errs []error
cidx := make([]int, nc)
for i, cn := range names {
var err error
cidx[i], err = dt.ColumnIndex(cn)
if err != nil {
errs = append(errs, err)
}
}
return cidx, errors.Join(errs...)
}
// ColumnName returns the name of given column
func (dt *Table) ColumnName(i int) string {
return dt.ColumnNames[i]
}
// UpdateColumnNameMap updates the column name map, returning an error
// if any of the column names are duplicates.
func (dt *Table) UpdateColumnNameMap() error {
nc := dt.NumColumns()
dt.ColumnNameMap = make(map[string]int, nc)
var errs []error
for i, nm := range dt.ColumnNames {
if _, has := dt.ColumnNameMap[nm]; has {
err := fmt.Errorf("table.Table duplicate column name: %s", nm)
slog.Warn(err.Error())
errs = append(errs, err)
} else {
dt.ColumnNameMap[nm] = i
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
// AddColumn adds a new column to the table, of given type and column name
// (which must be unique). The cells of this column hold a single scalar value:
// see AddColumnTensor for n-dimensional cells.
func AddColumn[T string | bool | float32 | float64 | int | int32 | byte](dt *Table, name string) tensor.Tensor {
rows := max(1, dt.Rows)
tsr := tensor.New[T]([]int{rows}, "Row")
dt.AddColumn(tsr, name)
return tsr
}
// InsertColumn inserts a new column to the table, of given type and column name
// (which must be unique), at given index.
// The cells of this column hold a single scalar value.
func InsertColumn[T string | bool | float32 | float64 | int | int32 | byte](dt *Table, name string, idx int) tensor.Tensor {
rows := max(1, dt.Rows)
tsr := tensor.New[T]([]int{rows}, "Row")
dt.InsertColumn(tsr, name, idx)
return tsr
}
// AddTensorColumn adds a new n-dimensional column to the table, of given type, column name
// (which must be unique), and dimensionality of each _cell_.
// An outer-most Row dimension will be added to this dimensionality to create
// the tensor column.
func AddTensorColumn[T string | bool | float32 | float64 | int | int32 | byte](dt *Table, name string, cellSizes []int, dimNames ...string) tensor.Tensor {
rows := max(1, dt.Rows)
sz := append([]int{rows}, cellSizes...)
nms := append([]string{"Row"}, dimNames...)
tsr := tensor.New[T](sz, nms...)
dt.AddColumn(tsr, name)
return tsr
}
// AddColumn adds the given tensor as a column to the table,
// returning an error and not adding if the name is not unique.
// Automatically adjusts the shape to fit the current number of rows.
func (dt *Table) AddColumn(tsr tensor.Tensor, name string) error {
dt.ColumnNames = append(dt.ColumnNames, name)
err := dt.UpdateColumnNameMap()
if err != nil {
dt.ColumnNames = dt.ColumnNames[:len(dt.ColumnNames)-1]
return err
}
dt.Columns = append(dt.Columns, tsr)
rows := max(1, dt.Rows)
tsr.SetNumRows(rows)
return nil
}
// InsertColumn inserts the given tensor as a column to the table at given index,
// returning an error and not adding if the name is not unique.
// Automatically adjusts the shape to fit the current number of rows.
func (dt *Table) InsertColumn(tsr tensor.Tensor, name string, idx int) error {
if _, has := dt.ColumnNameMap[name]; has {
err := fmt.Errorf("table.Table duplicate column name: %s", name)
slog.Warn(err.Error())
return err
}
dt.ColumnNames = slices.Insert(dt.ColumnNames, idx, name)
dt.UpdateColumnNameMap()
dt.Columns = slices.Insert(dt.Columns, idx, tsr)
rows := max(1, dt.Rows)
tsr.SetNumRows(rows)
return nil
}
// AddColumnOfType adds a new scalar column to the table, of given reflect type,
// column name (which must be unique),
// The cells of this column hold a single (scalar) value of given type.
// Supported types are string, bool (for [tensor.Bits]), float32, float64, int, int32, and byte.
func (dt *Table) AddColumnOfType(typ reflect.Kind, name string) tensor.Tensor {
rows := max(1, dt.Rows)
tsr := tensor.NewOfType(typ, []int{rows}, "Row")
dt.AddColumn(tsr, name)
return tsr
}
// AddTensorColumnOfType adds a new n-dimensional column to the table, of given reflect type,
// column name (which must be unique), and dimensionality of each _cell_.
// An outer-most Row dimension will be added to this dimensionality to create
// the tensor column.
// Supported types are string, bool (for [tensor.Bits]), float32, float64, int, int32, and byte.
func (dt *Table) AddTensorColumnOfType(typ reflect.Kind, name string, cellSizes []int, dimNames ...string) tensor.Tensor {
rows := max(1, dt.Rows)
sz := append([]int{rows}, cellSizes...)
nms := append([]string{"Row"}, dimNames...)
tsr := tensor.NewOfType(typ, sz, nms...)
dt.AddColumn(tsr, name)
return tsr
}
// AddStringColumn adds a new String column with given name.
// The cells of this column hold a single string value.
func (dt *Table) AddStringColumn(name string) *tensor.String {
return AddColumn[string](dt, name).(*tensor.String)
}
// AddFloat64Column adds a new float64 column with given name.
// The cells of this column hold a single scalar value.
func (dt *Table) AddFloat64Column(name string) *tensor.Float64 {
return AddColumn[float64](dt, name).(*tensor.Float64)
}
// AddFloat64TensorColumn adds a new n-dimensional float64 column with given name
// and dimensionality of each _cell_.
// An outer-most Row dimension will be added to this dimensionality to create
// the tensor column.
func (dt *Table) AddFloat64TensorColumn(name string, cellSizes []int, dimNames ...string) *tensor.Float64 {
return AddTensorColumn[float64](dt, name, cellSizes, dimNames...).(*tensor.Float64)
}
// AddFloat32Column adds a new float32 column with given name.
// The cells of this column hold a single scalar value.
func (dt *Table) AddFloat32Column(name string) *tensor.Float32 {
return AddColumn[float32](dt, name).(*tensor.Float32)
}
// AddFloat32TensorColumn adds a new n-dimensional float32 column with given name
// and dimensionality of each _cell_.
// An outer-most Row dimension will be added to this dimensionality to create
// the tensor column.
func (dt *Table) AddFloat32TensorColumn(name string, cellSizes []int, dimNames ...string) *tensor.Float32 {
return AddTensorColumn[float32](dt, name, cellSizes, dimNames...).(*tensor.Float32)
}
// AddIntColumn adds a new int column with given name.
// The cells of this column hold a single scalar value.
func (dt *Table) AddIntColumn(name string) *tensor.Int {
return AddColumn[int](dt, name).(*tensor.Int)
}
// AddIntTensorColumn adds a new n-dimensional int column with given name
// and dimensionality of each _cell_.
// An outer-most Row dimension will be added to this dimensionality to create
// the tensor column.
func (dt *Table) AddIntTensorColumn(name string, cellSizes []int, dimNames ...string) *tensor.Int {
return AddTensorColumn[int](dt, name, cellSizes, dimNames...).(*tensor.Int)
}
// DeleteColumnName deletes column of given name.
// returns error if not found.
func (dt *Table) DeleteColumnName(name string) error {
ci, err := dt.ColumnIndex(name)
if err != nil {
return err
}
dt.DeleteColumnIndex(ci)
return nil
}
// DeleteColumnIndex deletes column of given index
func (dt *Table) DeleteColumnIndex(idx int) {
dt.Columns = append(dt.Columns[:idx], dt.Columns[idx+1:]...)
dt.ColumnNames = append(dt.ColumnNames[:idx], dt.ColumnNames[idx+1:]...)
dt.UpdateColumnNameMap()
}
// DeleteAll deletes all columns -- full reset
func (dt *Table) DeleteAll() {
dt.Columns = nil
dt.ColumnNames = nil
dt.Rows = 0
dt.ColumnNameMap = nil
}
// AddRows adds n rows to each of the columns
func (dt *Table) AddRows(n int) { //types:add
dt.SetNumRows(dt.Rows + n)
}
// SetNumRows sets the number of rows in the table, across all columns
// if rows = 0 then effective number of rows in tensors is 1, as this dim cannot be 0
func (dt *Table) SetNumRows(rows int) *Table { //types:add
dt.Rows = rows // can be 0
rows = max(1, rows)
for _, tsr := range dt.Columns {
tsr.SetNumRows(rows)
}
return dt
}
// note: no really clean definition of CopyFrom -- no point of re-using existing
// table -- just clone it.
// Clone returns a complete copy of this table
func (dt *Table) Clone() *Table {
cp := NewTable().SetNumRows(dt.Rows)
cp.CopyMetaDataFrom(dt)
for i, cl := range dt.Columns {
cp.AddColumn(cl.Clone(), dt.ColumnNames[i])
}
return cp
}
// AppendRows appends shared columns in both tables with input table rows
func (dt *Table) AppendRows(dt2 *Table) {
shared := false
strow := dt.Rows
for iCol := range dt.Columns {
colName := dt.ColumnName(iCol)
_, err := dt2.ColumnIndex(colName)
if err != nil {
continue
}
if !shared {
shared = true
dt.AddRows(dt2.Rows)
}
for iRow := 0; iRow < dt2.Rows; iRow++ {
dt.CopyCell(colName, iRow+strow, dt2, colName, iRow)
}
}
}
// SetMetaData sets given meta-data key to given value, safely creating the
// map if not yet initialized. Standard Keys are:
// * name -- name of table
// * desc -- description of table
// * read-only -- makes gui read-only (inactive edits) for tensorcore.Table
// * ColumnName:* -- prefix for all column-specific meta-data
// - desc -- description of column
func (dt *Table) SetMetaData(key, val string) {
if dt.MetaData == nil {
dt.MetaData = make(map[string]string)
}
dt.MetaData[key] = val
}
// CopyMetaDataFrom copies meta data from other table
func (dt *Table) CopyMetaDataFrom(cp *Table) {
nm := len(cp.MetaData)
if nm == 0 {
return
}
if dt.MetaData == nil {
dt.MetaData = make(map[string]string, nm)
}
for k, v := range cp.MetaData {
dt.MetaData[k] = v
}
}
// Named arg values for Contains, IgnoreCase
const (
// Contains means the string only needs to contain the target string (see Equals)
Contains bool = true
// Equals means the string must equal the target string (see Contains)
Equals = false
// IgnoreCase means that differences in case are ignored in comparing strings
IgnoreCase = true
// UseCase means that case matters when comparing strings
UseCase = false
)
// RowsByStringIndex returns the list of rows that have given
// string value in given column index.
// if contains, only checks if row contains string; if ignoreCase, ignores case.
// Use named args for greater clarity.
func (dt *Table) RowsByStringIndex(column int, str string, contains, ignoreCase bool) []int {
col := dt.Columns[column]
lowstr := strings.ToLower(str)
var idxs []int
for i := 0; i < dt.Rows; i++ {
val := col.String1D(i)
has := false
switch {
case contains && ignoreCase:
has = strings.Contains(strings.ToLower(val), lowstr)
case contains:
has = strings.Contains(val, str)
case ignoreCase:
has = strings.EqualFold(val, str)
default:
has = (val == str)
}
if has {
idxs = append(idxs, i)
}
}
return idxs
}
// RowsByString returns the list of rows that have given
// string value in given column name. returns nil & error if name invalid.
// if contains, only checks if row contains string; if ignoreCase, ignores case.
// Use named args for greater clarity.
func (dt *Table) RowsByString(column string, str string, contains, ignoreCase bool) ([]int, error) {
ci, err := dt.ColumnIndex(column)
if err != nil {
return nil, err
}
return dt.RowsByStringIndex(ci, str, contains, ignoreCase), nil
}
//////////////////////////////////////////////////////////////////////////////////////
// Cell convenience access methods
// FloatIndex returns the float64 value of cell at given column, row index
// for columns that have 1-dimensional tensors.
// Returns NaN if column is not a 1-dimensional tensor or row not valid.
func (dt *Table) FloatIndex(column, row int) float64 {
if dt.IsValidRow(row) != nil {
return math.NaN()
}
ct := dt.Columns[column]
if ct.NumDims() != 1 {
return math.NaN()
}
return ct.Float1D(row)
}
// Float returns the float64 value of cell at given column (by name),
// row index for columns that have 1-dimensional tensors.
// Returns NaN if column is not a 1-dimensional tensor
// or col name not found, or row not valid.
func (dt *Table) Float(column string, row int) float64 {
if dt.IsValidRow(row) != nil {
return math.NaN()
}
ct, err := dt.ColumnByName(column)
if err != nil {
return math.NaN()
}
if ct.NumDims() != 1 {
return math.NaN()
}
return ct.Float1D(row)
}
// StringIndex returns the string value of cell at given column, row index
// for columns that have 1-dimensional tensors.
// Returns "" if column is not a 1-dimensional tensor or row not valid.
func (dt *Table) StringIndex(column, row int) string {
if dt.IsValidRow(row) != nil {
return ""
}
ct := dt.Columns[column]
if ct.NumDims() != 1 {
return ""
}
return ct.String1D(row)
}
// NOTE: String conflicts with [fmt.Stringer], so we have to use StringValue
// StringValue returns the string value of cell at given column (by name), row index
// for columns that have 1-dimensional tensors.
// Returns "" if column is not a 1-dimensional tensor or row not valid.
func (dt *Table) StringValue(column string, row int) string {
if dt.IsValidRow(row) != nil {
return ""
}
ct, err := dt.ColumnByName(column)
if err != nil {
return ""
}
if ct.NumDims() != 1 {
return ""
}
return ct.String1D(row)
}
// TensorIndex returns the tensor SubSpace for given column, row index
// for columns that have higher-dimensional tensors so each row is
// represented by an n-1 dimensional tensor, with the outer dimension
// being the row number. Returns nil if column is a 1-dimensional
// tensor or there is any error from the tensor.Tensor.SubSpace call.
func (dt *Table) TensorIndex(column, row int) tensor.Tensor {
if dt.IsValidRow(row) != nil {
return nil
}
ct := dt.Columns[column]
if ct.NumDims() == 1 {
return nil
}
return ct.SubSpace([]int{row})
}
// Tensor returns the tensor SubSpace for given column (by name), row index
// for columns that have higher-dimensional tensors so each row is
// represented by an n-1 dimensional tensor, with the outer dimension
// being the row number. Returns nil on any error.
func (dt *Table) Tensor(column string, row int) tensor.Tensor {
if dt.IsValidRow(row) != nil {
return nil
}
ct, err := dt.ColumnByName(column)
if err != nil {
return nil
}
if ct.NumDims() == 1 {
return nil
}
return ct.SubSpace([]int{row})
}
// TensorFloat1D returns the float value of a Tensor cell's cell at given
// 1D offset within cell, for given column (by name), row index
// for columns that have higher-dimensional tensors so each row is
// represented by an n-1 dimensional tensor, with the outer dimension
// being the row number. Returns 0 on any error.
func (dt *Table) TensorFloat1D(column string, row int, idx int) float64 {
if dt.IsValidRow(row) != nil {
return math.NaN()
}
ct, err := dt.ColumnByName(column)
if err != nil {
return math.NaN()
}
if ct.NumDims() == 1 {
return math.NaN()
}
_, sz := ct.RowCellSize()
if idx >= sz || idx < 0 {
return math.NaN()
}
off := row*sz + idx
return ct.Float1D(off)
}
/////////////////////////////////////////////////////////////////////////////////////
// Set
// SetFloatIndex sets the float64 value of cell at given column, row index
// for columns that have 1-dimensional tensors.
func (dt *Table) SetFloatIndex(column, row int, val float64) error {
if err := dt.IsValidRow(row); err != nil {
return err
}
ct := dt.Columns[column]
if ct.NumDims() != 1 {
return fmt.Errorf("table.Table SetFloatIndex: Column %d is a tensor, must use SetTensorFloat1D", column)
}
ct.SetFloat1D(row, val)
return nil
}
// SetFloat sets the float64 value of cell at given column (by name), row index
// for columns that have 1-dimensional tensors.
func (dt *Table) SetFloat(column string, row int, val float64) error {
if err := dt.IsValidRow(row); err != nil {
return err
}
ct, err := dt.ColumnByName(column)
if err != nil {
return err
}
if ct.NumDims() != 1 {
return fmt.Errorf("table.Table SetFloat: Column %s is a tensor, must use SetTensorFloat1D", column)
}
ct.SetFloat1D(row, val)
return nil
}
// SetStringIndex sets the string value of cell at given column, row index
// for columns that have 1-dimensional tensors. Returns true if set.
func (dt *Table) SetStringIndex(column, row int, val string) error {
if err := dt.IsValidRow(row); err != nil {
return err
}
ct := dt.Columns[column]
if ct.NumDims() != 1 {
return fmt.Errorf("table.Table SetStringIndex: Column %d is a tensor, must use SetTensorFloat1D", column)
}
ct.SetString1D(row, val)
return nil
}
// SetString sets the string value of cell at given column (by name), row index
// for columns that have 1-dimensional tensors. Returns true if set.
func (dt *Table) SetString(column string, row int, val string) error {
if err := dt.IsValidRow(row); err != nil {
return err
}
ct, err := dt.ColumnByName(column)
if err != nil {
return err
}
if ct.NumDims() != 1 {
return fmt.Errorf("table.Table SetString: Column %s is a tensor, must use SetTensorFloat1D", column)
}
ct.SetString1D(row, val)
return nil
}
// SetTensorIndex sets the tensor value of cell at given column, row index
// for columns that have n-dimensional tensors. Returns true if set.
func (dt *Table) SetTensorIndex(column, row int, val tensor.Tensor) error {
if err := dt.IsValidRow(row); err != nil {
return err
}
ct := dt.Columns[column]
_, csz := ct.RowCellSize()
st := row * csz
sz := min(csz, val.Len())
if ct.IsString() {
for j := 0; j < sz; j++ {
ct.SetString1D(st+j, val.String1D(j))
}
} else {
for j := 0; j < sz; j++ {
ct.SetFloat1D(st+j, val.Float1D(j))
}
}
return nil
}
// SetTensor sets the tensor value of cell at given column (by name), row index
// for columns that have n-dimensional tensors. Returns true if set.
func (dt *Table) SetTensor(column string, row int, val tensor.Tensor) error {
if err := dt.IsValidRow(row); err != nil {
return err
}
ci, err := dt.ColumnIndex(column)
if err != nil {
return err
}
return dt.SetTensorIndex(ci, row, val)
}
// SetTensorFloat1D sets the tensor cell's float cell value at given 1D index within cell,
// at given column (by name), row index for columns that have n-dimensional tensors.
// Returns true if set.
func (dt *Table) SetTensorFloat1D(column string, row int, idx int, val float64) error {
if err := dt.IsValidRow(row); err != nil {
return err
}
ct, err := dt.ColumnByName(column)
if err != nil {
return err
}
_, sz := ct.RowCellSize()
if idx >= sz || idx < 0 {
return fmt.Errorf("table.Table IsValidRow: index %d is out of valid range [0..%d]", idx, sz)
}
off := row*sz + idx
ct.SetFloat1D(off, val)
return nil
}
//////////////////////////////////////////////////////////////////////////////////////
// Copy Cell
// CopyCell copies into cell at given column, row from cell in other table.
// It is robust to differences in type; uses destination cell type.
// Returns error if column names are invalid.
func (dt *Table) CopyCell(column string, row int, cpt *Table, cpColNm string, cpRow int) error {
ct, err := dt.ColumnByName(column)
if err != nil {
return err
}
cpct, err := cpt.ColumnByName(cpColNm)
if err != nil {
return err
}
_, sz := ct.RowCellSize()
if sz == 1 {
if ct.IsString() {
ct.SetString1D(row, cpct.String1D(cpRow))
return nil
}
ct.SetFloat1D(row, cpct.Float1D(cpRow))
return nil
}
_, cpsz := cpct.RowCellSize()
st := row * sz
cst := cpRow * cpsz
msz := min(sz, cpsz)
if ct.IsString() {
for j := 0; j < msz; j++ {
ct.SetString1D(st+j, cpct.String1D(cst+j))
}
} else {
for j := 0; j < msz; j++ {
ct.SetFloat1D(st+j, cpct.Float1D(cst+j))
}
}
return nil
}
// Copyright (c) 2024, 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 table
import (
"errors"
"fmt"
"reflect"
"strings"
"cogentcore.org/core/tensor"
)
// InsertKeyColumns returns a copy of the given Table with new columns
// having given values, inserted at the start, used as legend keys etc.
// args must be in pairs: column name, value. All rows get the same value.
func (dt *Table) InsertKeyColumns(args ...string) *Table {
n := len(args)
if n%2 != 0 {
fmt.Println("InsertKeyColumns requires even number of args as column name, value pairs")
return dt
}
c := dt.Clone()
nc := n / 2
for j := range nc {
colNm := args[2*j]
val := args[2*j+1]
col := tensor.NewString([]int{c.Rows})
c.InsertColumn(col, colNm, 0)
for i := range col.Values {
col.Values[i] = val
}
}
return c
}
// ConfigFromTable configures the columns of this table according to the
// values in the first two columns of given format table, conventionally named
// Name, Type (but names are not used), which must be of the string type.
func (dt *Table) ConfigFromTable(ft *Table) error {
nmcol := ft.Columns[0]
tycol := ft.Columns[1]
var errs []error
for i := range ft.Rows {
name := nmcol.String1D(i)
typ := strings.ToLower(tycol.String1D(i))
kind := reflect.Float64
switch typ {
case "string":
kind = reflect.String
case "bool":
kind = reflect.Bool
case "float32":
kind = reflect.Float32
case "float64":
kind = reflect.Float64
case "int":
kind = reflect.Int
case "int32":
kind = reflect.Int32
case "byte", "uint8":
kind = reflect.Uint8
default:
err := fmt.Errorf("ConfigFromTable: type string %q not recognized", typ)
errs = append(errs, err)
}
dt.AddColumnOfType(kind, name)
}
return errors.Join(errs...)
}
// Copyright (c) 2024, 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 tensor
//go:generate core generate
import (
"fmt"
"reflect"
"gonum.org/v1/gonum/mat"
)
// todo: add a conversion function to copy data from Column-Major to a tensor:
// It is also possible to use Column-Major order, which is used in R, Julia, and MATLAB
// where the inner-most index is first and outer-most last.
// Tensor is the interface for n-dimensional tensors.
// Per C / Go / Python conventions, indexes are Row-Major, ordered from
// outer to inner left-to-right, so the inner-most is right-most.
// It is implemented by the Base and Number generic types specialized
// by different concrete types: float64, float32, int, int32, byte,
// string, bits (bools).
// For float32 and float64 values, use NaN to indicate missing values.
// All of the data analysis and plot packages skip NaNs.
type Tensor interface {
fmt.Stringer
mat.Matrix
// Shape returns a pointer to the shape that fully parametrizes the tensor shape
Shape() *Shape
// Len returns the number of elements in the tensor (product of shape dimensions).
Len() int
// NumDims returns the total number of dimensions.
NumDims() int
// DimSize returns size of given dimension
DimSize(dim int) int
// RowCellSize returns the size of the outer-most Row shape dimension,
// and the size of all the remaining inner dimensions (the "cell" size).
// Used for Tensors that are columns in a data table.
RowCellSize() (rows, cells int)
// DataType returns the type of the data elements in the tensor.
// Bool is returned for the Bits tensor type.
DataType() reflect.Kind
// Sizeof returns the number of bytes contained in the Values of this tensor.
// for String types, this is just the string pointers.
Sizeof() int64
// Bytes returns the underlying byte representation of the tensor values.
// This is the actual underlying data, so make a copy if it can be
// unintentionally modified or retained more than for immediate use.
Bytes() []byte
// returns true if the data type is a String. otherwise is numeric.
IsString() bool
// Float returns the value of given index as a float64.
Float(i []int) float64
// SetFloat sets the value of given index as a float64
SetFloat(i []int, val float64)
// NOTE: String conflicts with [fmt.Stringer], so we have to use StringValue
// StringValue returns the value of given index as a string
StringValue(i []int) string
// SetString sets the value of given index as a string
SetString(i []int, val string)
// Float1D returns the value of given 1-dimensional index (0-Len()-1) as a float64
Float1D(i int) float64
// SetFloat1D sets the value of given 1-dimensional index (0-Len()-1) as a float64
SetFloat1D(i int, val float64)
// FloatRowCell returns the value at given row and cell, where row is outer-most dim,
// and cell is 1D index into remaining inner dims. For Table columns.
FloatRowCell(row, cell int) float64
// SetFloatRowCell sets the value at given row and cell, where row is outer-most dim,
// and cell is 1D index into remaining inner dims. For Table columns.
SetFloatRowCell(row, cell int, val float64)
// Floats sets []float64 slice of all elements in the tensor
// (length is ensured to be sufficient).
// This can be used for all of the gonum/floats methods
// for basic math, gonum/stats, etc.
Floats(flt *[]float64)
// SetFloats sets tensor values from a []float64 slice (copies values).
SetFloats(vals []float64)
// String1D returns the value of given 1-dimensional index (0-Len()-1) as a string
String1D(i int) string
// SetString1D sets the value of given 1-dimensional index (0-Len()-1) as a string
SetString1D(i int, val string)
// StringRowCell returns the value at given row and cell, where row is outer-most dim,
// and cell is 1D index into remaining inner dims. For Table columns
StringRowCell(row, cell int) string
// SetStringRowCell sets the value at given row and cell, where row is outer-most dim,
// and cell is 1D index into remaining inner dims. For Table columns
SetStringRowCell(row, cell int, val string)
// SubSpace returns a new tensor with innermost subspace at given
// offset(s) in outermost dimension(s) (len(offs) < NumDims).
// The new tensor points to the values of the this tensor (i.e., modifications
// will affect both), as its Values slice is a view onto the original (which
// is why only inner-most contiguous supsaces are supported).
// Use Clone() method to separate the two.
SubSpace(offs []int) Tensor
// Range returns the min, max (and associated indexes, -1 = no values) for the tensor.
// This is needed for display and is thus in the core api in optimized form
// Other math operations can be done using gonum/floats package.
Range() (min, max float64, minIndex, maxIndex int)
// SetZeros is simple convenience function initialize all values to 0
SetZeros()
// Clone clones this tensor, creating a duplicate copy of itself with its
// own separate memory representation of all the values, and returns
// that as a Tensor (which can be converted into the known type as needed).
Clone() Tensor
// CopyFrom copies all avail values from other tensor into this tensor, with an
// optimized implementation if the other tensor is of the same type, and
// otherwise it goes through appropriate standard type.
CopyFrom(from Tensor)
// CopyShapeFrom copies just the shape from given source tensor
// calling SetShape with the shape params from source (see for more docs).
CopyShapeFrom(from Tensor)
// CopyCellsFrom copies given range of values from other tensor into this tensor,
// using flat 1D indexes: to = starting index in this Tensor to start copying into,
// start = starting index on from Tensor to start copying from, and n = number of
// values to copy. Uses an optimized implementation if the other tensor is
// of the same type, and otherwise it goes through appropriate standard type.
CopyCellsFrom(from Tensor, to, start, n int)
// SetShape sets the sizes parameters of the tensor, and resizes backing storage appropriately.
// existing names will be preserved if not presented.
SetShape(sizes []int, names ...string)
// SetNumRows sets the number of rows (outer-most dimension).
SetNumRows(rows int)
// SetMetaData sets a key=value meta data (stored as a map[string]string).
// For TensorGrid display: top-zero=+/-, odd-row=+/-, image=+/-,
// min, max set fixed min / max values, background=color
SetMetaData(key, val string)
// MetaData retrieves value of given key, bool = false if not set
MetaData(key string) (string, bool)
// MetaDataMap returns the underlying map used for meta data
MetaDataMap() map[string]string
// CopyMetaData copies meta data from given source tensor
CopyMetaData(from Tensor)
}
// New returns a new n-dimensional tensor of given value type
// with the given sizes per dimension (shape), and optional dimension names.
func New[T string | bool | float32 | float64 | int | int32 | byte](sizes []int, names ...string) Tensor {
var v T
switch any(v).(type) {
case string:
return NewString(sizes, names...)
case bool:
return NewBits(sizes, names...)
case float64:
return NewNumber[float64](sizes, names...)
case float32:
return NewNumber[float32](sizes, names...)
case int:
return NewNumber[int](sizes, names...)
case int32:
return NewNumber[int32](sizes, names...)
case byte:
return NewNumber[byte](sizes, names...)
default:
panic("tensor.New: unexpected error: type not supported")
}
}
// NewOfType returns a new n-dimensional tensor of given reflect.Kind type
// with the given sizes per dimension (shape), and optional dimension names.
// Supported types are string, bool (for [Bits]), float32, float64, int, int32, and byte.
func NewOfType(typ reflect.Kind, sizes []int, names ...string) Tensor {
switch typ {
case reflect.String:
return NewString(sizes, names...)
case reflect.Bool:
return NewBits(sizes, names...)
case reflect.Float64:
return NewNumber[float64](sizes, names...)
case reflect.Float32:
return NewNumber[float32](sizes, names...)
case reflect.Int:
return NewNumber[int](sizes, names...)
case reflect.Int32:
return NewNumber[int32](sizes, names...)
case reflect.Uint8:
return NewNumber[byte](sizes, names...)
default:
panic(fmt.Sprintf("tensor.NewOfType: type not supported: %v", typ))
}
}
// CopyDense copies a gonum mat.Dense matrix into given Tensor
// using standard Float64 interface
func CopyDense(to Tensor, dm *mat.Dense) {
nr, nc := dm.Dims()
to.SetShape([]int{nr, nc})
idx := 0
for ri := 0; ri < nr; ri++ {
for ci := 0; ci < nc; ci++ {
v := dm.At(ri, ci)
to.SetFloat1D(idx, v)
idx++
}
}
}
// Copyright (c) 2019, 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 tensorcore
import (
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/stats/simat"
)
const LabelSpace = float32(8)
// SimMatGrid is a widget that displays a similarity / distance matrix
// with tensor values as a grid of colored squares, and labels for rows and columns.
type SimMatGrid struct { //types:add
TensorGrid
// the similarity / distance matrix
SimMat *simat.SimMat `set:"-"`
rowMaxSz math32.Vector2 // maximum label size
rowMinBlank int // minimum number of blank rows
rowNGps int // number of groups in row (non-blank after blank)
colMaxSz math32.Vector2 // maximum label size
colMinBlank int // minimum number of blank cols
colNGps int // number of groups in col (non-blank after blank)
}
// Defaults sets defaults for values that are at nonsensical initial values
func (tg *SimMatGrid) Init() {
tg.TensorGrid.Init()
tg.Display.GridView = &tg.TensorGrid
tg.Display.Defaults()
tg.Display.TopZero = true
}
// SetSimMat sets the similarity matrix and triggers a display update
func (tg *SimMatGrid) SetSimMat(smat *simat.SimMat) *SimMatGrid {
tg.SimMat = smat
tg.Tensor = smat.Mat
if tg.Tensor != nil {
tg.Display.FromMeta(tg.Tensor)
}
tg.Update()
return tg
}
func (tg *SimMatGrid) SizeLabel(lbs []string, col bool) (minBlank, ngps int, sz math32.Vector2) {
mx := 0
mxi := 0
minBlank = len(lbs)
if minBlank == 0 {
return
}
curblk := 0
ngps = 0
for i, lb := range lbs {
l := len(lb)
if l == 0 {
curblk++
} else {
if curblk > 0 {
ngps++
}
if i > 0 {
minBlank = min(minBlank, curblk)
}
curblk = 0
if l > mx {
mx = l
mxi = i
}
}
}
minBlank = min(minBlank, curblk)
tr := paint.Text{}
fr := tg.Styles.FontRender()
if col {
tr.SetStringRot90(lbs[mxi], fr, &tg.Styles.UnitContext, &tg.Styles.Text, true, 0)
} else {
tr.SetString(lbs[mxi], fr, &tg.Styles.UnitContext, &tg.Styles.Text, true, 0, 0)
}
tsz := tg.Geom.Size.Actual.Content
if !col {
tr.LayoutStdLR(&tg.Styles.Text, fr, &tg.Styles.UnitContext, tsz)
}
return minBlank, ngps, tr.BBox.Size()
}
func (tg *SimMatGrid) SizeUp() {
tg.rowMinBlank, tg.rowNGps, tg.rowMaxSz = tg.SizeLabel(tg.SimMat.Rows, false)
tg.colMinBlank, tg.colNGps, tg.colMaxSz = tg.SizeLabel(tg.SimMat.Columns, true)
tg.colMaxSz.Y += tg.rowMaxSz.Y // needs one more for some reason
rtxtsz := tg.rowMaxSz.Y / float32(tg.rowMinBlank+1)
ctxtsz := tg.colMaxSz.X / float32(tg.colMinBlank+1)
txtsz := math32.Max(rtxtsz, ctxtsz)
rows, cols, _, _ := tensor.Projection2DShape(tg.Tensor.Shape(), tg.Display.OddRow)
rowEx := tg.rowNGps
colEx := tg.colNGps
frw := float32(rows) + float32(rowEx)*tg.Display.DimExtra // extra spacing
fcl := float32(cols) + float32(colEx)*tg.Display.DimExtra // extra spacing
max := float32(math32.Max(frw, fcl))
gsz := tg.Display.TotPrefSize / max
gsz = math32.Max(gsz, tg.Display.GridMinSize)
gsz = math32.Max(gsz, txtsz)
gsz = math32.Min(gsz, tg.Display.GridMaxSize)
minsz := math32.Vec2(tg.rowMaxSz.X+LabelSpace+gsz*float32(cols), tg.colMaxSz.Y+LabelSpace+gsz*float32(rows))
sz := &tg.Geom.Size
sz.FitSizeMax(&sz.Actual.Content, minsz)
}
func (tg *SimMatGrid) Render() {
if tg.SimMat == nil || tg.SimMat.Mat.Len() == 0 {
return
}
tg.EnsureColorMap()
tg.UpdateRange()
pc := &tg.Scene.PaintContext
pos := tg.Geom.Pos.Content
sz := tg.Geom.Size.Actual.Content
effsz := sz
effsz.X -= tg.rowMaxSz.X + LabelSpace
effsz.Y -= tg.colMaxSz.Y + LabelSpace
pc.FillBox(pos, sz, tg.Styles.Background)
tsr := tg.SimMat.Mat
rows, cols, _, _ := tensor.Projection2DShape(tsr.Shape(), tg.Display.OddRow)
rowEx := tg.rowNGps
colEx := tg.colNGps
frw := float32(rows) + float32(rowEx)*tg.Display.DimExtra // extra spacing
fcl := float32(cols) + float32(colEx)*tg.Display.DimExtra // extra spacing
tsz := math32.Vec2(fcl, frw)
gsz := effsz.Div(tsz)
// Render Rows
epos := pos
epos.Y += tg.colMaxSz.Y + LabelSpace
nr := len(tg.SimMat.Rows)
mx := min(nr, rows)
tr := paint.Text{}
txsty := tg.Styles.Text
txsty.AlignV = styles.Start
ygp := 0
prvyblk := false
fr := tg.Styles.FontRender()
for y := 0; y < mx; y++ {
lb := tg.SimMat.Rows[y]
if len(lb) == 0 {
prvyblk = true
continue
}
if prvyblk {
ygp++
prvyblk = false
}
yex := float32(ygp) * tg.Display.DimExtra
tr.SetString(lb, fr, &tg.Styles.UnitContext, &txsty, true, 0, 0)
tr.LayoutStdLR(&txsty, fr, &tg.Styles.UnitContext, tg.rowMaxSz)
cr := math32.Vec2(0, float32(y)+yex)
pr := epos.Add(cr.Mul(gsz))
tr.Render(pc, pr)
}
// Render Cols
epos = pos
epos.X += tg.rowMaxSz.X + LabelSpace
nc := len(tg.SimMat.Columns)
mx = min(nc, cols)
xgp := 0
prvxblk := false
for x := 0; x < mx; x++ {
lb := tg.SimMat.Columns[x]
if len(lb) == 0 {
prvxblk = true
continue
}
if prvxblk {
xgp++
prvxblk = false
}
xex := float32(xgp) * tg.Display.DimExtra
tr.SetStringRot90(lb, fr, &tg.Styles.UnitContext, &tg.Styles.Text, true, 0)
cr := math32.Vec2(float32(x)+xex, 0)
pr := epos.Add(cr.Mul(gsz))
tr.Render(pc, pr)
}
pos.X += tg.rowMaxSz.X + LabelSpace
pos.Y += tg.colMaxSz.Y + LabelSpace
ssz := gsz.MulScalar(tg.Display.GridFill) // smaller size with margin
prvyblk = false
ygp = 0
for y := 0; y < rows; y++ {
ylb := tg.SimMat.Rows[y]
if len(ylb) > 0 && prvyblk {
ygp++
prvyblk = false
}
yex := float32(ygp) * tg.Display.DimExtra
prvxblk = false
xgp = 0
for x := 0; x < cols; x++ {
xlb := tg.SimMat.Columns[x]
if len(xlb) > 0 && prvxblk {
xgp++
prvxblk = false
}
xex := float32(xgp) * tg.Display.DimExtra
ey := y
if !tg.Display.TopZero {
ey = (rows - 1) - y
}
val := tensor.Projection2DValue(tsr, tg.Display.OddRow, ey, x)
cr := math32.Vec2(float32(x)+xex, float32(y)+yex)
pr := pos.Add(cr.Mul(gsz))
_, clr := tg.Color(val)
pc.FillBox(pr, ssz, colors.Uniform(clr))
if len(xlb) == 0 {
prvxblk = true
}
}
if len(ylb) == 0 {
prvyblk = true
}
}
}
// 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 tensorcore provides GUI Cogent Core widgets for tensor types.
package tensorcore
//go:generate core generate
import (
"bytes"
"encoding/csv"
"fmt"
"image"
"log"
"strconv"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/table"
"cogentcore.org/core/tree"
)
// Table provides a GUI widget for representing [table.Table] values.
type Table struct {
core.ListBase
// the idx view of the table that we're a view of
Table *table.IndexView `set:"-"`
// overall display options for tensor display
TensorDisplay TensorDisplay `set:"-"`
// per column tensor display params
ColumnTensorDisplay map[int]*TensorDisplay `set:"-"`
// per column blank tensor values
ColumnTensorBlank map[int]*tensor.Float64 `set:"-"`
// number of columns in table (as of last update)
NCols int `edit:"-"`
// current sort index
SortIndex int
// whether current sort order is descending
SortDescending bool
// headerWidths has number of characters in each header, per visfields
headerWidths []int `copier:"-" display:"-" json:"-" xml:"-"`
// colMaxWidths records maximum width in chars of string type fields
colMaxWidths []int `set:"-" copier:"-" json:"-" xml:"-"`
// blank values for out-of-range rows
BlankString string
BlankFloat float64
}
// check for interface impl
var _ core.Lister = (*Table)(nil)
func (tb *Table) Init() {
tb.ListBase.Init()
tb.SortIndex = -1
tb.TensorDisplay.Defaults()
tb.ColumnTensorDisplay = map[int]*TensorDisplay{}
tb.ColumnTensorBlank = map[int]*tensor.Float64{}
tb.Makers.Normal[0] = func(p *tree.Plan) { // TODO: reduce redundancy with ListBase Maker
svi := tb.This.(core.Lister)
svi.UpdateSliceSize()
scrollTo := -1
if tb.InitSelectedIndex >= 0 {
tb.SelectedIndex = tb.InitSelectedIndex
tb.InitSelectedIndex = -1
scrollTo = tb.SelectedIndex
}
if scrollTo >= 0 {
tb.ScrollToIndex(scrollTo)
}
tb.UpdateStartIndex()
tb.UpdateMaxWidths()
tb.Updater(func() {
tb.UpdateStartIndex()
})
tb.MakeHeader(p)
tb.MakeGrid(p, func(p *tree.Plan) {
for i := 0; i < tb.VisibleRows; i++ {
svi.MakeRow(p, i)
}
})
}
}
func (tb *Table) SliceIndex(i int) (si, vi int, invis bool) {
si = tb.StartIndex + i
vi = -1
if si < len(tb.Table.Indexes) {
vi = tb.Table.Indexes[si]
}
invis = vi < 0
return
}
// StyleValue performs additional value widget styling
func (tb *Table) StyleValue(w core.Widget, s *styles.Style, row, col int) {
hw := float32(tb.headerWidths[col])
if col == tb.SortIndex {
hw += 6
}
if len(tb.colMaxWidths) > col {
hw = max(float32(tb.colMaxWidths[col]), hw)
}
hv := units.Ch(hw)
s.Min.X.Value = max(s.Min.X.Value, hv.Convert(s.Min.X.Unit, &s.UnitContext).Value)
s.SetTextWrap(false)
}
// SetTable sets the source table that we are viewing, using a sequential IndexView
// and then configures the display
func (tb *Table) SetTable(et *table.Table) *Table {
if et == nil {
return nil
}
tb.Table = table.NewIndexView(et)
tb.This.(core.Lister).UpdateSliceSize()
tb.SetSliceBase()
tb.Update()
return tb
}
// SetSlice sets the source table to a [table.NewSliceTable]
// from the given slice.
func (tb *Table) SetSlice(sl any) *Table {
return tb.SetTable(errors.Log1(table.NewSliceTable(sl)))
}
// AsyncUpdateTable updates the display for asynchronous updating from
// other goroutines. Also updates indexview (calling Sequential).
func (tb *Table) AsyncUpdateTable() {
tb.AsyncLock()
tb.Table.Sequential()
tb.ScrollToIndexNoUpdate(tb.SliceSize - 1)
tb.Update()
tb.AsyncUnlock()
}
// SetIndexView sets the source IndexView of a table (using a copy so original is not modified)
// and then configures the display
func (tb *Table) SetIndexView(ix *table.IndexView) *Table {
if ix == nil {
return tb
}
tb.Table = ix.Clone() // always copy
tb.This.(core.Lister).UpdateSliceSize()
tb.StartIndex = 0
tb.VisibleRows = tb.MinRows
if !tb.IsReadOnly() {
tb.SelectedIndex = -1
}
tb.ResetSelectedIndexes()
tb.SelectMode = false
tb.MakeIter = 0
tb.Update()
return tb
}
func (tb *Table) UpdateSliceSize() int {
tb.Table.DeleteInvalid() // table could have changed
if tb.Table.Len() == 0 {
tb.Table.Sequential()
}
tb.SliceSize = tb.Table.Len()
tb.NCols = tb.Table.Table.NumColumns()
return tb.SliceSize
}
func (tb *Table) UpdateMaxWidths() {
if len(tb.headerWidths) != tb.NCols {
tb.headerWidths = make([]int, tb.NCols)
tb.colMaxWidths = make([]int, tb.NCols)
}
if tb.SliceSize == 0 {
return
}
for fli := 0; fli < tb.NCols; fli++ {
tb.colMaxWidths[fli] = 0
col := tb.Table.Table.Columns[fli]
stsr, isstr := col.(*tensor.String)
if !isstr {
continue
}
mxw := 0
for _, ixi := range tb.Table.Indexes {
if ixi >= 0 {
sval := stsr.Values[ixi]
mxw = max(mxw, len(sval))
}
}
tb.colMaxWidths[fli] = mxw
}
}
func (tb *Table) MakeHeader(p *tree.Plan) {
tree.AddAt(p, "header", func(w *core.Frame) {
core.ToolbarStyles(w)
w.FinalStyler(func(s *styles.Style) {
s.Padding.Zero()
s.Grow.Set(0, 0)
s.Gap.Set(units.Em(0.5)) // matches grid default
})
w.Maker(func(p *tree.Plan) {
if tb.ShowIndexes {
tree.AddAt(p, "_head-index", func(w *core.Text) { // TODO: is not working
w.SetType(core.TextBodyMedium)
w.Styler(func(s *styles.Style) {
s.Align.Self = styles.Center
})
w.SetText("Index")
})
}
for fli := 0; fli < tb.NCols; fli++ {
field := tb.Table.Table.ColumnNames[fli]
tree.AddAt(p, "head-"+field, func(w *core.Button) {
w.SetType(core.ButtonAction)
w.Styler(func(s *styles.Style) {
s.Justify.Content = styles.Start
})
w.OnClick(func(e events.Event) {
tb.SortSliceAction(fli)
})
w.Updater(func() {
field := tb.Table.Table.ColumnNames[fli]
w.SetText(field).SetTooltip(field + " (tap to sort by)")
tb.headerWidths[fli] = len(field)
if fli == tb.SortIndex {
if tb.SortDescending {
w.SetIndicator(icons.KeyboardArrowDown)
} else {
w.SetIndicator(icons.KeyboardArrowUp)
}
} else {
w.SetIndicator(icons.Blank)
}
})
})
}
})
})
}
// SliceHeader returns the Frame header for slice grid
func (tb *Table) SliceHeader() *core.Frame {
return tb.Child(0).(*core.Frame)
}
// RowWidgetNs returns number of widgets per row and offset for index label
func (tb *Table) RowWidgetNs() (nWidgPerRow, idxOff int) {
nWidgPerRow = 1 + tb.NCols
idxOff = 1
if !tb.ShowIndexes {
nWidgPerRow -= 1
idxOff = 0
}
return
}
func (tb *Table) MakeRow(p *tree.Plan, i int) {
svi := tb.This.(core.Lister)
si, _, invis := svi.SliceIndex(i)
itxt := strconv.Itoa(i)
if tb.ShowIndexes {
tb.MakeGridIndex(p, i, si, itxt, invis)
}
for fli := 0; fli < tb.NCols; fli++ {
col := tb.Table.Table.Columns[fli]
valnm := fmt.Sprintf("value-%v.%v", fli, itxt)
_, isstr := col.(*tensor.String)
if col.NumDims() == 1 {
str := ""
fval := float64(0)
tree.AddNew(p, valnm, func() core.Value {
if isstr {
return core.NewValue(&str, "")
} else {
return core.NewValue(&fval, "")
}
}, func(w core.Value) {
wb := w.AsWidget()
tb.MakeValue(w, i)
w.AsTree().SetProperty(core.ListColProperty, fli)
if !tb.IsReadOnly() {
wb.OnChange(func(e events.Event) {
if si < len(tb.Table.Indexes) {
if isstr {
tb.Table.Table.SetStringIndex(fli, tb.Table.Indexes[si], str)
} else {
tb.Table.Table.SetFloatIndex(fli, tb.Table.Indexes[si], fval)
}
}
tb.This.(core.Lister).UpdateMaxWidths()
tb.SendChange()
})
}
wb.Updater(func() {
_, vi, invis := svi.SliceIndex(i)
if !invis {
if isstr {
str = tb.Table.Table.StringIndex(fli, vi)
core.Bind(&str, w)
} else {
fval = tb.Table.Table.FloatIndex(fli, vi)
core.Bind(&fval, w)
}
} else {
if isstr {
core.Bind(tb.BlankString, w)
} else {
core.Bind(tb.BlankFloat, w)
}
}
wb.SetReadOnly(tb.IsReadOnly())
wb.SetState(invis, states.Invisible)
if svi.HasStyler() {
w.Style()
}
if invis {
wb.SetSelected(false)
}
})
})
} else {
tree.AddAt(p, valnm, func(w *TensorGrid) {
w.SetReadOnly(tb.IsReadOnly())
wb := w.AsWidget()
w.SetProperty(core.ListRowProperty, i)
w.SetProperty(core.ListColProperty, fli)
w.Styler(func(s *styles.Style) {
s.Grow.Set(0, 0)
})
wb.Updater(func() {
si, vi, invis := svi.SliceIndex(i)
var cell tensor.Tensor
if invis {
cell = tb.ColTensorBlank(fli, col)
} else {
cell = tb.Table.Table.TensorIndex(fli, vi)
}
wb.ValueTitle = tb.ValueTitle + "[" + strconv.Itoa(si) + "]"
w.SetState(invis, states.Invisible)
w.SetTensor(cell)
w.Display = *tb.GetColumnTensorDisplay(fli)
})
})
}
}
}
// ColTensorBlank returns tensor blanks for given tensor col
func (tb *Table) ColTensorBlank(cidx int, col tensor.Tensor) *tensor.Float64 {
if ctb, has := tb.ColumnTensorBlank[cidx]; has {
return ctb
}
ctb := tensor.New[float64](col.Shape().Sizes, col.Shape().Names...).(*tensor.Float64)
tb.ColumnTensorBlank[cidx] = ctb
return ctb
}
// GetColumnTensorDisplay returns tensor display parameters for this column
// either the overall defaults or the per-column if set
func (tb *Table) GetColumnTensorDisplay(col int) *TensorDisplay {
if ctd, has := tb.ColumnTensorDisplay[col]; has {
return ctd
}
if tb.Table != nil {
cl := tb.Table.Table.Columns[col]
if len(cl.MetaDataMap()) > 0 {
return tb.SetColumnTensorDisplay(col)
}
}
return &tb.TensorDisplay
}
// SetColumnTensorDisplay sets per-column tensor display params and returns them
// if already set, just returns them
func (tb *Table) SetColumnTensorDisplay(col int) *TensorDisplay {
if ctd, has := tb.ColumnTensorDisplay[col]; has {
return ctd
}
ctd := &TensorDisplay{}
*ctd = tb.TensorDisplay
if tb.Table != nil {
cl := tb.Table.Table.Columns[col]
ctd.FromMeta(cl)
}
tb.ColumnTensorDisplay[col] = ctd
return ctd
}
// NewAt inserts a new blank element at given index in the slice -- -1
// means the end
func (tb *Table) NewAt(idx int) {
tb.NewAtSelect(idx)
tb.Table.InsertRows(idx, 1)
tb.SelectIndexEvent(idx, events.SelectOne)
tb.Update()
tb.IndexGrabFocus(idx)
}
// DeleteAt deletes element at given index from slice
func (tb *Table) DeleteAt(idx int) {
if idx < 0 || idx >= tb.SliceSize {
return
}
tb.DeleteAtSelect(idx)
tb.Table.DeleteRows(idx, 1)
tb.Update()
}
// SortSliceAction sorts the slice for given field index -- toggles ascending
// vs. descending if already sorting on this dimension
func (tb *Table) SortSliceAction(fldIndex int) {
sgh := tb.SliceHeader()
_, idxOff := tb.RowWidgetNs()
for fli := 0; fli < tb.NCols; fli++ {
hdr := sgh.Child(idxOff + fli).(*core.Button)
hdr.SetType(core.ButtonAction)
if fli == fldIndex {
if tb.SortIndex == fli {
tb.SortDescending = !tb.SortDescending
} else {
tb.SortDescending = false
}
}
}
tb.SortIndex = fldIndex
if fldIndex == -1 {
tb.Table.SortIndexes()
} else {
tb.Table.SortColumn(tb.SortIndex, !tb.SortDescending)
}
tb.Update() // requires full update due to sort button icon
}
// TensorDisplayAction allows user to select tensor display options for column
// pass -1 for global params for the entire table
func (tb *Table) TensorDisplayAction(fldIndex int) {
ctd := &tb.TensorDisplay
if fldIndex >= 0 {
ctd = tb.SetColumnTensorDisplay(fldIndex)
}
d := core.NewBody("Tensor grid display options")
core.NewForm(d).SetStruct(ctd)
d.RunFullDialog(tb)
// tv.UpdateSliceGrid()
tb.NeedsRender()
}
func (tb *Table) HasStyler() bool { return false }
func (tb *Table) StyleRow(w core.Widget, idx, fidx int) {}
// SortFieldName returns the name of the field being sorted, along with :up or
// :down depending on descending
func (tb *Table) SortFieldName() string {
if tb.SortIndex >= 0 && tb.SortIndex < tb.NCols {
nm := tb.Table.Table.ColumnNames[tb.SortIndex]
if tb.SortDescending {
nm += ":down"
} else {
nm += ":up"
}
return nm
}
return ""
}
// SetSortField sets sorting to happen on given field and direction -- see
// SortFieldName for details
func (tb *Table) SetSortFieldName(nm string) {
if nm == "" {
return
}
spnm := strings.Split(nm, ":")
got := false
for fli := 0; fli < tb.NCols; fli++ {
fld := tb.Table.Table.ColumnNames[fli]
if fld == spnm[0] {
got = true
// fmt.Println("sorting on:", fld.Name, fli, "from:", nm)
tb.SortIndex = fli
}
}
if len(spnm) == 2 {
if spnm[1] == "down" {
tb.SortDescending = true
} else {
tb.SortDescending = false
}
}
_ = got
// if got {
// tv.SortSlice()
// }
}
// RowFirstVisWidget returns the first visible widget for given row (could be
// index or not) -- false if out of range
func (tb *Table) RowFirstVisWidget(row int) (*core.WidgetBase, bool) {
if !tb.IsRowInBounds(row) {
return nil, false
}
nWidgPerRow, idxOff := tb.RowWidgetNs()
lg := tb.ListGrid
w := lg.Children[row*nWidgPerRow].(core.Widget).AsWidget()
if w.Geom.TotalBBox != (image.Rectangle{}) {
return w, true
}
ridx := nWidgPerRow * row
for fli := 0; fli < tb.NCols; fli++ {
w := lg.Child(ridx + idxOff + fli).(core.Widget).AsWidget()
if w.Geom.TotalBBox != (image.Rectangle{}) {
return w, true
}
}
return nil, false
}
// RowGrabFocus grabs the focus for the first focusable widget in given row --
// returns that element or nil if not successful -- note: grid must have
// already rendered for focus to be grabbed!
func (tb *Table) RowGrabFocus(row int) *core.WidgetBase {
if !tb.IsRowInBounds(row) || tb.InFocusGrab { // range check
return nil
}
nWidgPerRow, idxOff := tb.RowWidgetNs()
ridx := nWidgPerRow * row
lg := tb.ListGrid
// first check if we already have focus
for fli := 0; fli < tb.NCols; fli++ {
w := lg.Child(ridx + idxOff + fli).(core.Widget).AsWidget()
if w.StateIs(states.Focused) || w.ContainsFocus() {
return w
}
}
tb.InFocusGrab = true
defer func() { tb.InFocusGrab = false }()
for fli := 0; fli < tb.NCols; fli++ {
w := lg.Child(ridx + idxOff + fli).(core.Widget).AsWidget()
if w.CanFocus() {
w.SetFocus()
return w
}
}
return nil
}
//////////////////////////////////////////////////////
// Header layout
func (tb *Table) SizeFinal() {
tb.ListBase.SizeFinal()
lg := tb.ListGrid
sh := tb.SliceHeader()
sh.ForWidgetChildren(func(i int, cw core.Widget, cwb *core.WidgetBase) bool {
sgb := core.AsWidget(lg.Child(i))
gsz := &sgb.Geom.Size
if gsz.Actual.Total.X == 0 {
return tree.Continue
}
ksz := &cwb.Geom.Size
ksz.Actual.Total.X = gsz.Actual.Total.X
ksz.Actual.Content.X = gsz.Actual.Content.X
ksz.Alloc.Total.X = gsz.Alloc.Total.X
ksz.Alloc.Content.X = gsz.Alloc.Content.X
return tree.Continue
})
gsz := &lg.Geom.Size
ksz := &sh.Geom.Size
if gsz.Actual.Total.X > 0 {
ksz.Actual.Total.X = gsz.Actual.Total.X
ksz.Actual.Content.X = gsz.Actual.Content.X
ksz.Alloc.Total.X = gsz.Alloc.Total.X
ksz.Alloc.Content.X = gsz.Alloc.Content.X
}
}
// SelectedColumnStrings returns the string values of given column name.
func (tb *Table) SelectedColumnStrings(colName string) []string {
dt := tb.Table.Table
jis := tb.SelectedIndexesList(false)
if len(jis) == 0 || dt == nil {
return nil
}
var s []string
for _, i := range jis {
v := dt.StringValue(colName, i)
s = append(s, v)
}
return s
}
//////////////////////////////////////////////////////////////////////////////
// Copy / Cut / Paste
func (tb *Table) MakeToolbar(p *tree.Plan) {
if tb.Table == nil || tb.Table.Table == nil {
return
}
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(tb.Table.AddRows).SetIcon(icons.Add)
w.SetAfterFunc(func() { tb.Update() })
})
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(tb.Table.SortColumnName).SetText("Sort").SetIcon(icons.Sort)
w.SetAfterFunc(func() { tb.Update() })
})
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(tb.Table.FilterColumnName).SetText("Filter").SetIcon(icons.FilterAlt)
w.SetAfterFunc(func() { tb.Update() })
})
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(tb.Table.Sequential).SetText("Unfilter").SetIcon(icons.FilterAltOff)
w.SetAfterFunc(func() { tb.Update() })
})
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(tb.Table.OpenCSV).SetIcon(icons.Open)
w.SetAfterFunc(func() { tb.Update() })
})
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(tb.Table.SaveCSV).SetIcon(icons.Save)
w.SetAfterFunc(func() { tb.Update() })
})
}
func (tb *Table) MimeDataType() string {
return fileinfo.DataCsv
}
// CopySelectToMime copies selected rows to mime data
func (tb *Table) CopySelectToMime() mimedata.Mimes {
nitms := len(tb.SelectedIndexes)
if nitms == 0 {
return nil
}
ix := &table.IndexView{}
ix.Table = tb.Table.Table
idx := tb.SelectedIndexesList(false) // ascending
iidx := make([]int, len(idx))
for i, di := range idx {
iidx[i] = tb.Table.Indexes[di]
}
ix.Indexes = iidx
var b bytes.Buffer
ix.WriteCSV(&b, table.Tab, table.Headers)
md := mimedata.NewTextBytes(b.Bytes())
md[0].Type = fileinfo.DataCsv
return md
}
// FromMimeData returns records from csv of mime data
func (tb *Table) FromMimeData(md mimedata.Mimes) [][]string {
var recs [][]string
for _, d := range md {
if d.Type == fileinfo.DataCsv {
b := bytes.NewBuffer(d.Data)
cr := csv.NewReader(b)
cr.Comma = table.Tab.Rune()
rec, err := cr.ReadAll()
if err != nil || len(rec) == 0 {
log.Printf("Error reading CSV from clipboard: %s\n", err)
return nil
}
recs = append(recs, rec...)
}
}
return recs
}
// PasteAssign assigns mime data (only the first one!) to this idx
func (tb *Table) PasteAssign(md mimedata.Mimes, idx int) {
recs := tb.FromMimeData(md)
if len(recs) == 0 {
return
}
tb.Table.Table.ReadCSVRow(recs[1], tb.Table.Indexes[idx])
tb.UpdateChange()
}
// PasteAtIndex inserts object(s) from mime data at (before) given slice index
// adds to end of table
func (tb *Table) PasteAtIndex(md mimedata.Mimes, idx int) {
recs := tb.FromMimeData(md)
nr := len(recs) - 1
if nr <= 0 {
return
}
tb.Table.InsertRows(idx, nr)
for ri := 0; ri < nr; ri++ {
rec := recs[1+ri]
rw := tb.Table.Indexes[idx+ri]
tb.Table.Table.ReadCSVRow(rec, rw)
}
tb.SendChange()
tb.SelectIndexEvent(idx, events.SelectOne)
tb.Update()
}
// 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 tensorcore provides GUI Cogent Core widgets for tensor types.
package tensorcore
import (
"fmt"
"image"
"strconv"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/table"
"cogentcore.org/core/tree"
)
// TensorEditor provides a GUI widget for representing [tensor.Tensor] values.
type TensorEditor struct {
core.ListBase
// the tensor that we're a view of
Tensor tensor.Tensor `set:"-"`
// overall layout options for tensor display
Layout TensorLayout `set:"-"`
// number of columns in table (as of last update)
NCols int `edit:"-"`
// headerWidths has number of characters in each header, per visfields
headerWidths []int `copier:"-" display:"-" json:"-" xml:"-"`
// colMaxWidths records maximum width in chars of string type fields
colMaxWidths []int `set:"-" copier:"-" json:"-" xml:"-"`
// blank values for out-of-range rows
BlankString string
BlankFloat float64
}
// check for interface impl
var _ core.Lister = (*TensorEditor)(nil)
func (tb *TensorEditor) Init() {
tb.ListBase.Init()
tb.Makers.Normal[0] = func(p *tree.Plan) { // TODO: reduce redundancy with ListBase Maker
svi := tb.This.(core.Lister)
svi.UpdateSliceSize()
scrollTo := -1
if tb.InitSelectedIndex >= 0 {
tb.SelectedIndex = tb.InitSelectedIndex
tb.InitSelectedIndex = -1
scrollTo = tb.SelectedIndex
}
if scrollTo >= 0 {
tb.ScrollToIndex(scrollTo)
}
tb.UpdateStartIndex()
tb.UpdateMaxWidths()
tb.Updater(func() {
tb.UpdateStartIndex()
})
tb.MakeHeader(p)
tb.MakeGrid(p, func(p *tree.Plan) {
for i := 0; i < tb.VisibleRows; i++ {
svi.MakeRow(p, i)
}
})
}
}
func (tb *TensorEditor) SliceIndex(i int) (si, vi int, invis bool) {
si = tb.StartIndex + i
vi = si
invis = si >= tb.SliceSize
if !tb.Layout.TopZero {
vi = (tb.SliceSize - 1) - si
}
return
}
// StyleValue performs additional value widget styling
func (tb *TensorEditor) StyleValue(w core.Widget, s *styles.Style, row, col int) {
hw := float32(tb.headerWidths[col])
if len(tb.colMaxWidths) > col {
hw = max(float32(tb.colMaxWidths[col]), hw)
}
hv := units.Ch(hw)
s.Min.X.Value = max(s.Min.X.Value, hv.Convert(s.Min.X.Unit, &s.UnitContext).Value)
s.SetTextWrap(false)
}
// SetTensor sets the source tensor that we are viewing,
// and then configures the display.
func (tb *TensorEditor) SetTensor(et tensor.Tensor) *TensorEditor {
if et == nil {
return nil
}
tb.Tensor = et
tb.This.(core.Lister).UpdateSliceSize()
tb.SetSliceBase()
tb.Update()
return tb
}
func (tb *TensorEditor) UpdateSliceSize() int {
tb.SliceSize, tb.NCols, _, _ = tensor.Projection2DShape(tb.Tensor.Shape(), tb.Layout.OddRow)
return tb.SliceSize
}
func (tb *TensorEditor) UpdateMaxWidths() {
if len(tb.headerWidths) != tb.NCols {
tb.headerWidths = make([]int, tb.NCols)
tb.colMaxWidths = make([]int, tb.NCols)
}
if tb.SliceSize == 0 {
return
}
_, isstr := tb.Tensor.(*tensor.String)
for fli := 0; fli < tb.NCols; fli++ {
tb.colMaxWidths[fli] = 0
if !isstr {
continue
}
mxw := 0
// for _, ixi := range tb.Tensor.Indexes {
// if ixi >= 0 {
// sval := stsr.Values[ixi]
// mxw = max(mxw, len(sval))
// }
// }
tb.colMaxWidths[fli] = mxw
}
}
func (tb *TensorEditor) MakeHeader(p *tree.Plan) {
tree.AddAt(p, "header", func(w *core.Frame) {
core.ToolbarStyles(w)
w.FinalStyler(func(s *styles.Style) {
s.Padding.Zero()
s.Grow.Set(0, 0)
s.Gap.Set(units.Em(0.5)) // matches grid default
})
w.Maker(func(p *tree.Plan) {
if tb.ShowIndexes {
tree.AddAt(p, "_head-index", func(w *core.Text) { // TODO: is not working
w.SetType(core.TextBodyMedium)
w.Styler(func(s *styles.Style) {
s.Align.Self = styles.Center
})
w.SetText("Index")
})
}
for fli := 0; fli < tb.NCols; fli++ {
hdr := tb.ColumnHeader(fli)
tree.AddAt(p, "head-"+hdr, func(w *core.Button) {
w.SetType(core.ButtonAction)
w.Styler(func(s *styles.Style) {
s.Justify.Content = styles.Start
})
w.Updater(func() {
hdr := tb.ColumnHeader(fli)
w.SetText(hdr).SetTooltip(hdr)
tb.headerWidths[fli] = len(hdr)
})
})
}
})
})
}
func (tb *TensorEditor) ColumnHeader(col int) string {
_, cc := tensor.Projection2DCoords(tb.Tensor.Shape(), tb.Layout.OddRow, 0, col)
sitxt := ""
for i, ccc := range cc {
sitxt += fmt.Sprintf("%03d", ccc)
if i < len(cc)-1 {
sitxt += ","
}
}
return sitxt
}
// SliceHeader returns the Frame header for slice grid
func (tb *TensorEditor) SliceHeader() *core.Frame {
return tb.Child(0).(*core.Frame)
}
// RowWidgetNs returns number of widgets per row and offset for index label
func (tb *TensorEditor) RowWidgetNs() (nWidgPerRow, idxOff int) {
nWidgPerRow = 1 + tb.NCols
idxOff = 1
if !tb.ShowIndexes {
nWidgPerRow -= 1
idxOff = 0
}
return
}
func (tb *TensorEditor) MakeRow(p *tree.Plan, i int) {
svi := tb.This.(core.Lister)
si, _, invis := svi.SliceIndex(i)
itxt := strconv.Itoa(i)
if tb.ShowIndexes {
tb.MakeGridIndex(p, i, si, itxt, invis)
}
_, isstr := tb.Tensor.(*tensor.String)
for fli := 0; fli < tb.NCols; fli++ {
valnm := fmt.Sprintf("value-%v.%v", fli, itxt)
fval := float64(0)
str := ""
tree.AddNew(p, valnm, func() core.Value {
if isstr {
return core.NewValue(&str, "")
} else {
return core.NewValue(&fval, "")
}
}, func(w core.Value) {
wb := w.AsWidget()
tb.MakeValue(w, i)
w.AsTree().SetProperty(core.ListColProperty, fli)
if !tb.IsReadOnly() {
wb.OnChange(func(e events.Event) {
_, vi, invis := svi.SliceIndex(i)
if !invis {
if isstr {
tensor.Projection2DSetString(tb.Tensor, tb.Layout.OddRow, vi, fli, str)
} else {
tensor.Projection2DSet(tb.Tensor, tb.Layout.OddRow, vi, fli, fval)
}
}
tb.This.(core.Lister).UpdateMaxWidths()
tb.SendChange()
})
}
wb.Updater(func() {
_, vi, invis := svi.SliceIndex(i)
if !invis {
if isstr {
str = tensor.Projection2DString(tb.Tensor, tb.Layout.OddRow, vi, fli)
core.Bind(&str, w)
} else {
fval = tensor.Projection2DValue(tb.Tensor, tb.Layout.OddRow, vi, fli)
core.Bind(&fval, w)
}
} else {
if isstr {
core.Bind(tb.BlankString, w)
} else {
core.Bind(tb.BlankFloat, w)
}
}
wb.SetReadOnly(tb.IsReadOnly())
wb.SetState(invis, states.Invisible)
if svi.HasStyler() {
w.Style()
}
if invis {
wb.SetSelected(false)
}
})
})
}
}
func (tb *TensorEditor) HasStyler() bool { return false }
func (tb *TensorEditor) StyleRow(w core.Widget, idx, fidx int) {}
// RowFirstVisWidget returns the first visible widget for given row (could be
// index or not) -- false if out of range
func (tb *TensorEditor) RowFirstVisWidget(row int) (*core.WidgetBase, bool) {
if !tb.IsRowInBounds(row) {
return nil, false
}
nWidgPerRow, idxOff := tb.RowWidgetNs()
lg := tb.ListGrid
w := lg.Children[row*nWidgPerRow].(core.Widget).AsWidget()
if w.Geom.TotalBBox != (image.Rectangle{}) {
return w, true
}
ridx := nWidgPerRow * row
for fli := 0; fli < tb.NCols; fli++ {
w := lg.Child(ridx + idxOff + fli).(core.Widget).AsWidget()
if w.Geom.TotalBBox != (image.Rectangle{}) {
return w, true
}
}
return nil, false
}
// RowGrabFocus grabs the focus for the first focusable widget in given row --
// returns that element or nil if not successful -- note: grid must have
// already rendered for focus to be grabbed!
func (tb *TensorEditor) RowGrabFocus(row int) *core.WidgetBase {
if !tb.IsRowInBounds(row) || tb.InFocusGrab { // range check
return nil
}
nWidgPerRow, idxOff := tb.RowWidgetNs()
ridx := nWidgPerRow * row
lg := tb.ListGrid
// first check if we already have focus
for fli := 0; fli < tb.NCols; fli++ {
w := lg.Child(ridx + idxOff + fli).(core.Widget).AsWidget()
if w.StateIs(states.Focused) || w.ContainsFocus() {
return w
}
}
tb.InFocusGrab = true
defer func() { tb.InFocusGrab = false }()
for fli := 0; fli < tb.NCols; fli++ {
w := lg.Child(ridx + idxOff + fli).(core.Widget).AsWidget()
if w.CanFocus() {
w.SetFocus()
return w
}
}
return nil
}
//////////////////////////////////////////////////////
// Header layout
func (tb *TensorEditor) SizeFinal() {
tb.ListBase.SizeFinal()
lg := tb.ListGrid
sh := tb.SliceHeader()
sh.ForWidgetChildren(func(i int, cw core.Widget, cwb *core.WidgetBase) bool {
sgb := core.AsWidget(lg.Child(i))
gsz := &sgb.Geom.Size
if gsz.Actual.Total.X == 0 {
return tree.Continue
}
ksz := &cwb.Geom.Size
ksz.Actual.Total.X = gsz.Actual.Total.X
ksz.Actual.Content.X = gsz.Actual.Content.X
ksz.Alloc.Total.X = gsz.Alloc.Total.X
ksz.Alloc.Content.X = gsz.Alloc.Content.X
return tree.Continue
})
gsz := &lg.Geom.Size
ksz := &sh.Geom.Size
if gsz.Actual.Total.X > 0 {
ksz.Actual.Total.X = gsz.Actual.Total.X
ksz.Actual.Content.X = gsz.Actual.Content.X
ksz.Alloc.Total.X = gsz.Alloc.Total.X
ksz.Alloc.Content.X = gsz.Alloc.Content.X
}
}
//////////////////////////////////////////////////////////////////////////////
// Copy / Cut / Paste
// SaveTSV writes a tensor to a tab-separated-values (TSV) file.
// Outer-most dims are rows in the file, and inner-most is column --
// Reading just grabs all values and doesn't care about shape.
func (tb *TensorEditor) SaveCSV(filename core.Filename) error { //types:add
return tensor.SaveCSV(tb.Tensor, filename, table.Tab.Rune())
}
// OpenTSV reads a tensor from a tab-separated-values (TSV) file.
// using the Go standard encoding/csv reader conforming
// to the official CSV standard.
// Reads all values and assigns as many as fit.
func (tb *TensorEditor) OpenCSV(filename core.Filename) error { //types:add
return tensor.OpenCSV(tb.Tensor, filename, table.Tab.Rune())
}
func (tb *TensorEditor) MakeToolbar(p *tree.Plan) {
if tb.Tensor == nil {
return
}
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(tb.OpenCSV).SetIcon(icons.Open)
w.SetAfterFunc(func() { tb.Update() })
})
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(tb.SaveCSV).SetIcon(icons.Save)
w.SetAfterFunc(func() { tb.Update() })
})
}
func (tb *TensorEditor) MimeDataType() string {
return fileinfo.DataCsv
}
// CopySelectToMime copies selected rows to mime data
func (tb *TensorEditor) CopySelectToMime() mimedata.Mimes {
nitms := len(tb.SelectedIndexes)
if nitms == 0 {
return nil
}
// idx := tb.SelectedIndexesList(false) // ascending
// var b bytes.Buffer
// ix.WriteCSV(&b, table.Tab, table.Headers)
// md := mimedata.NewTextBytes(b.Bytes())
// md[0].Type = fileinfo.DataCsv
// return md
return nil
}
// FromMimeData returns records from csv of mime data
func (tb *TensorEditor) FromMimeData(md mimedata.Mimes) [][]string {
var recs [][]string
for _, d := range md {
if d.Type == fileinfo.DataCsv {
// b := bytes.NewBuffer(d.Data)
// cr := csv.NewReader(b)
// cr.Comma = table.Tab.Rune()
// rec, err := cr.ReadAll()
// if err != nil || len(rec) == 0 {
// log.Printf("Error reading CSV from clipboard: %s\n", err)
// return nil
// }
// recs = append(recs, rec...)
}
}
return recs
}
// PasteAssign assigns mime data (only the first one!) to this idx
func (tb *TensorEditor) PasteAssign(md mimedata.Mimes, idx int) {
// recs := tb.FromMimeData(md)
// if len(recs) == 0 {
// return
// }
// tb.Tensor.ReadCSVRow(recs[1], tb.Tensor.Indexes[idx])
// tb.UpdateChange()
}
// PasteAtIndex inserts object(s) from mime data at (before) given slice index
// adds to end of table
func (tb *TensorEditor) PasteAtIndex(md mimedata.Mimes, idx int) {
// recs := tb.FromMimeData(md)
// nr := len(recs) - 1
// if nr <= 0 {
// return
// }
// tb.Tensor.InsertRows(idx, nr)
// for ri := 0; ri < nr; ri++ {
// rec := recs[1+ri]
// rw := tb.Tensor.Indexes[idx+ri]
// tb.Tensor.ReadCSVRow(rec, rw)
// }
// tb.SendChange()
// tb.SelectIndexEvent(idx, events.SelectOne)
// tb.Update()
}
// Copyright (c) 2019, 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 tensorcore
import (
"image/color"
"log"
"strconv"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/colormap"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/math32"
"cogentcore.org/core/math32/minmax"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tensor"
)
// TensorLayout are layout options for displaying tensors
type TensorLayout struct { //types:add
// even-numbered dimensions are displayed as Y*X rectangles.
// This determines along which dimension to display any remaining
// odd dimension: OddRow = true = organize vertically along row
// dimension, false = organize horizontally across column dimension.
OddRow bool
// if true, then the Y=0 coordinate is displayed from the top-down;
// otherwise the Y=0 coordinate is displayed from the bottom up,
// which is typical for emergent network patterns.
TopZero bool
// display the data as a bitmap image. if a 2D tensor, then it will
// be a greyscale image. if a 3D tensor with size of either the first
// or last dim = either 3 or 4, then it is a RGB(A) color image.
Image bool
}
// TensorDisplay are options for displaying tensors
type TensorDisplay struct { //types:add
TensorLayout
// range to plot
Range minmax.Range64 `display:"inline"`
// if not using fixed range, this is the actual range of data
MinMax minmax.F64 `display:"inline"`
// the name of the color map to use in translating values to colors
ColorMap core.ColorMapName
// what proportion of grid square should be filled by color block -- 1 = all, .5 = half, etc
GridFill float32 `min:"0.1" max:"1" step:"0.1" default:"0.9,1"`
// amount of extra space to add at dimension boundaries, as a proportion of total grid size
DimExtra float32 `min:"0" max:"1" step:"0.02" default:"0.1,0.3"`
// minimum size for grid squares -- they will never be smaller than this
GridMinSize float32
// maximum size for grid squares -- they will never be larger than this
GridMaxSize float32
// total preferred display size along largest dimension.
// grid squares will be sized to fit within this size,
// subject to harder GridMin / Max size constraints
TotPrefSize float32
// font size in standard point units for labels (e.g., SimMat)
FontSize float32
// our gridview, for update method
GridView *TensorGrid `copier:"-" json:"-" xml:"-" display:"-"`
}
// Defaults sets defaults for values that are at nonsensical initial values
func (td *TensorDisplay) Defaults() {
if td.ColorMap == "" {
td.ColorMap = "ColdHot"
}
if td.Range.Max == 0 && td.Range.Min == 0 {
td.Range.SetMin(-1)
td.Range.SetMax(1)
}
if td.GridMinSize == 0 {
td.GridMinSize = 2
}
if td.GridMaxSize == 0 {
td.GridMaxSize = 16
}
if td.TotPrefSize == 0 {
td.TotPrefSize = 100
}
if td.GridFill == 0 {
td.GridFill = 0.9
td.DimExtra = 0.3
}
if td.FontSize == 0 {
td.FontSize = 24
}
}
// FromMeta sets display options from Tensor meta-data
func (td *TensorDisplay) FromMeta(tsr tensor.Tensor) {
if op, has := tsr.MetaData("top-zero"); has {
if op == "+" || op == "true" {
td.TopZero = true
}
}
if op, has := tsr.MetaData("odd-row"); has {
if op == "+" || op == "true" {
td.OddRow = true
}
}
if op, has := tsr.MetaData("image"); has {
if op == "+" || op == "true" {
td.Image = true
}
}
if op, has := tsr.MetaData("min"); has {
mv, _ := strconv.ParseFloat(op, 64)
td.Range.Min = mv
}
if op, has := tsr.MetaData("max"); has {
mv, _ := strconv.ParseFloat(op, 64)
td.Range.Max = mv
}
if op, has := tsr.MetaData("fix-min"); has {
if op == "+" || op == "true" {
td.Range.FixMin = true
} else {
td.Range.FixMin = false
}
}
if op, has := tsr.MetaData("fix-max"); has {
if op == "+" || op == "true" {
td.Range.FixMax = true
} else {
td.Range.FixMax = false
}
}
if op, has := tsr.MetaData("colormap"); has {
td.ColorMap = core.ColorMapName(op)
}
if op, has := tsr.MetaData("grid-fill"); has {
mv, _ := strconv.ParseFloat(op, 32)
td.GridFill = float32(mv)
}
if op, has := tsr.MetaData("grid-min"); has {
mv, _ := strconv.ParseFloat(op, 32)
td.GridMinSize = float32(mv)
}
if op, has := tsr.MetaData("grid-max"); has {
mv, _ := strconv.ParseFloat(op, 32)
td.GridMaxSize = float32(mv)
}
if op, has := tsr.MetaData("dim-extra"); has {
mv, _ := strconv.ParseFloat(op, 32)
td.DimExtra = float32(mv)
}
if op, has := tsr.MetaData("font-size"); has {
mv, _ := strconv.ParseFloat(op, 32)
td.FontSize = float32(mv)
}
}
////////////////////////////////////////////////////////////////////////////
// TensorGrid
// TensorGrid is a widget that displays tensor values as a grid of colored squares.
type TensorGrid struct {
core.WidgetBase
// the tensor that we view
Tensor tensor.Tensor `set:"-"`
// display options
Display TensorDisplay
// the actual colormap
ColorMap *colormap.Map
}
func (tg *TensorGrid) WidgetValue() any { return &tg.Tensor }
func (tg *TensorGrid) SetWidgetValue(value any) error {
tg.SetTensor(value.(tensor.Tensor))
return nil
}
func (tg *TensorGrid) Init() {
tg.WidgetBase.Init()
tg.Display.GridView = tg
tg.Display.Defaults()
tg.Styler(func(s *styles.Style) {
s.SetAbilities(true, abilities.DoubleClickable)
ms := tg.MinSize()
s.Min.Set(units.Dot(ms.X), units.Dot(ms.Y))
s.Grow.Set(1, 1)
})
tg.OnDoubleClick(func(e events.Event) {
tg.OpenTensorEditor()
})
tg.AddContextMenu(func(m *core.Scene) {
core.NewFuncButton(m).SetFunc(tg.OpenTensorEditor).SetIcon(icons.Edit)
core.NewFuncButton(m).SetFunc(tg.EditSettings).SetIcon(icons.Edit)
})
}
// SetTensor sets the tensor. Must call Update after this.
func (tg *TensorGrid) SetTensor(tsr tensor.Tensor) *TensorGrid {
if _, ok := tsr.(*tensor.String); ok {
log.Printf("TensorGrid: String tensors cannot be displayed using TensorGrid\n")
return tg
}
tg.Tensor = tsr
if tg.Tensor != nil {
tg.Display.FromMeta(tg.Tensor)
}
return tg
}
// OpenTensorEditor pulls up a TensorEditor of our tensor
func (tg *TensorGrid) OpenTensorEditor() { //types:add
d := core.NewBody("Tensor Editor")
tb := core.NewToolbar(d)
te := NewTensorEditor(d).SetTensor(tg.Tensor)
te.OnChange(func(e events.Event) {
tg.NeedsRender()
})
tb.Maker(te.MakeToolbar)
d.RunWindowDialog(tg)
}
func (tg *TensorGrid) EditSettings() { //types:add
d := core.NewBody("Tensor Grid Display Options")
core.NewForm(d).SetStruct(&tg.Display).
OnChange(func(e events.Event) {
tg.NeedsRender()
})
d.RunWindowDialog(tg)
}
// MinSize returns minimum size based on tensor and display settings
func (tg *TensorGrid) MinSize() math32.Vector2 {
if tg.Tensor == nil || tg.Tensor.Len() == 0 {
return math32.Vector2{}
}
if tg.Display.Image {
return math32.Vec2(float32(tg.Tensor.DimSize(1)), float32(tg.Tensor.DimSize(0)))
}
rows, cols, rowEx, colEx := tensor.Projection2DShape(tg.Tensor.Shape(), tg.Display.OddRow)
frw := float32(rows) + float32(rowEx)*tg.Display.DimExtra // extra spacing
fcl := float32(cols) + float32(colEx)*tg.Display.DimExtra // extra spacing
mx := float32(max(frw, fcl))
gsz := tg.Display.TotPrefSize / mx
gsz = max(gsz, tg.Display.GridMinSize)
gsz = min(gsz, tg.Display.GridMaxSize)
gsz = max(gsz, 2)
return math32.Vec2(gsz*float32(fcl), gsz*float32(frw))
}
// EnsureColorMap makes sure there is a valid color map that matches specified name
func (tg *TensorGrid) EnsureColorMap() {
if tg.ColorMap != nil && tg.ColorMap.Name != string(tg.Display.ColorMap) {
tg.ColorMap = nil
}
if tg.ColorMap == nil {
ok := false
tg.ColorMap, ok = colormap.AvailableMaps[string(tg.Display.ColorMap)]
if !ok {
tg.Display.ColorMap = ""
tg.Display.Defaults()
}
tg.ColorMap = colormap.AvailableMaps[string(tg.Display.ColorMap)]
}
}
func (tg *TensorGrid) Color(val float64) (norm float64, clr color.Color) {
if tg.ColorMap.Indexed {
clr = tg.ColorMap.MapIndex(int(val))
} else {
norm = tg.Display.Range.ClipNormValue(val)
clr = tg.ColorMap.Map(float32(norm))
}
return
}
func (tg *TensorGrid) UpdateRange() {
if !tg.Display.Range.FixMin || !tg.Display.Range.FixMax {
min, max, _, _ := tg.Tensor.Range()
if !tg.Display.Range.FixMin {
nmin := minmax.NiceRoundNumber(min, true) // true = below #
tg.Display.Range.Min = nmin
}
if !tg.Display.Range.FixMax {
nmax := minmax.NiceRoundNumber(max, false) // false = above #
tg.Display.Range.Max = nmax
}
}
}
func (tg *TensorGrid) Render() {
if tg.Tensor == nil || tg.Tensor.Len() == 0 {
return
}
tg.EnsureColorMap()
tg.UpdateRange()
pc := &tg.Scene.PaintContext
pos := tg.Geom.Pos.Content
sz := tg.Geom.Size.Actual.Content
// sz.SetSubScalar(tg.Disp.BotRtSpace.Dots)
pc.FillBox(pos, sz, tg.Styles.Background)
tsr := tg.Tensor
if tg.Display.Image {
ysz := tsr.DimSize(0)
xsz := tsr.DimSize(1)
nclr := 1
outclr := false // outer dimension is color
if tsr.NumDims() == 3 {
if tsr.DimSize(0) == 3 || tsr.DimSize(0) == 4 {
outclr = true
ysz = tsr.DimSize(1)
xsz = tsr.DimSize(2)
nclr = tsr.DimSize(0)
} else {
nclr = tsr.DimSize(2)
}
}
tsz := math32.Vec2(float32(xsz), float32(ysz))
gsz := sz.Div(tsz)
for y := 0; y < ysz; y++ {
for x := 0; x < xsz; x++ {
ey := y
if !tg.Display.TopZero {
ey = (ysz - 1) - y
}
switch {
case outclr:
var r, g, b, a float64
a = 1
r = tg.Display.Range.ClipNormValue(tsr.Float([]int{0, y, x}))
g = tg.Display.Range.ClipNormValue(tsr.Float([]int{1, y, x}))
b = tg.Display.Range.ClipNormValue(tsr.Float([]int{2, y, x}))
if nclr > 3 {
a = tg.Display.Range.ClipNormValue(tsr.Float([]int{3, y, x}))
}
cr := math32.Vec2(float32(x), float32(ey))
pr := pos.Add(cr.Mul(gsz))
pc.StrokeStyle.Color = colors.Uniform(colors.FromFloat64(r, g, b, a))
pc.FillBox(pr, gsz, pc.StrokeStyle.Color)
case nclr > 1:
var r, g, b, a float64
a = 1
r = tg.Display.Range.ClipNormValue(tsr.Float([]int{y, x, 0}))
g = tg.Display.Range.ClipNormValue(tsr.Float([]int{y, x, 1}))
b = tg.Display.Range.ClipNormValue(tsr.Float([]int{y, x, 2}))
if nclr > 3 {
a = tg.Display.Range.ClipNormValue(tsr.Float([]int{y, x, 3}))
}
cr := math32.Vec2(float32(x), float32(ey))
pr := pos.Add(cr.Mul(gsz))
pc.StrokeStyle.Color = colors.Uniform(colors.FromFloat64(r, g, b, a))
pc.FillBox(pr, gsz, pc.StrokeStyle.Color)
default:
val := tg.Display.Range.ClipNormValue(tsr.Float([]int{y, x}))
cr := math32.Vec2(float32(x), float32(ey))
pr := pos.Add(cr.Mul(gsz))
pc.StrokeStyle.Color = colors.Uniform(colors.FromFloat64(val, val, val, 1))
pc.FillBox(pr, gsz, pc.StrokeStyle.Color)
}
}
}
return
}
rows, cols, rowEx, colEx := tensor.Projection2DShape(tsr.Shape(), tg.Display.OddRow)
frw := float32(rows) + float32(rowEx)*tg.Display.DimExtra // extra spacing
fcl := float32(cols) + float32(colEx)*tg.Display.DimExtra // extra spacing
rowsInner := rows
colsInner := cols
if rowEx > 0 {
rowsInner = rows / rowEx
}
if colEx > 0 {
colsInner = cols / colEx
}
tsz := math32.Vec2(fcl, frw)
gsz := sz.Div(tsz)
ssz := gsz.MulScalar(tg.Display.GridFill) // smaller size with margin
for y := 0; y < rows; y++ {
yex := float32(int(y/rowsInner)) * tg.Display.DimExtra
for x := 0; x < cols; x++ {
xex := float32(int(x/colsInner)) * tg.Display.DimExtra
ey := y
if !tg.Display.TopZero {
ey = (rows - 1) - y
}
val := tensor.Projection2DValue(tsr, tg.Display.OddRow, ey, x)
cr := math32.Vec2(float32(x)+xex, float32(y)+yex)
pr := pos.Add(cr.Mul(gsz))
_, clr := tg.Color(val)
pc.FillBox(pr, ssz, colors.Uniform(clr))
}
}
}
// Code generated by "core generate"; DO NOT EDIT.
package tensorcore
import (
"cogentcore.org/core/colors/colormap"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/stats/simat"
"cogentcore.org/core/tensor/table"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/tensor/tensorcore.SimMatGrid", IDName: "sim-mat-grid", Doc: "SimMatGrid is a widget that displays a similarity / distance matrix\nwith tensor values as a grid of colored squares, and labels for rows and columns.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Embeds: []types.Field{{Name: "TensorGrid"}}, Fields: []types.Field{{Name: "SimMat", Doc: "the similarity / distance matrix"}, {Name: "rowMaxSz"}, {Name: "rowMinBlank"}, {Name: "rowNGps"}, {Name: "colMaxSz"}, {Name: "colMinBlank"}, {Name: "colNGps"}}})
// NewSimMatGrid returns a new [SimMatGrid] with the given optional parent:
// SimMatGrid is a widget that displays a similarity / distance matrix
// with tensor values as a grid of colored squares, and labels for rows and columns.
func NewSimMatGrid(parent ...tree.Node) *SimMatGrid { return tree.New[SimMatGrid](parent...) }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/tensor/tensorcore.Table", IDName: "table", Doc: "Table provides a GUI widget for representing [table.Table] values.", Embeds: []types.Field{{Name: "ListBase"}}, Fields: []types.Field{{Name: "Table", Doc: "the idx view of the table that we're a view of"}, {Name: "TensorDisplay", Doc: "overall display options for tensor display"}, {Name: "ColumnTensorDisplay", Doc: "per column tensor display params"}, {Name: "ColumnTensorBlank", Doc: "per column blank tensor values"}, {Name: "NCols", Doc: "number of columns in table (as of last update)"}, {Name: "SortIndex", Doc: "current sort index"}, {Name: "SortDescending", Doc: "whether current sort order is descending"}, {Name: "headerWidths", Doc: "headerWidths has number of characters in each header, per visfields"}, {Name: "colMaxWidths", Doc: "colMaxWidths records maximum width in chars of string type fields"}, {Name: "BlankString", Doc: "\tblank values for out-of-range rows"}, {Name: "BlankFloat"}}})
// NewTable returns a new [Table] with the given optional parent:
// Table provides a GUI widget for representing [table.Table] values.
func NewTable(parent ...tree.Node) *Table { return tree.New[Table](parent...) }
// SetNCols sets the [Table.NCols]:
// number of columns in table (as of last update)
func (t *Table) SetNCols(v int) *Table { t.NCols = v; return t }
// SetSortIndex sets the [Table.SortIndex]:
// current sort index
func (t *Table) SetSortIndex(v int) *Table { t.SortIndex = v; return t }
// SetSortDescending sets the [Table.SortDescending]:
// whether current sort order is descending
func (t *Table) SetSortDescending(v bool) *Table { t.SortDescending = v; return t }
// SetBlankString sets the [Table.BlankString]:
//
// blank values for out-of-range rows
func (t *Table) SetBlankString(v string) *Table { t.BlankString = v; return t }
// SetBlankFloat sets the [Table.BlankFloat]
func (t *Table) SetBlankFloat(v float64) *Table { t.BlankFloat = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/tensor/tensorcore.TensorEditor", IDName: "tensor-editor", Doc: "TensorEditor provides a GUI widget for representing [tensor.Tensor] values.", Methods: []types.Method{{Name: "SaveCSV", Doc: "SaveTSV writes a tensor to a tab-separated-values (TSV) file.\nOuter-most dims are rows in the file, and inner-most is column --\nReading just grabs all values and doesn't care about shape.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename"}, Returns: []string{"error"}}, {Name: "OpenCSV", Doc: "OpenTSV reads a tensor from a tab-separated-values (TSV) file.\nusing the Go standard encoding/csv reader conforming\nto the official CSV standard.\nReads all values and assigns as many as fit.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename"}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "ListBase"}}, Fields: []types.Field{{Name: "Tensor", Doc: "the tensor that we're a view of"}, {Name: "Layout", Doc: "overall layout options for tensor display"}, {Name: "NCols", Doc: "number of columns in table (as of last update)"}, {Name: "headerWidths", Doc: "headerWidths has number of characters in each header, per visfields"}, {Name: "colMaxWidths", Doc: "colMaxWidths records maximum width in chars of string type fields"}, {Name: "BlankString", Doc: "\tblank values for out-of-range rows"}, {Name: "BlankFloat"}}})
// NewTensorEditor returns a new [TensorEditor] with the given optional parent:
// TensorEditor provides a GUI widget for representing [tensor.Tensor] values.
func NewTensorEditor(parent ...tree.Node) *TensorEditor { return tree.New[TensorEditor](parent...) }
// SetNCols sets the [TensorEditor.NCols]:
// number of columns in table (as of last update)
func (t *TensorEditor) SetNCols(v int) *TensorEditor { t.NCols = v; return t }
// SetBlankString sets the [TensorEditor.BlankString]:
//
// blank values for out-of-range rows
func (t *TensorEditor) SetBlankString(v string) *TensorEditor { t.BlankString = v; return t }
// SetBlankFloat sets the [TensorEditor.BlankFloat]
func (t *TensorEditor) SetBlankFloat(v float64) *TensorEditor { t.BlankFloat = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/tensor/tensorcore.TensorLayout", IDName: "tensor-layout", Doc: "TensorLayout are layout options for displaying tensors", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "OddRow", Doc: "even-numbered dimensions are displayed as Y*X rectangles.\nThis determines along which dimension to display any remaining\nodd dimension: OddRow = true = organize vertically along row\ndimension, false = organize horizontally across column dimension."}, {Name: "TopZero", Doc: "if true, then the Y=0 coordinate is displayed from the top-down;\notherwise the Y=0 coordinate is displayed from the bottom up,\nwhich is typical for emergent network patterns."}, {Name: "Image", Doc: "display the data as a bitmap image. if a 2D tensor, then it will\nbe a greyscale image. if a 3D tensor with size of either the first\nor last dim = either 3 or 4, then it is a RGB(A) color image."}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/tensor/tensorcore.TensorDisplay", IDName: "tensor-display", Doc: "TensorDisplay are options for displaying tensors", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Embeds: []types.Field{{Name: "TensorLayout"}}, Fields: []types.Field{{Name: "Range", Doc: "range to plot"}, {Name: "MinMax", Doc: "if not using fixed range, this is the actual range of data"}, {Name: "ColorMap", Doc: "the name of the color map to use in translating values to colors"}, {Name: "GridFill", Doc: "what proportion of grid square should be filled by color block -- 1 = all, .5 = half, etc"}, {Name: "DimExtra", Doc: "amount of extra space to add at dimension boundaries, as a proportion of total grid size"}, {Name: "GridMinSize", Doc: "minimum size for grid squares -- they will never be smaller than this"}, {Name: "GridMaxSize", Doc: "maximum size for grid squares -- they will never be larger than this"}, {Name: "TotPrefSize", Doc: "total preferred display size along largest dimension.\ngrid squares will be sized to fit within this size,\nsubject to harder GridMin / Max size constraints"}, {Name: "FontSize", Doc: "font size in standard point units for labels (e.g., SimMat)"}, {Name: "GridView", Doc: "our gridview, for update method"}}})
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/tensor/tensorcore.TensorGrid", IDName: "tensor-grid", Doc: "TensorGrid is a widget that displays tensor values as a grid of colored squares.", Methods: []types.Method{{Name: "OpenTensorEditor", Doc: "OpenTensorEditor pulls up a TensorEditor of our tensor", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "EditSettings", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "WidgetBase"}}, Fields: []types.Field{{Name: "Tensor", Doc: "the tensor that we view"}, {Name: "Display", Doc: "display options"}, {Name: "ColorMap", Doc: "the actual colormap"}}})
// NewTensorGrid returns a new [TensorGrid] with the given optional parent:
// TensorGrid is a widget that displays tensor values as a grid of colored squares.
func NewTensorGrid(parent ...tree.Node) *TensorGrid { return tree.New[TensorGrid](parent...) }
// SetDisplay sets the [TensorGrid.Display]:
// display options
func (t *TensorGrid) SetDisplay(v TensorDisplay) *TensorGrid { t.Display = v; return t }
// SetColorMap sets the [TensorGrid.ColorMap]:
// the actual colormap
func (t *TensorGrid) SetColorMap(v *colormap.Map) *TensorGrid { t.ColorMap = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/tensor/tensorcore.TensorButton", IDName: "tensor-button", Doc: "TensorButton represents a Tensor with a button for making a [TensorGrid]\nviewer for an [tensor.Tensor].", Embeds: []types.Field{{Name: "Button"}}, Fields: []types.Field{{Name: "Tensor"}}})
// NewTensorButton returns a new [TensorButton] with the given optional parent:
// TensorButton represents a Tensor with a button for making a [TensorGrid]
// viewer for an [tensor.Tensor].
func NewTensorButton(parent ...tree.Node) *TensorButton { return tree.New[TensorButton](parent...) }
// SetTensor sets the [TensorButton.Tensor]
func (t *TensorButton) SetTensor(v tensor.Tensor) *TensorButton { t.Tensor = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/tensor/tensorcore.TableButton", IDName: "table-button", Doc: "TableButton presents a button that pulls up the [Table] viewer for a [table.Table].", Embeds: []types.Field{{Name: "Button"}}, Fields: []types.Field{{Name: "Table"}}})
// NewTableButton returns a new [TableButton] with the given optional parent:
// TableButton presents a button that pulls up the [Table] viewer for a [table.Table].
func NewTableButton(parent ...tree.Node) *TableButton { return tree.New[TableButton](parent...) }
// SetTable sets the [TableButton.Table]
func (t *TableButton) SetTable(v *table.Table) *TableButton { t.Table = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/tensor/tensorcore.SimMatButton", IDName: "sim-mat-button", Doc: "SimMatValue presents a button that pulls up the [SimMatGrid] viewer for a [table.Table].", Embeds: []types.Field{{Name: "Button"}}, Fields: []types.Field{{Name: "SimMat"}}})
// NewSimMatButton returns a new [SimMatButton] with the given optional parent:
// SimMatValue presents a button that pulls up the [SimMatGrid] viewer for a [table.Table].
func NewSimMatButton(parent ...tree.Node) *SimMatButton { return tree.New[SimMatButton](parent...) }
// SetSimMat sets the [SimMatButton.SimMat]
func (t *SimMatButton) SetSimMat(v *simat.SimMat) *SimMatButton { t.SimMat = v; return t }
// Copyright (c) 2019, 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 tensorcore
import (
"cogentcore.org/core/core"
"cogentcore.org/core/icons"
"cogentcore.org/core/tensor"
"cogentcore.org/core/tensor/stats/simat"
"cogentcore.org/core/tensor/table"
)
func init() {
core.AddValueType[table.Table, TableButton]()
core.AddValueType[tensor.Float32, TensorButton]()
core.AddValueType[tensor.Float64, TensorButton]()
core.AddValueType[tensor.Int, TensorButton]()
core.AddValueType[tensor.Int32, TensorButton]()
core.AddValueType[tensor.Byte, TensorButton]()
core.AddValueType[tensor.String, TensorButton]()
core.AddValueType[tensor.Bits, TensorButton]()
core.AddValueType[simat.SimMat, SimMatButton]()
}
// TensorButton represents a Tensor with a button for making a [TensorGrid]
// viewer for an [tensor.Tensor].
type TensorButton struct {
core.Button
Tensor tensor.Tensor
}
func (tb *TensorButton) WidgetValue() any { return &tb.Tensor }
func (tb *TensorButton) Init() {
tb.Button.Init()
tb.SetType(core.ButtonTonal).SetIcon(icons.Edit)
tb.Updater(func() {
text := "None"
if tb.Tensor != nil {
text = "Tensor"
}
tb.SetText(text)
})
core.InitValueButton(tb, true, func(d *core.Body) {
NewTensorGrid(d).SetTensor(tb.Tensor)
})
}
// TableButton presents a button that pulls up the [Table] viewer for a [table.Table].
type TableButton struct {
core.Button
Table *table.Table
}
func (tb *TableButton) WidgetValue() any { return &tb.Table }
func (tb *TableButton) Init() {
tb.Button.Init()
tb.SetType(core.ButtonTonal).SetIcon(icons.Edit)
tb.Updater(func() {
text := "None"
if tb.Table != nil {
if nm, has := tb.Table.MetaData["name"]; has {
text = nm
} else {
text = "Table"
}
}
tb.SetText(text)
})
core.InitValueButton(tb, true, func(d *core.Body) {
NewTable(d).SetTable(tb.Table)
})
}
// SimMatValue presents a button that pulls up the [SimMatGrid] viewer for a [table.Table].
type SimMatButton struct {
core.Button
SimMat *simat.SimMat
}
func (tb *SimMatButton) WidgetValue() any { return &tb.SimMat }
func (tb *SimMatButton) Init() {
tb.Button.Init()
tb.SetType(core.ButtonTonal).SetIcon(icons.Edit)
tb.Updater(func() {
text := "None"
if tb.SimMat != nil && tb.SimMat.Mat != nil {
if nm, has := tb.SimMat.Mat.MetaData("name"); has {
text = nm
} else {
text = "SimMat"
}
}
tb.SetText(text)
})
core.InitValueButton(tb, true, func(d *core.Body) {
NewSimMatGrid(d).SetSimMat(tb.SimMat)
})
}
// Copyright (c) 2020, 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 tensormpi
import (
"fmt"
"log"
"cogentcore.org/core/base/mpi"
)
// Alloc allocates n items to current mpi proc based on WorldSize and WorldRank.
// Returns start and end (exclusive) range for current proc.
func AllocN(n int) (st, end int, err error) {
nproc := mpi.WorldSize()
if n%nproc != 0 {
err = fmt.Errorf("tensormpi.AllocN: number: %d is not an even multiple of number of MPI procs: %d -- must be!", n, nproc)
log.Println(err)
}
pt := n / nproc
st = pt * mpi.WorldRank()
end = st + pt
return
}
// Copyright (c) 2021, 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 tensormpi
import (
"errors"
"fmt"
"math/rand"
"cogentcore.org/core/base/mpi"
)
// RandCheck checks that the current random numbers generated across each
// MPI processor are identical.
func RandCheck(comm *mpi.Comm) error {
ws := comm.Size()
rnd := rand.Int()
src := []int{rnd}
agg := make([]int, ws)
err := comm.AllGatherInt(agg, src)
if err != nil {
return err
}
errs := ""
for i := range agg {
if agg[i] != rnd {
errs += fmt.Sprintf("%d ", i)
}
}
if errs != "" {
err = errors.New("tensormpi.RandCheck: random numbers differ in procs: " + errs)
mpi.Printf("%s\n", err)
}
return err
}
// Copyright (c) 2020, 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 tensormpi
import (
"cogentcore.org/core/base/mpi"
"cogentcore.org/core/tensor/table"
)
// GatherTableRows does an MPI AllGather on given src table data, gathering into dest.
// dest will have np * src.Rows Rows, filled with each processor's data, in order.
// dest must be a clone of src: if not same number of cols, will be configured from src.
func GatherTableRows(dest, src *table.Table, comm *mpi.Comm) {
sr := src.Rows
np := mpi.WorldSize()
dr := np * sr
if len(dest.Columns) != len(src.Columns) {
*dest = *src.Clone()
}
dest.SetNumRows(dr)
for ci, st := range src.Columns {
dt := dest.Columns[ci]
GatherTensorRows(dt, st, comm)
}
}
// ReduceTable does an MPI AllReduce on given src table data using given operation,
// gathering into dest.
// each processor must have the same table organization -- the tensor values are
// just aggregated directly across processors.
// dest will be a clone of src if not the same (cos & rows),
// does nothing for strings.
func ReduceTable(dest, src *table.Table, comm *mpi.Comm, op mpi.Op) {
sr := src.Rows
if len(dest.Columns) != len(src.Columns) {
*dest = *src.Clone()
}
dest.SetNumRows(sr)
for ci, st := range src.Columns {
dt := dest.Columns[ci]
ReduceTensor(dt, st, comm, op)
}
}
// Copyright (c) 2020, 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 tensormpi
import (
"reflect"
"cogentcore.org/core/base/mpi"
"cogentcore.org/core/tensor"
)
// GatherTensorRows does an MPI AllGather on given src tensor data, gathering into dest,
// using a row-based tensor organization (as in an table.Table).
// dest will have np * src.Rows Rows, filled with each processor's data, in order.
// dest must have same overall shape as src at start, but rows will be enforced.
func GatherTensorRows(dest, src tensor.Tensor, comm *mpi.Comm) error {
dt := src.DataType()
if dt == reflect.String {
return GatherTensorRowsString(dest.(*tensor.String), src.(*tensor.String), comm)
}
sr, _ := src.RowCellSize()
dr, _ := dest.RowCellSize()
np := mpi.WorldSize()
dl := np * sr
if dr != dl {
dest.SetNumRows(dl)
dr = dl
}
var err error
switch dt {
case reflect.Bool:
// todo
case reflect.Uint8:
dt := dest.(*tensor.Byte)
st := src.(*tensor.Byte)
err = comm.AllGatherU8(dt.Values, st.Values)
case reflect.Int32:
dt := dest.(*tensor.Int32)
st := src.(*tensor.Int32)
err = comm.AllGatherI32(dt.Values, st.Values)
case reflect.Int:
dt := dest.(*tensor.Int)
st := src.(*tensor.Int)
err = comm.AllGatherInt(dt.Values, st.Values)
case reflect.Float32:
dt := dest.(*tensor.Float32)
st := src.(*tensor.Float32)
err = comm.AllGatherF32(dt.Values, st.Values)
case reflect.Float64:
dt := dest.(*tensor.Float64)
st := src.(*tensor.Float64)
err = comm.AllGatherF64(dt.Values, st.Values)
}
return err
}
// GatherTensorRowsString does an MPI AllGather on given String src tensor data,
// gathering into dest, using a row-based tensor organization (as in an table.Table).
// dest will have np * src.Rows Rows, filled with each processor's data, in order.
// dest must have same overall shape as src at start, but rows will be enforced.
func GatherTensorRowsString(dest, src *tensor.String, comm *mpi.Comm) error {
sr, _ := src.RowCellSize()
dr, _ := dest.RowCellSize()
np := mpi.WorldSize()
dl := np * sr
if dr != dl {
dest.SetNumRows(dl)
dr = dl
}
ssz := len(src.Values)
dsz := len(dest.Values)
sln := make([]int, ssz)
dln := make([]int, dsz)
for i, s := range src.Values {
sln[i] = len(s)
}
err := comm.AllGatherInt(dln, sln)
if err != nil {
return err
}
mxlen := 0
for _, l := range dln {
mxlen = max(mxlen, l)
}
if mxlen == 0 {
return nil // nothing to transfer
}
sdt := make([]byte, ssz*mxlen)
ddt := make([]byte, dsz*mxlen)
idx := 0
for _, s := range src.Values {
l := len(s)
copy(sdt[idx:idx+l], []byte(s))
idx += mxlen
}
err = comm.AllGatherU8(ddt, sdt)
idx = 0
for i := range dest.Values {
l := dln[i]
s := string(ddt[idx : idx+l])
dest.Values[i] = s
idx += mxlen
}
return err
}
// ReduceTensor does an MPI AllReduce on given src tensor data, using given operation,
// gathering into dest. dest must have same overall shape as src -- will be enforced.
// IMPORTANT: src and dest must be different slices!
// each processor must have the same shape and organization for this to make sense.
// does nothing for strings.
func ReduceTensor(dest, src tensor.Tensor, comm *mpi.Comm, op mpi.Op) error {
dt := src.DataType()
if dt == reflect.String {
return nil
}
slen := src.Len()
if slen != dest.Len() {
dest.CopyShapeFrom(src)
}
var err error
switch dt {
case reflect.Bool:
dt := dest.(*tensor.Bits)
st := src.(*tensor.Bits)
err = comm.AllReduceU8(op, dt.Values, st.Values)
case reflect.Uint8:
dt := dest.(*tensor.Byte)
st := src.(*tensor.Byte)
err = comm.AllReduceU8(op, dt.Values, st.Values)
case reflect.Int32:
dt := dest.(*tensor.Int32)
st := src.(*tensor.Int32)
err = comm.AllReduceI32(op, dt.Values, st.Values)
case reflect.Int:
dt := dest.(*tensor.Int)
st := src.(*tensor.Int)
err = comm.AllReduceInt(op, dt.Values, st.Values)
case reflect.Float32:
dt := dest.(*tensor.Float32)
st := src.(*tensor.Float32)
err = comm.AllReduceF32(op, dt.Values, st.Values)
case reflect.Float64:
dt := dest.(*tensor.Float64)
st := src.(*tensor.Float64)
err = comm.AllReduceF64(op, dt.Values, st.Values)
}
return err
}
// 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 texteditor
// TODO: consider moving back to core or somewhere else based on the
// result of https://github.com/cogentcore/core/issues/711
import (
"image"
"log/slog"
"path/filepath"
"strings"
"sync"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/spell"
)
// initSpell ensures that the [spell.Spell] spell checker is set up.
func initSpell() error {
if core.TheApp.Platform().IsMobile() { // todo: too slow -- fix with aspell
return nil
}
if spell.Spell != nil {
return nil
}
pdir := core.TheApp.CogentCoreDataDir()
openpath := filepath.Join(pdir, "user_dict_en_us")
spell.Spell = spell.NewSpell(openpath)
return nil
}
// spellCheck has all the texteditor spell check state
type spellCheck struct {
// line number in source that spelling is operating on, if relevant
srcLn int
// character position in source that spelling is operating on (start of word to be corrected)
srcCh int
// list of suggested corrections
suggest []string
// word being checked
word string
// last word learned -- can be undone -- stored in lowercase format
lastLearned string
// the user's correction selection
correction string
// the event listeners for the spell (it sends Select events)
listeners events.Listeners
// stage is the popup [core.Stage] associated with the [spellState]
stage *core.Stage
showMu sync.Mutex
}
// newSpell returns a new [spellState]
func newSpell() *spellCheck {
initSpell()
return &spellCheck{}
}
// checkWord checks the model to determine if the word is known,
// bool is true if known, false otherwise. If not known,
// returns suggestions for close matching words.
func (sp *spellCheck) checkWord(word string) ([]string, bool) {
if spell.Spell == nil {
return nil, false
}
return spell.Spell.CheckWord(word)
}
// setWord sets the word to spell and other associated info
func (sp *spellCheck) setWord(word string, sugs []string, srcLn, srcCh int) *spellCheck {
sp.word = word
sp.suggest = sugs
sp.srcLn = srcLn
sp.srcCh = srcCh
return sp
}
// show is the main call for listing spelling corrections.
// Calls ShowNow which builds the correction popup menu
// Similar to completion.show but does not use a timer
// Displays popup immediately for any unknown word
func (sp *spellCheck) show(text string, ctx core.Widget, pos image.Point) {
if sp.stage != nil {
sp.cancel()
}
sp.showNow(text, ctx, pos)
}
// showNow actually builds the correction popup menu
func (sp *spellCheck) showNow(word string, ctx core.Widget, pos image.Point) {
if sp.stage != nil {
sp.cancel()
}
sp.showMu.Lock()
defer sp.showMu.Unlock()
sc := core.NewScene(ctx.AsTree().Name + "-spell")
core.StyleMenuScene(sc)
sp.stage = core.NewPopupStage(core.CompleterStage, sc, ctx).SetPos(pos)
if sp.isLastLearned(word) {
core.NewButton(sc).SetText("unlearn").SetTooltip("unlearn the last learned word").
OnClick(func(e events.Event) {
sp.cancel()
sp.unLearnLast()
})
} else {
count := len(sp.suggest)
if count == 1 && sp.suggest[0] == word {
return
}
if count == 0 {
core.NewButton(sc).SetText("no suggestion")
} else {
for i := 0; i < count; i++ {
text := sp.suggest[i]
core.NewButton(sc).SetText(text).OnClick(func(e events.Event) {
sp.cancel()
sp.spell(text)
})
}
}
core.NewSeparator(sc)
core.NewButton(sc).SetText("learn").OnClick(func(e events.Event) {
sp.cancel()
sp.learnWord()
})
core.NewButton(sc).SetText("ignore").OnClick(func(e events.Event) {
sp.cancel()
sp.ignoreWord()
})
}
if sc.NumChildren() > 0 {
sc.Events.SetStartFocus(sc.Child(0).(core.Widget))
}
sp.stage.Run()
}
// spell sends a Select event to Listeners indicating that the user has made a
// selection from the list of possible corrections
func (sp *spellCheck) spell(s string) {
sp.cancel()
sp.correction = s
sp.listeners.Call(&events.Base{Typ: events.Select})
}
// onSelect registers given listener function for Select events on Value.
// This is the primary notification event for all Complete elements.
func (sp *spellCheck) onSelect(fun func(e events.Event)) {
sp.on(events.Select, fun)
}
// on adds an event listener function for the given event type
func (sp *spellCheck) on(etype events.Types, fun func(e events.Event)) {
sp.listeners.Add(etype, fun)
}
// learnWord gets the misspelled/unknown word and passes to learnWord
func (sp *spellCheck) learnWord() {
sp.lastLearned = strings.ToLower(sp.word)
spell.Spell.AddWord(sp.word)
}
// isLastLearned returns true if given word was the last one learned
func (sp *spellCheck) isLastLearned(wrd string) bool {
lword := strings.ToLower(wrd)
return lword == sp.lastLearned
}
// unLearnLast unlearns the last learned word -- in case accidental
func (sp *spellCheck) unLearnLast() {
if sp.lastLearned == "" {
slog.Error("spell.UnLearnLast: no last learned word")
return
}
lword := sp.lastLearned
sp.lastLearned = ""
spell.Spell.DeleteWord(lword)
}
// ignoreWord adds the word to the ignore list
func (sp *spellCheck) ignoreWord() {
spell.Spell.IgnoreWord(sp.word)
}
// cancel cancels any pending spell correction.
// call when new events nullify prior correction.
// returns true if canceled
func (sp *spellCheck) cancel() bool {
if sp.stage == nil {
return false
}
st := sp.stage
sp.stage = nil
st.ClosePopup()
return true
}
// 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 texteditor
import (
"fmt"
"image"
"io/fs"
"log"
"log/slog"
"os"
"path/filepath"
"slices"
"time"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/parse/complete"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/token"
"cogentcore.org/core/spell"
"cogentcore.org/core/texteditor/highlighting"
"cogentcore.org/core/texteditor/text"
)
// Buffer is a buffer of text, which can be viewed by [Editor](s).
// It holds the raw text lines (in original string and rune formats,
// and marked-up from syntax highlighting), and sends signals for making
// edits to the text and coordinating those edits across multiple views.
// Editors always only view a single buffer, so they directly call methods
// on the buffer to drive updates, which are then broadcast.
// It also has methods for loading and saving buffers to files.
// Unlike GUI widgets, its methods generally send events, without an
// explicit Event suffix.
// Internally, the buffer represents new lines using \n = LF, but saving
// and loading can deal with Windows/DOS CRLF format.
type Buffer struct { //types:add
text.Lines
// Filename is the filename of the file that was last loaded or saved.
// It is used when highlighting code.
Filename core.Filename `json:"-" xml:"-"`
// Autosave specifies whether the file should be automatically
// saved after changes are made.
Autosave bool
// Info is the full information about the current file.
Info fileinfo.FileInfo
// LineColors are the colors to use for rendering circles
// next to the line numbers of certain lines.
LineColors map[int]image.Image
// editors are the editors that are currently viewing this buffer.
editors []*Editor
// posHistory is the history of cursor positions.
// It can be used to move back through them.
posHistory []lexer.Pos
// Complete is the functions and data for text completion.
Complete *core.Complete `json:"-" xml:"-"`
// spell is the functions and data for spelling correction.
spell *spellCheck
// currentEditor is the current text editor, such as the one that initiated the
// Complete or Correct process. The cursor position in this view is updated, and
// it is reset to nil after usage.
currentEditor *Editor
// listeners is used for sending standard system events.
// Change is sent for BufferDone, BufferInsert, and BufferDelete.
listeners events.Listeners
// Bool flags:
// autoSaving is used in atomically safe way to protect autosaving
autoSaving bool
// notSaved indicates if the text has been changed (edited) relative to the
// original, since last Save. This can be true even when changed flag is
// false, because changed is cleared on EditDone, e.g., when texteditor
// is being monitored for OnChange and user does Control+Enter.
// Use IsNotSaved() method to query state.
notSaved bool
// fileModOK have already asked about fact that file has changed since being
// opened, user is ok
fileModOK bool
}
// NewBuffer makes a new [Buffer] with default settings
// and initializes it.
func NewBuffer() *Buffer {
tb := &Buffer{}
tb.SetHighlighting(highlighting.StyleDefault)
tb.Options.EditorSettings = core.SystemSettings.Editor
tb.SetText(nil) // to initialize
return tb
}
// bufferSignals are signals that [Buffer] can send to [Editor].
type bufferSignals int32 //enums:enum -trim-prefix buffer
const (
// bufferDone means that editing was completed and applied to Txt field
// -- data is Txt bytes
bufferDone bufferSignals = iota
// bufferNew signals that entirely new text is present.
// All views should do full layout update.
bufferNew
// bufferMods signals that potentially diffuse modifications
// have been made. Views should do a Layout and Render.
bufferMods
// bufferInsert signals that some text was inserted.
// data is text.Edit describing change.
// The Buf always reflects the current state *after* the edit.
bufferInsert
// bufferDelete signals that some text was deleted.
// data is text.Edit describing change.
// The Buf always reflects the current state *after* the edit.
bufferDelete
// bufferMarkupUpdated signals that the Markup text has been updated
// This signal is typically sent from a separate goroutine,
// so should be used with a mutex
bufferMarkupUpdated
// bufferClosed signals that the text was closed.
bufferClosed
)
// signalEditors sends the given signal and optional edit info
// to all the [Editor]s for this [Buffer]
func (tb *Buffer) signalEditors(sig bufferSignals, edit *text.Edit) {
for _, vw := range tb.editors {
if vw != nil && vw.This != nil { // editor can be deleting
vw.bufferSignal(sig, edit)
}
}
if sig == bufferDone {
e := &events.Base{Typ: events.Change}
e.Init()
tb.listeners.Call(e)
} else if sig == bufferInsert || sig == bufferDelete {
e := &events.Base{Typ: events.Input}
e.Init()
tb.listeners.Call(e)
}
}
// OnChange adds an event listener function for the [events.Change] event.
func (tb *Buffer) OnChange(fun func(e events.Event)) {
tb.listeners.Add(events.Change, fun)
}
// OnInput adds an event listener function for the [events.Input] event.
func (tb *Buffer) OnInput(fun func(e events.Event)) {
tb.listeners.Add(events.Input, fun)
}
// IsNotSaved returns true if buffer was changed (edited) since last Save.
func (tb *Buffer) IsNotSaved() bool {
// note: could use a mutex on this if there are significant race issues
return tb.notSaved
}
// clearNotSaved sets Changed and NotSaved to false.
func (tb *Buffer) clearNotSaved() {
tb.SetChanged(false)
tb.notSaved = false
}
// Init initializes the buffer. Called automatically in SetText.
func (tb *Buffer) Init() {
if tb.MarkupDoneFunc != nil {
return
}
tb.MarkupDoneFunc = func() {
tb.signalEditors(bufferMarkupUpdated, nil)
}
tb.ChangedFunc = func() {
tb.notSaved = true
}
}
// SetText sets the text to the given bytes.
// Pass nil to initialize an empty buffer.
func (tb *Buffer) SetText(text []byte) *Buffer {
tb.Init()
tb.Lines.SetText(text)
tb.signalEditors(bufferNew, nil)
return tb
}
// SetString sets the text to the given string.
func (tb *Buffer) SetString(txt string) *Buffer {
return tb.SetText([]byte(txt))
}
func (tb *Buffer) Update() {
tb.signalMods()
}
// editDone finalizes any current editing, sends signal
func (tb *Buffer) editDone() {
tb.AutoSaveDelete()
tb.SetChanged(false)
tb.signalEditors(bufferDone, nil)
}
// Text returns the current text as a []byte array, applying all current
// changes by calling editDone, which will generate a signal if there have been
// changes.
func (tb *Buffer) Text() []byte {
tb.editDone()
return tb.Bytes()
}
// String returns the current text as a string, applying all current
// changes by calling editDone, which will generate a signal if there have been
// changes.
func (tb *Buffer) String() string {
return string(tb.Text())
}
// signalMods sends the BufMods signal for misc, potentially
// widespread modifications to buffer.
func (tb *Buffer) signalMods() {
tb.signalEditors(bufferMods, nil)
}
// SetReadOnly sets whether the buffer is read-only.
func (tb *Buffer) SetReadOnly(readonly bool) *Buffer {
tb.Undos.Off = readonly
return tb
}
// SetFilename sets the filename associated with the buffer and updates
// the code highlighting information accordingly.
func (tb *Buffer) SetFilename(fn string) *Buffer {
tb.Filename = core.Filename(fn)
tb.Stat()
tb.SetFileInfo(&tb.Info)
return tb
}
// Stat gets info about the file, including the highlighting language.
func (tb *Buffer) Stat() error {
tb.fileModOK = false
err := tb.Info.InitFile(string(tb.Filename))
tb.ConfigKnown() // may have gotten file type info even if not existing
return err
}
// ConfigKnown configures options based on the supported language info in parse.
// Returns true if supported.
func (tb *Buffer) ConfigKnown() bool {
if tb.Info.Known != fileinfo.Unknown {
if tb.spell == nil {
tb.setSpell()
}
if tb.Complete == nil {
tb.setCompleter(&tb.ParseState, completeParse, completeEditParse, lookupParse)
}
return tb.Options.ConfigKnown(tb.Info.Known)
}
return false
}
// SetFileExt sets syntax highlighting and other parameters
// based on the given file extension (without the . prefix),
// for cases where an actual file with [fileinfo.FileInfo] is not
// available.
func (tb *Buffer) SetFileExt(ext string) *Buffer {
tb.Lines.SetFileExt(ext)
return tb
}
// SetFileType sets the syntax highlighting and other parameters
// based on the given fileinfo.Known file type
func (tb *Buffer) SetLanguage(ftyp fileinfo.Known) *Buffer {
tb.Lines.SetLanguage(ftyp)
return tb
}
// FileModCheck checks if the underlying file has been modified since last
// Stat (open, save); if haven't yet prompted, user is prompted to ensure
// that this is OK. It returns true if the file was modified.
func (tb *Buffer) FileModCheck() bool {
if tb.fileModOK {
return false
}
info, err := os.Stat(string(tb.Filename))
if err != nil {
return false
}
if info.ModTime() != time.Time(tb.Info.ModTime) {
if !tb.IsNotSaved() { // we haven't edited: just revert
tb.Revert()
return true
}
sc := tb.sceneFromEditor()
d := core.NewBody("File changed on disk: " + fsx.DirAndFile(string(tb.Filename)))
core.NewText(d).SetType(core.TextSupporting).SetText(fmt.Sprintf("File has changed on disk since being opened or saved by you; what do you want to do? If you <code>Revert from Disk</code>, you will lose any existing edits in open buffer. If you <code>Ignore and Proceed</code>, the next save will overwrite the changed file on disk, losing any changes there. File: %v", tb.Filename))
d.AddBottomBar(func(bar *core.Frame) {
core.NewButton(bar).SetText("Save as to different file").OnClick(func(e events.Event) {
d.Close()
core.CallFunc(sc, tb.SaveAs)
})
core.NewButton(bar).SetText("Revert from disk").OnClick(func(e events.Event) {
d.Close()
tb.Revert()
})
core.NewButton(bar).SetText("Ignore and proceed").OnClick(func(e events.Event) {
d.Close()
tb.fileModOK = true
})
})
d.RunDialog(sc)
return true
}
return false
}
// Open loads the given file into the buffer.
func (tb *Buffer) Open(filename core.Filename) error { //types:add
err := tb.openFile(filename)
if err != nil {
return err
}
return nil
}
// OpenFS loads the given file in the given filesystem into the buffer.
func (tb *Buffer) OpenFS(fsys fs.FS, filename string) error {
txt, err := fs.ReadFile(fsys, filename)
if err != nil {
return err
}
tb.SetFilename(filename)
tb.SetText(txt)
return nil
}
// openFile just loads the given file into the buffer, without doing
// any markup or signaling. It is typically used in other functions or
// for temporary buffers.
func (tb *Buffer) openFile(filename core.Filename) error {
txt, err := os.ReadFile(string(filename))
if err != nil {
return err
}
tb.SetFilename(string(filename))
tb.SetText(txt)
return nil
}
// Revert re-opens text from the current file,
// if the filename is set; returns false if not.
// It uses an optimized diff-based update to preserve
// existing formatting, making it very fast if not very different.
func (tb *Buffer) Revert() bool { //types:add
tb.StopDelayedReMarkup()
tb.AutoSaveDelete() // justin case
if tb.Filename == "" {
return false
}
didDiff := false
if tb.NumLines() < diffRevertLines {
ob := NewBuffer()
err := ob.openFile(tb.Filename)
if errors.Log(err) != nil {
sc := tb.sceneFromEditor()
if sc != nil { // only if viewing
core.ErrorSnackbar(sc, err, "Error reopening file")
}
return false
}
tb.Stat() // "own" the new file..
if ob.NumLines() < diffRevertLines {
diffs := tb.DiffBuffers(&ob.Lines)
if len(diffs) < diffRevertDiffs {
tb.PatchFromBuffer(&ob.Lines, diffs)
didDiff = true
}
}
}
if !didDiff {
tb.openFile(tb.Filename)
}
tb.clearNotSaved()
tb.AutoSaveDelete()
tb.signalEditors(bufferNew, nil)
return true
}
// SaveAsFunc saves the current text into the given file.
// Does an editDone first to save edits and checks for an existing file.
// If it does exist then prompts to overwrite or not.
// If afterFunc is non-nil, then it is called with the status of the user action.
func (tb *Buffer) SaveAsFunc(filename core.Filename, afterFunc func(canceled bool)) {
tb.editDone()
if !errors.Log1(fsx.FileExists(string(filename))) {
tb.saveFile(filename)
if afterFunc != nil {
afterFunc(false)
}
} else {
sc := tb.sceneFromEditor()
d := core.NewBody("File exists")
core.NewText(d).SetType(core.TextSupporting).SetText(fmt.Sprintf("The file already exists; do you want to overwrite it? File: %v", filename))
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar).OnClick(func(e events.Event) {
if afterFunc != nil {
afterFunc(true)
}
})
d.AddOK(bar).OnClick(func(e events.Event) {
tb.saveFile(filename)
if afterFunc != nil {
afterFunc(false)
}
})
})
d.RunDialog(sc)
}
}
// SaveAs saves the current text into given file; does an editDone first to save edits
// and checks for an existing file; if it does exist then prompts to overwrite or not.
func (tb *Buffer) SaveAs(filename core.Filename) { //types:add
tb.SaveAsFunc(filename, nil)
}
// saveFile writes current buffer to file, with no prompting, etc
func (tb *Buffer) saveFile(filename core.Filename) error {
err := os.WriteFile(string(filename), tb.Bytes(), 0644)
if err != nil {
core.ErrorSnackbar(tb.sceneFromEditor(), err)
slog.Error(err.Error())
} else {
tb.clearNotSaved()
tb.Filename = filename
tb.Stat()
}
return err
}
// Save saves the current text into the current filename associated with this buffer.
func (tb *Buffer) Save() error { //types:add
if tb.Filename == "" {
return fmt.Errorf("core.Buf: filename is empty for Save")
}
tb.editDone()
info, err := os.Stat(string(tb.Filename))
if err == nil && info.ModTime() != time.Time(tb.Info.ModTime) {
sc := tb.sceneFromEditor()
d := core.NewBody("File Changed on Disk")
core.NewText(d).SetType(core.TextSupporting).SetText(fmt.Sprintf("File has changed on disk since you opened or saved it; what do you want to do? File: %v", tb.Filename))
d.AddBottomBar(func(bar *core.Frame) {
core.NewButton(bar).SetText("Save to different file").OnClick(func(e events.Event) {
d.Close()
core.CallFunc(sc, tb.SaveAs)
})
core.NewButton(bar).SetText("Open from disk, losing changes").OnClick(func(e events.Event) {
d.Close()
tb.Revert()
})
core.NewButton(bar).SetText("Save file, overwriting").OnClick(func(e events.Event) {
d.Close()
tb.saveFile(tb.Filename)
})
})
d.RunDialog(sc)
}
return tb.saveFile(tb.Filename)
}
// Close closes the buffer, prompting to save if there are changes, and disconnects
// from editors. If afterFun is non-nil, then it is called with the status of the user
// action.
func (tb *Buffer) Close(afterFun func(canceled bool)) bool {
if tb.IsNotSaved() {
tb.StopDelayedReMarkup()
sc := tb.sceneFromEditor()
if tb.Filename != "" {
d := core.NewBody("Close without saving?")
core.NewText(d).SetType(core.TextSupporting).SetText(fmt.Sprintf("Do you want to save your changes to file: %v?", tb.Filename))
d.AddBottomBar(func(bar *core.Frame) {
core.NewButton(bar).SetText("Cancel").OnClick(func(e events.Event) {
d.Close()
if afterFun != nil {
afterFun(true)
}
})
core.NewButton(bar).SetText("Close without saving").OnClick(func(e events.Event) {
d.Close()
tb.clearNotSaved()
tb.AutoSaveDelete()
tb.Close(afterFun)
})
core.NewButton(bar).SetText("Save").OnClick(func(e events.Event) {
tb.Save()
tb.Close(afterFun) // 2nd time through won't prompt
})
})
d.RunDialog(sc)
} else {
d := core.NewBody("Close without saving?")
core.NewText(d).SetType(core.TextSupporting).SetText("Do you want to save your changes (no filename for this buffer yet)? If so, Cancel and then do Save As")
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar).OnClick(func(e events.Event) {
if afterFun != nil {
afterFun(true)
}
})
d.AddOK(bar).SetText("Close without saving").OnClick(func(e events.Event) {
tb.clearNotSaved()
tb.AutoSaveDelete()
tb.Close(afterFun)
})
})
d.RunDialog(sc)
}
return false // awaiting decisions..
}
tb.signalEditors(bufferClosed, nil)
tb.SetText(nil)
tb.Filename = ""
tb.clearNotSaved()
if afterFun != nil {
afterFun(false)
}
return true
}
////////////////////////////////////////////////////////////////////////////////////////
// AutoSave
// autoSaveOff turns off autosave and returns the
// prior state of Autosave flag.
// Call AutoSaveRestore with rval when done.
// See BatchUpdate methods for auto-use of this.
func (tb *Buffer) autoSaveOff() bool {
asv := tb.Autosave
tb.Autosave = false
return asv
}
// autoSaveRestore restores prior Autosave setting,
// from AutoSaveOff
func (tb *Buffer) autoSaveRestore(asv bool) {
tb.Autosave = asv
}
// AutoSaveFilename returns the autosave filename.
func (tb *Buffer) AutoSaveFilename() string {
path, fn := filepath.Split(string(tb.Filename))
if fn == "" {
fn = "new_file"
}
asfn := filepath.Join(path, "#"+fn+"#")
return asfn
}
// autoSave does the autosave -- safe to call in a separate goroutine
func (tb *Buffer) autoSave() error {
if tb.autoSaving {
return nil
}
tb.autoSaving = true
asfn := tb.AutoSaveFilename()
b := tb.Bytes()
err := os.WriteFile(asfn, b, 0644)
if err != nil {
log.Printf("core.Buf: Could not AutoSave file: %v, error: %v\n", asfn, err)
}
tb.autoSaving = false
return err
}
// AutoSaveDelete deletes any existing autosave file
func (tb *Buffer) AutoSaveDelete() {
asfn := tb.AutoSaveFilename()
err := os.Remove(asfn)
// the file may not exist, which is fine
if err != nil && !errors.Is(err, fs.ErrNotExist) {
errors.Log(err)
}
}
// AutoSaveCheck checks if an autosave file exists; logic for dealing with
// it is left to larger app; call this before opening a file.
func (tb *Buffer) AutoSaveCheck() bool {
asfn := tb.AutoSaveFilename()
if _, err := os.Stat(asfn); os.IsNotExist(err) {
return false // does not exist
}
return true
}
/////////////////////////////////////////////////////////////////////////////
// Appending Lines
// AppendTextMarkup appends new text to end of buffer, using insert, returns
// edit, and uses supplied markup to render it.
func (tb *Buffer) AppendTextMarkup(text []byte, markup []byte, signal bool) *text.Edit {
tbe := tb.Lines.AppendTextMarkup(text, markup)
if tbe != nil && signal {
tb.signalEditors(bufferInsert, tbe)
}
return tbe
}
// AppendTextLineMarkup appends one line of new text to end of buffer, using
// insert, and appending a LF at the end of the line if it doesn't already
// have one. User-supplied markup is used. Returns the edit region.
func (tb *Buffer) AppendTextLineMarkup(text []byte, markup []byte, signal bool) *text.Edit {
tbe := tb.Lines.AppendTextLineMarkup(text, markup)
if tbe != nil && signal {
tb.signalEditors(bufferInsert, tbe)
}
return tbe
}
/////////////////////////////////////////////////////////////////////////////
// Editors
// addEditor adds a editor of this buffer, connecting our signals to the editor
func (tb *Buffer) addEditor(vw *Editor) {
tb.editors = append(tb.editors, vw)
}
// deleteEditor removes given editor from our buffer
func (tb *Buffer) deleteEditor(vw *Editor) {
tb.editors = slices.DeleteFunc(tb.editors, func(e *Editor) bool {
return e == vw
})
}
// sceneFromEditor returns Scene from text editor, if avail
func (tb *Buffer) sceneFromEditor() *core.Scene {
if len(tb.editors) > 0 {
return tb.editors[0].Scene
}
return nil
}
// AutoScrollEditors ensures that our editors are always viewing the end of the buffer
func (tb *Buffer) AutoScrollEditors() {
for _, ed := range tb.editors {
if ed != nil && ed.This != nil {
ed.renderLayout()
ed.SetCursorTarget(tb.EndPos())
}
}
}
// batchUpdateStart call this when starting a batch of updates.
// It calls AutoSaveOff and returns the prior state of that flag
// which must be restored using BatchUpdateEnd.
func (tb *Buffer) batchUpdateStart() (autoSave bool) {
tb.Undos.NewGroup()
autoSave = tb.autoSaveOff()
return
}
// batchUpdateEnd call to complete BatchUpdateStart
func (tb *Buffer) batchUpdateEnd(autoSave bool) {
tb.autoSaveRestore(autoSave)
}
const (
// EditSignal is used as an arg for edit methods with a signal arg, indicating
// that a signal should be emitted.
EditSignal = true
// EditNoSignal is used as an arg for edit methods with a signal arg, indicating
// that a signal should NOT be emitted.
EditNoSignal = false
// ReplaceMatchCase is used for MatchCase arg in ReplaceText method
ReplaceMatchCase = true
// ReplaceNoMatchCase is used for MatchCase arg in ReplaceText method
ReplaceNoMatchCase = false
)
// DeleteText is the primary method for deleting text from the buffer.
// It deletes region of text between start and end positions,
// optionally signaling views after text lines have been updated.
// Sets the timestamp on resulting Edit to now.
// An Undo record is automatically saved depending on Undo.Off setting.
func (tb *Buffer) DeleteText(st, ed lexer.Pos, signal bool) *text.Edit {
tb.FileModCheck()
tbe := tb.Lines.DeleteText(st, ed)
if tbe == nil {
return tbe
}
if signal {
tb.signalEditors(bufferDelete, tbe)
}
if tb.Autosave {
go tb.autoSave()
}
return tbe
}
// deleteTextRect deletes rectangular region of text between start, end
// defining the upper-left and lower-right corners of a rectangle.
// Fails if st.Ch >= ed.Ch. Sets the timestamp on resulting text.Edit to now.
// An Undo record is automatically saved depending on Undo.Off setting.
func (tb *Buffer) deleteTextRect(st, ed lexer.Pos, signal bool) *text.Edit {
tb.FileModCheck()
tbe := tb.Lines.DeleteTextRect(st, ed)
if tbe == nil {
return tbe
}
if signal {
tb.signalMods()
}
if tb.Autosave {
go tb.autoSave()
}
return tbe
}
// insertText is the primary method for inserting text into the buffer.
// It inserts new text at given starting position, optionally signaling
// views after text has been inserted. Sets the timestamp on resulting Edit to now.
// An Undo record is automatically saved depending on Undo.Off setting.
func (tb *Buffer) insertText(st lexer.Pos, text []byte, signal bool) *text.Edit {
tb.FileModCheck() // will just revert changes if shouldn't have changed
tbe := tb.Lines.InsertText(st, text)
if tbe == nil {
return tbe
}
if signal {
tb.signalEditors(bufferInsert, tbe)
}
if tb.Autosave {
go tb.autoSave()
}
return tbe
}
// insertTextRect inserts a rectangle of text defined in given text.Edit record,
// (e.g., from RegionRect or DeleteRect), optionally signaling
// views after text has been inserted.
// Returns a copy of the Edit record with an updated timestamp.
// An Undo record is automatically saved depending on Undo.Off setting.
func (tb *Buffer) insertTextRect(tbe *text.Edit, signal bool) *text.Edit {
tb.FileModCheck() // will just revert changes if shouldn't have changed
nln := tb.NumLines()
re := tb.Lines.InsertTextRect(tbe)
if re == nil {
return re
}
if signal {
if re.Reg.End.Ln >= nln {
ie := &text.Edit{}
ie.Reg.Start.Ln = nln - 1
ie.Reg.End.Ln = re.Reg.End.Ln
tb.signalEditors(bufferInsert, ie)
} else {
tb.signalMods()
}
}
if tb.Autosave {
go tb.autoSave()
}
return re
}
// ReplaceText does DeleteText for given region, and then InsertText at given position
// (typically same as delSt but not necessarily), optionally emitting a signal after the insert.
// if matchCase is true, then the lexer.MatchCase function is called to match the
// case (upper / lower) of the new inserted text to that of the text being replaced.
// returns the text.Edit for the inserted text.
func (tb *Buffer) ReplaceText(delSt, delEd, insPos lexer.Pos, insTxt string, signal, matchCase bool) *text.Edit {
tbe := tb.Lines.ReplaceText(delSt, delEd, insPos, insTxt, matchCase)
if tbe == nil {
return tbe
}
if signal {
tb.signalMods() // todo: could be more specific?
}
if tb.Autosave {
go tb.autoSave()
}
return tbe
}
// savePosHistory saves the cursor position in history stack of cursor positions --
// tracks across views -- returns false if position was on same line as last one saved
func (tb *Buffer) savePosHistory(pos lexer.Pos) bool {
if tb.posHistory == nil {
tb.posHistory = make([]lexer.Pos, 0, 1000)
}
sz := len(tb.posHistory)
if sz > 0 {
if tb.posHistory[sz-1].Ln == pos.Ln {
return false
}
}
tb.posHistory = append(tb.posHistory, pos)
// fmt.Printf("saved pos hist: %v\n", pos)
return true
}
/////////////////////////////////////////////////////////////////////////////
// Undo
// undo undoes next group of items on the undo stack
func (tb *Buffer) undo() []*text.Edit {
autoSave := tb.batchUpdateStart()
defer tb.batchUpdateEnd(autoSave)
tbe := tb.Lines.Undo()
if tbe == nil || tb.Undos.Pos == 0 { // no more undo = fully undone
tb.SetChanged(false)
tb.notSaved = false
tb.AutoSaveDelete()
}
tb.signalMods()
return tbe
}
// redo redoes next group of items on the undo stack,
// and returns the last record, nil if no more
func (tb *Buffer) redo() []*text.Edit {
autoSave := tb.batchUpdateStart()
defer tb.batchUpdateEnd(autoSave)
tbe := tb.Lines.Redo()
if tbe != nil {
tb.signalMods()
}
return tbe
}
/////////////////////////////////////////////////////////////////////////////
// LineColors
// SetLineColor sets the color to use for rendering a circle next to the line
// number at the given line.
func (tb *Buffer) SetLineColor(ln int, color image.Image) {
if tb.LineColors == nil {
tb.LineColors = make(map[int]image.Image)
}
tb.LineColors[ln] = color
}
// HasLineColor checks if given line has a line color set
func (tb *Buffer) HasLineColor(ln int) bool {
if ln < 0 {
return false
}
if tb.LineColors == nil {
return false
}
_, has := tb.LineColors[ln]
return has
}
// DeleteLineColor deletes the line color at the given line.
func (tb *Buffer) DeleteLineColor(ln int) {
if ln < 0 {
tb.LineColors = nil
return
}
if tb.LineColors == nil {
return
}
delete(tb.LineColors, ln)
}
/////////////////////////////////////////////////////////////////////////////
// Indenting
// see parse/lexer/indent.go for support functions
// indentLine indents line by given number of tab stops, using tabs or spaces,
// for given tab size (if using spaces) -- either inserts or deletes to reach target.
// Returns edit record for any change.
func (tb *Buffer) indentLine(ln, ind int) *text.Edit {
autoSave := tb.batchUpdateStart()
defer tb.batchUpdateEnd(autoSave)
tbe := tb.Lines.IndentLine(ln, ind)
tb.signalMods()
return tbe
}
// AutoIndentRegion does auto-indent over given region; end is *exclusive*
func (tb *Buffer) AutoIndentRegion(start, end int) {
autoSave := tb.batchUpdateStart()
defer tb.batchUpdateEnd(autoSave)
tb.Lines.AutoIndentRegion(start, end)
tb.signalMods()
}
// CommentRegion inserts comment marker on given lines; end is *exclusive*
func (tb *Buffer) CommentRegion(start, end int) {
autoSave := tb.batchUpdateStart()
defer tb.batchUpdateEnd(autoSave)
tb.Lines.CommentRegion(start, end)
tb.signalMods()
}
// JoinParaLines merges sequences of lines with hard returns forming paragraphs,
// separated by blank lines, into a single line per paragraph,
// within the given line regions; endLine is *inclusive*
func (tb *Buffer) JoinParaLines(startLine, endLine int) {
autoSave := tb.batchUpdateStart()
defer tb.batchUpdateEnd(autoSave)
tb.JoinParaLines(startLine, endLine)
tb.signalMods()
}
// TabsToSpaces replaces tabs with spaces over given region; end is *exclusive*
func (tb *Buffer) TabsToSpaces(start, end int) {
autoSave := tb.batchUpdateStart()
defer tb.batchUpdateEnd(autoSave)
tb.Lines.TabsToSpaces(start, end)
tb.signalMods()
}
// SpacesToTabs replaces tabs with spaces over given region; end is *exclusive*
func (tb *Buffer) SpacesToTabs(start, end int) {
autoSave := tb.batchUpdateStart()
defer tb.batchUpdateEnd(autoSave)
tb.Lines.SpacesToTabs(start, end)
tb.signalMods()
}
// DiffBuffersUnified computes the diff between this buffer and the other buffer,
// returning a unified diff with given amount of context (default of 3 will be
// used if -1)
func (tb *Buffer) DiffBuffersUnified(ob *Buffer, context int) []byte {
astr := tb.Strings(true) // needs newlines for some reason
bstr := ob.Strings(true)
return text.DiffLinesUnified(astr, bstr, context, string(tb.Filename), tb.Info.ModTime.String(),
string(ob.Filename), ob.Info.ModTime.String())
}
///////////////////////////////////////////////////////////////////////////////
// Complete and Spell
// setCompleter sets completion functions so that completions will
// automatically be offered as the user types
func (tb *Buffer) setCompleter(data any, matchFun complete.MatchFunc, editFun complete.EditFunc,
lookupFun complete.LookupFunc) {
if tb.Complete != nil {
if tb.Complete.Context == data {
tb.Complete.MatchFunc = matchFun
tb.Complete.EditFunc = editFun
tb.Complete.LookupFunc = lookupFun
return
}
tb.deleteCompleter()
}
tb.Complete = core.NewComplete().SetContext(data).SetMatchFunc(matchFun).
SetEditFunc(editFun).SetLookupFunc(lookupFun)
tb.Complete.OnSelect(func(e events.Event) {
tb.completeText(tb.Complete.Completion)
})
// todo: what about CompleteExtend event type?
// TODO(kai/complete): clean this up and figure out what to do about Extend and only connecting once
// note: only need to connect once..
// tb.Complete.CompleteSig.ConnectOnly(func(dlg *core.Dialog) {
// tbf, _ := recv.Embed(TypeBuf).(*Buf)
// if sig == int64(core.CompleteSelect) {
// tbf.CompleteText(data.(string)) // always use data
// } else if sig == int64(core.CompleteExtend) {
// tbf.CompleteExtend(data.(string)) // always use data
// }
// })
}
func (tb *Buffer) deleteCompleter() {
if tb.Complete == nil {
return
}
tb.Complete = nil
}
// completeText edits the text using the string chosen from the completion menu
func (tb *Buffer) completeText(s string) {
if s == "" {
return
}
// give the completer a chance to edit the completion before insert,
// also it return a number of runes past the cursor to delete
st := lexer.Pos{tb.Complete.SrcLn, 0}
en := lexer.Pos{tb.Complete.SrcLn, tb.LineLen(tb.Complete.SrcLn)}
var tbes string
tbe := tb.Region(st, en)
if tbe != nil {
tbes = string(tbe.ToBytes())
}
c := tb.Complete.GetCompletion(s)
pos := lexer.Pos{tb.Complete.SrcLn, tb.Complete.SrcCh}
ed := tb.Complete.EditFunc(tb.Complete.Context, tbes, tb.Complete.SrcCh, c, tb.Complete.Seed)
if ed.ForwardDelete > 0 {
delEn := lexer.Pos{tb.Complete.SrcLn, tb.Complete.SrcCh + ed.ForwardDelete}
tb.DeleteText(pos, delEn, EditNoSignal)
}
// now the normal completion insertion
st = pos
st.Ch -= len(tb.Complete.Seed)
tb.ReplaceText(st, pos, st, ed.NewText, EditSignal, ReplaceNoMatchCase)
if tb.currentEditor != nil {
ep := st
ep.Ch += len(ed.NewText) + ed.CursorAdjust
tb.currentEditor.SetCursorShow(ep)
tb.currentEditor = nil
}
}
// isSpellEnabled returns true if spelling correction is enabled,
// taking into account given position in text if it is relevant for cases
// where it is only conditionally enabled
func (tb *Buffer) isSpellEnabled(pos lexer.Pos) bool {
if tb.spell == nil || !tb.Options.SpellCorrect {
return false
}
switch tb.Info.Cat {
case fileinfo.Doc: // not in code!
return !tb.InTokenCode(pos)
case fileinfo.Code:
return tb.InComment(pos) || tb.InLitString(pos)
default:
return false
}
}
// setSpell sets spell correct functions so that spell correct will
// automatically be offered as the user types
func (tb *Buffer) setSpell() {
if tb.spell != nil {
return
}
initSpell()
tb.spell = newSpell()
tb.spell.onSelect(func(e events.Event) {
tb.correctText(tb.spell.correction)
})
}
// correctText edits the text using the string chosen from the correction menu
func (tb *Buffer) correctText(s string) {
st := lexer.Pos{tb.spell.srcLn, tb.spell.srcCh} // start of word
tb.RemoveTag(st, token.TextSpellErr)
oend := st
oend.Ch += len(tb.spell.word)
tb.ReplaceText(st, oend, st, s, EditSignal, ReplaceNoMatchCase)
if tb.currentEditor != nil {
ep := st
ep.Ch += len(s)
tb.currentEditor.SetCursorShow(ep)
tb.currentEditor = nil
}
}
// SpellCheckLineErrors runs spell check on given line, and returns Lex tags
// with token.TextSpellErr for any misspelled words
func (tb *Buffer) SpellCheckLineErrors(ln int) lexer.Line {
if !tb.IsValidLine(ln) {
return nil
}
return spell.CheckLexLine(tb.Line(ln), tb.HiTags(ln))
}
// spellCheckLineTag runs spell check on given line, and sets Tags for any
// misspelled words and updates markup for that line.
func (tb *Buffer) spellCheckLineTag(ln int) {
if !tb.IsValidLine(ln) {
return
}
ser := tb.SpellCheckLineErrors(ln)
ntgs := tb.AdjustedTags(ln)
ntgs.DeleteToken(token.TextSpellErr)
for _, t := range ser {
ntgs.AddSort(t)
}
tb.SetTags(ln, ntgs)
tb.MarkupLines(ln, ln)
tb.StartDelayedReMarkup()
}
// 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 texteditor
import (
"fmt"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/complete"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/parser"
"cogentcore.org/core/texteditor/text"
)
// completeParse uses [parse] symbols and language; the string is a line of text
// up to point where user has typed.
// The data must be the *FileState from which the language type is obtained.
func completeParse(data any, text string, posLine, posChar int) (md complete.Matches) {
sfs := data.(*parse.FileStates)
if sfs == nil {
// log.Printf("CompletePi: data is nil not FileStates or is nil - can't complete\n")
return md
}
lp, err := parse.LanguageSupport.Properties(sfs.Known)
if err != nil {
// log.Printf("CompletePi: %v\n", err)
return md
}
if lp.Lang == nil {
return md
}
// note: must have this set to ture to allow viewing of AST
// must set it in pi/parse directly -- so it is changed in the fileparse too
parser.GUIActive = true // note: this is key for debugging -- runs slower but makes the tree unique
md = lp.Lang.CompleteLine(sfs, text, lexer.Pos{posLine, posChar})
return md
}
// completeEditParse uses the selected completion to edit the text.
func completeEditParse(data any, text string, cursorPos int, comp complete.Completion, seed string) (ed complete.Edit) {
sfs := data.(*parse.FileStates)
if sfs == nil {
// log.Printf("CompleteEditPi: data is nil not FileStates or is nil - can't complete\n")
return ed
}
lp, err := parse.LanguageSupport.Properties(sfs.Known)
if err != nil {
// log.Printf("CompleteEditPi: %v\n", err)
return ed
}
if lp.Lang == nil {
return ed
}
return lp.Lang.CompleteEdit(sfs, text, cursorPos, comp, seed)
}
// lookupParse uses [parse] symbols and language; the string is a line of text
// up to point where user has typed.
// The data must be the *FileState from which the language type is obtained.
func lookupParse(data any, txt string, posLine, posChar int) (ld complete.Lookup) {
sfs := data.(*parse.FileStates)
if sfs == nil {
// log.Printf("LookupPi: data is nil not FileStates or is nil - can't lookup\n")
return ld
}
lp, err := parse.LanguageSupport.Properties(sfs.Known)
if err != nil {
// log.Printf("LookupPi: %v\n", err)
return ld
}
if lp.Lang == nil {
return ld
}
// note: must have this set to ture to allow viewing of AST
// must set it in pi/parse directly -- so it is changed in the fileparse too
parser.GUIActive = true // note: this is key for debugging -- runs slower but makes the tree unique
ld = lp.Lang.Lookup(sfs, txt, lexer.Pos{posLine, posChar})
if len(ld.Text) > 0 {
TextDialog(nil, "Lookup: "+txt, string(ld.Text))
return ld
}
if ld.Filename != "" {
tx := text.FileRegionBytes(ld.Filename, ld.StLine, ld.EdLine, true, 10) // comments, 10 lines back max
prmpt := fmt.Sprintf("%v [%d:%d]", ld.Filename, ld.StLine, ld.EdLine)
TextDialog(nil, "Lookup: "+txt+": "+prmpt, string(tx))
return ld
}
return ld
}
// 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 texteditor
import (
"fmt"
"image"
"image/draw"
"cogentcore.org/core/core"
"cogentcore.org/core/math32"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/styles/states"
)
var (
// editorBlinker manages cursor blinking
editorBlinker = core.Blinker{}
// editorSpriteName is the name of the window sprite used for the cursor
editorSpriteName = "texteditor.Editor.Cursor"
)
func init() {
core.TheApp.AddQuitCleanFunc(editorBlinker.QuitClean)
editorBlinker.Func = func() {
w := editorBlinker.Widget
editorBlinker.Unlock() // comes in locked
if w == nil {
return
}
ed := AsEditor(w)
ed.AsyncLock()
if !w.AsWidget().StateIs(states.Focused) || !w.AsWidget().IsVisible() {
ed.blinkOn = false
ed.renderCursor(false)
} else {
ed.blinkOn = !ed.blinkOn
ed.renderCursor(ed.blinkOn)
}
ed.AsyncUnlock()
}
}
// startCursor starts the cursor blinking and renders it
func (ed *Editor) startCursor() {
if ed == nil || ed.This == nil {
return
}
if !ed.IsVisible() {
return
}
ed.blinkOn = true
ed.renderCursor(true)
if core.SystemSettings.CursorBlinkTime == 0 {
return
}
editorBlinker.SetWidget(ed.This.(core.Widget))
editorBlinker.Blink(core.SystemSettings.CursorBlinkTime)
}
// clearCursor turns off cursor and stops it from blinking
func (ed *Editor) clearCursor() {
ed.stopCursor()
ed.renderCursor(false)
}
// stopCursor stops the cursor from blinking
func (ed *Editor) stopCursor() {
if ed == nil || ed.This == nil {
return
}
editorBlinker.ResetWidget(ed.This.(core.Widget))
}
// cursorBBox returns a bounding-box for a cursor at given position
func (ed *Editor) cursorBBox(pos lexer.Pos) image.Rectangle {
cpos := ed.charStartPos(pos)
cbmin := cpos.SubScalar(ed.CursorWidth.Dots)
cbmax := cpos.AddScalar(ed.CursorWidth.Dots)
cbmax.Y += ed.fontHeight
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 *Editor) renderCursor(on bool) {
if ed == nil || ed.This == nil {
return
}
if !on {
if ed.Scene == nil {
return
}
ms := ed.Scene.Stage.Main
if ms == nil {
return
}
spnm := ed.cursorSpriteName()
ms.Sprites.InactivateSprite(spnm)
return
}
if !ed.IsVisible() {
return
}
if ed.renders == nil {
return
}
ed.cursorMu.Lock()
defer ed.cursorMu.Unlock()
sp := ed.cursorSprite(on)
if sp == nil {
return
}
sp.Geom.Pos = ed.charStartPos(ed.CursorPos).ToPointFloor()
}
// cursorSpriteName returns the name of the cursor sprite
func (ed *Editor) cursorSpriteName() string {
spnm := fmt.Sprintf("%v-%v", editorSpriteName, ed.fontHeight)
return spnm
}
// 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 *Editor) cursorSprite(on bool) *core.Sprite {
sc := ed.Scene
if sc == nil {
return nil
}
ms := sc.Stage.Main
if ms == nil {
return nil // only MainStage has sprites
}
spnm := ed.cursorSpriteName()
sp, ok := ms.Sprites.SpriteByName(spnm)
if !ok {
bbsz := image.Point{int(math32.Ceil(ed.CursorWidth.Dots)), int(math32.Ceil(ed.fontHeight))}
if bbsz.X < 2 { // at least 2
bbsz.X = 2
}
sp = core.NewSprite(spnm, bbsz, image.Point{})
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
}
// Copyright (c) 2024, 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 diffbrowser
//go:generate core generate
import (
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/stringsx"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/styles"
"cogentcore.org/core/texteditor"
"cogentcore.org/core/tree"
)
// Browser is a diff browser, for browsing a set of paired files
// for viewing differences between them, organized into a tree
// structure, e.g., reflecting their source in a filesystem.
type Browser struct {
core.Frame
// starting paths for the files being compared
PathA, PathB string
}
func (br *Browser) Init() {
br.Frame.Init()
br.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
})
br.OnShow(func(e events.Event) {
br.OpenFiles()
})
tree.AddChildAt(br, "splits", func(w *core.Splits) {
w.SetSplits(.15, .85)
tree.AddChildAt(w, "treeframe", func(w *core.Frame) {
w.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.Overflow.Set(styles.OverflowAuto)
s.Grow.Set(1, 1)
})
tree.AddChildAt(w, "tree", func(w *Node) {})
})
tree.AddChildAt(w, "tabs", func(w *core.Tabs) {
w.Type = core.FunctionalTabs
})
})
}
// NewBrowserWindow opens a new diff Browser in a new window
func NewBrowserWindow() (*Browser, *core.Body) {
b := core.NewBody("Diff browser")
br := NewBrowser(b)
br.UpdateTree() // must have tree
b.AddTopBar(func(bar *core.Frame) {
core.NewToolbar(bar).Maker(br.MakeToolbar)
})
return br, b
}
func (br *Browser) Splits() *core.Splits {
return br.FindPath("splits").(*core.Splits)
}
func (br *Browser) Tree() *Node {
sp := br.Splits()
return sp.Child(0).AsTree().Child(0).(*Node)
}
func (br *Browser) Tabs() *core.Tabs {
return br.FindPath("splits/tabs").(*core.Tabs)
}
// OpenFiles Updates the tree based on files
func (br *Browser) OpenFiles() { //types:add
tv := br.Tree()
if tv == nil {
return
}
tv.Open()
}
func (br *Browser) MakeToolbar(p *tree.Plan) {
// tree.Add(p, func(w *core.FuncButton) {
// w.SetFunc(br.OpenFiles).SetText("").SetIcon(icons.Refresh).SetShortcut("Command+U")
// })
}
// ViewDiff views diff for given file Node, returning a texteditor.DiffEditor
func (br *Browser) ViewDiff(fn *Node) *texteditor.DiffEditor {
df := fsx.DirAndFile(fn.FileA)
tabs := br.Tabs()
tab := tabs.RecycleTab(df)
if tab.HasChildren() {
dv := tab.Child(1).(*texteditor.DiffEditor)
return dv
}
tb := core.NewToolbar(tab)
de := texteditor.NewDiffEditor(tab)
tb.Maker(de.MakeToolbar)
de.SetFileA(fn.FileA).SetFileB(fn.FileB).SetRevisionA(fn.RevA).SetRevisionB(fn.RevB)
de.DiffStrings(stringsx.SplitLines(fn.TextA), stringsx.SplitLines(fn.TextB))
br.Update()
return de
}
// Copyright (c) 2024, 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 diffbrowser
import (
"log/slog"
"os"
"path/filepath"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
)
// Node is an element in the diff tree
type Node struct {
core.Tree
// file names (full path) being compared. Name of node is just the filename.
// Typically A is the older, base version and B is the newer one being compared.
FileA, FileB string
// VCS revisions for files if applicable
RevA, RevB string
// Status of the change from A to B: A=Added, D=Deleted, M=Modified, R=Renamed
Status string
// Text content of the files
TextA, TextB string
// Info about the B file, for getting icons etc
Info fileinfo.FileInfo
}
func (tn *Node) Init() {
tn.Tree.Init()
tn.IconOpen = icons.FolderOpen
tn.IconClosed = icons.Folder
tn.ContextMenus = nil
tn.AddContextMenu(tn.ContextMenu)
tn.Parts.AsWidget().OnDoubleClick(func(e events.Event) {
if tn.HasChildren() {
return
}
br := tn.Browser()
if br == nil {
return
}
sels := tn.GetSelectedNodes()
if sels != nil {
br.ViewDiff(tn)
}
})
tn.Parts.Styler(func(s *styles.Style) {
s.Gap.X.Em(0.4)
})
tree.AddChildInit(tn.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")
})
})
}
// Browser returns the parent browser
func (tn *Node) Browser() *Browser {
return tree.ParentByType[*Browser](tn)
}
func (tn *Node) ContextMenu(m *core.Scene) {
vd := core.NewButton(m).SetText("View Diffs").SetIcon(icons.Add)
vd.Styler(func(s *styles.Style) {
s.SetState(!tn.HasSelection(), states.Disabled)
})
vd.OnClick(func(e events.Event) {
br := tn.Browser()
if br == nil {
return
}
sels := tn.GetSelectedNodes()
sn := sels[len(sels)-1].(*Node)
br.ViewDiff(sn)
})
}
// DiffDirs creates a tree of files within the two paths,
// where the files have the same names, yet differ in content.
// The excludeFile function, if non-nil, will exclude files or
// directories from consideration if it returns true.
func (br *Browser) DiffDirs(pathA, pathB string, excludeFile func(fname string) bool) {
br.PathA = pathA
br.PathB = pathB
tv := br.Tree()
tv.SetText(fsx.DirAndFile(pathA))
br.diffDirsAt(pathA, pathB, tv, excludeFile)
}
// diffDirsAt creates a tree of files with the same names
// that differ within two dirs.
func (br *Browser) diffDirsAt(pathA, pathB string, node *Node, excludeFile func(fname string) bool) {
da := fsx.Dirs(pathA)
db := fsx.Dirs(pathB)
node.SetFileA(pathA).SetFileB(pathB)
for _, pa := range da {
if excludeFile != nil && excludeFile(pa) {
continue
}
for _, pb := range db {
if pa == pb {
nn := NewNode(node)
nn.SetText(pa)
br.diffDirsAt(filepath.Join(pathA, pa), filepath.Join(pathB, pb), nn, excludeFile)
}
}
}
fsa := fsx.Filenames(pathA)
fsb := fsx.Filenames(pathB)
for _, fa := range fsa {
isDir := false
for _, pa := range da {
if fa == pa {
isDir = true
break
}
}
if isDir {
continue
}
if excludeFile != nil && excludeFile(fa) {
continue
}
for _, fb := range fsb {
if fa != fb {
continue
}
pfa := filepath.Join(pathA, fa)
pfb := filepath.Join(pathB, fb)
ca, err := os.ReadFile(pfa)
if err != nil {
slog.Error(err.Error())
continue
}
cb, err := os.ReadFile(pfb)
if err != nil {
slog.Error(err.Error())
continue
}
sa := string(ca)
sb := string(cb)
if sa == sb {
continue
}
nn := NewNode(node)
nn.SetText(fa)
nn.SetFileA(pfa).SetFileB(pfb).SetTextA(sa).SetTextB(sb)
nn.Info.InitFile(pfb)
nn.IconLeaf = nn.Info.Ic
}
}
}
// Code generated by "core generate"; DO NOT EDIT.
package diffbrowser
import (
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/texteditor/diffbrowser.Browser", IDName: "browser", Doc: "Browser is a diff browser, for browsing a set of paired files\nfor viewing differences between them, organized into a tree\nstructure, e.g., reflecting their source in a filesystem.", Methods: []types.Method{{Name: "OpenFiles", Doc: "OpenFiles Updates the tree based on files", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "PathA", Doc: "starting paths for the files being compared"}, {Name: "PathB", Doc: "starting paths for the files being compared"}}})
// NewBrowser returns a new [Browser] with the given optional parent:
// Browser is a diff browser, for browsing a set of paired files
// for viewing differences between them, organized into a tree
// structure, e.g., reflecting their source in a filesystem.
func NewBrowser(parent ...tree.Node) *Browser { return tree.New[Browser](parent...) }
// SetPathA sets the [Browser.PathA]:
// starting paths for the files being compared
func (t *Browser) SetPathA(v string) *Browser { t.PathA = v; return t }
// SetPathB sets the [Browser.PathB]:
// starting paths for the files being compared
func (t *Browser) SetPathB(v string) *Browser { t.PathB = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/texteditor/diffbrowser.Node", IDName: "node", Doc: "Node is an element in the diff tree", Embeds: []types.Field{{Name: "Tree"}}, Fields: []types.Field{{Name: "FileA", Doc: "file names (full path) being compared. Name of node is just the filename.\nTypically A is the older, base version and B is the newer one being compared."}, {Name: "FileB", Doc: "file names (full path) being compared. Name of node is just the filename.\nTypically A is the older, base version and B is the newer one being compared."}, {Name: "RevA", Doc: "VCS revisions for files if applicable"}, {Name: "RevB", Doc: "VCS revisions for files if applicable"}, {Name: "Status", Doc: "Status of the change from A to B: A=Added, D=Deleted, M=Modified, R=Renamed"}, {Name: "TextA", Doc: "Text content of the files"}, {Name: "TextB", Doc: "Text content of the files"}, {Name: "Info", Doc: "Info about the B file, for getting icons etc"}}})
// NewNode returns a new [Node] with the given optional parent:
// Node is an element in the diff tree
func NewNode(parent ...tree.Node) *Node { return tree.New[Node](parent...) }
// SetFileA sets the [Node.FileA]:
// file names (full path) being compared. Name of node is just the filename.
// Typically A is the older, base version and B is the newer one being compared.
func (t *Node) SetFileA(v string) *Node { t.FileA = v; return t }
// SetFileB sets the [Node.FileB]:
// file names (full path) being compared. Name of node is just the filename.
// Typically A is the older, base version and B is the newer one being compared.
func (t *Node) SetFileB(v string) *Node { t.FileB = v; return t }
// SetRevA sets the [Node.RevA]:
// VCS revisions for files if applicable
func (t *Node) SetRevA(v string) *Node { t.RevA = v; return t }
// SetRevB sets the [Node.RevB]:
// VCS revisions for files if applicable
func (t *Node) SetRevB(v string) *Node { t.RevB = v; return t }
// SetStatus sets the [Node.Status]:
// Status of the change from A to B: A=Added, D=Deleted, M=Modified, R=Renamed
func (t *Node) SetStatus(v string) *Node { t.Status = v; return t }
// SetTextA sets the [Node.TextA]:
// Text content of the files
func (t *Node) SetTextA(v string) *Node { t.TextA = v; return t }
// SetTextB sets the [Node.TextB]:
// Text content of the files
func (t *Node) SetTextB(v string) *Node { t.TextB = v; return t }
// SetInfo sets the [Node.Info]:
// Info about the B file, for getting icons etc
func (t *Node) SetInfo(v fileinfo.FileInfo) *Node { t.Info = v; return t }
// Copyright (c) 2024, 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 diffbrowser
import (
"log/slog"
"path/filepath"
"strings"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/stringsx"
"cogentcore.org/core/base/vcs"
)
// NewDiffBrowserVCS returns a new diff browser for files that differ
// between two given revisions in the repository.
func NewDiffBrowserVCS(repo vcs.Repo, revA, revB string) {
brow, b := NewBrowserWindow()
brow.DiffVCS(repo, revA, revB)
b.RunWindow()
}
// DiffVCS creates a tree of files changed in given revision.
func (br *Browser) DiffVCS(repo vcs.Repo, revA, revB string) {
cinfo, err := repo.FilesChanged(revA, revB, false)
if err != nil {
slog.Error(err.Error())
return
}
br.PathA = repo.LocalPath()
br.PathB = br.PathA
files := stringsx.SplitLines(string(cinfo))
tv := br.Tree()
tv.SetText(fsx.DirAndFile(br.PathA))
cdir := ""
var cdirs []string
var cnodes []*Node
root := br.Tree()
for _, fl := range files {
fd := strings.Fields(fl)
if len(fd) < 2 {
continue
}
status := fd[0]
if len(status) > 1 {
status = status[:1]
}
fpa := fd[1]
fpb := fpa
if len(fd) == 3 {
fpb = fd[2]
}
fp := fpb
dir, fn := filepath.Split(fp)
dir = filepath.Dir(dir)
if dir != cdir {
dirs := strings.Split(dir, "/")
nd := len(dirs)
mn := min(len(cdirs), nd)
di := 0
for i := 0; i < mn; i++ {
if cdirs[i] != dirs[i] {
break
} else {
di = i
}
}
cnodes = cnodes[:di]
for i := di; i < nd; i++ {
var nn *Node
if i == 0 {
nn = NewNode(root)
} else {
nn = NewNode(cnodes[i-1])
}
dp := filepath.Join(br.PathA, filepath.Join(dirs[:i+1]...))
nn.SetFileA(dp).SetFileB(dp)
nn.SetText(dirs[i])
cnodes = append(cnodes, nn)
}
cdir = dir
cdirs = dirs
}
var nn *Node
nd := len(cnodes)
if nd == 0 {
nn = NewNode(root)
} else {
nn = NewNode(cnodes[nd-1])
}
dpa := filepath.Join(br.PathA, fpa)
dpb := filepath.Join(br.PathA, fpb)
nn.SetFileA(dpa).SetFileB(dpb).SetRevA(revA).SetRevB(revB).SetStatus(status)
nn.SetText(fn + " [" + status + "]")
if status != "D" {
fbB, err := repo.FileContents(dpb, revB)
if err != nil {
slog.Error(err.Error())
}
nn.SetTextB(string(fbB))
nn.Info.InitFile(dpb)
nn.IconLeaf = nn.Info.Ic
}
if status != "A" {
fbA, err := repo.FileContents(dpa, revA)
if err != nil {
slog.Error(err.Error())
}
nn.SetTextA(string(fbA))
if status == "D" {
nn.Info.InitFile(dpa)
nn.IconLeaf = nn.Info.Ic
}
}
}
}
// Copyright (c) 2020, 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 texteditor
import (
"bytes"
"fmt"
"log/slog"
"os"
"strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/base/fsx"
"cogentcore.org/core/base/stringsx"
"cogentcore.org/core/base/vcs"
"cogentcore.org/core/colors"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/math32"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/token"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/texteditor/text"
"cogentcore.org/core/tree"
)
// DiffFiles shows the diffs between this file as the A file, and other file as B file,
// in a DiffEditorDialog
func DiffFiles(ctx core.Widget, afile, bfile string) (*DiffEditor, error) {
ab, err := os.ReadFile(afile)
if err != nil {
slog.Error(err.Error())
return nil, err
}
bb, err := os.ReadFile(bfile)
if err != nil {
slog.Error(err.Error())
return nil, err
}
astr := stringsx.SplitLines(string(ab))
bstr := stringsx.SplitLines(string(bb))
dlg := DiffEditorDialog(ctx, "Diff File View", astr, bstr, afile, bfile, "", "")
return dlg, nil
}
// DiffEditorDialogFromRevs opens a dialog for displaying diff between file
// at two different revisions from given repository
// if empty, defaults to: A = current HEAD, B = current WC file.
// -1, -2 etc also work as universal ways of specifying prior revisions.
func DiffEditorDialogFromRevs(ctx core.Widget, repo vcs.Repo, file string, fbuf *Buffer, rev_a, rev_b string) (*DiffEditor, error) {
var astr, bstr []string
if rev_b == "" { // default to current file
if fbuf != nil {
bstr = fbuf.Strings(false)
} else {
fb, err := text.FileBytes(file)
if err != nil {
core.ErrorDialog(ctx, err)
return nil, err
}
bstr = text.BytesToLineStrings(fb, false) // don't add new lines
}
} else {
fb, err := repo.FileContents(file, rev_b)
if err != nil {
core.ErrorDialog(ctx, err)
return nil, err
}
bstr = text.BytesToLineStrings(fb, false) // don't add new lines
}
fb, err := repo.FileContents(file, rev_a)
if err != nil {
core.ErrorDialog(ctx, err)
return nil, err
}
astr = text.BytesToLineStrings(fb, false) // don't add new lines
if rev_a == "" {
rev_a = "HEAD"
}
return DiffEditorDialog(ctx, "DiffVcs: "+fsx.DirAndFile(file), astr, bstr, file, file, rev_a, rev_b), nil
}
// DiffEditorDialog opens a dialog for displaying diff between two files as line-strings
func DiffEditorDialog(ctx core.Widget, title string, astr, bstr []string, afile, bfile, arev, brev string) *DiffEditor {
d := core.NewBody("Diff editor")
d.SetTitle(title)
de := NewDiffEditor(d)
de.SetFileA(afile).SetFileB(bfile).SetRevisionA(arev).SetRevisionB(brev)
de.DiffStrings(astr, bstr)
d.AddTopBar(func(bar *core.Frame) {
tb := core.NewToolbar(bar)
de.toolbar = tb
tb.Maker(de.MakeToolbar)
})
d.NewWindow().SetContext(ctx).SetNewWindow(true).Run()
return de
}
// TextDialog opens a dialog for displaying text string
func TextDialog(ctx core.Widget, title, text string) *Editor {
d := core.NewBody(title)
ed := NewEditor(d)
ed.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
})
ed.Buffer.SetText([]byte(text))
d.AddBottomBar(func(bar *core.Frame) {
core.NewButton(bar).SetText("Copy to clipboard").SetIcon(icons.ContentCopy).
OnClick(func(e events.Event) {
d.Clipboard().Write(mimedata.NewText(text))
})
d.AddOK(bar)
})
d.RunWindowDialog(ctx)
return ed
}
// DiffEditor presents two side-by-side [Editor]s showing the differences
// between two files (represented as lines of strings).
type DiffEditor struct {
core.Frame
// first file name being compared
FileA string
// second file name being compared
FileB string
// revision for first file, if relevant
RevisionA string
// revision for second file, if relevant
RevisionB string
// [Buffer] for A showing the aligned edit view
bufferA *Buffer
// [Buffer] for B showing the aligned edit view
bufferB *Buffer
// aligned diffs records diff for aligned lines
alignD text.Diffs
// diffs applied
diffs text.DiffSelected
inInputEvent bool
toolbar *core.Toolbar
}
func (dv *DiffEditor) Init() {
dv.Frame.Init()
dv.bufferA = NewBuffer()
dv.bufferB = NewBuffer()
dv.bufferA.Options.LineNumbers = true
dv.bufferB.Options.LineNumbers = true
dv.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
})
f := func(name string, buf *Buffer) {
tree.AddChildAt(dv, name, func(w *DiffTextEditor) {
w.SetBuffer(buf)
w.SetReadOnly(true)
w.Styler(func(s *styles.Style) {
s.Min.X.Ch(80)
s.Min.Y.Em(40)
})
w.On(events.Scroll, func(e events.Event) {
dv.syncEditors(events.Scroll, e, name)
})
w.On(events.Input, func(e events.Event) {
dv.syncEditors(events.Input, e, name)
})
})
}
f("text-a", dv.bufferA)
f("text-b", dv.bufferB)
}
func (dv *DiffEditor) updateToolbar() {
if dv.toolbar == nil {
return
}
dv.toolbar.Restyle()
}
// setFilenames sets the filenames and updates markup accordingly.
// Called in DiffStrings
func (dv *DiffEditor) setFilenames() {
dv.bufferA.SetFilename(dv.FileA)
dv.bufferB.SetFilename(dv.FileB)
dv.bufferA.Stat()
dv.bufferB.Stat()
}
// syncEditors synchronizes the text [Editor] scrolling and cursor positions
func (dv *DiffEditor) syncEditors(typ events.Types, e events.Event, name string) {
tva, tvb := dv.textEditors()
me, other := tva, tvb
if name == "text-b" {
me, other = tvb, tva
}
switch typ {
case events.Scroll:
other.Geom.Scroll.Y = me.Geom.Scroll.Y
other.ScrollUpdateFromGeom(math32.Y)
case events.Input:
if dv.inInputEvent {
return
}
dv.inInputEvent = true
other.SetCursorShow(me.CursorPos)
dv.inInputEvent = false
}
}
// nextDiff moves to next diff region
func (dv *DiffEditor) nextDiff(ab int) bool {
tva, tvb := dv.textEditors()
tv := tva
if ab == 1 {
tv = tvb
}
nd := len(dv.alignD)
curLn := tv.CursorPos.Ln
di, df := dv.alignD.DiffForLine(curLn)
if di < 0 {
return false
}
for {
di++
if di >= nd {
return false
}
df = dv.alignD[di]
if df.Tag != 'e' {
break
}
}
tva.SetCursorTarget(lexer.Pos{Ln: df.I1})
return true
}
// prevDiff moves to previous diff region
func (dv *DiffEditor) prevDiff(ab int) bool {
tva, tvb := dv.textEditors()
tv := tva
if ab == 1 {
tv = tvb
}
curLn := tv.CursorPos.Ln
di, df := dv.alignD.DiffForLine(curLn)
if di < 0 {
return false
}
for {
di--
if di < 0 {
return false
}
df = dv.alignD[di]
if df.Tag != 'e' {
break
}
}
tva.SetCursorTarget(lexer.Pos{Ln: df.I1})
return true
}
// saveAs saves A or B edits into given file.
// It checks for an existing file, prompts to overwrite or not.
func (dv *DiffEditor) saveAs(ab bool, filename core.Filename) {
if !errors.Log1(fsx.FileExists(string(filename))) {
dv.saveFile(ab, filename)
} else {
d := core.NewBody("File Exists, Overwrite?")
core.NewText(d).SetType(core.TextSupporting).SetText(fmt.Sprintf("File already exists, overwrite? File: %v", filename))
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar)
d.AddOK(bar).OnClick(func(e events.Event) {
dv.saveFile(ab, filename)
})
})
d.RunDialog(dv)
}
}
// saveFile writes A or B edits to file, with no prompting, etc
func (dv *DiffEditor) saveFile(ab bool, filename core.Filename) error {
var txt string
if ab {
txt = strings.Join(dv.diffs.B.Edit, "\n")
} else {
txt = strings.Join(dv.diffs.A.Edit, "\n")
}
err := os.WriteFile(string(filename), []byte(txt), 0644)
if err != nil {
core.ErrorSnackbar(dv, err)
slog.Error(err.Error())
}
return err
}
// saveFileA saves the current state of file A to given filename
func (dv *DiffEditor) saveFileA(fname core.Filename) { //types:add
dv.saveAs(false, fname)
dv.updateToolbar()
}
// saveFileB saves the current state of file B to given filename
func (dv *DiffEditor) saveFileB(fname core.Filename) { //types:add
dv.saveAs(true, fname)
dv.updateToolbar()
}
// DiffStrings computes differences between two lines-of-strings and displays in
// DiffEditor.
func (dv *DiffEditor) DiffStrings(astr, bstr []string) {
dv.setFilenames()
dv.diffs.SetStringLines(astr, bstr)
dv.bufferA.LineColors = nil
dv.bufferB.LineColors = nil
del := colors.Scheme.Error.Base
ins := colors.Scheme.Success.Base
chg := colors.Scheme.Primary.Base
nd := len(dv.diffs.Diffs)
dv.alignD = make(text.Diffs, nd)
var ab, bb [][]byte
absln := 0
bspc := []byte(" ")
for i, df := range dv.diffs.Diffs {
switch df.Tag {
case 'r':
di := df.I2 - df.I1
dj := df.J2 - df.J1
mx := max(di, dj)
ad := df
ad.I1 = absln
ad.I2 = absln + di
ad.J1 = absln
ad.J2 = absln + dj
dv.alignD[i] = ad
for i := 0; i < mx; i++ {
dv.bufferA.SetLineColor(absln+i, chg)
dv.bufferB.SetLineColor(absln+i, chg)
blen := 0
alen := 0
if i < di {
aln := []byte(astr[df.I1+i])
alen = len(aln)
ab = append(ab, aln)
}
if i < dj {
bln := []byte(bstr[df.J1+i])
blen = len(bln)
bb = append(bb, bln)
} else {
bb = append(bb, bytes.Repeat(bspc, alen))
}
if i >= di {
ab = append(ab, bytes.Repeat(bspc, blen))
}
}
absln += mx
case 'd':
di := df.I2 - df.I1
ad := df
ad.I1 = absln
ad.I2 = absln + di
ad.J1 = absln
ad.J2 = absln + di
dv.alignD[i] = ad
for i := 0; i < di; i++ {
dv.bufferA.SetLineColor(absln+i, ins)
dv.bufferB.SetLineColor(absln+i, del)
aln := []byte(astr[df.I1+i])
alen := len(aln)
ab = append(ab, aln)
bb = append(bb, bytes.Repeat(bspc, alen))
}
absln += di
case 'i':
dj := df.J2 - df.J1
ad := df
ad.I1 = absln
ad.I2 = absln + dj
ad.J1 = absln
ad.J2 = absln + dj
dv.alignD[i] = ad
for i := 0; i < dj; i++ {
dv.bufferA.SetLineColor(absln+i, del)
dv.bufferB.SetLineColor(absln+i, ins)
bln := []byte(bstr[df.J1+i])
blen := len(bln)
bb = append(bb, bln)
ab = append(ab, bytes.Repeat(bspc, blen))
}
absln += dj
case 'e':
di := df.I2 - df.I1
ad := df
ad.I1 = absln
ad.I2 = absln + di
ad.J1 = absln
ad.J2 = absln + di
dv.alignD[i] = ad
for i := 0; i < di; i++ {
ab = append(ab, []byte(astr[df.I1+i]))
bb = append(bb, []byte(bstr[df.J1+i]))
}
absln += di
}
}
dv.bufferA.SetTextLines(ab) // don't copy
dv.bufferB.SetTextLines(bb) // don't copy
dv.tagWordDiffs()
dv.bufferA.ReMarkup()
dv.bufferB.ReMarkup()
}
// tagWordDiffs goes through replace diffs and tags differences at the
// word level between the two regions.
func (dv *DiffEditor) tagWordDiffs() {
for _, df := range dv.alignD {
if df.Tag != 'r' {
continue
}
di := df.I2 - df.I1
dj := df.J2 - df.J1
mx := max(di, dj)
stln := df.I1
for i := 0; i < mx; i++ {
ln := stln + i
ra := dv.bufferA.Line(ln)
rb := dv.bufferB.Line(ln)
lna := lexer.RuneFields(ra)
lnb := lexer.RuneFields(rb)
fla := lna.RuneStrings(ra)
flb := lnb.RuneStrings(rb)
nab := max(len(fla), len(flb))
ldif := text.DiffLines(fla, flb)
ndif := len(ldif)
if nab > 25 && ndif > nab/2 { // more than half of big diff -- skip
continue
}
for _, ld := range ldif {
switch ld.Tag {
case 'r':
sla := lna[ld.I1]
ela := lna[ld.I2-1]
dv.bufferA.AddTag(ln, sla.St, ela.Ed, token.TextStyleError)
slb := lnb[ld.J1]
elb := lnb[ld.J2-1]
dv.bufferB.AddTag(ln, slb.St, elb.Ed, token.TextStyleError)
case 'd':
sla := lna[ld.I1]
ela := lna[ld.I2-1]
dv.bufferA.AddTag(ln, sla.St, ela.Ed, token.TextStyleDeleted)
case 'i':
slb := lnb[ld.J1]
elb := lnb[ld.J2-1]
dv.bufferB.AddTag(ln, slb.St, elb.Ed, token.TextStyleDeleted)
}
}
}
}
}
// applyDiff applies change from the other buffer to the buffer for given file
// name, from diff that includes given line.
func (dv *DiffEditor) applyDiff(ab int, line int) bool {
tva, tvb := dv.textEditors()
tv := tva
if ab == 1 {
tv = tvb
}
if line < 0 {
line = tv.CursorPos.Ln
}
di, df := dv.alignD.DiffForLine(line)
if di < 0 || df.Tag == 'e' {
return false
}
if ab == 0 {
dv.bufferA.Undos.Off = false
// srcLen := len(dv.BufB.Lines[df.J2])
spos := lexer.Pos{Ln: df.I1, Ch: 0}
epos := lexer.Pos{Ln: df.I2, Ch: 0}
src := dv.bufferB.Region(spos, epos)
dv.bufferA.DeleteText(spos, epos, true)
dv.bufferA.insertText(spos, src.ToBytes(), true) // we always just copy, is blank for delete..
dv.diffs.BtoA(di)
} else {
dv.bufferB.Undos.Off = false
spos := lexer.Pos{Ln: df.J1, Ch: 0}
epos := lexer.Pos{Ln: df.J2, Ch: 0}
src := dv.bufferA.Region(spos, epos)
dv.bufferB.DeleteText(spos, epos, true)
dv.bufferB.insertText(spos, src.ToBytes(), true)
dv.diffs.AtoB(di)
}
dv.updateToolbar()
return true
}
// undoDiff undoes last applied change, if any.
func (dv *DiffEditor) undoDiff(ab int) error {
tva, tvb := dv.textEditors()
if ab == 1 {
if !dv.diffs.B.Undo() {
err := errors.New("No more edits to undo")
core.ErrorSnackbar(dv, err)
return err
}
tvb.undo()
} else {
if !dv.diffs.A.Undo() {
err := errors.New("No more edits to undo")
core.ErrorSnackbar(dv, err)
return err
}
tva.undo()
}
return nil
}
func (dv *DiffEditor) MakeToolbar(p *tree.Plan) {
txta := "A: " + fsx.DirAndFile(dv.FileA)
if dv.RevisionA != "" {
txta += ": " + dv.RevisionA
}
tree.Add(p, func(w *core.Text) {
w.SetText(txta)
})
tree.Add(p, func(w *core.Button) {
w.SetText("Next").SetIcon(icons.KeyboardArrowDown).SetTooltip("move down to next diff region")
w.OnClick(func(e events.Event) {
dv.nextDiff(0)
})
w.Styler(func(s *styles.Style) {
s.SetState(len(dv.alignD) <= 1, states.Disabled)
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("Prev").SetIcon(icons.KeyboardArrowUp).SetTooltip("move up to previous diff region")
w.OnClick(func(e events.Event) {
dv.prevDiff(0)
})
w.Styler(func(s *styles.Style) {
s.SetState(len(dv.alignD) <= 1, states.Disabled)
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("A <- B").SetIcon(icons.ContentCopy).SetTooltip("for current diff region, apply change from corresponding version in B, and move to next diff")
w.OnClick(func(e events.Event) {
dv.applyDiff(0, -1)
dv.nextDiff(0)
})
w.Styler(func(s *styles.Style) {
s.SetState(len(dv.alignD) <= 1, states.Disabled)
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("Undo").SetIcon(icons.Undo).SetTooltip("undo last diff apply action (A <- B)")
w.OnClick(func(e events.Event) {
dv.undoDiff(0)
})
w.Styler(func(s *styles.Style) {
s.SetState(!dv.bufferA.IsNotSaved(), states.Disabled)
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("Save").SetIcon(icons.Save).SetTooltip("save edited version of file with the given; prompts for filename")
w.OnClick(func(e events.Event) {
fb := core.NewSoloFuncButton(w).SetFunc(dv.saveFileA)
fb.Args[0].SetValue(core.Filename(dv.FileA))
fb.CallFunc()
})
w.Styler(func(s *styles.Style) {
s.SetState(!dv.bufferA.IsNotSaved(), states.Disabled)
})
})
tree.Add(p, func(w *core.Separator) {})
txtb := "B: " + fsx.DirAndFile(dv.FileB)
if dv.RevisionB != "" {
txtb += ": " + dv.RevisionB
}
tree.Add(p, func(w *core.Text) {
w.SetText(txtb)
})
tree.Add(p, func(w *core.Button) {
w.SetText("Next").SetIcon(icons.KeyboardArrowDown).SetTooltip("move down to next diff region")
w.OnClick(func(e events.Event) {
dv.nextDiff(1)
})
w.Styler(func(s *styles.Style) {
s.SetState(len(dv.alignD) <= 1, states.Disabled)
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("Prev").SetIcon(icons.KeyboardArrowUp).SetTooltip("move up to previous diff region")
w.OnClick(func(e events.Event) {
dv.prevDiff(1)
})
w.Styler(func(s *styles.Style) {
s.SetState(len(dv.alignD) <= 1, states.Disabled)
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("A -> B").SetIcon(icons.ContentCopy).SetTooltip("for current diff region, apply change from corresponding version in A, and move to next diff")
w.OnClick(func(e events.Event) {
dv.applyDiff(1, -1)
dv.nextDiff(1)
})
w.Styler(func(s *styles.Style) {
s.SetState(len(dv.alignD) <= 1, states.Disabled)
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("Undo").SetIcon(icons.Undo).SetTooltip("undo last diff apply action (A -> B)")
w.OnClick(func(e events.Event) {
dv.undoDiff(1)
})
w.Styler(func(s *styles.Style) {
s.SetState(!dv.bufferB.IsNotSaved(), states.Disabled)
})
})
tree.Add(p, func(w *core.Button) {
w.SetText("Save").SetIcon(icons.Save).SetTooltip("save edited version of file -- prompts for filename -- this will convert file back to its original form (removing side-by-side alignment) and end the diff editing function")
w.OnClick(func(e events.Event) {
fb := core.NewSoloFuncButton(w).SetFunc(dv.saveFileB)
fb.Args[0].SetValue(core.Filename(dv.FileB))
fb.CallFunc()
})
w.Styler(func(s *styles.Style) {
s.SetState(!dv.bufferB.IsNotSaved(), states.Disabled)
})
})
}
func (dv *DiffEditor) textEditors() (*DiffTextEditor, *DiffTextEditor) {
av := dv.Child(0).(*DiffTextEditor)
bv := dv.Child(1).(*DiffTextEditor)
return av, bv
}
////////////////////////////////////////////////////////////////////////////////
// DiffTextEditor
// DiffTextEditor supports double-click based application of edits from one
// buffer to the other.
type DiffTextEditor struct {
Editor
}
func (ed *DiffTextEditor) Init() {
ed.Editor.Init()
ed.Styler(func(s *styles.Style) {
s.Grow.Set(1, 1)
})
ed.OnDoubleClick(func(e events.Event) {
pt := ed.PointToRelPos(e.Pos())
if pt.X >= 0 && pt.X < int(ed.LineNumberOffset) {
newPos := ed.PixelToCursor(pt)
ln := newPos.Ln
dv := ed.diffEditor()
if dv != nil && ed.Buffer != nil {
if ed.Name == "text-a" {
dv.applyDiff(0, ln)
} else {
dv.applyDiff(1, ln)
}
}
e.SetHandled()
return
}
})
}
func (ed *DiffTextEditor) diffEditor() *DiffEditor {
return tree.ParentByType[*DiffEditor](ed)
}
// Package bytes is a partial port of Python difflib module for bytes.
//
// It provides tools to compare sequences of bytes and generate textual diffs.
//
// The following class and functions have been ported:
//
// - SequenceMatcher
//
// - unified_diff
//
// - context_diff
//
// Getting unified diffs was the main goal of the port. Keep in mind this code
// is mostly suitable to output text differences in a human friendly way, there
// are no guarantees generated diffs are consumable by patch(1).
package bytes
import (
"bufio"
"bytes"
"errors"
"fmt"
"hash/adler32"
"io"
"strings"
"unicode"
)
func calculateRatio(matches, length int) float64 {
if length > 0 {
return 2.0 * float64(matches) / float64(length)
}
return 1.0
}
func listifyString(str []byte) (lst [][]byte) {
lst = make([][]byte, len(str))
for i := range str {
lst[i] = str[i : i+1]
}
return lst
}
type Match struct {
A int
B int
Size int
}
type OpCode struct {
Tag byte
I1 int
I2 int
J1 int
J2 int
}
type lineHash uint32
func _hash(line []byte) lineHash {
return lineHash(adler32.Checksum(line))
}
// B2J is essentially a map from lines to line numbers, so that later it can
// be made a bit cleverer than the standard map in that it will not need to
// store copies of the lines.
// It needs to hold a reference to the underlying slice of lines.
type B2J struct {
store map[lineHash][][]int
b [][]byte
}
type lineType int8
const (
lineNONE lineType = 0
lineNORMAL lineType = 1
lineJUNK lineType = -1
linePOPULAR lineType = -2
)
func (b2j *B2J) _find(line *[]byte) (h lineHash, slotIndex int,
slot []int, lt lineType) {
h = _hash(*line)
for slotIndex, slot = range b2j.store[h] {
// Thanks to the qualities of sha1, the probability of having more than
// one line content with the same hash is very low. Nevertheless, store
// each of them in a different slot, that we can differentiate by
// looking at the line contents in the b slice.
// In place of all the line numbers where the line appears, a slot can
// also contain [lineno, -1] if b[lineno] is junk.
if bytes.Equal(*line, b2j.b[slot[0]]) {
// The content already has a slot in its hash bucket.
if len(slot) == 2 && slot[1] < 0 {
lt = lineType(slot[1])
} else {
lt = lineNORMAL
}
return // every return variable has the correct value
}
}
// The line content still has no slot.
slotIndex = -1
slot = nil
lt = lineNONE
return
}
func newB2J(b [][]byte, isJunk func([]byte) bool, autoJunk bool) *B2J {
b2j := B2J{store: map[lineHash][][]int{}, b: b}
ntest := len(b)
if autoJunk && ntest >= 200 {
ntest = ntest/100 + 1
}
for lineno, line := range b {
h, slotIndex, slot, lt := b2j._find(&line)
switch lt {
case lineNORMAL:
if len(slot) >= ntest {
b2j.store[h][slotIndex] = []int{slot[0], int(linePOPULAR)}
} else {
b2j.store[h][slotIndex] = append(slot, lineno)
}
case lineNONE:
if isJunk != nil && isJunk(line) {
b2j.store[h] = append(b2j.store[h], []int{lineno, int(lineJUNK)})
} else {
b2j.store[h] = append(b2j.store[h], []int{lineno})
}
default:
}
}
return &b2j
}
func (b2j *B2J) get(line []byte) []int {
_, _, slot, lt := b2j._find(&line)
if lt == lineNORMAL {
return slot
}
return []int{}
}
func (b2j *B2J) isBJunk(line []byte) bool {
_, _, _, lt := b2j._find(&line)
return lt == lineJUNK
}
// SequenceMatcher compares sequence of strings. The basic
// algorithm predates, and is a little fancier than, an algorithm
// published in the late 1980's by Ratcliff and Obershelp under the
// hyperbolic name "gestalt pattern matching". The basic idea is to find
// the longest contiguous matching subsequence that contains no "junk"
// elements (R-O doesn't address junk). The same idea is then applied
// recursively to the pieces of the sequences to the left and to the right
// of the matching subsequence. This does not yield minimal edit
// sequences, but does tend to yield matches that "look right" to people.
//
// SequenceMatcher tries to compute a "human-friendly diff" between two
// sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the
// longest *contiguous* & junk-free matching subsequence. That's what
// catches peoples' eyes. The Windows(tm) windiff has another interesting
// notion, pairing up elements that appear uniquely in each sequence.
// That, and the method here, appear to yield more intuitive difference
// reports than does diff. This method appears to be the least vulnerable
// to synching up on blocks of "junk lines", though (like blank lines in
// ordinary text files, or maybe "<P>" lines in HTML files). That may be
// because this is the only method of the 3 that has a *concept* of
// "junk" <wink>.
//
// Timing: Basic R-O is cubic time worst case and quadratic time expected
// case. SequenceMatcher is quadratic time for the worst case and has
// expected-case behavior dependent in a complicated way on how many
// elements the sequences have in common; best case time is linear.
type SequenceMatcher struct {
a [][]byte
b [][]byte
b2j B2J
IsJunk func([]byte) bool
autoJunk bool
matchingBlocks []Match
fullBCount map[lineHash]int
opCodes []OpCode
}
func NewMatcher(a, b [][]byte) *SequenceMatcher {
m := SequenceMatcher{autoJunk: true}
m.SetSeqs(a, b)
return &m
}
func NewMatcherWithJunk(a, b [][]byte, autoJunk bool,
isJunk func([]byte) bool) *SequenceMatcher {
m := SequenceMatcher{IsJunk: isJunk, autoJunk: autoJunk}
m.SetSeqs(a, b)
return &m
}
// SetSeqs sets two sequences to be compared.
func (m *SequenceMatcher) SetSeqs(a, b [][]byte) {
m.SetSeq1(a)
m.SetSeq2(b)
}
// SetSeq1 sets the first sequence to be compared. The second sequence to be compared is
// not changed.
//
// SequenceMatcher computes and caches detailed information about the second
// sequence, so if you want to compare one sequence S against many sequences,
// use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other
// sequences.
//
// See also SetSeqs() and SetSeq2().
func (m *SequenceMatcher) SetSeq1(a [][]byte) {
if &a == &m.a {
return
}
m.a = a
m.matchingBlocks = nil
m.opCodes = nil
}
// SetSeq2 sets the second sequence to be compared. The first sequence to be compared is
// not changed.
func (m *SequenceMatcher) SetSeq2(b [][]byte) {
if &b == &m.b {
return
}
m.b = b
m.matchingBlocks = nil
m.opCodes = nil
m.fullBCount = nil
m.chainB()
}
func (m *SequenceMatcher) chainB() {
// Populate line -> index mapping
b2j := *newB2J(m.b, m.IsJunk, m.autoJunk)
m.b2j = b2j
}
// Find longest matching block in a[alo:ahi] and b[blo:bhi].
//
// If IsJunk is not defined:
//
// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where
//
// alo <= i <= i+k <= ahi
// blo <= j <= j+k <= bhi
//
// and for all (i',j',k') meeting those conditions,
//
// k >= k'
// i <= i'
// and if i == i', j <= j'
//
// In other words, of all maximal matching blocks, return one that
// starts earliest in a, and of all those maximal matching blocks that
// start earliest in a, return the one that starts earliest in b.
//
// If IsJunk is defined, first the longest matching block is
// determined as above, but with the additional restriction that no
// junk element appears in the block. Then that block is extended as
// far as possible by matching (only) junk elements on both sides. So
// the resulting block never matches on junk except as identical junk
// happens to be adjacent to an "interesting" match.
//
// If no blocks match, return (alo, blo, 0).
func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match {
// CAUTION: stripping common prefix or suffix would be incorrect.
// E.g.,
// ab
// acab
// Longest matching block is "ab", but if common prefix is
// stripped, it's "a" (tied with "b"). UNIX(tm) diff does so
// strip, so ends up claiming that ab is changed to acab by
// inserting "ca" in the middle. That's minimal but unintuitive:
// "it's obvious" that someone inserted "ac" at the front.
// Windiff ends up at the same place as diff, but by pairing up
// the unique 'b's and then matching the first two 'a's.
besti, bestj, bestsize := alo, blo, 0
// find longest junk-free match
// during an iteration of the loop, j2len[j] = length of longest
// junk-free match ending with a[i-1] and b[j]
N := bhi - blo
j2len := make([]int, N)
newj2len := make([]int, N)
var indices []int
for i := alo; i != ahi; i++ {
// look at all instances of a[i] in b; note that because
// b2j has no junk keys, the loop is skipped if a[i] is junk
newindices := m.b2j.get(m.a[i])
for _, j := range newindices {
// a[i] matches b[j]
if j < blo {
continue
}
if j >= bhi {
break
}
k := 1
if j > blo {
k = j2len[j-1-blo] + 1
}
newj2len[j-blo] = k
if k > bestsize {
besti, bestj, bestsize = i-k+1, j-k+1, k
}
}
// j2len = newj2len, clear and reuse j2len as newj2len
for _, j := range indices {
if j < blo {
continue
}
if j >= bhi {
break
}
j2len[j-blo] = 0
}
indices = newindices
j2len, newj2len = newj2len, j2len
}
// Extend the best by non-junk elements on each end. In particular,
// "popular" non-junk elements aren't in b2j, which greatly speeds
// the inner loop above, but also means "the best" match so far
// doesn't contain any junk *or* popular non-junk elements.
for besti > alo && bestj > blo && !m.b2j.isBJunk(m.b[bestj-1]) &&
bytes.Equal(m.a[besti-1], m.b[bestj-1]) {
besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
}
for besti+bestsize < ahi && bestj+bestsize < bhi &&
!m.b2j.isBJunk(m.b[bestj+bestsize]) &&
bytes.Equal(m.a[besti+bestsize], m.b[bestj+bestsize]) {
bestsize += 1
}
// Now that we have a wholly interesting match (albeit possibly
// empty!), we may as well suck up the matching junk on each
// side of it too. Can't think of a good reason not to, and it
// saves post-processing the (possibly considerable) expense of
// figuring out what to do with it. In the case of an empty
// interesting match, this is clearly the right thing to do,
// because no other kind of match is possible in the regions.
for besti > alo && bestj > blo && m.b2j.isBJunk(m.b[bestj-1]) &&
bytes.Equal(m.a[besti-1], m.b[bestj-1]) {
besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
}
for besti+bestsize < ahi && bestj+bestsize < bhi &&
m.b2j.isBJunk(m.b[bestj+bestsize]) &&
bytes.Equal(m.a[besti+bestsize], m.b[bestj+bestsize]) {
bestsize += 1
}
return Match{A: besti, B: bestj, Size: bestsize}
}
// GetMatchingBlocks returns a list of triples describing matching subsequences.
//
// Each triple is of the form (i, j, n), and means that
// a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in
// i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are
// adjacent triples in the list, and the second is not the last triple in the
// list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe
// adjacent equal blocks.
//
// The last triple is a dummy, (len(a), len(b), 0), and is the only
// triple with n==0.
func (m *SequenceMatcher) GetMatchingBlocks() []Match {
if m.matchingBlocks != nil {
return m.matchingBlocks
}
var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match
matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match {
match := m.findLongestMatch(alo, ahi, blo, bhi)
i, j, k := match.A, match.B, match.Size
if match.Size > 0 {
if alo < i && blo < j {
matched = matchBlocks(alo, i, blo, j, matched)
}
matched = append(matched, match)
if i+k < ahi && j+k < bhi {
matched = matchBlocks(i+k, ahi, j+k, bhi, matched)
}
}
return matched
}
matched := matchBlocks(0, len(m.a), 0, len(m.b), nil)
// It's possible that we have adjacent equal blocks in the
// matching_blocks list now.
var nonAdjacent []Match
i1, j1, k1 := 0, 0, 0
for _, b := range matched {
// Is this block adjacent to i1, j1, k1?
i2, j2, k2 := b.A, b.B, b.Size
if i1+k1 == i2 && j1+k1 == j2 {
// Yes, so collapse them -- this just increases the length of
// the first block by the length of the second, and the first
// block so lengthened remains the block to compare against.
k1 += k2
} else {
// Not adjacent. Remember the first block (k1==0 means it's
// the dummy we started with), and make the second block the
// new block to compare against.
if k1 > 0 {
nonAdjacent = append(nonAdjacent, Match{i1, j1, k1})
}
i1, j1, k1 = i2, j2, k2
}
}
if k1 > 0 {
nonAdjacent = append(nonAdjacent, Match{i1, j1, k1})
}
nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0})
m.matchingBlocks = nonAdjacent
return m.matchingBlocks
}
// GetOpCodes returns a list of 5-tuples describing how to turn a into b.
//
// Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple
// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the
// tuple preceding it, and likewise for j1 == the previous j2.
//
// The tags are characters, with these meanings:
//
// 'r' (replace): a[i1:i2] should be replaced by b[j1:j2]
//
// 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case.
//
// 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case.
//
// 'e' (equal): a[i1:i2] == b[j1:j2]
func (m *SequenceMatcher) GetOpCodes() []OpCode {
if m.opCodes != nil {
return m.opCodes
}
i, j := 0, 0
matching := m.GetMatchingBlocks()
opCodes := make([]OpCode, 0, len(matching))
for _, m := range matching {
// invariant: we've pumped out correct diffs to change
// a[:i] into b[:j], and the next matching block is
// a[ai:ai+size] == b[bj:bj+size]. So we need to pump
// out a diff to change a[i:ai] into b[j:bj], pump out
// the matching block, and move (i,j) beyond the match
ai, bj, size := m.A, m.B, m.Size
tag := byte(0)
if i < ai && j < bj {
tag = 'r'
} else if i < ai {
tag = 'd'
} else if j < bj {
tag = 'i'
}
if tag > 0 {
opCodes = append(opCodes, OpCode{tag, i, ai, j, bj})
}
i, j = ai+size, bj+size
// the list of matching blocks is terminated by a
// sentinel with size 0
if size > 0 {
opCodes = append(opCodes, OpCode{'e', ai, i, bj, j})
}
}
m.opCodes = opCodes
return m.opCodes
}
// GetGroupedOpCodes isolates change clusters by eliminating ranges with no changes.
//
// Return a generator of groups with up to n lines of context.
// Each group is in the same format as returned by GetOpCodes().
func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
if n < 0 {
n = 3
}
codes := m.GetOpCodes()
if len(codes) == 0 {
codes = []OpCode{{'e', 0, 1, 0, 1}}
}
// Fixup leading and trailing groups if they show no changes.
if codes[0].Tag == 'e' {
c := codes[0]
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2}
}
if codes[len(codes)-1].Tag == 'e' {
c := codes[len(codes)-1]
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)}
}
nn := n + n
var groups [][]OpCode
var group []OpCode
for _, c := range codes {
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
// End the current group and start a new one whenever
// there is a large range with no changes.
if c.Tag == 'e' && i2-i1 > nn {
group = append(group, OpCode{c.Tag, i1, min(i2, i1+n),
j1, min(j2, j1+n)})
groups = append(groups, group)
group = []OpCode{}
i1, j1 = max(i1, i2-n), max(j1, j2-n)
}
group = append(group, OpCode{c.Tag, i1, i2, j1, j2})
}
if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') {
groups = append(groups, group)
}
return groups
}
// Ratio returns a measure of the sequences' similarity (float in [0,1]).
//
// Where T is the total number of elements in both sequences, and
// M is the number of matches, this is 2.0*M / T.
// Note that this is 1 if the sequences are identical, and 0 if
// they have nothing in common.
//
// .Ratio() is expensive to compute if you haven't already computed
// .GetMatchingBlocks() or .GetOpCodes(), in which case you may
// want to try .QuickRatio() or .RealQuickRation() first to get an
// upper bound.
func (m *SequenceMatcher) Ratio() float64 {
matches := 0
for _, m := range m.GetMatchingBlocks() {
matches += m.Size
}
return calculateRatio(matches, len(m.a)+len(m.b))
}
// QuickRatio returns an upper bound on ratio() relatively quickly.
//
// This isn't defined beyond that it is an upper bound on .Ratio(), and
// is faster to compute.
func (m *SequenceMatcher) QuickRatio() float64 {
// viewing a and b as multisets, set matches to the cardinality
// of their intersection; this counts the number of matches
// without regard to order, so is clearly an upper bound. We do
// so on hashes of the lines themselves, so this might even be
// greater due hash collisions incurring false positives, but
// we don't care because we want an upper bound anyway.
if m.fullBCount == nil {
m.fullBCount = map[lineHash]int{}
for _, s := range m.b {
h := _hash(s)
m.fullBCount[h] = m.fullBCount[h] + 1
}
}
// avail[x] is the number of times x appears in 'b' less the
// number of times we've seen it in 'a' so far ... kinda
avail := map[lineHash]int{}
matches := 0
for _, s := range m.a {
h := _hash(s)
n, ok := avail[h]
if !ok {
n = m.fullBCount[h]
}
avail[h] = n - 1
if n > 0 {
matches += 1
}
}
return calculateRatio(matches, len(m.a)+len(m.b))
}
// RealQuickRatio returns an upper bound on ratio() very quickly.
//
// This isn't defined beyond that it is an upper bound on .Ratio(), and
// is faster to compute than either .Ratio() or .QuickRatio().
func (m *SequenceMatcher) RealQuickRatio() float64 {
la, lb := len(m.a), len(m.b)
return calculateRatio(min(la, lb), la+lb)
}
func count_leading(line []byte, ch byte) (count int) {
// Return number of `ch` characters at the start of `line`.
count = 0
n := len(line)
for (count < n) && (line[count] == ch) {
count++
}
return count
}
type DiffLine struct {
Tag byte
Line []byte
}
func NewDiffLine(tag byte, line []byte) (l DiffLine) {
l = DiffLine{}
l.Tag = tag
l.Line = line
return l
}
type Differ struct {
Linejunk func([]byte) bool
Charjunk func([]byte) bool
}
func NewDiffer() *Differ {
return &Differ{}
}
var MINUS = []byte("-")
var SPACE = []byte(" ")
var PLUS = []byte("+")
var CARET = []byte("^")
func (d *Differ) Compare(a [][]byte, b [][]byte) (diffs [][]byte, err error) {
// Compare two sequences of lines; generate the resulting delta.
// Each sequence must contain individual single-line strings ending with
// newlines. Such sequences can be obtained from the `readlines()` method
// of file-like objects. The delta generated also consists of newline-
// terminated strings, ready to be printed as-is via the writeline()
// method of a file-like object.
diffs = [][]byte{}
cruncher := NewMatcherWithJunk(a, b, true, d.Linejunk)
opcodes := cruncher.GetOpCodes()
for _, current := range opcodes {
alo := current.I1
ahi := current.I2
blo := current.J1
bhi := current.J2
var g [][]byte
if current.Tag == 'r' {
g, _ = d.FancyReplace(a, alo, ahi, b, blo, bhi)
} else if current.Tag == 'd' {
g = d.Dump(MINUS, a, alo, ahi)
} else if current.Tag == 'i' {
g = d.Dump(PLUS, b, blo, bhi)
} else if current.Tag == 'e' {
g = d.Dump(SPACE, a, alo, ahi)
} else {
return nil, fmt.Errorf("unknown tag %q", current.Tag)
}
diffs = append(diffs, g...)
}
return diffs, nil
}
func (d *Differ) StructuredDump(tag byte, x [][]byte, low int, high int) (out []DiffLine) {
size := high - low
out = make([]DiffLine, size)
for i := 0; i < size; i++ {
out[i] = NewDiffLine(tag, x[i+low])
}
return out
}
func (d *Differ) Dump(tag []byte, x [][]byte, low int, high int) (out [][]byte) {
// Generate comparison results for a same-tagged range.
sout := d.StructuredDump(tag[0], x, low, high)
out = make([][]byte, len(sout))
var bld bytes.Buffer
bld.Grow(1024)
for i, line := range sout {
bld.Reset()
bld.WriteByte(line.Tag)
bld.Write(SPACE)
bld.Write(line.Line)
out[i] = append(out[i], bld.Bytes()...)
}
return out
}
func (d *Differ) PlainReplace(a [][]byte, alo int, ahi int, b [][]byte, blo int, bhi int) (out [][]byte, err error) {
if !(alo < ahi) || !(blo < bhi) { // assertion
return nil, errors.New("low greater than or equal to high")
}
// dump the shorter block first -- reduces the burden on short-term
// memory if the blocks are of very different sizes
if bhi-blo < ahi-alo {
out = d.Dump(PLUS, b, blo, bhi)
out = append(out, d.Dump(MINUS, a, alo, ahi)...)
} else {
out = d.Dump(MINUS, a, alo, ahi)
out = append(out, d.Dump(PLUS, b, blo, bhi)...)
}
return out, nil
}
func (d *Differ) FancyReplace(a [][]byte, alo int, ahi int, b [][]byte, blo int, bhi int) (out [][]byte, err error) {
// When replacing one block of lines with another, search the blocks
// for *similar* lines; the best-matching pair (if any) is used as a
// synch point, and intraline difference marking is done on the
// similar pair. Lots of work, but often worth it.
// don't synch up unless the lines have a similarity score of at
// least cutoff; best_ratio tracks the best score seen so far
best_ratio := 0.74
cutoff := 0.75
cruncher := NewMatcherWithJunk(a, b, true, d.Charjunk)
eqi := -1 // 1st indices of equal lines (if any)
eqj := -1
out = [][]byte{}
// search for the pair that matches best without being identical
// (identical lines must be junk lines, & we don't want to synch up
// on junk -- unless we have to)
var best_i, best_j int
for j := blo; j < bhi; j++ {
bj := b[j]
cruncher.SetSeq2(listifyString(bj))
for i := alo; i < ahi; i++ {
ai := a[i]
if bytes.Equal(ai, bj) {
if eqi == -1 {
eqi = i
eqj = j
}
continue
}
cruncher.SetSeq1(listifyString(ai))
// computing similarity is expensive, so use the quick
// upper bounds first -- have seen this speed up messy
// compares by a factor of 3.
// note that ratio() is only expensive to compute the first
// time it's called on a sequence pair; the expensive part
// of the computation is cached by cruncher
if cruncher.RealQuickRatio() > best_ratio &&
cruncher.QuickRatio() > best_ratio &&
cruncher.Ratio() > best_ratio {
best_ratio = cruncher.Ratio()
best_i = i
best_j = j
}
}
}
if best_ratio < cutoff {
// no non-identical "pretty close" pair
if eqi == -1 {
// no identical pair either -- treat it as a straight replace
out, _ = d.PlainReplace(a, alo, ahi, b, blo, bhi)
return out, nil
}
// no close pair, but an identical pair -- synch up on that
best_i = eqi
best_j = eqj
best_ratio = 1.0
} else {
// there's a close pair, so forget the identical pair (if any)
eqi = -1
}
// a[best_i] very similar to b[best_j]; eqi is None iff they're not
// identical
// pump out diffs from before the synch point
out = append(out, d.fancyHelper(a, alo, best_i, b, blo, best_j)...)
// do intraline marking on the synch pair
aelt, belt := a[best_i], b[best_j]
if eqi == -1 {
// pump out a '-', '?', '+', '?' quad for the synched lines
var atags, btags []byte
cruncher.SetSeqs(listifyString(aelt), listifyString(belt))
opcodes := cruncher.GetOpCodes()
for _, current := range opcodes {
ai1 := current.I1
ai2 := current.I2
bj1 := current.J1
bj2 := current.J2
la, lb := ai2-ai1, bj2-bj1
if current.Tag == 'r' {
atags = append(atags, bytes.Repeat(CARET, la)...)
btags = append(btags, bytes.Repeat(CARET, lb)...)
} else if current.Tag == 'd' {
atags = append(atags, bytes.Repeat(MINUS, la)...)
} else if current.Tag == 'i' {
btags = append(btags, bytes.Repeat(PLUS, lb)...)
} else if current.Tag == 'e' {
atags = append(atags, bytes.Repeat(SPACE, la)...)
btags = append(btags, bytes.Repeat(SPACE, lb)...)
} else {
return nil, fmt.Errorf("unknown tag %q",
current.Tag)
}
}
out = append(out, d.QFormat(aelt, belt, atags, btags)...)
} else {
// the synch pair is identical
out = append(out, append([]byte{' ', ' '}, aelt...))
}
// pump out diffs from after the synch point
out = append(out, d.fancyHelper(a, best_i+1, ahi, b, best_j+1, bhi)...)
return out, nil
}
func (d *Differ) fancyHelper(a [][]byte, alo int, ahi int, b [][]byte, blo int, bhi int) (out [][]byte) {
if alo < ahi {
if blo < bhi {
out, _ = d.FancyReplace(a, alo, ahi, b, blo, bhi)
} else {
out = d.Dump(MINUS, a, alo, ahi)
}
} else if blo < bhi {
out = d.Dump(PLUS, b, blo, bhi)
} else {
out = [][]byte{}
}
return out
}
func (d *Differ) QFormat(aline []byte, bline []byte, atags []byte, btags []byte) (out [][]byte) {
// Format "?" output and deal with leading tabs.
// Can hurt, but will probably help most of the time.
common := min(count_leading(aline, '\t'), count_leading(bline, '\t'))
common = min(common, count_leading(atags[:common], ' '))
common = min(common, count_leading(btags[:common], ' '))
atags = bytes.TrimRightFunc(atags[common:], unicode.IsSpace)
btags = bytes.TrimRightFunc(btags[common:], unicode.IsSpace)
out = [][]byte{append([]byte("- "), aline...)}
if len(atags) > 0 {
t := make([]byte, 0, len(atags)+common+3)
t = append(t, []byte("? ")...)
for i := 0; i < common; i++ {
t = append(t, byte('\t'))
}
t = append(t, atags...)
t = append(t, byte('\n'))
out = append(out, t)
}
out = append(out, append([]byte("+ "), bline...))
if len(btags) > 0 {
t := make([]byte, 0, len(btags)+common+3)
t = append(t, []byte("? ")...)
for i := 0; i < common; i++ {
t = append(t, byte('\t'))
}
t = append(t, btags...)
t = append(t, byte('\n'))
out = append(out, t)
}
return out
}
// Convert range to the "ed" format
func formatRangeUnified(start, stop int) []byte {
// Per the diff spec at http://www.unix.org/single_unix_specification/
beginning := start + 1 // lines start numbering with one
length := stop - start
if length == 1 {
return []byte(fmt.Sprintf("%d", beginning))
}
if length == 0 {
beginning -= 1 // empty ranges begin at line just before the range
}
return []byte(fmt.Sprintf("%d,%d", beginning, length))
}
// UnifiedDiff contains unified diff parameters
type UnifiedDiff struct {
A [][]byte // First sequence lines
FromFile string // First file name
FromDate string // First file time
B [][]byte // Second sequence lines
ToFile string // Second file name
ToDate string // Second file time
Eol []byte // Headers end of line, defaults to LF
Context int // Number of context lines
}
// WriteUnifiedDiff compares two sequences of lines and generates the delta as a unified diff.
//
// Unified diffs are a compact way of showing line changes and a few
// lines of context. The number of context lines is set by 'n' which
// defaults to three.
//
// By default, the diff control lines (those with ---, +++, or @@) are
// created with a trailing newline. This is helpful so that inputs
// created from file.readlines() result in diffs that are suitable for
// file.writelines() since both the inputs and outputs have trailing
// newlines.
//
// For inputs that do not have trailing newlines, set the lineterm
// argument to "" so that the output will be uniformly newline free.
//
// The unidiff format normally has a header for filenames and modification
// times. Any or all of these may be specified using strings for
// 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.
// The modification times are normally expressed in the ISO 8601 format.
func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) error {
//buf := bufio.NewWriter(writer)
//defer buf.Flush()
var bld strings.Builder
bld.Reset()
wf := func(format string, args ...interface{}) error {
_, err := fmt.Fprintf(&bld, format, args...)
return err
}
ws := func(s []byte) error {
_, err := bld.Write(s)
return err
}
if len(diff.Eol) == 0 {
diff.Eol = []byte("\n")
}
started := false
m := NewMatcher(diff.A, diff.B)
for _, g := range m.GetGroupedOpCodes(diff.Context) {
if !started {
started = true
fromDate := ""
if len(diff.FromDate) > 0 {
fromDate = "\t" + diff.FromDate
}
toDate := ""
if len(diff.ToDate) > 0 {
toDate = "\t" + diff.ToDate
}
if diff.FromFile != "" || diff.ToFile != "" {
err := wf("--- %s%s%s", diff.FromFile, fromDate, diff.Eol)
if err != nil {
return err
}
err = wf("+++ %s%s%s", diff.ToFile, toDate, diff.Eol)
if err != nil {
return err
}
}
}
first, last := g[0], g[len(g)-1]
range1 := formatRangeUnified(first.I1, last.I2)
range2 := formatRangeUnified(first.J1, last.J2)
if err := wf("@@ -%s +%s @@%s", range1, range2, diff.Eol); err != nil {
return err
}
for _, c := range g {
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
if c.Tag == 'e' {
for _, line := range diff.A[i1:i2] {
if err := ws(SPACE); err != nil {
return err
}
if err := ws(line); err != nil {
return err
}
}
continue
}
if c.Tag == 'r' || c.Tag == 'd' {
for _, line := range diff.A[i1:i2] {
if err := ws(MINUS); err != nil {
return err
}
if err := ws(line); err != nil {
return err
}
}
}
if c.Tag == 'r' || c.Tag == 'i' {
for _, line := range diff.B[j1:j2] {
if err := ws(PLUS); err != nil {
return err
}
if err := ws(line); err != nil {
return err
}
}
}
}
}
buf := bufio.NewWriter(writer)
buf.WriteString(bld.String())
buf.Flush()
return nil
}
// GetUnifiedDiffString is like WriteUnifiedDiff but returns the diff a []byte.
func GetUnifiedDiffString(diff UnifiedDiff) ([]byte, error) {
w := &bytes.Buffer{}
err := WriteUnifiedDiff(w, diff)
return w.Bytes(), err
}
// Convert range to the "ed" format.
func formatRangeContext(start, stop int) []byte {
// Per the diff spec at http://www.unix.org/single_unix_specification/
beginning := start + 1 // lines start numbering with one
length := stop - start
if length == 0 {
beginning -= 1 // empty ranges begin at line just before the range
}
if length <= 1 {
return []byte(fmt.Sprintf("%d", beginning))
}
return []byte(fmt.Sprintf("%d,%d", beginning, beginning+length-1))
}
type ContextDiff UnifiedDiff
// WriteContextDiff compare two sequences of lines and generates the delta as a context diff.
//
// Context diffs are a compact way of showing line changes and a few
// lines of context. The number of context lines is set by diff.Context
// which defaults to three.
//
// By default, the diff control lines (those with *** or ---) are
// created with a trailing newline.
//
// For inputs that do not have trailing newlines, set the diff.Eol
// argument to "" so that the output will be uniformly newline free.
//
// The context diff format normally has a header for filenames and
// modification times. Any or all of these may be specified using
// strings for diff.FromFile, diff.ToFile, diff.FromDate, diff.ToDate.
// The modification times are normally expressed in the ISO 8601 format.
// If not specified, the strings default to blanks.
func WriteContextDiff(writer io.Writer, diff ContextDiff) error {
buf := bufio.NewWriter(writer)
defer buf.Flush()
var diffErr error
wf := func(format string, args ...interface{}) {
_, err := buf.WriteString(fmt.Sprintf(format, args...))
if diffErr == nil && err != nil {
diffErr = err
}
}
ws := func(s []byte) {
_, err := buf.Write(s)
if diffErr == nil && err != nil {
diffErr = err
}
}
if len(diff.Eol) == 0 {
diff.Eol = []byte("\n")
}
prefix := map[byte][]byte{
'i': []byte("+ "),
'd': []byte("- "),
'r': []byte("! "),
'e': []byte(" "),
}
started := false
m := NewMatcher(diff.A, diff.B)
for _, g := range m.GetGroupedOpCodes(diff.Context) {
if !started {
started = true
fromDate := ""
if len(diff.FromDate) > 0 {
fromDate = "\t" + diff.FromDate
}
toDate := ""
if len(diff.ToDate) > 0 {
toDate = "\t" + diff.ToDate
}
if diff.FromFile != "" || diff.ToFile != "" {
wf("*** %s%s%s", diff.FromFile, fromDate, diff.Eol)
wf("--- %s%s%s", diff.ToFile, toDate, diff.Eol)
}
}
first, last := g[0], g[len(g)-1]
ws([]byte("***************"))
ws(diff.Eol)
range1 := formatRangeContext(first.I1, last.I2)
wf("*** %s ****%s", range1, diff.Eol)
for _, c := range g {
if c.Tag == 'r' || c.Tag == 'd' {
for _, cc := range g {
if cc.Tag == 'i' {
continue
}
for _, line := range diff.A[cc.I1:cc.I2] {
ws(prefix[cc.Tag])
ws(line)
}
}
break
}
}
range2 := formatRangeContext(first.J1, last.J2)
wf("--- %s ----%s", range2, diff.Eol)
for _, c := range g {
if c.Tag == 'r' || c.Tag == 'i' {
for _, cc := range g {
if cc.Tag == 'd' {
continue
}
for _, line := range diff.B[cc.J1:cc.J2] {
ws(prefix[cc.Tag])
ws(line)
}
}
break
}
}
}
return diffErr
}
// GetContextDiffString Like WriteContextDiff but returns the diff a []byte.
func GetContextDiffString(diff ContextDiff) ([]byte, error) {
w := &bytes.Buffer{}
err := WriteContextDiff(w, diff)
return w.Bytes(), err
}
// SplitLines splits a []byte on "\n" while preserving them. The output can be used
// as input for UnifiedDiff and ContextDiff structures.
func SplitLines(s []byte) [][]byte {
lines := bytes.SplitAfter(s, []byte("\n"))
lines[len(lines)-1] = append(lines[len(lines)-1], '\n')
return lines
}
// Package difflib is a partial port of Python difflib module.
//
// It provides tools to compare sequences of strings and generate textual diffs.
//
// The following class and functions have been ported:
//
// - SequenceMatcher
//
// - unified_diff
//
// - context_diff
//
// Getting unified diffs was the main goal of the port. Keep in mind this code
// is mostly suitable to output text differences in a human friendly way, there
// are no guarantees generated diffs are consumable by patch(1).
package difflib
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"strings"
"unicode"
)
func calculateRatio(matches, length int) float64 {
if length > 0 {
return 2.0 * float64(matches) / float64(length)
}
return 1.0
}
func listifyString(str string) (lst []string) {
lst = make([]string, len(str))
for i, c := range str {
lst[i] = string(c)
}
return lst
}
type Match struct {
A int
B int
Size int
}
type OpCode struct {
Tag byte
I1 int
I2 int
J1 int
J2 int
}
// SequenceMatcher compares sequence of strings. The basic
// algorithm predates, and is a little fancier than, an algorithm
// published in the late 1980's by Ratcliff and Obershelp under the
// hyperbolic name "gestalt pattern matching". The basic idea is to find
// the longest contiguous matching subsequence that contains no "junk"
// elements (R-O doesn't address junk). The same idea is then applied
// recursively to the pieces of the sequences to the left and to the right
// of the matching subsequence. This does not yield minimal edit
// sequences, but does tend to yield matches that "look right" to people.
//
// SequenceMatcher tries to compute a "human-friendly diff" between two
// sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the
// longest *contiguous* & junk-free matching subsequence. That's what
// catches peoples' eyes. The Windows(tm) windiff has another interesting
// notion, pairing up elements that appear uniquely in each sequence.
// That, and the method here, appear to yield more intuitive difference
// reports than does diff. This method appears to be the least vulnerable
// to synching up on blocks of "junk lines", though (like blank lines in
// ordinary text files, or maybe "<P>" lines in HTML files). That may be
// because this is the only method of the 3 that has a *concept* of
// "junk" <wink>.
//
// Timing: Basic R-O is cubic time worst case and quadratic time expected
// case. SequenceMatcher is quadratic time for the worst case and has
// expected-case behavior dependent in a complicated way on how many
// elements the sequences have in common; best case time is linear.
type SequenceMatcher struct {
a []string
b []string
b2j map[string][]int
IsJunk func(string) bool
autoJunk bool
bJunk map[string]bool
matchingBlocks []Match
fullBCount map[string]int
bPopular map[string]bool
opCodes []OpCode
}
func NewMatcher(a, b []string) *SequenceMatcher {
m := SequenceMatcher{autoJunk: true}
m.SetSeqs(a, b)
return &m
}
func NewMatcherWithJunk(a, b []string, autoJunk bool,
isJunk func(string) bool) *SequenceMatcher {
m := SequenceMatcher{IsJunk: isJunk, autoJunk: autoJunk}
m.SetSeqs(a, b)
return &m
}
// SetSeqs sets two sequences to be compared.
func (m *SequenceMatcher) SetSeqs(a, b []string) {
m.SetSeq1(a)
m.SetSeq2(b)
}
// SetSeq1 sets the first sequence to be compared. The second sequence to be compared is
// not changed.
//
// SequenceMatcher computes and caches detailed information about the second
// sequence, so if you want to compare one sequence S against many sequences,
// use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other
// sequences.
//
// See also SetSeqs() and SetSeq2().
func (m *SequenceMatcher) SetSeq1(a []string) {
if &a == &m.a {
return
}
m.a = a
m.matchingBlocks = nil
m.opCodes = nil
}
// SetSeq2 sets the second sequence to be compared. The first sequence to be compared is
// not changed.
func (m *SequenceMatcher) SetSeq2(b []string) {
if &b == &m.b {
return
}
m.b = b
m.matchingBlocks = nil
m.opCodes = nil
m.fullBCount = nil
m.chainB()
}
func (m *SequenceMatcher) chainB() {
// Populate line -> index mapping
b2j := map[string][]int{}
junk := map[string]bool{}
popular := map[string]bool{}
ntest := len(m.b)
if m.autoJunk && ntest >= 200 {
ntest = ntest/100 + 1
}
for i, s := range m.b {
if !junk[s] {
if m.IsJunk != nil && m.IsJunk(s) {
junk[s] = true
} else if !popular[s] {
ids := append(b2j[s], i)
if len(ids) <= ntest {
b2j[s] = ids
} else {
delete(b2j, s)
popular[s] = true
}
}
}
}
m.b2j = b2j
m.bJunk = junk
m.bPopular = popular
}
func (m *SequenceMatcher) isBJunk(s string) bool {
_, ok := m.bJunk[s]
return ok
}
// Find longest matching block in a[alo:ahi] and b[blo:bhi].
//
// If IsJunk is not defined:
//
// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where
//
// alo <= i <= i+k <= ahi
// blo <= j <= j+k <= bhi
//
// and for all (i',j',k') meeting those conditions,
//
// k >= k'
// i <= i'
// and if i == i', j <= j'
//
// In other words, of all maximal matching blocks, return one that
// starts earliest in a, and of all those maximal matching blocks that
// start earliest in a, return the one that starts earliest in b.
//
// If IsJunk is defined, first the longest matching block is
// determined as above, but with the additional restriction that no
// junk element appears in the block. Then that block is extended as
// far as possible by matching (only) junk elements on both sides. So
// the resulting block never matches on junk except as identical junk
// happens to be adjacent to an "interesting" match.
//
// If no blocks match, return (alo, blo, 0).
func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match {
// CAUTION: stripping common prefix or suffix would be incorrect.
// E.g.,
// ab
// acab
// Longest matching block is "ab", but if common prefix is
// stripped, it's "a" (tied with "b"). UNIX(tm) diff does so
// strip, so ends up claiming that ab is changed to acab by
// inserting "ca" in the middle. That's minimal but unintuitive:
// "it's obvious" that someone inserted "ac" at the front.
// Windiff ends up at the same place as diff, but by pairing up
// the unique 'b's and then matching the first two 'a's.
besti, bestj, bestsize := alo, blo, 0
// find longest junk-free match
// during an iteration of the loop, j2len[j] = length of longest
// junk-free match ending with a[i-1] and b[j]
N := bhi - blo
j2len := make([]int, N)
newj2len := make([]int, N)
var indices []int
for i := alo; i != ahi; i++ {
// look at all instances of a[i] in b; note that because
// b2j has no junk keys, the loop is skipped if a[i] is junk
newindices := m.b2j[m.a[i]]
for _, j := range newindices {
// a[i] matches b[j]
if j < blo {
continue
}
if j >= bhi {
break
}
k := 1
if j > blo {
k = j2len[j-1-blo] + 1
}
newj2len[j-blo] = k
if k > bestsize {
besti, bestj, bestsize = i-k+1, j-k+1, k
}
}
// j2len = newj2len, clear and reuse j2len as newj2len
for _, j := range indices {
if j < blo {
continue
}
if j >= bhi {
break
}
j2len[j-blo] = 0
}
indices = newindices
j2len, newj2len = newj2len, j2len
}
// Extend the best by non-junk elements on each end. In particular,
// "popular" non-junk elements aren't in b2j, which greatly speeds
// the inner loop above, but also means "the best" match so far
// doesn't contain any junk *or* popular non-junk elements.
for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) &&
m.a[besti-1] == m.b[bestj-1] {
besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
}
for besti+bestsize < ahi && bestj+bestsize < bhi &&
!m.isBJunk(m.b[bestj+bestsize]) &&
m.a[besti+bestsize] == m.b[bestj+bestsize] {
bestsize += 1
}
// Now that we have a wholly interesting match (albeit possibly
// empty!), we may as well suck up the matching junk on each
// side of it too. Can't think of a good reason not to, and it
// saves post-processing the (possibly considerable) expense of
// figuring out what to do with it. In the case of an empty
// interesting match, this is clearly the right thing to do,
// because no other kind of match is possible in the regions.
for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) &&
m.a[besti-1] == m.b[bestj-1] {
besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
}
for besti+bestsize < ahi && bestj+bestsize < bhi &&
m.isBJunk(m.b[bestj+bestsize]) &&
m.a[besti+bestsize] == m.b[bestj+bestsize] {
bestsize += 1
}
return Match{A: besti, B: bestj, Size: bestsize}
}
// GetMatchingBlocks returns a list of triples describing matching subsequences.
//
// Each triple is of the form (i, j, n), and means that
// a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in
// i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are
// adjacent triples in the list, and the second is not the last triple in the
// list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe
// adjacent equal blocks.
//
// The last triple is a dummy, (len(a), len(b), 0), and is the only
// triple with n==0.
func (m *SequenceMatcher) GetMatchingBlocks() []Match {
if m.matchingBlocks != nil {
return m.matchingBlocks
}
var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match
matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match {
match := m.findLongestMatch(alo, ahi, blo, bhi)
i, j, k := match.A, match.B, match.Size
if match.Size > 0 {
if alo < i && blo < j {
matched = matchBlocks(alo, i, blo, j, matched)
}
matched = append(matched, match)
if i+k < ahi && j+k < bhi {
matched = matchBlocks(i+k, ahi, j+k, bhi, matched)
}
}
return matched
}
matched := matchBlocks(0, len(m.a), 0, len(m.b), nil)
// It's possible that we have adjacent equal blocks in the
// matching_blocks list now.
var nonAdjacent []Match
i1, j1, k1 := 0, 0, 0
for _, b := range matched {
// Is this block adjacent to i1, j1, k1?
i2, j2, k2 := b.A, b.B, b.Size
if i1+k1 == i2 && j1+k1 == j2 {
// Yes, so collapse them -- this just increases the length of
// the first block by the length of the second, and the first
// block so lengthened remains the block to compare against.
k1 += k2
} else {
// Not adjacent. Remember the first block (k1==0 means it's
// the dummy we started with), and make the second block the
// new block to compare against.
if k1 > 0 {
nonAdjacent = append(nonAdjacent, Match{i1, j1, k1})
}
i1, j1, k1 = i2, j2, k2
}
}
if k1 > 0 {
nonAdjacent = append(nonAdjacent, Match{i1, j1, k1})
}
nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0})
m.matchingBlocks = nonAdjacent
return m.matchingBlocks
}
// GetOpCodes returns a list of 5-tuples describing how to turn a into b.
//
// Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple
// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the
// tuple preceding it, and likewise for j1 == the previous j2.
//
// The tags are characters, with these meanings:
//
// 'r' (replace): a[i1:i2] should be replaced by b[j1:j2]
//
// 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case.
//
// 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case.
//
// 'e' (equal): a[i1:i2] == b[j1:j2]
func (m *SequenceMatcher) GetOpCodes() []OpCode {
if m.opCodes != nil {
return m.opCodes
}
i, j := 0, 0
matching := m.GetMatchingBlocks()
opCodes := make([]OpCode, 0, len(matching))
for _, m := range matching {
// invariant: we've pumped out correct diffs to change
// a[:i] into b[:j], and the next matching block is
// a[ai:ai+size] == b[bj:bj+size]. So we need to pump
// out a diff to change a[i:ai] into b[j:bj], pump out
// the matching block, and move (i,j) beyond the match
ai, bj, size := m.A, m.B, m.Size
tag := byte(0)
if i < ai && j < bj {
tag = 'r'
} else if i < ai {
tag = 'd'
} else if j < bj {
tag = 'i'
}
if tag > 0 {
opCodes = append(opCodes, OpCode{tag, i, ai, j, bj})
}
i, j = ai+size, bj+size
// the list of matching blocks is terminated by a
// sentinel with size 0
if size > 0 {
opCodes = append(opCodes, OpCode{'e', ai, i, bj, j})
}
}
m.opCodes = opCodes
return m.opCodes
}
// GetGroupedOpCodes isolates change clusters by eliminating ranges with no changes.
//
// Return a generator of groups with up to n lines of context.
// Each group is in the same format as returned by GetOpCodes().
func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
if n < 0 {
n = 3
}
codes := m.GetOpCodes()
if len(codes) == 0 {
codes = []OpCode{{'e', 0, 1, 0, 1}}
}
// Fixup leading and trailing groups if they show no changes.
if codes[0].Tag == 'e' {
c := codes[0]
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2}
}
if codes[len(codes)-1].Tag == 'e' {
c := codes[len(codes)-1]
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)}
}
nn := n + n
var (
groups [][]OpCode
group []OpCode
)
for _, c := range codes {
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
// End the current group and start a new one whenever
// there is a large range with no changes.
if c.Tag == 'e' && i2-i1 > nn {
group = append(group, OpCode{c.Tag, i1, min(i2, i1+n),
j1, min(j2, j1+n)})
groups = append(groups, group)
group = []OpCode{}
i1, j1 = max(i1, i2-n), max(j1, j2-n)
}
group = append(group, OpCode{c.Tag, i1, i2, j1, j2})
}
if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') {
groups = append(groups, group)
}
return groups
}
// Ratio returns a measure of the sequences' similarity (float in [0,1]).
//
// Where T is the total number of elements in both sequences, and
// M is the number of matches, this is 2.0*M / T.
// Note that this is 1 if the sequences are identical, and 0 if
// they have nothing in common.
//
// .Ratio() is expensive to compute if you haven't already computed
// .GetMatchingBlocks() or .GetOpCodes(), in which case you may
// want to try .QuickRatio() or .RealQuickRation() first to get an
// upper bound.
func (m *SequenceMatcher) Ratio() float64 {
matches := 0
for _, m := range m.GetMatchingBlocks() {
matches += m.Size
}
return calculateRatio(matches, len(m.a)+len(m.b))
}
// QuickRatio returns an upper bound on ratio() relatively quickly.
//
// This isn't defined beyond that it is an upper bound on .Ratio(), and
// is faster to compute.
func (m *SequenceMatcher) QuickRatio() float64 {
// viewing a and b as multisets, set matches to the cardinality
// of their intersection; this counts the number of matches
// without regard to order, so is clearly an upper bound
if m.fullBCount == nil {
m.fullBCount = map[string]int{}
for _, s := range m.b {
m.fullBCount[s] = m.fullBCount[s] + 1
}
}
// avail[x] is the number of times x appears in 'b' less the
// number of times we've seen it in 'a' so far ... kinda
avail := map[string]int{}
matches := 0
for _, s := range m.a {
n, ok := avail[s]
if !ok {
n = m.fullBCount[s]
}
avail[s] = n - 1
if n > 0 {
matches += 1
}
}
return calculateRatio(matches, len(m.a)+len(m.b))
}
// RealQuickRatio returns an upper bound on ratio() very quickly.
//
// This isn't defined beyond that it is an upper bound on .Ratio(), and
// is faster to compute than either .Ratio() or .QuickRatio().
func (m *SequenceMatcher) RealQuickRatio() float64 {
la, lb := len(m.a), len(m.b)
return calculateRatio(min(la, lb), la+lb)
}
func count_leading(line string, ch byte) (count int) {
// Return number of `ch` characters at the start of `line`.
count = 0
n := len(line)
for (count < n) && (line[count] == ch) {
count++
}
return count
}
type DiffLine struct {
Tag byte
Line string
}
func NewDiffLine(tag byte, line string) (l DiffLine) {
l = DiffLine{}
l.Tag = tag
l.Line = line
return l
}
type Differ struct {
Linejunk func(string) bool
Charjunk func(string) bool
}
func NewDiffer() *Differ {
return &Differ{}
}
func (d *Differ) Compare(a []string, b []string) (diffs []string, err error) {
// Compare two sequences of lines; generate the resulting delta.
// Each sequence must contain individual single-line strings ending with
// newlines. Such sequences can be obtained from the `readlines()` method
// of file-like objects. The delta generated also consists of newline-
// terminated strings, ready to be printed as-is via the writeline()
// method of a file-like object.
diffs = []string{}
cruncher := NewMatcherWithJunk(a, b, true, d.Linejunk)
opcodes := cruncher.GetOpCodes()
for _, current := range opcodes {
alo := current.I1
ahi := current.I2
blo := current.J1
bhi := current.J2
var g []string
if current.Tag == 'r' {
g, _ = d.FancyReplace(a, alo, ahi, b, blo, bhi)
} else if current.Tag == 'd' {
g = d.Dump("-", a, alo, ahi)
} else if current.Tag == 'i' {
g = d.Dump("+", b, blo, bhi)
} else if current.Tag == 'e' {
g = d.Dump(" ", a, alo, ahi)
} else {
return nil, fmt.Errorf("unknown tag %q", current.Tag)
}
diffs = append(diffs, g...)
}
return diffs, nil
}
func (d *Differ) StructuredDump(tag byte, x []string, low int, high int) (out []DiffLine) {
size := high - low
out = make([]DiffLine, size)
for i := 0; i < size; i++ {
out[i] = NewDiffLine(tag, x[i+low])
}
return out
}
func (d *Differ) Dump(tag string, x []string, low int, high int) (out []string) {
// Generate comparison results for a same-tagged range.
sout := d.StructuredDump(tag[0], x, low, high)
out = make([]string, len(sout))
var bld strings.Builder
bld.Grow(1024)
for i, line := range sout {
bld.Reset()
bld.WriteByte(line.Tag)
bld.WriteString(" ")
bld.WriteString(line.Line)
out[i] = bld.String()
}
return out
}
func (d *Differ) PlainReplace(a []string, alo int, ahi int, b []string, blo int, bhi int) (out []string, err error) {
if !(alo < ahi) || !(blo < bhi) { // assertion
return nil, errors.New("low greater than or equal to high")
}
// dump the shorter block first -- reduces the burden on short-term
// memory if the blocks are of very different sizes
if bhi-blo < ahi-alo {
out = d.Dump("+", b, blo, bhi)
out = append(out, d.Dump("-", a, alo, ahi)...)
} else {
out = d.Dump("-", a, alo, ahi)
out = append(out, d.Dump("+", b, blo, bhi)...)
}
return out, nil
}
func (d *Differ) FancyReplace(a []string, alo int, ahi int, b []string, blo int, bhi int) (out []string, err error) {
// When replacing one block of lines with another, search the blocks
// for *similar* lines; the best-matching pair (if any) is used as a
// synch point, and intraline difference marking is done on the
// similar pair. Lots of work, but often worth it.
// don't synch up unless the lines have a similarity score of at
// least cutoff; best_ratio tracks the best score seen so far
best_ratio := 0.74
cutoff := 0.75
cruncher := NewMatcherWithJunk(a, b, true, d.Charjunk)
eqi := -1 // 1st indices of equal lines (if any)
eqj := -1
out = []string{}
// search for the pair that matches best without being identical
// (identical lines must be junk lines, & we don't want to synch up
// on junk -- unless we have to)
var best_i, best_j int
for j := blo; j < bhi; j++ {
bj := b[j]
cruncher.SetSeq2(listifyString(bj))
for i := alo; i < ahi; i++ {
ai := a[i]
if ai == bj {
if eqi == -1 {
eqi = i
eqj = j
}
continue
}
cruncher.SetSeq1(listifyString(ai))
// computing similarity is expensive, so use the quick
// upper bounds first -- have seen this speed up messy
// compares by a factor of 3.
// note that ratio() is only expensive to compute the first
// time it's called on a sequence pair; the expensive part
// of the computation is cached by cruncher
if cruncher.RealQuickRatio() > best_ratio &&
cruncher.QuickRatio() > best_ratio &&
cruncher.Ratio() > best_ratio {
best_ratio = cruncher.Ratio()
best_i = i
best_j = j
}
}
}
if best_ratio < cutoff {
// no non-identical "pretty close" pair
if eqi == -1 {
// no identical pair either -- treat it as a straight replace
out, _ = d.PlainReplace(a, alo, ahi, b, blo, bhi)
return out, nil
}
// no close pair, but an identical pair -- synch up on that
best_i = eqi
best_j = eqj
best_ratio = 1.0
} else {
// there's a close pair, so forget the identical pair (if any)
eqi = -1
}
// a[best_i] very similar to b[best_j]; eqi is None iff they're not
// identical
// pump out diffs from before the synch point
out = append(out, d.fancyHelper(a, alo, best_i, b, blo, best_j)...)
// do intraline marking on the synch pair
aelt, belt := a[best_i], b[best_j]
if eqi == -1 {
// pump out a '-', '?', '+', '?' quad for the synched lines
var atags, btags string
cruncher.SetSeqs(listifyString(aelt), listifyString(belt))
opcodes := cruncher.GetOpCodes()
for _, current := range opcodes {
ai1 := current.I1
ai2 := current.I2
bj1 := current.J1
bj2 := current.J2
la, lb := ai2-ai1, bj2-bj1
if current.Tag == 'r' {
atags += strings.Repeat("^", la)
btags += strings.Repeat("^", lb)
} else if current.Tag == 'd' {
atags += strings.Repeat("-", la)
} else if current.Tag == 'i' {
btags += strings.Repeat("+", lb)
} else if current.Tag == 'e' {
atags += strings.Repeat(" ", la)
btags += strings.Repeat(" ", lb)
} else {
return nil, fmt.Errorf("unknown tag %q",
current.Tag)
}
}
out = append(out, d.QFormat(aelt, belt, atags, btags)...)
} else {
// the synch pair is identical
out = append(out, " "+aelt)
}
// pump out diffs from after the synch point
out = append(out, d.fancyHelper(a, best_i+1, ahi, b, best_j+1, bhi)...)
return out, nil
}
func (d *Differ) fancyHelper(a []string, alo int, ahi int, b []string, blo int, bhi int) (out []string) {
if alo < ahi {
if blo < bhi {
out, _ = d.FancyReplace(a, alo, ahi, b, blo, bhi)
} else {
out = d.Dump("-", a, alo, ahi)
}
} else if blo < bhi {
out = d.Dump("+", b, blo, bhi)
} else {
out = []string{}
}
return out
}
func (d *Differ) QFormat(aline string, bline string, atags string, btags string) (out []string) {
// Format "?" output and deal with leading tabs.
// Can hurt, but will probably help most of the time.
common := min(count_leading(aline, '\t'), count_leading(bline, '\t'))
common = min(common, count_leading(atags[:common], ' '))
common = min(common, count_leading(btags[:common], ' '))
atags = strings.TrimRightFunc(atags[common:], unicode.IsSpace)
btags = strings.TrimRightFunc(btags[common:], unicode.IsSpace)
out = []string{"- " + aline}
if len(atags) > 0 {
out = append(out, fmt.Sprintf("? %s%s\n",
strings.Repeat("\t", common), atags))
}
out = append(out, "+ "+bline)
if len(btags) > 0 {
out = append(out, fmt.Sprintf("? %s%s\n",
strings.Repeat("\t", common), btags))
}
return out
}
// Convert range to the "ed" format
func formatRangeUnified(start, stop int) string {
// Per the diff spec at http://www.unix.org/single_unix_specification/
beginning := start + 1 // lines start numbering with one
length := stop - start
if length == 1 {
return fmt.Sprintf("%d", beginning)
}
if length == 0 {
beginning -= 1 // empty ranges begin at line just before the range
}
return fmt.Sprintf("%d,%d", beginning, length)
}
// LineDiffParams contains unified diff parameters
type LineDiffParams struct {
A []string // First sequence lines
FromFile string // First file name
FromDate string // First file time
B []string // Second sequence lines
ToFile string // Second file name
ToDate string // Second file time
Eol string // Headers end of line, defaults to LF
Context int // Number of context lines
AutoJunk bool // If true, use autojunking
IsJunkLine func(string) bool // How to spot junk lines
}
// WriteUnifiedDiff compares two sequences of lines and generates the delta as a unified diff.
//
// Unified diffs are a compact way of showing line changes and a few
// lines of context. The number of context lines is set by 'n' which
// defaults to three.
//
// By default, the diff control lines (those with ---, +++, or @@) are
// created with a trailing newline. This is helpful so that inputs
// created from file.readlines() result in diffs that are suitable for
// file.writelines() since both the inputs and outputs have trailing
// newlines.
//
// For inputs that do not have trailing newlines, set the lineterm
// argument to "" so that the output will be uniformly newline free.
//
// The unidiff format normally has a header for filenames and modification
// times. Any or all of these may be specified using strings for
// 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.
// The modification times are normally expressed in the ISO 8601 format.
func WriteUnifiedDiff(writer io.Writer, diff LineDiffParams) error {
//buf := bufio.NewWriter(writer)
//defer buf.Flush()
var bld strings.Builder
bld.Reset()
wf := func(format string, args ...any) error {
_, err := fmt.Fprintf(&bld, format, args...)
return err
}
ws := func(s string) error {
_, err := bld.WriteString(s)
return err
}
if len(diff.Eol) == 0 {
diff.Eol = "\n"
}
started := false
m := NewMatcher(diff.A, diff.B)
if diff.AutoJunk || diff.IsJunkLine != nil {
m = NewMatcherWithJunk(diff.A, diff.B, diff.AutoJunk, diff.IsJunkLine)
}
for _, g := range m.GetGroupedOpCodes(diff.Context) {
if !started {
started = true
fromDate := ""
if len(diff.FromDate) > 0 {
fromDate = "\t" + diff.FromDate
}
toDate := ""
if len(diff.ToDate) > 0 {
toDate = "\t" + diff.ToDate
}
if diff.FromFile != "" || diff.ToFile != "" {
err := wf("--- %s%s%s", diff.FromFile, fromDate, diff.Eol)
if err != nil {
return err
}
err = wf("+++ %s%s%s", diff.ToFile, toDate, diff.Eol)
if err != nil {
return err
}
}
}
first, last := g[0], g[len(g)-1]
range1 := formatRangeUnified(first.I1, last.I2)
range2 := formatRangeUnified(first.J1, last.J2)
if err := wf("@@ -%s +%s @@%s", range1, range2, diff.Eol); err != nil {
return err
}
for _, c := range g {
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
if c.Tag == 'e' {
for _, line := range diff.A[i1:i2] {
if err := ws(" " + line); err != nil {
return err
}
}
continue
}
if c.Tag == 'r' || c.Tag == 'd' {
for _, line := range diff.A[i1:i2] {
if err := ws("-" + line); err != nil {
return err
}
}
}
if c.Tag == 'r' || c.Tag == 'i' {
for _, line := range diff.B[j1:j2] {
if err := ws("+" + line); err != nil {
return err
}
}
}
}
}
buf := bufio.NewWriter(writer)
buf.WriteString(bld.String())
buf.Flush()
return nil
}
// GetUnifiedDiffString is like WriteUnifiedDiff but returns the diff a string.
func GetUnifiedDiffString(diff LineDiffParams) (string, error) {
w := &bytes.Buffer{}
err := WriteUnifiedDiff(w, diff)
return w.String(), err
}
// Convert range to the "ed" format.
func formatRangeContext(start, stop int) string {
// Per the diff spec at http://www.unix.org/single_unix_specification/
beginning := start + 1 // lines start numbering with one
length := stop - start
if length == 0 {
beginning -= 1 // empty ranges begin at line just before the range
}
if length <= 1 {
return fmt.Sprintf("%d", beginning)
}
return fmt.Sprintf("%d,%d", beginning, beginning+length-1)
}
// ContextDiff is for backward compatibility. Ugh.
type ContextDiff = LineDiffParams
type UnifiedDiff = LineDiffParams
// WriteContextDiff compares two sequences of lines and generates the delta as a context diff.
//
// Context diffs are a compact way of showing line changes and a few
// lines of context. The number of context lines is set by diff.Context
// which defaults to three.
//
// By default, the diff control lines (those with *** or ---) are
// created with a trailing newline.
//
// For inputs that do not have trailing newlines, set the diff.Eol
// argument to "" so that the output will be uniformly newline free.
//
// The context diff format normally has a header for filenames and
// modification times. Any or all of these may be specified using
// strings for diff.FromFile, diff.ToFile, diff.FromDate, diff.ToDate.
// The modification times are normally expressed in the ISO 8601 format.
// If not specified, the strings default to blanks.
func WriteContextDiff(writer io.Writer, diff LineDiffParams) error {
buf := bufio.NewWriter(writer)
defer buf.Flush()
var diffErr error
wf := func(format string, args ...any) {
_, err := buf.WriteString(fmt.Sprintf(format, args...))
if diffErr == nil && err != nil {
diffErr = err
}
}
ws := func(s string) {
_, err := buf.WriteString(s)
if diffErr == nil && err != nil {
diffErr = err
}
}
if len(diff.Eol) == 0 {
diff.Eol = "\n"
}
prefix := map[byte]string{
'i': "+ ",
'd': "- ",
'r': "! ",
'e': " ",
}
started := false
m := NewMatcher(diff.A, diff.B)
if diff.AutoJunk || diff.IsJunkLine != nil {
m = NewMatcherWithJunk(diff.A, diff.B, diff.AutoJunk, diff.IsJunkLine)
}
for _, g := range m.GetGroupedOpCodes(diff.Context) {
if !started {
started = true
fromDate := ""
if len(diff.FromDate) > 0 {
fromDate = "\t" + diff.FromDate
}
toDate := ""
if len(diff.ToDate) > 0 {
toDate = "\t" + diff.ToDate
}
if diff.FromFile != "" || diff.ToFile != "" {
wf("*** %s%s%s", diff.FromFile, fromDate, diff.Eol)
wf("--- %s%s%s", diff.ToFile, toDate, diff.Eol)
}
}
first, last := g[0], g[len(g)-1]
ws("***************" + diff.Eol)
range1 := formatRangeContext(first.I1, last.I2)
wf("*** %s ****%s", range1, diff.Eol)
for _, c := range g {
if c.Tag == 'r' || c.Tag == 'd' {
for _, cc := range g {
if cc.Tag == 'i' {
continue
}
for _, line := range diff.A[cc.I1:cc.I2] {
ws(prefix[cc.Tag] + line)
}
}
break
}
}
range2 := formatRangeContext(first.J1, last.J2)
wf("--- %s ----%s", range2, diff.Eol)
for _, c := range g {
if c.Tag == 'r' || c.Tag == 'i' {
for _, cc := range g {
if cc.Tag == 'd' {
continue
}
for _, line := range diff.B[cc.J1:cc.J2] {
ws(prefix[cc.Tag] + line)
}
}
break
}
}
}
return diffErr
}
// GetContextDiffString is like WriteContextDiff but returns the diff as a string.
func GetContextDiffString(diff LineDiffParams) (string, error) {
w := &bytes.Buffer{}
err := WriteContextDiff(w, diff)
return w.String(), err
}
// SplitLines splits a string on "\n" while preserving them. The output can be used
// as input for LineDiffParams.
func SplitLines(s string) []string {
lines := strings.SplitAfter(s, "\n")
lines[len(lines)-1] += "\n"
return lines
}
package tester
import (
"math/rand"
"time"
)
func prepareStrings(seed int64) (A, B []string) {
if seed == -1 {
seed = time.Now().UnixNano()
}
rand.Seed(seed)
// Generate 4000 random lines
lines := [4000]string{}
for i := range lines {
l := rand.Intn(100)
p := make([]byte, l)
rand.Read(p)
lines[i] = string(p)
}
// Generate two 4000 lines documents by picking some lines at random
A = make([]string, 4000)
B = make([]string, len(A))
for i := range A {
// make the first 50 lines more likely to appear
if rand.Intn(100) < 40 {
A[i] = lines[rand.Intn(50)]
} else {
A[i] = lines[rand.Intn(len(lines))]
}
if rand.Intn(100) < 40 {
B[i] = lines[rand.Intn(50)]
} else {
B[i] = lines[rand.Intn(len(lines))]
}
}
// Do some copies from A to B
maxcopy := rand.Intn(len(A)-1) + 1
for copied, tocopy := 0, rand.Intn(2*len(A)/3); copied < tocopy; {
l := rand.Intn(rand.Intn(maxcopy-1) + 1)
for a, b, n := rand.Intn(len(A)), rand.Intn(len(B)), 0; a < len(A) && b < len(B) && n < l; a, b, n = a+1, b+1, n+1 {
B[b] = A[a]
copied++
}
}
// And some from B to A
for copied, tocopy := 0, rand.Intn(2*len(A)/3); copied < tocopy; {
l := rand.Intn(rand.Intn(maxcopy-1) + 1)
for a, b, n := rand.Intn(len(A)), rand.Intn(len(B)), 0; a < len(A) && b < len(B) && n < l; a, b, n = a+1, b+1, n+1 {
A[a] = B[b]
copied++
}
}
return
}
func PrepareStringsToDiff(count, seed int) (As, Bs [][]string) {
As = make([][]string, count)
Bs = make([][]string, count)
for i := range As {
As[i], Bs[i] = prepareStrings(int64(i + seed))
}
return
}
// 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 texteditor
//go:generate core generate
import (
"image"
"slices"
"sync"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/colors"
"cogentcore.org/core/core"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/texteditor/highlighting"
"cogentcore.org/core/texteditor/text"
)
// TODO: move these into an editor settings object
var (
// Maximum amount of clipboard history to retain
clipboardHistoryMax = 100 // `default:"100" min:"0" max:"1000" step:"5"`
// text buffer max lines to use diff-based revert to more quickly update e.g., after file has been reformatted
diffRevertLines = 10000 // `default:"10000" min:"0" step:"1000"`
// text buffer max diffs to use diff-based revert to more quickly update e.g., after file has been reformatted -- if too many differences, just revert
diffRevertDiffs = 20 // `default:"20" min:"0" step:"1"`
)
// Editor is a widget for editing multiple lines of complicated text (as compared to
// [core.TextField] for a single line of simple text). The Editor is driven by a [Buffer]
// buffer which contains all the text, and manages all the edits,
// sending update events out to the editors.
//
// Use NeedsRender to drive an render update for any change that does
// not change the line-level layout of the text.
// Use NeedsLayout whenever there are changes across lines that require
// re-layout of the text. This sets the Widget NeedsRender flag and triggers
// layout during that render.
//
// Multiple editors can be attached to a given buffer. All updating in the
// Editor should be within a single goroutine, as it would require
// extensive protections throughout code otherwise.
type Editor struct { //core:embedder
core.Frame
// Buffer is the text buffer being edited.
Buffer *Buffer `set:"-" json:"-" xml:"-"`
// CursorWidth is the width of the cursor.
// This should be set in Stylers like all other style properties.
CursorWidth units.Value
// LineNumberColor is the color used for the side bar containing the line numbers.
// This should be set in Stylers like all other style properties.
LineNumberColor image.Image
// SelectColor is the color used for the user text selection background color.
// This should be set in Stylers like all other style properties.
SelectColor image.Image
// HighlightColor is the color used for the text highlight background color (like in find).
// This should be set in Stylers like all other style properties.
HighlightColor image.Image
// CursorColor is the color used for the text editor cursor bar.
// This should be set in Stylers like all other style properties.
CursorColor image.Image
// NumLines is the number of lines in the view, synced with the [Buffer] after edits,
// but always reflects the storage size of renders etc.
NumLines int `set:"-" display:"-" json:"-" xml:"-"`
// renders is a slice of paint.Text representing the renders of the text lines,
// with one render per line (each line could visibly wrap-around, so these are logical lines, not display lines).
renders []paint.Text
// offsets is a slice of float32 representing the starting render offsets for the top of each line.
offsets []float32
// lineNumberDigits is the number of line number digits needed.
lineNumberDigits int
// LineNumberOffset is the horizontal offset for the start of text after line numbers.
LineNumberOffset float32 `set:"-" display:"-" json:"-" xml:"-"`
// lineNumberRender is the render for line numbers.
lineNumberRender paint.Text
// CursorPos is the current cursor position.
CursorPos lexer.Pos `set:"-" edit:"-" json:"-" xml:"-"`
// cursorTarget is the target cursor position for externally set targets.
// It ensures that the target position is visible.
cursorTarget lexer.Pos
// cursorColumn is the desired cursor column, where the cursor was last when moved using left / right arrows.
// It is used when doing up / down to not always go to short line columns.
cursorColumn int
// posHistoryIndex is the current index within PosHistory.
posHistoryIndex int
// selectStart is the starting point for selection, which will either be the start or end of selected region
// depending on subsequent selection.
selectStart lexer.Pos
// SelectRegion is the current selection region.
SelectRegion text.Region `set:"-" edit:"-" json:"-" xml:"-"`
// previousSelectRegion is the previous selection region that was actually rendered.
// It is needed to update the render.
previousSelectRegion text.Region
// Highlights is a slice of regions representing the highlighted regions, e.g., for search results.
Highlights []text.Region `set:"-" edit:"-" json:"-" xml:"-"`
// scopelights is a slice of regions representing the highlighted regions specific to scope markers.
scopelights []text.Region
// LinkHandler handles link clicks.
// If it is nil, they are sent to the standard web URL handler.
LinkHandler func(tl *paint.TextLink)
// ISearch is the interactive search data.
ISearch ISearch `set:"-" edit:"-" json:"-" xml:"-"`
// QReplace is the query replace data.
QReplace QReplace `set:"-" edit:"-" json:"-" xml:"-"`
// selectMode is a boolean indicating whether to select text as the cursor moves.
selectMode bool
// fontHeight is the font height, cached during styling.
fontHeight float32
// lineHeight is the line height, cached during styling.
lineHeight float32
// fontAscent is the font ascent, cached during styling.
fontAscent float32
// fontDescent is the font descent, cached during styling.
fontDescent float32
// nLinesChars is the height in lines and width in chars of the visible area.
nLinesChars image.Point
// linesSize is the total size of all lines as rendered.
linesSize math32.Vector2
// totalSize is the LinesSize plus extra space and line numbers etc.
totalSize math32.Vector2
// lineLayoutSize is the Geom.Size.Actual.Total subtracting extra space and line numbers.
// This is what LayoutStdLR sees for laying out each line.
lineLayoutSize math32.Vector2
// lastlineLayoutSize is the last LineLayoutSize used in laying out lines.
// It is used to trigger a new layout only when needed.
lastlineLayoutSize math32.Vector2
// blinkOn oscillates between on and off for blinking.
blinkOn bool
// cursorMu is a mutex protecting cursor rendering, shared between blink and main code.
cursorMu sync.Mutex
// hasLinks is a boolean indicating if at least one of the renders has links.
// It determines if we set the cursor for hand movements.
hasLinks bool
// hasLineNumbers indicates that this editor has line numbers
// (per [Buffer] option)
hasLineNumbers bool // TODO: is this really necessary?
// needsLayout is set by NeedsLayout: Editor does significant
// internal layout in LayoutAllLines, and its layout is simply based
// on what it gets allocated, so it does not affect the rest
// of the Scene.
needsLayout bool
// lastWasTabAI indicates that last key was a Tab auto-indent
lastWasTabAI bool
// lastWasUndo indicates that last key was an undo
lastWasUndo bool
// targetSet indicates that the CursorTarget is set
targetSet bool
lastRecenter int
lastAutoInsert rune
lastFilename core.Filename
}
func (ed *Editor) WidgetValue() any { return ed.Buffer.Text() }
func (ed *Editor) SetWidgetValue(value any) error {
ed.Buffer.SetString(reflectx.ToString(value))
return nil
}
func (ed *Editor) Init() {
ed.Frame.Init()
ed.AddContextMenu(ed.contextMenu)
ed.SetBuffer(NewBuffer())
ed.Styler(func(s *styles.Style) {
s.SetAbilities(true, abilities.Activatable, abilities.Focusable, abilities.Hoverable, abilities.Slideable, abilities.DoubleClickable, abilities.TripleClickable)
ed.CursorWidth.Dp(1)
ed.LineNumberColor = colors.Uniform(colors.Transparent)
ed.SelectColor = colors.Scheme.Select.Container
ed.HighlightColor = colors.Scheme.Warn.Container
ed.CursorColor = colors.Scheme.Primary.Base
s.Cursor = cursors.Text
s.VirtualKeyboard = styles.KeyboardMultiLine
if core.SystemSettings.Editor.WordWrap {
s.Text.WhiteSpace = styles.WhiteSpacePreWrap
} else {
s.Text.WhiteSpace = styles.WhiteSpacePre
}
s.SetMono(true)
s.Grow.Set(1, 0)
s.Overflow.Set(styles.OverflowAuto) // absorbs all
s.Border.Radius = styles.BorderRadiusLarge
s.Margin.Zero()
s.Padding.Set(units.Em(0.5))
s.Align.Content = styles.Start
s.Align.Items = styles.Start
s.Text.Align = styles.Start
s.Text.AlignV = styles.Start
s.Text.TabSize = core.SystemSettings.Editor.TabSize
s.Color = colors.Scheme.OnSurface
s.Min.X.Em(10)
s.MaxBorder.Width.Set(units.Dp(2))
s.Background = colors.Scheme.SurfaceContainerLow
// note: a blank background does NOT work for depth color rendering
if s.Is(states.Focused) {
s.StateLayer = 0
s.Border.Width.Set(units.Dp(2))
}
})
ed.handleKeyChord()
ed.handleMouse()
ed.handleLinkCursor()
ed.handleFocus()
ed.OnClose(func(e events.Event) {
ed.editDone()
})
ed.Updater(ed.NeedsLayout)
}
func (ed *Editor) Destroy() {
ed.stopCursor()
ed.Frame.Destroy()
}
// editDone completes editing and copies the active edited text to the text;
// called when the return key is pressed or goes out of focus
func (ed *Editor) editDone() {
if ed.Buffer != nil {
ed.Buffer.editDone()
}
ed.clearSelected()
ed.clearCursor()
ed.SendChange()
}
// reMarkup triggers a complete re-markup of the entire text --
// can do this when needed if the markup gets off due to multi-line
// formatting issues -- via Recenter key
func (ed *Editor) reMarkup() {
if ed.Buffer == nil {
return
}
ed.Buffer.ReMarkup()
}
// IsNotSaved returns true if buffer was changed (edited) since last Save.
func (ed *Editor) IsNotSaved() bool {
return ed.Buffer != nil && ed.Buffer.IsNotSaved()
}
// Clear resets all the text in the buffer for this editor.
func (ed *Editor) Clear() {
if ed.Buffer == nil {
return
}
ed.Buffer.SetText([]byte{})
}
///////////////////////////////////////////////////////////////////////////////
// Buffer communication
// resetState resets all the random state variables, when opening a new buffer etc
func (ed *Editor) resetState() {
ed.SelectReset()
ed.Highlights = nil
ed.ISearch.On = false
ed.QReplace.On = false
if ed.Buffer == nil || ed.lastFilename != ed.Buffer.Filename { // don't reset if reopening..
ed.CursorPos = lexer.Pos{}
}
if ed.Buffer != nil {
ed.Buffer.SetReadOnly(ed.IsReadOnly())
}
}
// SetBuffer sets the [Buffer] that this is an editor of, and interconnects their events.
func (ed *Editor) SetBuffer(buf *Buffer) *Editor {
oldbuf := ed.Buffer
if ed == nil || buf != nil && oldbuf == buf {
return ed
}
// had := false
if oldbuf != nil {
// had = true
oldbuf.Lock()
oldbuf.deleteEditor(ed)
oldbuf.Unlock() // done with oldbuf now
}
ed.Buffer = buf
ed.resetState()
if buf != nil {
buf.Lock()
buf.addEditor(ed)
bhl := len(buf.posHistory)
if bhl > 0 {
cp := buf.posHistory[bhl-1]
ed.posHistoryIndex = bhl - 1
buf.Unlock()
ed.SetCursorShow(cp)
} else {
buf.Unlock()
ed.SetCursorShow(lexer.Pos{})
}
}
ed.layoutAllLines() // relocks
ed.NeedsLayout()
return ed
}
// linesInserted inserts new lines of text and reformats them
func (ed *Editor) linesInserted(tbe *text.Edit) {
stln := tbe.Reg.Start.Ln + 1
nsz := (tbe.Reg.End.Ln - tbe.Reg.Start.Ln)
if stln > len(ed.renders) { // invalid
return
}
ed.renders = slices.Insert(ed.renders, stln, make([]paint.Text, nsz)...)
// Offs
tmpof := make([]float32, nsz)
ov := float32(0)
if stln < len(ed.offsets) {
ov = ed.offsets[stln]
} else {
ov = ed.offsets[len(ed.offsets)-1]
}
for i := range tmpof {
tmpof[i] = ov
}
ed.offsets = slices.Insert(ed.offsets, stln, tmpof...)
ed.NumLines += nsz
ed.NeedsLayout()
}
// linesDeleted deletes lines of text and reformats remaining one
func (ed *Editor) linesDeleted(tbe *text.Edit) {
stln := tbe.Reg.Start.Ln
edln := tbe.Reg.End.Ln
dsz := edln - stln
ed.renders = append(ed.renders[:stln], ed.renders[edln:]...)
ed.offsets = append(ed.offsets[:stln], ed.offsets[edln:]...)
ed.NumLines -= dsz
ed.NeedsLayout()
}
// bufferSignal receives a signal from the Buffer when the underlying text
// is changed.
func (ed *Editor) bufferSignal(sig bufferSignals, tbe *text.Edit) {
switch sig {
case bufferDone:
case bufferNew:
ed.resetState()
ed.SetCursorShow(ed.CursorPos)
ed.NeedsLayout()
case bufferMods:
ed.NeedsLayout()
case bufferInsert:
if ed == nil || ed.This == nil || !ed.IsVisible() {
return
}
ndup := ed.renders == nil
// fmt.Printf("ed %v got %v\n", ed.Nm, tbe.Reg.Start)
if tbe.Reg.Start.Ln != tbe.Reg.End.Ln {
// fmt.Printf("ed %v lines insert %v - %v\n", ed.Nm, tbe.Reg.Start, tbe.Reg.End)
ed.linesInserted(tbe) // triggers full layout
} else {
ed.layoutLine(tbe.Reg.Start.Ln) // triggers layout if line width exceeds
}
if ndup {
ed.Update()
}
case bufferDelete:
if ed == nil || ed.This == nil || !ed.IsVisible() {
return
}
ndup := ed.renders == nil
if tbe.Reg.Start.Ln != tbe.Reg.End.Ln {
ed.linesDeleted(tbe) // triggers full layout
} else {
ed.layoutLine(tbe.Reg.Start.Ln)
}
if ndup {
ed.Update()
}
case bufferMarkupUpdated:
ed.NeedsLayout() // comes from another goroutine
case bufferClosed:
ed.SetBuffer(nil)
}
}
///////////////////////////////////////////////////////////////////////////////
// Undo / Redo
// undo undoes previous action
func (ed *Editor) undo() {
tbes := ed.Buffer.undo()
if tbes != nil {
tbe := tbes[len(tbes)-1]
if tbe.Delete { // now an insert
ed.SetCursorShow(tbe.Reg.End)
} else {
ed.SetCursorShow(tbe.Reg.Start)
}
} else {
ed.cursorMovedEvent() // updates status..
ed.scrollCursorToCenterIfHidden()
}
ed.savePosHistory(ed.CursorPos)
ed.NeedsRender()
}
// redo redoes previously undone action
func (ed *Editor) redo() {
tbes := ed.Buffer.redo()
if tbes != nil {
tbe := tbes[len(tbes)-1]
if tbe.Delete {
ed.SetCursorShow(tbe.Reg.Start)
} else {
ed.SetCursorShow(tbe.Reg.End)
}
} else {
ed.scrollCursorToCenterIfHidden()
}
ed.savePosHistory(ed.CursorPos)
ed.NeedsRender()
}
// styleEditor applies the editor styles.
func (ed *Editor) styleEditor() {
if ed.NeedsRebuild() {
highlighting.UpdateFromTheme()
if ed.Buffer != nil {
ed.Buffer.SetHighlighting(highlighting.StyleDefault)
}
}
ed.Frame.Style()
ed.CursorWidth.ToDots(&ed.Styles.UnitContext)
}
func (ed *Editor) Style() {
ed.styleEditor()
ed.styleSizes()
}
// Code generated by "core generate"; DO NOT EDIT.
package texteditor
import (
"cogentcore.org/core/enums"
)
var _bufferSignalsValues = []bufferSignals{0, 1, 2, 3, 4, 5, 6}
// bufferSignalsN is the highest valid value for type bufferSignals, plus one.
const bufferSignalsN bufferSignals = 7
var _bufferSignalsValueMap = map[string]bufferSignals{`Done`: 0, `New`: 1, `Mods`: 2, `Insert`: 3, `Delete`: 4, `MarkupUpdated`: 5, `Closed`: 6}
var _bufferSignalsDescMap = map[bufferSignals]string{0: `bufferDone means that editing was completed and applied to Txt field -- data is Txt bytes`, 1: `bufferNew signals that entirely new text is present. All views should do full layout update.`, 2: `bufferMods signals that potentially diffuse modifications have been made. Views should do a Layout and Render.`, 3: `bufferInsert signals that some text was inserted. data is text.Edit describing change. The Buf always reflects the current state *after* the edit.`, 4: `bufferDelete signals that some text was deleted. data is text.Edit describing change. The Buf always reflects the current state *after* the edit.`, 5: `bufferMarkupUpdated signals that the Markup text has been updated This signal is typically sent from a separate goroutine, so should be used with a mutex`, 6: `bufferClosed signals that the text was closed.`}
var _bufferSignalsMap = map[bufferSignals]string{0: `Done`, 1: `New`, 2: `Mods`, 3: `Insert`, 4: `Delete`, 5: `MarkupUpdated`, 6: `Closed`}
// String returns the string representation of this bufferSignals value.
func (i bufferSignals) String() string { return enums.String(i, _bufferSignalsMap) }
// SetString sets the bufferSignals value from its string representation,
// and returns an error if the string is invalid.
func (i *bufferSignals) SetString(s string) error {
return enums.SetString(i, s, _bufferSignalsValueMap, "bufferSignals")
}
// Int64 returns the bufferSignals value as an int64.
func (i bufferSignals) Int64() int64 { return int64(i) }
// SetInt64 sets the bufferSignals value from an int64.
func (i *bufferSignals) SetInt64(in int64) { *i = bufferSignals(in) }
// Desc returns the description of the bufferSignals value.
func (i bufferSignals) Desc() string { return enums.Desc(i, _bufferSignalsDescMap) }
// bufferSignalsValues returns all possible values for the type bufferSignals.
func bufferSignalsValues() []bufferSignals { return _bufferSignalsValues }
// Values returns all possible values for the type bufferSignals.
func (i bufferSignals) Values() []enums.Enum { return enums.Values(_bufferSignalsValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i bufferSignals) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *bufferSignals) UnmarshalText(text []byte) error {
return enums.UnmarshalText(i, text, "bufferSignals")
}
// 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 texteditor
import (
"fmt"
"image"
"unicode"
"cogentcore.org/core/base/indent"
"cogentcore.org/core/core"
"cogentcore.org/core/cursors"
"cogentcore.org/core/events"
"cogentcore.org/core/events/key"
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/system"
"cogentcore.org/core/texteditor/text"
)
func (ed *Editor) handleFocus() {
ed.OnFocusLost(func(e events.Event) {
if ed.IsReadOnly() {
ed.clearCursor()
return
}
if ed.AbilityIs(abilities.Focusable) {
ed.editDone()
ed.SetState(false, states.Focused)
}
})
}
func (ed *Editor) handleKeyChord() {
ed.OnKeyChord(func(e events.Event) {
ed.keyInput(e)
})
}
// shiftSelect sets the selection start if the shift key is down but wasn't on the last key move.
// If the shift key has been released the select region is set to text.RegionNil
func (ed *Editor) shiftSelect(kt events.Event) {
hasShift := kt.HasAnyModifier(key.Shift)
if hasShift {
if ed.SelectRegion == text.RegionNil {
ed.selectStart = ed.CursorPos
}
} else {
ed.SelectRegion = text.RegionNil
}
}
// shiftSelectExtend updates the select region if the shift key is down and renders the selected text.
// If the shift key is not down the previously selected text is rerendered to clear the highlight
func (ed *Editor) shiftSelectExtend(kt events.Event) {
hasShift := kt.HasAnyModifier(key.Shift)
if hasShift {
ed.selectRegionUpdate(ed.CursorPos)
}
}
// keyInput handles keyboard input into the text field and from the completion menu
func (ed *Editor) keyInput(e events.Event) {
if core.DebugSettings.KeyEventTrace {
fmt.Printf("View KeyInput: %v\n", ed.Path())
}
kf := keymap.Of(e.KeyChord())
if e.IsHandled() {
return
}
if ed.Buffer == nil || ed.Buffer.NumLines() == 0 {
return
}
// cancelAll cancels search, completer, and..
cancelAll := func() {
ed.CancelComplete()
ed.cancelCorrect()
ed.iSearchCancel()
ed.qReplaceCancel()
ed.lastAutoInsert = 0
}
if kf != keymap.Recenter { // always start at centering
ed.lastRecenter = 0
}
if kf != keymap.Undo && ed.lastWasUndo {
ed.Buffer.EmacsUndoSave()
ed.lastWasUndo = false
}
gotTabAI := false // got auto-indent tab this time
// first all the keys that work for both inactive and active
switch kf {
case keymap.MoveRight:
cancelAll()
e.SetHandled()
ed.shiftSelect(e)
ed.cursorForward(1)
ed.shiftSelectExtend(e)
ed.iSpellKeyInput(e)
case keymap.WordRight:
cancelAll()
e.SetHandled()
ed.shiftSelect(e)
ed.cursorForwardWord(1)
ed.shiftSelectExtend(e)
case keymap.MoveLeft:
cancelAll()
e.SetHandled()
ed.shiftSelect(e)
ed.cursorBackward(1)
ed.shiftSelectExtend(e)
case keymap.WordLeft:
cancelAll()
e.SetHandled()
ed.shiftSelect(e)
ed.cursorBackwardWord(1)
ed.shiftSelectExtend(e)
case keymap.MoveUp:
cancelAll()
e.SetHandled()
ed.shiftSelect(e)
ed.cursorUp(1)
ed.shiftSelectExtend(e)
ed.iSpellKeyInput(e)
case keymap.MoveDown:
cancelAll()
e.SetHandled()
ed.shiftSelect(e)
ed.cursorDown(1)
ed.shiftSelectExtend(e)
ed.iSpellKeyInput(e)
case keymap.PageUp:
cancelAll()
e.SetHandled()
ed.shiftSelect(e)
ed.cursorPageUp(1)
ed.shiftSelectExtend(e)
case keymap.PageDown:
cancelAll()
e.SetHandled()
ed.shiftSelect(e)
ed.cursorPageDown(1)
ed.shiftSelectExtend(e)
case keymap.Home:
cancelAll()
e.SetHandled()
ed.shiftSelect(e)
ed.cursorStartLine()
ed.shiftSelectExtend(e)
case keymap.End:
cancelAll()
e.SetHandled()
ed.shiftSelect(e)
ed.cursorEndLine()
ed.shiftSelectExtend(e)
case keymap.DocHome:
cancelAll()
e.SetHandled()
ed.shiftSelect(e)
ed.CursorStartDoc()
ed.shiftSelectExtend(e)
case keymap.DocEnd:
cancelAll()
e.SetHandled()
ed.shiftSelect(e)
ed.cursorEndDoc()
ed.shiftSelectExtend(e)
case keymap.Recenter:
cancelAll()
e.SetHandled()
ed.reMarkup()
ed.cursorRecenter()
case keymap.SelectMode:
cancelAll()
e.SetHandled()
ed.selectModeToggle()
case keymap.CancelSelect:
ed.CancelComplete()
e.SetHandled()
ed.escPressed() // generic cancel
case keymap.SelectAll:
cancelAll()
e.SetHandled()
ed.selectAll()
case keymap.Copy:
cancelAll()
e.SetHandled()
ed.Copy(true) // reset
case keymap.Search:
e.SetHandled()
ed.qReplaceCancel()
ed.CancelComplete()
ed.iSearchStart()
case keymap.Abort:
cancelAll()
e.SetHandled()
ed.escPressed()
case keymap.Jump:
cancelAll()
e.SetHandled()
ed.JumpToLinePrompt()
case keymap.HistPrev:
cancelAll()
e.SetHandled()
ed.CursorToHistoryPrev()
case keymap.HistNext:
cancelAll()
e.SetHandled()
ed.CursorToHistoryNext()
case keymap.Lookup:
cancelAll()
e.SetHandled()
ed.Lookup()
}
if ed.IsReadOnly() {
switch {
case kf == keymap.FocusNext: // tab
e.SetHandled()
ed.CursorNextLink(true)
case kf == keymap.FocusPrev: // tab
e.SetHandled()
ed.CursorPrevLink(true)
case kf == keymap.None && ed.ISearch.On:
if unicode.IsPrint(e.KeyRune()) && !e.HasAnyModifier(key.Control, key.Meta) {
ed.iSearchKeyInput(e)
}
case e.KeyRune() == ' ' || kf == keymap.Accept || kf == keymap.Enter:
e.SetHandled()
ed.CursorPos.Ch--
ed.CursorNextLink(true) // todo: cursorcurlink
ed.OpenLinkAt(ed.CursorPos)
}
return
}
if e.IsHandled() {
ed.lastWasTabAI = gotTabAI
return
}
switch kf {
case keymap.Replace:
e.SetHandled()
ed.CancelComplete()
ed.iSearchCancel()
ed.QReplacePrompt()
case keymap.Backspace:
// todo: previous item in qreplace
if ed.ISearch.On {
ed.iSearchBackspace()
} else {
e.SetHandled()
ed.cursorBackspace(1)
ed.iSpellKeyInput(e)
ed.offerComplete()
}
case keymap.Kill:
cancelAll()
e.SetHandled()
ed.cursorKill()
case keymap.Delete:
cancelAll()
e.SetHandled()
ed.cursorDelete(1)
ed.iSpellKeyInput(e)
case keymap.BackspaceWord:
cancelAll()
e.SetHandled()
ed.cursorBackspaceWord(1)
case keymap.DeleteWord:
cancelAll()
e.SetHandled()
ed.cursorDeleteWord(1)
case keymap.Cut:
cancelAll()
e.SetHandled()
ed.Cut()
case keymap.Paste:
cancelAll()
e.SetHandled()
ed.Paste()
case keymap.Transpose:
cancelAll()
e.SetHandled()
ed.cursorTranspose()
case keymap.TransposeWord:
cancelAll()
e.SetHandled()
ed.cursorTransposeWord()
case keymap.PasteHist:
cancelAll()
e.SetHandled()
ed.pasteHistory()
case keymap.Accept:
cancelAll()
e.SetHandled()
ed.editDone()
case keymap.Undo:
cancelAll()
e.SetHandled()
ed.undo()
ed.lastWasUndo = true
case keymap.Redo:
cancelAll()
e.SetHandled()
ed.redo()
case keymap.Complete:
ed.iSearchCancel()
e.SetHandled()
if ed.Buffer.isSpellEnabled(ed.CursorPos) {
ed.offerCorrect()
} else {
ed.offerComplete()
}
case keymap.Enter:
cancelAll()
if !e.HasAnyModifier(key.Control, key.Meta) {
e.SetHandled()
if ed.Buffer.Options.AutoIndent {
lp, _ := parse.LanguageSupport.Properties(ed.Buffer.ParseState.Known)
if lp != nil && lp.Lang != nil && lp.HasFlag(parse.ReAutoIndent) {
// only re-indent current line for supported types
tbe, _, _ := ed.Buffer.AutoIndent(ed.CursorPos.Ln) // reindent current line
if tbe != nil {
// go back to end of line!
npos := lexer.Pos{Ln: ed.CursorPos.Ln, Ch: ed.Buffer.LineLen(ed.CursorPos.Ln)}
ed.setCursor(npos)
}
}
ed.InsertAtCursor([]byte("\n"))
tbe, _, cpos := ed.Buffer.AutoIndent(ed.CursorPos.Ln)
if tbe != nil {
ed.SetCursorShow(lexer.Pos{Ln: tbe.Reg.End.Ln, Ch: cpos})
}
} else {
ed.InsertAtCursor([]byte("\n"))
}
ed.iSpellKeyInput(e)
}
// todo: KeFunFocusPrev -- unindent
case keymap.FocusNext: // tab
cancelAll()
if !e.HasAnyModifier(key.Control, key.Meta) {
e.SetHandled()
lasttab := ed.lastWasTabAI
if !lasttab && ed.CursorPos.Ch == 0 && ed.Buffer.Options.AutoIndent {
_, _, cpos := ed.Buffer.AutoIndent(ed.CursorPos.Ln)
ed.CursorPos.Ch = cpos
ed.renderCursor(true)
gotTabAI = true
} else {
ed.InsertAtCursor(indent.Bytes(ed.Buffer.Options.IndentChar(), 1, ed.Styles.Text.TabSize))
}
ed.NeedsRender()
ed.iSpellKeyInput(e)
}
case keymap.FocusPrev: // shift-tab
cancelAll()
if !e.HasAnyModifier(key.Control, key.Meta) {
e.SetHandled()
if ed.CursorPos.Ch > 0 {
ind, _ := lexer.LineIndent(ed.Buffer.Line(ed.CursorPos.Ln), ed.Styles.Text.TabSize)
if ind > 0 {
ed.Buffer.IndentLine(ed.CursorPos.Ln, ind-1)
intxt := indent.Bytes(ed.Buffer.Options.IndentChar(), ind-1, ed.Styles.Text.TabSize)
npos := lexer.Pos{Ln: ed.CursorPos.Ln, Ch: len(intxt)}
ed.SetCursorShow(npos)
}
}
ed.iSpellKeyInput(e)
}
case keymap.None:
if unicode.IsPrint(e.KeyRune()) {
if !e.HasAnyModifier(key.Control, key.Meta) {
ed.keyInputInsertRune(e)
}
}
ed.iSpellKeyInput(e)
}
ed.lastWasTabAI = gotTabAI
}
// keyInputInsertBracket handle input of opening bracket-like entity
// (paren, brace, bracket)
func (ed *Editor) keyInputInsertBracket(kt events.Event) {
pos := ed.CursorPos
match := true
newLine := false
curLn := ed.Buffer.Line(pos.Ln)
lnLen := len(curLn)
lp, _ := parse.LanguageSupport.Properties(ed.Buffer.ParseState.Known)
if lp != nil && lp.Lang != nil {
match, newLine = lp.Lang.AutoBracket(&ed.Buffer.ParseState, kt.KeyRune(), pos, curLn)
} else {
if kt.KeyRune() == '{' {
if pos.Ch == lnLen {
if lnLen == 0 || unicode.IsSpace(curLn[pos.Ch-1]) {
newLine = true
}
match = true
} else {
match = unicode.IsSpace(curLn[pos.Ch])
}
} else {
match = pos.Ch == lnLen || unicode.IsSpace(curLn[pos.Ch]) // at end or if space after
}
}
if match {
ket, _ := lexer.BracePair(kt.KeyRune())
if newLine && ed.Buffer.Options.AutoIndent {
ed.InsertAtCursor([]byte(string(kt.KeyRune()) + "\n"))
tbe, _, cpos := ed.Buffer.AutoIndent(ed.CursorPos.Ln)
if tbe != nil {
pos = lexer.Pos{Ln: tbe.Reg.End.Ln, Ch: cpos}
ed.SetCursorShow(pos)
}
ed.InsertAtCursor([]byte("\n" + string(ket)))
ed.Buffer.AutoIndent(ed.CursorPos.Ln)
} else {
ed.InsertAtCursor([]byte(string(kt.KeyRune()) + string(ket)))
pos.Ch++
}
ed.lastAutoInsert = ket
} else {
ed.InsertAtCursor([]byte(string(kt.KeyRune())))
pos.Ch++
}
ed.SetCursorShow(pos)
ed.setCursorColumn(ed.CursorPos)
}
// keyInputInsertRune handles the insertion of a typed character
func (ed *Editor) keyInputInsertRune(kt events.Event) {
kt.SetHandled()
if ed.ISearch.On {
ed.CancelComplete()
ed.iSearchKeyInput(kt)
} else 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.Buffer.Options.AutoIndent && ed.CursorPos.Ch == ed.Buffer.LineLen(ed.CursorPos.Ln) {
ed.CancelComplete()
ed.lastAutoInsert = 0
ed.InsertAtCursor([]byte(string(kt.KeyRune())))
tbe, _, cpos := ed.Buffer.AutoIndent(ed.CursorPos.Ln)
if tbe != nil {
ed.SetCursorShow(lexer.Pos{Ln: tbe.Reg.End.Ln, Ch: cpos})
}
} else if ed.lastAutoInsert == kt.KeyRune() { // if we type what we just inserted, just move past
ed.CursorPos.Ch++
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()
}
}
if kt.KeyRune() == '}' || kt.KeyRune() == ')' || kt.KeyRune() == ']' {
cp := ed.CursorPos
np := cp
np.Ch--
tp, found := ed.Buffer.BraceMatch(kt.KeyRune(), np)
if found {
ed.scopelights = append(ed.scopelights, text.NewRegionPos(tp, lexer.Pos{tp.Ln, tp.Ch + 1}))
ed.scopelights = append(ed.scopelights, text.NewRegionPos(np, lexer.Pos{cp.Ln, cp.Ch}))
}
}
}
}
// openLink opens given link, either by sending LinkSig signal if there are
// receivers, or by calling the TextLinkHandler if non-nil, or URLHandler if
// non-nil (which by default opens user's default browser via
// system/App.OpenURL())
func (ed *Editor) openLink(tl *paint.TextLink) {
if ed.LinkHandler != nil {
ed.LinkHandler(tl)
} else {
system.TheApp.OpenURL(tl.URL)
}
}
// linkAt returns link at given cursor position, if one exists there --
// returns true and the link if there is a link, and false otherwise
func (ed *Editor) linkAt(pos lexer.Pos) (*paint.TextLink, bool) {
if !(pos.Ln < len(ed.renders) && len(ed.renders[pos.Ln].Links) > 0) {
return nil, false
}
cpos := ed.charStartPos(pos).ToPointCeil()
cpos.Y += 2
cpos.X += 2
lpos := ed.charStartPos(lexer.Pos{Ln: pos.Ln})
rend := &ed.renders[pos.Ln]
for ti := range rend.Links {
tl := &rend.Links[ti]
tlb := tl.Bounds(rend, lpos)
if cpos.In(tlb) {
return tl, true
}
}
return nil, false
}
// OpenLinkAt opens a link at given cursor position, if one exists there --
// returns true and the link if there is a link, and false otherwise -- highlights selected link
func (ed *Editor) OpenLinkAt(pos lexer.Pos) (*paint.TextLink, bool) {
tl, ok := ed.linkAt(pos)
if ok {
rend := &ed.renders[pos.Ln]
st, _ := rend.SpanPosToRuneIndex(tl.StartSpan, tl.StartIndex)
end, _ := rend.SpanPosToRuneIndex(tl.EndSpan, tl.EndIndex)
reg := text.NewRegion(pos.Ln, st, pos.Ln, end)
_ = reg
ed.HighlightRegion(reg)
ed.SetCursorTarget(pos)
ed.savePosHistory(ed.CursorPos)
ed.openLink(tl)
}
return tl, ok
}
// handleMouse handles mouse events
func (ed *Editor) handleMouse() {
ed.On(events.MouseDown, func(e events.Event) { // note: usual is Click..
if !ed.StateIs(states.Focused) {
ed.SetFocus()
}
pt := ed.PointToRelPos(e.Pos())
newPos := ed.PixelToCursor(pt)
switch e.MouseButton() {
case events.Left:
ed.SetState(true, states.Focused)
ed.setCursorFromMouse(pt, newPos, e.SelectMode())
ed.savePosHistory(ed.CursorPos)
case events.Middle:
if !ed.IsReadOnly() {
ed.setCursorFromMouse(pt, newPos, e.SelectMode())
ed.savePosHistory(ed.CursorPos)
}
}
})
ed.On(events.MouseUp, func(e events.Event) { // note: usual is Click..
pt := ed.PointToRelPos(e.Pos())
newPos := ed.PixelToCursor(pt)
switch e.MouseButton() {
case events.Left:
ed.OpenLinkAt(newPos)
case events.Middle:
if !ed.IsReadOnly() {
ed.Paste()
}
}
})
ed.OnDoubleClick(func(e events.Event) {
if !ed.StateIs(states.Focused) {
ed.SetFocus()
ed.Send(events.Focus, e) // sets focused flag
}
e.SetHandled()
if ed.selectWord() {
ed.CursorPos = ed.SelectRegion.Start
}
ed.NeedsRender()
})
ed.On(events.TripleClick, func(e events.Event) {
if !ed.StateIs(states.Focused) {
ed.SetFocus()
ed.Send(events.Focus, e) // sets focused flag
}
e.SetHandled()
sz := ed.Buffer.LineLen(ed.CursorPos.Ln)
if sz > 0 {
ed.SelectRegion.Start.Ln = ed.CursorPos.Ln
ed.SelectRegion.Start.Ch = 0
ed.SelectRegion.End.Ln = ed.CursorPos.Ln
ed.SelectRegion.End.Ch = sz
}
ed.NeedsRender()
})
ed.On(events.SlideMove, func(e events.Event) {
e.SetHandled()
if !ed.selectMode {
ed.selectModeToggle()
}
pt := ed.PointToRelPos(e.Pos())
newPos := ed.PixelToCursor(pt)
ed.setCursorFromMouse(pt, newPos, events.SelectOne)
})
}
func (ed *Editor) handleLinkCursor() {
ed.On(events.MouseMove, func(e events.Event) {
if !ed.hasLinks {
return
}
pt := ed.PointToRelPos(e.Pos())
mpos := ed.PixelToCursor(pt)
if mpos.Ln >= ed.NumLines {
return
}
pos := ed.renderStartPos()
pos.Y += ed.offsets[mpos.Ln]
pos.X += ed.LineNumberOffset
rend := &ed.renders[mpos.Ln]
inLink := false
for _, tl := range rend.Links {
tlb := tl.Bounds(rend, pos)
if e.Pos().In(tlb) {
inLink = true
break
}
}
if inLink {
ed.Styles.Cursor = cursors.Pointer
} else {
ed.Styles.Cursor = cursors.Text
}
})
}
// setCursorFromMouse sets cursor position from mouse mouse action -- handles
// the selection updating etc.
func (ed *Editor) setCursorFromMouse(pt image.Point, newPos lexer.Pos, selMode events.SelectModes) {
oldPos := ed.CursorPos
if newPos == oldPos {
return
}
// fmt.Printf("set cursor fm mouse: %v\n", newPos)
defer ed.NeedsRender()
if !ed.selectMode && selMode == events.ExtendContinuous {
if ed.SelectRegion == text.RegionNil {
ed.selectStart = ed.CursorPos
}
ed.setCursor(newPos)
ed.selectRegionUpdate(ed.CursorPos)
ed.renderCursor(true)
return
}
ed.setCursor(newPos)
if ed.selectMode || selMode != events.SelectOne {
if !ed.selectMode && selMode != events.SelectOne {
ed.selectMode = true
ed.selectStart = newPos
ed.selectRegionUpdate(ed.CursorPos)
}
if !ed.StateIs(states.Sliding) && selMode == events.SelectOne {
ln := ed.CursorPos.Ln
ch := ed.CursorPos.Ch
if ln != ed.SelectRegion.Start.Ln || ch < ed.SelectRegion.Start.Ch || ch > ed.SelectRegion.End.Ch {
ed.SelectReset()
}
} else {
ed.selectRegionUpdate(ed.CursorPos)
}
if ed.StateIs(states.Sliding) {
ed.AutoScroll(math32.FromPoint(pt).Sub(ed.Geom.Scroll))
} else {
ed.scrollCursorToCenterIfHidden()
}
} else if ed.HasSelection() {
ln := ed.CursorPos.Ln
ch := ed.CursorPos.Ch
if ln != ed.SelectRegion.Start.Ln || ch < ed.SelectRegion.Start.Ch || ch > ed.SelectRegion.End.Ch {
ed.SelectReset()
}
}
}
///////////////////////////////////////////////////////////
// Context Menu
// ShowContextMenu displays the context menu with options dependent on situation
func (ed *Editor) ShowContextMenu(e events.Event) {
if ed.Buffer.spell != nil && !ed.HasSelection() && ed.Buffer.isSpellEnabled(ed.CursorPos) {
if ed.Buffer.spell != nil {
if ed.offerCorrect() {
return
}
}
}
ed.WidgetBase.ShowContextMenu(e)
}
// contextMenu builds the text editor context menu
func (ed *Editor) contextMenu(m *core.Scene) {
core.NewButton(m).SetText("Copy").SetIcon(icons.ContentCopy).
SetKey(keymap.Copy).SetState(!ed.HasSelection(), states.Disabled).
OnClick(func(e events.Event) {
ed.Copy(true)
})
if !ed.IsReadOnly() {
core.NewButton(m).SetText("Cut").SetIcon(icons.ContentCopy).
SetKey(keymap.Cut).SetState(!ed.HasSelection(), states.Disabled).
OnClick(func(e events.Event) {
ed.Cut()
})
core.NewButton(m).SetText("Paste").SetIcon(icons.ContentPaste).
SetKey(keymap.Paste).SetState(ed.Clipboard().IsEmpty(), states.Disabled).
OnClick(func(e events.Event) {
ed.Paste()
})
core.NewSeparator(m)
core.NewFuncButton(m).SetFunc(ed.Buffer.Save).SetIcon(icons.Save)
core.NewFuncButton(m).SetFunc(ed.Buffer.SaveAs).SetIcon(icons.SaveAs)
core.NewFuncButton(m).SetFunc(ed.Buffer.Open).SetIcon(icons.Open)
core.NewFuncButton(m).SetFunc(ed.Buffer.Revert).SetIcon(icons.Reset)
} else {
core.NewButton(m).SetText("Clear").SetIcon(icons.ClearAll).
OnClick(func(e events.Event) {
ed.Clear()
})
}
}
// 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 texteditor
import (
"unicode"
"cogentcore.org/core/base/stringsx"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/styles"
"cogentcore.org/core/texteditor/text"
)
// findMatches finds the matches with given search string (literal, not regex)
// and case sensitivity, updates highlights for all. returns false if none
// found
func (ed *Editor) findMatches(find string, useCase, lexItems bool) ([]text.Match, bool) {
fsz := len(find)
if fsz == 0 {
ed.Highlights = nil
return nil, false
}
_, matches := ed.Buffer.Search([]byte(find), !useCase, lexItems)
if len(matches) == 0 {
ed.Highlights = nil
return matches, false
}
hi := make([]text.Region, len(matches))
for i, m := range matches {
hi[i] = m.Reg
if i > viewMaxFindHighlights {
break
}
}
ed.Highlights = hi
return matches, true
}
// matchFromPos finds the match at or after the given text position -- returns 0, false if none
func (ed *Editor) matchFromPos(matches []text.Match, cpos lexer.Pos) (int, bool) {
for i, m := range matches {
reg := ed.Buffer.AdjustRegion(m.Reg)
if reg.Start == cpos || cpos.IsLess(reg.Start) {
return i, true
}
}
return 0, false
}
// ISearch holds all the interactive search data
type ISearch struct {
// if true, in interactive search mode
On bool `json:"-" xml:"-"`
// current interactive search string
Find string `json:"-" xml:"-"`
// pay attention to case in isearch -- triggered by typing an upper-case letter
useCase bool
// current search matches
Matches []text.Match `json:"-" xml:"-"`
// position within isearch matches
pos int
// position in search list from previous search
prevPos int
// starting position for search -- returns there after on cancel
startPos lexer.Pos
}
// viewMaxFindHighlights is the maximum number of regions to highlight on find
var viewMaxFindHighlights = 1000
// PrevISearchString is the previous ISearch string
var PrevISearchString string
// iSearchMatches finds ISearch matches -- returns true if there are any
func (ed *Editor) iSearchMatches() bool {
got := false
ed.ISearch.Matches, got = ed.findMatches(ed.ISearch.Find, ed.ISearch.useCase, false)
return got
}
// iSearchNextMatch finds next match after given cursor position, and highlights
// it, etc
func (ed *Editor) iSearchNextMatch(cpos lexer.Pos) bool {
if len(ed.ISearch.Matches) == 0 {
ed.iSearchEvent()
return false
}
ed.ISearch.pos, _ = ed.matchFromPos(ed.ISearch.Matches, cpos)
ed.iSearchSelectMatch(ed.ISearch.pos)
return true
}
// iSearchSelectMatch selects match at given match index (e.g., ed.ISearch.Pos)
func (ed *Editor) iSearchSelectMatch(midx int) {
nm := len(ed.ISearch.Matches)
if midx >= nm {
ed.iSearchEvent()
return
}
m := ed.ISearch.Matches[midx]
reg := ed.Buffer.AdjustRegion(m.Reg)
pos := reg.Start
ed.SelectRegion = reg
ed.setCursor(pos)
ed.savePosHistory(ed.CursorPos)
ed.scrollCursorToCenterIfHidden()
ed.iSearchEvent()
}
// iSearchEvent sends the signal that ISearch is updated
func (ed *Editor) iSearchEvent() {
ed.Send(events.Input)
}
// iSearchStart is an emacs-style interactive search mode -- this is called when
// the search command itself is entered
func (ed *Editor) iSearchStart() {
if ed.ISearch.On {
if ed.ISearch.Find != "" { // already searching -- find next
sz := len(ed.ISearch.Matches)
if sz > 0 {
if ed.ISearch.pos < sz-1 {
ed.ISearch.pos++
} else {
ed.ISearch.pos = 0
}
ed.iSearchSelectMatch(ed.ISearch.pos)
}
} else { // restore prev
if PrevISearchString != "" {
ed.ISearch.Find = PrevISearchString
ed.ISearch.useCase = lexer.HasUpperCase(ed.ISearch.Find)
ed.iSearchMatches()
ed.iSearchNextMatch(ed.CursorPos)
ed.ISearch.startPos = ed.CursorPos
}
// nothing..
}
} else {
ed.ISearch.On = true
ed.ISearch.Find = ""
ed.ISearch.startPos = ed.CursorPos
ed.ISearch.useCase = false
ed.ISearch.Matches = nil
ed.SelectReset()
ed.ISearch.pos = -1
ed.iSearchEvent()
}
ed.NeedsRender()
}
// iSearchKeyInput is an emacs-style interactive search mode -- this is called
// when keys are typed while in search mode
func (ed *Editor) iSearchKeyInput(kt events.Event) {
kt.SetHandled()
r := kt.KeyRune()
// if ed.ISearch.Find == PrevISearchString { // undo starting point
// ed.ISearch.Find = ""
// }
if unicode.IsUpper(r) { // todo: more complex
ed.ISearch.useCase = true
}
ed.ISearch.Find += string(r)
ed.iSearchMatches()
sz := len(ed.ISearch.Matches)
if sz == 0 {
ed.ISearch.pos = -1
ed.iSearchEvent()
return
}
ed.iSearchNextMatch(ed.CursorPos)
ed.NeedsRender()
}
// iSearchBackspace gets rid of one item in search string
func (ed *Editor) iSearchBackspace() {
if ed.ISearch.Find == PrevISearchString { // undo starting point
ed.ISearch.Find = ""
ed.ISearch.useCase = false
ed.ISearch.Matches = nil
ed.SelectReset()
ed.ISearch.pos = -1
ed.iSearchEvent()
return
}
if len(ed.ISearch.Find) <= 1 {
ed.SelectReset()
ed.ISearch.Find = ""
ed.ISearch.useCase = false
return
}
ed.ISearch.Find = ed.ISearch.Find[:len(ed.ISearch.Find)-1]
ed.iSearchMatches()
sz := len(ed.ISearch.Matches)
if sz == 0 {
ed.ISearch.pos = -1
ed.iSearchEvent()
return
}
ed.iSearchNextMatch(ed.CursorPos)
ed.NeedsRender()
}
// iSearchCancel cancels ISearch mode
func (ed *Editor) iSearchCancel() {
if !ed.ISearch.On {
return
}
if ed.ISearch.Find != "" {
PrevISearchString = ed.ISearch.Find
}
ed.ISearch.prevPos = ed.ISearch.pos
ed.ISearch.Find = ""
ed.ISearch.useCase = false
ed.ISearch.On = false
ed.ISearch.pos = -1
ed.ISearch.Matches = nil
ed.Highlights = nil
ed.savePosHistory(ed.CursorPos)
ed.SelectReset()
ed.iSearchEvent()
ed.NeedsRender()
}
// QReplace holds all the query-replace data
type QReplace struct {
// if true, in interactive search mode
On bool `json:"-" xml:"-"`
// current interactive search string
Find string `json:"-" xml:"-"`
// current interactive search string
Replace string `json:"-" xml:"-"`
// pay attention to case in isearch -- triggered by typing an upper-case letter
useCase bool
// search only as entire lexically tagged item boundaries -- key for replacing short local variables like i
lexItems bool
// current search matches
Matches []text.Match `json:"-" xml:"-"`
// position within isearch matches
pos int `json:"-" xml:"-"`
// starting position for search -- returns there after on cancel
startPos lexer.Pos
}
var (
// prevQReplaceFinds are the previous QReplace strings
prevQReplaceFinds []string
// prevQReplaceRepls are the previous QReplace strings
prevQReplaceRepls []string
)
// qReplaceEvent sends the event that QReplace is updated
func (ed *Editor) qReplaceEvent() {
ed.Send(events.Input)
}
// QReplacePrompt is an emacs-style query-replace mode -- this starts the process, prompting
// user for items to search etc
func (ed *Editor) QReplacePrompt() {
find := ""
if ed.HasSelection() {
find = string(ed.Selection().ToBytes())
}
d := core.NewBody("Query-Replace")
core.NewText(d).SetType(core.TextSupporting).SetText("Enter strings for find and replace, then select Query-Replace; with dialog dismissed press <b>y</b> to replace current match, <b>n</b> to skip, <b>Enter</b> or <b>q</b> to quit, <b>!</b> to replace-all remaining")
fc := core.NewChooser(d).SetEditable(true).SetDefaultNew(true)
fc.Styler(func(s *styles.Style) {
s.Grow.Set(1, 0)
s.Min.X.Ch(80)
})
fc.SetStrings(prevQReplaceFinds...).SetCurrentIndex(0)
if find != "" {
fc.SetCurrentValue(find)
}
rc := core.NewChooser(d).SetEditable(true).SetDefaultNew(true)
rc.Styler(func(s *styles.Style) {
s.Grow.Set(1, 0)
s.Min.X.Ch(80)
})
rc.SetStrings(prevQReplaceRepls...).SetCurrentIndex(0)
lexitems := ed.QReplace.lexItems
lxi := core.NewSwitch(d).SetText("Lexical Items").SetChecked(lexitems)
lxi.SetTooltip("search matches entire lexically tagged items -- good for finding local variable names like 'i' and not matching everything")
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar)
d.AddOK(bar).SetText("Query-Replace").OnClick(func(e events.Event) {
var find, repl string
if s, ok := fc.CurrentItem.Value.(string); ok {
find = s
}
if s, ok := rc.CurrentItem.Value.(string); ok {
repl = s
}
lexItems := lxi.IsChecked()
ed.QReplaceStart(find, repl, lexItems)
})
})
d.RunDialog(ed)
}
// QReplaceStart starts query-replace using given find, replace strings
func (ed *Editor) QReplaceStart(find, repl string, lexItems bool) {
ed.QReplace.On = true
ed.QReplace.Find = find
ed.QReplace.Replace = repl
ed.QReplace.lexItems = lexItems
ed.QReplace.startPos = ed.CursorPos
ed.QReplace.useCase = lexer.HasUpperCase(find)
ed.QReplace.Matches = nil
ed.QReplace.pos = -1
stringsx.InsertFirstUnique(&prevQReplaceFinds, find, core.SystemSettings.SavedPathsMax)
stringsx.InsertFirstUnique(&prevQReplaceRepls, repl, core.SystemSettings.SavedPathsMax)
ed.qReplaceMatches()
ed.QReplace.pos, _ = ed.matchFromPos(ed.QReplace.Matches, ed.CursorPos)
ed.qReplaceSelectMatch(ed.QReplace.pos)
ed.qReplaceEvent()
}
// qReplaceMatches finds QReplace matches -- returns true if there are any
func (ed *Editor) qReplaceMatches() bool {
got := false
ed.QReplace.Matches, got = ed.findMatches(ed.QReplace.Find, ed.QReplace.useCase, ed.QReplace.lexItems)
return got
}
// qReplaceNextMatch finds next match using, QReplace.Pos and highlights it, etc
func (ed *Editor) qReplaceNextMatch() bool {
nm := len(ed.QReplace.Matches)
if nm == 0 {
return false
}
ed.QReplace.pos++
if ed.QReplace.pos >= nm {
return false
}
ed.qReplaceSelectMatch(ed.QReplace.pos)
return true
}
// qReplaceSelectMatch selects match at given match index (e.g., ed.QReplace.Pos)
func (ed *Editor) qReplaceSelectMatch(midx int) {
nm := len(ed.QReplace.Matches)
if midx >= nm {
return
}
m := ed.QReplace.Matches[midx]
reg := ed.Buffer.AdjustRegion(m.Reg)
pos := reg.Start
ed.SelectRegion = reg
ed.setCursor(pos)
ed.savePosHistory(ed.CursorPos)
ed.scrollCursorToCenterIfHidden()
ed.qReplaceEvent()
}
// qReplaceReplace replaces at given match index (e.g., ed.QReplace.Pos)
func (ed *Editor) qReplaceReplace(midx int) {
nm := len(ed.QReplace.Matches)
if midx >= nm {
return
}
m := ed.QReplace.Matches[midx]
rep := ed.QReplace.Replace
reg := ed.Buffer.AdjustRegion(m.Reg)
pos := reg.Start
// last arg is matchCase, only if not using case to match and rep is also lower case
matchCase := !ed.QReplace.useCase && !lexer.HasUpperCase(rep)
ed.Buffer.ReplaceText(reg.Start, reg.End, pos, rep, EditSignal, matchCase)
ed.Highlights[midx] = text.RegionNil
ed.setCursor(pos)
ed.savePosHistory(ed.CursorPos)
ed.scrollCursorToCenterIfHidden()
ed.qReplaceEvent()
}
// QReplaceReplaceAll replaces all remaining from index
func (ed *Editor) QReplaceReplaceAll(midx int) {
nm := len(ed.QReplace.Matches)
if midx >= nm {
return
}
for mi := midx; mi < nm; mi++ {
ed.qReplaceReplace(mi)
}
}
// qReplaceKeyInput is an emacs-style interactive search mode -- this is called
// when keys are typed while in search mode
func (ed *Editor) qReplaceKeyInput(kt events.Event) {
kt.SetHandled()
switch {
case kt.KeyRune() == 'y':
ed.qReplaceReplace(ed.QReplace.pos)
if !ed.qReplaceNextMatch() {
ed.qReplaceCancel()
}
case kt.KeyRune() == 'n':
if !ed.qReplaceNextMatch() {
ed.qReplaceCancel()
}
case kt.KeyRune() == 'q' || kt.KeyChord() == "ReturnEnter":
ed.qReplaceCancel()
case kt.KeyRune() == '!':
ed.QReplaceReplaceAll(ed.QReplace.pos)
ed.qReplaceCancel()
}
ed.NeedsRender()
}
// qReplaceCancel cancels QReplace mode
func (ed *Editor) qReplaceCancel() {
if !ed.QReplace.On {
return
}
ed.QReplace.On = false
ed.QReplace.pos = -1
ed.QReplace.Matches = nil
ed.Highlights = nil
ed.savePosHistory(ed.CursorPos)
ed.SelectReset()
ed.qReplaceEvent()
ed.NeedsRender()
}
// escPressed emitted for [keymap.Abort] or [keymap.CancelSelect];
// effect depends on state.
func (ed *Editor) escPressed() {
switch {
case ed.ISearch.On:
ed.iSearchCancel()
ed.SetCursorShow(ed.ISearch.startPos)
case ed.QReplace.On:
ed.qReplaceCancel()
ed.SetCursorShow(ed.ISearch.startPos)
case ed.HasSelection():
ed.SelectReset()
default:
ed.Highlights = nil
}
ed.NeedsRender()
}
// Code generated by "core generate -add-types"; DO NOT EDIT.
package highlighting
import (
"cogentcore.org/core/enums"
)
var _TrileanValues = []Trilean{0, 1, 2}
// TrileanN is the highest valid value for type Trilean, plus one.
const TrileanN Trilean = 3
var _TrileanValueMap = map[string]Trilean{`Pass`: 0, `Yes`: 1, `No`: 2}
var _TrileanDescMap = map[Trilean]string{0: ``, 1: ``, 2: ``}
var _TrileanMap = map[Trilean]string{0: `Pass`, 1: `Yes`, 2: `No`}
// String returns the string representation of this Trilean value.
func (i Trilean) String() string { return enums.String(i, _TrileanMap) }
// SetString sets the Trilean value from its string representation,
// and returns an error if the string is invalid.
func (i *Trilean) SetString(s string) error {
return enums.SetString(i, s, _TrileanValueMap, "Trilean")
}
// Int64 returns the Trilean value as an int64.
func (i Trilean) Int64() int64 { return int64(i) }
// SetInt64 sets the Trilean value from an int64.
func (i *Trilean) SetInt64(in int64) { *i = Trilean(in) }
// Desc returns the description of the Trilean value.
func (i Trilean) Desc() string { return enums.Desc(i, _TrileanDescMap) }
// TrileanValues returns all possible values for the type Trilean.
func TrileanValues() []Trilean { return _TrileanValues }
// Values returns all possible values for the type Trilean.
func (i Trilean) Values() []enums.Enum { return enums.Values(_TrileanValues) }
// MarshalText implements the [encoding.TextMarshaler] interface.
func (i Trilean) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
func (i *Trilean) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Trilean") }
// 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 highlighting
import (
stdhtml "html"
"log/slog"
"strings"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/core"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/lexer"
_ "cogentcore.org/core/parse/supportedlanguages"
"cogentcore.org/core/parse/token"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
)
// Highlighter performs syntax highlighting,
// using [parse] if available, otherwise falls back on chroma.
type Highlighter struct {
// syntax highlighting style to use
StyleName core.HighlightingName
// chroma-based language name for syntax highlighting the code
language string
// Has is whether there are highlighting parameters set
// (only valid after [Highlighter.init] has been called).
Has bool
// tab size, in chars
TabSize int
// Commpiled CSS properties for given highlighting style
CSSProperties map[string]any
// parser state info
parseState *parse.FileStates
// if supported, this is the [parse.Language] support for parsing
parseLanguage parse.Language
// current highlighting style
style *Style
// external toggle to turn off automatic highlighting
off bool
lastLanguage string
lastStyle core.HighlightingName
lexer chroma.Lexer
formatter *html.Formatter
}
// UsingParse returns true if markup is using parse lexer / parser, which affects
// use of results
func (hi *Highlighter) UsingParse() bool {
return hi.parseLanguage != nil
}
// Init initializes the syntax highlighting for current params
func (hi *Highlighter) Init(info *fileinfo.FileInfo, pist *parse.FileStates) {
hi.parseState = pist
if info.Known != fileinfo.Unknown {
if lp, err := parse.LanguageSupport.Properties(info.Known); err == nil {
if lp.Lang != nil {
hi.lexer = nil
hi.parseLanguage = lp.Lang
} else {
hi.parseLanguage = nil
}
}
}
if hi.parseLanguage == nil {
lexer := lexers.MatchMimeType(info.Mime)
if lexer == nil {
lexer = lexers.Match(info.Name)
}
if lexer != nil {
hi.language = lexer.Config().Name
hi.lexer = lexer
}
}
if hi.StyleName == "" || (hi.parseLanguage == nil && hi.lexer == nil) {
hi.Has = false
return
}
hi.Has = true
if hi.StyleName != hi.lastStyle {
hi.style = AvailableStyle(hi.StyleName)
hi.CSSProperties = hi.style.ToProperties()
hi.lastStyle = hi.StyleName
}
if hi.lexer != nil && hi.language != hi.lastLanguage {
hi.lexer = chroma.Coalesce(lexers.Get(hi.language))
hi.formatter = html.New(html.WithClasses(true), html.TabWidth(hi.TabSize))
hi.lastLanguage = hi.language
}
}
// SetStyle sets the highlighting style and updates corresponding settings
func (hi *Highlighter) SetStyle(style core.HighlightingName) {
if style == "" {
return
}
st := AvailableStyle(hi.StyleName)
if st == nil {
slog.Error("Highlighter Style not found:", "style", style)
return
}
hi.StyleName = style
hi.style = st
hi.CSSProperties = hi.style.ToProperties()
hi.lastStyle = hi.StyleName
}
// MarkupTagsAll returns all the markup tags according to current
// syntax highlighting settings
func (hi *Highlighter) MarkupTagsAll(txt []byte) ([]lexer.Line, error) {
if hi.off {
return nil, nil
}
if hi.parseLanguage != nil {
hi.parseLanguage.ParseFile(hi.parseState, txt) // processes in Proc(), does Switch()
return hi.parseState.Done().Src.Lexs, nil // Done() is results of above
} else if hi.lexer != nil {
return hi.chromaTagsAll(txt)
}
return nil, nil
}
// MarkupTagsLine returns tags for one line according to current
// syntax highlighting settings
func (hi *Highlighter) MarkupTagsLine(ln int, txt []rune) (lexer.Line, error) {
if hi.off {
return nil, nil
}
if hi.parseLanguage != nil {
ll := hi.parseLanguage.HighlightLine(hi.parseState, ln, txt)
return ll, nil
} else if hi.lexer != nil {
return hi.chromaTagsLine(txt)
}
return nil, nil
}
// chromaTagsForLine generates the chroma tags for one line of chroma tokens
func chromaTagsForLine(tags *lexer.Line, toks []chroma.Token) {
cp := 0
for _, tok := range toks {
str := []rune(strings.TrimSuffix(tok.Value, "\n"))
slen := len(str)
if slen == 0 {
continue
}
if tok.Type == chroma.None { // always a parsing err AFAIK
// fmt.Printf("type: %v st: %v ed: %v txt: %v\n", tok.Type, cp, ep, str)
continue
}
ep := cp + slen
if tok.Type < chroma.Text {
ht := TokenFromChroma(tok.Type)
tags.AddLex(token.KeyToken{Token: ht}, cp, ep)
}
cp = ep
}
}
// chromaTagsAll returns all the markup tags according to current
// syntax highlighting settings
func (hi *Highlighter) chromaTagsAll(txt []byte) ([]lexer.Line, error) {
txtstr := string(txt) // expensive!
iterator, err := hi.lexer.Tokenise(nil, txtstr)
if err != nil {
slog.Error(err.Error())
return nil, err
}
lines := chroma.SplitTokensIntoLines(iterator.Tokens())
sz := len(lines)
tags := make([]lexer.Line, sz)
for li, lt := range lines {
chromaTagsForLine(&tags[li], lt)
}
return tags, nil
}
// chromaTagsLine returns tags for one line according to current
// syntax highlighting settings
func (hi *Highlighter) chromaTagsLine(txt []rune) (lexer.Line, error) {
return ChromaTagsLine(hi.lexer, string(txt))
}
// ChromaTagsLine returns tags for one line according to given chroma lexer
func ChromaTagsLine(clex chroma.Lexer, txt string) (lexer.Line, error) {
n := len(txt)
if n == 0 {
return nil, nil
}
if txt[n-1] != '\n' {
txt += "\n"
}
iterator, err := clex.Tokenise(nil, txt)
if err != nil {
slog.Error(err.Error())
return nil, err
}
var tags lexer.Line
toks := iterator.Tokens()
chromaTagsForLine(&tags, toks)
return tags, nil
}
// maxLineLen prevents overflow in allocating line length
const (
maxLineLen = 64 * 1024 * 1024
maxNumTags = 1024
EscapeHTML = true
NoEscapeHTML = false
)
// MarkupLine returns the line with html class tags added for each tag
// takes both the hi tags and extra tags. Only fully nested tags are supported,
// with any dangling ends truncated.
func MarkupLine(txt []rune, hitags, tags lexer.Line, escapeHtml bool) []byte {
if len(txt) > maxLineLen { // avoid overflow
return nil
}
sz := len(txt)
if sz == 0 {
return nil
}
var escf func([]rune) []byte
if escapeHtml {
escf = HtmlEscapeRunes
} else {
escf = func(r []rune) []byte {
return []byte(string(r))
}
}
ttags := lexer.MergeLines(hitags, tags) // ensures that inner-tags are *after* outer tags
nt := len(ttags)
if nt == 0 || nt > maxNumTags {
return escf(txt)
}
sps := []byte(`<span class="`)
sps2 := []byte(`">`)
spe := []byte(`</span>`)
taglen := len(sps) + len(sps2) + len(spe) + 2
musz := sz + nt*taglen
mu := make([]byte, 0, musz)
cp := 0
var tstack []int // stack of tags indexes that remain to be completed, sorted soonest at end
for i, tr := range ttags {
if cp >= sz {
break
}
for si := len(tstack) - 1; si >= 0; si-- {
ts := ttags[tstack[si]]
if ts.Ed <= tr.St {
ep := min(sz, ts.Ed)
if cp < ep {
mu = append(mu, escf(txt[cp:ep])...)
cp = ep
}
mu = append(mu, spe...)
tstack = append(tstack[:si], tstack[si+1:]...)
}
}
if cp >= sz || tr.St >= sz {
break
}
if tr.St > cp {
mu = append(mu, escf(txt[cp:tr.St])...)
}
mu = append(mu, sps...)
clsnm := tr.Token.Token.StyleName()
mu = append(mu, []byte(clsnm)...)
mu = append(mu, sps2...)
ep := tr.Ed
addEnd := true
if i < nt-1 {
if ttags[i+1].St < tr.Ed { // next one starts before we end, add to stack
addEnd = false
ep = ttags[i+1].St
if len(tstack) == 0 {
tstack = append(tstack, i)
} else {
for si := len(tstack) - 1; si >= 0; si-- {
ts := ttags[tstack[si]]
if tr.Ed <= ts.Ed {
ni := si // + 1 // new index in stack -- right *before* current
tstack = append(tstack, i)
copy(tstack[ni+1:], tstack[ni:])
tstack[ni] = i
}
}
}
}
}
ep = min(len(txt), ep)
if tr.St < ep {
mu = append(mu, escf(txt[tr.St:ep])...)
}
if addEnd {
mu = append(mu, spe...)
}
cp = ep
}
if sz > cp {
mu = append(mu, escf(txt[cp:sz])...)
}
// pop any left on stack..
for si := len(tstack) - 1; si >= 0; si-- {
mu = append(mu, spe...)
}
return mu
}
// HtmlEscapeBytes escapes special characters like "<" to become "<". It
// escapes only five such characters: <, >, &, ' and ".
// It operates on a *copy* of the byte string and does not modify the input!
// otherwise it causes major problems..
func HtmlEscapeBytes(b []byte) []byte {
return []byte(stdhtml.EscapeString(string(b)))
}
// HtmlEscapeRunes escapes special characters like "<" to become "<". It
// escapes only five such characters: <, >, &, ' and ".
// It operates on a *copy* of the byte string and does not modify the input!
// otherwise it causes major problems..
func HtmlEscapeRunes(r []rune) []byte {
return []byte(stdhtml.EscapeString(string(r)))
}
// 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 highlighting provides syntax highlighting styles; it is based on
// github.com/alecthomas/chroma, which in turn was based on the python
// pygments package. Note that this package depends on core and parse
// and cannot be imported there; is imported in texteditor.
package highlighting
//go:generate core generate -add-types
import (
"encoding/json"
"image/color"
"log/slog"
"os"
"strings"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/cam/hct"
"cogentcore.org/core/colors/matcolor"
"cogentcore.org/core/core"
"cogentcore.org/core/parse/token"
"cogentcore.org/core/styles"
)
// Trilean value for StyleEntry value inheritance.
type Trilean int32 //enums:enum
const (
Pass Trilean = iota
Yes
No
)
func (t Trilean) Prefix(s string) string {
if t == Yes {
return s
} else if t == No {
return "no" + s
}
return ""
}
// StyleEntry is one value in the map of highlight style values
type StyleEntry struct {
// text color
Color color.RGBA
// background color
Background color.RGBA
// border color? not sure what this is -- not really used
Border color.RGBA `display:"-"`
// bold font
Bold Trilean
// italic font
Italic Trilean
// underline
Underline Trilean
// don't inherit these settings from sub-category or category levels -- otherwise everything with a Pass is inherited
NoInherit bool
}
// // FromChroma copies styles from chroma
//
// func (he *StyleEntry) FromChroma(ce chroma.StyleEntry) {
// if ce.Colour.IsSet() {
// he.Color.SetString(ce.Colour.String(), nil)
// } else {
// he.Color.SetToNil()
// }
// if ce.Background.IsSet() {
// he.Background.SetString(ce.Background.String(), nil)
// } else {
// he.Background.SetToNil()
// }
// if ce.Border.IsSet() {
// he.Border.SetString(ce.Border.String(), nil)
// } else {
// he.Border.SetToNil()
// }
// he.Bold = Trilean(ce.Bold)
// he.Italic = Trilean(ce.Italic)
// he.Underline = Trilean(ce.Underline)
// he.NoInherit = ce.NoInherit
// }
//
// // StyleEntryFromChroma returns a new style entry from corresponding chroma version
//
// func StyleEntryFromChroma(ce chroma.StyleEntry) StyleEntry {
// he := StyleEntry{}
// he.FromChroma(ce)
// return he
// }
// UpdateFromTheme normalizes the colors of the style entry such that they have consistent
// chromas and tones that guarantee sufficient text contrast in accordance with the color theme.
func (se *StyleEntry) UpdateFromTheme() {
hc := hct.FromColor(se.Color)
ctone := float32(40)
if matcolor.SchemeIsDark {
ctone = 80
}
se.Color = hc.WithChroma(max(hc.Chroma, 48)).WithTone(ctone).AsRGBA()
if !colors.IsNil(se.Background) {
hb := hct.FromColor(se.Background)
btone := max(hb.Tone, 94)
if matcolor.SchemeIsDark {
btone = min(hb.Tone, 17)
}
se.Background = hb.WithChroma(max(hb.Chroma, 6)).WithTone(btone).AsRGBA()
}
}
func (se StyleEntry) String() string {
out := []string{}
if se.Bold != Pass {
out = append(out, se.Bold.Prefix("bold"))
}
if se.Italic != Pass {
out = append(out, se.Italic.Prefix("italic"))
}
if se.Underline != Pass {
out = append(out, se.Underline.Prefix("underline"))
}
if se.NoInherit {
out = append(out, "noinherit")
}
if !colors.IsNil(se.Color) {
out = append(out, colors.AsString(se.Color))
}
if !colors.IsNil(se.Background) {
out = append(out, "bg:"+colors.AsString(se.Background))
}
if !colors.IsNil(se.Border) {
out = append(out, "border:"+colors.AsString(se.Border))
}
return strings.Join(out, " ")
}
// ToCSS converts StyleEntry to CSS attributes.
func (se StyleEntry) ToCSS() string {
styles := []string{}
if !colors.IsNil(se.Color) {
styles = append(styles, "color: "+colors.AsString(se.Color))
}
if !colors.IsNil(se.Background) {
styles = append(styles, "background-color: "+colors.AsString(se.Background))
}
if se.Bold == Yes {
styles = append(styles, "font-weight: bold")
}
if se.Italic == Yes {
styles = append(styles, "font-style: italic")
}
if se.Underline == Yes {
styles = append(styles, "text-decoration: underline")
}
return strings.Join(styles, "; ")
}
// ToProperties converts the StyleEntry to key-value properties.
func (se StyleEntry) ToProperties() map[string]any {
pr := map[string]any{}
if !colors.IsNil(se.Color) {
pr["color"] = se.Color
}
if !colors.IsNil(se.Background) {
pr["background-color"] = se.Background
}
if se.Bold == Yes {
pr["font-weight"] = styles.WeightBold
}
if se.Italic == Yes {
pr["font-style"] = styles.Italic
}
if se.Underline == Yes {
pr["text-decoration"] = 1 << uint32(styles.Underline)
}
return pr
}
// Sub subtracts two style entries, returning an entry with only the differences set
func (s StyleEntry) Sub(e StyleEntry) StyleEntry {
out := StyleEntry{}
if e.Color != s.Color {
out.Color = s.Color
}
if e.Background != s.Background {
out.Background = s.Background
}
if e.Border != s.Border {
out.Border = s.Border
}
if e.Bold != s.Bold {
out.Bold = s.Bold
}
if e.Italic != s.Italic {
out.Italic = s.Italic
}
if e.Underline != s.Underline {
out.Underline = s.Underline
}
return out
}
// Inherit styles from ancestors.
//
// Ancestors should be provided from oldest, furthest away to newest, closest.
func (s StyleEntry) Inherit(ancestors ...StyleEntry) StyleEntry {
out := s
for i := len(ancestors) - 1; i >= 0; i-- {
if out.NoInherit {
return out
}
ancestor := ancestors[i]
if colors.IsNil(out.Color) {
out.Color = ancestor.Color
}
if colors.IsNil(out.Background) {
out.Background = ancestor.Background
}
if colors.IsNil(out.Border) {
out.Border = ancestor.Border
}
if out.Bold == Pass {
out.Bold = ancestor.Bold
}
if out.Italic == Pass {
out.Italic = ancestor.Italic
}
if out.Underline == Pass {
out.Underline = ancestor.Underline
}
}
return out
}
func (s StyleEntry) IsZero() bool {
return colors.IsNil(s.Color) && colors.IsNil(s.Background) && colors.IsNil(s.Border) && s.Bold == Pass && s.Italic == Pass &&
s.Underline == Pass && !s.NoInherit
}
///////////////////////////////////////////////////////////////////////////////////
// Style
// Style is a full style map of styles for different token.Tokens tag values
type Style map[token.Tokens]*StyleEntry
// CopyFrom copies a style from source style
func (hs *Style) CopyFrom(ss *Style) {
if ss == nil {
return
}
*hs = make(Style, len(*ss))
for k, v := range *ss {
(*hs)[k] = v
}
}
// TagRaw returns a StyleEntry for given tag without any inheritance of anything
// will be IsZero if not defined for this style
func (hs Style) TagRaw(tag token.Tokens) StyleEntry {
if len(hs) == 0 {
return StyleEntry{}
}
if se, has := hs[tag]; has {
return *se
}
return StyleEntry{}
}
// Tag returns a StyleEntry for given Tag.
// Will try sub-category or category if an exact match is not found.
// does NOT add the background properties -- those are always kept separate.
func (hs Style) Tag(tag token.Tokens) StyleEntry {
se := hs.TagRaw(tag).Inherit(
hs.TagRaw(token.Text),
hs.TagRaw(tag.Cat()),
hs.TagRaw(tag.SubCat()))
return se
}
// ToCSS generates a CSS style sheet for this style, by token.Tokens tag
func (hs Style) ToCSS() map[token.Tokens]string {
css := map[token.Tokens]string{}
for ht := range token.Names {
entry := hs.Tag(ht)
if entry.IsZero() {
continue
}
css[ht] = entry.ToCSS()
}
return css
}
// ToProperties generates a list of key-value properties for this style.
func (hs Style) ToProperties() map[string]any {
pr := map[string]any{}
for ht, nm := range token.Names {
entry := hs.Tag(ht)
if entry.IsZero() {
if tp, ok := Properties[ht]; ok {
pr["."+nm] = tp
}
continue
}
pr["."+nm] = entry.ToProperties()
}
return pr
}
// Open hi style from a JSON-formatted file.
func (hs Style) OpenJSON(filename core.Filename) error {
b, err := os.ReadFile(string(filename))
if err != nil {
// PromptDialog(nil, "File Not Found", err.Error(), true, false, nil, nil, nil)
slog.Error(err.Error())
return err
}
return json.Unmarshal(b, &hs)
}
// Save hi style to a JSON-formatted file.
func (hs Style) SaveJSON(filename core.Filename) error {
b, err := json.MarshalIndent(hs, "", " ")
if err != nil {
slog.Error(err.Error()) // unlikely
return err
}
err = os.WriteFile(string(filename), b, 0644)
if err != nil {
// PromptDialog(nil, "Could not Save to File", err.Error(), true, false, nil, nil, nil)
slog.Error(err.Error())
}
return err
}
// Properties are default properties for custom tags (tokens); if set in style then used
// there but otherwise we use these as a fallback; typically not overridden
var Properties = map[token.Tokens]map[string]any{
token.TextSpellErr: {
"text-decoration": 1 << uint32(styles.DecoDottedUnderline), // bitflag!
},
}
// 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 highlighting
import (
_ "embed"
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"sort"
"cogentcore.org/core/core"
"cogentcore.org/core/parse"
)
//go:embed defaults.highlighting
var defaults []byte
// Styles is a collection of styles
type Styles map[string]*Style
// StandardStyles are the styles from chroma package
var StandardStyles Styles
// CustomStyles are user's special styles
var CustomStyles = Styles{}
// AvailableStyles are all highlighting styles
var AvailableStyles Styles
// StyleDefault is the default highlighting style name
var StyleDefault = core.HighlightingName("emacs")
// StyleNames are all the names of all the available highlighting styles
var StyleNames []string
// UpdateFromTheme normalizes the colors of all style entry such that they have consistent
// chromas and tones that guarantee sufficient text contrast in accordance with the color theme.
func UpdateFromTheme() {
for _, s := range AvailableStyles {
for _, se := range *s {
se.UpdateFromTheme()
}
}
}
// AvailableStyle returns a style by name from the AvailStyles list -- if not found
// default is used as a fallback
func AvailableStyle(nm core.HighlightingName) *Style {
if AvailableStyles == nil {
Init()
}
if st, ok := AvailableStyles[string(nm)]; ok {
return st
}
return AvailableStyles[string(StyleDefault)]
}
// Add adds a new style to the list
func (hs *Styles) Add() *Style {
hse := &Style{}
nm := fmt.Sprintf("NewStyle_%v", len(*hs))
(*hs)[nm] = hse
return hse
}
// CopyFrom copies styles from another collection
func (hs *Styles) CopyFrom(os Styles) {
if *hs == nil {
*hs = make(Styles, len(os))
}
for nm, cse := range os {
(*hs)[nm] = cse
}
}
// MergeAvailStyles updates AvailStyles as combination of std and custom styles
func MergeAvailStyles() {
AvailableStyles = make(Styles, len(CustomStyles)+len(StandardStyles))
AvailableStyles.CopyFrom(StandardStyles)
AvailableStyles.CopyFrom(CustomStyles)
StyleNames = AvailableStyles.Names()
}
// Open hi styles from a JSON-formatted file. You can save and open
// styles to / from files to share, experiment, transfer, etc.
func (hs *Styles) OpenJSON(filename core.Filename) error { //types:add
b, err := os.ReadFile(string(filename))
if err != nil {
// PromptDialog(nil, "File Not Found", err.Error(), true, false, nil, nil, nil)
// slog.Error(err.Error())
return err
}
return json.Unmarshal(b, hs)
}
// Save hi styles to a JSON-formatted file. You can save and open
// styles to / from files to share, experiment, transfer, etc.
func (hs *Styles) SaveJSON(filename core.Filename) error { //types:add
b, err := json.MarshalIndent(hs, "", " ")
if err != nil {
slog.Error(err.Error()) // unlikely
return err
}
err = os.WriteFile(string(filename), b, 0644)
if err != nil {
// PromptDialog(nil, "Could not Save to File", err.Error(), true, false, nil, nil, nil)
slog.Error(err.Error())
}
return err
}
// SettingsStylesFilename is the name of the preferences file in App data
// directory for saving / loading the custom styles
var SettingsStylesFilename = "highlighting.json"
// StylesChanged is used for gui updating while editing
var StylesChanged = false
// OpenSettings opens Styles from Cogent Core standard prefs directory, using SettingsStylesFilename
func (hs *Styles) OpenSettings() error {
pdir := core.TheApp.CogentCoreDataDir()
pnm := filepath.Join(pdir, SettingsStylesFilename)
StylesChanged = false
return hs.OpenJSON(core.Filename(pnm))
}
// SaveSettings saves Styles to Cogent Core standard prefs directory, using SettingsStylesFilename
func (hs *Styles) SaveSettings() error {
pdir := core.TheApp.CogentCoreDataDir()
pnm := filepath.Join(pdir, SettingsStylesFilename)
StylesChanged = false
MergeAvailStyles()
return hs.SaveJSON(core.Filename(pnm))
}
// SaveAll saves all styles individually to chosen directory
func (hs *Styles) SaveAll(dir core.Filename) {
for nm, st := range *hs {
fnm := filepath.Join(string(dir), nm+".highlighting")
st.SaveJSON(core.Filename(fnm))
}
}
// OpenDefaults opens the default highlighting styles (from chroma originally)
// These are encoded as an embed from defaults.highlighting
func (hs *Styles) OpenDefaults() error {
err := json.Unmarshal(defaults, hs)
if err != nil {
slog.Error(err.Error())
return err
}
return err
}
// Names outputs names of styles in collection
func (hs *Styles) Names() []string {
nms := make([]string, len(*hs))
idx := 0
for nm := range *hs {
nms[idx] = nm
idx++
}
sort.StringSlice(nms).Sort()
return nms
}
// ViewStandard shows the standard styles that are compiled
// into the program via chroma package
func (hs *Styles) ViewStandard() {
Editor(&StandardStyles)
}
// Init must be called to initialize the hi styles -- post startup
// so chroma stuff is all in place, and loads custom styles
func Init() {
parse.LanguageSupport.OpenStandard()
StandardStyles.OpenDefaults()
CustomStyles.OpenSettings()
if len(CustomStyles) == 0 {
cs := &Style{}
cs.CopyFrom(StandardStyles[string(StyleDefault)])
CustomStyles["custom-sample"] = cs
}
MergeAvailStyles()
UpdateFromTheme()
}
// 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 highlighting
import (
"cogentcore.org/core/parse/token"
"github.com/alecthomas/chroma/v2"
)
// FromChroma converts a chroma.TokenType to a parse token.Tokens
func TokenFromChroma(ct chroma.TokenType) token.Tokens {
if ChromaToTokensMap == nil {
ChromaToTokensMap = make(map[chroma.TokenType]token.Tokens, len(TokensToChromaMap))
for k, v := range TokensToChromaMap {
ChromaToTokensMap[v] = k
}
}
tok := ChromaToTokensMap[ct]
return tok
}
// TokenToChroma converts to a chroma.TokenType
func TokenToChroma(tok token.Tokens) chroma.TokenType {
return TokensToChromaMap[tok]
}
// ChromaToTokensMap maps from chroma.TokenType to Tokens -- built from opposite map
var ChromaToTokensMap map[chroma.TokenType]token.Tokens
// TokensToChromaMap maps from Tokens to chroma.TokenType
var TokensToChromaMap = map[token.Tokens]chroma.TokenType{
token.EOF: chroma.EOFType,
token.Background: chroma.Background,
token.Error: chroma.Error,
token.None: chroma.None,
token.Keyword: chroma.Keyword,
token.KeywordConstant: chroma.KeywordConstant,
token.KeywordDeclaration: chroma.KeywordDeclaration,
token.KeywordNamespace: chroma.KeywordNamespace,
token.KeywordPseudo: chroma.KeywordPseudo,
token.KeywordReserved: chroma.KeywordReserved,
token.KeywordType: chroma.KeywordType,
token.Name: chroma.Name,
token.NameAttribute: chroma.NameAttribute,
token.NameBuiltin: chroma.NameBuiltin,
token.NameBuiltinPseudo: chroma.NameBuiltinPseudo,
token.NameClass: chroma.NameClass,
token.NameConstant: chroma.NameConstant,
token.NameDecorator: chroma.NameDecorator,
token.NameEntity: chroma.NameEntity,
token.NameException: chroma.NameException,
token.NameFunction: chroma.NameFunction,
token.NameFunctionMagic: chroma.NameFunctionMagic,
token.NameLabel: chroma.NameLabel,
token.NameNamespace: chroma.NameNamespace,
token.NameOperator: chroma.NameOperator,
token.NameOther: chroma.NameOther,
token.NamePseudo: chroma.NamePseudo,
token.NameProperty: chroma.NameProperty,
token.NameTag: chroma.NameTag,
token.NameVar: chroma.NameVariable,
token.NameVarAnonymous: chroma.NameVariableAnonymous,
token.NameVarClass: chroma.NameVariableClass,
token.NameVarGlobal: chroma.NameVariableGlobal,
token.NameVarInstance: chroma.NameVariableInstance,
token.NameVarMagic: chroma.NameVariableMagic,
token.Literal: chroma.Literal,
token.LiteralDate: chroma.LiteralDate,
token.LiteralOther: chroma.LiteralOther,
token.LitStr: chroma.LiteralString,
token.LitStrAffix: chroma.LiteralStringAffix,
token.LitStrAtom: chroma.LiteralStringAtom,
token.LitStrBacktick: chroma.LiteralStringBacktick,
token.LitStrBoolean: chroma.LiteralStringBoolean,
token.LitStrChar: chroma.LiteralStringChar,
token.LitStrDelimiter: chroma.LiteralStringDelimiter,
token.LitStrDoc: chroma.LiteralStringDoc,
token.LitStrDouble: chroma.LiteralStringDouble,
token.LitStrEscape: chroma.LiteralStringEscape,
token.LitStrHeredoc: chroma.LiteralStringHeredoc,
token.LitStrInterpol: chroma.LiteralStringInterpol,
token.LitStrName: chroma.LiteralStringName,
token.LitStrOther: chroma.LiteralStringOther,
token.LitStrRegex: chroma.LiteralStringRegex,
token.LitStrSingle: chroma.LiteralStringSingle,
token.LitStrSymbol: chroma.LiteralStringSymbol,
token.LitNum: chroma.LiteralNumber,
token.LitNumBin: chroma.LiteralNumberBin,
token.LitNumFloat: chroma.LiteralNumberFloat,
token.LitNumHex: chroma.LiteralNumberHex,
token.LitNumInteger: chroma.LiteralNumberInteger,
token.LitNumIntegerLong: chroma.LiteralNumberIntegerLong,
token.LitNumOct: chroma.LiteralNumberOct,
token.Operator: chroma.Operator,
token.OperatorWord: chroma.OperatorWord,
token.Punctuation: chroma.Punctuation,
token.Comment: chroma.Comment,
token.CommentHashbang: chroma.CommentHashbang,
token.CommentMultiline: chroma.CommentMultiline,
token.CommentSingle: chroma.CommentSingle,
token.CommentSpecial: chroma.CommentSpecial,
token.CommentPreproc: chroma.CommentPreproc,
token.CommentPreprocFile: chroma.CommentPreprocFile,
token.Text: chroma.Text,
token.TextWhitespace: chroma.TextWhitespace,
token.TextSymbol: chroma.TextSymbol,
token.TextPunctuation: chroma.TextPunctuation,
token.TextStyle: chroma.Generic,
token.TextStyleDeleted: chroma.GenericDeleted,
token.TextStyleEmph: chroma.GenericEmph,
token.TextStyleError: chroma.GenericError,
token.TextStyleHeading: chroma.GenericHeading,
token.TextStyleInserted: chroma.GenericInserted,
token.TextStyleOutput: chroma.GenericOutput,
token.TextStylePrompt: chroma.GenericPrompt,
token.TextStyleStrong: chroma.GenericStrong,
token.TextStyleSubheading: chroma.GenericSubheading,
token.TextStyleTraceback: chroma.GenericTraceback,
token.TextStyleUnderline: chroma.GenericUnderline,
}
// 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 highlighting
import (
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/icons"
"cogentcore.org/core/tree"
)
func init() {
core.AddValueType[core.HighlightingName, Button]()
}
// Button represents a [core.HighlightingName] with a button.
type Button struct {
core.Button
HighlightingName string
}
func (hb *Button) WidgetValue() any { return &hb.HighlightingName }
func (hb *Button) Init() {
hb.Button.Init()
hb.SetType(core.ButtonTonal).SetIcon(icons.Brush)
hb.Updater(func() {
hb.SetText(hb.HighlightingName)
})
core.InitValueButton(hb, false, func(d *core.Body) {
d.SetTitle("Select a syntax highlighting style")
si := 0
ls := core.NewList(d).SetSlice(&StyleNames).SetSelectedValue(hb.HighlightingName).BindSelect(&si)
ls.OnChange(func(e events.Event) {
hb.HighlightingName = StyleNames[si]
})
})
}
// Editor opens an editor of highlighting styles.
func Editor(st *Styles) {
if core.RecycleMainWindow(st) {
return
}
d := core.NewBody("Highlighting styles").SetData(st)
core.NewText(d).SetType(core.TextSupporting).SetText("View standard to see the builtin styles, from which you can add and customize by saving ones from the standard and then loading them into a custom file to modify.")
kl := core.NewKeyedList(d).SetMap(st)
StylesChanged = false
kl.OnChange(func(e events.Event) {
StylesChanged = true
})
d.AddTopBar(func(bar *core.Frame) {
core.NewToolbar(bar).Maker(func(p *tree.Plan) {
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(st.OpenJSON).SetText("Open from file").SetIcon(icons.Open)
w.Args[0].SetTag(`extension:".highlighting"`)
})
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(st.SaveJSON).SetText("Save from file").SetIcon(icons.Save)
w.Args[0].SetTag(`extension:".highlighting"`)
})
tree.Add(p, func(w *core.FuncButton) {
w.SetFunc(st.ViewStandard).SetIcon(icons.Visibility)
})
tree.Add(p, func(w *core.Separator) {})
kl.MakeToolbar(p)
})
})
d.RunWindow() // note: no context here so not dialog
}
// 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 texteditor
import (
"fmt"
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/core"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles"
)
// maxGrowLines is the maximum number of lines to grow to
// (subject to other styling constraints).
const maxGrowLines = 25
// styleSizes gets the size info based on Style settings.
func (ed *Editor) styleSizes() {
sty := &ed.Styles
spc := sty.BoxSpace()
sty.Font = paint.OpenFont(sty.FontRender(), &sty.UnitContext)
ed.fontHeight = sty.Font.Face.Metrics.Height
ed.lineHeight = sty.Text.EffLineHeight(ed.fontHeight)
ed.fontDescent = math32.FromFixed(ed.Styles.Font.Face.Face.Metrics().Descent)
ed.fontAscent = math32.FromFixed(ed.Styles.Font.Face.Face.Metrics().Ascent)
ed.lineNumberDigits = max(1+int(math32.Log10(float32(ed.NumLines))), 3)
lno := true
if ed.Buffer != nil {
lno = ed.Buffer.Options.LineNumbers
}
if lno {
ed.hasLineNumbers = true
ed.LineNumberOffset = float32(ed.lineNumberDigits+3)*sty.Font.Face.Metrics.Ch + spc.Left // space for icon
} else {
ed.hasLineNumbers = false
ed.LineNumberOffset = 0
}
}
// updateFromAlloc updates size info based on allocated size:
// NLinesChars, LineNumberOff, LineLayoutSize
func (ed *Editor) updateFromAlloc() {
sty := &ed.Styles
asz := ed.Geom.Size.Alloc.Content
spsz := sty.BoxSpace().Size()
asz.SetSub(spsz)
sbw := math32.Ceil(ed.Styles.ScrollbarWidth.Dots)
asz.X -= sbw
if ed.HasScroll[math32.X] {
asz.Y -= sbw
}
ed.lineLayoutSize = asz
if asz == (math32.Vector2{}) {
ed.nLinesChars.Y = 20
ed.nLinesChars.X = 80
} else {
ed.nLinesChars.Y = int(math32.Floor(float32(asz.Y) / ed.lineHeight))
if sty.Font.Face != nil {
ed.nLinesChars.X = int(math32.Floor(float32(asz.X) / sty.Font.Face.Metrics.Ch))
}
}
ed.lineLayoutSize.X -= ed.LineNumberOffset
}
func (ed *Editor) internalSizeFromLines() {
ed.totalSize = ed.linesSize
ed.totalSize.X += ed.LineNumberOffset
ed.Geom.Size.Internal = ed.totalSize
ed.Geom.Size.Internal.Y += ed.lineHeight
}
// layoutAllLines generates paint.Text Renders of lines
// from the Markup version of the source in Buf.
// It computes the total LinesSize and TotalSize.
func (ed *Editor) layoutAllLines() {
ed.updateFromAlloc()
if ed.lineLayoutSize.Y == 0 || ed.Styles.Font.Size.Value == 0 {
return
}
if ed.Buffer == nil || ed.Buffer.NumLines() == 0 {
ed.NumLines = 0
return
}
ed.lastFilename = ed.Buffer.Filename
ed.NumLines = ed.Buffer.NumLines()
ed.Buffer.Lock()
ed.Buffer.Highlighter.TabSize = ed.Styles.Text.TabSize
buf := ed.Buffer
nln := ed.NumLines
if nln >= len(buf.Markup) {
nln = len(buf.Markup)
}
ed.renders = slicesx.SetLength(ed.renders, nln)
ed.offsets = slicesx.SetLength(ed.offsets, nln)
sz := ed.lineLayoutSize
sty := &ed.Styles
fst := sty.FontRender()
fst.Background = nil
off := float32(0)
mxwd := sz.X // always start with our render size
ed.hasLinks = false
cssAgg := ed.textStyleProperties()
for ln := 0; ln < nln; ln++ {
if ln >= len(ed.renders) || ln >= len(buf.Markup) {
break
}
rn := &ed.renders[ln]
rn.SetHTMLPre(buf.Markup[ln], fst, &sty.Text, &sty.UnitContext, cssAgg)
rn.Layout(&sty.Text, sty.FontRender(), &sty.UnitContext, sz)
if !ed.hasLinks && len(rn.Links) > 0 {
ed.hasLinks = true
}
ed.offsets[ln] = off
lsz := math32.Ceil(math32.Max(rn.BBox.Size().Y, ed.lineHeight))
off += lsz
mxwd = math32.Max(mxwd, rn.BBox.Size().X)
}
buf.Unlock()
ed.linesSize = math32.Vec2(mxwd, off)
ed.lastlineLayoutSize = ed.lineLayoutSize
ed.internalSizeFromLines()
}
// reLayoutAllLines updates the Renders Layout given current size, if changed
func (ed *Editor) reLayoutAllLines() {
ed.updateFromAlloc()
if ed.lineLayoutSize.Y == 0 || ed.Styles.Font.Size.Value == 0 {
return
}
if ed.Buffer == nil || ed.Buffer.NumLines() == 0 {
return
}
if ed.lastlineLayoutSize == ed.lineLayoutSize {
ed.internalSizeFromLines()
return
}
ed.layoutAllLines()
}
// note: Layout reverts to basic Widget behavior for layout if no kids, like us..
func (ed *Editor) SizeUp() {
ed.Frame.SizeUp() // sets Actual size based on styles
sz := &ed.Geom.Size
if ed.Buffer == nil {
return
}
nln := ed.Buffer.NumLines()
if nln == 0 {
return
}
if ed.Styles.Grow.Y == 0 {
maxh := maxGrowLines * ed.lineHeight
ty := styles.ClampMin(styles.ClampMax(min(float32(nln+1)*ed.lineHeight, maxh), sz.Max.Y), sz.Min.Y)
sz.Actual.Content.Y = ty
sz.Actual.Total.Y = sz.Actual.Content.Y + sz.Space.Y
if core.DebugSettings.LayoutTrace {
fmt.Println(ed, "texteditor SizeUp targ:", ty, "nln:", nln, "Actual:", sz.Actual.Content)
}
}
}
func (ed *Editor) SizeDown(iter int) bool {
if iter == 0 {
ed.layoutAllLines()
} else {
ed.reLayoutAllLines()
}
// use actual lineSize from layout to ensure fit
sz := &ed.Geom.Size
maxh := maxGrowLines * ed.lineHeight
ty := ed.linesSize.Y + 1*ed.lineHeight
ty = styles.ClampMin(styles.ClampMax(min(ty, maxh), sz.Max.Y), sz.Min.Y)
if ed.Styles.Grow.Y == 0 {
sz.Actual.Content.Y = ty
sz.Actual.Total.Y = sz.Actual.Content.Y + sz.Space.Y
}
if core.DebugSettings.LayoutTrace {
fmt.Println(ed, "texteditor SizeDown targ:", ty, "linesSize:", ed.linesSize.Y, "Actual:", sz.Actual.Content)
}
redo := ed.Frame.SizeDown(iter)
if ed.Styles.Grow.Y == 0 {
sz.Actual.Content.Y = ty
sz.Actual.Content.Y = min(ty, sz.Alloc.Content.Y)
}
sz.Actual.Total.Y = sz.Actual.Content.Y + sz.Space.Y
chg := ed.ManageOverflow(iter, true) // this must go first.
return redo || chg
}
func (ed *Editor) SizeFinal() {
ed.Frame.SizeFinal()
ed.reLayoutAllLines()
}
func (ed *Editor) Position() {
ed.Frame.Position()
ed.ConfigScrolls()
}
func (ed *Editor) ApplyScenePos() {
ed.Frame.ApplyScenePos()
ed.PositionScrolls()
}
// layoutLine generates render of given line (including highlighting).
// If the line with exceeds the current maximum, or the number of effective
// lines (e.g., from word-wrap) is different, then NeedsLayout is called
// and it returns true.
func (ed *Editor) layoutLine(ln int) bool {
if ed.Buffer == nil || ed.Buffer.NumLines() == 0 || ln >= len(ed.renders) {
return false
}
sty := &ed.Styles
fst := sty.FontRender()
fst.Background = nil
mxwd := float32(ed.linesSize.X)
needLay := false
rn := &ed.renders[ln]
curspans := len(rn.Spans)
ed.Buffer.Lock()
rn.SetHTMLPre(ed.Buffer.Markup[ln], fst, &sty.Text, &sty.UnitContext, ed.textStyleProperties())
ed.Buffer.Unlock()
rn.Layout(&sty.Text, sty.FontRender(), &sty.UnitContext, ed.lineLayoutSize)
if !ed.hasLinks && len(rn.Links) > 0 {
ed.hasLinks = true
}
nwspans := len(rn.Spans)
if nwspans != curspans && (nwspans > 1 || curspans > 1) {
needLay = true
}
if rn.BBox.Size().X > mxwd {
needLay = true
}
if needLay {
ed.NeedsLayout()
} else {
ed.NeedsRender()
}
return needLay
}
// 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 texteditor
import (
"image"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/math32"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/texteditor/text"
)
///////////////////////////////////////////////////////////////////////////////
// Cursor Navigation
// cursorMovedEvent sends the event that cursor has moved
func (ed *Editor) cursorMovedEvent() {
ed.Send(events.Input, nil)
}
// validateCursor sets current cursor to a valid cursor position
func (ed *Editor) validateCursor() {
if ed.Buffer != nil {
ed.CursorPos = ed.Buffer.ValidPos(ed.CursorPos)
} else {
ed.CursorPos = lexer.PosZero
}
}
// wrappedLines returns the number of wrapped lines (spans) for given line number
func (ed *Editor) wrappedLines(ln int) int {
if ln >= len(ed.renders) {
return 0
}
return len(ed.renders[ln].Spans)
}
// wrappedLineNumber returns the wrapped line number (span index) and rune index
// within that span of the given character position within line in position,
// and false if out of range (last valid position returned in that case -- still usable).
func (ed *Editor) wrappedLineNumber(pos lexer.Pos) (si, ri int, ok bool) {
if pos.Ln >= len(ed.renders) {
return 0, 0, false
}
return ed.renders[pos.Ln].RuneSpanPos(pos.Ch)
}
// setCursor sets a new cursor position, enforcing it in range.
// This is the main final pathway for all cursor movement.
func (ed *Editor) setCursor(pos lexer.Pos) {
if ed.NumLines == 0 || ed.Buffer == nil {
ed.CursorPos = lexer.PosZero
return
}
ed.clearScopelights()
ed.CursorPos = ed.Buffer.ValidPos(pos)
ed.cursorMovedEvent()
txt := ed.Buffer.Line(ed.CursorPos.Ln)
ch := ed.CursorPos.Ch
if ch < len(txt) {
r := txt[ch]
if r == '{' || r == '}' || r == '(' || r == ')' || r == '[' || r == ']' {
tp, found := ed.Buffer.BraceMatch(txt[ch], ed.CursorPos)
if found {
ed.scopelights = append(ed.scopelights, text.NewRegionPos(ed.CursorPos, lexer.Pos{ed.CursorPos.Ln, ed.CursorPos.Ch + 1}))
ed.scopelights = append(ed.scopelights, text.NewRegionPos(tp, lexer.Pos{tp.Ln, tp.Ch + 1}))
}
}
}
ed.NeedsRender()
}
// SetCursorShow sets a new cursor position, enforcing it in range, and shows
// the cursor (scroll to if hidden, render)
func (ed *Editor) SetCursorShow(pos lexer.Pos) {
ed.setCursor(pos)
ed.scrollCursorToCenterIfHidden()
ed.renderCursor(true)
}
// SetCursorTarget sets a new cursor target position, ensures that it is visible
func (ed *Editor) SetCursorTarget(pos lexer.Pos) {
ed.targetSet = true
ed.cursorTarget = pos
ed.SetCursorShow(pos)
ed.NeedsRender()
// fmt.Println(ed, "set target:", ed.CursorTarg)
}
// setCursorColumn sets the current target cursor column (cursorColumn) to that
// of the given position
func (ed *Editor) setCursorColumn(pos lexer.Pos) {
if wln := ed.wrappedLines(pos.Ln); wln > 1 {
si, ri, ok := ed.wrappedLineNumber(pos)
if ok && si > 0 {
ed.cursorColumn = ri
} else {
ed.cursorColumn = pos.Ch
}
} else {
ed.cursorColumn = pos.Ch
}
}
// savePosHistory saves the cursor position in history stack of cursor positions
func (ed *Editor) savePosHistory(pos lexer.Pos) {
if ed.Buffer == nil {
return
}
ed.Buffer.savePosHistory(pos)
ed.posHistoryIndex = len(ed.Buffer.posHistory) - 1
}
// CursorToHistoryPrev moves cursor to previous position on history list --
// returns true if moved
func (ed *Editor) CursorToHistoryPrev() bool {
if ed.NumLines == 0 || ed.Buffer == nil {
ed.CursorPos = lexer.PosZero
return false
}
sz := len(ed.Buffer.posHistory)
if sz == 0 {
return false
}
ed.posHistoryIndex--
if ed.posHistoryIndex < 0 {
ed.posHistoryIndex = 0
return false
}
ed.posHistoryIndex = min(sz-1, ed.posHistoryIndex)
pos := ed.Buffer.posHistory[ed.posHistoryIndex]
ed.CursorPos = ed.Buffer.ValidPos(pos)
ed.cursorMovedEvent()
ed.scrollCursorToCenterIfHidden()
ed.renderCursor(true)
return true
}
// CursorToHistoryNext moves cursor to previous position on history list --
// returns true if moved
func (ed *Editor) CursorToHistoryNext() bool {
if ed.NumLines == 0 || ed.Buffer == nil {
ed.CursorPos = lexer.PosZero
return false
}
sz := len(ed.Buffer.posHistory)
if sz == 0 {
return false
}
ed.posHistoryIndex++
if ed.posHistoryIndex >= sz-1 {
ed.posHistoryIndex = sz - 1
return false
}
pos := ed.Buffer.posHistory[ed.posHistoryIndex]
ed.CursorPos = ed.Buffer.ValidPos(pos)
ed.cursorMovedEvent()
ed.scrollCursorToCenterIfHidden()
ed.renderCursor(true)
return true
}
// selectRegionUpdate updates current select region based on given cursor position
// relative to SelectStart position
func (ed *Editor) selectRegionUpdate(pos lexer.Pos) {
if pos.IsLess(ed.selectStart) {
ed.SelectRegion.Start = pos
ed.SelectRegion.End = ed.selectStart
} else {
ed.SelectRegion.Start = ed.selectStart
ed.SelectRegion.End = pos
}
}
// cursorSelect updates selection based on cursor movements, given starting
// cursor position and ed.CursorPos is current
func (ed *Editor) cursorSelect(org lexer.Pos) {
if !ed.selectMode {
return
}
ed.selectRegionUpdate(ed.CursorPos)
}
// cursorForward moves the cursor forward
func (ed *Editor) cursorForward(steps int) {
ed.validateCursor()
org := ed.CursorPos
for i := 0; i < steps; i++ {
ed.CursorPos.Ch++
if ed.CursorPos.Ch > ed.Buffer.LineLen(ed.CursorPos.Ln) {
if ed.CursorPos.Ln < ed.NumLines-1 {
ed.CursorPos.Ch = 0
ed.CursorPos.Ln++
} else {
ed.CursorPos.Ch = ed.Buffer.LineLen(ed.CursorPos.Ln)
}
}
}
ed.setCursorColumn(ed.CursorPos)
ed.SetCursorShow(ed.CursorPos)
ed.cursorSelect(org)
ed.NeedsRender()
}
// cursorForwardWord moves the cursor forward by words
func (ed *Editor) cursorForwardWord(steps int) {
ed.validateCursor()
org := ed.CursorPos
for i := 0; i < steps; i++ {
txt := ed.Buffer.Line(ed.CursorPos.Ln)
sz := len(txt)
if sz > 0 && ed.CursorPos.Ch < sz {
ch := ed.CursorPos.Ch
var done = false
for ch < sz && !done { // if on a wb, go past
r1 := txt[ch]
r2 := rune(-1)
if ch < sz-1 {
r2 = txt[ch+1]
}
if core.IsWordBreak(r1, r2) {
ch++
} else {
done = true
}
}
done = false
for ch < sz && !done {
r1 := txt[ch]
r2 := rune(-1)
if ch < sz-1 {
r2 = txt[ch+1]
}
if !core.IsWordBreak(r1, r2) {
ch++
} else {
done = true
}
}
ed.CursorPos.Ch = ch
} else {
if ed.CursorPos.Ln < ed.NumLines-1 {
ed.CursorPos.Ch = 0
ed.CursorPos.Ln++
} else {
ed.CursorPos.Ch = ed.Buffer.LineLen(ed.CursorPos.Ln)
}
}
}
ed.setCursorColumn(ed.CursorPos)
ed.SetCursorShow(ed.CursorPos)
ed.cursorSelect(org)
ed.NeedsRender()
}
// cursorDown moves the cursor down line(s)
func (ed *Editor) cursorDown(steps int) {
ed.validateCursor()
org := ed.CursorPos
pos := ed.CursorPos
for i := 0; i < steps; i++ {
gotwrap := false
if wln := ed.wrappedLines(pos.Ln); wln > 1 {
si, ri, _ := ed.wrappedLineNumber(pos)
if si < wln-1 {
si++
mxlen := min(len(ed.renders[pos.Ln].Spans[si].Text), ed.cursorColumn)
if ed.cursorColumn < mxlen {
ri = ed.cursorColumn
} else {
ri = mxlen
}
nwc, _ := ed.renders[pos.Ln].SpanPosToRuneIndex(si, ri)
pos.Ch = nwc
gotwrap = true
}
}
if !gotwrap {
pos.Ln++
if pos.Ln >= ed.NumLines {
pos.Ln = ed.NumLines - 1
break
}
mxlen := min(ed.Buffer.LineLen(pos.Ln), ed.cursorColumn)
if ed.cursorColumn < mxlen {
pos.Ch = ed.cursorColumn
} else {
pos.Ch = mxlen
}
}
}
ed.SetCursorShow(pos)
ed.cursorSelect(org)
ed.NeedsRender()
}
// cursorPageDown moves the cursor down page(s), where a page is defined abcdef
// dynamically as just moving the cursor off the screen
func (ed *Editor) cursorPageDown(steps int) {
ed.validateCursor()
org := ed.CursorPos
for i := 0; i < steps; i++ {
lvln := ed.lastVisibleLine(ed.CursorPos.Ln)
ed.CursorPos.Ln = lvln
if ed.CursorPos.Ln >= ed.NumLines {
ed.CursorPos.Ln = ed.NumLines - 1
}
ed.CursorPos.Ch = min(ed.Buffer.LineLen(ed.CursorPos.Ln), ed.cursorColumn)
ed.scrollCursorToTop()
ed.renderCursor(true)
}
ed.setCursor(ed.CursorPos)
ed.cursorSelect(org)
ed.NeedsRender()
}
// cursorBackward moves the cursor backward
func (ed *Editor) cursorBackward(steps int) {
ed.validateCursor()
org := ed.CursorPos
for i := 0; i < steps; i++ {
ed.CursorPos.Ch--
if ed.CursorPos.Ch < 0 {
if ed.CursorPos.Ln > 0 {
ed.CursorPos.Ln--
ed.CursorPos.Ch = ed.Buffer.LineLen(ed.CursorPos.Ln)
} else {
ed.CursorPos.Ch = 0
}
}
}
ed.setCursorColumn(ed.CursorPos)
ed.SetCursorShow(ed.CursorPos)
ed.cursorSelect(org)
ed.NeedsRender()
}
// cursorBackwardWord moves the cursor backward by words
func (ed *Editor) cursorBackwardWord(steps int) {
ed.validateCursor()
org := ed.CursorPos
for i := 0; i < steps; i++ {
txt := ed.Buffer.Line(ed.CursorPos.Ln)
sz := len(txt)
if sz > 0 && ed.CursorPos.Ch > 0 {
ch := min(ed.CursorPos.Ch, sz-1)
var done = false
for ch < sz && !done { // if on a wb, go past
r1 := txt[ch]
r2 := rune(-1)
if ch > 0 {
r2 = txt[ch-1]
}
if core.IsWordBreak(r1, r2) {
ch--
if ch == -1 {
done = true
}
} else {
done = true
}
}
done = false
for ch < sz && ch >= 0 && !done {
r1 := txt[ch]
r2 := rune(-1)
if ch > 0 {
r2 = txt[ch-1]
}
if !core.IsWordBreak(r1, r2) {
ch--
} else {
done = true
}
}
ed.CursorPos.Ch = ch
} else {
if ed.CursorPos.Ln > 0 {
ed.CursorPos.Ln--
ed.CursorPos.Ch = ed.Buffer.LineLen(ed.CursorPos.Ln)
} else {
ed.CursorPos.Ch = 0
}
}
}
ed.setCursorColumn(ed.CursorPos)
ed.SetCursorShow(ed.CursorPos)
ed.cursorSelect(org)
ed.NeedsRender()
}
// cursorUp moves the cursor up line(s)
func (ed *Editor) cursorUp(steps int) {
ed.validateCursor()
org := ed.CursorPos
pos := ed.CursorPos
for i := 0; i < steps; i++ {
gotwrap := false
if wln := ed.wrappedLines(pos.Ln); wln > 1 {
si, ri, _ := ed.wrappedLineNumber(pos)
if si > 0 {
ri = ed.cursorColumn
nwc, _ := ed.renders[pos.Ln].SpanPosToRuneIndex(si-1, ri)
if nwc == pos.Ch {
ed.cursorColumn = 0
ri = 0
nwc, _ = ed.renders[pos.Ln].SpanPosToRuneIndex(si-1, ri)
}
pos.Ch = nwc
gotwrap = true
}
}
if !gotwrap {
pos.Ln--
if pos.Ln < 0 {
pos.Ln = 0
break
}
if wln := ed.wrappedLines(pos.Ln); wln > 1 { // just entered end of wrapped line
si := wln - 1
ri := ed.cursorColumn
nwc, _ := ed.renders[pos.Ln].SpanPosToRuneIndex(si, ri)
pos.Ch = nwc
} else {
mxlen := min(ed.Buffer.LineLen(pos.Ln), ed.cursorColumn)
if ed.cursorColumn < mxlen {
pos.Ch = ed.cursorColumn
} else {
pos.Ch = mxlen
}
}
}
}
ed.SetCursorShow(pos)
ed.cursorSelect(org)
ed.NeedsRender()
}
// cursorPageUp moves the cursor up page(s), where a page is defined
// dynamically as just moving the cursor off the screen
func (ed *Editor) cursorPageUp(steps int) {
ed.validateCursor()
org := ed.CursorPos
for i := 0; i < steps; i++ {
lvln := ed.firstVisibleLine(ed.CursorPos.Ln)
ed.CursorPos.Ln = lvln
if ed.CursorPos.Ln <= 0 {
ed.CursorPos.Ln = 0
}
ed.CursorPos.Ch = min(ed.Buffer.LineLen(ed.CursorPos.Ln), ed.cursorColumn)
ed.scrollCursorToBottom()
ed.renderCursor(true)
}
ed.setCursor(ed.CursorPos)
ed.cursorSelect(org)
ed.NeedsRender()
}
// cursorRecenter re-centers the view around the cursor position, toggling
// between putting cursor in middle, top, and bottom of view
func (ed *Editor) cursorRecenter() {
ed.validateCursor()
ed.savePosHistory(ed.CursorPos)
cur := (ed.lastRecenter + 1) % 3
switch cur {
case 0:
ed.scrollCursorToBottom()
case 1:
ed.scrollCursorToVerticalCenter()
case 2:
ed.scrollCursorToTop()
}
ed.lastRecenter = cur
}
// cursorStartLine moves the cursor to the start of the line, updating selection
// if select mode is active
func (ed *Editor) cursorStartLine() {
ed.validateCursor()
org := ed.CursorPos
pos := ed.CursorPos
gotwrap := false
if wln := ed.wrappedLines(pos.Ln); wln > 1 {
si, ri, _ := ed.wrappedLineNumber(pos)
if si > 0 {
ri = 0
nwc, _ := ed.renders[pos.Ln].SpanPosToRuneIndex(si, ri)
pos.Ch = nwc
ed.CursorPos = pos
ed.cursorColumn = ri
gotwrap = true
}
}
if !gotwrap {
ed.CursorPos.Ch = 0
ed.cursorColumn = ed.CursorPos.Ch
}
// fmt.Printf("sol cursorcol: %v\n", ed.CursorCol)
ed.setCursor(ed.CursorPos)
ed.scrollCursorToRight()
ed.renderCursor(true)
ed.cursorSelect(org)
ed.NeedsRender()
}
// CursorStartDoc moves the cursor to the start of the text, updating selection
// if select mode is active
func (ed *Editor) CursorStartDoc() {
ed.validateCursor()
org := ed.CursorPos
ed.CursorPos.Ln = 0
ed.CursorPos.Ch = 0
ed.cursorColumn = ed.CursorPos.Ch
ed.setCursor(ed.CursorPos)
ed.scrollCursorToTop()
ed.renderCursor(true)
ed.cursorSelect(org)
ed.NeedsRender()
}
// cursorEndLine moves the cursor to the end of the text
func (ed *Editor) cursorEndLine() {
ed.validateCursor()
org := ed.CursorPos
pos := ed.CursorPos
gotwrap := false
if wln := ed.wrappedLines(pos.Ln); wln > 1 {
si, ri, _ := ed.wrappedLineNumber(pos)
ri = len(ed.renders[pos.Ln].Spans[si].Text) - 1
nwc, _ := ed.renders[pos.Ln].SpanPosToRuneIndex(si, ri)
if si == len(ed.renders[pos.Ln].Spans)-1 { // last span
ri++
nwc++
}
ed.cursorColumn = ri
pos.Ch = nwc
ed.CursorPos = pos
gotwrap = true
}
if !gotwrap {
ed.CursorPos.Ch = ed.Buffer.LineLen(ed.CursorPos.Ln)
ed.cursorColumn = ed.CursorPos.Ch
}
ed.setCursor(ed.CursorPos)
ed.scrollCursorToRight()
ed.renderCursor(true)
ed.cursorSelect(org)
ed.NeedsRender()
}
// cursorEndDoc moves the cursor to the end of the text, updating selection if
// select mode is active
func (ed *Editor) cursorEndDoc() {
ed.validateCursor()
org := ed.CursorPos
ed.CursorPos.Ln = max(ed.NumLines-1, 0)
ed.CursorPos.Ch = ed.Buffer.LineLen(ed.CursorPos.Ln)
ed.cursorColumn = ed.CursorPos.Ch
ed.setCursor(ed.CursorPos)
ed.scrollCursorToBottom()
ed.renderCursor(true)
ed.cursorSelect(org)
ed.NeedsRender()
}
// todo: ctrl+backspace = delete word
// shift+arrow = select
// uparrow = start / down = end
// cursorBackspace deletes character(s) immediately before cursor
func (ed *Editor) cursorBackspace(steps int) {
ed.validateCursor()
org := ed.CursorPos
if ed.HasSelection() {
org = ed.SelectRegion.Start
ed.deleteSelection()
ed.SetCursorShow(org)
return
}
// note: no update b/c signal from buf will drive update
ed.cursorBackward(steps)
ed.scrollCursorToCenterIfHidden()
ed.renderCursor(true)
ed.Buffer.DeleteText(ed.CursorPos, org, EditSignal)
ed.NeedsRender()
}
// cursorDelete deletes character(s) immediately after the cursor
func (ed *Editor) cursorDelete(steps int) {
ed.validateCursor()
if ed.HasSelection() {
ed.deleteSelection()
return
}
// note: no update b/c signal from buf will drive update
org := ed.CursorPos
ed.cursorForward(steps)
ed.Buffer.DeleteText(org, ed.CursorPos, EditSignal)
ed.SetCursorShow(org)
ed.NeedsRender()
}
// cursorBackspaceWord deletes words(s) immediately before cursor
func (ed *Editor) cursorBackspaceWord(steps int) {
ed.validateCursor()
org := ed.CursorPos
if ed.HasSelection() {
ed.deleteSelection()
ed.SetCursorShow(org)
return
}
// note: no update b/c signal from buf will drive update
ed.cursorBackwardWord(steps)
ed.scrollCursorToCenterIfHidden()
ed.renderCursor(true)
ed.Buffer.DeleteText(ed.CursorPos, org, EditSignal)
ed.NeedsRender()
}
// cursorDeleteWord deletes word(s) immediately after the cursor
func (ed *Editor) cursorDeleteWord(steps int) {
ed.validateCursor()
if ed.HasSelection() {
ed.deleteSelection()
return
}
// note: no update b/c signal from buf will drive update
org := ed.CursorPos
ed.cursorForwardWord(steps)
ed.Buffer.DeleteText(org, ed.CursorPos, EditSignal)
ed.SetCursorShow(org)
ed.NeedsRender()
}
// cursorKill deletes text from cursor to end of text
func (ed *Editor) cursorKill() {
ed.validateCursor()
org := ed.CursorPos
pos := ed.CursorPos
atEnd := false
if wln := ed.wrappedLines(pos.Ln); wln > 1 {
si, ri, _ := ed.wrappedLineNumber(pos)
llen := len(ed.renders[pos.Ln].Spans[si].Text)
if si == wln-1 {
llen--
}
atEnd = (ri == llen)
} else {
llen := ed.Buffer.LineLen(pos.Ln)
atEnd = (ed.CursorPos.Ch == llen)
}
if atEnd {
ed.cursorForward(1)
} else {
ed.cursorEndLine()
}
ed.Buffer.DeleteText(org, ed.CursorPos, EditSignal)
ed.SetCursorShow(org)
ed.NeedsRender()
}
// cursorTranspose swaps the character at the cursor with the one before it
func (ed *Editor) cursorTranspose() {
ed.validateCursor()
pos := ed.CursorPos
if pos.Ch == 0 {
return
}
ppos := pos
ppos.Ch--
lln := ed.Buffer.LineLen(pos.Ln)
end := false
if pos.Ch >= lln {
end = true
pos.Ch = lln - 1
ppos.Ch = lln - 2
}
chr := ed.Buffer.LineChar(pos.Ln, pos.Ch)
pchr := ed.Buffer.LineChar(pos.Ln, ppos.Ch)
repl := string([]rune{chr, pchr})
pos.Ch++
ed.Buffer.ReplaceText(ppos, pos, ppos, repl, EditSignal, ReplaceMatchCase)
if !end {
ed.SetCursorShow(pos)
}
ed.NeedsRender()
}
// CursorTranspose swaps the word at the cursor with the one before it
func (ed *Editor) cursorTransposeWord() {
}
// JumpToLinePrompt jumps to given line number (minus 1) from prompt
func (ed *Editor) JumpToLinePrompt() {
val := ""
d := core.NewBody("Jump to line")
core.NewText(d).SetType(core.TextSupporting).SetText("Line number to jump to")
tf := core.NewTextField(d).SetPlaceholder("Line number")
tf.OnChange(func(e events.Event) {
val = tf.Text()
})
d.AddBottomBar(func(bar *core.Frame) {
d.AddCancel(bar)
d.AddOK(bar).SetText("Jump").OnClick(func(e events.Event) {
val = tf.Text()
ln, err := reflectx.ToInt(val)
if err == nil {
ed.jumpToLine(int(ln))
}
})
})
d.RunDialog(ed)
}
// jumpToLine jumps to given line number (minus 1)
func (ed *Editor) jumpToLine(ln int) {
ed.SetCursorShow(lexer.Pos{Ln: ln - 1})
ed.savePosHistory(ed.CursorPos)
ed.NeedsLayout()
}
// findNextLink finds next link after given position, returns false if no such links
func (ed *Editor) findNextLink(pos lexer.Pos) (lexer.Pos, text.Region, bool) {
for ln := pos.Ln; ln < ed.NumLines; ln++ {
if len(ed.renders[ln].Links) == 0 {
pos.Ch = 0
pos.Ln = ln + 1
continue
}
rend := &ed.renders[ln]
si, ri, _ := rend.RuneSpanPos(pos.Ch)
for ti := range rend.Links {
tl := &rend.Links[ti]
if tl.StartSpan >= si && tl.StartIndex >= ri {
st, _ := rend.SpanPosToRuneIndex(tl.StartSpan, tl.StartIndex)
ed, _ := rend.SpanPosToRuneIndex(tl.EndSpan, tl.EndIndex)
reg := text.NewRegion(ln, st, ln, ed)
pos.Ch = st + 1 // get into it so next one will go after..
return pos, reg, true
}
}
pos.Ln = ln + 1
pos.Ch = 0
}
return pos, text.RegionNil, false
}
// findPrevLink finds previous link before given position, returns false if no such links
func (ed *Editor) findPrevLink(pos lexer.Pos) (lexer.Pos, text.Region, bool) {
for ln := pos.Ln - 1; ln >= 0; ln-- {
if len(ed.renders[ln].Links) == 0 {
if ln-1 >= 0 {
pos.Ch = ed.Buffer.LineLen(ln-1) - 2
} else {
ln = ed.NumLines
pos.Ch = ed.Buffer.LineLen(ln - 2)
}
continue
}
rend := &ed.renders[ln]
si, ri, _ := rend.RuneSpanPos(pos.Ch)
nl := len(rend.Links)
for ti := nl - 1; ti >= 0; ti-- {
tl := &rend.Links[ti]
if tl.StartSpan <= si && tl.StartIndex < ri {
st, _ := rend.SpanPosToRuneIndex(tl.StartSpan, tl.StartIndex)
ed, _ := rend.SpanPosToRuneIndex(tl.EndSpan, tl.EndIndex)
reg := text.NewRegion(ln, st, ln, ed)
pos.Ln = ln
pos.Ch = st + 1
return pos, reg, true
}
}
}
return pos, text.RegionNil, false
}
// CursorNextLink moves cursor to next link. wraparound wraps around to top of
// buffer if none found -- returns true if found
func (ed *Editor) CursorNextLink(wraparound bool) bool {
if ed.NumLines == 0 {
return false
}
ed.validateCursor()
npos, reg, has := ed.findNextLink(ed.CursorPos)
if !has {
if !wraparound {
return false
}
npos, reg, has = ed.findNextLink(lexer.Pos{}) // wraparound
if !has {
return false
}
}
ed.HighlightRegion(reg)
ed.SetCursorShow(npos)
ed.savePosHistory(ed.CursorPos)
ed.NeedsRender()
return true
}
// CursorPrevLink moves cursor to previous link. wraparound wraps around to
// bottom of buffer if none found. returns true if found
func (ed *Editor) CursorPrevLink(wraparound bool) bool {
if ed.NumLines == 0 {
return false
}
ed.validateCursor()
npos, reg, has := ed.findPrevLink(ed.CursorPos)
if !has {
if !wraparound {
return false
}
npos, reg, has = ed.findPrevLink(lexer.Pos{}) // wraparound
if !has {
return false
}
}
ed.HighlightRegion(reg)
ed.SetCursorShow(npos)
ed.savePosHistory(ed.CursorPos)
ed.NeedsRender()
return true
}
///////////////////////////////////////////////////////////////////////////////
// Scrolling
// scrollInView tells any parent scroll layout to scroll to get given box
// (e.g., cursor BBox) in view -- returns true if scrolled
func (ed *Editor) scrollInView(bbox image.Rectangle) bool {
return ed.ScrollToBox(bbox)
}
// scrollCursorToCenterIfHidden checks if the cursor is not visible, and if
// so, scrolls to the center, along both dimensions.
func (ed *Editor) scrollCursorToCenterIfHidden() bool {
curBBox := ed.cursorBBox(ed.CursorPos)
did := false
lht := int(ed.lineHeight)
bb := ed.renderBBox()
if bb.Size().Y <= lht {
return false
}
if (curBBox.Min.Y-lht) < bb.Min.Y || (curBBox.Max.Y+lht) > bb.Max.Y {
did = ed.scrollCursorToVerticalCenter()
// fmt.Println("v min:", curBBox.Min.Y, bb.Min.Y, "max:", curBBox.Max.Y+lht, bb.Max.Y, did)
}
if curBBox.Max.X < bb.Min.X+int(ed.LineNumberOffset) {
did2 := ed.scrollCursorToRight()
// fmt.Println("h max", curBBox.Max.X, bb.Min.X+int(ed.LineNumberOffset), did2)
did = did || did2
} else if curBBox.Min.X > bb.Max.X {
did2 := ed.scrollCursorToRight()
// fmt.Println("h min", curBBox.Min.X, bb.Max.X, did2)
did = did || did2
}
if did {
// fmt.Println("scroll to center", did)
}
return did
}
///////////////////////////////////////////////////////////////////////////////
// Scrolling -- Vertical
// scrollToTop tells any parent scroll layout to scroll to get given vertical
// coordinate at top of view to extent possible -- returns true if scrolled
func (ed *Editor) scrollToTop(pos int) bool {
ed.NeedsRender()
return ed.ScrollDimToStart(math32.Y, pos)
}
// scrollCursorToTop tells any parent scroll layout to scroll to get cursor
// at top of view to extent possible -- returns true if scrolled.
func (ed *Editor) scrollCursorToTop() bool {
curBBox := ed.cursorBBox(ed.CursorPos)
return ed.scrollToTop(curBBox.Min.Y)
}
// scrollToBottom tells any parent scroll layout to scroll to get given
// vertical coordinate at bottom of view to extent possible -- returns true if
// scrolled
func (ed *Editor) scrollToBottom(pos int) bool {
ed.NeedsRender()
return ed.ScrollDimToEnd(math32.Y, pos)
}
// scrollCursorToBottom tells any parent scroll layout to scroll to get cursor
// at bottom of view to extent possible -- returns true if scrolled.
func (ed *Editor) scrollCursorToBottom() bool {
curBBox := ed.cursorBBox(ed.CursorPos)
return ed.scrollToBottom(curBBox.Max.Y)
}
// scrollToVerticalCenter tells any parent scroll layout to scroll to get given
// vertical coordinate to center of view to extent possible -- returns true if
// scrolled
func (ed *Editor) scrollToVerticalCenter(pos int) bool {
ed.NeedsRender()
return ed.ScrollDimToCenter(math32.Y, pos)
}
// scrollCursorToVerticalCenter tells any parent scroll layout to scroll to get
// cursor at vert center of view to extent possible -- returns true if
// scrolled.
func (ed *Editor) scrollCursorToVerticalCenter() bool {
curBBox := ed.cursorBBox(ed.CursorPos)
mid := (curBBox.Min.Y + curBBox.Max.Y) / 2
return ed.scrollToVerticalCenter(mid)
}
func (ed *Editor) scrollCursorToTarget() {
// fmt.Println(ed, "to target:", ed.CursorTarg)
ed.CursorPos = ed.cursorTarget
ed.scrollCursorToVerticalCenter()
ed.targetSet = false
}
///////////////////////////////////////////////////////////////////////////////
// Scrolling -- Horizontal
// scrollToRight tells any parent scroll layout to scroll to get given
// horizontal coordinate at right of view to extent possible -- returns true
// if scrolled
func (ed *Editor) scrollToRight(pos int) bool {
return ed.ScrollDimToEnd(math32.X, pos)
}
// scrollCursorToRight tells any parent scroll layout to scroll to get cursor
// at right of view to extent possible -- returns true if scrolled.
func (ed *Editor) scrollCursorToRight() bool {
curBBox := ed.cursorBBox(ed.CursorPos)
return ed.scrollToRight(curBBox.Max.X)
}
// 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 texteditor
import (
"bufio"
"bytes"
"io"
"slices"
"sync"
"time"
"cogentcore.org/core/texteditor/highlighting"
)
// OutputBufferMarkupFunc is a function that returns a marked-up version of a given line of
// output text by adding html tags. It is essential that it ONLY adds tags,
// and otherwise has the exact same visible bytes as the input
type OutputBufferMarkupFunc func(line []byte) []byte
// OutputBuffer is a [Buffer] that records the output from an [io.Reader] using
// [bufio.Scanner]. It is optimized to combine fast chunks of output into
// large blocks of updating. It also supports an arbitrary markup function
// that operates on each line of output bytes.
type OutputBuffer struct { //types:add -setters
// the output that we are reading from, as an io.Reader
Output io.Reader
// the [Buffer] that we output to
Buffer *Buffer
// how much time to wait while batching output (default: 200ms)
Batch time.Duration
// optional markup function that adds html tags to given line of output -- essential that it ONLY adds tags, and otherwise has the exact same visible bytes as the input
MarkupFunc OutputBufferMarkupFunc
// current buffered output raw lines, which are not yet sent to the Buffer
currentOutputLines [][]byte
// current buffered output markup lines, which are not yet sent to the Buffer
currentOutputMarkupLines [][]byte
// mutex protecting updating of CurrentOutputLines and Buffer, and timer
mu sync.Mutex
// time when last output was sent to buffer
lastOutput time.Time
// time.AfterFunc that is started after new input is received and not immediately output -- ensures that it will get output if no further burst happens
afterTimer *time.Timer
}
// MonitorOutput monitors the output and updates the [Buffer].
func (ob *OutputBuffer) MonitorOutput() {
if ob.Batch == 0 {
ob.Batch = 200 * time.Millisecond
}
outscan := bufio.NewScanner(ob.Output) // line at a time
ob.currentOutputLines = make([][]byte, 0, 100)
ob.currentOutputMarkupLines = make([][]byte, 0, 100)
for outscan.Scan() {
b := outscan.Bytes()
bc := slices.Clone(b) // outscan bytes are temp
bec := highlighting.HtmlEscapeBytes(bc)
ob.mu.Lock()
if ob.afterTimer != nil {
ob.afterTimer.Stop()
ob.afterTimer = nil
}
ob.currentOutputLines = append(ob.currentOutputLines, bc)
mup := bec
if ob.MarkupFunc != nil {
mup = ob.MarkupFunc(bec)
}
ob.currentOutputMarkupLines = append(ob.currentOutputMarkupLines, mup)
lag := time.Since(ob.lastOutput)
if lag > ob.Batch {
ob.lastOutput = time.Now()
ob.outputToBuffer()
} else {
ob.afterTimer = time.AfterFunc(ob.Batch*2, func() {
ob.mu.Lock()
ob.lastOutput = time.Now()
ob.outputToBuffer()
ob.afterTimer = nil
ob.mu.Unlock()
})
}
ob.mu.Unlock()
}
ob.outputToBuffer()
}
// outputToBuffer sends the current output to Buffer.
// MUST be called under mutex protection
func (ob *OutputBuffer) outputToBuffer() {
lfb := []byte("\n")
if len(ob.currentOutputLines) == 0 {
return
}
tlns := bytes.Join(ob.currentOutputLines, lfb)
mlns := bytes.Join(ob.currentOutputMarkupLines, lfb)
tlns = append(tlns, lfb...)
mlns = append(mlns, lfb...)
ob.Buffer.Undos.Off = true
ob.Buffer.AppendTextMarkup(tlns, mlns, EditSignal)
// ob.Buf.AppendText(mlns, EditSignal) // todo: trying to allow markup according to styles
ob.Buffer.AutoScrollEditors()
ob.currentOutputLines = make([][]byte, 0, 100)
ob.currentOutputMarkupLines = make([][]byte, 0, 100)
}
// 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 texteditor
import (
"fmt"
"image"
"image/color"
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/colors/matcolor"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
"cogentcore.org/core/texteditor/text"
)
// Rendering Notes: all rendering is done in Render call.
// Layout must be called whenever content changes across lines.
// NeedsLayout indicates that the [Editor] needs a new layout pass.
func (ed *Editor) NeedsLayout() {
ed.NeedsRender()
ed.needsLayout = true
}
func (ed *Editor) renderLayout() {
chg := ed.ManageOverflow(3, true)
ed.layoutAllLines()
ed.ConfigScrolls()
if chg {
ed.Frame.NeedsLayout() // required to actually update scrollbar vs not
}
}
func (ed *Editor) RenderWidget() {
if ed.PushBounds() {
if ed.needsLayout {
ed.renderLayout()
ed.needsLayout = false
}
if ed.targetSet {
ed.scrollCursorToTarget()
}
ed.PositionScrolls()
ed.renderAllLines()
if ed.StateIs(states.Focused) {
ed.startCursor()
} else {
ed.stopCursor()
}
ed.RenderChildren()
ed.RenderScrolls()
ed.PopBounds()
} else {
ed.stopCursor()
}
}
// textStyleProperties returns the styling properties for text based on HiStyle Markup
func (ed *Editor) textStyleProperties() map[string]any {
if ed.Buffer == nil {
return nil
}
return ed.Buffer.Highlighter.CSSProperties
}
// renderStartPos is absolute rendering start position from our content pos with scroll
// This can be offscreen (left, up) based on scrolling.
func (ed *Editor) renderStartPos() math32.Vector2 {
return ed.Geom.Pos.Content.Add(ed.Geom.Scroll)
}
// renderBBox is the render region
func (ed *Editor) renderBBox() image.Rectangle {
bb := ed.Geom.ContentBBox
spc := ed.Styles.BoxSpace().Size().ToPointCeil()
// bb.Min = bb.Min.Add(spc)
bb.Max = bb.Max.Sub(spc)
return bb
}
// charStartPos returns the starting (top left) render coords for the given
// position -- makes no attempt to rationalize that pos (i.e., if not in
// visible range, position will be out of range too)
func (ed *Editor) charStartPos(pos lexer.Pos) math32.Vector2 {
spos := ed.renderStartPos()
spos.X += ed.LineNumberOffset
if pos.Ln >= len(ed.offsets) {
if len(ed.offsets) > 0 {
pos.Ln = len(ed.offsets) - 1
} else {
return spos
}
} else {
spos.Y += ed.offsets[pos.Ln]
}
if pos.Ln >= len(ed.renders) {
return spos
}
rp := &ed.renders[pos.Ln]
if len(rp.Spans) > 0 {
// note: Y from rune pos is baseline
rrp, _, _, _ := ed.renders[pos.Ln].RuneRelPos(pos.Ch)
spos.X += rrp.X
spos.Y += rrp.Y - ed.renders[pos.Ln].Spans[0].RelPos.Y // relative
}
return spos
}
// charStartPosVisible returns the starting pos for given position
// that is currently visible, based on bounding boxes.
func (ed *Editor) charStartPosVisible(pos lexer.Pos) math32.Vector2 {
spos := ed.charStartPos(pos)
bb := ed.renderBBox()
bbmin := math32.FromPoint(bb.Min)
bbmin.X += ed.LineNumberOffset
bbmax := math32.FromPoint(bb.Max)
spos.SetMax(bbmin)
spos.SetMin(bbmax)
return spos
}
// charEndPos returns the ending (bottom right) render coords for the given
// position -- makes no attempt to rationalize that pos (i.e., if not in
// visible range, position will be out of range too)
func (ed *Editor) charEndPos(pos lexer.Pos) math32.Vector2 {
spos := ed.renderStartPos()
pos.Ln = min(pos.Ln, ed.NumLines-1)
if pos.Ln < 0 {
spos.Y += float32(ed.linesSize.Y)
spos.X += ed.LineNumberOffset
return spos
}
if pos.Ln >= len(ed.offsets) {
spos.Y += float32(ed.linesSize.Y)
spos.X += ed.LineNumberOffset
return spos
}
spos.Y += ed.offsets[pos.Ln]
spos.X += ed.LineNumberOffset
r := ed.renders[pos.Ln]
if len(r.Spans) > 0 {
// note: Y from rune pos is baseline
rrp, _, _, _ := r.RuneEndPos(pos.Ch)
spos.X += rrp.X
spos.Y += rrp.Y - r.Spans[0].RelPos.Y // relative
}
spos.Y += ed.lineHeight // end of that line
return spos
}
// lineBBox returns the bounding box for given line
func (ed *Editor) lineBBox(ln int) math32.Box2 {
tbb := ed.renderBBox()
var bb math32.Box2
bb.Min = ed.renderStartPos()
bb.Min.X += ed.LineNumberOffset
bb.Max = bb.Min
bb.Max.Y += ed.lineHeight
bb.Max.X = float32(tbb.Max.X)
if ln >= len(ed.offsets) {
if len(ed.offsets) > 0 {
ln = len(ed.offsets) - 1
} else {
return bb
}
} else {
bb.Min.Y += ed.offsets[ln]
bb.Max.Y += ed.offsets[ln]
}
if ln >= len(ed.renders) {
return bb
}
rp := &ed.renders[ln]
bb.Max = bb.Min.Add(rp.BBox.Size())
return bb
}
// TODO: make viewDepthColors HCT based?
// viewDepthColors are changes in color values from default background for different
// depths. For dark mode, these are increments, for light mode they are decrements.
var viewDepthColors = []color.RGBA{
{0, 0, 0, 0},
{5, 5, 0, 0},
{15, 15, 0, 0},
{5, 15, 0, 0},
{0, 15, 5, 0},
{0, 15, 15, 0},
{0, 5, 15, 0},
{5, 0, 15, 0},
{5, 0, 5, 0},
}
// renderDepthBackground renders the depth background color.
func (ed *Editor) renderDepthBackground(stln, edln int) {
if ed.Buffer == nil {
return
}
if !ed.Buffer.Options.DepthColor || ed.IsDisabled() || !ed.StateIs(states.Focused) {
return
}
buf := ed.Buffer
bb := ed.renderBBox()
bln := buf.NumLines()
sty := &ed.Styles
isDark := matcolor.SchemeIsDark
nclrs := len(viewDepthColors)
lstdp := 0
for ln := stln; ln <= edln; ln++ {
lst := ed.charStartPos(lexer.Pos{Ln: ln}).Y // note: charstart pos includes descent
led := lst + math32.Max(ed.renders[ln].BBox.Size().Y, ed.lineHeight)
if int(math32.Ceil(led)) < bb.Min.Y {
continue
}
if int(math32.Floor(lst)) > bb.Max.Y {
continue
}
if ln >= bln { // may be out of sync
continue
}
ht := buf.HiTags(ln)
lsted := 0
for ti := range ht {
lx := &ht[ti]
if lx.Token.Depth > 0 {
var vdc color.RGBA
if isDark { // reverse order too
vdc = viewDepthColors[nclrs-1-lx.Token.Depth%nclrs]
} else {
vdc = viewDepthColors[lx.Token.Depth%nclrs]
}
bg := gradient.Apply(sty.Background, func(c color.Color) color.Color {
if isDark { // reverse order too
return colors.Add(c, vdc)
}
return colors.Sub(c, vdc)
})
st := min(lsted, lx.St)
reg := text.Region{Start: lexer.Pos{Ln: ln, Ch: st}, End: lexer.Pos{Ln: ln, Ch: lx.Ed}}
lsted = lx.Ed
lstdp = lx.Token.Depth
ed.renderRegionBoxStyle(reg, sty, bg, true) // full width alway
}
}
if lstdp > 0 {
ed.renderRegionToEnd(lexer.Pos{Ln: ln, Ch: lsted}, sty, sty.Background)
}
}
}
// renderSelect renders the selection region as a selected background color.
func (ed *Editor) renderSelect() {
if !ed.HasSelection() {
return
}
ed.renderRegionBox(ed.SelectRegion, ed.SelectColor)
}
// renderHighlights renders the highlight regions as a
// highlighted background color.
func (ed *Editor) renderHighlights(stln, edln int) {
for _, reg := range ed.Highlights {
reg := ed.Buffer.AdjustRegion(reg)
if reg.IsNil() || (stln >= 0 && (reg.Start.Ln > edln || reg.End.Ln < stln)) {
continue
}
ed.renderRegionBox(reg, ed.HighlightColor)
}
}
// renderScopelights renders a highlight background color for regions
// in the Scopelights list.
func (ed *Editor) renderScopelights(stln, edln int) {
for _, reg := range ed.scopelights {
reg := ed.Buffer.AdjustRegion(reg)
if reg.IsNil() || (stln >= 0 && (reg.Start.Ln > edln || reg.End.Ln < stln)) {
continue
}
ed.renderRegionBox(reg, ed.HighlightColor)
}
}
// renderRegionBox renders a region in background according to given background
func (ed *Editor) renderRegionBox(reg text.Region, bg image.Image) {
ed.renderRegionBoxStyle(reg, &ed.Styles, bg, false)
}
// renderRegionBoxStyle renders a region in given style and background
func (ed *Editor) renderRegionBoxStyle(reg text.Region, sty *styles.Style, bg image.Image, fullWidth bool) {
st := reg.Start
end := reg.End
spos := ed.charStartPosVisible(st)
epos := ed.charStartPosVisible(end)
epos.Y += ed.lineHeight
bb := ed.renderBBox()
stx := math32.Ceil(float32(bb.Min.X) + ed.LineNumberOffset)
if int(math32.Ceil(epos.Y)) < bb.Min.Y || int(math32.Floor(spos.Y)) > bb.Max.Y {
return
}
ex := float32(bb.Max.X)
if fullWidth {
epos.X = ex
}
pc := &ed.Scene.PaintContext
stsi, _, _ := ed.wrappedLineNumber(st)
edsi, _, _ := ed.wrappedLineNumber(end)
if st.Ln == end.Ln && stsi == edsi {
pc.FillBox(spos, epos.Sub(spos), bg) // same line, done
return
}
// on diff lines: fill to end of stln
seb := spos
seb.Y += ed.lineHeight
seb.X = ex
pc.FillBox(spos, seb.Sub(spos), bg)
sfb := seb
sfb.X = stx
if sfb.Y < epos.Y { // has some full box
efb := epos
efb.Y -= ed.lineHeight
efb.X = ex
pc.FillBox(sfb, efb.Sub(sfb), bg)
}
sed := epos
sed.Y -= ed.lineHeight
sed.X = stx
pc.FillBox(sed, epos.Sub(sed), bg)
}
// renderRegionToEnd renders a region in given style and background, to end of line from start
func (ed *Editor) renderRegionToEnd(st lexer.Pos, sty *styles.Style, bg image.Image) {
spos := ed.charStartPosVisible(st)
epos := spos
epos.Y += ed.lineHeight
vsz := epos.Sub(spos)
if vsz.X <= 0 || vsz.Y <= 0 {
return
}
pc := &ed.Scene.PaintContext
pc.FillBox(spos, epos.Sub(spos), bg) // same line, done
}
// renderAllLines displays all the visible lines on the screen,
// after PushBounds has already been called.
func (ed *Editor) renderAllLines() {
ed.RenderStandardBox()
pc := &ed.Scene.PaintContext
bb := ed.renderBBox()
pos := ed.renderStartPos()
stln := -1
edln := -1
for ln := 0; ln < ed.NumLines; ln++ {
if ln >= len(ed.offsets) || ln >= len(ed.renders) {
break
}
lst := pos.Y + ed.offsets[ln]
led := lst + math32.Max(ed.renders[ln].BBox.Size().Y, ed.lineHeight)
if int(math32.Ceil(led)) < bb.Min.Y {
continue
}
if int(math32.Floor(lst)) > bb.Max.Y {
continue
}
if stln < 0 {
stln = ln
}
edln = ln
}
if stln < 0 || edln < 0 { // shouldn't happen.
return
}
pc.PushBounds(bb)
if ed.hasLineNumbers {
ed.renderLineNumbersBoxAll()
for ln := stln; ln <= edln; ln++ {
ed.renderLineNumber(ln, false) // don't re-render std fill boxes
}
}
ed.renderDepthBackground(stln, edln)
ed.renderHighlights(stln, edln)
ed.renderScopelights(stln, edln)
ed.renderSelect()
if ed.hasLineNumbers {
tbb := bb
tbb.Min.X += int(ed.LineNumberOffset)
pc.PushBounds(tbb)
}
for ln := stln; ln <= edln; ln++ {
lst := pos.Y + ed.offsets[ln]
lp := pos
lp.Y = lst
lp.X += ed.LineNumberOffset
if lp.Y+ed.fontAscent > float32(bb.Max.Y) {
break
}
ed.renders[ln].Render(pc, lp) // not top pos; already has baseline offset
}
if ed.hasLineNumbers {
pc.PopBounds()
}
pc.PopBounds()
}
// renderLineNumbersBoxAll renders the background for the line numbers in the LineNumberColor
func (ed *Editor) renderLineNumbersBoxAll() {
if !ed.hasLineNumbers {
return
}
pc := &ed.Scene.PaintContext
bb := ed.renderBBox()
spos := math32.FromPoint(bb.Min)
epos := math32.FromPoint(bb.Max)
epos.X = spos.X + ed.LineNumberOffset
sz := epos.Sub(spos)
pc.FillStyle.Color = ed.LineNumberColor
pc.DrawRoundedRectangle(spos.X, spos.Y, sz.X, sz.Y, ed.Styles.Border.Radius.Dots())
pc.Fill()
}
// renderLineNumber renders given line number; called within context of other render.
// if defFill is true, it fills box color for default background color (use false for
// batch mode).
func (ed *Editor) renderLineNumber(ln int, defFill bool) {
if !ed.hasLineNumbers || ed.Buffer == nil {
return
}
bb := ed.renderBBox()
tpos := math32.Vector2{
X: float32(bb.Min.X), // + spc.Pos().X
Y: ed.charEndPos(lexer.Pos{Ln: ln}).Y - ed.fontDescent,
}
if tpos.Y > float32(bb.Max.Y) {
return
}
sc := ed.Scene
sty := &ed.Styles
fst := sty.FontRender()
pc := &sc.PaintContext
fst.Background = nil
lfmt := fmt.Sprintf("%d", ed.lineNumberDigits)
lfmt = "%" + lfmt + "d"
lnstr := fmt.Sprintf(lfmt, ln+1)
if ed.CursorPos.Ln == ln {
fst.Color = colors.Scheme.Primary.Base
fst.Weight = styles.WeightBold
// need to open with new weight
fst.Font = paint.OpenFont(fst, &ed.Styles.UnitContext)
} else {
fst.Color = colors.Scheme.OnSurfaceVariant
}
ed.lineNumberRender.SetString(lnstr, fst, &sty.UnitContext, &sty.Text, true, 0, 0)
ed.lineNumberRender.Render(pc, tpos)
// render circle
lineColor := ed.Buffer.LineColors[ln]
if lineColor != nil {
start := ed.charStartPos(lexer.Pos{Ln: ln})
end := ed.charEndPos(lexer.Pos{Ln: ln + 1})
if ln < ed.NumLines-1 {
end.Y -= ed.lineHeight
}
if end.Y >= float32(bb.Max.Y) {
return
}
// starts at end of line number text
start.X = tpos.X + ed.lineNumberRender.BBox.Size().X
// ends at end of line number offset
end.X = float32(bb.Min.X) + ed.LineNumberOffset
r := (end.X - start.X) / 2
center := start.AddScalar(r)
// cut radius in half so that it doesn't look too big
r /= 2
pc.FillStyle.Color = lineColor
pc.DrawCircle(center.X, center.Y, r)
pc.Fill()
}
}
// firstVisibleLine finds the first visible line, starting at given line
// (typically cursor -- if zero, a visible line is first found) -- returns
// stln if nothing found above it.
func (ed *Editor) firstVisibleLine(stln int) int {
bb := ed.renderBBox()
if stln == 0 {
perln := float32(ed.linesSize.Y) / float32(ed.NumLines)
stln = int(ed.Geom.Scroll.Y/perln) - 1
if stln < 0 {
stln = 0
}
for ln := stln; ln < ed.NumLines; ln++ {
lbb := ed.lineBBox(ln)
if int(math32.Ceil(lbb.Max.Y)) > bb.Min.Y { // visible
stln = ln
break
}
}
}
lastln := stln
for ln := stln - 1; ln >= 0; ln-- {
cpos := ed.charStartPos(lexer.Pos{Ln: ln})
if int(math32.Ceil(cpos.Y)) < bb.Min.Y { // top just offscreen
break
}
lastln = ln
}
return lastln
}
// lastVisibleLine finds the last visible line, starting at given line
// (typically cursor) -- returns stln if nothing found beyond it.
func (ed *Editor) lastVisibleLine(stln int) int {
bb := ed.renderBBox()
lastln := stln
for ln := stln + 1; ln < ed.NumLines; ln++ {
pos := lexer.Pos{Ln: ln}
cpos := ed.charStartPos(pos)
if int(math32.Floor(cpos.Y)) > bb.Max.Y { // just offscreen
break
}
lastln = ln
}
return lastln
}
// PixelToCursor finds the cursor position that corresponds to the given pixel
// location (e.g., from mouse click) which has had ScBBox.Min subtracted from
// it (i.e, relative to upper left of text area)
func (ed *Editor) PixelToCursor(pt image.Point) lexer.Pos {
if ed.NumLines == 0 {
return lexer.PosZero
}
bb := ed.renderBBox()
sty := &ed.Styles
yoff := float32(bb.Min.Y)
xoff := float32(bb.Min.X)
stln := ed.firstVisibleLine(0)
cln := stln
fls := ed.charStartPos(lexer.Pos{Ln: stln}).Y - yoff
if pt.Y < int(math32.Floor(fls)) {
cln = stln
} else if pt.Y > bb.Max.Y {
cln = ed.NumLines - 1
} else {
got := false
for ln := stln; ln < ed.NumLines; ln++ {
ls := ed.charStartPos(lexer.Pos{Ln: ln}).Y - yoff
es := ls
es += math32.Max(ed.renders[ln].BBox.Size().Y, ed.lineHeight)
if pt.Y >= int(math32.Floor(ls)) && pt.Y < int(math32.Ceil(es)) {
got = true
cln = ln
break
}
}
if !got {
cln = ed.NumLines - 1
}
}
// fmt.Printf("cln: %v pt: %v\n", cln, pt)
if cln >= len(ed.renders) {
return lexer.Pos{Ln: cln, Ch: 0}
}
lnsz := ed.Buffer.LineLen(cln)
if lnsz == 0 || sty.Font.Face == nil {
return lexer.Pos{Ln: cln, Ch: 0}
}
scrl := ed.Geom.Scroll.Y
nolno := float32(pt.X - int(ed.LineNumberOffset))
sc := int((nolno + scrl) / sty.Font.Face.Metrics.Ch)
sc -= sc / 4
sc = max(0, sc)
cch := sc
lnst := ed.charStartPos(lexer.Pos{Ln: cln})
lnst.Y -= yoff
lnst.X -= xoff
rpt := math32.FromPoint(pt).Sub(lnst)
si, ri, ok := ed.renders[cln].PosToRune(rpt)
if ok {
cch, _ := ed.renders[cln].SpanPosToRuneIndex(si, ri)
return lexer.Pos{Ln: cln, Ch: cch}
}
return lexer.Pos{Ln: cln, Ch: cch}
}
// 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 texteditor
import (
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/core"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/texteditor/text"
)
//////////////////////////////////////////////////////////
// Regions
// HighlightRegion creates a new highlighted region,
// triggers updating.
func (ed *Editor) HighlightRegion(reg text.Region) {
ed.Highlights = []text.Region{reg}
ed.NeedsRender()
}
// ClearHighlights clears the Highlights slice of all regions
func (ed *Editor) ClearHighlights() {
if len(ed.Highlights) == 0 {
return
}
ed.Highlights = ed.Highlights[:0]
ed.NeedsRender()
}
// clearScopelights clears the scopelights slice of all regions
func (ed *Editor) clearScopelights() {
if len(ed.scopelights) == 0 {
return
}
sl := make([]text.Region, len(ed.scopelights))
copy(sl, ed.scopelights)
ed.scopelights = ed.scopelights[:0]
ed.NeedsRender()
}
//////////////////////////////////////////////////////////
// Selection
// clearSelected resets both the global selected flag and any current selection
func (ed *Editor) clearSelected() {
// ed.WidgetBase.ClearSelected()
ed.SelectReset()
}
// HasSelection returns whether there is a selected region of text
func (ed *Editor) HasSelection() bool {
return ed.SelectRegion.Start.IsLess(ed.SelectRegion.End)
}
// Selection returns the currently selected text as a text.Edit, which
// captures start, end, and full lines in between -- nil if no selection
func (ed *Editor) Selection() *text.Edit {
if ed.HasSelection() {
return ed.Buffer.Region(ed.SelectRegion.Start, ed.SelectRegion.End)
}
return nil
}
// selectModeToggle toggles the SelectMode, updating selection with cursor movement
func (ed *Editor) selectModeToggle() {
if ed.selectMode {
ed.selectMode = false
} else {
ed.selectMode = true
ed.selectStart = ed.CursorPos
ed.selectRegionUpdate(ed.CursorPos)
}
ed.savePosHistory(ed.CursorPos)
}
// selectAll selects all the text
func (ed *Editor) selectAll() {
ed.SelectRegion.Start = lexer.PosZero
ed.SelectRegion.End = ed.Buffer.EndPos()
ed.NeedsRender()
}
// wordBefore returns the word before the lexer.Pos
// uses IsWordBreak to determine the bounds of the word
func (ed *Editor) wordBefore(tp lexer.Pos) *text.Edit {
txt := ed.Buffer.Line(tp.Ln)
ch := tp.Ch
ch = min(ch, len(txt))
st := ch
for i := ch - 1; i >= 0; i-- {
if i == 0 { // start of line
st = 0
break
}
r1 := txt[i]
r2 := txt[i-1]
if core.IsWordBreak(r1, r2) {
st = i + 1
break
}
}
if st != ch {
return ed.Buffer.Region(lexer.Pos{Ln: tp.Ln, Ch: st}, tp)
}
return nil
}
// isWordEnd returns true if the cursor is just past the last letter of a word
// word is a string of characters none of which are classified as a word break
func (ed *Editor) isWordEnd(tp lexer.Pos) bool {
txt := ed.Buffer.Line(ed.CursorPos.Ln)
sz := len(txt)
if sz == 0 {
return false
}
if tp.Ch >= len(txt) { // end of line
r := txt[len(txt)-1]
return core.IsWordBreak(r, -1)
}
if tp.Ch == 0 { // start of line
r := txt[0]
return !core.IsWordBreak(r, -1)
}
r1 := txt[tp.Ch-1]
r2 := txt[tp.Ch]
return !core.IsWordBreak(r1, rune(-1)) && core.IsWordBreak(r2, rune(-1))
}
// isWordMiddle - returns true if the cursor is anywhere inside a word,
// i.e. the character before the cursor and the one after the cursor
// are not classified as word break characters
func (ed *Editor) isWordMiddle(tp lexer.Pos) bool {
txt := ed.Buffer.Line(ed.CursorPos.Ln)
sz := len(txt)
if sz < 2 {
return false
}
if tp.Ch >= len(txt) { // end of line
return false
}
if tp.Ch == 0 { // start of line
return false
}
r1 := txt[tp.Ch-1]
r2 := txt[tp.Ch]
return !core.IsWordBreak(r1, rune(-1)) && !core.IsWordBreak(r2, rune(-1))
}
// selectWord selects the word (whitespace, punctuation delimited) that the cursor is on
// returns true if word selected
func (ed *Editor) selectWord() bool {
if ed.Buffer == nil {
return false
}
txt := ed.Buffer.Line(ed.CursorPos.Ln)
sz := len(txt)
if sz == 0 {
return false
}
reg := ed.wordAt()
ed.SelectRegion = reg
ed.selectStart = ed.SelectRegion.Start
return true
}
// wordAt finds the region of the word at the current cursor position
func (ed *Editor) wordAt() (reg text.Region) {
reg.Start = ed.CursorPos
reg.End = ed.CursorPos
txt := ed.Buffer.Line(ed.CursorPos.Ln)
sz := len(txt)
if sz == 0 {
return reg
}
sch := min(ed.CursorPos.Ch, sz-1)
if !core.IsWordBreak(txt[sch], rune(-1)) {
for sch > 0 {
r2 := rune(-1)
if sch-2 >= 0 {
r2 = txt[sch-2]
}
if core.IsWordBreak(txt[sch-1], r2) {
break
}
sch--
}
reg.Start.Ch = sch
ech := ed.CursorPos.Ch + 1
for ech < sz {
r2 := rune(-1)
if ech < sz-1 {
r2 = rune(txt[ech+1])
}
if core.IsWordBreak(txt[ech], r2) {
break
}
ech++
}
reg.End.Ch = ech
} else { // keep the space start -- go to next space..
ech := ed.CursorPos.Ch + 1
for ech < sz {
if !core.IsWordBreak(txt[ech], rune(-1)) {
break
}
ech++
}
for ech < sz {
r2 := rune(-1)
if ech < sz-1 {
r2 = rune(txt[ech+1])
}
if core.IsWordBreak(txt[ech], r2) {
break
}
ech++
}
reg.End.Ch = ech
}
return reg
}
// SelectReset resets the selection
func (ed *Editor) SelectReset() {
ed.selectMode = false
if !ed.HasSelection() {
return
}
ed.SelectRegion = text.RegionNil
ed.previousSelectRegion = text.RegionNil
}
///////////////////////////////////////////////////////////////////////////////
// Cut / Copy / Paste
// editorClipboardHistory is the [Editor] clipboard history; everything that has been copied
var editorClipboardHistory [][]byte
// addEditorClipboardHistory adds the given clipboard bytes to top of history stack
func addEditorClipboardHistory(clip []byte) {
max := clipboardHistoryMax
if editorClipboardHistory == nil {
editorClipboardHistory = make([][]byte, 0, max)
}
ch := &editorClipboardHistory
sz := len(*ch)
if sz > max {
*ch = (*ch)[:max]
}
if sz >= max {
copy((*ch)[1:max], (*ch)[0:max-1])
(*ch)[0] = clip
} else {
*ch = append(*ch, nil)
if sz > 0 {
copy((*ch)[1:], (*ch)[0:sz])
}
(*ch)[0] = clip
}
}
// editorClipHistoryChooserLength is the max length of clip history to show in chooser
var editorClipHistoryChooserLength = 40
// editorClipHistoryChooserList returns a string slice of length-limited clip history, for chooser
func editorClipHistoryChooserList() []string {
cl := make([]string, len(editorClipboardHistory))
for i, hc := range editorClipboardHistory {
szl := len(hc)
if szl > editorClipHistoryChooserLength {
cl[i] = string(hc[:editorClipHistoryChooserLength])
} else {
cl[i] = string(hc)
}
}
return cl
}
// pasteHistory presents a chooser of clip history items, pastes into text if selected
func (ed *Editor) pasteHistory() {
if editorClipboardHistory == nil {
return
}
cl := editorClipHistoryChooserList()
m := core.NewMenuFromStrings(cl, "", func(idx int) {
clip := editorClipboardHistory[idx]
if clip != nil {
ed.Clipboard().Write(mimedata.NewTextBytes(clip))
ed.InsertAtCursor(clip)
ed.savePosHistory(ed.CursorPos)
ed.NeedsRender()
}
})
core.NewMenuStage(m, ed, ed.cursorBBox(ed.CursorPos).Min).Run()
}
// Cut cuts any selected text and adds it to the clipboard, also returns cut text
func (ed *Editor) Cut() *text.Edit {
if !ed.HasSelection() {
return nil
}
org := ed.SelectRegion.Start
cut := ed.deleteSelection()
if cut != nil {
cb := cut.ToBytes()
ed.Clipboard().Write(mimedata.NewTextBytes(cb))
addEditorClipboardHistory(cb)
}
ed.SetCursorShow(org)
ed.savePosHistory(ed.CursorPos)
ed.NeedsRender()
return cut
}
// deleteSelection deletes any selected text, without adding to clipboard --
// returns text deleted as text.Edit (nil if none)
func (ed *Editor) deleteSelection() *text.Edit {
tbe := ed.Buffer.DeleteText(ed.SelectRegion.Start, ed.SelectRegion.End, EditSignal)
ed.SelectReset()
return tbe
}
// Copy copies any selected text to the clipboard, and returns that text,
// optionally resetting the current selection
func (ed *Editor) Copy(reset bool) *text.Edit {
tbe := ed.Selection()
if tbe == nil {
return nil
}
cb := tbe.ToBytes()
addEditorClipboardHistory(cb)
ed.Clipboard().Write(mimedata.NewTextBytes(cb))
if reset {
ed.SelectReset()
}
ed.savePosHistory(ed.CursorPos)
ed.NeedsRender()
return tbe
}
// Paste inserts text from the clipboard at current cursor position
func (ed *Editor) Paste() {
data := ed.Clipboard().Read([]string{fileinfo.TextPlain})
if data != nil {
ed.InsertAtCursor(data.TypeData(fileinfo.TextPlain))
ed.savePosHistory(ed.CursorPos)
}
ed.NeedsRender()
}
// InsertAtCursor inserts given text at current cursor position
func (ed *Editor) InsertAtCursor(txt []byte) {
if ed.HasSelection() {
tbe := ed.deleteSelection()
ed.CursorPos = tbe.AdjustPos(ed.CursorPos, text.AdjustPosDelStart) // move to start if in reg
}
tbe := ed.Buffer.insertText(ed.CursorPos, txt, EditSignal)
if tbe == nil {
return
}
pos := tbe.Reg.End
if len(txt) == 1 && txt[0] == '\n' {
pos.Ch = 0 // sometimes it doesn't go to the start..
}
ed.SetCursorShow(pos)
ed.setCursorColumn(ed.CursorPos)
ed.NeedsRender()
}
///////////////////////////////////////////////////////////
// Rectangular regions
// editorClipboardRect is the internal clipboard for Rect rectangle-based
// regions -- the raw text is posted on the system clipboard but the
// rect information is in a special format.
var editorClipboardRect *text.Edit
// CutRect cuts rectangle defined by selected text (upper left to lower right)
// and adds it to the clipboard, also returns cut text.
func (ed *Editor) CutRect() *text.Edit {
if !ed.HasSelection() {
return nil
}
npos := lexer.Pos{Ln: ed.SelectRegion.End.Ln, Ch: ed.SelectRegion.Start.Ch}
cut := ed.Buffer.deleteTextRect(ed.SelectRegion.Start, ed.SelectRegion.End, EditSignal)
if cut != nil {
cb := cut.ToBytes()
ed.Clipboard().Write(mimedata.NewTextBytes(cb))
editorClipboardRect = cut
}
ed.SetCursorShow(npos)
ed.savePosHistory(ed.CursorPos)
ed.NeedsRender()
return cut
}
// CopyRect copies any selected text to the clipboard, and returns that text,
// optionally resetting the current selection
func (ed *Editor) CopyRect(reset bool) *text.Edit {
tbe := ed.Buffer.RegionRect(ed.SelectRegion.Start, ed.SelectRegion.End)
if tbe == nil {
return nil
}
cb := tbe.ToBytes()
ed.Clipboard().Write(mimedata.NewTextBytes(cb))
editorClipboardRect = tbe
if reset {
ed.SelectReset()
}
ed.savePosHistory(ed.CursorPos)
ed.NeedsRender()
return tbe
}
// PasteRect inserts text from the clipboard at current cursor position
func (ed *Editor) PasteRect() {
if editorClipboardRect == nil {
return
}
ce := editorClipboardRect.Clone()
nl := ce.Reg.End.Ln - ce.Reg.Start.Ln
nch := ce.Reg.End.Ch - ce.Reg.Start.Ch
ce.Reg.Start.Ln = ed.CursorPos.Ln
ce.Reg.End.Ln = ed.CursorPos.Ln + nl
ce.Reg.Start.Ch = ed.CursorPos.Ch
ce.Reg.End.Ch = ed.CursorPos.Ch + nch
tbe := ed.Buffer.insertTextRect(ce, EditSignal)
pos := tbe.Reg.End
ed.SetCursorShow(pos)
ed.setCursorColumn(ed.CursorPos)
ed.savePosHistory(ed.CursorPos)
ed.NeedsRender()
}
// ReCaseSelection changes the case of the currently selected text.
// Returns the new text; empty if nothing selected.
func (ed *Editor) ReCaseSelection(c strcase.Cases) string {
if !ed.HasSelection() {
return ""
}
sel := ed.Selection()
nstr := strcase.To(string(sel.ToBytes()), c)
ed.Buffer.ReplaceText(sel.Reg.Start, sel.Reg.End, sel.Reg.Start, nstr, EditSignal, ReplaceNoMatchCase)
return nstr
}
// 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 texteditor
import (
"strings"
"unicode"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/keymap"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/token"
"cogentcore.org/core/texteditor/text"
)
///////////////////////////////////////////////////////////////////////////////
// Complete and Spell
// offerComplete pops up a menu of possible completions
func (ed *Editor) offerComplete() {
if ed.Buffer.Complete == nil || ed.ISearch.On || ed.QReplace.On || ed.IsDisabled() {
return
}
ed.Buffer.Complete.Cancel()
if !ed.Buffer.Options.Completion {
return
}
if ed.Buffer.InComment(ed.CursorPos) || ed.Buffer.InLitString(ed.CursorPos) {
return
}
ed.Buffer.Complete.SrcLn = ed.CursorPos.Ln
ed.Buffer.Complete.SrcCh = ed.CursorPos.Ch
st := lexer.Pos{ed.CursorPos.Ln, 0}
en := lexer.Pos{ed.CursorPos.Ln, ed.CursorPos.Ch}
tbe := ed.Buffer.Region(st, en)
var s string
if tbe != nil {
s = string(tbe.ToBytes())
s = strings.TrimLeft(s, " \t") // trim ' ' and '\t'
}
// count := ed.Buf.ByteOffs[ed.CursorPos.Ln] + ed.CursorPos.Ch
cpos := ed.charStartPos(ed.CursorPos).ToPoint() // physical location
cpos.X += 5
cpos.Y += 10
// ed.Buffer.setByteOffs() // make sure the pos offset is updated!!
// todo: why? for above
ed.Buffer.currentEditor = ed
ed.Buffer.Complete.SrcLn = ed.CursorPos.Ln
ed.Buffer.Complete.SrcCh = ed.CursorPos.Ch
ed.Buffer.Complete.Show(ed, cpos, s)
}
// CancelComplete cancels any pending completion.
// Call this when new events have moved beyond any prior completion scenario.
func (ed *Editor) CancelComplete() {
if ed.Buffer == nil {
return
}
if ed.Buffer.Complete == nil {
return
}
if ed.Buffer.Complete.Cancel() {
ed.Buffer.currentEditor = nil
}
}
// Lookup attempts to lookup symbol at current location, popping up a window
// if something is found.
func (ed *Editor) Lookup() { //types:add
if ed.Buffer.Complete == nil || ed.ISearch.On || ed.QReplace.On || ed.IsDisabled() {
return
}
var ln int
var ch int
if ed.HasSelection() {
ln = ed.SelectRegion.Start.Ln
if ed.SelectRegion.End.Ln != ln {
return // no multiline selections for lookup
}
ch = ed.SelectRegion.End.Ch
} else {
ln = ed.CursorPos.Ln
if ed.isWordEnd(ed.CursorPos) {
ch = ed.CursorPos.Ch
} else {
ch = ed.wordAt().End.Ch
}
}
ed.Buffer.Complete.SrcLn = ln
ed.Buffer.Complete.SrcCh = ch
st := lexer.Pos{ed.CursorPos.Ln, 0}
en := lexer.Pos{ed.CursorPos.Ln, ch}
tbe := ed.Buffer.Region(st, en)
var s string
if tbe != nil {
s = string(tbe.ToBytes())
s = strings.TrimLeft(s, " \t") // trim ' ' and '\t'
}
// count := ed.Buf.ByteOffs[ed.CursorPos.Ln] + ed.CursorPos.Ch
cpos := ed.charStartPos(ed.CursorPos).ToPoint() // physical location
cpos.X += 5
cpos.Y += 10
// ed.Buffer.setByteOffs() // make sure the pos offset is updated!!
// todo: why?
ed.Buffer.currentEditor = ed
ed.Buffer.Complete.Lookup(s, ed.CursorPos.Ln, ed.CursorPos.Ch, ed.Scene, cpos)
}
// iSpellKeyInput locates the word to spell check based on cursor position and
// the key input, then passes the text region to SpellCheck
func (ed *Editor) iSpellKeyInput(kt events.Event) {
if !ed.Buffer.isSpellEnabled(ed.CursorPos) {
return
}
isDoc := ed.Buffer.Info.Cat == fileinfo.Doc
tp := ed.CursorPos
kf := keymap.Of(kt.KeyChord())
switch kf {
case keymap.MoveUp:
if isDoc {
ed.Buffer.spellCheckLineTag(tp.Ln)
}
case keymap.MoveDown:
if isDoc {
ed.Buffer.spellCheckLineTag(tp.Ln)
}
case keymap.MoveRight:
if ed.isWordEnd(tp) {
reg := ed.wordBefore(tp)
ed.spellCheck(reg)
break
}
if tp.Ch == 0 { // end of line
tp.Ln--
if isDoc {
ed.Buffer.spellCheckLineTag(tp.Ln) // redo prior line
}
tp.Ch = ed.Buffer.LineLen(tp.Ln)
reg := ed.wordBefore(tp)
ed.spellCheck(reg)
break
}
txt := ed.Buffer.Line(tp.Ln)
var r rune
atend := false
if tp.Ch >= len(txt) {
atend = true
tp.Ch++
} else {
r = txt[tp.Ch]
}
if atend || core.IsWordBreak(r, rune(-1)) {
tp.Ch-- // we are one past the end of word
reg := ed.wordBefore(tp)
ed.spellCheck(reg)
}
case keymap.Enter:
tp.Ln--
if isDoc {
ed.Buffer.spellCheckLineTag(tp.Ln) // redo prior line
}
tp.Ch = ed.Buffer.LineLen(tp.Ln)
reg := ed.wordBefore(tp)
ed.spellCheck(reg)
case keymap.FocusNext:
tp.Ch-- // we are one past the end of word
reg := ed.wordBefore(tp)
ed.spellCheck(reg)
case keymap.Backspace, keymap.Delete:
if ed.isWordMiddle(ed.CursorPos) {
reg := ed.wordAt()
ed.spellCheck(ed.Buffer.Region(reg.Start, reg.End))
} else {
reg := ed.wordBefore(tp)
ed.spellCheck(reg)
}
case keymap.None:
if unicode.IsSpace(kt.KeyRune()) || unicode.IsPunct(kt.KeyRune()) && kt.KeyRune() != '\'' { // contractions!
tp.Ch-- // we are one past the end of word
reg := ed.wordBefore(tp)
ed.spellCheck(reg)
} else {
if ed.isWordMiddle(ed.CursorPos) {
reg := ed.wordAt()
ed.spellCheck(ed.Buffer.Region(reg.Start, reg.End))
}
}
}
}
// spellCheck offers spelling corrections if we are at a word break or other word termination
// and the word before the break is unknown -- returns true if misspelled word found
func (ed *Editor) spellCheck(reg *text.Edit) bool {
if ed.Buffer.spell == nil {
return false
}
wb := string(reg.ToBytes())
lwb := lexer.FirstWordApostrophe(wb) // only lookup words
if len(lwb) <= 2 {
return false
}
widx := strings.Index(wb, lwb) // adjust region for actual part looking up
ld := len(wb) - len(lwb)
reg.Reg.Start.Ch += widx
reg.Reg.End.Ch += widx - ld
sugs, knwn := ed.Buffer.spell.checkWord(lwb)
if knwn {
ed.Buffer.RemoveTag(reg.Reg.Start, token.TextSpellErr)
return false
}
// fmt.Printf("spell err: %s\n", wb)
ed.Buffer.spell.setWord(wb, sugs, reg.Reg.Start.Ln, reg.Reg.Start.Ch)
ed.Buffer.RemoveTag(reg.Reg.Start, token.TextSpellErr)
ed.Buffer.AddTagEdit(reg, token.TextSpellErr)
return true
}
// offerCorrect pops up a menu of possible spelling corrections for word at
// current CursorPos. If no misspelling there or not in spellcorrect mode
// returns false
func (ed *Editor) offerCorrect() bool {
if ed.Buffer.spell == nil || ed.ISearch.On || ed.QReplace.On || ed.IsDisabled() {
return false
}
sel := ed.SelectRegion
if !ed.selectWord() {
ed.SelectRegion = sel
return false
}
tbe := ed.Selection()
if tbe == nil {
ed.SelectRegion = sel
return false
}
ed.SelectRegion = sel
wb := string(tbe.ToBytes())
wbn := strings.TrimLeft(wb, " \t")
if len(wb) != len(wbn) {
return false // SelectWord captures leading whitespace - don't offer if there is leading whitespace
}
sugs, knwn := ed.Buffer.spell.checkWord(wb)
if knwn && !ed.Buffer.spell.isLastLearned(wb) {
return false
}
ed.Buffer.spell.setWord(wb, sugs, tbe.Reg.Start.Ln, tbe.Reg.Start.Ch)
cpos := ed.charStartPos(ed.CursorPos).ToPoint() // physical location
cpos.X += 5
cpos.Y += 10
ed.Buffer.currentEditor = ed
ed.Buffer.spell.show(wb, ed.Scene, cpos)
return true
}
// cancelCorrect cancels any pending spell correction.
// Call this when new events have moved beyond any prior correction scenario.
func (ed *Editor) cancelCorrect() {
if ed.Buffer.spell == nil || ed.ISearch.On || ed.QReplace.On {
return
}
if !ed.Buffer.Options.SpellCorrect {
return
}
ed.Buffer.currentEditor = nil
ed.Buffer.spell.cancel()
}
// Copyright (c) 2020, 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 text
import (
"bytes"
"fmt"
"strings"
"cogentcore.org/core/texteditor/difflib"
)
// note: original difflib is: "github.com/pmezard/go-difflib/difflib"
// Diffs are raw differences between text, in terms of lines, reporting a
// sequence of operations that would convert one buffer (a) into the other
// buffer (b). Each operation is either an 'r' (replace), 'd' (delete), 'i'
// (insert) or 'e' (equal).
type Diffs []difflib.OpCode
// DiffForLine returns the diff record applying to given line, and its index in slice
func (di Diffs) DiffForLine(line int) (int, difflib.OpCode) {
for i, df := range di {
if line >= df.I1 && (line < df.I2 || line < df.J2) {
return i, df
}
}
return -1, difflib.OpCode{}
}
func DiffOpReverse(op difflib.OpCode) difflib.OpCode {
op.J1, op.I1 = op.I1, op.J1 // swap
op.J2, op.I2 = op.I2, op.J2 // swap
t := op.Tag
switch t {
case 'd':
op.Tag = 'i'
case 'i':
op.Tag = 'd'
}
return op
}
// Reverse returns the reverse-direction diffs, switching a vs. b
func (di Diffs) Reverse() Diffs {
rd := make(Diffs, len(di))
for i := range di {
rd[i] = DiffOpReverse(di[i])
}
return rd
}
func DiffOpString(op difflib.OpCode) string {
switch op.Tag {
case 'r':
return fmt.Sprintf("delete lines: %v - %v, insert lines: %v - %v", op.I1, op.I2, op.J1, op.J2)
case 'd':
return fmt.Sprintf("delete lines: %v - %v", op.I1, op.I2)
case 'i':
return fmt.Sprintf("insert lines at %v: %v - %v", op.I1, op.J1, op.J2)
case 'e':
return fmt.Sprintf("same lines %v - %v == %v - %v", op.I1, op.I2, op.J1, op.J2)
}
return "<bad tag>"
}
// String satisfies the Stringer interface
func (di Diffs) String() string {
var b strings.Builder
for _, df := range di {
b.WriteString(DiffOpString(df) + "\n")
}
return b.String()
}
// DiffLines computes the diff between two string arrays (one string per line),
// reporting a sequence of operations that would convert buffer a into buffer b.
// Each operation is either an 'r' (replace), 'd' (delete), 'i' (insert)
// or 'e' (equal). Everything is line-based (0, offset).
func DiffLines(astr, bstr []string) Diffs {
m := difflib.NewMatcherWithJunk(astr, bstr, false, nil) // no junk
return m.GetOpCodes()
}
// DiffLinesUnified computes the diff between two string arrays (one string per line),
// returning a unified diff with given amount of context (default of 3 will be
// used if -1), with given file names and modification dates.
func DiffLinesUnified(astr, bstr []string, context int, afile, adate, bfile, bdate string) []byte {
ud := difflib.UnifiedDiff{A: astr, FromFile: afile, FromDate: adate,
B: bstr, ToFile: bfile, ToDate: bdate, Context: context}
var buf bytes.Buffer
difflib.WriteUnifiedDiff(&buf, ud)
return buf.Bytes()
}
// PatchRec is a self-contained record of a DiffLines result that contains
// the source lines of the b buffer needed to patch a into b
type PatchRec struct {
// diff operation: 'r', 'd', 'i', 'e'
Op difflib.OpCode
// lines from B buffer needed for 'r' and 'i' operations
Blines []string
}
// Patch is a collection of patch records needed to turn original a buffer into b
type Patch []*PatchRec
// NumBlines returns the total number of Blines source code in the patch
func (pt Patch) NumBlines() int {
nl := 0
for _, pr := range pt {
nl += len(pr.Blines)
}
return nl
}
// ToPatch creates a Patch list from given Diffs output from DiffLines and the
// b strings from which the needed lines of source are copied.
// ApplyPatch with this on the a strings will result in the b strings.
// The resulting Patch is independent of bstr slice.
func (dif Diffs) ToPatch(bstr []string) Patch {
pt := make(Patch, len(dif))
for pi, op := range dif {
pr := &PatchRec{Op: op}
if op.Tag == 'r' || op.Tag == 'i' {
nl := (op.J2 - op.J1)
pr.Blines = make([]string, nl)
for i := 0; i < nl; i++ {
pr.Blines[i] = bstr[op.J1+i]
}
}
pt[pi] = pr
}
return pt
}
// Apply applies given Patch to given file as list of strings
// this does no checking except range checking so it won't crash
// so if input string is not appropriate for given Patch, results
// may be nonsensical.
func (pt Patch) Apply(astr []string) []string {
np := len(pt)
if np == 0 {
return astr
}
sz := len(astr)
lr := pt[np-1]
bstr := make([]string, lr.Op.J2)
for _, pr := range pt {
switch pr.Op.Tag {
case 'e':
nl := (pr.Op.J2 - pr.Op.J1)
for i := 0; i < nl; i++ {
if pr.Op.I1+i < sz {
bstr[pr.Op.J1+i] = astr[pr.Op.I1+i]
}
}
case 'r', 'i':
nl := (pr.Op.J2 - pr.Op.J1)
for i := 0; i < nl; i++ {
bstr[pr.Op.J1+i] = pr.Blines[i]
}
}
}
return bstr
}
// 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 text
import (
"slices"
"cogentcore.org/core/texteditor/difflib"
)
// DiffSelectData contains data for one set of text
type DiffSelectData struct {
// original text
Orig []string
// edits applied
Edit []string
// mapping of original line numbers (index) to edited line numbers,
// accounting for the edits applied so far
LineMap []int
// todo: in principle one should be able to reverse the edits to undo
// but the orig is different -- figure it out later..
// Undos: stack of diffs applied
Undos Diffs
// undo records
EditUndo [][]string
// undo records for ALineMap
LineMapUndo [][]int
}
// SetStringLines sets the data from given lines of strings
// The Orig is set directly and Edit is cloned
// if the input will be modified during the processing,
// call slices.Clone first
func (ds *DiffSelectData) SetStringLines(s []string) {
ds.Orig = s
ds.Edit = slices.Clone(s)
nl := len(s)
ds.LineMap = make([]int, nl)
for i := range ds.LineMap {
ds.LineMap[i] = i
}
}
func (ds *DiffSelectData) SaveUndo(op difflib.OpCode) {
ds.Undos = append(ds.Undos, op)
ds.EditUndo = append(ds.EditUndo, slices.Clone(ds.Edit))
ds.LineMapUndo = append(ds.LineMapUndo, slices.Clone(ds.LineMap))
}
func (ds *DiffSelectData) Undo() bool {
n := len(ds.LineMapUndo)
if n == 0 {
return false
}
ds.Undos = ds.Undos[:n-1]
ds.LineMap = ds.LineMapUndo[n-1]
ds.LineMapUndo = ds.LineMapUndo[:n-1]
ds.Edit = ds.EditUndo[n-1]
ds.EditUndo = ds.EditUndo[:n-1]
return true
}
// ApplyOneDiff applies given diff operator to given "B" lines
// using original "A" lines and given b line map
func ApplyOneDiff(op difflib.OpCode, bedit *[]string, aorig []string, blmap []int) {
// fmt.Println("applying:", DiffOpString(op))
switch op.Tag {
case 'r':
na := op.J2 - op.J1
nb := op.I2 - op.I1
b1 := blmap[op.I1]
nc := min(na, nb)
for i := 0; i < nc; i++ {
(*bedit)[b1+i] = aorig[op.J1+i]
}
db := na - nb
if db > 0 {
*bedit = slices.Insert(*bedit, b1+nb, aorig[op.J1+nb:op.J2]...)
} else {
*bedit = slices.Delete(*bedit, b1+na, b1+nb)
}
for i := op.I2; i < len(blmap); i++ {
blmap[i] = blmap[i] + db
}
case 'd':
nb := op.I2 - op.I1
b1 := blmap[op.I1]
*bedit = slices.Delete(*bedit, b1, b1+nb)
for i := op.I2; i < len(blmap); i++ {
blmap[i] = blmap[i] - nb
}
case 'i':
na := op.J2 - op.J1
b1 := op.I1
if op.I1 < len(blmap) {
b1 = blmap[op.I1]
} else {
b1 = len(*bedit)
}
*bedit = slices.Insert(*bedit, b1, aorig[op.J1:op.J2]...)
for i := op.I2; i < len(blmap); i++ {
blmap[i] = blmap[i] + na
}
}
}
// DiffSelected supports the incremental application of selected diffs
// between two files (either A -> B or B <- A), with Undo
type DiffSelected struct {
A DiffSelectData
B DiffSelectData
// Diffs are the diffs between A and B
Diffs Diffs
}
func NewDiffSelected(astr, bstr []string) *DiffSelected {
ds := &DiffSelected{}
ds.SetStringLines(astr, bstr)
return ds
}
// SetStringLines sets the data from given lines of strings
func (ds *DiffSelected) SetStringLines(astr, bstr []string) {
ds.A.SetStringLines(astr)
ds.B.SetStringLines(bstr)
ds.Diffs = DiffLines(astr, bstr)
}
// AtoB applies given diff index from A to B
func (ds *DiffSelected) AtoB(idx int) {
op := DiffOpReverse(ds.Diffs[idx])
ds.B.SaveUndo(op)
ApplyOneDiff(op, &ds.B.Edit, ds.A.Orig, ds.B.LineMap)
}
// BtoA applies given diff index from B to A
func (ds *DiffSelected) BtoA(idx int) {
op := ds.Diffs[idx]
ds.A.SaveUndo(op)
ApplyOneDiff(op, &ds.A.Edit, ds.B.Orig, ds.A.LineMap)
}
// Copyright (c) 2020, 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 text
import (
"slices"
"time"
"cogentcore.org/core/parse/lexer"
)
// Edit describes an edit action to a buffer -- this is the data passed
// via signals to viewers of the buffer. Actions are only deletions and
// insertions (a change is a sequence of those, given normal editing
// processes). The textview.Buf always reflects the current state *after* the edit.
type Edit struct {
// region for the edit (start is same for previous and current, end is in original pre-delete text for a delete, and in new lines data for an insert. Also contains the Time stamp for this edit.
Reg Region
// text deleted or inserted -- in lines. For Rect this is just for the spanning character distance per line, times number of lines.
Text [][]rune
// optional grouping number, for grouping edits in Undo for example
Group int
// action is either a deletion or an insertion
Delete bool
// this is a rectangular region with upper left corner = Reg.Start and lower right corner = Reg.End -- otherwise it is for the full continuous region.
Rect bool
}
// ToBytes returns the Text of this edit record to a byte string, with
// newlines at end of each line -- nil if Text is empty
func (te *Edit) ToBytes() []byte {
if te == nil {
return nil
}
sz := len(te.Text)
if sz == 0 {
return nil
}
if sz == 1 {
return []byte(string(te.Text[0]))
}
tsz := 0
for i := range te.Text {
tsz += len(te.Text[i]) + 10 // don't bother converting to runes, just extra slack
}
b := make([]byte, 0, tsz)
for i := range te.Text {
b = append(b, []byte(string(te.Text[i]))...)
if i < sz-1 {
b = append(b, '\n')
}
}
return b
}
// AdjustPosDel determines what to do with positions within deleted region
type AdjustPosDel int
// these are options for what to do with positions within deleted region
// for the AdjustPos function
const (
// AdjustPosDelErr means return a PosErr when in deleted region
AdjustPosDelErr AdjustPosDel = iota
// AdjustPosDelStart means return start of deleted region
AdjustPosDelStart
// AdjustPosDelEnd means return end of deleted region
AdjustPosDelEnd
)
// Clone returns a clone of the edit record.
func (te *Edit) Clone() *Edit {
rc := &Edit{}
rc.CopyFrom(te)
return rc
}
// Copy copies from other Edit
func (te *Edit) CopyFrom(cp *Edit) {
te.Reg = cp.Reg
te.Group = cp.Group
te.Delete = cp.Delete
te.Rect = cp.Rect
nln := len(cp.Text)
if nln == 0 {
te.Text = nil
}
te.Text = make([][]rune, nln)
for i, r := range cp.Text {
te.Text[i] = slices.Clone(r)
}
}
// AdjustPos adjusts the given text position as a function of the edit.
// if the position was within a deleted region of text, del determines
// what is returned
func (te *Edit) AdjustPos(pos lexer.Pos, del AdjustPosDel) lexer.Pos {
if te == nil {
return pos
}
if pos.IsLess(te.Reg.Start) || pos == te.Reg.Start {
return pos
}
dl := te.Reg.End.Ln - te.Reg.Start.Ln
if pos.Ln > te.Reg.End.Ln {
if te.Delete {
pos.Ln -= dl
} else {
pos.Ln += dl
}
return pos
}
if te.Delete {
if pos.Ln < te.Reg.End.Ln || pos.Ch < te.Reg.End.Ch {
switch del {
case AdjustPosDelStart:
return te.Reg.Start
case AdjustPosDelEnd:
return te.Reg.End
case AdjustPosDelErr:
return lexer.PosErr
}
}
// this means pos.Ln == te.Reg.End.Ln, Ch >= end
if dl == 0 {
pos.Ch -= (te.Reg.End.Ch - te.Reg.Start.Ch)
} else {
pos.Ch -= te.Reg.End.Ch
}
} else {
if dl == 0 {
pos.Ch += (te.Reg.End.Ch - te.Reg.Start.Ch)
} else {
pos.Ln += dl
}
}
return pos
}
// AdjustPosIfAfterTime checks the time stamp and IfAfterTime,
// it adjusts the given text position as a function of the edit
// del determines what to do with positions within a deleted region
// either move to start or end of the region, or return an error.
func (te *Edit) AdjustPosIfAfterTime(pos lexer.Pos, t time.Time, del AdjustPosDel) lexer.Pos {
if te == nil {
return pos
}
if te.Reg.IsAfterTime(t) {
return te.AdjustPos(pos, del)
}
return pos
}
// AdjustReg adjusts the given text region as a function of the edit, including
// checking that the timestamp on the region is after the edit time, if
// the region has a valid Time stamp (otherwise always does adjustment).
// If the starting position is within a deleted region, it is moved to the
// end of the deleted region, and if the ending position was within a deleted
// region, it is moved to the start. If the region becomes empty, RegionNil
// will be returned.
func (te *Edit) AdjustReg(reg Region) Region {
if te == nil {
return reg
}
if !reg.Time.IsZero() && !te.Reg.IsAfterTime(reg.Time.Time()) {
return reg
}
reg.Start = te.AdjustPos(reg.Start, AdjustPosDelEnd)
reg.End = te.AdjustPos(reg.End, AdjustPosDelStart)
if reg.IsNil() {
return RegionNil
}
return reg
}
// 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 text
import (
"bytes"
"log"
"regexp"
"slices"
"strings"
"sync"
"time"
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/indent"
"cogentcore.org/core/base/runes"
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/base/stringsx"
"cogentcore.org/core/core"
"cogentcore.org/core/parse"
"cogentcore.org/core/parse/lexer"
"cogentcore.org/core/parse/token"
"cogentcore.org/core/texteditor/highlighting"
)
const (
// ReplaceMatchCase is used for MatchCase arg in ReplaceText method
ReplaceMatchCase = true
// ReplaceNoMatchCase is used for MatchCase arg in ReplaceText method
ReplaceNoMatchCase = false
)
var (
// maximum number of lines to look for matching scope syntax (parens, brackets)
maxScopeLines = 100 // `default:"100" min:"10" step:"10"`
// maximum number of lines to apply syntax highlighting markup on
maxMarkupLines = 10000 // `default:"10000" min:"1000" step:"1000"`
// amount of time to wait before starting a new background markup process, after text changes within a single line (always does after line insertion / deletion)
markupDelay = 500 * time.Millisecond // `default:"500" min:"100" step:"100"`
)
// Lines manages multi-line text, with original source text encoded as bytes
// and runes, and a corresponding markup representation with syntax highlighting
// and other HTML-encoded text markup on top of the raw text.
// The markup is updated in a separate goroutine for efficiency.
// Everything is protected by an overall sync.Mutex and safe to concurrent access,
// and thus nothing is exported and all access is through protected accessor functions.
// In general, all unexported methods do NOT lock, and all exported methods do.
type Lines struct {
// Options are the options for how text editing and viewing works.
Options Options
// Highlighter does the syntax highlighting markup, and contains the
// parameters thereof, such as the language and style.
Highlighter highlighting.Highlighter
// Undos is the undo manager.
Undos Undo
// Markup is the marked-up version of the edited text lines, after being run
// through the syntax highlighting process. This is what is actually rendered.
// You MUST access it only under a Lock()!
Markup [][]byte
// ParseState is the parsing state information for the file.
ParseState parse.FileStates
// ChangedFunc is called whenever the text content is changed.
// The changed flag is always updated on changes, but this can be
// used for other flags or events that need to be tracked. The
// Lock is off when this is called.
ChangedFunc func()
// MarkupDoneFunc is called when the offline markup pass is done
// so that the GUI can be updated accordingly. The lock is off
// when this is called.
MarkupDoneFunc func()
// changed indicates whether any changes have been made.
// Use [IsChanged] method to access.
changed bool
// lineBytes are the live lines of text being edited,
// with the latest modifications, continuously updated
// back-and-forth with the lines runes.
lineBytes [][]byte
// Lines are the live lines of text being edited, with the latest modifications.
// They are encoded as runes per line, which is necessary for one-to-one rune/glyph
// rendering correspondence. All TextPos positions are in rune indexes, not byte
// indexes.
lines [][]rune
// tags are the extra custom tagged regions for each line.
tags []lexer.Line
// hiTags are the syntax highlighting tags, which are auto-generated.
hiTags []lexer.Line
// markupEdits are the edits that were made during the time it takes to generate
// the new markup tags -- rare but it does happen.
markupEdits []*Edit
// markupDelayTimer is the markup delay timer.
markupDelayTimer *time.Timer
// markupDelayMu is the mutex for updating the markup delay timer.
markupDelayMu sync.Mutex
// use Lock(), Unlock() directly for overall mutex on any content updates
sync.Mutex
}
// SetText sets the text to the given bytes (makes a copy).
// Pass nil to initialize an empty buffer.
func (ls *Lines) SetText(text []byte) {
ls.Lock()
defer ls.Unlock()
ls.bytesToLines(text)
ls.initFromLineBytes()
}
// SetTextLines sets linesBytes from given lines of bytes, making a copy
// and removing any trailing \r carriage returns, to standardize.
func (ls *Lines) SetTextLines(lns [][]byte) {
ls.Lock()
defer ls.Unlock()
ls.setLineBytes(lns)
ls.initFromLineBytes()
}
// Bytes returns the current text lines as a slice of bytes,
// with an additional line feed at the end, per POSIX standards.
func (ls *Lines) Bytes() []byte {
ls.Lock()
defer ls.Unlock()
return ls.bytes()
}
// SetFileInfo sets the syntax highlighting and other parameters
// based on the type of file specified by given fileinfo.FileInfo.
func (ls *Lines) SetFileInfo(info *fileinfo.FileInfo) {
ls.Lock()
defer ls.Unlock()
ls.ParseState.SetSrc(string(info.Path), "", info.Known)
ls.Highlighter.Init(info, &ls.ParseState)
ls.Options.ConfigKnown(info.Known)
if ls.numLines() > 0 {
ls.initialMarkup()
ls.startDelayedReMarkup()
}
}
// SetFileType sets the syntax highlighting and other parameters
// based on the given fileinfo.Known file type
func (ls *Lines) SetLanguage(ftyp fileinfo.Known) {
ls.SetFileInfo(fileinfo.NewFileInfoType(ftyp))
}
// SetFileExt sets syntax highlighting and other parameters
// based on the given file extension (without the . prefix),
// for cases where an actual file with [fileinfo.FileInfo] is not
// available.
func (ls *Lines) SetFileExt(ext string) {
if len(ext) == 0 {
return
}
if ext[0] == '.' {
ext = ext[1:]
}
fn := "_fake." + strings.ToLower(ext)
fi, _ := fileinfo.NewFileInfo(fn)
ls.SetFileInfo(fi)
}
// SetHighlighting sets the highlighting style.
func (ls *Lines) SetHighlighting(style core.HighlightingName) {
ls.Lock()
defer ls.Unlock()
ls.Highlighter.SetStyle(style)
}
// IsChanged reports whether any edits have been applied to text
func (ls *Lines) IsChanged() bool {
ls.Lock()
defer ls.Unlock()
return ls.changed
}
// SetChanged sets the changed flag to given value (e.g., when file saved)
func (ls *Lines) SetChanged(changed bool) {
ls.Lock()
defer ls.Unlock()
ls.changed = changed
}
// NumLines returns the number of lines.
func (ls *Lines) NumLines() int {
ls.Lock()
defer ls.Unlock()
return ls.numLines()
}
// IsValidLine returns true if given line number is in range.
func (ls *Lines) IsValidLine(ln int) bool {
if ln < 0 {
return false
}
return ls.isValidLine(ln)
}
// Line returns a (copy of) specific line of runes.
func (ls *Lines) Line(ln int) []rune {
if !ls.IsValidLine(ln) {
return nil
}
ls.Lock()
defer ls.Unlock()
return slices.Clone(ls.lines[ln])
}
// LineBytes returns a (copy of) specific line of bytes.
func (ls *Lines) LineBytes(ln int) []byte {
if !ls.IsValidLine(ln) {
return nil
}
ls.Lock()
defer ls.Unlock()
return slices.Clone(ls.lineBytes[ln])
}
// strings returns the current text as []string array.
// If addNewLine is true, each string line has a \n appended at end.
func (ls *Lines) Strings(addNewLine bool) []string {
ls.Lock()
defer ls.Unlock()
return ls.strings(addNewLine)
}
// LineLen returns the length of the given line, in runes.
func (ls *Lines) LineLen(ln int) int {
if !ls.IsValidLine(ln) {
return 0
}
ls.Lock()
defer ls.Unlock()
return len(ls.lines[ln])
}
// LineChar returns rune at given line and character position.
// returns a 0 if character position is not valid
func (ls *Lines) LineChar(ln, ch int) rune {
if !ls.IsValidLine(ln) {
return 0
}
ls.Lock()
defer ls.Unlock()
if len(ls.lines[ln]) <= ch {
return 0
}
return ls.lines[ln][ch]
}
// HiTags returns the highlighting tags for given line, nil if invalid
func (ls *Lines) HiTags(ln int) lexer.Line {
if !ls.IsValidLine(ln) {
return nil
}
ls.Lock()
defer ls.Unlock()
return ls.hiTags[ln]
}
// EndPos returns the ending position at end of lines.
func (ls *Lines) EndPos() lexer.Pos {
ls.Lock()
defer ls.Unlock()
return ls.endPos()
}
// ValidPos returns a position that is in a valid range.
func (ls *Lines) ValidPos(pos lexer.Pos) lexer.Pos {
ls.Lock()
defer ls.Unlock()
return ls.validPos(pos)
}
// Region returns a Edit representation of text between start and end positions.
// returns nil if not a valid region. sets the timestamp on the Edit to now.
func (ls *Lines) Region(st, ed lexer.Pos) *Edit {
ls.Lock()
defer ls.Unlock()
return ls.region(st, ed)
}
// RegionRect returns a Edit representation of text between
// start and end positions as a rectangle,
// returns nil if not a valid region. sets the timestamp on the Edit to now.
func (ls *Lines) RegionRect(st, ed lexer.Pos) *Edit {
ls.Lock()
defer ls.Unlock()
return ls.regionRect(st, ed)
}
/////////////////////////////////////////////////////////////////////////////
// Edits
// DeleteText is the primary method for deleting text from the lines.
// It deletes region of text between start and end positions.
// Sets the timestamp on resulting Edit to now.
// An Undo record is automatically saved depending on Undo.Off setting.
func (ls *Lines) DeleteText(st, ed lexer.Pos) *Edit {
ls.Lock()
defer ls.Unlock()
return ls.deleteText(st, ed)
}
// DeleteTextRect deletes rectangular region of text between start, end
// defining the upper-left and lower-right corners of a rectangle.
// Fails if st.Ch >= ed.Ch. Sets the timestamp on resulting Edit to now.
// An Undo record is automatically saved depending on Undo.Off setting.
func (ls *Lines) DeleteTextRect(st, ed lexer.Pos) *Edit {
ls.Lock()
defer ls.Unlock()
return ls.deleteTextRect(st, ed)
}
// InsertText is the primary method for inserting text,
// at given starting position. Sets the timestamp on resulting Edit to now.
// An Undo record is automatically saved depending on Undo.Off setting.
func (ls *Lines) InsertText(st lexer.Pos, text []byte) *Edit {
ls.Lock()
defer ls.Unlock()
return ls.insertText(st, text)
}
// InsertTextRect inserts a rectangle of text defined in given Edit record,
// (e.g., from RegionRect or DeleteRect).
// Returns a copy of the Edit record with an updated timestamp.
// An Undo record is automatically saved depending on Undo.Off setting.
func (ls *Lines) InsertTextRect(tbe *Edit) *Edit {
ls.Lock()
defer ls.Unlock()
return ls.insertTextRect(tbe)
}
// ReplaceText does DeleteText for given region, and then InsertText at given position
// (typically same as delSt but not necessarily).
// if matchCase is true, then the lexer.MatchCase function is called to match the
// case (upper / lower) of the new inserted text to that of the text being replaced.
// returns the Edit for the inserted text.
// An Undo record is automatically saved depending on Undo.Off setting.
func (ls *Lines) ReplaceText(delSt, delEd, insPos lexer.Pos, insTxt string, matchCase bool) *Edit {
ls.Lock()
defer ls.Unlock()
return ls.replaceText(delSt, delEd, insPos, insTxt, matchCase)
}
// AppendTextMarkup appends new text to end of lines, using insert, returns
// edit, and uses supplied markup to render it, for preformatted output.
func (ls *Lines) AppendTextMarkup(text []byte, markup []byte) *Edit {
ls.Lock()
defer ls.Unlock()
return ls.appendTextMarkup(text, markup)
}
// AppendTextLineMarkup appends one line of new text to end of lines, using
// insert, and appending a LF at the end of the line if it doesn't already
// have one. User-supplied markup is used. Returns the edit region.
func (ls *Lines) AppendTextLineMarkup(text []byte, markup []byte) *Edit {
ls.Lock()
defer ls.Unlock()
return ls.appendTextLineMarkup(text, markup)
}
// ReMarkup starts a background task of redoing the markup
func (ls *Lines) ReMarkup() {
ls.Lock()
defer ls.Unlock()
ls.reMarkup()
}
// Undo undoes next group of items on the undo stack,
// and returns all the edits performed.
func (ls *Lines) Undo() []*Edit {
ls.Lock()
defer ls.Unlock()
return ls.undo()
}
// Redo redoes next group of items on the undo stack,
// and returns all the edits performed.
func (ls *Lines) Redo() []*Edit {
ls.Lock()
defer ls.Unlock()
return ls.redo()
}
/////////////////////////////////////////////////////////////////////////////
// Edit helpers
// InComment returns true if the given text position is within
// a commented region.
func (ls *Lines) InComment(pos lexer.Pos) bool {
ls.Lock()
defer ls.Unlock()
return ls.inComment(pos)
}
// HiTagAtPos returns the highlighting (markup) lexical tag at given position
// using current Markup tags, and index, -- could be nil if none or out of range.
func (ls *Lines) HiTagAtPos(pos lexer.Pos) (*lexer.Lex, int) {
ls.Lock()
defer ls.Unlock()
return ls.hiTagAtPos(pos)
}
// InTokenSubCat returns true if the given text position is marked with lexical
// type in given SubCat sub-category.
func (ls *Lines) InTokenSubCat(pos lexer.Pos, subCat token.Tokens) bool {
ls.Lock()
defer ls.Unlock()
return ls.inTokenSubCat(pos, subCat)
}
// InLitString returns true if position is in a string literal.
func (ls *Lines) InLitString(pos lexer.Pos) bool {
ls.Lock()
defer ls.Unlock()
return ls.inLitString(pos)
}
// InTokenCode returns true if position is in a Keyword,
// Name, Operator, or Punctuation.
// This is useful for turning off spell checking in docs
func (ls *Lines) InTokenCode(pos lexer.Pos) bool {
ls.Lock()
defer ls.Unlock()
return ls.inTokenCode(pos)
}
// LexObjPathString returns the string at given lex, and including prior
// lex-tagged regions that include sequences of PunctSepPeriod and NameTag
// which are used for object paths -- used for e.g., debugger to pull out
// variable expressions that can be evaluated.
func (ls *Lines) LexObjPathString(ln int, lx *lexer.Lex) string {
ls.Lock()
defer ls.Unlock()
return ls.lexObjPathString(ln, lx)
}
// AdjustedTags updates tag positions for edits, for given line
// and returns the new tags
func (ls *Lines) AdjustedTags(ln int) lexer.Line {
ls.Lock()
defer ls.Unlock()
return ls.adjustedTags(ln)
}
// AdjustedTagsLine updates tag positions for edits, for given list of tags,
// associated with given line of text.
func (ls *Lines) AdjustedTagsLine(tags lexer.Line, ln int) lexer.Line {
ls.Lock()
defer ls.Unlock()
return ls.adjustedTagsLine(tags, ln)
}
// MarkupLines generates markup of given range of lines.
// end is *inclusive* line. Called after edits, under Lock().
// returns true if all lines were marked up successfully.
func (ls *Lines) MarkupLines(st, ed int) bool {
ls.Lock()
defer ls.Unlock()
return ls.markupLines(st, ed)
}
// StartDelayedReMarkup starts a timer for doing markup after an interval.
func (ls *Lines) StartDelayedReMarkup() {
ls.Lock()
defer ls.Unlock()
ls.startDelayedReMarkup()
}
// IndentLine indents line by given number of tab stops, using tabs or spaces,
// for given tab size (if using spaces) -- either inserts or deletes to reach target.
// Returns edit record for any change.
func (ls *Lines) IndentLine(ln, ind int) *Edit {
ls.Lock()
defer ls.Unlock()
return ls.indentLine(ln, ind)
}
// autoIndent indents given line to the level of the prior line, adjusted
// appropriately if the current line starts with one of the given un-indent
// strings, or the prior line ends with one of the given indent strings.
// Returns any edit that took place (could be nil), along with the auto-indented
// level and character position for the indent of the current line.
func (ls *Lines) AutoIndent(ln int) (tbe *Edit, indLev, chPos int) {
ls.Lock()
defer ls.Unlock()
return ls.autoIndent(ln)
}
// AutoIndentRegion does auto-indent over given region; end is *exclusive*.
func (ls *Lines) AutoIndentRegion(start, end int) {
ls.Lock()
defer ls.Unlock()
ls.autoIndentRegion(start, end)
}
// CommentRegion inserts comment marker on given lines; end is *exclusive*.
func (ls *Lines) CommentRegion(start, end int) {
ls.Lock()
defer ls.Unlock()
ls.commentRegion(start, end)
}
// JoinParaLines merges sequences of lines with hard returns forming paragraphs,
// separated by blank lines, into a single line per paragraph,
// within the given line regions; endLine is *inclusive*.
func (ls *Lines) JoinParaLines(startLine, endLine int) {
ls.Lock()
defer ls.Unlock()
ls.joinParaLines(startLine, endLine)
}
// TabsToSpaces replaces tabs with spaces over given region; end is *exclusive*.
func (ls *Lines) TabsToSpaces(start, end int) {
ls.Lock()
defer ls.Unlock()
ls.tabsToSpaces(start, end)
}
// SpacesToTabs replaces tabs with spaces over given region; end is *exclusive*
func (ls *Lines) SpacesToTabs(start, end int) {
ls.Lock()
defer ls.Unlock()
ls.spacesToTabs(start, end)
}
func (ls *Lines) CountWordsLinesRegion(reg Region) (words, lines int) {
ls.Lock()
defer ls.Unlock()
words, lines = CountWordsLinesRegion(ls.lines, reg)
return
}
/////////////////////////////////////////////////////////////////////////////
// Search etc
// Search looks for a string (no regexp) within buffer,
// with given case-sensitivity, returning number of occurrences
// and specific match position list. Column positions are in runes.
func (ls *Lines) Search(find []byte, ignoreCase, lexItems bool) (int, []Match) {
ls.Lock()
defer ls.Unlock()
if lexItems {
return SearchLexItems(ls.lines, ls.hiTags, find, ignoreCase)
} else {
return SearchRuneLines(ls.lines, find, ignoreCase)
}
}
// SearchRegexp looks for a string (regexp) within buffer,
// returning number of occurrences and specific match position list.
// Column positions are in runes.
func (ls *Lines) SearchRegexp(re *regexp.Regexp) (int, []Match) {
ls.Lock()
defer ls.Unlock()
return SearchByteLinesRegexp(ls.lineBytes, re)
}
// BraceMatch finds the brace, bracket, or parens that is the partner
// of the one passed to function.
func (ls *Lines) BraceMatch(r rune, st lexer.Pos) (en lexer.Pos, found bool) {
ls.Lock()
defer ls.Unlock()
return lexer.BraceMatch(ls.lines, ls.hiTags, r, st, maxScopeLines)
}
//////////////////////////////////////////////////////////////////////
// Impl below
// numLines returns number of lines
func (ls *Lines) numLines() int {
return len(ls.lines)
}
// isValidLine returns true if given line number is in range.
func (ls *Lines) isValidLine(ln int) bool {
if ln < 0 {
return false
}
return ln < ls.numLines()
}
// bytesToLines sets the lineBytes from source .text,
// making a copy of the bytes so they don't refer back to text,
// and removing any trailing \r carriage returns, to standardize.
func (ls *Lines) bytesToLines(txt []byte) {
if txt == nil {
txt = []byte("")
}
ls.setLineBytes(bytes.Split(txt, []byte("\n")))
}
// setLineBytes sets the lineBytes from source [][]byte, making copies,
// and removing any trailing \r carriage returns, to standardize.
// also removes any trailing blank line if line ended with \n
func (ls *Lines) setLineBytes(lns [][]byte) {
n := len(lns)
ls.lineBytes = slicesx.SetLength(ls.lineBytes, n)
for i, l := range lns {
ls.lineBytes[i] = slicesx.CopyFrom(ls.lineBytes[i], stringsx.ByteTrimCR(l))
}
if n > 1 && len(ls.lineBytes[n-1]) == 0 { // lines have lf at end typically
ls.lineBytes = ls.lineBytes[:n-1]
}
}
// initFromLineBytes initializes everything from lineBytes
func (ls *Lines) initFromLineBytes() {
n := len(ls.lineBytes)
ls.lines = slicesx.SetLength(ls.lines, n)
ls.tags = slicesx.SetLength(ls.tags, n)
ls.hiTags = slicesx.SetLength(ls.hiTags, n)
ls.Markup = slicesx.SetLength(ls.Markup, n)
for ln, txt := range ls.lineBytes {
ls.lines[ln] = runes.SetFromBytes(ls.lines[ln], txt)
ls.Markup[ln] = highlighting.HtmlEscapeRunes(ls.lines[ln])
}
ls.initialMarkup()
ls.startDelayedReMarkup()
}
// bytes returns the current text lines as a slice of bytes.
// with an additional line feed at the end, per POSIX standards.
func (ls *Lines) bytes() []byte {
txt := bytes.Join(ls.lineBytes, []byte("\n"))
// https://stackoverflow.com/questions/729692/why-should-text-files-end-with-a-newline
txt = append(txt, []byte("\n")...)
return txt
}
// lineOffsets returns the index offsets for the start of each line
// within an overall slice of bytes (e.g., from bytes).
func (ls *Lines) lineOffsets() []int {
n := len(ls.lineBytes)
of := make([]int, n)
bo := 0
for ln, txt := range ls.lineBytes {
of[ln] = bo
bo += len(txt) + 1 // lf
}
return of
}
// strings returns the current text as []string array.
// If addNewLine is true, each string line has a \n appended at end.
func (ls *Lines) strings(addNewLine bool) []string {
str := make([]string, ls.numLines())
for i, l := range ls.lines {
str[i] = string(l)
if addNewLine {
str[i] += "\n"
}
}
return str
}
/////////////////////////////////////////////////////////////////////////////
// Appending Lines
// endPos returns the ending position at end of lines
func (ls *Lines) endPos() lexer.Pos {
n := ls.numLines()
if n == 0 {
return lexer.PosZero
}
return lexer.Pos{n - 1, len(ls.lines[n-1])}
}
// appendTextMarkup appends new text to end of lines, using insert, returns
// edit, and uses supplied markup to render it.
func (ls *Lines) appendTextMarkup(text []byte, markup []byte) *Edit {
if len(text) == 0 {
return &Edit{}
}
ed := ls.endPos()
tbe := ls.insertText(ed, text)
st := tbe.Reg.Start.Ln
el := tbe.Reg.End.Ln
sz := (el - st) + 1
msplt := bytes.Split(markup, []byte("\n"))
if len(msplt) < sz {
log.Printf("Buf AppendTextMarkup: markup text less than appended text: is: %v, should be: %v\n", len(msplt), sz)
el = min(st+len(msplt)-1, el)
}
for ln := st; ln <= el; ln++ {
ls.Markup[ln] = msplt[ln-st]
}
return tbe
}
// appendTextLineMarkup appends one line of new text to end of lines, using
// insert, and appending a LF at the end of the line if it doesn't already
// have one. User-supplied markup is used. Returns the edit region.
func (ls *Lines) appendTextLineMarkup(text []byte, markup []byte) *Edit {
ed := ls.endPos()
sz := len(text)
addLF := true
if sz > 0 {
if text[sz-1] == '\n' {
addLF = false
}
}
efft := text
if addLF {
efft = make([]byte, sz+1)
copy(efft, text)
efft[sz] = '\n'
}
tbe := ls.insertText(ed, efft)
ls.Markup[tbe.Reg.Start.Ln] = markup
return tbe
}
/////////////////////////////////////////////////////////////////////////////
// Edits
// validPos returns a position that is in a valid range
func (ls *Lines) validPos(pos lexer.Pos) lexer.Pos {
n := ls.numLines()
if n == 0 {
return lexer.PosZero
}
if pos.Ln < 0 {
pos.Ln = 0
}
if pos.Ln >= n {
pos.Ln = n - 1
pos.Ch = len(ls.lines[pos.Ln])
return pos
}
pos.Ln = min(pos.Ln, n-1)
llen := len(ls.lines[pos.Ln])
pos.Ch = min(pos.Ch, llen)
if pos.Ch < 0 {
pos.Ch = 0
}
return pos
}
// region returns a Edit representation of text between start and end positions
// returns nil if not a valid region. sets the timestamp on the Edit to now
func (ls *Lines) region(st, ed lexer.Pos) *Edit {
st = ls.validPos(st)
ed = ls.validPos(ed)
n := ls.numLines()
// not here:
// if ed.Ln >= n {
// fmt.Println("region err in range:", ed.Ln, len(ls.lines), ed.Ch)
// }
if st == ed {
return nil
}
if !st.IsLess(ed) {
log.Printf("text.region: starting position must be less than ending!: st: %v, ed: %v\n", st, ed)
return nil
}
tbe := &Edit{Reg: NewRegionPos(st, ed)}
if ed.Ln == st.Ln {
sz := ed.Ch - st.Ch
if sz <= 0 {
return nil
}
tbe.Text = make([][]rune, 1)
tbe.Text[0] = make([]rune, sz)
copy(tbe.Text[0][:sz], ls.lines[st.Ln][st.Ch:ed.Ch])
} else {
// first get chars on start and end
if ed.Ln >= n {
ed.Ln = n - 1
ed.Ch = len(ls.lines[ed.Ln])
}
nlns := (ed.Ln - st.Ln) + 1
tbe.Text = make([][]rune, nlns)
stln := st.Ln
if st.Ch > 0 {
ec := len(ls.lines[st.Ln])
sz := ec - st.Ch
if sz > 0 {
tbe.Text[0] = make([]rune, sz)
copy(tbe.Text[0][0:sz], ls.lines[st.Ln][st.Ch:])
}
stln++
}
edln := ed.Ln
if ed.Ch < len(ls.lines[ed.Ln]) {
tbe.Text[ed.Ln-st.Ln] = make([]rune, ed.Ch)
copy(tbe.Text[ed.Ln-st.Ln], ls.lines[ed.Ln][:ed.Ch])
edln--
}
for ln := stln; ln <= edln; ln++ {
ti := ln - st.Ln
sz := len(ls.lines[ln])
tbe.Text[ti] = make([]rune, sz)
copy(tbe.Text[ti], ls.lines[ln])
}
}
return tbe
}
// regionRect returns a Edit representation of text between
// start and end positions as a rectangle,
// returns nil if not a valid region. sets the timestamp on the Edit to now
func (ls *Lines) regionRect(st, ed lexer.Pos) *Edit {
st = ls.validPos(st)
ed = ls.validPos(ed)
if st == ed {
return nil
}
if !st.IsLess(ed) || st.Ch >= ed.Ch {
log.Printf("core.Buf.RegionRect: starting position must be less than ending!: st: %v, ed: %v\n", st, ed)
return nil
}
tbe := &Edit{Reg: NewRegionPos(st, ed)}
tbe.Rect = true
// first get chars on start and end
nlns := (ed.Ln - st.Ln) + 1
nch := (ed.Ch - st.Ch)
tbe.Text = make([][]rune, nlns)
for i := 0; i < nlns; i++ {
ln := st.Ln + i
lr := ls.lines[ln]
ll := len(lr)
var txt []rune
if ll > st.Ch {
sz := min(ll-st.Ch, nch)
txt = make([]rune, sz, nch)
edl := min(ed.Ch, ll)
copy(txt, lr[st.Ch:edl])
}
if len(txt) < nch { // rect
txt = append(txt, runes.Repeat([]rune(" "), nch-len(txt))...)
}
tbe.Text[i] = txt
}
return tbe
}
// callChangedFunc calls the ChangedFunc if it is set,
// starting from a Lock state, losing and then regaining the lock.
func (ls *Lines) callChangedFunc() {
if ls.ChangedFunc == nil {
return
}
ls.Unlock()
ls.ChangedFunc()
ls.Lock()
}
// deleteText is the primary method for deleting text,
// between start and end positions.
// An Undo record is automatically saved depending on Undo.Off setting.
func (ls *Lines) deleteText(st, ed lexer.Pos) *Edit {
tbe := ls.deleteTextImpl(st, ed)
ls.saveUndo(tbe)
return tbe
}
func (ls *Lines) deleteTextImpl(st, ed lexer.Pos) *Edit {
tbe := ls.region(st, ed)
if tbe == nil {
return nil
}
tbe.Delete = true
nl := ls.numLines()
if ed.Ln == st.Ln {
if st.Ln < nl {
ec := min(ed.Ch, len(ls.lines[st.Ln])) // somehow region can still not be valid.
ls.lines[st.Ln] = append(ls.lines[st.Ln][:st.Ch], ls.lines[st.Ln][ec:]...)
ls.linesEdited(tbe)
}
} else {
// first get chars on start and end
stln := st.Ln + 1
cpln := st.Ln
ls.lines[st.Ln] = ls.lines[st.Ln][:st.Ch]
eoedl := 0
if ed.Ln >= nl {
// todo: somehow this is happening in patch diffs -- can't figure out why
// fmt.Println("err in range:", ed.Ln, nl, ed.Ch)
ed.Ln = nl - 1
}
if ed.Ch < len(ls.lines[ed.Ln]) {
eoedl = len(ls.lines[ed.Ln][ed.Ch:])
}
var eoed []rune
if eoedl > 0 { // save it
eoed = make([]rune, eoedl)
copy(eoed, ls.lines[ed.Ln][ed.Ch:])
}
ls.lines = append(ls.lines[:stln], ls.lines[ed.Ln+1:]...)
if eoed != nil {
ls.lines[cpln] = append(ls.lines[cpln], eoed...)
}
ls.linesDeleted(tbe)
}
ls.changed = true
ls.callChangedFunc()
return tbe
}
// deleteTextRect deletes rectangular region of text between start, end
// defining the upper-left and lower-right corners of a rectangle.
// Fails if st.Ch >= ed.Ch. Sets the timestamp on resulting Edit to now.
// An Undo record is automatically saved depending on Undo.Off setting.
func (ls *Lines) deleteTextRect(st, ed lexer.Pos) *Edit {
tbe := ls.deleteTextRectImpl(st, ed)
ls.saveUndo(tbe)
return tbe
}
func (ls *Lines) deleteTextRectImpl(st, ed lexer.Pos) *Edit {
tbe := ls.regionRect(st, ed)
if tbe == nil {
return nil
}
tbe.Delete = true
for ln := st.Ln; ln <= ed.Ln; ln++ {
l := ls.lines[ln]
if len(l) > st.Ch {
if ed.Ch < len(l)-1 {
ls.lines[ln] = append(l[:st.Ch], l[ed.Ch:]...)
} else {
ls.lines[ln] = l[:st.Ch]
}
}
}
ls.linesEdited(tbe)
ls.changed = true
ls.callChangedFunc()
return tbe
}
// insertText is the primary method for inserting text,
// at given starting position. Sets the timestamp on resulting Edit to now.
// An Undo record is automatically saved depending on Undo.Off setting.
func (ls *Lines) insertText(st lexer.Pos, text []byte) *Edit {
tbe := ls.insertTextImpl(st, text)
ls.saveUndo(tbe)
return tbe
}
func (ls *Lines) insertTextImpl(st lexer.Pos, text []byte) *Edit {
if len(text) == 0 {
return nil
}
st = ls.validPos(st)
lns := bytes.Split(text, []byte("\n"))
sz := len(lns)
rs := bytes.Runes(lns[0])
rsz := len(rs)
ed := st
var tbe *Edit
st.Ch = min(len(ls.lines[st.Ln]), st.Ch)
if sz == 1 {
ls.lines[st.Ln] = slices.Insert(ls.lines[st.Ln], st.Ch, rs...)
ed.Ch += rsz
tbe = ls.region(st, ed)
ls.linesEdited(tbe)
} else {
if ls.lines[st.Ln] == nil {
ls.lines[st.Ln] = []rune("")
}
eostl := len(ls.lines[st.Ln][st.Ch:]) // end of starting line
var eost []rune
if eostl > 0 { // save it
eost = make([]rune, eostl)
copy(eost, ls.lines[st.Ln][st.Ch:])
}
ls.lines[st.Ln] = append(ls.lines[st.Ln][:st.Ch], rs...)
nsz := sz - 1
tmp := make([][]rune, nsz)
for i := 1; i < sz; i++ {
tmp[i-1] = bytes.Runes(lns[i])
}
stln := st.Ln + 1
ls.lines = slices.Insert(ls.lines, stln, tmp...)
ed.Ln += nsz
ed.Ch = len(ls.lines[ed.Ln])
if eost != nil {
ls.lines[ed.Ln] = append(ls.lines[ed.Ln], eost...)
}
tbe = ls.region(st, ed)
ls.linesInserted(tbe)
}
ls.changed = true
ls.callChangedFunc()
return tbe
}
// insertTextRect inserts a rectangle of text defined in given Edit record,
// (e.g., from RegionRect or DeleteRect).
// Returns a copy of the Edit record with an updated timestamp.
// An Undo record is automatically saved depending on Undo.Off setting.
func (ls *Lines) insertTextRect(tbe *Edit) *Edit {
re := ls.insertTextRectImpl(tbe)
ls.saveUndo(re)
return tbe
}
func (ls *Lines) insertTextRectImpl(tbe *Edit) *Edit {
st := tbe.Reg.Start
ed := tbe.Reg.End
nlns := (ed.Ln - st.Ln) + 1
if nlns <= 0 {
return nil
}
ls.changed = true
// make sure there are enough lines -- add as needed
cln := ls.numLines()
if cln <= ed.Ln {
nln := (1 + ed.Ln) - cln
tmp := make([][]rune, nln)
ls.lines = append(ls.lines, tmp...)
ie := &Edit{}
ie.Reg.Start.Ln = cln - 1
ie.Reg.End.Ln = ed.Ln
ls.linesInserted(ie)
}
nch := (ed.Ch - st.Ch)
for i := 0; i < nlns; i++ {
ln := st.Ln + i
lr := ls.lines[ln]
ir := tbe.Text[i]
if len(lr) < st.Ch {
lr = append(lr, runes.Repeat([]rune(" "), st.Ch-len(lr))...)
}
nt := append(lr, ir...) // first append to end to extend capacity
copy(nt[st.Ch+nch:], nt[st.Ch:]) // move stuff to end
copy(nt[st.Ch:], ir) // copy into position
ls.lines[ln] = nt
}
re := tbe.Clone()
re.Delete = false
re.Reg.TimeNow()
ls.linesEdited(re)
return re
}
// ReplaceText does DeleteText for given region, and then InsertText at given position
// (typically same as delSt but not necessarily).
// if matchCase is true, then the lexer.MatchCase function is called to match the
// case (upper / lower) of the new inserted text to that of the text being replaced.
// returns the Edit for the inserted text.
// An Undo record is automatically saved depending on Undo.Off setting.
func (ls *Lines) replaceText(delSt, delEd, insPos lexer.Pos, insTxt string, matchCase bool) *Edit {
if matchCase {
red := ls.region(delSt, delEd)
cur := string(red.ToBytes())
insTxt = lexer.MatchCase(cur, insTxt)
}
if len(insTxt) > 0 {
ls.deleteText(delSt, delEd)
return ls.insertText(insPos, []byte(insTxt))
}
return ls.deleteText(delSt, delEd)
}
/////////////////////////////////////////////////////////////////////////////
// Undo
// saveUndo saves given edit to undo stack
func (ls *Lines) saveUndo(tbe *Edit) {
if tbe == nil {
return
}
ls.Undos.Save(tbe)
}
// undo undoes next group of items on the undo stack
func (ls *Lines) undo() []*Edit {
tbe := ls.Undos.UndoPop()
if tbe == nil {
// note: could clear the changed flag on tbe == nil in parent
return nil
}
stgp := tbe.Group
var eds []*Edit
for {
if tbe.Rect {
if tbe.Delete {
utbe := ls.insertTextRectImpl(tbe)
utbe.Group = stgp + tbe.Group
if ls.Options.EmacsUndo {
ls.Undos.SaveUndo(utbe)
}
eds = append(eds, utbe)
} else {
utbe := ls.deleteTextRectImpl(tbe.Reg.Start, tbe.Reg.End)
utbe.Group = stgp + tbe.Group
if ls.Options.EmacsUndo {
ls.Undos.SaveUndo(utbe)
}
eds = append(eds, utbe)
}
} else {
if tbe.Delete {
utbe := ls.insertTextImpl(tbe.Reg.Start, tbe.ToBytes())
utbe.Group = stgp + tbe.Group
if ls.Options.EmacsUndo {
ls.Undos.SaveUndo(utbe)
}
eds = append(eds, utbe)
} else {
utbe := ls.deleteTextImpl(tbe.Reg.Start, tbe.Reg.End)
utbe.Group = stgp + tbe.Group
if ls.Options.EmacsUndo {
ls.Undos.SaveUndo(utbe)
}
eds = append(eds, utbe)
}
}
tbe = ls.Undos.UndoPopIfGroup(stgp)
if tbe == nil {
break
}
}
return eds
}
// EmacsUndoSave is called by View at end of latest set of undo commands.
// If EmacsUndo mode is active, saves the current UndoStack to the regular Undo stack
// at the end, and moves undo to the very end -- undo is a constant stream.
func (ls *Lines) EmacsUndoSave() {
if !ls.Options.EmacsUndo {
return
}
ls.Undos.UndoStackSave()
}
// redo redoes next group of items on the undo stack,
// and returns the last record, nil if no more
func (ls *Lines) redo() []*Edit {
tbe := ls.Undos.RedoNext()
if tbe == nil {
return nil
}
var eds []*Edit
stgp := tbe.Group
for {
if tbe.Rect {
if tbe.Delete {
ls.deleteTextRectImpl(tbe.Reg.Start, tbe.Reg.End)
} else {
ls.insertTextRectImpl(tbe)
}
} else {
if tbe.Delete {
ls.deleteTextImpl(tbe.Reg.Start, tbe.Reg.End)
} else {
ls.insertTextImpl(tbe.Reg.Start, tbe.ToBytes())
}
}
eds = append(eds, tbe)
tbe = ls.Undos.RedoNextIfGroup(stgp)
if tbe == nil {
break
}
}
return eds
}
// DiffBuffers computes the diff between this buffer and the other buffer,
// reporting a sequence of operations that would convert this buffer (a) into
// the other buffer (b). Each operation is either an 'r' (replace), 'd'
// (delete), 'i' (insert) or 'e' (equal). Everything is line-based (0, offset).
func (ls *Lines) DiffBuffers(ob *Lines) Diffs {
ls.Lock()
defer ls.Unlock()
return ls.diffBuffers(ob)
}
// PatchFromBuffer patches (edits) using content from other,
// according to diff operations (e.g., as generated from DiffBufs).
func (ls *Lines) PatchFromBuffer(ob *Lines, diffs Diffs) bool {
ls.Lock()
defer ls.Unlock()
return ls.patchFromBuffer(ob, diffs)
}
/////////////////////////////////////////////////////////////////////////////
// Syntax Highlighting Markup
// linesEdited re-marks-up lines in edit (typically only 1).
func (ls *Lines) linesEdited(tbe *Edit) {
st, ed := tbe.Reg.Start.Ln, tbe.Reg.End.Ln
for ln := st; ln <= ed; ln++ {
ls.lineBytes[ln] = []byte(string(ls.lines[ln]))
ls.Markup[ln] = highlighting.HtmlEscapeRunes(ls.lines[ln])
}
ls.markupLines(st, ed)
ls.startDelayedReMarkup()
}
// linesInserted inserts new lines for all other line-based slices
// corresponding to lines inserted in the lines slice.
func (ls *Lines) linesInserted(tbe *Edit) {
stln := tbe.Reg.Start.Ln + 1
nsz := (tbe.Reg.End.Ln - tbe.Reg.Start.Ln)
ls.markupEdits = append(ls.markupEdits, tbe)
ls.lineBytes = slices.Insert(ls.lineBytes, stln, make([][]byte, nsz)...)
ls.Markup = slices.Insert(ls.Markup, stln, make([][]byte, nsz)...)
ls.tags = slices.Insert(ls.tags, stln, make([]lexer.Line, nsz)...)
ls.hiTags = slices.Insert(ls.hiTags, stln, make([]lexer.Line, nsz)...)
if ls.Highlighter.UsingParse() {
pfs := ls.ParseState.Done()
pfs.Src.LinesInserted(stln, nsz)
}
ls.linesEdited(tbe)
}
// linesDeleted deletes lines in Markup corresponding to lines
// deleted in Lines text.
func (ls *Lines) linesDeleted(tbe *Edit) {
ls.markupEdits = append(ls.markupEdits, tbe)
stln := tbe.Reg.Start.Ln
edln := tbe.Reg.End.Ln
ls.lineBytes = append(ls.lineBytes[:stln], ls.lineBytes[edln:]...)
ls.Markup = append(ls.Markup[:stln], ls.Markup[edln:]...)
ls.tags = append(ls.tags[:stln], ls.tags[edln:]...)
ls.hiTags = append(ls.hiTags[:stln], ls.hiTags[edln:]...)
if ls.Highlighter.UsingParse() {
pfs := ls.ParseState.Done()
pfs.Src.LinesDeleted(stln, edln)
}
st := tbe.Reg.Start.Ln
ls.lineBytes[st] = []byte(string(ls.lines[st]))
ls.Markup[st] = highlighting.HtmlEscapeRunes(ls.lines[st])
ls.markupLines(st, st)
ls.startDelayedReMarkup()
}
///////////////////////////////////////////////////////////////////////////////////////
// Markup
// initialMarkup does the first-pass markup on the file
func (ls *Lines) initialMarkup() {
if !ls.Highlighter.Has || ls.numLines() == 0 {
return
}
if ls.Highlighter.UsingParse() {
fs := ls.ParseState.Done() // initialize
fs.Src.SetBytes(ls.bytes())
}
mxhi := min(100, ls.numLines())
txt := bytes.Join(ls.lineBytes[:mxhi], []byte("\n"))
txt = append(txt, []byte("\n")...)
tags, err := ls.markupTags(txt)
if err == nil {
ls.markupApplyTags(tags)
}
}
// startDelayedReMarkup starts a timer for doing markup after an interval.
func (ls *Lines) startDelayedReMarkup() {
ls.markupDelayMu.Lock()
defer ls.markupDelayMu.Unlock()
if !ls.Highlighter.Has || ls.numLines() == 0 || ls.numLines() > maxMarkupLines {
return
}
if ls.markupDelayTimer != nil {
ls.markupDelayTimer.Stop()
ls.markupDelayTimer = nil
}
ls.markupDelayTimer = time.AfterFunc(markupDelay, func() {
ls.markupDelayTimer = nil
ls.asyncMarkup() // already in a goroutine
})
}
// StopDelayedReMarkup stops timer for doing markup after an interval
func (ls *Lines) StopDelayedReMarkup() {
ls.markupDelayMu.Lock()
defer ls.markupDelayMu.Unlock()
if ls.markupDelayTimer != nil {
ls.markupDelayTimer.Stop()
ls.markupDelayTimer = nil
}
}
// reMarkup runs re-markup on text in background
func (ls *Lines) reMarkup() {
if !ls.Highlighter.Has || ls.numLines() == 0 || ls.numLines() > maxMarkupLines {
return
}
ls.StopDelayedReMarkup()
go ls.asyncMarkup()
}
// AdjustRegion adjusts given text region for any edits that
// have taken place since time stamp on region (using the Undo stack).
// If region was wholly within a deleted region, then RegionNil will be
// returned -- otherwise it is clipped appropriately as function of deletes.
func (ls *Lines) AdjustRegion(reg Region) Region {
return ls.Undos.AdjustRegion(reg)
}
// adjustedTags updates tag positions for edits, for given list of tags
func (ls *Lines) adjustedTags(ln int) lexer.Line {
if !ls.isValidLine(ln) {
return nil
}
return ls.adjustedTagsLine(ls.tags[ln], ln)
}
// adjustedTagsLine updates tag positions for edits, for given list of tags
func (ls *Lines) adjustedTagsLine(tags lexer.Line, ln int) lexer.Line {
sz := len(tags)
if sz == 0 {
return nil
}
ntags := make(lexer.Line, 0, sz)
for _, tg := range tags {
reg := Region{Start: lexer.Pos{Ln: ln, Ch: tg.St}, End: lexer.Pos{Ln: ln, Ch: tg.Ed}}
reg.Time = tg.Time
reg = ls.Undos.AdjustRegion(reg)
if !reg.IsNil() {
ntr := ntags.AddLex(tg.Token, reg.Start.Ch, reg.End.Ch)
ntr.Time.Now()
}
}
return ntags
}
// asyncMarkup does the markupTags from a separate goroutine.
// Does not start or end with lock, but acquires at end to apply.
func (ls *Lines) asyncMarkup() {
ls.Lock()
txt := ls.bytes()
ls.markupEdits = nil // only accumulate after this point; very rare
ls.Unlock()
tags, err := ls.markupTags(txt)
if err != nil {
return
}
ls.Lock()
ls.markupApplyTags(tags)
ls.Unlock()
if ls.MarkupDoneFunc != nil {
ls.MarkupDoneFunc()
}
}
// markupTags generates the new markup tags from the highligher.
// this is a time consuming step, done via asyncMarkup typically.
// does not require any locking.
func (ls *Lines) markupTags(txt []byte) ([]lexer.Line, error) {
return ls.Highlighter.MarkupTagsAll(txt)
}
// markupApplyEdits applies any edits in markupEdits to the
// tags prior to applying the tags. returns the updated tags.
func (ls *Lines) markupApplyEdits(tags []lexer.Line) []lexer.Line {
edits := ls.markupEdits
ls.markupEdits = nil
if ls.Highlighter.UsingParse() {
pfs := ls.ParseState.Done()
for _, tbe := range edits {
if tbe.Delete {
stln := tbe.Reg.Start.Ln
edln := tbe.Reg.End.Ln
pfs.Src.LinesDeleted(stln, edln)
} else {
stln := tbe.Reg.Start.Ln + 1
nlns := (tbe.Reg.End.Ln - tbe.Reg.Start.Ln)
pfs.Src.LinesInserted(stln, nlns)
}
}
for ln := range tags {
tags[ln] = pfs.LexLine(ln) // does clone, combines comments too
}
} else {
for _, tbe := range edits {
if tbe.Delete {
stln := tbe.Reg.Start.Ln
edln := tbe.Reg.End.Ln
tags = append(tags[:stln], tags[edln:]...)
} else {
stln := tbe.Reg.Start.Ln + 1
nlns := (tbe.Reg.End.Ln - tbe.Reg.Start.Ln)
stln = min(stln, len(tags))
tags = slices.Insert(tags, stln, make([]lexer.Line, nlns)...)
}
}
}
return tags
}
// markupApplyTags applies given tags to current text
// and sets the markup lines. Must be called under Lock.
func (ls *Lines) markupApplyTags(tags []lexer.Line) {
tags = ls.markupApplyEdits(tags)
maxln := min(len(tags), ls.numLines())
for ln := range maxln {
ls.hiTags[ln] = tags[ln]
ls.tags[ln] = ls.adjustedTags(ln)
ls.Markup[ln] = highlighting.MarkupLine(ls.lines[ln], tags[ln], ls.tags[ln], highlighting.EscapeHTML)
}
}
// markupLines generates markup of given range of lines.
// end is *inclusive* line. Called after edits, under Lock().
// returns true if all lines were marked up successfully.
func (ls *Lines) markupLines(st, ed int) bool {
n := ls.numLines()
if !ls.Highlighter.Has || n == 0 {
return false
}
if ed >= n {
ed = n - 1
}
allgood := true
for ln := st; ln <= ed; ln++ {
ltxt := ls.lines[ln]
mt, err := ls.Highlighter.MarkupTagsLine(ln, ltxt)
if err == nil {
ls.hiTags[ln] = mt
ls.Markup[ln] = highlighting.MarkupLine(ltxt, mt, ls.adjustedTags(ln), highlighting.EscapeHTML)
} else {
ls.Markup[ln] = highlighting.HtmlEscapeRunes(ltxt)
allgood = false
}
}
// Now we trigger a background reparse of everything in a separate parse.FilesState
// that gets switched into the current.
return allgood
}
/////////////////////////////////////////////////////////////////////////////
// Tags
// AddTag adds a new custom tag for given line, at given position.
func (ls *Lines) AddTag(ln, st, ed int, tag token.Tokens) {
if !ls.IsValidLine(ln) {
return
}
ls.Lock()
defer ls.Unlock()
tr := lexer.NewLex(token.KeyToken{Token: tag}, st, ed)
tr.Time.Now()
if len(ls.tags[ln]) == 0 {
ls.tags[ln] = append(ls.tags[ln], tr)
} else {
ls.tags[ln] = ls.adjustedTags(ln) // must re-adjust before adding new ones!
ls.tags[ln].AddSort(tr)
}
ls.markupLines(ln, ln)
}
// AddTagEdit adds a new custom tag for given line, using Edit for location.
func (ls *Lines) AddTagEdit(tbe *Edit, tag token.Tokens) {
ls.AddTag(tbe.Reg.Start.Ln, tbe.Reg.Start.Ch, tbe.Reg.End.Ch, tag)
}
// RemoveTag removes tag (optionally only given tag if non-zero)
// at given position if it exists. returns tag.
func (ls *Lines) RemoveTag(pos lexer.Pos, tag token.Tokens) (reg lexer.Lex, ok bool) {
if !ls.IsValidLine(pos.Ln) {
return
}
ls.Lock()
defer ls.Unlock()
ls.tags[pos.Ln] = ls.adjustedTags(pos.Ln) // re-adjust for current info
for i, t := range ls.tags[pos.Ln] {
if t.ContainsPos(pos.Ch) {
if tag > 0 && t.Token.Token != tag {
continue
}
ls.tags[pos.Ln].DeleteIndex(i)
reg = t
ok = true
break
}
}
if ok {
ls.markupLines(pos.Ln, pos.Ln)
}
return
}
// SetTags tags for given line.
func (ls *Lines) SetTags(ln int, tags lexer.Line) {
if !ls.IsValidLine(ln) {
return
}
ls.Lock()
defer ls.Unlock()
ls.tags[ln] = tags
}
// lexObjPathString returns the string at given lex, and including prior
// lex-tagged regions that include sequences of PunctSepPeriod and NameTag
// which are used for object paths -- used for e.g., debugger to pull out
// variable expressions that can be evaluated.
func (ls *Lines) lexObjPathString(ln int, lx *lexer.Lex) string {
if !ls.isValidLine(ln) {
return ""
}
lln := len(ls.lines[ln])
if lx.Ed > lln {
return ""
}
stlx := lexer.ObjPathAt(ls.hiTags[ln], lx)
if stlx.St >= lx.Ed {
return ""
}
return string(ls.lines[ln][stlx.St:lx.Ed])
}
// hiTagAtPos returns the highlighting (markup) lexical tag at given position
// using current Markup tags, and index, -- could be nil if none or out of range
func (ls *Lines) hiTagAtPos(pos lexer.Pos) (*lexer.Lex, int) {
if !ls.isValidLine(pos.Ln) {
return nil, -1
}
return ls.hiTags[pos.Ln].AtPos(pos.Ch)
}
// inTokenSubCat returns true if the given text position is marked with lexical
// type in given SubCat sub-category.
func (ls *Lines) inTokenSubCat(pos lexer.Pos, subCat token.Tokens) bool {
lx, _ := ls.hiTagAtPos(pos)
return lx != nil && lx.Token.Token.InSubCat(subCat)
}
// inLitString returns true if position is in a string literal
func (ls *Lines) inLitString(pos lexer.Pos) bool {
return ls.inTokenSubCat(pos, token.LitStr)
}
// inTokenCode returns true if position is in a Keyword,
// Name, Operator, or Punctuation.
// This is useful for turning off spell checking in docs
func (ls *Lines) inTokenCode(pos lexer.Pos) bool {
lx, _ := ls.hiTagAtPos(pos)
if lx == nil {
return false
}
return lx.Token.Token.IsCode()
}
/////////////////////////////////////////////////////////////////////////////
// Indenting
// see parse/lexer/indent.go for support functions
// indentLine indents line by given number of tab stops, using tabs or spaces,
// for given tab size (if using spaces) -- either inserts or deletes to reach target.
// Returns edit record for any change.
func (ls *Lines) indentLine(ln, ind int) *Edit {
tabSz := ls.Options.TabSize
ichr := indent.Tab
if ls.Options.SpaceIndent {
ichr = indent.Space
}
curind, _ := lexer.LineIndent(ls.lines[ln], tabSz)
if ind > curind {
return ls.insertText(lexer.Pos{Ln: ln}, indent.Bytes(ichr, ind-curind, tabSz))
} else if ind < curind {
spos := indent.Len(ichr, ind, tabSz)
cpos := indent.Len(ichr, curind, tabSz)
return ls.deleteText(lexer.Pos{Ln: ln, Ch: spos}, lexer.Pos{Ln: ln, Ch: cpos})
}
return nil
}
// autoIndent indents given line to the level of the prior line, adjusted
// appropriately if the current line starts with one of the given un-indent
// strings, or the prior line ends with one of the given indent strings.
// Returns any edit that took place (could be nil), along with the auto-indented
// level and character position for the indent of the current line.
func (ls *Lines) autoIndent(ln int) (tbe *Edit, indLev, chPos int) {
tabSz := ls.Options.TabSize
lp, _ := parse.LanguageSupport.Properties(ls.ParseState.Known)
var pInd, delInd int
if lp != nil && lp.Lang != nil {
pInd, delInd, _, _ = lp.Lang.IndentLine(&ls.ParseState, ls.lines, ls.hiTags, ln, tabSz)
} else {
pInd, delInd, _, _ = lexer.BracketIndentLine(ls.lines, ls.hiTags, ln, tabSz)
}
ichr := ls.Options.IndentChar()
indLev = pInd + delInd
chPos = indent.Len(ichr, indLev, tabSz)
tbe = ls.indentLine(ln, indLev)
return
}
// autoIndentRegion does auto-indent over given region; end is *exclusive*
func (ls *Lines) autoIndentRegion(start, end int) {
end = min(ls.numLines(), end)
for ln := start; ln < end; ln++ {
ls.autoIndent(ln)
}
}
// commentStart returns the char index where the comment
// starts on given line, -1 if no comment.
func (ls *Lines) commentStart(ln int) int {
if !ls.isValidLine(ln) {
return -1
}
comst, _ := ls.Options.CommentStrings()
if comst == "" {
return -1
}
return runes.Index(ls.lines[ln], []rune(comst))
}
// inComment returns true if the given text position is within
// a commented region.
func (ls *Lines) inComment(pos lexer.Pos) bool {
if ls.inTokenSubCat(pos, token.Comment) {
return true
}
cs := ls.commentStart(pos.Ln)
if cs < 0 {
return false
}
return pos.Ch > cs
}
// lineCommented returns true if the given line is a full-comment
// line (i.e., starts with a comment).
func (ls *Lines) lineCommented(ln int) bool {
if !ls.isValidLine(ln) {
return false
}
tags := ls.hiTags[ln]
if len(tags) == 0 {
return false
}
return tags[0].Token.Token.InCat(token.Comment)
}
// commentRegion inserts comment marker on given lines; end is *exclusive*.
func (ls *Lines) commentRegion(start, end int) {
tabSz := ls.Options.TabSize
ch := 0
ind, _ := lexer.LineIndent(ls.lines[start], tabSz)
if ind > 0 {
if ls.Options.SpaceIndent {
ch = ls.Options.TabSize * ind
} else {
ch = ind
}
}
comst, comed := ls.Options.CommentStrings()
if comst == "" {
log.Printf("text.Lines: attempt to comment region without any comment syntax defined")
return
}
eln := min(ls.numLines(), end)
ncom := 0
nln := eln - start
for ln := start; ln < eln; ln++ {
if ls.lineCommented(ln) {
ncom++
}
}
trgln := max(nln-2, 1)
doCom := true
if ncom >= trgln {
doCom = false
}
for ln := start; ln < eln; ln++ {
if doCom {
ls.insertText(lexer.Pos{Ln: ln, Ch: ch}, []byte(comst))
if comed != "" {
lln := len(ls.lines[ln])
ls.insertText(lexer.Pos{Ln: ln, Ch: lln}, []byte(comed))
}
} else {
idx := ls.commentStart(ln)
if idx >= 0 {
ls.deleteText(lexer.Pos{Ln: ln, Ch: idx}, lexer.Pos{Ln: ln, Ch: idx + len(comst)})
}
if comed != "" {
idx := runes.IndexFold(ls.lines[ln], []rune(comed))
if idx >= 0 {
ls.deleteText(lexer.Pos{Ln: ln, Ch: idx}, lexer.Pos{Ln: ln, Ch: idx + len(comed)})
}
}
}
}
}
// joinParaLines merges sequences of lines with hard returns forming paragraphs,
// separated by blank lines, into a single line per paragraph,
// within the given line regions; endLine is *inclusive*.
func (ls *Lines) joinParaLines(startLine, endLine int) {
// current end of region being joined == last blank line
curEd := endLine
for ln := endLine; ln >= startLine; ln-- { // reverse order
lb := ls.lineBytes[ln]
lbt := bytes.TrimSpace(lb)
if len(lbt) == 0 || ln == startLine {
if ln < curEd-1 {
stp := lexer.Pos{Ln: ln + 1}
if ln == startLine {
stp.Ln--
}
ep := lexer.Pos{Ln: curEd - 1}
if curEd == endLine {
ep.Ln = curEd
}
eln := ls.lines[ep.Ln]
ep.Ch = len(eln)
tlb := bytes.Join(ls.lineBytes[stp.Ln:ep.Ln+1], []byte(" "))
ls.replaceText(stp, ep, stp, string(tlb), ReplaceNoMatchCase)
}
curEd = ln
}
}
}
// tabsToSpacesLine replaces tabs with spaces in the given line.
func (ls *Lines) tabsToSpacesLine(ln int) {
tabSz := ls.Options.TabSize
lr := ls.lines[ln]
st := lexer.Pos{Ln: ln}
ed := lexer.Pos{Ln: ln}
i := 0
for {
if i >= len(lr) {
break
}
r := lr[i]
if r == '\t' {
po := i % tabSz
nspc := tabSz - po
st.Ch = i
ed.Ch = i + 1
ls.replaceText(st, ed, st, indent.Spaces(1, nspc), ReplaceNoMatchCase)
i += nspc
lr = ls.lines[ln]
} else {
i++
}
}
}
// tabsToSpaces replaces tabs with spaces over given region; end is *exclusive*.
func (ls *Lines) tabsToSpaces(start, end int) {
end = min(ls.numLines(), end)
for ln := start; ln < end; ln++ {
ls.tabsToSpacesLine(ln)
}
}
// spacesToTabsLine replaces spaces with tabs in the given line.
func (ls *Lines) spacesToTabsLine(ln int) {
tabSz := ls.Options.TabSize
lr := ls.lines[ln]
st := lexer.Pos{Ln: ln}
ed := lexer.Pos{Ln: ln}
i := 0
nspc := 0
for {
if i >= len(lr) {
break
}
r := lr[i]
if r == ' ' {
nspc++
if nspc == tabSz {
st.Ch = i - (tabSz - 1)
ed.Ch = i + 1
ls.replaceText(st, ed, st, "\t", ReplaceNoMatchCase)
i -= tabSz - 1
lr = ls.lines[ln]
nspc = 0
} else {
i++
}
} else {
nspc = 0
i++
}
}
}
// spacesToTabs replaces tabs with spaces over given region; end is *exclusive*
func (ls *Lines) spacesToTabs(start, end int) {
end = min(ls.numLines(), end)
for ln := start; ln < end; ln++ {
ls.spacesToTabsLine(ln)
}
}
///////////////////////////////////////////////////////////////////
// Diff
// diffBuffers computes the diff between this buffer and the other buffer,
// reporting a sequence of operations that would convert this buffer (a) into
// the other buffer (b). Each operation is either an 'r' (replace), 'd'
// (delete), 'i' (insert) or 'e' (equal). Everything is line-based (0, offset).
func (ls *Lines) diffBuffers(ob *Lines) Diffs {
astr := ls.strings(false)
bstr := ob.strings(false)
return DiffLines(astr, bstr)
}
// patchFromBuffer patches (edits) using content from other,
// according to diff operations (e.g., as generated from DiffBufs).
func (ls *Lines) patchFromBuffer(ob *Lines, diffs Diffs) bool {
sz := len(diffs)
mods := false
for i := sz - 1; i >= 0; i-- { // go in reverse so changes are valid!
df := diffs[i]
switch df.Tag {
case 'r':
ls.deleteText(lexer.Pos{Ln: df.I1}, lexer.Pos{Ln: df.I2})
// fmt.Printf("patch rep del: %v %v\n", tbe.Reg, string(tbe.ToBytes()))
ot := ob.Region(lexer.Pos{Ln: df.J1}, lexer.Pos{Ln: df.J2})
ls.insertText(lexer.Pos{Ln: df.I1}, ot.ToBytes())
// fmt.Printf("patch rep ins: %v %v\n", tbe.Reg, string(tbe.ToBytes()))
mods = true
case 'd':
ls.deleteText(lexer.Pos{Ln: df.I1}, lexer.Pos{Ln: df.I2})
// fmt.Printf("patch del: %v %v\n", tbe.Reg, string(tbe.ToBytes()))
mods = true
case 'i':
ot := ob.Region(lexer.Pos{Ln: df.J1}, lexer.Pos{Ln: df.J2})
ls.insertText(lexer.Pos{Ln: df.I1}, ot.ToBytes())
// fmt.Printf("patch ins: %v %v\n", tbe.Reg, string(tbe.ToBytes()))
mods = true
}
}
return mods
}
// Copyright (c) 2020, 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 text
import (
"cogentcore.org/core/base/fileinfo"
"cogentcore.org/core/base/indent"
"cogentcore.org/core/core"
"cogentcore.org/core/parse"
)
// Options contains options for [texteditor.Buffer]s. It contains
// everything necessary to customize editing of a certain text file.
type Options struct {
// editor settings from core settings
core.EditorSettings
// character(s) that start a single-line comment; if empty then multi-line comment syntax will be used
CommentLine string
// character(s) that start a multi-line comment or one that requires both start and end
CommentStart string
// character(s) that end a multi-line comment or one that requires both start and end
CommentEnd string
}
// CommentStrings returns the comment start and end strings, using line-based CommentLn first if set
// and falling back on multi-line / general purpose start / end syntax
func (tb *Options) CommentStrings() (comst, comed string) {
comst = tb.CommentLine
if comst == "" {
comst = tb.CommentStart
comed = tb.CommentEnd
}
return
}
// IndentChar returns the indent character based on SpaceIndent option
func (tb *Options) IndentChar() indent.Character {
if tb.SpaceIndent {
return indent.Space
}
return indent.Tab
}
// ConfigKnown configures options based on the supported language info in parse.
// Returns true if supported.
func (tb *Options) ConfigKnown(sup fileinfo.Known) bool {
if sup == fileinfo.Unknown {
return false
}
lp, ok := parse.StandardLanguageProperties[sup]
if !ok {
return false
}
tb.CommentLine = lp.CommentLn
tb.CommentStart = lp.CommentSt
tb.CommentEnd = lp.CommentEd
for _, flg := range lp.Flags {
switch flg {
case parse.IndentSpace:
tb.SpaceIndent = true
case parse.IndentTab:
tb.SpaceIndent = false
}
}
return true
}
// KnownComments returns the comment strings for supported file types,
// and returns the standard C-style comments otherwise.
func KnownComments(fpath string) (comLn, comSt, comEd string) {
comLn = "//"
comSt = "/*"
comEd = "*/"
mtyp, _, err := fileinfo.MimeFromFile(fpath)
if err != nil {
return
}
sup := fileinfo.MimeKnown(mtyp)
if sup == fileinfo.Unknown {
return
}
lp, ok := parse.StandardLanguageProperties[sup]
if !ok {
return
}
comLn = lp.CommentLn
comSt = lp.CommentSt
comEd = lp.CommentEd
return
}
// Copyright (c) 2020, 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 text
import (
"fmt"
"strings"
"time"
"cogentcore.org/core/base/nptime"
"cogentcore.org/core/parse/lexer"
)
// Region represents a text region as a start / end position, and includes
// a Time stamp for when the region was created as valid positions into the textview.Buf.
// The character end position is an *exclusive* position (i.e., the region ends at
// the character just prior to that character) but the lines are always *inclusive*
// (i.e., it is the actual line, not the next line).
type Region struct {
// starting position
Start lexer.Pos
// ending position: line number is *inclusive* but character position is *exclusive* (-1)
End lexer.Pos
// time when region was set -- needed for updating locations in the text based on time stamp (using efficient non-pointer time)
Time nptime.Time
}
// RegionNil is the empty (zero) text region -- all zeros
var RegionNil Region
// IsNil checks if the region is empty, because the start is after or equal to the end
func (tr *Region) IsNil() bool {
return !tr.Start.IsLess(tr.End)
}
// IsSameLine returns true if region starts and ends on the same line
func (tr *Region) IsSameLine() bool {
return tr.Start.Ln == tr.End.Ln
}
// Contains returns true if line is within region
func (tr *Region) Contains(ln int) bool {
return tr.Start.Ln >= ln && ln <= tr.End.Ln
}
// TimeNow grabs the current time as the edit time
func (tr *Region) TimeNow() {
tr.Time.Now()
}
// NewRegion creates a new text region using separate line and char
// values for start and end, and also sets the time stamp to now
func NewRegion(stLn, stCh, edLn, edCh int) Region {
tr := Region{Start: lexer.Pos{Ln: stLn, Ch: stCh}, End: lexer.Pos{Ln: edLn, Ch: edCh}}
tr.TimeNow()
return tr
}
// NewRegionPos creates a new text region using position values
// and also sets the time stamp to now
func NewRegionPos(st, ed lexer.Pos) Region {
tr := Region{Start: st, End: ed}
tr.TimeNow()
return tr
}
// IsAfterTime reports if this region's time stamp is after given time value
// if region Time stamp has not been set, it always returns true
func (tr *Region) IsAfterTime(t time.Time) bool {
if tr.Time.IsZero() {
return true
}
return tr.Time.Time().After(t)
}
// Ago returns how long ago this Region's time stamp is relative
// to given time.
func (tr *Region) Ago(t time.Time) time.Duration {
return t.Sub(tr.Time.Time())
}
// Age returns the time interval from [time.Now]
func (tr *Region) Age() time.Duration {
return tr.Ago(time.Now())
}
// Since returns the time interval between
// this Region's time stamp and that of the given earlier region's stamp.
func (tr *Region) Since(earlier *Region) time.Duration {
return earlier.Ago(tr.Time.Time())
}
// FromString decodes text region from a string representation of form:
// [#]LxxCxx-LxxCxx -- used in e.g., URL links -- returns true if successful
func (tr *Region) FromString(link string) bool {
link = strings.TrimPrefix(link, "#")
fmt.Sscanf(link, "L%dC%d-L%dC%d", &tr.Start.Ln, &tr.Start.Ch, &tr.End.Ln, &tr.End.Ch)
tr.Start.Ln--
tr.Start.Ch--
tr.End.Ln--
tr.End.Ch--
return true
}
// NewRegionLen makes a new Region from a starting point and a length
// along same line
func NewRegionLen(start lexer.Pos, len int) Region {
reg := Region{}
reg.Start = start
reg.End = start
reg.End.Ch += len
return reg
}
// Copyright (c) 2020, 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 text
import (
"bufio"
"bytes"
"io"
"log"
"os"
"regexp"
"unicode/utf8"
"cogentcore.org/core/base/runes"
"cogentcore.org/core/parse/lexer"
)
// Match records one match for search within file, positions in runes
type Match struct {
// region surrounding the match -- column positions are in runes, not bytes
Reg Region
// text surrounding the match, at most FileSearchContext on either side (within a single line)
Text []byte
}
// SearchContext is how much text to include on either side of the search match
var SearchContext = 30
var mst = []byte("<mark>")
var mstsz = len(mst)
var med = []byte("</mark>")
var medsz = len(med)
// NewMatch returns a new Match entry for given rune line with match starting
// at st and ending before ed, on given line
func NewMatch(rn []rune, st, ed, ln int) Match {
sz := len(rn)
reg := NewRegion(ln, st, ln, ed)
cist := max(st-SearchContext, 0)
cied := min(ed+SearchContext, sz)
sctx := []byte(string(rn[cist:st]))
fstr := []byte(string(rn[st:ed]))
ectx := []byte(string(rn[ed:cied]))
tlen := mstsz + medsz + len(sctx) + len(fstr) + len(ectx)
txt := make([]byte, tlen)
copy(txt, sctx)
ti := st - cist
copy(txt[ti:], mst)
ti += mstsz
copy(txt[ti:], fstr)
ti += len(fstr)
copy(txt[ti:], med)
ti += medsz
copy(txt[ti:], ectx)
return Match{Reg: reg, Text: txt}
}
const (
// IgnoreCase is passed to search functions to indicate case should be ignored
IgnoreCase = true
// UseCase is passed to search functions to indicate case is relevant
UseCase = false
)
// SearchRuneLines looks for a string (no regexp) within lines of runes,
// with given case-sensitivity returning number of occurrences
// and specific match position list. Column positions are in runes.
func SearchRuneLines(src [][]rune, find []byte, ignoreCase bool) (int, []Match) {
fr := bytes.Runes(find)
fsz := len(fr)
if fsz == 0 {
return 0, nil
}
cnt := 0
var matches []Match
for ln, rn := range src {
sz := len(rn)
ci := 0
for ci < sz {
var i int
if ignoreCase {
i = runes.IndexFold(rn[ci:], fr)
} else {
i = runes.Index(rn[ci:], fr)
}
if i < 0 {
break
}
i += ci
ci = i + fsz
mat := NewMatch(rn, i, ci, ln)
matches = append(matches, mat)
cnt++
}
}
return cnt, matches
}
// SearchLexItems looks for a string (no regexp),
// as entire lexically tagged items,
// with given case-sensitivity returning number of occurrences
// and specific match position list. Column positions are in runes.
func SearchLexItems(src [][]rune, lexs []lexer.Line, find []byte, ignoreCase bool) (int, []Match) {
fr := bytes.Runes(find)
fsz := len(fr)
if fsz == 0 {
return 0, nil
}
cnt := 0
var matches []Match
mx := min(len(src), len(lexs))
for ln := 0; ln < mx; ln++ {
rln := src[ln]
lxln := lexs[ln]
for _, lx := range lxln {
sz := lx.Ed - lx.St
if sz != fsz {
continue
}
rn := rln[lx.St:lx.Ed]
var i int
if ignoreCase {
i = runes.IndexFold(rn, fr)
} else {
i = runes.Index(rn, fr)
}
if i < 0 {
continue
}
mat := NewMatch(rln, lx.St, lx.Ed, ln)
matches = append(matches, mat)
cnt++
}
}
return cnt, matches
}
// Search looks for a string (no regexp) from an io.Reader input stream,
// using given case-sensitivity.
// Returns number of occurrences and specific match position list.
// Column positions are in runes.
func Search(reader io.Reader, find []byte, ignoreCase bool) (int, []Match) {
fr := bytes.Runes(find)
fsz := len(fr)
if fsz == 0 {
return 0, nil
}
cnt := 0
var matches []Match
scan := bufio.NewScanner(reader)
ln := 0
for scan.Scan() {
rn := bytes.Runes(scan.Bytes()) // note: temp -- must copy -- convert to runes anyway
sz := len(rn)
ci := 0
for ci < sz {
var i int
if ignoreCase {
i = runes.IndexFold(rn[ci:], fr)
} else {
i = runes.Index(rn[ci:], fr)
}
if i < 0 {
break
}
i += ci
ci = i + fsz
mat := NewMatch(rn, i, ci, ln)
matches = append(matches, mat)
cnt++
}
ln++
}
if err := scan.Err(); err != nil {
// note: we expect: bufio.Scanner: token too long when reading binary files
// not worth printing here. otherwise is very reliable.
// log.Printf("core.FileSearch error: %v\n", err)
}
return cnt, matches
}
// SearchFile looks for a string (no regexp) within a file, in a
// case-sensitive way, returning number of occurrences and specific match
// position list -- column positions are in runes.
func SearchFile(filename string, find []byte, ignoreCase bool) (int, []Match) {
fp, err := os.Open(filename)
if err != nil {
log.Printf("text.SearchFile: open error: %v\n", err)
return 0, nil
}
defer fp.Close()
return Search(fp, find, ignoreCase)
}
// SearchRegexp looks for a string (using regexp) from an io.Reader input stream.
// Returns number of occurrences and specific match position list.
// Column positions are in runes.
func SearchRegexp(reader io.Reader, re *regexp.Regexp) (int, []Match) {
cnt := 0
var matches []Match
scan := bufio.NewScanner(reader)
ln := 0
for scan.Scan() {
b := scan.Bytes() // note: temp -- must copy -- convert to runes anyway
fi := re.FindAllIndex(b, -1)
if fi == nil {
ln++
continue
}
sz := len(b)
ri := make([]int, sz+1) // byte indexes to rune indexes
rn := make([]rune, 0, sz)
for i, w := 0, 0; i < sz; i += w {
r, wd := utf8.DecodeRune(b[i:])
w = wd
ri[i] = len(rn)
rn = append(rn, r)
}
ri[sz] = len(rn)
for _, f := range fi {
st := f[0]
ed := f[1]
mat := NewMatch(rn, ri[st], ri[ed], ln)
matches = append(matches, mat)
cnt++
}
ln++
}
if err := scan.Err(); err != nil {
// note: we expect: bufio.Scanner: token too long when reading binary files
// not worth printing here. otherwise is very reliable.
// log.Printf("core.FileSearch error: %v\n", err)
}
return cnt, matches
}
// SearchFileRegexp looks for a string (using regexp) within a file,
// returning number of occurrences and specific match
// position list -- column positions are in runes.
func SearchFileRegexp(filename string, re *regexp.Regexp) (int, []Match) {
fp, err := os.Open(filename)
if err != nil {
log.Printf("text.SearchFile: open error: %v\n", err)
return 0, nil
}
defer fp.Close()
return SearchRegexp(fp, re)
}
// SearchByteLinesRegexp looks for a regexp within lines of bytes,
// with given case-sensitivity returning number of occurrences
// and specific match position list. Column positions are in runes.
func SearchByteLinesRegexp(src [][]byte, re *regexp.Regexp) (int, []Match) {
cnt := 0
var matches []Match
for ln, b := range src {
fi := re.FindAllIndex(b, -1)
if fi == nil {
continue
}
sz := len(b)
ri := make([]int, sz+1) // byte indexes to rune indexes
rn := make([]rune, 0, sz)
for i, w := 0, 0; i < sz; i += w {
r, wd := utf8.DecodeRune(b[i:])
w = wd
ri[i] = len(rn)
rn = append(rn, r)
}
ri[sz] = len(rn)
for _, f := range fi {
st := f[0]
ed := f[1]
mat := NewMatch(rn, ri[st], ri[ed], ln)
matches = append(matches, mat)
cnt++
}
}
return cnt, matches
}
// Copyright (c) 2020, 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 text
import (
"fmt"
"sync"
"time"
)
// UndoTrace; set to true to get a report of undo actions
var UndoTrace = false
// UndoGroupDelay is the amount of time above which a new group
// is started, for grouping undo events
var UndoGroupDelay = 250 * time.Millisecond
// Undo is the textview.Buf undo manager
type Undo struct {
// if true, saving and using undos is turned off (e.g., inactive buffers)
Off bool
// undo stack of edits
Stack []*Edit
// undo stack of *undo* edits -- added to whenever an Undo is done -- for emacs-style undo
UndoStack []*Edit
// undo position in stack
Pos int
// group counter
Group int
// mutex protecting all updates
Mu sync.Mutex `json:"-" xml:"-"`
}
// NewGroup increments the Group counter so subsequent undos will be grouped separately
func (un *Undo) NewGroup() {
un.Mu.Lock()
un.Group++
un.Mu.Unlock()
}
// Reset clears all undo records
func (un *Undo) Reset() {
un.Pos = 0
un.Group = 0
un.Stack = nil
un.UndoStack = nil
}
// Save saves given edit to undo stack, with current group marker unless timer interval
// exceeds UndoGroupDelay since last item.
func (un *Undo) Save(tbe *Edit) {
if un.Off {
return
}
un.Mu.Lock()
defer un.Mu.Unlock()
if un.Pos < len(un.Stack) {
if UndoTrace {
fmt.Printf("Undo: resetting to pos: %v len was: %v\n", un.Pos, len(un.Stack))
}
un.Stack = un.Stack[:un.Pos]
}
if len(un.Stack) > 0 {
since := tbe.Reg.Since(&un.Stack[len(un.Stack)-1].Reg)
if since > UndoGroupDelay {
un.Group++
if UndoTrace {
fmt.Printf("Undo: incrementing group to: %v since: %v\n", un.Group, since)
}
}
}
tbe.Group = un.Group
if UndoTrace {
fmt.Printf("Undo: save to pos: %v: group: %v\n->\t%v\n", un.Pos, un.Group, string(tbe.ToBytes()))
}
un.Stack = append(un.Stack, tbe)
un.Pos = len(un.Stack)
}
// UndoPop pops the top item off of the stack for use in Undo. returns nil if none.
func (un *Undo) UndoPop() *Edit {
if un.Off {
return nil
}
un.Mu.Lock()
defer un.Mu.Unlock()
if un.Pos == 0 {
return nil
}
un.Pos--
tbe := un.Stack[un.Pos]
if UndoTrace {
fmt.Printf("Undo: UndoPop of Gp: %v pos: %v delete? %v at: %v text: %v\n", un.Group, un.Pos, tbe.Delete, tbe.Reg, string(tbe.ToBytes()))
}
return tbe
}
// UndoPopIfGroup pops the top item off of the stack if it is the same as given group
func (un *Undo) UndoPopIfGroup(gp int) *Edit {
if un.Off {
return nil
}
un.Mu.Lock()
defer un.Mu.Unlock()
if un.Pos == 0 {
return nil
}
tbe := un.Stack[un.Pos-1]
if tbe.Group != gp {
return nil
}
un.Pos--
if UndoTrace {
fmt.Printf("Undo: UndoPopIfGroup of Gp: %v pos: %v delete? %v at: %v text: %v\n", un.Group, un.Pos, tbe.Delete, tbe.Reg, string(tbe.ToBytes()))
}
return tbe
}
// SaveUndo saves given edit to UndoStack (stack of undoes that have have undone..)
// for emacs mode.
func (un *Undo) SaveUndo(tbe *Edit) {
un.UndoStack = append(un.UndoStack, tbe)
}
// UndoStackSave if EmacsUndo mode is active, saves the UndoStack
// to the regular Undo stack, at the end, and moves undo to the very end.
// Undo is a constant stream..
func (un *Undo) UndoStackSave() {
if un.Off {
return
}
un.Mu.Lock()
defer un.Mu.Unlock()
if len(un.UndoStack) == 0 {
return
}
un.Stack = append(un.Stack, un.UndoStack...)
un.Pos = len(un.Stack)
un.UndoStack = nil
if UndoTrace {
fmt.Printf("Undo: undo stack saved to main stack, new pos: %v\n", un.Pos)
}
}
// RedoNext returns the current item on Stack for Redo, and increments the position
// returns nil if at end of stack.
func (un *Undo) RedoNext() *Edit {
if un.Off {
return nil
}
un.Mu.Lock()
defer un.Mu.Unlock()
if un.Pos >= len(un.Stack) {
return nil
}
tbe := un.Stack[un.Pos]
if UndoTrace {
fmt.Printf("Undo: RedoNext of Gp: %v at pos: %v delete? %v at: %v text: %v\n", un.Group, un.Pos, tbe.Delete, tbe.Reg, string(tbe.ToBytes()))
}
un.Pos++
return tbe
}
// RedoNextIfGroup returns the current item on Stack for Redo if it is same group
// and increments the position. returns nil if at end of stack.
func (un *Undo) RedoNextIfGroup(gp int) *Edit {
if un.Off {
return nil
}
un.Mu.Lock()
defer un.Mu.Unlock()
if un.Pos >= len(un.Stack) {
return nil
}
tbe := un.Stack[un.Pos]
if tbe.Group != gp {
return nil
}
if UndoTrace {
fmt.Printf("Undo: RedoNextIfGroup of Gp: %v at pos: %v delete? %v at: %v text: %v\n", un.Group, un.Pos, tbe.Delete, tbe.Reg, string(tbe.ToBytes()))
}
un.Pos++
return tbe
}
// AdjustRegion adjusts given text region for any edits that
// have taken place since time stamp on region (using the Undo stack).
// If region was wholly within a deleted region, then RegionNil will be
// returned -- otherwise it is clipped appropriately as function of deletes.
func (un *Undo) AdjustRegion(reg Region) Region {
if un.Off {
return reg
}
un.Mu.Lock()
defer un.Mu.Unlock()
for _, utbe := range un.Stack {
reg = utbe.AdjustReg(reg)
if reg == RegionNil {
return reg
}
}
return reg
}
// Copyright (c) 2020, 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 text
import (
"bufio"
"bytes"
"io"
"log/slog"
"os"
"strings"
)
// BytesToLineStrings returns []string lines from []byte input.
// If addNewLn is true, each string line has a \n appended at end.
func BytesToLineStrings(txt []byte, addNewLn bool) []string {
lns := bytes.Split(txt, []byte("\n"))
nl := len(lns)
if nl == 0 {
return nil
}
str := make([]string, nl)
for i, l := range lns {
str[i] = string(l)
if addNewLn {
str[i] += "\n"
}
}
return str
}
// StringLinesToByteLines returns [][]byte lines from []string lines
func StringLinesToByteLines(str []string) [][]byte {
nl := len(str)
bl := make([][]byte, nl)
for i, s := range str {
bl[i] = []byte(s)
}
return bl
}
// FileBytes returns the bytes of given file.
func FileBytes(fpath string) ([]byte, error) {
fp, err := os.Open(fpath)
if err != nil {
slog.Error(err.Error())
return nil, err
}
txt, err := io.ReadAll(fp)
fp.Close()
if err != nil {
slog.Error(err.Error())
return nil, err
}
return txt, nil
}
// FileRegionBytes returns the bytes of given file within given
// start / end lines, either of which might be 0 (in which case full file
// is returned).
// If preComments is true, it also automatically includes any comments
// that might exist just prior to the start line if stLn is > 0, going back
// a maximum of lnBack lines.
func FileRegionBytes(fpath string, stLn, edLn int, preComments bool, lnBack int) []byte {
txt, err := FileBytes(fpath)
if err != nil {
return nil
}
if stLn == 0 && edLn == 0 {
return txt
}
lns := bytes.Split(txt, []byte("\n"))
nln := len(lns)
if edLn > 0 && edLn > stLn && edLn < nln {
el := min(edLn+1, nln-1)
lns = lns[:el]
}
if preComments && stLn > 0 && stLn < nln {
comLn, comSt, comEd := KnownComments(fpath)
stLn = PreCommentStart(lns, stLn, comLn, comSt, comEd, lnBack)
}
if stLn > 0 && stLn < len(lns) {
lns = lns[stLn:]
}
txt = bytes.Join(lns, []byte("\n"))
txt = append(txt, '\n')
return txt
}
// PreCommentStart returns the starting line for comment line(s) that just
// precede the given stLn line number within the given lines of bytes,
// using the given line-level and block start / end comment chars.
// returns stLn if nothing found. Only looks back a total of lnBack lines.
func PreCommentStart(lns [][]byte, stLn int, comLn, comSt, comEd string, lnBack int) int {
comLnb := []byte(strings.TrimSpace(comLn))
comStb := []byte(strings.TrimSpace(comSt))
comEdb := []byte(strings.TrimSpace(comEd))
nback := 0
gotEd := false
for i := stLn - 1; i >= 0; i-- {
l := lns[i]
fl := bytes.Fields(l)
if len(fl) == 0 {
stLn = i + 1
break
}
if !gotEd {
for _, ff := range fl {
if bytes.Equal(ff, comEdb) {
gotEd = true
break
}
}
if gotEd {
continue
}
}
if bytes.Equal(fl[0], comStb) {
stLn = i
break
}
if !bytes.Equal(fl[0], comLnb) && !gotEd {
stLn = i + 1
break
}
nback++
if nback > lnBack {
stLn = i
break
}
}
return stLn
}
// CountWordsLinesRegion counts the number of words (aka Fields, space-separated strings)
// and lines in given region of source (lines = 1 + End.Ln - Start.Ln)
func CountWordsLinesRegion(src [][]rune, reg Region) (words, lines int) {
lns := len(src)
mx := min(lns-1, reg.End.Ln)
for ln := reg.Start.Ln; ln <= mx; ln++ {
sln := src[ln]
if ln == reg.Start.Ln {
sln = sln[reg.Start.Ch:]
} else if ln == reg.End.Ln {
sln = sln[:reg.End.Ch]
}
flds := strings.Fields(string(sln))
words += len(flds)
}
lines = 1 + (reg.End.Ln - reg.Start.Ln)
return
}
// CountWordsLines counts the number of words (aka Fields, space-separated strings)
// and lines given io.Reader input
func CountWordsLines(reader io.Reader) (words, lines int) {
scan := bufio.NewScanner(reader)
for scan.Scan() {
flds := bytes.Fields(scan.Bytes())
words += len(flds)
lines++
}
return
}
// Copyright (c) 2020, 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 texteditor
import (
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/tree"
)
// TwinEditors presents two side-by-side [Editor]s in [core.Splits]
// that scroll in sync with each other.
type TwinEditors struct {
core.Splits
// [Buffer] for A
BufferA *Buffer `json:"-" xml:"-"`
// [Buffer] for B
BufferB *Buffer `json:"-" xml:"-"`
inInputEvent bool
}
func (te *TwinEditors) Init() {
te.Splits.Init()
te.BufferA = NewBuffer()
te.BufferB = NewBuffer()
f := func(name string, buf *Buffer) {
tree.AddChildAt(te, name, func(w *Editor) {
w.SetBuffer(buf)
w.Styler(func(s *styles.Style) {
s.Min.X.Ch(80)
s.Min.Y.Em(40)
})
w.On(events.Scroll, func(e events.Event) {
te.syncEditors(events.Scroll, e, name)
})
w.On(events.Input, func(e events.Event) {
te.syncEditors(events.Input, e, name)
})
})
}
f("text-a", te.BufferA)
f("text-b", te.BufferB)
}
// SetFiles sets the files for each [Buffer].
func (te *TwinEditors) SetFiles(fileA, fileB string) {
te.BufferA.Filename = core.Filename(fileA)
te.BufferA.Stat() // update markup
te.BufferB.Filename = core.Filename(fileB)
te.BufferB.Stat() // update markup
}
// syncEditors synchronizes the [Editor] scrolling and cursor positions
func (te *TwinEditors) syncEditors(typ events.Types, e events.Event, name string) {
tva, tvb := te.Editors()
me, other := tva, tvb
if name == "text-b" {
me, other = tvb, tva
}
switch typ {
case events.Scroll:
other.Geom.Scroll.Y = me.Geom.Scroll.Y
other.ScrollUpdateFromGeom(math32.Y)
case events.Input:
if te.inInputEvent {
return
}
te.inInputEvent = true
other.SetCursorShow(me.CursorPos)
te.inInputEvent = false
}
}
// Editors returns the two text [Editor]s.
func (te *TwinEditors) Editors() (*Editor, *Editor) {
ae := te.Child(0).(*Editor)
be := te.Child(1).(*Editor)
return ae, be
}
// Code generated by "core generate"; DO NOT EDIT.
package texteditor
import (
"image"
"io"
"time"
"cogentcore.org/core/paint"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/texteditor.Buffer", IDName: "buffer", Doc: "Buffer is a buffer of text, which can be viewed by [Editor](s).\nIt holds the raw text lines (in original string and rune formats,\nand marked-up from syntax highlighting), and sends signals for making\nedits to the text and coordinating those edits across multiple views.\nEditors always only view a single buffer, so they directly call methods\non the buffer to drive updates, which are then broadcast.\nIt also has methods for loading and saving buffers to files.\nUnlike GUI widgets, its methods generally send events, without an\nexplicit Event suffix.\nInternally, the buffer represents new lines using \\n = LF, but saving\nand loading can deal with Windows/DOS CRLF format.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, 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"}}, {Name: "SaveAs", Doc: "SaveAs saves the current text into given file; does an editDone first to save edits\nand checks for an existing file; if it does exist then prompts to overwrite or not.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename"}}, {Name: "Save", Doc: "Save saves the current text into the current filename associated with this buffer.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Returns: []string{"error"}}}, Embeds: []types.Field{{Name: "Lines"}}, Fields: []types.Field{{Name: "Filename", Doc: "Filename is the filename of the file that was last loaded or saved.\nIt is used when highlighting code."}, {Name: "Autosave", Doc: "Autosave specifies whether the file should be automatically\nsaved after changes are made."}, {Name: "Info", Doc: "Info is the full information about the current file."}, {Name: "LineColors", Doc: "LineColors are the colors to use for rendering circles\nnext to the line numbers of certain lines."}, {Name: "editors", Doc: "editors are the editors that are currently viewing this buffer."}, {Name: "posHistory", Doc: "posHistory is the history of cursor positions.\nIt can be used to move back through them."}, {Name: "Complete", Doc: "Complete is the functions and data for text completion."}, {Name: "spell", Doc: "spell is the functions and data for spelling correction."}, {Name: "currentEditor", Doc: "currentEditor is the current text editor, such as the one that initiated the\nComplete or Correct process. The cursor position in this view is updated, and\nit is reset to nil after usage."}, {Name: "listeners", Doc: "listeners is used for sending standard system events.\nChange is sent for BufferDone, BufferInsert, and BufferDelete."}, {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/texteditor.DiffEditor", IDName: "diff-editor", Doc: "DiffEditor presents two side-by-side [Editor]s showing the differences\nbetween two files (represented as lines of strings).", Methods: []types.Method{{Name: "saveFileA", Doc: "saveFileA saves the current state of file A to given filename", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"fname"}}, {Name: "saveFileB", Doc: "saveFileB saves the current state of file B to given filename", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"fname"}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "FileA", Doc: "first file name being compared"}, {Name: "FileB", Doc: "second file name being compared"}, {Name: "RevisionA", Doc: "revision for first file, if relevant"}, {Name: "RevisionB", Doc: "revision for second file, if relevant"}, {Name: "bufferA", Doc: "[Buffer] for A showing the aligned edit view"}, {Name: "bufferB", Doc: "[Buffer] for B showing the aligned edit view"}, {Name: "alignD", Doc: "aligned diffs records diff for aligned lines"}, {Name: "diffs", Doc: "diffs applied"}, {Name: "inInputEvent"}}})
// NewDiffEditor returns a new [DiffEditor] with the given optional parent:
// DiffEditor presents two side-by-side [Editor]s showing the differences
// between two files (represented as lines of strings).
func NewDiffEditor(parent ...tree.Node) *DiffEditor { return tree.New[DiffEditor](parent...) }
// SetFileA sets the [DiffEditor.FileA]:
// first file name being compared
func (t *DiffEditor) SetFileA(v string) *DiffEditor { t.FileA = v; return t }
// SetFileB sets the [DiffEditor.FileB]:
// second file name being compared
func (t *DiffEditor) SetFileB(v string) *DiffEditor { t.FileB = v; return t }
// SetRevisionA sets the [DiffEditor.RevisionA]:
// revision for first file, if relevant
func (t *DiffEditor) SetRevisionA(v string) *DiffEditor { t.RevisionA = v; return t }
// SetRevisionB sets the [DiffEditor.RevisionB]:
// revision for second file, if relevant
func (t *DiffEditor) SetRevisionB(v string) *DiffEditor { t.RevisionB = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/texteditor.DiffTextEditor", IDName: "diff-text-editor", Doc: "DiffTextEditor supports double-click based application of edits from one\nbuffer to the other.", Embeds: []types.Field{{Name: "Editor"}}})
// NewDiffTextEditor returns a new [DiffTextEditor] with the given optional parent:
// DiffTextEditor supports double-click based application of edits from one
// buffer to the other.
func NewDiffTextEditor(parent ...tree.Node) *DiffTextEditor {
return tree.New[DiffTextEditor](parent...)
}
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/texteditor.Editor", IDName: "editor", Doc: "Editor is a widget for editing multiple lines of complicated text (as compared to\n[core.TextField] for a single line of simple text). The Editor is driven by a [Buffer]\nbuffer which contains all the text, and manages all the edits,\nsending update events out to the editors.\n\nUse NeedsRender to drive an render update for any change that does\nnot change the line-level layout of the text.\nUse NeedsLayout whenever there are changes across lines that require\nre-layout of the text. This sets the Widget NeedsRender flag and triggers\nlayout during that render.\n\nMultiple editors can be attached to a given buffer. All updating in the\nEditor should be within a single goroutine, as it would require\nextensive protections throughout code otherwise.", Directives: []types.Directive{{Tool: "core", Directive: "embedder"}}, Methods: []types.Method{{Name: "Lookup", Doc: "Lookup attempts to lookup symbol at current location, popping up a window\nif something is found.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Embeds: []types.Field{{Name: "Frame"}}, Fields: []types.Field{{Name: "Buffer", Doc: "Buffer is the text buffer being edited."}, {Name: "CursorWidth", Doc: "CursorWidth is the width of the cursor.\nThis should be set in Stylers like all other style properties."}, {Name: "LineNumberColor", Doc: "LineNumberColor is the color used for the side bar containing the line numbers.\nThis should be set in Stylers like all other style properties."}, {Name: "SelectColor", Doc: "SelectColor is the color used for the user text selection background color.\nThis should be set in Stylers like all other style properties."}, {Name: "HighlightColor", Doc: "HighlightColor is the color used for the text highlight background color (like in find).\nThis should be set in Stylers like all other style properties."}, {Name: "CursorColor", Doc: "CursorColor is the color used for the text editor cursor bar.\nThis should be set in Stylers like all other style properties."}, {Name: "NumLines", Doc: "NumLines is the number of lines in the view, synced with the [Buffer] after edits,\nbut always reflects the storage size of renders etc."}, {Name: "renders", Doc: "renders is a slice of paint.Text representing the renders of the text lines,\nwith one render per line (each line could visibly wrap-around, so these are logical lines, not display lines)."}, {Name: "offsets", Doc: "offsets is a slice of float32 representing the starting render offsets for the top of each line."}, {Name: "lineNumberDigits", Doc: "lineNumberDigits is the number of line number digits needed."}, {Name: "LineNumberOffset", Doc: "LineNumberOffset is the horizontal offset for the start of text after line numbers."}, {Name: "lineNumberRender", Doc: "lineNumberRender is the render for line numbers."}, {Name: "CursorPos", Doc: "CursorPos is the current cursor position."}, {Name: "cursorTarget", Doc: "cursorTarget is the target cursor position for externally set targets.\nIt ensures that the target position is visible."}, {Name: "cursorColumn", Doc: "cursorColumn is the desired cursor column, where the cursor was last when moved using left / right arrows.\nIt is used when doing up / down to not always go to short line columns."}, {Name: "posHistoryIndex", Doc: "posHistoryIndex is the current index within PosHistory."}, {Name: "selectStart", Doc: "selectStart is the starting point for selection, which will either be the start or end of selected region\ndepending on subsequent selection."}, {Name: "SelectRegion", Doc: "SelectRegion is the current selection region."}, {Name: "previousSelectRegion", Doc: "previousSelectRegion is the previous selection region that was actually rendered.\nIt is needed to update the render."}, {Name: "Highlights", Doc: "Highlights is a slice of regions representing the highlighted regions, e.g., for search results."}, {Name: "scopelights", Doc: "scopelights is a slice of regions representing the highlighted regions specific to scope markers."}, {Name: "LinkHandler", Doc: "LinkHandler handles link clicks.\nIf it is nil, they are sent to the standard web URL handler."}, {Name: "ISearch", Doc: "ISearch is the interactive search data."}, {Name: "QReplace", Doc: "QReplace is the query replace data."}, {Name: "selectMode", Doc: "selectMode is a boolean indicating whether to select text as the cursor moves."}, {Name: "fontHeight", Doc: "fontHeight is the font height, cached during styling."}, {Name: "lineHeight", Doc: "lineHeight is the line height, cached during styling."}, {Name: "fontAscent", Doc: "fontAscent is the font ascent, cached during styling."}, {Name: "fontDescent", Doc: "fontDescent is the font descent, cached during styling."}, {Name: "nLinesChars", Doc: "nLinesChars is the height in lines and width in chars of the visible area."}, {Name: "linesSize", Doc: "linesSize is the total size of all lines as rendered."}, {Name: "totalSize", Doc: "totalSize is the LinesSize plus extra space and line numbers etc."}, {Name: "lineLayoutSize", Doc: "lineLayoutSize is the Geom.Size.Actual.Total subtracting extra space and line numbers.\nThis is what LayoutStdLR sees for laying out each line."}, {Name: "lastlineLayoutSize", Doc: "lastlineLayoutSize is the last LineLayoutSize used in laying out lines.\nIt is used to trigger a new layout only when needed."}, {Name: "blinkOn", Doc: "blinkOn oscillates between on and off for blinking."}, {Name: "cursorMu", Doc: "cursorMu is a mutex protecting cursor rendering, shared between blink and main code."}, {Name: "hasLinks", Doc: "hasLinks is a boolean indicating if at least one of the renders has links.\nIt determines if we set the cursor for hand movements."}, {Name: "hasLineNumbers", Doc: "hasLineNumbers indicates that this editor has line numbers\n(per [Buffer] option)"}, {Name: "needsLayout", Doc: "needsLayout is set by NeedsLayout: Editor does significant\ninternal layout in LayoutAllLines, and its layout is simply based\non what it gets allocated, so it does not affect the rest\nof the Scene."}, {Name: "lastWasTabAI", Doc: "lastWasTabAI indicates that last key was a Tab auto-indent"}, {Name: "lastWasUndo", Doc: "lastWasUndo indicates that last key was an undo"}, {Name: "targetSet", Doc: "targetSet indicates that the CursorTarget is set"}, {Name: "lastRecenter"}, {Name: "lastAutoInsert"}, {Name: "lastFilename"}}})
// NewEditor returns a new [Editor] with the given optional parent:
// Editor is a widget for editing multiple lines of complicated text (as compared to
// [core.TextField] for a single line of simple text). The Editor is driven by a [Buffer]
// buffer which contains all the text, and manages all the edits,
// sending update events out to the editors.
//
// Use NeedsRender to drive an render update for any change that does
// not change the line-level layout of the text.
// Use NeedsLayout whenever there are changes across lines that require
// re-layout of the text. This sets the Widget NeedsRender flag and triggers
// layout during that render.
//
// Multiple editors can be attached to a given buffer. All updating in the
// Editor should be within a single goroutine, as it would require
// extensive protections throughout code otherwise.
func NewEditor(parent ...tree.Node) *Editor { return tree.New[Editor](parent...) }
// EditorEmbedder is an interface that all types that embed Editor satisfy
type EditorEmbedder interface {
AsEditor() *Editor
}
// AsEditor returns the given value as a value of type Editor if the type
// of the given value embeds Editor, or nil otherwise
func AsEditor(n tree.Node) *Editor {
if t, ok := n.(EditorEmbedder); ok {
return t.AsEditor()
}
return nil
}
// AsEditor satisfies the [EditorEmbedder] interface
func (t *Editor) AsEditor() *Editor { return t }
// SetCursorWidth sets the [Editor.CursorWidth]:
// CursorWidth is the width of the cursor.
// This should be set in Stylers like all other style properties.
func (t *Editor) SetCursorWidth(v units.Value) *Editor { t.CursorWidth = v; return t }
// SetLineNumberColor sets the [Editor.LineNumberColor]:
// LineNumberColor is the color used for the side bar containing the line numbers.
// This should be set in Stylers like all other style properties.
func (t *Editor) SetLineNumberColor(v image.Image) *Editor { t.LineNumberColor = v; return t }
// SetSelectColor sets the [Editor.SelectColor]:
// SelectColor is the color used for the user text selection background color.
// This should be set in Stylers like all other style properties.
func (t *Editor) SetSelectColor(v image.Image) *Editor { t.SelectColor = v; return t }
// SetHighlightColor sets the [Editor.HighlightColor]:
// HighlightColor is the color used for the text highlight background color (like in find).
// This should be set in Stylers like all other style properties.
func (t *Editor) SetHighlightColor(v image.Image) *Editor { t.HighlightColor = v; return t }
// SetCursorColor sets the [Editor.CursorColor]:
// CursorColor is the color used for the text editor cursor bar.
// This should be set in Stylers like all other style properties.
func (t *Editor) SetCursorColor(v image.Image) *Editor { t.CursorColor = v; return t }
// SetLinkHandler sets the [Editor.LinkHandler]:
// LinkHandler handles link clicks.
// If it is nil, they are sent to the standard web URL handler.
func (t *Editor) SetLinkHandler(v func(tl *paint.TextLink)) *Editor { t.LinkHandler = v; return t }
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/texteditor.OutputBuffer", IDName: "output-buffer", Doc: "OutputBuffer is a [Buffer] that records the output from an [io.Reader] using\n[bufio.Scanner]. It is optimized to combine fast chunks of output into\nlarge blocks of updating. It also supports an arbitrary markup function\nthat operates on each line of output bytes.", Directives: []types.Directive{{Tool: "types", Directive: "add", Args: []string{"-setters"}}}, Fields: []types.Field{{Name: "Output", Doc: "the output that we are reading from, as an io.Reader"}, {Name: "Buffer", Doc: "the [Buffer] that we output to"}, {Name: "Batch", Doc: "how much time to wait while batching output (default: 200ms)"}, {Name: "MarkupFunc", Doc: "optional markup function that adds html tags to given line of output -- essential that it ONLY adds tags, and otherwise has the exact same visible bytes as the input"}, {Name: "currentOutputLines", Doc: "current buffered output raw lines, which are not yet sent to the Buffer"}, {Name: "currentOutputMarkupLines", Doc: "current buffered output markup lines, which are not yet sent to the Buffer"}, {Name: "mu", Doc: "mutex protecting updating of CurrentOutputLines and Buffer, and timer"}, {Name: "lastOutput", Doc: "time when last output was sent to buffer"}, {Name: "afterTimer", Doc: "time.AfterFunc that is started after new input is received and not immediately output -- ensures that it will get output if no further burst happens"}}})
// SetOutput sets the [OutputBuffer.Output]:
// the output that we are reading from, as an io.Reader
func (t *OutputBuffer) SetOutput(v io.Reader) *OutputBuffer { t.Output = v; return t }
// SetBuffer sets the [OutputBuffer.Buffer]:
// the [Buffer] that we output to
func (t *OutputBuffer) SetBuffer(v *Buffer) *OutputBuffer { t.Buffer = v; return t }
// SetBatch sets the [OutputBuffer.Batch]:
// how much time to wait while batching output (default: 200ms)
func (t *OutputBuffer) SetBatch(v time.Duration) *OutputBuffer { t.Batch = v; return t }
// SetMarkupFunc sets the [OutputBuffer.MarkupFunc]:
// optional markup function that adds html tags to given line of output -- essential that it ONLY adds tags, and otherwise has the exact same visible bytes as the input
func (t *OutputBuffer) SetMarkupFunc(v OutputBufferMarkupFunc) *OutputBuffer {
t.MarkupFunc = v
return t
}
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/texteditor.TwinEditors", IDName: "twin-editors", Doc: "TwinEditors presents two side-by-side [Editor]s in [core.Splits]\nthat scroll in sync with each other.", Embeds: []types.Field{{Name: "Splits"}}, Fields: []types.Field{{Name: "BufferA", Doc: "[Buffer] for A"}, {Name: "BufferB", Doc: "[Buffer] for B"}, {Name: "inInputEvent"}}})
// NewTwinEditors returns a new [TwinEditors] with the given optional parent:
// TwinEditors presents two side-by-side [Editor]s in [core.Splits]
// that scroll in sync with each other.
func NewTwinEditors(parent ...tree.Node) *TwinEditors { return tree.New[TwinEditors](parent...) }
// SetBufferA sets the [TwinEditors.BufferA]:
// [Buffer] for A
func (t *TwinEditors) SetBufferA(v *Buffer) *TwinEditors { t.BufferA = v; return t }
// SetBufferB sets the [TwinEditors.BufferB]:
// [Buffer] for B
func (t *TwinEditors) SetBufferB(v *Buffer) *TwinEditors { t.BufferB = v; return t }
// 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 tree
import (
"reflect"
"slices"
"strconv"
"sync/atomic"
"cogentcore.org/core/base/slicesx"
"cogentcore.org/core/types"
)
// admin.go has infrastructure code outside of the [Node] interface.
// New returns a new node of the given type with the given optional parent.
// If the name is unspecified, it defaults to the ID (kebab-case) name of
// the type, plus the [Node.NumLifetimeChildren] of the parent.
func New[T NodeValue](parent ...Node) *T { //yaegi:add
n := new(T)
ni := any(n).(Node)
InitNode(ni)
if len(parent) == 0 {
ni.AsTree().SetName(ni.AsTree().NodeType().IDName)
return n
}
p := parent[0]
p.AsTree().Children = append(p.AsTree().Children, ni)
SetParent(ni, p)
return n
}
// NewOfType returns a new node of the given [types.Type] with the given optional parent.
// If the name is unspecified, it defaults to the ID (kebab-case) name of
// the type, plus the [Node.NumLifetimeChildren] of the parent.
func NewOfType(typ *types.Type, parent ...Node) Node {
if len(parent) == 0 {
n := newOfType(typ)
InitNode(n)
n.AsTree().SetName(n.AsTree().NodeType().IDName)
return n
}
return parent[0].AsTree().NewChild(typ)
}
// InitNode initializes the node. It should not be called by end-user code.
// It must be exported since it is referenced in generic functions included in yaegi.
func InitNode(n Node) {
nb := n.AsTree()
if nb.This != n {
nb.This = n
nb.This.Init()
}
}
// SetParent sets the parent of the given node to the given parent node.
// This is only for nodes with no existing parent; see [MoveToParent] to
// move nodes that already have a parent. It does not add the node to the
// parent's list of children; see [Node.AddChild] for a version that does.
// It automatically gets the [Node.This] of the parent.
func SetParent(child Node, parent Node) {
nb := child.AsTree()
nb.Parent = parent.AsTree().This
setUniqueName(child, false)
child.AsTree().This.OnAdd()
if oca := nb.Parent.AsTree().OnChildAdded; oca != nil {
oca(child)
}
}
// MoveToParent removes the given node from its current parent
// and adds it as a child of the given new parent.
// The old and new parents can be in different trees (or not).
func MoveToParent(child Node, parent Node) {
oldParent := child.AsTree().Parent
if oldParent != nil {
idx := IndexOf(oldParent.AsTree().Children, child)
if idx >= 0 {
oldParent.AsTree().Children = slices.Delete(oldParent.AsTree().Children, idx, idx+1)
}
}
parent.AsTree().AddChild(child)
}
// ParentByType returns the first parent of the given node that is
// of the given type, if any such node exists.
func ParentByType[T Node](n Node) T {
if IsRoot(n) {
var z T
return z
}
if p, ok := n.AsTree().Parent.(T); ok {
return p
}
return ParentByType[T](n.AsTree().Parent)
}
// ChildByType returns the first child of the given node that is
// of the given type, if any such node exists.
func ChildByType[T Node](n Node, startIndex ...int) T {
nb := n.AsTree()
idx := slicesx.Search(nb.Children, func(ch Node) bool {
_, ok := ch.(T)
return ok
}, startIndex...)
ch := nb.Child(idx)
if ch == nil {
var z T
return z
}
return ch.(T)
}
// IsRoot returns whether the given node is the root node in its tree.
func IsRoot(n Node) bool {
return n.AsTree().Parent == nil
}
// Root returns the root node of the given node's tree.
func Root(n Node) Node {
if IsRoot(n) {
return n
}
return Root(n.AsTree().Parent)
}
// nodeType is the [reflect.Type] of [Node].
var nodeType = reflect.TypeFor[Node]()
// IsNode returns whether the given type or a pointer to it
// implements the [Node] interface.
func IsNode(typ reflect.Type) bool {
if typ == nil {
return false
}
return typ.Implements(nodeType) || reflect.PointerTo(typ).Implements(nodeType)
}
// newOfType returns a new instance of the given [Node] type.
func newOfType(typ *types.Type) Node {
return reflect.New(reflect.TypeOf(typ.Instance).Elem()).Interface().(Node)
}
// SetUniqueName sets the name of the node to be unique, using
// the number of lifetime children of the parent node as a unique
// identifier. If the node already has a name, it adds the unique id
// to it. Otherwise, it uses the type name of the node plus the unique id.
func SetUniqueName(n Node) {
setUniqueName(n, true)
}
// setUniqueName is the implementation of [SetUniqueName] that takes whether
// to add the unique id to the name even if it is already set.
func setUniqueName(n Node, addIfSet bool) {
nb := n.AsTree()
pn := nb.Parent
if pn == nil {
return
}
c := atomic.AddUint64(&pn.AsTree().numLifetimeChildren, 1)
id := "-" + strconv.FormatUint(c-1, 10) // must subtract 1 so we start at 0
if nb.Name == "" {
nb.SetName(nb.NodeType().IDName + id)
} else if addIfSet {
nb.SetName(nb.Name + id)
}
}
// 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 tree
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"slices"
"strconv"
"strings"
"cogentcore.org/core/base/reflectx"
"cogentcore.org/core/types"
)
// MarshalJSON marshals the node by injecting the [Node.NodeType] as a nodeType
// field and the [NodeBase.NumChildren] as a numChildren field at the start of
// the standard JSON encoding output.
func (n *NodeBase) MarshalJSON() ([]byte, error) {
// the non pointer value does not implement MarshalJSON, so it will not result in infinite recursion
b, err := json.Marshal(reflectx.Underlying(reflect.ValueOf(n.This)).Interface())
if err != nil {
return b, err
}
data := `"nodeType":"` + n.NodeType().Name + `",`
if n.NumChildren() > 0 {
data += `"numChildren":` + strconv.Itoa(n.NumChildren()) + ","
}
b = slices.Insert(b, 1, []byte(data)...)
return b, nil
}
// unmarshalTypeCache is a cache of [reflect.Type] values used
// for unmarshalling in [NodeBase.UnmarshalJSON]. This cache has
// a noticeable performance benefit of around 1.2x in
// [BenchmarkNodeUnmarshalJSON], a benefit that should only increase
// for larger trees.
var unmarshalTypeCache = map[string]reflect.Type{}
// UnmarshalJSON unmarshals the node by extracting the nodeType and numChildren fields
// added by [NodeBase.MarshalJSON] and then updating the node to the correct type and
// creating the correct number of children. Note that this method can not update the type
// of the node if it has no parent; to load a root node from JSON and have it be of the
// correct type, see the [UnmarshalRootJSON] function. If the type of the node is changed
// by this function, the node pointer will no longer be valid, and the node must be fetched
// again through the children of its parent. You do not need to call [UnmarshalRootJSON]
// or worry about pointers changing if this node is already of the correct type.
func (n *NodeBase) UnmarshalJSON(b []byte) error {
typeStart := bytes.Index(b, []byte(`":`)) + 3
typeEnd := bytes.Index(b, []byte(`",`))
typeName := string(b[typeStart:typeEnd])
// we may end up with an extraneous quote / space at the start
typeName = strings.TrimPrefix(strings.TrimSpace(typeName), `"`)
typ := types.TypeByName(typeName)
if typ == nil {
return fmt.Errorf("tree.NodeBase.UnmarshalJSON: type %q not found", typeName)
}
// if our type does not match, we must replace our This to make it match
if n.NodeType() != typ {
parent := n.Parent
index := n.IndexInParent()
if index >= 0 {
n.Delete()
n.This = NewOfType(typ)
parent.AsTree().InsertChild(n.This, index)
n = n.This.AsTree() // our NodeBase pointer is now different
}
}
// We must delete any existing children first.
n.DeleteChildren()
remainder := b[typeEnd+2:]
numStart := bytes.Index(remainder, []byte(`"numChildren":`))
if numStart >= 0 { // numChildren may not be specified if it is 0
numStart += 14 // start of actual number bytes
numEnd := bytes.Index(remainder, []byte(`,`))
numString := string(remainder[numStart:numEnd])
// we may end up with extraneous space at the start
numString = strings.TrimSpace(numString)
numChildren, err := strconv.Atoi(numString)
if err != nil {
return err
}
// We make placeholder NodeBase children that will be replaced
// with children of the correct type during their UnmarshalJSON.
for range numChildren {
New[NodeBase](n)
}
}
uv := reflectx.UnderlyingPointer(reflect.ValueOf(n.This))
rtyp := unmarshalTypeCache[typeName]
if rtyp == nil {
// We must create a new type that has the exact same fields as the original type
// so that we can unmarshal into it without having infinite recursion on the
// UnmarshalJSON method. This works because [reflect.StructOf] does not promote
// methods on embedded fields, meaning that the UnmarshalJSON method on the NodeBase
// is not carried over and thus is not called, avoiding infinite recursion.
uvt := uv.Type().Elem()
fields := make([]reflect.StructField, uvt.NumField())
for i := range fields {
fields[i] = uvt.Field(i)
}
nt := reflect.StructOf(fields)
rtyp = reflect.PointerTo(nt)
unmarshalTypeCache[typeName] = rtyp
}
// We can directly convert because our new struct type has the exact same fields.
uvi := uv.Convert(rtyp).Interface()
err := json.Unmarshal(b, uvi)
if err != nil {
return err
}
return nil
}
// UnmarshalRootJSON loads the given JSON to produce a new root node of
// the correct type with all properties and children loaded. If you have
// a root node that you know is already of the correct type, you can just
// call [NodeBase.UnmarshalJSON] on it instead.
func UnmarshalRootJSON(b []byte) (Node, error) {
// we must make a temporary parent so that the type of the node can be updated
parent := New[NodeBase]()
// this NodeBase type is just temporary and will be fixed by [NodeBase.UnmarshalJSON]
nb := New[NodeBase](parent)
err := nb.UnmarshalJSON(b)
if err != nil {
return nil, err
}
// the node must be fetched from the parent's children since the pointer may have changed
n := parent.Child(0)
// we must safely remove the node from its temporary parent
n.AsTree().Parent = nil
parent.Children = nil
parent.Destroy()
return n, nil
}
// 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 tree provides a powerful and extensible tree system,
// centered on the core [Node] interface.
package tree
//go:generate core generate
//go:generate core generate ./testdata
import (
"cogentcore.org/core/base/plan"
)
// Node is an interface that all tree nodes satisfy. The core functionality
// of a tree node is defined on [NodeBase], and all higher-level tree types
// must embed it. This interface only contains the tree functionality that
// higher-level tree types may need to override. You can call [Node.AsTree]
// to get the [NodeBase] of a Node and access the core tree functionality.
// All values that implement [Node] are pointer values; see [NodeValue]
// for an interface for non-pointer values.
type Node interface {
// AsTree returns the [NodeBase] of this Node. Most core
// tree functionality is implemented on [NodeBase].
AsTree() *NodeBase
// Init is called when the node is first initialized.
// It is called before the node is added to the tree,
// so it will not have any parents or siblings.
// It will be called only once in the lifetime of the node.
// It does nothing by default, but it can be implemented
// by higher-level types that want to do something.
// It is the main place that initialization steps should
// be done, like adding Stylers, Makers, and event handlers
// to widgets in Cogent Core.
Init()
// OnAdd is called when the node is added to a parent.
// It will be called only once in the lifetime of the node,
// unless the node is moved. It will not be called on root
// nodes, as they are never added to a parent.
// It does nothing by default, but it can be implemented
// by higher-level types that want to do something.
OnAdd()
// Destroy recursively deletes and destroys the node, all of its children,
// and all of its children's children, etc. Node types can implement this
// to do additional necessary destruction; if they do, they should call
// [NodeBase.Destroy] at the end of their implementation.
Destroy()
// NodeWalkDown is a method that nodes can implement to traverse additional nodes
// like widget parts during [NodeBase.WalkDown]. It is called with the function passed
// to [Node.WalkDown] after the function is called with the node itself.
NodeWalkDown(fun func(n Node) bool)
// CopyFieldsFrom copies the fields of the node from the given node.
// By default, it is [NodeBase.CopyFieldsFrom], which automatically does
// a deep copy of all of the fields of the node that do not a have a
// `copier:"-"` struct tag. Node types should only implement a custom
// CopyFieldsFrom method when they have fields that need special copying
// logic that can not be automatically handled. All custom CopyFieldsFrom
// methods should call [NodeBase.CopyFieldsFrom] first and then only do manual
// handling of specific fields that can not be automatically copied. See
// [cogentcore.org/core/core.WidgetBase.CopyFieldsFrom] for an example of a
// custom CopyFieldsFrom method.
CopyFieldsFrom(from Node)
// This is necessary for tree planning to work.
plan.Namer
}
// NodeValue is an interface that all non-pointer tree nodes satisfy.
// Pointer tree nodes satisfy [Node], not NodeValue. NodeValue and [Node]
// are mutually exclusive; a [Node] cannot be a NodeValue and vice versa.
// However, a pointer to a NodeValue type is guaranteed to be a [Node],
// and a non-pointer version of a [Node] type is guaranteed to be a NodeValue.
type NodeValue interface {
// NodeValue should only be implemented by [NodeBase],
// and it should not be called. It must be exported due
// to a nuance with the way that [reflect.StructOf] works,
// which results in panics with embedded structs that have
// unexported non-pointer methods.
NodeValue()
}
// NodeValue implements [NodeValue]. It should not be called.
func (nb NodeBase) NodeValue() {}
// 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 tree
import (
"log/slog"
"maps"
"reflect"
"slices"
"strconv"
"strings"
"github.com/jinzhu/copier"
"cogentcore.org/core/base/elide"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/base/tiered"
"cogentcore.org/core/types"
)
// NodeBase implements the [Node] interface and provides the core functionality
// for the Cogent Core tree system. You must use NodeBase as an embedded struct
// in all higher-level tree types.
//
// All nodes must be properly initialized by using one of [New], [NodeBase.NewChild],
// [NodeBase.AddChild], [NodeBase.InsertChild], [NodeBase.Clone], [Update], or [Plan].
// This ensures that the [NodeBase.This] field is set correctly and the [Node.Init]
// method is called.
//
// All nodes support JSON marshalling and unmarshalling through the standard [encoding/json]
// interfaces, so you can use the standard functions for loading and saving trees. However,
// if you want to load a root node of the correct type from JSON, you need to use the
// [UnmarshalRootJSON] function.
//
// All node types must be added to the Cogent Core type registry via typegen,
// so you must add a go:generate line that runs `core generate` to any packages
// you write that have new node types defined.
type NodeBase struct {
// Name is the name of this node, which is typically unique relative to other children of
// the same parent. It can be used for finding and serializing nodes. If not otherwise set,
// it defaults to the ID (kebab-case) name of the node type combined with the total number
// of children that have ever been added to the node's parent.
Name string `copier:"-"`
// This is the value of this Node as its true underlying type. This allows methods
// defined on base types to call methods defined on higher-level types, which
// is necessary for various parts of tree and widget functionality. This is set
// to nil when the node is deleted.
This Node `copier:"-" json:"-" xml:"-" display:"-" set:"-"`
// Parent is the parent of this node, which is set automatically when this node is
// added as a child of a parent. To change the parent of a node, use [MoveToParent];
// you should typically not set this field directly. Nodes can only have one parent
// at a time.
Parent Node `copier:"-" json:"-" xml:"-" display:"-" set:"-"`
// Children is the list of children of this node. All of them are set to have this node
// as their parent. You can directly modify this list, but you should typically use the
// various NodeBase child helper functions when applicable so that everything is updated
// properly, such as when deleting children.
Children []Node `table:"-" copier:"-" set:"-" json:",omitempty"`
// Properties is a property map for arbitrary key-value properties.
// When possible, use typed fields on a new type embedding NodeBase instead of this.
// You should typically use the [NodeBase.SetProperty], [NodeBase.Property], and
// [NodeBase.DeleteProperty] methods for modifying and accessing properties.
Properties map[string]any `table:"-" xml:"-" copier:"-" set:"-" json:",omitempty"`
// Updaters is a tiered set of functions called in sequential descending (reverse) order
// in [NodeBase.RunUpdaters] to update the node. You can use [NodeBase.Updater],
// [NodeBase.FirstUpdater], or [NodeBase.FinalUpdater] to add one. This typically
// typically contains [NodeBase.UpdateFromMake] at the start of the normal list.
Updaters tiered.Tiered[[]func()] `copier:"-" json:"-" xml:"-" set:"-" edit:"-" display:"add-fields"`
// Makers is a tiered set of functions called in sequential ascending order
// in [NodeBase.Make] to make the plan for how the node's children should
// be configured. You can use [NodeBase.Maker], [NodeBase.FirstMaker], or
// [NodeBase.FinalMaker] to add one.
Makers tiered.Tiered[[]func(p *Plan)] `copier:"-" json:"-" xml:"-" set:"-" edit:"-" display:"add-fields"`
// OnChildAdded is called when a node is added as a direct child of this node.
// When a node is added to a parent, it calls [Node.OnAdd] on itself and then
// this function on its parent if it is non-nil.
OnChildAdded func(n Node) `copier:"-" json:"-" xml:"-" edit:"-"`
// numLifetimeChildren is the number of children that have ever been added to this
// node, which is used for automatic unique naming.
numLifetimeChildren uint64
// index is the last value of our index, which is used as a starting point for
// finding us in our parent next time. It is not guaranteed to be accurate;
// use the [NodeBase.IndexInParent] method.
index int
// depth is the depth of the node while using [NodeBase.WalkDownBreadth].
depth int
}
// String implements the [fmt.Stringer] interface by returning the path of the node.
func (n *NodeBase) String() string {
if n == nil || n.This == nil {
return "nil"
}
return elide.Middle(n.Path(), 38)
}
// AsTree returns the [NodeBase] for this Node.
func (n *NodeBase) AsTree() *NodeBase {
return n
}
// PlanName implements [plan.Namer].
func (n *NodeBase) PlanName() string {
return n.Name
}
// NodeType returns the [types.Type] of this node.
// If there is no [types.Type] registered for this node already,
// it registers one and then returns it.
func (n *NodeBase) NodeType() *types.Type {
if t := types.TypeByValue(n.This); t != nil {
if t.Instance == nil {
t.Instance = n.NewInstance()
}
return t
}
name := types.TypeNameValue(n.This)
li := strings.LastIndex(name, ".")
return types.AddType(&types.Type{
Name: name,
IDName: strcase.ToKebab(name[li+1:]),
Instance: n.NewInstance(),
})
}
// NewInstance returns a new instance of this node type.
func (n *NodeBase) NewInstance() Node {
return reflect.New(reflect.TypeOf(n.This).Elem()).Interface().(Node)
}
// Parents:
// IndexInParent returns our index within our parent node. It caches the
// last value and uses that for an optimized search so subsequent calls
// are typically quite fast. Returns -1 if we don't have a parent.
func (n *NodeBase) IndexInParent() int {
if n.Parent == nil {
return -1
}
idx := IndexOf(n.Parent.AsTree().Children, n.This, n.index) // very fast if index is close
n.index = idx
return idx
}
// ParentLevel finds a given potential parent node recursively up the
// hierarchy, returning the level above the current node that the parent was
// found, and -1 if not found.
func (n *NodeBase) ParentLevel(parent Node) int {
parLev := -1
level := 0
n.WalkUpParent(func(k Node) bool {
if k == parent {
parLev = level
return Break
}
level++
return Continue
})
return parLev
}
// ParentByName finds first parent recursively up hierarchy that matches
// the given name. It returns nil if not found.
func (n *NodeBase) ParentByName(name string) Node {
if IsRoot(n) {
return nil
}
if n.Parent.AsTree().Name == name {
return n.Parent
}
return n.Parent.AsTree().ParentByName(name)
}
// Children:
// HasChildren returns whether this node has any children.
func (n *NodeBase) HasChildren() bool {
return len(n.Children) > 0
}
// NumChildren returns the number of children this node has.
func (n *NodeBase) NumChildren() int {
return len(n.Children)
}
// Child returns the child of this node at the given index and returns nil if
// the index is out of range.
func (n *NodeBase) Child(i int) Node {
if i >= len(n.Children) || i < 0 {
return nil
}
return n.Children[i]
}
// ChildByName returns the first child that has the given name, and nil
// if no such element is found. startIndex arg allows for optimized
// bidirectional find if you have an idea where it might be, which
// can be a key speedup for large lists. If no value is specified for
// startIndex, it starts in the middle, which is a good default.
func (n *NodeBase) ChildByName(name string, startIndex ...int) Node {
return n.Child(IndexByName(n.Children, name, startIndex...))
}
// Paths:
// TODO: is this the best way to escape paths?
// EscapePathName returns a name that replaces any / with \\
func EscapePathName(name string) string {
return strings.ReplaceAll(name, "/", `\\`)
}
// UnescapePathName returns a name that replaces any \\ with /
func UnescapePathName(name string) string {
return strings.ReplaceAll(name, `\\`, "/")
}
// Path returns the path to this node from the tree root,
// using [Node.Name]s separated by / delimeters. Any
// existing / characters in names are escaped to \\
func (n *NodeBase) Path() string {
if n.Parent != nil {
return n.Parent.AsTree().Path() + "/" + EscapePathName(n.Name)
}
return "/" + EscapePathName(n.Name)
}
// PathFrom returns the path to this node from the given parent node,
// using [Node.Name]s separated by / delimeters. Any
// existing / characters in names are escaped to \\
//
// The paths that it returns exclude the
// name of the parent and the leading slash; for example, in the tree
// a/b/c/d/e, the result of d.PathFrom(b) would be c/d. PathFrom
// automatically gets the [NodeBase.This] version of the given parent,
// so a base type can be passed in without manually accessing [NodeBase.This].
func (n *NodeBase) PathFrom(parent Node) string {
if n.This == parent {
return ""
}
// critical to get `This`
parent = parent.AsTree().This
// we bail a level below the parent so it isn't in the path
if n.Parent == nil || n.Parent == parent {
return EscapePathName(n.Name)
}
ppath := n.Parent.AsTree().PathFrom(parent)
return ppath + "/" + EscapePathName(n.Name)
}
// FindPath returns the node at the given path from this node.
// FindPath only works correctly when names are unique.
// The given path must be consistent with the format produced
// by [NodeBase.PathFrom]. There is also support for index-based
// access (ie: [0] for the first child) for cases where indexes
// are more useful than names. It returns nil if no node is found
// at the given path.
func (n *NodeBase) FindPath(path string) Node {
curn := n.This
pels := strings.Split(strings.Trim(strings.TrimSpace(path), "\""), "/")
for _, pe := range pels {
if len(pe) == 0 {
continue
}
idx := findPathChild(curn, UnescapePathName(pe))
if idx < 0 {
return nil
}
curn = curn.AsTree().Children[idx]
}
return curn
}
// findPathChild finds the child with the given string representation in [NodeBase.FindPath].
func findPathChild(n Node, child string) int {
if child[0] == '[' && child[len(child)-1] == ']' {
idx, err := strconv.Atoi(child[1 : len(child)-1])
if err != nil {
return idx
}
if idx < 0 { // from end
idx = len(n.AsTree().Children) + idx
}
return idx
}
return IndexByName(n.AsTree().Children, child)
}
// Adding and Inserting Children:
// AddChild adds given child at end of children list.
// The kid node is assumed to not be on another tree (see [MoveToParent])
// and the existing name should be unique among children.
func (n *NodeBase) AddChild(kid Node) {
InitNode(kid)
n.Children = append(n.Children, kid)
SetParent(kid, n) // key to set new parent before deleting: indicates move instead of delete
}
// NewChild creates a new child of the given type and adds it at the end
// of the list of children. The name defaults to the ID (kebab-case) name
// of the type, plus the [Node.NumLifetimeChildren] of the parent.
func (n *NodeBase) NewChild(typ *types.Type) Node {
kid := newOfType(typ)
InitNode(kid)
n.Children = append(n.Children, kid)
SetParent(kid, n)
return kid
}
// InsertChild adds given child at position in children list.
// The kid node is assumed to not be on another tree (see [MoveToParent])
// and the existing name should be unique among children.
func (n *NodeBase) InsertChild(kid Node, index int) {
InitNode(kid)
n.Children = slices.Insert(n.Children, index, kid)
SetParent(kid, n)
}
// Deleting Children:
// DeleteChildAt deletes child at the given index. It returns false
// if there is no child at the given index.
func (n *NodeBase) DeleteChildAt(index int) bool {
child := n.Child(index)
if child == nil {
return false
}
n.Children = slices.Delete(n.Children, index, index+1)
child.Destroy()
return true
}
// DeleteChild deletes the given child node, returning false if
// it can not find it.
func (n *NodeBase) DeleteChild(child Node) bool {
if child == nil {
return false
}
idx := IndexOf(n.Children, child)
if idx < 0 {
return false
}
return n.DeleteChildAt(idx)
}
// DeleteChildByName deletes child node by name, returning false
// if it can not find it.
func (n *NodeBase) DeleteChildByName(name string) bool {
idx := IndexByName(n.Children, name)
if idx < 0 {
return false
}
return n.DeleteChildAt(idx)
}
// DeleteChildren deletes all children nodes.
func (n *NodeBase) DeleteChildren() {
kids := n.Children
n.Children = n.Children[:0] // preserves capacity of list
for _, kid := range kids {
if kid == nil {
continue
}
kid.Destroy()
}
}
// Delete deletes this node from its parent's children list
// and then destroys itself.
func (n *NodeBase) Delete() {
if n.Parent == nil {
n.This.Destroy()
} else {
n.Parent.AsTree().DeleteChild(n.This)
}
}
// Destroy recursively deletes and destroys the node, all of its children,
// and all of its children's children, etc.
func (n *NodeBase) Destroy() {
if n.This == nil { // already destroyed
return
}
n.DeleteChildren()
n.This = nil
}
// Property Storage:
// SetProperty sets given the given property to the given value.
func (n *NodeBase) SetProperty(key string, value any) {
if n.Properties == nil {
n.Properties = map[string]any{}
}
n.Properties[key] = value
}
// Property returns the property value for the given key.
// It returns nil if it doesn't exist.
func (n *NodeBase) Property(key string) any {
return n.Properties[key]
}
// DeleteProperty deletes the property with the given key.
func (n *NodeBase) DeleteProperty(key string) {
if n.Properties == nil {
return
}
delete(n.Properties, key)
}
// Tree Walking:
const (
// Continue = true can be returned from tree iteration functions to continue
// processing down the tree, as compared to Break = false which stops this branch.
Continue = true
// Break = false can be returned from tree iteration functions to stop processing
// this branch of the tree.
Break = false
)
// WalkUp calls the given function on the node and all of its parents,
// sequentially in the current goroutine (generally necessary for going up,
// which is typically quite fast anyway). It stops walking if the function
// returns [Break] and keeps walking if it returns [Continue]. It returns
// whether walking was finished (false if it was aborted with [Break]).
func (n *NodeBase) WalkUp(fun func(n Node) bool) bool {
cur := n.This
for {
if !fun(cur) { // false return means stop
return false
}
parent := cur.AsTree().Parent
if parent == nil || parent == cur { // prevent loops
return true
}
cur = parent
}
}
// WalkUpParent calls the given function on all of the node's parents (but not
// the node itself), sequentially in the current goroutine (generally necessary
// for going up, which is typically quite fast anyway). It stops walking if the
// function returns [Break] and keeps walking if it returns [Continue]. It returns
// whether walking was finished (false if it was aborted with [Break]).
func (n *NodeBase) WalkUpParent(fun func(n Node) bool) bool {
if IsRoot(n) {
return true
}
cur := n.Parent
for {
if !fun(cur) { // false return means stop
return false
}
parent := cur.AsTree().Parent
if parent == nil || parent == cur { // prevent loops
return true
}
cur = parent
}
}
// WalkDown strategy: https://stackoverflow.com/questions/5278580/non-recursive-depth-first-search-algorithm
// WalkDown calls the given function on the node and all of its children
// in a depth-first manner over all of the children, sequentially in the
// current goroutine. It stops walking the current branch of the tree if
// the function returns [Break] and keeps walking if it returns [Continue].
// It is non-recursive and safe for concurrent calling. The [Node.NodeWalkDown]
// method is called for every node after the given function, which enables nodes
// to also traverse additional nodes, like widget parts.
func (n *NodeBase) WalkDown(fun func(n Node) bool) {
if n.This == nil {
return
}
tm := map[Node]int{} // traversal map
start := n.This
cur := start
tm[cur] = -1
outer:
for {
cb := cur.AsTree()
// fun can destroy the node, so we have to check for nil before and after.
// A false return from fun indicates to stop.
if cb.This != nil && fun(cur) && cb.This != nil {
cb.This.NodeWalkDown(fun)
if cb.HasChildren() {
tm[cur] = 0 // 0 for no fields
nxt := cb.Child(0)
if nxt != nil && nxt.AsTree().This != nil {
cur = nxt.AsTree().This
tm[cur] = -1
continue
}
}
} else {
tm[cur] = cb.NumChildren()
}
// if we get here, we're in the ascent branch -- move to the right and then up
for {
cb := cur.AsTree() // may have changed, so must get again
curChild := tm[cur]
if (curChild + 1) < cb.NumChildren() {
curChild++
tm[cur] = curChild
nxt := cb.Child(curChild)
if nxt != nil && nxt.AsTree().This != nil {
cur = nxt.AsTree().This
tm[cur] = -1
continue outer
}
continue
}
delete(tm, cur)
// couldn't go right, move up..
if cur == start {
break outer // done!
}
parent := cb.Parent
if parent == nil || parent == cur { // shouldn't happen, but does..
// fmt.Printf("nil / cur parent %v\n", par)
break outer
}
cur = parent
}
}
}
// NodeWalkDown is a placeholder implementation of [Node.NodeWalkDown]
// that does nothing.
func (n *NodeBase) NodeWalkDown(fun func(n Node) bool) {}
// WalkDownPost iterates in a depth-first manner over the children, calling
// shouldContinue on each node to test if processing should proceed (if it returns
// [Break] then that branch of the tree is not further processed),
// and then calls the given function after all of a node's children
// have been iterated over. In effect, this means that the given function
// is called for deeper nodes first. This uses node state information to manage
// the traversal and is very fast, but can only be called by one goroutine at a
// time, so you should use a Mutex if there is a chance of multiple threads
// running at the same time. The nodes are processed in the current goroutine.
func (n *NodeBase) WalkDownPost(shouldContinue func(n Node) bool, fun func(n Node) bool) {
if n.This == nil {
return
}
tm := map[Node]int{} // traversal map
start := n.This
cur := start
tm[cur] = -1
outer:
for {
cb := cur.AsTree()
if cb.This != nil && shouldContinue(cur) { // false return means stop
if cb.HasChildren() {
tm[cur] = 0 // 0 for no fields
nxt := cb.Child(0)
if nxt != nil && nxt.AsTree().This != nil {
cur = nxt.AsTree().This
tm[cur] = -1
continue
}
}
} else {
tm[cur] = cb.NumChildren()
}
// if we get here, we're in the ascent branch -- move to the right and then up
for {
cb := cur.AsTree() // may have changed, so must get again
curChild := tm[cur]
if (curChild + 1) < cb.NumChildren() {
curChild++
tm[cur] = curChild
nxt := cb.Child(curChild)
if nxt != nil && nxt.AsTree().This != nil {
cur = nxt.AsTree().This
tm[cur] = -1
continue outer
}
continue
}
fun(cur) // now we call the function, last..
// couldn't go right, move up..
delete(tm, cur)
if cur == start {
break outer // done!
}
parent := cb.Parent
if parent == nil || parent == cur { // shouldn't happen
break outer
}
cur = parent
}
}
}
// Note: it does not appear that there is a good
// recursive breadth-first-search strategy:
// https://herringtondarkholme.github.io/2014/02/17/generator/
// https://stackoverflow.com/questions/2549541/performing-breadth-first-search-recursively/2549825#2549825
// WalkDownBreadth calls the given function on the node and all of its children
// in breadth-first order. It stops walking the current branch of the tree if the
// function returns [Break] and keeps walking if it returns [Continue]. It is
// non-recursive, but not safe for concurrent calling.
func (n *NodeBase) WalkDownBreadth(fun func(n Node) bool) {
start := n.This
level := 0
start.AsTree().depth = level
queue := make([]Node, 1)
queue[0] = start
for {
if len(queue) == 0 {
break
}
cur := queue[0]
depth := cur.AsTree().depth
queue = queue[1:]
if cur.AsTree().This != nil && fun(cur) { // false return means don't proceed
for _, cn := range cur.AsTree().Children {
if cn != nil && cn.AsTree().This != nil {
cn.AsTree().depth = depth + 1
queue = append(queue, cn)
}
}
}
}
}
// Deep Copy:
// note: we use the copy from direction (instead of copy to), as the receiver
// is modified whereas the from is not and assignment is typically in the same
// direction.
// CopyFrom copies the data and children of the given node to this node.
// It is essential that the source node has unique names. It is very efficient
// by using the [Node.ConfigChildren] method which attempts to preserve any
// existing nodes in the destination if they have the same name and type, so a
// copy from a source to a target that only differ minimally will be
// minimally destructive. Only copying to the same type is supported.
// The struct field tag copier:"-" can be added for any fields that
// should not be copied. Also, unexported fields are not copied.
// See [Node.CopyFieldsFrom] for more information on field copying.
func (n *NodeBase) CopyFrom(from Node) {
if from == nil {
slog.Error("tree.NodeBase.CopyFrom: nil source", "destinationNode", n)
return
}
copyFrom(n.This, from)
}
// copyFrom is the implementation of [NodeBase.CopyFrom].
func copyFrom(to, from Node) {
fc := from.AsTree().Children
if len(fc) == 0 {
to.AsTree().DeleteChildren()
} else {
p := make(TypePlan, len(fc))
for i, c := range fc {
p[i].Type = c.AsTree().NodeType()
p[i].Name = c.AsTree().Name
}
UpdateSlice(&to.AsTree().Children, to, p)
}
maps.Copy(to.AsTree().Properties, from.AsTree().Properties)
to.AsTree().This.CopyFieldsFrom(from)
for i, kid := range to.AsTree().Children {
fmk := from.AsTree().Child(i)
copyFrom(kid, fmk)
}
}
// Clone creates and returns a deep copy of the tree from this node down.
// Any pointers within the cloned tree will correctly point within the new
// cloned tree (see [Node.CopyFrom] for more information).
func (n *NodeBase) Clone() Node {
nc := n.NewInstance()
InitNode(nc)
nc.AsTree().SetName(n.Name)
nc.AsTree().CopyFrom(n.This)
return nc
}
// CopyFieldsFrom copies the fields of the node from the given node.
// By default, it is [NodeBase.CopyFieldsFrom], which automatically does
// a deep copy of all of the fields of the node that do not a have a
// `copier:"-"` struct tag. Node types should only implement a custom
// CopyFieldsFrom method when they have fields that need special copying
// logic that can not be automatically handled. All custom CopyFieldsFrom
// methods should call [NodeBase.CopyFieldsFrom] first and then only do manual
// handling of specific fields that can not be automatically copied. See
// [cogentcore.org/core/core.WidgetBase.CopyFieldsFrom] for an example of a
// custom CopyFieldsFrom method.
func (n *NodeBase) CopyFieldsFrom(from Node) {
err := copier.CopyWithOption(n.This, from.AsTree().This, copier.Option{CaseSensitive: true, DeepCopy: true})
if err != nil {
slog.Error("tree.NodeBase.CopyFieldsFrom", "err", err)
}
}
// Event methods:
// Init is a placeholder implementation of
// [Node.Init] that does nothing.
func (n *NodeBase) Init() {}
// OnAdd is a placeholder implementation of
// [Node.OnAdd] that does nothing.
func (n *NodeBase) OnAdd() {}
// Copyright (c) 2024, 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 tree
import (
"log/slog"
"path/filepath"
"runtime"
"strconv"
"strings"
"cogentcore.org/core/base/plan"
"cogentcore.org/core/base/profile"
)
// Plan represents a plan for how the children of a [Node] should be configured.
// A Plan instance is passed to [NodeBase.Makers], which are responsible for
// configuring it. To add a child item to a plan, use [Add], [AddAt], or [AddNew].
// To add a child item maker to a [Node], use [AddChild] or [AddChildAt]. To extend
// an existing child item, use [AddInit] or [AddChildInit].
type Plan struct {
// Children are the [PlanItem]s for the children.
Children []*PlanItem
// EnforceEmpty is whether an empty plan results in the removal
// of all children of the parent. If there are [NodeBase.Makers]
// defined then this is true by default; otherwise it is false.
EnforceEmpty bool
}
// PlanItem represents a plan for how a child [Node] should be constructed and initialized.
// See [Plan] for more information.
type PlanItem struct {
// Name is the name of the planned node.
Name string
// New returns a new node of the correct type for this child.
New func() Node
// Init is a slice of functions that are called once in sequential ascending order
// after [PlanItem.New] to initialize the node for the first time.
Init []func(n Node)
}
// Updater adds a new function to [NodeBase.Updaters.Normal], which are called in sequential
// descending (reverse) order in [NodeBase.RunUpdaters] to update the node.
func (nb *NodeBase) Updater(updater func()) {
nb.Updaters.Normal = append(nb.Updaters.Normal, updater)
}
// FirstUpdater adds a new function to [NodeBase.Updaters.First], which are called in sequential
// descending (reverse) order in [NodeBase.RunUpdaters] to update the node.
func (nb *NodeBase) FirstUpdater(updater func()) {
nb.Updaters.First = append(nb.Updaters.First, updater)
}
// FinalUpdater adds a new function to [NodeBase.Updaters.Final], which are called in sequential
// descending (reverse) order in [NodeBase.RunUpdaters] to update the node.
func (nb *NodeBase) FinalUpdater(updater func()) {
nb.Updaters.Final = append(nb.Updaters.Final, updater)
}
// Maker adds a new function to [NodeBase.Makers.Normal], which are called in sequential
// ascending order in [NodeBase.Make] to make the plan for how the node's children
// should be configured.
func (nb *NodeBase) Maker(maker func(p *Plan)) {
nb.Makers.Normal = append(nb.Makers.Normal, maker)
}
// FirstMaker adds a new function to [NodeBase.Makers.First], which are called in sequential
// ascending order in [NodeBase.Make] to make the plan for how the node's children
// should be configured.
func (nb *NodeBase) FirstMaker(maker func(p *Plan)) {
nb.Makers.First = append(nb.Makers.First, maker)
}
// FinalMaker adds a new function to [NodeBase.Makers.Final], which are called in sequential
// ascending order in [NodeBase.Make] to make the plan for how the node's children
// should be configured.
func (nb *NodeBase) FinalMaker(maker func(p *Plan)) {
nb.Makers.Final = append(nb.Makers.Final, maker)
}
// Make makes a plan for how the node's children should be structured.
// It does this by running [NodeBase.Makers] in sequential ascending order.
func (nb *NodeBase) Make(p *Plan) {
// only enforce empty if makers exist
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 {
maker(p)
}
})
}
// UpdateFromMake updates the node using [NodeBase.Make].
func (nb *NodeBase) UpdateFromMake() {
p := &Plan{}
nb.Make(p)
p.Update(nb)
}
// RunUpdaters runs the [NodeBase.Updaters] in sequential descending (reverse) order.
// 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]()
}
})
}
// Add adds a new [PlanItem] to the given [Plan] for a [Node] with
// the given function to initialize the node. The node is
// guaranteed to be added to its parent before the init function
// is called. The name of the node is automatically generated based
// on the file and line number of the calling function.
func Add[T NodeValue](p *Plan, init func(w *T)) { //yaegi:add
AddAt(p, AutoPlanName(2), init)
}
// AutoPlanName returns the dir-filename of [runtime.Caller](level),
// with all / . replaced to -, which is suitable as a unique name
// for a [PlanItem.Name].
func AutoPlanName(level int) string {
_, file, line, _ := runtime.Caller(level)
name := filepath.Base(file)
dir := filepath.Base(filepath.Dir(file))
path := dir + "-" + name
path = strings.ReplaceAll(strings.ReplaceAll(path, "/", "-"), ".", "-") + "-" + strconv.Itoa(line)
return path
}
// AddAt adds a new [PlanItem] to the given [Plan] for a [Node] with
// the given name and function to initialize the node. The node
// is guaranteed to be added to its parent before the init function
// is called.
func AddAt[T NodeValue](p *Plan, name string, init func(w *T)) { //yaegi:add
p.Add(name, func() Node {
return any(New[T]()).(Node)
}, func(n Node) {
init(any(n).(*T))
})
}
// AddNew adds a new [PlanItem] to the given [Plan] for a [Node] with
// the given name, function for constructing the node, and function
// for initializing the node. The node is guaranteed to be added
// to its parent before the init function is called.
// It should only be called instead of [Add] and [AddAt] when the node
// must be made new, like when using [cogentcore.org/core/core.NewValue].
func AddNew[T Node](p *Plan, name string, new func() T, init func(w T)) { //yaegi:add
p.Add(name, func() Node {
return new()
}, func(n Node) {
init(n.(T))
})
}
// AddInit adds a new function for initializing the [Node] with the given name
// in the given [Plan]. The node must already exist in the plan; this is for
// extending an existing [PlanItem], not adding a new one. The node is guaranteed to
// be added to its parent before the init function is called. The init functions are
// called in sequential ascending order.
func AddInit[T NodeValue](p *Plan, name string, init func(w *T)) { //yaegi:add
for _, child := range p.Children {
if child.Name == name {
child.Init = append(child.Init, func(n Node) {
init(any(n).(*T))
})
return
}
}
slog.Error("AddInit: child not found", "name", name)
}
// AddChild adds a new [NodeBase.Maker] to the the given parent [Node] that
// adds a [PlanItem] with the given init function using [Add]. In other words,
// this adds a maker that will add a child to the given parent.
func AddChild[T NodeValue](parent Node, init func(w *T)) { //yaegi:add
name := AutoPlanName(2) // must get here to get correct name
parent.AsTree().Maker(func(p *Plan) {
AddAt(p, name, init)
})
}
// AddChildAt adds a new [NodeBase.Maker] to the the given parent [Node] that
// adds a [PlanItem] with the given name and init function using [AddAt]. In other
// words, this adds a maker that will add a child to the given parent.
func AddChildAt[T NodeValue](parent Node, name string, init func(w *T)) { //yaegi:add
parent.AsTree().Maker(func(p *Plan) {
AddAt(p, name, init)
})
}
// AddChildInit adds a new [NodeBase.Maker] to the the given parent [Node] that
// adds a new function for initializing the node with the given name
// in the given [Plan]. The node must already exist in the plan; this is for
// extending an existing [PlanItem], not adding a new one. The node is guaranteed
// to be added to its parent before the init function is called. The init functions are
// called in sequential ascending order.
func AddChildInit[T NodeValue](parent Node, name string, init func(w *T)) { //yaegi:add
parent.AsTree().Maker(func(p *Plan) {
AddInit(p, name, init)
})
}
// Add adds a new [PlanItem] with the given name and functions to the [Plan].
// It should typically not be called by end-user code; see the generic
// [Add], [AddAt], [AddNew], [AddChild], [AddChildAt], [AddInit], and [AddChildInit]
// functions instead.
func (p *Plan) Add(name string, new func() Node, init func(w Node)) {
p.Children = append(p.Children, &PlanItem{Name: name, New: new, Init: []func(n Node){init}})
}
// Update updates the children of the given [Node] in accordance with the [Plan].
func (p *Plan) Update(n Node) {
if len(p.Children) == 0 && !p.EnforceEmpty {
return
}
pr := profile.Start("plan.Update")
plan.Update(&n.AsTree().Children, len(p.Children),
func(i int) string {
return p.Children[i].Name
}, func(name string, i int) Node {
item := p.Children[i]
child := item.New()
child.AsTree().SetName(item.Name)
return child
}, func(child Node, i int) {
SetParent(child, n)
for _, f := range p.Children[i].Init {
f(child)
}
}, func(child Node) {
child.Destroy()
},
)
pr.End()
}
// 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 tree
import (
"cogentcore.org/core/base/slicesx"
)
// IndexOf returns the index of the given node in the given slice,
// or -1 if it is not found. The optional startIndex argument
// allows for optimized bidirectional searching if you have a guess
// at where the node might be, which can be a key speedup for large
// slices. If no value is specified for startIndex, it starts in the
// middle, which is a good default.
func IndexOf(slice []Node, child Node, startIndex ...int) int {
return slicesx.Search(slice, func(e Node) bool { return e == child }, startIndex...)
}
// IndexByName returns the index of the first element in the given slice that
// has the given name, or -1 if none is found. See [IndexOf] for info on startIndex.
func IndexByName(slice []Node, name string, startIndex ...int) int {
return slicesx.Search(slice, func(ch Node) bool { return ch.AsTree().Name == name }, startIndex...)
}
// Code generated by "core generate"; DO NOT EDIT.
package tree
import (
"cogentcore.org/core/types"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/tree.NodeBase", IDName: "node-base", Doc: "NodeBase implements the [Node] interface and provides the core functionality\nfor the Cogent Core tree system. You must use NodeBase as an embedded struct\nin all higher-level tree types.\n\nAll nodes must be properly initialized by using one of [New], [NodeBase.NewChild],\n[NodeBase.AddChild], [NodeBase.InsertChild], [NodeBase.Clone], [Update], or [Plan].\nThis ensures that the [NodeBase.This] field is set correctly and the [Node.Init]\nmethod is called.\n\nAll nodes support JSON marshalling and unmarshalling through the standard [encoding/json]\ninterfaces, so you can use the standard functions for loading and saving trees. However,\nif you want to load a root node of the correct type from JSON, you need to use the\n[UnmarshalRootJSON] function.\n\nAll node types must be added to the Cogent Core type registry via typegen,\nso you must add a go:generate line that runs `core generate` to any packages\nyou write that have new node types defined.", Fields: []types.Field{{Name: "Name", Doc: "Name is the name of this node, which is typically unique relative to other children of\nthe same parent. It can be used for finding and serializing nodes. If not otherwise set,\nit defaults to the ID (kebab-case) name of the node type combined with the total number\nof children that have ever been added to the node's parent."}, {Name: "This", Doc: "This is the value of this Node as its true underlying type. This allows methods\ndefined on base types to call methods defined on higher-level types, which\nis necessary for various parts of tree and widget functionality. This is set\nto nil when the node is deleted."}, {Name: "Parent", Doc: "Parent is the parent of this node, which is set automatically when this node is\nadded as a child of a parent. To change the parent of a node, use [MoveToParent];\nyou should typically not set this field directly. Nodes can only have one parent\nat a time."}, {Name: "Children", Doc: "Children is the list of children of this node. All of them are set to have this node\nas their parent. You can directly modify this list, but you should typically use the\nvarious NodeBase child helper functions when applicable so that everything is updated\nproperly, such as when deleting children."}, {Name: "Properties", Doc: "Properties is a property map for arbitrary key-value properties.\nWhen possible, use typed fields on a new type embedding NodeBase instead of this.\nYou should typically use the [NodeBase.SetProperty], [NodeBase.Property], and\n[NodeBase.DeleteProperty] methods for modifying and accessing properties."}, {Name: "Updaters", Doc: "Updaters is a tiered set of functions called in sequential descending (reverse) order\nin [NodeBase.RunUpdaters] to update the node. You can use [NodeBase.Updater],\n[NodeBase.FirstUpdater], or [NodeBase.FinalUpdater] to add one. This typically\ntypically contains [NodeBase.UpdateFromMake] at the start of the normal list."}, {Name: "Makers", Doc: "Makers is a tiered set of functions called in sequential ascending order\nin [NodeBase.Make] to make the plan for how the node's children should\nbe configured. You can use [NodeBase.Maker], [NodeBase.FirstMaker], or\n[NodeBase.FinalMaker] to add one."}, {Name: "OnChildAdded", Doc: "OnChildAdded is called when a node is added as a direct child of this node.\nWhen a node is added to a parent, it calls [Node.OnAdd] on itself and then\nthis function on its parent if it is non-nil."}, {Name: "numLifetimeChildren", Doc: "numLifetimeChildren is the number of children that have ever been added to this\nnode, which is used for automatic unique naming."}, {Name: "index", Doc: "index is the last value of our index, which is used as a starting point for\nfinding us in our parent next time. It is not guaranteed to be accurate;\nuse the [NodeBase.IndexInParent] method."}, {Name: "depth", Doc: "depth is the depth of the node while using [NodeBase.WalkDownBreadth]."}}})
// NewNodeBase returns a new [NodeBase] with the given optional parent:
// NodeBase implements the [Node] interface and provides the core functionality
// for the Cogent Core tree system. You must use NodeBase as an embedded struct
// in all higher-level tree types.
//
// All nodes must be properly initialized by using one of [New], [NodeBase.NewChild],
// [NodeBase.AddChild], [NodeBase.InsertChild], [NodeBase.Clone], [Update], or [Plan].
// This ensures that the [NodeBase.This] field is set correctly and the [Node.Init]
// method is called.
//
// All nodes support JSON marshalling and unmarshalling through the standard [encoding/json]
// interfaces, so you can use the standard functions for loading and saving trees. However,
// if you want to load a root node of the correct type from JSON, you need to use the
// [UnmarshalRootJSON] function.
//
// All node types must be added to the Cogent Core type registry via typegen,
// so you must add a go:generate line that runs `core generate` to any packages
// you write that have new node types defined.
func NewNodeBase(parent ...Node) *NodeBase { return New[NodeBase](parent...) }
// SetName sets the [NodeBase.Name]:
// Name is the name of this node, which is typically unique relative to other children of
// the same parent. It can be used for finding and serializing nodes. If not otherwise set,
// it defaults to the ID (kebab-case) name of the node type combined with the total number
// of children that have ever been added to the node's parent.
func (t *NodeBase) SetName(v string) *NodeBase { t.Name = v; return t }
// SetOnChildAdded sets the [NodeBase.OnChildAdded]:
// OnChildAdded is called when a node is added as a direct child of this node.
// When a node is added to a parent, it calls [Node.OnAdd] on itself and then
// this function on its parent if it is non-nil.
func (t *NodeBase) SetOnChildAdded(v func(n Node)) *NodeBase { t.OnChildAdded = v; return t }
// 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 tree
import (
"cogentcore.org/core/base/plan"
"cogentcore.org/core/types"
)
// TypePlan is a plan for the organization of a list of tree nodes,
// specified by the Type of element at a given index, with a given name.
// It is used in [Update] and [UpdateSlice] to actually update the items
// according to the plan.
type TypePlan []TypePlanItem
// TypePlanItem holds a type and a name, for specifying the [TypePlan].
type TypePlanItem struct {
Type *types.Type
Name string
}
// Add adds a new [TypePlanItem] with the given type and name.
func (t *TypePlan) Add(typ *types.Type, name string) {
*t = append(*t, TypePlanItem{typ, name})
}
// UpdateSlice ensures that the given slice contains the elements
// according to the [TypePlan], specified by unique element names.
// The given Node is set as the parent of the created nodes.
// It returns whether any changes were made.
func UpdateSlice(slice *[]Node, parent Node, p TypePlan) bool {
return plan.Update(slice, len(p),
func(i int) string { return p[i].Name },
func(name string, i int) Node {
n := newOfType(p[i].Type)
n.AsTree().SetName(name)
InitNode(n)
return n
}, func(child Node, i int) {
if parent != nil {
SetParent(child, parent)
}
},
func(child Node) {
child.Destroy()
},
)
}
// Update ensures that the children of the given [Node] contain the elements
// according to the [TypePlan], specified by unique element names.
// It returns whether any changes were made.
func Update(n Node, p TypePlan) bool {
return UpdateSlice(&n.AsTree().Children, n, p)
}
// Copyright (c) 2020, Cogent Core. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
This file provides basic tree walking functions for iterative traversal
of the tree in up / down directions. As compared to the Node walk methods,
these are for more dynamic, piecemeal processing.
*/
package tree
// Last returns the last node in the tree.
func Last(n Node) Node {
n = lastChild(n)
last := n
n.AsTree().WalkDown(func(k Node) bool {
last = k
return Continue
})
return last
}
// lastChild returns the last child under the given node,
// or the node itself if it has no children.
func lastChild(n Node) Node {
nb := n.AsTree()
if nb.HasChildren() {
return lastChild(nb.Child(nb.NumChildren() - 1))
}
return n
}
// Previous returns the previous node in the tree,
// or nil if this is the root node.
func Previous(n Node) Node {
nb := n.AsTree()
if nb.Parent == nil {
return nil
}
myidx := n.AsTree().IndexInParent()
if myidx > 0 {
nn := nb.Parent.AsTree().Child(myidx - 1)
return lastChild(nn)
}
return nb.Parent
}
// Next returns next node in the tree,
// or nil if this is the last node.
func Next(n Node) Node {
if !n.AsTree().HasChildren() {
return NextSibling(n)
}
return n.AsTree().Child(0)
}
// NextSibling returns the next sibling of this node,
// or nil if it has none.
func NextSibling(n Node) Node {
nb := n.AsTree()
if nb.Parent == nil {
return nil
}
myidx := n.AsTree().IndexInParent()
if myidx >= 0 && myidx < nb.Parent.AsTree().NumChildren()-1 {
return nb.Parent.AsTree().Child(myidx + 1)
}
return NextSibling(nb.Parent)
}
// 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.
// Command typgen provides the generation of type information for
// Go types, methods, and functions.
package main
import (
"cogentcore.org/core/cli"
"cogentcore.org/core/types/typegen"
)
func main() {
opts := cli.DefaultOptions("typegen", "Typegen provides the generation of type information for Go types, methods, and functions.")
cli.Run(opts, &typegen.Config{}, typegen.Generate)
}
// 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 types
import (
"strings"
)
// Directive represents a comment directive in the format:
//
// //tool:directive args...
type Directive struct {
Tool string
Directive string
Args []string
}
// String returns a string representation of the directive
// in the format:
//
// //tool:directive args...
func (d Directive) String() string {
return "//" + d.Tool + ":" + d.Directive + " " + strings.Join(d.Args, " ")
}
func (d Directive) GoString() string { return StructGoString(d) }
// 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 types
import (
"reflect"
"cogentcore.org/core/base/reflectx"
)
// Field represents a field or embed in a struct.
type Field struct {
// Name is the name of the field (eg: Icon)
Name string
// Doc has all of the comment documentation
// info as one string with directives removed.
Doc string
}
func (f Field) GoString() string { return StructGoString(f) }
// GetField recursively attempts to extract the [Field]
// with the given name from the given struct [reflect.Value],
// by searching through all of the embeds if it can not find
// it directly in the struct.
func GetField(val reflect.Value, field string) *Field {
val = reflectx.NonPointerValue(val)
if !val.IsValid() {
return nil
}
typ := TypeByName(TypeName(val.Type()))
// if we are not in the type registry, there is nothing that we can do
if typ == nil {
return nil
}
for _, f := range typ.Fields {
if f.Name == field {
// we have successfully gotten the field
return &f
}
}
// otherwise, we go through all of the embeds and call
// GetField recursively on them
for _, e := range typ.Embeds {
rf := val.FieldByName(e.Name)
f := GetField(rf, field)
// we have successfully gotten the field
if f != nil {
return f
}
}
return nil
}
// 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 types
// Func represents a global function.
type Func struct {
// Name is the fully qualified name of the function
// (eg: cogentcore.org/core/core.NewButton)
Name string
// Doc has all of the comment documentation
// info as one string with directives removed.
Doc string
// Directives are the parsed comment directives
Directives []Directive
// Args are the names of the arguments to the function
Args []string
// Returns are the names of the return values of the function
Returns []string
// ID is the unique function ID number
ID uint64
}
func (f Func) GoString() string { return StructGoString(f) }
// Method represents a method.
type Method struct {
// Name is the name of the method (eg: NewChild)
Name string
// Doc has all of the comment documentation
// info as one string with directives removed.
Doc string
// Directives are the parsed comment directives
Directives []Directive
// Args are the names of the arguments to the function
Args []string
// Returns are the names of the return values of the function
Returns []string
}
func (m Method) GoString() string { return StructGoString(m) }
// 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 types
import (
"fmt"
"log/slog"
"reflect"
"runtime"
"sync/atomic"
)
var (
// Funcs records all types (i.e., a type registry)
// key is long type name: package_url.Func, e.g., cogentcore.org/core/core.Button
Funcs = map[string]*Func{}
// FuncIDCounter is an atomically incremented uint64 used
// for assigning new [Func.ID] numbers
FuncIDCounter uint64
)
// FuncByName returns a Func by name (package_url.Type, e.g., cogentcore.org/core/core.Button),
func FuncByName(nm string) *Func {
fi, ok := Funcs[nm]
if !ok {
return nil
}
return fi
}
// FuncByNameTry returns a Func by name (package_url.Type, e.g., cogentcore.org/core/core.Button),
// or error if not found
func FuncByNameTry(nm string) (*Func, error) {
fi, ok := Funcs[nm]
if !ok {
return nil, fmt.Errorf("func %q not found", nm)
}
return fi, nil
}
// FuncInfo returns function info for given function.
func FuncInfo(f any) *Func {
return FuncByName(FuncName(f))
}
// FuncInfoTry returns function info for given function.
func FuncInfoTry(f any) (*Func, error) {
return FuncByNameTry(FuncName(f))
}
// AddFunc adds a constructed [Func] to the registry
// and returns it. This sets the ID.
func AddFunc(fun *Func) *Func {
if _, has := Funcs[fun.Name]; has {
slog.Debug("types.AddFunc: Func already exists", "Func.Name", fun.Name)
return fun
}
fun.ID = atomic.AddUint64(&FuncIDCounter, 1)
Funcs[fun.Name] = fun
return fun
}
// FuncName returns the fully package-qualified name of given function
// This is guaranteed to be unique and used for the Funcs registry.
func FuncName(f any) string {
return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
}
// 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 types
import (
"fmt"
"reflect"
"strings"
)
// Type represents a type.
type Type struct {
// Name is the fully package-path-qualified name of the type
// (eg: cogentcore.org/core/core.Button).
Name string
// IDName is the short, package-unqualified, kebab-case name of
// the type that is suitable for use in an ID (eg: button).
IDName string
// Doc has all of the comment documentation
// info as one string with directives removed.
Doc string
// Directives has the parsed comment directives.
Directives []Directive
// Methods of the type, which are available for all types.
Methods []Method
// Embedded fields of struct types.
Embeds []Field
// Fields of struct types.
Fields []Field
// Instance is an instance of a non-nil pointer to the type,
// which is set by [For] and other external functions such that
// a [Type] can be used to make new instances of the type by
// reflection. It is not set by typegen.
Instance any
// ID is the unique type ID number set by [AddType].
ID uint64
}
func (tp *Type) String() string {
return tp.Name
}
// ShortName returns the short name of the type (package.Type)
func (tp *Type) ShortName() string {
li := strings.LastIndex(tp.Name, "/")
return tp.Name[li+1:]
}
func (tp *Type) Label() string {
return tp.ShortName()
}
// ReflectType returns the [reflect.Type] for this type, using [Type.Instance].
func (tp *Type) ReflectType() reflect.Type {
if tp.Instance == nil {
return nil
}
return reflect.TypeOf(tp.Instance).Elem()
}
// StructGoString creates a GoString for the given struct,
// omitting any zero values.
func StructGoString(str any) string {
s := reflect.ValueOf(str)
typ := s.Type()
strs := []string{}
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
if f.IsZero() {
continue
}
nm := typ.Field(i).Name
strs = append(strs, fmt.Sprintf("%s: %#v", nm, f))
}
return "{" + strings.Join(strs, ", ") + "}"
}
func (tp Type) GoString() string { return StructGoString(tp) }
// 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 typegen
import (
"reflect"
"strings"
"text/template"
"unicode"
"cogentcore.org/core/types"
)
// TypeTmpl is the template for [types.Type] declarations.
// It takes a [*Type] as its value.
var TypeTmpl = template.Must(template.New("Type").
Funcs(template.FuncMap{
"TypesTypeOf": TypesTypeOf,
}).Parse(
`
var _ = types.AddType(&types.Type
{{- $typ := TypesTypeOf . -}}
{{- printf "%#v" $typ -}}
)
`))
// TypesTypeOf converts the given [*Type] to a [*types.Type].
func TypesTypeOf(typ *Type) *types.Type {
cp := typ.Type
res := &cp
res.Fields = typ.Fields.Fields
res.Embeds = typ.Embeds.Fields
return res
}
// FuncTmpl is the template for [types.Func] declarations.
// It takes a [*types.Func] as its value.
var FuncTmpl = template.Must(template.New("Func").Parse(
`
var _ = types.AddFunc(&types.Func
{{- printf "%#v" . -}}
)
`))
// SetterMethodsTmpl is the template for setter methods for a type.
// It takes a [*Type] as its value.
var SetterMethodsTmpl = template.Must(template.New("SetterMethods").
Funcs(template.FuncMap{
"SetterFields": SetterFields,
"SetterType": SetterType,
"DocToComment": DocToComment,
}).Parse(
`
{{$typ := .}}
{{range (SetterFields .)}}
// Set{{.Name}} sets the [{{$typ.LocalName}}.{{.Name}}] {{- if ne .Doc ""}}:{{end}}
{{DocToComment .Doc}}
func (t *{{$typ.LocalName}}) Set{{.Name}}(v {{SetterType . $typ}}) *{{$typ.LocalName}} { t.{{.Name}} = v; return t }
{{end}}
`))
// SetterFields returns all of the exported fields and embedded fields of the given type
// that don't have a `set:"-"` struct tag.
func SetterFields(typ *Type) []types.Field {
res := []types.Field{}
for _, f := range typ.Fields.Fields {
// we do not generate setters for unexported fields
if unicode.IsLower([]rune(f.Name)[0]) {
continue
}
// unspecified indicates to add a set method; only "-" means no set
hasSetter := reflect.StructTag(typ.Fields.Tags[f.Name]).Get("set") != "-"
if hasSetter {
res = append(res, f)
}
}
return res
}
// SetterType returns the setter type name for the given field in the context of the
// given type. It converts slices to variadic arguments.
func SetterType(f types.Field, typ *Type) string {
lt := typ.Fields.LocalTypes[f.Name]
if strings.HasPrefix(lt, "[]") {
return "..." + strings.TrimPrefix(lt, "[]")
}
return lt
}
// DocToComment converts the given doc string to an appropriate comment string.
func DocToComment(doc string) string {
return "// " + strings.ReplaceAll(doc, "\n", "\n// ")
}
// 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 typegen
import (
"bytes"
"fmt"
"go/ast"
"go/types"
"maps"
"os"
"slices"
"strings"
"text/template"
"log/slog"
"cogentcore.org/core/base/generate"
"cogentcore.org/core/base/ordmap"
"cogentcore.org/core/base/strcase"
"cogentcore.org/core/cli"
ctypes "cogentcore.org/core/types"
"golang.org/x/tools/go/packages"
)
// Generator holds the state of the generator.
// It is primarily used to buffer the output.
type Generator struct {
Config *Config // The configuration information
Buf bytes.Buffer // The accumulated output.
Pkgs []*packages.Package // The packages we are scanning.
Pkg *packages.Package // The packages we are currently on.
File *ast.File // The file we are currently on.
Cmap ast.CommentMap // The comment map for the file we are currently on.
Types []*Type // The types
Methods ordmap.Map[string, []ctypes.Method] // The methods, keyed by the the full package name of the type of the receiver
Funcs ordmap.Map[string, ctypes.Func] // The functions
Interfaces ordmap.Map[string, *types.Interface] // The cached interfaces, created from [Config.InterfaceConfigs]
}
// NewGenerator returns a new generator with the
// given configuration information and parsed packages.
func NewGenerator(config *Config, pkgs []*packages.Package) *Generator {
return &Generator{Config: config, Pkgs: pkgs}
}
// PackageModes returns the package load modes needed for typegen,
// based on the given config information.
func PackageModes(cfg *Config) packages.LoadMode {
res := packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo
// we only need deps if we are checking for interface impls
if cfg.InterfaceConfigs.Len() > 0 {
res |= packages.NeedDeps
}
return res
}
// Printf prints the formatted string to the
// accumulated output in [Generator.Buf]
func (g *Generator) Printf(format string, args ...any) {
fmt.Fprintf(&g.Buf, format, args...)
}
// PrintHeader prints the header and package clause
// to the accumulated output
func (g *Generator) PrintHeader() {
// we need a manual import of types and ordmap because they are
// external, but goimports will handle everything else
generate.PrintHeader(&g.Buf, g.Pkg.Name, "cogentcore.org/core/types", "cogentcore.org/core/base/ordmap")
}
// Find goes through all of the types, functions, variables,
// and constants in the package, finds those marked with types:add,
// and adds them to [Generator.Types] and [Generator.Funcs]
func (g *Generator) Find() error {
err := g.GetInterfaces()
if err != nil {
return err
}
g.Types = []*Type{}
err = generate.Inspect(g.Pkg, g.Inspect)
if err != nil {
return fmt.Errorf("error while inspecting: %w", err)
}
return nil
}
// GetInterfaces sets [Generator.Interfaces] based on
// [Generator.Config.InterfaceConfigs]. It should typically not
// be called by end-user code.
func (g *Generator) GetInterfaces() error {
if g.Config.InterfaceConfigs.Len() == 0 {
return nil
}
for _, typ := range g.Pkg.TypesInfo.Types {
nm := typ.Type.String()
if _, ok := g.Config.InterfaceConfigs.ValueByKeyTry(nm); ok {
utyp := typ.Type.Underlying()
iface, ok := utyp.(*types.Interface)
if !ok {
return fmt.Errorf("invalid InterfaceConfigs value: type %q is not a *types.Interface but a %T (type value %v)", nm, utyp, utyp)
}
g.Interfaces.Add(nm, iface)
}
}
return nil
}
// AllowedEnumTypes are the types that can be used for enums
// that are not bit flags (bit flags can only be int64s).
// It is stored as a map for quick and convenient access.
var AllowedEnumTypes = map[string]bool{"int": true, "int64": true, "int32": true, "int16": true, "int8": true, "uint": true, "uint64": true, "uint32": true, "uint16": true, "uint8": true}
// Inspect looks at the given AST node and adds it
// to [Generator.Types] if it is marked with an appropriate
// comment directive. It returns whether the AST inspector should
// continue, and an error if there is one. It should only
// be called in [ast.Inspect].
func (g *Generator) Inspect(n ast.Node) (bool, error) {
switch v := n.(type) {
case *ast.File:
g.File = v
g.Cmap = ast.NewCommentMap(g.Pkg.Fset, v, v.Comments)
case *ast.GenDecl:
return g.InspectGenDecl(v)
case *ast.FuncDecl:
return g.InspectFuncDecl(v)
}
return true, nil
}
// InspectGenDecl is the implementation of [Generator.Inspect]
// for [ast.GenDecl] nodes.
func (g *Generator) InspectGenDecl(gd *ast.GenDecl) (bool, error) {
doc := strings.TrimSuffix(gd.Doc.Text(), "\n")
for _, spec := range gd.Specs {
ts, ok := spec.(*ast.TypeSpec)
if !ok {
return true, nil
}
cfg := &Config{}
*cfg = *g.Config
if cfg.InterfaceConfigs.Len() > 0 {
typ := g.Pkg.TypesInfo.Defs[ts.Name].Type()
if !types.IsInterface(typ) {
for _, kv := range cfg.InterfaceConfigs.Order {
in := kv.Key
ic := kv.Value
iface := g.Interfaces.ValueByKey(in)
if iface == nil {
slog.Info("missing interface object", "interface", in)
continue
}
if !types.Implements(typ, iface) && !types.Implements(types.NewPointer(typ), iface) { // either base type or pointer can implement
continue
}
*cfg = *ic
}
}
}
// By default, we use the comments on the GenDecl, as that
// is where they are normally stored. However, when there
// are comments on the type spec itself, that means we are
// probably in a type block, and thus we must use the comments
// on the type spec itself.
commentNode := ast.Node(gd)
if ts.Doc != nil || ts.Comment != nil {
commentNode = ts
}
if ts.Doc != nil {
doc = strings.TrimSuffix(ts.Doc.Text(), "\n")
}
dirs, hasAdd, hasSkip, err := g.LoadFromNodeComments(cfg, commentNode)
if err != nil {
return false, err
}
if (!hasAdd && !cfg.AddTypes) || hasSkip { // we must be told to add or we will not add
return true, nil
}
typ := &Type{
Type: ctypes.Type{
Name: FullName(g.Pkg, ts.Name.Name),
IDName: strcase.ToKebab(ts.Name.Name),
Doc: doc,
Directives: dirs,
},
LocalName: ts.Name.Name,
AST: ts,
Pkg: g.Pkg.Name,
Config: cfg,
}
if st, ok := ts.Type.(*ast.StructType); ok && st.Fields != nil {
emblist := &ast.FieldList{}
delOff := 0
for i := range len(st.Fields.List) {
i -= delOff
field := st.Fields.List[i]
// if we have no names, we are embed, so add to embeds and remove from fields
if len(field.Names) == 0 {
emblist.List = append(emblist.List, field)
st.Fields.List = slices.Delete(st.Fields.List, i, i+1)
delOff++
}
}
embeds, err := g.GetFields(emblist, cfg)
if err != nil {
return false, err
}
typ.Embeds = embeds
fields, err := g.GetFields(st.Fields, cfg)
if err != nil {
return false, err
}
typ.Fields = fields
}
if in, ok := ts.Type.(*ast.InterfaceType); ok {
prev := g.Config.AddMethods
// the only point of an interface is the methods,
// so we add them by default
g.Config.AddMethods = true
for _, m := range in.Methods.List {
if f, ok := m.Type.(*ast.FuncType); ok {
// add in any line comments
if m.Doc == nil {
m.Doc = m.Comment
} else if m.Comment != nil {
m.Doc.List = append(m.Doc.List, m.Comment.List...)
}
g.InspectFuncDecl(&ast.FuncDecl{
Doc: m.Doc,
Recv: &ast.FieldList{List: []*ast.Field{{Type: ts.Name}}},
Name: m.Names[0],
Type: f,
})
}
}
g.Config.AddMethods = prev
}
g.Types = append(g.Types, typ)
}
return true, nil
}
// LocalTypeNameQualifier returns a [types.Qualifier] similar to that
// returned by [types.RelativeTo], but using the package name instead
// of the package path so that it can be used in code.
func LocalTypeNameQualifier(pkg *types.Package) types.Qualifier {
if pkg == nil {
return nil
}
return func(other *types.Package) string {
if pkg == other {
return "" // same package; unqualified
}
return other.Name()
}
}
// InspectFuncDecl is the implementation of [Generator.Inspect]
// for [ast.FuncDecl] nodes.
func (g *Generator) InspectFuncDecl(fd *ast.FuncDecl) (bool, error) {
cfg := &Config{}
*cfg = *g.Config
dirs, hasAdd, hasSkip, err := g.LoadFromNodeComments(cfg, fd)
if err != nil {
return false, err
}
doc := strings.TrimSuffix(fd.Doc.Text(), "\n")
if fd.Recv == nil {
if (!hasAdd && !cfg.AddFuncs) || hasSkip { // we must be told to add or we will not add
return true, nil
}
fun := ctypes.Func{
Name: FullName(g.Pkg, fd.Name.Name),
Doc: doc,
Directives: dirs,
}
args, err := g.GetFields(fd.Type.Params, cfg)
if err != nil {
return false, fmt.Errorf("error getting function args: %w", err)
}
for _, arg := range args.Fields {
fun.Args = append(fun.Args, arg.Name)
}
rets, err := g.GetFields(fd.Type.Results, cfg)
if err != nil {
return false, fmt.Errorf("error getting function return values: %w", err)
}
for _, ret := range rets.Fields {
fun.Returns = append(fun.Returns, ret.Name)
}
g.Funcs.Add(fun.Name, fun)
} else {
if (!hasAdd && !cfg.AddMethods) || hasSkip { // we must be told to add or we will not add
return true, nil
}
method := ctypes.Method{
Name: fd.Name.Name,
Doc: doc,
Directives: dirs,
}
args, err := g.GetFields(fd.Type.Params, cfg)
if err != nil {
return false, fmt.Errorf("error getting method args: %w", err)
}
for _, arg := range args.Fields {
method.Args = append(method.Args, arg.Name)
}
rets, err := g.GetFields(fd.Type.Results, cfg)
if err != nil {
return false, fmt.Errorf("error getting method return values: %w", err)
}
for _, ret := range rets.Fields {
method.Returns = append(method.Returns, ret.Name)
}
typ := fd.Recv.List[0].Type
// get rid of any pointer receiver
tnm := strings.TrimPrefix(types.ExprString(typ), "*")
typnm := FullName(g.Pkg, tnm)
g.Methods.Add(typnm, append(g.Methods.ValueByKey(typnm), method))
}
return true, nil
}
// FullName returns the fully qualified name of an identifier
// in the given package with the given name.
func FullName(pkg *packages.Package, name string) string {
// idents in main packages are just "main.IdentName"
if pkg.Name == "main" {
return "main." + name
}
return pkg.PkgPath + "." + name
}
// GetFields creates and returns a new [ctypes.Fields] object
// from the given [ast.FieldList], in the context of the
// given surrounding config. If the given field list is
// nil, GetFields still returns an empty but valid
// [ctypes.Fields] value and no error.
func (g *Generator) GetFields(list *ast.FieldList, cfg *Config) (Fields, error) {
res := Fields{LocalTypes: map[string]string{}, Tags: map[string]string{}}
if list == nil {
return res, nil
}
for _, field := range list.List {
ltn := types.ExprString(field.Type)
ftyp := g.Pkg.TypesInfo.TypeOf(field.Type)
tn := ftyp.String()
switch ftyp.(type) {
case *types.Slice, *types.Array, *types.Map:
default:
// if the type is not a slice, array, or map, we get the name of the type
// before anything involving square brackets so that generic types don't confuse it
tn, _, _ = strings.Cut(tn, "[")
tn, _, _ = strings.Cut(tn, "]")
}
name := ""
if len(field.Names) == 1 {
name = field.Names[0].Name
} else if len(field.Names) == 0 {
// if we have no name, fall back on type name
name = tn
// we must get rid of any package name, as field
// names never have package names
li := strings.LastIndex(name, ".")
if li >= 0 {
name = name[li+1:] // need to get rid of .
}
} else {
// if we have more than one name, that typically indicates
// type-omitted arguments (eg: "func(x, y float32)"), so
// we handle all of the names seperately here and then continue.
for _, nm := range field.Names {
nfield := *field
nfield.Names = []*ast.Ident{nm}
nlist := &ast.FieldList{List: []*ast.Field{&nfield}}
nfields, err := g.GetFields(nlist, cfg)
if err != nil {
return res, err
}
res.Fields = append(res.Fields, nfields.Fields...)
maps.Copy(res.LocalTypes, nfields.LocalTypes)
maps.Copy(res.Tags, nfields.Tags)
}
continue
}
fo := ctypes.Field{
Name: name,
Doc: strings.TrimSuffix(field.Doc.Text(), "\n"),
}
res.Fields = append(res.Fields, fo)
res.LocalTypes[name] = ltn
tag := ""
if field.Tag != nil {
// need to get rid of leading and trailing backquotes
tag = strings.TrimPrefix(strings.TrimSuffix(field.Tag.Value, "`"), "`")
}
res.Tags[name] = tag
}
return res, nil
}
// LoadFromNodeComments is a helper function that calls [LoadFromComments] with the correctly
// filtered comment map comments of the given node.
func (g *Generator) LoadFromNodeComments(cfg *Config, n ast.Node) (dirs []ctypes.Directive, hasAdd bool, hasSkip bool, err error) {
cs := g.Cmap.Filter(n).Comments()
tf := g.Pkg.Fset.File(g.File.FileStart)
np := tf.Line(n.Pos())
keep := []*ast.CommentGroup{}
for _, c := range cs {
// if the comment's line is after ours, we ignore it, as it is likely associated with something else
if tf.Line(c.Pos()) > np {
continue
}
keep = append(keep, c)
}
return LoadFromComments(cfg, keep...)
}
// LoadFromComments is a helper function that combines the results of [LoadFromComment]
// for the given comment groups.
func LoadFromComments(cfg *Config, c ...*ast.CommentGroup) (dirs []ctypes.Directive, hasAdd bool, hasSkip bool, err error) {
for _, cg := range c {
cdirs, cadd, cskip, err := LoadFromComment(cg, cfg)
if err != nil {
return nil, false, false, err
}
dirs = append(dirs, cdirs...)
hasAdd = hasAdd || cadd
hasSkip = hasSkip || cskip
}
return
}
// LoadFromComment processes the given comment group, setting the
// values of the given config object based on any types directives
// in the comment group, and returning all directives found, whether
// there was a types:add directive, and any error. If the given
// documentation is nil, LoadFromComment still returns an empty but valid
// [ctypes.Directives] value, false, and no error.
func LoadFromComment(c *ast.CommentGroup, cfg *Config) (dirs []ctypes.Directive, hasAdd bool, hasSkip bool, err error) {
if c == nil {
return
}
for _, c := range c.List {
dir, err := cli.ParseDirective(c.Text)
if err != nil {
return nil, false, false, fmt.Errorf("error parsing comment directive from %q: %w", c.Text, err)
}
if dir == nil {
continue
}
if dir.Tool == "types" && dir.Directive == "add" {
hasAdd = true
}
if dir.Tool == "types" {
if dir.Directive == "skip" {
hasSkip = true
}
if dir.Directive == "add" || dir.Directive == "skip" {
leftovers, err := cli.SetFromArgs(cfg, dir.Args, cli.ErrNotFound)
if err != nil {
return nil, false, false, fmt.Errorf("error setting config info from comment directive args: %w (from directive %q)", err, c.Text)
}
if len(leftovers) > 0 {
return nil, false, false, fmt.Errorf("expected 0 positional arguments but got %d (list: %v) (from directive %q)", len(leftovers), leftovers, c.Text)
}
} else {
return nil, false, false, fmt.Errorf("unrecognized types directive %q (from %q)", dir.Directive, c.Text)
}
}
dirs = append(dirs, *dir)
}
return dirs, hasAdd, hasSkip, nil
}
// Generate produces the code for the types
// stored in [Generator.Types] and stores them in
// [Generator.Buf]. It returns whether there were
// any types to generate methods for, and
// any error that occurred.
func (g *Generator) Generate() (bool, error) {
if len(g.Types) == 0 && g.Funcs.Len() == 0 {
return false, nil
}
for _, typ := range g.Types {
typ.Methods = append(typ.Methods, g.Methods.ValueByKey(typ.Name)...)
g.ExecTmpl(TypeTmpl, typ)
for _, tmpl := range typ.Config.Templates {
g.ExecTmpl(tmpl, typ)
}
if typ.Config.Setters {
g.ExecTmpl(SetterMethodsTmpl, typ)
}
}
for _, fun := range g.Funcs.Order {
g.ExecTmpl(FuncTmpl, fun.Value)
}
return true, nil
}
// ExecTmpl executes the given template with the given data and
// writes the result to [Generator.Buf]. It fatally logs any error.
// All typegen templates take a [*Type] or [*ctypes.Func] as their data.
func (g *Generator) ExecTmpl(t *template.Template, data any) {
err := t.Execute(&g.Buf, data)
if err != nil {
slog.Error("programmer error: internal error: error executing template", "err", err)
os.Exit(1)
}
}
// Write formats the data in the the Generator's buffer
// ([Generator.Buf]) and writes it to the file specified by
// [Generator.Config.Output].
func (g *Generator) Write() error {
return generate.Write(generate.Filepath(g.Pkg, g.Config.Output), g.Buf.Bytes(), nil)
}
// 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 typgen provides the generation of type information for
// Go types, methods, and functions.
package typegen
//go:generate go run ../cmd/typegen -output typegen_gen.go
import (
"fmt"
"cogentcore.org/core/base/generate"
"golang.org/x/tools/go/packages"
)
// ParsePackages parses the package(s) located in the configuration source directory.
func ParsePackages(cfg *Config) ([]*packages.Package, error) {
pcfg := &packages.Config{
Mode: PackageModes(cfg),
// TODO: Need to think about types and functions in test files. Maybe write typegen_test.go
// in a separate pass? For later.
Tests: false,
}
pkgs, err := generate.Load(pcfg, cfg.Dir)
if err != nil {
return nil, fmt.Errorf("typegen: Generate: error parsing package: %w", err)
}
return pkgs, err
}
// Generate generates typegen type info, using the
// configuration information, loading the packages from the
// configuration source directory, and writing the result
// to the configuration output file.
//
// It is a simple entry point to typegen that does all
// of the steps; for more specific functionality, create
// a new [Generator] with [NewGenerator] and call methods on it.
//
//cli:cmd -root
func Generate(cfg *Config) error { //types:add
pkgs, err := ParsePackages(cfg)
if err != nil {
return err
}
return GeneratePkgs(cfg, pkgs)
}
// GeneratePkgs generates enum methods using
// the given configuration object and packages parsed
// from the configuration source directory,
// and writes the result to the config output file.
// It is a simple entry point to typegen that does all
// of the steps; for more specific functionality, create
// a new [Generator] with [NewGenerator] and call methods on it.
func GeneratePkgs(cfg *Config, pkgs []*packages.Package) error {
g := NewGenerator(cfg, pkgs)
for _, pkg := range g.Pkgs {
g.Pkg = pkg
g.Buf.Reset()
err := g.Find()
if err != nil {
return fmt.Errorf("typegen: Generate: error finding types for package %q: %w", pkg.Name, err)
}
g.PrintHeader()
has, err := g.Generate()
if !has {
continue
}
if err != nil {
return fmt.Errorf("typegen: Generate: error generating code for package %q: %w", pkg.Name, err)
}
err = g.Write()
if err != nil {
return fmt.Errorf("typegen: Generate: error writing code for package %q: %w", pkg.Name, err)
}
}
return nil
}
// 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 types provides type information for Go types, methods,
// and functions.
package types
import (
"cmp"
"reflect"
"slices"
"strings"
"sync/atomic"
"cogentcore.org/core/base/reflectx"
)
var (
// Types is a type registry, initialized to contain all builtin types. New types
// can be added with [AddType]. The key is the long type name: package/path.Type,
// e.g., cogentcore.org/core/core.Button.
Types = map[string]*Type{}
// typeIDCounter is an atomically incremented uint64 used
// for assigning new [Type.ID] numbers.
typeIDCounter uint64
)
func init() {
addBuiltin[bool]("bool")
addBuiltin[complex64]("complex64")
addBuiltin[complex128]("complex128")
addBuiltin[float32]("float32")
addBuiltin[float64]("float64")
addBuiltin[int]("int")
addBuiltin[int64]("int8")
addBuiltin[int16]("int16")
addBuiltin[int32]("int32")
addBuiltin[int64]("int64")
addBuiltin[string]("string")
addBuiltin[uint]("uint")
addBuiltin[uint8]("uint8")
addBuiltin[uint16]("uint16")
addBuiltin[uint32]("uint32")
addBuiltin[uint64]("uint64")
addBuiltin[uint64]("uintptr")
}
// addBuiltin adds the given builtin type with the given name to the type registry.
func addBuiltin[T any](name string) {
var v T
AddType(&Type{Name: name, IDName: name, Instance: v})
}
// TypeByName returns a Type by name (package/path.Type, e.g., cogentcore.org/core/core.Button),
func TypeByName(name string) *Type {
return Types[name]
}
// TypeByValue returns the [Type] of the given value
func TypeByValue(v any) *Type {
return TypeByName(TypeNameValue(v))
}
// TypeByReflectType returns the [Type] of the given reflect type
func TypeByReflectType(typ reflect.Type) *Type {
return TypeByName(TypeName(typ))
}
// For returns the [Type] of the generic type parameter,
// setting its [Type.Instance] to a new(T) if it is nil.
func For[T any]() *Type {
var v T
t := TypeByValue(v)
if t != nil && t.Instance == nil {
t.Instance = new(T)
}
return t
}
// AddType adds a constructed [Type] to the registry
// and returns it. This sets the ID.
func AddType(typ *Type) *Type {
typ.ID = atomic.AddUint64(&typeIDCounter, 1)
Types[typ.Name] = typ
return typ
}
// TypeName returns the long, full package-path qualified type name.
// This is guaranteed to be unique and used for the Types registry.
func TypeName(typ reflect.Type) string {
return reflectx.LongTypeName(typ)
}
// TypeNameValue returns the long, full package-path qualified type name
// of the given Go value. Automatically finds the non-pointer base type.
// This is guaranteed to be unique and used for the Types registry.
func TypeNameValue(v any) string {
typ := reflectx.Underlying(reflect.ValueOf(v)).Type()
return TypeName(typ)
}
// BuiltinTypes returns all of the builtin types in the type registry.
func BuiltinTypes() []*Type {
res := []*Type{}
for _, t := range Types {
if !strings.Contains(t.Name, ".") {
res = append(res, t)
}
}
slices.SortFunc(res, func(a, b *Type) int {
return cmp.Compare(a.Name, b.Name)
})
return res
}
// GetDoc gets the documentation for the given value with the given parent struct, field, and label.
// The value, parent value, and field may be nil/invalid. GetDoc uses the given label to format
// the documentation with [FormatDoc] before returning it.
func GetDoc(value, parent reflect.Value, field reflect.StructField, label string) (string, bool) {
// if we are not part of a struct, we just get the documentation for our type
if !parent.IsValid() {
if !value.IsValid() {
return "", false
}
rtyp := reflectx.NonPointerType(value.Type())
typ := TypeByName(TypeName(rtyp))
if typ == nil {
return "", false
}
return FormatDoc(typ.Doc, rtyp.Name(), label), true
}
// otherwise, we get our field documentation in our parent
f := GetField(parent, field.Name)
if f != nil {
return FormatDoc(f.Doc, field.Name, label), true
}
// if we aren't in the type registry, we fall back on struct tag
doc, ok := field.Tag.Lookup("doc")
if !ok {
return "", false
}
return FormatDoc(doc, field.Name, label), true
}
// FormatDoc formats the given Go documentation string for an identifier with the given
// CamelCase name and intended label. It replaces the name with the label and cleans
// up trailing punctuation.
func FormatDoc(doc, name, label string) string {
doc = strings.ReplaceAll(doc, name, label)
// if we only have one period, get rid of it if it is at the end
if strings.Count(doc, ".") == 1 {
doc = strings.TrimSuffix(doc, ".")
}
return doc
}
// Copyright (c) 2021, 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 undo package provides a generic undo / redo functionality based on `[]string`
representations of any kind of state representation (typically JSON dump of the 'document'
state). It stores the compact diffs from one state change to the next, with raw copies saved
at infrequent intervals to tradeoff cost of computing diffs.
In addition state (which is optional on any given step), a description of the action
and arbitrary string-encoded data can be saved with each record. Thus, for cases
where the state doesn't change, you can just save some data about the action sufficient
to undo / redo it.
A new record must be saved of the state just *before* an action takes place
and the nature of the action taken.
Thus, undoing the action restores the state to that prior state.
Redoing the action means restoring the state *after* the action.
This means that the first Undo action must save the current state
before doing the undo.
The Index is always on the last state saved, which will then be the one
that would be undone for an undo action.
*/
package undo
import (
"log"
"strings"
"sync"
"cogentcore.org/core/texteditor/text"
)
// DefaultRawInterval is interval for saving raw state -- need to do this
// at some interval to prevent having it take too long to compute patches
// from all the diffs.
var DefaultRawInterval = 50
// Record is one undo record, associated with one action that changed state from one to next.
// The state saved in this record is the state *before* the action took place.
// The state is either saved as a Raw value or as a diff Patch to the previous state.
type Record struct {
// description of this action, for user to see
Action string
// action data, encoded however you want -- some undo records can just be about this action data that can be interpreted to Undo / Redo a non-state-changing action
Data string
// if present, then direct save of full state -- do this at intervals to speed up computing prior states
Raw []string
// patch to get from previous record to this one
Patch text.Patch
// this record is an UndoSave, when Undo first called from end of stack
UndoSave bool
}
// Init sets the action and data in a record -- overwriting any prior values
func (rc *Record) Init(action, data string) {
rc.Action = action
rc.Data = data
rc.Patch = nil
rc.Raw = nil
rc.UndoSave = false
}
// Stack is the undo stack manager that manages the undo and redo process.
type Stack struct {
// current index in the undo records -- this is the record that will be undone if user hits undo
Index int
// the list of saved state / action records
Records []*Record
// interval for saving raw data -- need to do this at some interval to prevent having it take too long to compute patches from all the diffs.
RawInterval int
// mutex that protects updates -- we do diff computation as a separate goroutine so it is instant from perspective of UI
Mu sync.Mutex
}
// RecState returns the state for given index, reconstructing from diffs
// as needed. Must be called under lock.
func (us *Stack) RecState(idx int) []string {
stidx := 0
var cdt []string
for i := idx; i >= 0; i-- {
r := us.Records[i]
if r.Raw != nil {
stidx = i
cdt = r.Raw
break
}
}
for i := stidx + 1; i <= idx; i++ {
r := us.Records[i]
if r.Patch != nil {
cdt = r.Patch.Apply(cdt)
}
}
return cdt
}
// Save saves a new action as next action to be undone, with given action
// data and current full state of the system (either of which are optional).
// The state must be available for saving -- we do not copy in case we save the
// full raw copy.
func (us *Stack) Save(action, data string, state []string) {
us.Mu.Lock() // we start lock
if us.Records == nil {
if us.RawInterval == 0 {
us.RawInterval = DefaultRawInterval
}
us.Records = make([]*Record, 1)
us.Index = 0
nr := &Record{Action: action, Data: data, Raw: state}
us.Records[0] = nr
us.Mu.Unlock()
return
}
// recs will be [old..., Index] after this
us.Index++
var nr *Record
if len(us.Records) > us.Index {
us.Records = us.Records[:us.Index+1]
nr = us.Records[us.Index]
} else if len(us.Records) == us.Index {
nr = &Record{}
us.Records = append(us.Records, nr)
} else {
log.Printf("undo.Stack error: index: %d > len(um.Recs): %d\n", us.Index, len(us.Records))
us.Index = len(us.Records)
nr = &Record{}
us.Records = append(us.Records, nr)
}
nr.Init(action, data)
if state == nil {
us.Mu.Unlock()
return
}
go us.SaveState(nr, us.Index, state) // fork off save -- it will unlock when done
// now we return straight away, with lock still held
}
// MustSaveUndoStart returns true if the current state must be saved as the start of
// the first Undo action when at the end of the stack. If this returns true, then
// call SaveUndoStart. It sets a special flag on the record.
func (us *Stack) MustSaveUndoStart() bool {
return us.Index == len(us.Records)-1
}
// SaveUndoStart saves the current state -- call if MustSaveUndoStart is true.
// Sets a special flag for this record, and action, data are empty.
// Does NOT increment the index, so next undo is still as expected.
func (us *Stack) SaveUndoStart(state []string) {
us.Mu.Lock()
nr := &Record{UndoSave: true}
us.Records = append(us.Records, nr)
us.SaveState(nr, us.Index+1, state) // do it now because we need to immediately do Undo, does unlock
}
// SaveReplace replaces the current Undo record with new state,
// instead of creating a new record. This is useful for when
// you have a stream of the same type of manipulations
// and just want to save the last (it is better to handle that case
// up front as saving the state can be relatively expensive, but
// in some cases it is not possible).
func (us *Stack) SaveReplace(action, data string, state []string) {
us.Mu.Lock()
nr := us.Records[us.Index]
go us.SaveState(nr, us.Index, state)
}
// SaveState saves given record of state at given index
func (us *Stack) SaveState(nr *Record, idx int, state []string) {
if idx%us.RawInterval == 0 {
nr.Raw = state
us.Mu.Unlock()
return
}
prv := us.RecState(idx - 1)
dif := text.DiffLines(prv, state)
nr.Patch = dif.ToPatch(state)
us.Mu.Unlock()
}
// HasUndoAvail returns true if there is at least one undo record available.
// This does NOT get the lock -- may rarely be inaccurate but is used for
// gui enabling so not such a big deal.
func (us *Stack) HasUndoAvail() bool {
return us.Index >= 0
}
// HasRedoAvail returns true if there is at least one redo record available.
// This does NOT get the lock -- may rarely be inaccurate but is used for
// GUI enabling so not such a big deal.
func (us *Stack) HasRedoAvail() bool {
return us.Index < len(us.Records)-2
}
// Undo returns the action, action data, and state at the current index
// and decrements the index to the previous record.
// This state is the state just prior to the action.
// If already at the start (Index = -1) then returns empty everything
// Before calling, first check MustSaveUndoStart() -- if false, then you need
// to call SaveUndoStart() so that the state just before Undoing can be redone!
func (us *Stack) Undo() (action, data string, state []string) {
us.Mu.Lock()
if us.Index < 0 || us.Index >= len(us.Records) {
us.Mu.Unlock()
return
}
rec := us.Records[us.Index]
action = rec.Action
data = rec.Data
state = us.RecState(us.Index)
us.Index--
us.Mu.Unlock()
return
}
// UndoTo returns the action, action data, and state at the given index
// and decrements the index to the previous record.
// If idx is out of range then returns empty everything
func (us *Stack) UndoTo(idx int) (action, data string, state []string) {
us.Mu.Lock()
if idx < 0 || idx >= len(us.Records) {
us.Mu.Unlock()
return
}
rec := us.Records[idx]
action = rec.Action
data = rec.Data
state = us.RecState(idx)
us.Index = idx - 1
us.Mu.Unlock()
return
}
// Redo returns the action, data at the *next* index, and the state at the
// index *after that*.
// returning nil if already at end of saved records.
func (us *Stack) Redo() (action, data string, state []string) {
us.Mu.Lock()
if us.Index >= len(us.Records)-2 {
us.Mu.Unlock()
return
}
us.Index++
rec := us.Records[us.Index] // action being redone is this one
action = rec.Action
data = rec.Data
state = us.RecState(us.Index + 1) // state is the one *after* it
us.Mu.Unlock()
return
}
// RedoTo returns the action, action data, and state at the given index,
// returning nil if already at end of saved records.
func (us *Stack) RedoTo(idx int) (action, data string, state []string) {
us.Mu.Lock()
if idx >= len(us.Records)-1 || idx <= 0 {
us.Mu.Unlock()
return
}
us.Index = idx
rec := us.Records[idx]
action = rec.Action
data = rec.Data
state = us.RecState(idx + 1)
us.Mu.Unlock()
return
}
// Reset resets the undo state
func (us *Stack) Reset() {
us.Mu.Lock()
us.Records = nil
us.Index = 0
us.Mu.Unlock()
}
// UndoList returns the list actions in order from the most recent back in time
// suitable for a menu of actions to undo.
func (us *Stack) UndoList() []string {
al := make([]string, us.Index)
for i := us.Index; i >= 0; i-- {
al[us.Index-i] = us.Records[i].Action
}
return al
}
// RedoList returns the list actions in order from the current forward to end
// suitable for a menu of actions to redo
func (us *Stack) RedoList() []string {
nl := len(us.Records)
if us.Index >= nl-2 {
return nil
}
st := us.Index + 1
n := (nl - 1) - st
al := make([]string, n)
for i := st; i < nl-1; i++ {
al[i-st] = us.Records[i].Action
}
return al
}
// MemUsed reports the amount of memory used for record
func (rc *Record) MemUsed() int {
mem := 0
if rc.Raw != nil {
for _, s := range rc.Raw {
mem += len(s)
}
} else {
for _, pr := range rc.Patch {
for _, s := range pr.Blines {
mem += len(s)
}
}
}
return mem
}
// MemStats reports the memory usage statistics.
// if details is true, each record is reported.
func (us *Stack) MemStats(details bool) string {
sb := strings.Builder{}
// TODO(kai): add this back once we figure out how to do core.FileSize
/*
sum := 0
for i, r := range um.Recs {
mem := r.MemUsed()
sum += mem
if details {
sb.WriteString(fmt.Sprintf("%d\t%s\tmem:%s\n", i, r.Action, core.FileSize(mem).String()))
}
}
sb.WriteString(fmt.Sprintf("Total: %s\n", core.FileSize(sum).String()))
*/
return sb.String()
}
// Code generated by "core generate"; DO NOT EDIT.
package video
import (
"cogentcore.org/core/tree"
"cogentcore.org/core/types"
"github.com/cogentcore/reisen"
)
var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/video.Video", IDName: "video", Doc: "Video represents a video playback widget without any controls.\nSee [Player] for a version with controls.", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Embeds: []types.Field{{Name: "WidgetBase"}}, Fields: []types.Field{{Name: "Media", Doc: "Media is the video media."}, {Name: "Rotation", Doc: "degrees of rotation to apply to the video images\n90 = left 90, -90 = right 90"}, {Name: "Stop", Doc: "setting this to true will stop the playing"}, {Name: "frameBuffer"}, {Name: "lastFrame", Doc: "last frame we have rendered"}, {Name: "frameTarg", Doc: "target frame number to be played"}, {Name: "framePlayed", Doc: "actual frame number displayed"}, {Name: "frameStop", Doc: "frame number to stop playing at, if > 0"}}})
// NewVideo returns a new [Video] with the given optional parent:
// Video represents a video playback widget without any controls.
// See [Player] for a version with controls.
func NewVideo(parent ...tree.Node) *Video { return tree.New[Video](parent...) }
// SetMedia sets the [Video.Media]:
// Media is the video media.
func (t *Video) SetMedia(v *reisen.Media) *Video { t.Media = v; return t }
// SetRotation sets the [Video.Rotation]:
// degrees of rotation to apply to the video images
// 90 = left 90, -90 = right 90
func (t *Video) SetRotation(v float32) *Video { t.Rotation = v; return t }
// SetStop sets the [Video.Stop]:
// setting this to true will stop the playing
func (t *Video) SetStop(v bool) *Video { t.Stop = v; return t }
// 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 video implements a video player widget in Cogent Core.
package video
//go:generate core generate
import (
"bytes"
"encoding/binary"
"image"
"image/draw"
"time"
"cogentcore.org/core/core"
"cogentcore.org/core/system"
"github.com/cogentcore/reisen"
"github.com/faiface/beep"
"github.com/faiface/beep/speaker"
)
// Video represents a video playback widget without any controls.
// See [Player] for a version with controls.
type Video struct { //types:add
core.WidgetBase
// Media is the video media.
Media *reisen.Media
// degrees of rotation to apply to the video images
// 90 = left 90, -90 = right 90
Rotation float32
// setting this to true will stop the playing
Stop bool
frameBuffer <-chan *image.RGBA
// last frame we have rendered
lastFrame *image.RGBA
// target frame number to be played
frameTarg int
// actual frame number displayed
framePlayed int
// frame number to stop playing at, if > 0
frameStop int
}
func (v *Video) OnAdd() {
v.WidgetBase.OnAdd()
v.Scene.AddDirectRender(v)
}
func (v *Video) Destroy() {
v.Scene.DeleteDirectRender(v)
// v.Media.Destroy()
// todo: frameBuffer
v.WidgetBase.Destroy()
}
// RenderDraw draws the current image to RenderWindow drawer
func (v *Video) RenderDraw(drw system.Drawer, op draw.Op) {
if !v.IsVisible() {
return
}
frame := v.lastFrame
unchanged := true
if v.framePlayed >= v.frameTarg && frame == nil {
return
}
newFrame, ok := <-v.frameBuffer
if !ok && frame == nil {
v.Stop = true
return
}
if ok {
frame = newFrame
v.lastFrame = frame
unchanged = false
}
v.framePlayed++
bb, sbb, empty := v.DirectRenderDrawBBoxes(frame.Bounds())
if empty {
return
}
drw.Scale(bb, frame, sbb, v.Rotation, draw.Src, unchanged)
}
// Open opens the video specified by the given filepath.
func (v *Video) Open(fpath string) error {
// Initialize the audio speaker.
err := speaker.Init(sampleRate, SpeakerSampleRate.N(time.Second/10))
if err != nil {
return err
}
media, err := reisen.NewMedia(fpath)
if err != nil {
return err
}
v.Media = media
return nil
}
// Play starts playing the video at the specified size. Values of 0
// indicate to use the inherent size of the video for that dimension.
func (v *Video) Play(width, height float32) error {
videoFPS, _ := v.Media.Streams()[0].FrameRate()
if videoFPS == 0 || videoFPS > 100 {
videoFPS = 30
}
// seconds per frame for frame ticker
spf := time.Duration(float64(time.Second) / float64(videoFPS))
// fmt.Println(videoFPS, spf)
// Start decoding streams.
var sampleSource <-chan [2]float64
frameBuffer, sampleSource, errChan, err := v.ReadVideoAndAudio()
if err != nil {
return err
}
v.frameBuffer = frameBuffer
start := time.Now()
v.Stop = false
// todo: should set a v.frameStop target, and also get this as an arg
// to set for playing just a snippet. probably need more general timestamp etc stuff there.
v.frameTarg = 0
v.framePlayed = 0
v.NeedsRender()
// Start playing audio samples.
speaker.Play(v.StreamSamples(sampleSource))
_ = errChan
go func() {
for {
// todo: this is causing everything to stop on my sample video that
// has an error a ways into it -- maybe need a buffered chan or something?
// also see commented-out parts where it tries to send the errors
// or something?
// select {
// case err, ok := <-errChan:
// if ok {
// fmt.Println(err)
// // return err
// }
// default:
if v.Stop {
return
}
d := time.Now().Sub(start)
td := time.Duration(v.frameTarg) * spf
shouldStop := v.frameStop > 0 && v.frameTarg >= v.frameStop
if d > td && !shouldStop {
v.AsyncLock()
v.frameTarg++
v.NeedsRender()
v.AsyncUnlock()
} else if v.frameTarg > v.framePlayed {
v.AsyncLock()
v.NeedsRender()
v.AsyncUnlock()
} else if shouldStop {
return
} else {
time.Sleep(td - d)
}
}
}()
return nil
}
const (
frameBufferSize = 1024
sampleRate = 44100
channelCount = 2
bitDepth = 8
sampleBufferSize = 32 * channelCount * bitDepth * 1024
SpeakerSampleRate beep.SampleRate = 44100
)
// ReadVideoAndAudio reads video and audio frames from the opened media and
// sends the decoded data to che channels to be played.
func (v *Video) ReadVideoAndAudio() (<-chan *image.RGBA, <-chan [2]float64, chan error, error) {
frameBuffer := make(chan *image.RGBA, frameBufferSize)
sampleBuffer := make(chan [2]float64, sampleBufferSize)
errs := make(chan error)
err := v.Media.OpenDecode()
if err != nil {
return nil, nil, nil, err
}
videoStream := v.Media.VideoStreams()[0]
err = videoStream.Open()
if err != nil {
return nil, nil, nil, err
}
audioStream := v.Media.AudioStreams()[0]
err = audioStream.Open()
if err != nil {
return nil, nil, nil, err
}
go func() {
for {
packet, gotPacket, err := v.Media.ReadPacket()
if err != nil {
go func(err error) {
errs <- err
}(err)
}
if !gotPacket {
break
}
switch packet.Type() {
case reisen.StreamVideo:
s := v.Media.Streams()[packet.StreamIndex()].(*reisen.VideoStream)
videoFrame, gotFrame, err := s.ReadVideoFrame()
_ = err
// note: this is causing a panic send on closed channel
// if err != nil {
// go func(err error) {
// errs <- err
// }(err)
// }
if !gotFrame {
break
}
if videoFrame == nil {
continue
}
frameBuffer <- videoFrame.Image()
case reisen.StreamAudio:
s := v.Media.Streams()[packet.StreamIndex()].(*reisen.AudioStream)
audioFrame, gotFrame, err := s.ReadAudioFrame()
// note: this is causing a panic send on closed channel
// if err != nil {
// go func(err error) {
// errs <- err
// }(err)
// }
if !gotFrame {
break
}
if audioFrame == nil {
continue
}
// Turn the raw byte data into
// audio samples of type [2]float64.
reader := bytes.NewReader(audioFrame.Data())
// See the README.md file for
// detailed scheme of the sample structure.
for reader.Len() > 0 {
sample := [2]float64{0, 0}
var result float64
err = binary.Read(reader, binary.LittleEndian, &result)
if err != nil {
go func(err error) {
errs <- err
}(err)
}
sample[0] = result
err = binary.Read(reader, binary.LittleEndian, &result)
if err != nil {
go func(err error) {
errs <- err
}(err)
}
sample[1] = result
sampleBuffer <- sample
}
}
}
videoStream.Close()
audioStream.Close()
v.Media.CloseDecode()
close(frameBuffer)
close(sampleBuffer)
close(errs)
}()
return frameBuffer, sampleBuffer, errs, nil
}
// StreamSamples creates a new custom streamer for
// playing audio samples provided by the source channel.
//
// See https://github.com/faiface/beep/wiki/Making-own-streamers
// for reference.
func (v *Video) StreamSamples(sampleSource <-chan [2]float64) beep.Streamer {
return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) {
numRead := 0
for i := 0; i < len(samples); i++ {
sample, ok := <-sampleSource
if !ok {
numRead = i + 1
break
}
samples[i] = sample
numRead++
}
if numRead < len(samples) {
return numRead, false
}
return numRead, true
})
}
// Code generated by 'yaegi extract cogentcore.org/core/base/errors'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/base/errors"
"github.com/cogentcore/yaegi/interp"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/base/errors/errors"] = map[string]reflect.Value{
// function, constant and variable definitions
"As": reflect.ValueOf(errors.As),
"CallerInfo": reflect.ValueOf(errors.CallerInfo),
"ErrUnsupported": reflect.ValueOf(&errors.ErrUnsupported).Elem(),
"Is": reflect.ValueOf(errors.Is),
"Join": reflect.ValueOf(errors.Join),
"Log": reflect.ValueOf(errors.Log),
"Log1": reflect.ValueOf(interp.GenericFunc("func Log1[T any](v T, err error) T { //yaegi:add\n\tif err != nil {\n\t\tslog.Error(err.Error() + \" | \" + CallerInfo())\n\t}\n\treturn v\n}")),
"Must": reflect.ValueOf(errors.Must),
"New": reflect.ValueOf(errors.New),
"Unwrap": reflect.ValueOf(errors.Unwrap),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/base/fileinfo'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/base/fileinfo"
"go/constant"
"go/token"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/base/fileinfo/fileinfo"] = map[string]reflect.Value{
// function, constant and variable definitions
"Aac": reflect.ValueOf(fileinfo.Aac),
"Ada": reflect.ValueOf(fileinfo.Ada),
"Any": reflect.ValueOf(fileinfo.Any),
"AnyArchive": reflect.ValueOf(fileinfo.AnyArchive),
"AnyAudio": reflect.ValueOf(fileinfo.AnyAudio),
"AnyBackup": reflect.ValueOf(fileinfo.AnyBackup),
"AnyBin": reflect.ValueOf(fileinfo.AnyBin),
"AnyCode": reflect.ValueOf(fileinfo.AnyCode),
"AnyData": reflect.ValueOf(fileinfo.AnyData),
"AnyDoc": reflect.ValueOf(fileinfo.AnyDoc),
"AnyExe": reflect.ValueOf(fileinfo.AnyExe),
"AnyFolder": reflect.ValueOf(fileinfo.AnyFolder),
"AnyFont": reflect.ValueOf(fileinfo.AnyFont),
"AnyImage": reflect.ValueOf(fileinfo.AnyImage),
"AnyKnown": reflect.ValueOf(fileinfo.AnyKnown),
"AnyModel": reflect.ValueOf(fileinfo.AnyModel),
"AnySheet": reflect.ValueOf(fileinfo.AnySheet),
"AnyText": reflect.ValueOf(fileinfo.AnyText),
"AnyVideo": reflect.ValueOf(fileinfo.AnyVideo),
"Archive": reflect.ValueOf(fileinfo.Archive),
"Audio": reflect.ValueOf(fileinfo.Audio),
"AvailableMimes": reflect.ValueOf(&fileinfo.AvailableMimes).Elem(),
"Avi": reflect.ValueOf(fileinfo.Avi),
"BZip": reflect.ValueOf(fileinfo.BZip),
"Backup": reflect.ValueOf(fileinfo.Backup),
"Bash": reflect.ValueOf(fileinfo.Bash),
"BibTeX": reflect.ValueOf(fileinfo.BibTeX),
"Bin": reflect.ValueOf(fileinfo.Bin),
"Bmp": reflect.ValueOf(fileinfo.Bmp),
"C": reflect.ValueOf(fileinfo.C),
"CSharp": reflect.ValueOf(fileinfo.CSharp),
"CategoriesN": reflect.ValueOf(fileinfo.CategoriesN),
"CategoriesValues": reflect.ValueOf(fileinfo.CategoriesValues),
"CategoryFromMime": reflect.ValueOf(fileinfo.CategoryFromMime),
"Code": reflect.ValueOf(fileinfo.Code),
"Color": reflect.ValueOf(fileinfo.Color),
"CopyFile": reflect.ValueOf(fileinfo.CopyFile),
"Cosh": reflect.ValueOf(fileinfo.Cosh),
"Csh": reflect.ValueOf(fileinfo.Csh),
"Css": reflect.ValueOf(fileinfo.Css),
"Csv": reflect.ValueOf(fileinfo.Csv),
"CustomMimes": reflect.ValueOf(&fileinfo.CustomMimes).Elem(),
"D": reflect.ValueOf(fileinfo.D),
"Data": reflect.ValueOf(fileinfo.Data),
"DataCsv": reflect.ValueOf(constant.MakeFromLiteral("\"text/csv\"", token.STRING, 0)),
"DataJson": reflect.ValueOf(constant.MakeFromLiteral("\"application/json\"", token.STRING, 0)),
"DataXml": reflect.ValueOf(constant.MakeFromLiteral("\"application/xml\"", token.STRING, 0)),
"Diff": reflect.ValueOf(fileinfo.Diff),
"Dmg": reflect.ValueOf(fileinfo.Dmg),
"Doc": reflect.ValueOf(fileinfo.Doc),
"EBook": reflect.ValueOf(fileinfo.EBook),
"EPub": reflect.ValueOf(fileinfo.EPub),
"Eiffel": reflect.ValueOf(fileinfo.Eiffel),
"Erlang": reflect.ValueOf(fileinfo.Erlang),
"Exe": reflect.ValueOf(fileinfo.Exe),
"ExtKnown": reflect.ValueOf(fileinfo.ExtKnown),
"ExtMimeMap": reflect.ValueOf(&fileinfo.ExtMimeMap).Elem(),
"FSharp": reflect.ValueOf(fileinfo.FSharp),
"Filenames": reflect.ValueOf(fileinfo.Filenames),
"Flac": reflect.ValueOf(fileinfo.Flac),
"Folder": reflect.ValueOf(fileinfo.Folder),
"Font": reflect.ValueOf(fileinfo.Font),
"Forth": reflect.ValueOf(fileinfo.Forth),
"Fortran": reflect.ValueOf(fileinfo.Fortran),
"GZip": reflect.ValueOf(fileinfo.GZip),
"Gif": reflect.ValueOf(fileinfo.Gif),
"Gimp": reflect.ValueOf(fileinfo.Gimp),
"Go": reflect.ValueOf(fileinfo.Go),
"GraphVis": reflect.ValueOf(fileinfo.GraphVis),
"Haskell": reflect.ValueOf(fileinfo.Haskell),
"Heic": reflect.ValueOf(fileinfo.Heic),
"Heif": reflect.ValueOf(fileinfo.Heif),
"Html": reflect.ValueOf(fileinfo.Html),
"ICal": reflect.ValueOf(fileinfo.ICal),
"Icons": reflect.ValueOf(&fileinfo.Icons).Elem(),
"Image": reflect.ValueOf(fileinfo.Image),
"Ini": reflect.ValueOf(fileinfo.Ini),
"IsMatch": reflect.ValueOf(fileinfo.IsMatch),
"IsMatchList": reflect.ValueOf(fileinfo.IsMatchList),
"Java": reflect.ValueOf(fileinfo.Java),
"JavaScript": reflect.ValueOf(fileinfo.JavaScript),
"Jpeg": reflect.ValueOf(fileinfo.Jpeg),
"Json": reflect.ValueOf(fileinfo.Json),
"KnownByName": reflect.ValueOf(fileinfo.KnownByName),
"KnownFromFile": reflect.ValueOf(fileinfo.KnownFromFile),
"KnownMimes": reflect.ValueOf(&fileinfo.KnownMimes).Elem(),
"KnownN": reflect.ValueOf(fileinfo.KnownN),
"KnownValues": reflect.ValueOf(fileinfo.KnownValues),
"Lisp": reflect.ValueOf(fileinfo.Lisp),
"Lua": reflect.ValueOf(fileinfo.Lua),
"MSExcel": reflect.ValueOf(fileinfo.MSExcel),
"MSPowerpoint": reflect.ValueOf(fileinfo.MSPowerpoint),
"MSWord": reflect.ValueOf(fileinfo.MSWord),
"Makefile": reflect.ValueOf(fileinfo.Makefile),
"Markdown": reflect.ValueOf(fileinfo.Markdown),
"Mathematica": reflect.ValueOf(fileinfo.Mathematica),
"Matlab": reflect.ValueOf(fileinfo.Matlab),
"MergeAvailableMimes": reflect.ValueOf(fileinfo.MergeAvailableMimes),
"Midi": reflect.ValueOf(fileinfo.Midi),
"MimeFromFile": reflect.ValueOf(fileinfo.MimeFromFile),
"MimeFromKnown": reflect.ValueOf(fileinfo.MimeFromKnown),
"MimeKnown": reflect.ValueOf(fileinfo.MimeKnown),
"MimeNoChar": reflect.ValueOf(fileinfo.MimeNoChar),
"MimeString": reflect.ValueOf(fileinfo.MimeString),
"MimeSub": reflect.ValueOf(fileinfo.MimeSub),
"MimeTop": reflect.ValueOf(fileinfo.MimeTop),
"Model": reflect.ValueOf(fileinfo.Model),
"Mov": reflect.ValueOf(fileinfo.Mov),
"Mp3": reflect.ValueOf(fileinfo.Mp3),
"Mp4": reflect.ValueOf(fileinfo.Mp4),
"Mpeg": reflect.ValueOf(fileinfo.Mpeg),
"Multipart": reflect.ValueOf(fileinfo.Multipart),
"NewFileInfo": reflect.ValueOf(fileinfo.NewFileInfo),
"NewFileInfoType": reflect.ValueOf(fileinfo.NewFileInfoType),
"Number": reflect.ValueOf(fileinfo.Number),
"OCaml": reflect.ValueOf(fileinfo.OCaml),
"Obj": reflect.ValueOf(fileinfo.Obj),
"ObjC": reflect.ValueOf(fileinfo.ObjC),
"Ogg": reflect.ValueOf(fileinfo.Ogg),
"Ogv": reflect.ValueOf(fileinfo.Ogv),
"OpenPres": reflect.ValueOf(fileinfo.OpenPres),
"OpenSheet": reflect.ValueOf(fileinfo.OpenSheet),
"OpenText": reflect.ValueOf(fileinfo.OpenText),
"Pascal": reflect.ValueOf(fileinfo.Pascal),
"Pbm": reflect.ValueOf(fileinfo.Pbm),
"Pdf": reflect.ValueOf(fileinfo.Pdf),
"Perl": reflect.ValueOf(fileinfo.Perl),
"Pgm": reflect.ValueOf(fileinfo.Pgm),
"Php": reflect.ValueOf(fileinfo.Php),
"PlainText": reflect.ValueOf(fileinfo.PlainText),
"Png": reflect.ValueOf(fileinfo.Png),
"Pnm": reflect.ValueOf(fileinfo.Pnm),
"Postscript": reflect.ValueOf(fileinfo.Postscript),
"Ppm": reflect.ValueOf(fileinfo.Ppm),
"Prolog": reflect.ValueOf(fileinfo.Prolog),
"Protobuf": reflect.ValueOf(fileinfo.Protobuf),
"Python": reflect.ValueOf(fileinfo.Python),
"R": reflect.ValueOf(fileinfo.R),
"Rtf": reflect.ValueOf(fileinfo.Rtf),
"Ruby": reflect.ValueOf(fileinfo.Ruby),
"Rust": reflect.ValueOf(fileinfo.Rust),
"Scala": reflect.ValueOf(fileinfo.Scala),
"SevenZ": reflect.ValueOf(fileinfo.SevenZ),
"Shar": reflect.ValueOf(fileinfo.Shar),
"Sheet": reflect.ValueOf(fileinfo.Sheet),
"StandardMimes": reflect.ValueOf(&fileinfo.StandardMimes).Elem(),
"String": reflect.ValueOf(fileinfo.String),
"Svg": reflect.ValueOf(fileinfo.Svg),
"Table": reflect.ValueOf(fileinfo.Table),
"Tar": reflect.ValueOf(fileinfo.Tar),
"Tcl": reflect.ValueOf(fileinfo.Tcl),
"TeX": reflect.ValueOf(fileinfo.TeX),
"Tensor": reflect.ValueOf(fileinfo.Tensor),
"Texinfo": reflect.ValueOf(fileinfo.Texinfo),
"Text": reflect.ValueOf(fileinfo.Text),
"TextPlain": reflect.ValueOf(constant.MakeFromLiteral("\"text/plain\"", token.STRING, 0)),
"Tiff": reflect.ValueOf(fileinfo.Tiff),
"Toml": reflect.ValueOf(fileinfo.Toml),
"Trash": reflect.ValueOf(fileinfo.Trash),
"Troff": reflect.ValueOf(fileinfo.Troff),
"TrueType": reflect.ValueOf(fileinfo.TrueType),
"Tsv": reflect.ValueOf(fileinfo.Tsv),
"Unknown": reflect.ValueOf(fileinfo.Unknown),
"UnknownCategory": reflect.ValueOf(fileinfo.UnknownCategory),
"Uri": reflect.ValueOf(fileinfo.Uri),
"VCal": reflect.ValueOf(fileinfo.VCal),
"VCard": reflect.ValueOf(fileinfo.VCard),
"Video": reflect.ValueOf(fileinfo.Video),
"Vrml": reflect.ValueOf(fileinfo.Vrml),
"Wav": reflect.ValueOf(fileinfo.Wav),
"WebOpenFont": reflect.ValueOf(fileinfo.WebOpenFont),
"Wmv": reflect.ValueOf(fileinfo.Wmv),
"X3d": reflect.ValueOf(fileinfo.X3d),
"Xbm": reflect.ValueOf(fileinfo.Xbm),
"Xml": reflect.ValueOf(fileinfo.Xml),
"Xpm": reflect.ValueOf(fileinfo.Xpm),
"Xz": reflect.ValueOf(fileinfo.Xz),
"Yaml": reflect.ValueOf(fileinfo.Yaml),
"Zip": reflect.ValueOf(fileinfo.Zip),
// type definitions
"Categories": reflect.ValueOf((*fileinfo.Categories)(nil)),
"FileInfo": reflect.ValueOf((*fileinfo.FileInfo)(nil)),
"Known": reflect.ValueOf((*fileinfo.Known)(nil)),
"MimeType": reflect.ValueOf((*fileinfo.MimeType)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/base/fsx'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/base/fsx"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/base/fsx/fsx"] = map[string]reflect.Value{
// function, constant and variable definitions
"DirAndFile": reflect.ValueOf(fsx.DirAndFile),
"DirFS": reflect.ValueOf(fsx.DirFS),
"Dirs": reflect.ValueOf(fsx.Dirs),
"ExtSplit": reflect.ValueOf(fsx.ExtSplit),
"FileExists": reflect.ValueOf(fsx.FileExists),
"FileExistsFS": reflect.ValueOf(fsx.FileExistsFS),
"Filenames": reflect.ValueOf(fsx.Filenames),
"Files": reflect.ValueOf(fsx.Files),
"FindFilesOnPaths": reflect.ValueOf(fsx.FindFilesOnPaths),
"GoSrcDir": reflect.ValueOf(fsx.GoSrcDir),
"HasFile": reflect.ValueOf(fsx.HasFile),
"LatestMod": reflect.ValueOf(fsx.LatestMod),
"RelativeFilePath": reflect.ValueOf(fsx.RelativeFilePath),
"SplitRootPathFS": reflect.ValueOf(fsx.SplitRootPathFS),
"Sub": reflect.ValueOf(fsx.Sub),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/base/labels'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/base/labels"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/base/labels/labels"] = map[string]reflect.Value{
// function, constant and variable definitions
"FriendlyMapLabel": reflect.ValueOf(labels.FriendlyMapLabel),
"FriendlySliceLabel": reflect.ValueOf(labels.FriendlySliceLabel),
"FriendlyStructLabel": reflect.ValueOf(labels.FriendlyStructLabel),
"FriendlyTypeName": reflect.ValueOf(labels.FriendlyTypeName),
"ToLabel": reflect.ValueOf(labels.ToLabel),
"ToLabeler": reflect.ValueOf(labels.ToLabeler),
// type definitions
"Labeler": reflect.ValueOf((*labels.Labeler)(nil)),
"SliceLabeler": reflect.ValueOf((*labels.SliceLabeler)(nil)),
// interface wrapper definitions
"_Labeler": reflect.ValueOf((*_cogentcore_org_core_base_labels_Labeler)(nil)),
"_SliceLabeler": reflect.ValueOf((*_cogentcore_org_core_base_labels_SliceLabeler)(nil)),
}
}
// _cogentcore_org_core_base_labels_Labeler is an interface wrapper for Labeler type
type _cogentcore_org_core_base_labels_Labeler struct {
IValue interface{}
WLabel func() string
}
func (W _cogentcore_org_core_base_labels_Labeler) Label() string { return W.WLabel() }
// _cogentcore_org_core_base_labels_SliceLabeler is an interface wrapper for SliceLabeler type
type _cogentcore_org_core_base_labels_SliceLabeler struct {
IValue interface{}
WElemLabel func(idx int) string
}
func (W _cogentcore_org_core_base_labels_SliceLabeler) ElemLabel(idx int) string {
return W.WElemLabel(idx)
}
// Code generated by 'yaegi extract cogentcore.org/core/base/reflectx'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/base/reflectx"
"image/color"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/base/reflectx/reflectx"] = map[string]reflect.Value{
// function, constant and variable definitions
"CloneToType": reflect.ValueOf(reflectx.CloneToType),
"CopyMapRobust": reflect.ValueOf(reflectx.CopyMapRobust),
"CopySliceRobust": reflect.ValueOf(reflectx.CopySliceRobust),
"FormatDefault": reflect.ValueOf(reflectx.FormatDefault),
"IsNil": reflect.ValueOf(reflectx.IsNil),
"KindIsBasic": reflect.ValueOf(reflectx.KindIsBasic),
"KindIsNumber": reflect.ValueOf(reflectx.KindIsNumber),
"LongTypeName": reflect.ValueOf(reflectx.LongTypeName),
"MapAdd": reflect.ValueOf(reflectx.MapAdd),
"MapDelete": reflect.ValueOf(reflectx.MapDelete),
"MapDeleteAll": reflect.ValueOf(reflectx.MapDeleteAll),
"MapKeyType": reflect.ValueOf(reflectx.MapKeyType),
"MapSort": reflect.ValueOf(reflectx.MapSort),
"MapValueSort": reflect.ValueOf(reflectx.MapValueSort),
"MapValueType": reflect.ValueOf(reflectx.MapValueType),
"NonDefaultFields": reflect.ValueOf(reflectx.NonDefaultFields),
"NonNilNew": reflect.ValueOf(reflectx.NonNilNew),
"NonPointerType": reflect.ValueOf(reflectx.NonPointerType),
"NonPointerValue": reflect.ValueOf(reflectx.NonPointerValue),
"NumAllFields": reflect.ValueOf(reflectx.NumAllFields),
"OnePointerValue": reflect.ValueOf(reflectx.OnePointerValue),
"PointerValue": reflect.ValueOf(reflectx.PointerValue),
"SetFromDefaultTag": reflect.ValueOf(reflectx.SetFromDefaultTag),
"SetFromDefaultTags": reflect.ValueOf(reflectx.SetFromDefaultTags),
"SetMapRobust": reflect.ValueOf(reflectx.SetMapRobust),
"SetRobust": reflect.ValueOf(reflectx.SetRobust),
"ShortTypeName": reflect.ValueOf(reflectx.ShortTypeName),
"SliceDeleteAt": reflect.ValueOf(reflectx.SliceDeleteAt),
"SliceElementType": reflect.ValueOf(reflectx.SliceElementType),
"SliceElementValue": reflect.ValueOf(reflectx.SliceElementValue),
"SliceNewAt": reflect.ValueOf(reflectx.SliceNewAt),
"SliceSort": reflect.ValueOf(reflectx.SliceSort),
"StringJSON": reflect.ValueOf(reflectx.StringJSON),
"StructSliceSort": reflect.ValueOf(reflectx.StructSliceSort),
"StructTags": reflect.ValueOf(reflectx.StructTags),
"ToBool": reflect.ValueOf(reflectx.ToBool),
"ToFloat": reflect.ValueOf(reflectx.ToFloat),
"ToFloat32": reflect.ValueOf(reflectx.ToFloat32),
"ToInt": reflect.ValueOf(reflectx.ToInt),
"ToString": reflect.ValueOf(reflectx.ToString),
"ToStringPrec": reflect.ValueOf(reflectx.ToStringPrec),
"Underlying": reflect.ValueOf(reflectx.Underlying),
"UnderlyingPointer": reflect.ValueOf(reflectx.UnderlyingPointer),
"ValueIsDefault": reflect.ValueOf(reflectx.ValueIsDefault),
"ValueSliceSort": reflect.ValueOf(reflectx.ValueSliceSort),
"WalkFields": reflect.ValueOf(reflectx.WalkFields),
// type definitions
"SetAnyer": reflect.ValueOf((*reflectx.SetAnyer)(nil)),
"SetColorer": reflect.ValueOf((*reflectx.SetColorer)(nil)),
"SetStringer": reflect.ValueOf((*reflectx.SetStringer)(nil)),
"ShouldSaver": reflect.ValueOf((*reflectx.ShouldSaver)(nil)),
// interface wrapper definitions
"_SetAnyer": reflect.ValueOf((*_cogentcore_org_core_base_reflectx_SetAnyer)(nil)),
"_SetColorer": reflect.ValueOf((*_cogentcore_org_core_base_reflectx_SetColorer)(nil)),
"_SetStringer": reflect.ValueOf((*_cogentcore_org_core_base_reflectx_SetStringer)(nil)),
"_ShouldSaver": reflect.ValueOf((*_cogentcore_org_core_base_reflectx_ShouldSaver)(nil)),
}
}
// _cogentcore_org_core_base_reflectx_SetAnyer is an interface wrapper for SetAnyer type
type _cogentcore_org_core_base_reflectx_SetAnyer struct {
IValue interface{}
WSetAny func(v any) error
}
func (W _cogentcore_org_core_base_reflectx_SetAnyer) SetAny(v any) error { return W.WSetAny(v) }
// _cogentcore_org_core_base_reflectx_SetColorer is an interface wrapper for SetColorer type
type _cogentcore_org_core_base_reflectx_SetColorer struct {
IValue interface{}
WSetColor func(c color.Color)
}
func (W _cogentcore_org_core_base_reflectx_SetColorer) SetColor(c color.Color) { W.WSetColor(c) }
// _cogentcore_org_core_base_reflectx_SetStringer is an interface wrapper for SetStringer type
type _cogentcore_org_core_base_reflectx_SetStringer struct {
IValue interface{}
WSetString func(s string) error
}
func (W _cogentcore_org_core_base_reflectx_SetStringer) SetString(s string) error {
return W.WSetString(s)
}
// _cogentcore_org_core_base_reflectx_ShouldSaver is an interface wrapper for ShouldSaver type
type _cogentcore_org_core_base_reflectx_ShouldSaver struct {
IValue interface{}
WShouldSave func() bool
}
func (W _cogentcore_org_core_base_reflectx_ShouldSaver) ShouldSave() bool { return W.WShouldSave() }
// Code generated by 'yaegi extract cogentcore.org/core/colors/gradient'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/colors/gradient"
"cogentcore.org/core/math32"
"image"
"image/color"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/colors/gradient/gradient"] = map[string]reflect.Value{
// function, constant and variable definitions
"Apply": reflect.ValueOf(gradient.Apply),
"ApplyOpacity": reflect.ValueOf(gradient.ApplyOpacity),
"Cache": reflect.ValueOf(&gradient.Cache).Elem(),
"CopyFrom": reflect.ValueOf(gradient.CopyFrom),
"CopyOf": reflect.ValueOf(gradient.CopyOf),
"FromAny": reflect.ValueOf(gradient.FromAny),
"FromString": reflect.ValueOf(gradient.FromString),
"NewApplier": reflect.ValueOf(gradient.NewApplier),
"NewBase": reflect.ValueOf(gradient.NewBase),
"NewLinear": reflect.ValueOf(gradient.NewLinear),
"NewRadial": reflect.ValueOf(gradient.NewRadial),
"ObjectBoundingBox": reflect.ValueOf(gradient.ObjectBoundingBox),
"Pad": reflect.ValueOf(gradient.Pad),
"ReadXML": reflect.ValueOf(gradient.ReadXML),
"Reflect": reflect.ValueOf(gradient.Reflect),
"Repeat": reflect.ValueOf(gradient.Repeat),
"SpreadsN": reflect.ValueOf(gradient.SpreadsN),
"SpreadsValues": reflect.ValueOf(gradient.SpreadsValues),
"UnitsN": reflect.ValueOf(gradient.UnitsN),
"UnitsValues": reflect.ValueOf(gradient.UnitsValues),
"UnmarshalXML": reflect.ValueOf(gradient.UnmarshalXML),
"UserSpaceOnUse": reflect.ValueOf(gradient.UserSpaceOnUse),
"XMLAttr": reflect.ValueOf(gradient.XMLAttr),
// type definitions
"Applier": reflect.ValueOf((*gradient.Applier)(nil)),
"ApplyFunc": reflect.ValueOf((*gradient.ApplyFunc)(nil)),
"ApplyFuncs": reflect.ValueOf((*gradient.ApplyFuncs)(nil)),
"Base": reflect.ValueOf((*gradient.Base)(nil)),
"Gradient": reflect.ValueOf((*gradient.Gradient)(nil)),
"Linear": reflect.ValueOf((*gradient.Linear)(nil)),
"Radial": reflect.ValueOf((*gradient.Radial)(nil)),
"Spreads": reflect.ValueOf((*gradient.Spreads)(nil)),
"Stop": reflect.ValueOf((*gradient.Stop)(nil)),
"Units": reflect.ValueOf((*gradient.Units)(nil)),
// interface wrapper definitions
"_Gradient": reflect.ValueOf((*_cogentcore_org_core_colors_gradient_Gradient)(nil)),
}
}
// _cogentcore_org_core_colors_gradient_Gradient is an interface wrapper for Gradient type
type _cogentcore_org_core_colors_gradient_Gradient struct {
IValue interface{}
WAsBase func() *gradient.Base
WAt func(x int, y int) color.Color
WBounds func() image.Rectangle
WColorModel func() color.Model
WUpdate func(opacity float32, box math32.Box2, objTransform math32.Matrix2)
}
func (W _cogentcore_org_core_colors_gradient_Gradient) AsBase() *gradient.Base { return W.WAsBase() }
func (W _cogentcore_org_core_colors_gradient_Gradient) At(x int, y int) color.Color {
return W.WAt(x, y)
}
func (W _cogentcore_org_core_colors_gradient_Gradient) Bounds() image.Rectangle { return W.WBounds() }
func (W _cogentcore_org_core_colors_gradient_Gradient) ColorModel() color.Model {
return W.WColorModel()
}
func (W _cogentcore_org_core_colors_gradient_Gradient) Update(opacity float32, box math32.Box2, objTransform math32.Matrix2) {
W.WUpdate(opacity, box, objTransform)
}
// Code generated by 'yaegi extract cogentcore.org/core/colors'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/colors"
"image"
"image/color"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/colors/colors"] = map[string]reflect.Value{
// function, constant and variable definitions
"Add": reflect.ValueOf(colors.Add),
"Aliceblue": reflect.ValueOf(&colors.Aliceblue).Elem(),
"AlphaBlend": reflect.ValueOf(colors.AlphaBlend),
"Antiquewhite": reflect.ValueOf(&colors.Antiquewhite).Elem(),
"ApplyOpacity": reflect.ValueOf(colors.ApplyOpacity),
"ApplyOpacityNRGBA": reflect.ValueOf(colors.ApplyOpacityNRGBA),
"Aqua": reflect.ValueOf(&colors.Aqua).Elem(),
"Aquamarine": reflect.ValueOf(&colors.Aquamarine).Elem(),
"AsHex": reflect.ValueOf(colors.AsHex),
"AsRGBA": reflect.ValueOf(colors.AsRGBA),
"AsString": reflect.ValueOf(colors.AsString),
"Azure": reflect.ValueOf(&colors.Azure).Elem(),
"BaseContext": reflect.ValueOf(colors.BaseContext),
"Beige": reflect.ValueOf(&colors.Beige).Elem(),
"Bisque": reflect.ValueOf(&colors.Bisque).Elem(),
"Black": reflect.ValueOf(&colors.Black).Elem(),
"Blanchedalmond": reflect.ValueOf(&colors.Blanchedalmond).Elem(),
"Blend": reflect.ValueOf(colors.Blend),
"BlendRGB": reflect.ValueOf(colors.BlendRGB),
"BlendTypesN": reflect.ValueOf(colors.BlendTypesN),
"BlendTypesValues": reflect.ValueOf(colors.BlendTypesValues),
"Blue": reflect.ValueOf(&colors.Blue).Elem(),
"Blueviolet": reflect.ValueOf(&colors.Blueviolet).Elem(),
"Brown": reflect.ValueOf(&colors.Brown).Elem(),
"Burlywood": reflect.ValueOf(&colors.Burlywood).Elem(),
"CAM16": reflect.ValueOf(colors.CAM16),
"Cadetblue": reflect.ValueOf(&colors.Cadetblue).Elem(),
"Chartreuse": reflect.ValueOf(&colors.Chartreuse).Elem(),
"Chocolate": reflect.ValueOf(&colors.Chocolate).Elem(),
"Clearer": reflect.ValueOf(colors.Clearer),
"Coral": reflect.ValueOf(&colors.Coral).Elem(),
"Cornflowerblue": reflect.ValueOf(&colors.Cornflowerblue).Elem(),
"Cornsilk": reflect.ValueOf(&colors.Cornsilk).Elem(),
"Crimson": reflect.ValueOf(&colors.Crimson).Elem(),
"Cyan": reflect.ValueOf(&colors.Cyan).Elem(),
"Darkblue": reflect.ValueOf(&colors.Darkblue).Elem(),
"Darkcyan": reflect.ValueOf(&colors.Darkcyan).Elem(),
"Darkgoldenrod": reflect.ValueOf(&colors.Darkgoldenrod).Elem(),
"Darkgray": reflect.ValueOf(&colors.Darkgray).Elem(),
"Darkgreen": reflect.ValueOf(&colors.Darkgreen).Elem(),
"Darkgrey": reflect.ValueOf(&colors.Darkgrey).Elem(),
"Darkkhaki": reflect.ValueOf(&colors.Darkkhaki).Elem(),
"Darkmagenta": reflect.ValueOf(&colors.Darkmagenta).Elem(),
"Darkolivegreen": reflect.ValueOf(&colors.Darkolivegreen).Elem(),
"Darkorange": reflect.ValueOf(&colors.Darkorange).Elem(),
"Darkorchid": reflect.ValueOf(&colors.Darkorchid).Elem(),
"Darkred": reflect.ValueOf(&colors.Darkred).Elem(),
"Darksalmon": reflect.ValueOf(&colors.Darksalmon).Elem(),
"Darkseagreen": reflect.ValueOf(&colors.Darkseagreen).Elem(),
"Darkslateblue": reflect.ValueOf(&colors.Darkslateblue).Elem(),
"Darkslategray": reflect.ValueOf(&colors.Darkslategray).Elem(),
"Darkslategrey": reflect.ValueOf(&colors.Darkslategrey).Elem(),
"Darkturquoise": reflect.ValueOf(&colors.Darkturquoise).Elem(),
"Darkviolet": reflect.ValueOf(&colors.Darkviolet).Elem(),
"Deeppink": reflect.ValueOf(&colors.Deeppink).Elem(),
"Deepskyblue": reflect.ValueOf(&colors.Deepskyblue).Elem(),
"Dimgray": reflect.ValueOf(&colors.Dimgray).Elem(),
"Dimgrey": reflect.ValueOf(&colors.Dimgrey).Elem(),
"Dodgerblue": reflect.ValueOf(&colors.Dodgerblue).Elem(),
"Firebrick": reflect.ValueOf(&colors.Firebrick).Elem(),
"Floralwhite": reflect.ValueOf(&colors.Floralwhite).Elem(),
"Forestgreen": reflect.ValueOf(&colors.Forestgreen).Elem(),
"FromAny": reflect.ValueOf(colors.FromAny),
"FromFloat32": reflect.ValueOf(colors.FromFloat32),
"FromFloat64": reflect.ValueOf(colors.FromFloat64),
"FromHex": reflect.ValueOf(colors.FromHex),
"FromNRGBA": reflect.ValueOf(colors.FromNRGBA),
"FromNRGBAF32": reflect.ValueOf(colors.FromNRGBAF32),
"FromName": reflect.ValueOf(colors.FromName),
"FromRGB": reflect.ValueOf(colors.FromRGB),
"FromRGBAF32": reflect.ValueOf(colors.FromRGBAF32),
"FromString": reflect.ValueOf(colors.FromString),
"Fuchsia": reflect.ValueOf(&colors.Fuchsia).Elem(),
"Gainsboro": reflect.ValueOf(&colors.Gainsboro).Elem(),
"Ghostwhite": reflect.ValueOf(&colors.Ghostwhite).Elem(),
"Gold": reflect.ValueOf(&colors.Gold).Elem(),
"Goldenrod": reflect.ValueOf(&colors.Goldenrod).Elem(),
"Gray": reflect.ValueOf(&colors.Gray).Elem(),
"Green": reflect.ValueOf(&colors.Green).Elem(),
"Greenyellow": reflect.ValueOf(&colors.Greenyellow).Elem(),
"Grey": reflect.ValueOf(&colors.Grey).Elem(),
"HCT": reflect.ValueOf(colors.HCT),
"Honeydew": reflect.ValueOf(&colors.Honeydew).Elem(),
"Hotpink": reflect.ValueOf(&colors.Hotpink).Elem(),
"Indianred": reflect.ValueOf(&colors.Indianred).Elem(),
"Indigo": reflect.ValueOf(&colors.Indigo).Elem(),
"Inverse": reflect.ValueOf(colors.Inverse),
"IsNil": reflect.ValueOf(colors.IsNil),
"Ivory": reflect.ValueOf(&colors.Ivory).Elem(),
"Khaki": reflect.ValueOf(&colors.Khaki).Elem(),
"Lavender": reflect.ValueOf(&colors.Lavender).Elem(),
"Lavenderblush": reflect.ValueOf(&colors.Lavenderblush).Elem(),
"Lawngreen": reflect.ValueOf(&colors.Lawngreen).Elem(),
"Lemonchiffon": reflect.ValueOf(&colors.Lemonchiffon).Elem(),
"Lightblue": reflect.ValueOf(&colors.Lightblue).Elem(),
"Lightcoral": reflect.ValueOf(&colors.Lightcoral).Elem(),
"Lightcyan": reflect.ValueOf(&colors.Lightcyan).Elem(),
"Lightgoldenrodyellow": reflect.ValueOf(&colors.Lightgoldenrodyellow).Elem(),
"Lightgray": reflect.ValueOf(&colors.Lightgray).Elem(),
"Lightgreen": reflect.ValueOf(&colors.Lightgreen).Elem(),
"Lightgrey": reflect.ValueOf(&colors.Lightgrey).Elem(),
"Lightpink": reflect.ValueOf(&colors.Lightpink).Elem(),
"Lightsalmon": reflect.ValueOf(&colors.Lightsalmon).Elem(),
"Lightseagreen": reflect.ValueOf(&colors.Lightseagreen).Elem(),
"Lightskyblue": reflect.ValueOf(&colors.Lightskyblue).Elem(),
"Lightslategray": reflect.ValueOf(&colors.Lightslategray).Elem(),
"Lightslategrey": reflect.ValueOf(&colors.Lightslategrey).Elem(),
"Lightsteelblue": reflect.ValueOf(&colors.Lightsteelblue).Elem(),
"Lightyellow": reflect.ValueOf(&colors.Lightyellow).Elem(),
"Lime": reflect.ValueOf(&colors.Lime).Elem(),
"Limegreen": reflect.ValueOf(&colors.Limegreen).Elem(),
"Linen": reflect.ValueOf(&colors.Linen).Elem(),
"Magenta": reflect.ValueOf(&colors.Magenta).Elem(),
"Map": reflect.ValueOf(&colors.Map).Elem(),
"Maroon": reflect.ValueOf(&colors.Maroon).Elem(),
"Mediumaquamarine": reflect.ValueOf(&colors.Mediumaquamarine).Elem(),
"Mediumblue": reflect.ValueOf(&colors.Mediumblue).Elem(),
"Mediumorchid": reflect.ValueOf(&colors.Mediumorchid).Elem(),
"Mediumpurple": reflect.ValueOf(&colors.Mediumpurple).Elem(),
"Mediumseagreen": reflect.ValueOf(&colors.Mediumseagreen).Elem(),
"Mediumslateblue": reflect.ValueOf(&colors.Mediumslateblue).Elem(),
"Mediumspringgreen": reflect.ValueOf(&colors.Mediumspringgreen).Elem(),
"Mediumturquoise": reflect.ValueOf(&colors.Mediumturquoise).Elem(),
"Mediumvioletred": reflect.ValueOf(&colors.Mediumvioletred).Elem(),
"Midnightblue": reflect.ValueOf(&colors.Midnightblue).Elem(),
"Mintcream": reflect.ValueOf(&colors.Mintcream).Elem(),
"Mistyrose": reflect.ValueOf(&colors.Mistyrose).Elem(),
"Moccasin": reflect.ValueOf(&colors.Moccasin).Elem(),
"NRGBAF32Model": reflect.ValueOf(&colors.NRGBAF32Model).Elem(),
"Names": reflect.ValueOf(&colors.Names).Elem(),
"Navajowhite": reflect.ValueOf(&colors.Navajowhite).Elem(),
"Navy": reflect.ValueOf(&colors.Navy).Elem(),
"Oldlace": reflect.ValueOf(&colors.Oldlace).Elem(),
"Olive": reflect.ValueOf(&colors.Olive).Elem(),
"Olivedrab": reflect.ValueOf(&colors.Olivedrab).Elem(),
"Opaquer": reflect.ValueOf(colors.Opaquer),
"Orange": reflect.ValueOf(&colors.Orange).Elem(),
"Orangered": reflect.ValueOf(&colors.Orangered).Elem(),
"Orchid": reflect.ValueOf(&colors.Orchid).Elem(),
"Palegoldenrod": reflect.ValueOf(&colors.Palegoldenrod).Elem(),
"Palegreen": reflect.ValueOf(&colors.Palegreen).Elem(),
"Palette": reflect.ValueOf(&colors.Palette).Elem(),
"Paleturquoise": reflect.ValueOf(&colors.Paleturquoise).Elem(),
"Palevioletred": reflect.ValueOf(&colors.Palevioletred).Elem(),
"Papayawhip": reflect.ValueOf(&colors.Papayawhip).Elem(),
"Pattern": reflect.ValueOf(colors.Pattern),
"Peachpuff": reflect.ValueOf(&colors.Peachpuff).Elem(),
"Peru": reflect.ValueOf(&colors.Peru).Elem(),
"Pink": reflect.ValueOf(&colors.Pink).Elem(),
"Plum": reflect.ValueOf(&colors.Plum).Elem(),
"Powderblue": reflect.ValueOf(&colors.Powderblue).Elem(),
"Purple": reflect.ValueOf(&colors.Purple).Elem(),
"RGB": reflect.ValueOf(colors.RGB),
"RGBAF32Model": reflect.ValueOf(&colors.RGBAF32Model).Elem(),
"Rebeccapurple": reflect.ValueOf(&colors.Rebeccapurple).Elem(),
"Red": reflect.ValueOf(&colors.Red).Elem(),
"Rosybrown": reflect.ValueOf(&colors.Rosybrown).Elem(),
"Royalblue": reflect.ValueOf(&colors.Royalblue).Elem(),
"Saddlebrown": reflect.ValueOf(&colors.Saddlebrown).Elem(),
"Salmon": reflect.ValueOf(&colors.Salmon).Elem(),
"Sandybrown": reflect.ValueOf(&colors.Sandybrown).Elem(),
"Scheme": reflect.ValueOf(&colors.Scheme).Elem(),
"Schemes": reflect.ValueOf(&colors.Schemes).Elem(),
"Seagreen": reflect.ValueOf(&colors.Seagreen).Elem(),
"Seashell": reflect.ValueOf(&colors.Seashell).Elem(),
"SetScheme": reflect.ValueOf(colors.SetScheme),
"SetSchemes": reflect.ValueOf(colors.SetSchemes),
"SetSchemesFromKey": reflect.ValueOf(colors.SetSchemesFromKey),
"Sienna": reflect.ValueOf(&colors.Sienna).Elem(),
"Silver": reflect.ValueOf(&colors.Silver).Elem(),
"Skyblue": reflect.ValueOf(&colors.Skyblue).Elem(),
"Slateblue": reflect.ValueOf(&colors.Slateblue).Elem(),
"Slategray": reflect.ValueOf(&colors.Slategray).Elem(),
"Slategrey": reflect.ValueOf(&colors.Slategrey).Elem(),
"Snow": reflect.ValueOf(&colors.Snow).Elem(),
"Spaced": reflect.ValueOf(colors.Spaced),
"Springgreen": reflect.ValueOf(&colors.Springgreen).Elem(),
"Steelblue": reflect.ValueOf(&colors.Steelblue).Elem(),
"Sub": reflect.ValueOf(colors.Sub),
"Tan": reflect.ValueOf(&colors.Tan).Elem(),
"Teal": reflect.ValueOf(&colors.Teal).Elem(),
"Thistle": reflect.ValueOf(&colors.Thistle).Elem(),
"ToBase": reflect.ValueOf(colors.ToBase),
"ToContainer": reflect.ValueOf(colors.ToContainer),
"ToFloat32": reflect.ValueOf(colors.ToFloat32),
"ToFloat64": reflect.ValueOf(colors.ToFloat64),
"ToOn": reflect.ValueOf(colors.ToOn),
"ToOnContainer": reflect.ValueOf(colors.ToOnContainer),
"ToUniform": reflect.ValueOf(colors.ToUniform),
"Tomato": reflect.ValueOf(&colors.Tomato).Elem(),
"Transparent": reflect.ValueOf(&colors.Transparent).Elem(),
"Turquoise": reflect.ValueOf(&colors.Turquoise).Elem(),
"Uniform": reflect.ValueOf(colors.Uniform),
"Violet": reflect.ValueOf(&colors.Violet).Elem(),
"Wheat": reflect.ValueOf(&colors.Wheat).Elem(),
"White": reflect.ValueOf(&colors.White).Elem(),
"Whitesmoke": reflect.ValueOf(&colors.Whitesmoke).Elem(),
"WithA": reflect.ValueOf(colors.WithA),
"WithAF32": reflect.ValueOf(colors.WithAF32),
"WithB": reflect.ValueOf(colors.WithB),
"WithG": reflect.ValueOf(colors.WithG),
"WithR": reflect.ValueOf(colors.WithR),
"Yellow": reflect.ValueOf(&colors.Yellow).Elem(),
"Yellowgreen": reflect.ValueOf(&colors.Yellowgreen).Elem(),
// type definitions
"BlendTypes": reflect.ValueOf((*colors.BlendTypes)(nil)),
"Context": reflect.ValueOf((*colors.Context)(nil)),
"NRGBAF32": reflect.ValueOf((*colors.NRGBAF32)(nil)),
"RGBAF32": reflect.ValueOf((*colors.RGBAF32)(nil)),
// interface wrapper definitions
"_Context": reflect.ValueOf((*_cogentcore_org_core_colors_Context)(nil)),
}
}
// _cogentcore_org_core_colors_Context is an interface wrapper for Context type
type _cogentcore_org_core_colors_Context struct {
IValue interface{}
WBase func() color.RGBA
WImageByURL func(url string) image.Image
}
func (W _cogentcore_org_core_colors_Context) Base() color.RGBA { return W.WBase() }
func (W _cogentcore_org_core_colors_Context) ImageByURL(url string) image.Image {
return W.WImageByURL(url)
}
// Code generated by 'yaegi extract cogentcore.org/core/core'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/system"
"cogentcore.org/core/tree"
"github.com/cogentcore/yaegi/interp"
"go/constant"
"go/token"
"image"
"image/draw"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/core/core"] = map[string]reflect.Value{
// function, constant and variable definitions
"AllRenderWindows": reflect.ValueOf(&core.AllRenderWindows).Elem(),
"AllSettings": reflect.ValueOf(&core.AllSettings).Elem(),
"AppAbout": reflect.ValueOf(&core.AppAbout).Elem(),
"AppColor": reflect.ValueOf(&core.AppColor).Elem(),
"AppIcon": reflect.ValueOf(&core.AppIcon).Elem(),
"AppearanceSettings": reflect.ValueOf(&core.AppearanceSettings).Elem(),
"AsButton": reflect.ValueOf(core.AsButton),
"AsFrame": reflect.ValueOf(core.AsFrame),
"AsTextField": reflect.ValueOf(core.AsTextField),
"AsTree": reflect.ValueOf(core.AsTree),
"AsWidget": reflect.ValueOf(core.AsWidget),
"Bind": reflect.ValueOf(interp.GenericFunc("func Bind[T Value](value any, vw T, tags ...string) T { //yaegi:add\n\t// TODO: make tags be reflect.StructTag once yaegi is fixed to work with that\n\twb := vw.AsWidget()\n\talreadyBound := wb.ValueUpdate != nil\n\twb.ValueUpdate = func() {\n\t\tif vws, ok := any(vw).(ValueSetter); ok {\n\t\t\tErrorSnackbar(vw, vws.SetWidgetValue(value))\n\t\t} else {\n\t\t\tErrorSnackbar(vw, reflectx.SetRobust(vw.WidgetValue(), value))\n\t\t}\n\t}\n\twb.ValueOnChange = func() {\n\t\tErrorSnackbar(vw, reflectx.SetRobust(value, vw.WidgetValue()))\n\t}\n\tif alreadyBound {\n\t\tResetWidgetValue(vw)\n\t}\n\twb.ValueTitle = labels.FriendlyTypeName(reflectx.NonPointerType(reflect.TypeOf(value)))\n\tif ob, ok := any(vw).(OnBinder); ok {\n\t\ttag := reflect.StructTag(\"\")\n\t\tif len(tags) > 0 {\n\t\t\ttag = reflect.StructTag(tags[0])\n\t\t}\n\t\tob.OnBind(value, tag)\n\t}\n\twb.ValueUpdate() // we update it with the initial value immediately\n\treturn vw\n}")),
"ButtonAction": reflect.ValueOf(core.ButtonAction),
"ButtonElevated": reflect.ValueOf(core.ButtonElevated),
"ButtonFilled": reflect.ValueOf(core.ButtonFilled),
"ButtonMenu": reflect.ValueOf(core.ButtonMenu),
"ButtonOutlined": reflect.ValueOf(core.ButtonOutlined),
"ButtonText": reflect.ValueOf(core.ButtonText),
"ButtonTonal": reflect.ValueOf(core.ButtonTonal),
"ButtonTypesN": reflect.ValueOf(core.ButtonTypesN),
"ButtonTypesValues": reflect.ValueOf(core.ButtonTypesValues),
"CallFunc": reflect.ValueOf(core.CallFunc),
"ChooserFilled": reflect.ValueOf(core.ChooserFilled),
"ChooserOutlined": reflect.ValueOf(core.ChooserOutlined),
"ChooserTypesN": reflect.ValueOf(core.ChooserTypesN),
"ChooserTypesValues": reflect.ValueOf(core.ChooserTypesValues),
"CompleteEditText": reflect.ValueOf(core.CompleteEditText),
"CompleterStage": reflect.ValueOf(core.CompleterStage),
"ConstantSpacing": reflect.ValueOf(core.ConstantSpacing),
"DebugSettings": reflect.ValueOf(&core.DebugSettings).Elem(),
"DeviceSettings": reflect.ValueOf(&core.DeviceSettings).Elem(),
"DialogStage": reflect.ValueOf(core.DialogStage),
"ErrorDialog": reflect.ValueOf(core.ErrorDialog),
"ErrorSnackbar": reflect.ValueOf(core.ErrorSnackbar),
"ExternalParent": reflect.ValueOf(&core.ExternalParent).Elem(),
"FilePickerDirOnlyFilter": reflect.ValueOf(core.FilePickerDirOnlyFilter),
"FilePickerExtensionOnlyFilter": reflect.ValueOf(core.FilePickerExtensionOnlyFilter),
"ForceAppColor": reflect.ValueOf(&core.ForceAppColor).Elem(),
"FunctionalTabs": reflect.ValueOf(core.FunctionalTabs),
"InitValueButton": reflect.ValueOf(core.InitValueButton),
"InspectorWindow": reflect.ValueOf(core.InspectorWindow),
"IsWordBreak": reflect.ValueOf(core.IsWordBreak),
"LayoutPassesN": reflect.ValueOf(core.LayoutPassesN),
"LayoutPassesValues": reflect.ValueOf(core.LayoutPassesValues),
"ListColProperty": reflect.ValueOf(constant.MakeFromLiteral("\"ls-col\"", token.STRING, 0)),
"ListRowProperty": reflect.ValueOf(constant.MakeFromLiteral("\"ls-row\"", token.STRING, 0)),
"LoadAllSettings": reflect.ValueOf(core.LoadAllSettings),
"MenuStage": reflect.ValueOf(core.MenuStage),
"MessageDialog": reflect.ValueOf(core.MessageDialog),
"MessageSnackbar": reflect.ValueOf(core.MessageSnackbar),
"MeterCircle": reflect.ValueOf(core.MeterCircle),
"MeterLinear": reflect.ValueOf(core.MeterLinear),
"MeterSemicircle": reflect.ValueOf(core.MeterSemicircle),
"MeterTypesN": reflect.ValueOf(core.MeterTypesN),
"MeterTypesValues": reflect.ValueOf(core.MeterTypesValues),
"NavigationAuto": reflect.ValueOf(core.NavigationAuto),
"NavigationBar": reflect.ValueOf(core.NavigationBar),
"NavigationDrawer": reflect.ValueOf(core.NavigationDrawer),
"NewBody": reflect.ValueOf(core.NewBody),
"NewButton": reflect.ValueOf(core.NewButton),
"NewCanvas": reflect.ValueOf(core.NewCanvas),
"NewChooser": reflect.ValueOf(core.NewChooser),
"NewColorButton": reflect.ValueOf(core.NewColorButton),
"NewColorMapButton": reflect.ValueOf(core.NewColorMapButton),
"NewColorPicker": reflect.ValueOf(core.NewColorPicker),
"NewComplete": reflect.ValueOf(core.NewComplete),
"NewDatePicker": reflect.ValueOf(core.NewDatePicker),
"NewDurationInput": reflect.ValueOf(core.NewDurationInput),
"NewFileButton": reflect.ValueOf(core.NewFileButton),
"NewFilePicker": reflect.ValueOf(core.NewFilePicker),
"NewFontButton": reflect.ValueOf(core.NewFontButton),
"NewForm": reflect.ValueOf(core.NewForm),
"NewFormButton": reflect.ValueOf(core.NewFormButton),
"NewFrame": reflect.ValueOf(core.NewFrame),
"NewFuncButton": reflect.ValueOf(core.NewFuncButton),
"NewHandle": reflect.ValueOf(core.NewHandle),
"NewIcon": reflect.ValueOf(core.NewIcon),
"NewIconButton": reflect.ValueOf(core.NewIconButton),
"NewImage": reflect.ValueOf(core.NewImage),
"NewInlineList": reflect.ValueOf(core.NewInlineList),
"NewInspector": reflect.ValueOf(core.NewInspector),
"NewKeyChordButton": reflect.ValueOf(core.NewKeyChordButton),
"NewKeyMapButton": reflect.ValueOf(core.NewKeyMapButton),
"NewKeyedList": reflect.ValueOf(core.NewKeyedList),
"NewKeyedListButton": reflect.ValueOf(core.NewKeyedListButton),
"NewList": reflect.ValueOf(core.NewList),
"NewListButton": reflect.ValueOf(core.NewListButton),
"NewMenu": reflect.ValueOf(core.NewMenu),
"NewMenuFromStrings": reflect.ValueOf(core.NewMenuFromStrings),
"NewMenuStage": reflect.ValueOf(core.NewMenuStage),
"NewMeter": reflect.ValueOf(core.NewMeter),
"NewPages": reflect.ValueOf(core.NewPages),
"NewPopupStage": reflect.ValueOf(core.NewPopupStage),
"NewSVG": reflect.ValueOf(core.NewSVG),
"NewScene": reflect.ValueOf(core.NewScene),
"NewSeparator": reflect.ValueOf(core.NewSeparator),
"NewSlider": reflect.ValueOf(core.NewSlider),
"NewSoloFuncButton": reflect.ValueOf(core.NewSoloFuncButton),
"NewSpace": reflect.ValueOf(core.NewSpace),
"NewSpinner": reflect.ValueOf(core.NewSpinner),
"NewSplits": reflect.ValueOf(core.NewSplits),
"NewSprite": reflect.ValueOf(core.NewSprite),
"NewStretch": reflect.ValueOf(core.NewStretch),
"NewSwitch": reflect.ValueOf(core.NewSwitch),
"NewSwitches": reflect.ValueOf(core.NewSwitches),
"NewTable": reflect.ValueOf(core.NewTable),
"NewTabs": reflect.ValueOf(core.NewTabs),
"NewText": reflect.ValueOf(core.NewText),
"NewTextField": reflect.ValueOf(core.NewTextField),
"NewTimeInput": reflect.ValueOf(core.NewTimeInput),
"NewTimePicker": reflect.ValueOf(core.NewTimePicker),
"NewToolbar": reflect.ValueOf(core.NewToolbar),
"NewTree": reflect.ValueOf(core.NewTree),
"NewTreeButton": reflect.ValueOf(core.NewTreeButton),
"NewTypeChooser": reflect.ValueOf(core.NewTypeChooser),
"NewValue": reflect.ValueOf(core.NewValue),
"NewWidgetBase": reflect.ValueOf(core.NewWidgetBase),
"NoSentenceCaseFor": reflect.ValueOf(&core.NoSentenceCaseFor).Elem(),
"ProfileToggle": reflect.ValueOf(core.ProfileToggle),
"RecycleDialog": reflect.ValueOf(core.RecycleDialog),
"RecycleMainWindow": reflect.ValueOf(core.RecycleMainWindow),
"ResetWidgetValue": reflect.ValueOf(core.ResetWidgetValue),
"SaveSettings": reflect.ValueOf(core.SaveSettings),
"SettingsEditor": reflect.ValueOf(core.SettingsEditor),
"SettingsWindow": reflect.ValueOf(core.SettingsWindow),
"SizeClassesN": reflect.ValueOf(core.SizeClassesN),
"SizeClassesValues": reflect.ValueOf(core.SizeClassesValues),
"SizeCompact": reflect.ValueOf(core.SizeCompact),
"SizeDownPass": reflect.ValueOf(core.SizeDownPass),
"SizeExpanded": reflect.ValueOf(core.SizeExpanded),
"SizeFinalPass": reflect.ValueOf(core.SizeFinalPass),
"SizeMedium": reflect.ValueOf(core.SizeMedium),
"SizeUpPass": reflect.ValueOf(core.SizeUpPass),
"SliderScrollbar": reflect.ValueOf(core.SliderScrollbar),
"SliderSlider": reflect.ValueOf(core.SliderSlider),
"SliderTypesN": reflect.ValueOf(core.SliderTypesN),
"SliderTypesValues": reflect.ValueOf(core.SliderTypesValues),
"SnackbarStage": reflect.ValueOf(core.SnackbarStage),
"SplitsTilesN": reflect.ValueOf(core.SplitsTilesN),
"SplitsTilesValues": reflect.ValueOf(core.SplitsTilesValues),
"StageTypesN": reflect.ValueOf(core.StageTypesN),
"StageTypesValues": reflect.ValueOf(core.StageTypesValues),
"StandardTabs": reflect.ValueOf(core.StandardTabs),
"StyleMenuScene": reflect.ValueOf(core.StyleMenuScene),
"SwitchCheckbox": reflect.ValueOf(core.SwitchCheckbox),
"SwitchChip": reflect.ValueOf(core.SwitchChip),
"SwitchRadioButton": reflect.ValueOf(core.SwitchRadioButton),
"SwitchSegmentedButton": reflect.ValueOf(core.SwitchSegmentedButton),
"SwitchSwitch": reflect.ValueOf(core.SwitchSwitch),
"SwitchTypesN": reflect.ValueOf(core.SwitchTypesN),
"SwitchTypesValues": reflect.ValueOf(core.SwitchTypesValues),
"SystemSettings": reflect.ValueOf(&core.SystemSettings).Elem(),
"TabTypesN": reflect.ValueOf(core.TabTypesN),
"TabTypesValues": reflect.ValueOf(core.TabTypesValues),
"TextBodyLarge": reflect.ValueOf(core.TextBodyLarge),
"TextBodyMedium": reflect.ValueOf(core.TextBodyMedium),
"TextBodySmall": reflect.ValueOf(core.TextBodySmall),
"TextDisplayLarge": reflect.ValueOf(core.TextDisplayLarge),
"TextDisplayMedium": reflect.ValueOf(core.TextDisplayMedium),
"TextDisplaySmall": reflect.ValueOf(core.TextDisplaySmall),
"TextFieldFilled": reflect.ValueOf(core.TextFieldFilled),
"TextFieldOutlined": reflect.ValueOf(core.TextFieldOutlined),
"TextFieldTypesN": reflect.ValueOf(core.TextFieldTypesN),
"TextFieldTypesValues": reflect.ValueOf(core.TextFieldTypesValues),
"TextHeadlineLarge": reflect.ValueOf(core.TextHeadlineLarge),
"TextHeadlineMedium": reflect.ValueOf(core.TextHeadlineMedium),
"TextHeadlineSmall": reflect.ValueOf(core.TextHeadlineSmall),
"TextLabelLarge": reflect.ValueOf(core.TextLabelLarge),
"TextLabelMedium": reflect.ValueOf(core.TextLabelMedium),
"TextLabelSmall": reflect.ValueOf(core.TextLabelSmall),
"TextSupporting": reflect.ValueOf(core.TextSupporting),
"TextTitleLarge": reflect.ValueOf(core.TextTitleLarge),
"TextTitleMedium": reflect.ValueOf(core.TextTitleMedium),
"TextTitleSmall": reflect.ValueOf(core.TextTitleSmall),
"TextTypesN": reflect.ValueOf(core.TextTypesN),
"TextTypesValues": reflect.ValueOf(core.TextTypesValues),
"TheApp": reflect.ValueOf(&core.TheApp).Elem(),
"ThemeAuto": reflect.ValueOf(core.ThemeAuto),
"ThemeDark": reflect.ValueOf(core.ThemeDark),
"ThemeLight": reflect.ValueOf(core.ThemeLight),
"ThemesN": reflect.ValueOf(core.ThemesN),
"ThemesValues": reflect.ValueOf(core.ThemesValues),
"TileFirstLong": reflect.ValueOf(core.TileFirstLong),
"TilePlus": reflect.ValueOf(core.TilePlus),
"TileSecondLong": reflect.ValueOf(core.TileSecondLong),
"TileSpan": reflect.ValueOf(core.TileSpan),
"TileSplit": reflect.ValueOf(core.TileSplit),
"ToHTML": reflect.ValueOf(core.ToHTML),
"ToolbarStyles": reflect.ValueOf(core.ToolbarStyles),
"TooltipStage": reflect.ValueOf(core.TooltipStage),
"UpdateAll": reflect.ValueOf(core.UpdateAll),
"UpdateSettings": reflect.ValueOf(core.UpdateSettings),
"ValueTypes": reflect.ValueOf(&core.ValueTypes).Elem(),
"Wait": reflect.ValueOf(core.Wait),
"WindowStage": reflect.ValueOf(core.WindowStage),
// type definitions
"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)),
"ButtonTypes": reflect.ValueOf((*core.ButtonTypes)(nil)),
"Canvas": reflect.ValueOf((*core.Canvas)(nil)),
"Chooser": reflect.ValueOf((*core.Chooser)(nil)),
"ChooserItem": reflect.ValueOf((*core.ChooserItem)(nil)),
"ChooserTypes": reflect.ValueOf((*core.ChooserTypes)(nil)),
"ColorButton": reflect.ValueOf((*core.ColorButton)(nil)),
"ColorMapButton": reflect.ValueOf((*core.ColorMapButton)(nil)),
"ColorMapName": reflect.ValueOf((*core.ColorMapName)(nil)),
"ColorPicker": reflect.ValueOf((*core.ColorPicker)(nil)),
"Complete": reflect.ValueOf((*core.Complete)(nil)),
"DatePicker": reflect.ValueOf((*core.DatePicker)(nil)),
"DebugSettingsData": reflect.ValueOf((*core.DebugSettingsData)(nil)),
"DeviceSettingsData": reflect.ValueOf((*core.DeviceSettingsData)(nil)),
"DurationInput": reflect.ValueOf((*core.DurationInput)(nil)),
"EditorSettings": reflect.ValueOf((*core.EditorSettings)(nil)),
"Events": reflect.ValueOf((*core.Events)(nil)),
"FileButton": reflect.ValueOf((*core.FileButton)(nil)),
"FilePaths": reflect.ValueOf((*core.FilePaths)(nil)),
"FilePicker": reflect.ValueOf((*core.FilePicker)(nil)),
"FilePickerFilterer": reflect.ValueOf((*core.FilePickerFilterer)(nil)),
"Filename": reflect.ValueOf((*core.Filename)(nil)),
"FontButton": reflect.ValueOf((*core.FontButton)(nil)),
"FontName": reflect.ValueOf((*core.FontName)(nil)),
"Form": reflect.ValueOf((*core.Form)(nil)),
"FormButton": reflect.ValueOf((*core.FormButton)(nil)),
"Frame": reflect.ValueOf((*core.Frame)(nil)),
"FuncArg": reflect.ValueOf((*core.FuncArg)(nil)),
"FuncButton": reflect.ValueOf((*core.FuncButton)(nil)),
"Handle": reflect.ValueOf((*core.Handle)(nil)),
"HighlightingName": reflect.ValueOf((*core.HighlightingName)(nil)),
"Icon": reflect.ValueOf((*core.Icon)(nil)),
"IconButton": reflect.ValueOf((*core.IconButton)(nil)),
"Image": reflect.ValueOf((*core.Image)(nil)),
"InlineList": reflect.ValueOf((*core.InlineList)(nil)),
"Inspector": reflect.ValueOf((*core.Inspector)(nil)),
"KeyChordButton": reflect.ValueOf((*core.KeyChordButton)(nil)),
"KeyMapButton": reflect.ValueOf((*core.KeyMapButton)(nil)),
"KeyedList": reflect.ValueOf((*core.KeyedList)(nil)),
"KeyedListButton": reflect.ValueOf((*core.KeyedListButton)(nil)),
"LayoutPasses": reflect.ValueOf((*core.LayoutPasses)(nil)),
"Layouter": reflect.ValueOf((*core.Layouter)(nil)),
"List": reflect.ValueOf((*core.List)(nil)),
"ListBase": reflect.ValueOf((*core.ListBase)(nil)),
"ListButton": reflect.ValueOf((*core.ListButton)(nil)),
"ListGrid": reflect.ValueOf((*core.ListGrid)(nil)),
"ListStyler": reflect.ValueOf((*core.ListStyler)(nil)),
"Lister": reflect.ValueOf((*core.Lister)(nil)),
"MenuSearcher": reflect.ValueOf((*core.MenuSearcher)(nil)),
"Meter": reflect.ValueOf((*core.Meter)(nil)),
"MeterTypes": reflect.ValueOf((*core.MeterTypes)(nil)),
"OnBinder": reflect.ValueOf((*core.OnBinder)(nil)),
"Pages": reflect.ValueOf((*core.Pages)(nil)),
"SVG": reflect.ValueOf((*core.SVG)(nil)),
"Scene": reflect.ValueOf((*core.Scene)(nil)),
"ScreenSettings": reflect.ValueOf((*core.ScreenSettings)(nil)),
"Separator": reflect.ValueOf((*core.Separator)(nil)),
"Settings": reflect.ValueOf((*core.Settings)(nil)),
"SettingsBase": reflect.ValueOf((*core.SettingsBase)(nil)),
"SettingsOpener": reflect.ValueOf((*core.SettingsOpener)(nil)),
"SettingsSaver": reflect.ValueOf((*core.SettingsSaver)(nil)),
"ShouldDisplayer": reflect.ValueOf((*core.ShouldDisplayer)(nil)),
"SizeClasses": reflect.ValueOf((*core.SizeClasses)(nil)),
"Slider": reflect.ValueOf((*core.Slider)(nil)),
"SliderTypes": reflect.ValueOf((*core.SliderTypes)(nil)),
"Space": reflect.ValueOf((*core.Space)(nil)),
"Spinner": reflect.ValueOf((*core.Spinner)(nil)),
"Splits": reflect.ValueOf((*core.Splits)(nil)),
"SplitsTiles": reflect.ValueOf((*core.SplitsTiles)(nil)),
"Sprite": reflect.ValueOf((*core.Sprite)(nil)),
"Sprites": reflect.ValueOf((*core.Sprites)(nil)),
"Stage": reflect.ValueOf((*core.Stage)(nil)),
"StageTypes": reflect.ValueOf((*core.StageTypes)(nil)),
"Stretch": reflect.ValueOf((*core.Stretch)(nil)),
"Switch": reflect.ValueOf((*core.Switch)(nil)),
"SwitchItem": reflect.ValueOf((*core.SwitchItem)(nil)),
"SwitchTypes": reflect.ValueOf((*core.SwitchTypes)(nil)),
"Switches": reflect.ValueOf((*core.Switches)(nil)),
"SystemSettingsData": reflect.ValueOf((*core.SystemSettingsData)(nil)),
"Tab": reflect.ValueOf((*core.Tab)(nil)),
"TabTypes": reflect.ValueOf((*core.TabTypes)(nil)),
"Table": reflect.ValueOf((*core.Table)(nil)),
"TableStyler": reflect.ValueOf((*core.TableStyler)(nil)),
"Tabs": reflect.ValueOf((*core.Tabs)(nil)),
"Text": reflect.ValueOf((*core.Text)(nil)),
"TextField": reflect.ValueOf((*core.TextField)(nil)),
"TextFieldEmbedder": reflect.ValueOf((*core.TextFieldEmbedder)(nil)),
"TextFieldTypes": reflect.ValueOf((*core.TextFieldTypes)(nil)),
"TextTypes": reflect.ValueOf((*core.TextTypes)(nil)),
"Themes": reflect.ValueOf((*core.Themes)(nil)),
"TimeInput": reflect.ValueOf((*core.TimeInput)(nil)),
"TimePicker": reflect.ValueOf((*core.TimePicker)(nil)),
"Toolbar": reflect.ValueOf((*core.Toolbar)(nil)),
"ToolbarMaker": reflect.ValueOf((*core.ToolbarMaker)(nil)),
"Tree": reflect.ValueOf((*core.Tree)(nil)),
"TreeButton": reflect.ValueOf((*core.TreeButton)(nil)),
"Treer": reflect.ValueOf((*core.Treer)(nil)),
"TypeChooser": reflect.ValueOf((*core.TypeChooser)(nil)),
"User": reflect.ValueOf((*core.User)(nil)),
"Validator": reflect.ValueOf((*core.Validator)(nil)),
"Value": reflect.ValueOf((*core.Value)(nil)),
"ValueSetter": reflect.ValueOf((*core.ValueSetter)(nil)),
"Valuer": reflect.ValueOf((*core.Valuer)(nil)),
"Widget": reflect.ValueOf((*core.Widget)(nil)),
"WidgetBase": reflect.ValueOf((*core.WidgetBase)(nil)),
// interface wrapper definitions
"_ButtonEmbedder": reflect.ValueOf((*_cogentcore_org_core_core_ButtonEmbedder)(nil)),
"_Layouter": reflect.ValueOf((*_cogentcore_org_core_core_Layouter)(nil)),
"_Lister": reflect.ValueOf((*_cogentcore_org_core_core_Lister)(nil)),
"_MenuSearcher": reflect.ValueOf((*_cogentcore_org_core_core_MenuSearcher)(nil)),
"_OnBinder": reflect.ValueOf((*_cogentcore_org_core_core_OnBinder)(nil)),
"_Settings": reflect.ValueOf((*_cogentcore_org_core_core_Settings)(nil)),
"_SettingsOpener": reflect.ValueOf((*_cogentcore_org_core_core_SettingsOpener)(nil)),
"_SettingsSaver": reflect.ValueOf((*_cogentcore_org_core_core_SettingsSaver)(nil)),
"_ShouldDisplayer": reflect.ValueOf((*_cogentcore_org_core_core_ShouldDisplayer)(nil)),
"_TextFieldEmbedder": reflect.ValueOf((*_cogentcore_org_core_core_TextFieldEmbedder)(nil)),
"_ToolbarMaker": reflect.ValueOf((*_cogentcore_org_core_core_ToolbarMaker)(nil)),
"_Treer": reflect.ValueOf((*_cogentcore_org_core_core_Treer)(nil)),
"_Validator": reflect.ValueOf((*_cogentcore_org_core_core_Validator)(nil)),
"_Value": reflect.ValueOf((*_cogentcore_org_core_core_Value)(nil)),
"_ValueSetter": reflect.ValueOf((*_cogentcore_org_core_core_ValueSetter)(nil)),
"_Valuer": reflect.ValueOf((*_cogentcore_org_core_core_Valuer)(nil)),
"_Widget": reflect.ValueOf((*_cogentcore_org_core_core_Widget)(nil)),
}
}
// _cogentcore_org_core_core_ButtonEmbedder is an interface wrapper for ButtonEmbedder type
type _cogentcore_org_core_core_ButtonEmbedder struct {
IValue interface{}
WAsButton func() *core.Button
}
func (W _cogentcore_org_core_core_ButtonEmbedder) AsButton() *core.Button { return W.WAsButton() }
// _cogentcore_org_core_core_Layouter is an interface wrapper for Layouter type
type _cogentcore_org_core_core_Layouter struct {
IValue interface{}
WApplyScenePos func()
WAsFrame func() *core.Frame
WAsTree func() *tree.NodeBase
WAsWidget func() *core.WidgetBase
WChildBackground func(child core.Widget) image.Image
WContextMenuPos func(e events.Event) image.Point
WCopyFieldsFrom func(from tree.Node)
WDestroy func()
WInit func()
WLayoutSpace func()
WManageOverflow func(iter int, updateSize bool) bool
WNodeWalkDown func(fun func(n tree.Node) bool)
WOnAdd func()
WPlanName func() string
WPosition func()
WRender func()
WRenderDraw func(drw system.Drawer, op draw.Op)
WRenderWidget func()
WScrollChanged func(d math32.Dims, sb *core.Slider)
WScrollGeom func(d math32.Dims) (pos math32.Vector2, sz math32.Vector2)
WScrollValues func(d math32.Dims) (maxSize float32, visSize float32, visPct float32)
WSetScrollParams func(d math32.Dims, sb *core.Slider)
WShowContextMenu func(e events.Event)
WSizeDown func(iter int) bool
WSizeDownSetAllocs func(iter int)
WSizeFinal func()
WSizeFromChildren func(iter int, pass core.LayoutPasses) math32.Vector2
WSizeUp func()
WStyle func()
WWidgetTooltip func(pos image.Point) (string, image.Point)
}
func (W _cogentcore_org_core_core_Layouter) ApplyScenePos() { W.WApplyScenePos() }
func (W _cogentcore_org_core_core_Layouter) AsFrame() *core.Frame { return W.WAsFrame() }
func (W _cogentcore_org_core_core_Layouter) AsTree() *tree.NodeBase { return W.WAsTree() }
func (W _cogentcore_org_core_core_Layouter) AsWidget() *core.WidgetBase { return W.WAsWidget() }
func (W _cogentcore_org_core_core_Layouter) ChildBackground(child core.Widget) image.Image {
return W.WChildBackground(child)
}
func (W _cogentcore_org_core_core_Layouter) ContextMenuPos(e events.Event) image.Point {
return W.WContextMenuPos(e)
}
func (W _cogentcore_org_core_core_Layouter) CopyFieldsFrom(from tree.Node) { W.WCopyFieldsFrom(from) }
func (W _cogentcore_org_core_core_Layouter) Destroy() { W.WDestroy() }
func (W _cogentcore_org_core_core_Layouter) Init() { W.WInit() }
func (W _cogentcore_org_core_core_Layouter) LayoutSpace() { W.WLayoutSpace() }
func (W _cogentcore_org_core_core_Layouter) ManageOverflow(iter int, updateSize bool) bool {
return W.WManageOverflow(iter, updateSize)
}
func (W _cogentcore_org_core_core_Layouter) NodeWalkDown(fun func(n tree.Node) bool) {
W.WNodeWalkDown(fun)
}
func (W _cogentcore_org_core_core_Layouter) OnAdd() { W.WOnAdd() }
func (W _cogentcore_org_core_core_Layouter) PlanName() string { return W.WPlanName() }
func (W _cogentcore_org_core_core_Layouter) Position() { W.WPosition() }
func (W _cogentcore_org_core_core_Layouter) Render() { W.WRender() }
func (W _cogentcore_org_core_core_Layouter) RenderDraw(drw system.Drawer, op draw.Op) {
W.WRenderDraw(drw, op)
}
func (W _cogentcore_org_core_core_Layouter) RenderWidget() { W.WRenderWidget() }
func (W _cogentcore_org_core_core_Layouter) ScrollChanged(d math32.Dims, sb *core.Slider) {
W.WScrollChanged(d, sb)
}
func (W _cogentcore_org_core_core_Layouter) ScrollGeom(d math32.Dims) (pos math32.Vector2, sz math32.Vector2) {
return W.WScrollGeom(d)
}
func (W _cogentcore_org_core_core_Layouter) ScrollValues(d math32.Dims) (maxSize float32, visSize float32, visPct float32) {
return W.WScrollValues(d)
}
func (W _cogentcore_org_core_core_Layouter) SetScrollParams(d math32.Dims, sb *core.Slider) {
W.WSetScrollParams(d, sb)
}
func (W _cogentcore_org_core_core_Layouter) ShowContextMenu(e events.Event) { W.WShowContextMenu(e) }
func (W _cogentcore_org_core_core_Layouter) SizeDown(iter int) bool { return W.WSizeDown(iter) }
func (W _cogentcore_org_core_core_Layouter) SizeDownSetAllocs(iter int) { W.WSizeDownSetAllocs(iter) }
func (W _cogentcore_org_core_core_Layouter) SizeFinal() { W.WSizeFinal() }
func (W _cogentcore_org_core_core_Layouter) SizeFromChildren(iter int, pass core.LayoutPasses) math32.Vector2 {
return W.WSizeFromChildren(iter, pass)
}
func (W _cogentcore_org_core_core_Layouter) SizeUp() { W.WSizeUp() }
func (W _cogentcore_org_core_core_Layouter) Style() { W.WStyle() }
func (W _cogentcore_org_core_core_Layouter) WidgetTooltip(pos image.Point) (string, image.Point) {
return W.WWidgetTooltip(pos)
}
// _cogentcore_org_core_core_Lister is an interface wrapper for Lister type
type _cogentcore_org_core_core_Lister struct {
IValue interface{}
WAsListBase func() *core.ListBase
WAsTree func() *tree.NodeBase
WCopyFieldsFrom func(from tree.Node)
WCopySelectToMime func() mimedata.Mimes
WDeleteAt func(idx int)
WDestroy func()
WHasStyler func() bool
WInit func()
WMakeRow func(p *tree.Plan, i int)
WMimeDataType func() string
WNewAt func(idx int)
WNodeWalkDown func(fun func(n tree.Node) bool)
WOnAdd func()
WPasteAssign func(md mimedata.Mimes, idx int)
WPasteAtIndex func(md mimedata.Mimes, idx int)
WPlanName func() string
WRowGrabFocus func(row int) *core.WidgetBase
WRowWidgetNs func() (nWidgPerRow int, idxOff int)
WSliceIndex func(i int) (si int, vi int, invis bool)
WStyleRow func(w core.Widget, idx int, fidx int)
WStyleValue func(w core.Widget, s *styles.Style, row int, col int)
WUpdateMaxWidths func()
WUpdateSliceSize func() int
}
func (W _cogentcore_org_core_core_Lister) AsListBase() *core.ListBase { return W.WAsListBase() }
func (W _cogentcore_org_core_core_Lister) AsTree() *tree.NodeBase { return W.WAsTree() }
func (W _cogentcore_org_core_core_Lister) CopyFieldsFrom(from tree.Node) { W.WCopyFieldsFrom(from) }
func (W _cogentcore_org_core_core_Lister) CopySelectToMime() mimedata.Mimes {
return W.WCopySelectToMime()
}
func (W _cogentcore_org_core_core_Lister) DeleteAt(idx int) { W.WDeleteAt(idx) }
func (W _cogentcore_org_core_core_Lister) Destroy() { W.WDestroy() }
func (W _cogentcore_org_core_core_Lister) HasStyler() bool { return W.WHasStyler() }
func (W _cogentcore_org_core_core_Lister) Init() { W.WInit() }
func (W _cogentcore_org_core_core_Lister) MakeRow(p *tree.Plan, i int) { W.WMakeRow(p, i) }
func (W _cogentcore_org_core_core_Lister) MimeDataType() string { return W.WMimeDataType() }
func (W _cogentcore_org_core_core_Lister) NewAt(idx int) { W.WNewAt(idx) }
func (W _cogentcore_org_core_core_Lister) NodeWalkDown(fun func(n tree.Node) bool) {
W.WNodeWalkDown(fun)
}
func (W _cogentcore_org_core_core_Lister) OnAdd() { W.WOnAdd() }
func (W _cogentcore_org_core_core_Lister) PasteAssign(md mimedata.Mimes, idx int) {
W.WPasteAssign(md, idx)
}
func (W _cogentcore_org_core_core_Lister) PasteAtIndex(md mimedata.Mimes, idx int) {
W.WPasteAtIndex(md, idx)
}
func (W _cogentcore_org_core_core_Lister) PlanName() string { return W.WPlanName() }
func (W _cogentcore_org_core_core_Lister) RowGrabFocus(row int) *core.WidgetBase {
return W.WRowGrabFocus(row)
}
func (W _cogentcore_org_core_core_Lister) RowWidgetNs() (nWidgPerRow int, idxOff int) {
return W.WRowWidgetNs()
}
func (W _cogentcore_org_core_core_Lister) SliceIndex(i int) (si int, vi int, invis bool) {
return W.WSliceIndex(i)
}
func (W _cogentcore_org_core_core_Lister) StyleRow(w core.Widget, idx int, fidx int) {
W.WStyleRow(w, idx, fidx)
}
func (W _cogentcore_org_core_core_Lister) StyleValue(w core.Widget, s *styles.Style, row int, col int) {
W.WStyleValue(w, s, row, col)
}
func (W _cogentcore_org_core_core_Lister) UpdateMaxWidths() { W.WUpdateMaxWidths() }
func (W _cogentcore_org_core_core_Lister) UpdateSliceSize() int { return W.WUpdateSliceSize() }
// _cogentcore_org_core_core_MenuSearcher is an interface wrapper for MenuSearcher type
type _cogentcore_org_core_core_MenuSearcher struct {
IValue interface{}
WMenuSearch func(items *[]core.ChooserItem)
}
func (W _cogentcore_org_core_core_MenuSearcher) MenuSearch(items *[]core.ChooserItem) {
W.WMenuSearch(items)
}
// _cogentcore_org_core_core_OnBinder is an interface wrapper for OnBinder type
type _cogentcore_org_core_core_OnBinder struct {
IValue interface{}
WOnBind func(value any, tags reflect.StructTag)
}
func (W _cogentcore_org_core_core_OnBinder) OnBind(value any, tags reflect.StructTag) {
W.WOnBind(value, tags)
}
// _cogentcore_org_core_core_Settings is an interface wrapper for Settings type
type _cogentcore_org_core_core_Settings struct {
IValue interface{}
WApply func()
WDefaults func()
WFilename func() string
WLabel func() string
WMakeToolbar func(p *tree.Plan)
}
func (W _cogentcore_org_core_core_Settings) Apply() { W.WApply() }
func (W _cogentcore_org_core_core_Settings) Defaults() { W.WDefaults() }
func (W _cogentcore_org_core_core_Settings) Filename() string { return W.WFilename() }
func (W _cogentcore_org_core_core_Settings) Label() string { return W.WLabel() }
func (W _cogentcore_org_core_core_Settings) MakeToolbar(p *tree.Plan) { W.WMakeToolbar(p) }
// _cogentcore_org_core_core_SettingsOpener is an interface wrapper for SettingsOpener type
type _cogentcore_org_core_core_SettingsOpener struct {
IValue interface{}
WApply func()
WDefaults func()
WFilename func() string
WLabel func() string
WMakeToolbar func(p *tree.Plan)
WOpen func() error
}
func (W _cogentcore_org_core_core_SettingsOpener) Apply() { W.WApply() }
func (W _cogentcore_org_core_core_SettingsOpener) Defaults() { W.WDefaults() }
func (W _cogentcore_org_core_core_SettingsOpener) Filename() string { return W.WFilename() }
func (W _cogentcore_org_core_core_SettingsOpener) Label() string { return W.WLabel() }
func (W _cogentcore_org_core_core_SettingsOpener) MakeToolbar(p *tree.Plan) { W.WMakeToolbar(p) }
func (W _cogentcore_org_core_core_SettingsOpener) Open() error { return W.WOpen() }
// _cogentcore_org_core_core_SettingsSaver is an interface wrapper for SettingsSaver type
type _cogentcore_org_core_core_SettingsSaver struct {
IValue interface{}
WApply func()
WDefaults func()
WFilename func() string
WLabel func() string
WMakeToolbar func(p *tree.Plan)
WSave func() error
}
func (W _cogentcore_org_core_core_SettingsSaver) Apply() { W.WApply() }
func (W _cogentcore_org_core_core_SettingsSaver) Defaults() { W.WDefaults() }
func (W _cogentcore_org_core_core_SettingsSaver) Filename() string { return W.WFilename() }
func (W _cogentcore_org_core_core_SettingsSaver) Label() string { return W.WLabel() }
func (W _cogentcore_org_core_core_SettingsSaver) MakeToolbar(p *tree.Plan) { W.WMakeToolbar(p) }
func (W _cogentcore_org_core_core_SettingsSaver) Save() error { return W.WSave() }
// _cogentcore_org_core_core_ShouldDisplayer is an interface wrapper for ShouldDisplayer type
type _cogentcore_org_core_core_ShouldDisplayer struct {
IValue interface{}
WShouldDisplay func(field string) bool
}
func (W _cogentcore_org_core_core_ShouldDisplayer) ShouldDisplay(field string) bool {
return W.WShouldDisplay(field)
}
// _cogentcore_org_core_core_TextFieldEmbedder is an interface wrapper for TextFieldEmbedder type
type _cogentcore_org_core_core_TextFieldEmbedder struct {
IValue interface{}
WAsTextField func() *core.TextField
}
func (W _cogentcore_org_core_core_TextFieldEmbedder) AsTextField() *core.TextField {
return W.WAsTextField()
}
// _cogentcore_org_core_core_ToolbarMaker is an interface wrapper for ToolbarMaker type
type _cogentcore_org_core_core_ToolbarMaker struct {
IValue interface{}
WMakeToolbar func(p *tree.Plan)
}
func (W _cogentcore_org_core_core_ToolbarMaker) MakeToolbar(p *tree.Plan) { W.WMakeToolbar(p) }
// _cogentcore_org_core_core_Treer is an interface wrapper for Treer type
type _cogentcore_org_core_core_Treer struct {
IValue interface{}
WApplyScenePos func()
WAsCoreTree func() *core.Tree
WAsTree func() *tree.NodeBase
WAsWidget func() *core.WidgetBase
WCanOpen func() bool
WChildBackground func(child core.Widget) image.Image
WContextMenuPos func(e events.Event) image.Point
WCopy func()
WCopyFieldsFrom func(from tree.Node)
WCut func()
WDestroy func()
WDragDrop func(e events.Event)
WDropDeleteSource func(e events.Event)
WInit func()
WMimeData func(md *mimedata.Mimes)
WNodeWalkDown func(fun func(n tree.Node) bool)
WOnAdd func()
WOnClose func()
WOnOpen func()
WPaste func()
WPlanName func() string
WPosition func()
WRender func()
WRenderDraw func(drw system.Drawer, op draw.Op)
WRenderWidget func()
WShowContextMenu func(e events.Event)
WSizeDown func(iter int) bool
WSizeFinal func()
WSizeUp func()
WStyle func()
WWidgetTooltip func(pos image.Point) (string, image.Point)
}
func (W _cogentcore_org_core_core_Treer) ApplyScenePos() { W.WApplyScenePos() }
func (W _cogentcore_org_core_core_Treer) AsCoreTree() *core.Tree { return W.WAsCoreTree() }
func (W _cogentcore_org_core_core_Treer) AsTree() *tree.NodeBase { return W.WAsTree() }
func (W _cogentcore_org_core_core_Treer) AsWidget() *core.WidgetBase { return W.WAsWidget() }
func (W _cogentcore_org_core_core_Treer) CanOpen() bool { return W.WCanOpen() }
func (W _cogentcore_org_core_core_Treer) ChildBackground(child core.Widget) image.Image {
return W.WChildBackground(child)
}
func (W _cogentcore_org_core_core_Treer) ContextMenuPos(e events.Event) image.Point {
return W.WContextMenuPos(e)
}
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) 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) }
func (W _cogentcore_org_core_core_Treer) Init() { W.WInit() }
func (W _cogentcore_org_core_core_Treer) MimeData(md *mimedata.Mimes) { W.WMimeData(md) }
func (W _cogentcore_org_core_core_Treer) NodeWalkDown(fun func(n tree.Node) bool) {
W.WNodeWalkDown(fun)
}
func (W _cogentcore_org_core_core_Treer) OnAdd() { W.WOnAdd() }
func (W _cogentcore_org_core_core_Treer) OnClose() { W.WOnClose() }
func (W _cogentcore_org_core_core_Treer) OnOpen() { W.WOnOpen() }
func (W _cogentcore_org_core_core_Treer) Paste() { W.WPaste() }
func (W _cogentcore_org_core_core_Treer) PlanName() string { return W.WPlanName() }
func (W _cogentcore_org_core_core_Treer) Position() { W.WPosition() }
func (W _cogentcore_org_core_core_Treer) Render() { W.WRender() }
func (W _cogentcore_org_core_core_Treer) RenderDraw(drw system.Drawer, op draw.Op) {
W.WRenderDraw(drw, op)
}
func (W _cogentcore_org_core_core_Treer) RenderWidget() { W.WRenderWidget() }
func (W _cogentcore_org_core_core_Treer) ShowContextMenu(e events.Event) { W.WShowContextMenu(e) }
func (W _cogentcore_org_core_core_Treer) SizeDown(iter int) bool { return W.WSizeDown(iter) }
func (W _cogentcore_org_core_core_Treer) SizeFinal() { W.WSizeFinal() }
func (W _cogentcore_org_core_core_Treer) SizeUp() { W.WSizeUp() }
func (W _cogentcore_org_core_core_Treer) Style() { W.WStyle() }
func (W _cogentcore_org_core_core_Treer) WidgetTooltip(pos image.Point) (string, image.Point) {
return W.WWidgetTooltip(pos)
}
// _cogentcore_org_core_core_Validator is an interface wrapper for Validator type
type _cogentcore_org_core_core_Validator struct {
IValue interface{}
WValidate func() error
}
func (W _cogentcore_org_core_core_Validator) Validate() error { return W.WValidate() }
// _cogentcore_org_core_core_Value is an interface wrapper for Value type
type _cogentcore_org_core_core_Value struct {
IValue interface{}
WApplyScenePos func()
WAsTree func() *tree.NodeBase
WAsWidget func() *core.WidgetBase
WChildBackground func(child core.Widget) image.Image
WContextMenuPos func(e events.Event) image.Point
WCopyFieldsFrom func(from tree.Node)
WDestroy func()
WInit func()
WNodeWalkDown func(fun func(n tree.Node) bool)
WOnAdd func()
WPlanName func() string
WPosition func()
WRender func()
WRenderDraw func(drw system.Drawer, op draw.Op)
WRenderWidget func()
WShowContextMenu func(e events.Event)
WSizeDown func(iter int) bool
WSizeFinal func()
WSizeUp func()
WStyle func()
WWidgetTooltip func(pos image.Point) (string, image.Point)
WWidgetValue func() any
}
func (W _cogentcore_org_core_core_Value) ApplyScenePos() { W.WApplyScenePos() }
func (W _cogentcore_org_core_core_Value) AsTree() *tree.NodeBase { return W.WAsTree() }
func (W _cogentcore_org_core_core_Value) AsWidget() *core.WidgetBase { return W.WAsWidget() }
func (W _cogentcore_org_core_core_Value) ChildBackground(child core.Widget) image.Image {
return W.WChildBackground(child)
}
func (W _cogentcore_org_core_core_Value) ContextMenuPos(e events.Event) image.Point {
return W.WContextMenuPos(e)
}
func (W _cogentcore_org_core_core_Value) CopyFieldsFrom(from tree.Node) { W.WCopyFieldsFrom(from) }
func (W _cogentcore_org_core_core_Value) Destroy() { W.WDestroy() }
func (W _cogentcore_org_core_core_Value) Init() { W.WInit() }
func (W _cogentcore_org_core_core_Value) NodeWalkDown(fun func(n tree.Node) bool) {
W.WNodeWalkDown(fun)
}
func (W _cogentcore_org_core_core_Value) OnAdd() { W.WOnAdd() }
func (W _cogentcore_org_core_core_Value) PlanName() string { return W.WPlanName() }
func (W _cogentcore_org_core_core_Value) Position() { W.WPosition() }
func (W _cogentcore_org_core_core_Value) Render() { W.WRender() }
func (W _cogentcore_org_core_core_Value) RenderDraw(drw system.Drawer, op draw.Op) {
W.WRenderDraw(drw, op)
}
func (W _cogentcore_org_core_core_Value) RenderWidget() { W.WRenderWidget() }
func (W _cogentcore_org_core_core_Value) ShowContextMenu(e events.Event) { W.WShowContextMenu(e) }
func (W _cogentcore_org_core_core_Value) SizeDown(iter int) bool { return W.WSizeDown(iter) }
func (W _cogentcore_org_core_core_Value) SizeFinal() { W.WSizeFinal() }
func (W _cogentcore_org_core_core_Value) SizeUp() { W.WSizeUp() }
func (W _cogentcore_org_core_core_Value) Style() { W.WStyle() }
func (W _cogentcore_org_core_core_Value) WidgetTooltip(pos image.Point) (string, image.Point) {
return W.WWidgetTooltip(pos)
}
func (W _cogentcore_org_core_core_Value) WidgetValue() any { return W.WWidgetValue() }
// _cogentcore_org_core_core_ValueSetter is an interface wrapper for ValueSetter type
type _cogentcore_org_core_core_ValueSetter struct {
IValue interface{}
WSetWidgetValue func(value any) error
}
func (W _cogentcore_org_core_core_ValueSetter) SetWidgetValue(value any) error {
return W.WSetWidgetValue(value)
}
// _cogentcore_org_core_core_Valuer is an interface wrapper for Valuer type
type _cogentcore_org_core_core_Valuer struct {
IValue interface{}
WValue func() core.Value
}
func (W _cogentcore_org_core_core_Valuer) Value() core.Value { return W.WValue() }
// _cogentcore_org_core_core_Widget is an interface wrapper for Widget type
type _cogentcore_org_core_core_Widget struct {
IValue interface{}
WApplyScenePos func()
WAsTree func() *tree.NodeBase
WAsWidget func() *core.WidgetBase
WChildBackground func(child core.Widget) image.Image
WContextMenuPos func(e events.Event) image.Point
WCopyFieldsFrom func(from tree.Node)
WDestroy func()
WInit func()
WNodeWalkDown func(fun func(n tree.Node) bool)
WOnAdd func()
WPlanName func() string
WPosition func()
WRender func()
WRenderDraw func(drw system.Drawer, op draw.Op)
WRenderWidget func()
WShowContextMenu func(e events.Event)
WSizeDown func(iter int) bool
WSizeFinal func()
WSizeUp func()
WStyle func()
WWidgetTooltip func(pos image.Point) (string, image.Point)
}
func (W _cogentcore_org_core_core_Widget) ApplyScenePos() { W.WApplyScenePos() }
func (W _cogentcore_org_core_core_Widget) AsTree() *tree.NodeBase { return W.WAsTree() }
func (W _cogentcore_org_core_core_Widget) AsWidget() *core.WidgetBase { return W.WAsWidget() }
func (W _cogentcore_org_core_core_Widget) ChildBackground(child core.Widget) image.Image {
return W.WChildBackground(child)
}
func (W _cogentcore_org_core_core_Widget) ContextMenuPos(e events.Event) image.Point {
return W.WContextMenuPos(e)
}
func (W _cogentcore_org_core_core_Widget) CopyFieldsFrom(from tree.Node) { W.WCopyFieldsFrom(from) }
func (W _cogentcore_org_core_core_Widget) Destroy() { W.WDestroy() }
func (W _cogentcore_org_core_core_Widget) Init() { W.WInit() }
func (W _cogentcore_org_core_core_Widget) NodeWalkDown(fun func(n tree.Node) bool) {
W.WNodeWalkDown(fun)
}
func (W _cogentcore_org_core_core_Widget) OnAdd() { W.WOnAdd() }
func (W _cogentcore_org_core_core_Widget) PlanName() string { return W.WPlanName() }
func (W _cogentcore_org_core_core_Widget) Position() { W.WPosition() }
func (W _cogentcore_org_core_core_Widget) Render() { W.WRender() }
func (W _cogentcore_org_core_core_Widget) RenderDraw(drw system.Drawer, op draw.Op) {
W.WRenderDraw(drw, op)
}
func (W _cogentcore_org_core_core_Widget) RenderWidget() { W.WRenderWidget() }
func (W _cogentcore_org_core_core_Widget) ShowContextMenu(e events.Event) { W.WShowContextMenu(e) }
func (W _cogentcore_org_core_core_Widget) SizeDown(iter int) bool { return W.WSizeDown(iter) }
func (W _cogentcore_org_core_core_Widget) SizeFinal() { W.WSizeFinal() }
func (W _cogentcore_org_core_core_Widget) SizeUp() { W.WSizeUp() }
func (W _cogentcore_org_core_core_Widget) Style() { W.WStyle() }
func (W _cogentcore_org_core_core_Widget) WidgetTooltip(pos image.Point) (string, image.Point) {
return W.WWidgetTooltip(pos)
}
// Code generated by 'yaegi extract cogentcore.org/core/events'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/enums"
"cogentcore.org/core/events"
"cogentcore.org/core/events/key"
"image"
"reflect"
"time"
)
func init() {
Symbols["cogentcore.org/core/events/events"] = map[string]reflect.Value{
// function, constant and variable definitions
"ButtonsN": reflect.ValueOf(events.ButtonsN),
"ButtonsValues": reflect.ValueOf(events.ButtonsValues),
"Change": reflect.ValueOf(events.Change),
"Click": reflect.ValueOf(events.Click),
"Close": reflect.ValueOf(events.Close),
"ContextMenu": reflect.ValueOf(events.ContextMenu),
"Custom": reflect.ValueOf(events.Custom),
"DefaultModBits": reflect.ValueOf(events.DefaultModBits),
"DoubleClick": reflect.ValueOf(events.DoubleClick),
"DragEnter": reflect.ValueOf(events.DragEnter),
"DragLeave": reflect.ValueOf(events.DragLeave),
"DragMove": reflect.ValueOf(events.DragMove),
"DragStart": reflect.ValueOf(events.DragStart),
"Drop": reflect.ValueOf(events.Drop),
"DropCopy": reflect.ValueOf(events.DropCopy),
"DropDeleteSource": reflect.ValueOf(events.DropDeleteSource),
"DropIgnore": reflect.ValueOf(events.DropIgnore),
"DropLink": reflect.ValueOf(events.DropLink),
"DropModsN": reflect.ValueOf(events.DropModsN),
"DropModsValues": reflect.ValueOf(events.DropModsValues),
"DropMove": reflect.ValueOf(events.DropMove),
"EventFlagsN": reflect.ValueOf(events.EventFlagsN),
"EventFlagsValues": reflect.ValueOf(events.EventFlagsValues),
"ExtendContinuous": reflect.ValueOf(events.ExtendContinuous),
"ExtendOne": reflect.ValueOf(events.ExtendOne),
"Focus": reflect.ValueOf(events.Focus),
"FocusLost": reflect.ValueOf(events.FocusLost),
"Handled": reflect.ValueOf(events.Handled),
"Input": reflect.ValueOf(events.Input),
"KeyChord": reflect.ValueOf(events.KeyChord),
"KeyDown": reflect.ValueOf(events.KeyDown),
"KeyUp": reflect.ValueOf(events.KeyUp),
"Left": reflect.ValueOf(events.Left),
"LongHoverEnd": reflect.ValueOf(events.LongHoverEnd),
"LongHoverStart": reflect.ValueOf(events.LongHoverStart),
"LongPressEnd": reflect.ValueOf(events.LongPressEnd),
"LongPressStart": reflect.ValueOf(events.LongPressStart),
"Magnify": reflect.ValueOf(events.Magnify),
"Middle": reflect.ValueOf(events.Middle),
"MouseDown": reflect.ValueOf(events.MouseDown),
"MouseDrag": reflect.ValueOf(events.MouseDrag),
"MouseEnter": reflect.ValueOf(events.MouseEnter),
"MouseLeave": reflect.ValueOf(events.MouseLeave),
"MouseMove": reflect.ValueOf(events.MouseMove),
"MouseUp": reflect.ValueOf(events.MouseUp),
"NewDragDrop": reflect.ValueOf(events.NewDragDrop),
"NewExternalDrop": reflect.ValueOf(events.NewExternalDrop),
"NewKey": reflect.ValueOf(events.NewKey),
"NewMagnify": reflect.ValueOf(events.NewMagnify),
"NewMouse": reflect.ValueOf(events.NewMouse),
"NewMouseDrag": reflect.ValueOf(events.NewMouseDrag),
"NewMouseMove": reflect.ValueOf(events.NewMouseMove),
"NewOSEvent": reflect.ValueOf(events.NewOSEvent),
"NewOSFiles": reflect.ValueOf(events.NewOSFiles),
"NewScroll": reflect.ValueOf(events.NewScroll),
"NewTouch": reflect.ValueOf(events.NewTouch),
"NewWindow": reflect.ValueOf(events.NewWindow),
"NewWindowPaint": reflect.ValueOf(events.NewWindowPaint),
"NewWindowResize": reflect.ValueOf(events.NewWindowResize),
"NoButton": reflect.ValueOf(events.NoButton),
"NoDropMod": reflect.ValueOf(events.NoDropMod),
"NoSelect": reflect.ValueOf(events.NoSelect),
"NoWinAction": reflect.ValueOf(events.NoWinAction),
"OS": reflect.ValueOf(events.OS),
"OSOpenFiles": reflect.ValueOf(events.OSOpenFiles),
"Right": reflect.ValueOf(events.Right),
"Rotate": reflect.ValueOf(events.Rotate),
"ScreenUpdate": reflect.ValueOf(events.ScreenUpdate),
"Scroll": reflect.ValueOf(events.Scroll),
"ScrollWheelSpeed": reflect.ValueOf(&events.ScrollWheelSpeed).Elem(),
"Select": reflect.ValueOf(events.Select),
"SelectModeBits": reflect.ValueOf(events.SelectModeBits),
"SelectModesN": reflect.ValueOf(events.SelectModesN),
"SelectModesValues": reflect.ValueOf(events.SelectModesValues),
"SelectOne": reflect.ValueOf(events.SelectOne),
"SelectQuiet": reflect.ValueOf(events.SelectQuiet),
"Show": reflect.ValueOf(events.Show),
"SlideMove": reflect.ValueOf(events.SlideMove),
"SlideStart": reflect.ValueOf(events.SlideStart),
"SlideStop": reflect.ValueOf(events.SlideStop),
"TouchEnd": reflect.ValueOf(events.TouchEnd),
"TouchMove": reflect.ValueOf(events.TouchMove),
"TouchStart": reflect.ValueOf(events.TouchStart),
"TraceEventCompression": reflect.ValueOf(&events.TraceEventCompression).Elem(),
"TraceWindowPaint": reflect.ValueOf(&events.TraceWindowPaint).Elem(),
"TripleClick": reflect.ValueOf(events.TripleClick),
"TypesN": reflect.ValueOf(events.TypesN),
"TypesValues": reflect.ValueOf(events.TypesValues),
"Unique": reflect.ValueOf(events.Unique),
"UnknownType": reflect.ValueOf(events.UnknownType),
"Unselect": reflect.ValueOf(events.Unselect),
"UnselectQuiet": reflect.ValueOf(events.UnselectQuiet),
"WinActionsN": reflect.ValueOf(events.WinActionsN),
"WinActionsValues": reflect.ValueOf(events.WinActionsValues),
"WinClose": reflect.ValueOf(events.WinClose),
"WinFocus": reflect.ValueOf(events.WinFocus),
"WinFocusLost": reflect.ValueOf(events.WinFocusLost),
"WinMinimize": reflect.ValueOf(events.WinMinimize),
"WinMove": reflect.ValueOf(events.WinMove),
"WinShow": reflect.ValueOf(events.WinShow),
"Window": reflect.ValueOf(events.Window),
"WindowPaint": reflect.ValueOf(events.WindowPaint),
"WindowResize": reflect.ValueOf(events.WindowResize),
// type definitions
"Base": reflect.ValueOf((*events.Base)(nil)),
"Buttons": reflect.ValueOf((*events.Buttons)(nil)),
"CustomEvent": reflect.ValueOf((*events.CustomEvent)(nil)),
"Deque": reflect.ValueOf((*events.Deque)(nil)),
"DragDrop": reflect.ValueOf((*events.DragDrop)(nil)),
"DropMods": reflect.ValueOf((*events.DropMods)(nil)),
"Event": reflect.ValueOf((*events.Event)(nil)),
"EventFlags": reflect.ValueOf((*events.EventFlags)(nil)),
"Key": reflect.ValueOf((*events.Key)(nil)),
"Listeners": reflect.ValueOf((*events.Listeners)(nil)),
"Mouse": reflect.ValueOf((*events.Mouse)(nil)),
"MouseScroll": reflect.ValueOf((*events.MouseScroll)(nil)),
"OSEvent": reflect.ValueOf((*events.OSEvent)(nil)),
"OSFiles": reflect.ValueOf((*events.OSFiles)(nil)),
"SelectModes": reflect.ValueOf((*events.SelectModes)(nil)),
"Sequence": reflect.ValueOf((*events.Sequence)(nil)),
"Source": reflect.ValueOf((*events.Source)(nil)),
"SourceState": reflect.ValueOf((*events.SourceState)(nil)),
"Touch": reflect.ValueOf((*events.Touch)(nil)),
"TouchMagnify": reflect.ValueOf((*events.TouchMagnify)(nil)),
"Types": reflect.ValueOf((*events.Types)(nil)),
"WinActions": reflect.ValueOf((*events.WinActions)(nil)),
"WindowEvent": reflect.ValueOf((*events.WindowEvent)(nil)),
// interface wrapper definitions
"_Event": reflect.ValueOf((*_cogentcore_org_core_events_Event)(nil)),
}
}
// _cogentcore_org_core_events_Event is an interface wrapper for Event type
type _cogentcore_org_core_events_Event struct {
IValue interface{}
WAsBase func() *events.Base
WClearHandled func()
WClone func() events.Event
WHasAllModifiers func(mods ...enums.BitFlag) bool
WHasAnyModifier func(mods ...enums.BitFlag) bool
WHasPos func() bool
WInit func()
WIsHandled func() bool
WIsSame func(oth events.Event) bool
WIsUnique func() bool
WKeyChord func() key.Chord
WKeyCode func() key.Codes
WKeyRune func() rune
WLocalOff func() image.Point
WModifiers func() key.Modifiers
WMouseButton func() events.Buttons
WNeedsFocus func() bool
WNewFromClone func(typ events.Types) events.Event
WPos func() image.Point
WPrevDelta func() image.Point
WPrevPos func() image.Point
WPrevTime func() time.Time
WSelectMode func() events.SelectModes
WSetHandled func()
WSetLocalOff func(off image.Point)
WSetTime func()
WSincePrev func() time.Duration
WSinceStart func() time.Duration
WStartDelta func() image.Point
WStartPos func() image.Point
WStartTime func() time.Time
WString func() string
WTime func() time.Time
WType func() events.Types
WWindowPos func() image.Point
WWindowPrevPos func() image.Point
WWindowStartPos func() image.Point
}
func (W _cogentcore_org_core_events_Event) AsBase() *events.Base { return W.WAsBase() }
func (W _cogentcore_org_core_events_Event) ClearHandled() { W.WClearHandled() }
func (W _cogentcore_org_core_events_Event) Clone() events.Event { return W.WClone() }
func (W _cogentcore_org_core_events_Event) HasAllModifiers(mods ...enums.BitFlag) bool {
return W.WHasAllModifiers(mods...)
}
func (W _cogentcore_org_core_events_Event) HasAnyModifier(mods ...enums.BitFlag) bool {
return W.WHasAnyModifier(mods...)
}
func (W _cogentcore_org_core_events_Event) HasPos() bool { return W.WHasPos() }
func (W _cogentcore_org_core_events_Event) Init() { W.WInit() }
func (W _cogentcore_org_core_events_Event) IsHandled() bool { return W.WIsHandled() }
func (W _cogentcore_org_core_events_Event) IsSame(oth events.Event) bool { return W.WIsSame(oth) }
func (W _cogentcore_org_core_events_Event) IsUnique() bool { return W.WIsUnique() }
func (W _cogentcore_org_core_events_Event) KeyChord() key.Chord { return W.WKeyChord() }
func (W _cogentcore_org_core_events_Event) KeyCode() key.Codes { return W.WKeyCode() }
func (W _cogentcore_org_core_events_Event) KeyRune() rune { return W.WKeyRune() }
func (W _cogentcore_org_core_events_Event) LocalOff() image.Point { return W.WLocalOff() }
func (W _cogentcore_org_core_events_Event) Modifiers() key.Modifiers { return W.WModifiers() }
func (W _cogentcore_org_core_events_Event) MouseButton() events.Buttons { return W.WMouseButton() }
func (W _cogentcore_org_core_events_Event) NeedsFocus() bool { return W.WNeedsFocus() }
func (W _cogentcore_org_core_events_Event) NewFromClone(typ events.Types) events.Event {
return W.WNewFromClone(typ)
}
func (W _cogentcore_org_core_events_Event) Pos() image.Point { return W.WPos() }
func (W _cogentcore_org_core_events_Event) PrevDelta() image.Point { return W.WPrevDelta() }
func (W _cogentcore_org_core_events_Event) PrevPos() image.Point { return W.WPrevPos() }
func (W _cogentcore_org_core_events_Event) PrevTime() time.Time { return W.WPrevTime() }
func (W _cogentcore_org_core_events_Event) SelectMode() events.SelectModes { return W.WSelectMode() }
func (W _cogentcore_org_core_events_Event) SetHandled() { W.WSetHandled() }
func (W _cogentcore_org_core_events_Event) SetLocalOff(off image.Point) { W.WSetLocalOff(off) }
func (W _cogentcore_org_core_events_Event) SetTime() { W.WSetTime() }
func (W _cogentcore_org_core_events_Event) SincePrev() time.Duration { return W.WSincePrev() }
func (W _cogentcore_org_core_events_Event) SinceStart() time.Duration { return W.WSinceStart() }
func (W _cogentcore_org_core_events_Event) StartDelta() image.Point { return W.WStartDelta() }
func (W _cogentcore_org_core_events_Event) StartPos() image.Point { return W.WStartPos() }
func (W _cogentcore_org_core_events_Event) StartTime() time.Time { return W.WStartTime() }
func (W _cogentcore_org_core_events_Event) String() string {
if W.WString == nil {
return ""
}
return W.WString()
}
func (W _cogentcore_org_core_events_Event) Time() time.Time { return W.WTime() }
func (W _cogentcore_org_core_events_Event) Type() events.Types { return W.WType() }
func (W _cogentcore_org_core_events_Event) WindowPos() image.Point { return W.WWindowPos() }
func (W _cogentcore_org_core_events_Event) WindowPrevPos() image.Point { return W.WWindowPrevPos() }
func (W _cogentcore_org_core_events_Event) WindowStartPos() image.Point { return W.WWindowStartPos() }
// Code generated by 'yaegi extract cogentcore.org/core/filetree'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/filetree"
"cogentcore.org/core/system"
"cogentcore.org/core/tree"
"image"
"image/draw"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/filetree/filetree"] = map[string]reflect.Value{
// function, constant and variable definitions
"AsNode": reflect.ValueOf(filetree.AsNode),
"FindLocationAll": reflect.ValueOf(filetree.FindLocationAll),
"FindLocationDir": reflect.ValueOf(filetree.FindLocationDir),
"FindLocationFile": reflect.ValueOf(filetree.FindLocationFile),
"FindLocationN": reflect.ValueOf(filetree.FindLocationN),
"FindLocationNotTop": reflect.ValueOf(filetree.FindLocationNotTop),
"FindLocationOpen": reflect.ValueOf(filetree.FindLocationOpen),
"FindLocationValues": reflect.ValueOf(filetree.FindLocationValues),
"NewNode": reflect.ValueOf(filetree.NewNode),
"NewTree": reflect.ValueOf(filetree.NewTree),
"NewVCSLog": reflect.ValueOf(filetree.NewVCSLog),
"NodeHighlighting": reflect.ValueOf(&filetree.NodeHighlighting).Elem(),
"NodeNameCountSort": reflect.ValueOf(filetree.NodeNameCountSort),
"Search": reflect.ValueOf(filetree.Search),
// type definitions
"DirFlagMap": reflect.ValueOf((*filetree.DirFlagMap)(nil)),
"Filer": reflect.ValueOf((*filetree.Filer)(nil)),
"FindLocation": reflect.ValueOf((*filetree.FindLocation)(nil)),
"Node": reflect.ValueOf((*filetree.Node)(nil)),
"NodeEmbedder": reflect.ValueOf((*filetree.NodeEmbedder)(nil)),
"NodeNameCount": reflect.ValueOf((*filetree.NodeNameCount)(nil)),
"SearchResults": reflect.ValueOf((*filetree.SearchResults)(nil)),
"Tree": reflect.ValueOf((*filetree.Tree)(nil)),
"VCSLog": reflect.ValueOf((*filetree.VCSLog)(nil)),
// interface wrapper definitions
"_Filer": reflect.ValueOf((*_cogentcore_org_core_filetree_Filer)(nil)),
"_NodeEmbedder": reflect.ValueOf((*_cogentcore_org_core_filetree_NodeEmbedder)(nil)),
}
}
// _cogentcore_org_core_filetree_Filer is an interface wrapper for Filer type
type _cogentcore_org_core_filetree_Filer struct {
IValue interface{}
WApplyScenePos func()
WAsCoreTree func() *core.Tree
WAsFileNode func() *filetree.Node
WAsTree func() *tree.NodeBase
WAsWidget func() *core.WidgetBase
WCanOpen func() bool
WChildBackground func(child core.Widget) image.Image
WContextMenuPos func(e events.Event) image.Point
WCopy func()
WCopyFieldsFrom func(from tree.Node)
WCut func()
WDestroy func()
WDragDrop func(e events.Event)
WDropDeleteSource func(e events.Event)
WGetFileInfo func() error
WInit func()
WMimeData func(md *mimedata.Mimes)
WNodeWalkDown func(fun func(n tree.Node) bool)
WOnAdd func()
WOnClose func()
WOnOpen func()
WOpenFile func() error
WPaste func()
WPlanName func() string
WPosition func()
WRenameFiles func()
WRender func()
WRenderDraw func(drw system.Drawer, op draw.Op)
WRenderWidget func()
WShowContextMenu func(e events.Event)
WSizeDown func(iter int) bool
WSizeFinal func()
WSizeUp func()
WStyle func()
WWidgetTooltip func(pos image.Point) (string, image.Point)
}
func (W _cogentcore_org_core_filetree_Filer) ApplyScenePos() { W.WApplyScenePos() }
func (W _cogentcore_org_core_filetree_Filer) AsCoreTree() *core.Tree { return W.WAsCoreTree() }
func (W _cogentcore_org_core_filetree_Filer) AsFileNode() *filetree.Node { return W.WAsFileNode() }
func (W _cogentcore_org_core_filetree_Filer) AsTree() *tree.NodeBase { return W.WAsTree() }
func (W _cogentcore_org_core_filetree_Filer) AsWidget() *core.WidgetBase { return W.WAsWidget() }
func (W _cogentcore_org_core_filetree_Filer) CanOpen() bool { return W.WCanOpen() }
func (W _cogentcore_org_core_filetree_Filer) ChildBackground(child core.Widget) image.Image {
return W.WChildBackground(child)
}
func (W _cogentcore_org_core_filetree_Filer) ContextMenuPos(e events.Event) image.Point {
return W.WContextMenuPos(e)
}
func (W _cogentcore_org_core_filetree_Filer) Copy() { W.WCopy() }
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) 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) }
func (W _cogentcore_org_core_filetree_Filer) GetFileInfo() error { return W.WGetFileInfo() }
func (W _cogentcore_org_core_filetree_Filer) Init() { W.WInit() }
func (W _cogentcore_org_core_filetree_Filer) MimeData(md *mimedata.Mimes) { W.WMimeData(md) }
func (W _cogentcore_org_core_filetree_Filer) NodeWalkDown(fun func(n tree.Node) bool) {
W.WNodeWalkDown(fun)
}
func (W _cogentcore_org_core_filetree_Filer) OnAdd() { W.WOnAdd() }
func (W _cogentcore_org_core_filetree_Filer) OnClose() { W.WOnClose() }
func (W _cogentcore_org_core_filetree_Filer) OnOpen() { W.WOnOpen() }
func (W _cogentcore_org_core_filetree_Filer) OpenFile() error { return W.WOpenFile() }
func (W _cogentcore_org_core_filetree_Filer) Paste() { W.WPaste() }
func (W _cogentcore_org_core_filetree_Filer) PlanName() string { return W.WPlanName() }
func (W _cogentcore_org_core_filetree_Filer) Position() { W.WPosition() }
func (W _cogentcore_org_core_filetree_Filer) RenameFiles() { W.WRenameFiles() }
func (W _cogentcore_org_core_filetree_Filer) Render() { W.WRender() }
func (W _cogentcore_org_core_filetree_Filer) RenderDraw(drw system.Drawer, op draw.Op) {
W.WRenderDraw(drw, op)
}
func (W _cogentcore_org_core_filetree_Filer) RenderWidget() { W.WRenderWidget() }
func (W _cogentcore_org_core_filetree_Filer) ShowContextMenu(e events.Event) { W.WShowContextMenu(e) }
func (W _cogentcore_org_core_filetree_Filer) SizeDown(iter int) bool { return W.WSizeDown(iter) }
func (W _cogentcore_org_core_filetree_Filer) SizeFinal() { W.WSizeFinal() }
func (W _cogentcore_org_core_filetree_Filer) SizeUp() { W.WSizeUp() }
func (W _cogentcore_org_core_filetree_Filer) Style() { W.WStyle() }
func (W _cogentcore_org_core_filetree_Filer) WidgetTooltip(pos image.Point) (string, image.Point) {
return W.WWidgetTooltip(pos)
}
// _cogentcore_org_core_filetree_NodeEmbedder is an interface wrapper for NodeEmbedder type
type _cogentcore_org_core_filetree_NodeEmbedder struct {
IValue interface{}
WAsNode func() *filetree.Node
}
func (W _cogentcore_org_core_filetree_NodeEmbedder) AsNode() *filetree.Node { return W.WAsNode() }
// Code generated by 'yaegi extract cogentcore.org/core/htmlcore'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/htmlcore"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/htmlcore/htmlcore"] = map[string]reflect.Value{
// function, constant and variable definitions
"BindTextEditor": reflect.ValueOf(&htmlcore.BindTextEditor).Elem(),
"ElementHandlers": reflect.ValueOf(&htmlcore.ElementHandlers).Elem(),
"ExtractText": reflect.ValueOf(htmlcore.ExtractText),
"Get": reflect.ValueOf(htmlcore.Get),
"NewContext": reflect.ValueOf(htmlcore.NewContext),
"ReadHTML": reflect.ValueOf(htmlcore.ReadHTML),
"ReadHTMLString": reflect.ValueOf(htmlcore.ReadHTMLString),
"ReadMD": reflect.ValueOf(htmlcore.ReadMD),
"ReadMDString": reflect.ValueOf(htmlcore.ReadMDString),
"WikilinkBaseURL": reflect.ValueOf(&htmlcore.WikilinkBaseURL).Elem(),
// type definitions
"Context": reflect.ValueOf((*htmlcore.Context)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/icons'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/icons"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/icons/icons"] = map[string]reflect.Value{
// function, constant and variable definitions
"Abc": reflect.ValueOf(&icons.Abc).Elem(),
"AbcFill": reflect.ValueOf(&icons.AbcFill).Elem(),
"AccountCircle": reflect.ValueOf(&icons.AccountCircle).Elem(),
"AccountCircleFill": reflect.ValueOf(&icons.AccountCircleFill).Elem(),
"AccountCircleOff": reflect.ValueOf(&icons.AccountCircleOff).Elem(),
"AccountCircleOffFill": reflect.ValueOf(&icons.AccountCircleOffFill).Elem(),
"Ad": reflect.ValueOf(&icons.Ad).Elem(),
"AdFill": reflect.ValueOf(&icons.AdFill).Elem(),
"AdOff": reflect.ValueOf(&icons.AdOff).Elem(),
"AdOffFill": reflect.ValueOf(&icons.AdOffFill).Elem(),
"Adb": reflect.ValueOf(&icons.Adb).Elem(),
"AdbFill": reflect.ValueOf(&icons.AdbFill).Elem(),
"Add": reflect.ValueOf(&icons.Add).Elem(),
"AddAPhoto": reflect.ValueOf(&icons.AddAPhoto).Elem(),
"AddAPhotoFill": reflect.ValueOf(&icons.AddAPhotoFill).Elem(),
"AddAlert": reflect.ValueOf(&icons.AddAlert).Elem(),
"AddAlertFill": reflect.ValueOf(&icons.AddAlertFill).Elem(),
"AddBox": reflect.ValueOf(&icons.AddBox).Elem(),
"AddBoxFill": reflect.ValueOf(&icons.AddBoxFill).Elem(),
"AddCall": reflect.ValueOf(&icons.AddCall).Elem(),
"AddCallFill": reflect.ValueOf(&icons.AddCallFill).Elem(),
"AddCard": reflect.ValueOf(&icons.AddCard).Elem(),
"AddCardFill": reflect.ValueOf(&icons.AddCardFill).Elem(),
"AddChart": reflect.ValueOf(&icons.AddChart).Elem(),
"AddChartFill": reflect.ValueOf(&icons.AddChartFill).Elem(),
"AddCircle": reflect.ValueOf(&icons.AddCircle).Elem(),
"AddCircleFill": reflect.ValueOf(&icons.AddCircleFill).Elem(),
"AddComment": reflect.ValueOf(&icons.AddComment).Elem(),
"AddCommentFill": reflect.ValueOf(&icons.AddCommentFill).Elem(),
"AddFill": reflect.ValueOf(&icons.AddFill).Elem(),
"AddHome": reflect.ValueOf(&icons.AddHome).Elem(),
"AddHomeFill": reflect.ValueOf(&icons.AddHomeFill).Elem(),
"AddHomeWork": reflect.ValueOf(&icons.AddHomeWork).Elem(),
"AddHomeWorkFill": reflect.ValueOf(&icons.AddHomeWorkFill).Elem(),
"AddLink": reflect.ValueOf(&icons.AddLink).Elem(),
"AddLinkFill": reflect.ValueOf(&icons.AddLinkFill).Elem(),
"AddLocation": reflect.ValueOf(&icons.AddLocation).Elem(),
"AddLocationFill": reflect.ValueOf(&icons.AddLocationFill).Elem(),
"AddNotes": reflect.ValueOf(&icons.AddNotes).Elem(),
"AddNotesFill": reflect.ValueOf(&icons.AddNotesFill).Elem(),
"AddShoppingCart": reflect.ValueOf(&icons.AddShoppingCart).Elem(),
"AddShoppingCartFill": reflect.ValueOf(&icons.AddShoppingCartFill).Elem(),
"AddTask": reflect.ValueOf(&icons.AddTask).Elem(),
"AddTaskFill": reflect.ValueOf(&icons.AddTaskFill).Elem(),
"AddToQueue": reflect.ValueOf(&icons.AddToQueue).Elem(),
"AddToQueueFill": reflect.ValueOf(&icons.AddToQueueFill).Elem(),
"Adjust": reflect.ValueOf(&icons.Adjust).Elem(),
"AdjustFill": reflect.ValueOf(&icons.AdjustFill).Elem(),
"AdminMeds": reflect.ValueOf(&icons.AdminMeds).Elem(),
"AdminMedsFill": reflect.ValueOf(&icons.AdminMedsFill).Elem(),
"AdminPanelSettings": reflect.ValueOf(&icons.AdminPanelSettings).Elem(),
"AdminPanelSettingsFill": reflect.ValueOf(&icons.AdminPanelSettingsFill).Elem(),
"Agender": reflect.ValueOf(&icons.Agender).Elem(),
"AgenderFill": reflect.ValueOf(&icons.AgenderFill).Elem(),
"Agriculture": reflect.ValueOf(&icons.Agriculture).Elem(),
"AgricultureFill": reflect.ValueOf(&icons.AgricultureFill).Elem(),
"Air": reflect.ValueOf(&icons.Air).Elem(),
"AirFill": reflect.ValueOf(&icons.AirFill).Elem(),
"Airplay": reflect.ValueOf(&icons.Airplay).Elem(),
"AirplayFill": reflect.ValueOf(&icons.AirplayFill).Elem(),
"Alarm": reflect.ValueOf(&icons.Alarm).Elem(),
"AlarmAdd": reflect.ValueOf(&icons.AlarmAdd).Elem(),
"AlarmAddFill": reflect.ValueOf(&icons.AlarmAddFill).Elem(),
"AlarmFill": reflect.ValueOf(&icons.AlarmFill).Elem(),
"AlarmOff": reflect.ValueOf(&icons.AlarmOff).Elem(),
"AlarmOffFill": reflect.ValueOf(&icons.AlarmOffFill).Elem(),
"AlarmOn": reflect.ValueOf(&icons.AlarmOn).Elem(),
"AlarmOnFill": reflect.ValueOf(&icons.AlarmOnFill).Elem(),
"Album": reflect.ValueOf(&icons.Album).Elem(),
"AlbumFill": reflect.ValueOf(&icons.AlbumFill).Elem(),
"AlignCenter": reflect.ValueOf(&icons.AlignCenter).Elem(),
"AlignCenterFill": reflect.ValueOf(&icons.AlignCenterFill).Elem(),
"AlignEnd": reflect.ValueOf(&icons.AlignEnd).Elem(),
"AlignEndFill": reflect.ValueOf(&icons.AlignEndFill).Elem(),
"AlignFlexCenter": reflect.ValueOf(&icons.AlignFlexCenter).Elem(),
"AlignFlexCenterFill": reflect.ValueOf(&icons.AlignFlexCenterFill).Elem(),
"AlignFlexEnd": reflect.ValueOf(&icons.AlignFlexEnd).Elem(),
"AlignFlexEndFill": reflect.ValueOf(&icons.AlignFlexEndFill).Elem(),
"AlignFlexStart": reflect.ValueOf(&icons.AlignFlexStart).Elem(),
"AlignFlexStartFill": reflect.ValueOf(&icons.AlignFlexStartFill).Elem(),
"AlignHorizontalCenter": reflect.ValueOf(&icons.AlignHorizontalCenter).Elem(),
"AlignHorizontalCenterFill": reflect.ValueOf(&icons.AlignHorizontalCenterFill).Elem(),
"AlignHorizontalLeft": reflect.ValueOf(&icons.AlignHorizontalLeft).Elem(),
"AlignHorizontalLeftFill": reflect.ValueOf(&icons.AlignHorizontalLeftFill).Elem(),
"AlignHorizontalRight": reflect.ValueOf(&icons.AlignHorizontalRight).Elem(),
"AlignHorizontalRightFill": reflect.ValueOf(&icons.AlignHorizontalRightFill).Elem(),
"AlignItemsStretch": reflect.ValueOf(&icons.AlignItemsStretch).Elem(),
"AlignItemsStretchFill": reflect.ValueOf(&icons.AlignItemsStretchFill).Elem(),
"AlignJustifyCenter": reflect.ValueOf(&icons.AlignJustifyCenter).Elem(),
"AlignJustifyCenterFill": reflect.ValueOf(&icons.AlignJustifyCenterFill).Elem(),
"AlignJustifyFlexEnd": reflect.ValueOf(&icons.AlignJustifyFlexEnd).Elem(),
"AlignJustifyFlexEndFill": reflect.ValueOf(&icons.AlignJustifyFlexEndFill).Elem(),
"AlignJustifyFlexStart": reflect.ValueOf(&icons.AlignJustifyFlexStart).Elem(),
"AlignJustifyFlexStartFill": reflect.ValueOf(&icons.AlignJustifyFlexStartFill).Elem(),
"AlignJustifySpaceAround": reflect.ValueOf(&icons.AlignJustifySpaceAround).Elem(),
"AlignJustifySpaceAroundFill": reflect.ValueOf(&icons.AlignJustifySpaceAroundFill).Elem(),
"AlignJustifySpaceBetween": reflect.ValueOf(&icons.AlignJustifySpaceBetween).Elem(),
"AlignJustifySpaceBetweenFill": reflect.ValueOf(&icons.AlignJustifySpaceBetweenFill).Elem(),
"AlignJustifySpaceEven": reflect.ValueOf(&icons.AlignJustifySpaceEven).Elem(),
"AlignJustifySpaceEvenFill": reflect.ValueOf(&icons.AlignJustifySpaceEvenFill).Elem(),
"AlignJustifyStretch": reflect.ValueOf(&icons.AlignJustifyStretch).Elem(),
"AlignJustifyStretchFill": reflect.ValueOf(&icons.AlignJustifyStretchFill).Elem(),
"AlignSelfStretch": reflect.ValueOf(&icons.AlignSelfStretch).Elem(),
"AlignSelfStretchFill": reflect.ValueOf(&icons.AlignSelfStretchFill).Elem(),
"AlignSpaceAround": reflect.ValueOf(&icons.AlignSpaceAround).Elem(),
"AlignSpaceAroundFill": reflect.ValueOf(&icons.AlignSpaceAroundFill).Elem(),
"AlignSpaceBetween": reflect.ValueOf(&icons.AlignSpaceBetween).Elem(),
"AlignSpaceBetweenFill": reflect.ValueOf(&icons.AlignSpaceBetweenFill).Elem(),
"AlignSpaceEven": reflect.ValueOf(&icons.AlignSpaceEven).Elem(),
"AlignSpaceEvenFill": reflect.ValueOf(&icons.AlignSpaceEvenFill).Elem(),
"AlignStart": reflect.ValueOf(&icons.AlignStart).Elem(),
"AlignStartFill": reflect.ValueOf(&icons.AlignStartFill).Elem(),
"AlignStretch": reflect.ValueOf(&icons.AlignStretch).Elem(),
"AlignStretchFill": reflect.ValueOf(&icons.AlignStretchFill).Elem(),
"AlignVerticalBottom": reflect.ValueOf(&icons.AlignVerticalBottom).Elem(),
"AlignVerticalBottomFill": reflect.ValueOf(&icons.AlignVerticalBottomFill).Elem(),
"AlignVerticalCenter": reflect.ValueOf(&icons.AlignVerticalCenter).Elem(),
"AlignVerticalCenterFill": reflect.ValueOf(&icons.AlignVerticalCenterFill).Elem(),
"AlignVerticalTop": reflect.ValueOf(&icons.AlignVerticalTop).Elem(),
"AlignVerticalTopFill": reflect.ValueOf(&icons.AlignVerticalTopFill).Elem(),
"AllInbox": reflect.ValueOf(&icons.AllInbox).Elem(),
"AllInboxFill": reflect.ValueOf(&icons.AllInboxFill).Elem(),
"AllMatch": reflect.ValueOf(&icons.AllMatch).Elem(),
"AllMatchFill": reflect.ValueOf(&icons.AllMatchFill).Elem(),
"AllOut": reflect.ValueOf(&icons.AllOut).Elem(),
"AllOutFill": reflect.ValueOf(&icons.AllOutFill).Elem(),
"AltRoute": reflect.ValueOf(&icons.AltRoute).Elem(),
"AltRouteFill": reflect.ValueOf(&icons.AltRouteFill).Elem(),
"AlternateEmail": reflect.ValueOf(&icons.AlternateEmail).Elem(),
"AlternateEmailFill": reflect.ValueOf(&icons.AlternateEmailFill).Elem(),
"Altitude": reflect.ValueOf(&icons.Altitude).Elem(),
"AltitudeFill": reflect.ValueOf(&icons.AltitudeFill).Elem(),
"Amend": reflect.ValueOf(&icons.Amend).Elem(),
"AmendFill": reflect.ValueOf(&icons.AmendFill).Elem(),
"Analytics": reflect.ValueOf(&icons.Analytics).Elem(),
"AnalyticsFill": reflect.ValueOf(&icons.AnalyticsFill).Elem(),
"Anchor": reflect.ValueOf(&icons.Anchor).Elem(),
"AnchorFill": reflect.ValueOf(&icons.AnchorFill).Elem(),
"Android": reflect.ValueOf(&icons.Android).Elem(),
"AndroidFill": reflect.ValueOf(&icons.AndroidFill).Elem(),
"Animation": reflect.ValueOf(&icons.Animation).Elem(),
"AnimationFill": reflect.ValueOf(&icons.AnimationFill).Elem(),
"Apartment": reflect.ValueOf(&icons.Apartment).Elem(),
"ApartmentFill": reflect.ValueOf(&icons.ApartmentFill).Elem(),
"Api": reflect.ValueOf(&icons.Api).Elem(),
"ApiFill": reflect.ValueOf(&icons.ApiFill).Elem(),
"ApkDocument": reflect.ValueOf(&icons.ApkDocument).Elem(),
"ApkDocumentFill": reflect.ValueOf(&icons.ApkDocumentFill).Elem(),
"ApkInstall": reflect.ValueOf(&icons.ApkInstall).Elem(),
"ApkInstallFill": reflect.ValueOf(&icons.ApkInstallFill).Elem(),
"AppBadging": reflect.ValueOf(&icons.AppBadging).Elem(),
"AppBadgingFill": reflect.ValueOf(&icons.AppBadgingFill).Elem(),
"AppBlocking": reflect.ValueOf(&icons.AppBlocking).Elem(),
"AppBlockingFill": reflect.ValueOf(&icons.AppBlockingFill).Elem(),
"AppPromo": reflect.ValueOf(&icons.AppPromo).Elem(),
"AppPromoFill": reflect.ValueOf(&icons.AppPromoFill).Elem(),
"AppRegistration": reflect.ValueOf(&icons.AppRegistration).Elem(),
"AppRegistrationFill": reflect.ValueOf(&icons.AppRegistrationFill).Elem(),
"AppShortcut": reflect.ValueOf(&icons.AppShortcut).Elem(),
"AppShortcutFill": reflect.ValueOf(&icons.AppShortcutFill).Elem(),
"Approval": reflect.ValueOf(&icons.Approval).Elem(),
"ApprovalDelegation": reflect.ValueOf(&icons.ApprovalDelegation).Elem(),
"ApprovalDelegationFill": reflect.ValueOf(&icons.ApprovalDelegationFill).Elem(),
"ApprovalFill": reflect.ValueOf(&icons.ApprovalFill).Elem(),
"Apps": reflect.ValueOf(&icons.Apps).Elem(),
"AppsFill": reflect.ValueOf(&icons.AppsFill).Elem(),
"AppsOutage": reflect.ValueOf(&icons.AppsOutage).Elem(),
"AppsOutageFill": reflect.ValueOf(&icons.AppsOutageFill).Elem(),
"ArOnYou": reflect.ValueOf(&icons.ArOnYou).Elem(),
"ArOnYouFill": reflect.ValueOf(&icons.ArOnYouFill).Elem(),
"Architecture": reflect.ValueOf(&icons.Architecture).Elem(),
"ArchitectureFill": reflect.ValueOf(&icons.ArchitectureFill).Elem(),
"Archive": reflect.ValueOf(&icons.Archive).Elem(),
"ArchiveFill": reflect.ValueOf(&icons.ArchiveFill).Elem(),
"AreaChart": reflect.ValueOf(&icons.AreaChart).Elem(),
"AreaChartFill": reflect.ValueOf(&icons.AreaChartFill).Elem(),
"ArrowAndEdge": reflect.ValueOf(&icons.ArrowAndEdge).Elem(),
"ArrowAndEdgeFill": reflect.ValueOf(&icons.ArrowAndEdgeFill).Elem(),
"ArrowBack": reflect.ValueOf(&icons.ArrowBack).Elem(),
"ArrowBackFill": reflect.ValueOf(&icons.ArrowBackFill).Elem(),
"ArrowBackIos": reflect.ValueOf(&icons.ArrowBackIos).Elem(),
"ArrowBackIosFill": reflect.ValueOf(&icons.ArrowBackIosFill).Elem(),
"ArrowBackIosNew": reflect.ValueOf(&icons.ArrowBackIosNew).Elem(),
"ArrowBackIosNewFill": reflect.ValueOf(&icons.ArrowBackIosNewFill).Elem(),
"ArrowCircleDown": reflect.ValueOf(&icons.ArrowCircleDown).Elem(),
"ArrowCircleDownFill": reflect.ValueOf(&icons.ArrowCircleDownFill).Elem(),
"ArrowCircleLeft": reflect.ValueOf(&icons.ArrowCircleLeft).Elem(),
"ArrowCircleLeftFill": reflect.ValueOf(&icons.ArrowCircleLeftFill).Elem(),
"ArrowCircleRight": reflect.ValueOf(&icons.ArrowCircleRight).Elem(),
"ArrowCircleRightFill": reflect.ValueOf(&icons.ArrowCircleRightFill).Elem(),
"ArrowCircleUp": reflect.ValueOf(&icons.ArrowCircleUp).Elem(),
"ArrowCircleUpFill": reflect.ValueOf(&icons.ArrowCircleUpFill).Elem(),
"ArrowDownward": reflect.ValueOf(&icons.ArrowDownward).Elem(),
"ArrowDownwardAlt": reflect.ValueOf(&icons.ArrowDownwardAlt).Elem(),
"ArrowDownwardAltFill": reflect.ValueOf(&icons.ArrowDownwardAltFill).Elem(),
"ArrowDownwardFill": reflect.ValueOf(&icons.ArrowDownwardFill).Elem(),
"ArrowDropDown": reflect.ValueOf(&icons.ArrowDropDown).Elem(),
"ArrowDropDownCircle": reflect.ValueOf(&icons.ArrowDropDownCircle).Elem(),
"ArrowDropDownCircleFill": reflect.ValueOf(&icons.ArrowDropDownCircleFill).Elem(),
"ArrowDropDownFill": reflect.ValueOf(&icons.ArrowDropDownFill).Elem(),
"ArrowDropUp": reflect.ValueOf(&icons.ArrowDropUp).Elem(),
"ArrowDropUpFill": reflect.ValueOf(&icons.ArrowDropUpFill).Elem(),
"ArrowForward": reflect.ValueOf(&icons.ArrowForward).Elem(),
"ArrowForwardFill": reflect.ValueOf(&icons.ArrowForwardFill).Elem(),
"ArrowForwardIos": reflect.ValueOf(&icons.ArrowForwardIos).Elem(),
"ArrowForwardIosFill": reflect.ValueOf(&icons.ArrowForwardIosFill).Elem(),
"ArrowInsert": reflect.ValueOf(&icons.ArrowInsert).Elem(),
"ArrowInsertFill": reflect.ValueOf(&icons.ArrowInsertFill).Elem(),
"ArrowLeft": reflect.ValueOf(&icons.ArrowLeft).Elem(),
"ArrowLeftAlt": reflect.ValueOf(&icons.ArrowLeftAlt).Elem(),
"ArrowLeftAltFill": reflect.ValueOf(&icons.ArrowLeftAltFill).Elem(),
"ArrowLeftFill": reflect.ValueOf(&icons.ArrowLeftFill).Elem(),
"ArrowOrEdge": reflect.ValueOf(&icons.ArrowOrEdge).Elem(),
"ArrowOrEdgeFill": reflect.ValueOf(&icons.ArrowOrEdgeFill).Elem(),
"ArrowOutward": reflect.ValueOf(&icons.ArrowOutward).Elem(),
"ArrowOutwardFill": reflect.ValueOf(&icons.ArrowOutwardFill).Elem(),
"ArrowRange": reflect.ValueOf(&icons.ArrowRange).Elem(),
"ArrowRangeFill": reflect.ValueOf(&icons.ArrowRangeFill).Elem(),
"ArrowRight": reflect.ValueOf(&icons.ArrowRight).Elem(),
"ArrowRightAlt": reflect.ValueOf(&icons.ArrowRightAlt).Elem(),
"ArrowRightAltFill": reflect.ValueOf(&icons.ArrowRightAltFill).Elem(),
"ArrowRightFill": reflect.ValueOf(&icons.ArrowRightFill).Elem(),
"ArrowSelectorTool": reflect.ValueOf(&icons.ArrowSelectorTool).Elem(),
"ArrowSelectorToolFill": reflect.ValueOf(&icons.ArrowSelectorToolFill).Elem(),
"ArrowSplit": reflect.ValueOf(&icons.ArrowSplit).Elem(),
"ArrowSplitFill": reflect.ValueOf(&icons.ArrowSplitFill).Elem(),
"ArrowTopLeft": reflect.ValueOf(&icons.ArrowTopLeft).Elem(),
"ArrowTopLeftFill": reflect.ValueOf(&icons.ArrowTopLeftFill).Elem(),
"ArrowTopRight": reflect.ValueOf(&icons.ArrowTopRight).Elem(),
"ArrowTopRightFill": reflect.ValueOf(&icons.ArrowTopRightFill).Elem(),
"ArrowUpward": reflect.ValueOf(&icons.ArrowUpward).Elem(),
"ArrowUpwardAlt": reflect.ValueOf(&icons.ArrowUpwardAlt).Elem(),
"ArrowUpwardAltFill": reflect.ValueOf(&icons.ArrowUpwardAltFill).Elem(),
"ArrowUpwardFill": reflect.ValueOf(&icons.ArrowUpwardFill).Elem(),
"ArrowsMoreDown": reflect.ValueOf(&icons.ArrowsMoreDown).Elem(),
"ArrowsMoreDownFill": reflect.ValueOf(&icons.ArrowsMoreDownFill).Elem(),
"ArrowsMoreUp": reflect.ValueOf(&icons.ArrowsMoreUp).Elem(),
"ArrowsMoreUpFill": reflect.ValueOf(&icons.ArrowsMoreUpFill).Elem(),
"ArrowsOutward": reflect.ValueOf(&icons.ArrowsOutward).Elem(),
"ArrowsOutwardFill": reflect.ValueOf(&icons.ArrowsOutwardFill).Elem(),
"Article": reflect.ValueOf(&icons.Article).Elem(),
"ArticleFill": reflect.ValueOf(&icons.ArticleFill).Elem(),
"AspectRatio": reflect.ValueOf(&icons.AspectRatio).Elem(),
"AspectRatioFill": reflect.ValueOf(&icons.AspectRatioFill).Elem(),
"AssistantDirection": reflect.ValueOf(&icons.AssistantDirection).Elem(),
"AssistantDirectionFill": reflect.ValueOf(&icons.AssistantDirectionFill).Elem(),
"AssistantNavigation": reflect.ValueOf(&icons.AssistantNavigation).Elem(),
"AssistantNavigationFill": reflect.ValueOf(&icons.AssistantNavigationFill).Elem(),
"AttachEmail": reflect.ValueOf(&icons.AttachEmail).Elem(),
"AttachEmailFill": reflect.ValueOf(&icons.AttachEmailFill).Elem(),
"AttachFile": reflect.ValueOf(&icons.AttachFile).Elem(),
"AttachFileAdd": reflect.ValueOf(&icons.AttachFileAdd).Elem(),
"AttachFileAddFill": reflect.ValueOf(&icons.AttachFileAddFill).Elem(),
"AttachFileFill": reflect.ValueOf(&icons.AttachFileFill).Elem(),
"AttachMoney": reflect.ValueOf(&icons.AttachMoney).Elem(),
"AttachMoneyFill": reflect.ValueOf(&icons.AttachMoneyFill).Elem(),
"Attachment": reflect.ValueOf(&icons.Attachment).Elem(),
"AttachmentFill": reflect.ValueOf(&icons.AttachmentFill).Elem(),
"Attribution": reflect.ValueOf(&icons.Attribution).Elem(),
"AttributionFill": reflect.ValueOf(&icons.AttributionFill).Elem(),
"AudioFile": reflect.ValueOf(&icons.AudioFile).Elem(),
"AudioFileFill": reflect.ValueOf(&icons.AudioFileFill).Elem(),
"AudioVideoReceiver": reflect.ValueOf(&icons.AudioVideoReceiver).Elem(),
"AudioVideoReceiverFill": reflect.ValueOf(&icons.AudioVideoReceiverFill).Elem(),
"AutoDelete": reflect.ValueOf(&icons.AutoDelete).Elem(),
"AutoDeleteFill": reflect.ValueOf(&icons.AutoDeleteFill).Elem(),
"AutoReadPause": reflect.ValueOf(&icons.AutoReadPause).Elem(),
"AutoReadPauseFill": reflect.ValueOf(&icons.AutoReadPauseFill).Elem(),
"AutoReadPlay": reflect.ValueOf(&icons.AutoReadPlay).Elem(),
"AutoReadPlayFill": reflect.ValueOf(&icons.AutoReadPlayFill).Elem(),
"AutofpsSelect": reflect.ValueOf(&icons.AutofpsSelect).Elem(),
"AutofpsSelectFill": reflect.ValueOf(&icons.AutofpsSelectFill).Elem(),
"Autopause": reflect.ValueOf(&icons.Autopause).Elem(),
"AutopauseFill": reflect.ValueOf(&icons.AutopauseFill).Elem(),
"Autoplay": reflect.ValueOf(&icons.Autoplay).Elem(),
"AutoplayFill": reflect.ValueOf(&icons.AutoplayFill).Elem(),
"Autorenew": reflect.ValueOf(&icons.Autorenew).Elem(),
"AutorenewFill": reflect.ValueOf(&icons.AutorenewFill).Elem(),
"Autostop": reflect.ValueOf(&icons.Autostop).Elem(),
"AutostopFill": reflect.ValueOf(&icons.AutostopFill).Elem(),
"AvTimer": reflect.ValueOf(&icons.AvTimer).Elem(),
"AvTimerFill": reflect.ValueOf(&icons.AvTimerFill).Elem(),
"AwardStar": reflect.ValueOf(&icons.AwardStar).Elem(),
"AwardStarFill": reflect.ValueOf(&icons.AwardStarFill).Elem(),
"BackHand": reflect.ValueOf(&icons.BackHand).Elem(),
"BackHandFill": reflect.ValueOf(&icons.BackHandFill).Elem(),
"BackToTab": reflect.ValueOf(&icons.BackToTab).Elem(),
"BackToTabFill": reflect.ValueOf(&icons.BackToTabFill).Elem(),
"BackgroundDotLarge": reflect.ValueOf(&icons.BackgroundDotLarge).Elem(),
"BackgroundDotLargeFill": reflect.ValueOf(&icons.BackgroundDotLargeFill).Elem(),
"BackgroundGridSmall": reflect.ValueOf(&icons.BackgroundGridSmall).Elem(),
"BackgroundGridSmallFill": reflect.ValueOf(&icons.BackgroundGridSmallFill).Elem(),
"BackgroundReplace": reflect.ValueOf(&icons.BackgroundReplace).Elem(),
"BackgroundReplaceFill": reflect.ValueOf(&icons.BackgroundReplaceFill).Elem(),
"BacklightHigh": reflect.ValueOf(&icons.BacklightHigh).Elem(),
"BacklightHighFill": reflect.ValueOf(&icons.BacklightHighFill).Elem(),
"BacklightLow": reflect.ValueOf(&icons.BacklightLow).Elem(),
"BacklightLowFill": reflect.ValueOf(&icons.BacklightLowFill).Elem(),
"Backspace": reflect.ValueOf(&icons.Backspace).Elem(),
"BackspaceFill": reflect.ValueOf(&icons.BackspaceFill).Elem(),
"Backup": reflect.ValueOf(&icons.Backup).Elem(),
"BackupFill": reflect.ValueOf(&icons.BackupFill).Elem(),
"BackupTable": reflect.ValueOf(&icons.BackupTable).Elem(),
"BackupTableFill": reflect.ValueOf(&icons.BackupTableFill).Elem(),
"Badge": reflect.ValueOf(&icons.Badge).Elem(),
"BadgeCriticalBattery": reflect.ValueOf(&icons.BadgeCriticalBattery).Elem(),
"BadgeCriticalBatteryFill": reflect.ValueOf(&icons.BadgeCriticalBatteryFill).Elem(),
"BadgeFill": reflect.ValueOf(&icons.BadgeFill).Elem(),
"Balance": reflect.ValueOf(&icons.Balance).Elem(),
"BalanceFill": reflect.ValueOf(&icons.BalanceFill).Elem(),
"Ballot": reflect.ValueOf(&icons.Ballot).Elem(),
"BallotFill": reflect.ValueOf(&icons.BallotFill).Elem(),
"BarChart": reflect.ValueOf(&icons.BarChart).Elem(),
"BarChart4Bars": reflect.ValueOf(&icons.BarChart4Bars).Elem(),
"BarChart4BarsFill": reflect.ValueOf(&icons.BarChart4BarsFill).Elem(),
"BarChartFill": reflect.ValueOf(&icons.BarChartFill).Elem(),
"Barcode": reflect.ValueOf(&icons.Barcode).Elem(),
"BarcodeFill": reflect.ValueOf(&icons.BarcodeFill).Elem(),
"BarcodeReader": reflect.ValueOf(&icons.BarcodeReader).Elem(),
"BarcodeReaderFill": reflect.ValueOf(&icons.BarcodeReaderFill).Elem(),
"BarcodeScanner": reflect.ValueOf(&icons.BarcodeScanner).Elem(),
"BarcodeScannerFill": reflect.ValueOf(&icons.BarcodeScannerFill).Elem(),
"BatchPrediction": reflect.ValueOf(&icons.BatchPrediction).Elem(),
"BatchPredictionFill": reflect.ValueOf(&icons.BatchPredictionFill).Elem(),
"Battery0Bar": reflect.ValueOf(&icons.Battery0Bar).Elem(),
"Battery0BarFill": reflect.ValueOf(&icons.Battery0BarFill).Elem(),
"Battery1Bar": reflect.ValueOf(&icons.Battery1Bar).Elem(),
"Battery1BarFill": reflect.ValueOf(&icons.Battery1BarFill).Elem(),
"Battery2Bar": reflect.ValueOf(&icons.Battery2Bar).Elem(),
"Battery2BarFill": reflect.ValueOf(&icons.Battery2BarFill).Elem(),
"Battery3Bar": reflect.ValueOf(&icons.Battery3Bar).Elem(),
"Battery3BarFill": reflect.ValueOf(&icons.Battery3BarFill).Elem(),
"Battery4Bar": reflect.ValueOf(&icons.Battery4Bar).Elem(),
"Battery4BarFill": reflect.ValueOf(&icons.Battery4BarFill).Elem(),
"Battery5Bar": reflect.ValueOf(&icons.Battery5Bar).Elem(),
"Battery5BarFill": reflect.ValueOf(&icons.Battery5BarFill).Elem(),
"Battery6Bar": reflect.ValueOf(&icons.Battery6Bar).Elem(),
"Battery6BarFill": reflect.ValueOf(&icons.Battery6BarFill).Elem(),
"BatteryAlert": reflect.ValueOf(&icons.BatteryAlert).Elem(),
"BatteryAlertFill": reflect.ValueOf(&icons.BatteryAlertFill).Elem(),
"BatteryChange": reflect.ValueOf(&icons.BatteryChange).Elem(),
"BatteryChangeFill": reflect.ValueOf(&icons.BatteryChangeFill).Elem(),
"BatteryCharging20": reflect.ValueOf(&icons.BatteryCharging20).Elem(),
"BatteryCharging20Fill": reflect.ValueOf(&icons.BatteryCharging20Fill).Elem(),
"BatteryCharging30": reflect.ValueOf(&icons.BatteryCharging30).Elem(),
"BatteryCharging30Fill": reflect.ValueOf(&icons.BatteryCharging30Fill).Elem(),
"BatteryCharging50": reflect.ValueOf(&icons.BatteryCharging50).Elem(),
"BatteryCharging50Fill": reflect.ValueOf(&icons.BatteryCharging50Fill).Elem(),
"BatteryCharging60": reflect.ValueOf(&icons.BatteryCharging60).Elem(),
"BatteryCharging60Fill": reflect.ValueOf(&icons.BatteryCharging60Fill).Elem(),
"BatteryCharging80": reflect.ValueOf(&icons.BatteryCharging80).Elem(),
"BatteryCharging80Fill": reflect.ValueOf(&icons.BatteryCharging80Fill).Elem(),
"BatteryCharging90": reflect.ValueOf(&icons.BatteryCharging90).Elem(),
"BatteryCharging90Fill": reflect.ValueOf(&icons.BatteryCharging90Fill).Elem(),
"BatteryChargingFull": reflect.ValueOf(&icons.BatteryChargingFull).Elem(),
"BatteryChargingFullFill": reflect.ValueOf(&icons.BatteryChargingFullFill).Elem(),
"BatteryError": reflect.ValueOf(&icons.BatteryError).Elem(),
"BatteryErrorFill": reflect.ValueOf(&icons.BatteryErrorFill).Elem(),
"BatteryHoriz000": reflect.ValueOf(&icons.BatteryHoriz000).Elem(),
"BatteryHoriz000Fill": reflect.ValueOf(&icons.BatteryHoriz000Fill).Elem(),
"BatteryHoriz050": reflect.ValueOf(&icons.BatteryHoriz050).Elem(),
"BatteryHoriz050Fill": reflect.ValueOf(&icons.BatteryHoriz050Fill).Elem(),
"BatteryHoriz075": reflect.ValueOf(&icons.BatteryHoriz075).Elem(),
"BatteryHoriz075Fill": reflect.ValueOf(&icons.BatteryHoriz075Fill).Elem(),
"BatteryLow": reflect.ValueOf(&icons.BatteryLow).Elem(),
"BatteryLowFill": reflect.ValueOf(&icons.BatteryLowFill).Elem(),
"BatteryPlus": reflect.ValueOf(&icons.BatteryPlus).Elem(),
"BatteryPlusFill": reflect.ValueOf(&icons.BatteryPlusFill).Elem(),
"BatteryProfile": reflect.ValueOf(&icons.BatteryProfile).Elem(),
"BatteryProfileFill": reflect.ValueOf(&icons.BatteryProfileFill).Elem(),
"BatterySaver": reflect.ValueOf(&icons.BatterySaver).Elem(),
"BatterySaverFill": reflect.ValueOf(&icons.BatterySaverFill).Elem(),
"BatteryShare": reflect.ValueOf(&icons.BatteryShare).Elem(),
"BatteryShareFill": reflect.ValueOf(&icons.BatteryShareFill).Elem(),
"BatteryStatusGood": reflect.ValueOf(&icons.BatteryStatusGood).Elem(),
"BatteryStatusGoodFill": reflect.ValueOf(&icons.BatteryStatusGoodFill).Elem(),
"BatteryUnknown": reflect.ValueOf(&icons.BatteryUnknown).Elem(),
"BatteryUnknownFill": reflect.ValueOf(&icons.BatteryUnknownFill).Elem(),
"BatteryVeryLow": reflect.ValueOf(&icons.BatteryVeryLow).Elem(),
"BatteryVeryLowFill": reflect.ValueOf(&icons.BatteryVeryLowFill).Elem(),
"Bed": reflect.ValueOf(&icons.Bed).Elem(),
"BedFill": reflect.ValueOf(&icons.BedFill).Elem(),
"Bedtime": reflect.ValueOf(&icons.Bedtime).Elem(),
"BedtimeFill": reflect.ValueOf(&icons.BedtimeFill).Elem(),
"BedtimeOff": reflect.ValueOf(&icons.BedtimeOff).Elem(),
"BedtimeOffFill": reflect.ValueOf(&icons.BedtimeOffFill).Elem(),
"Blank": reflect.ValueOf(&icons.Blank).Elem(),
"Blanket": reflect.ValueOf(&icons.Blanket).Elem(),
"BlanketFill": reflect.ValueOf(&icons.BlanketFill).Elem(),
"Blender": reflect.ValueOf(&icons.Blender).Elem(),
"BlenderFill": reflect.ValueOf(&icons.BlenderFill).Elem(),
"Blind": reflect.ValueOf(&icons.Blind).Elem(),
"BlindFill": reflect.ValueOf(&icons.BlindFill).Elem(),
"Blinds": reflect.ValueOf(&icons.Blinds).Elem(),
"BlindsClosed": reflect.ValueOf(&icons.BlindsClosed).Elem(),
"BlindsClosedFill": reflect.ValueOf(&icons.BlindsClosedFill).Elem(),
"BlindsFill": reflect.ValueOf(&icons.BlindsFill).Elem(),
"Block": reflect.ValueOf(&icons.Block).Elem(),
"BlockFill": reflect.ValueOf(&icons.BlockFill).Elem(),
"Bluetooth": reflect.ValueOf(&icons.Bluetooth).Elem(),
"BluetoothConnected": reflect.ValueOf(&icons.BluetoothConnected).Elem(),
"BluetoothConnectedFill": reflect.ValueOf(&icons.BluetoothConnectedFill).Elem(),
"BluetoothDisabled": reflect.ValueOf(&icons.BluetoothDisabled).Elem(),
"BluetoothDisabledFill": reflect.ValueOf(&icons.BluetoothDisabledFill).Elem(),
"BluetoothDrive": reflect.ValueOf(&icons.BluetoothDrive).Elem(),
"BluetoothDriveFill": reflect.ValueOf(&icons.BluetoothDriveFill).Elem(),
"BluetoothFill": reflect.ValueOf(&icons.BluetoothFill).Elem(),
"BluetoothSearching": reflect.ValueOf(&icons.BluetoothSearching).Elem(),
"BluetoothSearchingFill": reflect.ValueOf(&icons.BluetoothSearchingFill).Elem(),
"BlurCircular": reflect.ValueOf(&icons.BlurCircular).Elem(),
"BlurCircularFill": reflect.ValueOf(&icons.BlurCircularFill).Elem(),
"BlurLinear": reflect.ValueOf(&icons.BlurLinear).Elem(),
"BlurLinearFill": reflect.ValueOf(&icons.BlurLinearFill).Elem(),
"BlurMedium": reflect.ValueOf(&icons.BlurMedium).Elem(),
"BlurMediumFill": reflect.ValueOf(&icons.BlurMediumFill).Elem(),
"BlurOff": reflect.ValueOf(&icons.BlurOff).Elem(),
"BlurOffFill": reflect.ValueOf(&icons.BlurOffFill).Elem(),
"BlurOn": reflect.ValueOf(&icons.BlurOn).Elem(),
"BlurOnFill": reflect.ValueOf(&icons.BlurOnFill).Elem(),
"BlurShort": reflect.ValueOf(&icons.BlurShort).Elem(),
"BlurShortFill": reflect.ValueOf(&icons.BlurShortFill).Elem(),
"Bolt": reflect.ValueOf(&icons.Bolt).Elem(),
"BoltFill": reflect.ValueOf(&icons.BoltFill).Elem(),
"Book": reflect.ValueOf(&icons.Book).Elem(),
"BookFill": reflect.ValueOf(&icons.BookFill).Elem(),
"BookOnline": reflect.ValueOf(&icons.BookOnline).Elem(),
"BookOnlineFill": reflect.ValueOf(&icons.BookOnlineFill).Elem(),
"Bookmark": reflect.ValueOf(&icons.Bookmark).Elem(),
"BookmarkAdd": reflect.ValueOf(&icons.BookmarkAdd).Elem(),
"BookmarkAddFill": reflect.ValueOf(&icons.BookmarkAddFill).Elem(),
"BookmarkAdded": reflect.ValueOf(&icons.BookmarkAdded).Elem(),
"BookmarkAddedFill": reflect.ValueOf(&icons.BookmarkAddedFill).Elem(),
"BookmarkFill": reflect.ValueOf(&icons.BookmarkFill).Elem(),
"BookmarkManager": reflect.ValueOf(&icons.BookmarkManager).Elem(),
"BookmarkManagerFill": reflect.ValueOf(&icons.BookmarkManagerFill).Elem(),
"BookmarkRemove": reflect.ValueOf(&icons.BookmarkRemove).Elem(),
"BookmarkRemoveFill": reflect.ValueOf(&icons.BookmarkRemoveFill).Elem(),
"Bookmarks": reflect.ValueOf(&icons.Bookmarks).Elem(),
"BookmarksFill": reflect.ValueOf(&icons.BookmarksFill).Elem(),
"BorderAll": reflect.ValueOf(&icons.BorderAll).Elem(),
"BorderAllFill": reflect.ValueOf(&icons.BorderAllFill).Elem(),
"BorderBottom": reflect.ValueOf(&icons.BorderBottom).Elem(),
"BorderBottomFill": reflect.ValueOf(&icons.BorderBottomFill).Elem(),
"BorderClear": reflect.ValueOf(&icons.BorderClear).Elem(),
"BorderClearFill": reflect.ValueOf(&icons.BorderClearFill).Elem(),
"BorderColor": reflect.ValueOf(&icons.BorderColor).Elem(),
"BorderColorFill": reflect.ValueOf(&icons.BorderColorFill).Elem(),
"BorderHorizontal": reflect.ValueOf(&icons.BorderHorizontal).Elem(),
"BorderHorizontalFill": reflect.ValueOf(&icons.BorderHorizontalFill).Elem(),
"BorderInner": reflect.ValueOf(&icons.BorderInner).Elem(),
"BorderInnerFill": reflect.ValueOf(&icons.BorderInnerFill).Elem(),
"BorderLeft": reflect.ValueOf(&icons.BorderLeft).Elem(),
"BorderLeftFill": reflect.ValueOf(&icons.BorderLeftFill).Elem(),
"BorderOuter": reflect.ValueOf(&icons.BorderOuter).Elem(),
"BorderOuterFill": reflect.ValueOf(&icons.BorderOuterFill).Elem(),
"BorderRight": reflect.ValueOf(&icons.BorderRight).Elem(),
"BorderRightFill": reflect.ValueOf(&icons.BorderRightFill).Elem(),
"BorderStyle": reflect.ValueOf(&icons.BorderStyle).Elem(),
"BorderStyleFill": reflect.ValueOf(&icons.BorderStyleFill).Elem(),
"BorderTop": reflect.ValueOf(&icons.BorderTop).Elem(),
"BorderTopFill": reflect.ValueOf(&icons.BorderTopFill).Elem(),
"BorderVertical": reflect.ValueOf(&icons.BorderVertical).Elem(),
"BorderVerticalFill": reflect.ValueOf(&icons.BorderVerticalFill).Elem(),
"BottomAppBar": reflect.ValueOf(&icons.BottomAppBar).Elem(),
"BottomAppBarFill": reflect.ValueOf(&icons.BottomAppBarFill).Elem(),
"BottomDrawer": reflect.ValueOf(&icons.BottomDrawer).Elem(),
"BottomDrawerFill": reflect.ValueOf(&icons.BottomDrawerFill).Elem(),
"BottomNavigation": reflect.ValueOf(&icons.BottomNavigation).Elem(),
"BottomNavigationFill": reflect.ValueOf(&icons.BottomNavigationFill).Elem(),
"BottomPanelClose": reflect.ValueOf(&icons.BottomPanelClose).Elem(),
"BottomPanelCloseFill": reflect.ValueOf(&icons.BottomPanelCloseFill).Elem(),
"BottomPanelOpen": reflect.ValueOf(&icons.BottomPanelOpen).Elem(),
"BottomPanelOpenFill": reflect.ValueOf(&icons.BottomPanelOpenFill).Elem(),
"BottomRightClick": reflect.ValueOf(&icons.BottomRightClick).Elem(),
"BottomRightClickFill": reflect.ValueOf(&icons.BottomRightClickFill).Elem(),
"BottomSheets": reflect.ValueOf(&icons.BottomSheets).Elem(),
"BottomSheetsFill": reflect.ValueOf(&icons.BottomSheetsFill).Elem(),
"Box": reflect.ValueOf(&icons.Box).Elem(),
"BoxAdd": reflect.ValueOf(&icons.BoxAdd).Elem(),
"BoxAddFill": reflect.ValueOf(&icons.BoxAddFill).Elem(),
"BoxEdit": reflect.ValueOf(&icons.BoxEdit).Elem(),
"BoxEditFill": reflect.ValueOf(&icons.BoxEditFill).Elem(),
"BoxFill": reflect.ValueOf(&icons.BoxFill).Elem(),
"Boy": reflect.ValueOf(&icons.Boy).Elem(),
"BoyFill": reflect.ValueOf(&icons.BoyFill).Elem(),
"Brightness1": reflect.ValueOf(&icons.Brightness1).Elem(),
"Brightness1Fill": reflect.ValueOf(&icons.Brightness1Fill).Elem(),
"Brightness2": reflect.ValueOf(&icons.Brightness2).Elem(),
"Brightness2Fill": reflect.ValueOf(&icons.Brightness2Fill).Elem(),
"Brightness3": reflect.ValueOf(&icons.Brightness3).Elem(),
"Brightness3Fill": reflect.ValueOf(&icons.Brightness3Fill).Elem(),
"Brightness4": reflect.ValueOf(&icons.Brightness4).Elem(),
"Brightness4Fill": reflect.ValueOf(&icons.Brightness4Fill).Elem(),
"Brightness5": reflect.ValueOf(&icons.Brightness5).Elem(),
"Brightness5Fill": reflect.ValueOf(&icons.Brightness5Fill).Elem(),
"Brightness6": reflect.ValueOf(&icons.Brightness6).Elem(),
"Brightness6Fill": reflect.ValueOf(&icons.Brightness6Fill).Elem(),
"Brightness7": reflect.ValueOf(&icons.Brightness7).Elem(),
"Brightness7Fill": reflect.ValueOf(&icons.Brightness7Fill).Elem(),
"BrightnessAlert": reflect.ValueOf(&icons.BrightnessAlert).Elem(),
"BrightnessAlertFill": reflect.ValueOf(&icons.BrightnessAlertFill).Elem(),
"BrightnessAuto": reflect.ValueOf(&icons.BrightnessAuto).Elem(),
"BrightnessAutoFill": reflect.ValueOf(&icons.BrightnessAutoFill).Elem(),
"BrightnessEmpty": reflect.ValueOf(&icons.BrightnessEmpty).Elem(),
"BrightnessEmptyFill": reflect.ValueOf(&icons.BrightnessEmptyFill).Elem(),
"BrightnessHigh": reflect.ValueOf(&icons.BrightnessHigh).Elem(),
"BrightnessHighFill": reflect.ValueOf(&icons.BrightnessHighFill).Elem(),
"BrightnessLow": reflect.ValueOf(&icons.BrightnessLow).Elem(),
"BrightnessLowFill": reflect.ValueOf(&icons.BrightnessLowFill).Elem(),
"BrightnessMedium": reflect.ValueOf(&icons.BrightnessMedium).Elem(),
"BrightnessMediumFill": reflect.ValueOf(&icons.BrightnessMediumFill).Elem(),
"BroadcastOnHome": reflect.ValueOf(&icons.BroadcastOnHome).Elem(),
"BroadcastOnHomeFill": reflect.ValueOf(&icons.BroadcastOnHomeFill).Elem(),
"BroadcastOnPersonal": reflect.ValueOf(&icons.BroadcastOnPersonal).Elem(),
"BroadcastOnPersonalFill": reflect.ValueOf(&icons.BroadcastOnPersonalFill).Elem(),
"BrokenImage": reflect.ValueOf(&icons.BrokenImage).Elem(),
"BrokenImageFill": reflect.ValueOf(&icons.BrokenImageFill).Elem(),
"Browse": reflect.ValueOf(&icons.Browse).Elem(),
"BrowseActivity": reflect.ValueOf(&icons.BrowseActivity).Elem(),
"BrowseActivityFill": reflect.ValueOf(&icons.BrowseActivityFill).Elem(),
"BrowseFill": reflect.ValueOf(&icons.BrowseFill).Elem(),
"BrowseGallery": reflect.ValueOf(&icons.BrowseGallery).Elem(),
"BrowseGalleryFill": reflect.ValueOf(&icons.BrowseGalleryFill).Elem(),
"BrowserUpdated": reflect.ValueOf(&icons.BrowserUpdated).Elem(),
"BrowserUpdatedFill": reflect.ValueOf(&icons.BrowserUpdatedFill).Elem(),
"Brush": reflect.ValueOf(&icons.Brush).Elem(),
"BrushFill": reflect.ValueOf(&icons.BrushFill).Elem(),
"Bubble": reflect.ValueOf(&icons.Bubble).Elem(),
"BubbleChart": reflect.ValueOf(&icons.BubbleChart).Elem(),
"BubbleChartFill": reflect.ValueOf(&icons.BubbleChartFill).Elem(),
"BubbleFill": reflect.ValueOf(&icons.BubbleFill).Elem(),
"Bubbles": reflect.ValueOf(&icons.Bubbles).Elem(),
"BubblesFill": reflect.ValueOf(&icons.BubblesFill).Elem(),
"BugReport": reflect.ValueOf(&icons.BugReport).Elem(),
"BugReportFill": reflect.ValueOf(&icons.BugReportFill).Elem(),
"Build": reflect.ValueOf(&icons.Build).Elem(),
"BuildCircle": reflect.ValueOf(&icons.BuildCircle).Elem(),
"BuildCircleFill": reflect.ValueOf(&icons.BuildCircleFill).Elem(),
"BuildFill": reflect.ValueOf(&icons.BuildFill).Elem(),
"BurstMode": reflect.ValueOf(&icons.BurstMode).Elem(),
"BurstModeFill": reflect.ValueOf(&icons.BurstModeFill).Elem(),
"BusinessChip": reflect.ValueOf(&icons.BusinessChip).Elem(),
"BusinessChipFill": reflect.ValueOf(&icons.BusinessChipFill).Elem(),
"BusinessMessages": reflect.ValueOf(&icons.BusinessMessages).Elem(),
"BusinessMessagesFill": reflect.ValueOf(&icons.BusinessMessagesFill).Elem(),
"ButtonsAlt": reflect.ValueOf(&icons.ButtonsAlt).Elem(),
"ButtonsAltFill": reflect.ValueOf(&icons.ButtonsAltFill).Elem(),
"Cable": reflect.ValueOf(&icons.Cable).Elem(),
"CableFill": reflect.ValueOf(&icons.CableFill).Elem(),
"Cached": reflect.ValueOf(&icons.Cached).Elem(),
"CachedFill": reflect.ValueOf(&icons.CachedFill).Elem(),
"Cake": reflect.ValueOf(&icons.Cake).Elem(),
"CakeAdd": reflect.ValueOf(&icons.CakeAdd).Elem(),
"CakeAddFill": reflect.ValueOf(&icons.CakeAddFill).Elem(),
"CakeFill": reflect.ValueOf(&icons.CakeFill).Elem(),
"Calculate": reflect.ValueOf(&icons.Calculate).Elem(),
"CalculateFill": reflect.ValueOf(&icons.CalculateFill).Elem(),
"CalendarAddOn": reflect.ValueOf(&icons.CalendarAddOn).Elem(),
"CalendarAddOnFill": reflect.ValueOf(&icons.CalendarAddOnFill).Elem(),
"CalendarAppsScript": reflect.ValueOf(&icons.CalendarAppsScript).Elem(),
"CalendarAppsScriptFill": reflect.ValueOf(&icons.CalendarAppsScriptFill).Elem(),
"CalendarMonth": reflect.ValueOf(&icons.CalendarMonth).Elem(),
"CalendarMonthFill": reflect.ValueOf(&icons.CalendarMonthFill).Elem(),
"CalendarToday": reflect.ValueOf(&icons.CalendarToday).Elem(),
"CalendarTodayFill": reflect.ValueOf(&icons.CalendarTodayFill).Elem(),
"CalendarViewDay": reflect.ValueOf(&icons.CalendarViewDay).Elem(),
"CalendarViewDayFill": reflect.ValueOf(&icons.CalendarViewDayFill).Elem(),
"CalendarViewMonth": reflect.ValueOf(&icons.CalendarViewMonth).Elem(),
"CalendarViewMonthFill": reflect.ValueOf(&icons.CalendarViewMonthFill).Elem(),
"CalendarViewWeek": reflect.ValueOf(&icons.CalendarViewWeek).Elem(),
"CalendarViewWeekFill": reflect.ValueOf(&icons.CalendarViewWeekFill).Elem(),
"Camera": reflect.ValueOf(&icons.Camera).Elem(),
"CameraFill": reflect.ValueOf(&icons.CameraFill).Elem(),
"CameraFront": reflect.ValueOf(&icons.CameraFront).Elem(),
"CameraFrontFill": reflect.ValueOf(&icons.CameraFrontFill).Elem(),
"CameraIndoor": reflect.ValueOf(&icons.CameraIndoor).Elem(),
"CameraIndoorFill": reflect.ValueOf(&icons.CameraIndoorFill).Elem(),
"CameraOutdoor": reflect.ValueOf(&icons.CameraOutdoor).Elem(),
"CameraOutdoorFill": reflect.ValueOf(&icons.CameraOutdoorFill).Elem(),
"CameraRear": reflect.ValueOf(&icons.CameraRear).Elem(),
"CameraRearFill": reflect.ValueOf(&icons.CameraRearFill).Elem(),
"CameraRoll": reflect.ValueOf(&icons.CameraRoll).Elem(),
"CameraRollFill": reflect.ValueOf(&icons.CameraRollFill).Elem(),
"CameraVideo": reflect.ValueOf(&icons.CameraVideo).Elem(),
"CameraVideoFill": reflect.ValueOf(&icons.CameraVideoFill).Elem(),
"Cameraswitch": reflect.ValueOf(&icons.Cameraswitch).Elem(),
"CameraswitchFill": reflect.ValueOf(&icons.CameraswitchFill).Elem(),
"Cancel": reflect.ValueOf(&icons.Cancel).Elem(),
"CancelFill": reflect.ValueOf(&icons.CancelFill).Elem(),
"CancelPresentation": reflect.ValueOf(&icons.CancelPresentation).Elem(),
"CancelPresentationFill": reflect.ValueOf(&icons.CancelPresentationFill).Elem(),
"CancelScheduleSend": reflect.ValueOf(&icons.CancelScheduleSend).Elem(),
"CancelScheduleSendFill": reflect.ValueOf(&icons.CancelScheduleSendFill).Elem(),
"CandlestickChart": reflect.ValueOf(&icons.CandlestickChart).Elem(),
"CandlestickChartFill": reflect.ValueOf(&icons.CandlestickChartFill).Elem(),
"Capture": reflect.ValueOf(&icons.Capture).Elem(),
"CaptureFill": reflect.ValueOf(&icons.CaptureFill).Elem(),
"CardMembership": reflect.ValueOf(&icons.CardMembership).Elem(),
"CardMembershipFill": reflect.ValueOf(&icons.CardMembershipFill).Elem(),
"Cards": reflect.ValueOf(&icons.Cards).Elem(),
"CardsFill": reflect.ValueOf(&icons.CardsFill).Elem(),
"Cast": reflect.ValueOf(&icons.Cast).Elem(),
"CastConnected": reflect.ValueOf(&icons.CastConnected).Elem(),
"CastConnectedFill": reflect.ValueOf(&icons.CastConnectedFill).Elem(),
"CastFill": reflect.ValueOf(&icons.CastFill).Elem(),
"CastPause": reflect.ValueOf(&icons.CastPause).Elem(),
"CastPauseFill": reflect.ValueOf(&icons.CastPauseFill).Elem(),
"CastWarning": reflect.ValueOf(&icons.CastWarning).Elem(),
"CastWarningFill": reflect.ValueOf(&icons.CastWarningFill).Elem(),
"Category": reflect.ValueOf(&icons.Category).Elem(),
"CategoryFill": reflect.ValueOf(&icons.CategoryFill).Elem(),
"Celebration": reflect.ValueOf(&icons.Celebration).Elem(),
"CelebrationFill": reflect.ValueOf(&icons.CelebrationFill).Elem(),
"CellMerge": reflect.ValueOf(&icons.CellMerge).Elem(),
"CellMergeFill": reflect.ValueOf(&icons.CellMergeFill).Elem(),
"CenterFocusStrong": reflect.ValueOf(&icons.CenterFocusStrong).Elem(),
"CenterFocusStrongFill": reflect.ValueOf(&icons.CenterFocusStrongFill).Elem(),
"CenterFocusWeak": reflect.ValueOf(&icons.CenterFocusWeak).Elem(),
"CenterFocusWeakFill": reflect.ValueOf(&icons.CenterFocusWeakFill).Elem(),
"Chair": reflect.ValueOf(&icons.Chair).Elem(),
"ChairFill": reflect.ValueOf(&icons.ChairFill).Elem(),
"ChangeCircle": reflect.ValueOf(&icons.ChangeCircle).Elem(),
"ChangeCircleFill": reflect.ValueOf(&icons.ChangeCircleFill).Elem(),
"ChangeHistory": reflect.ValueOf(&icons.ChangeHistory).Elem(),
"ChangeHistoryFill": reflect.ValueOf(&icons.ChangeHistoryFill).Elem(),
"Charger": reflect.ValueOf(&icons.Charger).Elem(),
"ChargerFill": reflect.ValueOf(&icons.ChargerFill).Elem(),
"ChartData": reflect.ValueOf(&icons.ChartData).Elem(),
"ChartDataFill": reflect.ValueOf(&icons.ChartDataFill).Elem(),
"Chat": reflect.ValueOf(&icons.Chat).Elem(),
"ChatAddOn": reflect.ValueOf(&icons.ChatAddOn).Elem(),
"ChatAddOnFill": reflect.ValueOf(&icons.ChatAddOnFill).Elem(),
"ChatAppsScript": reflect.ValueOf(&icons.ChatAppsScript).Elem(),
"ChatAppsScriptFill": reflect.ValueOf(&icons.ChatAppsScriptFill).Elem(),
"ChatBubble": reflect.ValueOf(&icons.ChatBubble).Elem(),
"ChatBubbleFill": reflect.ValueOf(&icons.ChatBubbleFill).Elem(),
"ChatError": reflect.ValueOf(&icons.ChatError).Elem(),
"ChatErrorFill": reflect.ValueOf(&icons.ChatErrorFill).Elem(),
"ChatFill": reflect.ValueOf(&icons.ChatFill).Elem(),
"ChatPasteGo": reflect.ValueOf(&icons.ChatPasteGo).Elem(),
"ChatPasteGoFill": reflect.ValueOf(&icons.ChatPasteGoFill).Elem(),
"Check": reflect.ValueOf(&icons.Check).Elem(),
"CheckBox": reflect.ValueOf(&icons.CheckBox).Elem(),
"CheckBoxFill": reflect.ValueOf(&icons.CheckBoxFill).Elem(),
"CheckBoxOutlineBlank": reflect.ValueOf(&icons.CheckBoxOutlineBlank).Elem(),
"CheckBoxOutlineBlankFill": reflect.ValueOf(&icons.CheckBoxOutlineBlankFill).Elem(),
"CheckCircle": reflect.ValueOf(&icons.CheckCircle).Elem(),
"CheckCircleFill": reflect.ValueOf(&icons.CheckCircleFill).Elem(),
"CheckFill": reflect.ValueOf(&icons.CheckFill).Elem(),
"CheckInOut": reflect.ValueOf(&icons.CheckInOut).Elem(),
"CheckInOutFill": reflect.ValueOf(&icons.CheckInOutFill).Elem(),
"CheckIndeterminateSmall": reflect.ValueOf(&icons.CheckIndeterminateSmall).Elem(),
"CheckIndeterminateSmallFill": reflect.ValueOf(&icons.CheckIndeterminateSmallFill).Elem(),
"CheckSmall": reflect.ValueOf(&icons.CheckSmall).Elem(),
"CheckSmallFill": reflect.ValueOf(&icons.CheckSmallFill).Elem(),
"Checklist": reflect.ValueOf(&icons.Checklist).Elem(),
"ChecklistFill": reflect.ValueOf(&icons.ChecklistFill).Elem(),
"ChecklistRtl": reflect.ValueOf(&icons.ChecklistRtl).Elem(),
"ChecklistRtlFill": reflect.ValueOf(&icons.ChecklistRtlFill).Elem(),
"Cheer": reflect.ValueOf(&icons.Cheer).Elem(),
"CheerFill": reflect.ValueOf(&icons.CheerFill).Elem(),
"Chess": reflect.ValueOf(&icons.Chess).Elem(),
"ChessFill": reflect.ValueOf(&icons.ChessFill).Elem(),
"ChevronLeft": reflect.ValueOf(&icons.ChevronLeft).Elem(),
"ChevronLeftFill": reflect.ValueOf(&icons.ChevronLeftFill).Elem(),
"ChevronRight": reflect.ValueOf(&icons.ChevronRight).Elem(),
"ChevronRightFill": reflect.ValueOf(&icons.ChevronRightFill).Elem(),
"Chips": reflect.ValueOf(&icons.Chips).Elem(),
"ChipsFill": reflect.ValueOf(&icons.ChipsFill).Elem(),
"Chronic": reflect.ValueOf(&icons.Chronic).Elem(),
"ChronicFill": reflect.ValueOf(&icons.ChronicFill).Elem(),
"Circle": reflect.ValueOf(&icons.Circle).Elem(),
"CircleFill": reflect.ValueOf(&icons.CircleFill).Elem(),
"CircleNotifications": reflect.ValueOf(&icons.CircleNotifications).Elem(),
"CircleNotificationsFill": reflect.ValueOf(&icons.CircleNotificationsFill).Elem(),
"Circles": reflect.ValueOf(&icons.Circles).Elem(),
"CirclesExt": reflect.ValueOf(&icons.CirclesExt).Elem(),
"CirclesExtFill": reflect.ValueOf(&icons.CirclesExtFill).Elem(),
"CirclesFill": reflect.ValueOf(&icons.CirclesFill).Elem(),
"Clarify": reflect.ValueOf(&icons.Clarify).Elem(),
"ClarifyFill": reflect.ValueOf(&icons.ClarifyFill).Elem(),
"ClearAll": reflect.ValueOf(&icons.ClearAll).Elem(),
"ClearAllFill": reflect.ValueOf(&icons.ClearAllFill).Elem(),
"ClearDay": reflect.ValueOf(&icons.ClearDay).Elem(),
"ClearDayFill": reflect.ValueOf(&icons.ClearDayFill).Elem(),
"ClearNight": reflect.ValueOf(&icons.ClearNight).Elem(),
"ClearNightFill": reflect.ValueOf(&icons.ClearNightFill).Elem(),
"ClockLoader10": reflect.ValueOf(&icons.ClockLoader10).Elem(),
"ClockLoader10Fill": reflect.ValueOf(&icons.ClockLoader10Fill).Elem(),
"ClockLoader20": reflect.ValueOf(&icons.ClockLoader20).Elem(),
"ClockLoader20Fill": reflect.ValueOf(&icons.ClockLoader20Fill).Elem(),
"ClockLoader40": reflect.ValueOf(&icons.ClockLoader40).Elem(),
"ClockLoader40Fill": reflect.ValueOf(&icons.ClockLoader40Fill).Elem(),
"ClockLoader60": reflect.ValueOf(&icons.ClockLoader60).Elem(),
"ClockLoader60Fill": reflect.ValueOf(&icons.ClockLoader60Fill).Elem(),
"ClockLoader80": reflect.ValueOf(&icons.ClockLoader80).Elem(),
"ClockLoader80Fill": reflect.ValueOf(&icons.ClockLoader80Fill).Elem(),
"ClockLoader90": reflect.ValueOf(&icons.ClockLoader90).Elem(),
"ClockLoader90Fill": reflect.ValueOf(&icons.ClockLoader90Fill).Elem(),
"Close": reflect.ValueOf(&icons.Close).Elem(),
"CloseFill": reflect.ValueOf(&icons.CloseFill).Elem(),
"CloseFullscreen": reflect.ValueOf(&icons.CloseFullscreen).Elem(),
"CloseFullscreenFill": reflect.ValueOf(&icons.CloseFullscreenFill).Elem(),
"ClosedCaption": reflect.ValueOf(&icons.ClosedCaption).Elem(),
"ClosedCaptionDisabled": reflect.ValueOf(&icons.ClosedCaptionDisabled).Elem(),
"ClosedCaptionDisabledFill": reflect.ValueOf(&icons.ClosedCaptionDisabledFill).Elem(),
"ClosedCaptionFill": reflect.ValueOf(&icons.ClosedCaptionFill).Elem(),
"Cloud": reflect.ValueOf(&icons.Cloud).Elem(),
"CloudCircle": reflect.ValueOf(&icons.CloudCircle).Elem(),
"CloudCircleFill": reflect.ValueOf(&icons.CloudCircleFill).Elem(),
"CloudDone": reflect.ValueOf(&icons.CloudDone).Elem(),
"CloudDoneFill": reflect.ValueOf(&icons.CloudDoneFill).Elem(),
"CloudDownload": reflect.ValueOf(&icons.CloudDownload).Elem(),
"CloudDownloadFill": reflect.ValueOf(&icons.CloudDownloadFill).Elem(),
"CloudFill": reflect.ValueOf(&icons.CloudFill).Elem(),
"CloudOff": reflect.ValueOf(&icons.CloudOff).Elem(),
"CloudOffFill": reflect.ValueOf(&icons.CloudOffFill).Elem(),
"CloudSync": reflect.ValueOf(&icons.CloudSync).Elem(),
"CloudSyncFill": reflect.ValueOf(&icons.CloudSyncFill).Elem(),
"CloudUpload": reflect.ValueOf(&icons.CloudUpload).Elem(),
"CloudUploadFill": reflect.ValueOf(&icons.CloudUploadFill).Elem(),
"Code": reflect.ValueOf(&icons.Code).Elem(),
"CodeBlocks": reflect.ValueOf(&icons.CodeBlocks).Elem(),
"CodeBlocksFill": reflect.ValueOf(&icons.CodeBlocksFill).Elem(),
"CodeFill": reflect.ValueOf(&icons.CodeFill).Elem(),
"CodeOff": reflect.ValueOf(&icons.CodeOff).Elem(),
"CodeOffFill": reflect.ValueOf(&icons.CodeOffFill).Elem(),
"Coffee": reflect.ValueOf(&icons.Coffee).Elem(),
"CoffeeFill": reflect.ValueOf(&icons.CoffeeFill).Elem(),
"CogentCore": reflect.ValueOf(&icons.CogentCore).Elem(),
"Cognition": reflect.ValueOf(&icons.Cognition).Elem(),
"CognitionFill": reflect.ValueOf(&icons.CognitionFill).Elem(),
"CollapseAll": reflect.ValueOf(&icons.CollapseAll).Elem(),
"CollapseAllFill": reflect.ValueOf(&icons.CollapseAllFill).Elem(),
"CollectionsBookmark": reflect.ValueOf(&icons.CollectionsBookmark).Elem(),
"CollectionsBookmarkFill": reflect.ValueOf(&icons.CollectionsBookmarkFill).Elem(),
"Colorize": reflect.ValueOf(&icons.Colorize).Elem(),
"ColorizeFill": reflect.ValueOf(&icons.ColorizeFill).Elem(),
"Colors": reflect.ValueOf(&icons.Colors).Elem(),
"ColorsFill": reflect.ValueOf(&icons.ColorsFill).Elem(),
"ComicBubble": reflect.ValueOf(&icons.ComicBubble).Elem(),
"ComicBubbleFill": reflect.ValueOf(&icons.ComicBubbleFill).Elem(),
"Comment": reflect.ValueOf(&icons.Comment).Elem(),
"CommentBank": reflect.ValueOf(&icons.CommentBank).Elem(),
"CommentBankFill": reflect.ValueOf(&icons.CommentBankFill).Elem(),
"CommentFill": reflect.ValueOf(&icons.CommentFill).Elem(),
"CommentsDisabled": reflect.ValueOf(&icons.CommentsDisabled).Elem(),
"CommentsDisabledFill": reflect.ValueOf(&icons.CommentsDisabledFill).Elem(),
"Commit": reflect.ValueOf(&icons.Commit).Elem(),
"CommitFill": reflect.ValueOf(&icons.CommitFill).Elem(),
"Communication": reflect.ValueOf(&icons.Communication).Elem(),
"CommunicationFill": reflect.ValueOf(&icons.CommunicationFill).Elem(),
"Communities": reflect.ValueOf(&icons.Communities).Elem(),
"CommunitiesFill": reflect.ValueOf(&icons.CommunitiesFill).Elem(),
"Compare": reflect.ValueOf(&icons.Compare).Elem(),
"CompareArrows": reflect.ValueOf(&icons.CompareArrows).Elem(),
"CompareArrowsFill": reflect.ValueOf(&icons.CompareArrowsFill).Elem(),
"CompareFill": reflect.ValueOf(&icons.CompareFill).Elem(),
"ComponentExchange": reflect.ValueOf(&icons.ComponentExchange).Elem(),
"ComponentExchangeFill": reflect.ValueOf(&icons.ComponentExchangeFill).Elem(),
"Compress": reflect.ValueOf(&icons.Compress).Elem(),
"CompressFill": reflect.ValueOf(&icons.CompressFill).Elem(),
"Computer": reflect.ValueOf(&icons.Computer).Elem(),
"ComputerFill": reflect.ValueOf(&icons.ComputerFill).Elem(),
"ConfirmationNumber": reflect.ValueOf(&icons.ConfirmationNumber).Elem(),
"ConfirmationNumberFill": reflect.ValueOf(&icons.ConfirmationNumberFill).Elem(),
"ConnectWithoutContact": reflect.ValueOf(&icons.ConnectWithoutContact).Elem(),
"ConnectWithoutContactFill": reflect.ValueOf(&icons.ConnectWithoutContactFill).Elem(),
"ConnectedTv": reflect.ValueOf(&icons.ConnectedTv).Elem(),
"ConnectedTvFill": reflect.ValueOf(&icons.ConnectedTvFill).Elem(),
"ConnectingAirports": reflect.ValueOf(&icons.ConnectingAirports).Elem(),
"ConnectingAirportsFill": reflect.ValueOf(&icons.ConnectingAirportsFill).Elem(),
"Constant": reflect.ValueOf(&icons.Constant).Elem(),
"Construction": reflect.ValueOf(&icons.Construction).Elem(),
"ConstructionFill": reflect.ValueOf(&icons.ConstructionFill).Elem(),
"ContactEmergency": reflect.ValueOf(&icons.ContactEmergency).Elem(),
"ContactEmergencyFill": reflect.ValueOf(&icons.ContactEmergencyFill).Elem(),
"ContactMail": reflect.ValueOf(&icons.ContactMail).Elem(),
"ContactMailFill": reflect.ValueOf(&icons.ContactMailFill).Elem(),
"ContactPage": reflect.ValueOf(&icons.ContactPage).Elem(),
"ContactPageFill": reflect.ValueOf(&icons.ContactPageFill).Elem(),
"ContactPhone": reflect.ValueOf(&icons.ContactPhone).Elem(),
"ContactPhoneFill": reflect.ValueOf(&icons.ContactPhoneFill).Elem(),
"ContactSupport": reflect.ValueOf(&icons.ContactSupport).Elem(),
"ContactSupportFill": reflect.ValueOf(&icons.ContactSupportFill).Elem(),
"Contactless": reflect.ValueOf(&icons.Contactless).Elem(),
"ContactlessFill": reflect.ValueOf(&icons.ContactlessFill).Elem(),
"ContactlessOff": reflect.ValueOf(&icons.ContactlessOff).Elem(),
"ContactlessOffFill": reflect.ValueOf(&icons.ContactlessOffFill).Elem(),
"Contacts": reflect.ValueOf(&icons.Contacts).Elem(),
"ContactsFill": reflect.ValueOf(&icons.ContactsFill).Elem(),
"ContentCopy": reflect.ValueOf(&icons.ContentCopy).Elem(),
"ContentCopyFill": reflect.ValueOf(&icons.ContentCopyFill).Elem(),
"ContentCut": reflect.ValueOf(&icons.ContentCut).Elem(),
"ContentCutFill": reflect.ValueOf(&icons.ContentCutFill).Elem(),
"ContentPaste": reflect.ValueOf(&icons.ContentPaste).Elem(),
"ContentPasteFill": reflect.ValueOf(&icons.ContentPasteFill).Elem(),
"ContentPasteGo": reflect.ValueOf(&icons.ContentPasteGo).Elem(),
"ContentPasteGoFill": reflect.ValueOf(&icons.ContentPasteGoFill).Elem(),
"ContentPasteOff": reflect.ValueOf(&icons.ContentPasteOff).Elem(),
"ContentPasteOffFill": reflect.ValueOf(&icons.ContentPasteOffFill).Elem(),
"ContentPasteSearch": reflect.ValueOf(&icons.ContentPasteSearch).Elem(),
"ContentPasteSearchFill": reflect.ValueOf(&icons.ContentPasteSearchFill).Elem(),
"Contract": reflect.ValueOf(&icons.Contract).Elem(),
"ContractDelete": reflect.ValueOf(&icons.ContractDelete).Elem(),
"ContractDeleteFill": reflect.ValueOf(&icons.ContractDeleteFill).Elem(),
"ContractEdit": reflect.ValueOf(&icons.ContractEdit).Elem(),
"ContractEditFill": reflect.ValueOf(&icons.ContractEditFill).Elem(),
"ContractFill": reflect.ValueOf(&icons.ContractFill).Elem(),
"Contrast": reflect.ValueOf(&icons.Contrast).Elem(),
"ContrastFill": reflect.ValueOf(&icons.ContrastFill).Elem(),
"ContrastRtlOff": reflect.ValueOf(&icons.ContrastRtlOff).Elem(),
"ContrastRtlOffFill": reflect.ValueOf(&icons.ContrastRtlOffFill).Elem(),
"ControlCamera": reflect.ValueOf(&icons.ControlCamera).Elem(),
"ControlCameraFill": reflect.ValueOf(&icons.ControlCameraFill).Elem(),
"ControlPointDuplicate": reflect.ValueOf(&icons.ControlPointDuplicate).Elem(),
"ControlPointDuplicateFill": reflect.ValueOf(&icons.ControlPointDuplicateFill).Elem(),
"ControllerGen": reflect.ValueOf(&icons.ControllerGen).Elem(),
"ControllerGenFill": reflect.ValueOf(&icons.ControllerGenFill).Elem(),
"ConversionPath": reflect.ValueOf(&icons.ConversionPath).Elem(),
"ConversionPathFill": reflect.ValueOf(&icons.ConversionPathFill).Elem(),
"ConversionPathOff": reflect.ValueOf(&icons.ConversionPathOff).Elem(),
"ConversionPathOffFill": reflect.ValueOf(&icons.ConversionPathOffFill).Elem(),
"ConveyorBelt": reflect.ValueOf(&icons.ConveyorBelt).Elem(),
"ConveyorBeltFill": reflect.ValueOf(&icons.ConveyorBeltFill).Elem(),
"Cookie": reflect.ValueOf(&icons.Cookie).Elem(),
"CookieFill": reflect.ValueOf(&icons.CookieFill).Elem(),
"CookieOff": reflect.ValueOf(&icons.CookieOff).Elem(),
"CookieOffFill": reflect.ValueOf(&icons.CookieOffFill).Elem(),
"Copy": reflect.ValueOf(&icons.Copy).Elem(),
"CopyAll": reflect.ValueOf(&icons.CopyAll).Elem(),
"CopyAllFill": reflect.ValueOf(&icons.CopyAllFill).Elem(),
"Copyright": reflect.ValueOf(&icons.Copyright).Elem(),
"CopyrightFill": reflect.ValueOf(&icons.CopyrightFill).Elem(),
"Counter0": reflect.ValueOf(&icons.Counter0).Elem(),
"Counter0Fill": reflect.ValueOf(&icons.Counter0Fill).Elem(),
"Counter1": reflect.ValueOf(&icons.Counter1).Elem(),
"Counter1Fill": reflect.ValueOf(&icons.Counter1Fill).Elem(),
"Counter2": reflect.ValueOf(&icons.Counter2).Elem(),
"Counter2Fill": reflect.ValueOf(&icons.Counter2Fill).Elem(),
"Counter3": reflect.ValueOf(&icons.Counter3).Elem(),
"Counter3Fill": reflect.ValueOf(&icons.Counter3Fill).Elem(),
"Counter4": reflect.ValueOf(&icons.Counter4).Elem(),
"Counter4Fill": reflect.ValueOf(&icons.Counter4Fill).Elem(),
"Counter5": reflect.ValueOf(&icons.Counter5).Elem(),
"Counter5Fill": reflect.ValueOf(&icons.Counter5Fill).Elem(),
"Counter6": reflect.ValueOf(&icons.Counter6).Elem(),
"Counter6Fill": reflect.ValueOf(&icons.Counter6Fill).Elem(),
"Counter7": reflect.ValueOf(&icons.Counter7).Elem(),
"Counter7Fill": reflect.ValueOf(&icons.Counter7Fill).Elem(),
"Counter8": reflect.ValueOf(&icons.Counter8).Elem(),
"Counter8Fill": reflect.ValueOf(&icons.Counter8Fill).Elem(),
"Counter9": reflect.ValueOf(&icons.Counter9).Elem(),
"Counter9Fill": reflect.ValueOf(&icons.Counter9Fill).Elem(),
"CreateNewFolder": reflect.ValueOf(&icons.CreateNewFolder).Elem(),
"CreateNewFolderFill": reflect.ValueOf(&icons.CreateNewFolderFill).Elem(),
"CreditCard": reflect.ValueOf(&icons.CreditCard).Elem(),
"CreditCardFill": reflect.ValueOf(&icons.CreditCardFill).Elem(),
"CreditCardOff": reflect.ValueOf(&icons.CreditCardOff).Elem(),
"CreditCardOffFill": reflect.ValueOf(&icons.CreditCardOffFill).Elem(),
"CreditScore": reflect.ValueOf(&icons.CreditScore).Elem(),
"CreditScoreFill": reflect.ValueOf(&icons.CreditScoreFill).Elem(),
"CrisisAlert": reflect.ValueOf(&icons.CrisisAlert).Elem(),
"CrisisAlertFill": reflect.ValueOf(&icons.CrisisAlertFill).Elem(),
"Crop": reflect.ValueOf(&icons.Crop).Elem(),
"Crop169": reflect.ValueOf(&icons.Crop169).Elem(),
"Crop169Fill": reflect.ValueOf(&icons.Crop169Fill).Elem(),
"Crop32": reflect.ValueOf(&icons.Crop32).Elem(),
"Crop32Fill": reflect.ValueOf(&icons.Crop32Fill).Elem(),
"Crop54": reflect.ValueOf(&icons.Crop54).Elem(),
"Crop54Fill": reflect.ValueOf(&icons.Crop54Fill).Elem(),
"Crop75": reflect.ValueOf(&icons.Crop75).Elem(),
"Crop75Fill": reflect.ValueOf(&icons.Crop75Fill).Elem(),
"CropFill": reflect.ValueOf(&icons.CropFill).Elem(),
"CropFree": reflect.ValueOf(&icons.CropFree).Elem(),
"CropFreeFill": reflect.ValueOf(&icons.CropFreeFill).Elem(),
"CropLandscape": reflect.ValueOf(&icons.CropLandscape).Elem(),
"CropLandscapeFill": reflect.ValueOf(&icons.CropLandscapeFill).Elem(),
"CropPortrait": reflect.ValueOf(&icons.CropPortrait).Elem(),
"CropPortraitFill": reflect.ValueOf(&icons.CropPortraitFill).Elem(),
"CropRotate": reflect.ValueOf(&icons.CropRotate).Elem(),
"CropRotateFill": reflect.ValueOf(&icons.CropRotateFill).Elem(),
"CropSquare": reflect.ValueOf(&icons.CropSquare).Elem(),
"CropSquareFill": reflect.ValueOf(&icons.CropSquareFill).Elem(),
"Crowdsource": reflect.ValueOf(&icons.Crowdsource).Elem(),
"CrowdsourceFill": reflect.ValueOf(&icons.CrowdsourceFill).Elem(),
"Css": reflect.ValueOf(&icons.Css).Elem(),
"CssFill": reflect.ValueOf(&icons.CssFill).Elem(),
"Csv": reflect.ValueOf(&icons.Csv).Elem(),
"CsvFill": reflect.ValueOf(&icons.CsvFill).Elem(),
"CurrencyBitcoin": reflect.ValueOf(&icons.CurrencyBitcoin).Elem(),
"CurrencyBitcoinFill": reflect.ValueOf(&icons.CurrencyBitcoinFill).Elem(),
"CurrencyExchange": reflect.ValueOf(&icons.CurrencyExchange).Elem(),
"CurrencyExchangeFill": reflect.ValueOf(&icons.CurrencyExchangeFill).Elem(),
"CurrencyFranc": reflect.ValueOf(&icons.CurrencyFranc).Elem(),
"CurrencyFrancFill": reflect.ValueOf(&icons.CurrencyFrancFill).Elem(),
"CurrencyLira": reflect.ValueOf(&icons.CurrencyLira).Elem(),
"CurrencyLiraFill": reflect.ValueOf(&icons.CurrencyLiraFill).Elem(),
"CurrencyPound": reflect.ValueOf(&icons.CurrencyPound).Elem(),
"CurrencyPoundFill": reflect.ValueOf(&icons.CurrencyPoundFill).Elem(),
"CurrencyRuble": reflect.ValueOf(&icons.CurrencyRuble).Elem(),
"CurrencyRubleFill": reflect.ValueOf(&icons.CurrencyRubleFill).Elem(),
"CurrencyRupee": reflect.ValueOf(&icons.CurrencyRupee).Elem(),
"CurrencyRupeeFill": reflect.ValueOf(&icons.CurrencyRupeeFill).Elem(),
"CurrencyYen": reflect.ValueOf(&icons.CurrencyYen).Elem(),
"CurrencyYenFill": reflect.ValueOf(&icons.CurrencyYenFill).Elem(),
"CurrencyYuan": reflect.ValueOf(&icons.CurrencyYuan).Elem(),
"CurrencyYuanFill": reflect.ValueOf(&icons.CurrencyYuanFill).Elem(),
"CustomTypography": reflect.ValueOf(&icons.CustomTypography).Elem(),
"CustomTypographyFill": reflect.ValueOf(&icons.CustomTypographyFill).Elem(),
"Cut": reflect.ValueOf(&icons.Cut).Elem(),
"CutFill": reflect.ValueOf(&icons.CutFill).Elem(),
"Cycle": reflect.ValueOf(&icons.Cycle).Elem(),
"CycleFill": reflect.ValueOf(&icons.CycleFill).Elem(),
"Cyclone": reflect.ValueOf(&icons.Cyclone).Elem(),
"CycloneFill": reflect.ValueOf(&icons.CycloneFill).Elem(),
"Dangerous": reflect.ValueOf(&icons.Dangerous).Elem(),
"DangerousFill": reflect.ValueOf(&icons.DangerousFill).Elem(),
"DarkMode": reflect.ValueOf(&icons.DarkMode).Elem(),
"DarkModeFill": reflect.ValueOf(&icons.DarkModeFill).Elem(),
"Dashboard": reflect.ValueOf(&icons.Dashboard).Elem(),
"DashboardCustomize": reflect.ValueOf(&icons.DashboardCustomize).Elem(),
"DashboardCustomizeFill": reflect.ValueOf(&icons.DashboardCustomizeFill).Elem(),
"DashboardFill": reflect.ValueOf(&icons.DashboardFill).Elem(),
"DataAlert": reflect.ValueOf(&icons.DataAlert).Elem(),
"DataAlertFill": reflect.ValueOf(&icons.DataAlertFill).Elem(),
"DataArray": reflect.ValueOf(&icons.DataArray).Elem(),
"DataArrayFill": reflect.ValueOf(&icons.DataArrayFill).Elem(),
"DataCheck": reflect.ValueOf(&icons.DataCheck).Elem(),
"DataCheckFill": reflect.ValueOf(&icons.DataCheckFill).Elem(),
"DataExploration": reflect.ValueOf(&icons.DataExploration).Elem(),
"DataExplorationFill": reflect.ValueOf(&icons.DataExplorationFill).Elem(),
"DataInfoAlert": reflect.ValueOf(&icons.DataInfoAlert).Elem(),
"DataInfoAlertFill": reflect.ValueOf(&icons.DataInfoAlertFill).Elem(),
"DataLossPrevention": reflect.ValueOf(&icons.DataLossPrevention).Elem(),
"DataLossPreventionFill": reflect.ValueOf(&icons.DataLossPreventionFill).Elem(),
"DataObject": reflect.ValueOf(&icons.DataObject).Elem(),
"DataObjectFill": reflect.ValueOf(&icons.DataObjectFill).Elem(),
"DataSaverOn": reflect.ValueOf(&icons.DataSaverOn).Elem(),
"DataSaverOnFill": reflect.ValueOf(&icons.DataSaverOnFill).Elem(),
"DataTable": reflect.ValueOf(&icons.DataTable).Elem(),
"DataTableFill": reflect.ValueOf(&icons.DataTableFill).Elem(),
"DataThresholding": reflect.ValueOf(&icons.DataThresholding).Elem(),
"DataThresholdingFill": reflect.ValueOf(&icons.DataThresholdingFill).Elem(),
"DataUsage": reflect.ValueOf(&icons.DataUsage).Elem(),
"DataUsageFill": reflect.ValueOf(&icons.DataUsageFill).Elem(),
"Database": reflect.ValueOf(&icons.Database).Elem(),
"DatabaseFill": reflect.ValueOf(&icons.DatabaseFill).Elem(),
"Dataset": reflect.ValueOf(&icons.Dataset).Elem(),
"DatasetFill": reflect.ValueOf(&icons.DatasetFill).Elem(),
"DatasetLinked": reflect.ValueOf(&icons.DatasetLinked).Elem(),
"DatasetLinkedFill": reflect.ValueOf(&icons.DatasetLinkedFill).Elem(),
"DateRange": reflect.ValueOf(&icons.DateRange).Elem(),
"DateRangeFill": reflect.ValueOf(&icons.DateRangeFill).Elem(),
"Deblur": reflect.ValueOf(&icons.Deblur).Elem(),
"DeblurFill": reflect.ValueOf(&icons.DeblurFill).Elem(),
"Debug": reflect.ValueOf(&icons.Debug).Elem(),
"DebugFill": reflect.ValueOf(&icons.DebugFill).Elem(),
"DecimalDecrease": reflect.ValueOf(&icons.DecimalDecrease).Elem(),
"DecimalDecreaseFill": reflect.ValueOf(&icons.DecimalDecreaseFill).Elem(),
"DecimalIncrease": reflect.ValueOf(&icons.DecimalIncrease).Elem(),
"DecimalIncreaseFill": reflect.ValueOf(&icons.DecimalIncreaseFill).Elem(),
"Deck": reflect.ValueOf(&icons.Deck).Elem(),
"DeckFill": reflect.ValueOf(&icons.DeckFill).Elem(),
"Dehaze": reflect.ValueOf(&icons.Dehaze).Elem(),
"DehazeFill": reflect.ValueOf(&icons.DehazeFill).Elem(),
"Delete": reflect.ValueOf(&icons.Delete).Elem(),
"DeleteFill": reflect.ValueOf(&icons.DeleteFill).Elem(),
"DeleteForever": reflect.ValueOf(&icons.DeleteForever).Elem(),
"DeleteForeverFill": reflect.ValueOf(&icons.DeleteForeverFill).Elem(),
"DeleteSweep": reflect.ValueOf(&icons.DeleteSweep).Elem(),
"DeleteSweepFill": reflect.ValueOf(&icons.DeleteSweepFill).Elem(),
"DensityLarge": reflect.ValueOf(&icons.DensityLarge).Elem(),
"DensityLargeFill": reflect.ValueOf(&icons.DensityLargeFill).Elem(),
"DensityMedium": reflect.ValueOf(&icons.DensityMedium).Elem(),
"DensityMediumFill": reflect.ValueOf(&icons.DensityMediumFill).Elem(),
"DensitySmall": reflect.ValueOf(&icons.DensitySmall).Elem(),
"DensitySmallFill": reflect.ValueOf(&icons.DensitySmallFill).Elem(),
"DeployedCode": reflect.ValueOf(&icons.DeployedCode).Elem(),
"DeployedCodeAlert": reflect.ValueOf(&icons.DeployedCodeAlert).Elem(),
"DeployedCodeAlertFill": reflect.ValueOf(&icons.DeployedCodeAlertFill).Elem(),
"DeployedCodeFill": reflect.ValueOf(&icons.DeployedCodeFill).Elem(),
"DeployedCodeHistory": reflect.ValueOf(&icons.DeployedCodeHistory).Elem(),
"DeployedCodeHistoryFill": reflect.ValueOf(&icons.DeployedCodeHistoryFill).Elem(),
"DeployedCodeUpdate": reflect.ValueOf(&icons.DeployedCodeUpdate).Elem(),
"DeployedCodeUpdateFill": reflect.ValueOf(&icons.DeployedCodeUpdateFill).Elem(),
"Description": reflect.ValueOf(&icons.Description).Elem(),
"DescriptionFill": reflect.ValueOf(&icons.DescriptionFill).Elem(),
"Deselect": reflect.ValueOf(&icons.Deselect).Elem(),
"DeselectFill": reflect.ValueOf(&icons.DeselectFill).Elem(),
"DesignServices": reflect.ValueOf(&icons.DesignServices).Elem(),
"DesignServicesFill": reflect.ValueOf(&icons.DesignServicesFill).Elem(),
"Desk": reflect.ValueOf(&icons.Desk).Elem(),
"DeskFill": reflect.ValueOf(&icons.DeskFill).Elem(),
"Deskphone": reflect.ValueOf(&icons.Deskphone).Elem(),
"DeskphoneFill": reflect.ValueOf(&icons.DeskphoneFill).Elem(),
"Desktop": reflect.ValueOf(&icons.Desktop).Elem(),
"DesktopAccessDisabled": reflect.ValueOf(&icons.DesktopAccessDisabled).Elem(),
"DesktopAccessDisabledFill": reflect.ValueOf(&icons.DesktopAccessDisabledFill).Elem(),
"DesktopFill": reflect.ValueOf(&icons.DesktopFill).Elem(),
"DesktopMac": reflect.ValueOf(&icons.DesktopMac).Elem(),
"DesktopMacFill": reflect.ValueOf(&icons.DesktopMacFill).Elem(),
"DesktopWindows": reflect.ValueOf(&icons.DesktopWindows).Elem(),
"DesktopWindowsFill": reflect.ValueOf(&icons.DesktopWindowsFill).Elem(),
"Details": reflect.ValueOf(&icons.Details).Elem(),
"DetailsFill": reflect.ValueOf(&icons.DetailsFill).Elem(),
"Detector": reflect.ValueOf(&icons.Detector).Elem(),
"DetectorAlarm": reflect.ValueOf(&icons.DetectorAlarm).Elem(),
"DetectorAlarmFill": reflect.ValueOf(&icons.DetectorAlarmFill).Elem(),
"DetectorBattery": reflect.ValueOf(&icons.DetectorBattery).Elem(),
"DetectorBatteryFill": reflect.ValueOf(&icons.DetectorBatteryFill).Elem(),
"DetectorCo": reflect.ValueOf(&icons.DetectorCo).Elem(),
"DetectorCoFill": reflect.ValueOf(&icons.DetectorCoFill).Elem(),
"DetectorFill": reflect.ValueOf(&icons.DetectorFill).Elem(),
"DetectorOffline": reflect.ValueOf(&icons.DetectorOffline).Elem(),
"DetectorOfflineFill": reflect.ValueOf(&icons.DetectorOfflineFill).Elem(),
"DetectorSmoke": reflect.ValueOf(&icons.DetectorSmoke).Elem(),
"DetectorSmokeFill": reflect.ValueOf(&icons.DetectorSmokeFill).Elem(),
"DetectorStatus": reflect.ValueOf(&icons.DetectorStatus).Elem(),
"DetectorStatusFill": reflect.ValueOf(&icons.DetectorStatusFill).Elem(),
"DeveloperBoard": reflect.ValueOf(&icons.DeveloperBoard).Elem(),
"DeveloperBoardFill": reflect.ValueOf(&icons.DeveloperBoardFill).Elem(),
"DeveloperBoardOff": reflect.ValueOf(&icons.DeveloperBoardOff).Elem(),
"DeveloperBoardOffFill": reflect.ValueOf(&icons.DeveloperBoardOffFill).Elem(),
"DeveloperGuide": reflect.ValueOf(&icons.DeveloperGuide).Elem(),
"DeveloperGuideFill": reflect.ValueOf(&icons.DeveloperGuideFill).Elem(),
"DeveloperMode": reflect.ValueOf(&icons.DeveloperMode).Elem(),
"DeveloperModeFill": reflect.ValueOf(&icons.DeveloperModeFill).Elem(),
"DeveloperModeTv": reflect.ValueOf(&icons.DeveloperModeTv).Elem(),
"DeveloperModeTvFill": reflect.ValueOf(&icons.DeveloperModeTvFill).Elem(),
"DeviceHub": reflect.ValueOf(&icons.DeviceHub).Elem(),
"DeviceHubFill": reflect.ValueOf(&icons.DeviceHubFill).Elem(),
"DeviceReset": reflect.ValueOf(&icons.DeviceReset).Elem(),
"DeviceResetFill": reflect.ValueOf(&icons.DeviceResetFill).Elem(),
"DeviceThermostat": reflect.ValueOf(&icons.DeviceThermostat).Elem(),
"DeviceThermostatFill": reflect.ValueOf(&icons.DeviceThermostatFill).Elem(),
"DeviceUnknown": reflect.ValueOf(&icons.DeviceUnknown).Elem(),
"DeviceUnknownFill": reflect.ValueOf(&icons.DeviceUnknownFill).Elem(),
"Devices": reflect.ValueOf(&icons.Devices).Elem(),
"DevicesFill": reflect.ValueOf(&icons.DevicesFill).Elem(),
"DevicesFold": reflect.ValueOf(&icons.DevicesFold).Elem(),
"DevicesFoldFill": reflect.ValueOf(&icons.DevicesFoldFill).Elem(),
"DevicesOff": reflect.ValueOf(&icons.DevicesOff).Elem(),
"DevicesOffFill": reflect.ValueOf(&icons.DevicesOffFill).Elem(),
"DevicesOther": reflect.ValueOf(&icons.DevicesOther).Elem(),
"DevicesOtherFill": reflect.ValueOf(&icons.DevicesOtherFill).Elem(),
"DevicesWearables": reflect.ValueOf(&icons.DevicesWearables).Elem(),
"DevicesWearablesFill": reflect.ValueOf(&icons.DevicesWearablesFill).Elem(),
"DialerSip": reflect.ValueOf(&icons.DialerSip).Elem(),
"DialerSipFill": reflect.ValueOf(&icons.DialerSipFill).Elem(),
"Dialogs": reflect.ValueOf(&icons.Dialogs).Elem(),
"DialogsFill": reflect.ValueOf(&icons.DialogsFill).Elem(),
"Dialpad": reflect.ValueOf(&icons.Dialpad).Elem(),
"DialpadFill": reflect.ValueOf(&icons.DialpadFill).Elem(),
"Diamond": reflect.ValueOf(&icons.Diamond).Elem(),
"DiamondFill": reflect.ValueOf(&icons.DiamondFill).Elem(),
"Difference": reflect.ValueOf(&icons.Difference).Elem(),
"DifferenceFill": reflect.ValueOf(&icons.DifferenceFill).Elem(),
"Dining": reflect.ValueOf(&icons.Dining).Elem(),
"DiningFill": reflect.ValueOf(&icons.DiningFill).Elem(),
"DinnerDining": reflect.ValueOf(&icons.DinnerDining).Elem(),
"DinnerDiningFill": reflect.ValueOf(&icons.DinnerDiningFill).Elem(),
"Directions": reflect.ValueOf(&icons.Directions).Elem(),
"DirectionsFill": reflect.ValueOf(&icons.DirectionsFill).Elem(),
"DirectionsOff": reflect.ValueOf(&icons.DirectionsOff).Elem(),
"DirectionsOffFill": reflect.ValueOf(&icons.DirectionsOffFill).Elem(),
"DirectorySync": reflect.ValueOf(&icons.DirectorySync).Elem(),
"DirectorySyncFill": reflect.ValueOf(&icons.DirectorySyncFill).Elem(),
"DirtyLens": reflect.ValueOf(&icons.DirtyLens).Elem(),
"DirtyLensFill": reflect.ValueOf(&icons.DirtyLensFill).Elem(),
"DisabledByDefault": reflect.ValueOf(&icons.DisabledByDefault).Elem(),
"DisabledByDefaultFill": reflect.ValueOf(&icons.DisabledByDefaultFill).Elem(),
"DisabledVisible": reflect.ValueOf(&icons.DisabledVisible).Elem(),
"DisabledVisibleFill": reflect.ValueOf(&icons.DisabledVisibleFill).Elem(),
"DiscFull": reflect.ValueOf(&icons.DiscFull).Elem(),
"DiscFullFill": reflect.ValueOf(&icons.DiscFullFill).Elem(),
"DisplayExternalInput": reflect.ValueOf(&icons.DisplayExternalInput).Elem(),
"DisplayExternalInputFill": reflect.ValueOf(&icons.DisplayExternalInputFill).Elem(),
"DisplaySettings": reflect.ValueOf(&icons.DisplaySettings).Elem(),
"DisplaySettingsFill": reflect.ValueOf(&icons.DisplaySettingsFill).Elem(),
"Distance": reflect.ValueOf(&icons.Distance).Elem(),
"DistanceFill": reflect.ValueOf(&icons.DistanceFill).Elem(),
"Diversity1": reflect.ValueOf(&icons.Diversity1).Elem(),
"Diversity1Fill": reflect.ValueOf(&icons.Diversity1Fill).Elem(),
"Diversity2": reflect.ValueOf(&icons.Diversity2).Elem(),
"Diversity2Fill": reflect.ValueOf(&icons.Diversity2Fill).Elem(),
"Diversity3": reflect.ValueOf(&icons.Diversity3).Elem(),
"Diversity3Fill": reflect.ValueOf(&icons.Diversity3Fill).Elem(),
"Diversity4": reflect.ValueOf(&icons.Diversity4).Elem(),
"Diversity4Fill": reflect.ValueOf(&icons.Diversity4Fill).Elem(),
"Dns": reflect.ValueOf(&icons.Dns).Elem(),
"DnsFill": reflect.ValueOf(&icons.DnsFill).Elem(),
"DoNotDisturbOff": reflect.ValueOf(&icons.DoNotDisturbOff).Elem(),
"DoNotDisturbOffFill": reflect.ValueOf(&icons.DoNotDisturbOffFill).Elem(),
"DoNotDisturbOn": reflect.ValueOf(&icons.DoNotDisturbOn).Elem(),
"DoNotDisturbOnFill": reflect.ValueOf(&icons.DoNotDisturbOnFill).Elem(),
"Dock": reflect.ValueOf(&icons.Dock).Elem(),
"DockFill": reflect.ValueOf(&icons.DockFill).Elem(),
"DockToBottom": reflect.ValueOf(&icons.DockToBottom).Elem(),
"DockToBottomFill": reflect.ValueOf(&icons.DockToBottomFill).Elem(),
"DockToLeft": reflect.ValueOf(&icons.DockToLeft).Elem(),
"DockToLeftFill": reflect.ValueOf(&icons.DockToLeftFill).Elem(),
"DockToRight": reflect.ValueOf(&icons.DockToRight).Elem(),
"DockToRightFill": reflect.ValueOf(&icons.DockToRightFill).Elem(),
"DocsAddOn": reflect.ValueOf(&icons.DocsAddOn).Elem(),
"DocsAddOnFill": reflect.ValueOf(&icons.DocsAddOnFill).Elem(),
"DocsAppsScript": reflect.ValueOf(&icons.DocsAppsScript).Elem(),
"DocsAppsScriptFill": reflect.ValueOf(&icons.DocsAppsScriptFill).Elem(),
"Document": reflect.ValueOf(&icons.Document).Elem(),
"DocumentFill": reflect.ValueOf(&icons.DocumentFill).Elem(),
"DocumentScanner": reflect.ValueOf(&icons.DocumentScanner).Elem(),
"DocumentScannerFill": reflect.ValueOf(&icons.DocumentScannerFill).Elem(),
"Domain": reflect.ValueOf(&icons.Domain).Elem(),
"DomainAdd": reflect.ValueOf(&icons.DomainAdd).Elem(),
"DomainAddFill": reflect.ValueOf(&icons.DomainAddFill).Elem(),
"DomainDisabled": reflect.ValueOf(&icons.DomainDisabled).Elem(),
"DomainDisabledFill": reflect.ValueOf(&icons.DomainDisabledFill).Elem(),
"DomainFill": reflect.ValueOf(&icons.DomainFill).Elem(),
"DomainVerification": reflect.ValueOf(&icons.DomainVerification).Elem(),
"DomainVerificationFill": reflect.ValueOf(&icons.DomainVerificationFill).Elem(),
"DomainVerificationOff": reflect.ValueOf(&icons.DomainVerificationOff).Elem(),
"DomainVerificationOffFill": reflect.ValueOf(&icons.DomainVerificationOffFill).Elem(),
"Done": reflect.ValueOf(&icons.Done).Elem(),
"DoneAll": reflect.ValueOf(&icons.DoneAll).Elem(),
"DoneAllFill": reflect.ValueOf(&icons.DoneAllFill).Elem(),
"DoneFill": reflect.ValueOf(&icons.DoneFill).Elem(),
"DoneOutline": reflect.ValueOf(&icons.DoneOutline).Elem(),
"DoneOutlineFill": reflect.ValueOf(&icons.DoneOutlineFill).Elem(),
"DonutLarge": reflect.ValueOf(&icons.DonutLarge).Elem(),
"DonutLargeFill": reflect.ValueOf(&icons.DonutLargeFill).Elem(),
"DonutSmall": reflect.ValueOf(&icons.DonutSmall).Elem(),
"DonutSmallFill": reflect.ValueOf(&icons.DonutSmallFill).Elem(),
"DoorOpen": reflect.ValueOf(&icons.DoorOpen).Elem(),
"DoorOpenFill": reflect.ValueOf(&icons.DoorOpenFill).Elem(),
"DoubleArrow": reflect.ValueOf(&icons.DoubleArrow).Elem(),
"DoubleArrowFill": reflect.ValueOf(&icons.DoubleArrowFill).Elem(),
"Download": reflect.ValueOf(&icons.Download).Elem(),
"DownloadDone": reflect.ValueOf(&icons.DownloadDone).Elem(),
"DownloadDoneFill": reflect.ValueOf(&icons.DownloadDoneFill).Elem(),
"DownloadFill": reflect.ValueOf(&icons.DownloadFill).Elem(),
"DownloadForOffline": reflect.ValueOf(&icons.DownloadForOffline).Elem(),
"DownloadForOfflineFill": reflect.ValueOf(&icons.DownloadForOfflineFill).Elem(),
"Downloading": reflect.ValueOf(&icons.Downloading).Elem(),
"DownloadingFill": reflect.ValueOf(&icons.DownloadingFill).Elem(),
"Draft": reflect.ValueOf(&icons.Draft).Elem(),
"DraftFill": reflect.ValueOf(&icons.DraftFill).Elem(),
"DraftOrders": reflect.ValueOf(&icons.DraftOrders).Elem(),
"DraftOrdersFill": reflect.ValueOf(&icons.DraftOrdersFill).Elem(),
"Drafts": reflect.ValueOf(&icons.Drafts).Elem(),
"DraftsFill": reflect.ValueOf(&icons.DraftsFill).Elem(),
"DragClick": reflect.ValueOf(&icons.DragClick).Elem(),
"DragClickFill": reflect.ValueOf(&icons.DragClickFill).Elem(),
"DragHandle": reflect.ValueOf(&icons.DragHandle).Elem(),
"DragHandleFill": reflect.ValueOf(&icons.DragHandleFill).Elem(),
"DragIndicator": reflect.ValueOf(&icons.DragIndicator).Elem(),
"DragIndicatorFill": reflect.ValueOf(&icons.DragIndicatorFill).Elem(),
"DragPan": reflect.ValueOf(&icons.DragPan).Elem(),
"DragPanFill": reflect.ValueOf(&icons.DragPanFill).Elem(),
"Draw": reflect.ValueOf(&icons.Draw).Elem(),
"DrawAbstract": reflect.ValueOf(&icons.DrawAbstract).Elem(),
"DrawAbstractFill": reflect.ValueOf(&icons.DrawAbstractFill).Elem(),
"DrawCollage": reflect.ValueOf(&icons.DrawCollage).Elem(),
"DrawCollageFill": reflect.ValueOf(&icons.DrawCollageFill).Elem(),
"DrawFill": reflect.ValueOf(&icons.DrawFill).Elem(),
"DriveFileMove": reflect.ValueOf(&icons.DriveFileMove).Elem(),
"DriveFileMoveFill": reflect.ValueOf(&icons.DriveFileMoveFill).Elem(),
"DriveFolderUpload": reflect.ValueOf(&icons.DriveFolderUpload).Elem(),
"DriveFolderUploadFill": reflect.ValueOf(&icons.DriveFolderUploadFill).Elem(),
"Dropdown": reflect.ValueOf(&icons.Dropdown).Elem(),
"DropdownFill": reflect.ValueOf(&icons.DropdownFill).Elem(),
"DualScreen": reflect.ValueOf(&icons.DualScreen).Elem(),
"DualScreenFill": reflect.ValueOf(&icons.DualScreenFill).Elem(),
"Dvr": reflect.ValueOf(&icons.Dvr).Elem(),
"DvrFill": reflect.ValueOf(&icons.DvrFill).Elem(),
"DynamicFeed": reflect.ValueOf(&icons.DynamicFeed).Elem(),
"DynamicFeedFill": reflect.ValueOf(&icons.DynamicFeedFill).Elem(),
"DynamicForm": reflect.ValueOf(&icons.DynamicForm).Elem(),
"DynamicFormFill": reflect.ValueOf(&icons.DynamicFormFill).Elem(),
"Earbuds": reflect.ValueOf(&icons.Earbuds).Elem(),
"EarbudsBattery": reflect.ValueOf(&icons.EarbudsBattery).Elem(),
"EarbudsBatteryFill": reflect.ValueOf(&icons.EarbudsBatteryFill).Elem(),
"EarbudsFill": reflect.ValueOf(&icons.EarbudsFill).Elem(),
"East": reflect.ValueOf(&icons.East).Elem(),
"EastFill": reflect.ValueOf(&icons.EastFill).Elem(),
"Eco": reflect.ValueOf(&icons.Eco).Elem(),
"EcoFill": reflect.ValueOf(&icons.EcoFill).Elem(),
"Edit": reflect.ValueOf(&icons.Edit).Elem(),
"EditAttributes": reflect.ValueOf(&icons.EditAttributes).Elem(),
"EditAttributesFill": reflect.ValueOf(&icons.EditAttributesFill).Elem(),
"EditCalendar": reflect.ValueOf(&icons.EditCalendar).Elem(),
"EditCalendarFill": reflect.ValueOf(&icons.EditCalendarFill).Elem(),
"EditDocument": reflect.ValueOf(&icons.EditDocument).Elem(),
"EditDocumentFill": reflect.ValueOf(&icons.EditDocumentFill).Elem(),
"EditFill": reflect.ValueOf(&icons.EditFill).Elem(),
"EditLocation": reflect.ValueOf(&icons.EditLocation).Elem(),
"EditLocationAlt": reflect.ValueOf(&icons.EditLocationAlt).Elem(),
"EditLocationAltFill": reflect.ValueOf(&icons.EditLocationAltFill).Elem(),
"EditLocationFill": reflect.ValueOf(&icons.EditLocationFill).Elem(),
"EditNote": reflect.ValueOf(&icons.EditNote).Elem(),
"EditNoteFill": reflect.ValueOf(&icons.EditNoteFill).Elem(),
"EditNotifications": reflect.ValueOf(&icons.EditNotifications).Elem(),
"EditNotificationsFill": reflect.ValueOf(&icons.EditNotificationsFill).Elem(),
"EditOff": reflect.ValueOf(&icons.EditOff).Elem(),
"EditOffFill": reflect.ValueOf(&icons.EditOffFill).Elem(),
"EditSquare": reflect.ValueOf(&icons.EditSquare).Elem(),
"EditSquareFill": reflect.ValueOf(&icons.EditSquareFill).Elem(),
"Egg": reflect.ValueOf(&icons.Egg).Elem(),
"EggAlt": reflect.ValueOf(&icons.EggAlt).Elem(),
"EggAltFill": reflect.ValueOf(&icons.EggAltFill).Elem(),
"EggFill": reflect.ValueOf(&icons.EggFill).Elem(),
"Eject": reflect.ValueOf(&icons.Eject).Elem(),
"EjectFill": reflect.ValueOf(&icons.EjectFill).Elem(),
"ElectricalServices": reflect.ValueOf(&icons.ElectricalServices).Elem(),
"ElectricalServicesFill": reflect.ValueOf(&icons.ElectricalServicesFill).Elem(),
"Elevation": reflect.ValueOf(&icons.Elevation).Elem(),
"ElevationFill": reflect.ValueOf(&icons.ElevationFill).Elem(),
"Emergency": reflect.ValueOf(&icons.Emergency).Elem(),
"EmergencyFill": reflect.ValueOf(&icons.EmergencyFill).Elem(),
"EmojiEvents": reflect.ValueOf(&icons.EmojiEvents).Elem(),
"EmojiEventsFill": reflect.ValueOf(&icons.EmojiEventsFill).Elem(),
"EmojiFlags": reflect.ValueOf(&icons.EmojiFlags).Elem(),
"EmojiFlagsFill": reflect.ValueOf(&icons.EmojiFlagsFill).Elem(),
"EmojiFoodBeverage": reflect.ValueOf(&icons.EmojiFoodBeverage).Elem(),
"EmojiFoodBeverageFill": reflect.ValueOf(&icons.EmojiFoodBeverageFill).Elem(),
"EmojiNature": reflect.ValueOf(&icons.EmojiNature).Elem(),
"EmojiNatureFill": reflect.ValueOf(&icons.EmojiNatureFill).Elem(),
"EmojiObjects": reflect.ValueOf(&icons.EmojiObjects).Elem(),
"EmojiObjectsFill": reflect.ValueOf(&icons.EmojiObjectsFill).Elem(),
"EmojiPeople": reflect.ValueOf(&icons.EmojiPeople).Elem(),
"EmojiPeopleFill": reflect.ValueOf(&icons.EmojiPeopleFill).Elem(),
"EmojiSymbols": reflect.ValueOf(&icons.EmojiSymbols).Elem(),
"EmojiSymbolsFill": reflect.ValueOf(&icons.EmojiSymbolsFill).Elem(),
"EmojiTransportation": reflect.ValueOf(&icons.EmojiTransportation).Elem(),
"EmojiTransportationFill": reflect.ValueOf(&icons.EmojiTransportationFill).Elem(),
"Emoticon": reflect.ValueOf(&icons.Emoticon).Elem(),
"EmoticonFill": reflect.ValueOf(&icons.EmoticonFill).Elem(),
"EmptyDashboard": reflect.ValueOf(&icons.EmptyDashboard).Elem(),
"EmptyDashboardFill": reflect.ValueOf(&icons.EmptyDashboardFill).Elem(),
"Enable": reflect.ValueOf(&icons.Enable).Elem(),
"EnableFill": reflect.ValueOf(&icons.EnableFill).Elem(),
"Encrypted": reflect.ValueOf(&icons.Encrypted).Elem(),
"EncryptedFill": reflect.ValueOf(&icons.EncryptedFill).Elem(),
"EnergySavingsLeaf": reflect.ValueOf(&icons.EnergySavingsLeaf).Elem(),
"EnergySavingsLeafFill": reflect.ValueOf(&icons.EnergySavingsLeafFill).Elem(),
"Engineering": reflect.ValueOf(&icons.Engineering).Elem(),
"EngineeringFill": reflect.ValueOf(&icons.EngineeringFill).Elem(),
"EnhancedEncryption": reflect.ValueOf(&icons.EnhancedEncryption).Elem(),
"EnhancedEncryptionFill": reflect.ValueOf(&icons.EnhancedEncryptionFill).Elem(),
"Equal": reflect.ValueOf(&icons.Equal).Elem(),
"EqualFill": reflect.ValueOf(&icons.EqualFill).Elem(),
"Equalizer": reflect.ValueOf(&icons.Equalizer).Elem(),
"EqualizerFill": reflect.ValueOf(&icons.EqualizerFill).Elem(),
"Error": reflect.ValueOf(&icons.Error).Elem(),
"ErrorFill": reflect.ValueOf(&icons.ErrorFill).Elem(),
"Euro": reflect.ValueOf(&icons.Euro).Elem(),
"EuroFill": reflect.ValueOf(&icons.EuroFill).Elem(),
"EuroSymbol": reflect.ValueOf(&icons.EuroSymbol).Elem(),
"EuroSymbolFill": reflect.ValueOf(&icons.EuroSymbolFill).Elem(),
"Event": reflect.ValueOf(&icons.Event).Elem(),
"EventAvailable": reflect.ValueOf(&icons.EventAvailable).Elem(),
"EventAvailableFill": reflect.ValueOf(&icons.EventAvailableFill).Elem(),
"EventBusy": reflect.ValueOf(&icons.EventBusy).Elem(),
"EventBusyFill": reflect.ValueOf(&icons.EventBusyFill).Elem(),
"EventFill": reflect.ValueOf(&icons.EventFill).Elem(),
"EventList": reflect.ValueOf(&icons.EventList).Elem(),
"EventListFill": reflect.ValueOf(&icons.EventListFill).Elem(),
"EventNote": reflect.ValueOf(&icons.EventNote).Elem(),
"EventNoteFill": reflect.ValueOf(&icons.EventNoteFill).Elem(),
"EventRepeat": reflect.ValueOf(&icons.EventRepeat).Elem(),
"EventRepeatFill": reflect.ValueOf(&icons.EventRepeatFill).Elem(),
"EventSeat": reflect.ValueOf(&icons.EventSeat).Elem(),
"EventSeatFill": reflect.ValueOf(&icons.EventSeatFill).Elem(),
"EventUpcoming": reflect.ValueOf(&icons.EventUpcoming).Elem(),
"EventUpcomingFill": reflect.ValueOf(&icons.EventUpcomingFill).Elem(),
"Exclamation": reflect.ValueOf(&icons.Exclamation).Elem(),
"ExclamationFill": reflect.ValueOf(&icons.ExclamationFill).Elem(),
"Exercise": reflect.ValueOf(&icons.Exercise).Elem(),
"ExerciseFill": reflect.ValueOf(&icons.ExerciseFill).Elem(),
"ExitToApp": reflect.ValueOf(&icons.ExitToApp).Elem(),
"ExitToAppFill": reflect.ValueOf(&icons.ExitToAppFill).Elem(),
"Expand": reflect.ValueOf(&icons.Expand).Elem(),
"ExpandAll": reflect.ValueOf(&icons.ExpandAll).Elem(),
"ExpandAllFill": reflect.ValueOf(&icons.ExpandAllFill).Elem(),
"ExpandCircleDown": reflect.ValueOf(&icons.ExpandCircleDown).Elem(),
"ExpandCircleDownFill": reflect.ValueOf(&icons.ExpandCircleDownFill).Elem(),
"ExpandCircleRight": reflect.ValueOf(&icons.ExpandCircleRight).Elem(),
"ExpandCircleRightFill": reflect.ValueOf(&icons.ExpandCircleRightFill).Elem(),
"ExpandCircleUp": reflect.ValueOf(&icons.ExpandCircleUp).Elem(),
"ExpandCircleUpFill": reflect.ValueOf(&icons.ExpandCircleUpFill).Elem(),
"ExpandContent": reflect.ValueOf(&icons.ExpandContent).Elem(),
"ExpandContentFill": reflect.ValueOf(&icons.ExpandContentFill).Elem(),
"ExpandFill": reflect.ValueOf(&icons.ExpandFill).Elem(),
"ExpandLess": reflect.ValueOf(&icons.ExpandLess).Elem(),
"ExpandLessFill": reflect.ValueOf(&icons.ExpandLessFill).Elem(),
"ExpandMore": reflect.ValueOf(&icons.ExpandMore).Elem(),
"ExpandMoreFill": reflect.ValueOf(&icons.ExpandMoreFill).Elem(),
"Explicit": reflect.ValueOf(&icons.Explicit).Elem(),
"ExplicitFill": reflect.ValueOf(&icons.ExplicitFill).Elem(),
"Explore": reflect.ValueOf(&icons.Explore).Elem(),
"ExploreFill": reflect.ValueOf(&icons.ExploreFill).Elem(),
"ExploreOff": reflect.ValueOf(&icons.ExploreOff).Elem(),
"ExploreOffFill": reflect.ValueOf(&icons.ExploreOffFill).Elem(),
"Explosion": reflect.ValueOf(&icons.Explosion).Elem(),
"ExplosionFill": reflect.ValueOf(&icons.ExplosionFill).Elem(),
"ExportNotes": reflect.ValueOf(&icons.ExportNotes).Elem(),
"ExportNotesFill": reflect.ValueOf(&icons.ExportNotesFill).Elem(),
"Exposure": reflect.ValueOf(&icons.Exposure).Elem(),
"ExposureFill": reflect.ValueOf(&icons.ExposureFill).Elem(),
"ExposureNeg1": reflect.ValueOf(&icons.ExposureNeg1).Elem(),
"ExposureNeg1Fill": reflect.ValueOf(&icons.ExposureNeg1Fill).Elem(),
"ExposureNeg2": reflect.ValueOf(&icons.ExposureNeg2).Elem(),
"ExposureNeg2Fill": reflect.ValueOf(&icons.ExposureNeg2Fill).Elem(),
"ExposurePlus1": reflect.ValueOf(&icons.ExposurePlus1).Elem(),
"ExposurePlus1Fill": reflect.ValueOf(&icons.ExposurePlus1Fill).Elem(),
"ExposurePlus2": reflect.ValueOf(&icons.ExposurePlus2).Elem(),
"ExposurePlus2Fill": reflect.ValueOf(&icons.ExposurePlus2Fill).Elem(),
"ExposureZero": reflect.ValueOf(&icons.ExposureZero).Elem(),
"ExposureZeroFill": reflect.ValueOf(&icons.ExposureZeroFill).Elem(),
"Extension": reflect.ValueOf(&icons.Extension).Elem(),
"ExtensionFill": reflect.ValueOf(&icons.ExtensionFill).Elem(),
"ExtensionOff": reflect.ValueOf(&icons.ExtensionOff).Elem(),
"ExtensionOffFill": reflect.ValueOf(&icons.ExtensionOffFill).Elem(),
"Face": reflect.ValueOf(&icons.Face).Elem(),
"Face2": reflect.ValueOf(&icons.Face2).Elem(),
"Face2Fill": reflect.ValueOf(&icons.Face2Fill).Elem(),
"Face3": reflect.ValueOf(&icons.Face3).Elem(),
"Face3Fill": reflect.ValueOf(&icons.Face3Fill).Elem(),
"Face4": reflect.ValueOf(&icons.Face4).Elem(),
"Face4Fill": reflect.ValueOf(&icons.Face4Fill).Elem(),
"Face5": reflect.ValueOf(&icons.Face5).Elem(),
"Face5Fill": reflect.ValueOf(&icons.Face5Fill).Elem(),
"Face6": reflect.ValueOf(&icons.Face6).Elem(),
"Face6Fill": reflect.ValueOf(&icons.Face6Fill).Elem(),
"FaceFill": reflect.ValueOf(&icons.FaceFill).Elem(),
"FaceRetouchingOff": reflect.ValueOf(&icons.FaceRetouchingOff).Elem(),
"FaceRetouchingOffFill": reflect.ValueOf(&icons.FaceRetouchingOffFill).Elem(),
"FactCheck": reflect.ValueOf(&icons.FactCheck).Elem(),
"FactCheckFill": reflect.ValueOf(&icons.FactCheckFill).Elem(),
"Factory": reflect.ValueOf(&icons.Factory).Elem(),
"FactoryFill": reflect.ValueOf(&icons.FactoryFill).Elem(),
"FamilyHistory": reflect.ValueOf(&icons.FamilyHistory).Elem(),
"FamilyHistoryFill": reflect.ValueOf(&icons.FamilyHistoryFill).Elem(),
"FamilyLink": reflect.ValueOf(&icons.FamilyLink).Elem(),
"FamilyLinkFill": reflect.ValueOf(&icons.FamilyLinkFill).Elem(),
"FastForward": reflect.ValueOf(&icons.FastForward).Elem(),
"FastForwardFill": reflect.ValueOf(&icons.FastForwardFill).Elem(),
"FastRewind": reflect.ValueOf(&icons.FastRewind).Elem(),
"FastRewindFill": reflect.ValueOf(&icons.FastRewindFill).Elem(),
"Faucet": reflect.ValueOf(&icons.Faucet).Elem(),
"FaucetFill": reflect.ValueOf(&icons.FaucetFill).Elem(),
"Favorite": reflect.ValueOf(&icons.Favorite).Elem(),
"FavoriteFill": reflect.ValueOf(&icons.FavoriteFill).Elem(),
"Fax": reflect.ValueOf(&icons.Fax).Elem(),
"FaxFill": reflect.ValueOf(&icons.FaxFill).Elem(),
"FeatureSearch": reflect.ValueOf(&icons.FeatureSearch).Elem(),
"FeatureSearchFill": reflect.ValueOf(&icons.FeatureSearchFill).Elem(),
"FeaturedPlayList": reflect.ValueOf(&icons.FeaturedPlayList).Elem(),
"FeaturedPlayListFill": reflect.ValueOf(&icons.FeaturedPlayListFill).Elem(),
"FeaturedVideo": reflect.ValueOf(&icons.FeaturedVideo).Elem(),
"FeaturedVideoFill": reflect.ValueOf(&icons.FeaturedVideoFill).Elem(),
"Feed": reflect.ValueOf(&icons.Feed).Elem(),
"FeedFill": reflect.ValueOf(&icons.FeedFill).Elem(),
"Feedback": reflect.ValueOf(&icons.Feedback).Elem(),
"FeedbackFill": reflect.ValueOf(&icons.FeedbackFill).Elem(),
"Female": reflect.ValueOf(&icons.Female).Elem(),
"FemaleFill": reflect.ValueOf(&icons.FemaleFill).Elem(),
"Fence": reflect.ValueOf(&icons.Fence).Elem(),
"FenceFill": reflect.ValueOf(&icons.FenceFill).Elem(),
"Festival": reflect.ValueOf(&icons.Festival).Elem(),
"FestivalFill": reflect.ValueOf(&icons.FestivalFill).Elem(),
"Field": reflect.ValueOf(&icons.Field).Elem(),
"File": reflect.ValueOf(&icons.File).Elem(),
"FileCopy": reflect.ValueOf(&icons.FileCopy).Elem(),
"FileCopyFill": reflect.ValueOf(&icons.FileCopyFill).Elem(),
"FileDownloadDone": reflect.ValueOf(&icons.FileDownloadDone).Elem(),
"FileDownloadDoneFill": reflect.ValueOf(&icons.FileDownloadDoneFill).Elem(),
"FileDownloadOff": reflect.ValueOf(&icons.FileDownloadOff).Elem(),
"FileDownloadOffFill": reflect.ValueOf(&icons.FileDownloadOffFill).Elem(),
"FileExe": reflect.ValueOf(&icons.FileExe).Elem(),
"FileMarkdown": reflect.ValueOf(&icons.FileMarkdown).Elem(),
"FileOpen": reflect.ValueOf(&icons.FileOpen).Elem(),
"FileOpenFill": reflect.ValueOf(&icons.FileOpenFill).Elem(),
"FilePresent": reflect.ValueOf(&icons.FilePresent).Elem(),
"FilePresentFill": reflect.ValueOf(&icons.FilePresentFill).Elem(),
"FileUploadOff": reflect.ValueOf(&icons.FileUploadOff).Elem(),
"FileUploadOffFill": reflect.ValueOf(&icons.FileUploadOffFill).Elem(),
"Filter": reflect.ValueOf(&icons.Filter).Elem(),
"Filter1": reflect.ValueOf(&icons.Filter1).Elem(),
"Filter1Fill": reflect.ValueOf(&icons.Filter1Fill).Elem(),
"Filter2": reflect.ValueOf(&icons.Filter2).Elem(),
"Filter2Fill": reflect.ValueOf(&icons.Filter2Fill).Elem(),
"Filter3": reflect.ValueOf(&icons.Filter3).Elem(),
"Filter3Fill": reflect.ValueOf(&icons.Filter3Fill).Elem(),
"Filter4": reflect.ValueOf(&icons.Filter4).Elem(),
"Filter4Fill": reflect.ValueOf(&icons.Filter4Fill).Elem(),
"Filter5": reflect.ValueOf(&icons.Filter5).Elem(),
"Filter5Fill": reflect.ValueOf(&icons.Filter5Fill).Elem(),
"Filter6": reflect.ValueOf(&icons.Filter6).Elem(),
"Filter6Fill": reflect.ValueOf(&icons.Filter6Fill).Elem(),
"Filter7": reflect.ValueOf(&icons.Filter7).Elem(),
"Filter7Fill": reflect.ValueOf(&icons.Filter7Fill).Elem(),
"Filter8": reflect.ValueOf(&icons.Filter8).Elem(),
"Filter8Fill": reflect.ValueOf(&icons.Filter8Fill).Elem(),
"Filter9": reflect.ValueOf(&icons.Filter9).Elem(),
"Filter9Fill": reflect.ValueOf(&icons.Filter9Fill).Elem(),
"Filter9Plus": reflect.ValueOf(&icons.Filter9Plus).Elem(),
"Filter9PlusFill": reflect.ValueOf(&icons.Filter9PlusFill).Elem(),
"FilterAlt": reflect.ValueOf(&icons.FilterAlt).Elem(),
"FilterAltFill": reflect.ValueOf(&icons.FilterAltFill).Elem(),
"FilterAltOff": reflect.ValueOf(&icons.FilterAltOff).Elem(),
"FilterAltOffFill": reflect.ValueOf(&icons.FilterAltOffFill).Elem(),
"FilterBAndW": reflect.ValueOf(&icons.FilterBAndW).Elem(),
"FilterBAndWFill": reflect.ValueOf(&icons.FilterBAndWFill).Elem(),
"FilterCenterFocus": reflect.ValueOf(&icons.FilterCenterFocus).Elem(),
"FilterCenterFocusFill": reflect.ValueOf(&icons.FilterCenterFocusFill).Elem(),
"FilterDrama": reflect.ValueOf(&icons.FilterDrama).Elem(),
"FilterDramaFill": reflect.ValueOf(&icons.FilterDramaFill).Elem(),
"FilterFill": reflect.ValueOf(&icons.FilterFill).Elem(),
"FilterFrames": reflect.ValueOf(&icons.FilterFrames).Elem(),
"FilterFramesFill": reflect.ValueOf(&icons.FilterFramesFill).Elem(),
"FilterHdr": reflect.ValueOf(&icons.FilterHdr).Elem(),
"FilterHdrFill": reflect.ValueOf(&icons.FilterHdrFill).Elem(),
"FilterList": reflect.ValueOf(&icons.FilterList).Elem(),
"FilterListFill": reflect.ValueOf(&icons.FilterListFill).Elem(),
"FilterListOff": reflect.ValueOf(&icons.FilterListOff).Elem(),
"FilterListOffFill": reflect.ValueOf(&icons.FilterListOffFill).Elem(),
"FilterNone": reflect.ValueOf(&icons.FilterNone).Elem(),
"FilterNoneFill": reflect.ValueOf(&icons.FilterNoneFill).Elem(),
"FilterTiltShift": reflect.ValueOf(&icons.FilterTiltShift).Elem(),
"FilterTiltShiftFill": reflect.ValueOf(&icons.FilterTiltShiftFill).Elem(),
"FilterVintage": reflect.ValueOf(&icons.FilterVintage).Elem(),
"FilterVintageFill": reflect.ValueOf(&icons.FilterVintageFill).Elem(),
"Finance": reflect.ValueOf(&icons.Finance).Elem(),
"FinanceChip": reflect.ValueOf(&icons.FinanceChip).Elem(),
"FinanceChipFill": reflect.ValueOf(&icons.FinanceChipFill).Elem(),
"FinanceFill": reflect.ValueOf(&icons.FinanceFill).Elem(),
"FindInPage": reflect.ValueOf(&icons.FindInPage).Elem(),
"FindInPageFill": reflect.ValueOf(&icons.FindInPageFill).Elem(),
"FindReplace": reflect.ValueOf(&icons.FindReplace).Elem(),
"FindReplaceFill": reflect.ValueOf(&icons.FindReplaceFill).Elem(),
"Fingerprint": reflect.ValueOf(&icons.Fingerprint).Elem(),
"FingerprintFill": reflect.ValueOf(&icons.FingerprintFill).Elem(),
"FirstPage": reflect.ValueOf(&icons.FirstPage).Elem(),
"FirstPageFill": reflect.ValueOf(&icons.FirstPageFill).Elem(),
"FitPage": reflect.ValueOf(&icons.FitPage).Elem(),
"FitPageFill": reflect.ValueOf(&icons.FitPageFill).Elem(),
"FitScreen": reflect.ValueOf(&icons.FitScreen).Elem(),
"FitScreenFill": reflect.ValueOf(&icons.FitScreenFill).Elem(),
"FitWidth": reflect.ValueOf(&icons.FitWidth).Elem(),
"FitWidthFill": reflect.ValueOf(&icons.FitWidthFill).Elem(),
"Flag": reflect.ValueOf(&icons.Flag).Elem(),
"FlagCircle": reflect.ValueOf(&icons.FlagCircle).Elem(),
"FlagCircleFill": reflect.ValueOf(&icons.FlagCircleFill).Elem(),
"FlagFill": reflect.ValueOf(&icons.FlagFill).Elem(),
"Flaky": reflect.ValueOf(&icons.Flaky).Elem(),
"FlakyFill": reflect.ValueOf(&icons.FlakyFill).Elem(),
"Flare": reflect.ValueOf(&icons.Flare).Elem(),
"FlareFill": reflect.ValueOf(&icons.FlareFill).Elem(),
"FlashlightOff": reflect.ValueOf(&icons.FlashlightOff).Elem(),
"FlashlightOffFill": reflect.ValueOf(&icons.FlashlightOffFill).Elem(),
"FlashlightOn": reflect.ValueOf(&icons.FlashlightOn).Elem(),
"FlashlightOnFill": reflect.ValueOf(&icons.FlashlightOnFill).Elem(),
"FlexDirection": reflect.ValueOf(&icons.FlexDirection).Elem(),
"FlexDirectionFill": reflect.ValueOf(&icons.FlexDirectionFill).Elem(),
"FlexNoWrap": reflect.ValueOf(&icons.FlexNoWrap).Elem(),
"FlexNoWrapFill": reflect.ValueOf(&icons.FlexNoWrapFill).Elem(),
"FlexWrap": reflect.ValueOf(&icons.FlexWrap).Elem(),
"FlexWrapFill": reflect.ValueOf(&icons.FlexWrapFill).Elem(),
"Flight": reflect.ValueOf(&icons.Flight).Elem(),
"FlightFill": reflect.ValueOf(&icons.FlightFill).Elem(),
"Flightsmode": reflect.ValueOf(&icons.Flightsmode).Elem(),
"FlightsmodeFill": reflect.ValueOf(&icons.FlightsmodeFill).Elem(),
"Flip": reflect.ValueOf(&icons.Flip).Elem(),
"FlipCameraAndroid": reflect.ValueOf(&icons.FlipCameraAndroid).Elem(),
"FlipCameraAndroidFill": reflect.ValueOf(&icons.FlipCameraAndroidFill).Elem(),
"FlipCameraIos": reflect.ValueOf(&icons.FlipCameraIos).Elem(),
"FlipCameraIosFill": reflect.ValueOf(&icons.FlipCameraIosFill).Elem(),
"FlipFill": reflect.ValueOf(&icons.FlipFill).Elem(),
"FlipToBack": reflect.ValueOf(&icons.FlipToBack).Elem(),
"FlipToBackFill": reflect.ValueOf(&icons.FlipToBackFill).Elem(),
"FlipToFront": reflect.ValueOf(&icons.FlipToFront).Elem(),
"FlipToFrontFill": reflect.ValueOf(&icons.FlipToFrontFill).Elem(),
"Floor": reflect.ValueOf(&icons.Floor).Elem(),
"FloorFill": reflect.ValueOf(&icons.FloorFill).Elem(),
"Flowsheet": reflect.ValueOf(&icons.Flowsheet).Elem(),
"FlowsheetFill": reflect.ValueOf(&icons.FlowsheetFill).Elem(),
"Fluid": reflect.ValueOf(&icons.Fluid).Elem(),
"FluidBalance": reflect.ValueOf(&icons.FluidBalance).Elem(),
"FluidBalanceFill": reflect.ValueOf(&icons.FluidBalanceFill).Elem(),
"FluidFill": reflect.ValueOf(&icons.FluidFill).Elem(),
"FluidMed": reflect.ValueOf(&icons.FluidMed).Elem(),
"FluidMedFill": reflect.ValueOf(&icons.FluidMedFill).Elem(),
"Flutter": reflect.ValueOf(&icons.Flutter).Elem(),
"FlutterDash": reflect.ValueOf(&icons.FlutterDash).Elem(),
"FlutterDashFill": reflect.ValueOf(&icons.FlutterDashFill).Elem(),
"FlutterFill": reflect.ValueOf(&icons.FlutterFill).Elem(),
"Folder": reflect.ValueOf(&icons.Folder).Elem(),
"FolderCopy": reflect.ValueOf(&icons.FolderCopy).Elem(),
"FolderCopyFill": reflect.ValueOf(&icons.FolderCopyFill).Elem(),
"FolderDelete": reflect.ValueOf(&icons.FolderDelete).Elem(),
"FolderDeleteFill": reflect.ValueOf(&icons.FolderDeleteFill).Elem(),
"FolderFill": reflect.ValueOf(&icons.FolderFill).Elem(),
"FolderManaged": reflect.ValueOf(&icons.FolderManaged).Elem(),
"FolderManagedFill": reflect.ValueOf(&icons.FolderManagedFill).Elem(),
"FolderOff": reflect.ValueOf(&icons.FolderOff).Elem(),
"FolderOffFill": reflect.ValueOf(&icons.FolderOffFill).Elem(),
"FolderOpen": reflect.ValueOf(&icons.FolderOpen).Elem(),
"FolderOpenFill": reflect.ValueOf(&icons.FolderOpenFill).Elem(),
"FolderShared": reflect.ValueOf(&icons.FolderShared).Elem(),
"FolderSharedFill": reflect.ValueOf(&icons.FolderSharedFill).Elem(),
"FolderSpecial": reflect.ValueOf(&icons.FolderSpecial).Elem(),
"FolderSpecialFill": reflect.ValueOf(&icons.FolderSpecialFill).Elem(),
"FolderSupervised": reflect.ValueOf(&icons.FolderSupervised).Elem(),
"FolderSupervisedFill": reflect.ValueOf(&icons.FolderSupervisedFill).Elem(),
"FolderZip": reflect.ValueOf(&icons.FolderZip).Elem(),
"FolderZipFill": reflect.ValueOf(&icons.FolderZipFill).Elem(),
"FontDownload": reflect.ValueOf(&icons.FontDownload).Elem(),
"FontDownloadFill": reflect.ValueOf(&icons.FontDownloadFill).Elem(),
"FontDownloadOff": reflect.ValueOf(&icons.FontDownloadOff).Elem(),
"FontDownloadOffFill": reflect.ValueOf(&icons.FontDownloadOffFill).Elem(),
"FormatAlignCenter": reflect.ValueOf(&icons.FormatAlignCenter).Elem(),
"FormatAlignCenterFill": reflect.ValueOf(&icons.FormatAlignCenterFill).Elem(),
"FormatAlignJustify": reflect.ValueOf(&icons.FormatAlignJustify).Elem(),
"FormatAlignJustifyFill": reflect.ValueOf(&icons.FormatAlignJustifyFill).Elem(),
"FormatAlignLeft": reflect.ValueOf(&icons.FormatAlignLeft).Elem(),
"FormatAlignLeftFill": reflect.ValueOf(&icons.FormatAlignLeftFill).Elem(),
"FormatAlignRight": reflect.ValueOf(&icons.FormatAlignRight).Elem(),
"FormatAlignRightFill": reflect.ValueOf(&icons.FormatAlignRightFill).Elem(),
"FormatBold": reflect.ValueOf(&icons.FormatBold).Elem(),
"FormatBoldFill": reflect.ValueOf(&icons.FormatBoldFill).Elem(),
"FormatClear": reflect.ValueOf(&icons.FormatClear).Elem(),
"FormatClearFill": reflect.ValueOf(&icons.FormatClearFill).Elem(),
"FormatColorFill": reflect.ValueOf(&icons.FormatColorFill).Elem(),
"FormatColorFillFill": reflect.ValueOf(&icons.FormatColorFillFill).Elem(),
"FormatColorReset": reflect.ValueOf(&icons.FormatColorReset).Elem(),
"FormatColorResetFill": reflect.ValueOf(&icons.FormatColorResetFill).Elem(),
"FormatColorText": reflect.ValueOf(&icons.FormatColorText).Elem(),
"FormatColorTextFill": reflect.ValueOf(&icons.FormatColorTextFill).Elem(),
"FormatH1": reflect.ValueOf(&icons.FormatH1).Elem(),
"FormatH1Fill": reflect.ValueOf(&icons.FormatH1Fill).Elem(),
"FormatH2": reflect.ValueOf(&icons.FormatH2).Elem(),
"FormatH2Fill": reflect.ValueOf(&icons.FormatH2Fill).Elem(),
"FormatH3": reflect.ValueOf(&icons.FormatH3).Elem(),
"FormatH3Fill": reflect.ValueOf(&icons.FormatH3Fill).Elem(),
"FormatH4": reflect.ValueOf(&icons.FormatH4).Elem(),
"FormatH4Fill": reflect.ValueOf(&icons.FormatH4Fill).Elem(),
"FormatH5": reflect.ValueOf(&icons.FormatH5).Elem(),
"FormatH5Fill": reflect.ValueOf(&icons.FormatH5Fill).Elem(),
"FormatH6": reflect.ValueOf(&icons.FormatH6).Elem(),
"FormatH6Fill": reflect.ValueOf(&icons.FormatH6Fill).Elem(),
"FormatImageLeft": reflect.ValueOf(&icons.FormatImageLeft).Elem(),
"FormatImageLeftFill": reflect.ValueOf(&icons.FormatImageLeftFill).Elem(),
"FormatImageRight": reflect.ValueOf(&icons.FormatImageRight).Elem(),
"FormatImageRightFill": reflect.ValueOf(&icons.FormatImageRightFill).Elem(),
"FormatIndentDecrease": reflect.ValueOf(&icons.FormatIndentDecrease).Elem(),
"FormatIndentDecreaseFill": reflect.ValueOf(&icons.FormatIndentDecreaseFill).Elem(),
"FormatIndentIncrease": reflect.ValueOf(&icons.FormatIndentIncrease).Elem(),
"FormatIndentIncreaseFill": reflect.ValueOf(&icons.FormatIndentIncreaseFill).Elem(),
"FormatInkHighlighter": reflect.ValueOf(&icons.FormatInkHighlighter).Elem(),
"FormatInkHighlighterFill": reflect.ValueOf(&icons.FormatInkHighlighterFill).Elem(),
"FormatItalic": reflect.ValueOf(&icons.FormatItalic).Elem(),
"FormatItalicFill": reflect.ValueOf(&icons.FormatItalicFill).Elem(),
"FormatLetterSpacing": reflect.ValueOf(&icons.FormatLetterSpacing).Elem(),
"FormatLetterSpacing2": reflect.ValueOf(&icons.FormatLetterSpacing2).Elem(),
"FormatLetterSpacing2Fill": reflect.ValueOf(&icons.FormatLetterSpacing2Fill).Elem(),
"FormatLetterSpacingFill": reflect.ValueOf(&icons.FormatLetterSpacingFill).Elem(),
"FormatLetterSpacingStandard": reflect.ValueOf(&icons.FormatLetterSpacingStandard).Elem(),
"FormatLetterSpacingStandardFill": reflect.ValueOf(&icons.FormatLetterSpacingStandardFill).Elem(),
"FormatLetterSpacingWide": reflect.ValueOf(&icons.FormatLetterSpacingWide).Elem(),
"FormatLetterSpacingWideFill": reflect.ValueOf(&icons.FormatLetterSpacingWideFill).Elem(),
"FormatLetterSpacingWider": reflect.ValueOf(&icons.FormatLetterSpacingWider).Elem(),
"FormatLetterSpacingWiderFill": reflect.ValueOf(&icons.FormatLetterSpacingWiderFill).Elem(),
"FormatLineSpacing": reflect.ValueOf(&icons.FormatLineSpacing).Elem(),
"FormatLineSpacingFill": reflect.ValueOf(&icons.FormatLineSpacingFill).Elem(),
"FormatListBulleted": reflect.ValueOf(&icons.FormatListBulleted).Elem(),
"FormatListBulletedAdd": reflect.ValueOf(&icons.FormatListBulletedAdd).Elem(),
"FormatListBulletedAddFill": reflect.ValueOf(&icons.FormatListBulletedAddFill).Elem(),
"FormatListBulletedFill": reflect.ValueOf(&icons.FormatListBulletedFill).Elem(),
"FormatListNumbered": reflect.ValueOf(&icons.FormatListNumbered).Elem(),
"FormatListNumberedFill": reflect.ValueOf(&icons.FormatListNumberedFill).Elem(),
"FormatListNumberedRtl": reflect.ValueOf(&icons.FormatListNumberedRtl).Elem(),
"FormatListNumberedRtlFill": reflect.ValueOf(&icons.FormatListNumberedRtlFill).Elem(),
"FormatOverline": reflect.ValueOf(&icons.FormatOverline).Elem(),
"FormatOverlineFill": reflect.ValueOf(&icons.FormatOverlineFill).Elem(),
"FormatPaint": reflect.ValueOf(&icons.FormatPaint).Elem(),
"FormatPaintFill": reflect.ValueOf(&icons.FormatPaintFill).Elem(),
"FormatParagraph": reflect.ValueOf(&icons.FormatParagraph).Elem(),
"FormatParagraphFill": reflect.ValueOf(&icons.FormatParagraphFill).Elem(),
"FormatQuote": reflect.ValueOf(&icons.FormatQuote).Elem(),
"FormatQuoteFill": reflect.ValueOf(&icons.FormatQuoteFill).Elem(),
"FormatShapes": reflect.ValueOf(&icons.FormatShapes).Elem(),
"FormatShapesFill": reflect.ValueOf(&icons.FormatShapesFill).Elem(),
"FormatSize": reflect.ValueOf(&icons.FormatSize).Elem(),
"FormatSizeFill": reflect.ValueOf(&icons.FormatSizeFill).Elem(),
"FormatStrikethrough": reflect.ValueOf(&icons.FormatStrikethrough).Elem(),
"FormatStrikethroughFill": reflect.ValueOf(&icons.FormatStrikethroughFill).Elem(),
"FormatTextClip": reflect.ValueOf(&icons.FormatTextClip).Elem(),
"FormatTextClipFill": reflect.ValueOf(&icons.FormatTextClipFill).Elem(),
"FormatTextOverflow": reflect.ValueOf(&icons.FormatTextOverflow).Elem(),
"FormatTextOverflowFill": reflect.ValueOf(&icons.FormatTextOverflowFill).Elem(),
"FormatTextWrap": reflect.ValueOf(&icons.FormatTextWrap).Elem(),
"FormatTextWrapFill": reflect.ValueOf(&icons.FormatTextWrapFill).Elem(),
"FormatTextdirectionLToR": reflect.ValueOf(&icons.FormatTextdirectionLToR).Elem(),
"FormatTextdirectionLToRFill": reflect.ValueOf(&icons.FormatTextdirectionLToRFill).Elem(),
"FormatTextdirectionRToL": reflect.ValueOf(&icons.FormatTextdirectionRToL).Elem(),
"FormatTextdirectionRToLFill": reflect.ValueOf(&icons.FormatTextdirectionRToLFill).Elem(),
"FormatUnderlined": reflect.ValueOf(&icons.FormatUnderlined).Elem(),
"FormatUnderlinedFill": reflect.ValueOf(&icons.FormatUnderlinedFill).Elem(),
"FormatUnderlinedSquiggle": reflect.ValueOf(&icons.FormatUnderlinedSquiggle).Elem(),
"FormatUnderlinedSquiggleFill": reflect.ValueOf(&icons.FormatUnderlinedSquiggleFill).Elem(),
"FormsAddOn": reflect.ValueOf(&icons.FormsAddOn).Elem(),
"FormsAddOnFill": reflect.ValueOf(&icons.FormsAddOnFill).Elem(),
"FormsAppsScript": reflect.ValueOf(&icons.FormsAppsScript).Elem(),
"FormsAppsScriptFill": reflect.ValueOf(&icons.FormsAppsScriptFill).Elem(),
"Forum": reflect.ValueOf(&icons.Forum).Elem(),
"ForumFill": reflect.ValueOf(&icons.ForumFill).Elem(),
"Forward": reflect.ValueOf(&icons.Forward).Elem(),
"Forward10": reflect.ValueOf(&icons.Forward10).Elem(),
"Forward10Fill": reflect.ValueOf(&icons.Forward10Fill).Elem(),
"Forward30": reflect.ValueOf(&icons.Forward30).Elem(),
"Forward30Fill": reflect.ValueOf(&icons.Forward30Fill).Elem(),
"Forward5": reflect.ValueOf(&icons.Forward5).Elem(),
"Forward5Fill": reflect.ValueOf(&icons.Forward5Fill).Elem(),
"ForwardCircle": reflect.ValueOf(&icons.ForwardCircle).Elem(),
"ForwardCircleFill": reflect.ValueOf(&icons.ForwardCircleFill).Elem(),
"ForwardFill": reflect.ValueOf(&icons.ForwardFill).Elem(),
"ForwardMedia": reflect.ValueOf(&icons.ForwardMedia).Elem(),
"ForwardMediaFill": reflect.ValueOf(&icons.ForwardMediaFill).Elem(),
"ForwardToInbox": reflect.ValueOf(&icons.ForwardToInbox).Elem(),
"ForwardToInboxFill": reflect.ValueOf(&icons.ForwardToInboxFill).Elem(),
"FrameInspect": reflect.ValueOf(&icons.FrameInspect).Elem(),
"FrameInspectFill": reflect.ValueOf(&icons.FrameInspectFill).Elem(),
"FramePerson": reflect.ValueOf(&icons.FramePerson).Elem(),
"FramePersonFill": reflect.ValueOf(&icons.FramePersonFill).Elem(),
"FramePersonOff": reflect.ValueOf(&icons.FramePersonOff).Elem(),
"FramePersonOffFill": reflect.ValueOf(&icons.FramePersonOffFill).Elem(),
"FrameReload": reflect.ValueOf(&icons.FrameReload).Elem(),
"FrameReloadFill": reflect.ValueOf(&icons.FrameReloadFill).Elem(),
"FrameSource": reflect.ValueOf(&icons.FrameSource).Elem(),
"FrameSourceFill": reflect.ValueOf(&icons.FrameSourceFill).Elem(),
"FreeCancellation": reflect.ValueOf(&icons.FreeCancellation).Elem(),
"FreeCancellationFill": reflect.ValueOf(&icons.FreeCancellationFill).Elem(),
"FrontHand": reflect.ValueOf(&icons.FrontHand).Elem(),
"FrontHandFill": reflect.ValueOf(&icons.FrontHandFill).Elem(),
"FullCoverage": reflect.ValueOf(&icons.FullCoverage).Elem(),
"FullCoverageFill": reflect.ValueOf(&icons.FullCoverageFill).Elem(),
"FullStackedBarChart": reflect.ValueOf(&icons.FullStackedBarChart).Elem(),
"FullStackedBarChartFill": reflect.ValueOf(&icons.FullStackedBarChartFill).Elem(),
"Fullscreen": reflect.ValueOf(&icons.Fullscreen).Elem(),
"FullscreenExit": reflect.ValueOf(&icons.FullscreenExit).Elem(),
"FullscreenExitFill": reflect.ValueOf(&icons.FullscreenExitFill).Elem(),
"FullscreenFill": reflect.ValueOf(&icons.FullscreenFill).Elem(),
"Function": reflect.ValueOf(&icons.Function).Elem(),
"FunctionFill": reflect.ValueOf(&icons.FunctionFill).Elem(),
"Functions": reflect.ValueOf(&icons.Functions).Elem(),
"FunctionsFill": reflect.ValueOf(&icons.FunctionsFill).Elem(),
"GTranslate": reflect.ValueOf(&icons.GTranslate).Elem(),
"GTranslateFill": reflect.ValueOf(&icons.GTranslateFill).Elem(),
"GalleryThumbnail": reflect.ValueOf(&icons.GalleryThumbnail).Elem(),
"GalleryThumbnailFill": reflect.ValueOf(&icons.GalleryThumbnailFill).Elem(),
"Gamepad": reflect.ValueOf(&icons.Gamepad).Elem(),
"GamepadFill": reflect.ValueOf(&icons.GamepadFill).Elem(),
"Genres": reflect.ValueOf(&icons.Genres).Elem(),
"GenresFill": reflect.ValueOf(&icons.GenresFill).Elem(),
"Gesture": reflect.ValueOf(&icons.Gesture).Elem(),
"GestureFill": reflect.ValueOf(&icons.GestureFill).Elem(),
"GestureSelect": reflect.ValueOf(&icons.GestureSelect).Elem(),
"GestureSelectFill": reflect.ValueOf(&icons.GestureSelectFill).Elem(),
"Gif": reflect.ValueOf(&icons.Gif).Elem(),
"GifBox": reflect.ValueOf(&icons.GifBox).Elem(),
"GifBoxFill": reflect.ValueOf(&icons.GifBoxFill).Elem(),
"GifFill": reflect.ValueOf(&icons.GifFill).Elem(),
"Girl": reflect.ValueOf(&icons.Girl).Elem(),
"GirlFill": reflect.ValueOf(&icons.GirlFill).Elem(),
"Git": reflect.ValueOf(&icons.Git).Elem(),
"GitHub": reflect.ValueOf(&icons.GitHub).Elem(),
"GlassCup": reflect.ValueOf(&icons.GlassCup).Elem(),
"GlassCupFill": reflect.ValueOf(&icons.GlassCupFill).Elem(),
"GlobeAsia": reflect.ValueOf(&icons.GlobeAsia).Elem(),
"GlobeAsiaFill": reflect.ValueOf(&icons.GlobeAsiaFill).Elem(),
"GlobeUk": reflect.ValueOf(&icons.GlobeUk).Elem(),
"GlobeUkFill": reflect.ValueOf(&icons.GlobeUkFill).Elem(),
"Glyphs": reflect.ValueOf(&icons.Glyphs).Elem(),
"GlyphsFill": reflect.ValueOf(&icons.GlyphsFill).Elem(),
"Go": reflect.ValueOf(&icons.Go).Elem(),
"GoToLine": reflect.ValueOf(&icons.GoToLine).Elem(),
"GoToLineFill": reflect.ValueOf(&icons.GoToLineFill).Elem(),
"Grade": reflect.ValueOf(&icons.Grade).Elem(),
"GradeFill": reflect.ValueOf(&icons.GradeFill).Elem(),
"Gradient": reflect.ValueOf(&icons.Gradient).Elem(),
"GradientFill": reflect.ValueOf(&icons.GradientFill).Elem(),
"Grading": reflect.ValueOf(&icons.Grading).Elem(),
"GradingFill": reflect.ValueOf(&icons.GradingFill).Elem(),
"GraphicEq": reflect.ValueOf(&icons.GraphicEq).Elem(),
"GraphicEqFill": reflect.ValueOf(&icons.GraphicEqFill).Elem(),
"Grid3x3": reflect.ValueOf(&icons.Grid3x3).Elem(),
"Grid3x3Fill": reflect.ValueOf(&icons.Grid3x3Fill).Elem(),
"Grid3x3Off": reflect.ValueOf(&icons.Grid3x3Off).Elem(),
"Grid3x3OffFill": reflect.ValueOf(&icons.Grid3x3OffFill).Elem(),
"Grid4x4": reflect.ValueOf(&icons.Grid4x4).Elem(),
"Grid4x4Fill": reflect.ValueOf(&icons.Grid4x4Fill).Elem(),
"GridGoldenratio": reflect.ValueOf(&icons.GridGoldenratio).Elem(),
"GridGoldenratioFill": reflect.ValueOf(&icons.GridGoldenratioFill).Elem(),
"GridGuides": reflect.ValueOf(&icons.GridGuides).Elem(),
"GridGuidesFill": reflect.ValueOf(&icons.GridGuidesFill).Elem(),
"GridOff": reflect.ValueOf(&icons.GridOff).Elem(),
"GridOffFill": reflect.ValueOf(&icons.GridOffFill).Elem(),
"GridOn": reflect.ValueOf(&icons.GridOn).Elem(),
"GridOnFill": reflect.ValueOf(&icons.GridOnFill).Elem(),
"GridView": reflect.ValueOf(&icons.GridView).Elem(),
"GridViewFill": reflect.ValueOf(&icons.GridViewFill).Elem(),
"Group": reflect.ValueOf(&icons.Group).Elem(),
"GroupAdd": reflect.ValueOf(&icons.GroupAdd).Elem(),
"GroupAddFill": reflect.ValueOf(&icons.GroupAddFill).Elem(),
"GroupFill": reflect.ValueOf(&icons.GroupFill).Elem(),
"GroupOff": reflect.ValueOf(&icons.GroupOff).Elem(),
"GroupOffFill": reflect.ValueOf(&icons.GroupOffFill).Elem(),
"GroupRemove": reflect.ValueOf(&icons.GroupRemove).Elem(),
"GroupRemoveFill": reflect.ValueOf(&icons.GroupRemoveFill).Elem(),
"GroupWork": reflect.ValueOf(&icons.GroupWork).Elem(),
"GroupWorkFill": reflect.ValueOf(&icons.GroupWorkFill).Elem(),
"GroupedBarChart": reflect.ValueOf(&icons.GroupedBarChart).Elem(),
"GroupedBarChartFill": reflect.ValueOf(&icons.GroupedBarChartFill).Elem(),
"Groups": reflect.ValueOf(&icons.Groups).Elem(),
"Groups2": reflect.ValueOf(&icons.Groups2).Elem(),
"Groups2Fill": reflect.ValueOf(&icons.Groups2Fill).Elem(),
"Groups3": reflect.ValueOf(&icons.Groups3).Elem(),
"Groups3Fill": reflect.ValueOf(&icons.Groups3Fill).Elem(),
"GroupsFill": reflect.ValueOf(&icons.GroupsFill).Elem(),
"HandGesture": reflect.ValueOf(&icons.HandGesture).Elem(),
"HandGestureFill": reflect.ValueOf(&icons.HandGestureFill).Elem(),
"Handshake": reflect.ValueOf(&icons.Handshake).Elem(),
"HandshakeFill": reflect.ValueOf(&icons.HandshakeFill).Elem(),
"HangoutVideo": reflect.ValueOf(&icons.HangoutVideo).Elem(),
"HangoutVideoFill": reflect.ValueOf(&icons.HangoutVideoFill).Elem(),
"HangoutVideoOff": reflect.ValueOf(&icons.HangoutVideoOff).Elem(),
"HangoutVideoOffFill": reflect.ValueOf(&icons.HangoutVideoOffFill).Elem(),
"HardDrive": reflect.ValueOf(&icons.HardDrive).Elem(),
"HardDrive2": reflect.ValueOf(&icons.HardDrive2).Elem(),
"HardDrive2Fill": reflect.ValueOf(&icons.HardDrive2Fill).Elem(),
"HardDriveFill": reflect.ValueOf(&icons.HardDriveFill).Elem(),
"Hardware": reflect.ValueOf(&icons.Hardware).Elem(),
"HardwareFill": reflect.ValueOf(&icons.HardwareFill).Elem(),
"Hd": reflect.ValueOf(&icons.Hd).Elem(),
"HdFill": reflect.ValueOf(&icons.HdFill).Elem(),
"Headphones": reflect.ValueOf(&icons.Headphones).Elem(),
"HeadphonesBattery": reflect.ValueOf(&icons.HeadphonesBattery).Elem(),
"HeadphonesBatteryFill": reflect.ValueOf(&icons.HeadphonesBatteryFill).Elem(),
"HeadphonesFill": reflect.ValueOf(&icons.HeadphonesFill).Elem(),
"HeadsetMic": reflect.ValueOf(&icons.HeadsetMic).Elem(),
"HeadsetMicFill": reflect.ValueOf(&icons.HeadsetMicFill).Elem(),
"HeadsetOff": reflect.ValueOf(&icons.HeadsetOff).Elem(),
"HeadsetOffFill": reflect.ValueOf(&icons.HeadsetOffFill).Elem(),
"Hearing": reflect.ValueOf(&icons.Hearing).Elem(),
"HearingDisabled": reflect.ValueOf(&icons.HearingDisabled).Elem(),
"HearingDisabledFill": reflect.ValueOf(&icons.HearingDisabledFill).Elem(),
"HearingFill": reflect.ValueOf(&icons.HearingFill).Elem(),
"HeartMinus": reflect.ValueOf(&icons.HeartMinus).Elem(),
"HeartMinusFill": reflect.ValueOf(&icons.HeartMinusFill).Elem(),
"HeartPlus": reflect.ValueOf(&icons.HeartPlus).Elem(),
"HeartPlusFill": reflect.ValueOf(&icons.HeartPlusFill).Elem(),
"Height": reflect.ValueOf(&icons.Height).Elem(),
"HeightFill": reflect.ValueOf(&icons.HeightFill).Elem(),
"Help": reflect.ValueOf(&icons.Help).Elem(),
"HelpFill": reflect.ValueOf(&icons.HelpFill).Elem(),
"Hexagon": reflect.ValueOf(&icons.Hexagon).Elem(),
"HexagonFill": reflect.ValueOf(&icons.HexagonFill).Elem(),
"Hide": reflect.ValueOf(&icons.Hide).Elem(),
"HideFill": reflect.ValueOf(&icons.HideFill).Elem(),
"HideImage": reflect.ValueOf(&icons.HideImage).Elem(),
"HideImageFill": reflect.ValueOf(&icons.HideImageFill).Elem(),
"HideSource": reflect.ValueOf(&icons.HideSource).Elem(),
"HideSourceFill": reflect.ValueOf(&icons.HideSourceFill).Elem(),
"HighDensity": reflect.ValueOf(&icons.HighDensity).Elem(),
"HighDensityFill": reflect.ValueOf(&icons.HighDensityFill).Elem(),
"HighQuality": reflect.ValueOf(&icons.HighQuality).Elem(),
"HighQualityFill": reflect.ValueOf(&icons.HighQualityFill).Elem(),
"Highlight": reflect.ValueOf(&icons.Highlight).Elem(),
"HighlightFill": reflect.ValueOf(&icons.HighlightFill).Elem(),
"HighlighterSize1": reflect.ValueOf(&icons.HighlighterSize1).Elem(),
"HighlighterSize1Fill": reflect.ValueOf(&icons.HighlighterSize1Fill).Elem(),
"HighlighterSize2": reflect.ValueOf(&icons.HighlighterSize2).Elem(),
"HighlighterSize2Fill": reflect.ValueOf(&icons.HighlighterSize2Fill).Elem(),
"HighlighterSize3": reflect.ValueOf(&icons.HighlighterSize3).Elem(),
"HighlighterSize3Fill": reflect.ValueOf(&icons.HighlighterSize3Fill).Elem(),
"HighlighterSize4": reflect.ValueOf(&icons.HighlighterSize4).Elem(),
"HighlighterSize4Fill": reflect.ValueOf(&icons.HighlighterSize4Fill).Elem(),
"HighlighterSize5": reflect.ValueOf(&icons.HighlighterSize5).Elem(),
"HighlighterSize5Fill": reflect.ValueOf(&icons.HighlighterSize5Fill).Elem(),
"History": reflect.ValueOf(&icons.History).Elem(),
"HistoryFill": reflect.ValueOf(&icons.HistoryFill).Elem(),
"HistoryToggleOff": reflect.ValueOf(&icons.HistoryToggleOff).Elem(),
"HistoryToggleOffFill": reflect.ValueOf(&icons.HistoryToggleOffFill).Elem(),
"Hive": reflect.ValueOf(&icons.Hive).Elem(),
"HiveFill": reflect.ValueOf(&icons.HiveFill).Elem(),
"Home": reflect.ValueOf(&icons.Home).Elem(),
"HomeAppLogo": reflect.ValueOf(&icons.HomeAppLogo).Elem(),
"HomeAppLogoFill": reflect.ValueOf(&icons.HomeAppLogoFill).Elem(),
"HomeFill": reflect.ValueOf(&icons.HomeFill).Elem(),
"HomeIotDevice": reflect.ValueOf(&icons.HomeIotDevice).Elem(),
"HomeIotDeviceFill": reflect.ValueOf(&icons.HomeIotDeviceFill).Elem(),
"HomePin": reflect.ValueOf(&icons.HomePin).Elem(),
"HomePinFill": reflect.ValueOf(&icons.HomePinFill).Elem(),
"HomeStorage": reflect.ValueOf(&icons.HomeStorage).Elem(),
"HomeStorageFill": reflect.ValueOf(&icons.HomeStorageFill).Elem(),
"HorizontalDistribute": reflect.ValueOf(&icons.HorizontalDistribute).Elem(),
"HorizontalDistributeFill": reflect.ValueOf(&icons.HorizontalDistributeFill).Elem(),
"HorizontalRule": reflect.ValueOf(&icons.HorizontalRule).Elem(),
"HorizontalRuleFill": reflect.ValueOf(&icons.HorizontalRuleFill).Elem(),
"HorizontalSplit": reflect.ValueOf(&icons.HorizontalSplit).Elem(),
"HorizontalSplitFill": reflect.ValueOf(&icons.HorizontalSplitFill).Elem(),
"HourglassBottom": reflect.ValueOf(&icons.HourglassBottom).Elem(),
"HourglassBottomFill": reflect.ValueOf(&icons.HourglassBottomFill).Elem(),
"HourglassDisabled": reflect.ValueOf(&icons.HourglassDisabled).Elem(),
"HourglassDisabledFill": reflect.ValueOf(&icons.HourglassDisabledFill).Elem(),
"HourglassEmpty": reflect.ValueOf(&icons.HourglassEmpty).Elem(),
"HourglassEmptyFill": reflect.ValueOf(&icons.HourglassEmptyFill).Elem(),
"HourglassTop": reflect.ValueOf(&icons.HourglassTop).Elem(),
"HourglassTopFill": reflect.ValueOf(&icons.HourglassTopFill).Elem(),
"House": reflect.ValueOf(&icons.House).Elem(),
"HouseFill": reflect.ValueOf(&icons.HouseFill).Elem(),
"Html": reflect.ValueOf(&icons.Html).Elem(),
"HtmlFill": reflect.ValueOf(&icons.HtmlFill).Elem(),
"Http": reflect.ValueOf(&icons.Http).Elem(),
"HttpFill": reflect.ValueOf(&icons.HttpFill).Elem(),
"Hub": reflect.ValueOf(&icons.Hub).Elem(),
"HubFill": reflect.ValueOf(&icons.HubFill).Elem(),
"Iframe": reflect.ValueOf(&icons.Iframe).Elem(),
"IframeFill": reflect.ValueOf(&icons.IframeFill).Elem(),
"IframeOff": reflect.ValueOf(&icons.IframeOff).Elem(),
"IframeOffFill": reflect.ValueOf(&icons.IframeOffFill).Elem(),
"Image": reflect.ValueOf(&icons.Image).Elem(),
"ImageAspectRatio": reflect.ValueOf(&icons.ImageAspectRatio).Elem(),
"ImageAspectRatioFill": reflect.ValueOf(&icons.ImageAspectRatioFill).Elem(),
"ImageFill": reflect.ValueOf(&icons.ImageFill).Elem(),
"ImageNotSupported": reflect.ValueOf(&icons.ImageNotSupported).Elem(),
"ImageNotSupportedFill": reflect.ValueOf(&icons.ImageNotSupportedFill).Elem(),
"ImageSearch": reflect.ValueOf(&icons.ImageSearch).Elem(),
"ImageSearchFill": reflect.ValueOf(&icons.ImageSearchFill).Elem(),
"Imagesmode": reflect.ValueOf(&icons.Imagesmode).Elem(),
"ImagesmodeFill": reflect.ValueOf(&icons.ImagesmodeFill).Elem(),
"ImportContacts": reflect.ValueOf(&icons.ImportContacts).Elem(),
"ImportContactsFill": reflect.ValueOf(&icons.ImportContactsFill).Elem(),
"ImportantDevices": reflect.ValueOf(&icons.ImportantDevices).Elem(),
"ImportantDevicesFill": reflect.ValueOf(&icons.ImportantDevicesFill).Elem(),
"InactiveOrder": reflect.ValueOf(&icons.InactiveOrder).Elem(),
"InactiveOrderFill": reflect.ValueOf(&icons.InactiveOrderFill).Elem(),
"Inbox": reflect.ValueOf(&icons.Inbox).Elem(),
"InboxCustomize": reflect.ValueOf(&icons.InboxCustomize).Elem(),
"InboxCustomizeFill": reflect.ValueOf(&icons.InboxCustomizeFill).Elem(),
"InboxFill": reflect.ValueOf(&icons.InboxFill).Elem(),
"IncompleteCircle": reflect.ValueOf(&icons.IncompleteCircle).Elem(),
"IncompleteCircleFill": reflect.ValueOf(&icons.IncompleteCircleFill).Elem(),
"IndeterminateCheckBox": reflect.ValueOf(&icons.IndeterminateCheckBox).Elem(),
"IndeterminateCheckBoxFill": reflect.ValueOf(&icons.IndeterminateCheckBoxFill).Elem(),
"Info": reflect.ValueOf(&icons.Info).Elem(),
"InfoFill": reflect.ValueOf(&icons.InfoFill).Elem(),
"InfoI": reflect.ValueOf(&icons.InfoI).Elem(),
"InfoIFill": reflect.ValueOf(&icons.InfoIFill).Elem(),
"Infrared": reflect.ValueOf(&icons.Infrared).Elem(),
"InfraredFill": reflect.ValueOf(&icons.InfraredFill).Elem(),
"InkEraser": reflect.ValueOf(&icons.InkEraser).Elem(),
"InkEraserFill": reflect.ValueOf(&icons.InkEraserFill).Elem(),
"InkEraserOff": reflect.ValueOf(&icons.InkEraserOff).Elem(),
"InkEraserOffFill": reflect.ValueOf(&icons.InkEraserOffFill).Elem(),
"InkHighlighter": reflect.ValueOf(&icons.InkHighlighter).Elem(),
"InkHighlighterFill": reflect.ValueOf(&icons.InkHighlighterFill).Elem(),
"InkMarker": reflect.ValueOf(&icons.InkMarker).Elem(),
"InkMarkerFill": reflect.ValueOf(&icons.InkMarkerFill).Elem(),
"InkPen": reflect.ValueOf(&icons.InkPen).Elem(),
"InkPenFill": reflect.ValueOf(&icons.InkPenFill).Elem(),
"Input": reflect.ValueOf(&icons.Input).Elem(),
"InputCircle": reflect.ValueOf(&icons.InputCircle).Elem(),
"InputCircleFill": reflect.ValueOf(&icons.InputCircleFill).Elem(),
"InputFill": reflect.ValueOf(&icons.InputFill).Elem(),
"InsertChart": reflect.ValueOf(&icons.InsertChart).Elem(),
"InsertChartFill": reflect.ValueOf(&icons.InsertChartFill).Elem(),
"InsertPageBreak": reflect.ValueOf(&icons.InsertPageBreak).Elem(),
"InsertPageBreakFill": reflect.ValueOf(&icons.InsertPageBreakFill).Elem(),
"InsertText": reflect.ValueOf(&icons.InsertText).Elem(),
"InsertTextFill": reflect.ValueOf(&icons.InsertTextFill).Elem(),
"InstallDesktop": reflect.ValueOf(&icons.InstallDesktop).Elem(),
"InstallDesktopFill": reflect.ValueOf(&icons.InstallDesktopFill).Elem(),
"InstallMobile": reflect.ValueOf(&icons.InstallMobile).Elem(),
"InstallMobileFill": reflect.ValueOf(&icons.InstallMobileFill).Elem(),
"InstantMix": reflect.ValueOf(&icons.InstantMix).Elem(),
"InstantMixFill": reflect.ValueOf(&icons.InstantMixFill).Elem(),
"IntegrationInstructions": reflect.ValueOf(&icons.IntegrationInstructions).Elem(),
"IntegrationInstructionsFill": reflect.ValueOf(&icons.IntegrationInstructionsFill).Elem(),
"InteractiveSpace": reflect.ValueOf(&icons.InteractiveSpace).Elem(),
"InteractiveSpaceFill": reflect.ValueOf(&icons.InteractiveSpaceFill).Elem(),
"Interests": reflect.ValueOf(&icons.Interests).Elem(),
"InterestsFill": reflect.ValueOf(&icons.InterestsFill).Elem(),
"Inventory": reflect.ValueOf(&icons.Inventory).Elem(),
"Inventory2": reflect.ValueOf(&icons.Inventory2).Elem(),
"Inventory2Fill": reflect.ValueOf(&icons.Inventory2Fill).Elem(),
"InventoryFill": reflect.ValueOf(&icons.InventoryFill).Elem(),
"InvertColors": reflect.ValueOf(&icons.InvertColors).Elem(),
"InvertColorsFill": reflect.ValueOf(&icons.InvertColorsFill).Elem(),
"InvertColorsOff": reflect.ValueOf(&icons.InvertColorsOff).Elem(),
"InvertColorsOffFill": reflect.ValueOf(&icons.InvertColorsOffFill).Elem(),
"IosShare": reflect.ValueOf(&icons.IosShare).Elem(),
"IosShareFill": reflect.ValueOf(&icons.IosShareFill).Elem(),
"Javascript": reflect.ValueOf(&icons.Javascript).Elem(),
"JavascriptFill": reflect.ValueOf(&icons.JavascriptFill).Elem(),
"Join": reflect.ValueOf(&icons.Join).Elem(),
"JoinFill": reflect.ValueOf(&icons.JoinFill).Elem(),
"JoinInner": reflect.ValueOf(&icons.JoinInner).Elem(),
"JoinInnerFill": reflect.ValueOf(&icons.JoinInnerFill).Elem(),
"JoinLeft": reflect.ValueOf(&icons.JoinLeft).Elem(),
"JoinLeftFill": reflect.ValueOf(&icons.JoinLeftFill).Elem(),
"JoinRight": reflect.ValueOf(&icons.JoinRight).Elem(),
"JoinRightFill": reflect.ValueOf(&icons.JoinRightFill).Elem(),
"Joystick": reflect.ValueOf(&icons.Joystick).Elem(),
"JoystickFill": reflect.ValueOf(&icons.JoystickFill).Elem(),
"Json": reflect.ValueOf(&icons.Json).Elem(),
"JumpToElement": reflect.ValueOf(&icons.JumpToElement).Elem(),
"JumpToElementFill": reflect.ValueOf(&icons.JumpToElementFill).Elem(),
"Key": reflect.ValueOf(&icons.Key).Elem(),
"KeyFill": reflect.ValueOf(&icons.KeyFill).Elem(),
"KeyOff": reflect.ValueOf(&icons.KeyOff).Elem(),
"KeyOffFill": reflect.ValueOf(&icons.KeyOffFill).Elem(),
"KeyVisualizer": reflect.ValueOf(&icons.KeyVisualizer).Elem(),
"KeyVisualizerFill": reflect.ValueOf(&icons.KeyVisualizerFill).Elem(),
"Keyboard": reflect.ValueOf(&icons.Keyboard).Elem(),
"KeyboardAlt": reflect.ValueOf(&icons.KeyboardAlt).Elem(),
"KeyboardAltFill": reflect.ValueOf(&icons.KeyboardAltFill).Elem(),
"KeyboardArrowDown": reflect.ValueOf(&icons.KeyboardArrowDown).Elem(),
"KeyboardArrowDownFill": reflect.ValueOf(&icons.KeyboardArrowDownFill).Elem(),
"KeyboardArrowLeft": reflect.ValueOf(&icons.KeyboardArrowLeft).Elem(),
"KeyboardArrowLeftFill": reflect.ValueOf(&icons.KeyboardArrowLeftFill).Elem(),
"KeyboardArrowRight": reflect.ValueOf(&icons.KeyboardArrowRight).Elem(),
"KeyboardArrowRightFill": reflect.ValueOf(&icons.KeyboardArrowRightFill).Elem(),
"KeyboardArrowUp": reflect.ValueOf(&icons.KeyboardArrowUp).Elem(),
"KeyboardArrowUpFill": reflect.ValueOf(&icons.KeyboardArrowUpFill).Elem(),
"KeyboardBackspace": reflect.ValueOf(&icons.KeyboardBackspace).Elem(),
"KeyboardBackspaceFill": reflect.ValueOf(&icons.KeyboardBackspaceFill).Elem(),
"KeyboardCapslock": reflect.ValueOf(&icons.KeyboardCapslock).Elem(),
"KeyboardCapslockBadge": reflect.ValueOf(&icons.KeyboardCapslockBadge).Elem(),
"KeyboardCapslockBadgeFill": reflect.ValueOf(&icons.KeyboardCapslockBadgeFill).Elem(),
"KeyboardCapslockFill": reflect.ValueOf(&icons.KeyboardCapslockFill).Elem(),
"KeyboardCommandKey": reflect.ValueOf(&icons.KeyboardCommandKey).Elem(),
"KeyboardCommandKeyFill": reflect.ValueOf(&icons.KeyboardCommandKeyFill).Elem(),
"KeyboardControlKey": reflect.ValueOf(&icons.KeyboardControlKey).Elem(),
"KeyboardControlKeyFill": reflect.ValueOf(&icons.KeyboardControlKeyFill).Elem(),
"KeyboardDoubleArrowDown": reflect.ValueOf(&icons.KeyboardDoubleArrowDown).Elem(),
"KeyboardDoubleArrowDownFill": reflect.ValueOf(&icons.KeyboardDoubleArrowDownFill).Elem(),
"KeyboardDoubleArrowLeft": reflect.ValueOf(&icons.KeyboardDoubleArrowLeft).Elem(),
"KeyboardDoubleArrowLeftFill": reflect.ValueOf(&icons.KeyboardDoubleArrowLeftFill).Elem(),
"KeyboardDoubleArrowRight": reflect.ValueOf(&icons.KeyboardDoubleArrowRight).Elem(),
"KeyboardDoubleArrowRightFill": reflect.ValueOf(&icons.KeyboardDoubleArrowRightFill).Elem(),
"KeyboardDoubleArrowUp": reflect.ValueOf(&icons.KeyboardDoubleArrowUp).Elem(),
"KeyboardDoubleArrowUpFill": reflect.ValueOf(&icons.KeyboardDoubleArrowUpFill).Elem(),
"KeyboardExternalInput": reflect.ValueOf(&icons.KeyboardExternalInput).Elem(),
"KeyboardExternalInputFill": reflect.ValueOf(&icons.KeyboardExternalInputFill).Elem(),
"KeyboardFill": reflect.ValueOf(&icons.KeyboardFill).Elem(),
"KeyboardFull": reflect.ValueOf(&icons.KeyboardFull).Elem(),
"KeyboardFullFill": reflect.ValueOf(&icons.KeyboardFullFill).Elem(),
"KeyboardHide": reflect.ValueOf(&icons.KeyboardHide).Elem(),
"KeyboardHideFill": reflect.ValueOf(&icons.KeyboardHideFill).Elem(),
"KeyboardKeys": reflect.ValueOf(&icons.KeyboardKeys).Elem(),
"KeyboardKeysFill": reflect.ValueOf(&icons.KeyboardKeysFill).Elem(),
"KeyboardOff": reflect.ValueOf(&icons.KeyboardOff).Elem(),
"KeyboardOffFill": reflect.ValueOf(&icons.KeyboardOffFill).Elem(),
"KeyboardOnscreen": reflect.ValueOf(&icons.KeyboardOnscreen).Elem(),
"KeyboardOnscreenFill": reflect.ValueOf(&icons.KeyboardOnscreenFill).Elem(),
"KeyboardOptionKey": reflect.ValueOf(&icons.KeyboardOptionKey).Elem(),
"KeyboardOptionKeyFill": reflect.ValueOf(&icons.KeyboardOptionKeyFill).Elem(),
"KeyboardPreviousLanguage": reflect.ValueOf(&icons.KeyboardPreviousLanguage).Elem(),
"KeyboardPreviousLanguageFill": reflect.ValueOf(&icons.KeyboardPreviousLanguageFill).Elem(),
"KeyboardReturn": reflect.ValueOf(&icons.KeyboardReturn).Elem(),
"KeyboardReturnFill": reflect.ValueOf(&icons.KeyboardReturnFill).Elem(),
"KeyboardTab": reflect.ValueOf(&icons.KeyboardTab).Elem(),
"KeyboardTabFill": reflect.ValueOf(&icons.KeyboardTabFill).Elem(),
"KeyboardTabRtl": reflect.ValueOf(&icons.KeyboardTabRtl).Elem(),
"KeyboardTabRtlFill": reflect.ValueOf(&icons.KeyboardTabRtlFill).Elem(),
"KeyboardVoice": reflect.ValueOf(&icons.KeyboardVoice).Elem(),
"KeyboardVoiceFill": reflect.ValueOf(&icons.KeyboardVoiceFill).Elem(),
"Label": reflect.ValueOf(&icons.Label).Elem(),
"LabelFill": reflect.ValueOf(&icons.LabelFill).Elem(),
"LabelImportant": reflect.ValueOf(&icons.LabelImportant).Elem(),
"LabelImportantFill": reflect.ValueOf(&icons.LabelImportantFill).Elem(),
"LabelOff": reflect.ValueOf(&icons.LabelOff).Elem(),
"LabelOffFill": reflect.ValueOf(&icons.LabelOffFill).Elem(),
"Labs": reflect.ValueOf(&icons.Labs).Elem(),
"LabsFill": reflect.ValueOf(&icons.LabsFill).Elem(),
"Lan": reflect.ValueOf(&icons.Lan).Elem(),
"LanFill": reflect.ValueOf(&icons.LanFill).Elem(),
"Landscape": reflect.ValueOf(&icons.Landscape).Elem(),
"LandscapeFill": reflect.ValueOf(&icons.LandscapeFill).Elem(),
"Landslide": reflect.ValueOf(&icons.Landslide).Elem(),
"LandslideFill": reflect.ValueOf(&icons.LandslideFill).Elem(),
"Language": reflect.ValueOf(&icons.Language).Elem(),
"LanguageChineseArray": reflect.ValueOf(&icons.LanguageChineseArray).Elem(),
"LanguageChineseArrayFill": reflect.ValueOf(&icons.LanguageChineseArrayFill).Elem(),
"LanguageChineseCangjie": reflect.ValueOf(&icons.LanguageChineseCangjie).Elem(),
"LanguageChineseCangjieFill": reflect.ValueOf(&icons.LanguageChineseCangjieFill).Elem(),
"LanguageChineseDayi": reflect.ValueOf(&icons.LanguageChineseDayi).Elem(),
"LanguageChineseDayiFill": reflect.ValueOf(&icons.LanguageChineseDayiFill).Elem(),
"LanguageChinesePinyin": reflect.ValueOf(&icons.LanguageChinesePinyin).Elem(),
"LanguageChinesePinyinFill": reflect.ValueOf(&icons.LanguageChinesePinyinFill).Elem(),
"LanguageChineseQuick": reflect.ValueOf(&icons.LanguageChineseQuick).Elem(),
"LanguageChineseQuickFill": reflect.ValueOf(&icons.LanguageChineseQuickFill).Elem(),
"LanguageChineseWubi": reflect.ValueOf(&icons.LanguageChineseWubi).Elem(),
"LanguageChineseWubiFill": reflect.ValueOf(&icons.LanguageChineseWubiFill).Elem(),
"LanguageFill": reflect.ValueOf(&icons.LanguageFill).Elem(),
"LanguageFrench": reflect.ValueOf(&icons.LanguageFrench).Elem(),
"LanguageFrenchFill": reflect.ValueOf(&icons.LanguageFrenchFill).Elem(),
"LanguageGbEnglish": reflect.ValueOf(&icons.LanguageGbEnglish).Elem(),
"LanguageGbEnglishFill": reflect.ValueOf(&icons.LanguageGbEnglishFill).Elem(),
"LanguageInternational": reflect.ValueOf(&icons.LanguageInternational).Elem(),
"LanguageInternationalFill": reflect.ValueOf(&icons.LanguageInternationalFill).Elem(),
"LanguageKoreanLatin": reflect.ValueOf(&icons.LanguageKoreanLatin).Elem(),
"LanguageKoreanLatinFill": reflect.ValueOf(&icons.LanguageKoreanLatinFill).Elem(),
"LanguagePinyin": reflect.ValueOf(&icons.LanguagePinyin).Elem(),
"LanguagePinyinFill": reflect.ValueOf(&icons.LanguagePinyinFill).Elem(),
"LanguageSpanish": reflect.ValueOf(&icons.LanguageSpanish).Elem(),
"LanguageSpanishFill": reflect.ValueOf(&icons.LanguageSpanishFill).Elem(),
"LanguageUs": reflect.ValueOf(&icons.LanguageUs).Elem(),
"LanguageUsColemak": reflect.ValueOf(&icons.LanguageUsColemak).Elem(),
"LanguageUsColemakFill": reflect.ValueOf(&icons.LanguageUsColemakFill).Elem(),
"LanguageUsDvorak": reflect.ValueOf(&icons.LanguageUsDvorak).Elem(),
"LanguageUsDvorakFill": reflect.ValueOf(&icons.LanguageUsDvorakFill).Elem(),
"LanguageUsFill": reflect.ValueOf(&icons.LanguageUsFill).Elem(),
"Laps": reflect.ValueOf(&icons.Laps).Elem(),
"LapsFill": reflect.ValueOf(&icons.LapsFill).Elem(),
"LaptopChromebook": reflect.ValueOf(&icons.LaptopChromebook).Elem(),
"LaptopChromebookFill": reflect.ValueOf(&icons.LaptopChromebookFill).Elem(),
"LaptopMac": reflect.ValueOf(&icons.LaptopMac).Elem(),
"LaptopMacFill": reflect.ValueOf(&icons.LaptopMacFill).Elem(),
"LaptopWindows": reflect.ValueOf(&icons.LaptopWindows).Elem(),
"LaptopWindowsFill": reflect.ValueOf(&icons.LaptopWindowsFill).Elem(),
"LassoSelect": reflect.ValueOf(&icons.LassoSelect).Elem(),
"LassoSelectFill": reflect.ValueOf(&icons.LassoSelectFill).Elem(),
"LastPage": reflect.ValueOf(&icons.LastPage).Elem(),
"LastPageFill": reflect.ValueOf(&icons.LastPageFill).Elem(),
"Latex": reflect.ValueOf(&icons.Latex).Elem(),
"Layers": reflect.ValueOf(&icons.Layers).Elem(),
"LayersClear": reflect.ValueOf(&icons.LayersClear).Elem(),
"LayersClearFill": reflect.ValueOf(&icons.LayersClearFill).Elem(),
"LayersFill": reflect.ValueOf(&icons.LayersFill).Elem(),
"Leaderboard": reflect.ValueOf(&icons.Leaderboard).Elem(),
"LeaderboardFill": reflect.ValueOf(&icons.LeaderboardFill).Elem(),
"LeftClick": reflect.ValueOf(&icons.LeftClick).Elem(),
"LeftClickFill": reflect.ValueOf(&icons.LeftClickFill).Elem(),
"LeftPanelClose": reflect.ValueOf(&icons.LeftPanelClose).Elem(),
"LeftPanelCloseFill": reflect.ValueOf(&icons.LeftPanelCloseFill).Elem(),
"LeftPanelOpen": reflect.ValueOf(&icons.LeftPanelOpen).Elem(),
"LeftPanelOpenFill": reflect.ValueOf(&icons.LeftPanelOpenFill).Elem(),
"LegendToggle": reflect.ValueOf(&icons.LegendToggle).Elem(),
"LegendToggleFill": reflect.ValueOf(&icons.LegendToggleFill).Elem(),
"LensBlur": reflect.ValueOf(&icons.LensBlur).Elem(),
"LensBlurFill": reflect.ValueOf(&icons.LensBlurFill).Elem(),
"LetterSwitch": reflect.ValueOf(&icons.LetterSwitch).Elem(),
"LetterSwitchFill": reflect.ValueOf(&icons.LetterSwitchFill).Elem(),
"LibraryAdd": reflect.ValueOf(&icons.LibraryAdd).Elem(),
"LibraryAddCheck": reflect.ValueOf(&icons.LibraryAddCheck).Elem(),
"LibraryAddCheckFill": reflect.ValueOf(&icons.LibraryAddCheckFill).Elem(),
"LibraryAddFill": reflect.ValueOf(&icons.LibraryAddFill).Elem(),
"LibraryBooks": reflect.ValueOf(&icons.LibraryBooks).Elem(),
"LibraryBooksFill": reflect.ValueOf(&icons.LibraryBooksFill).Elem(),
"LibraryMusic": reflect.ValueOf(&icons.LibraryMusic).Elem(),
"LibraryMusicFill": reflect.ValueOf(&icons.LibraryMusicFill).Elem(),
"Light": reflect.ValueOf(&icons.Light).Elem(),
"LightFill": reflect.ValueOf(&icons.LightFill).Elem(),
"Lightbulb": reflect.ValueOf(&icons.Lightbulb).Elem(),
"LightbulbCircle": reflect.ValueOf(&icons.LightbulbCircle).Elem(),
"LightbulbCircleFill": reflect.ValueOf(&icons.LightbulbCircleFill).Elem(),
"LightbulbFill": reflect.ValueOf(&icons.LightbulbFill).Elem(),
"LineAxis": reflect.ValueOf(&icons.LineAxis).Elem(),
"LineAxisFill": reflect.ValueOf(&icons.LineAxisFill).Elem(),
"LineCurve": reflect.ValueOf(&icons.LineCurve).Elem(),
"LineCurveFill": reflect.ValueOf(&icons.LineCurveFill).Elem(),
"LineEnd": reflect.ValueOf(&icons.LineEnd).Elem(),
"LineEndArrow": reflect.ValueOf(&icons.LineEndArrow).Elem(),
"LineEndArrowFill": reflect.ValueOf(&icons.LineEndArrowFill).Elem(),
"LineEndArrowNotch": reflect.ValueOf(&icons.LineEndArrowNotch).Elem(),
"LineEndArrowNotchFill": reflect.ValueOf(&icons.LineEndArrowNotchFill).Elem(),
"LineEndCircle": reflect.ValueOf(&icons.LineEndCircle).Elem(),
"LineEndCircleFill": reflect.ValueOf(&icons.LineEndCircleFill).Elem(),
"LineEndDiamond": reflect.ValueOf(&icons.LineEndDiamond).Elem(),
"LineEndDiamondFill": reflect.ValueOf(&icons.LineEndDiamondFill).Elem(),
"LineEndFill": reflect.ValueOf(&icons.LineEndFill).Elem(),
"LineEndSquare": reflect.ValueOf(&icons.LineEndSquare).Elem(),
"LineEndSquareFill": reflect.ValueOf(&icons.LineEndSquareFill).Elem(),
"LineStart": reflect.ValueOf(&icons.LineStart).Elem(),
"LineStartArrow": reflect.ValueOf(&icons.LineStartArrow).Elem(),
"LineStartArrowFill": reflect.ValueOf(&icons.LineStartArrowFill).Elem(),
"LineStartArrowNotch": reflect.ValueOf(&icons.LineStartArrowNotch).Elem(),
"LineStartArrowNotchFill": reflect.ValueOf(&icons.LineStartArrowNotchFill).Elem(),
"LineStartCircle": reflect.ValueOf(&icons.LineStartCircle).Elem(),
"LineStartCircleFill": reflect.ValueOf(&icons.LineStartCircleFill).Elem(),
"LineStartDiamond": reflect.ValueOf(&icons.LineStartDiamond).Elem(),
"LineStartDiamondFill": reflect.ValueOf(&icons.LineStartDiamondFill).Elem(),
"LineStartFill": reflect.ValueOf(&icons.LineStartFill).Elem(),
"LineStartSquare": reflect.ValueOf(&icons.LineStartSquare).Elem(),
"LineStartSquareFill": reflect.ValueOf(&icons.LineStartSquareFill).Elem(),
"LineStyle": reflect.ValueOf(&icons.LineStyle).Elem(),
"LineStyleFill": reflect.ValueOf(&icons.LineStyleFill).Elem(),
"LineWeight": reflect.ValueOf(&icons.LineWeight).Elem(),
"LineWeightFill": reflect.ValueOf(&icons.LineWeightFill).Elem(),
"LinearScale": reflect.ValueOf(&icons.LinearScale).Elem(),
"LinearScaleFill": reflect.ValueOf(&icons.LinearScaleFill).Elem(),
"Link": reflect.ValueOf(&icons.Link).Elem(),
"LinkFill": reflect.ValueOf(&icons.LinkFill).Elem(),
"LinkOff": reflect.ValueOf(&icons.LinkOff).Elem(),
"LinkOffFill": reflect.ValueOf(&icons.LinkOffFill).Elem(),
"List": reflect.ValueOf(&icons.List).Elem(),
"ListAlt": reflect.ValueOf(&icons.ListAlt).Elem(),
"ListAltAdd": reflect.ValueOf(&icons.ListAltAdd).Elem(),
"ListAltAddFill": reflect.ValueOf(&icons.ListAltAddFill).Elem(),
"ListAltFill": reflect.ValueOf(&icons.ListAltFill).Elem(),
"ListFill": reflect.ValueOf(&icons.ListFill).Elem(),
"Lists": reflect.ValueOf(&icons.Lists).Elem(),
"ListsFill": reflect.ValueOf(&icons.ListsFill).Elem(),
"LiveHelp": reflect.ValueOf(&icons.LiveHelp).Elem(),
"LiveHelpFill": reflect.ValueOf(&icons.LiveHelpFill).Elem(),
"LiveTv": reflect.ValueOf(&icons.LiveTv).Elem(),
"LiveTvFill": reflect.ValueOf(&icons.LiveTvFill).Elem(),
"LocationAway": reflect.ValueOf(&icons.LocationAway).Elem(),
"LocationAwayFill": reflect.ValueOf(&icons.LocationAwayFill).Elem(),
"LocationChip": reflect.ValueOf(&icons.LocationChip).Elem(),
"LocationChipFill": reflect.ValueOf(&icons.LocationChipFill).Elem(),
"LocationHome": reflect.ValueOf(&icons.LocationHome).Elem(),
"LocationHomeFill": reflect.ValueOf(&icons.LocationHomeFill).Elem(),
"LocationOff": reflect.ValueOf(&icons.LocationOff).Elem(),
"LocationOffFill": reflect.ValueOf(&icons.LocationOffFill).Elem(),
"LocationOn": reflect.ValueOf(&icons.LocationOn).Elem(),
"LocationOnFill": reflect.ValueOf(&icons.LocationOnFill).Elem(),
"Lock": reflect.ValueOf(&icons.Lock).Elem(),
"LockClock": reflect.ValueOf(&icons.LockClock).Elem(),
"LockClockFill": reflect.ValueOf(&icons.LockClockFill).Elem(),
"LockFill": reflect.ValueOf(&icons.LockFill).Elem(),
"LockOpen": reflect.ValueOf(&icons.LockOpen).Elem(),
"LockOpenFill": reflect.ValueOf(&icons.LockOpenFill).Elem(),
"LockOpenRight": reflect.ValueOf(&icons.LockOpenRight).Elem(),
"LockOpenRightFill": reflect.ValueOf(&icons.LockOpenRightFill).Elem(),
"LockPerson": reflect.ValueOf(&icons.LockPerson).Elem(),
"LockPersonFill": reflect.ValueOf(&icons.LockPersonFill).Elem(),
"LockReset": reflect.ValueOf(&icons.LockReset).Elem(),
"LockResetFill": reflect.ValueOf(&icons.LockResetFill).Elem(),
"Login": reflect.ValueOf(&icons.Login).Elem(),
"LoginFill": reflect.ValueOf(&icons.LoginFill).Elem(),
"LogoDev": reflect.ValueOf(&icons.LogoDev).Elem(),
"LogoDevFill": reflect.ValueOf(&icons.LogoDevFill).Elem(),
"Logout": reflect.ValueOf(&icons.Logout).Elem(),
"LogoutFill": reflect.ValueOf(&icons.LogoutFill).Elem(),
"Loupe": reflect.ValueOf(&icons.Loupe).Elem(),
"LoupeFill": reflect.ValueOf(&icons.LoupeFill).Elem(),
"LowDensity": reflect.ValueOf(&icons.LowDensity).Elem(),
"LowDensityFill": reflect.ValueOf(&icons.LowDensityFill).Elem(),
"LowPriority": reflect.ValueOf(&icons.LowPriority).Elem(),
"LowPriorityFill": reflect.ValueOf(&icons.LowPriorityFill).Elem(),
"Loyalty": reflect.ValueOf(&icons.Loyalty).Elem(),
"LoyaltyFill": reflect.ValueOf(&icons.LoyaltyFill).Elem(),
"Lyrics": reflect.ValueOf(&icons.Lyrics).Elem(),
"LyricsFill": reflect.ValueOf(&icons.LyricsFill).Elem(),
"MacroAuto": reflect.ValueOf(&icons.MacroAuto).Elem(),
"MacroAutoFill": reflect.ValueOf(&icons.MacroAutoFill).Elem(),
"MacroOff": reflect.ValueOf(&icons.MacroOff).Elem(),
"MacroOffFill": reflect.ValueOf(&icons.MacroOffFill).Elem(),
"MagnificationLarge": reflect.ValueOf(&icons.MagnificationLarge).Elem(),
"MagnificationLargeFill": reflect.ValueOf(&icons.MagnificationLargeFill).Elem(),
"MagnificationSmall": reflect.ValueOf(&icons.MagnificationSmall).Elem(),
"MagnificationSmallFill": reflect.ValueOf(&icons.MagnificationSmallFill).Elem(),
"MagnifyDocked": reflect.ValueOf(&icons.MagnifyDocked).Elem(),
"MagnifyDockedFill": reflect.ValueOf(&icons.MagnifyDockedFill).Elem(),
"MagnifyFullscreen": reflect.ValueOf(&icons.MagnifyFullscreen).Elem(),
"MagnifyFullscreenFill": reflect.ValueOf(&icons.MagnifyFullscreenFill).Elem(),
"Mail": reflect.ValueOf(&icons.Mail).Elem(),
"MailFill": reflect.ValueOf(&icons.MailFill).Elem(),
"MailLock": reflect.ValueOf(&icons.MailLock).Elem(),
"MailLockFill": reflect.ValueOf(&icons.MailLockFill).Elem(),
"Make": reflect.ValueOf(&icons.Make).Elem(),
"Makefile": reflect.ValueOf(&icons.Makefile).Elem(),
"Male": reflect.ValueOf(&icons.Male).Elem(),
"MaleFill": reflect.ValueOf(&icons.MaleFill).Elem(),
"Man": reflect.ValueOf(&icons.Man).Elem(),
"Man2": reflect.ValueOf(&icons.Man2).Elem(),
"Man2Fill": reflect.ValueOf(&icons.Man2Fill).Elem(),
"Man3": reflect.ValueOf(&icons.Man3).Elem(),
"Man3Fill": reflect.ValueOf(&icons.Man3Fill).Elem(),
"Man4": reflect.ValueOf(&icons.Man4).Elem(),
"Man4Fill": reflect.ValueOf(&icons.Man4Fill).Elem(),
"ManFill": reflect.ValueOf(&icons.ManFill).Elem(),
"ManageAccounts": reflect.ValueOf(&icons.ManageAccounts).Elem(),
"ManageAccountsFill": reflect.ValueOf(&icons.ManageAccountsFill).Elem(),
"ManageHistory": reflect.ValueOf(&icons.ManageHistory).Elem(),
"ManageHistoryFill": reflect.ValueOf(&icons.ManageHistoryFill).Elem(),
"ManageSearch": reflect.ValueOf(&icons.ManageSearch).Elem(),
"ManageSearchFill": reflect.ValueOf(&icons.ManageSearchFill).Elem(),
"Manga": reflect.ValueOf(&icons.Manga).Elem(),
"MangaFill": reflect.ValueOf(&icons.MangaFill).Elem(),
"Map": reflect.ValueOf(&icons.Map).Elem(),
"MapFill": reflect.ValueOf(&icons.MapFill).Elem(),
"Margin": reflect.ValueOf(&icons.Margin).Elem(),
"MarginFill": reflect.ValueOf(&icons.MarginFill).Elem(),
"MarkAsUnread": reflect.ValueOf(&icons.MarkAsUnread).Elem(),
"MarkAsUnreadFill": reflect.ValueOf(&icons.MarkAsUnreadFill).Elem(),
"MarkChatRead": reflect.ValueOf(&icons.MarkChatRead).Elem(),
"MarkChatReadFill": reflect.ValueOf(&icons.MarkChatReadFill).Elem(),
"MarkChatUnread": reflect.ValueOf(&icons.MarkChatUnread).Elem(),
"MarkChatUnreadFill": reflect.ValueOf(&icons.MarkChatUnreadFill).Elem(),
"MarkEmailRead": reflect.ValueOf(&icons.MarkEmailRead).Elem(),
"MarkEmailReadFill": reflect.ValueOf(&icons.MarkEmailReadFill).Elem(),
"MarkEmailUnread": reflect.ValueOf(&icons.MarkEmailUnread).Elem(),
"MarkEmailUnreadFill": reflect.ValueOf(&icons.MarkEmailUnreadFill).Elem(),
"MarkUnreadChatAlt": reflect.ValueOf(&icons.MarkUnreadChatAlt).Elem(),
"MarkUnreadChatAltFill": reflect.ValueOf(&icons.MarkUnreadChatAltFill).Elem(),
"MarkunreadMailbox": reflect.ValueOf(&icons.MarkunreadMailbox).Elem(),
"MarkunreadMailboxFill": reflect.ValueOf(&icons.MarkunreadMailboxFill).Elem(),
"MaskedTransitions": reflect.ValueOf(&icons.MaskedTransitions).Elem(),
"MaskedTransitionsFill": reflect.ValueOf(&icons.MaskedTransitionsFill).Elem(),
"MatchCase": reflect.ValueOf(&icons.MatchCase).Elem(),
"MatchCaseFill": reflect.ValueOf(&icons.MatchCaseFill).Elem(),
"MatchWord": reflect.ValueOf(&icons.MatchWord).Elem(),
"MatchWordFill": reflect.ValueOf(&icons.MatchWordFill).Elem(),
"Maximize": reflect.ValueOf(&icons.Maximize).Elem(),
"MaximizeFill": reflect.ValueOf(&icons.MaximizeFill).Elem(),
"MeasuringTape": reflect.ValueOf(&icons.MeasuringTape).Elem(),
"MeasuringTapeFill": reflect.ValueOf(&icons.MeasuringTapeFill).Elem(),
"MediaBluetoothOff": reflect.ValueOf(&icons.MediaBluetoothOff).Elem(),
"MediaBluetoothOffFill": reflect.ValueOf(&icons.MediaBluetoothOffFill).Elem(),
"MediaBluetoothOn": reflect.ValueOf(&icons.MediaBluetoothOn).Elem(),
"MediaBluetoothOnFill": reflect.ValueOf(&icons.MediaBluetoothOnFill).Elem(),
"MediaLink": reflect.ValueOf(&icons.MediaLink).Elem(),
"MediaLinkFill": reflect.ValueOf(&icons.MediaLinkFill).Elem(),
"Memory": reflect.ValueOf(&icons.Memory).Elem(),
"MemoryAlt": reflect.ValueOf(&icons.MemoryAlt).Elem(),
"MemoryAltFill": reflect.ValueOf(&icons.MemoryAltFill).Elem(),
"MemoryFill": reflect.ValueOf(&icons.MemoryFill).Elem(),
"Menu": reflect.ValueOf(&icons.Menu).Elem(),
"MenuBook": reflect.ValueOf(&icons.MenuBook).Elem(),
"MenuBookFill": reflect.ValueOf(&icons.MenuBookFill).Elem(),
"MenuFill": reflect.ValueOf(&icons.MenuFill).Elem(),
"MenuOpen": reflect.ValueOf(&icons.MenuOpen).Elem(),
"MenuOpenFill": reflect.ValueOf(&icons.MenuOpenFill).Elem(),
"Merge": reflect.ValueOf(&icons.Merge).Elem(),
"MergeFill": reflect.ValueOf(&icons.MergeFill).Elem(),
"MergeType": reflect.ValueOf(&icons.MergeType).Elem(),
"MergeTypeFill": reflect.ValueOf(&icons.MergeTypeFill).Elem(),
"Method": reflect.ValueOf(&icons.Method).Elem(),
"Mic": reflect.ValueOf(&icons.Mic).Elem(),
"MicDouble": reflect.ValueOf(&icons.MicDouble).Elem(),
"MicDoubleFill": reflect.ValueOf(&icons.MicDoubleFill).Elem(),
"MicExternalOff": reflect.ValueOf(&icons.MicExternalOff).Elem(),
"MicExternalOffFill": reflect.ValueOf(&icons.MicExternalOffFill).Elem(),
"MicExternalOn": reflect.ValueOf(&icons.MicExternalOn).Elem(),
"MicExternalOnFill": reflect.ValueOf(&icons.MicExternalOnFill).Elem(),
"MicFill": reflect.ValueOf(&icons.MicFill).Elem(),
"MicOff": reflect.ValueOf(&icons.MicOff).Elem(),
"MicOffFill": reflect.ValueOf(&icons.MicOffFill).Elem(),
"Minimize": reflect.ValueOf(&icons.Minimize).Elem(),
"MinimizeFill": reflect.ValueOf(&icons.MinimizeFill).Elem(),
"MissedVideoCall": reflect.ValueOf(&icons.MissedVideoCall).Elem(),
"MissedVideoCallFill": reflect.ValueOf(&icons.MissedVideoCallFill).Elem(),
"Mms": reflect.ValueOf(&icons.Mms).Elem(),
"MmsFill": reflect.ValueOf(&icons.MmsFill).Elem(),
"MobileFriendly": reflect.ValueOf(&icons.MobileFriendly).Elem(),
"MobileFriendlyFill": reflect.ValueOf(&icons.MobileFriendlyFill).Elem(),
"MobileOff": reflect.ValueOf(&icons.MobileOff).Elem(),
"MobileOffFill": reflect.ValueOf(&icons.MobileOffFill).Elem(),
"MobileScreenShare": reflect.ValueOf(&icons.MobileScreenShare).Elem(),
"MobileScreenShareFill": reflect.ValueOf(&icons.MobileScreenShareFill).Elem(),
"MobiledataOff": reflect.ValueOf(&icons.MobiledataOff).Elem(),
"MobiledataOffFill": reflect.ValueOf(&icons.MobiledataOffFill).Elem(),
"ModeCool": reflect.ValueOf(&icons.ModeCool).Elem(),
"ModeCoolFill": reflect.ValueOf(&icons.ModeCoolFill).Elem(),
"ModeCoolOff": reflect.ValueOf(&icons.ModeCoolOff).Elem(),
"ModeCoolOffFill": reflect.ValueOf(&icons.ModeCoolOffFill).Elem(),
"ModeFan": reflect.ValueOf(&icons.ModeFan).Elem(),
"ModeFanFill": reflect.ValueOf(&icons.ModeFanFill).Elem(),
"ModeFanOff": reflect.ValueOf(&icons.ModeFanOff).Elem(),
"ModeFanOffFill": reflect.ValueOf(&icons.ModeFanOffFill).Elem(),
"ModeHeat": reflect.ValueOf(&icons.ModeHeat).Elem(),
"ModeHeatCool": reflect.ValueOf(&icons.ModeHeatCool).Elem(),
"ModeHeatCoolFill": reflect.ValueOf(&icons.ModeHeatCoolFill).Elem(),
"ModeHeatFill": reflect.ValueOf(&icons.ModeHeatFill).Elem(),
"ModeHeatOff": reflect.ValueOf(&icons.ModeHeatOff).Elem(),
"ModeHeatOffFill": reflect.ValueOf(&icons.ModeHeatOffFill).Elem(),
"ModeOffOn": reflect.ValueOf(&icons.ModeOffOn).Elem(),
"ModeOffOnFill": reflect.ValueOf(&icons.ModeOffOnFill).Elem(),
"ModeStandby": reflect.ValueOf(&icons.ModeStandby).Elem(),
"ModeStandbyFill": reflect.ValueOf(&icons.ModeStandbyFill).Elem(),
"ModelTraining": reflect.ValueOf(&icons.ModelTraining).Elem(),
"ModelTrainingFill": reflect.ValueOf(&icons.ModelTrainingFill).Elem(),
"MonetizationOn": reflect.ValueOf(&icons.MonetizationOn).Elem(),
"MonetizationOnFill": reflect.ValueOf(&icons.MonetizationOnFill).Elem(),
"Money": reflect.ValueOf(&icons.Money).Elem(),
"MoneyFill": reflect.ValueOf(&icons.MoneyFill).Elem(),
"MoneyOff": reflect.ValueOf(&icons.MoneyOff).Elem(),
"MoneyOffFill": reflect.ValueOf(&icons.MoneyOffFill).Elem(),
"Monitor": reflect.ValueOf(&icons.Monitor).Elem(),
"MonitorFill": reflect.ValueOf(&icons.MonitorFill).Elem(),
"Monitoring": reflect.ValueOf(&icons.Monitoring).Elem(),
"MonitoringFill": reflect.ValueOf(&icons.MonitoringFill).Elem(),
"MonochromePhotos": reflect.ValueOf(&icons.MonochromePhotos).Elem(),
"MonochromePhotosFill": reflect.ValueOf(&icons.MonochromePhotosFill).Elem(),
"Mood": reflect.ValueOf(&icons.Mood).Elem(),
"MoodBad": reflect.ValueOf(&icons.MoodBad).Elem(),
"MoodBadFill": reflect.ValueOf(&icons.MoodBadFill).Elem(),
"MoodFill": reflect.ValueOf(&icons.MoodFill).Elem(),
"More": reflect.ValueOf(&icons.More).Elem(),
"MoreDown": reflect.ValueOf(&icons.MoreDown).Elem(),
"MoreDownFill": reflect.ValueOf(&icons.MoreDownFill).Elem(),
"MoreFill": reflect.ValueOf(&icons.MoreFill).Elem(),
"MoreHoriz": reflect.ValueOf(&icons.MoreHoriz).Elem(),
"MoreHorizFill": reflect.ValueOf(&icons.MoreHorizFill).Elem(),
"MoreTime": reflect.ValueOf(&icons.MoreTime).Elem(),
"MoreTimeFill": reflect.ValueOf(&icons.MoreTimeFill).Elem(),
"MoreUp": reflect.ValueOf(&icons.MoreUp).Elem(),
"MoreUpFill": reflect.ValueOf(&icons.MoreUpFill).Elem(),
"MoreVert": reflect.ValueOf(&icons.MoreVert).Elem(),
"MoreVertFill": reflect.ValueOf(&icons.MoreVertFill).Elem(),
"Mouse": reflect.ValueOf(&icons.Mouse).Elem(),
"MouseFill": reflect.ValueOf(&icons.MouseFill).Elem(),
"Move": reflect.ValueOf(&icons.Move).Elem(),
"MoveDown": reflect.ValueOf(&icons.MoveDown).Elem(),
"MoveDownFill": reflect.ValueOf(&icons.MoveDownFill).Elem(),
"MoveFill": reflect.ValueOf(&icons.MoveFill).Elem(),
"MoveGroup": reflect.ValueOf(&icons.MoveGroup).Elem(),
"MoveGroupFill": reflect.ValueOf(&icons.MoveGroupFill).Elem(),
"MoveItem": reflect.ValueOf(&icons.MoveItem).Elem(),
"MoveItemFill": reflect.ValueOf(&icons.MoveItemFill).Elem(),
"MoveLocation": reflect.ValueOf(&icons.MoveLocation).Elem(),
"MoveLocationFill": reflect.ValueOf(&icons.MoveLocationFill).Elem(),
"MoveSelectionDown": reflect.ValueOf(&icons.MoveSelectionDown).Elem(),
"MoveSelectionDownFill": reflect.ValueOf(&icons.MoveSelectionDownFill).Elem(),
"MoveSelectionLeft": reflect.ValueOf(&icons.MoveSelectionLeft).Elem(),
"MoveSelectionLeftFill": reflect.ValueOf(&icons.MoveSelectionLeftFill).Elem(),
"MoveSelectionRight": reflect.ValueOf(&icons.MoveSelectionRight).Elem(),
"MoveSelectionRightFill": reflect.ValueOf(&icons.MoveSelectionRightFill).Elem(),
"MoveSelectionUp": reflect.ValueOf(&icons.MoveSelectionUp).Elem(),
"MoveSelectionUpFill": reflect.ValueOf(&icons.MoveSelectionUpFill).Elem(),
"MoveToInbox": reflect.ValueOf(&icons.MoveToInbox).Elem(),
"MoveToInboxFill": reflect.ValueOf(&icons.MoveToInboxFill).Elem(),
"MoveUp": reflect.ValueOf(&icons.MoveUp).Elem(),
"MoveUpFill": reflect.ValueOf(&icons.MoveUpFill).Elem(),
"MovedLocation": reflect.ValueOf(&icons.MovedLocation).Elem(),
"MovedLocationFill": reflect.ValueOf(&icons.MovedLocationFill).Elem(),
"Movie": reflect.ValueOf(&icons.Movie).Elem(),
"MovieEdit": reflect.ValueOf(&icons.MovieEdit).Elem(),
"MovieEditFill": reflect.ValueOf(&icons.MovieEditFill).Elem(),
"MovieFill": reflect.ValueOf(&icons.MovieFill).Elem(),
"MovieInfo": reflect.ValueOf(&icons.MovieInfo).Elem(),
"MovieInfoFill": reflect.ValueOf(&icons.MovieInfoFill).Elem(),
"Moving": reflect.ValueOf(&icons.Moving).Elem(),
"MovingFill": reflect.ValueOf(&icons.MovingFill).Elem(),
"MultilineChart": reflect.ValueOf(&icons.MultilineChart).Elem(),
"MultilineChartFill": reflect.ValueOf(&icons.MultilineChartFill).Elem(),
"MultipleStop": reflect.ValueOf(&icons.MultipleStop).Elem(),
"MultipleStopFill": reflect.ValueOf(&icons.MultipleStopFill).Elem(),
"MusicNote": reflect.ValueOf(&icons.MusicNote).Elem(),
"MusicNoteFill": reflect.ValueOf(&icons.MusicNoteFill).Elem(),
"MusicOff": reflect.ValueOf(&icons.MusicOff).Elem(),
"MusicOffFill": reflect.ValueOf(&icons.MusicOffFill).Elem(),
"MusicVideo": reflect.ValueOf(&icons.MusicVideo).Elem(),
"MusicVideoFill": reflect.ValueOf(&icons.MusicVideoFill).Elem(),
"MyLocation": reflect.ValueOf(&icons.MyLocation).Elem(),
"MyLocationFill": reflect.ValueOf(&icons.MyLocationFill).Elem(),
"Mystery": reflect.ValueOf(&icons.Mystery).Elem(),
"MysteryFill": reflect.ValueOf(&icons.MysteryFill).Elem(),
"Nature": reflect.ValueOf(&icons.Nature).Elem(),
"NatureFill": reflect.ValueOf(&icons.NatureFill).Elem(),
"NavigateBefore": reflect.ValueOf(&icons.NavigateBefore).Elem(),
"NavigateBeforeFill": reflect.ValueOf(&icons.NavigateBeforeFill).Elem(),
"NavigateNext": reflect.ValueOf(&icons.NavigateNext).Elem(),
"NavigateNextFill": reflect.ValueOf(&icons.NavigateNextFill).Elem(),
"Navigation": reflect.ValueOf(&icons.Navigation).Elem(),
"NavigationFill": reflect.ValueOf(&icons.NavigationFill).Elem(),
"NetworkCell": reflect.ValueOf(&icons.NetworkCell).Elem(),
"NetworkCellFill": reflect.ValueOf(&icons.NetworkCellFill).Elem(),
"NetworkCheck": reflect.ValueOf(&icons.NetworkCheck).Elem(),
"NetworkCheckFill": reflect.ValueOf(&icons.NetworkCheckFill).Elem(),
"NetworkLocked": reflect.ValueOf(&icons.NetworkLocked).Elem(),
"NetworkLockedFill": reflect.ValueOf(&icons.NetworkLockedFill).Elem(),
"NetworkManage": reflect.ValueOf(&icons.NetworkManage).Elem(),
"NetworkManageFill": reflect.ValueOf(&icons.NetworkManageFill).Elem(),
"NetworkPing": reflect.ValueOf(&icons.NetworkPing).Elem(),
"NetworkPingFill": reflect.ValueOf(&icons.NetworkPingFill).Elem(),
"NetworkWifi": reflect.ValueOf(&icons.NetworkWifi).Elem(),
"NetworkWifi1Bar": reflect.ValueOf(&icons.NetworkWifi1Bar).Elem(),
"NetworkWifi1BarFill": reflect.ValueOf(&icons.NetworkWifi1BarFill).Elem(),
"NetworkWifi2Bar": reflect.ValueOf(&icons.NetworkWifi2Bar).Elem(),
"NetworkWifi2BarFill": reflect.ValueOf(&icons.NetworkWifi2BarFill).Elem(),
"NetworkWifi3Bar": reflect.ValueOf(&icons.NetworkWifi3Bar).Elem(),
"NetworkWifi3BarFill": reflect.ValueOf(&icons.NetworkWifi3BarFill).Elem(),
"NetworkWifiFill": reflect.ValueOf(&icons.NetworkWifiFill).Elem(),
"NewLabel": reflect.ValueOf(&icons.NewLabel).Elem(),
"NewLabelFill": reflect.ValueOf(&icons.NewLabelFill).Elem(),
"NewReleases": reflect.ValueOf(&icons.NewReleases).Elem(),
"NewReleasesFill": reflect.ValueOf(&icons.NewReleasesFill).Elem(),
"NewWindow": reflect.ValueOf(&icons.NewWindow).Elem(),
"NewWindowFill": reflect.ValueOf(&icons.NewWindowFill).Elem(),
"News": reflect.ValueOf(&icons.News).Elem(),
"NewsFill": reflect.ValueOf(&icons.NewsFill).Elem(),
"Newsmode": reflect.ValueOf(&icons.Newsmode).Elem(),
"NewsmodeFill": reflect.ValueOf(&icons.NewsmodeFill).Elem(),
"Newspaper": reflect.ValueOf(&icons.Newspaper).Elem(),
"NewspaperFill": reflect.ValueOf(&icons.NewspaperFill).Elem(),
"NextPlan": reflect.ValueOf(&icons.NextPlan).Elem(),
"NextPlanFill": reflect.ValueOf(&icons.NextPlanFill).Elem(),
"NextWeek": reflect.ValueOf(&icons.NextWeek).Elem(),
"NextWeekFill": reflect.ValueOf(&icons.NextWeekFill).Elem(),
"Nfc": reflect.ValueOf(&icons.Nfc).Elem(),
"NfcFill": reflect.ValueOf(&icons.NfcFill).Elem(),
"NoAccounts": reflect.ValueOf(&icons.NoAccounts).Elem(),
"NoAccountsFill": reflect.ValueOf(&icons.NoAccountsFill).Elem(),
"NoAdultContent": reflect.ValueOf(&icons.NoAdultContent).Elem(),
"NoAdultContentFill": reflect.ValueOf(&icons.NoAdultContentFill).Elem(),
"NoCrash": reflect.ValueOf(&icons.NoCrash).Elem(),
"NoCrashFill": reflect.ValueOf(&icons.NoCrashFill).Elem(),
"NoEncryption": reflect.ValueOf(&icons.NoEncryption).Elem(),
"NoEncryptionFill": reflect.ValueOf(&icons.NoEncryptionFill).Elem(),
"NoFlash": reflect.ValueOf(&icons.NoFlash).Elem(),
"NoFlashFill": reflect.ValueOf(&icons.NoFlashFill).Elem(),
"NoSim": reflect.ValueOf(&icons.NoSim).Elem(),
"NoSimFill": reflect.ValueOf(&icons.NoSimFill).Elem(),
"NoSound": reflect.ValueOf(&icons.NoSound).Elem(),
"NoSoundFill": reflect.ValueOf(&icons.NoSoundFill).Elem(),
"NoTransfer": reflect.ValueOf(&icons.NoTransfer).Elem(),
"NoTransferFill": reflect.ValueOf(&icons.NoTransferFill).Elem(),
"None": reflect.ValueOf(&icons.None).Elem(),
"North": reflect.ValueOf(&icons.North).Elem(),
"NorthEast": reflect.ValueOf(&icons.NorthEast).Elem(),
"NorthEastFill": reflect.ValueOf(&icons.NorthEastFill).Elem(),
"NorthFill": reflect.ValueOf(&icons.NorthFill).Elem(),
"NorthWest": reflect.ValueOf(&icons.NorthWest).Elem(),
"NorthWestFill": reflect.ValueOf(&icons.NorthWestFill).Elem(),
"NotStarted": reflect.ValueOf(&icons.NotStarted).Elem(),
"NotStartedFill": reflect.ValueOf(&icons.NotStartedFill).Elem(),
"Note": reflect.ValueOf(&icons.Note).Elem(),
"NoteAdd": reflect.ValueOf(&icons.NoteAdd).Elem(),
"NoteAddFill": reflect.ValueOf(&icons.NoteAddFill).Elem(),
"NoteAlt": reflect.ValueOf(&icons.NoteAlt).Elem(),
"NoteAltFill": reflect.ValueOf(&icons.NoteAltFill).Elem(),
"NoteFill": reflect.ValueOf(&icons.NoteFill).Elem(),
"Notes": reflect.ValueOf(&icons.Notes).Elem(),
"NotesFill": reflect.ValueOf(&icons.NotesFill).Elem(),
"NotificationAdd": reflect.ValueOf(&icons.NotificationAdd).Elem(),
"NotificationAddFill": reflect.ValueOf(&icons.NotificationAddFill).Elem(),
"NotificationImportant": reflect.ValueOf(&icons.NotificationImportant).Elem(),
"NotificationImportantFill": reflect.ValueOf(&icons.NotificationImportantFill).Elem(),
"NotificationMultiple": reflect.ValueOf(&icons.NotificationMultiple).Elem(),
"NotificationMultipleFill": reflect.ValueOf(&icons.NotificationMultipleFill).Elem(),
"Notifications": reflect.ValueOf(&icons.Notifications).Elem(),
"NotificationsActive": reflect.ValueOf(&icons.NotificationsActive).Elem(),
"NotificationsActiveFill": reflect.ValueOf(&icons.NotificationsActiveFill).Elem(),
"NotificationsFill": reflect.ValueOf(&icons.NotificationsFill).Elem(),
"NotificationsOff": reflect.ValueOf(&icons.NotificationsOff).Elem(),
"NotificationsOffFill": reflect.ValueOf(&icons.NotificationsOffFill).Elem(),
"NotificationsPaused": reflect.ValueOf(&icons.NotificationsPaused).Elem(),
"NotificationsPausedFill": reflect.ValueOf(&icons.NotificationsPausedFill).Elem(),
"Numbers": reflect.ValueOf(&icons.Numbers).Elem(),
"NumbersFill": reflect.ValueOf(&icons.NumbersFill).Elem(),
"OfflineBolt": reflect.ValueOf(&icons.OfflineBolt).Elem(),
"OfflineBoltFill": reflect.ValueOf(&icons.OfflineBoltFill).Elem(),
"OfflinePin": reflect.ValueOf(&icons.OfflinePin).Elem(),
"OfflinePinFill": reflect.ValueOf(&icons.OfflinePinFill).Elem(),
"OfflineShare": reflect.ValueOf(&icons.OfflineShare).Elem(),
"OfflineShareFill": reflect.ValueOf(&icons.OfflineShareFill).Elem(),
"OnDeviceTraining": reflect.ValueOf(&icons.OnDeviceTraining).Elem(),
"OnDeviceTrainingFill": reflect.ValueOf(&icons.OnDeviceTrainingFill).Elem(),
"OnlinePrediction": reflect.ValueOf(&icons.OnlinePrediction).Elem(),
"OnlinePredictionFill": reflect.ValueOf(&icons.OnlinePredictionFill).Elem(),
"Opacity": reflect.ValueOf(&icons.Opacity).Elem(),
"OpacityFill": reflect.ValueOf(&icons.OpacityFill).Elem(),
"Open": reflect.ValueOf(&icons.Open).Elem(),
"OpenFill": reflect.ValueOf(&icons.OpenFill).Elem(),
"OpenInBrowser": reflect.ValueOf(&icons.OpenInBrowser).Elem(),
"OpenInBrowserFill": reflect.ValueOf(&icons.OpenInBrowserFill).Elem(),
"OpenInFull": reflect.ValueOf(&icons.OpenInFull).Elem(),
"OpenInFullFill": reflect.ValueOf(&icons.OpenInFullFill).Elem(),
"OpenInNew": reflect.ValueOf(&icons.OpenInNew).Elem(),
"OpenInNewDown": reflect.ValueOf(&icons.OpenInNewDown).Elem(),
"OpenInNewDownFill": reflect.ValueOf(&icons.OpenInNewDownFill).Elem(),
"OpenInNewFill": reflect.ValueOf(&icons.OpenInNewFill).Elem(),
"OpenInNewOff": reflect.ValueOf(&icons.OpenInNewOff).Elem(),
"OpenInNewOffFill": reflect.ValueOf(&icons.OpenInNewOffFill).Elem(),
"OpenInPhone": reflect.ValueOf(&icons.OpenInPhone).Elem(),
"OpenInPhoneFill": reflect.ValueOf(&icons.OpenInPhoneFill).Elem(),
"OpenWith": reflect.ValueOf(&icons.OpenWith).Elem(),
"OpenWithFill": reflect.ValueOf(&icons.OpenWithFill).Elem(),
"OrderApprove": reflect.ValueOf(&icons.OrderApprove).Elem(),
"OrderApproveFill": reflect.ValueOf(&icons.OrderApproveFill).Elem(),
"OrderPlay": reflect.ValueOf(&icons.OrderPlay).Elem(),
"OrderPlayFill": reflect.ValueOf(&icons.OrderPlayFill).Elem(),
"Outbound": reflect.ValueOf(&icons.Outbound).Elem(),
"OutboundFill": reflect.ValueOf(&icons.OutboundFill).Elem(),
"Outbox": reflect.ValueOf(&icons.Outbox).Elem(),
"OutboxAlt": reflect.ValueOf(&icons.OutboxAlt).Elem(),
"OutboxAltFill": reflect.ValueOf(&icons.OutboxAltFill).Elem(),
"OutboxFill": reflect.ValueOf(&icons.OutboxFill).Elem(),
"OutgoingMail": reflect.ValueOf(&icons.OutgoingMail).Elem(),
"OutgoingMailFill": reflect.ValueOf(&icons.OutgoingMailFill).Elem(),
"Outlet": reflect.ValueOf(&icons.Outlet).Elem(),
"OutletFill": reflect.ValueOf(&icons.OutletFill).Elem(),
"Output": reflect.ValueOf(&icons.Output).Elem(),
"OutputCircle": reflect.ValueOf(&icons.OutputCircle).Elem(),
"OutputCircleFill": reflect.ValueOf(&icons.OutputCircleFill).Elem(),
"OutputFill": reflect.ValueOf(&icons.OutputFill).Elem(),
"Overview": reflect.ValueOf(&icons.Overview).Elem(),
"OverviewFill": reflect.ValueOf(&icons.OverviewFill).Elem(),
"OverviewKey": reflect.ValueOf(&icons.OverviewKey).Elem(),
"OverviewKeyFill": reflect.ValueOf(&icons.OverviewKeyFill).Elem(),
"Pace": reflect.ValueOf(&icons.Pace).Elem(),
"PaceFill": reflect.ValueOf(&icons.PaceFill).Elem(),
"Package": reflect.ValueOf(&icons.Package).Elem(),
"PackageFill": reflect.ValueOf(&icons.PackageFill).Elem(),
"Padding": reflect.ValueOf(&icons.Padding).Elem(),
"PaddingFill": reflect.ValueOf(&icons.PaddingFill).Elem(),
"PageControl": reflect.ValueOf(&icons.PageControl).Elem(),
"PageControlFill": reflect.ValueOf(&icons.PageControlFill).Elem(),
"PageInfo": reflect.ValueOf(&icons.PageInfo).Elem(),
"PageInfoFill": reflect.ValueOf(&icons.PageInfoFill).Elem(),
"Pages": reflect.ValueOf(&icons.Pages).Elem(),
"PagesFill": reflect.ValueOf(&icons.PagesFill).Elem(),
"Pageview": reflect.ValueOf(&icons.Pageview).Elem(),
"PageviewFill": reflect.ValueOf(&icons.PageviewFill).Elem(),
"Paid": reflect.ValueOf(&icons.Paid).Elem(),
"PaidFill": reflect.ValueOf(&icons.PaidFill).Elem(),
"Palette": reflect.ValueOf(&icons.Palette).Elem(),
"PaletteFill": reflect.ValueOf(&icons.PaletteFill).Elem(),
"Pallet": reflect.ValueOf(&icons.Pallet).Elem(),
"PalletFill": reflect.ValueOf(&icons.PalletFill).Elem(),
"PanTool": reflect.ValueOf(&icons.PanTool).Elem(),
"PanToolAlt": reflect.ValueOf(&icons.PanToolAlt).Elem(),
"PanToolAltFill": reflect.ValueOf(&icons.PanToolAltFill).Elem(),
"PanToolFill": reflect.ValueOf(&icons.PanToolFill).Elem(),
"PanZoom": reflect.ValueOf(&icons.PanZoom).Elem(),
"PanZoomFill": reflect.ValueOf(&icons.PanZoomFill).Elem(),
"Panorama": reflect.ValueOf(&icons.Panorama).Elem(),
"PanoramaFill": reflect.ValueOf(&icons.PanoramaFill).Elem(),
"Password": reflect.ValueOf(&icons.Password).Elem(),
"PasswordFill": reflect.ValueOf(&icons.PasswordFill).Elem(),
"Paste": reflect.ValueOf(&icons.Paste).Elem(),
"Pattern": reflect.ValueOf(&icons.Pattern).Elem(),
"PatternFill": reflect.ValueOf(&icons.PatternFill).Elem(),
"Pause": reflect.ValueOf(&icons.Pause).Elem(),
"PauseCircle": reflect.ValueOf(&icons.PauseCircle).Elem(),
"PauseCircleFill": reflect.ValueOf(&icons.PauseCircleFill).Elem(),
"PauseFill": reflect.ValueOf(&icons.PauseFill).Elem(),
"PausePresentation": reflect.ValueOf(&icons.PausePresentation).Elem(),
"PausePresentationFill": reflect.ValueOf(&icons.PausePresentationFill).Elem(),
"Payments": reflect.ValueOf(&icons.Payments).Elem(),
"PaymentsFill": reflect.ValueOf(&icons.PaymentsFill).Elem(),
"PenSize1": reflect.ValueOf(&icons.PenSize1).Elem(),
"PenSize1Fill": reflect.ValueOf(&icons.PenSize1Fill).Elem(),
"PenSize2": reflect.ValueOf(&icons.PenSize2).Elem(),
"PenSize2Fill": reflect.ValueOf(&icons.PenSize2Fill).Elem(),
"PenSize3": reflect.ValueOf(&icons.PenSize3).Elem(),
"PenSize3Fill": reflect.ValueOf(&icons.PenSize3Fill).Elem(),
"PenSize4": reflect.ValueOf(&icons.PenSize4).Elem(),
"PenSize4Fill": reflect.ValueOf(&icons.PenSize4Fill).Elem(),
"PenSize5": reflect.ValueOf(&icons.PenSize5).Elem(),
"PenSize5Fill": reflect.ValueOf(&icons.PenSize5Fill).Elem(),
"Pending": reflect.ValueOf(&icons.Pending).Elem(),
"PendingActions": reflect.ValueOf(&icons.PendingActions).Elem(),
"PendingActionsFill": reflect.ValueOf(&icons.PendingActionsFill).Elem(),
"PendingFill": reflect.ValueOf(&icons.PendingFill).Elem(),
"Pentagon": reflect.ValueOf(&icons.Pentagon).Elem(),
"PentagonFill": reflect.ValueOf(&icons.PentagonFill).Elem(),
"Percent": reflect.ValueOf(&icons.Percent).Elem(),
"PercentFill": reflect.ValueOf(&icons.PercentFill).Elem(),
"PermCameraMic": reflect.ValueOf(&icons.PermCameraMic).Elem(),
"PermCameraMicFill": reflect.ValueOf(&icons.PermCameraMicFill).Elem(),
"PermContactCalendar": reflect.ValueOf(&icons.PermContactCalendar).Elem(),
"PermContactCalendarFill": reflect.ValueOf(&icons.PermContactCalendarFill).Elem(),
"PermDataSetting": reflect.ValueOf(&icons.PermDataSetting).Elem(),
"PermDataSettingFill": reflect.ValueOf(&icons.PermDataSettingFill).Elem(),
"PermDeviceInformation": reflect.ValueOf(&icons.PermDeviceInformation).Elem(),
"PermDeviceInformationFill": reflect.ValueOf(&icons.PermDeviceInformationFill).Elem(),
"PermMedia": reflect.ValueOf(&icons.PermMedia).Elem(),
"PermMediaFill": reflect.ValueOf(&icons.PermMediaFill).Elem(),
"PermPhoneMsg": reflect.ValueOf(&icons.PermPhoneMsg).Elem(),
"PermPhoneMsgFill": reflect.ValueOf(&icons.PermPhoneMsgFill).Elem(),
"PermScanWifi": reflect.ValueOf(&icons.PermScanWifi).Elem(),
"PermScanWifiFill": reflect.ValueOf(&icons.PermScanWifiFill).Elem(),
"Person": reflect.ValueOf(&icons.Person).Elem(),
"Person2": reflect.ValueOf(&icons.Person2).Elem(),
"Person2Fill": reflect.ValueOf(&icons.Person2Fill).Elem(),
"Person3": reflect.ValueOf(&icons.Person3).Elem(),
"Person3Fill": reflect.ValueOf(&icons.Person3Fill).Elem(),
"Person4": reflect.ValueOf(&icons.Person4).Elem(),
"Person4Fill": reflect.ValueOf(&icons.Person4Fill).Elem(),
"PersonAdd": reflect.ValueOf(&icons.PersonAdd).Elem(),
"PersonAddDisabled": reflect.ValueOf(&icons.PersonAddDisabled).Elem(),
"PersonAddDisabledFill": reflect.ValueOf(&icons.PersonAddDisabledFill).Elem(),
"PersonAddFill": reflect.ValueOf(&icons.PersonAddFill).Elem(),
"PersonApron": reflect.ValueOf(&icons.PersonApron).Elem(),
"PersonApronFill": reflect.ValueOf(&icons.PersonApronFill).Elem(),
"PersonBook": reflect.ValueOf(&icons.PersonBook).Elem(),
"PersonBookFill": reflect.ValueOf(&icons.PersonBookFill).Elem(),
"PersonCelebrate": reflect.ValueOf(&icons.PersonCelebrate).Elem(),
"PersonCelebrateFill": reflect.ValueOf(&icons.PersonCelebrateFill).Elem(),
"PersonFill": reflect.ValueOf(&icons.PersonFill).Elem(),
"PersonOff": reflect.ValueOf(&icons.PersonOff).Elem(),
"PersonOffFill": reflect.ValueOf(&icons.PersonOffFill).Elem(),
"PersonPin": reflect.ValueOf(&icons.PersonPin).Elem(),
"PersonPinCircle": reflect.ValueOf(&icons.PersonPinCircle).Elem(),
"PersonPinCircleFill": reflect.ValueOf(&icons.PersonPinCircleFill).Elem(),
"PersonPinFill": reflect.ValueOf(&icons.PersonPinFill).Elem(),
"PersonPlay": reflect.ValueOf(&icons.PersonPlay).Elem(),
"PersonPlayFill": reflect.ValueOf(&icons.PersonPlayFill).Elem(),
"PersonRaisedHand": reflect.ValueOf(&icons.PersonRaisedHand).Elem(),
"PersonRaisedHandFill": reflect.ValueOf(&icons.PersonRaisedHandFill).Elem(),
"PersonRemove": reflect.ValueOf(&icons.PersonRemove).Elem(),
"PersonRemoveFill": reflect.ValueOf(&icons.PersonRemoveFill).Elem(),
"PersonSearch": reflect.ValueOf(&icons.PersonSearch).Elem(),
"PersonSearchFill": reflect.ValueOf(&icons.PersonSearchFill).Elem(),
"Phishing": reflect.ValueOf(&icons.Phishing).Elem(),
"PhishingFill": reflect.ValueOf(&icons.PhishingFill).Elem(),
"PhoneAndroid": reflect.ValueOf(&icons.PhoneAndroid).Elem(),
"PhoneAndroidFill": reflect.ValueOf(&icons.PhoneAndroidFill).Elem(),
"PhoneBluetoothSpeaker": reflect.ValueOf(&icons.PhoneBluetoothSpeaker).Elem(),
"PhoneBluetoothSpeakerFill": reflect.ValueOf(&icons.PhoneBluetoothSpeakerFill).Elem(),
"PhoneCallback": reflect.ValueOf(&icons.PhoneCallback).Elem(),
"PhoneCallbackFill": reflect.ValueOf(&icons.PhoneCallbackFill).Elem(),
"PhoneDisabled": reflect.ValueOf(&icons.PhoneDisabled).Elem(),
"PhoneDisabledFill": reflect.ValueOf(&icons.PhoneDisabledFill).Elem(),
"PhoneEnabled": reflect.ValueOf(&icons.PhoneEnabled).Elem(),
"PhoneEnabledFill": reflect.ValueOf(&icons.PhoneEnabledFill).Elem(),
"PhoneForwarded": reflect.ValueOf(&icons.PhoneForwarded).Elem(),
"PhoneForwardedFill": reflect.ValueOf(&icons.PhoneForwardedFill).Elem(),
"PhoneInTalk": reflect.ValueOf(&icons.PhoneInTalk).Elem(),
"PhoneInTalkFill": reflect.ValueOf(&icons.PhoneInTalkFill).Elem(),
"PhoneIphone": reflect.ValueOf(&icons.PhoneIphone).Elem(),
"PhoneIphoneFill": reflect.ValueOf(&icons.PhoneIphoneFill).Elem(),
"PhoneLocked": reflect.ValueOf(&icons.PhoneLocked).Elem(),
"PhoneLockedFill": reflect.ValueOf(&icons.PhoneLockedFill).Elem(),
"PhoneMissed": reflect.ValueOf(&icons.PhoneMissed).Elem(),
"PhoneMissedFill": reflect.ValueOf(&icons.PhoneMissedFill).Elem(),
"PhonePaused": reflect.ValueOf(&icons.PhonePaused).Elem(),
"PhonePausedFill": reflect.ValueOf(&icons.PhonePausedFill).Elem(),
"Photo": reflect.ValueOf(&icons.Photo).Elem(),
"PhotoAlbum": reflect.ValueOf(&icons.PhotoAlbum).Elem(),
"PhotoAlbumFill": reflect.ValueOf(&icons.PhotoAlbumFill).Elem(),
"PhotoCamera": reflect.ValueOf(&icons.PhotoCamera).Elem(),
"PhotoCameraBack": reflect.ValueOf(&icons.PhotoCameraBack).Elem(),
"PhotoCameraBackFill": reflect.ValueOf(&icons.PhotoCameraBackFill).Elem(),
"PhotoCameraFill": reflect.ValueOf(&icons.PhotoCameraFill).Elem(),
"PhotoCameraFront": reflect.ValueOf(&icons.PhotoCameraFront).Elem(),
"PhotoCameraFrontFill": reflect.ValueOf(&icons.PhotoCameraFrontFill).Elem(),
"PhotoFill": reflect.ValueOf(&icons.PhotoFill).Elem(),
"PhotoFrame": reflect.ValueOf(&icons.PhotoFrame).Elem(),
"PhotoFrameFill": reflect.ValueOf(&icons.PhotoFrameFill).Elem(),
"PhotoLibrary": reflect.ValueOf(&icons.PhotoLibrary).Elem(),
"PhotoLibraryFill": reflect.ValueOf(&icons.PhotoLibraryFill).Elem(),
"PhotoPrints": reflect.ValueOf(&icons.PhotoPrints).Elem(),
"PhotoPrintsFill": reflect.ValueOf(&icons.PhotoPrintsFill).Elem(),
"PhotoSizeSelectLarge": reflect.ValueOf(&icons.PhotoSizeSelectLarge).Elem(),
"PhotoSizeSelectLargeFill": reflect.ValueOf(&icons.PhotoSizeSelectLargeFill).Elem(),
"PhotoSizeSelectSmall": reflect.ValueOf(&icons.PhotoSizeSelectSmall).Elem(),
"PhotoSizeSelectSmallFill": reflect.ValueOf(&icons.PhotoSizeSelectSmallFill).Elem(),
"Php": reflect.ValueOf(&icons.Php).Elem(),
"PhpFill": reflect.ValueOf(&icons.PhpFill).Elem(),
"Piano": reflect.ValueOf(&icons.Piano).Elem(),
"PianoFill": reflect.ValueOf(&icons.PianoFill).Elem(),
"PianoOff": reflect.ValueOf(&icons.PianoOff).Elem(),
"PianoOffFill": reflect.ValueOf(&icons.PianoOffFill).Elem(),
"PictureAsPdf": reflect.ValueOf(&icons.PictureAsPdf).Elem(),
"PictureAsPdfFill": reflect.ValueOf(&icons.PictureAsPdfFill).Elem(),
"PictureInPicture": reflect.ValueOf(&icons.PictureInPicture).Elem(),
"PictureInPictureAlt": reflect.ValueOf(&icons.PictureInPictureAlt).Elem(),
"PictureInPictureAltFill": reflect.ValueOf(&icons.PictureInPictureAltFill).Elem(),
"PictureInPictureFill": reflect.ValueOf(&icons.PictureInPictureFill).Elem(),
"PieChart": reflect.ValueOf(&icons.PieChart).Elem(),
"PieChartFill": reflect.ValueOf(&icons.PieChartFill).Elem(),
"Pill": reflect.ValueOf(&icons.Pill).Elem(),
"PillFill": reflect.ValueOf(&icons.PillFill).Elem(),
"PillOff": reflect.ValueOf(&icons.PillOff).Elem(),
"PillOffFill": reflect.ValueOf(&icons.PillOffFill).Elem(),
"Pin": reflect.ValueOf(&icons.Pin).Elem(),
"PinDrop": reflect.ValueOf(&icons.PinDrop).Elem(),
"PinDropFill": reflect.ValueOf(&icons.PinDropFill).Elem(),
"PinEnd": reflect.ValueOf(&icons.PinEnd).Elem(),
"PinEndFill": reflect.ValueOf(&icons.PinEndFill).Elem(),
"PinFill": reflect.ValueOf(&icons.PinFill).Elem(),
"PinInvoke": reflect.ValueOf(&icons.PinInvoke).Elem(),
"PinInvokeFill": reflect.ValueOf(&icons.PinInvokeFill).Elem(),
"Pinch": reflect.ValueOf(&icons.Pinch).Elem(),
"PinchFill": reflect.ValueOf(&icons.PinchFill).Elem(),
"PinchZoomIn": reflect.ValueOf(&icons.PinchZoomIn).Elem(),
"PinchZoomInFill": reflect.ValueOf(&icons.PinchZoomInFill).Elem(),
"PinchZoomOut": reflect.ValueOf(&icons.PinchZoomOut).Elem(),
"PinchZoomOutFill": reflect.ValueOf(&icons.PinchZoomOutFill).Elem(),
"Pip": reflect.ValueOf(&icons.Pip).Elem(),
"PipExit": reflect.ValueOf(&icons.PipExit).Elem(),
"PipExitFill": reflect.ValueOf(&icons.PipExitFill).Elem(),
"PipFill": reflect.ValueOf(&icons.PipFill).Elem(),
"PivotTableChart": reflect.ValueOf(&icons.PivotTableChart).Elem(),
"PivotTableChartFill": reflect.ValueOf(&icons.PivotTableChartFill).Elem(),
"PlaceItem": reflect.ValueOf(&icons.PlaceItem).Elem(),
"PlaceItemFill": reflect.ValueOf(&icons.PlaceItemFill).Elem(),
"Plagiarism": reflect.ValueOf(&icons.Plagiarism).Elem(),
"PlagiarismFill": reflect.ValueOf(&icons.PlagiarismFill).Elem(),
"PlayArrow": reflect.ValueOf(&icons.PlayArrow).Elem(),
"PlayArrowFill": reflect.ValueOf(&icons.PlayArrowFill).Elem(),
"PlayCircle": reflect.ValueOf(&icons.PlayCircle).Elem(),
"PlayCircleFill": reflect.ValueOf(&icons.PlayCircleFill).Elem(),
"PlayDisabled": reflect.ValueOf(&icons.PlayDisabled).Elem(),
"PlayDisabledFill": reflect.ValueOf(&icons.PlayDisabledFill).Elem(),
"PlayForWork": reflect.ValueOf(&icons.PlayForWork).Elem(),
"PlayForWorkFill": reflect.ValueOf(&icons.PlayForWorkFill).Elem(),
"PlayLesson": reflect.ValueOf(&icons.PlayLesson).Elem(),
"PlayLessonFill": reflect.ValueOf(&icons.PlayLessonFill).Elem(),
"PlayPause": reflect.ValueOf(&icons.PlayPause).Elem(),
"PlayPauseFill": reflect.ValueOf(&icons.PlayPauseFill).Elem(),
"PlayShapes": reflect.ValueOf(&icons.PlayShapes).Elem(),
"PlayShapesFill": reflect.ValueOf(&icons.PlayShapesFill).Elem(),
"PlayingCards": reflect.ValueOf(&icons.PlayingCards).Elem(),
"PlayingCardsFill": reflect.ValueOf(&icons.PlayingCardsFill).Elem(),
"PlaylistAdd": reflect.ValueOf(&icons.PlaylistAdd).Elem(),
"PlaylistAddCheck": reflect.ValueOf(&icons.PlaylistAddCheck).Elem(),
"PlaylistAddCheckCircle": reflect.ValueOf(&icons.PlaylistAddCheckCircle).Elem(),
"PlaylistAddCheckCircleFill": reflect.ValueOf(&icons.PlaylistAddCheckCircleFill).Elem(),
"PlaylistAddCheckFill": reflect.ValueOf(&icons.PlaylistAddCheckFill).Elem(),
"PlaylistAddCircle": reflect.ValueOf(&icons.PlaylistAddCircle).Elem(),
"PlaylistAddCircleFill": reflect.ValueOf(&icons.PlaylistAddCircleFill).Elem(),
"PlaylistAddFill": reflect.ValueOf(&icons.PlaylistAddFill).Elem(),
"PlaylistPlay": reflect.ValueOf(&icons.PlaylistPlay).Elem(),
"PlaylistPlayFill": reflect.ValueOf(&icons.PlaylistPlayFill).Elem(),
"PlaylistRemove": reflect.ValueOf(&icons.PlaylistRemove).Elem(),
"PlaylistRemoveFill": reflect.ValueOf(&icons.PlaylistRemoveFill).Elem(),
"Podcasts": reflect.ValueOf(&icons.Podcasts).Elem(),
"PodcastsFill": reflect.ValueOf(&icons.PodcastsFill).Elem(),
"Podium": reflect.ValueOf(&icons.Podium).Elem(),
"PodiumFill": reflect.ValueOf(&icons.PodiumFill).Elem(),
"PointOfSale": reflect.ValueOf(&icons.PointOfSale).Elem(),
"PointOfSaleFill": reflect.ValueOf(&icons.PointOfSaleFill).Elem(),
"PointScan": reflect.ValueOf(&icons.PointScan).Elem(),
"PointScanFill": reflect.ValueOf(&icons.PointScanFill).Elem(),
"Policy": reflect.ValueOf(&icons.Policy).Elem(),
"PolicyFill": reflect.ValueOf(&icons.PolicyFill).Elem(),
"Polyline": reflect.ValueOf(&icons.Polyline).Elem(),
"PolylineFill": reflect.ValueOf(&icons.PolylineFill).Elem(),
"Polymer": reflect.ValueOf(&icons.Polymer).Elem(),
"PolymerFill": reflect.ValueOf(&icons.PolymerFill).Elem(),
"PortableWifiOff": reflect.ValueOf(&icons.PortableWifiOff).Elem(),
"PortableWifiOffFill": reflect.ValueOf(&icons.PortableWifiOffFill).Elem(),
"PositionBottomLeft": reflect.ValueOf(&icons.PositionBottomLeft).Elem(),
"PositionBottomLeftFill": reflect.ValueOf(&icons.PositionBottomLeftFill).Elem(),
"PositionBottomRight": reflect.ValueOf(&icons.PositionBottomRight).Elem(),
"PositionBottomRightFill": reflect.ValueOf(&icons.PositionBottomRightFill).Elem(),
"PositionTopRight": reflect.ValueOf(&icons.PositionTopRight).Elem(),
"PositionTopRightFill": reflect.ValueOf(&icons.PositionTopRightFill).Elem(),
"Post": reflect.ValueOf(&icons.Post).Elem(),
"PostAdd": reflect.ValueOf(&icons.PostAdd).Elem(),
"PostAddFill": reflect.ValueOf(&icons.PostAddFill).Elem(),
"PostFill": reflect.ValueOf(&icons.PostFill).Elem(),
"Power": reflect.ValueOf(&icons.Power).Elem(),
"PowerFill": reflect.ValueOf(&icons.PowerFill).Elem(),
"PowerInput": reflect.ValueOf(&icons.PowerInput).Elem(),
"PowerInputFill": reflect.ValueOf(&icons.PowerInputFill).Elem(),
"PowerOff": reflect.ValueOf(&icons.PowerOff).Elem(),
"PowerOffFill": reflect.ValueOf(&icons.PowerOffFill).Elem(),
"PowerSettingsNew": reflect.ValueOf(&icons.PowerSettingsNew).Elem(),
"PowerSettingsNewFill": reflect.ValueOf(&icons.PowerSettingsNewFill).Elem(),
"Preliminary": reflect.ValueOf(&icons.Preliminary).Elem(),
"PreliminaryFill": reflect.ValueOf(&icons.PreliminaryFill).Elem(),
"PresentToAll": reflect.ValueOf(&icons.PresentToAll).Elem(),
"PresentToAllFill": reflect.ValueOf(&icons.PresentToAllFill).Elem(),
"Preview": reflect.ValueOf(&icons.Preview).Elem(),
"PreviewFill": reflect.ValueOf(&icons.PreviewFill).Elem(),
"PreviewOff": reflect.ValueOf(&icons.PreviewOff).Elem(),
"PreviewOffFill": reflect.ValueOf(&icons.PreviewOffFill).Elem(),
"PriceChange": reflect.ValueOf(&icons.PriceChange).Elem(),
"PriceChangeFill": reflect.ValueOf(&icons.PriceChangeFill).Elem(),
"PriceCheck": reflect.ValueOf(&icons.PriceCheck).Elem(),
"PriceCheckFill": reflect.ValueOf(&icons.PriceCheckFill).Elem(),
"Print": reflect.ValueOf(&icons.Print).Elem(),
"PrintAdd": reflect.ValueOf(&icons.PrintAdd).Elem(),
"PrintAddFill": reflect.ValueOf(&icons.PrintAddFill).Elem(),
"PrintConnect": reflect.ValueOf(&icons.PrintConnect).Elem(),
"PrintConnectFill": reflect.ValueOf(&icons.PrintConnectFill).Elem(),
"PrintDisabled": reflect.ValueOf(&icons.PrintDisabled).Elem(),
"PrintDisabledFill": reflect.ValueOf(&icons.PrintDisabledFill).Elem(),
"PrintError": reflect.ValueOf(&icons.PrintError).Elem(),
"PrintErrorFill": reflect.ValueOf(&icons.PrintErrorFill).Elem(),
"PrintFill": reflect.ValueOf(&icons.PrintFill).Elem(),
"PrintLock": reflect.ValueOf(&icons.PrintLock).Elem(),
"PrintLockFill": reflect.ValueOf(&icons.PrintLockFill).Elem(),
"Priority": reflect.ValueOf(&icons.Priority).Elem(),
"PriorityFill": reflect.ValueOf(&icons.PriorityFill).Elem(),
"PriorityHigh": reflect.ValueOf(&icons.PriorityHigh).Elem(),
"PriorityHighFill": reflect.ValueOf(&icons.PriorityHighFill).Elem(),
"Privacy": reflect.ValueOf(&icons.Privacy).Elem(),
"PrivacyFill": reflect.ValueOf(&icons.PrivacyFill).Elem(),
"PrivacyTip": reflect.ValueOf(&icons.PrivacyTip).Elem(),
"PrivacyTipFill": reflect.ValueOf(&icons.PrivacyTipFill).Elem(),
"PrivateConnectivity": reflect.ValueOf(&icons.PrivateConnectivity).Elem(),
"PrivateConnectivityFill": reflect.ValueOf(&icons.PrivateConnectivityFill).Elem(),
"Problem": reflect.ValueOf(&icons.Problem).Elem(),
"ProblemFill": reflect.ValueOf(&icons.ProblemFill).Elem(),
"ProcessChart": reflect.ValueOf(&icons.ProcessChart).Elem(),
"ProcessChartFill": reflect.ValueOf(&icons.ProcessChartFill).Elem(),
"Productivity": reflect.ValueOf(&icons.Productivity).Elem(),
"ProductivityFill": reflect.ValueOf(&icons.ProductivityFill).Elem(),
"ProgressActivity": reflect.ValueOf(&icons.ProgressActivity).Elem(),
"ProgressActivityFill": reflect.ValueOf(&icons.ProgressActivityFill).Elem(),
"Publish": reflect.ValueOf(&icons.Publish).Elem(),
"PublishFill": reflect.ValueOf(&icons.PublishFill).Elem(),
"PublishedWithChanges": reflect.ValueOf(&icons.PublishedWithChanges).Elem(),
"PublishedWithChangesFill": reflect.ValueOf(&icons.PublishedWithChangesFill).Elem(),
"PunchClock": reflect.ValueOf(&icons.PunchClock).Elem(),
"PunchClockFill": reflect.ValueOf(&icons.PunchClockFill).Elem(),
"PushPin": reflect.ValueOf(&icons.PushPin).Elem(),
"PushPinFill": reflect.ValueOf(&icons.PushPinFill).Elem(),
"QrCode": reflect.ValueOf(&icons.QrCode).Elem(),
"QrCode2": reflect.ValueOf(&icons.QrCode2).Elem(),
"QrCode2Add": reflect.ValueOf(&icons.QrCode2Add).Elem(),
"QrCode2AddFill": reflect.ValueOf(&icons.QrCode2AddFill).Elem(),
"QrCode2Fill": reflect.ValueOf(&icons.QrCode2Fill).Elem(),
"QrCodeFill": reflect.ValueOf(&icons.QrCodeFill).Elem(),
"QrCodeScanner": reflect.ValueOf(&icons.QrCodeScanner).Elem(),
"QrCodeScannerFill": reflect.ValueOf(&icons.QrCodeScannerFill).Elem(),
"QueryStats": reflect.ValueOf(&icons.QueryStats).Elem(),
"QueryStatsFill": reflect.ValueOf(&icons.QueryStatsFill).Elem(),
"QuestionExchange": reflect.ValueOf(&icons.QuestionExchange).Elem(),
"QuestionExchangeFill": reflect.ValueOf(&icons.QuestionExchangeFill).Elem(),
"QuestionMark": reflect.ValueOf(&icons.QuestionMark).Elem(),
"QuestionMarkFill": reflect.ValueOf(&icons.QuestionMarkFill).Elem(),
"QueueMusic": reflect.ValueOf(&icons.QueueMusic).Elem(),
"QueueMusicFill": reflect.ValueOf(&icons.QueueMusicFill).Elem(),
"QueuePlayNext": reflect.ValueOf(&icons.QueuePlayNext).Elem(),
"QueuePlayNextFill": reflect.ValueOf(&icons.QueuePlayNextFill).Elem(),
"QuickPhrases": reflect.ValueOf(&icons.QuickPhrases).Elem(),
"QuickPhrasesFill": reflect.ValueOf(&icons.QuickPhrasesFill).Elem(),
"QuickReference": reflect.ValueOf(&icons.QuickReference).Elem(),
"QuickReferenceAll": reflect.ValueOf(&icons.QuickReferenceAll).Elem(),
"QuickReferenceAllFill": reflect.ValueOf(&icons.QuickReferenceAllFill).Elem(),
"QuickReferenceFill": reflect.ValueOf(&icons.QuickReferenceFill).Elem(),
"Quickreply": reflect.ValueOf(&icons.Quickreply).Elem(),
"QuickreplyFill": reflect.ValueOf(&icons.QuickreplyFill).Elem(),
"Quiz": reflect.ValueOf(&icons.Quiz).Elem(),
"QuizFill": reflect.ValueOf(&icons.QuizFill).Elem(),
"Radar": reflect.ValueOf(&icons.Radar).Elem(),
"RadarFill": reflect.ValueOf(&icons.RadarFill).Elem(),
"Radio": reflect.ValueOf(&icons.Radio).Elem(),
"RadioButtonChecked": reflect.ValueOf(&icons.RadioButtonChecked).Elem(),
"RadioButtonCheckedFill": reflect.ValueOf(&icons.RadioButtonCheckedFill).Elem(),
"RadioButtonPartial": reflect.ValueOf(&icons.RadioButtonPartial).Elem(),
"RadioButtonUnchecked": reflect.ValueOf(&icons.RadioButtonUnchecked).Elem(),
"RadioButtonUncheckedFill": reflect.ValueOf(&icons.RadioButtonUncheckedFill).Elem(),
"RadioFill": reflect.ValueOf(&icons.RadioFill).Elem(),
"RateReview": reflect.ValueOf(&icons.RateReview).Elem(),
"RateReviewFill": reflect.ValueOf(&icons.RateReviewFill).Elem(),
"ReadMore": reflect.ValueOf(&icons.ReadMore).Elem(),
"ReadMoreFill": reflect.ValueOf(&icons.ReadMoreFill).Elem(),
"ReadinessScore": reflect.ValueOf(&icons.ReadinessScore).Elem(),
"ReadinessScoreFill": reflect.ValueOf(&icons.ReadinessScoreFill).Elem(),
"RearCamera": reflect.ValueOf(&icons.RearCamera).Elem(),
"RearCameraFill": reflect.ValueOf(&icons.RearCameraFill).Elem(),
"Rebase": reflect.ValueOf(&icons.Rebase).Elem(),
"RebaseEdit": reflect.ValueOf(&icons.RebaseEdit).Elem(),
"RebaseEditFill": reflect.ValueOf(&icons.RebaseEditFill).Elem(),
"RebaseFill": reflect.ValueOf(&icons.RebaseFill).Elem(),
"Receipt": reflect.ValueOf(&icons.Receipt).Elem(),
"ReceiptFill": reflect.ValueOf(&icons.ReceiptFill).Elem(),
"ReceiptLong": reflect.ValueOf(&icons.ReceiptLong).Elem(),
"ReceiptLongFill": reflect.ValueOf(&icons.ReceiptLongFill).Elem(),
"Recommend": reflect.ValueOf(&icons.Recommend).Elem(),
"RecommendFill": reflect.ValueOf(&icons.RecommendFill).Elem(),
"RecordVoiceOver": reflect.ValueOf(&icons.RecordVoiceOver).Elem(),
"RecordVoiceOverFill": reflect.ValueOf(&icons.RecordVoiceOverFill).Elem(),
"Rectangle": reflect.ValueOf(&icons.Rectangle).Elem(),
"RectangleFill": reflect.ValueOf(&icons.RectangleFill).Elem(),
"Recycling": reflect.ValueOf(&icons.Recycling).Elem(),
"RecyclingFill": reflect.ValueOf(&icons.RecyclingFill).Elem(),
"Redeem": reflect.ValueOf(&icons.Redeem).Elem(),
"RedeemFill": reflect.ValueOf(&icons.RedeemFill).Elem(),
"Redo": reflect.ValueOf(&icons.Redo).Elem(),
"RedoFill": reflect.ValueOf(&icons.RedoFill).Elem(),
"ReduceCapacity": reflect.ValueOf(&icons.ReduceCapacity).Elem(),
"ReduceCapacityFill": reflect.ValueOf(&icons.ReduceCapacityFill).Elem(),
"Refresh": reflect.ValueOf(&icons.Refresh).Elem(),
"RefreshFill": reflect.ValueOf(&icons.RefreshFill).Elem(),
"RegularExpression": reflect.ValueOf(&icons.RegularExpression).Elem(),
"RegularExpressionFill": reflect.ValueOf(&icons.RegularExpressionFill).Elem(),
"Relax": reflect.ValueOf(&icons.Relax).Elem(),
"RelaxFill": reflect.ValueOf(&icons.RelaxFill).Elem(),
"ReleaseAlert": reflect.ValueOf(&icons.ReleaseAlert).Elem(),
"ReleaseAlertFill": reflect.ValueOf(&icons.ReleaseAlertFill).Elem(),
"RememberMe": reflect.ValueOf(&icons.RememberMe).Elem(),
"RememberMeFill": reflect.ValueOf(&icons.RememberMeFill).Elem(),
"Reminder": reflect.ValueOf(&icons.Reminder).Elem(),
"ReminderFill": reflect.ValueOf(&icons.ReminderFill).Elem(),
"RemoteGen": reflect.ValueOf(&icons.RemoteGen).Elem(),
"RemoteGenFill": reflect.ValueOf(&icons.RemoteGenFill).Elem(),
"Remove": reflect.ValueOf(&icons.Remove).Elem(),
"RemoveDone": reflect.ValueOf(&icons.RemoveDone).Elem(),
"RemoveDoneFill": reflect.ValueOf(&icons.RemoveDoneFill).Elem(),
"RemoveFill": reflect.ValueOf(&icons.RemoveFill).Elem(),
"RemoveFromQueue": reflect.ValueOf(&icons.RemoveFromQueue).Elem(),
"RemoveFromQueueFill": reflect.ValueOf(&icons.RemoveFromQueueFill).Elem(),
"RemoveModerator": reflect.ValueOf(&icons.RemoveModerator).Elem(),
"RemoveModeratorFill": reflect.ValueOf(&icons.RemoveModeratorFill).Elem(),
"RemoveSelection": reflect.ValueOf(&icons.RemoveSelection).Elem(),
"RemoveSelectionFill": reflect.ValueOf(&icons.RemoveSelectionFill).Elem(),
"RemoveShoppingCart": reflect.ValueOf(&icons.RemoveShoppingCart).Elem(),
"RemoveShoppingCartFill": reflect.ValueOf(&icons.RemoveShoppingCartFill).Elem(),
"ReopenWindow": reflect.ValueOf(&icons.ReopenWindow).Elem(),
"ReopenWindowFill": reflect.ValueOf(&icons.ReopenWindowFill).Elem(),
"Reorder": reflect.ValueOf(&icons.Reorder).Elem(),
"ReorderFill": reflect.ValueOf(&icons.ReorderFill).Elem(),
"Repartition": reflect.ValueOf(&icons.Repartition).Elem(),
"RepartitionFill": reflect.ValueOf(&icons.RepartitionFill).Elem(),
"Repeat": reflect.ValueOf(&icons.Repeat).Elem(),
"RepeatFill": reflect.ValueOf(&icons.RepeatFill).Elem(),
"RepeatOn": reflect.ValueOf(&icons.RepeatOn).Elem(),
"RepeatOnFill": reflect.ValueOf(&icons.RepeatOnFill).Elem(),
"RepeatOne": reflect.ValueOf(&icons.RepeatOne).Elem(),
"RepeatOneFill": reflect.ValueOf(&icons.RepeatOneFill).Elem(),
"RepeatOneOn": reflect.ValueOf(&icons.RepeatOneOn).Elem(),
"RepeatOneOnFill": reflect.ValueOf(&icons.RepeatOneOnFill).Elem(),
"Replay": reflect.ValueOf(&icons.Replay).Elem(),
"Replay10": reflect.ValueOf(&icons.Replay10).Elem(),
"Replay10Fill": reflect.ValueOf(&icons.Replay10Fill).Elem(),
"Replay30": reflect.ValueOf(&icons.Replay30).Elem(),
"Replay30Fill": reflect.ValueOf(&icons.Replay30Fill).Elem(),
"Replay5": reflect.ValueOf(&icons.Replay5).Elem(),
"Replay5Fill": reflect.ValueOf(&icons.Replay5Fill).Elem(),
"ReplayFill": reflect.ValueOf(&icons.ReplayFill).Elem(),
"Reply": reflect.ValueOf(&icons.Reply).Elem(),
"ReplyAll": reflect.ValueOf(&icons.ReplyAll).Elem(),
"ReplyAllFill": reflect.ValueOf(&icons.ReplyAllFill).Elem(),
"ReplyFill": reflect.ValueOf(&icons.ReplyFill).Elem(),
"Report": reflect.ValueOf(&icons.Report).Elem(),
"ReportFill": reflect.ValueOf(&icons.ReportFill).Elem(),
"ReportOff": reflect.ValueOf(&icons.ReportOff).Elem(),
"ReportOffFill": reflect.ValueOf(&icons.ReportOffFill).Elem(),
"RequestPage": reflect.ValueOf(&icons.RequestPage).Elem(),
"RequestPageFill": reflect.ValueOf(&icons.RequestPageFill).Elem(),
"RequestQuote": reflect.ValueOf(&icons.RequestQuote).Elem(),
"RequestQuoteFill": reflect.ValueOf(&icons.RequestQuoteFill).Elem(),
"Reset": reflect.ValueOf(&icons.Reset).Elem(),
"ResetFill": reflect.ValueOf(&icons.ResetFill).Elem(),
"ResetImage": reflect.ValueOf(&icons.ResetImage).Elem(),
"ResetImageFill": reflect.ValueOf(&icons.ResetImageFill).Elem(),
"ResetTv": reflect.ValueOf(&icons.ResetTv).Elem(),
"ResetTvFill": reflect.ValueOf(&icons.ResetTvFill).Elem(),
"Resize": reflect.ValueOf(&icons.Resize).Elem(),
"ResizeFill": reflect.ValueOf(&icons.ResizeFill).Elem(),
"Restart": reflect.ValueOf(&icons.Restart).Elem(),
"RestartFill": reflect.ValueOf(&icons.RestartFill).Elem(),
"RestoreFromTrash": reflect.ValueOf(&icons.RestoreFromTrash).Elem(),
"RestoreFromTrashFill": reflect.ValueOf(&icons.RestoreFromTrashFill).Elem(),
"RestorePage": reflect.ValueOf(&icons.RestorePage).Elem(),
"RestorePageFill": reflect.ValueOf(&icons.RestorePageFill).Elem(),
"Resume": reflect.ValueOf(&icons.Resume).Elem(),
"ResumeFill": reflect.ValueOf(&icons.ResumeFill).Elem(),
"Reviews": reflect.ValueOf(&icons.Reviews).Elem(),
"ReviewsFill": reflect.ValueOf(&icons.ReviewsFill).Elem(),
"RightClick": reflect.ValueOf(&icons.RightClick).Elem(),
"RightClickFill": reflect.ValueOf(&icons.RightClickFill).Elem(),
"RightPanelClose": reflect.ValueOf(&icons.RightPanelClose).Elem(),
"RightPanelCloseFill": reflect.ValueOf(&icons.RightPanelCloseFill).Elem(),
"RightPanelOpen": reflect.ValueOf(&icons.RightPanelOpen).Elem(),
"RightPanelOpenFill": reflect.ValueOf(&icons.RightPanelOpenFill).Elem(),
"RingVolume": reflect.ValueOf(&icons.RingVolume).Elem(),
"RingVolumeFill": reflect.ValueOf(&icons.RingVolumeFill).Elem(),
"Robot": reflect.ValueOf(&icons.Robot).Elem(),
"Robot2": reflect.ValueOf(&icons.Robot2).Elem(),
"Robot2Fill": reflect.ValueOf(&icons.Robot2Fill).Elem(),
"RobotFill": reflect.ValueOf(&icons.RobotFill).Elem(),
"Rocket": reflect.ValueOf(&icons.Rocket).Elem(),
"RocketFill": reflect.ValueOf(&icons.RocketFill).Elem(),
"RocketLaunch": reflect.ValueOf(&icons.RocketLaunch).Elem(),
"RocketLaunchFill": reflect.ValueOf(&icons.RocketLaunchFill).Elem(),
"Rotate90DegreesCcw": reflect.ValueOf(&icons.Rotate90DegreesCcw).Elem(),
"Rotate90DegreesCcwFill": reflect.ValueOf(&icons.Rotate90DegreesCcwFill).Elem(),
"Rotate90DegreesCw": reflect.ValueOf(&icons.Rotate90DegreesCw).Elem(),
"Rotate90DegreesCwFill": reflect.ValueOf(&icons.Rotate90DegreesCwFill).Elem(),
"RotateLeft": reflect.ValueOf(&icons.RotateLeft).Elem(),
"RotateLeftFill": reflect.ValueOf(&icons.RotateLeftFill).Elem(),
"RotateRight": reflect.ValueOf(&icons.RotateRight).Elem(),
"RotateRightFill": reflect.ValueOf(&icons.RotateRightFill).Elem(),
"RoundedCorner": reflect.ValueOf(&icons.RoundedCorner).Elem(),
"RoundedCornerFill": reflect.ValueOf(&icons.RoundedCornerFill).Elem(),
"Route": reflect.ValueOf(&icons.Route).Elem(),
"RouteFill": reflect.ValueOf(&icons.RouteFill).Elem(),
"Router": reflect.ValueOf(&icons.Router).Elem(),
"RouterFill": reflect.ValueOf(&icons.RouterFill).Elem(),
"Routine": reflect.ValueOf(&icons.Routine).Elem(),
"RoutineFill": reflect.ValueOf(&icons.RoutineFill).Elem(),
"RssFeed": reflect.ValueOf(&icons.RssFeed).Elem(),
"RssFeedFill": reflect.ValueOf(&icons.RssFeedFill).Elem(),
"Rsvp": reflect.ValueOf(&icons.Rsvp).Elem(),
"RsvpFill": reflect.ValueOf(&icons.RsvpFill).Elem(),
"Rtt": reflect.ValueOf(&icons.Rtt).Elem(),
"RttFill": reflect.ValueOf(&icons.RttFill).Elem(),
"Rule": reflect.ValueOf(&icons.Rule).Elem(),
"RuleFill": reflect.ValueOf(&icons.RuleFill).Elem(),
"RuleFolder": reflect.ValueOf(&icons.RuleFolder).Elem(),
"RuleFolderFill": reflect.ValueOf(&icons.RuleFolderFill).Elem(),
"RuleSettings": reflect.ValueOf(&icons.RuleSettings).Elem(),
"RuleSettingsFill": reflect.ValueOf(&icons.RuleSettingsFill).Elem(),
"RunCircle": reflect.ValueOf(&icons.RunCircle).Elem(),
"RunCircleFill": reflect.ValueOf(&icons.RunCircleFill).Elem(),
"RunningWithErrors": reflect.ValueOf(&icons.RunningWithErrors).Elem(),
"RunningWithErrorsFill": reflect.ValueOf(&icons.RunningWithErrorsFill).Elem(),
"SafetyCheck": reflect.ValueOf(&icons.SafetyCheck).Elem(),
"SafetyCheckFill": reflect.ValueOf(&icons.SafetyCheckFill).Elem(),
"SafetyCheckOff": reflect.ValueOf(&icons.SafetyCheckOff).Elem(),
"SafetyCheckOffFill": reflect.ValueOf(&icons.SafetyCheckOffFill).Elem(),
"Sanitizer": reflect.ValueOf(&icons.Sanitizer).Elem(),
"SanitizerFill": reflect.ValueOf(&icons.SanitizerFill).Elem(),
"Satellite": reflect.ValueOf(&icons.Satellite).Elem(),
"SatelliteAlt": reflect.ValueOf(&icons.SatelliteAlt).Elem(),
"SatelliteAltFill": reflect.ValueOf(&icons.SatelliteAltFill).Elem(),
"SatelliteFill": reflect.ValueOf(&icons.SatelliteFill).Elem(),
"Save": reflect.ValueOf(&icons.Save).Elem(),
"SaveAs": reflect.ValueOf(&icons.SaveAs).Elem(),
"SaveAsFill": reflect.ValueOf(&icons.SaveAsFill).Elem(),
"SaveFill": reflect.ValueOf(&icons.SaveFill).Elem(),
"SavedSearch": reflect.ValueOf(&icons.SavedSearch).Elem(),
"SavedSearchFill": reflect.ValueOf(&icons.SavedSearchFill).Elem(),
"Savings": reflect.ValueOf(&icons.Savings).Elem(),
"SavingsFill": reflect.ValueOf(&icons.SavingsFill).Elem(),
"Scale": reflect.ValueOf(&icons.Scale).Elem(),
"ScaleFill": reflect.ValueOf(&icons.ScaleFill).Elem(),
"Scan": reflect.ValueOf(&icons.Scan).Elem(),
"ScanDelete": reflect.ValueOf(&icons.ScanDelete).Elem(),
"ScanDeleteFill": reflect.ValueOf(&icons.ScanDeleteFill).Elem(),
"ScanFill": reflect.ValueOf(&icons.ScanFill).Elem(),
"Scanner": reflect.ValueOf(&icons.Scanner).Elem(),
"ScannerFill": reflect.ValueOf(&icons.ScannerFill).Elem(),
"ScatterPlot": reflect.ValueOf(&icons.ScatterPlot).Elem(),
"ScatterPlotFill": reflect.ValueOf(&icons.ScatterPlotFill).Elem(),
"Scene": reflect.ValueOf(&icons.Scene).Elem(),
"SceneFill": reflect.ValueOf(&icons.SceneFill).Elem(),
"Schedule": reflect.ValueOf(&icons.Schedule).Elem(),
"ScheduleFill": reflect.ValueOf(&icons.ScheduleFill).Elem(),
"ScheduleSend": reflect.ValueOf(&icons.ScheduleSend).Elem(),
"ScheduleSendFill": reflect.ValueOf(&icons.ScheduleSendFill).Elem(),
"Schema": reflect.ValueOf(&icons.Schema).Elem(),
"SchemaFill": reflect.ValueOf(&icons.SchemaFill).Elem(),
"School": reflect.ValueOf(&icons.School).Elem(),
"SchoolFill": reflect.ValueOf(&icons.SchoolFill).Elem(),
"Science": reflect.ValueOf(&icons.Science).Elem(),
"ScienceFill": reflect.ValueOf(&icons.ScienceFill).Elem(),
"Score": reflect.ValueOf(&icons.Score).Elem(),
"ScoreFill": reflect.ValueOf(&icons.ScoreFill).Elem(),
"Scoreboard": reflect.ValueOf(&icons.Scoreboard).Elem(),
"ScoreboardFill": reflect.ValueOf(&icons.ScoreboardFill).Elem(),
"ScreenLockLandscape": reflect.ValueOf(&icons.ScreenLockLandscape).Elem(),
"ScreenLockLandscapeFill": reflect.ValueOf(&icons.ScreenLockLandscapeFill).Elem(),
"ScreenLockPortrait": reflect.ValueOf(&icons.ScreenLockPortrait).Elem(),
"ScreenLockPortraitFill": reflect.ValueOf(&icons.ScreenLockPortraitFill).Elem(),
"ScreenLockRotation": reflect.ValueOf(&icons.ScreenLockRotation).Elem(),
"ScreenLockRotationFill": reflect.ValueOf(&icons.ScreenLockRotationFill).Elem(),
"ScreenRecord": reflect.ValueOf(&icons.ScreenRecord).Elem(),
"ScreenRecordFill": reflect.ValueOf(&icons.ScreenRecordFill).Elem(),
"ScreenRotation": reflect.ValueOf(&icons.ScreenRotation).Elem(),
"ScreenRotationAlt": reflect.ValueOf(&icons.ScreenRotationAlt).Elem(),
"ScreenRotationAltFill": reflect.ValueOf(&icons.ScreenRotationAltFill).Elem(),
"ScreenRotationFill": reflect.ValueOf(&icons.ScreenRotationFill).Elem(),
"ScreenRotationUp": reflect.ValueOf(&icons.ScreenRotationUp).Elem(),
"ScreenRotationUpFill": reflect.ValueOf(&icons.ScreenRotationUpFill).Elem(),
"ScreenSearchDesktop": reflect.ValueOf(&icons.ScreenSearchDesktop).Elem(),
"ScreenSearchDesktopFill": reflect.ValueOf(&icons.ScreenSearchDesktopFill).Elem(),
"ScreenShare": reflect.ValueOf(&icons.ScreenShare).Elem(),
"ScreenShareFill": reflect.ValueOf(&icons.ScreenShareFill).Elem(),
"Screenshot": reflect.ValueOf(&icons.Screenshot).Elem(),
"ScreenshotFill": reflect.ValueOf(&icons.ScreenshotFill).Elem(),
"ScreenshotFrame": reflect.ValueOf(&icons.ScreenshotFrame).Elem(),
"ScreenshotFrameFill": reflect.ValueOf(&icons.ScreenshotFrameFill).Elem(),
"ScreenshotKeyboard": reflect.ValueOf(&icons.ScreenshotKeyboard).Elem(),
"ScreenshotKeyboardFill": reflect.ValueOf(&icons.ScreenshotKeyboardFill).Elem(),
"ScreenshotMonitor": reflect.ValueOf(&icons.ScreenshotMonitor).Elem(),
"ScreenshotMonitorFill": reflect.ValueOf(&icons.ScreenshotMonitorFill).Elem(),
"ScreenshotRegion": reflect.ValueOf(&icons.ScreenshotRegion).Elem(),
"ScreenshotRegionFill": reflect.ValueOf(&icons.ScreenshotRegionFill).Elem(),
"ScreenshotTablet": reflect.ValueOf(&icons.ScreenshotTablet).Elem(),
"ScreenshotTabletFill": reflect.ValueOf(&icons.ScreenshotTabletFill).Elem(),
"ScrollableHeader": reflect.ValueOf(&icons.ScrollableHeader).Elem(),
"ScrollableHeaderFill": reflect.ValueOf(&icons.ScrollableHeaderFill).Elem(),
"Sd": reflect.ValueOf(&icons.Sd).Elem(),
"SdCard": reflect.ValueOf(&icons.SdCard).Elem(),
"SdCardAlert": reflect.ValueOf(&icons.SdCardAlert).Elem(),
"SdCardAlertFill": reflect.ValueOf(&icons.SdCardAlertFill).Elem(),
"SdCardFill": reflect.ValueOf(&icons.SdCardFill).Elem(),
"SdFill": reflect.ValueOf(&icons.SdFill).Elem(),
"Search": reflect.ValueOf(&icons.Search).Elem(),
"SearchCheck": reflect.ValueOf(&icons.SearchCheck).Elem(),
"SearchCheckFill": reflect.ValueOf(&icons.SearchCheckFill).Elem(),
"SearchFill": reflect.ValueOf(&icons.SearchFill).Elem(),
"SearchOff": reflect.ValueOf(&icons.SearchOff).Elem(),
"SearchOffFill": reflect.ValueOf(&icons.SearchOffFill).Elem(),
"Security": reflect.ValueOf(&icons.Security).Elem(),
"SecurityFill": reflect.ValueOf(&icons.SecurityFill).Elem(),
"SecurityUpdateGood": reflect.ValueOf(&icons.SecurityUpdateGood).Elem(),
"SecurityUpdateGoodFill": reflect.ValueOf(&icons.SecurityUpdateGoodFill).Elem(),
"SecurityUpdateWarning": reflect.ValueOf(&icons.SecurityUpdateWarning).Elem(),
"SecurityUpdateWarningFill": reflect.ValueOf(&icons.SecurityUpdateWarningFill).Elem(),
"Segment": reflect.ValueOf(&icons.Segment).Elem(),
"SegmentFill": reflect.ValueOf(&icons.SegmentFill).Elem(),
"Select": reflect.ValueOf(&icons.Select).Elem(),
"SelectAll": reflect.ValueOf(&icons.SelectAll).Elem(),
"SelectAllFill": reflect.ValueOf(&icons.SelectAllFill).Elem(),
"SelectCheckBox": reflect.ValueOf(&icons.SelectCheckBox).Elem(),
"SelectCheckBoxFill": reflect.ValueOf(&icons.SelectCheckBoxFill).Elem(),
"SelectFill": reflect.ValueOf(&icons.SelectFill).Elem(),
"SelectToSpeak": reflect.ValueOf(&icons.SelectToSpeak).Elem(),
"SelectToSpeakFill": reflect.ValueOf(&icons.SelectToSpeakFill).Elem(),
"SelectWindow": reflect.ValueOf(&icons.SelectWindow).Elem(),
"SelectWindowFill": reflect.ValueOf(&icons.SelectWindowFill).Elem(),
"SelectWindowOff": reflect.ValueOf(&icons.SelectWindowOff).Elem(),
"SelectWindowOffFill": reflect.ValueOf(&icons.SelectWindowOffFill).Elem(),
"Sell": reflect.ValueOf(&icons.Sell).Elem(),
"SellFill": reflect.ValueOf(&icons.SellFill).Elem(),
"Send": reflect.ValueOf(&icons.Send).Elem(),
"SendAndArchive": reflect.ValueOf(&icons.SendAndArchive).Elem(),
"SendAndArchiveFill": reflect.ValueOf(&icons.SendAndArchiveFill).Elem(),
"SendFill": reflect.ValueOf(&icons.SendFill).Elem(),
"SendMoney": reflect.ValueOf(&icons.SendMoney).Elem(),
"SendMoneyFill": reflect.ValueOf(&icons.SendMoneyFill).Elem(),
"SendTimeExtension": reflect.ValueOf(&icons.SendTimeExtension).Elem(),
"SendTimeExtensionFill": reflect.ValueOf(&icons.SendTimeExtensionFill).Elem(),
"SendToMobile": reflect.ValueOf(&icons.SendToMobile).Elem(),
"SendToMobileFill": reflect.ValueOf(&icons.SendToMobileFill).Elem(),
"Sensors": reflect.ValueOf(&icons.Sensors).Elem(),
"SensorsFill": reflect.ValueOf(&icons.SensorsFill).Elem(),
"SensorsOff": reflect.ValueOf(&icons.SensorsOff).Elem(),
"SensorsOffFill": reflect.ValueOf(&icons.SensorsOffFill).Elem(),
"SentimentCalm": reflect.ValueOf(&icons.SentimentCalm).Elem(),
"SentimentCalmFill": reflect.ValueOf(&icons.SentimentCalmFill).Elem(),
"SentimentContent": reflect.ValueOf(&icons.SentimentContent).Elem(),
"SentimentContentFill": reflect.ValueOf(&icons.SentimentContentFill).Elem(),
"SentimentDissatisfied": reflect.ValueOf(&icons.SentimentDissatisfied).Elem(),
"SentimentDissatisfiedFill": reflect.ValueOf(&icons.SentimentDissatisfiedFill).Elem(),
"SentimentExcited": reflect.ValueOf(&icons.SentimentExcited).Elem(),
"SentimentExcitedFill": reflect.ValueOf(&icons.SentimentExcitedFill).Elem(),
"SentimentExtremelyDissatisfied": reflect.ValueOf(&icons.SentimentExtremelyDissatisfied).Elem(),
"SentimentExtremelyDissatisfiedFill": reflect.ValueOf(&icons.SentimentExtremelyDissatisfiedFill).Elem(),
"SentimentFrustrated": reflect.ValueOf(&icons.SentimentFrustrated).Elem(),
"SentimentFrustratedFill": reflect.ValueOf(&icons.SentimentFrustratedFill).Elem(),
"SentimentNeutral": reflect.ValueOf(&icons.SentimentNeutral).Elem(),
"SentimentNeutralFill": reflect.ValueOf(&icons.SentimentNeutralFill).Elem(),
"SentimentSad": reflect.ValueOf(&icons.SentimentSad).Elem(),
"SentimentSadFill": reflect.ValueOf(&icons.SentimentSadFill).Elem(),
"SentimentSatisfied": reflect.ValueOf(&icons.SentimentSatisfied).Elem(),
"SentimentSatisfiedFill": reflect.ValueOf(&icons.SentimentSatisfiedFill).Elem(),
"SentimentStressed": reflect.ValueOf(&icons.SentimentStressed).Elem(),
"SentimentStressedFill": reflect.ValueOf(&icons.SentimentStressedFill).Elem(),
"SentimentVeryDissatisfied": reflect.ValueOf(&icons.SentimentVeryDissatisfied).Elem(),
"SentimentVeryDissatisfiedFill": reflect.ValueOf(&icons.SentimentVeryDissatisfiedFill).Elem(),
"SentimentVerySatisfied": reflect.ValueOf(&icons.SentimentVerySatisfied).Elem(),
"SentimentVerySatisfiedFill": reflect.ValueOf(&icons.SentimentVerySatisfiedFill).Elem(),
"SentimentWorried": reflect.ValueOf(&icons.SentimentWorried).Elem(),
"SentimentWorriedFill": reflect.ValueOf(&icons.SentimentWorriedFill).Elem(),
"Settings": reflect.ValueOf(&icons.Settings).Elem(),
"SettingsAccessibility": reflect.ValueOf(&icons.SettingsAccessibility).Elem(),
"SettingsAccessibilityFill": reflect.ValueOf(&icons.SettingsAccessibilityFill).Elem(),
"SettingsAccountBox": reflect.ValueOf(&icons.SettingsAccountBox).Elem(),
"SettingsAccountBoxFill": reflect.ValueOf(&icons.SettingsAccountBoxFill).Elem(),
"SettingsAlert": reflect.ValueOf(&icons.SettingsAlert).Elem(),
"SettingsAlertFill": reflect.ValueOf(&icons.SettingsAlertFill).Elem(),
"SettingsApplications": reflect.ValueOf(&icons.SettingsApplications).Elem(),
"SettingsApplicationsFill": reflect.ValueOf(&icons.SettingsApplicationsFill).Elem(),
"SettingsBRoll": reflect.ValueOf(&icons.SettingsBRoll).Elem(),
"SettingsBRollFill": reflect.ValueOf(&icons.SettingsBRollFill).Elem(),
"SettingsBackupRestore": reflect.ValueOf(&icons.SettingsBackupRestore).Elem(),
"SettingsBackupRestoreFill": reflect.ValueOf(&icons.SettingsBackupRestoreFill).Elem(),
"SettingsBluetooth": reflect.ValueOf(&icons.SettingsBluetooth).Elem(),
"SettingsBluetoothFill": reflect.ValueOf(&icons.SettingsBluetoothFill).Elem(),
"SettingsBrightness": reflect.ValueOf(&icons.SettingsBrightness).Elem(),
"SettingsBrightnessFill": reflect.ValueOf(&icons.SettingsBrightnessFill).Elem(),
"SettingsCell": reflect.ValueOf(&icons.SettingsCell).Elem(),
"SettingsCellFill": reflect.ValueOf(&icons.SettingsCellFill).Elem(),
"SettingsEthernet": reflect.ValueOf(&icons.SettingsEthernet).Elem(),
"SettingsEthernetFill": reflect.ValueOf(&icons.SettingsEthernetFill).Elem(),
"SettingsFill": reflect.ValueOf(&icons.SettingsFill).Elem(),
"SettingsInputAntenna": reflect.ValueOf(&icons.SettingsInputAntenna).Elem(),
"SettingsInputAntennaFill": reflect.ValueOf(&icons.SettingsInputAntennaFill).Elem(),
"SettingsInputComponent": reflect.ValueOf(&icons.SettingsInputComponent).Elem(),
"SettingsInputComponentFill": reflect.ValueOf(&icons.SettingsInputComponentFill).Elem(),
"SettingsPhone": reflect.ValueOf(&icons.SettingsPhone).Elem(),
"SettingsPhoneFill": reflect.ValueOf(&icons.SettingsPhoneFill).Elem(),
"SettingsPhotoCamera": reflect.ValueOf(&icons.SettingsPhotoCamera).Elem(),
"SettingsPhotoCameraFill": reflect.ValueOf(&icons.SettingsPhotoCameraFill).Elem(),
"SettingsPower": reflect.ValueOf(&icons.SettingsPower).Elem(),
"SettingsPowerFill": reflect.ValueOf(&icons.SettingsPowerFill).Elem(),
"SettingsRemote": reflect.ValueOf(&icons.SettingsRemote).Elem(),
"SettingsRemoteFill": reflect.ValueOf(&icons.SettingsRemoteFill).Elem(),
"SettingsVideoCamera": reflect.ValueOf(&icons.SettingsVideoCamera).Elem(),
"SettingsVideoCameraFill": reflect.ValueOf(&icons.SettingsVideoCameraFill).Elem(),
"SettingsVoice": reflect.ValueOf(&icons.SettingsVoice).Elem(),
"SettingsVoiceFill": reflect.ValueOf(&icons.SettingsVoiceFill).Elem(),
"Shadow": reflect.ValueOf(&icons.Shadow).Elem(),
"ShadowFill": reflect.ValueOf(&icons.ShadowFill).Elem(),
"ShapeLine": reflect.ValueOf(&icons.ShapeLine).Elem(),
"ShapeLineFill": reflect.ValueOf(&icons.ShapeLineFill).Elem(),
"Shapes": reflect.ValueOf(&icons.Shapes).Elem(),
"ShapesFill": reflect.ValueOf(&icons.ShapesFill).Elem(),
"Share": reflect.ValueOf(&icons.Share).Elem(),
"ShareFill": reflect.ValueOf(&icons.ShareFill).Elem(),
"ShareLocation": reflect.ValueOf(&icons.ShareLocation).Elem(),
"ShareLocationFill": reflect.ValueOf(&icons.ShareLocationFill).Elem(),
"ShareOff": reflect.ValueOf(&icons.ShareOff).Elem(),
"ShareOffFill": reflect.ValueOf(&icons.ShareOffFill).Elem(),
"ShareReviews": reflect.ValueOf(&icons.ShareReviews).Elem(),
"ShareReviewsFill": reflect.ValueOf(&icons.ShareReviewsFill).Elem(),
"ShareWindows": reflect.ValueOf(&icons.ShareWindows).Elem(),
"ShareWindowsFill": reflect.ValueOf(&icons.ShareWindowsFill).Elem(),
"SheetsRtl": reflect.ValueOf(&icons.SheetsRtl).Elem(),
"SheetsRtlFill": reflect.ValueOf(&icons.SheetsRtlFill).Elem(),
"ShelfAutoHide": reflect.ValueOf(&icons.ShelfAutoHide).Elem(),
"ShelfAutoHideFill": reflect.ValueOf(&icons.ShelfAutoHideFill).Elem(),
"ShelfPosition": reflect.ValueOf(&icons.ShelfPosition).Elem(),
"ShelfPositionFill": reflect.ValueOf(&icons.ShelfPositionFill).Elem(),
"Shelves": reflect.ValueOf(&icons.Shelves).Elem(),
"ShelvesFill": reflect.ValueOf(&icons.ShelvesFill).Elem(),
"Shield": reflect.ValueOf(&icons.Shield).Elem(),
"ShieldFill": reflect.ValueOf(&icons.ShieldFill).Elem(),
"ShieldLock": reflect.ValueOf(&icons.ShieldLock).Elem(),
"ShieldLockFill": reflect.ValueOf(&icons.ShieldLockFill).Elem(),
"ShieldLocked": reflect.ValueOf(&icons.ShieldLocked).Elem(),
"ShieldLockedFill": reflect.ValueOf(&icons.ShieldLockedFill).Elem(),
"ShieldMoon": reflect.ValueOf(&icons.ShieldMoon).Elem(),
"ShieldMoonFill": reflect.ValueOf(&icons.ShieldMoonFill).Elem(),
"ShieldPerson": reflect.ValueOf(&icons.ShieldPerson).Elem(),
"ShieldPersonFill": reflect.ValueOf(&icons.ShieldPersonFill).Elem(),
"ShieldWithHeart": reflect.ValueOf(&icons.ShieldWithHeart).Elem(),
"ShieldWithHeartFill": reflect.ValueOf(&icons.ShieldWithHeartFill).Elem(),
"ShieldWithHouse": reflect.ValueOf(&icons.ShieldWithHouse).Elem(),
"ShieldWithHouseFill": reflect.ValueOf(&icons.ShieldWithHouseFill).Elem(),
"Shift": reflect.ValueOf(&icons.Shift).Elem(),
"ShiftFill": reflect.ValueOf(&icons.ShiftFill).Elem(),
"ShiftLock": reflect.ValueOf(&icons.ShiftLock).Elem(),
"ShiftLockFill": reflect.ValueOf(&icons.ShiftLockFill).Elem(),
"Shop": reflect.ValueOf(&icons.Shop).Elem(),
"ShopFill": reflect.ValueOf(&icons.ShopFill).Elem(),
"ShopTwo": reflect.ValueOf(&icons.ShopTwo).Elem(),
"ShopTwoFill": reflect.ValueOf(&icons.ShopTwoFill).Elem(),
"ShoppingBag": reflect.ValueOf(&icons.ShoppingBag).Elem(),
"ShoppingBagFill": reflect.ValueOf(&icons.ShoppingBagFill).Elem(),
"ShoppingBasket": reflect.ValueOf(&icons.ShoppingBasket).Elem(),
"ShoppingBasketFill": reflect.ValueOf(&icons.ShoppingBasketFill).Elem(),
"ShoppingCart": reflect.ValueOf(&icons.ShoppingCart).Elem(),
"ShoppingCartCheckout": reflect.ValueOf(&icons.ShoppingCartCheckout).Elem(),
"ShoppingCartCheckoutFill": reflect.ValueOf(&icons.ShoppingCartCheckoutFill).Elem(),
"ShoppingCartFill": reflect.ValueOf(&icons.ShoppingCartFill).Elem(),
"ShortText": reflect.ValueOf(&icons.ShortText).Elem(),
"ShortTextFill": reflect.ValueOf(&icons.ShortTextFill).Elem(),
"ShowChart": reflect.ValueOf(&icons.ShowChart).Elem(),
"ShowChartFill": reflect.ValueOf(&icons.ShowChartFill).Elem(),
"Shuffle": reflect.ValueOf(&icons.Shuffle).Elem(),
"ShuffleFill": reflect.ValueOf(&icons.ShuffleFill).Elem(),
"ShuffleOn": reflect.ValueOf(&icons.ShuffleOn).Elem(),
"ShuffleOnFill": reflect.ValueOf(&icons.ShuffleOnFill).Elem(),
"SideNavigation": reflect.ValueOf(&icons.SideNavigation).Elem(),
"SideNavigationFill": reflect.ValueOf(&icons.SideNavigationFill).Elem(),
"SignLanguage": reflect.ValueOf(&icons.SignLanguage).Elem(),
"SignLanguageFill": reflect.ValueOf(&icons.SignLanguageFill).Elem(),
"SignalCellular0Bar": reflect.ValueOf(&icons.SignalCellular0Bar).Elem(),
"SignalCellular0BarFill": reflect.ValueOf(&icons.SignalCellular0BarFill).Elem(),
"SignalCellular1Bar": reflect.ValueOf(&icons.SignalCellular1Bar).Elem(),
"SignalCellular1BarFill": reflect.ValueOf(&icons.SignalCellular1BarFill).Elem(),
"SignalCellular2Bar": reflect.ValueOf(&icons.SignalCellular2Bar).Elem(),
"SignalCellular2BarFill": reflect.ValueOf(&icons.SignalCellular2BarFill).Elem(),
"SignalCellular3Bar": reflect.ValueOf(&icons.SignalCellular3Bar).Elem(),
"SignalCellular3BarFill": reflect.ValueOf(&icons.SignalCellular3BarFill).Elem(),
"SignalCellular4Bar": reflect.ValueOf(&icons.SignalCellular4Bar).Elem(),
"SignalCellular4BarFill": reflect.ValueOf(&icons.SignalCellular4BarFill).Elem(),
"SignalCellularAdd": reflect.ValueOf(&icons.SignalCellularAdd).Elem(),
"SignalCellularAddFill": reflect.ValueOf(&icons.SignalCellularAddFill).Elem(),
"SignalCellularAlt": reflect.ValueOf(&icons.SignalCellularAlt).Elem(),
"SignalCellularAlt1Bar": reflect.ValueOf(&icons.SignalCellularAlt1Bar).Elem(),
"SignalCellularAlt1BarFill": reflect.ValueOf(&icons.SignalCellularAlt1BarFill).Elem(),
"SignalCellularAlt2Bar": reflect.ValueOf(&icons.SignalCellularAlt2Bar).Elem(),
"SignalCellularAlt2BarFill": reflect.ValueOf(&icons.SignalCellularAlt2BarFill).Elem(),
"SignalCellularAltFill": reflect.ValueOf(&icons.SignalCellularAltFill).Elem(),
"SignalCellularConnectedNoInternet0Bar": reflect.ValueOf(&icons.SignalCellularConnectedNoInternet0Bar).Elem(),
"SignalCellularConnectedNoInternet0BarFill": reflect.ValueOf(&icons.SignalCellularConnectedNoInternet0BarFill).Elem(),
"SignalCellularConnectedNoInternet4Bar": reflect.ValueOf(&icons.SignalCellularConnectedNoInternet4Bar).Elem(),
"SignalCellularConnectedNoInternet4BarFill": reflect.ValueOf(&icons.SignalCellularConnectedNoInternet4BarFill).Elem(),
"SignalCellularNodata": reflect.ValueOf(&icons.SignalCellularNodata).Elem(),
"SignalCellularNodataFill": reflect.ValueOf(&icons.SignalCellularNodataFill).Elem(),
"SignalCellularNull": reflect.ValueOf(&icons.SignalCellularNull).Elem(),
"SignalCellularNullFill": reflect.ValueOf(&icons.SignalCellularNullFill).Elem(),
"SignalCellularOff": reflect.ValueOf(&icons.SignalCellularOff).Elem(),
"SignalCellularOffFill": reflect.ValueOf(&icons.SignalCellularOffFill).Elem(),
"SignalCellularPause": reflect.ValueOf(&icons.SignalCellularPause).Elem(),
"SignalCellularPauseFill": reflect.ValueOf(&icons.SignalCellularPauseFill).Elem(),
"SignalDisconnected": reflect.ValueOf(&icons.SignalDisconnected).Elem(),
"SignalDisconnectedFill": reflect.ValueOf(&icons.SignalDisconnectedFill).Elem(),
"SignalWifi0Bar": reflect.ValueOf(&icons.SignalWifi0Bar).Elem(),
"SignalWifi0BarFill": reflect.ValueOf(&icons.SignalWifi0BarFill).Elem(),
"SignalWifi4Bar": reflect.ValueOf(&icons.SignalWifi4Bar).Elem(),
"SignalWifi4BarFill": reflect.ValueOf(&icons.SignalWifi4BarFill).Elem(),
"SignalWifiBad": reflect.ValueOf(&icons.SignalWifiBad).Elem(),
"SignalWifiBadFill": reflect.ValueOf(&icons.SignalWifiBadFill).Elem(),
"SignalWifiOff": reflect.ValueOf(&icons.SignalWifiOff).Elem(),
"SignalWifiOffFill": reflect.ValueOf(&icons.SignalWifiOffFill).Elem(),
"SignalWifiStatusbarNotConnected": reflect.ValueOf(&icons.SignalWifiStatusbarNotConnected).Elem(),
"SignalWifiStatusbarNotConnectedFill": reflect.ValueOf(&icons.SignalWifiStatusbarNotConnectedFill).Elem(),
"SignalWifiStatusbarNull": reflect.ValueOf(&icons.SignalWifiStatusbarNull).Elem(),
"SignalWifiStatusbarNullFill": reflect.ValueOf(&icons.SignalWifiStatusbarNullFill).Elem(),
"Signature": reflect.ValueOf(&icons.Signature).Elem(),
"SignatureFill": reflect.ValueOf(&icons.SignatureFill).Elem(),
"SkipNext": reflect.ValueOf(&icons.SkipNext).Elem(),
"SkipNextFill": reflect.ValueOf(&icons.SkipNextFill).Elem(),
"SkipPrevious": reflect.ValueOf(&icons.SkipPrevious).Elem(),
"SkipPreviousFill": reflect.ValueOf(&icons.SkipPreviousFill).Elem(),
"SlideLibrary": reflect.ValueOf(&icons.SlideLibrary).Elem(),
"SlideLibraryFill": reflect.ValueOf(&icons.SlideLibraryFill).Elem(),
"Sliders": reflect.ValueOf(&icons.Sliders).Elem(),
"SlidersFill": reflect.ValueOf(&icons.SlidersFill).Elem(),
"Slideshow": reflect.ValueOf(&icons.Slideshow).Elem(),
"SlideshowFill": reflect.ValueOf(&icons.SlideshowFill).Elem(),
"SlowMotionVideo": reflect.ValueOf(&icons.SlowMotionVideo).Elem(),
"SlowMotionVideoFill": reflect.ValueOf(&icons.SlowMotionVideoFill).Elem(),
"SmartDisplay": reflect.ValueOf(&icons.SmartDisplay).Elem(),
"SmartDisplayFill": reflect.ValueOf(&icons.SmartDisplayFill).Elem(),
"SmartOutlet": reflect.ValueOf(&icons.SmartOutlet).Elem(),
"SmartOutletFill": reflect.ValueOf(&icons.SmartOutletFill).Elem(),
"SmartScreen": reflect.ValueOf(&icons.SmartScreen).Elem(),
"SmartScreenFill": reflect.ValueOf(&icons.SmartScreenFill).Elem(),
"SmartToy": reflect.ValueOf(&icons.SmartToy).Elem(),
"SmartToyFill": reflect.ValueOf(&icons.SmartToyFill).Elem(),
"Smartphone": reflect.ValueOf(&icons.Smartphone).Elem(),
"SmartphoneFill": reflect.ValueOf(&icons.SmartphoneFill).Elem(),
"SmbShare": reflect.ValueOf(&icons.SmbShare).Elem(),
"SmbShareFill": reflect.ValueOf(&icons.SmbShareFill).Elem(),
"Sms": reflect.ValueOf(&icons.Sms).Elem(),
"SmsFill": reflect.ValueOf(&icons.SmsFill).Elem(),
"SnippetFolder": reflect.ValueOf(&icons.SnippetFolder).Elem(),
"SnippetFolderFill": reflect.ValueOf(&icons.SnippetFolderFill).Elem(),
"Snooze": reflect.ValueOf(&icons.Snooze).Elem(),
"SnoozeFill": reflect.ValueOf(&icons.SnoozeFill).Elem(),
"SocialLeaderboard": reflect.ValueOf(&icons.SocialLeaderboard).Elem(),
"SocialLeaderboardFill": reflect.ValueOf(&icons.SocialLeaderboardFill).Elem(),
"Sort": reflect.ValueOf(&icons.Sort).Elem(),
"SortByAlpha": reflect.ValueOf(&icons.SortByAlpha).Elem(),
"SortByAlphaFill": reflect.ValueOf(&icons.SortByAlphaFill).Elem(),
"SortFill": reflect.ValueOf(&icons.SortFill).Elem(),
"Sos": reflect.ValueOf(&icons.Sos).Elem(),
"SosFill": reflect.ValueOf(&icons.SosFill).Elem(),
"SourceNotes": reflect.ValueOf(&icons.SourceNotes).Elem(),
"SourceNotesFill": reflect.ValueOf(&icons.SourceNotesFill).Elem(),
"South": reflect.ValueOf(&icons.South).Elem(),
"SouthAmerica": reflect.ValueOf(&icons.SouthAmerica).Elem(),
"SouthAmericaFill": reflect.ValueOf(&icons.SouthAmericaFill).Elem(),
"SouthEast": reflect.ValueOf(&icons.SouthEast).Elem(),
"SouthEastFill": reflect.ValueOf(&icons.SouthEastFill).Elem(),
"SouthFill": reflect.ValueOf(&icons.SouthFill).Elem(),
"SouthWest": reflect.ValueOf(&icons.SouthWest).Elem(),
"SouthWestFill": reflect.ValueOf(&icons.SouthWestFill).Elem(),
"SpaceBar": reflect.ValueOf(&icons.SpaceBar).Elem(),
"SpaceBarFill": reflect.ValueOf(&icons.SpaceBarFill).Elem(),
"SpaceDashboard": reflect.ValueOf(&icons.SpaceDashboard).Elem(),
"SpaceDashboardFill": reflect.ValueOf(&icons.SpaceDashboardFill).Elem(),
"SpatialAudio": reflect.ValueOf(&icons.SpatialAudio).Elem(),
"SpatialAudioFill": reflect.ValueOf(&icons.SpatialAudioFill).Elem(),
"SpatialAudioOff": reflect.ValueOf(&icons.SpatialAudioOff).Elem(),
"SpatialAudioOffFill": reflect.ValueOf(&icons.SpatialAudioOffFill).Elem(),
"SpatialTracking": reflect.ValueOf(&icons.SpatialTracking).Elem(),
"SpatialTrackingFill": reflect.ValueOf(&icons.SpatialTrackingFill).Elem(),
"Speaker": reflect.ValueOf(&icons.Speaker).Elem(),
"SpeakerFill": reflect.ValueOf(&icons.SpeakerFill).Elem(),
"SpeakerGroup": reflect.ValueOf(&icons.SpeakerGroup).Elem(),
"SpeakerGroupFill": reflect.ValueOf(&icons.SpeakerGroupFill).Elem(),
"SpeakerNotes": reflect.ValueOf(&icons.SpeakerNotes).Elem(),
"SpeakerNotesFill": reflect.ValueOf(&icons.SpeakerNotesFill).Elem(),
"SpeakerNotesOff": reflect.ValueOf(&icons.SpeakerNotesOff).Elem(),
"SpeakerNotesOffFill": reflect.ValueOf(&icons.SpeakerNotesOffFill).Elem(),
"SpeakerPhone": reflect.ValueOf(&icons.SpeakerPhone).Elem(),
"SpeakerPhoneFill": reflect.ValueOf(&icons.SpeakerPhoneFill).Elem(),
"SpecialCharacter": reflect.ValueOf(&icons.SpecialCharacter).Elem(),
"SpecialCharacterFill": reflect.ValueOf(&icons.SpecialCharacterFill).Elem(),
"SpeechToText": reflect.ValueOf(&icons.SpeechToText).Elem(),
"SpeechToTextFill": reflect.ValueOf(&icons.SpeechToTextFill).Elem(),
"Speed": reflect.ValueOf(&icons.Speed).Elem(),
"SpeedFill": reflect.ValueOf(&icons.SpeedFill).Elem(),
"Spellcheck": reflect.ValueOf(&icons.Spellcheck).Elem(),
"SpellcheckFill": reflect.ValueOf(&icons.SpellcheckFill).Elem(),
"Splitscreen": reflect.ValueOf(&icons.Splitscreen).Elem(),
"SplitscreenBottom": reflect.ValueOf(&icons.SplitscreenBottom).Elem(),
"SplitscreenBottomFill": reflect.ValueOf(&icons.SplitscreenBottomFill).Elem(),
"SplitscreenFill": reflect.ValueOf(&icons.SplitscreenFill).Elem(),
"SplitscreenLeft": reflect.ValueOf(&icons.SplitscreenLeft).Elem(),
"SplitscreenLeftFill": reflect.ValueOf(&icons.SplitscreenLeftFill).Elem(),
"SplitscreenRight": reflect.ValueOf(&icons.SplitscreenRight).Elem(),
"SplitscreenRightFill": reflect.ValueOf(&icons.SplitscreenRightFill).Elem(),
"SplitscreenTop": reflect.ValueOf(&icons.SplitscreenTop).Elem(),
"SplitscreenTopFill": reflect.ValueOf(&icons.SplitscreenTopFill).Elem(),
"Spoke": reflect.ValueOf(&icons.Spoke).Elem(),
"SpokeFill": reflect.ValueOf(&icons.SpokeFill).Elem(),
"Sports": reflect.ValueOf(&icons.Sports).Elem(),
"SportsFill": reflect.ValueOf(&icons.SportsFill).Elem(),
"Sprint": reflect.ValueOf(&icons.Sprint).Elem(),
"SprintFill": reflect.ValueOf(&icons.SprintFill).Elem(),
"Square": reflect.ValueOf(&icons.Square).Elem(),
"SquareFill": reflect.ValueOf(&icons.SquareFill).Elem(),
"SquareFoot": reflect.ValueOf(&icons.SquareFoot).Elem(),
"SquareFootFill": reflect.ValueOf(&icons.SquareFootFill).Elem(),
"Stack": reflect.ValueOf(&icons.Stack).Elem(),
"StackFill": reflect.ValueOf(&icons.StackFill).Elem(),
"StackOff": reflect.ValueOf(&icons.StackOff).Elem(),
"StackOffFill": reflect.ValueOf(&icons.StackOffFill).Elem(),
"StackStar": reflect.ValueOf(&icons.StackStar).Elem(),
"StackStarFill": reflect.ValueOf(&icons.StackStarFill).Elem(),
"StackedBarChart": reflect.ValueOf(&icons.StackedBarChart).Elem(),
"StackedBarChartFill": reflect.ValueOf(&icons.StackedBarChartFill).Elem(),
"StackedEmail": reflect.ValueOf(&icons.StackedEmail).Elem(),
"StackedEmailFill": reflect.ValueOf(&icons.StackedEmailFill).Elem(),
"StackedInbox": reflect.ValueOf(&icons.StackedInbox).Elem(),
"StackedInboxFill": reflect.ValueOf(&icons.StackedInboxFill).Elem(),
"StackedLineChart": reflect.ValueOf(&icons.StackedLineChart).Elem(),
"StackedLineChartFill": reflect.ValueOf(&icons.StackedLineChartFill).Elem(),
"Star": reflect.ValueOf(&icons.Star).Elem(),
"StarFill": reflect.ValueOf(&icons.StarFill).Elem(),
"StarHalf": reflect.ValueOf(&icons.StarHalf).Elem(),
"StarHalfFill": reflect.ValueOf(&icons.StarHalfFill).Elem(),
"StarRate": reflect.ValueOf(&icons.StarRate).Elem(),
"StarRateFill": reflect.ValueOf(&icons.StarRateFill).Elem(),
"StarRateHalf": reflect.ValueOf(&icons.StarRateHalf).Elem(),
"StarRateHalfFill": reflect.ValueOf(&icons.StarRateHalfFill).Elem(),
"Stars": reflect.ValueOf(&icons.Stars).Elem(),
"StarsFill": reflect.ValueOf(&icons.StarsFill).Elem(),
"Start": reflect.ValueOf(&icons.Start).Elem(),
"StartFill": reflect.ValueOf(&icons.StartFill).Elem(),
"Stat1": reflect.ValueOf(&icons.Stat1).Elem(),
"Stat1Fill": reflect.ValueOf(&icons.Stat1Fill).Elem(),
"Stat2": reflect.ValueOf(&icons.Stat2).Elem(),
"Stat2Fill": reflect.ValueOf(&icons.Stat2Fill).Elem(),
"Stat3": reflect.ValueOf(&icons.Stat3).Elem(),
"Stat3Fill": reflect.ValueOf(&icons.Stat3Fill).Elem(),
"StatMinus1": reflect.ValueOf(&icons.StatMinus1).Elem(),
"StatMinus1Fill": reflect.ValueOf(&icons.StatMinus1Fill).Elem(),
"StatMinus2": reflect.ValueOf(&icons.StatMinus2).Elem(),
"StatMinus2Fill": reflect.ValueOf(&icons.StatMinus2Fill).Elem(),
"StatMinus3": reflect.ValueOf(&icons.StatMinus3).Elem(),
"StatMinus3Fill": reflect.ValueOf(&icons.StatMinus3Fill).Elem(),
"StayCurrentLandscape": reflect.ValueOf(&icons.StayCurrentLandscape).Elem(),
"StayCurrentLandscapeFill": reflect.ValueOf(&icons.StayCurrentLandscapeFill).Elem(),
"StayCurrentPortrait": reflect.ValueOf(&icons.StayCurrentPortrait).Elem(),
"StayCurrentPortraitFill": reflect.ValueOf(&icons.StayCurrentPortraitFill).Elem(),
"StayPrimaryLandscape": reflect.ValueOf(&icons.StayPrimaryLandscape).Elem(),
"StayPrimaryLandscapeFill": reflect.ValueOf(&icons.StayPrimaryLandscapeFill).Elem(),
"StayPrimaryPortrait": reflect.ValueOf(&icons.StayPrimaryPortrait).Elem(),
"StayPrimaryPortraitFill": reflect.ValueOf(&icons.StayPrimaryPortraitFill).Elem(),
"Step": reflect.ValueOf(&icons.Step).Elem(),
"StepFill": reflect.ValueOf(&icons.StepFill).Elem(),
"StepInto": reflect.ValueOf(&icons.StepInto).Elem(),
"StepIntoFill": reflect.ValueOf(&icons.StepIntoFill).Elem(),
"StepOut": reflect.ValueOf(&icons.StepOut).Elem(),
"StepOutFill": reflect.ValueOf(&icons.StepOutFill).Elem(),
"StepOver": reflect.ValueOf(&icons.StepOver).Elem(),
"StepOverFill": reflect.ValueOf(&icons.StepOverFill).Elem(),
"Steppers": reflect.ValueOf(&icons.Steppers).Elem(),
"SteppersFill": reflect.ValueOf(&icons.SteppersFill).Elem(),
"Steps": reflect.ValueOf(&icons.Steps).Elem(),
"StepsFill": reflect.ValueOf(&icons.StepsFill).Elem(),
"StickyNote": reflect.ValueOf(&icons.StickyNote).Elem(),
"StickyNote2": reflect.ValueOf(&icons.StickyNote2).Elem(),
"StickyNote2Fill": reflect.ValueOf(&icons.StickyNote2Fill).Elem(),
"StickyNoteFill": reflect.ValueOf(&icons.StickyNoteFill).Elem(),
"Stop": reflect.ValueOf(&icons.Stop).Elem(),
"StopCircle": reflect.ValueOf(&icons.StopCircle).Elem(),
"StopCircleFill": reflect.ValueOf(&icons.StopCircleFill).Elem(),
"StopFill": reflect.ValueOf(&icons.StopFill).Elem(),
"StopScreenShare": reflect.ValueOf(&icons.StopScreenShare).Elem(),
"StopScreenShareFill": reflect.ValueOf(&icons.StopScreenShareFill).Elem(),
"Straight": reflect.ValueOf(&icons.Straight).Elem(),
"StraightFill": reflect.ValueOf(&icons.StraightFill).Elem(),
"Straighten": reflect.ValueOf(&icons.Straighten).Elem(),
"StraightenFill": reflect.ValueOf(&icons.StraightenFill).Elem(),
"Strategy": reflect.ValueOf(&icons.Strategy).Elem(),
"StrategyFill": reflect.ValueOf(&icons.StrategyFill).Elem(),
"Stream": reflect.ValueOf(&icons.Stream).Elem(),
"StreamApps": reflect.ValueOf(&icons.StreamApps).Elem(),
"StreamAppsFill": reflect.ValueOf(&icons.StreamAppsFill).Elem(),
"StreamFill": reflect.ValueOf(&icons.StreamFill).Elem(),
"Streetview": reflect.ValueOf(&icons.Streetview).Elem(),
"StreetviewFill": reflect.ValueOf(&icons.StreetviewFill).Elem(),
"StrikethroughS": reflect.ValueOf(&icons.StrikethroughS).Elem(),
"StrikethroughSFill": reflect.ValueOf(&icons.StrikethroughSFill).Elem(),
"StrokeFull": reflect.ValueOf(&icons.StrokeFull).Elem(),
"StrokeFullFill": reflect.ValueOf(&icons.StrokeFullFill).Elem(),
"StrokePartial": reflect.ValueOf(&icons.StrokePartial).Elem(),
"StrokePartialFill": reflect.ValueOf(&icons.StrokePartialFill).Elem(),
"Style": reflect.ValueOf(&icons.Style).Elem(),
"StyleFill": reflect.ValueOf(&icons.StyleFill).Elem(),
"Styler": reflect.ValueOf(&icons.Styler).Elem(),
"StylerFill": reflect.ValueOf(&icons.StylerFill).Elem(),
"Stylus": reflect.ValueOf(&icons.Stylus).Elem(),
"StylusFill": reflect.ValueOf(&icons.StylusFill).Elem(),
"StylusLaserPointer": reflect.ValueOf(&icons.StylusLaserPointer).Elem(),
"StylusLaserPointerFill": reflect.ValueOf(&icons.StylusLaserPointerFill).Elem(),
"StylusNote": reflect.ValueOf(&icons.StylusNote).Elem(),
"StylusNoteFill": reflect.ValueOf(&icons.StylusNoteFill).Elem(),
"SubdirectoryArrowLeft": reflect.ValueOf(&icons.SubdirectoryArrowLeft).Elem(),
"SubdirectoryArrowLeftFill": reflect.ValueOf(&icons.SubdirectoryArrowLeftFill).Elem(),
"SubdirectoryArrowRight": reflect.ValueOf(&icons.SubdirectoryArrowRight).Elem(),
"SubdirectoryArrowRightFill": reflect.ValueOf(&icons.SubdirectoryArrowRightFill).Elem(),
"Subheader": reflect.ValueOf(&icons.Subheader).Elem(),
"SubheaderFill": reflect.ValueOf(&icons.SubheaderFill).Elem(),
"Subject": reflect.ValueOf(&icons.Subject).Elem(),
"SubjectFill": reflect.ValueOf(&icons.SubjectFill).Elem(),
"Subscript": reflect.ValueOf(&icons.Subscript).Elem(),
"SubscriptFill": reflect.ValueOf(&icons.SubscriptFill).Elem(),
"Subtitles": reflect.ValueOf(&icons.Subtitles).Elem(),
"SubtitlesFill": reflect.ValueOf(&icons.SubtitlesFill).Elem(),
"SubtitlesOff": reflect.ValueOf(&icons.SubtitlesOff).Elem(),
"SubtitlesOffFill": reflect.ValueOf(&icons.SubtitlesOffFill).Elem(),
"Subway": reflect.ValueOf(&icons.Subway).Elem(),
"SubwayFill": reflect.ValueOf(&icons.SubwayFill).Elem(),
"Summarize": reflect.ValueOf(&icons.Summarize).Elem(),
"SummarizeFill": reflect.ValueOf(&icons.SummarizeFill).Elem(),
"Superscript": reflect.ValueOf(&icons.Superscript).Elem(),
"SuperscriptFill": reflect.ValueOf(&icons.SuperscriptFill).Elem(),
"SupervisorAccount": reflect.ValueOf(&icons.SupervisorAccount).Elem(),
"SupervisorAccountFill": reflect.ValueOf(&icons.SupervisorAccountFill).Elem(),
"Support": reflect.ValueOf(&icons.Support).Elem(),
"SupportAgent": reflect.ValueOf(&icons.SupportAgent).Elem(),
"SupportAgentFill": reflect.ValueOf(&icons.SupportAgentFill).Elem(),
"SupportFill": reflect.ValueOf(&icons.SupportFill).Elem(),
"SurroundSound": reflect.ValueOf(&icons.SurroundSound).Elem(),
"SurroundSoundFill": reflect.ValueOf(&icons.SurroundSoundFill).Elem(),
"SwapHoriz": reflect.ValueOf(&icons.SwapHoriz).Elem(),
"SwapHorizFill": reflect.ValueOf(&icons.SwapHorizFill).Elem(),
"SwapHorizontalCircle": reflect.ValueOf(&icons.SwapHorizontalCircle).Elem(),
"SwapHorizontalCircleFill": reflect.ValueOf(&icons.SwapHorizontalCircleFill).Elem(),
"SwapVert": reflect.ValueOf(&icons.SwapVert).Elem(),
"SwapVertFill": reflect.ValueOf(&icons.SwapVertFill).Elem(),
"SwapVerticalCircle": reflect.ValueOf(&icons.SwapVerticalCircle).Elem(),
"SwapVerticalCircleFill": reflect.ValueOf(&icons.SwapVerticalCircleFill).Elem(),
"Sweep": reflect.ValueOf(&icons.Sweep).Elem(),
"SweepFill": reflect.ValueOf(&icons.SweepFill).Elem(),
"Swipe": reflect.ValueOf(&icons.Swipe).Elem(),
"SwipeDown": reflect.ValueOf(&icons.SwipeDown).Elem(),
"SwipeDownAlt": reflect.ValueOf(&icons.SwipeDownAlt).Elem(),
"SwipeDownAltFill": reflect.ValueOf(&icons.SwipeDownAltFill).Elem(),
"SwipeDownFill": reflect.ValueOf(&icons.SwipeDownFill).Elem(),
"SwipeFill": reflect.ValueOf(&icons.SwipeFill).Elem(),
"SwipeLeft": reflect.ValueOf(&icons.SwipeLeft).Elem(),
"SwipeLeftAlt": reflect.ValueOf(&icons.SwipeLeftAlt).Elem(),
"SwipeLeftAltFill": reflect.ValueOf(&icons.SwipeLeftAltFill).Elem(),
"SwipeLeftFill": reflect.ValueOf(&icons.SwipeLeftFill).Elem(),
"SwipeRight": reflect.ValueOf(&icons.SwipeRight).Elem(),
"SwipeRightAlt": reflect.ValueOf(&icons.SwipeRightAlt).Elem(),
"SwipeRightAltFill": reflect.ValueOf(&icons.SwipeRightAltFill).Elem(),
"SwipeRightFill": reflect.ValueOf(&icons.SwipeRightFill).Elem(),
"SwipeUp": reflect.ValueOf(&icons.SwipeUp).Elem(),
"SwipeUpAlt": reflect.ValueOf(&icons.SwipeUpAlt).Elem(),
"SwipeUpAltFill": reflect.ValueOf(&icons.SwipeUpAltFill).Elem(),
"SwipeUpFill": reflect.ValueOf(&icons.SwipeUpFill).Elem(),
"SwipeVertical": reflect.ValueOf(&icons.SwipeVertical).Elem(),
"SwipeVerticalFill": reflect.ValueOf(&icons.SwipeVerticalFill).Elem(),
"Switch": reflect.ValueOf(&icons.Switch).Elem(),
"SwitchAccess": reflect.ValueOf(&icons.SwitchAccess).Elem(),
"SwitchAccessFill": reflect.ValueOf(&icons.SwitchAccessFill).Elem(),
"SwitchAccount": reflect.ValueOf(&icons.SwitchAccount).Elem(),
"SwitchAccountFill": reflect.ValueOf(&icons.SwitchAccountFill).Elem(),
"SwitchCamera": reflect.ValueOf(&icons.SwitchCamera).Elem(),
"SwitchCameraFill": reflect.ValueOf(&icons.SwitchCameraFill).Elem(),
"SwitchFill": reflect.ValueOf(&icons.SwitchFill).Elem(),
"SwitchLeft": reflect.ValueOf(&icons.SwitchLeft).Elem(),
"SwitchLeftFill": reflect.ValueOf(&icons.SwitchLeftFill).Elem(),
"SwitchRight": reflect.ValueOf(&icons.SwitchRight).Elem(),
"SwitchRightFill": reflect.ValueOf(&icons.SwitchRightFill).Elem(),
"SwitchVideo": reflect.ValueOf(&icons.SwitchVideo).Elem(),
"SwitchVideoFill": reflect.ValueOf(&icons.SwitchVideoFill).Elem(),
"Switches": reflect.ValueOf(&icons.Switches).Elem(),
"SwitchesFill": reflect.ValueOf(&icons.SwitchesFill).Elem(),
"Sync": reflect.ValueOf(&icons.Sync).Elem(),
"SyncAlt": reflect.ValueOf(&icons.SyncAlt).Elem(),
"SyncAltFill": reflect.ValueOf(&icons.SyncAltFill).Elem(),
"SyncDisabled": reflect.ValueOf(&icons.SyncDisabled).Elem(),
"SyncDisabledFill": reflect.ValueOf(&icons.SyncDisabledFill).Elem(),
"SyncFill": reflect.ValueOf(&icons.SyncFill).Elem(),
"SyncLock": reflect.ValueOf(&icons.SyncLock).Elem(),
"SyncLockFill": reflect.ValueOf(&icons.SyncLockFill).Elem(),
"SyncProblem": reflect.ValueOf(&icons.SyncProblem).Elem(),
"SyncProblemFill": reflect.ValueOf(&icons.SyncProblemFill).Elem(),
"SyncSavedLocally": reflect.ValueOf(&icons.SyncSavedLocally).Elem(),
"SyncSavedLocallyFill": reflect.ValueOf(&icons.SyncSavedLocallyFill).Elem(),
"SystemUpdate": reflect.ValueOf(&icons.SystemUpdate).Elem(),
"SystemUpdateFill": reflect.ValueOf(&icons.SystemUpdateFill).Elem(),
"Tab": reflect.ValueOf(&icons.Tab).Elem(),
"TabClose": reflect.ValueOf(&icons.TabClose).Elem(),
"TabCloseFill": reflect.ValueOf(&icons.TabCloseFill).Elem(),
"TabCloseRight": reflect.ValueOf(&icons.TabCloseRight).Elem(),
"TabCloseRightFill": reflect.ValueOf(&icons.TabCloseRightFill).Elem(),
"TabDuplicate": reflect.ValueOf(&icons.TabDuplicate).Elem(),
"TabDuplicateFill": reflect.ValueOf(&icons.TabDuplicateFill).Elem(),
"TabFill": reflect.ValueOf(&icons.TabFill).Elem(),
"TabGroup": reflect.ValueOf(&icons.TabGroup).Elem(),
"TabGroupFill": reflect.ValueOf(&icons.TabGroupFill).Elem(),
"TabMove": reflect.ValueOf(&icons.TabMove).Elem(),
"TabMoveFill": reflect.ValueOf(&icons.TabMoveFill).Elem(),
"TabNewRight": reflect.ValueOf(&icons.TabNewRight).Elem(),
"TabNewRightFill": reflect.ValueOf(&icons.TabNewRightFill).Elem(),
"TabRecent": reflect.ValueOf(&icons.TabRecent).Elem(),
"TabRecentFill": reflect.ValueOf(&icons.TabRecentFill).Elem(),
"TabUnselected": reflect.ValueOf(&icons.TabUnselected).Elem(),
"TabUnselectedFill": reflect.ValueOf(&icons.TabUnselectedFill).Elem(),
"Table": reflect.ValueOf(&icons.Table).Elem(),
"TableChart": reflect.ValueOf(&icons.TableChart).Elem(),
"TableChartFill": reflect.ValueOf(&icons.TableChartFill).Elem(),
"TableChartView": reflect.ValueOf(&icons.TableChartView).Elem(),
"TableChartViewFill": reflect.ValueOf(&icons.TableChartViewFill).Elem(),
"TableFill": reflect.ValueOf(&icons.TableFill).Elem(),
"TableRows": reflect.ValueOf(&icons.TableRows).Elem(),
"TableRowsFill": reflect.ValueOf(&icons.TableRowsFill).Elem(),
"TableRowsNarrow": reflect.ValueOf(&icons.TableRowsNarrow).Elem(),
"TableRowsNarrowFill": reflect.ValueOf(&icons.TableRowsNarrowFill).Elem(),
"TableView": reflect.ValueOf(&icons.TableView).Elem(),
"TableViewFill": reflect.ValueOf(&icons.TableViewFill).Elem(),
"Tablet": reflect.ValueOf(&icons.Tablet).Elem(),
"TabletAndroid": reflect.ValueOf(&icons.TabletAndroid).Elem(),
"TabletAndroidFill": reflect.ValueOf(&icons.TabletAndroidFill).Elem(),
"TabletFill": reflect.ValueOf(&icons.TabletFill).Elem(),
"TabletMac": reflect.ValueOf(&icons.TabletMac).Elem(),
"TabletMacFill": reflect.ValueOf(&icons.TabletMacFill).Elem(),
"Tabs": reflect.ValueOf(&icons.Tabs).Elem(),
"TabsFill": reflect.ValueOf(&icons.TabsFill).Elem(),
"Tag": reflect.ValueOf(&icons.Tag).Elem(),
"TagFill": reflect.ValueOf(&icons.TagFill).Elem(),
"TapAndPlay": reflect.ValueOf(&icons.TapAndPlay).Elem(),
"TapAndPlayFill": reflect.ValueOf(&icons.TapAndPlayFill).Elem(),
"Tapas": reflect.ValueOf(&icons.Tapas).Elem(),
"TapasFill": reflect.ValueOf(&icons.TapasFill).Elem(),
"Target": reflect.ValueOf(&icons.Target).Elem(),
"TargetFill": reflect.ValueOf(&icons.TargetFill).Elem(),
"Task": reflect.ValueOf(&icons.Task).Elem(),
"TaskAlt": reflect.ValueOf(&icons.TaskAlt).Elem(),
"TaskAltFill": reflect.ValueOf(&icons.TaskAltFill).Elem(),
"TaskFill": reflect.ValueOf(&icons.TaskFill).Elem(),
"Terminal": reflect.ValueOf(&icons.Terminal).Elem(),
"TerminalFill": reflect.ValueOf(&icons.TerminalFill).Elem(),
"Tex": reflect.ValueOf(&icons.Tex).Elem(),
"TextAd": reflect.ValueOf(&icons.TextAd).Elem(),
"TextAdFill": reflect.ValueOf(&icons.TextAdFill).Elem(),
"TextDecrease": reflect.ValueOf(&icons.TextDecrease).Elem(),
"TextDecreaseFill": reflect.ValueOf(&icons.TextDecreaseFill).Elem(),
"TextFields": reflect.ValueOf(&icons.TextFields).Elem(),
"TextFieldsFill": reflect.ValueOf(&icons.TextFieldsFill).Elem(),
"TextFormat": reflect.ValueOf(&icons.TextFormat).Elem(),
"TextFormatFill": reflect.ValueOf(&icons.TextFormatFill).Elem(),
"TextIncrease": reflect.ValueOf(&icons.TextIncrease).Elem(),
"TextIncreaseFill": reflect.ValueOf(&icons.TextIncreaseFill).Elem(),
"TextRotateUp": reflect.ValueOf(&icons.TextRotateUp).Elem(),
"TextRotateUpFill": reflect.ValueOf(&icons.TextRotateUpFill).Elem(),
"TextRotateVertical": reflect.ValueOf(&icons.TextRotateVertical).Elem(),
"TextRotateVerticalFill": reflect.ValueOf(&icons.TextRotateVerticalFill).Elem(),
"TextRotationAngledown": reflect.ValueOf(&icons.TextRotationAngledown).Elem(),
"TextRotationAngledownFill": reflect.ValueOf(&icons.TextRotationAngledownFill).Elem(),
"TextRotationAngleup": reflect.ValueOf(&icons.TextRotationAngleup).Elem(),
"TextRotationAngleupFill": reflect.ValueOf(&icons.TextRotationAngleupFill).Elem(),
"TextRotationDown": reflect.ValueOf(&icons.TextRotationDown).Elem(),
"TextRotationDownFill": reflect.ValueOf(&icons.TextRotationDownFill).Elem(),
"TextRotationNone": reflect.ValueOf(&icons.TextRotationNone).Elem(),
"TextRotationNoneFill": reflect.ValueOf(&icons.TextRotationNoneFill).Elem(),
"TextSelectEnd": reflect.ValueOf(&icons.TextSelectEnd).Elem(),
"TextSelectEndFill": reflect.ValueOf(&icons.TextSelectEndFill).Elem(),
"TextSelectJumpToBeginning": reflect.ValueOf(&icons.TextSelectJumpToBeginning).Elem(),
"TextSelectJumpToBeginningFill": reflect.ValueOf(&icons.TextSelectJumpToBeginningFill).Elem(),
"TextSelectJumpToEnd": reflect.ValueOf(&icons.TextSelectJumpToEnd).Elem(),
"TextSelectJumpToEndFill": reflect.ValueOf(&icons.TextSelectJumpToEndFill).Elem(),
"TextSelectMoveBackCharacter": reflect.ValueOf(&icons.TextSelectMoveBackCharacter).Elem(),
"TextSelectMoveBackCharacterFill": reflect.ValueOf(&icons.TextSelectMoveBackCharacterFill).Elem(),
"TextSelectMoveBackWord": reflect.ValueOf(&icons.TextSelectMoveBackWord).Elem(),
"TextSelectMoveBackWordFill": reflect.ValueOf(&icons.TextSelectMoveBackWordFill).Elem(),
"TextSelectMoveDown": reflect.ValueOf(&icons.TextSelectMoveDown).Elem(),
"TextSelectMoveDownFill": reflect.ValueOf(&icons.TextSelectMoveDownFill).Elem(),
"TextSelectMoveForwardCharacter": reflect.ValueOf(&icons.TextSelectMoveForwardCharacter).Elem(),
"TextSelectMoveForwardCharacterFill": reflect.ValueOf(&icons.TextSelectMoveForwardCharacterFill).Elem(),
"TextSelectMoveForwardWord": reflect.ValueOf(&icons.TextSelectMoveForwardWord).Elem(),
"TextSelectMoveForwardWordFill": reflect.ValueOf(&icons.TextSelectMoveForwardWordFill).Elem(),
"TextSelectMoveUp": reflect.ValueOf(&icons.TextSelectMoveUp).Elem(),
"TextSelectMoveUpFill": reflect.ValueOf(&icons.TextSelectMoveUpFill).Elem(),
"TextSelectStart": reflect.ValueOf(&icons.TextSelectStart).Elem(),
"TextSelectStartFill": reflect.ValueOf(&icons.TextSelectStartFill).Elem(),
"TextSnippet": reflect.ValueOf(&icons.TextSnippet).Elem(),
"TextSnippetFill": reflect.ValueOf(&icons.TextSnippetFill).Elem(),
"TextToSpeech": reflect.ValueOf(&icons.TextToSpeech).Elem(),
"TextToSpeechFill": reflect.ValueOf(&icons.TextToSpeechFill).Elem(),
"Texture": reflect.ValueOf(&icons.Texture).Elem(),
"TextureFill": reflect.ValueOf(&icons.TextureFill).Elem(),
"Thermometer": reflect.ValueOf(&icons.Thermometer).Elem(),
"ThermometerFill": reflect.ValueOf(&icons.ThermometerFill).Elem(),
"ThermometerGain": reflect.ValueOf(&icons.ThermometerGain).Elem(),
"ThermometerGainFill": reflect.ValueOf(&icons.ThermometerGainFill).Elem(),
"ThermometerLoss": reflect.ValueOf(&icons.ThermometerLoss).Elem(),
"ThermometerLossFill": reflect.ValueOf(&icons.ThermometerLossFill).Elem(),
"Thermostat": reflect.ValueOf(&icons.Thermostat).Elem(),
"ThermostatAuto": reflect.ValueOf(&icons.ThermostatAuto).Elem(),
"ThermostatAutoFill": reflect.ValueOf(&icons.ThermostatAutoFill).Elem(),
"ThermostatCarbon": reflect.ValueOf(&icons.ThermostatCarbon).Elem(),
"ThermostatCarbonFill": reflect.ValueOf(&icons.ThermostatCarbonFill).Elem(),
"ThermostatFill": reflect.ValueOf(&icons.ThermostatFill).Elem(),
"ThumbDown": reflect.ValueOf(&icons.ThumbDown).Elem(),
"ThumbDownFill": reflect.ValueOf(&icons.ThumbDownFill).Elem(),
"ThumbUp": reflect.ValueOf(&icons.ThumbUp).Elem(),
"ThumbUpFill": reflect.ValueOf(&icons.ThumbUpFill).Elem(),
"ThumbnailBar": reflect.ValueOf(&icons.ThumbnailBar).Elem(),
"ThumbnailBarFill": reflect.ValueOf(&icons.ThumbnailBarFill).Elem(),
"ThumbsUpDown": reflect.ValueOf(&icons.ThumbsUpDown).Elem(),
"ThumbsUpDownFill": reflect.ValueOf(&icons.ThumbsUpDownFill).Elem(),
"TimeAuto": reflect.ValueOf(&icons.TimeAuto).Elem(),
"TimeAutoFill": reflect.ValueOf(&icons.TimeAutoFill).Elem(),
"Timelapse": reflect.ValueOf(&icons.Timelapse).Elem(),
"TimelapseFill": reflect.ValueOf(&icons.TimelapseFill).Elem(),
"Timeline": reflect.ValueOf(&icons.Timeline).Elem(),
"TimelineFill": reflect.ValueOf(&icons.TimelineFill).Elem(),
"Timer": reflect.ValueOf(&icons.Timer).Elem(),
"Timer10": reflect.ValueOf(&icons.Timer10).Elem(),
"Timer10Alt1": reflect.ValueOf(&icons.Timer10Alt1).Elem(),
"Timer10Alt1Fill": reflect.ValueOf(&icons.Timer10Alt1Fill).Elem(),
"Timer10Fill": reflect.ValueOf(&icons.Timer10Fill).Elem(),
"Timer10Select": reflect.ValueOf(&icons.Timer10Select).Elem(),
"Timer10SelectFill": reflect.ValueOf(&icons.Timer10SelectFill).Elem(),
"Timer3": reflect.ValueOf(&icons.Timer3).Elem(),
"Timer3Alt1": reflect.ValueOf(&icons.Timer3Alt1).Elem(),
"Timer3Alt1Fill": reflect.ValueOf(&icons.Timer3Alt1Fill).Elem(),
"Timer3Fill": reflect.ValueOf(&icons.Timer3Fill).Elem(),
"Timer3Select": reflect.ValueOf(&icons.Timer3Select).Elem(),
"Timer3SelectFill": reflect.ValueOf(&icons.Timer3SelectFill).Elem(),
"TimerFill": reflect.ValueOf(&icons.TimerFill).Elem(),
"TimerOff": reflect.ValueOf(&icons.TimerOff).Elem(),
"TimerOffFill": reflect.ValueOf(&icons.TimerOffFill).Elem(),
"TireRepair": reflect.ValueOf(&icons.TireRepair).Elem(),
"TireRepairFill": reflect.ValueOf(&icons.TireRepairFill).Elem(),
"Title": reflect.ValueOf(&icons.Title).Elem(),
"TitleFill": reflect.ValueOf(&icons.TitleFill).Elem(),
"Toc": reflect.ValueOf(&icons.Toc).Elem(),
"TocFill": reflect.ValueOf(&icons.TocFill).Elem(),
"Today": reflect.ValueOf(&icons.Today).Elem(),
"TodayFill": reflect.ValueOf(&icons.TodayFill).Elem(),
"ToggleMid": reflect.ValueOf(&icons.ToggleMid).Elem(),
"ToggleOff": reflect.ValueOf(&icons.ToggleOff).Elem(),
"ToggleOffFill": reflect.ValueOf(&icons.ToggleOffFill).Elem(),
"ToggleOn": reflect.ValueOf(&icons.ToggleOn).Elem(),
"ToggleOnFill": reflect.ValueOf(&icons.ToggleOnFill).Elem(),
"Token": reflect.ValueOf(&icons.Token).Elem(),
"TokenFill": reflect.ValueOf(&icons.TokenFill).Elem(),
"Toml": reflect.ValueOf(&icons.Toml).Elem(),
"Tonality": reflect.ValueOf(&icons.Tonality).Elem(),
"TonalityFill": reflect.ValueOf(&icons.TonalityFill).Elem(),
"Toolbar": reflect.ValueOf(&icons.Toolbar).Elem(),
"ToolbarFill": reflect.ValueOf(&icons.ToolbarFill).Elem(),
"Tooltip": reflect.ValueOf(&icons.Tooltip).Elem(),
"TooltipFill": reflect.ValueOf(&icons.TooltipFill).Elem(),
"TopPanelClose": reflect.ValueOf(&icons.TopPanelClose).Elem(),
"TopPanelCloseFill": reflect.ValueOf(&icons.TopPanelCloseFill).Elem(),
"TopPanelOpen": reflect.ValueOf(&icons.TopPanelOpen).Elem(),
"TopPanelOpenFill": reflect.ValueOf(&icons.TopPanelOpenFill).Elem(),
"Topic": reflect.ValueOf(&icons.Topic).Elem(),
"TopicFill": reflect.ValueOf(&icons.TopicFill).Elem(),
"TouchApp": reflect.ValueOf(&icons.TouchApp).Elem(),
"TouchAppFill": reflect.ValueOf(&icons.TouchAppFill).Elem(),
"TouchpadMouse": reflect.ValueOf(&icons.TouchpadMouse).Elem(),
"TouchpadMouseFill": reflect.ValueOf(&icons.TouchpadMouseFill).Elem(),
"Tour": reflect.ValueOf(&icons.Tour).Elem(),
"TourFill": reflect.ValueOf(&icons.TourFill).Elem(),
"Toys": reflect.ValueOf(&icons.Toys).Elem(),
"ToysFan": reflect.ValueOf(&icons.ToysFan).Elem(),
"ToysFanFill": reflect.ValueOf(&icons.ToysFanFill).Elem(),
"ToysFill": reflect.ValueOf(&icons.ToysFill).Elem(),
"TrackChanges": reflect.ValueOf(&icons.TrackChanges).Elem(),
"TrackChangesFill": reflect.ValueOf(&icons.TrackChangesFill).Elem(),
"Traffic": reflect.ValueOf(&icons.Traffic).Elem(),
"TrafficFill": reflect.ValueOf(&icons.TrafficFill).Elem(),
"Transcribe": reflect.ValueOf(&icons.Transcribe).Elem(),
"TranscribeFill": reflect.ValueOf(&icons.TranscribeFill).Elem(),
"Transform": reflect.ValueOf(&icons.Transform).Elem(),
"TransformFill": reflect.ValueOf(&icons.TransformFill).Elem(),
"Translate": reflect.ValueOf(&icons.Translate).Elem(),
"TranslateFill": reflect.ValueOf(&icons.TranslateFill).Elem(),
"TrendingDown": reflect.ValueOf(&icons.TrendingDown).Elem(),
"TrendingDownFill": reflect.ValueOf(&icons.TrendingDownFill).Elem(),
"TrendingFlat": reflect.ValueOf(&icons.TrendingFlat).Elem(),
"TrendingFlatFill": reflect.ValueOf(&icons.TrendingFlatFill).Elem(),
"TrendingUp": reflect.ValueOf(&icons.TrendingUp).Elem(),
"TrendingUpFill": reflect.ValueOf(&icons.TrendingUpFill).Elem(),
"Trophy": reflect.ValueOf(&icons.Trophy).Elem(),
"TrophyFill": reflect.ValueOf(&icons.TrophyFill).Elem(),
"Troubleshoot": reflect.ValueOf(&icons.Troubleshoot).Elem(),
"TroubleshootFill": reflect.ValueOf(&icons.TroubleshootFill).Elem(),
"Tsv": reflect.ValueOf(&icons.Tsv).Elem(),
"TsvFill": reflect.ValueOf(&icons.TsvFill).Elem(),
"Tty": reflect.ValueOf(&icons.Tty).Elem(),
"TtyFill": reflect.ValueOf(&icons.TtyFill).Elem(),
"Tune": reflect.ValueOf(&icons.Tune).Elem(),
"TuneFill": reflect.ValueOf(&icons.TuneFill).Elem(),
"TurnLeft": reflect.ValueOf(&icons.TurnLeft).Elem(),
"TurnLeftFill": reflect.ValueOf(&icons.TurnLeftFill).Elem(),
"TurnRight": reflect.ValueOf(&icons.TurnRight).Elem(),
"TurnRightFill": reflect.ValueOf(&icons.TurnRightFill).Elem(),
"TurnSharpLeft": reflect.ValueOf(&icons.TurnSharpLeft).Elem(),
"TurnSharpLeftFill": reflect.ValueOf(&icons.TurnSharpLeftFill).Elem(),
"TurnSharpRight": reflect.ValueOf(&icons.TurnSharpRight).Elem(),
"TurnSharpRightFill": reflect.ValueOf(&icons.TurnSharpRightFill).Elem(),
"TurnSlightLeft": reflect.ValueOf(&icons.TurnSlightLeft).Elem(),
"TurnSlightLeftFill": reflect.ValueOf(&icons.TurnSlightLeftFill).Elem(),
"TurnSlightRight": reflect.ValueOf(&icons.TurnSlightRight).Elem(),
"TurnSlightRightFill": reflect.ValueOf(&icons.TurnSlightRightFill).Elem(),
"Tv": reflect.ValueOf(&icons.Tv).Elem(),
"TvFill": reflect.ValueOf(&icons.TvFill).Elem(),
"TvGen": reflect.ValueOf(&icons.TvGen).Elem(),
"TvGenFill": reflect.ValueOf(&icons.TvGenFill).Elem(),
"TvGuide": reflect.ValueOf(&icons.TvGuide).Elem(),
"TvGuideFill": reflect.ValueOf(&icons.TvGuideFill).Elem(),
"TvOff": reflect.ValueOf(&icons.TvOff).Elem(),
"TvOffFill": reflect.ValueOf(&icons.TvOffFill).Elem(),
"TvRemote": reflect.ValueOf(&icons.TvRemote).Elem(),
"TvRemoteFill": reflect.ValueOf(&icons.TvRemoteFill).Elem(),
"TvSignin": reflect.ValueOf(&icons.TvSignin).Elem(),
"TvSigninFill": reflect.ValueOf(&icons.TvSigninFill).Elem(),
"Type": reflect.ValueOf(&icons.Type).Elem(),
"Unarchive": reflect.ValueOf(&icons.Unarchive).Elem(),
"UnarchiveFill": reflect.ValueOf(&icons.UnarchiveFill).Elem(),
"Undo": reflect.ValueOf(&icons.Undo).Elem(),
"UndoFill": reflect.ValueOf(&icons.UndoFill).Elem(),
"UnfoldLess": reflect.ValueOf(&icons.UnfoldLess).Elem(),
"UnfoldLessDouble": reflect.ValueOf(&icons.UnfoldLessDouble).Elem(),
"UnfoldLessDoubleFill": reflect.ValueOf(&icons.UnfoldLessDoubleFill).Elem(),
"UnfoldLessFill": reflect.ValueOf(&icons.UnfoldLessFill).Elem(),
"UnfoldMore": reflect.ValueOf(&icons.UnfoldMore).Elem(),
"UnfoldMoreDouble": reflect.ValueOf(&icons.UnfoldMoreDouble).Elem(),
"UnfoldMoreDoubleFill": reflect.ValueOf(&icons.UnfoldMoreDoubleFill).Elem(),
"UnfoldMoreFill": reflect.ValueOf(&icons.UnfoldMoreFill).Elem(),
"Ungroup": reflect.ValueOf(&icons.Ungroup).Elem(),
"UngroupFill": reflect.ValueOf(&icons.UngroupFill).Elem(),
"UniversalCurrencyAlt": reflect.ValueOf(&icons.UniversalCurrencyAlt).Elem(),
"UniversalCurrencyAltFill": reflect.ValueOf(&icons.UniversalCurrencyAltFill).Elem(),
"Unknown2": reflect.ValueOf(&icons.Unknown2).Elem(),
"Unknown2Fill": reflect.ValueOf(&icons.Unknown2Fill).Elem(),
"Unknown5": reflect.ValueOf(&icons.Unknown5).Elem(),
"Unknown5Fill": reflect.ValueOf(&icons.Unknown5Fill).Elem(),
"UnknownDocument": reflect.ValueOf(&icons.UnknownDocument).Elem(),
"UnknownDocumentFill": reflect.ValueOf(&icons.UnknownDocumentFill).Elem(),
"UnknownMed": reflect.ValueOf(&icons.UnknownMed).Elem(),
"UnknownMedFill": reflect.ValueOf(&icons.UnknownMedFill).Elem(),
"Unpublished": reflect.ValueOf(&icons.Unpublished).Elem(),
"UnpublishedFill": reflect.ValueOf(&icons.UnpublishedFill).Elem(),
"Unsubscribe": reflect.ValueOf(&icons.Unsubscribe).Elem(),
"UnsubscribeFill": reflect.ValueOf(&icons.UnsubscribeFill).Elem(),
"Upcoming": reflect.ValueOf(&icons.Upcoming).Elem(),
"UpcomingFill": reflect.ValueOf(&icons.UpcomingFill).Elem(),
"Update": reflect.ValueOf(&icons.Update).Elem(),
"UpdateDisabled": reflect.ValueOf(&icons.UpdateDisabled).Elem(),
"UpdateDisabledFill": reflect.ValueOf(&icons.UpdateDisabledFill).Elem(),
"UpdateFill": reflect.ValueOf(&icons.UpdateFill).Elem(),
"Upgrade": reflect.ValueOf(&icons.Upgrade).Elem(),
"UpgradeFill": reflect.ValueOf(&icons.UpgradeFill).Elem(),
"Upload": reflect.ValueOf(&icons.Upload).Elem(),
"UploadFile": reflect.ValueOf(&icons.UploadFile).Elem(),
"UploadFileFill": reflect.ValueOf(&icons.UploadFileFill).Elem(),
"UploadFill": reflect.ValueOf(&icons.UploadFill).Elem(),
"Urology": reflect.ValueOf(&icons.Urology).Elem(),
"UrologyFill": reflect.ValueOf(&icons.UrologyFill).Elem(),
"Usb": reflect.ValueOf(&icons.Usb).Elem(),
"UsbFill": reflect.ValueOf(&icons.UsbFill).Elem(),
"UsbOff": reflect.ValueOf(&icons.UsbOff).Elem(),
"UsbOffFill": reflect.ValueOf(&icons.UsbOffFill).Elem(),
"Used": reflect.ValueOf(&icons.Used).Elem(),
"Valve": reflect.ValueOf(&icons.Valve).Elem(),
"ValveFill": reflect.ValueOf(&icons.ValveFill).Elem(),
"Variable": reflect.ValueOf(&icons.Variable).Elem(),
"Variables": reflect.ValueOf(&icons.Variables).Elem(),
"VariablesFill": reflect.ValueOf(&icons.VariablesFill).Elem(),
"Verified": reflect.ValueOf(&icons.Verified).Elem(),
"VerifiedFill": reflect.ValueOf(&icons.VerifiedFill).Elem(),
"VerifiedUser": reflect.ValueOf(&icons.VerifiedUser).Elem(),
"VerifiedUserFill": reflect.ValueOf(&icons.VerifiedUserFill).Elem(),
"VerticalAlignBottom": reflect.ValueOf(&icons.VerticalAlignBottom).Elem(),
"VerticalAlignBottomFill": reflect.ValueOf(&icons.VerticalAlignBottomFill).Elem(),
"VerticalAlignCenter": reflect.ValueOf(&icons.VerticalAlignCenter).Elem(),
"VerticalAlignCenterFill": reflect.ValueOf(&icons.VerticalAlignCenterFill).Elem(),
"VerticalAlignTop": reflect.ValueOf(&icons.VerticalAlignTop).Elem(),
"VerticalAlignTopFill": reflect.ValueOf(&icons.VerticalAlignTopFill).Elem(),
"VerticalDistribute": reflect.ValueOf(&icons.VerticalDistribute).Elem(),
"VerticalDistributeFill": reflect.ValueOf(&icons.VerticalDistributeFill).Elem(),
"VerticalShades": reflect.ValueOf(&icons.VerticalShades).Elem(),
"VerticalShadesClosed": reflect.ValueOf(&icons.VerticalShadesClosed).Elem(),
"VerticalShadesClosedFill": reflect.ValueOf(&icons.VerticalShadesClosedFill).Elem(),
"VerticalShadesFill": reflect.ValueOf(&icons.VerticalShadesFill).Elem(),
"VerticalSplit": reflect.ValueOf(&icons.VerticalSplit).Elem(),
"VerticalSplitFill": reflect.ValueOf(&icons.VerticalSplitFill).Elem(),
"VideoCall": reflect.ValueOf(&icons.VideoCall).Elem(),
"VideoCallFill": reflect.ValueOf(&icons.VideoCallFill).Elem(),
"VideoCameraBack": reflect.ValueOf(&icons.VideoCameraBack).Elem(),
"VideoCameraBackFill": reflect.ValueOf(&icons.VideoCameraBackFill).Elem(),
"VideoCameraFront": reflect.ValueOf(&icons.VideoCameraFront).Elem(),
"VideoCameraFrontFill": reflect.ValueOf(&icons.VideoCameraFrontFill).Elem(),
"VideoCameraFrontOff": reflect.ValueOf(&icons.VideoCameraFrontOff).Elem(),
"VideoCameraFrontOffFill": reflect.ValueOf(&icons.VideoCameraFrontOffFill).Elem(),
"VideoChat": reflect.ValueOf(&icons.VideoChat).Elem(),
"VideoChatFill": reflect.ValueOf(&icons.VideoChatFill).Elem(),
"VideoFile": reflect.ValueOf(&icons.VideoFile).Elem(),
"VideoFileFill": reflect.ValueOf(&icons.VideoFileFill).Elem(),
"VideoLabel": reflect.ValueOf(&icons.VideoLabel).Elem(),
"VideoLabelFill": reflect.ValueOf(&icons.VideoLabelFill).Elem(),
"VideoLibrary": reflect.ValueOf(&icons.VideoLibrary).Elem(),
"VideoLibraryFill": reflect.ValueOf(&icons.VideoLibraryFill).Elem(),
"VideoSearch": reflect.ValueOf(&icons.VideoSearch).Elem(),
"VideoSearchFill": reflect.ValueOf(&icons.VideoSearchFill).Elem(),
"VideoSettings": reflect.ValueOf(&icons.VideoSettings).Elem(),
"VideoSettingsFill": reflect.ValueOf(&icons.VideoSettingsFill).Elem(),
"VideoStable": reflect.ValueOf(&icons.VideoStable).Elem(),
"VideoStableFill": reflect.ValueOf(&icons.VideoStableFill).Elem(),
"Videocam": reflect.ValueOf(&icons.Videocam).Elem(),
"VideocamFill": reflect.ValueOf(&icons.VideocamFill).Elem(),
"VideocamOff": reflect.ValueOf(&icons.VideocamOff).Elem(),
"VideocamOffFill": reflect.ValueOf(&icons.VideocamOffFill).Elem(),
"VideogameAsset": reflect.ValueOf(&icons.VideogameAsset).Elem(),
"VideogameAssetFill": reflect.ValueOf(&icons.VideogameAssetFill).Elem(),
"VideogameAssetOff": reflect.ValueOf(&icons.VideogameAssetOff).Elem(),
"VideogameAssetOffFill": reflect.ValueOf(&icons.VideogameAssetOffFill).Elem(),
"ViewAgenda": reflect.ValueOf(&icons.ViewAgenda).Elem(),
"ViewAgendaFill": reflect.ValueOf(&icons.ViewAgendaFill).Elem(),
"ViewArray": reflect.ValueOf(&icons.ViewArray).Elem(),
"ViewArrayFill": reflect.ValueOf(&icons.ViewArrayFill).Elem(),
"ViewCarousel": reflect.ValueOf(&icons.ViewCarousel).Elem(),
"ViewCarouselFill": reflect.ValueOf(&icons.ViewCarouselFill).Elem(),
"ViewColumn": reflect.ValueOf(&icons.ViewColumn).Elem(),
"ViewColumn2": reflect.ValueOf(&icons.ViewColumn2).Elem(),
"ViewColumn2Fill": reflect.ValueOf(&icons.ViewColumn2Fill).Elem(),
"ViewColumnFill": reflect.ValueOf(&icons.ViewColumnFill).Elem(),
"ViewComfy": reflect.ValueOf(&icons.ViewComfy).Elem(),
"ViewComfyAlt": reflect.ValueOf(&icons.ViewComfyAlt).Elem(),
"ViewComfyAltFill": reflect.ValueOf(&icons.ViewComfyAltFill).Elem(),
"ViewComfyFill": reflect.ValueOf(&icons.ViewComfyFill).Elem(),
"ViewCompact": reflect.ValueOf(&icons.ViewCompact).Elem(),
"ViewCompactAlt": reflect.ValueOf(&icons.ViewCompactAlt).Elem(),
"ViewCompactAltFill": reflect.ValueOf(&icons.ViewCompactAltFill).Elem(),
"ViewCompactFill": reflect.ValueOf(&icons.ViewCompactFill).Elem(),
"ViewCozy": reflect.ValueOf(&icons.ViewCozy).Elem(),
"ViewCozyFill": reflect.ValueOf(&icons.ViewCozyFill).Elem(),
"ViewDay": reflect.ValueOf(&icons.ViewDay).Elem(),
"ViewDayFill": reflect.ValueOf(&icons.ViewDayFill).Elem(),
"ViewHeadline": reflect.ValueOf(&icons.ViewHeadline).Elem(),
"ViewHeadlineFill": reflect.ValueOf(&icons.ViewHeadlineFill).Elem(),
"ViewInAr": reflect.ValueOf(&icons.ViewInAr).Elem(),
"ViewInArFill": reflect.ValueOf(&icons.ViewInArFill).Elem(),
"ViewInArOff": reflect.ValueOf(&icons.ViewInArOff).Elem(),
"ViewInArOffFill": reflect.ValueOf(&icons.ViewInArOffFill).Elem(),
"ViewKanban": reflect.ValueOf(&icons.ViewKanban).Elem(),
"ViewKanbanFill": reflect.ValueOf(&icons.ViewKanbanFill).Elem(),
"ViewList": reflect.ValueOf(&icons.ViewList).Elem(),
"ViewListFill": reflect.ValueOf(&icons.ViewListFill).Elem(),
"ViewModule": reflect.ValueOf(&icons.ViewModule).Elem(),
"ViewModuleFill": reflect.ValueOf(&icons.ViewModuleFill).Elem(),
"ViewQuilt": reflect.ValueOf(&icons.ViewQuilt).Elem(),
"ViewQuiltFill": reflect.ValueOf(&icons.ViewQuiltFill).Elem(),
"ViewSidebar": reflect.ValueOf(&icons.ViewSidebar).Elem(),
"ViewSidebarFill": reflect.ValueOf(&icons.ViewSidebarFill).Elem(),
"ViewStream": reflect.ValueOf(&icons.ViewStream).Elem(),
"ViewStreamFill": reflect.ValueOf(&icons.ViewStreamFill).Elem(),
"ViewTimeline": reflect.ValueOf(&icons.ViewTimeline).Elem(),
"ViewTimelineFill": reflect.ValueOf(&icons.ViewTimelineFill).Elem(),
"ViewWeek": reflect.ValueOf(&icons.ViewWeek).Elem(),
"ViewWeekFill": reflect.ValueOf(&icons.ViewWeekFill).Elem(),
"Vignette": reflect.ValueOf(&icons.Vignette).Elem(),
"VignetteFill": reflect.ValueOf(&icons.VignetteFill).Elem(),
"Visibility": reflect.ValueOf(&icons.Visibility).Elem(),
"VisibilityFill": reflect.ValueOf(&icons.VisibilityFill).Elem(),
"VisibilityLock": reflect.ValueOf(&icons.VisibilityLock).Elem(),
"VisibilityLockFill": reflect.ValueOf(&icons.VisibilityLockFill).Elem(),
"VisibilityOff": reflect.ValueOf(&icons.VisibilityOff).Elem(),
"VisibilityOffFill": reflect.ValueOf(&icons.VisibilityOffFill).Elem(),
"VoiceChat": reflect.ValueOf(&icons.VoiceChat).Elem(),
"VoiceChatFill": reflect.ValueOf(&icons.VoiceChatFill).Elem(),
"VoiceOverOff": reflect.ValueOf(&icons.VoiceOverOff).Elem(),
"VoiceOverOffFill": reflect.ValueOf(&icons.VoiceOverOffFill).Elem(),
"Voicemail": reflect.ValueOf(&icons.Voicemail).Elem(),
"VoicemailFill": reflect.ValueOf(&icons.VoicemailFill).Elem(),
"VolumeDown": reflect.ValueOf(&icons.VolumeDown).Elem(),
"VolumeDownAlt": reflect.ValueOf(&icons.VolumeDownAlt).Elem(),
"VolumeDownAltFill": reflect.ValueOf(&icons.VolumeDownAltFill).Elem(),
"VolumeDownFill": reflect.ValueOf(&icons.VolumeDownFill).Elem(),
"VolumeMute": reflect.ValueOf(&icons.VolumeMute).Elem(),
"VolumeMuteFill": reflect.ValueOf(&icons.VolumeMuteFill).Elem(),
"VolumeOff": reflect.ValueOf(&icons.VolumeOff).Elem(),
"VolumeOffFill": reflect.ValueOf(&icons.VolumeOffFill).Elem(),
"VolumeUp": reflect.ValueOf(&icons.VolumeUp).Elem(),
"VolumeUpFill": reflect.ValueOf(&icons.VolumeUpFill).Elem(),
"VotingChip": reflect.ValueOf(&icons.VotingChip).Elem(),
"VotingChipFill": reflect.ValueOf(&icons.VotingChipFill).Elem(),
"VpnKey": reflect.ValueOf(&icons.VpnKey).Elem(),
"VpnKeyAlert": reflect.ValueOf(&icons.VpnKeyAlert).Elem(),
"VpnKeyAlertFill": reflect.ValueOf(&icons.VpnKeyAlertFill).Elem(),
"VpnKeyFill": reflect.ValueOf(&icons.VpnKeyFill).Elem(),
"VpnKeyOff": reflect.ValueOf(&icons.VpnKeyOff).Elem(),
"VpnKeyOffFill": reflect.ValueOf(&icons.VpnKeyOffFill).Elem(),
"VpnLock": reflect.ValueOf(&icons.VpnLock).Elem(),
"VpnLockFill": reflect.ValueOf(&icons.VpnLockFill).Elem(),
"Wallet": reflect.ValueOf(&icons.Wallet).Elem(),
"WalletFill": reflect.ValueOf(&icons.WalletFill).Elem(),
"Wallpaper": reflect.ValueOf(&icons.Wallpaper).Elem(),
"WallpaperFill": reflect.ValueOf(&icons.WallpaperFill).Elem(),
"WallpaperSlideshow": reflect.ValueOf(&icons.WallpaperSlideshow).Elem(),
"WallpaperSlideshowFill": reflect.ValueOf(&icons.WallpaperSlideshowFill).Elem(),
"Warehouse": reflect.ValueOf(&icons.Warehouse).Elem(),
"WarehouseFill": reflect.ValueOf(&icons.WarehouseFill).Elem(),
"Warning": reflect.ValueOf(&icons.Warning).Elem(),
"WarningFill": reflect.ValueOf(&icons.WarningFill).Elem(),
"WarningOff": reflect.ValueOf(&icons.WarningOff).Elem(),
"WarningOffFill": reflect.ValueOf(&icons.WarningOffFill).Elem(),
"Wash": reflect.ValueOf(&icons.Wash).Elem(),
"WashFill": reflect.ValueOf(&icons.WashFill).Elem(),
"Watch": reflect.ValueOf(&icons.Watch).Elem(),
"WatchButtonPress": reflect.ValueOf(&icons.WatchButtonPress).Elem(),
"WatchButtonPressFill": reflect.ValueOf(&icons.WatchButtonPressFill).Elem(),
"WatchFill": reflect.ValueOf(&icons.WatchFill).Elem(),
"WatchOff": reflect.ValueOf(&icons.WatchOff).Elem(),
"WatchOffFill": reflect.ValueOf(&icons.WatchOffFill).Elem(),
"WatchScreentime": reflect.ValueOf(&icons.WatchScreentime).Elem(),
"WatchScreentimeFill": reflect.ValueOf(&icons.WatchScreentimeFill).Elem(),
"WatchWake": reflect.ValueOf(&icons.WatchWake).Elem(),
"WatchWakeFill": reflect.ValueOf(&icons.WatchWakeFill).Elem(),
"Water": reflect.ValueOf(&icons.Water).Elem(),
"WaterFill": reflect.ValueOf(&icons.WaterFill).Elem(),
"WaterfallChart": reflect.ValueOf(&icons.WaterfallChart).Elem(),
"WaterfallChartFill": reflect.ValueOf(&icons.WaterfallChartFill).Elem(),
"WavingHand": reflect.ValueOf(&icons.WavingHand).Elem(),
"WavingHandFill": reflect.ValueOf(&icons.WavingHandFill).Elem(),
"Web": reflect.ValueOf(&icons.Web).Elem(),
"WebAsset": reflect.ValueOf(&icons.WebAsset).Elem(),
"WebAssetFill": reflect.ValueOf(&icons.WebAssetFill).Elem(),
"WebAssetOff": reflect.ValueOf(&icons.WebAssetOff).Elem(),
"WebAssetOffFill": reflect.ValueOf(&icons.WebAssetOffFill).Elem(),
"WebFill": reflect.ValueOf(&icons.WebFill).Elem(),
"WebStories": reflect.ValueOf(&icons.WebStories).Elem(),
"WebStoriesFill": reflect.ValueOf(&icons.WebStoriesFill).Elem(),
"Webhook": reflect.ValueOf(&icons.Webhook).Elem(),
"WebhookFill": reflect.ValueOf(&icons.WebhookFill).Elem(),
"Weekend": reflect.ValueOf(&icons.Weekend).Elem(),
"WeekendFill": reflect.ValueOf(&icons.WeekendFill).Elem(),
"West": reflect.ValueOf(&icons.West).Elem(),
"WestFill": reflect.ValueOf(&icons.WestFill).Elem(),
"Whatshot": reflect.ValueOf(&icons.Whatshot).Elem(),
"WhatshotFill": reflect.ValueOf(&icons.WhatshotFill).Elem(),
"Widgets": reflect.ValueOf(&icons.Widgets).Elem(),
"WidgetsFill": reflect.ValueOf(&icons.WidgetsFill).Elem(),
"Width": reflect.ValueOf(&icons.Width).Elem(),
"WidthFill": reflect.ValueOf(&icons.WidthFill).Elem(),
"WidthFull": reflect.ValueOf(&icons.WidthFull).Elem(),
"WidthFullFill": reflect.ValueOf(&icons.WidthFullFill).Elem(),
"WidthNormal": reflect.ValueOf(&icons.WidthNormal).Elem(),
"WidthNormalFill": reflect.ValueOf(&icons.WidthNormalFill).Elem(),
"WidthWide": reflect.ValueOf(&icons.WidthWide).Elem(),
"WidthWideFill": reflect.ValueOf(&icons.WidthWideFill).Elem(),
"Wifi": reflect.ValueOf(&icons.Wifi).Elem(),
"Wifi1Bar": reflect.ValueOf(&icons.Wifi1Bar).Elem(),
"Wifi1BarFill": reflect.ValueOf(&icons.Wifi1BarFill).Elem(),
"Wifi2Bar": reflect.ValueOf(&icons.Wifi2Bar).Elem(),
"Wifi2BarFill": reflect.ValueOf(&icons.Wifi2BarFill).Elem(),
"WifiAdd": reflect.ValueOf(&icons.WifiAdd).Elem(),
"WifiAddFill": reflect.ValueOf(&icons.WifiAddFill).Elem(),
"WifiFill": reflect.ValueOf(&icons.WifiFill).Elem(),
"WifiFind": reflect.ValueOf(&icons.WifiFind).Elem(),
"WifiFindFill": reflect.ValueOf(&icons.WifiFindFill).Elem(),
"WifiHome": reflect.ValueOf(&icons.WifiHome).Elem(),
"WifiHomeFill": reflect.ValueOf(&icons.WifiHomeFill).Elem(),
"WifiLock": reflect.ValueOf(&icons.WifiLock).Elem(),
"WifiLockFill": reflect.ValueOf(&icons.WifiLockFill).Elem(),
"WifiNotification": reflect.ValueOf(&icons.WifiNotification).Elem(),
"WifiNotificationFill": reflect.ValueOf(&icons.WifiNotificationFill).Elem(),
"WifiOff": reflect.ValueOf(&icons.WifiOff).Elem(),
"WifiOffFill": reflect.ValueOf(&icons.WifiOffFill).Elem(),
"Window": reflect.ValueOf(&icons.Window).Elem(),
"WindowClosed": reflect.ValueOf(&icons.WindowClosed).Elem(),
"WindowClosedFill": reflect.ValueOf(&icons.WindowClosedFill).Elem(),
"WindowFill": reflect.ValueOf(&icons.WindowFill).Elem(),
"WindowOpen": reflect.ValueOf(&icons.WindowOpen).Elem(),
"WindowOpenFill": reflect.ValueOf(&icons.WindowOpenFill).Elem(),
"WindowSensor": reflect.ValueOf(&icons.WindowSensor).Elem(),
"WindowSensorFill": reflect.ValueOf(&icons.WindowSensorFill).Elem(),
"Woman": reflect.ValueOf(&icons.Woman).Elem(),
"Woman2": reflect.ValueOf(&icons.Woman2).Elem(),
"Woman2Fill": reflect.ValueOf(&icons.Woman2Fill).Elem(),
"WomanFill": reflect.ValueOf(&icons.WomanFill).Elem(),
"Work": reflect.ValueOf(&icons.Work).Elem(),
"WorkAlert": reflect.ValueOf(&icons.WorkAlert).Elem(),
"WorkAlertFill": reflect.ValueOf(&icons.WorkAlertFill).Elem(),
"WorkFill": reflect.ValueOf(&icons.WorkFill).Elem(),
"WorkHistory": reflect.ValueOf(&icons.WorkHistory).Elem(),
"WorkHistoryFill": reflect.ValueOf(&icons.WorkHistoryFill).Elem(),
"WorkUpdate": reflect.ValueOf(&icons.WorkUpdate).Elem(),
"WorkUpdateFill": reflect.ValueOf(&icons.WorkUpdateFill).Elem(),
"WorkspacePremium": reflect.ValueOf(&icons.WorkspacePremium).Elem(),
"WorkspacePremiumFill": reflect.ValueOf(&icons.WorkspacePremiumFill).Elem(),
"Workspaces": reflect.ValueOf(&icons.Workspaces).Elem(),
"WorkspacesFill": reflect.ValueOf(&icons.WorkspacesFill).Elem(),
"WrapText": reflect.ValueOf(&icons.WrapText).Elem(),
"WrapTextFill": reflect.ValueOf(&icons.WrapTextFill).Elem(),
"WrongLocation": reflect.ValueOf(&icons.WrongLocation).Elem(),
"WrongLocationFill": reflect.ValueOf(&icons.WrongLocationFill).Elem(),
"Wysiwyg": reflect.ValueOf(&icons.Wysiwyg).Elem(),
"WysiwygFill": reflect.ValueOf(&icons.WysiwygFill).Elem(),
"YoutubeActivity": reflect.ValueOf(&icons.YoutubeActivity).Elem(),
"YoutubeActivityFill": reflect.ValueOf(&icons.YoutubeActivityFill).Elem(),
"YoutubeSearchedFor": reflect.ValueOf(&icons.YoutubeSearchedFor).Elem(),
"YoutubeSearchedForFill": reflect.ValueOf(&icons.YoutubeSearchedForFill).Elem(),
"ZoomIn": reflect.ValueOf(&icons.ZoomIn).Elem(),
"ZoomInFill": reflect.ValueOf(&icons.ZoomInFill).Elem(),
"ZoomInMap": reflect.ValueOf(&icons.ZoomInMap).Elem(),
"ZoomInMapFill": reflect.ValueOf(&icons.ZoomInMapFill).Elem(),
"ZoomOut": reflect.ValueOf(&icons.ZoomOut).Elem(),
"ZoomOutFill": reflect.ValueOf(&icons.ZoomOutFill).Elem(),
"ZoomOutMap": reflect.ValueOf(&icons.ZoomOutMap).Elem(),
"ZoomOutMapFill": reflect.ValueOf(&icons.ZoomOutMapFill).Elem(),
// type definitions
"Icon": reflect.ValueOf((*icons.Icon)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/keymap'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/keymap"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/keymap/keymap"] = map[string]reflect.Value{
// function, constant and variable definitions
"Abort": reflect.ValueOf(keymap.Abort),
"Accept": reflect.ValueOf(keymap.Accept),
"ActiveMap": reflect.ValueOf(&keymap.ActiveMap).Elem(),
"ActiveMapName": reflect.ValueOf(&keymap.ActiveMapName).Elem(),
"AvailableMaps": reflect.ValueOf(&keymap.AvailableMaps).Elem(),
"Backspace": reflect.ValueOf(keymap.Backspace),
"BackspaceWord": reflect.ValueOf(keymap.BackspaceWord),
"CancelSelect": reflect.ValueOf(keymap.CancelSelect),
"CloseAlt1": reflect.ValueOf(keymap.CloseAlt1),
"CloseAlt2": reflect.ValueOf(keymap.CloseAlt2),
"Complete": reflect.ValueOf(keymap.Complete),
"Copy": reflect.ValueOf(keymap.Copy),
"Cut": reflect.ValueOf(keymap.Cut),
"DefaultMap": reflect.ValueOf(&keymap.DefaultMap).Elem(),
"Delete": reflect.ValueOf(keymap.Delete),
"DeleteWord": reflect.ValueOf(keymap.DeleteWord),
"DocEnd": reflect.ValueOf(keymap.DocEnd),
"DocHome": reflect.ValueOf(keymap.DocHome),
"Duplicate": reflect.ValueOf(keymap.Duplicate),
"End": reflect.ValueOf(keymap.End),
"Enter": reflect.ValueOf(keymap.Enter),
"Find": reflect.ValueOf(keymap.Find),
"FocusNext": reflect.ValueOf(keymap.FocusNext),
"FocusPrev": reflect.ValueOf(keymap.FocusPrev),
"FunctionsN": reflect.ValueOf(keymap.FunctionsN),
"FunctionsValues": reflect.ValueOf(keymap.FunctionsValues),
"HistNext": reflect.ValueOf(keymap.HistNext),
"HistPrev": reflect.ValueOf(keymap.HistPrev),
"Home": reflect.ValueOf(keymap.Home),
"Insert": reflect.ValueOf(keymap.Insert),
"InsertAfter": reflect.ValueOf(keymap.InsertAfter),
"Jump": reflect.ValueOf(keymap.Jump),
"Kill": reflect.ValueOf(keymap.Kill),
"Lookup": reflect.ValueOf(keymap.Lookup),
"Menu": reflect.ValueOf(keymap.Menu),
"MoveDown": reflect.ValueOf(keymap.MoveDown),
"MoveLeft": reflect.ValueOf(keymap.MoveLeft),
"MoveRight": reflect.ValueOf(keymap.MoveRight),
"MoveUp": reflect.ValueOf(keymap.MoveUp),
"MultiA": reflect.ValueOf(keymap.MultiA),
"MultiB": reflect.ValueOf(keymap.MultiB),
"New": reflect.ValueOf(keymap.New),
"NewAlt1": reflect.ValueOf(keymap.NewAlt1),
"NewAlt2": reflect.ValueOf(keymap.NewAlt2),
"None": reflect.ValueOf(keymap.None),
"Of": reflect.ValueOf(keymap.Of),
"Open": reflect.ValueOf(keymap.Open),
"OpenAlt1": reflect.ValueOf(keymap.OpenAlt1),
"OpenAlt2": reflect.ValueOf(keymap.OpenAlt2),
"PageDown": reflect.ValueOf(keymap.PageDown),
"PageUp": reflect.ValueOf(keymap.PageUp),
"Paste": reflect.ValueOf(keymap.Paste),
"PasteHist": reflect.ValueOf(keymap.PasteHist),
"Recenter": reflect.ValueOf(keymap.Recenter),
"Redo": reflect.ValueOf(keymap.Redo),
"Refresh": reflect.ValueOf(keymap.Refresh),
"Replace": reflect.ValueOf(keymap.Replace),
"Save": reflect.ValueOf(keymap.Save),
"SaveAlt": reflect.ValueOf(keymap.SaveAlt),
"SaveAs": reflect.ValueOf(keymap.SaveAs),
"Search": reflect.ValueOf(keymap.Search),
"SelectAll": reflect.ValueOf(keymap.SelectAll),
"SelectMode": reflect.ValueOf(keymap.SelectMode),
"SetActiveMap": reflect.ValueOf(keymap.SetActiveMap),
"SetActiveMapName": reflect.ValueOf(keymap.SetActiveMapName),
"StandardMaps": reflect.ValueOf(&keymap.StandardMaps).Elem(),
"Transpose": reflect.ValueOf(keymap.Transpose),
"TransposeWord": reflect.ValueOf(keymap.TransposeWord),
"Undo": reflect.ValueOf(keymap.Undo),
"WinClose": reflect.ValueOf(keymap.WinClose),
"WinFocusNext": reflect.ValueOf(keymap.WinFocusNext),
"WinSnapshot": reflect.ValueOf(keymap.WinSnapshot),
"WordLeft": reflect.ValueOf(keymap.WordLeft),
"WordRight": reflect.ValueOf(keymap.WordRight),
"ZoomIn": reflect.ValueOf(keymap.ZoomIn),
"ZoomOut": reflect.ValueOf(keymap.ZoomOut),
// type definitions
"Functions": reflect.ValueOf((*keymap.Functions)(nil)),
"Map": reflect.ValueOf((*keymap.Map)(nil)),
"MapItem": reflect.ValueOf((*keymap.MapItem)(nil)),
"MapName": reflect.ValueOf((*keymap.MapName)(nil)),
"Maps": reflect.ValueOf((*keymap.Maps)(nil)),
"MapsItem": reflect.ValueOf((*keymap.MapsItem)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/math32'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/math32"
"go/constant"
"go/token"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/math32/math32"] = map[string]reflect.Value{
// function, constant and variable definitions
"Abs": reflect.ValueOf(math32.Abs),
"Acos": reflect.ValueOf(math32.Acos),
"Acosh": reflect.ValueOf(math32.Acosh),
"Asin": reflect.ValueOf(math32.Asin),
"Asinh": reflect.ValueOf(math32.Asinh),
"Atan": reflect.ValueOf(math32.Atan),
"Atan2": reflect.ValueOf(math32.Atan2),
"Atanh": reflect.ValueOf(math32.Atanh),
"B2": reflect.ValueOf(math32.B2),
"B2Empty": reflect.ValueOf(math32.B2Empty),
"B2FromFixed": reflect.ValueOf(math32.B2FromFixed),
"B2FromRect": reflect.ValueOf(math32.B2FromRect),
"B3": reflect.ValueOf(math32.B3),
"B3Empty": reflect.ValueOf(math32.B3Empty),
"BarycoordFromPoint": reflect.ValueOf(math32.BarycoordFromPoint),
"Cbrt": reflect.ValueOf(math32.Cbrt),
"Ceil": reflect.ValueOf(math32.Ceil),
"Clamp": reflect.ValueOf(math32.Clamp),
"ClampInt": reflect.ValueOf(math32.ClampInt),
"ContainsPoint": reflect.ValueOf(math32.ContainsPoint),
"CopyFloat32s": reflect.ValueOf(math32.CopyFloat32s),
"CopyFloat64s": reflect.ValueOf(math32.CopyFloat64s),
"Copysign": reflect.ValueOf(math32.Copysign),
"Cos": reflect.ValueOf(math32.Cos),
"Cosh": reflect.ValueOf(math32.Cosh),
"DegToRad": reflect.ValueOf(math32.DegToRad),
"DegToRadFactor": reflect.ValueOf(constant.MakeFromLiteral("0.0174532925199432957692369076848861271344287188854172545609719143893343406766598654219872641535175884721781352014070117566218413351995865581278454486637752166873317868068496601374750554214188014157116413116455078125", token.FLOAT, 0)),
"Dim": reflect.ValueOf(math32.Dim),
"DimsN": reflect.ValueOf(math32.DimsN),
"DimsValues": reflect.ValueOf(math32.DimsValues),
"E": reflect.ValueOf(constant.MakeFromLiteral("2.71828182845904523536028747135266249775724709369995957496696762566337824315673231520670375558666729784504486779277967997696994772644702281675346915668215131895555530285035761295375777990557253360748291015625", token.FLOAT, 0)),
"Erf": reflect.ValueOf(math32.Erf),
"Erfc": reflect.ValueOf(math32.Erfc),
"Erfcinv": reflect.ValueOf(math32.Erfcinv),
"Erfinv": reflect.ValueOf(math32.Erfinv),
"Exp": reflect.ValueOf(math32.Exp),
"Exp2": reflect.ValueOf(math32.Exp2),
"Expm1": reflect.ValueOf(math32.Expm1),
"FMA": reflect.ValueOf(math32.FMA),
"FastExp": reflect.ValueOf(math32.FastExp),
"FitGeomInWindow": reflect.ValueOf(math32.FitGeomInWindow),
"Floor": reflect.ValueOf(math32.Floor),
"Frexp": reflect.ValueOf(math32.Frexp),
"FromFixed": reflect.ValueOf(math32.FromFixed),
"FromPoint": reflect.ValueOf(math32.FromPoint),
"Gamma": reflect.ValueOf(math32.Gamma),
"Hypot": reflect.ValueOf(math32.Hypot),
"Identity2": reflect.ValueOf(math32.Identity2),
"Identity3": reflect.ValueOf(math32.Identity3),
"Identity4": reflect.ValueOf(math32.Identity4),
"Ilogb": reflect.ValueOf(math32.Ilogb),
"Inf": reflect.ValueOf(math32.Inf),
"Infinity": reflect.ValueOf(&math32.Infinity).Elem(),
"IntMultiple": reflect.ValueOf(math32.IntMultiple),
"IntMultipleGE": reflect.ValueOf(math32.IntMultipleGE),
"IsInf": reflect.ValueOf(math32.IsInf),
"IsNaN": reflect.ValueOf(math32.IsNaN),
"J0": reflect.ValueOf(math32.J0),
"J1": reflect.ValueOf(math32.J1),
"Jn": reflect.ValueOf(math32.Jn),
"Ldexp": reflect.ValueOf(math32.Ldexp),
"Lerp": reflect.ValueOf(math32.Lerp),
"Lgamma": reflect.ValueOf(math32.Lgamma),
"Ln10": reflect.ValueOf(constant.MakeFromLiteral("2.30258509299404568401799145468436420760110148862877297603332784146804725494827975466552490443295866962642372461496758838959542646932914211937012833592062802600362869664962772731087170541286468505859375", token.FLOAT, 0)),
"Ln2": reflect.ValueOf(constant.MakeFromLiteral("0.6931471805599453094172321214581765680755001343602552541206800092715999496201383079363438206637927920954189307729314303884387720696314608777673678644642390655170150035209453154294578780536539852619171142578125", token.FLOAT, 0)),
"Log": reflect.ValueOf(math32.Log),
"Log10": reflect.ValueOf(math32.Log10),
"Log10E": reflect.ValueOf(constant.MakeFromLiteral("0.43429448190325182765112891891660508229439700580366656611445378416636798190620320263064286300825210972160277489744884502676719847561509639618196799746596688688378591625127711495224502868950366973876953125", token.FLOAT, 0)),
"Log1p": reflect.ValueOf(math32.Log1p),
"Log2": reflect.ValueOf(math32.Log2),
"Log2E": reflect.ValueOf(constant.MakeFromLiteral("1.44269504088896340735992468100189213742664595415298593413544940772066427768997545329060870636212628972710992130324953463427359402479619301286929040235571747101382214539290471666532766903401352465152740478515625", token.FLOAT, 0)),
"Logb": reflect.ValueOf(math32.Logb),
"Matrix3FromMatrix2": reflect.ValueOf(math32.Matrix3FromMatrix2),
"Matrix3FromMatrix4": reflect.ValueOf(math32.Matrix3FromMatrix4),
"Matrix3Rotate2D": reflect.ValueOf(math32.Matrix3Rotate2D),
"Matrix3Scale2D": reflect.ValueOf(math32.Matrix3Scale2D),
"Matrix3Translate2D": reflect.ValueOf(math32.Matrix3Translate2D),
"Max": reflect.ValueOf(math32.Max),
"MaxFloat32": reflect.ValueOf(constant.MakeFromLiteral("340282346638528859811704183484516925440", token.FLOAT, 0)),
"MaxPos": reflect.ValueOf(math32.MaxPos),
"Min": reflect.ValueOf(math32.Min),
"MinPos": reflect.ValueOf(math32.MinPos),
"Mod": reflect.ValueOf(math32.Mod),
"Modf": reflect.ValueOf(math32.Modf),
"NaN": reflect.ValueOf(math32.NaN),
"NewArrayF32": reflect.ValueOf(math32.NewArrayF32),
"NewArrayU32": reflect.ValueOf(math32.NewArrayU32),
"NewEulerAnglesFromMatrix": reflect.ValueOf(math32.NewEulerAnglesFromMatrix),
"NewFrustum": reflect.ValueOf(math32.NewFrustum),
"NewFrustumFromMatrix": reflect.ValueOf(math32.NewFrustumFromMatrix),
"NewLine2": reflect.ValueOf(math32.NewLine2),
"NewLine3": reflect.ValueOf(math32.NewLine3),
"NewLookAt": reflect.ValueOf(math32.NewLookAt),
"NewPlane": reflect.ValueOf(math32.NewPlane),
"NewQuat": reflect.ValueOf(math32.NewQuat),
"NewQuatAxisAngle": reflect.ValueOf(math32.NewQuatAxisAngle),
"NewQuatEuler": reflect.ValueOf(math32.NewQuatEuler),
"NewRay": reflect.ValueOf(math32.NewRay),
"NewSphere": reflect.ValueOf(math32.NewSphere),
"NewTriangle": reflect.ValueOf(math32.NewTriangle),
"NewVector3Color": reflect.ValueOf(math32.NewVector3Color),
"NewVector4Color": reflect.ValueOf(math32.NewVector4Color),
"Nextafter": reflect.ValueOf(math32.Nextafter),
"Normal": reflect.ValueOf(math32.Normal),
"OtherDim": reflect.ValueOf(math32.OtherDim),
"ParseAngle32": reflect.ValueOf(math32.ParseAngle32),
"ParseFloat32": reflect.ValueOf(math32.ParseFloat32),
"Phi": reflect.ValueOf(constant.MakeFromLiteral("1.6180339887498948482045868343656381177203091798057628621354486119746080982153796619881086049305501566952211682590824739205931370737029882996587050475921915678674035433959321750307935872115194797515869140625", token.FLOAT, 0)),
"Pi": reflect.ValueOf(constant.MakeFromLiteral("3.141592653589793238462643383279502884197169399375105820974944594789982923695635954704435713335896673485663389728754819466702315787113662862838515639906529162340867271374644786874341662041842937469482421875", token.FLOAT, 0)),
"PointDim": reflect.ValueOf(math32.PointDim),
"PointsCheckN": reflect.ValueOf(math32.PointsCheckN),
"Pow": reflect.ValueOf(math32.Pow),
"Pow10": reflect.ValueOf(math32.Pow10),
"RadToDeg": reflect.ValueOf(math32.RadToDeg),
"RadToDegFactor": reflect.ValueOf(constant.MakeFromLiteral("57.295779513082320876798154814105170332405472466564321549160243902428585054360559672397261399470815487380868161395148776362013889310162423528726959840779630006155203887467652901221981665003113448619842529296875", token.FLOAT, 0)),
"ReadPoints": reflect.ValueOf(math32.ReadPoints),
"RectFromPosSizeMax": reflect.ValueOf(math32.RectFromPosSizeMax),
"RectFromPosSizeMin": reflect.ValueOf(math32.RectFromPosSizeMin),
"RectInNotEmpty": reflect.ValueOf(math32.RectInNotEmpty),
"Remainder": reflect.ValueOf(math32.Remainder),
"Rotate2D": reflect.ValueOf(math32.Rotate2D),
"Round": reflect.ValueOf(math32.Round),
"RoundToEven": reflect.ValueOf(math32.RoundToEven),
"SRGBFromLinear": reflect.ValueOf(math32.SRGBFromLinear),
"SRGBToLinear": reflect.ValueOf(math32.SRGBToLinear),
"Scale2D": reflect.ValueOf(math32.Scale2D),
"SetPointDim": reflect.ValueOf(math32.SetPointDim),
"Shear2D": reflect.ValueOf(math32.Shear2D),
"Sign": reflect.ValueOf(math32.Sign),
"Signbit": reflect.ValueOf(math32.Signbit),
"Sin": reflect.ValueOf(math32.Sin),
"Sincos": reflect.ValueOf(math32.Sincos),
"Sinh": reflect.ValueOf(math32.Sinh),
"Skew2D": reflect.ValueOf(math32.Skew2D),
"SmallestNonzeroFloat32": reflect.ValueOf(constant.MakeFromLiteral("1.40129846432481707092372958328991613128026194187651577175706828388979108268586060148663818836212158203125e-45", token.FLOAT, 0)),
"Sqrt": reflect.ValueOf(math32.Sqrt),
"Sqrt2": reflect.ValueOf(constant.MakeFromLiteral("1.414213562373095048801688724209698078569671875376948073176679739576083351575381440094441524123797447886801949755143139115339040409162552642832693297721230919563348109313505318596071447245776653289794921875", token.FLOAT, 0)),
"SqrtE": reflect.ValueOf(constant.MakeFromLiteral("1.64872127070012814684865078781416357165377610071014801157507931167328763229187870850146925823776361770041160388013884200789716007979526823569827080974091691342077871211546646890155898290686309337615966796875", token.FLOAT, 0)),
"SqrtPhi": reflect.ValueOf(constant.MakeFromLiteral("1.2720196495140689642524224617374914917156080418400962486166403754616080542166459302584536396369727769747312116100875915825863540562126478288118732191412003988041797518382391984914647764526307582855224609375", token.FLOAT, 0)),
"SqrtPi": reflect.ValueOf(constant.MakeFromLiteral("1.772453850905516027298167483341145182797549456122387128213807789740599698370237052541269446184448945647349951047154197675245574635259260134350885938555625028620527962319730619356050738133490085601806640625", token.FLOAT, 0)),
"Tan": reflect.ValueOf(math32.Tan),
"Tanh": reflect.ValueOf(math32.Tanh),
"ToFixed": reflect.ValueOf(math32.ToFixed),
"ToFixedPoint": reflect.ValueOf(math32.ToFixedPoint),
"Translate2D": reflect.ValueOf(math32.Translate2D),
"Trunc": reflect.ValueOf(math32.Trunc),
"Truncate": reflect.ValueOf(math32.Truncate),
"Truncate64": reflect.ValueOf(math32.Truncate64),
"Vec2": reflect.ValueOf(math32.Vec2),
"Vec2i": reflect.ValueOf(math32.Vec2i),
"Vec3": reflect.ValueOf(math32.Vec3),
"Vec3i": reflect.ValueOf(math32.Vec3i),
"Vec4": reflect.ValueOf(math32.Vec4),
"Vector2FromFixed": reflect.ValueOf(math32.Vector2FromFixed),
"Vector2Scalar": reflect.ValueOf(math32.Vector2Scalar),
"Vector2iScalar": reflect.ValueOf(math32.Vector2iScalar),
"Vector3FromVector4": reflect.ValueOf(math32.Vector3FromVector4),
"Vector3Scalar": reflect.ValueOf(math32.Vector3Scalar),
"Vector3iScalar": reflect.ValueOf(math32.Vector3iScalar),
"Vector4FromVector3": reflect.ValueOf(math32.Vector4FromVector3),
"Vector4Scalar": reflect.ValueOf(math32.Vector4Scalar),
"W": reflect.ValueOf(math32.W),
"X": reflect.ValueOf(math32.X),
"Y": reflect.ValueOf(math32.Y),
"Y0": reflect.ValueOf(math32.Y0),
"Y1": reflect.ValueOf(math32.Y1),
"Yn": reflect.ValueOf(math32.Yn),
"Z": reflect.ValueOf(math32.Z),
// type definitions
"ArrayF32": reflect.ValueOf((*math32.ArrayF32)(nil)),
"ArrayU32": reflect.ValueOf((*math32.ArrayU32)(nil)),
"Box2": reflect.ValueOf((*math32.Box2)(nil)),
"Box3": reflect.ValueOf((*math32.Box3)(nil)),
"Dims": reflect.ValueOf((*math32.Dims)(nil)),
"Frustum": reflect.ValueOf((*math32.Frustum)(nil)),
"Geom2DInt": reflect.ValueOf((*math32.Geom2DInt)(nil)),
"Line2": reflect.ValueOf((*math32.Line2)(nil)),
"Line3": reflect.ValueOf((*math32.Line3)(nil)),
"Matrix2": reflect.ValueOf((*math32.Matrix2)(nil)),
"Matrix3": reflect.ValueOf((*math32.Matrix3)(nil)),
"Matrix4": reflect.ValueOf((*math32.Matrix4)(nil)),
"Plane": reflect.ValueOf((*math32.Plane)(nil)),
"Quat": reflect.ValueOf((*math32.Quat)(nil)),
"Ray": reflect.ValueOf((*math32.Ray)(nil)),
"Sphere": reflect.ValueOf((*math32.Sphere)(nil)),
"Triangle": reflect.ValueOf((*math32.Triangle)(nil)),
"Vector2": reflect.ValueOf((*math32.Vector2)(nil)),
"Vector2i": reflect.ValueOf((*math32.Vector2i)(nil)),
"Vector3": reflect.ValueOf((*math32.Vector3)(nil)),
"Vector3i": reflect.ValueOf((*math32.Vector3i)(nil)),
"Vector4": reflect.ValueOf((*math32.Vector4)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/pages'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/pages"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/pages/pages"] = map[string]reflect.Value{
// function, constant and variable definitions
"ExampleHandler": reflect.ValueOf(pages.ExampleHandler),
"Examples": reflect.ValueOf(&pages.Examples).Elem(),
"NewPage": reflect.ValueOf(pages.NewPage),
"NumExamples": reflect.ValueOf(&pages.NumExamples).Elem(),
// type definitions
"Page": reflect.ValueOf((*pages.Page)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/paint'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/paint"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/paint/paint"] = map[string]reflect.Value{
// function, constant and variable definitions
"ClampBorderRadius": reflect.ValueOf(paint.ClampBorderRadius),
"EdgeBlurFactors": reflect.ValueOf(paint.EdgeBlurFactors),
"FindEllipseCenter": reflect.ValueOf(paint.FindEllipseCenter),
"FontAlts": reflect.ValueOf(paint.FontAlts),
"FontExts": reflect.ValueOf(&paint.FontExts).Elem(),
"FontFaceName": reflect.ValueOf(paint.FontFaceName),
"FontFallbacks": reflect.ValueOf(&paint.FontFallbacks).Elem(),
"FontInfoExample": reflect.ValueOf(&paint.FontInfoExample).Elem(),
"FontLibrary": reflect.ValueOf(&paint.FontLibrary).Elem(),
"FontPaths": reflect.ValueOf(&paint.FontPaths).Elem(),
"FontSerifMonoGuess": reflect.ValueOf(paint.FontSerifMonoGuess),
"FontStyleCSS": reflect.ValueOf(paint.FontStyleCSS),
"GaussianBlur": reflect.ValueOf(paint.GaussianBlur),
"GaussianBlurKernel1D": reflect.ValueOf(paint.GaussianBlurKernel1D),
"MaxDx": reflect.ValueOf(paint.MaxDx),
"NewContext": reflect.ValueOf(paint.NewContext),
"NewContextFromImage": reflect.ValueOf(paint.NewContextFromImage),
"NewContextFromRGBA": reflect.ValueOf(paint.NewContextFromRGBA),
"NextRuneAt": reflect.ValueOf(paint.NextRuneAt),
"OpenFont": reflect.ValueOf(paint.OpenFont),
"OpenFontFace": reflect.ValueOf(paint.OpenFontFace),
"SetHTMLSimpleTag": reflect.ValueOf(paint.SetHTMLSimpleTag),
"TextFontRenderMu": reflect.ValueOf(&paint.TextFontRenderMu).Elem(),
"TextWrapSizeEstimate": reflect.ValueOf(paint.TextWrapSizeEstimate),
// type definitions
"Context": reflect.ValueOf((*paint.Context)(nil)),
"FontInfo": reflect.ValueOf((*paint.FontInfo)(nil)),
"FontLib": reflect.ValueOf((*paint.FontLib)(nil)),
"Rune": reflect.ValueOf((*paint.Rune)(nil)),
"Span": reflect.ValueOf((*paint.Span)(nil)),
"State": reflect.ValueOf((*paint.State)(nil)),
"Text": reflect.ValueOf((*paint.Text)(nil)),
"TextLink": reflect.ValueOf((*paint.TextLink)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/plot/plotcore'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/plot/plotcore"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/plot/plotcore/plotcore"] = map[string]reflect.Value{
// function, constant and variable definitions
"Bar": reflect.ValueOf(plotcore.Bar),
"FixMax": reflect.ValueOf(plotcore.FixMax),
"FixMin": reflect.ValueOf(plotcore.FixMin),
"FloatMax": reflect.ValueOf(plotcore.FloatMax),
"FloatMin": reflect.ValueOf(plotcore.FloatMin),
"NewPlot": reflect.ValueOf(plotcore.NewPlot),
"NewPlotEditor": reflect.ValueOf(plotcore.NewPlotEditor),
"NewSubPlot": reflect.ValueOf(plotcore.NewSubPlot),
"Off": reflect.ValueOf(plotcore.Off),
"On": reflect.ValueOf(plotcore.On),
"PlotTypesN": reflect.ValueOf(plotcore.PlotTypesN),
"PlotTypesValues": reflect.ValueOf(plotcore.PlotTypesValues),
"XY": reflect.ValueOf(plotcore.XY),
// type definitions
"ColumnOptions": reflect.ValueOf((*plotcore.ColumnOptions)(nil)),
"Plot": reflect.ValueOf((*plotcore.Plot)(nil)),
"PlotEditor": reflect.ValueOf((*plotcore.PlotEditor)(nil)),
"PlotOptions": reflect.ValueOf((*plotcore.PlotOptions)(nil)),
"PlotTypes": reflect.ValueOf((*plotcore.PlotTypes)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/plot/plots'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/plot/plots"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/plot/plots/plots"] = map[string]reflect.Value{
// function, constant and variable definitions
"AddTableLine": reflect.ValueOf(plots.AddTableLine),
"AddTableLinePoints": reflect.ValueOf(plots.AddTableLinePoints),
"Box": reflect.ValueOf(plots.Box),
"Circle": reflect.ValueOf(plots.Circle),
"Cross": reflect.ValueOf(plots.Cross),
"DrawBox": reflect.ValueOf(plots.DrawBox),
"DrawCircle": reflect.ValueOf(plots.DrawCircle),
"DrawCross": reflect.ValueOf(plots.DrawCross),
"DrawPlus": reflect.ValueOf(plots.DrawPlus),
"DrawPyramid": reflect.ValueOf(plots.DrawPyramid),
"DrawRing": reflect.ValueOf(plots.DrawRing),
"DrawShape": reflect.ValueOf(plots.DrawShape),
"DrawSquare": reflect.ValueOf(plots.DrawSquare),
"DrawTriangle": reflect.ValueOf(plots.DrawTriangle),
"MidStep": reflect.ValueOf(plots.MidStep),
"NewBarChart": reflect.ValueOf(plots.NewBarChart),
"NewLabels": reflect.ValueOf(plots.NewLabels),
"NewLine": reflect.ValueOf(plots.NewLine),
"NewLinePoints": reflect.ValueOf(plots.NewLinePoints),
"NewScatter": reflect.ValueOf(plots.NewScatter),
"NewTableXYer": reflect.ValueOf(plots.NewTableXYer),
"NewXErrorBars": reflect.ValueOf(plots.NewXErrorBars),
"NewYErrorBars": reflect.ValueOf(plots.NewYErrorBars),
"NoStep": reflect.ValueOf(plots.NoStep),
"Plus": reflect.ValueOf(plots.Plus),
"PostStep": reflect.ValueOf(plots.PostStep),
"PreStep": reflect.ValueOf(plots.PreStep),
"Pyramid": reflect.ValueOf(plots.Pyramid),
"Ring": reflect.ValueOf(plots.Ring),
"ShapesN": reflect.ValueOf(plots.ShapesN),
"ShapesValues": reflect.ValueOf(plots.ShapesValues),
"Square": reflect.ValueOf(plots.Square),
"StepKindN": reflect.ValueOf(plots.StepKindN),
"StepKindValues": reflect.ValueOf(plots.StepKindValues),
"TableColumnIndex": reflect.ValueOf(plots.TableColumnIndex),
"Triangle": reflect.ValueOf(plots.Triangle),
// type definitions
"BarChart": reflect.ValueOf((*plots.BarChart)(nil)),
"Errors": reflect.ValueOf((*plots.Errors)(nil)),
"Labels": reflect.ValueOf((*plots.Labels)(nil)),
"Line": reflect.ValueOf((*plots.Line)(nil)),
"Scatter": reflect.ValueOf((*plots.Scatter)(nil)),
"Shapes": reflect.ValueOf((*plots.Shapes)(nil)),
"StepKind": reflect.ValueOf((*plots.StepKind)(nil)),
"Table": reflect.ValueOf((*plots.Table)(nil)),
"TableXYer": reflect.ValueOf((*plots.TableXYer)(nil)),
"XErrorBars": reflect.ValueOf((*plots.XErrorBars)(nil)),
"XErrorer": reflect.ValueOf((*plots.XErrorer)(nil)),
"XErrors": reflect.ValueOf((*plots.XErrors)(nil)),
"XYLabeler": reflect.ValueOf((*plots.XYLabeler)(nil)),
"XYLabels": reflect.ValueOf((*plots.XYLabels)(nil)),
"YErrorBars": reflect.ValueOf((*plots.YErrorBars)(nil)),
"YErrorer": reflect.ValueOf((*plots.YErrorer)(nil)),
"YErrors": reflect.ValueOf((*plots.YErrors)(nil)),
// interface wrapper definitions
"_Table": reflect.ValueOf((*_cogentcore_org_core_plot_plots_Table)(nil)),
"_XErrorer": reflect.ValueOf((*_cogentcore_org_core_plot_plots_XErrorer)(nil)),
"_XYLabeler": reflect.ValueOf((*_cogentcore_org_core_plot_plots_XYLabeler)(nil)),
"_YErrorer": reflect.ValueOf((*_cogentcore_org_core_plot_plots_YErrorer)(nil)),
}
}
// _cogentcore_org_core_plot_plots_Table is an interface wrapper for Table type
type _cogentcore_org_core_plot_plots_Table struct {
IValue interface{}
WColumnName func(i int) string
WNumColumns func() int
WNumRows func() int
WPlotData func(column int, row int) float32
}
func (W _cogentcore_org_core_plot_plots_Table) ColumnName(i int) string { return W.WColumnName(i) }
func (W _cogentcore_org_core_plot_plots_Table) NumColumns() int { return W.WNumColumns() }
func (W _cogentcore_org_core_plot_plots_Table) NumRows() int { return W.WNumRows() }
func (W _cogentcore_org_core_plot_plots_Table) PlotData(column int, row int) float32 {
return W.WPlotData(column, row)
}
// _cogentcore_org_core_plot_plots_XErrorer is an interface wrapper for XErrorer type
type _cogentcore_org_core_plot_plots_XErrorer struct {
IValue interface{}
WXError func(i int) (low float32, high float32)
}
func (W _cogentcore_org_core_plot_plots_XErrorer) XError(i int) (low float32, high float32) {
return W.WXError(i)
}
// _cogentcore_org_core_plot_plots_XYLabeler is an interface wrapper for XYLabeler type
type _cogentcore_org_core_plot_plots_XYLabeler struct {
IValue interface{}
WLabel func(i int) string
WLen func() int
WXY func(i int) (x float32, y float32)
}
func (W _cogentcore_org_core_plot_plots_XYLabeler) Label(i int) string { return W.WLabel(i) }
func (W _cogentcore_org_core_plot_plots_XYLabeler) Len() int { return W.WLen() }
func (W _cogentcore_org_core_plot_plots_XYLabeler) XY(i int) (x float32, y float32) { return W.WXY(i) }
// _cogentcore_org_core_plot_plots_YErrorer is an interface wrapper for YErrorer type
type _cogentcore_org_core_plot_plots_YErrorer struct {
IValue interface{}
WYError func(i int) (float32, float32)
}
func (W _cogentcore_org_core_plot_plots_YErrorer) YError(i int) (float32, float32) {
return W.WYError(i)
}
// Code generated by 'yaegi extract cogentcore.org/core/plot'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/plot"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/plot/plot"] = map[string]reflect.Value{
// function, constant and variable definitions
"CheckFloats": reflect.ValueOf(plot.CheckFloats),
"CheckNaNs": reflect.ValueOf(plot.CheckNaNs),
"CopyValues": reflect.ValueOf(plot.CopyValues),
"CopyXYZs": reflect.ValueOf(plot.CopyXYZs),
"CopyXYs": reflect.ValueOf(plot.CopyXYs),
"DefaultFontFamily": reflect.ValueOf(&plot.DefaultFontFamily).Elem(),
"ErrInfinity": reflect.ValueOf(&plot.ErrInfinity).Elem(),
"ErrNoData": reflect.ValueOf(&plot.ErrNoData).Elem(),
"New": reflect.ValueOf(plot.New),
"PlotXYs": reflect.ValueOf(plot.PlotXYs),
"Range": reflect.ValueOf(plot.Range),
"UTCUnixTime": reflect.ValueOf(&plot.UTCUnixTime).Elem(),
"UnixTimeIn": reflect.ValueOf(plot.UnixTimeIn),
"XYRange": reflect.ValueOf(plot.XYRange),
// type definitions
"Axis": reflect.ValueOf((*plot.Axis)(nil)),
"ConstantTicks": reflect.ValueOf((*plot.ConstantTicks)(nil)),
"DataRanger": reflect.ValueOf((*plot.DataRanger)(nil)),
"DefaultTicks": reflect.ValueOf((*plot.DefaultTicks)(nil)),
"InvertedScale": reflect.ValueOf((*plot.InvertedScale)(nil)),
"Labeler": reflect.ValueOf((*plot.Labeler)(nil)),
"Legend": reflect.ValueOf((*plot.Legend)(nil)),
"LegendEntry": reflect.ValueOf((*plot.LegendEntry)(nil)),
"LegendPosition": reflect.ValueOf((*plot.LegendPosition)(nil)),
"LineStyle": reflect.ValueOf((*plot.LineStyle)(nil)),
"LinearScale": reflect.ValueOf((*plot.LinearScale)(nil)),
"LogScale": reflect.ValueOf((*plot.LogScale)(nil)),
"LogTicks": reflect.ValueOf((*plot.LogTicks)(nil)),
"Normalizer": reflect.ValueOf((*plot.Normalizer)(nil)),
"Plot": reflect.ValueOf((*plot.Plot)(nil)),
"Plotter": reflect.ValueOf((*plot.Plotter)(nil)),
"Text": reflect.ValueOf((*plot.Text)(nil)),
"TextStyle": reflect.ValueOf((*plot.TextStyle)(nil)),
"Thumbnailer": reflect.ValueOf((*plot.Thumbnailer)(nil)),
"Tick": reflect.ValueOf((*plot.Tick)(nil)),
"Ticker": reflect.ValueOf((*plot.Ticker)(nil)),
"TickerFunc": reflect.ValueOf((*plot.TickerFunc)(nil)),
"TimeTicks": reflect.ValueOf((*plot.TimeTicks)(nil)),
"Valuer": reflect.ValueOf((*plot.Valuer)(nil)),
"Values": reflect.ValueOf((*plot.Values)(nil)),
"XValues": reflect.ValueOf((*plot.XValues)(nil)),
"XYValues": reflect.ValueOf((*plot.XYValues)(nil)),
"XYZ": reflect.ValueOf((*plot.XYZ)(nil)),
"XYZer": reflect.ValueOf((*plot.XYZer)(nil)),
"XYZs": reflect.ValueOf((*plot.XYZs)(nil)),
"XYer": reflect.ValueOf((*plot.XYer)(nil)),
"XYs": reflect.ValueOf((*plot.XYs)(nil)),
"YValues": reflect.ValueOf((*plot.YValues)(nil)),
// interface wrapper definitions
"_DataRanger": reflect.ValueOf((*_cogentcore_org_core_plot_DataRanger)(nil)),
"_Labeler": reflect.ValueOf((*_cogentcore_org_core_plot_Labeler)(nil)),
"_Normalizer": reflect.ValueOf((*_cogentcore_org_core_plot_Normalizer)(nil)),
"_Plotter": reflect.ValueOf((*_cogentcore_org_core_plot_Plotter)(nil)),
"_Thumbnailer": reflect.ValueOf((*_cogentcore_org_core_plot_Thumbnailer)(nil)),
"_Ticker": reflect.ValueOf((*_cogentcore_org_core_plot_Ticker)(nil)),
"_Valuer": reflect.ValueOf((*_cogentcore_org_core_plot_Valuer)(nil)),
"_XYZer": reflect.ValueOf((*_cogentcore_org_core_plot_XYZer)(nil)),
"_XYer": reflect.ValueOf((*_cogentcore_org_core_plot_XYer)(nil)),
}
}
// _cogentcore_org_core_plot_DataRanger is an interface wrapper for DataRanger type
type _cogentcore_org_core_plot_DataRanger struct {
IValue interface{}
WDataRange func(pt *plot.Plot) (xmin float32, xmax float32, ymin float32, ymax float32)
}
func (W _cogentcore_org_core_plot_DataRanger) DataRange(pt *plot.Plot) (xmin float32, xmax float32, ymin float32, ymax float32) {
return W.WDataRange(pt)
}
// _cogentcore_org_core_plot_Labeler is an interface wrapper for Labeler type
type _cogentcore_org_core_plot_Labeler struct {
IValue interface{}
WLabel func(i int) string
}
func (W _cogentcore_org_core_plot_Labeler) Label(i int) string { return W.WLabel(i) }
// _cogentcore_org_core_plot_Normalizer is an interface wrapper for Normalizer type
type _cogentcore_org_core_plot_Normalizer struct {
IValue interface{}
WNormalize func(min float32, max float32, x float32) float32
}
func (W _cogentcore_org_core_plot_Normalizer) Normalize(min float32, max float32, x float32) float32 {
return W.WNormalize(min, max, x)
}
// _cogentcore_org_core_plot_Plotter is an interface wrapper for Plotter type
type _cogentcore_org_core_plot_Plotter struct {
IValue interface{}
WPlot func(pt *plot.Plot)
WXYData func() (data plot.XYer, pixels plot.XYer)
}
func (W _cogentcore_org_core_plot_Plotter) Plot(pt *plot.Plot) { W.WPlot(pt) }
func (W _cogentcore_org_core_plot_Plotter) XYData() (data plot.XYer, pixels plot.XYer) {
return W.WXYData()
}
// _cogentcore_org_core_plot_Thumbnailer is an interface wrapper for Thumbnailer type
type _cogentcore_org_core_plot_Thumbnailer struct {
IValue interface{}
WThumbnail func(pt *plot.Plot)
}
func (W _cogentcore_org_core_plot_Thumbnailer) Thumbnail(pt *plot.Plot) { W.WThumbnail(pt) }
// _cogentcore_org_core_plot_Ticker is an interface wrapper for Ticker type
type _cogentcore_org_core_plot_Ticker struct {
IValue interface{}
WTicks func(min float32, max float32) []plot.Tick
}
func (W _cogentcore_org_core_plot_Ticker) Ticks(min float32, max float32) []plot.Tick {
return W.WTicks(min, max)
}
// _cogentcore_org_core_plot_Valuer is an interface wrapper for Valuer type
type _cogentcore_org_core_plot_Valuer struct {
IValue interface{}
WLen func() int
WValue func(i int) float32
}
func (W _cogentcore_org_core_plot_Valuer) Len() int { return W.WLen() }
func (W _cogentcore_org_core_plot_Valuer) Value(i int) float32 { return W.WValue(i) }
// _cogentcore_org_core_plot_XYZer is an interface wrapper for XYZer type
type _cogentcore_org_core_plot_XYZer struct {
IValue interface{}
WLen func() int
WXY func(i int) (float32, float32)
WXYZ func(i int) (float32, float32, float32)
}
func (W _cogentcore_org_core_plot_XYZer) Len() int { return W.WLen() }
func (W _cogentcore_org_core_plot_XYZer) XY(i int) (float32, float32) { return W.WXY(i) }
func (W _cogentcore_org_core_plot_XYZer) XYZ(i int) (float32, float32, float32) { return W.WXYZ(i) }
// _cogentcore_org_core_plot_XYer is an interface wrapper for XYer type
type _cogentcore_org_core_plot_XYer struct {
IValue interface{}
WLen func() int
WXY func(i int) (x float32, y float32)
}
func (W _cogentcore_org_core_plot_XYer) Len() int { return W.WLen() }
func (W _cogentcore_org_core_plot_XYer) XY(i int) (x float32, y float32) { return W.WXY(i) }
// Code generated by 'yaegi extract cogentcore.org/core/styles/abilities'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/styles/abilities"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/styles/abilities/abilities"] = map[string]reflect.Value{
// function, constant and variable definitions
"AbilitiesN": reflect.ValueOf(abilities.AbilitiesN),
"AbilitiesValues": reflect.ValueOf(abilities.AbilitiesValues),
"Activatable": reflect.ValueOf(abilities.Activatable),
"Checkable": reflect.ValueOf(abilities.Checkable),
"Clickable": reflect.ValueOf(abilities.Clickable),
"DoubleClickable": reflect.ValueOf(abilities.DoubleClickable),
"Draggable": reflect.ValueOf(abilities.Draggable),
"Droppable": reflect.ValueOf(abilities.Droppable),
"Focusable": reflect.ValueOf(abilities.Focusable),
"Hoverable": reflect.ValueOf(abilities.Hoverable),
"LongHoverable": reflect.ValueOf(abilities.LongHoverable),
"LongPressable": reflect.ValueOf(abilities.LongPressable),
"RepeatClickable": reflect.ValueOf(abilities.RepeatClickable),
"Scrollable": reflect.ValueOf(abilities.Scrollable),
"Selectable": reflect.ValueOf(abilities.Selectable),
"Slideable": reflect.ValueOf(abilities.Slideable),
"TripleClickable": reflect.ValueOf(abilities.TripleClickable),
// type definitions
"Abilities": reflect.ValueOf((*abilities.Abilities)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/styles/states'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/styles/states"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/styles/states/states"] = map[string]reflect.Value{
// function, constant and variable definitions
"Active": reflect.ValueOf(states.Active),
"Checked": reflect.ValueOf(states.Checked),
"Disabled": reflect.ValueOf(states.Disabled),
"DragHovered": reflect.ValueOf(states.DragHovered),
"Dragging": reflect.ValueOf(states.Dragging),
"Focused": reflect.ValueOf(states.Focused),
"Hovered": reflect.ValueOf(states.Hovered),
"Indeterminate": reflect.ValueOf(states.Indeterminate),
"Invisible": reflect.ValueOf(states.Invisible),
"LongHovered": reflect.ValueOf(states.LongHovered),
"LongPressed": reflect.ValueOf(states.LongPressed),
"ReadOnly": reflect.ValueOf(states.ReadOnly),
"Selected": reflect.ValueOf(states.Selected),
"Sliding": reflect.ValueOf(states.Sliding),
"StatesN": reflect.ValueOf(states.StatesN),
"StatesValues": reflect.ValueOf(states.StatesValues),
// type definitions
"States": reflect.ValueOf((*states.States)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/styles/units'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/styles/units"
"go/constant"
"go/token"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/styles/units/units"] = map[string]reflect.Value{
// function, constant and variable definitions
"Ch": reflect.ValueOf(units.Ch),
"Cm": reflect.ValueOf(units.Cm),
"CmPerInch": reflect.ValueOf(constant.MakeFromLiteral("2.539999999999999999965305530480463858111761510372161865234375", token.FLOAT, 0)),
"Custom": reflect.ValueOf(units.Custom),
"Dot": reflect.ValueOf(units.Dot),
"Dp": reflect.ValueOf(units.Dp),
"DpPerInch": reflect.ValueOf(constant.MakeFromLiteral("160", token.INT, 0)),
"Eh": reflect.ValueOf(units.Eh),
"Em": reflect.ValueOf(units.Em),
"Ew": reflect.ValueOf(units.Ew),
"Ex": reflect.ValueOf(units.Ex),
"In": reflect.ValueOf(units.In),
"Mm": reflect.ValueOf(units.Mm),
"MmPerInch": reflect.ValueOf(constant.MakeFromLiteral("25.39999999999999999965305530480463858111761510372161865234375", token.FLOAT, 0)),
"New": reflect.ValueOf(units.New),
"Pc": reflect.ValueOf(units.Pc),
"PcPerInch": reflect.ValueOf(constant.MakeFromLiteral("6", token.INT, 0)),
"Ph": reflect.ValueOf(units.Ph),
"Pt": reflect.ValueOf(units.Pt),
"PtPerInch": reflect.ValueOf(constant.MakeFromLiteral("72", token.INT, 0)),
"Pw": reflect.ValueOf(units.Pw),
"Px": reflect.ValueOf(units.Px),
"PxPerInch": reflect.ValueOf(constant.MakeFromLiteral("96", token.INT, 0)),
"Q": reflect.ValueOf(units.Q),
"Rem": reflect.ValueOf(units.Rem),
"StringToValue": reflect.ValueOf(units.StringToValue),
"UnitCh": reflect.ValueOf(units.UnitCh),
"UnitCm": reflect.ValueOf(units.UnitCm),
"UnitDot": reflect.ValueOf(units.UnitDot),
"UnitDp": reflect.ValueOf(units.UnitDp),
"UnitEh": reflect.ValueOf(units.UnitEh),
"UnitEm": reflect.ValueOf(units.UnitEm),
"UnitEw": reflect.ValueOf(units.UnitEw),
"UnitEx": reflect.ValueOf(units.UnitEx),
"UnitIn": reflect.ValueOf(units.UnitIn),
"UnitMm": reflect.ValueOf(units.UnitMm),
"UnitPc": reflect.ValueOf(units.UnitPc),
"UnitPh": reflect.ValueOf(units.UnitPh),
"UnitPt": reflect.ValueOf(units.UnitPt),
"UnitPw": reflect.ValueOf(units.UnitPw),
"UnitPx": reflect.ValueOf(units.UnitPx),
"UnitQ": reflect.ValueOf(units.UnitQ),
"UnitRem": reflect.ValueOf(units.UnitRem),
"UnitVh": reflect.ValueOf(units.UnitVh),
"UnitVmax": reflect.ValueOf(units.UnitVmax),
"UnitVmin": reflect.ValueOf(units.UnitVmin),
"UnitVw": reflect.ValueOf(units.UnitVw),
"UnitsN": reflect.ValueOf(units.UnitsN),
"UnitsValues": reflect.ValueOf(units.UnitsValues),
"Vh": reflect.ValueOf(units.Vh),
"Vmax": reflect.ValueOf(units.Vmax),
"Vmin": reflect.ValueOf(units.Vmin),
"Vw": reflect.ValueOf(units.Vw),
"Zero": reflect.ValueOf(units.Zero),
// type definitions
"Context": reflect.ValueOf((*units.Context)(nil)),
"Units": reflect.ValueOf((*units.Units)(nil)),
"Value": reflect.ValueOf((*units.Value)(nil)),
"XY": reflect.ValueOf((*units.XY)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/styles'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/styles"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/styles/styles"] = map[string]reflect.Value{
// function, constant and variable definitions
"AlignFactor": reflect.ValueOf(styles.AlignFactor),
"AlignPos": reflect.ValueOf(styles.AlignPos),
"AlignsN": reflect.ValueOf(styles.AlignsN),
"AlignsValues": reflect.ValueOf(styles.AlignsValues),
"AnchorEnd": reflect.ValueOf(styles.AnchorEnd),
"AnchorMiddle": reflect.ValueOf(styles.AnchorMiddle),
"AnchorStart": reflect.ValueOf(styles.AnchorStart),
"Auto": reflect.ValueOf(styles.Auto),
"Baseline": reflect.ValueOf(styles.Baseline),
"BaselineShiftsN": reflect.ValueOf(styles.BaselineShiftsN),
"BaselineShiftsValues": reflect.ValueOf(styles.BaselineShiftsValues),
"BidiBidiOverride": reflect.ValueOf(styles.BidiBidiOverride),
"BidiEmbed": reflect.ValueOf(styles.BidiEmbed),
"BidiNormal": reflect.ValueOf(styles.BidiNormal),
"BorderDashed": reflect.ValueOf(styles.BorderDashed),
"BorderDotted": reflect.ValueOf(styles.BorderDotted),
"BorderDouble": reflect.ValueOf(styles.BorderDouble),
"BorderGroove": reflect.ValueOf(styles.BorderGroove),
"BorderInset": reflect.ValueOf(styles.BorderInset),
"BorderNone": reflect.ValueOf(styles.BorderNone),
"BorderOutset": reflect.ValueOf(styles.BorderOutset),
"BorderRadiusExtraLarge": reflect.ValueOf(&styles.BorderRadiusExtraLarge).Elem(),
"BorderRadiusExtraLargeTop": reflect.ValueOf(&styles.BorderRadiusExtraLargeTop).Elem(),
"BorderRadiusExtraSmall": reflect.ValueOf(&styles.BorderRadiusExtraSmall).Elem(),
"BorderRadiusExtraSmallTop": reflect.ValueOf(&styles.BorderRadiusExtraSmallTop).Elem(),
"BorderRadiusFull": reflect.ValueOf(&styles.BorderRadiusFull).Elem(),
"BorderRadiusLarge": reflect.ValueOf(&styles.BorderRadiusLarge).Elem(),
"BorderRadiusLargeEnd": reflect.ValueOf(&styles.BorderRadiusLargeEnd).Elem(),
"BorderRadiusLargeTop": reflect.ValueOf(&styles.BorderRadiusLargeTop).Elem(),
"BorderRadiusMedium": reflect.ValueOf(&styles.BorderRadiusMedium).Elem(),
"BorderRadiusSmall": reflect.ValueOf(&styles.BorderRadiusSmall).Elem(),
"BorderRidge": reflect.ValueOf(styles.BorderRidge),
"BorderSolid": reflect.ValueOf(styles.BorderSolid),
"BorderStylesN": reflect.ValueOf(styles.BorderStylesN),
"BorderStylesValues": reflect.ValueOf(styles.BorderStylesValues),
"Bottom": reflect.ValueOf(styles.Bottom),
"BoxShadow0": reflect.ValueOf(styles.BoxShadow0),
"BoxShadow1": reflect.ValueOf(styles.BoxShadow1),
"BoxShadow2": reflect.ValueOf(styles.BoxShadow2),
"BoxShadow3": reflect.ValueOf(styles.BoxShadow3),
"BoxShadow4": reflect.ValueOf(styles.BoxShadow4),
"BoxShadow5": reflect.ValueOf(styles.BoxShadow5),
"BoxShadowMargin": reflect.ValueOf(styles.BoxShadowMargin),
"Center": reflect.ValueOf(styles.Center),
"ClampMax": reflect.ValueOf(styles.ClampMax),
"ClampMaxVector": reflect.ValueOf(styles.ClampMaxVector),
"ClampMin": reflect.ValueOf(styles.ClampMin),
"ClampMinVector": reflect.ValueOf(styles.ClampMinVector),
"Column": reflect.ValueOf(styles.Column),
"Custom": reflect.ValueOf(styles.Custom),
"DecoBackgroundColor": reflect.ValueOf(styles.DecoBackgroundColor),
"DecoBlink": reflect.ValueOf(styles.DecoBlink),
"DecoDottedUnderline": reflect.ValueOf(styles.DecoDottedUnderline),
"DecoNone": reflect.ValueOf(styles.DecoNone),
"DecoParaStart": reflect.ValueOf(styles.DecoParaStart),
"DecoSub": reflect.ValueOf(styles.DecoSub),
"DecoSuper": reflect.ValueOf(styles.DecoSuper),
"DefaultScrollbarWidth": reflect.ValueOf(&styles.DefaultScrollbarWidth).Elem(),
"DirectionsN": reflect.ValueOf(styles.DirectionsN),
"DirectionsValues": reflect.ValueOf(styles.DirectionsValues),
"DisplayNone": reflect.ValueOf(styles.DisplayNone),
"DisplaysN": reflect.ValueOf(styles.DisplaysN),
"DisplaysValues": reflect.ValueOf(styles.DisplaysValues),
"End": reflect.ValueOf(styles.End),
"FillRuleEvenOdd": reflect.ValueOf(styles.FillRuleEvenOdd),
"FillRuleNonZero": reflect.ValueOf(styles.FillRuleNonZero),
"FillRulesN": reflect.ValueOf(styles.FillRulesN),
"FillRulesValues": reflect.ValueOf(styles.FillRulesValues),
"FitContain": reflect.ValueOf(styles.FitContain),
"FitCover": reflect.ValueOf(styles.FitCover),
"FitFill": reflect.ValueOf(styles.FitFill),
"FitNone": reflect.ValueOf(styles.FitNone),
"FitScaleDown": reflect.ValueOf(styles.FitScaleDown),
"FixFontMods": reflect.ValueOf(styles.FixFontMods),
"Flex": reflect.ValueOf(styles.Flex),
"FontNameFromMods": reflect.ValueOf(styles.FontNameFromMods),
"FontNameToMods": reflect.ValueOf(styles.FontNameToMods),
"FontNormal": reflect.ValueOf(styles.FontNormal),
"FontSizePoints": reflect.ValueOf(&styles.FontSizePoints).Elem(),
"FontStrCondensed": reflect.ValueOf(styles.FontStrCondensed),
"FontStrExpanded": reflect.ValueOf(styles.FontStrExpanded),
"FontStrExtraCondensed": reflect.ValueOf(styles.FontStrExtraCondensed),
"FontStrExtraExpanded": reflect.ValueOf(styles.FontStrExtraExpanded),
"FontStrNarrower": reflect.ValueOf(styles.FontStrNarrower),
"FontStrNormal": reflect.ValueOf(styles.FontStrNormal),
"FontStrSemiCondensed": reflect.ValueOf(styles.FontStrSemiCondensed),
"FontStrSemiExpanded": reflect.ValueOf(styles.FontStrSemiExpanded),
"FontStrUltraCondensed": reflect.ValueOf(styles.FontStrUltraCondensed),
"FontStrUltraExpanded": reflect.ValueOf(styles.FontStrUltraExpanded),
"FontStrWider": reflect.ValueOf(styles.FontStrWider),
"FontStretchN": reflect.ValueOf(styles.FontStretchN),
"FontStretchNames": reflect.ValueOf(&styles.FontStretchNames).Elem(),
"FontStretchValues": reflect.ValueOf(styles.FontStretchValues),
"FontStyleNames": reflect.ValueOf(&styles.FontStyleNames).Elem(),
"FontStylesN": reflect.ValueOf(styles.FontStylesN),
"FontStylesValues": reflect.ValueOf(styles.FontStylesValues),
"FontVarNormal": reflect.ValueOf(styles.FontVarNormal),
"FontVarSmallCaps": reflect.ValueOf(styles.FontVarSmallCaps),
"FontVariantsN": reflect.ValueOf(styles.FontVariantsN),
"FontVariantsValues": reflect.ValueOf(styles.FontVariantsValues),
"FontWeightNameValues": reflect.ValueOf(&styles.FontWeightNameValues).Elem(),
"FontWeightNames": reflect.ValueOf(&styles.FontWeightNames).Elem(),
"FontWeightToNameMap": reflect.ValueOf(&styles.FontWeightToNameMap).Elem(),
"FontWeightsN": reflect.ValueOf(styles.FontWeightsN),
"FontWeightsValues": reflect.ValueOf(styles.FontWeightsValues),
"Grid": reflect.ValueOf(styles.Grid),
"Italic": reflect.ValueOf(styles.Italic),
"ItemAlign": reflect.ValueOf(styles.ItemAlign),
"KeyboardEmail": reflect.ValueOf(styles.KeyboardEmail),
"KeyboardMultiLine": reflect.ValueOf(styles.KeyboardMultiLine),
"KeyboardNone": reflect.ValueOf(styles.KeyboardNone),
"KeyboardNumber": reflect.ValueOf(styles.KeyboardNumber),
"KeyboardPassword": reflect.ValueOf(styles.KeyboardPassword),
"KeyboardPhone": reflect.ValueOf(styles.KeyboardPhone),
"KeyboardSingleLine": reflect.ValueOf(styles.KeyboardSingleLine),
"KeyboardURL": reflect.ValueOf(styles.KeyboardURL),
"LR": reflect.ValueOf(styles.LR),
"LRTB": reflect.ValueOf(styles.LRTB),
"LTR": reflect.ValueOf(styles.LTR),
"Left": reflect.ValueOf(styles.Left),
"LineCapButt": reflect.ValueOf(styles.LineCapButt),
"LineCapCubic": reflect.ValueOf(styles.LineCapCubic),
"LineCapQuadratic": reflect.ValueOf(styles.LineCapQuadratic),
"LineCapRound": reflect.ValueOf(styles.LineCapRound),
"LineCapSquare": reflect.ValueOf(styles.LineCapSquare),
"LineCapsN": reflect.ValueOf(styles.LineCapsN),
"LineCapsValues": reflect.ValueOf(styles.LineCapsValues),
"LineHeightNormal": reflect.ValueOf(&styles.LineHeightNormal).Elem(),
"LineJoinArcs": reflect.ValueOf(styles.LineJoinArcs),
"LineJoinArcsClip": reflect.ValueOf(styles.LineJoinArcsClip),
"LineJoinBevel": reflect.ValueOf(styles.LineJoinBevel),
"LineJoinMiter": reflect.ValueOf(styles.LineJoinMiter),
"LineJoinMiterClip": reflect.ValueOf(styles.LineJoinMiterClip),
"LineJoinRound": reflect.ValueOf(styles.LineJoinRound),
"LineJoinsN": reflect.ValueOf(styles.LineJoinsN),
"LineJoinsValues": reflect.ValueOf(styles.LineJoinsValues),
"LineThrough": reflect.ValueOf(styles.LineThrough),
"NewFontFace": reflect.ValueOf(styles.NewFontFace),
"NewSideColors": reflect.ValueOf(styles.NewSideColors),
"NewSideFloats": reflect.ValueOf(styles.NewSideFloats),
"NewSideValues": reflect.ValueOf(styles.NewSideValues),
"NewStyle": reflect.ValueOf(styles.NewStyle),
"ObjectFitsN": reflect.ValueOf(styles.ObjectFitsN),
"ObjectFitsValues": reflect.ValueOf(styles.ObjectFitsValues),
"ObjectSizeFromFit": reflect.ValueOf(styles.ObjectSizeFromFit),
"Oblique": reflect.ValueOf(styles.Oblique),
"OverflowAuto": reflect.ValueOf(styles.OverflowAuto),
"OverflowHidden": reflect.ValueOf(styles.OverflowHidden),
"OverflowScroll": reflect.ValueOf(styles.OverflowScroll),
"OverflowVisible": reflect.ValueOf(styles.OverflowVisible),
"OverflowsN": reflect.ValueOf(styles.OverflowsN),
"OverflowsValues": reflect.ValueOf(styles.OverflowsValues),
"Overline": reflect.ValueOf(styles.Overline),
"PrefFontFamily": reflect.ValueOf(&styles.PrefFontFamily).Elem(),
"RL": reflect.ValueOf(styles.RL),
"RLTB": reflect.ValueOf(styles.RLTB),
"RTL": reflect.ValueOf(styles.RTL),
"Right": reflect.ValueOf(styles.Right),
"Row": reflect.ValueOf(styles.Row),
"SetClampMax": reflect.ValueOf(styles.SetClampMax),
"SetClampMaxVector": reflect.ValueOf(styles.SetClampMaxVector),
"SetClampMin": reflect.ValueOf(styles.SetClampMin),
"SetClampMinVector": reflect.ValueOf(styles.SetClampMinVector),
"SetStylePropertiesXML": reflect.ValueOf(styles.SetStylePropertiesXML),
"SettingsFont": reflect.ValueOf(&styles.SettingsFont).Elem(),
"SettingsMonoFont": reflect.ValueOf(&styles.SettingsMonoFont).Elem(),
"ShiftBaseline": reflect.ValueOf(styles.ShiftBaseline),
"ShiftSub": reflect.ValueOf(styles.ShiftSub),
"ShiftSuper": reflect.ValueOf(styles.ShiftSuper),
"SideIndexesN": reflect.ValueOf(styles.SideIndexesN),
"SideIndexesValues": reflect.ValueOf(styles.SideIndexesValues),
"SpaceAround": reflect.ValueOf(styles.SpaceAround),
"SpaceBetween": reflect.ValueOf(styles.SpaceBetween),
"SpaceEvenly": reflect.ValueOf(styles.SpaceEvenly),
"Stacked": reflect.ValueOf(styles.Stacked),
"Start": reflect.ValueOf(styles.Start),
"StyleDefault": reflect.ValueOf(&styles.StyleDefault).Elem(),
"StylePropertiesXML": reflect.ValueOf(styles.StylePropertiesXML),
"SubProperties": reflect.ValueOf(styles.SubProperties),
"TB": reflect.ValueOf(styles.TB),
"TBRL": reflect.ValueOf(styles.TBRL),
"TextAnchorsN": reflect.ValueOf(styles.TextAnchorsN),
"TextAnchorsValues": reflect.ValueOf(styles.TextAnchorsValues),
"TextDecorationsN": reflect.ValueOf(styles.TextDecorationsN),
"TextDecorationsValues": reflect.ValueOf(styles.TextDecorationsValues),
"TextDirectionsN": reflect.ValueOf(styles.TextDirectionsN),
"TextDirectionsValues": reflect.ValueOf(styles.TextDirectionsValues),
"ToCSS": reflect.ValueOf(styles.ToCSS),
"Top": reflect.ValueOf(styles.Top),
"Underline": reflect.ValueOf(styles.Underline),
"UnicodeBidiN": reflect.ValueOf(styles.UnicodeBidiN),
"UnicodeBidiValues": reflect.ValueOf(styles.UnicodeBidiValues),
"VectorEffectNonScalingStroke": reflect.ValueOf(styles.VectorEffectNonScalingStroke),
"VectorEffectNone": reflect.ValueOf(styles.VectorEffectNone),
"VectorEffectsN": reflect.ValueOf(styles.VectorEffectsN),
"VectorEffectsValues": reflect.ValueOf(styles.VectorEffectsValues),
"VirtualKeyboardsN": reflect.ValueOf(styles.VirtualKeyboardsN),
"VirtualKeyboardsValues": reflect.ValueOf(styles.VirtualKeyboardsValues),
"Weight100": reflect.ValueOf(styles.Weight100),
"Weight200": reflect.ValueOf(styles.Weight200),
"Weight300": reflect.ValueOf(styles.Weight300),
"Weight400": reflect.ValueOf(styles.Weight400),
"Weight500": reflect.ValueOf(styles.Weight500),
"Weight600": reflect.ValueOf(styles.Weight600),
"Weight700": reflect.ValueOf(styles.Weight700),
"Weight800": reflect.ValueOf(styles.Weight800),
"Weight900": reflect.ValueOf(styles.Weight900),
"WeightBlack": reflect.ValueOf(styles.WeightBlack),
"WeightBold": reflect.ValueOf(styles.WeightBold),
"WeightBolder": reflect.ValueOf(styles.WeightBolder),
"WeightExtraBold": reflect.ValueOf(styles.WeightExtraBold),
"WeightExtraLight": reflect.ValueOf(styles.WeightExtraLight),
"WeightLight": reflect.ValueOf(styles.WeightLight),
"WeightLighter": reflect.ValueOf(styles.WeightLighter),
"WeightMedium": reflect.ValueOf(styles.WeightMedium),
"WeightNormal": reflect.ValueOf(styles.WeightNormal),
"WeightSemiBold": reflect.ValueOf(styles.WeightSemiBold),
"WeightThin": reflect.ValueOf(styles.WeightThin),
"WhiteSpaceNormal": reflect.ValueOf(styles.WhiteSpaceNormal),
"WhiteSpaceNowrap": reflect.ValueOf(styles.WhiteSpaceNowrap),
"WhiteSpacePre": reflect.ValueOf(styles.WhiteSpacePre),
"WhiteSpacePreLine": reflect.ValueOf(styles.WhiteSpacePreLine),
"WhiteSpacePreWrap": reflect.ValueOf(styles.WhiteSpacePreWrap),
"WhiteSpacesN": reflect.ValueOf(styles.WhiteSpacesN),
"WhiteSpacesValues": reflect.ValueOf(styles.WhiteSpacesValues),
// type definitions
"AlignSet": reflect.ValueOf((*styles.AlignSet)(nil)),
"Aligns": reflect.ValueOf((*styles.Aligns)(nil)),
"BaselineShifts": reflect.ValueOf((*styles.BaselineShifts)(nil)),
"Border": reflect.ValueOf((*styles.Border)(nil)),
"BorderStyles": reflect.ValueOf((*styles.BorderStyles)(nil)),
"Directions": reflect.ValueOf((*styles.Directions)(nil)),
"Displays": reflect.ValueOf((*styles.Displays)(nil)),
"Fill": reflect.ValueOf((*styles.Fill)(nil)),
"FillRules": reflect.ValueOf((*styles.FillRules)(nil)),
"Font": reflect.ValueOf((*styles.Font)(nil)),
"FontFace": reflect.ValueOf((*styles.FontFace)(nil)),
"FontMetrics": reflect.ValueOf((*styles.FontMetrics)(nil)),
"FontRender": reflect.ValueOf((*styles.FontRender)(nil)),
"FontStretch": reflect.ValueOf((*styles.FontStretch)(nil)),
"FontStyles": reflect.ValueOf((*styles.FontStyles)(nil)),
"FontVariants": reflect.ValueOf((*styles.FontVariants)(nil)),
"FontWeights": reflect.ValueOf((*styles.FontWeights)(nil)),
"LineCaps": reflect.ValueOf((*styles.LineCaps)(nil)),
"LineJoins": reflect.ValueOf((*styles.LineJoins)(nil)),
"ObjectFits": reflect.ValueOf((*styles.ObjectFits)(nil)),
"Overflows": reflect.ValueOf((*styles.Overflows)(nil)),
"Paint": reflect.ValueOf((*styles.Paint)(nil)),
"Shadow": reflect.ValueOf((*styles.Shadow)(nil)),
"SideColors": reflect.ValueOf((*styles.SideColors)(nil)),
"SideFloats": reflect.ValueOf((*styles.SideFloats)(nil)),
"SideIndexes": reflect.ValueOf((*styles.SideIndexes)(nil)),
"SideValues": reflect.ValueOf((*styles.SideValues)(nil)),
"Stroke": reflect.ValueOf((*styles.Stroke)(nil)),
"Style": reflect.ValueOf((*styles.Style)(nil)),
"Text": reflect.ValueOf((*styles.Text)(nil)),
"TextAnchors": reflect.ValueOf((*styles.TextAnchors)(nil)),
"TextDecorations": reflect.ValueOf((*styles.TextDecorations)(nil)),
"TextDirections": reflect.ValueOf((*styles.TextDirections)(nil)),
"UnicodeBidi": reflect.ValueOf((*styles.UnicodeBidi)(nil)),
"VectorEffects": reflect.ValueOf((*styles.VectorEffects)(nil)),
"VirtualKeyboards": reflect.ValueOf((*styles.VirtualKeyboards)(nil)),
"WhiteSpaces": reflect.ValueOf((*styles.WhiteSpaces)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/tensor/table'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/tensor/table"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/tensor/table/table"] = map[string]reflect.Value{
// function, constant and variable definitions
"AddAggName": reflect.ValueOf(table.AddAggName),
"Ascending": reflect.ValueOf(table.Ascending),
"ColumnNameOnly": reflect.ValueOf(table.ColumnNameOnly),
"Comma": reflect.ValueOf(table.Comma),
"ConfigFromDataValues": reflect.ValueOf(table.ConfigFromDataValues),
"ConfigFromHeaders": reflect.ValueOf(table.ConfigFromHeaders),
"ConfigFromTableHeaders": reflect.ValueOf(table.ConfigFromTableHeaders),
"Contains": reflect.ValueOf(table.Contains),
"DelimsN": reflect.ValueOf(table.DelimsN),
"DelimsValues": reflect.ValueOf(table.DelimsValues),
"Descending": reflect.ValueOf(table.Descending),
"Detect": reflect.ValueOf(table.Detect),
"DetectTableHeaders": reflect.ValueOf(table.DetectTableHeaders),
"Equals": reflect.ValueOf(table.Equals),
"Headers": reflect.ValueOf(table.Headers),
"IgnoreCase": reflect.ValueOf(table.IgnoreCase),
"InferDataType": reflect.ValueOf(table.InferDataType),
"NewIndexView": reflect.ValueOf(table.NewIndexView),
"NewSliceTable": reflect.ValueOf(table.NewSliceTable),
"NewTable": reflect.ValueOf(table.NewTable),
"NoHeaders": reflect.ValueOf(table.NoHeaders),
"ShapeFromString": reflect.ValueOf(table.ShapeFromString),
"Space": reflect.ValueOf(table.Space),
"Tab": reflect.ValueOf(table.Tab),
"TableColumnType": reflect.ValueOf(table.TableColumnType),
"TableHeaderChar": reflect.ValueOf(table.TableHeaderChar),
"TableHeaderToType": reflect.ValueOf(&table.TableHeaderToType).Elem(),
"UpdateSliceTable": reflect.ValueOf(table.UpdateSliceTable),
"UseCase": reflect.ValueOf(table.UseCase),
// type definitions
"Delims": reflect.ValueOf((*table.Delims)(nil)),
"Filterer": reflect.ValueOf((*table.Filterer)(nil)),
"IndexView": reflect.ValueOf((*table.IndexView)(nil)),
"LessFunc": reflect.ValueOf((*table.LessFunc)(nil)),
"SplitAgg": reflect.ValueOf((*table.SplitAgg)(nil)),
"Splits": reflect.ValueOf((*table.Splits)(nil)),
"SplitsLessFunc": reflect.ValueOf((*table.SplitsLessFunc)(nil)),
"Table": reflect.ValueOf((*table.Table)(nil)),
}
}
// Code generated by 'yaegi extract cogentcore.org/core/texteditor'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/texteditor"
"reflect"
)
func init() {
Symbols["cogentcore.org/core/texteditor/texteditor"] = map[string]reflect.Value{
// function, constant and variable definitions
"AsEditor": reflect.ValueOf(texteditor.AsEditor),
"DiffEditorDialog": reflect.ValueOf(texteditor.DiffEditorDialog),
"DiffEditorDialogFromRevs": reflect.ValueOf(texteditor.DiffEditorDialogFromRevs),
"DiffFiles": reflect.ValueOf(texteditor.DiffFiles),
"EditNoSignal": reflect.ValueOf(texteditor.EditNoSignal),
"EditSignal": reflect.ValueOf(texteditor.EditSignal),
"NewBuffer": reflect.ValueOf(texteditor.NewBuffer),
"NewDiffEditor": reflect.ValueOf(texteditor.NewDiffEditor),
"NewDiffTextEditor": reflect.ValueOf(texteditor.NewDiffTextEditor),
"NewEditor": reflect.ValueOf(texteditor.NewEditor),
"NewTwinEditors": reflect.ValueOf(texteditor.NewTwinEditors),
"PrevISearchString": reflect.ValueOf(&texteditor.PrevISearchString).Elem(),
"ReplaceMatchCase": reflect.ValueOf(texteditor.ReplaceMatchCase),
"ReplaceNoMatchCase": reflect.ValueOf(texteditor.ReplaceNoMatchCase),
"TextDialog": reflect.ValueOf(texteditor.TextDialog),
// type definitions
"Buffer": reflect.ValueOf((*texteditor.Buffer)(nil)),
"DiffEditor": reflect.ValueOf((*texteditor.DiffEditor)(nil)),
"DiffTextEditor": reflect.ValueOf((*texteditor.DiffTextEditor)(nil)),
"Editor": reflect.ValueOf((*texteditor.Editor)(nil)),
"EditorEmbedder": reflect.ValueOf((*texteditor.EditorEmbedder)(nil)),
"ISearch": reflect.ValueOf((*texteditor.ISearch)(nil)),
"OutputBuffer": reflect.ValueOf((*texteditor.OutputBuffer)(nil)),
"OutputBufferMarkupFunc": reflect.ValueOf((*texteditor.OutputBufferMarkupFunc)(nil)),
"QReplace": reflect.ValueOf((*texteditor.QReplace)(nil)),
"TwinEditors": reflect.ValueOf((*texteditor.TwinEditors)(nil)),
// interface wrapper definitions
"_EditorEmbedder": reflect.ValueOf((*_cogentcore_org_core_texteditor_EditorEmbedder)(nil)),
}
}
// _cogentcore_org_core_texteditor_EditorEmbedder is an interface wrapper for EditorEmbedder type
type _cogentcore_org_core_texteditor_EditorEmbedder struct {
IValue interface{}
WAsEditor func() *texteditor.Editor
}
func (W _cogentcore_org_core_texteditor_EditorEmbedder) AsEditor() *texteditor.Editor {
return W.WAsEditor()
}
// Code generated by 'yaegi extract cogentcore.org/core/tree'. DO NOT EDIT.
package symbols
import (
"cogentcore.org/core/tree"
"github.com/cogentcore/yaegi/interp"
"reflect"
)
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),
"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),
// type definitions
"Node": reflect.ValueOf((*tree.Node)(nil)),
"NodeBase": reflect.ValueOf((*tree.NodeBase)(nil)),
"NodeValue": reflect.ValueOf((*tree.NodeValue)(nil)),
"Plan": reflect.ValueOf((*tree.Plan)(nil)),
"PlanItem": reflect.ValueOf((*tree.PlanItem)(nil)),
"TypePlan": reflect.ValueOf((*tree.TypePlan)(nil)),
"TypePlanItem": reflect.ValueOf((*tree.TypePlanItem)(nil)),
// interface wrapper definitions
"_Node": reflect.ValueOf((*_cogentcore_org_core_tree_Node)(nil)),
"_NodeValue": reflect.ValueOf((*_cogentcore_org_core_tree_NodeValue)(nil)),
}
}
// _cogentcore_org_core_tree_Node is an interface wrapper for Node type
type _cogentcore_org_core_tree_Node struct {
IValue interface{}
WAsTree func() *tree.NodeBase
WCopyFieldsFrom func(from tree.Node)
WDestroy func()
WInit func()
WNodeWalkDown func(fun func(n tree.Node) bool)
WOnAdd func()
WPlanName func() string
}
func (W _cogentcore_org_core_tree_Node) AsTree() *tree.NodeBase { return W.WAsTree() }
func (W _cogentcore_org_core_tree_Node) CopyFieldsFrom(from tree.Node) { W.WCopyFieldsFrom(from) }
func (W _cogentcore_org_core_tree_Node) Destroy() { W.WDestroy() }
func (W _cogentcore_org_core_tree_Node) Init() { W.WInit() }
func (W _cogentcore_org_core_tree_Node) NodeWalkDown(fun func(n tree.Node) bool) {
W.WNodeWalkDown(fun)
}
func (W _cogentcore_org_core_tree_Node) OnAdd() { W.WOnAdd() }
func (W _cogentcore_org_core_tree_Node) PlanName() string { return W.WPlanName() }
// _cogentcore_org_core_tree_NodeValue is an interface wrapper for NodeValue type
type _cogentcore_org_core_tree_NodeValue struct {
IValue interface{}
WNodeValue func()
}
func (W _cogentcore_org_core_tree_NodeValue) NodeValue() { W.WNodeValue() }
// Code generated by 'yaegi extract fmt'. DO NOT EDIT.
//go:build go1.22
// +build go1.22
package symbols
import (
"fmt"
"reflect"
)
func init() {
Symbols["fmt/fmt"] = map[string]reflect.Value{
// function, constant and variable definitions
"Append": reflect.ValueOf(fmt.Append),
"Appendf": reflect.ValueOf(fmt.Appendf),
"Appendln": reflect.ValueOf(fmt.Appendln),
"Errorf": reflect.ValueOf(fmt.Errorf),
"FormatString": reflect.ValueOf(fmt.FormatString),
"Fprint": reflect.ValueOf(fmt.Fprint),
"Fprintf": reflect.ValueOf(fmt.Fprintf),
"Fprintln": reflect.ValueOf(fmt.Fprintln),
"Fscan": reflect.ValueOf(fmt.Fscan),
"Fscanf": reflect.ValueOf(fmt.Fscanf),
"Fscanln": reflect.ValueOf(fmt.Fscanln),
"Print": reflect.ValueOf(fmt.Print),
"Printf": reflect.ValueOf(fmt.Printf),
"Println": reflect.ValueOf(fmt.Println),
"Scan": reflect.ValueOf(fmt.Scan),
"Scanf": reflect.ValueOf(fmt.Scanf),
"Scanln": reflect.ValueOf(fmt.Scanln),
"Sprint": reflect.ValueOf(fmt.Sprint),
"Sprintf": reflect.ValueOf(fmt.Sprintf),
"Sprintln": reflect.ValueOf(fmt.Sprintln),
"Sscan": reflect.ValueOf(fmt.Sscan),
"Sscanf": reflect.ValueOf(fmt.Sscanf),
"Sscanln": reflect.ValueOf(fmt.Sscanln),
// type definitions
"Formatter": reflect.ValueOf((*fmt.Formatter)(nil)),
"GoStringer": reflect.ValueOf((*fmt.GoStringer)(nil)),
"ScanState": reflect.ValueOf((*fmt.ScanState)(nil)),
"Scanner": reflect.ValueOf((*fmt.Scanner)(nil)),
"State": reflect.ValueOf((*fmt.State)(nil)),
"Stringer": reflect.ValueOf((*fmt.Stringer)(nil)),
// interface wrapper definitions
"_Formatter": reflect.ValueOf((*_fmt_Formatter)(nil)),
"_GoStringer": reflect.ValueOf((*_fmt_GoStringer)(nil)),
"_ScanState": reflect.ValueOf((*_fmt_ScanState)(nil)),
"_Scanner": reflect.ValueOf((*_fmt_Scanner)(nil)),
"_State": reflect.ValueOf((*_fmt_State)(nil)),
"_Stringer": reflect.ValueOf((*_fmt_Stringer)(nil)),
}
}
// _fmt_Formatter is an interface wrapper for Formatter type
type _fmt_Formatter struct {
IValue interface{}
WFormat func(f fmt.State, verb rune)
}
func (W _fmt_Formatter) Format(f fmt.State, verb rune) { W.WFormat(f, verb) }
// _fmt_GoStringer is an interface wrapper for GoStringer type
type _fmt_GoStringer struct {
IValue interface{}
WGoString func() string
}
func (W _fmt_GoStringer) GoString() string { return W.WGoString() }
// _fmt_ScanState is an interface wrapper for ScanState type
type _fmt_ScanState struct {
IValue interface{}
WRead func(buf []byte) (n int, err error)
WReadRune func() (r rune, size int, err error)
WSkipSpace func()
WToken func(skipSpace bool, f func(rune) bool) (token []byte, err error)
WUnreadRune func() error
WWidth func() (wid int, ok bool)
}
func (W _fmt_ScanState) Read(buf []byte) (n int, err error) { return W.WRead(buf) }
func (W _fmt_ScanState) ReadRune() (r rune, size int, err error) { return W.WReadRune() }
func (W _fmt_ScanState) SkipSpace() { W.WSkipSpace() }
func (W _fmt_ScanState) Token(skipSpace bool, f func(rune) bool) (token []byte, err error) {
return W.WToken(skipSpace, f)
}
func (W _fmt_ScanState) UnreadRune() error { return W.WUnreadRune() }
func (W _fmt_ScanState) Width() (wid int, ok bool) { return W.WWidth() }
// _fmt_Scanner is an interface wrapper for Scanner type
type _fmt_Scanner struct {
IValue interface{}
WScan func(state fmt.ScanState, verb rune) error
}
func (W _fmt_Scanner) Scan(state fmt.ScanState, verb rune) error { return W.WScan(state, verb) }
// _fmt_State is an interface wrapper for State type
type _fmt_State struct {
IValue interface{}
WFlag func(c int) bool
WPrecision func() (prec int, ok bool)
WWidth func() (wid int, ok bool)
WWrite func(b []byte) (n int, err error)
}
func (W _fmt_State) Flag(c int) bool { return W.WFlag(c) }
func (W _fmt_State) Precision() (prec int, ok bool) { return W.WPrecision() }
func (W _fmt_State) Width() (wid int, ok bool) { return W.WWidth() }
func (W _fmt_State) Write(b []byte) (n int, err error) { return W.WWrite(b) }
// _fmt_Stringer is an interface wrapper for Stringer type
type _fmt_Stringer struct {
IValue interface{}
WString func() string
}
func (W _fmt_Stringer) String() string {
if W.WString == nil {
return ""
}
return W.WString()
}
// Code generated by 'yaegi extract image/color'. DO NOT EDIT.
//go:build go1.22
// +build go1.22
package symbols
import (
"image/color"
"reflect"
)
func init() {
Symbols["image/color/color"] = map[string]reflect.Value{
// function, constant and variable definitions
"Alpha16Model": reflect.ValueOf(&color.Alpha16Model).Elem(),
"AlphaModel": reflect.ValueOf(&color.AlphaModel).Elem(),
"Black": reflect.ValueOf(&color.Black).Elem(),
"CMYKModel": reflect.ValueOf(&color.CMYKModel).Elem(),
"CMYKToRGB": reflect.ValueOf(color.CMYKToRGB),
"Gray16Model": reflect.ValueOf(&color.Gray16Model).Elem(),
"GrayModel": reflect.ValueOf(&color.GrayModel).Elem(),
"ModelFunc": reflect.ValueOf(color.ModelFunc),
"NRGBA64Model": reflect.ValueOf(&color.NRGBA64Model).Elem(),
"NRGBAModel": reflect.ValueOf(&color.NRGBAModel).Elem(),
"NYCbCrAModel": reflect.ValueOf(&color.NYCbCrAModel).Elem(),
"Opaque": reflect.ValueOf(&color.Opaque).Elem(),
"RGBA64Model": reflect.ValueOf(&color.RGBA64Model).Elem(),
"RGBAModel": reflect.ValueOf(&color.RGBAModel).Elem(),
"RGBToCMYK": reflect.ValueOf(color.RGBToCMYK),
"RGBToYCbCr": reflect.ValueOf(color.RGBToYCbCr),
"Transparent": reflect.ValueOf(&color.Transparent).Elem(),
"White": reflect.ValueOf(&color.White).Elem(),
"YCbCrModel": reflect.ValueOf(&color.YCbCrModel).Elem(),
"YCbCrToRGB": reflect.ValueOf(color.YCbCrToRGB),
// type definitions
"Alpha": reflect.ValueOf((*color.Alpha)(nil)),
"Alpha16": reflect.ValueOf((*color.Alpha16)(nil)),
"CMYK": reflect.ValueOf((*color.CMYK)(nil)),
"Color": reflect.ValueOf((*color.Color)(nil)),
"Gray": reflect.ValueOf((*color.Gray)(nil)),
"Gray16": reflect.ValueOf((*color.Gray16)(nil)),
"Model": reflect.ValueOf((*color.Model)(nil)),
"NRGBA": reflect.ValueOf((*color.NRGBA)(nil)),
"NRGBA64": reflect.ValueOf((*color.NRGBA64)(nil)),
"NYCbCrA": reflect.ValueOf((*color.NYCbCrA)(nil)),
"Palette": reflect.ValueOf((*color.Palette)(nil)),
"RGBA": reflect.ValueOf((*color.RGBA)(nil)),
"RGBA64": reflect.ValueOf((*color.RGBA64)(nil)),
"YCbCr": reflect.ValueOf((*color.YCbCr)(nil)),
// interface wrapper definitions
"_Color": reflect.ValueOf((*_image_color_Color)(nil)),
"_Model": reflect.ValueOf((*_image_color_Model)(nil)),
}
}
// _image_color_Color is an interface wrapper for Color type
type _image_color_Color struct {
IValue interface{}
WRGBA func() (r uint32, g uint32, b uint32, a uint32)
}
func (W _image_color_Color) RGBA() (r uint32, g uint32, b uint32, a uint32) { return W.WRGBA() }
// _image_color_Model is an interface wrapper for Model type
type _image_color_Model struct {
IValue interface{}
WConvert func(c color.Color) color.Color
}
func (W _image_color_Model) Convert(c color.Color) color.Color { return W.WConvert(c) }
// Code generated by 'yaegi extract image/draw'. DO NOT EDIT.
//go:build go1.22
// +build go1.22
package symbols
import (
"image"
"image/color"
"image/draw"
"reflect"
)
func init() {
Symbols["image/draw/draw"] = map[string]reflect.Value{
// function, constant and variable definitions
"Draw": reflect.ValueOf(draw.Draw),
"DrawMask": reflect.ValueOf(draw.DrawMask),
"FloydSteinberg": reflect.ValueOf(&draw.FloydSteinberg).Elem(),
"Over": reflect.ValueOf(draw.Over),
"Src": reflect.ValueOf(draw.Src),
// type definitions
"Drawer": reflect.ValueOf((*draw.Drawer)(nil)),
"Image": reflect.ValueOf((*draw.Image)(nil)),
"Op": reflect.ValueOf((*draw.Op)(nil)),
"Quantizer": reflect.ValueOf((*draw.Quantizer)(nil)),
"RGBA64Image": reflect.ValueOf((*draw.RGBA64Image)(nil)),
// interface wrapper definitions
"_Drawer": reflect.ValueOf((*_image_draw_Drawer)(nil)),
"_Image": reflect.ValueOf((*_image_draw_Image)(nil)),
"_Quantizer": reflect.ValueOf((*_image_draw_Quantizer)(nil)),
"_RGBA64Image": reflect.ValueOf((*_image_draw_RGBA64Image)(nil)),
}
}
// _image_draw_Drawer is an interface wrapper for Drawer type
type _image_draw_Drawer struct {
IValue interface{}
WDraw func(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point)
}
func (W _image_draw_Drawer) Draw(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point) {
W.WDraw(dst, r, src, sp)
}
// _image_draw_Image is an interface wrapper for Image type
type _image_draw_Image struct {
IValue interface{}
WAt func(x int, y int) color.Color
WBounds func() image.Rectangle
WColorModel func() color.Model
WSet func(x int, y int, c color.Color)
}
func (W _image_draw_Image) At(x int, y int) color.Color { return W.WAt(x, y) }
func (W _image_draw_Image) Bounds() image.Rectangle { return W.WBounds() }
func (W _image_draw_Image) ColorModel() color.Model { return W.WColorModel() }
func (W _image_draw_Image) Set(x int, y int, c color.Color) { W.WSet(x, y, c) }
// _image_draw_Quantizer is an interface wrapper for Quantizer type
type _image_draw_Quantizer struct {
IValue interface{}
WQuantize func(p color.Palette, m image.Image) color.Palette
}
func (W _image_draw_Quantizer) Quantize(p color.Palette, m image.Image) color.Palette {
return W.WQuantize(p, m)
}
// _image_draw_RGBA64Image is an interface wrapper for RGBA64Image type
type _image_draw_RGBA64Image struct {
IValue interface{}
WAt func(x int, y int) color.Color
WBounds func() image.Rectangle
WColorModel func() color.Model
WRGBA64At func(x int, y int) color.RGBA64
WSet func(x int, y int, c color.Color)
WSetRGBA64 func(x int, y int, c color.RGBA64)
}
func (W _image_draw_RGBA64Image) At(x int, y int) color.Color { return W.WAt(x, y) }
func (W _image_draw_RGBA64Image) Bounds() image.Rectangle { return W.WBounds() }
func (W _image_draw_RGBA64Image) ColorModel() color.Model { return W.WColorModel() }
func (W _image_draw_RGBA64Image) RGBA64At(x int, y int) color.RGBA64 { return W.WRGBA64At(x, y) }
func (W _image_draw_RGBA64Image) Set(x int, y int, c color.Color) { W.WSet(x, y, c) }
func (W _image_draw_RGBA64Image) SetRGBA64(x int, y int, c color.RGBA64) { W.WSetRGBA64(x, y, c) }
// Code generated by 'yaegi extract image'. DO NOT EDIT.
//go:build go1.22
// +build go1.22
package symbols
import (
"image"
"image/color"
"reflect"
)
func init() {
Symbols["image/image"] = map[string]reflect.Value{
// function, constant and variable definitions
"Black": reflect.ValueOf(&image.Black).Elem(),
"Decode": reflect.ValueOf(image.Decode),
"DecodeConfig": reflect.ValueOf(image.DecodeConfig),
"ErrFormat": reflect.ValueOf(&image.ErrFormat).Elem(),
"NewAlpha": reflect.ValueOf(image.NewAlpha),
"NewAlpha16": reflect.ValueOf(image.NewAlpha16),
"NewCMYK": reflect.ValueOf(image.NewCMYK),
"NewGray": reflect.ValueOf(image.NewGray),
"NewGray16": reflect.ValueOf(image.NewGray16),
"NewNRGBA": reflect.ValueOf(image.NewNRGBA),
"NewNRGBA64": reflect.ValueOf(image.NewNRGBA64),
"NewNYCbCrA": reflect.ValueOf(image.NewNYCbCrA),
"NewPaletted": reflect.ValueOf(image.NewPaletted),
"NewRGBA": reflect.ValueOf(image.NewRGBA),
"NewRGBA64": reflect.ValueOf(image.NewRGBA64),
"NewUniform": reflect.ValueOf(image.NewUniform),
"NewYCbCr": reflect.ValueOf(image.NewYCbCr),
"Opaque": reflect.ValueOf(&image.Opaque).Elem(),
"Pt": reflect.ValueOf(image.Pt),
"Rect": reflect.ValueOf(image.Rect),
"RegisterFormat": reflect.ValueOf(image.RegisterFormat),
"Transparent": reflect.ValueOf(&image.Transparent).Elem(),
"White": reflect.ValueOf(&image.White).Elem(),
"YCbCrSubsampleRatio410": reflect.ValueOf(image.YCbCrSubsampleRatio410),
"YCbCrSubsampleRatio411": reflect.ValueOf(image.YCbCrSubsampleRatio411),
"YCbCrSubsampleRatio420": reflect.ValueOf(image.YCbCrSubsampleRatio420),
"YCbCrSubsampleRatio422": reflect.ValueOf(image.YCbCrSubsampleRatio422),
"YCbCrSubsampleRatio440": reflect.ValueOf(image.YCbCrSubsampleRatio440),
"YCbCrSubsampleRatio444": reflect.ValueOf(image.YCbCrSubsampleRatio444),
"ZP": reflect.ValueOf(&image.ZP).Elem(),
"ZR": reflect.ValueOf(&image.ZR).Elem(),
// type definitions
"Alpha": reflect.ValueOf((*image.Alpha)(nil)),
"Alpha16": reflect.ValueOf((*image.Alpha16)(nil)),
"CMYK": reflect.ValueOf((*image.CMYK)(nil)),
"Config": reflect.ValueOf((*image.Config)(nil)),
"Gray": reflect.ValueOf((*image.Gray)(nil)),
"Gray16": reflect.ValueOf((*image.Gray16)(nil)),
"Image": reflect.ValueOf((*image.Image)(nil)),
"NRGBA": reflect.ValueOf((*image.NRGBA)(nil)),
"NRGBA64": reflect.ValueOf((*image.NRGBA64)(nil)),
"NYCbCrA": reflect.ValueOf((*image.NYCbCrA)(nil)),
"Paletted": reflect.ValueOf((*image.Paletted)(nil)),
"PalettedImage": reflect.ValueOf((*image.PalettedImage)(nil)),
"Point": reflect.ValueOf((*image.Point)(nil)),
"RGBA": reflect.ValueOf((*image.RGBA)(nil)),
"RGBA64": reflect.ValueOf((*image.RGBA64)(nil)),
"RGBA64Image": reflect.ValueOf((*image.RGBA64Image)(nil)),
"Rectangle": reflect.ValueOf((*image.Rectangle)(nil)),
"Uniform": reflect.ValueOf((*image.Uniform)(nil)),
"YCbCr": reflect.ValueOf((*image.YCbCr)(nil)),
"YCbCrSubsampleRatio": reflect.ValueOf((*image.YCbCrSubsampleRatio)(nil)),
// interface wrapper definitions
"_Image": reflect.ValueOf((*_image_Image)(nil)),
"_PalettedImage": reflect.ValueOf((*_image_PalettedImage)(nil)),
"_RGBA64Image": reflect.ValueOf((*_image_RGBA64Image)(nil)),
}
}
// _image_Image is an interface wrapper for Image type
type _image_Image struct {
IValue interface{}
WAt func(x int, y int) color.Color
WBounds func() image.Rectangle
WColorModel func() color.Model
}
func (W _image_Image) At(x int, y int) color.Color { return W.WAt(x, y) }
func (W _image_Image) Bounds() image.Rectangle { return W.WBounds() }
func (W _image_Image) ColorModel() color.Model { return W.WColorModel() }
// _image_PalettedImage is an interface wrapper for PalettedImage type
type _image_PalettedImage struct {
IValue interface{}
WAt func(x int, y int) color.Color
WBounds func() image.Rectangle
WColorIndexAt func(x int, y int) uint8
WColorModel func() color.Model
}
func (W _image_PalettedImage) At(x int, y int) color.Color { return W.WAt(x, y) }
func (W _image_PalettedImage) Bounds() image.Rectangle { return W.WBounds() }
func (W _image_PalettedImage) ColorIndexAt(x int, y int) uint8 { return W.WColorIndexAt(x, y) }
func (W _image_PalettedImage) ColorModel() color.Model { return W.WColorModel() }
// _image_RGBA64Image is an interface wrapper for RGBA64Image type
type _image_RGBA64Image struct {
IValue interface{}
WAt func(x int, y int) color.Color
WBounds func() image.Rectangle
WColorModel func() color.Model
WRGBA64At func(x int, y int) color.RGBA64
}
func (W _image_RGBA64Image) At(x int, y int) color.Color { return W.WAt(x, y) }
func (W _image_RGBA64Image) Bounds() image.Rectangle { return W.WBounds() }
func (W _image_RGBA64Image) ColorModel() color.Model { return W.WColorModel() }
func (W _image_RGBA64Image) RGBA64At(x int, y int) color.RGBA64 { return W.WRGBA64At(x, y) }
// Code generated by 'yaegi extract log/slog'. DO NOT EDIT.
//go:build go1.22
// +build go1.22
package symbols
import (
"context"
"go/constant"
"go/token"
"log/slog"
"reflect"
)
func init() {
Symbols["log/slog/slog"] = map[string]reflect.Value{
// function, constant and variable definitions
"Any": reflect.ValueOf(slog.Any),
"AnyValue": reflect.ValueOf(slog.AnyValue),
"Bool": reflect.ValueOf(slog.Bool),
"BoolValue": reflect.ValueOf(slog.BoolValue),
"Debug": reflect.ValueOf(slog.Debug),
"DebugContext": reflect.ValueOf(slog.DebugContext),
"Default": reflect.ValueOf(slog.Default),
"Duration": reflect.ValueOf(slog.Duration),
"DurationValue": reflect.ValueOf(slog.DurationValue),
"Error": reflect.ValueOf(slog.Error),
"ErrorContext": reflect.ValueOf(slog.ErrorContext),
"Float64": reflect.ValueOf(slog.Float64),
"Float64Value": reflect.ValueOf(slog.Float64Value),
"Group": reflect.ValueOf(slog.Group),
"GroupValue": reflect.ValueOf(slog.GroupValue),
"Info": reflect.ValueOf(slog.Info),
"InfoContext": reflect.ValueOf(slog.InfoContext),
"Int": reflect.ValueOf(slog.Int),
"Int64": reflect.ValueOf(slog.Int64),
"Int64Value": reflect.ValueOf(slog.Int64Value),
"IntValue": reflect.ValueOf(slog.IntValue),
"KindAny": reflect.ValueOf(slog.KindAny),
"KindBool": reflect.ValueOf(slog.KindBool),
"KindDuration": reflect.ValueOf(slog.KindDuration),
"KindFloat64": reflect.ValueOf(slog.KindFloat64),
"KindGroup": reflect.ValueOf(slog.KindGroup),
"KindInt64": reflect.ValueOf(slog.KindInt64),
"KindLogValuer": reflect.ValueOf(slog.KindLogValuer),
"KindString": reflect.ValueOf(slog.KindString),
"KindTime": reflect.ValueOf(slog.KindTime),
"KindUint64": reflect.ValueOf(slog.KindUint64),
"LevelDebug": reflect.ValueOf(slog.LevelDebug),
"LevelError": reflect.ValueOf(slog.LevelError),
"LevelInfo": reflect.ValueOf(slog.LevelInfo),
"LevelKey": reflect.ValueOf(constant.MakeFromLiteral("\"level\"", token.STRING, 0)),
"LevelWarn": reflect.ValueOf(slog.LevelWarn),
"Log": reflect.ValueOf(slog.Log),
"LogAttrs": reflect.ValueOf(slog.LogAttrs),
"MessageKey": reflect.ValueOf(constant.MakeFromLiteral("\"msg\"", token.STRING, 0)),
"New": reflect.ValueOf(slog.New),
"NewJSONHandler": reflect.ValueOf(slog.NewJSONHandler),
"NewLogLogger": reflect.ValueOf(slog.NewLogLogger),
"NewRecord": reflect.ValueOf(slog.NewRecord),
"NewTextHandler": reflect.ValueOf(slog.NewTextHandler),
"SetDefault": reflect.ValueOf(slog.SetDefault),
"SetLogLoggerLevel": reflect.ValueOf(slog.SetLogLoggerLevel),
"SourceKey": reflect.ValueOf(constant.MakeFromLiteral("\"source\"", token.STRING, 0)),
"String": reflect.ValueOf(slog.String),
"StringValue": reflect.ValueOf(slog.StringValue),
"Time": reflect.ValueOf(slog.Time),
"TimeKey": reflect.ValueOf(constant.MakeFromLiteral("\"time\"", token.STRING, 0)),
"TimeValue": reflect.ValueOf(slog.TimeValue),
"Uint64": reflect.ValueOf(slog.Uint64),
"Uint64Value": reflect.ValueOf(slog.Uint64Value),
"Warn": reflect.ValueOf(slog.Warn),
"WarnContext": reflect.ValueOf(slog.WarnContext),
"With": reflect.ValueOf(slog.With),
// type definitions
"Attr": reflect.ValueOf((*slog.Attr)(nil)),
"Handler": reflect.ValueOf((*slog.Handler)(nil)),
"HandlerOptions": reflect.ValueOf((*slog.HandlerOptions)(nil)),
"JSONHandler": reflect.ValueOf((*slog.JSONHandler)(nil)),
"Kind": reflect.ValueOf((*slog.Kind)(nil)),
"Level": reflect.ValueOf((*slog.Level)(nil)),
"LevelVar": reflect.ValueOf((*slog.LevelVar)(nil)),
"Leveler": reflect.ValueOf((*slog.Leveler)(nil)),
"LogValuer": reflect.ValueOf((*slog.LogValuer)(nil)),
"Logger": reflect.ValueOf((*slog.Logger)(nil)),
"Record": reflect.ValueOf((*slog.Record)(nil)),
"Source": reflect.ValueOf((*slog.Source)(nil)),
"TextHandler": reflect.ValueOf((*slog.TextHandler)(nil)),
"Value": reflect.ValueOf((*slog.Value)(nil)),
// interface wrapper definitions
"_Handler": reflect.ValueOf((*_log_slog_Handler)(nil)),
"_Leveler": reflect.ValueOf((*_log_slog_Leveler)(nil)),
"_LogValuer": reflect.ValueOf((*_log_slog_LogValuer)(nil)),
}
}
// _log_slog_Handler is an interface wrapper for Handler type
type _log_slog_Handler struct {
IValue interface{}
WEnabled func(a0 context.Context, a1 slog.Level) bool
WHandle func(a0 context.Context, a1 slog.Record) error
WWithAttrs func(attrs []slog.Attr) slog.Handler
WWithGroup func(name string) slog.Handler
}
func (W _log_slog_Handler) Enabled(a0 context.Context, a1 slog.Level) bool { return W.WEnabled(a0, a1) }
func (W _log_slog_Handler) Handle(a0 context.Context, a1 slog.Record) error { return W.WHandle(a0, a1) }
func (W _log_slog_Handler) WithAttrs(attrs []slog.Attr) slog.Handler { return W.WWithAttrs(attrs) }
func (W _log_slog_Handler) WithGroup(name string) slog.Handler { return W.WWithGroup(name) }
// _log_slog_Leveler is an interface wrapper for Leveler type
type _log_slog_Leveler struct {
IValue interface{}
WLevel func() slog.Level
}
func (W _log_slog_Leveler) Level() slog.Level { return W.WLevel() }
// _log_slog_LogValuer is an interface wrapper for LogValuer type
type _log_slog_LogValuer struct {
IValue interface{}
WLogValue func() slog.Value
}
func (W _log_slog_LogValuer) LogValue() slog.Value { return W.WLogValue() }
// Code generated by 'yaegi extract reflect'. DO NOT EDIT.
//go:build go1.22
// +build go1.22
package symbols
import (
"reflect"
)
func init() {
Symbols["reflect/reflect"] = map[string]reflect.Value{
// function, constant and variable definitions
"Append": reflect.ValueOf(reflect.Append),
"AppendSlice": reflect.ValueOf(reflect.AppendSlice),
"Array": reflect.ValueOf(reflect.Array),
"ArrayOf": reflect.ValueOf(reflect.ArrayOf),
"Bool": reflect.ValueOf(reflect.Bool),
"BothDir": reflect.ValueOf(reflect.BothDir),
"Chan": reflect.ValueOf(reflect.Chan),
"ChanOf": reflect.ValueOf(reflect.ChanOf),
"Complex128": reflect.ValueOf(reflect.Complex128),
"Complex64": reflect.ValueOf(reflect.Complex64),
"Copy": reflect.ValueOf(reflect.Copy),
"DeepEqual": reflect.ValueOf(reflect.DeepEqual),
"Float32": reflect.ValueOf(reflect.Float32),
"Float64": reflect.ValueOf(reflect.Float64),
"Func": reflect.ValueOf(reflect.Func),
"FuncOf": reflect.ValueOf(reflect.FuncOf),
"Indirect": reflect.ValueOf(reflect.Indirect),
"Int": reflect.ValueOf(reflect.Int),
"Int16": reflect.ValueOf(reflect.Int16),
"Int32": reflect.ValueOf(reflect.Int32),
"Int64": reflect.ValueOf(reflect.Int64),
"Int8": reflect.ValueOf(reflect.Int8),
"Interface": reflect.ValueOf(reflect.Interface),
"Invalid": reflect.ValueOf(reflect.Invalid),
"MakeChan": reflect.ValueOf(reflect.MakeChan),
"MakeFunc": reflect.ValueOf(reflect.MakeFunc),
"MakeMap": reflect.ValueOf(reflect.MakeMap),
"MakeMapWithSize": reflect.ValueOf(reflect.MakeMapWithSize),
"MakeSlice": reflect.ValueOf(reflect.MakeSlice),
"Map": reflect.ValueOf(reflect.Map),
"MapOf": reflect.ValueOf(reflect.MapOf),
"New": reflect.ValueOf(reflect.New),
"NewAt": reflect.ValueOf(reflect.NewAt),
"Pointer": reflect.ValueOf(reflect.Pointer),
"PointerTo": reflect.ValueOf(reflect.PointerTo),
"Ptr": reflect.ValueOf(reflect.Ptr),
"PtrTo": reflect.ValueOf(reflect.PtrTo),
"RecvDir": reflect.ValueOf(reflect.RecvDir),
"Select": reflect.ValueOf(reflect.Select),
"SelectDefault": reflect.ValueOf(reflect.SelectDefault),
"SelectRecv": reflect.ValueOf(reflect.SelectRecv),
"SelectSend": reflect.ValueOf(reflect.SelectSend),
"SendDir": reflect.ValueOf(reflect.SendDir),
"Slice": reflect.ValueOf(reflect.Slice),
"SliceOf": reflect.ValueOf(reflect.SliceOf),
"String": reflect.ValueOf(reflect.String),
"Struct": reflect.ValueOf(reflect.Struct),
"StructOf": reflect.ValueOf(reflect.StructOf),
"Swapper": reflect.ValueOf(reflect.Swapper),
"TypeOf": reflect.ValueOf(reflect.TypeOf),
"Uint": reflect.ValueOf(reflect.Uint),
"Uint16": reflect.ValueOf(reflect.Uint16),
"Uint32": reflect.ValueOf(reflect.Uint32),
"Uint64": reflect.ValueOf(reflect.Uint64),
"Uint8": reflect.ValueOf(reflect.Uint8),
"Uintptr": reflect.ValueOf(reflect.Uintptr),
"UnsafePointer": reflect.ValueOf(reflect.UnsafePointer),
"ValueOf": reflect.ValueOf(reflect.ValueOf),
"VisibleFields": reflect.ValueOf(reflect.VisibleFields),
"Zero": reflect.ValueOf(reflect.Zero),
// type definitions
"ChanDir": reflect.ValueOf((*reflect.ChanDir)(nil)),
"Kind": reflect.ValueOf((*reflect.Kind)(nil)),
"MapIter": reflect.ValueOf((*reflect.MapIter)(nil)),
"Method": reflect.ValueOf((*reflect.Method)(nil)),
"SelectCase": reflect.ValueOf((*reflect.SelectCase)(nil)),
"SelectDir": reflect.ValueOf((*reflect.SelectDir)(nil)),
"SliceHeader": reflect.ValueOf((*reflect.SliceHeader)(nil)),
"StringHeader": reflect.ValueOf((*reflect.StringHeader)(nil)),
"StructField": reflect.ValueOf((*reflect.StructField)(nil)),
"StructTag": reflect.ValueOf((*reflect.StructTag)(nil)),
"Type": reflect.ValueOf((*reflect.Type)(nil)),
"Value": reflect.ValueOf((*reflect.Value)(nil)),
"ValueError": reflect.ValueOf((*reflect.ValueError)(nil)),
// interface wrapper definitions
"_Type": reflect.ValueOf((*_reflect_Type)(nil)),
}
}
// _reflect_Type is an interface wrapper for Type type
type _reflect_Type struct {
IValue interface{}
WAlign func() int
WAssignableTo func(u reflect.Type) bool
WBits func() int
WChanDir func() reflect.ChanDir
WComparable func() bool
WConvertibleTo func(u reflect.Type) bool
WElem func() reflect.Type
WField func(i int) reflect.StructField
WFieldAlign func() int
WFieldByIndex func(index []int) reflect.StructField
WFieldByName func(name string) (reflect.StructField, bool)
WFieldByNameFunc func(match func(string) bool) (reflect.StructField, bool)
WImplements func(u reflect.Type) bool
WIn func(i int) reflect.Type
WIsVariadic func() bool
WKey func() reflect.Type
WKind func() reflect.Kind
WLen func() int
WMethod func(a0 int) reflect.Method
WMethodByName func(a0 string) (reflect.Method, bool)
WName func() string
WNumField func() int
WNumIn func() int
WNumMethod func() int
WNumOut func() int
WOut func(i int) reflect.Type
WPkgPath func() string
WSize func() uintptr
WString func() string
}
func (W _reflect_Type) Align() int { return W.WAlign() }
func (W _reflect_Type) AssignableTo(u reflect.Type) bool { return W.WAssignableTo(u) }
func (W _reflect_Type) Bits() int { return W.WBits() }
func (W _reflect_Type) ChanDir() reflect.ChanDir { return W.WChanDir() }
func (W _reflect_Type) Comparable() bool { return W.WComparable() }
func (W _reflect_Type) ConvertibleTo(u reflect.Type) bool { return W.WConvertibleTo(u) }
func (W _reflect_Type) Elem() reflect.Type { return W.WElem() }
func (W _reflect_Type) Field(i int) reflect.StructField { return W.WField(i) }
func (W _reflect_Type) FieldAlign() int { return W.WFieldAlign() }
func (W _reflect_Type) FieldByIndex(index []int) reflect.StructField { return W.WFieldByIndex(index) }
func (W _reflect_Type) FieldByName(name string) (reflect.StructField, bool) {
return W.WFieldByName(name)
}
func (W _reflect_Type) FieldByNameFunc(match func(string) bool) (reflect.StructField, bool) {
return W.WFieldByNameFunc(match)
}
func (W _reflect_Type) Implements(u reflect.Type) bool { return W.WImplements(u) }
func (W _reflect_Type) In(i int) reflect.Type { return W.WIn(i) }
func (W _reflect_Type) IsVariadic() bool { return W.WIsVariadic() }
func (W _reflect_Type) Key() reflect.Type { return W.WKey() }
func (W _reflect_Type) Kind() reflect.Kind { return W.WKind() }
func (W _reflect_Type) Len() int { return W.WLen() }
func (W _reflect_Type) Method(a0 int) reflect.Method { return W.WMethod(a0) }
func (W _reflect_Type) MethodByName(a0 string) (reflect.Method, bool) { return W.WMethodByName(a0) }
func (W _reflect_Type) Name() string { return W.WName() }
func (W _reflect_Type) NumField() int { return W.WNumField() }
func (W _reflect_Type) NumIn() int { return W.WNumIn() }
func (W _reflect_Type) NumMethod() int { return W.WNumMethod() }
func (W _reflect_Type) NumOut() int { return W.WNumOut() }
func (W _reflect_Type) Out(i int) reflect.Type { return W.WOut(i) }
func (W _reflect_Type) PkgPath() string { return W.WPkgPath() }
func (W _reflect_Type) Size() uintptr { return W.WSize() }
func (W _reflect_Type) String() string {
if W.WString == nil {
return ""
}
return W.WString()
}
// Code generated by 'yaegi extract strconv'. DO NOT EDIT.
//go:build go1.22
// +build go1.22
package symbols
import (
"go/constant"
"go/token"
"reflect"
"strconv"
)
func init() {
Symbols["strconv/strconv"] = map[string]reflect.Value{
// function, constant and variable definitions
"AppendBool": reflect.ValueOf(strconv.AppendBool),
"AppendFloat": reflect.ValueOf(strconv.AppendFloat),
"AppendInt": reflect.ValueOf(strconv.AppendInt),
"AppendQuote": reflect.ValueOf(strconv.AppendQuote),
"AppendQuoteRune": reflect.ValueOf(strconv.AppendQuoteRune),
"AppendQuoteRuneToASCII": reflect.ValueOf(strconv.AppendQuoteRuneToASCII),
"AppendQuoteRuneToGraphic": reflect.ValueOf(strconv.AppendQuoteRuneToGraphic),
"AppendQuoteToASCII": reflect.ValueOf(strconv.AppendQuoteToASCII),
"AppendQuoteToGraphic": reflect.ValueOf(strconv.AppendQuoteToGraphic),
"AppendUint": reflect.ValueOf(strconv.AppendUint),
"Atoi": reflect.ValueOf(strconv.Atoi),
"CanBackquote": reflect.ValueOf(strconv.CanBackquote),
"ErrRange": reflect.ValueOf(&strconv.ErrRange).Elem(),
"ErrSyntax": reflect.ValueOf(&strconv.ErrSyntax).Elem(),
"FormatBool": reflect.ValueOf(strconv.FormatBool),
"FormatComplex": reflect.ValueOf(strconv.FormatComplex),
"FormatFloat": reflect.ValueOf(strconv.FormatFloat),
"FormatInt": reflect.ValueOf(strconv.FormatInt),
"FormatUint": reflect.ValueOf(strconv.FormatUint),
"IntSize": reflect.ValueOf(constant.MakeFromLiteral("64", token.INT, 0)),
"IsGraphic": reflect.ValueOf(strconv.IsGraphic),
"IsPrint": reflect.ValueOf(strconv.IsPrint),
"Itoa": reflect.ValueOf(strconv.Itoa),
"ParseBool": reflect.ValueOf(strconv.ParseBool),
"ParseComplex": reflect.ValueOf(strconv.ParseComplex),
"ParseFloat": reflect.ValueOf(strconv.ParseFloat),
"ParseInt": reflect.ValueOf(strconv.ParseInt),
"ParseUint": reflect.ValueOf(strconv.ParseUint),
"Quote": reflect.ValueOf(strconv.Quote),
"QuoteRune": reflect.ValueOf(strconv.QuoteRune),
"QuoteRuneToASCII": reflect.ValueOf(strconv.QuoteRuneToASCII),
"QuoteRuneToGraphic": reflect.ValueOf(strconv.QuoteRuneToGraphic),
"QuoteToASCII": reflect.ValueOf(strconv.QuoteToASCII),
"QuoteToGraphic": reflect.ValueOf(strconv.QuoteToGraphic),
"QuotedPrefix": reflect.ValueOf(strconv.QuotedPrefix),
"Unquote": reflect.ValueOf(strconv.Unquote),
"UnquoteChar": reflect.ValueOf(strconv.UnquoteChar),
// type definitions
"NumError": reflect.ValueOf((*strconv.NumError)(nil)),
}
}
// Code generated by 'yaegi extract strings'. DO NOT EDIT.
//go:build go1.22
// +build go1.22
package symbols
import (
"reflect"
"strings"
)
func init() {
Symbols["strings/strings"] = map[string]reflect.Value{
// function, constant and variable definitions
"Clone": reflect.ValueOf(strings.Clone),
"Compare": reflect.ValueOf(strings.Compare),
"Contains": reflect.ValueOf(strings.Contains),
"ContainsAny": reflect.ValueOf(strings.ContainsAny),
"ContainsFunc": reflect.ValueOf(strings.ContainsFunc),
"ContainsRune": reflect.ValueOf(strings.ContainsRune),
"Count": reflect.ValueOf(strings.Count),
"Cut": reflect.ValueOf(strings.Cut),
"CutPrefix": reflect.ValueOf(strings.CutPrefix),
"CutSuffix": reflect.ValueOf(strings.CutSuffix),
"EqualFold": reflect.ValueOf(strings.EqualFold),
"Fields": reflect.ValueOf(strings.Fields),
"FieldsFunc": reflect.ValueOf(strings.FieldsFunc),
"HasPrefix": reflect.ValueOf(strings.HasPrefix),
"HasSuffix": reflect.ValueOf(strings.HasSuffix),
"Index": reflect.ValueOf(strings.Index),
"IndexAny": reflect.ValueOf(strings.IndexAny),
"IndexByte": reflect.ValueOf(strings.IndexByte),
"IndexFunc": reflect.ValueOf(strings.IndexFunc),
"IndexRune": reflect.ValueOf(strings.IndexRune),
"Join": reflect.ValueOf(strings.Join),
"LastIndex": reflect.ValueOf(strings.LastIndex),
"LastIndexAny": reflect.ValueOf(strings.LastIndexAny),
"LastIndexByte": reflect.ValueOf(strings.LastIndexByte),
"LastIndexFunc": reflect.ValueOf(strings.LastIndexFunc),
"Map": reflect.ValueOf(strings.Map),
"NewReader": reflect.ValueOf(strings.NewReader),
"NewReplacer": reflect.ValueOf(strings.NewReplacer),
"Repeat": reflect.ValueOf(strings.Repeat),
"Replace": reflect.ValueOf(strings.Replace),
"ReplaceAll": reflect.ValueOf(strings.ReplaceAll),
"Split": reflect.ValueOf(strings.Split),
"SplitAfter": reflect.ValueOf(strings.SplitAfter),
"SplitAfterN": reflect.ValueOf(strings.SplitAfterN),
"SplitN": reflect.ValueOf(strings.SplitN),
"Title": reflect.ValueOf(strings.Title),
"ToLower": reflect.ValueOf(strings.ToLower),
"ToLowerSpecial": reflect.ValueOf(strings.ToLowerSpecial),
"ToTitle": reflect.ValueOf(strings.ToTitle),
"ToTitleSpecial": reflect.ValueOf(strings.ToTitleSpecial),
"ToUpper": reflect.ValueOf(strings.ToUpper),
"ToUpperSpecial": reflect.ValueOf(strings.ToUpperSpecial),
"ToValidUTF8": reflect.ValueOf(strings.ToValidUTF8),
"Trim": reflect.ValueOf(strings.Trim),
"TrimFunc": reflect.ValueOf(strings.TrimFunc),
"TrimLeft": reflect.ValueOf(strings.TrimLeft),
"TrimLeftFunc": reflect.ValueOf(strings.TrimLeftFunc),
"TrimPrefix": reflect.ValueOf(strings.TrimPrefix),
"TrimRight": reflect.ValueOf(strings.TrimRight),
"TrimRightFunc": reflect.ValueOf(strings.TrimRightFunc),
"TrimSpace": reflect.ValueOf(strings.TrimSpace),
"TrimSuffix": reflect.ValueOf(strings.TrimSuffix),
// type definitions
"Builder": reflect.ValueOf((*strings.Builder)(nil)),
"Reader": reflect.ValueOf((*strings.Reader)(nil)),
"Replacer": reflect.ValueOf((*strings.Replacer)(nil)),
}
}
// Code generated by 'yaegi extract time'. DO NOT EDIT.
//go:build go1.22
// +build go1.22
package symbols
import (
"go/constant"
"go/token"
"reflect"
"time"
)
func init() {
Symbols["time/time"] = map[string]reflect.Value{
// function, constant and variable definitions
"ANSIC": reflect.ValueOf(constant.MakeFromLiteral("\"Mon Jan _2 15:04:05 2006\"", token.STRING, 0)),
"After": reflect.ValueOf(time.After),
"AfterFunc": reflect.ValueOf(time.AfterFunc),
"April": reflect.ValueOf(time.April),
"August": reflect.ValueOf(time.August),
"Date": reflect.ValueOf(time.Date),
"DateOnly": reflect.ValueOf(constant.MakeFromLiteral("\"2006-01-02\"", token.STRING, 0)),
"DateTime": reflect.ValueOf(constant.MakeFromLiteral("\"2006-01-02 15:04:05\"", token.STRING, 0)),
"December": reflect.ValueOf(time.December),
"February": reflect.ValueOf(time.February),
"FixedZone": reflect.ValueOf(time.FixedZone),
"Friday": reflect.ValueOf(time.Friday),
"Hour": reflect.ValueOf(time.Hour),
"January": reflect.ValueOf(time.January),
"July": reflect.ValueOf(time.July),
"June": reflect.ValueOf(time.June),
"Kitchen": reflect.ValueOf(constant.MakeFromLiteral("\"3:04PM\"", token.STRING, 0)),
"Layout": reflect.ValueOf(constant.MakeFromLiteral("\"01/02 03:04:05PM '06 -0700\"", token.STRING, 0)),
"LoadLocation": reflect.ValueOf(time.LoadLocation),
"LoadLocationFromTZData": reflect.ValueOf(time.LoadLocationFromTZData),
"Local": reflect.ValueOf(&time.Local).Elem(),
"March": reflect.ValueOf(time.March),
"May": reflect.ValueOf(time.May),
"Microsecond": reflect.ValueOf(time.Microsecond),
"Millisecond": reflect.ValueOf(time.Millisecond),
"Minute": reflect.ValueOf(time.Minute),
"Monday": reflect.ValueOf(time.Monday),
"Nanosecond": reflect.ValueOf(time.Nanosecond),
"NewTicker": reflect.ValueOf(time.NewTicker),
"NewTimer": reflect.ValueOf(time.NewTimer),
"November": reflect.ValueOf(time.November),
"Now": reflect.ValueOf(time.Now),
"October": reflect.ValueOf(time.October),
"Parse": reflect.ValueOf(time.Parse),
"ParseDuration": reflect.ValueOf(time.ParseDuration),
"ParseInLocation": reflect.ValueOf(time.ParseInLocation),
"RFC1123": reflect.ValueOf(constant.MakeFromLiteral("\"Mon, 02 Jan 2006 15:04:05 MST\"", token.STRING, 0)),
"RFC1123Z": reflect.ValueOf(constant.MakeFromLiteral("\"Mon, 02 Jan 2006 15:04:05 -0700\"", token.STRING, 0)),
"RFC3339": reflect.ValueOf(constant.MakeFromLiteral("\"2006-01-02T15:04:05Z07:00\"", token.STRING, 0)),
"RFC3339Nano": reflect.ValueOf(constant.MakeFromLiteral("\"2006-01-02T15:04:05.999999999Z07:00\"", token.STRING, 0)),
"RFC822": reflect.ValueOf(constant.MakeFromLiteral("\"02 Jan 06 15:04 MST\"", token.STRING, 0)),
"RFC822Z": reflect.ValueOf(constant.MakeFromLiteral("\"02 Jan 06 15:04 -0700\"", token.STRING, 0)),
"RFC850": reflect.ValueOf(constant.MakeFromLiteral("\"Monday, 02-Jan-06 15:04:05 MST\"", token.STRING, 0)),
"RubyDate": reflect.ValueOf(constant.MakeFromLiteral("\"Mon Jan 02 15:04:05 -0700 2006\"", token.STRING, 0)),
"Saturday": reflect.ValueOf(time.Saturday),
"Second": reflect.ValueOf(time.Second),
"September": reflect.ValueOf(time.September),
"Since": reflect.ValueOf(time.Since),
"Sleep": reflect.ValueOf(time.Sleep),
"Stamp": reflect.ValueOf(constant.MakeFromLiteral("\"Jan _2 15:04:05\"", token.STRING, 0)),
"StampMicro": reflect.ValueOf(constant.MakeFromLiteral("\"Jan _2 15:04:05.000000\"", token.STRING, 0)),
"StampMilli": reflect.ValueOf(constant.MakeFromLiteral("\"Jan _2 15:04:05.000\"", token.STRING, 0)),
"StampNano": reflect.ValueOf(constant.MakeFromLiteral("\"Jan _2 15:04:05.000000000\"", token.STRING, 0)),
"Sunday": reflect.ValueOf(time.Sunday),
"Thursday": reflect.ValueOf(time.Thursday),
"Tick": reflect.ValueOf(time.Tick),
"TimeOnly": reflect.ValueOf(constant.MakeFromLiteral("\"15:04:05\"", token.STRING, 0)),
"Tuesday": reflect.ValueOf(time.Tuesday),
"UTC": reflect.ValueOf(&time.UTC).Elem(),
"Unix": reflect.ValueOf(time.Unix),
"UnixDate": reflect.ValueOf(constant.MakeFromLiteral("\"Mon Jan _2 15:04:05 MST 2006\"", token.STRING, 0)),
"UnixMicro": reflect.ValueOf(time.UnixMicro),
"UnixMilli": reflect.ValueOf(time.UnixMilli),
"Until": reflect.ValueOf(time.Until),
"Wednesday": reflect.ValueOf(time.Wednesday),
// type definitions
"Duration": reflect.ValueOf((*time.Duration)(nil)),
"Location": reflect.ValueOf((*time.Location)(nil)),
"Month": reflect.ValueOf((*time.Month)(nil)),
"ParseError": reflect.ValueOf((*time.ParseError)(nil)),
"Ticker": reflect.ValueOf((*time.Ticker)(nil)),
"Time": reflect.ValueOf((*time.Time)(nil)),
"Timer": reflect.ValueOf((*time.Timer)(nil)),
"Weekday": reflect.ValueOf((*time.Weekday)(nil)),
}
}
// Copyright (c) 2024, 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 yaegicore provides functions connecting
// https://github.com/cogentcore/yaegi to Cogent Core.
package yaegicore
import (
"fmt"
"reflect"
"strings"
"sync/atomic"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/htmlcore"
"cogentcore.org/core/texteditor"
"cogentcore.org/core/yaegicore/symbols"
"github.com/cogentcore/yaegi/interp"
)
var autoPlanNameCounter uint64
func init() {
htmlcore.BindTextEditor = BindTextEditor
symbols.Symbols["."] = map[string]reflect.Value{} // make "." available for use
}
// BindTextEditor binds the given text editor to a yaegi interpreter
// such that the contents of the text editor are interpreted as Go
// code, which is run in the context of the given parent widget.
// It is used as the default value of [htmlcore.BindTextEditor].
func BindTextEditor(ed *texteditor.Editor, parent core.Widget) {
oc := func() {
in := interp.New(interp.Options{})
core.ExternalParent = parent
symbols.Symbols["."]["b"] = reflect.ValueOf(parent)
// the normal AutoPlanName cannot be used because the stack trace in yaegi is not helpful
symbols.Symbols["cogentcore.org/core/tree/tree"]["AutoPlanName"] = reflect.ValueOf(func(int) string {
return fmt.Sprintf("yaegi-%v", atomic.AddUint64(&autoPlanNameCounter, 1))
})
errors.Log(in.Use(symbols.Symbols))
in.ImportUsed()
parent.AsTree().DeleteChildren()
str := ed.Buffer.String()
// all code must be in a function for declarations to be handled correctly
if !strings.Contains(str, "func main()") {
str = "func main() {\n" + str + "\n}"
}
_, err := in.Eval(str)
if err != nil {
core.ErrorSnackbar(ed, err, "Error interpreting Go code")
return
}
parent.AsWidget().Update()
}
ed.OnChange(func(e events.Event) { oc() })
oc()
}